Skip to content

Commit

Permalink
Add HiveLayout (#1995)
Browse files Browse the repository at this point in the history
  • Loading branch information
hasparus authored Jan 21, 2025
1 parent 1b10874 commit 6cd1c51
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/slimy-elephants-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@theguild/components': minor
---

Add HiveLayout and HiveLayoutConfig. Tweak HiveNavigation and HiveFooter.
4 changes: 2 additions & 2 deletions packages/components/src/components/anchor.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { forwardRef, ReactElement } from 'react';
import NextLink from 'next/link';
import clsx from 'clsx';
import { cn } from '../cn';
import { ILink } from '../types/components';

export type AnchorProps = ILink;
export const Anchor = forwardRef<HTMLAnchorElement, AnchorProps>(function Anchor(
{ href = '', children, newWindow, className, ...props },
forwardedRef,
): ReactElement {
const classes = clsx(className, 'outline-none transition focus-visible:ring');
const classes = cn('outline-none focus-visible:ring', className);

if (typeof href === 'string') {
if (href.startsWith('#')) {
Expand Down
23 changes: 21 additions & 2 deletions packages/components/src/components/hive-footer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FOUR_MAIN_PRODUCTS, SIX_HIGHLIGHTED_PRODUCTS } from '../../products';
import { ILink } from '../../types/components';
import { Anchor } from '../anchor';
import { ContactTextLink } from '../contact-us';
import { __LANDING_WIDTHS_ID } from '../hive-layout-config';
import {
CSAStarLevelOneIcon,
DiscordIcon,
Expand All @@ -15,6 +16,17 @@ import {
YouTubeIcon,
} from '../icons/index';

const INNER_BOX_WIDTH_STYLE =
'max-w-[90rem] [body:has(#hive-l-widths)_&]:max-w-[75rem] [body:has(#hive-l-widths)_&]:mx-4';

if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line no-console
console.assert(
__LANDING_WIDTHS_ID === 'hive-l-widths',
'__LANDING_WIDTHS_ID diverged from the className used in HiveFooter.',
);
}

export type HiveFooterProps = {
className?: string;
logo?: ReactNode;
Expand All @@ -33,8 +45,15 @@ export function HiveFooter({
items = { ...HiveFooter.DEFAULT_ITEMS, ...items };

return (
<footer className={cn('relative flex justify-center px-4 py-6 xl:px-[120px]', className)}>
<div className="mx-4 grid w-full max-w-[75rem] grid-cols-1 gap-x-6 text-green-800 max-lg:gap-y-16 sm:grid-cols-4 lg:gap-x-8 xl:gap-x-10 dark:text-neutral-400">
<footer
className={cn('relative flex justify-center px-4 pb-6 pt-[72px] xl:px-[120px]', className)}
>
<div
className={cn(
'grid w-full grid-cols-1 gap-x-6 text-green-800 max-lg:gap-y-16 sm:grid-cols-4 lg:gap-x-8 xl:gap-x-10 dark:text-neutral-400',
INNER_BOX_WIDTH_STYLE,
)}
>
<div className="max-lg:col-span-full">
<Anchor
href={href}
Expand Down
15 changes: 15 additions & 0 deletions packages/components/src/components/hive-layout-config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @internal Don't expose this to websites.
*/
export const __LANDING_WIDTHS_ID = 'hive-l-widths';

export interface HiveLayoutConfigProps {
widths: 'landing-narrow' | 'docs-wide';
}

/**
* @see {@link HiveLayout} from `@theguild/components/server` for documentation.
*/
export function HiveLayoutConfig({ widths }: HiveLayoutConfigProps) {
return widths === 'landing-narrow' ? <div id={__LANDING_WIDTHS_ID} /> : null;
}
20 changes: 16 additions & 4 deletions packages/components/src/components/hive-navigation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { GraphQLFoundationLogo, GuildLogo, HiveCombinationMark, TheGuild } from
import { PRODUCTS, SIX_HIGHLIGHTED_PRODUCTS } from '../../products';
import { Anchor } from '../anchor';
import { CallToAction } from '../call-to-action';
import { __LANDING_WIDTHS_ID } from '../hive-layout-config';
import {
AccountBox,
AppsIcon,
Expand All @@ -45,6 +46,16 @@ export * from './graphql-conf-card';

const ENTERPRISE_MENU_HIDDEN = true;

const WIDTH_STYLE = 'max-w-[90rem] [body:has(#hive-l-widths)_&]:max-w-[1392px]';

if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line no-console
console.assert(
__LANDING_WIDTHS_ID === 'hive-l-widths',
'__LANDING_WIDTHS_ID diverged from the className used in HiveNavigation.',
);
}

export type HiveNavigationProps = {
companyMenuChildren?: ReactNode;
children?: ReactNode;
Expand All @@ -57,7 +68,6 @@ export type HiveNavigationProps = {
navLinks?: { href: string; children: ReactNode }[];
developerMenu: DeveloperMenuProps['developerMenu'];
search?: ReactElement;
searchProps?: ComponentProps<typeof Search>;
};

/**
Expand Down Expand Up @@ -85,16 +95,18 @@ export function HiveNavigation({
},
],
developerMenu,
search = <Search />,
// we need the background transition disabled to avoid an unpleasant flicker
// when navigating from a forced light mode page to a dark mode page
search = <Search className="[&_input]:transition-none" />,
}: HiveNavigationProps) {
const containerRef = useRef<HTMLDivElement>(null!);

return (
<div
ref={containerRef}
className={cn(
'sticky top-0 z-20 border-b border-beige-400/[var(--border-opacity)] bg-[rgb(var(--nextra-bg))] px-6 py-4 text-green-1000 transition-[border-color] duration-500 md:mb-[7px] md:mt-2 dark:border-neutral-700/[var(--border-opacity)] dark:text-neutral-200 [&.light]:border-beige-400/[var(--border-opacity)] [&.light]:bg-white [&.light]:text-green-1000',
className?.includes('light') && 'light',
'sticky top-0 z-20 border-b border-beige-400/[var(--border-opacity)] bg-[rgb(var(--nextra-bg))] px-6 py-4 text-green-1000 transition-[border-color] duration-500 md:mb-[7px] md:mt-2 dark:border-neutral-700/[var(--border-opacity)] dark:text-neutral-200',
WIDTH_STYLE,
)}
style={{ '--border-opacity': 0 }}
>
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ export * from './version-dropdown';
export * from './dropdown';
export { FrequentlyAskedQuestions } from './faq';
export { ComparisonTable } from './comparison-table';
export { HiveLayoutConfig } from './hive-layout-config';
22 changes: 14 additions & 8 deletions packages/components/src/server/body.client.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
'use client';

import { FC, ReactNode } from 'react';
import { DetailedHTMLProps, FC, HtmlHTMLAttributes } from 'react';
import { usePathname } from 'next/navigation';
import { cn } from '../cn';

export const Body: FC<{
lightOnlyPages: string[];
children: ReactNode;
}> = ({ lightOnlyPages, children }) => {
const pathname = usePathname();
export interface BodyProps
extends DetailedHTMLProps<HtmlHTMLAttributes<HTMLBodyElement>, HTMLBodyElement> {
lightOnlyPages?: string[];
}

const isLightOnlyPage = lightOnlyPages.includes(pathname);
export const Body: FC<BodyProps> = ({ lightOnlyPages, children, className, ...rest }) => {
const pathname = usePathname();
const isLightOnlyPage = lightOnlyPages?.includes(pathname);

return <body className={isLightOnlyPage ? 'light text-green-1000' : undefined}>{children}</body>;
return (
<body className={cn(className, isLightOnlyPage && 'light text-green-1000')} {...rest}>
{children}
</body>
);
};
144 changes: 144 additions & 0 deletions packages/components/src/server/hive-layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { DetailedHTMLProps, HtmlHTMLAttributes, ReactElement, ReactNode } from 'react';
import { Layout } from 'nextra-theme-docs';
import { Head } from 'nextra/components';
import { getPageMap } from 'nextra/page-map';
import { cn } from '../cn';
import { Body } from './body.client';

export interface HiveLayoutProps
extends DetailedHTMLProps<HtmlHTMLAttributes<HTMLHtmlElement>, HTMLHtmlElement> {
children: ReactNode;
head: ReactNode;
navbar: ReactElement;
footer: ReactElement;
fontFamily: string;
lightOnlyPages: string[];
bodyProps?: DetailedHTMLProps<HtmlHTMLAttributes<HTMLBodyElement>, HTMLBodyElement>;
docsRepositoryBase: string;
}

/**
* Alternative to `GuildLayout` for Hive-branded websites.
*
* Accepts navbar and footer as slots/children props, because they're highly customizable,
* and their defaults belong to HiveNavigation and HiveFooter component default props.
*
* ## Configuration
*
* Pages can differ by widths and supported color schemes:
*
* - The footer in docs has 90rem width, in landing pages it has 75rem.
* - The navbar in docs has 90rem width, in landing pages it has 1392px.
* - Landing pages only support light mode for _business and prioritization reasons_.
*
* TODO: Consider unifying this in design phase.
*
* For now, a page or a layout can configue these as follows:
*
* ### Light-only pages
*
* @example
* ```tsx
* <HiveLayout bodyProps={{ lightOnlyPages: ['/', '/friends'] }} />
* ```
*
* This will force light theme to the pages with paths `/` and `/friends`,
* by adding `.light` class to the <body /> element.
*
* ### Landing page widths
*
* @example
* ```tsx
* import { HiveLayoutConfig } from '@theguild/components'
*
* <HiveLayoutConfig widths="landing-narrow" />
* ```
*/
export const HiveLayout = async ({
children,
head,
navbar,
footer,
className,
fontFamily,
lightOnlyPages,
bodyProps,
docsRepositoryBase,
...rest
}: HiveLayoutProps) => {
const pageMap = await getPageMap();
return (
<html
lang="en"
// Required to be set for `nextra-theme-docs` styles
dir="ltr"
// Suggested by `next-themes` package https://github.com/pacocoursey/next-themes#with-app
suppressHydrationWarning
className={cn('font-sans', className)}
{...rest}
>
<Head>
<style>{
/* css */ `
:root {
--font-sans: ${fontFamily};
}
:root.dark {
--nextra-primary-hue: 67.1deg;
--nextra-primary-saturation: 100%;
--nextra-primary-lightness: 55%;
--nextra-bg: 17, 17, 17;
}
:root.dark *::selection {
background-color: hsl(191deg 95% 72% / 0.25)
}
:root.light, :root.dark:has(body.light) {
--nextra-primary-hue: 191deg;
--nextra-primary-saturation: 40%;
--nextra-bg: 255, 255, 255;
}
.x\\:tracking-tight,
.nextra-steps :is(h2, h3, h4) {
letter-spacing: normal;
}
html:has(body.light) {
scroll-behavior: smooth;
background: #fff;
color-scheme: light !important;
}
html:has(body.light) .nextra-search-results mark {
background: oklch(0.611752 0.07807 214.47 / 0.8);
}
html:has(body.light) .nextra-sidebar-footer {
display: none;
}
#crisp-chatbox { z-index: 40 !important; }
`
}</style>
{head}
</Head>
<Body lightOnlyPages={lightOnlyPages} {...bodyProps}>
<Layout
editLink="Edit this page on GitHub"
docsRepositoryBase={docsRepositoryBase}
pageMap={pageMap}
feedback={{
labels: 'kind/docs',
}}
sidebar={{
defaultMenuCollapseLevel: 1,
}}
navbar={navbar}
footer={footer}
>
{children}
</Layout>
</Body>
</html>
);
};
3 changes: 2 additions & 1 deletion packages/components/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export {
export { evaluate } from 'nextra/evaluate';
export { fetchPackageInfo } from './npm.js';
export { sharedMetaItems } from './shared-meta-items.js';
export { Body } from './body.client.js';
export * from './body.client.js';
export { remarkLinkRewrite } from './remark-link-rewrite.js';

/**
Expand All @@ -26,3 +26,4 @@ export { remarkLinkRewrite } from './remark-link-rewrite.js';
* which is disallowed. Either remove the export, or the "use client" directive. Read more: https://nextjs.org
*/
export { GuildLayout, getDefaultMetadata } from './theme-layout.js';
export { HiveLayout } from './hive-layout.js';
6 changes: 2 additions & 4 deletions website/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ReactNode } from 'react';
import { getDefaultMetadata, GuildLayout } from '@theguild/components/server';
import '@theguild/components/style.css';
import { GitHubIcon, PaperIcon, PencilIcon } from '@theguild/components';
import { GitHubIcon, PaperIcon, PencilIcon, Search } from '@theguild/components';

const description = 'Documentation for The Guild';
const websiteName = 'Guild Docs';
Expand Down Expand Up @@ -35,9 +35,6 @@ const RootLayout = async ({ children }: { children: ReactNode }) => {
}}
navbarProps={{
navLinks: [{ href: '/docs', children: 'Documentation' }],
searchProps: {
placeholder: 'Search...',
},
developerMenu: [
{
href: '/docs',
Expand All @@ -55,6 +52,7 @@ const RootLayout = async ({ children }: { children: ReactNode }) => {
children: 'GitHub',
},
],
search: <Search placeholder="Search..." className="[&_input]:transition-none" />,
}}
lightOnlyPages={['/']}
>
Expand Down

0 comments on commit 6cd1c51

Please sign in to comment.