@jant/core 0.5.4 → 0.6.1
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/telegram/register-webhooks.js +93 -0
- package/dist/app-CMSW_AYG.js +6 -0
- package/dist/{app-BtNdUAqz.js → app-DYQdDMs8.js} +2249 -387
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-BRTh1ii1.js +274 -0
- package/dist/client/_assets/client-CO4b-RKd.css +2 -0
- package/dist/client/_assets/{client-auth-DJ_5wx9N.js → client-auth-CSNcTJwP.js} +81 -81
- package/dist/{env-CgaH9Mut.js → env-C7e2Nlnt.js} +30 -1
- package/dist/{export-CR9Megtb.js → export-Bbn86HmS.js} +1 -1
- package/dist/{github-sync-DYZq9rQp.js → github-sync-CBQPRZ8H.js} +1 -1
- package/dist/{github-sync-8Vv06aCr.js → github-sync-dXsiZa_e.js} +2 -2
- package/dist/index.js +4 -4
- package/dist/node.js +61 -5
- package/package.json +2 -1
- package/src/__tests__/helpers/app.ts +15 -2
- package/src/app.tsx +3 -0
- package/src/client/thread-context.ts +146 -2
- package/src/client/tiptap/__tests__/link-toolbar.test.ts +1 -1
- package/src/client/tiptap/bubble-menu.ts +1 -16
- package/src/client/tiptap/extensions.ts +2 -6
- package/src/client/tiptap/link-toolbar.ts +0 -21
- package/src/client/tiptap/toolbar-mode.ts +0 -43
- package/src/db/migrations/0022_old_gressill.sql +24 -0
- package/src/db/migrations/0023_broad_terror.sql +20 -0
- package/src/db/migrations/0024_red_the_twelve.sql +3 -0
- package/src/db/migrations/0025_exotic_wendell_rand.sql +1 -0
- package/src/db/migrations/meta/0022_snapshot.json +2267 -0
- package/src/db/migrations/meta/0023_snapshot.json +2396 -0
- package/src/db/migrations/meta/0024_snapshot.json +2417 -0
- package/src/db/migrations/meta/0025_snapshot.json +2424 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0020_bizarre_smasher.sql +24 -0
- package/src/db/migrations/pg/0021_sharp_puppet_master.sql +20 -0
- package/src/db/migrations/pg/0022_blushing_blue_shield.sql +3 -0
- package/src/db/migrations/pg/0023_organic_zemo.sql +1 -0
- package/src/db/migrations/pg/meta/0020_snapshot.json +2904 -0
- package/src/db/migrations/pg/meta/0021_snapshot.json +3060 -0
- package/src/db/migrations/pg/meta/0022_snapshot.json +3078 -0
- package/src/db/migrations/pg/meta/0023_snapshot.json +3084 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +82 -0
- package/src/db/schema.ts +90 -0
- package/src/i18n/coverage.generated.ts +2 -2
- package/src/i18n/locales/public/en.po +8 -0
- package/src/i18n/locales/public/zh-Hans.po +8 -0
- package/src/i18n/locales/public/zh-Hant.po +8 -0
- package/src/i18n/locales/settings/en.po +135 -0
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +136 -1
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +136 -1
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/image-dimensions.test.ts +314 -0
- package/src/lib/__tests__/telegram-entities.test.ts +180 -0
- package/src/lib/__tests__/telegram-pool-webhooks.test.ts +127 -0
- package/src/lib/env.ts +45 -0
- package/src/lib/ids.ts +3 -0
- package/src/lib/image-dimensions.ts +258 -0
- package/src/lib/telegram-entities.ts +240 -0
- package/src/lib/telegram-pool-webhooks.ts +86 -0
- package/src/lib/telegram-settings-status.tsx +109 -0
- package/src/lib/telegram.ts +363 -0
- package/src/node/runtime.ts +6 -0
- package/src/routes/api/__tests__/telegram.test.ts +612 -0
- package/src/routes/api/telegram.ts +782 -0
- package/src/routes/api/upload-multipart.ts +34 -12
- package/src/routes/api/upload.ts +23 -2
- package/src/routes/dash/settings.tsx +131 -1
- package/src/routes/pages/__tests__/post-page-title.test.ts +70 -0
- package/src/routes/pages/page.tsx +3 -2
- package/src/runtime/cloudflare.ts +20 -9
- package/src/runtime/node.ts +20 -9
- package/src/runtime/site.ts +2 -1
- package/src/services/__tests__/telegram.test.ts +148 -0
- package/src/services/index.ts +9 -0
- package/src/services/telegram.ts +613 -0
- package/src/services/upload-session.ts +39 -12
- package/src/styles/tokens.css +1 -0
- package/src/styles/ui.css +117 -38
- package/src/types/app-context.ts +6 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +40 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +48 -17
- package/src/ui/dash/settings/TelegramContent.tsx +549 -0
- package/src/ui/feed/ThreadPreview.tsx +90 -38
- package/src/ui/feed/__tests__/thread-preview.test.ts +66 -5
- package/src/ui/pages/PostPage.tsx +77 -15
- package/dist/app-DLINgGBd.js +0 -6
- package/dist/client/_assets/client-BErXNT6k.css +0 -2
- package/dist/client/_assets/client-CtAgWT8i.js +0 -274
|
@@ -69,6 +69,8 @@ var env_exports = /* @__PURE__ */ __exportAll({
|
|
|
69
69
|
getSiteResolutionMode: () => getSiteResolutionMode,
|
|
70
70
|
getSiteUrl: () => getSiteUrl,
|
|
71
71
|
getStorageDriverEnv: () => getStorageDriverEnv,
|
|
72
|
+
getTelegramBotPool: () => getTelegramBotPool,
|
|
73
|
+
getTelegramWebhookSecret: () => getTelegramWebhookSecret,
|
|
72
74
|
parsePortValue: () => parsePortValue,
|
|
73
75
|
shouldTrustProxy: () => shouldTrustProxy,
|
|
74
76
|
shouldUseSecureCookies: () => shouldUseSecureCookies
|
|
@@ -243,10 +245,37 @@ function getGitHubAppConfig(env) {
|
|
|
243
245
|
function shouldTrustProxy(env) {
|
|
244
246
|
return getEnvString(env, "TRUST_PROXY") === "true";
|
|
245
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Parses the platform-managed Telegram bot pool from `TELEGRAM_BOT_TOKENS`.
|
|
250
|
+
*
|
|
251
|
+
* The env value is a comma-separated list of `<bot_id>:<secret>` tokens. The
|
|
252
|
+
* first entry is the public-facing bot (`bot1`); the rest are surfaced only
|
|
253
|
+
* contextually when a binding code reaches an already-bound bot slot.
|
|
254
|
+
*
|
|
255
|
+
* @param env - Runtime environment bindings
|
|
256
|
+
* @returns Parsed pool bots in declared order; empty when unset/invalid
|
|
257
|
+
* @example
|
|
258
|
+
* getTelegramBotPool({ TELEGRAM_BOT_TOKENS: "111:aaa,222:bbb" });
|
|
259
|
+
*/ function getTelegramBotPool(env) {
|
|
260
|
+
const raw = getEnvString(env, "TELEGRAM_BOT_TOKENS");
|
|
261
|
+
if (!raw) return [];
|
|
262
|
+
return raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((token) => {
|
|
263
|
+
return {
|
|
264
|
+
botId: token.split(":")[0]?.trim() ?? "",
|
|
265
|
+
token
|
|
266
|
+
};
|
|
267
|
+
}).filter((bot) => bot.botId.length > 0);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Returns the shared `secret_token` used when registering every pool bot's
|
|
271
|
+
* webhook. Only meaningful alongside `TELEGRAM_BOT_TOKENS`.
|
|
272
|
+
*/ function getTelegramWebhookSecret(env) {
|
|
273
|
+
return getEnvString(env, "TELEGRAM_WEBHOOK_SECRET");
|
|
274
|
+
}
|
|
246
275
|
function shouldUseSecureCookies(env, publicRequestUrl) {
|
|
247
276
|
const siteOrigin = getConfiguredSingleSiteOrigin(env);
|
|
248
277
|
if (siteOrigin) return new URL(siteOrigin).protocol === "https:";
|
|
249
278
|
return new URL(publicRequestUrl).protocol === "https:";
|
|
250
279
|
}
|
|
251
280
|
//#endregion
|
|
252
|
-
export {
|
|
281
|
+
export { shouldTrustProxy as C, getTelegramWebhookSecret as S, coalesceDisplayText as T, getInternalAdminToken as _, getConfiguredSingleSiteUrl as a, getSiteResolutionMode as b, getDevApiToken as c, getHostedControlPlaneBaseUrl as d, getHostedControlPlaneDomainCheckSecret as f, getHostedControlPlaneSsoSecret as g, getHostedControlPlaneProviderLabel as h, getConfiguredSingleSitePathPrefix as i, getEnvString as l, getHostedControlPlaneInternalToken as m, getAuthSecret as n, getConfiguredStorageDriver as o, getHostedControlPlaneInternalBaseUrl as p, getConfiguredSingleSiteOrigin as r, getCorsOrigins as s, env_exports as t, getGitHubAppConfig as u, getLocalStoragePath as v, shouldUseSecureCookies as w, getTelegramBotPool as x, getPort as y };
|
|
@@ -3270,7 +3270,7 @@ function serializeMarkdownDocument(doc) {
|
|
|
3270
3270
|
}
|
|
3271
3271
|
//#endregion
|
|
3272
3272
|
//#region src/styles/tokens.css?raw
|
|
3273
|
-
var tokens_default = "/**\n * Design Tokens\n *\n * CSS custom properties for all visual aspects of the UI.\n * These are the stable customization API — override in custom CSS\n * to change typography, layout, surfaces, and element sizing.\n */\n\n:root {\n /* Typography — Font families */\n --font-cjk-serif-fallback:\n \"Songti SC\", STSong, SimSun, \"Songti TC\", PMingLiU, MingLiU,\n \"Noto Serif SC\", \"Noto Serif CJK SC\", \"Noto Serif TC\", \"Noto Serif CJK TC\";\n --font-body:\n system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\n \"Helvetica Neue\", Helvetica, Arial, \"PingFang TC\", \"PingFang SC\",\n \"Hiragino Sans CNS\", \"Hiragino Sans GB\", \"Microsoft JhengHei\",\n \"Microsoft YaHei\", \"Noto Sans CJK TC\", \"Noto Sans CJK SC\", sans-serif;\n --font-heading:\n \"New York Small\", \"New York\", \"Iowan Old Style\", Charter,\n \"Bitstream Charter\", \"Source Serif 4\", Cambria, \"Sitka Text\", Georgia,\n var(--font-cjk-serif-fallback), ui-serif, serif;\n --font-site-title:\n \"New York Small\", \"New York\", \"Iowan Old Style\", Charter,\n \"Bitstream Charter\", \"Source Serif 4\", Cambria, \"Sitka Text\", Georgia,\n var(--font-cjk-serif-fallback), ui-serif, serif;\n --font-serif:\n var(--font-cjk-serif-fallback), ui-serif, \"New York Small\", \"New York\",\n \"Iowan Old Style\", Charter, Georgia, \"Times New Roman\", Times, serif;\n --font-ui:\n system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\n \"Helvetica Neue\", Helvetica, Arial, \"PingFang TC\", \"PingFang SC\",\n \"Hiragino Sans CNS\", \"Hiragino Sans GB\", \"Microsoft JhengHei\",\n \"Microsoft YaHei\", \"Noto Sans CJK TC\", \"Noto Sans CJK SC\", sans-serif;\n --font-mono:\n ui-monospace, Menlo, Monaco, Consolas, \"Cascadia Code\", \"Courier New\",\n monospace;\n /*\n * Blockquote font family.\n *\n * Defaults to `inherit` so blockquotes follow the body font in sans-only\n * themes (Clean, Friendly, Bold) and serif-only themes (Tufte, Bookish).\n * Mixed themes that want a distinct blockquote voice — e.g. Classic, which\n * pairs a sans body with serif headings — override this token in their\n * `cssVariables` to point at a serif stack. This also restores a type-level\n * distinction for CJK, where italic is disabled.\n */\n --font-blockquote: inherit;\n\n /* Typography — Font weights */\n --fw-light: 300;\n --fw-regular: 400;\n --fw-medium: 500;\n --fw-semibold: 600;\n --fw-bold: 700;\n --fw-extrabold: 800;\n /*\n * Unified type scale.\n *\n * Every font-size in the system — reading content AND UI controls —\n * must reference one of these tokens. No raw rem/px values elsewhere.\n *\n * --type-display 2.7rem (40.5px) — page titles, h1\n * --type-title 2.1rem (31.5px) — h2, feed card titles\n * --type-subtitle 1.8rem (27px) — h3\n * --type-body 1.4rem (21px) — running text, form inputs\n * --type-secondary 1.1rem (16.5px) — meta, nav, sidenotes, UI labels\n * --type-base 1.0rem (15px) — code, UI buttons, controls\n * --type-sm 0.9rem (13.5px) — code blocks, small UI text\n * --type-xs 0.8rem (12px) — captions, descriptions, chips\n * --type-2xs 0.65rem (9.75px) — tiny labels, file sizes, badges\n */\n --type-display: 2.7rem;\n --type-title: 2.1rem;\n --type-subtitle: 1.8rem;\n --type-body: 1.4rem;\n --type-secondary: 1.1rem;\n --type-base: 1rem;\n --type-sm: 0.9rem;\n --type-xs: 0.8rem;\n --type-2xs: 0.65rem;\n\n /* Content reading tokens — desktop maps to core scale,\n mobile overrides below cap sizes for small screens.\n --type-content-scale uniformly scales all content sizes\n without affecting UI controls. */\n --type-content-scale: 0.81;\n --type-content-display: calc(var(--type-display) * var(--type-content-scale));\n --type-content-title: calc(var(--type-title) * var(--type-content-scale));\n --type-content-subtitle: calc(\n var(--type-subtitle) * var(--type-content-scale)\n );\n --type-content-body: calc(var(--type-body) * var(--type-content-scale));\n\n /* Semantic aliases */\n --type-code: var(--type-base);\n --type-code-block: var(--type-sm);\n --type-body-size: var(--type-content-body);\n --type-body-tracking: 0;\n --type-ui-title: var(--type-secondary);\n --type-ui-control: var(--type-base);\n --type-ui-meta: var(--type-base);\n --type-ui-hint: var(--type-sm);\n --type-ui-caption: var(--type-xs);\n --type-ui-micro: var(--type-2xs);\n --type-ui-input: var(--type-secondary);\n --type-thread-context: var(--type-base);\n --type-thread-context-title: var(--type-secondary);\n --type-thread-context-meta: var(--type-sm);\n --feed-note-title-size: var(--type-content-title);\n --feed-note-title-leading: var(--type-heading-leading);\n --type-body-leading: 1.5;\n --type-display-leading: 1.15;\n --type-heading-leading: 1.15;\n --type-heading-weight: var(--fw-regular);\n --type-heading-tracking: 0;\n --type-display-weight: var(--fw-regular);\n --type-display-tracking: 0;\n --type-label-weight: var(--fw-medium);\n --type-label-tracking: 0.08em;\n\n /* Layout — Tufte proportional model */\n --content-max-width: 42rem;\n --form-max-width: 42rem;\n /* compose dialogs + feed content cap */\n --layout-body-max-width: 1088px;\n --layout-content-width: 55%;\n --layout-sidenote-width: 50%;\n --layout-sidenote-margin: -60%;\n --site-padding: 1.5rem;\n --content-gap: 1rem;\n --space-xl: 2rem;\n --space-2xl: 4rem;\n /*\n * Home timeline vertical rhythm — the single source of truth for the gaps\n * between stacked sections on the home page:\n *\n * site description → separator → first post → divider → post → ...\n *\n * where \"separator\" is the description hairline (logged out) or the compose\n * prompt (logged in). Every one of those gaps derives from this token, and\n * the individual pieces (compose prompt, description hairline, post cards)\n * carry no rhythm margin of their own — the structural wrappers\n * (.site-home-header, hr.feed-divider) own the spacing. Retune the whole\n * feed cadence by changing this one value.\n *\n * Note: per-format card padding (.feed-quote-post, thread previews) is the\n * card's own internal spacing and is intentionally NOT part of this rhythm.\n *\n * Kept as a free-standing value (not pinned to the --space-* scale) so the\n * feed cadence can be tuned independently.\n */\n --site-feed-rhythm: 4.4rem;\n /*\n * The post footer's action row (reply / menu buttons) is a taller tap\n * target than its visible content, leaving a few px of dead space below\n * the last visible element of every post (~5.9px logged out from the\n * footer min-height, ~6.8px logged in from the menu button). hr.feed-divider\n * subtracts this from its top margin so the VISIBLE gap between posts equals\n * --site-feed-rhythm — the same as the header spacing. Re-measure if the\n * footer button size or meta font changes.\n */\n --post-footer-overshoot: 6px;\n /*\n * The site intro (description) block is deliberately tighter than\n * --site-feed-rhythm so it reads as a compact header unit rather than a\n * full feed section. -top: gap from the header nav down to the intro.\n * -bottom: gap from the intro down to its separator — the hairline (logged\n * out) or the compose prompt text (logged in). Same values in both states.\n */\n --site-intro-gap-top: 24px;\n --site-intro-gap-bottom: 36px;\n\n /* Sidebar layout (admin + site sidebar pages) */\n --sidebar-width: 12rem;\n --sidebar-gap: 2rem;\n\n /* Surfaces */\n --card-bg: var(--card);\n --card-radius: 0;\n --card-padding: 1rem;\n --card-border-width: 0;\n --card-shadow: none;\n\n /* Elements */\n --avatar-size: 28px;\n --avatar-radius: 50%;\n --media-radius: 0.5rem;\n\n /* Icons */\n --icon-stroke: 2;\n --icon-stroke-fine: 1.5;\n\n /* Derived color tokens (from BaseCoat variables) */\n --site-accent: var(--primary);\n --site-accent-text: var(--primary-foreground);\n --site-column-outline: var(--border);\n --site-border-light: color-mix(\n in srgb,\n var(--site-column-outline) 52%,\n transparent\n );\n --site-threadline: var(--border);\n --site-page-bg: var(--background);\n --site-elevated-bg: var(--background);\n --site-nav-hover-bg: var(--accent);\n --site-text-primary: var(--foreground);\n --site-text-secondary: var(--muted-foreground);\n --site-reading-title: color-mix(\n in oklch,\n var(--site-text-primary) 81%,\n black\n );\n --site-reading-heading: color-mix(\n in oklch,\n var(--site-text-primary) 86%,\n black\n );\n --site-reading-body: color-mix(in oklch, var(--site-text-primary) 90%, black);\n --site-reading-quote: color-mix(\n in oklch,\n var(--site-text-primary) 95%,\n black\n );\n --site-reading-meta: color-mix(\n in srgb,\n var(--site-text-secondary) 72%,\n var(--site-text-primary)\n );\n --site-reading-caption: color-mix(\n in srgb,\n var(--site-text-secondary) 88%,\n var(--site-text-primary)\n );\n --site-content-link: inherit;\n --site-content-link-hover: var(--site-text-primary);\n --site-content-link-underline: color-mix(\n in srgb,\n var(--site-text-secondary) 58%,\n transparent\n );\n --site-reading-link: var(--site-reading-body);\n --site-reading-link-hover: var(--site-reading-heading);\n --site-reading-link-underline: color-mix(\n in srgb,\n var(--site-reading-meta) 58%,\n transparent\n );\n --site-text-placeholder: oklch(from var(--muted-foreground) l c h / 0.5);\n --site-media-outline: var(--border);\n --site-divider: var(--border);\n --site-feed-card-bg: color-mix(\n in srgb,\n var(--site-elevated-bg) 88%,\n var(--site-nav-hover-bg)\n );\n --site-feed-card-border: color-mix(\n in srgb,\n var(--site-divider) 78%,\n transparent\n );\n --site-feed-card-shadow: color-mix(\n in srgb,\n var(--site-text-primary) 12%,\n transparent\n );\n --site-feed-divider-color: color-mix(\n in srgb,\n var(--site-text-secondary) 30%,\n transparent\n );\n --site-feed-link-tint: color-mix(in srgb, var(--site-accent) 7%, transparent);\n --site-feed-quote-tint: color-mix(\n in srgb,\n var(--site-accent) 10%,\n transparent\n );\n --site-blockquote-rail: color-mix(\n in srgb,\n var(--site-accent) 22%,\n var(--site-divider)\n );\n --site-blockquote-bg: color-mix(\n in srgb,\n var(--site-feed-quote-tint) 62%,\n var(--site-page-bg)\n );\n --site-blockquote-text: color-mix(\n in oklch,\n var(--site-text-primary) 92%,\n black\n );\n --site-summary-blockquote-bg: color-mix(\n in srgb,\n var(--site-feed-quote-tint) 42%,\n var(--site-page-bg)\n );\n --site-reading-blockquote-rail: color-mix(\n in srgb,\n var(--site-reading-link) 18%,\n var(--site-reading-meta)\n );\n --site-reading-blockquote-bg: color-mix(\n in srgb,\n var(--site-accent) 6%,\n var(--site-page-bg)\n );\n --site-thread-context-bg: color-mix(\n in srgb,\n var(--site-nav-hover-bg) 58%,\n transparent\n );\n --site-thread-context-border: color-mix(\n in srgb,\n var(--site-divider) 74%,\n transparent\n );\n --site-thread-gap-bg: color-mix(\n in srgb,\n var(--site-nav-hover-bg) 42%,\n transparent\n );\n --site-thread-item-spacing: 32px;\n --site-thread-dot-ring: color-mix(\n in srgb,\n var(--site-accent) 16%,\n transparent\n );\n --compose-paper-bg: var(--site-page-bg);\n --compose-control-bg: color-mix(\n in srgb,\n var(--site-nav-hover-bg) 72%,\n var(--compose-paper-bg)\n );\n --compose-control-bg-strong: color-mix(\n in srgb,\n var(--site-nav-hover-bg) 88%,\n var(--compose-paper-bg)\n );\n --compose-control-border: color-mix(\n in srgb,\n var(--site-column-outline) 68%,\n transparent\n );\n --compose-blockquote-rail: color-mix(\n in srgb,\n var(--site-accent) 24%,\n var(--compose-control-border)\n );\n --compose-blockquote-bg: color-mix(\n in srgb,\n var(--compose-control-bg) 56%,\n var(--compose-paper-bg)\n );\n --compose-blockquote-bg-focus: color-mix(\n in srgb,\n var(--compose-control-bg-strong) 70%,\n var(--compose-paper-bg)\n );\n --compose-blockquote-text: color-mix(\n in srgb,\n var(--site-text-primary) 88%,\n var(--site-text-secondary)\n );\n --compose-floating-bg: color-mix(\n in srgb,\n var(--compose-paper-bg) 94%,\n var(--site-nav-hover-bg) 6%\n );\n\n /* Search highlight */\n --search-mark-bg: oklch(0.92 0.14 90 / 0.55);\n --search-mark-color: oklch(0.35 0.09 70);\n\n /* Admin */\n --dash-bg: oklch(0.97 0.005 80);\n --dash-card-radius: 10px;\n}\n\n@media (max-width: 760px) {\n :root {\n --site-padding: 1.875rem;\n --layout-content-width: 100%;\n }\n}\n\n/*\n * Dark-mode reading color overrides.\n *\n * These rules must beat the active color theme's light-mode block, which\n * uses `:root:root { ... }` (specificity 0,0,2,0) with no media query and\n * therefore applies in both light and dark modes. Most themes do not\n * redefine `--site-reading-*` in their dark block, so without higher\n * specificity here the light reading-body color would leak into dark mode\n * (resulting in near-invisible body text on a dark background).\n *\n * We repeat `:root:root` to reach specificity 0,0,3,0, which outranks the\n * theme's light `:root:root` (0,0,2,0). The theme's dark blocks use\n * `:root:root[data-theme-mode=\"dark\"]` or `:root:root:not([data-theme-mode=\"light\"])`\n * (also 0,0,3,0), so a theme that explicitly defines dark reading colors\n * still wins via source order.\n */\n@media (prefers-color-scheme: dark) {\n :root:root:not([data-theme-mode=\"light\"]) {\n --site-reading-title: var(--site-text-primary);\n --site-reading-heading: color-mix(\n in oklch,\n var(--site-text-primary) 94%,\n var(--site-text-secondary)\n );\n --site-reading-body: color-mix(\n in oklch,\n var(--site-text-primary) 96%,\n black\n );\n --site-reading-quote: color-mix(\n in oklch,\n var(--site-text-primary) 98%,\n black\n );\n --site-reading-meta: color-mix(\n in srgb,\n var(--site-text-secondary) 92%,\n var(--site-text-primary)\n );\n --site-reading-caption: color-mix(\n in srgb,\n var(--site-text-secondary) 96%,\n var(--site-text-primary)\n );\n --site-reading-link: var(--site-reading-body);\n --search-mark-bg: oklch(0.45 0.1 85 / 0.5);\n --search-mark-color: oklch(0.92 0.08 90);\n --dash-bg: oklch(0.2 0.005 80);\n }\n}\n\n:root:root[data-theme-mode=\"dark\"] {\n --site-reading-title: var(--site-text-primary);\n --site-reading-heading: color-mix(\n in oklch,\n var(--site-text-primary) 94%,\n var(--site-text-secondary)\n );\n --site-reading-body: color-mix(in oklch, var(--site-text-primary) 96%, black);\n --site-reading-quote: color-mix(\n in oklch,\n var(--site-text-primary) 98%,\n black\n );\n --site-reading-meta: color-mix(\n in srgb,\n var(--site-text-secondary) 92%,\n var(--site-text-primary)\n );\n --site-reading-caption: color-mix(\n in srgb,\n var(--site-text-secondary) 96%,\n var(--site-text-primary)\n );\n --site-reading-link: var(--site-reading-body);\n --search-mark-bg: oklch(0.45 0.1 85 / 0.5);\n --search-mark-color: oklch(0.92 0.08 90);\n --dash-bg: oklch(0.2 0.005 80);\n}\n\n@media (max-width: 760px), (hover: none) and (pointer: coarse) {\n :root {\n /* Content layer — tighter scale for mobile reading\n Ratio ≈ 1.9 : 1.4 : 1.15 : 1 (display:title:subtitle:body)\n 13px base → 31.9 / 24 / 19.5 / 16.9 */\n --type-content-display: calc(2.45rem * var(--type-content-scale));\n --type-content-title: calc(1.85rem * var(--type-content-scale));\n --type-content-subtitle: calc(1.5rem * var(--type-content-scale));\n --type-content-body: calc(1.3rem * var(--type-content-scale));\n\n /* UI layer — pixel floors for touch targets */\n --type-ui-title: max(16px, var(--type-secondary));\n --type-ui-control: max(15px, var(--type-base));\n --type-ui-meta: max(15px, var(--type-secondary));\n --type-ui-hint: max(13px, var(--type-sm));\n --type-ui-caption: max(13px, var(--type-xs));\n --type-ui-micro: max(12px, var(--type-2xs));\n --type-ui-input: max(16px, var(--type-secondary));\n --type-thread-context: max(15px, var(--type-base));\n --type-thread-context-title: max(16px, var(--type-secondary));\n --type-thread-context-meta: max(14px, var(--type-sm));\n }\n}\n";
|
|
3273
|
+
var tokens_default = "/**\n * Design Tokens\n *\n * CSS custom properties for all visual aspects of the UI.\n * These are the stable customization API — override in custom CSS\n * to change typography, layout, surfaces, and element sizing.\n */\n\n:root {\n /* Typography — Font families */\n --font-cjk-serif-fallback:\n \"Songti SC\", STSong, SimSun, \"Songti TC\", PMingLiU, MingLiU,\n \"Noto Serif SC\", \"Noto Serif CJK SC\", \"Noto Serif TC\", \"Noto Serif CJK TC\";\n --font-body:\n system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\n \"Helvetica Neue\", Helvetica, Arial, \"PingFang TC\", \"PingFang SC\",\n \"Hiragino Sans CNS\", \"Hiragino Sans GB\", \"Microsoft JhengHei\",\n \"Microsoft YaHei\", \"Noto Sans CJK TC\", \"Noto Sans CJK SC\", sans-serif;\n --font-heading:\n \"New York Small\", \"New York\", \"Iowan Old Style\", Charter,\n \"Bitstream Charter\", \"Source Serif 4\", Cambria, \"Sitka Text\", Georgia,\n var(--font-cjk-serif-fallback), ui-serif, serif;\n --font-site-title:\n \"New York Small\", \"New York\", \"Iowan Old Style\", Charter,\n \"Bitstream Charter\", \"Source Serif 4\", Cambria, \"Sitka Text\", Georgia,\n var(--font-cjk-serif-fallback), ui-serif, serif;\n --font-serif:\n var(--font-cjk-serif-fallback), ui-serif, \"New York Small\", \"New York\",\n \"Iowan Old Style\", Charter, Georgia, \"Times New Roman\", Times, serif;\n --font-ui:\n system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\n \"Helvetica Neue\", Helvetica, Arial, \"PingFang TC\", \"PingFang SC\",\n \"Hiragino Sans CNS\", \"Hiragino Sans GB\", \"Microsoft JhengHei\",\n \"Microsoft YaHei\", \"Noto Sans CJK TC\", \"Noto Sans CJK SC\", sans-serif;\n --font-mono:\n ui-monospace, Menlo, Monaco, Consolas, \"Cascadia Code\", \"Courier New\",\n monospace;\n /*\n * Blockquote font family.\n *\n * Defaults to `inherit` so blockquotes follow the body font in sans-only\n * themes (Clean, Friendly, Bold) and serif-only themes (Tufte, Bookish).\n * Mixed themes that want a distinct blockquote voice — e.g. Classic, which\n * pairs a sans body with serif headings — override this token in their\n * `cssVariables` to point at a serif stack. This also restores a type-level\n * distinction for CJK, where italic is disabled.\n */\n --font-blockquote: inherit;\n\n /* Typography — Font weights */\n --fw-light: 300;\n --fw-regular: 400;\n --fw-medium: 500;\n --fw-semibold: 600;\n --fw-bold: 700;\n --fw-extrabold: 800;\n /*\n * Unified type scale.\n *\n * Every font-size in the system — reading content AND UI controls —\n * must reference one of these tokens. No raw rem/px values elsewhere.\n *\n * --type-display 2.7rem (40.5px) — page titles, h1\n * --type-title 2.1rem (31.5px) — h2, feed card titles\n * --type-subtitle 1.8rem (27px) — h3\n * --type-body 1.4rem (21px) — running text, form inputs\n * --type-secondary 1.1rem (16.5px) — meta, nav, sidenotes, UI labels\n * --type-base 1.0rem (15px) — code, UI buttons, controls\n * --type-sm 0.9rem (13.5px) — code blocks, small UI text\n * --type-xs 0.8rem (12px) — captions, descriptions, chips\n * --type-2xs 0.65rem (9.75px) — tiny labels, file sizes, badges\n */\n --type-display: 2.7rem;\n --type-title: 2.1rem;\n --type-subtitle: 1.8rem;\n --type-body: 1.4rem;\n --type-secondary: 1.1rem;\n --type-base: 1rem;\n --type-sm: 0.9rem;\n --type-xs: 0.8rem;\n --type-2xs: 0.65rem;\n\n /* Content reading tokens — desktop maps to core scale,\n mobile overrides below cap sizes for small screens.\n --type-content-scale uniformly scales all content sizes\n without affecting UI controls. */\n --type-content-scale: 0.81;\n --type-content-display: calc(var(--type-display) * var(--type-content-scale));\n --type-content-title: calc(var(--type-title) * var(--type-content-scale));\n --type-content-subtitle: calc(\n var(--type-subtitle) * var(--type-content-scale)\n );\n --type-content-body: calc(var(--type-body) * var(--type-content-scale));\n\n /* Semantic aliases */\n --type-code: var(--type-base);\n --type-code-block: var(--type-sm);\n --type-body-size: var(--type-content-body);\n --type-body-tracking: 0;\n --type-ui-title: var(--type-secondary);\n --type-ui-control: var(--type-base);\n --type-ui-meta: var(--type-base);\n --type-ui-hint: var(--type-sm);\n --type-ui-caption: var(--type-xs);\n --type-ui-micro: var(--type-2xs);\n --type-ui-input: var(--type-secondary);\n --type-thread-context: var(--type-base);\n --type-thread-context-title: var(--type-secondary);\n --type-thread-context-meta: var(--type-sm);\n --feed-note-title-size: var(--type-content-title);\n --feed-note-title-leading: var(--type-heading-leading);\n --type-body-leading: 1.5;\n --type-display-leading: 1.15;\n --type-heading-leading: 1.15;\n --type-heading-weight: var(--fw-regular);\n --type-heading-tracking: 0;\n --type-display-weight: var(--fw-regular);\n --type-display-tracking: 0;\n --type-label-weight: var(--fw-medium);\n --type-label-tracking: 0.08em;\n\n /* Layout — Tufte proportional model */\n --content-max-width: 42rem;\n --form-max-width: 42rem;\n /* compose dialogs + feed content cap */\n --layout-body-max-width: 1088px;\n --layout-content-width: 55%;\n --layout-sidenote-width: 50%;\n --layout-sidenote-margin: -60%;\n --site-padding: 1.5rem;\n --content-gap: 1rem;\n --space-xl: 2rem;\n --space-2xl: 4rem;\n /*\n * Home timeline vertical rhythm — the single source of truth for the gaps\n * between stacked sections on the home page:\n *\n * site description → separator → first post → divider → post → ...\n *\n * where \"separator\" is the description hairline (logged out) or the compose\n * prompt (logged in). Every one of those gaps derives from this token, and\n * the individual pieces (compose prompt, description hairline, post cards)\n * carry no rhythm margin of their own — the structural wrappers\n * (.site-home-header, hr.feed-divider) own the spacing. Retune the whole\n * feed cadence by changing this one value.\n *\n * Note: per-format card padding (.feed-quote-post, thread previews) is the\n * card's own internal spacing and is intentionally NOT part of this rhythm.\n *\n * Kept as a free-standing value (not pinned to the --space-* scale) so the\n * feed cadence can be tuned independently.\n */\n --site-feed-rhythm: 4.4rem;\n /*\n * The post footer's action row (reply / menu buttons) is a taller tap\n * target than its visible content, leaving a few px of dead space below\n * the last visible element of every post (~5.9px logged out from the\n * footer min-height, ~6.8px logged in from the menu button). hr.feed-divider\n * subtracts this from its top margin so the VISIBLE gap between posts equals\n * --site-feed-rhythm — the same as the header spacing. Re-measure if the\n * footer button size or meta font changes.\n */\n --post-footer-overshoot: 6px;\n /*\n * The site intro (description) block is deliberately tighter than\n * --site-feed-rhythm so it reads as a compact header unit rather than a\n * full feed section. -top: gap from the header nav down to the intro.\n * -bottom: gap from the intro down to its separator — the hairline (logged\n * out) or the compose prompt text (logged in). Same values in both states.\n */\n --site-intro-gap-top: 24px;\n --site-intro-gap-bottom: 36px;\n\n /* Sidebar layout (admin + site sidebar pages) */\n --sidebar-width: 12rem;\n --sidebar-gap: 2rem;\n\n /* Surfaces */\n --card-bg: var(--card);\n --card-radius: 0;\n --card-padding: 1rem;\n --card-border-width: 0;\n --card-shadow: none;\n\n /* Elements */\n --avatar-size: 28px;\n --avatar-radius: 50%;\n --media-radius: 0.5rem;\n\n /* Icons */\n --icon-stroke: 2;\n --icon-stroke-fine: 1.5;\n\n /* Derived color tokens (from BaseCoat variables) */\n --site-accent: var(--primary);\n --site-accent-text: var(--primary-foreground);\n --site-column-outline: var(--border);\n --site-border-light: color-mix(\n in srgb,\n var(--site-column-outline) 52%,\n transparent\n );\n --site-threadline: var(--border);\n --site-page-bg: var(--background);\n --site-elevated-bg: var(--background);\n --site-nav-hover-bg: var(--accent);\n --site-text-primary: var(--foreground);\n --site-text-secondary: var(--muted-foreground);\n --site-reading-title: color-mix(\n in oklch,\n var(--site-text-primary) 81%,\n black\n );\n --site-reading-heading: color-mix(\n in oklch,\n var(--site-text-primary) 86%,\n black\n );\n --site-reading-body: color-mix(in oklch, var(--site-text-primary) 90%, black);\n --site-reading-quote: color-mix(\n in oklch,\n var(--site-text-primary) 95%,\n black\n );\n --site-reading-meta: color-mix(\n in srgb,\n var(--site-text-secondary) 72%,\n var(--site-text-primary)\n );\n --site-reading-caption: color-mix(\n in srgb,\n var(--site-text-secondary) 88%,\n var(--site-text-primary)\n );\n --site-content-link: inherit;\n --site-content-link-hover: var(--site-text-primary);\n --site-content-link-underline: color-mix(\n in srgb,\n var(--site-text-secondary) 58%,\n transparent\n );\n --site-reading-link: var(--site-reading-body);\n --site-reading-link-hover: var(--site-reading-heading);\n --site-reading-link-underline: color-mix(\n in srgb,\n var(--site-reading-meta) 58%,\n transparent\n );\n --site-text-placeholder: oklch(from var(--muted-foreground) l c h / 0.5);\n --site-media-outline: var(--border);\n --site-divider: var(--border);\n --site-feed-card-bg: color-mix(\n in srgb,\n var(--site-elevated-bg) 88%,\n var(--site-nav-hover-bg)\n );\n --site-feed-card-border: color-mix(\n in srgb,\n var(--site-divider) 78%,\n transparent\n );\n --site-feed-card-shadow: color-mix(\n in srgb,\n var(--site-text-primary) 12%,\n transparent\n );\n --site-feed-divider-color: color-mix(\n in srgb,\n var(--site-text-secondary) 30%,\n transparent\n );\n --site-feed-link-tint: color-mix(in srgb, var(--site-accent) 7%, transparent);\n --site-feed-quote-tint: color-mix(\n in srgb,\n var(--site-accent) 10%,\n transparent\n );\n --site-blockquote-rail: color-mix(\n in srgb,\n var(--site-accent) 22%,\n var(--site-divider)\n );\n --site-blockquote-bg: color-mix(\n in srgb,\n var(--site-feed-quote-tint) 62%,\n var(--site-page-bg)\n );\n --site-blockquote-text: color-mix(\n in oklch,\n var(--site-text-primary) 92%,\n black\n );\n --site-summary-blockquote-bg: color-mix(\n in srgb,\n var(--site-feed-quote-tint) 42%,\n var(--site-page-bg)\n );\n --site-reading-blockquote-rail: color-mix(\n in srgb,\n var(--site-reading-link) 18%,\n var(--site-reading-meta)\n );\n --site-reading-blockquote-bg: color-mix(\n in srgb,\n var(--site-accent) 6%,\n var(--site-page-bg)\n );\n --site-thread-context-bg: color-mix(\n in srgb,\n var(--site-nav-hover-bg) 58%,\n transparent\n );\n --site-thread-context-border: color-mix(\n in srgb,\n var(--site-divider) 74%,\n transparent\n );\n --site-thread-gap-bg: color-mix(\n in srgb,\n var(--site-nav-hover-bg) 42%,\n transparent\n );\n --site-thread-item-spacing: 32px;\n --site-thread-context-max-height: 160px;\n --site-thread-dot-ring: color-mix(\n in srgb,\n var(--site-accent) 16%,\n transparent\n );\n --compose-paper-bg: var(--site-page-bg);\n --compose-control-bg: color-mix(\n in srgb,\n var(--site-nav-hover-bg) 72%,\n var(--compose-paper-bg)\n );\n --compose-control-bg-strong: color-mix(\n in srgb,\n var(--site-nav-hover-bg) 88%,\n var(--compose-paper-bg)\n );\n --compose-control-border: color-mix(\n in srgb,\n var(--site-column-outline) 68%,\n transparent\n );\n --compose-blockquote-rail: color-mix(\n in srgb,\n var(--site-accent) 24%,\n var(--compose-control-border)\n );\n --compose-blockquote-bg: color-mix(\n in srgb,\n var(--compose-control-bg) 56%,\n var(--compose-paper-bg)\n );\n --compose-blockquote-bg-focus: color-mix(\n in srgb,\n var(--compose-control-bg-strong) 70%,\n var(--compose-paper-bg)\n );\n --compose-blockquote-text: color-mix(\n in srgb,\n var(--site-text-primary) 88%,\n var(--site-text-secondary)\n );\n --compose-floating-bg: color-mix(\n in srgb,\n var(--compose-paper-bg) 94%,\n var(--site-nav-hover-bg) 6%\n );\n\n /* Search highlight */\n --search-mark-bg: oklch(0.92 0.14 90 / 0.55);\n --search-mark-color: oklch(0.35 0.09 70);\n\n /* Admin */\n --dash-bg: oklch(0.97 0.005 80);\n --dash-card-radius: 10px;\n}\n\n@media (max-width: 760px) {\n :root {\n --site-padding: 1.875rem;\n --layout-content-width: 100%;\n }\n}\n\n/*\n * Dark-mode reading color overrides.\n *\n * These rules must beat the active color theme's light-mode block, which\n * uses `:root:root { ... }` (specificity 0,0,2,0) with no media query and\n * therefore applies in both light and dark modes. Most themes do not\n * redefine `--site-reading-*` in their dark block, so without higher\n * specificity here the light reading-body color would leak into dark mode\n * (resulting in near-invisible body text on a dark background).\n *\n * We repeat `:root:root` to reach specificity 0,0,3,0, which outranks the\n * theme's light `:root:root` (0,0,2,0). The theme's dark blocks use\n * `:root:root[data-theme-mode=\"dark\"]` or `:root:root:not([data-theme-mode=\"light\"])`\n * (also 0,0,3,0), so a theme that explicitly defines dark reading colors\n * still wins via source order.\n */\n@media (prefers-color-scheme: dark) {\n :root:root:not([data-theme-mode=\"light\"]) {\n --site-reading-title: var(--site-text-primary);\n --site-reading-heading: color-mix(\n in oklch,\n var(--site-text-primary) 94%,\n var(--site-text-secondary)\n );\n --site-reading-body: color-mix(\n in oklch,\n var(--site-text-primary) 96%,\n black\n );\n --site-reading-quote: color-mix(\n in oklch,\n var(--site-text-primary) 98%,\n black\n );\n --site-reading-meta: color-mix(\n in srgb,\n var(--site-text-secondary) 92%,\n var(--site-text-primary)\n );\n --site-reading-caption: color-mix(\n in srgb,\n var(--site-text-secondary) 96%,\n var(--site-text-primary)\n );\n --site-reading-link: var(--site-reading-body);\n --search-mark-bg: oklch(0.45 0.1 85 / 0.5);\n --search-mark-color: oklch(0.92 0.08 90);\n --dash-bg: oklch(0.2 0.005 80);\n }\n}\n\n:root:root[data-theme-mode=\"dark\"] {\n --site-reading-title: var(--site-text-primary);\n --site-reading-heading: color-mix(\n in oklch,\n var(--site-text-primary) 94%,\n var(--site-text-secondary)\n );\n --site-reading-body: color-mix(in oklch, var(--site-text-primary) 96%, black);\n --site-reading-quote: color-mix(\n in oklch,\n var(--site-text-primary) 98%,\n black\n );\n --site-reading-meta: color-mix(\n in srgb,\n var(--site-text-secondary) 92%,\n var(--site-text-primary)\n );\n --site-reading-caption: color-mix(\n in srgb,\n var(--site-text-secondary) 96%,\n var(--site-text-primary)\n );\n --site-reading-link: var(--site-reading-body);\n --search-mark-bg: oklch(0.45 0.1 85 / 0.5);\n --search-mark-color: oklch(0.92 0.08 90);\n --dash-bg: oklch(0.2 0.005 80);\n}\n\n@media (max-width: 760px), (hover: none) and (pointer: coarse) {\n :root {\n /* Content layer — tighter scale for mobile reading\n Ratio ≈ 1.9 : 1.4 : 1.15 : 1 (display:title:subtitle:body)\n 13px base → 31.9 / 24 / 19.5 / 16.9 */\n --type-content-display: calc(2.45rem * var(--type-content-scale));\n --type-content-title: calc(1.85rem * var(--type-content-scale));\n --type-content-subtitle: calc(1.5rem * var(--type-content-scale));\n --type-content-body: calc(1.3rem * var(--type-content-scale));\n\n /* UI layer — pixel floors for touch targets */\n --type-ui-title: max(16px, var(--type-secondary));\n --type-ui-control: max(15px, var(--type-base));\n --type-ui-meta: max(15px, var(--type-secondary));\n --type-ui-hint: max(13px, var(--type-sm));\n --type-ui-caption: max(13px, var(--type-xs));\n --type-ui-micro: max(12px, var(--type-2xs));\n --type-ui-input: max(16px, var(--type-secondary));\n --type-thread-context: max(15px, var(--type-base));\n --type-thread-context-title: max(16px, var(--type-secondary));\n --type-thread-context-meta: max(14px, var(--type-sm));\n }\n}\n";
|
|
3274
3274
|
//#endregion
|
|
3275
3275
|
//#region src/services/export-theme/theme.toml?raw
|
|
3276
3276
|
var theme_default = "name = \"jant\"\nlicense = \"MIT\"\nlicenselink = \"https://github.com/jant-me/jant/blob/main/LICENSE\"\ndescription = \"Default theme packaged with Jant exports.\"\nhomepage = \"https://jant.so\"\ntags = [\"blog\", \"microblog\", \"minimal\"]\nfeatures = [\"pagination\", \"aliases\"]\nmin_version = \"0.160.1\"\n\n[author]\n name = \"Jant\"\n homepage = \"https://jant.so\"\n";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as parseMarkdownDocument, r as parseFrontMatter, t as createExportService } from "./export-
|
|
1
|
+
import { c as parseMarkdownDocument, r as parseFrontMatter, t as createExportService } from "./export-Bbn86HmS.js";
|
|
2
2
|
import { r as getInstallationToken } from "./github-app-D0GvNnqp.js";
|
|
3
3
|
import { r as parseRepoSlug, t as createGitHubClient } from "./github-api-Bh0PH3zr.js";
|
|
4
4
|
//#region src/lib/markdown-to-tiptap.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import "./url-umUptr5z.js";
|
|
2
|
-
import "./export-
|
|
3
|
-
import { i as classifyRepoForSync, o as createGitHubSyncService } from "./github-sync-
|
|
2
|
+
import "./export-Bbn86HmS.js";
|
|
3
|
+
import { i as classifyRepoForSync, o as createGitHubSyncService } from "./github-sync-CBQPRZ8H.js";
|
|
4
4
|
export { classifyRepoForSync, createGitHubSyncService };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { _ as url_exports } from "./url-umUptr5z.js";
|
|
2
|
-
import { A as
|
|
3
|
-
import { T as time_exports, a as markdown_exports } from "./export-
|
|
4
|
-
import "./env-
|
|
5
|
-
import "./github-sync-
|
|
2
|
+
import { A as MAX_MEDIA_ATTACHMENTS, C as toMediaView, D as toPostViews, E as toPostView, F as STATUSES, I as TEXT_ATTACHMENT_CONTENT_FORMATS, M as MEDIA_KINDS, N as NAV_ITEM_TYPES, O as toSearchResultView, P as SORT_ORDERS, S as toArchiveGroupsWithMedia, T as toNavItemViews, b as createMediaContext, h as defaultFeedRenderer, j as MAX_PINNED_POSTS, k as FORMATS, t as createApp, w as toNavItemView, x as toArchiveGroups } from "./app-DYQdDMs8.js";
|
|
3
|
+
import { T as time_exports, a as markdown_exports } from "./export-Bbn86HmS.js";
|
|
4
|
+
import "./env-C7e2Nlnt.js";
|
|
5
|
+
import "./github-sync-CBQPRZ8H.js";
|
|
6
6
|
export { FORMATS, MAX_MEDIA_ATTACHMENTS, MAX_PINNED_POSTS, MEDIA_KINDS, NAV_ITEM_TYPES, SORT_ORDERS, STATUSES, TEXT_ATTACHMENT_CONTENT_FORMATS, createApp, createMediaContext, defaultFeedRenderer, markdown_exports as markdown, time_exports as time, toArchiveGroups, toArchiveGroupsWithMedia, toMediaView, toNavItemView, toNavItemViews, toPostView, toPostViews, toSearchResultView, url_exports as url };
|
package/dist/node.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "./url-umUptr5z.js";
|
|
2
|
-
import {
|
|
3
|
-
import { t as createExportService } from "./export-
|
|
4
|
-
import { b as getSiteResolutionMode, i as getConfiguredSingleSitePathPrefix, l as getEnvString, r as getConfiguredSingleSiteOrigin, x as
|
|
5
|
-
import "./github-sync-
|
|
2
|
+
import { B as isAssetPath, L as buildThemeStyle, R as BUILTIN_COLOR_THEMES, _ as sqliteSchemaBundle, a as resolveDatabaseDialect, c as resolveConfig, d as setWebhook, f as BUILTIN_FONT_THEMES, g as pgSchemaBundle, i as createSiteService, l as getWebhookUrl, m as getFontThemeCssVariables, n as createNodeCliRuntime, o as getHostBasedStartupConfigurationIssues, p as getCjkSerifCssVariables, r as createNodeRequestRuntime, s as createStorageDriver, t as createApp, u as setMyCommands, v as createNodeDatabase, y as schema_exports, z as getPublicAssetBasePath } from "./app-DYQdDMs8.js";
|
|
3
|
+
import { t as createExportService } from "./export-Bbn86HmS.js";
|
|
4
|
+
import { C as shouldTrustProxy, S as getTelegramWebhookSecret, b as getSiteResolutionMode, d as getHostedControlPlaneBaseUrl, i as getConfiguredSingleSitePathPrefix, l as getEnvString, r as getConfiguredSingleSiteOrigin, x as getTelegramBotPool, y as getPort } from "./env-C7e2Nlnt.js";
|
|
5
|
+
import "./github-sync-CBQPRZ8H.js";
|
|
6
6
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
7
7
|
import { serve } from "@hono/node-server";
|
|
8
8
|
import Database from "better-sqlite3";
|
|
@@ -470,11 +470,66 @@ async function createNodeRequestHandler(options) {
|
|
|
470
470
|
}
|
|
471
471
|
}
|
|
472
472
|
//#endregion
|
|
473
|
+
//#region src/lib/telegram-pool-webhooks.ts
|
|
474
|
+
/**
|
|
475
|
+
* Telegram managed-pool webhook registration.
|
|
476
|
+
*
|
|
477
|
+
* In hosted mode the bot pool is platform-owned (`TELEGRAM_BOT_TOKENS`) and
|
|
478
|
+
* there is no settings-page action that would register its webhooks. Rather
|
|
479
|
+
* than make the operator run a CLI step, the Node server self-registers on
|
|
480
|
+
* startup: it derives the webhook URL from `HOSTED_CONTROL_PLANE_BASE_URL`
|
|
481
|
+
* (the public control-plane host that forwards to core) and points each pool
|
|
482
|
+
* bot at `<base>/api/telegram/webhook/<botId>`.
|
|
483
|
+
*
|
|
484
|
+
* This is gated on `HOSTED_CONTROL_PLANE_BASE_URL` being set, so a local dev
|
|
485
|
+
* box with `TELEGRAM_BOT_TOKENS` in its env never touches Telegram. It is
|
|
486
|
+
* idempotent — a `getWebhookInfo` check skips bots already pointed at the
|
|
487
|
+
* right URL, so a steady-state restart issues only cheap reads. Callers run
|
|
488
|
+
* it fire-and-forget; it must never block or fail startup.
|
|
489
|
+
*/
|
|
490
|
+
/**
|
|
491
|
+
* Registers webhooks for every managed-pool bot, skipping those already
|
|
492
|
+
* pointed at the correct URL. No-ops when the pool is unset, the deployment
|
|
493
|
+
* is not hosted, or no shared webhook secret is configured.
|
|
494
|
+
*
|
|
495
|
+
* @param env - Runtime environment bindings
|
|
496
|
+
*/ async function registerTelegramPoolWebhooks(env) {
|
|
497
|
+
const pool = getTelegramBotPool(env);
|
|
498
|
+
if (pool.length === 0) return;
|
|
499
|
+
const baseUrl = getHostedControlPlaneBaseUrl(env);
|
|
500
|
+
if (!baseUrl) return;
|
|
501
|
+
const secret = getTelegramWebhookSecret(env);
|
|
502
|
+
if (!secret) {
|
|
503
|
+
console.error("[Jant] TELEGRAM_BOT_TOKENS is set but TELEGRAM_WEBHOOK_SECRET is missing — skipping webhook registration.");
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const origin = baseUrl.replace(/\/+$/, "");
|
|
507
|
+
for (const bot of pool) {
|
|
508
|
+
const webhookUrl = `${origin}/api/telegram/webhook/${bot.botId}`;
|
|
509
|
+
try {
|
|
510
|
+
if (await getWebhookUrl(bot.token) !== webhookUrl) {
|
|
511
|
+
await setWebhook(bot.token, webhookUrl, secret);
|
|
512
|
+
console.log(`[Jant] Telegram webhook registered: bot=${bot.botId}`);
|
|
513
|
+
}
|
|
514
|
+
} catch (err) {
|
|
515
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
516
|
+
console.error(`[Jant] Telegram webhook registration failed: bot=${bot.botId} error=${message}`);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
await setMyCommands(bot.token);
|
|
521
|
+
} catch (err) {
|
|
522
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
523
|
+
console.error(`[Jant] Telegram setMyCommands failed: bot=${bot.botId} error=${message}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
//#endregion
|
|
473
528
|
//#region src/node/runtime.ts
|
|
474
529
|
async function start(env = process.env, app) {
|
|
475
530
|
const handler = await createNodeRequestHandler({
|
|
476
531
|
env,
|
|
477
|
-
app: async () => app ?? (await import("./app-
|
|
532
|
+
app: async () => app ?? (await import("./app-CMSW_AYG.js")).createApp()
|
|
478
533
|
});
|
|
479
534
|
const hostname = resolveHost(env);
|
|
480
535
|
const port = resolvePort(env);
|
|
@@ -487,6 +542,7 @@ async function start(env = process.env, app) {
|
|
|
487
542
|
}, (info) => {
|
|
488
543
|
didResolve = true;
|
|
489
544
|
server.off("error", onError);
|
|
545
|
+
registerTelegramPoolWebhooks(env);
|
|
490
546
|
let closed = false;
|
|
491
547
|
resolvePromise({
|
|
492
548
|
server,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jant/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "A modern, open-source microblogging platform built on Cloudflare Workers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"sortablejs": "^1.15.7",
|
|
71
71
|
"typeid-js": "^1.2.0",
|
|
72
72
|
"unist-util-visit": "^5.1.0",
|
|
73
|
+
"uqr": "^0.1.3",
|
|
73
74
|
"yaml": "^2.8.2",
|
|
74
75
|
"zod": "^4.3.6"
|
|
75
76
|
},
|
|
@@ -39,6 +39,10 @@ interface TestAppOptions {
|
|
|
39
39
|
hostedControlPlaneSsoSecret?: string;
|
|
40
40
|
/** Optional hosted handoff service override */
|
|
41
41
|
hostedHandoff?: HostedHandoffService;
|
|
42
|
+
/** Optional `TELEGRAM_BOT_TOKENS` env binding for Telegram webhook tests */
|
|
43
|
+
telegramBotTokens?: string;
|
|
44
|
+
/** Optional `TELEGRAM_WEBHOOK_SECRET` env binding for Telegram webhook tests */
|
|
45
|
+
telegramWebhookSecret?: string;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
/**
|
|
@@ -53,10 +57,13 @@ export function createTestApp(options: TestAppOptions = {}) {
|
|
|
53
57
|
// Create a mock D1 for search service
|
|
54
58
|
const mockD1 = createMockD1(sqlite);
|
|
55
59
|
|
|
56
|
-
const
|
|
60
|
+
const servicesConfig = {
|
|
57
61
|
slugIdLength: 5,
|
|
58
62
|
siteResolutionMode: options.siteResolutionMode ?? "single-site",
|
|
59
|
-
}
|
|
63
|
+
};
|
|
64
|
+
const servicesForSite = (siteId: string) =>
|
|
65
|
+
createServices(db, mockD1, siteId, servicesConfig);
|
|
66
|
+
const services = servicesForSite(DEFAULT_TEST_SITE_ID);
|
|
60
67
|
|
|
61
68
|
// Fresh limiter per test app so counters don't leak between tests.
|
|
62
69
|
const rateLimiter = createMemoryRateLimiter();
|
|
@@ -75,6 +82,8 @@ export function createTestApp(options: TestAppOptions = {}) {
|
|
|
75
82
|
DEMO_MODE: options.demoMode ? "true" : "false",
|
|
76
83
|
INTERNAL_ADMIN_TOKEN: options.internalAdminToken,
|
|
77
84
|
HOSTED_CONTROL_PLANE_SSO_SECRET: options.hostedControlPlaneSsoSecret,
|
|
85
|
+
TELEGRAM_BOT_TOKENS: options.telegramBotTokens,
|
|
86
|
+
TELEGRAM_WEBHOOK_SECRET: options.telegramWebhookSecret,
|
|
78
87
|
NODE_DATABASE: {
|
|
79
88
|
db,
|
|
80
89
|
dialect: "sqlite",
|
|
@@ -85,6 +94,10 @@ export function createTestApp(options: TestAppOptions = {}) {
|
|
|
85
94
|
} as AppVariables["services"] extends never ? never : Bindings;
|
|
86
95
|
|
|
87
96
|
c.set("services", services as AppVariables["services"]);
|
|
97
|
+
c.set(
|
|
98
|
+
"servicesForSite",
|
|
99
|
+
servicesForSite as AppVariables["servicesForSite"],
|
|
100
|
+
);
|
|
88
101
|
c.set(
|
|
89
102
|
"hostedHandoff",
|
|
90
103
|
options.hostedHandoff ??
|
package/src/app.tsx
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
githubSyncWebhookRoutes,
|
|
53
53
|
githubSyncAdminRoutes,
|
|
54
54
|
} from "./routes/api/github-sync.js";
|
|
55
|
+
import { telegramWebhookRoutes } from "./routes/api/telegram.js";
|
|
55
56
|
import { internalTextAttachmentsRoutes } from "./routes/api/internal/text-attachments.js";
|
|
56
57
|
import { internalSearchReindexRoutes } from "./routes/api/internal/search-reindex.js";
|
|
57
58
|
import { internalUploadsRoutes } from "./routes/api/internal/uploads.js";
|
|
@@ -304,6 +305,7 @@ export function createApp(): App {
|
|
|
304
305
|
}
|
|
305
306
|
const runtime = await createRequestRuntime(c.env, publicRequestUrl);
|
|
306
307
|
c.set("services", runtime.services);
|
|
308
|
+
c.set("servicesForSite", runtime.servicesForSite);
|
|
307
309
|
c.set("hostedHandoff", runtime.hostedHandoff);
|
|
308
310
|
c.set("storage", runtime.storage);
|
|
309
311
|
c.set("auth", runtime.auth);
|
|
@@ -348,6 +350,7 @@ export function createApp(): App {
|
|
|
348
350
|
app.route("/api/internal/search/reindex", internalSearchReindexRoutes);
|
|
349
351
|
app.route("/api/internal/uploads", internalUploadsRoutes);
|
|
350
352
|
app.route("/api/github-sync", githubSyncWebhookRoutes);
|
|
353
|
+
app.route("/api/telegram", telegramWebhookRoutes);
|
|
351
354
|
|
|
352
355
|
// Fetch text media content by ID (same-origin proxy to avoid CORS with CDN URLs)
|
|
353
356
|
app.get("/api/media/:id/content", async (c) => {
|
|
@@ -1,9 +1,153 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Thread Context Interactions
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 1. The ancestor-context shell is server-rendered in the "collapsed" state
|
|
5
|
+
* (cap + fade + visible Show more button). For the common case where the
|
|
6
|
+
* content actually overflows that cap, the server state is already correct
|
|
7
|
+
* and JS does nothing visual at all — no first-paint reflow. For the rarer
|
|
8
|
+
* case where the content fits inside the cap (a couple of tiny posts), JS
|
|
9
|
+
* post-load removes the cap and hides the toggle so the user never sees
|
|
10
|
+
* a no-op "Show more" button.
|
|
11
|
+
* 2. Handle expand/collapse toggle clicks with a max-height transition.
|
|
12
|
+
* 3. Auto-scroll to current post on thread detail pages.
|
|
5
13
|
*/
|
|
6
14
|
|
|
15
|
+
const OVERFLOW_THRESHOLD_PX = 8;
|
|
16
|
+
|
|
17
|
+
function parsePixelValue(value: string, fallback: number): number {
|
|
18
|
+
const parsed = Number.parseFloat(value);
|
|
19
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getCapPx(shell: HTMLElement): number {
|
|
23
|
+
const value = getComputedStyle(shell)
|
|
24
|
+
.getPropertyValue("--site-thread-context-max-height")
|
|
25
|
+
.trim();
|
|
26
|
+
return parsePixelValue(value, 240);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function setupShell(toggle: HTMLElement): void {
|
|
30
|
+
if (toggle.dataset.threadContextToggleBound === "1") return;
|
|
31
|
+
toggle.dataset.threadContextToggleBound = "1";
|
|
32
|
+
|
|
33
|
+
const shell = toggle.previousElementSibling;
|
|
34
|
+
if (
|
|
35
|
+
!(shell instanceof HTMLElement) ||
|
|
36
|
+
shell.dataset.threadContext === undefined
|
|
37
|
+
) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const label = toggle.querySelector<HTMLElement>(
|
|
42
|
+
".thread-context-toggle-label",
|
|
43
|
+
);
|
|
44
|
+
const labelMore = toggle.dataset.labelMore ?? "Show more";
|
|
45
|
+
const labelLess = toggle.dataset.labelLess ?? "Show less";
|
|
46
|
+
|
|
47
|
+
let userInteracted = false;
|
|
48
|
+
|
|
49
|
+
const setExpandedLabel = (expanded: boolean): void => {
|
|
50
|
+
toggle.setAttribute("aria-expanded", expanded ? "true" : "false");
|
|
51
|
+
if (label) label.textContent = expanded ? labelLess : labelMore;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const allImagesSettled = (): boolean =>
|
|
55
|
+
Array.from(shell.querySelectorAll("img")).every((img) => img.complete);
|
|
56
|
+
|
|
57
|
+
const evaluate = (): void => {
|
|
58
|
+
// Skip once the user has taken control; their state is intentional.
|
|
59
|
+
if (userInteracted) return;
|
|
60
|
+
const cap = getCapPx(shell);
|
|
61
|
+
// scrollHeight gives the natural content height regardless of whether
|
|
62
|
+
// overflow is currently clipped — valid in collapsed and expanded states.
|
|
63
|
+
const overflows = shell.scrollHeight > cap + OVERFLOW_THRESHOLD_PX;
|
|
64
|
+
|
|
65
|
+
// We always keep `data-collapsed` set so the fade overlay stays visible
|
|
66
|
+
// as a "this is context" hint, regardless of whether the cap actually
|
|
67
|
+
// clips anything. Only the toggle button depends on real overflow —
|
|
68
|
+
// showing it for content that already fits would let the user click a
|
|
69
|
+
// no-op control.
|
|
70
|
+
if (shell.dataset.collapsed === undefined) shell.dataset.collapsed = "";
|
|
71
|
+
if (overflows) {
|
|
72
|
+
toggle.hidden = false;
|
|
73
|
+
setExpandedLabel(false);
|
|
74
|
+
} else if (allImagesSettled()) {
|
|
75
|
+
// Only hide once we're sure nothing pending will grow the shell.
|
|
76
|
+
// Hiding while an image is mid-download would briefly remove the
|
|
77
|
+
// button, and the user could click the spot where it just was.
|
|
78
|
+
toggle.hidden = true;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
evaluate();
|
|
83
|
+
|
|
84
|
+
shell.querySelectorAll("img").forEach((img) => {
|
|
85
|
+
if (img.complete) return;
|
|
86
|
+
img.addEventListener("load", evaluate, { once: true });
|
|
87
|
+
img.addEventListener("error", evaluate, { once: true });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if ("ResizeObserver" in globalThis) {
|
|
91
|
+
let raf = 0;
|
|
92
|
+
const observer = new globalThis.ResizeObserver(() => {
|
|
93
|
+
cancelAnimationFrame(raf);
|
|
94
|
+
raf = requestAnimationFrame(evaluate);
|
|
95
|
+
});
|
|
96
|
+
observer.observe(shell);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Per-click cleanup that fires when max-height settles. The shell's
|
|
100
|
+
// descendants (the fade overlay) also transition, and their transitionend
|
|
101
|
+
// events bubble to the shell — we have to filter by propertyName or
|
|
102
|
+
// cleanup runs early and the animation snaps. Multiple cleanups can stack
|
|
103
|
+
// safely because each is self-removing and idempotent.
|
|
104
|
+
const scheduleCleanup = (): void => {
|
|
105
|
+
let done = false;
|
|
106
|
+
const cleanup = (e?: globalThis.TransitionEvent): void => {
|
|
107
|
+
if (done) return;
|
|
108
|
+
if (e && e.propertyName !== "max-height") return;
|
|
109
|
+
done = true;
|
|
110
|
+
shell.removeEventListener("transitionend", cleanup);
|
|
111
|
+
if (shell.dataset.collapsed === undefined) shell.style.maxHeight = "";
|
|
112
|
+
};
|
|
113
|
+
shell.addEventListener("transitionend", cleanup);
|
|
114
|
+
window.setTimeout(cleanup, 600);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
toggle.addEventListener("click", () => {
|
|
118
|
+
userInteracted = true;
|
|
119
|
+
const collapsed = shell.dataset.collapsed !== undefined;
|
|
120
|
+
|
|
121
|
+
if (collapsed) {
|
|
122
|
+
// Expand: pin to the natural content height so the transition has a
|
|
123
|
+
// concrete target, then drop the cap.
|
|
124
|
+
shell.style.maxHeight = `${shell.scrollHeight}px`;
|
|
125
|
+
void shell.offsetHeight;
|
|
126
|
+
delete shell.dataset.collapsed;
|
|
127
|
+
setExpandedLabel(true);
|
|
128
|
+
} else {
|
|
129
|
+
// Collapse: pin to the current height, then on the next frame restore
|
|
130
|
+
// the cap so the transition animates back down.
|
|
131
|
+
shell.style.maxHeight = `${shell.scrollHeight}px`;
|
|
132
|
+
void shell.offsetHeight;
|
|
133
|
+
requestAnimationFrame(() => {
|
|
134
|
+
shell.dataset.collapsed = "";
|
|
135
|
+
shell.style.maxHeight = "";
|
|
136
|
+
});
|
|
137
|
+
setExpandedLabel(false);
|
|
138
|
+
}
|
|
139
|
+
scheduleCleanup();
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function setupThreadContexts(
|
|
144
|
+
root: globalThis.Document | globalThis.Element = document,
|
|
145
|
+
): void {
|
|
146
|
+
root
|
|
147
|
+
.querySelectorAll<HTMLElement>("[data-thread-context-toggle]")
|
|
148
|
+
.forEach(setupShell);
|
|
149
|
+
}
|
|
150
|
+
|
|
7
151
|
function isFirstThreadDetailItem(current: HTMLElement): boolean {
|
|
8
152
|
const group = current.closest<HTMLElement>(".thread-group-detail");
|
|
9
153
|
if (!group) return false;
|
|
@@ -39,8 +183,8 @@ function scrollCurrentDetailPostIntoView(
|
|
|
39
183
|
});
|
|
40
184
|
}
|
|
41
185
|
|
|
42
|
-
// Auto-scroll to current post on detail pages
|
|
43
186
|
document.addEventListener("DOMContentLoaded", () => {
|
|
187
|
+
setupThreadContexts(document);
|
|
44
188
|
scrollCurrentDetailPostIntoView(document);
|
|
45
189
|
});
|
|
46
190
|
|
|
@@ -10,11 +10,7 @@ import { Extension, type Editor } from "@tiptap/core";
|
|
|
10
10
|
import { Plugin, PluginKey, Selection } from "@tiptap/pm/state";
|
|
11
11
|
import type { EditorView } from "@tiptap/pm/view";
|
|
12
12
|
import { isLinkToolbarInputActive } from "./link-toolbar.js";
|
|
13
|
-
import {
|
|
14
|
-
applyDockedToolbarOffset,
|
|
15
|
-
isComposeDockedToolbar,
|
|
16
|
-
type FormattingToolbarMode,
|
|
17
|
-
} from "./toolbar-mode.js";
|
|
13
|
+
import type { FormattingToolbarMode } from "./toolbar-mode.js";
|
|
18
14
|
import {
|
|
19
15
|
getFixedFloatingContainerRect,
|
|
20
16
|
getFloatingPosition,
|
|
@@ -206,19 +202,8 @@ export const BubbleMenu = Extension.create({
|
|
|
206
202
|
|
|
207
203
|
function show(view: EditorView) {
|
|
208
204
|
if (!el) return;
|
|
209
|
-
const docked = isComposeDockedToolbar(toolbarMode);
|
|
210
|
-
|
|
211
|
-
el.classList.toggle("tiptap-bubble-menu-docked", docked);
|
|
212
205
|
el.style.display = "flex";
|
|
213
206
|
|
|
214
|
-
if (docked) {
|
|
215
|
-
applyDockedToolbarOffset(el, view);
|
|
216
|
-
el.style.removeProperty("left");
|
|
217
|
-
el.style.removeProperty("top");
|
|
218
|
-
syncActive();
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
207
|
// Position above selection center
|
|
223
208
|
const { from, to } = view.state.selection;
|
|
224
209
|
const start = view.coordsAtPos(from);
|
|
@@ -85,9 +85,7 @@ export function createSettingsEditorExtensions(
|
|
|
85
85
|
BubbleMenu.configure({
|
|
86
86
|
toolbarMode: "compose",
|
|
87
87
|
}),
|
|
88
|
-
LinkToolbar
|
|
89
|
-
toolbarMode: "compose",
|
|
90
|
-
}),
|
|
88
|
+
LinkToolbar,
|
|
91
89
|
];
|
|
92
90
|
}
|
|
93
91
|
|
|
@@ -118,9 +116,7 @@ export function createEditorExtensions(
|
|
|
118
116
|
BubbleMenu.configure({
|
|
119
117
|
toolbarMode: options.toolbarMode ?? "default",
|
|
120
118
|
}),
|
|
121
|
-
LinkToolbar
|
|
122
|
-
toolbarMode: options.toolbarMode ?? "default",
|
|
123
|
-
}),
|
|
119
|
+
LinkToolbar,
|
|
124
120
|
ExitableMarks,
|
|
125
121
|
InsertParagraphAround,
|
|
126
122
|
TabIndent,
|
|
@@ -13,11 +13,6 @@ import { Extension } from "@tiptap/core";
|
|
|
13
13
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
14
14
|
import type { EditorState } from "@tiptap/pm/state";
|
|
15
15
|
import type { EditorView } from "@tiptap/pm/view";
|
|
16
|
-
import {
|
|
17
|
-
applyDockedToolbarOffset,
|
|
18
|
-
isComposeDockedToolbar,
|
|
19
|
-
type FormattingToolbarMode,
|
|
20
|
-
} from "./toolbar-mode.js";
|
|
21
16
|
import {
|
|
22
17
|
getFixedFloatingContainerRect,
|
|
23
18
|
getFloatingPosition,
|
|
@@ -96,15 +91,8 @@ function getLinkRange(state: EditorState): LinkRange | null {
|
|
|
96
91
|
export const LinkToolbar = Extension.create({
|
|
97
92
|
name: "linkToolbar",
|
|
98
93
|
|
|
99
|
-
addOptions() {
|
|
100
|
-
return {
|
|
101
|
-
toolbarMode: "default" as FormattingToolbarMode,
|
|
102
|
-
};
|
|
103
|
-
},
|
|
104
|
-
|
|
105
94
|
addProseMirrorPlugins() {
|
|
106
95
|
const editor = this.editor;
|
|
107
|
-
const toolbarMode = this.options.toolbarMode as FormattingToolbarMode;
|
|
108
96
|
|
|
109
97
|
// DOM elements
|
|
110
98
|
let inputEl: HTMLElement | null = null;
|
|
@@ -204,17 +192,8 @@ export const LinkToolbar = Extension.create({
|
|
|
204
192
|
from: number,
|
|
205
193
|
to: number,
|
|
206
194
|
) {
|
|
207
|
-
const docked = isComposeDockedToolbar(toolbarMode);
|
|
208
|
-
el.classList.toggle("tiptap-link-input-docked", docked);
|
|
209
195
|
el.style.display = "flex";
|
|
210
196
|
|
|
211
|
-
if (docked) {
|
|
212
|
-
applyDockedToolbarOffset(el, view);
|
|
213
|
-
el.style.removeProperty("left");
|
|
214
|
-
el.style.removeProperty("top");
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
197
|
const dialog = view.dom.closest("dialog");
|
|
219
198
|
const start = view.coordsAtPos(from);
|
|
220
199
|
const end = view.coordsAtPos(to);
|