@portel/photon-core 1.5.0 → 2.1.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 (163) hide show
  1. package/dist/auto-ui.js +1 -1
  2. package/dist/auto-ui.js.map +1 -1
  3. package/dist/base.d.ts +1 -1
  4. package/dist/base.d.ts.map +1 -1
  5. package/dist/base.js +2 -2
  6. package/dist/base.js.map +1 -1
  7. package/dist/cli-ui-renderer.js +1 -1
  8. package/dist/cli-ui-renderer.js.map +1 -1
  9. package/dist/design-system/index.d.ts +21 -0
  10. package/dist/design-system/index.d.ts.map +1 -0
  11. package/dist/design-system/index.js +27 -0
  12. package/dist/design-system/index.js.map +1 -0
  13. package/dist/design-system/tokens.d.ts +149 -0
  14. package/dist/design-system/tokens.d.ts.map +1 -0
  15. package/dist/design-system/tokens.js +413 -0
  16. package/dist/design-system/tokens.js.map +1 -0
  17. package/dist/design-system/transaction-ui.d.ts +70 -0
  18. package/dist/design-system/transaction-ui.d.ts.map +1 -0
  19. package/dist/design-system/transaction-ui.js +982 -0
  20. package/dist/design-system/transaction-ui.js.map +1 -0
  21. package/dist/generator.d.ts +56 -6
  22. package/dist/generator.d.ts.map +1 -1
  23. package/dist/generator.js.map +1 -1
  24. package/dist/index.d.ts +6 -7
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +46 -56
  27. package/dist/index.js.map +1 -1
  28. package/dist/io.d.ts +103 -2
  29. package/dist/io.d.ts.map +1 -1
  30. package/dist/io.js +37 -1
  31. package/dist/io.js.map +1 -1
  32. package/dist/rendering/components.d.ts +29 -0
  33. package/dist/rendering/components.d.ts.map +1 -0
  34. package/dist/rendering/components.js +773 -0
  35. package/dist/rendering/components.js.map +1 -0
  36. package/dist/rendering/field-analyzer.d.ts +48 -0
  37. package/dist/rendering/field-analyzer.d.ts.map +1 -0
  38. package/dist/rendering/field-analyzer.js +270 -0
  39. package/dist/rendering/field-analyzer.js.map +1 -0
  40. package/dist/rendering/field-renderers.d.ts +64 -0
  41. package/dist/rendering/field-renderers.d.ts.map +1 -0
  42. package/dist/rendering/field-renderers.js +317 -0
  43. package/dist/rendering/field-renderers.js.map +1 -0
  44. package/dist/rendering/index.d.ts +28 -0
  45. package/dist/rendering/index.d.ts.map +1 -0
  46. package/dist/rendering/index.js +60 -0
  47. package/dist/rendering/index.js.map +1 -0
  48. package/dist/rendering/layout-selector.d.ts +48 -0
  49. package/dist/rendering/layout-selector.d.ts.map +1 -0
  50. package/dist/rendering/layout-selector.js +347 -0
  51. package/dist/rendering/layout-selector.js.map +1 -0
  52. package/dist/rendering/template-engine.d.ts +41 -0
  53. package/dist/rendering/template-engine.d.ts.map +1 -0
  54. package/dist/rendering/template-engine.js +236 -0
  55. package/dist/rendering/template-engine.js.map +1 -0
  56. package/dist/schema-extractor.d.ts +30 -0
  57. package/dist/schema-extractor.d.ts.map +1 -1
  58. package/dist/schema-extractor.js +205 -12
  59. package/dist/schema-extractor.js.map +1 -1
  60. package/dist/stateful.js +1 -1
  61. package/dist/stateful.js.map +1 -1
  62. package/dist/types.d.ts +9 -1
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/types.js.map +1 -1
  65. package/dist/ucp/ap2/handlers.d.ts +242 -0
  66. package/dist/ucp/ap2/handlers.d.ts.map +1 -0
  67. package/dist/ucp/ap2/handlers.js +482 -0
  68. package/dist/ucp/ap2/handlers.js.map +1 -0
  69. package/dist/ucp/ap2/mandates.d.ts +95 -0
  70. package/dist/ucp/ap2/mandates.d.ts.map +1 -0
  71. package/dist/ucp/ap2/mandates.js +234 -0
  72. package/dist/ucp/ap2/mandates.js.map +1 -0
  73. package/dist/ucp/ap2/types.d.ts +305 -0
  74. package/dist/ucp/ap2/types.d.ts.map +1 -0
  75. package/dist/ucp/ap2/types.js +8 -0
  76. package/dist/ucp/ap2/types.js.map +1 -0
  77. package/dist/ucp/capabilities/checkout.d.ts +118 -0
  78. package/dist/ucp/capabilities/checkout.d.ts.map +1 -0
  79. package/dist/ucp/capabilities/checkout.js +344 -0
  80. package/dist/ucp/capabilities/checkout.js.map +1 -0
  81. package/dist/ucp/capabilities/identity.d.ts +130 -0
  82. package/dist/ucp/capabilities/identity.d.ts.map +1 -0
  83. package/dist/ucp/capabilities/identity.js +290 -0
  84. package/dist/ucp/capabilities/identity.js.map +1 -0
  85. package/dist/ucp/capabilities/order.d.ts +142 -0
  86. package/dist/ucp/capabilities/order.d.ts.map +1 -0
  87. package/dist/ucp/capabilities/order.js +383 -0
  88. package/dist/ucp/capabilities/order.js.map +1 -0
  89. package/dist/ucp/index.d.ts +18 -0
  90. package/dist/ucp/index.d.ts.map +1 -0
  91. package/dist/ucp/index.js +19 -0
  92. package/dist/ucp/index.js.map +1 -0
  93. package/dist/ucp/manifest.d.ts +62 -0
  94. package/dist/ucp/manifest.d.ts.map +1 -0
  95. package/dist/ucp/manifest.js +180 -0
  96. package/dist/ucp/manifest.js.map +1 -0
  97. package/dist/ucp/types.d.ts +327 -0
  98. package/dist/ucp/types.d.ts.map +1 -0
  99. package/dist/ucp/types.js +8 -0
  100. package/dist/ucp/types.js.map +1 -0
  101. package/package.json +3 -4
  102. package/src/auto-ui.ts +1 -1
  103. package/src/base.ts +2 -2
  104. package/src/cli-ui-renderer.ts +1 -1
  105. package/src/design-system/index.ts +30 -0
  106. package/src/design-system/tokens.ts +451 -0
  107. package/src/design-system/transaction-ui.ts +1038 -0
  108. package/src/generator.ts +58 -2
  109. package/src/index.ts +135 -124
  110. package/src/io.ts +108 -3
  111. package/src/rendering/components.ts +785 -0
  112. package/src/rendering/field-analyzer.ts +299 -0
  113. package/src/rendering/field-renderers.ts +356 -0
  114. package/src/rendering/index.ts +63 -0
  115. package/src/rendering/layout-selector.ts +390 -0
  116. package/src/rendering/template-engine.ts +254 -0
  117. package/src/schema-extractor.ts +225 -12
  118. package/src/stateful.ts +1 -1
  119. package/src/types.ts +10 -1
  120. package/src/ucp/ap2/handlers.ts +779 -0
  121. package/src/ucp/ap2/mandates.ts +354 -0
  122. package/src/ucp/ap2/types.ts +441 -0
  123. package/src/ucp/capabilities/checkout.ts +497 -0
  124. package/src/ucp/capabilities/identity.ts +425 -0
  125. package/src/ucp/capabilities/order.ts +549 -0
  126. package/src/ucp/index.ts +27 -0
  127. package/src/ucp/manifest.ts +257 -0
  128. package/src/ucp/types.ts +454 -0
  129. package/dist/cli-formatter.d.ts +0 -92
  130. package/dist/cli-formatter.d.ts.map +0 -1
  131. package/dist/cli-formatter.js +0 -486
  132. package/dist/cli-formatter.js.map +0 -1
  133. package/dist/context.d.ts +0 -6
  134. package/dist/context.d.ts.map +0 -1
  135. package/dist/context.js +0 -3
  136. package/dist/context.js.map +0 -1
  137. package/dist/elicit.d.ts +0 -93
  138. package/dist/elicit.d.ts.map +0 -1
  139. package/dist/elicit.js +0 -373
  140. package/dist/elicit.js.map +0 -1
  141. package/dist/mcp-client.d.ts +0 -218
  142. package/dist/mcp-client.d.ts.map +0 -1
  143. package/dist/mcp-client.js +0 -424
  144. package/dist/mcp-client.js.map +0 -1
  145. package/dist/mcp-sdk-transport.d.ts +0 -88
  146. package/dist/mcp-sdk-transport.d.ts.map +0 -1
  147. package/dist/mcp-sdk-transport.js +0 -360
  148. package/dist/mcp-sdk-transport.js.map +0 -1
  149. package/dist/photon-config.d.ts +0 -86
  150. package/dist/photon-config.d.ts.map +0 -1
  151. package/dist/photon-config.js +0 -156
  152. package/dist/photon-config.js.map +0 -1
  153. package/dist/progress.d.ts +0 -93
  154. package/dist/progress.d.ts.map +0 -1
  155. package/dist/progress.js +0 -195
  156. package/dist/progress.js.map +0 -1
  157. package/src/cli-formatter.ts +0 -579
  158. package/src/context.ts +0 -7
  159. package/src/elicit.ts +0 -438
  160. package/src/mcp-client.ts +0 -561
  161. package/src/mcp-sdk-transport.ts +0 -449
  162. package/src/photon-config.ts +0 -201
  163. package/src/progress.ts +0 -224
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Smart Rendering Module
3
+ *
4
+ * iOS-inspired, React Admin-influenced rendering system that:
5
+ * - Convention over configuration - works beautifully with zero annotations
6
+ * - Smart field detection - guesses meaning from field names and types
7
+ * - Progressive customization - JSDoc annotations for fine-tuning
8
+ * - List-centric paradigm - everything renders as configurable list items
9
+ *
10
+ * Design System based on:
11
+ * - Material Design 3 tokens: https://m3.material.io/foundations/design-tokens
12
+ * - Apple HIG (8pt grid): https://developer.apple.com/design/human-interface-guidelines/layout
13
+ */
14
+
15
+ export * from './field-analyzer.js';
16
+ export * from './layout-selector.js';
17
+ export * from './components.js';
18
+ export * from './field-renderers.js';
19
+ export * from './template-engine.js';
20
+ export * from '../design-system/index.js';
21
+
22
+ import { generateFieldAnalyzerJS } from './field-analyzer.js';
23
+ import { generateLayoutSelectorJS } from './layout-selector.js';
24
+ import { generateComponentsJS, generateComponentCSS } from './components.js';
25
+ import { generateFieldRenderersJS, generateFieldRendererCSS } from './field-renderers.js';
26
+ import { generateTemplateEngineJS, generateTemplateEngineCSS } from './template-engine.js';
27
+ import { generateDesignSystemCSS, generateDesignSystemJS } from '../design-system/index.js';
28
+
29
+ /**
30
+ * Generate all JavaScript code for embedding in HTML
31
+ */
32
+ export function generateSmartRenderingJS(): string {
33
+ return [
34
+ '// ==========================================================================',
35
+ '// Photon Design System + Smart Rendering',
36
+ '// Based on Material Design 3 + Apple HIG',
37
+ '// ==========================================================================',
38
+ '',
39
+ generateDesignSystemJS(),
40
+ '',
41
+ generateFieldAnalyzerJS(),
42
+ '',
43
+ generateLayoutSelectorJS(),
44
+ '',
45
+ generateFieldRenderersJS(),
46
+ '',
47
+ generateComponentsJS(),
48
+ '',
49
+ generateTemplateEngineJS(),
50
+ ].join('\n');
51
+ }
52
+
53
+ /**
54
+ * Generate all CSS for embedding in HTML
55
+ */
56
+ export function generateSmartRenderingCSS(): string {
57
+ return [
58
+ generateDesignSystemCSS(),
59
+ generateComponentCSS(),
60
+ generateFieldRendererCSS(),
61
+ generateTemplateEngineCSS(),
62
+ ].join('\n');
63
+ }
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Layout Selector - Auto-detect best layout for data
3
+ *
4
+ * Determines the optimal component/layout to render data based on:
5
+ * 1. Explicit @format annotation (highest priority)
6
+ * 2. Data shape (array vs object vs primitive)
7
+ * 3. Data content (has images -> grid, etc.)
8
+ */
9
+
10
+ export type LayoutType =
11
+ | 'text' // Simple text/markdown display
12
+ | 'card' // Single object as card with header + fields
13
+ | 'list' // Array of objects as iOS-style list
14
+ | 'grid' // Array of objects as visual grid
15
+ | 'tree' // Nested object as collapsible tree
16
+ | 'kv' // Flat object as key-value table
17
+ | 'chips' // Array of strings as chips/tags
18
+ | 'table' // Legacy: grid table (backward compat)
19
+ | 'markdown' // Legacy: markdown rendering
20
+ | 'mermaid' // Legacy: mermaid diagrams
21
+ | 'code' // Code block with syntax highlighting
22
+ | 'json' // Raw JSON display
23
+ | 'html'; // Raw HTML (for custom UIs)
24
+
25
+ export interface LayoutHints {
26
+ title?: string; // Field to use as title
27
+ subtitle?: string; // Field to use as subtitle
28
+ icon?: string; // Field to use as icon
29
+ badge?: string; // Field to use as badge
30
+ detail?: string; // Field to use as detail
31
+ image?: string; // Field to use as image (grid)
32
+ style?: string; // List style (plain, grouped, inset, etc.)
33
+ accessory?: string; // Accessory type (chevron, switch, etc.)
34
+ columns?: number; // Grid columns
35
+ fields?: string[]; // Specific fields to show
36
+ }
37
+
38
+ // Map legacy @format values to new layout types
39
+ const FORMAT_TO_LAYOUT: Record<string, LayoutType> = {
40
+ 'table': 'list', // table -> list (smart rendering)
41
+ 'list': 'list',
42
+ 'grid': 'grid',
43
+ 'card': 'card',
44
+ 'kv': 'kv',
45
+ 'tree': 'tree',
46
+ 'json': 'json',
47
+ 'markdown': 'markdown',
48
+ 'mermaid': 'mermaid',
49
+ 'code': 'code',
50
+ 'text': 'text',
51
+ 'primitive': 'text',
52
+ 'chips': 'chips',
53
+ 'html': 'html',
54
+ };
55
+
56
+ /**
57
+ * Select the best layout type for given data
58
+ */
59
+ export function selectLayout(
60
+ data: any,
61
+ format?: string,
62
+ hints?: LayoutHints
63
+ ): LayoutType {
64
+ // 1. Explicit format takes precedence (backward compat)
65
+ if (format) {
66
+ // Handle code:language format
67
+ if (format.startsWith('code:')) return 'code';
68
+
69
+ const layout = FORMAT_TO_LAYOUT[format] || 'json';
70
+
71
+ // Smart fallback: if list/table format but data is not an array, use card
72
+ if ((layout === 'list' || format === 'table') && !Array.isArray(data) && typeof data === 'object' && data !== null) {
73
+ return 'card';
74
+ }
75
+
76
+ return layout;
77
+ }
78
+
79
+ // 2. Null/undefined
80
+ if (data === null || data === undefined) {
81
+ return 'text';
82
+ }
83
+
84
+ // 3. Primitives
85
+ if (typeof data === 'string') {
86
+ // Check if it looks like markdown
87
+ if (hasMarkdownSyntax(data)) return 'markdown';
88
+ // Check if it looks like mermaid
89
+ if (data.trim().startsWith('graph ') || data.trim().startsWith('flowchart ')) {
90
+ return 'mermaid';
91
+ }
92
+ return 'text';
93
+ }
94
+
95
+ if (typeof data === 'number' || typeof data === 'boolean') {
96
+ return 'text';
97
+ }
98
+
99
+ // 4. Arrays
100
+ if (Array.isArray(data)) {
101
+ if (data.length === 0) return 'text'; // Empty array
102
+
103
+ const first = data[0];
104
+
105
+ // Array of strings -> chips
106
+ if (typeof first === 'string') {
107
+ return 'chips';
108
+ }
109
+
110
+ // Array of objects
111
+ if (typeof first === 'object' && first !== null) {
112
+ // Check if items have image fields -> grid
113
+ if (hasImageFields(first)) {
114
+ return 'grid';
115
+ }
116
+ // Default: list
117
+ return 'list';
118
+ }
119
+
120
+ // Mixed or primitive arrays
121
+ return 'chips';
122
+ }
123
+
124
+ // 5. Objects
125
+ if (typeof data === 'object') {
126
+ // Check for special fields
127
+ if ('diagram' in data && typeof data.diagram === 'string') {
128
+ return 'mermaid';
129
+ }
130
+
131
+ // Check if deeply nested -> tree
132
+ if (isNested(data)) {
133
+ return 'tree';
134
+ }
135
+
136
+ // Flat object -> card (or kv for many fields)
137
+ const fieldCount = Object.keys(data).length;
138
+ if (fieldCount > 10) {
139
+ return 'kv'; // Too many fields for card
140
+ }
141
+
142
+ return 'card';
143
+ }
144
+
145
+ // Fallback
146
+ return 'json';
147
+ }
148
+
149
+ /**
150
+ * Check if object has fields that look like actual images (not just icon characters)
151
+ * We need to verify the VALUE looks like an image URL, not just the field name
152
+ */
153
+ export function hasImageFields(obj: object): boolean {
154
+ const imageFieldNames = /^(image|photo|thumbnail|picture|poster|cover)$/i;
155
+ const avatarFieldName = /^(avatar)$/i;
156
+ const imageUrlPattern = /\.(jpg|jpeg|png|gif|webp|svg)(\?.*)?$/i;
157
+ const dataUrlPattern = /^data:image\//i;
158
+
159
+ for (const [key, value] of Object.entries(obj)) {
160
+ // Skip non-string values
161
+ if (typeof value !== 'string') continue;
162
+
163
+ // Check for image URL patterns in value
164
+ if (imageUrlPattern.test(value) || dataUrlPattern.test(value)) return true;
165
+
166
+ // For image field names (not avatar), check if value looks like a URL
167
+ if (imageFieldNames.test(key) && (value.startsWith('http') || value.startsWith('/'))) {
168
+ return true;
169
+ }
170
+
171
+ // For avatar fields specifically, only treat as image if it's actually a URL
172
+ // Single characters or short strings are icons, not images
173
+ if (avatarFieldName.test(key) && value.length > 10 && (value.startsWith('http') || value.startsWith('/'))) {
174
+ return true;
175
+ }
176
+ }
177
+
178
+ return false;
179
+ }
180
+
181
+ /**
182
+ * Check if object is nested (has object/array values)
183
+ */
184
+ export function isNested(obj: object, depth: number = 0): boolean {
185
+ if (depth > 2) return true; // Max depth check
186
+
187
+ for (const value of Object.values(obj)) {
188
+ if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'object') {
189
+ return true;
190
+ }
191
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
192
+ if (isNested(value, depth + 1)) return true;
193
+ }
194
+ }
195
+
196
+ return false;
197
+ }
198
+
199
+ /**
200
+ * Check if string contains markdown syntax
201
+ */
202
+ export function hasMarkdownSyntax(text: string): boolean {
203
+ // Check for common markdown patterns
204
+ const patterns = [
205
+ /^#{1,6}\s/m, // Headers
206
+ /\*\*[^*]+\*\*/, // Bold
207
+ /\*[^*]+\*/, // Italic
208
+ /\[[^\]]+\]\([^)]+\)/, // Links
209
+ /```[\s\S]*```/, // Code blocks
210
+ /^\s*[-*+]\s/m, // Lists
211
+ /^\s*\d+\.\s/m, // Numbered lists
212
+ ];
213
+
214
+ return patterns.some(p => p.test(text));
215
+ }
216
+
217
+ /**
218
+ * Parse layout hints from nested JSDoc syntax
219
+ * Example: {@title name, @subtitle email, @style inset}
220
+ */
221
+ export function parseLayoutHints(hintsString: string): LayoutHints {
222
+ const hints: LayoutHints = {};
223
+
224
+ if (!hintsString) return hints;
225
+
226
+ // Split by comma and parse each hint
227
+ const parts = hintsString.split(',').map(s => s.trim());
228
+
229
+ for (const part of parts) {
230
+ // Match @key value or @key value:renderer
231
+ const match = part.match(/@(\w+)\s+([^:]+)(?::(\w+))?/);
232
+ if (match) {
233
+ const [, key, value, renderer] = match;
234
+ const cleanValue = value.trim();
235
+
236
+ switch (key) {
237
+ case 'title': hints.title = cleanValue; break;
238
+ case 'subtitle': hints.subtitle = cleanValue; break;
239
+ case 'icon': hints.icon = cleanValue; break;
240
+ case 'badge': hints.badge = cleanValue; break;
241
+ case 'detail': hints.detail = cleanValue; break;
242
+ case 'image': hints.image = cleanValue; break;
243
+ case 'style': hints.style = cleanValue; break;
244
+ case 'accessory': hints.accessory = cleanValue; break;
245
+ case 'columns':
246
+ hints.columns = parseInt(cleanValue, 10);
247
+ break;
248
+ case 'fields':
249
+ hints.fields = cleanValue.split(/\s+/);
250
+ break;
251
+ }
252
+ }
253
+ }
254
+
255
+ return hints;
256
+ }
257
+
258
+ /**
259
+ * Generate JavaScript code for layout selector (to embed in HTML)
260
+ */
261
+ export function generateLayoutSelectorJS(): string {
262
+ return `
263
+ // Layout Selector - Auto-detect best layout for data
264
+ const FORMAT_TO_LAYOUT = {
265
+ 'table': 'list',
266
+ 'list': 'list',
267
+ 'grid': 'grid',
268
+ 'card': 'card',
269
+ 'kv': 'kv',
270
+ 'tree': 'tree',
271
+ 'json': 'json',
272
+ 'markdown': 'markdown',
273
+ 'mermaid': 'mermaid',
274
+ 'code': 'code',
275
+ 'text': 'text',
276
+ 'primitive': 'text',
277
+ 'chips': 'chips',
278
+ 'html': 'html',
279
+ };
280
+
281
+ function selectLayout(data, format, hints) {
282
+ if (format) {
283
+ if (format.startsWith('code:')) return 'code';
284
+ var layout = FORMAT_TO_LAYOUT[format] || 'json';
285
+ // Smart fallback: if list/table format but data is not an array, use card
286
+ if ((layout === 'list' || format === 'table') && !Array.isArray(data) && typeof data === 'object' && data !== null) {
287
+ return 'card';
288
+ }
289
+ return layout;
290
+ }
291
+
292
+ if (data === null || data === undefined) return 'text';
293
+
294
+ if (typeof data === 'string') {
295
+ if (hasMarkdownSyntax(data)) return 'markdown';
296
+ if (data.trim().startsWith('graph ') || data.trim().startsWith('flowchart ')) {
297
+ return 'mermaid';
298
+ }
299
+ return 'text';
300
+ }
301
+
302
+ if (typeof data === 'number' || typeof data === 'boolean') return 'text';
303
+
304
+ if (Array.isArray(data)) {
305
+ if (data.length === 0) return 'text';
306
+ const first = data[0];
307
+ if (typeof first === 'string') return 'chips';
308
+ if (typeof first === 'object' && first !== null) {
309
+ if (hasImageFields(first)) return 'grid';
310
+ return 'list';
311
+ }
312
+ return 'chips';
313
+ }
314
+
315
+ if (typeof data === 'object') {
316
+ if ('diagram' in data && typeof data.diagram === 'string') return 'mermaid';
317
+ if (isNested(data)) return 'tree';
318
+ const fieldCount = Object.keys(data).length;
319
+ if (fieldCount > 10) return 'kv';
320
+ return 'card';
321
+ }
322
+
323
+ return 'json';
324
+ }
325
+
326
+ function hasImageFields(obj) {
327
+ const imageFieldNames = /^(image|photo|thumbnail|picture|poster|cover)$/i;
328
+ const avatarFieldName = /^(avatar)$/i;
329
+ const imageUrlPattern = /\\.(jpg|jpeg|png|gif|webp|svg)(\\?.*)?$/i;
330
+ const dataUrlPattern = /^data:image\\//i;
331
+ for (const [key, value] of Object.entries(obj)) {
332
+ if (typeof value !== 'string') continue;
333
+ if (imageUrlPattern.test(value) || dataUrlPattern.test(value)) return true;
334
+ if (imageFieldNames.test(key) && (value.startsWith('http') || value.startsWith('/'))) return true;
335
+ if (avatarFieldName.test(key) && value.length > 10 && (value.startsWith('http') || value.startsWith('/'))) return true;
336
+ }
337
+ return false;
338
+ }
339
+
340
+ function isNested(obj, depth = 0) {
341
+ if (depth > 2) return true;
342
+ for (const value of Object.values(obj)) {
343
+ if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'object') return true;
344
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
345
+ if (isNested(value, depth + 1)) return true;
346
+ }
347
+ }
348
+ return false;
349
+ }
350
+
351
+ function hasMarkdownSyntax(text) {
352
+ const patterns = [
353
+ /^#{1,6}\\s/m,
354
+ /\\*\\*[^*]+\\*\\*/,
355
+ /\\*[^*]+\\*/,
356
+ /\\[[^\\]]+\\]\\([^)]+\\)/,
357
+ /\`\`\`[\\s\\S]*\`\`\`/,
358
+ /^\\s*[-*+]\\s/m,
359
+ /^\\s*\\d+\\.\\s/m,
360
+ ];
361
+ return patterns.some(p => p.test(text));
362
+ }
363
+
364
+ function parseLayoutHints(hintsString) {
365
+ const hints = {};
366
+ if (!hintsString) return hints;
367
+ const parts = hintsString.split(',').map(s => s.trim());
368
+ for (const part of parts) {
369
+ const match = part.match(/@(\\w+)\\s+([^:]+)(?::(\\w+))?/);
370
+ if (match) {
371
+ const [, key, value, renderer] = match;
372
+ const cleanValue = value.trim();
373
+ switch (key) {
374
+ case 'title': hints.title = cleanValue; break;
375
+ case 'subtitle': hints.subtitle = cleanValue; break;
376
+ case 'icon': hints.icon = cleanValue; break;
377
+ case 'badge': hints.badge = cleanValue; break;
378
+ case 'detail': hints.detail = cleanValue; break;
379
+ case 'image': hints.image = cleanValue; break;
380
+ case 'style': hints.style = cleanValue; break;
381
+ case 'accessory': hints.accessory = cleanValue; break;
382
+ case 'columns': hints.columns = parseInt(cleanValue, 10); break;
383
+ case 'fields': hints.fields = cleanValue.split(/\\s+/); break;
384
+ }
385
+ }
386
+ }
387
+ return hints;
388
+ }
389
+ `;
390
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Template Engine - Custom HTML Template Support
3
+ *
4
+ * Enables custom UI templates that map elements to class methods.
5
+ * Perfect for specialized interfaces like:
6
+ * - TV remote control
7
+ * - Numeric keypad
8
+ * - Dashboard with gauges
9
+ * - Media player controls
10
+ *
11
+ * Template Binding Attributes:
12
+ * - data-method: Method to call on click
13
+ * - data-args: JSON arguments to pass
14
+ * - data-result: Container for method output
15
+ * - data-bind: Live data binding
16
+ * - data-if: Conditional visibility
17
+ */
18
+
19
+ export interface TemplateBinding {
20
+ element: string; // CSS selector
21
+ method: string; // Method name
22
+ args?: any; // Arguments to pass
23
+ event?: string; // Event to listen for (default: 'click')
24
+ }
25
+
26
+ export interface TemplateConfig {
27
+ id: string; // Template identifier
28
+ path: string; // Path to template file
29
+ bindings?: TemplateBinding[]; // Pre-defined bindings (optional)
30
+ }
31
+
32
+ /**
33
+ * Parse template HTML and extract data-method bindings
34
+ */
35
+ export function parseTemplateBindings(html: string): TemplateBinding[] {
36
+ const bindings: TemplateBinding[] = [];
37
+
38
+ // Match elements with data-method attribute
39
+ const methodRegex = /data-method=["']([^"']+)["']/g;
40
+ const argsRegex = /data-args=["']([^"']+)["']/g;
41
+
42
+ // Note: This is a simplified parser - actual implementation
43
+ // would use DOM parsing in the browser
44
+
45
+ return bindings;
46
+ }
47
+
48
+ /**
49
+ * Generate JavaScript for template engine (to embed in HTML)
50
+ */
51
+ export function generateTemplateEngineJS(): string {
52
+ return `
53
+ // ==========================================================================
54
+ // Template Engine - Custom UI Template Support
55
+ // ==========================================================================
56
+
57
+ /**
58
+ * Initialize template bindings for custom UI
59
+ * Finds all elements with data-method and binds click handlers
60
+ */
61
+ function initTemplateBindings(container, invokeMethod) {
62
+ // Find all elements with data-method attribute
63
+ const methodElements = container.querySelectorAll('[data-method]');
64
+
65
+ methodElements.forEach(el => {
66
+ const methodName = el.getAttribute('data-method');
67
+ const argsStr = el.getAttribute('data-args');
68
+ const eventType = el.getAttribute('data-event') || 'click';
69
+
70
+ // Parse args if present
71
+ let args = {};
72
+ if (argsStr) {
73
+ try {
74
+ args = JSON.parse(argsStr);
75
+ } catch (e) {
76
+ console.warn('Invalid data-args JSON:', argsStr);
77
+ }
78
+ }
79
+
80
+ // Add event listener
81
+ el.addEventListener(eventType, async (e) => {
82
+ e.preventDefault();
83
+ e.stopPropagation();
84
+
85
+ // Add loading state
86
+ el.classList.add('template-loading');
87
+
88
+ try {
89
+ // Invoke the method
90
+ const result = await invokeMethod(methodName, args);
91
+
92
+ // Find result container and update
93
+ const resultContainer = container.querySelector('[data-result]');
94
+ if (resultContainer && result !== undefined) {
95
+ // Use smart rendering for the result
96
+ const rendered = renderSmartResult(result, null, null);
97
+ if (rendered) {
98
+ resultContainer.innerHTML = rendered;
99
+ } else {
100
+ resultContainer.textContent = JSON.stringify(result, null, 2);
101
+ }
102
+ }
103
+ } catch (error) {
104
+ console.error('Template method error:', error);
105
+ const resultContainer = container.querySelector('[data-result]');
106
+ if (resultContainer) {
107
+ resultContainer.innerHTML = '<div class="template-error">' + escapeHtml(error.message) + '</div>';
108
+ }
109
+ } finally {
110
+ el.classList.remove('template-loading');
111
+ }
112
+ });
113
+ });
114
+
115
+ // Handle data-bind for live updates
116
+ const bindElements = container.querySelectorAll('[data-bind]');
117
+ bindElements.forEach(el => {
118
+ const bindKey = el.getAttribute('data-bind');
119
+ // Store reference for later updates
120
+ el._bindKey = bindKey;
121
+ });
122
+
123
+ // Handle data-if for conditional visibility
124
+ const ifElements = container.querySelectorAll('[data-if]');
125
+ ifElements.forEach(el => {
126
+ const condition = el.getAttribute('data-if');
127
+ el._ifCondition = condition;
128
+ // Initially hidden until condition is evaluated
129
+ el.style.display = 'none';
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Update bound elements with new data
135
+ */
136
+ function updateTemplateBindings(container, data) {
137
+ // Update data-bind elements
138
+ const bindElements = container.querySelectorAll('[data-bind]');
139
+ bindElements.forEach(el => {
140
+ const bindKey = el._bindKey || el.getAttribute('data-bind');
141
+ if (bindKey && data[bindKey] !== undefined) {
142
+ if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
143
+ el.value = data[bindKey];
144
+ } else {
145
+ el.textContent = data[bindKey];
146
+ }
147
+ }
148
+ });
149
+
150
+ // Update data-if elements
151
+ const ifElements = container.querySelectorAll('[data-if]');
152
+ ifElements.forEach(el => {
153
+ const condition = el._ifCondition || el.getAttribute('data-if');
154
+ if (condition) {
155
+ // Evaluate condition against data
156
+ const value = data[condition];
157
+ el.style.display = value ? '' : 'none';
158
+ }
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Load and render a custom template
164
+ */
165
+ async function loadTemplate(templatePath, container, invokeMethod) {
166
+ try {
167
+ // Fetch template content
168
+ const response = await fetch('/api/template?path=' + encodeURIComponent(templatePath));
169
+ if (!response.ok) {
170
+ throw new Error('Failed to load template: ' + response.statusText);
171
+ }
172
+
173
+ const html = await response.text();
174
+ container.innerHTML = html;
175
+
176
+ // Initialize bindings
177
+ initTemplateBindings(container, invokeMethod);
178
+
179
+ return true;
180
+ } catch (error) {
181
+ console.error('Template load error:', error);
182
+ container.innerHTML = '<div class="template-error">Failed to load template: ' + escapeHtml(error.message) + '</div>';
183
+ return false;
184
+ }
185
+ }
186
+ `;
187
+ }
188
+
189
+ /**
190
+ * Generate CSS for template engine
191
+ */
192
+ export function generateTemplateEngineCSS(): string {
193
+ return `
194
+ /* Template Engine Styles */
195
+ .template-loading {
196
+ opacity: 0.6;
197
+ pointer-events: none;
198
+ position: relative;
199
+ }
200
+
201
+ .template-loading::after {
202
+ content: '';
203
+ position: absolute;
204
+ top: 50%;
205
+ left: 50%;
206
+ width: 16px;
207
+ height: 16px;
208
+ margin: -8px 0 0 -8px;
209
+ border: 2px solid var(--accent);
210
+ border-top-color: transparent;
211
+ border-radius: 50%;
212
+ animation: template-spin 0.8s linear infinite;
213
+ }
214
+
215
+ @keyframes template-spin {
216
+ to { transform: rotate(360deg); }
217
+ }
218
+
219
+ .template-error {
220
+ color: var(--error);
221
+ padding: 12px;
222
+ background: rgba(239, 68, 68, 0.1);
223
+ border-radius: 8px;
224
+ font-size: 0.875rem;
225
+ }
226
+
227
+ /* Template button styles */
228
+ [data-method] {
229
+ cursor: pointer;
230
+ transition: all 0.15s ease;
231
+ }
232
+
233
+ [data-method]:hover {
234
+ filter: brightness(1.1);
235
+ }
236
+
237
+ [data-method]:active {
238
+ transform: scale(0.98);
239
+ }
240
+
241
+ /* Result container */
242
+ [data-result] {
243
+ min-height: 40px;
244
+ padding: 8px;
245
+ border-radius: 8px;
246
+ background: var(--bg-tertiary);
247
+ }
248
+
249
+ [data-result]:empty::before {
250
+ content: '—';
251
+ color: var(--text-muted);
252
+ }
253
+ `;
254
+ }