@terra-symposium/docs 0.1.74 → 0.1.76

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.74",
3
+ "version": "0.1.76",
4
4
  "description": "Terra Docs workspace and Markdown site generator.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,7 +35,11 @@
35
35
  "import": "./lib-dist/components/LocalDocsWorkspace.js"
36
36
  },
37
37
  "./site-layout.astro": "./src/components/SiteLayout.astro",
38
- "./not-found.astro": "./src/components/NotFound.astro"
38
+ "./not-found.astro": "./src/components/NotFound.astro",
39
+ "./skills": {
40
+ "types": "./site/src/components/SkillsExplorer.tsx",
41
+ "import": "./site/src/components/SkillsExplorer.tsx"
42
+ }
39
43
  },
40
44
  "bin": {
41
45
  "terra-docs": "bin/terra-docs.mjs"
@@ -13,7 +13,7 @@ const packageRoot = fileURLToPath(new URL('../', import.meta.url));
13
13
  const requireFromPackage = createRequire(import.meta.url);
14
14
  const projectRoot = process.env.TERRA_DOCS_ROOT ?? process.cwd();
15
15
  const publicDir = resolve(projectRoot, '.terra-docs/public');
16
- const outDir = resolve(projectRoot, process.env.TERRA_DOCS_OUT ?? 'docs-dist');
16
+ const outDir = resolve(projectRoot, process.env.TERRA_DOCS_OUT ?? 'dist/docs');
17
17
  const site = process.env.TERRA_DOCS_SITE;
18
18
  const base = process.env.TERRA_DOCS_BASE ?? '/';
19
19
  const projectManifest = (() => {
@@ -34,6 +34,7 @@ export function DocsSiteSearch({ base = '/' }: Props) {
34
34
  };
35
35
 
36
36
  useEffect(() => {
37
+ const onOpenSearch = () => open();
37
38
  const onKeyDown = (event: KeyboardEvent) => {
38
39
  if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
39
40
  event.preventDefault();
@@ -46,8 +47,12 @@ export function DocsSiteSearch({ base = '/' }: Props) {
46
47
  open();
47
48
  }
48
49
  };
50
+ document.addEventListener('docs:open-search', onOpenSearch);
49
51
  document.addEventListener('keydown', onKeyDown);
50
- return () => document.removeEventListener('keydown', onKeyDown);
52
+ return () => {
53
+ document.removeEventListener('docs:open-search', onOpenSearch);
54
+ document.removeEventListener('keydown', onKeyDown);
55
+ };
51
56
  }, []);
52
57
 
53
58
  useEffect(() => {
@@ -85,16 +90,6 @@ export function DocsSiteSearch({ base = '/' }: Props) {
85
90
 
86
91
  return (
87
92
  <div className="terra-docs-search">
88
- <button
89
- className="terra-docs-search-trigger"
90
- type="button"
91
- onClick={open}
92
- aria-label="Search documentation"
93
- >
94
- <span aria-hidden="true">⌕</span>
95
- <span>Search docs</span>
96
- <kbd>⌘ K</kbd>
97
- </button>
98
93
  <dialog className="terra-docs-search-dialog" ref={dialog} onClose={() => setQuery('')}>
99
94
  <div className="terra-docs-search-panel">
100
95
  <label className="terra-docs-search-input-row">
@@ -0,0 +1,157 @@
1
+ import { useMemo, useState } from 'react';
2
+ import '../styles/skills-explorer.css';
3
+
4
+ export type SkillRecord = {
5
+ body: string;
6
+ description: string;
7
+ directory: string;
8
+ metadata: Record<string, unknown>;
9
+ metadataIssue?: string;
10
+ name: string;
11
+ scope: string;
12
+ sourcePath: string;
13
+ title: string;
14
+ };
15
+
16
+ function metadataValue(value: unknown) {
17
+ if (typeof value === 'string') return value;
18
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
19
+ return JSON.stringify(value);
20
+ }
21
+
22
+ function plural(count: number, singular: string) {
23
+ return `${count} ${singular}${count === 1 ? '' : 's'}`;
24
+ }
25
+
26
+ export function SkillsExplorer({ skills }: { skills: SkillRecord[] }) {
27
+ const [query, setQuery] = useState('');
28
+ const [activePath, setActivePath] = useState(skills[0]?.sourcePath ?? '');
29
+ const filteredSkills = useMemo(() => {
30
+ const normalizedQuery = query.trim().toLocaleLowerCase();
31
+ if (!normalizedQuery) return skills;
32
+ return skills.filter((skill) =>
33
+ [
34
+ skill.name,
35
+ skill.title,
36
+ skill.description,
37
+ skill.directory,
38
+ skill.scope,
39
+ skill.sourcePath,
40
+ JSON.stringify(skill.metadata),
41
+ skill.body,
42
+ ]
43
+ .join('\n')
44
+ .toLocaleLowerCase()
45
+ .includes(normalizedQuery),
46
+ );
47
+ }, [query, skills]);
48
+ const selectedSkill =
49
+ filteredSkills.find((skill) => skill.sourcePath === activePath) ?? filteredSkills[0] ?? null;
50
+ const groups = useMemo(() => {
51
+ const byScope = new Map<string, SkillRecord[]>();
52
+ for (const skill of filteredSkills) {
53
+ const entries = byScope.get(skill.scope) ?? [];
54
+ entries.push(skill);
55
+ byScope.set(skill.scope, entries);
56
+ }
57
+ return [...byScope.entries()];
58
+ }, [filteredSkills]);
59
+ const metadataEntries = selectedSkill
60
+ ? Object.entries(selectedSkill.metadata).filter(([key]) => !['name', 'description'].includes(key))
61
+ : [];
62
+
63
+ return (
64
+ <section aria-label="Repository skills" className="docs-skills">
65
+ <header className="docs-skills__header">
66
+ <div>
67
+ <p className="docs-skills__eyebrow">Repository inventory</p>
68
+ <h2>Skills</h2>
69
+ <p>Browse the workflows and guidance files included in this repository.</p>
70
+ </div>
71
+ <div aria-live="polite" className="docs-skills__summary">
72
+ <strong>{skills.length}</strong>
73
+ <span>{plural(skills.length, 'skill')} indexed</span>
74
+ </div>
75
+ </header>
76
+
77
+ {skills.length ? (
78
+ <div className="docs-skills__layout">
79
+ <nav aria-label="Available skills" className="docs-skills__index">
80
+ <label className="docs-skills__filter">
81
+ <span>Filter skills</span>
82
+ <input
83
+ onChange={(event) => setQuery(event.currentTarget.value)}
84
+ placeholder="Name, topic, or text"
85
+ type="search"
86
+ value={query}
87
+ />
88
+ </label>
89
+ <p aria-live="polite" className="docs-skills__count">
90
+ Showing {filteredSkills.length} of {skills.length}
91
+ </p>
92
+ {groups.length ? (
93
+ groups.map(([scope, entries]) => (
94
+ <section aria-label={scope} className="docs-skills__group" key={scope}>
95
+ <h3>{scope}</h3>
96
+ <ul>
97
+ {entries.map((skill) => (
98
+ <li key={skill.sourcePath}>
99
+ <button
100
+ aria-current={selectedSkill?.sourcePath === skill.sourcePath ? 'true' : undefined}
101
+ className="docs-skills__item"
102
+ onClick={() => setActivePath(skill.sourcePath)}
103
+ type="button"
104
+ >
105
+ <span className="docs-skills__item-title">{skill.title}</span>
106
+ <span className="docs-skills__item-description">{skill.description || skill.name}</span>
107
+ <span className="docs-skills__item-path">{skill.directory}</span>
108
+ </button>
109
+ </li>
110
+ ))}
111
+ </ul>
112
+ </section>
113
+ ))
114
+ ) : (
115
+ <p className="docs-skills__empty">No skills match “{query}”.</p>
116
+ )}
117
+ </nav>
118
+
119
+ {selectedSkill ? (
120
+ <article aria-live="polite" className="docs-skills__detail">
121
+ <header>
122
+ <p className="docs-skills__eyebrow">{selectedSkill.scope}</p>
123
+ <h3>{selectedSkill.title}</h3>
124
+ <p className="docs-skills__description">{selectedSkill.description}</p>
125
+ <p className="docs-skills__source-path">{selectedSkill.sourcePath}</p>
126
+ </header>
127
+ {metadataEntries.length ? (
128
+ <dl className="docs-skills__metadata">
129
+ {metadataEntries.map(([key, value]) => (
130
+ <div key={key}>
131
+ <dt>{key.replace(/[-_]+/g, ' ')}</dt>
132
+ <dd>{metadataValue(value)}</dd>
133
+ </div>
134
+ ))}
135
+ </dl>
136
+ ) : null}
137
+ {selectedSkill.metadataIssue ? (
138
+ <p className="docs-skills__metadata-issue">Frontmatter could not be parsed: {selectedSkill.metadataIssue}</p>
139
+ ) : null}
140
+ <details className="docs-skills__source" open>
141
+ <summary>Full skill instructions</summary>
142
+ <pre><code>{selectedSkill.body}</code></pre>
143
+ </details>
144
+ </article>
145
+ ) : null}
146
+ </div>
147
+ ) : (
148
+ <p className="docs-skills__empty">
149
+ No SKILL.md files were found in this repository. Add skills under a .agents/skills or
150
+ .codex/skills folder, then rebuild the Docs site.
151
+ </p>
152
+ )}
153
+ </section>
154
+ );
155
+ }
156
+
157
+ export default SkillsExplorer;
@@ -1,6 +1,7 @@
1
1
  /// <reference types="vite/client" />
2
2
 
3
3
  import type { DocsNavNode } from '../../../src/components/navigation.js';
4
+ import { getHue } from '@terra-symposium/styling';
4
5
 
5
6
  type SidebarFrontmatter = {
6
7
  label?: string;
@@ -110,6 +111,7 @@ for (const page of sitePages.filter((item) => !item.hidden && item.slug !== '404
110
111
  const overview = currentNodes.get('overview') ?? {
111
112
  id: 'overview',
112
113
  title: 'Overview',
114
+ hue: getHue('Overview'),
113
115
  children: new Map(),
114
116
  };
115
117
  if (!currentNodes.has('overview')) currentNodes.set('overview', overview);
@@ -120,7 +122,12 @@ for (const page of sitePages.filter((item) => !item.hidden && item.slug !== '404
120
122
  prefix = prefix ? `${prefix}/${segment}` : segment;
121
123
  let node = currentNodes.get(segment);
122
124
  if (!node) {
123
- node = { id: prefix, title: label(segment), children: new Map() };
125
+ node = {
126
+ id: prefix,
127
+ title: label(segment),
128
+ ...(prefix === segment ? { hue: getHue(label(segment)) } : {}),
129
+ children: new Map(),
130
+ };
124
131
  currentNodes.set(segment, node);
125
132
  }
126
133
  current = node;
@@ -0,0 +1,268 @@
1
+ .docs-skills {
2
+ margin-block: 2rem;
3
+ }
4
+
5
+ .docs-skills__header {
6
+ display: flex;
7
+ align-items: flex-end;
8
+ justify-content: space-between;
9
+ gap: 1.5rem;
10
+ margin-bottom: 1rem;
11
+ }
12
+
13
+ .docs-skills__header h2,
14
+ .docs-skills__detail h3 {
15
+ margin: 0;
16
+ }
17
+
18
+ .docs-skills__header p:not(.docs-skills__eyebrow) {
19
+ margin: 0.4rem 0 0;
20
+ color: color-mix(in srgb, currentColor 72%, transparent);
21
+ }
22
+
23
+ .docs-skills__eyebrow {
24
+ margin: 0 0 0.35rem;
25
+ color: color-mix(in srgb, currentColor 64%, transparent);
26
+ font-size: 0.75rem;
27
+ font-weight: 700;
28
+ letter-spacing: 0.07em;
29
+ text-transform: uppercase;
30
+ }
31
+
32
+ .docs-skills__summary {
33
+ display: grid;
34
+ justify-items: end;
35
+ color: color-mix(in srgb, currentColor 70%, transparent);
36
+ font-size: 0.85rem;
37
+ }
38
+
39
+ .docs-skills__summary strong {
40
+ color: var(--docs-accent, currentColor);
41
+ font-size: 1.6rem;
42
+ line-height: 1;
43
+ }
44
+
45
+ .docs-skills__layout {
46
+ display: grid;
47
+ grid-template-columns: minmax(16rem, 0.8fr) minmax(0, 1.6fr);
48
+ align-items: start;
49
+ gap: 1rem;
50
+ }
51
+
52
+ .docs-skills__index,
53
+ .docs-skills__detail {
54
+ min-width: 0;
55
+ overflow: hidden;
56
+ border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
57
+ border-radius: 0.75rem;
58
+ background: color-mix(in srgb, currentColor 3%, transparent);
59
+ }
60
+
61
+ .docs-skills__index {
62
+ max-height: 78vh;
63
+ overflow: auto;
64
+ padding: 0.75rem;
65
+ }
66
+
67
+ .docs-skills__filter {
68
+ display: grid;
69
+ gap: 0.35rem;
70
+ font-size: 0.8rem;
71
+ font-weight: 600;
72
+ }
73
+
74
+ .docs-skills__filter input {
75
+ box-sizing: border-box;
76
+ width: 100%;
77
+ min-height: 2.5rem;
78
+ border: 1px solid color-mix(in srgb, currentColor 24%, transparent);
79
+ border-radius: 0.45rem;
80
+ background: var(--background, transparent);
81
+ color: inherit;
82
+ font: inherit;
83
+ font-weight: 400;
84
+ padding: 0.45rem 0.6rem;
85
+ }
86
+
87
+ .docs-skills__filter input:focus-visible,
88
+ .docs-skills__item:focus-visible {
89
+ outline: 2px solid var(--docs-accent, currentColor);
90
+ outline-offset: 2px;
91
+ }
92
+
93
+ .docs-skills__count {
94
+ margin: 0.65rem 0 1rem;
95
+ color: color-mix(in srgb, currentColor 65%, transparent);
96
+ font-size: 0.8rem;
97
+ }
98
+
99
+ .docs-skills__group + .docs-skills__group {
100
+ margin-top: 1rem;
101
+ }
102
+
103
+ .docs-skills__group h3 {
104
+ margin: 0 0 0.4rem;
105
+ color: color-mix(in srgb, currentColor 68%, transparent);
106
+ font-size: 0.75rem;
107
+ font-weight: 700;
108
+ overflow-wrap: anywhere;
109
+ text-transform: uppercase;
110
+ }
111
+
112
+ .docs-skills__group ul {
113
+ display: grid;
114
+ gap: 0.35rem;
115
+ margin: 0;
116
+ padding: 0;
117
+ list-style: none;
118
+ }
119
+
120
+ .docs-skills__item {
121
+ display: grid;
122
+ width: 100%;
123
+ min-width: 0;
124
+ gap: 0.2rem;
125
+ border: 0;
126
+ border-radius: 0.5rem;
127
+ background: transparent;
128
+ color: inherit;
129
+ cursor: pointer;
130
+ font: inherit;
131
+ padding: 0.55rem;
132
+ text-align: left;
133
+ }
134
+
135
+ .docs-skills__item:hover,
136
+ .docs-skills__item[aria-current='true'] {
137
+ background: color-mix(in srgb, var(--docs-accent, currentColor) 12%, transparent);
138
+ }
139
+
140
+ .docs-skills__item[aria-current='true'] .docs-skills__item-title {
141
+ color: var(--docs-accent, currentColor);
142
+ font-weight: 700;
143
+ }
144
+
145
+ .docs-skills__item-title {
146
+ font-weight: 600;
147
+ }
148
+
149
+ .docs-skills__item-description,
150
+ .docs-skills__item-path {
151
+ color: color-mix(in srgb, currentColor 68%, transparent);
152
+ font-size: 0.78rem;
153
+ line-height: 1.4;
154
+ }
155
+
156
+ .docs-skills__item-path,
157
+ .docs-skills__source-path {
158
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
159
+ overflow-wrap: anywhere;
160
+ }
161
+
162
+ .docs-skills__detail > header {
163
+ padding: 1.25rem 1.25rem 0.75rem;
164
+ }
165
+
166
+ .docs-skills__description {
167
+ margin: 0.6rem 0;
168
+ color: color-mix(in srgb, currentColor 74%, transparent);
169
+ }
170
+
171
+ .docs-skills__source-path {
172
+ margin: 0;
173
+ color: color-mix(in srgb, currentColor 60%, transparent);
174
+ font-size: 0.75rem;
175
+ }
176
+
177
+ .docs-skills__metadata {
178
+ display: grid;
179
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
180
+ gap: 0.6rem;
181
+ margin: 0;
182
+ padding: 0.75rem 1.25rem 1.25rem;
183
+ }
184
+
185
+ .docs-skills__metadata > div {
186
+ min-width: 0;
187
+ border: 1px solid color-mix(in srgb, currentColor 15%, transparent);
188
+ border-radius: 0.5rem;
189
+ padding: 0.6rem 0.7rem;
190
+ }
191
+
192
+ .docs-skills__metadata dt {
193
+ color: color-mix(in srgb, currentColor 62%, transparent);
194
+ font-size: 0.72rem;
195
+ font-weight: 700;
196
+ text-transform: capitalize;
197
+ }
198
+
199
+ .docs-skills__metadata dd {
200
+ margin: 0.2rem 0 0;
201
+ font-size: 0.82rem;
202
+ overflow-wrap: anywhere;
203
+ }
204
+
205
+ .docs-skills__metadata-issue {
206
+ margin: 0 1.25rem 1rem;
207
+ color: #a12b2b;
208
+ font-size: 0.85rem;
209
+ }
210
+
211
+ .docs-skills__source {
212
+ border-top: 1px solid color-mix(in srgb, currentColor 16%, transparent);
213
+ }
214
+
215
+ .docs-skills__source summary {
216
+ cursor: pointer;
217
+ font-weight: 600;
218
+ padding: 0.8rem 1.25rem;
219
+ }
220
+
221
+ .docs-skills__source summary:focus-visible {
222
+ outline: 2px solid var(--docs-accent, currentColor);
223
+ outline-offset: -2px;
224
+ }
225
+
226
+ .docs-skills__source pre {
227
+ max-height: 60vh;
228
+ overflow: auto;
229
+ margin: 0;
230
+ border-top: 1px solid color-mix(in srgb, currentColor 12%, transparent);
231
+ background: color-mix(in srgb, currentColor 4%, transparent);
232
+ padding: 1rem 1.25rem;
233
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
234
+ font-size: 0.8rem;
235
+ line-height: 1.6;
236
+ white-space: pre-wrap;
237
+ overflow-wrap: anywhere;
238
+ }
239
+
240
+ .docs-skills__empty {
241
+ border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
242
+ border-radius: 0.75rem;
243
+ padding: 1rem;
244
+ }
245
+
246
+ @media (max-width: 48rem) {
247
+ .docs-skills__header {
248
+ align-items: flex-start;
249
+ }
250
+
251
+ .docs-skills__layout {
252
+ grid-template-columns: minmax(0, 1fr);
253
+ }
254
+
255
+ .docs-skills__index {
256
+ max-height: 45vh;
257
+ }
258
+ }
259
+
260
+ @media (max-width: 30rem) {
261
+ .docs-skills__header {
262
+ flex-direction: column;
263
+ }
264
+
265
+ .docs-skills__summary {
266
+ justify-items: start;
267
+ }
268
+ }