@umami/shiso 0.54.0 → 0.61.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +9 -49
  3. package/bin/shiso.mjs +132 -0
  4. package/docs.schema.json +835 -0
  5. package/mdx.config.ts +143 -0
  6. package/package.json +73 -83
  7. package/scripts/check-package.mjs +18 -0
  8. package/scripts/generate-icon-registry.mjs +196 -0
  9. package/scripts/generate-last-modified.mjs +128 -0
  10. package/scripts/generate-search-index.mjs +252 -0
  11. package/scripts/load-docs-config.mjs +244 -0
  12. package/scripts/prerender.mjs +187 -0
  13. package/scripts/validate-config.mjs +104 -0
  14. package/scripts/vite-docs-config.mjs +60 -0
  15. package/src/App.tsx +39 -0
  16. package/src/components/Banner.tsx +69 -0
  17. package/src/components/CodeBlock.tsx +46 -0
  18. package/src/components/ConfiguredIcon.tsx +15 -0
  19. package/src/components/ContextualMenu.tsx +93 -0
  20. package/src/components/DocContent.tsx +100 -0
  21. package/src/components/Docs.tsx +126 -0
  22. package/src/components/Footer.tsx +84 -0
  23. package/src/components/Header.tsx +72 -0
  24. package/src/components/Layout.tsx +16 -0
  25. package/src/components/PageLinks.tsx +71 -0
  26. package/src/components/Search.tsx +208 -0
  27. package/src/components/SideNav.tsx +347 -0
  28. package/src/components/SocialIcon.tsx +88 -0
  29. package/src/components/ThemeToggle.tsx +34 -0
  30. package/src/components/TopNav.tsx +127 -0
  31. package/src/components/docs/Accordion.tsx +68 -0
  32. package/src/components/docs/Badge.tsx +171 -0
  33. package/src/components/docs/Callout.tsx +73 -0
  34. package/src/components/docs/Card.tsx +158 -0
  35. package/src/components/docs/CodeGroup.tsx +73 -0
  36. package/src/components/docs/Columns.tsx +20 -0
  37. package/src/components/docs/Expandable.tsx +28 -0
  38. package/src/components/docs/Frame.tsx +56 -0
  39. package/src/components/docs/Icon.tsx +30 -0
  40. package/src/components/docs/ParamField.tsx +45 -0
  41. package/src/components/docs/PropertiesTable.tsx +84 -0
  42. package/src/components/docs/ResponseField.tsx +36 -0
  43. package/src/components/docs/Steps.tsx +47 -0
  44. package/src/components/docs/Tabs.tsx +116 -0
  45. package/src/components/docs/Tooltip.tsx +21 -0
  46. package/src/components/docs/index.ts +15 -0
  47. package/src/components/docs/styles.ts +82 -0
  48. package/src/components/docs/utils.ts +118 -0
  49. package/src/components/icons/index.ts +17 -0
  50. package/src/components/ui/accordion.tsx +69 -0
  51. package/src/components/ui/alert.tsx +69 -0
  52. package/src/components/ui/badge.tsx +49 -0
  53. package/src/components/ui/button.tsx +58 -0
  54. package/src/components/ui/card.tsx +88 -0
  55. package/src/components/ui/collapsible.tsx +15 -0
  56. package/src/components/ui/command.tsx +173 -0
  57. package/src/components/ui/dialog.tsx +137 -0
  58. package/src/components/ui/dropdown-menu.tsx +257 -0
  59. package/src/components/ui/scroll-area.tsx +71 -0
  60. package/src/components/ui/sheet.tsx +124 -0
  61. package/src/components/ui/tabs.tsx +73 -0
  62. package/src/components/ui/tooltip.tsx +52 -0
  63. package/src/declarations.d.ts +9 -0
  64. package/src/entry-client.tsx +17 -0
  65. package/src/entry-server.tsx +77 -0
  66. package/src/generated/last-modified.ts +2 -0
  67. package/src/lib/content.ts +44 -0
  68. package/src/lib/docs-config.ts +682 -0
  69. package/src/lib/head.ts +220 -0
  70. package/src/lib/icon-registry.generated.ts +4 -0
  71. package/src/lib/icons.ts +29 -0
  72. package/src/lib/inline-markdown.tsx +86 -0
  73. package/src/lib/mdast.ts +56 -0
  74. package/src/lib/paths.ts +86 -0
  75. package/src/lib/remark-toc.ts +71 -0
  76. package/src/lib/search/config.ts +43 -0
  77. package/src/lib/search/provider.ts +80 -0
  78. package/src/lib/search/providers/local.ts +15 -0
  79. package/src/lib/search-index.generated.ts +4 -0
  80. package/src/lib/search.ts +100 -0
  81. package/src/lib/site-config.ts +104 -0
  82. package/src/lib/site-model.ts +221 -0
  83. package/src/lib/slug.ts +38 -0
  84. package/src/lib/types.ts +478 -0
  85. package/src/lib/utils.ts +6 -0
  86. package/src/pages/DocPage.tsx +33 -0
  87. package/src/styles/global.css +268 -0
  88. package/src/styles/tokens.css +114 -0
  89. package/types/client.d.ts +3 -0
  90. package/types/search.d.ts +17 -0
  91. package/vite.config.ts +342 -0
  92. package/LICENSE +0 -21
  93. package/dist/index.css +0 -189
  94. package/dist/index.d.ts +0 -57
  95. package/dist/index.js +0 -464
  96. package/dist/index.mjs +0 -437
  97. package/server/index.d.ts +0 -30
  98. package/server/index.js +0 -189
  99. package/styles.css +0 -4766
@@ -0,0 +1,4 @@
1
+ // Build-time alias target. Shiso replaces this module with the project's generated index.
2
+ import type { SearchRecord } from '@/lib/search';
3
+
4
+ export const SEARCH_INDEX: SearchRecord[] = [];
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Client-side search over the build-time index
3
+ * (see scripts/generate-search-index.mjs).
4
+ *
5
+ * Plain substring scoring, no dependencies: docs sites are small enough that
6
+ * scanning a few hundred sections per keystroke is cheap, and substring
7
+ * matches behave predictably for exact terms like config keys.
8
+ */
9
+
10
+ export interface SearchRecord {
11
+ /** Base-relative route of the page. */
12
+ url: string;
13
+ /** Page title. */
14
+ page: string;
15
+ /** Section heading, absent for the page intro. */
16
+ heading?: string;
17
+ /** Heading anchor id, matching the rendered heading. */
18
+ id?: string;
19
+ /** Plain text of the section. */
20
+ text: string;
21
+ }
22
+
23
+ export interface SearchResult {
24
+ /** Route including the section anchor. */
25
+ url: string;
26
+ /** Page title. */
27
+ page: string;
28
+ /** Section heading, absent for the page intro. */
29
+ heading?: string;
30
+ /** Snippet of section text around the first match, when the text matched. */
31
+ snippet?: string;
32
+ /** Provider-specific relevance score. Use 0 when a provider does not expose one. */
33
+ score: number;
34
+ }
35
+
36
+ const SNIPPET_RADIUS = 60;
37
+
38
+ function makeSnippet(text: string, index: number, length: number): string {
39
+ const start = Math.max(0, index - SNIPPET_RADIUS);
40
+ const end = Math.min(text.length, index + length + SNIPPET_RADIUS);
41
+
42
+ return `${start > 0 ? '…' : ''}${text.slice(start, end).trim()}${end < text.length ? '…' : ''}`;
43
+ }
44
+
45
+ export function searchIndex(records: SearchRecord[], query: string, limit = 10): SearchResult[] {
46
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
47
+
48
+ if (!terms.length) {
49
+ return [];
50
+ }
51
+
52
+ const results: SearchResult[] = [];
53
+
54
+ for (const record of records) {
55
+ const page = record.page.toLowerCase();
56
+ const heading = (record.heading || '').toLowerCase();
57
+ const text = record.text.toLowerCase();
58
+
59
+ let score = 0;
60
+ let snippetAt = -1;
61
+ let snippetLength = 0;
62
+ let matched = true;
63
+
64
+ for (const term of terms) {
65
+ if (page.includes(term)) {
66
+ score += page === term ? 40 : 20;
67
+ } else if (heading.includes(term)) {
68
+ score += heading === term ? 30 : 15;
69
+ } else {
70
+ const index = text.indexOf(term);
71
+
72
+ if (index === -1) {
73
+ matched = false;
74
+ break;
75
+ }
76
+
77
+ score += 5;
78
+
79
+ if (snippetAt === -1) {
80
+ snippetAt = index;
81
+ snippetLength = term.length;
82
+ }
83
+ }
84
+ }
85
+
86
+ if (!matched || !score) {
87
+ continue;
88
+ }
89
+
90
+ results.push({
91
+ url: record.id ? `${record.url}#${record.id}` : record.url,
92
+ page: record.page,
93
+ heading: record.heading,
94
+ snippet: snippetAt >= 0 ? makeSnippet(record.text, snippetAt, snippetLength) : undefined,
95
+ score,
96
+ });
97
+ }
98
+
99
+ return results.sort((a, b) => b.score - a.score).slice(0, limit);
100
+ }
@@ -0,0 +1,104 @@
1
+ import rawConfig from 'virtual:shiso-docs-config';
2
+ import { resolveDocFile } from '@/lib/content';
3
+ import { assertDocsConfig, normalizeDocsConfig } from '@/lib/docs-config';
4
+ import { stripBase, stripDocsPrefix } from '@/lib/paths';
5
+ import { resolveSiteModel } from '@/lib/site-model';
6
+ import type {
7
+ DocsConfig,
8
+ NormalizedDocsConfig,
9
+ NormalizedDocsPage,
10
+ RedirectRule,
11
+ SeoConfig,
12
+ } from '@/lib/types';
13
+
14
+ assertDocsConfig(rawConfig, 'docs.json');
15
+
16
+ export const siteConfig: DocsConfig = rawConfig;
17
+
18
+ export const docsConfig: NormalizedDocsConfig = normalizeDocsConfig(siteConfig, resolveDocFile);
19
+
20
+ export const siteModel = resolveSiteModel(siteConfig, docsConfig);
21
+
22
+ export const siteName = siteModel.name;
23
+
24
+ /** Trailing-slash-insensitive route key for redirect matching. */
25
+ function toRouteKey(routePath: string): string {
26
+ const trimmed = routePath.replace(/\/+$/, '');
27
+ return trimmed || '/';
28
+ }
29
+
30
+ /**
31
+ * Redirect rules with exact-match sources. Wildcard patterns are part of the
32
+ * standard but not implemented; they are skipped with a warning.
33
+ */
34
+ export function getRedirects(): RedirectRule[] {
35
+ return (siteConfig.redirects || []).filter(rule => {
36
+ if (!rule?.source || !rule.destination) {
37
+ return false;
38
+ }
39
+
40
+ if (/[:*]/.test(rule.source)) {
41
+ console.warn(
42
+ `[shiso] Redirect source "${rule.source}" uses a wildcard pattern, which is not ` +
43
+ 'implemented yet — it will be skipped.',
44
+ );
45
+ return false;
46
+ }
47
+
48
+ return true;
49
+ });
50
+ }
51
+
52
+ const redirectBySource = new Map(
53
+ getRedirects().map(rule => [toRouteKey(rule.source), rule.destination]),
54
+ );
55
+
56
+ /** Destination for a base-relative route covered by a redirect rule, if any. */
57
+ export function matchRedirect(routePath: string): string | null {
58
+ return redirectBySource.get(toRouteKey(routePath)) || null;
59
+ }
60
+
61
+ export function getSeo(): SeoConfig {
62
+ const seo = siteConfig.seo || {};
63
+
64
+ return {
65
+ metatags: seo.metatags || {},
66
+ indexing: seo.indexing === 'all' ? 'all' : 'navigable',
67
+ };
68
+ }
69
+
70
+ /** True when the last-modified timestamp should show for a page. */
71
+ export function showTimestamp(frontmatterValue: unknown): boolean {
72
+ if (typeof frontmatterValue === 'boolean') {
73
+ return frontmatterValue;
74
+ }
75
+
76
+ return siteModel.showTimestamp;
77
+ }
78
+
79
+ export function normalizeParamSlug(slug: string): string {
80
+ const cleaned = slug.replace(/^\/+|\/+$/g, '');
81
+
82
+ if (!cleaned) {
83
+ return 'index';
84
+ }
85
+
86
+ if (cleaned === 'index') {
87
+ return cleaned;
88
+ }
89
+
90
+ return cleaned.replace(/\/index$/, '') || 'index';
91
+ }
92
+
93
+ export function getPageByPathname(pathname: string): NormalizedDocsPage | null {
94
+ const slug = normalizeParamSlug(stripDocsPrefix(stripBase(pathname)));
95
+ return docsConfig.pageByLookupSlug[slug] || null;
96
+ }
97
+
98
+ export function getPageTitle(pageTitle?: string): string {
99
+ if (pageTitle && siteName) {
100
+ return `${pageTitle} – ${siteName}`;
101
+ }
102
+
103
+ return pageTitle || siteName || '';
104
+ }
@@ -0,0 +1,221 @@
1
+ import { toAbsoluteUrl, toHref } from '@/lib/paths';
2
+ import { resolveSearchConfig } from '@/lib/search/config';
3
+ import type {
4
+ ConfigLink,
5
+ ContextualOption,
6
+ ContextualOptionObject,
7
+ DocsConfig,
8
+ LinkTarget,
9
+ NormalizedDocsConfig,
10
+ NormalizedDocsPage,
11
+ NormalizedFooter,
12
+ NormalizedLink,
13
+ NormalizedNavbar,
14
+ ResolvedContextualOption,
15
+ SiteModel,
16
+ ThemeLabels,
17
+ } from '@/lib/types';
18
+
19
+ const SHISO_THEME_LABELS: ThemeLabels = {
20
+ menu: 'Menu',
21
+ documentationNavigation: 'Documentation navigation',
22
+ sections: 'Sections',
23
+ tableOfContents: 'On this page',
24
+ tableOfContentsNavigation: 'Table of contents',
25
+ searchTitle: 'Search',
26
+ searching: 'Searching...',
27
+ searchUnavailable: 'Search unavailable',
28
+ noResults: 'No results',
29
+ lastUpdated: 'Last updated on',
30
+ notFound: 'Page not found',
31
+ dismissBanner: 'Dismiss banner',
32
+ toggleTheme: 'Toggle theme',
33
+ moreOptions: 'More options',
34
+ copied: 'Copied',
35
+ expand: 'Expand',
36
+ collapse: 'Collapse',
37
+ copyPage: 'Copy page',
38
+ copyPageDescription: 'Copy this page as Markdown',
39
+ viewMarkdown: 'View as Markdown',
40
+ viewMarkdownDescription: 'Open this page as plain Markdown',
41
+ openInChatGPT: 'Open in ChatGPT',
42
+ openInClaude: 'Open in Claude',
43
+ openInPerplexity: 'Open in Perplexity',
44
+ askQuestionsAboutPage: 'Ask questions about this page',
45
+ };
46
+
47
+ function isInternalHref(href: string): boolean {
48
+ return /^(?:#|\/|\.\.?\/)/.test(href);
49
+ }
50
+
51
+ export function resolveLinkTarget(href: string, target?: LinkTarget): LinkTarget {
52
+ return target || (isInternalHref(href) ? '_self' : '_blank');
53
+ }
54
+
55
+ function normalizeLink(link: ConfigLink): NormalizedLink {
56
+ return {
57
+ href: link.href,
58
+ label: link.label?.trim() || undefined,
59
+ ariaLabel: link.ariaLabel?.trim() || undefined,
60
+ icon: link.icon?.trim() || undefined,
61
+ target: resolveLinkTarget(link.href, link.target),
62
+ };
63
+ }
64
+
65
+ function normalizeNavbar(config: DocsConfig['navbar']): NormalizedNavbar | null {
66
+ if (!config) {
67
+ return null;
68
+ }
69
+
70
+ const links = (config.links || []).filter(link => !!link?.href).map(normalizeLink);
71
+ const primary = config.primary?.href ? normalizeLink(config.primary) : undefined;
72
+
73
+ return links.length || primary ? { links, primary } : null;
74
+ }
75
+
76
+ function normalizeFooter(config: DocsConfig['footer']): NormalizedFooter | null {
77
+ const socials = (config?.socials || []).filter(link => !!link?.href).map(normalizeLink);
78
+ const links = (config?.links || [])
79
+ .filter(column => column?.items?.length)
80
+ .map(column => ({
81
+ ...column,
82
+ items: column.items.map(item => ({
83
+ ...item,
84
+ target: resolveLinkTarget(item.href, item.target),
85
+ })),
86
+ }));
87
+ const attribution = config?.attribution !== false;
88
+
89
+ return socials.length || links.length || attribution ? { socials, links, attribution } : null;
90
+ }
91
+
92
+ export function resolveSiteModel(config: DocsConfig, docs: NormalizedDocsConfig): SiteModel {
93
+ const appearance = config.appearance || {};
94
+ const logo = config.logo
95
+ ? typeof config.logo === 'string'
96
+ ? { light: config.logo, dark: config.logo }
97
+ : {
98
+ light: config.logo.light || config.logo.dark,
99
+ dark: config.logo.dark || config.logo.light,
100
+ href: config.logo.href,
101
+ target: config.logo.href
102
+ ? resolveLinkTarget(config.logo.href, config.logo.target)
103
+ : undefined,
104
+ }
105
+ : null;
106
+
107
+ return {
108
+ name: config.name?.trim() || undefined,
109
+ logo,
110
+ navbar: normalizeNavbar(config.navbar),
111
+ footer: normalizeFooter(config.footer),
112
+ banner: config.banner?.content?.trim()
113
+ ? { content: config.banner.content.trim(), dismissible: config.banner.dismissible === true }
114
+ : null,
115
+ appearance: {
116
+ default:
117
+ appearance.default === 'light' || appearance.default === 'dark'
118
+ ? appearance.default
119
+ : 'system',
120
+ strict: appearance.strict === true,
121
+ },
122
+ styling: {
123
+ eyebrows: config.styling?.eyebrows === 'breadcrumbs' ? 'breadcrumbs' : 'section',
124
+ },
125
+ search: resolveSearchConfig(config.search),
126
+ contextualOptions: config.contextual?.options || [],
127
+ error404: { ...config.errors?.['404'], redirect: config.errors?.['404']?.redirect !== false },
128
+ showTimestamp: config.metadata?.timestamp === true,
129
+ drilldown: config.interaction?.drilldown,
130
+ locale: config.$shiso?.locale?.trim() || 'en-US',
131
+ labels: SHISO_THEME_LABELS,
132
+ docs,
133
+ };
134
+ }
135
+
136
+ function aiPrompt(mdUrl: string): string {
137
+ return `Read ${mdUrl} so I can ask questions about it.`;
138
+ }
139
+
140
+ export function resolveContextualOptions(
141
+ options: ContextualOption[],
142
+ page: NormalizedDocsPage,
143
+ labels: ThemeLabels = SHISO_THEME_LABELS,
144
+ ): ResolvedContextualOption[] {
145
+ const mdHref = `${toHref(page.url)}.md`;
146
+ const absolutePageUrl = toAbsoluteUrl(page.url);
147
+ const mdUrl = absolutePageUrl ? `${absolutePageUrl}.md` : undefined;
148
+ const resolved: ResolvedContextualOption[] = [];
149
+
150
+ for (const option of options) {
151
+ if (typeof option !== 'string') {
152
+ const custom = option as ContextualOptionObject;
153
+ const href = custom.href.replaceAll('$path', page.url).replaceAll('$page', mdUrl || mdHref);
154
+
155
+ resolved.push({
156
+ key: custom.title,
157
+ title: custom.title,
158
+ description: custom.description,
159
+ icon: custom.icon,
160
+ action: 'link',
161
+ href,
162
+ target: resolveLinkTarget(href, custom.target),
163
+ });
164
+ continue;
165
+ }
166
+
167
+ if (option === 'copy') {
168
+ resolved.push({
169
+ key: option,
170
+ title: labels.copyPage,
171
+ description: labels.copyPageDescription,
172
+ icon: 'copy',
173
+ action: 'copy',
174
+ href: mdHref,
175
+ target: '_self',
176
+ });
177
+ } else if (option === 'view') {
178
+ resolved.push({
179
+ key: option,
180
+ title: labels.viewMarkdown,
181
+ description: labels.viewMarkdownDescription,
182
+ icon: 'external-link',
183
+ action: 'link',
184
+ href: mdHref,
185
+ target: '_blank',
186
+ });
187
+ } else if (mdUrl && option === 'chatgpt') {
188
+ resolved.push({
189
+ key: option,
190
+ title: labels.openInChatGPT,
191
+ description: labels.askQuestionsAboutPage,
192
+ icon: 'external-link',
193
+ action: 'link',
194
+ href: `https://chatgpt.com/?q=${encodeURIComponent(aiPrompt(mdUrl))}`,
195
+ target: '_blank',
196
+ });
197
+ } else if (mdUrl && option === 'claude') {
198
+ resolved.push({
199
+ key: option,
200
+ title: labels.openInClaude,
201
+ description: labels.askQuestionsAboutPage,
202
+ icon: 'external-link',
203
+ action: 'link',
204
+ href: `https://claude.ai/new?q=${encodeURIComponent(aiPrompt(mdUrl))}`,
205
+ target: '_blank',
206
+ });
207
+ } else if (mdUrl && option === 'perplexity') {
208
+ resolved.push({
209
+ key: option,
210
+ title: labels.openInPerplexity,
211
+ description: labels.askQuestionsAboutPage,
212
+ icon: 'external-link',
213
+ action: 'link',
214
+ href: `https://www.perplexity.ai/search?q=${encodeURIComponent(aiPrompt(mdUrl))}`,
215
+ target: '_blank',
216
+ });
217
+ }
218
+ }
219
+
220
+ return resolved;
221
+ }
@@ -0,0 +1,38 @@
1
+ import GithubSlugger, { slug as slugifyOnce } from 'github-slugger';
2
+
3
+ /**
4
+ * The single slug algorithm in the repo.
5
+ *
6
+ * Heading anchors are produced in two independent places — `remarkToc` (which
7
+ * builds the on-page table of contents) and `rehype-slug` (which sets the `id`
8
+ * on the rendered heading). Both must agree or every TOC link breaks, so both
9
+ * go through github-slugger here rather than reimplementing it.
10
+ */
11
+
12
+ /**
13
+ * Creates a stateful slugger. Call `.slug(text)` once per heading in document
14
+ * order; repeated headings get `-1`, `-2`, ... suffixes exactly as rehype-slug
15
+ * does, because it is the same implementation.
16
+ */
17
+ export function createSlugger(): GithubSlugger {
18
+ return new GithubSlugger();
19
+ }
20
+
21
+ /**
22
+ * Stateless slugify for one-off ids that do not need de-duplication.
23
+ *
24
+ * github-slugger does not trim, so padded input would otherwise produce leading
25
+ * and trailing dashes. Heading text is trimmed upstream by `headingText`; this
26
+ * makes the guarantee hold for every other caller too.
27
+ */
28
+ export function slugify(value: string): string {
29
+ return slugifyOnce(value.trim());
30
+ }
31
+
32
+ /**
33
+ * Slugifies a label into a config-level identifier (tab ids, section ids).
34
+ * Falls back when the value contains no slug-able characters.
35
+ */
36
+ export function slugifyId(value: string, fallback: string): string {
37
+ return slugify(value) || fallback;
38
+ }