@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
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* mouse or the keyboard has no obvious way to scroll it. This module:
|
|
7
7
|
*
|
|
8
8
|
* - Toggles `.can-scroll-start` / `.can-scroll-end` on the wrap so CSS can
|
|
9
|
-
* fade the edge that has hidden content and reveal the matching arrow
|
|
9
|
+
* fade the edge that has hidden content and reveal the matching arrow,
|
|
10
|
+
* re-measuring on scroll and on any size change of the strip or its items.
|
|
10
11
|
* - Wires the prev/next arrow buttons to scroll the strip by one page.
|
|
11
12
|
* - Scrolls the strip with Arrow / Home / End keys while it is focused.
|
|
12
13
|
*/
|
|
@@ -41,6 +42,39 @@ function scrollByStep(scroller: HTMLElement, direction: 1 | -1): void {
|
|
|
41
42
|
});
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Re-measure whenever the strip or its items change size.
|
|
47
|
+
*
|
|
48
|
+
* Scroll events alone are not enough: they only fire while the strip actually
|
|
49
|
+
* overflows, so a hint measured against a narrower strip — a smaller window at
|
|
50
|
+
* load time, a zoom change, an item that resolves its width late — latches on
|
|
51
|
+
* forever. The arrow keeps pointing at content that is already fully visible,
|
|
52
|
+
* and no scroll can ever happen to correct it.
|
|
53
|
+
*/
|
|
54
|
+
function watchLayout(wrap: HTMLElement, scroller: HTMLElement): void {
|
|
55
|
+
if (!("ResizeObserver" in globalThis)) return;
|
|
56
|
+
|
|
57
|
+
let raf = 0;
|
|
58
|
+
const schedule = (): void => {
|
|
59
|
+
cancelAnimationFrame(raf);
|
|
60
|
+
raf = requestAnimationFrame(() => updateHints(wrap));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// The strip covers viewport and column width changes; its items cover
|
|
64
|
+
// content width changes. Re-observe when a morph swaps the items out.
|
|
65
|
+
const observer = new globalThis.ResizeObserver(schedule);
|
|
66
|
+
const observeAll = (): void => {
|
|
67
|
+
observer.disconnect();
|
|
68
|
+
observer.observe(scroller);
|
|
69
|
+
for (const item of scroller.children) observer.observe(item);
|
|
70
|
+
};
|
|
71
|
+
observeAll();
|
|
72
|
+
new globalThis.MutationObserver(() => {
|
|
73
|
+
observeAll();
|
|
74
|
+
schedule();
|
|
75
|
+
}).observe(scroller, { childList: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
44
78
|
function initWrap(wrap: HTMLElement): void {
|
|
45
79
|
// Guard against double-init (initAll + the MutationObserver can overlap).
|
|
46
80
|
if (wrap.dataset.scrollHintReady === "1") return;
|
|
@@ -48,11 +82,13 @@ function initWrap(wrap: HTMLElement): void {
|
|
|
48
82
|
if (!scroller) return;
|
|
49
83
|
wrap.dataset.scrollHintReady = "1";
|
|
50
84
|
|
|
51
|
-
// Initial check + keep edge hints in sync while scrolling
|
|
85
|
+
// Initial check + keep edge hints in sync while scrolling and while the
|
|
86
|
+
// strip is resized or its items change.
|
|
52
87
|
updateHints(wrap);
|
|
53
88
|
scroller.addEventListener("scroll", () => updateHints(wrap), {
|
|
54
89
|
passive: true,
|
|
55
90
|
});
|
|
91
|
+
watchLayout(wrap, scroller);
|
|
56
92
|
|
|
57
93
|
// Prev/next arrow buttons — the mouse affordance.
|
|
58
94
|
wrap
|
|
@@ -117,3 +153,8 @@ if (document.readyState === "loading") {
|
|
|
117
153
|
initAll();
|
|
118
154
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
119
155
|
}
|
|
156
|
+
|
|
157
|
+
export const __testOnly = {
|
|
158
|
+
initWrap,
|
|
159
|
+
updateHints,
|
|
160
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy access to mediabunny.
|
|
3
|
+
*
|
|
4
|
+
* mediabunny is ≈900 KB minified (≈170 KB gzipped) and only does anything once
|
|
5
|
+
* a video or audio file is picked for upload, so the processors fetch it on
|
|
6
|
+
* demand instead of shipping it in `client-auth.js`. Same pattern as `heic-to`
|
|
7
|
+
* in `compose-bridge.ts`; the build guard in `vite.shared.ts` keeps it out of
|
|
8
|
+
* the entry chunks.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const { Input, BlobSource, ALL_FORMATS } = await loadMediabunny();
|
|
13
|
+
* const input = new Input({ source: new BlobSource(file), formats: ALL_FORMATS });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
type Mediabunny = typeof import("mediabunny");
|
|
18
|
+
|
|
19
|
+
let loading: Promise<Mediabunny> | undefined;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load mediabunny once and share the module across callers.
|
|
23
|
+
*
|
|
24
|
+
* @returns The mediabunny module namespace
|
|
25
|
+
*/
|
|
26
|
+
export function loadMediabunny(): Promise<Mediabunny> {
|
|
27
|
+
loading ??= import("mediabunny");
|
|
28
|
+
return loading;
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import "./components/jant-post-picker.js";
|
|
2
|
+
import type {
|
|
3
|
+
JantPostPicker,
|
|
4
|
+
PostPickerItem,
|
|
5
|
+
PostPickerOptions,
|
|
6
|
+
} from "./components/jant-post-picker.js";
|
|
7
|
+
|
|
8
|
+
const POST_PICKER_TAG = "jant-post-picker";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Ensure the shared post picker is mounted once.
|
|
12
|
+
*
|
|
13
|
+
* @returns The mounted picker element
|
|
14
|
+
*/
|
|
15
|
+
export function ensurePostPicker(): JantPostPicker {
|
|
16
|
+
const existing = document.querySelector<JantPostPicker>(POST_PICKER_TAG);
|
|
17
|
+
if (existing) return existing;
|
|
18
|
+
|
|
19
|
+
const picker = document.createElement(POST_PICKER_TAG) as JantPostPicker;
|
|
20
|
+
document.body.appendChild(picker);
|
|
21
|
+
return picker;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Open the shared picker and resolve to the post the author chose.
|
|
26
|
+
*
|
|
27
|
+
* @param options - Copy, and the search that produces the candidates
|
|
28
|
+
* @returns The picked post's ID, or null when dismissed
|
|
29
|
+
* @example
|
|
30
|
+
* const id = await pickPost({
|
|
31
|
+
* heading: "Link a translation",
|
|
32
|
+
* placeholder: "Search your posts…",
|
|
33
|
+
* emptyHint: "Nothing matched.",
|
|
34
|
+
* search: (q) => fetchCandidates(q),
|
|
35
|
+
* });
|
|
36
|
+
*/
|
|
37
|
+
export function pickPost(options: PostPickerOptions): Promise<string | null> {
|
|
38
|
+
return ensurePostPicker().pick(options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type { PostPickerItem, PostPickerOptions };
|
|
@@ -4,10 +4,16 @@ import {
|
|
|
4
4
|
toPublicAssetPath,
|
|
5
5
|
} from "../lib/asset-path.js";
|
|
6
6
|
import { toPublicHref } from "../lib/url.js";
|
|
7
|
+
import { isPerLanguageSurface } from "../lib/per-language-surfaces.js";
|
|
7
8
|
|
|
8
9
|
type FetchInput = Parameters<typeof fetch>[0];
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The deployment's public path prefix, as the server stamped it on the page.
|
|
13
|
+
*
|
|
14
|
+
* @returns Prefix like `/blog`, or an empty string when the site is rooted
|
|
15
|
+
*/
|
|
16
|
+
export function sitePathPrefix(): string {
|
|
11
17
|
return document.documentElement.dataset.sitePathPrefix || "";
|
|
12
18
|
}
|
|
13
19
|
|
|
@@ -15,6 +21,27 @@ function assetBasePath(): string {
|
|
|
15
21
|
return document.documentElement.dataset.assetBasePath || ASSET_BASE_PATH;
|
|
16
22
|
}
|
|
17
23
|
|
|
24
|
+
function viewBasePath(): string {
|
|
25
|
+
const base = document.documentElement.dataset.viewBasePath;
|
|
26
|
+
return base === undefined ? sitePathPrefix() : base;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Content language of the view the reader is in, as the server stamped it.
|
|
31
|
+
*
|
|
32
|
+
* The primary language is served at the root and stamps nothing, so `undefined`
|
|
33
|
+
* means "this site's default view", not "unknown". Send it to an API that
|
|
34
|
+
* narrows by language so the numbers it returns match the ones the page was
|
|
35
|
+
* rendered with.
|
|
36
|
+
*
|
|
37
|
+
* @returns BCP 47 tag like `en`, or undefined in the root view
|
|
38
|
+
* @example
|
|
39
|
+
* viewLang(); // "ja" on /ja/collections, undefined on /collections
|
|
40
|
+
*/
|
|
41
|
+
export function viewLang(): string | undefined {
|
|
42
|
+
return document.documentElement.dataset.viewLang || undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
18
45
|
export function publicPath(path: string): string {
|
|
19
46
|
if (isAssetPath(path)) {
|
|
20
47
|
return toPublicAssetPath(path, assetBasePath());
|
|
@@ -22,6 +49,52 @@ export function publicPath(path: string): string {
|
|
|
22
49
|
return toPublicHref(path, sitePathPrefix());
|
|
23
50
|
}
|
|
24
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Build a link to a target the caller knows exists once per language.
|
|
54
|
+
*
|
|
55
|
+
* The client-side counterpart of the server's `toViewPath`: it always carries
|
|
56
|
+
* the current view's language prefix. Reach for it when the call site knows
|
|
57
|
+
* what it is linking to — a collection row knows its target is a collection
|
|
58
|
+
* page, which is served per language even though it sits at `/{slug}` in the
|
|
59
|
+
* same root namespace as language-neutral post permalinks.
|
|
60
|
+
*
|
|
61
|
+
* When the target's kind is not known at the call site, use {@link navPath}.
|
|
62
|
+
*
|
|
63
|
+
* @param path - Internal app path such as `/collections` or a collection slug
|
|
64
|
+
* @returns Public path prefixed by the current view's language and site prefix
|
|
65
|
+
* @example
|
|
66
|
+
* viewPath("/collections"); // "/collections", or "/en/collections" under /en
|
|
67
|
+
*/
|
|
68
|
+
export function viewPath(path: string): string {
|
|
69
|
+
if (isAssetPath(path)) {
|
|
70
|
+
return toPublicAssetPath(path, assetBasePath());
|
|
71
|
+
}
|
|
72
|
+
return toPublicHref(path, viewBasePath());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build a link to a target whose kind the call site does not know.
|
|
77
|
+
*
|
|
78
|
+
* The command palette navigates to settings pages, posts, and archives through
|
|
79
|
+
* one code path, so it cannot assert what any given target is. This keeps the
|
|
80
|
+
* reader in their language view for the surfaces that demonstrably exist per
|
|
81
|
+
* language, and leaves everything else at its single site-wide address — so a
|
|
82
|
+
* jump to `/settings` never becomes `/en/settings`, which is a 404.
|
|
83
|
+
*
|
|
84
|
+
* Paths in the root namespace (`/{slug}`) are left alone, because a post and a
|
|
85
|
+
* collection are indistinguishable there and posts have one address by design.
|
|
86
|
+
* A call site that knows better should say so with {@link viewPath}.
|
|
87
|
+
*
|
|
88
|
+
* @param path - Internal app path, query string included if there is one
|
|
89
|
+
* @returns Public path, carrying the view's language prefix where one applies
|
|
90
|
+
* @example
|
|
91
|
+
* navPath("/archive?media=any"); // "/en/archive?media=any" under /en
|
|
92
|
+
* navPath("/settings"); // "/settings" — the dashboard is one place
|
|
93
|
+
*/
|
|
94
|
+
export function navPath(path: string): string {
|
|
95
|
+
return isPerLanguageSurface(path) ? viewPath(path) : publicPath(path);
|
|
96
|
+
}
|
|
97
|
+
|
|
25
98
|
function normalizeFetchInput(input: FetchInput | URL): FetchInput | URL {
|
|
26
99
|
if (typeof input === "string") {
|
|
27
100
|
if (/^https?:\/\//.test(input)) {
|
|
@@ -19,26 +19,22 @@ import { showToast } from "./toast.js";
|
|
|
19
19
|
function parseSettingsInitialData(data: unknown): SettingsInitialData | null {
|
|
20
20
|
const siteName = getJsonString(data, "siteName");
|
|
21
21
|
const siteDescription = getJsonString(data, "siteDescription");
|
|
22
|
-
const siteLanguage = getJsonString(data, "siteLanguage");
|
|
23
|
-
// Tolerate older payloads without the key: empty = follow content language.
|
|
24
|
-
const dashboardLanguage = getJsonString(data, "dashboardLanguage") ?? "";
|
|
25
|
-
const cjkSerifFont = getJsonString(data, "cjkSerifFont");
|
|
26
22
|
const mainRssFeed = getJsonString(data, "mainRssFeed");
|
|
27
23
|
const timeZone = getJsonString(data, "timeZone");
|
|
28
24
|
const siteFooter = getJsonString(data, "siteFooter");
|
|
29
25
|
const showJantBrandingOnHome = getJsonBoolean(data, "showJantBrandingOnHome");
|
|
30
26
|
const noindex = getJsonBoolean(data, "noindex");
|
|
27
|
+
const discover = getJsonString(data, "discover");
|
|
31
28
|
|
|
32
29
|
if (
|
|
33
30
|
siteName === undefined ||
|
|
34
31
|
siteDescription === undefined ||
|
|
35
|
-
siteLanguage === undefined ||
|
|
36
|
-
cjkSerifFont === undefined ||
|
|
37
32
|
mainRssFeed === undefined ||
|
|
38
33
|
timeZone === undefined ||
|
|
39
34
|
siteFooter === undefined ||
|
|
40
35
|
showJantBrandingOnHome === undefined ||
|
|
41
|
-
noindex === undefined
|
|
36
|
+
noindex === undefined ||
|
|
37
|
+
discover === undefined
|
|
42
38
|
) {
|
|
43
39
|
return null;
|
|
44
40
|
}
|
|
@@ -46,14 +42,12 @@ function parseSettingsInitialData(data: unknown): SettingsInitialData | null {
|
|
|
46
42
|
return {
|
|
47
43
|
siteName,
|
|
48
44
|
siteDescription,
|
|
49
|
-
siteLanguage,
|
|
50
|
-
dashboardLanguage,
|
|
51
|
-
cjkSerifFont,
|
|
52
45
|
mainRssFeed,
|
|
53
46
|
timeZone,
|
|
54
47
|
siteFooter,
|
|
55
48
|
showJantBrandingOnHome,
|
|
56
49
|
noindex,
|
|
50
|
+
discover,
|
|
57
51
|
};
|
|
58
52
|
}
|
|
59
53
|
|
|
@@ -8,9 +8,18 @@
|
|
|
8
8
|
const moreControllers = new WeakMap();
|
|
9
9
|
const drawerControllers = new WeakMap();
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
/** Header dropdowns, in the order they appear: the "More" nav overflow and,
|
|
12
|
+
* on a multilingual site, the language switcher. Both behave identically —
|
|
13
|
+
* click to toggle, outside click or Escape to close, opening one closes the
|
|
14
|
+
* other via the shared `basecoat:popover` event. */
|
|
15
|
+
const DROPDOWNS = [
|
|
16
|
+
{ trigger: ".site-header-more-btn", popover: ".site-header-more-popover" },
|
|
17
|
+
{ trigger: ".site-header-lang-btn", popover: ".site-header-lang-popover" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function initDropdown(root, triggerSelector, popoverSelector) {
|
|
21
|
+
const trigger = root.querySelector(triggerSelector);
|
|
22
|
+
const popover = root.querySelector(popoverSelector);
|
|
14
23
|
|
|
15
24
|
if (!trigger || !popover) return;
|
|
16
25
|
if (trigger.dataset.moreInitialized === "true") return;
|
|
@@ -129,8 +138,10 @@ export function initSiteHeaderNav(root = document) {
|
|
|
129
138
|
// --- Freshness indicators ---
|
|
130
139
|
initNavFreshness(root);
|
|
131
140
|
|
|
132
|
-
// ---
|
|
133
|
-
|
|
141
|
+
// --- Header dropdowns ---
|
|
142
|
+
for (const { trigger, popover } of DROPDOWNS) {
|
|
143
|
+
initDropdown(root, trigger, popover);
|
|
144
|
+
}
|
|
134
145
|
|
|
135
146
|
// --- Mobile drawer ---
|
|
136
147
|
if (!hamburger || !drawer || !backdrop) return;
|
|
@@ -210,7 +221,8 @@ export function initSiteHeaderNav(root = document) {
|
|
|
210
221
|
}
|
|
211
222
|
|
|
212
223
|
export function destroySiteHeaderNav(root = document) {
|
|
213
|
-
|
|
224
|
+
const triggerSelector = DROPDOWNS.map((d) => d.trigger).join(", ");
|
|
225
|
+
for (const trigger of root.querySelectorAll(triggerSelector)) {
|
|
214
226
|
const controller = moreControllers.get(trigger);
|
|
215
227
|
controller?.abort();
|
|
216
228
|
moreControllers.delete(trigger);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart collection dialog host.
|
|
3
|
+
*
|
|
4
|
+
* One dialog element for the whole page, mounted lazily and reused. Three
|
|
5
|
+
* places open it — the collections page menu, a directory row's menu, and the
|
|
6
|
+
* smart collection page's own menu — and they all go through
|
|
7
|
+
* {@link openSmartCollectionDialog}, because creating and editing are one
|
|
8
|
+
* surface.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import "./components/jant-smart-collection-dialog.js";
|
|
12
|
+
import type { JantSmartCollectionDialog } from "./components/jant-smart-collection-dialog.js";
|
|
13
|
+
import type {
|
|
14
|
+
SmartCollectionDialogLabels,
|
|
15
|
+
SmartCollectionDialogState,
|
|
16
|
+
} from "./components/smart-collection-dialog-types.js";
|
|
17
|
+
import { setCollectionVocabulary } from "./components/smart-collection-conditions.js";
|
|
18
|
+
import { publicPath } from "./runtime-paths.js";
|
|
19
|
+
|
|
20
|
+
const LABELS_ATTRIBUTE = "data-smart-collection-dialog-labels";
|
|
21
|
+
|
|
22
|
+
let labels: SmartCollectionDialogLabels | null = null;
|
|
23
|
+
let element: JantSmartCollectionDialog | null = null;
|
|
24
|
+
let collections: Promise<SiteCollection[]> | null = null;
|
|
25
|
+
|
|
26
|
+
interface SiteCollection {
|
|
27
|
+
id: string;
|
|
28
|
+
slug: string;
|
|
29
|
+
title: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The dialog's strings, rendered into the page by whichever surface can open
|
|
34
|
+
* it. Read once and cached: they are the same on every surface of a page, and
|
|
35
|
+
* the collection vocabulary is written into this object once it loads.
|
|
36
|
+
*/
|
|
37
|
+
function readLabels(): SmartCollectionDialogLabels | null {
|
|
38
|
+
if (labels) return labels;
|
|
39
|
+
const holder = document.querySelector<HTMLElement>(`[${LABELS_ATTRIBUTE}]`);
|
|
40
|
+
const raw = holder?.getAttribute(LABELS_ATTRIBUTE);
|
|
41
|
+
if (!raw) return null;
|
|
42
|
+
try {
|
|
43
|
+
labels = JSON.parse(raw) as SmartCollectionDialogLabels;
|
|
44
|
+
return labels;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Load the site's collections so the collection condition can offer them.
|
|
52
|
+
*
|
|
53
|
+
* Fetched at most once per page, and only when the dialog is actually opened —
|
|
54
|
+
* most page loads never open it. The names are written into the labels on every
|
|
55
|
+
* open rather than only on the first: an opened-once flag would leave the
|
|
56
|
+
* second open with a collection menu that lists nothing.
|
|
57
|
+
*/
|
|
58
|
+
async function ensureCollectionVocabulary(
|
|
59
|
+
target: SmartCollectionDialogLabels,
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
collections ??= fetchCollections();
|
|
62
|
+
const loaded = await collections;
|
|
63
|
+
setCollectionVocabulary(loaded);
|
|
64
|
+
for (const collection of loaded) {
|
|
65
|
+
target.values[`collection.${collection.slug}`] = collection.title;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function fetchCollections(): Promise<SiteCollection[]> {
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch(publicPath("/api/collections"));
|
|
72
|
+
if (!res.ok) return [];
|
|
73
|
+
const json = (await res.json()) as { collections?: SiteCollection[] };
|
|
74
|
+
return json.collections ?? [];
|
|
75
|
+
} catch {
|
|
76
|
+
// The condition is still offered; it simply lists nothing to choose.
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ensureElement(
|
|
82
|
+
dialogLabels: SmartCollectionDialogLabels,
|
|
83
|
+
): JantSmartCollectionDialog {
|
|
84
|
+
if (element?.isConnected) {
|
|
85
|
+
element.labels = dialogLabels;
|
|
86
|
+
return element;
|
|
87
|
+
}
|
|
88
|
+
element = document.createElement(
|
|
89
|
+
"jant-smart-collection-dialog",
|
|
90
|
+
) as JantSmartCollectionDialog;
|
|
91
|
+
element.labels = dialogLabels;
|
|
92
|
+
document.body.appendChild(element);
|
|
93
|
+
return element;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Open the smart collection dialog.
|
|
98
|
+
*
|
|
99
|
+
* @param options.smartCollectionId - Edit this one; omit to create a new one
|
|
100
|
+
* @param options.prefill - Starting values, shown to the author before anything
|
|
101
|
+
* is saved
|
|
102
|
+
* @returns Whether anything was created, changed, or deleted
|
|
103
|
+
* @example
|
|
104
|
+
* await openSmartCollectionDialog({ smartCollectionId: "smc_…" });
|
|
105
|
+
*/
|
|
106
|
+
export async function openSmartCollectionDialog(options: {
|
|
107
|
+
smartCollectionId?: string;
|
|
108
|
+
prefill?: SmartCollectionDialogState;
|
|
109
|
+
}): Promise<boolean> {
|
|
110
|
+
const dialogLabels = readLabels();
|
|
111
|
+
if (!dialogLabels) return false;
|
|
112
|
+
|
|
113
|
+
await ensureCollectionVocabulary(dialogLabels);
|
|
114
|
+
const dialog = ensureElement(dialogLabels);
|
|
115
|
+
return dialog.open(options);
|
|
116
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart collection page owner menu.
|
|
3
|
+
*
|
|
4
|
+
* The same menu shape a collection page has, item for item — Edit, Add to
|
|
5
|
+
* navigation, Edit navigation, Delete — with one difference: Edit opens the
|
|
6
|
+
* dialog rather than navigating to an editor page, because a smart collection
|
|
7
|
+
* has no editor page. There is no Edit button beside the title either; a
|
|
8
|
+
* collection page has none, and the menu is where a page keeps its owner
|
|
9
|
+
* actions.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getCollectionsDirectoryPath } from "../lib/collection-paths.js";
|
|
13
|
+
import { addSmartCollectionToNavigation } from "./collection-navigation.js";
|
|
14
|
+
import { NAVIGATION_SETTINGS_PATH } from "../lib/settings-paths.js";
|
|
15
|
+
import { showConfirmDialog } from "./confirm.js";
|
|
16
|
+
import { showToast, showToastWithAction } from "./toast.js";
|
|
17
|
+
import { openSmartCollectionDialog } from "./smart-collection-dialog-host.js";
|
|
18
|
+
|
|
19
|
+
interface PageActionLabels {
|
|
20
|
+
addToNavigation: string;
|
|
21
|
+
addingToNavigation: string;
|
|
22
|
+
addedToNavigation: string;
|
|
23
|
+
editNavigation: string;
|
|
24
|
+
addToNavigationFailed: string;
|
|
25
|
+
confirmDelete: string;
|
|
26
|
+
deleteCollection: string;
|
|
27
|
+
cancel: string;
|
|
28
|
+
saveFailed: string;
|
|
29
|
+
deleted: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const parseLabels = (value: string | undefined): PageActionLabels => {
|
|
33
|
+
const empty: PageActionLabels = {
|
|
34
|
+
addToNavigation: "",
|
|
35
|
+
addingToNavigation: "",
|
|
36
|
+
addedToNavigation: "",
|
|
37
|
+
editNavigation: "",
|
|
38
|
+
addToNavigationFailed: "",
|
|
39
|
+
confirmDelete: "",
|
|
40
|
+
deleteCollection: "",
|
|
41
|
+
cancel: "",
|
|
42
|
+
saveFailed: "",
|
|
43
|
+
deleted: "",
|
|
44
|
+
};
|
|
45
|
+
if (!value) return empty;
|
|
46
|
+
try {
|
|
47
|
+
return { ...empty, ...(JSON.parse(value) as Partial<PageActionLabels>) };
|
|
48
|
+
} catch {
|
|
49
|
+
return empty;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
document
|
|
54
|
+
.querySelectorAll<HTMLElement>("[data-smart-collection-page-actions]")
|
|
55
|
+
.forEach((root) => {
|
|
56
|
+
if (root.dataset.smartCollectionPageActionsInitialized === "true") return;
|
|
57
|
+
|
|
58
|
+
const labels = parseLabels(root.dataset.collectionPageLabels);
|
|
59
|
+
const smartCollectionId = root.dataset.smartCollectionId;
|
|
60
|
+
const redirectUrl =
|
|
61
|
+
root.dataset.collectionPageRedirectUrl || getCollectionsDirectoryPath();
|
|
62
|
+
const trigger = root.querySelector<HTMLElement>(
|
|
63
|
+
"[data-collection-page-action='toggle-menu']",
|
|
64
|
+
);
|
|
65
|
+
const menu = root.querySelector<HTMLElement>("[data-collection-page-menu]");
|
|
66
|
+
const addToNavigationButton = root.querySelector<HTMLButtonElement>(
|
|
67
|
+
"[data-collection-page-action='add-to-navigation']",
|
|
68
|
+
);
|
|
69
|
+
const editNavigationLink = root.querySelector<HTMLAnchorElement>(
|
|
70
|
+
"[data-collection-page-edit-navigation]",
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (!smartCollectionId || !trigger || !menu) return;
|
|
74
|
+
|
|
75
|
+
const closeMenu = (focusTrigger = false) => {
|
|
76
|
+
if (menu.hidden) return;
|
|
77
|
+
menu.hidden = true;
|
|
78
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
79
|
+
if (focusTrigger) trigger.focus();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const openMenu = (focusFirstItem = false) => {
|
|
83
|
+
menu.hidden = false;
|
|
84
|
+
trigger.setAttribute("aria-expanded", "true");
|
|
85
|
+
if (focusFirstItem) {
|
|
86
|
+
menu.querySelector<HTMLElement>("[role='menuitem']")?.focus();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleEdit = async () => {
|
|
91
|
+
closeMenu(false);
|
|
92
|
+
const changed = await openSmartCollectionDialog({ smartCollectionId });
|
|
93
|
+
// The address, the title, the conditions, and the count can all have
|
|
94
|
+
// moved, so the page is re-read rather than patched in place.
|
|
95
|
+
if (changed) window.location.reload();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handleDelete = async () => {
|
|
99
|
+
closeMenu(false);
|
|
100
|
+
const confirmed = await showConfirmDialog({
|
|
101
|
+
message: labels.confirmDelete,
|
|
102
|
+
confirmLabel: labels.deleteCollection,
|
|
103
|
+
cancelLabel: labels.cancel,
|
|
104
|
+
tone: "danger",
|
|
105
|
+
});
|
|
106
|
+
if (!confirmed) return;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const res = await fetch(`/api/smart-collections/${smartCollectionId}`, {
|
|
110
|
+
method: "DELETE",
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
113
|
+
showToast(labels.deleted);
|
|
114
|
+
window.location.href = redirectUrl;
|
|
115
|
+
} catch {
|
|
116
|
+
showToast(labels.saveFailed, "error");
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const handleAddToNavigation = async () => {
|
|
121
|
+
if (!addToNavigationButton || addToNavigationButton.disabled) return;
|
|
122
|
+
|
|
123
|
+
closeMenu(false);
|
|
124
|
+
const label = addToNavigationButton.querySelector<HTMLElement>(
|
|
125
|
+
".collections-page-menu-item-label",
|
|
126
|
+
);
|
|
127
|
+
addToNavigationButton.disabled = true;
|
|
128
|
+
if (label) label.textContent = labels.addingToNavigation;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await addSmartCollectionToNavigation(smartCollectionId);
|
|
132
|
+
|
|
133
|
+
addToNavigationButton.hidden = true;
|
|
134
|
+
if (editNavigationLink) editNavigationLink.hidden = false;
|
|
135
|
+
root.dataset.collectionInNavigation = "true";
|
|
136
|
+
showToastWithAction(labels.addedToNavigation, {
|
|
137
|
+
label: labels.editNavigation,
|
|
138
|
+
href:
|
|
139
|
+
editNavigationLink?.getAttribute("href") ??
|
|
140
|
+
NAVIGATION_SETTINGS_PATH,
|
|
141
|
+
});
|
|
142
|
+
} catch {
|
|
143
|
+
showToast(labels.addToNavigationFailed, "error");
|
|
144
|
+
} finally {
|
|
145
|
+
addToNavigationButton.disabled = false;
|
|
146
|
+
if (label) label.textContent = labels.addToNavigation;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
trigger.addEventListener("click", (event) => {
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
event.stopPropagation();
|
|
153
|
+
if (menu.hidden) openMenu(false);
|
|
154
|
+
else closeMenu(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
trigger.addEventListener("keydown", (event) => {
|
|
158
|
+
if (
|
|
159
|
+
event.key === "Enter" ||
|
|
160
|
+
event.key === " " ||
|
|
161
|
+
event.key === "ArrowDown"
|
|
162
|
+
) {
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
if (menu.hidden) openMenu(true);
|
|
165
|
+
else closeMenu(false);
|
|
166
|
+
}
|
|
167
|
+
if (event.key === "Escape") {
|
|
168
|
+
event.preventDefault();
|
|
169
|
+
closeMenu(true);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
menu.addEventListener("keydown", (event) => {
|
|
174
|
+
if (event.key === "Escape") {
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
closeMenu(true);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
root.addEventListener("click", (event) => {
|
|
181
|
+
const target = event.target as HTMLElement | null;
|
|
182
|
+
const actionEl = target?.closest<HTMLElement>(
|
|
183
|
+
"[data-collection-page-action]",
|
|
184
|
+
);
|
|
185
|
+
if (!actionEl || !root.contains(actionEl)) return;
|
|
186
|
+
|
|
187
|
+
const action = actionEl.dataset.collectionPageAction;
|
|
188
|
+
if (action === "edit") {
|
|
189
|
+
event.preventDefault();
|
|
190
|
+
void handleEdit();
|
|
191
|
+
}
|
|
192
|
+
if (action === "delete") {
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
void handleDelete();
|
|
195
|
+
}
|
|
196
|
+
if (action === "add-to-navigation") {
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
void handleAddToNavigation();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
document.addEventListener("click", (event) => {
|
|
203
|
+
if (!(event.target instanceof Node)) return;
|
|
204
|
+
if (!root.contains(event.target)) closeMenu(false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
root.dataset.smartCollectionPageActionsInitialized = "true";
|
|
208
|
+
});
|