Skip to content

Commit 87b0913

Browse files
authored
Scaffold specific styling (#28)
* Bug fixes * Got styled pre tags in Docusaurus working * Code cleanup * More cleanup * Forked sidebar code into scaffold-specific versions * Fleshed out sidebar styling * Fleshed out styling * CSS variable tweaks * Added note * Removed motion * Made code more reusable * Cleaned up renderer organization * More cleanup * Default to not showing schemas in nav, since they need love * Mark all components as client-side only * Significantly refactored sidebar to support Nextra's needs * Got all components working * Styled SideBar in Nextra
1 parent b2b8f5d commit 87b0913

34 files changed

+871
-533
lines changed

packages/docs-md/eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default [
1111
rootDir: getDirname(),
1212
entryPoints: {
1313
"eslint.config.mjs": ["default"],
14-
"src/index.ts": ["Settings", "TryItNow", "SideBar", "SideBarCta"],
14+
"src/index.ts": ["Settings", "TryItNow", "SideBar", "SideBarTrigger"],
1515
},
1616
ignores: ["src/pages/data/wasm_exec.js"],
1717
restrictedImports: [

packages/docs-md/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
"arg": "^5.0.2",
3434
"jotai": "^2.12.5",
3535
"js-yaml": "^4.1.0",
36-
"motion": "^12.15.0",
3736
"zod": "^3.25.56"
3837
}
3938
}

packages/docs-md/src/cli/cli.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,8 @@ import { load } from "js-yaml";
1515
import z from "zod/v4";
1616

1717
import { generatePages } from "../pages/generatePages.ts";
18-
import {
19-
DocusaurusRenderer,
20-
DocusaurusSite,
21-
} from "../renderers/docusaurus/renderer.ts";
22-
import { NextraRenderer, NextraSite } from "../renderers/nextra/renderer.ts";
18+
import { DocusaurusSite } from "../renderers/docusaurus.ts";
19+
import { NextraSite } from "../renderers/nextra.ts";
2320
import { type Settings, settingsSchema } from "../types/settings.ts";
2421
import type { Site } from "../types/site.ts";
2522
import { assertNever } from "../util/assertNever.ts";
@@ -159,11 +156,11 @@ const specContents = JSON.stringify(load(specData));
159156
let site: Site;
160157
switch (settings.output.framework) {
161158
case "docusaurus": {
162-
site = new DocusaurusSite(DocusaurusRenderer);
159+
site = new DocusaurusSite();
163160
break;
164161
}
165162
case "nextra": {
166-
site = new NextraSite(NextraRenderer);
163+
site = new NextraSite();
167164
break;
168165
}
169166
default: {
@@ -181,9 +178,11 @@ const pageContents = await generatePages({
181178
if (args["--clean"]) {
182179
rmSync(settings.output.pageOutDir, {
183180
recursive: true,
181+
force: true,
184182
});
185183
rmSync(settings.output.componentOutDir, {
186184
recursive: true,
185+
force: true,
187186
});
188187
}
189188

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"use client";
2+
3+
import { atom, useAtom } from "jotai";
4+
import type { FC, PropsWithChildren } from "react";
5+
import { useCallback, useEffect, useState } from "react";
6+
7+
import type { SidebarContent } from "./types.ts";
8+
9+
const sidebarContentAtom = atom<SidebarContent | null>(null);
10+
11+
export function SideBarContents({
12+
SideBarContainer,
13+
}: {
14+
SideBarContainer: FC<{
15+
content: SidebarContent;
16+
closeRequest: () => void;
17+
}>;
18+
}) {
19+
// We keep separate track of the open state vs content because we want to
20+
// start animating the closing of the sidebar before the content is cleared,
21+
// so that we see it slide off screen. This means we can't use content as an
22+
// animation trigger because it would otherwise clear all at once
23+
const [content, setContent] = useAtom(sidebarContentAtom);
24+
const [open, setOpen] = useState(false);
25+
26+
const onAnimationComplete = useCallback(() => {
27+
if (!open) {
28+
setContent(null);
29+
}
30+
}, [open]);
31+
useEffect(() => {
32+
if (content) {
33+
setOpen(true);
34+
}
35+
}, [content]);
36+
37+
const closeRequest = useCallback(() => {
38+
setOpen(false);
39+
}, []);
40+
41+
return (
42+
<div
43+
style={{
44+
position: "fixed",
45+
right: "0",
46+
top: "10%",
47+
maxHeight: "85%",
48+
maxWidth: "50%",
49+
zIndex: 1000,
50+
overflowY: "auto",
51+
transform: open ? "translateX(0)" : "translateX(100%)",
52+
transition: "transform 0.2s ease-in-out",
53+
}}
54+
onTransitionEnd={onAnimationComplete}
55+
>
56+
{content && (
57+
<SideBarContainer content={content} closeRequest={closeRequest} />
58+
)}
59+
</div>
60+
);
61+
}
62+
63+
export type SideBarTriggerProps = PropsWithChildren<{
64+
cta: string;
65+
title: string;
66+
}>;
67+
68+
export function SideBarTriggerContents({
69+
cta,
70+
children,
71+
title,
72+
Button,
73+
}: SideBarTriggerProps & {
74+
Button: FC<PropsWithChildren<{ onClick: () => void }>>;
75+
}) {
76+
const [, setContent] = useAtom(sidebarContentAtom);
77+
const onClick = useCallback(
78+
() => setContent({ title, content: children }),
79+
[title, children]
80+
);
81+
return <Button onClick={onClick}>{cta}</Button>;
82+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"use client";
2+
3+
import type { PropsWithChildren } from "react";
4+
import React from "react";
5+
6+
type SidebarContent = {
7+
title: string;
8+
content: React.ReactNode;
9+
};
10+
11+
export function DocusaurusSideBar({
12+
content,
13+
closeRequest,
14+
}: {
15+
content: SidebarContent;
16+
closeRequest: () => void;
17+
}) {
18+
return (
19+
<div
20+
style={{
21+
backgroundColor: "var(--ifm-hero-background-color)",
22+
color: "var(--ifm-hero-text-color)",
23+
border:
24+
"var(--ifm-global-border-width) solid var(--ifm-blockquote-border-color)",
25+
borderRadius: "var(--ifm-global-radius)",
26+
boxShadow: "var(--ifm-global-shadow-tl)",
27+
padding:
28+
"var(--ifm-alert-padding-vertical) var(--ifm-alert-padding-horizontal)",
29+
}}
30+
>
31+
<div
32+
style={{
33+
display: "flex",
34+
justifyContent: "space-between",
35+
alignItems: "center",
36+
}}
37+
>
38+
<div
39+
style={{
40+
fontWeight: "bold",
41+
// Note: the docs at https://docusaurus.community/knowledge/design/css/variables/ say this variable
42+
// should be `--ifm-heading-h3-font-size`, but it doesn't exist. It's `--ifm-h3-font-size` instead.
43+
fontSize: "var(--ifm-h3-font-size)",
44+
}}
45+
>
46+
{content?.title}
47+
</div>
48+
<button onClick={closeRequest}>X</button>
49+
</div>
50+
<hr
51+
style={{
52+
height: "1px",
53+
backgroundColor: "var(--ifm-breadcrumb-color-active)",
54+
}}
55+
/>
56+
{content?.content}
57+
</div>
58+
);
59+
}
60+
61+
export function DocusaurusSideBarTrigger({
62+
onClick,
63+
children,
64+
}: PropsWithChildren<{ onClick: () => void }>) {
65+
return (
66+
<button
67+
onClick={onClick}
68+
style={{
69+
padding: "8px 16px",
70+
}}
71+
>
72+
{children}
73+
</button>
74+
);
75+
}
Lines changed: 22 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,24 @@
1-
"use client";
2-
3-
import { atom, useAtom } from "jotai";
4-
import { motion } from "motion/react";
5-
import type { PropsWithChildren } from "react";
6-
import React, { useCallback, useEffect, useState } from "react";
7-
8-
type SidebarContent = {
9-
title: string;
10-
content: React.ReactNode;
1+
// IMPORTANT! This file MUST NOT be marked as "use client", otherwise it will
2+
// cause Nextra to error when trying to render. This is because MDX files cannot
3+
// import files marked with "use client", for some reason, but it's perfectly
4+
// happy to import a server component (this file) that then imports a client
5+
// component.
6+
7+
import type { SideBarTriggerProps } from "./containers.tsx";
8+
import { SideBarContents, SideBarTriggerContents } from "./containers.tsx";
9+
import { DocusaurusSideBar, DocusaurusSideBarTrigger } from "./docusaurus.tsx";
10+
import { NextraSideBar, NextraSideBarTrigger } from "./nextra.tsx";
11+
12+
export const SideBar = {
13+
Docusaurus: () => <SideBarContents SideBarContainer={DocusaurusSideBar} />,
14+
Nextra: () => <SideBarContents SideBarContainer={NextraSideBar} />,
1115
};
1216

13-
const sidebarContentAtom = atom<SidebarContent | null>(null);
14-
15-
export function SideBar() {
16-
// We keep separate track of the open state vs content because we want to
17-
// start animating the closing of the sidebar before the content is cleared,
18-
// so that we see it slide off screen. This means we can't use content as an
19-
// animation trigger because it would otherwise clear all at
20-
const [content, setContent] = useAtom(sidebarContentAtom);
21-
const [open, setOpen] = useState(false);
22-
23-
const onAnimationComplete = useCallback(() => {
24-
if (!open) {
25-
setContent(null);
26-
}
27-
}, [open]);
28-
useEffect(() => {
29-
if (content) {
30-
setOpen(true);
31-
}
32-
}, [content]);
33-
34-
// TODO: also need to listen for keyboard events
35-
const clickShield = useCallback((e: React.MouseEvent) => {
36-
e.stopPropagation();
37-
e.preventDefault();
38-
}, []);
39-
const closeRequest = useCallback(() => {
40-
setOpen(false);
41-
}, []);
42-
43-
return (
44-
<motion.div
45-
style={{
46-
position: "fixed",
47-
right: "-100%",
48-
top: "10%",
49-
maxHeight: "85%",
50-
maxWidth: "50%",
51-
zIndex: 1000,
52-
overflowY: "scroll",
53-
}}
54-
animate={{
55-
right: open ? "0" : "-100%",
56-
transition: {
57-
duration: 0.3,
58-
},
59-
}}
60-
onAnimationComplete={onAnimationComplete}
61-
>
62-
{content && (
63-
<details
64-
open
65-
style={{
66-
border: "1px solid #ccc",
67-
padding: "8px",
68-
borderRadius: "8px",
69-
}}
70-
>
71-
<summary
72-
style={{
73-
cursor: "default",
74-
display: "flex",
75-
justifyContent: "space-between",
76-
}}
77-
onClick={clickShield}
78-
>
79-
{content?.title}
80-
<button onClick={closeRequest}>X</button>
81-
</summary>
82-
{content?.content}
83-
</details>
84-
)}
85-
</motion.div>
86-
);
87-
}
88-
89-
export function SideBarCta({
90-
cta,
91-
children,
92-
title,
93-
}: PropsWithChildren<{
94-
cta: string;
95-
title: string;
96-
}>) {
97-
const [, setContent] = useAtom(sidebarContentAtom);
98-
const onClick = useCallback(
99-
() => setContent({ title, content: children }),
100-
[title, children]
101-
);
102-
return (
103-
<button
104-
onClick={onClick}
105-
style={{
106-
padding: "8px 16px",
107-
border: "1px solid #ccc",
108-
borderRadius: "8px",
109-
}}
110-
>
111-
{cta}
112-
</button>
113-
);
114-
}
17+
export const SideBarTrigger = {
18+
Docusaurus: (props: SideBarTriggerProps) => (
19+
<SideBarTriggerContents {...props} Button={DocusaurusSideBarTrigger} />
20+
),
21+
Nextra: (props: SideBarTriggerProps) => (
22+
<SideBarTriggerContents {...props} Button={NextraSideBarTrigger} />
23+
),
24+
};

0 commit comments

Comments
 (0)