diff --git a/packages/docusaurus-theme/css/hero-background.css b/packages/docusaurus-theme/css/hero-background.css new file mode 100644 index 0000000..5b75434 --- /dev/null +++ b/packages/docusaurus-theme/css/hero-background.css @@ -0,0 +1 @@ +/* HeroBackground — reserved for future animation styles */ diff --git a/packages/docusaurus-theme/css/product-picker.css b/packages/docusaurus-theme/css/product-picker.css index 3270882..bd10933 100644 --- a/packages/docusaurus-theme/css/product-picker.css +++ b/packages/docusaurus-theme/css/product-picker.css @@ -25,12 +25,14 @@ position: relative; padding-right: 1.2rem; transition: color 0.3s ease; + font-weight: var(--ifm-font-weight-bold); } .nf-picker-trigger:hover, .nf-resources-dropdown:hover, .navbar__item.dropdown--show .nf-picker-trigger, -.navbar__item.dropdown--show .nf-resources-dropdown { +.navbar__item.dropdown--show .nf-resources-dropdown, +.navbar__item.nf-picker--open .nf-picker-trigger { color: var(--ifm-color-primary); text-decoration: none; } @@ -59,17 +61,17 @@ opacity: 1; } -/* Open: chevron rotates 180° */ +/* Open: chevron holds the hover drop position */ .nf-picker--open .nf-picker-trigger::after, .navbar__item.dropdown--show .nf-resources-dropdown::after { - transform: translateY(-50%) rotate(180deg); + transform: translateY(-20%); 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.nf-picker--open .nf-picker-trigger, [data-theme='dark'] .navbar__item.dropdown--show .nf-resources-dropdown { color: #22d3ee; } @@ -138,6 +140,7 @@ /* Narrower panel for the 2-column Resources menu */ .dropdown__menu:has(.picker-resources) { max-width: 700px; } +.nf-picker-panel--narrow { max-width: 680px; } /* Ensure visibility when open */ .dropdown--show > .dropdown__menu, @@ -179,9 +182,9 @@ 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; } +.picker-link:hover { background: rgba(0, 118, 255, 0.06); transform: translateX(3px); text-decoration: none; } +.picker-link strong { color: var(--ifm-font-color-base); font-size: 0.95rem; font-weight: 900; letter-spacing: -0.02em; display: block; text-decoration: none; } +.picker-link span { color: #64748b; font-size: 0.82rem; display: block; margin-top: 2px; line-height: 1.35; text-decoration: none; } /* ── Mobile (<= 996 px) ─────────────────────────────────────────────────── */ @media (max-width: 996px) { diff --git a/packages/docusaurus-theme/css/theme.css b/packages/docusaurus-theme/css/theme.css index 0b06560..276aa22 100644 --- a/packages/docusaurus-theme/css/theme.css +++ b/packages/docusaurus-theme/css/theme.css @@ -1,20 +1,51 @@ -/** - * NetFoundry Docusaurus Theme - Combined Styles - * - * This file is automatically loaded by the theme via getClientModules(). - * - * Consuming projects no longer need to manually add these imports - * to their custom.css files. - */ - -/* CSS variables for light mode */ -@import "./vars.css"; - -/* CSS variables for dark mode */ -@import "./vars-dark.css"; - -/* Layout styles */ -@import "./layout.css"; - -/* Legacy design system variables and comprehensive styling */ -@import "./legacy.css"; +/** + * NetFoundry Docusaurus Theme - Combined Styles + * + * This file is automatically loaded by the theme via getClientModules(). + * + * Consuming projects no longer need to manually add these imports + * to their custom.css files. + */ + +/* CSS variables for light mode */ +@import "./vars.css"; + +/* CSS variables for dark mode */ +@import "./vars-dark.css"; + +/* Layout styles */ +@import "./layout.css"; + +/* Legacy design system variables and comprehensive styling */ +@import "./legacy.css"; + +/* ── Footer social link hover ───────────────────────────────────────────── */ +footer a[class*="footerSocialLink"] { + transition: all 0.2s ease; +} + +[data-theme='dark'] footer a[class*="footerSocialLink"] { + background-color: #1a2640; + color: #64748b; + border: 1px solid rgba(148, 163, 184, 0.1); +} +[data-theme='dark'] footer a[class*="footerSocialLink"]:hover { + background-color: #22d3ee; + color: #020617; + border-color: transparent; + transform: translateY(-3px); + box-shadow: 0 6px 16px rgba(34, 211, 238, 0.35); +} + +[data-theme='light'] footer a[class*="footerSocialLink"] { + background-color: #e2e8f0; + color: #475569; + border: 1px solid rgba(0, 0, 0, 0.06); +} +[data-theme='light'] footer a[class*="footerSocialLink"]:hover { + background-color: #0891b2; + color: #ffffff; + border-color: transparent; + transform: translateY(-3px); + box-shadow: 0 6px 16px rgba(8, 145, 178, 0.3); +} diff --git a/packages/docusaurus-theme/package.json b/packages/docusaurus-theme/package.json index f59c710..6fe4694 100644 --- a/packages/docusaurus-theme/package.json +++ b/packages/docusaurus-theme/package.json @@ -51,13 +51,17 @@ "react-dom": "^18 || ^19" }, "dependencies": { + "@docsearch/css": "^3", "@docsearch/react": "^3", + "@types/three": "^0.183.1", "algoliasearch": "^5", "clsx": "^2.0.0", "instantsearch.js": "^4", "react-device-detect": "^2.2.3", "react-github-btn": "^1.4.0", - "react-instantsearch": "^7" + "react-instantsearch": "^7", + "three": "^0.134.0", + "vanta": "^0.5.24" }, "devDependencies": { "@docusaurus/core": "^3", @@ -67,6 +71,7 @@ "@types/js-yaml": "^4.0.9", "@types/react": "^18", "@types/react-dom": "^18", + "@types/three": "^0.183.1", "jest": "^30.0.4", "react": "^18", "react-dom": "^18", diff --git a/packages/docusaurus-theme/src/components/HeroBackground.tsx b/packages/docusaurus-theme/src/components/HeroBackground.tsx new file mode 100644 index 0000000..ac29d04 --- /dev/null +++ b/packages/docusaurus-theme/src/components/HeroBackground.tsx @@ -0,0 +1,276 @@ +import React, {useEffect, useRef} from 'react'; + +const PKT_GLOW = 'rgba(34,197,94,'; +const PKT_CORE = '#86efac'; + +function project(position: any, camera: any, W: number, H: number) { + const v = position.clone(); + v.project(camera); + return {x: (v.x + 1) / 2 * W, y: (-v.y + 1) / 2 * H}; +} + +function inBounds(p: {x:number;y:number}, W: number, H: number) { + return p.x >= 0 && p.x <= W && p.y >= 0 && p.y <= H; +} + +type PacketState = { + fromIdx: number; toIdx: number; speed: number; t: number; + phase: 'travel' | 'pulse' | 'wait'; + pulse: number; waitUntil: number; +}; + +export default function HeroBackground(): React.ReactElement { + const vantaRef = useRef(null); + const canvasRef = useRef(null); + const effectRef = useRef(null); + const pausedRef = useRef(false); + const mouseRef = useRef({x: -1, y: -1, active: false}); + const revealRef = useRef({x: -1, y: -1}); // smoothed reveal position + + useEffect(() => { + if (typeof window === 'undefined' || !vantaRef.current) return; + let cancelled = false; + let rafId: number; + + // Global mousemove so events fire regardless of what element is on top + const onMouseMove = (e: MouseEvent) => { + const canvas = canvasRef.current; + if (!canvas) return; + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + mouseRef.current = { + x, y, + active: x >= 0 && x <= rect.width && y >= 0 && y <= rect.height, + }; + }; + const onMouseLeave = () => { mouseRef.current.active = false; }; + window.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseleave', onMouseLeave); + + const onVisibility = () => { + pausedRef.current = document.hidden; + if (effectRef.current?.renderer) { + effectRef.current.renderer.setAnimationLoop( + document.hidden ? null : () => effectRef.current?.onUpdate?.() + ); + } + }; + document.addEventListener('visibilitychange', onVisibility); + + const observer = new IntersectionObserver( + ([entry]) => { + pausedRef.current = !entry.isIntersecting; + if (effectRef.current?.renderer) { + effectRef.current.renderer.setAnimationLoop( + entry.isIntersecting ? () => effectRef.current?.onUpdate?.() : null + ); + } + }, + {threshold: 0} + ); + if (vantaRef.current) observer.observe(vantaRef.current); + + Promise.all([ + import('three'), + import('vanta/dist/vanta.net.min'), + ]).then(([THREE, vantaMod]) => { + if (cancelled || !vantaRef.current) return; + if (effectRef.current) { effectRef.current.destroy(); effectRef.current = null; } + const VANTA = (vantaMod as any).default ?? vantaMod; + effectRef.current = VANTA({ + el: vantaRef.current, + THREE, + mouseControls: false, + touchControls: false, + gyroControls: false, + color: 0x22d3ee, + backgroundColor: 0x020617, + points: 7, + maxDistance: 26, + spacing: 22, + showDots: true, + speed: 0.8, + }); + + requestAnimationFrame(() => requestAnimationFrame(() => { + if (cancelled) return; + const canvas = canvasRef.current; + if (!canvas || !effectRef.current?.points?.length) return; + + const vPoints: any[] = effectRef.current.points; + const cam = effectRef.current.camera; + const W = canvas.clientWidth; + const H = canvas.clientHeight; + + // Only use edges well inside maxDistance so they stay connected as nodes drift + const SAFE_DIST = 18; + + // Pick a single fresh valid edge: both endpoints on-screen, firmly connected + const pickEdge = (): {a: number; b: number} | null => { + const cW2 = canvas.clientWidth; + const cH2 = canvas.clientHeight; + const candidates: {a:number; b:number; len:number}[] = []; + for (let i = 0; i < vPoints.length; i++) { + for (let j = i + 1; j < vPoints.length; j++) { + const dx = vPoints[i].position.x - vPoints[j].position.x; + const dy = vPoints[i].position.y - vPoints[j].position.y; + const dz = vPoints[i].position.z - vPoints[j].position.z; + if (Math.sqrt(dx*dx + dy*dy + dz*dz) > SAFE_DIST) continue; + const pa = project(vPoints[i].position, cam, cW2, cH2); + const pb = project(vPoints[j].position, cam, cW2, cH2); + if (!inBounds(pa, W, H) || !inBounds(pb, W, H)) continue; + const len = Math.sqrt((pa.x-pb.x)**2 + (pa.y-pb.y)**2); + if (len > 20) candidates.push({a: i, b: j, len}); // skip tiny hops + } + } + if (!candidates.length) return null; + candidates.sort((x, y) => y.len - x.len); + // Pick randomly from top half so we get variety + const pool = candidates.slice(0, Math.max(1, Math.floor(candidates.length * 0.5))); + return pool[Math.floor(Math.random() * pool.length)]; + }; + + const speeds = [0.22, 0.20, 0.25, 0.23, 0.21, 0.24, 0.19, 0.26]; + const startTs = [0.30, 0.70, 0.10, 0.55, 0.45, 0.80, 0.20, 0.62]; + const initialEdges = Array.from({length: 8}, () => pickEdge()); + const routes = initialEdges + .map((e, i) => e ? {fromIdx: e.a, toIdx: e.b, speed: speeds[i], startT: startTs[i]} : null) + .filter(Boolean) as {fromIdx:number; toIdx:number; speed:number; startT:number}[]; + + const packets: PacketState[] = routes.map(r => ({ + fromIdx: r.fromIdx, toIdx: r.toIdx, speed: r.speed, + t: r.startT, phase: 'travel', pulse: 0, waitUntil: 0, + })); + + let last = performance.now(); + + const tick = (now: number) => { + rafId = requestAnimationFrame(tick); + if (pausedRef.current) return; + + const dt = Math.min((now - last) / 1000, 0.05); + last = now; + + const dpr = window.devicePixelRatio || 1; + const cW = canvas.clientWidth; + const cH = canvas.clientHeight; + if (canvas.width !== cW * dpr || canvas.height !== cH * dpr) { + canvas.width = cW * dpr; + canvas.height = cH * dpr; + } + + const ctx = canvas.getContext('2d')!; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.clearRect(0, 0, cW, cH); + + // ── Flashlight reveal ──────────────────────────────────────────── + // Dark overlay — always present + ctx.fillStyle = 'rgba(2,6,23,0.85)'; + ctx.fillRect(0, 0, cW, cH); + + // Only cut the hole when the mouse is inside the hero + if (mouseRef.current.active) { + if (revealRef.current.x < 0) { + revealRef.current.x = mouseRef.current.x; + revealRef.current.y = mouseRef.current.y; + } + revealRef.current.x += (mouseRef.current.x - revealRef.current.x) * 0.1; + revealRef.current.y += (mouseRef.current.y - revealRef.current.y) * 0.1; + + ctx.globalCompositeOperation = 'destination-out'; + const hole = ctx.createRadialGradient( + revealRef.current.x, revealRef.current.y, 0, + revealRef.current.x, revealRef.current.y, 180 + ); + hole.addColorStop(0, 'rgba(0,0,0,1)'); + hole.addColorStop(0.6, 'rgba(0,0,0,0.85)'); + hole.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = hole; + ctx.fillRect(0, 0, cW, cH); + ctx.globalCompositeOperation = 'source-over'; + } + + // ── Packets drawn after overlay so destination-out doesn't erase them ── + // Reveal factor dims orbs outside the flashlight to match the mesh behavior. + const revealActive = mouseRef.current.active && revealRef.current.x >= 0; + const HOLE_R = 180; + const revealFactor = (px: number, py: number): number => { + if (!revealActive) return 0.28; + const d = Math.sqrt((px - revealRef.current.x) ** 2 + (py - revealRef.current.y) ** 2); + if (d < HOLE_R * 0.6) return 1; + if (d > HOLE_R) return 0.28; + const t = (d - HOLE_R * 0.6) / (HOLE_R * 0.4); + return 1 - t * 0.72; + }; + + for (const p of packets) { + const A = project(vPoints[p.fromIdx].position, cam, cW, cH); + const B = project(vPoints[p.toIdx].position, cam, cW, cH); + + if (p.phase === 'travel') { + p.t += p.speed * dt; + if (p.t >= 1) { p.t = 1; p.phase = 'pulse'; p.pulse = 0; } + const x = A.x + (B.x - A.x) * p.t; + const y = A.y + (B.y - A.y) * p.t; + const rf = revealFactor(x, y); + const g = ctx.createRadialGradient(x, y, 0, x, y, 7); + g.addColorStop(0, PKT_GLOW + (0.85 * rf).toFixed(2) + ')'); + g.addColorStop(1, PKT_GLOW + '0)'); + ctx.beginPath(); ctx.arc(x, y, 7, 0, Math.PI*2); + ctx.fillStyle = g; ctx.fill(); + ctx.globalAlpha = rf; + ctx.beginPath(); ctx.arc(x, y, 2, 0, Math.PI*2); + ctx.fillStyle = PKT_CORE; ctx.fill(); + ctx.globalAlpha = 1; + } + + if (p.phase === 'pulse') { + p.pulse += dt * 22; + const alpha = Math.max(0, 1 - p.pulse / 18); + const rf = revealFactor(B.x, B.y); + ctx.beginPath(); ctx.arc(B.x, B.y, p.pulse, 0, Math.PI*2); + ctx.strokeStyle = `rgba(34,197,94,${(alpha * rf).toFixed(2)})`; + ctx.lineWidth = 1.5; ctx.stroke(); + if (p.pulse >= 18) { + p.phase = 'wait'; + p.waitUntil = now + 700 + Math.random() * 600; + p.t = 0; + } + } + + if (p.phase === 'wait' && now >= p.waitUntil) { + const next = pickEdge(); + if (next) { p.fromIdx = next.a; p.toIdx = next.b; } + p.phase = 'travel'; p.t = 0; + } + } + + }; + + rafId = requestAnimationFrame(tick); + })); + }); + + return () => { + cancelled = true; + window.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseleave', onMouseLeave); + document.removeEventListener('visibilitychange', onVisibility); + observer.disconnect(); + if (rafId) cancelAnimationFrame(rafId); + if (effectRef.current) { effectRef.current.destroy(); effectRef.current = null; } + }; + }, []); + + return ( +
+
+
+ ); +} diff --git a/packages/docusaurus-theme/src/components/NetFoundryFooter/NetFoundryFooter.tsx b/packages/docusaurus-theme/src/components/NetFoundryFooter/NetFoundryFooter.tsx index 102de0c..f0f86e0 100644 --- a/packages/docusaurus-theme/src/components/NetFoundryFooter/NetFoundryFooter.tsx +++ b/packages/docusaurus-theme/src/components/NetFoundryFooter/NetFoundryFooter.tsx @@ -167,7 +167,7 @@ export function NetFoundryFooter(props: NetFoundryFooterProps) {
-

© 2025 NetFoundry Inc. OpenZiti is an open source project sponsored by NetFoundry. All rights reserved.

+

© 2026 NetFoundry Inc. OpenZiti is an open source project sponsored by NetFoundry. All rights reserved.

diff --git a/packages/docusaurus-theme/src/components/index.ts b/packages/docusaurus-theme/src/components/index.ts index 744424a..b66dafd 100644 --- a/packages/docusaurus-theme/src/components/index.ts +++ b/packages/docusaurus-theme/src/components/index.ts @@ -1,4 +1,5 @@ export * from './Alert' +export {default as HeroBackground} from './HeroBackground' export * from './CodeBlock'; export * from './Common'; export * from './NetFoundry' diff --git a/packages/docusaurus-theme/src/index.ts b/packages/docusaurus-theme/src/index.ts index b3d7eeb..359dce6 100644 --- a/packages/docusaurus-theme/src/index.ts +++ b/packages/docusaurus-theme/src/index.ts @@ -22,7 +22,9 @@ export default function themeNetFoundry( // Automatically inject CSS getClientModules() { const modules: string[] = [ + require.resolve('@docsearch/css'), require.resolve('../css/theme.css'), + require.resolve('../css/hero-background.css'), ]; // Add custom CSS if specified in options diff --git a/packages/docusaurus-theme/src/vanta.d.ts b/packages/docusaurus-theme/src/vanta.d.ts new file mode 100644 index 0000000..d5f2f92 --- /dev/null +++ b/packages/docusaurus-theme/src/vanta.d.ts @@ -0,0 +1 @@ +declare module 'vanta/dist/vanta.net.min'; diff --git a/packages/docusaurus-theme/theme/NavbarItem/ComponentTypes.tsx b/packages/docusaurus-theme/theme/NavbarItem/ComponentTypes.tsx index 0053ff0..8a670f4 100644 --- a/packages/docusaurus-theme/theme/NavbarItem/ComponentTypes.tsx +++ b/packages/docusaurus-theme/theme/NavbarItem/ComponentTypes.tsx @@ -1,4 +1,5 @@ import ProductPicker from './types/ProductPicker'; +import ResourcesPicker from './types/ResourcesPicker'; // @theme-original resolves to OUR OWN file in a plugin theme (Docusaurus sets // both @theme and @theme-original to the plugin file). @theme-init resolves to @@ -10,5 +11,6 @@ const ComponentTypesOrig = require('@theme-init/NavbarItem/ComponentTypes').defa export default { ...ComponentTypesOrig, - 'custom-productPicker': ProductPicker, + 'custom-productPicker': ProductPicker, + 'custom-resourcesPicker': ResourcesPicker, }; diff --git a/packages/docusaurus-theme/theme/NavbarItem/types/ProductPicker/index.tsx b/packages/docusaurus-theme/theme/NavbarItem/types/ProductPicker/index.tsx index 6e26792..920cf33 100644 --- a/packages/docusaurus-theme/theme/NavbarItem/types/ProductPicker/index.tsx +++ b/packages/docusaurus-theme/theme/NavbarItem/types/ProductPicker/index.tsx @@ -1,156 +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}} -
- - ))} -
- ))} -
-
- )} -
- ); -} +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-tertiary', 'picker-header--nf-secondary', 'picker-header--nf-primary']; +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/NavbarItem/types/ResourcesPicker/index.tsx b/packages/docusaurus-theme/theme/NavbarItem/types/ResourcesPicker/index.tsx new file mode 100644 index 0000000..264baae --- /dev/null +++ b/packages/docusaurus-theme/theme/NavbarItem/types/ResourcesPicker/index.tsx @@ -0,0 +1,132 @@ +import React, {useState, useRef, useEffect, useCallback} from 'react'; +import Link from '@docusaurus/Link'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import {useThemeConfig} from '@docusaurus/theme-common'; +import clsx from 'clsx'; + +const NF_LOGO_DEFAULT = 'https://raw.githubusercontent.com/netfoundry/branding/refs/heads/main/images/svg/icon/netfoundry-icon-color.svg'; + +const YOUTUBE_ICON = ``; +const DISCOURSE_ICON = ``; + +type Props = { + label?: string; + position?: 'left' | 'right'; + className?: string; +}; + +export default function ResourcesPicker({label = 'Resources', className}: Props) { + const themeConfig = useThemeConfig() as any; + const consoleLogo = themeConfig?.netfoundry?.consoleLogo ?? NF_LOGO_DEFAULT; + const openzitiLogo = useBaseUrl('/img/openziti-sm-logo.svg'); + + 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]); + + // Close when another 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]); + + const handlePanelEnter = useCallback(() => { + hasEnteredPanel.current = true; + }, []); + + const handlePanelLeave = useCallback(() => { + if (hasEnteredPanel.current) close(); + }, [close]); + + const columns = [ + { + header: 'Learn & Engage', + headerClass: 'picker-header--nf-tertiary', + links: [ + { label: 'NetFoundry Blog', description: 'Latest news, updates, and insights from NetFoundry.', href: 'https://netfoundry.io/blog/', logoSrc: consoleLogo }, + { label: 'OpenZiti Tech Blog', description: 'Technical articles and community updates.', href: 'https://blog.openziti.io/', logoSrc: openzitiLogo }, + ], + }, + { + header: 'Community & Support', + headerClass: 'picker-header--nf-secondary', + links: [ + { label: 'NetFoundry YouTube', description: 'Video tutorials, demos, and technical deep dives.', href: 'https://www.youtube.com/c/NetFoundry', svgIcon: YOUTUBE_ICON }, + { label: 'OpenZiti Discourse', description: 'Ask questions and connect with the community.', href: 'https://openziti.discourse.group/', svgIcon: DISCOURSE_ICON }, + ], + }, + ]; + + return ( +
+ {}} + onClick={e => { e.preventDefault(); setOpen(o => !o); }} + onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); setOpen(o => !o); } }}> + {label} + + {open && ( +
e.stopPropagation()} + onMouseEnter={handlePanelEnter} + onMouseLeave={handlePanelLeave}> +
+ {columns.map((col, i) => ( +
+ {col.header} + {col.links.map((link, j) => ( + + {'logoSrc' in link + ? + : + } +
+ {link.label} + {link.description} +
+ + ))} +
+ ))} +
+
+ )} +
+ ); +} diff --git a/packages/test-site/docusaurus.config.ts b/packages/test-site/docusaurus.config.ts index 4661032..551efdb 100644 --- a/packages/test-site/docusaurus.config.ts +++ b/packages/test-site/docusaurus.config.ts @@ -176,38 +176,50 @@ export default { { label: 'zLAN', to: '/docs/zlan', - logo: 'https://netfoundry.io/docs/img/zlan-logo.svg', + logo: '/img/zlan-logo.svg', description: 'Zero-trust access for OT networks.', }, ], }, ], footer: { - description: 'This is just a test site for the NetFoundry Docusaurus theme.', + description: 'Secure, high-performance networking for the modern era.', + copyright: `Copyright © 2026 NetFoundry Inc.`, socialProps: { githubUrl: 'https://github.com/netfoundry/', youtubeUrl: 'https://youtube.com/netfoundry/', linkedInUrl: 'https://www.linkedin.com/company/netfoundry/', - twitterUrl: 'https://twitter.com/netfoundry/', + twitterUrl: 'https://x.com/netfoundry/', }, + documentationLinks: [ + {href: '/docs/learn/quickstarts/services/ztha', label: 'Get started'}, + {href: '/docs/reference/developer/api/', label: 'API reference'}, + {href: '/docs/reference/developer/sdk/', label: 'SDK integration'}, + ], + communityLinks: [ + {href: 'https://github.com/openziti/ziti', label: 'GitHub'}, + {href: 'https://openziti.discourse.group/', label: 'OpenZiti Discourse'}, + {href: '/docs/openziti/policies/CONTRIBUTING', label: 'Contribute'}, + ], + resourceLinks: [ + {href: 'https://netfoundry.io/', label: 'NetFoundry'}, + {href: 'https://netfoundry.io/blog/', label: 'NetFoundry Tech Blog'}, + {href: 'https://blog.openziti.io', label: 'OpenZiti Tech Blog'}, + ], }, }, // 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', + title: 'NetFoundry Docs', logo: { alt: 'NetFoundry Logo', src: 'https://raw.githubusercontent.com/netfoundry/branding/refs/heads/main/images/svg/icon/netfoundry-icon-color.svg', }, items: [ - { type: 'custom-productPicker', position: 'left' }, - { - to: '/docs', - label: 'Main Docs', - position: 'left', - }, + { type: 'custom-productPicker', position: 'left' }, + { type: 'custom-resourcesPicker', position: 'left' }, ], }, prism: { diff --git a/packages/test-site/package.json b/packages/test-site/package.json index 815e9be..79b28b4 100644 --- a/packages/test-site/package.json +++ b/packages/test-site/package.json @@ -18,6 +18,7 @@ "@docusaurus/preset-classic": "3.9.2", "@docusaurus/theme-common": "3.9.2", "@docusaurus/theme-mermaid": "^3.9.2", + "@docusaurus/theme-search-algolia": "3.9.2", "@docusaurus/utils": "3.9.2", "@mdx-js/react": "^3.0.0", "@netfoundry/docusaurus-theme": "^0.1.2", diff --git a/packages/test-site/src/pages/index.tsx b/packages/test-site/src/pages/index.tsx index 8c4367f..1cfa17c 100644 --- a/packages/test-site/src/pages/index.tsx +++ b/packages/test-site/src/pages/index.tsx @@ -3,6 +3,7 @@ import Layout from '@theme/Layout'; import Link from '@docusaurus/Link'; import clsx from 'clsx'; import styles from './landing.module.css'; +import {HeroBackground} from '@netfoundry/docusaurus-theme/ui'; const CYAN = '#22d3ee'; const GREEN = '#22c55e'; @@ -15,7 +16,7 @@ const products = [ { 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.' }, + { 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]; @@ -47,12 +48,13 @@ export default function Home(): JSX.Element { return (
+

NetFoundry Docs

-

Secure, high-performance networking for the modern era.

+

Secure your workloads with Identity-First Connectivity™

Get Started - Request Demo + Request Demo
diff --git a/packages/test-site/src/pages/landing.module.css b/packages/test-site/src/pages/landing.module.css index f7a749b..8b5828f 100644 --- a/packages/test-site/src/pages/landing.module.css +++ b/packages/test-site/src/pages/landing.module.css @@ -1,160 +1,166 @@ -.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; } -} +.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.75), 0 2px 12px rgba(0, 0, 0, 0.9); +} +.nf-green-text { + color: #22c55e; + font-family: inherit; font-weight: inherit; font-size: inherit; + text-shadow: 0 0 20px rgba(34, 197, 94, 0.75), 0 2px 12px rgba(0, 0, 0, 0.9); +} +.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; text-decoration: none; +} +.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; text-decoration: none; +} + +:global([data-theme='dark']) .nf-btn-primary, +:global([data-theme='dark']) .nf-btn-primary:hover, +:global([data-theme='dark']) .nf-btn-ghost, +:global([data-theme='dark']) .nf-btn-ghost:hover { 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; text-decoration: none; + 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); +} +.nf-bento-card:hover * { text-decoration: none; } +: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/packages/test-site/static/img/openziti-sm-logo.svg b/packages/test-site/static/img/openziti-sm-logo.svg new file mode 100644 index 0000000..d038e67 --- /dev/null +++ b/packages/test-site/static/img/openziti-sm-logo.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + Sheet.181 + + Page-1 + + Sheet.183 + + Sheet.184 + + + + + Lightning_Crashes + + Sheet.186 + + + + + Sheet.187 + + Sheet.188 + + Sheet.189 + + + + + + + \ No newline at end of file diff --git a/packages/test-site/static/img/zlan-logo.svg b/packages/test-site/static/img/zlan-logo.svg new file mode 100644 index 0000000..462d42d --- /dev/null +++ b/packages/test-site/static/img/zlan-logo.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/unified-doc/build-docs.sh b/unified-doc/build-docs.sh index 1960f74..db2fb4c 100755 --- a/unified-doc/build-docs.sh +++ b/unified-doc/build-docs.sh @@ -321,10 +321,6 @@ mkdir -p "${SDK_ROOT_TARGET}" # -d = skip docusaurus build (unified-doc does its own build) "${script_dir}/_remotes/openziti/gendoc.sh" -d "${OTHER_FLAGS[@]}" -# --- GENERATE SELFHOSTED CHANGELOG --- -echo "Generating selfhosted changelog from CHANGELOG file..." -node "${script_dir}/_remotes/selfhosted/docusaurus/scripts/generate-changelog.mjs" - # --- DOCUSAURUS BUILD --- pushd "${script_dir}" >/dev/null yarn install diff --git a/unified-doc/src/pages/index.tsx b/unified-doc/src/pages/index.tsx index ca6cb1f..081a08d 100644 --- a/unified-doc/src/pages/index.tsx +++ b/unified-doc/src/pages/index.tsx @@ -46,7 +46,7 @@ const products = [ logo: `${IMG}/zrok-1.0.0-rocket-purple.svg`, tag: 'Open Source', accent: GREEN, - link: `${DOCS_BASE}zrok/getting-started`, + link: `${DOCS_BASE}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.' }, { diff --git a/yarn.lock b/yarn.lock index c674eb1..bd2e7f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1633,6 +1633,11 @@ resolved "https://registry.yarnpkg.com/@csstools/utilities/-/utilities-2.0.0.tgz#f7ff0fee38c9ffb5646d47b6906e0bc8868bde60" integrity sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ== +"@dimforge/rapier3d-compat@~0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz#7b3365e1dfdc5cd957b45afe920b4ac06c7cd389" + integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow== + "@discoveryjs/json-ext@0.5.7": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" @@ -1643,7 +1648,7 @@ resolved "https://registry.yarnpkg.com/@docsearch/core/-/core-4.5.3.tgz#b64b7855348882ba4789e6a28a51764abb1ff9a1" integrity sha512-x/P5+HVzv9ALtbuJIfpkF8Eyc5RE8YCsFcOgLrrtWa9Ui+53ggZA5seIAanCRORbS4+m982lu7rZmebSiuMIcw== -"@docsearch/css@3.9.0": +"@docsearch/css@3.9.0", "@docsearch/css@^3": version "3.9.0" resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.9.0.tgz#3bc29c96bf024350d73b0cfb7c2a7b71bf251cd5" integrity sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA== @@ -3191,6 +3196,11 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== +"@tweenjs/tween.js@~23.1.3": + version "23.1.3" + resolved "https://registry.yarnpkg.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz#eff0245735c04a928bb19c026b58c2a56460539d" + integrity sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA== + "@tybys/wasm-util@^0.10.0": version "0.10.1" resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414" @@ -3796,11 +3806,29 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/stats.js@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/stats.js/-/stats.js-0.17.4.tgz#1933e5ff153a23c7664487833198d685c22e791e" + integrity sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA== + "@types/stylis@4.2.7": version "4.2.7" resolved "https://registry.yarnpkg.com/@types/stylis/-/stylis-4.2.7.tgz#1813190525da9d2a2b6976583bdd4af5301d9fd4" integrity sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA== +"@types/three@^0.183.1": + version "0.183.1" + resolved "https://registry.yarnpkg.com/@types/three/-/three-0.183.1.tgz#d812d028b38ad68843725e3e7bd3268607cef150" + integrity sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw== + dependencies: + "@dimforge/rapier3d-compat" "~0.12.0" + "@tweenjs/tween.js" "~23.1.3" + "@types/stats.js" "*" + "@types/webxr" ">=0.5.17" + "@webgpu/types" "*" + fflate "~0.8.2" + meshoptimizer "~1.0.1" + "@types/trusted-types@^2.0.7": version "2.0.7" resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" @@ -3816,6 +3844,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== +"@types/webxr@>=0.5.17": + version "0.5.24" + resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.24.tgz#734d5d90dadc5809a53e422726c60337fa2f4a44" + integrity sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg== + "@types/ws@^8.5.10": version "8.18.1" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" @@ -4063,6 +4096,11 @@ "@webassemblyjs/ast" "1.14.1" "@xtuc/long" "4.2.2" +"@webgpu/types@*": + version "0.1.69" + resolved "https://registry.yarnpkg.com/@webgpu/types/-/types-0.1.69.tgz#6b849bf370a1f29c78bd3aeba8e84c1150b237f2" + integrity sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ== + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -6319,6 +6357,11 @@ feed@^4.2.2: dependencies: xml-js "^1.6.11" +fflate@~0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" + integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== + figures@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -8420,6 +8463,11 @@ mermaid@>=11.6.0: ts-dedent "^2.2.0" uuid "^11.1.0" +meshoptimizer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/meshoptimizer/-/meshoptimizer-1.0.1.tgz#c3ef0d509a8b84ac562493dba5a108fd67fa76dc" + integrity sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g== + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -11603,6 +11651,11 @@ thingies@^2.5.0: resolved "https://registry.yarnpkg.com/thingies/-/thingies-2.5.0.tgz#5f7b882c933b85989f8466b528a6247a6881e04f" integrity sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw== +three@^0.134.0: + version "0.134.0" + resolved "https://registry.yarnpkg.com/three/-/three-0.134.0.tgz#d7ad4d85d050da0861bf39749b06ddfb5f17157f" + integrity sha512-LbBerg7GaSPjYtTOnu41AMp7tV6efUNR3p4Wk5NzkSsNTBuA5mDGOfwwZL1jhhVMLx9V20HolIUo0+U3AXehbg== + thunky@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" @@ -11980,6 +12033,11 @@ value-equal@^1.0.1: resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== +vanta@^0.5.24: + version "0.5.24" + resolved "https://registry.yarnpkg.com/vanta/-/vanta-0.5.24.tgz#46ec96b0b88cdcc57b7cee383e1d5f74bd3d4fb3" + integrity sha512-fvieEbHy1ZS23zrcX+topzqAgA4Uct1enngOEWLFBgs9TtOf6RDFOYatH7KSVdrABzQDMCQ5myQy+nTSZZwLzg== + vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"