@portel/photon-core 2.4.0 → 2.5.0

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.
Files changed (89) hide show
  1. package/dist/asset-discovery.d.ts.map +1 -1
  2. package/dist/asset-discovery.js +2 -1
  3. package/dist/asset-discovery.js.map +1 -1
  4. package/dist/base.d.ts +6 -0
  5. package/dist/base.d.ts.map +1 -1
  6. package/dist/base.js +11 -1
  7. package/dist/base.js.map +1 -1
  8. package/dist/collections/ReactiveArray.d.ts +97 -0
  9. package/dist/collections/ReactiveArray.d.ts.map +1 -0
  10. package/dist/collections/ReactiveArray.js +158 -0
  11. package/dist/collections/ReactiveArray.js.map +1 -0
  12. package/dist/collections/ReactiveMap.d.ts +50 -0
  13. package/dist/collections/ReactiveMap.d.ts.map +1 -0
  14. package/dist/collections/ReactiveMap.js +71 -0
  15. package/dist/collections/ReactiveMap.js.map +1 -0
  16. package/dist/collections/ReactiveSet.d.ts +50 -0
  17. package/dist/collections/ReactiveSet.d.ts.map +1 -0
  18. package/dist/collections/ReactiveSet.js +71 -0
  19. package/dist/collections/ReactiveSet.js.map +1 -0
  20. package/dist/collections/index.d.ts +44 -0
  21. package/dist/collections/index.d.ts.map +1 -0
  22. package/dist/collections/index.js +44 -0
  23. package/dist/collections/index.js.map +1 -0
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +16 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/types.d.ts +2 -0
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/ui-types/Cards.d.ts +139 -0
  31. package/dist/ui-types/Cards.d.ts.map +1 -0
  32. package/dist/ui-types/Cards.js +235 -0
  33. package/dist/ui-types/Cards.js.map +1 -0
  34. package/dist/ui-types/Chart.d.ts +136 -0
  35. package/dist/ui-types/Chart.d.ts.map +1 -0
  36. package/dist/ui-types/Chart.js +188 -0
  37. package/dist/ui-types/Chart.js.map +1 -0
  38. package/dist/ui-types/Field.d.ts +342 -0
  39. package/dist/ui-types/Field.d.ts.map +1 -0
  40. package/dist/ui-types/Field.js +200 -0
  41. package/dist/ui-types/Field.js.map +1 -0
  42. package/dist/ui-types/FieldRenderer.d.ts +32 -0
  43. package/dist/ui-types/FieldRenderer.d.ts.map +1 -0
  44. package/dist/ui-types/FieldRenderer.js +277 -0
  45. package/dist/ui-types/FieldRenderer.js.map +1 -0
  46. package/dist/ui-types/Form.d.ts +212 -0
  47. package/dist/ui-types/Form.d.ts.map +1 -0
  48. package/dist/ui-types/Form.js +278 -0
  49. package/dist/ui-types/Form.js.map +1 -0
  50. package/dist/ui-types/Progress.d.ts +130 -0
  51. package/dist/ui-types/Progress.d.ts.map +1 -0
  52. package/dist/ui-types/Progress.js +191 -0
  53. package/dist/ui-types/Progress.js.map +1 -0
  54. package/dist/ui-types/Stats.d.ts +108 -0
  55. package/dist/ui-types/Stats.d.ts.map +1 -0
  56. package/dist/ui-types/Stats.js +162 -0
  57. package/dist/ui-types/Stats.js.map +1 -0
  58. package/dist/ui-types/Table.d.ts +206 -0
  59. package/dist/ui-types/Table.d.ts.map +1 -0
  60. package/dist/ui-types/Table.js +367 -0
  61. package/dist/ui-types/Table.js.map +1 -0
  62. package/dist/ui-types/base.d.ts +17 -0
  63. package/dist/ui-types/base.d.ts.map +1 -0
  64. package/dist/ui-types/base.js +18 -0
  65. package/dist/ui-types/base.js.map +1 -0
  66. package/dist/ui-types/index.d.ts +42 -0
  67. package/dist/ui-types/index.d.ts.map +1 -0
  68. package/dist/ui-types/index.js +50 -0
  69. package/dist/ui-types/index.js.map +1 -0
  70. package/package.json +2 -2
  71. package/src/asset-discovery.ts +2 -1
  72. package/src/base.ts +13 -1
  73. package/src/collections/ReactiveArray.ts +179 -0
  74. package/src/collections/ReactiveMap.ts +81 -0
  75. package/src/collections/ReactiveSet.ts +81 -0
  76. package/src/collections/index.ts +44 -0
  77. package/src/index.ts +75 -0
  78. package/src/types.ts +2 -0
  79. package/src/ui-types/Cards.ts +286 -0
  80. package/src/ui-types/Chart.ts +239 -0
  81. package/src/ui-types/Field.ts +594 -0
  82. package/src/ui-types/FieldRenderer.ts +364 -0
  83. package/src/ui-types/Form.ts +363 -0
  84. package/src/ui-types/Progress.ts +237 -0
  85. package/src/ui-types/Stats.ts +204 -0
  86. package/src/ui-types/Table.ts +438 -0
  87. package/src/ui-types/base.ts +25 -0
  88. package/src/ui-types/index.ts +96 -0
  89. package/src/ui-types/ui-types.test.ts +444 -0
@@ -0,0 +1,594 @@
1
+ /**
2
+ * Field - Unified field system for UI types
3
+ *
4
+ * Fields define how data is displayed. They work across Table, Cards, List, etc.
5
+ * Each field type handles its own rendering logic and formatting.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { Field, Table, Cards } from '@portel/photon-core';
10
+ *
11
+ * // Define fields once, use anywhere
12
+ * const productFields = [
13
+ * Field.image('thumbnail', { width: 80, rounded: true }),
14
+ * Field.text('name', { link: '/products/{id}' }),
15
+ * Field.price('price', { original: 'msrp', currency: 'USD' }),
16
+ * Field.rating('rating', { count: 'reviewCount' }),
17
+ * Field.badge('status', { colors: { active: 'green' } }),
18
+ * Field.actions([{ label: 'Edit', method: 'edit' }]),
19
+ * ];
20
+ *
21
+ * // Works in Table
22
+ * new Table().fields(productFields).rows(data);
23
+ *
24
+ * // Works in Cards
25
+ * new Cards().fields(productFields).items(data);
26
+ * ```
27
+ */
28
+
29
+ // ═══════════════════════════════════════════════════════════════════════════════
30
+ // Common Types
31
+ // ═══════════════════════════════════════════════════════════════════════════════
32
+
33
+ export type FieldAlignment = 'left' | 'center' | 'right';
34
+
35
+ export interface BaseFieldOptions {
36
+ /** Display label (auto-inferred from source if not provided) */
37
+ label?: string;
38
+ /** Enable sorting on this field */
39
+ sortable?: boolean;
40
+ /** Sort by different field */
41
+ sortBy?: string;
42
+ /** Text alignment */
43
+ align?: FieldAlignment;
44
+ /** Column width (CSS value like '100px', '20%') */
45
+ columnWidth?: string;
46
+ /** Text to show when value is empty/null */
47
+ emptyText?: string;
48
+ /** Hide this field */
49
+ hidden?: boolean;
50
+ /** Additional CSS class */
51
+ className?: string;
52
+ /** Tooltip text */
53
+ tooltip?: string;
54
+ }
55
+
56
+ // ═══════════════════════════════════════════════════════════════════════════════
57
+ // Text Fields
58
+ // ═══════════════════════════════════════════════════════════════════════════════
59
+
60
+ export interface TextFieldOptions extends BaseFieldOptions {
61
+ /** Make text a link. Use {field} for interpolation */
62
+ link?: string;
63
+ /** Open link in new tab */
64
+ external?: boolean;
65
+ /** Truncate text to max characters */
66
+ truncate?: number;
67
+ /** Text variant */
68
+ variant?: 'heading' | 'body' | 'caption' | 'code';
69
+ /** Make text copyable */
70
+ copyable?: boolean;
71
+ }
72
+
73
+ export interface EmailFieldOptions extends BaseFieldOptions {
74
+ /** Show as mailto link */
75
+ linked?: boolean;
76
+ }
77
+
78
+ export interface UrlFieldOptions extends BaseFieldOptions {
79
+ /** Display text (otherwise shows URL) */
80
+ text?: string;
81
+ /** Truncate URL display */
82
+ truncate?: number;
83
+ }
84
+
85
+ export interface PhoneFieldOptions extends BaseFieldOptions {
86
+ /** Show as tel: link */
87
+ linked?: boolean;
88
+ /** Format pattern */
89
+ format?: string;
90
+ }
91
+
92
+ // ═══════════════════════════════════════════════════════════════════════════════
93
+ // Numeric Fields
94
+ // ═══════════════════════════════════════════════════════════════════════════════
95
+
96
+ export interface NumberFieldOptions extends BaseFieldOptions {
97
+ /** Decimal places */
98
+ decimals?: number;
99
+ /** Use compact notation (1.2K, 5M) */
100
+ compact?: boolean;
101
+ /** Prefix text */
102
+ prefix?: string;
103
+ /** Suffix text */
104
+ suffix?: string;
105
+ /** Locale for formatting */
106
+ locale?: string;
107
+ }
108
+
109
+ export interface CurrencyFieldOptions extends BaseFieldOptions {
110
+ /** Currency code (USD, EUR, etc.) */
111
+ currency?: string;
112
+ /** Locale for formatting */
113
+ locale?: string;
114
+ /** Show currency symbol */
115
+ showSymbol?: boolean;
116
+ /** Decimal places */
117
+ decimals?: number;
118
+ }
119
+
120
+ export interface PercentFieldOptions extends BaseFieldOptions {
121
+ /** Decimal places */
122
+ decimals?: number;
123
+ /** Multiply by 100 (if value is 0.5 for 50%) */
124
+ multiply?: boolean;
125
+ }
126
+
127
+ // ═══════════════════════════════════════════════════════════════════════════════
128
+ // Date Fields
129
+ // ═══════════════════════════════════════════════════════════════════════════════
130
+
131
+ export type DateFormat = 'relative' | 'short' | 'medium' | 'long' | 'iso' | string;
132
+
133
+ export interface DateFieldOptions extends BaseFieldOptions {
134
+ /** Date format */
135
+ format?: DateFormat;
136
+ /** Show time component */
137
+ showTime?: boolean;
138
+ /** Locale for formatting */
139
+ locale?: string;
140
+ }
141
+
142
+ export interface TimeFieldOptions extends BaseFieldOptions {
143
+ /** Time format */
144
+ format?: '12h' | '24h';
145
+ /** Show seconds */
146
+ showSeconds?: boolean;
147
+ }
148
+
149
+ export interface DateRangeFieldOptions extends BaseFieldOptions {
150
+ /** End date source */
151
+ endSource: string;
152
+ /** Format for dates */
153
+ format?: DateFormat;
154
+ /** Separator between dates */
155
+ separator?: string;
156
+ }
157
+
158
+ // ═══════════════════════════════════════════════════════════════════════════════
159
+ // Boolean Fields
160
+ // ═══════════════════════════════════════════════════════════════════════════════
161
+
162
+ export interface BooleanFieldOptions extends BaseFieldOptions {
163
+ /** Label for true value */
164
+ trueLabel?: string;
165
+ /** Label for false value */
166
+ falseLabel?: string;
167
+ /** Icon for true value */
168
+ trueIcon?: string;
169
+ /** Icon for false value */
170
+ falseIcon?: string;
171
+ /** Use colored badges instead of icons */
172
+ asBadge?: boolean;
173
+ }
174
+
175
+ // ═══════════════════════════════════════════════════════════════════════════════
176
+ // Media Fields
177
+ // ═══════════════════════════════════════════════════════════════════════════════
178
+
179
+ export interface ImageFieldOptions extends BaseFieldOptions {
180
+ /** Image width */
181
+ width?: number;
182
+ /** Image height */
183
+ height?: number;
184
+ /** Make image circular */
185
+ rounded?: boolean;
186
+ /** Fallback image URL */
187
+ fallback?: string;
188
+ /** Alt text source */
189
+ altSource?: string;
190
+ /** Enable lightbox on click */
191
+ lightbox?: boolean;
192
+ }
193
+
194
+ export interface AvatarFieldOptions extends BaseFieldOptions {
195
+ /** Size in pixels */
196
+ size?: number;
197
+ /** Source for name (for initials fallback) */
198
+ nameSource?: string;
199
+ }
200
+
201
+ export interface GalleryFieldOptions extends BaseFieldOptions {
202
+ /** Max images to show inline */
203
+ maxVisible?: number;
204
+ /** Thumbnail size */
205
+ thumbnailSize?: number;
206
+ }
207
+
208
+ export interface FileFieldOptions extends BaseFieldOptions {
209
+ /** Show file size */
210
+ showSize?: boolean;
211
+ /** Show file type icon */
212
+ showIcon?: boolean;
213
+ }
214
+
215
+ // ═══════════════════════════════════════════════════════════════════════════════
216
+ // Status/Category Fields
217
+ // ═══════════════════════════════════════════════════════════════════════════════
218
+
219
+ export interface BadgeFieldOptions extends BaseFieldOptions {
220
+ /** Color mapping: { value: color } */
221
+ colors?: Record<string, string>;
222
+ /** Icon mapping: { value: icon } */
223
+ icons?: Record<string, string>;
224
+ /** Variant style */
225
+ variant?: 'solid' | 'outline' | 'subtle';
226
+ /** Size */
227
+ size?: 'sm' | 'md' | 'lg';
228
+ }
229
+
230
+ export interface TagsFieldOptions extends BaseFieldOptions {
231
+ /** Max tags to show */
232
+ max?: number;
233
+ /** Color for tags */
234
+ color?: string;
235
+ }
236
+
237
+ // ═══════════════════════════════════════════════════════════════════════════════
238
+ // Rating Fields
239
+ // ═══════════════════════════════════════════════════════════════════════════════
240
+
241
+ export interface RatingFieldOptions extends BaseFieldOptions {
242
+ /** Maximum rating value */
243
+ max?: number;
244
+ /** Source for review count */
245
+ countSource?: string;
246
+ /** Show numeric value */
247
+ showValue?: boolean;
248
+ /** Icon for filled star */
249
+ icon?: string;
250
+ /** Color for stars */
251
+ color?: string;
252
+ }
253
+
254
+ // ═══════════════════════════════════════════════════════════════════════════════
255
+ // Commerce Fields
256
+ // ═══════════════════════════════════════════════════════════════════════════════
257
+
258
+ export interface PriceFieldOptions extends BaseFieldOptions {
259
+ /** Source for original/compare-at price */
260
+ originalSource?: string;
261
+ /** Currency code */
262
+ currency?: string;
263
+ /** Locale for formatting */
264
+ locale?: string;
265
+ /** Show discount percentage badge */
266
+ showDiscount?: boolean;
267
+ }
268
+
269
+ export interface StockFieldOptions extends BaseFieldOptions {
270
+ /** Label when in stock */
271
+ inStockLabel?: string;
272
+ /** Label when out of stock */
273
+ outOfStockLabel?: string;
274
+ /** Threshold for "low stock" warning */
275
+ lowStockThreshold?: number;
276
+ /** Label for low stock */
277
+ lowStockLabel?: string;
278
+ /** Show quantity */
279
+ showQuantity?: boolean;
280
+ }
281
+
282
+ export interface QuantityFieldOptions extends BaseFieldOptions {
283
+ /** Minimum value */
284
+ min?: number;
285
+ /** Maximum value */
286
+ max?: number;
287
+ /** Step increment */
288
+ step?: number;
289
+ /** Method to call on change */
290
+ onChange?: string;
291
+ }
292
+
293
+ // ═══════════════════════════════════════════════════════════════════════════════
294
+ // Reference Fields
295
+ // ═══════════════════════════════════════════════════════════════════════════════
296
+
297
+ export interface UserFieldOptions extends BaseFieldOptions {
298
+ /** Source for avatar image */
299
+ avatarSource?: string;
300
+ /** Source for display name */
301
+ nameSource?: string;
302
+ /** Source for secondary text (email, role) */
303
+ secondarySource?: string;
304
+ /** Link template */
305
+ link?: string;
306
+ }
307
+
308
+ export interface ReferenceFieldOptions extends BaseFieldOptions {
309
+ /** Source for display text */
310
+ displaySource?: string;
311
+ /** Link template */
312
+ link?: string;
313
+ /** Resource type (for routing) */
314
+ resource?: string;
315
+ }
316
+
317
+ // ═══════════════════════════════════════════════════════════════════════════════
318
+ // Action Fields
319
+ // ═══════════════════════════════════════════════════════════════════════════════
320
+
321
+ export interface ActionItem {
322
+ /** Button label */
323
+ label: string;
324
+ /** Photon method to call */
325
+ method: string;
326
+ /** Icon name */
327
+ icon?: string;
328
+ /** Button variant */
329
+ variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
330
+ /** Require confirmation */
331
+ confirm?: boolean | string;
332
+ /** Confirmation message */
333
+ confirmMessage?: string;
334
+ /** Disable condition (field name that must be truthy to disable) */
335
+ disabledWhen?: string;
336
+ /** Hide condition */
337
+ hiddenWhen?: string;
338
+ }
339
+
340
+ export interface ActionsFieldOptions extends BaseFieldOptions {
341
+ /** Show as dropdown menu */
342
+ dropdown?: boolean;
343
+ /** Dropdown trigger label */
344
+ dropdownLabel?: string;
345
+ }
346
+
347
+ // ═══════════════════════════════════════════════════════════════════════════════
348
+ // Custom Field
349
+ // ═══════════════════════════════════════════════════════════════════════════════
350
+
351
+ export type RenderFunction = (value: any, record: Record<string, any>) => string;
352
+
353
+ export interface CustomFieldOptions extends BaseFieldOptions {
354
+ /** Render function */
355
+ render: RenderFunction;
356
+ }
357
+
358
+ // ═══════════════════════════════════════════════════════════════════════════════
359
+ // Field Definition
360
+ // ═══════════════════════════════════════════════════════════════════════════════
361
+
362
+ export type FieldType =
363
+ | 'text' | 'email' | 'url' | 'phone'
364
+ | 'number' | 'currency' | 'percent'
365
+ | 'date' | 'datetime' | 'time' | 'dateRange'
366
+ | 'boolean'
367
+ | 'image' | 'avatar' | 'gallery' | 'file'
368
+ | 'badge' | 'tags'
369
+ | 'rating'
370
+ | 'price' | 'stock' | 'quantity'
371
+ | 'user' | 'reference'
372
+ | 'actions'
373
+ | 'custom';
374
+
375
+ export interface FieldDefinition {
376
+ type: FieldType;
377
+ source: string;
378
+ options: Record<string, any>;
379
+ actions?: ActionItem[];
380
+ render?: RenderFunction;
381
+ }
382
+
383
+ // ═══════════════════════════════════════════════════════════════════════════════
384
+ // Field Factory
385
+ // ═══════════════════════════════════════════════════════════════════════════════
386
+
387
+ /**
388
+ * Field factory for creating field definitions
389
+ *
390
+ * @example
391
+ * ```typescript
392
+ * const fields = [
393
+ * Field.text('name'),
394
+ * Field.email('email'),
395
+ * Field.price('price', { currency: 'USD', originalSource: 'msrp' }),
396
+ * Field.badge('status', { colors: { active: 'green', inactive: 'gray' } }),
397
+ * Field.actions([{ label: 'Edit', method: 'edit' }]),
398
+ * ];
399
+ * ```
400
+ */
401
+ export const Field = {
402
+ // ─────────────────────────────────────────────────────────────────────────────
403
+ // Text Fields
404
+ // ─────────────────────────────────────────────────────────────────────────────
405
+
406
+ text(source: string, options?: TextFieldOptions): FieldDefinition {
407
+ return { type: 'text', source, options: options ?? {} };
408
+ },
409
+
410
+ email(source: string, options?: EmailFieldOptions): FieldDefinition {
411
+ return { type: 'email', source, options: { linked: true, ...options } };
412
+ },
413
+
414
+ url(source: string, options?: UrlFieldOptions): FieldDefinition {
415
+ return { type: 'url', source, options: options ?? {} };
416
+ },
417
+
418
+ phone(source: string, options?: PhoneFieldOptions): FieldDefinition {
419
+ return { type: 'phone', source, options: { linked: true, ...options } };
420
+ },
421
+
422
+ // ─────────────────────────────────────────────────────────────────────────────
423
+ // Numeric Fields
424
+ // ─────────────────────────────────────────────────────────────────────────────
425
+
426
+ number(source: string, options?: NumberFieldOptions): FieldDefinition {
427
+ return { type: 'number', source, options: options ?? {} };
428
+ },
429
+
430
+ currency(source: string, options?: CurrencyFieldOptions): FieldDefinition {
431
+ return { type: 'currency', source, options: { currency: 'USD', showSymbol: true, ...options } };
432
+ },
433
+
434
+ percent(source: string, options?: PercentFieldOptions): FieldDefinition {
435
+ return { type: 'percent', source, options: { decimals: 1, ...options } };
436
+ },
437
+
438
+ // ─────────────────────────────────────────────────────────────────────────────
439
+ // Date Fields
440
+ // ─────────────────────────────────────────────────────────────────────────────
441
+
442
+ date(source: string, options?: DateFieldOptions): FieldDefinition {
443
+ return { type: 'date', source, options: { format: 'medium', ...options } };
444
+ },
445
+
446
+ datetime(source: string, options?: DateFieldOptions): FieldDefinition {
447
+ return { type: 'datetime', source, options: { format: 'medium', showTime: true, ...options } };
448
+ },
449
+
450
+ time(source: string, options?: TimeFieldOptions): FieldDefinition {
451
+ return { type: 'time', source, options: options ?? {} };
452
+ },
453
+
454
+ dateRange(startSource: string, options: DateRangeFieldOptions): FieldDefinition {
455
+ return { type: 'dateRange', source: startSource, options: { separator: ' → ', ...options } };
456
+ },
457
+
458
+ // ─────────────────────────────────────────────────────────────────────────────
459
+ // Boolean Fields
460
+ // ─────────────────────────────────────────────────────────────────────────────
461
+
462
+ boolean(source: string, options?: BooleanFieldOptions): FieldDefinition {
463
+ return { type: 'boolean', source, options: { trueIcon: '✓', falseIcon: '✗', ...options } };
464
+ },
465
+
466
+ // ─────────────────────────────────────────────────────────────────────────────
467
+ // Media Fields
468
+ // ─────────────────────────────────────────────────────────────────────────────
469
+
470
+ image(source: string, options?: ImageFieldOptions): FieldDefinition {
471
+ return { type: 'image', source, options: options ?? {} };
472
+ },
473
+
474
+ avatar(source: string, options?: AvatarFieldOptions): FieldDefinition {
475
+ return { type: 'avatar', source, options: { size: 40, ...options } };
476
+ },
477
+
478
+ gallery(source: string, options?: GalleryFieldOptions): FieldDefinition {
479
+ return { type: 'gallery', source, options: { maxVisible: 4, thumbnailSize: 60, ...options } };
480
+ },
481
+
482
+ file(source: string, options?: FileFieldOptions): FieldDefinition {
483
+ return { type: 'file', source, options: { showIcon: true, ...options } };
484
+ },
485
+
486
+ // ─────────────────────────────────────────────────────────────────────────────
487
+ // Status/Category Fields
488
+ // ─────────────────────────────────────────────────────────────────────────────
489
+
490
+ badge(source: string, options?: BadgeFieldOptions): FieldDefinition {
491
+ return { type: 'badge', source, options: { variant: 'subtle', ...options } };
492
+ },
493
+
494
+ tags(source: string, options?: TagsFieldOptions): FieldDefinition {
495
+ return { type: 'tags', source, options: { max: 3, ...options } };
496
+ },
497
+
498
+ // ─────────────────────────────────────────────────────────────────────────────
499
+ // Rating Fields
500
+ // ─────────────────────────────────────────────────────────────────────────────
501
+
502
+ rating(source: string, options?: RatingFieldOptions): FieldDefinition {
503
+ return { type: 'rating', source, options: { max: 5, icon: '★', color: '#f59e0b', ...options } };
504
+ },
505
+
506
+ // ─────────────────────────────────────────────────────────────────────────────
507
+ // Commerce Fields
508
+ // ─────────────────────────────────────────────────────────────────────────────
509
+
510
+ price(source: string, options?: PriceFieldOptions): FieldDefinition {
511
+ return { type: 'price', source, options: { currency: 'USD', ...options } };
512
+ },
513
+
514
+ stock(source: string, options?: StockFieldOptions): FieldDefinition {
515
+ return {
516
+ type: 'stock',
517
+ source,
518
+ options: {
519
+ inStockLabel: 'In Stock',
520
+ outOfStockLabel: 'Out of Stock',
521
+ lowStockThreshold: 5,
522
+ lowStockLabel: 'Low Stock',
523
+ ...options,
524
+ },
525
+ };
526
+ },
527
+
528
+ quantity(source: string, options?: QuantityFieldOptions): FieldDefinition {
529
+ return { type: 'quantity', source, options: { min: 1, max: 99, step: 1, ...options } };
530
+ },
531
+
532
+ // ─────────────────────────────────────────────────────────────────────────────
533
+ // Reference Fields
534
+ // ─────────────────────────────────────────────────────────────────────────────
535
+
536
+ user(source: string, options?: UserFieldOptions): FieldDefinition {
537
+ return { type: 'user', source, options: options ?? {} };
538
+ },
539
+
540
+ reference(source: string, options?: ReferenceFieldOptions): FieldDefinition {
541
+ return { type: 'reference', source, options: options ?? {} };
542
+ },
543
+
544
+ // ─────────────────────────────────────────────────────────────────────────────
545
+ // Action Fields
546
+ // ─────────────────────────────────────────────────────────────────────────────
547
+
548
+ actions(items: ActionItem[], options?: ActionsFieldOptions): FieldDefinition {
549
+ return { type: 'actions', source: '', options: options ?? {}, actions: items };
550
+ },
551
+
552
+ // ─────────────────────────────────────────────────────────────────────────────
553
+ // Custom Field
554
+ // ─────────────────────────────────────────────────────────────────────────────
555
+
556
+ custom(source: string, render: RenderFunction, options?: BaseFieldOptions): FieldDefinition {
557
+ return { type: 'custom', source, options: options ?? {}, render };
558
+ },
559
+ };
560
+
561
+ // ═══════════════════════════════════════════════════════════════════════════════
562
+ // Field Utilities
563
+ // ═══════════════════════════════════════════════════════════════════════════════
564
+
565
+ /**
566
+ * Get value from record using dot notation
567
+ */
568
+ export function getFieldValue(record: Record<string, any>, source: string): any {
569
+ if (!source) return undefined;
570
+ return source.split('.').reduce((obj, key) => obj?.[key], record);
571
+ }
572
+
573
+ /**
574
+ * Format label from source (camelCase → Title Case)
575
+ */
576
+ export function formatFieldLabel(source: string): string {
577
+ const lastPart = source.split('.').pop() ?? source;
578
+ return lastPart
579
+ .replace(/([A-Z])/g, ' $1')
580
+ .replace(/[_-]/g, ' ')
581
+ .replace(/^\w/, c => c.toUpperCase())
582
+ .trim();
583
+ }
584
+
585
+ /**
586
+ * Interpolate template string with record values
587
+ * e.g., "/users/{id}" with { id: 123 } → "/users/123"
588
+ */
589
+ export function interpolateTemplate(template: string, record: Record<string, any>): string {
590
+ return template.replace(/\{([^}]+)\}/g, (_, key) => {
591
+ const value = getFieldValue(record, key);
592
+ return value !== undefined ? String(value) : '';
593
+ });
594
+ }