Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
9 changes: 7 additions & 2 deletions content/docs/(overview)/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,7 +40,7 @@ The CLI is the `marfa` program. Open a second terminal and install it:
<Tabs items={["macOS", "Linux"]}>
<Tab value="macOS">

Download it from the terminal, as below.
Download it and put it on your path:

<include cwd>src/generated/cli-download.md</include>

Expand Down Expand Up @@ -89,6 +92,7 @@ Open `http://localhost:8600/setup`, enter the setup code the server printed, and
<Terminal>

```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
```
Expand All @@ -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
```

</Terminal>
Expand All @@ -117,6 +120,7 @@ Give the CLI the instance's address and your key, and check that it connects:
<Terminal>

```bash
# Replace <the key> with the key from the last step
export MARFA_API_URL=http://localhost:8600
export MARFA_API_KEY=<the key>
marfa status
Expand Down Expand Up @@ -169,6 +173,7 @@ The CLI prints the note's `id`, which Marfa made, its type, when it was made and
<Terminal>

```bash
# Replace <id> with the note's id
marfa items update <id> --version 1 --prop body="Start with the essays, then the letters."
```

Expand Down
12 changes: 11 additions & 1 deletion source.config.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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'] },
},
});
3 changes: 2 additions & 1 deletion src/components/mdx.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('Next<span>Create the project</span>');
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', () => {
Expand Down
107 changes: 48 additions & 59 deletions src/components/steps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <BaseSteps>{children}</BaseSteps>;
Expand All @@ -45,41 +46,16 @@ function PagedSteps({ children }: { children: ReactNode }) {
const steps = Children.toArray(children).filter(isValidElement) as ReactElement<WithChildren>[];
const titles = steps.map(titleOf);
const [current, setCurrent] = useState(0);
const [height, setHeight] = useState<number>();
const [measuring, setMeasuring] = useState(true);
const root = useRef<HTMLDivElement>(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(() => {
Expand All @@ -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 (
<div ref={root} className="marfa-paged" data-measuring={measuring || undefined}>
<div ref={root} className="marfa-paged">
<div className="marfa-paged-head">
<div className="marfa-paged-track">
{titles.map((title, index) => (
Expand All @@ -130,32 +106,45 @@ function PagedSteps({ children }: { children: ReactNode }) {
<span className="marfa-paged-count" aria-live="polite">
{current + 1} of {steps.length}
</span>
</div>
<div className="marfa-paged-body" style={{ minHeight: height }}>
{steps.map((step, index) => (
<div
key={index}
ref={(element) => {
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}
</div>
))}
</div>
<div className="marfa-paged-nav">
<button type="button" className="marfa-paged-prev" disabled={current === 0} onClick={() => go(current - 1)}>
Previous
<button
type="button"
className={iconButton}
aria-label="Previous step"
disabled={current === 0}
onClick={() => go(current - 1)}
>
<ChevronLeft strokeWidth={1.75} />
</button>
<button
type="button"
className={iconButton}
aria-label="Next step"
disabled={current === last}
onClick={() => go(current + 1)}
>
<ChevronRight strokeWidth={1.75} />
</button>
{current < steps.length - 1 ? (
<button type="button" className="marfa-paged-next" onClick={() => go(current + 1)}>
Next<span>{titles[current + 1]}</span>
</button>
) : null}
</div>
{steps.map((step, index) => (
<div
key={index}
ref={(element) => {
panels.current[index] = element;
}}
className="marfa-paged-step"
hidden={index !== current}
aria-label={`Step ${index + 1} of ${steps.length}`}
role="group"
>
{step.props.children}
</div>
))}
{current < last ? (
<button type="button" className="marfa-paged-next" onClick={() => go(current + 1)}>
Next: {titles[current + 1]}
<ChevronRight strokeWidth={1.75} aria-hidden="true" />
</button>
) : null}
</div>
);
}
75 changes: 20 additions & 55 deletions src/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -653,23 +653,24 @@ 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;
}
.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;
Expand Down Expand Up @@ -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 ────────────────────────────────────────────────────────────────
Expand Down
Loading