@umami/shiso 1.8.0 → 1.10.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.
package/CHANGELOG.md CHANGED
@@ -5,15 +5,12 @@ All notable changes to `@umami/shiso` are documented here. This project follows
5
5
 
6
6
  ## Unreleased
7
7
 
8
- ### Fixed
9
-
10
- - Dev server: allow serving raw assets (fonts, images) from Shiso's real
11
- install location when the framework is linked from outside the project's
12
- workspace root (`link:`/`file:` installs previously returned 403 for
13
- bundled font files).
14
-
15
8
  ### Added
16
9
 
10
+ - `Link` export from `@umami/shiso/components`: a client-side navigation link
11
+ bound to Shiso's router, for TSX standalone pages and other app code.
12
+ Importing `react-router` directly from app code creates a second router
13
+ instance without Shiso's context, so use this instead.
17
14
  - Public `@umami/shiso/components` entry point for importing Shiso's built-in
18
15
  content components from TSX standalone pages and other React modules.
19
16
  - TSX standalone pages under `content/pages`. Component pages retain Shiso's
@@ -24,7 +21,6 @@ All notable changes to `@umami/shiso` are documented here. This project follows
24
21
  descendants) are excluded from `.docs-markdown` typography styles — headings,
25
22
  lists, tables, links, and spacing rules — so embedded UI components render
26
23
  with their own styles. Guards use `:not(:where(…))` and add no specificity.
27
-
28
24
  - Per-page `search: false` frontmatter to hide the header search control and
29
25
  disable its keyboard shortcut on selected docs or standalone pages.
30
26
  - Link tabs: a navigation tab may define `href` instead of `groups`/`pages`/
@@ -32,6 +28,19 @@ All notable changes to `@umami/shiso` are documented here. This project follows
32
28
  navigation. Internal hrefs route client-side and highlight when active;
33
29
  external URLs open in a new browser tab.
34
30
 
31
+ ### Fixed
32
+
33
+ - Let standalone page content expand through the layout's flex chain so short
34
+ pages keep the footer at the bottom of the viewport.
35
+ - Bundle `@umami/shiso` entry points into the SSR build (`ssr.noExternal`) so
36
+ TSX standalone pages that import `@umami/shiso/components` prerender
37
+ correctly; previously Node hit the unresolved project-time virtual import
38
+ `@/lib/icon-registry.generated` inside the published chunks.
39
+ - Dev server: allow serving raw assets (fonts, images) from Shiso's real
40
+ install location when the framework is linked from outside the project's
41
+ workspace root (`link:`/`file:` installs previously returned 403 for
42
+ bundled font files).
43
+
35
44
  ## 2.0.0 - 2026-08-21
36
45
 
37
46
  ### Breaking
@@ -1,4 +1,4 @@
1
- import { $ as POPUP_COLLISION_AVOIDANCE, $n as isLastTraversableNode, $t as listNavigation, A as popupStoreSelectors, An as jsdom, B as useSyncedFloatingRootContext, Bn as EMPTY_OBJECT, Bt as resolveIcon, C as pressableTriggerOpenStateMapping, Cn as activeElement, Ct as inertValue, D as useFocus, Dn as ARROW_LEFT, E as useHoverFloatingInteraction, En as ARROW_DOWN, F as setPopupOpenState, Fn as useTimeout, G as FloatingNode, Gn as useCompositeRootContext, Gt as useCompositeListItem, H as useDismiss, Hn as useButton, Ht as useOpenChangeComplete, I as useImplicitActiveTrigger, In as Button, J as useFloatingParentNodeId, Jn as useStableCallback, Jt as cancelOpen, K as FloatingTree, Kn as mergeProps$1, Kt as useAnimationFrame, L as useOpenStateTransitions, Ln as cn, Lt as Badge, M as FOCUSABLE_POPUP_PROPS, Mn as mac, N as PopupHandleAttachment, Nn as useBaseUiId, O as usePopupHandleStore, On as ARROW_RIGHT, P as attachPreventUnmountOnClose, Pn as useId$1, Q as DROPDOWN_COLLISION_AVOIDANCE, Qn as isHTMLElement, Qt as itemPress, R as usePopupInteractionProps, Rn as useRenderElement, S as popupTransitionStateMapping, Sn as isTypeableCombobox, St as COMPOSITE_KEYS, T as useHoverReferenceInteraction, Tn as getTarget, U as useClick, Un as dispatchClickWithModifiers, Ut as useAnimationsFinished, V as ReactStore, Vn as useMergedRefs, Vt as getIcon, W as FloatingFocusManager, Wn as ownerDocument, Wt as useValueAsRef, X as FloatingTreeStore, Xn as getParentNode, Xt as focusOut, Y as useFloatingTree, Yn as useRefWithInit, Yt as escapeKey, Z as FloatingPortal, Zn as getWindow, Zt as imperativeAction, _n as isListIndexDisabled, _t as CollapsibleContent, a as DialogTitle, an as CompositeList, ar as Link, b as getDisabledMountTransitionStyles, bn as stopEvent, c as useOpenInteractionType, cn as getNextTabbable, d as useScrollLock, dn as isOutsideEvent, dr as useNavigate, en as outsidePress, er as X$1, et as enqueueFocus, f as DialogPortal$1, fn as findNonDisabledListIndex, g as DialogBackdrop, gn as isIndexOutOfListBounds, gt as Collapsible, h as DialogClose, hn as isElementVisible, i as DialogContent$1, in as triggerPress, j as PopupTriggerMap, jn as webkit, k as createInitialPopupStoreState, kn as ARROW_UP, l as DialogTitle$1, ln as getTabbableAfterElement, lr as Routes, m as DialogPopup, mn as getMinListIndex, mt as renderInlineMarkdown, n as ZoomableImage, nn as triggerFocus, nr as Check, nt as fastComponent, o as DialogTrigger, on as addEventListener, or as Navigate, p as InternalBackdrop, pn as getMaxListIndex, q as useFloatingNodeId, qn as useIsoLayoutEffect, qt as createChangeEventDetails, r as Dialog$1, rn as triggerHover, rr as createLucideIcon, rt as fastComponentRef, s as DialogTrigger$1, sn as useDirection, sr as Route, t as docs_exports, tn as siblingOpen, tr as ChevronRight, tt as FocusGuard, u as DialogRoot, un as getTabbableBeforeElement, ur as useLocation, v as TooltipProvider, vn as isVirtualClick, vt as CollapsibleTrigger, w as safePolygon, wn as contains, wt as useCompositeItem, x as useAnchorPositioning, xn as getFloatingFocusElement, y as usePositioner, yn as isVirtualPointerEvent, z as useTriggerDataForwarding, zn as EMPTY_ARRAY } from "./docs.js";
1
+ import { $ as POPUP_COLLISION_AVOIDANCE, $n as isHTMLElement, $t as itemPress, A as popupStoreSelectors, An as ARROW_UP, B as useSyncedFloatingRootContext, Bn as EMPTY_ARRAY, C as pressableTriggerOpenStateMapping, Cn as isTypeableCombobox, Ct as COMPOSITE_KEYS, D as useFocus, Dn as ARROW_DOWN, E as useHoverFloatingInteraction, En as getTarget, F as setPopupOpenState, Fn as useId$1, G as FloatingNode, Gn as ownerDocument, Gt as useValueAsRef, H as useDismiss, Hn as useMergedRefs, Ht as getIcon, I as useImplicitActiveTrigger, In as useTimeout, J as useFloatingParentNodeId, Jn as useIsoLayoutEffect, Jt as createChangeEventDetails, K as FloatingTree, Kn as useCompositeRootContext, Kt as useCompositeListItem, L as useOpenStateTransitions, Ln as Button, M as FOCUSABLE_POPUP_PROPS, Mn as webkit, N as PopupHandleAttachment, Nn as mac, O as usePopupHandleStore, On as ARROW_LEFT, P as attachPreventUnmountOnClose, Pn as useBaseUiId, Q as DROPDOWN_COLLISION_AVOIDANCE, Qn as getWindow, Qt as imperativeAction, R as usePopupInteractionProps, Rn as cn, Rt as Badge, S as popupTransitionStateMapping, Sn as getFloatingFocusElement, T as useHoverReferenceInteraction, Tn as contains, Tt as useCompositeItem, U as useClick, Un as useButton, Ut as useOpenChangeComplete, V as ReactStore, Vn as EMPTY_OBJECT, Vt as resolveIcon, W as FloatingFocusManager, Wn as dispatchClickWithModifiers, Wt as useAnimationsFinished, X as FloatingTreeStore, Xn as useRefWithInit, Xt as escapeKey, Y as useFloatingTree, Yn as useStableCallback, Yt as cancelOpen, Z as FloatingPortal, Zn as getParentNode, Zt as focusOut, _n as isIndexOutOfListBounds, _t as Collapsible, a as DialogTitle, an as triggerPress, b as getDisabledMountTransitionStyles, bn as isVirtualPointerEvent, c as useOpenInteractionType, cn as useDirection, cr as Route, d as useScrollLock, dn as getTabbableBeforeElement, dr as useLocation, en as listNavigation, er as isLastTraversableNode, et as enqueueFocus, f as DialogPortal$1, fn as isOutsideEvent, fr as useNavigate, g as DialogBackdrop, gn as isElementVisible, h as DialogClose, hn as getMinListIndex, ht as renderInlineMarkdown, i as DialogContent$1, in as triggerHover, ir as createLucideIcon, j as PopupTriggerMap, jn as jsdom, k as createInitialPopupStoreState, kn as ARROW_RIGHT, l as DialogTitle$1, ln as getNextTabbable, m as DialogPopup, mn as getMaxListIndex, n as ZoomableImage, nn as siblingOpen, nr as ChevronRight, nt as fastComponent, o as DialogTrigger, on as CompositeList, or as Link, p as InternalBackdrop, pn as findNonDisabledListIndex, q as useFloatingNodeId, qn as mergeProps$1, qt as useAnimationFrame, r as Dialog$1, rn as triggerFocus, rr as Check, rt as fastComponentRef, s as DialogTrigger$1, sn as addEventListener, sr as Navigate, t as docs_exports, tn as outsidePress, tr as X$1, tt as FocusGuard, u as DialogRoot, un as getTabbableAfterElement, ur as Routes, v as TooltipProvider, vn as isListIndexDisabled, vt as CollapsibleContent, w as safePolygon, wn as activeElement, wt as inertValue, x as useAnchorPositioning, xn as stopEvent, y as usePositioner, yn as isVirtualClick, yt as CollapsibleTrigger, z as useTriggerDataForwarding, zn as useRenderElement } from "./docs.js";
2
2
  import { n as highlightTerms } from "./search.js";
3
3
  import { resolveSearchProvider } from "../search.js";
4
4
  import * as React from "react";
@@ -14,6 +14,40 @@ import shiso from "virtual:shiso-config";
14
14
  import rawConfig from "virtual:shiso-docs-config";
15
15
  import { LAST_MODIFIED } from "@/generated/last-modified";
16
16
 
17
+ //#region ../../node_modules/.pnpm/lucide-react@1.28.0_react@19.2.8/node_modules/lucide-react/dist/esm/icons/arrow-left.mjs
18
+ /**
19
+ * @license lucide-react v1.28.0 - ISC
20
+ *
21
+ * This source code is licensed under the ISC license.
22
+ * See the LICENSE file in the root directory of this source tree.
23
+ */
24
+ const __iconNode$10 = [["path", {
25
+ d: "m12 19-7-7 7-7",
26
+ key: "1l729n"
27
+ }], ["path", {
28
+ d: "M19 12H5",
29
+ key: "x3x0zl"
30
+ }]];
31
+ const ArrowLeft = createLucideIcon("arrow-left", __iconNode$10);
32
+
33
+ //#endregion
34
+ //#region ../../node_modules/.pnpm/lucide-react@1.28.0_react@19.2.8/node_modules/lucide-react/dist/esm/icons/arrow-right.mjs
35
+ /**
36
+ * @license lucide-react v1.28.0 - ISC
37
+ *
38
+ * This source code is licensed under the ISC license.
39
+ * See the LICENSE file in the root directory of this source tree.
40
+ */
41
+ const __iconNode$9 = [["path", {
42
+ d: "M5 12h14",
43
+ key: "1ays0h"
44
+ }], ["path", {
45
+ d: "m12 5 7 7-7 7",
46
+ key: "xquz4c"
47
+ }]];
48
+ const ArrowRight = createLucideIcon("arrow-right", __iconNode$9);
49
+
50
+ //#endregion
17
51
  //#region ../../node_modules/.pnpm/lucide-react@1.28.0_react@19.2.8/node_modules/lucide-react/dist/esm/icons/copy.mjs
18
52
  /**
19
53
  * @license lucide-react v1.28.0 - ISC
@@ -4026,11 +4060,24 @@ const DEFAULT_SEARCH_PROMPT = "Search...";
4026
4060
  const DEFAULT_SEARCH_PROVIDER = "local";
4027
4061
  const DEFAULT_SEARCH_SHORTCUT = "k";
4028
4062
  const DEFAULT_SEARCH_SHORTCUT_LABEL = "Ctrl K";
4063
+ const DEFAULT_SEARCH_POSITION = "header";
4064
+ /** Positions the built-in theme knows how to render. */
4065
+ const SEARCH_POSITIONS = ["header", "sidebar"];
4066
+ /**
4067
+ * Themes must always support "header", so anything unrecognized (or not
4068
+ * implemented by the active theme) lands there instead of vanishing.
4069
+ */
4070
+ function resolveSearchPosition(position, supported = SEARCH_POSITIONS) {
4071
+ if (typeof position === "string" && supported.includes(position)) return position;
4072
+ if (position !== void 0 && position !== "header") console.warn(`[shiso] Unsupported search.position "${String(position)}" — using "${DEFAULT_SEARCH_POSITION}".`);
4073
+ return DEFAULT_SEARCH_POSITION;
4074
+ }
4029
4075
  /** Normalizes docs.json search settings for both the UI and provider loader. */
4030
4076
  function resolveSearchConfig(config) {
4031
4077
  if (config === false) return {
4032
4078
  enabled: false,
4033
4079
  prompt: DEFAULT_SEARCH_PROMPT,
4080
+ position: DEFAULT_SEARCH_POSITION,
4034
4081
  provider: DEFAULT_SEARCH_PROVIDER,
4035
4082
  options: {},
4036
4083
  shortcut: false,
@@ -4039,6 +4086,7 @@ function resolveSearchConfig(config) {
4039
4086
  return {
4040
4087
  enabled: true,
4041
4088
  prompt: config?.prompt?.trim() || "Search...",
4089
+ position: resolveSearchPosition(config?.position),
4042
4090
  provider: config?.provider?.trim().toLowerCase() || "local",
4043
4091
  options: config?.options || {},
4044
4092
  shortcut: config?.shortcut === false ? false : config?.shortcut?.trim().toLowerCase() || "k",
@@ -4060,6 +4108,8 @@ const SHISO_THEME_LABELS = {
4060
4108
  noResults: "No results",
4061
4109
  lastUpdated: "Last updated on",
4062
4110
  relatedTopics: "Related topics",
4111
+ previousPage: "Previous",
4112
+ nextPage: "Next",
4063
4113
  notFound: "Page not found",
4064
4114
  dismissBanner: "Dismiss banner",
4065
4115
  toggleTheme: "Toggle theme",
@@ -7205,7 +7255,7 @@ function renderWithQueryHighlight(text, query) {
7205
7255
  * Provider-neutral search dialog. The selected provider and its index or
7206
7256
  * client are loaded on demand, so search stays out of the initial bundle.
7207
7257
  */
7208
- function Search({ config, labels }) {
7258
+ function Search({ config, labels, className }) {
7209
7259
  const navigate = useNavigate();
7210
7260
  const { pathname } = useLocation();
7211
7261
  const [open, setOpen] = useState(false);
@@ -7294,12 +7344,12 @@ function Search({ config, labels }) {
7294
7344
  children: [/* @__PURE__ */ jsxs(DialogTrigger, {
7295
7345
  render: /* @__PURE__ */ jsx(Button, {
7296
7346
  variant: "outline",
7297
- className: "h-auto gap-2 rounded-md bg-card px-2.5 py-1.5 text-muted-foreground hover:border-input hover:bg-card hover:text-foreground"
7347
+ className: cn("h-auto gap-2 rounded-md bg-card px-2.5 py-1.5 text-muted-foreground hover:border-input hover:bg-card hover:text-foreground", className)
7298
7348
  }),
7299
7349
  children: [
7300
7350
  /* @__PURE__ */ jsx(Search$1, { className: "size-3.5" }),
7301
7351
  /* @__PURE__ */ jsx("span", {
7302
- className: "min-w-24 text-left",
7352
+ className: "min-w-24 grow text-left",
7303
7353
  children: config.prompt
7304
7354
  }),
7305
7355
  config.shortcut ? /* @__PURE__ */ jsx("kbd", {
@@ -7348,6 +7398,21 @@ function Search({ config, labels }) {
7348
7398
  })]
7349
7399
  });
7350
7400
  }
7401
+ /**
7402
+ * Renders the search control only when `search.position` targets this slot.
7403
+ * Layout components drop one of these into each position they support; the
7404
+ * per-page `search: false` frontmatter flag is honored here as well.
7405
+ */
7406
+ function SearchSlot({ site, position, className }) {
7407
+ const { pathname } = useLocation();
7408
+ if (!site.search.enabled || site.search.position !== position) return null;
7409
+ if (getPageFrontmatter(pathname)?.search === false) return null;
7410
+ return /* @__PURE__ */ jsx(Search, {
7411
+ config: site.search,
7412
+ labels: site.labels,
7413
+ className
7414
+ });
7415
+ }
7351
7416
 
7352
7417
  //#endregion
7353
7418
  //#region src/components/ThemeToggle.tsx
@@ -7527,10 +7592,9 @@ function NavbarLinkItem({ link, primary = false }) {
7527
7592
  });
7528
7593
  }
7529
7594
  function Header({ site }) {
7530
- const { logo, navbar, name, appearance, labels, search } = site;
7595
+ const { logo, navbar, name, appearance, labels } = site;
7531
7596
  const { pathname } = useLocation();
7532
7597
  const docs = getScopeByPathname(pathname).docs;
7533
- const showSearch = getPageFrontmatter(pathname)?.search !== false;
7534
7598
  const brandHref = logo?.href || (hasRootStandalonePage ? "/" : docsHomeUrl);
7535
7599
  const hasBrand = !!name || !!logo?.light || !!logo?.dark;
7536
7600
  const brandClassName = "inline-flex items-center gap-2 text-xl font-bold text-foreground tracking-[-0.03em]";
@@ -7579,10 +7643,15 @@ function Header({ site }) {
7579
7643
  /* @__PURE__ */ jsxs("div", {
7580
7644
  className: "flex min-w-0 items-center gap-2 justify-self-end",
7581
7645
  children: [
7582
- showSearch ? /* @__PURE__ */ jsx(Search, {
7583
- config: search,
7584
- labels
7585
- }) : null,
7646
+ /* @__PURE__ */ jsx(SearchSlot, {
7647
+ site,
7648
+ position: "header"
7649
+ }),
7650
+ /* @__PURE__ */ jsx(SearchSlot, {
7651
+ site,
7652
+ position: "sidebar",
7653
+ className: "lg:hidden"
7654
+ }),
7586
7655
  navbar?.links.map((link) => /* @__PURE__ */ jsx(NavbarLinkItem, { link }, link.href)),
7587
7656
  !appearance.strict && /* @__PURE__ */ jsx(ThemeToggle, { label: labels.toggleTheme }),
7588
7657
  navbar?.primary ? /* @__PURE__ */ jsx(NavbarLinkItem, {
@@ -7816,7 +7885,7 @@ function Layout({ children, site }) {
7816
7885
  /* @__PURE__ */ jsx("div", {
7817
7886
  className: "mx-auto flex min-h-0 w-full max-w-[1600px] grow flex-col px-5",
7818
7887
  children: /* @__PURE__ */ jsx("main", {
7819
- className: "min-h-0 grow",
7888
+ className: "flex min-h-0 grow flex-col",
7820
7889
  children
7821
7890
  })
7822
7891
  })
@@ -8001,30 +8070,44 @@ function DocContent({ page, doc, site }) {
8001
8070
  })]
8002
8071
  }),
8003
8072
  /* @__PURE__ */ jsxs("div", {
8004
- className: "mt-8 flex items-center justify-between",
8073
+ className: "mt-8 flex items-end justify-between",
8005
8074
  "data-pagefind-ignore": true,
8006
8075
  children: [/* @__PURE__ */ jsx(NavigationButton, {
8007
8076
  ...prev,
8077
+ eyebrow: site.labels.previousPage,
8008
8078
  isPrev: true
8009
- }), /* @__PURE__ */ jsx(NavigationButton, { ...next })]
8079
+ }), /* @__PURE__ */ jsx(NavigationButton, {
8080
+ ...next,
8081
+ eyebrow: site.labels.nextPage
8082
+ })]
8010
8083
  })
8011
8084
  ]
8012
8085
  });
8013
8086
  }
8014
- const NavigationButton = ({ label, url, isPrev }) => {
8087
+ const NavigationButton = ({ label, url, eyebrow, isPrev }) => {
8015
8088
  if (!url || !label) return /* @__PURE__ */ jsx("div", {});
8016
8089
  return /* @__PURE__ */ jsxs(Link, {
8017
8090
  to: url,
8018
- className: "group my-3 inline-flex items-center gap-3 text-base font-bold text-foreground",
8091
+ className: cn("group my-3 inline-flex items-end gap-3 text-base text-foreground", { "text-right": !isPrev }),
8092
+ rel: isPrev ? "prev" : "next",
8019
8093
  children: [
8020
- isPrev && /* @__PURE__ */ jsx(ChevronRight, {
8094
+ isPrev && /* @__PURE__ */ jsx(ArrowLeft, {
8021
8095
  size: 14,
8022
- className: "rotate-180 text-muted-foreground transition-colors group-hover:text-foreground"
8096
+ className: "mb-[0.3rem] text-muted-foreground transition-colors group-hover:text-foreground"
8023
8097
  }),
8024
- label,
8025
- !isPrev && /* @__PURE__ */ jsx(ChevronRight, {
8098
+ /* @__PURE__ */ jsxs("span", {
8099
+ className: "flex flex-col",
8100
+ children: [/* @__PURE__ */ jsx("span", {
8101
+ className: "text-xs font-bold text-muted-foreground",
8102
+ children: eyebrow
8103
+ }), /* @__PURE__ */ jsx("span", {
8104
+ className: "font-medium transition-colors group-hover:text-primary",
8105
+ children: label
8106
+ })]
8107
+ }),
8108
+ !isPrev && /* @__PURE__ */ jsx(ArrowRight, {
8026
8109
  size: 14,
8027
- className: "text-muted-foreground transition-colors group-hover:text-foreground"
8110
+ className: "mb-[0.3rem] text-muted-foreground transition-colors group-hover:text-foreground"
8028
8111
  })
8029
8112
  ]
8030
8113
  });
@@ -8307,7 +8390,7 @@ function SideNav({ tabs, navigation, anchors, activeTabId, isSticky, drilldown,
8307
8390
  const { pathname } = useLocation();
8308
8391
  const nodes = navigation[activeTabId] || navigation[tabs[0]?.id] || [];
8309
8392
  return /* @__PURE__ */ jsx(ScrollArea, {
8310
- className: cn("w-full max-w-full", { "h-full": isSticky }),
8393
+ className: cn("w-full max-w-full", { "min-h-0 grow": isSticky }),
8311
8394
  children: /* @__PURE__ */ jsxs("nav", {
8312
8395
  className: "flex w-full flex-col gap-6 pr-4 text-sm",
8313
8396
  "aria-label": navigationLabel,
@@ -8462,9 +8545,13 @@ function Docs({ page, doc, site }) {
8462
8545
  })]
8463
8546
  }), /* @__PURE__ */ jsxs("div", {
8464
8547
  className: "flex items-start gap-12 lg:min-h-[calc(100dvh-var(--header-height))] lg:pt-6",
8465
- children: [/* @__PURE__ */ jsx("div", {
8466
- className: "hidden min-w-0 max-w-60 basis-60 self-start lg:sticky lg:top-[calc(var(--header-height)+1.5rem)] lg:block lg:h-[calc(100dvh-var(--header-height)-3rem)] lg:shrink-0",
8467
- children: /* @__PURE__ */ jsx(SideNav, {
8548
+ children: [/* @__PURE__ */ jsxs("div", {
8549
+ className: "hidden min-w-0 max-w-60 basis-60 flex-col gap-4 self-start lg:sticky lg:top-[calc(var(--header-height)+1.5rem)] lg:flex lg:h-[calc(100dvh-var(--header-height)-3rem)] lg:shrink-0",
8550
+ children: [/* @__PURE__ */ jsx(SearchSlot, {
8551
+ site,
8552
+ position: "sidebar",
8553
+ className: "w-full"
8554
+ }), /* @__PURE__ */ jsx(SideNav, {
8468
8555
  tabs,
8469
8556
  navigation,
8470
8557
  anchors: scopeDocs.anchors,
@@ -8474,7 +8561,7 @@ function Docs({ page, doc, site }) {
8474
8561
  navigationLabel: site.labels.documentationNavigation,
8475
8562
  expandLabel: site.labels.expand,
8476
8563
  collapseLabel: site.labels.collapse
8477
- })
8564
+ })]
8478
8565
  }), /* @__PURE__ */ jsxs("div", {
8479
8566
  className: "flex min-w-0 grow self-stretch flex-col",
8480
8567
  children: [/* @__PURE__ */ jsxs("div", {
@@ -8547,7 +8634,7 @@ function StandalonePageView({ page, site }) {
8547
8634
  const Content = doc.default;
8548
8635
  const isComponentPage = page.filePath.endsWith(".tsx");
8549
8636
  return /* @__PURE__ */ jsxs("div", {
8550
- className: "flex min-h-full flex-col",
8637
+ className: "flex grow flex-col",
8551
8638
  children: [isComponentPage ? /* @__PURE__ */ jsx("div", {
8552
8639
  className: "grow",
8553
8640
  children: /* @__PURE__ */ jsx(Content, {})
@@ -2503,7 +2503,7 @@ HistoryRouter.displayName = "unstable_HistoryRouter";
2503
2503
  * @param {LinkProps.defaultShouldRevalidate} props.defaultShouldRevalidate n/a
2504
2504
  * @param {LinkProps.mask} props.mask [modes: framework, data] n/a
2505
2505
  */
2506
- const Link = React$1.forwardRef(function LinkWithRef({ onClick, discover = "render", prefetch = "none", relative, reloadDocument, replace, mask, state, target, to, preventScrollReset, viewTransition, defaultShouldRevalidate, ...rest }, forwardedRef) {
2506
+ const Link$1 = React$1.forwardRef(function LinkWithRef({ onClick, discover = "render", prefetch = "none", relative, reloadDocument, replace, mask, state, target, to, preventScrollReset, viewTransition, defaultShouldRevalidate, ...rest }, forwardedRef) {
2507
2507
  let { basename, navigator, useTransitions } = React$1.useContext(NavigationContext);
2508
2508
  let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to);
2509
2509
  let parsed = parseToInfo(to, basename);
@@ -2544,7 +2544,7 @@ const Link = React$1.forwardRef(function LinkWithRef({ onClick, discover = "rend
2544
2544
  });
2545
2545
  return shouldPrefetch && !isAbsolute ? /* @__PURE__ */ React$1.createElement(React$1.Fragment, null, link, /* @__PURE__ */ React$1.createElement(PrefetchPageLinks, { page: href })) : link;
2546
2546
  });
2547
- Link.displayName = "Link";
2547
+ Link$1.displayName = "Link";
2548
2548
  /**
2549
2549
  * Wraps {@link Link | `<Link>`} with additional props for styling active and
2550
2550
  * pending states.
@@ -2621,7 +2621,7 @@ const NavLink = React$1.forwardRef(function NavLinkWithRef({ "aria-current": ari
2621
2621
  isTransitioning ? "transitioning" : null
2622
2622
  ].filter(Boolean).join(" ");
2623
2623
  let style = typeof styleProp === "function" ? styleProp(renderProps) : styleProp;
2624
- return /* @__PURE__ */ React$1.createElement(Link, {
2624
+ return /* @__PURE__ */ React$1.createElement(Link$1, {
2625
2625
  ...rest,
2626
2626
  "aria-current": ariaCurrent,
2627
2627
  className,
@@ -10182,7 +10182,7 @@ function Button({ href, variant = "default", size = "default", icon, className,
10182
10182
  rel: "noreferrer",
10183
10183
  children: content
10184
10184
  });
10185
- return /* @__PURE__ */ jsx(Link, {
10185
+ return /* @__PURE__ */ jsx(Link$1, {
10186
10186
  to: href,
10187
10187
  className: linkClassName,
10188
10188
  children: content
@@ -10400,7 +10400,7 @@ function Card({ title, href, icon, color, img, cta, horizontal, arrow, type, chi
10400
10400
  rel: "noreferrer",
10401
10401
  children: content
10402
10402
  });
10403
- return /* @__PURE__ */ jsx(Link, {
10403
+ return /* @__PURE__ */ jsx(Link$1, {
10404
10404
  to: href,
10405
10405
  className: linkClassName,
10406
10406
  children: content
@@ -11666,6 +11666,18 @@ function Icon({ icon, src, color, size = 16, className }) {
11666
11666
  });
11667
11667
  }
11668
11668
 
11669
+ //#endregion
11670
+ //#region src/components/docs/Link.tsx
11671
+ /**
11672
+ * Client-side navigation link bound to Shiso's router. Exposed from
11673
+ * `@umami/shiso/components` so TSX standalone pages and other app code can
11674
+ * navigate without reloading (importing `react-router` directly would create a
11675
+ * second router instance without Shiso's context).
11676
+ */
11677
+ function Link(props) {
11678
+ return /* @__PURE__ */ jsx(Link$1, { ...props });
11679
+ }
11680
+
11669
11681
  //#endregion
11670
11682
  //#region src/components/docs/ParamField.tsx
11671
11683
  function ParamField({ name, query, path, header, body, type, required, children }) {
@@ -18901,6 +18913,7 @@ var docs_exports = /* @__PURE__ */ __exportAll({
18901
18913
  Frame: () => Frame,
18902
18914
  Icon: () => Icon,
18903
18915
  Info: () => Info,
18916
+ Link: () => Link,
18904
18917
  Note: () => Note,
18905
18918
  Param: () => Param,
18906
18919
  ParamField: () => ParamField,
@@ -18918,4 +18931,4 @@ var docs_exports = /* @__PURE__ */ __exportAll({
18918
18931
  });
18919
18932
 
18920
18933
  //#endregion
18921
- export { POPUP_COLLISION_AVOIDANCE as $, isLastTraversableNode as $n, listNavigation as $t, popupStoreSelectors as A, jsdom as An, Info as At, useSyncedFloatingRootContext as B, EMPTY_OBJECT as Bn, resolveIcon as Bt, pressableTriggerOpenStateMapping as C, activeElement as Cn, inertValue as Ct, useFocus as D, ARROW_LEFT$1 as Dn, Callout as Dt, useHoverFloatingInteraction as E, ARROW_DOWN$1 as En, CardGroup as Et, setPopupOpenState as F, useTimeout as Fn, Button as Ft, FloatingNode as G, useCompositeRootContext as Gn, useCompositeListItem as Gt, useDismiss as H, useButton as Hn, useOpenChangeComplete as Ht, useImplicitActiveTrigger as I, Button$1 as In, Badge as It, useFloatingParentNodeId as J, useStableCallback as Jn, cancelOpen as Jt, FloatingTree as K, mergeProps as Kn, useAnimationFrame as Kt, useOpenStateTransitions as L, cn as Ln, Badge$1 as Lt, FOCUSABLE_POPUP_PROPS as M, mac as Mn, Tip as Mt, PopupHandleAttachment as N, useBaseUiId as Nn, Warning as Nt, usePopupHandleStore as O, ARROW_RIGHT$1 as On, Check as Ot, attachPreventUnmountOnClose as P, useId as Pn, WarningBanner as Pt, DROPDOWN_COLLISION_AVOIDANCE as Q, isHTMLElement as Qn, itemPress as Qt, usePopupInteractionProps as R, useRenderElement as Rn, Accordion as Rt, popupTransitionStateMapping as S, isTypeableCombobox as Sn, COMPOSITE_KEYS as St, useHoverReferenceInteraction as T, getTarget as Tn, Card as Tt, useClick as U, dispatchClickWithModifiers as Un, useAnimationsFinished as Ut, ReactStore as V, useMergedRefs as Vn, getIcon as Vt, FloatingFocusManager as W, ownerDocument as Wn, useValueAsRef as Wt, FloatingTreeStore as X, getParentNode as Xn, focusOut as Xt, useFloatingTree as Y, useRefWithInit as Yn, escapeKey as Yt, FloatingPortal as Z, getWindow as Zn, imperativeAction as Zt, Tooltip as _, isListIndexDisabled as _n, CollapsibleContent as _t, DialogTitle as a, CompositeList as an, Link as ar, Tabs as at, getDisabledMountTransitionStyles as b, stopEvent as bn, Columns as bt, useOpenInteractionType as c, getNextTabbable as cn, Router as cr, ResponseField as ct, useScrollLock as d, isOutsideEvent as dn, useNavigate as dr, ParamField as dt, outsidePress as en, X as er, enqueueFocus as et, DialogPortal$1 as f, findNonDisabledListIndex as fn, createPath as fr, Icon as ft, DialogBackdrop as g, isIndexOutOfListBounds as gn, Collapsible as gt, DialogClose as h, isElementVisible as hn, Expandable as ht, DialogContent as i, triggerPress as in, BrowserRouter as ir, Tab as it, PopupTriggerMap as j, webkit as jn, Note as jt, createInitialPopupStoreState as k, ARROW_UP$1 as kn, Danger as kt, DialogTitle$1 as l, getTabbableAfterElement as ln, Routes as lr, PropertiesTable as lt, DialogPopup as m, getMinListIndex as mn, ABSOLUTE_URL_REGEX as mr, renderInlineMarkdown as mt, ZoomableImage as n, triggerFocus as nn, Check$1 as nr, fastComponent as nt, DialogTrigger as o, addEventListener as on, Navigate as or, Step as ot, InternalBackdrop as p, getMaxListIndex as pn, parsePath as pr, Frame as pt, useFloatingNodeId as q, useIsoLayoutEffect as qn, createChangeEventDetails as qt, Dialog as r, triggerHover as rn, createLucideIcon as rr, fastComponentRef as rt, DialogTrigger$1 as s, useDirection as sn, Route as sr, Steps as st, docs_exports as t, siblingOpen as tn, ChevronRight as tr, FocusGuard as tt, DialogRoot as u, getTabbableBeforeElement as un, useLocation as ur, Param as ut, TooltipProvider as v, isVirtualClick as vn, CollapsibleTrigger as vt, safePolygon as w, contains as wn, useCompositeItem as wt, useAnchorPositioning as x, getFloatingFocusElement as xn, CodeGroup as xt, usePositioner as y, isVirtualPointerEvent as yn, Column as yt, useTriggerDataForwarding as z, EMPTY_ARRAY$1 as zn, AccordionGroup as zt };
18934
+ export { POPUP_COLLISION_AVOIDANCE as $, isHTMLElement as $n, itemPress as $t, popupStoreSelectors as A, ARROW_UP$1 as An, Danger as At, useSyncedFloatingRootContext as B, EMPTY_ARRAY$1 as Bn, AccordionGroup as Bt, pressableTriggerOpenStateMapping as C, isTypeableCombobox as Cn, COMPOSITE_KEYS as Ct, useFocus as D, ARROW_DOWN$1 as Dn, CardGroup as Dt, useHoverFloatingInteraction as E, getTarget as En, Card as Et, setPopupOpenState as F, useId as Fn, WarningBanner as Ft, FloatingNode as G, ownerDocument as Gn, useValueAsRef as Gt, useDismiss as H, useMergedRefs as Hn, getIcon as Ht, useImplicitActiveTrigger as I, useTimeout as In, Button as It, useFloatingParentNodeId as J, useIsoLayoutEffect as Jn, createChangeEventDetails as Jt, FloatingTree as K, useCompositeRootContext as Kn, useCompositeListItem as Kt, useOpenStateTransitions as L, Button$1 as Ln, Badge as Lt, FOCUSABLE_POPUP_PROPS as M, webkit as Mn, Note as Mt, PopupHandleAttachment as N, mac as Nn, Tip as Nt, usePopupHandleStore as O, ARROW_LEFT$1 as On, Callout as Ot, attachPreventUnmountOnClose as P, useBaseUiId as Pn, Warning as Pt, DROPDOWN_COLLISION_AVOIDANCE as Q, getWindow as Qn, imperativeAction as Qt, usePopupInteractionProps as R, cn as Rn, Badge$1 as Rt, popupTransitionStateMapping as S, getFloatingFocusElement as Sn, CodeGroup as St, useHoverReferenceInteraction as T, contains as Tn, useCompositeItem as Tt, useClick as U, useButton as Un, useOpenChangeComplete as Ut, ReactStore as V, EMPTY_OBJECT as Vn, resolveIcon as Vt, FloatingFocusManager as W, dispatchClickWithModifiers as Wn, useAnimationsFinished as Wt, FloatingTreeStore as X, useRefWithInit as Xn, escapeKey as Xt, useFloatingTree as Y, useStableCallback as Yn, cancelOpen as Yt, FloatingPortal as Z, getParentNode as Zn, focusOut as Zt, Tooltip as _, isIndexOutOfListBounds as _n, Collapsible as _t, DialogTitle as a, triggerPress as an, BrowserRouter as ar, Tabs as at, getDisabledMountTransitionStyles as b, isVirtualPointerEvent as bn, Column as bt, useOpenInteractionType as c, useDirection as cn, Route as cr, ResponseField as ct, useScrollLock as d, getTabbableBeforeElement as dn, useLocation as dr, ParamField as dt, listNavigation as en, isLastTraversableNode as er, enqueueFocus as et, DialogPortal$1 as f, isOutsideEvent as fn, useNavigate as fr, Link as ft, DialogBackdrop as g, isElementVisible as gn, Expandable as gt, DialogClose as h, getMinListIndex as hn, ABSOLUTE_URL_REGEX as hr, renderInlineMarkdown as ht, DialogContent as i, triggerHover as in, createLucideIcon as ir, Tab as it, PopupTriggerMap as j, jsdom as jn, Info as jt, createInitialPopupStoreState as k, ARROW_RIGHT$1 as kn, Check as kt, DialogTitle$1 as l, getNextTabbable as ln, Router as lr, PropertiesTable as lt, DialogPopup as m, getMaxListIndex as mn, parsePath as mr, Frame as mt, ZoomableImage as n, siblingOpen as nn, ChevronRight as nr, fastComponent as nt, DialogTrigger as o, CompositeList as on, Link$1 as or, Step as ot, InternalBackdrop as p, findNonDisabledListIndex as pn, createPath as pr, Icon as pt, useFloatingNodeId as q, mergeProps as qn, useAnimationFrame as qt, Dialog as r, triggerFocus as rn, Check$1 as rr, fastComponentRef as rt, DialogTrigger$1 as s, addEventListener as sn, Navigate as sr, Steps as st, docs_exports as t, outsidePress as tn, X as tr, FocusGuard as tt, DialogRoot as u, getTabbableAfterElement as un, Routes as ur, Param as ut, TooltipProvider as v, isListIndexDisabled as vn, CollapsibleContent as vt, safePolygon as w, activeElement as wn, inertValue as wt, useAnchorPositioning as x, stopEvent as xn, Columns as xt, usePositioner as y, isVirtualClick as yn, CollapsibleTrigger as yt, useTriggerDataForwarding as z, useRenderElement as zn, Accordion as zt };
@@ -1,3 +1,3 @@
1
- import { At as Info, Dt as Callout, Et as CardGroup, Ft as Button, It as Badge, Mt as Tip, Nt as Warning, Ot as Check, Pt as WarningBanner, Rt as Accordion, Tt as Card, _ as Tooltip, at as Tabs, bt as Columns, ct as ResponseField, dt as ParamField, ft as Icon, ht as Expandable, it as Tab, jt as Note, kt as Danger, lt as PropertiesTable, n as ZoomableImage, ot as Step, pt as Frame, st as Steps, ut as Param, xt as CodeGroup, yt as Column, zt as AccordionGroup } from "./chunks/docs.js";
1
+ import { At as Danger, Bt as AccordionGroup, Dt as CardGroup, Et as Card, Ft as WarningBanner, It as Button, Lt as Badge, Mt as Note, Nt as Tip, Ot as Callout, Pt as Warning, St as CodeGroup, _ as Tooltip, at as Tabs, bt as Column, ct as ResponseField, dt as ParamField, ft as Link, gt as Expandable, it as Tab, jt as Info, kt as Check, lt as PropertiesTable, mt as Frame, n as ZoomableImage, ot as Step, pt as Icon, st as Steps, ut as Param, xt as Columns, zt as Accordion } from "./chunks/docs.js";
2
2
 
3
- export { Accordion, AccordionGroup, Badge, Button, Callout, Card, CardGroup, Check, CodeGroup, Column, Columns, Danger, Expandable, Frame, Icon, Info, Note, Param, ParamField, PropertiesTable, ResponseField, Step, Steps, Tab, Tabs, Tip, Tooltip, Warning, WarningBanner, ZoomableImage };
3
+ export { Accordion, AccordionGroup, Badge, Button, Callout, Card, CardGroup, Check, CodeGroup, Column, Columns, Danger, Expandable, Frame, Icon, Info, Link, Note, Param, ParamField, PropertiesTable, ResponseField, Step, Steps, Tab, Tabs, Tip, Tooltip, Warning, WarningBanner, ZoomableImage };
@@ -1,4 +1,4 @@
1
- import { ir as BrowserRouter } from "./chunks/docs.js";
1
+ import { ar as BrowserRouter } from "./chunks/docs.js";
2
2
  import { m as BASE_URL, t as App } from "./chunks/App.js";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
 
@@ -1,4 +1,4 @@
1
- import { cr as Router, fr as createPath, mr as ABSOLUTE_URL_REGEX, pr as parsePath } from "./chunks/docs.js";
1
+ import { hr as ABSOLUTE_URL_REGEX, lr as Router, mr as parsePath, pr as createPath } from "./chunks/docs.js";
2
2
  import { a as docsSite, c as getSeo, d as getDocModule, f as getLastModified, h as toAbsoluteUrl, i as docsHomeUrl, l as siteName, m as BASE_URL, n as buildHead, o as getLocaleByPathname, p as getScopeForPage, r as renderHeadToString, s as getRedirects, t as App, u as standalonePages } from "./chunks/App.js";
3
3
  import * as React$1 from "react";
4
4
  import { jsx } from "react/jsx-runtime";
package/docs.schema.json CHANGED
@@ -279,6 +279,14 @@
279
279
  "allOf": [{ "$ref": "#/definitions/stringValue" }],
280
280
  "description": "Placeholder text for the search input."
281
281
  },
282
+ "position": {
283
+ "anyOf": [
284
+ { "type": "string", "enum": ["header", "sidebar"] },
285
+ { "$ref": "#/definitions/configRefValue" }
286
+ ],
287
+ "default": "header",
288
+ "description": "Where the search control renders: \"header\" (right side of the header) or \"sidebar\" (top of the navigation column; stays in the header on small screens). Themes that do not support a position fall back to \"header\"."
289
+ },
282
290
  "provider": {
283
291
  "allOf": [{ "$ref": "#/definitions/nonEmptyStringValue" }],
284
292
  "description": "Search provider id: \"local\" (default), \"pagefind\", or a provider registered at runtime."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umami/shiso",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "Open-source documentation framework for Markdown and MDX sites.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,12 +1,13 @@
1
1
  import { Link } from 'react-router';
2
2
  import { ContextualMenu } from '@/components/ContextualMenu';
3
- import { ChevronRight, FileText } from '@/components/icons';
3
+ import { ArrowLeft, ArrowRight, 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
7
  import { docsSite, getPageByPathname } from '@/lib/site-config';
8
8
  import { resolveContextualOptions } from '@/lib/site-model';
9
9
  import type { DocModule, NormalizedDocsPage, RelatedEntry, SiteModel } from '@/lib/types';
10
+ import { cn } from '@/lib/utils';
10
11
 
11
12
  interface RelatedLink {
12
13
  href: string;
@@ -148,9 +149,9 @@ export function DocContent({ page, doc, site }: DocContentProps) {
148
149
  </ul>
149
150
  </nav>
150
151
  )}
151
- <div className="mt-8 flex items-center justify-between" data-pagefind-ignore>
152
- <NavigationButton {...prev} isPrev />
153
- <NavigationButton {...next} />
152
+ <div className="mt-8 flex items-end justify-between" data-pagefind-ignore>
153
+ <NavigationButton {...prev} eyebrow={site.labels.previousPage} isPrev />
154
+ <NavigationButton {...next} eyebrow={site.labels.nextPage} />
154
155
  </div>
155
156
  </article>
156
157
  );
@@ -159,10 +160,13 @@ export function DocContent({ page, doc, site }: DocContentProps) {
159
160
  const NavigationButton = ({
160
161
  label,
161
162
  url,
163
+ eyebrow,
162
164
  isPrev,
163
165
  }: {
164
166
  label?: string;
165
167
  url?: string;
168
+ /** Direction caption ("Previous" / "Next") shown above the page title. */
169
+ eyebrow: string;
166
170
  isPrev?: boolean;
167
171
  }) => {
168
172
  if (!url || !label) {
@@ -172,19 +176,25 @@ const NavigationButton = ({
172
176
  return (
173
177
  <Link
174
178
  to={url}
175
- className="group my-3 inline-flex items-center gap-3 text-base font-bold text-foreground"
179
+ className={cn('group my-3 inline-flex items-end gap-3 text-base text-foreground', {
180
+ 'text-right': !isPrev,
181
+ })}
182
+ rel={isPrev ? 'prev' : 'next'}
176
183
  >
177
184
  {isPrev && (
178
- <ChevronRight
185
+ <ArrowLeft
179
186
  size={14}
180
- className="rotate-180 text-muted-foreground transition-colors group-hover:text-foreground"
187
+ className="mb-[0.3rem] text-muted-foreground transition-colors group-hover:text-foreground"
181
188
  />
182
189
  )}
183
- {label}
190
+ <span className="flex flex-col">
191
+ <span className="text-xs font-bold text-muted-foreground">{eyebrow}</span>
192
+ <span className="font-medium transition-colors group-hover:text-primary">{label}</span>
193
+ </span>
184
194
  {!isPrev && (
185
- <ChevronRight
195
+ <ArrowRight
186
196
  size={14}
187
- className="text-muted-foreground transition-colors group-hover:text-foreground"
197
+ className="mb-[0.3rem] text-muted-foreground transition-colors group-hover:text-foreground"
188
198
  />
189
199
  )}
190
200
  </Link>
@@ -5,6 +5,7 @@ import { Footer } from '@/components/Footer';
5
5
  import { Menu } from '@/components/icons';
6
6
  import { LanguageSwitcher } from '@/components/LanguageSwitcher';
7
7
  import { PageLinks } from '@/components/PageLinks';
8
+ import { SearchSlot } from '@/components/Search';
8
9
  import { SideNav } from '@/components/SideNav';
9
10
  import { Button } from '@/components/ui/button';
10
11
  import { Sheet, SheetContent, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
@@ -104,7 +105,8 @@ export function Docs({ page, doc, site }: DocsProps) {
104
105
  </SheetContent>
105
106
  </Sheet>
106
107
  <div className="flex items-start gap-12 lg:min-h-[calc(100dvh-var(--header-height))] lg:pt-6">
107
- <div className="hidden min-w-0 max-w-60 basis-60 self-start lg:sticky lg:top-[calc(var(--header-height)+1.5rem)] lg:block lg:h-[calc(100dvh-var(--header-height)-3rem)] lg:shrink-0">
108
+ <div className="hidden min-w-0 max-w-60 basis-60 flex-col gap-4 self-start lg:sticky lg:top-[calc(var(--header-height)+1.5rem)] lg:flex lg:h-[calc(100dvh-var(--header-height)-3rem)] lg:shrink-0">
109
+ <SearchSlot site={site} position="sidebar" className="w-full" />
108
110
  <SideNav
109
111
  tabs={tabs}
110
112
  navigation={navigation}
@@ -1,17 +1,12 @@
1
1
  import { Link, useLocation } from 'react-router';
2
2
  import { ConfiguredIcon } from '@/components/ConfiguredIcon';
3
3
  import { LanguageSwitcher } from '@/components/LanguageSwitcher';
4
- import { Search } from '@/components/Search';
4
+ import { SearchSlot } from '@/components/Search';
5
5
  import { ThemeToggle } from '@/components/ThemeToggle';
6
6
  import { TopNav } from '@/components/TopNav';
7
7
  import { VersionSwitcher } from '@/components/VersionSwitcher';
8
8
  import { isExternalHref } from '@/lib/paths';
9
- import {
10
- docsHomeUrl,
11
- getPageFrontmatter,
12
- getScopeByPathname,
13
- hasRootStandalonePage,
14
- } from '@/lib/site-config';
9
+ import { docsHomeUrl, getScopeByPathname, hasRootStandalonePage } from '@/lib/site-config';
15
10
  import type { NormalizedLink, SiteModel } from '@/lib/types';
16
11
 
17
12
  /**
@@ -63,11 +58,10 @@ function NavbarLinkItem({ link, primary = false }: { link: NormalizedLink; prima
63
58
  }
64
59
 
65
60
  export function Header({ site }: { site: SiteModel }) {
66
- const { logo, navbar, name, appearance, labels, search } = site;
61
+ const { logo, navbar, name, appearance, labels } = site;
67
62
  const { pathname } = useLocation();
68
63
  // The header renders the navigation of whichever scope owns the current page.
69
64
  const docs = getScopeByPathname(pathname).docs;
70
- const showSearch = getPageFrontmatter(pathname)?.search !== false;
71
65
  // The brand links to the standalone home page when one owns "/".
72
66
  const brandHref = logo?.href || (hasRootStandalonePage ? '/' : docsHomeUrl);
73
67
  const hasBrand = !!name || !!logo?.light || !!logo?.dark;
@@ -120,7 +114,10 @@ export function Header({ site }: { site: SiteModel }) {
120
114
  {docs.showTabs ? <TopNav docs={docs} label={labels.sections} /> : null}
121
115
  </div>
122
116
  <div className="flex min-w-0 items-center gap-2 justify-self-end">
123
- {showSearch ? <Search config={search} labels={labels} /> : null}
117
+ <SearchSlot site={site} position="header" />
118
+ {/* The sidebar collapses into a sheet on small screens, so its
119
+ search control moves up here until the column is visible. */}
120
+ <SearchSlot site={site} position="sidebar" className="lg:hidden" />
124
121
  {navbar?.links.map(link => (
125
122
  <NavbarLinkItem key={link.href} link={link} />
126
123
  ))}
@@ -17,7 +17,7 @@ export function Layout({ children, site }: { children: ReactNode; site: SiteMode
17
17
  <Banner banner={site.banner} dismissLabel={site.labels.dismissBanner} />
18
18
  <Header site={site} />
19
19
  <div className="mx-auto flex min-h-0 w-full max-w-[1600px] grow flex-col px-5">
20
- <main className="min-h-0 grow">{children}</main>
20
+ <main className="flex min-h-0 grow flex-col">{children}</main>
21
21
  </div>
22
22
  </div>
23
23
  );
@@ -14,8 +14,9 @@ import { Dialog, DialogContent, DialogTitle, DialogTrigger } from '@/components/
14
14
  import { highlightTerms, type SearchResult } from '@/lib/search';
15
15
  import type { ResolvedSearchConfig } from '@/lib/search/config';
16
16
  import { resolveSearchProvider, type SearchProvider } from '@/lib/search/provider';
17
- import { getScopeByPathname } from '@/lib/site-config';
18
- import type { ThemeLabels } from '@/lib/types';
17
+ import { getPageFrontmatter, getScopeByPathname } from '@/lib/site-config';
18
+ import type { SearchPosition, SiteModel, ThemeLabels } from '@/lib/types';
19
+ import { cn } from '@/lib/utils';
19
20
 
20
21
  /** Enough results to make the list scroll; the dialog caps its own height. */
21
22
  const RESULT_LIMIT = 30;
@@ -54,7 +55,16 @@ function renderWithQueryHighlight(text: string, query: string) {
54
55
  * Provider-neutral search dialog. The selected provider and its index or
55
56
  * client are loaded on demand, so search stays out of the initial bundle.
56
57
  */
57
- export function Search({ config, labels }: { config: ResolvedSearchConfig; labels: ThemeLabels }) {
58
+ export function Search({
59
+ config,
60
+ labels,
61
+ className,
62
+ }: {
63
+ config: ResolvedSearchConfig;
64
+ labels: ThemeLabels;
65
+ /** Extra classes for the trigger button (e.g. to stretch it in a column). */
66
+ className?: string;
67
+ }) {
58
68
  const navigate = useNavigate();
59
69
  const { pathname } = useLocation();
60
70
  const [open, setOpen] = useState(false);
@@ -179,12 +189,15 @@ export function Search({ config, labels }: { config: ResolvedSearchConfig; label
179
189
  render={
180
190
  <Button
181
191
  variant="outline"
182
- className="h-auto gap-2 rounded-md bg-card px-2.5 py-1.5 text-muted-foreground hover:border-input hover:bg-card hover:text-foreground"
192
+ className={cn(
193
+ 'h-auto gap-2 rounded-md bg-card px-2.5 py-1.5 text-muted-foreground hover:border-input hover:bg-card hover:text-foreground',
194
+ className,
195
+ )}
183
196
  />
184
197
  }
185
198
  >
186
199
  <SearchIcon className="size-3.5" />
187
- <span className="min-w-24 text-left">{config.prompt}</span>
200
+ <span className="min-w-24 grow text-left">{config.prompt}</span>
188
201
  {config.shortcut ? (
189
202
  <kbd className="rounded-sm border border-border bg-muted px-[0.3rem] py-[0.05rem] text-[0.7rem] font-sans">
190
203
  {config.shortcutLabel}
@@ -253,3 +266,30 @@ export function Search({ config, labels }: { config: ResolvedSearchConfig; label
253
266
  </Dialog>
254
267
  );
255
268
  }
269
+
270
+ /**
271
+ * Renders the search control only when `search.position` targets this slot.
272
+ * Layout components drop one of these into each position they support; the
273
+ * per-page `search: false` frontmatter flag is honored here as well.
274
+ */
275
+ export function SearchSlot({
276
+ site,
277
+ position,
278
+ className,
279
+ }: {
280
+ site: SiteModel;
281
+ position: SearchPosition;
282
+ className?: string;
283
+ }) {
284
+ const { pathname } = useLocation();
285
+
286
+ if (!site.search.enabled || site.search.position !== position) {
287
+ return null;
288
+ }
289
+
290
+ if (getPageFrontmatter(pathname)?.search === false) {
291
+ return null;
292
+ }
293
+
294
+ return <Search config={site.search} labels={site.labels} className={className} />;
295
+ }
@@ -1,4 +1,3 @@
1
- import { cn } from '@/lib/utils';
2
1
  import { type ReactNode, useEffect, useState } from 'react';
3
2
  import { Link, useLocation, useNavigate } from 'react-router';
4
3
  import { resolveIcon } from '@/components/docs/utils';
@@ -9,9 +8,9 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/component
9
8
  import { ScrollArea } from '@/components/ui/scroll-area';
10
9
  import { flattenNav, isNodeHidden } from '@/lib/docs-config';
11
10
  import type { DocsTab, NavGroupNode, NavNode } from '@/lib/types';
11
+ import { cn } from '@/lib/utils';
12
12
 
13
- const sectionLabelClass =
14
- 'flex min-w-0 items-center gap-[0.4rem] pb-2 pr-1 font-bold text-inherit';
13
+ const sectionLabelClass = 'flex min-w-0 items-center gap-[0.4rem] pb-2 pr-1 font-bold text-inherit';
15
14
  const groupLabelClass =
16
15
  'flex min-w-0 items-center gap-[0.4rem] py-2 pl-3 pr-1 font-medium text-inherit';
17
16
  const selectedClass =
@@ -319,7 +318,7 @@ export function SideNav({
319
318
  const nodes = navigation[activeTabId] || navigation[tabs[0]?.id] || [];
320
319
 
321
320
  return (
322
- <ScrollArea className={cn('w-full max-w-full', { 'h-full': isSticky })}>
321
+ <ScrollArea className={cn('w-full max-w-full', { 'min-h-0 grow': isSticky })}>
323
322
  <nav className="flex w-full flex-col gap-6 pr-4 text-sm" aria-label={navigationLabel}>
324
323
  {anchors.length ? (
325
324
  <NavNodes
@@ -0,0 +1,14 @@
1
+ import type { ComponentProps } from 'react';
2
+ import { Link as RouterLink } from 'react-router';
3
+
4
+ export type LinkProps = ComponentProps<typeof RouterLink>;
5
+
6
+ /**
7
+ * Client-side navigation link bound to Shiso's router. Exposed from
8
+ * `@umami/shiso/components` so TSX standalone pages and other app code can
9
+ * navigate without reloading (importing `react-router` directly would create a
10
+ * second router instance without Shiso's context).
11
+ */
12
+ export function Link(props: LinkProps) {
13
+ return <RouterLink {...props} />;
14
+ }
@@ -8,6 +8,7 @@ export * from './Columns';
8
8
  export * from './Expandable';
9
9
  export * from './Frame';
10
10
  export * from './Icon';
11
+ export * from './Link';
11
12
  export * from './ParamField';
12
13
  export * from './PropertiesTable';
13
14
  export * from './ResponseField';
@@ -1,4 +1,6 @@
1
1
  export {
2
+ ArrowLeft,
3
+ ArrowRight,
2
4
  Check,
3
5
  Check as CheckIcon,
4
6
  ChevronRight,
@@ -1,13 +1,39 @@
1
- import type { SearchConfig } from '@/lib/types';
1
+ import type { SearchConfig, SearchPosition } from '@/lib/types';
2
2
 
3
3
  export const DEFAULT_SEARCH_PROMPT = 'Search...';
4
4
  export const DEFAULT_SEARCH_PROVIDER = 'local';
5
5
  export const DEFAULT_SEARCH_SHORTCUT = 'k';
6
6
  export const DEFAULT_SEARCH_SHORTCUT_LABEL = 'Ctrl K';
7
+ export const DEFAULT_SEARCH_POSITION: SearchPosition = 'header';
8
+
9
+ /** Positions the built-in theme knows how to render. */
10
+ export const SEARCH_POSITIONS: readonly SearchPosition[] = ['header', 'sidebar'];
11
+
12
+ /**
13
+ * Themes must always support "header", so anything unrecognized (or not
14
+ * implemented by the active theme) lands there instead of vanishing.
15
+ */
16
+ export function resolveSearchPosition(
17
+ position: unknown,
18
+ supported: readonly SearchPosition[] = SEARCH_POSITIONS,
19
+ ): SearchPosition {
20
+ if (typeof position === 'string' && (supported as readonly string[]).includes(position)) {
21
+ return position as SearchPosition;
22
+ }
23
+
24
+ if (position !== undefined && position !== DEFAULT_SEARCH_POSITION) {
25
+ console.warn(
26
+ `[shiso] Unsupported search.position "${String(position)}" — using "${DEFAULT_SEARCH_POSITION}".`,
27
+ );
28
+ }
29
+
30
+ return DEFAULT_SEARCH_POSITION;
31
+ }
7
32
 
8
33
  export interface ResolvedSearchConfig {
9
34
  enabled: boolean;
10
35
  prompt: string;
36
+ position: SearchPosition;
11
37
  provider: string;
12
38
  options: Record<string, unknown>;
13
39
  shortcut: string | false;
@@ -22,6 +48,7 @@ export function resolveSearchConfig(
22
48
  return {
23
49
  enabled: false,
24
50
  prompt: DEFAULT_SEARCH_PROMPT,
51
+ position: DEFAULT_SEARCH_POSITION,
25
52
  provider: DEFAULT_SEARCH_PROVIDER,
26
53
  options: {},
27
54
  shortcut: false,
@@ -32,6 +59,7 @@ export function resolveSearchConfig(
32
59
  return {
33
60
  enabled: true,
34
61
  prompt: config?.prompt?.trim() || DEFAULT_SEARCH_PROMPT,
62
+ position: resolveSearchPosition(config?.position),
35
63
  provider: config?.provider?.trim().toLowerCase() || DEFAULT_SEARCH_PROVIDER,
36
64
  options: config?.options || {},
37
65
  shortcut:
@@ -29,6 +29,8 @@ const SHISO_THEME_LABELS: ThemeLabels = {
29
29
  noResults: 'No results',
30
30
  lastUpdated: 'Last updated on',
31
31
  relatedTopics: 'Related topics',
32
+ previousPage: 'Previous',
33
+ nextPage: 'Next',
32
34
  notFound: 'Page not found',
33
35
  dismissBanner: 'Dismiss banner',
34
36
  toggleTheme: 'Toggle theme',
package/src/lib/types.ts CHANGED
@@ -255,9 +255,20 @@ export interface FontsConfig extends FontSpec {
255
255
  body?: FontSpec;
256
256
  }
257
257
 
258
+ /**
259
+ * Named slots a theme can place the search control in. Every theme must
260
+ * support "header"; unsupported positions fall back to it.
261
+ */
262
+ export type SearchPosition = 'header' | 'sidebar';
263
+
258
264
  export interface SearchConfig {
259
265
  /** Placeholder text for the search input. */
260
266
  prompt?: string;
267
+ /**
268
+ * Where the search control renders: "header" (default, right side of the
269
+ * header) or "sidebar" (top of the navigation column).
270
+ */
271
+ position?: SearchPosition;
261
272
  /** Provider id: "local" (default), "pagefind", or a runtime-registered id. */
262
273
  provider?: string;
263
274
  /** Provider-specific configuration. */
@@ -487,6 +498,8 @@ export interface ThemeLabels {
487
498
  noResults: string;
488
499
  lastUpdated: string;
489
500
  relatedTopics: string;
501
+ previousPage: string;
502
+ nextPage: string;
490
503
  notFound: string;
491
504
  dismissBanner: string;
492
505
  toggleTheme: string;
@@ -555,7 +568,7 @@ export interface DocFrontmatter {
555
568
  title?: string;
556
569
  description?: string;
557
570
  noindex?: boolean;
558
- /** Hide the header search control and disable its shortcut on this page. */
571
+ /** Hide the search control and disable its shortcut on this page. */
559
572
  search?: false;
560
573
  /** Overrides the site-wide `metadata.timestamp` setting for this page. */
561
574
  timestamp?: boolean;
@@ -31,7 +31,7 @@ export function StandalonePageView({ page, site }: { page: StandalonePage; site:
31
31
  const isComponentPage = page.filePath.endsWith('.tsx');
32
32
 
33
33
  return (
34
- <div className="flex min-h-full flex-col">
34
+ <div className="flex grow flex-col">
35
35
  {isComponentPage ? (
36
36
  <div className="grow">
37
37
  <Content />
@@ -1,4 +1,5 @@
1
- import type { ImgHTMLAttributes, ReactElement, ReactNode } from 'react';
1
+ import type { ComponentProps, ImgHTMLAttributes, ReactElement, ReactNode } from 'react';
2
+ import type { Link as RouterLink } from 'react-router';
2
3
 
3
4
  export interface AccordionProps {
4
5
  title?: ReactNode;
@@ -215,3 +216,7 @@ export interface ZoomableImageProps extends ImgHTMLAttributes<HTMLImageElement>
215
216
  }
216
217
 
217
218
  export declare function ZoomableImage(props: ZoomableImageProps): ReactElement;
219
+
220
+ /** Client-side navigation link bound to Shiso's router. */
221
+ export type LinkProps = ComponentProps<typeof RouterLink>;
222
+ export declare function Link(props: LinkProps): ReactElement;
package/vite.config.ts CHANGED
@@ -434,6 +434,13 @@ export default defineConfig(async () => {
434
434
  // virtual imports that are resolved by the plugins below.
435
435
  exclude: ['@umami/shiso'],
436
436
  },
437
+ ssr: {
438
+ // App code may import framework entry points directly (e.g.
439
+ // "@umami/shiso/components" from TSX standalone pages). Bundle them in
440
+ // the SSR build so project-time virtual imports resolve through the
441
+ // aliases below instead of failing in Node during prerender.
442
+ noExternal: ['@umami/shiso'],
443
+ },
437
444
  plugins: [
438
445
  configModule.plugin,
439
446
  tailwindcss(),