@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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { readAllStylesheets } from "./helpers/component-css.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The sanctioned width breakpoints, mirroring the ledger comment in
|
|
6
|
+
* `styles/tokens.css`. Each number is one job; a job never gets a second
|
|
7
|
+
* number.
|
|
8
|
+
*
|
|
9
|
+
* This is not bookkeeping for its own sake. The composer once switched to its
|
|
10
|
+
* full-screen sheet at 700px while its own controls took their touch sizing at
|
|
11
|
+
* 760px, so a 750px window rendered a thumb-sized Post button (46px, against
|
|
12
|
+
* 33px one pixel wider) inside a windowed dialog. Nothing was wrong in either
|
|
13
|
+
* rule on its own — they had simply drifted to two numbers for one switch, and
|
|
14
|
+
* no test could see it.
|
|
15
|
+
*/
|
|
16
|
+
const LEDGER = [
|
|
17
|
+
480, // Header hamburger; the composer's post-meta pill sheds its words.
|
|
18
|
+
580, // Nav collapse tier-sm.
|
|
19
|
+
640, // Media grid and lightbox chrome.
|
|
20
|
+
700, // The phone switch.
|
|
21
|
+
780, // Nav collapse tier-md.
|
|
22
|
+
960, // Nav collapse tier-lg.
|
|
23
|
+
1024, // Tufte two-column → single-column.
|
|
24
|
+
1079, // Collections sidebar drops out.
|
|
25
|
+
1200, // Header search collapses to an icon.
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Height breakpoints are a separate axis with a separate job: a short viewport
|
|
30
|
+
* has no room for the composer's chrome regardless of how wide it is. One
|
|
31
|
+
* number, stated on both sides.
|
|
32
|
+
*/
|
|
33
|
+
const HEIGHT_LEDGER = [760] as const;
|
|
34
|
+
|
|
35
|
+
interface Condition {
|
|
36
|
+
/** The stylesheet the condition appears in. */
|
|
37
|
+
file: string;
|
|
38
|
+
/** 1-indexed line of the `@media` prelude the condition sits in. */
|
|
39
|
+
line: number;
|
|
40
|
+
/** `min` or `max`. */
|
|
41
|
+
bound: "min" | "max";
|
|
42
|
+
/** `width` or `height`. */
|
|
43
|
+
axis: "width" | "height";
|
|
44
|
+
/** The pixel value as written. */
|
|
45
|
+
value: number;
|
|
46
|
+
/** The condition as written, for failure messages. */
|
|
47
|
+
text: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Every `(min-width: …)` / `(max-width: …)` / `(min-height: …)` /
|
|
52
|
+
* `(max-height: …)` condition in an `@media` prelude across the hand-written
|
|
53
|
+
* stylesheets.
|
|
54
|
+
*
|
|
55
|
+
* `@media` only. A `@container` query carrying the same words measures its
|
|
56
|
+
* container, not the viewport, so it answers to the component it wraps rather
|
|
57
|
+
* than to this ledger — the archive filter bar splits on its own inline size
|
|
58
|
+
* at 39rem and is right to.
|
|
59
|
+
*
|
|
60
|
+
* A prelude may span several lines (the composer's full-screen sheet lists
|
|
61
|
+
* three conditions), so the line number is counted to the condition itself
|
|
62
|
+
* rather than to the `@media` that opens it.
|
|
63
|
+
*/
|
|
64
|
+
function collectConditions(): Condition[] {
|
|
65
|
+
const found: Condition[] = [];
|
|
66
|
+
|
|
67
|
+
for (const { name, css } of readAllStylesheets()) {
|
|
68
|
+
const preludes = /@media\b([^{;]*)/g;
|
|
69
|
+
let prelude: RegExpExecArray | null;
|
|
70
|
+
|
|
71
|
+
while ((prelude = preludes.exec(css)) !== null) {
|
|
72
|
+
const offset = prelude.index + "@media".length;
|
|
73
|
+
const pattern = /\((min|max)-(width|height):\s*([^)]+)\)/g;
|
|
74
|
+
let match: RegExpExecArray | null;
|
|
75
|
+
|
|
76
|
+
while ((match = pattern.exec(prelude[1])) !== null) {
|
|
77
|
+
const [text, bound, axis, raw] = match;
|
|
78
|
+
const px = /^(\d+(?:\.\d+)?)px$/.exec(raw.trim());
|
|
79
|
+
|
|
80
|
+
found.push({
|
|
81
|
+
file: name,
|
|
82
|
+
line: css.slice(0, offset + match.index).split("\n").length,
|
|
83
|
+
bound: bound as "min" | "max",
|
|
84
|
+
axis: axis as "width" | "height",
|
|
85
|
+
// A non-px unit lands as NaN and fails the ledger check by name,
|
|
86
|
+
// which is the point: `rem` in a media query resolves against the
|
|
87
|
+
// initial font-size rather than the root this document sets, so the
|
|
88
|
+
// number it reads as is not the number it is.
|
|
89
|
+
value: px ? Number(px[1]) : Number.NaN,
|
|
90
|
+
text,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return found;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The breakpoint a condition belongs to. `min-width: 700px` and
|
|
101
|
+
* `max-width: 699px` are the two sides of the same 700px switch, so the `max`
|
|
102
|
+
* side is stated one pixel below the number it divides on.
|
|
103
|
+
*/
|
|
104
|
+
function breakpointOf(condition: Condition): number {
|
|
105
|
+
return condition.bound === "max" ? condition.value + 1 : condition.value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
describe("stylesheet breakpoints", () => {
|
|
109
|
+
it("uses only the breakpoints on the ledger", () => {
|
|
110
|
+
const strays = collectConditions()
|
|
111
|
+
.filter((condition) => {
|
|
112
|
+
const ledger = condition.axis === "width" ? LEDGER : HEIGHT_LEDGER;
|
|
113
|
+
// Both spellings of a breakpoint are accepted here — `max-width: 640px`
|
|
114
|
+
// and `max-width: 639px` are the same switch. Which one a file may use
|
|
115
|
+
// is settled by the overlap test below.
|
|
116
|
+
return !(
|
|
117
|
+
ledger.includes(condition.value as never) ||
|
|
118
|
+
ledger.includes(breakpointOf(condition) as never)
|
|
119
|
+
);
|
|
120
|
+
})
|
|
121
|
+
.map(
|
|
122
|
+
(condition) =>
|
|
123
|
+
`${condition.file}:${condition.line} ${condition.text}` +
|
|
124
|
+
` — not on the ledger in styles/tokens.css`,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(strays).toEqual([]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("never lets both sides of a breakpoint match the same viewport", () => {
|
|
131
|
+
const conditions = collectConditions();
|
|
132
|
+
const overlaps: string[] = [];
|
|
133
|
+
|
|
134
|
+
for (const axis of ["width", "height"] as const) {
|
|
135
|
+
const onAxis = conditions.filter((condition) => condition.axis === axis);
|
|
136
|
+
|
|
137
|
+
for (const value of new Set(
|
|
138
|
+
onAxis.filter((c) => c.bound === "min").map((c) => c.value),
|
|
139
|
+
)) {
|
|
140
|
+
// `min-*: N` and `max-*: N` both match at exactly N. Wherever a
|
|
141
|
+
// breakpoint is written from both sides, the lower side has to sit one
|
|
142
|
+
// pixel below it — otherwise a viewport of exactly N gets the phone
|
|
143
|
+
// layout and the desktop spacing at once.
|
|
144
|
+
const clash = onAxis.filter(
|
|
145
|
+
(condition) => condition.bound === "max" && condition.value === value,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
overlaps.push(
|
|
149
|
+
...clash.map(
|
|
150
|
+
(condition) =>
|
|
151
|
+
`${condition.file}:${condition.line} ${condition.text}` +
|
|
152
|
+
` overlaps (min-${axis}: ${value}px) at exactly ${value}px` +
|
|
153
|
+
` — write it as (max-${axis}: ${value - 1}px)`,
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
expect(overlaps).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("keeps the phone switch on one number", () => {
|
|
163
|
+
// The regression this file exists for. Touch sizing and the mobile type
|
|
164
|
+
// scale used to ride on 760px while the layout they sit inside switched at
|
|
165
|
+
// 700px; anything between the two was a desktop window wearing phone
|
|
166
|
+
// controls. Any width breakpoint in the 700s that is not the nav ladder's
|
|
167
|
+
// 780px is that drift coming back.
|
|
168
|
+
const nearMisses = collectConditions()
|
|
169
|
+
.filter(
|
|
170
|
+
(condition) =>
|
|
171
|
+
condition.axis === "width" &&
|
|
172
|
+
condition.value > 700 &&
|
|
173
|
+
condition.value < 780,
|
|
174
|
+
)
|
|
175
|
+
.map(
|
|
176
|
+
(condition) =>
|
|
177
|
+
`${condition.file}:${condition.line} ${condition.text}` +
|
|
178
|
+
` — the phone switch is 700px; see the ledger in styles/tokens.css`,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(nearMisses).toEqual([]);
|
|
182
|
+
});
|
|
183
|
+
});
|
package/src/app.tsx
CHANGED
|
@@ -16,11 +16,13 @@ import { hostedDomainCheckRoutes } from "./routes/hosted/domain-check.js";
|
|
|
16
16
|
|
|
17
17
|
// Routes - Pages
|
|
18
18
|
import { homeRoutes } from "./routes/pages/home.js";
|
|
19
|
+
import { languageRoutes } from "./routes/pages/language.js";
|
|
19
20
|
import { pageRoutes } from "./routes/pages/page.js";
|
|
20
21
|
import { collectionRoutes } from "./routes/pages/collection.js";
|
|
21
22
|
import { archiveRoutes } from "./routes/pages/archive.js";
|
|
22
23
|
import { searchRoutes } from "./routes/pages/search.js";
|
|
23
24
|
import { featuredRoutes } from "./routes/pages/featured.js";
|
|
25
|
+
import { subscribeRoutes } from "./routes/pages/subscribe.js";
|
|
24
26
|
import { latestRoutes } from "./routes/pages/latest.js";
|
|
25
27
|
import { collectionsPageRoutes } from "./routes/pages/collections.js";
|
|
26
28
|
import { newPostRoutes } from "./routes/pages/new.js";
|
|
@@ -37,6 +39,7 @@ import { postsApiRoutes } from "./routes/api/posts.js";
|
|
|
37
39
|
import { attachmentsApiRoutes } from "./routes/api/attachments.js";
|
|
38
40
|
import { navItemsApiRoutes } from "./routes/api/nav-items.js";
|
|
39
41
|
import { collectionsApiRoutes } from "./routes/api/collections.js";
|
|
42
|
+
import { smartCollectionsApiRoutes } from "./routes/api/smart-collections.js";
|
|
40
43
|
import { settingsApiRoutes } from "./routes/api/settings.js";
|
|
41
44
|
import { uploadApiRoutes } from "./routes/api/upload.js";
|
|
42
45
|
import { uploadsApiRoutes } from "./routes/api/uploads.js";
|
|
@@ -59,6 +62,7 @@ import { internalPostBodyHtmlRoutes } from "./routes/api/internal/post-body-html
|
|
|
59
62
|
import { internalUploadsRoutes } from "./routes/api/internal/uploads.js";
|
|
60
63
|
import { publicArchiveApiRoutes } from "./routes/api/public/archive.js";
|
|
61
64
|
import { publicPostsApiRoutes } from "./routes/api/public/posts.js";
|
|
65
|
+
import { discoverApiRoutes } from "./routes/api/discover.js";
|
|
62
66
|
// Routes - Compose
|
|
63
67
|
import { composeRoutes } from "./routes/compose.js";
|
|
64
68
|
|
|
@@ -71,7 +75,7 @@ import { siteSkillRoutes } from "./routes/site-skill.js";
|
|
|
71
75
|
// Middleware
|
|
72
76
|
import { requireAuth } from "./middleware/auth.js";
|
|
73
77
|
import { attachSession } from "./middleware/session.js";
|
|
74
|
-
import { defaultCacheControl } from "./middleware/cache-control.js";
|
|
78
|
+
import { defaultCacheControl, noStore } from "./middleware/cache-control.js";
|
|
75
79
|
import { requireRssFeedsEnabled } from "./middleware/public-content-access.js";
|
|
76
80
|
import { requireOnboarding } from "./middleware/onboarding.js";
|
|
77
81
|
import { errorHandler } from "./middleware/error-handler.js";
|
|
@@ -90,6 +94,10 @@ import {
|
|
|
90
94
|
import { isAssetPath } from "./lib/asset-path.js";
|
|
91
95
|
import { getHostedCanonicalRedirect } from "./lib/hosted-domain.js";
|
|
92
96
|
import { stripSitePathPrefix, toPublicHref } from "./lib/url.js";
|
|
97
|
+
import {
|
|
98
|
+
matchesIfNoneMatch,
|
|
99
|
+
withConditionalResponse,
|
|
100
|
+
} from "./lib/http-cache.js";
|
|
93
101
|
import { withWorkerResponseCache } from "./lib/worker-response-cache.js";
|
|
94
102
|
import { createRequestRuntime } from "./runtime/index.js";
|
|
95
103
|
import { getInstanceReadiness } from "./runtime/readiness.js";
|
|
@@ -265,22 +273,30 @@ export function createApp(): App {
|
|
|
265
273
|
const app = new Hono<{ Bindings: Bindings; Variables: AppVariables }>();
|
|
266
274
|
const defaultFetch = app.fetch.bind(app);
|
|
267
275
|
|
|
268
|
-
app.fetch = (request, env, executionCtx) => {
|
|
276
|
+
app.fetch = async (request, env, executionCtx) => {
|
|
269
277
|
const bindings = env as Bindings | undefined;
|
|
270
278
|
const preparedRequest = prepareRequestForRouting(
|
|
271
279
|
request,
|
|
272
280
|
getConfiguredSingleSitePathPrefix(bindings),
|
|
273
281
|
);
|
|
274
282
|
if (preparedRequest instanceof Response) {
|
|
275
|
-
return
|
|
283
|
+
return preparedRequest;
|
|
276
284
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
285
|
+
// Revalidation sits outside the response cache, so a reader holding the
|
|
286
|
+
// current version is answered from a cache hit as cheaply as from a miss,
|
|
287
|
+
// and a miss still stores the full document on its way out.
|
|
288
|
+
return withConditionalResponse(
|
|
280
289
|
request,
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
290
|
+
await withWorkerResponseCache({
|
|
291
|
+
bindings,
|
|
292
|
+
executionCtx,
|
|
293
|
+
request,
|
|
294
|
+
next: () =>
|
|
295
|
+
Promise.resolve(
|
|
296
|
+
defaultFetch(preparedRequest, bindings, executionCtx),
|
|
297
|
+
),
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
284
300
|
};
|
|
285
301
|
|
|
286
302
|
// Global error handler: maps DomainError → HTTP responses
|
|
@@ -307,7 +323,11 @@ export function createApp(): App {
|
|
|
307
323
|
if (startupConfigError) {
|
|
308
324
|
return c.html(startupConfigError, 500);
|
|
309
325
|
}
|
|
310
|
-
const runtime = await createRequestRuntime(
|
|
326
|
+
const runtime = await createRequestRuntime(
|
|
327
|
+
c.env,
|
|
328
|
+
publicRequestUrl,
|
|
329
|
+
c.req.raw,
|
|
330
|
+
);
|
|
311
331
|
c.set("services", runtime.services);
|
|
312
332
|
c.set("servicesForSite", runtime.servicesForSite);
|
|
313
333
|
c.set("hostedHandoff", runtime.hostedHandoff);
|
|
@@ -324,12 +344,35 @@ export function createApp(): App {
|
|
|
324
344
|
// downstream handlers don't each call auth.api.getSession themselves.
|
|
325
345
|
app.use("*", attachSession());
|
|
326
346
|
|
|
327
|
-
// Default every response without an explicit Cache-Control
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
347
|
+
// Default every response without an explicit Cache-Control: `private,
|
|
348
|
+
// no-cache` for an anonymous reader, `private, no-store` once signed in.
|
|
349
|
+
// Jant pages are auth-variant, so a shared/CDN cache must never store them
|
|
350
|
+
// — that is what `private` is for in both — while a reader's own browser
|
|
351
|
+
// keeps the page and its back/forward cache. Routes serving genuinely public
|
|
352
|
+
// resources (media, feeds, sitemaps, favicons) set their own Cache-Control
|
|
353
|
+
// and are untouched.
|
|
331
354
|
app.use("*", defaultCacheControl());
|
|
332
355
|
|
|
356
|
+
// Credential surfaces override that default. Each of these is served to
|
|
357
|
+
// someone with no session yet, so the anonymous branch above would let the
|
|
358
|
+
// browser keep it — and its URL — on disk: a reset link's one-time token, a
|
|
359
|
+
// sign-in form, an SSO handoff. One list rather than a `use()` beside each
|
|
360
|
+
// route group, because it has to be registered up here anyway: `/api/auth/*`
|
|
361
|
+
// ends in a terminal handler that never yields, so a middleware registered
|
|
362
|
+
// after it never runs. The handlers themselves live under "Auth routes"
|
|
363
|
+
// below and in `app.all("/api/auth/*")`; a new one belongs in both places.
|
|
364
|
+
for (const path of [
|
|
365
|
+
"/setup",
|
|
366
|
+
"/signin",
|
|
367
|
+
"/signout",
|
|
368
|
+
"/reset",
|
|
369
|
+
"/__sso",
|
|
370
|
+
"/__dev/login",
|
|
371
|
+
"/api/auth/*",
|
|
372
|
+
]) {
|
|
373
|
+
app.use(path, noStore());
|
|
374
|
+
}
|
|
375
|
+
|
|
333
376
|
app.use("*", async (c, next) => {
|
|
334
377
|
const redirectUrl = await getHostedCanonicalRedirect({
|
|
335
378
|
currentSite: c.var.currentSite,
|
|
@@ -371,8 +414,10 @@ export function createApp(): App {
|
|
|
371
414
|
const storage = c.var.storage;
|
|
372
415
|
if (!storage) return c.notFound();
|
|
373
416
|
|
|
417
|
+
// The outer conditional layer would answer this too, but only after the
|
|
418
|
+
// object has been fetched and rendered; matching here skips that work.
|
|
374
419
|
const etag = `"${media.updatedAt}"`;
|
|
375
|
-
if (c.req.header("If-None-Match")
|
|
420
|
+
if (matchesIfNoneMatch(c.req.header("If-None-Match"), etag)) {
|
|
376
421
|
return new Response(null, { status: 304, headers: { ETag: etag } });
|
|
377
422
|
}
|
|
378
423
|
|
|
@@ -529,15 +574,18 @@ export function createApp(): App {
|
|
|
529
574
|
// API Routes
|
|
530
575
|
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
531
576
|
app.route("/api/public/archive", publicArchiveApiRoutes);
|
|
577
|
+
app.route("/api/discover", discoverApiRoutes);
|
|
532
578
|
app.route("/api/posts", postsApiRoutes);
|
|
533
579
|
app.route("/api/nav-items", navItemsApiRoutes);
|
|
534
580
|
app.route("/api/collections", collectionsApiRoutes);
|
|
581
|
+
app.route("/api/smart-collections", smartCollectionsApiRoutes);
|
|
535
582
|
app.route("/api/settings", settingsApiRoutes);
|
|
536
583
|
app.route("/api/custom-urls", customUrlsApiRoutes);
|
|
537
584
|
app.route("/api/export", exportApiRoutes);
|
|
538
585
|
app.route("/api/github-sync", githubSyncAdminRoutes);
|
|
539
586
|
|
|
540
|
-
// Auth routes
|
|
587
|
+
// Auth routes. Their paths are also listed in the `noStore()` block above,
|
|
588
|
+
// which is what keeps a credential page out of the browser's disk cache.
|
|
541
589
|
app.route("/", setupRoutes);
|
|
542
590
|
app.route("/", signinRoutes);
|
|
543
591
|
app.route("/", resetRoutes);
|
|
@@ -571,6 +619,7 @@ export function createApp(): App {
|
|
|
571
619
|
app.route("/", newPostRoutes);
|
|
572
620
|
app.route("/archive", archiveRoutes);
|
|
573
621
|
app.route("/featured", featuredRoutes);
|
|
622
|
+
app.route("/subscribe", subscribeRoutes);
|
|
574
623
|
app.route("/latest", latestRoutes);
|
|
575
624
|
app.route("/", partialPageRoutes);
|
|
576
625
|
app.route("/_", brandRoutes);
|
|
@@ -579,6 +628,12 @@ export function createApp(): App {
|
|
|
579
628
|
app.route("/collections", collectionRoutes);
|
|
580
629
|
app.route("/", homeRoutes);
|
|
581
630
|
|
|
631
|
+
// Per-language views of every surface above. Mounted here on purpose: after
|
|
632
|
+
// the static route groups, so `/archive` is the archive rather than a
|
|
633
|
+
// language named "archive", and before the catch-all, so `/en` is a language
|
|
634
|
+
// view rather than a lookup for a post whose slug is "en".
|
|
635
|
+
app.route("/:lang", languageRoutes);
|
|
636
|
+
|
|
582
637
|
// Custom page catch-all (must be last)
|
|
583
638
|
app.route("/", pageRoutes);
|
|
584
639
|
|
package/src/auth.ts
CHANGED
|
@@ -66,7 +66,24 @@ export function createAuth(
|
|
|
66
66
|
},
|
|
67
67
|
},
|
|
68
68
|
session: {
|
|
69
|
-
|
|
69
|
+
// An idle window, not a hard cap. `attachSession` writes better-auth's
|
|
70
|
+
// re-issued cookie back to the browser, so any visit inside the window
|
|
71
|
+
// pushes both the stored session and the cookie out another 90 days —
|
|
72
|
+
// a single author on their own machine should effectively never be
|
|
73
|
+
// asked to sign in again.
|
|
74
|
+
expiresIn: 3600 * 24 * 90, // 90 days
|
|
75
|
+
// How stale a session may get before a read renews it. Same as
|
|
76
|
+
// better-auth's default, but stated here because it sets the renewal
|
|
77
|
+
// cadence and that shouldn't be invisible library behaviour.
|
|
78
|
+
updateAge: 3600 * 24, // 1 day
|
|
79
|
+
// Turns off better-auth's re-authentication gate, whose default lets a
|
|
80
|
+
// session read `/list-sessions` only within 24 hours of sign-in — and it
|
|
81
|
+
// measures that from `createdAt`, which renewal never moves, so the
|
|
82
|
+
// Sessions page would answer "Session is not fresh" from day two onward.
|
|
83
|
+
// The gate also guards nothing here: revoking a session, the destructive
|
|
84
|
+
// half of that page, is not behind it, and the endpoints that would care
|
|
85
|
+
// (delete-user, unlink-account) are not part of Jant.
|
|
86
|
+
freshAge: 0,
|
|
70
87
|
cookieCache: {
|
|
71
88
|
enabled: true,
|
|
72
89
|
maxAge: 60 * 5, // 5 minutes
|
|
@@ -11,6 +11,10 @@ vi.mock("../confirm.js", () => ({
|
|
|
11
11
|
showConfirmDialog: vi.fn(),
|
|
12
12
|
}));
|
|
13
13
|
|
|
14
|
+
vi.mock("../collection-dialog-host.js", () => ({
|
|
15
|
+
openCollectionDialog: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
14
18
|
function createMarkup() {
|
|
15
19
|
document.body.innerHTML = `
|
|
16
20
|
<div id="toast-container"></div>
|
|
@@ -29,9 +33,9 @@ function createMarkup() {
|
|
|
29
33
|
More actions
|
|
30
34
|
</button>
|
|
31
35
|
<div data-collection-page-menu hidden>
|
|
32
|
-
<
|
|
36
|
+
<button type="button" role="menuitem" data-collection-page-action="edit">
|
|
33
37
|
Edit
|
|
34
|
-
</
|
|
38
|
+
</button>
|
|
35
39
|
<button
|
|
36
40
|
type="button"
|
|
37
41
|
role="menuitem"
|
|
@@ -132,6 +136,51 @@ describe("collection detail page actions", () => {
|
|
|
132
136
|
expect(window.location.pathname).toBe("/collections");
|
|
133
137
|
});
|
|
134
138
|
|
|
139
|
+
it("edits the collection in the dialog, and follows a moved address", async () => {
|
|
140
|
+
createMarkup();
|
|
141
|
+
const { openCollectionDialog } =
|
|
142
|
+
await import("../collection-dialog-host.js");
|
|
143
|
+
vi.mocked(openCollectionDialog).mockResolvedValue({
|
|
144
|
+
changed: true,
|
|
145
|
+
collection: { id: "collection-1", slug: "renamed", title: "Renamed" },
|
|
146
|
+
});
|
|
147
|
+
await import("../collection-page-actions.js");
|
|
148
|
+
|
|
149
|
+
document
|
|
150
|
+
.querySelector<HTMLElement>("[data-collection-page-action='toggle-menu']")
|
|
151
|
+
?.click();
|
|
152
|
+
document
|
|
153
|
+
.querySelector<HTMLElement>("[data-collection-page-action='edit']")
|
|
154
|
+
?.click();
|
|
155
|
+
await flushAsyncWork();
|
|
156
|
+
|
|
157
|
+
expect(openCollectionDialog).toHaveBeenCalledWith({
|
|
158
|
+
collectionId: "collection-1",
|
|
159
|
+
});
|
|
160
|
+
expect(window.location.pathname).toBe("/renamed");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("leaves the page where it is when the dialog is dismissed", async () => {
|
|
164
|
+
createMarkup();
|
|
165
|
+
const { openCollectionDialog } =
|
|
166
|
+
await import("../collection-dialog-host.js");
|
|
167
|
+
vi.mocked(openCollectionDialog).mockResolvedValue({ changed: false });
|
|
168
|
+
const reload = vi.fn();
|
|
169
|
+
vi.spyOn(window.location, "reload").mockImplementation(reload);
|
|
170
|
+
await import("../collection-page-actions.js");
|
|
171
|
+
|
|
172
|
+
document
|
|
173
|
+
.querySelector<HTMLElement>("[data-collection-page-action='toggle-menu']")
|
|
174
|
+
?.click();
|
|
175
|
+
document
|
|
176
|
+
.querySelector<HTMLElement>("[data-collection-page-action='edit']")
|
|
177
|
+
?.click();
|
|
178
|
+
await flushAsyncWork();
|
|
179
|
+
|
|
180
|
+
expect(reload).not.toHaveBeenCalled();
|
|
181
|
+
expect(window.location.pathname).toBe("/original-slug");
|
|
182
|
+
});
|
|
183
|
+
|
|
135
184
|
it("adds the collection to navigation and reveals the settings action", async () => {
|
|
136
185
|
createMarkup();
|
|
137
186
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Selecting a photo used to fetch the 3MB HEIC decoder and read the whole
|
|
5
|
+
* file into memory just to check four bytes of it, then decode the processed
|
|
6
|
+
* image a second time for its blurhash. The bridge now sniffs the header
|
|
7
|
+
* itself, imports `heic-to` only for a real HEIC, and takes the blurhash the
|
|
8
|
+
* processor already sampled.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import type {
|
|
13
|
+
ComposeAttachment,
|
|
14
|
+
ComposeLabels,
|
|
15
|
+
} from "../components/compose-types.js";
|
|
16
|
+
import type { JantComposeEditor } from "../components/jant-compose-editor.js";
|
|
17
|
+
|
|
18
|
+
const processToFile = vi.fn();
|
|
19
|
+
const uploadViaSession = vi.fn();
|
|
20
|
+
const heicTo = vi.fn();
|
|
21
|
+
const heicToImports = { count: 0 };
|
|
22
|
+
|
|
23
|
+
vi.mock("../image-processor.js", () => ({
|
|
24
|
+
ImageProcessor: {
|
|
25
|
+
processToFile: (...args: unknown[]) => processToFile(...args),
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock("../upload-session.js", () => ({
|
|
30
|
+
uploadViaSession: (...args: unknown[]) => uploadViaSession(...args),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock("heic-to", () => {
|
|
34
|
+
heicToImports.count += 1;
|
|
35
|
+
return { heicTo: (...args: unknown[]) => heicTo(...args) };
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await import("../compose-bridge.js");
|
|
39
|
+
await import("../components/jant-compose-editor.js");
|
|
40
|
+
|
|
41
|
+
const PROCESSED = new File(["webp"], "photo.webp", { type: "image/webp" });
|
|
42
|
+
const BLURHASH = "LEHV6nWB2yk8pyo0adR*.7kCMdnj";
|
|
43
|
+
|
|
44
|
+
function jpegFile() {
|
|
45
|
+
return new File(
|
|
46
|
+
[new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0, 0, 0, 0])],
|
|
47
|
+
"photo.jpg",
|
|
48
|
+
{
|
|
49
|
+
type: "image/jpeg",
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function heicFile() {
|
|
55
|
+
const bytes = new Uint8Array(16);
|
|
56
|
+
bytes.set([...new TextEncoder().encode("ftyp")], 4);
|
|
57
|
+
bytes.set([...new TextEncoder().encode("heic")], 8);
|
|
58
|
+
return new File([bytes], "IMG_0001.HEIC", { type: "image/heic" });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function selectFile(file: File) {
|
|
62
|
+
document.dispatchEvent(
|
|
63
|
+
new CustomEvent("jant:files-selected", {
|
|
64
|
+
detail: { files: [{ file, clientId: "attachment-1" }] },
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function settle() {
|
|
70
|
+
for (let i = 0; i < 10; i++) {
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("compose bridge image upload", () => {
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
processToFile.mockReset().mockResolvedValue({
|
|
78
|
+
file: PROCESSED,
|
|
79
|
+
width: 1200,
|
|
80
|
+
height: 900,
|
|
81
|
+
blurhash: BLURHASH,
|
|
82
|
+
});
|
|
83
|
+
uploadViaSession.mockReset().mockResolvedValue({
|
|
84
|
+
id: "med_1",
|
|
85
|
+
filename: "photo.webp",
|
|
86
|
+
url: "/media/photo.webp",
|
|
87
|
+
mimeType: "image/webp",
|
|
88
|
+
size: 4,
|
|
89
|
+
});
|
|
90
|
+
heicTo
|
|
91
|
+
.mockReset()
|
|
92
|
+
.mockResolvedValue(new Blob(["jpeg"], { type: "image/jpeg" }));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("uploads a JPEG with the processor's blurhash and never loads heic-to", async () => {
|
|
96
|
+
const file = jpegFile();
|
|
97
|
+
|
|
98
|
+
selectFile(file);
|
|
99
|
+
await settle();
|
|
100
|
+
|
|
101
|
+
expect(heicToImports.count).toBe(0);
|
|
102
|
+
expect(heicTo).not.toHaveBeenCalled();
|
|
103
|
+
expect(processToFile).toHaveBeenCalledWith(file);
|
|
104
|
+
expect(uploadViaSession).toHaveBeenCalledTimes(1);
|
|
105
|
+
expect(uploadViaSession.mock.calls[0][0]).toBe(PROCESSED);
|
|
106
|
+
expect(uploadViaSession.mock.calls[0][1]).toMatchObject({
|
|
107
|
+
width: 1200,
|
|
108
|
+
height: 900,
|
|
109
|
+
blurhash: BLURHASH,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("converts a file whose bytes say HEIC before processing it", async () => {
|
|
114
|
+
selectFile(heicFile());
|
|
115
|
+
await settle();
|
|
116
|
+
|
|
117
|
+
expect(heicToImports.count).toBe(1);
|
|
118
|
+
expect(heicTo).toHaveBeenCalledWith(
|
|
119
|
+
expect.objectContaining({ type: "image/jpeg" }),
|
|
120
|
+
);
|
|
121
|
+
const converted = processToFile.mock.calls[0][0] as File;
|
|
122
|
+
expect(converted.name).toBe("IMG_0001.jpg");
|
|
123
|
+
expect(converted.type).toBe("image/jpeg");
|
|
124
|
+
expect(uploadViaSession.mock.calls[0][1]).toMatchObject({
|
|
125
|
+
blurhash: BLURHASH,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("reports to the editor that took the post over, not the one it started in", async () => {
|
|
130
|
+
// Entering or leaving thread mode renders the post in a new editor. An
|
|
131
|
+
// upload still running then used to keep reporting to the old one, so the
|
|
132
|
+
// new editor showed "uploading" forever and the file never reached the
|
|
133
|
+
// local draft.
|
|
134
|
+
let finishUpload: (value: unknown) => void = () => {};
|
|
135
|
+
uploadViaSession.mockReset().mockReturnValue(
|
|
136
|
+
new Promise((resolve) => {
|
|
137
|
+
finishUpload = resolve;
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
const editor = async () => {
|
|
141
|
+
const el = document.createElement(
|
|
142
|
+
"jant-compose-editor",
|
|
143
|
+
) as JantComposeEditor;
|
|
144
|
+
el.labels = {} as ComposeLabels;
|
|
145
|
+
document.body.appendChild(el);
|
|
146
|
+
await el.updateComplete;
|
|
147
|
+
return el;
|
|
148
|
+
};
|
|
149
|
+
const file = jpegFile();
|
|
150
|
+
const pending: ComposeAttachment = {
|
|
151
|
+
clientId: "attachment-1",
|
|
152
|
+
file,
|
|
153
|
+
previewUrl: "blob:preview",
|
|
154
|
+
posterUrl: null,
|
|
155
|
+
status: "pending",
|
|
156
|
+
progress: null,
|
|
157
|
+
mediaId: null,
|
|
158
|
+
remoteUrl: null,
|
|
159
|
+
alt: "",
|
|
160
|
+
error: null,
|
|
161
|
+
summary: null,
|
|
162
|
+
chars: null,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const started = await editor();
|
|
166
|
+
started._attachments = [pending];
|
|
167
|
+
started._attachmentOrder = [pending.clientId];
|
|
168
|
+
started.dispatchEvent(
|
|
169
|
+
new CustomEvent("jant:files-selected", {
|
|
170
|
+
bubbles: true,
|
|
171
|
+
detail: { files: [{ file, clientId: pending.clientId }] },
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
await settle();
|
|
175
|
+
expect(uploadViaSession).toHaveBeenCalledTimes(1);
|
|
176
|
+
|
|
177
|
+
started.remove();
|
|
178
|
+
const next = await editor();
|
|
179
|
+
next.takeOver(started);
|
|
180
|
+
|
|
181
|
+
finishUpload({
|
|
182
|
+
id: "med_1",
|
|
183
|
+
filename: "photo.webp",
|
|
184
|
+
url: "/media/photo.webp",
|
|
185
|
+
mimeType: "image/webp",
|
|
186
|
+
size: 4,
|
|
187
|
+
});
|
|
188
|
+
await settle();
|
|
189
|
+
|
|
190
|
+
expect(next._attachments[0]).toMatchObject({
|
|
191
|
+
status: "done",
|
|
192
|
+
mediaId: "med_1",
|
|
193
|
+
remoteUrl: "/media/photo.webp",
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|