@ciderpress/ui 1.0.0-rc.12 → 1.0.0-rc.14

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 (52) hide show
  1. package/dist/head/css/themes/honeycrisp.css +1 -1
  2. package/dist/node.mjs +17 -13
  3. package/dist/plugins/mermaid/MermaidRenderer.tsx +2 -2
  4. package/dist/theme/components/home/hero-banner.tsx +225 -0
  5. package/dist/theme/components/home/hero-demo.css +492 -61
  6. package/dist/theme/components/home/hero-demo.test.tsx +159 -0
  7. package/dist/theme/components/home/hero-demo.tsx +335 -32
  8. package/dist/theme/components/home/hero.css +142 -9
  9. package/dist/theme/components/home/hero.test.tsx +105 -0
  10. package/dist/theme/components/home/hero.tsx +201 -4
  11. package/dist/theme/components/home/home-visual.css +98 -0
  12. package/dist/theme/components/home/home-visual.test.tsx +27 -0
  13. package/dist/theme/components/home/home-visual.tsx +20 -13
  14. package/dist/theme/components/home/layout.tsx +125 -1
  15. package/dist/theme/components/home/split.css +2 -2
  16. package/dist/theme/components/home/tabs.css +1 -1
  17. package/dist/theme/components/home/trust-strip.css +12 -0
  18. package/dist/theme/components/home/trust-strip.test.tsx +33 -0
  19. package/dist/theme/components/home/trust-strip.tsx +76 -15
  20. package/dist/theme/components/nav/layout.tsx +10 -14
  21. package/dist/theme/components/nav/nav-logo.tsx +2 -3
  22. package/dist/theme/components/sidebar/sidebar-filter.test.ts +49 -9
  23. package/dist/theme/components/sidebar/sidebar-filter.ts +24 -4
  24. package/dist/theme/index.tsx +8 -1
  25. package/dist/theme/lib/rich-text-parse.ts +0 -2
  26. package/dist/theme/lib/theme-favicon.ts +0 -3
  27. package/dist/theme/styles/themes/honeycrisp.css +20 -20
  28. package/package.json +20 -19
  29. package/src/theme/components/home/hero-banner.tsx +225 -0
  30. package/src/theme/components/home/hero-demo.css +492 -61
  31. package/src/theme/components/home/hero-demo.test.tsx +159 -0
  32. package/src/theme/components/home/hero-demo.tsx +335 -32
  33. package/src/theme/components/home/hero.css +142 -9
  34. package/src/theme/components/home/hero.test.tsx +105 -0
  35. package/src/theme/components/home/hero.tsx +201 -4
  36. package/src/theme/components/home/home-visual.css +98 -0
  37. package/src/theme/components/home/home-visual.test.tsx +27 -0
  38. package/src/theme/components/home/home-visual.tsx +20 -13
  39. package/src/theme/components/home/layout.tsx +125 -1
  40. package/src/theme/components/home/split.css +2 -2
  41. package/src/theme/components/home/tabs.css +1 -1
  42. package/src/theme/components/home/trust-strip.css +12 -0
  43. package/src/theme/components/home/trust-strip.test.tsx +33 -0
  44. package/src/theme/components/home/trust-strip.tsx +76 -15
  45. package/src/theme/components/nav/layout.tsx +10 -14
  46. package/src/theme/components/nav/nav-logo.tsx +2 -3
  47. package/src/theme/components/sidebar/sidebar-filter.test.ts +49 -9
  48. package/src/theme/components/sidebar/sidebar-filter.ts +24 -4
  49. package/src/theme/index.tsx +8 -1
  50. package/src/theme/lib/rich-text-parse.ts +0 -2
  51. package/src/theme/lib/theme-favicon.ts +0 -3
  52. package/src/theme/styles/themes/honeycrisp.css +20 -20
@@ -0,0 +1,159 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { act } from 'react'
4
+ import { createRoot } from 'react-dom/client'
5
+ import type { Root } from 'react-dom/client'
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7
+
8
+ import { HeroDemo } from './hero-demo.tsx'
9
+
10
+ describe('hero demo', () => {
11
+ beforeEach(() => {
12
+ vi.useFakeTimers()
13
+ })
14
+
15
+ afterEach(() => {
16
+ vi.useRealTimers()
17
+ document.body.replaceChildren()
18
+ })
19
+
20
+ it('should explain the repository discovery workflow', async () => {
21
+ expect.assertions(4)
22
+ const rendered = await renderHeroDemo()
23
+ const output = rendered.container.textContent
24
+ expect(output).toContain('ciderpress dev')
25
+ expect(output).not.toContain('pnpm')
26
+ expect(output).toContain('docs/**/*.md')
27
+ expect(output).toContain('generated navigation from repository structure')
28
+ await unmountHeroDemo(rendered)
29
+ })
30
+
31
+ it('should switch previews through keyboard-focusable tab controls', async () => {
32
+ expect.assertions(4)
33
+ const rendered = await renderHeroDemo()
34
+ const cli = getButton({ container: rendered.container, label: 'CLI' })
35
+ const docs = getButton({ container: rendered.container, label: 'View docs' })
36
+ docs.focus()
37
+ expect(document.activeElement).toBe(docs)
38
+ await click(docs)
39
+ expect(docs.getAttribute('aria-selected')).toBe('true')
40
+ expect(cli.getAttribute('aria-selected')).toBe('false')
41
+ expect(getButton({ container: rendered.container, label: 'rerun' }).tabIndex).toBe(0)
42
+ await unmountHeroDemo(rendered)
43
+ })
44
+
45
+ it('should include the generated Acme Corp documentation preview', async () => {
46
+ expect.assertions(4)
47
+ const rendered = await renderHeroDemo()
48
+ const output = rendered.container.textContent
49
+ expect(output).toContain('http://localhost:6174')
50
+ expect(output).toContain('Acme Docs')
51
+ expect(output).toContain('Getting Started')
52
+ expect(output).toContain('On this page')
53
+ await unmountHeroDemo(rendered)
54
+ })
55
+
56
+ it('should advance timed stages and reset them when rerun', async () => {
57
+ expect.assertions(6)
58
+ const rendered = await renderHeroDemo()
59
+ const log = getLog(rendered.container)
60
+ const starting = getTextElement({ container: rendered.container, text: '◒ Starting' })
61
+ const ready = getTextElement({ container: rendered.container, text: '● Ready' })
62
+ expect(log.getAttribute('aria-busy')).toBe('true')
63
+ expect(starting.getAttribute('aria-hidden')).toBe('false')
64
+ expect(ready.getAttribute('aria-hidden')).toBe('true')
65
+ await act(() => vi.advanceTimersByTime(4_100))
66
+ expect(log.getAttribute('aria-busy')).toBe('false')
67
+ expect(ready.getAttribute('aria-hidden')).toBe('false')
68
+ await click(getButton({ container: rendered.container, label: 'rerun' }))
69
+ expect(log.getAttribute('aria-busy')).toBe('true')
70
+ await unmountHeroDemo(rendered)
71
+ })
72
+ })
73
+
74
+ interface RenderedHeroDemo {
75
+ readonly container: HTMLDivElement
76
+ readonly root: Root
77
+ }
78
+
79
+ /**
80
+ * Render the interactive hero demo into the test document.
81
+ *
82
+ * @private
83
+ * @returns Mounted component controls.
84
+ */
85
+ async function renderHeroDemo(): Promise<RenderedHeroDemo> {
86
+ const container = document.createElement('div')
87
+ document.body.append(container)
88
+ const root = createRoot(container)
89
+ await act(async () => root.render(<HeroDemo />))
90
+ return { container, root }
91
+ }
92
+
93
+ /**
94
+ * Unmount a rendered hero demo and flush effect cleanup.
95
+ *
96
+ * @private
97
+ * @param rendered - Mounted component controls.
98
+ * @returns Promise resolved after cleanup.
99
+ */
100
+ async function unmountHeroDemo(rendered: RenderedHeroDemo): Promise<void> {
101
+ return act(async () => rendered.root.unmount())
102
+ }
103
+
104
+ /**
105
+ * Click a native button and flush React updates.
106
+ *
107
+ * @private
108
+ * @param button - Button to activate.
109
+ * @returns Promise resolved after event handling.
110
+ */
111
+ async function click(button: HTMLButtonElement): Promise<void> {
112
+ return act(async () => button.click())
113
+ }
114
+
115
+ /**
116
+ * Find a button by its accessible text.
117
+ *
118
+ * @private
119
+ * @param params - Rendered component container and visible button label.
120
+ * @returns Matching button.
121
+ */
122
+ function getButton(params: {
123
+ readonly container: HTMLElement
124
+ readonly label: string
125
+ }): HTMLButtonElement {
126
+ const button = Array.from(params.container.querySelectorAll('button')).find(
127
+ (candidate) => candidate.textContent.trim() === params.label
128
+ )
129
+ return button as HTMLButtonElement
130
+ }
131
+
132
+ /**
133
+ * Find the live terminal output region.
134
+ *
135
+ * @private
136
+ * @param container - Rendered component container.
137
+ * @returns Terminal log element.
138
+ */
139
+ function getLog(container: HTMLElement): HTMLElement {
140
+ const log = container.querySelector<HTMLElement>('[role="log"]')
141
+ return log as HTMLElement
142
+ }
143
+
144
+ /**
145
+ * Find an element by exact text content.
146
+ *
147
+ * @private
148
+ * @param params - Rendered component container and exact visible text.
149
+ * @returns Matching element.
150
+ */
151
+ function getTextElement(params: {
152
+ readonly container: HTMLElement
153
+ readonly text: string
154
+ }): HTMLElement {
155
+ const element = Array.from(params.container.querySelectorAll<HTMLElement>('*')).find(
156
+ (candidate) => candidate.children.length === 0 && candidate.textContent.trim() === params.text
157
+ )
158
+ return element as HTMLElement
159
+ }
@@ -1,46 +1,349 @@
1
+ import { match } from 'massaman/match'
1
2
  import type React from 'react'
3
+ import { useEffect, useState } from 'react'
2
4
 
3
5
  import './hero-demo.css'
4
6
 
7
+ type DemoView = 'cli' | 'docs'
8
+
9
+ const FINAL_RUN_STEP = 5
10
+ const INITIAL_RUN_DELAY = 800
11
+ const RUN_DELAYS = [1300, 1850, 2300, 2700, 3300] as const
12
+ const activity = [
13
+ ['23:18:04', 'synced', 'docs/getting-started/introduction.md', '12ms', 3],
14
+ ['23:18:04', 'synced', 'packages/api/docs/authentication.md', '8ms', 4],
15
+ ['23:18:05', 'restarted', 'openapi/acme.yaml', '41ms', 4],
16
+ ] as const
17
+
5
18
  /**
6
- * HeroDemo — default terminal-style demo block used inside Hero.
19
+ * Interactive hero demo showing Ciderpress turning repository Markdown into an internal docs site.
7
20
  *
8
- * Renders a fake macOS window with `pnpm ciderpress dev` output. Self-contained
9
- * so consumers can render it directly in MDX or via HomeLayout.
10
- *
11
- * @returns React element.
21
+ * @returns CLI and generated documentation preview
12
22
  */
13
23
  export function HeroDemo(): React.ReactElement {
24
+ const [activeView, setActiveView] = useState<DemoView>('cli')
25
+ const [runId, setRunId] = useState(0)
26
+ const [runStep, setRunStep] = useState(0)
27
+
28
+ useEffect(() => {
29
+ if (activeView === 'docs') {
30
+ return
31
+ }
32
+
33
+ setRunStep(0)
34
+ if (globalThis.matchMedia('(prefers-reduced-motion: reduce)').matches) {
35
+ setRunStep(FINAL_RUN_STEP)
36
+ return
37
+ }
38
+
39
+ const runDelay = match(runId)
40
+ .with(0, () => INITIAL_RUN_DELAY)
41
+ .otherwise(() => 0)
42
+ const timers = RUN_DELAYS.map((delay, index) =>
43
+ globalThis.setTimeout(() => setRunStep(index + 1), runDelay + delay)
44
+ )
45
+ return function clearRunTimers(): void {
46
+ return timers.reduce<undefined>((_, timer) => {
47
+ globalThis.clearTimeout(timer)
48
+ return undefined
49
+ }, undefined)
50
+ }
51
+ }, [activeView, runId])
52
+
53
+ function showCli(): void {
54
+ setActiveView('cli')
55
+ setRunId((current) => current + 1)
56
+ }
57
+
58
+ function showDocs(): void {
59
+ setActiveView('docs')
60
+ }
61
+
14
62
  return (
15
63
  <div className="cp-hero-demo">
16
- <div className="cp-hero-demo__bar">
17
- <span className="cp-hero-demo__dot" />
18
- <span className="cp-hero-demo__dot" />
19
- <span className="cp-hero-demo__dot" />
20
- <span className="cp-hero-demo__title">
21
- <em>~/code/acme</em> — pnpm ciderpress dev
22
- </span>
64
+ <div className="cp-window cp-window--terminal" hidden={activeView !== 'cli'}>
65
+ <div className="cp-window__titlebar">
66
+ <WindowDots />
67
+ <span className="cp-window__title">ciderpress dev — ~/code/acme-platform</span>
68
+ </div>
69
+ <div className="cp-window__content">
70
+ <div
71
+ className="cp-hero-demo__terminal"
72
+ role="log"
73
+ aria-label="Ciderpress development server output"
74
+ aria-live="polite"
75
+ aria-busy={runStep < FINAL_RUN_STEP}
76
+ >
77
+ <span className="cp-term-line cp-term-line--command">
78
+ <span className="cp-hero-demo__prompt" aria-hidden="true">
79
+ $
80
+ </span>
81
+ <span className="cp-hero-demo__typing" data-initial-run={runId === 0} key={runId}>
82
+ ciderpress dev
83
+ </span>
84
+ </span>
85
+ <span
86
+ className="cp-hero-demo__loading"
87
+ data-initial-run={runId === 0}
88
+ data-visible={runStep === 0}
89
+ aria-hidden={runStep !== 0}
90
+ >
91
+ ◒ loading
92
+ </span>
93
+ <div
94
+ className="cp-hero-demo__run-output"
95
+ data-visible={runStep >= 1}
96
+ aria-hidden={runStep < 1}
97
+ >
98
+ <pre className="cp-hero-demo__cli-banner" aria-label="Ciderpress">
99
+ {'█▀▀ █ █▀▄ █▀▀ █▀█ █▀█ █▀█ █▀▀ █▀▀ █▀▀\n█▄▄ █ █▄▀ ██▄ █▀▄ █▀▀ █▀▄ ██▄ ▄▄█ ▄▄█'}
100
+ </pre>
101
+ <div className="cp-hero-demo__status-row">
102
+ <span className="cp-term-text--cyan">http://localhost:6174</span>
103
+ <span className="cp-hero-demo__status">
104
+ <span
105
+ className="cp-term-text--yellow"
106
+ data-visible={runStep < FINAL_RUN_STEP}
107
+ aria-hidden={runStep >= FINAL_RUN_STEP}
108
+ >
109
+ ◒ Starting
110
+ </span>
111
+ <span
112
+ className="cp-term-text--green"
113
+ data-visible={runStep >= FINAL_RUN_STEP}
114
+ aria-hidden={runStep < FINAL_RUN_STEP}
115
+ >
116
+ ● Ready
117
+ </span>
118
+ </span>
119
+ </div>
120
+ </div>
121
+ <div
122
+ className="cp-hero-demo__run-output"
123
+ data-visible={runStep >= 2}
124
+ aria-hidden={runStep < 2}
125
+ >
126
+ <div className="cp-hero-demo__rule" />
127
+ <div className="cp-hero-demo__watching">
128
+ <span className="cp-term-text--dim">watching </span>
129
+ <span className="cp-term-text--white">docs/**/*.md</span>
130
+ <span className="cp-term-text--dim"> across 12 workspaces</span>
131
+ </div>
132
+ </div>
133
+ <div className="cp-hero-demo__activity">
134
+ {activity.map(([time, action, file, elapsed, visibleAt], index) => (
135
+ <div
136
+ className="cp-hero-demo__activity-row cp-hero-demo__run-output"
137
+ data-visible={runStep >= visibleAt}
138
+ aria-hidden={runStep < visibleAt}
139
+ key={file}
140
+ >
141
+ <span
142
+ className={match(index)
143
+ .with(0, () => '')
144
+ .otherwise(() => 'cp-term-text--dim')}
145
+ >
146
+ {time}
147
+ </span>
148
+ <span
149
+ className={match({ action, index })
150
+ .with({ action: 'restarted', index: 0 }, () => 'cp-term-text--yellow')
151
+ .with({ action: 'restarted' }, () => 'cp-term-text--yellow cp-term-text--dim')
152
+ .with({ index: 0 }, () => 'cp-term-text--green')
153
+ .otherwise(() => 'cp-term-text--green cp-term-text--dim')}
154
+ >
155
+ {action}
156
+ </span>
157
+ <span
158
+ className={match(index)
159
+ .with(0, () => '')
160
+ .otherwise(() => 'cp-term-text--dim')}
161
+ >
162
+ {file}
163
+ </span>
164
+ <span className="cp-term-text--dim">{elapsed}</span>
165
+ </div>
166
+ ))}
167
+ </div>
168
+ <div
169
+ className="cp-hero-demo__run-output"
170
+ data-visible={runStep >= FINAL_RUN_STEP}
171
+ aria-hidden={runStep < FINAL_RUN_STEP}
172
+ >
173
+ <div className="cp-hero-demo__rule" />
174
+ <div className="cp-hero-demo__stats">
175
+ <span>
176
+ <span className="cp-term-text--green">84</span>
177
+ <span className="cp-term-text--dim"> written · </span>
178
+ <span className="cp-term-text--yellow">0</span>
179
+ <span className="cp-term-text--dim"> skipped · </span>
180
+ <span className="cp-term-text--red">0</span>
181
+ <span className="cp-term-text--dim"> removed · 412ms</span>
182
+ </span>
183
+ <button className="cp-hero-demo__open" type="button" onClick={showDocs}>
184
+ <span className="cp-term-text--cyan">o</span>
185
+ <span> open docs</span>
186
+ </button>
187
+ </div>
188
+ </div>
189
+ <span className="cp-hero-demo__sr-only">
190
+ generated navigation from repository structure
191
+ </span>
192
+ </div>
193
+ </div>
194
+ </div>
195
+
196
+ <div className="cp-window cp-window--browser" hidden={activeView !== 'docs'}>
197
+ <div className="cp-window__titlebar">
198
+ <WindowDots />
199
+ <div className="cp-browser__tab">
200
+ <span className="cp-browser__tab-title">Acme Corp · Introduction</span>
201
+ </div>
202
+ </div>
203
+ <div className="cp-browser__url-bar">
204
+ <BrowserNav />
205
+ <span className="cp-browser__url">http://localhost:6174</span>
206
+ <svg className="cp-browser__menu-icon" viewBox="0 0 16 16" fill="currentColor">
207
+ <circle cx="8" cy="3" r="1.2" />
208
+ <circle cx="8" cy="8" r="1.2" />
209
+ <circle cx="8" cy="13" r="1.2" />
210
+ </svg>
211
+ </div>
212
+ <div className="cp-window__content">
213
+ <div className="cp-hero-demo__site">
214
+ <header className="cp-hero-demo__site-header">
215
+ <strong className="cp-hero-demo__site-wordmark">Acme Docs</strong>
216
+ <div className="cp-hero-demo__search">
217
+ Search <kbd>⌘K</kbd>
218
+ </div>
219
+ <nav className="cp-hero-demo__site-nav" aria-label="Generated documentation">
220
+ <strong>Getting Started</strong>
221
+ <span>Concepts</span>
222
+ <span>Guides</span>
223
+ <span>API</span>
224
+ </nav>
225
+ </header>
226
+ <div className="cp-hero-demo__docs-layout">
227
+ <aside className="cp-hero-demo__sidebar">
228
+ <span className="cp-hero-demo__sidebar-menu">▣ Menu</span>
229
+ <span>⌂ Home</span>
230
+ <span>▱ Changelog</span>
231
+ <span className="cp-hero-demo__sidebar-label">Getting started</span>
232
+ <strong>Introduction</strong>
233
+ <span>Quick Start</span>
234
+ <span className="cp-hero-demo__sidebar-label">Concepts</span>
235
+ <span>Content</span>
236
+ <span>Navigation</span>
237
+ <span>Publishing</span>
238
+ <span>Themes</span>
239
+ </aside>
240
+ <main className="cp-hero-demo__document">
241
+ <div className="cp-hero-demo__breadcrumb">⌂ › Getting Started › Introduction</div>
242
+ <article>
243
+ <h2>Introduction</h2>
244
+ <div className="cp-hero-demo__document-rule" />
245
+ <h3>What is Acme Docs?</h3>
246
+ <p>
247
+ Acme Docs is the source of truth for building, shipping, and operating the
248
+ platform. Content stays beside the code that it documents.
249
+ </p>
250
+ <code>ciderpress dev</code>
251
+ <p>One command, working docs site.</p>
252
+ <h3>Why Acme Docs?</h3>
253
+ </article>
254
+ </main>
255
+ <aside className="cp-hero-demo__toc">
256
+ <strong>On this page</strong>
257
+ <span>What is Acme Docs?</span>
258
+ <span>Why Acme Docs?</span>
259
+ <span>Features</span>
260
+ </aside>
261
+ </div>
262
+ </div>
263
+ </div>
23
264
  </div>
24
- <pre className="cp-hero-demo__body">
25
- <span className="cp-hero-demo__prompt">$ </span>pnpm ciderpress dev
26
- {'\n\n'}
27
- <span className="cp-hero-demo__ok"> ▸</span> ciderpress 0.5 — Rspress 2.0
28
- {'\n'}
29
- <span className="cp-hero-demo__ok"> ▸</span> watching{' '}
30
- <span className="cp-hero-demo__file">./docs</span>,{' '}
31
- <span className="cp-hero-demo__file">./openapi.yaml</span>
32
- {'\n'}
33
- <span className="cp-hero-demo__ok"> ▸</span> sidebar synced from 24 files
34
- {'\n'}
35
- <span className="cp-hero-demo__info"> ▸</span> openapi spec parsed — 18 endpoints
36
- {'\n'}
37
- <span className="cp-hero-demo__ok"> ✓</span> ready on{' '}
38
- <span className="cp-hero-demo__file">http://localhost:4321</span> in 412ms
39
- {'\n\n'}
40
- <span className="cp-hero-demo__cmt"> ↻ docs/guides/openapi.md changed</span>
41
- {'\n'}
42
- <span className="cp-hero-demo__cmt"> ↻ rebuilt sidebar in 8ms</span>
43
- </pre>
265
+
266
+ <div className="cp-hero-demo__switcher">
267
+ <div role="tablist" aria-label="Ciderpress preview">
268
+ <button
269
+ type="button"
270
+ role="tab"
271
+ aria-selected={activeView === 'cli'}
272
+ className={match(activeView)
273
+ .with('cli', () => 'is-active')
274
+ .otherwise(() => '')}
275
+ onClick={showCli}
276
+ >
277
+ CLI
278
+ </button>
279
+ <button
280
+ type="button"
281
+ role="tab"
282
+ aria-selected={activeView === 'docs'}
283
+ className={match(activeView)
284
+ .with('docs', () => 'is-active')
285
+ .otherwise(() => '')}
286
+ onClick={showDocs}
287
+ >
288
+ View docs
289
+ </button>
290
+ </div>
291
+ </div>
292
+ <button className="cp-hero-demo__rerun" type="button" onClick={showCli}>
293
+ <svg aria-hidden="true" viewBox="0 0 16 16" fill="none">
294
+ <path
295
+ d="M13 8a5 5 0 1 1-1.46-3.54L13 6M13 3v3h-3"
296
+ stroke="currentColor"
297
+ strokeWidth="1.5"
298
+ strokeLinecap="round"
299
+ strokeLinejoin="round"
300
+ />
301
+ </svg>
302
+ rerun
303
+ </button>
304
+ </div>
305
+ )
306
+ }
307
+
308
+ /**
309
+ * Render canonical desktop-window traffic lights.
310
+ *
311
+ * @private
312
+ * @returns Decorative window controls
313
+ */
314
+ function WindowDots(): React.ReactElement {
315
+ return (
316
+ <div className="cp-window__dots" aria-hidden="true">
317
+ <span className="cp-window__dot cp-window__dot--close" />
318
+ <span className="cp-window__dot cp-window__dot--minimize" />
319
+ <span className="cp-window__dot cp-window__dot--maximize" />
320
+ </div>
321
+ )
322
+ }
323
+
324
+ /**
325
+ * Render compact browser navigation controls.
326
+ *
327
+ * @private
328
+ * @returns Decorative browser controls
329
+ */
330
+ function BrowserNav(): React.ReactElement {
331
+ return (
332
+ <div className="cp-browser__nav" aria-hidden="true">
333
+ <svg className="cp-browser__nav-icon" viewBox="0 0 16 16" fill="currentColor">
334
+ <path d="M10.5 3 5.5 8l5 5" stroke="currentColor" strokeWidth="1.5" fill="none" />
335
+ </svg>
336
+ <svg className="cp-browser__nav-icon" viewBox="0 0 16 16" fill="currentColor">
337
+ <path d="m5.5 3 5 5-5 5" stroke="currentColor" strokeWidth="1.5" fill="none" />
338
+ </svg>
339
+ <svg className="cp-browser__nav-icon" viewBox="0 0 16 16" fill="currentColor">
340
+ <path
341
+ d="M13 8A5 5 0 1 1 3 8a5 5 0 0 1 10 0Z"
342
+ stroke="currentColor"
343
+ strokeWidth="1.5"
344
+ fill="none"
345
+ />
346
+ </svg>
44
347
  </div>
45
348
  )
46
349
  }