chs: re-add drifted files

dot_agents/dot_skill-lock.json
This commit is contained in:
Prad Nukala committed 2026-08-14 13:53:55 -04:00
1 parent 03ef5c94ea
commit 42d17e825e
30 files changed
+229 -144

No files matched your search

+2 -2
View File
@@ -44,8 +44,8 @@ The single biggest shift for most sources — especially Mintlify — is that **
- **Folders become groups, files become pages.** A page's sidebar label is its frontmatter `title`; a group's label is the humanized folder name. - **Folders become groups, files become pages.** A page's sidebar label is its frontmatter `title`; a group's label is the humanized folder name.
- **Ordering resolves highest-priority-first:** an explicit `navigation.sidebar` (replaces the whole tree) → a folder's `meta.ts` `pages` array → a page's frontmatter `sidebar.order` → the filesystem (`index` first, then numeric filename prefix like `01-`, then alphabetical). - **Ordering resolves highest-priority-first:** an explicit `navigation.sidebar` (replaces the whole tree) → a folder's `meta.ts` `pages` array → a page's frontmatter `sidebar.order` → the filesystem (`index` first, then numeric filename prefix like `01-`, then alphabetical).
- **`meta.ts` refines one folder** (`defineMeta({ title, icon, order, collapsed, pages })`). The `pages` array lists children by slug (numeric prefix and parentheses stripped); children you omit fall back to their own `sidebar.order`, then filesystem order (`index` still sorts first) — so a partial `pages` list is safe, but list every child when the source declared a complete order. - **`meta.ts` refines one folder** (`defineMeta({ title, icon, order, collapsed, pages, display })`). The `pages` array lists children by slug (numeric prefix and parentheses stripped); children you omit fall back to their own `sidebar.order`, then filesystem order (`index` still sorts first) — so a partial `pages` list is safe, but list every child when the source declared a complete order.
- **Sidebar render mode is global, not per-folder.** `navigation.sidebar.display` in `blume.config.ts` is `"flat"` (default), `"group"` (collapsible), or `"page"` (drill-in sub-panel) and applies to **every** group at once. (It used to live on each folder's `meta.ts` as `display`; that field was **removed** — writing it in a `meta.ts` is now a build error. Set it once in config instead.) An explicit `navigation.sidebar` item may still override its own group's `display`. - **Sidebar render mode: a global default with per-folder overrides.** `navigation.sidebar.display` in `blume.config.ts` is `"flat"` (default), `"group"` (collapsible), or `"page"` (drill-in sub-panel) and sets the mode for every group at once. A folder can override its **own** group: `display` in its `meta.ts`, or — sugar when the folder has an `index` page — `sidebar.display` in that index page's frontmatter. Precedence: index frontmatter → `meta.ts` → global config → `flat`; an override applies to that one group only (nested subgroups resolve their own chain). So a source's per-category collapse/drill-in modes migrate per folder — only reach for the global mode when the whole sidebar changes. Under an explicit `navigation.sidebar`, the config item's own `display` field is the only per-group control (frontmatter/meta `display` is ignored there, with a `BLUME_SIDEBAR_DISPLAY_IGNORED` warning).
- **An explicit `navigation.sidebar` replaces filesystem generation entirely.** Use it only for a nav shape files can't express. Its items are a page route string, a group (`{ label, items }`), or a link (`{ label, href }`). - **An explicit `navigation.sidebar` replaces filesystem generation entirely.** Use it only for a nav shape files can't express. Its items are a page route string, a group (`{ label, items }`), or a link (`{ label, href }`).
- **Config-declared nesting has no on-disk counterpart — materialize it or it flattens silently.** When a source (Mintlify `groups`, Nextra `_meta`, a Docusaurus sidebar…) declares a nested group, its pages usually sit **flat in one folder** and the grouping lives only in config. Filesystem-derived nav sees the flat folder and drops the inner group. To keep the nesting you must **either** move those pages into a real subfolder (`meta.ts` for label/`collapsed`) — which changes their URLs, so add `redirects` — **or** declare the group in an explicit `navigation.sidebar`, which nests the existing routes without moving a file. Walk config `pages`/nav arrays **recursively** during inventory and record where config nesting depth exceeds on-disk depth; that gap is exactly what gets lost. - **Config-declared nesting has no on-disk counterpart — materialize it or it flattens silently.** When a source (Mintlify `groups`, Nextra `_meta`, a Docusaurus sidebar…) declares a nested group, its pages usually sit **flat in one folder** and the grouping lives only in config. Filesystem-derived nav sees the flat folder and drops the inner group. To keep the nesting you must **either** move those pages into a real subfolder (`meta.ts` for label/`collapsed`) — which changes their URLs, so add `redirects` — **or** declare the group in an explicit `navigation.sidebar`, which nests the existing routes without moving a file. Walk config `pages`/nav arrays **recursively** during inventory and record where config nesting depth exceeds on-disk depth; that gap is exactly what gets lost.
@@ -41,7 +41,7 @@ Every Docusaurus repo serves **`static/`** at the site root (`static/img/foo.png
## Navigation: `sidebars.js` + `_category_.json` ## Navigation: `sidebars.js` + `_category_.json`
- **Autogenerated sidebar** (`{ type: 'autogenerated', dirName: '...' }`) → Blume's default filesystem navigation. Docusaurus strips numeric prefixes (`01-`) exactly like Blume, so the convention round-trips; no config needed. - **Autogenerated sidebar** (`{ type: 'autogenerated', dirName: '...' }`) → Blume's default filesystem navigation. Docusaurus strips numeric prefixes (`01-`) exactly like Blume, so the convention round-trips; no config needed.
- **Explicit sidebar** (arrays of doc IDs, categories, links) → restructure into folders where possible; use `navigation.sidebar` only for shapes files can't express. A category `{ type: 'category', label, items }` → a folder (label → `meta.ts` `title`); `collapsed` → `meta.ts` `collapsed` (**no `display` field in `meta.ts`** — if collapse matters, set the global `navigation.sidebar.display: "group"` once in config). - **Explicit sidebar** (arrays of doc IDs, categories, links) → restructure into folders where possible; use `navigation.sidebar` only for shapes files can't express. A category `{ type: 'category', label, items }` → a folder (label → `meta.ts` `title`); `collapsed` → `meta.ts` `collapsed`, and collapsible rendering → that folder's `meta.ts` `display: "group"` (or `navigation.sidebar.display: "group"` once in config when every category collapses).
- **`_category_.json`** (also `.yml`) (`{ label, position, collapsed, collapsible, link, className, customProps }`) → a folder `meta.ts`: `label`→`title`, `position`→`order`, `collapsed`→`collapsed`. `link.type: 'generated-index'` → an `index` page in the folder — **and the old URL was `/docs/category/<slug>`**, so add a redirect and rewrite inbound links. `link.type: 'doc'` → make that doc the folder's `index`. `collapsible`/`className`/`customProps` → drop (report). - **`_category_.json`** (also `.yml`) (`{ label, position, collapsed, collapsible, link, className, customProps }`) → a folder `meta.ts`: `label`→`title`, `position`→`order`, `collapsed`→`collapsed`. `link.type: 'generated-index'` → an `index` page in the folder — **and the old URL was `/docs/category/<slug>`**, so add a redirect and rewrite inbound links. `link.type: 'doc'` → make that doc the folder's `index`. `collapsible`/`className`/`customProps` → drop (report).
- **`src/pages/` — inventory, don't nuke.** Nearly every repo has a React landing page (`src/pages/index.tsx`) and often extra Markdown pages. Markdown pages → content pages; the React home page → rebuild as a docs index or a custom `.astro` page under `content.pages` — report either way. - **`src/pages/` — inventory, don't nuke.** Nearly every repo has a React landing page (`src/pages/index.tsx`) and often extra Markdown pages. Markdown pages → content pages; the React home page → rebuild as a docs index or a custom `.astro` page under `content.pages` — report either way.
@@ -39,7 +39,7 @@ Everything else is `defineConfig({ title })`.
| `description` | **drop** (folders have no description) | | `description` | **drop** (folders have no description) |
| `collapsible: false` | **drop** (report — no per-folder equivalent) | | `collapsible: false` | **drop** (report — no per-folder equivalent) |
`meta.ts` accepts **only** `title`, `icon`, `order`, `collapsed`, `pages` — there is **no `display` field** (writing one is a build error). Collapse rendering is global: when any folder needs collapsing, set `navigation.sidebar.display: "group"` once in `blume.config.ts`. `meta.ts` accepts **only** `title`, `icon`, `order`, `collapsed`, `pages`, `display`. Render mode is per-folder or global: a folder that needs collapsible rendering sets its own `meta.ts` `display: "group"` (drill-in is `"page"`); when the whole sidebar should collapse, set `navigation.sidebar.display: "group"` once in `blume.config.ts` instead of repeating it per folder.
**`root: true` folders are Fumadocs' tab mechanism** — Fumadocs UI renders them as layout tabs and scopes the sidebar to the active one. That is exactly Blume's `navigation.tabs`: add `{ label, path, icon? }` per root folder (label/icon from its `meta.json`), pointing at the folder's route. Blume then scopes the sidebar by URL prefix the same way. Don't try to model root folders inside `meta.ts`. **`root: true` folders are Fumadocs' tab mechanism** — Fumadocs UI renders them as layout tabs and scopes the sidebar to the active one. That is exactly Blume's `navigation.tabs`: add `{ label, path, icon? }` per root folder (label/icon from its `meta.json`), pointing at the folder's route. Blume then scopes the sidebar by URL prefix the same way. Don't try to model root folders inside `meta.ts`.
@@ -83,7 +83,7 @@ The codemod touches **only frontmatter**. Icons in MDX **body** (`<Icon icon="
Mintlify's `navigation` object (`tabs`/`anchors`/`dropdowns`/`products`/`versions`/`languages`/`groups`/`pages`) is fully config-declared. **Prefer restructuring content into folders**, not porting the config verbatim: Mintlify's `navigation` object (`tabs`/`anchors`/`dropdowns`/`products`/`versions`/`languages`/`groups`/`pages`) is fully config-declared. **Prefer restructuring content into folders**, not porting the config verbatim:
- **`groups`** (`{ group, pages: [...] }`) → a folder per group. The `group` name → the folder's humanized name or a `meta.ts` `title`. Nested groups → nested folders. `expanded: false` → `meta.ts` `collapsed: true` (inverted). `tag` → the folder/page `sidebar.badge`. **Folder metadata is a `meta.ts` module** (`export default defineMeta({ title, icon, order, collapsed, pages })`), a TypeScript file — **not** JSON. Blume does not read a `meta.json`, so any folder-level config you carry over from the source (or hand-write to preserve a group's label/order/collapse) must be authored as `meta.ts`. (Render mode — flat/group/page — is **not** a `meta.ts` field; it's the global `navigation.sidebar.display` in `blume.config.ts`.) - **`groups`** (`{ group, pages: [...] }`) → a folder per group. The `group` name → the folder's humanized name or a `meta.ts` `title`. Nested groups → nested folders. `expanded: false` → `meta.ts` `collapsed: true` (inverted). `tag` → the folder/page `sidebar.badge`. **Folder metadata is a `meta.ts` module** (`export default defineMeta({ title, icon, order, collapsed, pages, display })`), a TypeScript file — **not** JSON. Blume does not read a `meta.json`, so any folder-level config you carry over from the source (or hand-write to preserve a group's label/order/collapse) must be authored as `meta.ts`. (Render mode — flat/group/page — defaults to the global `navigation.sidebar.display` in `blume.config.ts`; a folder overrides its own group with `meta.ts` `display`, or `sidebar.display` in its `index` page's frontmatter.)
- **Config-only nested groups don't exist on disk — you must materialize them, or they flatten silently.** A nested `{ group, pages }` almost never has a matching subfolder: its pages sit **flat in the parent directory** (e.g. `platform/analytics/getting-started.mdx`, `…/quick-reference.mdx`) and the grouping lives **only** in the `docs.json` `pages` array. If you leave the files where they are, filesystem-derived nav sees one flat folder and the inner group vanishes — Mintlify's `Analytics → Reference → {…}` becomes a flat `Analytics → {…}`. To preserve it you must **either** move those pages into a real subfolder (`platform/analytics/reference/`, with a `meta.ts` for the label/`collapsed`), **or** declare the shape in an explicit `navigation.sidebar`. Walk **every** `pages` array recursively and treat any nested `group` object as a folder-move to plan, not files already in place. When inventorying the nav (workflow step 2), record the config nesting depth separately from the on-disk depth — they diverge exactly here. - **Config-only nested groups don't exist on disk — you must materialize them, or they flatten silently.** A nested `{ group, pages }` almost never has a matching subfolder: its pages sit **flat in the parent directory** (e.g. `platform/analytics/getting-started.mdx`, `…/quick-reference.mdx`) and the grouping lives **only** in the `docs.json` `pages` array. If you leave the files where they are, filesystem-derived nav sees one flat folder and the inner group vanishes — Mintlify's `Analytics → Reference → {…}` becomes a flat `Analytics → {…}`. To preserve it you must **either** move those pages into a real subfolder (`platform/analytics/reference/`, with a `meta.ts` for the label/`collapsed`), **or** declare the shape in an explicit `navigation.sidebar`. Walk **every** `pages` array recursively and treat any nested `group` object as a folder-move to plan, not files already in place. When inventorying the nav (workflow step 2), record the config nesting depth separately from the on-disk depth — they diverge exactly here.
- **This is the canonical case for `navigation.sidebar` over folders.** Materializing a purely-presentational nested group as a subfolder changes URLs (`/platform/analytics/getting-started` → `/platform/analytics/reference/getting-started`) for no reason other than a visual grouping, forcing a `redirects` entry per page. When you want to keep the nesting **and** the URLs, an explicit `navigation.sidebar` group (`{ label, items }`) is the better trade — it nests the existing routes without moving any file. Pick per group; don't reflexively flatten. - **This is the canonical case for `navigation.sidebar` over folders.** Materializing a purely-presentational nested group as a subfolder changes URLs (`/platform/analytics/getting-started` → `/platform/analytics/reference/getting-started`) for no reason other than a visual grouping, forcing a `redirects` entry per page. When you want to keep the nesting **and** the URLs, an explicit `navigation.sidebar` group (`{ label, items }`) is the better trade — it nests the existing routes without moving any file. Pick per group; don't reflexively flatten.
- **`pages`** entries are page refs (paths without extension) → files at the corresponding path. An entry that's `"GET /path"` is an OpenAPI endpoint stub → **delete it** (Blume generates these; see OpenAPI). - **`pages`** entries are page refs (paths without extension) → files at the corresponding path. An entry that's `"GET /path"` is an OpenAPI endpoint stub → **delete it** (Blume generates these; see OpenAPI).
@@ -44,3 +44,4 @@ Include these sections in the PR body or no-op summary:
- Verification run (commands and results) - Verification run (commands and results)
- Skipped checks, with reasons - Skipped checks, with reasons
- Remaining risk or follow-up - Remaining risk or follow-up
# chs-e2e-test
+9 -2
View File
@@ -176,7 +176,14 @@ Validate spec structure and auto-fix common issues:
import { validateSpec, autoFixSpec } from "@json-render/core"; import { validateSpec, autoFixSpec } from "@json-render/core";
const { valid, issues } = validateSpec(spec); const { valid, issues } = validateSpec(spec);
const fixed = autoFixSpec(spec); // issues include: missing_child, invalid_visible (malformed conditions),
// repeat_without_children, repeat_state_mismatch (statePath not an array in state)
const { spec: fixed, fixDetails } = autoFixSpec(spec);
// fixDetails entries are { message, lossy }. Lossless fixes relocate
// misplaced fields; lossy fixes prune dangling children references.
// In a repair loop, withhold lossy fixes until retries are exhausted:
const attempt = autoFixSpec(spec, { lossy: retriesExhausted });
``` ```
## Visibility Conditions ## Visibility Conditions
@@ -253,7 +260,7 @@ The `StateStore` interface: `get(path)`, `set(path, value)`, `update(updates)`,
| `diffToPatches` | Generate RFC 6902 JSON Patch operations from object diff | | `diffToPatches` | Generate RFC 6902 JSON Patch operations from object diff |
| `EditMode` | Type: `"patch" \| "merge" \| "diff"` | | `EditMode` | Type: `"patch" \| "merge" \| "diff"` |
| `validateSpec` | Validate spec structure | | `validateSpec` | Validate spec structure |
| `autoFixSpec` | Auto-fix common spec issues | | `autoFixSpec` | Auto-fix common spec issues; classifies fixes lossy/lossless, `{ lossy: false }` withholds pruning |
| `createSpecStreamCompiler` | Stream JSONL patches into spec | | `createSpecStreamCompiler` | Stream JSONL patches into spec |
| `createJsonRenderTransform` | TransformStream separating text from JSONL in mixed streams | | `createJsonRenderTransform` | TransformStream separating text from JSONL in mixed streams |
| `parseSpecStreamLine` | Parse single JSONL line | | `parseSpecStreamLine` | Parse single JSONL line |
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
name: domain-modeling name: domain-modeling
description: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, record an architectural decision, or when another skill needs to maintain the domain model. description: Build and sharpen a project's domain model. Use when discussing codebase terminology, writing or editing a CONTEXT.md, or recording or editing an ADR.
--- ---
# Domain Modeling # Domain Modeling
@@ -40,15 +40,20 @@ Description:
Options: Options:
-h, --help - Show this help. -h, --help - Show this help.
--workspace <slug> - Target workspace (uses credentials) --workspace <slug> - Target workspace (uses credentials)
-t, --title <title> - Document title (required) -t, --title <title> - Document title (required)
-c, --content <content> - Markdown content (inline) -c, --content <content> - Markdown content (inline)
-f, --content-file <path> - Read content from file -f, --content-file <path> - Read content from file
--project <project> - Attach to project (UUID, slug ID, or name) --project <project> - Attach to project (UUID, slug ID, or name)
--issue <issue> - Attach to issue (identifier like TC-123) --issue <issue> - Attach to issue (identifier like TC-123)
--icon <icon> - Document icon (emoji) --initiative <initiative> - Attach to initiative (UUID, slug ID, or name)
-i, --interactive - Interactive mode with prompts --team <team> - Attach to team (key); with --cycle, scopes the cycle lookup instead
--cycle <cycle> - Attach to cycle: name, number, 'active'/'now', 'next', 'previous', or a relative
offset like +1 (team from --team or config)
--release <release> - Attach to release (UUID, name, or version)
--icon <icon> - Document icon (emoji)
-i, --interactive - Interactive mode with prompts
``` ```
### delete ### delete
@@ -85,12 +90,17 @@ Description:
Options: Options:
-h, --help - Show this help. -h, --help - Show this help.
--workspace <slug> - Target workspace (uses credentials) --workspace <slug> - Target workspace (uses credentials)
--project <project> - Filter by project (slug or name) --project <project> - Filter by project (UUID, slug ID, or name)
--issue <issue> - Filter by issue (identifier like TC-123) --issue <issue> - Filter by issue (identifier like TC-123)
--json - Output as JSON --initiative <initiative> - Filter by initiative (UUID, slug ID, or name)
--limit <limit> - Limit results (Default: 50) --team <team> - Filter by team (key); with --cycle, scopes the cycle lookup instead
--cycle <cycle> - Filter by cycle: name, number, 'active'/'now', 'next', 'previous', or a relative
offset like +1 (team from --team or config)
--release <release> - Filter by release (UUID, name, or version)
--json - Output as JSON
--limit <limit> - Limit results (Default: 50)
``` ```
### update ### update
@@ -106,15 +116,21 @@ Description:
Options: Options:
-h, --help - Show this help. -h, --help - Show this help.
--workspace <slug> - Target workspace (uses credentials) --workspace <slug> - Target workspace (uses credentials)
-t, --title <title> - New title for the document -t, --title <title> - New title for the document
-c, --content <content> - New markdown content (inline) -c, --content <content> - New markdown content (inline)
-f, --content-file <path> - Read new content from file -f, --content-file <path> - Read new content from file
--icon <icon> - New icon (emoji) --icon <icon> - New icon (emoji)
--project <project> - Attach to project (UUID, slug ID, or name) --project <project> - Re-point to project (UUID, slug ID, or name); replaces the current attachment
-e, --edit - Open current content in $EDITOR for editing --issue <issue> - Re-point to issue (identifier like TC-123); replaces the current attachment
--force - Update content even when document comments may lose inline anchors --initiative <initiative> - Re-point to initiative (UUID, slug ID, or name); replaces the current attachment
--team <team> - Re-point to team (key); with --cycle, scopes the cycle lookup instead
--cycle <cycle> - Re-point to cycle: name, number, 'active'/'now', 'next', 'previous', or a
relative offset like +1 (team from --team or config)
--release <release> - Re-point to release (UUID, name, or version); replaces the current attachment
-e, --edit - Open current content in $EDITOR for editing
--force - Update content even when document comments may lose inline anchors
``` ```
### view ### view
+14 -12
View File
@@ -84,19 +84,19 @@ Use these root directories consistently:
- `app/` for runtime application code - `app/` for runtime application code
- `db/` for migrations and local database files - `db/` for migrations and local database files
- `public/` for static assets served as-is - root `public/` for static assets served as-is from the app root
- `test/` for shared helpers, fixtures, and integration coverage - `test/` for shared helpers, fixtures, and integration coverage
- `tmp/` for uploads, caches, local session files, and other scratch data - `tmp/` for uploads, caches, local session files, and other scratch data
Inside `app/`, organize by responsibility: Inside `app/`, organize by responsibility:
- `assets/` for client entrypoints and client-owned browser behavior
- `actions/` for controller-owned route handlers, route-local response rendering, and route-local UI/helpers that are not shared across route areas - `actions/` for controller-owned route handlers, route-local response rendering, and route-local UI/helpers that are not shared across route areas
- `data/` for schema, queries, persistence setup, migrations, and runtime data initialization - `data/` for schema, queries, persistence setup, migrations, and runtime data initialization
- `middleware/` for request lifecycle concerns such as auth, sessions, uploads, and database injection - `middleware/` for request lifecycle concerns such as auth, sessions, uploads, and database injection
- `public/` directories inside the narrowest owner for browser-reachable source code, with the browser runtime entrypoint at `app/actions/public/entry.ts`
- `ui/` for shared cross-route UI primitives - `ui/` for shared cross-route UI primitives
- `utils/` only for genuinely cross-layer helpers that do not clearly belong elsewhere - `utils/` only for genuinely cross-layer helpers that do not clearly belong elsewhere
- `routes.ts` for the route contract - `routes.ts` for the shared server-and-browser route contract and type-safe href generation
- `router.ts` for router setup and wiring - `router.ts` for router setup and wiring
### Placement Precedence ### Placement Precedence
@@ -148,10 +148,11 @@ When code could live in multiple places:
- Model HTTP behavior explicitly. Status codes, headers, redirects, cache rules, and content types are part of the route contract - Model HTTP behavior explicitly. Status codes, headers, redirects, cache rules, and content types are part of the route contract
- Make the server route correct first. A POST should already return the right HTML, redirect, or error response on its own before `clientEntry(...)` layers interactivity on top - Make the server route correct first. A POST should already return the right HTML, redirect, or error response on its own before `clientEntry(...)` layers interactivity on top
- Validate input at the boundary using `remix/data-schema` (and `remix/data-schema/form-data` for forms). `parseSafe` makes the failure path a return value instead of an exception - Validate input at the boundary using `remix/data-schema` (and `remix/data-schema/form-data` for forms). `parseSafe` makes the failure path a return value instead of an exception
- Derive `AppContext` from the middleware stack so `get(Database)`, `get(Session)`, `get(Auth)`, and similar keys stay typed. If the controller never reads from context, it doesn't need the harness - Derive `AppContext` from the middleware stack so `get(databaseContext)`, `get(Session)`, `get(Auth)`, and similar keys stay typed. If the controller never reads from context, it doesn't need the harness
- Outside actions and controllers, only use `getContext()` when `asyncContext()` is in the middleware stack - Outside actions and controllers, only use `getContext()` when `asyncContext()` is in the middleware stack
- Remix Component is not React: write `function Name(handle: Handle<Props>) { return () => ... }`, read props from `handle.props`, keep state in setup-scope variables, call `handle.update()` explicitly, and do DOM-sensitive work in event handlers or `queueTask(...)`, not in render - Remix Component is not React: write `function Name(handle: Handle<Props>) { return () => ... }`, read props from `handle.props`, keep state in setup-scope variables, call `handle.update()` explicitly, and do DOM-sensitive work in event handlers or `queueTask(...)`, not in render
- Prefer host-element mixins via `mix={mixin(...)}` for behavior and styling instead of inventing custom host prop conventions. Use `mix={[...]}` only when composing multiple mixins - Prefer host-element mixins via `mix={mixin(...)}` for behavior and styling instead of inventing custom host prop conventions. Use `mix={[...]}` only when composing multiple mixins
- Keep short, one-off static styles inline with `mix={css(...)}`. Extract a module-scoped style descriptor when it forms a reused visual recipe, has substantial selectors, media queries, or keyframes, or is large enough to obscure the component. Export a style descriptor only after multiple modules need the same visual recipe; otherwise keep it with its narrowest owner
- Hydrated `clientEntry(...)` props must be serializable. Do not pass functions, class instances, or opaque runtime objects - Hydrated `clientEntry(...)` props must be serializable. Do not pass functions, class instances, or opaque runtime objects
## Security And Session Defaults ## Security And Session Defaults
@@ -227,12 +228,12 @@ Use this map to find the right package quickly. Each entry says what the package
- `remix/data-schema/coerce` — coercion helpers for strings, numbers, booleans, dates, and ids. Use when input arrives as a string but should be a typed value - `remix/data-schema/coerce` — coercion helpers for strings, numbers, booleans, dates, and ids. Use when input arrives as a string but should be a typed value
- `remix/data-schema/form-data` — `f.object` and `f.field` for parsing `FormData` directly. Use in actions that read browser forms - `remix/data-schema/form-data` — `f.object` and `f.field` for parsing `FormData` directly. Use in actions that read browser forms
- `remix/data-schema/lazy` — recursive or mutually-referential schemas. Use when a schema needs to refer to itself or another schema that is declared later - `remix/data-schema/lazy` — recursive or mutually-referential schemas. Use when a schema needs to refer to itself or another schema that is declared later
- `remix/data-table` — typed tables and a `Database` interface. Use for `table`, `column`, `createDatabase` when modeling persisted data - `remix/data-table` — typed tables and the shared `Database` API. Use `table` and `column` when modeling persisted data, then create a concrete database from the matching dialect package. Integration packages can implement `DatabaseDriver` and extend `Database` to add another SQL dialect
- `remix/data-table/sqlite`, `remix/data-table/postgres`, `remix/data-table/mysql` — adapters. Use to back `createDatabase` with a real engine. SQLite accepts Node, Bun, and compatible synchronous clients with the shared `prepare`/`exec` surface - `remix/data-table/sqlite`, `remix/data-table/postgres`, `remix/data-table/mysql` — concrete database integrations. Use `createSqliteDatabase`, `createPostgresDatabase`, or `createMysqlDatabase`. SQLite accepts Node, Bun, and compatible synchronous clients with the shared `prepare`/`exec` surface
- `remix/data-table/migrations` — migration authoring and runners. Use for `createMigration`, `createMigrationRunner` - `remix/data-table/migrations` — migration authoring and registries. Use for `createMigration` and `createMigrationRegistry`; run migrations with `Database.migrate()`
- `remix/data-table/migrations/node` — `loadMigrations` from disk. Use in startup scripts that apply migrations - `remix/data-table/migrations/node` — `loadMigrations` from disk. Use in startup scripts that apply migrations
- `remix/data-table/operators` — query operators such as `inList(...)`. Use when `where` clauses need set or comparison logic - `remix/data-table/operators` — query operators such as `inList(...)`. Use when `where` clauses need set or comparison logic
- `remix/data-table/sql-helpers` — SQL helper utilities for adapter or advanced query work. Avoid this in normal app code unless you are intentionally working below the table/query API - `remix/data-table/sql-helpers` — SQL helper utilities for database integrations or advanced query work. Avoid this in normal app code unless you are intentionally working below the table/query API
### Auth, Sessions, and Cookies ### Auth, Sessions, and Cookies
@@ -318,17 +319,18 @@ export const routes = route({
```typescript ```typescript
import { createController } from 'remix/router' import { createController } from 'remix/router'
import { databaseContext } from '../middleware/database.ts'
import { routes } from '../routes.ts' import { routes } from '../routes.ts'
export default createController(routes.books, { export default createController(routes.books, {
actions: { actions: {
async index({ get }) { async index({ get }) {
let db = get(Database) let db = get(databaseContext)
let allBooks = await db.findMany(books, { orderBy: ['id', 'asc'] }) let allBooks = await db.findMany(books, { orderBy: ['id', 'asc'] })
return render(<BooksIndexPage allBooks={allBooks} />) return render(<BooksIndexPage allBooks={allBooks} />)
}, },
async show({ get, params }) { async show({ get, params }) {
let db = get(Database) let db = get(databaseContext)
let book = await db.findOne(books, { where: { slug: params.slug } }) let book = await db.findOne(books, { where: { slug: params.slug } })
if (!book) return new Response('Not Found', { status: 404 }) if (!book) return new Response('Not Found', { status: 404 })
return render(<BookShowPage book={book} />) return render(<BookShowPage book={book} />)
@@ -393,8 +395,8 @@ import { redirect } from 'remix/response/redirect'
import * as s from 'remix/data-schema' import * as s from 'remix/data-schema'
import * as f from 'remix/data-schema/form-data' import * as f from 'remix/data-schema/form-data'
import { Session } from 'remix/session' import { Session } from 'remix/session'
import { Database } from 'remix/data-table'
import { databaseContext } from '../middleware/database.ts'
import { routes } from '../routes.ts' import { routes } from '../routes.ts'
let bookSchema = f.object({ let bookSchema = f.object({
@@ -410,7 +412,7 @@ export default createController(routes.books, {
return render(<NewBookPage errors={parsed.issues} />, { status: 400 }) return render(<NewBookPage errors={parsed.issues} />, { status: 400 })
} }
let db = get(Database) let db = get(databaseContext)
let book = await db.create(books, parsed.value) let book = await db.create(books, parsed.value)
let session = get(Session) let session = get(Session)
@@ -8,13 +8,13 @@ How to serve browser scripts and styles from source. Read this when the task inv
- Choosing between `staticFiles()` for already-built files and `createAssetServer()` for source assets that need import rewriting, preloads, or fingerprinted URLs - Choosing between `staticFiles()` for already-built files and `createAssetServer()` for source assets that need import rewriting, preloads, or fingerprinted URLs
- Generating script URLs or `<link rel="modulepreload">` tags for a client entry - Generating script URLs or `<link rel="modulepreload">` tags for a client entry
- Enabling browser HMR for source-served modules - Enabling browser HMR for source-served modules
- Keeping server-only files out of the browser via `denyFiles` rules - Keeping files such as tests out of the browser via `denyFiles` rules
For routing the URL namespace itself, see `routing-and-controllers.md`. For client entry hydration and browser update handling, see `hydration-frames-navigation.md`. For the Node HMR runner and browser HMR channel, see `middleware-and-server.md`. For routing the URL namespace itself, see `routing-and-controllers.md`. For client entry hydration and browser update handling, see `hydration-frames-navigation.md`. For the Node HMR runner and browser HMR channel, see `middleware-and-server.md`.
## When To Reach For It ## When To Reach For It
Use `remix/assets` when the app serves browser JavaScript, TypeScript, or CSS from source files. This is the right tool for client entrypoints, browser-only helpers, styles under `app/assets/`, and monorepo code that should be compiled and served under a public URL namespace. Use `remix/assets` when the app serves browser JavaScript, TypeScript, or CSS from source files. This is the right tool for client entrypoints, browser-only helpers, styles, and monorepo code that should be compiled and served under a public URL namespace.
Use `staticFiles()` for files that already exist on disk exactly as they should be served. Use `createAssetServer()` for source scripts or styles that need rewriting, dependency scanning, preloads, sourcemaps, or fingerprinted URLs. Use `staticFiles()` for files that already exist on disk exactly as they should be served. Use `createAssetServer()` for source scripts or styles that need rewriting, dependency scanning, preloads, sourcemaps, or fingerprinted URLs.
@@ -36,9 +36,9 @@ let assetServer = createAssetServer({
'app/*path': 'app/*path', 'app/*path': 'app/*path',
'node_modules/*path': 'node_modules/*path', 'node_modules/*path': 'node_modules/*path',
}, },
allowFiles: ['app/assets/**'], allowFiles: ['app/routes.ts', 'app/**/public/**'],
allowPackages: ['remix'], allowPackages: ['remix'],
denyFiles: ['app/**/*.server.*'], denyFiles: ['app/**/*.test.*'],
target: { es: '2020', chrome: '109', safari: '16.4' }, target: { es: '2020', chrome: '109', safari: '16.4' },
sourceMaps: process.env.NODE_ENV === 'development' ? 'external' : undefined, sourceMaps: process.env.NODE_ENV === 'development' ? 'external' : undefined,
minify: process.env.NODE_ENV === 'production', minify: process.env.NODE_ENV === 'production',
@@ -61,10 +61,12 @@ export default createController(routes, {
## Rules ## Rules
- Treat `allowFiles`/`allowPackages` and `denyFiles` as the security boundary for browser-reachable source files. - Treat `allowFiles`/`allowPackages` and `denyFiles` as the security boundary for browser-reachable source files.
- Put browser-reachable app source in a `public/` directory inside `app/`, beside its narrowest owner, such as `app/ui/public/` or `app/actions/cart/public/`.
- Every local dependency in a browser module graph must match `allowFiles`, so keep the whole graph inside those `public/` directories. `app/routes.ts` is allowed separately so browser modules can build type-safe links with `routes.*.href(...)`.
- Deny test modules with `denyFiles` so tests can be colocated inside a `public/` directory without becoming browser-reachable.
- Use `allowFiles` and `denyFiles` for file paths and globs. Relative values resolve from `rootDir`. - Use `allowFiles` and `denyFiles` for file paths and globs. Relative values resolve from `rootDir`.
- Use `allowPackages` for exact package names, not globs or subpaths. Packages allowed by `allowPackages` also allow their installed `dependencies` and `optionalDependencies`; peer dependencies must be listed explicitly if they should be browser-reachable. - Use `allowPackages` for exact package names, not globs or subpaths. Packages allowed by `allowPackages` also allow their installed `dependencies` and `optionalDependencies`; peer dependencies must be listed explicitly if they should be browser-reachable.
- `denyFiles` takes precedence over both file and package allow rules. - `denyFiles` takes precedence over both file and package allow rules.
- Add `denyFiles` rules for server-only modules such as `*.server.*`, private config, or other files that should never be exposed.
- Set `rootDir` explicitly in monorepos so relative paths resolve from the intended project root. - Set `rootDir` explicitly in monorepos so relative paths resolve from the intended project root.
- `basePath` is the public URL namespace handled by the asset server. - `basePath` is the public URL namespace handled by the asset server.
- `fileMap` keys are URL patterns relative to `basePath`, and values are root-relative file path patterns. They use `route-pattern` syntax on both sides. - `fileMap` keys are URL patterns relative to `basePath`, and values are root-relative file path patterns. They use `route-pattern` syntax on both sides.
@@ -76,8 +78,8 @@ export default createController(routes, {
Use `getHref()` when you need the public URL for one module, and `getPreloads()` when you want `<link rel="modulepreload">` tags or `Link` headers for one or more entrypoints and their dependencies. Use `getHref()` when you need the public URL for one module, and `getPreloads()` when you want `<link rel="modulepreload">` tags or `Link` headers for one or more entrypoints and their dependencies.
```typescript ```typescript
let entryHref = await assetServer.getHref('app/assets/entry.ts') let entryHref = await assetServer.getHref('app/actions/public/entry.ts')
let preloads = await assetServer.getPreloads(['app/assets/entry.ts']) let entryPreloads = await assetServer.getPreloads('app/actions/public/entry.ts')
``` ```
Use this when rendering documents or layouts that boot browser behavior with a known client entry. Use this when rendering documents or layouts that boot browser behavior with a known client entry.
@@ -116,7 +118,8 @@ const isHmr = Boolean(isDevelopment && process.env.REMIX_NODE_HMR)
const assetServer = createAssetServer({ const assetServer = createAssetServer({
basePath: '/assets', basePath: '/assets',
fileMap: { '/app/*path': 'app/*path' }, fileMap: { '/app/*path': 'app/*path' },
allowFiles: ['app/assets/**'], allowFiles: ['app/routes.ts', 'app/**/public/**'],
denyFiles: ['app/**/*.test.*'],
watch: isDevelopment, watch: isDevelopment,
hmr: isHmr hmr: isHmr
? async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel() ? async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel()
@@ -137,7 +137,7 @@ Notice that there is no manual `Set-Cookie` plumbing in the action — the sessi
```typescript ```typescript
import { auth, createSessionAuthScheme } from 'remix/middleware/auth' import { auth, createSessionAuthScheme } from 'remix/middleware/auth'
import { Session } from 'remix/session' import { Session } from 'remix/session'
import { Database } from 'remix/data-table' import { databaseContext } from '~/middleware/database.ts'
export function loadAuth() { export function loadAuth() {
return auth({ return auth({
@@ -148,7 +148,7 @@ export function loadAuth() {
return data ?? null return data ?? null
}, },
async verify(value, context) { async verify(value, context) {
let db = context.get(Database) let db = context.get(databaseContext)
return (await db.find(users, value.userId)) ?? null return (await db.find(users, value.userId)) ?? null
}, },
invalidate(session) { invalidate(session) {
@@ -195,7 +195,7 @@ export let passwordProvider = createCredentialsAuthProvider({
return s.parse(loginSchema, formData) return s.parse(loginSchema, formData)
}, },
async verify({ email, password }, context) { async verify({ email, password }, context) {
let db = context.get(Database) let db = context.get(databaseContext)
let user = await db.findOne(users, { where: { email } }) let user = await db.findOne(users, { where: { email } })
if (!user || !(await verifyPassword(password, user.password_hash))) { if (!user || !(await verifyPassword(password, user.password_hash))) {
return null return null
@@ -300,7 +300,7 @@ export default createController(routes.auth.google, {
async callback(context) { async callback(context) {
let { result, returnTo } = await finishExternalAuth(googleProvider, context) let { result, returnTo } = await finishExternalAuth(googleProvider, context)
let db = context.get(Database) let db = context.get(databaseContext)
let { user, authAccount } = await resolveExternalAuth(db, result) let { user, authAccount } = await resolveExternalAuth(db, result)
let session = completeAuth(context) let session = completeAuth(context)
@@ -322,7 +322,7 @@ Use `refreshExternalAuth(provider, tokens)` when an app has stored OAuth/OIDC to
```typescript ```typescript
async function refreshGoogleTokens({ get }) { async function refreshGoogleTokens({ get }) {
let db = get(Database) let db = get(databaseContext)
let account = await db.findOne(authAccounts, { where: { provider: 'google' } }) let account = await db.findOne(authAccounts, { where: { provider: 'google' } })
if (!account) return null if (!account) return null
@@ -113,30 +113,30 @@ export const books = table({
## Database Setup ## Database Setup
Create a database with an adapter and expose it via middleware: Create a database and expose it via middleware:
```typescript ```typescript
import BetterSqlite3 from 'better-sqlite3' import BetterSqlite3 from 'better-sqlite3'
import { createDatabase, Database } from 'remix/data-table' import { createSqliteDatabase } from 'remix/data-table/sqlite'
import { createSqliteDatabaseAdapter } from 'remix/data-table/sqlite'
let sqlite = new BetterSqlite3('./db/app.db') let sqlite = new BetterSqlite3('./db/app.db')
sqlite.pragma('foreign_keys = ON') sqlite.pragma('foreign_keys = ON')
let adapter = createSqliteDatabaseAdapter(sqlite) export let db = createSqliteDatabase(sqlite)
export let db = createDatabase(adapter)
``` ```
`createSqliteDatabaseAdapter` accepts synchronous SQLite clients with a shared `prepare`/`exec` surface, including Node's `node:sqlite`, Bun's `bun:sqlite`, and compatible clients. Use whichever client fits the runtime instead of assuming `better-sqlite3` is required. `createSqliteDatabase` accepts synchronous SQLite clients with a shared `prepare`/`exec` surface, including Node's `node:sqlite`, Bun's `bun:sqlite`, and compatible clients. Use whichever client fits the runtime instead of assuming `better-sqlite3` is required.
### Database middleware ### Database middleware
```typescript ```typescript
import type { Middleware } from 'remix/router' import type { Database } from 'remix/data-table'
import { Database } from 'remix/data-table' import { createContextKey, type Middleware } from 'remix/router'
export const databaseContext = createContextKey<Database>()
export function loadDatabase(): Middleware { export function loadDatabase(): Middleware {
return async (context, next) => { return async (context, next) => {
context.set(Database, db) context.set(databaseContext, db)
return next() return next()
} }
} }
@@ -145,7 +145,7 @@ export function loadDatabase(): Middleware {
### Querying ### Querying
```typescript ```typescript
let db = get(Database) let db = get(databaseContext)
// Find by primary key // Find by primary key
let book = await db.find(books, id) let book = await db.find(books, id)
@@ -220,7 +220,7 @@ Do **not** import app code (e.g. `app/data/schema.ts`) into migration files. Mig
### Transaction modes ### Transaction modes
Migrations run inside a transaction by default (when the adapter supports transactional DDL). Override per migration with a directive comment in `up.sql`: Migrations run inside a transaction by default (when the database supports transactional DDL). Override per migration with a directive comment in `up.sql`:
```sql ```sql
-- data-table/transaction: none -- data-table/transaction: none
@@ -232,15 +232,15 @@ Modes: `auto` (default — wrap when supported), `required` (wrap; throw if unsu
### Running migrations ### Running migrations
```typescript ```typescript
import { createMigrationRunner } from 'remix/data-table/migrations'
import { loadMigrations } from 'remix/data-table/migrations/node' import { loadMigrations } from 'remix/data-table/migrations/node'
let migrations = await loadMigrations('./db/migrations') let migrations = await loadMigrations('./db/migrations')
let runner = createMigrationRunner(adapter, migrations) await db.migrate(migrations)
await runner.up()
``` ```
The runner checksums each `up.sql` and detects drift if a previously applied migration changes. Use `runner.status()` to inspect applied/pending/drifted state, and `runner.down()` to revert. The database checksums each `up.sql` and detects drift if a previously applied migration changes.
Use `db.migrationStatus(migrations)` to inspect applied/pending/drifted state, and
`db.migrate(migrations, { direction: 'down' })` to revert.
## Input Validation (`remix/data-schema`) ## Input Validation (`remix/data-schema`)
@@ -95,6 +95,7 @@ Client entry props must be serializable: strings, numbers, booleans, `null`, `un
Use `run` to start the client runtime. It scans the document for client entry markers, loads modules, and hydrates each one: Use `run` to start the client runtime. It scans the document for client entry markers, loads modules, and hydrates each one:
```tsx ```tsx
import type { ResolveFrameOptions } from 'remix/ui'
import { run } from 'remix/ui' import { run } from 'remix/ui'
const app = run({ const app = run({
@@ -102,14 +103,31 @@ const app = run({
let mod = await import(moduleUrl) let mod = await import(moduleUrl)
return mod[exportName] return mod[exportName]
}, },
async resolveFrame(src, signal, target) { async resolveFrame(src, options) {
let headers = new Headers({ accept: 'text/html' }) let headers = new Headers({ accept: 'text/html', 'x-remix-frame': 'true' })
if (target) headers.set('x-remix-target', target) if (options?.target) headers.set('x-remix-target', options.target)
let response = await fetch(src, { headers, signal }) let response = await fetch(src, {
body: getRequestBody(options),
headers,
method: options?.method,
signal: options?.signal,
})
return response.body ?? (await response.text()) return response.body ?? (await response.text())
}, },
}) })
function getRequestBody(options?: ResolveFrameOptions): BodyInit | undefined {
let formData = options?.formData
if (!formData) return
if (options.encType !== 'application/x-www-form-urlencoded') return formData
let body = new URLSearchParams()
for (let [name, value] of formData) {
body.append(name, typeof value === 'string' ? value : value.name)
}
return body
}
app.addEventListener('error', (event) => { app.addEventListener('error', (event) => {
console.error('Component error:', event.error) console.error('Component error:', event.error)
}) })
@@ -120,7 +138,7 @@ await app.ready()
### `run` options ### `run` options
- **`loadModule(moduleUrl, exportName)`** (required) — return the component function for each client entry. Typically uses dynamic `import()`. - **`loadModule(moduleUrl, exportName)`** (required) — return the component function for each client entry. Typically uses dynamic `import()`.
- **`resolveFrame(src, signal, target)`** (optional) — called when a `<Frame>` loads or reloads content. `target` is available when frame targeting matters. - **`resolveFrame(src, options)`** (optional) — called when a `<Frame>` loads or reloads content and for intercepted link and form navigations. `options` may contain `signal` and `target`; non-GET forms also provide `formData`, `method`, and `encType`.
### `app` methods ### `app` methods
@@ -189,6 +207,20 @@ handle.frames.top.reload()
When a frame reloads, matching DOM nodes are updated in place. Client entries receive updated props while preserving their local component state. When a frame reloads, matching DOM nodes are updated in place. Client entries receive updated props while preserving their local component state.
### Form navigation
When `run({ resolveFrame })` is active, eligible same-origin forms progressively enhance into frame navigations. Native validation and the form's `submit` event still run first.
- Forms target `handle.frames.top` by default.
- `rmx-target` selects a named frame.
- `rmx-src` selects a different frame request URL while preserving the form action as the navigation destination.
- `rmx-history="push|replace"` overrides how the navigation updates history.
- `rmx-reset-scroll="false"` preserves scroll position.
- `rmx-document` opts back into a document submission.
- Cross-origin forms, `method="dialog"`, and `target="_blank"` remain browser-owned.
GET controls are already encoded in `src`, so GET forms reach the resolver like links. Non-GET forms provide their native `FormData`, effective method, and encoding. The resolver owns body encoding and method-override conventions. Non-GET submissions to the current URL replace its history entry; GET submissions and submissions to a different URL push one. The `rmx-history` attribute overrides those defaults.
### Nested frames ### Nested frames
Frames can nest. Each frame owns its own DOM region and hydrates client entries independently. During SSR, `handle.frame.src` points at the frame being rendered, while `handle.frames.top.src` stays fixed at the outer document URL. Frames can nest. Each frame owns its own DOM region and hydrates client entries independently. During SSR, `handle.frame.src` points at the frame being rendered, while `handle.frames.top.src` stays fixed at the outer document URL.
@@ -252,7 +284,7 @@ navigate('/dashboard', { history: 'replace' })
Options: `src`, `target`, `history` (`'push' | 'replace'`), `resetScroll`. Options: `src`, `target`, `history` (`'push' | 'replace'`), `resetScroll`.
Attributes understood by the runtime: `rmx-target`, `rmx-src`, `rmx-document`. Attributes understood by the runtime: `rmx-target`, `rmx-src`, `rmx-history`, `rmx-reset-scroll`, `rmx-document`.
## Head Management ## Head Management
@@ -65,8 +65,9 @@ let router = createRouter({ middleware })
### Static files vs browser modules ### Static files vs browser modules
- Use `staticFiles()` for files that should be served directly from disk, such as images, fonts, or already-built assets in `public/` - Use `staticFiles()` for files that should be served directly from disk, such as images, fonts, or already-built assets in the root `public/` directory
- Use `remix/assets` when browser modules should be compiled and served from source files with import rewriting, preloads, or fingerprinted URLs - Use `remix/assets` when browser modules should be compiled and served from source files with import rewriting, preloads, or fingerprinted URLs
- `public/` directories inside `app/` hold browser-reachable source for the asset server
### Ordering notes ### Ordering notes
@@ -118,11 +119,11 @@ Use `context.set(key, value)` to add typed values accessible downstream via `con
```typescript ```typescript
import type { Middleware } from 'remix/router' import type { Middleware } from 'remix/router'
import { Database } from 'remix/data-table' import { databaseContext } from '~/middleware/database.ts'
export function loadDatabase(): Middleware { export function loadDatabase(): Middleware {
return async (context, next) => { return async (context, next) => {
context.set(Database, db) context.set(databaseContext, db)
return next() return next()
} }
} }
@@ -152,11 +153,11 @@ export function requireAdmin(): Middleware {
// app/utils/context.ts // app/utils/context.ts
import { getContext } from 'remix/middleware/async-context' import { getContext } from 'remix/middleware/async-context'
import { Auth } from 'remix/middleware/auth' import { Auth } from 'remix/middleware/auth'
import { Database } from 'remix/data-table' import { databaseContext } from '~/middleware/database.ts'
import { Session } from 'remix/session' import { Session } from 'remix/session'
export function getCurrentDb() { export function getCurrentDb() {
return getContext().get(Database) return getContext().get(databaseContext)
} }
export function getCurrentSession() { export function getCurrentSession() {
@@ -115,6 +115,8 @@ Adds client-side navigation behavior to any element. Makes non-anchor elements b
Options match `NavigationOptions`: `src`, `target`, `history` (`'push' | 'replace'`), `resetScroll`. Options match `NavigationOptions`: `src`, `target`, `history` (`'push' | 'replace'`), `resetScroll`.
On a native anchor, the `history` option renders the corresponding `rmx-history="push|replace"` attribute so the enhanced navigation uses the same history behavior.
## Native press and keyboard interactions ## Native press and keyboard interactions
Use native DOM events directly with `on(...)`. For buttons and links, `click` already includes keyboard activation when the element has the right semantics: Use native DOM events directly with `on(...)`. For buttons and links, `click` already includes keyboard activation when the element has the right semantics:
@@ -103,7 +103,7 @@ export const search = createAction(routes.search, {
The handler receives a context object with: The handler receives a context object with:
- `get(key)` — read a value set by middleware (e.g. `get(Database)`, `get(Session)`, `get(Auth)`) - `get(key)` — read a value set by middleware (e.g. `get(databaseContext)`, `get(Session)`, `get(Auth)`)
- `params` — typed route params - `params` — typed route params
- `url` — the request URL - `url` — the request URL
- `request` — the raw `Request` - `request` — the raw `Request`
@@ -132,7 +132,7 @@ For pages, render a component tree and return the resulting `Response`:
```typescript ```typescript
async handler({ get }) { async handler({ get }) {
let db = get(Database) let db = get(databaseContext)
let books = await db.findMany(books, { orderBy: ['id', 'asc'] }) let books = await db.findMany(books, { orderBy: ['id', 'asc'] })
return render(<IndexPage books={books} />) return render(<IndexPage books={books} />)
} }
@@ -152,7 +152,7 @@ async create({ get }) {
return render(<NewBookPage errors={parsed.issues} />, { status: 400 }) return render(<NewBookPage errors={parsed.issues} />, { status: 400 })
} }
let db = get(Database) let db = get(databaseContext)
let book = await db.create(books, parsed.value) let book = await db.create(books, parsed.value)
return redirect(routes.books.show.href({ slug: book.slug }), 303) return redirect(routes.books.show.href({ slug: book.slug }), 303)
@@ -167,7 +167,7 @@ For expected failures — validation, conflict, not found — return a `Response
```typescript ```typescript
async show({ get, params }) { async show({ get, params }) {
let db = get(Database) let db = get(databaseContext)
let book = await db.find(books, params.bookId) let book = await db.find(books, params.bookId)
if (!book) return new Response('Not Found', { status: 404 }) if (!book) return new Response('Not Found', { status: 404 })
return render(<ShowPage book={book} />) return render(<ShowPage book={book} />)
@@ -208,7 +208,7 @@ If you find yourself returning JSON for what is really a browser form submission
A controller owns the direct leaf routes in one route map. Each key in `actions` matches a direct leaf route key in the route definition passed to `router.map(...)`. Nested route-map keys do not belong inside a controller's `actions`; map those route maps with their own controllers. A controller owns the direct leaf routes in one route map. Each key in `actions` matches a direct leaf route key in the route definition passed to `router.map(...)`. Nested route-map keys do not belong inside a controller's `actions`; map those route maps with their own controllers.
Configure `RouterTypes.context` with your app context in the router module, then use `createController()` so `get(Database)`, `get(Session)`, `get(Auth)`, etc. are typed against your middleware stack without repeating a type clause on every controller. Configure `RouterTypes.context` with your app context in the router module, then use `createController()` so `get(databaseContext)`, `get(Session)`, `get(Auth)`, etc. are typed against your middleware stack without repeating a type clause on every controller.
```typescript ```typescript
import { createController } from 'remix/router' import { createController } from 'remix/router'
@@ -218,13 +218,13 @@ import { routes } from '../routes.ts'
export default createController(routes.books, { export default createController(routes.books, {
actions: { actions: {
async index({ get }) { async index({ get }) {
let db = get(Database) let db = get(databaseContext)
let items = await db.findMany(books, { orderBy: ['id', 'asc'] }) let items = await db.findMany(books, { orderBy: ['id', 'asc'] })
return render(<IndexPage items={items} />) return render(<IndexPage items={items} />)
}, },
async show({ get, params }) { async show({ get, params }) {
let db = get(Database) let db = get(databaseContext)
let book = await db.find(books, params.bookId) let book = await db.find(books, params.bookId)
if (!book) return new Response('Not Found', { status: 404 }) if (!book) return new Response('Not Found', { status: 404 })
return render(<ShowPage book={book} />) return render(<ShowPage book={book} />)
@@ -359,4 +359,4 @@ declare module 'remix/router' {
} }
``` ```
This gives typed `context.get(Database)`, `context.get(Session)`, `context.get(Auth)`, etc. This gives typed `context.get(databaseContext)`, `context.get(Session)`, `context.get(Auth)`, etc.
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
name: rust-testing name: rust-testing
description: Rust testing patterns including unit tests, integration tests, async testing, property-based testing, mocking, and coverage. Follows TDD methodology. description: Rust testing patterns including unit tests, integration tests, async testing, property-based testing, mocking, and coverage. Follows TDD methodology. Use when writing Rust tests — unit, integration, async, property-based, or coverage.
metadata: metadata:
origin: ECC origin: ECC
--- ---
+3 -4
View File
@@ -25,13 +25,12 @@ fi
# Flyline — enhanced Bash line editing (machine-optional, guarded) # Flyline — enhanced Bash line editing (machine-optional, guarded)
[ -f "$HOME/.local/lib/libflyline.so" ] && enable -f "$HOME/.local/lib/libflyline.so" flyline [ -f "$HOME/.local/lib/libflyline.so" ] && enable -f "$HOME/.local/lib/libflyline.so" flyline
# atuin shell history (needs bash-preexec)
[ -f "$HOME/.atuin/bin/env" ] && . "$HOME/.atuin/bin/env"
[ -f "$HOME/.bash-preexec.sh" ] && . "$HOME/.bash-preexec.sh"
command -v atuin >/dev/null 2>&1 && eval "$(atuin init bash)"
# Generated for envman. Do not edit. # Generated for envman. Do not edit.
[ -s "$HOME/.config/envman/load.sh" ] && . "$HOME/.config/envman/load.sh" [ -s "$HOME/.config/envman/load.sh" ] && . "$HOME/.config/envman/load.sh"
# Prompt — starship, consistent with fish # Prompt — starship, consistent with fish
command -v starship >/dev/null 2>&1 && eval "$(starship init bash)" command -v starship >/dev/null 2>&1 && eval "$(starship init bash)"
. "$HOME/.atuin/bin/env"
eval "$(atuin init bash)"
+4 -4
View File
@@ -262,13 +262,13 @@
"matcher": ".*" "matcher": ".*"
}, },
{ {
"matcher": "tldraw-offline",
"hooks": [ "hooks": [
{ {
"type": "command", "command": "sh '/home/prad/skills/tldraw-offline/inject-server-context.sh' SubagentStart",
"command": "sh '/home/prad/skills/tldraw-offline/inject-server-context.sh' SubagentStart" "type": "command"
} }
] ],
"matcher": "tldraw-offline"
} }
], ],
"SubagentStop": [ "SubagentStop": [
-1
View File
@@ -7,7 +7,6 @@ fish_add_path $HOME/.grok/bin
fish_add_path $HOME/.opencode/bin fish_add_path $HOME/.opencode/bin
fish_add_path -a $HOME/.foundry/bin fish_add_path -a $HOME/.foundry/bin
fish_add_path -a $HOME/local/share/pnpm fish_add_path -a $HOME/local/share/pnpm
source $HOME/.atuin/bin/env.fish
# pnpm # pnpm
set -gx PNPM_HOME $HOME/.local/share/pnpm set -gx PNPM_HOME $HOME/.local/share/pnpm
-2
View File
@@ -1,2 +0,0 @@
source "$HOME/.atuin/bin/env.fish"
+2 -3
View File
@@ -30,10 +30,9 @@ status is-interactive; and test "$FISH_BOTTOM_PROMPT" = 1; and tput cup $LINES 0
fish_add_path $HOME/.grok/bin fish_add_path $HOME/.grok/bin
# <<< grok installer <<< # <<< grok installer <<<
# Generated for envman. Do not edit.
test -s ~/.config/envman/load.fish; and source ~/.config/envman/load.fish
if status is-interactive if status is-interactive
atuin init fish | source atuin init fish | source
end end
# Generated for envman. Do not edit.
test -s ~/.config/envman/load.fish; and source ~/.config/envman/load.fish
@@ -1,25 +1,33 @@
function fish_user_key_bindings function fish_user_key_bindings
# Keys owned by kitty.conf never reach fish and are NOT bound here: # Vi mode first: it rebinds the preset tables and (on first activation)
# ctrl-g → lazygit overlay, ctrl-a → omp hsplit, ctrl-y → yazi vsplit, # re-triggers this function via the fish_key_bindings handler, so custom
# ctrl-h/j/k/l → vim-aware split nav (pass_keys.py). # binds must come after it to land on top.
# ctrl-e: yazi, same as ctrl-f (cd's to yazi's exit dir on quit). fish_vi_key_bindings
bind ctrl-e 'yy; commandline -f repaint'
bind ctrl-b purple repaint
bind ctrl-y clip-last
bind ctrl-o 'gho; commandline -f repaint' # Vi mode types in `insert`; bind customs there AND in `default` (normal)
bind ctrl-p 'pj; commandline -f repaint' # so the keys work regardless of mode.
bind ctrl-a 'omp; commandline -f repaint' for mode in insert default
# bind \ca 'launch_omp_livediff_overlay; commandline -f repaint' # Keys owned by kitty.conf never reach fish and are NOT bound here:
# ctrl-g → lazygit overlay, ctrl-a → omp hsplit, ctrl-y → yazi vsplit,
# ctrl-h/j/k/l → vim-aware split nav (pass_keys.py).
# ctrl-e: yazi, same as ctrl-f (cd's to yazi's exit dir on quit).
bind -M $mode ctrl-e 'yy; commandline -f repaint'
bind -M $mode ctrl-b purple repaint
bind -M $mode ctrl-y clip-last
# ctrl-f: pick file(s) with television and open in nvim. bind -M $mode ctrl-o 'gho; commandline -f repaint'
bind ctrl-f 'yy; commandline -f repaint' bind -M $mode ctrl-p 'pj; commandline -f repaint'
bind ctrl-g 'lazygit; commandline -f repaint' bind -M $mode ctrl-a 'omp; commandline -f repaint'
# ctrl-c routes through the clear wrapper (kitty scrollback + prompt # ctrl-f: pick file(s) with television and open in nvim.
# pinned to the bottom) AND wipes any text typed on the command line. bind -M $mode ctrl-f 'yy; commandline -f repaint'
bind ctrl-c 'commandline -r ""; clear; commandline -f repaint' bind -M $mode ctrl-g 'lazygit; commandline -f repaint'
# ctrl-q mirrors ctrl-d's preset (`exit`) — quit the shell. # ctrl-c routes through the clear wrapper (kitty scrollback + prompt
bind ctrl-q exit # pinned to the bottom) AND wipes any text typed on the command line.
bind -M $mode ctrl-c 'commandline -r ""; clear; commandline -f repaint'
# ctrl-q mirrors ctrl-d's preset (`exit`) — quit the shell.
bind -M $mode ctrl-q exit
end
end end
+1 -2
View File
@@ -46,8 +46,7 @@ map ctrl+shift+g launch_ov lazygit
map ctrl+shift+f toggle_layout stack map ctrl+shift+f toggle_layout stack
map ctrl+shift+e launch_tb nvim map ctrl+shift+e launch_tb nvim
map ctrl+shift+a launch_vs omp map ctrl+shift+a launch_vs omp
# suvadu history picker: pick → execute → drop to shell (script: ~/.local/bin/suv-pick) map ctrl+shift+p launch_hs atuin-pick.py
map ctrl+shift+p launch_hs suv-pick
# Misc # Misc
map ctrl+shift+c copy_to_clipboard map ctrl+shift+c copy_to_clipboard
@@ -102,7 +102,7 @@ window-rule {
match app-id="todoist" match app-id="todoist"
match app-id="com.anthropic.Claude" match app-id="com.anthropic.Claude"
match app-id="Hermes" match app-id="Hermes"
match app-id="chromium"
open-on-output "eDP-1" open-on-output "eDP-1"
} }
@@ -8,7 +8,7 @@ output "Tianma Microelectronics Ltd. TL134ADXP03 Unknown" {
always-center-single-column true always-center-single-column true
center-focused-column "never" center-focused-column "never"
gaps 8 gaps 8
default-column-width { proportion 0.9; } default-column-width { proportion 1.0; }
preset-column-widths { preset-column-widths {
proportion 0.5 proportion 0.5
proportion 1.0 proportion 1.0
+2 -2
View File
@@ -1,6 +1,6 @@
[[plugin.deps]] [[plugin.deps]]
use = "yazi-rs/plugins:smart-filter" use = "yazi-rs/plugins:smart-filter"
rev = "0be29a9" rev = "3f2b882"
hash = "d6f7351ea25a43c9b5f4315fa1e23935" hash = "d6f7351ea25a43c9b5f4315fa1e23935"
[[plugin.deps]] [[plugin.deps]]
@@ -10,7 +10,7 @@ hash = "8c1b36103121a66b37308afaf7124575"
[[plugin.deps]] [[plugin.deps]]
use = "yazi-rs/plugins:smart-enter" use = "yazi-rs/plugins:smart-enter"
rev = "0be29a9" rev = "3f2b882"
hash = "187cc58ba7ac3befd49c342129e6f1b6" hash = "187cc58ba7ac3befd49c342129e6f1b6"
[[plugin.deps]] [[plugin.deps]]
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""atuin-pick: atuin history picker for a kitty split (ctrl+shift+p).
atuin search -i draws its TUI on the tty and prints the selected command
to stdout (Esc prints nothing). Filter to history from the current
directory. Run the selection with rc loaded so aliases and functions
resolve, then drop to an interactive shell.
"""
import os
import subprocess
import sys
# kitty launches this outside a shell, so atuin's init never ran.
if "ATUIN_SESSION" not in os.environ:
os.environ["ATUIN_SESSION"] = subprocess.run(
["atuin", "uuid"], stdout=subprocess.PIPE, text=True
).stdout.strip()
sel = subprocess.run(
["atuin", "search", "-i", "--filter-mode", "directory"],
stdout=subprocess.PIPE,
text=True,
).stdout.strip()
if sel:
print(f"\u276f {sel}", file=sys.stderr)
subprocess.run(["zsh", "-ic", "--", sel])
os.execvp("zsh", ["zsh", "-i"])
-11
View File
@@ -1,11 +0,0 @@
#!/bin/zsh
# suv-pick: suvadu history picker for a kitty split (ctrl+shift+p).
# suv search draws its TUI on stderr and prints the selected command to
# stdout (Esc prints nothing). Run the selection with rc loaded so aliases
# and functions resolve, then drop to an interactive shell.
sel="$(command suv search)"
if [[ -n "$sel" ]]; then
print -ru2 -- "❯ $sel"
zsh -ic -- "$sel"
fi
exec zsh -i
+5 -2
View File
@@ -29,8 +29,7 @@ fi
# ZLE keybinds — zsh ports of fish_user_key_bindings.fish # ZLE keybinds — zsh ports of fish_user_key_bindings.fish
[ -f "$HOME/.config/zsh/keybinds.zsh" ] && . "$HOME/.config/zsh/keybinds.zsh" [ -f "$HOME/.config/zsh/keybinds.zsh" ] && . "$HOME/.config/zsh/keybinds.zsh"
# suvadu shell history (replaces atuin here; fish keeps atuin — suvadu has no # suvadu shell history
# fish support yet, bash still on atuin too)
command -v suv >/dev/null 2>&1 && eval "$(suv init zsh)" command -v suv >/dev/null 2>&1 && eval "$(suv init zsh)"
# Generated for envman. Do not edit. # Generated for envman. Do not edit.
@@ -49,3 +48,7 @@ for _zsh_hl in \
[ -r "$_zsh_hl" ] && . "$_zsh_hl" && break [ -r "$_zsh_hl" ] && . "$_zsh_hl" && break
done done
unset _zsh_hl unset _zsh_hl
. "$HOME/.atuin/bin/env"
eval "$(atuin init zsh)"