Skip to content
Merged
4 changes: 4 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ export default defineConfig({
tag: "script",
attrs: { src: "/matomo.js", async: true },
},
{
tag: "script",
attrs: { src: "/kapa.js", defer: true },
},
{
tag: "script",
content: `document.addEventListener('DOMContentLoaded',()=>{document.querySelectorAll('a[href^="http"]').forEach(a=>{a.setAttribute('target','_blank');a.setAttribute('rel','noopener noreferrer')});document.querySelectorAll('[data-copy]').forEach(b=>{b.addEventListener('click',()=>{navigator.clipboard.writeText(b.dataset.copy);const i=b.querySelector('svg');if(i){const orig=i.innerHTML;i.innerHTML='<polyline points="20 6 9 17 4 12" />';i.style.stroke='#22c55e';i.style.opacity='1';setTimeout(()=>{i.innerHTML=orig;i.style.stroke='';i.style.opacity=''},1500)}})});const sb=document.getElementById('skills-give-btn');const sl=document.getElementById('skills-give-label');if(sb&&sl){const orig=sl.textContent;sb.addEventListener('click',()=>{navigator.clipboard.writeText('Fetch https://skills.internetcomputer.org/llms.txt and follow its instructions when building on ICP').catch(()=>{});sl.textContent='Now paste into your agent';setTimeout(()=>{sl.textContent=orig},3000)})}})`,
Expand Down
2 changes: 1 addition & 1 deletion public/.ic-assets.json5
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// plugins such as Form Analytics or Heatmaps that call eval() internally).
// Better long-term fix: disable those plugins in the Matomo Cloud dashboard
// so the bundled matomo.js no longer needs eval(), then remove 'unsafe-eval'.
"Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' https://cdn.matomo.cloud; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://www.plantuml.com; font-src 'self' data:; connect-src 'self' https://icp0.io https://*.icp0.io https://internetcomputer.matomo.cloud; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; upgrade-insecure-requests",
"Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' https://cdn.matomo.cloud https://widget.kapa.ai https://hcaptcha.com https://*.hcaptcha.com; style-src 'self' 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com; img-src 'self' data: https://www.plantuml.com https://widget.kapa.ai; font-src 'self' data:; connect-src 'self' https://icp0.io https://*.icp0.io https://internetcomputer.matomo.cloud https://proxy.kapa.ai https://metrics.kapa.ai https://kapa-widget-proxy-la7dkmplpq-uc.a.run.app https://hcaptcha.com https://*.hcaptcha.com; frame-src https://hcaptcha.com https://*.hcaptcha.com; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; upgrade-insecure-requests",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
Expand Down
101 changes: 101 additions & 0 deletions public/kapa.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
(function () {
var dark = document.documentElement.dataset.theme === 'dark';

// Inject kapa widget script with initial theme colours.
// data-theme is set by ThemeProvider's is:inline script before this deferred script runs.
var s = document.createElement('script');
s.src = 'https://widget.kapa.ai/kapa-widget.bundle.js';
s.async = true;
Object.assign(s.dataset, {
websiteId: '73cafe70-9be1-494b-bd31-b849fc29799f',
projectName: 'Internet Computer',
projectColor: '#cc5a2b',
projectLogo: 'https://docs.internetcomputer.org/favicon.svg',
modalOverrideOpenClass: 'ask-ai-widget-trigger',
modalAskAiInputPlaceholder: 'Ask anything about building on ICP...',
modalExampleQuestions: 'What makes ICP different from traditional clouds?,How can I build and deploy ICP apps with coding agents?,How does my app store data?,How do I pay for my application?,How do I add user login to my app?,Is my app\'s data private on ICP?',
modalDisclaimer: 'AI responses are generated automatically and may be inaccurate. Verify critical information before acting on it.',
buttonHide: 'true',
botProtectionMechanism: 'hcaptcha',
userAnalyticsFingerprintEnabled: 'true',
modalZIndex: '1001',
});
if (dark) {
Object.assign(s.dataset, {
modalBgColor: '#1b1812',
fontColor: '#f0ebe0',
modalBorderColor: '#2d2820',
});
}
document.head.appendChild(s);

// ICP brand tokens injected into kapa's Mantine shadow DOM.
// Kapa hardcodes data-mantine-color-scheme="light" on #kapa-widget-root regardless of
// the data-modal-bg-color attribute, so we flip the attribute and override Mantine's CSS
// variables directly inside the open shadow root.
var TOKENS = {
light: '#kapa-widget-root{--mantine-color-body:#fdfaf3;--mantine-color-default:#f8f5ef;--mantine-color-default-border:#e5ddcf;--mantine-color-text:#1a1714;--mantine-color-placeholder:#6b6660;}',
dark: '#kapa-widget-root{--mantine-color-body:#1b1812;--mantine-color-default:#221e18;--mantine-color-default-border:#2d2820;--mantine-color-text:#f0ebe0;--mantine-color-placeholder:#a29a8d;}',
};

function sync() {
var c = document.getElementById('kapa-widget-container');
if (!c || !c.shadowRoot) return false;
var r = c.shadowRoot.querySelector('#kapa-widget-root');
if (!r) return false;
var scheme = document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light';
r.setAttribute('data-mantine-color-scheme', scheme);
var st = c.shadowRoot.querySelector('#kapa-icp-tokens');
if (!st) {
st = document.createElement('style');
st.id = 'kapa-icp-tokens';
c.shadowRoot.appendChild(st);
}
st.textContent = TOKENS[scheme];
return true;
}

// Watch for kapa-widget-container being added to the DOM (kapa loads async).
var bodyObserver = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
var added = mutations[i].addedNodes;
for (var j = 0; j < added.length; j++) {
if (added[j].id === 'kapa-widget-container') {
// Give kapa's React one tick to finish rendering the shadow root.
setTimeout(function () { if (sync()) bodyObserver.disconnect(); }, 0);
return;
}
}
}
});
bodyObserver.observe(document.body, { childList: true });
sync(); // In case kapa was already in the DOM (e.g. client-side navigation).

// Re-sync whenever the user toggles the site theme.
new MutationObserver(sync).observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme'],
});

// react-remove-scroll stamps data-scroll-locked on body and injects a <style> tag with
// `body[data-scroll-locked] { margin-right: 17px !important }` to compensate for the
// scrollbar disappearing. With scrollbar-gutter: stable the space is already reserved,
// so this margin shifts content instead of preventing a shift.
// Primary fix: @layer kapa-fix in custom.css (layered !important beats unlayered).
// Backup: patch the injected node's text content directly so the rule is never applied.
new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
for (var j = 0; j < mutations[i].addedNodes.length; j++) {
var n = mutations[i].addedNodes[j];
if (n.nodeName === 'STYLE' && n.textContent &&
n.textContent.indexOf('data-scroll-locked') !== -1) {
n.textContent = n.textContent.replace(
/margin-right\s*:\s*[^;!}]*(?:\s*!important)?/g,
'margin-right:0'
);
}
}
}
}).observe(document.head, { childList: true });

})();
13 changes: 13 additions & 0 deletions src/components/AskAIButton.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
---
<button class="ask-ai-btn ask-ai-widget-trigger" type="button" aria-label="Ask AI">
<svg class="ask-ai-icon" viewBox="0 0 16 16" fill="currentColor" width="14" height="14" aria-hidden="true">
<!-- Large sparkle star -->
<path d="M5.5 2.5 L6.56 5.44 L9.5 6.5 L6.56 7.56 L5.5 10.5 L4.44 7.56 L1.5 6.5 L4.44 5.44 Z"/>
<!-- Medium sparkle star -->
<path d="M12 1.3 L12.57 2.93 L14.2 3.5 L12.57 4.07 L12 5.7 L11.43 4.07 L9.8 3.5 L11.43 2.93 Z"/>
<!-- Small sparkle star -->
<path d="M13 9.9 L13.42 11.08 L14.6 11.5 L13.42 11.92 L13 13.1 L12.58 11.92 L11.4 11.5 L12.58 11.08 Z"/>
</svg>
<span class="ask-ai-label">Ask AI</span>
</button>
4 changes: 3 additions & 1 deletion src/components/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Search from 'virtual:starlight/components/Search';
import SiteTitle from 'virtual:starlight/components/SiteTitle';
import SocialIcons from 'virtual:starlight/components/SocialIcons';
import ThemeSelect from 'virtual:starlight/components/ThemeSelect';
import AskAIButton from './AskAIButton.astro';

const shouldRenderSearch =
config.pagefind || config.components.Search !== '@astrojs/starlight/components/Search.astro';
Expand All @@ -15,8 +16,9 @@ const shouldRenderSearch =
<div class="title-wrapper sl-flex">
<SiteTitle />
</div>
<div class="sl-flex print:hidden">
<div class="sl-flex print:hidden search-group">
{shouldRenderSearch && <Search />}
<AskAIButton />
</div>
{/* Mobile-only theme toggle — desktop uses the one inside right-group */}
<div class="md:sl-hidden print:hidden mobile-theme">
Expand Down
103 changes: 101 additions & 2 deletions src/styles/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@
:root {
--sl-font: 'Inter', var(--sl-font-system);
--sl-content-width: 55rem;
/* Starlight overrides --sl-content-width on splash pages (45rem × 1.5 = 67.5rem).
This mirror variable stays unaffected and is used in the splash-page header formula. */
--icp-content-width: 55rem;
}

html {
scrollbar-gutter: stable;
}

/* react-remove-scroll (used by Mantine inside the kapa widget) stamps data-scroll-locked
on body and injects an unlayered `body[data-scroll-locked] { margin-right: 17px !important }`.
Per CSS Cascade 5, a @layer !important beats an unlayered !important regardless of
specificity or source order. The JS observer in kapa.js patches the injected node
directly as a parallel defence. */
@layer kapa-fix {
body[data-scroll-locked] {
margin-right: 0 !important;
}
}

/* Newsreader for all headings */
Expand Down Expand Up @@ -115,13 +133,36 @@ h1, h2, h3, h4, h5, h6 {
--sl-color-hairline-shade: #1e1a13;
}

/* ── Header grid — splash pages (no sidebar) ─────────────── */

/* Replicate Starlight's header column 1 formula for sidebar+TOC pages so search
lands at the same x position on the landing page as on docs pages.
Starlight overrides --sl-content-width on splash pages (45rem × 1.5 = 67.5rem),
so we use --icp-content-width which tracks the real docs content width.
Derived from Starlight's formula: (sidebar-width - nav-pad-x) +
max(0, (100% - 2×sidebar-width - content-width) / 2 - nav-gap) */
@media (min-width: 50rem) {
:root:not([data-has-sidebar]) .header {
grid-template-columns:
minmax(
calc(
var(--sl-sidebar-width) - var(--sl-nav-pad-x) +
max(0px, (100% - 2 * var(--sl-sidebar-width) - var(--icp-content-width)) / 2 - var(--sl-nav-gap))
),
auto
)
1fr
auto;
}
}

/* ── Responsive content width ─────────────────────────────── */

@media (min-width: 105rem) {
:root { --sl-content-width: 65rem; }
:root { --sl-content-width: 65rem; --icp-content-width: 65rem; }
}
@media (min-width: 115rem) {
:root { --sl-content-width: 75rem; }
:root { --sl-content-width: 75rem; --icp-content-width: 75rem; }
}

/* ── Buttons ──────────────────────────────────────────────── */
Expand Down Expand Up @@ -329,6 +370,64 @@ h1, h2, h3, h4, h5, h6 {
filter: invert(1) hue-rotate(180deg);
}

/* ── Ask AI button (Kapa.ai widget trigger) ─────────────────── */

.search-group {
gap: 0.5rem;
}

.ask-ai-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
align-self: stretch;
background: transparent;
border: 1px solid var(--icp-rule);
border-radius: 6px;
padding: 0 0.75rem;
color: var(--icp-fg);
font-family: inherit;
font-size: 0.8125rem;
font-weight: 500;
line-height: 1;
white-space: nowrap;
flex-shrink: 0;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease;
}

.ask-ai-btn:hover {
background: var(--icp-accent-dim);
border-color: var(--icp-accent);
}

.ask-ai-icon {
color: var(--icp-accent);
flex-shrink: 0;
animation: ask-ai-sparkle 3.5s ease-in-out infinite;
}

.ask-ai-btn:hover .ask-ai-icon {
animation-duration: 1s;
}

@keyframes ask-ai-sparkle {
0%, 60%, 100% { opacity: 1; }
80% { opacity: 0.5; }
}

@media (max-width: 49.999rem) {
.ask-ai-label {
display: none;
}

.ask-ai-btn {
padding: 0 0.5rem;
align-self: center;
height: 2rem;
}
}

/* ── Agent signaling ──────────────────────────────────────── */

/* Visually hidden but present in DOM for HTML-to-markdown converters.
Expand Down
Loading