@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
|
@@ -97,7 +97,16 @@
|
|
|
97
97
|
{{- $selfUrl := .Permalink -}}
|
|
98
98
|
{{- $now := now.UTC.Format "2006-01-02T15:04:05Z" -}}
|
|
99
99
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
100
|
-
|
|
100
|
+
{{- /* Namespaces ride along only when something in them is emitted, the same
|
|
101
|
+
rule `lib/feed.ts` follows. */ -}}
|
|
102
|
+
{{- $anyMedia := false -}}
|
|
103
|
+
{{- range $posts -}}
|
|
104
|
+
{{- if .Params.media -}}{{- $anyMedia = true -}}{{- end -}}
|
|
105
|
+
{{- range .Pages -}}{{- if .Params.media -}}{{- $anyMedia = true -}}{{- end -}}{{- end -}}
|
|
106
|
+
{{- end -}}
|
|
107
|
+
{{- $jantNs := cond (gt (len $posts) 0) ` xmlns:jant="https://jant.me/ns"` "" -}}
|
|
108
|
+
{{- $mediaNs := cond $anyMedia ` xmlns:media="http://search.yahoo.com/mrss/"` "" -}}
|
|
109
|
+
<feed xmlns="http://www.w3.org/2005/Atom"{{ $jantNs | safeHTMLAttr }}{{ $mediaNs | safeHTMLAttr }}>
|
|
101
110
|
<title>{{ $feedTitle | transform.XMLEscape }}</title>
|
|
102
111
|
<subtitle>{{ $siteDescription | transform.XMLEscape }}</subtitle>
|
|
103
112
|
<link href="{{ $siteBase | transform.XMLEscape }}" rel="alternate"/>
|
|
@@ -129,15 +138,82 @@
|
|
|
129
138
|
{{- $summaryText = or (string (.Params.summary_text | default "")) (.Title | default "") (string (.Params.link_url | default "")) -}}
|
|
130
139
|
{{- end -}}
|
|
131
140
|
{{- if eq $summaryText "" -}}{{- $summaryText = printf "Post %s" (string (.Params.id | default "")) -}}{{- end -}}
|
|
141
|
+
|
|
142
|
+
{{- /* Whether the timeline cut the text of any post the entry's card shows.
|
|
143
|
+
Per post it is written into front matter by the exporter, which shares
|
|
144
|
+
the boundary with the served feed; Hugo's own summary rule would cut
|
|
145
|
+
somewhere else. Per entry the rule is `isEntryTruncated` in
|
|
146
|
+
`lib/feed.ts`: the root plus every reply the fold keeps — the first
|
|
147
|
+
THREAD_LEADING_REPLIES (2) and the last THREAD_TRAILING_REPLIES (3),
|
|
148
|
+
newest included — as `lib/thread-fold.ts` defines it. A thread of five
|
|
149
|
+
replies or fewer is shown whole; `first` and `last` clamp to the slice,
|
|
150
|
+
so the two windows simply overlap there. */ -}}
|
|
151
|
+
{{- $replies := (where $post.Pages "Params.visibility" "public").ByDate -}}
|
|
152
|
+
{{- $truncated := $post.Params.truncated | default false -}}
|
|
153
|
+
{{- range first 2 $replies -}}
|
|
154
|
+
{{- if .Params.truncated -}}{{- $truncated = true -}}{{- end -}}
|
|
155
|
+
{{- end -}}
|
|
156
|
+
{{- range last 3 $replies -}}
|
|
157
|
+
{{- if .Params.truncated -}}{{- $truncated = true -}}{{- end -}}
|
|
158
|
+
{{- end -}}
|
|
159
|
+
|
|
160
|
+
{{- /* Every attachment in the thread, each paired with the post that carries
|
|
161
|
+
it — a text attachment's page lives under its own post. */ -}}
|
|
162
|
+
{{- $entryMedia := slice -}}
|
|
163
|
+
{{- range ($post.Params.media | default slice) -}}
|
|
164
|
+
{{- $entryMedia = $entryMedia | append (dict "m" . "page" $permalink) -}}
|
|
165
|
+
{{- end -}}
|
|
166
|
+
{{- range $replies -}}
|
|
167
|
+
{{- $replyPermalink := .Permalink -}}
|
|
168
|
+
{{- range (.Params.media | default slice) -}}
|
|
169
|
+
{{- $entryMedia = $entryMedia | append (dict "m" . "page" $replyPermalink) -}}
|
|
170
|
+
{{- end -}}
|
|
171
|
+
{{- end -}}
|
|
132
172
|
<entry>
|
|
133
173
|
<title>{{ $entryTitle | transform.XMLEscape }}</title>
|
|
134
174
|
<link href="{{ $alternate | transform.XMLEscape }}" rel="alternate"/>
|
|
135
175
|
{{- if $isLink }}
|
|
136
176
|
<link href="{{ $permalink | transform.XMLEscape }}" rel="related"/>
|
|
137
177
|
{{- end }}
|
|
178
|
+
{{- /* Images carry no enclosure: the content already shows them full size
|
|
179
|
+
inside a link to the original, so one would only ask an attachment
|
|
180
|
+
shelf to list the picture again. */ -}}
|
|
181
|
+
{{- range $entryMedia }}
|
|
182
|
+
{{- $m := .m }}
|
|
183
|
+
{{- if ne (string ($m.kind | default "document")) "image" }}
|
|
184
|
+
<link rel="enclosure" type="{{ (string ($m.mime_type | default "application/octet-stream")) | transform.XMLEscape }}" href="{{ (string $m.src) | absURL | transform.XMLEscape }}"{{ with $m.size }} length="{{ . }}"{{ end }}{{ with $m.original_name }} title="{{ . | transform.XMLEscape }}"{{ end }}/>
|
|
185
|
+
{{- end }}
|
|
186
|
+
{{- end }}
|
|
138
187
|
<id>{{ $permalink | transform.XMLEscape }}</id>
|
|
139
188
|
<published>{{ $published }}</published>
|
|
140
189
|
<updated>{{ $updated }}</updated>
|
|
190
|
+
<jant:format>{{ $format | transform.XMLEscape }}</jant:format>
|
|
191
|
+
{{- if $truncated }}
|
|
192
|
+
<jant:truncated/>
|
|
193
|
+
{{- end }}
|
|
194
|
+
{{- /* A collection is a label the author chose, which is what <category>
|
|
195
|
+
is for. `jant:page` carries the URL because a single collection lives
|
|
196
|
+
in the root URL namespace. */ -}}
|
|
197
|
+
{{- range ($post.Params.collections | default slice) }}
|
|
198
|
+
<category term="{{ (string .slug) | transform.XMLEscape }}" label="{{ (string (.title | default .slug)) | transform.XMLEscape }}" jant:page="{{ (printf "/%s/" (string .slug)) | absURL | transform.XMLEscape }}"/>
|
|
199
|
+
{{- end }}
|
|
200
|
+
{{- range $entryMedia }}
|
|
201
|
+
{{- $m := .m }}
|
|
202
|
+
{{- $kind := string ($m.kind | default "document") }}
|
|
203
|
+
{{- $medium := cond (or (eq $kind "image") (or (eq $kind "video") (eq $kind "audio"))) $kind "document" }}
|
|
204
|
+
{{- $fileUrl := (string $m.src) | absURL }}
|
|
205
|
+
{{- /* Where a click should land. Only emitted where it is not the file:
|
|
206
|
+
a text attachment's markdown would download or dump unstyled. */ -}}
|
|
207
|
+
{{- $pageUrl := $fileUrl }}
|
|
208
|
+
{{- if eq $kind "text" }}{{ $pageUrl = printf "%s/text/%s" (strings.TrimSuffix "/" .page) (string $m.id) }}{{ end }}
|
|
209
|
+
{{- $desc := string ($m.alt | default ($m.summary | default "")) }}
|
|
210
|
+
{{- $poster := string ($m.poster | default "") }}
|
|
211
|
+
<media:content url="{{ $fileUrl | transform.XMLEscape }}" type="{{ (string ($m.mime_type | default "application/octet-stream")) | transform.XMLEscape }}" medium="{{ $medium }}"{{ with $m.size }} fileSize="{{ . }}"{{ end }}{{ with $m.width }} width="{{ . }}"{{ end }}{{ with $m.height }} height="{{ . }}"{{ end }}{{ with $m.duration_seconds }} duration="{{ math.Round . }}"{{ end }}{{ if ne $pageUrl $fileUrl }} jant:page="{{ $pageUrl | transform.XMLEscape }}"{{ end }}>
|
|
212
|
+
{{- with $m.original_name }}<media:title type="plain">{{ . | transform.XMLEscape }}</media:title>{{ end }}
|
|
213
|
+
{{- if ne $desc "" }}<media:description type="plain">{{ $desc | transform.XMLEscape }}</media:description>{{ end }}
|
|
214
|
+
{{- if ne $poster "" }}<media:thumbnail url="{{ $poster | absURL | transform.XMLEscape }}"/>{{ end -}}
|
|
215
|
+
</media:content>
|
|
216
|
+
{{- end }}
|
|
141
217
|
<summary type="text">{{ $summaryText | transform.XMLEscape }}</summary>
|
|
142
218
|
<content type="html"><![CDATA[
|
|
143
219
|
{{- partial "feed-post-content.xml" (dict "post" $post "permalink" $permalink "includeReplies" true) -}}
|
|
@@ -53,8 +53,23 @@
|
|
|
53
53
|
{{- $sourceUrl := string ($post.Params.source_url | default "") -}}
|
|
54
54
|
{{- $cite := "" -}}
|
|
55
55
|
{{- if ne $sourceUrl "" -}}{{- $cite = printf ` cite=%q` $sourceUrl -}}{{- end -}}
|
|
56
|
-
{{-
|
|
57
|
-
|
|
56
|
+
{{- /* Line breaks the author typed must survive as markup: the site
|
|
57
|
+
keeps them with `white-space: pre-line`, but feed readers strip
|
|
58
|
+
CSS. Blank lines split <p> blocks, single newlines become <br/>
|
|
59
|
+
(mirrors `renderPlainTextHtml` in `lib/feed.ts`). */ -}}
|
|
60
|
+
{{- $quoteBlocks := slice -}}
|
|
61
|
+
{{- range split (replaceRE `\r\n?` "\n" $quoteText) "\n\n" -}}
|
|
62
|
+
{{- $para := trim . " \t\n" -}}
|
|
63
|
+
{{- if ne $para "" -}}
|
|
64
|
+
{{- $lines := slice -}}
|
|
65
|
+
{{- range split $para "\n" -}}{{- $lines = $lines | append (transform.XMLEscape .) -}}{{- end -}}
|
|
66
|
+
{{- $quoteBlocks = $quoteBlocks | append (printf "<p>%s</p>" (delimit $lines "<br/>")) -}}
|
|
67
|
+
{{- end -}}
|
|
68
|
+
{{- end -}}
|
|
69
|
+
{{- if gt (len $quoteBlocks) 0 -}}
|
|
70
|
+
{{- $block := printf "<blockquote%s>%s</blockquote>" $cite (delimit $quoteBlocks "\n") -}}
|
|
71
|
+
{{- $parts = $parts | append $block -}}
|
|
72
|
+
{{- end -}}
|
|
58
73
|
{{- /* Attribution line — prefer name, fall back to URL host or the
|
|
59
74
|
raw URL itself (mirrors main site's extractDisplayDomain fallback
|
|
60
75
|
chain in `lib/url.ts`). */ -}}
|
|
@@ -88,6 +103,94 @@
|
|
|
88
103
|
{{- end -}}
|
|
89
104
|
{{- end -}}
|
|
90
105
|
|
|
106
|
+
{{- /* --- Attachments --- */ -}}
|
|
107
|
+
{{- /* One container per post, marked `data-post-media` — the attribute the
|
|
108
|
+
site puts on its gallery strip, so a consumer styles the strip instead of
|
|
109
|
+
reassembling it from the Media RSS elements. In a thread this is also the
|
|
110
|
+
only surface that says which post an attachment belongs to. Mirrors
|
|
111
|
+
`renderMediaItem` in `packages/core/src/lib/feed.ts`. */ -}}
|
|
112
|
+
{{- $mediaList := $post.Params.media | default slice -}}
|
|
113
|
+
{{- if gt (len $mediaList) 0 -}}
|
|
114
|
+
{{- $items := slice -}}
|
|
115
|
+
{{- range $mediaList -}}
|
|
116
|
+
{{- $kind := string (.kind | default "document") -}}
|
|
117
|
+
{{- $mime := string (.mime_type | default "application/octet-stream") -}}
|
|
118
|
+
{{- /* `trim` wraps the index rather than piping into it: a Go template
|
|
119
|
+
pipe binds to the last argument, so `index … 0 | trim " "` would trim
|
|
120
|
+
the index instead of the result. */ -}}
|
|
121
|
+
{{- $mimeLabel := trim (index (split $mime ";") 0) " " -}}
|
|
122
|
+
{{- $src := (string .src) | absURL -}}
|
|
123
|
+
{{- $name := string (.original_name | default "") -}}
|
|
124
|
+
{{- $alt := string (.alt | default "") -}}
|
|
125
|
+
{{- /* Duration and size, in the shapes `formatFeedDuration` and
|
|
126
|
+
`formatFeedBytes` produce. */ -}}
|
|
127
|
+
{{- $metaParts := slice -}}
|
|
128
|
+
{{- with .duration_seconds -}}
|
|
129
|
+
{{- $total := int (math.Round .) -}}
|
|
130
|
+
{{- if gt $total 0 -}}
|
|
131
|
+
{{- $metaParts = $metaParts | append (printf "%d:%02d" (div $total 60) (mod $total 60)) -}}
|
|
132
|
+
{{- end -}}
|
|
133
|
+
{{- end -}}
|
|
134
|
+
{{- with .size -}}
|
|
135
|
+
{{- $b := int . -}}
|
|
136
|
+
{{- if gt $b 0 -}}
|
|
137
|
+
{{- if lt $b 1024 -}}
|
|
138
|
+
{{- $metaParts = $metaParts | append (printf "%d B" $b) -}}
|
|
139
|
+
{{- else if lt $b 1048576 -}}
|
|
140
|
+
{{- $metaParts = $metaParts | append (printf "%d KB" (int (math.Round (div (float $b) 1024.0)))) -}}
|
|
141
|
+
{{- else -}}
|
|
142
|
+
{{- $metaParts = $metaParts | append (printf "%.1f MB" (div (float $b) 1048576.0)) -}}
|
|
143
|
+
{{- end -}}
|
|
144
|
+
{{- end -}}
|
|
145
|
+
{{- end -}}
|
|
146
|
+
{{- $meta := delimit $metaParts " · " -}}
|
|
147
|
+
{{- $metaSuffix := "" -}}
|
|
148
|
+
{{- if ne $meta "" -}}{{- $metaSuffix = printf " (%s)" (transform.XMLEscape $meta) -}}{{- end -}}
|
|
149
|
+
{{- $dims := "" -}}
|
|
150
|
+
{{- if and .width .height -}}{{- $dims = printf ` width="%d" height="%d"` (int .width) (int .height) -}}{{- end -}}
|
|
151
|
+
|
|
152
|
+
{{- if eq $kind "image" -}}
|
|
153
|
+
{{- $caption := "" -}}
|
|
154
|
+
{{- if ne $alt "" -}}{{- $caption = printf "<figcaption>%s</figcaption>" (transform.XMLEscape $alt) -}}{{- end -}}
|
|
155
|
+
{{- /* `%q` is Go string quoting, not attribute escaping: a `"` or `\`
|
|
156
|
+
in the alt would leave the attribute malformed. URLs go through
|
|
157
|
+
`%q` because they never carry either; author text goes through
|
|
158
|
+
`transform.XMLEscape`, the same as the caption beside it. */ -}}
|
|
159
|
+
{{- $items = $items | append (printf `<figure><a href=%q><img src=%q alt="%s"%s/></a>%s</figure>` $src $src (transform.XMLEscape $alt) $dims $caption) -}}
|
|
160
|
+
{{- else if eq $kind "video" -}}
|
|
161
|
+
{{- /* A poster is written only when there is a real still; the pipeline
|
|
162
|
+
leaves nothing usable otherwise, and `<img src="clip.mp4">` was the
|
|
163
|
+
bug that taught us. The link sits outside the <video> so it survives
|
|
164
|
+
a sanitizer that drops the player with its children, and
|
|
165
|
+
`preload="none"` keeps a reader from pulling the whole file. */ -}}
|
|
166
|
+
{{- $posterAttr := "" -}}
|
|
167
|
+
{{- with .poster -}}{{- $posterAttr = printf ` poster=%q` ((string .) | absURL) -}}{{- end -}}
|
|
168
|
+
{{- $altSuffix := "" -}}
|
|
169
|
+
{{- if ne $alt "" -}}{{- $altSuffix = printf ": %s" (transform.XMLEscape $alt) -}}{{- end -}}
|
|
170
|
+
{{- $items = $items | append (printf `<figure><video controls preload="none"%s%s><source src=%q type=%q/></video><figcaption><a href=%q>▶ Watch video</a>%s%s</figcaption></figure>` $posterAttr $dims $src $mimeLabel $src $metaSuffix $altSuffix) -}}
|
|
171
|
+
{{- else if eq $kind "text" -}}
|
|
172
|
+
{{- /* Markdown a browser would download or dump unstyled, so the link
|
|
173
|
+
goes to the page that renders it. */ -}}
|
|
174
|
+
{{- $href := printf "%s/text/%s" (strings.TrimSuffix "/" $permalink) (string .id) -}}
|
|
175
|
+
{{- $label := printf "📎 [%s] %s" (transform.XMLEscape $mimeLabel) (transform.XMLEscape (or $name "Attached text")) -}}
|
|
176
|
+
{{- $textMeta := $metaSuffix -}}
|
|
177
|
+
{{- with .chars -}}
|
|
178
|
+
{{- if gt (int .) 0 -}}{{- $textMeta = printf " (%d chars)" (int .) -}}{{- end -}}
|
|
179
|
+
{{- end -}}
|
|
180
|
+
{{- $summarySuffix := "" -}}
|
|
181
|
+
{{- with .summary -}}
|
|
182
|
+
{{- if ne (string .) "" -}}{{- $summarySuffix = printf ": %s" (transform.XMLEscape (string .)) -}}{{- end -}}
|
|
183
|
+
{{- end -}}
|
|
184
|
+
{{- $items = $items | append (printf `<p><a href=%q>%s</a>%s%s</p>` $href $label $textMeta $summarySuffix) -}}
|
|
185
|
+
{{- else -}}
|
|
186
|
+
{{- $fallbackName := cond (eq $kind "audio") "Audio" "Attachment" -}}
|
|
187
|
+
{{- $label := printf "📎 [%s] %s" (transform.XMLEscape $mimeLabel) (transform.XMLEscape (or $name $fallbackName)) -}}
|
|
188
|
+
{{- $items = $items | append (printf `<p><a href=%q>%s</a>%s</p>` $src $label $metaSuffix) -}}
|
|
189
|
+
{{- end -}}
|
|
190
|
+
{{- end -}}
|
|
191
|
+
{{- $parts = $parts | append (printf "<div data-post-media>\n%s\n</div>" (delimit $items "\n")) -}}
|
|
192
|
+
{{- end -}}
|
|
193
|
+
|
|
91
194
|
{{- /* --- Star rating --- */ -}}
|
|
92
195
|
{{- with $post.Params.rating -}}
|
|
93
196
|
{{- $r := int . -}}
|
|
@@ -8,10 +8,14 @@
|
|
|
8
8
|
`<jant-media-lightbox>` delegates clicks on those anchors.
|
|
9
9
|
|
|
10
10
|
Layout: mirrors MediaGallery.tsx — one horizontal row with a computed
|
|
11
|
-
`rowHeight`
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
`rowHeight` so mixed sizes align into a clean strip. The opening visual
|
|
12
|
+
leads (320px / its ratio), the widest visual is held to 520px so a landscape
|
|
13
|
+
video never swallows the column, and the result is clamped to 240–384px —
|
|
14
|
+
384 being the same 24rem cap a lone visual gets, so a second attachment
|
|
15
|
+
never makes the first picture grow. Non-visual attachments (audio/doc/text)
|
|
16
|
+
do not enter that math; their 3:4 portrait cards are sized from its result.
|
|
17
|
+
Single-visual posts skip the strip and expand to their natural aspect ratio
|
|
18
|
+
(capped at 24rem / 85% width).
|
|
15
19
|
|
|
16
20
|
Inline styles (not Tailwind utilities) because the exported theme ships
|
|
17
21
|
no utility CSS — only tokens.css + main.css + theme.css + site-media.css.
|
|
@@ -27,8 +31,7 @@
|
|
|
27
31
|
{{- $items := slice -}}
|
|
28
32
|
{{- $lightbox := slice -}}
|
|
29
33
|
{{- $lbIdx := 0 -}}
|
|
30
|
-
{{- $
|
|
31
|
-
{{- $firstRatio := 0.0 -}}
|
|
34
|
+
{{- $visualRatios := slice -}}
|
|
32
35
|
{{- range $media -}}
|
|
33
36
|
{{- $kind := .kind | default "file" -}}
|
|
34
37
|
{{- $item := . -}}
|
|
@@ -46,13 +49,15 @@
|
|
|
46
49
|
{{- $lightbox = $lightbox | append $lbEntry -}}
|
|
47
50
|
{{- $items = $items | append (merge $item (dict "_kind" $kind "_lbIdx" $lbIdx)) -}}
|
|
48
51
|
{{- $lbIdx = add $lbIdx 1 -}}
|
|
49
|
-
{{- /*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
{{- /* Every visual's aspect ratio, in display order — 4:3 when unknown,
|
|
53
|
+
matching getMediaAspectRatio() in MediaGallery.tsx. */ -}}
|
|
54
|
+
{{- $ratio := 1.3333 -}}
|
|
55
|
+
{{- if and $item.width $item.height (gt (float $item.width) 0.0) (gt (float $item.height) 0.0) -}}
|
|
56
|
+
{{- $ratio = div (float $item.width) (float $item.height) -}}
|
|
52
57
|
{{- end -}}
|
|
58
|
+
{{- $visualRatios = $visualRatios | append $ratio -}}
|
|
53
59
|
{{- else -}}
|
|
54
60
|
{{- $items = $items | append (merge $item (dict "_kind" $kind)) -}}
|
|
55
|
-
{{- $hasNonVisual = true -}}
|
|
56
61
|
{{- end -}}
|
|
57
62
|
{{- end -}}
|
|
58
63
|
|
|
@@ -62,14 +67,19 @@
|
|
|
62
67
|
{{- $firstKind := $first._kind -}}
|
|
63
68
|
{{- $singleVisual := and $single (or (eq $firstKind "image") (eq $firstKind "video")) -}}
|
|
64
69
|
|
|
65
|
-
{{- /* rowHeight — mirrors MediaGallery.tsx.
|
|
66
|
-
{{- $rowMin :=
|
|
67
|
-
{{- $rowMax :=
|
|
68
|
-
{{- $rowHeight :=
|
|
69
|
-
{{- if
|
|
70
|
-
{{- $
|
|
71
|
-
{{-
|
|
72
|
-
|
|
70
|
+
{{- /* rowHeight — mirrors getGalleryRowHeight() in MediaGallery.tsx. */ -}}
|
|
71
|
+
{{- $rowMin := 240 -}}
|
|
72
|
+
{{- $rowMax := 384 -}}
|
|
73
|
+
{{- $rowHeight := $rowMin -}}
|
|
74
|
+
{{- if $visualRatios -}}
|
|
75
|
+
{{- $widest := 0.0 -}}
|
|
76
|
+
{{- range $visualRatios -}}
|
|
77
|
+
{{- if gt . $widest }}{{ $widest = . }}{{ end -}}
|
|
78
|
+
{{- end -}}
|
|
79
|
+
{{- $byLead := div 320.0 (index $visualRatios 0) -}}
|
|
80
|
+
{{- $byWidestItem := div 520.0 $widest -}}
|
|
81
|
+
{{- $computed := math.Round (cond (lt $byWidestItem $byLead) $byWidestItem $byLead) -}}
|
|
82
|
+
{{- $rowHeight = cond (gt $computed (float $rowMax)) $rowMax (cond (lt $computed (float $rowMin)) $rowMin (int $computed)) -}}
|
|
73
83
|
{{- end -}}
|
|
74
84
|
{{- $docCardWidth := int (math.Round (mul (float $rowHeight) 0.75)) -}}
|
|
75
85
|
|
|
@@ -80,7 +90,7 @@
|
|
|
80
90
|
<div data-post-media
|
|
81
91
|
{{- if $lightbox }} data-lightbox-group="{{ $lightbox | jsonify }}"{{- end -}}
|
|
82
92
|
{{- if not $singleVisual }} tabindex="0" role="group" aria-label="Media gallery"{{- end -}}
|
|
83
|
-
style="display: flex; gap: 0.
|
|
93
|
+
style="display: flex; gap: 0.75rem;
|
|
84
94
|
{{- if not $singleVisual }} overflow-x: auto; scroll-behavior: smooth; scrollbar-width: none; -ms-overflow-style: none;{{- end }}">
|
|
85
95
|
{{- range $items -}}
|
|
86
96
|
{{- $k := ._kind -}}
|
|
@@ -130,10 +140,14 @@
|
|
|
130
140
|
{{- else -}}
|
|
131
141
|
{{- $videoWrapStyle = printf "%s display: block; flex-shrink: 0; width: %dpx; max-width: 85%%;" $videoWrapStyle $itemWidth -}}
|
|
132
142
|
{{- end -}}
|
|
143
|
+
{{- /* Fit travels with the size, as in MediaGallery.tsx, and
|
|
144
|
+
site-media.css sets none: a clip in a row is cropped to the
|
|
145
|
+
row's height like the pictures beside it; a lone clip is
|
|
146
|
+
letterboxed in case its frames disagree with the stored size. */ -}}
|
|
133
147
|
{{- $videoStyle := cond $singleVisual
|
|
134
148
|
(cond (and (gt (int (default 0 .width)) 0) (gt (int (default 0 .height)) 0))
|
|
135
|
-
(printf "display: block; width: 100%%; height: auto; aspect-ratio: %d/%d;" (int .width) (int .height))
|
|
136
|
-
"display: block; width: 100%; height: auto;")
|
|
149
|
+
(printf "display: block; width: 100%%; height: auto; aspect-ratio: %d/%d; object-fit: contain;" (int .width) (int .height))
|
|
150
|
+
"display: block; width: 100%; height: auto; object-fit: contain;")
|
|
137
151
|
(printf "display: block; width: 100%%; height: %dpx; object-fit: cover;" $rowHeight) -}}
|
|
138
152
|
{{- if $isShort -}}
|
|
139
153
|
<div class="media-video-wrap media-video-wrap-short" style="{{ $videoWrapStyle | safeCSS }}">
|
|
@@ -2,26 +2,27 @@
|
|
|
2
2
|
{{- $total := $paginator.TotalPages -}}
|
|
3
3
|
{{- $current := $paginator.PageNumber -}}
|
|
4
4
|
{{- if gt $total 1 -}}
|
|
5
|
-
{{- /* Mirror main site's getPageNumbers:
|
|
6
|
-
|
|
5
|
+
{{- /* Mirror main site's getPageNumbers: past seven pages, always seven
|
|
6
|
+
slots — 1, last, and a three-page window around the current page.
|
|
7
|
+
The second and second-to-last slots hold a page or 0 (an ellipsis
|
|
8
|
+
marker), so an ellipsis never stands in for a single page. */ -}}
|
|
7
9
|
{{- $pages := slice -}}
|
|
8
10
|
{{- if le $total 7 -}}
|
|
9
11
|
{{- range seq 1 $total -}}
|
|
10
12
|
{{- $pages = $pages | append . -}}
|
|
11
13
|
{{- end -}}
|
|
12
14
|
{{- else -}}
|
|
13
|
-
{{- $
|
|
14
|
-
{{- if
|
|
15
|
-
{{- if
|
|
16
|
-
{{- $
|
|
17
|
-
{{- $
|
|
18
|
-
{{-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
{{- end -}}
|
|
22
|
-
{{- $pages = $pages | append $p -}}
|
|
23
|
-
{{- $prev = $p -}}
|
|
15
|
+
{{- $start := sub $current 1 -}}
|
|
16
|
+
{{- if lt $start 3 -}}{{- $start = 3 -}}{{- end -}}
|
|
17
|
+
{{- if gt $start (sub $total 4) -}}{{- $start = sub $total 4 -}}{{- end -}}
|
|
18
|
+
{{- $end := add $start 2 -}}
|
|
19
|
+
{{- $pages = $pages | append 1 -}}
|
|
20
|
+
{{- $pages = $pages | append (cond (eq $start 3) 2 0) -}}
|
|
21
|
+
{{- range seq $start $end -}}
|
|
22
|
+
{{- $pages = $pages | append . -}}
|
|
24
23
|
{{- end -}}
|
|
24
|
+
{{- $pages = $pages | append (cond (eq $end (sub $total 2)) (sub $total 1) 0) -}}
|
|
25
|
+
{{- $pages = $pages | append $total -}}
|
|
25
26
|
{{- end -}}
|
|
26
27
|
<nav class="pagination" aria-label="Pagination">
|
|
27
28
|
{{- if $paginator.HasPrev -}}
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
{{- $collections := $post.Params.collections | default slice -}}
|
|
6
6
|
{{- $slug := $post.Params.slug | default $post.Slug -}}
|
|
7
7
|
{{- $linkUrl := $post.Params.link_url -}}
|
|
8
|
+
{{- /* A reply is not rendered as a page (`build: render: never`), so its
|
|
9
|
+
`.RelPermalink` is empty. The exporter registers every reply slug as an
|
|
10
|
+
alias on the root, and that alias is the address a reply has. */ -}}
|
|
11
|
+
{{- $permalink := $post.RelPermalink | default (printf "/%s/" $slug | relURL) -}}
|
|
8
12
|
<article class="post-card post-card-{{ $format }}" data-format="{{ $format }}" data-slug="{{ $slug }}"
|
|
9
13
|
{{- if $post.Params.pinned_at }} data-post-pinned{{ end -}}
|
|
10
14
|
{{- if $post.Params.featured_at }} data-post-featured{{ end -}}
|
|
@@ -27,7 +31,7 @@
|
|
|
27
31
|
<a href="{{ $linkUrl }}" rel="noopener noreferrer" target="_blank">{{ $post.Title }}</a>
|
|
28
32
|
</h2>
|
|
29
33
|
{{- else if $post.Title -}}
|
|
30
|
-
<h2 class="post-card-title post-card-link-title"><a href="{{ $
|
|
34
|
+
<h2 class="post-card-title post-card-link-title"><a href="{{ $permalink }}">{{ $post.Title }}</a></h2>
|
|
31
35
|
{{- end -}}
|
|
32
36
|
{{- /* Prefer Hugo's rendered HTML body — preserves paragraphs, links,
|
|
33
37
|
emphasis, lists. Falls back to the plain-text `summary_text` stored
|
|
@@ -73,7 +77,7 @@
|
|
|
73
77
|
{{- end -}}
|
|
74
78
|
{{- else -}}
|
|
75
79
|
{{- with $post.Title -}}
|
|
76
|
-
<h2 class="post-card-title"><a href="{{ $
|
|
80
|
+
<h2 class="post-card-title"><a href="{{ $permalink }}">{{ . }}</a></h2>
|
|
77
81
|
{{- end -}}
|
|
78
82
|
{{- /* Prefer Hugo's rendered HTML summary — preserves paragraphs, links,
|
|
79
83
|
emphasis, lists. Falls back to the plain-text `summary_text` stored
|
|
@@ -85,7 +89,7 @@
|
|
|
85
89
|
{{- end -}}
|
|
86
90
|
{{- end -}}
|
|
87
91
|
|
|
88
|
-
{{ partial "media-gallery.html" (dict "media" $media "permalink" $
|
|
92
|
+
{{ partial "media-gallery.html" (dict "media" $media "permalink" $permalink) }}
|
|
89
93
|
|
|
90
94
|
<footer class="post-card-footer post-menu-footer" data-post-meta>
|
|
91
95
|
<div class="post-footer-meta">
|
|
@@ -96,7 +100,7 @@
|
|
|
96
100
|
</svg>
|
|
97
101
|
</span>
|
|
98
102
|
{{- end -}}
|
|
99
|
-
<a class="post-footer-link post-card-permalink" href="{{ $
|
|
103
|
+
<a class="post-footer-link post-card-permalink" href="{{ $permalink }}">
|
|
100
104
|
<time datetime="{{ $post.Date.Format "2006-01-02T15:04:05Z07:00" }}">
|
|
101
105
|
{{ $post.Date.Format "Jan 2, 2006" }}
|
|
102
106
|
</time>
|
|
@@ -69,7 +69,8 @@
|
|
|
69
69
|
</div>
|
|
70
70
|
{{- end -}}
|
|
71
71
|
|
|
72
|
-
{{
|
|
72
|
+
{{- /* Replies are not rendered as pages, so the address is the reply's alias on the root. */ -}}
|
|
73
|
+
{{ partial "media-gallery.html" (dict "media" $media "permalink" ($reply.RelPermalink | default (printf "/%s/" $slug | relURL))) }}
|
|
73
74
|
|
|
74
75
|
<footer class="reply-footer post-menu-footer" data-post-meta>
|
|
75
76
|
<div class="post-footer-meta">
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
{{- /*
|
|
2
2
|
Thread preview partial.
|
|
3
3
|
|
|
4
|
-
Matches the main site's `ThreadPreview` component
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
Matches the main site's `ThreadPreview` component, and the fold both it and
|
|
5
|
+
the feed take from `lib/thread-fold.ts` (`foldThreadReplies`):
|
|
6
|
+
root post (full card, as "context")
|
|
7
|
+
the first THREAD_LEADING_REPLIES (2) (full cards, as "context")
|
|
8
|
+
"N more posts" gap pill ← only when the fold hides any
|
|
9
|
+
the two before the newest reply (full cards, as "context")
|
|
10
|
+
newest reply (full card, as "hero")
|
|
11
|
+
|
|
12
|
+
The trailing window is THREAD_TRAILING_REPLIES (3) with the hero inside it.
|
|
13
|
+
On a short thread the windows overlap, so each list drops whatever an
|
|
14
|
+
earlier one already shows, hero first — the same order the site dedupes in.
|
|
10
15
|
|
|
11
16
|
Each entry is wrapped in a `.thread-item` so the rail dot markers
|
|
12
17
|
line up the same way the main site does via `.thread-item::before`.
|
|
@@ -20,29 +25,34 @@
|
|
|
20
25
|
{{- if eq $replyCount 0 -}}
|
|
21
26
|
{{ partial "post-card.html" $root }}
|
|
22
27
|
{{- else -}}
|
|
23
|
-
{{- /* First-in / last-in replies, with overlap handled below. */ -}}
|
|
24
28
|
{{- $latest := index $replies (sub $replyCount 1) -}}
|
|
25
|
-
{{- $
|
|
26
|
-
{{-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
{{-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
{{- $leading := slice -}}
|
|
30
|
+
{{- range first 2 $replies -}}
|
|
31
|
+
{{- if ne .File.UniqueID $latest.File.UniqueID -}}
|
|
32
|
+
{{- $leading = $leading | append . -}}
|
|
33
|
+
{{- end -}}
|
|
34
|
+
{{- end -}}
|
|
35
|
+
{{- $trailing := slice -}}
|
|
36
|
+
{{- range last 3 $replies -}}
|
|
37
|
+
{{- $reply := . -}}
|
|
38
|
+
{{- $shown := eq $reply.File.UniqueID $latest.File.UniqueID -}}
|
|
39
|
+
{{- range $leading -}}
|
|
40
|
+
{{- if eq .File.UniqueID $reply.File.UniqueID -}}{{- $shown = true -}}{{- end -}}
|
|
33
41
|
{{- end -}}
|
|
42
|
+
{{- if not $shown -}}{{- $trailing = $trailing | append $reply -}}{{- end -}}
|
|
34
43
|
{{- end -}}
|
|
35
44
|
|
|
36
|
-
{{- /*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{{-
|
|
45
|
+
{{- /* Whatever neither window shows is the hidden middle. It is never
|
|
46
|
+
negative: the three lists are disjoint and all drawn from $replies. */ -}}
|
|
47
|
+
{{- $hiddenCount := sub $replyCount (add (add (len $leading) (len $trailing)) 1) -}}
|
|
48
|
+
{{- /* The gap leads to the first hidden reply, as the site's does. A fold
|
|
49
|
+
only hides anything once both windows are full, so that reply sits
|
|
50
|
+
right after the leading window. Replies are not rendered as pages
|
|
51
|
+
(`build: render: never`), so their `.RelPermalink` is empty; the
|
|
52
|
+
exporter registers every reply slug as an alias on the root, and
|
|
53
|
+
that is the address a reply has. */ -}}
|
|
54
|
+
{{- $gapHref := $root.RelPermalink -}}
|
|
55
|
+
{{- if gt $hiddenCount 0 -}}{{- $gapHref = printf "/%s/" (index $replies 2).Slug | relURL -}}{{- end -}}
|
|
46
56
|
|
|
47
57
|
{{- $slug := $root.Params.slug | default $root.Slug -}}
|
|
48
58
|
<article class="thread-preview" data-slug="{{ $slug }}" data-reply-count="{{ $replyCount }}">
|
|
@@ -51,23 +61,23 @@
|
|
|
51
61
|
{{ partial "post-card.html" $root }}
|
|
52
62
|
</div>
|
|
53
63
|
|
|
54
|
-
{{-
|
|
64
|
+
{{- range $leading -}}
|
|
55
65
|
<div class="thread-item thread-item-context">
|
|
56
|
-
{{ partial "post-card.html"
|
|
66
|
+
{{ partial "post-card.html" . }}
|
|
57
67
|
</div>
|
|
58
68
|
{{- end -}}
|
|
59
69
|
|
|
60
70
|
{{- if gt $hiddenCount 0 -}}
|
|
61
71
|
<div class="thread-item thread-item-gap">
|
|
62
|
-
<a class="thread-preview-gap" href="{{ $
|
|
72
|
+
<a class="thread-preview-gap" href="{{ $gapHref }}">
|
|
63
73
|
{{ $hiddenCount }} more {{ if eq $hiddenCount 1 }}post{{ else }}posts{{ end }}
|
|
64
74
|
</a>
|
|
65
75
|
</div>
|
|
66
76
|
{{- end -}}
|
|
67
77
|
|
|
68
|
-
{{-
|
|
78
|
+
{{- range $trailing -}}
|
|
69
79
|
<div class="thread-item thread-item-context">
|
|
70
|
-
{{ partial "post-card.html"
|
|
80
|
+
{{ partial "post-card.html" . }}
|
|
71
81
|
</div>
|
|
72
82
|
{{- end -}}
|
|
73
83
|
</div>
|
|
@@ -36,6 +36,13 @@ body {
|
|
|
36
36
|
background-color: var(--site-page-bg);
|
|
37
37
|
-webkit-font-smoothing: antialiased;
|
|
38
38
|
-moz-osx-font-smoothing: grayscale;
|
|
39
|
+
/* Every text node in an exported page is authored content, and authored text
|
|
40
|
+
can contain a run with no break opportunity — a long URL, an ID, a word
|
|
41
|
+
typed without spaces. Such a run does not wrap on its own and spills past
|
|
42
|
+
the content column across the page. `break-word` splits it only when it
|
|
43
|
+
cannot fit a line by itself, so ordinary text keeps its natural line breaks
|
|
44
|
+
and intrinsic sizing is unaffected. */
|
|
45
|
+
overflow-wrap: break-word;
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
img,
|
|
@@ -1067,6 +1074,11 @@ hr.feed-divider {
|
|
|
1067
1074
|
align-items: center;
|
|
1068
1075
|
gap: 0.45rem;
|
|
1069
1076
|
flex-wrap: wrap;
|
|
1077
|
+
/* The name and source are flex items, which refuse to shrink below their
|
|
1078
|
+
min-content width. The inherited `break-word` leaves min-content at the
|
|
1079
|
+
longest word and the item still overflows; `anywhere` also shrinks
|
|
1080
|
+
min-content, which is what lets the row wrap. */
|
|
1081
|
+
overflow-wrap: anywhere;
|
|
1070
1082
|
margin-top: 0.95rem;
|
|
1071
1083
|
color: var(--site-text-secondary);
|
|
1072
1084
|
font-family: var(--font-ui);
|
|
@@ -1998,7 +2010,7 @@ hr.feed-divider {
|
|
|
1998
2010
|
.pagination {
|
|
1999
2011
|
display: flex;
|
|
2000
2012
|
align-items: center;
|
|
2001
|
-
justify-content:
|
|
2013
|
+
justify-content: flex-start;
|
|
2002
2014
|
flex-wrap: wrap;
|
|
2003
2015
|
gap: 1rem;
|
|
2004
2016
|
padding: 1.5rem 0;
|