diff --git a/docs/app/assets/docs-header-theme.css b/docs/app/assets/docs-header-theme.css new file mode 100644 index 00000000000..9b17939ef61 --- /dev/null +++ b/docs/app/assets/docs-header-theme.css @@ -0,0 +1,204 @@ +/* Docs keep a 64px fixed header and a 40px announcement contribution. + The banner artwork and glass treatment match the marketing site. */ +.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%); +} +.docs-nav-frame > .docs-navbar-header { + height: 64px; + padding-inline: 32px; + background: var(--background); + border-bottom: 1px solid var(--border-subtle); + border-radius: 0; + box-shadow: none; + transition: border-radius 200ms ease, background-color 200ms ease; +} +.docs-nav-frame:has([data-announcement]) > .docs-navbar-header { + margin-top: -24px; + border-radius: 24px 24px 0 0; +} +.docs-nav-frame > .docs-navbar-header > .docs-navbar-inner { + max-width: 1536px; +} +.docs-navbar-wordmark { width: 102px; height: auto; } +.docs-navbar-docs-wordmark { height: 16px; width: auto; } +.docs-navbar-brand { gap: 12px; } +.docs-navbar-mobile-panel { + padding: 12px 24px 20px; + border-bottom: 1px solid var(--border); + border-radius: 0 0 24px 24px; + box-shadow: 0 12px 24px -16px rgb(0 0 0 / 20%); +} +.docs-navbar-mobile-link { padding-inline: 8px; } +.docs-navbar-mobile-link svg { color: var(--muted-foreground); } + +/* The in-page mobile drawer shares the desktop sidebar's typography. */ +.docs-sidebar-drawer { font-family: var(--font-sans); color: var(--foreground); } +.docs-sidebar-drawer p, +.docs-sidebar-drawer h3, +.docs-sidebar-drawer summary { font-weight: 400; } +.docs-sidebar-drawer h2 { + font-family: var(--font-sans); + font-size: 12px; + font-weight: 500; + letter-spacing: .025em; + text-transform: none; +} +.docs-sidebar-drawer .docs-sidebar-drawer-title { + font-size: 14px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0; +} +.docs-sidebar-drawer #sidebar-container { min-height: 0; } +.docs-sidebar-drawer #sidebar-container > div { padding-left: 16px; padding-right: 20px; } +.docs-sidebar-drawer a:where(:not([aria-current='page'], [aria-current='true'])) p { + color: var(--muted-foreground); +} +.docs-sidebar-drawer a:hover p { color: var(--foreground); } +.docs-sidebar-drawer a[aria-current='page'] p { font-weight: 500; } +.docs-sidebar-drawer a:focus-visible, +.docs-sidebar-drawer summary:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 3px; +} +@layer utilities { + html .docs-sidebar-drawer { + top: var(--docs-header-height) !important; + height: calc(100dvh - var(--docs-header-height)) !important; + } +} + +.docs-nav-frame .docs-announcement { + position: relative; + isolation: isolate; + height: 64px; + padding-bottom: 24px; + width: 100%; + overflow: hidden; + background: #e8e7f1; + color: #000; + border: 0; + font-family: Inter, Arial, sans-serif; +} +.docs-announcement-glass { + display: flex; + position: absolute; + z-index: -2; + inset: -48px -12%; + pointer-events: none; + 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/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-announcement::after { + content: ''; + position: absolute; + inset: 0; + z-index: -1; + pointer-events: none; + background: radial-gradient(ellipse 34% 90% at 50% 36%, rgb(255 255 255 / 30%), transparent), + linear-gradient(180deg, rgb(255 255 255 / 28%), transparent 45%); + box-shadow: inset 0 1px 0 rgb(255 255 255 / 75%); +} +.docs-announcement-link { + display: block; + height: 100%; + width: 100%; + color: #000; + background: transparent; +} +.docs-announcement-content { + display: flex; + height: 100%; + width: 100%; + margin-inline: auto; + padding-inline: 48px; + align-items: center; + justify-content: center; + gap: 10px; +} +.docs-announcement-badge { + display: inline-flex; + flex-shrink: 0; + align-items: center; + height: 20px; + padding-inline: 8px; + border-radius: 999px; + background: #000; + color: #fff; + font: 400 12px/18px Inter, Arial, sans-serif; +} +.docs-announcement-message { + color: #000; + font: 400 14px/20px Inter, Arial, sans-serif; + white-space: nowrap; +} +.docs-announcement-cta { + flex-shrink: 0; + padding: 5px 12px; + border-radius: 999px; + background: #fff; + color: #000; + font: 400 13px/20px Inter, Arial, sans-serif; + box-shadow: var(--docs-control-shadow); + white-space: nowrap; +} +.docs-announcement-dismiss { + position: absolute; + top: 20px; + right: 16px; + transform: translateY(-50%); + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: 0; + border-radius: 999px; + background: transparent; + color: #000; + cursor: pointer; +} +.docs-announcement-dismiss:hover { background: rgb(255 255 255 / 35%); } +.docs-announcement-link:focus-visible, +.docs-announcement-dismiss:focus-visible { outline: 2px solid #000; } +.docs-announcement-link:focus-visible { outline-offset: -4px; } +.docs-announcement-dismiss:focus-visible { outline-offset: 2px; } +@keyframes docs-announcement-glass-drift { + from { transform: translate3d(-5%, 0, 0) scale(1.04); } + to { transform: translate3d(5%, 0, 0) scale(1); } +} +@media (max-width: 1279px) { + .docs-nav-frame:has(.docs-navbar-menu[open]) > .docs-navbar-header { + border-radius: 0; + } +} +@media (max-width: 640px) { + .docs-announcement-cta { display: none; } +} +@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-wordmark { width: 92px; } + .docs-navbar-mobile-panel { padding-inline: 16px; } + .docs-announcement-content { padding-inline: 40px; gap: 8px; } + .docs-announcement-dismiss { right: 8px; } +} +@media (max-width: 379px) { + .docs-announcement-message { font-size: 13px; } +} +@media (prefers-reduced-motion: reduce) { + .docs-announcement-glass { animation: none; will-change: auto; } + .docs-nav-frame > .docs-navbar-header, + .docs-navbar-mobile-link { transition: none; } +} diff --git a/docs/app/assets/docs-overlays-theme.css b/docs/app/assets/docs-overlays-theme.css new file mode 100644 index 00000000000..5fb60ed7e40 --- /dev/null +++ b/docs/app/assets/docs-overlays-theme.css @@ -0,0 +1,338 @@ +/* Documentation chrome only. Live component examples keep their own styles. */ + +/* Search ships its own inline stylesheet, so these selectors deliberately + include the overlay/root to take precedence in both color modes. */ +html .ReflexSearch-root .ReflexSearch-button { + background: var(--background); + border-color: var(--border); + box-shadow: none; + font-family: var(--font-sans); + font-weight: 400; +} + +html .ReflexSearch-root .ReflexSearch-button:hover { + background: var(--muted); + border-color: var(--border-strong); +} + +html .ReflexSearch-root .ReflexSearch-shortcut { + background: var(--muted); + border-color: var(--border-subtle); + color: var(--muted-foreground); + font-weight: 400; +} + +html .ReflexSearch-overlay { + background: rgb(21 20 19 / 36%); + backdrop-filter: blur(6px); +} + +html .ReflexSearch-overlay .ReflexSearch-dialog { + background: var(--background); + border-color: var(--border); + border-radius: 24px; + box-shadow: 0 20px 64px rgb(0 0 0 / 14%); + font-family: var(--font-sans); +} + +html .ReflexSearch-overlay .ReflexSearch-inputRow { + height: 64px; + min-height: 64px; + padding-inline: 24px; +} + +html .ReflexSearch-overlay .ReflexSearch-dialog[data-results-visible="true"] .ReflexSearch-inputRow { + border-bottom-color: var(--border-subtle); +} + +html .ReflexSearch-overlay .ReflexSearch-input, +html .ReflexSearch-overlay .ReflexSearch-input::placeholder, +html .ReflexSearch-overlay .ReflexSearch-escape, +html .ReflexSearch-overlay .ReflexSearch-hit, +html .ReflexSearch-overlay .ReflexSearch-footer { + font-weight: 400; +} + +html .ReflexSearch-overlay .ReflexSearch-escape { + background: var(--muted); + border-color: var(--border); + border-radius: 6px; + color: var(--muted-foreground); +} + +html .ReflexSearch-overlay .ReflexSearch-escape:hover { + background: var(--accent-hover); + color: var(--foreground); +} + +html .ReflexSearch-overlay .ReflexSearch-hit { + background: transparent; + border-color: transparent; +} + +html .ReflexSearch-overlay .ReflexSearch-hit[data-selected="true"], +html .ReflexSearch-overlay .ReflexSearch-hit:hover { + background: var(--muted); + border-color: var(--border); +} + +html .ReflexSearch-overlay .ReflexSearch-hit:active { + background: var(--accent-hover); +} + +html .ReflexSearch-overlay .ReflexSearch-hitTitleRow strong { + font-weight: 500; +} + +html .ReflexSearch-overlay .ReflexSearch-hitDescription, +html .ReflexSearch-overlay .ReflexSearch-hitBreadcrumbs, +html .ReflexSearch-overlay .ReflexSearch-emptyState { + color: var(--muted-foreground); +} + +html .ReflexSearch-overlay .ReflexSearch-emptyState strong { + font-family: var(--font-heading); + font-size: 24px; + font-weight: 300; + line-height: 1.25; + letter-spacing: -0.025em; +} + +html .ReflexSearch-overlay .ReflexSearch-emptyIcon { + box-shadow: none; +} + +html .ReflexSearch-overlay .ReflexSearch-footer { + background: var(--subtle); + border-top-color: var(--border-subtle); + color: var(--muted-foreground); + padding-inline: 24px; +} + +html .ReflexSearch-overlay .ReflexSearch-footer a { + color: var(--muted-foreground); +} + +html .ReflexSearch-overlay .ReflexSearch-footer a:hover { + color: var(--foreground); +} + +html .ReflexSearch-overlay :is(.ReflexSearch-escape, .ReflexSearch-hit, .ReflexSearch-footer a):focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; +} + +/* Split page actions remain a single compact control. */ +.docs-page-actions > button, +.docs-page-actions > [data-slot="popover-trigger"] { + min-height: 36px; + background: var(--background); + border-color: var(--border); + box-shadow: none; + font-family: var(--font-sans); + font-weight: 400; +} + +.docs-page-actions > button:first-child { + border-radius: 999px 0 0 999px; + padding-left: 14px; +} + +.docs-page-actions > [data-slot="popover-trigger"] { + border-radius: 0 999px 999px 0; + padding-right: 12px; +} + +.docs-page-actions > button:hover { + background: var(--muted); +} + +.docs-page-actions > button:focus-visible { + position: relative; + z-index: 1; + outline: 2px solid var(--ring); + outline-offset: 2px; +} + +.docs-page-actions-menu { + background: var(--card); + border-color: var(--border); + border-radius: 20px; + padding: 8px; + box-shadow: 0 8px 32px rgb(0 0 0 / 8%); + font-family: var(--font-sans); +} + +.docs-page-actions-menu > div > :is(a, button) { + border-radius: 12px; + padding: 12px; +} + +.docs-page-actions-menu .font-medium { + font-weight: 400; +} + +/* Both feedback entry points render this same content inside a portal. */ +[data-slot="popover-popup"]:has(> .docs-feedback-content) { + width: 336px; + max-width: calc(100vw - 32px); + padding: 0; + background: var(--card); + border: 1px solid var(--border); + border-radius: 20px; + color: var(--foreground); + box-shadow: 0 8px 32px rgb(0 0 0 / 8%); + font-family: var(--font-sans); +} + +.docs-feedback-content { + padding: 20px; +} + +.docs-feedback-content div:has(> input[data-slot="input"]), +.docs-feedback-content textarea { + background: var(--background); + border-color: var(--border); + border-radius: 12px; + color: var(--foreground); + box-shadow: none; + font-family: var(--font-sans); + font-size: 14px; + font-weight: 400; +} + +.docs-feedback-content textarea { + min-height: 128px; + padding: 12px; + line-height: 1.5; +} + +.docs-feedback-content div:has(> input[data-slot="input"]) { + min-height: 40px; +} + +.docs-feedback-content input { + background: transparent; + color: var(--foreground); + font-family: var(--font-sans); + font-size: 14px; + font-weight: 400; +} + +.docs-feedback-content :is(input, textarea)::placeholder { + color: var(--muted-foreground); +} + +.docs-feedback-content button { + font-family: var(--font-sans); + font-weight: 400; + box-shadow: none; +} + +.docs-feedback-content button[type="submit"] { + min-height: 40px; + background: var(--primary); + color: var(--primary-foreground); + border-color: transparent; +} + +.docs-feedback-content button[type="submit"]:enabled:hover { + background: var(--primary-hover); +} + +.docs-feedback-content :is(textarea, button):focus-visible, +.docs-feedback-content div:has(> input[data-slot="input"]:focus-visible) { + outline: 2px solid var(--ring); + outline-offset: 2px; +} + +.docs-feedback-content textarea:is([data-invalid], [aria-invalid="true"], :user-invalid), +.docs-feedback-content div:has(> input:is([data-invalid], [aria-invalid="true"], :user-invalid)) { + border-color: var(--destructive-10); +} + +.docs-feedback-content :is(input, textarea):disabled { + color: var(--muted-foreground); + cursor: not-allowed; +} + +.docs-hint { + background: var(--foreground); + border: 1px solid var(--foreground); + border-radius: 8px; + color: var(--background); + box-shadow: none; + padding: 6px 10px; + font-family: var(--font-sans); + font-size: 12px; + font-weight: 400; + line-height: 1.5; +} + +/* Footers use body typography, including utility text inherited from the + older monospace shell. Keep service-status colors and theme states intact. */ +.docs-page-footer, +.docs-page-footer .font-small, +.marketing-footer { + font-family: var(--font-sans); + font-weight: 400; +} + +.docs-page-footer .font-small { + font-size: 12px; + line-height: 20px; + letter-spacing: normal; +} + +.docs-page-footer button { + font-family: var(--font-sans); + font-weight: 400; + box-shadow: none; +} + +.docs-page-footer :is(a, button):focus-visible { + outline: 2px solid var(--ring); + outline-offset: 4px; +} + +.marketing-footer h2 { + font-family: var(--font-sans); + font-weight: 400; + letter-spacing: normal; +} + +.marketing-footer input[type="email"] { + font-family: var(--font-sans); + font-weight: 400; +} + +@media (max-width: 40rem) { + html .ReflexSearch-overlay .ReflexSearch-dialog { + border-radius: 20px; + } + + html .ReflexSearch-overlay .ReflexSearch-inputRow, + html .ReflexSearch-overlay .ReflexSearch-footer { + padding-inline: 16px; + } + + html .ReflexSearch-overlay .ReflexSearch-escape { + border-radius: 10px; + } + + .docs-feedback-content { + padding: 16px; + } +} + +@media (prefers-reduced-motion: reduce) { + .docs-page-actions > button, + .docs-page-actions-menu, + .docs-page-actions-menu > div > :is(a, button), + [data-slot="popover-popup"]:has(> .docs-feedback-content), + .docs-feedback-content :is(input, textarea, button), + .docs-hint { + transition: none; + animation: none; + } +} diff --git a/docs/app/assets/docs-theme.css b/docs/app/assets/docs-theme.css new file mode 100644 index 00000000000..a80fa0fdeaa --- /dev/null +++ b/docs/app/assets/docs-theme.css @@ -0,0 +1,291 @@ +/* Docs-only brand layer. Shared package consumers retain their own theme. + Editorial selectors deliberately exclude live component examples. */ +/* Match marketing with variable WOFF2 subsets bundled from Fontsource. */ +@font-face { + font-family: 'Geist'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url(@fontsource-variable/geist/files/geist-latin-ext-wght-normal.woff2) format('woff2-variations'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'Geist'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url(@fontsource-variable/geist/files/geist-latin-wght-normal.woff2) format('woff2-variations'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url(@fontsource-variable/inter/files/inter-latin-ext-wght-normal.woff2) format('woff2-variations'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url(@fontsource-variable/inter/files/inter-latin-wght-normal.woff2) format('woff2-variations'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +:root { + --font-instrument-sans: Inter, Arial, sans-serif; + --font-sans: Inter, Arial, sans-serif; + --font-heading: Geist, Inter, Arial, sans-serif; + --font-weight-book: 400; + --font-weight-normal: 400; + --radius-card: 20px; + --radius-panel: 24px; +} + +:root, .light, .light-theme { + --background: #fdfcfc; + --foreground: #000; + --card: #fff; + --card-foreground: #000; + --primary: #000; + --primary-foreground: #fff; + --primary-hover: #2b2b2b; + --muted: #f5f3f1; + --muted-foreground: #69645e; + --subtle: #f9f8f7; + --subtle-foreground: #777169; + --accent: #f5f3f1; + --accent-hover: #ebe8e4; + --inset: #ebe8e4; + --border: #e5e2de; + --border-subtle: #efeeec; + --border-strong: #cbc5bf; + --ring: #5c52b4; +} + +.dark, .dark-theme { + --background: #151413; + --foreground: #f5f3f1; + --card: #1d1c1a; + --card-foreground: #f5f3f1; + --primary: #f5f3f1; + --primary-foreground: #151413; + --primary-hover: #ded9d2; + --muted: #22211f; + --muted-foreground: #b7b1a9; + --subtle: #1b1a18; + --subtle-foreground: #a39d95; + --accent: #292724; + --accent-hover: #35322e; + --inset: #35322e; + --border: #393631; + --border-subtle: #2c2a27; + --border-strong: #5c5750; + --ring: #afa5f4; +} + +.font-heading { font-family: var(--font-heading); } + +/* Article shell: quieter navigation, a comfortable line length, useful TOC. */ +.docs-reading-layout { max-width: 1536px; } +.docs-reading-layout .docs-sidebar { width: 272px; } +.docs-sidebar::before { background: var(--background); } +.docs-sidebar #sidebar-container > div { padding-left: 16px; padding-right: 20px; } +.docs-sidebar p, +.docs-sidebar h3, +.docs-sidebar summary { font-weight: 400; } +.docs-sidebar h2 { + font-family: var(--font-sans); + font-size: 12px; + font-weight: 500; + letter-spacing: .025em; + text-transform: none; +} +.docs-sidebar a:where(:not([aria-current='page'], [aria-current='true'])) p { + color: var(--muted-foreground); +} +.docs-sidebar a:hover p { color: var(--foreground); } +.docs-sidebar a[aria-current='page'] p { font-weight: 500; } +.docs-sidebar a:focus-visible, +.docs-sidebar summary:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 3px; +} +.docs-reading-column { max-width: 896px; padding-inline: 48px; } +.docs-reading-column .docs-prose { + font-size: 15px; + font-weight: 400; + line-height: 1.85; + letter-spacing: -.006em; +} +.docs-reading-column .docs-heading { + min-width: 0; + overflow-wrap: anywhere; + font-family: var(--font-heading); + font-weight: 400; + letter-spacing: -.035em; + line-height: 1.18; +} +.docs-reading-column h1.docs-heading { + font-size: clamp(34px, 3vw, 44px); + font-weight: 300; + margin-top: 8px; + margin-bottom: 12px; +} +.docs-reading-column h2.docs-heading { font-size: 30px; margin-top: 48px; } +.docs-reading-column h3.docs-heading { font-size: 23px; margin-top: 32px; } +.docs-reading-column h4.docs-heading { font-size: 17px; font-weight: 500; } +.docs-article .code-block { + border-color: var(--border); +} +.docs-reading-column .docs-page-navigation { + justify-content: space-between; + gap: 16px; + margin-top: 32px; + margin-bottom: 24px; +} +.docs-page-navigation > a { + gap: 2px; + padding: 10px 16px; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--background); + transition: background-color 160ms ease, border-color 160ms ease; +} +.docs-page-navigation > a > span:first-child { + gap: 6px; + font-size: 12px; + font-weight: 400; + line-height: 16px; +} +.docs-page-navigation > a > span:first-child svg { width: 14px; height: 14px; } +.docs-page-navigation > a > span:last-child { + font-size: 14px; + font-weight: 400; + line-height: 20px; + overflow-wrap: anywhere; +} +.docs-page-navigation > a:hover { + background: var(--muted); + border-color: var(--border-strong); +} +.docs-reading-layout .docs-toc { width: 220px; padding-right: 24px; } +.docs-toc p, .docs-toc a { font-size: 12px; font-weight: 400; line-height: 1.65; } +.docs-toc p { font-weight: 500; } +.docs-toc #toc-navigation { gap: 4px; } + +.docs-landing footer > div { max-width: 1176px; padding-inline: 32px; } +.docs-landing footer { border-top: 1px solid var(--border-subtle); } + +@media (min-width: 1280px) { + .docs-reading-layout .docs-toc { display: block; } +} +@media (max-width: 1279px) { + .docs-reading-column { padding-inline: 32px; } +} +@media (max-width: 1023px) { + .docs-reading-column { max-width: 800px; padding-inline: 24px; } + .docs-reading-column .docs-breadcrumb { margin-bottom: 28px; } +} +@media (max-width: 639px) { + .docs-reading-column { padding-inline: 20px; } + .docs-reading-column .docs-prose { font-size: 15px; } + .docs-reading-column h2.docs-heading { font-size: 26px; } + .docs-page-navigation > a { padding: 10px 12px; } + .docs-reading-column .docs-page-navigation { gap: 8px; } + .docs-landing footer > div { padding-inline: 16px; } +} +@media (prefers-reduced-motion: reduce) { + .docs-page-navigation > a { transition: none; } +} + +/* Documentation surfaces only: never select controls inside live examples. */ +.docs-demo-header { align-items: center; gap: 12px; margin-bottom: 12px; } +.docs-demo-header > a { margin: 0; font-family: var(--font-sans); font-weight: 400; font-size: 13px; } +.docs-article .doc-demo-tab-card { + border-radius: var(--radius-card); + border-color: var(--border); + background: var(--muted); +} +.docs-article .docs-demo-preview { border-radius: var(--radius-card); } +.docs-article :is(.docs-demo-preview, .docs-api-preview) { + align-items: safe center; +} +.docs-article .docs-callout { border-radius: 16px; } +.docs-article .docs-callout > div { font-weight: 400; } +.docs-markdown-table { border-radius: 16px; overflow-x: auto; } +.docs-markdown-table table { background: var(--background); border: 0; } +.docs-markdown-table thead { background: var(--muted); } +.docs-markdown-table :is(th, td) { font-size: 13px; line-height: 1.7; padding: 12px 16px; } +.docs-markdown-table th { font-weight: 500; color: var(--foreground); } +.docs-markdown-table td { font-weight: 400; } +.docs-markdown-table tbody tr:last-child { border-bottom: 0; } +.docs-article .code-style { border-color: var(--border); } + +/* Existing tab and code utilities use !important; keep overrides in their layer. */ +@layer utilities { + .docs-article .doc-demo-tab-list { + width: fit-content; + flex-shrink: 0; + margin: 0; + gap: 2px !important; + padding: 3px !important; + border: 1px solid var(--border) !important; + border-radius: 999px; + background: var(--muted) !important; + } + html .docs-article .doc-demo-tab { + border: 0 !important; + border-radius: 999px !important; + margin: 0; + padding: 7px 12px !important; + min-height: 32px; + font-family: var(--font-sans); + font-size: 13px !important; + font-weight: 400 !important; + background: transparent !important; + color: var(--muted-foreground) !important; + } + html .docs-article .doc-demo-tab[data-state='active'] { + background: var(--card) !important; + color: var(--foreground) !important; + box-shadow: 0 1px 3px rgb(0 0 0 / .06) !important; + } + html .docs-article .doc-demo-tab:hover { color: var(--foreground) !important; } + html .docs-article .doc-demo-tab:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; + } + .docs-article .code-block:not(.doc-demo-tab-card .code-block) { border-radius: 16px !important; } + .docs-article .code-block button { + font-family: var(--font-sans) !important; + font-weight: 400 !important; + border-radius: 999px !important; + background: var(--muted) !important; + } + .docs-article .code-block button:focus-visible { + outline: 2px solid var(--ring) !important; + outline-offset: 2px; + } + @media (hover: none) { + .docs-article .code-block button { opacity: 1 !important; pointer-events: auto; } + .docs-article .code-block:has(button) { padding-top: 48px !important; } + } + @media (prefers-reduced-motion: reduce) { + .docs-article .doc-demo-tab, .docs-article .code-block button { transition: none !important; } + } +} + +.docs-breadcrumb :is(a, span) { font-weight: 400; } +.docs-article .docs-table { box-shadow: none; border-radius: 16px; } + +.docs-markdown-table { box-shadow: none; } +.docs-markdown-table :is(th, td) { box-shadow: none; border-bottom: 1px solid var(--border-subtle); } +.docs-markdown-table th { background: var(--muted); } +.docs-markdown-table tbody tr { background: var(--background); } +.docs-markdown-table tbody tr:last-child td { border-bottom: 0; } diff --git a/docs/app/assets/header/opal.avif b/docs/app/assets/header/opal.avif new file mode 100644 index 00000000000..1dc0988c83e Binary files /dev/null and b/docs/app/assets/header/opal.avif differ diff --git a/docs/app/design.md b/docs/app/design.md index 0845240f7de..ad6e31fac59 100644 --- a/docs/app/design.md +++ b/docs/app/design.md @@ -1,7 +1,7 @@ # Docs design foundations Adapted from [the marketing design contract](https://github.com/reflex-dev/marketing/blob/main/design.md), `assets/tailwind-theme.css`, and the neutral ramp in `assets/marketing.css`. -The shared source is `packages/reflex-site-shared/src/reflex_site_shared/styles/assets/tailwind-theme.css`; edit it rather than generated `.web` files. +The shared base is `packages/reflex-site-shared/src/reflex_site_shared/styles/assets/tailwind-theme.css`. The docs app's current marketing treatment lives in `assets/docs-theme.css`, followed by `assets/docs-header-theme.css` for navigation and `assets/docs-overlays-theme.css` for search, feedback, and footer controls. All three load after the shared base. Keep these app-specific overrides out of the shared theme so package documentation sites retain their own branding. Never edit generated `.web` files. | Role | Classes | | --- | --- | @@ -16,10 +16,12 @@ The shared source is `packages/reflex-site-shared/src/reflex_site_shared/styles/ | Strong boundary | `border-border-strong` | | Keyboard focus | `focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring` | -Use Instrument Sans for text and JetBrains Mono for code. Marketing typography roles (`text-hero`, `text-page-title`, `text-section`, `text-caption`) are available; retain docs-specific content sizing where appropriate. Body weight is 425, editorial headings 450, and controls 500. +Use Inter for text, Geist for editorial headings, and JetBrains Mono for code. Inter and Geist use pinned Fontsource variable WOFF2 packages with Latin subsets and `font-display: swap`, bundled through `assets/docs-theme.css`. Body and controls use weight 400; page titles use Geist 300. Article body is 15px with a 1.85 line height; retain a comfortable reading width instead of using the marketing site's large display sizing for prose. -Use capsule buttons (`rounded-control`), 12px inputs (`rounded-xl`), 14px cards (`rounded-card`), and 16px panels (`rounded-panel`). Keep primary and ghost actions flat; outlined actions use a solid border and `shadow-small`. Preserve labels, links, disabled states, and keyboard interaction. +Use capsule buttons (`rounded-control`), 12px inputs (`rounded-xl`), 20px cards (`rounded-card`), and 24px panels (`rounded-panel`). Keep primary and ghost actions flat; outlined actions use a solid border. Preserve labels, links, disabled states, and keyboard interaction. -The light palette matches marketing: white surfaces, #181818 foreground, #595959 descriptive text, #767676 metadata, and #e2e2e2 control borders. Docs also retain a neutral dark palette. Use foreground/background pairs so controls remain legible in both modes. Keep status and illustration colors when they convey meaning. +The light palette matches marketing: #fdfcfc canvas, #f5f3f1 quiet surfaces, black foreground, #e5e2de boundaries, and #efeeec separators. Docs use a slightly darker #69645e for small descriptive text and #777169 for metadata. Docs retain a warm dark palette. Use foreground/background pairs so controls remain legible in both modes. Keep status and syntax colors when they convey meaning. -New UI must not use numbered primary/secondary colors. Their CSS aliases remain for internal controls and downstream consumers during migration. This pass changes foundations and colors; existing docs layouts and artwork remain independently maintained. +New UI must not use numbered primary/secondary colors. Their CSS aliases remain for internal controls and downstream consumers. Scope editorial rules to `.docs-heading`, `.docs-prose`, and named shell hooks. Never target every heading, paragraph, or button inside an article: live examples must retain their own typography and interactions. Respect reduced-motion preferences and keep focus indicators visible. + +The announcement banner reuses the marketing opal artwork in `assets/header/opal.avif`. Docs retain a compact 64px header with a 40px visible announcement. Mobile navigation and article offsets follow `--docs-header-height`; dismissal removes only the announcement contribution. diff --git a/docs/app/news/+marketing-style.docs.md b/docs/app/news/+marketing-style.docs.md new file mode 100644 index 00000000000..b3d97f690a2 --- /dev/null +++ b/docs/app/news/+marketing-style.docs.md @@ -0,0 +1 @@ +Align the documentation site with the marketing brand: Inter and Geist typography, the opal announcement banner, warm neutral surfaces, consistent controls, search, catalog cards, recipes, code examples, tables, callouts, compact Back/Next links, and footers. Preserve dark mode, keyboard access, mobile navigation, reduced motion, and live examples. Load the brand fonts as subsetted variable WOFF2 files and use the optimized AVIF announcement texture. diff --git a/docs/app/reflex.lock/bun.lock b/docs/app/reflex.lock/bun.lock index 3f0c95fa36c..fcd370a00f5 100644 --- a/docs/app/reflex.lock/bun.lock +++ b/docs/app/reflex.lock/bun.lock @@ -6,6 +6,8 @@ "name": "reflex", "dependencies": { "@base-ui/react": "1.9.0", + "@fontsource-variable/geist": "5.3.0", + "@fontsource-variable/inter": "5.3.0", "@fontsource-variable/instrument-sans": "5.3.0", "@fontsource-variable/jetbrains-mono": "5.3.0", "@glideapps/glide-data-grid": "6.0.3", @@ -46,7 +48,6 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "19.3.0", - "react-fast-marquee": "1.6.5", "react-helmet": "6.1.0", "react-leaflet": "5.0.0", "react-markdown": "10.1.0", @@ -177,6 +178,10 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="], + "@fontsource-variable/geist": ["@fontsource-variable/geist@5.3.0", "", {}, "sha512-j0m+vLQuG5XAYoHtGCVu0spvlGreR3EzpECUVzkFmI1mTVnAO38l/NEPDCFgZ177JxzYJCLSmTQibIiYPilGrA=="], + + "@fontsource-variable/inter": ["@fontsource-variable/inter@5.3.0", "", {}, "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA=="], + "@fontsource-variable/instrument-sans": ["@fontsource-variable/instrument-sans@5.3.0", "", {}, "sha512-u4gKbDBTNFGkg997tfQn3eHOhHuquWUFTRT/rwzuKtrxX5P2ekfs2x+LgBPP4P32+cC+vUwF1Cr+IdRoPQbrGw=="], "@fontsource-variable/jetbrains-mono": ["@fontsource-variable/jetbrains-mono@5.3.0", "", {}, "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw=="], @@ -1489,8 +1494,6 @@ "react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], - "react-fast-marquee": ["react-fast-marquee@1.6.5", "", { "peerDependencies": { "react": ">= 16.8.0 || ^18.0.0", "react-dom": ">= 16.8.0 || ^18.0.0" } }, "sha512-swDnPqrT2XISAih0o74zQVE2wQJFMvkx+9VZXYYNSLb/CUcAzU9pNj637Ar2+hyRw6b4tP6xh4GQZip2ZCpQpg=="], - "react-helmet": ["react-helmet@6.1.0", "", { "dependencies": { "object-assign": "^4.1.1", "prop-types": "^15.7.2", "react-fast-compare": "^3.1.1", "react-side-effect": "^2.1.0" }, "peerDependencies": { "react": ">=16.3.0" } }, "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw=="], "react-html-attributes": ["react-html-attributes@1.4.6", "", { "dependencies": { "html-element-attributes": "^1.0.0" } }, "sha512-uS3MmThNKFH2EZUQQw4k5pIcU7XIr208UE5dktrj/GOH1CMagqxDl4DCLpt3o2l9x+IB5nVYBeN3Cr4IutBXAg=="], diff --git a/docs/app/reflex.lock/package.json b/docs/app/reflex.lock/package.json index e9ab656ea45..7e05c1ec056 100644 --- a/docs/app/reflex.lock/package.json +++ b/docs/app/reflex.lock/package.json @@ -7,6 +7,8 @@ }, "dependencies": { "@base-ui/react": "1.9.0", + "@fontsource-variable/geist": "5.3.0", + "@fontsource-variable/inter": "5.3.0", "@fontsource-variable/instrument-sans": "5.3.0", "@fontsource-variable/jetbrains-mono": "5.3.0", "@glideapps/glide-data-grid": "6.0.3", @@ -47,7 +49,6 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "19.3.0", - "react-fast-marquee": "1.6.5", "react-helmet": "6.1.0", "react-leaflet": "5.0.0", "react-markdown": "10.1.0", diff --git a/docs/app/reflex_docs/components/docpage/navbar/buttons/sidebar.py b/docs/app/reflex_docs/components/docpage/navbar/buttons/sidebar.py index 65bbb206000..33bcc967e9d 100644 --- a/docs/app/reflex_docs/components/docpage/navbar/buttons/sidebar.py +++ b/docs/app/reflex_docs/components/docpage/navbar/buttons/sidebar.py @@ -18,7 +18,7 @@ def navbar_sidebar_button() -> rx.Component: "Cancel01Icon", class_name="hidden size-5 group-open/docs-menu:block" ), aria_label="Toggle navigation menu", - class_name="flex size-9 cursor-pointer list-none items-center justify-center rounded-full text-foreground hover:bg-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring [&::-webkit-details-marker]:hidden", + class_name="docs-navbar-menu-trigger flex size-9 cursor-pointer list-none items-center justify-center rounded-full text-foreground hover:bg-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring [&::-webkit-details-marker]:hidden", ), rx.el.div( *[ @@ -30,7 +30,7 @@ def navbar_sidebar_button() -> rx.Component: class_name="size-4 shrink-0", ), href=href, - class_name="flex w-full items-center justify-between gap-3 border-b border-border px-4 py-4 text-base font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-2 focus-visible:outline-ring last:border-b-0", + class_name="docs-navbar-mobile-link flex w-full items-center justify-between gap-3 border-b border-border px-4 py-4 text-base font-normal text-foreground transition-colors duration-150 hover:bg-accent focus-visible:outline-2 focus-visible:outline-ring motion-reduce:transition-none last:border-b-0", ) for label, href in ( ("Overview", "/docs/"), @@ -43,9 +43,9 @@ def navbar_sidebar_button() -> rx.Component: ], aria_label="Documentation navigation", role="navigation", - class_name="fixed inset-x-0 top-[var(--docs-header-height)] max-h-[calc(100dvh-var(--docs-header-height))] overflow-y-auto border-b border-border-subtle bg-background px-6 py-4 shadow-small", + class_name="docs-navbar-mobile-panel fixed inset-x-0 top-[var(--docs-header-height)] max-h-[calc(100dvh-var(--docs-header-height))] overflow-y-auto border-b border-border-subtle bg-background px-6 py-4 shadow-small", ), - class_name="group/docs-menu", + class_name="docs-navbar-menu group/docs-menu", ) diff --git a/docs/app/reflex_docs/components/hint.py b/docs/app/reflex_docs/components/hint.py index 7c2db4c5c5d..4d0281c877b 100644 --- a/docs/app/reflex_docs/components/hint.py +++ b/docs/app/reflex_docs/components/hint.py @@ -16,7 +16,7 @@ def hint( rx.text(text), side=side, align=align, - class_name="flex justify-center items-center bg-muted-foreground px-1.5 py-0.5 rounded-lg font-small text-white-1", + class_name="docs-hint flex justify-center items-center bg-muted-foreground px-1.5 py-0.5 rounded-lg font-small text-white-1", ), class_name=class_name, default_open=active, diff --git a/docs/app/reflex_docs/pages/ai_landing.py b/docs/app/reflex_docs/pages/ai_landing.py index 4a78509f2ed..0a94c4ea817 100644 --- a/docs/app/reflex_docs/pages/ai_landing.py +++ b/docs/app/reflex_docs/pages/ai_landing.py @@ -42,9 +42,16 @@ def _workflow_preview(icon: rx.Component, labels: tuple[str, ...]) -> rx.Compone rx.el.div( *[ rx.el.div( - ui.icon(symbol, size=16, class_name="text-[var(--ai-accent)]"), - rx.el.span(label, class_name="text-sm text-foreground"), - class_name="flex items-center gap-3 rounded-sm bg-[var(--ai-tint)] px-4 py-3", + ui.icon( + symbol, + size=16, + class_name="shrink-0 text-muted-foreground", + ), + rx.el.span( + label, + class_name="min-w-0 text-sm font-normal leading-6 text-foreground", + ), + class_name="flex min-w-0 items-center gap-3 rounded-lg bg-muted px-4 py-3", ) for symbol, label in zip( ("Message01Icon", "Layout02Icon", "Layers01Icon"), @@ -57,7 +64,7 @@ def _workflow_preview(icon: rx.Component, labels: tuple[str, ...]) -> rx.Compone class_name="w-full max-w-xs overflow-hidden rounded-xl border border-border bg-background", ), aria_hidden=True, - class_name="docs-workflow-preview flex items-center justify-center p-7 sm:p-9", + class_name="docs-ai-workflow-preview flex items-center justify-center bg-muted p-6 sm:p-8", ) @@ -77,25 +84,26 @@ def _workflow_card( rx.el.div( rx.el.h2( title, - class_name="text-2xl font-book tracking-tight text-foreground", + class_name="font-heading text-2xl font-normal leading-8 tracking-[-0.025em] text-foreground", ), rx.el.span( "Recommended", - class_name="rounded-full bg-foreground px-2.5 py-1 text-xs font-book leading-4 text-background", + class_name="rounded-full bg-primary px-2.5 py-1 text-xs font-normal leading-4 text-primary-foreground", ) if recommended else rx.fragment(), class_name="flex flex-wrap items-center gap-x-3 gap-y-2", ), rx.el.p( - description, class_name="mt-3 text-base leading-7 text-muted-foreground" + description, + class_name="mt-3 text-sm font-normal leading-6 text-muted-foreground", ), rx.el.div( action, ui.icon("ArrowRight01Icon", size=16, aria_hidden=True), - class_name="mt-auto flex items-center gap-2 pt-7 text-sm font-book text-foreground", + class_name="mt-auto flex items-center gap-2 pt-7 text-sm font-normal text-foreground", ), - class_name="flex flex-1 flex-col border-t border-border bg-background p-6 sm:p-8", + class_name="flex flex-1 flex-col border-t border-border-subtle bg-background p-6 sm:p-7", ), href=href, aria_label=title, @@ -103,7 +111,7 @@ def _workflow_card( if recommended else None, custom_attrs={"data-tone": tone}, - class_name="docs-ai-workflow-card docs-ai-card flex min-w-0 flex-col overflow-hidden rounded-panel border border-border shadow-small dark:shadow-none transition-colors hover:border-border-strong focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring", + class_name="docs-ai-workflow-card flex min-w-0 flex-col overflow-hidden rounded-[20px] border border-border-subtle bg-background transition-colors hover:border-border-strong motion-reduce:transition-none focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-ring", ) diff --git a/docs/app/reflex_docs/pages/docs/component.py b/docs/app/reflex_docs/pages/docs/component.py index 6676ae11190..1c6bfd89137 100644 --- a/docs/app/reflex_docs/pages/docs/component.py +++ b/docs/app/reflex_docs/pages/docs/component.py @@ -90,29 +90,40 @@ class PropDocsState(rx.State): ] +_PILL_BTN_BASE_CLASS = ( + "docs-api-option inline-flex min-h-10 min-w-10 cursor-pointer items-center " + "justify-center rounded-full border border-transparent px-3 text-sm font-normal " + "transition-colors motion-reduce:transition-none focus-visible:outline-2 " + "focus-visible:outline-offset-2 focus-visible:outline-ring" +) _PILL_BTN_CLASS = ( - "inline-flex h-7 cursor-pointer items-center justify-center rounded-md " - "border border-border bg-background px-2.5 text-sm font-medium text-muted-foreground " - "transition-colors hover:border-border hover:bg-muted hover:text-foreground " - "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-strong" + f"{_PILL_BTN_BASE_CLASS} bg-muted text-muted-foreground " + "hover:bg-accent-hover hover:text-foreground" ) _PILL_BTN_ACTIVE_CLASS = ( - "inline-flex h-7 cursor-pointer items-center justify-center rounded-md " - "border border-border-strong bg-accent px-2.5 text-sm font-medium text-foreground " - "shadow-[inset_0_0_0_1px_var(--border)] transition-colors " - "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-strong" + f"{_PILL_BTN_BASE_CLASS} bg-primary text-primary-foreground hover:bg-primary-hover" ) _PROPS_TABLE_COMPACT_CELL_CLASS = ( "cell-content max-h-[4.25rem] overflow-hidden " "[mask-image:linear-gradient(to_bottom,black_82%,transparent)] " "[-webkit-mask-image:linear-gradient(to_bottom,black_82%,transparent)]" ) +_API_HEADING_CLASS = ( + "docs-heading font-heading text-2xl font-normal leading-tight " + "tracking-[-0.025em] text-foreground" +) +_API_LINK_CLASS = ( + "docs-text-link text-sm font-normal leading-6 text-foreground " + "underline underline-offset-4 focus-visible:outline-2 " + "focus-visible:outline-offset-4 focus-visible:outline-ring" +) def _pill_button(label: str, active, on_click) -> rx.Component: return rx.el.button( label, type="button", + aria_pressed=active, on_click=on_click, class_name=rx.cond(active, _PILL_BTN_ACTIVE_CLASS, _PILL_BTN_CLASS), ) @@ -121,7 +132,7 @@ def _pill_button(label: str, active, on_click) -> rx.Component: def _pill_row(values, var, setter) -> rx.Component: return rx.el.div( *[_pill_button(v, var == v, setter(v)) for v in values], - class_name="flex flex-wrap gap-1.5", + class_name="flex min-w-0 flex-wrap gap-2", ) @@ -129,7 +140,7 @@ def _bool_pills(var, setter) -> rx.Component: return rx.el.div( _pill_button("false", ~var, setter(False)), _pill_button("true", var, setter(True)), - class_name="flex flex-wrap gap-1.5", + class_name="flex min-w-0 flex-wrap gap-2", ) @@ -373,8 +384,8 @@ def prop_docs( size=14, class_name=rx.cond( expanded, - "row-expand-icon mt-0.5 shrink-0 rotate-180 text-subtle-foreground opacity-100 transition-[opacity,transform]", - "row-expand-icon mt-0.5 shrink-0 text-subtle-foreground opacity-0 transition-[opacity,transform] group-hover:opacity-100", + "row-expand-icon mt-0.5 shrink-0 rotate-180 text-subtle-foreground opacity-100 transition-[opacity,transform] motion-reduce:transition-none", + "row-expand-icon mt-0.5 shrink-0 text-subtle-foreground opacity-0 transition-[opacity,transform] group-hover:opacity-100 motion-reduce:transition-none", ), ) if expanded is not None @@ -426,7 +437,7 @@ def prop_docs( ), rx.text( f"Union[{', '.join(all_types)}]", - class_name="font-small text-muted-foreground", + class_name="text-sm font-normal leading-6 text-muted-foreground", ), ), ), @@ -438,7 +449,7 @@ def prop_docs( rx.box( render_inline_markdown( description, - class_name="font-small text-muted-foreground whitespace-normal leading-snug break-words", + class_name="text-sm font-normal text-muted-foreground whitespace-normal leading-6 break-words", ), class_name=cell_content_class, ), @@ -460,10 +471,10 @@ def generate_props( prop_list = list(props) if len(prop_list) == 0: return rx.box( - rx.heading("Props", as_="h3", class_name="font-large text-foreground"), + rx.heading("Props", as_="h3", class_name=_API_HEADING_CLASS), rx.text( "No component specific props", - class_name="text-muted-foreground font-base", + class_name="text-sm font-normal leading-6 text-muted-foreground", ), class_name="flex flex-col overflow-x-auto justify-start py-2 w-full", ) @@ -718,9 +729,9 @@ def _render_dynamic_prop(indent: str, p, var) -> rx.Component: rx.el.div( comp, class_name=( - "flex flex-col items-center justify-center p-6 flex-1 " + "docs-api-preview flex flex-col items-center justify-center p-6 flex-1 " "bg-muted border-b lg:border-b-0 lg:border-r " - "border-border-subtle min-w-0" + "border-border-subtle min-w-0 overflow-x-auto" ), ), rx.el.div( @@ -728,7 +739,7 @@ def _render_dynamic_prop(indent: str, p, var) -> rx.Component: class_name="flex-1 p-4 bg-background min-w-0 overflow-x-auto", ), class_name=( - "flex flex-col lg:flex-row w-full rounded-xl border " + "docs-api-playground flex min-w-0 flex-col lg:flex-row w-full rounded-[20px] border " "border-border-subtle overflow-hidden" ), ) @@ -745,17 +756,19 @@ def _render_dynamic_prop(indent: str, p, var) -> rx.Component: class_name="code-style text-nowrap leading-normal text-muted-foreground", ), control, + role="group", + aria_label=f"{prop.name} options", class_name=( - "grid grid-cols-[8rem_minmax(0,1fr)] items-start gap-4 " - "border-b border-border-subtle px-4 py-3 transition-colors " - "last:border-b-0 hover:bg-muted" + "grid min-w-0 grid-cols-1 items-start gap-3 " + "border-b border-border-subtle px-5 py-4 last:border-b-0 " + "sm:grid-cols-[8rem_minmax(0,1fr)] sm:gap-4" ), ) for prop, control in interactive_controls ], class_name=( - "mb-4 w-full min-w-0 overflow-hidden rounded-xl border " - "border-border-subtle bg-background shadow-small" + "docs-api-controls mb-4 w-full min-w-0 overflow-hidden rounded-[20px] border " + "border-border-subtle bg-background" ), ) @@ -765,7 +778,7 @@ def _render_dynamic_prop(indent: str, p, var) -> rx.Component: rx.heading( "Props", as_="h3", - class_name="font-large text-foreground mt-4 mb-2 text-left self-start", + class_name=f"{_API_HEADING_CLASS} mt-4 mb-2 text-left self-start", ), docs_api_table(*rows), ) @@ -778,34 +791,30 @@ def generate_event_triggers( if not custom_handlers: return rx.box( - rx.heading( - "Event Triggers", as_="h3", class_name="font-large text-foreground" - ), + rx.heading("Event Triggers", as_="h3", class_name=_API_HEADING_CLASS), rx.link( "See the full list of default event triggers", href="https://reflex.dev/docs/api-reference/event-triggers/", - class_name="text-foreground font-base", + class_name=_API_LINK_CLASS, is_external=True, ), class_name="py-2 overflow-x-auto justify-start flex flex-col gap-4", ) - table_header_class_name = ( - "font-small text-foreground text-normal w-auto justify-start pl-4 font-bold" - ) + table_header_class_name = "w-auto justify-start px-4 py-3 text-left text-sm font-normal text-muted-foreground" return rx.box( - rx.heading("Event Triggers", as_="h3", class_name="font-large text-foreground"), + rx.heading("Event Triggers", as_="h3", class_name=_API_HEADING_CLASS), rx.link( "See the full list of default event triggers", href="https://reflex.dev/docs/api-reference/event-triggers/", - class_name="text-foreground font-base", + class_name=_API_LINK_CLASS, is_external=True, ), rx.scroll_area( rx.table.root( rx.el.style( """ - .rt-TableRoot:where(.rt-variant-surface) :where(.rt-TableRootTable) :where(.rt-TableHeader) { - --table-row-background-color: "transparent" + .docs-event-triggers-table.rt-TableRoot:where(.rt-variant-surface) :where(.rt-TableRootTable) :where(.rt-TableHeader) { + --table-row-background-color: transparent; } """ ), @@ -818,7 +827,7 @@ def generate_event_triggers( "Description", class_name=table_header_class_name ), ), - class_name="bg-accent", + class_name="bg-muted", ), rx.table.body( *[ @@ -829,16 +838,16 @@ def generate_event_triggers( ), rx.table.cell( handler.description or "", - class_name="justify-start p-4 text-muted-foreground font-small", + class_name="justify-start p-4 text-sm font-normal leading-6 text-muted-foreground", ), ) for handler in custom_handlers ], - class_name="bg-muted", + class_name="bg-background", ), variant="surface", size="1", - class_name="w-full border border-border-subtle", + class_name="docs-event-triggers-table w-full rounded-[20px] border border-border-subtle bg-background !shadow-none", ), class_name="w-full justify-start overflow-hidden", ), @@ -855,7 +864,7 @@ def generate_valid_children(comp: type[Component]) -> rx.Component: for child in comp._valid_children ] return rx.box( - rx.heading("Valid Children", as_="h3", class_name="font-large text-foreground"), + rx.heading("Valid Children", as_="h3", class_name=_API_HEADING_CLASS), rx.box(*valid_children, class_name="flex flex-row gap-2 flex-wrap"), class_name="pb-6 w-full items-start flex flex-col gap-4", ) @@ -968,9 +977,10 @@ def shared_reference(props): generate_props(props, BaseHTML, {}), ) - active_class_name = "font-small bg-muted p-2 text-muted-foreground rounded-xl shadow-large w-28 cursor-default border border-border-subtle text-center" + active_class_name = "flex min-h-10 w-28 items-center justify-center rounded-full bg-primary px-4 py-2 text-center text-sm font-normal text-primary-foreground" - non_active_class_name = "font-small w-28 transition-color hover:text-foreground text-muted-foreground p-2 text-center" + non_active_class_name = "flex min-h-10 w-28 items-center justify-center rounded-full px-4 py-2 text-center text-sm font-normal text-muted-foreground transition-colors hover:bg-accent-hover hover:text-foreground motion-reduce:transition-none" + link_class_name = "rounded-full focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring" def links(current_page, ll_doc_exists, path): path = str(path).rstrip("/") @@ -982,6 +992,8 @@ def links(current_page, ll_doc_exists, path): rx.link( rx.box(rx.text("High Level"), class_name=active_class_name), underline="none", + aria_current="page", + class_name=link_class_name, ), rx.link( rx.box( @@ -989,8 +1001,9 @@ def links(current_page, ll_doc_exists, path): ), href=path + "/low/", underline="none", + class_name=link_class_name, ), - class_name="bg-accent rounded-[1.125rem] p-2 gap-2 flex items-center justify-center", + class_name="flex items-center justify-center gap-1 rounded-full bg-muted p-1", ), class_name="flex mb-2", ) @@ -1004,13 +1017,16 @@ def links(current_page, ll_doc_exists, path): ), href=path + "/", underline="none", + class_name=link_class_name, ), rx.link( rx.box(rx.text("Low Level"), class_name=active_class_name), href=path + "/low/", underline="none", + aria_current="page", + class_name=link_class_name, ), - class_name="bg-accent rounded-[1.125rem] p-2 gap-2 flex items-center justify-center", + class_name="flex items-center justify-center gap-1 rounded-full bg-muted p-1", ), class_name="flex mb-2", ) diff --git a/docs/app/reflex_docs/pages/docs/library.py b/docs/app/reflex_docs/pages/docs/library.py index 17d046671e7..2f6c8a2be87 100644 --- a/docs/app/reflex_docs/pages/docs/library.py +++ b/docs/app/reflex_docs/pages/docs/library.py @@ -48,15 +48,15 @@ def generate_gallery( rx.el.a( rx.el.h2( get_display_name(category), - class_name="m-0 text-base font-medium leading-6", + class_name="m-0 font-heading text-lg font-normal leading-6 tracking-[-0.02em]", ), rx.icon( "chevron-right", aria_hidden=True, - class_name="size-3.5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100", + class_name="size-3.5 shrink-0 text-muted-foreground", ), href=f"/library/{prefix.strip('/') + '/' if prefix.strip('/') else ''}{category.lower()}/", - class_name="group flex min-h-8 w-fit items-center gap-2 self-start text-foreground no-underline transition-colors hover:text-muted-foreground focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring", + class_name="group flex min-h-10 w-fit items-center gap-2 self-start text-foreground no-underline transition-colors hover:text-muted-foreground motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring", ), rx.el.ul( *[ @@ -68,7 +68,7 @@ def generate_gallery( clist=c, prefix=prefix, ), - class_name="inline-flex min-h-8 items-center text-sm font-book leading-6 text-muted-foreground no-underline transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", + class_name="inline-flex min-h-10 items-center text-sm font-normal leading-6 text-muted-foreground no-underline transition-colors hover:text-foreground motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", ), ) for c in get_components_for_category( @@ -77,7 +77,7 @@ def generate_gallery( ], class_name="m-0 grid w-full min-w-0 max-w-[48rem] list-none grid-cols-1 gap-x-4 gap-y-1 p-0 sm:grid-cols-2 xl:grid-cols-3", ), - class_name="grid grid-cols-1 gap-3 border-b border-border py-6 md:grid-cols-[12rem_minmax(0,1fr)] md:gap-6", + class_name="grid min-w-0 grid-cols-1 gap-3 border-b border-border-subtle py-6 md:grid-cols-[12rem_minmax(0,1fr)] md:gap-6", ) for category in components ] @@ -138,5 +138,5 @@ def library(): ".", class_name="mt-8 text-sm leading-6 text-muted-foreground", ), - class_name="flex flex-col h-full mb-12", + class_name="docs-library-index flex min-w-0 flex-col h-full mb-12", ) diff --git a/docs/app/reflex_docs/pages/docs/recipes_overview.py b/docs/app/reflex_docs/pages/docs/recipes_overview.py index 1e67563bc7b..37039361ba7 100644 --- a/docs/app/reflex_docs/pages/docs/recipes_overview.py +++ b/docs/app/reflex_docs/pages/docs/recipes_overview.py @@ -16,7 +16,12 @@ def format_titles(path): return capitalized_last_part -def component_grid(): +def component_grid() -> rx.Component: + """Group recipe links in quiet, keyboard-accessible category cards. + + Returns: + A responsive grid of recipe categories and their guide links. + """ from reflex_docs.pages.docs import recipes_list icons = { @@ -28,50 +33,70 @@ def component_grid(): for item in recipes_list: category = item.split("/")[-1] sidebar.append( - rx.box( - rx.box( + rx.el.section( + rx.el.div( rx.el.h2( rx.utils.format.to_title_case(category), - class_name="font-large text-foreground", + class_name="font-heading text-xl font-normal leading-7 tracking-[-0.025em] text-foreground", ), rx.icon( icons.get(category, "shapes"), size=18, - class_name="!text-subtle-foreground", + aria_hidden=True, + class_name="shrink-0 text-muted-foreground", ), - class_name="px-4 py-2 flex flex-row !text-foreground gap-3 items-center justify-between", + class_name="flex items-center justify-between gap-3 px-6 pt-6 pb-4", ), - rx.box( + rx.el.ul( *[ - rx.link( - format_titles(c), - href=get_component_link(category, c), - class_name="font-small text-muted-foreground hover:!text-primary transition-color w-fit", + rx.el.li( + rx.el.a( + format_titles(c), + rx.icon( + "arrow-right", + size=14, + aria_hidden=True, + class_name="shrink-0 text-muted-foreground", + ), + href=get_component_link(category, c), + class_name="flex min-h-11 items-center justify-between gap-3 py-2 text-sm font-normal leading-6 text-muted-foreground no-underline transition-colors hover:text-foreground motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", + ), + class_name="min-w-0", ) for c in recipes_list[category] ], - class_name="flex flex-col gap-3 px-4 py-2 border-t border-border", + class_name="m-0 flex list-none flex-col border-t border-border-subtle px-6 py-3", ), - class_name="flex flex-col border border-border rounded-xl bg-muted shadow-large overflow-hidden", + class_name="flex min-w-0 flex-col rounded-[20px] border border-border-subtle bg-background", ) ) - return rx.box( - rx.box(*sidebar, class_name="grid grid-cols-1 lg:grid-cols-3 gap-6"), + return rx.el.div( + *sidebar, + class_name="grid min-w-0 grid-cols-1 gap-5 lg:grid-cols-3", ) -def info_card(title, content): - return rx.box( +def info_card(title: str, content: str) -> rx.Component: + """Explain one quality of the reusable recipe collection. + + Args: + title: Short benefit heading. + content: Supporting explanation. + + Returns: + A compact editorial card using the documentation theme. + """ + return rx.el.div( rx.el.h2( title, - class_name="font-md-smbold text-foreground", + class_name="font-heading text-xl font-normal leading-7 tracking-[-0.025em] text-foreground", ), - rx.text( + rx.el.p( content, - class_name="font-small text-muted-foreground", + class_name="text-sm font-normal leading-6 text-muted-foreground", ), - class_name="flex flex-col border gap-2 border-border rounded-xl bg-background shadow-large overflow-hidden px-4 py-2", + class_name="flex min-w-0 flex-col gap-2 rounded-[20px] bg-muted p-6", ) @@ -90,7 +115,7 @@ def card_section(): "Customizable", "Every aspect of the components can be customized to fit your needs.", ), - class_name="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-16", + class_name="grid min-w-0 grid-cols-1 gap-4 lg:grid-cols-3", ), ) @@ -107,5 +132,5 @@ def overview(): text="Categories", ), component_grid(), - class_name="flex flex-col h-full mb-12", + class_name="docs-recipes flex min-w-0 flex-col h-full mb-12", ) diff --git a/docs/app/reflex_docs/pages/docs_landing/__init__.py b/docs/app/reflex_docs/pages/docs_landing/__init__.py index d887ea422e1..8702e351727 100644 --- a/docs/app/reflex_docs/pages/docs_landing/__init__.py +++ b/docs/app/reflex_docs/pages/docs_landing/__init__.py @@ -28,7 +28,7 @@ def _docs_landing() -> rx.Component: rx.el.main( rx.el.div( hero(), - divider(class_name="max-w-full"), + divider(), ai_builder_section(), framework(), enterprise_section(), @@ -37,11 +37,11 @@ def _docs_landing() -> rx.Component: other_section(), docs_cta(), marketing_footer(show_color_mode_toggle=True), - class_name="flex flex-col relative justify-center items-center w-full overflow-hidden", + class_name="relative flex w-full flex-col items-center", ), class_name="flex flex-col w-full relative h-full justify-center items-center", ), - class_name="flex flex-col w-full justify-center items-center relative bg-background", + class_name="docs-landing relative flex w-full flex-col items-center bg-background font-sans text-foreground", ) diff --git a/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py b/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py index 2a4fa7029c6..465750d5c2e 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py @@ -3,11 +3,11 @@ import frontmatter import reflex as rx import reflex_components_internal as ui -from reflex_site_shared.components.marquee import marquee from reflex_site_shared.integrations import get_integration_logo_url from reflex_docs.pages.docs import ai_builder as ai_builder_pages from reflex_docs.pages.docs_landing.views.artwork import artwork +from reflex_docs.pages.docs_landing.views.layout import SECTION, section_heading from reflex_docs.pages.docs_landing.views.mcp_artwork import mcp_artwork @@ -76,41 +76,42 @@ def card( enterprise_only: bool = False, ) -> rx.Component: """Render a linked guide with a matching editorial illustration panel.""" - return rx.el.div( + return rx.el.a( + rx.el.div( + rx.el.div(content, class_name="flex w-full max-w-[18rem] justify-center"), + class_name="hidden h-44 items-center justify-center overflow-hidden border-b border-border-subtle bg-muted px-6 sm:flex", + aria_hidden=True, + ), rx.el.div( rx.el.div( rx.el.h3( title, - class_name="docs-ai-card-title text-xl font-book tracking-tight", + class_name="font-heading text-2xl font-normal leading-8 tracking-[-0.025em]", ), rx.el.span( "Enterprise-only", - class_name="w-fit rounded-full border border-border bg-muted px-2.5 py-1 text-xs text-muted-foreground", + class_name="w-fit rounded-full border border-border px-2.5 py-1 text-xs font-normal text-muted-foreground", ) if enterprise_only else None, - ui.icon( - "ArrowUpRight01Icon", - size=18, - aria_hidden=True, - class_name="ml-auto shrink-0", - ), - class_name="flex w-full items-center gap-3 text-foreground", + class_name="flex w-full flex-wrap items-center gap-x-3 gap-y-2 text-foreground", ), rx.el.p( description, class_name="text-sm font-normal leading-6 text-muted-foreground", ), - class_name="flex flex-1 flex-col items-start gap-3 p-7 sm:p-8", - ), - rx.el.div(content, class_name="docs-ai-card-art", aria_hidden=True), - rx.el.a( - href=href, - aria_label=title, - class_name="docs-ai-card-link absolute inset-0 rounded-panel focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-foreground", + rx.el.span( + "Read the guide", + ui.icon("ArrowRight01Icon", size=16, aria_hidden=True), + class_name="mt-auto inline-flex items-center gap-2 pt-4 text-sm font-normal text-foreground", + ), + class_name="flex flex-1 flex-col items-start gap-3 p-6 sm:p-7", ), + href=href, + aria_label=title, data_tone=tone, - class_name="docs-ai-card flex min-w-0 flex-col overflow-hidden rounded-panel border border-border bg-background relative text-left transition-colors hover:border-border-strong", + style={"--ai-accent": "var(--foreground)", "--ai-tint": "var(--accent)"}, + class_name="group flex min-w-0 flex-col overflow-hidden rounded-[20px] border border-border-subtle bg-background text-left transition-colors hover:bg-muted hover:border-border motion-reduce:transition-none focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-ring", ) @@ -127,53 +128,35 @@ def integration_icon_marquee(integration_name: str) -> rx.Component: ), ui.avatar.fallback( integration_name[0], - class_name="text-foreground text-base font-semibold uppercase size-full", + class_name="text-foreground text-base font-normal uppercase size-full", unstyled=True, ), unstyled=True, - class_name="docs-ai-integration flex size-14 shrink-0 items-center justify-center rounded-xl border border-border bg-background mx-2", + class_name="flex aspect-square w-full items-center justify-center rounded-xl border border-border-subtle bg-background", ) @rx.memo def integrations_marquee() -> rx.Component: + """Show a quiet sample of integration marks without continuous motion. + + Returns: + A decorative grid of supported integration logos. + """ integration_names = [ next(iter(item.values()))["name"] for item in get_integration_path() ] return rx.el.div( - marquee( - *[integration_icon_marquee(name) for name in reversed(integration_names)], - direction="left", - gradient=False, - class_name="h-auto w-full overflow-hidden", - speed=25, - pause_on_hover=False, - ), - marquee( - *[integration_icon_marquee(name) for name in integration_names], - direction="right", - gradient=False, - class_name="h-auto w-full overflow-hidden", - speed=25, - pause_on_hover=False, - ), - class_name="docs-ai-integrations flex w-full flex-col gap-4", + *[integration_icon_marquee(name) for name in sorted(integration_names)[:8]], + class_name="grid w-full max-w-[228px] grid-cols-4 gap-3", ) def ai_builder_section() -> rx.Component: return rx.el.section( rx.el.div( - rx.el.div( - rx.el.h2( - "AI Builder", - class_name="text-foreground text-3xl font-medium tracking-tight", - ), - rx.el.p( - "Learn how to build applications with Reflex AI.", - class_name="text-muted-foreground text-sm font-normal", - ), - class_name="flex flex-col gap-4", + section_heading( + "AI Builder", "Learn how to build applications with Reflex AI." ), rx.el.div( card( @@ -201,9 +184,9 @@ def ai_builder_section() -> rx.Component: tone="mint", enterprise_only=True, ), - class_name="grid grid-cols-1 lg:grid-cols-3 gap-6", + class_name="grid grid-cols-1 gap-4 md:grid-cols-3", ), - class_name="flex flex-col gap-10 max-lg:text-center relative max-w-[90rem] px-4 min-[55rem]:px-8 lg:px-12 mx-auto", + class_name=SECTION, ), - class_name="bg-muted w-full lg:pt-24 lg:pb-24 pb-10 max-lg:pt-10", + class_name="w-full border-b border-border-subtle bg-background", ) diff --git a/docs/app/reflex_docs/pages/docs_landing/views/ai_getting_started.svg b/docs/app/reflex_docs/pages/docs_landing/views/ai_getting_started.svg index 6764ad93eec..ab2e21d4e88 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/ai_getting_started.svg +++ b/docs/app/reflex_docs/pages/docs_landing/views/ai_getting_started.svg @@ -1,4 +1,4 @@ - + Build a customer dashboard @@ -14,4 +14,4 @@ - \ No newline at end of file + diff --git a/docs/app/reflex_docs/pages/docs_landing/views/cta.py b/docs/app/reflex_docs/pages/docs_landing/views/cta.py index 7ad36d2bbf8..bbd3fabf4b2 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/cta.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/cta.py @@ -1,59 +1,62 @@ """A quiet closing invitation using the editorial theme.""" import reflex as rx -import reflex_components_internal as ui from reflex_site_shared.components.marketing_button import button from reflex_site_shared.constants import REFLEX_BUILD_URL -from reflex_docs.pages.docs_landing.views.cta_artwork import cta_artwork +from reflex_docs.pages.docs_landing.views.layout import CONTAINER, FOCUS def docs_cta() -> rx.Component: - """Render the closing actions alongside the component illustration.""" + """Render an open closing invitation with the existing product and demo links. + + Returns: + The closing call-to-action section of the documentation overview. + """ return rx.el.section( rx.el.div( rx.el.div( rx.el.h2( "The platform to build and scale enterprise apps.", id="docs-cta-title", - class_name="max-w-xl text-3xl sm:text-4xl font-book leading-[1.15] tracking-[-0.03em] text-foreground text-balance", + class_name="max-w-3xl font-heading text-3xl font-light leading-[1.08] tracking-[-0.04em] text-foreground text-balance sm:text-4xl lg:text-5xl", ), rx.el.p( "Describe your idea, and let AI transform it into a complete, " "production-ready Python web application.", - class_name="max-w-lg text-base leading-7 font-normal text-muted-foreground", + class_name="max-w-xl text-base font-normal leading-7 text-muted-foreground", ), rx.el.div( rx.el.elements.a( button( "Try for free", - ui.icon("ArrowRight01Icon", aria_hidden=True), variant="primary", - class_name="!px-6", + size="lg", + class_name="!px-6 !font-normal", native_button=False, ), href=REFLEX_BUILD_URL, target="_blank", rel="noopener noreferrer", - class_name="rounded-full focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring", + class_name=f"rounded-full {FOCUS}", ), rx.el.elements.a( button( "Book a Demo", variant="ghost", - class_name="!px-6", + size="lg", + class_name="!border-0 !bg-muted !px-6 !font-normal !text-foreground !shadow-none hover:!bg-accent-hover", native_button=False, ), href="https://reflex.dev/demo/", - class_name="rounded-full focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring", + class_name=f"rounded-full {FOCUS}", ), - class_name="flex flex-wrap items-center gap-x-5 gap-y-3", + class_name="mt-2 flex flex-wrap items-center justify-center gap-x-7 gap-y-3", ), - class_name="relative z-10 flex flex-col items-start gap-6 p-6 sm:p-10 lg:py-14 lg:pl-12 lg:pr-0", + class_name="flex flex-col items-center gap-6 text-center", ), - cta_artwork(), - class_name="grid lg:grid-cols-[1.1fr_1fr] items-center overflow-hidden rounded-panel border border-border-subtle bg-muted", + class_name="border-t border-border-subtle pt-12 sm:pt-16", ), aria_labelledby="docs-cta-title", - class_name="docs-cta mx-auto w-full max-w-[90rem] px-4 min-[55rem]:px-8 lg:px-12 py-16 lg:py-24", + class_name=f"docs-cta {CONTAINER} pb-16 sm:pb-20", ) diff --git a/docs/app/reflex_docs/pages/docs_landing/views/divider.py b/docs/app/reflex_docs/pages/docs_landing/views/divider.py index 6ef22ca2b5b..c186728e15d 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/divider.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/divider.py @@ -4,14 +4,8 @@ def divider(class_name: str = "") -> rx.Component: return rx.el.div( - rx.el.div( - class_name="absolute top-0 -right-24 w-24 h-px bg-gradient-to-l from-transparent to-current text-border-subtle" - ), - rx.el.div( - class_name="absolute top-0 -left-24 w-24 h-px bg-gradient-to-r from-transparent to-current text-border-subtle" - ), class_name=ui.cn( - "w-full h-[1px] bg-border-subtle relative max-w-(--landing-layout-max-width) mx-auto", + "mx-auto h-px w-full max-w-[1176px] bg-border-subtle", class_name, ), ) diff --git a/docs/app/reflex_docs/pages/docs_landing/views/enterprise.py b/docs/app/reflex_docs/pages/docs_landing/views/enterprise.py index 13aa8367099..7d69124a7cc 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/enterprise.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/enterprise.py @@ -1,24 +1,17 @@ import reflex as rx from reflex_docs.pages.docs import enterprise as enterprise_page -from reflex_docs.pages.docs_landing.views.link_item import faded_borders, link_item +from reflex_docs.pages.docs_landing.views.layout import SECTION, section_heading +from reflex_docs.pages.docs_landing.views.link_item import link_item def enterprise_section() -> rx.Component: return rx.el.section( - rx.el.div( - rx.el.h2( - "Enterprise", - class_name="text-foreground text-3xl font-medium", - ), - rx.el.p( - "Learn how to build enterprise-ready applications with Reflex.", - class_name="text-muted-foreground text-sm font-[475]", - ), - class_name="flex flex-col gap-4", + section_heading( + "Enterprise", + "Learn how to build enterprise-ready applications with Reflex.", ), rx.el.div( - faded_borders(), link_item( "MenuSquareIcon", "Components", @@ -32,7 +25,7 @@ def enterprise_section() -> rx.Component: enterprise_page.overview.path, has_padding_left=True, ), - class_name="grid grid-cols-1 lg:grid-cols-2 border-t border-border-subtle relative", + class_name="grid grid-cols-1 gap-4 lg:grid-cols-2", ), - class_name="flex flex-col gap-10 max-lg:text-center relative max-w-[90rem] px-4 min-[55rem]:px-8 lg:px-12 mx-auto w-full justify-start lg:mb-24 mb-10 overflow-hidden", + class_name=f"{SECTION} border-t border-border-subtle", ) diff --git a/docs/app/reflex_docs/pages/docs_landing/views/framework.py b/docs/app/reflex_docs/pages/docs_landing/views/framework.py index 829589967c2..742c16f24ea 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/framework.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/framework.py @@ -4,6 +4,7 @@ from reflex_docs.pages.docs import database, enterprise, getting_started from reflex_docs.pages.docs.library import library +from reflex_docs.pages.docs_landing.views.layout import FOCUS, SECTION, section_heading from reflex_docs.pages.library_previews import core_components_dict @@ -14,31 +15,33 @@ def docs_item( rx.el.div( ui.icon( icon, - class_name="size-6 group-hover:text-primary-hover group-hover:dark:text-muted-foreground", + class_name="size-5 shrink-0 text-foreground", stroke_width=1.5, + aria_hidden=True, ), - rx.el.span( + rx.el.h3( title, - class_name="text-foreground text-xl font-[575] group-hover:text-primary-hover group-hover:dark:text-muted-foreground", + class_name="font-heading text-xl font-normal leading-7 tracking-[-0.025em] text-foreground", ), rx.el.div( "Enterprise-only", - class_name="text-foreground text-xs font-medium bg-background px-2.5 h-7 border-b border rounded-lg border-border-subtle flex justify-center items-center ml-1", + class_name="w-fit rounded-full border border-border px-2.5 py-1 text-xs font-normal text-muted-foreground", ) if enterprise_only else None, ui.icon( "ArrowRight01Icon", - class_name="size-4 ml-auto group-hover:text-primary-hover group-hover:dark:text-muted-foreground", + class_name="ml-auto size-4 shrink-0 text-muted-foreground", + aria_hidden=True, ), - class_name="flex row items-center gap-3 h-8", + class_name="flex flex-wrap items-center gap-x-3 gap-y-2", ), rx.el.p( description, - class_name="text-muted-foreground text-sm font-[475]", + class_name="text-sm font-normal leading-6 text-muted-foreground", ), to=href, - class_name="flex flex-col gap-2 py-8 pr-8 relative group focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring lg:max-w-[21rem] w-full max-lg:text-start hover:bg-[linear-gradient(243deg,var(--muted)_0%,var(--background)_100%)]", + class_name=f"group flex min-w-0 flex-col gap-3 rounded-[20px] border border-border-subtle p-6 text-left transition-colors hover:bg-muted motion-reduce:transition-none {FOCUS}", ) @@ -69,7 +72,7 @@ def links_section() -> rx.Component: "Manage and interact with your data seamlessly using Reflex’s straightforward models and querying approach.", database.overview.path, ), - class_name="flex flex-col border-r border-y border-border-subtle divide-y divide-border-subtle", + class_name="grid min-w-0 grid-cols-1 gap-4 sm:grid-cols-2", ) @@ -85,9 +88,11 @@ def component_link(name: str, href: str) -> rx.Component: """ return rx.el.a( rx.el.span(name), - ui.icon("ArrowRight01Icon", class_name="ml-auto size-4 shrink-0"), + ui.icon( + "ArrowRight01Icon", class_name="ml-auto size-4 shrink-0", aria_hidden=True + ), to=f"/library/{href.strip('/')}/", - class_name="flex min-h-9 w-full items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", + class_name="flex min-h-11 w-full items-center gap-3 rounded-lg px-3 py-2 text-sm font-normal leading-6 text-foreground transition-colors hover:bg-background motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", ) @@ -95,28 +100,33 @@ def components_section() -> rx.Component: return rx.el.div( rx.el.div( rx.el.div( - ui.icon("MenuSquareIcon", class_name="size-6", stroke_width=1.5), - rx.el.span( + ui.icon( + "MenuSquareIcon", + class_name="size-5", + stroke_width=1.5, + aria_hidden=True, + ), + rx.el.h3( "Component Library", - class_name="text-foreground text-xl font-[575] group-hover:text-primary-hover", + class_name="font-heading text-2xl font-normal tracking-[-0.025em] text-foreground", ), - class_name="flex row items-center gap-3 h-8", + class_name="flex items-center gap-3", ), rx.el.p( "Build your app with our comprehensive collection of UI components and features.", - class_name="text-muted-foreground text-sm font-[475] max-w-[16.5rem]", + class_name="max-w-sm text-sm font-normal leading-6 text-muted-foreground", ), rx.el.a( button( "Browse All Components", - variant="outline", + variant="ghost", native_button=False, - class_name="font-[525] w-fit text-foreground", + class_name="!border-0 !bg-background !font-normal !text-foreground !shadow-none hover:!bg-accent-hover", ), to=library.path, - class_name="w-fit mt-4", + class_name=f"mt-3 w-fit rounded-full {FOCUS}", ), - class_name="flex flex-col gap-2 max-lg:text-start", + class_name="flex flex-col items-start gap-3", ), rx.el.div( rx.el.div( @@ -133,7 +143,7 @@ def components_section() -> rx.Component: component_link("Forms", core_components_dict["forms"]["path"]), component_link("Layout", core_components_dict["layout"]["path"]), component_link("Media", core_components_dict["media"]["path"]), - class_name="flex flex-col gap-2", + class_name="flex min-w-0 flex-col gap-1", ), rx.el.div( component_link("Other", core_components_dict["other"]["path"]), @@ -145,43 +155,23 @@ def components_section() -> rx.Component: component_link( "Typography", core_components_dict["typography"]["path"] ), - class_name="flex flex-col gap-2", + class_name="flex min-w-0 flex-col gap-1", ), - class_name="grid grid-cols-1 lg:grid-cols-2 lg:gap-28 gap-10 mt-auto", + class_name="grid grid-cols-1 gap-1 border-t border-border pt-5 sm:grid-cols-2 sm:gap-x-4", ), - class_name="flex flex-col gap-4 lg:px-8 pt-8 max-lg:pr-8 pb-6 h-auto w-full flex-1 border-r border-border-subtle border-y", + class_name="flex min-w-0 flex-col gap-7 rounded-[20px] bg-muted p-6 sm:p-8", ) def framework() -> rx.Component: return rx.el.section( - rx.el.div( - rx.el.h2( - "Framework", - class_name="text-foreground text-3xl font-medium", - ), - rx.el.p( - "Learn how to build applications with Reflex Framework.", - class_name="text-muted-foreground text-sm font-[475]", - ), - class_name="flex flex-col gap-4", + section_heading( + "Framework", "Learn how to build applications with Reflex Framework." ), rx.el.div( - rx.el.div( - class_name="absolute bottom-0 -left-24 w-24 h-px bg-gradient-to-r from-transparent to-current text-border-subtle" - ), - rx.el.div( - class_name="absolute top-0 -left-24 w-24 h-px bg-gradient-to-r from-transparent to-current text-border-subtle" - ), - rx.el.div( - class_name="absolute bottom-0 -right-24 w-24 h-px bg-gradient-to-l from-transparent to-current text-border-subtle" - ), - rx.el.div( - class_name="absolute right-0 -top-24 h-24 w-px bg-gradient-to-b from-transparent to-current text-border-subtle" - ), links_section(), components_section(), - class_name="flex flex-col lg:flex-row relative", + class_name="grid min-w-0 grid-cols-1 gap-4 lg:grid-cols-[1.1fr_1fr]", ), - class_name="flex flex-col gap-10 max-lg:text-center relative max-w-[90rem] px-4 min-[55rem]:px-8 lg:px-12 mx-auto w-full justify-start pt-10 lg:pt-24 lg:mb-24 mb-10 overflow-hidden", + class_name=SECTION, ) diff --git a/docs/app/reflex_docs/pages/docs_landing/views/hero.py b/docs/app/reflex_docs/pages/docs_landing/views/hero.py index 860eab33415..774f76ef18d 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/hero.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/hero.py @@ -5,8 +5,35 @@ from reflex_site_shared.components.marketing_button import button from reflex_site_shared.views.hosting_banner import HostingBannerState -from reflex_docs.pages.docs import getting_started -from reflex_docs.pages.docs_landing.views.artwork import artwork +from reflex_docs.pages.docs import getting_started, hosting +from reflex_docs.pages.docs.library import library +from reflex_docs.pages.docs_landing.views.layout import CONTAINER, FOCUS + + +def _guide(title: str, description: str, href: str) -> rx.Component: + """Render a compact navigation row in the hero's guide index. + + Args: + title: Guide group name. + description: Short description of its contents. + href: Existing documentation destination. + + Returns: + A native, keyboard-accessible navigation link. + """ + return rx.el.a( + rx.el.div( + rx.el.span(title, class_name="text-base font-normal text-foreground"), + rx.el.span( + description, + class_name="mt-1 block text-sm leading-6 text-muted-foreground", + ), + class_name="min-w-0", + ), + ui.icon("ArrowUpRight01Icon", size=18, aria_hidden=True), + href=href, + class_name=f"flex items-center justify-between gap-6 rounded-xl px-4 py-5 transition-colors hover:bg-muted motion-reduce:transition-none {FOCUS}", + ) def hero() -> rx.Component: @@ -15,50 +42,67 @@ def hero() -> rx.Component: rx.el.div( rx.el.h1( "Reflex Documentation", - class_name="text-foreground text-4xl sm:text-5xl lg:text-6xl font-book tracking-[-0.03em] leading-[1.06] text-balance", + class_name="max-w-2xl font-heading text-4xl font-light leading-[1.04] tracking-[-0.045em] text-foreground text-balance sm:text-5xl lg:text-[64px]", ), rx.el.p( "Get up and running with Reflex in minutes. A complete set of resources " "to build, deploy, and scale your application.", - class_name="max-w-2xl text-muted-foreground text-base sm:text-lg leading-7 font-normal text-balance", + class_name="max-w-xl text-base font-normal leading-7 text-muted-foreground sm:text-lg sm:leading-8", ), rx.el.div( rx.el.a( button( "Explore Framework", - ui.icon("ArrowRight01Icon"), variant="primary", size="lg", - class_name="!px-6", + class_name="!px-6 !font-normal", native_button=False, ), href=getting_started.introduction.path, - class_name="rounded-full focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring", + class_name=f"rounded-full {FOCUS}", ), rx.el.a( button( "Build with AI", - ui.icon("ArrowRight01Icon"), - variant="outline", + variant="ghost", size="lg", - class_name="!px-6", + class_name="!border-0 !bg-muted !px-6 !font-normal !text-foreground !shadow-none hover:!bg-accent-hover", native_button=False, ), href="/ai/", - class_name="rounded-full focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring", + class_name=f"rounded-full {FOCUS}", ), - class_name="flex flex-wrap items-center gap-3", + class_name="mt-2 flex flex-wrap items-center gap-x-7 gap-y-3", ), - class_name="relative z-10 flex flex-col items-start gap-6 lg:max-w-[55%]", + class_name="flex min-w-0 flex-col items-start gap-6", ), - artwork( - "squares_docs_logo", - class_name="docs-hero-art absolute left-1/2 w-1/2 max-lg:hidden", + rx.el.nav( + rx.el.p( + "Find your next step", + class_name="px-4 pb-2 text-sm text-muted-foreground", + ), + _guide( + "Getting started", + "Learn the framework and build your first app.", + getting_started.introduction.path, + ), + _guide( + "Component library", + "Find the UI components for your application.", + library.path, + ), + _guide( + "Deploy your app", + "Take your application from local to live.", + hosting.deploy_quick_start.path, + ), + aria_label="Documentation starting points", + class_name="min-w-0 border-t border-border-subtle pt-6 lg:border-t-0 lg:border-l lg:pl-8 lg:pt-0", ), style={ "--docs-hero-header-height": rx.cond( HostingBannerState.is_banner_visible, "6.5rem", "4rem" ) }, - class_name="docs-hero relative max-w-[90rem] px-4 min-[55rem]:px-8 lg:px-12 mx-auto w-full pb-16 lg:pb-24", + class_name=f"docs-hero {CONTAINER} relative grid items-center gap-10 pb-12 sm:pb-16 lg:grid-cols-[1.45fr_1fr] lg:gap-16 lg:pb-20", ) diff --git a/docs/app/reflex_docs/pages/docs_landing/views/hosting.py b/docs/app/reflex_docs/pages/docs_landing/views/hosting.py index aabcddc9e12..fe995eb1fc1 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/hosting.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/hosting.py @@ -1,24 +1,16 @@ import reflex as rx from reflex_docs.pages.docs import hosting as hosting_page -from reflex_docs.pages.docs_landing.views.link_item import faded_borders, link_item +from reflex_docs.pages.docs_landing.views.layout import SECTION, section_heading +from reflex_docs.pages.docs_landing.views.link_item import link_item def hosting_section() -> rx.Component: return rx.el.section( - rx.el.div( - rx.el.h2( - "Cloud", - class_name="text-foreground text-3xl font-medium", - ), - rx.el.p( - "Learn how to host your applications with Reflex Hosting.", - class_name="text-muted-foreground text-sm font-[475]", - ), - class_name="flex flex-col gap-4", + section_heading( + "Cloud", "Learn how to host your applications with Reflex Hosting." ), rx.el.div( - faded_borders(), link_item( "CloudServerIcon", "Deployment", @@ -45,7 +37,7 @@ def hosting_section() -> rx.Component: hosting_page.deploy_quick_start.path, has_padding_left=True, ), - class_name="grid grid-cols-1 lg:grid-cols-2 border-t border-border-subtle relative", + class_name="grid grid-cols-1 gap-4 lg:grid-cols-2", ), - class_name="flex flex-col gap-10 max-lg:text-center relative max-w-[90rem] px-4 min-[55rem]:px-8 lg:px-12 mx-auto w-full justify-start lg:mb-24 overflow-hidden", + class_name=f"{SECTION} border-t border-border-subtle", ) diff --git a/docs/app/reflex_docs/pages/docs_landing/views/layout.py b/docs/app/reflex_docs/pages/docs_landing/views/layout.py new file mode 100644 index 00000000000..aa94f496513 --- /dev/null +++ b/docs/app/reflex_docs/pages/docs_landing/views/layout.py @@ -0,0 +1,30 @@ +"""Shared spacing and typography for the documentation overview.""" + +import reflex as rx + +CONTAINER = "mx-auto w-full max-w-[1176px] px-4 sm:px-8" +SECTION = f"{CONTAINER} py-12 sm:py-16" +FOCUS = ( + "focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring" +) +HEADING = "font-heading text-3xl font-light leading-[1.12] tracking-[-0.035em] text-foreground sm:text-4xl" + + +def section_heading(title: str, description: str) -> rx.Component: + """Introduce a documentation group with its purpose. + + Args: + title: Name of the documentation group. + description: Supporting explanation of the guides. + + Returns: + A responsive section introduction. + """ + return rx.el.div( + rx.el.h2(title, class_name=HEADING), + rx.el.p( + description, + class_name="max-w-lg text-base font-normal leading-7 text-muted-foreground", + ), + class_name="mb-8 flex flex-col gap-4 sm:mb-10 lg:flex-row lg:items-end lg:justify-between lg:gap-12", + ) diff --git a/docs/app/reflex_docs/pages/docs_landing/views/link_item.py b/docs/app/reflex_docs/pages/docs_landing/views/link_item.py index 02584d11767..ad34e75df7a 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/link_item.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/link_item.py @@ -1,55 +1,57 @@ import reflex as rx import reflex_components_internal as ui +from reflex_docs.pages.docs_landing.views.layout import FOCUS + def faded_borders() -> rx.Component: - return rx.fragment( - rx.el.div( - class_name="absolute bottom-0 -left-24 w-24 h-px bg-gradient-to-r from-transparent to-current text-border-subtle" - ), - rx.el.div( - class_name="absolute -top-px -left-24 w-24 h-px bg-gradient-to-r from-transparent to-current text-border-subtle" - ), - rx.el.div( - class_name="absolute bottom-0 -right-24 w-24 h-px bg-gradient-to-l from-transparent to-current text-border-subtle" - ), - rx.el.div( - class_name="absolute right-0 -top-24 h-24 w-px bg-gradient-to-b from-transparent to-current text-border-subtle" - ), - ) + """Keep the former decoration hook without drawing beyond the content frame. + + Returns: + An empty fragment for existing section callers. + """ + return rx.fragment() def link_item( icon: str, title: str, description: str, href: str, has_padding_left: bool = False ) -> rx.Component: - return rx.el.div( + """Render a guide as one readable, keyboard-accessible card. + + Args: + icon: Shared icon name. + title: Guide name. + description: Guide summary. + href: Existing destination. + has_padding_left: Retained for callers; all cards now use equal insets. + + Returns: + A single native navigation target. + """ + return rx.el.a( rx.el.div( ui.icon( icon, - class_name="size-6 shrink-0 group-hover:text-primary-hover group-hover:dark:text-muted-foreground", + class_name="size-5 shrink-0 text-foreground", stroke_width=1.5, + aria_hidden=True, ), - rx.el.span( + rx.el.h3( title, - class_name="text-foreground text-xl font-[575] group-hover:text-primary-hover group-hover:dark:text-muted-foreground", + class_name="min-w-0 font-heading text-2xl font-normal leading-7 tracking-[-0.025em] text-foreground", ), ui.icon( "ArrowRight01Icon", - class_name="size-4 ml-auto group-hover:text-primary-hover group-hover:dark:text-muted-foreground shrink-0", + class_name="ml-auto mt-1 size-4 shrink-0 text-muted-foreground", + aria_hidden=True, ), - class_name="flex flex-row gap-3 items-center max-lg:text-start", + class_name="flex items-start gap-3", ), - rx.el.span( + rx.el.p( description, - class_name="text-muted-foreground text-sm font-[475] text-start", - ), - rx.el.a( - to=href, - aria_label=title, - class_name="absolute inset-0 rounded-xl focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-primary", - ), - class_name=ui.cn( - "flex flex-col gap-2 pr-8 py-8 group border-r border-b border-border-subtle relative max-lg:p-6 hover:bg-[linear-gradient(243deg,var(--muted)_0%,var(--background)_100%)]", - "lg:pl-8 pl-6" if has_padding_left else "", + class_name="max-w-xl text-sm font-normal leading-6 text-muted-foreground", ), + to=href, + aria_label=title, + class_name=f"group flex min-w-0 flex-col gap-4 rounded-[20px] bg-muted p-6 text-left transition-colors hover:bg-accent-hover motion-reduce:transition-none sm:p-8 {FOCUS}", ) diff --git a/docs/app/reflex_docs/pages/docs_landing/views/other.py b/docs/app/reflex_docs/pages/docs_landing/views/other.py index 7a89454ce14..3f7dc3a2c3b 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/other.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/other.py @@ -1,23 +1,16 @@ import reflex as rx from reflex_site_shared.constants import CONTRIBUTING_URL -from reflex_docs.pages.docs_landing.views.link_item import faded_borders, link_item +from reflex_docs.pages.docs_landing.views.layout import SECTION, section_heading +from reflex_docs.pages.docs_landing.views.link_item import link_item def other_section() -> rx.Component: from reflex_docs.pages.docs import wrapping_react return rx.el.section( - rx.el.div( - rx.el.h2( - "Other", - class_name="text-foreground text-3xl font-medium", - ), - rx.el.p( - "Learn about other features and tools that Reflex offers.", - class_name="text-muted-foreground text-sm font-[475]", - ), - class_name="flex flex-col gap-4", + section_heading( + "Other", "Learn about other features and tools that Reflex offers." ), rx.el.div( link_item( @@ -33,8 +26,7 @@ def other_section() -> rx.Component: wrapping_react.overview.path, has_padding_left=True, ), - faded_borders(), - class_name="grid grid-cols-1 lg:grid-cols-2 border-t border-border-subtle relative", + class_name="grid grid-cols-1 gap-4 lg:grid-cols-2", ), - class_name="flex flex-col gap-10 max-lg:text-center relative max-w-[90rem] px-4 min-[55rem]:px-8 lg:px-12 mx-auto w-full justify-start lg:mb-24 mb-10 overflow-hidden", + class_name=f"{SECTION} border-t border-border-subtle", ) diff --git a/docs/app/reflex_docs/pages/docs_landing/views/self_hosting.py b/docs/app/reflex_docs/pages/docs_landing/views/self_hosting.py index 5260bf3a444..eae00654fac 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/self_hosting.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/self_hosting.py @@ -1,24 +1,16 @@ import reflex as rx from reflex_docs.pages.docs import hosting as hosting_page -from reflex_docs.pages.docs_landing.views.link_item import faded_borders, link_item +from reflex_docs.pages.docs_landing.views.layout import SECTION, section_heading +from reflex_docs.pages.docs_landing.views.link_item import link_item def self_hosting_section() -> rx.Component: return rx.el.section( - rx.el.div( - rx.el.h2( - "Self-Hosting", - class_name="text-foreground text-3xl font-medium", - ), - rx.el.p( - "Learn how to self-host your applications with Reflex.", - class_name="text-muted-foreground text-sm font-[475]", - ), - class_name="flex flex-col gap-4", + section_heading( + "Self-Hosting", "Learn how to self-host your applications with Reflex." ), rx.el.div( - faded_borders(), link_item( "ServerStack01Icon", "Run Reflex App in Dockerized Environment", @@ -32,7 +24,7 @@ def self_hosting_section() -> rx.Component: hosting_page.databricks.path, has_padding_left=True, ), - class_name="grid grid-cols-1 lg:grid-cols-2 border-t border-border-subtle relative", + class_name="grid grid-cols-1 gap-4 lg:grid-cols-2", ), - class_name="flex flex-col gap-10 max-lg:text-center relative max-w-[90rem] px-4 min-[55rem]:px-8 lg:px-12 mx-auto w-full justify-start lg:mb-24 mb-10 overflow-hidden max-lg:pt-10", + class_name=f"{SECTION} border-t border-border-subtle", ) diff --git a/docs/app/reflex_docs/pages/integrations/integration_gallery.py b/docs/app/reflex_docs/pages/integrations/integration_gallery.py index 89756636c2b..4c8fe4a40ff 100644 --- a/docs/app/reflex_docs/pages/integrations/integration_gallery.py +++ b/docs/app/reflex_docs/pages/integrations/integration_gallery.py @@ -19,14 +19,19 @@ def integration_filter_button(data: dict): - active_pill = "border border-ring bg-accent hover:bg-accent !text-primary-hover" + active = selected_filter.value == data["name"] + active_pill = ( + "border-primary bg-primary text-primary-foreground hover:bg-primary-hover" + ) + inactive_pill = "border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground" - return ui.button( - ui.icon(icon=data["icon"]), - rx.el.p(data["name"], class_name="text-sm"), - variant="outline", - class_name="flex flex-row items-center " - + rx.cond(selected_filter.value == data["name"], active_pill, "").to(str), + return rx.el.button( + ui.icon(icon=data["icon"], size=16, aria_hidden=True), + rx.el.span(data["name"]), + type="button", + aria_pressed=active, + class_name="docs-integration-filter inline-flex min-h-10 items-center justify-center gap-2 rounded-full border px-4 py-2 text-sm font-normal leading-5 transition-colors motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring " + + rx.cond(active, active_pill, inactive_pill).to(str), on_click=selected_filter.set_value(data["name"]), ) @@ -35,9 +40,9 @@ def integration_filters(): return rx.el.div( rx.el.div( *[integration_filter_button(data) for data in FilterOptions], - class_name="flex flex-row gap-3 items-center justify-center flex-wrap", + class_name="flex flex-wrap items-center gap-2", ), - class_name="w-full max-w-[64.19rem] pb-12", + class_name="docs-integration-filters w-full pb-8", ) @@ -58,7 +63,7 @@ def integration_gallery_cards(data): ), ui.avatar.fallback( data["name"][0], - class_name="text-foreground text-xl font-semibold uppercase size-full", + class_name="text-foreground text-xl font-normal uppercase size-full", unstyled=True, ), unstyled=True, @@ -72,19 +77,20 @@ def integration_gallery_cards(data): class_name="w-full flex flex-row items-center justify-between", ), rx.el.div( - rx.el.p( - data["title"], class_name="text-lg font-semibold text-foreground" + rx.el.h3( + data["title"], + class_name="font-heading text-xl font-normal leading-7 tracking-[-0.025em] text-foreground", ), rx.el.p( data["description"], - class_name="font-medium text-muted-foreground leading-[1.35]", + class_name="text-sm font-normal leading-6 text-muted-foreground", ), - class_name="flex flex-col gap-y-1", + class_name="flex min-w-0 flex-col gap-y-2", ), - class_name="flex flex-col gap-y-6 rounded-ui-xl border border-border-subtle bg-background shadow-small p-6 h-[13rem] justify-between hover:bg-muted", + class_name="flex min-h-[13rem] w-full min-w-0 flex-col justify-between gap-y-6 rounded-[20px] border border-border-subtle bg-background p-6 transition-colors hover:bg-muted hover:border-border motion-reduce:transition-none", ), href=data["path"], - class_name="group text-inherit hover:!text-inherit decoration-none no-underline " + class_name="docs-integration-card group min-w-0 rounded-[20px] text-inherit hover:!text-inherit no-underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring " + rx.cond( (selected_filter.value == data["tags"]) | (selected_filter.value == "All"), "flex", @@ -100,8 +106,9 @@ def integration_gallery(): integration_gallery_cards(next(iter(item.values()))) for item in get_integration_path() ], - class_name="w-full grid lg:grid-cols-2 md:grid-cols-2 grid-cols-1 gap-6", + class_name="grid w-full min-w-0 grid-cols-1 gap-5 md:grid-cols-2", ), + class_name="docs-integration-catalog w-full min-w-0", ) @@ -113,5 +120,5 @@ def integration_request_form(): "Click ", request_integration_dialog(), " to tell us what you need." ) ), - class_name="w-full max-w-[64.19rem] flex flex-col gap-y-1 text-md font-semibold py-10 items-center justify-center", + class_name="w-full flex flex-col items-center justify-center gap-y-1 py-10 text-center text-sm font-normal leading-6 text-muted-foreground", ) diff --git a/docs/app/reflex_docs/pages/library_previews.py b/docs/app/reflex_docs/pages/library_previews.py index d669c6fec4d..69fe89c3676 100644 --- a/docs/app/reflex_docs/pages/library_previews.py +++ b/docs/app/reflex_docs/pages/library_previews.py @@ -55,6 +55,16 @@ def get_preview_asset(name: str, section: str) -> tuple[str, str]: def component_card(name: str, link: str, section: str) -> rx.Component: + """Link a component preview to its reference without obscuring the artwork. + + Args: + name: Component name used to select its preview and visible label. + link: Canonical component reference URL. + section: Library category containing the preview. + + Returns: + A single focusable card with a decorative preview and readable label. + """ preview_section, preview_name = get_preview_asset(name, section) def preview_src(mode: str) -> str: @@ -71,33 +81,40 @@ def preview_src(mode: str) -> str: return rx.asset(path) return f"{REFLEX_ASSETS_CDN}{path}" - return rx.link( - rx.box( + return rx.el.a( + rx.el.div( rx.image( src=preview_src("light"), loading="lazy", - alt=f"Image preview of {name}", + alt="", style={"filter": "grayscale(1)"}, class_name="object-contain object-center h-full w-full dark:hidden", ), rx.image( src=preview_src("dark"), loading="lazy", - alt=f"Image preview of {name}", - style={"filter": "grayscale(1) brightness(1.8)"}, + alt="", + style={"filter": "grayscale(1)"}, class_name="object-contain object-center h-full w-full dark:block hidden", ), - rx.box( - rx.text( - get_display_name(name), - class_name="truncate font-base text-foreground", - ), - rx.icon("chevron-right", size=14, class_name="!text-subtle-foreground"), - class_name="bottom-0 absolute flex flex-row justify-between w-full px-4 py-2 items-center", + aria_hidden=True, + class_name="aspect-[320/232] w-full overflow-hidden border-b border-border-subtle bg-muted", + ), + rx.el.div( + rx.el.span( + get_display_name(name), + class_name="min-w-0 font-heading text-lg font-normal leading-6 tracking-[-0.02em] text-foreground", + ), + rx.icon( + "arrow-up-right", + size=16, + aria_hidden=True, + class_name="shrink-0 text-muted-foreground", ), - class_name="aspect-[320/232] rounded-xl border overflow-hidden relative box-border shadow-large bg-muted hover:bg-accent transition-bg border-border", + class_name="flex min-h-16 items-center justify-between gap-3 px-5 py-4", ), href=link, + class_name="docs-library-card group flex min-w-0 flex-col overflow-hidden rounded-[20px] border border-border-subtle bg-background no-underline transition-colors hover:border-border-strong hover:bg-subtle motion-reduce:transition-none focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-ring", ) @@ -156,7 +173,7 @@ def page() -> rx.Component: "and [rendering iterables](/docs/components/rendering-iterables/) " "to make your interface respond to state changes." ), - class_name="flex flex-col w-full mb-10", + class_name="docs-library flex min-w-0 flex-col w-full mb-10", ) return rx.box( rx.box( @@ -181,9 +198,9 @@ def page() -> rx.Component: component_category, component_list[component_category] ) ], - class_name="gap-6 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3", + class_name="gap-5 grid min-w-0 grid-cols-1 md:grid-cols-2 xl:grid-cols-3", ), - class_name="flex flex-col h-full mb-10", + class_name="docs-library flex min-w-0 flex-col h-full mb-10", ) return page diff --git a/docs/app/reflex_docs/pages/page404.py b/docs/app/reflex_docs/pages/page404.py index 57fa23704a5..3d51f7063ba 100644 --- a/docs/app/reflex_docs/pages/page404.py +++ b/docs/app/reflex_docs/pages/page404.py @@ -19,14 +19,14 @@ def not_found() -> rx.Component: docs_navbar(), rx.el.main( rx.el.div( - rx.el.p("404", class_name="text-sm font-book text-muted-foreground"), + rx.el.p("404", class_name="text-sm font-normal text-muted-foreground"), rx.el.h1( "Page not found", - class_name="text-4xl sm:text-5xl font-book tracking-tight text-foreground", + class_name="font-heading text-4xl font-light leading-[1.1] tracking-[-0.04em] text-foreground sm:text-5xl", ), rx.el.p( "We couldn't find that documentation page. Browse the docs or use search to find what you need.", - class_name="max-w-md text-base leading-7 text-muted-foreground", + class_name="max-w-md text-base font-normal leading-7 text-muted-foreground", ), rx.el.elements.a( button( @@ -34,15 +34,16 @@ def not_found() -> rx.Component: variant="primary", size="md", native_button=False, + class_name="!font-normal !shadow-none", ), href="/docs/", class_name="rounded-full focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring", ), - class_name="flex min-h-[65vh] flex-col items-start justify-center gap-6 mx-auto max-w-(--landing-layout-max-width) px-6 pb-16 pt-[calc(var(--docs-header-height)+4rem)]", + class_name="mx-auto flex min-h-[65vh] w-full max-w-[1176px] flex-col items-start justify-center gap-6 px-4 pb-16 pt-[calc(var(--docs-header-height)+4rem)] sm:px-8", ), editorial_footer(), ), - class_name="min-h-screen bg-background", + class_name="docs-not-found min-h-screen bg-background font-sans text-foreground", ) diff --git a/docs/app/reflex_docs/reflex_docs.py b/docs/app/reflex_docs/reflex_docs.py index dfc48f5e800..943ee55c3b1 100644 --- a/docs/app/reflex_docs/reflex_docs.py +++ b/docs/app/reflex_docs/reflex_docs.py @@ -74,6 +74,11 @@ def _llms_txt_directive() -> rx.Component: # Create the app. app = rxe.App( style=styles.BASE_STYLE, + stylesheets=[ + "/docs-theme.css", + "/docs-header-theme.css", + "/docs-overlays-theme.css", + ], app_wraps={}, theme=rx.theme( _llms_txt_directive(), diff --git a/docs/app/reflex_docs/templates/docpage/docpage.py b/docs/app/reflex_docs/templates/docpage/docpage.py index 6be438bfc8d..04299b581ef 100644 --- a/docs/app/reflex_docs/templates/docpage/docpage.py +++ b/docs/app/reflex_docs/templates/docpage/docpage.py @@ -354,7 +354,11 @@ def page_navigation_link(title: str, href: str, *, forward: bool) -> rx.Componen class_name=( "group flex min-w-0 flex-col gap-1 rounded-lg p-3 no-underline " "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring " - + ("items-end text-right" if forward else "items-start text-left") + + ( + "col-start-2 items-end text-right" + if forward + else "items-start text-left" + ) ), ) @@ -444,9 +448,6 @@ def wrapper(*args, **kwargs) -> rx.Component: forward=False, ) ) - else: - links.append(rx.fragment()) - links.append(rx.spacer()) if next: links.append( page_navigation_link( @@ -455,8 +456,6 @@ def wrapper(*args, **kwargs) -> rx.Component: forward=True, ) ) - else: - links.append(rx.fragment()) toc = [] doc_content = None @@ -488,14 +487,14 @@ def wrapper(*args, **kwargs) -> rx.Component: doc_content=doc_content, ), class_name=( - "px-0 pt-0 mb-[2rem]" + "docs-breadcrumb px-0 pt-0 mb-[2rem]" + ("" if show_breadcrumb else " lg:hidden") ), ), rx.box( rx.el.article( comp, - class_name="[&>div]:!p-0" + class_name="docs-article [&>div]:!p-0" + ( " [&_.rt-TableRoot]:!border-0 [&_.rt-TableRoot]:!rounded-none" " [&_.rt-TableCell]:!shadow-none [&_.rt-TableCell]:!border-b [&_.rt-TableCell]:!border-border" @@ -507,7 +506,8 @@ def wrapper(*args, **kwargs) -> rx.Component: ), rx.el.nav( *links, - class_name="flex flex-row gap-2 mt-8 lg:mt-10 mb-6 lg:mb-12", + aria_label="Adjacent documentation pages", + class_name="docs-page-navigation grid grid-cols-2 gap-2 mt-8 lg:mt-10 mb-6 lg:mb-12", ), docpage_footer(path=path.rstrip("/"), edit_href=edit_href), class_name="lg:mt-0 h-auto" @@ -518,7 +518,7 @@ def wrapper(*args, **kwargs) -> rx.Component: ), ), class_name=ui.cn( - "flex-1 min-w-0 h-auto mx-auto lg:max-w-[56rem] px-4 lg:px-8 xl:px-12 overflow-y-auto", + "docs-reading-column flex-1 min-w-0 h-auto mx-auto lg:max-w-[56rem] px-4 lg:px-8 xl:px-12 overflow-y-auto", "lg:max-w-[68rem]" if not show_right_sidebar else "", ), ), @@ -531,7 +531,7 @@ def wrapper(*args, **kwargs) -> rx.Component: else rx.box( class_name="w-[180px] h-screen sticky top-0 shrink-0 hidden xl:block" ), - class_name="flex justify-center mx-auto mt-0 max-w-[108rem] h-full min-h-screen w-full", + class_name="docs-reading-layout flex justify-center mx-auto mt-0 max-w-[108rem] h-full min-h-screen w-full", ), on_mount=rx.call_script(right_sidebar_item_highlight()), ) diff --git a/docs/app/reflex_docs/views/docs_announcement_banner.py b/docs/app/reflex_docs/views/docs_announcement_banner.py new file mode 100644 index 00000000000..f858e7c60ca --- /dev/null +++ b/docs/app/reflex_docs/views/docs_announcement_banner.py @@ -0,0 +1,58 @@ +"""Docs announcement using the marketing site's opal glass artwork.""" + +import reflex as rx +import reflex_components_internal as ui +from reflex_site_shared.views.announcement_banner import XY_IN_REFLEX_BUILD_URL +from reflex_site_shared.views.hosting_banner import HostingBannerState + + +def docs_announcement_banner() -> rx.Component: + """Render the shared announcement content with docs-local presentation. + + Returns: + A wrapper that shows the dismissible announcement while the banner is visible. + """ + return rx.el.div( + rx.cond( + HostingBannerState.is_banner_visible, + rx.el.div( + rx.el.div( + *[rx.el.span() for _ in range(5)], + aria_hidden=True, + class_name="docs-announcement-glass", + ), + rx.el.elements.a( + rx.el.span( + rx.el.span( + "New", + aria_hidden=True, + class_name="docs-announcement-badge", + ), + rx.el.span( + "xy is now in Reflex Build", + class_name="docs-announcement-message", + ), + rx.el.span("Read more", class_name="docs-announcement-cta"), + class_name="docs-announcement-content", + ), + href=XY_IN_REFLEX_BUILD_URL, + target="_blank", + rel="noopener noreferrer", + aria_label="New: xy is now in Reflex Build — Read more (opens in a new tab)", + class_name="docs-announcement-link", + ), + rx.el.button( + ui.icon("Cancel01Icon", class_name="size-4", aria_hidden=True), + type="button", + aria_label="Dismiss announcement", + on_click=HostingBannerState.hide_banner, + class_name="docs-announcement-dismiss", + ), + custom_attrs={"data-announcement": ""}, + role="region", + aria_label="Latest announcement", + class_name="docs-announcement", + ), + ), + on_mount=HostingBannerState.show_agent_toolkit_banner, + ) diff --git a/docs/app/reflex_docs/views/docs_navbar.py b/docs/app/reflex_docs/views/docs_navbar.py index 4b0880a53c4..b0020e3ea36 100644 --- a/docs/app/reflex_docs/views/docs_navbar.py +++ b/docs/app/reflex_docs/views/docs_navbar.py @@ -1,6 +1,5 @@ import reflex as rx import reflex_components_internal as ui -from reflex_site_shared.components.docs_shell import docs_navbar_frame from reflex_site_shared.components.icons import get_icon from reflex_site_shared.components.marketing_button import button from reflex_site_shared.constants import ( @@ -13,43 +12,58 @@ from reflex_docs.components.docpage.navbar.buttons.sidebar import navbar_sidebar_button from reflex_docs.pages.docs import getting_started, hosting +from reflex_docs.views.docs_announcement_banner import docs_announcement_banner from reflex_docs.views.search import search_bar def github_button() -> rx.Component: + """Render the compact GitHub link with its combined star count. + + Returns: + A link to the Reflex GitHub repository showing the combined star count. + """ total = GITHUB_STARS + XY_GITHUB_STARS stars = f"{(total + 999) // 1000}K" label = f"View Reflex on GitHub — {total:,} combined stars across Reflex and XY" return rx.el.elements.a( button( - get_icon(icon="github_navbar", class_name="shrink-0"), - stars, + get_icon( + icon="github_navbar", + class_name="size-4 shrink-0 [&>svg]:block [&>svg]:size-full", + ), + rx.el.span(stars, class_name="tabular-nums"), custom_attrs={"aria-label": label}, size="sm", variant="ghost", native_button=False, + class_name="gap-2 rounded-full px-3 text-sm font-normal text-muted-foreground hover:text-foreground", ), href=GITHUB_URL, target="_blank", rel="noopener noreferrer", custom_attrs={"aria-label": label}, + class_name="docs-navbar-github rounded-full focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", ) def logo() -> rx.Component: - """Link each wordmark to its respective site overview.""" + """Link each wordmark to its respective site overview. + + Returns: + The Reflex and Docs wordmarks, each linking to its overview. + """ focus_class = "rounded-sm focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring" return rx.el.div( rx.el.elements.a( rx.image( src=f"{REFLEX_ASSETS_CDN}logos/light/reflex.svg", alt="Reflex Logo", - class_name="shrink-0 block dark:hidden", + class_name="docs-navbar-wordmark block w-auto shrink-0 dark:hidden", ), rx.image( src=f"{REFLEX_ASSETS_CDN}logos/dark/reflex.svg", alt="Reflex Logo", - class_name="shrink-0 hidden dark:block", + class_name="docs-navbar-wordmark hidden w-auto shrink-0 dark:block", ), href=REFLEX_URL, aria_label="Reflex home", @@ -59,26 +73,37 @@ def logo() -> rx.Component: rx.image( src=f"{REFLEX_ASSETS_CDN}logos/light/docs.svg", alt="Docs Logo", - class_name="shrink-0 block dark:hidden", + class_name="docs-navbar-docs-wordmark block w-auto shrink-0 dark:hidden", style={"filter": "grayscale(1)"}, ), rx.image( src=f"{REFLEX_ASSETS_CDN}logos/dark/docs.svg", alt="Docs Logo", - class_name="shrink-0 hidden dark:block", + class_name="docs-navbar-docs-wordmark hidden w-auto shrink-0 dark:block", style={"filter": "grayscale(1) brightness(2)"}, ), href="/", aria_label="Docs overview", class_name=focus_class, ), - class_name="flex flex-row gap-2.5 items-center shrink-0 mr-10", + class_name="docs-navbar-brand flex shrink-0 flex-row items-center gap-3", ) def menu_item( text: str, href: str, active_str: str = "", external: bool = False ) -> rx.Component: + """Render a section link using the docs router or an explicit external URL. + + Args: + text: Visible section name. + href: Destination relative to the docs mount, or a complete external path. + active_str: Route segment used to identify the active section. + external: Whether the destination must bypass the docs router. + + Returns: + A navigation item containing one focusable link. + """ router_path = rx.State.router.page.path # For paths starting with "/" (like Start), use exact match @@ -120,7 +145,7 @@ def menu_item( href=href, aria_current=rx.cond(active, "page", None), class_name=ui.cn( - "inline-flex h-9 items-center justify-center whitespace-nowrap rounded-full px-4 text-sm font-book leading-none transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", + "docs-navbar-link inline-flex h-9 items-center justify-center whitespace-nowrap rounded-full px-3 text-sm font-normal leading-none transition-colors duration-150 motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", rx.cond( active, "bg-accent text-foreground", @@ -134,6 +159,11 @@ def menu_item( def navigation_menu() -> rx.Component: + """Render the docs section links, search, and responsive navigation actions. + + Returns: + The navigation menu holding the section links and header actions. + """ return ui.navigation_menu.root( ui.navigation_menu.list( menu_item("Overview", "/", "/"), @@ -141,7 +171,7 @@ def navigation_menu() -> rx.Component: menu_item("Framework", getting_started.introduction.path, "framework"), menu_item("Cloud", hosting.deploy_quick_start.path, "hosting"), menu_item("XY", "/docs/xy/", "xy", external=True), - class_name="hidden xl:flex flex-row items-center gap-2 m-0 h-full list-none", + class_name="docs-navbar-sections hidden xl:flex m-0 h-full list-none flex-row items-center gap-1", custom_attrs={"role": "menubar"}, ), ui.navigation_menu.list( @@ -154,6 +184,7 @@ def navigation_menu() -> rx.Component: ui.navigation_menu.item( search_bar(), unstyled=True, + class_name="docs-navbar-search flex items-center", custom_attrs={"role": "menuitem"}, ), ui.navigation_menu.item( @@ -162,11 +193,11 @@ def navigation_menu() -> rx.Component: "Book a Demo", size="sm", variant="primary", - class_name="whitespace-nowrap", + class_name="rounded-full whitespace-nowrap px-4 text-sm font-normal", native_button=False, ), href="https://reflex.dev/demo/", - class_name="rounded-full focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", + class_name="docs-navbar-cta rounded-full focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", ), unstyled=True, class_name="xl:flex hidden", @@ -174,11 +205,11 @@ def navigation_menu() -> rx.Component: ), ui.navigation_menu.item( navbar_sidebar_button(), - class_name="xl:hidden flex", + class_name="docs-navbar-mobile xl:hidden flex", unstyled=True, custom_attrs={"role": "menuitem"}, ), - class_name="flex flex-row lg:gap-4 gap-2 m-0 h-full list-none items-center", + class_name="docs-navbar-actions m-0 flex h-full shrink-0 list-none flex-row items-center gap-2 lg:gap-3", custom_attrs={"role": "menubar"}, ), ui.navigation_menu.portal( @@ -197,10 +228,26 @@ def navigation_menu() -> rx.Component: ), ), unstyled=True, - class_name="relative flex w-full items-center h-full justify-end xl:justify-between gap-6 mx-auto flex-row", + class_name="docs-navbar-nav relative mx-auto flex h-full min-w-0 w-full flex-row items-center justify-end gap-4 font-sans xl:justify-between", ) @rx.memo def docs_navbar() -> rx.Component: - return docs_navbar_frame(logo(), navigation_menu()) + """Render the compact docs header with the marketing announcement treatment. + + Returns: + The fixed header frame with the announcement banner above the navbar. + """ + return rx.el.div( + docs_announcement_banner(), + rx.el.header( + rx.el.div( + logo(), + navigation_menu(), + class_name="docs-navbar-inner mx-auto flex h-full w-full min-w-0 flex-row items-center justify-between gap-6", + ), + class_name="docs-navbar-header relative [&_nav]:!static [&_ul]:!static flex h-16 w-full items-center", + ), + class_name="docs-nav-frame fixed top-0 z-[9999] flex w-full flex-col self-center", + ) diff --git a/docs/app/rxconfig.py b/docs/app/rxconfig.py index 890bc07d606..810f2f6c9f3 100644 --- a/docs/app/rxconfig.py +++ b/docs/app/rxconfig.py @@ -12,6 +12,8 @@ frontend_packages=[ "tailwindcss-animated@2.1.0", "tailwindcss-scroll-mask@0.0.5", + "@fontsource-variable/geist@5.3.0", + "@fontsource-variable/inter@5.3.0", "@fontsource-variable/instrument-sans@5.3.0", "@fontsource-variable/jetbrains-mono@5.3.0", ], diff --git a/docs/app/tests/test_docpage_pager.py b/docs/app/tests/test_docpage_pager.py index 9e15ffe7035..97b12c8f173 100644 --- a/docs/app/tests/test_docpage_pager.py +++ b/docs/app/tests/test_docpage_pager.py @@ -1,6 +1,36 @@ """Regression coverage for complete previous/next navigation targets.""" +import importlib + import pytest +import reflex as rx + + +def test_pager_does_not_reserve_a_third_empty_column(monkeypatch): + """Both adjacent articles receive the available reading-column width.""" + import reflex_docs.pages # noqa: F401 + + docpage_module = importlib.import_module("reflex_docs.templates.docpage.docpage") + # Wrapping placeholder content keeps markdown demos that earlier tests + # already executed from defining their states a second time. + monkeypatch.setattr( + docpage_module, "doc_edit_hrefs", dict(docpage_module.doc_edit_hrefs) + ) + article = docpage_module.docpage(set_path="/getting-started/basics/", t="Basics")( + lambda: rx.el.div() + ) + + pending = [article.component()] + while pending: + node = pending.pop() + if "docs-page-navigation" in str(node.class_name): + assert len(node.children) == 2 + back, forward = node.children + assert type(back).__name__ == type(forward).__name__ == "ReactRouterLink" + assert "col-start-2" in str(forward.class_name) + return + pending.extend(node.children) + pytest.fail("Article navigation was not rendered") @pytest.mark.parametrize("forward", [False, True]) diff --git a/docs/app/tests/test_docs_features_browser.py b/docs/app/tests/test_docs_features_browser.py index bedc5338121..a0262a48952 100644 --- a/docs/app/tests/test_docs_features_browser.py +++ b/docs/app/tests/test_docs_features_browser.py @@ -1,6 +1,7 @@ """Browser regressions for imported docs features; set REFLEX_DOCS_PREVIEW_URL.""" import os +import re from urllib.parse import quote import pytest @@ -14,14 +15,15 @@ @pytest.mark.parametrize("width", [390, 768, 1440]) def test_docs_reading_typography_is_consistent(page: Page, width: int): - """Prose, inline links, and reference headings share the reading styles.""" + """Editorial type is consistent without overriding live-example headings.""" page.set_viewport_size({"width": width, "height": 1000}) page.goto(f"{PREVIEW_URL}/docs/getting-started/introduction/") paragraph = ( page.locator("p").filter(has_text="State holds the app's mutable data").first ) - expect(paragraph).to_have_css("font-weight", "450") - expect(paragraph).to_have_css("color", "rgb(24, 24, 24)") + expect(paragraph).to_have_css("font-family", re.compile(r"^Inter(?:,|$)")) + expect(paragraph).to_have_css("font-weight", "400") + expect(paragraph).to_have_css("color", "rgb(0, 0, 0)") link = paragraph.locator("a").first for property_name in ("font-size", "font-weight", "line-height", "letter-spacing"): expected = link.evaluate( @@ -29,10 +31,18 @@ def test_docs_reading_typography_is_consistent(page: Page, width: int): property_name, ) expect(link).to_have_css(property_name, expected) - heading_size = "32px" if width >= 1024 else "24px" - expect(page.get_by_role("heading", name="Goals", exact=True)).to_have_css( - "font-size", heading_size + heading_size = "30px" if width >= 640 else "26px" + heading = page.get_by_role("heading", name="Goals", exact=True) + expect(heading).to_have_css("font-size", heading_size) + expect(heading).to_have_css("font-family", re.compile(r"^Geist(?:,|$)")) + expect(heading).to_have_css("font-weight", "400") + live_heading = ( + page + .get_by_role("button", name="Increment", exact=True) + .locator("..") + .get_by_role("heading") ) + expect(live_heading).to_have_css("font-weight", "700") page.goto(f"{PREVIEW_URL}/docs/api-reference/app/") expect(page.get_by_role("heading", name="Fields", exact=True)).to_have_css( "font-size", heading_size @@ -40,20 +50,20 @@ def test_docs_reading_typography_is_consistent(page: Page, width: int): def test_sidebar_labels_match_reading_text_color(page: Page): - """Navigation labels share the body foreground while section captions recede.""" + """Section labels use foreground while inactive links and captions recede.""" page.set_viewport_size({"width": 1440, "height": 1000}) page.goto(f"{PREVIEW_URL}/docs/getting-started/introduction/") expect( page.get_by_role("link", name="Navigate to Components").locator("h3") - ).to_have_css("color", "rgb(24, 24, 24)") + ).to_have_css("color", "rgb(0, 0, 0)") expect(page.locator("summary").filter(has_text="Getting Started")).to_have_css( - "color", "rgb(24, 24, 24)" + "color", "rgb(0, 0, 0)" ) expect( page.get_by_role("link", name="Installation", exact=True).locator("p") - ).to_have_css("color", "rgb(24, 24, 24)") + ).to_have_css("color", "rgb(105, 100, 94)") expect(page.get_by_role("heading", name="Onboarding", exact=True)).to_have_css( - "color", "rgb(89, 89, 89)" + "color", "rgb(105, 100, 94)" ) @@ -247,7 +257,7 @@ def test_single_page_sidebar_links_keep_neutral_hover_color(page: Page): for label in ("Databases", "Webhooks", "Files"): link = sidebar.get_by_role("link", name=label, exact=True) link.hover() - expect(link.locator("p")).to_have_css("color", "rgb(24, 24, 24)") + expect(link.locator("p")).to_have_css("color", "rgb(0, 0, 0)") @pytest.mark.parametrize("width", [390, 1440]) @@ -262,15 +272,15 @@ def test_single_page_sidebar_link_has_selected_background(page: Page, width: int sidebar = page.locator("#sidebar-container") selected = sidebar.get_by_role("link", name="Webhooks", exact=True) expect(selected).to_have_attribute("aria-current", "page") - expect(selected).to_have_css("background-color", "rgb(246, 246, 246)") - expect(selected.locator("p")).to_have_css("color", "rgb(24, 24, 24)") + expect(selected).to_have_css("background-color", "rgb(245, 243, 241)") + expect(selected.locator("p")).to_have_css("color", "rgb(0, 0, 0)") inactive = sidebar.get_by_role("link", name="Files", exact=True) expect(inactive).to_have_css("background-color", "rgba(0, 0, 0, 0)") inactive.click() if width < 1024: page.get_by_role("button", name="Open documentation navigation").click() expect(sidebar.get_by_role("link", name="Files", exact=True)).to_have_css( - "background-color", "rgb(246, 246, 246)" + "background-color", "rgb(245, 243, 241)" ) expect(sidebar.get_by_role("link", name="Webhooks", exact=True)).to_have_css( "background-color", "rgba(0, 0, 0, 0)" diff --git a/docs/app/tests/test_docs_navbar.py b/docs/app/tests/test_docs_navbar.py index 070610cceb1..0a3f150e7e1 100644 --- a/docs/app/tests/test_docs_navbar.py +++ b/docs/app/tests/test_docs_navbar.py @@ -51,6 +51,20 @@ def _collect_links(component) -> list[tuple[str, str]]: return links +def _walk_components(component): + """Yield a component and its descendants, including conditional branches. + + Args: + component: The root component to walk. + + Yields: + Each component in the tree. + """ + yield component + for child in getattr(component, "children", ()): + yield from _walk_components(child) + + def test_internal_menu_items_use_router_links(navbar): """In-app navbar links must compile to React Router links. @@ -195,3 +209,52 @@ def test_mobile_menu_preserves_primary_docs_destinations(navbar): "/docs/xy/", ): assert ("anchor", path) in links + + +def test_docs_announcement_preserves_link_and_dismissal(navbar): + """The branded announcement remains an accessible, dismissible external link.""" + from reflex_docs.views.docs_announcement_banner import docs_announcement_banner + + banner = docs_announcement_banner() + assert _collect_links(banner) == [ + ("anchor", "https://reflex.dev/blog/xy-in-reflex-build/") + ] + region = next( + component + for component in _walk_components(banner) + if "data-announcement" in getattr(component, "custom_attrs", {}) + ) + artwork, link, dismiss = region.children + assert artwork.custom_attrs["aria-hidden"] is True + assert "Latest announcement" in str(region.custom_attrs["aria-label"]) + assert str(link.target) == '"_blank"' + assert "noopener noreferrer" in str(link.rel) + assert "opens in a new tab" in str(link.custom_attrs["aria-label"]) + assert "Dismiss announcement" in str(dismiss.custom_attrs["aria-label"]) + assert "hide_banner" in str(dismiss.event_triggers["on_click"]) + + +def test_mobile_navigation_retains_native_disclosure(navbar): + """Styling must preserve native summary activation and a named navigation area.""" + menu = navbar.navbar_sidebar_button() + assert menu.tag == "details" + assert menu.children[0].tag == "summary" + assert "Toggle navigation menu" in str(menu.children[0]) + assert str(menu.children[1].role) == '"navigation"' + assert "Documentation navigation" in str(menu.children[1]) + + +def test_docs_sidebar_drawer_has_a_title_and_keyboard_close(navbar): + """Mobile sidebar readers need a named dialog and a focusable close control.""" + from reflex_site_shared.views.sidebar import docs_sidebar_drawer + + drawer = docs_sidebar_drawer(rx.text("Guides"), rx.el.button("Open guides")) + nodes = list(_walk_components(drawer)) + titles = [node for node in nodes if node.tag == "Drawer.Title"] + assert len(titles) == 1 + assert "Documentation navigation" in str(titles[0]) + close = next(node for node in nodes if node.tag == "Drawer.Close") + assert close.children[0].tag == "button" + assert ( + close.children[0].custom_attrs["aria-label"] == "Close documentation navigation" + ) diff --git a/docs/app/tests/test_editorial_browser.py b/docs/app/tests/test_editorial_browser.py index c4ca24157ba..0a3e92c4295 100644 --- a/docs/app/tests/test_editorial_browser.py +++ b/docs/app/tests/test_editorial_browser.py @@ -60,6 +60,33 @@ def test_article_theme_preserves_live_example_styles_and_events(page: Page): expect(value).to_have_text(str(before + 1)) +@pytest.mark.parametrize("width", [320, 390]) +def test_wide_demo_starts_inside_its_scroll_container(page: Page, width: int): + """A fixed-width example must not lose its left edge on a narrow screen.""" + page.set_viewport_size({"width": width, "height": 900}) + page.goto(f"{PREVIEW_URL}/docs/library/media/video/", wait_until="networkidle") + preview = page.locator(".docs-demo-preview") + video = preview.locator("youtube-video") + expect(video).to_be_attached() + assert video.bounding_box()["x"] >= preview.bounding_box()["x"] + assert preview.evaluate("el => el.scrollWidth > el.clientWidth") + assert page.evaluate("document.documentElement.scrollWidth <= innerWidth") + + +@pytest.mark.parametrize("width", [320, 390]) +def test_long_component_names_wrap_inside_the_reading_column(page: Page, width: int): + """API identifiers must not create a second horizontal page scroller.""" + page.set_viewport_size({"width": width, "height": 900}) + page.goto( + f"{PREVIEW_URL}/docs/library/overlay/dropdown-menu/", wait_until="networkidle" + ) + column = page.locator(".docs-reading-column") + assert column.evaluate("el => el.scrollWidth <= el.clientWidth") + assert page.locator(".docs-heading").evaluate_all( + "els => els.every(el => el.getBoundingClientRect().right <= innerWidth - 20)" + ) + + def test_low_level_form_example_has_valid_inline_result_markup(page: Page): """Inline submitted values must not cause the browser to repair nested paragraphs.""" errors = [] diff --git a/docs/app/tests/test_site_quality.py b/docs/app/tests/test_site_quality.py index 167195d182f..a5d5e1d009b 100644 --- a/docs/app/tests/test_site_quality.py +++ b/docs/app/tests/test_site_quality.py @@ -150,3 +150,17 @@ def test_cloud_overview_exposes_markdown_and_toc(): assert len(toc) >= 4 assert markdown == CLOUD_OVERVIEW_MARKDOWN assert "Deploy your first app" in str(body) + + +def test_generated_preview_keeps_wide_components_scrollable(): + """The preview retains its example width inside a local scroll container.""" + from reflex_docs.pages.docs.component import component_docs + + rendered = component_docs( + (type(rx.button()), "rx.button"), + {"Button": 'lambda **props: rx.button("Wide preview", width="400px", **props)'}, + ) + playground = rendered.children[2].children[0] + preview = playground.children[0] + assert "overflow-x-auto" in str(preview.class_name) + assert "400px" in str(preview.children[0]) diff --git a/packages/reflex-site-shared/news/+docs-footer-mobile.bugfix.md b/packages/reflex-site-shared/news/+docs-footer-mobile.bugfix.md new file mode 100644 index 00000000000..bff0b1f38f4 --- /dev/null +++ b/packages/reflex-site-shared/news/+docs-footer-mobile.bugfix.md @@ -0,0 +1 @@ +Allow documentation footer theme and social controls to wrap on narrow screens so every link stays reachable. diff --git a/packages/reflex-site-shared/news/+docs-theme-hooks.docs.md b/packages/reflex-site-shared/news/+docs-theme-hooks.docs.md new file mode 100644 index 00000000000..dabcacf2120 --- /dev/null +++ b/packages/reflex-site-shared/news/+docs-theme-hooks.docs.md @@ -0,0 +1 @@ +Expose scoped CSS hooks for documentation chrome, editorial content, demo surfaces, tables, feedback, and footers so consuming sites can customize the brand without styling live examples. Add accessible field labels and selected-score states to the feedback form. Retain one Security link in the marketing footer. diff --git a/packages/reflex-site-shared/src/reflex_site_shared/components/blocks/demo.py b/packages/reflex-site-shared/src/reflex_site_shared/components/blocks/demo.py index 9ea526d5c5b..58ccded52c4 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/components/blocks/demo.py +++ b/packages/reflex-site-shared/src/reflex_site_shared/components/blocks/demo.py @@ -89,7 +89,7 @@ def _doc_demo_header(*triggers: rx.Component) -> rx.Component: _reflex_build_action(), doc_tab_list(*triggers), class_name=( - "flex w-full flex-wrap items-end justify-between gap-x-2 gap-y-1 " + "docs-demo-header flex w-full flex-wrap items-end justify-between gap-x-2 gap-y-1 " "sm:flex-nowrap" ), ) @@ -108,7 +108,7 @@ def docdemobox(*children, **props) -> rx.Component: return rx.box( *children, **props, - class_name="flex flex-col p-6 rounded-xl overflow-x-auto border border-border-subtle bg-muted items-center justify-center w-full", + class_name="docs-demo-preview flex flex-col p-6 rounded-xl overflow-x-auto border border-border-subtle bg-muted items-center justify-center w-full", ) diff --git a/packages/reflex-site-shared/src/reflex_site_shared/components/blocks/flexdown.py b/packages/reflex-site-shared/src/reflex_site_shared/components/blocks/flexdown.py index b2d6889366f..b44cda41713 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/components/blocks/flexdown.py +++ b/packages/reflex-site-shared/src/reflex_site_shared/components/blocks/flexdown.py @@ -53,7 +53,7 @@ def _markdown_table(*children, **props) -> rx.Component: class_name="w-full border-collapse text-sm border border-border-subtle rounded-lg overflow-hidden bg-white-1 ", **props, ), - class_name="w-full rounded-xl border border-border-subtle my-6 max-w-full overflow-hidden", + class_name="docs-markdown-table w-full rounded-xl border border-border-subtle my-6 max-w-full overflow-hidden", ) diff --git a/packages/reflex-site-shared/src/reflex_site_shared/components/blocks/headings.py b/packages/reflex-site-shared/src/reflex_site_shared/components/blocks/headings.py index aac78aeaef3..ba52291b83d 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/components/blocks/headings.py +++ b/packages/reflex-site-shared/src/reflex_site_shared/components/blocks/headings.py @@ -147,7 +147,12 @@ def create( "line_height": "1.2" if heading == "h1" else "1.25", **(style or {}), }, - class_name=class_name + " " + scroll_margin + " mt-" + mt, + class_name="docs-heading " + + class_name + + " " + + scroll_margin + + " mt-" + + mt, ), rx.icon( tag="link", diff --git a/packages/reflex-site-shared/src/reflex_site_shared/components/blocks/typography.py b/packages/reflex-site-shared/src/reflex_site_shared/components/blocks/typography.py index 2651889fd12..d8926d1db2b 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/components/blocks/typography.py +++ b/packages/reflex-site-shared/src/reflex_site_shared/components/blocks/typography.py @@ -5,7 +5,7 @@ import reflex as rx -DOCS_BODY_CLASS = "font-[450] text-foreground mb-4 leading-7" +DOCS_BODY_CLASS = "docs-prose font-[450] text-foreground mb-4 leading-7" DOCS_LINK_STYLE = { "font_size": "inherit", "font_weight": "inherit", @@ -65,7 +65,7 @@ def text_comp_2(text: rx.Var[str]) -> rx.Component: """ return rx.text( text, - class_name="font-[450] text-foreground max-w-[80%] mb-10 leading-7", + class_name="docs-prose font-[450] text-foreground max-w-[80%] mb-10 leading-7", ) diff --git a/packages/reflex-site-shared/src/reflex_site_shared/components/docs_api.py b/packages/reflex-site-shared/src/reflex_site_shared/components/docs_api.py index 671b9992b56..f2a78a4f651 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/components/docs_api.py +++ b/packages/reflex-site-shared/src/reflex_site_shared/components/docs_api.py @@ -282,7 +282,7 @@ def callable_api_reference( name, as_="h3", id=_heading_id(name), - class_name="font-large text-foreground mt-8 mb-2", + class_name="docs-heading font-large text-foreground mt-8 mb-2", ), rx.text( _summary(docstring), diff --git a/packages/reflex-site-shared/src/reflex_site_shared/components/docs_page_actions.py b/packages/reflex-site-shared/src/reflex_site_shared/components/docs_page_actions.py index eb04aabc7e4..075a130304e 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/components/docs_page_actions.py +++ b/packages/reflex-site-shared/src/reflex_site_shared/components/docs_page_actions.py @@ -310,7 +310,7 @@ def docs_page_actions( ) ), ), - class_name="hidden lg:flex flex-row items-center shrink-0", + class_name="docs-page-actions hidden lg:flex flex-row items-center shrink-0", ) diff --git a/packages/reflex-site-shared/src/reflex_site_shared/components/docs_shell.py b/packages/reflex-site-shared/src/reflex_site_shared/components/docs_shell.py index 2eb8773a3ec..d7238e1bd00 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/components/docs_shell.py +++ b/packages/reflex-site-shared/src/reflex_site_shared/components/docs_shell.py @@ -159,7 +159,7 @@ def docs_navbar_frame( ), class_name="relative [&_nav]:!static [&_ul]:!static mx-auto flex h-16 w-full max-w-full flex-row items-center border-b border-border-subtle bg-background px-6 shadow-[0_2px_6px_-2px_rgba(0,0,0,0.08)] 3xl:px-16", ), - class_name="fixed top-0 z-[9999] flex w-full flex-col self-center", + class_name="docs-nav-frame fixed top-0 z-[9999] flex w-full flex-col self-center", ) @@ -180,7 +180,7 @@ def docs_left_sidebar( return rx.box( content, class_name=ui.cn( - "sticky left-0 z-10 hidden w-[19.5rem] shrink-0 border-r border-border-subtle before:absolute before:bottom-0 before:right-0 before:top-0 before:-z-10 before:w-[100vw] before:bg-white-1 lg:block", + "docs-sidebar sticky left-0 z-10 hidden w-[19.5rem] shrink-0 border-r border-border-subtle before:absolute before:bottom-0 before:right-0 before:top-0 before:-z-10 before:w-[100vw] before:bg-white-1 lg:block", "top-[var(--docs-header-height)] h-[calc(100vh-var(--docs-header-height))]", ), style={} if show_banner else {"--docs-header-height": "4rem"}, @@ -418,6 +418,7 @@ def _feedback_choice_button( ), ), aria_label=label, + aria_pressed=active, on_click=DocsFeedbackState.set_score(score), ) @@ -437,6 +438,7 @@ def _feedback_thumb_card(score: int, icon: str, label: str) -> rx.Component: ui.icon(icon, size=16), label, type="button", + aria_pressed=DocsFeedbackState.score == score, on_click=DocsFeedbackState.set_score(score), class_name=rx.cond( DocsFeedbackState.score == score, @@ -458,6 +460,7 @@ def _feedback_content() -> rx.Component: rx.el.div( ui.textarea( name="feedback", + aria_label="Feedback", placeholder="Write a comment…", type="text", max_length=500, @@ -473,6 +476,7 @@ def _feedback_content() -> rx.Component: ), ui.input( name="email", + custom_attrs={"aria-label": "Contact email (optional)"}, type="email", placeholder="Contact email (optional)", max_length=100, @@ -491,7 +495,7 @@ def _feedback_content() -> rx.Component: ), class_name="flex flex-col gap-4 w-full", ), - class_name="p-2", + class_name="docs-feedback-content p-2", ) @@ -610,7 +614,7 @@ def docs_right_sidebar( ), class_name="h-full w-full mt-[calc(var(--docs-header-height)+2rem)]", ), - class_name="sticky top-0 hidden h-screen w-[240px] shrink-0 2xl:block", + class_name="docs-toc sticky top-0 hidden h-screen w-[240px] shrink-0 2xl:block", style={} if show_banner else {"--docs-header-height": "4rem"}, ) @@ -646,7 +650,7 @@ def docs_footer_shell( copyright_status, class_name="flex w-full flex-col justify-between gap-10 py-6 lg:py-8", ), - class_name="flex w-full max-w-full flex-col lg:max-w-none", + class_name="docs-page-footer flex w-full max-w-full flex-col lg:max-w-none", ) @@ -840,7 +844,7 @@ def _docs_page_footer_content( controls = rx.box( rx.box(dark_mode_toggle(), class_name="[&>div]:!ml-0"), _docs_social_menu(), - class_name="flex w-full flex-row items-end justify-between gap-6", + class_name="flex w-full flex-row flex-wrap items-end justify-between gap-6", ) copyright_status = rx.el.div( rx.text( diff --git a/packages/reflex-site-shared/src/reflex_site_shared/docs/markdown.py b/packages/reflex-site-shared/src/reflex_site_shared/docs/markdown.py index c1c7599d712..8e6e0991b0a 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/docs/markdown.py +++ b/packages/reflex-site-shared/src/reflex_site_shared/docs/markdown.py @@ -447,7 +447,7 @@ def table(self, block: TableBlock) -> rx.Component: rx.table.body(*rows), variant="surface", size="1", - class_name="w-full border border-border-subtle mb-4", + class_name="docs-markdown-table w-full border border-border-subtle mb-4", ) def transform_table_row(self, row: TableRow) -> rx.Component: @@ -695,6 +695,7 @@ def title_comp() -> rx.Component: margin_bottom="16px", margin_top="16px", width="100%", + class_name="docs-callout", ) def _render_tutorial_intro(self, block: DirectiveBlock) -> rx.Component: diff --git a/packages/reflex-site-shared/src/reflex_site_shared/views/marketing_footer.py b/packages/reflex-site-shared/src/reflex_site_shared/views/marketing_footer.py index 49dd546420c..f6645fa5616 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/views/marketing_footer.py +++ b/packages/reflex-site-shared/src/reflex_site_shared/views/marketing_footer.py @@ -470,7 +470,6 @@ def marketing_footer( "Trust", [ ("Security", "/security/"), - ("Security review", "/security/#security-review"), ("System status", "https://status.reflex.dev"), ], ), @@ -521,7 +520,7 @@ def marketing_footer( class_name=("mx-auto w-full max-w-[90rem] px-4 min-[55rem]:px-8 lg:px-12"), ), class_name=( - "relative w-full overflow-x-clip " + "marketing-footer relative w-full overflow-x-clip " + ( "bg-background text-foreground" if appearance == "light" diff --git a/packages/reflex-site-shared/src/reflex_site_shared/views/sidebar/__init__.py b/packages/reflex-site-shared/src/reflex_site_shared/views/sidebar/__init__.py index 680195e85db..cdc1437ee12 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/views/sidebar/__init__.py +++ b/packages/reflex-site-shared/src/reflex_site_shared/views/sidebar/__init__.py @@ -496,7 +496,11 @@ def navbar_sidebar_drawer( def docs_sidebar_drawer(sidebar: rx.Component, trigger: rx.Component) -> rx.Component: - """Docs sidebar drawer. + """Render the documentation sidebar in a named, dismissible mobile drawer. + + Args: + sidebar: Navigation content displayed inside the drawer. + trigger: Control that opens the drawer. Returns: The component. @@ -509,16 +513,34 @@ def docs_sidebar_drawer(sidebar: rx.Component, trigger: rx.Component) -> rx.Comp ), rx.drawer.content( rx.el.div( - rx.drawer.close( - rx.el.div( - class_name="absolute left-1/2 transform -translate-x-1/2 top-[-12px] flex-shrink-0 bg-subtle-foreground rounded-full w-[96px] h-[5px]", + rx.el.div( + aria_hidden=True, + class_name="absolute left-1/2 transform -translate-x-1/2 top-[-12px] flex-shrink-0 bg-subtle-foreground rounded-full w-[96px] h-[5px]", + ), + rx.el.div( + rx.drawer.title( + "Documentation navigation", + class_name="docs-sidebar-drawer-title m-0 text-sm font-normal leading-6 tracking-normal text-foreground", + ), + rx.drawer.close( + rx.el.button( + ui.icon( + "Cancel01Icon", + aria_hidden=True, + class_name="size-4", + ), + type="button", + aria_label="Close documentation navigation", + class_name="flex size-9 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", + ), + as_child=True, ), - as_child=True, + class_name="docs-sidebar-drawer-header flex shrink-0 items-center justify-between gap-4 border-b border-border-subtle px-5 py-3", ), sidebar, class_name="relative flex flex-col w-full flex-1 min-h-0", ), - class_name="!top-[4rem] flex-col !bg-background rounded-[24px_24px_0px_0px] w-full h-[calc(100dvh-4rem)] min-h-0 !outline-none", + class_name="docs-sidebar-drawer !top-[4rem] flex-col !bg-background rounded-[24px_24px_0px_0px] w-full h-[calc(100dvh-4rem)] min-h-0 !outline-none", ), ), ) diff --git a/tests/units/reflex_site_shared/test_docs_shell.py b/tests/units/reflex_site_shared/test_docs_shell.py index 389c78a98da..3ba1e500528 100644 --- a/tests/units/reflex_site_shared/test_docs_shell.py +++ b/tests/units/reflex_site_shared/test_docs_shell.py @@ -4,6 +4,9 @@ from typing import cast import pytest +from reflex_base.components.component import BaseComponent +from reflex_components_internal.components.base.input import InputRoot +from reflex_components_internal.components.base.textarea import Textarea from reflex_site_shared.components import docs_shell from reflex_site_shared.components.docs_shell import ( DocsFeedbackState, @@ -61,6 +64,23 @@ def test_shared_feedback_preserves_the_official_form_structure() -> None: ) +def test_feedback_fields_and_choices_have_accessible_names_and_state() -> None: + """Keep field purposes and selected scores available beyond visual styling.""" + pending: list[BaseComponent] = [docs_shell._feedback_content()] + fields: dict[str, InputRoot | Textarea] = {} + while pending: + node = pending.pop() + pending.extend(node.children) + if isinstance(node, (InputRoot, Textarea)): + fields["email" if isinstance(node, InputRoot) else "feedback"] = node + + assert fields["feedback"].custom_attrs["aria-label"] == "Feedback" + assert fields["email"].custom_attrs["aria-label"] == "Contact email (optional)" + rendered = str(docs_feedback_button_toc()) + assert rendered.count('"aria-pressed"') == 2 + assert '"aria-pressed"' in str(docs_feedback_button()) + + FOOTER = docs_shell._FOOTER_FEEDBACK_POPOVER TOC = docs_shell._TOC_FEEDBACK_POPOVER @@ -243,6 +263,24 @@ def test_official_docs_footer_content_is_shared() -> None: assert f'href:"https://reflex.dev{path}"' in rendered +def test_footer_theme_and_social_controls_can_wrap() -> None: + """Keep the social links reachable when they cannot fit beside theme controls.""" + pending: list[BaseComponent] = [ + docs_page_footer(issue_href="/issues/", edit_href="/edit/") + ] + while pending: + node = pending.pop() + if ( + len(node.children) == 2 + and "Toggle system color mode" in str(node.children[0]) + and "Social link for Forum" in str(node.children[1]) + ): + assert "flex-wrap" in str(cast(rx.Component, node).class_name) + return + pending.extend(node.children) + pytest.fail("Footer controls were not rendered") + + def test_docs_layout_uses_page_aware_footer_renderer() -> None: """Pass the discovered source page to a site-specific footer renderer.""" page = DocsPage(