@umami/shiso 1.5.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
@@ -1,9 +1,8 @@
1
+ import { n as highlightTerms } from "./search.js";
1
2
  import { resolveSearchProvider } from "../search.js";
2
3
  import * as React$1 from "react";
3
4
  import { Children, cloneElement, createContext, createElement, forwardRef, isValidElement, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, useSyncExternalStore as useSyncExternalStore$1 } from "react";
4
- import "@fontsource/inter/400.css";
5
- import "@fontsource/inter/700.css";
6
- import "@fontsource/inter/800.css";
5
+ import "@fontsource-variable/inter/index.css";
7
6
  import "@fontsource/jetbrains-mono/400.css";
8
7
  import "highlight.js/styles/github.css";
9
8
  import "@umami/shiso/styles.css";
@@ -9533,7 +9532,7 @@ function ScrollArea({ className, children, scrollbars = "vertical", ...props })
9533
9532
  children: [
9534
9533
  /* @__PURE__ */ jsx(ScrollAreaViewport, {
9535
9534
  "data-slot": "scroll-area-viewport",
9536
- 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"),
9537
9536
  children: /* @__PURE__ */ jsx(ScrollAreaContent, {
9538
9537
  "data-slot": "scroll-area-content",
9539
9538
  className: cn("box-border min-w-0", hasHorizontalScrollbar ? "w-max min-w-full max-w-none" : "w-full max-w-full"),
@@ -10970,7 +10969,7 @@ function AccordionItem({ className, ...props }) {
10970
10969
  }
10971
10970
  function AccordionTrigger({ className, children, ...props }) {
10972
10971
  return /* @__PURE__ */ jsx(AccordionHeader, {
10973
- className: "m-0 flex",
10972
+ className: "flex",
10974
10973
  children: /* @__PURE__ */ jsxs(AccordionTrigger$1, {
10975
10974
  "data-slot": "accordion-trigger",
10976
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),
@@ -11005,11 +11004,11 @@ function AccordionContent({ className, children, ...props }) {
11005
11004
  //#region src/components/docs/styles.ts
11006
11005
  /** Shared Tailwind utility groups for the built-in MDX components. */
11007
11006
  const styles = {
11008
- accordion: "my-3",
11009
- 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",
11010
- accordionItem: "rounded-lg border border-border bg-transparent",
11011
- accordionTrigger: "items-center px-4 py-3 text-base font-semibold text-foreground hover:no-underline",
11012
- 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",
11013
11012
  expandableTrigger: "items-center px-4 py-3 text-sm font-normal text-foreground hover:no-underline",
11014
11013
  callout: "my-4 flex items-start gap-3 rounded-lg border px-3 py-2.5 text-sm leading-6",
11015
11014
  calloutIcon: "flex h-6 w-5 shrink-0 items-center justify-center [&_svg]:block [&_svg]:size-4",
@@ -11022,15 +11021,15 @@ const styles = {
11022
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",
11023
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",
11024
11023
  danger: "border-destructive/40 bg-destructive/10 text-destructive",
11025
- card: "block h-full gap-0 rounded-lg border border-border bg-transparent p-4 text-base ring-0 hover:border-input",
11026
- 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",
11027
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",
11028
11027
  cardImageLayout: "p-0 [&_[data-slot=card-inner]]:p-6",
11029
11028
  cardImage: "block aspect-video w-full border-border border-b object-cover",
11030
11029
  cardInner: "flex items-start justify-between gap-3",
11031
11030
  cardMain: "flex min-w-0 flex-col gap-2",
11032
- cardHeader: "flex items-center gap-2",
11033
- 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",
11034
11033
  cardTitle: "text-base font-semibold text-foreground",
11035
11034
  cardBody: "text-muted-foreground [&>:first-child]:mt-0 [&>:last-child]:mb-0",
11036
11035
  cardCta: "flex shrink-0 items-center gap-[0.35rem] text-[0.9rem] font-medium text-muted-foreground",
@@ -11494,12 +11493,12 @@ function CardContent({ className, ...props }) {
11494
11493
  //#endregion
11495
11494
  //#region src/components/docs/Card.tsx
11496
11495
  const CARD_TYPE_ICONS = {
11497
- note: /* @__PURE__ */ jsx(Info$1, { size: 16 }),
11498
- info: /* @__PURE__ */ jsx(Info$1, { size: 16 }),
11499
- warning: /* @__PURE__ */ jsx(TriangleAlert, { size: 16 }),
11500
- tip: /* @__PURE__ */ jsx(Lightbulb, { size: 16 }),
11501
- check: /* @__PURE__ */ jsx(Check$1, { size: 16 }),
11502
- 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 })
11503
11502
  };
11504
11503
  const CARD_TYPE_HOVER = {
11505
11504
  note: "hover:border-border",
@@ -11510,7 +11509,7 @@ const CARD_TYPE_HOVER = {
11510
11509
  danger: "hover:border-destructive/40"
11511
11510
  };
11512
11511
  function DocsCardContent({ title, icon, color, img, cta, arrow, type, children }) {
11513
- const resolvedIcon = resolveIcon(icon, 16) || (type ? CARD_TYPE_ICONS[type] : null);
11512
+ const resolvedIcon = resolveIcon(icon, 24) || (type ? CARD_TYPE_ICONS[type] : null);
11514
11513
  return /* @__PURE__ */ jsxs(CardContent, {
11515
11514
  className: "p-0",
11516
11515
  children: [img ? /* @__PURE__ */ jsx("img", {
@@ -11525,6 +11524,7 @@ function DocsCardContent({ title, icon, color, img, cta, arrow, type, children }
11525
11524
  "data-slot": "card-main",
11526
11525
  children: [/* @__PURE__ */ jsxs("div", {
11527
11526
  className: styles.cardHeader,
11527
+ "data-slot": "card-header",
11528
11528
  children: [resolvedIcon ? /* @__PURE__ */ jsx("span", {
11529
11529
  className: `${styles.cardIcon} ${type ? "text-inherit" : "text-primary"}`,
11530
11530
  style: color ? { color } : void 0,
@@ -12892,18 +12892,18 @@ function PropertiesTable({ children }) {
12892
12892
  return /* @__PURE__ */ jsx("div", {
12893
12893
  className: "my-4 overflow-x-auto rounded-lg border border-border px-6 py-2",
12894
12894
  children: /* @__PURE__ */ jsxs("table", {
12895
- className: "m-0 min-w-[40rem] table-fixed border-collapse",
12895
+ className: "m-0 min-w-[40rem] table-fixed border-collapse border-0",
12896
12896
  children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
12897
12897
  /* @__PURE__ */ jsx("th", {
12898
- className: "w-1/3 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 pr-6 text-left text-sm font-semibold text-foreground",
12898
+ className: "w-1/5 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 pr-6 text-left text-sm font-semibold text-foreground",
12899
12899
  children: "Name"
12900
12900
  }),
12901
12901
  /* @__PURE__ */ jsx("th", {
12902
- className: "w-1/3 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 pr-6 text-left text-sm font-semibold text-foreground",
12902
+ className: "w-1/4 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 pr-6 text-left text-sm font-semibold text-foreground",
12903
12903
  children: "Type"
12904
12904
  }),
12905
12905
  /* @__PURE__ */ jsx("th", {
12906
- className: "w-1/3 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 text-left text-sm font-semibold text-foreground",
12906
+ className: "border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 text-left text-sm font-semibold text-foreground",
12907
12907
  children: "Description"
12908
12908
  })
12909
12909
  ] }) }), /* @__PURE__ */ jsx("tbody", { children: rows.map((row, index) => {
@@ -22651,7 +22651,9 @@ function normalizeScope(name, source, anchors, resolveDocFile, docsPrefix) {
22651
22651
  });
22652
22652
  });
22653
22653
  if (Array.isArray(tab.pages) && tab.pages.length) nodes.push(...collectPages(tab.pages, context, state));
22654
- 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.`);
22655
22657
  treeByTab.set(tabId, nodes);
22656
22658
  });
22657
22659
  const pending = state.pages;
@@ -22724,13 +22726,15 @@ function normalizeScope(name, source, anchors, resolveDocFile, docsPrefix) {
22724
22726
  tabs: tabs.map((tab, index) => {
22725
22727
  const tabId = tabIds[index];
22726
22728
  const firstPage = firstVisiblePageByTab.get(tabId);
22729
+ const href = typeof tab.href === "string" && tab.href ? tab.href : void 0;
22727
22730
  return {
22728
22731
  id: tabId,
22729
22732
  label: tab.tab?.trim() || "",
22730
- url: firstPage?.url || pageToUrl("index", docsPrefix),
22733
+ url: href || firstPage?.url || pageToUrl("index", docsPrefix),
22731
22734
  icon: tab.icon,
22732
22735
  presentation: tab.presentation || "tab",
22733
- hidden: tab.hidden || void 0
22736
+ hidden: tab.hidden || void 0,
22737
+ link: href ? true : void 0
22734
22738
  };
22735
22739
  }),
22736
22740
  showTabs,
@@ -23032,7 +23036,8 @@ function resolveSiteModel(config, docs, shiso) {
23032
23036
  light: config.logo.light || config.logo.dark,
23033
23037
  dark: config.logo.dark || config.logo.light,
23034
23038
  href: config.logo.href,
23035
- 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
23036
23041
  } : null;
23037
23042
  return {
23038
23043
  name: config.name?.trim() || void 0,
@@ -26097,6 +26102,11 @@ function renderSnippet(snippet) {
26097
26102
  children: part
26098
26103
  }, index) : part);
26099
26104
  }
26105
+ function renderWithQueryHighlight(text, query) {
26106
+ const terms = query.trim().split(/\s+/).filter(Boolean);
26107
+ if (!terms.length) return text;
26108
+ return renderSnippet(highlightTerms(text, terms));
26109
+ }
26100
26110
  /**
26101
26111
  * Provider-neutral search dialog. The selected provider and its index or
26102
26112
  * client are loaded on demand, so search stays out of the initial bundle.
@@ -26232,7 +26242,7 @@ function Search({ config, labels }) {
26232
26242
  onSelect: () => select(result),
26233
26243
  children: [/* @__PURE__ */ jsxs("div", {
26234
26244
  className: "text-[0.9rem] font-semibold text-foreground",
26235
- children: [result.page, result.heading ? ` › ${result.heading}` : ""]
26245
+ children: [renderWithQueryHighlight(result.page, query), result.heading ? /* @__PURE__ */ jsxs(Fragment, { children: [" › ", renderWithQueryHighlight(result.heading, query)] }) : null]
26236
26246
  }), result.snippet && /* @__PURE__ */ jsx("div", {
26237
26247
  className: "mt-[0.15rem] line-clamp-2 text-[0.8rem] text-muted-foreground",
26238
26248
  children: renderSnippet(result.snippet)
@@ -26340,6 +26350,13 @@ function TopNav({ docs, label }) {
26340
26350
  }, item.href))
26341
26351
  })] }, tab.id);
26342
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);
26343
26360
  return /* @__PURE__ */ jsxs(Link, {
26344
26361
  to: tab.url,
26345
26362
  className: tabClass(tab),
@@ -26422,19 +26439,19 @@ function Header({ site }) {
26422
26439
  const brandHref = logo?.href || (hasRootStandalonePage ? "/" : docsHomeUrl);
26423
26440
  const hasBrand = !!name || !!logo?.light || !!logo?.dark;
26424
26441
  const brandClassName = "inline-flex items-center gap-2 text-xl font-bold text-foreground tracking-[-0.03em]";
26425
- const brandContent = /* @__PURE__ */ jsxs(Fragment, { children: [
26426
- logo?.light ? /* @__PURE__ */ jsx("img", {
26427
- src: logo.light,
26428
- alt: "",
26429
- className: "h-6 w-auto dark:hidden"
26430
- }) : null,
26431
- logo?.dark ? /* @__PURE__ */ jsx("img", {
26432
- src: logo.dark,
26433
- alt: "",
26434
- className: "hidden h-6 w-auto dark:block"
26435
- }) : null,
26436
- name ? /* @__PURE__ */ jsx("span", { children: name }) : null
26437
- ] });
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] });
26438
26455
  return /* @__PURE__ */ jsx("header", {
26439
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",
26440
26457
  children: /* @__PURE__ */ jsxs("div", {
@@ -26828,13 +26845,13 @@ function DocContent({ page, doc, site }) {
26828
26845
  ...pagefindAttrs,
26829
26846
  children: [
26830
26847
  eyebrow && /* @__PURE__ */ jsx("div", {
26831
- className: "text-sm font-bold text-primary",
26848
+ className: "text-sm font-medium text-primary",
26832
26849
  children: eyebrow
26833
26850
  }),
26834
26851
  /* @__PURE__ */ jsxs("div", {
26835
26852
  className: "flex items-start justify-between gap-4",
26836
26853
  children: [title && /* @__PURE__ */ jsx("h1", {
26837
- className: "mt-2 text-4xl text-foreground leading-[1.2] tracking-[-0.03em] [font-family:var(--font-heading)] [font-weight:var(--font-heading-weight,800)]",
26854
+ className: "mt-2 text-4xl text-foreground leading-[1.2] tracking-[-0.03em] [font-family:var(--font-heading)] [font-weight:var(--font-heading-weight,700)]",
26838
26855
  children: title
26839
26856
  }), /* @__PURE__ */ jsx(ContextualMenu, {
26840
26857
  options: contextualOptions,
@@ -26842,7 +26859,7 @@ function DocContent({ page, doc, site }) {
26842
26859
  })]
26843
26860
  }),
26844
26861
  description && /* @__PURE__ */ jsx("p", {
26845
- className: "mt-1 mb-6 text-base text-muted-foreground",
26862
+ className: "mt-3 mb-8 text-lg text-muted-foreground leading-relaxed",
26846
26863
  children: description
26847
26864
  }),
26848
26865
  /* @__PURE__ */ jsx("div", {
@@ -1,66 +1,5 @@
1
- //#region src/lib/search.ts
2
- /**
3
- * Restricts records to the active scope. Records without a scope id (from
4
- * single-scope indexes or older caches) always pass.
5
- */
6
- function filterRecordsByScope(records, context) {
7
- if (!context?.scopeId) return records;
8
- return records.filter((record) => !record.scopeId || record.scopeId === context.scopeId);
9
- }
10
- const SNIPPET_RADIUS = 60;
11
- function escapeRegExp(value) {
12
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13
- }
14
- /** Wraps every term occurrence in `<mark>` so the dialog can highlight it. */
15
- function highlightTerms(snippet, terms) {
16
- if (!terms.length) return snippet;
17
- const pattern = new RegExp([...terms].sort((a, b) => b.length - a.length).map(escapeRegExp).join("|"), "gi");
18
- return snippet.replace(pattern, "<mark>$&</mark>");
19
- }
20
- function makeSnippet(text, index, length, terms) {
21
- const start = Math.max(0, index - SNIPPET_RADIUS);
22
- const end = Math.min(text.length, index + length + SNIPPET_RADIUS);
23
- return highlightTerms(`${start > 0 ? "…" : ""}${text.slice(start, end).trim()}${end < text.length ? "…" : ""}`, terms);
24
- }
25
- function searchIndex(records, query, limit = 10) {
26
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
27
- if (!terms.length) return [];
28
- const results = [];
29
- for (const record of records) {
30
- const page = record.page.toLowerCase();
31
- const heading = (record.heading || "").toLowerCase();
32
- const text = record.text.toLowerCase();
33
- let score = 0;
34
- let snippetAt = -1;
35
- let snippetLength = 0;
36
- let matched = true;
37
- for (const term of terms) if (page.includes(term)) score += page === term ? 40 : 20;
38
- else if (heading.includes(term)) score += heading === term ? 30 : 15;
39
- else {
40
- const index = text.indexOf(term);
41
- if (index === -1) {
42
- matched = false;
43
- break;
44
- }
45
- score += 5;
46
- if (snippetAt === -1) {
47
- snippetAt = index;
48
- snippetLength = term.length;
49
- }
50
- }
51
- if (!matched || !score) continue;
52
- results.push({
53
- url: record.id ? `${record.url}#${record.id}` : record.url,
54
- page: record.page,
55
- heading: record.heading,
56
- snippet: snippetAt >= 0 ? makeSnippet(record.text, snippetAt, snippetLength, terms) : void 0,
57
- score
58
- });
59
- }
60
- return results.sort((a, b) => b.score - a.score).slice(0, limit);
61
- }
1
+ import { r as searchIndex, t as filterRecordsByScope } from "./search.js";
62
2
 
63
- //#endregion
64
3
  //#region src/lib/search/providers/local.ts
65
4
  /** Built-in provider backed by the section index generated during the build. */
66
5
  function createLocalSearchProvider(_options = {}) {
@@ -0,0 +1,64 @@
1
+ //#region src/lib/search.ts
2
+ /**
3
+ * Restricts records to the active scope. Records without a scope id (from
4
+ * single-scope indexes or older caches) always pass.
5
+ */
6
+ function filterRecordsByScope(records, context) {
7
+ if (!context?.scopeId) return records;
8
+ return records.filter((record) => !record.scopeId || record.scopeId === context.scopeId);
9
+ }
10
+ const SNIPPET_RADIUS = 60;
11
+ function escapeRegExp(value) {
12
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13
+ }
14
+ /** Wraps every term occurrence in `<mark>` so the dialog can highlight it. */
15
+ function highlightTerms(snippet, terms) {
16
+ if (!terms.length) return snippet;
17
+ const pattern = new RegExp([...terms].sort((a, b) => b.length - a.length).map(escapeRegExp).join("|"), "gi");
18
+ return snippet.replace(pattern, "<mark>$&</mark>");
19
+ }
20
+ function makeSnippet(text, index, length, terms) {
21
+ const start = Math.max(0, index - SNIPPET_RADIUS);
22
+ const end = Math.min(text.length, index + length + SNIPPET_RADIUS);
23
+ return highlightTerms(`${start > 0 ? "…" : ""}${text.slice(start, end).trim()}${end < text.length ? "…" : ""}`, terms);
24
+ }
25
+ function searchIndex(records, query, limit = 10) {
26
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
27
+ if (!terms.length) return [];
28
+ const results = [];
29
+ for (const record of records) {
30
+ const page = record.page.toLowerCase();
31
+ const heading = (record.heading || "").toLowerCase();
32
+ const text = record.text.toLowerCase();
33
+ let score = 0;
34
+ let snippetAt = -1;
35
+ let snippetLength = 0;
36
+ let matched = true;
37
+ for (const term of terms) if (page.includes(term)) score += page === term ? 40 : 20;
38
+ else if (heading.includes(term)) score += heading === term ? 30 : 15;
39
+ else {
40
+ const index = text.indexOf(term);
41
+ if (index === -1) {
42
+ matched = false;
43
+ break;
44
+ }
45
+ score += 5;
46
+ if (snippetAt === -1) {
47
+ snippetAt = index;
48
+ snippetLength = term.length;
49
+ }
50
+ }
51
+ if (!matched || !score) continue;
52
+ results.push({
53
+ url: record.id ? `${record.url}#${record.id}` : record.url,
54
+ page: record.page,
55
+ heading: record.heading,
56
+ snippet: snippetAt >= 0 ? makeSnippet(record.text, snippetAt, snippetLength, terms) : void 0,
57
+ score
58
+ });
59
+ }
60
+ return results.sort((a, b) => b.score - a.score).slice(0, limit);
61
+ }
62
+
63
+ //#endregion
64
+ export { highlightTerms as n, searchIndex as r, filterRecordsByScope as t };
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.5.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": {
@@ -64,7 +64,7 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@base-ui/react": "^1.7.0",
67
- "@fontsource/inter": "^5.2.5",
67
+ "@fontsource-variable/inter": "^5.3.0",
68
68
  "@fontsource/jetbrains-mono": "^5.2.5",
69
69
  "@mdx-js/react": "^3.1.1",
70
70
  "@mdx-js/rollup": "^3.1.1",
package/src/App.tsx CHANGED
@@ -1,6 +1,4 @@
1
- import '@fontsource/inter/400.css';
2
- import '@fontsource/inter/700.css';
3
- import '@fontsource/inter/800.css';
1
+ import '@fontsource-variable/inter/index.css';
4
2
  import '@fontsource/jetbrains-mono/400.css';
5
3
  import 'highlight.js/styles/github.css';
6
4
  import '@umami/shiso/styles.css';
@@ -101,16 +101,18 @@ export function DocContent({ page, doc, site }: DocContentProps) {
101
101
 
102
102
  return (
103
103
  <article className="min-w-0 grow" {...pagefindAttrs}>
104
- {eyebrow && <div className="text-sm font-bold text-primary">{eyebrow}</div>}
104
+ {eyebrow && <div className="text-sm font-medium text-primary">{eyebrow}</div>}
105
105
  <div className="flex items-start justify-between gap-4">
106
106
  {title && (
107
- <h1 className="mt-2 text-4xl text-foreground leading-[1.2] tracking-[-0.03em] [font-family:var(--font-heading)] [font-weight:var(--font-heading-weight,800)]">
107
+ <h1 className="mt-2 text-4xl text-foreground leading-[1.2] tracking-[-0.03em] [font-family:var(--font-heading)] [font-weight:var(--font-heading-weight,700)]">
108
108
  {title}
109
109
  </h1>
110
110
  )}
111
111
  <ContextualMenu options={contextualOptions} labels={site.labels} />
112
112
  </div>
113
- {description && <p className="mt-1 mb-6 text-base text-muted-foreground">{description}</p>}
113
+ {description && (
114
+ <p className="mt-3 mb-8 text-lg text-muted-foreground leading-relaxed">{description}</p>
115
+ )}
114
116
  <div className="docs-markdown">
115
117
  <Content />
116
118
  </div>
@@ -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
  );
@@ -11,7 +11,7 @@ import {
11
11
  CommandList,
12
12
  } from '@/components/ui/command';
13
13
  import { Dialog, DialogContent, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
14
- import type { SearchResult } from '@/lib/search';
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
17
  import { getScopeByPathname } from '@/lib/site-config';
@@ -44,6 +44,12 @@ function renderSnippet(snippet: string) {
44
44
  );
45
45
  }
46
46
 
47
+ function renderWithQueryHighlight(text: string, query: string) {
48
+ const terms = query.trim().split(/\s+/).filter(Boolean);
49
+ if (!terms.length) return text;
50
+ return renderSnippet(highlightTerms(text, terms));
51
+ }
52
+
47
53
  /**
48
54
  * Provider-neutral search dialog. The selected provider and its index or
49
55
  * client are loaded on demand, so search stays out of the initial bundle.
@@ -224,8 +230,13 @@ export function Search({ config, labels }: { config: ResolvedSearchConfig; label
224
230
  onSelect={() => select(result)}
225
231
  >
226
232
  <div className="text-[0.9rem] font-semibold text-foreground">
227
- {result.page}
228
- {result.heading ? ` › ${result.heading}` : ''}
233
+ {renderWithQueryHighlight(result.page, query)}
234
+ {result.heading ? (
235
+ <>
236
+ {' › '}
237
+ {renderWithQueryHighlight(result.heading, query)}
238
+ </>
239
+ ) : null}
229
240
  </div>
230
241
  {result.snippet && (
231
242
  <div className="mt-[0.15rem] line-clamp-2 text-[0.8rem] text-muted-foreground">
@@ -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'}`}
@@ -24,16 +24,16 @@ export function PropertiesTable({ children }: PropertiesTableProps) {
24
24
 
25
25
  return (
26
26
  <div className="my-4 overflow-x-auto rounded-lg border border-border px-6 py-2">
27
- <table className="m-0 min-w-[40rem] table-fixed border-collapse">
27
+ <table className="m-0 min-w-[40rem] table-fixed border-collapse border-0">
28
28
  <thead>
29
29
  <tr>
30
- <th className="w-1/3 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 pr-6 text-left text-sm font-semibold text-foreground">
30
+ <th className="w-1/5 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 pr-6 text-left text-sm font-semibold text-foreground">
31
31
  Name
32
32
  </th>
33
- <th className="w-1/3 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 pr-6 text-left text-sm font-semibold text-foreground">
33
+ <th className="w-1/4 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 pr-6 text-left text-sm font-semibold text-foreground">
34
34
  Type
35
35
  </th>
36
- <th className="w-1/3 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 text-left text-sm font-semibold text-foreground">
36
+ <th className="border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 text-left text-sm font-semibold text-foreground">
37
37
  Description
38
38
  </th>
39
39
  </tr>
@@ -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,
@@ -60,19 +86,26 @@
60
86
  /* Markdown content */
61
87
 
62
88
  @layer components {
89
+ /* Body copy is muted; headings, bold text, links, and inline code stay at
90
+ full foreground brightness so they stand out against the running text. */
63
91
  .docs-markdown {
64
92
  margin-bottom: 2.5rem;
65
93
  font-size: 16px;
66
94
  line-height: 1.75;
95
+ color: color-mix(in srgb, var(--foreground) 40%, var(--muted-foreground));
96
+ }
97
+
98
+ [data-theme="dark"] .docs-markdown {
99
+ color: color-mix(in srgb, var(--foreground) 25%, var(--muted-foreground));
67
100
  }
68
101
 
69
102
  .docs-markdown > * + * {
70
103
  margin-top: 1rem;
71
104
  }
72
105
 
73
- .docs-markdown :where(h2, h3, h4, h5, h6) {
106
+ .docs-markdown :where(h1, h2, h3, h4, h5, h6) {
74
107
  font-family: var(--font-heading);
75
- font-weight: var(--font-heading-weight, 700);
108
+ font-weight: var(--font-heading-weight, 600);
76
109
  letter-spacing: -0.02em;
77
110
  line-height: 1.25;
78
111
  color: var(--foreground);
@@ -129,7 +162,13 @@
129
162
  margin: 0.35rem 0;
130
163
  }
131
164
 
165
+ .docs-markdown :where(strong, b) {
166
+ color: var(--foreground);
167
+ font-weight: 600;
168
+ }
169
+
132
170
  .docs-markdown a {
171
+ color: var(--foreground);
133
172
  text-decoration: underline;
134
173
  text-decoration-color: color-mix(in srgb, currentColor 30%, transparent);
135
174
  text-underline-offset: 0.2em;
@@ -140,6 +179,7 @@
140
179
  }
141
180
 
142
181
  .docs-markdown code:not(pre code) {
182
+ color: var(--foreground);
143
183
  border-radius: var(--radius-sm);
144
184
  background: color-mix(in srgb, currentColor 4%, transparent);
145
185
  padding: 0.1rem 0.3rem;
@@ -158,6 +198,10 @@
158
198
  border-radius: var(--radius-lg);
159
199
  }
160
200
 
201
+ .docs-markdown th {
202
+ color: var(--foreground);
203
+ }
204
+
161
205
  .docs-markdown th,
162
206
  .docs-markdown td {
163
207
  border: none;
@@ -40,7 +40,7 @@
40
40
  }
41
41
 
42
42
  :root {
43
- --font-sans: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
43
+ --font-sans: "Inter Variable", system-ui, -apple-system, "Segoe UI", sans-serif;
44
44
  --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
45
45
  --font-heading: var(--font-sans);
46
46
  --header-height: 3rem;