@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
|
@@ -7,11 +7,12 @@ vi.mock("../../upload-with-metadata.js", () => ({
|
|
|
7
7
|
import type { Editor, JSONContent } from "@tiptap/core";
|
|
8
8
|
import type { Slice } from "@tiptap/pm/model";
|
|
9
9
|
import * as ProseMirrorView from "@tiptap/pm/view";
|
|
10
|
-
import type { ComposeLabels } from "../compose-types.js";
|
|
10
|
+
import type { ComposeAttachment, ComposeLabels } from "../compose-types.js";
|
|
11
11
|
import { renderTiptapJson } from "../../../lib/tiptap-render.js";
|
|
12
12
|
import "../jant-compose-editor.js";
|
|
13
13
|
import type { JantComposeEditor } from "../jant-compose-editor.js";
|
|
14
14
|
import { uploadWithMetadata } from "../../upload-with-metadata.js";
|
|
15
|
+
import Sortable from "sortablejs";
|
|
15
16
|
|
|
16
17
|
function requireElement<T extends globalThis.Element>(
|
|
17
18
|
element: T | null,
|
|
@@ -108,6 +109,57 @@ function triggerEditorPaste(el: JantComposeEditor, files: File[]) {
|
|
|
108
109
|
return { handled, event };
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Drops files on the element itself, the way a drag onto the composer chrome
|
|
114
|
+
* (rather than into the ProseMirror body) arrives.
|
|
115
|
+
*/
|
|
116
|
+
function triggerEditorDrop(el: JantComposeEditor, files: File[]) {
|
|
117
|
+
const event = new Event("drop", { bubbles: true, cancelable: true });
|
|
118
|
+
Object.defineProperty(event, "dataTransfer", {
|
|
119
|
+
value: {
|
|
120
|
+
items: files.map((file) => ({
|
|
121
|
+
kind: "file",
|
|
122
|
+
type: file.type,
|
|
123
|
+
getAsFile: () => file,
|
|
124
|
+
})),
|
|
125
|
+
files,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
el.dispatchEvent(event);
|
|
129
|
+
return event;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Every image src currently in the editor's document, in document order. */
|
|
133
|
+
function imageSrcs(el: JantComposeEditor): string[] {
|
|
134
|
+
const srcs: string[] = [];
|
|
135
|
+
requireEditor(el).state.doc.descendants((node) => {
|
|
136
|
+
if (node.type.name === "image") srcs.push(node.attrs.src as string);
|
|
137
|
+
});
|
|
138
|
+
return srcs;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The same, read off the JSON the composer would submit. */
|
|
142
|
+
function bodyImageSrcs(json: unknown): string[] {
|
|
143
|
+
const srcs: string[] = [];
|
|
144
|
+
const walk = (node: unknown) => {
|
|
145
|
+
if (!node || typeof node !== "object") return;
|
|
146
|
+
const candidate = node as {
|
|
147
|
+
type?: string;
|
|
148
|
+
attrs?: { src?: unknown };
|
|
149
|
+
content?: unknown[];
|
|
150
|
+
};
|
|
151
|
+
if (
|
|
152
|
+
candidate.type === "image" &&
|
|
153
|
+
typeof candidate.attrs?.src === "string"
|
|
154
|
+
) {
|
|
155
|
+
srcs.push(candidate.attrs.src);
|
|
156
|
+
}
|
|
157
|
+
for (const child of candidate.content ?? []) walk(child);
|
|
158
|
+
};
|
|
159
|
+
walk(json);
|
|
160
|
+
return srcs;
|
|
161
|
+
}
|
|
162
|
+
|
|
111
163
|
function parsePastedText(el: JantComposeEditor, text: string): Slice | null {
|
|
112
164
|
const editor = requireEditor(el);
|
|
113
165
|
let slice: Slice | null = null;
|
|
@@ -152,6 +204,7 @@ const parseFromClipboard = (
|
|
|
152
204
|
|
|
153
205
|
const labels: ComposeLabels = {
|
|
154
206
|
cancel: "Cancel",
|
|
207
|
+
close: "Close",
|
|
155
208
|
note: "Note",
|
|
156
209
|
link: "Link",
|
|
157
210
|
quote: "Quote",
|
|
@@ -190,7 +243,11 @@ const labels: ComposeLabels = {
|
|
|
190
243
|
altHint: "Alt text improves accessibility",
|
|
191
244
|
addMore: "Add",
|
|
192
245
|
removeAttachment: "Remove attachment",
|
|
246
|
+
moveAttachmentEarlier: "Move attachment earlier",
|
|
247
|
+
moveAttachmentLater: "Move attachment later",
|
|
193
248
|
uploading: "Uploading...",
|
|
249
|
+
publishing: "Publishing...",
|
|
250
|
+
saving: "Saving...",
|
|
194
251
|
loadingPost: "Loading post...",
|
|
195
252
|
loadPostFailed: "Couldn't load this post. Try again.",
|
|
196
253
|
published: "Published!",
|
|
@@ -201,7 +258,7 @@ const labels: ComposeLabels = {
|
|
|
201
258
|
confirmCloseTitle: "Save to drafts?",
|
|
202
259
|
confirmCloseSubtitle: "Save to drafts to edit and post at a later time.",
|
|
203
260
|
confirmCloseSave: "Save",
|
|
204
|
-
confirmCloseCancel: "
|
|
261
|
+
confirmCloseCancel: "Keep editing",
|
|
205
262
|
confirmCloseDiscard: "Don't save",
|
|
206
263
|
confirmAttachedTitle: "Save text attachment?",
|
|
207
264
|
confirmAttachedSubtitle:
|
|
@@ -220,14 +277,32 @@ const labels: ComposeLabels = {
|
|
|
220
277
|
deleteDraft: "Delete Draft",
|
|
221
278
|
draftDeleted: "Draft deleted.",
|
|
222
279
|
publishFailedDraft: "Couldn't publish. Saved as draft.",
|
|
280
|
+
publishFailed: "Couldn't publish. Try again.",
|
|
281
|
+
publishFailedOffline: "Couldn't reach the server. Try again.",
|
|
223
282
|
uploadFailedDraft: "Some uploads failed. Saved as draft.",
|
|
224
283
|
addCollection: "Add Collection",
|
|
225
284
|
collectionCountLabel: "%name% + %count% more",
|
|
226
285
|
draftRestored: "Draft restored.",
|
|
286
|
+
draftStoreFailed: "This browser can't keep a local copy. Save as a draft.",
|
|
227
287
|
reply: "Reply",
|
|
228
288
|
publishHideFromLatest: "Hide from Latest",
|
|
229
289
|
publishPrivate: "Post as Private",
|
|
230
290
|
publishSettings: "Publish settings",
|
|
291
|
+
languageLabel: "Language",
|
|
292
|
+
languageAuto: "Detect",
|
|
293
|
+
languageAutoHint: "Read from what you write",
|
|
294
|
+
languageAutoDetected: "Read from what you write — looks like {language}",
|
|
295
|
+
languageAutoPending: "Not enough text to tell yet — publishes in {language}",
|
|
296
|
+
languageTriggerLabel: "Language: {language}",
|
|
297
|
+
translationOf: "Translation of “{title}”",
|
|
298
|
+
translationContext: "Translating",
|
|
299
|
+
translationContextInLanguage: "Translating into {language}",
|
|
300
|
+
translationContextOpen: "Open the original in a new tab",
|
|
301
|
+
translationContextOriginal: "The original",
|
|
302
|
+
translationContextHide: "Hide",
|
|
303
|
+
translationContextHideLong: "Hide the original",
|
|
304
|
+
translationContextShow: "Show",
|
|
305
|
+
translationContextShowLong: "Show the original",
|
|
231
306
|
publishVisibilityLabel: "Visibility",
|
|
232
307
|
publishVisibilityPublic: "Public",
|
|
233
308
|
publishVisibilityPublicHint: "Appears in Latest.",
|
|
@@ -265,7 +340,6 @@ const labels: ComposeLabels = {
|
|
|
265
340
|
threadLimitReached: "Threads can include up to 20 posts.",
|
|
266
341
|
showMore: "Show more",
|
|
267
342
|
showLess: "Show less",
|
|
268
|
-
closeCompose: "Close compose",
|
|
269
343
|
editing: "Editing",
|
|
270
344
|
composeDialogLabel: "Compose",
|
|
271
345
|
slashHint: "Type / for commands",
|
|
@@ -295,13 +369,6 @@ const labels: ComposeLabels = {
|
|
|
295
369
|
quickHint: "More options are available after you create it.",
|
|
296
370
|
quickSubmitLabel: "Done",
|
|
297
371
|
createdLabel: "Collection created.",
|
|
298
|
-
descriptionLabel: "Description (optional)",
|
|
299
|
-
descriptionPlaceholder: "What's this collection about?",
|
|
300
|
-
sortOrderLabel: "Sort Order",
|
|
301
|
-
sortNewest: "Newest first",
|
|
302
|
-
sortOldest: "Oldest first",
|
|
303
|
-
sortRatingDesc: "Highest rated",
|
|
304
|
-
submitLabel: "Save",
|
|
305
372
|
cancelLabel: "Cancel",
|
|
306
373
|
},
|
|
307
374
|
};
|
|
@@ -321,6 +388,58 @@ async function createElement(
|
|
|
321
388
|
return el;
|
|
322
389
|
}
|
|
323
390
|
|
|
391
|
+
function mediaAttachment(clientId: string, mediaId: string): ComposeAttachment {
|
|
392
|
+
const blob = new Blob(["fake"], { type: "image/png" });
|
|
393
|
+
return {
|
|
394
|
+
clientId,
|
|
395
|
+
file: new File([blob], `${clientId}.png`, { type: "image/png" }),
|
|
396
|
+
previewUrl: URL.createObjectURL(blob),
|
|
397
|
+
status: "done",
|
|
398
|
+
progress: null,
|
|
399
|
+
mediaId,
|
|
400
|
+
alt: "",
|
|
401
|
+
error: null,
|
|
402
|
+
posterUrl: null,
|
|
403
|
+
remoteUrl: null,
|
|
404
|
+
summary: null,
|
|
405
|
+
chars: null,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Forces the primary-pointer media query, and returns a restore function.
|
|
411
|
+
* Every call site restores in a `finally` so one test cannot leak into another.
|
|
412
|
+
*/
|
|
413
|
+
function stubPrimaryPointer(kind: "coarse" | "fine"): () => void {
|
|
414
|
+
const original = window.matchMedia;
|
|
415
|
+
// Overrides `matches` on a real MediaQueryList, so the component still gets
|
|
416
|
+
// a live object to subscribe to.
|
|
417
|
+
window.matchMedia = (query: string) => {
|
|
418
|
+
const list = original.call(window, query);
|
|
419
|
+
Object.defineProperty(list, "matches", {
|
|
420
|
+
configurable: true,
|
|
421
|
+
value: query.includes(`pointer: ${kind}`),
|
|
422
|
+
});
|
|
423
|
+
return list;
|
|
424
|
+
};
|
|
425
|
+
return () => {
|
|
426
|
+
window.matchMedia = original;
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function moveButton(
|
|
431
|
+
el: JantComposeEditor,
|
|
432
|
+
clientId: string,
|
|
433
|
+
direction: "earlier" | "later",
|
|
434
|
+
): HTMLButtonElement {
|
|
435
|
+
return requireElement(
|
|
436
|
+
el.querySelector<HTMLButtonElement>(
|
|
437
|
+
`[data-attachment-id="${clientId}"] [data-attachment-move="${direction}"]`,
|
|
438
|
+
),
|
|
439
|
+
`expected ${direction} move button for ${clientId}`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
324
443
|
function tiptapDoc(...content: JSONContent[]): JSONContent {
|
|
325
444
|
return { type: "doc", content };
|
|
326
445
|
}
|
|
@@ -340,6 +459,22 @@ function tiptapParagraph(text: string): JSONContent {
|
|
|
340
459
|
};
|
|
341
460
|
}
|
|
342
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Replays a pointer gesture. The browser retargets a click to the nearest
|
|
464
|
+
* common ancestor of the press and the release, so `clickTarget` is separate
|
|
465
|
+
* from the two — a drag that starts on the text and ends over a layout wrapper
|
|
466
|
+
* reports the wrapper.
|
|
467
|
+
*/
|
|
468
|
+
function pressAndClick(
|
|
469
|
+
pressTarget: HTMLElement,
|
|
470
|
+
releaseTarget: HTMLElement,
|
|
471
|
+
clickTarget: HTMLElement = releaseTarget,
|
|
472
|
+
) {
|
|
473
|
+
pressTarget.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
|
474
|
+
releaseTarget.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
|
|
475
|
+
clickTarget.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
476
|
+
}
|
|
477
|
+
|
|
343
478
|
async function toggleEmojiPicker(el: JantComposeEditor) {
|
|
344
479
|
(
|
|
345
480
|
el as unknown as {
|
|
@@ -1262,37 +1397,9 @@ describe("JantComposeEditor", () => {
|
|
|
1262
1397
|
|
|
1263
1398
|
it("moves attachments later with keyboard controls", async () => {
|
|
1264
1399
|
const el = await createElement("note");
|
|
1265
|
-
const blob = new Blob(["fake"], { type: "image/png" });
|
|
1266
|
-
const file = new File([blob], "test.png", { type: "image/png" });
|
|
1267
1400
|
el._attachments = [
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
file,
|
|
1271
|
-
previewUrl: URL.createObjectURL(blob),
|
|
1272
|
-
status: "done",
|
|
1273
|
-
progress: null,
|
|
1274
|
-
mediaId: "m1",
|
|
1275
|
-
alt: "",
|
|
1276
|
-
error: null,
|
|
1277
|
-
posterUrl: null,
|
|
1278
|
-
remoteUrl: null,
|
|
1279
|
-
summary: null,
|
|
1280
|
-
chars: null,
|
|
1281
|
-
},
|
|
1282
|
-
{
|
|
1283
|
-
clientId: "a2",
|
|
1284
|
-
file,
|
|
1285
|
-
previewUrl: URL.createObjectURL(blob),
|
|
1286
|
-
status: "done",
|
|
1287
|
-
progress: null,
|
|
1288
|
-
mediaId: "m2",
|
|
1289
|
-
alt: "",
|
|
1290
|
-
error: null,
|
|
1291
|
-
posterUrl: null,
|
|
1292
|
-
remoteUrl: null,
|
|
1293
|
-
summary: null,
|
|
1294
|
-
chars: null,
|
|
1295
|
-
},
|
|
1401
|
+
mediaAttachment("a1", "m1"),
|
|
1402
|
+
mediaAttachment("a2", "m2"),
|
|
1296
1403
|
];
|
|
1297
1404
|
el._attachmentOrder = ["a1", "a2"];
|
|
1298
1405
|
await el.updateComplete;
|
|
@@ -1314,6 +1421,127 @@ describe("JantComposeEditor", () => {
|
|
|
1314
1421
|
expect(el._attachmentOrder).toEqual(["a2", "a1"]);
|
|
1315
1422
|
});
|
|
1316
1423
|
|
|
1424
|
+
it("reorders attachments with the move buttons", async () => {
|
|
1425
|
+
const el = await createElement("note");
|
|
1426
|
+
el._attachments = [
|
|
1427
|
+
mediaAttachment("a1", "m1"),
|
|
1428
|
+
mediaAttachment("a2", "m2"),
|
|
1429
|
+
mediaAttachment("a3", "m3"),
|
|
1430
|
+
];
|
|
1431
|
+
el._attachmentOrder = ["a1", "a2", "a3"];
|
|
1432
|
+
await el.updateComplete;
|
|
1433
|
+
|
|
1434
|
+
moveButton(el, "a1", "later").click();
|
|
1435
|
+
await el.updateComplete;
|
|
1436
|
+
expect(el._attachmentOrder).toEqual(["a2", "a1", "a3"]);
|
|
1437
|
+
|
|
1438
|
+
moveButton(el, "a1", "earlier").click();
|
|
1439
|
+
await el.updateComplete;
|
|
1440
|
+
expect(el._attachmentOrder).toEqual(["a1", "a2", "a3"]);
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
it("disables the move button at each end of the strip", async () => {
|
|
1444
|
+
const el = await createElement("note");
|
|
1445
|
+
el._attachments = [
|
|
1446
|
+
mediaAttachment("a1", "m1"),
|
|
1447
|
+
mediaAttachment("a2", "m2"),
|
|
1448
|
+
];
|
|
1449
|
+
el._attachmentOrder = ["a1", "a2"];
|
|
1450
|
+
await el.updateComplete;
|
|
1451
|
+
|
|
1452
|
+
expect(moveButton(el, "a1", "earlier").disabled).toBe(true);
|
|
1453
|
+
expect(moveButton(el, "a1", "later").disabled).toBe(false);
|
|
1454
|
+
expect(moveButton(el, "a2", "earlier").disabled).toBe(false);
|
|
1455
|
+
expect(moveButton(el, "a2", "later").disabled).toBe(true);
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
it("keeps focus on the attachment that moved", async () => {
|
|
1459
|
+
const el = await createElement("note");
|
|
1460
|
+
el._attachments = [
|
|
1461
|
+
mediaAttachment("a1", "m1"),
|
|
1462
|
+
mediaAttachment("a2", "m2"),
|
|
1463
|
+
mediaAttachment("a3", "m3"),
|
|
1464
|
+
];
|
|
1465
|
+
el._attachmentOrder = ["a1", "a2", "a3"];
|
|
1466
|
+
await el.updateComplete;
|
|
1467
|
+
|
|
1468
|
+
const later = moveButton(el, "a1", "later");
|
|
1469
|
+
later.focus();
|
|
1470
|
+
later.click();
|
|
1471
|
+
await el.updateComplete;
|
|
1472
|
+
|
|
1473
|
+
expect(document.activeElement).toBe(moveButton(el, "a1", "later"));
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
it("falls back to the opposite button when a move reaches the end", async () => {
|
|
1477
|
+
const el = await createElement("note");
|
|
1478
|
+
el._attachments = [
|
|
1479
|
+
mediaAttachment("a1", "m1"),
|
|
1480
|
+
mediaAttachment("a2", "m2"),
|
|
1481
|
+
];
|
|
1482
|
+
el._attachmentOrder = ["a1", "a2"];
|
|
1483
|
+
await el.updateComplete;
|
|
1484
|
+
|
|
1485
|
+
const later = moveButton(el, "a1", "later");
|
|
1486
|
+
later.focus();
|
|
1487
|
+
later.click();
|
|
1488
|
+
await el.updateComplete;
|
|
1489
|
+
|
|
1490
|
+
expect(el._attachmentOrder).toEqual(["a2", "a1"]);
|
|
1491
|
+
expect(moveButton(el, "a1", "later").disabled).toBe(true);
|
|
1492
|
+
expect(document.activeElement).toBe(moveButton(el, "a1", "earlier"));
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1495
|
+
it("omits move buttons when there is nothing to reorder", async () => {
|
|
1496
|
+
const el = await createElement("note");
|
|
1497
|
+
el._attachments = [mediaAttachment("a1", "m1")];
|
|
1498
|
+
el._attachmentOrder = ["a1"];
|
|
1499
|
+
await el.updateComplete;
|
|
1500
|
+
|
|
1501
|
+
expect(el.querySelector("[data-attachment-move]")).toBeNull();
|
|
1502
|
+
expect(el.querySelector(".compose-attachment-alt")).not.toBeNull();
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
it("leaves drag reordering off on a touch device", async () => {
|
|
1506
|
+
const create = vi.spyOn(Sortable, "create");
|
|
1507
|
+
const restorePointer = stubPrimaryPointer("coarse");
|
|
1508
|
+
try {
|
|
1509
|
+
const el = await createElement("note");
|
|
1510
|
+
el._attachments = [
|
|
1511
|
+
mediaAttachment("a1", "m1"),
|
|
1512
|
+
mediaAttachment("a2", "m2"),
|
|
1513
|
+
];
|
|
1514
|
+
el._attachmentOrder = ["a1", "a2"];
|
|
1515
|
+
await el.updateComplete;
|
|
1516
|
+
|
|
1517
|
+
expect(create).not.toHaveBeenCalled();
|
|
1518
|
+
// The buttons are the whole reordering path there, so they must stay.
|
|
1519
|
+
expect(moveButton(el, "a1", "later").disabled).toBe(false);
|
|
1520
|
+
} finally {
|
|
1521
|
+
restorePointer();
|
|
1522
|
+
create.mockRestore();
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
it("keeps drag reordering on a pointer device", async () => {
|
|
1527
|
+
const create = vi.spyOn(Sortable, "create");
|
|
1528
|
+
const restorePointer = stubPrimaryPointer("fine");
|
|
1529
|
+
try {
|
|
1530
|
+
const el = await createElement("note");
|
|
1531
|
+
el._attachments = [
|
|
1532
|
+
mediaAttachment("a1", "m1"),
|
|
1533
|
+
mediaAttachment("a2", "m2"),
|
|
1534
|
+
];
|
|
1535
|
+
el._attachmentOrder = ["a1", "a2"];
|
|
1536
|
+
await el.updateComplete;
|
|
1537
|
+
|
|
1538
|
+
expect(create).toHaveBeenCalledTimes(1);
|
|
1539
|
+
} finally {
|
|
1540
|
+
restorePointer();
|
|
1541
|
+
create.mockRestore();
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1317
1545
|
it("preserves mixed attachment order when populate provides one", async () => {
|
|
1318
1546
|
const el = await createElement("note");
|
|
1319
1547
|
|
|
@@ -1427,4 +1655,280 @@ describe("JantComposeEditor", () => {
|
|
|
1427
1655
|
"clipboard.png",
|
|
1428
1656
|
]);
|
|
1429
1657
|
});
|
|
1658
|
+
|
|
1659
|
+
it("pastes images as attachments in a link, title or no title", async () => {
|
|
1660
|
+
const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
|
|
1661
|
+
const el = await createElement("link");
|
|
1662
|
+
// A link's title is required to publish, so it is filled in every link
|
|
1663
|
+
// worth pasting into — it can't be the signal that picks inline.
|
|
1664
|
+
el._title = "An article worth linking";
|
|
1665
|
+
await el.updateComplete;
|
|
1666
|
+
|
|
1667
|
+
const image = new File(["image"], "clipboard.png", { type: "image/png" });
|
|
1668
|
+
const { handled } = triggerEditorPaste(el, [image]);
|
|
1669
|
+
await el.updateComplete;
|
|
1670
|
+
|
|
1671
|
+
expect(handled).toBe(true);
|
|
1672
|
+
expect(uploadWithMetadataMock).not.toHaveBeenCalled();
|
|
1673
|
+
expect(el._attachments.map((attachment) => attachment.file.name)).toEqual([
|
|
1674
|
+
"clipboard.png",
|
|
1675
|
+
]);
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
it("pastes images as attachments in a quote carrying a title from a format switch", async () => {
|
|
1679
|
+
const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
|
|
1680
|
+
const el = await createElement("quote");
|
|
1681
|
+
// A quote has no title field, but switching over from a note leaves one
|
|
1682
|
+
// behind. Nothing the author can see may decide where the image lands.
|
|
1683
|
+
el._title = "Essay";
|
|
1684
|
+
await el.updateComplete;
|
|
1685
|
+
|
|
1686
|
+
const image = new File(["image"], "clipboard.png", { type: "image/png" });
|
|
1687
|
+
const { handled } = triggerEditorPaste(el, [image]);
|
|
1688
|
+
await el.updateComplete;
|
|
1689
|
+
|
|
1690
|
+
expect(handled).toBe(true);
|
|
1691
|
+
expect(uploadWithMetadataMock).not.toHaveBeenCalled();
|
|
1692
|
+
expect(el._attachments.map((attachment) => attachment.file.name)).toEqual([
|
|
1693
|
+
"clipboard.png",
|
|
1694
|
+
]);
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
it("carries an in-flight inline upload across a format switch", async () => {
|
|
1698
|
+
const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
|
|
1699
|
+
let finishUpload: (value: { url: string; id: string }) => void = () => {};
|
|
1700
|
+
uploadWithMetadataMock.mockReturnValue(
|
|
1701
|
+
new Promise((resolve) => {
|
|
1702
|
+
finishUpload = resolve;
|
|
1703
|
+
}),
|
|
1704
|
+
);
|
|
1705
|
+
|
|
1706
|
+
const el = await createElement("note");
|
|
1707
|
+
el._title = "Essay";
|
|
1708
|
+
await el.updateComplete;
|
|
1709
|
+
|
|
1710
|
+
const image = new File(["image"], "clipboard.png", { type: "image/png" });
|
|
1711
|
+
triggerEditorPaste(el, [image]);
|
|
1712
|
+
await el.updateComplete;
|
|
1713
|
+
expect(imageSrcs(el).every((src) => src.startsWith("blob:"))).toBe(true);
|
|
1714
|
+
expect(imageSrcs(el)).toHaveLength(1);
|
|
1715
|
+
|
|
1716
|
+
// The body survives the switch, so the upload watching it has to survive
|
|
1717
|
+
// too — otherwise the blob: placeholder is what gets published.
|
|
1718
|
+
el.format = "quote";
|
|
1719
|
+
await el.updateComplete;
|
|
1720
|
+
await el.updateComplete;
|
|
1721
|
+
|
|
1722
|
+
finishUpload({
|
|
1723
|
+
url: "https://example.test/clipboard.webp",
|
|
1724
|
+
id: "med_test",
|
|
1725
|
+
});
|
|
1726
|
+
await vi.waitFor(() => {
|
|
1727
|
+
expect(imageSrcs(el)).toEqual(["https://example.test/clipboard.webp"]);
|
|
1728
|
+
});
|
|
1729
|
+
expect(bodyImageSrcs(el._bodyJson)).toEqual([
|
|
1730
|
+
"https://example.test/clipboard.webp",
|
|
1731
|
+
]);
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
it("drops images into a link as attachments, same as pasting them", async () => {
|
|
1735
|
+
const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
|
|
1736
|
+
const el = await createElement("link");
|
|
1737
|
+
el._title = "An article worth linking";
|
|
1738
|
+
await el.updateComplete;
|
|
1739
|
+
|
|
1740
|
+
const image = new File(["image"], "dropped.png", { type: "image/png" });
|
|
1741
|
+
triggerEditorDrop(el, [image]);
|
|
1742
|
+
await el.updateComplete;
|
|
1743
|
+
|
|
1744
|
+
expect(uploadWithMetadataMock).not.toHaveBeenCalled();
|
|
1745
|
+
expect(el._attachments.map((attachment) => attachment.file.name)).toEqual([
|
|
1746
|
+
"dropped.png",
|
|
1747
|
+
]);
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
it("focuses the body end when blank space around the editor is clicked", async () => {
|
|
1751
|
+
const el = await createElement("note");
|
|
1752
|
+
const editor = requireEditor(el);
|
|
1753
|
+
editor.commands.setContent(
|
|
1754
|
+
tiptapDoc(tiptapParagraph("One two three"), tiptapParagraph("Tail")),
|
|
1755
|
+
);
|
|
1756
|
+
editor.commands.setTextSelection(1);
|
|
1757
|
+
|
|
1758
|
+
const blank = requireElement(
|
|
1759
|
+
el.querySelector<HTMLElement>(".compose-tiptap-wrap"),
|
|
1760
|
+
"expected compose tiptap wrapper",
|
|
1761
|
+
);
|
|
1762
|
+
pressAndClick(blank, blank);
|
|
1763
|
+
|
|
1764
|
+
expect(editor.state.selection.empty).toBe(true);
|
|
1765
|
+
expect(editor.state.selection.from).toBe(editor.state.doc.content.size - 1);
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
it("keeps a drag-selection that lifts off the text onto blank space", async () => {
|
|
1769
|
+
const el = await createElement("note");
|
|
1770
|
+
const editor = requireEditor(el);
|
|
1771
|
+
editor.commands.setContent(
|
|
1772
|
+
tiptapDoc(
|
|
1773
|
+
{
|
|
1774
|
+
type: "blockquote",
|
|
1775
|
+
content: [tiptapParagraph("One two three four five six")],
|
|
1776
|
+
},
|
|
1777
|
+
tiptapParagraph("Tail after the quote"),
|
|
1778
|
+
),
|
|
1779
|
+
);
|
|
1780
|
+
// Right-to-left drag across the quote: anchor after "six", head before "One".
|
|
1781
|
+
editor.commands.setTextSelection({ from: 2, to: 29 });
|
|
1782
|
+
|
|
1783
|
+
const quote = requireElement(
|
|
1784
|
+
el.querySelector<HTMLElement>(".compose-tiptap-body blockquote p"),
|
|
1785
|
+
"expected blockquote paragraph",
|
|
1786
|
+
);
|
|
1787
|
+
const blank = requireElement(
|
|
1788
|
+
el.querySelector<HTMLElement>(".compose-tiptap-wrap"),
|
|
1789
|
+
"expected compose tiptap wrapper",
|
|
1790
|
+
);
|
|
1791
|
+
// The pointer went down on the text and came up over the wrapper, so the
|
|
1792
|
+
// browser retargets the click to their common ancestor.
|
|
1793
|
+
pressAndClick(quote, blank, blank);
|
|
1794
|
+
|
|
1795
|
+
expect(editor.state.selection.from).toBe(2);
|
|
1796
|
+
expect(editor.state.selection.to).toBe(29);
|
|
1797
|
+
});
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
describe("JantComposeEditor.takeOver", () => {
|
|
1801
|
+
beforeEach(() => {
|
|
1802
|
+
document.body.innerHTML = "";
|
|
1803
|
+
vi.clearAllMocks();
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
/** An editor the dialog is about to render in place of another one. */
|
|
1807
|
+
function unrenderedEditor(format: "note" | "link" | "quote") {
|
|
1808
|
+
const el = document.createElement(
|
|
1809
|
+
"jant-compose-editor",
|
|
1810
|
+
) as JantComposeEditor;
|
|
1811
|
+
el.format = format;
|
|
1812
|
+
el.labels = labels;
|
|
1813
|
+
document.body.appendChild(el);
|
|
1814
|
+
return el;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
it("hands every post field to the editor that replaces it", async () => {
|
|
1818
|
+
// One sample per field the editor holds for its post. The first assertion
|
|
1819
|
+
// keeps this table complete: a field the editor gains without a way across
|
|
1820
|
+
// is how a quote's source link used to vanish on "Add to thread".
|
|
1821
|
+
const samples: Record<string, unknown> = {
|
|
1822
|
+
_title: "Left behind by a format switch",
|
|
1823
|
+
_showTitle: false,
|
|
1824
|
+
_bodyJson: tiptapDoc(tiptapParagraph("My thoughts on it")),
|
|
1825
|
+
_url: "https://example.com/source",
|
|
1826
|
+
_quoteText: "A borrowed sentence.",
|
|
1827
|
+
_quoteAuthor: "Someone Else",
|
|
1828
|
+
_rating: 4,
|
|
1829
|
+
_showRating: true,
|
|
1830
|
+
_attachedTexts: [
|
|
1831
|
+
{
|
|
1832
|
+
clientId: "text-1",
|
|
1833
|
+
bodyJson: tiptapDoc(tiptapParagraph("Attached")),
|
|
1834
|
+
bodyHtml: "",
|
|
1835
|
+
summary: "Attached",
|
|
1836
|
+
},
|
|
1837
|
+
],
|
|
1838
|
+
_attachments: [mediaAttachment("media-1", "med_1")],
|
|
1839
|
+
_attachmentOrder: ["text-1", "media-1"],
|
|
1840
|
+
};
|
|
1841
|
+
const contentProps = (
|
|
1842
|
+
customElements.get("jant-compose-editor") as unknown as {
|
|
1843
|
+
_CONTENT_PROPS: Set<string>;
|
|
1844
|
+
}
|
|
1845
|
+
)._CONTENT_PROPS;
|
|
1846
|
+
expect(Object.keys(samples).sort()).toEqual(
|
|
1847
|
+
[...contentProps, "_attachments"].sort(),
|
|
1848
|
+
);
|
|
1849
|
+
|
|
1850
|
+
const previous = await createElement("quote");
|
|
1851
|
+
for (const [key, value] of Object.entries(samples)) {
|
|
1852
|
+
if (key === "_bodyJson") continue;
|
|
1853
|
+
(previous as unknown as Record<string, unknown>)[key] = value;
|
|
1854
|
+
}
|
|
1855
|
+
requireEditor(previous).commands.setContent(
|
|
1856
|
+
samples._bodyJson as JSONContent,
|
|
1857
|
+
);
|
|
1858
|
+
await previous.updateComplete;
|
|
1859
|
+
// The dialog swaps elements before handing over, so the old one has
|
|
1860
|
+
// already left the DOM — and taken its TipTap instance with it.
|
|
1861
|
+
previous.remove();
|
|
1862
|
+
|
|
1863
|
+
const next = await createElement("quote");
|
|
1864
|
+
next.takeOver(previous);
|
|
1865
|
+
await next.updateComplete;
|
|
1866
|
+
|
|
1867
|
+
for (const [key, value] of Object.entries(samples)) {
|
|
1868
|
+
if (key === "_bodyJson") continue;
|
|
1869
|
+
expect((next as unknown as Record<string, unknown>)[key], key).toEqual(
|
|
1870
|
+
value,
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
expect(next.getNormalizedBodyJson()).toEqual(
|
|
1874
|
+
previous.getNormalizedBodyJson(),
|
|
1875
|
+
);
|
|
1876
|
+
expect(requireEditor(next).getText()).toBe("My thoughts on it");
|
|
1877
|
+
expect(next.getData().url).toBe("https://example.com/source");
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
it("keeps a handed-over title toggle through its first render", async () => {
|
|
1881
|
+
const previous = await createElement("note", { titleByDefault: true });
|
|
1882
|
+
previous._showTitle = false;
|
|
1883
|
+
previous.remove();
|
|
1884
|
+
|
|
1885
|
+
const next = unrenderedEditor("note");
|
|
1886
|
+
next.titleByDefault = true;
|
|
1887
|
+
next.takeOver(previous);
|
|
1888
|
+
await next.updateComplete;
|
|
1889
|
+
|
|
1890
|
+
expect(next._showTitle).toBe(false);
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
it.each([
|
|
1894
|
+
["after", true],
|
|
1895
|
+
["before", false],
|
|
1896
|
+
])(
|
|
1897
|
+
"carries an in-flight inline upload to an editor taking over %s its first render",
|
|
1898
|
+
async (_when, renderFirst) => {
|
|
1899
|
+
const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
|
|
1900
|
+
let finishUpload: (value: { url: string; id: string }) => void = () => {};
|
|
1901
|
+
uploadWithMetadataMock.mockReturnValue(
|
|
1902
|
+
new Promise((resolve) => {
|
|
1903
|
+
finishUpload = resolve;
|
|
1904
|
+
}),
|
|
1905
|
+
);
|
|
1906
|
+
|
|
1907
|
+
const previous = await createElement("note");
|
|
1908
|
+
previous._title = "Essay";
|
|
1909
|
+
await previous.updateComplete;
|
|
1910
|
+
const image = new File(["image"], "clipboard.png", { type: "image/png" });
|
|
1911
|
+
triggerEditorPaste(previous, [image]);
|
|
1912
|
+
await previous.updateComplete;
|
|
1913
|
+
expect(imageSrcs(previous)).toHaveLength(1);
|
|
1914
|
+
expect(imageSrcs(previous)[0]).toMatch(/^blob:/);
|
|
1915
|
+
previous.remove();
|
|
1916
|
+
|
|
1917
|
+
const next = unrenderedEditor("note");
|
|
1918
|
+
if (renderFirst) await next.updateComplete;
|
|
1919
|
+
next.takeOver(previous);
|
|
1920
|
+
await next.updateComplete;
|
|
1921
|
+
expect(imageSrcs(next)[0]).toMatch(/^blob:/);
|
|
1922
|
+
|
|
1923
|
+
finishUpload({ url: "https://example.test/clipboard.webp", id: "med_1" });
|
|
1924
|
+
await vi.waitFor(() => {
|
|
1925
|
+
expect(imageSrcs(next)).toEqual([
|
|
1926
|
+
"https://example.test/clipboard.webp",
|
|
1927
|
+
]);
|
|
1928
|
+
});
|
|
1929
|
+
expect(bodyImageSrcs(next._bodyJson)).toEqual([
|
|
1930
|
+
"https://example.test/clipboard.webp",
|
|
1931
|
+
]);
|
|
1932
|
+
},
|
|
1933
|
+
);
|
|
1430
1934
|
});
|