diff --git a/STYLE.md b/STYLE.md index 9031bc91..ad4c6149 100644 --- a/STYLE.md +++ b/STYLE.md @@ -128,7 +128,7 @@ Use a component for what it's for, and only where it helps the reader. | Component | Use it for | | --- | --- | | `Steps` | An ordered procedure: the Quickstart, the tutorials, a how-to with an order. Each step opens with a `###` heading | -| `Steps paged` | A tutorial: one step on screen at a time, with a progress bar and Previous and Next. Keep its steps close in length, since the panel is as tall as the longest. A how-to's few short steps stay a plain `Steps` list | +| `Steps paged` | A tutorial: one step on screen at a time, with a progress bar and Previous and Next arrows above it. Keep its steps close in length, so the page doesn't grow and shrink as the reader moves through them. A how-to's few short steps stay a plain `Steps` list | | `Terminal` | A command and what it prints (see Examples) | | Code tabs | The same example in more than one client (see Examples) | | `Tabs` | Alternatives that aren't code, such as two ways to claim an instance | diff --git a/content/docs/(overview)/quickstart.mdx b/content/docs/(overview)/quickstart.mdx index cf2e081b..f0dcabf0 100644 --- a/content/docs/(overview)/quickstart.mdx +++ b/content/docs/(overview)/quickstart.mdx @@ -15,11 +15,14 @@ By the end of this page you'll have a Marfa instance running on your computer, w Clone Marfa into a short path, such as your home directory, build it, and start the server: ```bash +# Get Marfa and build it cd ~ git clone https://github.com/withmarfa/marfa.git cd marfa pnpm install pnpm build + +# Start the server, with a secret that signs sign-ins cd packages/server export MARFA_AUTH_SECRET="$(openssl rand -hex 32)" pnpm dev @@ -37,7 +40,7 @@ The CLI is the `marfa` program. Open a second terminal and install it: -Download it from the terminal, as below. +Download it and put it on your path: src/generated/cli-download.md @@ -89,6 +92,7 @@ Open `http://localhost:8600/setup`, enter the setup code the server printed, and ```bash +# The private socket sits in the server's data folder cd ~/marfa/packages/server marfa --socket "$(pwd -P)/data/control/marfa.sock" keys create --label laptop --source laptop ``` @@ -100,7 +104,6 @@ key 01a15f24-3ade-75f0-8862-b72a2e8e85ab (laptop); the plaintext is shown once, Links between items Read and edit Definitions on your server Read and edit Profile Read and edit - Powers: Make, see, edit and revoke keys; See, edit, narrow and revoke every key; See and disconnect other apps; See and change Marfa's settings; See health, metrics, storage and job status; Run housekeeping, reset platform types and cancel bulk jobs; Replace or remove type definitions; Read and add any file, and manage file storage; Manage webhooks that send events to other services; See, manage and remove all connectors; Read the activity log; Delete items in the trash for good ``` @@ -117,6 +120,7 @@ Give the CLI the instance's address and your key, and check that it connects: ```bash +# Replace with the key from the last step export MARFA_API_URL=http://localhost:8600 export MARFA_API_KEY= marfa status @@ -169,6 +173,7 @@ The CLI prints the note's `id`, which Marfa made, its type, when it was made and ```bash +# Replace with the note's id marfa items update --version 1 --prop body="Start with the essays, then the letters." ``` diff --git a/source.config.ts b/source.config.ts index 30eb675b..acb8f032 100644 --- a/source.config.ts +++ b/source.config.ts @@ -1,4 +1,5 @@ import { defineConfig, defineDocs } from 'fumadocs-mdx/config'; +import { rehypeCodeDefaultOptions } from 'fumadocs-core/mdx-plugins'; import { pageSchema } from 'fumadocs-core/source/schema'; import { z } from 'zod'; import type { LLMsOptions } from 'fumadocs-core/mdx-plugins/remark-llms'; @@ -143,4 +144,13 @@ export const docs = defineDocs({ }, }); -export default defineConfig(); +// Shiki loads a language the first time a block uses it, and Markdown colours +// its frontmatter with YAML only when YAML is already loaded. The server and +// the browser compile pages in different orders, so a Markdown block could be +// coloured differently on each and fail hydration. Loading both up front gives +// every build the same highlighting. +export default defineConfig({ + mdxOptions: { + rehypeCodeOptions: { ...rehypeCodeDefaultOptions, langs: ['yaml', 'markdown'] }, + }, +}); diff --git a/src/components/mdx.test.tsx b/src/components/mdx.test.tsx index 19b598e1..1f889354 100644 --- a/src/components/mdx.test.tsx +++ b/src/components/mdx.test.tsx @@ -59,7 +59,8 @@ describe('MDX components', () => { expect(html).toContain('Make a key'); expect(html).toContain('Create the project'); expect(html).toContain('1 of 2'); - expect(html).toContain('NextCreate the project'); + expect(html).toContain('Next: Create the project'); + expect(html).toContain('aria-label="Next step"'); }); it('folds a long code block and leaves a short one whole', () => { diff --git a/src/components/steps.tsx b/src/components/steps.tsx index cdcac10c..2d4927db 100644 --- a/src/components/steps.tsx +++ b/src/components/steps.tsx @@ -10,16 +10,17 @@ import { type ReactElement, type ReactNode, } from 'react'; -import { cn } from '@/lib/cn'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { iconButton } from './site-header'; export const Step = BaseStep; /** * Fumadocs' Steps, or with `paged` one step at a time: a progress bar with the - * count, the step, and Previous and Next. Every step stays in the page, hidden - * with `hidden="until-found"`, so the browser's find and a link to a heading - * inside one still reach it, and the Markdown copy keeps them all. The panel - * is as tall as the tallest step, so the buttons don't move from step to step. + * count and Previous and Next arrows above the step, and a quiet link to the + * next step after it. Every step stays in the page, hidden with + * `hidden="until-found"`, so the browser's find and a link to a heading inside + * one still reach it, and the Markdown copy keeps them all. */ export function Steps({ paged, children }: { paged?: boolean; children: ReactNode }) { if (!paged) return {children}; @@ -45,41 +46,16 @@ function PagedSteps({ children }: { children: ReactNode }) { const steps = Children.toArray(children).filter(isValidElement) as ReactElement[]; const titles = steps.map(titleOf); const [current, setCurrent] = useState(0); - const [height, setHeight] = useState(); - const [measuring, setMeasuring] = useState(true); const root = useRef(null); const panels = useRef<(HTMLDivElement | null)[]>([]); - - // Lay every step out once, unseen, to find the tallest; again when the width changes. - useLayoutEffect(() => { - if (!measuring) return; - const tallest = Math.max(0, ...panels.current.map((panel) => panel?.offsetHeight ?? 0)); - // A step taller than the screen pushes the buttons out of view anyway, so - // the floor stops at most of a screen rather than leaving short steps - // with a screen of empty space. - setHeight(Math.min(tallest, Math.round(window.innerHeight * 0.7)) || undefined); - setMeasuring(false); - }, [measuring]); - - useEffect(() => { - let width = window.innerWidth; - const onResize = () => { - if (window.innerWidth === width) return; - width = window.innerWidth; - setMeasuring(true); - }; - window.addEventListener('resize', onResize); - return () => window.removeEventListener('resize', onResize); - }, []); + const last = steps.length - 1; // React writes `hidden` as a boolean, so the until-found value is set here. useLayoutEffect(() => { panels.current.forEach((panel, index) => { - if (!panel) return; - if (measuring || index === current) panel.removeAttribute('hidden'); - else panel.setAttribute('hidden', 'until-found'); + if (panel && index !== current) panel.setAttribute('hidden', 'until-found'); }); - }, [current, measuring]); + }, [current]); // A find or a link that lands in a hidden step opens it. useEffect(() => { @@ -105,13 +81,13 @@ function PagedSteps({ children }: { children: ReactNode }) { const go = useCallback((index: number) => { setCurrent(index); - const top = root.current?.getBoundingClientRect().top ?? 0; // Back to the top of the steps when the reader has scrolled past it, under the sticky header. + const top = root.current?.getBoundingClientRect().top ?? 0; if (top < 64) root.current?.scrollIntoView({ block: 'start' }); }, []); return ( -
+
{titles.map((title, index) => ( @@ -130,32 +106,45 @@ function PagedSteps({ children }: { children: ReactNode }) { {current + 1} of {steps.length} -
-
- {steps.map((step, index) => ( -
{ - panels.current[index] = element; - }} - className={cn('marfa-paged-step', index === current && 'marfa-paged-current')} - aria-label={`Step ${index + 1} of ${steps.length}`} - role="group" - > - {step.props.children} -
- ))} -
-
- + - {current < steps.length - 1 ? ( - - ) : null}
+ {steps.map((step, index) => ( +
{ + panels.current[index] = element; + }} + className="marfa-paged-step" + hidden={index !== current} + aria-label={`Step ${index + 1} of ${steps.length}`} + role="group" + > + {step.props.children} +
+ ))} + {current < last ? ( + + ) : null}
); } diff --git a/src/styles/app.css b/src/styles/app.css index 051eaf4b..ca2e18c5 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -653,9 +653,9 @@ div[data-orientation='horizontal']:has(> [role='tabpanel'] > figure.shiki:only-c } /* ── Steps, one at a time ───────────────────────────────────────────────── - A tutorial's steps as pages: a segmented progress bar and count, the step, - then Previous and Next. Every step shares one grid cell, and the body is as - tall as the tallest, so the buttons hold still. */ + A tutorial's steps as pages: a segmented progress bar, the count and the + Previous and Next arrows on one row above the step, where they stay in + view, and after the step a quiet link to the next one. */ .marfa-paged { margin-block: 1.5rem; scroll-margin-top: 5rem; @@ -663,13 +663,14 @@ div[data-orientation='horizontal']:has(> [role='tabpanel'] > figure.shiki:only-c .marfa-paged-head { display: flex; align-items: center; - gap: 0.75rem; + gap: 0.25rem; margin-bottom: 1.25rem; } .marfa-paged-track { display: flex; flex: 1; gap: 0.25rem; + margin-right: 0.5rem; } .marfa-paged-segment { position: relative; @@ -697,73 +698,37 @@ div[data-orientation='horizontal']:has(> [role='tabpanel'] > figure.shiki:only-c background-color: var(--color-fd-foreground); } .marfa-paged-count { + margin-right: 0.25rem; color: var(--color-fd-muted-foreground); font-size: 0.8125rem; font-variant-numeric: tabular-nums; white-space: nowrap; } -.marfa-paged-body { - display: grid; -} -.marfa-paged-step { - grid-area: 1 / 1; - min-width: 0; -} -.marfa-paged[data-measuring] .marfa-paged-step:not(.marfa-paged-current) { - visibility: hidden; +.marfa-paged-head > button:disabled { + opacity: 0.35; + pointer-events: none; } .marfa-paged-step > :first-child { margin-top: 0; } -.marfa-paged-nav { +.marfa-paged-next { display: flex; - justify-content: space-between; - gap: 0.75rem; + width: fit-content; + align-items: center; + gap: 0.25rem; margin-top: 1.5rem; - padding-top: 1rem; - border-top: 1px solid var(--color-fd-border); -} -.marfa-paged-nav button { - display: inline-flex; - align-items: baseline; - gap: 0.5rem; - min-width: 0; - padding: 0.5rem 0.875rem; - border-radius: var(--radius-md); + margin-inline-start: auto; + color: var(--color-fd-muted-foreground); font-size: 0.875rem; font-weight: 500; - transition: background-color 0.15s; -} -.marfa-paged-prev { - box-shadow: inset 0 0 0 1px var(--color-fd-border); - color: var(--color-fd-foreground); -} -.marfa-paged-prev:hover:not(:disabled) { - background-color: var(--color-fd-accent); -} -.marfa-paged-prev:disabled { - opacity: 0.4; - cursor: default; -} -.marfa-paged-next { - margin-inline-start: auto; - background-color: var(--color-fd-accent); - color: var(--color-fd-foreground); + transition: color 0.15s; } .marfa-paged-next:hover { - background-color: color-mix(in oklab, var(--color-fd-foreground) 12%, transparent); -} -.marfa-paged-next > span { - overflow: hidden; - color: var(--color-fd-muted-foreground); - font-weight: 400; - text-overflow: ellipsis; - white-space: nowrap; + color: var(--color-fd-foreground); } -@media (width < 40rem) { - .marfa-paged-next > span { - display: none; - } +.marfa-paged-next > svg { + width: 1rem; + height: 1rem; } /* ── Focus ────────────────────────────────────────────────────────────────