@jant/core 0.6.12 → 0.6.14

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.
Files changed (120) hide show
  1. package/bin/commands/import-site.js +2 -10
  2. package/bin/commands/migrate-text-attachments.js +1 -3
  3. package/bin/commands/site/export.js +1 -3
  4. package/bin/commands/telegram/register-webhooks.js +9 -3
  5. package/bin/lib/site-snapshot.js +1 -5
  6. package/dist/{app-D-zAhayc.js → app-2i7-WQHg.js} +426 -450
  7. package/dist/app-DZDlmh7C.js +6 -0
  8. package/dist/client/.vite/manifest.json +3 -3
  9. package/dist/client/_assets/{client-S1cdvulk.js → client-BvID5ucz.js} +1 -1
  10. package/dist/client/_assets/client-Cevfpm8H.css +2 -0
  11. package/dist/client/_assets/{client-auth-YW92ZH7u.js → client-auth-v3yvhbgZ.js} +596 -590
  12. package/dist/{export-Ckwh4BiE.js → export-DWn149b7.js} +24 -19
  13. package/dist/{github-sync-BMmK3JZ0.js → github-sync-C0zyF6XS.js} +2 -2
  14. package/dist/{github-sync-Dl9wQMar.js → github-sync-Ckk0sJxK.js} +1 -1
  15. package/dist/index.js +3 -3
  16. package/dist/node.js +4 -4
  17. package/package.json +1 -1
  18. package/src/__tests__/export-service.test.ts +1 -2
  19. package/src/__tests__/helpers/app.ts +3 -0
  20. package/src/__tests__/helpers/export-fixtures.ts +0 -1
  21. package/src/__tests__/import-site-command.test.ts +1 -2
  22. package/src/client/__tests__/site-header-nav.test.ts +36 -2
  23. package/src/client/components/__tests__/compose-title-from-heading.test.ts +57 -0
  24. package/src/client/components/__tests__/jant-command-palette.test.ts +137 -10
  25. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3 -0
  26. package/src/client/components/__tests__/jant-compose-editor.test.ts +94 -2
  27. package/src/client/components/__tests__/jant-nav-manager.test.ts +107 -22
  28. package/src/client/components/__tests__/jant-settings-general.test.ts +1 -0
  29. package/src/client/components/compose-title-from-heading.ts +66 -0
  30. package/src/client/components/compose-types.ts +8 -0
  31. package/src/client/components/inline-image-issues.ts +17 -0
  32. package/src/client/components/jant-command-palette.ts +100 -70
  33. package/src/client/components/jant-compose-dialog.ts +3 -1
  34. package/src/client/components/jant-compose-editor.ts +78 -1
  35. package/src/client/components/jant-compose-fullscreen.ts +2 -0
  36. package/src/client/components/jant-nav-manager.ts +245 -155
  37. package/src/client/components/nav-manager-types.ts +0 -10
  38. package/src/client/site-header-nav.d.ts +1 -0
  39. package/src/client/site-header-nav.js +105 -47
  40. package/src/client/tiptap/__tests__/block-insertion.test.ts +35 -0
  41. package/src/client/tiptap/__tests__/footnotes.test.ts +284 -0
  42. package/src/client/tiptap/__tests__/image-input-rules.test.ts +117 -0
  43. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +148 -1
  44. package/src/client/tiptap/bubble-menu.ts +3 -3
  45. package/src/client/tiptap/create-editor.ts +7 -0
  46. package/src/client/tiptap/extensions.ts +7 -2
  47. package/src/client/tiptap/footnotes.ts +156 -0
  48. package/src/client/tiptap/image-input-rules.ts +48 -0
  49. package/src/client/tiptap/image-node.ts +189 -7
  50. package/src/client/tiptap/markdown-clipboard.ts +7 -2
  51. package/src/client-auth.ts +0 -1
  52. package/src/i18n/locales/public/en.po +20 -4
  53. package/src/i18n/locales/public/en.ts +1 -1
  54. package/src/i18n/locales/public/zh-Hans.po +20 -4
  55. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  56. package/src/i18n/locales/public/zh-Hant.po +20 -4
  57. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  58. package/src/i18n/locales/settings/en.po +23 -20
  59. package/src/i18n/locales/settings/en.ts +1 -1
  60. package/src/i18n/locales/settings/zh-Hans.po +23 -20
  61. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  62. package/src/i18n/locales/settings/zh-Hant.po +23 -20
  63. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  64. package/src/lib/__tests__/markdown-to-tiptap.test.ts +4 -0
  65. package/src/lib/__tests__/navigation.test.ts +4 -37
  66. package/src/lib/__tests__/resolve-config.test.ts +6 -1
  67. package/src/lib/__tests__/view.test.ts +6 -30
  68. package/src/lib/github-sync-site-config.ts +0 -2
  69. package/src/lib/hugo-markdown.ts +1 -1
  70. package/src/lib/markdown-manager.ts +14 -5
  71. package/src/lib/navigation.ts +5 -22
  72. package/src/lib/resolve-config.ts +9 -2
  73. package/src/lib/site-header-fragment.tsx +37 -0
  74. package/src/lib/view.ts +2 -7
  75. package/src/node/__tests__/cli-site-snapshot.test.ts +45 -45
  76. package/src/routes/api/__tests__/nav-items.test.ts +26 -0
  77. package/src/routes/api/export.ts +0 -2
  78. package/src/routes/api/internal/__tests__/uploads.test.ts +9 -13
  79. package/src/routes/api/internal/sites.ts +5 -5
  80. package/src/routes/api/nav-items.ts +25 -4
  81. package/src/routes/dash/settings.tsx +8 -2
  82. package/src/routes/feed/__tests__/feed.test.ts +42 -0
  83. package/src/routes/feed/__tests__/sitemap.test.ts +2 -13
  84. package/src/routes/feed/feed.ts +1 -0
  85. package/src/routes/feed/sitemap.ts +2 -6
  86. package/src/routes/pages/archive.tsx +1 -0
  87. package/src/routes/pages/collection.tsx +1 -0
  88. package/src/routes/pages/featured.tsx +0 -4
  89. package/src/routes/pages/home.tsx +4 -51
  90. package/src/routes/pages/latest.tsx +2 -51
  91. package/src/services/__tests__/github-app-installations.test.ts +10 -10
  92. package/src/services/__tests__/navigation.test.ts +41 -0
  93. package/src/services/__tests__/post-timeline.test.ts +56 -0
  94. package/src/services/__tests__/post.test.ts +33 -0
  95. package/src/services/__tests__/settings.test.ts +5 -20
  96. package/src/services/export-theme/assets/client-site.js +10 -10
  97. package/src/services/export-theme/layouts/_default/rss.xml +5 -15
  98. package/src/services/export-theme/layouts/index.html +26 -39
  99. package/src/services/export.ts +5 -10
  100. package/src/services/github-app-installations.ts +1 -3
  101. package/src/services/navigation.ts +44 -13
  102. package/src/services/post.ts +80 -57
  103. package/src/services/settings.ts +0 -10
  104. package/src/services/site-admin.ts +0 -2
  105. package/src/services/upload-session.ts +4 -18
  106. package/src/styles/tokens.css +2 -2
  107. package/src/styles/ui.css +316 -34
  108. package/src/types/bindings.ts +0 -1
  109. package/src/types/config.ts +3 -7
  110. package/src/ui/compose/ComposeDialog.tsx +28 -0
  111. package/src/ui/dash/appearance/NavigationContent.tsx +1 -1
  112. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  113. package/src/ui/dash/settings/SettingsRootContent.tsx +3 -3
  114. package/src/ui/feed/__tests__/timeline-cards.test.ts +8 -3
  115. package/src/ui/layouts/BaseLayout.tsx +1 -1
  116. package/src/ui/layouts/SiteLayout.tsx +266 -227
  117. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +48 -0
  118. package/dist/app-B5P1b4TF.js +0 -6
  119. package/dist/client/_assets/client-BUxR-E8O.css +0 -2
  120. package/src/client/nav-manager-bridge.ts +0 -54
@@ -112,26 +112,44 @@ function HeaderLink({
112
112
  );
113
113
  }
114
114
 
115
- export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
115
+ export interface SiteHeaderProps {
116
+ siteName: string;
117
+ links: NavItemView[];
118
+ currentPath: string;
119
+ sitePathPrefix?: string;
120
+ siteAvatarUrl?: string;
121
+ showHeaderAvatar?: boolean;
122
+ }
123
+
124
+ function linkCollapseTier(idx: number): string {
125
+ if (idx < 2) return "";
126
+ if (idx === 2) return "site-header-link-collapse-sm";
127
+ if (idx === 3) return "site-header-link-collapse-md";
128
+ return "site-header-link-collapse-lg";
129
+ }
130
+
131
+ function moreLinkRevealTier(idx: number): string {
132
+ if (idx === 2) return "site-header-more-link-show-sm";
133
+ if (idx === 3) return "site-header-more-link-show-md";
134
+ return "site-header-more-link-show-lg";
135
+ }
136
+
137
+ function getResponsiveOverflowTier(
138
+ headerLinkCount: number,
139
+ ): "sm" | "md" | "lg" | null {
140
+ if (headerLinkCount >= 5) return "lg";
141
+ if (headerLinkCount === 4) return "md";
142
+ if (headerLinkCount === 3) return "sm";
143
+ return null;
144
+ }
145
+
146
+ export const SiteHeader: FC<SiteHeaderProps> = ({
116
147
  siteName,
117
148
  links,
118
149
  currentPath,
119
150
  sitePathPrefix = "",
120
- isAuthenticated,
121
- collections,
122
151
  siteAvatarUrl,
123
152
  showHeaderAvatar,
124
- siteDescriptionHtml,
125
- siteFooterHtml,
126
- showHomeBranding = false,
127
- sidebar,
128
- uploadMaxFileSize,
129
- showComposeDialog = true,
130
- showHeader = true,
131
- composeOpenShortcutDiscovered = false,
132
- slashCommandDiscovered = false,
133
- composeCollectionId,
134
- children,
135
153
  }) => {
136
154
  const { i18n } = useLingui();
137
155
  const linksWithLabels = links.map((link) => ({
@@ -154,56 +172,30 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
154
172
  comment: "@context: Hamburger menu button label",
155
173
  }),
156
174
  );
157
- const moreLabel = i18n._(NAV_MORE_LABEL);
158
- const newPostLabel = i18n._(
175
+ const closeMenuLabel = i18n._(
159
176
  msg({
160
- message: "New post",
161
- comment:
162
- "@context: Mobile floating action button label to open the compose dialog",
177
+ message: "Close menu",
178
+ comment: "@context: Close drawer button label",
163
179
  }),
164
180
  );
181
+ const moreLabel = i18n._(NAV_MORE_LABEL);
165
182
 
166
- // Split custom links by placement
183
+ // Split custom links by placement.
167
184
  const headerLinks = linksWithLabels.filter(
168
185
  (l) => l.placement === "header" || !l.placement,
169
186
  );
170
187
  const moreLinks = linksWithLabels.filter((l) => l.placement === "more");
171
- // Responsive collapse tiers — first two links always stay inline; the
172
- // remaining links collapse into the "More" popover in stages so wider
173
- // windows (tablets, narrow laptops) can show 3 or 4 links before falling
174
- // back to the minimum of 2.
175
- //
176
- // idx 2 → collapse-sm (hidden ≤580px, shown in popover)
177
- // idx 3 → collapse-md (hidden ≤780px)
178
- // idx ≥ 4 → collapse-lg (hidden ≤960px)
179
- function linkCollapseTier(idx: number): string {
180
- if (idx < 2) return "";
181
- if (idx === 2) return "site-header-link-collapse-sm";
182
- if (idx === 3) return "site-header-link-collapse-md";
183
- return "site-header-link-collapse-lg";
184
- }
185
- function moreLinkRevealTier(idx: number): string {
186
- if (idx === 2) return "site-header-more-link-show-sm";
187
- if (idx === 3) return "site-header-more-link-show-md";
188
- return "site-header-more-link-show-lg";
189
- }
190
- const hasResponsiveOverflow = headerLinks.length > 2;
188
+ const responsiveOverflowTier = getResponsiveOverflowTier(headerLinks.length);
189
+ const hasResponsiveOverflow = responsiveOverflowTier !== null;
191
190
  const hasSupplementalMoreLinks = moreLinks.length > 0;
192
191
  const showMoreMenu = hasResponsiveOverflow || hasSupplementalMoreLinks;
192
+
193
193
  // Decide the widest breakpoint at which the "More" button must appear
194
- // when there are no supplemental links. With 3 total links only idx 2 ever
195
- // collapses (≤780); with 4 links idx 3 starts collapsing at ≤920; with 5+
196
- // idx 4 starts collapsing at ≤1100.
197
- let responsiveTierClass = "";
198
- if (hasResponsiveOverflow && !hasSupplementalMoreLinks) {
199
- if (headerLinks.length >= 5) {
200
- responsiveTierClass = "site-header-more-tier-lg";
201
- } else if (headerLinks.length === 4) {
202
- responsiveTierClass = "site-header-more-tier-md";
203
- } else {
204
- responsiveTierClass = "site-header-more-tier-sm";
205
- }
206
- }
194
+ // when there are no supplemental links.
195
+ const responsiveTierClass =
196
+ responsiveOverflowTier && !hasSupplementalMoreLinks
197
+ ? `site-header-more-tier-${responsiveOverflowTier}`
198
+ : "";
207
199
  const moreMenuClass = [
208
200
  "site-header-more",
209
201
  hasResponsiveOverflow && !hasSupplementalMoreLinks
@@ -213,127 +205,87 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
213
205
  ]
214
206
  .filter(Boolean)
215
207
  .join(" ");
216
-
217
208
  const isHomePage =
218
209
  currentPath === toPublicPath("/", sitePathPrefix) ||
219
210
  currentPath === toPublicPath("/featured", sitePathPrefix) ||
220
211
  currentPath === toPublicPath("/latest", sitePathPrefix);
221
- const showMobileComposeFab = Boolean(
222
- (isHomePage || composeCollectionId) && isAuthenticated && showComposeDialog,
223
- );
224
- const mobileComposeFabClick = composeCollectionId
225
- ? `document.getElementById('compose-dialog')?.querySelector('jant-compose-dialog')?.openNew(${JSON.stringify(
226
- { collectionId: composeCollectionId },
227
- )})`
228
- : "document.getElementById('compose-dialog')?.querySelector('jant-compose-dialog')?.openNew()";
229
- const contentClass = [
230
- "site-content",
231
- isHomePage ? "site-content-home" : "",
232
- showMobileComposeFab ? "site-content-mobile-compose-enabled" : "",
233
- ]
234
- .filter(Boolean)
235
- .join(" ");
236
212
 
237
213
  return (
238
- <div class="site-page">
239
- {showHeader && (
240
- <header class="site-header">
241
- <div class="site-header-inner">
242
- <div
243
- class={`site-header-top site-header-top-bordered${isHomePage ? " site-header-top-home" : ""}`}
244
- >
245
- <a href={toPublicPath("/", sitePathPrefix)} class="site-logo">
246
- {showHeaderAvatar && siteAvatarUrl && (
247
- <img src={siteAvatarUrl} class="site-logo-avatar" alt="" />
248
- )}
249
- {siteName}
250
- </a>
251
- <nav class="site-header-nav" aria-label="Primary">
252
- {headerLinks.map((link, idx) => (
253
- <HeaderLink
254
- key={link.id}
255
- link={link}
256
- label={link.displayLabel}
257
- className={[
258
- idx < 2
259
- ? "site-header-link-primary"
260
- : "site-header-link-overflow",
261
- linkCollapseTier(idx),
262
- ]
263
- .filter(Boolean)
264
- .join(" ")}
265
- />
266
- ))}
267
- {showMoreMenu && (
268
- <div class={moreMenuClass}>
269
- <button
270
- type="button"
271
- class="site-header-more-btn"
272
- id="site-nav-more-trigger"
273
- aria-haspopup="menu"
274
- aria-expanded="false"
275
- >
276
- {moreLabel}
277
- <svg
278
- xmlns="http://www.w3.org/2000/svg"
279
- width="12"
280
- height="12"
281
- viewBox="0 0 24 24"
282
- fill="none"
283
- stroke="currentColor"
284
- stroke-width="2.5"
285
- stroke-linecap="round"
286
- stroke-linejoin="round"
287
- aria-hidden="true"
288
- >
289
- <path d="m6 9 6 6 6-6" />
290
- </svg>
291
- </button>
292
- <div
293
- id="site-nav-more-popover"
294
- class="site-header-more-popover"
295
- data-popover
296
- data-align="start"
214
+ <>
215
+ <header class="site-header" data-site-header-fragment="header">
216
+ <div class="site-header-inner">
217
+ <div
218
+ class={`site-header-top site-header-top-bordered${isHomePage ? " site-header-top-home" : ""}`}
219
+ >
220
+ <a href={toPublicPath("/", sitePathPrefix)} class="site-logo">
221
+ {showHeaderAvatar && siteAvatarUrl && (
222
+ <img src={siteAvatarUrl} class="site-logo-avatar" alt="" />
223
+ )}
224
+ {siteName}
225
+ </a>
226
+ <nav class="site-header-nav" aria-label="Primary">
227
+ {headerLinks.map((link, idx) => (
228
+ <HeaderLink
229
+ key={link.id}
230
+ link={link}
231
+ label={link.displayLabel}
232
+ className={[
233
+ idx < 2
234
+ ? "site-header-link-primary"
235
+ : "site-header-link-overflow",
236
+ linkCollapseTier(idx),
237
+ ]
238
+ .filter(Boolean)
239
+ .join(" ")}
240
+ />
241
+ ))}
242
+ {showMoreMenu && (
243
+ <div class={moreMenuClass}>
244
+ <button
245
+ type="button"
246
+ class="site-header-more-btn"
247
+ id="site-nav-more-trigger"
248
+ aria-haspopup="menu"
249
+ aria-expanded="false"
250
+ >
251
+ {moreLabel}
252
+ <svg
253
+ xmlns="http://www.w3.org/2000/svg"
254
+ width="12"
255
+ height="12"
256
+ viewBox="0 0 24 24"
257
+ fill="none"
258
+ stroke="currentColor"
259
+ stroke-width="2.5"
260
+ stroke-linecap="round"
261
+ stroke-linejoin="round"
297
262
  aria-hidden="true"
298
263
  >
299
- {headerLinks.slice(2).map((link, i) => {
300
- const idx = i + 2;
301
- const classes = [
302
- "site-header-more-link",
303
- "site-header-more-link-responsive",
304
- moreLinkRevealTier(idx),
305
- link.isActive ? "site-header-more-link-active" : "",
306
- ]
307
- .filter(Boolean)
308
- .join(" ");
309
- return (
310
- <a
311
- key={link.id}
312
- href={link.url}
313
- class={classes}
314
- {...(link.isExternal
315
- ? {
316
- target: "_blank",
317
- rel: "noopener noreferrer",
318
- }
319
- : {})}
320
- {...(link.freshAt
321
- ? { "data-fresh-at": link.freshAt }
322
- : {})}
323
- >
324
- {link.displayLabel}
325
- {link.isExternal && <ExternalLinkIcon />}
326
- </a>
327
- );
328
- })}
329
- {hasResponsiveOverflow && hasSupplementalMoreLinks && (
330
- <div class="site-header-more-divider site-header-more-divider-responsive" />
331
- )}
332
- {moreLinks.map((link) => (
264
+ <path d="m6 9 6 6 6-6" />
265
+ </svg>
266
+ </button>
267
+ <div
268
+ id="site-nav-more-popover"
269
+ class="site-header-more-popover"
270
+ data-popover
271
+ data-align="start"
272
+ aria-hidden="true"
273
+ >
274
+ {headerLinks.slice(2).map((link, i) => {
275
+ const idx = i + 2;
276
+ const classes = [
277
+ "site-header-more-link",
278
+ "site-header-more-link-responsive",
279
+ moreLinkRevealTier(idx),
280
+ link.isActive ? "site-header-more-link-active" : "",
281
+ ]
282
+ .filter(Boolean)
283
+ .join(" ");
284
+ return (
333
285
  <a
334
286
  key={link.id}
335
287
  href={link.url}
336
- class={`site-header-more-link site-header-more-link-supplemental ${link.isActive ? "site-header-more-link-active" : ""}`}
288
+ class={classes}
337
289
  {...(link.isExternal
338
290
  ? {
339
291
  target: "_blank",
@@ -347,75 +299,101 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
347
299
  {link.displayLabel}
348
300
  {link.isExternal && <ExternalLinkIcon />}
349
301
  </a>
350
- ))}
351
- </div>
302
+ );
303
+ })}
304
+ {responsiveOverflowTier && hasSupplementalMoreLinks && (
305
+ <div
306
+ class={`site-header-more-divider site-header-more-divider-responsive site-header-more-divider-show-${responsiveOverflowTier}`}
307
+ />
308
+ )}
309
+ {moreLinks.map((link) => (
310
+ <a
311
+ key={link.id}
312
+ href={link.url}
313
+ class={`site-header-more-link site-header-more-link-supplemental ${link.isActive ? "site-header-more-link-active" : ""}`}
314
+ {...(link.isExternal
315
+ ? {
316
+ target: "_blank",
317
+ rel: "noopener noreferrer",
318
+ }
319
+ : {})}
320
+ {...(link.freshAt
321
+ ? { "data-fresh-at": link.freshAt }
322
+ : {})}
323
+ >
324
+ {link.displayLabel}
325
+ {link.isExternal && <ExternalLinkIcon />}
326
+ </a>
327
+ ))}
352
328
  </div>
353
- )}
354
- </nav>
329
+ </div>
330
+ )}
331
+ </nav>
355
332
 
356
- {/* Search */}
357
- <div class="site-header-search-slot">
358
- <form
359
- class="site-header-search-form"
360
- action={searchHref}
361
- method="get"
362
- >
363
- <SearchIcon class="site-header-search-icon" />
364
- <input
365
- type="search"
366
- name="q"
367
- class="site-header-search-input"
368
- placeholder={searchLabel}
369
- aria-label={searchLabel}
370
- enterkeyhint="search"
371
- />
372
- </form>
373
- <a
374
- href={searchHref}
375
- class="site-header-search-link"
333
+ <div class="site-header-search-slot">
334
+ <form
335
+ class="site-header-search-form"
336
+ action={searchHref}
337
+ method="get"
338
+ >
339
+ <SearchIcon class="site-header-search-icon" />
340
+ <input
341
+ type="search"
342
+ name="q"
343
+ class="site-header-search-input"
344
+ placeholder={searchLabel}
376
345
  aria-label={searchLabel}
377
- title={searchLabel}
378
- >
379
- <SearchIcon class="site-header-search-link-icon" />
380
- </a>
381
- </div>
346
+ enterkeyhint="search"
347
+ />
348
+ </form>
349
+ <a
350
+ href={searchHref}
351
+ class="site-header-search-link"
352
+ aria-label={searchLabel}
353
+ title={searchLabel}
354
+ >
355
+ <SearchIcon class="site-header-search-link-icon" />
356
+ </a>
357
+ </div>
382
358
 
383
- <div class="site-header-right">
384
- {/* Mobile hamburger */}
385
- <button
386
- type="button"
387
- class="site-header-hamburger"
388
- aria-controls="site-nav-drawer"
389
- aria-expanded="false"
390
- aria-label={menuLabel}
359
+ <div class="site-header-right">
360
+ <button
361
+ type="button"
362
+ class="site-header-hamburger"
363
+ aria-controls="site-nav-drawer"
364
+ aria-expanded="false"
365
+ aria-label={menuLabel}
366
+ >
367
+ <svg
368
+ xmlns="http://www.w3.org/2000/svg"
369
+ width="20"
370
+ height="20"
371
+ viewBox="0 0 24 24"
372
+ fill="none"
373
+ stroke="currentColor"
374
+ stroke-width="2"
375
+ stroke-linecap="round"
376
+ stroke-linejoin="round"
391
377
  >
392
- <svg
393
- xmlns="http://www.w3.org/2000/svg"
394
- width="20"
395
- height="20"
396
- viewBox="0 0 24 24"
397
- fill="none"
398
- stroke="currentColor"
399
- stroke-width="2"
400
- stroke-linecap="round"
401
- stroke-linejoin="round"
402
- >
403
- <line x1="4" x2="20" y1="12" y2="12" />
404
- <line x1="4" x2="20" y1="6" y2="6" />
405
- <line x1="4" x2="20" y1="18" y2="18" />
406
- </svg>
407
- </button>
408
- </div>
378
+ <line x1="4" x2="20" y1="12" y2="12" />
379
+ <line x1="4" x2="20" y1="6" y2="6" />
380
+ <line x1="4" x2="20" y1="18" y2="18" />
381
+ </svg>
382
+ </button>
409
383
  </div>
410
384
  </div>
411
- </header>
412
- )}
385
+ </div>
386
+ </header>
413
387
 
414
- {/* Mobile navigation drawer */}
415
- <div class="site-nav-drawer-backdrop" aria-hidden="true" />
388
+ <div
389
+ class="site-nav-drawer-backdrop"
390
+ data-site-header-fragment="drawer-backdrop"
391
+ aria-hidden="true"
392
+ />
416
393
  <div
417
394
  id="site-nav-drawer"
418
395
  class="site-nav-drawer"
396
+ data-site-header-fragment="drawer"
419
397
  role="dialog"
420
398
  aria-modal="true"
421
399
  aria-label={menuLabel}
@@ -439,12 +417,7 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
439
417
  <button
440
418
  type="button"
441
419
  class="site-nav-drawer-close"
442
- aria-label={i18n._(
443
- msg({
444
- message: "Close menu",
445
- comment: "@context: Close drawer button label",
446
- }),
447
- )}
420
+ aria-label={closeMenuLabel}
448
421
  >
449
422
  <svg
450
423
  xmlns="http://www.w3.org/2000/svg"
@@ -499,6 +472,72 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
499
472
  )}
500
473
  </nav>
501
474
  </div>
475
+ </>
476
+ );
477
+ };
478
+
479
+ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
480
+ siteName,
481
+ links,
482
+ currentPath,
483
+ sitePathPrefix = "",
484
+ isAuthenticated,
485
+ collections,
486
+ siteAvatarUrl,
487
+ showHeaderAvatar,
488
+ siteDescriptionHtml,
489
+ siteFooterHtml,
490
+ showHomeBranding = false,
491
+ sidebar,
492
+ uploadMaxFileSize,
493
+ showComposeDialog = true,
494
+ showHeader = true,
495
+ composeOpenShortcutDiscovered = false,
496
+ slashCommandDiscovered = false,
497
+ composeCollectionId,
498
+ children,
499
+ }) => {
500
+ const { i18n } = useLingui();
501
+ const newPostLabel = i18n._(
502
+ msg({
503
+ message: "New post",
504
+ comment:
505
+ "@context: Mobile floating action button label to open the compose dialog",
506
+ }),
507
+ );
508
+
509
+ const isHomePage =
510
+ currentPath === toPublicPath("/", sitePathPrefix) ||
511
+ currentPath === toPublicPath("/featured", sitePathPrefix) ||
512
+ currentPath === toPublicPath("/latest", sitePathPrefix);
513
+ const showMobileComposeFab = Boolean(
514
+ (isHomePage || composeCollectionId) && isAuthenticated && showComposeDialog,
515
+ );
516
+ const mobileComposeFabClick = composeCollectionId
517
+ ? `document.getElementById('compose-dialog')?.querySelector('jant-compose-dialog')?.openNew(${JSON.stringify(
518
+ { collectionId: composeCollectionId },
519
+ )})`
520
+ : "document.getElementById('compose-dialog')?.querySelector('jant-compose-dialog')?.openNew()";
521
+ const contentClass = [
522
+ "site-content",
523
+ isHomePage ? "site-content-home" : "",
524
+ showMobileComposeFab ? "site-content-mobile-compose-enabled" : "",
525
+ ]
526
+ .filter(Boolean)
527
+ .join(" ");
528
+
529
+ return (
530
+ <div class="site-page">
531
+ {showHeader && (
532
+ <SiteHeader
533
+ siteName={siteName}
534
+ links={links}
535
+ currentPath={currentPath}
536
+ sitePathPrefix={sitePathPrefix}
537
+ siteAvatarUrl={siteAvatarUrl}
538
+ showHeaderAvatar={showHeaderAvatar}
539
+ />
540
+ )}
502
541
 
503
542
  <main class="site-main">
504
543
  {sidebar ? (
@@ -3,8 +3,24 @@ import { renderToString } from "hono/jsx/dom/server";
3
3
  import { describe, expect, it } from "vitest";
4
4
  import { I18nProvider } from "../../../i18n/context.js";
5
5
  import { createI18n } from "../../../i18n/i18n.js";
6
+ import type { NavItemView } from "../../../types.js";
6
7
  import { SiteLayout } from "../SiteLayout.js";
7
8
 
9
+ function createNavItem(
10
+ index: number,
11
+ placement: NavItemView["placement"],
12
+ ): NavItemView {
13
+ return {
14
+ id: `nav_${index}`,
15
+ type: "link",
16
+ label: `Link ${index}`,
17
+ url: `/link-${index}`,
18
+ placement,
19
+ isActive: false,
20
+ isExternal: false,
21
+ };
22
+ }
23
+
8
24
  function renderSiteLayout(
9
25
  props: Partial<Parameters<typeof SiteLayout>[0]> = {},
10
26
  ): string {
@@ -30,6 +46,38 @@ function renderSiteLayout(
30
46
  }
31
47
 
32
48
  describe("SiteLayout", () => {
49
+ it.each([
50
+ [3, "sm"],
51
+ [4, "md"],
52
+ [5, "lg"],
53
+ ] as const)(
54
+ "reveals the More divider at the first breakpoint that overflows %i header links",
55
+ (headerLinkCount, tier) => {
56
+ const headerLinks = Array.from({ length: headerLinkCount }, (_, index) =>
57
+ createNavItem(index, "header"),
58
+ );
59
+ const html = renderSiteLayout({
60
+ links: [...headerLinks, createNavItem(headerLinkCount, "more")],
61
+ });
62
+
63
+ expect(html).toContain(
64
+ `class="site-header-more-divider site-header-more-divider-responsive site-header-more-divider-show-${tier}"`,
65
+ );
66
+ },
67
+ );
68
+
69
+ it("omits the More divider when no header link can overflow", () => {
70
+ const html = renderSiteLayout({
71
+ links: [
72
+ createNavItem(0, "header"),
73
+ createNavItem(1, "header"),
74
+ createNavItem(2, "more"),
75
+ ],
76
+ });
77
+
78
+ expect(html).not.toContain("site-header-more-divider");
79
+ });
80
+
33
81
  it("renders the mobile compose FAB for authenticated timeline pages", () => {
34
82
  const html = renderSiteLayout({
35
83
  currentPath: "/latest",
@@ -1,6 +0,0 @@
1
- import "./url-BMYO-Zlt.js";
2
- import { t as createApp } from "./app-D-zAhayc.js";
3
- import "./export-Ckwh4BiE.js";
4
- import "./env-OHRKGcMj.js";
5
- import "./github-sync-Dl9wQMar.js";
6
- export { createApp };