mirror of
https://github.com/prdlk/leetcode.git
synced 2026-08-02 17:31:40 +00:00
docs(docs): migrate to array based folder structure
This commit is contained in:
+2
-4
@@ -1,4 +1,2 @@
|
|||||||
node_modules
|
.blume/
|
||||||
dist
|
dist/
|
||||||
.source
|
|
||||||
.vercel
|
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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": {}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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`.
|
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
@@ -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();
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
export { cn } from 'cnfast';
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"]
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user