From b0086b6beb1dfd8999ddd112527bf106f8843bca Mon Sep 17 00:00:00 2001 From: Hadrien David Date: Tue, 28 Jul 2026 17:08:39 -0400 Subject: [PATCH 1/4] docs: improve coding-agent discovery --- .agents/plugins/marketplace.json | 20 ++++ .claude-plugin/marketplace.json | 26 +++++ .claude-plugin/plugin.json | 15 +++ .codex-plugin/plugin.json | 29 +++++ README.md | 29 +++++ context7.json | 17 ++- docs/javascripts/markdown-actions.js | 40 +++++++ docs/stylesheets/markdown-actions.css | 17 +++ docs/troubleshooting.md | 147 ++++++++++++++++++++++++++ mkdocs.yml | 28 +++-- overrides/main.html | 39 +++++++ pyproject.toml | 2 +- 12 files changed, 399 insertions(+), 10 deletions(-) create mode 100644 .agents/plugins/marketplace.json create mode 100644 .claude-plugin/marketplace.json create mode 100644 .claude-plugin/plugin.json create mode 100644 .codex-plugin/plugin.json create mode 100644 docs/javascripts/markdown-actions.js create mode 100644 docs/stylesheets/markdown-actions.css create mode 100644 docs/troubleshooting.md create mode 100644 overrides/main.html diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..92052cc --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "fastsqla", + "interface": { + "displayName": "FastSQLA" + }, + "plugins": [ + { + "name": "fastsqla", + "source": { + "source": "local", + "path": "./" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + } + ] +} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..5690e11 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", + "name": "fastsqla", + "description": "Agent Skills for building FastAPI applications with FastSQLA.", + "owner": { + "name": "Hadrien David", + "email": "h@driendavid.com" + }, + "plugins": [ + { + "name": "fastsqla", + "source": "./", + "description": "Use FastSQLA's current setup, async session, and pagination APIs correctly.", + "version": "1.0.0", + "author": { + "name": "Hadrien David", + "email": "h@driendavid.com" + }, + "homepage": "https://hadrien.github.io/FastSQLA/", + "repository": "https://github.com/hadrien/FastSQLA", + "license": "MIT", + "keywords": ["fastapi", "sqlalchemy", "async", "pagination", "sqlmodel"], + "category": "development" + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..42dd18b --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-plugin.json", + "name": "fastsqla", + "version": "1.0.0", + "description": "Use FastSQLA's current setup, async session, and pagination APIs correctly.", + "author": { + "name": "Hadrien David", + "email": "h@driendavid.com" + }, + "homepage": "https://hadrien.github.io/FastSQLA/", + "repository": "https://github.com/hadrien/FastSQLA", + "license": "MIT", + "keywords": ["fastapi", "sqlalchemy", "async", "pagination", "sqlmodel"], + "skills": "./skills/" +} diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 0000000..1d14309 --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,29 @@ +{ + "name": "fastsqla", + "version": "1.0.0", + "description": "Use FastSQLA's current setup, async session, and pagination APIs correctly.", + "author": { + "name": "Hadrien David", + "email": "h@driendavid.com", + "url": "https://github.com/hadrien" + }, + "homepage": "https://hadrien.github.io/FastSQLA/", + "repository": "https://github.com/hadrien/FastSQLA", + "license": "MIT", + "keywords": ["fastapi", "sqlalchemy", "async", "pagination", "sqlmodel"], + "skills": "./skills/", + "interface": { + "displayName": "FastSQLA", + "shortDescription": "FastSQLA setup, sessions, and pagination", + "longDescription": "Build FastAPI applications with FastSQLA's current async SQLAlchemy and SQLModel APIs.", + "developerName": "Hadrien David", + "category": "Productivity", + "capabilities": ["Read", "Write"], + "websiteURL": "https://hadrien.github.io/FastSQLA/", + "defaultPrompt": [ + "Set up FastSQLA in this FastAPI application.", + "Use FastSQLA to add an async database endpoint.", + "Paginate this SQLAlchemy query with FastSQLA." + ] + } +} diff --git a/README.md b/README.md index cdc1f7c..2788f6b 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,35 @@ providing boilerplate and intuitive helpers. Additionally, it offers built-in customizable pagination and automatically manages the `SQLAlchemy` session lifecycle following [`SQLAlchemy`'s best practices](https://docs.sqlalchemy.org/en/20/orm/session_basics.html#when-do-i-construct-a-session-when-do-i-commit-it-and-when-do-i-close-it). +## For coding agents and LLMs + +FastSQLA publishes agent-readable documentation alongside the website: + +- [`llms.txt`](https://hadrien.github.io/FastSQLA/llms.txt) is the concise documentation + index. +- [`llms-full.txt`](https://hadrien.github.io/FastSQLA/llms-full.txt) contains the + complete documentation in one file. +- Every indexed page has a Markdown twin, such as + [`setup/index.md`](https://hadrien.github.io/FastSQLA/setup/index.md). +- [Context7](https://context7.com/hadrien/fastsqla) serves the current documentation and + FastSQLA-specific usage rules. + +The repository also bundles Agent Skills for setup, session management, and pagination. +Install all three as one plugin: + +### Claude Code + +```bash +claude plugin marketplace add hadrien/FastSQLA +claude plugin install fastsqla@fastsqla +``` + +### Codex + +```bash +codex plugin marketplace add hadrien/FastSQLA +codex plugin add fastsqla@fastsqla +``` ## Features diff --git a/context7.json b/context7.json index fd8c004..ae86cd8 100644 --- a/context7.json +++ b/context7.json @@ -1 +1,16 @@ -{"url": "https://context7.com/hadrien/fastsqla","public_key": "pk_HGTiXpaLrQ2YW61qLOKbF"} +{ + "$schema": "https://context7.com/schema/context7.json", + "url": "https://context7.com/hadrien/fastsqla", + "public_key": "pk_HGTiXpaLrQ2YW61qLOKbF", + "projectTitle": "FastSQLA", + "description": "Async SQLAlchemy sessions and pagination for FastAPI, with optional SQLModel support.", + "folders": ["docs", "skills"], + "excludeFiles": ["CHANGELOG.md"], + "rules": [ + "Configure FastAPI with fastsqla.lifespan or new_lifespan(); do not construct engines or sessions manually.", + "Declare fastsqla.Session as an endpoint parameter; FastSQLA commits on success and rolls back on exceptions.", + "Use fastsqla.open_session() outside request handlers, and only while the application lifespan is active.", + "Use fastsqla.Paginate and Page[T] for list endpoints; call await paginate(select(...)).", + "Install FastSQLA[sqlmodel] when using SQLModel; FastSQLA selects SQLModel's AsyncSession automatically." + ] +} diff --git a/docs/javascripts/markdown-actions.js b/docs/javascripts/markdown-actions.js new file mode 100644 index 0000000..d92ffa8 --- /dev/null +++ b/docs/javascripts/markdown-actions.js @@ -0,0 +1,40 @@ +const bindMarkdownCopy = () => { + const button = document.querySelector("[data-copy-markdown]"); + if (!button || button.dataset.copyBound === "true") { + return; + } + + button.dataset.copyBound = "true"; + button.addEventListener("click", async () => { + const actions = button.closest("[data-markdown-url]"); + const status = actions?.querySelector("[role='status']"); + + try { + if (!actions || !status || !navigator.clipboard) { + throw new Error("Markdown copy controls are unavailable."); + } + + const response = await fetch(actions.dataset.markdownUrl); + if (!response.ok) { + throw new Error(`Markdown request failed with status ${response.status}.`); + } + + await navigator.clipboard.writeText(await response.text()); + status.textContent = "Copied"; + window.setTimeout(() => { + status.textContent = ""; + }, 2000); + } catch (error) { + if (status) { + status.textContent = "Copy failed"; + } + console.error("Unable to copy the Markdown page.", error); + } + }); +}; + +if (typeof document$ === "undefined") { + document.addEventListener("DOMContentLoaded", bindMarkdownCopy); +} else { + document$.subscribe(bindMarkdownCopy); +} diff --git a/docs/stylesheets/markdown-actions.css b/docs/stylesheets/markdown-actions.css new file mode 100644 index 0000000..50e71df --- /dev/null +++ b/docs/stylesheets/markdown-actions.css @@ -0,0 +1,17 @@ +.md-content__markdown-actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-block-end: 1.25rem; +} + +.md-content__markdown-actions .md-button { + margin: 0; +} + +.md-content__markdown-status { + align-self: center; + min-height: 1.2em; + color: var(--md-default-fg-color--light); + font-size: 0.72rem; +} diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..a4da0c8 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,147 @@ +# Troubleshooting + +Search for the exact error text below, then apply the correction for that failure mode. + +## `Missing sqlalchemy_url in environ.` + +The default [`fastsqla.lifespan`][fastsqla.lifespan] reads the database URL from +`SQLALCHEMY_URL`. The variable was absent when FastAPI started. + +Set an async SQLAlchemy URL before starting the application: + +```bash +export SQLALCHEMY_URL=sqlite+aiosqlite:///db.sqlite +uvicorn example:app +``` + +For configuration in code, create the lifespan explicitly: + +```python +from fastapi import FastAPI +from fastsqla import new_lifespan + +app = FastAPI(lifespan=new_lifespan("sqlite+aiosqlite:///db.sqlite")) +``` + +## `The asyncio extension requires an async driver to be used` + +The URL selects a synchronous database driver. For example, `sqlite:///db.sqlite` loads +Python's synchronous `pysqlite` driver and produces: + +```text +The asyncio extension requires an async driver to be used. +The loaded 'pysqlite' is not async. +``` + +Install the asynchronous driver for the database and include it in the URL: + +| Database | Install | URL prefix | +|------------|-------------------------|------------------------| +| PostgreSQL | `pip install asyncpg` | `postgresql+asyncpg://` | +| SQLite | `pip install aiosqlite` | `sqlite+aiosqlite:///` | +| MySQL | `pip install aiomysql` | `mysql+aiomysql://` | + +## `Could not locate a bind configured on SQL expression or this Session.` + +A [`Session`][fastsqla.Session] or [`open_session()`][fastsqla.open_session] operation ran +outside the FastSQLA lifespan. The lifespan binds the shared session factory at startup +and clears it at shutdown. + +Attach the lifespan to FastAPI: + +```python +from fastapi import FastAPI +from fastsqla import lifespan + +app = FastAPI(lifespan=lifespan) +``` + +Use `Session` only in endpoint parameters. Use `open_session()` for background work that +runs after application startup and finishes before shutdown: + +```python +from fastsqla import open_session +from sqlalchemy import select + +async def refresh_cache() -> None: + async with open_session() as session: + heroes = (await session.scalars(select(Hero))).all() +``` + +Do not import or configure `SessionFactory`; it is an internal lifecycle detail. + +## `MissingGreenlet: greenlet_spawn has not been called` + +SQLAlchemy attempted implicit database I/O while ordinary Python code accessed an +unloaded ORM attribute. This often happens when response serialization touches a +lazy-loaded relationship. + +Load relationships explicitly inside the awaited query. `selectinload()` is a good +default for collections: + +```python +from fastsqla import Session +from sqlalchemy import select +from sqlalchemy.orm import selectinload + +async def get_team(team_id: int, session: Session) -> Team: + stmt = ( + select(Team) + .where(Team.id == team_id) + .options(selectinload(Team.heroes)) + ) + return (await session.scalars(stmt)).one() +``` + +See SQLAlchemy's guidance on +[preventing implicit I/O with `AsyncSession`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#preventing-implicit-io-when-using-asyncsession). + +## `Input should be greater than or equal to 0` + +The built-in [`Paginate`][fastsqla.Paginate] dependency rejects a negative `offset`. +Start at zero: + +```text +GET /heroes?offset=0&limit=10 +``` + +## `Input should be less than or equal to 100` + +The built-in `Paginate` dependency accepts `limit` values from 1 through 100. Use a +smaller value or define an intentional maximum: + +```python +from typing import Annotated + +from fastapi import Depends +from fastsqla import PaginateType, new_pagination + +LargePage = Annotated[ + PaginateType[HeroModel], + Depends(new_pagination(min_page_size=10, max_page_size=250)), +] +``` + +## `This Session's transaction has been rolled back due to a previous exception` + +A database operation such as `flush()` raised an exception, but application code caught +it and then tried to keep using the invalid transaction. + +Translate the original database error into an exception and let FastSQLA roll back: + +```python +from fastapi import HTTPException +from fastsqla import Session +from sqlalchemy.exc import IntegrityError + +async def create_hero(new_hero: HeroInput, session: Session) -> Hero: + hero = Hero(**new_hero.model_dump()) + session.add(hero) + try: + await session.flush() + except IntegrityError as exc: + raise HTTPException(status_code=409, detail="Hero already exists") from exc + return hero +``` + +Do not catch and ignore `IntegrityError`, and do not call `commit()` inside an endpoint. diff --git a/mkdocs.yml b/mkdocs.yml index 4f51665..93fb275 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: FastSQLA -site_url: https://mydomain.org/FastSQLA +site_url: https://hadrien.github.io/FastSQLA/ repo_name: hadrien/FastSQLA repo_url: https://github.com/hadrien/fastsqla edit_uri: edit/main/docs/ @@ -12,9 +12,11 @@ nav: - Object-relational mapping: orm.md - SQLAlchemy Session: session.md - Pagination: pagination.md + - Troubleshooting: troubleshooting.md - Changelog: changelog.md theme: + custom_dir: overrides favicon: images/favicon.png icon: logo: material/database @@ -52,6 +54,21 @@ theme: icon: material/weather-night name: Switch to system preference +extra: + markdown_pages: &markdown_pages + - index.md + - setup.md + - session.md + - orm.md + - pagination.md + - troubleshooting.md + +extra_css: + - stylesheets/markdown-actions.css + +extra_javascript: + - javascripts/markdown-actions.js + plugins: - autorefs - glightbox @@ -71,12 +88,7 @@ plugins: Async SQLAlchemy 2.0+ extension for FastAPI with SQLModel support, built-in pagination & more. full_output: llms-full.txt sections: - Sections: - - index.md - - setup.md - - session.md - - orm.md - - pagination.md + Sections: *markdown_pages markdown_extensions: - abbr @@ -97,4 +109,4 @@ markdown_extensions: permalink: true watch: - docs - - src \ No newline at end of file + - src diff --git a/overrides/main.html b/overrides/main.html new file mode 100644 index 0000000..019c446 --- /dev/null +++ b/overrides/main.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% set has_markdown_twin = page and page.file.src_uri in config.extra.markdown_pages %} +{% if has_markdown_twin %} + {% set markdown_path = page.url ~ "index.md" %} +{% endif %} + +{% block extrahead %} + {{ super() }} + {% if has_markdown_twin %} + + {% endif %} +{% endblock %} + +{% block content %} + {% if has_markdown_twin %} +
+ + View as Markdown + + + +
+ {% endif %} + {{ super() }} +{% endblock %} diff --git a/pyproject.toml b/pyproject.toml index 47ff39e..87c4734 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ "Topic :: Software Development", "Typing :: Typed", ] -keywords = ["FastAPI", "SQLAlchemy", "AsyncIO"] +keywords = ["fastapi", "sqlalchemy", "async", "session", "pagination", "sqlmodel"] dependencies = ["fastapi>=0.115.6", "sqlalchemy[asyncio]>=2.0.37", "structlog>=24.4.0"] [project.urls] From 686a8d75f8ed30f1f50c23e2ddb29a2230206f55 Mon Sep 17 00:00:00 2001 From: Hadrien David Date: Tue, 28 Jul 2026 17:19:22 -0400 Subject: [PATCH 2/4] docs: fix packaged pagination guidance --- skills/fastsqla-pagination/SKILL.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/skills/fastsqla-pagination/SKILL.md b/skills/fastsqla-pagination/SKILL.md index 6427679..064b777 100644 --- a/skills/fastsqla-pagination/SKILL.md +++ b/skills/fastsqla-pagination/SKILL.md @@ -55,12 +55,12 @@ Default query parameters added to the endpoint: ```python from fastapi import FastAPI -from fastsqla import Base, Page, Paginate +from fastsqla import Base, Page, Paginate, lifespan from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.orm import Mapped, mapped_column -app = FastAPI() +app = FastAPI(lifespan=lifespan) class Hero(Base): __tablename__ = "hero" @@ -78,6 +78,10 @@ async def list_heroes(paginate: Paginate[HeroModel]) -> Page[HeroModel]: return await paginate(select(Hero)) ``` +Set `SQLALCHEMY_URL` to an async SQLAlchemy URL for a database that contains the +mapped `hero` table before starting the app. For example, +`sqlite+aiosqlite:///app.db` uses the `aiosqlite` driver. + A request to `GET /heroes?offset=20&limit=10` returns the third page of results. ## Adding Filters @@ -105,7 +109,7 @@ For custom pagination behavior, use `new_pagination()` to create a new dependenc | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `min_page_size` | `int` | `10` | Default and minimum `limit` value | +| `min_page_size` | `int` | `10` | Default `limit` value | | `max_page_size` | `int` | `100` | Maximum allowed `limit` value | | `query_count_dependency` | `Callable[..., Awaitable[int]] \| None` | `None` | FastAPI dependency returning total item count. When `None`, uses `SELECT COUNT(*) FROM (subquery)`. | | `result_processor` | `Callable[[Result], Iterable]` | `lambda r: iter(r.unique().scalars())` | Transforms the SQLAlchemy `Result` into an iterable of items | From 747281899c4cf33e048acc14641f320a37fb16b8 Mon Sep 17 00:00:00 2001 From: Hadrien David Date: Tue, 28 Jul 2026 18:03:19 -0400 Subject: [PATCH 3/4] docs: compact markdown actions --- docs/javascripts/markdown-actions.js | 20 +++++-- docs/stylesheets/markdown-actions.css | 76 +++++++++++++++++++++++---- overrides/main.html | 63 ++++++++++++++-------- 3 files changed, 125 insertions(+), 34 deletions(-) diff --git a/docs/javascripts/markdown-actions.js b/docs/javascripts/markdown-actions.js index d92ffa8..ec08458 100644 --- a/docs/javascripts/markdown-actions.js +++ b/docs/javascripts/markdown-actions.js @@ -8,9 +8,10 @@ const bindMarkdownCopy = () => { button.addEventListener("click", async () => { const actions = button.closest("[data-markdown-url]"); const status = actions?.querySelector("[role='status']"); + const label = button.querySelector("[data-copy-label]"); try { - if (!actions || !status || !navigator.clipboard) { + if (!actions || !status || !label || !navigator.clipboard) { throw new Error("Markdown copy controls are unavailable."); } @@ -20,14 +21,27 @@ const bindMarkdownCopy = () => { } await navigator.clipboard.writeText(await response.text()); - status.textContent = "Copied"; + label.textContent = "Copied"; + status.textContent = "Page Markdown copied"; window.setTimeout(() => { + label.textContent = "Copy for LLM"; status.textContent = ""; }, 2000); } catch (error) { + if (label) { + label.textContent = "Copy failed"; + } if (status) { - status.textContent = "Copy failed"; + status.textContent = "Page Markdown could not be copied"; } + window.setTimeout(() => { + if (label) { + label.textContent = "Copy for LLM"; + } + if (status) { + status.textContent = ""; + } + }, 2000); console.error("Unable to copy the Markdown page.", error); } }); diff --git a/docs/stylesheets/markdown-actions.css b/docs/stylesheets/markdown-actions.css index 50e71df..6fa4296 100644 --- a/docs/stylesheets/markdown-actions.css +++ b/docs/stylesheets/markdown-actions.css @@ -1,17 +1,75 @@ +.md-typeset h1:has(+ .md-content__markdown-actions) { + margin-block-end: 0.6rem; +} + .md-content__markdown-actions { display: flex; flex-wrap: wrap; - gap: 0.5rem; - margin-block-end: 1.25rem; + align-items: center; + gap: 0.4rem; + margin-block-end: 1.6rem; } -.md-content__markdown-actions .md-button { - margin: 0; +.md-content__markdown-action { + display: inline-flex; + min-width: 2.2rem; + height: 2.2rem; + align-items: center; + justify-content: center; + gap: 0.4rem; + padding: 0 0.6rem; + border: 0.05rem solid var(--md-default-fg-color--light); + border-radius: 0.15rem; + appearance: none; + background: transparent; + color: var(--md-default-fg-color--light); + cursor: pointer; + font: inherit; + font-size: 0.7rem; + font-weight: 500; + line-height: 1; + text-decoration: none; + transition: border-color 125ms ease-out, background-color 125ms ease-out, color 125ms ease-out; } -.md-content__markdown-status { - align-self: center; - min-height: 1.2em; - color: var(--md-default-fg-color--light); - font-size: 0.72rem; +.md-content__markdown-action:is(:hover, :focus-visible) { + border-color: var(--md-accent-fg-color); + background-color: var(--md-accent-fg-color--transparent); + color: var(--md-accent-fg-color); + text-decoration: none; +} + +.md-content__markdown-action:focus-visible { + outline: 0.1rem solid var(--md-accent-fg-color); + outline-offset: 0.1rem; +} + +.md-content__markdown-action--icon { + width: 2.2rem; + padding: 0; +} + +.md-content__markdown-agents { + display: inline-flex; + gap: 0.4rem; +} + +.md-content__markdown-action-icon, +.md-content__markdown-action > svg { + width: 0.9rem; + height: 0.9rem; + flex: 0 0 auto; + fill: currentcolor; +} + +@media screen and (max-width: 29.984375rem) { + .md-content__markdown-actions { + margin-block-end: 1.2rem; + } +} + +@media (prefers-reduced-motion: reduce) { + .md-content__markdown-action { + transition: none; + } } diff --git a/overrides/main.html b/overrides/main.html index 019c446..8417c10 100644 --- a/overrides/main.html +++ b/overrides/main.html @@ -8,32 +8,51 @@ {% block extrahead %} {{ super() }} {% if has_markdown_twin %} - + {% endif %} {% endblock %} {% block content %} + {% set rendered_content %}{{ super() }}{% endset %} {% if has_markdown_twin %} -
- - View as Markdown - - - -
+ {% set markdown_url = config.site_url.rstrip('/') ~ "/" ~ markdown_path %} + {% set llm_prompt = "Read " ~ markdown_url ~ " and answer questions about the content." %} + {% set markdown_actions %} + + {% endset %} + {{ rendered_content | replace("", "" ~ markdown_actions, 1) }} + {% else %} + {{ rendered_content }} {% endif %} - {{ super() }} {% endblock %} From d9630bc9ac97e28f17a32d6e6370556d916838a7 Mon Sep 17 00:00:00 2001 From: Hadrien David Date: Tue, 28 Jul 2026 22:09:06 -0400 Subject: [PATCH 4/4] docs: refine agent discovery UI --- README.md | 60 +++++++++++++-------------- docs/javascripts/markdown-actions.js | 18 +++----- docs/stylesheets/markdown-actions.css | 53 +++++++++++++---------- overrides/main.html | 4 +- 4 files changed, 68 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 2788f6b..940a733 100644 --- a/README.md +++ b/README.md @@ -24,36 +24,6 @@ providing boilerplate and intuitive helpers. Additionally, it offers built-in customizable pagination and automatically manages the `SQLAlchemy` session lifecycle following [`SQLAlchemy`'s best practices](https://docs.sqlalchemy.org/en/20/orm/session_basics.html#when-do-i-construct-a-session-when-do-i-commit-it-and-when-do-i-close-it). -## For coding agents and LLMs - -FastSQLA publishes agent-readable documentation alongside the website: - -- [`llms.txt`](https://hadrien.github.io/FastSQLA/llms.txt) is the concise documentation - index. -- [`llms-full.txt`](https://hadrien.github.io/FastSQLA/llms-full.txt) contains the - complete documentation in one file. -- Every indexed page has a Markdown twin, such as - [`setup/index.md`](https://hadrien.github.io/FastSQLA/setup/index.md). -- [Context7](https://context7.com/hadrien/fastsqla) serves the current documentation and - FastSQLA-specific usage rules. - -The repository also bundles Agent Skills for setup, session management, and pagination. -Install all three as one plugin: - -### Claude Code - -```bash -claude plugin marketplace add hadrien/FastSQLA -claude plugin install fastsqla@fastsqla -``` - -### Codex - -```bash -codex plugin marketplace add hadrien/FastSQLA -codex plugin add fastsqla@fastsqla -``` - ## Features * Easy setup at app startup using @@ -337,3 +307,33 @@ You can also check the generated openapi doc by opening your browser to ## License This project is licensed under the terms of the [MIT license](https://github.com/hadrien/FastSQLA/blob/main/LICENSE). + +## For coding agents and LLMs + +FastSQLA publishes agent-readable documentation alongside the website: + +- [`llms.txt`](https://hadrien.github.io/FastSQLA/llms.txt) is the concise documentation + index. +- [`llms-full.txt`](https://hadrien.github.io/FastSQLA/llms-full.txt) contains the + complete documentation in one file. +- Every indexed page has a Markdown twin, such as + [`setup/index.md`](https://hadrien.github.io/FastSQLA/setup/index.md). +- [Context7](https://context7.com/hadrien/fastsqla) serves the current documentation and + FastSQLA-specific usage rules. + +The repository also bundles Agent Skills for setup, session management, and pagination. +Install all three as one plugin: + +### Claude Code + +```bash +claude plugin marketplace add hadrien/FastSQLA +claude plugin install fastsqla@fastsqla +``` + +### Codex + +```bash +codex plugin marketplace add hadrien/FastSQLA +codex plugin add fastsqla@fastsqla +``` diff --git a/docs/javascripts/markdown-actions.js b/docs/javascripts/markdown-actions.js index ec08458..1fd9ac5 100644 --- a/docs/javascripts/markdown-actions.js +++ b/docs/javascripts/markdown-actions.js @@ -9,6 +9,10 @@ const bindMarkdownCopy = () => { const actions = button.closest("[data-markdown-url]"); const status = actions?.querySelector("[role='status']"); const label = button.querySelector("[data-copy-label]"); + const resetCopyState = () => { + if (label) label.textContent = "Copy for LLM"; + if (status) status.textContent = ""; + }; try { if (!actions || !status || !label || !navigator.clipboard) { @@ -23,10 +27,7 @@ const bindMarkdownCopy = () => { await navigator.clipboard.writeText(await response.text()); label.textContent = "Copied"; status.textContent = "Page Markdown copied"; - window.setTimeout(() => { - label.textContent = "Copy for LLM"; - status.textContent = ""; - }, 2000); + window.setTimeout(resetCopyState, 2000); } catch (error) { if (label) { label.textContent = "Copy failed"; @@ -34,14 +35,7 @@ const bindMarkdownCopy = () => { if (status) { status.textContent = "Page Markdown could not be copied"; } - window.setTimeout(() => { - if (label) { - label.textContent = "Copy for LLM"; - } - if (status) { - status.textContent = ""; - } - }, 2000); + window.setTimeout(resetCopyState, 2000); console.error("Unable to copy the Markdown page.", error); } }); diff --git a/docs/stylesheets/markdown-actions.css b/docs/stylesheets/markdown-actions.css index 6fa4296..17ffae6 100644 --- a/docs/stylesheets/markdown-actions.css +++ b/docs/stylesheets/markdown-actions.css @@ -1,13 +1,13 @@ .md-typeset h1:has(+ .md-content__markdown-actions) { - margin-block-end: 0.6rem; + margin-block-end: 0.4rem; } .md-content__markdown-actions { display: flex; flex-wrap: wrap; align-items: center; - gap: 0.4rem; - margin-block-end: 1.6rem; + gap: 0 0.4rem; + margin-block-end: 1.2rem; } .md-content__markdown-action { @@ -16,31 +16,32 @@ height: 2.2rem; align-items: center; justify-content: center; - gap: 0.4rem; - padding: 0 0.6rem; - border: 0.05rem solid var(--md-default-fg-color--light); + gap: 0.3rem; + padding: 0 0.4rem; + border: 0; border-radius: 0.15rem; appearance: none; background: transparent; color: var(--md-default-fg-color--light); cursor: pointer; font: inherit; - font-size: 0.7rem; + font-size: 0.6rem; font-weight: 500; line-height: 1; + position: relative; text-decoration: none; - transition: border-color 125ms ease-out, background-color 125ms ease-out, color 125ms ease-out; + transition: color 125ms ease-out; } -.md-content__markdown-action:is(:hover, :focus-visible) { - border-color: var(--md-accent-fg-color); - background-color: var(--md-accent-fg-color--transparent); - color: var(--md-accent-fg-color); +.md-typeset .md-content__markdown-action:is(:hover, :focus-visible) { + --markdown-action-border: var(--markdown-action-brand, var(--md-accent-fg-color)); + --markdown-action-bg: var(--markdown-action-brand-bg, var(--md-accent-fg-color--transparent)); + color: var(--markdown-action-brand, var(--md-accent-fg-color)); text-decoration: none; } -.md-content__markdown-action:focus-visible { - outline: 0.1rem solid var(--md-accent-fg-color); +.md-typeset .md-content__markdown-action:focus-visible { + outline: 0.1rem solid var(--markdown-action-brand, var(--md-accent-fg-color)); outline-offset: 0.1rem; } @@ -49,25 +50,31 @@ padding: 0; } +.md-content__markdown-action::before { + content: ""; position: absolute; + inset: 0.3rem 0; + border: 0.05rem solid var(--markdown-action-border, var(--md-default-fg-color--light)); + border-radius: inherit; + background: var(--markdown-action-bg, transparent); + transition: border-color 125ms ease-out, background-color 125ms ease-out; +} +.md-content__markdown-action--icon::before { inset: 0.3rem; } +.md-content__markdown-action--chatgpt { --markdown-action-brand: #10a37f; --markdown-action-brand-bg: #10a37f14; } +.md-content__markdown-action--claude { --markdown-action-brand: #d97757; --markdown-action-brand-bg: #d9775714; } + .md-content__markdown-agents { display: inline-flex; - gap: 0.4rem; + gap: 0; } .md-content__markdown-action-icon, .md-content__markdown-action > svg { - width: 0.9rem; - height: 0.9rem; + width: 0.8rem; + height: 0.8rem; flex: 0 0 auto; fill: currentcolor; } -@media screen and (max-width: 29.984375rem) { - .md-content__markdown-actions { - margin-block-end: 1.2rem; - } -} - @media (prefers-reduced-motion: reduce) { .md-content__markdown-action { transition: none; diff --git a/overrides/main.html b/overrides/main.html index 8417c10..6c642f0 100644 --- a/overrides/main.html +++ b/overrides/main.html @@ -33,14 +33,14 @@ View Markdown - {% include ".icons/fontawesome/brands/openai.svg" %} -