@umami/shiso 1.6.0 → 1.7.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
@@ -3,6 +3,15 @@
3
3
  All notable changes to `@umami/shiso` are documented here. This project follows
4
4
  [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## Unreleased
7
+
8
+ ### Added
9
+
10
+ - Link tabs: a navigation tab may define `href` instead of `groups`/`pages`/
11
+ `dropdowns` to link to a standalone page or external URL from the top
12
+ navigation. Internal hrefs route client-side and highlight when active;
13
+ external URLs open in a new browser tab.
14
+
6
15
  ## 2.0.0 - 2026-08-21
7
16
 
8
17
  ### Breaking
@@ -9532,7 +9532,7 @@ function ScrollArea({ className, children, scrollbars = "vertical", ...props })
9532
9532
  children: [
9533
9533
  /* @__PURE__ */ jsx(ScrollAreaViewport, {
9534
9534
  "data-slot": "scroll-area-viewport",
9535
- className: "box-border h-full w-full max-w-full rounded-[inherit] overscroll-contain outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1",
9535
+ className: cn("box-border h-full w-full max-w-full rounded-[inherit] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1", scrollbars === "both" ? "overscroll-contain" : hasHorizontalScrollbar ? "overscroll-x-contain" : "overscroll-y-contain"),
9536
9536
  children: /* @__PURE__ */ jsx(ScrollAreaContent, {
9537
9537
  "data-slot": "scroll-area-content",
9538
9538
  className: cn("box-border min-w-0", hasHorizontalScrollbar ? "w-max min-w-full max-w-none" : "w-full max-w-full"),
@@ -10969,7 +10969,7 @@ function AccordionItem({ className, ...props }) {
10969
10969
  }
10970
10970
  function AccordionTrigger({ className, children, ...props }) {
10971
10971
  return /* @__PURE__ */ jsx(AccordionHeader, {
10972
- className: "m-0 flex",
10972
+ className: "flex",
10973
10973
  children: /* @__PURE__ */ jsxs(AccordionTrigger$1, {
10974
10974
  "data-slot": "accordion-trigger",
10975
10975
  className: cn("group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground", className),
@@ -11004,11 +11004,11 @@ function AccordionContent({ className, children, ...props }) {
11004
11004
  //#region src/components/docs/styles.ts
11005
11005
  /** Shared Tailwind utility groups for the built-in MDX components. */
11006
11006
  const styles = {
11007
- accordion: "my-3",
11008
- accordionGroup: "my-4 overflow-hidden rounded-lg border border-border [&_[data-slot=accordion-item]]:rounded-none [&_[data-slot=accordion-item]]:border-0 [&_[data-slot=accordion-item]+[data-slot=accordion-item]]:border-t [&_[data-slot=accordion-trigger]]:rounded-none",
11009
- accordionItem: "rounded-lg border border-border bg-transparent",
11010
- accordionTrigger: "items-center px-4 py-3 text-base font-semibold text-foreground hover:no-underline",
11011
- accordionContent: "px-4 pb-4 text-base leading-7",
11007
+ accordion: "my-3 [&_h3:has(>[data-slot=accordion-trigger])]:m-0",
11008
+ accordionGroup: "my-4 [&_h3:has(>[data-slot=accordion-trigger])]:m-0",
11009
+ accordionItem: "",
11010
+ accordionTrigger: "text-foreground",
11011
+ accordionContent: "text-muted-foreground [&_p]:mt-0 [&_p:last-child]:mb-0",
11012
11012
  expandableTrigger: "items-center px-4 py-3 text-sm font-normal text-foreground hover:no-underline",
11013
11013
  callout: "my-4 flex items-start gap-3 rounded-lg border px-3 py-2.5 text-sm leading-6",
11014
11014
  calloutIcon: "flex h-6 w-5 shrink-0 items-center justify-center [&_svg]:block [&_svg]:size-4",
@@ -11021,15 +11021,15 @@ const styles = {
11021
11021
  tip: "border-green-300 bg-green-50 text-green-900 dark:border-green-400/40 dark:bg-green-400/10 dark:text-green-300",
11022
11022
  check: "border-emerald-300 bg-emerald-50 text-emerald-900 dark:border-emerald-400/40 dark:bg-emerald-400/10 dark:text-emerald-300",
11023
11023
  danger: "border-destructive/40 bg-destructive/10 text-destructive",
11024
- card: "block h-full gap-0 rounded-lg border border-border bg-transparent p-4 text-base ring-0 hover:border-input",
11025
- cardHorizontal: "[&_[data-slot=card-inner]]:items-center [&_[data-slot=card-main]]:flex-row [&_[data-slot=card-main]]:items-center [&_[data-slot=card-main]]:gap-3 [&_[data-slot=card-body]]:m-0",
11024
+ card: "block h-full gap-0 rounded-lg border border-border bg-transparent p-4 text-base ring-0 hover:border-primary",
11025
+ cardHorizontal: "[&_[data-slot=card-inner]]:items-center [&_[data-slot=card-main]]:flex-row [&_[data-slot=card-main]]:items-center [&_[data-slot=card-main]]:gap-3 [&_[data-slot=card-body]]:m-0 [&_[data-slot=card-header]]:flex-row [&_[data-slot=card-header]]:items-center [&_[data-slot=card-header]]:gap-2",
11026
11026
  cardTyped: "[&_[data-slot=card-title]]:text-inherit [&_[data-slot=card-body]]:text-inherit [&_[data-slot=card-cta]]:text-inherit [&_[data-slot=card-arrow]]:text-inherit",
11027
11027
  cardImageLayout: "p-0 [&_[data-slot=card-inner]]:p-6",
11028
11028
  cardImage: "block aspect-video w-full border-border border-b object-cover",
11029
11029
  cardInner: "flex items-start justify-between gap-3",
11030
11030
  cardMain: "flex min-w-0 flex-col gap-2",
11031
- cardHeader: "flex items-center gap-2",
11032
- cardIcon: "inline-flex shrink-0 items-center justify-center [&_img]:size-4 [&_svg]:size-4",
11031
+ cardHeader: "flex flex-col items-start gap-3",
11032
+ cardIcon: "inline-flex shrink-0 items-center justify-center [&_img]:size-6 [&_svg]:size-6",
11033
11033
  cardTitle: "text-base font-semibold text-foreground",
11034
11034
  cardBody: "text-muted-foreground [&>:first-child]:mt-0 [&>:last-child]:mb-0",
11035
11035
  cardCta: "flex shrink-0 items-center gap-[0.35rem] text-[0.9rem] font-medium text-muted-foreground",
@@ -11493,12 +11493,12 @@ function CardContent({ className, ...props }) {
11493
11493
  //#endregion
11494
11494
  //#region src/components/docs/Card.tsx
11495
11495
  const CARD_TYPE_ICONS = {
11496
- note: /* @__PURE__ */ jsx(Info$1, { size: 16 }),
11497
- info: /* @__PURE__ */ jsx(Info$1, { size: 16 }),
11498
- warning: /* @__PURE__ */ jsx(TriangleAlert, { size: 16 }),
11499
- tip: /* @__PURE__ */ jsx(Lightbulb, { size: 16 }),
11500
- check: /* @__PURE__ */ jsx(Check$1, { size: 16 }),
11501
- danger: /* @__PURE__ */ jsx(CircleAlert, { size: 16 })
11496
+ note: /* @__PURE__ */ jsx(Info$1, { size: 24 }),
11497
+ info: /* @__PURE__ */ jsx(Info$1, { size: 24 }),
11498
+ warning: /* @__PURE__ */ jsx(TriangleAlert, { size: 24 }),
11499
+ tip: /* @__PURE__ */ jsx(Lightbulb, { size: 24 }),
11500
+ check: /* @__PURE__ */ jsx(Check$1, { size: 24 }),
11501
+ danger: /* @__PURE__ */ jsx(CircleAlert, { size: 24 })
11502
11502
  };
11503
11503
  const CARD_TYPE_HOVER = {
11504
11504
  note: "hover:border-border",
@@ -11509,7 +11509,7 @@ const CARD_TYPE_HOVER = {
11509
11509
  danger: "hover:border-destructive/40"
11510
11510
  };
11511
11511
  function DocsCardContent({ title, icon, color, img, cta, arrow, type, children }) {
11512
- const resolvedIcon = resolveIcon(icon, 16) || (type ? CARD_TYPE_ICONS[type] : null);
11512
+ const resolvedIcon = resolveIcon(icon, 24) || (type ? CARD_TYPE_ICONS[type] : null);
11513
11513
  return /* @__PURE__ */ jsxs(CardContent, {
11514
11514
  className: "p-0",
11515
11515
  children: [img ? /* @__PURE__ */ jsx("img", {
@@ -11524,6 +11524,7 @@ function DocsCardContent({ title, icon, color, img, cta, arrow, type, children }
11524
11524
  "data-slot": "card-main",
11525
11525
  children: [/* @__PURE__ */ jsxs("div", {
11526
11526
  className: styles.cardHeader,
11527
+ "data-slot": "card-header",
11527
11528
  children: [resolvedIcon ? /* @__PURE__ */ jsx("span", {
11528
11529
  className: `${styles.cardIcon} ${type ? "text-inherit" : "text-primary"}`,
11529
11530
  style: color ? { color } : void 0,
@@ -22650,7 +22651,9 @@ function normalizeScope(name, source, anchors, resolveDocFile, docsPrefix) {
22650
22651
  });
22651
22652
  });
22652
22653
  if (Array.isArray(tab.pages) && tab.pages.length) nodes.push(...collectPages(tab.pages, context, state));
22653
- if (state.pages.length === pagesBefore) throw new Error(`Invalid docs config: tab "${tabLabel}" does not contain any supported page entries.`);
22654
+ const isLinkTab = typeof tab.href === "string" && !!tab.href;
22655
+ if (isLinkTab && state.pages.length !== pagesBefore) throw new Error(`Invalid docs config: tab "${tabLabel}" cannot define both "href" and page entries.`);
22656
+ if (!isLinkTab && state.pages.length === pagesBefore) throw new Error(`Invalid docs config: tab "${tabLabel}" does not contain any supported page entries.`);
22654
22657
  treeByTab.set(tabId, nodes);
22655
22658
  });
22656
22659
  const pending = state.pages;
@@ -22723,13 +22726,15 @@ function normalizeScope(name, source, anchors, resolveDocFile, docsPrefix) {
22723
22726
  tabs: tabs.map((tab, index) => {
22724
22727
  const tabId = tabIds[index];
22725
22728
  const firstPage = firstVisiblePageByTab.get(tabId);
22729
+ const href = typeof tab.href === "string" && tab.href ? tab.href : void 0;
22726
22730
  return {
22727
22731
  id: tabId,
22728
22732
  label: tab.tab?.trim() || "",
22729
- url: firstPage?.url || pageToUrl("index", docsPrefix),
22733
+ url: href || firstPage?.url || pageToUrl("index", docsPrefix),
22730
22734
  icon: tab.icon,
22731
22735
  presentation: tab.presentation || "tab",
22732
- hidden: tab.hidden || void 0
22736
+ hidden: tab.hidden || void 0,
22737
+ link: href ? true : void 0
22733
22738
  };
22734
22739
  }),
22735
22740
  showTabs,
@@ -23031,7 +23036,8 @@ function resolveSiteModel(config, docs, shiso) {
23031
23036
  light: config.logo.light || config.logo.dark,
23032
23037
  dark: config.logo.dark || config.logo.light,
23033
23038
  href: config.logo.href,
23034
- target: config.logo.href ? resolveLinkTarget(config.logo.href, config.logo.target) : void 0
23039
+ target: config.logo.href ? resolveLinkTarget(config.logo.href, config.logo.target) : void 0,
23040
+ invert: config.logo.invert === true
23035
23041
  } : null;
23036
23042
  return {
23037
23043
  name: config.name?.trim() || void 0,
@@ -26344,6 +26350,13 @@ function TopNav({ docs, label }) {
26344
26350
  }, item.href))
26345
26351
  })] }, tab.id);
26346
26352
  }
26353
+ if (tab.link && isExternalHref(tab.url)) return /* @__PURE__ */ jsxs("a", {
26354
+ href: tab.url,
26355
+ className: tabClass(tab),
26356
+ target: "_blank",
26357
+ rel: "noreferrer",
26358
+ children: [/* @__PURE__ */ jsx(ConfiguredIcon, { icon: tab.icon }), tab.label]
26359
+ }, tab.id);
26347
26360
  return /* @__PURE__ */ jsxs(Link, {
26348
26361
  to: tab.url,
26349
26362
  className: tabClass(tab),
@@ -26426,19 +26439,19 @@ function Header({ site }) {
26426
26439
  const brandHref = logo?.href || (hasRootStandalonePage ? "/" : docsHomeUrl);
26427
26440
  const hasBrand = !!name || !!logo?.light || !!logo?.dark;
26428
26441
  const brandClassName = "inline-flex items-center gap-2 text-xl font-bold text-foreground tracking-[-0.03em]";
26429
- const brandContent = /* @__PURE__ */ jsxs(Fragment, { children: [
26430
- logo?.light ? /* @__PURE__ */ jsx("img", {
26431
- src: logo.light,
26432
- alt: "",
26433
- className: "h-6 w-auto dark:hidden"
26434
- }) : null,
26435
- logo?.dark ? /* @__PURE__ */ jsx("img", {
26436
- src: logo.dark,
26437
- alt: "",
26438
- className: "hidden h-6 w-auto dark:block"
26439
- }) : null,
26440
- name ? /* @__PURE__ */ jsx("span", { children: name }) : null
26441
- ] });
26442
+ const brandContent = /* @__PURE__ */ jsxs(Fragment, { children: [logo?.invert && logo.light ? /* @__PURE__ */ jsx("img", {
26443
+ src: logo.light,
26444
+ alt: "",
26445
+ className: "h-6 w-auto dark:brightness-0 dark:invert"
26446
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [logo?.light ? /* @__PURE__ */ jsx("img", {
26447
+ src: logo.light,
26448
+ alt: "",
26449
+ className: "h-6 w-auto dark:hidden"
26450
+ }) : null, logo?.dark ? /* @__PURE__ */ jsx("img", {
26451
+ src: logo.dark,
26452
+ alt: "",
26453
+ className: "hidden h-6 w-auto dark:block"
26454
+ }) : null] }), name ? /* @__PURE__ */ jsx("span", { children: name }) : null] });
26442
26455
  return /* @__PURE__ */ jsx("header", {
26443
26456
  className: "sticky top-0 z-50 h-[var(--header-height)] shrink-0 border-border border-b bg-[color-mix(in_srgb,var(--background)_92%,transparent)] backdrop-blur-md",
26444
26457
  children: /* @__PURE__ */ jsxs("div", {
package/docs.schema.json CHANGED
@@ -66,6 +66,10 @@
66
66
  "target": {
67
67
  "allOf": [{ "$ref": "#/definitions/targetValue" }],
68
68
  "description": "Browsing context for the logo link. Defaults from href."
69
+ },
70
+ "invert": {
71
+ "allOf": [{ "$ref": "#/definitions/booleanValue" }],
72
+ "description": "Render one logo and force it to white in dark mode with a CSS filter, instead of swapping to a dark variant. Best for monochrome logos."
69
73
  }
70
74
  }
71
75
  }
@@ -814,7 +818,8 @@
814
818
  "type": "object",
815
819
  "allOf": [
816
820
  { "anyOf": [{ "required": ["$ref"] }, { "required": ["tab"] }] },
817
- { "anyOf": [{ "required": ["$ref"] }, { "required": ["groups"] }, { "required": ["pages"] }, { "required": ["dropdowns"] }] }
821
+ { "anyOf": [{ "required": ["$ref"] }, { "required": ["groups"] }, { "required": ["pages"] }, { "required": ["dropdowns"] }, { "required": ["href"] }] },
822
+ { "not": { "anyOf": [{ "required": ["href", "groups"] }, { "required": ["href", "pages"] }, { "required": ["href", "dropdowns"] }] } }
818
823
  ],
819
824
  "additionalProperties": false,
820
825
  "properties": {
@@ -823,6 +828,7 @@
823
828
  "groups": { "$ref": "#/definitions/groupList" },
824
829
  "pages": { "$ref": "#/definitions/pageItemList" },
825
830
  "dropdowns": { "$ref": "#/definitions/dropdownList" },
831
+ "href": { "allOf": [{ "$ref": "#/definitions/nonEmptyStringValue" }], "description": "Link tab: navigates to this URL (a standalone page or external site) instead of owning docs pages. Cannot be combined with groups, pages, or dropdowns." },
826
832
  "icon": { "allOf": [{ "$ref": "#/definitions/stringValue" }], "description": "Icon name shown beside the tab label." },
827
833
  "hidden": { "allOf": [{ "$ref": "#/definitions/booleanValue" }], "description": "Hide the tab from the top navigation." }
828
834
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umami/shiso",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Open-source documentation framework for Markdown and MDX sites.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -69,8 +69,18 @@ export function Header({ site }: { site: SiteModel }) {
69
69
  'inline-flex items-center gap-2 text-xl font-bold text-foreground tracking-[-0.03em]';
70
70
  const brandContent = (
71
71
  <>
72
- {logo?.light ? <img src={logo.light} alt="" className="h-6 w-auto dark:hidden" /> : null}
73
- {logo?.dark ? <img src={logo.dark} alt="" className="hidden h-6 w-auto dark:block" /> : null}
72
+ {/* With `invert`, one logo serves both modes: a CSS filter forces it to
73
+ white in dark mode instead of swapping to a dark variant. */}
74
+ {logo?.invert && logo.light ? (
75
+ <img src={logo.light} alt="" className="h-6 w-auto dark:brightness-0 dark:invert" />
76
+ ) : (
77
+ <>
78
+ {logo?.light ? <img src={logo.light} alt="" className="h-6 w-auto dark:hidden" /> : null}
79
+ {logo?.dark ? (
80
+ <img src={logo.dark} alt="" className="hidden h-6 w-auto dark:block" />
81
+ ) : null}
82
+ </>
83
+ )}
74
84
  {name ? <span>{name}</span> : null}
75
85
  </>
76
86
  );
@@ -7,6 +7,7 @@ import {
7
7
  DropdownMenuItem,
8
8
  DropdownMenuTrigger,
9
9
  } from '@/components/ui/dropdown-menu';
10
+ import { isExternalHref } from '@/lib/paths';
10
11
  import { getStandalonePage } from '@/lib/site-config';
11
12
  import type { DocsTab, LinkTarget, NavNode, NormalizedDocsConfig } from '@/lib/types';
12
13
  import { cn } from '@/lib/utils';
@@ -112,6 +113,23 @@ export function TopNav({ docs, label }: { docs: NormalizedDocsConfig; label: str
112
113
  );
113
114
  }
114
115
 
116
+ // Link tabs may point outside the app; render external URLs as plain
117
+ // anchors so the browser handles them.
118
+ if (tab.link && isExternalHref(tab.url)) {
119
+ return (
120
+ <a
121
+ key={tab.id}
122
+ href={tab.url}
123
+ className={tabClass(tab)}
124
+ target="_blank"
125
+ rel="noreferrer"
126
+ >
127
+ <ConfiguredIcon icon={tab.icon} />
128
+ {tab.label}
129
+ </a>
130
+ );
131
+ }
132
+
115
133
  return (
116
134
  <Link
117
135
  key={tab.id}
@@ -28,12 +28,12 @@ export interface CardProps {
28
28
  export type CardType = 'note' | 'info' | 'warning' | 'tip' | 'check' | 'danger';
29
29
 
30
30
  const CARD_TYPE_ICONS: Record<CardType, ReactNode> = {
31
- note: <InfoIcon size={16} />,
32
- info: <InfoIcon size={16} />,
33
- warning: <TriangleAlert size={16} />,
34
- tip: <Lightbulb size={16} />,
35
- check: <CheckIcon size={16} />,
36
- danger: <CircleAlert size={16} />,
31
+ note: <InfoIcon size={24} />,
32
+ info: <InfoIcon size={24} />,
33
+ warning: <TriangleAlert size={24} />,
34
+ tip: <Lightbulb size={24} />,
35
+ check: <CheckIcon size={24} />,
36
+ danger: <CircleAlert size={24} />,
37
37
  };
38
38
 
39
39
  const CARD_TYPE_HOVER: Record<CardType, string> = {
@@ -55,14 +55,14 @@ function DocsCardContent({
55
55
  type,
56
56
  children,
57
57
  }: Omit<CardProps, 'href'>) {
58
- const resolvedIcon = resolveIcon(icon, 16) || (type ? CARD_TYPE_ICONS[type] : null);
58
+ const resolvedIcon = resolveIcon(icon, 24) || (type ? CARD_TYPE_ICONS[type] : null);
59
59
 
60
60
  return (
61
61
  <CardContent className="p-0">
62
62
  {img ? <img src={img} alt="" className={styles.cardImage} /> : null}
63
63
  <div className={styles.cardInner} data-slot="card-inner">
64
64
  <div className={styles.cardMain} data-slot="card-main">
65
- <div className={styles.cardHeader}>
65
+ <div className={styles.cardHeader} data-slot="card-header">
66
66
  {resolvedIcon ? (
67
67
  <span
68
68
  className={`${styles.cardIcon} ${type ? 'text-inherit' : 'text-primary'}`}
@@ -1,12 +1,14 @@
1
1
  /** Shared Tailwind utility groups for the built-in MDX components. */
2
2
  export const styles = {
3
- accordion: 'my-3',
4
- accordionGroup:
5
- 'my-4 overflow-hidden rounded-lg border border-border [&_[data-slot=accordion-item]]:rounded-none [&_[data-slot=accordion-item]]:border-0 [&_[data-slot=accordion-item]+[data-slot=accordion-item]]:border-t [&_[data-slot=accordion-trigger]]:rounded-none',
6
- accordionItem: 'rounded-lg border border-border bg-transparent',
7
- accordionTrigger:
8
- 'items-center px-4 py-3 text-base font-semibold text-foreground hover:no-underline',
9
- accordionContent: 'px-4 pb-4 text-base leading-7',
3
+ /* Accordions keep the native shadcn look: no outer box, hairline separators,
4
+ small medium-weight triggers with hover underline. Only margins are added.
5
+ The trigger header is an <h3> and panel text renders as <p>, so the
6
+ .docs-markdown typography margins must be cancelled for those elements. */
7
+ accordion: 'my-3 [&_h3:has(>[data-slot=accordion-trigger])]:m-0',
8
+ accordionGroup: 'my-4 [&_h3:has(>[data-slot=accordion-trigger])]:m-0',
9
+ accordionItem: '',
10
+ accordionTrigger: 'text-foreground',
11
+ accordionContent: 'text-muted-foreground [&_p]:mt-0 [&_p:last-child]:mb-0',
10
12
  expandableTrigger:
11
13
  'items-center px-4 py-3 text-sm font-normal text-foreground hover:no-underline',
12
14
 
@@ -25,17 +27,17 @@ export const styles = {
25
27
  'border-emerald-300 bg-emerald-50 text-emerald-900 dark:border-emerald-400/40 dark:bg-emerald-400/10 dark:text-emerald-300',
26
28
  danger: 'border-destructive/40 bg-destructive/10 text-destructive',
27
29
 
28
- card: 'block h-full gap-0 rounded-lg border border-border bg-transparent p-4 text-base ring-0 hover:border-input',
30
+ card: 'block h-full gap-0 rounded-lg border border-border bg-transparent p-4 text-base ring-0 hover:border-primary',
29
31
  cardHorizontal:
30
- '[&_[data-slot=card-inner]]:items-center [&_[data-slot=card-main]]:flex-row [&_[data-slot=card-main]]:items-center [&_[data-slot=card-main]]:gap-3 [&_[data-slot=card-body]]:m-0',
32
+ '[&_[data-slot=card-inner]]:items-center [&_[data-slot=card-main]]:flex-row [&_[data-slot=card-main]]:items-center [&_[data-slot=card-main]]:gap-3 [&_[data-slot=card-body]]:m-0 [&_[data-slot=card-header]]:flex-row [&_[data-slot=card-header]]:items-center [&_[data-slot=card-header]]:gap-2',
31
33
  cardTyped:
32
34
  '[&_[data-slot=card-title]]:text-inherit [&_[data-slot=card-body]]:text-inherit [&_[data-slot=card-cta]]:text-inherit [&_[data-slot=card-arrow]]:text-inherit',
33
35
  cardImageLayout: 'p-0 [&_[data-slot=card-inner]]:p-6',
34
36
  cardImage: 'block aspect-video w-full border-border border-b object-cover',
35
37
  cardInner: 'flex items-start justify-between gap-3',
36
38
  cardMain: 'flex min-w-0 flex-col gap-2',
37
- cardHeader: 'flex items-center gap-2',
38
- cardIcon: 'inline-flex shrink-0 items-center justify-center [&_img]:size-4 [&_svg]:size-4',
39
+ cardHeader: 'flex flex-col items-start gap-3',
40
+ cardIcon: 'inline-flex shrink-0 items-center justify-center [&_img]:size-6 [&_svg]:size-6',
39
41
  cardTitle: 'text-base font-semibold text-foreground',
40
42
  cardBody: 'text-muted-foreground [&>:first-child]:mt-0 [&>:last-child]:mb-0',
41
43
  cardCta:
@@ -24,7 +24,7 @@ function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
24
24
 
25
25
  function AccordionTrigger({ className, children, ...props }: AccordionPrimitive.Trigger.Props) {
26
26
  return (
27
- <AccordionPrimitive.Header className="m-0 flex">
27
+ <AccordionPrimitive.Header className="flex">
28
28
  <AccordionPrimitive.Trigger
29
29
  data-slot="accordion-trigger"
30
30
  className={cn(
@@ -22,7 +22,17 @@ function ScrollArea({ className, children, scrollbars = 'vertical', ...props }:
22
22
  >
23
23
  <ScrollAreaPrimitive.Viewport
24
24
  data-slot="scroll-area-viewport"
25
- className="box-border h-full w-full max-w-full rounded-[inherit] overscroll-contain outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
25
+ className={cn(
26
+ 'box-border h-full w-full max-w-full rounded-[inherit] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1',
27
+ // Contain overscroll only on the managed axis; containing both axes
28
+ // would swallow vertical wheel events over horizontal-only scroll
29
+ // areas (e.g. code blocks) and block page scrolling.
30
+ scrollbars === 'both'
31
+ ? 'overscroll-contain'
32
+ : hasHorizontalScrollbar
33
+ ? 'overscroll-x-contain'
34
+ : 'overscroll-y-contain',
35
+ )}
26
36
  >
27
37
  <ScrollAreaPrimitive.Content
28
38
  data-slot="scroll-area-content"
@@ -665,7 +665,15 @@ function normalizeScope(
665
665
  nodes.push(...collectPages(tab.pages, context, state));
666
666
  }
667
667
 
668
- if (state.pages.length === pagesBefore) {
668
+ const isLinkTab = typeof tab.href === 'string' && !!tab.href;
669
+
670
+ if (isLinkTab && state.pages.length !== pagesBefore) {
671
+ throw new Error(
672
+ `Invalid docs config: tab "${tabLabel}" cannot define both "href" and page entries.`,
673
+ );
674
+ }
675
+
676
+ if (!isLinkTab && state.pages.length === pagesBefore) {
669
677
  throw new Error(
670
678
  `Invalid docs config: tab "${tabLabel}" does not contain any supported page entries.`,
671
679
  );
@@ -781,14 +789,16 @@ function normalizeScope(
781
789
  const normalizedTabs = tabs.map((tab, index) => {
782
790
  const tabId = tabIds[index];
783
791
  const firstPage = firstVisiblePageByTab.get(tabId);
792
+ const href = typeof tab.href === 'string' && tab.href ? tab.href : undefined;
784
793
 
785
794
  return {
786
795
  id: tabId,
787
796
  label: tab.tab?.trim() || '',
788
- url: firstPage?.url || pageToUrl('index', docsPrefix),
797
+ url: href || firstPage?.url || pageToUrl('index', docsPrefix),
789
798
  icon: tab.icon,
790
799
  presentation: tab.presentation || 'tab',
791
800
  hidden: tab.hidden || undefined,
801
+ link: href ? true : undefined,
792
802
  };
793
803
  });
794
804
 
@@ -107,6 +107,7 @@ export function resolveSiteModel(
107
107
  target: config.logo.href
108
108
  ? resolveLinkTarget(config.logo.href, config.logo.target)
109
109
  : undefined,
110
+ invert: config.logo.invert === true,
110
111
  }
111
112
  : null;
112
113
 
package/src/lib/types.ts CHANGED
@@ -46,6 +46,8 @@ export interface TabItem {
46
46
  groups?: GroupItem[];
47
47
  pages?: PageItem[];
48
48
  dropdowns?: DropdownItem[];
49
+ /** Link tab: navigates to this URL instead of owning docs pages. */
50
+ href?: string;
49
51
  icon?: string;
50
52
  hidden?: boolean;
51
53
  /** Internal normalized presentation; not a docs.json field. */
@@ -99,7 +101,15 @@ export interface ThemeColors {
99
101
 
100
102
  export type LogoOption =
101
103
  | string
102
- | { light?: string; dark?: string; href?: string; target?: LinkTarget };
104
+ | {
105
+ light?: string;
106
+ dark?: string;
107
+ href?: string;
108
+ target?: LinkTarget;
109
+ /** Force the logo to white in dark mode with a CSS filter instead of
110
+ * swapping to a dark variant. Best for monochrome logos. */
111
+ invert?: boolean;
112
+ };
103
113
 
104
114
  /** Project-level settings supplied by shiso.config.ts. Mirrors the public
105
115
  * shape exported from "@umami/shiso/config". */
@@ -337,6 +347,8 @@ export interface DocsTab {
337
347
  icon?: string;
338
348
  presentation: 'tab' | 'dropdown';
339
349
  hidden?: boolean;
350
+ /** True for link tabs configured with `href` instead of docs pages. */
351
+ link?: boolean;
340
352
  }
341
353
 
342
354
  export interface NormalizedDocsPage {
@@ -494,7 +506,13 @@ export interface ThemeLabels {
494
506
 
495
507
  export interface SiteModel {
496
508
  name?: string;
497
- logo: { light?: string; dark?: string; href?: string; target?: LinkTarget } | null;
509
+ logo: {
510
+ light?: string;
511
+ dark?: string;
512
+ href?: string;
513
+ target?: LinkTarget;
514
+ invert?: boolean;
515
+ } | null;
498
516
  navbar: NormalizedNavbar | null;
499
517
  footer: NormalizedFooter | null;
500
518
  banner: BannerConfig | null;
@@ -5,6 +5,32 @@
5
5
 
6
6
  @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
7
7
 
8
+ /* tw-animate-css defines these keyframes against Radix-style height variables
9
+ (--radix-accordion-content-height etc.). Base UI exposes the panel height as
10
+ --accordion-panel-height instead, so the originals resolve to `auto` and the
11
+ open/close animation jumps. Redefining them inside @theme replaces the
12
+ originals in Tailwind's emitted output (a plain top-level @keyframes rule
13
+ would lose: Tailwind emits theme keyframes at the end of the stylesheet). */
14
+ @theme inline {
15
+ @keyframes accordion-down {
16
+ from {
17
+ height: 0;
18
+ }
19
+ to {
20
+ height: var(--accordion-panel-height);
21
+ }
22
+ }
23
+
24
+ @keyframes accordion-up {
25
+ from {
26
+ height: var(--accordion-panel-height);
27
+ }
28
+ to {
29
+ height: 0;
30
+ }
31
+ }
32
+ }
33
+
8
34
  @layer base {
9
35
  *,
10
36
  *::before,
@@ -77,7 +103,7 @@
77
103
  margin-top: 1rem;
78
104
  }
79
105
 
80
- .docs-markdown :where(h2, h3, h4, h5, h6) {
106
+ .docs-markdown :where(h1, h2, h3, h4, h5, h6) {
81
107
  font-family: var(--font-heading);
82
108
  font-weight: var(--font-heading-weight, 600);
83
109
  letter-spacing: -0.02em;