Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import remarkHeading from 'remark-heading-id';
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import { attributeMarkdown, wrapTables } from '/src/themes/octopus/utilities/custom-markdown.mjs';
import llmMdEmitter from './src/integrations/llm-md-emitter.ts';

// https://astro.build/config
export default defineConfig({
site: 'https://octopus.com',
integrations: [
mdx()
mdx(),
llmMdEmitter()
],
markdown: {
shikiConfig: {
Expand Down
4 changes: 4 additions & 0 deletions dictionary-octopus.txt
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ Inedo
inetmgr
inetsrv
inkey
inlinable
INSTALLLOCATION
internalcustomer
ioutil
Expand Down Expand Up @@ -323,6 +324,7 @@ nfsadmin
nlog
nmap
noconsolelogging
nodir
nologo
nologs
noninteractive
Expand Down Expand Up @@ -417,6 +419,7 @@ projecttriggers
proxied
proxying
pscustomobject
pubdate
publicip
pwsh
pycryptodome
Expand Down Expand Up @@ -548,6 +551,7 @@ tfvars
TFVC
thepassword
threadid
timeframes
timespan
tlsv1
tmpfs
Expand Down
170 changes: 170 additions & 0 deletions public/docs/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -1797,6 +1797,176 @@ li.has-children .sub-nav ul li:focus > a {
color: var(--color-menu-link-alt);
}

/* "Use Octopus docs with AI" dropdown */
.octo-copy-md {
margin-block-start: var(--block-gap);
}

.octo-copy-md__menu {
display: inline-block;
position: relative;
}

.octo-copy-md__trigger {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.875rem;
border: 1px solid var(--border-color-menu-open);
border-radius: 999px;
background-color: transparent;
color: var(--color-menu-link);
font: inherit;
cursor: pointer;
list-style: none;
text-decoration: none;
user-select: none;
transition:
border-color 200ms cubic-bezier(0.4, 0, 0.2, 1),
background-color 200ms cubic-bezier(0.4, 0, 0.2, 1),
color 200ms cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 200ms cubic-bezier(0.4, 0, 0.2, 1);
}

.octo-copy-md__trigger > * {
text-decoration: none;
color: inherit;
}

.octo-copy-md__trigger::-webkit-details-marker,
.octo-copy-md__trigger::marker {
content: '';
display: none;
}

.octo-copy-md__trigger-icon,
.octo-copy-md__trigger-caret {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
}

.octo-copy-md__trigger-icon::before {
content: '\f0eb'; /* fa-lightbulb */
font-family: fa-solid;
font-size: 0.95em;
line-height: 1;
color: var(--color-menu-link-alt);
transition: color 200ms cubic-bezier(0.4, 0, 0.2, 1);
}

.octo-copy-md__trigger-caret::before {
content: '\f078'; /* fa-chevron-down */
font-family: fa-solid;
font-size: 0.7em;
line-height: 1;
color: currentColor;
transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
}

.octo-copy-md__menu[open]
> .octo-copy-md__trigger
.octo-copy-md__trigger-caret::before {
transform: rotate(180deg);
}

.octo-copy-md__trigger:hover {
color: var(--color-menu-link-active);
border-color: var(--color-menu-link-alt);
}

.octo-copy-md__menu[open] > .octo-copy-md__trigger {
color: var(--color-menu-link-active);
border-color: var(--color-menu-link-alt);
background-color: var(--bg-color-menu-open);
box-shadow: 0 0.0625rem 0.25rem rgba(13, 128, 216, 0.08);
}

.octo-copy-md__trigger:focus-visible {
outline: 2px solid var(--color-menu-link-alt);
outline-offset: 2px;
}

.octo-copy-md .octo-copy-md__options {
position: absolute;
z-index: 10;
inset-block-start: calc(100% + 0.5rem);
inset-inline-start: 0;
margin: 0;
padding: 0.5rem;
list-style: none;
background-color: var(--bg-color-menu);
border: 1px solid var(--border-color-menu-open);
border-radius: 0.625rem;
min-width: 20rem;
box-sizing: border-box;
overflow: hidden;
box-shadow:
0 0.625rem 1.875rem rgba(15, 37, 53, 0.12),
0 0.125rem 0.375rem rgba(15, 37, 53, 0.06);
}

.octo-copy-md .octo-copy-md__options .octo-copy-md__option {
display: inline-flex;
align-items: center;
gap: 0.625rem;
padding: 0.625rem 0.75rem;
border: none;
border-radius: 0.375rem;
background: transparent;
color: var(--color-menu-link);
font: inherit;
text-align: start;
text-decoration: none;
cursor: pointer;
transition:
background-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
color 150ms cubic-bezier(0.4, 0, 0.2, 1);
}

.octo-copy-md .octo-copy-md__options .octo-copy-md__option:hover,
.octo-copy-md .octo-copy-md__options .octo-copy-md__option:focus-visible {
background-color: var(--bg-color-menu-open);
color: var(--color-menu-link-active);
outline: none;
}

.octo-copy-md__option::before {
font-family: fa-solid;
font-size: 0.95em;
width: 1.1em;
text-align: center;
color: var(--color-menu-link-alt);
flex-shrink: 0;
transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1);
}

.octo-copy-md__option--copy::before {
content: '\f0c5'; /* fa-copy */
}

.octo-copy-md__option--view::before {
content: '\f15c'; /* fa-file-lines */
}

.octo-copy-md__option--all::before {
content: '\f02d'; /* fa-book */
}

/* Visually hidden live region for copy-markdown.js status announcements. */
.octo-copy-md__sr-status {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

/* Video */

.yt-video {
Expand Down
1 change: 1 addition & 0 deletions public/docs/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import { addResizedEvent } from './modules/resizing.js';
import { addStickyNavigation } from './modules/nav-sticky.js';
import { mobileNav } from './modules/nav-mobile.js';
import { copyMarkdownMenus } from './modules/copy-markdown.js';
import { setClickableBlocks } from './modules/click-blocks.js';
import { setExternalLinkAttributes } from './modules/external-links.js';
import { monitorInputType } from './modules/input-type.js';
Expand Down
128 changes: 128 additions & 0 deletions public/docs/js/modules/copy-markdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// @ts-check
import { qs, qsa } from './query.js';

class CopyMarkdown {
constructor(menu) {
this.menu = menu;
this.trigger = qs('[data-copy-md-trigger]', menu);
this.liveRegion = qs('[data-copy-md-live]', menu);

this.addCopyHandlers();
this.addListeners();
}

announce(btn, message) {
const labelEl = btn.querySelector('[data-copy-md-text]');
if (labelEl) {
const restoreLabel = btn.dataset.copyMdLabel ?? '';
labelEl.textContent = message;
setTimeout(() => {
labelEl.textContent = restoreLabel;
}, 2000);
}
if (this.liveRegion) {
// Force a content change so AT re-announces a repeated message.
this.liveRegion.textContent = '';
setTimeout(() => {
this.liveRegion.textContent = message;
}, 50);
}
}

// navigator.clipboard requires a secure context; falls back to
// execCommand for HTTP and older browsers.
async writeToClipboard(text) {
if (
typeof navigator !== 'undefined' &&
navigator.clipboard &&
typeof navigator.clipboard.writeText === 'function' &&
(typeof window === 'undefined' || window.isSecureContext !== false)
) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.warn('[copy-md] navigator.clipboard failed, falling back', err);
}
}

return this.execCommandCopyFallback(text);
}

execCommandCopyFallback(text) {
if (typeof document === 'undefined') return false;
const ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', '');
ta.style.position = 'fixed';
ta.style.top = '0';
ta.style.left = '0';
ta.style.opacity = '0';
ta.style.pointerEvents = 'none';
document.body.appendChild(ta);
ta.select();
let ok = false;
try {
ok = document.execCommand('copy');
} catch (err) {
console.warn('[copy-md] execCommand fallback threw', err);
ok = false;
}
document.body.removeChild(ta);
return ok;
}

async handleCopy(btn) {
const url = btn.dataset.copyMdUrl;
const success = btn.dataset.copyMdSuccess ?? '';
const errorMsg = btn.dataset.copyMdError ?? 'Copy failed';
if (!url) return;
try {
const res = await fetch(url);
if (!res.ok) throw new Error('HTTP ' + res.status);
const text = await res.text();
const ok = await this.writeToClipboard(text);
if (!ok) throw new Error('clipboard-write-failed');
this.announce(btn, success);
} catch (err) {
console.error('[copy-md] failed:', err);
this.announce(btn, errorMsg);
}
}

handleKeyboardNavigation(e) {
if (!this.menu.open) return;

if (e.key === 'Escape') {
e.preventDefault();
this.menu.open = false;
this.trigger.focus();
}
}

handleOutsideClick(e) {
if (!this.menu.open) return;
if (e.target instanceof Node && this.menu.contains(e.target)) return;
this.menu.open = false;
}

addCopyHandlers() {
for (const btn of qsa('[data-copy-md-action="copy"]', this.menu)) {
btn.addEventListener('click', () => this.handleCopy(btn));
}
}

addListeners() {
this.menu.addEventListener('keydown', (e) =>
this.handleKeyboardNavigation(e)
);

document.addEventListener('click', (e) => this.handleOutsideClick(e));
}
}

const copyMarkdownMenus = Array.from(qsa('[data-copy-md-menu]')).map(
(menu) => new CopyMarkdown(menu)
);

export { copyMarkdownMenus };
22 changes: 21 additions & 1 deletion public/docs/js/modules/nav-mobile.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,34 @@ class MobileNav {
this.mobileMenuWrapper = qs('[data-mobile-menu-wrapper]');
this.hamburgerIcon = qs('[data-hamburger-icon]');
this.mobileMenu = qs('[data-mobile-menu]');
this.menuItems = qsa('.site-nav__list li');
this.mobileMenuList = qs('[data-mobile-menu-list]');

this.populateMobileMenu();
this.menuItems = qsa('[data-mobile-menu-list] li');

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

this.addListeners();
}

populateMobileMenu() {
// Idempotent on hot-reload.
if (this.mobileMenuList.children.length > 0) return;

const sourceList = document.querySelector('#site-nav .site-nav__list');
if (!sourceList) {
console.warn(
'[nav-mobile] #site-nav not found; mobile drawer will be empty'
);
return;
}

for (const child of sourceList.children) {
this.mobileMenuList.appendChild(child.cloneNode(true));
}
}

toggleMobileMenu() {
const isOpen = this.mobileMenuWrapper.classList.contains('is-active');
if (isOpen) {
Expand Down
Loading
Loading