@umami/shiso 1.1.12 → 1.3.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.
@@ -3290,11 +3290,11 @@ const createLucideIcon = (iconName, iconNode) => {
3290
3290
  * This source code is licensed under the ISC license.
3291
3291
  * See the LICENSE file in the root directory of this source tree.
3292
3292
  */
3293
- const __iconNode$17 = [["path", {
3293
+ const __iconNode$18 = [["path", {
3294
3294
  d: "M20 6 9 17l-5-5",
3295
3295
  key: "1gmf2c"
3296
3296
  }]];
3297
- const Check$1 = createLucideIcon("check", __iconNode$17);
3297
+ const Check$1 = createLucideIcon("check", __iconNode$18);
3298
3298
 
3299
3299
  //#endregion
3300
3300
  //#region ../../node_modules/.pnpm/lucide-react@1.28.0_react@19.2.8/node_modules/lucide-react/dist/esm/icons/chevron-down.mjs
@@ -3304,11 +3304,11 @@ const Check$1 = createLucideIcon("check", __iconNode$17);
3304
3304
  * This source code is licensed under the ISC license.
3305
3305
  * See the LICENSE file in the root directory of this source tree.
3306
3306
  */
3307
- const __iconNode$16 = [["path", {
3307
+ const __iconNode$17 = [["path", {
3308
3308
  d: "m6 9 6 6 6-6",
3309
3309
  key: "qrunsl"
3310
3310
  }]];
3311
- const ChevronDown = createLucideIcon("chevron-down", __iconNode$16);
3311
+ const ChevronDown = createLucideIcon("chevron-down", __iconNode$17);
3312
3312
 
3313
3313
  //#endregion
3314
3314
  //#region ../../node_modules/.pnpm/lucide-react@1.28.0_react@19.2.8/node_modules/lucide-react/dist/esm/icons/chevron-right.mjs
@@ -3318,11 +3318,11 @@ const ChevronDown = createLucideIcon("chevron-down", __iconNode$16);
3318
3318
  * This source code is licensed under the ISC license.
3319
3319
  * See the LICENSE file in the root directory of this source tree.
3320
3320
  */
3321
- const __iconNode$15 = [["path", {
3321
+ const __iconNode$16 = [["path", {
3322
3322
  d: "m9 18 6-6-6-6",
3323
3323
  key: "mthhwq"
3324
3324
  }]];
3325
- const ChevronRight = createLucideIcon("chevron-right", __iconNode$15);
3325
+ const ChevronRight = createLucideIcon("chevron-right", __iconNode$16);
3326
3326
 
3327
3327
  //#endregion
3328
3328
  //#region ../../node_modules/.pnpm/lucide-react@1.28.0_react@19.2.8/node_modules/lucide-react/dist/esm/icons/chevron-up.mjs
@@ -3332,11 +3332,11 @@ const ChevronRight = createLucideIcon("chevron-right", __iconNode$15);
3332
3332
  * This source code is licensed under the ISC license.
3333
3333
  * See the LICENSE file in the root directory of this source tree.
3334
3334
  */
3335
- const __iconNode$14 = [["path", {
3335
+ const __iconNode$15 = [["path", {
3336
3336
  d: "m18 15-6-6-6 6",
3337
3337
  key: "153udz"
3338
3338
  }]];
3339
- const ChevronUp = createLucideIcon("chevron-up", __iconNode$14);
3339
+ const ChevronUp = createLucideIcon("chevron-up", __iconNode$15);
3340
3340
 
3341
3341
  //#endregion
3342
3342
  //#region ../../node_modules/.pnpm/lucide-react@1.28.0_react@19.2.8/node_modules/lucide-react/dist/esm/icons/circle-alert.mjs
@@ -3346,7 +3346,7 @@ const ChevronUp = createLucideIcon("chevron-up", __iconNode$14);
3346
3346
  * This source code is licensed under the ISC license.
3347
3347
  * See the LICENSE file in the root directory of this source tree.
3348
3348
  */
3349
- const __iconNode$13 = [
3349
+ const __iconNode$14 = [
3350
3350
  ["circle", {
3351
3351
  cx: "12",
3352
3352
  cy: "12",
@@ -3368,7 +3368,7 @@ const __iconNode$13 = [
3368
3368
  key: "4dfq90"
3369
3369
  }]
3370
3370
  ];
3371
- const CircleAlert = createLucideIcon("circle-alert", __iconNode$13);
3371
+ const CircleAlert = createLucideIcon("circle-alert", __iconNode$14);
3372
3372
 
3373
3373
  //#endregion
3374
3374
  //#region ../../node_modules/.pnpm/lucide-react@1.28.0_react@19.2.8/node_modules/lucide-react/dist/esm/icons/copy.mjs
@@ -3378,7 +3378,7 @@ const CircleAlert = createLucideIcon("circle-alert", __iconNode$13);
3378
3378
  * This source code is licensed under the ISC license.
3379
3379
  * See the LICENSE file in the root directory of this source tree.
3380
3380
  */
3381
- const __iconNode$12 = [["rect", {
3381
+ const __iconNode$13 = [["rect", {
3382
3382
  width: "14",
3383
3383
  height: "14",
3384
3384
  x: "8",
@@ -3390,7 +3390,7 @@ const __iconNode$12 = [["rect", {
3390
3390
  d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",
3391
3391
  key: "zix9uf"
3392
3392
  }]];
3393
- const Copy = createLucideIcon("copy", __iconNode$12);
3393
+ const Copy = createLucideIcon("copy", __iconNode$13);
3394
3394
 
3395
3395
  //#endregion
3396
3396
  //#region ../../node_modules/.pnpm/lucide-react@1.28.0_react@19.2.8/node_modules/lucide-react/dist/esm/icons/external-link.mjs
@@ -3400,7 +3400,7 @@ const Copy = createLucideIcon("copy", __iconNode$12);
3400
3400
  * This source code is licensed under the ISC license.
3401
3401
  * See the LICENSE file in the root directory of this source tree.
3402
3402
  */
3403
- const __iconNode$11 = [
3403
+ const __iconNode$12 = [
3404
3404
  ["path", {
3405
3405
  d: "M15 3h6v6",
3406
3406
  key: "1q9fwt"
@@ -3414,7 +3414,39 @@ const __iconNode$11 = [
3414
3414
  key: "a6xqqp"
3415
3415
  }]
3416
3416
  ];
3417
- const ExternalLink = createLucideIcon("external-link", __iconNode$11);
3417
+ const ExternalLink = createLucideIcon("external-link", __iconNode$12);
3418
+
3419
+ //#endregion
3420
+ //#region ../../node_modules/.pnpm/lucide-react@1.28.0_react@19.2.8/node_modules/lucide-react/dist/esm/icons/file-text.mjs
3421
+ /**
3422
+ * @license lucide-react v1.28.0 - ISC
3423
+ *
3424
+ * This source code is licensed under the ISC license.
3425
+ * See the LICENSE file in the root directory of this source tree.
3426
+ */
3427
+ const __iconNode$11 = [
3428
+ ["path", {
3429
+ d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",
3430
+ key: "1oefj6"
3431
+ }],
3432
+ ["path", {
3433
+ d: "M14 2v5a1 1 0 0 0 1 1h5",
3434
+ key: "wfsgrz"
3435
+ }],
3436
+ ["path", {
3437
+ d: "M10 9H8",
3438
+ key: "b1mrlr"
3439
+ }],
3440
+ ["path", {
3441
+ d: "M16 13H8",
3442
+ key: "t4e002"
3443
+ }],
3444
+ ["path", {
3445
+ d: "M16 17H8",
3446
+ key: "z1uh3a"
3447
+ }]
3448
+ ];
3449
+ const FileText = createLucideIcon("file-text", __iconNode$11);
3418
3450
 
3419
3451
  //#endregion
3420
3452
  //#region ../../node_modules/.pnpm/lucide-react@1.28.0_react@19.2.8/node_modules/lucide-react/dist/esm/icons/globe.mjs
@@ -19706,7 +19738,7 @@ DialogBackdrop.displayName = "DialogBackdrop";
19706
19738
  *
19707
19739
  * Documentation: [Base UI Dialog](https://base-ui.com/react/components/dialog)
19708
19740
  */
19709
- const DialogClose$1 = /*#__PURE__*/ React$1.forwardRef(function DialogClose(componentProps, forwardedRef) {
19741
+ const DialogClose = /*#__PURE__*/ React$1.forwardRef(function DialogClose(componentProps, forwardedRef) {
19710
19742
  const { render, className, style, disabled = false, nativeButton = true, ...elementProps } = componentProps;
19711
19743
  const store = useDialogRootContext();
19712
19744
  const open = store.useState("open");
@@ -19728,7 +19760,7 @@ const DialogClose$1 = /*#__PURE__*/ React$1.forwardRef(function DialogClose(comp
19728
19760
  ]
19729
19761
  });
19730
19762
  });
19731
- DialogClose$1.displayName = "DialogClose";
19763
+ DialogClose.displayName = "DialogClose";
19732
19764
 
19733
19765
  //#endregion
19734
19766
  //#region ../../node_modules/.pnpm/@base-ui+react@1.7.0_@types_e9c1e83f6bc6140c3efaf3427f2fbf0a/node_modules/@base-ui/react/dialog/portal/DialogPortalContext.mjs
@@ -20463,12 +20495,6 @@ function DialogPortal$1({ ...props }) {
20463
20495
  ...props
20464
20496
  });
20465
20497
  }
20466
- function DialogClose({ ...props }) {
20467
- return /* @__PURE__ */ jsx(DialogClose$1, {
20468
- "data-slot": "dialog-close",
20469
- ...props
20470
- });
20471
- }
20472
20498
  function DialogOverlay$1({ className, ...props }) {
20473
20499
  return /* @__PURE__ */ jsx(DialogBackdrop, {
20474
20500
  "data-slot": "dialog-overlay",
@@ -20481,7 +20507,7 @@ function DialogContent$1({ className, children, showCloseButton = true, overlayC
20481
20507
  "data-slot": "dialog-content",
20482
20508
  className: cn("fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className),
20483
20509
  ...props,
20484
- children: [children, showCloseButton && /* @__PURE__ */ jsxs(DialogClose$1, {
20510
+ children: [children, showCloseButton && /* @__PURE__ */ jsxs(DialogClose, {
20485
20511
  "data-slot": "dialog-close",
20486
20512
  render: /* @__PURE__ */ jsx(Button, {
20487
20513
  variant: "ghost",
@@ -20514,40 +20540,29 @@ function ZoomableImage({ noZoom = false, className, alt, src, ...props }) {
20514
20540
  });
20515
20541
  if (noZoom || !src) return image;
20516
20542
  const label = alt ? `View “${alt}” full size` : "View image full size";
20517
- return /* @__PURE__ */ jsxs(Dialog$1, { children: [
20518
- /* @__PURE__ */ jsx(DialogTrigger, {
20519
- render: /* @__PURE__ */ jsx("button", {
20520
- type: "button",
20521
- "data-slot": "zoomable-image",
20522
- className: "my-6 block w-fit max-w-full cursor-zoom-in rounded-lg text-left outline-none focus-visible:ring-3 focus-visible:ring-ring/50",
20523
- "aria-label": label
20524
- }),
20525
- children: image
20526
- }),
20527
- /* @__PURE__ */ jsxs(DialogContent$1, {
20528
- showCloseButton: false,
20529
- overlayClassName: "bg-black/80 supports-backdrop-filter:backdrop-blur-sm",
20530
- className: "flex max-h-[calc(100vh-2rem)] w-auto max-w-[calc(100vw-2rem)] items-center justify-center bg-transparent p-0 ring-0 shadow-none sm:max-w-[calc(100vw-2rem)]",
20531
- children: [/* @__PURE__ */ jsx(DialogTitle, {
20532
- className: "sr-only",
20533
- children: alt || "Full-size image"
20534
- }), /* @__PURE__ */ jsx("img", {
20535
- src,
20536
- srcSet: props.srcSet,
20537
- sizes: props.sizes,
20538
- alt: alt || "",
20539
- className: "max-h-[calc(100vh-2rem)] max-w-[calc(100vw-2rem)] object-contain"
20540
- })]
20543
+ return /* @__PURE__ */ jsxs(Dialog$1, { children: [/* @__PURE__ */ jsx(DialogTrigger, {
20544
+ render: /* @__PURE__ */ jsx("button", {
20545
+ type: "button",
20546
+ "data-slot": "zoomable-image",
20547
+ className: "my-6 block w-fit max-w-full cursor-zoom-in rounded-lg text-left outline-none focus-visible:ring-3 focus-visible:ring-ring/50",
20548
+ "aria-label": label
20541
20549
  }),
20542
- /* @__PURE__ */ jsx(DialogPortal$1, { children: /* @__PURE__ */ jsx(DialogClose, {
20543
- render: /* @__PURE__ */ jsx("button", {
20544
- type: "button",
20545
- className: "fixed top-4 right-4 z-[60] inline-flex size-9 items-center justify-center rounded-full bg-black/60 text-white outline-none backdrop-blur-sm hover:bg-black/80 focus-visible:ring-3 focus-visible:ring-white/60",
20546
- "aria-label": "Close full-size image"
20547
- }),
20548
- children: /* @__PURE__ */ jsx(X$1, { className: "size-5" })
20549
- }) })
20550
- ] });
20550
+ children: image
20551
+ }), /* @__PURE__ */ jsxs(DialogContent$1, {
20552
+ showCloseButton: false,
20553
+ overlayClassName: "bg-black/80 supports-backdrop-filter:backdrop-blur-sm",
20554
+ className: "flex max-h-[calc(100vh-2rem)] w-auto max-w-[calc(100vw-2rem)] items-center justify-center bg-transparent p-0 ring-0 shadow-none sm:max-w-[calc(100vw-2rem)]",
20555
+ children: [/* @__PURE__ */ jsx(DialogTitle, {
20556
+ className: "sr-only",
20557
+ children: alt || "Full-size image"
20558
+ }), /* @__PURE__ */ jsx("img", {
20559
+ src,
20560
+ srcSet: props.srcSet,
20561
+ sizes: props.sizes,
20562
+ alt: alt || "",
20563
+ className: "max-h-[calc(100vh-2rem)] max-w-[calc(100vw-2rem)] object-contain"
20564
+ })]
20565
+ })] });
20551
20566
  }
20552
20567
 
20553
20568
  //#endregion
@@ -22903,6 +22918,7 @@ const SHISO_THEME_LABELS = {
22903
22918
  searchUnavailable: "Search unavailable",
22904
22919
  noResults: "No results",
22905
22920
  lastUpdated: "Last updated on",
22921
+ relatedTopics: "Related topics",
22906
22922
  notFound: "Page not found",
22907
22923
  dismissBanner: "Dismiss banner",
22908
22924
  toggleTheme: "Toggle theme",
@@ -26167,7 +26183,7 @@ function TopNav({ docs, label }) {
26167
26183
  const tabs = docs.tabs.filter((tab) => !tab.hidden);
26168
26184
  if (!tabs.length) return null;
26169
26185
  const selected = docs.pages.find((item) => item.url === pathname)?.tabId || [...tabs].sort((a, b) => b.url.length - a.url.length).find((tab) => pathname === tab.url || pathname.startsWith(`${tab.url}/`))?.id || tabs[0]?.id;
26170
- const tabClass = (tab) => clsx("flex h-full items-center gap-1 whitespace-nowrap border-transparent border-b-2 font-medium", {
26186
+ const tabClass = (tab) => cn("flex h-full items-center gap-1 whitespace-nowrap border-transparent border-b-2 font-medium", {
26171
26187
  "border-b-primary text-foreground": tab.id === selected,
26172
26188
  "text-muted-foreground hover:text-foreground": tab.id !== selected
26173
26189
  });
@@ -26609,6 +26625,31 @@ function ContextualMenu({ options, labels }) {
26609
26625
 
26610
26626
  //#endregion
26611
26627
  //#region src/components/DocContent.tsx
26628
+ /**
26629
+ * Related-topics entries from frontmatter. Bare paths resolve their title from
26630
+ * the docs registry; unknown internal paths without an explicit title are
26631
+ * skipped so dead links never render.
26632
+ */
26633
+ function resolveRelated(entries) {
26634
+ if (!Array.isArray(entries)) return [];
26635
+ const links = [];
26636
+ for (const entry of entries) {
26637
+ const href = typeof entry === "string" ? entry : entry?.href;
26638
+ if (!href || typeof href !== "string") continue;
26639
+ const external = !href.startsWith("/");
26640
+ const title = (typeof entry === "object" ? entry.title : void 0) || (external ? href : getPageByPathname(href)?.label);
26641
+ if (!title) {
26642
+ if (import.meta.env.DEV) console.warn(`[shiso] Related topic "${href}" does not match a page and has no title — skipped.`);
26643
+ continue;
26644
+ }
26645
+ links.push({
26646
+ href,
26647
+ title,
26648
+ external
26649
+ });
26650
+ }
26651
+ return links;
26652
+ }
26612
26653
  function DocContent({ page, doc, site }) {
26613
26654
  const pagerPages = getScopeForPage(docsSite, page).docs.pages.filter((item) => !item.hidden);
26614
26655
  const pageIndex = pagerPages.findIndex((item) => item.slug === page.slug);
@@ -26620,6 +26661,7 @@ function DocContent({ page, doc, site }) {
26620
26661
  const lastModified = (typeof doc.frontmatter?.timestamp === "boolean" ? doc.frontmatter.timestamp : site.showTimestamp) ? getLastModified(page.filePath) : void 0;
26621
26662
  const eyebrow = site.styling.eyebrows === "breadcrumbs" ? [.../* @__PURE__ */ new Set([page.tabLabel, page.section])].filter(Boolean).join(" / ") : page.section;
26622
26663
  const contextualOptions = resolveContextualOptions(site.contextualOptions, page, site.labels);
26664
+ const related = resolveRelated(doc.frontmatter?.related);
26623
26665
  const dateFormat = new Intl.DateTimeFormat(resolveLocale(page.language, site.locale), {
26624
26666
  dateStyle: "medium",
26625
26667
  timeZone: "UTC"
@@ -26660,6 +26702,33 @@ function DocContent({ page, doc, site }) {
26660
26702
  })
26661
26703
  ]
26662
26704
  }),
26705
+ related.length > 0 && /* @__PURE__ */ jsxs("nav", {
26706
+ className: "mt-8",
26707
+ "aria-label": site.labels.relatedTopics,
26708
+ children: [/* @__PURE__ */ jsx("div", {
26709
+ className: "text-sm text-muted-foreground",
26710
+ children: site.labels.relatedTopics
26711
+ }), /* @__PURE__ */ jsx("ul", {
26712
+ className: "mt-3 flex flex-col gap-2 text-sm",
26713
+ children: related.map(({ href, title, external }) => /* @__PURE__ */ jsxs("li", {
26714
+ className: "flex items-center gap-2",
26715
+ children: [/* @__PURE__ */ jsx(FileText, {
26716
+ size: 14,
26717
+ className: "shrink-0 text-muted-foreground"
26718
+ }), external ? /* @__PURE__ */ jsx("a", {
26719
+ href,
26720
+ target: "_blank",
26721
+ rel: "noreferrer",
26722
+ className: "text-foreground hover:text-primary",
26723
+ children: title
26724
+ }) : /* @__PURE__ */ jsx(Link, {
26725
+ to: href,
26726
+ className: "text-foreground hover:text-primary",
26727
+ children: title
26728
+ })]
26729
+ }, href))
26730
+ })]
26731
+ }),
26663
26732
  /* @__PURE__ */ jsxs("div", {
26664
26733
  className: "mt-8 flex items-center justify-between",
26665
26734
  children: [/* @__PURE__ */ jsx(NavigationButton, {
@@ -26773,7 +26842,7 @@ function PageLinks({ items = [], title, navigationLabel }) {
26773
26842
  const isActive = hash === id;
26774
26843
  return /* @__PURE__ */ jsx("a", {
26775
26844
  href: `#${id}`,
26776
- className: clsx("block border-l px-3 py-1.5", {
26845
+ className: cn("block border-l px-3 py-1.5", {
26777
26846
  "border-l-primary font-medium text-primary": isActive,
26778
26847
  "border-border text-muted-foreground hover:text-foreground": !isActive
26779
26848
  }),
@@ -26828,23 +26897,23 @@ function CollapsibleGroup({ node, pathname, depth, drilldown, expandLabel, colla
26828
26897
  const chevron = /* @__PURE__ */ jsx(ChevronRight, { className: "size-3.5 shrink-0 origin-center text-muted-foreground transition-[color,transform] duration-150 group-hover/section:text-sidebar-accent-foreground group-aria-expanded/collapsible-trigger:rotate-90" });
26829
26898
  const isTopLevel = depth === 0;
26830
26899
  const rootSelected = node.root?.page.url === pathname;
26831
- const headerClass = isTopLevel ? "flex min-w-0 items-center text-sidebar-foreground" : clsx("flex items-center border-l border-l-sidebar-border text-muted-foreground hover:text-sidebar-accent-foreground", { "pl-6": depth > 1 });
26900
+ const headerClass = isTopLevel ? "flex min-w-0 items-center text-sidebar-foreground" : cn("flex items-center border-l border-l-sidebar-border text-muted-foreground hover:text-sidebar-accent-foreground", { "pl-6": depth > 1 });
26832
26901
  const fixedHeader = /* @__PURE__ */ jsx("div", {
26833
- className: clsx(headerClass, {
26902
+ className: cn(headerClass, {
26834
26903
  [selectedClass]: rootSelected && !isTopLevel,
26835
26904
  "text-sidebar-primary": rootSelected && isTopLevel
26836
26905
  }),
26837
26906
  children: node.root ? /* @__PURE__ */ jsxs(Link, {
26838
26907
  to: node.root.page.url,
26839
- className: clsx(isTopLevel ? sectionLabelClass : groupLabelClass, "flex-1"),
26908
+ className: cn(isTopLevel ? sectionLabelClass : groupLabelClass, "flex-1"),
26840
26909
  children: [resolveIcon(node.icon), node.label]
26841
26910
  }) : /* @__PURE__ */ jsxs("span", {
26842
- className: clsx(isTopLevel ? sectionLabelClass : groupLabelClass, "flex-1"),
26911
+ className: cn(isTopLevel ? sectionLabelClass : groupLabelClass, "flex-1"),
26843
26912
  children: [resolveIcon(node.icon), node.label]
26844
26913
  })
26845
26914
  });
26846
26915
  const header = !collapsible ? fixedHeader : drilldown !== false && node.root ? /* @__PURE__ */ jsxs("div", {
26847
- className: clsx(headerClass, "group/section flex items-center", {
26916
+ className: cn(headerClass, "group/section flex items-center", {
26848
26917
  [selectedClass]: rootSelected && !isTopLevel,
26849
26918
  "text-sidebar-primary": rootSelected && isTopLevel
26850
26919
  }),
@@ -26866,7 +26935,7 @@ function CollapsibleGroup({ node, pathname, depth, drilldown, expandLabel, colla
26866
26935
  render: /* @__PURE__ */ jsx(Button, {
26867
26936
  type: "button",
26868
26937
  variant: "ghost",
26869
- className: clsx(headerClass, "group/section group/collapsible-trigger flex h-auto w-full items-center justify-start gap-[0.4rem] whitespace-normal rounded-none px-0 py-0 text-left hover:bg-transparent aria-expanded:bg-transparent dark:hover:bg-transparent", {
26938
+ className: cn(headerClass, "group/section group/collapsible-trigger flex h-auto w-full items-center justify-start gap-[0.4rem] whitespace-normal rounded-none px-0 py-0 text-left hover:bg-transparent aria-expanded:bg-transparent dark:hover:bg-transparent", {
26870
26939
  "pb-2 font-bold text-sidebar-foreground": isTopLevel,
26871
26940
  "border-l border-l-sidebar-border px-3 py-2 font-medium text-muted-foreground hover:text-sidebar-accent-foreground": !isTopLevel
26872
26941
  })
@@ -26917,7 +26986,7 @@ function NavNodes({ nodes, pathname, depth, drilldown, expandLabel, collapseLabe
26917
26986
  href: node.href,
26918
26987
  target: node.target,
26919
26988
  rel: node.target === "_blank" ? "noreferrer" : void 0,
26920
- className: clsx("flex min-w-0 items-center gap-[0.4rem] border-l border-l-sidebar-border px-3 py-[0.55rem] text-muted-foreground hover:text-sidebar-accent-foreground [overflow-wrap:anywhere]", { "pl-6": depth > 1 }),
26989
+ className: cn("flex min-w-0 items-center gap-[0.4rem] border-l border-l-sidebar-border px-3 py-[0.55rem] text-muted-foreground hover:text-sidebar-accent-foreground [overflow-wrap:anywhere]", { "pl-6": depth > 1 }),
26921
26990
  children: [
26922
26991
  resolveIcon(node.icon),
26923
26992
  node.label,
@@ -26934,7 +27003,7 @@ function NavNodes({ nodes, pathname, depth, drilldown, expandLabel, collapseLabe
26934
27003
  const isSelected = url === pathname;
26935
27004
  rendered.push(/* @__PURE__ */ jsxs(Link, {
26936
27005
  to: url,
26937
- className: clsx("flex min-w-0 items-center gap-[0.4rem] border-l border-l-sidebar-border px-3 py-[0.55rem] [overflow-wrap:anywhere]", {
27006
+ className: cn("flex min-w-0 items-center gap-[0.4rem] border-l border-l-sidebar-border px-3 py-[0.55rem] [overflow-wrap:anywhere]", {
26938
27007
  "pl-6": depth > 1,
26939
27008
  [selectedClass]: isSelected,
26940
27009
  "text-muted-foreground hover:text-sidebar-accent-foreground": !isSelected
@@ -26966,7 +27035,7 @@ function SideNav({ tabs, navigation, anchors, activeTabId, isSticky, drilldown,
26966
27035
  const { pathname } = useLocation();
26967
27036
  const nodes = navigation[activeTabId] || navigation[tabs[0]?.id] || [];
26968
27037
  return /* @__PURE__ */ jsx(ScrollArea, {
26969
- className: clsx("w-full max-w-full", { "h-full": isSticky }),
27038
+ className: cn("w-full max-w-full", { "h-full": isSticky }),
26970
27039
  children: /* @__PURE__ */ jsxs("nav", {
26971
27040
  className: "flex w-full flex-col gap-6 pr-4 text-sm",
26972
27041
  "aria-label": navigationLabel,
@@ -27022,7 +27091,7 @@ function SheetContent({ className, children, side = "right", showCloseButton = t
27022
27091
  "data-side": side,
27023
27092
  className: cn("fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm", className),
27024
27093
  ...props,
27025
- children: [children, showCloseButton && /* @__PURE__ */ jsxs(DialogClose$1, {
27094
+ children: [children, showCloseButton && /* @__PURE__ */ jsxs(DialogClose, {
27026
27095
  "data-slot": "sheet-close",
27027
27096
  render: /* @__PURE__ */ jsx(Button, {
27028
27097
  variant: "ghost",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umami/shiso",
3
- "version": "1.1.12",
3
+ "version": "1.3.0",
4
4
  "description": "Open-source documentation framework for Markdown and MDX sites.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,12 +1,56 @@
1
1
  import { Link } from 'react-router';
2
2
  import { ContextualMenu } from '@/components/ContextualMenu';
3
- import { ChevronRight } from '@/components/icons';
3
+ import { ChevronRight, FileText } from '@/components/icons';
4
4
  import { getLastModified } from '@/lib/content';
5
5
  import { getScopeForPage } from '@/lib/docs-config';
6
6
  import { resolveLocale } from '@/lib/locale';
7
- import { docsSite } from '@/lib/site-config';
7
+ import { docsSite, getPageByPathname } from '@/lib/site-config';
8
8
  import { resolveContextualOptions } from '@/lib/site-model';
9
- import type { DocModule, NormalizedDocsPage, SiteModel } from '@/lib/types';
9
+ import type { DocModule, NormalizedDocsPage, RelatedEntry, SiteModel } from '@/lib/types';
10
+
11
+ interface RelatedLink {
12
+ href: string;
13
+ title: string;
14
+ external: boolean;
15
+ }
16
+
17
+ /**
18
+ * Related-topics entries from frontmatter. Bare paths resolve their title from
19
+ * the docs registry; unknown internal paths without an explicit title are
20
+ * skipped so dead links never render.
21
+ */
22
+ function resolveRelated(entries: unknown): RelatedLink[] {
23
+ if (!Array.isArray(entries)) {
24
+ return [];
25
+ }
26
+
27
+ const links: RelatedLink[] = [];
28
+
29
+ for (const entry of entries as RelatedEntry[]) {
30
+ const href = typeof entry === 'string' ? entry : entry?.href;
31
+
32
+ if (!href || typeof href !== 'string') {
33
+ continue;
34
+ }
35
+
36
+ const external = !href.startsWith('/');
37
+ const explicitTitle = typeof entry === 'object' ? entry.title : undefined;
38
+ const title = explicitTitle || (external ? href : getPageByPathname(href)?.label);
39
+
40
+ if (!title) {
41
+ if (import.meta.env.DEV) {
42
+ console.warn(
43
+ `[shiso] Related topic "${href}" does not match a page and has no title — skipped.`,
44
+ );
45
+ }
46
+ continue;
47
+ }
48
+
49
+ links.push({ href, title, external });
50
+ }
51
+
52
+ return links;
53
+ }
10
54
 
11
55
  export interface DocContentProps {
12
56
  page: NormalizedDocsPage;
@@ -34,6 +78,7 @@ export function DocContent({ page, doc, site }: DocContentProps) {
34
78
  ? [...new Set([page.tabLabel, page.section])].filter(Boolean).join(' / ')
35
79
  : page.section;
36
80
  const contextualOptions = resolveContextualOptions(site.contextualOptions, page, site.labels);
81
+ const related = resolveRelated(doc.frontmatter?.related);
37
82
  // Dates follow the page's language when it is a valid locale code.
38
83
  const dateFormat = new Intl.DateTimeFormat(resolveLocale(page.language, site.locale), {
39
84
  dateStyle: 'medium',
@@ -61,6 +106,32 @@ export function DocContent({ page, doc, site }: DocContentProps) {
61
106
  <time dateTime={lastModified}>{dateFormat.format(new Date(lastModified))}</time>
62
107
  </div>
63
108
  )}
109
+ {related.length > 0 && (
110
+ <nav className="mt-8" aria-label={site.labels.relatedTopics}>
111
+ <div className="text-sm text-muted-foreground">{site.labels.relatedTopics}</div>
112
+ <ul className="mt-3 flex flex-col gap-2 text-sm">
113
+ {related.map(({ href, title, external }) => (
114
+ <li key={href} className="flex items-center gap-2">
115
+ <FileText size={14} className="shrink-0 text-muted-foreground" />
116
+ {external ? (
117
+ <a
118
+ href={href}
119
+ target="_blank"
120
+ rel="noreferrer"
121
+ className="text-foreground hover:text-primary"
122
+ >
123
+ {title}
124
+ </a>
125
+ ) : (
126
+ <Link to={href} className="text-foreground hover:text-primary">
127
+ {title}
128
+ </Link>
129
+ )}
130
+ </li>
131
+ ))}
132
+ </ul>
133
+ </nav>
134
+ )}
64
135
  <div className="mt-8 flex items-center justify-between">
65
136
  <NavigationButton {...prev} isPrev />
66
137
  <NavigationButton {...next} />
@@ -1,4 +1,4 @@
1
- import classNames from 'clsx';
1
+ import { cn } from '@/lib/utils';
2
2
  import { useEffect, useState } from 'react';
3
3
  import type { TocEntry } from '@/lib/types';
4
4
 
@@ -55,7 +55,7 @@ export function PageLinks({ items = [], title, navigationLabel }: PageLinksProps
55
55
  <a
56
56
  key={id}
57
57
  href={`#${id}`}
58
- className={classNames('block border-l px-3 py-1.5', {
58
+ className={cn('block border-l px-3 py-1.5', {
59
59
  'border-l-primary font-medium text-primary': isActive,
60
60
  'border-border text-muted-foreground hover:text-foreground': !isActive,
61
61
  })}
@@ -1,4 +1,4 @@
1
- import classNames from 'clsx';
1
+ import { cn } from '@/lib/utils';
2
2
  import { type ReactNode, useEffect, useState } from 'react';
3
3
  import { Link, useLocation, useNavigate } from 'react-router';
4
4
  import { resolveIcon } from '@/components/docs/utils';
@@ -96,14 +96,14 @@ function CollapsibleGroup({
96
96
  const rootSelected = node.root?.page.url === pathname;
97
97
  const headerClass = isTopLevel
98
98
  ? 'flex min-w-0 items-center text-sidebar-foreground'
99
- : classNames(
99
+ : cn(
100
100
  'flex items-center border-l border-l-sidebar-border text-muted-foreground hover:text-sidebar-accent-foreground',
101
101
  { 'pl-6': depth > 1 },
102
102
  );
103
103
 
104
104
  const fixedHeader = (
105
105
  <div
106
- className={classNames(headerClass, {
106
+ className={cn(headerClass, {
107
107
  [selectedClass]: rootSelected && !isTopLevel,
108
108
  'text-sidebar-primary': rootSelected && isTopLevel,
109
109
  })}
@@ -111,13 +111,13 @@ function CollapsibleGroup({
111
111
  {node.root ? (
112
112
  <Link
113
113
  to={node.root.page.url}
114
- className={classNames(isTopLevel ? sectionLabelClass : groupLabelClass, 'flex-1')}
114
+ className={cn(isTopLevel ? sectionLabelClass : groupLabelClass, 'flex-1')}
115
115
  >
116
116
  {resolveIcon(node.icon)}
117
117
  {node.label}
118
118
  </Link>
119
119
  ) : (
120
- <span className={classNames(isTopLevel ? sectionLabelClass : groupLabelClass, 'flex-1')}>
120
+ <span className={cn(isTopLevel ? sectionLabelClass : groupLabelClass, 'flex-1')}>
121
121
  {resolveIcon(node.icon)}
122
122
  {node.label}
123
123
  </span>
@@ -130,7 +130,7 @@ function CollapsibleGroup({
130
130
  fixedHeader
131
131
  ) : drilldown !== false && node.root ? (
132
132
  <div
133
- className={classNames(headerClass, 'group/section flex items-center', {
133
+ className={cn(headerClass, 'group/section flex items-center', {
134
134
  [selectedClass]: rootSelected && !isTopLevel,
135
135
  'text-sidebar-primary': rootSelected && isTopLevel,
136
136
  })}
@@ -159,7 +159,7 @@ function CollapsibleGroup({
159
159
  <Button
160
160
  type="button"
161
161
  variant="ghost"
162
- className={classNames(
162
+ className={cn(
163
163
  headerClass,
164
164
  'group/section group/collapsible-trigger flex h-auto w-full items-center justify-start gap-[0.4rem] whitespace-normal rounded-none px-0 py-0 text-left hover:bg-transparent aria-expanded:bg-transparent dark:hover:bg-transparent',
165
165
  {
@@ -243,7 +243,7 @@ function NavNodes({
243
243
  href={node.href}
244
244
  target={node.target}
245
245
  rel={node.target === '_blank' ? 'noreferrer' : undefined}
246
- className={classNames(
246
+ className={cn(
247
247
  'flex min-w-0 items-center gap-[0.4rem] border-l border-l-sidebar-border px-3 py-[0.55rem] text-muted-foreground hover:text-sidebar-accent-foreground [overflow-wrap:anywhere]',
248
248
  { 'pl-6': depth > 1 },
249
249
  )}
@@ -264,7 +264,7 @@ function NavNodes({
264
264
  <Link
265
265
  key={url}
266
266
  to={url}
267
- className={classNames(
267
+ className={cn(
268
268
  'flex min-w-0 items-center gap-[0.4rem] border-l border-l-sidebar-border px-3 py-[0.55rem] [overflow-wrap:anywhere]',
269
269
  {
270
270
  'pl-6': depth > 1,
@@ -319,7 +319,7 @@ export function SideNav({
319
319
  const nodes = navigation[activeTabId] || navigation[tabs[0]?.id] || [];
320
320
 
321
321
  return (
322
- <ScrollArea className={classNames('w-full max-w-full', { 'h-full': isSticky })}>
322
+ <ScrollArea className={cn('w-full max-w-full', { 'h-full': isSticky })}>
323
323
  <nav className="flex w-full flex-col gap-6 pr-4 text-sm" aria-label={navigationLabel}>
324
324
  {anchors.length ? (
325
325
  <NavNodes
@@ -1,4 +1,4 @@
1
- import classNames from 'clsx';
1
+ import { cn } from '@/lib/utils';
2
2
  import { Link, useLocation, useNavigate } from 'react-router';
3
3
  import { ConfiguredIcon } from '@/components/ConfiguredIcon';
4
4
  import { ChevronRight } from '@/components/icons';
@@ -62,7 +62,7 @@ export function TopNav({ docs, label }: { docs: NormalizedDocsConfig; label: str
62
62
  .find(tab => pathname === tab.url || pathname.startsWith(`${tab.url}/`))?.id ||
63
63
  tabs[0]?.id;
64
64
  const tabClass = (tab: DocsTab) =>
65
- classNames(
65
+ cn(
66
66
  'flex h-full items-center gap-1 whitespace-nowrap border-transparent border-b-2 font-medium',
67
67
  {
68
68
  'border-b-primary text-foreground': tab.id === selected,
@@ -1,13 +1,5 @@
1
- import { XIcon } from 'lucide-react';
2
1
  import type { ImgHTMLAttributes } from 'react';
3
- import {
4
- Dialog,
5
- DialogClose,
6
- DialogContent,
7
- DialogPortal,
8
- DialogTitle,
9
- DialogTrigger,
10
- } from '@/components/ui/dialog';
2
+ import { Dialog, DialogContent, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
11
3
  import { cn } from '@/lib/utils';
12
4
 
13
5
  export interface ZoomableImageProps extends ImgHTMLAttributes<HTMLImageElement> {
@@ -64,19 +56,6 @@ export function ZoomableImage({
64
56
  className="max-h-[calc(100vh-2rem)] max-w-[calc(100vw-2rem)] object-contain"
65
57
  />
66
58
  </DialogContent>
67
- <DialogPortal>
68
- <DialogClose
69
- render={
70
- <button
71
- type="button"
72
- className="fixed top-4 right-4 z-[60] inline-flex size-9 items-center justify-center rounded-full bg-black/60 text-white outline-none backdrop-blur-sm hover:bg-black/80 focus-visible:ring-3 focus-visible:ring-white/60"
73
- aria-label="Close full-size image"
74
- />
75
- }
76
- >
77
- <XIcon className="size-5" />
78
- </DialogClose>
79
- </DialogPortal>
80
59
  </Dialog>
81
60
  );
82
61
  }
@@ -5,6 +5,7 @@ export {
5
5
  CircleAlert,
6
6
  Copy,
7
7
  ExternalLink,
8
+ FileText,
8
9
  Hand,
9
10
  Info as InfoIcon,
10
11
  Lightbulb,
@@ -27,6 +27,7 @@ const SHISO_THEME_LABELS: ThemeLabels = {
27
27
  searchUnavailable: 'Search unavailable',
28
28
  noResults: 'No results',
29
29
  lastUpdated: 'Last updated on',
30
+ relatedTopics: 'Related topics',
30
31
  notFound: 'Page not found',
31
32
  dismissBanner: 'Dismiss banner',
32
33
  toggleTheme: 'Toggle theme',
package/src/lib/types.ts CHANGED
@@ -444,6 +444,7 @@ export interface ThemeLabels {
444
444
  searchUnavailable: string;
445
445
  noResults: string;
446
446
  lastUpdated: string;
447
+ relatedTopics: string;
447
448
  notFound: string;
448
449
  dismissBanner: string;
449
450
  toggleTheme: string;
@@ -499,12 +500,17 @@ export interface TocEntry {
499
500
  size: number;
500
501
  }
501
502
 
503
+ /** A related-topics entry: a page path, or a link with an explicit title. */
504
+ export type RelatedEntry = string | { href: string; title?: string };
505
+
502
506
  export interface DocFrontmatter {
503
507
  title?: string;
504
508
  description?: string;
505
509
  noindex?: boolean;
506
510
  /** Overrides the site-wide `metadata.timestamp` setting for this page. */
507
511
  timestamp?: boolean;
512
+ /** Related pages rendered above the prev/next pager. */
513
+ related?: RelatedEntry[];
508
514
  [key: string]: unknown;
509
515
  }
510
516
 
@@ -146,24 +146,46 @@
146
146
  font-size: 0.875rem;
147
147
  }
148
148
 
149
+ /* Styled to match the PropertiesTable component: rounded outer border,
150
+ horizontal row separators only, and no header fill. */
149
151
  .docs-markdown table {
150
152
  width: 100%;
151
- border-collapse: collapse;
152
153
  margin: 1rem 0;
154
+ font-size: 0.875rem;
155
+ border-collapse: separate;
156
+ border-spacing: 0;
157
+ border: 1px solid var(--border);
158
+ border-radius: var(--radius-lg);
153
159
  }
154
160
 
155
161
  .docs-markdown th,
156
162
  .docs-markdown td {
157
- border: 1px solid var(--border);
158
- padding: 0.5rem 0.75rem;
163
+ border: none;
164
+ border-bottom: 1px solid var(--border);
165
+ padding: 0.6rem 0.75rem;
159
166
  text-align: left;
167
+ vertical-align: top;
168
+ }
169
+
170
+ .docs-markdown th:first-child,
171
+ .docs-markdown td:first-child {
172
+ padding-left: 1.5rem;
173
+ }
174
+
175
+ .docs-markdown th:last-child,
176
+ .docs-markdown td:last-child {
177
+ padding-right: 1.5rem;
160
178
  }
161
179
 
162
180
  .docs-markdown th {
163
- background: var(--card);
181
+ background: transparent;
164
182
  font-weight: 600;
165
183
  }
166
184
 
185
+ .docs-markdown tbody tr:last-child td {
186
+ border-bottom: none;
187
+ }
188
+
167
189
  .docs-markdown table code:not(pre code) {
168
190
  font-size: 0.75rem;
169
191
  }
package/vite.config.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { readFile } from 'node:fs/promises';
1
2
  import path from 'node:path';
2
3
  import { fileURLToPath } from 'node:url';
3
4
  import tailwindcss from '@tailwindcss/vite';
@@ -281,6 +282,75 @@ function shisoHtml(getDocsConfig: () => DocsConfig): Plugin {
281
282
  };
282
283
  }
283
284
 
285
+ /**
286
+ * Serves the raw markdown source for `<route>.md` URLs during development,
287
+ * mirroring the `.md` copies the prerenderer publishes next to every page in
288
+ * production builds. The contextual menu's copy/view options and AI links
289
+ * depend on these URLs.
290
+ */
291
+ function shisoMarkdownDev(getDocsConfig: () => DocsConfig, root: string): Plugin {
292
+ return {
293
+ name: 'shiso-markdown-dev',
294
+ apply: 'serve',
295
+ configureServer(server) {
296
+ server.middlewares.use(async (req, res, next) => {
297
+ const url = (req.url || '').split('?')[0];
298
+
299
+ if ((req.method !== 'GET' && req.method !== 'HEAD') || !url.endsWith('.md')) {
300
+ return next();
301
+ }
302
+
303
+ const shiso =
304
+ (getDocsConfig() as { $shiso?: { docsPrefix?: string; contentDir?: string } }).$shiso ||
305
+ {};
306
+ const contentDir = (shiso.contentDir ?? 'content/docs').replace(/^\/+|\/+$/g, '');
307
+ const prefixValue = (shiso.docsPrefix ?? '/docs').trim().replace(/\/+$/, '');
308
+ const docsPrefix =
309
+ !prefixValue || prefixValue === '/'
310
+ ? ''
311
+ : prefixValue.startsWith('/')
312
+ ? prefixValue
313
+ : `/${prefixValue}`;
314
+
315
+ let route = decodeURIComponent(url).slice(0, -'.md'.length);
316
+ const base = server.config.base.replace(/\/+$/, '');
317
+
318
+ if (base && route.startsWith(base)) {
319
+ route = route.slice(base.length);
320
+ }
321
+
322
+ if (docsPrefix && route.startsWith(docsPrefix)) {
323
+ route = route.slice(docsPrefix.length);
324
+ }
325
+
326
+ const slug = route.replace(/^\/+/, '') || 'index';
327
+ const contentRoot = path.resolve(root, contentDir);
328
+ const candidates = [`${slug}.mdx`, `${slug}.md`, `${slug}/index.mdx`, `${slug}/index.md`];
329
+
330
+ for (const candidate of candidates) {
331
+ const filePath = path.resolve(contentRoot, candidate);
332
+
333
+ // Never read outside the content directory.
334
+ if (!filePath.startsWith(contentRoot + path.sep)) {
335
+ break;
336
+ }
337
+
338
+ try {
339
+ const source = await readFile(filePath, 'utf8');
340
+ res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
341
+ res.end(source);
342
+ return;
343
+ } catch {
344
+ // Try the next candidate.
345
+ }
346
+ }
347
+
348
+ next();
349
+ });
350
+ },
351
+ };
352
+ }
353
+
284
354
  const MDX_REACT_ENTRY = fileURLToPath(import.meta.resolve('@mdx-js/react'));
285
355
  const LUCIDE_REACT_ENTRY = path.join(
286
356
  path.dirname(fileURLToPath(import.meta.resolve('lucide-react/package.json'))),
@@ -319,6 +389,7 @@ export default defineConfig(async () => {
319
389
  path.join(generatedRoot, 'search-index.generated.ts'),
320
390
  ),
321
391
  shisoHtml(getDocsConfig),
392
+ shisoMarkdownDev(getDocsConfig, projectRoot),
322
393
  shisoMdx(),
323
394
  react({ include: /\.(mdx|md|tsx|ts|jsx|js)$/ }),
324
395
  ],