@terra-symposium/docs 0.1.77 → 0.1.78
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/bin/generate-skills.mjs +75 -22
- package/bin/terra-docs.mjs +145 -77
- package/lib-dist/components/StaticWorkspace.d.ts.map +1 -1
- package/lib-dist/components/StaticWorkspace.js +6 -4
- package/lib-dist/components/StaticWorkspace.js.map +1 -1
- package/lib-dist/lib/astro.d.ts +1 -1
- package/lib-dist/lib/astro.d.ts.map +1 -1
- package/lib-dist/lib/astro.js +10 -5
- package/lib-dist/lib/astro.js.map +1 -1
- package/lib-dist/style.css +1 -1
- package/package.json +6 -1
- package/site/astro.config.mjs +10 -16
- package/site/src/components/DocsSiteIsland.tsx +76 -0
- package/site/src/components/DocsSiteSearch.tsx +63 -15
- package/site/src/components/SkillsExplorer.tsx +122 -50
- package/site/src/lib/catalog.ts +143 -68
- package/src/components/SiteLayout.astro +23 -21
- package/site/src/styles/skills-explorer.css +0 -268
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terra-symposium/docs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.78",
|
|
4
4
|
"description": "Terra Docs workspace and Markdown site generator.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"docs:build": "pnpm build && node bin/terra-docs.mjs build --title 'Terra Docs'",
|
|
51
51
|
"docs:preview": "pnpm docs:build && node bin/terra-docs.mjs preview --title 'Terra Docs'",
|
|
52
52
|
"dev:library": "tsc --watch",
|
|
53
|
+
"prebuild": "pnpm exec terra-quality error-boundaries --all",
|
|
53
54
|
"build": "rm -rf lib-dist && tsc && tailwindcss --postcss tailwind.postcss.config.cjs -c tailwind.docs.config.mjs -i src/styles/style.css -o lib-dist/style.css --minify && tailwindcss --postcss tailwind.postcss.config.cjs -c tailwind.docs.config.mjs -i src/styles/workspace.css -o lib-dist/workspace.css --minify && node scripts/copy-runtime.mjs",
|
|
54
55
|
"build:demo": "vite build",
|
|
55
56
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.demo.json",
|
|
@@ -104,7 +105,10 @@
|
|
|
104
105
|
"@portabletext/editor": "7.9.0",
|
|
105
106
|
"@radix-ui/react-popover": "^1.1.15",
|
|
106
107
|
"@radix-ui/react-select": "2.4.1",
|
|
108
|
+
"@terra-symposium/standards-error-boundaries": "0.1.2",
|
|
107
109
|
"@terra-symposium/styling": "0.1.34",
|
|
110
|
+
"bezier-easing": "2.1.0",
|
|
111
|
+
"@xstate/react": "6.1.0",
|
|
108
112
|
"@uiw/codemirror-theme-github": "^4.25.9",
|
|
109
113
|
"@xyflow/react": "^12.3.6",
|
|
110
114
|
"astro": "^7.4.2",
|
|
@@ -118,6 +122,7 @@
|
|
|
118
122
|
"react-dom": "^19.3.1",
|
|
119
123
|
"sanitize-html": "^2.18.1",
|
|
120
124
|
"sonner": "^1.4.41",
|
|
125
|
+
"use-sync-external-store": "1.7.0",
|
|
121
126
|
"yaml": "^2.7.0"
|
|
122
127
|
},
|
|
123
128
|
"license": "Apache-2.0"
|
package/site/astro.config.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { terraErrorBoundaryIntegration as __terraErrorBoundaryIntegration } from '@terra-symposium/standards-error-boundaries/astro-integration';
|
|
1
2
|
import { fileURLToPath } from 'node:url';
|
|
2
3
|
import { resolve } from 'node:path';
|
|
3
4
|
import { readFileSync } from 'node:fs';
|
|
@@ -12,6 +13,9 @@ import { docsContentPlugin } from './docs-content-plugin.mjs';
|
|
|
12
13
|
|
|
13
14
|
const packageRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
14
15
|
const requireFromPackage = createRequire(import.meta.url);
|
|
16
|
+
const stylingEntry = requireFromPackage.resolve('@terra-symposium/styling');
|
|
17
|
+
const requireFromStyling = createRequire(stylingEntry);
|
|
18
|
+
const docsManifest = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8'));
|
|
15
19
|
const projectRoot = process.env.TERRA_DOCS_ROOT ?? process.cwd();
|
|
16
20
|
const publicDir = resolve(projectRoot, '.terra-docs/public');
|
|
17
21
|
const outDir = resolve(projectRoot, process.env.TERRA_DOCS_OUT ?? 'dist/docs');
|
|
@@ -42,19 +46,6 @@ function packageExportAliases() {
|
|
|
42
46
|
});
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
const packageDependencyResolver = {
|
|
46
|
-
name: 'terra-docs-package-dependencies',
|
|
47
|
-
enforce: 'pre',
|
|
48
|
-
resolveId(source) {
|
|
49
|
-
if (!/^(?:astro|@astrojs\/(?:markdown-remark|mdx|react))(?:\/|$)/.test(source)) return null;
|
|
50
|
-
try {
|
|
51
|
-
return requireFromPackage.resolve(source);
|
|
52
|
-
} catch {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
|
|
58
49
|
export default defineConfig({
|
|
59
50
|
root: projectRoot,
|
|
60
51
|
srcDir: resolve(packageRoot, 'site/src'),
|
|
@@ -64,12 +55,15 @@ export default defineConfig({
|
|
|
64
55
|
base,
|
|
65
56
|
devToolbar: { enabled: false },
|
|
66
57
|
build: { format: 'directory' },
|
|
67
|
-
integrations: [react(
|
|
58
|
+
integrations: [__terraErrorBoundaryIntegration(), react(), mdx()],
|
|
68
59
|
markdown: { processor: unified({ remarkPlugins: [[basePathRemarkPlugin, { base }]] }) },
|
|
69
60
|
vite: {
|
|
70
|
-
plugins: [
|
|
61
|
+
plugins: [docsContentPlugin({ root: projectRoot, base })],
|
|
71
62
|
optimizeDeps: { include: [...DOCS_VITE_OPTIMIZE_DEPS] },
|
|
72
|
-
resolve: {
|
|
63
|
+
resolve: {
|
|
64
|
+
alias: packageExportAliases(),
|
|
65
|
+
dedupe: ['react', 'react-dom'],
|
|
66
|
+
},
|
|
73
67
|
server: { fs: { allow: [projectRoot, packageRoot] } },
|
|
74
68
|
},
|
|
75
69
|
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* site/src/components/DocsSiteIsland.tsx
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/*//////////////////////////////////////////////////
|
|
6
|
+
IMPORTS
|
|
7
|
+
//////////////////////////////////////////////////*/
|
|
8
|
+
|
|
9
|
+
// COMPONENTS
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { ErrorBoundary } from '@terra-symposium/standards-error-boundaries/react';
|
|
12
|
+
|
|
13
|
+
// TYPES
|
|
14
|
+
import type { ComponentProps } from 'react';
|
|
15
|
+
|
|
16
|
+
/*//////////////////////////////////////////////////
|
|
17
|
+
CONFIG
|
|
18
|
+
//////////////////////////////////////////////////*/
|
|
19
|
+
|
|
20
|
+
const { lazy, Suspense } = React;
|
|
21
|
+
const DocsStaticWorkspace = lazy(() =>
|
|
22
|
+
import('../../../lib-dist/components/StaticWorkspace.js').then(({ DocsStaticWorkspace }) => ({
|
|
23
|
+
default: DocsStaticWorkspace,
|
|
24
|
+
})),
|
|
25
|
+
);
|
|
26
|
+
const DocsSiteSearch = lazy(() =>
|
|
27
|
+
import('./DocsSiteSearch.js').then(({ DocsSiteSearch }) => ({ default: DocsSiteSearch })),
|
|
28
|
+
);
|
|
29
|
+
type Props = ComponentProps<typeof DocsStaticWorkspace> & { searchBase?: string };
|
|
30
|
+
|
|
31
|
+
/*//////////////////////////////////////////////////
|
|
32
|
+
COMPONENTS
|
|
33
|
+
//////////////////////////////////////////////////*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Keeps Docs workspace and search rendering failures inside a visible, recoverable page boundary.
|
|
37
|
+
*
|
|
38
|
+
* @param props Workspace data and the base path used for search-index requests.
|
|
39
|
+
*
|
|
40
|
+
* @return The lazy Docs workspace and search dialog inside a recoverable error boundary.
|
|
41
|
+
*/
|
|
42
|
+
export function DocsSiteIsland({ searchBase = '/', ...workspaceProps }: Props) {
|
|
43
|
+
return (
|
|
44
|
+
<ErrorBoundary
|
|
45
|
+
name="Docs site"
|
|
46
|
+
onError={(error, report) => console.error(`[${report.name}]`, error, report.componentStack)}
|
|
47
|
+
fallback={() => (
|
|
48
|
+
<main
|
|
49
|
+
aria-labelledby="docs-site-error-title"
|
|
50
|
+
className="mx-auto my-12 max-w-2xl p-6"
|
|
51
|
+
role="alert"
|
|
52
|
+
>
|
|
53
|
+
<h1 id="docs-site-error-title">Documentation could not load</h1>
|
|
54
|
+
<p>
|
|
55
|
+
The Docs interface encountered a rendering error. The rest of this page remains
|
|
56
|
+
available.
|
|
57
|
+
</p>
|
|
58
|
+
<button onClick={() => window.location.reload()} type="button">
|
|
59
|
+
Reload Docs
|
|
60
|
+
</button>
|
|
61
|
+
</main>
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
<Suspense
|
|
65
|
+
fallback={
|
|
66
|
+
<p aria-live="polite" role="status">
|
|
67
|
+
Loading documentation…
|
|
68
|
+
</p>
|
|
69
|
+
}
|
|
70
|
+
>
|
|
71
|
+
<DocsStaticWorkspace {...workspaceProps} />
|
|
72
|
+
<DocsSiteSearch base={searchBase} />
|
|
73
|
+
</Suspense>
|
|
74
|
+
</ErrorBoundary>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -1,22 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* site/src/components/DocsSiteSearch.tsx
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/*//////////////////////////////////////////////////
|
|
6
|
+
IMPORTS
|
|
7
|
+
//////////////////////////////////////////////////*/
|
|
8
|
+
|
|
9
|
+
// API
|
|
1
10
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
11
|
|
|
12
|
+
/*//////////////////////////////////////////////////
|
|
13
|
+
CONFIG
|
|
14
|
+
//////////////////////////////////////////////////*/
|
|
15
|
+
|
|
16
|
+
const S_REGEX = /\s+/;
|
|
3
17
|
type SearchDocument = {
|
|
4
18
|
title: string;
|
|
5
19
|
description: string;
|
|
6
20
|
href: string;
|
|
7
21
|
text: string;
|
|
8
22
|
};
|
|
9
|
-
|
|
10
23
|
type Props = { base?: string };
|
|
11
24
|
|
|
25
|
+
/*//////////////////////////////////////////////////
|
|
26
|
+
HELPERS
|
|
27
|
+
//////////////////////////////////////////////////*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Builds a short search snippet around the first matching query term.
|
|
31
|
+
*
|
|
32
|
+
* @param text Document text to show in the result.
|
|
33
|
+
* @param query Term used to locate the most relevant excerpt position.
|
|
34
|
+
*
|
|
35
|
+
* @return A clipped excerpt with ellipses where surrounding text was omitted.
|
|
36
|
+
*/
|
|
12
37
|
function excerpt(text: string, query: string): string {
|
|
13
|
-
const
|
|
14
|
-
const offset =
|
|
38
|
+
const norm = text.replace(/\s+/g, ' ').trim();
|
|
39
|
+
const offset = norm.toLowerCase().indexOf(query.toLowerCase());
|
|
15
40
|
const start = Math.max(0, offset - 72);
|
|
16
|
-
const end = Math.min(
|
|
17
|
-
return `${start > 0 ? '…' : ''}${
|
|
41
|
+
const end = Math.min(norm.length, start + 190);
|
|
42
|
+
return `${start > 0 ? '…' : ''}${norm.slice(start, end)}${end < norm.length ? '…' : ''}`;
|
|
18
43
|
}
|
|
19
44
|
|
|
45
|
+
/*//////////////////////////////////////////////////
|
|
46
|
+
COMPONENTS
|
|
47
|
+
//////////////////////////////////////////////////*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Provides a keyboard-accessible dialog for searching the generated Docs index.
|
|
51
|
+
*
|
|
52
|
+
* @param props Optional base path used to fetch the site's search index.
|
|
53
|
+
*
|
|
54
|
+
* @return Search controls, loading and error states, and links to matching pages.
|
|
55
|
+
*/
|
|
20
56
|
export function DocsSiteSearch({ base = '/' }: Props) {
|
|
21
57
|
const dialog = useRef<HTMLDialogElement>(null);
|
|
22
58
|
const input = useRef<HTMLInputElement>(null);
|
|
@@ -25,16 +61,31 @@ export function DocsSiteSearch({ base = '/' }: Props) {
|
|
|
25
61
|
const [loaded, setLoaded] = useState(false);
|
|
26
62
|
const [failed, setFailed] = useState(false);
|
|
27
63
|
const [opened, setOpened] = useState(false);
|
|
28
|
-
const
|
|
29
|
-
|
|
64
|
+
const normBase = base.endsWith('/') ? base : `${base}/`;
|
|
65
|
+
/**
|
|
66
|
+
* Opens the dialog and moves keyboard focus to the search field.
|
|
67
|
+
*
|
|
68
|
+
* @return Nothing; the callback updates dialog state and browser focus.
|
|
69
|
+
*/
|
|
30
70
|
const open = () => {
|
|
31
71
|
if (!dialog.current?.open) dialog.current?.showModal();
|
|
32
72
|
setOpened(true);
|
|
33
73
|
window.setTimeout(() => input.current?.focus(), 0);
|
|
34
74
|
};
|
|
35
|
-
|
|
36
75
|
useEffect(() => {
|
|
76
|
+
/**
|
|
77
|
+
* Opens search in response to the Docs site's custom search event.
|
|
78
|
+
*
|
|
79
|
+
* @return Nothing; the callback delegates to the dialog opener.
|
|
80
|
+
*/
|
|
37
81
|
const onOpenSearch = () => open();
|
|
82
|
+
/**
|
|
83
|
+
* Opens search for the supported keyboard shortcuts outside form controls.
|
|
84
|
+
*
|
|
85
|
+
* @param event Keyboard event dispatched by the document.
|
|
86
|
+
*
|
|
87
|
+
* @return Nothing; a matching shortcut is prevented and opens the dialog.
|
|
88
|
+
*/
|
|
38
89
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
39
90
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
|
|
40
91
|
event.preventDefault();
|
|
@@ -54,10 +105,9 @@ export function DocsSiteSearch({ base = '/' }: Props) {
|
|
|
54
105
|
document.removeEventListener('keydown', onKeyDown);
|
|
55
106
|
};
|
|
56
107
|
}, []);
|
|
57
|
-
|
|
58
108
|
useEffect(() => {
|
|
59
109
|
if (!opened || loaded) return;
|
|
60
|
-
fetch(`${
|
|
110
|
+
fetch(`${normBase}_terra-docs/search-index.json`)
|
|
61
111
|
.then((response) => {
|
|
62
112
|
if (!response.ok) throw new Error(`Search index returned ${response.status}`);
|
|
63
113
|
return response.json() as Promise<SearchDocument[]>;
|
|
@@ -67,10 +117,9 @@ export function DocsSiteSearch({ base = '/' }: Props) {
|
|
|
67
117
|
setLoaded(true);
|
|
68
118
|
})
|
|
69
119
|
.catch(() => setFailed(true));
|
|
70
|
-
}, [loaded,
|
|
71
|
-
|
|
120
|
+
}, [loaded, normBase, opened]);
|
|
72
121
|
const results = useMemo(() => {
|
|
73
|
-
const terms = query.toLowerCase().trim().split(
|
|
122
|
+
const terms = query.toLowerCase().trim().split(S_REGEX).filter(Boolean);
|
|
74
123
|
if (!terms.length) return [];
|
|
75
124
|
return documents
|
|
76
125
|
.map((document) => {
|
|
@@ -87,7 +136,6 @@ export function DocsSiteSearch({ base = '/' }: Props) {
|
|
|
87
136
|
)
|
|
88
137
|
.slice(0, 10);
|
|
89
138
|
}, [documents, query]);
|
|
90
|
-
|
|
91
139
|
return (
|
|
92
140
|
<div className="terra-docs-search">
|
|
93
141
|
<dialog className="terra-docs-search-dialog" ref={dialog} onClose={() => setQuery('')}>
|
|
@@ -112,7 +160,7 @@ export function DocsSiteSearch({ base = '/' }: Props) {
|
|
|
112
160
|
<a key={document.href} href={document.href} onClick={() => dialog.current?.close()}>
|
|
113
161
|
<strong>{document.title}</strong>
|
|
114
162
|
{document.description ? <span>{document.description}</span> : null}
|
|
115
|
-
<small>{excerpt(document.text, query.trim().split(
|
|
163
|
+
<small>{excerpt(document.text, query.trim().split(S_REGEX)[0] ?? query)}</small>
|
|
116
164
|
</a>
|
|
117
165
|
))}
|
|
118
166
|
</div>
|
|
@@ -1,6 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* site/src/components/SkillsExplorer.tsx
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/*//////////////////////////////////////////////////
|
|
6
|
+
IMPORTS
|
|
7
|
+
//////////////////////////////////////////////////*/
|
|
8
|
+
|
|
9
|
+
// API
|
|
1
10
|
import { useMemo, useState } from 'react';
|
|
2
|
-
import '../styles/skills-explorer.css';
|
|
3
11
|
|
|
12
|
+
// COMPONENTS
|
|
13
|
+
import { Button, Input, Label } from '@terra-symposium/styling/ui';
|
|
14
|
+
|
|
15
|
+
/*//////////////////////////////////////////////////
|
|
16
|
+
CONFIG
|
|
17
|
+
//////////////////////////////////////////////////*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Skill content and metadata prepared for display in the repository skills explorer.
|
|
21
|
+
*/
|
|
4
22
|
export type SkillRecord = {
|
|
5
23
|
body: string;
|
|
6
24
|
description: string;
|
|
@@ -13,22 +31,52 @@ export type SkillRecord = {
|
|
|
13
31
|
title: string;
|
|
14
32
|
};
|
|
15
33
|
|
|
34
|
+
/*//////////////////////////////////////////////////
|
|
35
|
+
HELPERS
|
|
36
|
+
//////////////////////////////////////////////////*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Converts scalar metadata to readable text and structured metadata to JSON.
|
|
40
|
+
*
|
|
41
|
+
* @param value Metadata value parsed from a skill's YAML frontmatter.
|
|
42
|
+
*
|
|
43
|
+
* @return Text suitable for searching and displaying in the explorer.
|
|
44
|
+
*/
|
|
16
45
|
function metadataValue(value: unknown) {
|
|
17
46
|
if (typeof value === 'string') return value;
|
|
18
47
|
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
19
48
|
return JSON.stringify(value);
|
|
20
49
|
}
|
|
21
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Formats a count with a simple English plural suffix.
|
|
53
|
+
*
|
|
54
|
+
* @param count Number of matching items.
|
|
55
|
+
* @param singular Singular label for one item.
|
|
56
|
+
*
|
|
57
|
+
* @return The count followed by the singular or plural label.
|
|
58
|
+
*/
|
|
22
59
|
function plural(count: number, singular: string) {
|
|
23
60
|
return `${count} ${singular}${count === 1 ? '' : 's'}`;
|
|
24
61
|
}
|
|
25
62
|
|
|
63
|
+
/*//////////////////////////////////////////////////
|
|
64
|
+
COMPONENTS
|
|
65
|
+
//////////////////////////////////////////////////*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Lets readers filter, select, and inspect the repository's indexed skill files.
|
|
69
|
+
*
|
|
70
|
+
* @param props Skill records built from the repository's `SKILL.md` files.
|
|
71
|
+
*
|
|
72
|
+
* @return A filterable list and the selected skill's description and content.
|
|
73
|
+
*/
|
|
26
74
|
export function SkillsExplorer({ skills }: { skills: SkillRecord[] }) {
|
|
27
75
|
const [query, setQuery] = useState('');
|
|
28
|
-
const [
|
|
76
|
+
const [aPath, setActivePath] = useState(skills[0]?.sourcePath ?? '');
|
|
29
77
|
const filteredSkills = useMemo(() => {
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
78
|
+
const normQuery = query.trim().toLocaleLowerCase();
|
|
79
|
+
if (!normQuery) return skills;
|
|
32
80
|
return skills.filter((skill) =>
|
|
33
81
|
[
|
|
34
82
|
skill.name,
|
|
@@ -42,11 +90,11 @@ export function SkillsExplorer({ skills }: { skills: SkillRecord[] }) {
|
|
|
42
90
|
]
|
|
43
91
|
.join('\n')
|
|
44
92
|
.toLocaleLowerCase()
|
|
45
|
-
.includes(
|
|
93
|
+
.includes(normQuery),
|
|
46
94
|
);
|
|
47
95
|
}, [query, skills]);
|
|
48
96
|
const selectedSkill =
|
|
49
|
-
filteredSkills.find((skill) => skill.sourcePath ===
|
|
97
|
+
filteredSkills.find((skill) => skill.sourcePath === aPath) ?? filteredSkills[0] ?? null;
|
|
50
98
|
const groups = useMemo(() => {
|
|
51
99
|
const byScope = new Map<string, SkillRecord[]>();
|
|
52
100
|
for (const skill of filteredSkills) {
|
|
@@ -61,93 +109,118 @@ export function SkillsExplorer({ skills }: { skills: SkillRecord[] }) {
|
|
|
61
109
|
([key]) => !['name', 'description'].includes(key),
|
|
62
110
|
)
|
|
63
111
|
: [];
|
|
64
|
-
|
|
65
112
|
return (
|
|
66
|
-
<section aria-label="Repository skills" className="
|
|
67
|
-
<header className="
|
|
113
|
+
<section aria-label="Repository skills" className="my-8">
|
|
114
|
+
<header className="mb-4 flex flex-wrap items-end justify-between gap-6">
|
|
68
115
|
<div>
|
|
69
|
-
<p className="
|
|
70
|
-
|
|
71
|
-
|
|
116
|
+
<p className="mb-1 text-xs font-bold uppercase tracking-wider text-gray-10">
|
|
117
|
+
Repository inventory
|
|
118
|
+
</p>
|
|
119
|
+
<h2 className="m-0">Skills</h2>
|
|
120
|
+
<p className="mb-0 mt-1 text-gray-10">
|
|
121
|
+
Browse the workflows and guidance files included in this repository.
|
|
122
|
+
</p>
|
|
72
123
|
</div>
|
|
73
|
-
<div aria-live="polite" className="
|
|
74
|
-
<strong>{skills.length}</strong>
|
|
124
|
+
<div aria-live="polite" className="grid justify-items-end text-sm text-gray-10">
|
|
125
|
+
<strong className="text-2xl leading-none text-primary-11">{skills.length}</strong>
|
|
75
126
|
<span>{plural(skills.length, 'skill')} indexed</span>
|
|
76
127
|
</div>
|
|
77
128
|
</header>
|
|
78
|
-
|
|
79
129
|
{skills.length ? (
|
|
80
|
-
<div className="
|
|
81
|
-
<nav
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
130
|
+
<div className="grid min-w-0 items-start gap-4 md:grid-cols-[minmax(16rem,0.8fr)_minmax(0,1.6fr)]">
|
|
131
|
+
<nav
|
|
132
|
+
aria-label="Available skills"
|
|
133
|
+
className="max-h-[78vh] min-w-0 overflow-auto rounded-xl border border-gray-6 bg-gray-1 p-3"
|
|
134
|
+
>
|
|
135
|
+
<div>
|
|
136
|
+
<Label htmlFor="skills-explorer-filter">Filter skills</Label>
|
|
137
|
+
<Input
|
|
138
|
+
id="skills-explorer-filter"
|
|
85
139
|
onChange={(event) => setQuery(event.currentTarget.value)}
|
|
86
140
|
placeholder="Name, topic, or text"
|
|
87
141
|
type="search"
|
|
88
142
|
value={query}
|
|
89
143
|
/>
|
|
90
|
-
</
|
|
91
|
-
<p aria-live="polite" className="
|
|
144
|
+
</div>
|
|
145
|
+
<p aria-live="polite" className="mb-4 mt-2 text-xs text-gray-10">
|
|
92
146
|
Showing {filteredSkills.length} of {skills.length}
|
|
93
147
|
</p>
|
|
94
148
|
{groups.length ? (
|
|
95
149
|
groups.map(([scope, entries]) => (
|
|
96
|
-
<section aria-label={scope} className="
|
|
97
|
-
<h3>
|
|
98
|
-
|
|
150
|
+
<section aria-label={scope} className="mt-4 first:mt-0" key={scope}>
|
|
151
|
+
<h3 className="mb-2 break-words text-xs font-bold uppercase text-gray-10">
|
|
152
|
+
{scope}
|
|
153
|
+
</h3>
|
|
154
|
+
<ul className="m-0 grid list-none gap-1 p-0">
|
|
99
155
|
{entries.map((skill) => (
|
|
100
156
|
<li key={skill.sourcePath}>
|
|
101
|
-
<
|
|
157
|
+
<Button
|
|
102
158
|
aria-current={
|
|
103
159
|
selectedSkill?.sourcePath === skill.sourcePath ? 'true' : undefined
|
|
104
160
|
}
|
|
105
|
-
className="
|
|
161
|
+
className="h-auto w-full justify-start px-3 py-2 text-left"
|
|
106
162
|
onClick={() => setActivePath(skill.sourcePath)}
|
|
163
|
+
variant={
|
|
164
|
+
selectedSkill?.sourcePath === skill.sourcePath
|
|
165
|
+
? 'primary-tonal'
|
|
166
|
+
: 'gray-ghost'
|
|
167
|
+
}
|
|
107
168
|
type="button"
|
|
108
169
|
>
|
|
109
|
-
<span className="
|
|
110
|
-
|
|
111
|
-
|
|
170
|
+
<span className="grid min-w-0 gap-1">
|
|
171
|
+
<span className="font-semibold leading-snug">{skill.title}</span>
|
|
172
|
+
<span className="line-clamp-2 text-xs font-normal text-gray-10">
|
|
173
|
+
{skill.description || skill.name}
|
|
174
|
+
</span>
|
|
175
|
+
<span className="truncate text-[0.7rem] font-normal text-gray-9">
|
|
176
|
+
{skill.directory}
|
|
177
|
+
</span>
|
|
112
178
|
</span>
|
|
113
|
-
|
|
114
|
-
</button>
|
|
179
|
+
</Button>
|
|
115
180
|
</li>
|
|
116
181
|
))}
|
|
117
182
|
</ul>
|
|
118
183
|
</section>
|
|
119
184
|
))
|
|
120
185
|
) : (
|
|
121
|
-
<p className="
|
|
186
|
+
<p className="text-sm text-gray-10">No skills match “{query}”.</p>
|
|
122
187
|
)}
|
|
123
188
|
</nav>
|
|
124
|
-
|
|
125
189
|
{selectedSkill ? (
|
|
126
|
-
<article
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
<p className="
|
|
190
|
+
<article
|
|
191
|
+
aria-live="polite"
|
|
192
|
+
className="min-w-0 overflow-hidden rounded-xl border border-gray-6 bg-gray-1"
|
|
193
|
+
>
|
|
194
|
+
<header className="border-b border-gray-6 p-4">
|
|
195
|
+
<p className="mb-1 text-xs font-bold uppercase tracking-wider text-primary-11">
|
|
196
|
+
{selectedSkill.scope}
|
|
197
|
+
</p>
|
|
198
|
+
<h3 className="mb-2 mt-0">{selectedSkill.title}</h3>
|
|
199
|
+
<p className="mb-2 text-gray-10">{selectedSkill.description}</p>
|
|
200
|
+
<p className="mb-0 break-all font-mono text-xs text-gray-9">
|
|
201
|
+
{selectedSkill.sourcePath}
|
|
202
|
+
</p>
|
|
132
203
|
</header>
|
|
133
204
|
{metadataEntries.length ? (
|
|
134
|
-
<dl className="
|
|
205
|
+
<dl className="m-0 grid gap-3 border-b border-gray-6 p-4 sm:grid-cols-2">
|
|
135
206
|
{metadataEntries.map(([key, value]) => (
|
|
136
|
-
<div key={key}>
|
|
137
|
-
<dt
|
|
138
|
-
|
|
207
|
+
<div className="min-w-0" key={key}>
|
|
208
|
+
<dt className="text-xs font-semibold uppercase text-gray-10">
|
|
209
|
+
{key.replace(/[-_]+/g, ' ')}
|
|
210
|
+
</dt>
|
|
211
|
+
<dd className="mb-0 mt-1 break-words text-sm">{metadataValue(value)}</dd>
|
|
139
212
|
</div>
|
|
140
213
|
))}
|
|
141
214
|
</dl>
|
|
142
215
|
) : null}
|
|
143
216
|
{selectedSkill.metadataIssue ? (
|
|
144
|
-
<p className="
|
|
217
|
+
<p className="m-4 rounded-md border border-gray-6 bg-gray-2 p-3 text-sm">
|
|
145
218
|
Frontmatter could not be parsed: {selectedSkill.metadataIssue}
|
|
146
219
|
</p>
|
|
147
220
|
) : null}
|
|
148
|
-
<details className="
|
|
149
|
-
<summary>Full skill instructions</summary>
|
|
150
|
-
<pre>
|
|
221
|
+
<details className="p-4" open>
|
|
222
|
+
<summary className="cursor-pointer font-semibold">Full skill instructions</summary>
|
|
223
|
+
<pre className="mt-3 max-h-[60vh] overflow-auto whitespace-pre-wrap break-words rounded-lg bg-gray-2 p-4 text-sm">
|
|
151
224
|
<code>{selectedSkill.body}</code>
|
|
152
225
|
</pre>
|
|
153
226
|
</details>
|
|
@@ -155,7 +228,7 @@ export function SkillsExplorer({ skills }: { skills: SkillRecord[] }) {
|
|
|
155
228
|
) : null}
|
|
156
229
|
</div>
|
|
157
230
|
) : (
|
|
158
|
-
<p className="
|
|
231
|
+
<p className="rounded-lg border border-gray-6 bg-gray-1 p-4 text-sm text-gray-10">
|
|
159
232
|
No SKILL.md files were found in this repository. Add skills under a .agents/skills or
|
|
160
233
|
.codex/skills folder, then rebuild the Docs site.
|
|
161
234
|
</p>
|
|
@@ -163,5 +236,4 @@ export function SkillsExplorer({ skills }: { skills: SkillRecord[] }) {
|
|
|
163
236
|
</section>
|
|
164
237
|
);
|
|
165
238
|
}
|
|
166
|
-
|
|
167
239
|
export default SkillsExplorer;
|