@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
|
@@ -2,21 +2,32 @@ 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 {
|
|
6
|
-
getCollectionsDirectoryPath,
|
|
7
|
-
getNewCollectionPath,
|
|
8
|
-
} from "../../lib/collection-paths.js";
|
|
9
|
-
import { toPublicPath } from "../../lib/url.js";
|
|
10
5
|
import { CollectionDirectory } from "./CollectionDirectory.js";
|
|
11
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getCollectionDialogLabels,
|
|
8
|
+
getCollectionMutationLabels,
|
|
9
|
+
} from "./collection-management-labels.js";
|
|
10
|
+
import {
|
|
11
|
+
getSmartCollectionDialogLabels,
|
|
12
|
+
getSmartCollectionLabels,
|
|
13
|
+
} from "./smart-collection-labels.js";
|
|
14
|
+
import { getIconSvg } from "../../lib/icons.js";
|
|
12
15
|
|
|
13
16
|
const escapeJson = (data: unknown) =>
|
|
14
17
|
JSON.stringify(data).replace(/</g, "\\u003c");
|
|
15
18
|
|
|
16
19
|
export interface CollectionsManagerProps {
|
|
17
20
|
items: CollectionDirectoryItem[];
|
|
21
|
+
/** Collections and smart collections already in the site navigation. */
|
|
18
22
|
navigationCollectionIds?: string[];
|
|
19
23
|
sitePathPrefix?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Site path prefix plus the language prefix of the view being rendered.
|
|
26
|
+
* Collection pages exist once per language, so a reader on `/en/collections`
|
|
27
|
+
* must land on `/en/{slug}` rather than being dropped into the primary view.
|
|
28
|
+
* Required for the same reason as on {@link CollectionDirectoryProps}.
|
|
29
|
+
*/
|
|
30
|
+
basePath: string;
|
|
20
31
|
siteOrigin?: string;
|
|
21
32
|
}
|
|
22
33
|
|
|
@@ -24,19 +35,13 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
24
35
|
items,
|
|
25
36
|
navigationCollectionIds = [],
|
|
26
37
|
sitePathPrefix = "",
|
|
38
|
+
basePath,
|
|
27
39
|
siteOrigin = "",
|
|
28
40
|
}) => {
|
|
29
41
|
const { i18n } = useLingui();
|
|
30
|
-
const collectionsHref = toPublicPath(
|
|
31
|
-
getCollectionsDirectoryPath(),
|
|
32
|
-
sitePathPrefix,
|
|
33
|
-
);
|
|
34
|
-
const newCollectionHref = toPublicPath(
|
|
35
|
-
`${getNewCollectionPath()}?returnTo=${encodeURIComponent(collectionsHref)}`,
|
|
36
|
-
sitePathPrefix,
|
|
37
|
-
);
|
|
38
42
|
const mutationLabels = getCollectionMutationLabels(i18n);
|
|
39
43
|
|
|
44
|
+
const smartLabels = getSmartCollectionLabels(i18n);
|
|
40
45
|
const labels = {
|
|
41
46
|
collectionsTitle: i18n._(
|
|
42
47
|
msg({
|
|
@@ -44,6 +49,17 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
44
49
|
comment: "@context: Collections page heading",
|
|
45
50
|
}),
|
|
46
51
|
),
|
|
52
|
+
smartCollectionNoun: smartLabels.noun,
|
|
53
|
+
newSmartCollection: smartLabels.newSmartCollection,
|
|
54
|
+
editSmartCollection: smartLabels.editSmartCollection,
|
|
55
|
+
deleteSmartCollection: smartLabels.deleteSmartCollection,
|
|
56
|
+
confirmDeleteSmartCollection: smartLabels.confirmDelete,
|
|
57
|
+
smartCollectionDeleted: i18n._(
|
|
58
|
+
msg({
|
|
59
|
+
message: "Smart collection deleted.",
|
|
60
|
+
comment: "@context: Confirmation after deleting a smart collection",
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
47
63
|
organize: i18n._(
|
|
48
64
|
msg({
|
|
49
65
|
message: "Organize",
|
|
@@ -169,11 +185,12 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
169
185
|
class="collections-page-action-group"
|
|
170
186
|
data-collections-toolbar
|
|
171
187
|
>
|
|
172
|
-
<
|
|
173
|
-
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
174
190
|
class="collections-page-toolbar-button"
|
|
175
191
|
aria-label={labels.newCollection}
|
|
176
192
|
title={labels.newCollection}
|
|
193
|
+
data-collections-action="collection"
|
|
177
194
|
>
|
|
178
195
|
<svg
|
|
179
196
|
xmlns="http://www.w3.org/2000/svg"
|
|
@@ -190,7 +207,7 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
190
207
|
<path d="M12 5v14" />
|
|
191
208
|
<path d="M5 12h14" />
|
|
192
209
|
</svg>
|
|
193
|
-
</
|
|
210
|
+
</button>
|
|
194
211
|
<div class="relative">
|
|
195
212
|
<button
|
|
196
213
|
type="button"
|
|
@@ -250,6 +267,29 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
250
267
|
{labels.organize}
|
|
251
268
|
</span>
|
|
252
269
|
</button>
|
|
270
|
+
{/* Beside Add link and Add divider, not a second `+`:
|
|
271
|
+
the `+` is where a collection is created, and two plus
|
|
272
|
+
signs would immediately raise "which plus". */}
|
|
273
|
+
<button
|
|
274
|
+
type="button"
|
|
275
|
+
class="collections-page-menu-item"
|
|
276
|
+
data-collections-action="smart-collection"
|
|
277
|
+
>
|
|
278
|
+
{/* Full stroke, unlike the funnel that marks a smart
|
|
279
|
+
collection beside its title: here it sits among
|
|
280
|
+
menu icons drawn at the same weight, and `icon-fine`
|
|
281
|
+
would single it out as the lighter one. */}
|
|
282
|
+
<span
|
|
283
|
+
class="collections-page-menu-item-icon"
|
|
284
|
+
aria-hidden="true"
|
|
285
|
+
dangerouslySetInnerHTML={{
|
|
286
|
+
__html: getIconSvg("funnel") ?? "",
|
|
287
|
+
}}
|
|
288
|
+
/>
|
|
289
|
+
<span class="collections-page-menu-item-label">
|
|
290
|
+
{smartLabels.newSmartCollection}
|
|
291
|
+
</span>
|
|
292
|
+
</button>
|
|
253
293
|
<button
|
|
254
294
|
type="button"
|
|
255
295
|
class="collections-page-menu-item"
|
|
@@ -318,6 +358,18 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
318
358
|
</div>
|
|
319
359
|
</header>
|
|
320
360
|
|
|
361
|
+
{/* The dialog is a Lit component and cannot reach the i18n catalogs, so
|
|
362
|
+
the page that can open it carries its strings. */}
|
|
363
|
+
<div
|
|
364
|
+
hidden
|
|
365
|
+
data-collection-dialog-labels={escapeJson(
|
|
366
|
+
getCollectionDialogLabels(i18n),
|
|
367
|
+
)}
|
|
368
|
+
data-smart-collection-dialog-labels={escapeJson(
|
|
369
|
+
getSmartCollectionDialogLabels(i18n),
|
|
370
|
+
)}
|
|
371
|
+
/>
|
|
372
|
+
|
|
321
373
|
<jant-collections-manager
|
|
322
374
|
items={escapeJson(items)}
|
|
323
375
|
labels={escapeJson(labels)}
|
|
@@ -327,6 +379,7 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
327
379
|
items={items}
|
|
328
380
|
emptyMessage={labels.emptyState}
|
|
329
381
|
sitePathPrefix={sitePathPrefix}
|
|
382
|
+
basePath={basePath}
|
|
330
383
|
siteOrigin={siteOrigin}
|
|
331
384
|
/>
|
|
332
385
|
</jant-collections-manager>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { FC } from "hono/jsx";
|
|
2
|
+
import { getIconSvg } from "../../lib/icons.js";
|
|
3
|
+
import {
|
|
4
|
+
COPY_FIELD_BUTTON_ATTR,
|
|
5
|
+
COPY_FIELD_BUTTON_CLASS,
|
|
6
|
+
COPY_FIELD_CLASS,
|
|
7
|
+
COPY_FIELD_CONTROL_CLASS,
|
|
8
|
+
COPY_FIELD_FAILED_ATTR,
|
|
9
|
+
COPY_FIELD_INPUT_CLASS,
|
|
10
|
+
COPY_FIELD_VALUE_ATTR,
|
|
11
|
+
} from "../../lib/copy-field.js";
|
|
12
|
+
|
|
13
|
+
export interface CopyFieldProps {
|
|
14
|
+
/** Field label, also the input's accessible name. */
|
|
15
|
+
label: string;
|
|
16
|
+
/**
|
|
17
|
+
* Optional page this address is the feed of, linked from the label.
|
|
18
|
+
*
|
|
19
|
+
* A feed lives at `{page}/feed`, so every address here has a page whose
|
|
20
|
+
* contents it carries. Linking the label lets a reader look at those posts
|
|
21
|
+
* before subscribing — the page is the only honest preview of the feed.
|
|
22
|
+
* Omitted (settings, where the author already knows the site) the label
|
|
23
|
+
* stays plain text.
|
|
24
|
+
*/
|
|
25
|
+
labelHref?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Tooltip for the linked label, saying what following it does. The link text
|
|
28
|
+
* is the feed's name ("Latest", "All"), which alone does not tell a reader
|
|
29
|
+
* that it leads to a page — and `title` supplements the accessible name
|
|
30
|
+
* rather than replacing it, so the visible text still wins.
|
|
31
|
+
*/
|
|
32
|
+
labelTitle?: string;
|
|
33
|
+
/** The address to show and copy. Absolute — it is meant to leave this page. */
|
|
34
|
+
value: string;
|
|
35
|
+
/** Optional line under the label saying what this address gives you. */
|
|
36
|
+
description?: string;
|
|
37
|
+
/** Text on the copy button. */
|
|
38
|
+
copyLabel: string;
|
|
39
|
+
/** Toast shown after a successful copy. */
|
|
40
|
+
copiedMessage: string;
|
|
41
|
+
/** Toast shown when the clipboard is unavailable or refuses. */
|
|
42
|
+
failedMessage: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A read-only address with a copy button.
|
|
47
|
+
*
|
|
48
|
+
* Rendered whole on the server: the address is the content of the page this
|
|
49
|
+
* appears on, so it must not wait for JavaScript. `client/copy-field.ts` adds
|
|
50
|
+
* the copying and reveals the button, which ships hidden so that without the
|
|
51
|
+
* script the field degrades to an input the reader can select by hand rather
|
|
52
|
+
* than a button that does nothing.
|
|
53
|
+
*
|
|
54
|
+
* @param props - Label, address, and the strings the client needs for toasts
|
|
55
|
+
* @returns The field markup
|
|
56
|
+
* @example
|
|
57
|
+
* <CopyField
|
|
58
|
+
* label="Main feed"
|
|
59
|
+
* value="https://example.com/feed"
|
|
60
|
+
* copyLabel="Copy"
|
|
61
|
+
* copiedMessage="Feed URL copied."
|
|
62
|
+
* failedMessage="Could not copy. Select the address and copy it."
|
|
63
|
+
* />
|
|
64
|
+
*/
|
|
65
|
+
export const CopyField: FC<CopyFieldProps> = ({
|
|
66
|
+
label,
|
|
67
|
+
labelHref,
|
|
68
|
+
labelTitle,
|
|
69
|
+
value,
|
|
70
|
+
description,
|
|
71
|
+
copyLabel,
|
|
72
|
+
copiedMessage,
|
|
73
|
+
failedMessage,
|
|
74
|
+
}) => (
|
|
75
|
+
<div class={COPY_FIELD_CLASS} data-copy-field-root>
|
|
76
|
+
<p class="text-sm font-medium">
|
|
77
|
+
{labelHref ? (
|
|
78
|
+
// The affordance is the arrow, not an underline. Three underlined
|
|
79
|
+
// labels stacked over three inputs read as a mis-styled form, and the
|
|
80
|
+
// rule under a two-word label is heavy enough to compete with the
|
|
81
|
+
// address it introduces. An arrow is visible at rest — which the
|
|
82
|
+
// underline was there to be — and says where the link goes besides.
|
|
83
|
+
// The underline still arrives on hover, where it costs nothing.
|
|
84
|
+
<a
|
|
85
|
+
href={labelHref}
|
|
86
|
+
title={labelTitle}
|
|
87
|
+
class="group inline-flex items-center gap-1 underline-offset-4 hover:underline focus-visible:underline"
|
|
88
|
+
>
|
|
89
|
+
{label}
|
|
90
|
+
<span
|
|
91
|
+
class="text-muted-foreground transition-transform duration-150 group-hover:translate-x-0.5 group-focus-visible:translate-x-0.5"
|
|
92
|
+
aria-hidden="true"
|
|
93
|
+
dangerouslySetInnerHTML={{
|
|
94
|
+
__html: getIconSvg("arrow-right", "size-3.5 icon-fine") ?? "",
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
</a>
|
|
98
|
+
) : (
|
|
99
|
+
label
|
|
100
|
+
)}
|
|
101
|
+
</p>
|
|
102
|
+
{description ? (
|
|
103
|
+
<p class="text-sm text-muted-foreground">{description}</p>
|
|
104
|
+
) : null}
|
|
105
|
+
<div class={COPY_FIELD_CONTROL_CLASS}>
|
|
106
|
+
<input
|
|
107
|
+
type="text"
|
|
108
|
+
class={COPY_FIELD_INPUT_CLASS}
|
|
109
|
+
value={value}
|
|
110
|
+
readonly
|
|
111
|
+
aria-label={label}
|
|
112
|
+
{...{ [COPY_FIELD_VALUE_ATTR]: "" }}
|
|
113
|
+
/>
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
class={COPY_FIELD_BUTTON_CLASS}
|
|
117
|
+
hidden
|
|
118
|
+
{...{
|
|
119
|
+
[COPY_FIELD_BUTTON_ATTR]: copiedMessage,
|
|
120
|
+
[COPY_FIELD_FAILED_ATTR]: failedMessage,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{copyLabel}
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Empty Post Content
|
|
3
|
+
*
|
|
4
|
+
* The microformats floor for a card that renders neither a name nor any
|
|
5
|
+
* content of its own.
|
|
6
|
+
*
|
|
7
|
+
* A microformats2 parser invents an h-entry's `name` from its text content
|
|
8
|
+
* whenever the entry carries no explicit `p-name`, no `e-*` property, and no
|
|
9
|
+
* nested microformat. The author's card ships every `PostStatusBadges` badge in
|
|
10
|
+
* the DOM and hides the inapplicable ones with CSS — which a parser does not
|
|
11
|
+
* run — so a card with nothing else in it publishes a title of
|
|
12
|
+
* "PinnedPinnedPrivateDraft" followed by its own date.
|
|
13
|
+
*
|
|
14
|
+
* An explicit empty `e-content` states what is true, that this entry has no
|
|
15
|
+
* text content, and that is enough to stop the guess.
|
|
16
|
+
*
|
|
17
|
+
* It carries neither `prose` nor `data-post-body` on purpose: the body-width
|
|
18
|
+
* CSS and the client scripts that look up a post's body must not see it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { FC } from "hono/jsx";
|
|
22
|
+
|
|
23
|
+
export const EmptyPostContent: FC = () => <div class="e-content"></div>;
|
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
* Renders media attachments in a unified horizontal row: images with
|
|
5
5
|
* lightbox support, videos with play overlay, audio/documents as 3:4
|
|
6
6
|
* styled card tiles, and attached texts as summary cards.
|
|
7
|
+
*
|
|
8
|
+
* Attachments sit beside `e-content`, not inside it, so a consumer reading
|
|
9
|
+
* the h-entry would otherwise see a post with no media at all. The visual
|
|
10
|
+
* ones carry `u-photo` / `u-video` / `u-audio` — the standard microformats2
|
|
11
|
+
* properties, the same ones W3C Post Type Discovery reads — so what the
|
|
12
|
+
* timeline shows is what a parser gets. Placement follows the mf2 implied
|
|
13
|
+
* value rules: on the element whose own attribute already holds the URL,
|
|
14
|
+
* never on a wrapper that would resolve to something else.
|
|
15
|
+
*
|
|
16
|
+
* Documents and attached texts stay unmarked. There is no standard property
|
|
17
|
+
* for them, and inventing one would put a private vocabulary in public
|
|
18
|
+
* markup.
|
|
7
19
|
*/
|
|
8
20
|
|
|
9
21
|
import type { FC } from "hono/jsx";
|
|
@@ -63,8 +75,73 @@ export function getMediaPlaceholderDataUrl(
|
|
|
63
75
|
return blurhashToDataUrl(blurhash, decodeSize.width, decodeSize.height);
|
|
64
76
|
}
|
|
65
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Width for a post's only visual: its natural shape, capped at 24rem tall and
|
|
80
|
+
* never wider than the reading column the post's own text gets.
|
|
81
|
+
*
|
|
82
|
+
* The cap is `--layout-reading-width`, not `--layout-content-width` — below
|
|
83
|
+
* 1024px the latter flattens to 100% and a wide picture would outgrow the body
|
|
84
|
+
* copy it sits under. See the token comments in `styles/tokens.css`.
|
|
85
|
+
*
|
|
86
|
+
* @param ratio - Aspect ratio (width / height) from `getMediaAspectRatio`.
|
|
87
|
+
* @returns A CSS `width` value.
|
|
88
|
+
* @example
|
|
89
|
+
* getSingleVisualWidth(16 / 9);
|
|
90
|
+
* // "min(100%, calc(24rem * 1.777...), var(--layout-reading-width))"
|
|
91
|
+
*/
|
|
66
92
|
function getSingleVisualWidth(ratio: number): string {
|
|
67
|
-
return `min(100%, calc(24rem * ${ratio}), var(--layout-
|
|
93
|
+
return `min(100%, calc(24rem * ${ratio}), var(--layout-reading-width))`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Shortest a gallery strip gets, in CSS pixels. */
|
|
97
|
+
const ROW_MIN = 240;
|
|
98
|
+
/** Tallest a gallery strip gets — the 24rem cap a lone visual already has. */
|
|
99
|
+
const ROW_MAX = 384;
|
|
100
|
+
/** No single item in a strip is wider than this, in CSS pixels. */
|
|
101
|
+
const ITEM_MAX_WIDTH = 520;
|
|
102
|
+
/** Width the opening visual is measured at when it sets the row height. */
|
|
103
|
+
const NOMINAL_ITEM_WIDTH = 320;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Shared height for a multi-item gallery strip.
|
|
107
|
+
*
|
|
108
|
+
* Every visual in a strip gets this height and a width of
|
|
109
|
+
* `rowHeight * its own aspect ratio`, so one number decides the whole row.
|
|
110
|
+
* Three rules pick it, in order:
|
|
111
|
+
*
|
|
112
|
+
* 1. **The opener leads.** `NOMINAL_ITEM_WIDTH / leadRatio` is the height the
|
|
113
|
+
* first visual would have at a nominal 320px width, so a portrait opener
|
|
114
|
+
* earns a taller row than a landscape one.
|
|
115
|
+
* 2. **Nothing outgrows the strip.** The widest visual is held to
|
|
116
|
+
* `ITEM_MAX_WIDTH`, which stops a 16:9 video following a portrait picture
|
|
117
|
+
* from swallowing the whole content column.
|
|
118
|
+
* 3. **One band.** `ROW_MAX` matches the cap a lone visual gets on its own, so
|
|
119
|
+
* a second attachment never makes the first picture grow.
|
|
120
|
+
*
|
|
121
|
+
* Non-visual attachments stay out of this deliberately: a text file beside a
|
|
122
|
+
* picture is no reason to resize the picture. Their cards are sized *from* the
|
|
123
|
+
* result instead.
|
|
124
|
+
*
|
|
125
|
+
* @param visualRatios - Aspect ratios of the strip's images and videos in
|
|
126
|
+
* display order, each from {@link getMediaAspectRatio} so every value is
|
|
127
|
+
* finite and positive. Empty when the strip holds no visuals at all.
|
|
128
|
+
* @returns Row height in CSS pixels.
|
|
129
|
+
* @example
|
|
130
|
+
* getGalleryRowHeight([0.53, 1.78]); // 292 - portrait opener, then a 16:9 video
|
|
131
|
+
* getGalleryRowHeight([0.53, 0.53]); // 384 - two portraits
|
|
132
|
+
* getGalleryRowHeight([1.78, 1.78]); // 240 - two landscapes
|
|
133
|
+
* getGalleryRowHeight([]); // 240 - documents only
|
|
134
|
+
*/
|
|
135
|
+
export function getGalleryRowHeight(visualRatios: number[]): number {
|
|
136
|
+
const leadRatio = visualRatios[0];
|
|
137
|
+
if (leadRatio === undefined) return ROW_MIN;
|
|
138
|
+
|
|
139
|
+
const byLead = NOMINAL_ITEM_WIDTH / leadRatio;
|
|
140
|
+
const byWidestItem = ITEM_MAX_WIDTH / Math.max(...visualRatios);
|
|
141
|
+
|
|
142
|
+
return Math.round(
|
|
143
|
+
Math.min(ROW_MAX, Math.max(ROW_MIN, Math.min(byLead, byWidestItem))),
|
|
144
|
+
);
|
|
68
145
|
}
|
|
69
146
|
|
|
70
147
|
/**
|
|
@@ -188,18 +265,20 @@ const FileIcon = ({
|
|
|
188
265
|
);
|
|
189
266
|
};
|
|
190
267
|
|
|
268
|
+
/*
|
|
269
|
+
* No `h-*` Tailwind utility belongs on anything inside a post: a microformats2
|
|
270
|
+
* parser treats every `h-`-prefixed class as a microformat root, so `h-auto` on
|
|
271
|
+
* a single image published a phantom `h-auto` child microformat — carrying the
|
|
272
|
+
* thumbnail as its `photo` — on the h-entry of every post with a picture.
|
|
273
|
+
* Nothing was lost by dropping it: Preflight already declares
|
|
274
|
+
* `img, video { max-width: 100%; height: auto }`.
|
|
275
|
+
*/
|
|
191
276
|
export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
192
277
|
attachments,
|
|
193
278
|
postPermalink,
|
|
194
279
|
}) => {
|
|
195
280
|
if (attachments.length === 0) return null;
|
|
196
281
|
|
|
197
|
-
// Category checks for layout decisions
|
|
198
|
-
const hasNonVisualAttachment = attachments.some((a) => {
|
|
199
|
-
const cat = getMediaCategory(a.mimeType);
|
|
200
|
-
return cat !== "image" && cat !== "video";
|
|
201
|
-
});
|
|
202
|
-
|
|
203
282
|
// Build lightbox group from images + videos in position order
|
|
204
283
|
// (documents/texts don't use lightbox)
|
|
205
284
|
const lightboxItems = attachments
|
|
@@ -251,28 +330,13 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
251
330
|
firstItem !== undefined &&
|
|
252
331
|
(firstItem._kind === "image" || firstItem._kind === "video");
|
|
253
332
|
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
let rowHeight = hasNonVisual ? COMPACT_HEIGHT : 360;
|
|
262
|
-
if (!singleVisual && galleryItems.length > 1) {
|
|
263
|
-
const firstVisual = galleryItems.find(
|
|
264
|
-
(item) => item._kind === "image" || item._kind === "video",
|
|
265
|
-
);
|
|
266
|
-
if (firstVisual && "width" in firstVisual && "height" in firstVisual) {
|
|
267
|
-
const firstRatio = getMediaAspectRatio(
|
|
268
|
-
firstVisual.width,
|
|
269
|
-
firstVisual.height,
|
|
270
|
-
);
|
|
271
|
-
rowHeight = Math.round(
|
|
272
|
-
Math.min(ROW_MAX, Math.max(ROW_MIN, 320 / Math.max(firstRatio, 0.5))),
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
333
|
+
// Strip geometry. A lone visual never reads this — it keeps its natural
|
|
334
|
+
// ratio — but every other shape does, cards included.
|
|
335
|
+
const rowHeight = getGalleryRowHeight(
|
|
336
|
+
galleryItems
|
|
337
|
+
.filter((item) => item._kind === "image" || item._kind === "video")
|
|
338
|
+
.map((item) => getMediaAspectRatio(item.width, item.height)),
|
|
339
|
+
);
|
|
276
340
|
|
|
277
341
|
// Document card: 3:4 portrait, same height as row
|
|
278
342
|
const DOC_RATIO = 3 / 4;
|
|
@@ -290,7 +354,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
290
354
|
? JSON.stringify(lightboxItems)
|
|
291
355
|
: undefined
|
|
292
356
|
}
|
|
293
|
-
class={`flex gap-
|
|
357
|
+
class={`flex gap-3 ${singleVisual ? "" : "overflow-x-auto scroll-smooth"}`}
|
|
294
358
|
style={
|
|
295
359
|
singleVisual
|
|
296
360
|
? undefined
|
|
@@ -349,11 +413,17 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
349
413
|
width={item.width}
|
|
350
414
|
height={item.height}
|
|
351
415
|
style={imageStyle}
|
|
352
|
-
|
|
416
|
+
/* `u-photo` here rather than on the enclosing link:
|
|
417
|
+
`img.u-photo` resolves to `src` and carries `alt`
|
|
418
|
+
with it, so a consumer gets the same thumbnail the
|
|
419
|
+
timeline shows and the text describing it. The link
|
|
420
|
+
wraps the full-size original, which is a click
|
|
421
|
+
target rather than the post's photo. */
|
|
422
|
+
class={`u-photo ${
|
|
353
423
|
singleVisual
|
|
354
|
-
? "media-visual w-full
|
|
424
|
+
? "media-visual w-full rounded-lg"
|
|
355
425
|
: "media-visual w-full object-cover"
|
|
356
|
-
}
|
|
426
|
+
}`}
|
|
357
427
|
loading="lazy"
|
|
358
428
|
decoding="async"
|
|
359
429
|
/>
|
|
@@ -374,6 +444,19 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
374
444
|
? undefined
|
|
375
445
|
: `${Math.round(Math.max(160, rowHeight * ratio))}px`;
|
|
376
446
|
const posterSrc = item.posterUrl || placeholder;
|
|
447
|
+
/* `video.u-photo` resolves to `poster`, which is the still
|
|
448
|
+
the timeline already shows. Only a real poster qualifies:
|
|
449
|
+
the blurhash fallback is a data URL, and publishing one as
|
|
450
|
+
a photo would hand a consumer a 20-pixel smear. */
|
|
451
|
+
const posterClass = item.posterUrl ? "u-photo " : "";
|
|
452
|
+
/* Fit travels with the size, and `site-media.css` sets none.
|
|
453
|
+
In a row the clip is cropped to the strip's height like the
|
|
454
|
+
pictures beside it; alone its box already has the clip's
|
|
455
|
+
shape, and `contain` letterboxes frames that disagree with
|
|
456
|
+
the stored size. */
|
|
457
|
+
const videoClass = `${posterClass}media-visual w-full ${
|
|
458
|
+
singleVisual ? "object-contain" : "object-cover"
|
|
459
|
+
}`;
|
|
377
460
|
const aspectRatio =
|
|
378
461
|
item.width && item.height
|
|
379
462
|
? `${item.width}/${item.height}`
|
|
@@ -408,7 +491,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
408
491
|
<a
|
|
409
492
|
href={item.url}
|
|
410
493
|
data-lightbox-index={item._lbIdx}
|
|
411
|
-
class="media-visual-frame media-video-link"
|
|
494
|
+
class="u-video media-visual-frame media-video-link"
|
|
412
495
|
>
|
|
413
496
|
<video
|
|
414
497
|
preload="none"
|
|
@@ -422,11 +505,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
422
505
|
data-video-src={item.url}
|
|
423
506
|
data-feed-video-id={item.id}
|
|
424
507
|
style={videoStyle}
|
|
425
|
-
class={
|
|
426
|
-
singleVisual
|
|
427
|
-
? "media-visual w-full h-auto"
|
|
428
|
-
: "media-visual w-full object-cover"
|
|
429
|
-
}
|
|
508
|
+
class={videoClass}
|
|
430
509
|
/>
|
|
431
510
|
</a>
|
|
432
511
|
<button
|
|
@@ -468,7 +547,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
468
547
|
key={item.id}
|
|
469
548
|
href={item.url}
|
|
470
549
|
data-lightbox-index={item._lbIdx}
|
|
471
|
-
class={
|
|
550
|
+
class={`u-video ${singleVisual ? "" : "shrink-0"} media-video-wrap media-visual-frame`}
|
|
472
551
|
style={
|
|
473
552
|
singleVisual
|
|
474
553
|
? {
|
|
@@ -486,11 +565,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
486
565
|
width={item.width}
|
|
487
566
|
height={item.height}
|
|
488
567
|
style={videoStyle}
|
|
489
|
-
class={
|
|
490
|
-
singleVisual
|
|
491
|
-
? "media-visual w-full h-auto"
|
|
492
|
-
: "media-visual w-full object-cover"
|
|
493
|
-
}
|
|
568
|
+
class={videoClass}
|
|
494
569
|
/>
|
|
495
570
|
<div class="media-video-play-overlay">
|
|
496
571
|
<svg viewBox="0 0 24 24" fill="white">
|
|
@@ -514,7 +589,11 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
514
589
|
>
|
|
515
590
|
{/* Hidden audio element — JS controls it */}
|
|
516
591
|
<audio preload="none" class="media-audio-el">
|
|
517
|
-
<source
|
|
592
|
+
<source
|
|
593
|
+
class="u-audio"
|
|
594
|
+
src={item.url}
|
|
595
|
+
type={item.mimeType}
|
|
596
|
+
/>
|
|
518
597
|
</audio>
|
|
519
598
|
|
|
520
599
|
{/* Artwork area */}
|
|
@@ -8,6 +8,7 @@ import { msg } from "@lingui/core/macro";
|
|
|
8
8
|
import type { FC } from "hono/jsx";
|
|
9
9
|
import { useLingui } from "../../i18n/context.js";
|
|
10
10
|
import { formatPageLabel, getPageNumbers } from "../../lib/pagination.js";
|
|
11
|
+
import { Icon } from "./Icon.js";
|
|
11
12
|
|
|
12
13
|
export interface PaginationProps {
|
|
13
14
|
/** Base URL for pagination links (e.g., "/archive", "/search?q=test") */
|
|
@@ -183,10 +184,24 @@ export const PagePagination: FC<PagePaginationProps> = ({
|
|
|
183
184
|
// Numbered pagination when totalPages is known
|
|
184
185
|
if (totalPages && totalPages > 1) {
|
|
185
186
|
const pageNumbers = getPageNumbers(currentPage, totalPages);
|
|
187
|
+
// The icon shows only where the bar is too narrow for the words, and the
|
|
188
|
+
// word then stays as the link's accessible name (see `.page-pagination`).
|
|
189
|
+
const prevContent = (
|
|
190
|
+
<>
|
|
191
|
+
<Icon name="arrow-left" class="page-pagination-step-icon" />
|
|
192
|
+
<span class="page-pagination-step-label">{prevText}</span>
|
|
193
|
+
</>
|
|
194
|
+
);
|
|
195
|
+
const nextContent = (
|
|
196
|
+
<>
|
|
197
|
+
<span class="page-pagination-step-label">{nextText}</span>
|
|
198
|
+
<Icon name="arrow-right" class="page-pagination-step-icon" />
|
|
199
|
+
</>
|
|
200
|
+
);
|
|
186
201
|
|
|
187
202
|
return (
|
|
188
203
|
<nav
|
|
189
|
-
class="flex items-center justify-
|
|
204
|
+
class="page-pagination flex flex-wrap items-center justify-start gap-4 py-6"
|
|
190
205
|
aria-label="Pagination"
|
|
191
206
|
>
|
|
192
207
|
{hasPrev ? (
|
|
@@ -194,10 +209,10 @@ export const PagePagination: FC<PagePaginationProps> = ({
|
|
|
194
209
|
href={buildUrl(currentPage - 1)}
|
|
195
210
|
class="underline text-muted-foreground hover:text-foreground"
|
|
196
211
|
>
|
|
197
|
-
{
|
|
212
|
+
{prevContent}
|
|
198
213
|
</a>
|
|
199
214
|
) : (
|
|
200
|
-
<span class="text-muted-foreground/50">{
|
|
215
|
+
<span class="text-muted-foreground/50">{prevContent}</span>
|
|
201
216
|
)}
|
|
202
217
|
|
|
203
218
|
{pageNumbers.map((page, i) =>
|
|
@@ -225,10 +240,10 @@ export const PagePagination: FC<PagePaginationProps> = ({
|
|
|
225
240
|
href={buildUrl(currentPage + 1)}
|
|
226
241
|
class="underline text-muted-foreground hover:text-foreground"
|
|
227
242
|
>
|
|
228
|
-
{
|
|
243
|
+
{nextContent}
|
|
229
244
|
</a>
|
|
230
245
|
) : (
|
|
231
|
-
<span class="text-muted-foreground/50">{
|
|
246
|
+
<span class="text-muted-foreground/50">{nextContent}</span>
|
|
232
247
|
)}
|
|
233
248
|
</nav>
|
|
234
249
|
);
|