@jant/core 0.6.15 → 0.6.16
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/bin/commands/import-site.js +7 -0
- package/dist/app-C75Zc8dD.js +6 -0
- package/dist/{app-C-oi_t8W.js → app-C7_4oab9.js} +480 -196
- package/dist/client/.vite/manifest.json +8 -8
- package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
- package/dist/client/_assets/{client-DxY5BFe8.js → client-Bq0vre8Y.js} +3 -3
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/{client-auth-CakPESv9.js → client-auth-F2rb9WfF.js} +1015 -977
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-CsFx6F_M.js → export-Dl5KCiEs.js} +93 -7
- package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
- package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
- package/dist/{github-sync-ppWXN3jb.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Cq1pzzOI.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +6 -6
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- package/package.json +1 -1
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/client/__tests__/compose-bridge.test.ts +252 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
- package/src/client/__tests__/toast.test.ts +65 -1
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2015 -318
- package/src/client/components/__tests__/jant-compose-editor.test.ts +53 -62
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +35 -89
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +47 -3
- package/src/client/components/jant-command-palette.ts +12 -6
- package/src/client/components/jant-compose-dialog.ts +1464 -680
- package/src/client/components/jant-compose-editor.ts +225 -93
- package/src/client/components/jant-compose-fullscreen.ts +3 -5
- package/src/client/components/jant-post-menu.ts +91 -21
- package/src/client/compose-bridge.ts +143 -122
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -1
- package/src/client/post-refresh.ts +135 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/toast.ts +130 -38
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +18 -0
- package/src/db/schema.ts +20 -0
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/locales/public/en.po +89 -21
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +88 -20
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +88 -20
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/lib/__tests__/feed.test.ts +168 -0
- package/src/lib/__tests__/markdown.test.ts +16 -0
- package/src/lib/__tests__/slug.test.ts +18 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -0
- package/src/lib/feed.ts +113 -11
- package/src/lib/hugo-markdown.ts +5 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- package/src/lib/post-display.ts +42 -9
- package/src/lib/timeline.ts +39 -9
- package/src/lib/tiptap-render.ts +4 -2
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +28 -0
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/posts.ts +13 -9
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/posts.ts +7 -0
- package/src/routes/compose.tsx +6 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/pages/__tests__/archive-params.test.ts +292 -0
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- package/src/routes/pages/archive.tsx +65 -17
- package/src/routes/pages/page.tsx +5 -2
- package/src/services/__tests__/collection.test.ts +32 -1
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +277 -0
- package/src/services/collection.ts +8 -10
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export.ts +3 -5
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +297 -100
- package/src/services/search.ts +9 -0
- package/src/styles/components.css +0 -14
- package/src/styles/site-media.css +4 -4
- package/src/styles/tokens.css +62 -1
- package/src/styles/ui.css +1363 -1283
- package/src/types/entities.ts +5 -0
- package/src/types/props.ts +12 -0
- package/src/types/views.ts +6 -0
- package/src/ui/compose/ComposeDialog.tsx +17 -17
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- package/src/ui/pages/ArchivePage.tsx +131 -24
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +63 -0
- package/src/ui/shared/DraftPreviewBar.tsx +25 -9
- package/src/ui/shared/PostFooter.tsx +35 -12
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/post-article-attributes.ts +6 -0
- package/dist/app-BxCOR3Uc.js +0 -6
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { _ as
|
|
2
|
-
import { $ as
|
|
3
|
-
import { S as getTelegramWebhookSecret, T as coalesceDisplayText, _ as getInternalAdminToken, a as getConfiguredSingleSiteUrl, b as getSiteResolutionMode, c as getDevApiToken, d as getHostedControlPlaneBaseUrl, f as getHostedControlPlaneDomainCheckSecret, g as getHostedControlPlaneSsoSecret, h as getHostedControlPlaneProviderLabel$1, i as getConfiguredSingleSitePathPrefix, l as getEnvString, m as getHostedControlPlaneInternalToken, n as getAuthSecret, o as getConfiguredStorageDriver, p as getHostedControlPlaneInternalBaseUrl, r as getConfiguredSingleSiteOrigin, s as getCorsOrigins, u as getGitHubAppConfig, v as getLocalStoragePath, w as shouldUseSecureCookies, x as getTelegramBotPool } from "./env-
|
|
4
|
-
import { l as markdownToTiptapJson, o as createGitHubSyncService } from "./github-sync-
|
|
5
|
-
import { a as listInstallationReposPage, n as getInstallation, o as searchInstallationRepos, t as buildInstallUrl } from "./github-app-
|
|
6
|
-
import { r as parseRepoSlug, t as createGitHubClient } from "./github-api-
|
|
1
|
+
import { _ as toPublicHref, a as getSitePathPrefix, c as normalizePath, f as sanitizeUrl, g as toAbsoluteSiteUrl, h as toAbsoluteAssetUrl, i as getSiteOrigin, l as normalizeSitePathPrefix, m as stripSitePathPrefix, n as extractDisplayDomain, o as isFullUrl, p as slugify, r as extractDomain, s as isSafeInternalRedirect, t as buildSiteUrl, u as normalizeSiteUrl, v as toPublicPath, x as __exportAll, y as toSameSitePath } from "./url-CbLAtlZe.js";
|
|
2
|
+
import { $ as JANT_BRAND_PACK_FILENAME, A as COLLECTION_FRESHNESS_WINDOW_SECONDS, B as NAV_ITEM_TYPES$2, C as formatTime, D as toISOString, G as SYSTEM_NAV_KEY_VALUES, H as STATUSES$2, J as getImageUrl, K as TEXT_ATTACHMENT_CONTENT_FORMATS, L as MAX_SITE_DESCRIPTION_LENGTH, M as DEFAULT_NAVIGATION_PROFILE, N as FORMATS$2, O as CONFIG_FIELDS, P as MAX_COLLECTION_DESCRIPTION_LENGTH, Q as HOME_BRANDING_PREFIX, R as MAX_SITE_FOOTER_LENGTH, S as formatRelativeTime, T as now, U as STORAGE_DRIVERS, V as SORT_ORDERS, W as SYSTEM_NAV_KEYS, X as getPublicUrlForProvider, Y as getMediaUrl, Z as HOME_BRANDING_LINK_LABEL, _ as trimTiptapBody, a as rootActivityColumns, at as getJantBundledAsset, b as formatDate, c as render, ct as getJantLogoFilename, d as extractBodyText, dt as getJantPositiveLogoPngHref, et as JANT_HOME_URL, f as extractSummary, ft as JANT_LOGO_PATH_DATA, g as renderTiptapJson, h as renderTiptapDocumentAroundBoundary, ht as base64ToUint8Array, i as buildRootActivityExpr, it as getJantBrandPackHref, j as COLLECTION_SORT_ORDERS$2, k as THEME_MODES, l as toPlainText, lt as getJantLogoFills, m as renderTiptapDocument, mt as arrayBufferToBase64, nt as getDefaultJantAppleTouchIconBytes, o as tiptapJsonToMarkdown, ot as getJantIconFilename, p as extractSummaryHtml, pt as JANT_LOGO_VIEW_BOX, q as VISIBILITIES$2, rt as getDefaultJantFaviconIcoBytes, st as getJantIconHref, t as createExportService, tt as JANT_POSITIVE_LOGO_PNG_FILENAME, ut as getJantLogoHref, v as escapeHtml, w as formatYearMonth, x as formatRelativeAge, y as upgradeLegacyFootnotes, z as MEDIA_KINDS } from "./export-Dl5KCiEs.js";
|
|
3
|
+
import { S as getTelegramWebhookSecret, T as coalesceDisplayText, _ as getInternalAdminToken, a as getConfiguredSingleSiteUrl, b as getSiteResolutionMode, c as getDevApiToken, d as getHostedControlPlaneBaseUrl, f as getHostedControlPlaneDomainCheckSecret, g as getHostedControlPlaneSsoSecret, h as getHostedControlPlaneProviderLabel$1, i as getConfiguredSingleSitePathPrefix, l as getEnvString, m as getHostedControlPlaneInternalToken, n as getAuthSecret, o as getConfiguredStorageDriver, p as getHostedControlPlaneInternalBaseUrl, r as getConfiguredSingleSiteOrigin, s as getCorsOrigins, u as getGitHubAppConfig, v as getLocalStoragePath, w as shouldUseSecureCookies, x as getTelegramBotPool } from "./env-BPYViDJJ.js";
|
|
4
|
+
import { l as markdownToTiptapJson, o as createGitHubSyncService } from "./github-sync-HdK2EgvJ.js";
|
|
5
|
+
import { a as listInstallationReposPage, n as getInstallation, o as searchInstallationRepos, t as buildInstallUrl } from "./github-app-DdiD-bC4.js";
|
|
6
|
+
import { r as parseRepoSlug, t as createGitHubClient } from "./github-api-BNF35Lkx.js";
|
|
7
7
|
import { I18n } from "@lingui/core";
|
|
8
8
|
import * as lucideIcons from "lucide-static";
|
|
9
9
|
import { z } from "zod";
|
|
@@ -1913,7 +1913,7 @@ var Hono = class extends Hono$1 {
|
|
|
1913
1913
|
}
|
|
1914
1914
|
//#endregion
|
|
1915
1915
|
//#region src/i18n/locales/public/en.ts
|
|
1916
|
-
var messages$3 = JSON.parse("{\"+4u2g6\":[\"A ready-made 1:1 PNG for decks, mockups, directories, and other square placements.\"],\"+DPYOZ\":[\"Add a link to your main RSS feed. Change what /feed returns in General.\"],\"+G8qqW\":[\"Collection saved.\"],\"+IJm1Z\":[\"Muted\"],\"+Irvp3\":[\"Everything on this page is ready to use for articles, launch posts, directories, and product coverage.\"],\"+Qaboy\":[\"Favicon\"],\"+fWu2O\":[\"A calmer, warmer accent makes the default theme feel quieter and more intentional.\"],\"+jw/c1\":[\"Delete image\"],\"+nHhRH\":[\"Use \",[\"brandColorName\"]],\"+siMqD\":[\"Journal\"],\"/DFKdU\":[\"Type the quote...\"],\"/PfPLc\":[\"Label (optional)\"],\"/PoNoq\":[\"Edit link\"],\"/Ui2OV\":[\"Use the reverse logo on dark backgrounds.\"],\"/Ybds4\":[\"Primary Jant logo for websites, docs, press coverage, and editorial layouts.\"],\"/rTz0M\":[\"Audio\"],\"0EcUWz\":[\"Discard changes?\"],\"0Lj7or\":[\"Save text attachment?\"],\"0Vlj/m\":[\"This post isn’t published.\"],\"0XDp7X\":[\"Links should read clearly without glowing against the page.\"],\"0cspe/\":[\"Delete row\"],\"0ieXE7\":[\"Highest rated\"],\"11h9eK\":[\"Includes\"],\"15++NM\":[\"Inline emphasis\"],\"1DBGsz\":[\"Notes\"],\"1NeeWI\":[\"Square assets for avatars, apps, browsers, and shared links\"],\"1THMr2\":[\"Brand pack\"],\"1njn7W\":[\"Light\"],\"27KTPP\":[\"Draft preview\"],\"2B7HLH\":[\"New post\"],\"2C7mSG\":[\"Collection link\"],\"2ETv7R\":[\"Tune color in a real reading context\"],\"2HbvFp\":[\"Real post components\"],\"2MXb5X\":[\"Field notes on quiet design\"],\"2koDOQ\":[\"Thread accents\"],\"2lKpcz\":[\"Write your first post to get started.\"],\"2q/Q7x\":[\"Visibility\"],\"2sCqzD\":[\"Use this for websites, docs, articles, and other light or neutral surfaces.\"],\"3Cw1AI\":[\"Add Collection\"],\"3lJk5u\":[\"Keep the artwork unchanged.\"],\"3mdteM\":[\"before deciding whether the accent is carrying too much product energy.\"],\"3neqtf\":[\"Thread accent\"],\"3qkggm\":[\"Fullscreen\"],\"3vMdv3\":[\"This link is reserved. Choose something else.\"],\"3wKq0C\":[\"Couldn't save. Try again in a moment.\"],\"3xi01/\":[\"Look at the footer metadata last, to make sure the accent is not fighting the typography.\"],\"47iMgt\":[\"Editorial interfaces worth borrowing from\"],\"4D09NB\":[\"Link to your collections page\"],\"4GQThz\":[\"No collections yet. Start one to organize threads by topic.\"],\"4HLTdq\":[\"without media\"],\"4J/OYU\":[\"Collection created.\"],\"4eiXo+\":[\"Leave blank to generate one automatically.\"],\"4pV0kE\":[\"Avatar-ready\"],\"51EYZX\":[\"without title\"],\"5dcjwM\":[\"Choose today or an earlier date, or leave it blank to publish now.\"],\"5pAjd8\":[\"Accent should feel present, not loud.\"],\"5sEkBi\":[\"Open raw asset\"],\"61i3cO\":[\"Toggle header row\"],\"6QCZMo\":[\"Add row below\"],\"6UTABI\":[\"Collection order updated.\"],\"6WAK+2\":[\"Use current date\"],\"6Y4BBO\":[\"An abstract editorial layout in warm paper colors\"],\"6cjUDB\":[\"Brand assets\"],\"6lGV3K\":[\"Show less\"],\"6p0JeQ\":[\"to make sure both still feel like they belong to the same product.\"],\"6sVyMq\":[\"Add a URL before posting this link.\"],\"6yCv8j\":[\"Save these changes to the text attachment, discard them, or keep editing.\"],\"74kJNs\":[\"View earlier notes in this thread\"],\"7DvUqV\":[\"Read the page from top to bottom without looking at the swatches.\"],\"7aris6\":[\"March 15\"],\"7d1a0d\":[\"Public\"],\"7hYXO0\":[\"Use this on dark backgrounds, image-backed surfaces, and any placement where the green logo would lose contrast.\"],\"7ibPpM\":[\"Add column after\"],\"7kMW54\":[\"Open raw SVG\"],\"7nGhhM\":[\"What's on your mind?\"],\"7vhWI8\":[\"New Password\"],\"87a/t/\":[\"Label\"],\"8Btgys\":[\"Draft deleted.\"],\"8WX0J+\":[\"Your thoughts (optional)\"],\"8ZsakT\":[\"Password\"],\"8bpHix\":[\"Couldn't create your account. Check the details and try again.\"],\"8eC78s\":[\"A calmer accent makes\"],\"8tM8+a\":[\"Save as draft\"],\"90IRF2\":[\"This article is here to answer a specific question: does the default accent still feel calm once it has to carry a full reading experience?\"],\"9SHZas\":[\"Shows 'Settings' when logged in, 'Sign in' when logged out\"],\"9aloPG\":[\"References\"],\"9dr9Nh\":[\"Open external link\"],\"9j5qCS\":[\"Table options\"],\"9qWoxS\":[\"Feed\"],\"A1D8Yt\":[\"What the accent should do\"],\"A1taO8\":[\"Search\"],\"A2Vg/u\":[\"Navigation and reading states\"],\"AjHkcv\":[\"Default preview image for social shares and link unfurls.\"],\"AyHO4m\":[\"What's this collection about?\"],\"B1FFMj\":[\"Download Brand Pack\"],\"B495Gs\":[\"Archive\"],\"BdjLtf\":[\"thread\"],\"Bmaby2\":[\"All formats\"],\"C+9df9\":[\"Quoted or highlighted passages should feel like annotations, not warnings.\"],\"C0/57J\":[\"This is the last part of the collection link.\"],\"C4TjpG\":[\"Read less\"],\"CAh1km\":[\"collections\"],\"CH3bgf\":[\"RSS feed for this view\"],\"CmBCXY\":[\"Link updated.\"],\"D4em/+\":[\"Logos\"],\"DHhJ7s\":[\"Previous\"],\"DJLY+/\":[\" and try again.\"],\"DOx286\":[\"Draft restored.\"],\"DPfwMq\":[\"Done\"],\"DSJXZM\":[\"Enter a valid date.\"],\"DYlMYF\":[\"Built-in background\"],\"DoJzLz\":[\"Collections\"],\"Du2B9f\":[\"The default accent should support reading first. Start by comparing it against the\"],\"DxwUcG\":[\"Read the palette as content first\"],\"E3NcGH\":[\"Square logo PNG\"],\"EEYbdt\":[\"Publish\"],\"EGwzOK\":[\"Complete Setup\"],\"EHWwm1\":[\"The default accent should feel written, not branded.\"],\"EO3I6h\":[\"Upload didn't go through. Try again in a moment.\"],\"EQNPYo\":[\"Featured on \",[\"date\"],\" at \",[\"time\"]],\"EQtz4D\":[\"Open a few links and check whether they still feel native to the page.\"],\"EU3tBD\":[\"Link removed.\"],\"EetoJL\":[\"Guide the eye without taking over the layout.\"],\"Eiv3bO\":[\"Buttons can stay steady, but links, thread markers, and subtle emphasis should feel closer to ink on paper than dashboard chrome.\"],\"ElTnWL\":[\"Published on\"],\"EmQw8O\":[\"If this article still feels like a page you want to keep reading, the palette is probably close.\"],\"EsJdRp\":[\"Save theme\"],\"FESYvt\":[\"Describe this for people with visual impairments...\"],\"FEr96N\":[\"Theme\"],\"FGySZL\":[\"The default accent works best when it reads like a fountain-pen underline. Compare it against the\"],\"FM+KeU\":[\"No drafts yet. Save a draft to find it here.\"],\"Fdv5k7\":[\"What to look for while tuning it\"],\"FkMol5\":[\"Featured\"],\"FqCHF/\":[\"Threads\"],\"Fxf4jq\":[\"Description (optional)\"],\"G2u/aQ\":[\"Download official Jant logos, icons, and preview assets.\"],\"GAohqx\":[\"Delete column\"],\"GBJzTZ\":[\"Archive\"],\"GX2VMa\":[\"Create your admin account.\"],\"GY/1J4\":[\"Jant fallback canary string\"],\"GbIOhd\":[\"Reply quietly\"],\"GiRWtR\":[\"Why the default accent should feel written, not branded\"],\"GkpIs2\":[\"Remove this link from Collections? The destination won't change.\"],\"GorKul\":[\"Welcome to Jant\"],\"GxkJXS\":[\"Uploading...\"],\"H29JXm\":[\"+ ALT\"],\"H4lgRd\":[\"Authentication isn't set up. Check your server config.\"],\"HFPGej\":[\"No threads match these filters. Try adjusting your selection or clear all filters.\"],\"HG79RB\":[\"Post as Private\"],\"HNEHJP\":[\"Demo credentials are pre-filled — hit Sign In to continue.\"],\"HSI88F\":[\"Delete table\"],\"HbAIQc\":[\"A reference link for checking whether the accent feels editorial instead of promotional.\"],\"HrC0ab\":[\"All posts\"],\"Ht1V3q\":[\"For the same reason, inline code should stay neutral. Something like theme.siteAccent = soften(green, 12%) should not suddenly become the loudest thing on the page.\"],\"I22eN0\":[\"Shared links\"],\"I6zLrz\":[\"Use these when you need a transparent square logo, a shaped tile with a built-in background, a browser icon, or a default preview image.\"],\"ICsA6P\":[\"You have unsaved changes\"],\"IUX7p+\":[\"White logo on the Jant green rounded tile for app icon mockups, touch icons, directory listings, and other square placements that should feel softer.\"],\"IagCbF\":[\"URL\"],\"IjnQHI\":[\"with title\"],\"ImOQa9\":[\"Reply\"],\"J+2Rls\":[\"Leave blank to publish now. Use an earlier date when importing older posts.\"],\"J4tAHl\":[\"Headings should keep their hierarchy even when the accent gets softer.\"],\"JYj5R2\":[\"Browse files\"],\"JcD7qf\":[\"More actions\"],\"JqJ5Xv\":[\"Latest\"],\"JuN5GC\":[\"No file selected. Choose a file to upload.\"],\"JwLPQ/\":[\"This sign-in link has expired. Return to \"],\"KOqvXP\":[\"Do not recolor, stretch, rotate, outline, or add effects to the logo.\"],\"KbS2K9\":[\"Reset Password\"],\"KdSsVl\":[\"Author (optional)\"],\"Khu3PV\":[\"Publish settings\"],\"KiJn9B\":[\"Note\"],\"KlZ+t+\":[\"%name% + %count% more\"],\"KsvRin\":[\"Hide from Latest\"],\"KzmC5L\":[\"Controls\"],\"L7svJg\":[\"Reading\"],\"Lbkbwy\":[\"A quote card for judging accent color against softer, citation-heavy content.\"],\"LcvzvX\":[\"Tap to retry\"],\"LkA8jz\":[\"Add alt text\"],\"LxRg6f\":[\"live theme controls\"],\"M4tzVU\":[\"Latest posts\"],\"M8kJqa\":[\"Drafts\"],\"MHrjPM\":[\"Title\"],\"MILa7n\":[\"Square tile\"],\"MRYGql\":[\"Open image URL\"],\"MSc/Yq\":[\"Do you want to publish your changes or discard them?\"],\"MZTcRq\":[\"Image unavailable\"],\"Mc7+6G\":[\"Enter a valid URL starting with http://, https://, or mailto:.\"],\"MdMyne\":[\"Source link (optional)\"],\"MiMY3Q\":[\"Apple touch icon\"],\"MiyoI7\":[\"default note sample\"],\"MqghUt\":[\"Search posts...\"],\"Myqkib\":[\"Create a collection to get started.\"],\"N8UzTV\":[\"Replies\"],\"NAFbuE\":[\"Search snippet\"],\"NH9Z1R\":[\"Start here\"],\"NqsRbb\":[\"Jant logo\"],\"NvXuWk\":[\"Won't move the thread to the top of latest.\"],\"O1367B\":[\"All collections\"],\"O3oNi5\":[\"Email\"],\"OEdMhi\":[\"The best default color is the one you notice only after reading for a while.\"],\"OEt/to\":[\"Guidelines\"],\"OJxdgi\":[\"Keep this link under 200 characters.\"],\"OaoJcz\":[\"Social preview\"],\"OmfDbR\":[\"Site accent\"],\"Ovks1h\":[\"A softer blue feels more like ink than product chrome.\"],\"P/sHNL\":[\"Use this page to judge buttons, links, cards, forms, thread accents, and quiet surfaces before changing a theme globally.\"],\"PBxg/E\":[\"Not now\"],\"Q/uoSA\":[\"Quiet here for now.\"],\"Q2mGA7\":[\"Clear filter\"],\"QBqVyM\":[\"Home screen icon for iPhone and iPad shortcuts.\"],\"QebAts\":[\"Link added.\"],\"Qgbxdw\":[\"Designing a calmer default accent for Jant\"],\"Qn9Ao8\":[\"Circle tile\"],\"Qoq+GP\":[\"Read more\"],\"QyDt3L\":[\"File uploaded.\"],\"R5CMuK\":[\"Jant looks best when the accent feels editorial. Buttons can stay sturdy, but inline emphasis should feel like a pen mark, not a dashboard highlight.\"],\"R8AthW\":[\"Divider\"],\"R9Khdg\":[\"Auto\"],\"RAv3u7\":[\"Compare it against the theme controls\"],\"ROa4Ti\":[\"Interfaces for reading should guide the eye, not keep asking for attention.\"],\"RZOWDv\":[\"Add a custom shortcut to any page or site.\"],\"RdmNnl\":[\"Browser tab\"],\"RfGczC\":[\"Square logo\"],\"Rj01Fz\":[\"Links\"],\"S37om9\":[\"Included assets\"],\"S8NCfs\":[\"Save to drafts to edit and post at a later time.\"],\"SJGVAw\":[\"Feel editorial and slightly quieter.\"],\"SJmfuf\":[\"Site Name\"],\"SSsoa4\":[\"Add to Navigation\"],\"SaNhJE\":[\"feel deliberate instead of washed out.\"],\"SpTWH3\":[\"Download SVG\"],\"SvRuJt\":[\"Field Notes on Interface Tone\"],\"T/R+Qz\":[\"Primary\"],\"TNZKpI\":[\"Danger\"],\"Tn1w2R\":[\"Draft actions\"],\"TvaTxw\":[\"Doesn't appear in Latest. Still appears in collections you add it to.\"],\"UIMXHD\":[\"Remove Divider\"],\"UaZwcz\":[\"More options are available after you create it.\"],\"Uc5y7o\":[\"Choose the standard logo for websites, docs, directories, and editorial layouts.\"],\"V0fyg5\":[\"Collection added to navigation.\"],\"V18SVO\":[\"Use the logo on light backgrounds.\"],\"V4WsyL\":[\"Add Link\"],\"VCA6B2\":[\"These are actual feed components with real footers, summaries, and inline links. Use this section to judge whether the theme still feels calm once it is applied to realistic content.\"],\"VNqFYa\":[\"Loading post...\"],\"WCOanD\":[\"This reference is useful because it treats links and citations as part of the reading rhythm. Keep that in mind while tuning the\"],\"WbIbzR\":[\"Checking link...\"],\"WcWS//\":[\"Download file\"],\"WhsN3P\":[\"A good default accent in Jant should feel like editorial structure, not product branding. That means links, emphasis, and thread cues can be visible without turning the page into UI chrome.\"],\"Wn+/rH\":[\"Transparent square\"],\"XU7b+L\":[\"Primary logo files\"],\"XV1mAn\":[\"Only visible when signed in.\"],\"XrnWzN\":[\"Published!\"],\"Y7WAtz\":[\"An image couldn't be saved to your library — its original link was kept.\"],\"YIix5Y\":[\"Search...\"],\"YOzD/a\":[\"Replace image\"],\"YUglt2\":[\"Generating a link...\"],\"YXiA6e\":[\"Primary button\"],\"Ygx3Yl\":[\"Small browser icon used in tabs and bookmarks.\"],\"Z4OdvO\":[\"Adding…\"],\"Z6NwTi\":[\"Save as Draft\"],\"ZV5ykW\":[\"Download PNG\"],\"ZhhOwV\":[\"Quote\"],\"ZmSeP+\":[\"Save to drafts?\"],\"ZxFuun\":[[\"count\",\"plural\",{\"one\":[\"Found \",\"#\",\" result\"],\"other\":[\"Found \",\"#\",\" results\"]}]],\"a5j82I\":[\"No collections match that search. Try a different name.\"],\"aBFXGQ\":[\"This collection is empty. Add threads from the editor.\"],\"aHTB7P\":[\"Supplementary content attached to your post\"],\"aMEyv0\":[\"Stay sturdy and readable.\"],\"aN6wx0\":[\"Nothing in Featured yet. Mark a post as featured to show it here.\"],\"aYpXKS\":[\"and checking whether the accent is guiding attention or pulling too hard.\"],\"aaGV/9\":[\"New Link\"],\"af+9p6\":[\"Quiet metadata\"],\"an5hVd\":[\"Images\"],\"ao77hr\":[[\"count\",\"plural\",{\"one\":[\"#\",\" hidden post\"],\"other\":[\"#\",\" hidden posts\"]}]],\"auFlOr\":[\"Icons and previews\"],\"avuFKG\":[\"threads\"],\"b+dane\":[\"Insert %rows% by %cols% table\"],\"bFpC86\":[\"Everything in one download\"],\"bGtMpA\":[\"Add a label and URL.\"],\"bHOiy1\":[\"Password changes are off in demo mode. Sign in with the shared demo credentials.\"],\"bZ9ges\":[\"Add column before\"],\"bbdNeX\":[\"Sign in\"],\"bfCbdi\":[\"Current post\"],\"bkBJmZ\":[\"This is useful as a color check because it puts the accent next to quotation styling, metadata, and a quieter explanatory paragraph. Compare it back to the\"],\"bkMuwo\":[\"Link to posts you've marked as featured.\"],\"bzSI52\":[\"Discard\"],\"c2JRUS\":[\"Generate automatically\"],\"cIoW7X\":[\"Inline link\"],\"cTUByn\":[\"Newest first\"],\"cb7FR8\":[\"White logo on the Jant green square tile for platforms and layouts that expect a true edge-to-edge square.\"],\"cgmi4V\":[\"Delete Draft\"],\"cnGeoo\":[\"Delete\"],\"d+F4pf\":[\"The image should sit quietly inside the article instead of feeling like a card preview.\"],\"d/o/BH\":[\"Couldn't publish. Saved as draft.\"],\"d0DHp4\":[\"Delete this collection permanently? Threads inside won't be removed.\"],\"dD7NPy\":[\"Outline\"],\"dEgA5A\":[\"Cancel\"],\"dHko2w\":[\"single posts\"],\"dUsGbd\":[\"The right accent should disappear into the writing until you need it.\"],\"dXoieq\":[\"Summary\"],\"dYKrp3\":[\"Hidden from Latest\"],\"dbUuAj\":[\"Appears in Latest.\"],\"df4a/r\":[\"Couldn't load this post. Try again.\"],\"ePK91l\":[\"Edit\"],\"eWLklq\":[\"Quotes\"],\"f4MAoA\":[\"Some uploads failed. Saved as draft.\"],\"f5s9EI\":[\"Press N to write\"],\"f6Hub0\":[\"Sort\"],\"f8fH8W\":[\"Design\"],\"fD+f7T\":[\"RSS feed\"],\"fKrDxS\":[\"Brand tile\"],\"fMPkxb\":[\"Show more\"],\"fqDzSu\":[\"Rate\"],\"fttd2R\":[\"My Collection\"],\"fuWzQl\":[\"Nothing here yet. Add threads to one of these collections to fill this view.\"],\"gCcxP/\":[\"Threads can include up to \",[\"count\"],\" posts.\"],\"gFdWl+\":[\"A long-form article sample for checking the default palette in a true reading context.\"],\"gNKz6Z\":[\"Collection deleted.\"],\"gXH9r/\":[\"Open raw PNG\"],\"gpaPhA\":[\"Helps screen readers describe the image\"],\"h5RcXU\":[\"Post hidden\"],\"hLlWo5\":[\"A few simple rules.\"],\"hWpUeY\":[\"Auto link\"],\"hXzOVo\":[\"Next\"],\"heSQoS\":[\"Paste a URL...\"],\"hjeS3W\":[\"Link to your latest posts. Your homepage shows this feed.\"],\"hqeXKW\":[\"Single posts\"],\"hrkGms\":[\"Search\"],\"i0vDGK\":[\"Sort Order\"],\"i5+Y7d\":[\"Download the official Jant logo, icons, and preview files.\"],\"i6kro6\":[\"Edit custom link\"],\"i6nDCI\":[\"Choose a new password.\"],\"iG7KNr\":[\"Logo\"],\"iH8pgl\":[\"Back\"],\"ilSmIt\":[\"Hard edge\"],\"iu7tUI\":[\"Breadcrumb\"],\"jAXE5p\":[\"Reverse logo\"],\"jAqB/k\":[\"Post privately\"],\"jQflRT\":[\"This uses the real single-post detail rendering with a longer article, inline image, tables, lists, quotes, and code. The content column stays at the same width as the live site.\"],\"jd+8Mm\":[\"Social preview image\"],\"jdJOV1\":[\"Settings\"],\"ji7oVU\":[\"Edit post\"],\"jpctdh\":[\"View\"],\"jrsUoG\":[\"Type / for commands\"],\"jvyYZG\":[\"What's on your mind...\"],\"k3Iw35\":[\"Switch to the white logo when the standard green version would lose contrast.\"],\"kPMIr+\":[\"Give it a title...\"],\"kzvWob\":[\"Link to the post archive\"],\"laT1IJ\":[\"iOS home screen\"],\"lb+Xwx\":[\"Custom link\"],\"m16xKo\":[\"Add\"],\"mKT7g0\":[\"Text attachment\"],\"mc/vLq\":[\"This link is already in use. Choose something else.\"],\"muKqfV\":[\"Featured\"],\"n1ekoW\":[\"Sign In\"],\"n3ReIn\":[\"Collections\"],\"n6QD94\":[\"Oldest first\"],\"nFukaP\":[\"Wrong email or password. Check your credentials and try again.\"],\"nJ4U1U\":[[\"count\"],\" images couldn't be saved to your library — their original links were kept.\"],\"nV6twc\":[\"Organize\"],\"nd8Puv\":[\"White logo on the Jant green circle for profile images, badges, and other round placements where you want a ready-made asset.\"],\"ndrEYW\":[\"When the accent is slightly warmer and less literal, the whole page feels more like a writing space and less like product UI.\"],\"oO0hKx\":[[\"count\",\"plural\",{\"one\":[\"#\",\" more post\"],\"other\":[\"#\",\" more posts\"]}]],\"oTu7Wt\":[\"Combined Collections\"],\"ode0+L\":[\"Theme sample\"],\"ogssnn\":[\"with media\"],\"ovBPCi\":[\"Default\"],\"p1Z67P\":[\"When primary is too rigid, the whole page starts reading like product UI instead of writing space.\"],\"p2/GCq\":[\"Confirm Password\"],\"pB0OKE\":[\"New Divider\"],\"pBHx39\":[\"Dark backgrounds\"],\"pVrU5x\":[\"If this page feels too branded, the first place to soften is the default theme’s site accent, not the border or body text.\"],\"pvnfJD\":[\"Dark\"],\"q+bMmy\":[\"Table controls\"],\"q+hNag\":[\"Collection\"],\"q5YRzz\":[\"Color check\"],\"q8RviX\":[\"Titled\"],\"qc+12k\":[\"Choose table size\"],\"qcawwg\":[\"Publish now\"],\"qiN9NB\":[\"Surface\"],\"qt89I8\":[\"Draft saved.\"],\"quvfGs\":[\"instead of judging it as an isolated swatch.\"],\"r7kcaA\":[\"Drag collections, links, and dividers into the order you want.\"],\"rA2TFI\":[\"Switch the palette and mode without opening settings or changing the active site theme.\"],\"rV8ZnP\":[\"Edit publish date\"],\"rdUucN\":[\"Preview\"],\"s8G5Or\":[\"This upload would exceed your shared hosted media limit. Remove files or upgrade storage to continue.\"],\"s9gHf5\":[\"your-post-link\"],\"sER+bs\":[\"Files\"],\"sQpDn6\":[\"Exit fullscreen\"],\"sgr2wQ\":[\"collection\"],\"slujBW\":[\"Use lowercase letters, numbers, and hyphens only.\"],\"syiAKf\":[\"note treatment\"],\"t42hIC\":[\"Everything most people need is in one ZIP.\"],\"tCctex\":[\"The brand pack includes SVG logos, a transparent square PNG, rounded, square, and circle tiles, plus favicon, Apple touch icon, and the default social preview image.\"],\"tKlWWY\":[\"Emoji\"],\"tSWVu5\":[\"Published on \",[\"date\"],\" at \",[\"time\"]],\"tT/bSk\":[\"Add row above\"],\"tZ1tJc\":[\"Edit Navigation\"],\"tfDRzk\":[\"Save\"],\"tg5MRw\":[\"Sign in to start writing.\"],\"tgSBSE\":[\"Remove Link\"],\"uNPC+z\":[\"Couldn't add this collection to navigation. Try again.\"],\"uowbPn\":[\"Remove attachment\"],\"v3E8iS\":[\"A practical checklist\"],\"vSJd18\":[\"Video\"],\"vSYKYI\":[\"Main feed\"],\"vXCC6J\":[\"Something doesn't look right. Check the form and try again.\"],\"vcpc5o\":[\"Close menu\"],\"vdFnYM\":[\"Reset link\"],\"vdvpU5\":[\"/archive?format=quote or https://example.com\"],\"vgpfCi\":[\"Save draft\"],\"vpSPA1\":[\"Auth secret is missing. Check your environment variables.\"],\"vzU4k9\":[\"New Collection\"],\"w0Emel\":[\"Suggested link\"],\"w6mlns\":[\"Article detail page\"],\"wJ+GRy\":[\"All visibility\"],\"wL3cK8\":[\"Latest\"],\"wja8aL\":[\"Untitled\"],\"wlnK1t\":[\"A single ZIP with the main logo, reverse logo, square PNG, rounded, square, and circle tiles, plus favicon, Apple touch icon, and social preview image.\"],\"wm3Zlr\":[\"All years\"],\"xCWek4\":[\"File storage isn't set up. Check your server config.\"],\"xVrkxi\":[\"quiet design\"],\"xVvw1i\":[\"This reset link is no longer valid. Request a new one to continue.\"],\"xYilR2\":[\"Media\"],\"xeiujy\":[\"Text\"],\"xhTx3y\":[\"Choose the standard logo for most placements and the reverse logo when you need more contrast.\"],\"xjHB3/\":[\"Continue →\"],\"y28hnO\":[\"Post\"],\"y2o/Y0\":[\"This Link Has Expired\"],\"yGZVl1\":[\"More\"],\"yQ2kGp\":[\"Load more\"],\"yUtAh2\":[\"New Thread\"],\"ycM1Xg\":[\"No results. Try different keywords.\"],\"ynMAhG\":[\"Default logo\"],\"yzF66j\":[\"Link\"],\"zBFr9G\":[\"Paste a long article, AI response, or any text...\\n\\nMarkdown formatting will be preserved.\"],\"zJDAbh\":[\"Don't save\"],\"zcDmsG\":[\"Featured posts\"],\"zoK+eO\":[\"Add a title before posting this link.\"],\"zucql+\":[\"Menu\"],\"zwBp5t\":[\"Private\"]}");
|
|
1916
|
+
var messages$3 = JSON.parse("{\"+4u2g6\":[\"A ready-made 1:1 PNG for decks, mockups, directories, and other square placements.\"],\"+DPYOZ\":[\"Add a link to your main RSS feed. Change what /feed returns in General.\"],\"+G8qqW\":[\"Collection saved.\"],\"+IJm1Z\":[\"Muted\"],\"+Irvp3\":[\"Everything on this page is ready to use for articles, launch posts, directories, and product coverage.\"],\"+Qaboy\":[\"Favicon\"],\"+fWu2O\":[\"A calmer, warmer accent makes the default theme feel quieter and more intentional.\"],\"+jw/c1\":[\"Delete image\"],\"+nHhRH\":[\"Use \",[\"brandColorName\"]],\"+siMqD\":[\"Journal\"],\"/DFKdU\":[\"Type the quote...\"],\"/PfPLc\":[\"Label (optional)\"],\"/PoNoq\":[\"Edit link\"],\"/Ui2OV\":[\"Use the reverse logo on dark backgrounds.\"],\"/Ybds4\":[\"Primary Jant logo for websites, docs, press coverage, and editorial layouts.\"],\"/rTz0M\":[\"Audio\"],\"0EcUWz\":[\"Discard changes?\"],\"0Lj7or\":[\"Save text attachment?\"],\"0Vlj/m\":[\"This post isn’t published.\"],\"0XDp7X\":[\"Links should read clearly without glowing against the page.\"],\"0cspe/\":[\"Delete row\"],\"0ieXE7\":[\"Highest rated\"],\"11h9eK\":[\"Includes\"],\"15++NM\":[\"Inline emphasis\"],\"1DBGsz\":[\"Notes\"],\"1NeeWI\":[\"Square assets for avatars, apps, browsers, and shared links\"],\"1THMr2\":[\"Brand pack\"],\"1njn7W\":[\"Light\"],\"27KTPP\":[\"Draft preview\"],\"2B7HLH\":[\"New post\"],\"2C7mSG\":[\"Collection link\"],\"2ETv7R\":[\"Tune color in a real reading context\"],\"2HbvFp\":[\"Real post components\"],\"2MXb5X\":[\"Field notes on quiet design\"],\"2koDOQ\":[\"Thread accents\"],\"2lKpcz\":[\"Write your first post to get started.\"],\"2q/Q7x\":[\"Visibility\"],\"2sCqzD\":[\"Use this for websites, docs, articles, and other light or neutral surfaces.\"],\"3Cw1AI\":[\"Add Collection\"],\"3lJk5u\":[\"Keep the artwork unchanged.\"],\"3mdteM\":[\"before deciding whether the accent is carrying too much product energy.\"],\"3neqtf\":[\"Thread accent\"],\"3qkggm\":[\"Fullscreen\"],\"3vMdv3\":[\"This link is reserved. Choose something else.\"],\"3wKq0C\":[\"Couldn't save. Try again in a moment.\"],\"3xi01/\":[\"Look at the footer metadata last, to make sure the accent is not fighting the typography.\"],\"47iMgt\":[\"Editorial interfaces worth borrowing from\"],\"4D09NB\":[\"Link to your collections page\"],\"4GQThz\":[\"No collections yet. Start one to organize threads by topic.\"],\"4HLTdq\":[\"without media\"],\"4J/OYU\":[\"Collection created.\"],\"4eiXo+\":[\"Leave blank to generate one automatically.\"],\"4pV0kE\":[\"Avatar-ready\"],\"51EYZX\":[\"without title\"],\"5dcjwM\":[\"Choose today or an earlier date, or leave it blank to publish now.\"],\"5pAjd8\":[\"Accent should feel present, not loud.\"],\"5sEkBi\":[\"Open raw asset\"],\"61i3cO\":[\"Toggle header row\"],\"6QCZMo\":[\"Add row below\"],\"6UTABI\":[\"Collection order updated.\"],\"6WAK+2\":[\"Use current date\"],\"6Y4BBO\":[\"An abstract editorial layout in warm paper colors\"],\"6cjUDB\":[\"Brand assets\"],\"6lGV3K\":[\"Show less\"],\"6p0JeQ\":[\"to make sure both still feel like they belong to the same product.\"],\"6sVyMq\":[\"Add a URL before posting this link.\"],\"6yCv8j\":[\"Save these changes to the text attachment, discard them, or keep editing.\"],\"74kJNs\":[\"View earlier notes in this thread\"],\"7DvUqV\":[\"Read the page from top to bottom without looking at the swatches.\"],\"7aris6\":[\"March 15\"],\"7d1a0d\":[\"Public\"],\"7hYXO0\":[\"Use this on dark backgrounds, image-backed surfaces, and any placement where the green logo would lose contrast.\"],\"7ibPpM\":[\"Add column after\"],\"7kMW54\":[\"Open raw SVG\"],\"7nGhhM\":[\"What's on your mind?\"],\"7vhWI8\":[\"New Password\"],\"87a/t/\":[\"Label\"],\"8Btgys\":[\"Draft deleted.\"],\"8WX0J+\":[\"Your thoughts (optional)\"],\"8ZsakT\":[\"Password\"],\"8bpHix\":[\"Couldn't create your account. Check the details and try again.\"],\"8eC78s\":[\"A calmer accent makes\"],\"8tM8+a\":[\"Save as draft\"],\"90IRF2\":[\"This article is here to answer a specific question: does the default accent still feel calm once it has to carry a full reading experience?\"],\"9SHZas\":[\"Shows 'Settings' when logged in, 'Sign in' when logged out\"],\"9aloPG\":[\"References\"],\"9dr9Nh\":[\"Open external link\"],\"9j5qCS\":[\"Table options\"],\"9qWoxS\":[\"Feed\"],\"A1D8Yt\":[\"What the accent should do\"],\"A1taO8\":[\"Search\"],\"A2Vg/u\":[\"Navigation and reading states\"],\"AjHkcv\":[\"Default preview image for social shares and link unfurls.\"],\"AmQ/h/\":[\"Sort order\"],\"AyHO4m\":[\"What's this collection about?\"],\"B1FFMj\":[\"Download Brand Pack\"],\"B495Gs\":[\"Archive\"],\"BdjLtf\":[\"thread\"],\"Bmaby2\":[\"All formats\"],\"C+9df9\":[\"Quoted or highlighted passages should feel like annotations, not warnings.\"],\"C0/57J\":[\"This is the last part of the collection link.\"],\"C4TjpG\":[\"Read less\"],\"CAh1km\":[\"collections\"],\"CH3bgf\":[\"RSS feed for this view\"],\"CmBCXY\":[\"Link updated.\"],\"D4em/+\":[\"Logos\"],\"DHhJ7s\":[\"Previous\"],\"DJLY+/\":[\" and try again.\"],\"DOx286\":[\"Draft restored.\"],\"DPfwMq\":[\"Done\"],\"DSJXZM\":[\"Enter a valid date.\"],\"DYlMYF\":[\"Built-in background\"],\"DoJzLz\":[\"Collections\"],\"Du2B9f\":[\"The default accent should support reading first. Start by comparing it against the\"],\"DxwUcG\":[\"Read the palette as content first\"],\"E3NcGH\":[\"Square logo PNG\"],\"EEYbdt\":[\"Publish\"],\"EGwzOK\":[\"Complete Setup\"],\"EHWwm1\":[\"The default accent should feel written, not branded.\"],\"EO3I6h\":[\"Upload didn't go through. Try again in a moment.\"],\"EQNPYo\":[\"Featured on \",[\"date\"],\" at \",[\"time\"]],\"EQtz4D\":[\"Open a few links and check whether they still feel native to the page.\"],\"EU3tBD\":[\"Link removed.\"],\"EetoJL\":[\"Guide the eye without taking over the layout.\"],\"Eiv3bO\":[\"Buttons can stay steady, but links, thread markers, and subtle emphasis should feel closer to ink on paper than dashboard chrome.\"],\"ElTnWL\":[\"Published on\"],\"EmQw8O\":[\"If this article still feels like a page you want to keep reading, the palette is probably close.\"],\"EsJdRp\":[\"Save theme\"],\"FESYvt\":[\"Describe this for people with visual impairments...\"],\"FEr96N\":[\"Theme\"],\"FGySZL\":[\"The default accent works best when it reads like a fountain-pen underline. Compare it against the\"],\"FM+KeU\":[\"No drafts yet. Save a draft to find it here.\"],\"Fdv5k7\":[\"What to look for while tuning it\"],\"FkMol5\":[\"Featured\"],\"FqCHF/\":[\"Threads\"],\"Fxf4jq\":[\"Description (optional)\"],\"G2u/aQ\":[\"Download official Jant logos, icons, and preview assets.\"],\"GAohqx\":[\"Delete column\"],\"GBJzTZ\":[\"Archive\"],\"GX2VMa\":[\"Create your admin account.\"],\"GY/1J4\":[\"Jant fallback canary string\"],\"GbIOhd\":[\"Reply quietly\"],\"GiRWtR\":[\"Why the default accent should feel written, not branded\"],\"GkpIs2\":[\"Remove this link from Collections? The destination won't change.\"],\"GorKul\":[\"Welcome to Jant\"],\"GxkJXS\":[\"Uploading...\"],\"H29JXm\":[\"+ ALT\"],\"H4lgRd\":[\"Authentication isn't set up. Check your server config.\"],\"HFPGej\":[\"No threads match these filters. Try adjusting your selection or clear all filters.\"],\"HG79RB\":[\"Post as Private\"],\"HNEHJP\":[\"Demo credentials are pre-filled — hit Sign In to continue.\"],\"HSI88F\":[\"Delete table\"],\"HbAIQc\":[\"A reference link for checking whether the accent feels editorial instead of promotional.\"],\"HrC0ab\":[\"All posts\"],\"Ht1V3q\":[\"For the same reason, inline code should stay neutral. Something like theme.siteAccent = soften(green, 12%) should not suddenly become the loudest thing on the page.\"],\"I22eN0\":[\"Shared links\"],\"I6zLrz\":[\"Use these when you need a transparent square logo, a shaped tile with a built-in background, a browser icon, or a default preview image.\"],\"ICsA6P\":[\"You have unsaved changes\"],\"IUX7p+\":[\"White logo on the Jant green rounded tile for app icon mockups, touch icons, directory listings, and other square placements that should feel softer.\"],\"IagCbF\":[\"URL\"],\"IjnQHI\":[\"with title\"],\"ImOQa9\":[\"Reply\"],\"IvUVJl\":[\"Show as a list with full posts\"],\"J+2Rls\":[\"Leave blank to publish now. Use an earlier date when importing older posts.\"],\"J4tAHl\":[\"Headings should keep their hierarchy even when the accent gets softer.\"],\"JYj5R2\":[\"Browse files\"],\"JcD7qf\":[\"More actions\"],\"JqJ5Xv\":[\"Latest\"],\"JuN5GC\":[\"No file selected. Choose a file to upload.\"],\"JwLPQ/\":[\"This sign-in link has expired. Return to \"],\"KOqvXP\":[\"Do not recolor, stretch, rotate, outline, or add effects to the logo.\"],\"KbS2K9\":[\"Reset Password\"],\"KdSsVl\":[\"Author (optional)\"],\"Khu3PV\":[\"Publish settings\"],\"KiJn9B\":[\"Note\"],\"KlZ+t+\":[\"%name% + %count% more\"],\"KsvRin\":[\"Hide from Latest\"],\"KzmC5L\":[\"Controls\"],\"L7svJg\":[\"Reading\"],\"Lbkbwy\":[\"A quote card for judging accent color against softer, citation-heavy content.\"],\"LcvzvX\":[\"Tap to retry\"],\"LkA8jz\":[\"Add alt text\"],\"LxRg6f\":[\"live theme controls\"],\"M4tzVU\":[\"Latest posts\"],\"M8kJqa\":[\"Drafts\"],\"MHrjPM\":[\"Title\"],\"MILa7n\":[\"Square tile\"],\"MRYGql\":[\"Open image URL\"],\"MSc/Yq\":[\"Do you want to publish your changes or discard them?\"],\"MZTcRq\":[\"Image unavailable\"],\"Mc7+6G\":[\"Enter a valid URL starting with http://, https://, or mailto:.\"],\"MdMyne\":[\"Source link (optional)\"],\"MiMY3Q\":[\"Apple touch icon\"],\"MiyoI7\":[\"default note sample\"],\"MqghUt\":[\"Search posts...\"],\"Myqkib\":[\"Create a collection to get started.\"],\"N8UzTV\":[\"Replies\"],\"NAFbuE\":[\"Search snippet\"],\"NH9Z1R\":[\"Start here\"],\"NqsRbb\":[\"Jant logo\"],\"NvXuWk\":[\"Won't move the thread to the top of latest.\"],\"O1367B\":[\"All collections\"],\"O3oNi5\":[\"Email\"],\"OEdMhi\":[\"The best default color is the one you notice only after reading for a while.\"],\"OEt/to\":[\"Guidelines\"],\"OJxdgi\":[\"Keep this link under 200 characters.\"],\"OaiMaf\":[\"View mode\"],\"OaoJcz\":[\"Social preview\"],\"OmfDbR\":[\"Site accent\"],\"Oo7//P\":[\"Edit draft\"],\"Ovks1h\":[\"A softer blue feels more like ink than product chrome.\"],\"P/sHNL\":[\"Use this page to judge buttons, links, cards, forms, thread accents, and quiet surfaces before changing a theme globally.\"],\"PBxg/E\":[\"Not now\"],\"Q/uoSA\":[\"Quiet here for now.\"],\"Q2mGA7\":[\"Clear filter\"],\"QBqVyM\":[\"Home screen icon for iPhone and iPad shortcuts.\"],\"QOhkyl\":[\"Compose\"],\"QebAts\":[\"Link added.\"],\"Qgbxdw\":[\"Designing a calmer default accent for Jant\"],\"Qn9Ao8\":[\"Circle tile\"],\"Qoq+GP\":[\"Read more\"],\"QyDt3L\":[\"File uploaded.\"],\"R5CMuK\":[\"Jant looks best when the accent feels editorial. Buttons can stay sturdy, but inline emphasis should feel like a pen mark, not a dashboard highlight.\"],\"R8AthW\":[\"Divider\"],\"R9Khdg\":[\"Auto\"],\"RAv3u7\":[\"Compare it against the theme controls\"],\"ROa4Ti\":[\"Interfaces for reading should guide the eye, not keep asking for attention.\"],\"RZOWDv\":[\"Add a custom shortcut to any page or site.\"],\"RdmNnl\":[\"Browser tab\"],\"RfGczC\":[\"Square logo\"],\"Rj01Fz\":[\"Links\"],\"S37om9\":[\"Included assets\"],\"S8NCfs\":[\"Save to drafts to edit and post at a later time.\"],\"SJGVAw\":[\"Feel editorial and slightly quieter.\"],\"SJmfuf\":[\"Site Name\"],\"SSsoa4\":[\"Add to Navigation\"],\"SaNhJE\":[\"feel deliberate instead of washed out.\"],\"SbNisn\":[\"Sort by when each thread was last added to\"],\"SpTWH3\":[\"Download SVG\"],\"SvRuJt\":[\"Field Notes on Interface Tone\"],\"T/R+Qz\":[\"Primary\"],\"TNZKpI\":[\"Danger\"],\"Tn1w2R\":[\"Draft actions\"],\"TvaTxw\":[\"Doesn't appear in Latest. Still appears in collections you add it to.\"],\"UIMXHD\":[\"Remove Divider\"],\"UaZwcz\":[\"More options are available after you create it.\"],\"Uc5y7o\":[\"Choose the standard logo for websites, docs, directories, and editorial layouts.\"],\"V0fyg5\":[\"Collection added to navigation.\"],\"V18SVO\":[\"Use the logo on light backgrounds.\"],\"V4WsyL\":[\"Add Link\"],\"VCA6B2\":[\"These are actual feed components with real footers, summaries, and inline links. Use this section to judge whether the theme still feels calm once it is applied to realistic content.\"],\"VNqFYa\":[\"Loading post...\"],\"WCOanD\":[\"This reference is useful because it treats links and citations as part of the reading rhythm. Keep that in mind while tuning the\"],\"WIF6ui\":[\"Continue writing this draft\"],\"WbIbzR\":[\"Checking link...\"],\"WcWS//\":[\"Download file\"],\"WhsN3P\":[\"A good default accent in Jant should feel like editorial structure, not product branding. That means links, emphasis, and thread cues can be visible without turning the page into UI chrome.\"],\"Wn+/rH\":[\"Transparent square\"],\"XU7b+L\":[\"Primary logo files\"],\"XV1mAn\":[\"Only visible when signed in.\"],\"XrnWzN\":[\"Published!\"],\"Y7WAtz\":[\"An image couldn't be saved to your library — its original link was kept.\"],\"YIix5Y\":[\"Search...\"],\"YOzD/a\":[\"Replace image\"],\"YUglt2\":[\"Generating a link...\"],\"YXiA6e\":[\"Primary button\"],\"Ygx3Yl\":[\"Small browser icon used in tabs and bookmarks.\"],\"Z4OdvO\":[\"Adding…\"],\"Z6NwTi\":[\"Save as Draft\"],\"ZBH98o\":[\"Show as a grid of tiles\"],\"ZV5ykW\":[\"Download PNG\"],\"ZhhOwV\":[\"Quote\"],\"ZmSeP+\":[\"Save to drafts?\"],\"ZxFuun\":[[\"count\",\"plural\",{\"one\":[\"Found \",\"#\",\" result\"],\"other\":[\"Found \",\"#\",\" results\"]}]],\"a5j82I\":[\"No collections match that search. Try a different name.\"],\"aBFXGQ\":[\"This collection is empty. Add threads from the editor.\"],\"aHTB7P\":[\"Supplementary content attached to your post\"],\"aHd4P7\":[\"Close compose\"],\"aMEyv0\":[\"Stay sturdy and readable.\"],\"aN6wx0\":[\"Nothing in Featured yet. Mark a post as featured to show it here.\"],\"aYpXKS\":[\"and checking whether the accent is guiding attention or pulling too hard.\"],\"aaGV/9\":[\"New Link\"],\"af+9p6\":[\"Quiet metadata\"],\"an5hVd\":[\"Images\"],\"ao77hr\":[[\"count\",\"plural\",{\"one\":[\"#\",\" hidden post\"],\"other\":[\"#\",\" hidden posts\"]}]],\"auFlOr\":[\"Icons and previews\"],\"avuFKG\":[\"threads\"],\"b+dane\":[\"Insert %rows% by %cols% table\"],\"bFpC86\":[\"Everything in one download\"],\"bGtMpA\":[\"Add a label and URL.\"],\"bHOiy1\":[\"Password changes are off in demo mode. Sign in with the shared demo credentials.\"],\"bZ9ges\":[\"Add column before\"],\"bbdNeX\":[\"Sign in\"],\"bfCbdi\":[\"Current post\"],\"bkBJmZ\":[\"This is useful as a color check because it puts the accent next to quotation styling, metadata, and a quieter explanatory paragraph. Compare it back to the\"],\"bkMuwo\":[\"Link to posts you've marked as featured.\"],\"bzSI52\":[\"Discard\"],\"c2JRUS\":[\"Generate automatically\"],\"cH5kXP\":[\"Now\"],\"cIoW7X\":[\"Inline link\"],\"cTUByn\":[\"Newest first\"],\"cb7FR8\":[\"White logo on the Jant green square tile for platforms and layouts that expect a true edge-to-edge square.\"],\"cgmi4V\":[\"Delete Draft\"],\"cnGeoo\":[\"Delete\"],\"d+F4pf\":[\"The image should sit quietly inside the article instead of feeling like a card preview.\"],\"d/o/BH\":[\"Couldn't publish. Saved as draft.\"],\"d0DHp4\":[\"Delete this collection permanently? Threads inside won't be removed.\"],\"dD7NPy\":[\"Outline\"],\"dEgA5A\":[\"Cancel\"],\"dHko2w\":[\"single posts\"],\"dUsGbd\":[\"The right accent should disappear into the writing until you need it.\"],\"dXoieq\":[\"Summary\"],\"dYKrp3\":[\"Hidden from Latest\"],\"dbUuAj\":[\"Appears in Latest.\"],\"df4a/r\":[\"Couldn't load this post. Try again.\"],\"ePK91l\":[\"Edit\"],\"eWLklq\":[\"Quotes\"],\"eYZPF3\":[\"/…\"],\"eneWvv\":[\"Draft\"],\"f4MAoA\":[\"Some uploads failed. Saved as draft.\"],\"f5s9EI\":[\"Press N to write\"],\"f6Hub0\":[\"Sort\"],\"f8fH8W\":[\"Design\"],\"fD+f7T\":[\"RSS feed\"],\"fKrDxS\":[\"Brand tile\"],\"fMPkxb\":[\"Show more\"],\"fqDzSu\":[\"Rate\"],\"fttd2R\":[\"My Collection\"],\"fuWzQl\":[\"Nothing here yet. Add threads to one of these collections to fill this view.\"],\"gCcxP/\":[\"Threads can include up to \",[\"count\"],\" posts.\"],\"gFdWl+\":[\"A long-form article sample for checking the default palette in a true reading context.\"],\"gGx5tM\":[\"Editing\"],\"gNKz6Z\":[\"Collection deleted.\"],\"gXH9r/\":[\"Open raw PNG\"],\"gpaPhA\":[\"Helps screen readers describe the image\"],\"h5RcXU\":[\"Post hidden\"],\"hLlWo5\":[\"A few simple rules.\"],\"hXzOVo\":[\"Next\"],\"heSQoS\":[\"Paste a URL...\"],\"hjeS3W\":[\"Link to your latest posts. Your homepage shows this feed.\"],\"hqeXKW\":[\"Single posts\"],\"hrkGms\":[\"Search\"],\"i0vDGK\":[\"Sort Order\"],\"i5+Y7d\":[\"Download the official Jant logo, icons, and preview files.\"],\"i6kro6\":[\"Edit custom link\"],\"i6nDCI\":[\"Choose a new password.\"],\"iG7KNr\":[\"Logo\"],\"iH8pgl\":[\"Back\"],\"iZYiBK\":[\"newest changes first\"],\"ilSmIt\":[\"Hard edge\"],\"iu7tUI\":[\"Breadcrumb\"],\"jAXE5p\":[\"Reverse logo\"],\"jAqB/k\":[\"Post privately\"],\"jQflRT\":[\"This uses the real single-post detail rendering with a longer article, inline image, tables, lists, quotes, and code. The content column stays at the same width as the live site.\"],\"jd+8Mm\":[\"Social preview image\"],\"jdJOV1\":[\"Settings\"],\"ji7oVU\":[\"Edit post\"],\"jpctdh\":[\"View\"],\"jrsUoG\":[\"Type / for commands\"],\"jvyYZG\":[\"What's on your mind...\"],\"k3Iw35\":[\"Switch to the white logo when the standard green version would lose contrast.\"],\"kNiQp6\":[\"Pinned\"],\"kPMIr+\":[\"Give it a title...\"],\"kzvWob\":[\"Link to the post archive\"],\"laT1IJ\":[\"iOS home screen\"],\"lb+Xwx\":[\"Custom link\"],\"m16xKo\":[\"Add\"],\"mKT7g0\":[\"Text attachment\"],\"mc/vLq\":[\"This link is already in use. Choose something else.\"],\"muKqfV\":[\"Featured\"],\"n1ekoW\":[\"Sign In\"],\"n3ReIn\":[\"Collections\"],\"n6QD94\":[\"Oldest first\"],\"nFukaP\":[\"Wrong email or password. Check your credentials and try again.\"],\"nJ4U1U\":[[\"count\"],\" images couldn't be saved to your library — their original links were kept.\"],\"nV6twc\":[\"Organize\"],\"nd8Puv\":[\"White logo on the Jant green circle for profile images, badges, and other round placements where you want a ready-made asset.\"],\"ndrEYW\":[\"When the accent is slightly warmer and less literal, the whole page feels more like a writing space and less like product UI.\"],\"oO0hKx\":[[\"count\",\"plural\",{\"one\":[\"#\",\" more post\"],\"other\":[\"#\",\" more posts\"]}]],\"oTu7Wt\":[\"Combined Collections\"],\"ode0+L\":[\"Theme sample\"],\"ogssnn\":[\"with media\"],\"ovBPCi\":[\"Default\"],\"p1Z67P\":[\"When primary is too rigid, the whole page starts reading like product UI instead of writing space.\"],\"p2/GCq\":[\"Confirm Password\"],\"pB0OKE\":[\"New Divider\"],\"pBHx39\":[\"Dark backgrounds\"],\"pKeaoC\":[\"Sort by when each thread was published\"],\"pVrU5x\":[\"If this page feels too branded, the first place to soften is the default theme’s site accent, not the border or body text.\"],\"pvnfJD\":[\"Dark\"],\"q+bMmy\":[\"Table controls\"],\"q+hNag\":[\"Collection\"],\"q5YRzz\":[\"Color check\"],\"q8RviX\":[\"Titled\"],\"qc+12k\":[\"Choose table size\"],\"qiN9NB\":[\"Surface\"],\"qt89I8\":[\"Draft saved.\"],\"quvfGs\":[\"instead of judging it as an isolated swatch.\"],\"r7kcaA\":[\"Drag collections, links, and dividers into the order you want.\"],\"rA2TFI\":[\"Switch the palette and mode without opening settings or changing the active site theme.\"],\"rV8ZnP\":[\"Edit publish date\"],\"rdUucN\":[\"Preview\"],\"s8G5Or\":[\"This upload would exceed your shared hosted media limit. Remove files or upgrade storage to continue.\"],\"s9gHf5\":[\"your-post-link\"],\"sER+bs\":[\"Files\"],\"sQpDn6\":[\"Exit fullscreen\"],\"sgr2wQ\":[\"collection\"],\"slujBW\":[\"Use lowercase letters, numbers, and hyphens only.\"],\"syiAKf\":[\"note treatment\"],\"t42hIC\":[\"Everything most people need is in one ZIP.\"],\"tCctex\":[\"The brand pack includes SVG logos, a transparent square PNG, rounded, square, and circle tiles, plus favicon, Apple touch icon, and the default social preview image.\"],\"tKlWWY\":[\"Emoji\"],\"tSWVu5\":[\"Published on \",[\"date\"],\" at \",[\"time\"]],\"tT/bSk\":[\"Add row above\"],\"tZ1tJc\":[\"Edit Navigation\"],\"tfDRzk\":[\"Save\"],\"tg5MRw\":[\"Sign in to start writing.\"],\"tgSBSE\":[\"Remove Link\"],\"uNPC+z\":[\"Couldn't add this collection to navigation. Try again.\"],\"uowbPn\":[\"Remove attachment\"],\"v3E8iS\":[\"A practical checklist\"],\"vSJd18\":[\"Video\"],\"vSYKYI\":[\"Main feed\"],\"vXCC6J\":[\"Something doesn't look right. Check the form and try again.\"],\"vcpc5o\":[\"Close menu\"],\"vdFnYM\":[\"Reset link\"],\"vdvpU5\":[\"/archive?format=quote or https://example.com\"],\"vgpfCi\":[\"Save draft\"],\"vpSPA1\":[\"Auth secret is missing. Check your environment variables.\"],\"vzU4k9\":[\"New Collection\"],\"w0Emel\":[\"Suggested link\"],\"w1bUkO\":[\"Last edited on \",[\"date\"],\" at \",[\"time\"]],\"w6mlns\":[\"Article detail page\"],\"wJ+GRy\":[\"All visibility\"],\"wL3cK8\":[\"Latest\"],\"wja8aL\":[\"Untitled\"],\"wlnK1t\":[\"A single ZIP with the main logo, reverse logo, square PNG, rounded, square, and circle tiles, plus favicon, Apple touch icon, and social preview image.\"],\"wm3Zlr\":[\"All years\"],\"xCWek4\":[\"File storage isn't set up. Check your server config.\"],\"xVrkxi\":[\"quiet design\"],\"xVvw1i\":[\"This reset link is no longer valid. Request a new one to continue.\"],\"xYilR2\":[\"Media\"],\"xeiujy\":[\"Text\"],\"xhTx3y\":[\"Choose the standard logo for most placements and the reverse logo when you need more contrast.\"],\"xjHB3/\":[\"Continue →\"],\"y28hnO\":[\"Post\"],\"y2o/Y0\":[\"This Link Has Expired\"],\"yGZVl1\":[\"More\"],\"yQ2kGp\":[\"Load more\"],\"ycM1Xg\":[\"No results. Try different keywords.\"],\"ynMAhG\":[\"Default logo\"],\"yzF66j\":[\"Link\"],\"zBFr9G\":[\"Paste a long article, AI response, or any text...\\n\\nMarkdown formatting will be preserved.\"],\"zJDAbh\":[\"Don't save\"],\"zcDmsG\":[\"Featured posts\"],\"zoK+eO\":[\"Add a title before posting this link.\"],\"zucql+\":[\"Menu\"],\"zwBp5t\":[\"Private\"]}");
|
|
1917
1917
|
//#endregion
|
|
1918
1918
|
//#region src/i18n/locales/settings/en.ts
|
|
1919
1919
|
var messages$2 = JSON.parse("{\"++YsxG\":[\"Readers and search engines see\"],\"+4Z6iP\":[\"Create the repository on GitHub first — it can be empty.\"],\"+9JI/F\":[\"Connecting will sync your site onto \",[\"repo\"],\"'s default branch on top of its existing history. Existing files outside Jant's managed paths are kept. This can't be undone.\"],\"+AXdXp\":[\"Label and URL are required\"],\"+K0AvT\":[\"Disconnect\"],\"+wgt7C\":[\"Muted red-brown background, earthy and warm.\"],\"+zy2Nq\":[\"Type\"],\"/0D1Xp\":[\"Edit Collection\"],\"/3H2/s\":[\"This hosted site signs in through \",[\"providerLabel\"],\". Manage password and hosted access there.\"],\"/PXXBT\":[\"Nearly white, with a touch of warmth.\"],\"/PoNoq\":[\"Edit link\"],\"/zOUxl\":[\"QR code linking to the Telegram bot\"],\"05DXsb\":[\"Connect or manage the bot used to post from Telegram.\"],\"0OGSSc\":[\"Avatar display updated.\"],\"0UzCUX\":[\"Update the password you use to sign in\"],\"0bdA9b\":[\"Open Telegram to connect\"],\"0gECZD\":[\"Want to write a fuller introduction?\"],\"0uIjy/\":[\"Light sage-green with a matching green accent.\"],\"1DahsC\":[\"Search settings\"],\"1F6Mzc\":[\"No navigation items yet. Add links or enable system items below.\"],\"1H7gng\":[\"Dashboard language\"],\"1MYU3o\":[\"Couldn't create the page. Check the details and try again.\"],\"1mbBbL\":[\"Connect manually\"],\"1njn7W\":[\"Light\"],\"1qGdnL\":[\"The default. Calm and easy for long reading.\"],\"21mg6u\":[\"Choose a titled note that isn't already in navigation, or create a new page.\"],\"2C7mSG\":[\"Collection link\"],\"2DoBvq\":[\"Feeds\"],\"2Et8jU\":[\"Set results per search page; reset to inherit the main page size.\"],\"2FYpfJ\":[\"More\"],\"2Ithfh\":[\"Message the bot any text and it's published as a note.\"],\"2PTjMB\":[\"I want to delete \",[\"siteName\"]],\"2cFU6q\":[\"Site Footer\"],\"2fPEPI\":[\"A calm, cool backdrop.\"],\"2lkk2l\":[\"Yellow-tinted paper with an olive-green accent.\"],\"2oWZo7\":[\"Last commit\"],\"2r8pkt\":[\"Couldn't load pages. Try again in a moment.\"],\"2uuy4H\":[\"Connected via Personal Access Token\"],\"2wK4BX\":[\"Edit About page\"],\"35x8eZ\":[\"Showing \",[\"shown\"],\" of \",[\"total\"]],\"39QGku\":[\"Open the bot and send the binding code, then anything you message it becomes a note.\"],\"3B0RD1\":[\"Upload the profile image and generated site icons.\"],\"3Cw1AI\":[\"Add Collection\"],\"3VrybB\":[\"Redirect\"],\"3Yvsaz\":[\"302 (Temporary)\"],\"3n0zbB\":[\"Session management is off in demo mode. Use the shared demo session instead.\"],\"3sYJi5\":[\"Download a Hugo-compatible archive — host it statically or move to another Jant.\"],\"3vMdv3\":[\"This link is reserved. Choose something else.\"],\"3wKq0C\":[\"Couldn't save. Try again in a moment.\"],\"49Bsal\":[\"Feed settings updated.\"],\"4J/OYU\":[\"Collection created.\"],\"4Jge8E\":[\"Active Sessions\"],\"4KIa+q\":[\"Export downloaded.\"],\"4Q0iPK\":[\"Follow the device appearance or force a light or dark theme.\"],\"4cEClj\":[\"Sessions\"],\"4yyXWu\":[\"Pale bone-white with a muted green accent.\"],\"4zGJ5E\":[\"Delete Account Permanently\"],\"5QlUIt\":[\"Empty repository. Ready to connect.\"],\"5VQnR3\":[\"Use these when you want a feed URL that never changes.\"],\"5dpcN1\":[\"type to search all\"],\"5f1Wo9\":[\"Connected as \",[\"account\"]],\"5o8R81\":[\"Very light ivory with a faint tea-green accent.\"],\"6ArdBh\":[\"Uses featured posts for /feed.\"],\"6DjeBT\":[\"Demo sites always stay hidden from search engines.\"],\"6E3aK4\":[\"Manage Hosting\"],\"6FFB7q\":[\"Uses the latest public posts for /feed.\"],\"6K1Vef\":[\"Delete this blog permanently? This cannot be undone.\"],\"6NpNLc\":[\"This repository has existing content.\"],\"6Pa5AP\":[\"Connect or manage automatic content backups to GitHub.\"],\"6V3Ea3\":[\"Copied\"],\"6WdDG7\":[\"Page\"],\"71WIgc\":[\"Get a new code\"],\"71of4k\":[\"Limit posts included in each RSS feed.\"],\"746NHh\":[\"this blog\"],\"7811AW\":[\"This repository is already backing up this site.\"],\"7FCZPo\":[\"Content language\"],\"7FaY4u\":[\"Usage\"],\"7G9YLi\":[\"Allow search engines to index my site\"],\"7GISOt\":[\"Save bot token\"],\"7MZxzw\":[\"Password changed.\"],\"7vhWI8\":[\"New Password\"],\"81nFIS\":[\"Passwords don't match. Make sure both fields are identical.\"],\"87a/t/\":[\"Label\"],\"89Upyo\":[\"That theme isn't available. Pick another one.\"],\"8BfEpW\":[\"Hosted account\"],\"8N/Mcp\":[\"Archive filter parameters (e.g. format=note&view=list)\"],\"8T46pB\":[\"Bot token\"],\"8U2Z7f\":[\"New Custom URL\"],\"8ZsakT\":[\"Password\"],\"8gEghq\":[\"Bright warm-white with a moss-green accent.\"],\"9+vGLh\":[\"Custom CSS\"],\"9As8Nu\":[\"Create one on GitHub\"],\"9EjI4j\":[\"Warm sand\"],\"9FctRm\":[\"Use lowercase letters, numbers, and hyphens.\"],\"9J6wUO\":[\"Suggested links\"],\"9Lsvt5\":[\"Signed in \",[\"date\"]],\"9T7Cwm\":[\"Redirects, vanity paths, and URL control\"],\"9aUyym\":[\"See where you're signed in and revoke old sessions\"],\"9mqQnx\":[\"Limit paragraphs used in automatically generated summaries.\"],\"A1taO8\":[\"Search\"],\"A7tRC9\":[\"Which post stream the canonical /feed URL returns.\"],\"ADTn/D\":[\"Allow published content to be read without an API token.\"],\"AELGTd\":[\"Add it to navigation now or open the editor to add details.\"],\"ANzR96\":[\"Config Editor\"],\"AeXO77\":[\"Account\"],\"AnY+O9\":[\"Show \\\"Build with Jant\\\" at the bottom of the home page\"],\"ApZDMk\":[\"This is used for your favicon and apple-touch-icon. For best results, upload a square PNG with a solid background at least 512x512 pixels.\"],\"Aysjjh\":[\"Choose the color palette used across Jant.\"],\"B495Gs\":[\"Archive\"],\"B4ESok\":[\"API reference\"],\"BJ0tsF\":[\"Search runtime settings and open advanced configuration\"],\"BTYdze\":[\"Link added to navigation.\"],\"BzEFor\":[\"or\"],\"C0/57J\":[\"This is the last part of the collection link.\"],\"CDAdlf\":[\"Remove bot\"],\"CTAEes\":[\"Select a repository\"],\"CjZZgz\":[\"This repository already has commits\"],\"Cl92kR\":[\"Create new collection\"],\"D8k2s6\":[\"Connect Telegram\"],\"DCKkhU\":[\"Current Password\"],\"DKKKeF\":[\"Manage password and hosted access in \",[\"providerLabel\"]],\"DVFXa6\":[\"The cleanest, most neutral option.\"],\"DdXUay\":[\"Show the site avatar in the public header.\"],\"EO3I6h\":[\"Upload didn't go through. Try again in a moment.\"],\"Eax/et\":[\"Accent\"],\"EbDLD+\":[\"Choose the typography used across Jant.\"],\"Enslfm\":[\"Destination\"],\"F53UDu\":[\"Cool white\"],\"F7FKwe\":[\"Anything you paste here has full access to your visitors' browsers. Only use code from sources you trust.\"],\"F8SNqr\":[\"Warm orange\"],\"FMUJSP\":[\"Account & Data\"],\"Fe1cvJ\":[\"Delay new posts and replies before they appear in feeds. Use 0 to turn this off.\"],\"Fk3SSD\":[\"Calm and natural, easy on the eyes.\"],\"FkMol5\":[\"Featured\"],\"G/1oP+\":[\"Remove the webhook and stop syncing. Your repository content will not be deleted.\"],\"G0qJsQ\":[\"Security token missing. Refresh the page and try again.\"],\"G0tHaW\":[\"Create a public page that won't appear in Latest.\"],\"G2fuEb\":[\"Locked\"],\"G39wnK\":[\"Back up and sync content with a GitHub repository\"],\"GMMWcy\":[\"Name, metadata, language, and search defaults\"],\"GXsAby\":[\"Revoke\"],\"GxkJXS\":[\"Uploading...\"],\"GzKzUa\":[\"Demo limits\"],\"H4x7Sk\":[\"The language this admin dashboard shows in. Available in English, 简体中文, and 繁體中文.\"],\"HCNlq3\":[\"Clean and high-contrast, close to white.\"],\"HHDyGw\":[\"Soft green\"],\"HKH+W+\":[\"Data\"],\"Hp1l6f\":[\"Current\"],\"HxlY7t\":[\"Changing this updates what subscribers get from /feed.\"],\"HxuOlm\":[\"Site Header\"],\"I4HRxU\":[\"Every published post, including ones hidden from Latest.\"],\"I6gXOa\":[\"Path\"],\"I76CzF\":[\"Neutral gray\"],\"ID38tA\":[\"Account deletion is off in demo mode. The shared demo resets separately.\"],\"IF9tPu\":[\"When to use site export, database backups, and recovery drills.\"],\"IW5PBo\":[\"Copy Token\"],\"IagCbF\":[\"URL\"],\"IfB3m6\":[\"The language your posts are written in. Announced to readers and search engines through HTML lang and your RSS feed. Any BCP 47 tag works.\"],\"IreQBq\":[\"Repository\"],\"J6bLeg\":[\"Add a custom link to any URL\"],\"JL7LF5\":[\"available CSS variables, data attributes, and examples.\"],\"JcD7qf\":[\"More actions\"],\"JjX0OO\":[\"Copy your token now — it won't be shown again.\"],\"JrFTcr\":[\"Connecting…\"],\"JuN5GC\":[\"No file selected. Choose a file to upload.\"],\"K+0Hu0\":[\"No matching pages available to add. Try another title or create a new page.\"],\"K/F6pa\":[\"Saving…\"],\"KDw4GX\":[\"Try again\"],\"KSgo21\":[\"Pick a repository\"],\"KVVYBh\":[\"Add collection to navigation\"],\"KiJn9B\":[\"Note\"],\"Kk7jwL\":[\"Set posts per archive page; reset to inherit the main page size.\"],\"KwOLJF\":[\"The original Jant palette, and a solid everyday pick.\"],\"L+rMC9\":[\"Reset to default\"],\"L27RpE\":[\"Page created.\"],\"L3DEwT\":[\"Remove this avatar? Your favicon and header icon will go back to the default.\"],\"L4t4/q\":[\"March 14\"],\"L86zmP\":[\"Edit the multi-line introduction used on your home page and in metadata.\"],\"LJOxNn\":[\"CJK fallback\"],\"LdyooL\":[\"link\"],\"LjwaJ/\":[\"Sign-in, export, and deletion\"],\"M/D8PK\":[\"+ Install on another account\"],\"M/haSd\":[\"Always show the light version of the theme.\"],\"M1co/O\":[\"Configured\"],\"M2kIWU\":[\"Font theme\"],\"M4tzVU\":[\"Latest posts\"],\"M6CbAU\":[\"Toggle edit panel\"],\"MHrjPM\":[\"Title\"],\"MKIM3K\":[\"Search pages\"],\"MaYYE6\":[\"Post notes by messaging a Telegram bot\"],\"Me5t5H\":[\"Connect a GitHub repository to automatically back up your posts as Markdown files. Edits on GitHub sync back to your site.\"],\"MnbH31\":[\"page\"],\"Mq9FZ1\":[\"Creating page…\"],\"Mr4QPw\":[\"Disconnect Telegram? You can reconnect any time with a new binding code.\"],\"MtENL9\":[\"Tune how your site looks, reads, and runs.\"],\"N/8NPV\":[\"Before deleting, download a site export. You won't be able to recover this account after deletion.\"],\"N7UNHY\":[\"Featured feed\"],\"NHnUHF\":[\"Favicon and the profile mark in your header\"],\"NU2Fqi\":[\"Save CSS\"],\"NVjhde\":[\"Warm parchment\"],\"Nldjdr\":[\"No custom URLs yet. Create one to add redirects or custom paths for posts.\"],\"O7rgs6\":[\"Header RSS points to your \",[\"feed\"],\" feed (/feed). Change what /feed returns in General.\"],\"OJxdgi\":[\"Keep this link under 200 characters.\"],\"OSJXFg\":[\"Applies to your entire site, including admin pages. Pick a palette, then choose whether it follows the system or stays fixed.\"],\"OeUWA7\":[\"Add Page\"],\"OuuMXJ\":[\"Add site-wide HTML before the closing body tag.\"],\"Ox3+3h\":[\"No matches.\"],\"PEUV5I\":[\"Code injection updated.\"],\"PHh52z\":[\"Warm and rich, with strong brown tones.\"],\"PXj9lw\":[\"Stop accepting posts from Telegram. Your existing notes stay published.\"],\"PZ7HJ8\":[\"Blog Avatar\"],\"Pbm2/N\":[\"Create Collection\"],\"Pwqkdw\":[\"Loading…\"],\"PxJ9W6\":[\"Generate Token\"],\"Q/6Y+2\":[\"Needs Contents (read/write) and Webhooks (read/write) on the target repository.\"],\"Q/O0X4\":[\"This setting wasn't saved. Check the value and try again.\"],\"Q30z/l\":[\"Remove this collection from navigation? The collection itself won't be deleted.\"],\"Q99OtV\":[\"Pin a collection to your navigation bar. An asterisk (*) appears next to collections updated in the last 48 hours.\"],\"QCwsv1\":[\"Warm off-white\"],\"QKvrmL\":[\"Warm-neutral and easy for long reading.\"],\"QZmz0H\":[\"Built-in links\"],\"Qnrzvb\":[\"Active Tokens\"],\"R6Z4LE\":[\"Download failed. Please try again.\"],\"R9Khdg\":[\"Auto\"],\"RcdDOS\":[\"Create a bot by messaging @BotFather on Telegram, then paste the token it gives you.\"],\"RdVIcf\":[\"Cream and coffee brown\"],\"Rn2p1h\":[\"Nothing matches this search. Try a different name or description.\"],\"RxsRD6\":[\"Time Zone\"],\"SDND4q\":[\"Not configured\"],\"SJmfuf\":[\"Site Name\"],\"SKZhW9\":[\"Token name\"],\"SSsoa4\":[\"Add to Navigation\"],\"SVQQPe\":[\"Couldn't connect. Check the error and try again.\"],\"SWb0z+\":[\"That address is reserved. Choose another one.\"],\"SchpMp\":[\"Telegram\"],\"SqKp3o\":[\"The title used across your site, browser tabs, and feeds.\"],\"SrGs4T\":[\"Light blue-gray background, cool and even.\"],\"T41PG1\":[\"Limit characters used in automatically generated summaries.\"],\"TN0mN4\":[\"Search and edit runtime settings. Changes apply immediately; reset restores the environment or built-in default.\"],\"TSCeuF\":[\"Couldn't create the collection. Check the details and try again.\"],\"TpF3v+\":[\"Injected before </head>. Use for analytics, custom meta tags, and styles that must load early.\"],\"Tu6bMZ\":[\"Create About page\"],\"Tz0i8g\":[\"Settings\"],\"U5v6Gh\":[\"Edit Page\"],\"UFK415\":[\"Site-wide HTML for analytics and widgets\"],\"UTvFQq\":[\"Open \",[\"linkOpen\"],\"@\",[\"botUsername\"],[\"linkClose\"],\" and send:\"],\"UUn+Y5\":[\"Open setting\"],\"UaZwcz\":[\"More options are available after you create it.\"],\"Uj/btJ\":[\"Display avatar in my site header\"],\"UsODUn\":[\"Select an account\"],\"UxKoFf\":[\"Navigation\"],\"V+bhUy\":[\"Install GitHub App\"],\"V0fyg5\":[\"Collection added to navigation.\"],\"V4WsyL\":[\"Add Link\"],\"V5pZwT\":[\"Search settings updated.\"],\"VXUPla\":[\"Connect with GitHub App\"],\"VhMDMg\":[\"Change Password\"],\"Vn3jYy\":[\"Navigation items\"],\"VoZYGU\":[\"This will permanently delete all your data — posts, media, collections, settings, and your account. Your blog will be reset to its initial setup state. This cannot be undone.\"],\"WUnFK2\":[\"Pale cool-white with deep indigo text. High contrast.\"],\"Wa9q4P\":[\"Pure white\"],\"Wb7EHo\":[\"About page\"],\"Weq9zb\":[\"General\"],\"Wi9i06\":[\"Follow each visitor's system preference.\"],\"Wildi8\":[\"No pages available. Create one to add it to navigation.\"],\"Wx1M8N\":[\"Install the GitHub App to grant access without managing personal tokens. Permissions are scoped per repository and revocable from GitHub.\"],\"X+8FMk\":[\"Current password doesn't match. Try again.\"],\"X1G9eY\":[\"Navigation Preview\"],\"X2Cbc2\":[\"Archive feed\"],\"X5P6yv\":[\"Recently updated\"],\"X9Hujr\":[\"Manual Push\"],\"Xsu5WL\":[\"Add site-wide CSS in the dedicated code editor.\"],\"XtBJV8\":[\"Checking repository…\"],\"Xtc16w\":[\"Refresh repository list\"],\"Y+7JGK\":[\"Create Page\"],\"Y/F35r\":[\"Create a post with curl:\"],\"Y/N5N7\":[\"Soft cream background with a muted green accent.\"],\"YF6zHf\":[\"Site settings updated.\"],\"YdG2RF\":[\"Export Site\"],\"YkgZi7\":[\"Connect a Telegram bot, then anything you message it gets published as a note.\"],\"YwhjRx\":[\"Manage Account\"],\"Yxp859\":[\"Warm cream\"],\"ZDY7Fy\":[\"Syncing…\"],\"ZQKLI1\":[\"Danger Zone\"],\"ZS/CBL\":[\"Delete this navigation link? Visitors won't see it in your site header anymore.\"],\"ZXZrAo\":[\"Keep the page address under 200 characters.\"],\"ZgZX+d\":[\"Pure white with neutral grays. No color tint.\"],\"Zgq+c2\":[\"Set the default number of items shown per page.\"],\"ZhhOwV\":[\"Quote\"],\"ZiooJI\":[\"API Tokens\"],\"Zm7Qb0\":[\"Backup & Restore Guide\"],\"ZmUkwN\":[\"Add custom link to navigation\"],\"a14mj8\":[\"Unknown device\"],\"a1iEgy\":[\"Warm cream background with deep coffee-brown accents.\"],\"a3LDKx\":[\"Security\"],\"aAIQg2\":[\"Appearance\"],\"aFkzVF\":[\"The slug of the target post or collection\"],\"aiSl4C\":[\"The serif fallback used when the content language has no built-in profile.\"],\"alKG0+\":[\"Font Theme\"],\"anibOb\":[\"About this blog\"],\"any7NR\":[\"Theming guide\"],\"b+/jO6\":[\"301 (Permanent)\"],\"b+FyBD\":[\"Add page to navigation\"],\"bHOiy1\":[\"Password changes are off in demo mode. Sign in with the shared demo credentials.\"],\"bHYIks\":[\"Sign Out\"],\"bV2vng\":[\"Warm terracotta\"],\"bbR5vW\":[\"This palette\"],\"bbzY7X\":[\"Manage sign-in security, site exports, and irreversible actions.\"],\"bfHZ7r\":[\"Choose the time zone used to display dates and times.\"],\"bi10qS\":[\"Add it to navigation now or open the editor to add content.\"],\"bmrL08\":[\"Demo mode hides sessions, password changes, and account deletion. Export still works.\"],\"brfpw9\":[\"Edit the multi-line footer rendered at the bottom of public pages.\"],\"bviiKV\":[\"Headings, body text, and links in this theme.\"],\"c3MN2z\":[\"all available endpoints and request formats.\"],\"cS7/bk\":[\"Remove the saved bot token? Its webhook is deleted and any connected account is disconnected.\"],\"cSDy01\":[\"Custom CSS updated.\"],\"clzoNp\":[\"Always show the dark version of the theme.\"],\"cnGeoo\":[\"Delete\"],\"cwaLCZ\":[\"Page added to navigation.\"],\"d3FRkY\":[\"Could not copy. Try again.\"],\"d5oGUo\":[\"Create a new repository on GitHub\"],\"dEgA5A\":[\"Cancel\"],\"dTXUY+\":[\"Confirm account deletion\"],\"dYKrp3\":[\"Hidden from Latest\"],\"dk7TCH\":[\"Permanently delete all data and reset the blog\"],\"ds0nJk\":[\"Add site-wide HTML before the closing head tag.\"],\"dsWkIw\":[\"Disconnect from GitHub? The webhook will be removed. Your repository content will not be deleted.\"],\"e/tSI5\":[\"Navigation order updated.\"],\"eOL7vn\":[\"Minimal color, low distraction.\"],\"ePK91l\":[\"Edit\"],\"ebQKK7\":[\"Site\"],\"egK+Yy\":[\"Bearer tokens for scripts and automation\"],\"ehj/zN\":[\"Redirect Type\"],\"eneWvv\":[\"Draft\"],\"erTMh7\":[\"Last synced\"],\"f+m8jj\":[\"Feed URL copied.\"],\"f8fH8W\":[\"Design\"],\"fDz6PV\":[\"Choose the repository used for content backups.\"],\"fKRAwQ\":[\"Sandy background with a green accent.\"],\"fWYqkz\":[\"Code Injection\"],\"fYXQnC\":[\"Cozy and bold among the warm tones.\"],\"fttd2R\":[\"My Collection\"],\"gZ5owP\":[\"Search repositories\"],\"gbqbh6\":[\"Safe to leave this page — syncing continues in the background.\"],\"gkFvVN\":[\"Injected before </body>. Use for chat widgets and scripts that should not block page load.\"],\"gtQsRO\":[\"Create Custom URL\"],\"hBO/y4\":[\"Security token expired. Refresh the page and try again.\"],\"hGmyDl\":[\"Tokens let you access the API from scripts, shortcuts, and other tools without signing in.\"],\"hIHkRy\":[\"Connected via GitHub App\"],\"hJHHsU\":[\"Publish Atom feeds for the site, archive, and collections.\"],\"hdSi1b\":[\"Type \",[\"repo\"],\" to confirm\"],\"he3ygx\":[\"Copy\"],\"hvGGMK\":[\"Remove this page from navigation? The page itself won't be deleted.\"],\"i0qMbr\":[\"Home\"],\"iEUzMn\":[\"system\"],\"iH8pgl\":[\"Back\"],\"iSLIjg\":[\"Connect\"],\"iVOMRi\":[\"Home settings updated.\"],\"icB4Cv\":[\"Drag links here to show them under the More menu\"],\"id3vuh\":[\"Telegram is set up, but the bot couldn't be reached. Check the bot token and try again.\"],\"idD8Ev\":[\"Saved\"],\"ihn4zD\":[\"Search…\"],\"iiDXZc\":[\"Displayed at the bottom of all posts and pages.\"],\"itS31B\":[\"A warmer look, like slightly aged paper.\"],\"iwQZvS\":[\"Cool blue-gray\"],\"j4VrG6\":[\"Download Export ZIP\"],\"j5nQL2\":[\"e.g. iOS Shortcuts\"],\"jUV7CU\":[\"Upload Avatar\"],\"jVUmOK\":[\"Markdown supported\"],\"jdVYS8\":[\"Pick the one that fits your writing.\"],\"jgBjXJ\":[\"Revoke this token? Any scripts using it will stop working.\"],\"jpctdh\":[\"View\"],\"k1ifdL\":[\"Processing...\"],\"kLw03Y\":[\"Bright paper white\"],\"kMXclu\":[\"Download a site export\"],\"kNiQp6\":[\"Pinned\"],\"kQ3Otm\":[\"Create new page\"],\"kRhzWq\":[\"GitHub Sync\"],\"kVQs7s\":[\"Fine-grained styling overrides\"],\"ke1gWS\":[\"Custom URLs\"],\"kfcRb0\":[\"Avatar\"],\"kp8wiR\":[\"Checking address…\"],\"kxDZ2i\":[\"This code runs on every page of your site.\"],\"l2Op2p\":[\"Query Parameters\"],\"lLW3vJ\":[\"Target Slug\"],\"lV04bQ\":[\"Warm and earthy, but still light.\"],\"lYHJih\":[\"Revoke this session? That device will need to sign in again.\"],\"m16xKo\":[\"Add\"],\"mLOk1i\":[\"Push all posts to GitHub right now instead of waiting for the next automatic sync.\"],\"mSNmrX\":[\"List posts:\"],\"n8+EXe\":[\"Add common destinations that already exist on your site.\"],\"nG2qTk\":[\"Example link\"],\"nK07ni\":[\"Choose a typographic direction for your site. Each theme changes both the font pairing and the reading rhythm.\"],\"nbfdhU\":[\"Integrations\"],\"ntJYyh\":[\"Domains, plan, and billing in \",[\"providerLabel\"]],\"o/vNDE\":[\"lets you override any theme variable.\"],\"o5rj+L\":[\"No collections yet. Create one here to add it to navigation.\"],\"oGC9uP\":[\"owner/repo\"],\"oH2JHg\":[\"We'll prefill the name \",[\"name\"],\". The list refreshes on return.\"],\"oKOOsY\":[\"Color Theme\"],\"oL535e\":[\"Not synced yet\"],\"oNA4If\":[\"All collections are already in your navigation.\"],\"oUn9Z7\":[\"Near-neutral light gray with a steel-blue accent.\"],\"ofdy2l\":[\"Orange-tinted background. The warmest palette.\"],\"pNXxtS\":[\"Choose the content language announced to readers and search engines.\"],\"pZq3aX\":[\"Upload failed. Please try again.\"],\"pgTIrt\":[\"Choose the GitHub account and repository to sync with this site.\"],\"psoxDF\":[\"That font theme isn't available. Pick another one.\"],\"pt4OhQ\":[\"/about is already used. Rename that item before creating an About page.\"],\"pvnfJD\":[\"Dark\"],\"q+hNag\":[\"Collection\"],\"q/T5GS\":[\"The language used by the private Jant dashboard.\"],\"qSgO/Y\":[\"Follow content language\"],\"qdcESc\":[\"Create a new repository\"],\"qkSJzV\":[\"Searching pages…\"],\"r5EW6f\":[\"This repository is already backing up another Jant site (\",[\"host\"],\"). Pick a different repository.\"],\"rEspiY\":[\"Navigation placement updated.\"],\"rFmBG3\":[\"Color theme\"],\"re0mF0\":[\"The page is public but stays out of Latest.\"],\"rlonmB\":[\"Couldn't delete. Try again in a moment.\"],\"s3jOk7\":[\"Enter a page title.\"],\"satWc6\":[\"Main RSS feed\"],\"sgr2wQ\":[\"collection\"],\"sj4LLc\":[\"Show only modified\"],\"slujBW\":[\"Use lowercase letters, numbers, and hyphens only.\"],\"soRdOu\":[\"Off-white paper with a deep forest-green accent.\"],\"sqxcaY\":[\"Created \",[\"date\"]],\"sxkWRg\":[\"Advanced\"],\"t/YqKh\":[\"Remove\"],\"t3hvHq\":[\"Sync Now\"],\"tJ4H0O\":[\"your Telegram account\"],\"tfDRzk\":[\"Save\"],\"tgWuMB\":[\"Modified\"],\"tvgAq5\":[\"No accounts authorized yet\"],\"u1VTd3\":[\"Palette, surface tone, and overall mood\"],\"u3wRF+\":[\"Published\"],\"u6KOjV\":[\"Want more control?\"],\"uKLe0J\":[[\"count\"],\" settings shown\"],\"uVZcIA\":[\"That address is already in use. Choose another one.\"],\"udPwLB\":[\"Header\"],\"ui6aMF\":[\"These devices are currently signed in to your account. Revoke any session you don't recognize.\"],\"vBEKwo\":[\"Manage this site's active sessions here. Password and hosted access are managed through \",[\"providerLabel\"],\".\"],\"vOnuOa\":[\"Show or hide built-in destinations in the header and More menu.\"],\"vRldcl\":[\"Typography choices and reading texture\"],\"vSYKYI\":[\"Main feed\"],\"vTuib7\":[\"This controls what /feed returns.\"],\"vdFnYM\":[\"Reset link\"],\"vmQmHx\":[\"Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements.\"],\"vzX5FB\":[\"Delete Account\"],\"w8Rv8T\":[\"Label is required\"],\"wL3cK8\":[\"Latest\"],\"wLSxGY\":[\"Used when the content language has no built-in font profile. Your font theme still controls serif and sans styling.\"],\"wW6NCp\":[\"Last error\"],\"wc+17X\":[\"/* Your custom CSS here */\"],\"wuLtXn\":[\"No active sessions right now. Signed-in devices show up here.\"],\"x+HGBk\":[\"Ask search engines not to index public pages or include them in results.\"],\"xCWek4\":[\"File storage isn't set up. Check your server config.\"],\"xHt036\":[\"Personal Access Token\"],\"xKeZ0l\":[\"An earthier, warmer tone.\"],\"xYbozN\":[\"Soft ivory\"],\"xqViCq\":[\"Warm ivory\"],\"xxaahd\":[\"When you want a cooler, sharper look.\"],\"y/awtV\":[\"Header links, built-in destinations, and overflow menu\"],\"y28hnO\":[\"Post\"],\"y8Md/V\":[\"Language and time updated.\"],\"y9W9vo\":[\"Creating collection…\"],\"yNCqOt\":[\"Latest feed\"],\"yQ3kNF\":[\"Type the following phrase to confirm:\"],\"ydq1k2\":[\"Pick an account first\"],\"yjjCV8\":[\"Fixed feed URLs\"],\"yjkELF\":[\"Confirm New Password\"],\"ym+ccl\":[\"Show the Jant credit on the home page.\"],\"yzF66j\":[\"Link\"],\"z6wakA\":[\"A short intro shown on your home page.\"],\"zEizrk\":[\"Last used \",[\"date\"]],\"zSURJW\":[\"No repositories match.\"],\"zUlHGd\":[\"Page address\"],\"zXH2jX\":[\"Language & Time\"],\"zcDmsG\":[\"Featured posts\"],\"zlcDd2\":[\"Delete this custom URL? Visitors using it won't be redirected anymore.\"],\"zwBp5t\":[\"Private\"]}");
|
|
@@ -3456,10 +3456,10 @@ function normalizeThemeColorForMeta(color) {
|
|
|
3456
3456
|
* internal paths (e.g. `/_assets/client-HASH.js`) embedded by the Worker build
|
|
3457
3457
|
* from the Vite client manifest. Used only in production (IS_VITE_DEV=false).
|
|
3458
3458
|
*/ var IS_VITE_DEV = typeof __JANT_DEV__ !== "undefined" && __JANT_DEV__ === true;
|
|
3459
|
-
var CORE_VERSION = "0.6.
|
|
3460
|
-
var CLIENT_JS_FILE = "/_assets/client-
|
|
3461
|
-
var CLIENT_AUTH_JS_FILE = "/_assets/client-auth-
|
|
3462
|
-
var CLIENT_CSS_FILE = "/_assets/client-
|
|
3459
|
+
var CORE_VERSION = "0.6.16-31fe43de750ab168";
|
|
3460
|
+
var CLIENT_JS_FILE = "/_assets/client-Bq0vre8Y.js";
|
|
3461
|
+
var CLIENT_AUTH_JS_FILE = "/_assets/client-auth-F2rb9WfF.js";
|
|
3462
|
+
var CLIENT_CSS_FILE = "/_assets/client-D3VZyG4L.css";
|
|
3463
3463
|
var CLIENT_CJK_CSS_FILE = "/_assets/client-cjk-B7Z0snDu.css";
|
|
3464
3464
|
var CLIENT_CJK_TC_CSS_FILE = "/_assets/client-cjk-tc-BesJYrb2.css";
|
|
3465
3465
|
var CLIENT_CJK_JP_CSS_FILE = "/_assets/client-cjk-jp-DZwrTzQC.css";
|
|
@@ -3819,6 +3819,10 @@ var CUSTOM_SYMBOLS = {
|
|
|
3819
3819
|
viewBox: "0 0 24 24",
|
|
3820
3820
|
inner: `<line ${STROKE_POST_BADGE} x1="12" x2="12" y1="17" y2="22" /><path ${STROKE_POST_BADGE} d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z" />`
|
|
3821
3821
|
},
|
|
3822
|
+
"post-status-draft": {
|
|
3823
|
+
viewBox: "0 0 24 24",
|
|
3824
|
+
inner: `<path ${STROKE_POST_BADGE} d="M12 20h9" /><path ${STROKE_POST_BADGE} d="M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z" />`
|
|
3825
|
+
},
|
|
3822
3826
|
"post-status-private": {
|
|
3823
3827
|
viewBox: "0 0 24 24",
|
|
3824
3828
|
inner: `<path ${STROKE_POST_BADGE} d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" /><path ${STROKE_POST_BADGE} d="M14.084 14.158a3 3 0 0 1-4.242-4.242" /><path ${STROKE_POST_BADGE} d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" /><path ${STROKE_POST_BADGE} d="m2 2 20 20" />`
|
|
@@ -4004,7 +4008,7 @@ var IconSprite = () => {
|
|
|
4004
4008
|
const cjkFontProfile = resolveCjkFontProfile(appConfig?.siteLanguage ?? resolvedLang, cjkSerifFont);
|
|
4005
4009
|
const cjkStylesheetPath = cjkFontProfile === "zh-Hans" ? IS_VITE_DEV ? assetPath("/src/style-cjk.css") : toPublicAssetPath(CLIENT_CJK_CSS_FILE, assetBasePath) : cjkFontProfile === "zh-Hant" ? IS_VITE_DEV ? assetPath("/src/style-cjk-tc.css") : toPublicAssetPath(CLIENT_CJK_TC_CSS_FILE, assetBasePath) : cjkFontProfile === "ja" ? IS_VITE_DEV ? assetPath("/src/style-cjk-jp.css") : toPublicAssetPath(CLIENT_CJK_JP_CSS_FILE, assetBasePath) : cjkFontProfile === "ko" ? IS_VITE_DEV ? assetPath("/src/style-cjk-kr.css") : toPublicAssetPath(CLIENT_CJK_KR_CSS_FILE, assetBasePath) : null;
|
|
4006
4010
|
const clientScriptPath = IS_VITE_DEV ? resolvedClientBundle === "full" ? assetPath("/src/client-auth.ts") : assetPath("/src/client.ts") : toPublicAssetPath(resolvedClientBundle === "full" ? CLIENT_AUTH_JS_FILE : CLIENT_JS_FILE, assetBasePath);
|
|
4007
|
-
const faviconAssetVersion = resolvedFaviconVersion || "0.6.
|
|
4011
|
+
const faviconAssetVersion = resolvedFaviconVersion || "0.6.16-31fe43de750ab168";
|
|
4008
4012
|
const resolvedFaviconHref = faviconHref ?? (faviconAssetVersion ? toPublicPath(`/favicon.ico?v=${faviconAssetVersion}`, sitePathPrefix) : toPublicPath("/favicon.ico", sitePathPrefix));
|
|
4009
4013
|
const resolvedAppleTouchHref = appleTouchHref ?? (faviconAssetVersion ? toPublicPath(`/apple-touch-icon.png?v=${faviconAssetVersion}`, sitePathPrefix) : toPublicPath("/apple-touch-icon.png", sitePathPrefix));
|
|
4010
4014
|
const socialImageHref = resolvedSocialImagePath ? toAbsoluteAssetUrl(resolvedSocialImagePath, appConfig?.siteUrl || "", sitePathPrefix) : "";
|
|
@@ -6731,6 +6735,28 @@ function clipPreviewText(text, maxChars) {
|
|
|
6731
6735
|
};
|
|
6732
6736
|
}
|
|
6733
6737
|
/**
|
|
6738
|
+
* Resolves the unpublished draft sitting at the end of a Thread, if any.
|
|
6739
|
+
*
|
|
6740
|
+
* Both maps come from the same ordering over the same rows, differing only in
|
|
6741
|
+
* whether drafts are eligible — so when they disagree, the draft-inclusive
|
|
6742
|
+
* answer is by definition unpublished. No status lookup needed.
|
|
6743
|
+
*
|
|
6744
|
+
* @param threadId - Thread root ID to look up
|
|
6745
|
+
* @param publishedTails - Tails from `getThreadTailIds(ids)`
|
|
6746
|
+
* @param draftInclusiveTails - Tails from `getThreadTailIds(ids, { includeDrafts: true })`
|
|
6747
|
+
* @returns The trailing draft's Post ID, or undefined when the Thread ends
|
|
6748
|
+
* on a published Post
|
|
6749
|
+
* @example
|
|
6750
|
+
* ```ts
|
|
6751
|
+
* const draftTailId = resolveDraftTailId(post.threadId, tails, draftTails);
|
|
6752
|
+
* if (draftTailId) view.draftTailId = draftTailId;
|
|
6753
|
+
* ```
|
|
6754
|
+
*/ function resolveDraftTailId(threadId, publishedTails, draftInclusiveTails) {
|
|
6755
|
+
const withDrafts = draftInclusiveTails.get(threadId);
|
|
6756
|
+
if (!withDrafts) return void 0;
|
|
6757
|
+
return withDrafts === publishedTails.get(threadId) ? void 0 : withDrafts;
|
|
6758
|
+
}
|
|
6759
|
+
/**
|
|
6734
6760
|
* Batch converts PostWithMedia[] to PostView[].
|
|
6735
6761
|
*
|
|
6736
6762
|
* @param posts - Posts with media attachments
|
|
@@ -7128,7 +7154,7 @@ var getCollectionMutationLabels = (i18n) => ({
|
|
|
7128
7154
|
saveDraft: i18n._({ id: "Z6NwTi" }),
|
|
7129
7155
|
saveAsDraft: i18n._({ id: "8tM8+a" }),
|
|
7130
7156
|
discard: i18n._({ id: "bzSI52" }),
|
|
7131
|
-
titlePlaceholder: i18n._({ id: "
|
|
7157
|
+
titlePlaceholder: i18n._({ id: "wja8aL" }),
|
|
7132
7158
|
bodyPlaceholder: i18n._({ id: "jvyYZG" }),
|
|
7133
7159
|
urlPlaceholder: i18n._({ id: "heSQoS" }),
|
|
7134
7160
|
urlInvalid: i18n._({ id: "Mc7+6G" }),
|
|
@@ -7209,13 +7235,13 @@ var getCollectionMutationLabels = (i18n) => ({
|
|
|
7209
7235
|
publishDateReset: i18n._({ id: "6WAK+2" }),
|
|
7210
7236
|
publishDateInvalid: i18n._({ id: "DSJXZM" }),
|
|
7211
7237
|
publishDateFutureError: i18n._({ id: "5dcjwM" }),
|
|
7212
|
-
publishDateSummaryNow: i18n._({ id: "
|
|
7238
|
+
publishDateSummaryNow: i18n._({ id: "cH5kXP" }),
|
|
7213
7239
|
publishDateSummaryAction: i18n._({ id: "rV8ZnP" }),
|
|
7214
7240
|
publishSlugLabel: i18n._({ id: "lb+Xwx" }),
|
|
7215
7241
|
publishSlugPlaceholder: i18n._({ id: "s9gHf5" }),
|
|
7216
7242
|
publishSlugHint: i18n._({ id: "4eiXo+" }),
|
|
7217
7243
|
publishSlugAuto: i18n._({ id: "c2JRUS" }),
|
|
7218
|
-
publishSlugSummaryAuto: i18n._({ id: "
|
|
7244
|
+
publishSlugSummaryAuto: i18n._({ id: "eYZPF3" }),
|
|
7219
7245
|
publishSlugSummaryAction: i18n._({ id: "i6kro6" }),
|
|
7220
7246
|
publishSlugReset: i18n._({ id: "vdFnYM" }),
|
|
7221
7247
|
publishSlugSuggested: i18n._({ id: "w0Emel" }),
|
|
@@ -7231,9 +7257,9 @@ var getCollectionMutationLabels = (i18n) => ({
|
|
|
7231
7257
|
addCollection: i18n._({ id: "3Cw1AI" }),
|
|
7232
7258
|
collectionCountLabel: i18n._({ id: "KlZ+t+" }),
|
|
7233
7259
|
draftRestored: i18n._({ id: "DOx286" }),
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7260
|
+
closeCompose: i18n._({ id: "aHd4P7" }),
|
|
7261
|
+
editing: i18n._({ id: "gGx5tM" }),
|
|
7262
|
+
composeDialogLabel: i18n._({ id: "QOhkyl" }),
|
|
7237
7263
|
slashHint: i18n._({ id: "jrsUoG" }),
|
|
7238
7264
|
tableControls: {
|
|
7239
7265
|
toolbarLabel: i18n._({ id: "q+bMmy" }),
|
|
@@ -7266,7 +7292,7 @@ var getCollectionMutationLabels = (i18n) => ({
|
|
|
7266
7292
|
...slashCommandDiscovered ? { "slash-command-discovered": "" } : {},
|
|
7267
7293
|
children: /* @__PURE__ */ jsxDEV$1("div", {
|
|
7268
7294
|
class: "compose-dialog-inner",
|
|
7269
|
-
children:
|
|
7295
|
+
children: /* @__PURE__ */ jsxDEV$1("div", { class: "compose-body skel-section-md" })
|
|
7270
7296
|
})
|
|
7271
7297
|
});
|
|
7272
7298
|
};
|
|
@@ -8007,11 +8033,14 @@ var SiteLayout = ({ siteName, links, currentPath, sitePathPrefix = "", isAuthent
|
|
|
8007
8033
|
if (posts.length === 0) return [];
|
|
8008
8034
|
const postIds = posts.map((p) => p.id);
|
|
8009
8035
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
8010
|
-
const
|
|
8036
|
+
const threadIds = posts.map((p) => p.threadId);
|
|
8037
|
+
const [rawMediaMap, collectionsMap, threadContexts, aliasesMap, publishedTails, draftInclusiveTails] = await Promise.all([
|
|
8011
8038
|
c.var.services.media.getByPostIds(postIds),
|
|
8012
8039
|
c.var.services.collections.getCollectionsByPostIds(postIds),
|
|
8013
8040
|
c.var.services.posts.getThreadTimelineContext(postIds),
|
|
8014
|
-
c.var.services.paths.getPostAliases(postIds)
|
|
8041
|
+
c.var.services.paths.getPostAliases(postIds),
|
|
8042
|
+
c.var.isAuthenticated ? c.var.services.posts.getThreadTailIds(threadIds) : Promise.resolve(/* @__PURE__ */ new Map()),
|
|
8043
|
+
c.var.isAuthenticated ? c.var.services.posts.getThreadTailIds(threadIds, { includeDrafts: true }) : Promise.resolve(/* @__PURE__ */ new Map())
|
|
8015
8044
|
]);
|
|
8016
8045
|
const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl, mediaCtx.localPublicUrl, mediaCtx.sitePathPrefix);
|
|
8017
8046
|
const contextPostIds = /* @__PURE__ */ new Set();
|
|
@@ -8035,6 +8064,7 @@ var SiteLayout = ({ siteName, links, currentPath, sitePathPrefix = "", isAuthent
|
|
|
8035
8064
|
...post,
|
|
8036
8065
|
mediaAttachments: mediaMap.get(post.id) ?? []
|
|
8037
8066
|
}, mediaCtx, collectionsMap.get(post.id), void 0, firstAlias(post.id));
|
|
8067
|
+
const draftTailId = resolveDraftTailId(post.threadId, publishedTails, draftInclusiveTails);
|
|
8038
8068
|
const threadCtx = threadContexts.get(post.id);
|
|
8039
8069
|
if (threadCtx) {
|
|
8040
8070
|
postView.isLastInThread = false;
|
|
@@ -8042,19 +8072,24 @@ var SiteLayout = ({ siteName, links, currentPath, sitePathPrefix = "", isAuthent
|
|
|
8042
8072
|
...reply,
|
|
8043
8073
|
mediaAttachments: contextMediaMap.get(reply.id) ?? []
|
|
8044
8074
|
}, mediaCtx, contextCollectionsMap.get(reply.id), false, firstContextAlias(reply.id));
|
|
8075
|
+
const leadingReplyViews = threadCtx.leadingReplies.map(toContextPostView);
|
|
8076
|
+
const trailingReplyViews = threadCtx.trailingReplies.map(toContextPostView);
|
|
8077
|
+
const latestReplyView = toPostView({
|
|
8078
|
+
...threadCtx.latestReply,
|
|
8079
|
+
mediaAttachments: contextMediaMap.get(threadCtx.latestReply.id) ?? []
|
|
8080
|
+
}, mediaCtx, contextCollectionsMap.get(threadCtx.latestReply.id), true, firstContextAlias(threadCtx.latestReply.id));
|
|
8081
|
+
if (draftTailId) latestReplyView.draftTailId = draftTailId;
|
|
8045
8082
|
return {
|
|
8046
8083
|
post: postView,
|
|
8047
8084
|
threadPreview: {
|
|
8048
|
-
leadingReplies:
|
|
8049
|
-
trailingReplies:
|
|
8050
|
-
latestReply:
|
|
8051
|
-
...threadCtx.latestReply,
|
|
8052
|
-
mediaAttachments: contextMediaMap.get(threadCtx.latestReply.id) ?? []
|
|
8053
|
-
}, mediaCtx, contextCollectionsMap.get(threadCtx.latestReply.id), true, firstContextAlias(threadCtx.latestReply.id)),
|
|
8085
|
+
leadingReplies: leadingReplyViews,
|
|
8086
|
+
trailingReplies: trailingReplyViews,
|
|
8087
|
+
latestReply: latestReplyView,
|
|
8054
8088
|
totalReplyCount: threadCtx.totalReplyCount
|
|
8055
8089
|
}
|
|
8056
8090
|
};
|
|
8057
8091
|
}
|
|
8092
|
+
if (draftTailId) postView.draftTailId = draftTailId;
|
|
8058
8093
|
return { post: postView };
|
|
8059
8094
|
});
|
|
8060
8095
|
}
|
|
@@ -9571,6 +9606,8 @@ var MediaGallery = ({ attachments, postPermalink }) => {
|
|
|
9571
9606
|
...post.pinned ? { "data-post-pinned": "" } : {},
|
|
9572
9607
|
...post.pinnedInCollection ? { "data-post-pinned-in-collection": "" } : {},
|
|
9573
9608
|
...post.featured ? { "data-post-featured": "" } : {},
|
|
9609
|
+
...post.status === "draft" ? { "data-post-draft": "" } : {},
|
|
9610
|
+
...post.draftTailId ? { "data-thread-draft-tail-id": post.draftTailId } : {},
|
|
9574
9611
|
"data-post-visibility": post.visibility,
|
|
9575
9612
|
...isChildPost ? { "data-post-reply": "" } : {}
|
|
9576
9613
|
};
|
|
@@ -9673,7 +9710,10 @@ var MediaGallery = ({ attachments, postPermalink }) => {
|
|
|
9673
9710
|
};
|
|
9674
9711
|
var PostPublishedLink = ({ post, className }) => {
|
|
9675
9712
|
const { i18n } = useLingui();
|
|
9676
|
-
const publishedLabel = i18n._({ id: "
|
|
9713
|
+
const publishedLabel = post.status === "draft" ? i18n._({ id: "w1bUkO" }, {
|
|
9714
|
+
date: post.publishedAtFormatted,
|
|
9715
|
+
time: post.publishedAtTime
|
|
9716
|
+
}) : i18n._({ id: "tSWVu5" }, {
|
|
9677
9717
|
date: post.publishedAtFormatted,
|
|
9678
9718
|
time: post.publishedAtTime
|
|
9679
9719
|
});
|
|
@@ -9774,20 +9814,29 @@ var PostFooter = ({ post, detail, display }) => {
|
|
|
9774
9814
|
* toggle badges instantly without a page reload. Featured is rendered in the
|
|
9775
9815
|
* footer meta instead.
|
|
9776
9816
|
*/ var PostStatusBadges = () => {
|
|
9817
|
+
const { i18n } = useLingui();
|
|
9818
|
+
const pinnedLabel = i18n._({ id: "kNiQp6" });
|
|
9777
9819
|
return /* @__PURE__ */ jsxDEV$1("div", {
|
|
9778
9820
|
class: "post-status-badges",
|
|
9779
9821
|
children: [
|
|
9780
9822
|
/* @__PURE__ */ jsxDEV$1("span", {
|
|
9781
9823
|
class: "post-status-badge post-status-pinned",
|
|
9782
|
-
children: [/* @__PURE__ */ jsxDEV$1(Icon$1, { name: "post-status-pin" }),
|
|
9824
|
+
children: [/* @__PURE__ */ jsxDEV$1(Icon$1, { name: "post-status-pin" }), pinnedLabel]
|
|
9783
9825
|
}),
|
|
9784
9826
|
/* @__PURE__ */ jsxDEV$1("span", {
|
|
9785
9827
|
class: "post-status-badge post-status-pinned-in-collection",
|
|
9786
|
-
children: [/* @__PURE__ */ jsxDEV$1(Icon$1, { name: "post-status-pin" }),
|
|
9828
|
+
children: [/* @__PURE__ */ jsxDEV$1(Icon$1, { name: "post-status-pin" }), pinnedLabel]
|
|
9787
9829
|
}),
|
|
9788
9830
|
/* @__PURE__ */ jsxDEV$1("span", {
|
|
9789
9831
|
class: "post-status-badge post-status-private",
|
|
9790
|
-
children: [/* @__PURE__ */ jsxDEV$1(Icon$1, { name: "post-status-private" }), "
|
|
9832
|
+
children: [/* @__PURE__ */ jsxDEV$1(Icon$1, { name: "post-status-private" }), i18n._({ id: "zwBp5t" })]
|
|
9833
|
+
}),
|
|
9834
|
+
/* @__PURE__ */ jsxDEV$1("button", {
|
|
9835
|
+
type: "button",
|
|
9836
|
+
class: "post-status-badge post-status-draft",
|
|
9837
|
+
"data-draft-continue": true,
|
|
9838
|
+
title: i18n._({ id: "WIF6ui" }),
|
|
9839
|
+
children: [/* @__PURE__ */ jsxDEV$1(Icon$1, { name: "post-status-draft" }), i18n._({ id: "eneWvv" })]
|
|
9791
9840
|
})
|
|
9792
9841
|
]
|
|
9793
9842
|
});
|
|
@@ -9899,20 +9948,36 @@ var NoteCard = ({ post, mode = "feed", display }) => {
|
|
|
9899
9948
|
});
|
|
9900
9949
|
};
|
|
9901
9950
|
//#endregion
|
|
9951
|
+
//#region src/lib/link-preview.ts
|
|
9952
|
+
var LINK_PREVIEW_PROVIDER_LABELS = {
|
|
9953
|
+
youtube: "YouTube",
|
|
9954
|
+
vimeo: "Vimeo",
|
|
9955
|
+
bilibili: "Bilibili"
|
|
9956
|
+
};
|
|
9957
|
+
/**
|
|
9958
|
+
* Return the display label for a recognized Link preview provider.
|
|
9959
|
+
*
|
|
9960
|
+
* @param provider - Stored provider identifier, such as `youtube`
|
|
9961
|
+
* @returns A stable display label, or undefined for unknown providers
|
|
9962
|
+
* @example
|
|
9963
|
+
* ```ts
|
|
9964
|
+
* getLinkPreviewProviderLabel("youtube"); // "YouTube"
|
|
9965
|
+
* getLinkPreviewProviderLabel("unknown"); // undefined
|
|
9966
|
+
* ```
|
|
9967
|
+
*/ function getLinkPreviewProviderLabel(provider) {
|
|
9968
|
+
const normalizedProvider = provider?.trim().toLowerCase();
|
|
9969
|
+
return normalizedProvider ? LINK_PREVIEW_PROVIDER_LABELS[normalizedProvider] : void 0;
|
|
9970
|
+
}
|
|
9971
|
+
//#endregion
|
|
9902
9972
|
//#region src/ui/feed/LinkPreview.tsx
|
|
9903
9973
|
/**
|
|
9904
9974
|
* Link Preview
|
|
9905
9975
|
*
|
|
9906
9976
|
* Renders a preview thumbnail for link posts with recognized external content.
|
|
9907
9977
|
* For video providers (YouTube, etc.) shows a play button overlay and provider badge.
|
|
9908
|
-
*/ var
|
|
9909
|
-
youtube: "YouTube",
|
|
9910
|
-
vimeo: "Vimeo",
|
|
9911
|
-
bilibili: "Bilibili"
|
|
9912
|
-
};
|
|
9913
|
-
var LinkPreview = ({ imageUrl, linkUrl, kind, provider }) => {
|
|
9978
|
+
*/ var LinkPreview = ({ imageUrl, linkUrl, kind, provider }) => {
|
|
9914
9979
|
const isVideo = kind === "video";
|
|
9915
|
-
const providerLabel = provider
|
|
9980
|
+
const providerLabel = getLinkPreviewProviderLabel(provider);
|
|
9916
9981
|
return /* @__PURE__ */ jsxDEV$1("a", {
|
|
9917
9982
|
href: linkUrl,
|
|
9918
9983
|
class: "link-preview",
|
|
@@ -10679,7 +10744,7 @@ var PostPage = ({ post, threadPosts, isPreview = false }) => {
|
|
|
10679
10744
|
};
|
|
10680
10745
|
//#endregion
|
|
10681
10746
|
//#region src/ui/shared/DraftPreviewBar.tsx
|
|
10682
|
-
/** Persistent page chrome that distinguishes an authenticated draft preview. */ var DraftPreviewBar = () => {
|
|
10747
|
+
/** Persistent page chrome that distinguishes an authenticated draft preview. */ var DraftPreviewBar = ({ editHref }) => {
|
|
10683
10748
|
const { i18n } = useLingui();
|
|
10684
10749
|
const label = i18n._({ id: "27KTPP" });
|
|
10685
10750
|
return /* @__PURE__ */ jsxDEV$1("aside", {
|
|
@@ -10688,15 +10753,22 @@ var PostPage = ({ post, threadPosts, isPreview = false }) => {
|
|
|
10688
10753
|
"data-preview-status": true,
|
|
10689
10754
|
children: /* @__PURE__ */ jsxDEV$1("div", {
|
|
10690
10755
|
class: "draft-preview-bar-inner",
|
|
10691
|
-
children: [/* @__PURE__ */ jsxDEV$1("
|
|
10692
|
-
class: "draft-preview-bar-
|
|
10756
|
+
children: [/* @__PURE__ */ jsxDEV$1("div", {
|
|
10757
|
+
class: "draft-preview-bar-copy",
|
|
10693
10758
|
children: [/* @__PURE__ */ jsxDEV$1("span", {
|
|
10694
|
-
class: "draft-preview-bar-
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
10699
|
-
|
|
10759
|
+
class: "draft-preview-bar-label",
|
|
10760
|
+
children: [/* @__PURE__ */ jsxDEV$1("span", {
|
|
10761
|
+
class: "draft-preview-bar-dot",
|
|
10762
|
+
"aria-hidden": "true"
|
|
10763
|
+
}), label]
|
|
10764
|
+
}), /* @__PURE__ */ jsxDEV$1("span", {
|
|
10765
|
+
class: "draft-preview-bar-description",
|
|
10766
|
+
children: i18n._({ id: "0Vlj/m" })
|
|
10767
|
+
})]
|
|
10768
|
+
}), /* @__PURE__ */ jsxDEV$1("a", {
|
|
10769
|
+
class: "draft-preview-bar-action",
|
|
10770
|
+
href: editHref,
|
|
10771
|
+
children: i18n._({ id: "Oo7//P" })
|
|
10700
10772
|
})]
|
|
10701
10773
|
})
|
|
10702
10774
|
});
|
|
@@ -10810,17 +10882,21 @@ function canViewPost(post, options = {}) {
|
|
|
10810
10882
|
const post = await c.var.services.posts.getById(postId);
|
|
10811
10883
|
if (!post || !canViewPost(post, options)) return null;
|
|
10812
10884
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
10813
|
-
const [rawMediaMap, collectionsMap, lastPostMap, aliasesMap] = await Promise.all([
|
|
10885
|
+
const [rawMediaMap, collectionsMap, lastPostMap, draftTailMap, aliasesMap] = await Promise.all([
|
|
10814
10886
|
c.var.services.media.getByPostIds([post.id]),
|
|
10815
10887
|
c.var.services.collections.getCollectionsByPostIds([post.id]),
|
|
10816
|
-
c.var.services.posts.
|
|
10888
|
+
c.var.services.posts.getThreadTailIds([post.threadId]),
|
|
10889
|
+
c.var.isAuthenticated ? c.var.services.posts.getThreadTailIds([post.threadId], { includeDrafts: true }) : Promise.resolve(/* @__PURE__ */ new Map()),
|
|
10817
10890
|
c.var.services.paths.getPostAliases([post.id])
|
|
10818
10891
|
]);
|
|
10819
10892
|
const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl, mediaCtx.localPublicUrl, mediaCtx.sitePathPrefix);
|
|
10820
|
-
|
|
10893
|
+
const view = toPostView({
|
|
10821
10894
|
...post,
|
|
10822
10895
|
mediaAttachments: mediaMap.get(post.id) ?? []
|
|
10823
10896
|
}, mediaCtx, collectionsMap.get(post.id), lastPostMap.get(post.threadId) === post.id, aliasesMap.get(post.id)?.[0]);
|
|
10897
|
+
const draftTailId = resolveDraftTailId(post.threadId, lastPostMap, draftTailMap);
|
|
10898
|
+
if (draftTailId) view.draftTailId = draftTailId;
|
|
10899
|
+
return view;
|
|
10824
10900
|
}
|
|
10825
10901
|
/**
|
|
10826
10902
|
* Assembles the post permalink view, including the full thread when needed.
|
|
@@ -10833,7 +10909,8 @@ function canViewPost(post, options = {}) {
|
|
|
10833
10909
|
const post = typeof postOrId === "string" ? await c.var.services.posts.getById(postOrId) : postOrId;
|
|
10834
10910
|
if (!post || !canViewPost(post, options)) return null;
|
|
10835
10911
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
10836
|
-
const
|
|
10912
|
+
const includeDrafts = Boolean(options?.includeDraftThread || c.var.isAuthenticated);
|
|
10913
|
+
const threadPosts = (await c.var.services.posts.getThread(post.threadId)).filter((threadPost) => threadPost.status === "published" || includeDrafts && threadPost.status === "draft");
|
|
10837
10914
|
const allPostIds = threadPosts.length > 1 ? threadPosts.map((p) => p.id) : [post.id];
|
|
10838
10915
|
const [rawMediaMap, collectionsMap, aliasesMap] = await Promise.all([
|
|
10839
10916
|
c.var.services.media.getByPostIds(allPostIds),
|
|
@@ -10850,9 +10927,11 @@ function canViewPost(post, options = {}) {
|
|
|
10850
10927
|
...threadPost,
|
|
10851
10928
|
mediaAttachments: mediaMap.get(threadPost.id) ?? []
|
|
10852
10929
|
}, mediaCtx, collectionsMap.get(threadPost.id), index === threadPosts.length - 1, firstAlias(threadPost.id))) : void 0;
|
|
10853
|
-
const
|
|
10854
|
-
const
|
|
10855
|
-
const
|
|
10930
|
+
const publishedViews = threadPostViews?.filter((view) => view.status === "published");
|
|
10931
|
+
const metaViews = publishedViews?.length ? publishedViews : [postView];
|
|
10932
|
+
const socialImage = resolvePostSocialImage(postView, metaViews);
|
|
10933
|
+
const rootView = metaViews[0];
|
|
10934
|
+
const articleModifiedTime = metaViews.map((p) => p.updatedAt).reduce((latest, t) => t > latest ? t : latest);
|
|
10856
10935
|
return {
|
|
10857
10936
|
postView,
|
|
10858
10937
|
threadPostViews,
|
|
@@ -11076,8 +11155,10 @@ var posts$1 = sqliteTable("post", {
|
|
|
11076
11155
|
previewProvider: text("preview_provider"),
|
|
11077
11156
|
replyToId: text("reply_to_id"),
|
|
11078
11157
|
threadId: text("thread_id").notNull(),
|
|
11158
|
+
quietReply: integer("quiet_reply", { mode: "boolean" }).notNull().default(false),
|
|
11079
11159
|
publishedAt: integer("published_at"),
|
|
11080
11160
|
lastActivityAt: integer("last_activity_at"),
|
|
11161
|
+
threadUpdatedAt: integer("thread_updated_at"),
|
|
11081
11162
|
createdAt: integer("created_at").notNull(),
|
|
11082
11163
|
updatedAt: integer("updated_at").notNull()
|
|
11083
11164
|
}, (table) => [
|
|
@@ -11103,6 +11184,7 @@ var posts$1 = sqliteTable("post", {
|
|
|
11103
11184
|
index("idx_post_site_status_published").on(table.siteId, table.status, table.publishedAt),
|
|
11104
11185
|
index("idx_post_site_status_activity").on(table.siteId, table.status, table.lastActivityAt),
|
|
11105
11186
|
index("idx_post_site_root_published_activity").on(table.siteId, table.lastActivityAt, table.id).where(sql`${table.replyToId} IS NULL AND ${table.status} = 'published'`),
|
|
11187
|
+
index("idx_post_site_root_thread_updated").on(table.siteId, table.threadUpdatedAt, table.id).where(sql`${table.replyToId} IS NULL AND ${table.status} = 'published'`),
|
|
11106
11188
|
index("idx_post_site_root_draft_updated").on(table.siteId, table.updatedAt, table.id).where(sql`${table.replyToId} IS NULL AND ${table.status} = 'draft'`),
|
|
11107
11189
|
index("idx_post_site_reply_thread_created").on(table.siteId, table.threadId, table.createdAt, table.id).where(sql`${table.replyToId} IS NOT NULL AND ${table.status} = 'published'`),
|
|
11108
11190
|
index("idx_post_site_featured_thread_published").on(table.siteId, table.threadId, table.publishedAt, table.id).where(sql`${table.status} = 'published' AND ${table.featuredAt} IS NOT NULL`)
|
|
@@ -11700,8 +11782,10 @@ var posts = pgTable("post", {
|
|
|
11700
11782
|
previewProvider: text$1("preview_provider"),
|
|
11701
11783
|
replyToId: text$1("reply_to_id"),
|
|
11702
11784
|
threadId: text$1("thread_id").notNull(),
|
|
11785
|
+
quietReply: boolean("quiet_reply").notNull().default(false),
|
|
11703
11786
|
publishedAt: integer$1("published_at"),
|
|
11704
11787
|
lastActivityAt: integer$1("last_activity_at"),
|
|
11788
|
+
threadUpdatedAt: integer$1("thread_updated_at"),
|
|
11705
11789
|
createdAt: integer$1("created_at").notNull(),
|
|
11706
11790
|
updatedAt: integer$1("updated_at").notNull()
|
|
11707
11791
|
}, (table) => [
|
|
@@ -11727,6 +11811,7 @@ var posts = pgTable("post", {
|
|
|
11727
11811
|
index$1("idx_post_site_status_published").on(table.siteId, table.status, table.publishedAt),
|
|
11728
11812
|
index$1("idx_post_site_status_activity").on(table.siteId, table.status, table.lastActivityAt),
|
|
11729
11813
|
index$1("idx_post_site_root_published_activity").on(table.siteId, table.lastActivityAt, table.id).where(sql`${table.replyToId} IS NULL AND ${table.status} = 'published'`),
|
|
11814
|
+
index$1("idx_post_site_root_thread_updated").on(table.siteId, table.threadUpdatedAt, table.id).where(sql`${table.replyToId} IS NULL AND ${table.status} = 'published'`),
|
|
11730
11815
|
index$1("idx_post_site_root_draft_updated").on(table.siteId, table.updatedAt, table.id).where(sql`${table.replyToId} IS NULL AND ${table.status} = 'draft'`),
|
|
11731
11816
|
index$1("idx_post_site_reply_thread_created").on(table.siteId, table.threadId, table.createdAt, table.id).where(sql`${table.replyToId} IS NOT NULL AND ${table.status} = 'published'`),
|
|
11732
11817
|
index$1("idx_post_site_featured_thread_published").on(table.siteId, table.threadId, table.publishedAt, table.id).where(sql`${table.status} = 'published' AND ${table.featuredAt} IS NOT NULL`),
|
|
@@ -13605,6 +13690,7 @@ function createMediaService(db, siteId, databaseSchema = sqliteSchemaBundle, dat
|
|
|
13605
13690
|
if (merged.hasReplies !== void 0) params.set("replies", merged.hasReplies ? "any" : "none");
|
|
13606
13691
|
if (merged.visibility) params.set("visibility", merged.visibility === "latest_hidden" ? "hidden" : merged.visibility);
|
|
13607
13692
|
if (merged.view && merged.view !== "grid") params.set("view", merged.view);
|
|
13693
|
+
if (merged.sort === "updated") params.set("sort", "updated");
|
|
13608
13694
|
const qs = params.toString();
|
|
13609
13695
|
return qs ? toPublicPath(`/archive?${qs}`, sitePathPrefix) : toPublicPath("/archive", sitePathPrefix);
|
|
13610
13696
|
}
|
|
@@ -13627,7 +13713,7 @@ function getFormatLabelPlural(format) {
|
|
|
13627
13713
|
/** Icon name mapping for post formats. */ var FORMAT_ICONS = {
|
|
13628
13714
|
note: "notepad-text",
|
|
13629
13715
|
link: "external-link",
|
|
13630
|
-
quote: "
|
|
13716
|
+
quote: "quote"
|
|
13631
13717
|
};
|
|
13632
13718
|
/** Icon name mapping for media kinds. */ var MEDIA_KIND_ICONS = {
|
|
13633
13719
|
image: "image",
|
|
@@ -13842,34 +13928,62 @@ var ChipClearLink = ({ href, label }) => /* @__PURE__ */ jsxDEV$1("a", {
|
|
|
13842
13928
|
]
|
|
13843
13929
|
});
|
|
13844
13930
|
};
|
|
13931
|
+
/**
|
|
13932
|
+
* One option in a toolbar toggle. `label` is used for both the hover tooltip
|
|
13933
|
+
* and the accessible name — an icon-only control has to explain itself to
|
|
13934
|
+
* both audiences, and one string keeps them from drifting apart.
|
|
13935
|
+
*/ var ToggleOption = ({ href, icon, label, active }) => /* @__PURE__ */ jsxDEV$1("a", {
|
|
13936
|
+
href,
|
|
13937
|
+
class: `archive-view-btn${active ? " archive-view-btn-active" : ""}`,
|
|
13938
|
+
role: "radio",
|
|
13939
|
+
"aria-checked": active ? "true" : "false",
|
|
13940
|
+
"aria-label": label,
|
|
13941
|
+
title: label,
|
|
13942
|
+
children: /* @__PURE__ */ jsxDEV$1(Icon, {
|
|
13943
|
+
name: icon,
|
|
13944
|
+
class: "[&>svg]:size-4"
|
|
13945
|
+
})
|
|
13946
|
+
});
|
|
13845
13947
|
var ViewToggle = ({ filters, sitePathPrefix = "" }) => {
|
|
13948
|
+
const { i18n } = useLingui();
|
|
13846
13949
|
const currentView = filters.view ?? "grid";
|
|
13847
|
-
const gridUrl = buildFilterUrl(filters, { view: void 0 }, sitePathPrefix);
|
|
13848
|
-
const listUrl = buildFilterUrl(filters, { view: "list" }, sitePathPrefix);
|
|
13849
13950
|
return /* @__PURE__ */ jsxDEV$1("div", {
|
|
13850
13951
|
class: "archive-view-toggle",
|
|
13851
13952
|
role: "radiogroup",
|
|
13852
|
-
"aria-label": "
|
|
13853
|
-
children: [/* @__PURE__ */ jsxDEV$1(
|
|
13854
|
-
href:
|
|
13855
|
-
|
|
13856
|
-
|
|
13857
|
-
|
|
13858
|
-
|
|
13859
|
-
|
|
13860
|
-
|
|
13861
|
-
|
|
13862
|
-
})
|
|
13863
|
-
})
|
|
13864
|
-
|
|
13865
|
-
|
|
13866
|
-
|
|
13867
|
-
|
|
13868
|
-
|
|
13869
|
-
|
|
13870
|
-
|
|
13871
|
-
|
|
13872
|
-
|
|
13953
|
+
"aria-label": i18n._({ id: "OaiMaf" }),
|
|
13954
|
+
children: [/* @__PURE__ */ jsxDEV$1(ToggleOption, {
|
|
13955
|
+
href: buildFilterUrl(filters, { view: void 0 }, sitePathPrefix),
|
|
13956
|
+
icon: "layout-grid",
|
|
13957
|
+
active: currentView === "grid",
|
|
13958
|
+
label: i18n._({ id: "ZBH98o" })
|
|
13959
|
+
}), /* @__PURE__ */ jsxDEV$1(ToggleOption, {
|
|
13960
|
+
href: buildFilterUrl(filters, { view: "list" }, sitePathPrefix),
|
|
13961
|
+
icon: "list",
|
|
13962
|
+
active: currentView === "list",
|
|
13963
|
+
label: i18n._({ id: "IvUVJl" })
|
|
13964
|
+
})]
|
|
13965
|
+
});
|
|
13966
|
+
};
|
|
13967
|
+
/**
|
|
13968
|
+
* Time-axis toggle. Sits next to the view toggle because it changes how the
|
|
13969
|
+
* page is arranged, not which threads it contains — unlike the filter chips.
|
|
13970
|
+
*/ var SortToggle = ({ filters, sitePathPrefix = "" }) => {
|
|
13971
|
+
const { i18n } = useLingui();
|
|
13972
|
+
const sortsByActivity = filters.sort === "updated";
|
|
13973
|
+
return /* @__PURE__ */ jsxDEV$1("div", {
|
|
13974
|
+
class: "archive-view-toggle",
|
|
13975
|
+
role: "radiogroup",
|
|
13976
|
+
"aria-label": i18n._({ id: "AmQ/h/" }),
|
|
13977
|
+
children: [/* @__PURE__ */ jsxDEV$1(ToggleOption, {
|
|
13978
|
+
href: buildFilterUrl(filters, { sort: void 0 }, sitePathPrefix),
|
|
13979
|
+
icon: "clock",
|
|
13980
|
+
active: !sortsByActivity,
|
|
13981
|
+
label: i18n._({ id: "pKeaoC" })
|
|
13982
|
+
}), /* @__PURE__ */ jsxDEV$1(ToggleOption, {
|
|
13983
|
+
href: buildFilterUrl(filters, { sort: "updated" }, sitePathPrefix),
|
|
13984
|
+
icon: "history",
|
|
13985
|
+
active: sortsByActivity,
|
|
13986
|
+
label: i18n._({ id: "SbNisn" })
|
|
13873
13987
|
})]
|
|
13874
13988
|
});
|
|
13875
13989
|
};
|
|
@@ -14114,9 +14228,15 @@ var FilterBar = ({ filters, availableYears, availableCollections, isAuthenticate
|
|
|
14114
14228
|
iconOnly: true
|
|
14115
14229
|
})
|
|
14116
14230
|
]
|
|
14117
|
-
}), /* @__PURE__ */ jsxDEV$1(
|
|
14118
|
-
|
|
14119
|
-
|
|
14231
|
+
}), /* @__PURE__ */ jsxDEV$1("div", {
|
|
14232
|
+
class: "archive-toolbar-toggles",
|
|
14233
|
+
children: [/* @__PURE__ */ jsxDEV$1(SortToggle, {
|
|
14234
|
+
filters,
|
|
14235
|
+
sitePathPrefix
|
|
14236
|
+
}), /* @__PURE__ */ jsxDEV$1(ViewToggle, {
|
|
14237
|
+
filters,
|
|
14238
|
+
sitePathPrefix
|
|
14239
|
+
})]
|
|
14120
14240
|
})]
|
|
14121
14241
|
});
|
|
14122
14242
|
};
|
|
@@ -14309,6 +14429,7 @@ var ArchiveTile = ({ post, timeZone = "UTC" }) => {
|
|
|
14309
14429
|
var ArchivePage = ({ groups, items, totalCount, currentPage, totalPages, filters, availableYears, availableCollections, isAuthenticated, sitePathPrefix = "", timeZone = "UTC", feedHref }) => {
|
|
14310
14430
|
const { i18n } = useLingui();
|
|
14311
14431
|
const currentView = filters.view ?? "grid";
|
|
14432
|
+
const sortsByActivity = filters.sort === "updated";
|
|
14312
14433
|
const paginationBaseUrl = buildFilterUrl(filters, {}, sitePathPrefix);
|
|
14313
14434
|
const totalCountUnit = totalCount === 1 ? i18n._({ id: "BdjLtf" }) : i18n._({ id: "avuFKG" });
|
|
14314
14435
|
return /* @__PURE__ */ jsxDEV$1("div", {
|
|
@@ -14335,6 +14456,7 @@ var ArchivePage = ({ groups, items, totalCount, currentPage, totalPages, filters
|
|
|
14335
14456
|
}),
|
|
14336
14457
|
" ",
|
|
14337
14458
|
totalCountUnit,
|
|
14459
|
+
sortsByActivity && /* @__PURE__ */ jsxDEV$1(Fragment$1, { children: [" · ", i18n._({ id: "iZYiBK" })] }),
|
|
14338
14460
|
feedHref && /* @__PURE__ */ jsxDEV$1(Fragment$1, { children: [" ", /* @__PURE__ */ jsxDEV$1("a", {
|
|
14339
14461
|
href: toPublicPath(feedHref, sitePathPrefix),
|
|
14340
14462
|
class: "feed-link",
|
|
@@ -14364,7 +14486,7 @@ var ArchivePage = ({ groups, items, totalCount, currentPage, totalPages, filters
|
|
|
14364
14486
|
class: "contents",
|
|
14365
14487
|
children: [/* @__PURE__ */ jsxDEV$1(ArchiveMonthHeader, {
|
|
14366
14488
|
class: `archive-month-header${groupIndex > 0 ? " archive-month-header-spaced" : ""}`,
|
|
14367
|
-
label: group.label,
|
|
14489
|
+
label: sortsByActivity ? `Updated ${group.label}` : group.label,
|
|
14368
14490
|
count: group.totalCount
|
|
14369
14491
|
}), group.posts.map((post) => /* @__PURE__ */ jsxDEV$1(ArchiveTile, {
|
|
14370
14492
|
post,
|
|
@@ -14421,6 +14543,33 @@ var ArchivePage = ({ groups, items, totalCount, currentPage, totalPages, filters
|
|
|
14421
14543
|
return str.replaceAll("]]>", "]]]]><![CDATA[>");
|
|
14422
14544
|
}
|
|
14423
14545
|
/**
|
|
14546
|
+
* Resolve a URL for use outside the feed document's browser context.
|
|
14547
|
+
*
|
|
14548
|
+
* Feed readers do not consistently resolve root-relative URLs found inside
|
|
14549
|
+
* Atom HTML content or enclosure attributes, so every non-fragment URL must
|
|
14550
|
+
* carry its own origin.
|
|
14551
|
+
*/ function toAbsoluteFeedUrl(url, siteUrl) {
|
|
14552
|
+
const normalizedUrl = url.trim();
|
|
14553
|
+
if (!normalizedUrl || normalizedUrl.startsWith("#")) return normalizedUrl;
|
|
14554
|
+
try {
|
|
14555
|
+
const baseUrl = siteUrl.endsWith("/") ? siteUrl : `${siteUrl}/`;
|
|
14556
|
+
return new URL(normalizedUrl, baseUrl).toString();
|
|
14557
|
+
} catch {
|
|
14558
|
+
return normalizedUrl;
|
|
14559
|
+
}
|
|
14560
|
+
}
|
|
14561
|
+
/**
|
|
14562
|
+
* Resolve navigational and media URL attributes inside trusted post HTML.
|
|
14563
|
+
*
|
|
14564
|
+
* Fragment-only links stay local to the rendered feed entry so footnotes and
|
|
14565
|
+
* other in-entry references continue to work.
|
|
14566
|
+
*/ function absolutizeFeedHtmlUrls(html, siteUrl) {
|
|
14567
|
+
return html.replaceAll(/(\s)(href|poster|src)=(["'])([^"']*)\3/gi, (match, whitespace, attribute, quote, url) => {
|
|
14568
|
+
const absoluteUrl = toAbsoluteFeedUrl(url, siteUrl);
|
|
14569
|
+
return absoluteUrl ? `${whitespace}${attribute}=${quote}${absoluteUrl}${quote}` : match;
|
|
14570
|
+
});
|
|
14571
|
+
}
|
|
14572
|
+
/**
|
|
14424
14573
|
* Strip embedded content that is unsafe or unsupported in feed readers.
|
|
14425
14574
|
*
|
|
14426
14575
|
* - `<figure class="tiptap-embed-figure">` is replaced by its fallback link
|
|
@@ -14465,6 +14614,24 @@ function renderInlinePostHeader(post, permalinkUrl) {
|
|
|
14465
14614
|
*/ function renderRatingHtml(rating) {
|
|
14466
14615
|
return `<p>${"★".repeat(rating)}${"☆".repeat(5 - rating)} ${rating}/5</p>`;
|
|
14467
14616
|
}
|
|
14617
|
+
/**
|
|
14618
|
+
* Render a Link post preview as feed-safe HTML.
|
|
14619
|
+
*
|
|
14620
|
+
* Feed readers commonly strip CSS overlays and embedded players, so video
|
|
14621
|
+
* previews use a linked thumbnail plus a visible provider-aware action.
|
|
14622
|
+
* Non-video Link previews keep the linked thumbnail without a video label.
|
|
14623
|
+
*/ function renderLinkPreviewForFeed(post, siteUrl) {
|
|
14624
|
+
if (post.format !== "link") return "";
|
|
14625
|
+
const imageUrl = post.previewImageUrl?.trim();
|
|
14626
|
+
const linkUrl = post.url?.trim();
|
|
14627
|
+
if (!imageUrl || !linkUrl) return "";
|
|
14628
|
+
const isVideo = post.previewKind?.trim().toLowerCase() === "video";
|
|
14629
|
+
const providerLabel = getLinkPreviewProviderLabel(post.previewProvider);
|
|
14630
|
+
const fallbackAlt = isVideo ? providerLabel ? `${providerLabel} video` : "Video preview" : "Link preview";
|
|
14631
|
+
const altText = post.title?.trim() || fallbackAlt;
|
|
14632
|
+
const caption = isVideo ? `<figcaption><a href="${escapeXml(linkUrl)}">▶ ${providerLabel ? `Watch on ${providerLabel}` : "Watch video"}</a></figcaption>` : "";
|
|
14633
|
+
return `<figure><a href="${escapeXml(linkUrl)}"><img src="${escapeXml(toAbsoluteFeedUrl(imageUrl, siteUrl))}" alt="${escapeXml(altText)}"/></a>${caption}</figure>`;
|
|
14634
|
+
}
|
|
14468
14635
|
function formatFeedBytes(bytes) {
|
|
14469
14636
|
if (bytes < 1024) return `${bytes} B`;
|
|
14470
14637
|
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
@@ -14508,9 +14675,9 @@ function getMediaMeta(item) {
|
|
|
14508
14675
|
* - Audio, text, and document attachments render as plain links with size
|
|
14509
14676
|
* and duration metadata when known. Text attachments link to the rendered
|
|
14510
14677
|
* preview page when a post permalink is available.
|
|
14511
|
-
*/ function renderMediaItem(item, postPermalinkUrl) {
|
|
14678
|
+
*/ function renderMediaItem(item, siteUrl, postPermalinkUrl) {
|
|
14512
14679
|
const category = getMediaCategory(item.mimeType);
|
|
14513
|
-
const url = escapeXml(item.url);
|
|
14680
|
+
const url = escapeXml(toAbsoluteFeedUrl(item.url, siteUrl));
|
|
14514
14681
|
const name = item.originalName ?? "";
|
|
14515
14682
|
const altText = item.altText ?? "";
|
|
14516
14683
|
const caption = item.altText?.trim() || "";
|
|
@@ -14521,7 +14688,7 @@ function getMediaMeta(item) {
|
|
|
14521
14688
|
return `<figure><a href="${url}"><img src="${url}" alt="${escapeXml(altText)}"${dims}/></a>${figcaption}</figure>`;
|
|
14522
14689
|
}
|
|
14523
14690
|
if (category === "video") {
|
|
14524
|
-
const poster = item.posterUrl || item.thumbnailUrl;
|
|
14691
|
+
const poster = toAbsoluteFeedUrl(item.posterUrl || item.thumbnailUrl, siteUrl);
|
|
14525
14692
|
const dims = item.width && item.height ? ` width="${item.width}" height="${item.height}"` : "";
|
|
14526
14693
|
const metaSuffix = meta ? ` (${escapeXml(meta)})` : "";
|
|
14527
14694
|
return `<figure><a href="${url}"><img src="${escapeXml(poster)}" alt="${escapeXml(altText || name)}"${dims}/></a><figcaption><a href="${url}">▶ Watch video</a>${metaSuffix}</figcaption></figure>`;
|
|
@@ -14540,9 +14707,9 @@ function getMediaMeta(item) {
|
|
|
14540
14707
|
/**
|
|
14541
14708
|
* Render all media attachments for a post as HTML for embedding in feed
|
|
14542
14709
|
* content. Returns an empty string when the post has no media.
|
|
14543
|
-
*/ function renderMediaForFeed(media, postPermalinkUrl) {
|
|
14710
|
+
*/ function renderMediaForFeed(media, siteUrl, postPermalinkUrl) {
|
|
14544
14711
|
if (media.length === 0) return "";
|
|
14545
|
-
return media.map((item) => renderMediaItem(item, postPermalinkUrl)).join("\n");
|
|
14712
|
+
return media.map((item) => renderMediaItem(item, siteUrl, postPermalinkUrl)).join("\n");
|
|
14546
14713
|
}
|
|
14547
14714
|
/**
|
|
14548
14715
|
* Build the HTML content for a single post (root or reply).
|
|
@@ -14550,7 +14717,7 @@ function getMediaMeta(item) {
|
|
|
14550
14717
|
* @param post - Post view data
|
|
14551
14718
|
* @param permalinkUrl - Absolute permalink URL back to the blog post
|
|
14552
14719
|
* @param options - Rendering options for top-level versus inline posts
|
|
14553
|
-
*/ function buildSinglePostContent(post, permalinkUrl, options = {}) {
|
|
14720
|
+
*/ function buildSinglePostContent(post, siteUrl, permalinkUrl, options = {}) {
|
|
14554
14721
|
const parts = [];
|
|
14555
14722
|
if (options.inline) parts.push(...renderInlinePostHeader(post, permalinkUrl));
|
|
14556
14723
|
if (post.format === "quote" && post.quoteText) {
|
|
@@ -14564,8 +14731,10 @@ function getMediaMeta(item) {
|
|
|
14564
14731
|
parts.push(`<p>— ${source}</p>`);
|
|
14565
14732
|
}
|
|
14566
14733
|
}
|
|
14567
|
-
|
|
14568
|
-
|
|
14734
|
+
const linkPreviewHtml = renderLinkPreviewForFeed(post, siteUrl);
|
|
14735
|
+
if (linkPreviewHtml) parts.push(linkPreviewHtml);
|
|
14736
|
+
if (post.bodyHtml) parts.push(absolutizeFeedHtmlUrls(stripUnsafeFeedHtml(post.bodyHtml), siteUrl));
|
|
14737
|
+
const mediaHtml = renderMediaForFeed(post.media, siteUrl, permalinkUrl);
|
|
14569
14738
|
if (mediaHtml) parts.push(mediaHtml);
|
|
14570
14739
|
if (post.rating && post.rating > 0) parts.push(renderRatingHtml(post.rating));
|
|
14571
14740
|
if (parts.length === 0) parts.push(`<p>${escapeXml(getFeedSummaryText(post))}</p>`);
|
|
@@ -14579,7 +14748,7 @@ function getMediaMeta(item) {
|
|
|
14579
14748
|
* @param siteUrl - Site base URL for building absolute permalinks
|
|
14580
14749
|
* @param permalinkUrl - Absolute permalink URL for the root post
|
|
14581
14750
|
*/ function buildFeedContent(post, siteUrl, permalinkUrl) {
|
|
14582
|
-
const rootContent = buildSinglePostContent(post, permalinkUrl);
|
|
14751
|
+
const rootContent = buildSinglePostContent(post, siteUrl, permalinkUrl);
|
|
14583
14752
|
const replies = post.threadReplies;
|
|
14584
14753
|
if (!replies || replies.length === 0) return rootContent;
|
|
14585
14754
|
const parts = [rootContent];
|
|
@@ -14587,7 +14756,7 @@ function getMediaMeta(item) {
|
|
|
14587
14756
|
const replyPermalink = new URL(reply.permalink, siteUrl).toString();
|
|
14588
14757
|
parts.push("<hr/>");
|
|
14589
14758
|
parts.push(`<p><small><time datetime="${escapeXml(reply.publishedAt)}">${escapeXml(reply.publishedAtFormatted)}</time></small></p>`);
|
|
14590
|
-
parts.push(buildSinglePostContent(reply, replyPermalink, { inline: true }));
|
|
14759
|
+
parts.push(buildSinglePostContent(reply, siteUrl, replyPermalink, { inline: true }));
|
|
14591
14760
|
}
|
|
14592
14761
|
return parts.join("\n");
|
|
14593
14762
|
}
|
|
@@ -14617,7 +14786,7 @@ function getEntryMedia(post) {
|
|
|
14617
14786
|
const enclosureLinks = getEntryMedia(post).map((m) => {
|
|
14618
14787
|
const lengthAttr = m.size != null && m.size > 0 ? ` length="${m.size}"` : "";
|
|
14619
14788
|
const titleAttr = m.originalName ? ` title="${escapeXml(m.originalName)}"` : "";
|
|
14620
|
-
return `\n <link rel="enclosure" type="${escapeXml(m.mimeType)}" href="${escapeXml(m.url)}"${lengthAttr}${titleAttr}/>`;
|
|
14789
|
+
return `\n <link rel="enclosure" type="${escapeXml(m.mimeType)}" href="${escapeXml(toAbsoluteFeedUrl(m.url, siteUrl))}"${lengthAttr}${titleAttr}/>`;
|
|
14621
14790
|
}).join("");
|
|
14622
14791
|
return `
|
|
14623
14792
|
<entry>
|
|
@@ -14630,7 +14799,7 @@ function getEntryMedia(post) {
|
|
|
14630
14799
|
<content type="html"><![CDATA[${escapeCdata(buildFeedContent(post, siteUrl, permalinkUrl))}]]></content>
|
|
14631
14800
|
</entry>`;
|
|
14632
14801
|
}).join("");
|
|
14633
|
-
const
|
|
14802
|
+
const feedUpdated = posts.map((post) => post.feedUpdatedAt ?? post.updatedAt).reduce((latest, value) => latest === null || value > latest ? value : latest, null) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
14634
14803
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
14635
14804
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
14636
14805
|
<title>${escapeXml(feedTitle)}</title>
|
|
@@ -14638,7 +14807,7 @@ function getEntryMedia(post) {
|
|
|
14638
14807
|
<link href="${escapeXml(siteUrl)}" rel="alternate"/>
|
|
14639
14808
|
<link href="${escapeXml(selfUrl)}" rel="self"/>
|
|
14640
14809
|
<id>${escapeXml(selfUrl)}</id>
|
|
14641
|
-
<updated>${
|
|
14810
|
+
<updated>${feedUpdated}</updated>
|
|
14642
14811
|
${entries}
|
|
14643
14812
|
</feed>`;
|
|
14644
14813
|
}
|
|
@@ -14758,6 +14927,7 @@ ${entries.map((entry) => {
|
|
|
14758
14927
|
const visibility = visibilityParam && VALID_VISIBILITIES.includes(visibilityParam) ? visibilityParam : void 0;
|
|
14759
14928
|
const viewParam = q("view");
|
|
14760
14929
|
const view = viewParam && (viewParam === "grid" || viewParam === "list") ? viewParam : void 0;
|
|
14930
|
+
const sort = q("sort") === "updated" ? "updated" : "published";
|
|
14761
14931
|
const pageParam = c.req.query("page");
|
|
14762
14932
|
const currentPage = Math.max(1, parseInt(pageParam || "1", 10) || 1);
|
|
14763
14933
|
return {
|
|
@@ -14771,6 +14941,7 @@ ${entries.map((entry) => {
|
|
|
14771
14941
|
visibility,
|
|
14772
14942
|
visibilityAll,
|
|
14773
14943
|
view,
|
|
14944
|
+
sort,
|
|
14774
14945
|
currentPage
|
|
14775
14946
|
};
|
|
14776
14947
|
}
|
|
@@ -14783,12 +14954,11 @@ ${entries.map((entry) => {
|
|
|
14783
14954
|
*/ function buildArchivePostFilters(params, opts) {
|
|
14784
14955
|
const { isAuthenticated, collectionId } = opts;
|
|
14785
14956
|
const effectiveVisibility = isAuthenticated ? params.visibilityAll ? void 0 : params.visibility ?? void 0 : void 0;
|
|
14786
|
-
|
|
14787
|
-
|
|
14788
|
-
|
|
14789
|
-
|
|
14790
|
-
|
|
14791
|
-
}
|
|
14957
|
+
const yearRange = params.validYear ? {
|
|
14958
|
+
after: Date.UTC(params.validYear, 0, 1) / 1e3,
|
|
14959
|
+
before: Date.UTC(params.validYear + 1, 0, 1) / 1e3
|
|
14960
|
+
} : void 0;
|
|
14961
|
+
const sortsByActivity = params.sort === "updated";
|
|
14792
14962
|
return {
|
|
14793
14963
|
format: params.format,
|
|
14794
14964
|
status: "published",
|
|
@@ -14797,19 +14967,26 @@ ${entries.map((entry) => {
|
|
|
14797
14967
|
excludeLatestHidden: false,
|
|
14798
14968
|
...effectiveVisibility === "featured" ? { featured: true } : effectiveVisibility ? { visibility: effectiveVisibility } : {},
|
|
14799
14969
|
collectionId,
|
|
14800
|
-
|
|
14801
|
-
|
|
14970
|
+
...yearRange ? sortsByActivity ? {
|
|
14971
|
+
axisAfter: yearRange.after,
|
|
14972
|
+
axisBefore: yearRange.before
|
|
14973
|
+
} : {
|
|
14974
|
+
publishedAfter: yearRange.after,
|
|
14975
|
+
publishedBefore: yearRange.before
|
|
14976
|
+
} : {},
|
|
14802
14977
|
mediaKinds: params.mediaKinds,
|
|
14803
14978
|
hasMedia: params.hasMedia,
|
|
14804
14979
|
hasTitle: params.hasTitle,
|
|
14805
14980
|
hasReplies: params.hasReplies,
|
|
14806
|
-
sortBy: "published",
|
|
14981
|
+
sortBy: sortsByActivity ? "thread_updated" : "published",
|
|
14807
14982
|
ignorePinnedSort: true
|
|
14808
14983
|
};
|
|
14809
14984
|
}
|
|
14810
14985
|
/**
|
|
14811
14986
|
* Build a query string from parsed archive params (for feed self-URL and
|
|
14812
|
-
* archive page feed link). Omits page
|
|
14987
|
+
* archive page feed link). Omits view and page — those shape the rendered
|
|
14988
|
+
* page, not the result set — but carries `sort`, so the feed button on an
|
|
14989
|
+
* updated-sorted page hands back the matching feed.
|
|
14813
14990
|
*/ function buildArchiveFeedQuery(params) {
|
|
14814
14991
|
const qs = new URLSearchParams();
|
|
14815
14992
|
if (params.format) qs.set("format", params.format);
|
|
@@ -14819,6 +14996,7 @@ ${entries.map((entry) => {
|
|
|
14819
14996
|
else if (params.hasMedia !== void 0) qs.set("media", params.hasMedia ? "any" : "none");
|
|
14820
14997
|
if (params.hasTitle !== void 0) qs.set("title", params.hasTitle ? "any" : "none");
|
|
14821
14998
|
if (params.hasReplies !== void 0) qs.set("replies", params.hasReplies ? "any" : "none");
|
|
14999
|
+
if (params.sort === "updated") qs.set("sort", "updated");
|
|
14822
15000
|
const str = qs.toString();
|
|
14823
15001
|
return str ? `?${str}` : "";
|
|
14824
15002
|
}
|
|
@@ -14886,7 +15064,8 @@ var archiveRoutes = new Hono();
|
|
|
14886
15064
|
}),
|
|
14887
15065
|
services.posts.getDistinctYears({
|
|
14888
15066
|
status: "published",
|
|
14889
|
-
excludeReplies: true
|
|
15067
|
+
excludeReplies: true,
|
|
15068
|
+
sortBy: filters.sortBy
|
|
14890
15069
|
}),
|
|
14891
15070
|
services.collections.list()
|
|
14892
15071
|
]);
|
|
@@ -14902,7 +15081,7 @@ var archiveRoutes = new Hono();
|
|
|
14902
15081
|
else {
|
|
14903
15082
|
const grouped = /* @__PURE__ */ new Map();
|
|
14904
15083
|
for (const post of posts) {
|
|
14905
|
-
const key = formatYearMonth(post.publishedAt ?? post.updatedAt, appConfig.timeZone);
|
|
15084
|
+
const key = formatYearMonth(params.sort === "updated" ? post.threadUpdatedAt : post.publishedAt ?? post.updatedAt, appConfig.timeZone);
|
|
14906
15085
|
if (!grouped.has(key)) grouped.set(key, []);
|
|
14907
15086
|
grouped.get(key).push({
|
|
14908
15087
|
...post,
|
|
@@ -14937,7 +15116,8 @@ var archiveRoutes = new Hono();
|
|
|
14937
15116
|
hasTitle: params.hasTitle,
|
|
14938
15117
|
hasReplies: params.hasReplies,
|
|
14939
15118
|
visibility: effectiveVisibility,
|
|
14940
|
-
view: params.view
|
|
15119
|
+
view: params.view,
|
|
15120
|
+
sort: params.sort === "updated" ? "updated" : void 0
|
|
14941
15121
|
};
|
|
14942
15122
|
const feedQuery = buildArchiveFeedQuery(params);
|
|
14943
15123
|
const availableCollectionsList = allCollections.map((col) => ({
|
|
@@ -14945,7 +15125,7 @@ var archiveRoutes = new Hono();
|
|
|
14945
15125
|
title: col.title
|
|
14946
15126
|
}));
|
|
14947
15127
|
return renderPublicPage(c, {
|
|
14948
|
-
title: buildPageTitle("Archive", navData.siteName),
|
|
15128
|
+
title: buildPageTitle("Archive", params.sort === "updated" ? "Recently updated" : void 0, navData.siteName),
|
|
14949
15129
|
navData,
|
|
14950
15130
|
content: /* @__PURE__ */ jsxDEV$1(ArchivePage, {
|
|
14951
15131
|
groups,
|
|
@@ -15005,6 +15185,7 @@ async function buildArchiveFeedData(c, selfPath) {
|
|
|
15005
15185
|
const collection = params.collectionSlug ? await services.collections.getBySlug(params.collectionSlug) : void 0;
|
|
15006
15186
|
const rssPublishedBefore = getRssPublishedBefore(appConfig.rssPublishDelaySeconds);
|
|
15007
15187
|
const yearPublishedBefore = params.validYear ? Date.UTC(params.validYear + 1, 0, 1) / 1e3 : void 0;
|
|
15188
|
+
const sortsByActivity = params.sort === "updated";
|
|
15008
15189
|
const filters = {
|
|
15009
15190
|
format: params.format,
|
|
15010
15191
|
status: "published",
|
|
@@ -15016,9 +15197,13 @@ async function buildArchiveFeedData(c, selfPath) {
|
|
|
15016
15197
|
hasMedia: params.hasMedia,
|
|
15017
15198
|
hasTitle: params.hasTitle,
|
|
15018
15199
|
hasReplies: params.hasReplies,
|
|
15200
|
+
sortBy: sortsByActivity ? "thread_updated" : "published",
|
|
15019
15201
|
ignorePinnedSort: true,
|
|
15020
|
-
...params.validYear ?
|
|
15021
|
-
|
|
15202
|
+
...params.validYear ? sortsByActivity ? {
|
|
15203
|
+
axisAfter: Date.UTC(params.validYear, 0, 1) / 1e3,
|
|
15204
|
+
axisBefore: yearPublishedBefore
|
|
15205
|
+
} : { publishedAfter: Date.UTC(params.validYear, 0, 1) / 1e3 } : {},
|
|
15206
|
+
publishedBefore: yearPublishedBefore === void 0 || sortsByActivity ? rssPublishedBefore : Math.min(yearPublishedBefore, rssPublishedBefore),
|
|
15022
15207
|
limit: appConfig.rssFeedLimit
|
|
15023
15208
|
};
|
|
15024
15209
|
const posts = await services.posts.list(filters);
|
|
@@ -15985,6 +16170,7 @@ async function renderPost(c, post, options = {}) {
|
|
|
15985
16170
|
} : threadPost) : display.threadPostViews;
|
|
15986
16171
|
const previewTitle = options.isPreview ? getI18n(c)._({ id: "27KTPP" }) : null;
|
|
15987
16172
|
const canonicalHref = buildPostCanonicalHref(display.postView, display.threadPostViews, c.var.appConfig.siteUrl);
|
|
16173
|
+
const draftEditHref = `${toPublicPath(`/preview/${post.slug}`, c.var.appConfig.sitePathPrefix)}?edit=1`;
|
|
15988
16174
|
return renderPublicPage(c, {
|
|
15989
16175
|
title: previewTitle ? buildPageTitle(previewTitle, meta.title, navData.siteName) : buildPageTitle(meta.title, navData.siteName),
|
|
15990
16176
|
description: meta.description,
|
|
@@ -16001,8 +16187,7 @@ async function renderPost(c, post, options = {}) {
|
|
|
16001
16187
|
} : {},
|
|
16002
16188
|
navData,
|
|
16003
16189
|
...options.isPreview ? {
|
|
16004
|
-
pageChrome: /* @__PURE__ */ jsxDEV$1(DraftPreviewBar, {}),
|
|
16005
|
-
showComposeDialog: false,
|
|
16190
|
+
pageChrome: /* @__PURE__ */ jsxDEV$1(DraftPreviewBar, { editHref: draftEditHref }),
|
|
16006
16191
|
noindex: true
|
|
16007
16192
|
} : {},
|
|
16008
16193
|
content: /* @__PURE__ */ jsxDEV$1(PostPage, {
|
|
@@ -23279,8 +23464,8 @@ async function syncHostedControlPlaneSiteAvatar(input) {
|
|
|
23279
23464
|
return;
|
|
23280
23465
|
}
|
|
23281
23466
|
await markSyncPending(settings);
|
|
23282
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
23283
|
-
const { getGitHubAppConfig } = await import("./env-
|
|
23467
|
+
const { createGitHubSyncService } = await import("./github-sync-Bqg62IvV.js");
|
|
23468
|
+
const { getGitHubAppConfig } = await import("./env-BPYViDJJ.js").then((n) => n.t);
|
|
23284
23469
|
const run = runBackgroundSync(settings, createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
23285
23470
|
storage: c.var.storage,
|
|
23286
23471
|
githubApp: getGitHubAppConfig(c.env)
|
|
@@ -24290,7 +24475,7 @@ settingsRoutes.post("/github-sync/connect", async (c) => {
|
|
|
24290
24475
|
if (getGitHubAppConfig(c.env)) return dsToast("This deployment uses GitHub App authentication. Use Install GitHub App instead.", "error");
|
|
24291
24476
|
const body = await c.req.json();
|
|
24292
24477
|
if (!body.token?.trim() || !body.repo?.trim()) return dsToast("Token and repository are required.", "error");
|
|
24293
|
-
const { parseRepoSlug, createGitHubClient } = await import("./github-api-
|
|
24478
|
+
const { parseRepoSlug, createGitHubClient } = await import("./github-api-BNF35Lkx.js").then((n) => n.n);
|
|
24294
24479
|
const parsed = parseRepoSlug(body.repo);
|
|
24295
24480
|
if (!parsed) return dsToast("Invalid repository format. Use owner/repo.", "error");
|
|
24296
24481
|
const client = createGitHubClient(body.token);
|
|
@@ -24309,7 +24494,7 @@ settingsRoutes.post("/github-sync/connect", async (c) => {
|
|
|
24309
24494
|
await c.var.services.settings.set("GITHUB_SYNC_AUTH_MODE", "pat");
|
|
24310
24495
|
await c.var.services.settings.set("GITHUB_SYNC_APP_INSTALLATION_ID", "");
|
|
24311
24496
|
await c.var.services.settings.set("GITHUB_SYNC_ENABLED", "true");
|
|
24312
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
24497
|
+
const { createGitHubSyncService } = await import("./github-sync-Bqg62IvV.js");
|
|
24313
24498
|
const syncService = createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
24314
24499
|
storage: c.var.storage,
|
|
24315
24500
|
githubApp: getGitHubAppConfig(c.env)
|
|
@@ -24328,7 +24513,7 @@ settingsRoutes.post("/github-sync/connect", async (c) => {
|
|
|
24328
24513
|
return dsRedirect(publicPath(c, "/settings/github-sync"));
|
|
24329
24514
|
});
|
|
24330
24515
|
settingsRoutes.post("/github-sync/push", async (c) => {
|
|
24331
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
24516
|
+
const { createGitHubSyncService } = await import("./github-sync-Bqg62IvV.js");
|
|
24332
24517
|
const syncService = createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
24333
24518
|
storage: c.var.storage,
|
|
24334
24519
|
githubApp: getGitHubAppConfig(c.env)
|
|
@@ -24348,7 +24533,7 @@ settingsRoutes.post("/github-sync/push", async (c) => {
|
|
|
24348
24533
|
});
|
|
24349
24534
|
});
|
|
24350
24535
|
settingsRoutes.post("/github-sync/disconnect", async (c) => {
|
|
24351
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
24536
|
+
const { createGitHubSyncService } = await import("./github-sync-Bqg62IvV.js");
|
|
24352
24537
|
await createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), { githubApp: getGitHubAppConfig(c.env) }).teardownWebhook();
|
|
24353
24538
|
return dsRedirect(publicPath(c, "/settings/github-sync"));
|
|
24354
24539
|
});
|
|
@@ -24536,10 +24721,10 @@ function buildRepoPickerLabels(c) {
|
|
|
24536
24721
|
const repo = String(body.repo ?? "").trim();
|
|
24537
24722
|
const confirmForeign = body.confirmForeign === true || body.confirmForeign === "true";
|
|
24538
24723
|
if (!installationId || !repo) return wantsJson ? c.json({ error: "Missing installationId or repo." }, 400) : c.text("Missing installationId or repo.", 400);
|
|
24539
|
-
const { parseRepoSlug, createGitHubClient } = await import("./github-api-
|
|
24724
|
+
const { parseRepoSlug, createGitHubClient } = await import("./github-api-BNF35Lkx.js").then((n) => n.n);
|
|
24540
24725
|
const parsed = parseRepoSlug(repo);
|
|
24541
24726
|
if (!parsed) return wantsJson ? c.json({ error: "Invalid repository format." }, 400) : c.text("Invalid repository format.", 400);
|
|
24542
|
-
const { classifyRepoForSync } = await import("./github-sync-
|
|
24727
|
+
const { classifyRepoForSync } = await import("./github-sync-Bqg62IvV.js");
|
|
24543
24728
|
const ghClient = createGitHubClient(() => getInstallationTokenFromApp(app, installationId));
|
|
24544
24729
|
let classification;
|
|
24545
24730
|
try {
|
|
@@ -24569,7 +24754,7 @@ function buildRepoPickerLabels(c) {
|
|
|
24569
24754
|
await c.var.services.settings.set("GITHUB_SYNC_REPO", repo);
|
|
24570
24755
|
await c.var.services.settings.set("GITHUB_SYNC_TOKEN", "");
|
|
24571
24756
|
await c.var.services.settings.set("GITHUB_SYNC_ENABLED", "true");
|
|
24572
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
24757
|
+
const { createGitHubSyncService } = await import("./github-sync-Bqg62IvV.js");
|
|
24573
24758
|
const syncService = createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
24574
24759
|
storage: c.var.storage,
|
|
24575
24760
|
githubApp: app
|
|
@@ -24608,7 +24793,7 @@ function requireGitHubApp(c) {
|
|
|
24608
24793
|
* build a client for classification without adding the helper to the
|
|
24609
24794
|
* top-level imports (the module already lazy-imports github-api).
|
|
24610
24795
|
*/ async function getInstallationTokenFromApp(app, installationId) {
|
|
24611
|
-
const { getInstallationToken } = await import("./github-app-
|
|
24796
|
+
const { getInstallationToken } = await import("./github-app-DdiD-bC4.js").then((n) => n.i);
|
|
24612
24797
|
return getInstallationToken(app, installationId);
|
|
24613
24798
|
}
|
|
24614
24799
|
/** List GitHub App installations authorized for this site. */ settingsRoutes.get("/github-sync/app/installations", async (c) => {
|
|
@@ -24682,10 +24867,10 @@ function requireGitHubApp(c) {
|
|
|
24682
24867
|
const installationId = String(body.installationId ?? "").trim();
|
|
24683
24868
|
const repo = String(body.repo ?? "").trim();
|
|
24684
24869
|
if (!installationId || !repo) return c.json({ error: "Missing installationId or repo." }, 400);
|
|
24685
|
-
const { parseRepoSlug, createGitHubClient } = await import("./github-api-
|
|
24870
|
+
const { parseRepoSlug, createGitHubClient } = await import("./github-api-BNF35Lkx.js").then((n) => n.n);
|
|
24686
24871
|
const parsed = parseRepoSlug(repo);
|
|
24687
24872
|
if (!parsed) return c.json({ error: "Invalid repository format." }, 400);
|
|
24688
|
-
const { classifyRepoForSync } = await import("./github-sync-
|
|
24873
|
+
const { classifyRepoForSync } = await import("./github-sync-Bqg62IvV.js");
|
|
24689
24874
|
const client = createGitHubClient(() => getInstallationTokenFromApp(app, installationId));
|
|
24690
24875
|
try {
|
|
24691
24876
|
const classification = await classifyRepoForSync(client, parsed.owner, parsed.repo, c.var.currentSite.id);
|
|
@@ -25421,18 +25606,22 @@ postsApiRoutes.get("/:id/content", requireAuthApi(), async (c) => {
|
|
|
25421
25606
|
});
|
|
25422
25607
|
postsApiRoutes.get("/:id", requireAuthApi(), async (c) => {
|
|
25423
25608
|
const id = parseIdParam(c.req.param("id"), ID_PREFIX.post);
|
|
25424
|
-
const [post, mediaList, threadCollections] = await Promise.all([
|
|
25609
|
+
const [post, mediaList, threadCollections, threadPosition] = await Promise.all([
|
|
25425
25610
|
c.var.services.posts.getById(id),
|
|
25426
25611
|
c.var.services.media.getByPostId(id),
|
|
25427
|
-
c.var.services.collections.getCollectionsByPostId(id)
|
|
25612
|
+
c.var.services.collections.getCollectionsByPostId(id),
|
|
25613
|
+
c.var.services.posts.getThreadPosition(id)
|
|
25428
25614
|
]);
|
|
25429
25615
|
const foundPost = assertFound(post, "Post");
|
|
25430
25616
|
const { r2PublicUrl, imageTransformUrl, s3PublicUrl, localPublicUrl, sitePathPrefix } = c.var.appConfig;
|
|
25431
25617
|
const collectionIds = threadCollections.map((col) => col.id);
|
|
25432
|
-
return c.json(
|
|
25433
|
-
|
|
25434
|
-
|
|
25435
|
-
|
|
25618
|
+
return c.json({
|
|
25619
|
+
...toApiPost(foundPost, {
|
|
25620
|
+
collectionIds,
|
|
25621
|
+
attachments: mediaList.map((m) => toApiAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl, localPublicUrl, sitePathPrefix))
|
|
25622
|
+
}),
|
|
25623
|
+
threadPosition
|
|
25624
|
+
});
|
|
25436
25625
|
});
|
|
25437
25626
|
postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
25438
25627
|
const body = parseValidated(CreatePostApiSchema, await c.req.json());
|
|
@@ -28087,6 +28276,7 @@ function createPathService(db, siteId, databaseSchema = sqliteSchemaBundle) {
|
|
|
28087
28276
|
const postRows = await db.select({
|
|
28088
28277
|
title: posts.title,
|
|
28089
28278
|
format: posts.format,
|
|
28279
|
+
status: posts.status,
|
|
28090
28280
|
path: pathRegistry.path
|
|
28091
28281
|
}).from(pathRegistry).innerJoin(posts, and(eq(posts.id, pathRegistry.postId), eq(posts.siteId, siteId))).where(and(eq(pathRegistry.siteId, siteId), eq(pathRegistry.kind, "slug"), isNotNull(pathRegistry.postId), isNotNull(posts.title), eq(posts.format, "note")));
|
|
28092
28282
|
const collectionRows = await db.select({
|
|
@@ -28100,7 +28290,8 @@ function createPathService(db, siteId, databaseSchema = sqliteSchemaBundle) {
|
|
|
28100
28290
|
title: row.title,
|
|
28101
28291
|
path: row.path,
|
|
28102
28292
|
type: "post",
|
|
28103
|
-
format: row.format
|
|
28293
|
+
format: row.format,
|
|
28294
|
+
status: row.status
|
|
28104
28295
|
});
|
|
28105
28296
|
}
|
|
28106
28297
|
for (const row of collectionRows) items.push({
|
|
@@ -28269,11 +28460,20 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
28269
28460
|
const { navItems, pathRegistry, posts, sites, threadCollections } = databaseSchema;
|
|
28270
28461
|
const databaseDialect = config.databaseDialect ?? "sqlite";
|
|
28271
28462
|
const usesBatchWrites = !supportsDrizzleTransaction(db, databaseDialect);
|
|
28272
|
-
|
|
28273
|
-
|
|
28463
|
+
/**
|
|
28464
|
+
* Column that carries the time axis selected by `PostFilters.sortBy`.
|
|
28465
|
+
* Sorting, year/month bucketing, and date-range filters all read it through
|
|
28466
|
+
* here so they can never drift onto different columns.
|
|
28467
|
+
*/ function timeAxisColumn(filters) {
|
|
28468
|
+
if (filters.sortBy === "activity") return posts.lastActivityAt;
|
|
28469
|
+
if (filters.sortBy === "thread_updated") return posts.threadUpdatedAt;
|
|
28470
|
+
return posts.publishedAt;
|
|
28274
28471
|
}
|
|
28275
|
-
function
|
|
28276
|
-
return databaseDialect === "pg" ? sql`to_char(timezone('UTC', to_timestamp(${
|
|
28472
|
+
function buildYearMonthExpr(column) {
|
|
28473
|
+
return databaseDialect === "pg" ? sql`to_char(timezone('UTC', to_timestamp(${column})), 'YYYY-MM')` : sql`strftime('%Y-%m', ${column}, 'unixepoch')`;
|
|
28474
|
+
}
|
|
28475
|
+
function buildYearExpr(column) {
|
|
28476
|
+
return databaseDialect === "pg" ? sql`to_char(timezone('UTC', to_timestamp(${column})), 'YYYY')` : sql`strftime('%Y', ${column}, 'unixepoch')`;
|
|
28277
28477
|
}
|
|
28278
28478
|
const effectiveVisibilityExpr = sql`coalesce(
|
|
28279
28479
|
${posts.visibility},
|
|
@@ -28290,11 +28490,36 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
28290
28490
|
async function pathExists(path) {
|
|
28291
28491
|
return (await db.select({ id: pathRegistry.id }).from(pathRegistry).where(and(eq(pathRegistry.siteId, siteId), eq(pathRegistry.path, normalizePath(path)))).limit(1)).length > 0;
|
|
28292
28492
|
}
|
|
28293
|
-
|
|
28294
|
-
|
|
28295
|
-
|
|
28296
|
-
|
|
28297
|
-
|
|
28493
|
+
/**
|
|
28494
|
+
* Recompute both Thread activity timestamps on the root from the Thread's
|
|
28495
|
+
* rows.
|
|
28496
|
+
*
|
|
28497
|
+
* - `lastActivityAt` — newest published post excluding quiet replies. This
|
|
28498
|
+
* is "last announced" and drives Latest and the feeds.
|
|
28499
|
+
* - `threadUpdatedAt` — newest published post including quiet replies. This
|
|
28500
|
+
* is "last changed" and drives the archive's updated sort.
|
|
28501
|
+
*
|
|
28502
|
+
* Both are derived, never accumulated, so a quiet reply stays quiet no
|
|
28503
|
+
* matter what later edits or deletions trigger a recalculation.
|
|
28504
|
+
*
|
|
28505
|
+
* @param rootId - Thread root post ID
|
|
28506
|
+
*/ async function recalculateThreadActivity(rootId) {
|
|
28507
|
+
const rootRows = await db.select({
|
|
28508
|
+
announcedAt: sql`MAX(
|
|
28509
|
+
CASE
|
|
28510
|
+
WHEN ${posts.quietReply} THEN NULL
|
|
28511
|
+
ELSE ${posts.publishedAt}
|
|
28512
|
+
END
|
|
28513
|
+
)`.as("announced_at"),
|
|
28514
|
+
updatedAt: sql`MAX(${posts.publishedAt})`.as("thread_updated_at")
|
|
28515
|
+
}).from(posts).where(and(eq(posts.siteId, siteId), eq(posts.threadId, rootId), eq(posts.status, "published")));
|
|
28516
|
+
const announcedAt = rootRows[0]?.announcedAt ?? null;
|
|
28517
|
+
const threadUpdatedAt = rootRows[0]?.updatedAt ?? null;
|
|
28518
|
+
const fallback = (await db.select({ updatedAt: posts.updatedAt }).from(posts).where(and(eq(posts.siteId, siteId), eq(posts.id, rootId))).limit(1))[0]?.updatedAt ?? now();
|
|
28519
|
+
await db.update(posts).set({
|
|
28520
|
+
lastActivityAt: announcedAt ?? fallback,
|
|
28521
|
+
threadUpdatedAt: threadUpdatedAt ?? fallback
|
|
28522
|
+
}).where(and(eq(posts.siteId, siteId), eq(posts.id, rootId)));
|
|
28298
28523
|
}
|
|
28299
28524
|
function normalizeCollectionIds(collectionIds) {
|
|
28300
28525
|
return [...new Set(collectionIds)];
|
|
@@ -28373,6 +28598,11 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
28373
28598
|
if (filters.excludeReplies) conditions.push(isNull(posts.replyToId));
|
|
28374
28599
|
if (filters.publishedAfter !== void 0) conditions.push(sql`${posts.publishedAt} >= ${filters.publishedAfter}`);
|
|
28375
28600
|
if (filters.publishedBefore !== void 0) conditions.push(sql`${posts.publishedAt} < ${filters.publishedBefore}`);
|
|
28601
|
+
if (filters.axisAfter !== void 0 || filters.axisBefore !== void 0) {
|
|
28602
|
+
const axis = timeAxisColumn(filters);
|
|
28603
|
+
if (filters.axisAfter !== void 0) conditions.push(sql`${axis} >= ${filters.axisAfter}`);
|
|
28604
|
+
if (filters.axisBefore !== void 0) conditions.push(sql`${axis} < ${filters.axisBefore}`);
|
|
28605
|
+
}
|
|
28376
28606
|
if (filters.mediaKinds && filters.mediaKinds.length > 0) {
|
|
28377
28607
|
const placeholders = filters.mediaKinds.map((k) => sql`${k}`);
|
|
28378
28608
|
conditions.push(sql`${posts.id} IN (
|
|
@@ -28382,12 +28612,15 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
28382
28612
|
AND media_kind IN (${sql.join(placeholders, sql`, `)})
|
|
28383
28613
|
)`);
|
|
28384
28614
|
}
|
|
28385
|
-
if (filters.hasMedia !== void 0)
|
|
28386
|
-
|
|
28387
|
-
|
|
28388
|
-
|
|
28389
|
-
|
|
28390
|
-
|
|
28615
|
+
if (filters.hasMedia !== void 0) {
|
|
28616
|
+
const mediaExists = sql`EXISTS (
|
|
28617
|
+
SELECT 1
|
|
28618
|
+
FROM media
|
|
28619
|
+
WHERE site_id = ${siteId}
|
|
28620
|
+
AND post_id = ${posts.id}
|
|
28621
|
+
)`;
|
|
28622
|
+
conditions.push(filters.hasMedia ? mediaExists : sql`NOT ${mediaExists}`);
|
|
28623
|
+
}
|
|
28391
28624
|
if (filters.hasTitle !== void 0) if (filters.hasTitle) conditions.push(sql`${posts.title} IS NOT NULL AND ${posts.title} != ''`);
|
|
28392
28625
|
else conditions.push(sql`(${posts.title} IS NULL OR ${posts.title} = '')`);
|
|
28393
28626
|
if (filters.hasRating !== void 0) conditions.push(filters.hasRating ? isNotNull(posts.rating) : isNull(posts.rating));
|
|
@@ -28404,13 +28637,26 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
28404
28637
|
}
|
|
28405
28638
|
return conditions;
|
|
28406
28639
|
}
|
|
28407
|
-
async function getLastLivePostIdInThread(threadId) {
|
|
28408
|
-
return (await db.select({ id: posts.id }).from(posts).where(and(eq(posts.siteId, siteId), eq(posts.threadId, threadId))).orderBy(desc(posts.createdAt), desc(posts.id)).limit(1))[0]?.id ?? null;
|
|
28409
|
-
}
|
|
28410
28640
|
function getCursorSortTimestamp(row, filters) {
|
|
28411
28641
|
if (filters.sortBy === "published") return row.publishedAt ?? row.createdAt;
|
|
28642
|
+
if (filters.sortBy === "thread_updated") return row.threadUpdatedAt ?? -1;
|
|
28412
28643
|
return row.status === "draft" ? row.updatedAt : row.lastActivityAt ?? -1;
|
|
28413
28644
|
}
|
|
28645
|
+
/**
|
|
28646
|
+
* Chronological sort key for `list()`.
|
|
28647
|
+
*
|
|
28648
|
+
* Shared by the ORDER BY and the keyset cursor comparison — they must read
|
|
28649
|
+
* the same expression or pagination silently skips or repeats rows.
|
|
28650
|
+
*/ function buildSortTimestampExpr(filters) {
|
|
28651
|
+
if (filters.sortBy === "published") return sql`coalesce(${posts.publishedAt}, ${posts.createdAt})`;
|
|
28652
|
+
if (filters.sortBy === "thread_updated") return posts.threadUpdatedAt;
|
|
28653
|
+
if (filters.status === "draft") return posts.updatedAt;
|
|
28654
|
+
if (filters.status === "published") return posts.lastActivityAt;
|
|
28655
|
+
return sql`CASE
|
|
28656
|
+
WHEN ${posts.status} = 'draft' THEN ${posts.updatedAt}
|
|
28657
|
+
ELSE ${posts.lastActivityAt}
|
|
28658
|
+
END`;
|
|
28659
|
+
}
|
|
28414
28660
|
function buildLexicographicCursorCondition(keys) {
|
|
28415
28661
|
const [first, ...rest] = keys;
|
|
28416
28662
|
const comparison = first.direction === "desc" ? sql`${first.expr} < ${first.value}` : sql`${first.expr} > ${first.value}`;
|
|
@@ -28424,10 +28670,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
28424
28670
|
if (!filters.cursor) return null;
|
|
28425
28671
|
const cursorPost = (await db.select().from(posts).where(and(eq(posts.siteId, siteId), eq(posts.id, filters.cursor))).limit(1))[0];
|
|
28426
28672
|
if (!cursorPost) return null;
|
|
28427
|
-
const sortTimestampExpr = filters
|
|
28428
|
-
WHEN ${posts.status} = 'draft' THEN ${posts.updatedAt}
|
|
28429
|
-
ELSE ${posts.lastActivityAt}
|
|
28430
|
-
END`;
|
|
28673
|
+
const sortTimestampExpr = buildSortTimestampExpr(filters);
|
|
28431
28674
|
const pinnedSortExpr = sql`coalesce(${posts.pinnedAt}, -1)`;
|
|
28432
28675
|
const featuredPublishedSortExpr = sql`coalesce(
|
|
28433
28676
|
${posts.publishedAt}, ${posts.createdAt}, -1
|
|
@@ -28550,8 +28793,10 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
28550
28793
|
previewProvider: row.previewProvider,
|
|
28551
28794
|
replyToId: row.replyToId,
|
|
28552
28795
|
threadId: row.threadId,
|
|
28796
|
+
quietReply: row.quietReply,
|
|
28553
28797
|
publishedAt: row.publishedAt,
|
|
28554
28798
|
lastActivityAt: row.lastActivityAt ?? row.publishedAt ?? row.updatedAt,
|
|
28799
|
+
threadUpdatedAt: row.threadUpdatedAt ?? row.lastActivityAt ?? row.publishedAt ?? row.updatedAt,
|
|
28555
28800
|
createdAt: row.createdAt,
|
|
28556
28801
|
updatedAt: row.updatedAt
|
|
28557
28802
|
};
|
|
@@ -28616,16 +28861,18 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
28616
28861
|
if (options?.hasRating) conditions.push(isNotNull(posts.rating));
|
|
28617
28862
|
return conditions;
|
|
28618
28863
|
}
|
|
28619
|
-
|
|
28864
|
+
/**
|
|
28865
|
+
* Thread activity timestamp used to order collection threads.
|
|
28866
|
+
*
|
|
28867
|
+
* Rows here are Thread *members* grouped by thread_id, so the definition is
|
|
28868
|
+
* reached through a subquery to the root. The outer MAX only collapses the
|
|
28869
|
+
* group — the subquery returns one value per thread. The outer COALESCE
|
|
28870
|
+
* covers a member whose root row is missing.
|
|
28871
|
+
*/ function buildCollectionThreadActivityExpr(alias) {
|
|
28620
28872
|
return sql`MAX(
|
|
28621
28873
|
COALESCE(
|
|
28622
28874
|
(
|
|
28623
|
-
SELECT
|
|
28624
|
-
WHEN root.updated_at > root.created_at
|
|
28625
|
-
AND root.updated_at > COALESCE(root.last_activity_at, -1)
|
|
28626
|
-
THEN root.updated_at
|
|
28627
|
-
ELSE COALESCE(root.last_activity_at, root.updated_at)
|
|
28628
|
-
END
|
|
28875
|
+
SELECT ${buildRootActivityExpr(rootActivityColumns("root"))}
|
|
28629
28876
|
FROM post AS root
|
|
28630
28877
|
WHERE root.site_id = ${siteId}
|
|
28631
28878
|
AND root.id = ${posts.threadId}
|
|
@@ -28770,10 +29017,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
28770
29017
|
const conditions = buildFilterConditions(filters);
|
|
28771
29018
|
const cursorCondition = await buildListCursorCondition(filters);
|
|
28772
29019
|
if (filters.cursor && !cursorCondition) return [];
|
|
28773
|
-
const sortTimestamp = filters
|
|
28774
|
-
WHEN ${posts.status} = 'draft' THEN ${posts.updatedAt}
|
|
28775
|
-
ELSE ${posts.lastActivityAt}
|
|
28776
|
-
END`;
|
|
29020
|
+
const sortTimestamp = buildSortTimestampExpr(filters);
|
|
28777
29021
|
if (cursorCondition) conditions.push(cursorCondition);
|
|
28778
29022
|
const ratingPresence = sql`CASE
|
|
28779
29023
|
WHEN ${posts.rating} IS NULL THEN 0
|
|
@@ -28797,9 +29041,19 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
28797
29041
|
excludeReplies: true
|
|
28798
29042
|
});
|
|
28799
29043
|
if (afterId !== void 0) conditions.push(sql`${posts.id} > ${afterId}`);
|
|
29044
|
+
const threadModifiedAt = sql`COALESCE(
|
|
29045
|
+
(
|
|
29046
|
+
SELECT MAX(member.updated_at)
|
|
29047
|
+
FROM post AS member
|
|
29048
|
+
WHERE member.site_id = ${siteId}
|
|
29049
|
+
AND member.thread_id = "post"."id"
|
|
29050
|
+
AND member.status = 'published'
|
|
29051
|
+
),
|
|
29052
|
+
"post"."updated_at"
|
|
29053
|
+
)`;
|
|
28800
29054
|
const rows = await db.select({
|
|
28801
29055
|
id: posts.id,
|
|
28802
|
-
updatedAt:
|
|
29056
|
+
updatedAt: threadModifiedAt,
|
|
28803
29057
|
featuredAt: posts.featuredAt
|
|
28804
29058
|
}).from(posts).where(conditions.length > 0 ? and(...conditions) : void 0).orderBy(asc(posts.id)).limit(limit);
|
|
28805
29059
|
if (rows.length === 0) return [];
|
|
@@ -28846,8 +29100,9 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
28846
29100
|
return (await db.select({ id: posts.id }).from(posts).where(conditions.length > 0 ? and(...conditions) : void 0).limit(normalizedLimit)).length;
|
|
28847
29101
|
},
|
|
28848
29102
|
async countByYearMonth(filters = {}) {
|
|
28849
|
-
const
|
|
28850
|
-
const
|
|
29103
|
+
const axis = timeAxisColumn(filters);
|
|
29104
|
+
const conditions = [...buildFilterConditions(filters), isNotNull(axis)];
|
|
29105
|
+
const publishedYearMonthExpr = buildYearMonthExpr(axis);
|
|
28851
29106
|
return db.select({
|
|
28852
29107
|
yearMonth: publishedYearMonthExpr.as("year_month"),
|
|
28853
29108
|
count: sql`CAST(count(*) AS INTEGER)`.as("count")
|
|
@@ -28860,6 +29115,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
28860
29115
|
const requestedStatus = data.status !== void 0 ? ensurePostStatus(data.status) : void 0;
|
|
28861
29116
|
const requestedVisibility = data.visibility !== void 0 ? ensurePostVisibility(data.visibility) : void 0;
|
|
28862
29117
|
const rating = ensurePostRating(data.rating);
|
|
29118
|
+
const isQuietReply = Boolean(data.replyToId) && data.quietReply === true;
|
|
28863
29119
|
const rawBody = data.bodyMarkdown ? markdownToTiptapJson(data.bodyMarkdown) : data.body ?? null;
|
|
28864
29120
|
const trimmedBody = rawBody ? trimTiptapBody(rawBody) : null;
|
|
28865
29121
|
const preparedBody = trimmedBody ? tryPreparePostBodyHtml(id, trimmedBody) : null;
|
|
@@ -28888,16 +29144,15 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
28888
29144
|
const parent = await this.getById(data.replyToId);
|
|
28889
29145
|
if (!parent) throw new NotFoundError("Parent post");
|
|
28890
29146
|
if (resolvedPinnedAt !== null) throw new ConflictError("Cannot pin a thread reply. Pin the root post instead.");
|
|
28891
|
-
const
|
|
28892
|
-
if (
|
|
29147
|
+
const tailId = (await this.getThreadTailIds([parent.threadId], { includeDrafts: true })).get(parent.threadId);
|
|
29148
|
+
if (tailId && tailId !== parent.id) throw new ConflictError((await this.getById(tailId))?.status === "draft" ? "This thread ends with an unpublished draft. Finish that draft or discard it, then reply." : "This post is no longer the end of the thread. Reply to the latest post instead.");
|
|
28893
29149
|
threadId = parent.threadId;
|
|
28894
29150
|
const root = parent.threadId === parent.id ? parent : await this.getById(parent.threadId);
|
|
28895
|
-
if (root)
|
|
28896
|
-
if (data.status !== "draft") status = root.status;
|
|
28897
|
-
}
|
|
29151
|
+
if (root && data.status !== "draft") status = root.status;
|
|
28898
29152
|
visibility = null;
|
|
28899
29153
|
if ((data.collectionIds?.length ?? 0) > 0 || (data.collectionEntries?.length ?? 0) > 0) throw new ConflictError("Cannot set Collections while creating a Thread reply. Set them on the Thread root instead.");
|
|
28900
29154
|
}
|
|
29155
|
+
if (status === "draft" && resolvedFeaturedAt !== null) throw new ConflictError("Publish this post before featuring it.");
|
|
28901
29156
|
assertDraftPublishedAt(status, data.publishedAt);
|
|
28902
29157
|
const publishedAt = status === "published" ? data.publishedAt ?? timestamp : null;
|
|
28903
29158
|
let slug;
|
|
@@ -28975,8 +29230,10 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
28975
29230
|
rating,
|
|
28976
29231
|
replyToId: data.replyToId ?? null,
|
|
28977
29232
|
threadId,
|
|
29233
|
+
quietReply: isQuietReply,
|
|
28978
29234
|
publishedAt,
|
|
28979
29235
|
lastActivityAt: publishedAt ?? timestamp,
|
|
29236
|
+
threadUpdatedAt: publishedAt ?? timestamp,
|
|
28980
29237
|
createdAt: timestamp,
|
|
28981
29238
|
updatedAt: timestamp
|
|
28982
29239
|
}));
|
|
@@ -29026,8 +29283,10 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
29026
29283
|
rating,
|
|
29027
29284
|
replyToId: data.replyToId ?? null,
|
|
29028
29285
|
threadId,
|
|
29286
|
+
quietReply: isQuietReply,
|
|
29029
29287
|
publishedAt,
|
|
29030
29288
|
lastActivityAt: publishedAt ?? timestamp,
|
|
29289
|
+
threadUpdatedAt: publishedAt ?? timestamp,
|
|
29031
29290
|
createdAt: timestamp,
|
|
29032
29291
|
updatedAt: timestamp
|
|
29033
29292
|
});
|
|
@@ -29068,7 +29327,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
29068
29327
|
}
|
|
29069
29328
|
const post = await this.getById(id);
|
|
29070
29329
|
if (!post) throw new ConflictError(`Slug "${slug}" could not be resolved`);
|
|
29071
|
-
if (data.replyToId && status === "published"
|
|
29330
|
+
if (data.replyToId && status === "published") await recalculateThreadActivity(threadId);
|
|
29072
29331
|
return post;
|
|
29073
29332
|
},
|
|
29074
29333
|
async createWithAttachments(data, attachments, deps, summaryConfig) {
|
|
@@ -29107,10 +29366,12 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
29107
29366
|
if (!item) continue;
|
|
29108
29367
|
const { data, attachments } = item;
|
|
29109
29368
|
const prevPost = created[i - 1];
|
|
29369
|
+
const rootPublishedAt = created[0]?.publishedAt ?? void 0;
|
|
29110
29370
|
const postData = {
|
|
29111
29371
|
...data,
|
|
29112
29372
|
replyToId: i === 0 ? data.replyToId : prevPost?.id,
|
|
29113
|
-
quietReply: extendsExistingThreadQuietly ? true : data.quietReply
|
|
29373
|
+
quietReply: extendsExistingThreadQuietly ? true : data.quietReply,
|
|
29374
|
+
publishedAt: i === 0 ? data.publishedAt : data.publishedAt ?? rootPublishedAt
|
|
29114
29375
|
};
|
|
29115
29376
|
try {
|
|
29116
29377
|
const post = await this.createWithAttachments(postData, attachments, deps, summaryConfig);
|
|
@@ -29165,6 +29426,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
29165
29426
|
else if (data.pinned !== void 0) updates.pinnedAt = data.pinned ? timestamp : null;
|
|
29166
29427
|
if (data.featuredAt !== void 0) updates.featuredAt = data.featuredAt;
|
|
29167
29428
|
else if (data.featured !== void 0) updates.featuredAt = data.featured ? timestamp : null;
|
|
29429
|
+
if (nextStatus === "draft" && updates.featuredAt) throw new ConflictError("Publish this post before featuring it.");
|
|
29168
29430
|
let updatedBody;
|
|
29169
29431
|
if (data.body !== void 0 || data.bodyMarkdown !== void 0) {
|
|
29170
29432
|
const rawBody = data.bodyMarkdown ? markdownToTiptapJson(data.bodyMarkdown) : data.body ?? null;
|
|
@@ -29206,7 +29468,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
29206
29468
|
if (!(needsCascade || needsReplyVisibilityCleanup || needsCollectionSync || needsPageNavDelete || needsPageNavUrlUpdate)) {
|
|
29207
29469
|
const result = await db.update(posts).set(updates).where(and(eq(posts.siteId, siteId), eq(posts.id, id))).returning();
|
|
29208
29470
|
if (needsThreadActivityRecalc) {
|
|
29209
|
-
await
|
|
29471
|
+
await recalculateThreadActivity(existing.threadId);
|
|
29210
29472
|
return this.getById(id);
|
|
29211
29473
|
}
|
|
29212
29474
|
return hydratePost(result[0]);
|
|
@@ -29291,7 +29553,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
29291
29553
|
}
|
|
29292
29554
|
});
|
|
29293
29555
|
if (needsThreadActivityRecalc) {
|
|
29294
|
-
await
|
|
29556
|
+
await recalculateThreadActivity(existing.threadId);
|
|
29295
29557
|
return this.getById(id);
|
|
29296
29558
|
}
|
|
29297
29559
|
return hydratePost(updateResult?.[0]);
|
|
@@ -29374,7 +29636,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
29374
29636
|
else {
|
|
29375
29637
|
await db.update(posts).set({ replyToId: existing.replyToId }).where(and(eq(posts.siteId, siteId), eq(posts.replyToId, id)));
|
|
29376
29638
|
await db.delete(posts).where(and(eq(posts.siteId, siteId), eq(posts.id, id)));
|
|
29377
|
-
await
|
|
29639
|
+
await recalculateThreadActivity(existing.threadId);
|
|
29378
29640
|
}
|
|
29379
29641
|
return true;
|
|
29380
29642
|
},
|
|
@@ -29384,6 +29646,28 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
29384
29646
|
async getThread(rootId) {
|
|
29385
29647
|
return hydratePosts(await db.select().from(posts).where(and(eq(posts.siteId, siteId), eq(posts.threadId, rootId))).orderBy(posts.createdAt, posts.id));
|
|
29386
29648
|
},
|
|
29649
|
+
async getThreadPosition(postId) {
|
|
29650
|
+
const target = (await db.select({
|
|
29651
|
+
replyToId: posts.replyToId,
|
|
29652
|
+
threadId: posts.threadId
|
|
29653
|
+
}).from(posts).where(and(eq(posts.siteId, siteId), eq(posts.id, postId))).limit(1))[0];
|
|
29654
|
+
if (!target) return 0;
|
|
29655
|
+
if (!target.replyToId) return 1;
|
|
29656
|
+
const threadRows = await db.select({
|
|
29657
|
+
id: posts.id,
|
|
29658
|
+
replyToId: posts.replyToId
|
|
29659
|
+
}).from(posts).where(and(eq(posts.siteId, siteId), eq(posts.threadId, target.threadId)));
|
|
29660
|
+
const parentOf = new Map(threadRows.map((row) => [row.id, row.replyToId]));
|
|
29661
|
+
let position = 1;
|
|
29662
|
+
let cursor = target.replyToId;
|
|
29663
|
+
const visited = new Set([postId]);
|
|
29664
|
+
while (cursor && !visited.has(cursor)) {
|
|
29665
|
+
visited.add(cursor);
|
|
29666
|
+
position += 1;
|
|
29667
|
+
cursor = parentOf.get(cursor) ?? null;
|
|
29668
|
+
}
|
|
29669
|
+
return position;
|
|
29670
|
+
},
|
|
29387
29671
|
async updateThreadStatusAndVisibility(rootId, status, visibility) {
|
|
29388
29672
|
const nextStatus = ensurePostStatus(status);
|
|
29389
29673
|
const nextVisibility = ensurePostVisibility(visibility);
|
|
@@ -29417,7 +29701,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
29417
29701
|
updatedAt: timestamp
|
|
29418
29702
|
}).where(and(eq(posts.siteId, siteId), eq(posts.threadId, rootId), isNotNull(posts.replyToId)));
|
|
29419
29703
|
});
|
|
29420
|
-
await
|
|
29704
|
+
await recalculateThreadActivity(rootId);
|
|
29421
29705
|
},
|
|
29422
29706
|
async getReplyCounts(postIds) {
|
|
29423
29707
|
if (postIds.length === 0) return /* @__PURE__ */ new Map();
|
|
@@ -29698,20 +29982,21 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
29698
29982
|
}
|
|
29699
29983
|
return result;
|
|
29700
29984
|
},
|
|
29701
|
-
async
|
|
29985
|
+
async getThreadTailIds(threadIds, options) {
|
|
29702
29986
|
const result = /* @__PURE__ */ new Map();
|
|
29703
29987
|
if (threadIds.length === 0) return result;
|
|
29704
29988
|
const unique = [...new Set(threadIds)];
|
|
29705
29989
|
const rows = await db.select({
|
|
29706
29990
|
threadId: posts.threadId,
|
|
29707
29991
|
id: posts.id
|
|
29708
|
-
}).from(posts).where(and(eq(posts.siteId, siteId), inArray(posts.threadId, unique), eq(posts.status, "published"))).orderBy(posts.threadId, desc(posts.createdAt), desc(posts.id));
|
|
29992
|
+
}).from(posts).where(and(eq(posts.siteId, siteId), inArray(posts.threadId, unique), ...options?.includeDrafts ? [] : [eq(posts.status, "published")])).orderBy(posts.threadId, desc(posts.createdAt), desc(posts.id));
|
|
29709
29993
|
for (const row of rows) if (!result.has(row.threadId)) result.set(row.threadId, row.id);
|
|
29710
29994
|
return result;
|
|
29711
29995
|
},
|
|
29712
29996
|
async getDistinctYears(filters = {}) {
|
|
29713
|
-
const
|
|
29714
|
-
const
|
|
29997
|
+
const axis = timeAxisColumn(filters);
|
|
29998
|
+
const conditions = [...buildFilterConditions(filters), isNotNull(axis)];
|
|
29999
|
+
const publishedYearExpr = buildYearExpr(axis);
|
|
29715
30000
|
return (await db.select({ year: publishedYearExpr.as("year") }).from(posts).where(conditions.length > 0 ? and(...conditions) : void 0).groupBy(publishedYearExpr).orderBy(desc(publishedYearExpr))).map((r) => parseInt(r.year, 10));
|
|
29716
30001
|
},
|
|
29717
30002
|
async reindexBodyText(options = {}) {
|
|
@@ -31073,10 +31358,12 @@ function toPublicPost(post, mediaList, threadCollections, appConfig, options) {
|
|
|
31073
31358
|
previewImageUrl,
|
|
31074
31359
|
replyToId: post.replyToId,
|
|
31075
31360
|
threadId: post.threadId,
|
|
31361
|
+
quietReply: post.quietReply,
|
|
31076
31362
|
pinnedAt: post.pinnedAt,
|
|
31077
31363
|
featuredAt: post.featuredAt,
|
|
31078
31364
|
publishedAt: post.publishedAt,
|
|
31079
31365
|
lastActivityAt: post.lastActivityAt,
|
|
31366
|
+
threadUpdatedAt: post.threadUpdatedAt,
|
|
31080
31367
|
createdAt: post.createdAt,
|
|
31081
31368
|
updatedAt: post.updatedAt,
|
|
31082
31369
|
attachments: mediaList.map((media) => toApiAttachment(media, r2PublicUrl, imageTransformUrl, s3PublicUrl, localPublicUrl, sitePathPrefix)),
|
|
@@ -31366,7 +31653,7 @@ composeRoutes.post("/thread", async (c) => {
|
|
|
31366
31653
|
const root = (await c.var.services.posts.createThreadWithAttachments(postSchemas.map((data, index) => ({
|
|
31367
31654
|
data: {
|
|
31368
31655
|
format: data.format,
|
|
31369
|
-
slug:
|
|
31656
|
+
slug: data.slug || void 0,
|
|
31370
31657
|
title: data.format === "quote" ? data.sourceName || void 0 : data.title || void 0,
|
|
31371
31658
|
body: data.body || void 0,
|
|
31372
31659
|
bodyMarkdown: data.bodyMarkdown || void 0,
|
|
@@ -31378,7 +31665,7 @@ composeRoutes.post("/thread", async (c) => {
|
|
|
31378
31665
|
collectionIds: index === 0 ? data.collectionIds : void 0,
|
|
31379
31666
|
replyToId: index === 0 ? data.replyToId : void 0,
|
|
31380
31667
|
quietReply: data.quietReply,
|
|
31381
|
-
publishedAt:
|
|
31668
|
+
publishedAt: data.publishedAt
|
|
31382
31669
|
},
|
|
31383
31670
|
attachments: data.attachments
|
|
31384
31671
|
})), storageOpts, summaryConfig))[0];
|
|
@@ -33397,16 +33684,11 @@ function createCollectionService(db, siteId, paths, databaseSchema = sqliteSchem
|
|
|
33397
33684
|
}).filter((row) => row !== null);
|
|
33398
33685
|
}
|
|
33399
33686
|
async function listDirectoryCollections() {
|
|
33400
|
-
const threadActivityAt =
|
|
33401
|
-
|
|
33402
|
-
|
|
33403
|
-
|
|
33404
|
-
|
|
33405
|
-
${posts.lastActivityAt},
|
|
33406
|
-
${posts.publishedAt},
|
|
33407
|
-
${posts.updatedAt}
|
|
33408
|
-
)
|
|
33409
|
-
END`;
|
|
33687
|
+
const threadActivityAt = buildRootActivityExpr({
|
|
33688
|
+
lastActivityAt: posts.lastActivityAt,
|
|
33689
|
+
publishedAt: posts.publishedAt,
|
|
33690
|
+
updatedAt: posts.updatedAt
|
|
33691
|
+
});
|
|
33410
33692
|
const threadCount = sql`
|
|
33411
33693
|
CAST(COUNT(
|
|
33412
33694
|
CASE
|
|
@@ -33918,8 +34200,10 @@ function createCollectionService(db, siteId, paths, databaseSchema = sqliteSchem
|
|
|
33918
34200
|
previewProvider: null,
|
|
33919
34201
|
replyToId: row.reply_to_id,
|
|
33920
34202
|
threadId: row.thread_id,
|
|
34203
|
+
quietReply: Boolean(row.quiet_reply),
|
|
33921
34204
|
publishedAt: row.published_at,
|
|
33922
34205
|
lastActivityAt: row.last_activity_at ?? row.published_at ?? row.updated_at,
|
|
34206
|
+
threadUpdatedAt: row.thread_updated_at ?? row.last_activity_at ?? row.published_at ?? row.updated_at,
|
|
33923
34207
|
createdAt: row.created_at,
|
|
33924
34208
|
updatedAt: row.updated_at
|
|
33925
34209
|
},
|
|
@@ -34965,7 +35249,7 @@ function createSiteAdminService(db, databaseSchema = sqliteSchemaBundle, databas
|
|
|
34965
35249
|
const themeCss = buildThemeStyle(activeTheme, appConfig.themeMode, fontOverrides);
|
|
34966
35250
|
const navItemList = await navItems.list();
|
|
34967
35251
|
const appleTouchKey = allSettings[SETTINGS_KEYS.SITE_FAVICON_APPLE_TOUCH];
|
|
34968
|
-
const { createExportService } = await import("./export-
|
|
35252
|
+
const { createExportService } = await import("./export-Dl5KCiEs.js").then((n) => n.n);
|
|
34969
35253
|
const exportService = createExportService({
|
|
34970
35254
|
collections,
|
|
34971
35255
|
media: mediaService,
|