Skip to content

feat!: replace heading-class layout syntax with ::: containers#93

Open
teezeit wants to merge 19 commits into
akonan:mainfrom
teezeit:syntax-change-from-hash-to-dots
Open

feat!: replace heading-class layout syntax with ::: containers#93
teezeit wants to merge 19 commits into
akonan:mainfrom
teezeit:syntax-change-from-hash-to-dots

Conversation

@teezeit
Copy link
Copy Markdown
Contributor

@teezeit teezeit commented Apr 22, 2026

Why

The old heading-class approach used markdown headings as invisible layout declarations:

## Features {.grid-3 card}   ← heading renders as nothing, grid appears
### Item 1
### Item 2
##                            ← bare ## closes the grid

This violated wiremd's core principle: syntax should resemble output. A heading that disappears on render is confusing, hard to read, and inconsistent with every other container in the system (::: card, ::: hero, ::: alert all use ::: already).

What changed

Every layout container now uses explicit ::: syntax:

Old New
## Title {.grid-3} + ## ::: grid-3:::
## Title {.grid-3 card} ::: grid-3 card:::
## Title {.row} ::: row:::
## Title {.row .right} ::: row {.right}:::
## Reports {.tabs} + ### Tab ::: tabs + ::: tab Label:::
## Nav {.sidebar} inside layout ::: sidebar:::
## Content {.main} inside layout ::: main:::
Bare ## (closer) :::

Grid items still use ### headings. Alignment ({.left}, {.right}, {.center}) and col-span ({.col-span-N}) stay on ### children — only the container declaration changes.

Implementation

  • transformer.tstransformContainer() handles grid-N, row, tabs, tab container types; heading-class detection removed from processNodeList
  • html-renderer.ts — sidebar/main detection updated to look for container children with containerType === 'sidebar'/'main'
  • remark-containers.ts — fixed a bug where nested tabs/grid containers without a blank line before their closer were silently dropped (moved parseContainerOpener check before implicit-closer check)
  • Tests — all 439 non-CLI tests updated to new syntax and passing

Migration

A migration script lives at scripts/migrate-v0.3.py for automated conversion of existing .md files.

Manual pattern:

  1. Replace ## Title {.grid-N card}::: grid-N card
  2. Replace closing ##:::
  3. Replace ## Title {.row}::: row
  4. Replace ## Title {.tabs} + ### Tab panels → ::: tabs + ::: tab Label blocks
  5. Inside :::layout {.sidebar-main}, replace ## {.sidebar} / ## {.main} with ::: sidebar / ::: main

Gotcha: when the last child before ::: is a list, add a blank line before the closer — remark will otherwise fold it into the list item.

Docs & examples updated

  • QUICK-REFERENCE.md, SYNTAX-SPEC-v0.1.md, README.md, FAQ.md
  • All docs/ guide pages
  • All claude-skill/ references and examples
  • ~200 occurrences across examples/ and examples/gallery/ (25+ files)

🤖 Generated with Claude Code

teezeit and others added 19 commits April 17, 2026 14:14
- Add `{.grid-N card}` boolean modifier — grid items render with card
  chrome (border/shadow/bg) without changing the structural syntax
- Add `{.col-span-N}` on child headings to span multiple grid columns
- Fix: col-span items now reset to span 1 on mobile across all 7 CSS
  themes, preventing overflow when the grid collapses to 1 column
- Grid heading label is declaration-only and never rendered in output
- Add migration script (scripts/migrate-v0.2.sh) for existing files
- Update all examples, docs, and quick reference

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat: grid card modifier, col-span, and responsive fix
… routing (#2)

* feat: render button with href as <a> tag

[Button]{href:./page.md} now renders as a styled <a> anchor instead
of a non-navigating <button>. Checks both node.href and node.props.href
so the attribute form {href:url} works naturally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: support [[Button](url)] linked-button syntax

[[Text](url)] renders as a button-styled <a> anchor. CommonMark parses
this as text:"[" + link + text:"]" — the transformer now detects that
3-child pattern and hoists the link url to button.href.

Supports variants: [[Label](url)]* for primary, [[Label](url)]{.cls}
for attributes. Also fixes {href:url} attribute form which was stored
in props.href but renderer only checked node.href.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: reset link styling on buttons rendered as <a> tags

a.wmd-button gets text-decoration:none and color:inherit so button-links
look identical to regular buttons — no underline, no browser link color.
Applied once in getStyleCSS() so all 7 themes inherit it automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: multi-file routing in dev server

The dev server now renders any .md file on demand when its URL is
requested, enabling button-links like [[Docs](./docs.md)] to navigate
between wireframe files in the browser.

- startServer() accepts renderFile callback and rootDir
- GET /page.md → calls renderFile(path) and serves result
- GET /page.html → serves cached HTML or renders from .md
- GET / → serves main watched file (unchanged)
- GET unknown → 404
- startServer() now returns the server instance (testability)
- CLI passes renderFile and rootDir to startServer
- Add href? to button node type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: redirect / to entry file for symmetric navigation

GET / now redirects to /{inputFile} (e.g. /agency-site.md) so there
is no special root — every file is addressed by its own path. Links
back to the entry file work the same as links to any other file.

Fallback: when no inputFile is set (programmatic use), / still serves
the pre-rendered outputPath as before.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: nav link support, active state, and multi-button-link inline

Nav `[[ ]]` items now fully support links and active states:

- `[text](url)` inside `[[ ]]` creates a navigable nav item with real href.
  Previously the link URL was silently dropped (remark parses links as
  `link` nodes with no `.value`; the inline-containers plugin was using
  `c.value || ''` which discarded them).
  Fix: added `serializeChild` to reconstruct `[text](url)` from MDAST
  link nodes before splitting items on `|`.

- `*text*` inside `[[ ]]` is now treated as the active/current-page item
  (strips asterisks, adds `.active` class with theme-matched styling).
  Previously rendered as literal `*text*`.

- `[text](url)*` in nav renders as a primary-styled button link.

- Multiple `[[Btn](url)]` patterns on the same line now render correctly.
  Previously the second button's URL was dropped and brackets showed as
  literal text. Root cause: `transformParagraph`'s 3-child detection
  only handled a single button-link. For two buttons on one line remark
  produces 5 children (`[`, link, `]* [`, link, `]`).
  Fix: new `tryParseButtonLinkSequence` generalises detection to n ≥ 1
  (replaces the old 3-child check); multiple matches return a
  `button-group` container.

Also adds:
- Active nav-item CSS for all 6 themes (`.wmd-nav-item.wmd-active`)
- `docs/guide/syntax.md` — Button Links section
- `QUICK-REFERENCE.md` — nav link and active-state rows
- `examples/gallery/multi-page/` — three-page navigable prototype
  (home/about/contact sharing a nav with live button-link navigation)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add renderSidebarMainLayout() to html-renderer — groups children by
  {.sidebar}/{.main} heading markers into wmd-layout-sidebar/main divs
- Add sidebar + layout CSS to all 7 style themes (nav-matching backgrounds,
  full-width buttons, section label styling)
- Fix grid detection inside containers: extract processNodeList() shared
  helper so both transformToWiremdAST and transformContainer detect
  {.grid-N} headings — previously grids inside :::layout, :::card etc.
  were silently rendered as plain headings
- Add examples/sidebar-layout.md
- Add parser and renderer tests for sidebar layout and nested grids

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The wiremd CLI writes an .html output file next to the source .md by
default. These are build artifacts and shouldn't be tracked.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## What this fixes

The previous parser (`remark-containers.ts`) used a flat linear scan: when it found a `:::type` opener, it iterated forward collecting sibling nodes until it hit a closing `:::`. Any `:::type` encountered during that scan was either treated as plain text content or re-injected into the tree incorrectly.

This meant **nested containers silently didn't work** — `:::card` inside `:::modal` would appear as a sibling of the modal, not a child.

## What changed

**Rewrote `remark-containers.ts`** using a recursive `collectContainer()` approach:

- When collecting a container's children, any `:::type` opener found triggers a recursive call — producing a properly nested AST node.
- Three cases handled cleanly: (1) complete container in a single plain-text paragraph, (2) complete container with inline elements, (3) multi-block container with recursive nesting.
- **Implicit same-paragraph closing** — handles `[Save]* [Cancel]\n:::` where the closing `:::` shares a paragraph with the last content line.
- **Opener-content extraction** — `:::alert Warning: this action is irreversible` adds the inline text as the container's first child, with the type parsed as `alert` only.

## Bugs fixed

| Symptom | Root cause | Fix |
|---------|-----------|-----|
| `:::card` inside `:::modal` rendered as a sibling, not nested | Flat scan had no recursion; nested opener was re-injected at the wrong level | `collectContainer()` recurses when it encounters a nested opener |
| `[Save]\n:::` (no blank line before closer) — closer was missed | Implicit closer detection was inconsistent across code paths | Unified implicit-closer handling in Case 3 |
| `:::alert Some message` parsed containerType as `"alert Some message"` | Regex `([^{]+?)` captured all non-brace chars including spaces | New regex `(\S+)` stops at whitespace; remainder becomes inline content |

## Tests added

- `should nest a container inside another container` — asserts `:::card` is a child container of `:::modal`, not a sibling
- `should extract inline content from the opener line` — asserts `::: alert Warning text` produces correct containerType + first child

## Docs

Added nesting and opener-content examples to the Block Containers section of `docs/guide/syntax.md`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Intercept <a href="*.md"> clicks in the webview and post a navigate
  message so clicking a link switches the editor to that file instead
  of opening a browser URL
- Add resolveLink() supporting both relative and absolute hrefs;
  absolute paths walk up the directory tree to find the target file
- showTextDocument uses the current editor's viewColumn so the file
  replaces in-place rather than opening a new tab
- Register onDidDispose in deserializeWebviewPanel (mirrors the
  createOrShowPreview path) so restored panels clean up correctly
- Switch vscode:prepublish from tsc to esbuild bundle so wiremd is
  inlined into the VSIX and the extension activates correctly

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(tabs): interactive tabs via '## Title {.tabs}' heading class

Mirrors the grid pattern: heading with .tabs class declares the tabs
container; direct child headings (depth+1) become tab panels. First
tab active by default; {.active} on a sub-heading overrides.

- Parser: detect .tabs class in processNodeList, emit tabs/tab nodes
- Renderer: tabs case emits header row + panel divs, injects one-time
  click-delegated switcher script (idempotent via window.__wmdTabsInit)
- Styles: structural rules in getStyleCSS wrapper (hidden panels),
  sketch-themed pill headers matching nav-item aesthetic
- Tests: 7 parser + 6 renderer tests (TDD)

* docs(tabs): add tabs-demo example + rendered artifact

Source: examples/tabs-demo.md
Rendered: docs/tabs-demo.html (under docs/ so it's not caught by the
examples/**/*.html gitignore rule, letting mobile users preview via
htmlpreview.github.io).

* style(tabs): replace button look with underline tab style

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: add tabs syntax to guide and spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style(tabs): update tab header border color for improved visibility

---------

Co-authored-by: Claude <noreply@anthropic.com>
* Initial plan

* feat: parse :icon-name: syntax in table cells

Agent-Logs-Url: https://github.com/teezeit/wiremd/sessions/3928f112-c787-4fa9-9aae-b79fc8490d4e

Co-authored-by: teezeit <17304928+teezeit@users.noreply.github.com>

* fix: trim whitespace from icon cell remainder text

Agent-Logs-Url: https://github.com/teezeit/wiremd/sessions/3928f112-c787-4fa9-9aae-b79fc8490d4e

Co-authored-by: teezeit <17304928+teezeit@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: teezeit <17304928+teezeit@users.noreply.github.com>
* feat(vscode): update icon to 128px logo, fix vsix packaging

- Replace placeholder icon with wiremd logo (128×128 px)
- Exclude parent node_modules from vsix via .vscodeignore (180 MB → 95 KB)
- Rebuild wiremd-preview-0.1.0.vsix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(vscode): remove vsix binary from repo, gitignore *.vsix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(vscode): bundle preview-provider.ts with esbuild to inline wiremd

Without bundling, preview-provider.js relied on ../node_modules/wiremd
at runtime which broke after ../node_modules was excluded from the vsix.
Bundling inlines wiremd so the extension is fully self-contained.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add wireframe skill documentation and workflows

- Introduced SKILL.md for wireframe skill, detailing usage for documenting existing screens and wireframing future iterations.
- Added syntax reference in syntax.md for wiremd, covering disambiguation rules, buttons, inputs, navigation, and more.
- Created vscode.md for setting up and using the wiremd VS Code extension, including installation and daily workflows.
- Implemented build scripts for Obsidian plugin and VS Code extension, ensuring easy packaging and installation.
- Developed render-all.sh for rendering all wireframes to HTML and generating an index page.
- Added serve.sh for starting a live-reload preview server for wireframe files.
- Created CLAUDE.md to provide guidance on using the wiremd project, including commands, architecture, and testing.

* feat: add claude-skill/ as first-class distribution artifact

Move the wireframe skill from .claude/skills/wireframe/ into claude-skill/
at the repo root so it ships alongside the npm package (like vscode-extension/).
.claude/skills/wireframe is now a symlink to ../../claude-skill for local use.

- Add claude-skill/ with rewritten SKILL.md (generic, not JAM-specific)
- Add references/styles.md, references/examples/dashboard.md and settings-form.md
- Copy QUICK-REFERENCE.md into references/quick-reference.md (build:skill script)
- Update syntax.md: grid+card variant, tabs via button groups, progress bars,
  component mapping table, column spans
- Add claude-skill to package.json files array and build:skill script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ut detection (#18)

Grid item and tab panel inner loops were calling transformNode per node,
which only handles single MDAST nodes and skips the heading-based layout
detection (grids, tabs) that lives in processNodeList. Nested layouts
were silently rendered as plain headings.

Fix: collect raw MDAST nodes for each item/panel, then pass through
processNodeList — the same path used by :::containers.

Closes #15

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces `{.row}` as a new layout heading syntax alongside `{.grid-N}`.
Content directly under `## {.row}` is auto-wrapped as implicit flex items;
`###` children are supported for per-item `{.left}`/`{.center}`/`{.right}`
alignment. Row-level `{.right}` and `{.center}` map to justify-content.

Also fixes duplicate CSS class names on row/grid/tabs nodes (syntactic
class names were leaking into props.classes), suppresses empty `<h2>`
terminators from rendering, and wires up row rendering in the React and
Tailwind renderers.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…) (#23)

Adds inline badges for status labels, counts, and filter chips.
Parser detects |Label| and |Label|{.variant} in both plain and rich-content
paragraphs; renderers (HTML, React, Tailwind) and all 7 CSS styles updated.
Includes 23 new tests and docs updated across SKILL.md, syntax references,
QUICK-REFERENCE.md, SYNTAX-SPEC, and guide.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: resolve ![[file.md]] includes in parser, CLI, and VS Code preview

Add resolveIncludes() to the parser which expands ![[file.md]] syntax
by inlining the referenced file's content. Paths resolve relative to
the base file. Missing files render as a warning blockquote. Wired into
the CLI pipeline and the VS Code preview provider.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: document ![[file.md]] include syntax across all references

- docs/guide/syntax.md: add File Includes section
- QUICK-REFERENCE.md: add include row to component table, troubleshooting entry
- SYNTAX-SPEC-v0.1.md: move file includes from deferred to included (v0.1)
- claude-skill/references/syntax.md: replace obsolete :::display with ![[file.md]]
- claude-skill/SKILL.md: update gotcha #5 — :::display is obsolete, ![[]] works in CLI and VS Code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…tyle own it (#25)

Sidebar layout columns had redundant padding/background/border/box-shadow
baked into .layout-sidebar across all 7 themes. These fight with the
:::sidebar container's own styling when nested inside a sidebar-main layout.

Remove per-theme overrides from .layout-sidebar and add a single
.container-sidebar { width: 100% } rule so the sidebar fills its column.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…frame themes (#27)

Cards should fill their container — the 400px cap was forcing cards into
a narrow column even when placed in a grid or a full-width layout.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…items (#29)

* test(row): add failing cases for dropdown options lost inside implicit row

Covers two bugs (both now fixed):
- dropdown options empty when select is a direct row child
- option list leaking as a stray grid-item sibling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(row): associate dropdown option list with select inside implicit row items

When the row parser looped over direct children it called processNodeList
with a single-element slice, so nextNode was always undefined inside the
call.  Dropdown selects therefore got no options; the list then appeared as
a stray grid-item bullet list.

Fix: before pushing the grid-item, peek at the next sibling; if the current
node is a dropdown paragraph (ends with `v]`) and the sibling is a list,
include both in the same processNodeList call so the existing lookahead
logic can associate options with the select.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Old syntax used heading classes to declare layouts, which rendered the
heading itself as an invisible no-op — a violation of the core principle
that wiremd syntax should resemble its visual output:

  ## Title {.grid-3}   →  heading disappears, grid appears
  ## {.row}            →  heading disappears, row appears
  ## Reports {.tabs}   →  heading disappears, tabs appear

New syntax uses explicit ::: container blocks, matching every other
wiremd container and making the structure visible in the source:

  ::: grid-3           →  grid container
  ::: row              →  flex row
  ::: tabs             →  tabbed panel

**Breaking changes:**
- `## Heading {.grid-N}` → `::: grid-N` … `:::`
- `## Heading {.row}` → `::: row` … `:::`
- `## Heading {.row .right}` → `::: row {.right}` … `:::`
- `## Heading {.tabs}` + `### Tab` children → `::: tabs` with `::: tab Label` children
- `## {.sidebar}` / `## {.main}` inside `:::layout` → `::: sidebar` / `::: main`
- Bare `##` closers are gone — every container closes with `:::`

Grid items, alignment (`{.left}`, `{.right}`, `{.center}`), and
col-span (`{.col-span-N}`) still use `###` headings inside containers.

**Implementation:**
- `transformer.ts`: `transformContainer()` handles grid-N/row/tabs/tab;
  heading-class detection removed from `processNodeList`
- `html-renderer.ts`: sidebar/main detection updated for container children
- `remark-containers.ts`: fixed bug where nested tabs/grids without a
  blank line before their closer were silently dropped
- All 439 non-CLI tests updated and passing

**Docs & examples:**
- QUICK-REFERENCE, SYNTAX-SPEC, README, FAQ, all docs/ updated
- claude-skill references and examples updated
- All ~200 occurrences across examples/gallery/ migrated
- Migration script at scripts/migrate-v0.3.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants