@jant/core 0.6.16 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +60 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +16 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +159 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +23 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +5 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +471 -369
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +178 -17
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post.test.ts +46 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +278 -19
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1087 -7219
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { Editor } from "@tiptap/core";
|
|
5
|
+
import { TextSelection } from "@tiptap/pm/state";
|
|
6
|
+
import { createEditorExtensions } from "../extensions.js";
|
|
7
|
+
import { clearFormatting, toggleMarkAndExit } from "../bubble-menu.js";
|
|
8
|
+
|
|
9
|
+
const editors: Editor[] = [];
|
|
10
|
+
|
|
11
|
+
function createEditor(content: string): Editor {
|
|
12
|
+
const element = document.createElement("div");
|
|
13
|
+
document.body.appendChild(element);
|
|
14
|
+
|
|
15
|
+
const editor = new Editor({
|
|
16
|
+
element,
|
|
17
|
+
extensions: createEditorExtensions(),
|
|
18
|
+
content,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
editors.push(editor);
|
|
22
|
+
return editor;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getBubbleMenu(editor: Editor): HTMLElement {
|
|
26
|
+
const el = (editor.options.element as HTMLElement)
|
|
27
|
+
.closest("body")
|
|
28
|
+
?.querySelector<HTMLElement>(".tiptap-bubble-menu");
|
|
29
|
+
if (!el) throw new Error("Bubble menu was never created");
|
|
30
|
+
return el;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isVisible(editor: Editor): boolean {
|
|
34
|
+
return getBubbleMenu(editor).style.display !== "none";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
while (editors.length) editors.pop()?.destroy();
|
|
39
|
+
document.body.innerHTML = "";
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("bubble menu visibility", () => {
|
|
43
|
+
it("stays hidden while the selection is empty", () => {
|
|
44
|
+
const editor = createEditor("<p>A quiet note</p>");
|
|
45
|
+
editor.commands.focus();
|
|
46
|
+
|
|
47
|
+
expect(isVisible(editor)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("shows for a text selection", () => {
|
|
51
|
+
const editor = createEditor("<p>A quiet note</p>");
|
|
52
|
+
editor.view.dispatch(
|
|
53
|
+
editor.state.tr.setSelection(
|
|
54
|
+
TextSelection.create(editor.state.doc, 1, 8),
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(isVisible(editor)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("shows for select-all, which is an AllSelection rather than a text selection", () => {
|
|
62
|
+
const editor = createEditor("<p>First line</p><p>Second line</p>");
|
|
63
|
+
editor.commands.selectAll();
|
|
64
|
+
|
|
65
|
+
expect(isVisible(editor)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("stays hidden when select-all covers an empty document", () => {
|
|
69
|
+
const editor = createEditor("<p></p>");
|
|
70
|
+
editor.commands.selectAll();
|
|
71
|
+
|
|
72
|
+
expect(isVisible(editor)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("hides again once the selection collapses", () => {
|
|
76
|
+
const editor = createEditor("<p>A quiet note</p>");
|
|
77
|
+
editor.commands.selectAll();
|
|
78
|
+
expect(isVisible(editor)).toBe(true);
|
|
79
|
+
|
|
80
|
+
editor.commands.setTextSelection(2);
|
|
81
|
+
|
|
82
|
+
expect(isVisible(editor)).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("bubble menu actions on a document-wide selection", () => {
|
|
87
|
+
it("bolds the whole document and leaves the cursor outside the mark", () => {
|
|
88
|
+
const editor = createEditor("<p>First line</p><p>Second line</p>");
|
|
89
|
+
editor.commands.selectAll();
|
|
90
|
+
|
|
91
|
+
toggleMarkAndExit(editor, "bold");
|
|
92
|
+
|
|
93
|
+
expect(editor.getHTML()).toBe(
|
|
94
|
+
"<p><strong>First line</strong></p><p><strong>Second line</strong></p>",
|
|
95
|
+
);
|
|
96
|
+
expect(editor.state.selection.empty).toBe(true);
|
|
97
|
+
expect(editor.state.storedMarks ?? []).toHaveLength(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("clears formatting across every selected block", () => {
|
|
101
|
+
const editor = createEditor(
|
|
102
|
+
"<h2>A heading</h2><p><strong>bold</strong> and <em>italic</em></p>",
|
|
103
|
+
);
|
|
104
|
+
editor.commands.selectAll();
|
|
105
|
+
|
|
106
|
+
clearFormatting(editor);
|
|
107
|
+
|
|
108
|
+
expect(editor.getHTML()).toBe("<p>A heading</p><p>bold and italic</p>");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getFloatingArrowOffset,
|
|
6
|
+
getFloatingPosition,
|
|
7
|
+
getVisibleAnchorRect,
|
|
8
|
+
getVisibleClipRect,
|
|
9
|
+
} from "../floating-position.js";
|
|
5
10
|
|
|
6
11
|
describe("getFloatingPosition", () => {
|
|
7
12
|
const containerRect = {
|
|
@@ -72,3 +77,133 @@ describe("getFloatingPosition", () => {
|
|
|
72
77
|
expect(layout.top).toBe(8);
|
|
73
78
|
});
|
|
74
79
|
});
|
|
80
|
+
|
|
81
|
+
describe("getVisibleAnchorRect", () => {
|
|
82
|
+
// A three-line selection in a text column running from x=100 to x=300,
|
|
83
|
+
// inside a scroll region showing y=50..250.
|
|
84
|
+
const clipRect = { left: 100, top: 50, width: 220, height: 200 };
|
|
85
|
+
const lines = [
|
|
86
|
+
{ left: 180, right: 300, top: 60, bottom: 80 },
|
|
87
|
+
{ left: 100, right: 300, top: 80, bottom: 100 },
|
|
88
|
+
{ left: 100, right: 160, top: 100, bottom: 120 },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
it("centers on the whole selection but hugs its first line", () => {
|
|
92
|
+
const anchor = getVisibleAnchorRect(lines, clipRect);
|
|
93
|
+
|
|
94
|
+
expect(anchor).toEqual({ left: 100, right: 300, top: 60, bottom: 80 });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("ignores an enclosing block rect when picking the first line", () => {
|
|
98
|
+
const anchor = getVisibleAnchorRect(
|
|
99
|
+
[{ left: 100, right: 300, top: 60, bottom: 120 }, ...lines],
|
|
100
|
+
clipRect,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(anchor?.bottom).toBe(80);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("anchors to the first line still on screen when the selection scrolls past the top", () => {
|
|
107
|
+
const anchor = getVisibleAnchorRect(
|
|
108
|
+
[
|
|
109
|
+
{ left: 100, right: 300, top: -40, bottom: -20 },
|
|
110
|
+
{ left: 100, right: 300, top: -20, bottom: 0 },
|
|
111
|
+
...lines,
|
|
112
|
+
],
|
|
113
|
+
clipRect,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(anchor).toEqual({ left: 100, right: 300, top: 60, bottom: 80 });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("clips a selection that overruns the scroll region", () => {
|
|
120
|
+
const anchor = getVisibleAnchorRect(
|
|
121
|
+
[{ left: 100, right: 300, top: 20, bottom: 400 }],
|
|
122
|
+
clipRect,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect(anchor).toEqual({ left: 100, right: 300, top: 50, bottom: 250 });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("returns null when nothing is visible", () => {
|
|
129
|
+
const anchor = getVisibleAnchorRect(
|
|
130
|
+
[{ left: 100, right: 300, top: 300, bottom: 320 }],
|
|
131
|
+
clipRect,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(anchor).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("getVisibleClipRect", () => {
|
|
139
|
+
function stubRect(
|
|
140
|
+
el: HTMLElement,
|
|
141
|
+
rect: { left: number; top: number; width: number; height: number },
|
|
142
|
+
) {
|
|
143
|
+
const measured = {
|
|
144
|
+
...rect,
|
|
145
|
+
right: rect.left + rect.width,
|
|
146
|
+
bottom: rect.top + rect.height,
|
|
147
|
+
x: rect.left,
|
|
148
|
+
y: rect.top,
|
|
149
|
+
toJSON: () => rect,
|
|
150
|
+
};
|
|
151
|
+
el.getBoundingClientRect = () => measured;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
it("narrows the container down to a scrolling ancestor", () => {
|
|
155
|
+
const scroller = document.createElement("div");
|
|
156
|
+
scroller.style.overflowY = "auto";
|
|
157
|
+
const content = document.createElement("div");
|
|
158
|
+
scroller.appendChild(content);
|
|
159
|
+
document.body.appendChild(scroller);
|
|
160
|
+
stubRect(scroller, { left: 100, top: 120, width: 300, height: 200 });
|
|
161
|
+
|
|
162
|
+
const clip = getVisibleClipRect(content, {
|
|
163
|
+
left: 100,
|
|
164
|
+
top: 50,
|
|
165
|
+
width: 320,
|
|
166
|
+
height: 400,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(clip).toEqual({ left: 100, top: 120, width: 300, height: 200 });
|
|
170
|
+
scroller.remove();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("keeps the container when no ancestor reports a clipping region", () => {
|
|
174
|
+
const plain = document.createElement("div");
|
|
175
|
+
const content = document.createElement("div");
|
|
176
|
+
plain.appendChild(content);
|
|
177
|
+
document.body.appendChild(plain);
|
|
178
|
+
const containerRect = { left: 100, top: 50, width: 320, height: 400 };
|
|
179
|
+
|
|
180
|
+
expect(getVisibleClipRect(content, containerRect)).toEqual(containerRect);
|
|
181
|
+
plain.remove();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("getFloatingArrowOffset", () => {
|
|
186
|
+
const containerRect = { left: 100, top: 50, width: 320, height: 240 };
|
|
187
|
+
|
|
188
|
+
it("points at the anchor center", () => {
|
|
189
|
+
const offset = getFloatingArrowOffset({
|
|
190
|
+
anchorRect: { left: 200, right: 240, top: 70, bottom: 90 },
|
|
191
|
+
containerRect,
|
|
192
|
+
floatingLeft: 60,
|
|
193
|
+
floatingWidth: 160,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(offset).toBe(60);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("stays inside the surface when it is clamped away from the anchor", () => {
|
|
200
|
+
const offset = getFloatingArrowOffset({
|
|
201
|
+
anchorRect: { left: 400, right: 420, top: 70, bottom: 90 },
|
|
202
|
+
containerRect,
|
|
203
|
+
floatingLeft: 8,
|
|
204
|
+
floatingWidth: 160,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(offset).toBe(150);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -5,9 +5,11 @@ import { Editor, type JSONContent } from "@tiptap/core";
|
|
|
5
5
|
import StarterKit from "@tiptap/starter-kit";
|
|
6
6
|
import { ImageNode } from "../image-node.js";
|
|
7
7
|
import {
|
|
8
|
+
adoptPendingInlineImageUploads,
|
|
8
9
|
rehostInlineImage,
|
|
9
10
|
resolveInlineImageUrls,
|
|
10
11
|
hasPendingInlineImagePlaceholders,
|
|
12
|
+
uploadAndInsertInlineImage,
|
|
11
13
|
} from "../inline-image-upload.js";
|
|
12
14
|
|
|
13
15
|
const editors: Editor[] = [];
|
|
@@ -141,3 +143,38 @@ describe("resolveInlineImageUrls + hasPendingInlineImagePlaceholders", () => {
|
|
|
141
143
|
expect(resolved).toBe(json);
|
|
142
144
|
});
|
|
143
145
|
});
|
|
146
|
+
|
|
147
|
+
describe("adoptPendingInlineImageUploads", () => {
|
|
148
|
+
it("hands an in-flight upload on through a second adoption", async () => {
|
|
149
|
+
const first = createEditor();
|
|
150
|
+
let finishUpload: (value: { url: string }) => void = () => {};
|
|
151
|
+
const insert = uploadAndInsertInlineImage(
|
|
152
|
+
first,
|
|
153
|
+
new File(["image"], "shot.png", { type: "image/png" }),
|
|
154
|
+
() =>
|
|
155
|
+
new Promise((resolve) => {
|
|
156
|
+
finishUpload = resolve;
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
expect(imageSrcs(first)[0]?.startsWith("blob:")).toBe(true);
|
|
160
|
+
|
|
161
|
+
// Content can move more than once — fullscreen hands it to compose, and a
|
|
162
|
+
// format switch then rebuilds compose's editor under it.
|
|
163
|
+
const second = createEditor();
|
|
164
|
+
second.commands.setContent(first.getJSON());
|
|
165
|
+
adoptPendingInlineImageUploads(second);
|
|
166
|
+
|
|
167
|
+
const third = createEditor();
|
|
168
|
+
third.commands.setContent(second.getJSON());
|
|
169
|
+
const adopted = adoptPendingInlineImageUploads(third);
|
|
170
|
+
expect(adopted).toHaveLength(1);
|
|
171
|
+
// Submitting from here still knows the image is on its way.
|
|
172
|
+
expect(hasPendingInlineImagePlaceholders(third.getJSON())).toBe(true);
|
|
173
|
+
|
|
174
|
+
finishUpload({ url: "https://cdn.test/shot.webp" });
|
|
175
|
+
await insert;
|
|
176
|
+
await Promise.all(adopted);
|
|
177
|
+
|
|
178
|
+
expect(imageSrcs(third)).toEqual(["https://cdn.test/shot.webp"]);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -7,14 +7,23 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { Extension, type Editor } from "@tiptap/core";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
AllSelection,
|
|
12
|
+
Plugin,
|
|
13
|
+
PluginKey,
|
|
14
|
+
Selection,
|
|
15
|
+
TextSelection,
|
|
16
|
+
} from "@tiptap/pm/state";
|
|
11
17
|
import { liftTarget } from "@tiptap/pm/transform";
|
|
12
18
|
import type { EditorView } from "@tiptap/pm/view";
|
|
13
19
|
import { isLinkToolbarInputActive } from "./link-toolbar.js";
|
|
14
20
|
import type { FormattingToolbarMode } from "./toolbar-mode.js";
|
|
15
21
|
import {
|
|
16
22
|
getFixedFloatingContainerRect,
|
|
23
|
+
getFloatingArrowOffset,
|
|
17
24
|
getFloatingPosition,
|
|
25
|
+
getRangeAnchorRect,
|
|
26
|
+
getVisibleClipRect,
|
|
18
27
|
} from "./floating-position.js";
|
|
19
28
|
|
|
20
29
|
const bubbleMenuKey = new PluginKey("bubbleMenu");
|
|
@@ -315,20 +324,22 @@ export const BubbleMenu = Extension.create({
|
|
|
315
324
|
if (!el) return;
|
|
316
325
|
el.style.display = "flex";
|
|
317
326
|
|
|
318
|
-
//
|
|
327
|
+
// Anchor on the first visible line of the selection, centered on its
|
|
328
|
+
// horizontal span. Select-all lands on the text you are looking at
|
|
329
|
+
// rather than at the top of a document scrolled out of view.
|
|
319
330
|
const { from, to } = view.state.selection;
|
|
320
|
-
const start = view.coordsAtPos(from);
|
|
321
|
-
const end = view.coordsAtPos(to);
|
|
322
331
|
const dialog = view.dom.closest("dialog");
|
|
332
|
+
const containerRect = getFixedFloatingContainerRect(dialog);
|
|
333
|
+
const anchorRect = getRangeAnchorRect(
|
|
334
|
+
view,
|
|
335
|
+
from,
|
|
336
|
+
to,
|
|
337
|
+
getVisibleClipRect(view.dom, containerRect),
|
|
338
|
+
);
|
|
323
339
|
const rect = el.getBoundingClientRect();
|
|
324
340
|
const layout = getFloatingPosition({
|
|
325
|
-
anchorRect
|
|
326
|
-
|
|
327
|
-
right: end.right,
|
|
328
|
-
top: Math.min(start.top, end.top),
|
|
329
|
-
bottom: Math.max(start.bottom, end.bottom),
|
|
330
|
-
},
|
|
331
|
-
containerRect: getFixedFloatingContainerRect(dialog),
|
|
341
|
+
anchorRect,
|
|
342
|
+
containerRect,
|
|
332
343
|
floatingWidth: rect.width,
|
|
333
344
|
floatingHeight: rect.height,
|
|
334
345
|
preferredPlacement: "top",
|
|
@@ -338,6 +349,16 @@ export const BubbleMenu = Extension.create({
|
|
|
338
349
|
|
|
339
350
|
el.style.left = `${layout.left}px`;
|
|
340
351
|
el.style.top = `${layout.top}px`;
|
|
352
|
+
el.dataset.placement = layout.placement;
|
|
353
|
+
el.style.setProperty(
|
|
354
|
+
"--floating-arrow-x",
|
|
355
|
+
`${getFloatingArrowOffset({
|
|
356
|
+
anchorRect,
|
|
357
|
+
containerRect,
|
|
358
|
+
floatingLeft: layout.left,
|
|
359
|
+
floatingWidth: rect.width,
|
|
360
|
+
})}px`,
|
|
361
|
+
);
|
|
341
362
|
|
|
342
363
|
syncActive();
|
|
343
364
|
}
|
|
@@ -358,13 +379,23 @@ export const BubbleMenu = Extension.create({
|
|
|
358
379
|
function shouldShow(view: EditorView): boolean {
|
|
359
380
|
const { state } = view;
|
|
360
381
|
const { selection } = state;
|
|
361
|
-
|
|
362
|
-
if (!(selection instanceof TextSelection) || selection.empty)
|
|
363
|
-
return false;
|
|
364
|
-
if (!selection.$from.parent.isTextblock) return false;
|
|
382
|
+
if (selection.empty) return false;
|
|
365
383
|
// Hide when link input popup is open
|
|
366
384
|
if (isLinkToolbarInputActive()) return false;
|
|
367
|
-
|
|
385
|
+
|
|
386
|
+
// Select-all reports an AllSelection, not a TextSelection. Clearing
|
|
387
|
+
// formatting off a pasted draft and quoting a whole note are exactly
|
|
388
|
+
// what a document-wide selection is for, so it gets the same toolbar as
|
|
389
|
+
// dragging over everything — as long as there is text to format.
|
|
390
|
+
if (selection instanceof AllSelection) {
|
|
391
|
+
return (
|
|
392
|
+
state.doc.textBetween(selection.from, selection.to, " ").trim() !== ""
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Otherwise only non-empty text selections (not node selections)
|
|
397
|
+
if (!(selection instanceof TextSelection)) return false;
|
|
398
|
+
return selection.$from.parent.isTextblock;
|
|
368
399
|
}
|
|
369
400
|
|
|
370
401
|
return [
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import type { EditorView } from "@tiptap/pm/view";
|
|
2
|
+
|
|
3
|
+
export interface FloatingAnchorRect {
|
|
2
4
|
left: number;
|
|
3
5
|
right: number;
|
|
4
6
|
top: number;
|
|
5
7
|
bottom: number;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
interface FloatingContainerRect {
|
|
10
|
+
export interface FloatingContainerRect {
|
|
9
11
|
left: number;
|
|
10
12
|
top: number;
|
|
11
13
|
width: number;
|
|
@@ -165,3 +167,215 @@ export function getFloatingPosition(
|
|
|
165
167
|
maxHeight,
|
|
166
168
|
};
|
|
167
169
|
}
|
|
170
|
+
|
|
171
|
+
/** Overflow values that clip a descendant out of view. */
|
|
172
|
+
const CLIPPING_OVERFLOW = new Set([
|
|
173
|
+
"auto",
|
|
174
|
+
"scroll",
|
|
175
|
+
"hidden",
|
|
176
|
+
"clip",
|
|
177
|
+
"overlay",
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
function intersectRects(
|
|
181
|
+
a: FloatingContainerRect,
|
|
182
|
+
b: FloatingContainerRect,
|
|
183
|
+
): FloatingContainerRect {
|
|
184
|
+
const left = Math.max(a.left, b.left);
|
|
185
|
+
const top = Math.max(a.top, b.top);
|
|
186
|
+
const right = Math.min(a.left + a.width, b.left + b.width);
|
|
187
|
+
const bottom = Math.min(a.top + a.height, b.top + b.height);
|
|
188
|
+
return { left, top, width: right - left, height: bottom - top };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Narrows a floating container down to the region where the editor's content
|
|
193
|
+
* is actually on screen, by intersecting it with every clipping ancestor.
|
|
194
|
+
*
|
|
195
|
+
* The composer's editor sits inside its own scroll region, so a selection can
|
|
196
|
+
* run well past what the reader can see. Anchoring to the raw container would
|
|
197
|
+
* point the surface at text hidden behind the scroll edge; this keeps the
|
|
198
|
+
* anchor inside the scrollport.
|
|
199
|
+
*
|
|
200
|
+
* @param element - Editor element whose visible region is wanted
|
|
201
|
+
* @param containerRect - Bounds of the fixed-position floating container
|
|
202
|
+
* @returns Visible region in viewport coordinates
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* const clip = getVisibleClipRect(view.dom, getFixedFloatingContainerRect(dialog));
|
|
206
|
+
*/
|
|
207
|
+
export function getVisibleClipRect(
|
|
208
|
+
element: HTMLElement,
|
|
209
|
+
containerRect: FloatingContainerRect,
|
|
210
|
+
): FloatingContainerRect {
|
|
211
|
+
let clip = containerRect;
|
|
212
|
+
let node = element.parentElement;
|
|
213
|
+
|
|
214
|
+
while (node && node !== document.body) {
|
|
215
|
+
const style = getComputedStyle(node);
|
|
216
|
+
if (
|
|
217
|
+
CLIPPING_OVERFLOW.has(style.overflowY) ||
|
|
218
|
+
CLIPPING_OVERFLOW.has(style.overflowX)
|
|
219
|
+
) {
|
|
220
|
+
const rect = node.getBoundingClientRect();
|
|
221
|
+
const next = intersectRects(clip, {
|
|
222
|
+
left: rect.left,
|
|
223
|
+
top: rect.top,
|
|
224
|
+
width: rect.width,
|
|
225
|
+
height: rect.height,
|
|
226
|
+
});
|
|
227
|
+
// A zero-size intersection means the ancestor reported no layout at all
|
|
228
|
+
// (detached nodes, test environments). Keep the wider region instead of
|
|
229
|
+
// collapsing the anchor to nothing.
|
|
230
|
+
if (next.width > 0 && next.height > 0) clip = next;
|
|
231
|
+
}
|
|
232
|
+
node = node.parentElement;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return clip;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Reduces the line boxes of a selection to the rectangle a floating surface
|
|
240
|
+
* should point at.
|
|
241
|
+
*
|
|
242
|
+
* Horizontally this is the union of everything visible, so a selection that
|
|
243
|
+
* covers whole blocks centers on the text column rather than on wherever the
|
|
244
|
+
* first and last lines happen to break. Vertically it is the *first* visible
|
|
245
|
+
* line only: a selection running past the bottom of the screen still gets an
|
|
246
|
+
* anchor the reader can see, and flipping the surface below that line lands it
|
|
247
|
+
* inside the selection instead of somewhere off-screen.
|
|
248
|
+
*
|
|
249
|
+
* @param rects - Client rects of the selection, in viewport coordinates
|
|
250
|
+
* @param clipRect - Region the anchor must stay inside
|
|
251
|
+
* @returns Anchor rectangle, or null when nothing is visible
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* const anchor = getVisibleAnchorRect(range.getClientRects(), clipRect);
|
|
255
|
+
*/
|
|
256
|
+
export function getVisibleAnchorRect(
|
|
257
|
+
rects: readonly FloatingAnchorRect[],
|
|
258
|
+
clipRect: FloatingContainerRect,
|
|
259
|
+
): FloatingAnchorRect | null {
|
|
260
|
+
const clipRight = clipRect.left + clipRect.width;
|
|
261
|
+
const clipBottom = clipRect.top + clipRect.height;
|
|
262
|
+
|
|
263
|
+
let left = Number.POSITIVE_INFINITY;
|
|
264
|
+
let right = Number.NEGATIVE_INFINITY;
|
|
265
|
+
let top = Number.POSITIVE_INFINITY;
|
|
266
|
+
let bottom = Number.POSITIVE_INFINITY;
|
|
267
|
+
let found = false;
|
|
268
|
+
|
|
269
|
+
for (const rect of rects) {
|
|
270
|
+
const rectTop = Math.max(rect.top, clipRect.top);
|
|
271
|
+
const rectBottom = Math.min(rect.bottom, clipBottom);
|
|
272
|
+
if (rectBottom <= rectTop) continue;
|
|
273
|
+
const rectLeft = Math.max(rect.left, clipRect.left);
|
|
274
|
+
const rectRight = Math.min(rect.right, clipRight);
|
|
275
|
+
if (rectRight < rectLeft) continue;
|
|
276
|
+
|
|
277
|
+
left = Math.min(left, rectLeft);
|
|
278
|
+
right = Math.max(right, rectRight);
|
|
279
|
+
top = Math.min(top, rectTop);
|
|
280
|
+
// Every kept rect ends below `top`, so the smallest bottom belongs to the
|
|
281
|
+
// shortest box starting there — the first line, even when the browser also
|
|
282
|
+
// reports a full-height rect for an enclosing block.
|
|
283
|
+
bottom = Math.min(bottom, rectBottom);
|
|
284
|
+
found = true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return found ? { left, right, top, bottom } : null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function measureRangeRects(
|
|
291
|
+
view: EditorView,
|
|
292
|
+
from: number,
|
|
293
|
+
to: number,
|
|
294
|
+
): FloatingAnchorRect[] {
|
|
295
|
+
if (typeof document === "undefined" || !document.createRange) return [];
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const start = view.domAtPos(from);
|
|
299
|
+
const end = view.domAtPos(to);
|
|
300
|
+
const range = document.createRange();
|
|
301
|
+
range.setStart(start.node, start.offset);
|
|
302
|
+
range.setEnd(end.node, end.offset);
|
|
303
|
+
const rects = range.getClientRects?.();
|
|
304
|
+
return rects ? Array.from(rects) : [];
|
|
305
|
+
} catch {
|
|
306
|
+
// Positions that do not resolve to a DOM range (node selections, widgets)
|
|
307
|
+
// fall back to the caller's endpoint coordinates.
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Resolves the rectangle a floating surface should point at for a document
|
|
314
|
+
* range.
|
|
315
|
+
*
|
|
316
|
+
* Measuring the caret at each end is only correct while the range stays on one
|
|
317
|
+
* line: as soon as it wraps, the "anchor" mixes the first line's left edge with
|
|
318
|
+
* the last line's right edge and centers on a point inside neither. This
|
|
319
|
+
* measures the range's real line boxes instead.
|
|
320
|
+
*
|
|
321
|
+
* @param view - Editor view owning the range
|
|
322
|
+
* @param from - Range start position
|
|
323
|
+
* @param to - Range end position
|
|
324
|
+
* @param clipRect - Region the anchor must stay inside
|
|
325
|
+
* @returns Anchor rectangle in viewport coordinates
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* const anchor = getRangeAnchorRect(view, from, to, clipRect);
|
|
329
|
+
*/
|
|
330
|
+
export function getRangeAnchorRect(
|
|
331
|
+
view: EditorView,
|
|
332
|
+
from: number,
|
|
333
|
+
to: number,
|
|
334
|
+
clipRect: FloatingContainerRect,
|
|
335
|
+
): FloatingAnchorRect {
|
|
336
|
+
const measured = getVisibleAnchorRect(
|
|
337
|
+
measureRangeRects(view, from, to),
|
|
338
|
+
clipRect,
|
|
339
|
+
);
|
|
340
|
+
if (measured) return measured;
|
|
341
|
+
|
|
342
|
+
const start = view.coordsAtPos(from);
|
|
343
|
+
const end = view.coordsAtPos(to);
|
|
344
|
+
const endpoints: FloatingAnchorRect = {
|
|
345
|
+
left: Math.min(start.left, end.left),
|
|
346
|
+
right: Math.max(start.right, end.right),
|
|
347
|
+
top: start.top,
|
|
348
|
+
bottom: start.bottom,
|
|
349
|
+
};
|
|
350
|
+
return getVisibleAnchorRect([endpoints], clipRect) ?? endpoints;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Horizontal offset of a floating surface's pointer arrow, measured from the
|
|
355
|
+
* surface's own left edge.
|
|
356
|
+
*
|
|
357
|
+
* The surface is clamped to the container, so a centered arrow drifts off the
|
|
358
|
+
* anchor near the edges of the screen. This keeps it on the anchor without
|
|
359
|
+
* letting it slide past the rounded corners.
|
|
360
|
+
*
|
|
361
|
+
* @param options - Anchor, container, and the surface's placed geometry
|
|
362
|
+
* @returns Offset in pixels from the surface's left edge
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* el.style.setProperty("--floating-arrow-x", `${getFloatingArrowOffset({...})}px`);
|
|
366
|
+
*/
|
|
367
|
+
export function getFloatingArrowOffset(options: {
|
|
368
|
+
anchorRect: FloatingAnchorRect;
|
|
369
|
+
containerRect: FloatingContainerRect;
|
|
370
|
+
floatingLeft: number;
|
|
371
|
+
floatingWidth: number;
|
|
372
|
+
inset?: number;
|
|
373
|
+
}): number {
|
|
374
|
+
const inset = options.inset ?? 10;
|
|
375
|
+
const anchorCenter =
|
|
376
|
+
(options.anchorRect.left + options.anchorRect.right) / 2 -
|
|
377
|
+
options.containerRect.left;
|
|
378
|
+
const min = Math.min(inset, options.floatingWidth / 2);
|
|
379
|
+
const max = Math.max(options.floatingWidth - inset, min);
|
|
380
|
+
return clamp(anchorCenter - options.floatingLeft, min, max);
|
|
381
|
+
}
|
|
@@ -204,7 +204,9 @@ export async function rehostInlineImage(
|
|
|
204
204
|
* Scans the editor's document for image srcs that match pending registry entries
|
|
205
205
|
* (`blob:` placeholders from the insert flow, or remote/`data:` placeholders
|
|
206
206
|
* from the paste-rehost flow). For each match, takes ownership from the original
|
|
207
|
-
* editor and sets up replacement/removal watchers on the new editor.
|
|
207
|
+
* editor and sets up replacement/removal watchers on the new editor. Content can
|
|
208
|
+
* pass through several editors before it is submitted, so an upload adopted once
|
|
209
|
+
* can be adopted again.
|
|
208
210
|
*
|
|
209
211
|
* Call this immediately after `setContent` with JSON that may contain pending
|
|
210
212
|
* placeholders (e.g. after fullscreen close transfers content back to compose).
|
|
@@ -223,12 +225,14 @@ export function adoptPendingInlineImageUploads(
|
|
|
223
225
|
const src = node.attrs.src as string;
|
|
224
226
|
if (typeof src !== "string") return;
|
|
225
227
|
|
|
226
|
-
const uploaded = inflightUploads.get(src);
|
|
228
|
+
const uploaded = inflightUploads.get(src) ?? adoptedUploads.get(src);
|
|
227
229
|
if (!uploaded) return;
|
|
228
230
|
|
|
229
231
|
// Take ownership — prevents original editor's finally from revoking the URL.
|
|
230
232
|
// Move to adoptedUploads so resolveInlineImageUrls can still find the promise
|
|
231
|
-
// if the user submits before the upload completes.
|
|
233
|
+
// if the user submits before the upload completes. Content can be handed on
|
|
234
|
+
// more than once (fullscreen → compose → a format switch), so an entry
|
|
235
|
+
// already sitting in adoptedUploads is adopted again rather than skipped.
|
|
232
236
|
inflightUploads.delete(src);
|
|
233
237
|
adoptedUploads.set(src, uploaded);
|
|
234
238
|
|