@jfrader/storybook-docs 0.8.0 → 0.9.1

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.
package/dist/index.css CHANGED
@@ -115,11 +115,13 @@
115
115
  }
116
116
  .toc-track {
117
117
  display: block;
118
+ min-width: 0;
118
119
  grid-column: 3;
119
120
  grid-row: 2;
120
121
  }
121
122
  .toc {
122
123
  display: block;
124
+ min-width: 0;
123
125
  padding: 34px 18px 40px 6px;
124
126
  font-size: 12.5px;
125
127
  }
@@ -140,9 +142,12 @@
140
142
  border-left: 1px solid var(--sd-border);
141
143
  }
142
144
  .toc-link {
143
- display: flex;
144
- align-items: center;
145
+ display: block;
145
146
  width: 100%;
147
+ min-width: 0;
148
+ overflow: hidden;
149
+ white-space: nowrap;
150
+ text-overflow: ellipsis;
146
151
  min-block-size: 24px;
147
152
  padding: 4px 8px;
148
153
  font: inherit;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ 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 useEffect8, useRef as useRef5, useState as useState9 } from "react";
8
+ import { useEffect as useEffect8, useLayoutEffect as useLayoutEffect2, 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
 
@@ -363,6 +363,9 @@ function serializable(value, ancestors = /* @__PURE__ */ new Set()) {
363
363
  ancestors.delete(value);
364
364
  return result;
365
365
  }
366
+ function jsxRootTag(example) {
367
+ return example.replace(/^\s*\/\/[^\n]*(?:\n|$)/gm, "").match(/<([A-Za-z_$][\w$]*(?:\.[\w$]+)*)[\s/>]/)?.[1]?.split(".")[0];
368
+ }
366
369
  function usageSnippet(componentName, args) {
367
370
  if (!args || Object.keys(args).length === 0) return null;
368
371
  const attributes = [];
@@ -374,8 +377,8 @@ function usageSnippet(componentName, args) {
374
377
  else attributes.push(`${key}={${JSON.stringify(value)}}`);
375
378
  }
376
379
  const opening = `<${componentName}${attributes.length ? ` ${attributes.join(" ")}` : ""}`;
377
- if (args.children === void 0) return attributes.length ? `${opening} />` : null;
378
- if (!serializable(args.children) || args.children !== null && typeof args.children === "object") return null;
380
+ if (args.children === void 0 || args.children === null) return attributes.length ? `${opening} />` : null;
381
+ if (!serializable(args.children) || typeof args.children === "object") return null;
379
382
  const children = typeof args.children === "string" && !/[&<>{}]/.test(args.children) ? args.children : `{${JSON.stringify(args.children)}}`;
380
383
  return `${opening}>${children}</${componentName}>`;
381
384
  }
@@ -389,11 +392,12 @@ function buildUsageSnippet({ componentName, from, args, source, renderSource, js
389
392
  const excerpt = extracted?.startsWith(STORY_EXCERPT);
390
393
  const example = source?.trim() || (excerpt ? generated() || extracted : extracted || generated());
391
394
  if (!example) return null;
395
+ const rootTag = jsxRootTag(example);
392
396
  if (!source?.trim() && example === extracted) {
393
- const rootTag = example.replace(/^\s*\/\/[^\n]*(?:\n|$)/gm, "").trim().match(/^<([A-Za-z_$][\w$]*(?:\.[\w$]+)*)[\s/>]/)?.[1]?.split(".")[0];
394
397
  if (excerpt || rootTag !== rootName || !from.trim()) return example;
395
398
  }
396
399
  if (!from.trim()) return example;
400
+ if (rootTag !== rootName) return example;
397
401
  const imports = [...example.matchAll(/import\s*\{([^}]+)\}\s*from\s*(['"])(.*?)\2/g)];
398
402
  const imported = imports.some((match) => match[3] === from && match[1].split(",").some((name) => name.trim() === rootName));
399
403
  return imported ? example : `import { ${rootName} } from '${from}';
@@ -522,11 +526,12 @@ function LazySection({ children, placeholder = "\u2026" }) {
522
526
  }
523
527
 
524
528
  // src/toc.tsx
525
- import { useCallback, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
529
+ import { useCallback, useEffect as useEffect3, useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
526
530
 
527
531
  // src/rail.ts
528
532
  var RAIL_MEDIA = "(min-width: 1180px)";
529
533
  var RAIL_GAP = 20;
534
+ var RAIL_SETTLE_MS = 120;
530
535
  function followRail(rail, header) {
531
536
  const track = rail.parentElement;
532
537
  if (!track) return () => {
@@ -535,6 +540,8 @@ function followRail(rail, header) {
535
540
  let lastY = window.scrollY;
536
541
  let direction = "down";
537
542
  let spacer = 0;
543
+ let scrolling = false;
544
+ let settleTimer;
538
545
  const geometry = () => {
539
546
  const top = header.offsetHeight + RAIL_GAP;
540
547
  const viewportBottom = window.innerHeight - RAIL_GAP;
@@ -544,8 +551,8 @@ function followRail(rail, header) {
544
551
  viewportBottom,
545
552
  height,
546
553
  fits: top + height <= viewportBottom,
547
- trackTop: track.getBoundingClientRect().top + window.scrollY,
548
- maxOffset: Math.max(0, track.clientHeight - height)
554
+ trackTop: track.getBoundingClientRect().top + window.scrollY + RAIL_GAP,
555
+ maxOffset: Math.max(0, track.clientHeight - height - RAIL_GAP)
549
556
  };
550
557
  };
551
558
  const clear = () => {
@@ -558,17 +565,28 @@ function followRail(rail, header) {
558
565
  return;
559
566
  }
560
567
  spacer = g.fits ? 0 : Math.min(spacer, g.maxOffset);
568
+ track.style.setProperty("--sd-rail-start", `${RAIL_GAP}px`);
561
569
  track.style.setProperty("--sd-rail-offset", `${spacer}px`);
562
570
  rail.style.position = "sticky";
563
- rail.style.top = g.fits ? `${g.top}px` : direction === "down" ? `${g.viewportBottom - g.height}px` : "auto";
571
+ const parkedTop = g.fits ? g.top : g.viewportBottom - g.height;
572
+ const armedTop = Number.parseFloat(rail.style.top);
573
+ const stableTop = scrolling && Number.isFinite(armedTop) ? Math.min(armedTop, parkedTop) : parkedTop;
574
+ rail.style.top = g.fits || direction === "down" ? `${stableTop}px` : "auto";
564
575
  rail.style.bottom = g.fits || direction === "down" ? "auto" : `${window.innerHeight - g.top - g.height}px`;
565
576
  };
566
577
  const onScroll = () => {
567
578
  const y = window.scrollY;
579
+ scrolling = true;
580
+ window.clearTimeout(settleTimer);
581
+ settleTimer = window.setTimeout(() => {
582
+ scrolling = false;
583
+ arm();
584
+ }, RAIL_SETTLE_MS);
568
585
  const next = y > lastY ? "down" : y < lastY ? "up" : direction;
569
586
  if (next !== direction && media.matches) {
570
587
  const g = geometry();
571
- 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);
588
+ const armedTop = Number.parseFloat(rail.style.top);
589
+ spacer = direction === "down" ? Math.min(Math.max(spacer, lastY + (Number.isFinite(armedTop) ? armedTop : g.viewportBottom - g.height) - g.trackTop), g.maxOffset) : Math.max(Math.min(spacer, lastY + g.top - g.trackTop), 0);
572
590
  direction = next;
573
591
  arm(g);
574
592
  }
@@ -586,6 +604,7 @@ function followRail(rail, header) {
586
604
  arm();
587
605
  return () => {
588
606
  observer.disconnect();
607
+ window.clearTimeout(settleTimer);
589
608
  window.removeEventListener("scroll", onScroll);
590
609
  window.removeEventListener("resize", relayout);
591
610
  media.removeEventListener("change", relayout);
@@ -615,13 +634,13 @@ function storyLink(location, story) {
615
634
  return `${base}${hashPath(location.hash)}?${new URLSearchParams({ [STORY_PARAM]: story })}`;
616
635
  }
617
636
  var slugify = (text) => text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
618
- function useRailFollow(ref, ready = true) {
619
- useEffect3(() => {
637
+ function useRailFollow(ref, ready = true, pageKey) {
638
+ useLayoutEffect(() => {
620
639
  const element = ref.current;
621
640
  const header = element?.closest(".app")?.querySelector(".top");
622
- if (!element || !header) return;
641
+ if (!ready || !element || !header) return;
623
642
  return followRail(element, header);
624
- }, [ready, ref]);
643
+ }, [ready, ref, pageKey]);
625
644
  }
626
645
  function scanPage(root, pageKey) {
627
646
  const used = /* @__PURE__ */ new Set();
@@ -677,11 +696,14 @@ function scanPage(root, pageKey) {
677
696
  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(";")}`;
678
697
  var prefersReducedMotion = () => window.matchMedia("(prefers-reduced-motion: reduce)").matches;
679
698
  function scrollToElement(element) {
699
+ const hash = window.location.hash;
680
700
  element.scrollIntoView({ behavior: prefersReducedMotion() ? "auto" : "smooth", block: "start" });
681
701
  let ticks = SETTLE_TICKS;
682
702
  let lastY = window.scrollY;
703
+ let timer;
704
+ let cancelled = false;
683
705
  const settle = () => {
684
- if (!element.isConnected || ticks === 0) return;
706
+ if (cancelled || window.location.hash !== hash || !element.isConnected || ticks === 0) return;
685
707
  ticks -= 1;
686
708
  const moving = window.scrollY !== lastY;
687
709
  lastY = window.scrollY;
@@ -689,9 +711,13 @@ function scrollToElement(element) {
689
711
  element.scrollIntoView({ behavior: "auto", block: "start" });
690
712
  lastY = window.scrollY;
691
713
  }
692
- window.setTimeout(settle, SETTLE_TICK_MS);
714
+ timer = window.setTimeout(settle, SETTLE_TICK_MS);
715
+ };
716
+ timer = window.setTimeout(settle, SETTLE_TICK_MS);
717
+ return () => {
718
+ cancelled = true;
719
+ window.clearTimeout(timer);
693
720
  };
694
- window.setTimeout(settle, SETTLE_TICK_MS);
695
721
  }
696
722
  function useActiveIds(scan) {
697
723
  const [active, setActive] = useState3({ section: null, story: null });
@@ -721,6 +747,12 @@ function PageRail({ pageKey }) {
721
747
  pageKeyRef.current = pageKey;
722
748
  const [target, setTarget] = useState3(() => storyFromHash(window.location.hash));
723
749
  const flashed = useRef2(null);
750
+ const cancelScroll = useRef2(null);
751
+ const go = useCallback((element) => {
752
+ cancelScroll.current?.();
753
+ cancelScroll.current = scrollToElement(element);
754
+ }, []);
755
+ useEffect3(() => () => cancelScroll.current?.(), []);
724
756
  useEffect3(() => {
725
757
  const main = document.querySelector(".main");
726
758
  if (!main) return;
@@ -752,17 +784,16 @@ function PageRail({ pageKey }) {
752
784
  if (!target || !scan || scan.pageKey !== pageKey) return;
753
785
  const match = scan.frames.find((frame) => frame.story === target) ?? scan.frames.find((frame) => frame.story.toLowerCase() === target.toLowerCase()) ?? scan.frames.find((frame) => frame.name === target) ?? scan.frames.find((frame) => frame.name.toLowerCase() === target.toLowerCase());
754
786
  if (!match) return;
755
- scrollToElement(match.element);
787
+ go(match.element);
756
788
  flashed.current?.classList.remove("toc-flash");
757
789
  match.element.classList.add("toc-flash");
758
790
  flashed.current = match.element;
759
791
  window.setTimeout(() => match.element.classList.remove("toc-flash"), FLASH_MS);
760
792
  setTarget(null);
761
- }, [target, scan, pageKey]);
793
+ }, [target, scan, pageKey, go]);
762
794
  const railRef = useRef2(null);
763
795
  useRailFollow(railRef, scan !== null);
764
796
  const active = useActiveIds(scan);
765
- const go = useCallback((element) => scrollToElement(element), []);
766
797
  if (!scan) return null;
767
798
  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: [
768
799
  /* @__PURE__ */ jsx5("div", { className: "toc-title", children: "On this page" }),
@@ -772,17 +803,19 @@ function PageRail({ pageKey }) {
772
803
  {
773
804
  type: "button",
774
805
  className: "toc-link",
806
+ title: section.title,
775
807
  "aria-current": active.section === section.id ? "location" : void 0,
776
808
  onClick: () => go(section.heading),
777
809
  children: section.title
778
810
  }
779
811
  ),
780
- section.subs.length > 0 ? /* @__PURE__ */ jsx5("ul", { children: section.subs.map((sub) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5("button", { type: "button", className: "toc-link toc-sub", onClick: () => go(sub.element), children: sub.title }) }, sub.id)) }) : null,
812
+ section.subs.length > 0 ? /* @__PURE__ */ jsx5("ul", { children: section.subs.map((sub) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5("button", { type: "button", className: "toc-link toc-sub", title: sub.title, onClick: () => go(sub.element), children: sub.title }) }, sub.id)) }) : null,
781
813
  section.stories.length > 0 ? /* @__PURE__ */ jsx5("ul", { children: section.stories.map((story) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5(
782
814
  "button",
783
815
  {
784
816
  type: "button",
785
817
  className: "toc-link toc-story",
818
+ title: story.name,
786
819
  "aria-current": active.story === story.id ? "location" : void 0,
787
820
  onClick: () => go(story.element),
788
821
  children: story.name
@@ -1885,9 +1918,20 @@ function DocsApp() {
1885
1918
  const [mode, setMode] = useState9(theme.values?.[0] ?? "dark");
1886
1919
  const [navOpen, setNavOpen] = useState9(false);
1887
1920
  const navRef = useRef5(null);
1888
- useRailFollow(navRef);
1921
+ const pageKey = route.kind === "component" ? `components/${route.slug}` : route.kind;
1889
1922
  const headerRef = useRef5(null);
1890
1923
  const mainRef = useRef5(null);
1924
+ const renderedPage = useRef5(pageKey);
1925
+ useLayoutEffect2(() => {
1926
+ if (renderedPage.current !== pageKey) {
1927
+ const reset = () => window.scrollTo({ top: 0, left: 0, behavior: "instant" });
1928
+ reset();
1929
+ const frame = requestAnimationFrame(reset);
1930
+ renderedPage.current = pageKey;
1931
+ return () => cancelAnimationFrame(frame);
1932
+ }
1933
+ }, [pageKey]);
1934
+ useRailFollow(navRef, true, pageKey);
1891
1935
  useEffect8(() => {
1892
1936
  const header = headerRef.current;
1893
1937
  if (!header) return;
@@ -1907,7 +1951,6 @@ function DocsApp() {
1907
1951
  const onHash = () => {
1908
1952
  setRoute(parseRoute(window.location.hash));
1909
1953
  setNavOpen(false);
1910
- window.scrollTo(0, 0);
1911
1954
  mainRef.current?.focus({ preventScroll: true });
1912
1955
  };
1913
1956
  window.addEventListener("hashchange", onHash);
@@ -2019,8 +2062,8 @@ function DocsApp() {
2019
2062
  )) }, `${group}-${activeSlug ?? route.kind}`);
2020
2063
  })
2021
2064
  ] }) }),
2022
- /* @__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, {}) }),
2023
- route.kind !== "overview" ? /* @__PURE__ */ jsx16(PageRail, { pageKey: activeSlug ?? route.kind }) : null
2065
+ /* @__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) }, pageKey) : 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, {}) }),
2066
+ route.kind !== "overview" ? /* @__PURE__ */ jsx16(PageRail, { pageKey }, pageKey) : null
2024
2067
  ] });
2025
2068
  }
2026
2069
 
package/dist/styles.css CHANGED
@@ -194,7 +194,7 @@ body {
194
194
  .rail-track::before {
195
195
  content: '';
196
196
  display: block;
197
- height: var(--sd-rail-offset, 0px);
197
+ height: calc(var(--sd-rail-start, 0px) + var(--sd-rail-offset, 0px));
198
198
  }
199
199
  }
200
200
  .nav {
package/dist/toc.d.ts CHANGED
@@ -6,7 +6,7 @@ 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
- export declare function useRailFollow(ref: RefObject<HTMLElement | null>, ready?: boolean): void;
9
+ export declare function useRailFollow(ref: RefObject<HTMLElement | null>, ready?: boolean, pageKey?: string): void;
10
10
  /** The "On this page" rail and the `?story=` deep-link landing. */
11
11
  export declare function PageRail({ pageKey }: {
12
12
  pageKey: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jfrader/storybook-docs",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
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": {