@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 +9 -0
- package/dist/chunks/App.js +63 -46
- package/dist/chunks/local.js +1 -62
- package/dist/chunks/search.js +64 -0
- package/docs.schema.json +7 -1
- package/package.json +2 -2
- package/src/App.tsx +1 -3
- package/src/components/DocContent.tsx +5 -3
- package/src/components/Header.tsx +12 -2
- package/src/components/Search.tsx +14 -3
- package/src/components/TopNav.tsx +18 -0
- package/src/components/docs/Card.tsx +8 -8
- package/src/components/docs/PropertiesTable.tsx +4 -4
- package/src/components/docs/styles.ts +13 -11
- package/src/components/ui/accordion.tsx +1 -1
- package/src/components/ui/scroll-area.tsx +11 -1
- package/src/lib/docs-config.ts +12 -2
- package/src/lib/site-model.ts +1 -0
- package/src/lib/types.ts +20 -2
- package/src/styles/global.css +46 -2
- package/src/styles/tokens.css +1 -1
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
|
package/dist/chunks/App.js
CHANGED
|
@@ -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/
|
|
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]
|
|
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: "
|
|
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
|
|
11010
|
-
accordionItem: "
|
|
11011
|
-
accordionTrigger: "
|
|
11012
|
-
accordionContent: "
|
|
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-
|
|
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-
|
|
11033
|
-
cardIcon: "inline-flex shrink-0 items-center justify-center [&_img]:size-
|
|
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:
|
|
11498
|
-
info: /* @__PURE__ */ jsx(Info$1, { size:
|
|
11499
|
-
warning: /* @__PURE__ */ jsx(TriangleAlert, { size:
|
|
11500
|
-
tip: /* @__PURE__ */ jsx(Lightbulb, { size:
|
|
11501
|
-
check: /* @__PURE__ */ jsx(Check$1, { size:
|
|
11502
|
-
danger: /* @__PURE__ */ jsx(CircleAlert, { size:
|
|
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,
|
|
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/
|
|
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/
|
|
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: "
|
|
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
|
-
|
|
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 ?
|
|
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
|
|
26427
|
-
|
|
26428
|
-
|
|
26429
|
-
|
|
26430
|
-
|
|
26431
|
-
|
|
26432
|
-
|
|
26433
|
-
|
|
26434
|
-
|
|
26435
|
-
|
|
26436
|
-
|
|
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-
|
|
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,
|
|
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-
|
|
26862
|
+
className: "mt-3 mb-8 text-lg text-muted-foreground leading-relaxed",
|
|
26846
26863
|
children: description
|
|
26847
26864
|
}),
|
|
26848
26865
|
/* @__PURE__ */ jsx("div", {
|
package/dist/chunks/local.js
CHANGED
|
@@ -1,66 +1,5 @@
|
|
|
1
|
-
|
|
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.
|
|
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.
|
|
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/
|
|
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-
|
|
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,
|
|
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 &&
|
|
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
|
-
{
|
|
73
|
-
|
|
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
|
|
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 ?
|
|
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={
|
|
32
|
-
info: <InfoIcon size={
|
|
33
|
-
warning: <TriangleAlert size={
|
|
34
|
-
tip: <Lightbulb size={
|
|
35
|
-
check: <CheckIcon size={
|
|
36
|
-
danger: <CircleAlert size={
|
|
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,
|
|
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/
|
|
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/
|
|
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="
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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-
|
|
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-
|
|
38
|
-
cardIcon: 'inline-flex shrink-0 items-center justify-center [&_img]:size-
|
|
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="
|
|
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=
|
|
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"
|
package/src/lib/docs-config.ts
CHANGED
|
@@ -665,7 +665,15 @@ function normalizeScope(
|
|
|
665
665
|
nodes.push(...collectPages(tab.pages, context, state));
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
-
|
|
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
|
|
package/src/lib/site-model.ts
CHANGED
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
|
-
| {
|
|
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: {
|
|
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;
|
package/src/styles/global.css
CHANGED
|
@@ -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,
|
|
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;
|
package/src/styles/tokens.css
CHANGED
|
@@ -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;
|