diff --git a/STYLE.md b/STYLE.md index 4cf93a34..7473be7f 100644 --- a/STYLE.md +++ b/STYLE.md @@ -135,7 +135,7 @@ Use a component for what it's for, and only where it helps the reader. | `Tabs` | Alternatives that aren't code, such as two ways to claim an instance | | `Accordion` | Entries a reader opens one at a time, such as troubleshooting. Never a reference list: a list a reader looks up is a plain table on its Catalog or Reference page | | `Files` | A directory a reader will see on disk | -| `Callout` | Something that must interrupt: an action that can't be undone, a step that loses data, or the Overview's one-line warning that Marfa is early | +| `Callout` | Something that must interrupt: an action that can't be undone, a step that loses data, or the Overview's one-line warning that Marfa is early. Every callout has a bold `title` that says what happens, and a short body under it. It draws no icon. `type="warn"` is for a loss or something that can't be undone; a neutral note takes the default, `info` | | `Cards` | Where to go next: the Overview, an index of the docs, and the end of a tutorial or of the Introduction | Write an object's fields as a Markdown table (field, type, description), not with `TypeTable`: a table reads the same on the page and in the Markdown copy. diff --git a/content/docs/(overview)/overview.mdx b/content/docs/(overview)/overview.mdx index 32997760..fb858f7a 100644 --- a/content/docs/(overview)/overview.mdx +++ b/content/docs/(overview)/overview.mdx @@ -5,7 +5,7 @@ description: Marfa is a shared data layer for your apps, and you run it yourself Your notes, tasks, bookmarks and files live in one instance, as typed items. Your apps, scripts and AI agents read and write them through one API, each reaching only what you allow, and apps built on the Swift SDK keep working offline. These docs cover running an instance and building on it. -Marfa is early, so expect breaking changes until version 1.0. +Expect breaking changes until version 1.0. ## Start here diff --git a/content/docs/guides/connector.mdx b/content/docs/guides/connector.mdx index 58145a20..98ffb2d7 100644 --- a/content/docs/guides/connector.mdx +++ b/content/docs/guides/connector.mdx @@ -285,7 +285,7 @@ key 01a15f19-6c20-7c68-a0b3-40bd8a431442 (readlist); the plaintext is shown once -Copy the key now. Marfa shows it only once. +Marfa shows it once. If you lose it, make a new one. The key writes the connector's one type and can register it, and its source matches the connector's `source`. **The connector refuses a key that reaches further**, such as your own. [Manage keys](/guides/manage-keys) explains each flag. diff --git a/content/docs/guides/define-a-type.mdx b/content/docs/guides/define-a-type.mdx index dd75cc39..0d665a3d 100644 --- a/content/docs/guides/define-a-type.mdx +++ b/content/docs/guides/define-a-type.mdx @@ -205,7 +205,7 @@ Otherwise it refuses with `409 type_in_use` when items hold the type or a settin A `type_in_use` refusal counts the items in `details.item_count` and names each folder, view and webhook setting in `details.settings`, without the `id` of a setting your key can't read. **[Move the items to another type](/guides/move-items-to-another-type) first:** the move changes those settings too. `details.grants` lists the keys and apps whose type map names the type, if your key may manage them. They don't stop a delete, so change them through [Manage keys](/guides/manage-keys) when you're ready. - + `force=true` (the CLI's `--force`), sent with a key, deletes a type that items still hold or a setting names. It doesn't override a subtype or a `compatible_with` claim. The items and settings stay as they are, but until the type is registered again, Marfa refuses any write to the items' properties, tier or time. You can still tag them, archive them or move them to the trash. diff --git a/content/docs/guides/housekeeping.mdx b/content/docs/guides/housekeeping.mdx index 426d1383..09021807 100644 --- a/content/docs/guides/housekeeping.mdx +++ b/content/docs/guides/housekeeping.mdx @@ -103,7 +103,7 @@ The server refuses `409 housekeeping_job_running` while a run of the same housek An item stays in the bin until it is purged. Left alone, `trash-purge` purges each item once it has been in the bin longer than `TRASH_RETENTION_DAYS`, which is 60 days unless you change it. `trash_retention_days` in the [configuration](/guides/settings#change-settings-while-it-runs) overrides that while the server runs, and a retention of `0` keeps every item. A run removes a limited number of items, and while items past the retention remain after a run that removed some, the job runs again at once. -A purge deletes items and their history for good. The owner’s empty-trash operation has no dry run. Read the trash before purging it. +It removes their history too. The owner's empty-trash operation has no dry run, so read the trash before you purge it. To empty the trash through the private socket, use the owner's summary operation. **Each call attempts up to 200 selected items** and returns the committed purge count, per-item failures and a cursor for the next batch. Send `--cursor` or the HTTP `cursor` query parameter with the returned value until `next_cursor` is `null`. If a write outcome is unknown, read the trash again before retrying. The owner can also empty it from a recent browser session. diff --git a/content/docs/guides/manage-keys.mdx b/content/docs/guides/manage-keys.mdx index baa6fc93..ded05b25 100644 --- a/content/docs/guides/manage-keys.mdx +++ b/content/docs/guides/manage-keys.mdx @@ -11,7 +11,7 @@ The owner makes keys in a browser or through the private socket. A key holding ` Open **API keys** from [Home](/guides/sign-in-as-the-owner#start-from-home) and choose **Create key**. **Details** holds the name and when the key ends. **Access starts with everything off**, so turn on only what the key needs. **Advanced** holds its sources and where its items land. -The page shows the key once, under **Copy your key**. Copy it before you leave the page. +The page shows it once, under **Copy your key**. Copy it before you leave the page. A saved key can only get less access or an earlier end. **Creating a key, saving a change to it and revoking it need a sign-in from the last five minutes**: the page asks the owner to [confirm it's them](/guides/sign-in-as-the-owner#confirm-its-you) when theirs is older. The socket, and a key with the permission for the operation, aren't held to it. @@ -101,7 +101,7 @@ An update can say it must only narrow. Add `"narrow_only": true` to `PATCH /keys Replace a key when its holder changes. -If a key may have leaked, revoke it first, then make a new one. Revoking leaves the keys it made working, so if it held `keys.mint`, find them in the [audit log](/guides/audit-log) and revoke them too. The steps below are for a planned change: they keep the old key working until the new one is in place. +Then make a new one. Revoking leaves the keys it made working, so if it held `keys.mint`, find them in the [audit log](/guides/audit-log) and revoke them too. The steps below are for a planned change: they keep the old key working until the new one is in place. 1. Make a key with the same access and a new `--source`, and give it to the app. 2. Check that the new key works. An hour later, `marfa keys list` should show the old key's last use before the switch. diff --git a/source.config.ts b/source.config.ts index acb8f032..6c29e04b 100644 --- a/source.config.ts +++ b/source.config.ts @@ -110,8 +110,7 @@ const llms: LLMsOptions = { return `- [${attribute(node, 'title') ?? ''}](${attribute(node, 'href') ?? ''})${text ? `: ${text}` : ''}`; } case 'Callout': { - const title = attribute(node, 'title'); - const body = `${title ? `**${title}.** ` : ''}${inner()}`; + const body = `**${attribute(node, 'title') ?? ''}.** ${inner()}`; return body .split('\n') .map((line) => (line ? `> ${line}` : '>')) diff --git a/src/components/callout.tsx b/src/components/callout.tsx index 6d5c1e56..68e288b2 100644 --- a/src/components/callout.tsx +++ b/src/components/callout.tsx @@ -1,17 +1,35 @@ import type { ComponentProps, ReactNode } from 'react'; import { cn } from '../lib/cn'; -// Fumadocs' Callout, drawn as a faint wash of its type's color with no border, -// shadow or icon: the tint says what kind of note it is, and the title, when -// there is one, says the rest. `icon` is accepted and not drawn. +// A callout is a soft tinted panel: a bold title in the tint's dark color, the +// body under it, no border, shadow or icon. The tint says what kind of note it +// is and the title says what happens. The colors are the `--marfa-callout-*` +// tokens in app.css, which other components reuse. type CalloutType = 'info' | 'warn' | 'warning' | 'error' | 'success' | 'tip' | 'idea'; +// Written out whole, so Tailwind finds each class. const TINTS = { - info: 'bg-fd-info/6', - warning: 'bg-fd-warning/8', - error: 'bg-fd-error/6', - success: 'bg-fd-success/6', + info: { + panel: 'bg-(--marfa-callout-info-bg)', + title: 'text-(--marfa-callout-info-title)', + body: 'text-(--marfa-callout-info-body)', + }, + warning: { + panel: 'bg-(--marfa-callout-warning-bg)', + title: 'text-(--marfa-callout-warning-title)', + body: 'text-(--marfa-callout-warning-body)', + }, + error: { + panel: 'bg-(--marfa-callout-error-bg)', + title: 'text-(--marfa-callout-error-title)', + body: 'text-(--marfa-callout-error-body)', + }, + success: { + panel: 'bg-(--marfa-callout-success-bg)', + title: 'text-(--marfa-callout-success-title)', + body: 'text-(--marfa-callout-success-body)', + }, }; function resolve(type: CalloutType) { @@ -23,28 +41,21 @@ function resolve(type: CalloutType) { export function Callout({ type = 'info', title, - icon: _icon, children, className, ...props }: Omit, 'title'> & { type?: CalloutType; - title?: ReactNode; - icon?: ReactNode; + title: ReactNode; }) { + const tint = TINTS[resolve(type)]; return (
- {title ?

{title}

: null} -
- {children} -
+

{title}

+
:first-child]:mt-0 [&>:last-child]:mb-0', tint.body)}>{children}
); } diff --git a/src/lib/content-lint.test.ts b/src/lib/content-lint.test.ts index ac4fae45..b5d49eb4 100644 --- a/src/lib/content-lint.test.ts +++ b/src/lib/content-lint.test.ts @@ -27,6 +27,7 @@ type Rule = | 'statement-number' | 'frontmatter' | 'coming-soon' + | 'callout-title' | 'link' | 'include'; @@ -250,6 +251,23 @@ export function checkComingSoon(file: string, text: string): Violation[] { return out; } +/** Every ``, outside code. */ +const CALLOUT_TAG = //])((?:"[^"]*"|'[^']*'|\{[^}]*\}|[^>"'{])*)>/g; + +export function checkCalloutTitles(file: string, text: string): Violation[] { + const out: Violation[] = []; + for (const match of stripCode(text).matchAll(CALLOUT_TAG)) { + if (/\stitle=("[^"]*[^"\s][^"]*"|'[^']*[^'\s][^']*'|\{[^}]*\S[^}]*\})/.test(match[1])) continue; + out.push({ + file, + line: lineOf(text, match.index), + rule: 'callout-title', + detail: 'a Callout needs a title that says what happens, such as title="Copy the key now"', + }); + } + return out; +} + /** Routes that serve text rather than a docs page, and are fine to link to. */ const NON_PAGE_ROUTES = new Set(['/', '/llms.txt', '/llms-full.txt', '/docs-map.md', '/mcp']); @@ -526,6 +544,10 @@ describe('content lint: the corpus', () => { it('links only to pages that exist', () => { expect(lintAll((file, text) => checkLinks(file, text, PAGE_URLS))).toEqual([]); }); + + it('gives every callout a title', () => { + expect(lintAll(checkCalloutTitles)).toEqual([]); + }); }); // ── Witnesses: each rule fires on a small bad sample ───────────────────── @@ -626,6 +648,23 @@ describe('content lint: each rule fires on a bad sample', () => { expect(rules(checkComingSoon('(overview)/run-an-instance.mdx', 'Coming soon.'))).toEqual(['coming-soon']); }); + it('callout-title', () => { + const bad = [ + 'Copy the key now.', + '\nBody.\n', + 'Body.', + 'Body.', + ]; + for (const sample of bad) expect(rules(checkCalloutTitles('x.mdx', sample))).toEqual(['callout-title']); + expect(checkCalloutTitles('x.mdx', 'Fine.\n\nNo title.\n')[0].line).toBe(3); + }); + + it('callout-title accepts a title and ignores code', () => { + expect(checkCalloutTitles('x.mdx', 'Body.')).toEqual([]); + expect(checkCalloutTitles('x.mdx', 'Body.')).toEqual([]); + expect(checkCalloutTitles('x.mdx', 'Write `` like this.\n\n```mdx\nBody\n```\n')).toEqual([]); + }); + describe('link', () => { const urls = new Set(['/overview', '/api-reference/items/createItem']); diff --git a/src/styles/app.css b/src/styles/app.css index e51a02a2..7795d054 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -52,6 +52,22 @@ --color-fd-secondary: #f4f4f3; --color-fd-accent: #efefee; --color-fd-popover: #ffffff; + + /* Callout tints, the Soft family: a panel, a title and a body color for each + kind of note, built from one hue and chroma. Other components that carry a + kind of note, such as a status chip, use the same tokens. */ + --marfa-callout-warning-bg: oklch(96.3% calc(.16 * 0.14) 60); + --marfa-callout-warning-title: oklch(38% calc(.16 * 0.55) 60); + --marfa-callout-warning-body: oklch(38% calc(.16 * 0.3) 60 / 0.85); + --marfa-callout-info-bg: oklch(96.3% calc(.17 * 0.14) 255); + --marfa-callout-info-title: oklch(38% calc(.17 * 0.55) 255); + --marfa-callout-info-body: oklch(38% calc(.17 * 0.3) 255 / 0.85); + --marfa-callout-error-bg: oklch(96.3% calc(.17 * 0.14) 25); + --marfa-callout-error-title: oklch(38% calc(.17 * 0.55) 25); + --marfa-callout-error-body: oklch(38% calc(.17 * 0.3) 25 / 0.85); + --marfa-callout-success-bg: oklch(96.3% calc(.12 * 0.14) 165); + --marfa-callout-success-title: oklch(38% calc(.12 * 0.55) 165); + --marfa-callout-success-body: oklch(38% calc(.12 * 0.3) 165 / 0.85); } .dark { --marfa-shadow-near: rgb(0 0 0 / 0.3); @@ -62,6 +78,18 @@ --color-fd-secondary: #1e1e1e; --color-fd-accent: #242424; --color-fd-popover: #181818; + --marfa-callout-warning-bg: oklch(26% calc(.16 * 0.12) 60); + --marfa-callout-warning-title: oklch(86% calc(.16 * 0.4) 60); + --marfa-callout-warning-body: oklch(86% calc(.16 * 0.22) 60 / 0.8); + --marfa-callout-info-bg: oklch(26% calc(.17 * 0.12) 255); + --marfa-callout-info-title: oklch(86% calc(.17 * 0.4) 255); + --marfa-callout-info-body: oklch(86% calc(.17 * 0.22) 255 / 0.8); + --marfa-callout-error-bg: oklch(26% calc(.17 * 0.12) 25); + --marfa-callout-error-title: oklch(86% calc(.17 * 0.4) 25); + --marfa-callout-error-body: oklch(86% calc(.17 * 0.22) 25 / 0.8); + --marfa-callout-success-bg: oklch(26% calc(.12 * 0.12) 165); + --marfa-callout-success-title: oklch(86% calc(.12 * 0.4) 165); + --marfa-callout-success-body: oklch(86% calc(.12 * 0.22) 165 / 0.8); } /* ── Type ──────────────────────────────────────────────────────────────────