Skip to content
Draft
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
5 changes: 0 additions & 5 deletions _includes/head.html
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,4 @@
{% endif %}
<!-- End of crisp.chat script -->

{% if page.url contains "/learn" or page.url contains "/docs" or page.url contains "/search" or page.url contains
"/changelog" %}
<!-- Add Inkeep widget -->
<script type="module" src="https://cdn.jsdelivr.net/npm/@inkeep/cxkit-js@0.5.117/dist/embed.js" crossorigin="anonymous" defer></script>
{% endif %}
</head>
8 changes: 0 additions & 8 deletions _includes/learn/search-input.html

This file was deleted.

27 changes: 0 additions & 27 deletions _includes/learn/top-bar.html

This file was deleted.

81 changes: 0 additions & 81 deletions public/js/inkeep.js

This file was deleted.

33 changes: 33 additions & 0 deletions src/components/TopBar.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
import LiquidInclude from "@/components/LiquidInclude.astro";
import Search from "@/components/search/Search.astro";
import type { LiquidRenderer } from "@/lib/liquid/liquidRenderer";

interface Props {
lq: LiquidRenderer;
showBreadcrumb?: boolean;
}

const { lq, showBreadcrumb = false } = Astro.props;
---

<div class="top-bar">
<div>
{
showBreadcrumb && (
<div class="learn-breadcrumb-trail">
<LiquidInclude lq={lq} src="docs_breadcrumb.html" />
</div>
)
}
</div>

<div>
<LiquidInclude lq={lq} src="docs_version.html" />
<a href="/releases" class="whats-new">
<LiquidInclude lq={lq} src="svg-icons/stars.html" />
What’s new
</a>
<Search />
</div>
</div>
37 changes: 37 additions & 0 deletions src/components/inkeep/shared.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* Styles for the widget's shadow DOM, inlined through `theme.styles` rather than
linked, so each widget carries only what it needs. cxkit-js derives these class
names from each component's `_id` debug prop, kebab-cased (`modal__Content`
becomes `.ikp-modal__content`). */
.ikp-modal-widget__content {
font-family: Lato, sans-serif;
}

.ikp-ai-chat__toolbar-header {
visibility: hidden;
}

.ikp-ai-chat-page-wrapper {
padding-top: 10px;
}

.ikp-ai-chat__message-box {
background-color: #eef6fc;
border: 1px solid #e4ecfb;
box-shadow: none;
}

.ikp-ai-chat-footer__content-wrapper {
padding-bottom: 30px;
}

.ikp-code {
background-color: #f9fbfe;
color: #5a6072;
font-weight: 400;
}

.ikp-chat-bubble__user-message img {
left: -2px;
position: relative;
zoom: 1.3;
}
20 changes: 20 additions & 0 deletions src/components/search/Search.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<div id="inkeep"></div>

<script>
import { mountSearch } from "./search";

mountSearch();
</script>

<style>
#inkeep {
background-color: #fafbfe;
flex: 1;
height: 40px;

/* the width may vary across platforms due to different shortcut labels (Ctrl+K / ⌘K). */
min-width: 140px;
position: relative;
width: auto;
}
</style>
63 changes: 63 additions & 0 deletions src/components/search/header-offset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
export const MODAL_TOP = "--inkeep-modal-top";
export const GAP = 24;

// `.promo-banner` renders inside `.navigation-header`, above the nav row, and
// `--navigation-header-height` is a static 77/85px that does not account for it —
// nor for the 5px it is already short by. Measure instead.
const HEADER = "header.bootstrap, .navigation-header";
const BANNER_PARENT = ".navigation-header";

export const modalTop = (headerBottom: number): string =>
`${Math.max(0, Math.round(headerBottom)) + GAP}px`;

// The header is sticky at `top: -48px` so the promo banner scrolls away while the
// nav row stays pinned — its bottom edge moves between 130px and 82px. Read the
// live rect rather than assuming a resting height.
export function publishHeaderOffset(doc: Document): string | null {
const header = doc.querySelector(HEADER);
if (!header) return null;

const top = modalTop(header.getBoundingClientRect().bottom);
doc.documentElement.style.setProperty(MODAL_TOP, top);
return top;
}

export function trackHeaderOffset(doc: Document): () => void {
const header = doc.querySelector(HEADER);
const view = doc.defaultView;
if (!header || !view) return () => {};

let last = publishHeaderOffset(doc);

// Scrolling moves the header's bottom edge without firing either observer. The
// browser already caps scroll events at one per frame, so read straight through
// and skip the style write while the value holds.
const onScroll = () => {
const top = modalTop(header.getBoundingClientRect().bottom);
if (top === last) return;
last = top;
doc.documentElement.style.setProperty(MODAL_TOP, top);
};
view.addEventListener("scroll", onScroll, { passive: true });

const republish = () => {
last = publishHeaderOffset(doc);
};

// A ResizeObserver on the header does not fire when promo-banner.js removes the
// banner, even though the header visibly shrinks — measured, not assumed. The
// banner is a direct child, so childList alone catches it without subtree noise.
const bannerParent = doc.querySelector(BANNER_PARENT);
const mutation = new MutationObserver(republish);
if (bannerParent) mutation.observe(bannerParent, { childList: true });

// Still worth observing for the 77/85px breakpoint and font reflow.
const resize = new ResizeObserver(republish);
resize.observe(header);

return () => {
view.removeEventListener("scroll", onScroll);
mutation.disconnect();
resize.disconnect();
};
}
64 changes: 16 additions & 48 deletions public/css/inkeep.css → src/components/search/inkeep-shadow.css
Original file line number Diff line number Diff line change
@@ -1,31 +1,22 @@
inkeep-portal {
position: relative;
z-index: 100000;
}
/* Search-widget shadow DOM. Twin of metabase.github.io
apps/marketing-astro/src/components/search/inkeep-shadow.css — keep the two in sync. */

/* cxkit-js builds class names from each component's `_id` debug prop via a
helper that kebab-cases and lowercases the suffix (e.g., `modal__Content`
becomes `.ikp-modal__content`). Push the modal below our fixed header. */
.ikp-modal__content {
margin-top: var(--navigation-header-height, 77px);
}
/* `--inkeep-modal-top` is published by header-offset.ts: the widget pins the modal
at `top: 5%`, and CSS in here cannot measure the header past the shadow boundary.
The fallback is the desktop header plus the same gap.

.ikp-modal__overlay {
padding-top: var(--navigation-header-height, 77px);
Below 768px the widget sets `data-mobile` and goes full-screen via `inset: 0`, at a
specificity a bare class cannot reach — `:not()` opts us out rather than losing `top`
while `max-height` still applied and stranded a dead strip at the bottom. */
.ikp-modal__content:not([data-mobile]) {
max-height: calc(100dvh - var(--inkeep-modal-top, 101px) - 24px);
top: var(--inkeep-modal-top, 101px);
}

.ikp-search-bar-trigger__container {
font-family: Lato, "Helvetica Neue", Helvetica, sans-serif;
}

.ikp-modal-widget__content {
font-family: Lato, sans-serif;
}

.ikp-ai-chat__toolbar-header {
visibility: hidden;
}

.ikp-search-bar-trigger__wrapper .ikp-btn {
background-color: #eef6fc;
border: 1px solid #e4ecfb;
Expand All @@ -41,14 +32,10 @@ inkeep-portal {
background-color: white;
}

.ikp-search-bar-trigger__container--variant_subtle:is(:hover,[data-hover]) {
.ikp-search-bar-trigger__container--variant_subtle:is(:hover, [data-hover]) {
background-color: #eef6fc !important;
}

.ikp-ai-chat-page-wrapper {
padding-top: 10px;
}

.ikp-search-bar-trigger__text--size_compact {
font-size: 14px;
}
Expand All @@ -64,7 +51,9 @@ inkeep-portal {
font-weight: 700;
margin-left: 6px;
padding: 18px;
transition: background-color 0.2s ease, color 0.2s ease;
transition:
background-color 0.2s ease,
color 0.2s ease;
}

.ikp-switch-view-toggle button:hover {
Expand Down Expand Up @@ -92,33 +81,12 @@ inkeep-portal {
fill: #509ee3;
}

.ikp-code {
background-color: #f9fbfe;
color: #5a6072;
font-weight: 400;
}

.ikp-chat-bubble__user-message img {
left: -2px;
position: relative;
zoom: 1.3;
}

/* prettier-ignore -- wrapping this selector trips stylelint's indentation rule */
div.ikp-d_flex.ikp-flex_row.ikp-items_center.ikp-pt_3.ikp-pb_1:has(.ikp-search-bar-stand-alone__search-input) {
margin-right: -2px;
margin-top: 6px;
}

.ikp-ai-chat__message-box {
background-color: #eef6fc;
border: 1px solid #e4ecfb;
box-shadow: none;
}

.ikp-ai-chat-footer__content-wrapper {
padding-bottom: 30px;
}

@media screen and (max-width: 992px) {
.ikp-search-bar-trigger__text {
display: none;
Expand Down
Loading