@moldea.ai/website-ui 1.6.0 → 1.7.0

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/README.md CHANGED
@@ -7,7 +7,7 @@ The package owns the reusable design tokens, global website primitives, interact
7
7
  ## Install after release
8
8
 
9
9
  ```bash
10
- pnpm add @moldea.ai/website-ui@1.6.0
10
+ pnpm add @moldea.ai/website-ui@1.7.0
11
11
  ```
12
12
 
13
13
  The package currently supports Astro `7.2.2` and Tailwind CSS `4.3.3` exactly. Import the shared stylesheet once from the website's global stylesheet:
@@ -25,7 +25,7 @@ Tailwind scans the shipped Astro components through the package stylesheet's exp
25
25
  - Use `HeroBackdrop` inside a `relative overflow-hidden` section, with foreground content positioned `relative`. It owns the static, theme-aware radial/grid background shared with the Skill website. Do not duplicate its gradients in page markup.
26
26
  - Section introductions may place the title on the left and a concise explanation on the right at `lg`, stacking on mobile. Alternate this with compact stacked introductions when the content warrants it; do not force every section into the same composition.
27
27
  - Demonstrations should show recognizable files, one visible connection or mismatch, and a concise outcome. Compose `FilePreview`, `ConnectionLabel`, `ResultSummary`, `StatusBadge`, `CodeBlock`, and `Dialog`. Keep the essential result visible without interaction; dialogs contain optional evidence. Use `Accordion` when several examples share a section.
28
- - Preserve semantic inline code for filenames, variables, operations, and package names using `inline-code`; use `InlineBrandText` for prose containing the product name. Keep code wrapping and interaction feedback in the shared policies below.
28
+ - Preserve semantic inline code for filenames, variables, operations, and package names using `inline-code`; use `InlineBrandText` for prose containing the product name. Breadcrumbs, documentation navigation, the site header, and local-search results apply the same product-name treatment to their string labels. Keep code wrapping and interaction feedback in the shared policies below.
29
29
  - Verify compositions at 320px through desktop, in both themes, with keyboard focus and reduced motion. Flatten unnecessary mobile panels and preserve legible filenames, labels, and status text. Use icons and explicit text together rather than relying on color alone.
30
30
 
31
31
  ## Surface-aware selection
@@ -75,6 +75,7 @@ Every component has a dedicated public subpath:
75
75
  - `@moldea.ai/website-ui/brand-logo`
76
76
  - `@moldea.ai/website-ui/breadcrumbs`
77
77
  - `@moldea.ai/website-ui/code-block`
78
+ - `@moldea.ai/website-ui/code-copy-controls`
78
79
  - `@moldea.ai/website-ui/connection-label`
79
80
  - `@moldea.ai/website-ui/documentation-shell`
80
81
  - `@moldea.ai/website-ui/dialog`
@@ -94,7 +95,7 @@ Every component has a dedicated public subpath:
94
95
  - `@moldea.ai/website-ui/theme-bootstrap`
95
96
  - `@moldea.ai/website-ui/theme-control`
96
97
 
97
- `ThemeBootstrap` belongs in the document head before rendered content. Pass the same app-owned storage key to `ThemeControl`. Mount `NavigationProgress` once near the start of the document body in websites that use Astro's `ClientRouter`; it reports client navigation preparation without taking ownership of the app's layout. `BrandLogo` receives app-owned asset paths and labels rather than embedding one site's identity. `LocalSearch` receives app-owned copy, routes, and the generated index URL.
98
+ `ThemeBootstrap` belongs in the document head before rendered content. Pass the same app-owned storage key to `ThemeControl`. Mount `NavigationProgress` once near the start of the document body in websites that use Astro's `ClientRouter`; it reports client navigation preparation without taking ownership of the app's layout. Mount `CodeCopyControls` once in the body to add copy actions to eligible code blocks on direct loads and client navigation. `BrandLogo` receives app-owned asset paths and labels rather than embedding one site's identity. `LocalSearch` receives app-owned copy, routes, and the generated index URL.
98
99
 
99
100
  `SiteHeader`, `SiteFooter`, and `DocumentationShell` own responsive structure while consumers retain navigation data, accessible labels, copy, branding, actions, and page content. `TabbedPanels` keeps every panel readable without JavaScript and adds WAI-ARIA tab behavior, including Arrow Left, Arrow Right, Home, and End, after enhancement. `StatusBadge` exposes semantic tones and border treatments without defining domain status mappings.
100
101
 
@@ -159,11 +160,11 @@ For a dialog heading, compose `ResultSummary` in Dialog's `heading` slot, set `a
159
160
 
160
161
  For other hand-authored inline tokens, use `<code class="inline-code">`. This shared class matches rendered Markdown's code background, padding, monospace weight, and theme colors. `FilePreview` uses it for paths while retaining filename-preserving truncation. It does not parse text or add code semantics to plain strings; consumers must mark filenames and variable tokens explicitly.
161
162
 
162
- The compiled `markdown` entry renders sanitized documents and fragments with stable headings, syntax highlighting, safe external links, base-aware internal links, and keyboard-scrollable tables. Raw HTML is disabled. The replay component accepts only the normalized contracts from `evaluation-replay-model`; semantic and qualification evidence conversion remains application-owned.
163
+ The compiled `markdown` entry renders sanitized documents and fragments with stable headings, syntax highlighting, safe external links, base-aware internal links, and keyboard-scrollable tables. Raw HTML is disabled. The replay component accepts only the normalized contracts from `evaluation-replay-model`, including one initial trial and up to three ordered confirmation trials; semantic and qualification evidence conversion remains application-owned.
163
164
 
164
165
  ### Code and text wrapping
165
166
 
166
- `CodeBlock` receives literal `source`, optional `language`, and `variant="panel"` (default) or `variant="plain"` for use inside another surface. It owns compact typography, syntax highlighting, keyboard scrolling, and light/dark presentation. Pass raw code rather than constructing a Markdown fence; embedded fences and HTML remain literal source. `renderCodeBlock` from the public `markdown` subpath exposes the same rendering for non-component consumers.
167
+ `CodeBlock` receives literal `source`, optional `language`, `copyable`, and `variant="panel"` (default) or `variant="plain"` for use inside another surface. It owns compact typography, syntax highlighting, keyboard scrolling, and light/dark presentation. Pass raw code rather than constructing a Markdown fence; embedded fences and HTML remain literal source. `renderCodeBlock` from the public `markdown` subpath exposes the same rendering for non-component consumers.
167
168
 
168
169
  ```astro
169
170
  ---
@@ -179,10 +180,19 @@ import CodeBlock from '@moldea.ai/website-ui/code-block';
179
180
  language="sh"
180
181
  variant="plain"
181
182
  />
183
+ <CodeBlock
184
+ source="Illustrative excerpt"
185
+ language="text"
186
+ copyable={false}
187
+ />
182
188
  ```
183
189
 
184
190
  Code preserves its source line breaks and scrolls horizontally when needed. This includes JSON, YAML, shell commands, diffs, and Markdown source. Unlabelled fences also preserve lines, since they may contain code or aligned file trees. Only fences explicitly labelled `text`, `txt`, or `plaintext` wrap long lines while retaining their original line breaks. Use these labels for prose, not as a shortcut to force code to fit.
185
191
 
192
+ With `CodeCopyControls` mounted, `CodeBlock` is copyable by default. Set `copyable={false}` for an illustrative or incomplete block that does not need a copy action. For rendered Markdown and hand-authored code, place `data-code-copy="false"` on the `pre` element or an owning container. An opted-out ancestor disables every code block inside it, while neighboring blocks remain eligible. Opted-out blocks have no toolbar or extra focus stop and remain selectable.
193
+
194
+ The control copies the complete displayed code text, including indentation and blank lines, only after a visitor activates its button. Success is announced after the browser accepts the write. When clipboard access is unavailable or denied, the code stays selectable and the control explains how to copy it manually. The button keeps the stable accessible name “Copy code.”
195
+
186
196
  Markdown and literal code renderers apply this policy through `styles.css`. Rendered code blocks are named, keyboard-focusable regions with visible focus indicators. Hand-authored `<pre>` elements use the shared `code-block` class, `tabindex="0"`, `role="region"`, and a descriptive `aria-label`; add `data-code-language="text"` only for plain text. Do not add page-wide wrapping overrides to code, diagnostics, or replay content.
187
197
 
188
198
  ### Optional detail dialogs
@@ -44,7 +44,7 @@ export interface IEvaluationReplayVerdictStep {
44
44
  readonly status: 'failed' | 'passed';
45
45
  }
46
46
  export interface IEvaluationReplayTrial {
47
- readonly confirmationIndex: 1 | 2 | null;
47
+ readonly confirmationIndex: 1 | 2 | 3 | null;
48
48
  readonly evaluatedAt: string;
49
49
  readonly id: string;
50
50
  readonly kind: 'confirmation' | 'initial';
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/evaluation-replay/types.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,qBAAqB,GAC/B,cAAc,GAAG,WAAW,GAAG,wBAAwB,GAAG,mBAAmB,CAAC;AAGhF,MAAM,MAAM,qBAAqB,GAC7B,4BAA4B,GAC5B,4BAA4B,GAC5B,4BAA4B,GAC5B,8BAA8B,CAAC;AAGnC,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,CAAC;CACzC;AAGD,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;CACtC;AAGD,MAAM,MAAM,sCAAsC,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;AACxF,MAAM,MAAM,mCAAmC,GAAG,MAAM,GAAG,SAAS,CAAC;AAGrE,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,mCAAmC,CAAC;CACpD;AAGD,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,6BAA6B,EAAE,CAAC;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAGD,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,MAAM,EAAE,SAAS;QACxB,QAAQ,CAAC,OAAO,EAAE,SAAS,gCAAgC,EAAE,CAAC;QAC9D,QAAQ,CAAC,MAAM,EAAE,sCAAsC,CAAC;QACxD,QAAQ,CAAC,IAAI,EAAE,SAAS,6BAA6B,EAAE,CAAC;KACzD,EAAE,CAAC;IACJ,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;CAC5B;AAGD,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,qBAAqB,EAAE,wBAAwB,GAAG,mBAAmB,CAAC,CAAC;IAC9F,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;CACtC;AAGD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,iBAAiB,EAAE,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,KAAK,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAGD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,SAAS,sBAAsB,EAAE,CAAC;CACpD"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/evaluation-replay/types.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,qBAAqB,GAC/B,cAAc,GAAG,WAAW,GAAG,wBAAwB,GAAG,mBAAmB,CAAC;AAGhF,MAAM,MAAM,qBAAqB,GAC7B,4BAA4B,GAC5B,4BAA4B,GAC5B,4BAA4B,GAC5B,8BAA8B,CAAC;AAGnC,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,CAAC;CACzC;AAGD,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;CACtC;AAGD,MAAM,MAAM,sCAAsC,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;AACxF,MAAM,MAAM,mCAAmC,GAAG,MAAM,GAAG,SAAS,CAAC;AAGrE,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,mCAAmC,CAAC;CACpD;AAGD,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,6BAA6B,EAAE,CAAC;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAGD,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,MAAM,EAAE,SAAS;QACxB,QAAQ,CAAC,OAAO,EAAE,SAAS,gCAAgC,EAAE,CAAC;QAC9D,QAAQ,CAAC,MAAM,EAAE,sCAAsC,CAAC;QACxD,QAAQ,CAAC,IAAI,EAAE,SAAS,6BAA6B,EAAE,CAAC;KACzD,EAAE,CAAC;IACJ,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;CAC5B;AAGD,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,qBAAqB,EAAE,wBAAwB,GAAG,mBAAmB,CAAC,CAAC;IAC9F,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;CACtC;AAGD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,iBAAiB,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAC7C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,KAAK,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAGD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,SAAS,sBAAsB,EAAE,CAAC;CACpD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moldea.ai/website-ui",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Shared Astro foundations for moldea public websites.",
5
5
  "homepage": "https://github.com/moldea-ai/packages/tree/main/projects/website-ui#readme",
6
6
  "bugs": {
@@ -65,6 +65,9 @@
65
65
  "./code-block": {
66
66
  "import": "./src/components/code-block/code-block.component.astro"
67
67
  },
68
+ "./code-copy-controls": {
69
+ "import": "./src/components/code-copy-controls/code-copy-controls.component.astro"
70
+ },
68
71
  "./connection-label": {
69
72
  "import": "./src/components/connection-label/connection-label.component.astro"
70
73
  },
@@ -3,6 +3,8 @@ import { ChevronRight } from '@lucide/astro';
3
3
 
4
4
  import { withBase } from '../../site/index.ts';
5
5
 
6
+ import InlineBrandText from '../inline-brand-text/inline-brand-text.component.astro';
7
+
6
8
  export interface IBreadcrumb {
7
9
  href?: string;
8
10
  label: string;
@@ -35,14 +37,20 @@ const { basePath = import.meta.env.BASE_URL, items } = Astro.props;
35
37
  class="plain-link"
36
38
  href={withBase(item.href, basePath)}
37
39
  >
38
- {item.label}
40
+ <InlineBrandText
41
+ text={item.label}
42
+ variant="compact"
43
+ />
39
44
  </a>
40
45
  ) : (
41
46
  <span
42
47
  class="min-w-0 font-medium text-foreground"
43
48
  aria-current="page"
44
49
  >
45
- {item.label}
50
+ <InlineBrandText
51
+ text={item.label}
52
+ variant="compact"
53
+ />
46
54
  </span>
47
55
  )}
48
56
  </li>
@@ -3,15 +3,18 @@ import { renderCodeBlock } from '@moldea.ai/website-ui/markdown';
3
3
 
4
4
  // highlighted source, with a panel by default or a flat surface for composition inside another panel
5
5
  export interface Props {
6
+ copyable?: boolean;
6
7
  source: string;
7
8
  language?: string;
8
9
  variant?: 'panel' | 'plain';
9
10
  }
10
- const { source, language, variant = 'panel' } = Astro.props;
11
+ const { copyable = true, source, language, variant = 'panel' } = Astro.props;
11
12
  const html = await renderCodeBlock(source, language);
12
13
  ---
13
14
 
14
15
  <div
16
+ data-code-block
17
+ data-code-copy={copyable ? undefined : 'false'}
15
18
  class:list={[
16
19
  'prose-moldea min-w-0 text-sm [&_pre]:m-0 [&_pre]:text-xs [&_pre]:leading-6',
17
20
  variant === 'panel'
@@ -0,0 +1,111 @@
1
+ ---
2
+ import { Copy } from '@lucide/astro';
3
+
4
+ import ActionButton from '../action-button/action-button.component.astro';
5
+ ---
6
+
7
+ <template data-code-copy-controls-template>
8
+ <div
9
+ class="code-copy-toolbar flex min-h-8 min-w-0 flex-wrap items-center justify-end gap-x-2 gap-y-1"
10
+ data-code-copy-toolbar
11
+ >
12
+ <span
13
+ class="min-w-0 text-xs text-muted-foreground empty:hidden"
14
+ aria-live="polite"
15
+ aria-atomic="true"
16
+ data-code-copy-feedback
17
+ ></span>
18
+ <ActionButton
19
+ class="ms-auto"
20
+ size="sm"
21
+ variant="ghost"
22
+ aria-label="Copy code"
23
+ data-code-copy-button
24
+ >
25
+ <Copy
26
+ class="size-3.5"
27
+ aria-hidden="true"
28
+ />
29
+ <span aria-hidden="true">Copy</span>
30
+ </ActionButton>
31
+ </div>
32
+ </template>
33
+
34
+ <script>
35
+ const OPT_OUT_SELECTOR = '[data-code-copy="false"]';
36
+
37
+ /** Reports clipboard results beside the control without changing its accessible name. */
38
+ const setCopyFeedback = (feedback: HTMLElement, message: string): void => {
39
+ feedback.textContent = message;
40
+ };
41
+
42
+ /** Copies one block after an explicit button activation and reports the resolved outcome. */
43
+ const initializeCopyButton = (
44
+ button: HTMLButtonElement,
45
+ feedback: HTMLElement,
46
+ code: HTMLElement,
47
+ ): void => {
48
+ let latestRequest: symbol | undefined;
49
+
50
+ button.addEventListener('click', async () => {
51
+ const request = Symbol();
52
+ latestRequest = request;
53
+ setCopyFeedback(feedback, '');
54
+
55
+ const writeText = navigator.clipboard?.writeText;
56
+
57
+ if (writeText === undefined) {
58
+ setCopyFeedback(feedback, 'Copy is unavailable. Select the code, then copy it manually.');
59
+ return;
60
+ }
61
+
62
+ try {
63
+ await writeText.call(navigator.clipboard, code.textContent ?? '');
64
+
65
+ if (latestRequest === request && button.isConnected) {
66
+ setCopyFeedback(feedback, 'Copied.');
67
+ }
68
+ } catch {
69
+ if (latestRequest === request && button.isConnected) {
70
+ setCopyFeedback(feedback, 'Copy failed. Select the code, then copy it manually.');
71
+ }
72
+ }
73
+ });
74
+ };
75
+
76
+ /** Adds one copy toolbar to every eligible code block in the current document. */
77
+ const initializeCodeCopyControls = (): void => {
78
+ const template = document.querySelector<HTMLTemplateElement>(
79
+ '[data-code-copy-controls-template]',
80
+ );
81
+
82
+ if (template === null) return;
83
+
84
+ for (const pre of document.querySelectorAll<HTMLPreElement>('pre')) {
85
+ const code = pre.firstElementChild;
86
+
87
+ if (
88
+ !(code instanceof HTMLElement) ||
89
+ code.tagName !== 'CODE' ||
90
+ pre.dataset.codeCopyEnhanced === 'true' ||
91
+ pre.closest(OPT_OUT_SELECTOR) !== null
92
+ ) {
93
+ continue;
94
+ }
95
+
96
+ const fragment = template.content.cloneNode(true) as DocumentFragment;
97
+ const toolbar = fragment.querySelector<HTMLElement>('[data-code-copy-toolbar]');
98
+ const button = fragment.querySelector<HTMLButtonElement>('[data-code-copy-button]');
99
+ const feedback = fragment.querySelector<HTMLElement>('[data-code-copy-feedback]');
100
+
101
+ if (toolbar === null || button === null || feedback === null) continue;
102
+
103
+ initializeCopyButton(button, feedback, code);
104
+ pre.dataset.codeCopyEnhanced = 'true';
105
+ pre.before(toolbar);
106
+ }
107
+ };
108
+
109
+ document.addEventListener('astro:page-load', initializeCodeCopyControls);
110
+ initializeCodeCopyControls();
111
+ </script>
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import type { IDocumentationOutlineHeading } from './documentation-shell.component.astro';
3
+ import InlineBrandText from '../inline-brand-text/inline-brand-text.component.astro';
3
4
 
4
5
  interface Props {
5
6
  headings: readonly IDocumentationOutlineHeading[];
@@ -14,7 +15,10 @@ const { headings } = Astro.props;
14
15
  class:list={['plain-link text-sm leading-5', heading.depth === 3 && 'ps-3']}
15
16
  href={`#${heading.id}`}
16
17
  >
17
- {heading.text}
18
+ <InlineBrandText
19
+ text={heading.text}
20
+ variant="compact"
21
+ />
18
22
  </a>
19
23
  ))
20
24
  }
@@ -3,6 +3,7 @@ import { ArrowLeft, ArrowRight, ChevronDown, Menu } from '@lucide/astro';
3
3
 
4
4
  import { withBase } from '../../site/index.ts';
5
5
  import Breadcrumbs, { type IBreadcrumb } from '../breadcrumbs/breadcrumbs.component.astro';
6
+ import InlineBrandText from '../inline-brand-text/inline-brand-text.component.astro';
6
7
 
7
8
  import DocumentationOutline from './documentation-outline.component.astro';
8
9
 
@@ -77,7 +78,10 @@ const contentGridClass = hasNavigation
77
78
  class="size-4"
78
79
  aria-hidden="true"
79
80
  />
80
- {mobileNavigationLabel}
81
+ <InlineBrandText
82
+ text={mobileNavigationLabel}
83
+ variant="compact"
84
+ />
81
85
  </summary>
82
86
  <nav
83
87
  class="grid gap-5 border-t border-border p-4"
@@ -85,7 +89,14 @@ const contentGridClass = hasNavigation
85
89
  >
86
90
  {navigationGroups.map((group) => (
87
91
  <div>
88
- {group.label && <p class="eyebrow mb-2 px-2">{group.label}</p>}
92
+ {group.label && (
93
+ <p class="eyebrow mb-2 px-2">
94
+ <InlineBrandText
95
+ text={group.label}
96
+ variant="compact"
97
+ />
98
+ </p>
99
+ )}
89
100
  <div class="grid gap-1">
90
101
  {group.items.map((item) => (
91
102
  <a
@@ -93,7 +104,10 @@ const contentGridClass = hasNavigation
93
104
  href={withBase(item.href, basePath)}
94
105
  aria-current={item.href === currentRoute ? 'page' : undefined}
95
106
  >
96
- {item.label}
107
+ <InlineBrandText
108
+ text={item.label}
109
+ variant="compact"
110
+ />
97
111
  </a>
98
112
  ))}
99
113
  </div>
@@ -108,7 +122,10 @@ const contentGridClass = hasNavigation
108
122
  hasOutline && (
109
123
  <details class="surface-card group sticky top-16 z-40 mt-3 xl:hidden">
110
124
  <summary class="flex min-h-12 cursor-pointer list-none items-center justify-between gap-3 px-4 font-semibold outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset [&::-webkit-details-marker]:hidden">
111
- {outlineLabel}
125
+ <InlineBrandText
126
+ text={outlineLabel}
127
+ variant="compact"
128
+ />
112
129
  <ChevronDown
113
130
  class="size-4"
114
131
  aria-hidden="true"
@@ -135,7 +152,14 @@ const contentGridClass = hasNavigation
135
152
  >
136
153
  {navigationGroups.map((group) => (
137
154
  <div>
138
- {group.label && <p class="eyebrow mb-2 px-2">{group.label}</p>}
155
+ {group.label && (
156
+ <p class="eyebrow mb-2 px-2">
157
+ <InlineBrandText
158
+ text={group.label}
159
+ variant="compact"
160
+ />
161
+ </p>
162
+ )}
139
163
  <div class="grid gap-1">
140
164
  {group.items.map((item) => (
141
165
  <a
@@ -143,7 +167,10 @@ const contentGridClass = hasNavigation
143
167
  href={withBase(item.href, basePath)}
144
168
  aria-current={item.href === currentRoute ? 'page' : undefined}
145
169
  >
146
- {item.label}
170
+ <InlineBrandText
171
+ text={item.label}
172
+ variant="compact"
173
+ />
147
174
  </a>
148
175
  ))}
149
176
  </div>
@@ -178,7 +205,12 @@ const contentGridClass = hasNavigation
178
205
  <span class="block text-xs font-bold tracking-wide text-muted-foreground uppercase">
179
206
  Previous
180
207
  </span>
181
- <span class="mt-1 block font-semibold">{previousPage.label}</span>
208
+ <span class="mt-1 block font-semibold">
209
+ <InlineBrandText
210
+ text={previousPage.label}
211
+ variant="compact"
212
+ />
213
+ </span>
182
214
  </span>
183
215
  </a>
184
216
  ) : (
@@ -193,7 +225,12 @@ const contentGridClass = hasNavigation
193
225
  <span class="block text-xs font-bold tracking-wide text-muted-foreground uppercase">
194
226
  Next
195
227
  </span>
196
- <span class="mt-1 block font-semibold">{nextPage.label}</span>
228
+ <span class="mt-1 block font-semibold">
229
+ <InlineBrandText
230
+ text={nextPage.label}
231
+ variant="compact"
232
+ />
233
+ </span>
197
234
  </span>
198
235
  <ArrowRight
199
236
  class="size-4 shrink-0"
@@ -210,7 +247,12 @@ const contentGridClass = hasNavigation
210
247
  (hasOutline || hasNavigation) && (
211
248
  <aside class="hidden xl:block">
212
249
  <div class="sticky top-24 max-h-[calc(100vh-7rem)] overflow-y-auto pe-2">
213
- <p class="eyebrow mb-4">{outlineLabel}</p>
250
+ <p class="eyebrow mb-4">
251
+ <InlineBrandText
252
+ text={outlineLabel}
253
+ variant="compact"
254
+ />
255
+ </p>
214
256
  {hasOutline ? (
215
257
  <nav
216
258
  class="grid gap-2 border-s ps-4"
@@ -219,7 +261,9 @@ const contentGridClass = hasNavigation
219
261
  <DocumentationOutline headings={headings} />
220
262
  </nav>
221
263
  ) : (
222
- <p class="text-sm text-muted-foreground">{outlineEmptyLabel}</p>
264
+ <p class="text-sm text-muted-foreground">
265
+ <InlineBrandText text={outlineEmptyLabel} />
266
+ </p>
223
267
  )}
224
268
  </div>
225
269
  </aside>
@@ -2,6 +2,7 @@
2
2
  import { Search } from '@lucide/astro';
3
3
 
4
4
  import ActionButton from '../action-button/action-button.component.astro';
5
+ import InlineBrandText from '../inline-brand-text/inline-brand-text.component.astro';
5
6
 
6
7
  export interface Props {
7
8
  action: string;
@@ -74,7 +75,10 @@ const {
74
75
  size="lg"
75
76
  type="submit"
76
77
  >
77
- {buttonLabel}
78
+ <InlineBrandText
79
+ text={buttonLabel}
80
+ variant="compact"
81
+ />
78
82
  </ActionButton>
79
83
  </div>
80
84
  </form>
@@ -84,7 +88,7 @@ const {
84
88
  aria-live="polite"
85
89
  data-search-status
86
90
  >
87
- {initialPrompt}
91
+ <InlineBrandText text={initialPrompt} />
88
92
  </p>
89
93
  <ol
90
94
  class="mt-8 grid gap-4"
@@ -100,6 +104,24 @@ const {
100
104
  type ISearchDocument,
101
105
  } from '../../search/index.ts';
102
106
 
107
+ /** Replaces text with escaped text and semantic code nodes for every product-name match. */
108
+ const renderInlineBrandText = (element: HTMLElement, text: string): void => {
109
+ const content = document.createDocumentFragment();
110
+
111
+ for (const segment of text.split(/(\bmoldea\b)/giu)) {
112
+ if (/^moldea$/iu.test(segment)) {
113
+ const code = document.createElement('code');
114
+ code.className = 'inline-code tracking-normal';
115
+ code.textContent = 'moldea';
116
+ content.append(code);
117
+ } else {
118
+ content.append(document.createTextNode(segment));
119
+ }
120
+ }
121
+
122
+ element.replaceChildren(content);
123
+ };
124
+
103
125
  const createSearchResult = (searchDocument: ISearchDocument): HTMLLIElement => {
104
126
  const item = document.createElement('li');
105
127
  const link = document.createElement('a');
@@ -110,9 +132,9 @@ const {
110
132
  link.className = 'block rounded-md';
111
133
  link.href = searchDocument.url;
112
134
  title.className = 'text-xl font-semibold tracking-tight';
113
- title.textContent = searchDocument.title;
135
+ renderInlineBrandText(title, searchDocument.title);
114
136
  excerpt.className = 'mt-2 leading-6 text-muted-foreground';
115
- excerpt.textContent = searchDocument.description;
137
+ renderInlineBrandText(excerpt, searchDocument.description);
116
138
  link.append(title, excerpt);
117
139
  item.append(link);
118
140
 
@@ -157,11 +179,11 @@ const {
157
179
  results.replaceChildren();
158
180
 
159
181
  if (!query.trim()) {
160
- status.textContent = root.dataset.initialPrompt ?? '';
182
+ renderInlineBrandText(status, root.dataset.initialPrompt ?? '');
161
183
  return;
162
184
  }
163
185
 
164
- status.textContent = root.dataset.loadingMessage ?? '';
186
+ renderInlineBrandText(status, root.dataset.loadingMessage ?? '');
165
187
 
166
188
  const pendingDocuments = (documentsPromise ??= loadSearchDocuments(searchIndexUrl));
167
189
  const documents = await pendingDocuments;
@@ -170,15 +192,18 @@ const {
170
192
  if (latestRequest !== request || !root.isConnected) return;
171
193
 
172
194
  if (documents === null) {
173
- status.textContent = root.dataset.failureMessage ?? '';
195
+ renderInlineBrandText(status, root.dataset.failureMessage ?? '');
174
196
  return;
175
197
  }
176
198
 
177
199
  const matches = searchDocuments(query, documents);
178
200
  results.replaceChildren(...matches.map(createSearchResult));
179
- status.textContent = matches.length
180
- ? `${matches.length} result${matches.length === 1 ? '' : 's'} for “${query}”.`
181
- : (root.dataset.noResultsMessage ?? '');
201
+ renderInlineBrandText(
202
+ status,
203
+ matches.length
204
+ ? `${matches.length} result${matches.length === 1 ? '' : 's'} for “${query}”.`
205
+ : (root.dataset.noResultsMessage ?? ''),
206
+ );
182
207
  };
183
208
 
184
209
  form.addEventListener('submit', (event) => {
@@ -2,6 +2,7 @@
2
2
  import { GitFork, Menu, Search } from '@lucide/astro';
3
3
 
4
4
  import ActionLink from '../action-link/action-link.component.astro';
5
+ import InlineBrandText from '../inline-brand-text/inline-brand-text.component.astro';
5
6
  import ThemeControl from '../theme-control/theme-control.component.astro';
6
7
 
7
8
  export interface ISiteHeaderNavigationItem {
@@ -77,17 +78,26 @@ const responsiveClasses = breakpointClasses[breakpoint];
77
78
  class="xl:hidden"
78
79
  aria-hidden="true"
79
80
  >
80
- {compactLabel}
81
+ <InlineBrandText
82
+ text={compactLabel}
83
+ variant="compact"
84
+ />
81
85
  </span>
82
86
  <span
83
87
  class="hidden xl:inline"
84
88
  aria-hidden="true"
85
89
  >
86
- {label}
90
+ <InlineBrandText
91
+ text={label}
92
+ variant="compact"
93
+ />
87
94
  </span>
88
95
  </>
89
96
  ) : (
90
- label
97
+ <InlineBrandText
98
+ text={label}
99
+ variant="compact"
100
+ />
91
101
  )}
92
102
  </a>
93
103
  ))
@@ -150,7 +160,10 @@ const responsiveClasses = breakpointClasses[breakpoint];
150
160
  href={href}
151
161
  aria-current={isActive ? 'page' : undefined}
152
162
  >
153
- {label}
163
+ <InlineBrandText
164
+ text={label}
165
+ variant="compact"
166
+ />
154
167
  </a>
155
168
  ))
156
169
  }
@@ -167,7 +180,10 @@ const responsiveClasses = breakpointClasses[breakpoint];
167
180
  target="_blank"
168
181
  rel="noopener noreferrer"
169
182
  >
170
- {sourceLabel}
183
+ <InlineBrandText
184
+ text={sourceLabel}
185
+ variant="compact"
186
+ />
171
187
  </a>
172
188
  <slot name="mobile-actions" />
173
189
  </nav>
package/src/styles.css CHANGED
@@ -202,6 +202,22 @@
202
202
  @apply max-w-full overflow-x-auto rounded-xl border bg-code p-5 text-sm leading-6 text-code-foreground shadow-inset;
203
203
  }
204
204
 
205
+ .code-copy-toolbar {
206
+ @apply mb-2;
207
+ }
208
+
209
+ .prose-moldea .code-copy-toolbar {
210
+ @apply mt-5;
211
+ }
212
+
213
+ .prose-moldea[data-code-block] .code-copy-toolbar {
214
+ @apply mt-0;
215
+ }
216
+
217
+ .code-copy-toolbar + pre {
218
+ margin-top: 0;
219
+ }
220
+
205
221
  /* Preserve code, structured output, and file-tree alignment by default. */
206
222
  .code-block,
207
223
  .prose-moldea pre {