@portel/photon-core 2.4.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/asset-discovery.d.ts.map +1 -1
- package/dist/asset-discovery.js +2 -1
- package/dist/asset-discovery.js.map +1 -1
- package/dist/base.d.ts +6 -0
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +11 -1
- package/dist/base.js.map +1 -1
- package/dist/collections/ReactiveArray.d.ts +97 -0
- package/dist/collections/ReactiveArray.d.ts.map +1 -0
- package/dist/collections/ReactiveArray.js +158 -0
- package/dist/collections/ReactiveArray.js.map +1 -0
- package/dist/collections/ReactiveMap.d.ts +50 -0
- package/dist/collections/ReactiveMap.d.ts.map +1 -0
- package/dist/collections/ReactiveMap.js +71 -0
- package/dist/collections/ReactiveMap.js.map +1 -0
- package/dist/collections/ReactiveSet.d.ts +50 -0
- package/dist/collections/ReactiveSet.d.ts.map +1 -0
- package/dist/collections/ReactiveSet.js +71 -0
- package/dist/collections/ReactiveSet.js.map +1 -0
- package/dist/collections/index.d.ts +57 -0
- package/dist/collections/index.d.ts.map +1 -0
- package/dist/collections/index.js +59 -0
- package/dist/collections/index.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui-types/Cards.d.ts +139 -0
- package/dist/ui-types/Cards.d.ts.map +1 -0
- package/dist/ui-types/Cards.js +235 -0
- package/dist/ui-types/Cards.js.map +1 -0
- package/dist/ui-types/Chart.d.ts +136 -0
- package/dist/ui-types/Chart.d.ts.map +1 -0
- package/dist/ui-types/Chart.js +188 -0
- package/dist/ui-types/Chart.js.map +1 -0
- package/dist/ui-types/Field.d.ts +342 -0
- package/dist/ui-types/Field.d.ts.map +1 -0
- package/dist/ui-types/Field.js +200 -0
- package/dist/ui-types/Field.js.map +1 -0
- package/dist/ui-types/FieldRenderer.d.ts +32 -0
- package/dist/ui-types/FieldRenderer.d.ts.map +1 -0
- package/dist/ui-types/FieldRenderer.js +277 -0
- package/dist/ui-types/FieldRenderer.js.map +1 -0
- package/dist/ui-types/Form.d.ts +212 -0
- package/dist/ui-types/Form.d.ts.map +1 -0
- package/dist/ui-types/Form.js +278 -0
- package/dist/ui-types/Form.js.map +1 -0
- package/dist/ui-types/Progress.d.ts +130 -0
- package/dist/ui-types/Progress.d.ts.map +1 -0
- package/dist/ui-types/Progress.js +191 -0
- package/dist/ui-types/Progress.js.map +1 -0
- package/dist/ui-types/Stats.d.ts +108 -0
- package/dist/ui-types/Stats.d.ts.map +1 -0
- package/dist/ui-types/Stats.js +162 -0
- package/dist/ui-types/Stats.js.map +1 -0
- package/dist/ui-types/Table.d.ts +206 -0
- package/dist/ui-types/Table.d.ts.map +1 -0
- package/dist/ui-types/Table.js +367 -0
- package/dist/ui-types/Table.js.map +1 -0
- package/dist/ui-types/base.d.ts +17 -0
- package/dist/ui-types/base.d.ts.map +1 -0
- package/dist/ui-types/base.js +18 -0
- package/dist/ui-types/base.js.map +1 -0
- package/dist/ui-types/index.d.ts +42 -0
- package/dist/ui-types/index.d.ts.map +1 -0
- package/dist/ui-types/index.js +50 -0
- package/dist/ui-types/index.js.map +1 -0
- package/package.json +2 -2
- package/src/asset-discovery.ts +2 -1
- package/src/base.ts +13 -1
- package/src/collections/ReactiveArray.ts +179 -0
- package/src/collections/ReactiveMap.ts +81 -0
- package/src/collections/ReactiveSet.ts +81 -0
- package/src/collections/index.ts +60 -0
- package/src/index.ts +80 -0
- package/src/types.ts +2 -0
- package/src/ui-types/Cards.ts +286 -0
- package/src/ui-types/Chart.ts +239 -0
- package/src/ui-types/Field.ts +594 -0
- package/src/ui-types/FieldRenderer.ts +364 -0
- package/src/ui-types/Form.ts +363 -0
- package/src/ui-types/Progress.ts +237 -0
- package/src/ui-types/Stats.ts +204 -0
- package/src/ui-types/Table.ts +438 -0
- package/src/ui-types/base.ts +25 -0
- package/src/ui-types/index.ts +96 -0
- package/src/ui-types/ui-types.test.ts +444 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stats - Purpose-driven type for key metrics/KPIs
|
|
3
|
+
*
|
|
4
|
+
* Automatically renders as a dashboard-style stats display.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* async overview() {
|
|
9
|
+
* return new Stats()
|
|
10
|
+
* .stat('Users', 1234, { trend: '+12%', trendUp: true })
|
|
11
|
+
* .stat('Revenue', 50000, { format: 'currency', prefix: '$' })
|
|
12
|
+
* .stat('Orders', 89, { suffix: 'today' })
|
|
13
|
+
* .stat('Conversion', 3.2, { format: 'percent' });
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { PhotonUIType } from './base.js';
|
|
19
|
+
|
|
20
|
+
export type StatFormat = 'number' | 'currency' | 'percent' | 'compact';
|
|
21
|
+
|
|
22
|
+
export interface StatItem {
|
|
23
|
+
label: string;
|
|
24
|
+
value: number | string;
|
|
25
|
+
format?: StatFormat;
|
|
26
|
+
prefix?: string;
|
|
27
|
+
suffix?: string;
|
|
28
|
+
trend?: string;
|
|
29
|
+
trendUp?: boolean;
|
|
30
|
+
icon?: string;
|
|
31
|
+
color?: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface StatsOptions {
|
|
36
|
+
title?: string;
|
|
37
|
+
columns?: 2 | 3 | 4 | 6;
|
|
38
|
+
compact?: boolean;
|
|
39
|
+
bordered?: boolean;
|
|
40
|
+
animated?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class Stats extends PhotonUIType {
|
|
44
|
+
readonly _photonType = 'stats' as const;
|
|
45
|
+
|
|
46
|
+
private _stats: StatItem[] = [];
|
|
47
|
+
private _options: StatsOptions = {
|
|
48
|
+
columns: 4,
|
|
49
|
+
bordered: true,
|
|
50
|
+
animated: true,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create a new Stats display
|
|
55
|
+
*/
|
|
56
|
+
constructor() {
|
|
57
|
+
super();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Add a stat
|
|
62
|
+
*/
|
|
63
|
+
stat(label: string, value: number | string, options?: Partial<Omit<StatItem, 'label' | 'value'>>): this {
|
|
64
|
+
this._stats.push({
|
|
65
|
+
label,
|
|
66
|
+
value,
|
|
67
|
+
...options,
|
|
68
|
+
});
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Add a currency stat
|
|
74
|
+
*/
|
|
75
|
+
currency(label: string, value: number, options?: { prefix?: string; trend?: string; trendUp?: boolean }): this {
|
|
76
|
+
return this.stat(label, value, {
|
|
77
|
+
format: 'currency',
|
|
78
|
+
prefix: options?.prefix ?? '$',
|
|
79
|
+
trend: options?.trend,
|
|
80
|
+
trendUp: options?.trendUp,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Add a percentage stat
|
|
86
|
+
*/
|
|
87
|
+
percent(label: string, value: number, options?: { trend?: string; trendUp?: boolean }): this {
|
|
88
|
+
return this.stat(label, value, {
|
|
89
|
+
format: 'percent',
|
|
90
|
+
suffix: '%',
|
|
91
|
+
...options,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Add a count stat with compact formatting (1.2K, 5M, etc.)
|
|
97
|
+
*/
|
|
98
|
+
count(label: string, value: number, options?: { trend?: string; trendUp?: boolean; suffix?: string }): this {
|
|
99
|
+
return this.stat(label, value, {
|
|
100
|
+
format: 'compact',
|
|
101
|
+
...options,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Set section title
|
|
107
|
+
*/
|
|
108
|
+
title(title: string): this {
|
|
109
|
+
this._options.title = title;
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Set number of columns
|
|
115
|
+
*/
|
|
116
|
+
columns(count: 2 | 3 | 4 | 6): this {
|
|
117
|
+
this._options.columns = count;
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Use compact layout
|
|
123
|
+
*/
|
|
124
|
+
compact(enabled: boolean = true): this {
|
|
125
|
+
this._options.compact = enabled;
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Show borders
|
|
131
|
+
*/
|
|
132
|
+
bordered(enabled: boolean = true): this {
|
|
133
|
+
this._options.bordered = enabled;
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Enable count-up animation
|
|
139
|
+
*/
|
|
140
|
+
animated(enabled: boolean = true): this {
|
|
141
|
+
this._options.animated = enabled;
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get stat count
|
|
147
|
+
*/
|
|
148
|
+
get length(): number {
|
|
149
|
+
return this._stats.length;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
toJSON() {
|
|
153
|
+
return {
|
|
154
|
+
_photonType: this._photonType,
|
|
155
|
+
stats: this._stats,
|
|
156
|
+
options: this._options,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Render as plain text for MCP clients
|
|
162
|
+
*/
|
|
163
|
+
toString(): string {
|
|
164
|
+
const lines: string[] = [];
|
|
165
|
+
|
|
166
|
+
if (this._options.title) {
|
|
167
|
+
lines.push(`## ${this._options.title}`, '');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const stat of this._stats) {
|
|
171
|
+
let value = String(stat.value);
|
|
172
|
+
|
|
173
|
+
// Format value
|
|
174
|
+
if (stat.format === 'currency' || stat.prefix) {
|
|
175
|
+
value = (stat.prefix ?? '$') + value;
|
|
176
|
+
}
|
|
177
|
+
if (stat.format === 'percent' || stat.suffix === '%') {
|
|
178
|
+
value = value + '%';
|
|
179
|
+
} else if (stat.suffix) {
|
|
180
|
+
value = value + ' ' + stat.suffix;
|
|
181
|
+
}
|
|
182
|
+
if (stat.format === 'compact' && typeof stat.value === 'number') {
|
|
183
|
+
value = this._formatCompact(stat.value);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Add trend
|
|
187
|
+
let line = `**${stat.label}**: ${value}`;
|
|
188
|
+
if (stat.trend) {
|
|
189
|
+
const arrow = stat.trendUp ? '↑' : stat.trendUp === false ? '↓' : '';
|
|
190
|
+
line += ` (${arrow}${stat.trend})`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
lines.push(line);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return lines.join('\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private _formatCompact(num: number): string {
|
|
200
|
+
if (num >= 1_000_000) return (num / 1_000_000).toFixed(1) + 'M';
|
|
201
|
+
if (num >= 1_000) return (num / 1_000).toFixed(1) + 'K';
|
|
202
|
+
return String(num);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table - Purpose-driven type for tabular data
|
|
3
|
+
*
|
|
4
|
+
* Automatically renders as a table UI with sorting, filtering, etc.
|
|
5
|
+
*
|
|
6
|
+
* @example Basic usage
|
|
7
|
+
* ```typescript
|
|
8
|
+
* async users() {
|
|
9
|
+
* return new Table()
|
|
10
|
+
* .column('name', 'Name', 'string')
|
|
11
|
+
* .column('email', 'Email', 'string')
|
|
12
|
+
* .rows(users);
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @example With Field system (React Admin-style)
|
|
17
|
+
* ```typescript
|
|
18
|
+
* async products() {
|
|
19
|
+
* return new Table()
|
|
20
|
+
* .fields([
|
|
21
|
+
* Field.image('thumbnail', { width: 60, rounded: true }),
|
|
22
|
+
* Field.text('name', { link: '/products/{id}' }),
|
|
23
|
+
* Field.price('price', { originalSource: 'msrp', currency: 'USD' }),
|
|
24
|
+
* Field.rating('rating', { countSource: 'reviewCount' }),
|
|
25
|
+
* Field.badge('status', { colors: { active: 'green', draft: 'gray' } }),
|
|
26
|
+
* Field.actions([
|
|
27
|
+
* { label: 'Edit', method: 'edit', icon: 'pencil' },
|
|
28
|
+
* { label: 'Delete', method: 'delete', confirm: true },
|
|
29
|
+
* ]),
|
|
30
|
+
* ])
|
|
31
|
+
* .rows(products);
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { PhotonUIType } from './base.js';
|
|
37
|
+
import { FieldDefinition, formatFieldLabel, Field } from './Field.js';
|
|
38
|
+
import { renderFieldToText } from './FieldRenderer.js';
|
|
39
|
+
|
|
40
|
+
export type ColumnType = 'string' | 'number' | 'boolean' | 'date' | 'currency' | 'link' | 'image' | 'badge';
|
|
41
|
+
|
|
42
|
+
export interface TableColumn {
|
|
43
|
+
key: string;
|
|
44
|
+
label: string;
|
|
45
|
+
type: ColumnType;
|
|
46
|
+
sortable?: boolean;
|
|
47
|
+
width?: string;
|
|
48
|
+
align?: 'left' | 'center' | 'right';
|
|
49
|
+
format?: string; // For date/currency formatting
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TableOptions {
|
|
53
|
+
title?: string;
|
|
54
|
+
searchable?: boolean;
|
|
55
|
+
sortable?: boolean;
|
|
56
|
+
paginated?: boolean;
|
|
57
|
+
pageSize?: number;
|
|
58
|
+
selectable?: boolean;
|
|
59
|
+
striped?: boolean;
|
|
60
|
+
compact?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class Table extends PhotonUIType {
|
|
64
|
+
readonly _photonType = 'table' as const;
|
|
65
|
+
|
|
66
|
+
private _columns: TableColumn[] = [];
|
|
67
|
+
private _fields: FieldDefinition[] = [];
|
|
68
|
+
private _rows: Record<string, any>[] = [];
|
|
69
|
+
private _options: TableOptions = {};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create a new Table
|
|
73
|
+
* @param data Optional initial data (array of objects)
|
|
74
|
+
*/
|
|
75
|
+
constructor(data?: Record<string, any>[]) {
|
|
76
|
+
super();
|
|
77
|
+
if (data && data.length > 0) {
|
|
78
|
+
this._rows = data;
|
|
79
|
+
// Auto-infer columns from first row
|
|
80
|
+
this._inferColumns(data[0]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Add a column definition
|
|
86
|
+
*/
|
|
87
|
+
column(key: string, label: string, type: ColumnType = 'string', options?: Partial<TableColumn>): this {
|
|
88
|
+
this._columns.push({
|
|
89
|
+
key,
|
|
90
|
+
label,
|
|
91
|
+
type,
|
|
92
|
+
sortable: this._options.sortable ?? true,
|
|
93
|
+
...options,
|
|
94
|
+
});
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Add multiple columns at once
|
|
100
|
+
*/
|
|
101
|
+
columns(cols: Array<[key: string, label: string, type?: ColumnType] | TableColumn>): this {
|
|
102
|
+
for (const col of cols) {
|
|
103
|
+
if (Array.isArray(col)) {
|
|
104
|
+
this.column(col[0], col[1], col[2] ?? 'string');
|
|
105
|
+
} else {
|
|
106
|
+
this._columns.push(col);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
113
|
+
// Field-based API (React Admin-style)
|
|
114
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Add fields using the Field system
|
|
118
|
+
*/
|
|
119
|
+
fields(fieldDefs: FieldDefinition[]): this {
|
|
120
|
+
this._fields = fieldDefs;
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Add a single field
|
|
126
|
+
*/
|
|
127
|
+
field(fieldDef: FieldDefinition): this {
|
|
128
|
+
this._fields.push(fieldDef);
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Convenience methods for common field types
|
|
133
|
+
|
|
134
|
+
/** Add text field */
|
|
135
|
+
text(source: string, options?: Parameters<typeof Field.text>[1]): this {
|
|
136
|
+
return this.field(Field.text(source, options));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Add email field */
|
|
140
|
+
email(source: string, options?: Parameters<typeof Field.email>[1]): this {
|
|
141
|
+
return this.field(Field.email(source, options));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Add URL field */
|
|
145
|
+
url(source: string, options?: Parameters<typeof Field.url>[1]): this {
|
|
146
|
+
return this.field(Field.url(source, options));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Add phone field */
|
|
150
|
+
phone(source: string, options?: Parameters<typeof Field.phone>[1]): this {
|
|
151
|
+
return this.field(Field.phone(source, options));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Add number field */
|
|
155
|
+
number(source: string, options?: Parameters<typeof Field.number>[1]): this {
|
|
156
|
+
return this.field(Field.number(source, options));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Add currency field */
|
|
160
|
+
currency(source: string, options?: Parameters<typeof Field.currency>[1]): this {
|
|
161
|
+
return this.field(Field.currency(source, options));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Add percent field */
|
|
165
|
+
percent(source: string, options?: Parameters<typeof Field.percent>[1]): this {
|
|
166
|
+
return this.field(Field.percent(source, options));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Add date field */
|
|
170
|
+
date(source: string, options?: Parameters<typeof Field.date>[1]): this {
|
|
171
|
+
return this.field(Field.date(source, options));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Add datetime field */
|
|
175
|
+
datetime(source: string, options?: Parameters<typeof Field.datetime>[1]): this {
|
|
176
|
+
return this.field(Field.datetime(source, options));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Add boolean field */
|
|
180
|
+
boolean(source: string, options?: Parameters<typeof Field.boolean>[1]): this {
|
|
181
|
+
return this.field(Field.boolean(source, options));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Add image field */
|
|
185
|
+
image(source: string, options?: Parameters<typeof Field.image>[1]): this {
|
|
186
|
+
return this.field(Field.image(source, options));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Add avatar field */
|
|
190
|
+
avatar(source: string, options?: Parameters<typeof Field.avatar>[1]): this {
|
|
191
|
+
return this.field(Field.avatar(source, options));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Add badge field */
|
|
195
|
+
badge(source: string, options?: Parameters<typeof Field.badge>[1]): this {
|
|
196
|
+
return this.field(Field.badge(source, options));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Add tags field */
|
|
200
|
+
tags(source: string, options?: Parameters<typeof Field.tags>[1]): this {
|
|
201
|
+
return this.field(Field.tags(source, options));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Add rating field */
|
|
205
|
+
rating(source: string, options?: Parameters<typeof Field.rating>[1]): this {
|
|
206
|
+
return this.field(Field.rating(source, options));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Add price field */
|
|
210
|
+
price(source: string, options?: Parameters<typeof Field.price>[1]): this {
|
|
211
|
+
return this.field(Field.price(source, options));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Add stock field */
|
|
215
|
+
stock(source: string, options?: Parameters<typeof Field.stock>[1]): this {
|
|
216
|
+
return this.field(Field.stock(source, options));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Add user field */
|
|
220
|
+
user(source: string, options?: Parameters<typeof Field.user>[1]): this {
|
|
221
|
+
return this.field(Field.user(source, options));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Add reference field */
|
|
225
|
+
reference(source: string, options?: Parameters<typeof Field.reference>[1]): this {
|
|
226
|
+
return this.field(Field.reference(source, options));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Add actions field */
|
|
230
|
+
actions(items: Parameters<typeof Field.actions>[0], options?: Parameters<typeof Field.actions>[1]): this {
|
|
231
|
+
return this.field(Field.actions(items, options));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Add custom field */
|
|
235
|
+
custom(source: string, render: Parameters<typeof Field.custom>[1], options?: Parameters<typeof Field.custom>[2]): this {
|
|
236
|
+
return this.field(Field.custom(source, render, options));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
240
|
+
// Data Methods
|
|
241
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Set the table rows
|
|
245
|
+
*/
|
|
246
|
+
rows(data: Record<string, any>[]): this {
|
|
247
|
+
this._rows = data;
|
|
248
|
+
// If no columns defined, infer from data
|
|
249
|
+
if (this._columns.length === 0 && data.length > 0) {
|
|
250
|
+
this._inferColumns(data[0]);
|
|
251
|
+
}
|
|
252
|
+
return this;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Add a single row
|
|
257
|
+
*/
|
|
258
|
+
row(data: Record<string, any>): this {
|
|
259
|
+
this._rows.push(data);
|
|
260
|
+
if (this._columns.length === 0) {
|
|
261
|
+
this._inferColumns(data);
|
|
262
|
+
}
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Set table title
|
|
268
|
+
*/
|
|
269
|
+
title(title: string): this {
|
|
270
|
+
this._options.title = title;
|
|
271
|
+
return this;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Enable/disable search
|
|
276
|
+
*/
|
|
277
|
+
searchable(enabled: boolean = true): this {
|
|
278
|
+
this._options.searchable = enabled;
|
|
279
|
+
return this;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Enable/disable sorting
|
|
284
|
+
*/
|
|
285
|
+
sortable(enabled: boolean = true): this {
|
|
286
|
+
this._options.sortable = enabled;
|
|
287
|
+
return this;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Enable pagination
|
|
292
|
+
*/
|
|
293
|
+
paginated(pageSize: number = 10): this {
|
|
294
|
+
this._options.paginated = true;
|
|
295
|
+
this._options.pageSize = pageSize;
|
|
296
|
+
return this;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Enable row selection
|
|
301
|
+
*/
|
|
302
|
+
selectable(enabled: boolean = true): this {
|
|
303
|
+
this._options.selectable = enabled;
|
|
304
|
+
return this;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Use striped rows
|
|
309
|
+
*/
|
|
310
|
+
striped(enabled: boolean = true): this {
|
|
311
|
+
this._options.striped = enabled;
|
|
312
|
+
return this;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Use compact layout
|
|
317
|
+
*/
|
|
318
|
+
compact(enabled: boolean = true): this {
|
|
319
|
+
this._options.compact = enabled;
|
|
320
|
+
return this;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Infer columns from a data row
|
|
325
|
+
*/
|
|
326
|
+
private _inferColumns(row: Record<string, any>): void {
|
|
327
|
+
for (const [key, value] of Object.entries(row)) {
|
|
328
|
+
const type = this._inferType(value);
|
|
329
|
+
const label = this._formatLabel(key);
|
|
330
|
+
this._columns.push({ key, label, type, sortable: true });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Infer column type from value
|
|
336
|
+
*/
|
|
337
|
+
private _inferType(value: any): ColumnType {
|
|
338
|
+
if (typeof value === 'number') return 'number';
|
|
339
|
+
if (typeof value === 'boolean') return 'boolean';
|
|
340
|
+
if (value instanceof Date) return 'date';
|
|
341
|
+
if (typeof value === 'string') {
|
|
342
|
+
if (value.match(/^\d{4}-\d{2}-\d{2}/)) return 'date';
|
|
343
|
+
if (value.match(/^https?:\/\//)) return 'link';
|
|
344
|
+
if (value.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i)) return 'image';
|
|
345
|
+
}
|
|
346
|
+
return 'string';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Format key to human-readable label
|
|
351
|
+
*/
|
|
352
|
+
private _formatLabel(key: string): string {
|
|
353
|
+
return key
|
|
354
|
+
.replace(/([A-Z])/g, ' $1') // camelCase to spaces
|
|
355
|
+
.replace(/[_-]/g, ' ') // snake_case/kebab-case to spaces
|
|
356
|
+
.replace(/^\w/, c => c.toUpperCase()) // Capitalize first letter
|
|
357
|
+
.trim();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get row count
|
|
362
|
+
*/
|
|
363
|
+
get length(): number {
|
|
364
|
+
return this._rows.length;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Check if table is empty
|
|
369
|
+
*/
|
|
370
|
+
get isEmpty(): boolean {
|
|
371
|
+
return this._rows.length === 0;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Check if using Field system or legacy columns
|
|
376
|
+
*/
|
|
377
|
+
private get _useFields(): boolean {
|
|
378
|
+
return this._fields.length > 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Get effective headers for display
|
|
383
|
+
*/
|
|
384
|
+
private _getHeaders(): string[] {
|
|
385
|
+
if (this._useFields) {
|
|
386
|
+
return this._fields.map(f => f.options.label ?? formatFieldLabel(f.source));
|
|
387
|
+
}
|
|
388
|
+
return this._columns.map(c => c.label);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get cell values for a row
|
|
393
|
+
*/
|
|
394
|
+
private _getCells(row: Record<string, any>): string[] {
|
|
395
|
+
if (this._useFields) {
|
|
396
|
+
return this._fields.map(f => renderFieldToText(f, row));
|
|
397
|
+
}
|
|
398
|
+
return this._columns.map(c => String(row[c.key] ?? ''));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
toJSON() {
|
|
402
|
+
return {
|
|
403
|
+
_photonType: this._photonType,
|
|
404
|
+
columns: this._columns,
|
|
405
|
+
fields: this._fields,
|
|
406
|
+
rows: this._rows,
|
|
407
|
+
options: this._options,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Render as plain text/markdown for MCP clients
|
|
413
|
+
*/
|
|
414
|
+
toString(): string {
|
|
415
|
+
if (this._rows.length === 0) {
|
|
416
|
+
return this._options.title ? `${this._options.title}\n\n(No data)` : '(No data)';
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const lines: string[] = [];
|
|
420
|
+
|
|
421
|
+
if (this._options.title) {
|
|
422
|
+
lines.push(`## ${this._options.title}`, '');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Header row
|
|
426
|
+
const headers = this._getHeaders();
|
|
427
|
+
lines.push('| ' + headers.join(' | ') + ' |');
|
|
428
|
+
lines.push('| ' + headers.map(() => '---').join(' | ') + ' |');
|
|
429
|
+
|
|
430
|
+
// Data rows
|
|
431
|
+
for (const row of this._rows) {
|
|
432
|
+
const cells = this._getCells(row);
|
|
433
|
+
lines.push('| ' + cells.join(' | ') + ' |');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return lines.join('\n');
|
|
437
|
+
}
|
|
438
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for purpose-driven UI types
|
|
3
|
+
*
|
|
4
|
+
* All UI types extend this to get the _photonType discriminator
|
|
5
|
+
* that auto-UI uses to select the appropriate renderer.
|
|
6
|
+
*/
|
|
7
|
+
export abstract class PhotonUIType {
|
|
8
|
+
/** Discriminator for auto-UI type detection */
|
|
9
|
+
abstract readonly _photonType: string;
|
|
10
|
+
|
|
11
|
+
/** Convert to JSON-serializable format */
|
|
12
|
+
abstract toJSON(): Record<string, any>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if a value is a PhotonUIType
|
|
17
|
+
*/
|
|
18
|
+
export function isPhotonUIType(value: unknown): value is PhotonUIType {
|
|
19
|
+
return (
|
|
20
|
+
value !== null &&
|
|
21
|
+
typeof value === 'object' &&
|
|
22
|
+
'_photonType' in value &&
|
|
23
|
+
typeof (value as any)._photonType === 'string'
|
|
24
|
+
);
|
|
25
|
+
}
|