@jant/core 0.6.13 → 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 (30) hide show
  1. package/dist/{app-HnzmsaqU.js → app-2i7-WQHg.js} +26 -22
  2. package/dist/app-DZDlmh7C.js +6 -0
  3. package/dist/client/.vite/manifest.json +3 -3
  4. package/dist/client/_assets/{client-3_OaxhTs.js → client-BvID5ucz.js} +1 -1
  5. package/dist/client/_assets/client-Cevfpm8H.css +2 -0
  6. package/dist/client/_assets/{client-auth-TsYq90xm.js → client-auth-v3yvhbgZ.js} +175 -172
  7. package/dist/{export-Dfbq9aot.js → export-DWn149b7.js} +14 -6
  8. package/dist/{github-sync-BGTLx8Mf.js → github-sync-C0zyF6XS.js} +2 -2
  9. package/dist/{github-sync--PSoE-Ne.js → github-sync-Ckk0sJxK.js} +1 -1
  10. package/dist/index.js +3 -3
  11. package/dist/node.js +4 -4
  12. package/package.json +1 -1
  13. package/src/client/components/__tests__/jant-command-palette.test.ts +137 -10
  14. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3 -0
  15. package/src/client/components/jant-command-palette.ts +100 -70
  16. package/src/client/components/jant-compose-dialog.ts +1 -1
  17. package/src/client/tiptap/__tests__/footnotes.test.ts +284 -0
  18. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +148 -1
  19. package/src/client/tiptap/bubble-menu.ts +3 -3
  20. package/src/client/tiptap/footnotes.ts +156 -0
  21. package/src/client/tiptap/markdown-clipboard.ts +7 -2
  22. package/src/lib/__tests__/markdown-to-tiptap.test.ts +4 -0
  23. package/src/lib/markdown-manager.ts +14 -5
  24. package/src/styles/ui.css +116 -28
  25. package/src/ui/feed/__tests__/timeline-cards.test.ts +8 -3
  26. package/src/ui/layouts/BaseLayout.tsx +1 -1
  27. package/src/ui/layouts/SiteLayout.tsx +19 -13
  28. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +48 -0
  29. package/dist/app-CWJFPjuR.js +0 -6
  30. package/dist/client/_assets/client-DCrcHVJl.css +0 -2
@@ -723,6 +723,7 @@ export function createMarkdownContentExtensions(
723
723
  heading: { levels: [1, 2, 3] },
724
724
  link: { openOnClick: false, autolink: false },
725
725
  codeBlock: false,
726
+ trailingNode: { notAfter: ["footnoteDefinition"] },
726
727
  }),
727
728
  MarkdownCodeBlock,
728
729
  Table.configure({
@@ -742,11 +743,19 @@ export function createMarkdownContentExtensions(
742
743
  ];
743
744
  }
744
745
 
745
- function normalizeMarkdownDoc(node: JSONContent): JSONContent {
746
+ /**
747
+ * Normalizes Markdown parser output before it enters the editor schema.
748
+ *
749
+ * @param node - Parsed Tiptap document or descendant node
750
+ * @returns A normalized copy safe to load into an editor
751
+ * @example
752
+ * const normalized = normalizeMarkdownDocument(markdownManager.parse(source));
753
+ */
754
+ export function normalizeMarkdownDocument(node: JSONContent): JSONContent {
746
755
  const normalized: JSONContent = { ...node };
747
756
 
748
757
  if (normalized.content) {
749
- normalized.content = normalized.content.map(normalizeMarkdownDoc);
758
+ normalized.content = normalized.content.map(normalizeMarkdownDocument);
750
759
  }
751
760
 
752
761
  if (normalized.marks) {
@@ -810,9 +819,9 @@ function normalizeMarkdownDoc(node: JSONContent): JSONContent {
810
819
  child?.type === "text" &&
811
820
  typeof child.text === "string" &&
812
821
  nextChild?.type === "footnoteReference" &&
813
- child.text.endsWith("\n")
822
+ /\n[ \t]*$/.test(child.text)
814
823
  ) {
815
- const trimmedText = child.text.replace(/\n$/, "");
824
+ const trimmedText = child.text.replace(/\n[ \t]*$/, "");
816
825
  if (trimmedText) {
817
826
  nextContent.push({
818
827
  ...child,
@@ -854,7 +863,7 @@ export function getMarkdownManager(): MarkdownManager {
854
863
  }
855
864
 
856
865
  export function parseMarkdownDocument(markdown: string): JSONContent {
857
- return normalizeMarkdownDoc(getMarkdownManager().parse(markdown));
866
+ return normalizeMarkdownDocument(getMarkdownManager().parse(markdown));
858
867
  }
859
868
 
860
869
  export function serializeMarkdownDocument(doc: JSONContent): string {
package/src/styles/ui.css CHANGED
@@ -848,7 +848,7 @@
848
848
  display: flex;
849
849
  }
850
850
 
851
- .site-header-more-divider-responsive {
851
+ .site-header-more-divider-show-lg {
852
852
  display: block;
853
853
  }
854
854
  }
@@ -865,6 +865,10 @@
865
865
  .site-header-more-link-show-md {
866
866
  display: flex;
867
867
  }
868
+
869
+ .site-header-more-divider-show-md {
870
+ display: block;
871
+ }
868
872
  }
869
873
 
870
874
  @media (max-width: 580px) {
@@ -879,6 +883,10 @@
879
883
  .site-header-more-link-show-sm {
880
884
  display: flex;
881
885
  }
886
+
887
+ .site-header-more-divider-show-sm {
888
+ display: block;
889
+ }
882
890
  }
883
891
 
884
892
  @media (max-width: 480px) {
@@ -5425,6 +5433,30 @@
5425
5433
  color: var(--site-text-primary);
5426
5434
  }
5427
5435
 
5436
+ .compose-attached-done {
5437
+ margin-left: auto;
5438
+ padding: 6px 14px;
5439
+ border: none;
5440
+ border-radius: 8px;
5441
+ background-color: var(--site-text-primary);
5442
+ color: var(--site-elevated-bg);
5443
+ font-size: var(--type-ui-control);
5444
+ font-weight: var(--fw-medium);
5445
+ line-height: 1.25;
5446
+ cursor: pointer;
5447
+ transition: opacity 0.15s ease;
5448
+ }
5449
+
5450
+ .compose-attached-done:hover {
5451
+ opacity: 0.82;
5452
+ }
5453
+
5454
+ .compose-attached-done:focus-visible {
5455
+ outline: 2px solid
5456
+ color-mix(in srgb, var(--site-text-primary) 24%, transparent);
5457
+ outline-offset: 2px;
5458
+ }
5459
+
5428
5460
  .compose-attached-tiptap {
5429
5461
  flex: 1;
5430
5462
  min-height: 0;
@@ -5436,6 +5468,10 @@
5436
5468
  min-height: 100%;
5437
5469
  }
5438
5470
 
5471
+ .compose-attached-tiptap .tiptap > :first-child {
5472
+ margin-top: 0;
5473
+ }
5474
+
5439
5475
  .compose-attached-tiptap::-webkit-scrollbar {
5440
5476
  width: 3px;
5441
5477
  }
@@ -7735,28 +7771,9 @@
7735
7771
 
7736
7772
  /*
7737
7773
  * Reply context clones feed/detail card internals into a much narrower shell.
7738
- * Reset the reading widths and heading scale so the preview uses the thread
7739
- * context ladder instead of the original page/card layout.
7774
+ * Scale headings to the thread context ladder instead of the original
7775
+ * page/card layout.
7740
7776
  */
7741
- .compose-reply-context-body
7742
- :is(
7743
- .post-detail-title,
7744
- .post-detail-body,
7745
- .post-body-summary,
7746
- .feed-note-title,
7747
- .feed-link-title,
7748
- .feed-quote,
7749
- .feed-quote-attribution,
7750
- .feed-quote-commentary,
7751
- .feed-link-domain,
7752
- .feed-continue-link,
7753
- [data-post-body].prose,
7754
- [data-post-meta]
7755
- ) {
7756
- width: 100%;
7757
- max-width: none;
7758
- }
7759
-
7760
7777
  .compose-reply-context-body
7761
7778
  :is(.post-detail-title, .feed-note-title, .feed-link-title),
7762
7779
  .compose-reply-context-body .prose :is(h1, h2, h3, h4) {
@@ -7976,11 +7993,11 @@
7976
7993
  }
7977
7994
 
7978
7995
  .compose-thread-layout {
7979
- padding-top: 0.45rem;
7996
+ padding-top: 0.75rem;
7980
7997
  }
7981
7998
 
7982
7999
  .compose-thread-layout::before {
7983
- top: 0.45rem;
8000
+ top: 0.75rem;
7984
8001
  }
7985
8002
 
7986
8003
  .compose-reply-context {
@@ -8630,9 +8647,13 @@
8630
8647
  }
8631
8648
  }
8632
8649
 
8650
+ .command-palette-results-container {
8651
+ display: flex;
8652
+ flex-direction: column;
8653
+ min-height: 0;
8654
+ }
8655
+
8633
8656
  .command-palette-results {
8634
- list-style: none;
8635
- margin: 0;
8636
8657
  padding: 0.25rem 0;
8637
8658
  max-height: 20rem;
8638
8659
  overflow-y: auto;
@@ -8674,6 +8695,7 @@
8674
8695
  .command-palette-result-body {
8675
8696
  display: flex;
8676
8697
  flex-direction: column;
8698
+ flex: 1;
8677
8699
  gap: 0.125rem;
8678
8700
  min-width: 0;
8679
8701
  }
@@ -8694,6 +8716,37 @@
8694
8716
  text-overflow: ellipsis;
8695
8717
  }
8696
8718
 
8719
+ .command-palette-search-action {
8720
+ flex-shrink: 0;
8721
+ border-top: 1px solid var(--color-border);
8722
+ padding-block: 0.625rem;
8723
+ background-color: color-mix(
8724
+ in srgb,
8725
+ var(--color-muted) 32%,
8726
+ var(--compose-paper-bg)
8727
+ );
8728
+ }
8729
+
8730
+ .command-palette-search-action:hover,
8731
+ .command-palette-search-action.command-palette-result-selected {
8732
+ background-color: var(--color-accent);
8733
+ }
8734
+
8735
+ .command-palette-result-shortcut {
8736
+ flex-shrink: 0;
8737
+ margin-left: auto;
8738
+ border: 1px solid var(--color-border);
8739
+ border-radius: 0.25rem;
8740
+ padding: 0.125rem 0.375rem;
8741
+ background-color: var(--compose-paper-bg);
8742
+ color: var(--color-muted-foreground);
8743
+ font-family: inherit;
8744
+ font-size: 0.6875rem;
8745
+ font-weight: 500;
8746
+ line-height: 1.25rem;
8747
+ white-space: nowrap;
8748
+ }
8749
+
8697
8750
  .command-palette-empty {
8698
8751
  padding: 1.5rem 1rem;
8699
8752
  text-align: center;
@@ -9799,6 +9852,7 @@
9799
9852
  vertical-align: super;
9800
9853
  color: var(--site-accent);
9801
9854
  white-space: nowrap;
9855
+ cursor: pointer;
9802
9856
  }
9803
9857
 
9804
9858
  .compose-tiptap-body
@@ -9834,13 +9888,22 @@
9834
9888
 
9835
9889
  .compose-tiptap-body
9836
9890
  .tiptap
9837
- > :not(.tiptap-footnote-definition)
9838
- + .tiptap-footnote-definition {
9891
+ > :not(.tiptap-footnote-definition, .ProseMirror-gapcursor)
9892
+ ~ .tiptap-footnote-definition {
9839
9893
  margin-top: 2rem;
9840
9894
  padding-top: 1rem;
9841
9895
  border-top: 1px solid var(--site-divider);
9842
9896
  }
9843
9897
 
9898
+ .compose-tiptap-body
9899
+ .tiptap
9900
+ > .tiptap-footnote-definition
9901
+ ~ .tiptap-footnote-definition {
9902
+ margin-top: 1rem;
9903
+ padding-top: 0;
9904
+ border-top: 0;
9905
+ }
9906
+
9844
9907
  .compose-tiptap-body .tiptap .tiptap-footnote-definition > :first-child {
9845
9908
  margin-top: 0;
9846
9909
  }
@@ -12158,3 +12221,28 @@
12158
12221
  );
12159
12222
  color: color-mix(in srgb, var(--site-text-secondary) 80%, transparent);
12160
12223
  }
12224
+
12225
+ /*
12226
+ * Compose reply context renders cloned feed/detail post HTML. Keep this
12227
+ * outside @layer components so it can override the unlayered global post-body
12228
+ * width rules that intentionally make normal desktop post text use the 55%
12229
+ * reading column.
12230
+ */
12231
+ .compose-reply-context-body
12232
+ :is(
12233
+ .post-detail-title,
12234
+ .post-detail-body,
12235
+ .post-body-summary,
12236
+ .feed-note-title,
12237
+ .feed-link-title,
12238
+ .feed-quote,
12239
+ .feed-quote-attribution,
12240
+ .feed-quote-commentary,
12241
+ .feed-link-domain,
12242
+ .feed-continue-link,
12243
+ [data-post-body].prose,
12244
+ [data-post-meta]
12245
+ ) {
12246
+ width: 100%;
12247
+ max-width: none;
12248
+ }
@@ -352,10 +352,15 @@ describe("timeline cards", () => {
352
352
  new URL("../../../styles/ui.css", import.meta.url),
353
353
  "utf8",
354
354
  );
355
-
356
- expect(uiCss).toMatch(
357
- /\.compose-reply-context-body\s*:is\([\s\S]*\[data-post-body\]\.prose[\s\S]*\)\s*\{[\s\S]*width:\s*100%;[\s\S]*max-width:\s*none;/,
355
+ const unlayeredCssStart = uiCss.indexOf(" * Home description");
356
+ const resetRule = uiCss.match(
357
+ /\.compose-reply-context-body\s*:is\([^{}]*\[data-post-body\]\.prose[^{}]*\)\s*\{[^{}]*width:\s*100%;[^{}]*max-width:\s*none;[^{}]*\}/,
358
358
  );
359
+
360
+ expect(unlayeredCssStart).toBeGreaterThan(0);
361
+ expect(resetRule?.index).toBeGreaterThan(unlayeredCssStart);
362
+ expect(resetRule?.[0]).toContain("width: 100%;");
363
+ expect(resetRule?.[0]).toContain("max-width: none;");
359
364
  });
360
365
 
361
366
  it("keeps link and quote attachments hidden in compact mode", () => {
@@ -434,7 +434,7 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
434
434
  responsive header layout before external CSS/JS loads */}
435
435
  <style
436
436
  dangerouslySetInnerHTML={{
437
- __html: `.site-header-search-link,.site-header-hamburger,.site-header-more-responsive-only,.site-header-more-link-responsive,.site-header-more-divider-responsive{display:none!important}@media(max-width:1200px){.site-header-search-form{display:none!important}.site-header-search-link{display:inline-flex!important}}@media(max-width:960px){.site-header-link-collapse-lg{display:none!important}.site-header-more-responsive-only.site-header-more-tier-lg{display:inline-flex!important}.site-header-more-link-show-lg{display:flex!important}.site-header-more-divider-responsive{display:block!important}}@media(max-width:780px){.site-header-link-collapse-md{display:none!important}.site-header-more-responsive-only.site-header-more-tier-md{display:inline-flex!important}.site-header-more-link-show-md{display:flex!important}}@media(max-width:580px){.site-header-link-collapse-sm{display:none!important}.site-header-more-responsive-only.site-header-more-tier-sm{display:inline-flex!important}.site-header-more-link-show-sm{display:flex!important}}@media(max-width:480px){.site-header-nav,.site-header-more{display:none!important}.site-header-search-slot{display:flex!important}.site-header-hamburger{display:flex!important}.site-header-right{margin-left:.35rem}}`,
437
+ __html: `.site-header-search-link,.site-header-hamburger,.site-header-more-responsive-only,.site-header-more-link-responsive,.site-header-more-divider-responsive{display:none!important}@media(max-width:1200px){.site-header-search-form{display:none!important}.site-header-search-link{display:inline-flex!important}}@media(max-width:960px){.site-header-link-collapse-lg{display:none!important}.site-header-more-responsive-only.site-header-more-tier-lg{display:inline-flex!important}.site-header-more-link-show-lg{display:flex!important}.site-header-more-divider-show-lg{display:block!important}}@media(max-width:780px){.site-header-link-collapse-md{display:none!important}.site-header-more-responsive-only.site-header-more-tier-md{display:inline-flex!important}.site-header-more-link-show-md{display:flex!important}.site-header-more-divider-show-md{display:block!important}}@media(max-width:580px){.site-header-link-collapse-sm{display:none!important}.site-header-more-responsive-only.site-header-more-tier-sm{display:inline-flex!important}.site-header-more-link-show-sm{display:flex!important}.site-header-more-divider-show-sm{display:block!important}}@media(max-width:480px){.site-header-nav,.site-header-more{display:none!important}.site-header-search-slot{display:flex!important}.site-header-hamburger{display:flex!important}.site-header-right{margin-left:.35rem}}`,
438
438
  }}
439
439
  />
440
440
  {themeStyle && (
@@ -134,6 +134,15 @@ function moreLinkRevealTier(idx: number): string {
134
134
  return "site-header-more-link-show-lg";
135
135
  }
136
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
+
137
146
  export const SiteHeader: FC<SiteHeaderProps> = ({
138
147
  siteName,
139
148
  links,
@@ -176,22 +185,17 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
176
185
  (l) => l.placement === "header" || !l.placement,
177
186
  );
178
187
  const moreLinks = linksWithLabels.filter((l) => l.placement === "more");
179
- const hasResponsiveOverflow = headerLinks.length > 2;
188
+ const responsiveOverflowTier = getResponsiveOverflowTier(headerLinks.length);
189
+ const hasResponsiveOverflow = responsiveOverflowTier !== null;
180
190
  const hasSupplementalMoreLinks = moreLinks.length > 0;
181
191
  const showMoreMenu = hasResponsiveOverflow || hasSupplementalMoreLinks;
182
192
 
183
193
  // Decide the widest breakpoint at which the "More" button must appear
184
194
  // when there are no supplemental links.
185
- let responsiveTierClass = "";
186
- if (hasResponsiveOverflow && !hasSupplementalMoreLinks) {
187
- if (headerLinks.length >= 5) {
188
- responsiveTierClass = "site-header-more-tier-lg";
189
- } else if (headerLinks.length === 4) {
190
- responsiveTierClass = "site-header-more-tier-md";
191
- } else {
192
- responsiveTierClass = "site-header-more-tier-sm";
193
- }
194
- }
195
+ const responsiveTierClass =
196
+ responsiveOverflowTier && !hasSupplementalMoreLinks
197
+ ? `site-header-more-tier-${responsiveOverflowTier}`
198
+ : "";
195
199
  const moreMenuClass = [
196
200
  "site-header-more",
197
201
  hasResponsiveOverflow && !hasSupplementalMoreLinks
@@ -297,8 +301,10 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
297
301
  </a>
298
302
  );
299
303
  })}
300
- {hasResponsiveOverflow && hasSupplementalMoreLinks && (
301
- <div class="site-header-more-divider site-header-more-divider-responsive" />
304
+ {responsiveOverflowTier && hasSupplementalMoreLinks && (
305
+ <div
306
+ class={`site-header-more-divider site-header-more-divider-responsive site-header-more-divider-show-${responsiveOverflowTier}`}
307
+ />
302
308
  )}
303
309
  {moreLinks.map((link) => (
304
310
  <a
@@ -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-HnzmsaqU.js";
3
- import "./export-Dfbq9aot.js";
4
- import "./env-OHRKGcMj.js";
5
- import "./github-sync--PSoE-Ne.js";
6
- export { createApp };