Skip to content
Merged
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
10 changes: 8 additions & 2 deletions docs/app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,15 @@ The three `assets/docs-*-theme.css` / `assets/docs-theme.css` stylesheets adapt
the docs-only styling from [Reflex PR #7549](https://github.com/reflex-dev/reflex/pull/7549):
Inter body text, Geist headings, warm neutral light/dark palettes, compact
controls, and the opal header artwork. They are loaded after the shared base
theme. The header styles reuse the shared announcement markup and dismissal
state; the artwork URL includes the `/docs/xy` mount prefix. Fonts are
theme. The header reuses the shared announcement body and close event, adapting
its decorative markup and styling to `reflex-marketing`'s five opal panels,
44px desktop / 40px mobile contribution, and text-only CTA. The head script in
`xy_docs/announcement.py` uses marketing's release-scoped local-storage dismissal
key so dismissal survives reloads and same-origin navigation without a flash
or reserved gap. The artwork URL includes the `/docs/xy` mount prefix. Fonts are
bundled from the pinned Fontsource frontend packages, without a font CDN.
Announcement title, destination, and release in `xy_docs/announcement.py` follow
marketing's main branch and must be updated together when its copy changes.
Editorial styles use shared `docs-*` hooks and the hooks added by
`xy_docs_layout`; keep them scoped so live chart examples retain their own
typography and data colors. The shared Markdown layout still owns navigation
Expand Down
57 changes: 39 additions & 18 deletions docs/app/assets/docs-header-theme.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* XY docs header brand layer, adapted from reflex-dev/reflex#7549. */
/* Docs keep a 64px fixed header and a 40px announcement contribution.
The banner artwork and glass treatment match the marketing site. */
/* Keep the 64px docs header; match reflex-marketing's 44px desktop / 40px
mobile announcement, layered opal artwork, and persisted dismissal. */
html:has(.docs-nav-frame) { --docs-header-height: 64px; }
html:not([data-announcement-dismissed='true']):has(.docs-nav-frame [data-announcement]) { --docs-header-height: 108px; }
.docs-nav-frame {
left: 0;
--docs-control-shadow: 0 0 0 .5px rgb(0 0 0 / 12.5%), 0 1px 1px rgb(0 0 0 / 4%), 0 2px 4px rgb(0 0 0 / 4%);
Expand All @@ -14,8 +16,8 @@
box-shadow: none;
transition: border-radius 200ms ease, background-color 200ms ease;
}
.docs-nav-frame:has([data-announcement]) > .docs-navbar-header {
margin-top: -24px;
html:not([data-announcement-dismissed='true']) .docs-nav-frame:has([data-announcement]) > .docs-navbar-header {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
margin-top: -28px;
border-radius: 24px 24px 0 0;
}
.docs-nav-frame > .docs-navbar-header > .docs-navbar-inner {
Expand Down Expand Up @@ -71,26 +73,35 @@
.docs-nav-frame [data-announcement] {
position: relative;
isolation: isolate;
height: 64px;
padding-bottom: 24px;
height: 72px;
padding-bottom: 28px;
width: 100%;
overflow: hidden;
background: #e8e7f1;
color: #000;
border: 0;
font-family: Inter, Arial, sans-serif;
}
/* Shared banner content, links, and state stay in reflex-site-shared. */
.docs-nav-frame [data-announcement]::before {
content: '';
html[data-announcement-dismissed='true'] .docs-nav-frame [data-announcement] { display: none; }
/* Five mirrored artwork panels match reflex-marketing/assets/marketing.css. */
.docs-nav-frame .docs-announcement-glass {
display: flex;
position: absolute;
z-index: -2;
inset: -48px -12%;
pointer-events: none;
background: url('/docs/xy/header/opal.avif') center 38% / cover no-repeat;
filter: blur(6px);
will-change: transform;
animation: docs-announcement-glass-drift 26s ease-in-out infinite alternate;
}
.docs-announcement-glass > span {
flex: 1;
background: url('/docs/xy/header/opal.avif') center 22% / cover no-repeat;
}
.docs-announcement-glass > span:nth-child(2) { background-position: center 62%; transform: scaleX(-1); }
.docs-announcement-glass > span:nth-child(3) { background-position: center 38%; }
.docs-announcement-glass > span:nth-child(4) { background-position: center 78%; transform: scaleX(-1); }
.docs-announcement-glass > span:nth-child(5) { background-position: center 50%; }
.docs-nav-frame [data-announcement]::after {
content: '';
position: absolute;
Expand Down Expand Up @@ -146,10 +157,11 @@
box-shadow: var(--docs-control-shadow);
white-space: nowrap;
}
.docs-nav-frame [data-announcement] > a > span:first-child > span:nth-child(3) svg { display: none; }
.docs-nav-frame [data-announcement] > button {
position: absolute;
/* The shared button's -translate-y-1/2 centers its 28px box at y=20. */
top: 20px;
/* Center in marketing's visible 44px announcement contribution. */
top: 22px;
right: 16px;
display: inline-flex;
align-items: center;
Expand All @@ -172,26 +184,35 @@
to { transform: translate3d(5%, 0, 0) scale(1); }
}
@media (max-width: 1279px) {
.docs-nav-frame:has(.docs-navbar-menu[open]) > .docs-navbar-header {
html .docs-nav-frame:has(.docs-navbar-menu[open]) > .docs-navbar-header {
border-radius: 0;
}
}
@media (max-width: 960px) {
html:not([data-announcement-dismissed='true']):has(.docs-nav-frame [data-announcement]) { --docs-header-height: 104px; }
.docs-nav-frame [data-announcement] { height: 64px; padding-bottom: 24px; }
html:not([data-announcement-dismissed='true']) .docs-nav-frame:has([data-announcement]) > .docs-navbar-header { margin-top: -24px; }
.docs-nav-frame [data-announcement] > button { top: 20px; }
}
@media (max-width: 640px) {
.docs-nav-frame [data-announcement] > a > span:first-child > span:nth-child(3) { display: none; }
.docs-nav-frame [data-announcement] > a > span:first-child > span:nth-child(2) {
min-width: 0;
white-space: normal;
text-align: center;
line-height: 18px;
}
}
@media (max-width: 639px) {
.docs-nav-frame > .docs-navbar-header { padding-inline: 16px; }
.docs-navbar-inner { gap: 16px; }
.docs-navbar-brand { gap: 10px; }
.docs-navbar-mobile-panel { padding-inline: 16px; }
.docs-nav-frame [data-announcement] > a > span:first-child { padding-inline: 40px; gap: 8px; }
.docs-nav-frame [data-announcement] > a > span:first-child { padding-inline: 40px; }
.docs-nav-frame [data-announcement] > button { right: 8px; }
}
@media (max-width: 379px) {
.docs-nav-frame [data-announcement] > a > span:first-child > span:nth-child(2) { font-size: 13px; }
}
@media (prefers-reduced-motion: reduce) {
.docs-nav-frame [data-announcement]::before { animation: none; }
.docs-nav-frame .docs-announcement-glass { animation: none; will-change: auto; }
.docs-nav-frame > .docs-navbar-header,
.docs-navbar-mobile-link { transition: none; }
}
Expand Down
96 changes: 96 additions & 0 deletions docs/app/tests/test_announcement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Production contracts for the announcement shared with reflex-marketing."""

import os

import pytest
from xy_docs.announcement import ANNOUNCEMENT_RELEASE


def test_production_announcement_matches_marketing_and_persists_dismissal() -> None:
"""Check responsive artwork, storage, reloads, and hidden-header geometry."""
base_url = os.environ.get("XY_DOCS_BASE_URL")
if base_url is None:
pytest.skip("XY_DOCS_BASE_URL is only set for the production-DOM check")

from playwright.sync_api import expect, sync_playwright

with sync_playwright() as playwright:
browser = playwright.chromium.launch()
context = browser.new_context()
context.add_init_script(
"""if (!localStorage.getItem('reflex_announcement_dismissed_release')) {
localStorage.setItem('reflex_announcement_dismissed_release', 'xy-in-reflex-build-v1');
}"""
)
page = context.new_page()
for width in (1440, 1280, 1279, 960, 390, 320):
page.set_viewport_size({"width": width, "height": 900})
page.goto(base_url, wait_until="networkidle")
banner = page.locator("[data-announcement]")
expect(banner).to_be_visible()
expect(banner.locator(".docs-announcement-glass > span")).to_have_count(5)
link = banner.locator("a")
expect(link).to_have_accessible_name(
"New: Build vs. Buy for Internal Software — Read more (opens in a new tab)"
)
expect(link).to_have_attribute(
"href", "https://reflex.dev/blog/build-vs-buy-internal-software/"
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
)
expect(link).to_have_attribute("target", "_blank")
assert {"noopener", "noreferrer"} <= set((link.get_attribute("rel") or "").split())
assert page.locator(".docs-nav-frame").bounding_box()["height"] == (
108 if width > 960 else 104
)
message = banner.locator(".announcement-shimmer").bounding_box()
assert 0 <= message["x"] < message["x"] + message["width"] <= width
cta = banner.get_by_text("Read more", exact=True)
assert cta.is_visible() == (width > 640)
assert not banner.locator("a svg").is_visible()
header = page.locator(".docs-navbar-header")
expect(header).to_have_css("border-top-left-radius", "24px")
expect(header).to_have_css("border-top-right-radius", "24px")
if width < 1280:
menu = page.locator(".docs-navbar-menu > summary")
menu.click()
expect(header).to_have_css("border-top-left-radius", "0px")
expect(header).to_have_css("border-top-right-radius", "0px")
menu.click()
expect(header).to_have_css("border-top-left-radius", "24px")
expect(header).to_have_css("border-top-right-radius", "24px")
for color_mode in ("light", "dark"):
page.emulate_media(color_scheme=color_mode, reduced_motion="reduce")
assert banner.evaluate("el => getComputedStyle(el).color") == "rgb(0, 0, 0)"
assert (
banner.locator(".docs-announcement-glass").evaluate(
"el => getComputedStyle(el).animationName"
)
== "none"
)

# A second open tab receives marketing's same-origin storage event.
other = context.new_page()
other.goto(base_url, wait_until="networkidle")
page.get_by_role("button", name="Dismiss announcement").click()
for current in (page, other):
expect(current.locator("[data-announcement]")).to_be_hidden()
expect(current.locator(".docs-nav-frame")).to_have_css("height", "64px")
assert (
page.evaluate("localStorage.getItem('reflex_announcement_dismissed_release')")
== ANNOUNCEMENT_RELEASE
)
page.reload(wait_until="networkidle")
expect(page.locator("[data-announcement]")).to_be_hidden()
expect(page.locator(".docs-nav-frame")).to_have_css("height", "64px")
context.close()

blocked = browser.new_context()
blocked.add_init_script(
"Object.defineProperty(window, 'localStorage', {get() {throw new Error('blocked')}})"
)
page = blocked.new_page()
page.goto(base_url, wait_until="networkidle")
expect(page.locator("[data-announcement]")).to_be_visible()
page.get_by_role("button", name="Dismiss announcement").click()
expect(page.locator("[data-announcement]")).to_be_hidden()
expect(page.locator(".docs-nav-frame")).to_have_css("height", "64px")
browser.close()
47 changes: 43 additions & 4 deletions docs/app/tests/test_docs_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from collections.abc import Iterator
from pathlib import Path
from types import SimpleNamespace
from urllib.parse import urlparse
from urllib.parse import unquote, urlparse

import pytest
from reflex_base.components.memo import MemoComponent
Expand Down Expand Up @@ -149,6 +149,17 @@ def _walk_blocks(blocks: tuple[Block, ...]) -> Iterator[LinkSpan]:
yield from _walk_spans(cell.children)


def _rendered_ids(component: object) -> Iterator[str]:
"""Collect literal IDs from the rendered component tree."""
value = getattr(component, "id", None)
if not isinstance(value, str):
value = getattr(value, "_var_value", None)
if isinstance(value, str):
yield value
for child in getattr(component, "children", ()):
yield from _rendered_ids(child)


def _normalize_xy_docs_path(url: str) -> str | None:
"""Return an app-relative route for a canonical XY docs URL.

Expand Down Expand Up @@ -645,6 +656,24 @@ def test_markdown_heading_links_are_route_local_after_client_navigation() -> Non
assert "router_rx_state_" not in rendered


def test_authored_in_page_links_resolve_to_rendered_ids() -> None:
"""Check actual fragment targets, including punctuation kept in heading IDs."""
failures = []
for page in discover_docs(DOCS_CONFIG):
links = [
link
for link in _walk_blocks(parse_document(page.content).blocks)
if link.target.startswith("#")
]
if not links:
continue
targets = set(_rendered_ids(render_xy_markdown_page(page)))
failures.extend(
(page.route, link.target) for link in links if unquote(link.target[1:]) not in targets
)
assert not failures, failures


def test_colorbar_docs_match_the_declarative_and_custom_boundaries() -> None:
"""Keep inferred built-ins distinct from opaque framework replacements."""
content = (DOCS_ROOT / "components/colorbars.md").read_text(encoding="utf-8")
Expand Down Expand Up @@ -2453,9 +2482,19 @@ def test_xy_navbar_uses_xy_links_github_and_the_official_drawer() -> None:
assert "<details" not in rendered
assert "<summary" not in rendered
assert XY_REPOSITORY_URL in rendered
from reflex_site_shared.views.hosting_banner import hosting_banner

assert str(hosting_banner()) in rendered
assert "Build vs. Buy for Internal Software" in rendered
assert "xy is now in Reflex Build" not in rendered
assert '"data-shimmer-text":"Build vs. Buy for Internal Software"' in rendered
assert (
json.dumps("New: Build vs. Buy for Internal Software — Read more (opens in a new tab)")
in rendered
)
assert 'className:"docs-announcement-glass"' in rendered
assert '"data-announcement-dismiss":""' in rendered
assert 'href:"https://reflex.dev/blog/build-vs-buy-internal-software/"' in rendered
assert "https://reflex.dev/blog/xy-in-reflex-build/" not in rendered
assert "Dismiss announcement" in rendered
assert "hide_banner" in rendered
assert "Reserve your spot" not in rendered
assert "https://luma.com/a1ty77bt" not in rendered
assert "Reflex Agent Toolkit is launching" not in rendered
Expand Down
63 changes: 63 additions & 0 deletions docs/app/xy_docs/announcement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Marketing presentation and persistence around the shared announcement."""

import json

import reflex as rx

# Match reflex-marketing so dismissals carry across the same-origin sites.
ANNOUNCEMENT_RELEASE = "build-vs-buy-internal-software-v1"
ANNOUNCEMENT_TITLE = "Build vs. Buy for Internal Software"
ANNOUNCEMENT_URL = "https://reflex.dev/blog/build-vs-buy-internal-software/"


def match_marketing_announcement(component: rx.Component) -> None:
"""Adapt shared markup to reflex-marketing without copying its banner body."""
if "data-announcement" in component.custom_attrs:
link, dismiss = component.children
link.href = rx.Var.create(ANNOUNCEMENT_URL)
link.custom_attrs["aria-label"] = (
f"New: {ANNOUNCEMENT_TITLE} — Read more (opens in a new tab)"
)
link.children = link.children[:1]
link.children[0].children[0].custom_attrs["aria-hidden"] = True
message = link.children[0].children[1]
message.children = rx.fragment(ANNOUNCEMENT_TITLE).children
message.custom_attrs["data-shimmer-text"] = ANNOUNCEMENT_TITLE
dismiss.custom_attrs["data-announcement-dismiss"] = ""
component.children.insert(
0,
rx.el.div(
*[rx.el.span() for _ in range(5)],
class_name="docs-announcement-glass",
aria_hidden=True,
),
)
return
for child in component.children:
match_marketing_announcement(child)


def announcement_visibility_script() -> str:
"""Restore marketing's release-scoped dismissal before the page paints."""
return """(() => {
const release = __RELEASE__;
const key = 'reflex_announcement_dismissed_release';
const sync = () => {
let dismissed = false;
try { dismissed = localStorage.getItem(key) === release; } catch (_) {}
if (dismissed) document.documentElement.dataset.announcementDismissed = 'true';
else delete document.documentElement.dataset.announcementDismissed;
};
sync();
if (window.__xyAnnouncementVisibilityInstalled) return;
window.__xyAnnouncementVisibilityInstalled = true;
document.addEventListener('click', (event) => {
if (!(event.target instanceof Element) ||
!event.target.closest('.docs-nav-frame [data-announcement-dismiss]')) return;
try { localStorage.setItem(key, release); } catch (_) {}
document.documentElement.dataset.announcementDismissed = 'true';
}, true);
window.addEventListener('storage', (event) => {
if (event.key === key || event.key === null) sync();
});
})();""".replace("__RELEASE__", json.dumps(ANNOUNCEMENT_RELEASE))
3 changes: 3 additions & 0 deletions docs/app/xy_docs/navbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from reflex_site_shared.components.icons import get_icon
from reflex_site_shared.components.marketing_button import button

from xy_docs.announcement import match_marketing_announcement

XY_REPOSITORY_URL = "https://github.com/reflex-dev/xy"

_XY_NAV_LINKS = (
Expand Down Expand Up @@ -211,6 +213,7 @@ def xy_docs_navbar() -> rx.Component:
),
_navigation_menu(),
)
match_marketing_announcement(frame.children[0])
header = frame.children[1]
header.class_name = "docs-navbar-header " + header.class_name
header.children[0].class_name = "docs-navbar-inner " + header.children[0].class_name
Expand Down
Loading
Loading