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
18 changes: 17 additions & 1 deletion docs/app/assets/tailwind-theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@

@custom-variant dark (&:where(.dark, .dark *));

.dark .static-shiki .shiki {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}

.dark .static-shiki .shiki span {
color: var(--shiki-dark) !important;
background-color: transparent !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}

:root {
/* Primary */
--primary-1: var(--violet-1);
Expand Down Expand Up @@ -2184,4 +2200,4 @@

#ph-conversations-widget-container button[aria-label^="Open chat"]+div {
display: none !important;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def card(
class_name="flex flex-col gap-2 p-8",
),
content,
rx.el.a(href=href, class_name="absolute inset-0"),
rx.el.a(href=href, aria_label=title, class_name="absolute inset-0"),
class_name="flex flex-col bg-secondary-1/96 backdrop-blur-[16px] rounded-xl relative cursor-pointer transition-colors overflow-hidden shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_12px_24px_0_rgba(0,0,0,0.08),0_1px_1px_0_rgba(0,0,0,0.01),0_4px_8px_0_rgba(0,0,0,0.03)] dark:shadow-none dark:border dark:border-secondary-4",
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def docs_item(
description,
class_name="text-secondary-11 text-sm font-[475]",
),
rx.el.a(to=href, class_name="absolute inset-0"),
rx.el.a(to=href, aria_label=title, class_name="absolute inset-0"),
class_name="flex flex-col gap-2 py-8 pr-8 relative group lg:max-w-[21rem] w-full max-lg:text-start hover:bg-[linear-gradient(243deg,var(--secondary-2)_0%,var(--secondary-1)_100%)]",
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def link_item(
description,
class_name="text-secondary-11 text-sm font-[475] text-start",
),
rx.el.a(to=href, class_name="absolute inset-0"),
rx.el.a(to=href, aria_label=title, class_name="absolute inset-0"),
class_name=ui.cn(
"flex flex-col gap-2 pr-8 py-8 group border-r border-b border-secondary-4 relative max-lg:p-6 hover:bg-[linear-gradient(243deg,var(--secondary-2)_0%,var(--secondary-1)_100%)]",
"lg:pl-8 pl-6" if has_padding_left else "",
Expand Down
20 changes: 13 additions & 7 deletions docs/app/reflex_docs/templates/docpage/docpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,22 +234,26 @@ def feedback_content() -> rx.Component:
def feedback_button() -> rx.Component:
thumb_cn = "w-full gap-2 px-3 py-0.5 flex flex-row items-center justify-center whitespace-nowrap border cursor-pointer transition-colors font-small"
return ui.popover.root(
ui.popover.trigger(
render_=rx.el.div(
feedback_choice_button(
rx.el.div(
ui.popover.trigger(
render_=feedback_choice_button(
"Yes",
"ThumbsUpIcon",
1,
ui.cn("rounded-[20px_0_0_20px] border-r-0", thumb_cn),
),
feedback_choice_button(
),
ui.popover.trigger(
render_=feedback_choice_button(
"No",
"ThumbsDownIcon",
0,
ui.cn("rounded-[0_20px_20px_0]", thumb_cn),
),
class_name="w-full lg:w-auto items-center flex flex-row",
),
role="group",
aria_label="Was this page helpful?",
class_name="w-full lg:w-auto items-center flex flex-row",
),
ui.popover.portal(
ui.popover.positioner(
Expand Down Expand Up @@ -746,8 +750,10 @@ def breadcrumb(path: str, nav_sidebar: rx.Component, doc_content: str | None = N
return rx.box(
docs_sidebar_drawer(
nav_sidebar,
trigger=rx.box(
class_name="absolute inset-0 bg-transparent z-[1] lg:hidden flex",
trigger=rx.el.button(
type="button",
aria_label="Open docs navigation",
class_name="absolute inset-0 bg-transparent z-[1] lg:hidden flex border-none p-0 cursor-pointer",
),
),
rx.box(
Expand Down
170 changes: 162 additions & 8 deletions docs/app/reflex_docs/views/inkeep.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,147 @@
"""UI and logic inkeep chat component."""

import reflex as rx
from reflex.constants import MemoizationMode
from reflex.event import EventSpec
from reflex.experimental.client_state import ClientStateVar
from reflex.utils.imports import ImportVar
from reflex.vars import Var
from reflex.vars.base import VarData


class InkeepSearchBar(rx.NoSSRComponent):
tag = "InkeepSearchBar"
library = "@inkeep/cxkit-react@0.5.115"


_INKEEP_LOADED_STATE = "inkeep_loaded"
_INKEEP_OPEN_STATE = "inkeep_open"
_SEARCH_WRAPPER_ID = "inkeep-search-wrapper"
_SEARCH_TRIGGER_ID = "inkeep-search-trigger"
_SEARCH_TRIGGER_FALLBACK_STYLE = (
f"#{_SEARCH_WRAPPER_ID}:has(> [id^='inkeep-shadow']) "
f"> #{_SEARCH_TRIGGER_ID} {{ display: none; }}"
)

# Inline copy of assets/icons/search.svg so the placeholder needs no extra request.
_SEARCH_ICON_SVG = (
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"'
' viewBox="0 0 16 16" aria-hidden="true"><path stroke="currentColor"'
' stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25"'
' d="M11.332 11.333 13.999 14"/><path stroke="currentColor"'
' stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25"'
' d="M12.667 7.333A5.333 5.333 0 1 0 2 7.333a5.333 5.333 0 0 0 10.667 0"/></svg>'
)

# Runs on mount (as a real effect, not an inert rendered <script>) to:
# 1. Fix the placeholder's keyboard hint for non-Mac platforms, which can't
# be known during SSR (the default label is the Mac glyph).
# 2. Forward Cmd/Ctrl+K to the placeholder trigger before the widget loads,
# mounting the real widget with its modal open. Once Inkeep's shadow host
# mounts, this no-ops and Inkeep's own handler takes over.
_HOTKEY_AND_HINT_HOOK = f"""
useEffect(() => {{
const triggerId = "{_SEARCH_TRIGGER_ID}";
const wrapperId = "{_SEARCH_WRAPPER_ID}";
const platform =
(navigator.userAgentData && navigator.userAgentData.platform) ||
navigator.platform ||
navigator.userAgent ||
"";
if (!/mac|iphone|ipad|ipod/i.test(platform)) {{
const hint = document.getElementById(triggerId)?.querySelector("kbd > span");
if (hint) hint.textContent = "Ctrl";
}}
const onKeydown = (event) => {{
if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "k") return;
const trigger = document.getElementById(triggerId);
const widgetReady = document
.getElementById(wrapperId)
?.querySelector("[id^='inkeep-shadow']");
if (!trigger || widgetReady) return;
event.preventDefault();
trigger.click();
}};
document.addEventListener("keydown", onKeydown);
return () => document.removeEventListener("keydown", onKeydown);
}}, [])
"""


def _controlled_modal_settings(opened: ClientStateVar) -> Var:
"""Create controlled modal settings for the Inkeep search bar.

Args:
opened: Client state var tracking whether the modal is open.

Returns:
A Var containing the Inkeep props and client state dependencies.
"""
is_open = opened.value
on_open_change = opened.set
return Var(
"{...searchBarProps, modalSettings: "
f"{{isOpen: {is_open!s}, onOpenChange: {on_open_change!s}}}}}"
)._replace(
merge_var_data=VarData.merge(
is_open._get_all_var_data(),
on_open_change._get_all_var_data(),
)
)


def _search_trigger_placeholder(on_click: EventSpec) -> rx.Component:
"""Lightweight stand-in for the Inkeep search bar button.

Mirrors the widget's trigger styling so mounting the real search bar does
not shift layout, while keeping the ~276 KB Inkeep chunk (and the Google
Fonts stylesheet it injects) off the critical path until first use.

Args:
on_click: Event handler that mounts the real search widget.

Returns:
The placeholder button component.
"""
return rx.el.button(
rx.html(_SEARCH_ICON_SVG, class_name="flex shrink-0"),
rx.el.span("Search", class_name="max-xl:hidden"),
rx.el.kbd(
# Two keys with a 2px gap, mirroring Inkeep's kbd-wrapper markup.
rx.el.span("⌘"),
rx.el.span("K"),
class_name="max-xl:hidden ml-auto flex items-center justify-center gap-0.5 px-1 h-5 rounded bg-secondary-3 text-[0.8125rem] font-[475] leading-5 font-sans",
),
id=_SEARCH_TRIGGER_ID,
type="button",
aria_label="Search documentation",
on_click=on_click,
class_name=(
"flex flex-row items-center justify-center xl:justify-start gap-2 rounded-lg "
"h-8 min-h-8 w-8 xl:w-40 px-0 xl:px-2 "
"bg-secondary-1 dark:bg-secondary-2 hover:bg-secondary-2 dark:hover:bg-secondary-3 "
"text-secondary-11 text-sm font-medium leading-6 border-none cursor-pointer "
"shadow-[0_-1px_0_0_rgba(0,0,0,0.08)_inset,0_0_0_1px_rgba(0,0,0,0.08)_inset,0_1px_2px_0_rgba(0,0,0,0.02),0_1px_4px_0_rgba(0,0,0,0.02)] "
"dark:shadow-[0_-1px_0_0_rgba(255,255,255,0.06)_inset,0_0_0_1px_rgba(255,255,255,0.04)_inset]"
),
)


class Search(rx.el.Div):
_memoization_mode = MemoizationMode(recursive=False)

def add_imports(self):
"""Add the imports for the component."""
return {
"react": {ImportVar(tag="useContext")},
"react": {ImportVar(tag="useContext"), ImportVar(tag="useEffect")},
"$/utils/context": {ImportVar(tag="ColorModeContext")},
}

def add_hooks(self):
"""Add the hooks for the component."""
return [
"const { resolvedColorMode } = useContext(ColorModeContext)",
_HOTKEY_AND_HINT_HOOK,
"""
const escalationParams = {
type: "object",
Expand Down Expand Up @@ -135,6 +255,9 @@ def add_hooks(self):
forcedColorMode: resolvedColorMode, // options: 'light' or dark'
},
theme: {
// The site ships its own fonts; skip the widget's default Google Fonts
// (Inter) stylesheet injection.
disableLoadingDefaultFont: true,
// Add inline styles using the recommended approach from the docs
styles: [
{
Expand Down Expand Up @@ -280,7 +403,8 @@ def add_hooks(self):
width: auto;
}

.ikp-search-bar__kbd-wrapper {
[data-theme='light'] .ikp-search-bar__kbd-wrapper,
[data-theme='dark'] .ikp-search-bar__kbd-wrapper {
padding: 0px 0.25rem;
justify-content: center;
align-items: center;
Expand All @@ -299,8 +423,17 @@ def add_hooks(self):
width: fit-content;
}

.ikp-search-bar__text,
.ikp-search-bar__icon {
.ikp-search-bar__kbd-wrapper kbd {
/* Inkeep's base styles give the inner keys a monospace stack;
inherit the wrapper's font so the hint matches the
pre-mount placeholder trigger. */
font: inherit;
}

[data-theme='light'] .ikp-search-bar__text,
[data-theme='dark'] .ikp-search-bar__text,
[data-theme='light'] .ikp-search-bar__icon,
[data-theme='dark'] .ikp-search-bar__icon {
color: var(--secondary-11);
font-weight: 500;
font-style: normal;
Expand Down Expand Up @@ -390,11 +523,32 @@ def add_hooks(self):

@classmethod
def create(cls):
"""Create the search component."""
"""Create the search component.

The Inkeep widget is not mounted until the user interacts with the
search trigger, keeping its large bundle out of the initial page load.
The modal is controlled and starts open, so the interaction that
mounts the widget also opens search.
"""
loaded = ClientStateVar.create(
_INKEEP_LOADED_STATE, default=False, global_ref=False
)
opened = ClientStateVar.create(
_INKEEP_OPEN_STATE, default=True, global_ref=False
)
return super().create(
InkeepSearchBar.create(
special_props=[Var("{...searchBarProps}")],
)
rx.el.style(_SEARCH_TRIGGER_FALLBACK_STYLE),
rx.cond(
loaded.value,
InkeepSearchBar.create(
special_props=[_controlled_modal_settings(opened)],
),
rx.fragment(),
),
_search_trigger_placeholder(
on_click=rx.call_function(loaded.set_value(True)),
),
id=_SEARCH_WRAPPER_ID,
)


Expand Down
2 changes: 2 additions & 0 deletions docs/app/rxconfig.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import reflex as rx
from reflex_site_shared.static_shiki import StaticShikiPlugin

from agent_files import AgentFilesPlugin

Expand All @@ -14,5 +15,6 @@
rx.plugins.TailwindV4Plugin(),
rx.plugins.SitemapPlugin(trailing_slash="always"),
AgentFilesPlugin(),
StaticShikiPlugin(),
],
)
Loading
Loading