@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
package/src/db/schema.ts
CHANGED
|
@@ -15,26 +15,37 @@ import {
|
|
|
15
15
|
check,
|
|
16
16
|
} from "drizzle-orm/sqlite-core";
|
|
17
17
|
import { sql } from "drizzle-orm";
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
18
|
+
import {
|
|
19
|
+
ARCHIVE_LAYOUTS,
|
|
20
|
+
COLLECTION_DIRECTORY_ENTRY_TYPES,
|
|
21
|
+
COLLECTION_SORT_ORDERS,
|
|
22
|
+
CONTENT_DISPOSITIONS,
|
|
23
|
+
EARLIEST_FILTERABLE_YEAR,
|
|
24
|
+
FORMATS,
|
|
25
|
+
GITHUB_APP_ACCOUNT_TYPES,
|
|
26
|
+
LATEST_FILTERABLE_YEAR,
|
|
27
|
+
NAV_ITEM_PLACEMENTS,
|
|
28
|
+
NAV_ITEM_TYPES,
|
|
29
|
+
PATH_KINDS,
|
|
30
|
+
PUBLIC_ARCHIVE_VISIBILITIES,
|
|
31
|
+
SITE_DOMAIN_KINDS,
|
|
32
|
+
SITE_MEMBER_ROLES,
|
|
33
|
+
SITE_STATUSES,
|
|
34
|
+
SMART_COLLECTION_SORT_ORDERS,
|
|
35
|
+
STATUSES,
|
|
36
|
+
SYSTEM_NAV_KEY_VALUES,
|
|
37
|
+
UPLOAD_SESSION_STATES,
|
|
38
|
+
VISIBILITIES,
|
|
39
|
+
} from "../types/constants.js";
|
|
37
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Render a value list as the `IN (...)` body of a CHECK constraint.
|
|
43
|
+
*
|
|
44
|
+
* Every list it is handed comes from `types/constants.ts`. Declaring one
|
|
45
|
+
* locally instead is how a CHECK once shipped missing an enum value: the
|
|
46
|
+
* shared list grew, the schema's private copy did not, and the generated
|
|
47
|
+
* constraint rejected rows the application treats as valid.
|
|
48
|
+
*/
|
|
38
49
|
function sqlTextEnum(values: readonly string[]) {
|
|
39
50
|
return sql.raw(values.map((value) => `'${value}'`).join(", "));
|
|
40
51
|
}
|
|
@@ -164,6 +175,27 @@ export const posts = sqliteTable(
|
|
|
164
175
|
previewProvider: text("preview_provider"),
|
|
165
176
|
replyToId: text("reply_to_id"),
|
|
166
177
|
threadId: text("thread_id").notNull(),
|
|
178
|
+
/**
|
|
179
|
+
* BCP 47 content language in canonical form (`en`, `zh-Hans`).
|
|
180
|
+
*
|
|
181
|
+
* Uniform across a Thread: replies inherit the Root's value on creation and
|
|
182
|
+
* a language change rewrites every row in the Thread. Language filters can
|
|
183
|
+
* therefore be plain column predicates even on member-grained queries.
|
|
184
|
+
*
|
|
185
|
+
* NULL only exists before multilingual content is first enabled; enabling
|
|
186
|
+
* stamps every NULL row with the primary language.
|
|
187
|
+
*/
|
|
188
|
+
language: text("language"),
|
|
189
|
+
/**
|
|
190
|
+
* Shared translation-group key (TypeID, `tgr_` prefix). Rows sharing a value
|
|
191
|
+
* are translations of one another — one row per language, no direction.
|
|
192
|
+
*
|
|
193
|
+
* Only ever set on Thread Roots (`thread_id = id`); enforced in the post
|
|
194
|
+
* service rather than by a table CHECK, because adding a CHECK to `post`
|
|
195
|
+
* would make drizzle-kit rebuild the table and silently drop the FTS
|
|
196
|
+
* triggers (`post_ai` / `post_ad` / `post_au`) that keep search current.
|
|
197
|
+
*/
|
|
198
|
+
translationGroupId: text("translation_group_id"),
|
|
167
199
|
/**
|
|
168
200
|
* The author added this reply without announcing it. Persisted so both
|
|
169
201
|
* activity timestamps below stay recomputable from the rows alone.
|
|
@@ -246,6 +278,12 @@ export const posts = sqliteTable(
|
|
|
246
278
|
.where(
|
|
247
279
|
sql`${table.status} = 'published' AND ${table.featuredAt} IS NOT NULL`,
|
|
248
280
|
),
|
|
281
|
+
// One Post per language inside a translation group. The `site_id` prefix
|
|
282
|
+
// matches the indexing convention here and lets the same index serve
|
|
283
|
+
// "list the members of this group" without a second index.
|
|
284
|
+
uniqueIndex("uq_post_site_translation_group_language")
|
|
285
|
+
.on(table.siteId, table.translationGroupId, table.language)
|
|
286
|
+
.where(sql`${table.translationGroupId} IS NOT NULL`),
|
|
249
287
|
],
|
|
250
288
|
);
|
|
251
289
|
|
|
@@ -441,6 +479,108 @@ export const collections = sqliteTable(
|
|
|
441
479
|
],
|
|
442
480
|
);
|
|
443
481
|
|
|
482
|
+
// =============================================================================
|
|
483
|
+
// Smart Collections
|
|
484
|
+
// =============================================================================
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* A collection whose members are decided by conditions rather than by tagging.
|
|
488
|
+
*
|
|
489
|
+
* Kept apart from `collection` rather than folded into it. The two share an
|
|
490
|
+
* identity shape — a path registry row, a title, a description — but half the
|
|
491
|
+
* columns of a merged table would be meaningless for half its rows, every query
|
|
492
|
+
* would branch, and a smart collection can filter *by* a collection, which in
|
|
493
|
+
* one table becomes a self-reference and a cycle to defend against. The
|
|
494
|
+
* resemblance belongs in a read model, not in the storage.
|
|
495
|
+
*
|
|
496
|
+
* The condition columns are declared once in `lib/filter-dimensions.ts`; this
|
|
497
|
+
* table is where they land. `NULL` always means "this dimension is not part of
|
|
498
|
+
* the conditions".
|
|
499
|
+
*
|
|
500
|
+
* Never private: `visibility` is constrained to the public archive values, so
|
|
501
|
+
* there is no such thing as a smart collection page that answers 404 to a
|
|
502
|
+
* reader. Per-post visibility still applies exactly as it does on a manual
|
|
503
|
+
* collection page.
|
|
504
|
+
*
|
|
505
|
+
* `collection_id` carries no foreign key, which makes it the one id column here
|
|
506
|
+
* that does not. None of the three actions is the right one: CASCADE would
|
|
507
|
+
* delete a smart collection — a page, with an address — because a collection it
|
|
508
|
+
* filters by went away, SET NULL would silently widen it from "Rust notes" to
|
|
509
|
+
* every post, and RESTRICT can only say `FOREIGN KEY constraint failed`, which
|
|
510
|
+
* no one should ever be shown. The refusal that belongs here is
|
|
511
|
+
* `smartCollections.assertCollectionUnused()`, which names the smart
|
|
512
|
+
* collections standing in the way; a constraint underneath it would add no
|
|
513
|
+
* answer, only turn every bulk delete into an ordering rule nothing enforces.
|
|
514
|
+
*
|
|
515
|
+
* What a dangling id costs is bounded: the condition compiles to an `IN` over a
|
|
516
|
+
* collection no `thread_collection` row carries, so the smart collection matches
|
|
517
|
+
* nothing. Visible and repairable by editing the conditions — not a page that
|
|
518
|
+
* quietly grew to the whole archive.
|
|
519
|
+
*/
|
|
520
|
+
export const smartCollections = sqliteTable(
|
|
521
|
+
"smart_collection",
|
|
522
|
+
{
|
|
523
|
+
id: text("id").primaryKey(),
|
|
524
|
+
siteId: text("site_id")
|
|
525
|
+
.notNull()
|
|
526
|
+
.references(() => sites.id, { onDelete: "cascade" }),
|
|
527
|
+
title: text("title").notNull(),
|
|
528
|
+
description: text("description"),
|
|
529
|
+
|
|
530
|
+
// --- Conditions (see lib/filter-dimensions.ts) --------------------------
|
|
531
|
+
format: text("format", { enum: FORMATS }),
|
|
532
|
+
year: integer("year"),
|
|
533
|
+
// No foreign key, alone among this schema's id columns — see the note under
|
|
534
|
+
// the table on why this one is held in the service instead.
|
|
535
|
+
collectionId: text("collection_id"),
|
|
536
|
+
/** `any` | `none` | comma-joined MEDIA_KINDS. One folded vocabulary. */
|
|
537
|
+
media: text("media"),
|
|
538
|
+
hasTitle: integer("has_title", { mode: "boolean" }),
|
|
539
|
+
hasReplies: integer("has_replies", { mode: "boolean" }),
|
|
540
|
+
visibility: text("visibility", { enum: PUBLIC_ARCHIVE_VISIBILITIES }),
|
|
541
|
+
|
|
542
|
+
// --- Presentation (not conditions) --------------------------------------
|
|
543
|
+
sort: text("sort", { enum: SMART_COLLECTION_SORT_ORDERS })
|
|
544
|
+
.notNull()
|
|
545
|
+
.default("newest"),
|
|
546
|
+
/** NULL follows the site's `ARCHIVE_DEFAULT_LAYOUT`. */
|
|
547
|
+
layout: text("layout", { enum: ARCHIVE_LAYOUTS }),
|
|
548
|
+
|
|
549
|
+
createdAt: integer("created_at").notNull(),
|
|
550
|
+
updatedAt: integer("updated_at").notNull(),
|
|
551
|
+
},
|
|
552
|
+
(table) => [
|
|
553
|
+
check(
|
|
554
|
+
"chk_smart_collection_format",
|
|
555
|
+
sql`${table.format} IS NULL OR ${table.format} IN (${sqlTextEnum(FORMATS)})`,
|
|
556
|
+
),
|
|
557
|
+
check(
|
|
558
|
+
"chk_smart_collection_visibility",
|
|
559
|
+
sql`${table.visibility} IS NULL OR ${table.visibility} IN (${sqlTextEnum(PUBLIC_ARCHIVE_VISIBILITIES)})`,
|
|
560
|
+
),
|
|
561
|
+
check(
|
|
562
|
+
"chk_smart_collection_sort",
|
|
563
|
+
sql`${table.sort} IN (${sqlTextEnum(SMART_COLLECTION_SORT_ORDERS)})`,
|
|
564
|
+
),
|
|
565
|
+
check(
|
|
566
|
+
"chk_smart_collection_layout",
|
|
567
|
+
sql`${table.layout} IS NULL OR ${table.layout} IN (${sqlTextEnum(ARCHIVE_LAYOUTS)})`,
|
|
568
|
+
),
|
|
569
|
+
check(
|
|
570
|
+
"chk_smart_collection_year",
|
|
571
|
+
sql`${table.year} IS NULL OR ${table.year} BETWEEN ${sql.raw(String(EARLIEST_FILTERABLE_YEAR))} AND ${sql.raw(String(LATEST_FILTERABLE_YEAR))}`,
|
|
572
|
+
),
|
|
573
|
+
index("idx_smart_collection_site_created_at").on(
|
|
574
|
+
table.siteId,
|
|
575
|
+
table.createdAt,
|
|
576
|
+
),
|
|
577
|
+
index("idx_smart_collection_site_collection_id").on(
|
|
578
|
+
table.siteId,
|
|
579
|
+
table.collectionId,
|
|
580
|
+
),
|
|
581
|
+
],
|
|
582
|
+
);
|
|
583
|
+
|
|
444
584
|
// =============================================================================
|
|
445
585
|
// Path Registry (slug + alias + redirect)
|
|
446
586
|
// =============================================================================
|
|
@@ -462,6 +602,10 @@ export const pathRegistry = sqliteTable(
|
|
|
462
602
|
collectionId: text("collection_id").references(() => collections.id, {
|
|
463
603
|
onDelete: "cascade",
|
|
464
604
|
}),
|
|
605
|
+
smartCollectionId: text("smart_collection_id").references(
|
|
606
|
+
() => smartCollections.id,
|
|
607
|
+
{ onDelete: "cascade" },
|
|
608
|
+
),
|
|
465
609
|
redirectToPath: text("redirect_to_path"),
|
|
466
610
|
redirectType: integer("redirect_type"),
|
|
467
611
|
archiveQuery: text("archive_query"),
|
|
@@ -471,7 +615,7 @@ export const pathRegistry = sqliteTable(
|
|
|
471
615
|
(table) => [
|
|
472
616
|
check(
|
|
473
617
|
"chk_path_registry_kind",
|
|
474
|
-
sql`${table.kind} IN (
|
|
618
|
+
sql`${table.kind} IN (${sqlTextEnum(PATH_KINDS)})`,
|
|
475
619
|
),
|
|
476
620
|
uniqueIndex("uq_path_registry_site_path").on(table.siteId, table.path),
|
|
477
621
|
uniqueIndex("uq_path_registry_site_post_slug")
|
|
@@ -480,18 +624,28 @@ export const pathRegistry = sqliteTable(
|
|
|
480
624
|
uniqueIndex("uq_path_registry_site_collection_slug")
|
|
481
625
|
.on(table.siteId, table.collectionId)
|
|
482
626
|
.where(sql`${table.kind} = 'slug' AND ${table.collectionId} IS NOT NULL`),
|
|
627
|
+
uniqueIndex("uq_path_registry_site_smart_collection_slug")
|
|
628
|
+
.on(table.siteId, table.smartCollectionId)
|
|
629
|
+
.where(
|
|
630
|
+
sql`${table.kind} = 'slug' AND ${table.smartCollectionId} IS NOT NULL`,
|
|
631
|
+
),
|
|
483
632
|
index("idx_path_registry_site_post_id").on(table.siteId, table.postId),
|
|
484
633
|
index("idx_path_registry_site_collection_id").on(
|
|
485
634
|
table.siteId,
|
|
486
635
|
table.collectionId,
|
|
487
636
|
),
|
|
637
|
+
index("idx_path_registry_site_smart_collection_id").on(
|
|
638
|
+
table.siteId,
|
|
639
|
+
table.smartCollectionId,
|
|
640
|
+
),
|
|
488
641
|
check(
|
|
489
642
|
"chk_path_registry_shape",
|
|
490
643
|
sql`(
|
|
491
644
|
${table.kind} IN ('slug', 'alias')
|
|
492
645
|
AND (
|
|
493
|
-
(${table.postId} IS NOT NULL AND ${table.collectionId} IS NULL)
|
|
494
|
-
OR (${table.postId} IS NULL AND ${table.collectionId} IS NOT NULL)
|
|
646
|
+
(${table.postId} IS NOT NULL AND ${table.collectionId} IS NULL AND ${table.smartCollectionId} IS NULL)
|
|
647
|
+
OR (${table.postId} IS NULL AND ${table.collectionId} IS NOT NULL AND ${table.smartCollectionId} IS NULL)
|
|
648
|
+
OR (${table.postId} IS NULL AND ${table.collectionId} IS NULL AND ${table.smartCollectionId} IS NOT NULL)
|
|
495
649
|
)
|
|
496
650
|
AND ${table.redirectToPath} IS NULL
|
|
497
651
|
AND ${table.redirectType} IS NULL
|
|
@@ -500,6 +654,7 @@ export const pathRegistry = sqliteTable(
|
|
|
500
654
|
${table.kind} = 'redirect'
|
|
501
655
|
AND ${table.postId} IS NULL
|
|
502
656
|
AND ${table.collectionId} IS NULL
|
|
657
|
+
AND ${table.smartCollectionId} IS NULL
|
|
503
658
|
AND ${table.redirectToPath} IS NOT NULL
|
|
504
659
|
AND ${table.redirectType} IN (301, 302)
|
|
505
660
|
AND ${table.archiveQuery} IS NULL
|
|
@@ -507,6 +662,7 @@ export const pathRegistry = sqliteTable(
|
|
|
507
662
|
${table.kind} = 'archive'
|
|
508
663
|
AND ${table.postId} IS NULL
|
|
509
664
|
AND ${table.collectionId} IS NULL
|
|
665
|
+
AND ${table.smartCollectionId} IS NULL
|
|
510
666
|
AND ${table.redirectToPath} IS NULL
|
|
511
667
|
AND ${table.redirectType} IS NULL
|
|
512
668
|
AND ${table.archiveQuery} IS NOT NULL
|
|
@@ -526,10 +682,16 @@ export const collectionDirectoryItems = sqliteTable(
|
|
|
526
682
|
siteId: text("site_id")
|
|
527
683
|
.notNull()
|
|
528
684
|
.references(() => sites.id, { onDelete: "cascade" }),
|
|
529
|
-
type: text("type", {
|
|
685
|
+
type: text("type", {
|
|
686
|
+
enum: COLLECTION_DIRECTORY_ENTRY_TYPES,
|
|
687
|
+
}).notNull(),
|
|
530
688
|
collectionId: text("collection_id").references(() => collections.id, {
|
|
531
689
|
onDelete: "cascade",
|
|
532
690
|
}),
|
|
691
|
+
smartCollectionId: text("smart_collection_id").references(
|
|
692
|
+
() => smartCollections.id,
|
|
693
|
+
{ onDelete: "cascade" },
|
|
694
|
+
),
|
|
533
695
|
label: text("label"),
|
|
534
696
|
url: text("url"),
|
|
535
697
|
description: text("description"),
|
|
@@ -540,12 +702,16 @@ export const collectionDirectoryItems = sqliteTable(
|
|
|
540
702
|
(table) => [
|
|
541
703
|
check(
|
|
542
704
|
"chk_collection_directory_item_type",
|
|
543
|
-
sql`${table.type} IN (
|
|
705
|
+
sql`${table.type} IN (${sqlTextEnum(COLLECTION_DIRECTORY_ENTRY_TYPES)})`,
|
|
544
706
|
),
|
|
545
707
|
index("idx_collection_directory_item_site_collection_id").on(
|
|
546
708
|
table.siteId,
|
|
547
709
|
table.collectionId,
|
|
548
710
|
),
|
|
711
|
+
index("idx_collection_directory_item_site_smart_collection_id").on(
|
|
712
|
+
table.siteId,
|
|
713
|
+
table.smartCollectionId,
|
|
714
|
+
),
|
|
549
715
|
uniqueIndex("uq_collection_directory_item_site_position").on(
|
|
550
716
|
table.siteId,
|
|
551
717
|
table.position,
|
|
@@ -555,27 +721,41 @@ export const collectionDirectoryItems = sqliteTable(
|
|
|
555
721
|
.where(
|
|
556
722
|
sql`${table.type} = 'collection' AND ${table.collectionId} IS NOT NULL`,
|
|
557
723
|
),
|
|
724
|
+
uniqueIndex("uq_collection_directory_item_site_smart_collection_once")
|
|
725
|
+
.on(table.siteId, table.smartCollectionId)
|
|
726
|
+
.where(
|
|
727
|
+
sql`${table.type} = 'smart_collection' AND ${table.smartCollectionId} IS NOT NULL`,
|
|
728
|
+
),
|
|
558
729
|
check(
|
|
559
730
|
"chk_collection_directory_item_shape",
|
|
560
731
|
sql`(
|
|
561
732
|
${table.type} = 'collection'
|
|
562
733
|
AND ${table.collectionId} IS NOT NULL
|
|
734
|
+
AND ${table.smartCollectionId} IS NULL
|
|
735
|
+
AND ${table.label} IS NULL
|
|
736
|
+
AND ${table.url} IS NULL
|
|
737
|
+
) OR (
|
|
738
|
+
${table.type} = 'smart_collection'
|
|
739
|
+
AND ${table.collectionId} IS NULL
|
|
740
|
+
AND ${table.smartCollectionId} IS NOT NULL
|
|
563
741
|
AND ${table.label} IS NULL
|
|
564
742
|
AND ${table.url} IS NULL
|
|
565
743
|
) OR (
|
|
566
744
|
${table.type} = 'divider'
|
|
567
745
|
AND ${table.collectionId} IS NULL
|
|
746
|
+
AND ${table.smartCollectionId} IS NULL
|
|
568
747
|
AND ${table.url} IS NULL
|
|
569
748
|
) OR (
|
|
570
749
|
${table.type} = 'link'
|
|
571
750
|
AND ${table.collectionId} IS NULL
|
|
751
|
+
AND ${table.smartCollectionId} IS NULL
|
|
572
752
|
AND ${table.label} IS NOT NULL
|
|
573
753
|
AND ${table.url} IS NOT NULL
|
|
574
754
|
)`,
|
|
575
755
|
),
|
|
576
756
|
check(
|
|
577
757
|
"chk_collection_directory_item_label",
|
|
578
|
-
sql`${table.type}
|
|
758
|
+
sql`${table.type} NOT IN ('collection', 'smart_collection') OR ${table.label} IS NULL`,
|
|
579
759
|
),
|
|
580
760
|
check(
|
|
581
761
|
"chk_collection_directory_item_description",
|
|
@@ -641,6 +821,10 @@ export const navItems = sqliteTable(
|
|
|
641
821
|
collectionId: text("collection_id").references(() => collections.id, {
|
|
642
822
|
onDelete: "cascade",
|
|
643
823
|
}),
|
|
824
|
+
smartCollectionId: text("smart_collection_id").references(
|
|
825
|
+
() => smartCollections.id,
|
|
826
|
+
{ onDelete: "cascade" },
|
|
827
|
+
),
|
|
644
828
|
postId: text("post_id").references(() => posts.id, {
|
|
645
829
|
onDelete: "cascade",
|
|
646
830
|
}),
|
|
@@ -670,21 +854,31 @@ export const navItems = sqliteTable(
|
|
|
670
854
|
${table.type} = 'link'
|
|
671
855
|
AND ${table.systemKey} IS NULL
|
|
672
856
|
AND ${table.collectionId} IS NULL
|
|
857
|
+
AND ${table.smartCollectionId} IS NULL
|
|
673
858
|
AND ${table.postId} IS NULL
|
|
674
859
|
) OR (
|
|
675
860
|
${table.type} = 'system'
|
|
676
861
|
AND ${table.systemKey} IS NOT NULL
|
|
677
862
|
AND ${table.collectionId} IS NULL
|
|
863
|
+
AND ${table.smartCollectionId} IS NULL
|
|
678
864
|
AND ${table.postId} IS NULL
|
|
679
865
|
) OR (
|
|
680
866
|
${table.type} = 'collection'
|
|
681
867
|
AND ${table.systemKey} IS NULL
|
|
682
868
|
AND ${table.collectionId} IS NOT NULL
|
|
869
|
+
AND ${table.smartCollectionId} IS NULL
|
|
870
|
+
AND ${table.postId} IS NULL
|
|
871
|
+
) OR (
|
|
872
|
+
${table.type} = 'smart_collection'
|
|
873
|
+
AND ${table.systemKey} IS NULL
|
|
874
|
+
AND ${table.collectionId} IS NULL
|
|
875
|
+
AND ${table.smartCollectionId} IS NOT NULL
|
|
683
876
|
AND ${table.postId} IS NULL
|
|
684
877
|
) OR (
|
|
685
878
|
${table.type} = 'page'
|
|
686
879
|
AND ${table.systemKey} IS NULL
|
|
687
880
|
AND ${table.collectionId} IS NULL
|
|
881
|
+
AND ${table.smartCollectionId} IS NULL
|
|
688
882
|
AND ${table.postId} IS NOT NULL
|
|
689
883
|
)`,
|
|
690
884
|
),
|
|
@@ -695,6 +889,9 @@ export const navItems = sqliteTable(
|
|
|
695
889
|
uniqueIndex("uq_nav_item_site_collection_id")
|
|
696
890
|
.on(table.siteId, table.collectionId)
|
|
697
891
|
.where(sql`${table.collectionId} IS NOT NULL`),
|
|
892
|
+
uniqueIndex("uq_nav_item_site_smart_collection_id")
|
|
893
|
+
.on(table.siteId, table.smartCollectionId)
|
|
894
|
+
.where(sql`${table.smartCollectionId} IS NOT NULL`),
|
|
698
895
|
uniqueIndex("uq_nav_item_site_post_id")
|
|
699
896
|
.on(table.siteId, table.postId)
|
|
700
897
|
.where(sql`${table.postId} IS NOT NULL`),
|
|
@@ -812,8 +1009,6 @@ export const verification = sqliteTable("verification", {
|
|
|
812
1009
|
// GitHub App Installations (junction table: installation ↔ site, many-to-many)
|
|
813
1010
|
// ---------------------------------------------------------------------------
|
|
814
1011
|
|
|
815
|
-
const GITHUB_APP_ACCOUNT_TYPES = ["User", "Organization"] as const;
|
|
816
|
-
|
|
817
1012
|
export const githubAppInstallation = sqliteTable(
|
|
818
1013
|
"github_app_installation",
|
|
819
1014
|
{
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
detectLocaleFromHeader,
|
|
4
|
+
getCjkFontFromLanguageTag,
|
|
5
|
+
} from "../detect.js";
|
|
3
6
|
|
|
4
7
|
describe("detectLocaleFromHeader", () => {
|
|
5
8
|
it("returns en for en", () => {
|
|
@@ -45,94 +48,36 @@ describe("detectLocaleFromHeader", () => {
|
|
|
45
48
|
});
|
|
46
49
|
});
|
|
47
50
|
|
|
48
|
-
describe("
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
expect(
|
|
51
|
+
describe("getCjkFontFromLanguageTag", () => {
|
|
52
|
+
it("maps Simplified Chinese tags to the zh-Hans profile", () => {
|
|
53
|
+
expect(getCjkFontFromLanguageTag("zh-CN")).toBe("zh-Hans");
|
|
54
|
+
expect(getCjkFontFromLanguageTag("zh-SG")).toBe("zh-Hans");
|
|
55
|
+
expect(getCjkFontFromLanguageTag("zh-Hans")).toBe("zh-Hans");
|
|
56
|
+
// Bare `zh` is ambiguous in principle; Simplified is the majority reading.
|
|
57
|
+
expect(getCjkFontFromLanguageTag("zh")).toBe("zh-Hans");
|
|
52
58
|
});
|
|
53
59
|
|
|
54
|
-
it("maps
|
|
55
|
-
expect(
|
|
60
|
+
it("maps Traditional Chinese tags to the zh-Hant profile", () => {
|
|
61
|
+
expect(getCjkFontFromLanguageTag("zh-TW")).toBe("zh-Hant");
|
|
62
|
+
expect(getCjkFontFromLanguageTag("zh-HK")).toBe("zh-Hant");
|
|
63
|
+
expect(getCjkFontFromLanguageTag("zh-MO")).toBe("zh-Hant");
|
|
64
|
+
expect(getCjkFontFromLanguageTag("zh-Hant")).toBe("zh-Hant");
|
|
56
65
|
});
|
|
57
66
|
|
|
58
|
-
it("maps
|
|
59
|
-
expect(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
expect(detectCjkFontFromHeader("zh")).toBe("zh-Hans");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Traditional Chinese
|
|
67
|
-
it("maps zh-TW to zh-Hant", () => {
|
|
68
|
-
expect(detectCjkFontFromHeader("zh-TW")).toBe("zh-Hant");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("maps zh-HK to zh-Hant", () => {
|
|
72
|
-
expect(detectCjkFontFromHeader("zh-HK")).toBe("zh-Hant");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("maps zh-MO to zh-Hant", () => {
|
|
76
|
-
expect(detectCjkFontFromHeader("zh-MO")).toBe("zh-Hant");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("maps zh-Hant to zh-Hant", () => {
|
|
80
|
-
expect(detectCjkFontFromHeader("zh-Hant")).toBe("zh-Hant");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// Japanese
|
|
84
|
-
it("maps ja to ja", () => {
|
|
85
|
-
expect(detectCjkFontFromHeader("ja")).toBe("ja");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("maps ja-JP to ja", () => {
|
|
89
|
-
expect(detectCjkFontFromHeader("ja-JP")).toBe("ja");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
// Korean
|
|
93
|
-
it("maps ko to ko", () => {
|
|
94
|
-
expect(detectCjkFontFromHeader("ko")).toBe("ko");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("maps ko-KR to ko", () => {
|
|
98
|
-
expect(detectCjkFontFromHeader("ko-KR")).toBe("ko");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// q-value priority
|
|
102
|
-
it("picks highest q-value CJK language", () => {
|
|
103
|
-
expect(detectCjkFontFromHeader("fr;q=0.5,zh-CN;q=0.9,en;q=0.8")).toBe(
|
|
104
|
-
"zh-Hans",
|
|
105
|
-
);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("picks first CJK when q-values are equal", () => {
|
|
109
|
-
expect(detectCjkFontFromHeader("zh-TW,en")).toBe("zh-Hant");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Non-CJK fallback
|
|
113
|
-
it("returns off for non-CJK language", () => {
|
|
114
|
-
expect(detectCjkFontFromHeader("en-US")).toBe("off");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("returns off for undefined", () => {
|
|
118
|
-
expect(detectCjkFontFromHeader(undefined)).toBe("off");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("returns off for empty string", () => {
|
|
122
|
-
expect(detectCjkFontFromHeader("")).toBe("off");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("skips entries with q=0", () => {
|
|
126
|
-
expect(detectCjkFontFromHeader("zh-CN;q=0,en")).toBe("off");
|
|
67
|
+
it("maps Japanese and Korean tags", () => {
|
|
68
|
+
expect(getCjkFontFromLanguageTag("ja")).toBe("ja");
|
|
69
|
+
expect(getCjkFontFromLanguageTag("ja-JP")).toBe("ja");
|
|
70
|
+
expect(getCjkFontFromLanguageTag("ko")).toBe("ko");
|
|
71
|
+
expect(getCjkFontFromLanguageTag("ko-KR")).toBe("ko");
|
|
127
72
|
});
|
|
128
73
|
|
|
129
74
|
it("is case-insensitive", () => {
|
|
130
|
-
expect(
|
|
75
|
+
expect(getCjkFontFromLanguageTag("ZH-CN")).toBe("zh-Hans");
|
|
131
76
|
});
|
|
132
77
|
|
|
133
|
-
it("
|
|
134
|
-
expect(
|
|
135
|
-
|
|
136
|
-
);
|
|
78
|
+
it("returns nothing for languages with no CJK profile", () => {
|
|
79
|
+
expect(getCjkFontFromLanguageTag("en-US")).toBeUndefined();
|
|
80
|
+
expect(getCjkFontFromLanguageTag("")).toBeUndefined();
|
|
81
|
+
expect(getCjkFontFromLanguageTag(" ")).toBeUndefined();
|
|
137
82
|
});
|
|
138
83
|
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
formatLanguageList,
|
|
4
|
+
parseLanguageList,
|
|
5
|
+
toLanguagePrefix,
|
|
6
|
+
} from "../locales.js";
|
|
7
|
+
import { resolveSupportedLocaleTag } from "../supported-locales.js";
|
|
8
|
+
|
|
9
|
+
describe("resolveSupportedLocaleTag", () => {
|
|
10
|
+
it("matches a curated tag exactly", () => {
|
|
11
|
+
expect(resolveSupportedLocaleTag("ja")).toBe("ja");
|
|
12
|
+
expect(resolveSupportedLocaleTag("pt-BR")).toBe("pt-BR");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("is case-insensitive", () => {
|
|
16
|
+
expect(resolveSupportedLocaleTag("PT-br")).toBe("pt-BR");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("resolves a region tag to the right Chinese script", () => {
|
|
20
|
+
// The curated list offers zh by script, not by region, so this has to work
|
|
21
|
+
// or a Chinese browser lands on English.
|
|
22
|
+
expect(resolveSupportedLocaleTag("zh-CN")).toBe("zh-Hans");
|
|
23
|
+
expect(resolveSupportedLocaleTag("zh-SG")).toBe("zh-Hans");
|
|
24
|
+
expect(resolveSupportedLocaleTag("zh-TW")).toBe("zh-Hant");
|
|
25
|
+
expect(resolveSupportedLocaleTag("zh-HK")).toBe("zh-Hant");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("falls back to the bare language when the region is unknown", () => {
|
|
29
|
+
expect(resolveSupportedLocaleTag("de-AT")).toBe("de");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("respects q-values", () => {
|
|
33
|
+
expect(resolveSupportedLocaleTag("fr;q=0.5,ja;q=0.9,en;q=0.8")).toBe("ja");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("skips entries the client excluded", () => {
|
|
37
|
+
expect(resolveSupportedLocaleTag("ja;q=0,fr")).toBe("fr");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("falls back to English for anything unrecognized", () => {
|
|
41
|
+
expect(resolveSupportedLocaleTag("*")).toBe("en");
|
|
42
|
+
expect(resolveSupportedLocaleTag("")).toBe("en");
|
|
43
|
+
expect(resolveSupportedLocaleTag(undefined)).toBe("en");
|
|
44
|
+
expect(resolveSupportedLocaleTag("not a locale!!!")).toBe("en");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("language list settings value", () => {
|
|
49
|
+
it("round-trips canonical tags in order", () => {
|
|
50
|
+
expect(formatLanguageList(parseLanguageList("en,ja"))).toBe("en,ja");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("canonicalizes case and trims whitespace", () => {
|
|
54
|
+
expect(parseLanguageList(" en , ZH-hant ")).toEqual(["en", "zh-Hant"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("drops duplicates while keeping the first position", () => {
|
|
58
|
+
expect(parseLanguageList("ja,en,JA")).toEqual(["ja", "en"]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("drops blank and unparseable entries", () => {
|
|
62
|
+
expect(parseLanguageList("en,,not a locale!!!,ja")).toEqual(["en", "ja"]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("treats missing values as an empty list", () => {
|
|
66
|
+
expect(parseLanguageList(null)).toEqual([]);
|
|
67
|
+
expect(parseLanguageList(undefined)).toEqual([]);
|
|
68
|
+
expect(parseLanguageList("")).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("toLanguagePrefix", () => {
|
|
73
|
+
it("lowercases the canonical tag", () => {
|
|
74
|
+
expect(toLanguagePrefix("zh-Hant")).toBe("zh-hant");
|
|
75
|
+
expect(toLanguagePrefix("en")).toBe("en");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("maps distinct languages to distinct prefixes", () => {
|
|
79
|
+
const tags = ["en", "zh-Hans", "zh-Hant", "ja", "pt-BR"];
|
|
80
|
+
const prefixes = tags.map(toLanguagePrefix);
|
|
81
|
+
expect(new Set(prefixes).size).toBe(tags.length);
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/i18n/context.tsx
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Request-scoped
|
|
2
|
+
* Request-scoped render state for Hono JSX renders.
|
|
3
|
+
*
|
|
4
|
+
* Primarily the i18n instance, which is what components reach for. It also
|
|
5
|
+
* binds the viewer (`lib/viewer-context.ts`), because this provider is the one
|
|
6
|
+
* wrapper every server render already passes through — pages via `BaseLayout`,
|
|
7
|
+
* fragments via the partial routes — and both facts come from the same request.
|
|
3
8
|
*/
|
|
4
9
|
|
|
5
10
|
import type { Context } from "hono";
|
|
6
11
|
import type { FC, PropsWithChildren } from "hono/jsx";
|
|
12
|
+
import { bindViewer } from "../lib/viewer-context.js";
|
|
7
13
|
import type { I18n } from "./i18n.js";
|
|
8
14
|
import { getI18n as getI18nFromContext } from "./i18n.js";
|
|
9
15
|
|
|
@@ -11,7 +17,8 @@ import { getI18n as getI18nFromContext } from "./i18n.js";
|
|
|
11
17
|
let currentI18n: I18n | null = null;
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
|
-
* I18nProvider - binds
|
|
20
|
+
* I18nProvider - binds this render's request-scoped state: the i18n instance
|
|
21
|
+
* and the viewer.
|
|
15
22
|
*
|
|
16
23
|
* @example
|
|
17
24
|
* ```tsx
|
|
@@ -33,6 +40,9 @@ export const I18nProvider: FC<I18nProviderProps> = ({ c, children }) => {
|
|
|
33
40
|
// Note: In Hono JSX, rendering is synchronous and single-threaded per request
|
|
34
41
|
// so we can safely set global context without cleanup
|
|
35
42
|
currentI18n = getI18nFromContext(c);
|
|
43
|
+
// The same flag `BaseLayout` writes into `data-authenticated`, so what a card
|
|
44
|
+
// renders and what the CSS reveals can never disagree.
|
|
45
|
+
bindViewer(c.get("isAuthenticated") === true);
|
|
36
46
|
return <>{children}</>;
|
|
37
47
|
};
|
|
38
48
|
|