@@ -430,6 +430,27 @@ figcaption { font-size: 0.875em; line-height: 1.4285714; color: var(--text-prima
430430 border-radius: 0.5rem;
431431}
432432.art-search::placeholder { color: var(--text-muted); }
433+ /* Top-of-page controls: Copy page + prev/next chevrons on the title row. */
434+ .page-actions {
435+ display: flex; justify-content: flex-end; align-items: center; gap: 2px;
436+ margin-bottom: -2.4rem; position: relative; z-index: 2;
437+ }
438+ .pa-copy {
439+ display: inline-flex; align-items: center; gap: 6px;
440+ font: inherit; font-size: var(--text-sm); color: var(--text-body);
441+ background: none; border: none; border-radius: 0.5rem;
442+ padding: 6px 8px; cursor: pointer; transition: background-color 0.15s;
443+ }
444+ .pa-copy:hover { background: var(--surface-active); }
445+ .pa-copy svg { color: var(--text-icon); }
446+ .pa-nav {
447+ display: inline-flex; align-items: center; justify-content: center;
448+ width: 30px; height: 30px; border-radius: 0.5rem;
449+ color: var(--text-icon); transition: background-color 0.15s;
450+ }
451+ .pa-nav:hover { background: var(--surface-active); opacity: 1; }
452+ .pa-nav.is-disabled { opacity: 0.35; pointer-events: none; }
453+
433454/* The docs' theme toggle: 30px, rounded-lg, --text-icon, --surface-active hover. */
434455.art-theme {
435456 display: inline-flex; align-items: center; justify-content: center;
@@ -611,6 +632,7 @@ export const SIM_ARTIFACT_SHELL = `<script>
611632 if (headings.length === 0) return
612633 page.dataset.shell = 'ready'
613634
635+ const ICON_ATTRS = 'viewBox="-1 -2 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.55" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"'
614636 const slug = (text, i) =>
615637 (text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'section') + '-' + i
616638 headings.forEach((h, i) => { if (!h.id) h.id = slug(h.textContent || '', i) })
@@ -693,7 +715,6 @@ export const SIM_ARTIFACT_SHELL = `<script>
693715 // stylesheet block, so flipping the attribute is the whole mechanism.
694716 // Storage is unavailable in the sandboxed preview, so the choice lives
695717 // for the page view.
696- const ICON_ATTRS = 'viewBox="-1 -2 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.55" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"'
697718 const SUN = '<svg ' + ICON_ATTRS + '><circle cx="10.25" cy="9.75" r="4"/><path d="M10.25 3.75V1.25M16.25 9.75H18.75M10.25 15.75V18.25M4.25 9.75H1.75"/><path d="M14.49 5.51L16.26 3.74M14.49 13.99L16.26 15.76M6.01 13.99L4.24 15.76M6.01 5.51L4.24 3.74"/></svg>'
698719 const MOON = '<svg ' + ICON_ATTRS + '><path d="M10.25 1.5A5.84 5.84 0 0 0 18.5 9.75A8.25 8.25 0 1 1 10.25 1.5Z"/></svg>'
699720 const themeButton = document.createElement('button')
@@ -761,6 +782,45 @@ export const SIM_ARTIFACT_SHELL = `<script>
761782 }
762783 cols.append(left, mid, right)
763784
785+ // Top-of-page controls, like the docs: Copy page plus prev/next
786+ // chevrons on the title row, wired to the same targets as the footer.
787+ const DUPLICATE_ICON = '<svg ' + ICON_ATTRS + '><path d="M14.25 0.75H2.75C1.64543 0.75 0.75 1.64543 0.75 2.75V14.25"/><rect x="5.25" y="5.25" width="14" height="14" rx="2"/></svg>'
788+ const CHEV_L = '<svg ' + ICON_ATTRS + '><path d="M13.25 3L6.25 10.25L13.25 17.5"/></svg>'
789+ const CHEV_R = '<svg ' + ICON_ATTRS + '><path d="M6.25 3L13.25 10.25L6.25 17.5"/></svg>'
790+ const actions = document.createElement('div')
791+ actions.className = 'page-actions'
792+ const copyBtn = document.createElement('button')
793+ copyBtn.className = 'pa-copy'
794+ copyBtn.type = 'button'
795+ copyBtn.innerHTML = DUPLICATE_ICON + '<span>Copy page</span>'
796+ copyBtn.addEventListener('click', async () => {
797+ const label = copyBtn.querySelector('span')
798+ try {
799+ await navigator.clipboard.writeText(mid.innerText || '')
800+ if (label) label.textContent = 'Copied'
801+ } catch {
802+ if (label) label.textContent = 'Select + copy'
803+ }
804+ setTimeout(() => { const l = copyBtn.querySelector('span'); if (l) l.textContent = 'Copy page' }, 1500)
805+ })
806+ actions.appendChild(copyBtn)
807+ const prevCard = page.querySelector('.page-nav-card.prev')
808+ const nextCard = page.querySelector('.page-nav-card.next')
809+ if (prevCard || nextCard) {
810+ const mkNav = (card, svg, label) => {
811+ const a = document.createElement('a')
812+ a.className = 'pa-nav'
813+ a.setAttribute('aria-label', label)
814+ const href = card ? card.getAttribute('href') : null
815+ if (href) a.href = href
816+ else a.classList.add('is-disabled')
817+ a.innerHTML = svg
818+ return a
819+ }
820+ actions.append(mkNav(prevCard, CHEV_L, 'Previous page'), mkNav(nextCard, CHEV_R, 'Next page'))
821+ }
822+ mid.insertBefore(actions, mid.firstChild)
823+
764824 const navLinks = [...leftList.querySelectorAll('a')]
765825 search.addEventListener('input', () => {
766826 const q = search.value.trim().toLowerCase()
0 commit comments