@jant/core 0.6.15 → 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 +67 -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-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
- 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-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
- 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__/mise-config.test.ts +22 -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 +524 -2
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
- 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 +88 -1
- 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__/compose-format-convert.test.ts +0 -19
- 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 +79 -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 +3283 -389
- package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
- 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-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +121 -3
- 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 +106 -49
- package/src/client/components/jant-compose-dialog.ts +2757 -1194
- package/src/client/components/jant-compose-editor.ts +866 -397
- package/src/client/components/jant-compose-fullscreen.ts +53 -47
- 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 +1142 -268
- 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 +336 -242
- package/src/client/compose-launch.ts +89 -2
- package/src/client/compose-shortcuts.ts +9 -7
- 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/post-refresh.ts +135 -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/__tests__/link-input-rules.test.ts +121 -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-input-rules.ts +4 -4
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +143 -38
- 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__/backfill-thread-activity.test.ts +234 -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/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0031_many_ego.sql +3 -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/0031_snapshot.json +2562 -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 +28 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -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/0029_snapshot.json +3289 -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 +28 -0
- package/src/db/pg/schema.ts +213 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +242 -27
- package/src/db/thread-activity.ts +101 -0
- 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 +568 -168
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +562 -162
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +562 -162
- 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 +1691 -12
- 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__/markdown.test.ts +16 -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 +88 -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__/translit.test.ts +87 -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 +93 -34
- 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 +1021 -66
- 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 +21 -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/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- 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-display.ts +42 -9
- 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 +81 -10
- package/src/lib/tiptap-render.ts +5 -4
- package/src/lib/translit.ts +288 -0
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +188 -24
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +79 -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__/palette.test.ts +44 -0
- 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 +172 -9
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +30 -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 +11 -2
- 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/__tests__/sitemap.test.ts +68 -1
- 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 +657 -1
- 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__/featured.test.ts +6 -2
- 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__/preview.test.ts +3 -1
- 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 +507 -357
- 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 +183 -19
- 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 +139 -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-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +323 -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 +330 -47
- 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 +281 -24
- 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 +225 -7
- package/src/services/post.ts +1318 -252
- package/src/services/search.ts +48 -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 -1585
- package/src/styles/site-media.css +20 -6
- package/src/styles/tokens.css +144 -13
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1224 -7276
- 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 +92 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +168 -30
- package/src/types/views.ts +61 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +215 -21
- 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/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +103 -17
- 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 +401 -301
- 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 +122 -3
- 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/DraftPreviewBar.tsx +25 -9
- 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 +55 -23
- 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/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +8 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-BxCOR3Uc.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-pLre2DM_.js +0 -1
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
- package/dist/client/_assets/client-DxY5BFe8.js +0 -296
- package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
- package/dist/env-OHRKGcMj.js +0 -281
- package/dist/github-api-BgSiE71w.js +0 -176
- package/dist/github-app-BbklkFmU.js +0 -275
- package/dist/github-sync-Cq1pzzOI.js +0 -431
- package/dist/github-sync-ppWXN3jb.js +0 -4
- package/dist/url-BMYO-Zlt.js +0 -395
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
const lazyEntries = vi.hoisted(() => ({
|
|
6
|
+
ensureCompose: vi.fn(async () => {}),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("../lazy-entries.js", () => lazyEntries);
|
|
10
|
+
|
|
4
11
|
import {
|
|
12
|
+
ensureComposeDialog,
|
|
5
13
|
getReplyRefreshTarget,
|
|
6
14
|
getReplyTargetArticle,
|
|
15
|
+
readComposeDialogLabels,
|
|
7
16
|
} from "../compose-launch.js";
|
|
8
17
|
|
|
9
18
|
function getArticle(): HTMLElement {
|
|
@@ -167,3 +176,55 @@ describe("getReplyTargetArticle", () => {
|
|
|
167
176
|
);
|
|
168
177
|
});
|
|
169
178
|
});
|
|
179
|
+
|
|
180
|
+
describe("ensureComposeDialog", () => {
|
|
181
|
+
afterEach(() => {
|
|
182
|
+
document.body.innerHTML = "";
|
|
183
|
+
lazyEntries.ensureCompose.mockClear();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("loads the composer's bundle before handing the element back", async () => {
|
|
187
|
+
document.body.innerHTML = `<jant-compose-dialog></jant-compose-dialog>`;
|
|
188
|
+
|
|
189
|
+
const dialog = await ensureComposeDialog();
|
|
190
|
+
|
|
191
|
+
expect(lazyEntries.ensureCompose).toHaveBeenCalledTimes(1);
|
|
192
|
+
expect(dialog).toBe(document.querySelector("jant-compose-dialog"));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("loads nothing on a page without a composer", async () => {
|
|
196
|
+
expect(await ensureComposeDialog()).toBeNull();
|
|
197
|
+
expect(lazyEntries.ensureCompose).not.toHaveBeenCalled();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("readComposeDialogLabels", () => {
|
|
202
|
+
afterEach(() => {
|
|
203
|
+
document.body.innerHTML = "";
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("reads the server-rendered attribute before the element upgrades", () => {
|
|
207
|
+
document.body.innerHTML = `<jant-compose-dialog labels='{"addCollection":"Add Collection"}'></jant-compose-dialog>`;
|
|
208
|
+
|
|
209
|
+
expect(readComposeDialogLabels()?.addCollection).toBe("Add Collection");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("prefers the upgraded element's property", () => {
|
|
213
|
+
document.body.innerHTML = `<jant-compose-dialog labels='{"addCollection":"stale"}'></jant-compose-dialog>`;
|
|
214
|
+
const dialog = document.querySelector(
|
|
215
|
+
"jant-compose-dialog",
|
|
216
|
+
) as HTMLElement & {
|
|
217
|
+
labels?: unknown;
|
|
218
|
+
};
|
|
219
|
+
dialog.labels = { addCollection: "Add Collection" };
|
|
220
|
+
|
|
221
|
+
expect(readComposeDialogLabels()?.addCollection).toBe("Add Collection");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("returns null without a composer or with unreadable labels", () => {
|
|
225
|
+
expect(readComposeDialogLabels()).toBeNull();
|
|
226
|
+
|
|
227
|
+
document.body.innerHTML = `<jant-compose-dialog labels="{not json"></jant-compose-dialog>`;
|
|
228
|
+
expect(readComposeDialogLabels()).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
// The composer's bundle loads on first use; the harness element below stands
|
|
6
|
+
// in for the upgraded dialog, so loading is a no-op here.
|
|
7
|
+
vi.mock("../lazy-entries.js", () => ({
|
|
8
|
+
ensureCompose: async () => {},
|
|
9
|
+
ensureSettings: async () => {},
|
|
10
|
+
ensureManage: async () => {},
|
|
11
|
+
loadEntriesForPage: () => {},
|
|
12
|
+
}));
|
|
13
|
+
|
|
4
14
|
import { __testOnly as composeDiscoveryTestOnly } from "../compose-discovery.js";
|
|
5
15
|
import { __testOnly as composeShortcutsTestOnly } from "../compose-shortcuts.js";
|
|
6
16
|
|
|
@@ -8,6 +18,7 @@ type ComposeHarness = HTMLElement & {
|
|
|
8
18
|
openNew: (options?: unknown) => Promise<void>;
|
|
9
19
|
openReply: (...args: unknown[]) => Promise<void>;
|
|
10
20
|
openEdit: (id: string) => Promise<void>;
|
|
21
|
+
openDraft: (id: string) => Promise<void>;
|
|
11
22
|
};
|
|
12
23
|
|
|
13
24
|
function dispatchShortcut(
|
|
@@ -23,6 +34,11 @@ function dispatchShortcut(
|
|
|
23
34
|
return event;
|
|
24
35
|
}
|
|
25
36
|
|
|
37
|
+
/** Let a shortcut's trip through the lazy loader settle. */
|
|
38
|
+
async function flush(): Promise<void> {
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
function createComposeHarness(): ComposeHarness {
|
|
27
43
|
const composeEl = document.createElement(
|
|
28
44
|
"jant-compose-dialog",
|
|
@@ -30,6 +46,7 @@ function createComposeHarness(): ComposeHarness {
|
|
|
30
46
|
composeEl.openNew = vi.fn(async () => {});
|
|
31
47
|
composeEl.openReply = vi.fn(async () => {});
|
|
32
48
|
composeEl.openEdit = vi.fn(async () => {});
|
|
49
|
+
composeEl.openDraft = vi.fn(async () => {});
|
|
33
50
|
document.body.appendChild(composeEl);
|
|
34
51
|
return composeEl;
|
|
35
52
|
}
|
|
@@ -106,7 +123,7 @@ describe("compose shortcuts", () => {
|
|
|
106
123
|
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
|
|
107
124
|
});
|
|
108
125
|
|
|
109
|
-
it("opens a collection-scoped composer on collection pages with n", () => {
|
|
126
|
+
it("opens a collection-scoped composer on collection pages with n", async () => {
|
|
110
127
|
const composeEl = createComposeHarness();
|
|
111
128
|
|
|
112
129
|
const collectionPage = document.createElement("div");
|
|
@@ -116,6 +133,8 @@ describe("compose shortcuts", () => {
|
|
|
116
133
|
|
|
117
134
|
const event = dispatchShortcut(document, "n");
|
|
118
135
|
|
|
136
|
+
await flush();
|
|
137
|
+
|
|
119
138
|
expect(event.defaultPrevented).toBe(true);
|
|
120
139
|
expect(composeEl.openNew).toHaveBeenCalledWith({ collectionId: "col-2" });
|
|
121
140
|
});
|
|
@@ -124,7 +143,8 @@ describe("compose shortcuts", () => {
|
|
|
124
143
|
createComposeHarness();
|
|
125
144
|
|
|
126
145
|
dispatchShortcut(document, "n");
|
|
127
|
-
|
|
146
|
+
|
|
147
|
+
await flush();
|
|
128
148
|
|
|
129
149
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
130
150
|
composeDiscoveryTestOnly.COMPOSE_OPEN_SHORTCUT_DISCOVERY_API_PATH,
|
|
@@ -137,7 +157,7 @@ describe("compose shortcuts", () => {
|
|
|
137
157
|
});
|
|
138
158
|
});
|
|
139
159
|
|
|
140
|
-
it("ignores n while focus is inside an input", () => {
|
|
160
|
+
it("ignores n while focus is inside an input", async () => {
|
|
141
161
|
const composeEl = createComposeHarness();
|
|
142
162
|
|
|
143
163
|
const input = document.createElement("textarea");
|
|
@@ -145,10 +165,12 @@ describe("compose shortcuts", () => {
|
|
|
145
165
|
|
|
146
166
|
dispatchShortcut(input, "n");
|
|
147
167
|
|
|
168
|
+
await flush();
|
|
169
|
+
|
|
148
170
|
expect(composeEl.openNew).not.toHaveBeenCalled();
|
|
149
171
|
});
|
|
150
172
|
|
|
151
|
-
it("opens a reply composer for the latest thread post on detail pages with r", () => {
|
|
173
|
+
it("opens a reply composer for the latest thread post on detail pages with r", async () => {
|
|
152
174
|
const composeEl = createComposeHarness();
|
|
153
175
|
|
|
154
176
|
const postView = document.createElement("div");
|
|
@@ -199,6 +221,8 @@ describe("compose shortcuts", () => {
|
|
|
199
221
|
|
|
200
222
|
const event = dispatchShortcut(document, "r");
|
|
201
223
|
|
|
224
|
+
await flush();
|
|
225
|
+
|
|
202
226
|
expect(event.defaultPrevented).toBe(true);
|
|
203
227
|
expect(composeEl.openReply).toHaveBeenCalledTimes(1);
|
|
204
228
|
|
|
@@ -222,7 +246,7 @@ describe("compose shortcuts", () => {
|
|
|
222
246
|
);
|
|
223
247
|
});
|
|
224
248
|
|
|
225
|
-
it("targets the latest thread post for reply shortcuts even when an older post is hovered", () => {
|
|
249
|
+
it("targets the latest thread post for reply shortcuts even when an older post is hovered", async () => {
|
|
226
250
|
const composeEl = createComposeHarness();
|
|
227
251
|
const { hoveredArticle } = renderThreadDetailPage();
|
|
228
252
|
const originalQuerySelector = document.querySelector.bind(document);
|
|
@@ -238,6 +262,8 @@ describe("compose shortcuts", () => {
|
|
|
238
262
|
|
|
239
263
|
const event = dispatchShortcut(document, "r");
|
|
240
264
|
|
|
265
|
+
await flush();
|
|
266
|
+
|
|
241
267
|
expect(event.defaultPrevented).toBe(true);
|
|
242
268
|
expect(composeEl.openReply).toHaveBeenCalledWith(
|
|
243
269
|
"post-latest",
|
|
@@ -263,7 +289,8 @@ describe("compose shortcuts", () => {
|
|
|
263
289
|
);
|
|
264
290
|
|
|
265
291
|
const event = dispatchShortcut(document, "e");
|
|
266
|
-
|
|
292
|
+
|
|
293
|
+
await flush();
|
|
267
294
|
|
|
268
295
|
expect(event.defaultPrevented).toBe(true);
|
|
269
296
|
expect(composeEl.openEdit).toHaveBeenCalledWith("post-hovered");
|
|
@@ -277,14 +304,34 @@ describe("compose shortcuts", () => {
|
|
|
277
304
|
globalThis.history.pushState({}, "", "/about?edit=1");
|
|
278
305
|
|
|
279
306
|
composeShortcutsTestOnly.openEditFromQueryParam();
|
|
280
|
-
|
|
307
|
+
|
|
308
|
+
await flush();
|
|
281
309
|
|
|
282
310
|
expect(composeEl.openEdit).toHaveBeenCalledWith("post-current");
|
|
283
311
|
expect(window.location.pathname).toBe("/about");
|
|
284
312
|
expect(window.location.search).toBe("");
|
|
285
313
|
});
|
|
286
314
|
|
|
287
|
-
it("
|
|
315
|
+
it("loads the current draft through the drafts workflow on preview pages", async () => {
|
|
316
|
+
const composeEl = createComposeHarness();
|
|
317
|
+
document.body.setAttribute("data-authenticated", "true");
|
|
318
|
+
const previewBar = document.createElement("aside");
|
|
319
|
+
previewBar.dataset.previewStatus = "";
|
|
320
|
+
document.body.appendChild(previewBar);
|
|
321
|
+
renderThreadDetailPage();
|
|
322
|
+
globalThis.history.pushState({}, "", "/preview/draft-post?edit=1");
|
|
323
|
+
|
|
324
|
+
composeShortcutsTestOnly.openEditFromQueryParam();
|
|
325
|
+
|
|
326
|
+
await flush();
|
|
327
|
+
|
|
328
|
+
expect(composeEl.openDraft).toHaveBeenCalledWith("post-current");
|
|
329
|
+
expect(composeEl.openEdit).not.toHaveBeenCalled();
|
|
330
|
+
expect(window.location.pathname).toBe("/preview/draft-post");
|
|
331
|
+
expect(window.location.search).toBe("");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("keeps using the hovered post for reply shortcuts in the timeline", async () => {
|
|
288
335
|
const composeEl = createComposeHarness();
|
|
289
336
|
const article = document.createElement("article");
|
|
290
337
|
article.dataset.post = "";
|
|
@@ -312,6 +359,8 @@ describe("compose shortcuts", () => {
|
|
|
312
359
|
|
|
313
360
|
const event = dispatchShortcut(document, "r");
|
|
314
361
|
|
|
362
|
+
await flush();
|
|
363
|
+
|
|
315
364
|
expect(event.defaultPrevented).toBe(true);
|
|
316
365
|
expect(composeEl.openReply).toHaveBeenCalledTimes(1);
|
|
317
366
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
const launch = vi.hoisted(() => ({
|
|
6
|
+
openNewCompose: vi.fn(async () => {}),
|
|
7
|
+
openReplyForArticle: vi.fn(async () => {}),
|
|
8
|
+
openEditForPost: vi.fn(async () => {}),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("../compose-launch.js", () => launch);
|
|
12
|
+
|
|
13
|
+
import "../compose-triggers.js";
|
|
14
|
+
|
|
15
|
+
function click(selector: string): void {
|
|
16
|
+
const el = document.querySelector<HTMLElement>(selector);
|
|
17
|
+
if (!el) throw new Error(`expected ${selector} in DOM`);
|
|
18
|
+
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("compose triggers", () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
document.body.innerHTML = "";
|
|
24
|
+
launch.openNewCompose.mockClear();
|
|
25
|
+
launch.openReplyForArticle.mockClear();
|
|
26
|
+
launch.openEditForPost.mockClear();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("opens a new post from a [data-compose-open] button", () => {
|
|
30
|
+
document.body.innerHTML = `<button data-compose-open><span>New</span></button>`;
|
|
31
|
+
|
|
32
|
+
click("button span");
|
|
33
|
+
|
|
34
|
+
expect(launch.openNewCompose).toHaveBeenCalledWith(undefined);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("scopes the new post to the button's collection", () => {
|
|
38
|
+
document.body.innerHTML = `<button data-compose-open data-compose-collection-id="col_1">New</button>`;
|
|
39
|
+
|
|
40
|
+
click("button");
|
|
41
|
+
|
|
42
|
+
expect(launch.openNewCompose).toHaveBeenCalledWith({
|
|
43
|
+
collectionId: "col_1",
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("replies to the post around a [data-reply-trigger]", () => {
|
|
48
|
+
document.body.innerHTML = `
|
|
49
|
+
<article data-post data-post-id="pst_1">
|
|
50
|
+
<button data-reply-trigger>Reply</button>
|
|
51
|
+
</article>`;
|
|
52
|
+
|
|
53
|
+
click("[data-reply-trigger]");
|
|
54
|
+
|
|
55
|
+
const article = document.querySelector("article");
|
|
56
|
+
expect(launch.openReplyForArticle).toHaveBeenCalledWith(article);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("ignores a reply trigger outside any post", () => {
|
|
60
|
+
document.body.innerHTML = `<button data-reply-trigger>Reply</button>`;
|
|
61
|
+
|
|
62
|
+
click("[data-reply-trigger]");
|
|
63
|
+
|
|
64
|
+
expect(launch.openReplyForArticle).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("continues an inline draft from its badge, by its own or its post's id", () => {
|
|
68
|
+
document.body.innerHTML = `
|
|
69
|
+
<span data-draft-continue data-post-id="pst_draft">Draft</span>
|
|
70
|
+
<article data-post data-post-id="pst_2">
|
|
71
|
+
<span data-draft-continue>Draft</span>
|
|
72
|
+
</article>`;
|
|
73
|
+
|
|
74
|
+
click("[data-draft-continue][data-post-id]");
|
|
75
|
+
click("article [data-draft-continue]");
|
|
76
|
+
|
|
77
|
+
expect(launch.openEditForPost).toHaveBeenNthCalledWith(1, "pst_draft");
|
|
78
|
+
expect(launch.openEditForPost).toHaveBeenNthCalledWith(2, "pst_2");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("leaves unrelated clicks alone", () => {
|
|
82
|
+
document.body.innerHTML = `<button>Nothing</button>`;
|
|
83
|
+
|
|
84
|
+
click("button");
|
|
85
|
+
|
|
86
|
+
expect(launch.openNewCompose).not.toHaveBeenCalled();
|
|
87
|
+
expect(launch.openReplyForArticle).not.toHaveBeenCalled();
|
|
88
|
+
expect(launch.openEditForPost).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
4
|
+
import "../copy-field.js";
|
|
5
|
+
|
|
6
|
+
const FEED_URL = "https://example.com/feed";
|
|
7
|
+
const COPIED = "Feed URL copied.";
|
|
8
|
+
const FAILED = "Could not copy. Select the address and copy it.";
|
|
9
|
+
|
|
10
|
+
function requireElement<T>(element: T | null | undefined, message: string): T {
|
|
11
|
+
if (!element) throw new Error(message);
|
|
12
|
+
return element;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function renderField({ hiddenButton = false } = {}): {
|
|
16
|
+
input: HTMLInputElement;
|
|
17
|
+
button: HTMLButtonElement;
|
|
18
|
+
} {
|
|
19
|
+
document.body.innerHTML = `
|
|
20
|
+
<div id="toast-container"></div>
|
|
21
|
+
<div data-copy-field-root>
|
|
22
|
+
<div class="relative">
|
|
23
|
+
<input type="text" readonly value="${FEED_URL}" data-copy-field-value />
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
${hiddenButton ? "hidden" : ""}
|
|
27
|
+
data-copy-field="${COPIED}"
|
|
28
|
+
data-copy-field-failed="${FAILED}"
|
|
29
|
+
>Copy</button>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
input: requireElement(
|
|
36
|
+
document.querySelector<HTMLInputElement>("[data-copy-field-value]"),
|
|
37
|
+
"expected the address input",
|
|
38
|
+
),
|
|
39
|
+
button: requireElement(
|
|
40
|
+
document.querySelector<HTMLButtonElement>("[data-copy-field]"),
|
|
41
|
+
"expected the copy button",
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toastText(): string {
|
|
47
|
+
return document.getElementById("toast-container")?.textContent ?? "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function settle(): Promise<void> {
|
|
51
|
+
await Promise.resolve();
|
|
52
|
+
await Promise.resolve();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("copy field", () => {
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
vi.restoreAllMocks();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("copying", () => {
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
document.body.innerHTML = "";
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("copies the address and reports it", async () => {
|
|
66
|
+
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
67
|
+
Object.defineProperty(globalThis.navigator, "clipboard", {
|
|
68
|
+
value: { writeText },
|
|
69
|
+
configurable: true,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const { button } = renderField();
|
|
73
|
+
button.click();
|
|
74
|
+
await settle();
|
|
75
|
+
|
|
76
|
+
expect(writeText).toHaveBeenCalledWith(FEED_URL);
|
|
77
|
+
expect(toastText()).toContain(COPIED);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// The Clipboard API is absent on insecure origins and can be denied by
|
|
81
|
+
// permission. Failing loudly is what points the reader at selecting the
|
|
82
|
+
// address by hand instead.
|
|
83
|
+
it("reports a failure when the clipboard is unavailable", async () => {
|
|
84
|
+
Object.defineProperty(globalThis.navigator, "clipboard", {
|
|
85
|
+
value: undefined,
|
|
86
|
+
configurable: true,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const { button } = renderField();
|
|
90
|
+
button.click();
|
|
91
|
+
await settle();
|
|
92
|
+
|
|
93
|
+
expect(toastText()).toContain("Could not copy");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("reports a failure when the clipboard refuses", async () => {
|
|
97
|
+
const writeText = vi.fn().mockRejectedValue(new Error("denied"));
|
|
98
|
+
Object.defineProperty(globalThis.navigator, "clipboard", {
|
|
99
|
+
value: { writeText },
|
|
100
|
+
configurable: true,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const { button } = renderField();
|
|
104
|
+
button.click();
|
|
105
|
+
await settle();
|
|
106
|
+
|
|
107
|
+
expect(toastText()).toContain("Could not copy");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("finds fields added after load", async () => {
|
|
111
|
+
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
112
|
+
Object.defineProperty(globalThis.navigator, "clipboard", {
|
|
113
|
+
value: { writeText },
|
|
114
|
+
configurable: true,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Nothing re-runs against new markup; the listener is on the document.
|
|
118
|
+
const { button } = renderField();
|
|
119
|
+
button.click();
|
|
120
|
+
await settle();
|
|
121
|
+
|
|
122
|
+
expect(writeText).toHaveBeenCalledWith(FEED_URL);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("manual selection fallback", () => {
|
|
127
|
+
beforeEach(() => {
|
|
128
|
+
document.body.innerHTML = "";
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("selects the whole address on click", () => {
|
|
132
|
+
const { input } = renderField();
|
|
133
|
+
const select = vi.spyOn(input, "select");
|
|
134
|
+
|
|
135
|
+
input.dispatchEvent(new Event("click", { bubbles: true }));
|
|
136
|
+
|
|
137
|
+
expect(select).toHaveBeenCalledTimes(1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("selects the whole address on focus", () => {
|
|
141
|
+
const { input } = renderField();
|
|
142
|
+
const select = vi.spyOn(input, "select");
|
|
143
|
+
|
|
144
|
+
input.dispatchEvent(new Event("focus"));
|
|
145
|
+
|
|
146
|
+
expect(select).toHaveBeenCalledTimes(1);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// The server ships the button hidden so that without this script the field
|
|
151
|
+
// degrades to a selectable input rather than a button that does nothing.
|
|
152
|
+
describe("revealing the button", () => {
|
|
153
|
+
it("reveals a button the server rendered hidden", () => {
|
|
154
|
+
const { button } = renderField({ hiddenButton: true });
|
|
155
|
+
expect(button.hidden).toBe(true);
|
|
156
|
+
|
|
157
|
+
document.dispatchEvent(new Event("DOMContentLoaded"));
|
|
158
|
+
|
|
159
|
+
expect(button.hidden).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -3,9 +3,47 @@
|
|
|
3
3
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
4
4
|
import { initCustomUrlMenus } from "../custom-url-menu.js";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* happy-dom has no popover API, so the top layer is stubbed: `showPopover`
|
|
8
|
+
* and `hidePopover` drive a flag that `:popover-open` then reports, the same
|
|
9
|
+
* contract the browser offers.
|
|
10
|
+
*/
|
|
11
|
+
function stubTopLayer(menu: HTMLElement) {
|
|
12
|
+
let shown = false;
|
|
13
|
+
const nativeMatches = menu.matches.bind(menu);
|
|
14
|
+
|
|
15
|
+
Object.assign(menu, {
|
|
16
|
+
showPopover: () => {
|
|
17
|
+
shown = true;
|
|
18
|
+
},
|
|
19
|
+
hidePopover: () => {
|
|
20
|
+
shown = false;
|
|
21
|
+
},
|
|
22
|
+
matches: (selector: string) =>
|
|
23
|
+
selector === ":popover-open" ? shown : nativeMatches(selector),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return () => shown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** happy-dom lays nothing out, so anchoring math needs stated geometry. */
|
|
30
|
+
function stubRect(el: HTMLElement, rect: Partial<globalThis.DOMRect>) {
|
|
31
|
+
el.getBoundingClientRect = () =>
|
|
32
|
+
({
|
|
33
|
+
top: 0,
|
|
34
|
+
bottom: 0,
|
|
35
|
+
left: 0,
|
|
36
|
+
right: 0,
|
|
37
|
+
width: 0,
|
|
38
|
+
height: 0,
|
|
39
|
+
...rect,
|
|
40
|
+
}) as globalThis.DOMRect;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function renderMenus({ topLayer = false } = {}) {
|
|
7
44
|
document.body.innerHTML = `
|
|
8
45
|
<div data-custom-url-actions>
|
|
46
|
+
<code data-row-blank-space>/first</code>
|
|
9
47
|
<button
|
|
10
48
|
type="button"
|
|
11
49
|
data-custom-url-action="toggle-menu"
|
|
@@ -13,7 +51,7 @@ function renderMenus() {
|
|
|
13
51
|
>
|
|
14
52
|
More actions
|
|
15
53
|
</button>
|
|
16
|
-
<div data-custom-url-menu role="menu" hidden>
|
|
54
|
+
<div data-custom-url-menu role="menu" popover="manual" hidden>
|
|
17
55
|
<button type="button" role="menuitem" data-custom-url-action="delete">
|
|
18
56
|
Delete
|
|
19
57
|
</button>
|
|
@@ -27,7 +65,7 @@ function renderMenus() {
|
|
|
27
65
|
>
|
|
28
66
|
More actions
|
|
29
67
|
</button>
|
|
30
|
-
<div data-custom-url-menu role="menu" hidden>
|
|
68
|
+
<div data-custom-url-menu role="menu" popover="manual" hidden>
|
|
31
69
|
<button type="button" role="menuitem" data-custom-url-action="delete">
|
|
32
70
|
Delete
|
|
33
71
|
</button>
|
|
@@ -35,17 +73,24 @@ function renderMenus() {
|
|
|
35
73
|
</div>
|
|
36
74
|
`;
|
|
37
75
|
|
|
76
|
+
const menus = document.querySelectorAll<HTMLElement>(
|
|
77
|
+
"[data-custom-url-menu]",
|
|
78
|
+
);
|
|
79
|
+
// Stubbed before init: the script reads popover support once, up front.
|
|
80
|
+
const isInTopLayer = topLayer ? stubTopLayer(menus[0]!) : () => false;
|
|
81
|
+
|
|
38
82
|
initCustomUrlMenus();
|
|
39
83
|
|
|
40
84
|
const triggers = document.querySelectorAll<HTMLButtonElement>(
|
|
41
85
|
"[data-custom-url-action='toggle-menu']",
|
|
42
86
|
);
|
|
43
|
-
const menus = document.querySelectorAll<HTMLElement>(
|
|
44
|
-
"[data-custom-url-menu]",
|
|
45
|
-
);
|
|
46
87
|
const menuItems = document.querySelectorAll<HTMLElement>("[role='menuitem']");
|
|
47
88
|
|
|
48
89
|
return {
|
|
90
|
+
isInTopLayer,
|
|
91
|
+
firstRowBlankSpace: document.querySelector<HTMLElement>(
|
|
92
|
+
"[data-row-blank-space]",
|
|
93
|
+
),
|
|
49
94
|
firstTrigger: triggers[0],
|
|
50
95
|
secondTrigger: triggers[1],
|
|
51
96
|
firstMenu: menus[0],
|
|
@@ -72,6 +117,30 @@ describe("custom URL menus", () => {
|
|
|
72
117
|
expect(firstTrigger?.getAttribute("aria-expanded")).toBe("false");
|
|
73
118
|
});
|
|
74
119
|
|
|
120
|
+
it("closes when clicking blank space in its own row", () => {
|
|
121
|
+
const { firstTrigger, firstMenu, firstRowBlankSpace } = renderMenus();
|
|
122
|
+
|
|
123
|
+
firstTrigger?.click();
|
|
124
|
+
expect(firstMenu?.hidden).toBe(false);
|
|
125
|
+
|
|
126
|
+
firstRowBlankSpace?.dispatchEvent(
|
|
127
|
+
new MouseEvent("click", { bubbles: true }),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(firstMenu?.hidden).toBe(true);
|
|
131
|
+
expect(firstTrigger?.getAttribute("aria-expanded")).toBe("false");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("stays open when clicking inside the menu", () => {
|
|
135
|
+
const { firstTrigger, firstMenu } = renderMenus();
|
|
136
|
+
|
|
137
|
+
firstTrigger?.click();
|
|
138
|
+
firstMenu?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
139
|
+
|
|
140
|
+
expect(firstMenu?.hidden).toBe(false);
|
|
141
|
+
expect(firstTrigger?.getAttribute("aria-expanded")).toBe("true");
|
|
142
|
+
});
|
|
143
|
+
|
|
75
144
|
it("closes on Escape and returns focus to the trigger", () => {
|
|
76
145
|
const { firstTrigger, firstMenu, firstDeleteButton } = renderMenus();
|
|
77
146
|
|
|
@@ -87,6 +156,58 @@ describe("custom URL menus", () => {
|
|
|
87
156
|
expect(document.activeElement).toBe(firstTrigger);
|
|
88
157
|
});
|
|
89
158
|
|
|
159
|
+
it("shows in the top layer and anchors itself to the trigger", () => {
|
|
160
|
+
const { firstTrigger, firstMenu, isInTopLayer } = renderMenus({
|
|
161
|
+
topLayer: true,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
firstTrigger?.click();
|
|
165
|
+
|
|
166
|
+
expect(isInTopLayer()).toBe(true);
|
|
167
|
+
// The row list clips its children, so an open menu escapes to the top
|
|
168
|
+
// layer and is positioned against the viewport instead of the row.
|
|
169
|
+
expect(firstMenu?.style.top).not.toBe("");
|
|
170
|
+
expect(firstMenu?.style.left).not.toBe("");
|
|
171
|
+
|
|
172
|
+
document.body.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
173
|
+
|
|
174
|
+
expect(isInTopLayer()).toBe(false);
|
|
175
|
+
expect(firstMenu?.hidden).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// The viewport is 1024x768 under happy-dom.
|
|
179
|
+
it("anchors below the trigger when the viewport has room", () => {
|
|
180
|
+
const { firstTrigger, firstMenu } = renderMenus({ topLayer: true });
|
|
181
|
+
stubRect(firstTrigger!, { top: 100, bottom: 126, right: 500 });
|
|
182
|
+
stubRect(firstMenu!, { width: 140, height: 120 });
|
|
183
|
+
|
|
184
|
+
firstTrigger?.click();
|
|
185
|
+
|
|
186
|
+
expect(firstMenu?.style.top).toBe("132px");
|
|
187
|
+
expect(firstMenu?.style.left).toBe("360px");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("flips above the trigger when the viewport bottom is too close", () => {
|
|
191
|
+
const { firstTrigger, firstMenu } = renderMenus({ topLayer: true });
|
|
192
|
+
stubRect(firstTrigger!, { top: 700, bottom: 726, right: 500 });
|
|
193
|
+
stubRect(firstMenu!, { width: 140, height: 120 });
|
|
194
|
+
|
|
195
|
+
firstTrigger?.click();
|
|
196
|
+
|
|
197
|
+
expect(firstMenu?.style.top).toBe("574px");
|
|
198
|
+
expect(firstMenu?.style.left).toBe("360px");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("keeps the menu inside the left viewport edge", () => {
|
|
202
|
+
const { firstTrigger, firstMenu } = renderMenus({ topLayer: true });
|
|
203
|
+
stubRect(firstTrigger!, { top: 100, bottom: 126, right: 60 });
|
|
204
|
+
stubRect(firstMenu!, { width: 140, height: 120 });
|
|
205
|
+
|
|
206
|
+
firstTrigger?.click();
|
|
207
|
+
|
|
208
|
+
expect(firstMenu?.style.left).toBe("8px");
|
|
209
|
+
});
|
|
210
|
+
|
|
90
211
|
it("closes the previous menu when another one opens", () => {
|
|
91
212
|
const { firstTrigger, secondTrigger, firstMenu, secondMenu } =
|
|
92
213
|
renderMenus();
|