docs(docs): migrate to array based folder structure

This commit is contained in:
Prad Nukala
2026-07-14 11:07:15 -04:00
parent a7f9fc28e0
commit 172bdcf40e
24 changed files with 64 additions and 2914 deletions
+2 -4
View File
@@ -1,4 +1,2 @@
node_modules .blume/
dist dist/
.source
.vercel
-85
View File
@@ -1,85 +0,0 @@
# docs
A [Fumapress](https://press.fumadocs.dev) (Fumadocs + Waku) docs site with
typography by [shadcn/typeset](https://ui.shadcn.com/docs/typeset) — one CSS
file you own.
## Getting started
Requires **Node.js 22+**.
```bash
bun install
bun dev # http://localhost:3000
```
## Your typeset
| Control | Choice |
| ------- | ------ |
| Heading | DM Sans |
| Body | Instrument Sans |
| Mono | JetBrains Mono |
| Size | 16px |
| Leading | Loose (1.9) |
| Flow | Compact (1em) |
| Measure | 80ch |
- `src/typeset.css` is the stylesheet from the
[typeset builder](https://ui.shadcn.com/typeset). It lives in your project —
edit it directly, or regenerate it from the builder any time.
- `src/app.css` holds your presets. `.typeset-docs` carries the choices
above; `.typeset-compact` is a tighter preset for chat-like UI. Rhythm is
three variables: `--typeset-size`, `--typeset-leading`, `--typeset-flow`.
- `typeset-prompt.md` is an agent-ready prompt with the choices above baked
in (like the builder's Prompt tab). Paste it into your coding agent to wire
typeset into the surfaces that render markdown.
Wrap any rendered markdown/HTML:
```tsx
<article className="typeset typeset-docs typeset-measure">{page}</article>
<div className="typeset typeset-compact">{message}</div>
```
Opt components out with `not-typeset`:
```tsx
<div className="typeset typeset-docs">
<p>Styled prose.</p>
<Card className="not-typeset">Untouched component.</Card>
</div>
```
## Fumadocs prose seam
Fumadocs UI ships its own `prose` styles for docs page bodies. If you apply
`typeset` to the same container, both systems will add spacing. This
template zeroes out `margin-block-end` inside `.typeset.prose` (see
`src/app.css`) so flow comes only from typeset's `margin-block-start`.
To apply the typeset classes to docs pages, customize the page layout in
`press.config.tsx` — see the
[Fumapress layouts docs](https://press.fumadocs.dev/docs/layouts).
## Structure
```
├── waku.config.ts # Waku/Vite plugins: press(), mdx(), tailwindcss()
├── source.config.ts # Fumadocs MDX content source (content/docs)
├── press.config.tsx # Site config, adapters, plugins
├── typeset-prompt.md # Agent prompt with your typeset picks baked in
├── content/docs/ # Your MDX pages
└── src/
├── app.css # Tailwind + presets + your typeset presets
└── typeset.css # shadcn/typeset — yours to edit
```
## Deploying static
Fumapress builds static + dynamic routes by default. For a pure CDN deploy,
force static mode in `press.config.tsx`:
```tsx
export default defineConfig({ mode: "static", /* ... */ });
```
-1739
View File
File diff suppressed because it is too large Load Diff
-14
View File
@@ -1,14 +0,0 @@
{
"$schema": "node_modules/@fumadocs/cli/dist/schema.json",
"aliases": {
"uiDir": "./components/ui",
"componentsDir": "./components",
"layoutDir": "./layouts",
"cssDir": "./styles",
"libDir": "./lib"
},
"baseDir": "src",
"uiLibrary": "base-ui",
"framework": "waku",
"commands": {}
}
-68
View File
@@ -1,68 +0,0 @@
---
title: Introduction
description: Prad's Random Leetcode Notes
---
## Overview
This is a collection of random CS notes that I've made over the years. I've been learning CS for a while now and I thought it would be a good idea to make a public repository of my notes. This is the first iteration of the repository and I plan to add more content to it over time.
## The Golden Rule (Memorize This)
**Only sort if the problem asks for ORDER or RANKING.**
If the problem asks for:
- "first"
- "index"
- "position"
- "does it exist"
- "true/false"
- "count how many"
→ **DO NOT SORT**. Just use the frequency map and loop.
---
## Decision Cheat Sheet
| What the problem wants | Do you need to sort? | What to do instead | Example Problem |
|-----------------------------------------|----------------------|-------------------------------------|---------------------|
| Sort characters by how often they appear | YES | Freq + Sort | LC 451 |
| Top K most frequent | YES | Freq + Sort + slice | LC 347 |
| Sort array by frequency | YES | Freq + Sort | LC 1636 |
| First unique character (index) | NO | Freq + loop original string | LC 387 |
| Is it an anagram? | NO | Freq + check counts | LC 242 |
| How many numbers smaller than current | Kind of | Freq + prefix count | LC 1365 |
| Most frequent element | NO | Freq + find max | LC 169 |
---
## Mental Checklist (Say this out loud every time)
1. "Did I count the frequencies?" → Yes
2. "Does the problem care about the **original order**?"
- Yes → Loop the original string/array and check freq
- No → You can sort
3. "Does it want an **index** or a **boolean**?"
- Yes → Almost never sort
4. "Does it want the elements **re-ordered**?"
- Yes → Sort by frequency
---
## Super Short Version to Tattoo in Your Brain
**"If it wants the first / index / true-false → loop original.
If it wants sorted / top k / reordered → sort."**
---
## Practice Trigger Words
- "first non-repeating" → Loop original
- "return its index" → Loop original
- "sort by frequency" → Sort
- "top k frequent" → Sort + slice
- "valid anagram" → Just check counts
+62 -4
View File
@@ -1,10 +1,68 @@
--- ---
title: Introduction title: Introduction
description: Welcome to your new Blume docs. description: Prad's Random Leetcode Notes
--- ---
# Introduction ## Overview
This is a collection of random CS notes that I've made over the years. I've been learning CS for a while now and I thought it would be a good idea to make a public repository of my notes. This is the first iteration of the repository and I plan to add more content to it over time.
## The Golden Rule (Memorize This)
**Only sort if the problem asks for ORDER or RANKING.**
If the problem asks for:
- "first"
- "index"
- "position"
- "does it exist"
- "true/false"
- "count how many"
→ **DO NOT SORT**. Just use the frequency map and loop.
---
## Decision Cheat Sheet
| What the problem wants | Do you need to sort? | What to do instead | Example Problem |
|-----------------------------------------|----------------------|-------------------------------------|---------------------|
| Sort characters by how often they appear | YES | Freq + Sort | LC 451 |
| Top K most frequent | YES | Freq + Sort + slice | LC 347 |
| Sort array by frequency | YES | Freq + Sort | LC 1636 |
| First unique character (index) | NO | Freq + loop original string | LC 387 |
| Is it an anagram? | NO | Freq + check counts | LC 242 |
| How many numbers smaller than current | Kind of | Freq + prefix count | LC 1365 |
| Most frequent element | NO | Freq + find max | LC 169 |
---
## Mental Checklist (Say this out loud every time)
1. "Did I count the frequencies?" → Yes
2. "Does the problem care about the **original order**?"
- Yes → Loop the original string/array and check freq
- No → You can sort
3. "Does it want an **index** or a **boolean**?"
- Yes → Almost never sort
4. "Does it want the elements **re-ordered**?"
- Yes → Sort by frequency
---
## Super Short Version to Tattoo in Your Brain
**"If it wants the first / index / true-false → loop original.
If it wants sorted / top k / reordered → sort."**
---
## Practice Trigger Words
- "first non-repeating" → Loop original
- "return its index" → Loop original
- "sort by frequency" → Sort
- "top k frequent" → Sort + slice
- "valid anagram" → Just check counts
Welcome to **Blume** — markdown-first docs powered by Astro and Vite.
Edit `docs/index.mdx` to get started, then run `blume dev`.
-38
View File
@@ -1,38 +0,0 @@
{
"name": "docs",
"private": true,
"type": "module",
"scripts": {
"dev": "waku dev",
"build": "waku build",
"start": "waku start",
"types:check": "fumadocs-mdx && tsc --noEmit"
},
"dependencies": {
"@base-ui/react": "latest",
"@fontsource-variable/dm-sans": "latest",
"@fontsource-variable/instrument-sans": "latest",
"@fontsource-variable/jetbrains-mono": "latest",
"@fuma-translate/react": "^1.0.2",
"@fumadocs/base-ui": "^16.11.3",
"class-variance-authority": "^0.7.1",
"cnfast": "^0.0.8",
"fumadocs-core": "latest",
"fumadocs-mdx": "latest",
"fumadocs-ui": "npm:@fumadocs/base-ui@latest",
"fumapress": "latest",
"lucide-react": "^1.23.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-server-dom-webpack": "^19.2.0",
"vercel": "^55.0.0",
"waku": "latest"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.0"
}
}
-51
View File
@@ -1,51 +0,0 @@
import { defineConfig } from "fumapress";
import { fumadocsMdx } from "fumapress/adapters/mdx";
import defaultMdxComponents, { createRelativeLink } from "fumadocs-ui/mdx";
import { TypeTable } from "fumadocs-ui/components/type-table";
import { Step, Steps } from "fumadocs-ui/components/steps";
import { Tabs, Tab } from "fumadocs-ui/components/tabs";
import { Accordion, Accordions } from "fumadocs-ui/components/accordion";
import { flexsearchPlugin } from "fumapress/plugins/flexsearch";
import { llmsPlugin } from "fumapress/plugins/llms.txt";
import { docs } from "./.source/server";
export default defineConfig({
site: {
name: "Docs",
baseUrl: import.meta.env.DEV
? "http://localhost:3000"
: "https://cs.prad.nu",
git: {
user: "prdlk",
branch: "main",
repo: "leetcode",
},
},
content: {
docs: docs.toFumadocsSource(),
},
meta: {
root() {
return <meta property="og:type" content="website" />;
},
},
})
.plugins(flexsearchPlugin(), llmsPlugin())
.adapters(
fumadocsMdx({
async getMdxComponents(page) {
const source = await this.getLoader();
return {
...defaultMdxComponents,
TypeTable,
Steps,
Step,
Tabs,
Tab,
Accordion,
Accordions,
a: createRelativeLink(source, page),
};
},
}),
);
-11
View File
@@ -1,11 +0,0 @@
import { defineConfig, defineDocs } from "fumadocs-mdx/config";
export const docs = defineDocs({
dir: "content/docs",
docs: {
// Required by fumapress' llms.txt plugin (getText("processed")).
postprocess: { includeProcessedMarkdown: true },
},
});
export default defineConfig();
-65
View File
@@ -1,65 +0,0 @@
@import "tailwindcss";
@import "fumadocs-ui/css/neutral.css";
@import "fumadocs-ui/css/preset.css";
@import "fumapress/css/preset.css";
/* Fonts — self-hosted via Fontsource, referenced by the typeset presets. */
@import "@fontsource-variable/dm-sans";
@import "@fontsource-variable/instrument-sans";
@import "@fontsource-variable/jetbrains-mono";
/* shadcn/typeset — one CSS file you own.
Regenerate it any time at https://ui.shadcn.com/typeset */
@import "./typeset.css";
:root {
--font-dm-sans: "DM Sans Variable", sans-serif;
--font-instrument-sans: "Instrument Sans Variable", sans-serif;
--font-jetbrains-mono: "JetBrains Mono Variable", monospace;
}
@theme inline {
--default-font-family: var(--font-instrument-sans);
--font-heading: var(--font-dm-sans);
--font-mono: var(--font-jetbrains-mono);
}
/* ---------------------------------------------------------
Typeset presets — tune rhythm per context.
Three controls: size, leading, flow.
Built with https://ui.shadcn.com/typeset
--------------------------------------------------------- */
.typeset-docs {
--typeset-font-body: var(--font-instrument-sans);
--typeset-font-heading: var(--font-dm-sans);
--typeset-font-mono: var(--font-jetbrains-mono);
--typeset-size: 16px;
--typeset-leading: 1.9;
--typeset-flow: 1em;
}
/* Tighter rhythm, e.g. for changelog callouts or chat-like UI */
.typeset-compact {
--typeset-leading: 1.6;
--typeset-flow: 1em;
}
/* Measure (80ch) — the builder puts max-width on the wrapper,
not in typeset.css. Add this class next to `typeset` when your
layout doesn't already constrain line length. */
.typeset-measure {
max-width: 37em;
}
/* ---------------------------------------------------------
Fumadocs interop:
Fumadocs UI ships its own `prose` styling for page bodies.
When a container is both, let Typeset own the rhythm by
disabling the prose margins that would double up.
Keep interactive Fumadocs components (tabs, callouts,
accordions) out of Typeset with `not-typeset`.
--------------------------------------------------------- */
.typeset.prose :where(*) {
margin-block-end: 0;
}
-105
View File
@@ -1,105 +0,0 @@
'use client';
import { Check, LinkIcon } from 'lucide-react';
import { type ComponentProps, type ReactNode, useEffect, useRef, useState } from 'react';
import { cn } from '../lib/cn';
import { useCopyButton } from '@fumadocs/base-ui/utils/use-copy-button';
import { buttonVariants } from './ui/button';
import { mergeRefs } from '../lib/merge-refs';
import { useTranslations } from '@fuma-translate/react';
import {
Accordion as Root,
AccordionContent,
AccordionHeader,
AccordionItem,
AccordionTrigger,
} from './ui/accordion';
export function Accordions({
ref,
className,
defaultValue,
...props
}: ComponentProps<typeof Root>) {
const rootRef = useRef<HTMLDivElement>(null);
const composedRef = mergeRefs(ref, rootRef);
const [value, setValue] = useState<unknown[]>(defaultValue ?? []);
useEffect(() => {
const id = window.location.hash.substring(1);
const element = rootRef.current;
if (!element || id.length === 0) return;
const selected = document.getElementById(id);
if (!selected || !element.contains(selected)) return;
const value = selected.getAttribute('data-accordion-value');
if (value) setValue((prev) => [value, ...prev]);
}, []);
return (
<Root
ref={composedRef}
value={value}
onValueChange={setValue}
className={(s) =>
cn(
'divide-y divide-fd-border overflow-hidden rounded-lg border bg-fd-card',
typeof className === 'function' ? className(s) : className,
)
}
{...props}
/>
);
}
export function Accordion({
title,
id,
value = String(title),
children,
...props
}: Omit<ComponentProps<typeof AccordionItem>, 'value' | 'title'> & {
title: string | ReactNode;
value?: string;
}) {
return (
<AccordionItem value={value} {...props}>
<AccordionHeader id={id} data-accordion-value={value}>
<AccordionTrigger>{title}</AccordionTrigger>
{id ? <CopyButton id={id} /> : null}
</AccordionHeader>
<AccordionContent hiddenUntilFound>
<div className="px-4 pb-2 text-[0.9375rem] prose-no-margin [&[hidden]:not([hidden='until-found'])]:hidden">
{children}
</div>
</AccordionContent>
</AccordionItem>
);
}
function CopyButton({ id }: { id: string }) {
const t = useTranslations({ note: 'accordion' });
const [checked, onClick] = useCopyButton(() => {
const url = new URL(window.location.href);
url.hash = id;
return navigator.clipboard.writeText(url.toString());
});
return (
<button
type="button"
aria-label={t('Copy Link', { note: 'aria-label' })}
className={cn(
buttonVariants({
color: 'ghost',
className: 'text-fd-muted-foreground me-2',
}),
)}
onClick={onClick}
>
{checked ? <Check className="size-3.5" /> : <LinkIcon className="size-3.5" />}
</button>
);
}
-9
View File
@@ -1,9 +0,0 @@
import type { ReactNode } from 'react';
export function Steps({ children }: { children: ReactNode }) {
return <div className="fd-steps">{children}</div>;
}
export function Step({ children }: { children: ReactNode }) {
return <div className="fd-step">{children}</div>;
}
-85
View File
@@ -1,85 +0,0 @@
'use client';
import { Accordion as Primitive } from '@base-ui/react/accordion';
import { ChevronRight } from 'lucide-react';
import { type ComponentProps } from 'react';
import { cn } from '../../lib/cn';
export function Accordion({ className, ...props }: ComponentProps<typeof Primitive.Root>) {
return (
<Primitive.Root
className={(s) =>
cn(
'divide-y divide-fd-border overflow-hidden rounded-lg border bg-fd-card',
typeof className === 'function' ? className(s) : className,
)
}
{...props}
/>
);
}
export function AccordionItem({ children, ...props }: ComponentProps<typeof Primitive.Item>) {
return <Primitive.Item {...props}>{children}</Primitive.Item>;
}
export function AccordionHeader({
className,
children,
...props
}: ComponentProps<typeof Primitive.Header>) {
return (
<Primitive.Header
className={(s) =>
cn(
'scroll-m-24 not-prose flex flex-row items-center text-fd-card-foreground font-medium has-focus-visible:bg-fd-accent',
typeof className === 'function' ? className(s) : className,
)
}
{...props}
>
{children}
</Primitive.Header>
);
}
export function AccordionTrigger({
className,
children,
...props
}: ComponentProps<typeof Primitive.Trigger>) {
return (
<Primitive.Trigger
className={(s) =>
cn(
'group flex flex-1 items-center gap-2 px-3 py-2.5 text-start focus-visible:outline-none',
typeof className === 'function' ? className(s) : className,
)
}
{...props}
>
<ChevronRight className="size-4 shrink-0 text-fd-muted-foreground transition-transform duration-200 group-data-[panel-open]:rotate-90" />
{children}
</Primitive.Trigger>
);
}
export function AccordionContent({
className,
children,
...props
}: ComponentProps<typeof Primitive.Panel>) {
return (
<Primitive.Panel
className={(s) =>
cn(
'h-(--accordion-panel-height) overflow-hidden transition-[height] ease-out data-[ending-style]:h-0 data-[starting-style]:h-0',
typeof className === 'function' ? className(s) : className,
)
}
{...props}
>
{children}
</Primitive.Panel>
);
}
-28
View File
@@ -1,28 +0,0 @@
import { cva, type VariantProps } from 'class-variance-authority';
const variants = {
primary: 'bg-fd-primary text-fd-primary-foreground hover:bg-fd-primary/80',
outline: 'border hover:bg-fd-accent hover:text-fd-accent-foreground',
ghost: 'hover:bg-fd-accent hover:text-fd-accent-foreground',
secondary:
'border bg-fd-secondary text-fd-secondary-foreground hover:bg-fd-accent hover:text-fd-accent-foreground',
} as const;
export const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md p-2 text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring',
{
variants: {
variant: variants,
// fumadocs use `color` instead of `variant`
color: variants,
size: {
sm: 'gap-1 px-2 py-1.5 text-xs',
icon: 'p-1.5 [&_svg]:size-5',
'icon-sm': 'p-1.5 [&_svg]:size-4.5',
'icon-xs': 'p-1 [&_svg]:size-4',
},
},
},
);
export type ButtonProps = VariantProps<typeof buttonVariants>;
-1
View File
@@ -1 +0,0 @@
export { cn } from 'cnfast';
-13
View File
@@ -1,13 +0,0 @@
import type * as React from 'react';
export function mergeRefs<T>(...refs: (React.Ref<T> | undefined)[]): React.RefCallback<T> {
return (value) => {
refs.forEach((ref) => {
if (typeof ref === 'function') {
ref(value);
} else if (ref) {
ref.current = value;
}
});
};
}
-490
View File
@@ -1,490 +0,0 @@
/*
* shadcn/typeset
* https://ui.shadcn.com/docs/typeset.
*/
@layer components {
.typeset {
--typeset-font-body: inherit;
--typeset-font-heading: var(--font-heading);
--typeset-font-mono: var(--font-mono);
--typeset-size: 1em;
--typeset-leading: 1.75;
--typeset-flow: 1.25em;
}
}
@layer components {
.typeset {
font-family: var(--typeset-font-body);
font-size: calc(var(--typeset-size) * 1.125);
line-height: var(--typeset-leading);
color: var(--color-foreground, currentColor);
overflow-wrap: break-word;
margin-trim: block-start;
@media (min-width: 48rem), print {
font-size: var(--typeset-size);
}
--typeset-muted: var(
--color-muted-foreground,
color-mix(in oklab, currentColor 60%, transparent)
);
--typeset-rule: var(
--color-border,
color-mix(in oklab, currentColor 20%, transparent)
);
}
.typeset
*:not(
:where(
.not-typeset,
[data-not-typeset],
.not-typeset *,
[data-not-typeset] *
)
) {
/* Paragraphs. */
&:where(p) {
margin-block-start: var(--typeset-flow);
margin-block-end: 0;
}
/* Headings. */
&:where(h1, h2, h3, h4, h5, h6) {
color: var(--color-foreground, currentColor);
font-family: var(--typeset-font-heading);
font-weight: 600;
margin-block-end: 0;
}
&:where(h1) {
font-size: 1.75em;
line-height: 1.3;
margin-block-start: var(--typeset-flow);
}
&:where(h2) {
font-size: 1.25em;
line-height: 1.4;
margin-block-start: calc(var(--typeset-flow) * 1.4);
}
&:where(h3) {
font-size: 1.125em;
line-height: 1.45;
margin-block-start: var(--typeset-flow);
}
&:where(h4) {
font-size: 1em;
line-height: 1.5;
margin-block-start: var(--typeset-flow);
}
&:where(h5) {
font-size: 0.875em;
line-height: 1.5;
font-weight: 500;
color: var(--typeset-muted);
margin-block-start: calc(var(--typeset-flow) / 0.875);
}
&:where(h6) {
font-size: 0.8125em;
line-height: 1.5;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--typeset-muted);
margin-block-start: calc(var(--typeset-flow) / 0.8125);
}
/* Anchors. */
&:where([id]) {
scroll-margin-block-start: var(--typeset-flow);
}
/* Headings own the space below them. */
&:where(h1 + *, h2 + *, h3 + *, h4 + *, h5 + *, h6 + *) {
margin-block-start: 1em;
}
&:where(:is(h1, h2, h3, h4) :is(code)) {
font-size: 0.9em;
}
/* Links. */
&:where(a) {
color: inherit;
font-weight: 500;
text-decoration-line: underline;
text-decoration-color: color-mix(in oklab, currentColor 30%, transparent);
}
&:where(a:hover) {
text-decoration-color: currentColor;
}
&:where(a:focus-visible) {
outline: 2px solid var(--color-ring, currentColor);
outline-offset: 2px;
border-radius: 0.125em;
}
&:where(:is(h1, h2, h3, h4, h5, h6) :is(a)) {
font-weight: inherit;
}
/* Inline semantics. */
&:where(strong, b) {
font-weight: 600;
}
&:where(:is(h1, h2, h3, h4) :is(strong, b)) {
font-weight: 600;
}
&:where(mark) {
background-color: color-mix(
in oklab,
oklch(0.86 0.17 95) 40%,
transparent
);
color: inherit;
padding: 0.05em 0.15em;
border-radius: 0.2em;
}
&:where(abbr[title]) {
text-decoration-line: underline;
text-decoration-style: dotted;
text-underline-offset: 0.2em;
cursor: help;
}
&:where(del, s) {
color: var(--typeset-muted);
text-decoration-line: line-through;
}
&:where(sup, sub) {
font-size: 0.75em;
line-height: 0;
position: relative;
vertical-align: baseline;
}
&:where(sup) {
top: -0.5em;
}
&:where(sub) {
bottom: -0.25em;
}
&:where(sup a) {
text-decoration-line: none;
font-weight: 500;
}
/* Lists. */
&:where(ul) {
list-style-type: disc;
}
&:where(ol) {
list-style-type: decimal;
}
&:where(ul ul) {
list-style-type: circle;
}
&:where(ul ul ul) {
list-style-type: square;
}
&:where(ol[type="a" i]) {
list-style-type: lower-alpha;
}
&:where(ol[type="i" i]) {
list-style-type: lower-roman;
}
&:where(ol[type="A" s]) {
list-style-type: upper-alpha;
}
&:where(ol[type="I" s]) {
list-style-type: upper-roman;
}
&:where(ul, ol) {
margin-block-start: var(--typeset-flow);
margin-block-end: 0;
padding-inline-start: 1.5em;
}
&:where(li) {
margin-block-start: 0.5em;
padding-inline-start: 0.4em;
}
&:where(li > p, li > ul, li > ol) {
margin-block-start: 0.5em;
}
&:where(ul > li)::marker {
color: var(--typeset-muted);
}
&:where(ol > li)::marker {
color: var(--typeset-muted);
}
/* GFM task lists. */
&:where(ul.contains-task-list) {
list-style-type: none;
padding-inline-start: 0.25em;
}
&:where(li.task-list-item > input[type="checkbox"]) {
margin-inline-end: 0.5em;
vertical-align: -0.1em;
accent-color: var(--color-primary, currentColor);
}
/* Disclosures. */
&:where(details) {
margin-block-start: var(--typeset-flow);
margin-block-end: 0;
}
&:where(summary) {
cursor: pointer;
font-weight: 500;
}
&:where(summary)::marker {
color: var(--typeset-muted);
}
/* Keyboard keys. */
&:where(kbd) {
font-family: inherit;
font-size: 0.85em;
font-weight: 500;
border: 1px solid var(--typeset-rule);
border-block-end-width: 2px;
border-radius: min(calc(var(--radius, 0.5em) * 0.6), 0.35em);
padding: 0.0625em 0.35em;
}
/* Definition lists. */
&:where(dl) {
margin-block-start: var(--typeset-flow);
margin-block-end: 0;
}
&:where(dt) {
font-weight: 500;
margin-block-start: 1em;
}
&:where(dt + dt) {
margin-block-start: 0.25em;
}
&:where(dd) {
margin-block-start: 0.25em;
margin-inline-start: 0;
padding-inline-start: 1em;
color: var(--typeset-muted);
}
/* Inline code. */
&:where(:not(pre) > code) {
background-color: var(
--color-muted,
color-mix(in oklab, currentColor 8%, transparent)
);
font-family: var(--typeset-font-mono);
font-size: 0.85em;
border-radius: min(calc(var(--radius, 0.5em) * 0.6), 0.35em);
padding: 0.125em 0.3em;
}
/* Code blocks. */
&:where(pre) {
background-color: var(
--color-muted,
color-mix(in oklab, currentColor 8%, transparent)
);
font-family: var(--typeset-font-mono);
font-size: 0.875em;
line-height: 1.5;
tab-size: 2;
direction: ltr;
border-radius: var(--radius, 0.5em);
padding: 0.75em 1em;
overflow-x: auto;
margin-block-start: calc(var(--typeset-flow) / 0.875);
margin-block-end: 0;
}
&:where(pre code) {
background-color: transparent;
font-family: inherit;
font-size: inherit;
padding: 0;
border-radius: 0;
}
/* Blockquote. */
&:where(blockquote) {
border-inline-start: 2px solid var(--typeset-rule);
padding-inline-start: 1em;
margin-block-start: var(--typeset-flow);
margin-block-end: 0;
margin-inline: 0;
}
/* Dividers. */
&:where(hr) {
border: 0;
border-block-start: 1px solid var(--typeset-rule);
margin-block-start: calc(var(--typeset-flow) * 2.4);
margin-block-end: 0;
}
&:where(hr + h1, hr + h2, hr + h3, hr + h4) {
margin-block-start: var(--typeset-flow);
}
/* GFM footnotes. */
&:where(.footnotes, [data-footnotes]) {
margin-block-start: calc(var(--typeset-flow) * 2);
border-block-start: 1px solid var(--typeset-rule);
padding-block-start: var(--typeset-flow);
font-size: 0.875em;
color: var(--typeset-muted);
}
/* Math. */
&:where(math[display="block"]) {
margin-block-start: var(--typeset-flow);
margin-block-end: 0;
overflow-x: auto;
overflow-y: hidden;
padding-block: 0.25em;
}
/* Media. */
&:where(img, video) {
border-radius: var(--radius, 0.5em);
max-width: 100%;
height: auto;
margin-block-start: var(--typeset-flow);
margin-block-end: 0;
}
&:where(p img) {
margin-block-start: 0;
}
&:where(figure) {
margin-block-start: var(--typeset-flow);
margin-block-end: 0;
margin-inline: 0;
}
&:where(figcaption, caption) {
color: var(--typeset-muted);
font-size: 0.875em;
text-align: center;
margin-block-start: calc(0.75em / 0.875);
}
&:where(caption) {
caption-side: bottom;
}
/* Tables. Separators sit on cells, not tr:last-child: append-safe.
Bare tables stay real tables (keep their semantics) and wrap to fit. */
&:where(table) {
max-width: 100%;
font-size: 1em;
line-height: 1.5;
font-variant-numeric: tabular-nums;
border-collapse: separate;
border-spacing: 0;
border-block-end: 1px solid var(--typeset-rule);
margin-block-start: var(--typeset-flow);
margin-block-end: 0;
}
/* Horizontal scroll for any wide block. Wrap it and the wrapper owns the
flow margin. Tables shrink to fit, so widen the table to make it scroll
instead of compress. */
&:where(.typeset-scroll) {
overflow-x: auto;
margin-block-start: var(--typeset-flow);
}
&:where(.typeset-scroll > *) {
margin-block-start: 0;
}
&:where(.typeset-scroll table) {
width: max-content;
max-width: none;
}
&:where(thead th) {
font-weight: 500;
text-align: start;
white-space: nowrap;
padding: 0.65em 1em;
}
&:where(:is(tbody, tfoot) :is(td, th)) {
padding: 0.75em 1em;
text-align: start;
vertical-align: top;
border-block-start: 1px solid var(--typeset-rule);
}
&:where(tbody th, tfoot th) {
font-weight: 500;
}
&:where(th:first-child, td:first-child) {
padding-inline-start: 0;
}
&:where(th[align="center"], td[align="center"]) {
text-align: center;
}
&:where(th[align="right"], td[align="right"]) {
text-align: end;
}
/* Blocks inside list items keep the list's tighter rhythm. */
&:where(li > blockquote, li > table, li > figure) {
margin-block-start: 0.5em;
}
&:where(li > pre) {
margin-block-start: calc(0.5em / 0.875);
}
@media print {
&:where(pre, table, blockquote, figure) {
break-inside: avoid;
}
&:where(h1, h2, h3, h4) {
break-after: avoid;
}
}
/* Forced colors strips the code background, so give it a border. */
@media (forced-colors: active) {
&:where(:not(pre) > code) {
border: 1px solid;
}
}
}
/* First child adds no space above. Last so it wins ties. */
.typeset
> :where(:first-child):not(
:where(
.not-typeset,
[data-not-typeset],
.not-typeset *,
[data-not-typeset] *
)
),
.typeset
> :where(:first-child)
> :where(:first-child):not(
:where(
.not-typeset,
[data-not-typeset],
.not-typeset *,
[data-not-typeset] *
)
),
.typeset
:where(
li > :first-child,
blockquote > :first-child,
td > :first-child,
th > :first-child,
dd > :first-child,
figure > :first-child,
figure > picture > img
):not(
:where(
.not-typeset,
[data-not-typeset],
.not-typeset *,
[data-not-typeset] *
)
) {
margin-block-start: 0;
}
}
-20
View File
@@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"isolatedModules": true,
"noEmit": true,
"types": ["react/experimental"],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src", "press.config.tsx", "source.config.ts", ".source"],
"exclude": ["node_modules", "dist"]
}
-84
View File
@@ -1,84 +0,0 @@
Install shadcn/typeset in this Fumapress (Fumadocs + Waku) project.
Typeset is a single stylesheet that styles rendered markdown: wrap the output
in a `typeset` container and everything inside (headings, lists, tables,
code, blockquotes, math) is styled. Everything outside is untouched.
If this project was scaffolded with create-fumapress-typeset, steps 1-4 are
already done — verify each one and move on; every step below is safe to
re-run.
1. Download https://ui.shadcn.com/typeset.css and save it as
`src/typeset.css` (next to the main CSS file, `src/app.css`). If the
file already exists, replace it with the downloaded copy.
2. Import it in `src/app.css`, after the Tailwind and Fumadocs imports:
@import "./typeset.css";
3. Install the fonts:
bun add @fontsource-variable/dm-sans @fontsource-variable/instrument-sans @fontsource-variable/jetbrains-mono
Then import them in `src/app.css`:
@import "@fontsource-variable/dm-sans";
@import "@fontsource-variable/instrument-sans";
@import "@fontsource-variable/jetbrains-mono";
:root {
--font-dm-sans: "DM Sans Variable", sans-serif;
--font-instrument-sans: "Instrument Sans Variable", sans-serif;
--font-jetbrains-mono: "JetBrains Mono Variable", monospace;
}
4. Add this preset to `src/app.css`, after the typeset import. If a class
named `.typeset-docs` already exists, update its values in place. Leave
any other `typeset-*` presets untouched: they are separate surfaces:
.typeset-docs {
--typeset-font-body: var(--font-instrument-sans);
--typeset-font-heading: var(--font-dm-sans);
--typeset-font-mono: var(--font-jetbrains-mono);
--typeset-size: 16px;
--typeset-leading: 1.9;
--typeset-flow: 1em;
}
5. Do not apply the class anywhere yet. Search the project for surfaces that
render markdown or rich content:
- The Fumadocs docs page body. Fumapress owns that layout: customize it in
`press.config.tsx` with `createDocsLayoutPage` from
"fumapress/layouts/docs" (see https://press.fumadocs.dev/docs/layouts)
and wrap the page body there.
- MDX components, react-markdown/Streamdown renderers,
dangerouslySetInnerHTML with parsed markdown, `prose` classes, CMS
content renderers.
Present the candidates you find as a short list and ask the user which
surface should use typeset. Then wrap only the surface they pick:
<div className="typeset typeset-docs max-w-[37em]">
{content}
</div>
(`src/app.css` also defines `.typeset-measure` with the same
max-width, if a plain class is preferred over a Tailwind utility.)
If the picked surface already has its own typography (Fumadocs' `prose`
class, styled markdown components), list those styles and let the user
decide what to remove before wrapping. Note: `src/app.css` already zeroes
`margin-block-end` inside `.typeset.prose` so typeset owns the rhythm
when both classes share a container.
Notes:
- To exclude an embedded component from typeset styles, add the
`not-typeset` class or the `data-not-typeset` attribute to it. Keep
interactive Fumadocs components (tabs, callouts, accordions) out of typeset
this way.
- Verify on the surface the user picked: headings, lists, tables, and code
inside the container should be styled with no classes on the content
itself.
- Docs: https://ui.shadcn.com/docs/typeset