feat: add Angular workspace with demo app and library build

- Restructure into standard Angular library workspace
- Library source in projects/ngx-pretext-table/
- Demo app in src/ using PretextVirtualScrollDirective
- Switch @chenglou/pretext dependency to npm registry (^0.0.4)
- Fix moduleResolution to "bundler" for Angular 17 compatibility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-04 18:29:39 +02:00
parent e8bfdd3154
commit 0994550933
18 changed files with 12179 additions and 19 deletions
+10
View File
@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { DemoComponent } from './demo/demo.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [DemoComponent],
template: `<app-demo />`,
})
export class AppComponent {}
+290
View File
@@ -0,0 +1,290 @@
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
PretextVirtualScrollDirective,
type PretextScrollEvent,
type PretextColumnDef,
} from 'ngx-pretext-table';
interface DemoRow {
id: number;
name: string;
description: string;
tags: string[];
status: string;
}
@Component({
selector: 'app-demo',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, FormsModule, PretextVirtualScrollDirective],
template: `
<div class="demo-header">
<h2>Pretext + Virtual Scroll: Mixed Content Demo</h2>
<p>{{ totalRows | number }} rows — text measured by <strong>pretext</strong>,
inputs/tags use height registry, DOM corrections via ResizeObserver.</p>
<div class="stats">
<span class="stat">Visible: {{ rangeStart }}{{ rangeEnd }}</span>
<span class="stat">Rendered: {{ visibleData.length }} rows</span>
<span class="stat">Total: {{ totalRows | number }}</span>
</div>
</div>
<div pretextVirtualScroll
[data]="allData"
[columns]="pretextColumns"
[font]="font"
[lineHeight]="lineHeight"
[rowPadding]="rowPadding"
[minRowHeight]="44"
[scrollHeight]="'560px'"
[bufferRows]="5"
[measureRows]="true"
(visibleRangeChange)="onVisibleRangeChange($event)"
class="scroll-container">
<table class="pretext-table">
<thead>
<tr>
<th class="col-id">ID</th>
<th class="col-name">Name</th>
<th class="col-desc">Description</th>
<th class="col-tags">Tags</th>
<th class="col-status">Status</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of visibleData; let i = index"
[attr.data-vs-row]="rangeStart + i"
[style.height.px]="visibleRowHeights[i]">
<td class="col-id">{{ row.id }}</td>
<td class="col-name">{{ row.name }}</td>
<td class="col-desc wrap-cell">{{ row.description }}</td>
<td class="col-tags">
<span class="tag" *ngFor="let tag of row.tags">{{ tag }}</span>
</td>
<td class="col-status">
<select [ngModel]="row.status" class="status-select">
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="pending">Pending</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
<div class="legend">
<div class="legend-item">
<span class="legend-dot text-dot"></span> Description: pretext text layout
</div>
<div class="legend-item">
<span class="legend-dot fixed-dot"></span> Status: fixed height (36px)
</div>
<div class="legend-item">
<span class="legend-dot compute-dot"></span> Tags: computed from tag count
</div>
<div class="legend-item">
<span class="legend-dot measure-dot"></span> All rows: ResizeObserver correction
</div>
</div>
`,
styles: [`
:host {
display: block;
max-width: 1000px;
margin: 0 auto;
padding: 24px;
}
.demo-header { margin-bottom: 16px; }
.demo-header h2 { margin: 0 0 8px; font-size: 20px; }
.demo-header p { margin: 0 0 12px; color: #555; font-size: 14px; }
.stats { display: flex; gap: 12px; flex-wrap: wrap; }
.stat {
padding: 4px 10px;
background: #f0f0f0;
border-radius: 4px;
font-size: 12px;
font-family: monospace;
}
.scroll-container {
border: 1px solid #dee2e6;
border-radius: 6px;
background: #fff;
}
.pretext-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
font-size: 14px;
}
.pretext-table thead {
position: sticky;
top: 0;
z-index: 1;
background: #f8f9fa;
}
.pretext-table th {
padding: 10px 8px;
text-align: left;
font-weight: 600;
font-size: 13px;
color: #333;
border-bottom: 2px solid #dee2e6;
}
.pretext-table td {
padding: 8px;
vertical-align: top;
border-bottom: 1px solid #eee;
color: #444;
}
.pretext-table tr:hover td { background: #f8f9fa; }
.col-id { width: 50px; }
.col-name { width: 130px; }
.col-desc { width: 350px; }
.col-tags { width: 250px; }
.col-status { width: 120px; }
.wrap-cell {
white-space: normal;
word-wrap: break-word;
overflow-wrap: break-word;
}
.tag {
display: inline-block;
padding: 2px 8px;
margin: 2px 4px 2px 0;
background: #e8f0fe;
color: #1a73e8;
border-radius: 12px;
font-size: 12px;
white-space: nowrap;
}
.status-select {
width: 100%;
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 13px;
background: #fff;
}
.legend {
margin-top: 12px;
display: flex;
gap: 20px;
flex-wrap: wrap;
font-size: 12px;
color: #666;
}
.legend-item { display: flex; align-items: center; gap: 6px; }
.legend-dot {
width: 10px; height: 10px;
border-radius: 50%;
display: inline-block;
}
.text-dot { background: #4caf50; }
.fixed-dot { background: #ff9800; }
.compute-dot { background: #2196f3; }
.measure-dot { background: #9c27b0; }
`],
})
export class DemoComponent implements OnInit {
readonly totalRows = 10_000;
readonly font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
readonly lineHeight = 20;
readonly rowPadding = 16;
allData: DemoRow[] = [];
visibleData: DemoRow[] = [];
visibleRowHeights: number[] = [];
rangeStart = 0;
rangeEnd = 0;
pretextColumns: PretextColumnDef[] = [
{ field: 'id', width: 34 },
{ field: 'name', width: 114 },
{ field: 'description', width: 334 },
{
field: 'tags',
width: 234,
heightMode: {
kind: 'compute',
fn: (tags: string[]) => {
if (!tags || tags.length === 0) return 20;
const tagsPerRow = Math.max(1, Math.floor(234 / 74));
const rows = Math.ceil(tags.length / tagsPerRow);
return rows * 24 + (rows - 1) * 4;
},
},
},
{
field: 'status',
width: 104,
heightMode: { kind: 'fixed', height: 36 },
},
];
constructor(private cdr: ChangeDetectorRef) {}
ngOnInit(): void {
this.allData = this.generateData(this.totalRows);
}
onVisibleRangeChange(event: PretextScrollEvent): void {
this.visibleData = this.allData.slice(event.start, event.end);
this.visibleRowHeights = event.rowHeights;
this.rangeStart = event.start;
this.rangeEnd = event.end;
this.cdr.markForCheck();
}
private generateData(count: number): DemoRow[] {
const lorem = [
'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.',
'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum.',
'Excepteur sint occaecat cupidatat non proident, sunt in culpa.',
'Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit.',
'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet.',
'At vero eos et accusamus et iusto odio dignissimos ducimus.',
];
const names = [
'Alice Johnson', 'Bob Smith', 'Charlie Brown', 'Diana Prince',
'Eve Wilson', 'Frank Castle', 'Grace Hopper', 'Henry Ford',
'Iris Chang', 'Jack Ryan', 'Kate Moss', 'Leo Messi',
];
const tagPool = [
'Angular', 'React', 'Vue', 'Svelte', 'TypeScript',
'JavaScript', 'CSS', 'HTML', 'Node.js', 'Python',
'Docker', 'K8s', 'AWS', 'Azure', 'GCP',
];
const statuses = ['active', 'inactive', 'pending'];
return Array.from({ length: count }, (_, i) => ({
id: i + 1,
name: names[i % names.length],
description: Array.from({ length: 1 + (i % 5) }, (_, j) =>
lorem[(i + j) % lorem.length]).join(' '),
tags: Array.from({ length: i % 7 }, (_, j) =>
tagPool[(i + j * 3) % tagPool.length]),
status: statuses[i % statuses.length],
}));
}
}
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ngx-pretext-table Demo</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<app-root></app-root>
</body>
</html>
+4
View File
@@ -0,0 +1,4 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent).catch(err => console.error(err));
+10
View File
@@ -0,0 +1,10 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 16px;
background: #fafafa;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}