Metadata-Version: 2.4
Name: skaldr
Version: 0.4.0
Summary: Component framework that renders one YAML content file into a self-contained HTML report page
Keywords: report,html,yaml,components,jinja
Author: Alex Yanchenko
Author-email: Alex Yanchenko <a.inbox.2026@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Text Processing :: Markup :: HTML
Requires-Dist: jinja2>=3.1
Requires-Dist: markupsafe>=2.1
Requires-Dist: pydantic>=2.13.4
Requires-Dist: pyyaml>=6.0
Requires-Python: >=3.11
Project-URL: Repository, https://github.com/alex-yanchenko/skaldr
Project-URL: Issues, https://github.com/alex-yanchenko/skaldr/issues
Description-Content-Type: text/markdown

# skaldr

A component framework for **AI-authored report pages**. An AI (or a person) doing real
work — an investigation, a review, a proposal, an ingestion report — writes **one YAML
content file**; skaldr validates it and renders **one polished, self-contained HTML page**.
No time is spent on design, styling, or layout: those decisions are made once, here, by the
component library.

```bash
# from a checkout
uv run skaldr data/example.yaml
open out/example.html

# or installed (from PyPI, once published)
uv tool install skaldr
skaldr data/example.yaml
```

The CLI is deliberately tiny: `skaldr <content.yaml> [-o out.html]`, plus
`skaldr --write-schema schema/page.schema.json`. There are no styling flags — everything is
in the content file.

## The content file

```yaml
version: 1
meta:
  title: "Q3 Data Import — Issues & Proposed Fixes"
  subtitle: ["Reconciled review of the 10,000-record batch."]
  source: "Salesforce export"   # optional; feeds the provenance footer
  date: "Q3 2026"               # optional; never auto-now (builds are reproducible)
  toc: true                     # optional; auto table-of-contents from level-2 headings
  width: default                # optional; page width cap: default (1600) | wide (1920) | full
badges:                         # author-declared vocabulary (see below)
  OUR_SIDE: { label: "Our side", tone: amber, legend: "We can fix it before import." }
  BUG:      { label: "Bug",      tone: blue,  legend: "Defect in our pipeline code." }
blocks:
  - { type: heading, text: "Overview" }
  - { type: text, body: "Prose with **bold**, *italic*, `code`, ~~strike~~ and [links](https://x)." }
  - { type: cards, items: [{ label: "Imported cleanly", value: 8500, of: 10000, tone: success }] }
  # … more blocks
```

Top level is `version` · `meta` · optional `badges` · `blocks` — nothing else. Every block
carries a `type` discriminator; the model is a pydantic discriminated union, so an unknown
type, a field from the wrong block, or an unknown key each fails with a precise
`blocks.3.items.2.value`-style error before anything renders.

**Blocks:** `heading` · `text` · `list` · `fact_strip` · `key_value` · `cards` · `badge_row` ·
`callout` · `status_list` · `meter` · `table` · `code` · `quote` · `image` · `timeline` ·
`section` (collapsible) · `grid` (bounded 6-column layout). The `table` is the workhorse — typed
columns, grouped subtotals, sub-rows, and a `reconcile` block that hard-fails the build if the
counts don't sum to a declared total. Prose fields take a small markdown subset
(`**bold**`, `*italic*`, `` `code` ``, `~~strike~~`, links); raw HTML is never interpreted.

**Learn the format:**
- [`src/skaldr/skill/SKILL.md`](src/skaldr/skill/SKILL.md) — the authoring guide (and the Claude
  skill, below): every block, the table, badges, rich text, and the rules.
- [`data/example.yaml`](data/example.yaml) — a complete file exercising every block.
- [`schema/page.schema.json`](schema/page.schema.json) — the machine-readable contract; point
  your editor's YAML language server at it, or regenerate with `skaldr --write-schema`.

## Guarantees

- **One self-contained file** — inline CSS, system fonts, no external resources; the page
  carries its own `<!doctype>` + `<meta charset>` so it renders correctly from `file://`, any
  static host, or a claude.ai Artifact.
- **Validation is the product** — structural mistakes fail the build with a field path, never
  reach the reader's eyes.
- **Derived, not authored** — number formatting, percentages, subtotals, the legend, the TOC,
  and the provenance footer are all computed, so they can't drift from the data.
- **Light & dark** — the palette follows the viewer's OS theme; a small corner menu lets the
  reader switch theme and page width.

## Use with Claude Code

skaldr ships a Claude skill so an AI can author reports for you. After installing skaldr, run:

```bash
skaldr --install-skill
```

It links the skill into `~/.claude/skills/` (creating the dir, or telling you how if Claude Code
isn't set up). Then ask Claude — *"make me a skaldr report on X"* — and it writes the YAML and
renders it. The skill is just [`src/skaldr/skill/SKILL.md`](src/skaldr/skill/SKILL.md).

Re-run `skaldr --install-skill` after upgrading skaldr — the link points into the installed
package, so a `brew upgrade` (versioned install) needs a fresh link; re-running heals it.

## Layout

- [`src/skaldr/models.py`](src/skaldr/models.py) — the content-file contract (pydantic).
- [`src/skaldr/compute.py`](src/skaldr/compute.py) — derived values (legend, TOC, subtotals, footer).
- [`src/skaldr/render.py`](src/skaldr/render.py) + [`components/`](src/skaldr/components) — Jinja rendering.
- [`src/skaldr/styles.css`](src/skaldr/styles.css) — the single tokenised stylesheet.
