@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { JantComposeDialog } from "./components/jant-compose-dialog.js";
|
|
2
|
+
import { ensureCompose } from "./lazy-entries.js";
|
|
2
3
|
import { showToast } from "./toast.js";
|
|
3
4
|
|
|
4
5
|
interface ReplyToData {
|
|
@@ -17,12 +18,64 @@ export interface ComposeOpenOptions {
|
|
|
17
18
|
initialFormat?: "note" | "link" | "quote";
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* The composer element the server rendered, upgraded or not.
|
|
23
|
+
*
|
|
24
|
+
* Use it to ask whether the page has a composer; to call it, go through
|
|
25
|
+
* {@link ensureComposeDialog}.
|
|
26
|
+
*
|
|
27
|
+
* @returns The element, or null when the page has no composer
|
|
28
|
+
*/
|
|
20
29
|
export function getComposeDialog(): JantComposeDialog | null {
|
|
21
30
|
return document.querySelector(
|
|
22
31
|
"jant-compose-dialog",
|
|
23
32
|
) as JantComposeDialog | null;
|
|
24
33
|
}
|
|
25
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The composer, ready to take calls.
|
|
37
|
+
*
|
|
38
|
+
* The composer ships in its own bundle, loaded on first use, so the element
|
|
39
|
+
* the server rendered may still be an unupgraded tag when a button is
|
|
40
|
+
* clicked. This loads the bundle — which defines the element and upgrades it
|
|
41
|
+
* in place — before handing the element back.
|
|
42
|
+
*
|
|
43
|
+
* @returns The upgraded composer, or null when the page has no composer
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const dialog = await ensureComposeDialog();
|
|
47
|
+
* await dialog?.openTranslation(threadId, "zh-Hans");
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export async function ensureComposeDialog(): Promise<JantComposeDialog | null> {
|
|
51
|
+
const dialog = getComposeDialog();
|
|
52
|
+
if (!dialog) return null;
|
|
53
|
+
await ensureCompose();
|
|
54
|
+
return dialog;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The composer's translated strings, readable before its bundle has loaded.
|
|
59
|
+
*
|
|
60
|
+
* The post menu borrows labels the server rendered onto the composer for its
|
|
61
|
+
* inline collection form. Until the element upgrades, Lit has not parsed the
|
|
62
|
+
* `labels` attribute into a property, so read the attribute directly then.
|
|
63
|
+
*
|
|
64
|
+
* @returns The labels, or null when the page has no composer or no labels
|
|
65
|
+
*/
|
|
66
|
+
export function readComposeDialogLabels(): JantComposeDialog["labels"] | null {
|
|
67
|
+
const dialog = getComposeDialog();
|
|
68
|
+
if (!dialog) return null;
|
|
69
|
+
if (dialog.labels) return dialog.labels;
|
|
70
|
+
const raw = dialog.getAttribute("labels");
|
|
71
|
+
if (!raw) return null;
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(raw) as JantComposeDialog["labels"];
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
26
79
|
export function getActiveCollectionId(): string | undefined {
|
|
27
80
|
return (
|
|
28
81
|
document.querySelector<HTMLElement>(
|
|
@@ -133,14 +186,35 @@ function getReplyData(article: HTMLElement): ReplyToData {
|
|
|
133
186
|
export async function openNewCompose(
|
|
134
187
|
options?: ComposeOpenOptions,
|
|
135
188
|
): Promise<void> {
|
|
136
|
-
await
|
|
189
|
+
const dialog = await ensureComposeDialog();
|
|
190
|
+
await dialog?.openNew(options);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Open a post in the composer for editing.
|
|
195
|
+
*
|
|
196
|
+
* @param postId - The post to edit
|
|
197
|
+
*/
|
|
198
|
+
export async function openEditForPost(postId: string): Promise<void> {
|
|
199
|
+
const dialog = await ensureComposeDialog();
|
|
200
|
+
await dialog?.openEdit(postId);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Open a draft in the composer, from the draft preview page.
|
|
205
|
+
*
|
|
206
|
+
* @param postId - The draft to continue
|
|
207
|
+
*/
|
|
208
|
+
export async function openDraftForPost(postId: string): Promise<void> {
|
|
209
|
+
const dialog = await ensureComposeDialog();
|
|
210
|
+
await dialog?.openDraft(postId);
|
|
137
211
|
}
|
|
138
212
|
|
|
139
213
|
export async function openReplyForArticle(article: HTMLElement): Promise<void> {
|
|
140
214
|
const postId = article.dataset.postId;
|
|
141
215
|
if (!postId) return;
|
|
142
216
|
|
|
143
|
-
const dialog =
|
|
217
|
+
const dialog = await ensureComposeDialog();
|
|
144
218
|
if (!dialog) return;
|
|
145
219
|
|
|
146
220
|
// Only surfaces that hide the trailing draft redirect to it — the feed hands
|
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
getComposeDialog,
|
|
4
4
|
getCurrentDetailPostArticle,
|
|
5
5
|
getReplyTargetArticle,
|
|
6
|
+
openDraftForPost,
|
|
7
|
+
openEditForPost,
|
|
6
8
|
openNewCompose,
|
|
7
9
|
openReplyForArticle,
|
|
8
10
|
} from "./compose-launch.js";
|
|
@@ -64,14 +66,13 @@ function openEditFromQueryParam() {
|
|
|
64
66
|
|
|
65
67
|
const article = getCurrentDetailPostArticle();
|
|
66
68
|
const postId = article?.dataset.postId;
|
|
67
|
-
|
|
68
|
-
if (!postId || !composeEl) return;
|
|
69
|
+
if (!postId || !getComposeDialog()) return;
|
|
69
70
|
|
|
70
71
|
consumeEditQueryParam();
|
|
71
72
|
if (document.querySelector("[data-preview-status]")) {
|
|
72
|
-
void
|
|
73
|
+
void openDraftForPost(postId);
|
|
73
74
|
} else {
|
|
74
|
-
void
|
|
75
|
+
void openEditForPost(postId);
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -141,10 +142,7 @@ document.addEventListener("keydown", (event: globalThis.KeyboardEvent) => {
|
|
|
141
142
|
const postId = article.dataset.postId;
|
|
142
143
|
if (!postId) return;
|
|
143
144
|
event.preventDefault();
|
|
144
|
-
|
|
145
|
-
if (composeEl) {
|
|
146
|
-
void composeEl.openEdit(postId);
|
|
147
|
-
}
|
|
145
|
+
void openEditForPost(postId);
|
|
148
146
|
return;
|
|
149
147
|
}
|
|
150
148
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-rendered buttons that open the composer.
|
|
3
|
+
*
|
|
4
|
+
* The composer ships in its own bundle, loaded on first use, so markup the
|
|
5
|
+
* server renders must not call the element directly: an unupgraded
|
|
6
|
+
* `<jant-compose-dialog>` has no methods yet. Buttons carry a data attribute
|
|
7
|
+
* instead, and this module — always on a signed-in page — turns the click
|
|
8
|
+
* into a call through `compose-launch.ts`, which loads the bundle first.
|
|
9
|
+
*
|
|
10
|
+
* - `[data-compose-open]`, with an optional `data-compose-collection-id`:
|
|
11
|
+
* start a new post
|
|
12
|
+
* - `[data-reply-trigger]` inside `article[data-post]`: reply to that post
|
|
13
|
+
* - `[data-draft-continue]`: reopen an inline draft in the editor
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
openEditForPost,
|
|
18
|
+
openNewCompose,
|
|
19
|
+
openReplyForArticle,
|
|
20
|
+
} from "./compose-launch.js";
|
|
21
|
+
|
|
22
|
+
document.addEventListener("click", (event: MouseEvent) => {
|
|
23
|
+
const origin =
|
|
24
|
+
event.target instanceof globalThis.Element ? event.target : null;
|
|
25
|
+
if (!origin) return;
|
|
26
|
+
|
|
27
|
+
const composeOpen = origin.closest<HTMLElement>("[data-compose-open]");
|
|
28
|
+
if (composeOpen) {
|
|
29
|
+
const collectionId = composeOpen.dataset.composeCollectionId;
|
|
30
|
+
void openNewCompose(collectionId ? { collectionId } : undefined);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const replyTrigger = origin.closest<HTMLElement>("[data-reply-trigger]");
|
|
35
|
+
if (replyTrigger) {
|
|
36
|
+
const article = replyTrigger.closest<HTMLElement>("article[data-post]");
|
|
37
|
+
if (article) void openReplyForArticle(article);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The badge on an inline draft doubles as its most likely next action.
|
|
42
|
+
// Publish and Delete live in the post menu with every other post action.
|
|
43
|
+
const draftContinue = origin.closest<HTMLElement>("[data-draft-continue]");
|
|
44
|
+
if (draftContinue) {
|
|
45
|
+
const postId =
|
|
46
|
+
draftContinue.dataset.postId ??
|
|
47
|
+
draftContinue.closest<HTMLElement>("article[data-post]")?.dataset.postId;
|
|
48
|
+
if (postId) void openEditForPost(postId);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy field behavior.
|
|
3
|
+
*
|
|
4
|
+
* Enhances the read-only address fields rendered by `ui/shared/CopyField.tsx`
|
|
5
|
+
* and by the feed URL rows in General settings. Delegated from the document, so
|
|
6
|
+
* fields that appear later — the settings page renders its rows from Lit after
|
|
7
|
+
* load — are covered without re-running anything.
|
|
8
|
+
*
|
|
9
|
+
* Two behaviors carry the weight here and must both stay:
|
|
10
|
+
*
|
|
11
|
+
* - The button writes the address to the clipboard and reports either way.
|
|
12
|
+
* - Clicking or focusing the input selects the whole address. The Clipboard API
|
|
13
|
+
* is absent on insecure origins and can be denied by permission, so manual
|
|
14
|
+
* selection is what keeps the address obtainable when the button cannot work.
|
|
15
|
+
*
|
|
16
|
+
* The server renders the button `hidden` — without this script it would be an
|
|
17
|
+
* affordance that does nothing — and it is revealed here. The input alone is
|
|
18
|
+
* the no-JavaScript state, and it is a usable one.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
COPY_FIELD_BUTTON_ATTR,
|
|
23
|
+
COPY_FIELD_FAILED_ATTR,
|
|
24
|
+
COPY_FIELD_VALUE_ATTR,
|
|
25
|
+
} from "../lib/copy-field.js";
|
|
26
|
+
import { showToast } from "./toast.js";
|
|
27
|
+
|
|
28
|
+
const BUTTON_SELECTOR = `[${COPY_FIELD_BUTTON_ATTR}]`;
|
|
29
|
+
const VALUE_SELECTOR = `[${COPY_FIELD_VALUE_ATTR}]`;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Find the address a copy button belongs to.
|
|
33
|
+
*
|
|
34
|
+
* @param button - The copy button that was activated
|
|
35
|
+
* @returns The input holding the address, or null when the field is malformed
|
|
36
|
+
*/
|
|
37
|
+
function findValueInput(button: HTMLElement): HTMLInputElement | null {
|
|
38
|
+
return (
|
|
39
|
+
button.parentElement?.querySelector<HTMLInputElement>(VALUE_SELECTOR) ??
|
|
40
|
+
button
|
|
41
|
+
.closest("[data-copy-field-root]")
|
|
42
|
+
?.querySelector<HTMLInputElement>(VALUE_SELECTOR) ??
|
|
43
|
+
null
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function copy(button: HTMLElement): Promise<void> {
|
|
48
|
+
const input = findValueInput(button);
|
|
49
|
+
if (!input) return;
|
|
50
|
+
|
|
51
|
+
const failedMessage = button.getAttribute(COPY_FIELD_FAILED_ATTR) ?? "";
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
if (!globalThis.navigator.clipboard?.writeText) {
|
|
55
|
+
throw new Error("Clipboard unavailable");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await globalThis.navigator.clipboard.writeText(input.value);
|
|
59
|
+
showToast(button.getAttribute(COPY_FIELD_BUTTON_ATTR) || "");
|
|
60
|
+
} catch {
|
|
61
|
+
showToast(failedMessage, "error");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Reveal buttons the server rendered hidden. */
|
|
66
|
+
function revealButtons(root: globalThis.ParentNode = document): void {
|
|
67
|
+
for (const button of Array.from(
|
|
68
|
+
root.querySelectorAll<HTMLElement>(`${BUTTON_SELECTOR}[hidden]`),
|
|
69
|
+
)) {
|
|
70
|
+
button.hidden = false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
document.addEventListener("click", (event: globalThis.MouseEvent) => {
|
|
75
|
+
const target = event.target;
|
|
76
|
+
if (!(target instanceof globalThis.Element)) return;
|
|
77
|
+
|
|
78
|
+
const button = target.closest<HTMLElement>(BUTTON_SELECTOR);
|
|
79
|
+
if (button) {
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
void copy(button);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const input = target.closest<HTMLInputElement>(VALUE_SELECTOR);
|
|
86
|
+
if (input) input.select();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Focus does not bubble; its capturing counterpart does.
|
|
90
|
+
document.addEventListener(
|
|
91
|
+
"focus",
|
|
92
|
+
(event: globalThis.FocusEvent) => {
|
|
93
|
+
const target = event.target;
|
|
94
|
+
if (!(target instanceof globalThis.Element)) return;
|
|
95
|
+
|
|
96
|
+
const input = target.closest<HTMLInputElement>(VALUE_SELECTOR);
|
|
97
|
+
if (input) input.select();
|
|
98
|
+
},
|
|
99
|
+
true,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Module scripts are deferred, so by the time this runs the server's fields are
|
|
103
|
+
// already parsed — but the listener is registered unconditionally rather than
|
|
104
|
+
// branching on readyState, so a field that arrives late is revealed too.
|
|
105
|
+
revealButtons();
|
|
106
|
+
document.addEventListener("DOMContentLoaded", () => revealButtons(), {
|
|
107
|
+
once: true,
|
|
108
|
+
});
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom URL action menus on the settings page.
|
|
3
3
|
*
|
|
4
|
-
* Keeps row menus mutually exclusive and dismisses them on
|
|
5
|
-
* and
|
|
4
|
+
* Keeps row menus mutually exclusive and dismisses them on any click
|
|
5
|
+
* outside the trigger and the menu — including the rest of their own row —
|
|
6
|
+
* and on Escape.
|
|
7
|
+
*
|
|
8
|
+
* The row list rounds its corners with `overflow: hidden`, and no stacking
|
|
9
|
+
* order escapes an ancestor's clip. So an open menu is promoted to the top
|
|
10
|
+
* layer, which takes it out of the clip entirely and leaves it positioned
|
|
11
|
+
* against the viewport — hence the anchoring done here in script. Browsers
|
|
12
|
+
* without the popover API fall back to the absolutely positioned menu, which
|
|
13
|
+
* is clipped but still usable.
|
|
6
14
|
*/
|
|
7
15
|
|
|
16
|
+
/** Space between the trigger and the menu, in pixels. */
|
|
17
|
+
const MENU_GAP = 6;
|
|
18
|
+
/** Closest the menu may sit to a viewport edge, in pixels. */
|
|
19
|
+
const VIEWPORT_MARGIN = 8;
|
|
20
|
+
|
|
8
21
|
/**
|
|
9
22
|
* Initialize custom URL action menus within a root.
|
|
10
23
|
*
|
|
@@ -30,8 +43,39 @@ export function initCustomUrlMenus(
|
|
|
30
43
|
|
|
31
44
|
if (!(trigger instanceof HTMLButtonElement) || !menu) return;
|
|
32
45
|
|
|
46
|
+
const topLayer =
|
|
47
|
+
typeof menu.showPopover === "function" && menu.hasAttribute("popover");
|
|
48
|
+
|
|
49
|
+
/** Anchor the top-layer menu under the trigger, flipping above it when
|
|
50
|
+
* the viewport bottom is closer than the menu is tall. */
|
|
51
|
+
const anchorToTrigger = () => {
|
|
52
|
+
const anchor = trigger.getBoundingClientRect();
|
|
53
|
+
const { width, height } = menu.getBoundingClientRect();
|
|
54
|
+
const below = anchor.bottom + MENU_GAP;
|
|
55
|
+
const above = anchor.top - MENU_GAP - height;
|
|
56
|
+
const top =
|
|
57
|
+
below + height <= window.innerHeight - VIEWPORT_MARGIN ||
|
|
58
|
+
above < VIEWPORT_MARGIN
|
|
59
|
+
? below
|
|
60
|
+
: above;
|
|
61
|
+
|
|
62
|
+
menu.style.top = `${top}px`;
|
|
63
|
+
menu.style.left = `${Math.max(
|
|
64
|
+
VIEWPORT_MARGIN,
|
|
65
|
+
Math.min(
|
|
66
|
+
anchor.right - width,
|
|
67
|
+
window.innerWidth - VIEWPORT_MARGIN - width,
|
|
68
|
+
),
|
|
69
|
+
)}px`;
|
|
70
|
+
};
|
|
71
|
+
|
|
33
72
|
const close = (focusTrigger = false) => {
|
|
34
73
|
if (menu.hidden) return;
|
|
74
|
+
if (topLayer) {
|
|
75
|
+
window.removeEventListener("resize", anchorToTrigger);
|
|
76
|
+
window.removeEventListener("scroll", anchorToTrigger, true);
|
|
77
|
+
if (menu.matches(":popover-open")) menu.hidePopover();
|
|
78
|
+
}
|
|
35
79
|
menu.hidden = true;
|
|
36
80
|
trigger.setAttribute("aria-expanded", "false");
|
|
37
81
|
if (focusTrigger) trigger.focus();
|
|
@@ -44,6 +88,14 @@ export function initCustomUrlMenus(
|
|
|
44
88
|
}),
|
|
45
89
|
);
|
|
46
90
|
menu.hidden = false;
|
|
91
|
+
if (topLayer) {
|
|
92
|
+
if (!menu.matches(":popover-open")) menu.showPopover();
|
|
93
|
+
anchorToTrigger();
|
|
94
|
+
window.addEventListener("resize", anchorToTrigger);
|
|
95
|
+
// Capture phase: the menu follows a scroll of any ancestor, not
|
|
96
|
+
// only the document.
|
|
97
|
+
window.addEventListener("scroll", anchorToTrigger, true);
|
|
98
|
+
}
|
|
47
99
|
trigger.setAttribute("aria-expanded", "true");
|
|
48
100
|
if (focusFirstItem) {
|
|
49
101
|
const firstItem =
|
|
@@ -105,11 +157,15 @@ export function initCustomUrlMenus(
|
|
|
105
157
|
}
|
|
106
158
|
});
|
|
107
159
|
|
|
160
|
+
// The row is the action root, but it is not the menu's dismiss region:
|
|
161
|
+
// the rest of the row is blank space like any other, so only the
|
|
162
|
+
// trigger and the menu itself count as a click inside.
|
|
108
163
|
document.addEventListener("click", (event) => {
|
|
109
164
|
if (!(event.target instanceof Node)) return;
|
|
110
|
-
if (
|
|
111
|
-
|
|
165
|
+
if (trigger.contains(event.target) || menu.contains(event.target)) {
|
|
166
|
+
return;
|
|
112
167
|
}
|
|
168
|
+
close(false);
|
|
113
169
|
});
|
|
114
170
|
|
|
115
171
|
document.addEventListener("jant:custom-url-menu", (event) => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline SVG for the few Lucide icons client components render themselves.
|
|
3
|
+
*
|
|
4
|
+
* Server-rendered UI goes through `lib/icons.ts`, which indexes the whole
|
|
5
|
+
* `lucide-static` set (1,700+ icons, ≈1 MB minified). Client code must not
|
|
6
|
+
* import that module — one icon lookup would ship the entire set to the
|
|
7
|
+
* browser — so the handful of icons a component needs at runtime live here as
|
|
8
|
+
* literal markup, copied from `lucide-static` with the project's `icon-fine`
|
|
9
|
+
* stroke class appended.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Lucide `funnel`, the marker for a smart collection. */
|
|
13
|
+
export const FUNNEL_ICON_SVG =
|
|
14
|
+
'<svg class="lucide lucide-funnel icon-fine" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z"/></svg>';
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image processing plan and worker protocol.
|
|
3
|
+
*
|
|
4
|
+
* The pure decisions shared by the `ImageProcessor` facade on the main thread
|
|
5
|
+
* and the `image-worker` that decodes and encodes off it. Nothing here touches
|
|
6
|
+
* the DOM, so both sides — and the tests — import it without a canvas.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Cap for the shorter image side — the side that determines text sharpness. */
|
|
10
|
+
export const MAX_SHORT_SIDE = 1920;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Largest long side we can still redraw on a canvas. A canvas bounded by
|
|
14
|
+
* MAX_SHORT_SIDE × MAX_LONG_SIDE (1920 × 8192 ≈ 15.7M px) stays under the
|
|
15
|
+
* ~16.7M-pixel area limit older mobile Safari enforces. Anything longer
|
|
16
|
+
* can't be re-encoded, so it uploads as-is.
|
|
17
|
+
*/
|
|
18
|
+
export const MAX_LONG_SIDE = 8192;
|
|
19
|
+
|
|
20
|
+
/** Longest side of the tiny canvas a blurhash is sampled from. */
|
|
21
|
+
export const BLURHASH_MAX_SIDE = 32;
|
|
22
|
+
|
|
23
|
+
export interface ImageProcessOptions {
|
|
24
|
+
maxShortSide: number;
|
|
25
|
+
maxLongSide: number;
|
|
26
|
+
quality: number;
|
|
27
|
+
mimeType: "image/webp" | "image/jpeg" | "image/png";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_IMAGE_PROCESS_OPTIONS: ImageProcessOptions = {
|
|
31
|
+
maxShortSide: MAX_SHORT_SIDE,
|
|
32
|
+
maxLongSide: MAX_LONG_SIDE,
|
|
33
|
+
quality: 0.85,
|
|
34
|
+
mimeType: "image/webp",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export interface ImageProcessPlan {
|
|
38
|
+
/** When true, upload the original file untouched (too large to re-encode). */
|
|
39
|
+
passthrough: boolean;
|
|
40
|
+
/** Target dimensions — equal to the source dimensions when `passthrough`. */
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decide how to handle an image given its source dimensions.
|
|
47
|
+
*
|
|
48
|
+
* - Long side over `maxLongSide` → `passthrough` (canvas can't redraw it).
|
|
49
|
+
* - Short side within `maxShortSide` → keep dimensions, just re-encode.
|
|
50
|
+
* - Otherwise → scale down so the short side hits `maxShortSide`.
|
|
51
|
+
*
|
|
52
|
+
* @param sourceWidth - Natural image width in pixels
|
|
53
|
+
* @param sourceHeight - Natural image height in pixels
|
|
54
|
+
* @param options - `maxShortSide` and `maxLongSide` caps
|
|
55
|
+
* @returns The processing plan
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* planImageProcessing(1080, 6000, { maxShortSide: 1920, maxLongSide: 8192 });
|
|
60
|
+
* // { passthrough: false, width: 1080, height: 6000 }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function planImageProcessing(
|
|
64
|
+
sourceWidth: number,
|
|
65
|
+
sourceHeight: number,
|
|
66
|
+
options: { maxShortSide: number; maxLongSide: number },
|
|
67
|
+
): ImageProcessPlan {
|
|
68
|
+
const longSide = Math.max(sourceWidth, sourceHeight);
|
|
69
|
+
if (longSide > options.maxLongSide) {
|
|
70
|
+
return { passthrough: true, width: sourceWidth, height: sourceHeight };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const shortSide = Math.min(sourceWidth, sourceHeight);
|
|
74
|
+
if (shortSide <= options.maxShortSide) {
|
|
75
|
+
return { passthrough: false, width: sourceWidth, height: sourceHeight };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const scale = options.maxShortSide / shortSide;
|
|
79
|
+
return {
|
|
80
|
+
passthrough: false,
|
|
81
|
+
width: Math.round(sourceWidth * scale),
|
|
82
|
+
height: Math.round(sourceHeight * scale),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Settle the encoding format after a probe encode.
|
|
88
|
+
*
|
|
89
|
+
* A browser that cannot encode the requested format does not fail — it
|
|
90
|
+
* silently hands back a PNG (Safari has no WebP encoder). PNG ignores the
|
|
91
|
+
* quality parameter and produces oversized files, so anything other than the
|
|
92
|
+
* format asked for means "encode as JPEG", which every engine can do lossily.
|
|
93
|
+
* Probing a 1×1 canvas once beats encoding every photo twice.
|
|
94
|
+
*
|
|
95
|
+
* @param requested - The MIME type asked of the encoder
|
|
96
|
+
* @param produced - The MIME type of the blob it returned
|
|
97
|
+
* @returns The MIME type to encode real images with
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* resolveEncodedMimeType("image/webp", "image/png"); // "image/jpeg"
|
|
102
|
+
* resolveEncodedMimeType("image/webp", "image/webp"); // "image/webp"
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export function resolveEncodedMimeType(
|
|
106
|
+
requested: string,
|
|
107
|
+
produced: string,
|
|
108
|
+
): string {
|
|
109
|
+
return produced === requested ? requested : "image/jpeg";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Size of the canvas a blurhash is sampled from: the image scaled so its
|
|
114
|
+
* longest side is at most {@link BLURHASH_MAX_SIDE}, never below 1px.
|
|
115
|
+
*
|
|
116
|
+
* @param width - Source width in pixels
|
|
117
|
+
* @param height - Source height in pixels
|
|
118
|
+
* @returns The sample canvas dimensions
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* blurhashDimensions(4032, 3024); // { width: 32, height: 24 }
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export function blurhashDimensions(
|
|
126
|
+
width: number,
|
|
127
|
+
height: number,
|
|
128
|
+
): { width: number; height: number } {
|
|
129
|
+
const scale = Math.min(
|
|
130
|
+
BLURHASH_MAX_SIDE / width,
|
|
131
|
+
BLURHASH_MAX_SIDE / height,
|
|
132
|
+
1,
|
|
133
|
+
);
|
|
134
|
+
return {
|
|
135
|
+
width: Math.max(Math.round(width * scale), 1),
|
|
136
|
+
height: Math.max(Math.round(height * scale), 1),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Worker protocol ──────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
export interface ImageWorkerRequest {
|
|
143
|
+
id: number;
|
|
144
|
+
file: File;
|
|
145
|
+
options: ImageProcessOptions;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface ImageWorkerSuccess {
|
|
149
|
+
id: number;
|
|
150
|
+
ok: true;
|
|
151
|
+
blob: Blob;
|
|
152
|
+
width: number;
|
|
153
|
+
height: number;
|
|
154
|
+
/** False when `blob` is the untouched original (too large to re-encode). */
|
|
155
|
+
processed: boolean;
|
|
156
|
+
blurhash?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface ImageWorkerFailure {
|
|
160
|
+
id: number;
|
|
161
|
+
ok: false;
|
|
162
|
+
error: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export type ImageWorkerResponse = ImageWorkerSuccess | ImageWorkerFailure;
|