@umami/shiso 1.4.0 → 1.6.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 +17 -0
- package/bin/shiso.mjs +9 -1
- package/config.js +8 -0
- package/dist/chunks/App.js +255 -73
- package/dist/chunks/local.js +1 -62
- package/dist/chunks/search.js +64 -0
- package/dist/entry-client.js +1 -1
- package/dist/entry-server.js +16 -5
- package/docs.schema.json +29 -26
- package/package.json +8 -2
- package/scripts/build-runtime.mjs +1 -0
- package/scripts/generate-search-index.mjs +4 -3
- package/scripts/load-shiso-config.mjs +167 -0
- package/scripts/prerender.mjs +4 -2
- package/scripts/validate-config.mjs +23 -10
- package/scripts/vite-docs-config.mjs +52 -13
- package/src/App.tsx +13 -6
- package/src/components/DocContent.tsx +5 -3
- package/src/components/Header.tsx +61 -24
- package/src/components/Search.tsx +14 -3
- package/src/components/TopNav.tsx +9 -7
- package/src/components/docs/Button.tsx +67 -0
- package/src/components/docs/PropertiesTable.tsx +4 -4
- package/src/components/docs/index.ts +1 -0
- package/src/declarations.d.ts +7 -0
- package/src/entry-server.tsx +20 -3
- package/src/lib/content.ts +10 -2
- package/src/lib/head.ts +15 -7
- package/src/lib/locale.ts +1 -1
- package/src/lib/paths.ts +11 -7
- package/src/lib/site-config.ts +21 -3
- package/src/lib/site-model.ts +7 -2
- package/src/lib/standalone-pages.ts +129 -0
- package/src/lib/types.ts +33 -3
- package/src/pages/StandalonePage.tsx +42 -0
- package/src/styles/global.css +19 -1
- package/src/styles/tokens.css +1 -1
- package/types/config.d.ts +19 -0
- package/vite.config.ts +58 -18
package/src/lib/paths.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { ShisoOptions } from '@/lib/types';
|
|
1
|
+
import shiso from 'virtual:shiso-config';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* All URL construction goes through this module.
|
|
@@ -14,10 +13,11 @@ import type { ShisoOptions } from '@/lib/types';
|
|
|
14
13
|
* DOCS_PREFIX but not BASE_URL. React Router's `basename` adds BASE_URL, so
|
|
15
14
|
* only code that bypasses the router (prerender output paths, canonical URLs,
|
|
16
15
|
* raw <a href>) needs `toHref`.
|
|
16
|
+
*
|
|
17
|
+
* Values from `virtual:shiso-config` arrive with defaults applied and already
|
|
18
|
+
* normalized by scripts/load-shiso-config.mjs.
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
|
-
const shiso = ((rawConfig as { $shiso?: ShisoOptions }).$shiso || {}) as ShisoOptions;
|
|
20
|
-
|
|
21
21
|
/** Strips trailing slashes; "/" and "" both normalize to "". */
|
|
22
22
|
function normalizePrefix(value: string): string {
|
|
23
23
|
const trimmed = value.trim().replace(/\/+$/, '');
|
|
@@ -31,13 +31,17 @@ function normalizePrefix(value: string): string {
|
|
|
31
31
|
|
|
32
32
|
export const BASE_URL = normalizePrefix(import.meta.env?.BASE_URL || '/');
|
|
33
33
|
|
|
34
|
-
export const DOCS_PREFIX =
|
|
34
|
+
export const DOCS_PREFIX = shiso.docsPrefix;
|
|
35
35
|
|
|
36
36
|
/** Content directory, relative to the project root, without leading/trailing slashes. */
|
|
37
|
-
export const CONTENT_DIR =
|
|
37
|
+
export const CONTENT_DIR = shiso.contentDir;
|
|
38
|
+
|
|
39
|
+
/** Fixed root for standalone (non-docs) page files. Not configurable, so page
|
|
40
|
+
* slugs can never collide with the docs content tree. */
|
|
41
|
+
export const PAGES_DIR = 'content/pages';
|
|
38
42
|
|
|
39
43
|
/** Absolute origin used for canonical and og:url tags. Undefined when unconfigured. */
|
|
40
|
-
export const SITE_URL = shiso.siteUrl
|
|
44
|
+
export const SITE_URL = shiso.siteUrl;
|
|
41
45
|
|
|
42
46
|
/** Joins path segments with exactly one slash between them. */
|
|
43
47
|
export function joinPath(...parts: (string | undefined)[]): string {
|
package/src/lib/site-config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import shisoConfig from 'virtual:shiso-config';
|
|
1
2
|
import rawConfig from 'virtual:shiso-docs-config';
|
|
2
|
-
import { resolveDocFile } from '@/lib/content';
|
|
3
|
+
import { resolveDocFile, resolvePageFile } from '@/lib/content';
|
|
3
4
|
import {
|
|
4
5
|
assertDocsConfig,
|
|
5
6
|
getDefaultScope,
|
|
@@ -8,8 +9,9 @@ import {
|
|
|
8
9
|
normalizeDocsSite,
|
|
9
10
|
} from '@/lib/docs-config';
|
|
10
11
|
import { getTextDirection, resolveLocale } from '@/lib/locale';
|
|
11
|
-
import { stripBase } from '@/lib/paths';
|
|
12
|
+
import { DOCS_PREFIX, stripBase } from '@/lib/paths';
|
|
12
13
|
import { resolveSiteModel } from '@/lib/site-model';
|
|
14
|
+
import { getStandalonePageByPathname, normalizeStandalonePages } from '@/lib/standalone-pages';
|
|
13
15
|
import type {
|
|
14
16
|
DocsConfig,
|
|
15
17
|
DocsScope,
|
|
@@ -18,6 +20,7 @@ import type {
|
|
|
18
20
|
NormalizedDocsSite,
|
|
19
21
|
RedirectRule,
|
|
20
22
|
SeoConfig,
|
|
23
|
+
StandalonePage,
|
|
21
24
|
} from '@/lib/types';
|
|
22
25
|
|
|
23
26
|
assertDocsConfig(rawConfig, 'docs.json');
|
|
@@ -33,7 +36,22 @@ export const docsConfig: NormalizedDocsConfig = getDefaultScope(docsSite).docs;
|
|
|
33
36
|
/** Landing page of the default scope: the site-wide "docs home" URL. */
|
|
34
37
|
export const docsHomeUrl = getDefaultScope(docsSite).firstPageUrl;
|
|
35
38
|
|
|
36
|
-
export const siteModel = resolveSiteModel(siteConfig, docsConfig);
|
|
39
|
+
export const siteModel = resolveSiteModel(siteConfig, docsConfig, shisoConfig);
|
|
40
|
+
|
|
41
|
+
/** Standalone pages declared with the top-level `pages` key, e.g. a home page. */
|
|
42
|
+
export const standalonePages: StandalonePage[] = normalizeStandalonePages(
|
|
43
|
+
siteConfig,
|
|
44
|
+
resolvePageFile,
|
|
45
|
+
docsSite,
|
|
46
|
+
{ docsPrefix: DOCS_PREFIX },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/** True when a standalone page owns "/", replacing the root docs redirect. */
|
|
50
|
+
export const hasRootStandalonePage = standalonePages.some(page => page.path === '/');
|
|
51
|
+
|
|
52
|
+
export function getStandalonePage(pathname: string): StandalonePage | null {
|
|
53
|
+
return getStandalonePageByPathname(standalonePages, stripBase(pathname));
|
|
54
|
+
}
|
|
37
55
|
|
|
38
56
|
/** Scope that owns the current pathname; the default scope for unknown paths. */
|
|
39
57
|
export function getScopeByPathname(pathname: string): DocsScope {
|
package/src/lib/site-model.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
NormalizedLink,
|
|
13
13
|
NormalizedNavbar,
|
|
14
14
|
ResolvedContextualOption,
|
|
15
|
+
ResolvedShisoConfig,
|
|
15
16
|
SiteModel,
|
|
16
17
|
ThemeLabels,
|
|
17
18
|
} from '@/lib/types';
|
|
@@ -90,7 +91,11 @@ function normalizeFooter(config: DocsConfig['footer']): NormalizedFooter | null
|
|
|
90
91
|
return socials.length || links.length || attribution ? { socials, links, attribution } : null;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
export function resolveSiteModel(
|
|
94
|
+
export function resolveSiteModel(
|
|
95
|
+
config: DocsConfig,
|
|
96
|
+
docs: NormalizedDocsConfig,
|
|
97
|
+
shiso?: ResolvedShisoConfig,
|
|
98
|
+
): SiteModel {
|
|
94
99
|
const appearance = config.appearance || {};
|
|
95
100
|
const logo = config.logo
|
|
96
101
|
? typeof config.logo === 'string'
|
|
@@ -128,7 +133,7 @@ export function resolveSiteModel(config: DocsConfig, docs: NormalizedDocsConfig)
|
|
|
128
133
|
error404: { ...config.errors?.['404'], redirect: config.errors?.['404']?.redirect !== false },
|
|
129
134
|
showTimestamp: config.metadata?.timestamp === true,
|
|
130
135
|
drilldown: config.interaction?.drilldown,
|
|
131
|
-
locale:
|
|
136
|
+
locale: shiso?.locale || 'en-US',
|
|
132
137
|
labels: SHISO_THEME_LABELS,
|
|
133
138
|
docs,
|
|
134
139
|
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { DocsConfig, NormalizedDocsSite, StandalonePage } from '@/lib/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standalone pages: routes outside the docs navigation, declared with the
|
|
5
|
+
* top-level `pages` key in docs.json. They render with the site chrome
|
|
6
|
+
* (banner, header, footer) but no sidebar or table of contents, and a
|
|
7
|
+
* `path: "/"` entry replaces the default root redirect to the docs home.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface NormalizeStandaloneOptions {
|
|
11
|
+
/** Docs prefix ("" or "/prefix"); standalone paths may not live under it. */
|
|
12
|
+
docsPrefix?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function invalid(message: string): Error {
|
|
16
|
+
return new Error(`Invalid docs config: ${message}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Trims and canonicalizes a standalone route path; throws when malformed. */
|
|
20
|
+
function normalizePath(rawPath: unknown): string {
|
|
21
|
+
const value = typeof rawPath === 'string' ? rawPath.trim() : '';
|
|
22
|
+
|
|
23
|
+
if (!value.startsWith('/')) {
|
|
24
|
+
throw invalid(`standalone page path "${String(rawPath)}" must start with "/".`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (/[:*]/.test(value)) {
|
|
28
|
+
throw invalid(`standalone page path "${value}" must not use wildcard patterns.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (/\.mdx?$/i.test(value)) {
|
|
32
|
+
throw invalid(
|
|
33
|
+
`standalone page path "${value}" must be a route, not a file — drop the extension.`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const collapsed = value.replace(/\/{2,}/g, '/').replace(/\/+$/, '');
|
|
38
|
+
return collapsed || '/';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Mirrors normalizePageReference in docs-config.ts for the `page` slug. */
|
|
42
|
+
function normalizePageSlug(rawSlug: unknown): string {
|
|
43
|
+
const value = typeof rawSlug === 'string' ? rawSlug : '';
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
value
|
|
47
|
+
.trim()
|
|
48
|
+
.replace(/\\/g, '/')
|
|
49
|
+
.replace(/^\/+/, '')
|
|
50
|
+
.replace(/^pages\//, '')
|
|
51
|
+
.replace(/\.mdx?$/, '')
|
|
52
|
+
.replace(/\/+$/, '') || 'index'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function normalizeStandalonePages(
|
|
57
|
+
config: DocsConfig,
|
|
58
|
+
resolvePageFile: (fileSlug: string) => string | undefined,
|
|
59
|
+
site: NormalizedDocsSite,
|
|
60
|
+
options: NormalizeStandaloneOptions = {},
|
|
61
|
+
): StandalonePage[] {
|
|
62
|
+
const items = config.pages || [];
|
|
63
|
+
|
|
64
|
+
if (!items.length) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const docsPrefix = options.docsPrefix || '';
|
|
69
|
+
const pages: StandalonePage[] = [];
|
|
70
|
+
const seen = new Set<string>();
|
|
71
|
+
|
|
72
|
+
for (const item of items) {
|
|
73
|
+
const path = normalizePath(item?.path);
|
|
74
|
+
|
|
75
|
+
if (seen.has(path)) {
|
|
76
|
+
throw invalid(`duplicate standalone page path "${path}".`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
seen.add(path);
|
|
80
|
+
|
|
81
|
+
if (path === '/404') {
|
|
82
|
+
throw invalid('standalone page path "/404" is reserved for the error page.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const docsPage = site.pageByUrl[path];
|
|
86
|
+
|
|
87
|
+
if (docsPage) {
|
|
88
|
+
throw invalid(
|
|
89
|
+
`standalone page path "${path}" collides with the docs page "${docsPage.fileSlug}". ` +
|
|
90
|
+
'Standalone pages must live outside the docs navigation.',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (docsPrefix && (path === docsPrefix || path.startsWith(`${docsPrefix}/`))) {
|
|
95
|
+
throw invalid(
|
|
96
|
+
`standalone page path "${path}" is inside the docs prefix "${docsPrefix}". ` +
|
|
97
|
+
'Standalone pages must live outside the docs tree.',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const fileSlug = normalizePageSlug(item?.page);
|
|
102
|
+
const filePath = resolvePageFile(fileSlug);
|
|
103
|
+
|
|
104
|
+
if (!filePath) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Missing standalone page file for "${fileSlug}": expected ` +
|
|
107
|
+
`"content/pages/${fileSlug}.mdx" or ".md".`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
pages.push({ path, filePath, title: item?.title?.trim() || undefined });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return pages;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Exact standalone page lookup by base-relative pathname. Tolerates trailing
|
|
119
|
+
* slashes and an explicit `/index` suffix, like getPageByPathname.
|
|
120
|
+
*/
|
|
121
|
+
export function getStandalonePageByPathname(
|
|
122
|
+
pages: StandalonePage[],
|
|
123
|
+
pathname: string,
|
|
124
|
+
): StandalonePage | null {
|
|
125
|
+
const trimmed = pathname.replace(/\/+$/, '') || '/';
|
|
126
|
+
const collapsed = trimmed === '/index' ? '/' : trimmed.replace(/\/index$/, '') || '/';
|
|
127
|
+
|
|
128
|
+
return pages.find(page => page.path === trimmed || page.path === collapsed) || null;
|
|
129
|
+
}
|
package/src/lib/types.ts
CHANGED
|
@@ -101,8 +101,9 @@ export type LogoOption =
|
|
|
101
101
|
| string
|
|
102
102
|
| { light?: string; dark?: string; href?: string; target?: LinkTarget };
|
|
103
103
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
104
|
+
/** Project-level settings supplied by shiso.config.ts. Mirrors the public
|
|
105
|
+
* shape exported from "@umami/shiso/config". */
|
|
106
|
+
export interface ShisoConfig {
|
|
106
107
|
/** Where docs pages are mounted within the site. Default "/docs"; "" for root. */
|
|
107
108
|
docsPrefix?: string;
|
|
108
109
|
/** Content directory relative to the project root. Default "content/docs". */
|
|
@@ -113,6 +114,14 @@ export interface ShisoOptions {
|
|
|
113
114
|
locale?: string;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
/** ShisoConfig after defaults and normalization, as served by `virtual:shiso-config`. */
|
|
118
|
+
export interface ResolvedShisoConfig {
|
|
119
|
+
docsPrefix: string;
|
|
120
|
+
contentDir: string;
|
|
121
|
+
siteUrl?: string;
|
|
122
|
+
locale: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
116
125
|
export type LinkTarget = '_self' | '_blank';
|
|
117
126
|
|
|
118
127
|
/** A user-configured link. Presentation is determined entirely by its fields. */
|
|
@@ -166,6 +175,26 @@ export interface RedirectRule {
|
|
|
166
175
|
destination: string;
|
|
167
176
|
}
|
|
168
177
|
|
|
178
|
+
/** A standalone page outside the docs navigation, e.g. a landing page. */
|
|
179
|
+
export interface StandalonePageItem {
|
|
180
|
+
/** Route path, starting with "/". "/" replaces the root redirect to docs. */
|
|
181
|
+
path: string;
|
|
182
|
+
/** File slug under content/pages, e.g. "home" for content/pages/home.mdx. */
|
|
183
|
+
page: string;
|
|
184
|
+
/** Page title used in the document head. Frontmatter title wins. */
|
|
185
|
+
title?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Normalized standalone page. */
|
|
189
|
+
export interface StandalonePage {
|
|
190
|
+
/** Base-relative route, e.g. "/" or "/about". */
|
|
191
|
+
path: string;
|
|
192
|
+
/** Module key of the MDX file, e.g. "/content/pages/home.mdx". */
|
|
193
|
+
filePath: string;
|
|
194
|
+
/** Config-level head-title override. */
|
|
195
|
+
title?: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
169
198
|
export interface SeoConfig {
|
|
170
199
|
/** Extra meta tags added to every page, e.g. { "og:image": "/social.png" }. */
|
|
171
200
|
metatags?: Record<string, string>;
|
|
@@ -271,7 +300,6 @@ export interface BackgroundConfig {
|
|
|
271
300
|
|
|
272
301
|
export interface DocsConfig {
|
|
273
302
|
$schema?: string;
|
|
274
|
-
$shiso?: ShisoOptions;
|
|
275
303
|
theme?: string;
|
|
276
304
|
name?: string;
|
|
277
305
|
colors?: ThemeColors;
|
|
@@ -279,6 +307,8 @@ export interface DocsConfig {
|
|
|
279
307
|
favicon?: string;
|
|
280
308
|
description?: string;
|
|
281
309
|
navigation: NavigationConfig;
|
|
310
|
+
/** Standalone pages outside the docs navigation, e.g. a landing page at "/". */
|
|
311
|
+
pages?: StandalonePageItem[];
|
|
282
312
|
navbar?: NavbarConfig;
|
|
283
313
|
footer?: FooterConfig;
|
|
284
314
|
banner?: BannerConfig;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useLocation } from 'react-router';
|
|
3
|
+
import { Docs } from '@/components/Docs';
|
|
4
|
+
import { Footer } from '@/components/Footer';
|
|
5
|
+
import { getDocModule } from '@/lib/content';
|
|
6
|
+
import type { SiteModel, StandalonePage } from '@/lib/types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A standalone (non-docs) page: site chrome from Layout (banner, header),
|
|
10
|
+
* the MDX content at full container width — no sidebar, TOC, or pager — and
|
|
11
|
+
* the footer. MDX components come from the app-level MDXProvider.
|
|
12
|
+
*/
|
|
13
|
+
export function StandalonePageView({ page, site }: { page: StandalonePage; site: SiteModel }) {
|
|
14
|
+
const { pathname } = useLocation();
|
|
15
|
+
const doc = getDocModule(page.filePath);
|
|
16
|
+
|
|
17
|
+
// Start each newly loaded page at the top, like Docs does for docs routes.
|
|
18
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: pathname is the trigger
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!window.location.hash) {
|
|
21
|
+
window.scrollTo({ top: 0, left: 0 });
|
|
22
|
+
}
|
|
23
|
+
}, [pathname]);
|
|
24
|
+
|
|
25
|
+
// Normalization guarantees the file exists; this is a build-drift safety net.
|
|
26
|
+
if (!doc) {
|
|
27
|
+
return <Docs page={null} doc={null} site={site} />;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const Content = doc.default;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex min-h-full flex-col">
|
|
34
|
+
<article className="grow py-8">
|
|
35
|
+
<div className="docs-markdown">
|
|
36
|
+
<Content />
|
|
37
|
+
</div>
|
|
38
|
+
</article>
|
|
39
|
+
<Footer footer={site.footer} />
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
package/src/styles/global.css
CHANGED
|
@@ -60,10 +60,17 @@
|
|
|
60
60
|
/* Markdown content */
|
|
61
61
|
|
|
62
62
|
@layer components {
|
|
63
|
+
/* Body copy is muted; headings, bold text, links, and inline code stay at
|
|
64
|
+
full foreground brightness so they stand out against the running text. */
|
|
63
65
|
.docs-markdown {
|
|
64
66
|
margin-bottom: 2.5rem;
|
|
65
67
|
font-size: 16px;
|
|
66
68
|
line-height: 1.75;
|
|
69
|
+
color: color-mix(in srgb, var(--foreground) 40%, var(--muted-foreground));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
[data-theme="dark"] .docs-markdown {
|
|
73
|
+
color: color-mix(in srgb, var(--foreground) 25%, var(--muted-foreground));
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
.docs-markdown > * + * {
|
|
@@ -72,7 +79,7 @@
|
|
|
72
79
|
|
|
73
80
|
.docs-markdown :where(h2, h3, h4, h5, h6) {
|
|
74
81
|
font-family: var(--font-heading);
|
|
75
|
-
font-weight: var(--font-heading-weight,
|
|
82
|
+
font-weight: var(--font-heading-weight, 600);
|
|
76
83
|
letter-spacing: -0.02em;
|
|
77
84
|
line-height: 1.25;
|
|
78
85
|
color: var(--foreground);
|
|
@@ -129,7 +136,13 @@
|
|
|
129
136
|
margin: 0.35rem 0;
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
.docs-markdown :where(strong, b) {
|
|
140
|
+
color: var(--foreground);
|
|
141
|
+
font-weight: 600;
|
|
142
|
+
}
|
|
143
|
+
|
|
132
144
|
.docs-markdown a {
|
|
145
|
+
color: var(--foreground);
|
|
133
146
|
text-decoration: underline;
|
|
134
147
|
text-decoration-color: color-mix(in srgb, currentColor 30%, transparent);
|
|
135
148
|
text-underline-offset: 0.2em;
|
|
@@ -140,6 +153,7 @@
|
|
|
140
153
|
}
|
|
141
154
|
|
|
142
155
|
.docs-markdown code:not(pre code) {
|
|
156
|
+
color: var(--foreground);
|
|
143
157
|
border-radius: var(--radius-sm);
|
|
144
158
|
background: color-mix(in srgb, currentColor 4%, transparent);
|
|
145
159
|
padding: 0.1rem 0.3rem;
|
|
@@ -158,6 +172,10 @@
|
|
|
158
172
|
border-radius: var(--radius-lg);
|
|
159
173
|
}
|
|
160
174
|
|
|
175
|
+
.docs-markdown th {
|
|
176
|
+
color: var(--foreground);
|
|
177
|
+
}
|
|
178
|
+
|
|
161
179
|
.docs-markdown th,
|
|
162
180
|
.docs-markdown td {
|
|
163
181
|
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;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for shiso.config.ts. Kept self-contained (no imports) so the
|
|
3
|
+
* config file typechecks in consuming projects without pulling in the runtime.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Project-level settings supplied by shiso.config.ts. All fields are optional. */
|
|
7
|
+
export interface ShisoConfig {
|
|
8
|
+
/** Route prefix for docs pages within the site. Default "/docs"; "" serves docs at the site root. */
|
|
9
|
+
docsPrefix?: string;
|
|
10
|
+
/** Content directory relative to the project root. Default "content/docs". */
|
|
11
|
+
contentDir?: string;
|
|
12
|
+
/** Absolute site origin (e.g. "https://docs.example.com") used for canonical URLs, og:url, and the sitemap. */
|
|
13
|
+
siteUrl?: string;
|
|
14
|
+
/** Locale used for deterministic date formatting. Default "en-US". */
|
|
15
|
+
locale?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Identity helper that types a shiso.config.ts default export. */
|
|
19
|
+
export declare function defineConfig(config: ShisoConfig): ShisoConfig;
|
package/vite.config.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { generateIconRegistry } from './scripts/generate-icon-registry.mjs';
|
|
|
9
9
|
import { shisoLastModified } from './scripts/generate-last-modified.mjs';
|
|
10
10
|
import { generateSearchIndex } from './scripts/generate-search-index.mjs';
|
|
11
11
|
import { createDocsConfigModule } from './scripts/vite-docs-config.mjs';
|
|
12
|
-
import type { DocsConfig } from './src/lib/types.ts';
|
|
12
|
+
import type { DocsConfig, ResolvedShisoConfig } from './src/lib/types.ts';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Keeps src/lib/icon-registry.generated.ts in sync with the `icon="name"` values
|
|
@@ -37,15 +37,25 @@ function shisoIconRegistry(getDocsConfig: () => DocsConfig, root: string, output
|
|
|
37
37
|
* Keeps src/lib/search-index.generated.ts in sync with content, so the search
|
|
38
38
|
* dialog can query page text without a server.
|
|
39
39
|
*/
|
|
40
|
-
function shisoSearchIndex(
|
|
40
|
+
function shisoSearchIndex(
|
|
41
|
+
getDocsConfig: () => DocsConfig,
|
|
42
|
+
getShisoConfig: () => ResolvedShisoConfig,
|
|
43
|
+
root: string,
|
|
44
|
+
output: string,
|
|
45
|
+
): Plugin {
|
|
41
46
|
return {
|
|
42
47
|
name: 'shiso-search-index',
|
|
43
48
|
async buildStart() {
|
|
44
|
-
await generateSearchIndex({ config: getDocsConfig(), root, output });
|
|
49
|
+
await generateSearchIndex({ config: getDocsConfig(), shiso: getShisoConfig(), root, output });
|
|
45
50
|
},
|
|
46
51
|
async handleHotUpdate({ file }) {
|
|
47
|
-
if (/\.(md|mdx)$/.test(file) || file.endsWith('docs.json')) {
|
|
48
|
-
await generateSearchIndex({
|
|
52
|
+
if (/\.(md|mdx)$/.test(file) || file.endsWith('docs.json') || /shiso\.config\.\w+$/.test(file)) {
|
|
53
|
+
await generateSearchIndex({
|
|
54
|
+
config: getDocsConfig(),
|
|
55
|
+
shiso: getShisoConfig(),
|
|
56
|
+
root,
|
|
57
|
+
output,
|
|
58
|
+
});
|
|
49
59
|
}
|
|
50
60
|
},
|
|
51
61
|
};
|
|
@@ -288,7 +298,11 @@ function shisoHtml(getDocsConfig: () => DocsConfig): Plugin {
|
|
|
288
298
|
* production builds. The contextual menu's copy/view options and AI links
|
|
289
299
|
* depend on these URLs.
|
|
290
300
|
*/
|
|
291
|
-
function shisoMarkdownDev(
|
|
301
|
+
function shisoMarkdownDev(
|
|
302
|
+
getDocsConfig: () => DocsConfig,
|
|
303
|
+
getShisoConfig: () => ResolvedShisoConfig,
|
|
304
|
+
root: string,
|
|
305
|
+
): Plugin {
|
|
292
306
|
return {
|
|
293
307
|
name: 'shiso-markdown-dev',
|
|
294
308
|
apply: 'serve',
|
|
@@ -300,17 +314,8 @@ function shisoMarkdownDev(getDocsConfig: () => DocsConfig, root: string): Plugin
|
|
|
300
314
|
return next();
|
|
301
315
|
}
|
|
302
316
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
{};
|
|
306
|
-
const contentDir = (shiso.contentDir ?? 'content/docs').replace(/^\/+|\/+$/g, '');
|
|
307
|
-
const prefixValue = (shiso.docsPrefix ?? '/docs').trim().replace(/\/+$/, '');
|
|
308
|
-
const docsPrefix =
|
|
309
|
-
!prefixValue || prefixValue === '/'
|
|
310
|
-
? ''
|
|
311
|
-
: prefixValue.startsWith('/')
|
|
312
|
-
? prefixValue
|
|
313
|
-
: `/${prefixValue}`;
|
|
317
|
+
// Values arrive with defaults applied and already normalized.
|
|
318
|
+
const { contentDir, docsPrefix } = getShisoConfig();
|
|
314
319
|
|
|
315
320
|
let route = decodeURIComponent(url).slice(0, -'.md'.length);
|
|
316
321
|
const base = server.config.base.replace(/\/+$/, '');
|
|
@@ -319,6 +324,39 @@ function shisoMarkdownDev(getDocsConfig: () => DocsConfig, root: string): Plugin
|
|
|
319
324
|
route = route.slice(base.length);
|
|
320
325
|
}
|
|
321
326
|
|
|
327
|
+
// Standalone pages (top-level `pages` key) live under content/pages,
|
|
328
|
+
// outside the docs prefix. "/index.md" maps to the "/" entry.
|
|
329
|
+
const routeKey = (route.replace(/\/+$/, '') || '/').replace(/^\/index$/, '/');
|
|
330
|
+
const standalone = (getDocsConfig().pages || []).find(
|
|
331
|
+
item => (item?.path?.trim().replace(/\/+$/, '') || '/') === routeKey,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
if (standalone?.page) {
|
|
335
|
+
const pagesRoot = path.resolve(root, 'content/pages');
|
|
336
|
+
const pageSlug = standalone.page
|
|
337
|
+
.trim()
|
|
338
|
+
.replace(/^\/+/, '')
|
|
339
|
+
.replace(/\.mdx?$/, '');
|
|
340
|
+
|
|
341
|
+
for (const candidate of [`${pageSlug}.mdx`, `${pageSlug}.md`]) {
|
|
342
|
+
const filePath = path.resolve(pagesRoot, candidate);
|
|
343
|
+
|
|
344
|
+
// Never read outside the pages directory.
|
|
345
|
+
if (!filePath.startsWith(pagesRoot + path.sep)) {
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const source = await readFile(filePath, 'utf8');
|
|
351
|
+
res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
|
|
352
|
+
res.end(source);
|
|
353
|
+
return;
|
|
354
|
+
} catch {
|
|
355
|
+
// Try the next candidate.
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
322
360
|
if (docsPrefix && route.startsWith(docsPrefix)) {
|
|
323
361
|
route = route.slice(docsPrefix.length);
|
|
324
362
|
}
|
|
@@ -364,6 +402,7 @@ export default defineConfig(async () => {
|
|
|
364
402
|
root: projectRoot,
|
|
365
403
|
});
|
|
366
404
|
const getDocsConfig = configModule.getConfig as () => DocsConfig;
|
|
405
|
+
const getShisoConfig = configModule.getShisoConfig as () => ResolvedShisoConfig;
|
|
367
406
|
|
|
368
407
|
return {
|
|
369
408
|
optimizeDeps: {
|
|
@@ -385,11 +424,12 @@ export default defineConfig(async () => {
|
|
|
385
424
|
}),
|
|
386
425
|
shisoSearchIndex(
|
|
387
426
|
getDocsConfig,
|
|
427
|
+
getShisoConfig,
|
|
388
428
|
projectRoot,
|
|
389
429
|
path.join(generatedRoot, 'search-index.generated.ts'),
|
|
390
430
|
),
|
|
391
431
|
shisoHtml(getDocsConfig),
|
|
392
|
-
shisoMarkdownDev(getDocsConfig, projectRoot),
|
|
432
|
+
shisoMarkdownDev(getDocsConfig, getShisoConfig, projectRoot),
|
|
393
433
|
shisoMdx(),
|
|
394
434
|
react({ include: /\.(mdx|md|tsx|ts|jsx|js)$/ }),
|
|
395
435
|
],
|