diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4282300 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +NEXT_PUBLIC_SITE_URL=https://dripnex.app +NEXT_PUBLIC_DOCS_URL=https://docs.dripnex.app +NEXT_PUBLIC_DEVELOPERS_URL=https://developers.dripnex.app diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6ebe4e3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: pnpm/action-setup@v5 + - uses: actions/setup-node@v5 + with: + node-version: '22' + cache: 'pnpm' + - run: pnpm install --frozen-lockfile + - run: pnpm typecheck + - run: pnpm build + env: + NEXT_PUBLIC_SITE_URL: https://dripnex.app + NEXT_PUBLIC_DOCS_URL: https://docs.dripnex.app + NEXT_PUBLIC_DEVELOPERS_URL: https://dripnex-developers.pages.dev diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b4d1521 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.next/ +.source/ +node_modules/ +out/ +.vercel +.env +.env.local +*.tsbuildinfo +.wrangler/ diff --git a/README.md b/README.md index 8e8f86a..5be6a98 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ -# developers -Dripnex developer docs — plugin and theme API (Fumadocs). Mirrors developers.inkdrop.app. +# Dripnex developers + +Plugin and theme API docs for Dripnex. Fumadocs + Next.js static export on Cloudflare Pages. + +Modeled on [developers.inkdrop.app](https://developers.inkdrop.app/). The user manual lives in [`dripnex/docs-site`](https://github.com/dripnex/docs-site) ([docs.dripnex.app](https://docs.dripnex.app)). Marketing lives in [`dripnex/marketing`](https://github.com/dripnex/marketing). + +The API described here is `@dripnex/plugin-api` in [`dripnex/app`](https://github.com/dripnex/app) (`packages/plugin-api`, default branch `develop`). `PLUGIN_API_VERSION` is `"1"`. + +## Develop + +```bash +pnpm install +pnpm dev +``` + +Content is MDX under `content/docs/`. The site is served at `/`. + +## Deploy + +```bash +NEXT_PUBLIC_SITE_URL=https://dripnex.app \ +NEXT_PUBLIC_DOCS_URL=https://docs.dripnex.app \ +NEXT_PUBLIC_DEVELOPERS_URL=https://dripnex-developers.pages.dev \ +pnpm deploy +``` + +Pages project: `dripnex-developers` → https://dripnex-developers.pages.dev + +When DNS is ready, CNAME `developers` → `dripnex-developers.pages.dev` (proxied). diff --git a/app/[[...slug]]/page.tsx b/app/[[...slug]]/page.tsx new file mode 100644 index 0000000..7ae8e29 --- /dev/null +++ b/app/[[...slug]]/page.tsx @@ -0,0 +1,61 @@ +import { notFound } from 'next/navigation'; +import { DocsPage, DocsBody, DocsTitle, DocsDescription } from 'fumadocs-ui/page'; +import defaultMdxComponents from 'fumadocs-ui/mdx'; +import { Card, Cards } from 'fumadocs-ui/components/card'; +import { Callout } from 'fumadocs-ui/components/callout'; +import { Step, Steps } from 'fumadocs-ui/components/steps'; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; +import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; +import { File, Folder, Files } from 'fumadocs-ui/components/files'; +import { TypeTable } from 'fumadocs-ui/components/type-table'; +import { source } from '@/lib/source'; + +const mdxComponents = { + ...defaultMdxComponents, + Card, + Cards, + Callout, + Step, + Steps, + Tab, + Tabs, + Accordion, + Accordions, + File, + Folder, + Files, + TypeTable, +}; + +export default async function Page(props: { params: Promise<{ slug?: string[] }> }) { + const params = await props.params; + const page = source.getPage(params.slug); + if (!page) notFound(); + + const MDX = page.data.body; + + return ( + + {page.data.title} + {page.data.description} + + + + + ); +} + +export function generateStaticParams() { + return source.generateParams(); +} + +export async function generateMetadata(props: { params: Promise<{ slug?: string[] }> }) { + const params = await props.params; + const page = source.getPage(params.slug); + if (!page) notFound(); + + return { + title: page.data.title, + description: page.data.description, + }; +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..5de9cf3 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,135 @@ +@import 'tailwindcss'; +@import 'fumadocs-ui/css/neutral.css'; +@import 'fumadocs-ui/css/preset.css'; + +@theme { + --color-background: #09090b; + --color-surface: #111113; + --color-surface-elevated: #1a1a1f; + --color-inset: #0c0c0e; + --color-border: #27272a; + --color-border-accent: rgba(255, 255, 255, 0.12); + + --color-text-primary: #fafafa; + --color-text-secondary: #a1a1aa; + --color-text-muted: #52525b; + + --color-accent: #a1a1aa; + --color-accent-hover: #d4d4d8; + --color-accent-glow: rgba(255, 255, 255, 0.06); + + --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif; + --font-mono: 'JetBrains Mono Variable', ui-monospace, monospace; +} + +:root, +.dark { + --rd-violet: #8b5cf6; + --rd-violet-light: #a78bfa; + --rd-violet-lighter: #c4b5fd; + --rd-violet-glow-10: rgba(139, 92, 246, 0.1); + --rd-violet-glow-12: rgba(139, 92, 246, 0.12); + --rd-violet-glow-20: rgba(139, 92, 246, 0.2); + --rd-violet-glow-30: rgba(139, 92, 246, 0.3); + --rd-violet-glow-08: rgba(139, 92, 246, 0.08); + --rd-surface: #111113; + --rd-inset: #0c0c0e; + --rd-foreground: #fafafa; + --rd-muted-foreground: #a1a1aa; + --rd-subtle-foreground: #e4e4e7; + --rd-faint: #52525b; + --rd-border-subtle: rgba(255, 255, 255, 0.06); + --rd-border: rgba(255, 255, 255, 0.08); + + --color-fd-background: #09090b; + --color-fd-foreground: var(--rd-foreground); + --color-fd-muted: var(--rd-surface); + --color-fd-muted-foreground: var(--rd-muted-foreground); + --color-fd-popover: var(--rd-surface); + --color-fd-popover-foreground: var(--rd-subtle-foreground); + --color-fd-card: var(--rd-surface); + --color-fd-card-foreground: var(--rd-foreground); + --color-fd-border: var(--rd-border); + --color-fd-primary: var(--rd-violet); + --color-fd-primary-foreground: #ffffff; + --color-fd-secondary: #1a1a1f; + --color-fd-secondary-foreground: var(--rd-subtle-foreground); + --color-fd-accent: var(--rd-violet-glow-12); + --color-fd-accent-foreground: var(--rd-subtle-foreground); + --color-fd-ring: var(--rd-violet); +} + +.fd-sidebar { + --fd-sidebar-width: 260px; +} + +.fd-sidebar [data-active='true'] { + color: var(--rd-violet) !important; +} + +nav[data-fumadocs] { + border-bottom: 1px solid var(--rd-border-subtle); +} + +pre:has(code) { + background: var(--rd-inset) !important; + border: 1px solid var(--rd-border-subtle); + border-radius: 0.75rem; +} + +:not(pre) > code { + background: var(--rd-violet-glow-10) !important; + color: var(--rd-violet-lighter) !important; + border: 1px solid var(--rd-violet-glow-20); + border-radius: 0.375rem; + padding: 0.125rem 0.375rem; + font-size: 0.875em; +} + +[data-toc] a[data-active='true'] { + color: var(--rd-violet); + border-left-color: var(--rd-violet); +} + +.fd-card { + background: var(--rd-surface); + border-color: var(--rd-border); + transition: + border-color 0.2s, + box-shadow 0.2s; +} + +.fd-card:hover { + border-color: var(--rd-violet-glow-30); + box-shadow: 0 0 30px var(--rd-violet-glow-08); +} + +[data-fumadocs-search] { + --color-fd-background: var(--rd-inset); + --color-fd-popover: var(--rd-surface); +} + +nav[aria-label='Breadcrumb'] { + color: var(--rd-faint); +} + +nav[aria-label='Breadcrumb'] a:hover { + color: var(--rd-violet); +} + +.fd-page h1, +.fd-page h2, +.fd-page h3, +.fd-page h4 { + color: var(--rd-foreground); +} + +.fd-page a:not([class]) { + color: var(--rd-violet-light); + text-decoration-color: var(--rd-violet-glow-30); +} + +.fd-page a:not([class]):hover { + color: var(--rd-violet-lighter); + text-decoration-color: var(--rd-violet); +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..62ad604 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,76 @@ +import './globals.css'; +import '@fontsource/inter/400.css'; +import '@fontsource/inter/500.css'; +import '@fontsource/inter/600.css'; +import '@fontsource/inter/700.css'; +import '@fontsource-variable/jetbrains-mono'; +import { RootProvider } from 'fumadocs-ui/provider/next'; +import { DocsLayout } from 'fumadocs-ui/layouts/docs'; +import type { ReactNode } from 'react'; +import type { Metadata } from 'next'; +import { source } from '@/lib/source'; +import { baseOptions } from '@/lib/layout.shared'; +import { DEVELOPERS_URL, DOCS_URL } from '@/lib/site'; + +export const metadata: Metadata = { + metadataBase: new URL(DEVELOPERS_URL), + title: { + default: 'Dripnex Developers', + template: '%s | Dripnex Developers', + }, + description: + 'Plugin and theme API for Dripnex — extend the desktop editor with commands, CodeMirror, layout zones, and palettes.', + applicationName: 'Dripnex Developers', + icons: { + icon: [ + { url: '/favicon.ico' }, + { url: '/favicon.png', type: 'image/png', sizes: '32x32' }, + { url: '/icon.png', type: 'image/png', sizes: '512x512' }, + ], + apple: [{ url: '/apple-touch-icon.png', sizes: '180x180' }], + }, + openGraph: { + type: 'website', + url: DEVELOPERS_URL, + siteName: 'Dripnex Developers', + images: [{ url: '/icon.png', width: 512, height: 512, alt: 'Dripnex' }], + }, + twitter: { + card: 'summary', + images: ['/icon.png'], + }, +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + + +

+ Desktop plugin API.{' '} + + User manual + +

+ + ), + }} + > + {children} +
+
+ + + ); +} diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 0000000..96a745f --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,19 @@ +import Link from 'next/link'; + +export default function NotFound() { + return ( +
+

404

+

This page wandered off

+

+ That API page doesn't exist, or the URL changed. +

+ + Back to the API + +
+ ); +} diff --git a/content/docs/getting-started/index.mdx b/content/docs/getting-started/index.mdx new file mode 100644 index 0000000..cadc4a0 --- /dev/null +++ b/content/docs/getting-started/index.mdx @@ -0,0 +1,161 @@ +--- +title: Plugin structure +description: How a Dripnex plugin repo is laid out — manifest, main, and package files +--- + +# Plugin structure + +A community plugin is **its own git repository**. It is not a folder inside `dripnex/app`. Version is the git tag. The install artifact is a tarball from `dripnex-plugin pack` attached to a GitHub release. + +## What Dripnex loads + +Two worlds share one `PluginHost`: + +| Kind | Where it lives | How it activates | +| ---- | -------------- | ---------------- | +| **Built-in** | Shipped in the desktop bundle | Enabled unless `plugin_registry.enabled === false` | +| **Discovered** | `userData/plugins//` | Scanner → `loadPluginFromSource()` → same host | +| **Hackable files** | Data directory (`init.js`, `styles.css`, `keybindings.json`) | `init.js` loads as plugin `user-init` | + +Discovered packs always live in user data. There is no extraResources plugin tree in the app bundle. + +### Plugins directory (packaged desktop) + +| OS | Path | +| -- | ---- | +| macOS | `~/Library/Application Support/Dripnex/plugins` | +| Linux | `~/.config/Dripnex/plugins` | +| Windows | `%APPDATA%/Dripnex/plugins` | + +Override with `DRIPNEX_DATA_DIR` or `--user-data-dir` if the CLI and the app disagree about the folder. + +## Repo layout + +`dripnex-plugin init "My Plugin"` writes: + + + + + + + + + + + + + + + + + + + + + + + + +On disk after install, the scanner expects: + +``` +userData/plugins// + manifest.json # id, name, version, main?, configSchema? +
# usually dist/index.js + keymaps/ menus/ styles/ themes/ # optional + theme.json # optional palette +``` + +A folder is skipped unless `manifest.main` exists **or** it is theme-only (`theme.json` / `themes/`). + +## Manifest + +`manifest.json` is the on-disk identity. The **evaluated** module must export the same `id` as `manifest.json`. + +```json +{ + "id": "stamp", + "name": "Stamp", + "version": "0.1.0", + "description": "Insert the current date or timestamp at the cursor.", + "main": "dist/index.js" +} +``` + +| Field | Required | Rules | +| ----- | -------- | ----- | +| `id` | Yes | Kebab-case (`^[a-z][a-z0-9]*(-[a-z0-9]+)*$`). Install folder name. | +| `name` | Yes | Display name | +| `version` | Yes | Semver `major.minor.patch` (optional pre-release / build) | +| `description` | No | Short summary | +| `main` | For JS packs | Path to the bundled CommonJS file | +| `apiVersion` | No | Target plugin API major (current: `"1"`) | +| `configSchema` | No | Settings fields rendered in Settings → Plugins | +| `dependencies` | No | `pluginId` → semver range of other packs | + +`id`, `name`, `version`, and `activate` are also validated on the **exported** module (`validateManifest` in `@dripnex/plugin-api`). + +## The module must be CommonJS + +The renderer evaluates `main` with `new Function('module', 'exports', 'require', code)`. The export must be: + +```js +module.exports = { + id: 'stamp', + name: 'Stamp', + version: '0.1.0', + activate(context) { + /* … */ + return { dispose() { /* unregister */ } }; + }, +}; +``` + +If the filesystem `manifest.json` `id` and `module.exports.id` disagree, the pack is rejected. + +Host `require()` is a whitelist. Anything else must be **bundled into** `index.js`: + +- `react`, `react-dom`, `react/jsx-runtime` +- `@codemirror/state`, `@codemirror/view`, `@codemirror/language`, `@codemirror/commands`, `@codemirror/search` +- `@dripnex/plugin-api` + +That is why [dripnex/plugin-vim](https://github.com/dripnex/plugin-vim) bundles `@replit/codemirror-vim` instead of requiring it. + +## Declarative package files + +Applied **after** `activate()` from `packages/plugin-api` package-file parsers: + +| Path | Role | +| ---- | ---- | +| `keymaps/*.json` | Default chords for commands this pack already registered (`plugin::…` only) | +| `menus/*.json` | Plugins menu + context menus | +| `styles/*.css` | Injected stylesheets | +| `theme.json` / `themes/*.json` | Token palettes | + +Keymap chords only bind if the command was registered in `activate()`. They cannot rebind core `app:` / `editor:` commands. + +## Plugin contract + +Keep markdown portable. These are product rules, not extra host checks: + +1. **Removable without breaking `.md` files** — uninstall and notes stay valid markdown. +2. **No new syntax** other editors cannot read. +3. **No automatic content mutation** — never rewrite the user's markdown without an explicit command. +4. **Not required to interpret text** — notes must be readable without the pack. +5. **No inter-note dependencies** that break if a note is moved or deleted. + +## Official satellites (verified) + +Install with `dripnex-plugin install owner/repo` or **Settings → Plugins → Connect**. Manifest id, GitHub repo, and any registry slug are **not** interchangeable. + +| Repo | Manifest id | What it actually does | +| ---- | ----------- | --------------------- | +| [dripnex/plugin-stamp](https://github.com/dripnex/plugin-stamp) | `stamp` | Insert date / timestamp at the cursor | +| [dripnex/plugin-mermaid](https://github.com/dripnex/plugin-mermaid) | `mermaid` | Insert a ` ```mermaid ` fence. **No renderer.** Diagrams are the built-in `dripnex-mermaid` pack. | +| [dripnex/plugin-math](https://github.com/dripnex/plugin-math) | `math` | Insert `$$…$$`. **No KaTeX.** Math rendering is the built-in `dripnex-math` pack. | +| [dripnex/plugin-vim](https://github.com/dripnex/plugin-vim) | `dripnex-vim-mode` | Real `@replit/codemirror-vim`. Install spec is `dripnex/plugin-vim`, not the manifest id. | +| [dripnex/theme-parchment](https://github.com/dripnex/theme-parchment) | `theme-parchment` | Official warm-paper palette. Many other palettes live in `dripnex/theme-*` repos. | + +Built-in mermaid, math, and tables **renderers** ship in the app. Do not treat the mermaid/math satellites as those renderers. + +Next: the [init file](/getting-started/init-file) and [style tweaks](/getting-started/style-tweaks), then [create a plugin](/guides/create-a-plugin). diff --git a/content/docs/getting-started/init-file.mdx b/content/docs/getting-started/init-file.mdx new file mode 100644 index 0000000..e46df66 --- /dev/null +++ b/content/docs/getting-started/init-file.mdx @@ -0,0 +1,86 @@ +--- +title: The init file +description: Customize Dripnex on startup with init.js — the host PluginContext, not Inkdrop’s Atom APIs +--- + +# The init file + +`init.js` is a **user file** in the data directory, not a plugin repo. Open it from **Settings → Plugins**. Dripnex writes a template on first open. + +It is the same idea as Inkdrop's init file: run JavaScript at startup. The object you get is **Dripnex's** host API (`createInitApi` in `@dripnex/plugin-api`), not Inkdrop's Atom-style `inkdrop` global. + +## Two formats + +`loadInitScript()` accepts: + +1. **Free-form (default).** The file is wrapped as plugin `user-init` and runs at activate with a `dripnex` argument. +2. **CommonJS `PluginManifest`.** If the file assigns `module.exports` / `exports.*` and looks like a manifest (`id`, `activate`, `name`, or `version`), it is validated and loaded as a normal pack. + +Free-form is what the default template uses (`dripnex.menu.add`, `dripnex.commands.add`). Do not assume a browser `window.dripnex` with a `.menu` — the host injects the init API as the `dripnex` function argument. + +```js +dripnex.commands.add('paste-as-link', 'Paste as Link', () => { + const { from, to } = dripnex.editor.getSelection(); + const text = dripnex.editor.getContent().slice(from, to); + dripnex.editor.replaceRange(from, to, '[' + text + '](url)'); +}); +``` + +Reload plugins (**Settings → Plugins → Reload**) after saving. + +## The `dripnex` object (`InitApi`) + +| Surface | Notes | +| ------- | ----- | +| `editor`, `app`, `data`, `log`, `config`, `layout`, `decorations` | Same as `PluginContext` | +| `store` | Read-only snapshot. **No** `store.dispatch`. | +| `commands.add(id, name, execute, options?)` | Registers a command (palette) | +| `commands.dispatch(id, payload?)` | Host or plugin command (`app:save-note`, `plugin:…`) | +| `registerCommand`, `registerExtensions`, `registerAiCommand` | Same as context | +| `registerCssVariables`, `registerTheme`, `themes` | Palettes | +| `registerRemarkPlugin`, `registerRehypePlugin`, `registerPreviewComponent`, `registerCodeBlockRenderer` | Preview pipeline | +| `menu`, `clipboard`, `notifications`, `contextMenu`, `preview`, `components`, `markdownRenderer` | Same as context | +| `vim` | `@replit/codemirror-vim` `Vim` object **when** [plugin-vim](https://github.com/dripnex/plugin-vim) is loaded; otherwise unavailable | +| `getActiveEditor()` | `{ editor, cm }` where `cm` is `editor.getView()` (live CodeMirror 6 view or `null`) | + +There is no Inkdrop `inkdrop.packages`, `CompositeDisposable`, or Atom command registry. Map keys with `dripnex.commands.add` / `registerCommand`, or with `Vim.map` after the Vim pack has called `registerVim`. + +## Read-only store + +```js +const { editingNote, notes, navigation } = dripnex.store.getState(); + +const unsub = dripnex.store.subscribe(() => { + const { editingNote: next } = dripnex.store.getState(); + dripnex.log.debug(next.id, next.isDirty); +}); +``` + +- `notes.items` is the **visible list in this window**, not the whole library. Query with `dripnex.app.listNotes()` or `dripnex.data.getNotes()`. +- Mutate through `dripnex.commands.dispatch`, `editor`, or `data`. +- `settings` is appearance only (theme, accent, zoom). It never includes API keys. + +## Vim maps (after installing Vim) + +Vim is **not** built-in. Install `dripnex/plugin-vim` (manifest id `dripnex-vim-mode`) and enable it. The pack publishes `dripnex.vim` via `registerVim` so init can map keys: + +```js +const Vim = dripnex.vim; +if (Vim) { + Vim.map('jj', '', 'insert'); + Vim.map('Y', 'y$'); + Vim.defineEx('find', 'f', () => { + void dripnex.commands.dispatch('app:focus-search'); + }); +} +``` + +## Also in the data directory + +| File | Role | +| ---- | ---- | +| `init.js` | This page | +| `styles.css` | [Style tweaks](/getting-started/style-tweaks) | +| `keybindings.json` | Command id → chord, or `null` to unbind. Contexts: `editor`, `note-list`, `app`, `global`. | + +`j` / `k` in `note-list` move to next/prev note and do **not** fire in the editor (so they stay free for Vim). diff --git a/content/docs/getting-started/meta.json b/content/docs/getting-started/meta.json new file mode 100644 index 0000000..1bbf0f6 --- /dev/null +++ b/content/docs/getting-started/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Getting started", + "pages": ["index", "init-file", "style-tweaks"] +} diff --git a/content/docs/getting-started/style-tweaks.mdx b/content/docs/getting-started/style-tweaks.mdx new file mode 100644 index 0000000..b12b40c --- /dev/null +++ b/content/docs/getting-started/style-tweaks.mdx @@ -0,0 +1,59 @@ +--- +title: Style tweaks +description: Personal CSS and theme tokens without shipping a full theme pack +--- + +# Style tweaks + +`styles.css` in the data directory is injected into every renderer window. Save to apply. Open it from **Settings → Plugins** (written on first open, same as `init.js`). + +Use this for personal chrome. To **share** a palette, ship a [theme pack](/guides/create-a-theme). + +## CSS variables the app already uses + +Themes (and `styles.css`) may set tokens from `CORE_THEME_TOKENS` in `@dripnex/plugin-api`. `--accent-primary` is the chrome accent (buttons, settings). A palette that only sets `--accent` gets `--accent-primary` copied in `validateThemeTokens`. + +**Core tokens** + +`--bg-base`, `--bg-surface`, `--bg-elevated`, `--bg-inset`, `--bg-hover`, `--bg-active`, `--accent`, `--accent-primary`, `--accent-hover`, `--accent-muted`, `--accent-subtle`, `--text-primary`, `--text-secondary`, `--text-muted`, `--text-faint`, `--border`, `--border-subtle`, `--border-strong`, `--glass-bg`, `--glass-border`, `--glass-bg-menu`, `--glass-border-menu`, `--danger`, `--danger-muted`, `--warning`, `--warning-muted`, `--success`, `--success-muted`, `--status-active`, `--status-on-hold`, `--status-completed`, `--status-dropped` + +**Extension scopes** (`THEME_EXTENSION_SCOPES`) — any token starting with: + +`--syntax-`, `--preview-`, `--ui-`, `--cm-`, `--md-`, `--mde-` + +Unknown names are rejected for **registered themes**. Your personal `styles.css` is injected as raw CSS, so you can also target existing classes — keep selectors specific so they survive app updates. + +```css +:root { + --accent: #2a7d6f; + --accent-primary: #2a7d6f; +} + +.cm-editor { + font-size: 15px; +} +``` + +## From a plugin or init.js + +Packs can register variables without a `theme.json`: + +```js +dripnex.registerCssVariables('my-tweaks', { + '--accent': '#8b5cf6', + '--cm-link': '#a78bfa', +}); +``` + +`registerTheme` / `theme.json` go through the whitelist. Invalid tokens are dropped with a console warning. + +## Theme packs vs style tweaks + +| | `styles.css` | Theme pack | +| - | ------------ | ---------- | +| Audience | You | Anyone who installs `owner/repo` | +| Activation | Always, for your user data | Settings → Themes | +| Token validation | Raw CSS | `CORE_THEME_TOKENS` + extension scopes | +| Example | A larger editor font | [dripnex/theme-parchment](https://github.com/dripnex/theme-parchment) | + +Settings → Themes lists the default `tokens.css` plus installed plugin themes. diff --git a/content/docs/guides/create-a-plugin.mdx b/content/docs/guides/create-a-plugin.mdx new file mode 100644 index 0000000..6878381 --- /dev/null +++ b/content/docs/guides/create-a-plugin.mdx @@ -0,0 +1,162 @@ +--- +title: Create a plugin +description: Scaffold, activate, bundle to CommonJS, and load a Dripnex plugin from disk +--- + +# Create a plugin + +This walkthrough builds a pack you can load locally. The real first-party example is [dripnex/plugin-stamp](https://github.com/dripnex/plugin-stamp): CommonJS, `menu.add`, `editor.insertAtCursor`. + +## 1. Scaffold + +```bash +dripnex-plugin init "Hello Stamp" +cd hello-stamp +npm install +``` + +From the app monorepo you can also run `pnpm plugin init "Hello Stamp"`. + +`init` without `--type theme` creates `manifest.json`, `src/index.ts` (CJS export), `keymaps/`, `menus/`, and `styles/`. + +## 2. Write `activate` + +The host evaluates a **pre-bundled CommonJS** file. The CLI template already uses `module.exports`. Stamp's source (trimmed) is the shape you want: + +```js +module.exports = { + id: 'stamp', + name: 'Stamp', + version: '0.1.0', + description: 'Insert the current date or timestamp at the cursor.', + + activate(context) { + const removeDate = context.menu.add({ + label: 'Insert Date', + accelerator: 'Mod+Shift+T', + click: () => { + const now = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + context.editor.insertAtCursor( + `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}` + ); + return true; + }, + }); + + return { + dispose() { + removeDate(); + }, + }; + }, +}; +``` + +Keep `id` / `name` / `version` in sync with `manifest.json`. Return `{ dispose() }` and unregister everything you added. + +TypeScript is fine if your bundler emits CJS `module.exports`. Named ESM `export const plugin` is **not** what `loadPluginFromSource` reads. + +## 3. Commands and layout (optional) + +Commands land in the palette as `plugin::`: + +```js +const off = context.registerCommand( + { + id: 'hello', + name: 'Say Hello', + category: 'Hello Stamp', + keybinding: { key: 'H', modifiers: ['Mod', 'Shift'] }, + icon: 'Smile', + }, + () => { + context.notifications.addInfo('Hello from the plugin.'); + return true; + } +); +``` + +Mount UI with `context.layout.addComponent`. Zones are listed in [Layout zones](/reference/layout-zones). Status bar example: + +```js +context.layout.addComponent('editor-status-bar', { + id: 'hello-stamp:status', + component: function Status() { + return 'Hello'; + }, + order: 30, +}); +``` + +Community packs conventionally use `order` 30–99. + +## 4. Config schema + +Fields in `configSchema` render under **Settings → Plugins**: + +```js +module.exports = { + id: 'hello-stamp', + name: 'Hello Stamp', + version: '0.1.0', + configSchema: { + includeTime: { + type: 'boolean', + default: false, + description: 'Append local time', + }, + }, + activate(context) { + const includeTime = context.config.get('includeTime') ?? false; + const stop = context.config.observe('includeTime', (value) => { + context.log.info('includeTime', value); + }); + return { + dispose() { + stop(); + }, + }; + }, +}; +``` + +`config.get` is sync. `config.set` persists. `config.observe` fires when Settings (or another window) changes the value. + +Field types: `string`, `number`, `boolean`, `enum` (`options: [{ value, label }]`), `range` (`min`, `max`, `step`). + +## 5. Build and install locally + +```bash +npm run build +dripnex-plugin install . +``` + +Or `dripnex-plugin link` / `pnpm plugin link` from a monorepo checkout. Then **Settings → Plugins → Reload**. + +The pack must appear under `…/Dripnex/plugins//`. If the CLI printed success but the app shows nothing, set `DRIPNEX_DATA_DIR` to the same userData the packaged app uses. + +CLI `install` **refuses** if `` is already installed. Settings overwrite is allowed. To refresh from CLI: uninstall, then install again. + +## 6. Lifecycle + +``` +scan manifest.json → eval main → activate(context) → apply keymaps/menus/styles/themes → dispose() +``` + +`PluginRegistry` unsubscribes leftover `editor.on*` / `app.on*` listeners on deactivate. Still call your own `dispose()`. + +## Five rules + +1. Removable without breaking `.md` files. +2. No invented markdown other editors cannot read. +3. No automatic content mutation. +4. Notes stay readable without the pack. +5. No brittle inter-note graph that the pack owns. + +## Next + +- [Create a theme](/guides/create-a-theme) +- [Publish](/guides/publishing) (`dripnex-plugin pack` → git tag → GitHub release) +- [PluginContext](/reference/plugin-context) +- [Examples](/reference/examples) diff --git a/content/docs/guides/create-a-theme.mdx b/content/docs/guides/create-a-theme.mdx new file mode 100644 index 0000000..7dd0e28 --- /dev/null +++ b/content/docs/guides/create-a-theme.mdx @@ -0,0 +1,115 @@ +--- +title: Create a theme +description: Ship a Dripnex palette with theme.json, CORE_THEME_TOKENS, and THEME_EXTENSION_SCOPES +--- + +# Create a theme + +A theme pack is a git repo that contributes CSS variables. Official palettes live in `dripnex/theme-*` repositories. [dripnex/theme-parchment](https://github.com/dripnex/theme-parchment) is the reference: pack id `theme-parchment`, palette id `dripnex-parchment`. + +You do **not** need JavaScript. The scanner treats a folder as theme-only when `theme.json` or `themes/` is present even if `manifest.main` is missing. + +## Scaffold + +```bash +dripnex-plugin init "Harbor Dusk" --type theme +cd harbor-dusk +``` + +That writes `manifest.json`, `theme.json`, and `styles/index.css` (no `src/`). + +## `manifest.json` + +```json +{ + "id": "theme-harbor-dusk", + "name": "Harbor Dusk", + "version": "0.1.0", + "description": "A Dripnex theme: Harbor Dusk" +} +``` + +`id` is kebab-case. It is the install folder name. It does not have to equal the palette id inside `theme.json`. + +There is no `themeType` field on `PluginManifest`. Theme-ness is `theme.json` / `themes/*.json` (and optional `registerTheme` in JS). + +## `theme.json` + +Parsed by `parsePluginTheme`. Required: `colorScheme` (`"light"` | `"dark"`) and `tokens` (string map). Optional: `id`, `name`, `description`, `author`, `frosted`. + +If `id` / `name` are omitted, the pack id is used. `frosted: true` marks native window vibrancy; chrome tokens should be translucent. + +Tokens pass `validateThemeTokens`: + +- Names in `CORE_THEME_TOKENS`, or +- Names starting with a `THEME_EXTENSION_SCOPES` prefix (`--syntax-`, `--preview-`, `--ui-`, `--cm-`, `--md-`, `--mde-`) + +Everything else is dropped with `[ThemeRegistry] Theme "": rejected invalid token "…"`. If `--accent` is set and `--accent-primary` is not, `--accent-primary` is copied from `--accent`. + +Parchment (light) — subset of the real file: + +```json +{ + "id": "dripnex-parchment", + "name": "Parchment", + "description": "Warm paper. Reading notes, long sessions.", + "author": "Dripnex", + "colorScheme": "light", + "tokens": { + "--bg-base": "#f3ead4", + "--bg-surface": "#ebe0c4", + "--bg-elevated": "#faf3e3", + "--bg-inset": "#e6d9b8", + "--text-primary": "#3a3224", + "--text-secondary": "rgba(58, 50, 36, 0.74)", + "--text-muted": "rgba(58, 50, 36, 0.52)", + "--border": "rgba(58, 50, 36, 0.12)", + "--accent": "#2a7d6f", + "--accent-hover": "#21675c", + "--glass-bg": "rgba(243, 234, 212, 0.9)", + "--status-active": "#2a7d6f", + "--status-on-hold": "#c27a1a", + "--status-completed": "#3d8b4a", + "--status-dropped": "#c44b4b" + } +} +``` + +### Core tokens (`CORE_THEME_TOKENS`) + +`--bg-base`, `--bg-surface`, `--bg-elevated`, `--bg-inset`, `--bg-hover`, `--bg-active`, `--accent`, `--accent-primary`, `--accent-hover`, `--accent-muted`, `--accent-subtle`, `--text-primary`, `--text-secondary`, `--text-muted`, `--text-faint`, `--border`, `--border-subtle`, `--border-strong`, `--glass-bg`, `--glass-border`, `--glass-bg-menu`, `--glass-border-menu`, `--danger`, `--danger-muted`, `--warning`, `--warning-muted`, `--success`, `--success-muted`, `--status-active`, `--status-on-hold`, `--status-completed`, `--status-dropped` + +Editor / preview extras belong under `--cm-`, `--md-`, `--syntax-`, and so on — for example `--cm-heading`, `--cm-link` in the CLI theme template. + +## Optional CSS + +`styles/*.css` is registered as a plugin stylesheet after activate (theme-only packs use an empty `activate()`). Use it for selectors that are not tokens. Prefer tokens so Settings → Themes can switch palettes cleanly. + +## Registering from JavaScript + +If the pack has a `main` file, you can also call: + +```js +context.registerTheme({ + id: 'harbor-dusk', + name: 'Harbor Dusk', + colorScheme: 'dark', + tokens: { + '--bg-base': '#0c1117', + '--accent': '#5eead4', + }, +}); +``` + +Same whitelist. `context.themes.list()`, `getActive()`, `setActive(id | null)`, and `onDidChange` read the registry. + +There is **no** `context.getTheme()` / `onThemeChanged()`. Appearance lives on `store.getState().settings.theme` (`'dark' | 'light' | 'system'`) and `themes.getActive()`. + +## Pack and try it + +```bash +dripnex-plugin pack +dripnex-plugin install . +``` + +Then Settings → Themes. To share it, follow [Publishing](/guides/publishing): tag `v0.1.0` and attach `theme-harbor-dusk-0.1.0.tar.gz`. diff --git a/content/docs/guides/meta.json b/content/docs/guides/meta.json new file mode 100644 index 0000000..a1b3af7 --- /dev/null +++ b/content/docs/guides/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Guides", + "pages": ["create-a-plugin", "create-a-theme", "publishing"] +} diff --git a/content/docs/guides/publishing.mdx b/content/docs/guides/publishing.mdx new file mode 100644 index 0000000..d993b15 --- /dev/null +++ b/content/docs/guides/publishing.mdx @@ -0,0 +1,79 @@ +--- +title: Publishing +description: One git repo per pack. Version is the git tag. Artifact is the tarball from dripnex-plugin pack on a GitHub release. +--- + +# Publishing + +A community plugin or theme is **not** a folder inside Dripnex and **not** an upload to an app store. It is its own git repository. + +| Piece | What it is | +| ----- | ---------- | +| Repo | One pack, one repository | +| Version | `manifest.json` `version` **and** git tag `vX.Y.Z` | +| Artifact | `-.tar.gz` from `dripnex-plugin pack` | +| Install | `dripnex-plugin install owner/repo[@tag]` or Settings → Plugins → Connect | + +There is **no public marketplace** and no store-publish API for third-party authors. [dripnex.app/plugins](https://dripnex.app/plugins) is a marketing catalog of packs people should know about, not an upload target. + +## Author loop + +```bash +dripnex-plugin init "Mermaid Plus" +cd mermaid-plus +# write the pack, keep it useful +npm install && npm run build +dripnex-plugin pack +``` + +`pack` writes `{id}-{version}.tar.gz` next to the cwd (`manifest.id`, not the GitHub repo name). The archive includes `manifest.json`, `dist/` (or `main`), and any of `keymaps/`, `menus/`, `styles/`, `theme.json`, `themes/` that exist. + +Then: + +```bash +git tag v1.2.3 +git push origin v1.2.3 +gh release create v1.2.3 mermaid-plus-1.2.3.tar.gz --title "Mermaid Plus 1.2.3" +``` + +The release **must** attach that `.tar.gz`. A git tag or GitHub's source zip is not enough — install looks for a built archive (`manifest.json` + `dist/` or `theme.json`). + +`dripnex-plugin publish` packs and, when `gh` works, creates that GitHub release. Treat the GitHub release as the publish step. Do not wait on a community registry. + +## Install from a repo + +```bash +dripnex-plugin install dripnex/plugin-stamp +dripnex-plugin install acme/mermaid-plus@v1.2.3 +dripnex-plugin install https://github.com/acme/mermaid-plus +``` + +No tag means the **latest GitHub release**, first `.tar.gz` / `.tgz` asset. Then reload plugins or restart the app. + +The same `owner/repo` spec works in **Settings → Plugins → Connect** (and the “Other package” field). Prefer the GitHub repo, not the manifest id, when they differ: + +| Role | Vim example | +| ---- | ----------- | +| Manifest id / install folder | `dripnex-vim-mode` | +| GitHub repo (working install spec) | `dripnex/plugin-vim` | + +```bash +dripnex-plugin install dripnex/plugin-vim +``` + +Typing `dripnex-vim-mode` as if it were `owner/repo` is the wrong spec. + +CLI install errors if that `manifest.id` is already present. Settings can overwrite. There is no auto-update; Settings → Plugins → Updates is a manual check against known GitHub releases / first-party rows. + +## First-party Browse + +Official `dripnex/plugin-*` and `dripnex/theme-*` repos whose **latest GitHub release** has a packed `{id}-{version}.tar.gz` can appear in Settings → Plugins → Browse. That is an index of first-party satellites, not a community store you submit to. + +Do not list a pack in any first-party catalog until the release has the packed tarball — Browse install would 404. + +## What does not belong here + +- Proof toys (word count, typewriter, reading time, focus, active line) stay off [dripnex.app/plugins](https://dripnex.app/plugins). +- Do not open a plugin PR against `dripnex/app` unless it is becoming a **built-in**. +- Do not invent a second version number besides the git tag. +- Do not document or depend on a public marketplace publish endpoint. Phase 5 “plugin registry/marketplace” is explicitly later / not planned for this site. diff --git a/content/docs/index.mdx b/content/docs/index.mdx new file mode 100644 index 0000000..9dc6a69 --- /dev/null +++ b/content/docs/index.mdx @@ -0,0 +1,61 @@ +--- +title: Start building with the Dripnex API +description: Plugin and theme API for extending the Dripnex desktop app +--- + +# Start building with the Dripnex API + +The Dripnex plugin API lets you extend the desktop editor: commands, CodeMirror 6, layout zones, preview hooks, and color palettes. Plugins run in the renderer with a controlled `PluginContext`. There is no public marketplace — a pack is a git repo, a git tag, and a tarball on a GitHub release. + +Desktop only. Phone v1 has no plugin path. Current API major: `PLUGIN_API_VERSION = "1"`. + +End-user help lives in the [user manual](https://docs.dripnex.app). The product catalog is at [dripnex.app/plugins](https://dripnex.app/plugins). + +## Getting started + +Before you ship a pack, learn how Dripnex loads scripts and styles from the data directory: + + + + Customize startup with JavaScript in `init.js`. Register commands, map Vim keys, or read the store — without a plugin repo. + + + Restyle the UI with `styles.css` and CSS variables. You do not need a full theme pack for a personal tweak. + + + +How a **plugin repo** is structured (manifest, `main`, package files) is in [Plugin structure](/getting-started). + +## Guides + + + + Scaffold, activate, bundle to CommonJS, and load a pack from disk. + + + Ship a palette with `theme.json`, `CORE_THEME_TOKENS`, and extension scopes. + + + Tag `vX.Y.Z`, attach `{id}-{version}.tar.gz`, install with `dripnex-plugin install owner/repo`. + + + +## Resources + + + + Everything passed to `activate()` — commands, editor, layout, data, themes. + + + `registerCommand`, `dispatchCommand`, menus, and keymaps. + + + CodeMirror 6, decorations, and the live `EditorView`. + + + Token whitelist, `registerTheme`, and `context.themes`. + + + Where plugins can mount React components. + + diff --git a/content/docs/meta.json b/content/docs/meta.json new file mode 100644 index 0000000..0d355ee --- /dev/null +++ b/content/docs/meta.json @@ -0,0 +1,13 @@ +{ + "title": "Dripnex API", + "root": true, + "pages": [ + "index", + "---Getting started---", + "getting-started", + "---Guides---", + "guides", + "---Resources---", + "reference" + ] +} diff --git a/content/docs/reference/commands.mdx b/content/docs/reference/commands.mdx new file mode 100644 index 0000000..38bfbc6 --- /dev/null +++ b/content/docs/reference/commands.mdx @@ -0,0 +1,90 @@ +--- +title: Commands +description: registerCommand, dispatchCommand, menus, context menus, and package keymaps +--- + +# Commands + +## `registerCommand` + +```ts +registerCommand( + options: PluginCommandOptions, + execute: (payload?: Record) => boolean | void | Promise +): () => void +``` + +| Field | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| `id` | `string` | Yes | Id **within** the pack. Host qualifies it as `plugin::`. | +| `name` | `string` | Yes | Palette label | +| `category` | `string` | No | Palette grouping | +| `keybinding` | `{ key, modifiers? }` | No | `modifiers`: `'Mod'` (Cmd on macOS, Ctrl elsewhere), `'Shift'`, `'Alt'` | +| `icon` | `string` | No | Lucide icon name | +| `showInPalette` | `boolean` | No | Default true | + +Return `true` from `execute` when the command handled the invocation. + +```js +const off = context.registerCommand( + { + id: 'say-hello', + name: 'Say Hello', + keybinding: { key: 'H', modifiers: ['Mod', 'Shift'] }, + icon: 'Smile', + }, + () => { + context.notifications.addSuccess('Hello'); + return true; + } +); +``` + +From `init.js`, `dripnex.commands.add(id, name, execute, options?)` is the same registration without repeating `id`/`name` in the options object. + +## `dispatchCommand` + +```ts +dispatchCommand(id: string, payload?: Record): Promise +``` + +Use a **full** id: host commands such as `app:save-note`, or `plugin:stamp:…`. Init equivalent: `dripnex.commands.dispatch`. + +Vim's `:cmd {id}` (from plugin-vim) is this dispatch, not an Inkdrop command bus. + +## Plugins menu + +```js +context.menu.add({ + label: 'Insert Date', + accelerator: 'Mod+Shift+T', + click: () => context.editor.insertAtCursor(localDate(new Date())), +}); + +// or reuse a command you already registered +context.menu.add({ + label: 'Say Hello', + command: 'say-hello', +}); +``` + +`click` is registered as a command. `command` reuses an existing command id (qualified to `plugin::…`). + +## Context menus + +```ts +context.contextMenu.add( + 'note-list-item' | 'notebook-item' | 'tag-item' | 'editor', + { label, command?, click? } +) +``` + +## Package files + +After `activate()`, `keymaps/*.json` and `menus/*.json` are applied. + +**Keymap** — Dripnex form `{ "say-hello": "Mod+Shift+H" }` or Inkdrop form `{ "body": { "ctrl-alt-n": "say-hello" } }`. Chords parse `Mod+Shift+K` or `ctrl-alt-n`. Only `plugin::…` commands this pack registered are bound; core `app:` / `editor:` ids are skipped. + +**Menus** — JSON items with `label` + `command`, optional `accelerator`, optional `submenu`. Context-menu selectors map aliases such as `note-list`, `.cm-editor`, `tag` onto the four targets above. + +Declarative keymaps cannot invent commands. Register them in `activate()` first. diff --git a/content/docs/reference/data.mdx b/content/docs/reference/data.mdx new file mode 100644 index 0000000..fa63b2e --- /dev/null +++ b/content/docs/reference/data.mdx @@ -0,0 +1,96 @@ +--- +title: Data API +description: Notes, notebooks, tags, links, and graph — DataAPI plus the slimmer AppAPI +--- + +# Data API + +Plugins do not talk to a local HTTP server. Reads and writes go through `context.data` (`DataAPI`) and a smaller `context.app` (`AppAPI`). Types: `packages/plugin-api/src/data/dataTypes.ts` and `createDataAPI.ts`. + +## `AppAPI` (`context.app`) + +Read-only convenience: + +| Method | Returns | +| ------ | ------- | +| `getCurrentNote()` | `NoteInfo \| null` (`id`, `title`, `content`) | +| `searchNotes(query)` | `Promise<{ id, title }[]>` | +| `getNoteById(id)` | `Promise` | +| `getNoteTags(noteId)` | `Promise` | +| `getBacklinks(noteId)` | `Promise<{ noteId, noteTitle }[]>` | +| `listNotes()` | `Promise` | +| `listNotebooks()` | `Promise` | +| `listTags()` | `Promise` | +| `onNoteSelected` / `onNoteCreated` / `onNoteDeleted` | unsubscribe | + +`NoteSummaryInfo`: `id`, `title`, `notebookId`, `tags`, `wordCount`, `createdAt`, `updatedAt`, `isPinned`, `status`. + +`NotebookInfo`: `id`, `name`, `parentId`, optional `icon`. + +## `DataAPI` (`context.data`) + +Richer queries. Failures throw `DataAccessError` (`[DataAPI.] …`). + +### Notes + +```ts +getNotes(options?: NoteQueryOptions): Promise +getNote(id: string): Promise +searchNotes(query: string, options?: SearchOptions): Promise +countNotes(options?: NoteQueryOptions): Promise +createNote(input: { content: string; notebookId?: string }): Promise +updateNote(id: string, content: string): Promise +trashNote(id: string): Promise +onNotesChanged(cb): () => void +``` + +`NoteQueryOptions`: `notebookId`, `tag`, `status`, `isPinned`, `sortBy` (`title` \| `createdAt` \| `updatedAt` \| `wordCount`), `sortOrder`, `limit`, `offset`. + +`NoteQueryResult`: `{ notes, total, hasMore }`. + +### Notebooks + +```ts +getNotebooks(options?: { tree?: boolean; includeCounts?: boolean }): Promise +getNotebook(id: string): Promise +createNotebook(input: { name: string; parentId?: string | null }): Promise +updateNotebook(id, patch: { name?, icon?, parentId? }): Promise +deleteNotebook(id: string): Promise +onNotebooksChanged(cb): () => void +``` + +### Tags + +```ts +getTags(options?: TagQueryOptions): Promise +setTagColor(name: string, color: string | null): Promise +renameTag(oldName: string, newName: string): Promise +onTagsChanged(cb): () => void +``` + +`TagInfo`: `{ name, color?, count? }`. Query options: `includeColors`, `includeCount`, `filter` (substring), `limit`, `offset`. + +### Links and graph + +```ts +getBacklinks(noteId: string): Promise +getOutgoingLinks(noteId: string): Promise +getGraphData(options?: { notebookId?: string; depth?: number }): Promise +``` + +Outgoing links include `resolved: boolean` and `targetId` which may be `null`. + +### Change events + +```ts +interface DataChangeEvent { + kind: T; + action: 'created' | 'updated' | 'deleted' | 'renamed'; + id: string; + previousName?: string; +} +``` + +## Store vs data + +`context.store.getState().notes.items` is the **visible list in this window**. Use `data.getNotes()` / `app.listNotes()` for library queries. The store has no `dispatch`. diff --git a/content/docs/reference/editor.mdx b/content/docs/reference/editor.mdx new file mode 100644 index 0000000..cd34f83 --- /dev/null +++ b/content/docs/reference/editor.mdx @@ -0,0 +1,79 @@ +--- +title: Editor extensions +description: EditorAPI, CodeMirror 6 registerExtensions, decorations, and getView() +--- + +# Editor extensions + +## `EditorAPI` (`context.editor`) + +Controlled subset of editor operations: + +| Method | Returns | Notes | +| ------ | ------- | ----- | +| `getContent()` | `string` | Full buffer | +| `getSelection()` | `{ from, to }` | Document offsets | +| `setSelection(from, to?)` | `void` | | +| `replaceRange(from, to, text)` | `void` | | +| `insertAtCursor(text)` | `void` | | +| `getWordCount()` / `getCharCount()` / `getLineCount()` | `number` | | +| `onDocChanged(cb)` | unsubscribe | `cb(content: string)` | +| `onSelectionChanged(cb)` | unsubscribe | `cb({ from, to })` | +| `focus()` | `void` | | +| `getView()` | `EditorView \| null` | Live CodeMirror 6 view, or `null` if unmounted | + +```js +const content = context.editor.getContent(); +const { from, to } = context.editor.getSelection(); +context.editor.replaceRange(from, to, '[' + content.slice(from, to) + '](url)'); + +const view = context.editor.getView(); +if (view) { + view.dispatch(/* … */); +} +``` + +From init.js, `dripnex.getActiveEditor()` returns `{ editor, cm }` with `cm === editor.getView()`. + +## CodeMirror 6 extensions + +```ts +registerExtensions(id: string, extensions: Extension[]): () => void +``` + +Extensions are installed in a compartment (`pluginExtensionCompartment`) so they can be swapped without rebuilding the editor. Host `require('@codemirror/view')` (and `state`, `language`, `commands`, `search`) is the **app's singleton** — do not bundle a second CodeMirror. + +```js +const { keymap } = require('@codemirror/view'); + +const off = context.registerExtensions('hello-keymap', [ + keymap.of([ + { + key: 'Mod-Shift-h', + run: () => { + context.editor.insertAtCursor('hello'); + return true; + }, + }, + ]), +]); +``` + +Anything not on the [host require whitelist](/getting-started#the-module-must-be-commonjs) (for example `@replit/codemirror-vim`) must be bundled. That is how [plugin-vim](https://github.com/dripnex/plugin-vim) works: `registerExtensions` + `registerVim` + status bar, not an Atom keymap. + +## Decorations + +`context.decorations` (`EditorDecorationAPI`): + +| Method | Notes | +| ------ | ----- | +| `addLineHighlight(line, className)` | **1-indexed** line. Returns remove. | +| `addWidget(pos, dom)` | DOM widget at a document position. Returns remove. | +| `clear()` | Drop all decorations owned by this instance | + +The decoration `StateField` is created with `createDecorationAPI` and must be present as a CM6 extension (the host wires this for plugins). + +```js +const remove = context.decorations.addLineHighlight(1, 'cm-activeLine'); +remove(); +``` diff --git a/content/docs/reference/examples.mdx b/content/docs/reference/examples.mdx new file mode 100644 index 0000000..1bc85ca --- /dev/null +++ b/content/docs/reference/examples.mdx @@ -0,0 +1,144 @@ +--- +title: Examples +description: Patterns from official satellites and valid PluginContext usage +--- + +# Examples + +## Stamp (official satellite) + +[dripnex/plugin-stamp](https://github.com/dripnex/plugin-stamp) — insert date/timestamp. CommonJS, `menu.add`, `insertAtCursor`. Install: `dripnex-plugin install dripnex/plugin-stamp`. + +```js +function pad(n) { + return String(n).padStart(2, '0'); +} + +function localDate(now) { + return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`; +} + +function insert(context, text) { + if (!context.editor || typeof context.editor.insertAtCursor !== 'function') { + return false; + } + context.editor.insertAtCursor(text); + return true; +} + +module.exports = { + id: 'stamp', + name: 'Stamp', + version: '0.1.0', + description: 'Insert the current date or timestamp at the cursor.', + + activate(context) { + const removeDate = context.menu.add({ + label: 'Insert Date', + accelerator: 'Mod+Shift+T', + click: () => insert(context, localDate(new Date())), + }); + const removeStamp = context.menu.add({ + label: 'Insert Timestamp', + click: () => + insert( + context, + `${localDate(new Date())} ${pad(new Date().getHours())}:${pad(new Date().getMinutes())}` + ), + }); + + return { + dispose() { + removeDate(); + removeStamp(); + }, + }; + }, +}; +``` + +## Insert-only mermaid / math (not renderers) + +[plugin-mermaid](https://github.com/dripnex/plugin-mermaid) and [plugin-math](https://github.com/dripnex/plugin-math) insert a fence or `$$` block. Diagram and KaTeX **rendering** is built-in (`dripnex-mermaid`, `dripnex-math`). A pack that only inserts markdown should look like Stamp: `insertAtCursor('```mermaid\\n…\\n```')`, not `registerCodeBlockRenderer`. + +## Status bar + config + +Valid `PluginContext` usage (proof plugins like reading-time stay off the marketing catalog): + +```js +const React = require('react'); + +function ReadingTime({ meta }) { + const editor = meta && meta.editor; + const [minutes, setMinutes] = React.useState(0); + + React.useEffect(() => { + if (!editor) return; + const update = () => { + const words = editor.getWordCount(); + setMinutes(Math.max(1, Math.ceil(words / 200))); + }; + update(); + return editor.onDocChanged(update); + }, [editor]); + + if (!editor) return null; + return React.createElement('span', null, minutes + ' min read'); +} + +module.exports = { + id: 'reading-time', + name: 'Reading Time', + version: '1.0.0', + activate(context) { + context.layout.addComponent('editor-status-bar', { + id: 'reading-time:status', + component: ReadingTime, + order: 20, + meta: { editor: context.editor }, + }); + return { + dispose() { + context.layout.removeComponent('reading-time:status'); + }, + }; + }, +}; +``` + +## Clipboard command + +Use `context.clipboard`, not `navigator.clipboard`: + +```js +module.exports = { + id: 'copy-note', + name: 'Copy note', + version: '1.0.0', + activate(context) { + const off = context.registerCommand( + { id: 'copy-markdown', name: 'Copy as Markdown', icon: 'Copy' }, + async () => { + const content = context.editor.getContent(); + if (!content) return false; + await context.clipboard.writeText(content); + context.notifications.addSuccess('Copied'); + return true; + } + ); + return { dispose: off }; + }, +}; +``` + +## Dispatch a host command + +```js +await context.dispatchCommand('app:save-note'); +``` + +Init.js equivalent: `void dripnex.commands.dispatch('app:save-note')`. + +## Vim (community) + +[dripnex/plugin-vim](https://github.com/dripnex/plugin-vim) — manifest id `dripnex-vim-mode`. Bundles `@replit/codemirror-vim`, calls `registerExtensions` and `registerVim`. After it is enabled, `init.js` may use `dripnex.vim` ([init file](/getting-started/init-file#vim-maps-after-installing-vim)). diff --git a/content/docs/reference/layout-zones.mdx b/content/docs/reference/layout-zones.mdx new file mode 100644 index 0000000..aeec236 --- /dev/null +++ b/content/docs/reference/layout-zones.mdx @@ -0,0 +1,89 @@ +--- +title: Layout zones +description: LayoutZoneName values plugins can mount React components into +--- + +# Layout zones + +`context.layout` is a `LayoutManager`. Zone names are the `LayoutZoneName` union in `packages/plugin-api/src/layout/types.ts`. + +```ts +addComponent(zone: LayoutZoneName, entry: Omit): void +removeComponent(id: string): void +removeAllForPlugin(pluginId: string): void +``` + +`pluginId` is filled by the host. You pass `id`, `component`, `order`, and optional `meta`. + +## Zones + +| Zone | Typical use | +| ---- | ----------- | +| `sidebar-section` | Left sidebar blocks | +| `sidebar-footer` | Bottom of the sidebar | +| `editor-toolbar` | Above the editor | +| `editor-status-bar` | Bottom of the editor (Vim mode, timers) | +| `editor-header-actions` | Note header buttons | +| `editor-footer` | Below the editor | +| `preview-toolbar` | Preview chrome | +| `panel` | Side panel (large UI) | +| `modal` | Overlay — pair with `context.components.Modal` / `Dialog` | +| `settings-section` | Extra Settings UI (schema fields already render without this) | +| `note-list-header` | Top of the note list | +| `note-list-footer` | Bottom of the note list | +| `note-list-item-suffix` | Per-row suffix in the list | +| `command-palette-footer` | Below the palette | + +``` +┌─────────────────────────────────────────────────────┐ +│ sidebar-section │ editor-header-actions │ +│ ├───────────────────────────────┐ │ +│ │ editor-toolbar │ │ +│ ├───────────────────────────────┤ │ +│ │ [Editor] │panel│ +│ │ │ │ +│ ├───────────────────────────────┤ │ +│ note-list-footer│ editor-status-bar / footer │ │ +└─────────────────────────────────────────────────────┘ +``` + +## Zone entry + +```js +context.layout.addComponent('editor-status-bar', { + id: 'hello:status', + component: Status, + order: 30, + meta: { editor: context.editor }, +}); +``` + +```ts +function Status({ meta }: ZoneComponentProps) { + const editor = meta?.editor; + return editor ? String(editor.getWordCount()) : null; +} +``` + +`order`: lower first (further left / further up). Convention: 1–9 core, 10–29 built-ins, **30–99 community**. + +Components should be React function components. Host `require('react')` is the app singleton so hooks work. + +## Cleanup + +```js +activate(context) { + context.layout.addComponent('editor-status-bar', { + id: 'hello:status', + component: Status, + order: 30, + }); + return { + dispose() { + context.layout.removeComponent('hello:status'); + }, + }; +} +``` + +`removeAllForPlugin` runs on unload; still remove explicitly when you can. diff --git a/content/docs/reference/meta.json b/content/docs/reference/meta.json new file mode 100644 index 0000000..d252b4c --- /dev/null +++ b/content/docs/reference/meta.json @@ -0,0 +1,12 @@ +{ + "title": "Resources", + "pages": [ + "plugin-context", + "commands", + "editor", + "themes", + "layout-zones", + "data", + "examples" + ] +} diff --git a/content/docs/reference/plugin-context.mdx b/content/docs/reference/plugin-context.mdx new file mode 100644 index 0000000..939b236 --- /dev/null +++ b/content/docs/reference/plugin-context.mdx @@ -0,0 +1,123 @@ +--- +title: PluginContext +description: The object passed to activate() — methods and nested APIs from @dripnex/plugin-api +--- + +# PluginContext + +`activate(context: PluginContext)` receives this object. Types live in `packages/plugin-api/src/types.ts`. Current major: `PLUGIN_API_VERSION = "1"`. + +```ts +activate(context: PluginContext): PluginDisposable | void +``` + +Return `{ dispose() }` and undo registrations. `deactivate()` on the manifest is optional. + +## Nested objects + +| Property | Type | Role | +| -------- | ---- | ---- | +| `layout` | `LayoutManager` | [Layout zones](/reference/layout-zones) | +| `editor` | `EditorAPI` | [Editor](/reference/editor) | +| `decorations` | `EditorDecorationAPI` | Line highlights and widgets | +| `config` | `PluginConfigAPI` | `get` / `set` / `observe` | +| `log` | `PluginLogger` | `debug`, `info`, `warn`, `error` | +| `app` | `AppAPI` | Read-only notes, notebooks, tags, events | +| `store` | `AppStore` | `getState()` / `subscribe()`. No `dispatch`. | +| `data` | `DataAPI` | [Notes, notebooks, tags, graph](/reference/data) | +| `menu` | `{ add(item) }` | Plugins menu | +| `clipboard` | `{ readText, writeText }` | Async clipboard | +| `notifications` | `{ addSuccess, addInfo, addWarning, addError }` | Toasts | +| `contextMenu` | `{ add(target, item) }` | `note-list-item` \| `notebook-item` \| `tag-item` \| `editor` | +| `components` | `PluginComponents` | Stock `Button`, `Modal`, `Dialog` | +| `preview` | `{ on(event, handler) }` | `'a:click'` \| `'checkbox:change'` | +| `themes` | `{ list, getActive, setActive, onDidChange }` | [Themes](/reference/themes) | +| `markdownRenderer` | `MarkdownRenderer` | Inkdrop-shaped remark/rehype/React/fence maps | + +## Registration methods + +Each returns an unregister function unless noted. + +### `registerCommand(options, execute)` + +Palette command. See [Commands](/reference/commands). + +### `dispatchCommand(id, payload?)` + +Dispatch a **host or plugin** command by id (`app:save-note`, `plugin:…`). Returns `Promise`. + +### `registerExtensions(id, extensions)` + +CodeMirror 6 `Extension[]` in a compartment. See [Editor](/reference/editor). + +### `registerVim(api)` + +Publish a Vim API for `dripnex.vim` in `init.js`. Call at **module load**, not only inside `activate`, so init can `Vim.map` as soon as the pack is present. + +### `registerRemarkPlugin(id, plugin, options?)` / `registerRehypePlugin(id, plugin, options?)` + +Preview pipeline (appended after core). `PluginHookOptions`: `name?`, `version?`, `priority?` (lower runs first, default `100`). + +### `registerPreviewComponent(id, tagName, component)` + +Replace an HTML tag in the preview with a React component. + +### `registerCodeBlockRenderer(id, language, component)` + +Fence renderer. Props: `{ code, language, meta? }` (`CodeBlockRendererProps`). Built-in mermaid/math already register `mermaid` / `math` / `latex`. A satellite that only inserts a fence should not claim to be that renderer. + +### `registerAiCommand(options)` + +Appears in the AI panel and the command palette. Placeholders in `userPromptTemplate`: `{{selection}}`, `{{note}}`, `{{title}}`. `outputTarget`: `'replace'` \| `'insert'` \| `'panel'` (default `'panel'`). + +### `registerCssVariables(id, variables)` / `registerTheme(theme)` + +See [Themes](/reference/themes). + +## Config + +```ts +context.config.get('workMinutes'); +context.config.set('workMinutes', 30); +const stop = context.config.observe('workMinutes', (value) => { /* … */ }); +``` + +## Logger + +`debug` / `info` / `warn` / `error`. Prefixed with the plugin id in the developer console. + +## Components and preview events + +```js +const { Button, Modal, Dialog } = context.components; +// aliases: components.get('Button'), components.getComponentClass('Button') + +const off = context.preview.on('a:click', (detail) => { + // detail.href, detail.text — return false to prevent default +}); +context.preview.on('checkbox:change', (detail) => { + // detail.index, detail.checked +}); +``` + +Mount overlays with `layout.addComponent('modal', …)`. + +## Manifest (exported module) + +Besides on-disk `manifest.json`, the evaluated module must satisfy `PluginManifest`: + +| Field | Required | Notes | +| ----- | -------- | ----- | +| `id` | Yes | Kebab-case; must match `manifest.json` | +| `name` | Yes | | +| `version` | Yes | Semver | +| `description` | No | | +| `apiVersion` | No | e.g. `"1"` | +| `dependencies` | No | Other `pluginId` → semver range | +| `configSchema` | No | | +| `activate` | Yes | Function | +| `deactivate` | No | Function if present | + +There is **no** `themeType` on the type. There is **no** `getTheme` / `onThemeChanged` on `PluginContext`. + +`validateManifest` / `assertValidManifest` / `validateConfigValue` are exported from `@dripnex/plugin-api`. diff --git a/content/docs/reference/themes.mdx b/content/docs/reference/themes.mdx new file mode 100644 index 0000000..4bfbc43 --- /dev/null +++ b/content/docs/reference/themes.mdx @@ -0,0 +1,58 @@ +--- +title: Themes +description: CORE_THEME_TOKENS, THEME_EXTENSION_SCOPES, registerTheme, and context.themes +--- + +# Themes + +Palettes are `ThemeDefinition` objects in `themeRegistryStore`. User-facing switcher: Settings → Themes. + +## Token whitelist + +From `packages/plugin-api/src/theme/themeTypes.ts`. + +`isValidThemeToken(token)` is true when the name is in `CORE_THEME_TOKENS` or starts with a `THEME_EXTENSION_SCOPES` prefix. + +`validateThemeTokens(tokens, themeId)` keeps valid entries, warns on rejects, and copies `--accent` → `--accent-primary` when the latter is missing. + +**`CORE_THEME_TOKENS`:** `--bg-base`, `--bg-surface`, `--bg-elevated`, `--bg-inset`, `--bg-hover`, `--bg-active`, `--accent`, `--accent-primary`, `--accent-hover`, `--accent-muted`, `--accent-subtle`, `--text-primary`, `--text-secondary`, `--text-muted`, `--text-faint`, `--border`, `--border-subtle`, `--border-strong`, `--glass-bg`, `--glass-border`, `--glass-bg-menu`, `--glass-border-menu`, `--danger`, `--danger-muted`, `--warning`, `--warning-muted`, `--success`, `--success-muted`, `--status-active`, `--status-on-hold`, `--status-completed`, `--status-dropped` + +**`THEME_EXTENSION_SCOPES`:** `--syntax-`, `--preview-`, `--ui-`, `--cm-`, `--md-`, `--mde-` + +## `registerTheme` + +```ts +registerTheme(theme: { + id: string; + name: string; + description?: string; + author?: string; + colorScheme: 'dark' | 'light'; + tokens: Record; +}): () => void +``` + +`frosted?: boolean` exists on `ThemeDefinition` (native vibrancy). Package `theme.json` may set it; the `registerTheme` argument type on `PluginContext` does not list `frosted` — use `theme.json` for that flag. + +## `context.themes` + +```ts +list(): ThemeInfo[] +getActive(): ThemeInfo | null +setActive(id: string | null): boolean +onDidChange(callback: (id: string | null) => void): () => void +``` + +`ThemeInfo`: `{ id, name, colorScheme, description? }`. `setActive(null)` clears the plugin palette (back to the app default). + +Appearance (`'dark' | 'light' | 'system'`) is `store.getState().settings.theme`, not a `getTheme()` helper — that method is not on `PluginContext`. + +## CSS variables without a full palette + +```ts +registerCssVariables(id: string, variables: Record): () => void +``` + +## Package files + +`theme.json` / `themes/*.json` are parsed after activate. See [Create a theme](/guides/create-a-theme). Official examples: [theme-parchment](https://github.com/dripnex/theme-parchment) and other `dripnex/theme-*` repos. diff --git a/lib/layout.shared.tsx b/lib/layout.shared.tsx new file mode 100644 index 0000000..8924357 --- /dev/null +++ b/lib/layout.shared.tsx @@ -0,0 +1,37 @@ +import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; +import { DOCS_URL, SITE_URL } from '@/lib/site'; + +export function baseOptions(): BaseLayoutProps { + return { + nav: { + title: ( + + + + dripnex. + + developers + + ), + transparentMode: 'top', + }, + githubUrl: 'https://github.com/dripnex/developers', + links: [ + { + text: 'Product', + url: SITE_URL, + external: true, + }, + { + text: 'User docs', + url: DOCS_URL, + external: true, + }, + { + text: 'Plugins', + url: `${SITE_URL}/plugins`, + external: true, + }, + ], + }; +} diff --git a/lib/site.ts b/lib/site.ts new file mode 100644 index 0000000..0c35f29 --- /dev/null +++ b/lib/site.ts @@ -0,0 +1,4 @@ +export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://dripnex.app'; +export const DOCS_URL = process.env.NEXT_PUBLIC_DOCS_URL ?? 'https://docs.dripnex.app'; +export const DEVELOPERS_URL = + process.env.NEXT_PUBLIC_DEVELOPERS_URL ?? 'https://developers.dripnex.app'; diff --git a/lib/source.ts b/lib/source.ts new file mode 100644 index 0000000..8a0b363 --- /dev/null +++ b/lib/source.ts @@ -0,0 +1,7 @@ +import { loader } from 'fumadocs-core/source'; +import { docs } from '@/.source/server'; + +export const source = loader({ + baseUrl: '/', + source: docs.toFumadocsSource(), +}); diff --git a/mdx-components.tsx b/mdx-components.tsx new file mode 100644 index 0000000..daed3fd --- /dev/null +++ b/mdx-components.tsx @@ -0,0 +1,29 @@ +import type { MDXComponents } from 'mdx/types'; +import defaultComponents from 'fumadocs-ui/mdx'; +import { Card, Cards } from 'fumadocs-ui/components/card'; +import { Callout } from 'fumadocs-ui/components/callout'; +import { Step, Steps } from 'fumadocs-ui/components/steps'; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; +import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; +import { File, Folder, Files } from 'fumadocs-ui/components/files'; +import { TypeTable } from 'fumadocs-ui/components/type-table'; + +export function useMDXComponents(components: MDXComponents): MDXComponents { + return { + ...defaultComponents, + Card, + Cards, + Callout, + Step, + Steps, + Tab, + Tabs, + Accordion, + Accordions, + File, + Folder, + Files, + TypeTable, + ...components, + }; +} diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..1b3be08 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..649691f --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,12 @@ +import { createMDX } from 'fumadocs-mdx/next'; + +const withMDX = createMDX(); + +/** @type {import('next').NextConfig} */ +const config = { + output: 'export', + reactStrictMode: true, + images: { unoptimized: true }, +}; + +export default withMDX(config); diff --git a/package.json b/package.json new file mode 100644 index 0000000..46af6f1 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "@dripnex/developers", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "postinstall": "fumadocs-mdx", + "typecheck": "tsc --noEmit", + "deploy": "pnpm build && npx wrangler pages deploy out --project-name dripnex-developers --branch main" + }, + "dependencies": { + "@fontsource-variable/jetbrains-mono": "^5.2.8", + "@fontsource/inter": "^5.2.8", + "fumadocs-core": "^16.9.3", + "fumadocs-mdx": "^15.0.11", + "fumadocs-ui": "^16.9.3", + "next": "^16.2.7", + "next-themes": "^0.4.6", + "react": "^19.2.7", + "react-dom": "^19.2.7" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.3.0", + "@types/mdx": "^2.0.13", + "@types/node": "25.9.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "postcss": "^8.5.15", + "tailwindcss": "^4.3.0", + "typescript": "^6.0.3" + }, + "packageManager": "pnpm@11.9.0" +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..ba30d5f --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,7 @@ +/** @type {import('postcss').Config} */ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; +export default config; diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..e4be951 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..d766d84 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..b050d87 Binary files /dev/null and b/public/favicon.png differ diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000..e892e77 Binary files /dev/null and b/public/icon.png differ diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..95710fe Binary files /dev/null and b/public/logo.png differ diff --git a/source.config.ts b/source.config.ts new file mode 100644 index 0000000..0b6ee4d --- /dev/null +++ b/source.config.ts @@ -0,0 +1,7 @@ +import { defineDocs, defineConfig } from 'fumadocs-mdx/config'; + +export const docs = defineDocs({ + dir: 'content/docs', +}); + +export default defineConfig(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8f128ae --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { + "@/*": ["./*"], + "@/.source": ["./.source/index"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.mdx", + ".next/types/**/*.ts", + ".source/index.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": ["node_modules"] +}