@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terra-symposium/docs",
3
- "version": "0.1.77",
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"
@@ -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({ experimentalDisableStreaming: true }), mdx()],
58
+ integrations: [__terraErrorBoundaryIntegration(), react(), mdx()],
68
59
  markdown: { processor: unified({ remarkPlugins: [[basePathRemarkPlugin, { base }]] }) },
69
60
  vite: {
70
- plugins: [packageDependencyResolver, docsContentPlugin({ root: projectRoot, base })],
61
+ plugins: [docsContentPlugin({ root: projectRoot, base })],
71
62
  optimizeDeps: { include: [...DOCS_VITE_OPTIMIZE_DEPS] },
72
- resolve: { alias: packageExportAliases() },
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 normalized = text.replace(/\s+/g, ' ').trim();
14
- const offset = normalized.toLowerCase().indexOf(query.toLowerCase());
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(normalized.length, start + 190);
17
- return `${start > 0 ? '…' : ''}${normalized.slice(start, end)}${end < normalized.length ? '…' : ''}`;
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 normalizedBase = base.endsWith('/') ? base : `${base}/`;
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(`${normalizedBase}_terra-docs/search-index.json`)
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, normalizedBase, opened]);
71
-
120
+ }, [loaded, normBase, opened]);
72
121
  const results = useMemo(() => {
73
- const terms = query.toLowerCase().trim().split(/\s+/).filter(Boolean);
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(/\s+/)[0] ?? query)}</small>
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 [activePath, setActivePath] = useState(skills[0]?.sourcePath ?? '');
76
+ const [aPath, setActivePath] = useState(skills[0]?.sourcePath ?? '');
29
77
  const filteredSkills = useMemo(() => {
30
- const normalizedQuery = query.trim().toLocaleLowerCase();
31
- if (!normalizedQuery) return skills;
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(normalizedQuery),
93
+ .includes(normQuery),
46
94
  );
47
95
  }, [query, skills]);
48
96
  const selectedSkill =
49
- filteredSkills.find((skill) => skill.sourcePath === activePath) ?? filteredSkills[0] ?? null;
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="docs-skills">
67
- <header className="docs-skills__header">
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="docs-skills__eyebrow">Repository inventory</p>
70
- <h2>Skills</h2>
71
- <p>Browse the workflows and guidance files included in this repository.</p>
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="docs-skills__summary">
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="docs-skills__layout">
81
- <nav aria-label="Available skills" className="docs-skills__index">
82
- <label className="docs-skills__filter">
83
- <span>Filter skills</span>
84
- <input
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
- </label>
91
- <p aria-live="polite" className="docs-skills__count">
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="docs-skills__group" key={scope}>
97
- <h3>{scope}</h3>
98
- <ul>
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
- <button
157
+ <Button
102
158
  aria-current={
103
159
  selectedSkill?.sourcePath === skill.sourcePath ? 'true' : undefined
104
160
  }
105
- className="docs-skills__item"
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="docs-skills__item-title">{skill.title}</span>
110
- <span className="docs-skills__item-description">
111
- {skill.description || skill.name}
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
- <span className="docs-skills__item-path">{skill.directory}</span>
114
- </button>
179
+ </Button>
115
180
  </li>
116
181
  ))}
117
182
  </ul>
118
183
  </section>
119
184
  ))
120
185
  ) : (
121
- <p className="docs-skills__empty">No skills match “{query}”.</p>
186
+ <p className="text-sm text-gray-10">No skills match “{query}”.</p>
122
187
  )}
123
188
  </nav>
124
-
125
189
  {selectedSkill ? (
126
- <article aria-live="polite" className="docs-skills__detail">
127
- <header>
128
- <p className="docs-skills__eyebrow">{selectedSkill.scope}</p>
129
- <h3>{selectedSkill.title}</h3>
130
- <p className="docs-skills__description">{selectedSkill.description}</p>
131
- <p className="docs-skills__source-path">{selectedSkill.sourcePath}</p>
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="docs-skills__metadata">
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>{key.replace(/[-_]+/g, ' ')}</dt>
138
- <dd>{metadataValue(value)}</dd>
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="docs-skills__metadata-issue">
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="docs-skills__source" open>
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="docs-skills__empty">
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;