Skip to content

Commit 522a1bb

Browse files
Merge pull request #3133 from OctopusDeploy/km/llm-review
2 parents 9fcff1a + 9c5b361 commit 522a1bb

19 files changed

Lines changed: 1507 additions & 154 deletions

File tree

astro.config.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import remarkHeading from 'remark-heading-id';
33
import { defineConfig } from 'astro/config';
44
import mdx from '@astrojs/mdx';
55
import { attributeMarkdown, wrapTables } from '/src/themes/octopus/utilities/custom-markdown.mjs';
6+
import llmMdEmitter from './src/integrations/llm-md-emitter.ts';
67

78
// https://astro.build/config
89
export default defineConfig({
910
site: 'https://octopus.com',
1011
integrations: [
11-
mdx()
12+
mdx(),
13+
llmMdEmitter()
1214
],
1315
markdown: {
1416
shikiConfig: {

dictionary-octopus.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ Inedo
215215
inetmgr
216216
inetsrv
217217
inkey
218+
inlinable
218219
INSTALLLOCATION
219220
internalcustomer
220221
ioutil
@@ -323,6 +324,7 @@ nfsadmin
323324
nlog
324325
nmap
325326
noconsolelogging
327+
nodir
326328
nologo
327329
nologs
328330
noninteractive
@@ -417,6 +419,7 @@ projecttriggers
417419
proxied
418420
proxying
419421
pscustomobject
422+
pubdate
420423
publicip
421424
pwsh
422425
pycryptodome
@@ -548,6 +551,7 @@ tfvars
548551
TFVC
549552
thepassword
550553
threadid
554+
timeframes
551555
timespan
552556
tlsv1
553557
tmpfs

public/docs/css/main.css

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1797,6 +1797,176 @@ li.has-children .sub-nav ul li:focus > a {
17971797
color: var(--color-menu-link-alt);
17981798
}
17991799

1800+
/* "Use Octopus docs with AI" dropdown */
1801+
.octo-copy-md {
1802+
margin-block-start: var(--block-gap);
1803+
}
1804+
1805+
.octo-copy-md__menu {
1806+
display: inline-block;
1807+
position: relative;
1808+
}
1809+
1810+
.octo-copy-md__trigger {
1811+
display: inline-flex;
1812+
align-items: center;
1813+
gap: 0.5rem;
1814+
padding: 0.5rem 0.875rem;
1815+
border: 1px solid var(--border-color-menu-open);
1816+
border-radius: 999px;
1817+
background-color: transparent;
1818+
color: var(--color-menu-link);
1819+
font: inherit;
1820+
cursor: pointer;
1821+
list-style: none;
1822+
text-decoration: none;
1823+
user-select: none;
1824+
transition:
1825+
border-color 200ms cubic-bezier(0.4, 0, 0.2, 1),
1826+
background-color 200ms cubic-bezier(0.4, 0, 0.2, 1),
1827+
color 200ms cubic-bezier(0.4, 0, 0.2, 1),
1828+
box-shadow 200ms cubic-bezier(0.4, 0, 0.2, 1);
1829+
}
1830+
1831+
.octo-copy-md__trigger > * {
1832+
text-decoration: none;
1833+
color: inherit;
1834+
}
1835+
1836+
.octo-copy-md__trigger::-webkit-details-marker,
1837+
.octo-copy-md__trigger::marker {
1838+
content: '';
1839+
display: none;
1840+
}
1841+
1842+
.octo-copy-md__trigger-icon,
1843+
.octo-copy-md__trigger-caret {
1844+
display: inline-flex;
1845+
align-items: center;
1846+
justify-content: center;
1847+
line-height: 1;
1848+
}
1849+
1850+
.octo-copy-md__trigger-icon::before {
1851+
content: '\f0eb'; /* fa-lightbulb */
1852+
font-family: fa-solid;
1853+
font-size: 0.95em;
1854+
line-height: 1;
1855+
color: var(--color-menu-link-alt);
1856+
transition: color 200ms cubic-bezier(0.4, 0, 0.2, 1);
1857+
}
1858+
1859+
.octo-copy-md__trigger-caret::before {
1860+
content: '\f078'; /* fa-chevron-down */
1861+
font-family: fa-solid;
1862+
font-size: 0.7em;
1863+
line-height: 1;
1864+
color: currentColor;
1865+
transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
1866+
}
1867+
1868+
.octo-copy-md__menu[open]
1869+
> .octo-copy-md__trigger
1870+
.octo-copy-md__trigger-caret::before {
1871+
transform: rotate(180deg);
1872+
}
1873+
1874+
.octo-copy-md__trigger:hover {
1875+
color: var(--color-menu-link-active);
1876+
border-color: var(--color-menu-link-alt);
1877+
}
1878+
1879+
.octo-copy-md__menu[open] > .octo-copy-md__trigger {
1880+
color: var(--color-menu-link-active);
1881+
border-color: var(--color-menu-link-alt);
1882+
background-color: var(--bg-color-menu-open);
1883+
box-shadow: 0 0.0625rem 0.25rem rgba(13, 128, 216, 0.08);
1884+
}
1885+
1886+
.octo-copy-md__trigger:focus-visible {
1887+
outline: 2px solid var(--color-menu-link-alt);
1888+
outline-offset: 2px;
1889+
}
1890+
1891+
.octo-copy-md .octo-copy-md__options {
1892+
position: absolute;
1893+
z-index: 10;
1894+
inset-block-start: calc(100% + 0.5rem);
1895+
inset-inline-start: 0;
1896+
margin: 0;
1897+
padding: 0.5rem;
1898+
list-style: none;
1899+
background-color: var(--bg-color-menu);
1900+
border: 1px solid var(--border-color-menu-open);
1901+
border-radius: 0.625rem;
1902+
min-width: 20rem;
1903+
box-sizing: border-box;
1904+
overflow: hidden;
1905+
box-shadow:
1906+
0 0.625rem 1.875rem rgba(15, 37, 53, 0.12),
1907+
0 0.125rem 0.375rem rgba(15, 37, 53, 0.06);
1908+
}
1909+
1910+
.octo-copy-md .octo-copy-md__options .octo-copy-md__option {
1911+
display: inline-flex;
1912+
align-items: center;
1913+
gap: 0.625rem;
1914+
padding: 0.625rem 0.75rem;
1915+
border: none;
1916+
border-radius: 0.375rem;
1917+
background: transparent;
1918+
color: var(--color-menu-link);
1919+
font: inherit;
1920+
text-align: start;
1921+
text-decoration: none;
1922+
cursor: pointer;
1923+
transition:
1924+
background-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
1925+
color 150ms cubic-bezier(0.4, 0, 0.2, 1);
1926+
}
1927+
1928+
.octo-copy-md .octo-copy-md__options .octo-copy-md__option:hover,
1929+
.octo-copy-md .octo-copy-md__options .octo-copy-md__option:focus-visible {
1930+
background-color: var(--bg-color-menu-open);
1931+
color: var(--color-menu-link-active);
1932+
outline: none;
1933+
}
1934+
1935+
.octo-copy-md__option::before {
1936+
font-family: fa-solid;
1937+
font-size: 0.95em;
1938+
width: 1.1em;
1939+
text-align: center;
1940+
color: var(--color-menu-link-alt);
1941+
flex-shrink: 0;
1942+
transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1);
1943+
}
1944+
1945+
.octo-copy-md__option--copy::before {
1946+
content: '\f0c5'; /* fa-copy */
1947+
}
1948+
1949+
.octo-copy-md__option--view::before {
1950+
content: '\f15c'; /* fa-file-lines */
1951+
}
1952+
1953+
.octo-copy-md__option--all::before {
1954+
content: '\f02d'; /* fa-book */
1955+
}
1956+
1957+
/* Visually hidden live region for copy-markdown.js status announcements. */
1958+
.octo-copy-md__sr-status {
1959+
position: absolute;
1960+
width: 1px;
1961+
height: 1px;
1962+
padding: 0;
1963+
margin: -1px;
1964+
overflow: hidden;
1965+
clip: rect(0, 0, 0, 0);
1966+
white-space: nowrap;
1967+
border: 0;
1968+
}
1969+
18001970
/* Video */
18011971

18021972
.yt-video {

public/docs/js/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import { addResizedEvent } from './modules/resizing.js';
77
import { addStickyNavigation } from './modules/nav-sticky.js';
88
import { mobileNav } from './modules/nav-mobile.js';
9+
import { copyMarkdownMenus } from './modules/copy-markdown.js';
910
import { setClickableBlocks } from './modules/click-blocks.js';
1011
import { setExternalLinkAttributes } from './modules/external-links.js';
1112
import { monitorInputType } from './modules/input-type.js';
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// @ts-check
2+
import { qs, qsa } from './query.js';
3+
4+
class CopyMarkdown {
5+
constructor(menu) {
6+
this.menu = menu;
7+
this.trigger = qs('[data-copy-md-trigger]', menu);
8+
this.liveRegion = qs('[data-copy-md-live]', menu);
9+
10+
this.addCopyHandlers();
11+
this.addListeners();
12+
}
13+
14+
announce(btn, message) {
15+
const labelEl = btn.querySelector('[data-copy-md-text]');
16+
if (labelEl) {
17+
const restoreLabel = btn.dataset.copyMdLabel ?? '';
18+
labelEl.textContent = message;
19+
setTimeout(() => {
20+
labelEl.textContent = restoreLabel;
21+
}, 2000);
22+
}
23+
if (this.liveRegion) {
24+
// Force a content change so AT re-announces a repeated message.
25+
this.liveRegion.textContent = '';
26+
setTimeout(() => {
27+
this.liveRegion.textContent = message;
28+
}, 50);
29+
}
30+
}
31+
32+
// navigator.clipboard requires a secure context; falls back to
33+
// execCommand for HTTP and older browsers.
34+
async writeToClipboard(text) {
35+
if (
36+
typeof navigator !== 'undefined' &&
37+
navigator.clipboard &&
38+
typeof navigator.clipboard.writeText === 'function' &&
39+
(typeof window === 'undefined' || window.isSecureContext !== false)
40+
) {
41+
try {
42+
await navigator.clipboard.writeText(text);
43+
return true;
44+
} catch (err) {
45+
console.warn('[copy-md] navigator.clipboard failed, falling back', err);
46+
}
47+
}
48+
49+
return this.execCommandCopyFallback(text);
50+
}
51+
52+
execCommandCopyFallback(text) {
53+
if (typeof document === 'undefined') return false;
54+
const ta = document.createElement('textarea');
55+
ta.value = text;
56+
ta.setAttribute('readonly', '');
57+
ta.style.position = 'fixed';
58+
ta.style.top = '0';
59+
ta.style.left = '0';
60+
ta.style.opacity = '0';
61+
ta.style.pointerEvents = 'none';
62+
document.body.appendChild(ta);
63+
ta.select();
64+
let ok = false;
65+
try {
66+
ok = document.execCommand('copy');
67+
} catch (err) {
68+
console.warn('[copy-md] execCommand fallback threw', err);
69+
ok = false;
70+
}
71+
document.body.removeChild(ta);
72+
return ok;
73+
}
74+
75+
async handleCopy(btn) {
76+
const url = btn.dataset.copyMdUrl;
77+
const success = btn.dataset.copyMdSuccess ?? '';
78+
const errorMsg = btn.dataset.copyMdError ?? 'Copy failed';
79+
if (!url) return;
80+
try {
81+
const res = await fetch(url);
82+
if (!res.ok) throw new Error('HTTP ' + res.status);
83+
const text = await res.text();
84+
const ok = await this.writeToClipboard(text);
85+
if (!ok) throw new Error('clipboard-write-failed');
86+
this.announce(btn, success);
87+
} catch (err) {
88+
console.error('[copy-md] failed:', err);
89+
this.announce(btn, errorMsg);
90+
}
91+
}
92+
93+
handleKeyboardNavigation(e) {
94+
if (!this.menu.open) return;
95+
96+
if (e.key === 'Escape') {
97+
e.preventDefault();
98+
this.menu.open = false;
99+
this.trigger.focus();
100+
}
101+
}
102+
103+
handleOutsideClick(e) {
104+
if (!this.menu.open) return;
105+
if (e.target instanceof Node && this.menu.contains(e.target)) return;
106+
this.menu.open = false;
107+
}
108+
109+
addCopyHandlers() {
110+
for (const btn of qsa('[data-copy-md-action="copy"]', this.menu)) {
111+
btn.addEventListener('click', () => this.handleCopy(btn));
112+
}
113+
}
114+
115+
addListeners() {
116+
this.menu.addEventListener('keydown', (e) =>
117+
this.handleKeyboardNavigation(e)
118+
);
119+
120+
document.addEventListener('click', (e) => this.handleOutsideClick(e));
121+
}
122+
}
123+
124+
const copyMarkdownMenus = Array.from(qsa('[data-copy-md-menu]')).map(
125+
(menu) => new CopyMarkdown(menu)
126+
);
127+
128+
export { copyMarkdownMenus };

public/docs/js/modules/nav-mobile.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,34 @@ class MobileNav {
77
this.mobileMenuWrapper = qs('[data-mobile-menu-wrapper]');
88
this.hamburgerIcon = qs('[data-hamburger-icon]');
99
this.mobileMenu = qs('[data-mobile-menu]');
10-
this.menuItems = qsa('.site-nav__list li');
10+
this.mobileMenuList = qs('[data-mobile-menu-list]');
11+
12+
this.populateMobileMenu();
13+
this.menuItems = qsa('[data-mobile-menu-list] li');
1114

1215
// Initially hide the menu
1316
this.mobileMenu.style.visibility = 'hidden';
1417

1518
this.addListeners();
1619
}
1720

21+
populateMobileMenu() {
22+
// Idempotent on hot-reload.
23+
if (this.mobileMenuList.children.length > 0) return;
24+
25+
const sourceList = document.querySelector('#site-nav .site-nav__list');
26+
if (!sourceList) {
27+
console.warn(
28+
'[nav-mobile] #site-nav not found; mobile drawer will be empty'
29+
);
30+
return;
31+
}
32+
33+
for (const child of sourceList.children) {
34+
this.mobileMenuList.appendChild(child.cloneNode(true));
35+
}
36+
}
37+
1838
toggleMobileMenu() {
1939
const isOpen = this.mobileMenuWrapper.classList.contains('is-active');
2040
if (isOpen) {

0 commit comments

Comments
 (0)