@terra-symposium/docs 0.1.76 → 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.
@@ -1,14 +1,44 @@
1
+ /**
2
+ * site/src/lib/catalog.ts
3
+ */
4
+
1
5
  /// <reference types="vite/client" />
2
6
 
3
- import type { DocsNavNode } from '../../../src/components/navigation.js';
7
+ /*//////////////////////////////////////////////////
8
+ IMPORTS
9
+ //////////////////////////////////////////////////*/
10
+
11
+ // API
4
12
  import { getHue } from '@terra-symposium/styling';
5
13
 
6
- type SidebarFrontmatter = {
7
- label?: string;
8
- order?: number;
9
- hidden?: boolean;
10
- };
14
+ // TYPES
15
+ import type { DocsNavNode } from '../../../src/components/navigation.js';
11
16
 
17
+ /*//////////////////////////////////////////////////
18
+ CONFIG
19
+ //////////////////////////////////////////////////*/
20
+
21
+ /**
22
+ * Matches hyphens and underscores that separate words in route labels.
23
+ *
24
+ * @return A global regular expression for replacing route word separators.
25
+ */
26
+ const G_REGEX = () => /[-_]+/g;
27
+ /**
28
+ * Matches the first Unicode letter after a word boundary for title capitalization.
29
+ *
30
+ * @return A global Unicode-aware regular expression for capitalizing word initials.
31
+ */
32
+ const B_P_L_GU_REGEX = () => /\b\p{L}/gu;
33
+ /**
34
+ * Matches leading or trailing slashes that should be removed from a path.
35
+ *
36
+ * @return A regular expression that matches leading or trailing slashes.
37
+ */
38
+ const G_CLASS_2 = () => /^\/+|\/+$/g;
39
+ /** Matches a leading YAML frontmatter block in a Markdown or MDX page. */
40
+ const S_N_S_S_N_S_N_REGEX = /^---\s*\n[\s\S]*?\n---\s*\n/;
41
+ type SidebarFrontmatter = { label?: string; order?: number; hidden?: boolean };
12
42
  type DocsFrontmatter = {
13
43
  title?: string;
14
44
  description?: string;
@@ -19,13 +49,10 @@ type DocsFrontmatter = {
19
49
  visibility?: 'visible' | 'hidden';
20
50
  sidebar?: SidebarFrontmatter;
21
51
  };
22
-
23
- type ContentModule = {
24
- Content?: unknown;
25
- default?: unknown;
26
- frontmatter?: DocsFrontmatter;
27
- };
28
-
52
+ type ContentModule = { Content?: unknown; default?: unknown; frontmatter?: DocsFrontmatter };
53
+ /**
54
+ * Renderable Docs page metadata derived from its Markdown source and frontmatter.
55
+ */
29
56
  export type SitePage = {
30
57
  file: string;
31
58
  slug: string;
@@ -38,7 +65,6 @@ export type SitePage = {
38
65
  content: unknown;
39
66
  markdown: string;
40
67
  };
41
-
42
68
  const modules = import.meta.glob<ContentModule>('/docs/**/*.{md,mdx}', { eager: true });
43
69
  const sources = import.meta.glob<string>('/docs/**/*.{md,mdx}', {
44
70
  query: '?raw',
@@ -46,28 +72,13 @@ const sources = import.meta.glob<string>('/docs/**/*.{md,mdx}', {
46
72
  eager: true,
47
73
  });
48
74
  const base = import.meta.env.BASE_URL;
49
-
50
- function titleFromSource(source: string, slug: string): string {
51
- const body = source.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, '');
52
- const heading = /^#\s+(.+?)\s*#*\s*$/m.exec(body)?.[1]?.trim();
53
- if (heading) return heading;
54
- const name = slug.split('/').at(-1) || 'Overview';
55
- return name.replace(/[-_]+/g, ' ').replace(/\b\p{L}/gu, (letter) => letter.toUpperCase());
56
- }
57
-
58
- function routeForFile(file: string): string {
59
- const relative = file.replace(/^\/?docs\//, '').replace(/\.(?:md|mdx)$/i, '');
60
- return relative.replace(/(?:^|\/)index$/i, '').replace(/^\/+|\/+$/g, '');
61
- }
62
-
63
75
  const routes = new Map<string, string>();
76
+ /** Visible Docs pages sorted by configured sidebar order and source path. */
64
77
  export const sitePages: SitePage[] = Object.entries(modules)
65
78
  .map(([file, module]) => {
66
79
  const slug = routeForFile(file);
67
80
  const previous = routes.get(slug);
68
- if (previous) {
69
- throw new Error(`Duplicate Docs route "${slug || '/'}": ${previous} and ${file}`);
70
- }
81
+ if (previous) throw new Error(`Duplicate Docs route "${slug || '/'}": ${previous} and ${file}`);
71
82
  routes.set(slug, file);
72
83
  const markdown = sources[file] ?? '';
73
84
  const frontmatter = module.frontmatter ?? {};
@@ -84,25 +95,117 @@ export const sitePages: SitePage[] = Object.entries(modules)
84
95
  frontmatter.sidebar?.hidden ?? frontmatter.hidden ?? frontmatter.visibility === 'hidden',
85
96
  order: frontmatter.sidebar?.order ?? frontmatter.order ?? Number.MAX_SAFE_INTEGER,
86
97
  content: module.Content ?? module.default,
87
- markdown: markdown.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, ''),
98
+ markdown: markdown.replace(S_N_S_S_N_S_N_REGEX, ''),
88
99
  };
89
100
  })
90
101
  .sort((left, right) => left.order - right.order || left.file.localeCompare(right.file));
102
+ type MutableNode = Omit<DocsNavNode, 'children'> & { children: Map<string, MutableNode> };
103
+ const rootNodes = new Map<string, MutableNode>();
104
+ export const siteNavigation = materialize(rootNodes);
91
105
 
92
- if (sitePages.length === 0) {
93
- throw new Error(
94
- 'No Markdown files found in docs/. Add docs/index.md to start a Terra Docs site.',
95
- );
106
+ /*//////////////////////////////////////////////////
107
+ HELPERS
108
+ //////////////////////////////////////////////////*/
109
+
110
+ /**
111
+ * Resolves a page title from its first heading or, when absent, its route slug.
112
+ *
113
+ * @param source Markdown source after import, including optional frontmatter.
114
+ * @param slug Normalized route path used as the title fallback.
115
+ *
116
+ * @return The authored heading or a title-cased final route segment.
117
+ */
118
+ function titleFromSource(source: string, slug: string): string {
119
+ const body = source.replace(S_N_S_S_N_S_N_REGEX, '');
120
+ const heading = /^#\s+(.+?)\s*#*\s*$/m.exec(body)?.[1]?.trim();
121
+ if (heading) return heading;
122
+ const name = slug.split('/').at(-1) || 'Overview';
123
+ return name.replace(G_REGEX(), ' ').replace(B_P_L_GU_REGEX(), (letter) => letter.toUpperCase());
96
124
  }
97
125
 
98
- type MutableNode = Omit<DocsNavNode, 'children'> & { children: Map<string, MutableNode> };
99
- const rootNodes = new Map<string, MutableNode>();
126
+ /**
127
+ * Converts a Markdown or MDX file path into its slash-delimited route slug.
128
+ *
129
+ * @param file Vite glob path for the source page.
130
+ *
131
+ * @return The page path without its extension or trailing `index` segment.
132
+ */
133
+ function routeForFile(file: string): string {
134
+ const relative = file.replace(/^\/?docs\//, '').replace(/\.(?:md|mdx)$/i, '');
135
+ return relative.replace(/(?:^|\/)index$/i, '').replace(G_CLASS_2(), '');
136
+ }
100
137
 
138
+ /**
139
+ * Creates a display label from a route segment by replacing separators and capitalizing words.
140
+ *
141
+ * @param value Route segment to display in navigation.
142
+ *
143
+ * @return Human-readable navigation text.
144
+ */
101
145
  function label(value: string): string {
102
- return value.replace(/[-_]+/g, ' ').replace(/\b\p{L}/gu, (letter) => letter.toUpperCase());
146
+ return value.replace(G_REGEX(), ' ').replace(B_P_L_GU_REGEX(), (letter) => letter.toUpperCase());
103
147
  }
104
148
 
105
- for (const page of sitePages.filter((item) => !item.hidden && item.slug !== '404')) {
149
+ /**
150
+ * Builds the navigation fields associated with a page at its public route.
151
+ *
152
+ * @param page Page whose title, label, description, and hue supply the navigation metadata.
153
+ * @param href Public URL for the page.
154
+ *
155
+ * @return The metadata fields to merge into its navigation node.
156
+ */
157
+ function pageMetadata(page: SitePage, href: string): Partial<MutableNode> {
158
+ return {
159
+ id: page.slug || 'index',
160
+ title: page.sidebarLabel ?? page.title,
161
+ href,
162
+ ...(page.description ? { description: page.description } : {}),
163
+ ...(page.hue !== undefined ? { hue: page.hue } : {}),
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Converts the mutable map-based navigation tree into the serializable Docs navigation shape.
169
+ *
170
+ * @param nodes Child nodes indexed by their route segment.
171
+ *
172
+ * @return Navigation nodes with nested children represented as arrays.
173
+ */
174
+ function materialize(nodes: Map<string, MutableNode>): DocsNavNode[] {
175
+ return [...nodes.values()].map(({ children, ...node }) => ({
176
+ ...node,
177
+ ...(children.size ? { children: materialize(children) } : {}),
178
+ }));
179
+ }
180
+
181
+ /**
182
+ * Builds the public URL for a page, respecting a configured deployment base path.
183
+ *
184
+ * @param slug Page route without a leading or trailing slash.
185
+ * @param base Public base path, with or without its surrounding slashes.
186
+ *
187
+ * @return A route with a trailing slash for the Docs site.
188
+ */
189
+ export function pageHref(slug: string, base = '/'): string {
190
+ const prefix = base === '/' ? '' : `/${base.replace(G_CLASS_2(), '')}`;
191
+ return slug ? `${prefix}/${slug}/` : `${prefix}/`;
192
+ }
193
+
194
+ /**
195
+ * Looks up a page by its normalized route slug.
196
+ *
197
+ * @param slug Route slug to find.
198
+ *
199
+ * @return The matching page, or `undefined` when the catalog has no such route.
200
+ */
201
+ export function pageForSlug(slug: string): SitePage | undefined {
202
+ return sitePages.find((page) => page.slug === slug);
203
+ }
204
+ if (sitePages.length === 0)
205
+ throw new Error(
206
+ 'No Markdown files found in docs/. Add docs/index.md to start a Terra Docs site.',
207
+ );
208
+ for (const page of sitePages.filter((i) => !i.hidden && i.slug !== '404')) {
106
209
  const segments = page.slug.split('/').filter(Boolean);
107
210
  let currentNodes = rootNodes;
108
211
  let current: MutableNode | undefined;
@@ -135,31 +238,3 @@ for (const page of sitePages.filter((item) => !item.hidden && item.slug !== '404
135
238
  }
136
239
  if (current) Object.assign(current, pageMetadata(page, pageHref(page.slug, base)));
137
240
  }
138
-
139
- function pageMetadata(page: SitePage, href: string): Partial<MutableNode> {
140
- return {
141
- id: page.slug || 'index',
142
- title: page.sidebarLabel ?? page.title,
143
- href,
144
- ...(page.description ? { description: page.description } : {}),
145
- ...(page.hue !== undefined ? { hue: page.hue } : {}),
146
- };
147
- }
148
-
149
- function materialize(nodes: Map<string, MutableNode>): DocsNavNode[] {
150
- return [...nodes.values()].map(({ children, ...node }) => ({
151
- ...node,
152
- ...(children.size ? { children: materialize(children) } : {}),
153
- }));
154
- }
155
-
156
- export const siteNavigation = materialize(rootNodes);
157
-
158
- export function pageHref(slug: string, base = '/'): string {
159
- const prefix = base === '/' ? '' : `/${base.replace(/^\/+|\/+$/g, '')}`;
160
- return slug ? `${prefix}/${slug}/` : `${prefix}/`;
161
- }
162
-
163
- export function pageForSlug(slug: string): SitePage | undefined {
164
- return sitePages.find((page) => page.slug === slug);
165
- }
@@ -1,7 +1,7 @@
1
1
  ---
2
- import { DocsStaticWorkspace } from '../../lib-dist/components/StaticWorkspace.js';
2
+ import { DocsSiteIsland } from '../../site/src/components/DocsSiteIsland.js';
3
+ import AstroErrorBoundary from '@terra-symposium/standards-error-boundaries/astro-boundary';
3
4
  import '../../lib-dist/style.css';
4
- import { DocsSiteSearch } from '../../site/src/components/DocsSiteSearch.js';
5
5
  import '../../site/src/styles/search.css';
6
6
  import type { DocsAttachment, DocsSection, DocsNavNode } from '../../lib-dist/lib/index.js';
7
7
 
@@ -25,25 +25,27 @@ const { title, homeHref = '/', description, pageTitle, nodes = [], sections, cur
25
25
  const staticContent = Astro.slots.has('default');
26
26
  ---
27
27
 
28
- <DocsStaticWorkspace
29
- client:load
30
- title={title}
31
- homeHref={homeHref}
32
- description={description}
33
- pageTitle={pageTitle}
34
- nodes={nodes}
35
- sections={sections}
36
- currentPath={currentPath}
37
- rootPath={rootPath}
38
- markdown={markdown}
39
- content={content}
40
- attachments={attachments}
41
- updatedAt={updatedAt}
42
- staticContent={staticContent}
43
- >
44
- <slot />
45
- </DocsStaticWorkspace>
46
- <DocsSiteSearch client:load base={searchBase} />
28
+ <AstroErrorBoundary title="Documentation could not load" description="The Docs interface failed to start. The error has been recorded for diagnosis." reloadLabel="Reload Docs">
29
+ <DocsSiteIsland
30
+ client:load
31
+ title={title}
32
+ homeHref={homeHref}
33
+ description={description}
34
+ pageTitle={pageTitle}
35
+ nodes={nodes}
36
+ sections={sections}
37
+ currentPath={currentPath}
38
+ rootPath={rootPath}
39
+ markdown={markdown}
40
+ content={content}
41
+ attachments={attachments}
42
+ updatedAt={updatedAt}
43
+ staticContent={staticContent}
44
+ searchBase={searchBase}
45
+ >
46
+ <slot />
47
+ </DocsSiteIsland>
48
+ </AstroErrorBoundary>
47
49
 
48
50
  <style is:global>
49
51
  .terra-docs-page-content { max-width: 100%; }
@@ -1,268 +0,0 @@
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
- }