@jfrader/storybook-docs 0.4.3 → 0.5.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.
@@ -0,0 +1,15 @@
1
+ interface HelperExpansion {
2
+ inlined: string[];
3
+ remaining: {
4
+ name: string;
5
+ reason: string;
6
+ }[];
7
+ }
8
+ export declare function extractStorySourceDetails(source: string): {
9
+ sources: Record<string, string>;
10
+ helpers: Record<string, HelperExpansion>;
11
+ };
12
+ export declare function extractStorySources(source: string): Record<string, string>;
13
+ export declare function storySourcesIndexCode(slugs: Iterable<string>): string;
14
+ export declare function storySourcesCode(sources: Record<string, string>): string;
15
+ export {};
@@ -40,10 +40,11 @@ export declare function getStoryControls(meta: StoryShape, story: StoryShape, co
40
40
  controls: StoryControl[];
41
41
  unavailable: string | null;
42
42
  };
43
- export declare function ControlledStoryFrame({ module, story, componentName, componentDefaults }: {
43
+ export declare function ControlledStoryFrame({ module, story, componentName, componentDefaults, renderSource }: {
44
44
  module: Record<string, unknown>;
45
45
  story: string;
46
46
  componentName: string;
47
47
  /** Defaults read from the component source, shown until the reader changes them. */
48
48
  componentDefaults?: Record<string, unknown>;
49
+ renderSource?: string;
49
50
  }): import("react").JSX.Element;
package/dist/css.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module '*.css';
package/dist/index.css CHANGED
@@ -114,11 +114,8 @@
114
114
  display: block;
115
115
  grid-column: 3;
116
116
  grid-row: 2;
117
- position: sticky;
118
- top: var(--sd-header-height, 56px);
119
117
  align-self: start;
120
- max-height: calc(100dvh - var(--sd-header-height, 56px));
121
- overflow-y: auto;
118
+ transform: translateY(var(--sd-rail-follow, 0px));
122
119
  padding: 34px 18px 40px 6px;
123
120
  font-size: 12.5px;
124
121
  }
@@ -173,13 +170,12 @@
173
170
  margin: 0;
174
171
  padding: 0 20px 18px;
175
172
  min-width: 0;
176
- border: 0;
177
173
  background: none;
178
174
  color: var(--sd-text-muted);
179
175
  font-size: 13px;
180
176
  }
181
- .sd-controls legend {
182
- padding: 0;
177
+ .sd-controls-label {
178
+ margin: 16px 0 10px;
183
179
  font-size: 11px;
184
180
  font-weight: 600;
185
181
  letter-spacing: 0.06em;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /// <reference path="./virtual.d.ts" />
2
+ /// <reference path="./css.d.ts" />
2
3
  export { start } from './start.js';
3
4
  export { ComponentDemo, type ComponentDemoProps } from './component-demo.js';
4
5
  export type { StorybookDocsConfig, CatalogEntry, ThemeRole } from './config.js';
package/dist/index.js CHANGED
@@ -5,12 +5,12 @@ import { createRoot } from "react-dom/client";
5
5
  import { site as site3, themeCss, theme as theme2 } from "virtual:storybook-docs/site";
6
6
 
7
7
  // src/app.tsx
8
- import { useEffect as useEffect7, useRef as useRef5, useState as useState8 } from "react";
9
- import { catalog as catalog3, groups as groups2, stats as stats2 } from "virtual:storybook-docs/catalog";
8
+ import { useEffect as useEffect8, useRef as useRef5, useState as useState9 } from "react";
9
+ import { catalog as catalog3, groups as groups2 } from "virtual:storybook-docs/catalog";
10
10
  import { pages, site as site2, theme } from "virtual:storybook-docs/site";
11
11
 
12
12
  // src/pages/component-page.tsx
13
- import { useMemo as useMemo3 } from "react";
13
+ import { useEffect as useEffect6, useMemo as useMemo3, useState as useState7 } from "react";
14
14
  import { storySource as storySource2 } from "virtual:storybook-docs/catalog";
15
15
 
16
16
  // src/guidance.tsx
@@ -374,7 +374,8 @@ function usageSnippet(componentName, args) {
374
374
  const children = typeof args.children === "string" && !/[&<>{}]/.test(args.children) ? args.children : `{${JSON.stringify(args.children)}}`;
375
375
  return `${opening}>${children}</${componentName}>`;
376
376
  }
377
- function buildUsageSnippet({ componentName, from, args, source, jsxName, component }) {
377
+ function buildUsageSnippet({ componentName, from, args, source, renderSource, jsxName, component }) {
378
+ if (!source?.trim() && renderSource?.trim()) return renderSource;
378
379
  const rootName = componentName.split(".")[0];
379
380
  const identity = component;
380
381
  const tag = jsxName ?? (typeof identity?.displayName === "string" && identity.displayName.startsWith(`${rootName}.`) ? identity.displayName : componentName);
@@ -531,6 +532,39 @@ function storyLink(location, story) {
531
532
  return `${base}${hashPath(location.hash)}?${new URLSearchParams({ [STORY_PARAM]: story })}`;
532
533
  }
533
534
  var slugify = (text) => text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
535
+ function useRailFollow(ref, ready = true) {
536
+ useEffect3(() => {
537
+ const element = ref.current;
538
+ if (!element) return;
539
+ let frame = 0;
540
+ let applied = 0;
541
+ const limit = () => window.innerHeight - (Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sd-header-height")) || 56) - 20;
542
+ const apply = () => {
543
+ frame = 0;
544
+ const bottomInDocument = element.getBoundingClientRect().bottom + window.scrollY - applied;
545
+ const crossing = bottomInDocument - limit();
546
+ const next = Math.max(0, window.scrollY - crossing);
547
+ if (next !== applied) {
548
+ applied = next;
549
+ element.style.setProperty("--sd-rail-follow", `${next}px`);
550
+ }
551
+ };
552
+ const schedule = () => {
553
+ if (!frame) frame = requestAnimationFrame(apply);
554
+ };
555
+ apply();
556
+ window.addEventListener("scroll", schedule, { passive: true });
557
+ window.addEventListener("resize", schedule);
558
+ const observer = new ResizeObserver(schedule);
559
+ observer.observe(element);
560
+ return () => {
561
+ window.removeEventListener("scroll", schedule);
562
+ window.removeEventListener("resize", schedule);
563
+ observer.disconnect();
564
+ if (frame) cancelAnimationFrame(frame);
565
+ };
566
+ }, [ready, ref]);
567
+ }
534
568
  function scanPage(root, pageKey) {
535
569
  const used = /* @__PURE__ */ new Set();
536
570
  const unique = (prefix, text) => {
@@ -542,9 +576,12 @@ function scanPage(root, pageKey) {
542
576
  };
543
577
  const sections = [];
544
578
  const frames = [];
545
- const subHeadings = [...root.querySelectorAll("h3")].filter((heading) => heading.textContent);
579
+ const subHeadings = [...root.querySelectorAll("h3")].filter(
580
+ (heading) => heading.textContent && !heading.closest(".story-lane") && !heading.closest(".frame")
581
+ );
546
582
  root.querySelectorAll("h2").forEach((heading) => {
547
583
  if (!heading.textContent) return;
584
+ if (heading.closest(".story-lane") || heading.closest(".frame")) return;
548
585
  heading.id ||= unique("section", heading.textContent);
549
586
  heading.dataset.toc = "";
550
587
  const section = heading.parentElement?.tagName === "SECTION" ? heading.parentElement : null;
@@ -570,6 +607,13 @@ function scanPage(root, pageKey) {
570
607
  });
571
608
  sections.push({ id: heading.id, title: heading.textContent, element: section ?? heading, heading, stories, subs });
572
609
  });
610
+ const counts = /* @__PURE__ */ new Map();
611
+ for (const item of frames) counts.set(item.name, (counts.get(item.name) ?? 0) + 1);
612
+ for (const item of frames) {
613
+ if ((counts.get(item.name) ?? 0) > 1 && item.story && item.story !== item.name) {
614
+ item.name = `${item.name} \xB7 ${item.story}`;
615
+ }
616
+ }
573
617
  return { pageKey, sections, frames };
574
618
  }
575
619
  var signature = (scan) => `${scan.pageKey}|${scan.sections.map((s) => `${s.id}(${s.subs.map((sub) => sub.id).join("+")}):${s.stories.map((st) => `${st.id}:${st.name}`).join(",")}`).join(";")}`;
@@ -657,10 +701,12 @@ function PageRail({ pageKey }) {
657
701
  window.setTimeout(() => match.element.classList.remove("toc-flash"), FLASH_MS);
658
702
  setTarget(null);
659
703
  }, [target, scan, pageKey]);
704
+ const railRef = useRef2(null);
705
+ useRailFollow(railRef, scan !== null);
660
706
  const active = useActiveIds(scan);
661
707
  const go = useCallback((element) => scrollToElement(element), []);
662
708
  if (!scan) return null;
663
- return /* @__PURE__ */ jsx5(Fragment, { children: scan.sections.length > 0 ? /* @__PURE__ */ jsxs3("aside", { className: "toc", "aria-label": "On this page", children: [
709
+ return /* @__PURE__ */ jsx5(Fragment, { children: scan.sections.length > 0 ? /* @__PURE__ */ jsxs3("aside", { className: "toc", "aria-label": "On this page", ref: railRef, children: [
664
710
  /* @__PURE__ */ jsx5("div", { className: "toc-title", children: "On this page" }),
665
711
  /* @__PURE__ */ jsx5("ul", { children: scan.sections.map((section) => /* @__PURE__ */ jsxs3("li", { children: [
666
712
  /* @__PURE__ */ jsx5(
@@ -799,6 +845,7 @@ function ComponentDemo({
799
845
  children
800
846
  }) {
801
847
  const [codeOpen, setCodeOpen] = useState4(false);
848
+ const controlsLabelId = useId();
802
849
  const canvasRef = useRef3(null);
803
850
  const [scrollable, setScrollable] = useState4(false);
804
851
  useEffect4(() => {
@@ -844,8 +891,8 @@ function ComponentDemo({
844
891
  skipped.join(", "),
845
892
  ". See it in Storybook."
846
893
  ] }) : null,
847
- unavailable ? /* @__PURE__ */ jsx6("p", { className: "sd-controls-note", children: unavailable }) : /* @__PURE__ */ jsxs4("fieldset", { className: "sd-controls", children: [
848
- /* @__PURE__ */ jsxs4("legend", { children: [
894
+ unavailable ? /* @__PURE__ */ jsx6("p", { className: "sd-controls-note", children: unavailable }) : /* @__PURE__ */ jsxs4("div", { className: "sd-controls", role: "group", "aria-labelledby": controlsLabelId, children: [
895
+ /* @__PURE__ */ jsxs4("p", { className: "sd-controls-label", id: controlsLabelId, children: [
849
896
  "Controls for ",
850
897
  storyName
851
898
  ] }),
@@ -934,7 +981,7 @@ function getStoryControls(meta, story, componentDefaults = {}) {
934
981
  unavailable: controls.length === 0 ? "No controls." : null
935
982
  };
936
983
  }
937
- function ControlledStoryFrame({ module, story, componentName, componentDefaults }) {
984
+ function ControlledStoryFrame({ module, story, componentName, componentDefaults, renderSource }) {
938
985
  const meta = module.default ?? {};
939
986
  const definition = module[story] ?? {};
940
987
  const { args: defaults, controls, unavailable } = getStoryControls(meta, definition, componentDefaults);
@@ -950,12 +997,13 @@ function ControlledStoryFrame({ module, story, componentName, componentDefaults
950
997
  const storyName = definition.name ?? story;
951
998
  const skipped = ["loaders", "beforeEach"].filter((key) => meta[key] || definition[key]);
952
999
  if (definition.parameters?.viewport ?? meta.parameters?.viewport) skipped.push("viewport");
953
- const codeLanguage = docsOverride?.source?.language ?? docsMeta?.source?.language ?? "tsx";
1000
+ const codeLanguage = authoredCode?.trim() ? docsOverride?.source?.language ?? docsMeta?.source?.language ?? "tsx" : "tsx";
954
1001
  const snippet = buildUsageSnippet({
955
1002
  componentName,
956
1003
  from: storySource,
957
1004
  args: { ...meta.args, ...definition.args, ...edits },
958
1005
  source: authoredCode,
1006
+ renderSource,
959
1007
  // A story name is not a component name: only a part-qualified name (the rare `Dialog.Content`
960
1008
  // story) is worth using as the JSX tag, otherwise the component's own name is the honest one.
961
1009
  jsxName: storyName.includes(".") ? storyName : void 0,
@@ -1003,6 +1051,7 @@ function useProps(slug) {
1003
1051
  }
1004
1052
 
1005
1053
  // src/pages/component-page.tsx
1054
+ import { loadStorySources } from "virtual:storybook-docs/story-sources";
1006
1055
  import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1007
1056
  function formatValue(value) {
1008
1057
  if (value === void 0) return "\u2014";
@@ -1129,6 +1178,19 @@ function PropsTable({
1129
1178
  }
1130
1179
  function ComponentPage({ entry }) {
1131
1180
  const { module, error } = useStoryModule(entry);
1181
+ const [storySources, setStorySources] = useState7(null);
1182
+ useEffect6(() => {
1183
+ let active = true;
1184
+ loadStorySources(entry.slug).then(({ sources: sources2 }) => {
1185
+ if (active) setStorySources({ slug: entry.slug, sources: sources2 });
1186
+ }).catch(() => {
1187
+ if (active) setStorySources(null);
1188
+ });
1189
+ return () => {
1190
+ active = false;
1191
+ };
1192
+ }, [entry.slug]);
1193
+ const sources = storySources?.slug === entry.slug ? storySources.sources : {};
1132
1194
  const props = useProps(entry.slug);
1133
1195
  const meta = module?.default ?? {};
1134
1196
  const argTypes = meta.argTypes ?? {};
@@ -1200,7 +1262,7 @@ function ComponentPage({ entry }) {
1200
1262
  /* @__PURE__ */ jsx8(Skeleton, { height: 150 })
1201
1263
  ] }) : null,
1202
1264
  module ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
1203
- example ? /* @__PURE__ */ jsx8(ControlledStoryFrame, { module, story: example, componentName: entry.name, componentDefaults }, `${entry.slug}:${example}`) : null,
1265
+ example ? /* @__PURE__ */ jsx8(ControlledStoryFrame, { module, story: example, componentName: entry.name, componentDefaults, renderSource: sources[example] }, `${entry.slug}:${example}`) : null,
1204
1266
  entry.hasComponent ? /* @__PURE__ */ jsx8("h2", { children: "Usage" }) : null,
1205
1267
  snippet ? /* @__PURE__ */ jsx8(CodeBlock, { code: snippet, language: codeLanguage }) : entry.hasComponent ? /* @__PURE__ */ jsx8("p", { children: "No usage code is authored for this component." }) : null,
1206
1268
  usage ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
@@ -1210,7 +1272,7 @@ function ComponentPage({ entry }) {
1210
1272
  /* @__PURE__ */ jsx8(Guidance, { slug: entry.slug }),
1211
1273
  sections.map((section) => /* @__PURE__ */ jsxs5("section", { children: [
1212
1274
  /* @__PURE__ */ jsx8("h2", { children: section.title }),
1213
- section.stories.map((story) => /* @__PURE__ */ jsx8(ControlledStoryFrame, { module, story, componentName: entry.name, componentDefaults }, `${entry.slug}:${story}`))
1275
+ section.stories.map((story) => /* @__PURE__ */ jsx8(ControlledStoryFrame, { module, story, componentName: entry.name, componentDefaults, renderSource: sources[story] }, `${entry.slug}:${story}`))
1214
1276
  ] }, section.title))
1215
1277
  ] }) : null,
1216
1278
  /* @__PURE__ */ jsx8("h2", { children: "Props" }),
@@ -1531,7 +1593,7 @@ function ReleasesPage() {
1531
1593
  }
1532
1594
 
1533
1595
  // src/search.tsx
1534
- import { useEffect as useEffect6, useMemo as useMemo4, useRef as useRef4, useState as useState7 } from "react";
1596
+ import { useEffect as useEffect7, useMemo as useMemo4, useRef as useRef4, useState as useState8 } from "react";
1535
1597
  import { catalog as catalog2 } from "virtual:storybook-docs/catalog";
1536
1598
 
1537
1599
  // src/search-match.ts
@@ -1582,19 +1644,19 @@ function renderHighlighted(text, query) {
1582
1644
  );
1583
1645
  }
1584
1646
  function SearchBox() {
1585
- const [query, setQuery] = useState7("");
1586
- const [open, setOpen] = useState7(false);
1587
- const [active, setActive] = useState7(0);
1647
+ const [query, setQuery] = useState8("");
1648
+ const [open, setOpen] = useState8(false);
1649
+ const [active, setActive] = useState8(0);
1588
1650
  const inputRef = useRef4(null);
1589
1651
  const listId = "storybook-docs-search-results";
1590
1652
  const optionId = (key) => `${listId}-${key}`;
1591
1653
  const results = useMemo4(() => {
1592
1654
  return searchCatalog(catalog2, query);
1593
1655
  }, [query]);
1594
- useEffect6(() => {
1656
+ useEffect7(() => {
1595
1657
  setActive(0);
1596
1658
  }, [query]);
1597
- useEffect6(() => {
1659
+ useEffect7(() => {
1598
1660
  const onGlobalKeyDown = (event) => {
1599
1661
  if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
1600
1662
  event.preventDefault();
@@ -1617,7 +1679,7 @@ function SearchBox() {
1617
1679
  const showDropdown = open && (hasResults || hasQuery);
1618
1680
  const activeRow = hasResults && results[active] ? results[active] : void 0;
1619
1681
  const activeOptionId = activeRow ? optionId(rowKey(activeRow)) : void 0;
1620
- useEffect6(() => {
1682
+ useEffect7(() => {
1621
1683
  if (showDropdown && activeOptionId) {
1622
1684
  const el = document.getElementById(activeOptionId);
1623
1685
  el?.scrollIntoView({ block: "nearest" });
@@ -1753,12 +1815,14 @@ function parseRoute(hash) {
1753
1815
  return { kind: "overview" };
1754
1816
  }
1755
1817
  function DocsApp() {
1756
- const [route, setRoute] = useState8(() => parseRoute(window.location.hash));
1757
- const [mode, setMode] = useState8(theme.values?.[0] ?? "dark");
1758
- const [navOpen, setNavOpen] = useState8(false);
1818
+ const [route, setRoute] = useState9(() => parseRoute(window.location.hash));
1819
+ const [mode, setMode] = useState9(theme.values?.[0] ?? "dark");
1820
+ const [navOpen, setNavOpen] = useState9(false);
1821
+ const navRef = useRef5(null);
1822
+ useRailFollow(navRef);
1759
1823
  const headerRef = useRef5(null);
1760
1824
  const mainRef = useRef5(null);
1761
- useEffect7(() => {
1825
+ useEffect8(() => {
1762
1826
  const header = headerRef.current;
1763
1827
  if (!header) return;
1764
1828
  const root = document.documentElement;
@@ -1773,7 +1837,7 @@ function DocsApp() {
1773
1837
  else root.style.removeProperty("--sd-header-height");
1774
1838
  };
1775
1839
  }, []);
1776
- useEffect7(() => {
1840
+ useEffect8(() => {
1777
1841
  const onHash = () => {
1778
1842
  setRoute(parseRoute(window.location.hash));
1779
1843
  setNavOpen(false);
@@ -1783,7 +1847,7 @@ function DocsApp() {
1783
1847
  window.addEventListener("hashchange", onHash);
1784
1848
  return () => window.removeEventListener("hashchange", onHash);
1785
1849
  }, []);
1786
- useEffect7(() => {
1850
+ useEffect8(() => {
1787
1851
  document.documentElement.setAttribute(theme.attribute ?? "data-theme", mode);
1788
1852
  }, [mode]);
1789
1853
  const [darkMode, lightMode] = theme.values ?? ["dark", "light"];
@@ -1845,7 +1909,7 @@ function DocsApp() {
1845
1909
  /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", children: "\u2197" })
1846
1910
  ] }, link.href))
1847
1911
  ] }),
1848
- /* @__PURE__ */ jsxs13("nav", { id: "docs-nav", className: navOpen ? "nav open" : "nav", "aria-label": "Documentation", children: [
1912
+ /* @__PURE__ */ jsxs13("nav", { id: "docs-nav", className: navOpen ? "nav open" : "nav", "aria-label": "Documentation", ref: navRef, children: [
1849
1913
  /* @__PURE__ */ jsx16("a", { href: routeHref("/"), className: route.kind === "overview" ? "on" : void 0, "aria-current": route.kind === "overview" ? "page" : void 0, children: "Overview" }),
1850
1914
  releases2.changelog ? /* @__PURE__ */ jsxs13(
1851
1915
  "a",
@@ -1889,14 +1953,7 @@ function DocsApp() {
1889
1953
  },
1890
1954
  entry.slug
1891
1955
  ))
1892
- ] }, group)),
1893
- /* @__PURE__ */ jsxs13("div", { className: "nav-note", children: [
1894
- "navigation, examples and API tables generated from ",
1895
- stats2.components,
1896
- " story files \xB7 ",
1897
- stats2.stories,
1898
- " stories"
1899
- ] })
1956
+ ] }, group))
1900
1957
  ] }),
1901
1958
  /* @__PURE__ */ jsx16("main", { id: "main", className: "main", tabIndex: -1, ref: mainRef, children: route.kind === "component" ? /* @__PURE__ */ jsx16(ComponentPage, { entry: catalog3.find((entry) => entry.slug === route.slug) }) : route.kind === "color" ? /* @__PURE__ */ jsx16(ColorPage, {}) : route.kind === "typography" ? /* @__PURE__ */ jsx16(TypePage, {}) : route.kind === "spacing" ? /* @__PURE__ */ jsx16(SpacingPage, {}) : route.kind === "effects" ? /* @__PURE__ */ jsx16(EffectsPage, {}) : route.kind === "releases" ? /* @__PURE__ */ jsx16(ReleasesPage, {}) : /* @__PURE__ */ jsx16(OverviewPage, {}) }),
1902
1959
  route.kind !== "overview" ? /* @__PURE__ */ jsx16(PageRail, { pageKey: activeSlug ?? route.kind }) : null
@@ -42,11 +42,12 @@ export interface ExampleStory {
42
42
  export declare function selectExample(stories: ExampleStory[], parts?: string[], defaults?: Record<string, unknown> | null): string | null;
43
43
  /** Only story-authored values: never synthesize children or component defaults. */
44
44
  export declare function usageSnippet(componentName: string, args: Record<string, unknown> | undefined): string | null;
45
- export declare function buildUsageSnippet({ componentName, from, args, source, jsxName, component }: {
45
+ export declare function buildUsageSnippet({ componentName, from, args, source, renderSource, jsxName, component }: {
46
46
  componentName: string;
47
47
  from: string;
48
48
  args?: Record<string, unknown>;
49
49
  source?: string;
50
+ renderSource?: string;
50
51
  jsxName?: string;
51
52
  component?: unknown;
52
53
  }): string | null;
package/dist/styles.css CHANGED
@@ -190,6 +190,9 @@ body {
190
190
  border-right: 0;
191
191
  background: var(--sd-surface);
192
192
  padding: 14px 12px 80px;
193
+ /* Follows the document like the right rail, parking when its own end is visible. */
194
+ align-self: start;
195
+ transform: translateY(var(--sd-rail-follow, 0px));
193
196
  }
194
197
  .nav a {
195
198
  display: block;
@@ -217,12 +220,6 @@ body {
217
220
  color: var(--sd-text-muted);
218
221
  font-weight: 700;
219
222
  }
220
- .nav-note {
221
- margin: 22px 10px 0;
222
- font-size: 11.5px;
223
- line-height: 1.6;
224
- color: var(--sd-text-muted);
225
- }
226
223
 
227
224
  .main {
228
225
  padding: 48px clamp(28px, 4vw, 64px) 160px;
package/dist/toc.d.ts CHANGED
@@ -1,9 +1,18 @@
1
+ /** @jsxImportSource react */
2
+ import { type RefObject } from 'react';
1
3
  import './toc.css';
2
4
  /** The hash without its query: `#/components/button?story=Variants` → `#/components/button`. */
3
5
  export declare function hashPath(hash: string): string;
4
6
  /** The `?story=` deep-link target of a hash, if any. */
5
7
  export declare function storyFromHash(hash: string): string | null;
6
8
  export declare function storyLink(location: Location, story: string): string;
9
+ /**
10
+ * A rail travels with the document until its own end would pass the fold, then it parks there —
11
+ * the reader keeps seeing where it continues, and scrolling up lets it fall back into flow. This is
12
+ * the scroll-linked translation docs sites use, because `position: sticky` cannot move an element
13
+ * taller than the viewport.
14
+ */
15
+ export declare function useRailFollow(ref: RefObject<HTMLElement | null>, ready?: boolean): void;
7
16
  /** The "On this page" rail and the `?story=` deep-link landing. */
8
17
  export declare function PageRail({ pageKey }: {
9
18
  pageKey: string;
package/dist/virtual.d.ts CHANGED
@@ -28,6 +28,10 @@ declare module 'virtual:storybook-docs/guidance' {
28
28
  export function loadGuidance(slug: string): Promise<{ markdown: string }>;
29
29
  }
30
30
 
31
+ declare module 'virtual:storybook-docs/story-sources' {
32
+ export function loadStorySources(slug: string): Promise<{ sources: Record<string, string> }>;
33
+ }
34
+
31
35
  declare module 'virtual:storybook-docs/releases' {
32
36
  export const releases: { version: string | null; changelog: string | null };
33
37
  }
@@ -608,6 +608,222 @@ export const fireEvent = () => true;
608
608
  export const cleanup = () => {};
609
609
  `;
610
610
 
611
+ // src/build/story-sources.ts
612
+ import ts2 from "typescript";
613
+ function unwrap(node) {
614
+ while (ts2.isParenthesizedExpression(node) || ts2.isAsExpression(node) || ts2.isSatisfiesExpression(node) || ts2.isTypeAssertionExpression(node)) node = node.expression;
615
+ return node;
616
+ }
617
+ var MAX_HELPER_DEPTH = 3;
618
+ function extractStorySourceDetails(source) {
619
+ const file = ts2.createSourceFile("stories.tsx", source, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TSX);
620
+ const declarations = /* @__PURE__ */ new Map();
621
+ const exports = /* @__PURE__ */ new Map();
622
+ for (const statement of file.statements) {
623
+ const exported = ts2.canHaveModifiers(statement) && ts2.getModifiers(statement)?.some((modifier) => modifier.kind === ts2.SyntaxKind.ExportKeyword);
624
+ if (ts2.isVariableStatement(statement)) {
625
+ for (const declaration of statement.declarationList.declarations) {
626
+ if (!ts2.isIdentifier(declaration.name) || !declaration.initializer) continue;
627
+ declarations.set(declaration.name.text, declaration.initializer);
628
+ if (exported) exports.set(declaration.name.text, declaration.name.text);
629
+ }
630
+ } else if (ts2.isFunctionDeclaration(statement) && statement.name) {
631
+ declarations.set(statement.name.text, statement);
632
+ if (exported) exports.set(statement.name.text, statement.name.text);
633
+ } else if (ts2.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && ts2.isNamedExports(statement.exportClause)) {
634
+ for (const element of statement.exportClause.elements) {
635
+ exports.set(element.name.text, (element.propertyName ?? element.name).text);
636
+ }
637
+ }
638
+ }
639
+ const renderFor = (node, seen = /* @__PURE__ */ new Set()) => {
640
+ if (ts2.isFunctionDeclaration(node)) return node;
641
+ node = unwrap(node);
642
+ if (ts2.isIdentifier(node)) {
643
+ if (seen.has(node.text)) return;
644
+ seen.add(node.text);
645
+ const declaration = declarations.get(node.text);
646
+ return declaration ? renderFor(declaration, seen) : void 0;
647
+ }
648
+ if (ts2.isPropertyAccessExpression(node) && node.name.text === "render" || ts2.isElementAccessExpression(node) && ts2.isStringLiteral(node.argumentExpression) && node.argumentExpression.text === "render") return renderFor(node.expression, seen);
649
+ if (ts2.isArrowFunction(node) || ts2.isFunctionExpression(node)) return node;
650
+ if (!ts2.isObjectLiteralExpression(node)) return;
651
+ for (const property of [...node.properties].reverse()) {
652
+ if (ts2.isSpreadAssignment(property)) {
653
+ const render = renderFor(property.expression, new Set(seen));
654
+ if (render !== void 0) return render;
655
+ } else if (property.name && (ts2.isIdentifier(property.name) || ts2.isStringLiteral(property.name)) && property.name.text === "render") {
656
+ if (ts2.isMethodDeclaration(property)) return property;
657
+ if (ts2.isPropertyAssignment(property)) return renderFor(property.initializer, seen) ?? null;
658
+ if (ts2.isShorthandPropertyAssignment(property)) return renderFor(property.name, seen) ?? null;
659
+ return null;
660
+ }
661
+ }
662
+ };
663
+ const hasJsx = (node) => ts2.isJsxElement(node) || ts2.isJsxSelfClosingElement(node) || ts2.isJsxFragment(node) || (ts2.forEachChild(node, hasJsx) ?? false);
664
+ const returnsCache = /* @__PURE__ */ new Map();
665
+ const returnCounts = /* @__PURE__ */ new Map();
666
+ const returnedJsx = (render) => {
667
+ const cached = returnsCache.get(render);
668
+ if (cached) return cached;
669
+ const expressions = [];
670
+ if (!render.body) return expressions;
671
+ let returnCount = 0;
672
+ if (ts2.isBlock(render.body)) {
673
+ const visit = (node) => {
674
+ if (ts2.isFunctionLike(node)) return;
675
+ if (ts2.isReturnStatement(node)) {
676
+ returnCount += 1;
677
+ if (node.expression && hasJsx(node.expression)) expressions.push(unwrap(node.expression));
678
+ return;
679
+ }
680
+ ts2.forEachChild(node, visit);
681
+ };
682
+ ts2.forEachChild(render.body, visit);
683
+ } else {
684
+ returnCount = 1;
685
+ if (hasJsx(render.body)) expressions.push(unwrap(render.body));
686
+ }
687
+ returnsCache.set(render, expressions);
688
+ returnCounts.set(render, returnCount);
689
+ return expressions;
690
+ };
691
+ const helperFor = (name, seen = /* @__PURE__ */ new Set()) => {
692
+ if (seen.has(name)) return;
693
+ seen.add(name);
694
+ const declaration = declarations.get(name);
695
+ if (!declaration) return;
696
+ if (ts2.isFunctionDeclaration(declaration)) return declaration;
697
+ const expression = unwrap(declaration);
698
+ if (ts2.isIdentifier(expression)) return helperFor(expression.text, seen);
699
+ if (ts2.isArrowFunction(expression) || ts2.isFunctionExpression(expression)) return expression;
700
+ };
701
+ const bindingContains = (binding, name) => ts2.isIdentifier(binding) ? binding.text === name : binding.elements.some((element) => ts2.isBindingElement(element) && bindingContains(element.name, name));
702
+ const isShadowed = (expression, name) => {
703
+ for (let node = expression.parent; node; node = node.parent) {
704
+ if (ts2.isFunctionLike(node) && node.parameters.some((parameter) => bindingContains(parameter.name, name))) return true;
705
+ if (ts2.isBlock(node) && node.statements.some((statement) => ts2.isVariableStatement(statement) && statement.declarationList.declarations.some((declaration) => bindingContains(declaration.name, name)) || ts2.isFunctionDeclaration(statement) && statement.name?.text === name)) return true;
706
+ }
707
+ return false;
708
+ };
709
+ const mapCache = /* @__PURE__ */ new Map();
710
+ const hasUnknownMap = (helper, jsx) => {
711
+ const cached = mapCache.get(jsx);
712
+ if (cached !== void 0) return cached;
713
+ const bindings = /* @__PURE__ */ new Map();
714
+ for (const [name, declaration] of declarations) {
715
+ bindings.set(name, ts2.isFunctionDeclaration(declaration) ? void 0 : declaration);
716
+ }
717
+ const bind = (scope2, name, initializer) => {
718
+ if (ts2.isIdentifier(name)) scope2.set(name.text, initializer);
719
+ else for (const element of name.elements) {
720
+ if (ts2.isBindingElement(element)) bind(scope2, element.name, element.initializer);
721
+ }
722
+ };
723
+ const addScope = (node, parent) => {
724
+ const scope2 = new Map(parent);
725
+ if (ts2.isFunctionLike(node)) {
726
+ for (const parameter of node.parameters) bind(scope2, parameter.name, parameter.initializer);
727
+ }
728
+ if (ts2.isBlock(node)) {
729
+ for (const statement of node.statements) {
730
+ if (ts2.isVariableStatement(statement)) {
731
+ for (const declaration of statement.declarationList.declarations) bind(scope2, declaration.name, declaration.initializer);
732
+ } else if (ts2.isFunctionDeclaration(statement) && statement.name) scope2.set(statement.name.text, void 0);
733
+ }
734
+ }
735
+ return scope2;
736
+ };
737
+ const ancestors = [];
738
+ for (let node = jsx.parent; node !== helper; node = node.parent) ancestors.push(node);
739
+ let scope = addScope(helper, bindings);
740
+ for (const node of ancestors.reverse()) scope = addScope(node, scope);
741
+ const knownCollection = (expression, bindings2, seen = /* @__PURE__ */ new Set()) => {
742
+ expression = unwrap(expression);
743
+ if (ts2.isArrayLiteralExpression(expression)) {
744
+ return expression.elements.every((element) => !ts2.isSpreadElement(element) || knownCollection(element.expression, bindings2, new Set(seen)));
745
+ }
746
+ if (ts2.isIdentifier(expression)) {
747
+ if (seen.has(expression.text)) return false;
748
+ seen.add(expression.text);
749
+ const initializer = bindings2.get(expression.text);
750
+ return Boolean(initializer && knownCollection(initializer, bindings2, seen));
751
+ }
752
+ if (ts2.isCallExpression(expression) && ts2.isPropertyAccessExpression(expression.expression) && ["slice", "filter", "toSorted", "toReversed"].includes(expression.expression.name.text)) {
753
+ return knownCollection(expression.expression.expression, bindings2, seen);
754
+ }
755
+ return false;
756
+ };
757
+ const visit = (node, bindings2) => {
758
+ if (ts2.isFunctionLike(node) || ts2.isBlock(node)) bindings2 = addScope(node, bindings2);
759
+ if (ts2.isCallExpression(node)) {
760
+ const target = node.expression;
761
+ const map = ts2.isPropertyAccessExpression(target) && target.name.text === "map" || ts2.isElementAccessExpression(target) && ts2.isStringLiteral(target.argumentExpression) && target.argumentExpression.text === "map";
762
+ if (map && !knownCollection(target.expression, bindings2)) return true;
763
+ }
764
+ return ts2.forEachChild(node, (child) => visit(child, bindings2)) ?? false;
765
+ };
766
+ const unknown = visit(jsx, scope);
767
+ mapCache.set(jsx, unknown);
768
+ return unknown;
769
+ };
770
+ const inlineRoot = (expression, detail, trail = []) => {
771
+ const tag = ts2.isJsxElement(expression) ? expression.openingElement.tagName : ts2.isJsxSelfClosingElement(expression) ? expression.tagName : void 0;
772
+ const name = tag && ts2.isIdentifier(tag) && /^[A-Z]/.test(tag.text) ? tag.text : void 0;
773
+ const helper = name && helperFor(name);
774
+ if (!name || !declarations.has(name)) return expression.getText(file);
775
+ if (!helper) {
776
+ const reason2 = "local definition is not a direct function";
777
+ detail.remaining.push({ name, reason: reason2 });
778
+ return `// ${name} not expanded: ${reason2}.
779
+ ${expression.getText(file)}`;
780
+ }
781
+ const returned = returnedJsx(helper);
782
+ const jsx = returned[0];
783
+ const reason = isShadowed(expression, name) ? "file helper is shadowed in this scope" : trail.includes(name) ? "recursive helper cycle" : trail.length >= MAX_HELPER_DEPTH ? `helper depth limit (${MAX_HELPER_DEPTH})` : returnCounts.get(helper) !== 1 || returned.length !== 1 || !jsx || !(ts2.isJsxElement(jsx) || ts2.isJsxSelfClosingElement(jsx) || ts2.isJsxFragment(jsx)) ? "no single literal JSX return" : hasUnknownMap(helper, jsx) ? "maps over a collection without a local array definition" : void 0;
784
+ if (reason) {
785
+ detail.remaining.push({ name, reason });
786
+ return `// ${name} not expanded: ${reason}.
787
+ ${expression.getText(file)}`;
788
+ }
789
+ detail.inlined.push(name);
790
+ return `// JSX from ${name}, as authored.
791
+ ${inlineRoot(jsx, detail, [...trail, name])}`;
792
+ };
793
+ const sources = {};
794
+ const helpers = {};
795
+ for (const [name, local] of exports) {
796
+ const declaration = declarations.get(local);
797
+ const render = declaration && renderFor(declaration);
798
+ if (!render?.body) continue;
799
+ const expressions = returnedJsx(render);
800
+ if (expressions.length) {
801
+ const detail = { inlined: [], remaining: [] };
802
+ const jsx = expressions.map((expression) => inlineRoot(expression, detail)).join("\n\n");
803
+ sources[name] = expressions.length > 1 ? `// JSX return branches as authored in the story.
804
+ ${jsx}` : jsx;
805
+ if (detail.inlined.length || detail.remaining.length) helpers[name] = detail;
806
+ }
807
+ }
808
+ return { sources, helpers };
809
+ }
810
+ function extractStorySources(source) {
811
+ return extractStorySourceDetails(source).sources;
812
+ }
813
+ function storySourcesIndexCode(slugs) {
814
+ return `
815
+ export function loadStorySources(slug) {
816
+ switch (slug) {
817
+ ${[...slugs].map((slug) => ` case ${JSON.stringify(slug)}: return import(${JSON.stringify(`${VIRTUAL_PREFIX}story-sources/${slug}`)});`).join("\n")}
818
+ default: return Promise.resolve({ sources: {} });
819
+ }
820
+ }
821
+ `;
822
+ }
823
+ function storySourcesCode(sources) {
824
+ return `export const sources = ${JSON.stringify(sources)};`;
825
+ }
826
+
611
827
  // src/vite-plugin.ts
612
828
  function storybookDocs(options) {
613
829
  const { config } = options;
@@ -616,6 +832,7 @@ function storybookDocs(options) {
616
832
  catalog.files.clear();
617
833
  const engineStyles = path7.join(import.meta.dirname, "styles.css");
618
834
  const propsCache = /* @__PURE__ */ new Map();
835
+ const storySourcesCache = /* @__PURE__ */ new Map();
619
836
  const propsCollector = createPropsCollector();
620
837
  let guidanceFiles = /* @__PURE__ */ new Map();
621
838
  const loadProps = (slug) => {
@@ -636,6 +853,7 @@ export const storySource = ${JSON.stringify(config.stories.from)};`,
636
853
  "stub-storybook-test": () => STORYBOOK_TEST_STUB,
637
854
  props: () => propsIndexCode(catalog.entries.map((entry) => entry.slug)),
638
855
  guidance: () => guidanceIndexCode(guidanceFiles.keys()),
856
+ "story-sources": () => storySourcesIndexCode(catalog.files.keys()),
639
857
  releases: () => releasesCode(readReleases(config, projectRoot)),
640
858
  code: () => codeModuleCode(resolveCodeModule(config.code?.module, projectRoot))
641
859
  };
@@ -665,6 +883,7 @@ export const storySource = ${JSON.stringify(config.stories.from)};`,
665
883
  },
666
884
  buildStart() {
667
885
  propsCache.clear();
886
+ storySourcesCache.clear();
668
887
  propsCollector.clearPrograms();
669
888
  },
670
889
  closeBundle() {
@@ -682,11 +901,26 @@ export const storySource = ${JSON.stringify(config.stories.from)};`,
682
901
  const module = Object.hasOwn(modules, name) ? modules[name] : void 0;
683
902
  if (module) return module();
684
903
  if (name.startsWith("props/")) return loadProps(name.slice("props/".length));
904
+ if (name.startsWith("story-sources/")) {
905
+ const storyFile = catalog.files.get(name.slice("story-sources/".length));
906
+ if (!storyFile) return storySourcesCode({});
907
+ const file = realpathSync(storyFile);
908
+ let sources = storySourcesCache.get(file);
909
+ if (!sources) {
910
+ sources = extractStorySources(readFileSync3(file, "utf8"));
911
+ storySourcesCache.set(file, sources);
912
+ }
913
+ this.addWatchFile(file);
914
+ return storySourcesCode(sources);
915
+ }
685
916
  if (name.startsWith("guidance/")) {
686
917
  const file = guidanceFiles.get(name.slice("guidance/".length));
687
918
  return guidanceCode(file ? readFileSync3(file, "utf8") : "");
688
919
  }
689
920
  return null;
921
+ },
922
+ watchChange(file) {
923
+ storySourcesCache.delete(file);
690
924
  }
691
925
  };
692
926
  }
package/docs/RELEASING.md CHANGED
@@ -125,3 +125,29 @@ Official npm references (check them when changing authentication):
125
125
  [trusted publishers](https://docs.npmjs.com/trusted-publishers/),
126
126
  [provenance](https://docs.npmjs.com/generating-provenance-statements/),
127
127
  [granular tokens](https://docs.npmjs.com/creating-and-viewing-access-tokens/).
128
+
129
+ ## First publish of a new package name
130
+
131
+ Established procedure, after a morning spent rediscovering it:
132
+
133
+ 1. **`npm trust` works before the package exists.** `npm trust github <pkg> --repo <owner>/<repo> --file publish.yml --allow-publish`
134
+ returns **201** even for a name that has never been published, as long as you are authenticated. Do this first; it is
135
+ what lets every later `v*` tag publish itself through OIDC with provenance, no tokens.
136
+ 2. **Workflows that set `NODE_AUTH_TOKEN` cannot use trusted publishing.** An empty secret sends an
137
+ unauthenticated request and npm answers `404` on the `PUT`, which reads like a missing package. This workflow
138
+ therefore sets no token.
139
+ 3. **The first publish has to happen once from a machine** — the registry has no artifact yet:
140
+
141
+ ```bash
142
+ cd <the package checkout>
143
+ npm login --registry https://registry.npmjs.org # prints a browser URL; no OTP code needed
144
+ NPM_CONFIG_USERCONFIG=<config> npm publish # browser URL again, then "+ <pkg>@<version>"
145
+ ```
146
+
147
+ A `~/.npmrc` that maps the scope to another registry (e.g. `@scope:registry=https://npm.pkg.github.com/`) makes a
148
+ plain `npm publish` target that registry instead and fail with `403`. Point the scope at npmjs in the config used
149
+ for the publish.
150
+ 4. **Verify from the registry, not from the local output:** `curl -s https://registry.npmjs.org/<scope>%2f<pkg>`
151
+ must list the version. Until it does, nothing is published.
152
+ 5. After that, releases are tags: `git tag -a vX.Y.Z && git push origin vX.Y.Z`, and the publish workflow does the
153
+ rest. Do **not** push a tag for a version you already published by hand — npm rejects the duplicate.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jfrader/storybook-docs",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "A documentation front for any design system that has Storybook: navigation, live examples and API tables are generated from the story files themselves.",
6
6
  "exports": {
@@ -27,12 +27,6 @@
27
27
  "react-dom": "^19.3.0",
28
28
  "vite": "^8.3.4"
29
29
  },
30
- "packageManager": "pnpm@10.12.1",
31
- "pnpm": {
32
- "onlyBuiltDependencies": [
33
- "esbuild"
34
- ]
35
- },
36
30
  "dependencies": {
37
31
  "marked": "^18.1.1",
38
32
  "react-docgen-typescript": "^2.4.0",
@@ -60,15 +54,6 @@
60
54
  "props",
61
55
  "docs"
62
56
  ],
63
- "scripts": {
64
- "test": "node --test test/*.test.ts",
65
- "typecheck": "tsc --noEmit",
66
- "build": "pnpm build:package && pnpm --dir examples/jfrader-ui build",
67
- "build:package": "node scripts/build.mjs",
68
- "smoke:package": "node scripts/smoke-package.mjs",
69
- "check:frames": "node scripts/check-example-frames.mjs",
70
- "prepack": "pnpm build:package"
71
- },
72
57
  "engines": {
73
58
  "node": ">=22.12.0"
74
59
  },
@@ -81,5 +66,13 @@
81
66
  ],
82
67
  "sideEffects": [
83
68
  "**/*.css"
84
- ]
85
- }
69
+ ],
70
+ "scripts": {
71
+ "test": "node --test test/*.test.ts",
72
+ "typecheck": "tsc --noEmit",
73
+ "build": "pnpm build:package && pnpm --dir examples/jfrader-ui build",
74
+ "build:package": "node scripts/build.mjs",
75
+ "smoke:package": "node scripts/smoke-package.mjs",
76
+ "check:frames": "node scripts/check-example-frames.mjs"
77
+ }
78
+ }