Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format is based on Keep a Changelog and the project follows Semantic Version

## [Unreleased]

### Added

- **core/markdown**: New `ui.markdown(source, props?)` widget (`experimental` tier) rendering a GitHub-Flavored-Markdown subset onto existing widgets: headings, paragraphs with wrapping, fenced/indented code with tokenizer-based monochrome syntax emphasis, nested and task lists, blockquotes, pipe tables with alignment, thematic breaks, and inline strong/em/del/code/links/autolinks/entities. The zero-dependency parser is bounded and fuzz-tested, never throws on untrusted input, and is exported as `parseMarkdown`/`renderMarkdown`/`renderMarkdownBlock` with frozen `MarkdownDocument` AST types. JSX exposes `<Markdown source="…" />`.
- **core/markdown**: `createMarkdownStream()` for append-only sources (agent transcripts, live logs): re-parses only the volatile tail block per append and caches completed blocks plus their rendered VNodes with referential identity, keeping appends O(tail). Chunk boundaries (including split CRLF pairs) never change the result — `document()` always deep-equals `parseMarkdown(source())`, pinned by fuzz tests.
- **core/app**: `app.ready()` resolves once `start()`/`run()` has finished starting and the app accepts `update()` calls — removes the startup race for timers, sockets, and streams that feed state while `run()` blocks. Resolves immediately when already running; rejects on startup failure or dispose-before-start.

### Fixed

- **core/renderer**: Per-side box borders now render standalone edges. A box with only `borderLeft` (or any vertical-only side combination) previously drew nothing when shorter than 2 rows; corner rows are now only required when a horizontal edge is present. Markdown blockquotes use this to render a GitHub-style dim left bar instead of a rounded box.

## [0.1.0-beta.1] - 2026-06-11

### Changed
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This roadmap is directional only. It is not a commitment to scope, ordering, or

- Graduate or prune `experimental`-tier widgets so the 1.0 surface is intentional
- Freeze drawlist and event protocol guarantees together with Zireael
- Streaming markdown widget and an agent-console template
- An agent-console template showcasing streamed markdown transcripts
- Developer tooling: layout, focus, and frame-timing inspection overlay
- Code coverage reporting and blocking benchmark regression gates in CI

Expand Down
15 changes: 15 additions & 0 deletions docs/guide/lifecycle-and-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ Use `run()` for batteries-included lifecycle wiring in Node/Bun apps:

Use `start()` directly when you need manual signal/process control.

Use `ready()` to wait for startup from outside the `run()` call — for
example when a timer, socket, or stream feeds `update()` while `run()`
blocks. Calling `update()` before startup completes throws
`ZRUI_INVALID_STATE`; `ready()` removes the race:

```ts
const running = app.run();
await app.ready();
feed.on("data", (chunk) => app.update((s) => apply(s, chunk)));
await running;
```

`ready()` resolves immediately when the app is already running, and rejects
if startup fails or the app is disposed before starting.

## State machine diagram

```mermaid
Expand Down
1 change: 1 addition & 0 deletions docs/widgets/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Content rendering, labels, and informational widgets.
|---------|-------------|-----------|-----------|
| [`ui.text(content, style?)`](text.md) | Display text with optional styling or variant | No | `stable` |
| [`ui.richText(spans, props?)`](rich-text.md) | Multi-styled text spans | No | `beta` |
| [`ui.markdown(source, props?)`](markdown.md) | GitHub-Flavored-Markdown subset rendered onto core widgets | No | `experimental` |
| [`ui.icon(iconPath, props?)`](icon.md) | Single-character icon from the icon registry | No | `beta` |
| [`ui.badge(text, props?)`](badge.md) | Small status indicator label | No | `beta` |
| [`ui.status(status, props?)`](status.md) | Online/offline/away/busy status dot | No | `beta` |
Expand Down
93 changes: 93 additions & 0 deletions docs/widgets/markdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# `Markdown`

Renders a GitHub-Flavored-Markdown subset by composing existing widgets
(`text`, `richText`, `box`, `row`, `column`, `divider`). Non-interactive,
`experimental` tier.

## Usage

```ts
ui.markdown(prBody)

ui.markdown("# Title\n\nShips `ui.markdown` with **GFM subset** support.", {
blockGap: 1,
})
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `source` | `string` | **required** (first argument) | Markdown source text |
| `blockGap` | `number` | `1` | Vertical spacing rows between top-level blocks |
| `key` | `string` | - | Reconciliation key |

## Supported syntax

| Construct | Notes |
|-----------|-------|
| ATX headings `#`–`######` | Level styles are attribute-only (bold/underline/dim) |
| Paragraphs | Soft breaks join with spaces; ` ` or `\` at line end hard-breaks |
| `**strong**`, `*em*`, `~~del~~` | `_underscore_` emphasis respects word boundaries |
| `` `code` `` | Rendered inverse; double-backtick spans contain backticks |
| `[label](url)` / `<https://…>` / bare URLs | Rendered underlined; trailing punctuation trimmed from bare URLs |
| Fenced + indented code blocks | Fence info maps to CodeEditor tokenizer presets for monochrome syntax emphasis |
| Lists | Ordered (`1.` / `1)`, start number kept), unordered, nested, tight |
| Task items `- [x]` | Checked markers render dim |
| Blockquotes | GitHub-style dim left bar (nesting supported) |
| Pipe tables | `:-`, `:-:`, `-:` alignment; `\|` escapes; ragged rows normalized |
| `---` / `* * *` / `___` | Thematic break renders as a divider |
| Entities | `&amp;`, `&lt;`, `&gt;`, `&quot;`, `&apos;`, `&nbsp;`, numeric forms |

Intentional divergences from full GFM: no setext headings, reference links,
images, footnotes, or raw HTML (HTML tags render as literal text), no lazy
paragraph continuation, and the CommonMark emphasis algorithm is approximated
with flanking checks.

## Notes

- Parsing never throws. Malformed constructs degrade to literal text, nesting
depth is bounded, and inline scanning runs on a work budget so adversarial
input (PR bodies, agent output) cannot trigger quadratic blowups.
- Styling is attribute-only, so output adapts to every theme; no colors are
hard-coded.
- Styled paragraphs wrap at word boundaries; unstyled paragraphs use
grapheme-safe `text` wrapping.
- For append-only sources use `createMarkdownStream()` (see below); for
static documents that re-render often, pre-parse with
`parseMarkdown(source)` and render with `renderMarkdown(doc, options)`.
- The parsed `MarkdownDocument` AST (`MarkdownBlock` / `MarkdownInline`) is
exported and deeply frozen.

## Streaming

For append-only sources — agent transcripts, live logs — use
`createMarkdownStream(options?)`. Under append-only input every top-level
block except the last is immutable, so the stream re-parses only the tail
block per append and caches both parsed blocks and their rendered VNodes.
Completed blocks keep referential identity, so reconciliation and layout
stability signatures skip untouched subtrees; appends stay O(tail) instead of
O(document).

```ts
import { createMarkdownStream } from "@rezi-ui/core";

const stream = createMarkdownStream({ blockGap: 1 });

onTokens((chunk) => {
stream.append(chunk); // chunks may split lines, CRLF pairs, or words
app.update((s) => ({ ...s, transcriptRev: s.transcriptRev + 1 }));
});

app.view((state) => ui.box({ border: "none" }, [stream.vnode()]));
```

The stream guarantees that `stream.document()` always deep-equals
`parseMarkdown(stream.source())` regardless of chunk boundaries.
`reset(source?)` clears the buffer; `document()` exposes the parsed AST.

## Related

- [RichText](rich-text.md)
- [Code Editor](code-editor.md)
- [Logs Console](logs-console.md)
1 change: 1 addition & 0 deletions docs/widgets/stability.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ The full catalog is tiered below. `stable` is intentionally conservative and res
| Indicators | [Progress](progress.md) | `beta` |
| Indicators | [Skeleton](skeleton.md) | `beta` |
| Indicators | [RichText](rich-text.md) | `beta` |
| Indicators | [Markdown](markdown.md) | `experimental` |
| Indicators | [Kbd](kbd.md) | `beta` |
| Indicators | [Badge](badge.md) | `beta` |
| Indicators | [Status](status.md) | `beta` |
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ nav:
- Progress: widgets/progress.md
- Skeleton: widgets/skeleton.md
- RichText: widgets/rich-text.md
- Markdown: widgets/markdown.md
- Kbd: widgets/kbd.md
- Badge: widgets/badge.md
- Status: widgets/status.md
Expand Down
Loading
Loading