@writedocs/generator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +17 -0
  3. package/astro.config.mjs +419 -0
  4. package/bin/writedocs.js +73 -0
  5. package/package.json +79 -0
  6. package/src/assets/wd_watermark.png +0 -0
  7. package/src/assets/wd_watermark_dark.png +0 -0
  8. package/src/cli/build-auth.js +53 -0
  9. package/src/cli/build.js +40 -0
  10. package/src/cli/dev.js +12 -0
  11. package/src/cli/generate-api-pages.js +359 -0
  12. package/src/cli/init.js +81 -0
  13. package/src/cli/preflight.js +40 -0
  14. package/src/cli/run-astro.js +57 -0
  15. package/src/cli/run-pagefind.js +66 -0
  16. package/src/cli/write-redirects-file.js +80 -0
  17. package/src/components/Accordion.astro +164 -0
  18. package/src/components/AccordionGroup.astro +40 -0
  19. package/src/components/ApiLangSelect.astro +168 -0
  20. package/src/components/ApiPlayground.astro +281 -0
  21. package/src/components/ApiReferencePanel.astro +1754 -0
  22. package/src/components/ApiSchemaField.astro +54 -0
  23. package/src/components/AppIcon.astro +32 -0
  24. package/src/components/Badge.astro +128 -0
  25. package/src/components/Callout.astro +168 -0
  26. package/src/components/Card.astro +136 -0
  27. package/src/components/CardGroup.astro +20 -0
  28. package/src/components/CodeGroup.astro +184 -0
  29. package/src/components/CopyPageMenu.astro +246 -0
  30. package/src/components/Danger.astro +12 -0
  31. package/src/components/Expandable.astro +126 -0
  32. package/src/components/Frame.astro +102 -0
  33. package/src/components/Hint.astro +99 -0
  34. package/src/components/Icon.astro +70 -0
  35. package/src/components/Image.astro +147 -0
  36. package/src/components/Info.astro +12 -0
  37. package/src/components/Note.astro +12 -0
  38. package/src/components/Parameter.astro +119 -0
  39. package/src/components/RequestExample.astro +33 -0
  40. package/src/components/ResponseExample.astro +19 -0
  41. package/src/components/Searchbar.astro +117 -0
  42. package/src/components/Step.astro +10 -0
  43. package/src/components/Steps.astro +32 -0
  44. package/src/components/Tab.astro +9 -0
  45. package/src/components/Tabs.astro +52 -0
  46. package/src/components/Tip.astro +12 -0
  47. package/src/components/Video.astro +135 -0
  48. package/src/components/Warning.astro +12 -0
  49. package/src/components/index.ts +48 -0
  50. package/src/content.config.ts +223 -0
  51. package/src/layout/BaseLayout.astro +750 -0
  52. package/src/layout/components/AnalyticsScripts.astro +77 -0
  53. package/src/layout/components/AskAiWidget.astro +37 -0
  54. package/src/layout/components/Breadcrumbs.astro +97 -0
  55. package/src/layout/components/ImageZoom.astro +19 -0
  56. package/src/layout/components/MobileMenu.astro +200 -0
  57. package/src/layout/components/NavTree.astro +351 -0
  58. package/src/layout/components/SearchModal.astro +42 -0
  59. package/src/layout/components/Sidebar.astro +122 -0
  60. package/src/layout/components/SiteFooter.astro +85 -0
  61. package/src/layout/components/TableOfContents.astro +117 -0
  62. package/src/layout/components/TopBar.astro +311 -0
  63. package/src/layout/styles/banner.css +44 -0
  64. package/src/layout/styles/base.css +234 -0
  65. package/src/layout/styles/dropdown.css +133 -0
  66. package/src/layout/styles/footer.css +108 -0
  67. package/src/layout/styles/image-zoom.css +50 -0
  68. package/src/layout/styles/mobile-menu.css +258 -0
  69. package/src/layout/styles/search-modal.css +122 -0
  70. package/src/layout/styles/topbar.css +437 -0
  71. package/src/lib/config.ts +2131 -0
  72. package/src/lib/mdx-auto-hydrate.js +70 -0
  73. package/src/lib/mdx-inject-builtins.js +87 -0
  74. package/src/lib/mdx-substitute-variables.js +66 -0
  75. package/src/lib/mdx-title-anchor-ids.js +84 -0
  76. package/src/lib/mermaid-rehype.js +72 -0
  77. package/src/lib/openapi-render.ts +479 -0
  78. package/src/lib/shiki-code-block.js +102 -0
  79. package/src/lib/shiki-copy-button.js +45 -0
  80. package/src/lib/styles-asset-integration.js +210 -0
  81. package/src/lib/writedocs-temp-dir.js +93 -0
  82. package/src/pages/404.astro +62 -0
  83. package/src/pages/[...slug].astro +1270 -0
  84. package/src/pages/[...slug].md.ts +78 -0
  85. package/src/pages/llms-full.txt.ts +71 -0
  86. package/src/pages/llms.txt.ts +141 -0
  87. package/src/scripts/banner.ts +20 -0
  88. package/src/scripts/dropdowns.ts +61 -0
  89. package/src/scripts/image-zoom.ts +66 -0
  90. package/src/scripts/mobile-menu.ts +55 -0
  91. package/src/scripts/search.ts +155 -0
  92. package/src/scripts/sidebar-scroll.ts +65 -0
  93. package/src/scripts/theme-toggle.ts +35 -0
  94. package/src/scripts/topbar-offset.ts +141 -0
  95. package/src/styles/global.css +18 -0
@@ -0,0 +1,65 @@
1
+ // ClientRouter swaps <body> (and everything in it) on every navigation -
2
+ // [...slug].astro re-renders Sidebar.astro/NavTree.astro fresh per page,
3
+ // since the active-link highlight and each collapsible group's open/closed
4
+ // state are computed server-side from the *new* URL (see NavTree.astro's
5
+ // own comment on `open = isActive || navTreeContainsSlug(...)`). That's
6
+ // exactly the behavior a reader wants for highlighting/auto-expanding the
7
+ // right branch - but it also means .wd-sidebar is a brand-new DOM node on
8
+ // every navigation, with its own independent scroll position (it's
9
+ // `overflow-y: auto`, not the window) starting back at 0, same as a full
10
+ // page reload would. For a page nested deep in a long nav tree, that reads
11
+ // as the sidebar "jumping back to the top" on every click - disorienting,
12
+ // since the reader loses their place in the tree they were navigating.
13
+ // This module carries the old sidebar's scroll offset across the swap and
14
+ // re-applies it to the new one, without touching anything about how the
15
+ // highlight/expand state itself is computed (that server-rendered-per-page
16
+ // behavior stays exactly as it is - only the scroll position is preserved).
17
+ //
18
+ // A module-scoped variable (not sessionStorage) is enough: this script's
19
+ // own module graph is loaded once and never re-evaluated by a ClientRouter
20
+ // swap (only <body>'s content is replaced), so the value naturally
21
+ // survives from one navigation to the next without needing to serialize it
22
+ // anywhere. It deliberately does NOT survive a real full-page reload
23
+ // (Cmd+R/F5) - resetting to the top on an actual reload is ordinary,
24
+ // expected browser behavior, not something this needs to fight.
25
+ let savedScrollTop: number | null = null;
26
+
27
+ /** The sidebar rendered inline in the page (inside `.wd-sidebar-col`, for
28
+ * desktop widths) and the identical copy reused inside MobileMenu.astro's
29
+ * own drawer both carry the same `.wd-sidebar` class and both always
30
+ * exist in the DOM at once (see MobileMenu.astro's own comment) - a media
31
+ * query just hides whichever one isn't the current layout's. Picking
32
+ * "whichever one actually has a real layout box" is enough to find the
33
+ * one the reader could actually see and scroll, without needing to ask
34
+ * which layout mode is active. Returns null on a page with no sidebar at
35
+ * all (`mode: custom`/`blank`, see BaseLayout.astro) - both copies are
36
+ * simply absent there. */
37
+ function visibleSidebar(): HTMLElement | null {
38
+ const candidates = document.querySelectorAll<HTMLElement>('.wd-sidebar');
39
+ for (const el of candidates) {
40
+ if (el.offsetWidth > 0 || el.offsetHeight > 0) return el;
41
+ }
42
+ return null;
43
+ }
44
+
45
+ /** Reads the current scroll offset right before ClientRouter tears down
46
+ * the old page's DOM - `astro:before-swap` is the last moment the old
47
+ * `.wd-sidebar` is still attached and its `scrollTop` still means
48
+ * anything. */
49
+ export function saveSidebarScroll() {
50
+ const sidebar = visibleSidebar();
51
+ savedScrollTop = sidebar ? sidebar.scrollTop : null;
52
+ }
53
+
54
+ /** Re-applies the saved offset onto the new page's own `.wd-sidebar`.
55
+ * Bound to `astro:after-swap` (fires synchronously right after the swap,
56
+ * before the browser's next paint - see BaseLayout.astro's own
57
+ * `wdApplyStoredPreferences` for the identical timing rationale) rather
58
+ * than the later `astro:page-load`, so the sidebar never visibly flashes
59
+ * at the top before jumping to the preserved position. */
60
+ export function restoreSidebarScroll() {
61
+ if (savedScrollTop === null) return;
62
+ const sidebar = visibleSidebar();
63
+ if (sidebar) sidebar.scrollTop = savedScrollTop;
64
+ savedScrollTop = null;
65
+ }
@@ -0,0 +1,35 @@
1
+ // Wires up every `.wd-theme-toggle` button on the page - not just the
2
+ // first. The real topbar (TopBar.astro) and the mobile drawer's own header
3
+ // (MobileMenu.astro) each render their own copy of this button, so both
4
+ // stay in sync for free: [data-theme] on <html>, not per-button JS state,
5
+ // is what actually drives which icon each button shows - they just need
6
+ // the same click handler, independently guarded (per button, via
7
+ // dataset.wdInit) so a re-run (e.g. astro:page-load on a client-side nav,
8
+ // or simply being imported and called by more than one component) never
9
+ // double-binds any single button.
10
+ export function initThemeToggle(root: ParentNode) {
11
+ const btns = root.querySelectorAll<HTMLButtonElement>('.wd-theme-toggle');
12
+ btns.forEach((btn) => {
13
+ if (btn.dataset.wdInit) return;
14
+ btn.dataset.wdInit = 'true';
15
+ btn.addEventListener('click', () => {
16
+ const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
17
+ document.documentElement.dataset.theme = next;
18
+ try {
19
+ localStorage.setItem('wd-theme', next);
20
+ } catch (e) {
21
+ // localStorage unavailable (private browsing, etc.) - theme still
22
+ // applies for the current page, just won't persist.
23
+ }
24
+ // Mermaid diagrams are rendered to static SVG once (baked with
25
+ // whichever theme was active at render time), unlike Shiki code
26
+ // blocks which swap colors live via CSS custom properties - so a
27
+ // theme change needs to trigger an actual re-render, not just a CSS
28
+ // update. [...slug].astro's initMermaid() (article content, not this
29
+ // file) listens for this rather than living here itself, since the
30
+ // diagrams themselves are article content and this file has no
31
+ // reason to know mermaid exists.
32
+ document.dispatchEvent(new CustomEvent('wd:theme-change', { detail: { theme: next } }));
33
+ });
34
+ });
35
+ }
@@ -0,0 +1,141 @@
1
+ // The sticky topbar's actual rendered height varies - one row vs two (the
2
+ // tabs/global-dropdowns row only exists when there's something to put in
3
+ // it - see TopBar.astro's own `showSecondRow`), and `topbar.links`
4
+ // wrapping onto its own row below ~860px (topbar.css). Every place that
5
+ // needs to offset content below this sticky chrome - heading
6
+ // `scroll-margin-top` ([...slug].astro), and the sidebar/TOC/API-
7
+ // reference-panel's own sticky `top`/`max-height` (Sidebar.astro,
8
+ // TableOfContents.astro, ApiReferencePanel.astro) - used to hardcode a
9
+ // single `5rem` guess that only ever matched the simplest case (one
10
+ // topbar row): silently wrong - a heading landing partly under the
11
+ // topbar on an anchor jump, or a sliver of dead space/slight overlap on
12
+ // the sticky columns - the moment a site's topbar grew a second row.
13
+ // This measures the real thing instead and exposes it as
14
+ // `--wd-topbar-offset` on <html>, which every one of those hardcoded
15
+ // values now reads from via `var(--wd-topbar-offset, 5rem)` - `5rem`
16
+ // stays as that var()'s own fallback, for the brief instant before this
17
+ // script's first measurement runs.
18
+ //
19
+ // writedocs.json's dismissible `banner` (BaseLayout.astro, above the topbar)
20
+ // isn't `position: sticky` itself - it scrolls away normally, unlike
21
+ // `.wd-topbar` (`position: sticky; top: 0`) right below it. Measuring
22
+ // just the topbar's own height used to be enough on the (wrong)
23
+ // assumption that nothing reads this offset until scroll has already
24
+ // carried the banner out of view - true for the heading
25
+ // `scroll-margin-top` case (a scroll-into-view only settles once
26
+ // something has actually been scrolled), but not for the sidebar/TOC/
27
+ // API-reference-panel's sticky `max-height` (Sidebar.astro,
28
+ // TableOfContents.astro, ApiReferencePanel.astro), which reads this
29
+ // offset immediately on page load, banner and all - a tall enough column
30
+ // there would overflow the viewport by exactly the banner's own height
31
+ // until the reader scrolled past it. Reading the topbar's own *bottom*
32
+ // edge (`getBoundingClientRect().bottom`) instead of its height fixes
33
+ // this for free, with no separate banner-specific case to maintain: while
34
+ // the banner is still in normal flow above the not-yet-stuck topbar, that
35
+ // bottom edge already equals banner height + topbar height; once scroll
36
+ // has carried the banner away and the topbar is genuinely stuck at
37
+ // `top: 0`, it settles back to exactly the topbar's own height - the
38
+ // same value this used to hardcode from `.height` alone. The only added
39
+ // cost is that this now needs to be re-measured on scroll too, not just
40
+ // resize (below), since it changes continuously while the banner is
41
+ // still scrolling past.
42
+
43
+ function applyOffset(topbar: HTMLElement) {
44
+ let bottom = topbar.getBoundingClientRect().bottom;
45
+ // .wd-shell (BaseLayout.astro) carries its own padding-top - breathing
46
+ // room between the topbar and the three columns below it (base.css).
47
+ // The sidebar/TOC/API-reference-panel's sticky top/max-height (the
48
+ // main reason this function exists - see the file-level comment) needs
49
+ // this folded in too: those columns' *natural*, not-yet-stuck flow
50
+ // position starts at the bottom of that padding, not at the topbar's
51
+ // own bottom edge alone, so a max-height computed from the smaller
52
+ // value left them tall enough to overshoot the viewport by exactly
53
+ // this padding - same shape of bug as the banner one above, just a
54
+ // fixed ~12px instead of a scrolling one. Folding it in here (instead
55
+ // of, say, only in Sidebar/TableOfContents/ApiReferencePanel's own
56
+ // CSS) keeps every reader of this one variable consistent, at the cost
57
+ // of very slightly over-padding the scroll-margin-top use case above -
58
+ // an extra dozen pixels of clearance above a jumped-to heading is
59
+ // unnoticeable; overflowing the viewport is not.
60
+ const shell = document.querySelector<HTMLElement>('.wd-shell');
61
+ if (shell) {
62
+ const shellPaddingTop = parseFloat(getComputedStyle(shell).paddingTop);
63
+ if (!Number.isNaN(shellPaddingTop)) bottom += shellPaddingTop;
64
+ }
65
+ // A zero (or negative, impossible but defensive) reading means the
66
+ // topbar hasn't actually been laid out yet - leave the CSS var()
67
+ // fallback in place rather than pin a bogus 0px offset that would
68
+ // itself need correcting a moment later.
69
+ if (bottom > 0) {
70
+ document.documentElement.style.setProperty('--wd-topbar-offset', `${bottom}px`);
71
+ }
72
+ }
73
+
74
+ /** Corrects the browser's own initial scroll-to-fragment jump. The
75
+ * browser scrolls a same-page #hash target into view as part of initial
76
+ * navigation - which can happen before this deferred module script ever
77
+ * runs, using whatever `scroll-margin-top` is in the CSS at that moment
78
+ * (the `5rem` fallback, not yet the real measured value once a site's
79
+ * topbar is taller than that). Re-running `scrollIntoView()` here -
80
+ * after `--wd-topbar-offset` is already set correctly - repeats that
81
+ * same browser-native, scroll-margin-aware scroll with the right value,
82
+ * once. `behavior: 'auto'` (this codebase never sets a global
83
+ * `scroll-behavior: smooth` that would turn this into a visible
84
+ * animation) - this corrects a wrong landing spot, it isn't itself a
85
+ * user-facing scroll action. */
86
+ function correctInitialHashScroll() {
87
+ if (!location.hash) return;
88
+ let target: HTMLElement | null;
89
+ try {
90
+ target = document.getElementById(decodeURIComponent(location.hash.slice(1)));
91
+ } catch {
92
+ return; // malformed %-escape in the hash - nothing sane to scroll to
93
+ }
94
+ target?.scrollIntoView({ block: 'start', behavior: 'auto' });
95
+ }
96
+
97
+ // Module-scoped rather than per-call: only one topbar is ever live at a
98
+ // time, so each astro:page-load re-run should replace the previous
99
+ // ResizeObserver (and scroll listener, below) entirely, not accumulate a
100
+ // new one on top of the last - the dataset.wdInit-guard pattern this
101
+ // codebase uses elsewhere doesn't apply here, since that pattern is for
102
+ // re-binding listeners onto elements that might persist across a re-run,
103
+ // and the topbar itself never does (ClientRouter swaps <body> - see
104
+ // BaseLayout's own comment on swapRootAttributes()). The scroll listener
105
+ // is bound to `window`, which *does* persist across that swap unlike the
106
+ // topbar element the ResizeObserver watches - left unremoved, every
107
+ // navigation would add one more, all still firing on every future scroll.
108
+ let observer: ResizeObserver | null = null;
109
+ let scrollHandler: (() => void) | null = null;
110
+
111
+ export function initTopbarOffset() {
112
+ observer?.disconnect();
113
+ if (scrollHandler) window.removeEventListener('scroll', scrollHandler);
114
+
115
+ const topbar = document.querySelector<HTMLElement>('.wd-topbar');
116
+ if (!topbar) return; // mode: blank pages render no topbar at all
117
+
118
+ applyOffset(topbar);
119
+ correctInitialHashScroll();
120
+
121
+ // Font loading or topbar.links wrapping on window resize can both
122
+ // change the topbar's real height after this first measurement.
123
+ observer = new ResizeObserver(() => applyOffset(topbar));
124
+ observer.observe(topbar);
125
+
126
+ // Keeps the offset correct while a banner (if any) scrolls past above
127
+ // the not-yet-stuck topbar (see applyOffset's own comment on reading
128
+ // .bottom instead of .height) - rAF-throttled to one measurement per
129
+ // frame rather than one per scroll event, same as any other scroll-
130
+ // driven layout read.
131
+ let ticking = false;
132
+ scrollHandler = () => {
133
+ if (ticking) return;
134
+ ticking = true;
135
+ requestAnimationFrame(() => {
136
+ applyOffset(topbar);
137
+ ticking = false;
138
+ });
139
+ };
140
+ window.addEventListener('scroll', scrollHandler, { passive: true });
141
+ }
@@ -0,0 +1,18 @@
1
+ /* Wires Tailwind's utility classes into every writedocs build. Nothing in
2
+ writedocs' own components (BaseLayout, NavTree, Sidebar, Card, etc.)
3
+ uses Tailwind - they keep their existing scoped <style> blocks built
4
+ on the --wd-* CSS variables, since those already support per-site
5
+ writedocs.json theming (colors, dark mode) at render time. This import
6
+ just makes Tailwind's utilities available for custom/future content,
7
+ e.g. hand-authored MDX.
8
+
9
+ Deliberately importing theme + utilities only, NOT the full
10
+ "tailwindcss" entrypoint - that also pulls in Tailwind's preflight
11
+ (a browser-default CSS reset), which sets h1-h6's font-size/
12
+ font-weight to `inherit`. MDX content headings here rely on the
13
+ browser's own default heading styles (nothing in this codebase sets
14
+ them explicitly), so preflight was silently collapsing every
15
+ heading down to plain paragraph text. Skipping it keeps utility
16
+ classes available with zero effect on existing unstyled elements. */
17
+ @import "tailwindcss/theme.css";
18
+ @import "tailwindcss/utilities.css";