@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
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
import type { FC, PropsWithChildren } from "hono/jsx";
|
|
12
12
|
import type { Context } from "hono";
|
|
13
13
|
import { raw } from "hono/utils/html";
|
|
14
|
+
import { escapeHtml } from "../../lib/html.js";
|
|
14
15
|
import { msg } from "@lingui/core/macro";
|
|
15
16
|
import {
|
|
17
|
+
getPreconnectHints,
|
|
16
18
|
getPublicAssetBasePath,
|
|
17
19
|
toAssetPath,
|
|
18
20
|
toPublicAssetPath,
|
|
@@ -21,8 +23,12 @@ import { getJantIconHref } from "../../lib/jant-branding.js";
|
|
|
21
23
|
import { getPublicUrlForProvider } from "../../lib/image.js";
|
|
22
24
|
import { getThemeBrowserColors, resolveBuiltinTheme } from "../../lib/theme.js";
|
|
23
25
|
import { toAbsoluteAssetUrl, toPublicPath } from "../../lib/url.js";
|
|
26
|
+
import type { LanguageAlternate } from "../../lib/view-language.js";
|
|
27
|
+
import { toLanguagePrefix } from "../../i18n/locales.js";
|
|
24
28
|
import {
|
|
25
29
|
CLIENT_AUTH_JS_FILE,
|
|
30
|
+
CLIENT_AUTHOR_CSS_FILE,
|
|
31
|
+
CLIENT_COMPOSE_PRELOAD,
|
|
26
32
|
CLIENT_CJK_CSS_FILE,
|
|
27
33
|
CLIENT_CJK_JP_CSS_FILE,
|
|
28
34
|
CLIENT_CJK_KR_CSS_FILE,
|
|
@@ -30,10 +36,13 @@ import {
|
|
|
30
36
|
CLIENT_CSS_FILE,
|
|
31
37
|
CLIENT_JS_FILE,
|
|
32
38
|
CORE_VERSION,
|
|
33
|
-
IS_VITE_DEV,
|
|
34
39
|
} from "../../lib/version.js";
|
|
40
|
+
import { IS_VITE_DEV } from "../../lib/build-env.js";
|
|
35
41
|
import { I18nProvider } from "../../i18n/index.js";
|
|
36
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
getCjkFontCssVariables,
|
|
44
|
+
resolveCjkFontProfile,
|
|
45
|
+
} from "../font-themes.js";
|
|
37
46
|
import { resetIconCollector } from "../shared/icon-collector.js";
|
|
38
47
|
import { Icon } from "../shared/Icon.js";
|
|
39
48
|
import { IconSprite } from "../shared/IconSprite.js";
|
|
@@ -43,6 +52,17 @@ export interface ToastProps {
|
|
|
43
52
|
type?: "success" | "error";
|
|
44
53
|
}
|
|
45
54
|
|
|
55
|
+
/** One `<link rel="alternate" type="application/atom+xml">` autodiscovery target. */
|
|
56
|
+
export interface PageFeedLink {
|
|
57
|
+
/** Public path (or absolute URL) of the feed. */
|
|
58
|
+
href: string;
|
|
59
|
+
/**
|
|
60
|
+
* Label feed readers show for this feed, e.g. a collection's title. Omitted
|
|
61
|
+
* when blank, so the reader falls back to the feed document's own title.
|
|
62
|
+
*/
|
|
63
|
+
title?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
46
66
|
export interface BaseLayoutProps {
|
|
47
67
|
title: string;
|
|
48
68
|
description?: string;
|
|
@@ -81,7 +101,32 @@ export interface BaseLayoutProps {
|
|
|
81
101
|
* thread at both the reply URL and the thread-root URL).
|
|
82
102
|
*/
|
|
83
103
|
canonicalHref?: string;
|
|
84
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Absolute URLs of this page in the site's other languages, rendered as
|
|
106
|
+
* `<link rel="alternate" hreflang>`. Set on pages that exist once per
|
|
107
|
+
* language (list surfaces) or that have translations (posts).
|
|
108
|
+
*/
|
|
109
|
+
alternateLanguages?: LanguageAlternate[];
|
|
110
|
+
/**
|
|
111
|
+
* The feed this page itself publishes — a collection's feed on a collection
|
|
112
|
+
* page, the filtered archive feed on `/archive`. Rendered ahead of the
|
|
113
|
+
* site-wide feed links, since a reader subscribing from a page means the
|
|
114
|
+
* page they are on.
|
|
115
|
+
*/
|
|
116
|
+
pageFeed?: PageFeedLink;
|
|
117
|
+
/**
|
|
118
|
+
* Crawler policy for this page, overriding the site-wide setting.
|
|
119
|
+
*
|
|
120
|
+
* `true` is the blunt form for utility pages that should leave no trace:
|
|
121
|
+
* `noindex, nofollow`. `"follow"` keeps the URL out of the index while
|
|
122
|
+
* letting a crawler walk its links — for a page that is one URL out of a
|
|
123
|
+
* combinatorial family (a filtered archive) but whose links lead to the
|
|
124
|
+
* real pages.
|
|
125
|
+
*
|
|
126
|
+
* A site-wide noindex always wins: a page may be stricter than the site
|
|
127
|
+
* setting, never looser.
|
|
128
|
+
*/
|
|
129
|
+
noindex?: boolean | "follow";
|
|
85
130
|
isAuthenticated?: boolean;
|
|
86
131
|
clientBundle?: "public" | "full";
|
|
87
132
|
}
|
|
@@ -105,6 +150,8 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
105
150
|
articlePublishedTime,
|
|
106
151
|
articleModifiedTime,
|
|
107
152
|
canonicalHref,
|
|
153
|
+
alternateLanguages,
|
|
154
|
+
pageFeed,
|
|
108
155
|
noindex,
|
|
109
156
|
isAuthenticated = false,
|
|
110
157
|
clientBundle,
|
|
@@ -130,8 +177,17 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
130
177
|
getJantIconHref("socialImage", appConfig?.sitePathPrefix || "");
|
|
131
178
|
const resolvedFaviconVersion =
|
|
132
179
|
faviconVersion ?? (appConfig?.faviconVersion || undefined);
|
|
133
|
-
|
|
180
|
+
// A site-wide noindex is the strictest statement available, so it wins
|
|
181
|
+
// outright — a per-page policy can only narrow further, never relax.
|
|
182
|
+
const resolvedNoindex = appConfig?.noindex ? true : (noindex ?? false);
|
|
134
183
|
const sitePathPrefix = appConfig?.sitePathPrefix || "";
|
|
184
|
+
// Where "here" is for links the client builds. The server has `toViewPath`
|
|
185
|
+
// for this; without the same base in the DOM, a client-rendered link to a
|
|
186
|
+
// per-language surface silently drops the reader into the primary view.
|
|
187
|
+
const viewLang = c?.get("viewLang");
|
|
188
|
+
const viewBasePath = viewLang
|
|
189
|
+
? `${sitePathPrefix}/${toLanguagePrefix(viewLang)}`
|
|
190
|
+
: sitePathPrefix;
|
|
135
191
|
const assetBasePath = IS_VITE_DEV
|
|
136
192
|
? "/"
|
|
137
193
|
: appConfig?.assetBasePath || getPublicAssetBasePath(sitePathPrefix);
|
|
@@ -145,6 +201,13 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
145
201
|
appConfig?.s3PublicUrl,
|
|
146
202
|
appConfig?.localPublicUrl,
|
|
147
203
|
) ?? "";
|
|
204
|
+
// Empty unless assets or media live on their own host; the two collapse to
|
|
205
|
+
// one set of hints when they share a host.
|
|
206
|
+
const preconnectHints = getPreconnectHints({
|
|
207
|
+
assetBasePath,
|
|
208
|
+
mediaBaseUrl: mediaBase,
|
|
209
|
+
siteUrl: appConfig?.siteUrl,
|
|
210
|
+
});
|
|
148
211
|
const currentUrl = c ? c.get("publicRequestUrl") : undefined;
|
|
149
212
|
const rawPath = c?.req?.path ?? "/";
|
|
150
213
|
const manifestStartPath = sitePathPrefix
|
|
@@ -177,11 +240,22 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
177
240
|
const browserThemeColors = getThemeBrowserColors(activeTheme);
|
|
178
241
|
const resolvedClientBundle =
|
|
179
242
|
clientBundle ?? (isAuthenticated ? "full" : "public");
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
243
|
+
// Derived from the language of *this page*, not the site's: on a post page
|
|
244
|
+
// that is the post's own language, on a language-filtered list it is the view
|
|
245
|
+
// language, and otherwise it falls back to the site language. Simplified,
|
|
246
|
+
// Traditional, Japanese and Korean render the same code points with different
|
|
247
|
+
// glyphs, so a page in one must not inherit another's font stack.
|
|
248
|
+
const cjkFontProfile = resolveCjkFontProfile(resolvedLang);
|
|
249
|
+
const cjkFontDeclarations = Object.entries(
|
|
250
|
+
getCjkFontCssVariables(resolvedLang),
|
|
251
|
+
)
|
|
252
|
+
.map(([name, value]) => ` ${name}: ${value};`)
|
|
253
|
+
.join("\n");
|
|
254
|
+
// `:root:root` matches the specificity `buildThemeStyle` uses, so ordering
|
|
255
|
+
// alone decides the winner.
|
|
256
|
+
const cjkFontStyle = cjkFontDeclarations
|
|
257
|
+
? `:root:root {\n${cjkFontDeclarations}\n}`
|
|
258
|
+
: "";
|
|
185
259
|
const cjkStylesheetPath =
|
|
186
260
|
cjkFontProfile === "zh-Hans"
|
|
187
261
|
? IS_VITE_DEV
|
|
@@ -200,6 +274,16 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
200
274
|
? assetPath("/src/style-cjk-kr.css")
|
|
201
275
|
: toPublicAssetPath(CLIENT_CJK_KR_CSS_FILE, assetBasePath)
|
|
202
276
|
: null;
|
|
277
|
+
// The composer, the editor chrome, the settings pages and the draft preview
|
|
278
|
+
// bar are the bulk of the hand-written component CSS, and none of it renders
|
|
279
|
+
// for a signed-out visitor. Linking it only here keeps it off the critical
|
|
280
|
+
// path of every public page.
|
|
281
|
+
const authorStylesheetPath =
|
|
282
|
+
resolvedClientBundle === "full"
|
|
283
|
+
? IS_VITE_DEV
|
|
284
|
+
? assetPath("/src/style-author.css")
|
|
285
|
+
: toPublicAssetPath(CLIENT_AUTHOR_CSS_FILE, assetBasePath)
|
|
286
|
+
: null;
|
|
203
287
|
const clientScriptPath = IS_VITE_DEV
|
|
204
288
|
? resolvedClientBundle === "full"
|
|
205
289
|
? assetPath("/src/client-auth.ts")
|
|
@@ -211,6 +295,15 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
211
295
|
resolvedClientBundle === "full" ? CLIENT_AUTH_JS_FILE : CLIENT_JS_FILE,
|
|
212
296
|
assetBasePath,
|
|
213
297
|
);
|
|
298
|
+
// The composer loads on first use; fetching its files now means the first
|
|
299
|
+
// open costs no round trip. Dev serves modules straight from source, so
|
|
300
|
+
// there is nothing to preload there.
|
|
301
|
+
const composePreloadPaths =
|
|
302
|
+
!IS_VITE_DEV && resolvedClientBundle === "full"
|
|
303
|
+
? CLIENT_COMPOSE_PRELOAD.map((file) =>
|
|
304
|
+
toPublicAssetPath(file, assetBasePath),
|
|
305
|
+
)
|
|
306
|
+
: [];
|
|
214
307
|
const faviconAssetVersion = resolvedFaviconVersion || CORE_VERSION;
|
|
215
308
|
const resolvedFaviconHref =
|
|
216
309
|
faviconHref ??
|
|
@@ -290,6 +383,8 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
290
383
|
appConfig?.mainRssFeed === "latest"
|
|
291
384
|
? { href: featuredFeedHref, title: featuredFeedTitle }
|
|
292
385
|
: { href: latestFeedHref, title: latestFeedTitle };
|
|
386
|
+
const pageFeedHref = feedsEnabled ? pageFeed?.href.trim() || null : null;
|
|
387
|
+
const pageFeedTitle = pageFeed?.title?.trim() || undefined;
|
|
293
388
|
|
|
294
389
|
return (
|
|
295
390
|
<>
|
|
@@ -299,6 +394,8 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
299
394
|
data-theme={appConfig?.themeId}
|
|
300
395
|
data-theme-mode={themeMode}
|
|
301
396
|
data-site-path-prefix={sitePathPrefix}
|
|
397
|
+
data-view-base-path={viewBasePath}
|
|
398
|
+
data-view-lang={viewLang ?? undefined}
|
|
302
399
|
data-asset-base-path={assetBasePath}
|
|
303
400
|
data-media-base={mediaBase}
|
|
304
401
|
>
|
|
@@ -308,6 +405,13 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
308
405
|
name="viewport"
|
|
309
406
|
content="width=device-width, initial-scale=1.0"
|
|
310
407
|
/>
|
|
408
|
+
{preconnectHints.map((hint) => (
|
|
409
|
+
<link
|
|
410
|
+
rel="preconnect"
|
|
411
|
+
href={hint.href}
|
|
412
|
+
{...(hint.crossorigin ? { crossorigin: "anonymous" } : {})}
|
|
413
|
+
/>
|
|
414
|
+
))}
|
|
311
415
|
{themeMode === "dark" ? (
|
|
312
416
|
<meta name="theme-color" content={browserThemeColors.dark} />
|
|
313
417
|
) : themeMode === "light" ? (
|
|
@@ -381,7 +485,14 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
381
485
|
<meta name="twitter:image:alt" content={socialImageAltText} />
|
|
382
486
|
)}
|
|
383
487
|
{resolvedNoindex && (
|
|
384
|
-
<meta
|
|
488
|
+
<meta
|
|
489
|
+
name="robots"
|
|
490
|
+
content={
|
|
491
|
+
resolvedNoindex === "follow"
|
|
492
|
+
? "noindex, follow"
|
|
493
|
+
: "noindex, nofollow"
|
|
494
|
+
}
|
|
495
|
+
/>
|
|
385
496
|
)}
|
|
386
497
|
{!resolvedNoindex && jsonLd != null && (
|
|
387
498
|
<script
|
|
@@ -397,6 +508,21 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
397
508
|
/>
|
|
398
509
|
)}
|
|
399
510
|
{canonicalHref && <link rel="canonical" href={canonicalHref} />}
|
|
511
|
+
{/* Emitted as raw HTML on purpose: hono/jsx deduplicates `<link>`
|
|
512
|
+
elements by `href` alone, which silently drops the self-referential
|
|
513
|
+
alternate (it shares the canonical link's URL) and any `x-default`
|
|
514
|
+
(it shares the primary language's). Both are required for an
|
|
515
|
+
hreflang set to be honoured. Values are escaped here. */}
|
|
516
|
+
{alternateLanguages && alternateLanguages.length > 0
|
|
517
|
+
? raw(
|
|
518
|
+
alternateLanguages
|
|
519
|
+
.map(
|
|
520
|
+
(alternate) =>
|
|
521
|
+
`<link rel="alternate" hreflang="${escapeHtml(alternate.hreflang)}" href="${escapeHtml(alternate.href)}"/>`,
|
|
522
|
+
)
|
|
523
|
+
.join(""),
|
|
524
|
+
)
|
|
525
|
+
: null}
|
|
400
526
|
<link rel="icon" href={resolvedFaviconHref} sizes="16x16 32x32" />
|
|
401
527
|
<link rel="apple-touch-icon" href={resolvedAppleTouchHref} />
|
|
402
528
|
<link
|
|
@@ -408,6 +534,14 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
408
534
|
sitePathPrefix,
|
|
409
535
|
)}
|
|
410
536
|
/>
|
|
537
|
+
{pageFeedHref && (
|
|
538
|
+
<link
|
|
539
|
+
rel="alternate"
|
|
540
|
+
type="application/atom+xml"
|
|
541
|
+
title={pageFeedTitle}
|
|
542
|
+
href={pageFeedHref}
|
|
543
|
+
/>
|
|
544
|
+
)}
|
|
411
545
|
{mainFeedHref && (
|
|
412
546
|
<link
|
|
413
547
|
rel="alternate"
|
|
@@ -438,6 +572,9 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
438
572
|
{cjkStylesheetPath && (
|
|
439
573
|
<link rel="stylesheet" href={cjkStylesheetPath} />
|
|
440
574
|
)}
|
|
575
|
+
{authorStylesheetPath && (
|
|
576
|
+
<link rel="stylesheet" href={authorStylesheetPath} />
|
|
577
|
+
)}
|
|
441
578
|
{/* Critical inline style: prevent mobile nav jitter by applying
|
|
442
579
|
responsive header layout before external CSS/JS loads */}
|
|
443
580
|
<style
|
|
@@ -445,6 +582,11 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
445
582
|
__html: `.site-header-search-link,.site-header-hamburger,.site-header-more-responsive-only,.site-header-more-link-responsive,.site-header-more-divider-responsive{display:none!important}@media(max-width:1200px){.site-header-search-form{display:none!important}.site-header-search-link{display:inline-flex!important}}@media(max-width:960px){.site-header-link-collapse-lg{display:none!important}.site-header-more-responsive-only.site-header-more-tier-lg{display:inline-flex!important}.site-header-more-link-show-lg{display:flex!important}.site-header-more-divider-show-lg{display:block!important}}@media(max-width:780px){.site-header-link-collapse-md{display:none!important}.site-header-more-responsive-only.site-header-more-tier-md{display:inline-flex!important}.site-header-more-link-show-md{display:flex!important}.site-header-more-divider-show-md{display:block!important}}@media(max-width:580px){.site-header-link-collapse-sm{display:none!important}.site-header-more-responsive-only.site-header-more-tier-sm{display:inline-flex!important}.site-header-more-link-show-sm{display:flex!important}.site-header-more-divider-show-sm{display:block!important}}@media(max-width:480px){.site-header-nav,.site-header-more{display:none!important}.site-header-search-slot{display:flex!important}.site-header-hamburger{display:flex!important}.site-header-right{margin-left:.35rem}}`,
|
|
446
583
|
}}
|
|
447
584
|
/>
|
|
585
|
+
{/* Emitted before the theme style so a font theme that names its own
|
|
586
|
+
CJK stack still wins. */}
|
|
587
|
+
{cjkFontStyle && (
|
|
588
|
+
<style dangerouslySetInnerHTML={{ __html: cjkFontStyle }} />
|
|
589
|
+
)}
|
|
448
590
|
{themeStyle && (
|
|
449
591
|
<style dangerouslySetInnerHTML={{ __html: themeStyle }} />
|
|
450
592
|
)}
|
|
@@ -453,6 +595,9 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
453
595
|
)}
|
|
454
596
|
{customHeadHtml && raw(customHeadHtml)}
|
|
455
597
|
<script type="module" src={clientScriptPath} />
|
|
598
|
+
{composePreloadPaths.map((href) => (
|
|
599
|
+
<link rel="modulepreload" href={href} />
|
|
600
|
+
))}
|
|
456
601
|
</head>
|
|
457
602
|
<body
|
|
458
603
|
class="bg-background text-foreground antialiased"
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
import { msg } from "@lingui/core/macro";
|
|
9
9
|
import type { FC, PropsWithChildren } from "hono/jsx";
|
|
10
10
|
import { useLingui } from "../../i18n/context.js";
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
LanguageSwitcherOption,
|
|
13
|
+
NavItemView,
|
|
14
|
+
SiteLayoutProps,
|
|
15
|
+
} from "../../types.js";
|
|
12
16
|
import { toPublicPath } from "../../lib/url.js";
|
|
13
17
|
import { ComposeDialog } from "../compose/ComposeDialog.js";
|
|
14
18
|
import { ComposePrompt } from "../compose/ComposePrompt.js";
|
|
@@ -117,8 +121,17 @@ export interface SiteHeaderProps {
|
|
|
117
121
|
links: NavItemView[];
|
|
118
122
|
currentPath: string;
|
|
119
123
|
sitePathPrefix?: string;
|
|
120
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Public base for language-scoped links — the logo, search, and home
|
|
126
|
+
* detection. Equals `sitePathPrefix` outside a language view, and carries
|
|
127
|
+
* the language prefix inside one (`/blog/ja`), so a page in the Japanese
|
|
128
|
+
* view leads home to `/ja` rather than to the primary language.
|
|
129
|
+
*/
|
|
130
|
+
basePath?: string;
|
|
131
|
+
siteAvatarThumbUrl?: string;
|
|
121
132
|
showHeaderAvatar?: boolean;
|
|
133
|
+
/** Languages this site publishes in. Empty on a single-language site. */
|
|
134
|
+
languageSwitcher?: LanguageSwitcherOption[];
|
|
122
135
|
}
|
|
123
136
|
|
|
124
137
|
function linkCollapseTier(idx: number): string {
|
|
@@ -148,10 +161,13 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
|
|
|
148
161
|
links,
|
|
149
162
|
currentPath,
|
|
150
163
|
sitePathPrefix = "",
|
|
151
|
-
|
|
164
|
+
basePath = sitePathPrefix,
|
|
165
|
+
siteAvatarThumbUrl,
|
|
152
166
|
showHeaderAvatar,
|
|
167
|
+
languageSwitcher = [],
|
|
153
168
|
}) => {
|
|
154
169
|
const { i18n } = useLingui();
|
|
170
|
+
const homeHref = basePath || "/";
|
|
155
171
|
const linksWithLabels = links.map((link) => ({
|
|
156
172
|
...link,
|
|
157
173
|
displayLabel: getNavItemDisplayLabel(link, i18n, sitePathPrefix),
|
|
@@ -164,7 +180,7 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
|
|
|
164
180
|
comment: "@context: Search icon link in browse nav",
|
|
165
181
|
}),
|
|
166
182
|
);
|
|
167
|
-
const searchHref = toPublicPath("/search",
|
|
183
|
+
const searchHref = toPublicPath("/search", basePath);
|
|
168
184
|
|
|
169
185
|
const menuLabel = i18n._(
|
|
170
186
|
msg({
|
|
@@ -180,6 +196,32 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
|
|
|
180
196
|
);
|
|
181
197
|
const moreLabel = i18n._(NAV_MORE_LABEL);
|
|
182
198
|
|
|
199
|
+
// The switcher is a "take me to this language's site" control, so it names
|
|
200
|
+
// each language in that language — a reader looking for their own language
|
|
201
|
+
// should not have to read the current one to find it.
|
|
202
|
+
const languageLabel = i18n._(
|
|
203
|
+
msg({
|
|
204
|
+
message: "Language",
|
|
205
|
+
comment: "@context: Accessible label for the site language switcher",
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
// Which language the reader is in, and whether that is worth saying out
|
|
209
|
+
// loud: the root is the site's default, everything else is a variant of it.
|
|
210
|
+
const currentLanguage = languageSwitcher.find((option) => option.isCurrent);
|
|
211
|
+
const offPrimaryLanguage = Boolean(
|
|
212
|
+
currentLanguage && !currentLanguage.isPrimary,
|
|
213
|
+
);
|
|
214
|
+
// The visible name is written in its own language, so the accessible name
|
|
215
|
+
// has to carry it too — a control read out as "Language" alone would lose
|
|
216
|
+
// the one thing it says.
|
|
217
|
+
const currentLanguageLabel = i18n._(
|
|
218
|
+
msg({
|
|
219
|
+
message: "Language: {language}",
|
|
220
|
+
comment:
|
|
221
|
+
"@context: Accessible label for the site language switcher when it names the language on screen",
|
|
222
|
+
}),
|
|
223
|
+
{ language: currentLanguage?.label ?? "" },
|
|
224
|
+
);
|
|
183
225
|
// Split custom links by placement.
|
|
184
226
|
const headerLinks = linksWithLabels.filter(
|
|
185
227
|
(l) => l.placement === "header" || !l.placement,
|
|
@@ -206,9 +248,9 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
|
|
|
206
248
|
.filter(Boolean)
|
|
207
249
|
.join(" ");
|
|
208
250
|
const isHomePage =
|
|
209
|
-
currentPath ===
|
|
210
|
-
currentPath === toPublicPath("/featured",
|
|
211
|
-
currentPath === toPublicPath("/latest",
|
|
251
|
+
currentPath === homeHref ||
|
|
252
|
+
currentPath === toPublicPath("/featured", basePath) ||
|
|
253
|
+
currentPath === toPublicPath("/latest", basePath);
|
|
212
254
|
|
|
213
255
|
return (
|
|
214
256
|
<>
|
|
@@ -217,9 +259,9 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
|
|
|
217
259
|
<div
|
|
218
260
|
class={`site-header-top site-header-top-bordered${isHomePage ? " site-header-top-home" : ""}`}
|
|
219
261
|
>
|
|
220
|
-
<a href={
|
|
221
|
-
{showHeaderAvatar &&
|
|
222
|
-
<img src={
|
|
262
|
+
<a href={homeHref} class="site-logo">
|
|
263
|
+
{showHeaderAvatar && siteAvatarThumbUrl && (
|
|
264
|
+
<img src={siteAvatarThumbUrl} class="site-logo-avatar" alt="" />
|
|
223
265
|
)}
|
|
224
266
|
{siteName}
|
|
225
267
|
</a>
|
|
@@ -357,6 +399,93 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
|
|
|
357
399
|
</div>
|
|
358
400
|
|
|
359
401
|
<div class="site-header-right">
|
|
402
|
+
{languageSwitcher.length > 1 && (
|
|
403
|
+
<div
|
|
404
|
+
class={`site-header-lang${
|
|
405
|
+
offPrimaryLanguage ? " site-header-lang-named" : ""
|
|
406
|
+
}`}
|
|
407
|
+
>
|
|
408
|
+
<button
|
|
409
|
+
type="button"
|
|
410
|
+
class="site-header-more-btn site-header-lang-btn"
|
|
411
|
+
id="site-nav-lang-trigger"
|
|
412
|
+
aria-haspopup="menu"
|
|
413
|
+
aria-expanded="false"
|
|
414
|
+
aria-label={
|
|
415
|
+
offPrimaryLanguage ? currentLanguageLabel : languageLabel
|
|
416
|
+
}
|
|
417
|
+
>
|
|
418
|
+
{/* A globe, never a flag: flags name countries rather
|
|
419
|
+
than languages. The primary language is the site as
|
|
420
|
+
it comes, so the globe stands alone there; every
|
|
421
|
+
other view is a variant of it, and a reader who
|
|
422
|
+
landed on one — from a search result, a shared link,
|
|
423
|
+
a post written in another language — is told which,
|
|
424
|
+
without opening anything. */}
|
|
425
|
+
<svg
|
|
426
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
427
|
+
width="15"
|
|
428
|
+
height="15"
|
|
429
|
+
viewBox="0 0 24 24"
|
|
430
|
+
fill="none"
|
|
431
|
+
stroke="currentColor"
|
|
432
|
+
stroke-width="2"
|
|
433
|
+
stroke-linecap="round"
|
|
434
|
+
stroke-linejoin="round"
|
|
435
|
+
aria-hidden="true"
|
|
436
|
+
class="site-header-lang-globe"
|
|
437
|
+
>
|
|
438
|
+
<circle cx="12" cy="12" r="10" />
|
|
439
|
+
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
|
|
440
|
+
<path d="M2 12h20" />
|
|
441
|
+
</svg>
|
|
442
|
+
{offPrimaryLanguage && (
|
|
443
|
+
<span
|
|
444
|
+
class="site-header-lang-name"
|
|
445
|
+
lang={currentLanguage?.lang}
|
|
446
|
+
>
|
|
447
|
+
{currentLanguage?.label}
|
|
448
|
+
</span>
|
|
449
|
+
)}
|
|
450
|
+
<svg
|
|
451
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
452
|
+
width="12"
|
|
453
|
+
height="12"
|
|
454
|
+
viewBox="0 0 24 24"
|
|
455
|
+
fill="none"
|
|
456
|
+
stroke="currentColor"
|
|
457
|
+
stroke-width="2.5"
|
|
458
|
+
stroke-linecap="round"
|
|
459
|
+
stroke-linejoin="round"
|
|
460
|
+
aria-hidden="true"
|
|
461
|
+
>
|
|
462
|
+
<path d="m6 9 6 6 6-6" />
|
|
463
|
+
</svg>
|
|
464
|
+
</button>
|
|
465
|
+
<div
|
|
466
|
+
id="site-nav-lang-popover"
|
|
467
|
+
class="site-header-more-popover site-header-lang-popover"
|
|
468
|
+
data-popover
|
|
469
|
+
data-align="end"
|
|
470
|
+
aria-hidden="true"
|
|
471
|
+
>
|
|
472
|
+
{languageSwitcher.map((option) => (
|
|
473
|
+
<a
|
|
474
|
+
key={option.lang}
|
|
475
|
+
href={option.href}
|
|
476
|
+
hreflang={option.lang}
|
|
477
|
+
lang={option.lang}
|
|
478
|
+
class={`site-header-more-link ${option.isCurrent ? "site-header-more-link-active" : ""}`}
|
|
479
|
+
{...(option.isCurrent
|
|
480
|
+
? { "aria-current": "true" }
|
|
481
|
+
: {})}
|
|
482
|
+
>
|
|
483
|
+
{option.label}
|
|
484
|
+
</a>
|
|
485
|
+
))}
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
)}
|
|
360
489
|
<button
|
|
361
490
|
type="button"
|
|
362
491
|
class="site-header-hamburger"
|
|
@@ -401,13 +530,10 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
|
|
|
401
530
|
inert
|
|
402
531
|
>
|
|
403
532
|
<div class="site-nav-drawer-header">
|
|
404
|
-
<a
|
|
405
|
-
|
|
406
|
-
class="site-nav-drawer-brand"
|
|
407
|
-
>
|
|
408
|
-
{showHeaderAvatar && siteAvatarUrl && (
|
|
533
|
+
<a href={homeHref} class="site-nav-drawer-brand">
|
|
534
|
+
{showHeaderAvatar && siteAvatarThumbUrl && (
|
|
409
535
|
<img
|
|
410
|
-
src={
|
|
536
|
+
src={siteAvatarThumbUrl}
|
|
411
537
|
class="site-nav-drawer-brand-avatar"
|
|
412
538
|
alt=""
|
|
413
539
|
/>
|
|
@@ -470,6 +596,24 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
|
|
|
470
596
|
))}
|
|
471
597
|
</>
|
|
472
598
|
)}
|
|
599
|
+
{languageSwitcher.length > 1 && (
|
|
600
|
+
<>
|
|
601
|
+
<div class="site-nav-drawer-divider" />
|
|
602
|
+
<span class="site-nav-drawer-section-label">{languageLabel}</span>
|
|
603
|
+
{languageSwitcher.map((option) => (
|
|
604
|
+
<a
|
|
605
|
+
key={option.lang}
|
|
606
|
+
href={option.href}
|
|
607
|
+
hreflang={option.lang}
|
|
608
|
+
lang={option.lang}
|
|
609
|
+
class={`site-nav-drawer-link site-nav-drawer-link-secondary ${option.isCurrent ? "site-nav-drawer-link-active" : ""}`}
|
|
610
|
+
{...(option.isCurrent ? { "aria-current": "true" } : {})}
|
|
611
|
+
>
|
|
612
|
+
{option.label}
|
|
613
|
+
</a>
|
|
614
|
+
))}
|
|
615
|
+
</>
|
|
616
|
+
)}
|
|
473
617
|
</nav>
|
|
474
618
|
</div>
|
|
475
619
|
</>
|
|
@@ -481,9 +625,10 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
|
|
|
481
625
|
links,
|
|
482
626
|
currentPath,
|
|
483
627
|
sitePathPrefix = "",
|
|
628
|
+
basePath = sitePathPrefix,
|
|
484
629
|
isAuthenticated,
|
|
485
630
|
collections,
|
|
486
|
-
|
|
631
|
+
siteAvatarThumbUrl,
|
|
487
632
|
showHeaderAvatar,
|
|
488
633
|
siteDescriptionHtml,
|
|
489
634
|
siteFooterHtml,
|
|
@@ -495,6 +640,9 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
|
|
|
495
640
|
composeOpenShortcutDiscovered = false,
|
|
496
641
|
slashCommandDiscovered = false,
|
|
497
642
|
composeCollectionId,
|
|
643
|
+
languageSwitcher,
|
|
644
|
+
composeLanguages,
|
|
645
|
+
composeContextLanguage,
|
|
498
646
|
children,
|
|
499
647
|
}) => {
|
|
500
648
|
const { i18n } = useLingui();
|
|
@@ -507,35 +655,33 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
|
|
|
507
655
|
);
|
|
508
656
|
|
|
509
657
|
const isHomePage =
|
|
510
|
-
currentPath ===
|
|
511
|
-
currentPath === toPublicPath("/featured",
|
|
512
|
-
currentPath === toPublicPath("/latest",
|
|
658
|
+
currentPath === (basePath || "/") ||
|
|
659
|
+
currentPath === toPublicPath("/featured", basePath) ||
|
|
660
|
+
currentPath === toPublicPath("/latest", basePath);
|
|
513
661
|
const showMobileComposeFab = Boolean(
|
|
514
662
|
(isHomePage || composeCollectionId) && isAuthenticated && showComposeDialog,
|
|
515
663
|
);
|
|
516
|
-
const
|
|
517
|
-
? `document.getElementById('compose-dialog')?.querySelector('jant-compose-dialog')?.openNew(${JSON.stringify(
|
|
518
|
-
{ collectionId: composeCollectionId },
|
|
519
|
-
)})`
|
|
520
|
-
: "document.getElementById('compose-dialog')?.querySelector('jant-compose-dialog')?.openNew()";
|
|
521
|
-
const contentClass = [
|
|
522
|
-
"site-content",
|
|
523
|
-
isHomePage ? "site-content-home" : "",
|
|
524
|
-
showMobileComposeFab ? "site-content-mobile-compose-enabled" : "",
|
|
525
|
-
]
|
|
664
|
+
const contentClass = ["site-content", isHomePage ? "site-content-home" : ""]
|
|
526
665
|
.filter(Boolean)
|
|
527
666
|
.join(" ");
|
|
667
|
+
// The page, not the content, keeps room for the button: the footer and the
|
|
668
|
+
// Jant credit come after the content, and they must end above it too.
|
|
669
|
+
const pageClass = showMobileComposeFab
|
|
670
|
+
? "site-page site-page-mobile-compose-enabled"
|
|
671
|
+
: "site-page";
|
|
528
672
|
|
|
529
673
|
return (
|
|
530
|
-
<div class=
|
|
674
|
+
<div class={pageClass}>
|
|
531
675
|
{showHeader && (
|
|
532
676
|
<SiteHeader
|
|
533
677
|
siteName={siteName}
|
|
534
678
|
links={links}
|
|
535
679
|
currentPath={currentPath}
|
|
536
680
|
sitePathPrefix={sitePathPrefix}
|
|
537
|
-
|
|
681
|
+
basePath={basePath}
|
|
682
|
+
siteAvatarThumbUrl={siteAvatarThumbUrl}
|
|
538
683
|
showHeaderAvatar={showHeaderAvatar}
|
|
684
|
+
languageSwitcher={languageSwitcher}
|
|
539
685
|
/>
|
|
540
686
|
)}
|
|
541
687
|
|
|
@@ -582,7 +728,10 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
|
|
|
582
728
|
type="button"
|
|
583
729
|
class="site-mobile-compose-fab"
|
|
584
730
|
aria-label={newPostLabel}
|
|
585
|
-
data-
|
|
731
|
+
data-compose-open
|
|
732
|
+
{...(composeCollectionId
|
|
733
|
+
? { "data-compose-collection-id": composeCollectionId }
|
|
734
|
+
: {})}
|
|
586
735
|
>
|
|
587
736
|
<ComposeFeatherIcon class="site-mobile-compose-fab-icon" />
|
|
588
737
|
</button>
|
|
@@ -602,12 +751,21 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
|
|
|
602
751
|
|
|
603
752
|
<jant-media-lightbox />
|
|
604
753
|
<jant-text-preview />
|
|
605
|
-
{isAuthenticated &&
|
|
754
|
+
{isAuthenticated && (
|
|
755
|
+
<jant-post-menu
|
|
756
|
+
languages={JSON.stringify(composeLanguages ?? []).replace(
|
|
757
|
+
/</g,
|
|
758
|
+
"\\u003c",
|
|
759
|
+
)}
|
|
760
|
+
/>
|
|
761
|
+
)}
|
|
606
762
|
{isAuthenticated && showComposeDialog && (
|
|
607
763
|
<ComposeDialog
|
|
608
764
|
collections={collections}
|
|
609
765
|
uploadMaxFileSize={uploadMaxFileSize}
|
|
610
766
|
slashCommandDiscovered={slashCommandDiscovered}
|
|
767
|
+
languages={composeLanguages}
|
|
768
|
+
contextLanguage={composeContextLanguage}
|
|
611
769
|
/>
|
|
612
770
|
)}
|
|
613
771
|
</div>
|