diff --git a/packages/LOCAL-DEV.md b/packages/LOCAL-DEV.md index a931f64..7fe9a0a 100644 --- a/packages/LOCAL-DEV.md +++ b/packages/LOCAL-DEV.md @@ -38,22 +38,44 @@ lives in `remotes/` with its own `docusaurus.config.ts` (for standalone use) and ## Step 1: Develop with test-site -The test-site references the theme via local path — React component changes reflect -instantly with hot reload: +The test-site references the theme via local path. Start the dev server: ```bash cd packages/test-site yarn start ``` -Edit files in `packages/docusaurus-theme/src/*` — HMR works automatically. +### What needs a rebuild -**No build step needed** for component/JS changes. +| What you changed | What to do | +|---|---| +| `src/` components (React/TS) | Nothing — webpack HMR picks them up instantly | +| `theme/` component overrides | Rebuild needed — see watch mode below | +| `css/` stylesheets | Rebuild needed — see below | -### CSS changes require a theme build +### Watching `theme/` changes during development -Theme CSS files (in `packages/docusaurus-theme/css/`) are loaded from the built -package, not watched by the dev server. After editing CSS: +`theme/` overrides (e.g. swizzled Docusaurus components) are compiled to `dist/theme/` +and served from there. Run the TypeScript compiler in watch mode in a second terminal +so changes are recompiled automatically: + +```bash +# Terminal 1 — theme watcher +cd packages/docusaurus-theme +yarn watch + +# Terminal 2 — test site dev server +cd packages/test-site +yarn start +``` + +On every save in `theme/`, `tsc --watch` recompiles in ~1 second and Docusaurus +hot-reloads the result. + +### CSS changes + +Theme CSS files (`packages/docusaurus-theme/css/`) are not watched automatically. +After editing CSS run a full build: ```bash cd packages/docusaurus-theme @@ -156,7 +178,8 @@ yarn build | Stage | Theme build? | Command | |-------|-------------|---------| -| Dev with test-site (JS/React) | No | `cd packages/test-site && yarn start` | +| Dev with test-site (`src/` components) | No | `cd packages/test-site && yarn start` | +| Dev with test-site (`theme/` overrides) | Watch mode | `cd packages/docusaurus-theme && yarn watch` | | Dev with test-site (CSS) | Yes | `cd packages/docusaurus-theme && yarn build` | | Test with file: | Yes | `yarn build` then remote `yarn install --force` | | Publish | Yes | `npm version patch && npm publish` | diff --git a/packages/docusaurus-theme/css/product-picker.css b/packages/docusaurus-theme/css/product-picker.css new file mode 100644 index 0000000..3270882 --- /dev/null +++ b/packages/docusaurus-theme/css/product-picker.css @@ -0,0 +1,210 @@ +/* ========================================================================= + PRODUCT PICKER + Styles for the Products product-picker dropdown in the navbar. + + Import in docusaurus.config.ts customCss: + require.resolve('@netfoundry/docusaurus-theme/css/product-picker.css') + ========================================================================= */ + +/* ── Product / resource icons inside the picker ─────────────────────────── */ +.picker-logo { width: 32px; height: 32px; object-fit: contain; flex-shrink: 0; margin-right: 0.8rem; } +.picker-icon { width: 20px; height: 20px; margin-right: 0.8rem; background-color: var(--ifm-color-primary); -webkit-mask-size: contain; mask-size: contain; -webkit-mask-repeat: no-repeat; display: inline-block; } + +/* Light/dark logo switching */ +.picker-logo--dark { display: none; } +[data-theme='dark'] .picker-logo--light { display: none; } +[data-theme='dark'] .picker-logo--dark { display: block; } + +/* zrok logo needs a subtle shadow in light mode (white logo on white bg) */ +[data-theme='light'] img[src*="zrok-logo"] { filter: drop-shadow(0 0 1.5px rgba(0, 0, 0, 0.55)); } + +/* ── Dropdown trigger button in the navbar ──────────────────────────────── */ +.nf-picker-trigger, +.nf-resources-dropdown { + color: var(--ifm-font-color-base); + position: relative; + padding-right: 1.2rem; + transition: color 0.3s ease; +} + +.nf-picker-trigger:hover, +.nf-resources-dropdown:hover, +.navbar__item.dropdown--show .nf-picker-trigger, +.navbar__item.dropdown--show .nf-resources-dropdown { + color: var(--ifm-color-primary); + text-decoration: none; +} + +/* Animated chevron */ +.nf-picker-trigger::after, +.nf-resources-dropdown::after { + content: ''; + position: absolute; + right: 0.2rem; + top: 50%; + transform: translateY(-50%); + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid currentColor; + transition: all 0.3s ease; + opacity: 0.6; +} + +/* Hover: chevron drops slightly */ +.navbar__link.nf-picker-trigger:hover::after, +.navbar__link.nf-resources-dropdown:hover::after { + transform: translateY(-20%); + opacity: 1; +} + +/* Open: chevron rotates 180° */ +.nf-picker--open .nf-picker-trigger::after, +.navbar__item.dropdown--show .nf-resources-dropdown::after { + transform: translateY(-50%) rotate(180deg); + opacity: 1; +} + +/* Dark mode: cyan accent on hover */ +[data-theme='dark'] .nf-picker-trigger:hover, +[data-theme='dark'] .nf-resources-dropdown:hover, +[data-theme='dark'] .nf-picker--open .nf-picker-trigger, +[data-theme='dark'] .navbar__item.dropdown--show .nf-resources-dropdown { + color: #22d3ee; +} + +/* ── ProductPicker panel (custom navbar item) ───────────────────────────── */ +.nf-picker-panel { + position: fixed; + top: 3.75rem; + left: 50%; + transform: translateX(-50%); + width: 85vw; + max-width: 1000px; + padding: 1.5rem 2rem; + border-radius: 12px; + border: 1px solid rgba(0, 118, 255, 0.12); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.15); + background: var(--ifm-card-background-color); + z-index: 1000; +} + +/* CSS bridge: transparent strip above the panel eats the gap */ +.nf-picker-panel::before { + content: ''; + display: block; + position: absolute; + top: -20px; + left: 0; + right: 0; + height: 20px; +} + +/* ── Legacy html-type dropdown panel ────────────────────────────────────── */ + +/* + * CSS bridge: an invisible pseudo-element that extends the panel's hit area + * upward by 20 px. When the cursor moves from the trigger button down into + * this transparent strip, mouseenter fires on the panel (ul) immediately — + * cancelling the hide timer before it expires — so the menu never flickers + * during diagonal movement across the gap between navbar and panel. + */ +.dropdown__menu:has(.picker-content)::before { + content: ''; + display: block; + position: absolute; + top: -20px; + left: 0; + right: 0; + height: 20px; +} + +/* :has() raises specificity above plain .dropdown__menu — no !important needed */ +.dropdown__menu:has(.picker-content) { + position: fixed; + top: 3.75rem; + left: 50%; + transform: translateX(-50%); + width: 85vw; + max-width: 1000px; + padding: 1.5rem 2rem; + border-radius: 12px; + border: 1px solid rgba(0, 118, 255, 0.12); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.15); + background: var(--ifm-card-background-color); + z-index: 1000; +} + +/* Narrower panel for the 2-column Resources menu */ +.dropdown__menu:has(.picker-resources) { max-width: 700px; } + +/* Ensure visibility when open */ +.dropdown--show > .dropdown__menu, +.dropdown:hover > .dropdown__menu { + display: block; + visibility: visible; + opacity: 1; +} + +/* ── Grid layout ────────────────────────────────────────────────────────── */ +.picker-content { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 2rem; } +.picker-resources { grid-template-columns: 1fr 1fr; } + +/* ── Column headers ─────────────────────────────────────────────────────── */ +.picker-header { + font-size: 0.7rem; + font-weight: 900; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.1em; + border-bottom: 2px solid rgba(148, 163, 184, 0.2); + display: block; + padding-bottom: 0.5rem; + margin-bottom: 0.75rem; +} +.picker-header--nf-primary { color: var(--nf-primary); border-bottom-color: rgba(var(--nf-color-primary), 0.3); } +.picker-header--nf-secondary { color: var(--nf-secondary); border-bottom-color: rgba(var(--nf-color-secondary), 0.3); } +.picker-header--nf-tertiary { color: var(--nf-tertiary); border-bottom-color: rgba(var(--nf-color-tertiary), 0.3); } +[data-theme='light'] .picker-header { filter: brightness(0.6); } +[data-theme='dark'] .picker-header--nf-tertiary { filter: brightness(1.6); } + +/* ── Product / resource links ───────────────────────────────────────────── */ +.picker-link { + display: flex; + align-items: flex-start; + gap: 0.75rem; + padding: 0.55rem 0.65rem; + text-decoration: none; + transition: all 0.2s ease; + border-radius: 8px; +} +.picker-link:hover { background: rgba(0, 118, 255, 0.06); transform: translateX(3px); } +.picker-link strong { color: var(--ifm-font-color-base); font-size: 0.95rem; font-weight: 900; letter-spacing: -0.02em; display: block; } +.picker-link span { color: #64748b; font-size: 0.82rem; display: block; margin-top: 2px; line-height: 1.35; } + +/* ── Mobile (<= 996 px) ─────────────────────────────────────────────────── */ +@media (max-width: 996px) { + /* Fixed grid makes no sense in the narrow sidebar — hide the raw HTML blob */ + .menu__list .menu__list-item:has(> .picker-content) { display: none; } + + /* Docusaurus already renders a chevron; hide ours to avoid doubles */ + .nf-picker-trigger::after, + .nf-resources-dropdown::after { display: none; } + + .nf-picker-trigger, + .nf-resources-dropdown { padding-right: 0.5rem; } +} + +/* ── Desktop (>= 997 px) ────────────────────────────────────────────────── */ +@media (min-width: 997px) { + /* Hide mobile-only fallback links when product picker is active */ + .dropdown__menu .mobile-nav-link { display: none; } + + /* Force panel visible when JS sets dropdown--show (swizzled component) */ + .navbar__item.dropdown--show .dropdown__menu:has(.picker-content) { + display: block; + visibility: visible; + opacity: 1; + } +} diff --git a/packages/docusaurus-theme/css/vars.css b/packages/docusaurus-theme/css/vars.css index 0f330d5..cb9a025 100644 --- a/packages/docusaurus-theme/css/vars.css +++ b/packages/docusaurus-theme/css/vars.css @@ -1,6 +1,14 @@ :root { --ifm-navbar-height: 50px; --nf-docs-max-width: 1400px; + + --nf-color-primary: 119, 194, 252; + --nf-color-secondary: 78, 219, 63; + --nf-color-tertiary: 3, 92, 230; + + --nf-primary: rgb(var(--nf-color-primary)); + --nf-secondary: rgb(var(--nf-color-secondary)); + --nf-tertiary: rgb(var(--nf-color-tertiary)); /*--nf-docs-main-color: purple;*/ .container { /*background: sandybrown;*/ diff --git a/packages/docusaurus-theme/package.json b/packages/docusaurus-theme/package.json index 6686b7d..f59c710 100644 --- a/packages/docusaurus-theme/package.json +++ b/packages/docusaurus-theme/package.json @@ -1,6 +1,6 @@ { "name": "@netfoundry/docusaurus-theme", - "version": "0.7.0", + "version": "0.8.0", "description": "NetFoundry Docusaurus theme with shared layout, footer, and styling", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -12,6 +12,7 @@ "scripts": { "clean": "node scripts/clean.mjs", "build": "yarn clean && tsc && yarn build:css", + "watch": "tsc --watch", "build:css": "node scripts/build-css.mjs", "test": "jest", "prepublishOnly": "yarn build && yarn test" @@ -36,7 +37,11 @@ "types": "./dist/src/node.d.ts", "default": "./dist/src/node.js" }, - "./css/*": "./css/*" + "./css/*": "./css/*", + "./theme/NavbarItem/types/ProductPicker": { + "types": "./theme/NavbarItem/types/ProductPicker/index.tsx", + "default": "./theme/NavbarItem/types/ProductPicker/index.tsx" + } }, "peerDependencies": { "@docusaurus/core": "^3", diff --git a/packages/docusaurus-theme/src/options.ts b/packages/docusaurus-theme/src/options.ts index 7bb87bc..a93dc1c 100644 --- a/packages/docusaurus-theme/src/options.ts +++ b/packages/docusaurus-theme/src/options.ts @@ -36,6 +36,34 @@ export interface StarBannerConfig { repoUrl: string; /** Label text for the star button */ label: string; + /** Only show banner when the current path starts with this prefix (e.g. '/docs/openziti') */ + pathPrefix?: string; +} + +/** + * A single link entry in the product picker + */ +export interface ProductPickerLink { + /** Display name */ + label: string; + /** Route or URL */ + to: string; + /** Logo shown in light mode (and dark mode if logoDark is absent) */ + logo?: string; + /** Logo shown only in dark mode */ + logoDark?: string; + /** Short description shown beneath the label */ + description?: string; +} + +/** + * A column in the product picker dropdown. + * Header color is assigned automatically by column index (primary → secondary → tertiary). + */ +export interface ProductPickerColumn { + /** Column heading text */ + header: string; + links: ProductPickerLink[]; } /** @@ -80,10 +108,12 @@ export interface NetFoundryThemeOptions { export interface NetFoundryThemeConfig { /** Footer configuration */ footer?: FooterConfig; - /** Star banner configuration */ - starBanner?: StarBannerConfig; - /** Whether to show the star banner (default: false) */ - showStarBanner?: boolean; + /** Path-aware star banners — each entry shows only when the current path starts with pathPrefix (omit pathPrefix to show everywhere) */ + starBanners?: StarBannerConfig[]; + /** Product picker columns. If omitted, the theme falls back to built-in NetFoundry defaults. */ + productPickerColumns?: ProductPickerColumn[]; + /** Logo URL for the NetFoundry Console link in the product picker (overrides the default NetFoundry branding icon) */ + consoleLogo?: string; } /** diff --git a/packages/docusaurus-theme/theme/Layout/index.tsx b/packages/docusaurus-theme/theme/Layout/index.tsx index 545773a..ad5a103 100644 --- a/packages/docusaurus-theme/theme/Layout/index.tsx +++ b/packages/docusaurus-theme/theme/Layout/index.tsx @@ -1,5 +1,6 @@ import React, { type ReactNode } from 'react'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import {useLocation} from 'react-router-dom'; import { NetFoundryLayout, defaultNetFoundryFooterProps, @@ -32,6 +33,7 @@ export default function Layout({ description, }: LayoutProps): ReactNode { const { siteConfig } = useDocusaurusContext(); + const {pathname} = useLocation(); const themeConfig = siteConfig.themeConfig as ThemeConfigWithNetFoundry; const nfConfig = themeConfig.netfoundry ?? {}; @@ -47,14 +49,13 @@ export default function Layout({ }, }; - // Build star props if enabled - const starProps = - nfConfig.showStarBanner && nfConfig.starBanner - ? { - repoUrl: nfConfig.starBanner.repoUrl, - label: nfConfig.starBanner.label, - } - : undefined; + // Pick the first banner whose pathPrefix matches (or has no prefix) + const matchedBanner = nfConfig.starBanners?.find(b => + !b.pathPrefix || pathname.startsWith(b.pathPrefix) + ); + const starProps = matchedBanner + ? {repoUrl: matchedBanner.repoUrl, label: matchedBanner.label} + : undefined; return ( ; + +export default { + ...ComponentTypesOrig, + 'custom-productPicker': ProductPicker, +}; diff --git a/packages/docusaurus-theme/theme/NavbarItem/DropdownNavbarItem/Desktop/index.tsx b/packages/docusaurus-theme/theme/NavbarItem/DropdownNavbarItem/Desktop/index.tsx new file mode 100644 index 0000000..d23dc83 --- /dev/null +++ b/packages/docusaurus-theme/theme/NavbarItem/DropdownNavbarItem/Desktop/index.tsx @@ -0,0 +1,125 @@ +/** + * Swizzled DropdownNavbarItemDesktop + * + * Opens on hover. The panel stays open until the user actually moves their + * cursor into it — so the gap between the trigger and the fixed panel never + * causes a flicker. Once the cursor has entered the panel, leaving it + * (or clicking outside) closes it normally. + */ +import React, {useState, useRef, useEffect, useCallback} from 'react'; +import clsx from 'clsx'; +import NavbarNavLinkOrig from '@theme/NavbarItem/NavbarNavLink'; +import NavbarItemOrig from '@theme/NavbarItem'; + +const NavbarNavLink = NavbarNavLinkOrig as React.ComponentType; +const NavbarItem = NavbarItemOrig as React.ComponentType; + +export default function DropdownNavbarItemDesktop({ + items, + position, + className, + onClick, + ...props +}: any) { + const dropdownRef = useRef(null); + const hasEnteredPanel = useRef(false); + const [showDropdown, setShowDropdown] = useState(false); + + // Close on click / touch outside + useEffect(() => { + const close = (e: MouseEvent | TouchEvent) => { + if (!dropdownRef.current?.contains(e.target as Node)) { + setShowDropdown(false); + hasEnteredPanel.current = false; + } + }; + document.addEventListener('mousedown', close); + document.addEventListener('touchstart', close); + return () => { + document.removeEventListener('mousedown', close); + document.removeEventListener('touchstart', close); + }; + }, []); + + // Sync: close other open megamenus when this one opens + useEffect(() => { + const onOtherOpen = (e: any) => { + if (e.detail.label !== props.label) { + setShowDropdown(false); + hasEnteredPanel.current = false; + } + }; + window.addEventListener('nf-picker:open', onOtherOpen); + return () => window.removeEventListener('nf-picker:open', onOtherOpen); + }, [props.label]); + + // Open on hover — reset entry state each time + const handleMouseEnter = useCallback(() => { + hasEnteredPanel.current = false; + window.dispatchEvent(new CustomEvent('nf-picker:open', {detail: {label: props.label}})); + setShowDropdown(true); + console.log('[product-picker] popped open:', props.label); + }, [props.label]); + + // Leaving the trigger: do nothing — the panel stays open until the user + // either enters it (then leaves) or clicks outside. + const handleTriggerLeave = useCallback(() => { + console.log('[product-picker] trigger leave — hasEnteredPanel:', hasEnteredPanel.current); + }, []); + + // Once the cursor enters the panel, normal leave/blur can close it + const handlePanelEnter = useCallback(() => { + hasEnteredPanel.current = true; + console.log('[product-picker] panel focus obtained'); + }, []); + + const handlePanelLeave = useCallback(() => { + console.log('[product-picker] panel focus lost — hasEnteredPanel:', hasEnteredPanel.current); + if (hasEnteredPanel.current) { + setShowDropdown(false); + hasEnteredPanel.current = false; + console.log('[product-picker] closing — cursor left panel'); + } + }, []); + + return ( +
+ e.preventDefault()} + onKeyDown={(e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + setShowDropdown(prev => !prev); + } + }}> + {props.children ?? props.label} + +
    + {items.map((childItemProps: any, i: number) => ( + + ))} +
+
+ ); +} diff --git a/packages/docusaurus-theme/theme/NavbarItem/types/ProductPicker/index.tsx b/packages/docusaurus-theme/theme/NavbarItem/types/ProductPicker/index.tsx new file mode 100644 index 0000000..6e26792 --- /dev/null +++ b/packages/docusaurus-theme/theme/NavbarItem/types/ProductPicker/index.tsx @@ -0,0 +1,156 @@ +import React, {useState, useRef, useEffect, useCallback} from 'react'; +import Link from '@docusaurus/Link'; +import clsx from 'clsx'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import {useThemeConfig} from '@docusaurus/theme-common'; + +export type PickerLink = { + label: string; + to: string; + logo?: string; + logoDark?: string; + description?: string; +}; + +export type PickerColumn = { + header: string; + headerClass?: string; + links: PickerLink[]; +}; + +type Props = { + label?: string; + position?: 'left' | 'right'; + className?: string; +}; + +const HEADER_CLASSES = ['picker-header--nf-primary', 'picker-header--nf-secondary', 'picker-header--nf-tertiary']; +const NF_LOGO_DEFAULT = 'https://raw.githubusercontent.com/netfoundry/branding/refs/heads/main/images/svg/icon/netfoundry-icon-color.svg'; + +const buildDefaultColumns = (img: string, consoleLogo: string): PickerColumn[] => [ + { + header: 'Managed Cloud', + headerClass: HEADER_CLASSES[0], + links: [ + { label: 'NetFoundry Console', to: '#', logo: consoleLogo, description: 'Cloud-managed orchestration and global fabric control.' }, + { label: 'Frontdoor', to: '/docs/frontdoor', logo: `${img}/frontdoor-sm-logo.svg`, description: 'Secure application access gateway.' }, + ], + }, + { + header: 'Open Source', + headerClass: HEADER_CLASSES[1], + links: [ + { label: 'OpenZiti', to: '/docs/openziti', logo: `${img}/openziti-sm-logo.svg`, description: 'Programmable zero-trust mesh infrastructure.' }, + { label: 'zrok', to: '/docs/zrok', logo: `${img}/zrok-1.0.0-rocket-purple.svg`, logoDark: `${img}/zrok-1.0.0-rocket-green.svg`, description: 'Secure peer-to-peer sharing built on OpenZiti.' }, + ], + }, + { + header: 'Your own infrastructure', + headerClass: HEADER_CLASSES[2], + links: [ + { label: 'Self-Hosted', to: '/docs/selfhosted', logo: `${img}/onprem-sm-logo.svg`, description: 'Deploy the full stack in your own environment.' }, + { label: 'zLAN', to: '/docs/zlan', logo: `${img}/zlan-logo.svg`, description: 'Zero-trust access for OT networks.' }, + ], + }, +]; + +export default function ProductPicker({label = 'Products', className}: Props) { + const {siteConfig} = useDocusaurusContext(); + const themeConfig = useThemeConfig() as any; + const consoleLogo = themeConfig?.netfoundry?.consoleLogo ?? NF_LOGO_DEFAULT; + const img = `${siteConfig.url}${siteConfig.baseUrl}img`; + const columns: PickerColumn[] = (themeConfig?.netfoundry?.productPickerColumns ?? []) + .map((col: any, i: number) => ({...col, headerClass: HEADER_CLASSES[i] ?? ''})); + const resolvedColumns = columns.length ? columns : buildDefaultColumns(img, consoleLogo); + const wrapRef = useRef(null); + const hasEnteredPanel = useRef(false); + const [open, setOpen] = useState(false); + + const close = useCallback(() => { + setOpen(false); + hasEnteredPanel.current = false; + }, []); + + // Close on outside click/touch + useEffect(() => { + const onOutside = (e: MouseEvent | TouchEvent) => { + if (!wrapRef.current?.contains(e.target as Node)) close(); + }; + document.addEventListener('mousedown', onOutside); + document.addEventListener('touchstart', onOutside); + return () => { + document.removeEventListener('mousedown', onOutside); + document.removeEventListener('touchstart', onOutside); + }; + }, [close]); + + // Sync: close when another product picker opens + useEffect(() => { + const onOtherOpen = (e: any) => { + if (e.detail.label !== label) close(); + }; + window.addEventListener('nf-picker:open', onOtherOpen); + return () => window.removeEventListener('nf-picker:open', onOtherOpen); + }, [label, close]); + + const handleTriggerEnter = useCallback(() => { + hasEnteredPanel.current = false; + window.dispatchEvent(new CustomEvent('nf-picker:open', {detail: {label}})); + setOpen(true); + }, [label]); + + // Stay open until user enters the panel — no timer + const handleTriggerLeave = useCallback(() => {}, []); + + const handlePanelEnter = useCallback(() => { + hasEnteredPanel.current = true; + }, []); + + const handlePanelLeave = useCallback(() => { + if (hasEnteredPanel.current) close(); + }, [close]); + + return ( +
+ { e.preventDefault(); setOpen(o => !o); }} + onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); setOpen(o => !o); } }}> + {label} + + {open && ( +
e.stopPropagation()} + onMouseEnter={handlePanelEnter} + onMouseLeave={handlePanelLeave}> +
+ {resolvedColumns.map((col, i) => ( +
+ {col.header} + {col.links.map((link, j) => ( + + {link.logo && } + {link.logoDark && } +
+ {link.label} + {link.description && {link.description}} +
+ + ))} +
+ ))} +
+
+ )} +
+ ); +} diff --git a/packages/docusaurus-theme/theme/docusaurus-theme-modules.d.ts b/packages/docusaurus-theme/theme/docusaurus-theme-modules.d.ts new file mode 100644 index 0000000..1e35bd3 --- /dev/null +++ b/packages/docusaurus-theme/theme/docusaurus-theme-modules.d.ts @@ -0,0 +1,32 @@ +/** + * Type stubs for Docusaurus @theme/* virtual modules. + * These are resolved at runtime by Docusaurus's webpack aliases and are not + * available to the standalone TypeScript compiler. Declared as `any`-based + * components here since all call sites cast them to ComponentType anyway. + */ +declare module '@docusaurus/Link' { + const Link: any; + export default Link; +} + +declare module '@theme-original/NavbarItem/ComponentTypes' { + const ComponentTypes: any; + export default ComponentTypes; +} + +declare module '@theme-init/NavbarItem/ComponentTypes' { + const ComponentTypes: any; + export default ComponentTypes; +} + +declare module '@theme/NavbarItem/NavbarNavLink' { + import type React from 'react'; + const NavbarNavLink: React.ComponentType; + export default NavbarNavLink; +} + +declare module '@theme/NavbarItem' { + import type React from 'react'; + const NavbarItem: React.ComponentType; + export default NavbarItem; +} diff --git a/packages/docusaurus-theme/tsconfig.json b/packages/docusaurus-theme/tsconfig.json index 480d588..56ae35a 100644 --- a/packages/docusaurus-theme/tsconfig.json +++ b/packages/docusaurus-theme/tsconfig.json @@ -17,5 +17,10 @@ } }, "include": ["src/**/*", "theme/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], + "watchOptions": { + "watchFile": "dynamicPriorityPolling", + "watchDirectory": "dynamicPriorityPolling", + "fallbackPolling": "dynamicPriority" + } } diff --git a/packages/test-site/docusaurus.config.ts b/packages/test-site/docusaurus.config.ts index 5e5d16c..4661032 100644 --- a/packages/test-site/docusaurus.config.ts +++ b/packages/test-site/docusaurus.config.ts @@ -112,7 +112,10 @@ export default { }, blog: false, theme: { - customCss: require.resolve('./src/custom/custom.css'), + customCss: [ + require.resolve('./src/custom/custom.css'), + require.resolve('../docusaurus-theme/css/product-picker.css'), + ], } } ] @@ -121,11 +124,64 @@ export default { themeConfig: { // NetFoundry theme configuration netfoundry: { - showStarBanner: true, - starBanner: { - repoUrl: 'https://github.com/openziti/ziti', - label: 'Star OpenZiti on GitHub', - }, + starBanners: [ + { pathPrefix: '/docs/openziti', repoUrl: 'https://github.com/openziti/ziti', label: 'Star OpenZiti on GitHub' }, + { pathPrefix: '/docs/zrok', repoUrl: 'https://github.com/openziti/zrok', label: 'Star zrok on GitHub' }, + ], + productPickerColumns: [ + { + header: 'Managed Cloud', + links: [ + { + label: 'NetFoundry Console', + to: '#', + logo: 'https://raw.githubusercontent.com/netfoundry/branding/refs/heads/main/images/svg/icon/netfoundry-icon-color.svg', + description: 'Cloud-managed orchestration and global fabric control.', + }, + { + label: 'Frontdoor', + to: '/docs/frontdoor', + logo: 'https://netfoundry.io/docs/img/frontdoor-sm-logo.svg', + description: 'Secure application access gateway.', + }, + ], + }, + { + header: 'Open Source', + links: [ + { + label: 'OpenZiti', + to: '/docs/openziti', + logo: 'https://netfoundry.io/docs/img/openziti-sm-logo.svg', + description: 'Programmable zero-trust mesh infrastructure.', + }, + { + label: 'zrok', + to: '/docs/zrok', + logo: 'https://netfoundry.io/docs/img/zrok-1.0.0-rocket-purple.svg', + logoDark: 'https://netfoundry.io/docs/img/zrok-1.0.0-rocket-green.svg', + description: 'Secure peer-to-peer sharing built on OpenZiti.', + }, + ], + }, + { + header: 'Your own infrastructure', + links: [ + { + label: 'Self-Hosted', + to: '/docs/selfhosted', + logo: 'https://netfoundry.io/docs/img/onprem-sm-logo.svg', + description: 'Deploy the full stack in your own environment.', + }, + { + label: 'zLAN', + to: '/docs/zlan', + logo: 'https://netfoundry.io/docs/img/zlan-logo.svg', + description: 'Zero-trust access for OT networks.', + }, + ], + }, + ], footer: { description: 'This is just a test site for the NetFoundry Docusaurus theme.', socialProps: { @@ -146,17 +202,7 @@ export default { src: 'https://raw.githubusercontent.com/netfoundry/branding/refs/heads/main/images/svg/icon/netfoundry-icon-color.svg', }, items: [ - { - label: 'Docs', - position: 'left', - items: [ - { to: '/docs/openziti', label: 'OpenZiti' }, - { to: '/docs/frontdoor', label: 'Frontdoor' }, - { to: '/docs/onprem', label: 'On-Prem' }, - { to: '/docs/zlan', label: 'zLAN' }, - { to: '/docs/zrok', label: 'zrok' }, - ], - }, + { type: 'custom-productPicker', position: 'left' }, { to: '/docs', label: 'Main Docs', diff --git a/packages/test-site/src/pages/index.tsx b/packages/test-site/src/pages/index.tsx index 8fd4e4d..8c4367f 100644 --- a/packages/test-site/src/pages/index.tsx +++ b/packages/test-site/src/pages/index.tsx @@ -1,26 +1,81 @@ import React, {JSX} from 'react'; import Layout from '@theme/Layout'; -import {Alert, NetFoundryHorizontalSection} from '@netfoundry/docusaurus-theme/ui'; +import Link from '@docusaurus/Link'; +import clsx from 'clsx'; +import styles from './landing.module.css'; + +const CYAN = '#22d3ee'; +const GREEN = '#22c55e'; +const IMG = 'https://netfoundry.io/docs/img'; +const NF_LOGO = 'https://raw.githubusercontent.com/netfoundry/branding/refs/heads/main/images/svg/icon/netfoundry-icon-color.svg'; + +const products = [ + { id: 'console', title: 'NetFoundry Console', logo: NF_LOGO, tag: 'Managed', accent: CYAN, link: '#', features: ['Fully managed SaaS', 'Global edge fabric', 'No infra to operate', 'Policy-based access'], description: "The cloud-managed control plane for NetFoundry's global zero-trust fabric. Orchestrate identities, policies, and edge routers — no infrastructure to run." }, + { id: 'openziti', title: 'OpenZiti', logo: `${IMG}/openziti-sm-logo.svg`, tag: 'Open Source', accent: GREEN, link: '/docs/openziti', description: 'The open-source zero-trust networking framework at the heart of the NetFoundry platform. Embed dark, app-native security directly in your code — no VPN, no perimeter.' }, + { id: 'frontdoor', title: 'Frontdoor', logo: `${IMG}/frontdoor-sm-logo.svg`, tag: 'Managed', accent: CYAN, link: '/docs/frontdoor', features: ['No agent or VPN required', 'Zero firewall rules', 'Identity-based access', 'Any app, any browser'], description: 'Secure, clientless access to any application — without a VPN or firewall rule. Expose nothing to the internet while giving authorized users instant access.' }, + { id: 'zrok', title: 'zrok', logo: `${IMG}/zrok-1.0.0-rocket-purple.svg`, tag: 'Open Source', accent: GREEN, link: '/docs/zrok', description: 'Geo-scale secure sharing built on the OpenZiti mesh. Share services, files, or HTTP endpoints peer-to-peer — no open ports, no NAT traversal tricks.' }, + { id: 'selfhosted', title: 'NetFoundry Self-Hosted', logo: `${IMG}/onprem-sm-logo.svg`, tag: 'Self-Hosted', accent: CYAN, link: '/docs/onprem', features: ['Full infrastructure control', 'Air-gap compatible', 'On-prem or any cloud', 'Enterprise SLA'], description: 'Deploy the full NetFoundry control plane and fabric in your own environment. Full sovereignty over your zero-trust infrastructure — on-prem, air-gapped, or any cloud.' }, + { id: 'zlan', title: 'zLAN', logo: `${IMG}/zlan-logo.svg`, tag: 'OT Security', accent: CYAN, link: '/docs/zlan', features: ['Deep OT/IT traffic visibility', 'Identity-aware micro-segmentation', 'Centralized zero-trust policy', 'Built on NetFoundry Self-Hosted'], description: 'Identity-aware micro-segmentation firewall for operational technology networks. Deep traffic visibility, centralized policy, and zero-trust access control for OT environments.' }, +]; + +type Product = (typeof products)[number]; +const byId = Object.fromEntries(products.map(p => [p.id, p])) as Record; + +function BentoCard({product, featured = false}: {product: Product; featured?: boolean}): JSX.Element { + const accentMod = product.accent === CYAN ? styles['nf-bento-card--accent-cyan'] : styles['nf-bento-card--accent-green']; + return ( +
+ + {product.tag} +
+ {product.logo && {product.title}} +

{product.title}

+
+

{product.description}

+ {product.features && ( +
    + {product.features.map(f =>
  • {f}
  • )} +
+ )} +
Explore →
+ +
+ ); +} export default function Home(): JSX.Element { - const title = 'Home AA'; - const desc = "TheDescriptiond"; - return ( - -
- - -

Hello

-

This is a basic Docusaurus page in TSX.

-

This is a basic Docusaurus page in TSX.

-

This is a basic Docusaurus page in TSX.

- - - - -
-
-
-
- ); + return ( + +
+
+

NetFoundry Docs

+

Secure, high-performance networking for the modern era.

+
+ Get Started + Request Demo +
+
+
+
+
+
+
Managed Cloud
+
+ +
open-source counterpart
+ +
+
+ +
open-source counterpart
+ +
+
Run on your own infrastructure
+ + +
+
+
+
+ ); } diff --git a/packages/test-site/src/pages/landing.module.css b/packages/test-site/src/pages/landing.module.css new file mode 100644 index 0000000..f7a749b --- /dev/null +++ b/packages/test-site/src/pages/landing.module.css @@ -0,0 +1,160 @@ +.nf-hero-stage { + position: relative; width: 100%; min-height: 370px; + display: flex; align-items: center; justify-content: center; + overflow: hidden; text-align: center; background: #020617; z-index: 4; +} +.nf-hero-stage::after { + content: ''; position: absolute; bottom: 0; left: 0; right: 0; + height: 220px; background: linear-gradient(to bottom, transparent 0%, #0f172a 100%); + pointer-events: none; z-index: 1; +} +:global([data-theme='light']) .nf-hero-stage::after { + height: 80px; + background: linear-gradient(to bottom, transparent 0%, #020617 100%); +} +.nf-hero-overlay { display: none; } +.nf-hero-content { + position: relative; z-index: 2; padding: 2.5rem 3.5rem; + background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; +} +.nf-hero-title { + font-size: 4rem; font-weight: 900; color: #ffffff; margin-bottom: 0.75rem; + letter-spacing: -0.02em; line-height: 1.05; + text-shadow: 0 0 20px rgba(34, 211, 238, 0.8), 0 2px 12px rgba(0, 0, 0, 0.9); +} +.nf-green-text { + background: linear-gradient(to right, #22c55e 0%, #86efac 100%); + -webkit-background-clip: text; background-clip: text; + -webkit-text-fill-color: transparent; display: inline-block; +} +.nf-hero-subtext { + color: rgba(203, 213, 225, 0.95); font-size: 1.15rem; max-width: 560px; + margin: 0 auto 2rem; line-height: 1.65; text-shadow: 0 1px 8px rgba(0, 0, 0, 0.95); +} +.nf-hero-ctas { display: flex; gap: 1rem; justify-content: center; } +.nf-btn-primary { + display: inline-flex; align-items: center; padding: 0.65rem 1.75rem; + background: #0076FF; color: #ffffff; border-radius: 8px; font-weight: 700; + font-size: 0.95rem; text-decoration: none; transition: all 0.25s ease; border: 2px solid #0076FF; +} +.nf-btn-primary:hover { + background: #005ce6; border-color: #005ce6; transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 118, 255, 0.4); color: #ffffff; +} +.nf-btn-ghost { + display: inline-flex; align-items: center; padding: 0.65rem 1.75rem; + background: transparent; color: #ffffff; border-radius: 8px; font-weight: 700; + font-size: 0.95rem; text-decoration: none; transition: all 0.25s ease; + border: 2px solid rgba(255, 255, 255, 0.25); +} +.nf-btn-ghost:hover { + background: rgba(255, 255, 255, 0.06); border-color: rgba(34, 211, 238, 0.5); + transform: translateY(-2px); color: #ffffff; +} + +.nf-features-section { width: 100%; background: #0f172a; padding: 5rem 0 2.5rem; } +:global([data-theme='light']) .nf-features-section { + background: linear-gradient(to bottom, + #020617 0px, #020617 80px, #404350 18%, #7d808a 28%, + #b8bbc1 38%, #e4e7ea 48%, #f8fafc 100%); +} + +.nf-bento-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; } + +.nf-bento-divider { + grid-column: 1 / -1; display: flex; align-items: center; gap: 1rem; + padding: 1.5rem 0 0.65rem; color: #94a3b8; font-size: 0.82rem; font-weight: 800; + letter-spacing: 0.12em; text-transform: uppercase; white-space: nowrap; +} +.nf-bento-divider::before, .nf-bento-divider::after { + content: ''; flex: 1; height: 1px; background: rgba(148, 163, 184, 0.2); +} +.nf-divider--top { padding-top: 0; } +.nf-divider--managed { + color: #22d3ee; font-size: 1.05rem; letter-spacing: 0.15em; + text-shadow: 0 0 18px rgba(34, 211, 238, 0.5); +} +.nf-divider--managed::before, .nf-divider--managed::after { background: rgba(34, 211, 238, 0.4); height: 2px; } +:global([data-theme='light']) .nf-bento-divider { color: #64748b; } +:global([data-theme='light']) .nf-bento-divider::before, +:global([data-theme='light']) .nf-bento-divider::after { background: rgba(100, 116, 139, 0.25); } +:global([data-theme='light']) .nf-divider--managed { color: #0891b2; text-shadow: none; } +:global([data-theme='light']) .nf-divider--managed::before, +:global([data-theme='light']) .nf-divider--managed::after { background: rgba(8, 145, 178, 0.35); height: 2px; } + +.nf-pair { display: flex; flex-direction: column; } +.nf-pair-connector { + display: flex; align-items: center; gap: 1rem; padding: 0.5rem 0; + font-size: 0.7rem; font-weight: 800; letter-spacing: 0.12em; + text-transform: uppercase; color: #94a3b8; +} +.nf-pair-connector::before, .nf-pair-connector::after { + content: ''; flex: 1; height: 1px; background: rgba(148, 163, 184, 0.2); +} +:global([data-theme='light']) .nf-pair-connector { color: #64748b; } +:global([data-theme='light']) .nf-pair-connector::before, +:global([data-theme='light']) .nf-pair-connector::after { background: rgba(148, 163, 184, 0.35); } + +.nf-bento-wrap { display: flex; flex-direction: column; } + +.nf-bento-card { + position: relative; display: flex; flex-direction: column; flex: 1; + padding: 1rem; border-radius: 12px; text-decoration: none; + background: #1a1b2e; border: 1px solid rgba(148, 163, 184, 0.1); + border-top: 2px solid #22d3ee; + box-shadow: 0 1px 3px rgba(0,0,0,0.3), 0 6px 20px rgba(0,0,0,0.3), 0 16px 40px rgba(0,0,0,0.2); + transition: transform 0.2s ease, box-shadow 0.2s ease, border-top-color 0.2s ease; +} +.nf-bento-card:hover { + transform: translateY(-4px); border-top-color: #22c55e; + box-shadow: 0 2px 6px rgba(0,0,0,0.4), 0 12px 32px rgba(0,0,0,0.45), + 0 24px 60px rgba(0,0,0,0.25), 0 0 0 1px rgba(34,197,94,0.12); +} +:global([data-theme='light']) .nf-bento-card { + background: #edf3f8; border-color: rgba(0,0,0,0.08); + box-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 4px 14px rgba(0,0,0,0.05); +} +:global([data-theme='light']) .nf-bento-card:hover { + box-shadow: 0 2px 6px rgba(0,0,0,0.09), 0 8px 24px rgba(0,0,0,0.07), 0 0 0 1px rgba(34,197,94,0.18); +} +:global([data-theme='light']) .nf-bento-card--accent-cyan { border-top-color: #0891b2; } +:global([data-theme='light']) .nf-bento-card--accent-green { border-top-color: #16a34a; } +.nf-bento-card--featured { padding: 1.25rem; border-top-width: 3px; } +.nf-bento-card--featured .nf-card-logo { width: 48px; height: 48px; } +.nf-bento-card--featured .nf-card-header h3 { font-size: 1.25rem; } + +.nf-card-badge { + position: absolute; top: 1rem; right: 1rem; display: inline-flex; width: fit-content; + background: rgba(34,197,94,0.1); color: #4ade80; border: 1px solid rgba(34,197,94,0.2); + font-size: 0.6rem; font-weight: 800; letter-spacing: 0.1em; + padding: 2px 8px; border-radius: 4px; text-transform: uppercase; +} +:global([data-theme='light']) .nf-card-badge { color: #15803d; border-color: rgba(34,197,94,0.25); } + +.nf-card-header { + display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; padding-right: 3rem; +} +.nf-card-header h3 { margin: 0; } +.nf-bento-card h3 { color: #f1f5f9; font-weight: 900; font-size: 1.05rem; line-height: 1.3; letter-spacing: -0.02em; } +:global([data-theme='light']) .nf-bento-card h3 { color: #0f172a; } +.nf-bento-card p { color: #94a3b8; font-size: 0.875rem; line-height: 1.65; flex-grow: 1; margin: 0 0 0.5rem; } +:global([data-theme='light']) .nf-bento-card p { color: #475569; } + +.nf-bento-features { list-style: none; padding: 0; margin: 0 0 0.5rem; display: flex; flex-direction: column; gap: 0.25rem; } +.nf-bento-features li { display: flex; align-items: center; gap: 0.4rem; font-size: 0.775rem; color: #64748b; } +.nf-bento-features li::before { content: '✓'; color: #22c55e; font-weight: 800; font-size: 0.75rem; flex-shrink: 0; } +:global([data-theme='light']) .nf-bento-features li::before { color: #16a34a; } + +.nf-card-link { color: #22d3ee; font-size: 0.8rem; font-weight: 700; margin-top: auto; padding-top: 0.5rem; letter-spacing: 0.03em; } +:global([data-theme='light']) .nf-card-link { color: #0284c7; } +.nf-card-logo { width: 40px; height: 40px; object-fit: contain; flex-shrink: 0; } + +@media (max-width: 996px) { + .nf-hero-title { font-size: 2.2rem; } + .nf-hero-content { padding: 2rem 1.25rem; } + .nf-hero-ctas { flex-wrap: wrap; } + .nf-btn-primary, .nf-btn-ghost { padding: 0.55rem 1.25rem; font-size: 0.9rem; } +} +@media (max-width: 640px) { + .nf-bento-grid { grid-template-columns: 1fr; } +} diff --git a/unified-doc/.gitignore b/unified-doc/.gitignore index 24f464f..8ecf707 100644 --- a/unified-doc/.gitignore +++ b/unified-doc/.gitignore @@ -35,5 +35,4 @@ backstop_data/bitmaps_* backstop_data/html_report* backstop_data/ci_report* - - +src/generated/ diff --git a/unified-doc/README.md b/unified-doc/README.md index b85d2c7..700e51e 100644 --- a/unified-doc/README.md +++ b/unified-doc/README.md @@ -16,6 +16,27 @@ yarn start This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. +### Developing the theme locally + +To test local changes to `packages/docusaurus-theme` without publishing: + +```bash +# Link the local theme (run once) +./dev-link.sh + +# Terminal 1 — recompile theme on change +cd ../packages/docusaurus-theme && yarn watch + +# Terminal 2 — run the site +yarn start +``` + +To restore the published npm version: + +```bash +./dev-link.sh unlink +``` + ## Build ```bash diff --git a/unified-doc/dev-link.sh b/unified-doc/dev-link.sh new file mode 100755 index 0000000..9bb40f4 --- /dev/null +++ b/unified-doc/dev-link.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Links the local docusaurus-theme into unified-doc for development. +# Run once to set up, then use `yarn watch` in packages/docusaurus-theme +# and `yarn start` in unified-doc as normal. +# +# Usage: +# ./dev-link.sh # link +# ./dev-link.sh unlink # restore npm version + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +THEME_DIR="$SCRIPT_DIR/../packages/docusaurus-theme" +UNIFIED_DIR="$SCRIPT_DIR" + +if [ ! -d "$UNIFIED_DIR" ]; then + echo "ERROR: unified-doc not found at $UNIFIED_DIR" + exit 1 +fi + +if [ "${1}" = "unlink" ]; then + echo "→ Unlinking @netfoundry/docusaurus-theme from unified-doc..." + cd "$UNIFIED_DIR" && yarn unlink @netfoundry/docusaurus-theme && yarn install --force + echo "✓ Restored npm version" +else + echo "→ Registering theme package for linking..." + cd "$THEME_DIR" && yarn link + + echo "→ Linking into unified-doc..." + cd "$UNIFIED_DIR" && yarn link @netfoundry/docusaurus-theme + + echo "" + echo "✓ Done. Now run in two terminals:" + echo " [1] cd packages/docusaurus-theme && yarn watch" + echo " [2] cd $UNIFIED_DIR && yarn start" +fi diff --git a/unified-doc/docusaurus.config.ts b/unified-doc/docusaurus.config.ts index b43818d..3b0fb0b 100644 --- a/unified-doc/docusaurus.config.ts +++ b/unified-doc/docusaurus.config.ts @@ -87,11 +87,11 @@ const REMARK_MAPPINGS = [ { from: '@openzitidocs', to: `${docsBase}openziti`}, { from: '@zrokdocs', to: `${docsBase}zrok`}, { from: '@static', to: docsBase}, - { from: '/openziti/', to: `${docsBase}${routeBase('openziti')}/` }, - { from: '/frontdoor/', to: `${docsBase}${routeBase('frontdoor')}/` }, - { from: '/selfhosted/', to: `${docsBase}${routeBase('selfhosted')}/` }, - { from: '/zrok/', to: `${docsBase}${routeBase('zrok')}/` }, - { from: '/zlan/', to: `${docsBase}${routeBase('zlan')}/` }, + { from: '/openziti', to: `${docsBase}${routeBase('openziti')}` }, + { from: '/frontdoor', to: `${docsBase}${routeBase('frontdoor')}` }, + { from: '/selfhosted', to: `${docsBase}${routeBase('selfhosted')}` }, + { from: '/zrok', to: `${docsBase}${routeBase('zrok')}` }, + { from: '/zlan', to: `${docsBase}${routeBase('zlan')}` }, ]; console.log("CANONICAL URL : " + cfg.docusaurus.url); @@ -217,12 +217,31 @@ const config: Config = { }, themes: [ ['@docusaurus/theme-classic', { - customCss: require.resolve('./src/css/custom.css'), + customCss: [ + require.resolve('./src/css/custom.css'), + require.resolve('@netfoundry/docusaurus-theme/css/product-picker.css'), + ], }], + '@netfoundry/docusaurus-theme', '@docusaurus/theme-mermaid', '@docusaurus/theme-search-algolia', ], plugins: [ + function emitDocsBase() { + return { + name: 'emit-docs-base', + async loadContent() { + const fs = require('fs'); + const dir = path.resolve(__dirname, 'src/generated'); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, {recursive: true}); + const outPath = path.resolve(dir, 'docsBase.ts'); + const docsLinkBase = `${docsBase}${isVercel ? 'docs/' : ''}`; + const content = `// auto-generated — do not commit\nexport const DOCS_BASE = '${docsLinkBase}';\n`; + fs.writeFileSync(outPath, content); + console.log(`\n✅ [emit-docs-base] wrote DOCS_BASE='${docsLinkBase}' → ${outPath}\n`); + }, + }; + }, '@docusaurus/plugin-debug', function webpackAliases() { return { @@ -254,11 +273,11 @@ const config: Config = { }, ['@docusaurus/plugin-content-pages',{path: 'src/pages',routeBasePath: '/'}], - build(BUILD_FLAGS.FRONTDOOR) && ['@docusaurus/plugin-content-pages',{id: `frontdoor-pages`, path: `${frontdoor}/docusaurus/src/pages`, routeBasePath: '/frontdoor'}], - build(BUILD_FLAGS.SELFHOSTED) && ['@docusaurus/plugin-content-pages',{id: `selfhosted-pages`, path: `${selfhosted}/docusaurus/src/pages`, routeBasePath: '/selfhosted'}], - build(BUILD_FLAGS.OPENZITI) && ['@docusaurus/plugin-content-pages',{id: `openziti-pages`, path: `${openziti}/docusaurus/src/pages`, routeBasePath: '/openziti'}], - build(BUILD_FLAGS.ZLAN) && ['@docusaurus/plugin-content-pages',{id: `zlan-pages`, path: `${zlan}/docusaurus/src/pages`, routeBasePath: '/zlan'}], - build(BUILD_FLAGS.ZROK) && ['@docusaurus/plugin-content-pages',{id: `zrok-pages`, path: `${zrokRoot}/src/pages`, routeBasePath: '/zrok'}], + build(BUILD_FLAGS.FRONTDOOR) && ['@docusaurus/plugin-content-pages',{id: `frontdoor-pages`, path: `${frontdoor}/docusaurus/src/pages`, routeBasePath: `/${routeBase('frontdoor')}`}], + build(BUILD_FLAGS.SELFHOSTED) && ['@docusaurus/plugin-content-pages',{id: `selfhosted-pages`, path: `${selfhosted}/docusaurus/src/pages`, routeBasePath: `/${routeBase('selfhosted')}`}], + build(BUILD_FLAGS.OPENZITI) && ['@docusaurus/plugin-content-pages',{id: `openziti-pages`, path: `${openziti}/docusaurus/src/pages`, routeBasePath: `/${routeBase('openziti')}`}], + build(BUILD_FLAGS.ZLAN) && ['@docusaurus/plugin-content-pages',{id: `zlan-pages`, path: `${zlan}/docusaurus/src/pages`, routeBasePath: `/${routeBase('zlan')}`}], + build(BUILD_FLAGS.ZROK) && ['@docusaurus/plugin-content-pages',{id: `zrok-pages`, path: `${zrokRoot}/src/pages`, routeBasePath: `/${routeBase('zrok')}`}], build(BUILD_FLAGS.ZROK) && extendDocsPlugins(zrokDocsPluginConfig(zrokRoot, REMARK_MAPPINGS, routeBase('zrok'))), build(BUILD_FLAGS.SELFHOSTED) && [ '@docusaurus/plugin-content-docs', @@ -353,6 +372,62 @@ const config: Config = { build(BUILD_FLAGS.SELFHOSTED) && onpremRedirects(routeBase('selfhosted')), ].filter(Boolean), themeConfig: { + netfoundry: { + productPickerColumns: [ + { + header: 'Managed Cloud', + links: [ + { + label: 'NetFoundry Console', + to: '#', + logo: 'https://raw.githubusercontent.com/netfoundry/branding/refs/heads/main/images/svg/icon/netfoundry-icon-color.svg', + description: 'Cloud-managed orchestration and global fabric control.', + }, + { + label: 'Frontdoor', + to: '/docs/frontdoor/intro', + logo: 'https://netfoundry.io/docs/img/frontdoor-sm-logo.svg', + description: 'Secure application access gateway.', + }, + ], + }, + { + header: 'Open Source', + links: [ + { + label: 'OpenZiti', + to: '/docs/openziti/learn/introduction', + logo: 'https://netfoundry.io/docs/img/openziti-sm-logo.svg', + description: 'Programmable zero-trust mesh infrastructure.', + }, + { + label: 'zrok', + to: '/docs/zrok/getting-started', + logo: 'https://netfoundry.io/docs/img/zrok-1.0.0-rocket-purple.svg', + logoDark: 'https://netfoundry.io/docs/img/zrok-1.0.0-rocket-green.svg', + description: 'Secure peer-to-peer sharing built on OpenZiti.', + }, + ], + }, + { + header: 'Your own infrastructure', + links: [ + { + label: 'Self-Hosted', + to: '/docs/selfhosted/intro', + logo: 'https://netfoundry.io/docs/img/onprem-sm-logo.svg', + description: 'Deploy the full stack in your own environment.', + }, + { + label: 'zLAN', + to: '/docs/zlan/intro', + logo: 'https://netfoundry.io/docs/img/zlan-logo.svg', + description: 'Zero-trust access for OT networks.', + }, + ], + }, + ], + }, docs: { sidebar: { hideable: false, @@ -366,20 +441,12 @@ const config: Config = { image: 'https://netfoundry.io/wp-content/uploads/2024/07/netfoundry-logo-tag-color-stacked-1.svg', navbar: { hideOnScroll: false, - title: 'NetFoundry Documentation', - logo: { - alt: 'NetFoundry Logo', - src: 'https://raw.githubusercontent.com/netfoundry/branding/refs/heads/main/images/svg/icon/netfoundry-icon-color.svg', - }, + title: '', items: [ { - label: 'Docs', + type: 'custom-productPicker', position: 'left', - items: [ - { to: '/selfhosted/intro', label: 'Self-Hosted' }, - { to: '/frontdoor/intro', label: 'Frontdoor' }, - { to: '/openziti/learn/introduction', label: 'OpenZiti' }, - ], + label: 'Products', }, ], }, diff --git a/unified-doc/docusaurus.config.ts.back b/unified-doc/docusaurus.config.ts.back deleted file mode 100644 index 39d541c..0000000 --- a/unified-doc/docusaurus.config.ts.back +++ /dev/null @@ -1,434 +0,0 @@ -import {themes as prismThemes} from 'prism-react-renderer'; -import type {Config, PluginConfig} from '@docusaurus/types'; -import type * as Preset from '@docusaurus/preset-classic'; -import * as path from "node:path"; -import { - LogLevel, - remarkCodeSections, - remarkReplaceMetaUrl, - remarkScopedPath, - remarkYouTube -} from "@netfoundry/docusaurus-theme/plugins"; -import remarkGithubAdmonitionsToDirectives from "remark-github-admonitions-to-directives"; -import {pluginHotjar} from "@netfoundry/docusaurus-theme/node"; -import {PublishConfig} from 'src/components/docusaurus' -import {zrokDocsPluginConfig} from "./_remotes/zrok/website/docusaurus-plugin-zrok-docs.ts"; - -// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) -const frontdoor = `./_remotes/frontdoor`; -const onprem = `./_remotes/onprem`; -const openziti = `./_remotes/openziti`; -const zrokRoot = `./_remotes/zrok/website`; -const zlan = `./_remotes/zlan`; - -const isVercel = process.env.IS_VERCEL === 'true'; -const docsBase = isVercel ? '/' : '/docs/'; - -const buildMask = parseInt(process.env.DOCUSAURUS_BUILD_MASK ?? "0xFF", 16); - -const BUILD_FLAGS = { - NONE: 0x0, - OPENZITI: 0x1, - FRONTDOOR: 0x2, - ONPREM: 0x4, - ZROK: 0x8, - ZLAN: 0x10, -}; - -function build(flag: number) { - return (buildMask & flag) !== 0; -} - -const staging: PublishConfig = { - docusaurus: { - url: 'https://netfoundry.io' - }, - algolia: { - appId: 'QRGW6TJXHP', - apiKey: '267457291182a398c5ee19fcb0bcae77', - indexName: 'nfdocs_stg', - }, - hotjar: { - id: "6443487" - }, - google: { - tag: 'GTM-5SF399H3' - } -} - -const prod: PublishConfig = { - docusaurus: { - url: 'https://netfoundry.io' - }, - algolia: { - appId: 'UWUTF7ESUI', - apiKey: '3a4a0691d0e8e3bb7c27c702c6a86ea9', - indexName: 'nfdocs', - }, - hotjar: { - id: "6506483" - }, - google: { - tag: 'GTM-NHX4DX56' - } -} - -const cfg: PublishConfig = process.env.DOCUSAURUS_PUBLISH_ENV === 'prod' ? prod : staging; - -const REMARK_MAPPINGS = [ - { from: '@onpremdocs', to: `${docsBase}onprem` }, - { from: '@openzitidocs', to: `${docsBase}openziti`}, - { from: '@zrokdocs', to: `${docsBase}zrok`}, - { from: '@static', to: docsBase}, -]; - -console.log("CANONICAL URL : " + cfg.docusaurus.url); -console.log("DOCUSAURUS_PUBLISH_ENV : " + process.env.DOCUSAURUS_PUBLISH_ENV) -console.log(" docsBase : " + docsBase); -console.log(" algolia index : " + cfg.algolia.indexName); -console.log(" build mask : " + buildMask); -console.log(" hotjar app : " + cfg.hotjar.id); -console.log('REMARK_MAPPINGS:', JSON.stringify(REMARK_MAPPINGS, null, 2)); - - -function extendDocsPlugins(plugin: PluginConfig): PluginConfig { - if (!Array.isArray(plugin)) return plugin; - - const [pluginName, config] = plugin; - - config.beforeDefaultRemarkPlugins = [ - ...(config.beforeDefaultRemarkPlugins || []), - remarkGithubAdmonitionsToDirectives, - ]; - - config.remarkPlugins = [ - ...(config.remarkPlugins || []), - [remarkScopedPath, { mappings: REMARK_MAPPINGS, logLevel: LogLevel.Silent }], - [remarkCodeSections, { logLevel: LogLevel.Silent }], - ]; - - return [pluginName, config]; -} - -function dumpRoutes() { - return { - name: 'dump-routes', - async allContentLoaded({allContent, actions}: any) { - const fs = require('node:fs'); - - // route list (most stable in v3) - const routes = actions.routesPaths ?? actions.routePaths ?? []; - - // optional: also dump plugin content ids so you can correlate routes - fs.writeFileSync( - 'routes.json', - JSON.stringify({routes, allContent}, null, 2), - ); - }, - }; -} - -function assertNoDocsPrefix() { - return (tree: any, file: any) => { - const p = String(file.path || ''); - const {visit} = require('unist-util-visit'); - - visit(tree, 'link', (node: any) => { - if (typeof node.url === 'string' && node.url.startsWith('/docs/')) { - console.log(`[assertNoDocsPrefix] ${p} url=${node.url}`); - } - }); - - visit(tree, 'jsx', (node: any) => { - if (typeof node.value === 'string' && node.value.includes('"/docs/')) { - console.log(`[assertNoDocsPrefix] ${p} jsx contains "/docs/`); - } - }); - }; -} - - -const config: Config = { - title: 'NetFoundry Documentation', - tagline: 'Documentation for NetFoundry products and projects', - favicon: 'https://raw.githubusercontent.com/netfoundry/branding/refs/heads/main/images/png/icon/netfoundry-icon-color.png', - - // Future flags, see https://docusaurus.io/docs/api/docusaurus-config#future - future: { - v4: true, // Improve compatibility with the upcoming Docusaurus v4 - }, - - // Set the production url of your site here - url: cfg.docusaurus.url, - // Set the // pathname under which your site is served - // For GitHub pages deployment, it is often '//' - baseUrl: docsBase, - // trailingSlash: false, leave as is - - // GitHub pages deployment config. - // If you aren't using GitHub pages, you don't need these. - organizationName: 'netfoundry', // Usually your GitHub org/user name. - projectName: 'netfoundry', // Usually your repo name. - - onBrokenLinks: 'throw', - - // Even if you don't use internationalization, you can use this field to set - // useful metadata like html lang. For example, if your site is Chinese, you - // may want to replace "en" with "zh-Hans". - i18n: { - defaultLocale: 'en', - locales: ['en'], - }, - markdown: { - hooks: { - onBrokenMarkdownLinks: "throw" - }, - mermaid: true, - }, - staticDirectories: [ - 'static', - '_remotes/frontdoor/docusaurus/static/', - '_remotes/onprem/docusaurus/static/', - '_remotes/openziti/docusaurus/static/', - '_remotes/zlan/docusaurus/static/', - `${zrokRoot}/static/`, - `${zrokRoot}/docs/images` - ], - customFields: { - DOCUSAURUS_BASE_PATH: docsBase, - DOCUSAURUS_DOCS_PATH: docsBase, - OPENZITI_DOCS_BASE: `${docsBase}openziti`, - UNIFIED_DOC_PATH: true, - ALGOLIA_APPID: cfg.algolia.appId, - ALGOLIA_APIKEY: cfg.algolia.apiKey, - ALGOLIA_INDEXNAME: cfg.algolia.indexName, - }, - themes: [ - ['@docusaurus/theme-classic', { - customCss: require.resolve('./src/css/custom.css'), - }], - '@docusaurus/theme-mermaid', - '@docusaurus/theme-search-algolia', - ], - plugins: [ - '@docusaurus/plugin-debug', - function webpackAliases() { - return { - name: 'unified-doc-webpack-aliases', - configureWebpack(config:any, isServer:any) { - return { - resolve: { - alias: { - '@openziti': path.resolve(__dirname, `${openziti}/docusaurus`), - '@frontdoor': path.resolve(__dirname, `${frontdoor}/docusaurus`), - '@onprem': path.resolve(__dirname, `${onprem}/docusaurus`), - '@zlan': path.resolve(__dirname, `${zlan}/docusaurus`), - '@zrok': path.resolve(__dirname, `${zrokRoot}`), - '@zrokroot': path.resolve(__dirname, `${zrokRoot}`), - '@staticdir': path.resolve(__dirname, `docusaurus/static`), - }, - }, - module: { - rules: [ - { - test: /\.ya?ml$/, - use: 'yaml-loader', - }, - ], - }, - }; - }, - }; - }, - - ['@docusaurus/plugin-content-pages',{path: 'src/pages',routeBasePath: '/'}], - build(BUILD_FLAGS.FRONTDOOR) && ['@docusaurus/plugin-content-pages',{id: `frontdoor-pages`, path: `${frontdoor}/docusaurus/src/pages`, routeBasePath: '/frontdoor'}], - build(BUILD_FLAGS.ONPREM) && ['@docusaurus/plugin-content-pages',{id: `onprem-pages`, path: `${onprem}/docusaurus/src/pages`, routeBasePath: '/onprem'}], - build(BUILD_FLAGS.OPENZITI) && ['@docusaurus/plugin-content-pages',{id: `openziti-pages`, path: `${openziti}/docusaurus/src/pages`, routeBasePath: '/openziti'}], - build(BUILD_FLAGS.ZLAN) && ['@docusaurus/plugin-content-pages',{id: `zlan-pages`, path: `${zlan}/docusaurus/src/pages`, routeBasePath: '/zlan'}], - build(BUILD_FLAGS.ZROK) && ['@docusaurus/plugin-content-pages',{id: `zrok-pages`, path: `${zrokRoot}/src/pages`, routeBasePath: '/zrok'}], - build(BUILD_FLAGS.ONPREM) && [ - '@docusaurus/plugin-content-docs', - { - id: 'onprem', // do not change - affects algolia search - path: `${onprem}/docusaurus/docs`, - routeBasePath: 'onprem', - sidebarPath: `${onprem}/docusaurus/sidebars.ts`, - includeCurrentVersion: true, - beforeDefaultRemarkPlugins: [ - remarkGithubAdmonitionsToDirectives, - ], - remarkPlugins: [ - [remarkScopedPath, { mappings: REMARK_MAPPINGS, debug: false }], - [remarkCodeSections, { logLevel: LogLevel.Silent }], - ], - }, - ], - build(BUILD_FLAGS.FRONTDOOR) && [ - '@docusaurus/plugin-content-docs', - { - id: 'frontdoor', // do not change - affects algolia search - path: `${frontdoor}/docusaurus/docs`, - routeBasePath: 'frontdoor', - sidebarPath: `${frontdoor}/docusaurus/sidebars.ts`, - includeCurrentVersion: true, - beforeDefaultRemarkPlugins: [ - remarkGithubAdmonitionsToDirectives, - ], - remarkPlugins: [ - [remarkScopedPath, { mappings: REMARK_MAPPINGS, logLevel: LogLevel.Silent}], - [remarkCodeSections, { logLevel: LogLevel.Silent }], - ], - }, - ], - build(BUILD_FLAGS.OPENZITI) && [ - '@docusaurus/plugin-content-docs', - { - id: 'openziti', // do not change - affects algolia search - path: `${openziti}/docusaurus/docs`, - routeBasePath: 'openziti', - sidebarPath: `${openziti}/docusaurus/sidebars.ts`, - includeCurrentVersion: true, - beforeDefaultRemarkPlugins: [ - remarkGithubAdmonitionsToDirectives, - ], - remarkPlugins: [ - [remarkReplaceMetaUrl, {from: '@staticoz', to: `${docsBase}openziti`, logLevel: LogLevel.Silent}], - [remarkScopedPath, { mappings: REMARK_MAPPINGS, logLevel: LogLevel.Silent }], - [remarkCodeSections, { logLevel: LogLevel.Debug }], - ], - }, - ], - build(BUILD_FLAGS.ZLAN) && [ - '@docusaurus/plugin-content-docs', - { - id: 'zlan', // do not change - affects algolia search - path: `${zlan}/docusaurus/docs`, - routeBasePath: 'zlan', - sidebarPath: `${zlan}/docusaurus/sidebars.ts`, - includeCurrentVersion: true, - beforeDefaultRemarkPlugins: [ - remarkGithubAdmonitionsToDirectives, - ], - remarkPlugins: [ - [remarkScopedPath, { mappings: REMARK_MAPPINGS, logLevel: LogLevel.Silent }], - [remarkCodeSections, { logLevel: LogLevel.Silent }], - ], - }, - ], - build(BUILD_FLAGS.OPENZITI) && [ - '@docusaurus/plugin-content-blog', - { - showReadingTime: true, - routeBasePath: 'openziti/blog', - tagsBasePath: 'tags', - include: ['**/*.{md,mdx}'], - path: '_remotes/openziti/docusaurus/blog', - remarkPlugins: [ - remarkYouTube, - [remarkReplaceMetaUrl, {from: '@staticoz', to: `${docsBase}openziti`, logLevel: LogLevel.Silent}], - [remarkScopedPath, { mappings: REMARK_MAPPINGS, logLevel: LogLevel.Silent }], - [remarkCodeSections, { logLevel: LogLevel.Silent }], - ], - blogSidebarCount: 'ALL', - blogSidebarTitle: 'All posts', - }, - ], - build(BUILD_FLAGS.ZROK) && extendDocsPlugins(zrokDocsPluginConfig(zrokRoot, REMARK_MAPPINGS)), - // Fallback redirects for JSX pages with hardcoded /docs/ paths (from upstream repos) - isVercel && [ - '@docusaurus/plugin-client-redirects', - { - createRedirects(existingPath: string) { - // Redirect /docs/X → /X for all doc paths - return existingPath.match(/^\/(onprem|frontdoor|openziti|zrok|zlan)/) - ? [`/docs${existingPath}`] - : undefined; - }, - }, - ], - ['@docusaurus/plugin-sitemap', { changefreq: "daily", priority: 0.8 }], - [pluginHotjar, {}], - ['@docusaurus/plugin-google-tag-manager', {id: `openziti-gtm`, containerId: cfg.google.tag}], - ].filter(Boolean), - themeConfig: { - docs: { - sidebar: { - hideable: false, - autoCollapseCategories: true - } - }, - mermaid: { - theme: {light: 'neutral', dark: 'forest'}, - }, - // Replace with your project's social card - image: 'https://netfoundry.io/wp-content/uploads/2024/07/netfoundry-logo-tag-color-stacked-1.svg', - navbar: { - hideOnScroll: false, - title: 'NetFoundry Documentation', - logo: { - alt: 'NetFoundry Logo', - src: 'https://raw.githubusercontent.com/netfoundry/branding/refs/heads/main/images/svg/icon/netfoundry-icon-color.svg', - }, - items: [ - { - label: 'Docs', - position: 'left', - items: [ - { to: '/onprem/intro', label: 'On-Prem' }, - { to: '/frontdoor/intro', label: 'Frontdoor' }, - { to: '/openziti/learn/introduction', label: 'OpenZiti' }, - ], - }, - ], - }, - prism: { - theme: prismThemes.github, - darkTheme: prismThemes.dracula, - }, - algolia: { - appId: cfg.algolia.appId, - apiKey: cfg.algolia.apiKey, - indexName: cfg.algolia.indexName, - contextualSearch: true, - searchParameters: { - typoTolerance: "min", - hitsPerPage: 25, - attributesToRetrieve: ["content", "hierarchy", "url"], - attributesToHighlight: ["content", "hierarchy"], - restrictSearchableAttributes: ["content", "hierarchy"] - }, - searchPagePath: 'search' - }, - hotjar: { - applicationId: cfg.hotjar.id - }, - } satisfies Preset.ThemeConfig, - presets: [ - [ 'redocusaurus', - { - specs: [ - { - id: 'openapi', - spec: `${frontdoor}/docusaurus/static/frontdoor-api-spec.yaml`, - }, - { - id: 'edge-client', - spec: 'https://get.openziti.io/spec/client.yml', - }, - { - id: 'edge-management', - spec: 'https://get.openziti.io/spec/management.yml', - }, - ], - // Theme Options for modifying how redoc renders them - theme: { - // Change with your site colors - primaryColor: '#1890ff', - } - }, - ], - ], -}; - -export default config; diff --git a/unified-doc/package.json b/unified-doc/package.json index 8ddd5d0..33ee4a3 100644 --- a/unified-doc/package.json +++ b/unified-doc/package.json @@ -53,7 +53,7 @@ "@docusaurus/theme-mermaid": "^3.9.2", "@hotjar/browser": "^1.0.9", "@mdx-js/react": "^3.0.0", - "@netfoundry/docusaurus-theme": "^0.7.0", + "@netfoundry/docusaurus-theme": "0.8.0", "algoliasearch": "^5.36.0", "asciinema-player": "^3.10.0", "clsx": "^2.0.0", diff --git a/unified-doc/src/pages/index.module.css b/unified-doc/src/pages/index.module.css index 11782a8..f7a749b 100644 --- a/unified-doc/src/pages/index.module.css +++ b/unified-doc/src/pages/index.module.css @@ -1,29 +1,160 @@ -/** - * CSS files with the .module.css suffix will be treated as CSS modules - * and scoped locally. - */ - -.heroBanner { - padding: 4rem 0; - text-align: center; - position: relative; - overflow: hidden; -} - -@media screen and (max-width: 996px) { - .heroBanner { - padding: 2rem; - } -} - -.buttons { - display: flex; - align-items: center; - justify-content: center; -} - -.idxcard { - min-height: 200px; - display: flex; - flex-direction: column; -} \ No newline at end of file +.nf-hero-stage { + position: relative; width: 100%; min-height: 370px; + display: flex; align-items: center; justify-content: center; + overflow: hidden; text-align: center; background: #020617; z-index: 4; +} +.nf-hero-stage::after { + content: ''; position: absolute; bottom: 0; left: 0; right: 0; + height: 220px; background: linear-gradient(to bottom, transparent 0%, #0f172a 100%); + pointer-events: none; z-index: 1; +} +:global([data-theme='light']) .nf-hero-stage::after { + height: 80px; + background: linear-gradient(to bottom, transparent 0%, #020617 100%); +} +.nf-hero-overlay { display: none; } +.nf-hero-content { + position: relative; z-index: 2; padding: 2.5rem 3.5rem; + background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; +} +.nf-hero-title { + font-size: 4rem; font-weight: 900; color: #ffffff; margin-bottom: 0.75rem; + letter-spacing: -0.02em; line-height: 1.05; + text-shadow: 0 0 20px rgba(34, 211, 238, 0.8), 0 2px 12px rgba(0, 0, 0, 0.9); +} +.nf-green-text { + background: linear-gradient(to right, #22c55e 0%, #86efac 100%); + -webkit-background-clip: text; background-clip: text; + -webkit-text-fill-color: transparent; display: inline-block; +} +.nf-hero-subtext { + color: rgba(203, 213, 225, 0.95); font-size: 1.15rem; max-width: 560px; + margin: 0 auto 2rem; line-height: 1.65; text-shadow: 0 1px 8px rgba(0, 0, 0, 0.95); +} +.nf-hero-ctas { display: flex; gap: 1rem; justify-content: center; } +.nf-btn-primary { + display: inline-flex; align-items: center; padding: 0.65rem 1.75rem; + background: #0076FF; color: #ffffff; border-radius: 8px; font-weight: 700; + font-size: 0.95rem; text-decoration: none; transition: all 0.25s ease; border: 2px solid #0076FF; +} +.nf-btn-primary:hover { + background: #005ce6; border-color: #005ce6; transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 118, 255, 0.4); color: #ffffff; +} +.nf-btn-ghost { + display: inline-flex; align-items: center; padding: 0.65rem 1.75rem; + background: transparent; color: #ffffff; border-radius: 8px; font-weight: 700; + font-size: 0.95rem; text-decoration: none; transition: all 0.25s ease; + border: 2px solid rgba(255, 255, 255, 0.25); +} +.nf-btn-ghost:hover { + background: rgba(255, 255, 255, 0.06); border-color: rgba(34, 211, 238, 0.5); + transform: translateY(-2px); color: #ffffff; +} + +.nf-features-section { width: 100%; background: #0f172a; padding: 5rem 0 2.5rem; } +:global([data-theme='light']) .nf-features-section { + background: linear-gradient(to bottom, + #020617 0px, #020617 80px, #404350 18%, #7d808a 28%, + #b8bbc1 38%, #e4e7ea 48%, #f8fafc 100%); +} + +.nf-bento-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; } + +.nf-bento-divider { + grid-column: 1 / -1; display: flex; align-items: center; gap: 1rem; + padding: 1.5rem 0 0.65rem; color: #94a3b8; font-size: 0.82rem; font-weight: 800; + letter-spacing: 0.12em; text-transform: uppercase; white-space: nowrap; +} +.nf-bento-divider::before, .nf-bento-divider::after { + content: ''; flex: 1; height: 1px; background: rgba(148, 163, 184, 0.2); +} +.nf-divider--top { padding-top: 0; } +.nf-divider--managed { + color: #22d3ee; font-size: 1.05rem; letter-spacing: 0.15em; + text-shadow: 0 0 18px rgba(34, 211, 238, 0.5); +} +.nf-divider--managed::before, .nf-divider--managed::after { background: rgba(34, 211, 238, 0.4); height: 2px; } +:global([data-theme='light']) .nf-bento-divider { color: #64748b; } +:global([data-theme='light']) .nf-bento-divider::before, +:global([data-theme='light']) .nf-bento-divider::after { background: rgba(100, 116, 139, 0.25); } +:global([data-theme='light']) .nf-divider--managed { color: #0891b2; text-shadow: none; } +:global([data-theme='light']) .nf-divider--managed::before, +:global([data-theme='light']) .nf-divider--managed::after { background: rgba(8, 145, 178, 0.35); height: 2px; } + +.nf-pair { display: flex; flex-direction: column; } +.nf-pair-connector { + display: flex; align-items: center; gap: 1rem; padding: 0.5rem 0; + font-size: 0.7rem; font-weight: 800; letter-spacing: 0.12em; + text-transform: uppercase; color: #94a3b8; +} +.nf-pair-connector::before, .nf-pair-connector::after { + content: ''; flex: 1; height: 1px; background: rgba(148, 163, 184, 0.2); +} +:global([data-theme='light']) .nf-pair-connector { color: #64748b; } +:global([data-theme='light']) .nf-pair-connector::before, +:global([data-theme='light']) .nf-pair-connector::after { background: rgba(148, 163, 184, 0.35); } + +.nf-bento-wrap { display: flex; flex-direction: column; } + +.nf-bento-card { + position: relative; display: flex; flex-direction: column; flex: 1; + padding: 1rem; border-radius: 12px; text-decoration: none; + background: #1a1b2e; border: 1px solid rgba(148, 163, 184, 0.1); + border-top: 2px solid #22d3ee; + box-shadow: 0 1px 3px rgba(0,0,0,0.3), 0 6px 20px rgba(0,0,0,0.3), 0 16px 40px rgba(0,0,0,0.2); + transition: transform 0.2s ease, box-shadow 0.2s ease, border-top-color 0.2s ease; +} +.nf-bento-card:hover { + transform: translateY(-4px); border-top-color: #22c55e; + box-shadow: 0 2px 6px rgba(0,0,0,0.4), 0 12px 32px rgba(0,0,0,0.45), + 0 24px 60px rgba(0,0,0,0.25), 0 0 0 1px rgba(34,197,94,0.12); +} +:global([data-theme='light']) .nf-bento-card { + background: #edf3f8; border-color: rgba(0,0,0,0.08); + box-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 4px 14px rgba(0,0,0,0.05); +} +:global([data-theme='light']) .nf-bento-card:hover { + box-shadow: 0 2px 6px rgba(0,0,0,0.09), 0 8px 24px rgba(0,0,0,0.07), 0 0 0 1px rgba(34,197,94,0.18); +} +:global([data-theme='light']) .nf-bento-card--accent-cyan { border-top-color: #0891b2; } +:global([data-theme='light']) .nf-bento-card--accent-green { border-top-color: #16a34a; } +.nf-bento-card--featured { padding: 1.25rem; border-top-width: 3px; } +.nf-bento-card--featured .nf-card-logo { width: 48px; height: 48px; } +.nf-bento-card--featured .nf-card-header h3 { font-size: 1.25rem; } + +.nf-card-badge { + position: absolute; top: 1rem; right: 1rem; display: inline-flex; width: fit-content; + background: rgba(34,197,94,0.1); color: #4ade80; border: 1px solid rgba(34,197,94,0.2); + font-size: 0.6rem; font-weight: 800; letter-spacing: 0.1em; + padding: 2px 8px; border-radius: 4px; text-transform: uppercase; +} +:global([data-theme='light']) .nf-card-badge { color: #15803d; border-color: rgba(34,197,94,0.25); } + +.nf-card-header { + display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; padding-right: 3rem; +} +.nf-card-header h3 { margin: 0; } +.nf-bento-card h3 { color: #f1f5f9; font-weight: 900; font-size: 1.05rem; line-height: 1.3; letter-spacing: -0.02em; } +:global([data-theme='light']) .nf-bento-card h3 { color: #0f172a; } +.nf-bento-card p { color: #94a3b8; font-size: 0.875rem; line-height: 1.65; flex-grow: 1; margin: 0 0 0.5rem; } +:global([data-theme='light']) .nf-bento-card p { color: #475569; } + +.nf-bento-features { list-style: none; padding: 0; margin: 0 0 0.5rem; display: flex; flex-direction: column; gap: 0.25rem; } +.nf-bento-features li { display: flex; align-items: center; gap: 0.4rem; font-size: 0.775rem; color: #64748b; } +.nf-bento-features li::before { content: '✓'; color: #22c55e; font-weight: 800; font-size: 0.75rem; flex-shrink: 0; } +:global([data-theme='light']) .nf-bento-features li::before { color: #16a34a; } + +.nf-card-link { color: #22d3ee; font-size: 0.8rem; font-weight: 700; margin-top: auto; padding-top: 0.5rem; letter-spacing: 0.03em; } +:global([data-theme='light']) .nf-card-link { color: #0284c7; } +.nf-card-logo { width: 40px; height: 40px; object-fit: contain; flex-shrink: 0; } + +@media (max-width: 996px) { + .nf-hero-title { font-size: 2.2rem; } + .nf-hero-content { padding: 2rem 1.25rem; } + .nf-hero-ctas { flex-wrap: wrap; } + .nf-btn-primary, .nf-btn-ghost { padding: 0.55rem 1.25rem; font-size: 0.9rem; } +} +@media (max-width: 640px) { + .nf-bento-grid { grid-template-columns: 1fr; } +} diff --git a/unified-doc/src/pages/index.tsx b/unified-doc/src/pages/index.tsx index 6d579b3..ca6cb1f 100644 --- a/unified-doc/src/pages/index.tsx +++ b/unified-doc/src/pages/index.tsx @@ -1,152 +1,135 @@ -// src/pages/index.tsx -import type {ReactNode} from 'react'; +import React, {JSX} from 'react'; +import Layout from '@theme/Layout'; import Link from '@docusaurus/Link'; -import useBaseUrl from '@docusaurus/useBaseUrl'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; -import { - defaultNetFoundryFooterProps, - NetFoundryHorizontalSection, - NetFoundryLayout, -} from '@netfoundry/docusaurus-theme/ui'; -import styles from "./index.module.css"; +import clsx from 'clsx'; +import {DOCS_BASE} from '../generated/docsBase'; +import styles from './index.module.css'; -export default function Home(): ReactNode { - const {siteConfig} = useDocusaurusContext(); - const fp = defaultNetFoundryFooterProps(); - fp.description = 'NetFoundry documentation for open source projects and products'; +const CYAN = '#22d3ee'; +const GREEN = '#22c55e'; +const IMG = 'https://netfoundry.io/docs/img'; +const NF_LOGO = 'https://raw.githubusercontent.com/netfoundry/branding/refs/heads/main/images/svg/icon/netfoundry-icon-color.svg'; - return ( - -
- -
-

NetFoundry Docs

-

- Guides, references, and how-tos for NetFoundry products and OpenZiti. -

-
- NetFoundry SaaS Docs - - Self-Hosted Docs - - Frontdoor Docs - - OpenZiti Docs - - zLAN Docs -
-
-
+const products = [ + { + id: 'console', + title: 'NetFoundry Console', + logo: NF_LOGO, + tag: 'Managed', + accent: CYAN, + link: '#', + features: ['Fully managed SaaS', 'Global edge fabric', 'No infra to operate', 'Policy-based access'], + description: "The cloud-managed control plane for NetFoundry's global zero-trust fabric. Orchestrate identities, policies, and edge routers — no infrastructure to run." + }, + { + id: 'openziti', + title: 'OpenZiti', + logo: `${IMG}/openziti-sm-logo.svg`, + tag: 'Open Source', + accent: GREEN, + link: `${DOCS_BASE}openziti/learn/introduction`, + description: 'The open-source zero-trust networking framework at the heart of the NetFoundry platform. Embed dark, app-native security directly in your code — no VPN, no perimeter.' + }, + { + id: 'frontdoor', + title: 'Frontdoor', + logo: `${IMG}/frontdoor-sm-logo.svg`, + tag: 'Managed', + accent: CYAN, + link: `${DOCS_BASE}frontdoor/intro`, + features: ['No agent or VPN required', 'Zero firewall rules', 'Identity-based access', 'Any app, any browser'], + description: 'Secure, clientless access to any application — without a VPN or firewall rule. Expose nothing to the internet while giving authorized users instant access.' + }, + { + id: 'zrok', + title: 'zrok', + logo: `${IMG}/zrok-1.0.0-rocket-purple.svg`, + tag: 'Open Source', + accent: GREEN, + link: `${DOCS_BASE}zrok/getting-started`, + description: 'Geo-scale secure sharing built on the OpenZiti mesh. Share services, files, or HTTP endpoints peer-to-peer — no open ports, no NAT traversal tricks.' + }, + { + id: + 'selfhosted', + title: 'NetFoundry Self-Hosted', + logo: `${IMG}/onprem-sm-logo.svg`, + tag: 'Self-Hosted', + accent: CYAN, + link: `${DOCS_BASE}selfhosted/intro`, + features: ['Full infrastructure control', 'Air-gap compatible', 'On-prem or any cloud', 'Enterprise SLA'], + description: 'Deploy the full NetFoundry control plane and fabric in your own environment. Full sovereignty over your zero-trust infrastructure — on-prem, air-gapped, or any cloud.' + }, + { + id: 'zlan', + title: 'zLAN', + logo: `${IMG}/zlan-logo.svg`, + tag: 'OT Security', + accent: CYAN, + link: `${DOCS_BASE}zlan/intro`, + features: ['Deep OT/IT traffic visibility', 'Identity-aware micro-segmentation', 'Centralized zero-trust policy', 'Built on NetFoundry Self-Hosted'], + description: 'Identity-aware micro-segmentation firewall for operational technology networks. Deep traffic visibility, centralized policy, and zero-trust access control for OT environments.' + }, +]; - -
-
-
-
-

NetFoundry SaaS

-
- Enterprise cloud-hosted platform for OpenZiti overlays. -
-
- Go to NetFoundry SaaS -
-
-
-
-
-

NetFoundry Self-Hosted

-
- Enterprise self-hosted platform for OpenZiti overlays. -
-
- Go to NetFoundry Self-Hosted -
-
-
-
-
-

NetFoundry Frontdoor

-
- Zero-trust inbound access to private apps and services. -
-
- Go to NetFoundry Frontdoor -
-
-
-
-
-

OpenZiti

-
- Open-source zero-trust networking project and SDKs. -
-
- Go to OpenZiti -
-
-
-
-
-

zLAN

-
- Built on the robust foundation of NetFoundy OpenZiti, NetFoundry zLAN combines advanced firewall capabilities with the power of zero trust and secure network overlay -
-
- Go to zLAN -
-
-
-
-
-

zrok

-
- zrok is an open-source, self-hostable sharing platform that simplifies shielding and sharing network services or files. -
-
- Go to zrok -
-
-
-
+type Product = (typeof products)[number]; +const byId = Object.fromEntries(products.map(p => [p.id, p])) as Record; -
-
-
-

Quick Links

-
-
    -
  • NetFoundry Troubleshooting
  • -
  • Self-Hosted Deployment
  • -
  • Frontdoor Getting Started
  • -
  • OpenZiti CLI Reference
  • -
  • zLAN FAQ
  • -
-
-
-
-
-
-

Support

-
-
    -
  • NetFoundry Troubleshooting
  • -
  • Self-Hosted Troubleshooting
  • -
  • Frontdoor Troubleshooting
  • -
  • OpenZiti FAQ
  • -
  • zLAN FAQ
  • -
-
-
-
-
+function BentoCard({product, featured = false}: {product: Product; featured?: boolean}): JSX.Element { + const accentMod = product.accent === CYAN ? styles['nf-bento-card--accent-cyan'] : styles['nf-bento-card--accent-green']; + return ( +
+ + {product.tag} +
+ {product.logo && {product.title}} +

{product.title}

+
+

{product.description}

+ {product.features && ( +
    + {product.features.map(f =>
  • {f}
  • )} +
+ )} +
Explore →
+ +
+ ); +} -
-
-
-
- ); +export default function Home(): JSX.Element { + return ( + +
+
+

NetFoundry Docs

+

Secure, high-performance networking for the modern era.

+
+ Get Started + Request Demo +
+
+
+
+
+
+
Managed Cloud
+
+ +
open-source counterpart
+ +
+
+ +
open-source counterpart
+ +
+
Run on your own infrastructure
+ + +
+
+
+
+ ); } diff --git a/unified-doc/src/theme/Navbar/Content/index.tsx b/unified-doc/src/theme/Navbar/Content/index.tsx index 2d794ea..e661dd3 100644 --- a/unified-doc/src/theme/Navbar/Content/index.tsx +++ b/unified-doc/src/theme/Navbar/Content/index.tsx @@ -14,115 +14,21 @@ import {useNavbarMobileSidebar} from "@docusaurus/theme-common/internal"; type Props = React.ComponentProps; type Item = any; -// change to '' if you don't use /docs const DOCS_PREFIX = '/docs'; -const defaultItems: Item[] = [ - // {label: 'NetFoundry', to: '/', position: 'left'}, - // {label: 'Downloads', to: '/downloads', position: 'left'}, - // {label: 'Blog', to: '/blog', position: 'left'}, -]; +const productPicker: Item = { type: 'custom-productPicker', position: 'left' }; -const netfoundryDocs = {to: `https://support.netfoundry.io/hc/en-us/categories/360000991011-Docs-Guides`, label: 'NetFoundry SaaS'}; -const nfFrontDoorDocs = {to: `${DOCS_PREFIX}/frontdoor/intro`, label: 'Frontdoor'}; -const onPremDocs = {to: `${DOCS_PREFIX}/selfhosted/intro`, label: 'Self-Hosted'}; -const zlanDocs = {to: `${DOCS_PREFIX}/zlan/intro`, label: 'zLAN'}; -const ozDocs = {to: `${DOCS_PREFIX}/openziti/learn/introduction`, label: 'OpenZiti'}; -const zrokDocs = {to: `${DOCS_PREFIX}/zrok/getting-started`, label: 'zrok'}; - -const openZitiNav: Item[] = [ - { - label: 'OpenZiti Docs', - to: `${DOCS_PREFIX}/openziti/learn/introduction`, - position: 'left', - type: 'dropdown', - items: [ - netfoundryDocs, - nfFrontDoorDocs, - onPremDocs, - zlanDocs, - zrokDocs, - ], - } -]; - -const onpremNav: Item[] = [ - { - label: 'Self-Hosted Docs', - to: `${DOCS_PREFIX}/selfhosted/intro`, - position: 'left', - type: 'dropdown', - items: [ - netfoundryDocs, - nfFrontDoorDocs, - ozDocs, - zlanDocs, - zrokDocs, - ], - }, -]; - -const frontdoorNav: Item[] = [ - { - label: 'Frontdoor Docs', - to: `${DOCS_PREFIX}/frontdoor/intro`, - position: 'left', - type: 'dropdown', - items: [ - netfoundryDocs, - onPremDocs, - ozDocs, - zlanDocs, - zrokDocs, - ], - }, -]; - -const zlanNav: Item[] = [ - { - label: 'zLAN Docs', - to: `${DOCS_PREFIX}/zlan/intro`, - position: 'left', - type: 'dropdown', - items: [ - netfoundryDocs, - nfFrontDoorDocs, - onPremDocs, - ozDocs, - zrokDocs, - ], - }, -]; - -const zrokNav: Item[] = [ - { - label: 'zrok Docs', - to: `${DOCS_PREFIX}/zrok/getting-started`, - position: 'left', - type: 'dropdown', - items: [ - netfoundryDocs, - nfFrontDoorDocs, - onPremDocs, - ozDocs, - zlanDocs, - ], - }, - { - type: 'docsVersionDropdown', - docsPluginId: 'zrok', - dropdownItemsBefore: [], - dropdownItemsAfter: [], - }, -]; +const sectionLabel = (p: string): string => { + if (p.startsWith(`${DOCS_PREFIX}/frontdoor`)) return 'Frontdoor'; + if (p.startsWith(`${DOCS_PREFIX}/selfhosted`)) return 'Self-Hosted'; + if (p.startsWith(`${DOCS_PREFIX}/openziti`)) return 'OpenZiti'; + if (p.startsWith(`${DOCS_PREFIX}/zlan`)) return 'zLAN'; + if (p.startsWith(`${DOCS_PREFIX}/zrok`)) return 'zrok'; + return 'Products'; +}; const mapNavbar = (p: string): Item[] => { - if (p.startsWith(`${DOCS_PREFIX}/frontdoor`)) return frontdoorNav; - if (p.startsWith(`${DOCS_PREFIX}/selfhosted`)) return onpremNav; - if (p.startsWith(`${DOCS_PREFIX}/openziti`)) return openZitiNav; - if (p.startsWith(`${DOCS_PREFIX}/zlan`)) return zlanNav; - if (p.startsWith(`${DOCS_PREFIX}/zrok`)) return zrokNav; - return defaultItems; + return [{...productPicker, label: sectionLabel(p)}]; }; export default function NavbarContent(props: Props): JSX.Element { @@ -136,10 +42,8 @@ export default function NavbarContent(props: Props): JSX.Element { const left = items.filter((i) => i.position !== 'right'); const right = items.filter((i) => i.position === 'right'); - const mobileSidebar = useNavbarMobileSidebar(); - return (
diff --git a/unified-doc/src/theme/Navbar/Logo/index.tsx b/unified-doc/src/theme/Navbar/Logo/index.tsx index c21ecc6..9aa7cf6 100644 --- a/unified-doc/src/theme/Navbar/Logo/index.tsx +++ b/unified-doc/src/theme/Navbar/Logo/index.tsx @@ -12,17 +12,17 @@ const mapTitle = (p: string) => { const rootSegment = segments[1] === 'docs' ? segments[2] : segments[1]; const checkPath = (segment: string) => rootSegment === segment; - if (checkPath('frontdoor')) return {includeNFLogo: true, to: '/frontdoor', alt:'Frontdoor', logoLight: `/img/frontdoor-sm-logo.svg`, logoDark: `/img/frontdoor-sm-logo.svg`}; - if (checkPath('selfhosted')) return {includeNFLogo: true, to: '/selfhosted',alt:'Self-Hosted', logoLight: `/img/onprem-sm-logo.svg`, logoDark: `/img/onprem-sm-logo.svg`}; - if (checkPath('openziti')) return {includeNFLogo: true, to: '/openziti',alt:'OpenZiti', logoLight: `/img/openziti-sm-logo.svg`, logoDark: `/img/openziti-sm-logo.svg`}; - if (checkPath('zlan')) return {includeNFLogo: true, to: '/zlan', alt:'zlan', logoLight: `/img/zlan-logo.svg`, logoDark: `/img/zlan-logo.svg`}; - if (checkPath('zrok')) return {text: '', includeNFLogo: true, to: '/zrok', alt:'zrok', logoLight: `/img/zrok-1.0.0-rocket-purple.svg`, logoDark: `/img/zrok-1.0.0-rocket-green.svg`}; + if (checkPath('frontdoor')) return {includeNFLogo: true, to: '/docs/frontdoor', alt:'Frontdoor', logoLight: `/img/frontdoor-sm-logo.svg`, logoDark: `/img/frontdoor-sm-logo.svg`}; + if (checkPath('selfhosted')) return {includeNFLogo: true, to: '/docs/selfhosted',alt:'Self-Hosted', logoLight: `/img/onprem-sm-logo.svg`, logoDark: `/img/onprem-sm-logo.svg`}; + if (checkPath('openziti')) return {includeNFLogo: true, to: '/docs/openziti',alt:'OpenZiti', logoLight: `/img/openziti-sm-logo.svg`, logoDark: `/img/openziti-sm-logo.svg`}; + if (checkPath('zlan')) return {includeNFLogo: true, to: '/docs/zlan', alt:'zlan', logoLight: `/img/zlan-logo.svg`, logoDark: `/img/zlan-logo.svg`}; + if (checkPath('zrok')) return {text: '', includeNFLogo: true, to: '/docs/zrok', alt:'zrok', logoLight: `/img/zrok-1.0.0-rocket-purple.svg`, logoDark: `/img/zrok-1.0.0-rocket-green.svg`}; return { includeNFLogo: false, to: '/', alt:'NetFoundry', - logoLight: `/img/netfoundry-name-and-logo.svg`, - logoDark: `/img/netfoundry-name-and-logo-dark.svg` + logoLight: '', + logoDark: '' }; }; @@ -43,27 +43,31 @@ export default function NavbarLogo(): JSX.Element { return ( <> - - - - - - {title.text} - + + + + {(title.logoLight || title.text) && ( + + {title.logoLight && ( + + )} + {title.text && {title.text}} + + )} ); } \ No newline at end of file diff --git a/unified-doc/yarn.lock b/unified-doc/yarn.lock index ebbffbd..370f5b3 100644 --- a/unified-doc/yarn.lock +++ b/unified-doc/yarn.lock @@ -2599,10 +2599,10 @@ hls.js "~1.6.6" mux-embed "^5.8.3" -"@netfoundry/docusaurus-theme@^0.7.0": - version "0.7.0" - resolved "https://registry.yarnpkg.com/@netfoundry/docusaurus-theme/-/docusaurus-theme-0.7.0.tgz#a02dba7d8aede49bb8bd14d1e135ac6b6a1e6aa0" - integrity sha512-duxsWjmhigQTQs7ikKxQ1UzLdzPu2NKPjr18t/pjD9U9YV0eABCD1GQRJbwbfgHYEuqoGrV1LLEVn+xPkoTGDg== +"@netfoundry/docusaurus-theme@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@netfoundry/docusaurus-theme/-/docusaurus-theme-0.8.0.tgz#d2d714fa76c8b67bb819e1b8142a2c34f4fd64da" + integrity sha512-h+Wxx8tB3L9v673VlIBREzPJj1pEtdk388s2WpZWaek3ZVsv/Nva+f5h1HmZakJWAA76Agm0rWaiSSzg7b+wDA== dependencies: "@docsearch/react" "^3" algoliasearch "^5"