Skip to content

Commit 6cd1c51

Browse files
authored
Add HiveLayout (#1995)
1 parent 1b10874 commit 6cd1c51

File tree

10 files changed

+222
-21
lines changed

10 files changed

+222
-21
lines changed

.changeset/slimy-elephants-chew.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@theguild/components': minor
3+
---
4+
5+
Add HiveLayout and HiveLayoutConfig. Tweak HiveNavigation and HiveFooter.

packages/components/src/components/anchor.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { forwardRef, ReactElement } from 'react';
22
import NextLink from 'next/link';
3-
import clsx from 'clsx';
3+
import { cn } from '../cn';
44
import { ILink } from '../types/components';
55

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

1313
if (typeof href === 'string') {
1414
if (href.startsWith('#')) {

packages/components/src/components/hive-footer/index.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { FOUR_MAIN_PRODUCTS, SIX_HIGHLIGHTED_PRODUCTS } from '../../products';
66
import { ILink } from '../../types/components';
77
import { Anchor } from '../anchor';
88
import { ContactTextLink } from '../contact-us';
9+
import { __LANDING_WIDTHS_ID } from '../hive-layout-config';
910
import {
1011
CSAStarLevelOneIcon,
1112
DiscordIcon,
@@ -15,6 +16,17 @@ import {
1516
YouTubeIcon,
1617
} from '../icons/index';
1718

19+
const INNER_BOX_WIDTH_STYLE =
20+
'max-w-[90rem] [body:has(#hive-l-widths)_&]:max-w-[75rem] [body:has(#hive-l-widths)_&]:mx-4';
21+
22+
if (process.env.NODE_ENV === 'development') {
23+
// eslint-disable-next-line no-console
24+
console.assert(
25+
__LANDING_WIDTHS_ID === 'hive-l-widths',
26+
'__LANDING_WIDTHS_ID diverged from the className used in HiveFooter.',
27+
);
28+
}
29+
1830
export type HiveFooterProps = {
1931
className?: string;
2032
logo?: ReactNode;
@@ -33,8 +45,15 @@ export function HiveFooter({
3345
items = { ...HiveFooter.DEFAULT_ITEMS, ...items };
3446

3547
return (
36-
<footer className={cn('relative flex justify-center px-4 py-6 xl:px-[120px]', className)}>
37-
<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">
48+
<footer
49+
className={cn('relative flex justify-center px-4 pb-6 pt-[72px] xl:px-[120px]', className)}
50+
>
51+
<div
52+
className={cn(
53+
'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',
54+
INNER_BOX_WIDTH_STYLE,
55+
)}
56+
>
3857
<div className="max-lg:col-span-full">
3958
<Anchor
4059
href={href}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @internal Don't expose this to websites.
3+
*/
4+
export const __LANDING_WIDTHS_ID = 'hive-l-widths';
5+
6+
export interface HiveLayoutConfigProps {
7+
widths: 'landing-narrow' | 'docs-wide';
8+
}
9+
10+
/**
11+
* @see {@link HiveLayout} from `@theguild/components/server` for documentation.
12+
*/
13+
export function HiveLayoutConfig({ widths }: HiveLayoutConfigProps) {
14+
return widths === 'landing-narrow' ? <div id={__LANDING_WIDTHS_ID} /> : null;
15+
}

packages/components/src/components/hive-navigation/index.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { GraphQLFoundationLogo, GuildLogo, HiveCombinationMark, TheGuild } from
2222
import { PRODUCTS, SIX_HIGHLIGHTED_PRODUCTS } from '../../products';
2323
import { Anchor } from '../anchor';
2424
import { CallToAction } from '../call-to-action';
25+
import { __LANDING_WIDTHS_ID } from '../hive-layout-config';
2526
import {
2627
AccountBox,
2728
AppsIcon,
@@ -45,6 +46,16 @@ export * from './graphql-conf-card';
4546

4647
const ENTERPRISE_MENU_HIDDEN = true;
4748

49+
const WIDTH_STYLE = 'max-w-[90rem] [body:has(#hive-l-widths)_&]:max-w-[1392px]';
50+
51+
if (process.env.NODE_ENV === 'development') {
52+
// eslint-disable-next-line no-console
53+
console.assert(
54+
__LANDING_WIDTHS_ID === 'hive-l-widths',
55+
'__LANDING_WIDTHS_ID diverged from the className used in HiveNavigation.',
56+
);
57+
}
58+
4859
export type HiveNavigationProps = {
4960
companyMenuChildren?: ReactNode;
5061
children?: ReactNode;
@@ -57,7 +68,6 @@ export type HiveNavigationProps = {
5768
navLinks?: { href: string; children: ReactNode }[];
5869
developerMenu: DeveloperMenuProps['developerMenu'];
5970
search?: ReactElement;
60-
searchProps?: ComponentProps<typeof Search>;
6171
};
6272

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

92104
return (
93105
<div
94106
ref={containerRef}
95107
className={cn(
96-
'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',
97-
className?.includes('light') && 'light',
108+
'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',
109+
WIDTH_STYLE,
98110
)}
99111
style={{ '--border-opacity': 0 }}
100112
>

packages/components/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ export * from './version-dropdown';
3535
export * from './dropdown';
3636
export { FrequentlyAskedQuestions } from './faq';
3737
export { ComparisonTable } from './comparison-table';
38+
export { HiveLayoutConfig } from './hive-layout-config';
Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
'use client';
22

3-
import { FC, ReactNode } from 'react';
3+
import { DetailedHTMLProps, FC, HtmlHTMLAttributes } from 'react';
44
import { usePathname } from 'next/navigation';
5+
import { cn } from '../cn';
56

6-
export const Body: FC<{
7-
lightOnlyPages: string[];
8-
children: ReactNode;
9-
}> = ({ lightOnlyPages, children }) => {
10-
const pathname = usePathname();
7+
export interface BodyProps
8+
extends DetailedHTMLProps<HtmlHTMLAttributes<HTMLBodyElement>, HTMLBodyElement> {
9+
lightOnlyPages?: string[];
10+
}
1111

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

14-
return <body className={isLightOnlyPage ? 'light text-green-1000' : undefined}>{children}</body>;
16+
return (
17+
<body className={cn(className, isLightOnlyPage && 'light text-green-1000')} {...rest}>
18+
{children}
19+
</body>
20+
);
1521
};
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { DetailedHTMLProps, HtmlHTMLAttributes, ReactElement, ReactNode } from 'react';
2+
import { Layout } from 'nextra-theme-docs';
3+
import { Head } from 'nextra/components';
4+
import { getPageMap } from 'nextra/page-map';
5+
import { cn } from '../cn';
6+
import { Body } from './body.client';
7+
8+
export interface HiveLayoutProps
9+
extends DetailedHTMLProps<HtmlHTMLAttributes<HTMLHtmlElement>, HTMLHtmlElement> {
10+
children: ReactNode;
11+
head: ReactNode;
12+
navbar: ReactElement;
13+
footer: ReactElement;
14+
fontFamily: string;
15+
lightOnlyPages: string[];
16+
bodyProps?: DetailedHTMLProps<HtmlHTMLAttributes<HTMLBodyElement>, HTMLBodyElement>;
17+
docsRepositoryBase: string;
18+
}
19+
20+
/**
21+
* Alternative to `GuildLayout` for Hive-branded websites.
22+
*
23+
* Accepts navbar and footer as slots/children props, because they're highly customizable,
24+
* and their defaults belong to HiveNavigation and HiveFooter component default props.
25+
*
26+
* ## Configuration
27+
*
28+
* Pages can differ by widths and supported color schemes:
29+
*
30+
* - The footer in docs has 90rem width, in landing pages it has 75rem.
31+
* - The navbar in docs has 90rem width, in landing pages it has 1392px.
32+
* - Landing pages only support light mode for _business and prioritization reasons_.
33+
*
34+
* TODO: Consider unifying this in design phase.
35+
*
36+
* For now, a page or a layout can configue these as follows:
37+
*
38+
* ### Light-only pages
39+
*
40+
* @example
41+
* ```tsx
42+
* <HiveLayout bodyProps={{ lightOnlyPages: ['/', '/friends'] }} />
43+
* ```
44+
*
45+
* This will force light theme to the pages with paths `/` and `/friends`,
46+
* by adding `.light` class to the <body /> element.
47+
*
48+
* ### Landing page widths
49+
*
50+
* @example
51+
* ```tsx
52+
* import { HiveLayoutConfig } from '@theguild/components'
53+
*
54+
* <HiveLayoutConfig widths="landing-narrow" />
55+
* ```
56+
*/
57+
export const HiveLayout = async ({
58+
children,
59+
head,
60+
navbar,
61+
footer,
62+
className,
63+
fontFamily,
64+
lightOnlyPages,
65+
bodyProps,
66+
docsRepositoryBase,
67+
...rest
68+
}: HiveLayoutProps) => {
69+
const pageMap = await getPageMap();
70+
return (
71+
<html
72+
lang="en"
73+
// Required to be set for `nextra-theme-docs` styles
74+
dir="ltr"
75+
// Suggested by `next-themes` package https://github.com/pacocoursey/next-themes#with-app
76+
suppressHydrationWarning
77+
className={cn('font-sans', className)}
78+
{...rest}
79+
>
80+
<Head>
81+
<style>{
82+
/* css */ `
83+
:root {
84+
--font-sans: ${fontFamily};
85+
}
86+
:root.dark {
87+
--nextra-primary-hue: 67.1deg;
88+
--nextra-primary-saturation: 100%;
89+
--nextra-primary-lightness: 55%;
90+
--nextra-bg: 17, 17, 17;
91+
}
92+
:root.dark *::selection {
93+
background-color: hsl(191deg 95% 72% / 0.25)
94+
}
95+
:root.light, :root.dark:has(body.light) {
96+
--nextra-primary-hue: 191deg;
97+
--nextra-primary-saturation: 40%;
98+
--nextra-bg: 255, 255, 255;
99+
}
100+
101+
.x\\:tracking-tight,
102+
.nextra-steps :is(h2, h3, h4) {
103+
letter-spacing: normal;
104+
}
105+
106+
html:has(body.light) {
107+
scroll-behavior: smooth;
108+
background: #fff;
109+
color-scheme: light !important;
110+
}
111+
112+
html:has(body.light) .nextra-search-results mark {
113+
background: oklch(0.611752 0.07807 214.47 / 0.8);
114+
}
115+
116+
html:has(body.light) .nextra-sidebar-footer {
117+
display: none;
118+
}
119+
120+
#crisp-chatbox { z-index: 40 !important; }
121+
`
122+
}</style>
123+
{head}
124+
</Head>
125+
<Body lightOnlyPages={lightOnlyPages} {...bodyProps}>
126+
<Layout
127+
editLink="Edit this page on GitHub"
128+
docsRepositoryBase={docsRepositoryBase}
129+
pageMap={pageMap}
130+
feedback={{
131+
labels: 'kind/docs',
132+
}}
133+
sidebar={{
134+
defaultMenuCollapseLevel: 1,
135+
}}
136+
navbar={navbar}
137+
footer={footer}
138+
>
139+
{children}
140+
</Layout>
141+
</Body>
142+
</html>
143+
);
144+
};

packages/components/src/server/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export {
1515
export { evaluate } from 'nextra/evaluate';
1616
export { fetchPackageInfo } from './npm.js';
1717
export { sharedMetaItems } from './shared-meta-items.js';
18-
export { Body } from './body.client.js';
18+
export * from './body.client.js';
1919
export { remarkLinkRewrite } from './remark-link-rewrite.js';
2020

2121
/**
@@ -26,3 +26,4 @@ export { remarkLinkRewrite } from './remark-link-rewrite.js';
2626
* which is disallowed. Either remove the export, or the "use client" directive. Read more: https://nextjs.org
2727
*/
2828
export { GuildLayout, getDefaultMetadata } from './theme-layout.js';
29+
export { HiveLayout } from './hive-layout.js';

website/app/layout.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ReactNode } from 'react';
22
import { getDefaultMetadata, GuildLayout } from '@theguild/components/server';
33
import '@theguild/components/style.css';
4-
import { GitHubIcon, PaperIcon, PencilIcon } from '@theguild/components';
4+
import { GitHubIcon, PaperIcon, PencilIcon, Search } from '@theguild/components';
55

66
const description = 'Documentation for The Guild';
77
const websiteName = 'Guild Docs';
@@ -35,9 +35,6 @@ const RootLayout = async ({ children }: { children: ReactNode }) => {
3535
}}
3636
navbarProps={{
3737
navLinks: [{ href: '/docs', children: 'Documentation' }],
38-
searchProps: {
39-
placeholder: 'Search...',
40-
},
4138
developerMenu: [
4239
{
4340
href: '/docs',
@@ -55,6 +52,7 @@ const RootLayout = async ({ children }: { children: ReactNode }) => {
5552
children: 'GitHub',
5653
},
5754
],
55+
search: <Search placeholder="Search..." className="[&_input]:transition-none" />,
5856
}}
5957
lightOnlyPages={['/']}
6058
>

0 commit comments

Comments
 (0)