Skip to content

Commit 614682f

Browse files
bloveclaude
andauthored
feat: GFM tables + task-list checkboxes in chat markdown views (0.0.22) (#195)
* feat(chat): add IS_HEADER_ROW injection token for table row/cell DI Provides a Signal<boolean> token that MarkdownTableRowComponent supplies to its subtree so MarkdownTableCellComponent can render <th> vs <td> without passing the flag down as an @input. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(chat): add MarkdownTableComponent (chat-md-table) Renders GFM table nodes as <table class="chat-md-table"> with <thead> (first isHeader row) and <tbody> (remaining rows) via MarkdownChildrenComponent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(chat): add MarkdownTableRowComponent (chat-md-table-row) Renders <tr class="chat-md-table-row"> with optional --header modifier class. Provides IS_HEADER_ROW signal token to its injector subtree so descendant cells can select <th> vs <td> without an extra input binding. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(chat): add MarkdownTableCellComponent (chat-md-table-cell) Renders <th> when IS_HEADER_ROW token is truthy, <td> otherwise. Applies [style.text-align] from node().alignment when non-null. Spec covers td/th switching, alignment binding, and class presence. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(chat): render task-list checkbox prefix in MarkdownListItemComponent When node().task is defined, prepends a disabled <input type="checkbox"> and adds the chat-md-list-item--task BEM modifier class to the <li>. Spec covers: plain item (no checkbox), unchecked task, checked task. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(chat): register table views in registry; export from public surface; style scaffold - cacheplaneMarkdownViews now has 22 entries (table, table-row, table-cell added) - Registry spec updated to assert 22 sorted keys - public-api.ts exports MarkdownTable*, MarkdownTableRow*, MarkdownTableCell* and IS_HEADER_ROW - chat-markdown.styles.ts: display:contents on chat-md-table/row/cell wrappers for layout transparency; task-list class selectors alongside existing :has() rule Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(release): bump all 16 @Ngaf libs to 0.0.22 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add 0.0.22 CHANGELOG entry for tables and task-lists Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(chat): import inject() in markdown-table-row component --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b5d4bb5 commit 614682f

30 files changed

Lines changed: 467 additions & 19 deletions

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
## 0.0.22 — 2026-05-04
2+
3+
### Added
4+
5+
- **`@ngaf/chat`** — markdown view components for GFM tables (`chat-md-table`, `chat-md-table-row`, `chat-md-table-cell`) and task-list checkbox prefix on `MarkdownListItemComponent`. The view registry now exposes all 22 node types emitted by `@cacheplane/partial-markdown@0.2.0`.
6+
7+
### Changed
8+
9+
- All 16 @ngaf libraries synchronized to `0.0.22`.
10+
11+
---
12+
113
## 0.0.21 — 2026-05-04
214

315
### Added

libs/a2ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ngaf/a2ui",
3-
"version": "0.0.21",
3+
"version": "0.0.22",
44
"license": "MIT",
55
"repository": {
66
"type": "git",

libs/ag-ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ngaf/ag-ui",
3-
"version": "0.0.21",
3+
"version": "0.0.22",
44
"peerDependencies": {
55
"@ngaf/chat": "*",
66
"@ngaf/licensing": "*",

libs/chat/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ngaf/chat",
3-
"version": "0.0.21",
3+
"version": "0.0.22",
44
"exports": {
55
".": {
66
"types": "./index.d.ts",

libs/chat/src/lib/markdown/cacheplane-markdown-views.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { describe, it, expect } from 'vitest';
44
import { cacheplaneMarkdownViews } from './cacheplane-markdown-views';
55

66
describe('cacheplaneMarkdownViews', () => {
7-
it('registers all 19 markdown node types (v0.2 adds citation-reference)', () => {
7+
it('registers all 22 markdown node types (v0.2 adds table, table-row, table-cell)', () => {
88
expect(Object.keys(cacheplaneMarkdownViews).sort()).toEqual([
99
'autolink',
1010
'blockquote',
@@ -23,6 +23,9 @@ describe('cacheplaneMarkdownViews', () => {
2323
'soft-break',
2424
'strikethrough',
2525
'strong',
26+
'table',
27+
'table-cell',
28+
'table-row',
2629
'text',
2730
'thematic-break',
2831
]);

libs/chat/src/lib/markdown/cacheplane-markdown-views.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ import { MarkdownImageComponent } from './views/markdown-image.component';
2020
import { MarkdownSoftBreakComponent } from './views/markdown-soft-break.component';
2121
import { MarkdownHardBreakComponent } from './views/markdown-hard-break.component';
2222
import { MarkdownCitationReferenceComponent } from './views/markdown-citation-reference.component';
23+
import { MarkdownTableComponent } from './views/markdown-table.component';
24+
import { MarkdownTableRowComponent } from './views/markdown-table-row.component';
25+
import { MarkdownTableCellComponent } from './views/markdown-table-cell.component';
2326

2427
/**
2528
* Default view registry consumed by <chat-streaming-md>. Maps every
26-
* MarkdownNode.type emitted by @cacheplane/partial-markdown@0.1 to its
29+
* MarkdownNode.type emitted by @cacheplane/partial-markdown@0.2 to its
2730
* corresponding Angular component.
2831
*
2932
* Override per-node-type via `withViews(cacheplaneMarkdownViews, { … })`.
@@ -48,4 +51,7 @@ export const cacheplaneMarkdownViews: ViewRegistry = views({
4851
'soft-break': MarkdownSoftBreakComponent,
4952
'hard-break': MarkdownHardBreakComponent,
5053
'citation-reference': MarkdownCitationReferenceComponent,
54+
'table': MarkdownTableComponent,
55+
'table-row': MarkdownTableRowComponent,
56+
'table-cell': MarkdownTableCellComponent,
5157
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// libs/chat/src/lib/markdown/markdown-table-row.token.ts
2+
// SPDX-License-Identifier: MIT
3+
import { InjectionToken, Signal, signal } from '@angular/core';
4+
5+
/**
6+
* Provided by MarkdownTableRowComponent for header rows so that
7+
* MarkdownTableCellComponent can render <th> instead of <td>.
8+
* The value is a Signal<boolean> so that it tracks the row's isHeader reactively.
9+
*/
10+
export const IS_HEADER_ROW = new InjectionToken<Signal<boolean>>('IS_HEADER_ROW', {
11+
providedIn: null,
12+
factory: () => signal(false),
13+
});
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// libs/chat/src/lib/markdown/views/markdown-list-item.component.spec.ts
2+
// SPDX-License-Identifier: MIT
3+
import { describe, it, expect, beforeEach } from 'vitest';
4+
import { TestBed } from '@angular/core/testing';
5+
import { Component, signal } from '@angular/core';
6+
import { views } from '@ngaf/render';
7+
import type { MarkdownListItemNode } from '@cacheplane/partial-markdown';
8+
import { MarkdownListItemComponent } from './markdown-list-item.component';
9+
import { MarkdownTextComponent } from './markdown-text.component';
10+
import { MARKDOWN_VIEW_REGISTRY } from '../markdown-view-registry';
11+
12+
function makeItemNode(task?: { checked: boolean }): MarkdownListItemNode {
13+
return {
14+
id: 10, type: 'list-item', status: 'complete',
15+
parent: null, index: null,
16+
task,
17+
children: [],
18+
} as MarkdownListItemNode;
19+
}
20+
21+
@Component({
22+
standalone: true,
23+
imports: [MarkdownListItemComponent],
24+
template: `<chat-md-list-item [node]="node()" />`,
25+
})
26+
class HostComponent {
27+
node = signal<MarkdownListItemNode>(makeItemNode());
28+
}
29+
30+
describe('MarkdownListItemComponent', () => {
31+
beforeEach(() => {
32+
TestBed.configureTestingModule({
33+
imports: [HostComponent],
34+
providers: [{
35+
provide: MARKDOWN_VIEW_REGISTRY,
36+
useValue: views({ 'text': MarkdownTextComponent }),
37+
}],
38+
});
39+
});
40+
41+
it('renders a <li> element', () => {
42+
const fixture = TestBed.createComponent(HostComponent);
43+
fixture.detectChanges();
44+
expect(fixture.nativeElement.querySelector('li')).toBeTruthy();
45+
});
46+
47+
it('does not render a checkbox for plain items', () => {
48+
const fixture = TestBed.createComponent(HostComponent);
49+
fixture.detectChanges();
50+
expect(fixture.nativeElement.querySelector('input[type="checkbox"]')).toBeFalsy();
51+
});
52+
53+
it('does not apply task class for plain items', () => {
54+
const fixture = TestBed.createComponent(HostComponent);
55+
fixture.detectChanges();
56+
const li = fixture.nativeElement.querySelector('li');
57+
expect(li.classList.contains('chat-md-list-item--task')).toBe(false);
58+
});
59+
60+
it('renders a disabled unchecked checkbox for task items (unchecked)', () => {
61+
const fixture = TestBed.createComponent(HostComponent);
62+
fixture.componentInstance.node.set(makeItemNode({ checked: false }));
63+
fixture.detectChanges();
64+
const checkbox = fixture.nativeElement.querySelector('input[type="checkbox"]');
65+
expect(checkbox).toBeTruthy();
66+
expect(checkbox.disabled).toBe(true);
67+
expect(checkbox.checked).toBe(false);
68+
});
69+
70+
it('renders a disabled checked checkbox for task items (checked)', () => {
71+
const fixture = TestBed.createComponent(HostComponent);
72+
fixture.componentInstance.node.set(makeItemNode({ checked: true }));
73+
fixture.detectChanges();
74+
const checkbox = fixture.nativeElement.querySelector('input[type="checkbox"]');
75+
expect(checkbox).toBeTruthy();
76+
expect(checkbox.disabled).toBe(true);
77+
expect(checkbox.checked).toBe(true);
78+
});
79+
80+
it('applies task class when task is defined', () => {
81+
const fixture = TestBed.createComponent(HostComponent);
82+
fixture.componentInstance.node.set(makeItemNode({ checked: false }));
83+
fixture.detectChanges();
84+
const li = fixture.nativeElement.querySelector('li');
85+
expect(li.classList.contains('chat-md-list-item--task')).toBe(true);
86+
});
87+
});

libs/chat/src/lib/markdown/views/markdown-list-item.component.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@ import { MarkdownChildrenComponent } from '../markdown-children.component';
99
standalone: true,
1010
imports: [MarkdownChildrenComponent],
1111
changeDetection: ChangeDetectionStrategy.OnPush,
12-
template: `<li><chat-md-children [parent]="node()" /></li>`,
12+
template: `
13+
<li [class.chat-md-list-item--task]="node().task !== undefined">
14+
@if (node().task !== undefined) {
15+
<input type="checkbox" disabled [checked]="node().task!.checked" />
16+
}
17+
<chat-md-children [parent]="node()" />
18+
</li>
19+
`,
1320
})
1421
export class MarkdownListItemComponent {
1522
readonly node = input.required<MarkdownListItemNode>();
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// libs/chat/src/lib/markdown/views/markdown-table-cell.component.spec.ts
2+
// SPDX-License-Identifier: MIT
3+
import { describe, it, expect, beforeEach } from 'vitest';
4+
import { TestBed } from '@angular/core/testing';
5+
import { Component, signal, Signal } from '@angular/core';
6+
import { views } from '@ngaf/render';
7+
import type { MarkdownTableCellNode } from '@cacheplane/partial-markdown';
8+
import { MarkdownTableCellComponent } from './markdown-table-cell.component';
9+
import { MarkdownTextComponent } from './markdown-text.component';
10+
import { IS_HEADER_ROW } from '../markdown-table-row.token';
11+
import { MARKDOWN_VIEW_REGISTRY } from '../markdown-view-registry';
12+
13+
function makeCellNode(alignment: MarkdownTableCellNode['alignment'] = null): MarkdownTableCellNode {
14+
return {
15+
id: 3, type: 'table-cell', status: 'complete',
16+
parent: null, index: null,
17+
alignment,
18+
children: [],
19+
} as MarkdownTableCellNode;
20+
}
21+
22+
@Component({
23+
standalone: true,
24+
imports: [MarkdownTableCellComponent],
25+
template: `<chat-md-table-cell [node]="node()" />`,
26+
})
27+
class HostComponent {
28+
node = signal<MarkdownTableCellNode>(makeCellNode());
29+
}
30+
31+
describe('MarkdownTableCellComponent', () => {
32+
beforeEach(() => {
33+
TestBed.configureTestingModule({
34+
imports: [HostComponent],
35+
providers: [{
36+
provide: MARKDOWN_VIEW_REGISTRY,
37+
useValue: views({ 'text': MarkdownTextComponent }),
38+
}],
39+
});
40+
});
41+
42+
it('renders <td> by default (no IS_HEADER_ROW token)', () => {
43+
const fixture = TestBed.createComponent(HostComponent);
44+
fixture.detectChanges();
45+
expect(fixture.nativeElement.querySelector('td')).toBeTruthy();
46+
expect(fixture.nativeElement.querySelector('th')).toBeFalsy();
47+
});
48+
49+
it('renders <th> when IS_HEADER_ROW token is true', () => {
50+
TestBed.overrideProvider(IS_HEADER_ROW, { useValue: signal(true) as Signal<boolean> });
51+
const fixture = TestBed.createComponent(HostComponent);
52+
fixture.detectChanges();
53+
expect(fixture.nativeElement.querySelector('th')).toBeTruthy();
54+
expect(fixture.nativeElement.querySelector('td')).toBeFalsy();
55+
});
56+
57+
it('does not set text-align style when alignment is null', () => {
58+
const fixture = TestBed.createComponent(HostComponent);
59+
fixture.detectChanges();
60+
const td = fixture.nativeElement.querySelector('td');
61+
expect(td.style.textAlign).toBe('');
62+
});
63+
64+
it('sets text-align style from alignment value', () => {
65+
const fixture = TestBed.createComponent(HostComponent);
66+
fixture.componentInstance.node.set(makeCellNode('center'));
67+
fixture.detectChanges();
68+
const td = fixture.nativeElement.querySelector('td');
69+
expect(td.style.textAlign).toBe('center');
70+
});
71+
72+
it('applies chat-md-table-cell class', () => {
73+
const fixture = TestBed.createComponent(HostComponent);
74+
fixture.detectChanges();
75+
const el = fixture.nativeElement.querySelector('td') ?? fixture.nativeElement.querySelector('th');
76+
expect(el.classList.contains('chat-md-table-cell')).toBe(true);
77+
});
78+
});

0 commit comments

Comments
 (0)