@jant/core 0.6.16 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +60 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +16 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +159 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +23 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +5 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +471 -369
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +178 -17
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post.test.ts +46 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +278 -19
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1087 -7219
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
vi.mock("../../toast.js", () => ({
|
|
6
|
+
showToast: vi.fn(),
|
|
7
|
+
showToastWithAction: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
// The real slugify loads the transliteration dictionary through a dynamic
|
|
11
|
+
// import, which fake timers never advance past; the dialog's behaviour does
|
|
12
|
+
// not depend on which slug comes back.
|
|
13
|
+
vi.mock("../../lazy-slugify.js", () => ({
|
|
14
|
+
slugify: (text: string) =>
|
|
15
|
+
Promise.resolve(
|
|
16
|
+
text
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(/[^\w\s-]/g, "")
|
|
20
|
+
.replace(/[\s_-]+/g, "-")
|
|
21
|
+
.replace(/^-+|-+$/g, ""),
|
|
22
|
+
),
|
|
23
|
+
preloadSlug: () => {},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import "../jant-smart-collection-dialog.js";
|
|
27
|
+
import type { JantSmartCollectionDialog } from "../jant-smart-collection-dialog.js";
|
|
28
|
+
import { setCollectionVocabulary } from "../smart-collection-conditions.js";
|
|
29
|
+
import type { SmartCollectionDialogLabels } from "../smart-collection-dialog-types.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The dialog is the only place a smart collection is written, so the behaviours
|
|
33
|
+
* it has to hold are the ones the design turns on: one row per dimension,
|
|
34
|
+
* conditions that survive a round trip through the shared vocabulary, a live
|
|
35
|
+
* count, and Escape working from inside the fields.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const labels: SmartCollectionDialogLabels = {
|
|
39
|
+
createHeading: "New Smart Collection",
|
|
40
|
+
editHeading: "Edit Smart Collection",
|
|
41
|
+
whatItIs:
|
|
42
|
+
"Conditions choose what belongs here, not you. Posts you write later join on their own.",
|
|
43
|
+
title: "Title",
|
|
44
|
+
link: "Collection link",
|
|
45
|
+
linkHelp: "This is the last part of the collection link.",
|
|
46
|
+
editLink: "Edit link",
|
|
47
|
+
resetLink: "Reset link",
|
|
48
|
+
linkTaken: "This link is taken. Choose another.",
|
|
49
|
+
linkInvalid: "Use lowercase letters, numbers, and hyphens only.",
|
|
50
|
+
linkReserved: "This link is reserved. Choose something else.",
|
|
51
|
+
linkTooLong: "Keep this link under 200 characters.",
|
|
52
|
+
linkMovesWarning: "Changing the link breaks the old one immediately.",
|
|
53
|
+
description: "Description",
|
|
54
|
+
conditionsHeading: "Conditions",
|
|
55
|
+
matchAllHint: "Posts matching all of these",
|
|
56
|
+
noConditions: "No conditions yet. Add one to choose what lands here.",
|
|
57
|
+
addCondition: "Add condition",
|
|
58
|
+
removeCondition: "Remove condition",
|
|
59
|
+
countSummary: "{count} of {total} threads",
|
|
60
|
+
counting: "Counting…",
|
|
61
|
+
displayHeading: "Display",
|
|
62
|
+
orderBy: "Order by",
|
|
63
|
+
layout: "Layout",
|
|
64
|
+
cancel: "Cancel",
|
|
65
|
+
save: "Save",
|
|
66
|
+
saved: "Smart collection saved.",
|
|
67
|
+
saveFailed: "Could not save. Try again.",
|
|
68
|
+
loadFailed: "Could not open this smart collection. Try again.",
|
|
69
|
+
titleAndLinkRequired: "A smart collection needs a title and a link.",
|
|
70
|
+
dimensions: {
|
|
71
|
+
collection: "Collection",
|
|
72
|
+
format: "Format",
|
|
73
|
+
title: "Title",
|
|
74
|
+
year: "Year",
|
|
75
|
+
media: "Media",
|
|
76
|
+
replies: "Replies",
|
|
77
|
+
visibility: "Visibility",
|
|
78
|
+
},
|
|
79
|
+
values: {
|
|
80
|
+
"format.note": "Notes",
|
|
81
|
+
"format.link": "Links",
|
|
82
|
+
"format.quote": "Quotes",
|
|
83
|
+
"title.any": "Titled",
|
|
84
|
+
"title.none": "Untitled",
|
|
85
|
+
"replies.any": "Threads",
|
|
86
|
+
"replies.none": "Single posts",
|
|
87
|
+
"visibility.public": "Public",
|
|
88
|
+
"visibility.featured": "Featured",
|
|
89
|
+
"visibility.hidden": "Hidden from Latest",
|
|
90
|
+
"media.any": "With media",
|
|
91
|
+
"media.none": "Without media",
|
|
92
|
+
"media.image": "Images",
|
|
93
|
+
},
|
|
94
|
+
sortOptions: { newest: "Newest first", oldest: "Oldest first" },
|
|
95
|
+
layoutOptions: { "": "Follow site default", list: "List", grid: "Grid" },
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Responses the dialog's fetches get, keyed by the path they hit. */
|
|
99
|
+
let responses: Record<string, unknown>;
|
|
100
|
+
let requests: Array<{ url: string; method: string; body: unknown }>;
|
|
101
|
+
|
|
102
|
+
function mountDialog(): JantSmartCollectionDialog {
|
|
103
|
+
const element = document.createElement(
|
|
104
|
+
"jant-smart-collection-dialog",
|
|
105
|
+
) as JantSmartCollectionDialog;
|
|
106
|
+
element.labels = labels;
|
|
107
|
+
document.body.appendChild(element);
|
|
108
|
+
return element;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Let the debounced preview and slug requests fire and settle. */
|
|
112
|
+
async function settle(element: JantSmartCollectionDialog) {
|
|
113
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
114
|
+
await element.updateComplete;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
document.body.innerHTML = "";
|
|
119
|
+
vi.useFakeTimers();
|
|
120
|
+
setCollectionVocabulary([{ id: "col_books", slug: "books", title: "Books" }]);
|
|
121
|
+
|
|
122
|
+
responses = {
|
|
123
|
+
"/api/smart-collections/preview": { count: 3, baseline: 42 },
|
|
124
|
+
"/api/smart-collections/slug": { slug: "quotes", available: true },
|
|
125
|
+
};
|
|
126
|
+
requests = [];
|
|
127
|
+
|
|
128
|
+
vi.stubGlobal(
|
|
129
|
+
"fetch",
|
|
130
|
+
vi.fn(async (url: string, init?: globalThis.RequestInit) => {
|
|
131
|
+
const path = url.split("?")[0] ?? url;
|
|
132
|
+
requests.push({
|
|
133
|
+
url,
|
|
134
|
+
method: init?.method ?? "GET",
|
|
135
|
+
body: init?.body ? JSON.parse(String(init.body)) : undefined,
|
|
136
|
+
});
|
|
137
|
+
const body = responses[path] ?? { smartCollection: {} };
|
|
138
|
+
return {
|
|
139
|
+
ok: true,
|
|
140
|
+
status: 200,
|
|
141
|
+
json: async () => body,
|
|
142
|
+
} as Response;
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// happy-dom's <dialog> has no modal implementation.
|
|
147
|
+
HTMLDialogElement.prototype.showModal = function showModal() {
|
|
148
|
+
this.open = true;
|
|
149
|
+
};
|
|
150
|
+
HTMLDialogElement.prototype.close = function close() {
|
|
151
|
+
this.open = false;
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("JantSmartCollectionDialog", () => {
|
|
156
|
+
it("opens empty, and says so rather than showing a blank condition list", async () => {
|
|
157
|
+
const element = mountDialog();
|
|
158
|
+
void element.open({});
|
|
159
|
+
await settle(element);
|
|
160
|
+
|
|
161
|
+
expect(element.textContent).toContain("New Smart Collection");
|
|
162
|
+
expect(element.textContent).toContain(
|
|
163
|
+
"No conditions yet. Add one to choose what lands here.",
|
|
164
|
+
);
|
|
165
|
+
expect(element.querySelectorAll("[data-condition]")).toHaveLength(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("offers each dimension once, and stops offering one that is in use", async () => {
|
|
169
|
+
const element = mountDialog();
|
|
170
|
+
void element.open({ prefill: { selection: { format: "quote" } } });
|
|
171
|
+
await settle(element);
|
|
172
|
+
|
|
173
|
+
expect(element.querySelectorAll("[data-condition]")).toHaveLength(1);
|
|
174
|
+
|
|
175
|
+
const addButton = [...element.querySelectorAll("button")].find(
|
|
176
|
+
(button) => button.textContent?.trim() === "Add condition",
|
|
177
|
+
);
|
|
178
|
+
addButton?.click();
|
|
179
|
+
await element.updateComplete;
|
|
180
|
+
|
|
181
|
+
const offered = [...element.querySelectorAll("[role='menuitem']")].map(
|
|
182
|
+
(item) => item.textContent?.trim(),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Seven dimensions, one already used.
|
|
186
|
+
expect(offered).toHaveLength(6);
|
|
187
|
+
expect(offered).not.toContain("Format");
|
|
188
|
+
expect(offered).toContain("Year");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("dismisses the condition menu on a click elsewhere in the dialog", async () => {
|
|
192
|
+
const element = mountDialog();
|
|
193
|
+
void element.open({});
|
|
194
|
+
await settle(element);
|
|
195
|
+
|
|
196
|
+
const trigger =
|
|
197
|
+
element.querySelector<HTMLButtonElement>("[data-add-trigger]");
|
|
198
|
+
trigger?.click();
|
|
199
|
+
await element.updateComplete;
|
|
200
|
+
expect(element.querySelector("[data-add-menu]")).not.toBeNull();
|
|
201
|
+
|
|
202
|
+
// Not the backdrop — the panel stops that click, so a menu that only
|
|
203
|
+
// listened to the backdrop would stay open over the fields it covers.
|
|
204
|
+
element.querySelector<HTMLElement>(".collection-dialog-body")?.click();
|
|
205
|
+
await element.updateComplete;
|
|
206
|
+
|
|
207
|
+
expect(element.querySelector("[data-add-menu]")).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("closes the condition menu on Escape, and leaves the dialog open", async () => {
|
|
211
|
+
const element = mountDialog();
|
|
212
|
+
const closed = element.open({});
|
|
213
|
+
await settle(element);
|
|
214
|
+
|
|
215
|
+
element.querySelector<HTMLButtonElement>("[data-add-trigger]")?.click();
|
|
216
|
+
await element.updateComplete;
|
|
217
|
+
|
|
218
|
+
element.dispatchEvent(
|
|
219
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
|
|
220
|
+
);
|
|
221
|
+
await element.updateComplete;
|
|
222
|
+
|
|
223
|
+
expect(element.querySelector("[data-add-menu]")).toBeNull();
|
|
224
|
+
expect(element.querySelector("dialog")?.open).toBe(true);
|
|
225
|
+
|
|
226
|
+
// A second Escape, now that the menu is gone, closes the dialog itself.
|
|
227
|
+
element.dispatchEvent(
|
|
228
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
|
|
229
|
+
);
|
|
230
|
+
await expect(closed).resolves.toBe(false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("walks the condition menu with the arrow keys", async () => {
|
|
234
|
+
const element = mountDialog();
|
|
235
|
+
void element.open({});
|
|
236
|
+
await settle(element);
|
|
237
|
+
|
|
238
|
+
element.querySelector<HTMLButtonElement>("[data-add-trigger]")?.click();
|
|
239
|
+
await element.updateComplete;
|
|
240
|
+
await Promise.resolve();
|
|
241
|
+
|
|
242
|
+
const items = [
|
|
243
|
+
...element.querySelectorAll<HTMLButtonElement>(
|
|
244
|
+
"[data-add-menu] [role='menuitem']",
|
|
245
|
+
),
|
|
246
|
+
];
|
|
247
|
+
items[0]?.focus();
|
|
248
|
+
|
|
249
|
+
element.dispatchEvent(
|
|
250
|
+
new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
|
|
251
|
+
);
|
|
252
|
+
expect(document.activeElement).toBe(items[1]);
|
|
253
|
+
|
|
254
|
+
// Up from the first item wraps to the last rather than falling out.
|
|
255
|
+
items[0]?.focus();
|
|
256
|
+
element.dispatchEvent(
|
|
257
|
+
new KeyboardEvent("keydown", { key: "ArrowUp", bubbles: true }),
|
|
258
|
+
);
|
|
259
|
+
expect(document.activeElement).toBe(items[items.length - 1]);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("picks a condition on Enter instead of saving", async () => {
|
|
263
|
+
const element = mountDialog();
|
|
264
|
+
void element.open({ prefill: { title: "Quotes" } });
|
|
265
|
+
await settle(element);
|
|
266
|
+
|
|
267
|
+
element.querySelector<HTMLButtonElement>("[data-add-trigger]")?.click();
|
|
268
|
+
await element.updateComplete;
|
|
269
|
+
|
|
270
|
+
const item = element.querySelector<HTMLButtonElement>(
|
|
271
|
+
"[data-add-menu] [role='menuitem']",
|
|
272
|
+
);
|
|
273
|
+
item?.dispatchEvent(
|
|
274
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
|
275
|
+
);
|
|
276
|
+
await settle(element);
|
|
277
|
+
|
|
278
|
+
expect(
|
|
279
|
+
requests.some(
|
|
280
|
+
(request) =>
|
|
281
|
+
request.method === "POST" && !request.url.includes("/preview"),
|
|
282
|
+
),
|
|
283
|
+
).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("saves the conditions it was given, in the shared vocabulary", async () => {
|
|
287
|
+
const element = mountDialog();
|
|
288
|
+
void element.open({
|
|
289
|
+
prefill: {
|
|
290
|
+
title: "Quotes",
|
|
291
|
+
selection: { format: "quote", media: "any", title: false },
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
await settle(element);
|
|
295
|
+
|
|
296
|
+
const save = [...element.querySelectorAll("button")].find(
|
|
297
|
+
(button) => button.textContent?.trim() === "Save",
|
|
298
|
+
);
|
|
299
|
+
save?.click();
|
|
300
|
+
await settle(element);
|
|
301
|
+
|
|
302
|
+
const write = requests.find(
|
|
303
|
+
(request) =>
|
|
304
|
+
request.method === "POST" &&
|
|
305
|
+
request.url.includes("/api/smart-collections") &&
|
|
306
|
+
!request.url.includes("/preview"),
|
|
307
|
+
);
|
|
308
|
+
expect(write?.body).toMatchObject({
|
|
309
|
+
title: "Quotes",
|
|
310
|
+
selection: { format: "quote", media: "any", title: false },
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("removes a condition, and clears it from what gets saved", async () => {
|
|
315
|
+
const element = mountDialog();
|
|
316
|
+
void element.open({
|
|
317
|
+
prefill: { title: "Quotes", selection: { format: "quote", year: 2024 } },
|
|
318
|
+
});
|
|
319
|
+
await settle(element);
|
|
320
|
+
expect(element.querySelectorAll("[data-condition]")).toHaveLength(2);
|
|
321
|
+
|
|
322
|
+
const remove = element
|
|
323
|
+
.querySelector("[data-condition='year']")
|
|
324
|
+
?.querySelector("button");
|
|
325
|
+
remove?.click();
|
|
326
|
+
await settle(element);
|
|
327
|
+
|
|
328
|
+
expect(element.querySelectorAll("[data-condition]")).toHaveLength(1);
|
|
329
|
+
|
|
330
|
+
const save = [...element.querySelectorAll("button")].find(
|
|
331
|
+
(button) => button.textContent?.trim() === "Save",
|
|
332
|
+
);
|
|
333
|
+
save?.click();
|
|
334
|
+
await settle(element);
|
|
335
|
+
|
|
336
|
+
const write = requests.find(
|
|
337
|
+
(request) =>
|
|
338
|
+
request.method === "POST" && !request.url.includes("/preview"),
|
|
339
|
+
);
|
|
340
|
+
expect(write?.body).toMatchObject({ selection: { format: "quote" } });
|
|
341
|
+
expect(
|
|
342
|
+
(write?.body as { selection: Record<string, unknown> }).selection,
|
|
343
|
+
).not.toHaveProperty("year");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("counts what the conditions gather, against the site total", async () => {
|
|
347
|
+
const element = mountDialog();
|
|
348
|
+
void element.open({ prefill: { selection: { format: "quote" } } });
|
|
349
|
+
await settle(element);
|
|
350
|
+
|
|
351
|
+
expect(element.textContent).toContain("3 of 42 threads");
|
|
352
|
+
|
|
353
|
+
const preview = requests.find((request) =>
|
|
354
|
+
request.url.includes("/preview"),
|
|
355
|
+
);
|
|
356
|
+
expect(preview?.method).toBe("POST");
|
|
357
|
+
expect(preview?.body).toEqual({ selection: { format: "quote" } });
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("never offers the one visibility a published page cannot name", async () => {
|
|
361
|
+
const element = mountDialog();
|
|
362
|
+
void element.open({ prefill: { selection: { visibility: "public" } } });
|
|
363
|
+
await settle(element);
|
|
364
|
+
|
|
365
|
+
const options = [
|
|
366
|
+
...element.querySelectorAll("[data-condition='visibility'] option"),
|
|
367
|
+
].map((option) => (option as globalThis.HTMLOptionElement).value);
|
|
368
|
+
|
|
369
|
+
expect(options).toEqual(["public", "featured", "hidden"]);
|
|
370
|
+
expect(options).not.toContain("private");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("closes on Escape, from inside a field", async () => {
|
|
374
|
+
const element = mountDialog();
|
|
375
|
+
const closed = element.open({ prefill: { title: "Quotes" } });
|
|
376
|
+
await settle(element);
|
|
377
|
+
|
|
378
|
+
const input = element.querySelector<HTMLInputElement>(
|
|
379
|
+
"[data-field='title']",
|
|
380
|
+
);
|
|
381
|
+
// Dispatched on the field, not the dialog: an inner element that swallows
|
|
382
|
+
// keydown is exactly the case the component-level handler exists for.
|
|
383
|
+
input?.dispatchEvent(
|
|
384
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
|
|
385
|
+
);
|
|
386
|
+
await element.updateComplete;
|
|
387
|
+
|
|
388
|
+
await expect(closed).resolves.toBe(false);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("saves on Enter, but not from inside the description", async () => {
|
|
392
|
+
const element = mountDialog();
|
|
393
|
+
void element.open({ prefill: { title: "Quotes" } });
|
|
394
|
+
await settle(element);
|
|
395
|
+
|
|
396
|
+
const textarea = element.querySelector<HTMLTextAreaElement>("textarea");
|
|
397
|
+
textarea?.dispatchEvent(
|
|
398
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
|
399
|
+
);
|
|
400
|
+
await settle(element);
|
|
401
|
+
expect(
|
|
402
|
+
requests.some(
|
|
403
|
+
(request) =>
|
|
404
|
+
request.method === "POST" && !request.url.includes("/preview"),
|
|
405
|
+
),
|
|
406
|
+
).toBe(false);
|
|
407
|
+
|
|
408
|
+
const input = element.querySelector<HTMLInputElement>(
|
|
409
|
+
"[data-field='title']",
|
|
410
|
+
);
|
|
411
|
+
input?.dispatchEvent(
|
|
412
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
|
413
|
+
);
|
|
414
|
+
await settle(element);
|
|
415
|
+
expect(
|
|
416
|
+
requests.some(
|
|
417
|
+
(request) =>
|
|
418
|
+
request.method === "POST" && !request.url.includes("/preview"),
|
|
419
|
+
),
|
|
420
|
+
).toBe(true);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("holds Save shut until there is a title and a free address", async () => {
|
|
424
|
+
const element = mountDialog();
|
|
425
|
+
void element.open({});
|
|
426
|
+
await settle(element);
|
|
427
|
+
|
|
428
|
+
const save = () =>
|
|
429
|
+
[...element.querySelectorAll("button")].find(
|
|
430
|
+
(button) => button.textContent?.trim() === "Save",
|
|
431
|
+
);
|
|
432
|
+
expect(save()?.disabled).toBe(true);
|
|
433
|
+
|
|
434
|
+
const input = element.querySelector<HTMLInputElement>(
|
|
435
|
+
"[data-field='title']",
|
|
436
|
+
);
|
|
437
|
+
if (input) {
|
|
438
|
+
input.value = "Quotes";
|
|
439
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
440
|
+
}
|
|
441
|
+
await settle(element);
|
|
442
|
+
|
|
443
|
+
expect(save()?.disabled).toBe(false);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("holds Save shut on an address that is taken", async () => {
|
|
447
|
+
responses["/api/smart-collections/slug"] = {
|
|
448
|
+
slug: "books",
|
|
449
|
+
available: false,
|
|
450
|
+
};
|
|
451
|
+
const element = mountDialog();
|
|
452
|
+
void element.open({ prefill: { title: "Books" } });
|
|
453
|
+
await settle(element);
|
|
454
|
+
|
|
455
|
+
const save = [...element.querySelectorAll("button")].find(
|
|
456
|
+
(button) => button.textContent?.trim() === "Save",
|
|
457
|
+
);
|
|
458
|
+
expect(save?.disabled).toBe(true);
|
|
459
|
+
|
|
460
|
+
// Enter must not walk around the button it agrees with.
|
|
461
|
+
element
|
|
462
|
+
.querySelector("[data-field='title']")
|
|
463
|
+
?.dispatchEvent(
|
|
464
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
|
465
|
+
);
|
|
466
|
+
await settle(element);
|
|
467
|
+
|
|
468
|
+
expect(
|
|
469
|
+
requests.some(
|
|
470
|
+
(request) =>
|
|
471
|
+
request.method === "POST" && !request.url.includes("/preview"),
|
|
472
|
+
),
|
|
473
|
+
).toBe(false);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("says why it cannot save when Enter is pressed on an empty form", async () => {
|
|
477
|
+
const element = mountDialog();
|
|
478
|
+
void element.open({});
|
|
479
|
+
await settle(element);
|
|
480
|
+
|
|
481
|
+
element
|
|
482
|
+
.querySelector("[data-field='title']")
|
|
483
|
+
?.dispatchEvent(
|
|
484
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
|
485
|
+
);
|
|
486
|
+
await settle(element);
|
|
487
|
+
|
|
488
|
+
// In one line the alert can lay out, not a bare text node in a grid whose
|
|
489
|
+
// first column is zero wide.
|
|
490
|
+
const alert = element.querySelector("[role='alert'] section p");
|
|
491
|
+
expect(alert?.textContent?.trim()).toBe(
|
|
492
|
+
"A smart collection needs a title and a link.",
|
|
493
|
+
);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("drops a complaint the author has already answered", async () => {
|
|
497
|
+
const element = mountDialog();
|
|
498
|
+
void element.open({});
|
|
499
|
+
await settle(element);
|
|
500
|
+
|
|
501
|
+
element
|
|
502
|
+
.querySelector("[data-field='title']")
|
|
503
|
+
?.dispatchEvent(
|
|
504
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
|
505
|
+
);
|
|
506
|
+
await settle(element);
|
|
507
|
+
expect(element.querySelector("[role='alert']")).not.toBeNull();
|
|
508
|
+
|
|
509
|
+
const input = element.querySelector<HTMLInputElement>(
|
|
510
|
+
"[data-field='title']",
|
|
511
|
+
);
|
|
512
|
+
if (input) {
|
|
513
|
+
input.value = "Quotes";
|
|
514
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
515
|
+
}
|
|
516
|
+
await settle(element);
|
|
517
|
+
|
|
518
|
+
expect(element.querySelector("[role='alert']")).toBeNull();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("shows the whole link, folded away until asked", async () => {
|
|
522
|
+
const element = mountDialog();
|
|
523
|
+
void element.open({ prefill: { title: "Quotes" } });
|
|
524
|
+
await settle(element);
|
|
525
|
+
|
|
526
|
+
// Collapsed: the URL and a way in, no second labelled field.
|
|
527
|
+
const preview = element.querySelector(".collection-quick-link-preview");
|
|
528
|
+
expect(preview?.textContent?.trim()).toMatch(/\/quotes$/);
|
|
529
|
+
expect(element.querySelector("[data-field='slug']")).toBeNull();
|
|
530
|
+
|
|
531
|
+
const edit = [...element.querySelectorAll("button")].find(
|
|
532
|
+
(button) => button.textContent?.trim() === "Edit link",
|
|
533
|
+
);
|
|
534
|
+
edit?.click();
|
|
535
|
+
await settle(element);
|
|
536
|
+
|
|
537
|
+
const input = element.querySelector<HTMLInputElement>(
|
|
538
|
+
"[data-field='slug']",
|
|
539
|
+
);
|
|
540
|
+
expect(input?.value).toBe("quotes");
|
|
541
|
+
expect(element.textContent).toContain("Collection link");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("says a link is taken rather than failing on save", async () => {
|
|
545
|
+
responses["/api/smart-collections/slug"] = {
|
|
546
|
+
slug: "books",
|
|
547
|
+
available: false,
|
|
548
|
+
};
|
|
549
|
+
const element = mountDialog();
|
|
550
|
+
void element.open({ prefill: { title: "Books" } });
|
|
551
|
+
await settle(element);
|
|
552
|
+
|
|
553
|
+
expect(element.textContent).toContain(
|
|
554
|
+
"This link is taken. Choose another.",
|
|
555
|
+
);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("names the real reason a link is refused, not always a collision", async () => {
|
|
559
|
+
const element = mountDialog();
|
|
560
|
+
void element.open({ prefill: { title: "Quotes" } });
|
|
561
|
+
await settle(element);
|
|
562
|
+
|
|
563
|
+
[...element.querySelectorAll("button")]
|
|
564
|
+
.find((button) => button.textContent?.trim() === "Edit link")
|
|
565
|
+
?.click();
|
|
566
|
+
await settle(element);
|
|
567
|
+
|
|
568
|
+
const input = element.querySelector<HTMLInputElement>(
|
|
569
|
+
"[data-field='slug']",
|
|
570
|
+
);
|
|
571
|
+
if (input) {
|
|
572
|
+
input.value = "Not A Slug";
|
|
573
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
574
|
+
}
|
|
575
|
+
await settle(element);
|
|
576
|
+
|
|
577
|
+
expect(element.textContent).toContain(
|
|
578
|
+
"Use lowercase letters, numbers, and hyphens only.",
|
|
579
|
+
);
|
|
580
|
+
expect(element.textContent).not.toContain("This link is taken");
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("warns before an existing link moves", async () => {
|
|
584
|
+
responses["/api/smart-collections/smc_1"] = {
|
|
585
|
+
smartCollection: {
|
|
586
|
+
id: "smc_1",
|
|
587
|
+
slug: "quotes",
|
|
588
|
+
title: "Quotes",
|
|
589
|
+
selection: {},
|
|
590
|
+
sort: "newest",
|
|
591
|
+
layout: null,
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
const element = mountDialog();
|
|
595
|
+
void element.open({ smartCollectionId: "smc_1" });
|
|
596
|
+
await settle(element);
|
|
597
|
+
|
|
598
|
+
expect(element.textContent).not.toContain(
|
|
599
|
+
"Changing the link breaks the old one immediately.",
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
[...element.querySelectorAll("button")]
|
|
603
|
+
.find((button) => button.textContent?.trim() === "Edit link")
|
|
604
|
+
?.click();
|
|
605
|
+
await settle(element);
|
|
606
|
+
|
|
607
|
+
const slugInput = element.querySelector<HTMLInputElement>(
|
|
608
|
+
"[data-field='slug']",
|
|
609
|
+
);
|
|
610
|
+
if (slugInput) {
|
|
611
|
+
slugInput.value = "citations";
|
|
612
|
+
slugInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
613
|
+
}
|
|
614
|
+
await settle(element);
|
|
615
|
+
|
|
616
|
+
expect(element.textContent).toContain(
|
|
617
|
+
"Changing the link breaks the old one immediately.",
|
|
618
|
+
);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("says what a smart collection is when creating one, and not when editing", async () => {
|
|
622
|
+
const creating = mountDialog();
|
|
623
|
+
void creating.open({});
|
|
624
|
+
await settle(creating);
|
|
625
|
+
expect(creating.textContent).toContain(
|
|
626
|
+
"Conditions choose what belongs here",
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
responses["/api/smart-collections/smc_1"] = {
|
|
630
|
+
smartCollection: {
|
|
631
|
+
id: "smc_1",
|
|
632
|
+
slug: "quotes",
|
|
633
|
+
title: "Quotes",
|
|
634
|
+
selection: {},
|
|
635
|
+
sort: "newest",
|
|
636
|
+
layout: null,
|
|
637
|
+
},
|
|
638
|
+
};
|
|
639
|
+
const editing = mountDialog();
|
|
640
|
+
void editing.open({ smartCollectionId: "smc_1" });
|
|
641
|
+
await settle(editing);
|
|
642
|
+
expect(editing.textContent).not.toContain(
|
|
643
|
+
"Conditions choose what belongs here",
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it("leaves deleting to the menus that open it", async () => {
|
|
648
|
+
responses["/api/smart-collections/smc_1"] = {
|
|
649
|
+
smartCollection: {
|
|
650
|
+
id: "smc_1",
|
|
651
|
+
slug: "quotes",
|
|
652
|
+
title: "Quotes",
|
|
653
|
+
selection: {},
|
|
654
|
+
sort: "newest",
|
|
655
|
+
layout: null,
|
|
656
|
+
},
|
|
657
|
+
};
|
|
658
|
+
const element = mountDialog();
|
|
659
|
+
void element.open({ smartCollectionId: "smc_1" });
|
|
660
|
+
await settle(element);
|
|
661
|
+
|
|
662
|
+
// Nothing destructive shares a row with Save.
|
|
663
|
+
expect(element.textContent).not.toContain("Delete");
|
|
664
|
+
});
|
|
665
|
+
});
|