@jfrader/storybook-docs 0.4.4 → 0.6.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/index.css CHANGED
@@ -106,16 +106,20 @@
106
106
  .toc {
107
107
  display: none;
108
108
  }
109
+ .toc-track {
110
+ display: none;
111
+ }
109
112
  @media (min-width: 1180px) {
110
113
  .app.has-toc {
111
114
  grid-template-columns: 276px minmax(0, 1fr) 224px;
112
115
  }
113
- .toc {
116
+ .toc-track {
114
117
  display: block;
115
118
  grid-column: 3;
116
119
  grid-row: 2;
117
- align-self: start;
118
- transform: translateY(var(--sd-rail-follow, 0px));
120
+ }
121
+ .toc {
122
+ display: block;
119
123
  padding: 34px 18px 40px 6px;
120
124
  font-size: 12.5px;
121
125
  }
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";
8
+ import { Children, useEffect as useEffect8, useRef as useRef5, useState as useState9 } from "react";
9
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);
@@ -510,6 +511,77 @@ function LazySection({ children, placeholder = "\u2026" }) {
510
511
 
511
512
  // src/toc.tsx
512
513
  import { useCallback, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
514
+
515
+ // src/rail.ts
516
+ var RAIL_MEDIA = "(min-width: 1180px)";
517
+ var RAIL_GAP = 20;
518
+ function followRail(rail, header) {
519
+ const track = rail.parentElement;
520
+ if (!track) return () => {
521
+ };
522
+ const media = window.matchMedia(RAIL_MEDIA);
523
+ let lastY = window.scrollY;
524
+ let direction = "down";
525
+ let spacer = 0;
526
+ const geometry = () => {
527
+ const top = header.offsetHeight + RAIL_GAP;
528
+ const viewportBottom = window.innerHeight - RAIL_GAP;
529
+ const height = rail.offsetHeight;
530
+ return {
531
+ top,
532
+ viewportBottom,
533
+ height,
534
+ fits: top + height <= viewportBottom,
535
+ trackTop: track.getBoundingClientRect().top + window.scrollY,
536
+ maxOffset: Math.max(0, track.clientHeight - height)
537
+ };
538
+ };
539
+ const clear = () => {
540
+ rail.removeAttribute("style");
541
+ track.removeAttribute("style");
542
+ };
543
+ const arm = (g = geometry()) => {
544
+ if (!media.matches) {
545
+ clear();
546
+ return;
547
+ }
548
+ spacer = g.fits ? 0 : Math.min(spacer, g.maxOffset);
549
+ track.style.setProperty("--sd-rail-offset", `${spacer}px`);
550
+ rail.style.position = "sticky";
551
+ rail.style.top = g.fits ? `${g.top}px` : direction === "down" ? `${g.viewportBottom - g.height}px` : "auto";
552
+ rail.style.bottom = g.fits || direction === "down" ? "auto" : `${window.innerHeight - g.top - g.height}px`;
553
+ };
554
+ const onScroll = () => {
555
+ const y = window.scrollY;
556
+ const next = y > lastY ? "down" : y < lastY ? "up" : direction;
557
+ if (next !== direction && media.matches) {
558
+ const g = geometry();
559
+ spacer = direction === "down" ? Math.min(Math.max(spacer, lastY + g.viewportBottom - g.height - g.trackTop), g.maxOffset) : Math.max(Math.min(spacer, lastY + g.top - g.trackTop), 0);
560
+ direction = next;
561
+ arm(g);
562
+ }
563
+ direction = next;
564
+ lastY = y;
565
+ };
566
+ const relayout = () => arm();
567
+ const observer = new ResizeObserver(relayout);
568
+ observer.observe(rail);
569
+ observer.observe(track);
570
+ observer.observe(header);
571
+ window.addEventListener("scroll", onScroll, { passive: true });
572
+ window.addEventListener("resize", relayout);
573
+ media.addEventListener("change", relayout);
574
+ arm();
575
+ return () => {
576
+ observer.disconnect();
577
+ window.removeEventListener("scroll", onScroll);
578
+ window.removeEventListener("resize", relayout);
579
+ media.removeEventListener("change", relayout);
580
+ clear();
581
+ };
582
+ }
583
+
584
+ // src/toc.tsx
513
585
  import { Fragment, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
514
586
  var STORY_PARAM = "story";
515
587
  var SCROLL_OFFSET_PX = 76;
@@ -534,34 +606,9 @@ var slugify = (text) => text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCa
534
606
  function useRailFollow(ref, ready = true) {
535
607
  useEffect3(() => {
536
608
  const element = ref.current;
537
- if (!element) return;
538
- let frame = 0;
539
- let applied = 0;
540
- const limit = () => window.innerHeight - (Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sd-header-height")) || 56) - 20;
541
- const apply = () => {
542
- frame = 0;
543
- const bottomInDocument = element.getBoundingClientRect().bottom + window.scrollY - applied;
544
- const crossing = bottomInDocument - limit();
545
- const next = Math.max(0, window.scrollY - crossing);
546
- if (next !== applied) {
547
- applied = next;
548
- element.style.setProperty("--sd-rail-follow", `${next}px`);
549
- }
550
- };
551
- const schedule = () => {
552
- if (!frame) frame = requestAnimationFrame(apply);
553
- };
554
- apply();
555
- window.addEventListener("scroll", schedule, { passive: true });
556
- window.addEventListener("resize", schedule);
557
- const observer = new ResizeObserver(schedule);
558
- observer.observe(element);
559
- return () => {
560
- window.removeEventListener("scroll", schedule);
561
- window.removeEventListener("resize", schedule);
562
- observer.disconnect();
563
- if (frame) cancelAnimationFrame(frame);
564
- };
609
+ const header = element?.closest(".app")?.querySelector(".top");
610
+ if (!element || !header) return;
611
+ return followRail(element, header);
565
612
  }, [ready, ref]);
566
613
  }
567
614
  function scanPage(root, pageKey) {
@@ -705,7 +752,7 @@ function PageRail({ pageKey }) {
705
752
  const active = useActiveIds(scan);
706
753
  const go = useCallback((element) => scrollToElement(element), []);
707
754
  if (!scan) return null;
708
- return /* @__PURE__ */ jsx5(Fragment, { children: scan.sections.length > 0 ? /* @__PURE__ */ jsxs3("aside", { className: "toc", "aria-label": "On this page", ref: railRef, children: [
755
+ return /* @__PURE__ */ jsx5(Fragment, { children: scan.sections.length > 0 ? /* @__PURE__ */ jsx5("div", { className: "rail-track toc-track", children: /* @__PURE__ */ jsxs3("aside", { className: "toc", "aria-label": "On this page", ref: railRef, children: [
709
756
  /* @__PURE__ */ jsx5("div", { className: "toc-title", children: "On this page" }),
710
757
  /* @__PURE__ */ jsx5("ul", { children: scan.sections.map((section) => /* @__PURE__ */ jsxs3("li", { children: [
711
758
  /* @__PURE__ */ jsx5(
@@ -730,7 +777,7 @@ function PageRail({ pageKey }) {
730
777
  }
731
778
  ) }, story.id)) }) : null
732
779
  ] }, section.id)) })
733
- ] }) : null });
780
+ ] }) }) : null });
734
781
  }
735
782
 
736
783
  // src/component-demo.tsx
@@ -980,7 +1027,7 @@ function getStoryControls(meta, story, componentDefaults = {}) {
980
1027
  unavailable: controls.length === 0 ? "No controls." : null
981
1028
  };
982
1029
  }
983
- function ControlledStoryFrame({ module, story, componentName, componentDefaults }) {
1030
+ function ControlledStoryFrame({ module, story, componentName, componentDefaults, renderSource }) {
984
1031
  const meta = module.default ?? {};
985
1032
  const definition = module[story] ?? {};
986
1033
  const { args: defaults, controls, unavailable } = getStoryControls(meta, definition, componentDefaults);
@@ -996,12 +1043,13 @@ function ControlledStoryFrame({ module, story, componentName, componentDefaults
996
1043
  const storyName = definition.name ?? story;
997
1044
  const skipped = ["loaders", "beforeEach"].filter((key) => meta[key] || definition[key]);
998
1045
  if (definition.parameters?.viewport ?? meta.parameters?.viewport) skipped.push("viewport");
999
- const codeLanguage = docsOverride?.source?.language ?? docsMeta?.source?.language ?? "tsx";
1046
+ const codeLanguage = authoredCode?.trim() ? docsOverride?.source?.language ?? docsMeta?.source?.language ?? "tsx" : "tsx";
1000
1047
  const snippet = buildUsageSnippet({
1001
1048
  componentName,
1002
1049
  from: storySource,
1003
1050
  args: { ...meta.args, ...definition.args, ...edits },
1004
1051
  source: authoredCode,
1052
+ renderSource,
1005
1053
  // A story name is not a component name: only a part-qualified name (the rare `Dialog.Content`
1006
1054
  // story) is worth using as the JSX tag, otherwise the component's own name is the honest one.
1007
1055
  jsxName: storyName.includes(".") ? storyName : void 0,
@@ -1049,6 +1097,7 @@ function useProps(slug) {
1049
1097
  }
1050
1098
 
1051
1099
  // src/pages/component-page.tsx
1100
+ import { loadStorySources } from "virtual:storybook-docs/story-sources";
1052
1101
  import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1053
1102
  function formatValue(value) {
1054
1103
  if (value === void 0) return "\u2014";
@@ -1175,6 +1224,19 @@ function PropsTable({
1175
1224
  }
1176
1225
  function ComponentPage({ entry }) {
1177
1226
  const { module, error } = useStoryModule(entry);
1227
+ const [storySources, setStorySources] = useState7(null);
1228
+ useEffect6(() => {
1229
+ let active = true;
1230
+ loadStorySources(entry.slug).then(({ sources: sources2 }) => {
1231
+ if (active) setStorySources({ slug: entry.slug, sources: sources2 });
1232
+ }).catch(() => {
1233
+ if (active) setStorySources(null);
1234
+ });
1235
+ return () => {
1236
+ active = false;
1237
+ };
1238
+ }, [entry.slug]);
1239
+ const sources = storySources?.slug === entry.slug ? storySources.sources : {};
1178
1240
  const props = useProps(entry.slug);
1179
1241
  const meta = module?.default ?? {};
1180
1242
  const argTypes = meta.argTypes ?? {};
@@ -1246,7 +1308,7 @@ function ComponentPage({ entry }) {
1246
1308
  /* @__PURE__ */ jsx8(Skeleton, { height: 150 })
1247
1309
  ] }) : null,
1248
1310
  module ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
1249
- example ? /* @__PURE__ */ jsx8(ControlledStoryFrame, { module, story: example, componentName: entry.name, componentDefaults }, `${entry.slug}:${example}`) : null,
1311
+ example ? /* @__PURE__ */ jsx8(ControlledStoryFrame, { module, story: example, componentName: entry.name, componentDefaults, renderSource: sources[example] }, `${entry.slug}:${example}`) : null,
1250
1312
  entry.hasComponent ? /* @__PURE__ */ jsx8("h2", { children: "Usage" }) : null,
1251
1313
  snippet ? /* @__PURE__ */ jsx8(CodeBlock, { code: snippet, language: codeLanguage }) : entry.hasComponent ? /* @__PURE__ */ jsx8("p", { children: "No usage code is authored for this component." }) : null,
1252
1314
  usage ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
@@ -1256,7 +1318,7 @@ function ComponentPage({ entry }) {
1256
1318
  /* @__PURE__ */ jsx8(Guidance, { slug: entry.slug }),
1257
1319
  sections.map((section) => /* @__PURE__ */ jsxs5("section", { children: [
1258
1320
  /* @__PURE__ */ jsx8("h2", { children: section.title }),
1259
- section.stories.map((story) => /* @__PURE__ */ jsx8(ControlledStoryFrame, { module, story, componentName: entry.name, componentDefaults }, `${entry.slug}:${story}`))
1321
+ section.stories.map((story) => /* @__PURE__ */ jsx8(ControlledStoryFrame, { module, story, componentName: entry.name, componentDefaults, renderSource: sources[story] }, `${entry.slug}:${story}`))
1260
1322
  ] }, section.title))
1261
1323
  ] }) : null,
1262
1324
  /* @__PURE__ */ jsx8("h2", { children: "Props" }),
@@ -1577,7 +1639,7 @@ function ReleasesPage() {
1577
1639
  }
1578
1640
 
1579
1641
  // src/search.tsx
1580
- import { useEffect as useEffect6, useMemo as useMemo4, useRef as useRef4, useState as useState7 } from "react";
1642
+ import { useEffect as useEffect7, useMemo as useMemo4, useRef as useRef4, useState as useState8 } from "react";
1581
1643
  import { catalog as catalog2 } from "virtual:storybook-docs/catalog";
1582
1644
 
1583
1645
  // src/search-match.ts
@@ -1628,19 +1690,19 @@ function renderHighlighted(text, query) {
1628
1690
  );
1629
1691
  }
1630
1692
  function SearchBox() {
1631
- const [query, setQuery] = useState7("");
1632
- const [open, setOpen] = useState7(false);
1633
- const [active, setActive] = useState7(0);
1693
+ const [query, setQuery] = useState8("");
1694
+ const [open, setOpen] = useState8(false);
1695
+ const [active, setActive] = useState8(0);
1634
1696
  const inputRef = useRef4(null);
1635
1697
  const listId = "storybook-docs-search-results";
1636
1698
  const optionId = (key) => `${listId}-${key}`;
1637
1699
  const results = useMemo4(() => {
1638
1700
  return searchCatalog(catalog2, query);
1639
1701
  }, [query]);
1640
- useEffect6(() => {
1702
+ useEffect7(() => {
1641
1703
  setActive(0);
1642
1704
  }, [query]);
1643
- useEffect6(() => {
1705
+ useEffect7(() => {
1644
1706
  const onGlobalKeyDown = (event) => {
1645
1707
  if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
1646
1708
  event.preventDefault();
@@ -1663,7 +1725,7 @@ function SearchBox() {
1663
1725
  const showDropdown = open && (hasResults || hasQuery);
1664
1726
  const activeRow = hasResults && results[active] ? results[active] : void 0;
1665
1727
  const activeOptionId = activeRow ? optionId(rowKey(activeRow)) : void 0;
1666
- useEffect6(() => {
1728
+ useEffect7(() => {
1667
1729
  if (showDropdown && activeOptionId) {
1668
1730
  const el = document.getElementById(activeOptionId);
1669
1731
  el?.scrollIntoView({ block: "nearest" });
@@ -1785,6 +1847,23 @@ function SearchBox() {
1785
1847
  import { releases as releases2 } from "virtual:storybook-docs/releases";
1786
1848
  import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
1787
1849
  var routeHref = (path) => `#${hrefFor(path)}`;
1850
+ var NAV_GROUP_WINDOW = 12;
1851
+ function NavGroup({ id, title, active, activeIndex = -1, children }) {
1852
+ const [open, setOpen] = useState9(false);
1853
+ const [showAll, setShowAll] = useState9(false);
1854
+ const expanded = active || open;
1855
+ const links = Children.toArray(children);
1856
+ const capped = active && links.length > NAV_GROUP_WINDOW;
1857
+ const start2 = Math.min(Math.max(activeIndex - NAV_GROUP_WINDOW / 2, 0), Math.max(0, links.length - NAV_GROUP_WINDOW));
1858
+ const visibleLinks = capped && !showAll ? links.slice(start2, start2 + NAV_GROUP_WINDOW) : links;
1859
+ return /* @__PURE__ */ jsxs13("div", { role: "group", "aria-labelledby": id, children: [
1860
+ /* @__PURE__ */ jsx16("button", { type: "button", className: "grp", id, "aria-expanded": expanded, "aria-controls": `${id}-links`, disabled: active, onClick: () => setOpen(!open), children: title }),
1861
+ /* @__PURE__ */ jsxs13("div", { id: `${id}-links`, hidden: !expanded, children: [
1862
+ /* @__PURE__ */ jsx16("div", { id: `${id}-window`, children: visibleLinks }),
1863
+ capped ? /* @__PURE__ */ jsx16("button", { type: "button", className: "nav-more", "aria-expanded": showAll, "aria-controls": `${id}-window`, onClick: () => setShowAll(!showAll), children: showAll ? "Show less" : `Show all ${links.length}` }) : null
1864
+ ] })
1865
+ ] });
1866
+ }
1788
1867
  function parseRoute(hash) {
1789
1868
  const path = hashPath(hash).replace(new RegExp(`^#?\\/?${STORYBOOK_DOCS_BASE}`), "").replace(/^\/+/, "");
1790
1869
  if (path.startsWith("components/")) {
@@ -1799,14 +1878,14 @@ function parseRoute(hash) {
1799
1878
  return { kind: "overview" };
1800
1879
  }
1801
1880
  function DocsApp() {
1802
- const [route, setRoute] = useState8(() => parseRoute(window.location.hash));
1803
- const [mode, setMode] = useState8(theme.values?.[0] ?? "dark");
1804
- const [navOpen, setNavOpen] = useState8(false);
1881
+ const [route, setRoute] = useState9(() => parseRoute(window.location.hash));
1882
+ const [mode, setMode] = useState9(theme.values?.[0] ?? "dark");
1883
+ const [navOpen, setNavOpen] = useState9(false);
1805
1884
  const navRef = useRef5(null);
1806
1885
  useRailFollow(navRef);
1807
1886
  const headerRef = useRef5(null);
1808
1887
  const mainRef = useRef5(null);
1809
- useEffect7(() => {
1888
+ useEffect8(() => {
1810
1889
  const header = headerRef.current;
1811
1890
  if (!header) return;
1812
1891
  const root = document.documentElement;
@@ -1821,7 +1900,7 @@ function DocsApp() {
1821
1900
  else root.style.removeProperty("--sd-header-height");
1822
1901
  };
1823
1902
  }, []);
1824
- useEffect7(() => {
1903
+ useEffect8(() => {
1825
1904
  const onHash = () => {
1826
1905
  setRoute(parseRoute(window.location.hash));
1827
1906
  setNavOpen(false);
@@ -1831,7 +1910,7 @@ function DocsApp() {
1831
1910
  window.addEventListener("hashchange", onHash);
1832
1911
  return () => window.removeEventListener("hashchange", onHash);
1833
1912
  }, []);
1834
- useEffect7(() => {
1913
+ useEffect8(() => {
1835
1914
  document.documentElement.setAttribute(theme.attribute ?? "data-theme", mode);
1836
1915
  }, [mode]);
1837
1916
  const [darkMode, lightMode] = theme.values ?? ["dark", "light"];
@@ -1893,7 +1972,7 @@ function DocsApp() {
1893
1972
  /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", children: "\u2197" })
1894
1973
  ] }, link.href))
1895
1974
  ] }),
1896
- /* @__PURE__ */ jsxs13("nav", { id: "docs-nav", className: navOpen ? "nav open" : "nav", "aria-label": "Documentation", ref: navRef, children: [
1975
+ /* @__PURE__ */ jsx16("div", { className: "rail-track nav-track", children: /* @__PURE__ */ jsxs13("nav", { id: "docs-nav", className: navOpen ? "nav open" : "nav", "aria-label": "Documentation", ref: navRef, children: [
1897
1976
  /* @__PURE__ */ jsx16("a", { href: routeHref("/"), className: route.kind === "overview" ? "on" : void 0, "aria-current": route.kind === "overview" ? "page" : void 0, children: "Overview" }),
1898
1977
  releases2.changelog ? /* @__PURE__ */ jsxs13(
1899
1978
  "a",
@@ -1910,22 +1989,20 @@ function DocsApp() {
1910
1989
  ]
1911
1990
  }
1912
1991
  ) : null,
1913
- foundations.length > 0 ? /* @__PURE__ */ jsxs13("div", { role: "group", "aria-labelledby": "nav-grp-foundations", children: [
1914
- /* @__PURE__ */ jsx16("div", { className: "grp", id: "nav-grp-foundations", children: "Foundations" }),
1915
- foundations.map((item) => /* @__PURE__ */ jsx16(
1916
- "a",
1917
- {
1918
- href: item.href,
1919
- className: route.kind === item.kind ? "on" : void 0,
1920
- "aria-current": route.kind === item.kind ? "page" : void 0,
1921
- children: item.label
1922
- },
1923
- item.href
1924
- ))
1925
- ] }) : null,
1926
- groups2.map((group, index) => /* @__PURE__ */ jsxs13("div", { role: "group", "aria-labelledby": `nav-grp-${index}`, children: [
1927
- /* @__PURE__ */ jsx16("div", { className: "grp", id: `nav-grp-${index}`, children: group }),
1928
- catalog3.filter((entry) => entry.group === group).map((entry) => /* @__PURE__ */ jsx16(
1992
+ foundations.length > 0 ? /* @__PURE__ */ jsx16(NavGroup, { id: "nav-grp-foundations", title: "Foundations", active: foundations.some((item) => route.kind === item.kind), children: foundations.map((item) => /* @__PURE__ */ jsx16(
1993
+ "a",
1994
+ {
1995
+ href: item.href,
1996
+ className: route.kind === item.kind ? "on" : void 0,
1997
+ "aria-current": route.kind === item.kind ? "page" : void 0,
1998
+ children: item.label
1999
+ },
2000
+ item.href
2001
+ )) }, `foundations-${activeSlug ?? route.kind}`) : null,
2002
+ groups2.map((group, index) => {
2003
+ const entries = catalog3.filter((entry) => entry.group === group);
2004
+ const activeIndex = entries.findIndex((entry) => entry.slug === activeSlug);
2005
+ return /* @__PURE__ */ jsx16(NavGroup, { id: `nav-grp-${index}`, title: group, active: activeIndex !== -1, activeIndex, children: entries.map((entry) => /* @__PURE__ */ jsx16(
1929
2006
  "a",
1930
2007
  {
1931
2008
  href: routeHref(`/components/${entry.slug}`),
@@ -1936,9 +2013,9 @@ function DocsApp() {
1936
2013
  children: entry.name
1937
2014
  },
1938
2015
  entry.slug
1939
- ))
1940
- ] }, group))
1941
- ] }),
2016
+ )) }, `${group}-${activeSlug ?? route.kind}`);
2017
+ })
2018
+ ] }) }),
1942
2019
  /* @__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, {}) }),
1943
2020
  route.kind !== "overview" ? /* @__PURE__ */ jsx16(PageRail, { pageKey: activeSlug ?? route.kind }) : null
1944
2021
  ] });
package/dist/rail.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function followRail(rail: HTMLElement, header: HTMLElement): () => void;
@@ -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
@@ -186,23 +186,44 @@ body {
186
186
  outline-offset: 2px;
187
187
  }
188
188
 
189
+ .rail-track {
190
+ overflow-anchor: none;
191
+ min-width: 0;
192
+ }
193
+ @media (min-width: 1180px) {
194
+ .rail-track::before {
195
+ content: '';
196
+ display: block;
197
+ height: var(--sd-rail-offset, 0px);
198
+ }
199
+ }
189
200
  .nav {
190
201
  border-right: 0;
191
202
  background: var(--sd-surface);
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));
203
+ padding: 14px 12px 12px;
196
204
  }
197
- .nav a {
198
- display: block;
199
- padding: 6px 10px;
205
+ .nav a,
206
+ .nav-more {
207
+ display: flex;
208
+ align-items: center;
209
+ box-sizing: border-box;
210
+ min-height: 28px;
211
+ padding: 4px 10px;
200
212
  border-radius: 8px;
201
213
  color: var(--sd-text-muted);
202
214
  font-size: 13.5px;
203
215
  text-decoration: none;
204
216
  }
205
- .nav a:hover {
217
+ .nav-more {
218
+ width: 100%;
219
+ background: none;
220
+ border: 0;
221
+ font-family: inherit;
222
+ cursor: pointer;
223
+ text-align: left;
224
+ }
225
+ .nav a:hover,
226
+ .nav-more:hover {
206
227
  background: var(--sd-surface-hover);
207
228
  color: var(--sd-text);
208
229
  }
@@ -212,13 +233,26 @@ body {
212
233
  font-weight: 600;
213
234
  }
214
235
  .grp {
215
- margin: 16px 0 4px;
236
+ margin: 3px 0 2px;
216
237
  padding: 0 10px;
217
238
  font-size: 10.5px;
218
239
  letter-spacing: 0.09em;
219
240
  text-transform: uppercase;
220
241
  color: var(--sd-text-muted);
221
242
  font-weight: 700;
243
+ display: block;
244
+ width: 100%;
245
+ text-align: left;
246
+ font-family: inherit;
247
+ background: none;
248
+ border: 0;
249
+ cursor: pointer;
250
+ min-height: 28px;
251
+ }
252
+ .grp:focus-visible,
253
+ .nav-more:focus-visible {
254
+ outline: 2px solid var(--sd-focus-ring);
255
+ outline-offset: 2px;
222
256
  }
223
257
 
224
258
  .main {
@@ -618,6 +652,9 @@ code.type {
618
652
  .nav-toggle {
619
653
  display: inline-block;
620
654
  }
655
+ .nav-track {
656
+ display: contents;
657
+ }
621
658
  .search {
622
659
  order: 10;
623
660
  flex: 1 1 100%;
package/dist/toc.d.ts CHANGED
@@ -6,12 +6,6 @@ export declare function hashPath(hash: string): string;
6
6
  /** The `?story=` deep-link target of a hash, if any. */
7
7
  export declare function storyFromHash(hash: string): string | null;
8
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
9
  export declare function useRailFollow(ref: RefObject<HTMLElement | null>, ready?: boolean): void;
16
10
  /** The "On this page" rail and the `?story=` deep-link landing. */
17
11
  export declare function PageRail({ pageKey }: {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jfrader/storybook-docs",
3
- "version": "0.4.4",
3
+ "version": "0.6.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": {