@adecore/intelligent-ui 0.25.1 → 0.25.3
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/catalog.d.ts +18 -1
- package/dist/catalog.js +36 -18
- package/dist/compiler.js +5 -0
- package/dist/event-time.d.ts +1 -0
- package/dist/event-time.js +14 -8
- package/dist/event-warning.d.ts +2 -0
- package/dist/event-warning.js +10 -0
- package/dist/prop-list.d.ts +8 -0
- package/dist/prop-list.js +8 -0
- package/dist/protocol.d.ts +1 -0
- package/dist/protocol.js +2 -1
- package/dist/runtime.js +9 -0
- package/dist/syntax.d.ts +1 -0
- package/dist/syntax.js +4 -1
- package/dist/text.js +4 -4
- package/package.json +1 -1
- package/src/catalog.ts +37 -18
- package/src/compiler.ts +5 -0
- package/src/event-time.ts +17 -8
- package/src/event-warning.ts +14 -0
- package/src/prop-list.ts +18 -0
- package/src/protocol.ts +2 -1
- package/src/runtime.ts +11 -0
- package/src/syntax.ts +7 -1
- package/src/text.ts +4 -4
package/dist/catalog.d.ts
CHANGED
|
@@ -91,6 +91,7 @@ export declare const UI_CATALOG: {
|
|
|
91
91
|
value: z.ZodOptional<z.ZodNumber>;
|
|
92
92
|
max: z.ZodOptional<z.ZodNumber>;
|
|
93
93
|
}, z.core.$strict>;
|
|
94
|
+
readonly text: 'label';
|
|
94
95
|
};
|
|
95
96
|
readonly Steps: {
|
|
96
97
|
readonly group: 'status';
|
|
@@ -112,6 +113,7 @@ export declare const UI_CATALOG: {
|
|
|
112
113
|
detail: z.ZodOptional<z.ZodString>;
|
|
113
114
|
}, z.core.$strict>;
|
|
114
115
|
readonly parent: 'Steps';
|
|
116
|
+
readonly text: 'label';
|
|
115
117
|
};
|
|
116
118
|
readonly Timeline: {
|
|
117
119
|
readonly group: 'status';
|
|
@@ -124,7 +126,7 @@ export declare const UI_CATALOG: {
|
|
|
124
126
|
};
|
|
125
127
|
readonly Event: {
|
|
126
128
|
readonly group: 'status';
|
|
127
|
-
readonly description: 'One event at a time or date; children are the title.';
|
|
129
|
+
readonly description: 'One event at a time or date; children are the title. at is HH:MM[:SS], YYYY-MM-DD, or YYYY-MM-DDTHH:MM[:SS] with an optional Z or ±HH:MM.';
|
|
128
130
|
readonly schema: z.ZodObject<{
|
|
129
131
|
at: z.ZodString;
|
|
130
132
|
tone: z.ZodOptional<z.ZodEnum<{
|
|
@@ -137,6 +139,7 @@ export declare const UI_CATALOG: {
|
|
|
137
139
|
detail: z.ZodOptional<z.ZodString>;
|
|
138
140
|
}, z.core.$strict>;
|
|
139
141
|
readonly parent: 'Timeline';
|
|
142
|
+
readonly text: 'title';
|
|
140
143
|
};
|
|
141
144
|
readonly Stats: {
|
|
142
145
|
readonly group: 'data';
|
|
@@ -175,6 +178,7 @@ export declare const UI_CATALOG: {
|
|
|
175
178
|
label: z.ZodString;
|
|
176
179
|
}, z.core.$strict>;
|
|
177
180
|
readonly parent: 'EntityList';
|
|
181
|
+
readonly text: 'value';
|
|
178
182
|
};
|
|
179
183
|
readonly Table: {
|
|
180
184
|
readonly group: 'data';
|
|
@@ -260,6 +264,8 @@ export declare const UI_CATALOG: {
|
|
|
260
264
|
generated: z.ZodOptional<z.ZodLiteral<"latest">>;
|
|
261
265
|
alt: z.ZodOptional<z.ZodString>;
|
|
262
266
|
}, z.core.$strict>;
|
|
267
|
+
readonly needs: 'attachment|generated';
|
|
268
|
+
readonly text: 'caption';
|
|
263
269
|
};
|
|
264
270
|
readonly Gallery: {
|
|
265
271
|
readonly group: 'content';
|
|
@@ -280,6 +286,7 @@ export declare const UI_CATALOG: {
|
|
|
280
286
|
beforeLabel: z.ZodOptional<z.ZodString>;
|
|
281
287
|
afterLabel: z.ZodOptional<z.ZodString>;
|
|
282
288
|
}, z.core.$strict>;
|
|
289
|
+
readonly text: 'caption';
|
|
283
290
|
};
|
|
284
291
|
readonly Map: {
|
|
285
292
|
readonly group: 'places';
|
|
@@ -305,6 +312,8 @@ export declare const UI_CATALOG: {
|
|
|
305
312
|
value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>;
|
|
306
313
|
}, z.core.$strict>;
|
|
307
314
|
readonly parent: 'Map';
|
|
315
|
+
readonly needs: 'lat lon|address';
|
|
316
|
+
readonly text: 'name';
|
|
308
317
|
};
|
|
309
318
|
readonly Sources: {
|
|
310
319
|
readonly group: 'content';
|
|
@@ -366,6 +375,7 @@ export declare const UI_CATALOG: {
|
|
|
366
375
|
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>;
|
|
367
376
|
}, z.core.$strict>;
|
|
368
377
|
readonly parent: 'Checklist';
|
|
378
|
+
readonly text: 'label';
|
|
369
379
|
};
|
|
370
380
|
readonly Switch: {
|
|
371
381
|
readonly group: 'inputs';
|
|
@@ -374,6 +384,7 @@ export declare const UI_CATALOG: {
|
|
|
374
384
|
value: z.ZodBoolean;
|
|
375
385
|
}, z.core.$strict>;
|
|
376
386
|
readonly binding: 'value';
|
|
387
|
+
readonly text: 'label';
|
|
377
388
|
};
|
|
378
389
|
readonly Slider: {
|
|
379
390
|
readonly group: 'inputs';
|
|
@@ -386,6 +397,7 @@ export declare const UI_CATALOG: {
|
|
|
386
397
|
unit: z.ZodOptional<z.ZodString>;
|
|
387
398
|
}, z.core.$strict>;
|
|
388
399
|
readonly binding: 'value';
|
|
400
|
+
readonly text: 'label';
|
|
389
401
|
};
|
|
390
402
|
readonly Segmented: {
|
|
391
403
|
readonly group: 'inputs';
|
|
@@ -404,6 +416,7 @@ export declare const UI_CATALOG: {
|
|
|
404
416
|
}, z.core.$strict>;
|
|
405
417
|
readonly children: readonly ['Option'];
|
|
406
418
|
readonly binding: 'value';
|
|
419
|
+
readonly text: 'label';
|
|
407
420
|
};
|
|
408
421
|
readonly Option: {
|
|
409
422
|
readonly group: 'inputs';
|
|
@@ -412,6 +425,7 @@ export declare const UI_CATALOG: {
|
|
|
412
425
|
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>;
|
|
413
426
|
}, z.core.$strict>;
|
|
414
427
|
readonly parent: readonly ['Segmented', 'Select'];
|
|
428
|
+
readonly text: 'label';
|
|
415
429
|
};
|
|
416
430
|
readonly TextField: {
|
|
417
431
|
readonly group: 'inputs';
|
|
@@ -422,6 +436,7 @@ export declare const UI_CATALOG: {
|
|
|
422
436
|
placeholder: z.ZodOptional<z.ZodString>;
|
|
423
437
|
}, z.core.$strict>;
|
|
424
438
|
readonly binding: 'value';
|
|
439
|
+
readonly text: 'label';
|
|
425
440
|
};
|
|
426
441
|
readonly Button: {
|
|
427
442
|
readonly group: 'inputs';
|
|
@@ -430,6 +445,7 @@ export declare const UI_CATALOG: {
|
|
|
430
445
|
disabled: z.ZodOptional<z.ZodBoolean>;
|
|
431
446
|
}, z.core.$strict>;
|
|
432
447
|
readonly action: 'action';
|
|
448
|
+
readonly text: 'label';
|
|
433
449
|
};
|
|
434
450
|
readonly Show: {
|
|
435
451
|
readonly group: 'inputs';
|
|
@@ -461,6 +477,7 @@ export declare const UI_CATALOG: {
|
|
|
461
477
|
disabled: z.ZodOptional<z.ZodBoolean>;
|
|
462
478
|
}, z.core.$strict>;
|
|
463
479
|
readonly parent: 'Choices';
|
|
480
|
+
readonly text: 'label';
|
|
464
481
|
};
|
|
465
482
|
};
|
|
466
483
|
export type UiGroupName = keyof typeof UI_GROUPS;
|
package/dist/catalog.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { parseUiEventTime } from './event-time.js';
|
|
3
2
|
import { uiPropList } from './prop-list.js';
|
|
4
3
|
export const UI_CATALOG_VERSION = 1;
|
|
5
4
|
export const UiToneSchema = z.enum(['neutral', 'info', 'success', 'warning', 'danger']);
|
|
@@ -39,25 +38,28 @@ export const UI_CATALOG = {
|
|
|
39
38
|
Progress: {
|
|
40
39
|
group: 'status',
|
|
41
40
|
description: 'Progress with a label; omit value for indeterminate progress.',
|
|
42
|
-
schema: z.strictObject({ value: number.nonnegative().optional(), max: number.positive().optional() })
|
|
41
|
+
schema: z.strictObject({ value: number.nonnegative().optional(), max: number.positive().optional() }),
|
|
42
|
+
text: 'label'
|
|
43
43
|
},
|
|
44
44
|
Steps: { group: 'status', description: 'An ordered sequence of steps.', schema: empty, children: ['Step'] },
|
|
45
45
|
Step: {
|
|
46
46
|
group: 'status',
|
|
47
47
|
description: 'One step in a sequence.',
|
|
48
48
|
schema: z.strictObject({ state: z.enum(['done', 'running', 'pending', 'failed', 'skipped']), detail: text.optional() }),
|
|
49
|
-
parent: 'Steps'
|
|
49
|
+
parent: 'Steps',
|
|
50
|
+
text: 'label'
|
|
50
51
|
},
|
|
51
52
|
Timeline: { group: 'status', description: 'Events in time order.', schema: empty, children: ['Event'], count: { min: 1 } },
|
|
52
53
|
Event: {
|
|
53
54
|
group: 'status',
|
|
54
|
-
description: 'One event at a time or date; children are the title.',
|
|
55
|
+
description: 'One event at a time or date; children are the title. at is HH:MM[:SS], YYYY-MM-DD, or YYYY-MM-DDTHH:MM[:SS] with an optional Z or ±HH:MM.',
|
|
55
56
|
schema: z.strictObject({
|
|
56
|
-
at: text
|
|
57
|
+
at: text,
|
|
57
58
|
tone,
|
|
58
59
|
detail: text.optional()
|
|
59
60
|
}),
|
|
60
|
-
parent: 'Timeline'
|
|
61
|
+
parent: 'Timeline',
|
|
62
|
+
text: 'title'
|
|
61
63
|
},
|
|
62
64
|
Stats: { group: 'data', description: 'A small set of key numbers.', schema: empty, children: ['Stat'] },
|
|
63
65
|
Stat: {
|
|
@@ -67,7 +69,7 @@ export const UI_CATALOG = {
|
|
|
67
69
|
parent: 'Stats'
|
|
68
70
|
},
|
|
69
71
|
EntityList: { group: 'data', description: 'Labeled facts about an entity.', schema: empty, children: ['Entry'] },
|
|
70
|
-
Entry: { group: 'data', description: 'One labeled fact.', schema: z.strictObject({ label: text }), parent: 'EntityList' },
|
|
72
|
+
Entry: { group: 'data', description: 'One labeled fact.', schema: z.strictObject({ label: text }), parent: 'EntityList', text: 'value' },
|
|
71
73
|
Table: { group: 'data', description: 'Rows with named columns.', schema: z.strictObject({ rows }), children: ['Column'] },
|
|
72
74
|
Column: {
|
|
73
75
|
group: 'data',
|
|
@@ -99,13 +101,16 @@ export const UI_CATALOG = {
|
|
|
99
101
|
description: 'A chat attachment with an optional caption.',
|
|
100
102
|
schema: z
|
|
101
103
|
.strictObject({ attachment: name.optional(), generated: z.literal('latest').optional(), alt: text.optional() })
|
|
102
|
-
.refine((props) => Boolean(props.attachment) !== Boolean(props.generated), 'Choose attachment or generated, exclusively.')
|
|
104
|
+
.refine((props) => Boolean(props.attachment) !== Boolean(props.generated), 'Choose attachment or generated, exclusively.'),
|
|
105
|
+
needs: 'attachment|generated',
|
|
106
|
+
text: 'caption'
|
|
103
107
|
},
|
|
104
108
|
Gallery: { group: 'content', description: 'A grid of images with captions; at most 24.', schema: empty, children: ['Image'], count: { min: 1, max: 24 } },
|
|
105
109
|
Compare: {
|
|
106
110
|
group: 'content',
|
|
107
111
|
description: 'Two chat attachments in one frame with a handle to wipe between them; children are the caption.',
|
|
108
|
-
schema: z.strictObject({ before: name, after: name, beforeLabel: text.optional(), afterLabel: text.optional() })
|
|
112
|
+
schema: z.strictObject({ before: name, after: name, beforeLabel: text.optional(), afterLabel: text.optional() }),
|
|
113
|
+
text: 'caption'
|
|
109
114
|
},
|
|
110
115
|
Map: {
|
|
111
116
|
group: 'places',
|
|
@@ -127,7 +132,9 @@ export const UI_CATALOG = {
|
|
|
127
132
|
value: scalar.optional()
|
|
128
133
|
})
|
|
129
134
|
.refine((props) => (props.lat === undefined) === (props.lon === undefined) && (props.lat !== undefined || props.address !== undefined), 'Give lat with lon, or an address.'),
|
|
130
|
-
parent: 'Map'
|
|
135
|
+
parent: 'Map',
|
|
136
|
+
needs: 'lat lon|address',
|
|
137
|
+
text: 'name'
|
|
131
138
|
},
|
|
132
139
|
Sources: { group: 'content', description: 'Numbered sources without preloading.', schema: empty, children: ['Source'] },
|
|
133
140
|
Source: {
|
|
@@ -151,15 +158,16 @@ export const UI_CATALOG = {
|
|
|
151
158
|
children: ['Item'],
|
|
152
159
|
binding: 'value'
|
|
153
160
|
},
|
|
154
|
-
Item: { group: 'inputs', description: 'One selectable value.', schema: z.strictObject({ value: scalar }), parent: 'Checklist' },
|
|
155
|
-
Switch: { group: 'inputs', description: 'A local boolean.', schema: z.strictObject({ value: z.boolean() }), binding: 'value' },
|
|
161
|
+
Item: { group: 'inputs', description: 'One selectable value.', schema: z.strictObject({ value: scalar }), parent: 'Checklist', text: 'label' },
|
|
162
|
+
Switch: { group: 'inputs', description: 'A local boolean.', schema: z.strictObject({ value: z.boolean() }), binding: 'value', text: 'label' },
|
|
156
163
|
Slider: {
|
|
157
164
|
group: 'inputs',
|
|
158
165
|
description: 'A local number in a range.',
|
|
159
166
|
schema: z
|
|
160
167
|
.strictObject({ value: number, min: number, max: number, step: number.positive().optional(), unit: text.optional() })
|
|
161
168
|
.refine((props) => props.min < props.max && props.value >= props.min && props.value <= props.max, 'Value must lie within an increasing range.'),
|
|
162
|
-
binding: 'value'
|
|
169
|
+
binding: 'value',
|
|
170
|
+
text: 'label'
|
|
163
171
|
},
|
|
164
172
|
Segmented: { group: 'inputs', description: 'Choose one local value.', schema: z.strictObject({ value: scalar }), children: ['Option'], binding: 'value' },
|
|
165
173
|
Select: {
|
|
@@ -167,22 +175,31 @@ export const UI_CATALOG = {
|
|
|
167
175
|
description: 'Choose one local value from a dropdown, for more or longer options than Segmented holds; the text beside its Options is its label.',
|
|
168
176
|
schema: z.strictObject({ value: scalar }),
|
|
169
177
|
children: ['Option'],
|
|
170
|
-
binding: 'value'
|
|
178
|
+
binding: 'value',
|
|
179
|
+
text: 'label'
|
|
180
|
+
},
|
|
181
|
+
Option: {
|
|
182
|
+
group: 'inputs',
|
|
183
|
+
description: 'One local alternative.',
|
|
184
|
+
schema: z.strictObject({ value: scalar }),
|
|
185
|
+
parent: ['Segmented', 'Select'],
|
|
186
|
+
text: 'label'
|
|
171
187
|
},
|
|
172
|
-
Option: { group: 'inputs', description: 'One local alternative.', schema: z.strictObject({ value: scalar }), parent: ['Segmented', 'Select'] },
|
|
173
188
|
TextField: {
|
|
174
189
|
group: 'inputs',
|
|
175
190
|
description: 'A line of local text up to maxLength characters (256 without one, at most 1024); children are the label.',
|
|
176
191
|
schema: z
|
|
177
192
|
.strictObject({ value: text, maxLength: z.number().int().min(1).max(1024).optional(), placeholder: text.max(128).optional() })
|
|
178
193
|
.refine((props) => props.value.length <= (props.maxLength ?? 256), 'Value must not be longer than maxLength.'),
|
|
179
|
-
binding: 'value'
|
|
194
|
+
binding: 'value',
|
|
195
|
+
text: 'label'
|
|
180
196
|
},
|
|
181
197
|
Button: {
|
|
182
198
|
group: 'inputs',
|
|
183
199
|
description: 'Run one local state action when pressed; children are the label.',
|
|
184
200
|
schema: z.strictObject({ disabled: z.boolean().optional() }),
|
|
185
|
-
action: 'action'
|
|
201
|
+
action: 'action',
|
|
202
|
+
text: 'label'
|
|
186
203
|
},
|
|
187
204
|
Show: { group: 'inputs', description: 'Show children when the condition is true.', schema: z.strictObject({ when: z.boolean() }) },
|
|
188
205
|
Each: {
|
|
@@ -195,7 +212,8 @@ export const UI_CATALOG = {
|
|
|
195
212
|
group: 'choices',
|
|
196
213
|
description: 'Send its label and visible context once.',
|
|
197
214
|
schema: z.strictObject({ context: text.optional(), primary: z.boolean().optional(), disabled: z.boolean().optional() }),
|
|
198
|
-
parent: 'Choices'
|
|
215
|
+
parent: 'Choices',
|
|
216
|
+
text: 'label'
|
|
199
217
|
}
|
|
200
218
|
};
|
|
201
219
|
export function isUiComponent(name) {
|
package/dist/compiler.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { safeKey, UI_LIMITS, UiBudget, UiFailure } from './budget.js';
|
|
2
2
|
import { isUiComponent, UI_CATALOG, UI_CATALOG_VERSION } from './catalog.js';
|
|
3
3
|
import { uiActionProp, uiActionTarget, uiLocalState } from './actions.js';
|
|
4
|
+
import { unreadableEventTime } from './event-warning.js';
|
|
4
5
|
import { uiNodeFallback, uiPlainText } from './fallback.js';
|
|
5
6
|
import { evaluateUiExpression, sameUiValue } from './expression.js';
|
|
6
7
|
import { parseUiSyntax, uiDiagnostic } from './syntax.js';
|
|
@@ -241,6 +242,10 @@ export function compileUiBlock(source, options) {
|
|
|
241
242
|
throw new UiFailure('invalid_props', parsed.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; '));
|
|
242
243
|
}
|
|
243
244
|
}
|
|
245
|
+
const unreadable = !unresolved && syntax.complete && syntax.type === 'Event' ? unreadableEventTime(evaluated.at) : null;
|
|
246
|
+
if (unreadable !== null) {
|
|
247
|
+
diagnose(unreadable, syntax.start, syntax.end, syntax.id);
|
|
248
|
+
}
|
|
244
249
|
// The catalog promises six series, one per chart color; more still reach the values a person can open.
|
|
245
250
|
if (!unresolved && syntax.complete && syntax.type === 'Chart' && chartSeries(evaluated.data) > CHART_SERIES) {
|
|
246
251
|
diagnose(new UiFailure('too_many_series', `Chart draws up to ${CHART_SERIES} numeric series; the rest are only in its values.`), syntax.start, syntax.end, syntax.id);
|
package/dist/event-time.d.ts
CHANGED
package/dist/event-time.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const EVENT_TIME = /^(?:(\d{2}):(\d{2})
|
|
1
|
+
const EVENT_TIME = /^(?:(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?|(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?(Z|[+-]\d{2}:\d{2})?)?)$/;
|
|
2
2
|
function validClock(hour, minute, second = 0) {
|
|
3
3
|
return hour <= 23 && minute <= 59 && second <= 59;
|
|
4
4
|
}
|
|
@@ -11,17 +11,23 @@ function zoneOffset(zone) {
|
|
|
11
11
|
const offset = (zone[0] === '-' ? -1 : 1) * (hours * 60 + minutes);
|
|
12
12
|
return minutes > 59 || offset < -12 * 60 || offset > 14 * 60 ? null : offset;
|
|
13
13
|
}
|
|
14
|
-
/* The moment an Event's `at` names, or null when it is no `HH:MM`, `YYYY-MM-DD` or ISO date-time with an optional offset. */
|
|
14
|
+
/* The moment an Event's `at` names, or null when it is no `HH:MM[:SS]`, `YYYY-MM-DD` or ISO date-time with an optional offset. */
|
|
15
15
|
export function parseUiEventTime(at) {
|
|
16
16
|
const match = EVENT_TIME.exec(at);
|
|
17
17
|
if (match === null) {
|
|
18
18
|
return null;
|
|
19
19
|
}
|
|
20
|
-
const [, clockHour, clockMinute, year, month, day, hour, minute, second, zone] = match.map((part) => part);
|
|
20
|
+
const [, clockHour, clockMinute, clockSecond, year, month, day, hour, minute, second, zone] = match.map((part) => part);
|
|
21
21
|
if (clockHour !== undefined) {
|
|
22
22
|
// The day is not shown for a time, so any day will do.
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const clockSeconds = Number(clockSecond ?? 0);
|
|
24
|
+
return validClock(Number(clockHour), Number(clockMinute), clockSeconds)
|
|
25
|
+
? {
|
|
26
|
+
kind: 'time',
|
|
27
|
+
date: new Date(Date.UTC(2000, 0, 1, Number(clockHour), Number(clockMinute), clockSeconds)),
|
|
28
|
+
written: true,
|
|
29
|
+
seconds: clockSecond !== undefined
|
|
30
|
+
}
|
|
25
31
|
: null;
|
|
26
32
|
}
|
|
27
33
|
const [y, m, d] = [Number(year), Number(month), Number(day)];
|
|
@@ -30,15 +36,15 @@ export function parseUiEventTime(at) {
|
|
|
30
36
|
return null;
|
|
31
37
|
}
|
|
32
38
|
if (hour === undefined) {
|
|
33
|
-
return { kind: 'date', date: calendar, written: true };
|
|
39
|
+
return { kind: 'date', date: calendar, written: true, seconds: false };
|
|
34
40
|
}
|
|
35
41
|
const [h, min, sec] = [Number(hour), Number(minute), Number(second ?? 0)];
|
|
36
42
|
if (!validClock(h, min, sec)) {
|
|
37
43
|
return null;
|
|
38
44
|
}
|
|
39
45
|
if (zone === undefined) {
|
|
40
|
-
return { kind: 'datetime', date: new Date(Date.UTC(y, m - 1, d, h, min, sec)), written: true };
|
|
46
|
+
return { kind: 'datetime', date: new Date(Date.UTC(y, m - 1, d, h, min, sec)), written: true, seconds: false };
|
|
41
47
|
}
|
|
42
48
|
const offset = zoneOffset(zone);
|
|
43
|
-
return offset === null ? null : { kind: 'datetime', date: new Date(Date.UTC(y, m - 1, d, h, min, sec) - offset * 60000), written: false };
|
|
49
|
+
return offset === null ? null : { kind: 'datetime', date: new Date(Date.UTC(y, m - 1, d, h, min, sec) - offset * 60000), written: false, seconds: false };
|
|
44
50
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { UiFailure } from './budget.js';
|
|
2
|
+
import { parseUiEventTime } from './event-time.js';
|
|
3
|
+
/* The warning for an Event whose `at` cannot be read, or null when it can. The Event still draws, with `at` as written. */
|
|
4
|
+
export function unreadableEventTime(at) {
|
|
5
|
+
if (typeof at !== 'string' || parseUiEventTime(at) !== null) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const shown = at.length > 40 ? `${at.slice(0, 40)}...` : at;
|
|
9
|
+
return new UiFailure('unreadable_time', `Event at="${shown}" is not HH:MM[:SS], YYYY-MM-DD or YYYY-MM-DDTHH:MM[:SS] with an optional Z or ±HH:MM, so it is shown as written.`);
|
|
10
|
+
}
|
package/dist/prop-list.d.ts
CHANGED
|
@@ -5,3 +5,11 @@ export declare function uiPropList(entry: {
|
|
|
5
5
|
};
|
|
6
6
|
readonly action?: string;
|
|
7
7
|
}): string[];
|
|
8
|
+
export declare function uiRequirement(name: string, entry: {
|
|
9
|
+
readonly schema: {
|
|
10
|
+
readonly shape: Readonly<Record<string, z.ZodType>>;
|
|
11
|
+
};
|
|
12
|
+
readonly action?: string;
|
|
13
|
+
readonly needs?: string;
|
|
14
|
+
readonly text?: string;
|
|
15
|
+
}): string;
|
package/dist/prop-list.js
CHANGED
|
@@ -3,3 +3,11 @@ export function uiPropList(entry) {
|
|
|
3
3
|
const props = Object.entries(entry.schema.shape).map(([key, schema]) => `${key}${schema.isOptional() ? '?' : ''}`);
|
|
4
4
|
return entry.action ? [entry.action, ...props] : props;
|
|
5
5
|
}
|
|
6
|
+
/* What a component needs in the compact note: the props it cannot do without and, when its text is not the plain content, what that text is. */
|
|
7
|
+
export function uiRequirement(name, entry) {
|
|
8
|
+
const required = entry.needs ??
|
|
9
|
+
uiPropList(entry)
|
|
10
|
+
.filter((prop) => !prop.endsWith('?'))
|
|
11
|
+
.join(' ');
|
|
12
|
+
return `${name}${required ? ` ${required}` : ''}${entry.text ? ` (text: ${entry.text})` : ''}`;
|
|
13
|
+
}
|
package/dist/protocol.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare const UiDiagnosticSchema: z.ZodObject<{
|
|
|
7
7
|
start: z.ZodNumber;
|
|
8
8
|
end: z.ZodNumber;
|
|
9
9
|
nodeId: z.ZodOptional<z.ZodString>;
|
|
10
|
+
severity: z.ZodOptional<z.ZodLiteral<"warning">>;
|
|
10
11
|
}, z.core.$strip>;
|
|
11
12
|
export declare const UiBlockSchema: z.ZodType<UiBlock>;
|
|
12
13
|
export declare const UiBlocksSchema: z.ZodArray<z.ZodType<UiBlock, unknown, z.core.$ZodTypeInternals<UiBlock, unknown>>>;
|
package/dist/protocol.js
CHANGED
|
@@ -20,7 +20,8 @@ export const UiDiagnosticSchema = z.object({
|
|
|
20
20
|
message: z.string(),
|
|
21
21
|
start: position,
|
|
22
22
|
end: position,
|
|
23
|
-
nodeId: z.string().optional()
|
|
23
|
+
nodeId: z.string().optional(),
|
|
24
|
+
severity: z.literal('warning').optional()
|
|
24
25
|
});
|
|
25
26
|
export const UiBlockSchema = z.object({
|
|
26
27
|
id: z.string(),
|
package/dist/runtime.js
CHANGED
|
@@ -4,6 +4,7 @@ import { isUiComponent, UI_CATALOG, UI_CATALOG_VERSION } from './catalog.js';
|
|
|
4
4
|
import {} from './compiler.js';
|
|
5
5
|
import { uiActionProp, uiActionTarget, uiLocalState } from './actions.js';
|
|
6
6
|
import { uiCompiledNodes, uiVisibleInputs } from './inputs.js';
|
|
7
|
+
import { unreadableEventTime } from './event-warning.js';
|
|
7
8
|
import { uiDiagnostic } from './syntax.js';
|
|
8
9
|
export class UiState {
|
|
9
10
|
id = '';
|
|
@@ -150,6 +151,14 @@ export function evaluateUiBlock(block, state = new UiState(block), limits = {})
|
|
|
150
151
|
throw new UiFailure('invalid_props', parsed.error.issues.map((issue) => issue.message).join('; '));
|
|
151
152
|
}
|
|
152
153
|
view.props = parsed.data;
|
|
154
|
+
const unreadable = node.complete && node.type === 'Event' ? unreadableEventTime(props.at) : null;
|
|
155
|
+
// The compiler already warned about a time it could read from the source; this one waited on an Each or a query.
|
|
156
|
+
if (unreadable !== null &&
|
|
157
|
+
![...block.diagnostics, ...result.diagnostics].some((item) => item.code === 'unreadable_time' && item.nodeId === node.id)) {
|
|
158
|
+
if (result.diagnostics.length < budget.limits.diagnostics) {
|
|
159
|
+
result.diagnostics.push(uiDiagnostic(unreadable, node.start, node.end, node.id));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
153
162
|
if (node.type === 'Show') {
|
|
154
163
|
return props.when ? children(node.children, variables, depth + 1, suffix) : [];
|
|
155
164
|
}
|
package/dist/syntax.d.ts
CHANGED
package/dist/syntax.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { safeKey, UiBudget, UiFailure } from './budget.js';
|
|
2
2
|
import { parseUiExpression } from './expression.js';
|
|
3
|
+
const WARNING_CODES = new Set(['refused_prop', 'too_many_series', 'too_many_children', 'unreadable_time']);
|
|
3
4
|
export function uiDiagnostic(error, start, end, nodeId) {
|
|
5
|
+
const code = error instanceof UiFailure ? error.code : 'invalid_syntax';
|
|
4
6
|
return {
|
|
5
|
-
code
|
|
7
|
+
code,
|
|
8
|
+
...(WARNING_CODES.has(code) ? { severity: 'warning' } : {}),
|
|
6
9
|
message: error instanceof Error ? error.message : 'Invalid UI syntax.',
|
|
7
10
|
start,
|
|
8
11
|
end,
|
package/dist/text.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { UI_CATALOG, UI_GROUPS } from './catalog.js';
|
|
2
|
-
import { uiPropList } from './prop-list.js';
|
|
2
|
+
import { uiPropList, uiRequirement } from './prop-list.js';
|
|
3
3
|
import { UI_FENCE_LANGUAGE } from './compiler.js';
|
|
4
4
|
export function uiCompactCatalog() {
|
|
5
5
|
return Object.entries(UI_CATALOG)
|
|
@@ -25,12 +25,12 @@ function uiExample(language) {
|
|
|
25
25
|
function uiGroupComponents(group) {
|
|
26
26
|
return Object.entries(UI_CATALOG).filter(([, entry]) => entry.group === group);
|
|
27
27
|
}
|
|
28
|
-
/* One line per group with
|
|
28
|
+
/* One line per group with its components and what each needs, for a host that hands the optional props out per group. */
|
|
29
29
|
function uiGroupNames(detailHint) {
|
|
30
30
|
const lines = Object.keys(UI_GROUPS).map((group) => `${group}: ${uiGroupComponents(group)
|
|
31
|
-
.map(([name]) => name)
|
|
31
|
+
.map(([name, entry]) => uiRequirement(name, entry))
|
|
32
32
|
.join(', ')}`);
|
|
33
|
-
return `Components by group${detailHint ? ` (${detailHint})` : ''}:\n${lines.join('\n')}`;
|
|
33
|
+
return `Components by group${detailHint ? ` (${detailHint})` : ''}, required props (text: words between the tags):\n${lines.join('\n')}`;
|
|
34
34
|
}
|
|
35
35
|
export function uiSessionNote({ fenceLanguage = UI_FENCE_LANGUAGE, catalog = 'props', detailHint } = {}) {
|
|
36
36
|
const components = catalog === 'names' ? uiGroupNames(detailHint) : `Catalog (? means optional props): ${uiCompactCatalog()}`;
|
package/package.json
CHANGED
package/src/catalog.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { parseUiEventTime } from './event-time.ts';
|
|
3
2
|
import { uiPropList } from './prop-list.ts';
|
|
4
3
|
|
|
5
4
|
export const UI_CATALOG_VERSION = 1;
|
|
@@ -44,25 +43,29 @@ export const UI_CATALOG = {
|
|
|
44
43
|
Progress: {
|
|
45
44
|
group: 'status',
|
|
46
45
|
description: 'Progress with a label; omit value for indeterminate progress.',
|
|
47
|
-
schema: z.strictObject({ value: number.nonnegative().optional(), max: number.positive().optional() })
|
|
46
|
+
schema: z.strictObject({ value: number.nonnegative().optional(), max: number.positive().optional() }),
|
|
47
|
+
text: 'label'
|
|
48
48
|
},
|
|
49
49
|
Steps: { group: 'status', description: 'An ordered sequence of steps.', schema: empty, children: ['Step'] },
|
|
50
50
|
Step: {
|
|
51
51
|
group: 'status',
|
|
52
52
|
description: 'One step in a sequence.',
|
|
53
53
|
schema: z.strictObject({ state: z.enum(['done', 'running', 'pending', 'failed', 'skipped']), detail: text.optional() }),
|
|
54
|
-
parent: 'Steps'
|
|
54
|
+
parent: 'Steps',
|
|
55
|
+
text: 'label'
|
|
55
56
|
},
|
|
56
57
|
Timeline: { group: 'status', description: 'Events in time order.', schema: empty, children: ['Event'], count: { min: 1 } },
|
|
57
58
|
Event: {
|
|
58
59
|
group: 'status',
|
|
59
|
-
description:
|
|
60
|
+
description:
|
|
61
|
+
'One event at a time or date; children are the title. at is HH:MM[:SS], YYYY-MM-DD, or YYYY-MM-DDTHH:MM[:SS] with an optional Z or ±HH:MM.',
|
|
60
62
|
schema: z.strictObject({
|
|
61
|
-
at: text
|
|
63
|
+
at: text,
|
|
62
64
|
tone,
|
|
63
65
|
detail: text.optional()
|
|
64
66
|
}),
|
|
65
|
-
parent: 'Timeline'
|
|
67
|
+
parent: 'Timeline',
|
|
68
|
+
text: 'title'
|
|
66
69
|
},
|
|
67
70
|
Stats: { group: 'data', description: 'A small set of key numbers.', schema: empty, children: ['Stat'] },
|
|
68
71
|
Stat: {
|
|
@@ -72,7 +75,7 @@ export const UI_CATALOG = {
|
|
|
72
75
|
parent: 'Stats'
|
|
73
76
|
},
|
|
74
77
|
EntityList: { group: 'data', description: 'Labeled facts about an entity.', schema: empty, children: ['Entry'] },
|
|
75
|
-
Entry: { group: 'data', description: 'One labeled fact.', schema: z.strictObject({ label: text }), parent: 'EntityList' },
|
|
78
|
+
Entry: { group: 'data', description: 'One labeled fact.', schema: z.strictObject({ label: text }), parent: 'EntityList', text: 'value' },
|
|
76
79
|
Table: { group: 'data', description: 'Rows with named columns.', schema: z.strictObject({ rows }), children: ['Column'] },
|
|
77
80
|
Column: {
|
|
78
81
|
group: 'data',
|
|
@@ -104,13 +107,16 @@ export const UI_CATALOG = {
|
|
|
104
107
|
description: 'A chat attachment with an optional caption.',
|
|
105
108
|
schema: z
|
|
106
109
|
.strictObject({ attachment: name.optional(), generated: z.literal('latest').optional(), alt: text.optional() })
|
|
107
|
-
.refine((props) => Boolean(props.attachment) !== Boolean(props.generated), 'Choose attachment or generated, exclusively.')
|
|
110
|
+
.refine((props) => Boolean(props.attachment) !== Boolean(props.generated), 'Choose attachment or generated, exclusively.'),
|
|
111
|
+
needs: 'attachment|generated',
|
|
112
|
+
text: 'caption'
|
|
108
113
|
},
|
|
109
114
|
Gallery: { group: 'content', description: 'A grid of images with captions; at most 24.', schema: empty, children: ['Image'], count: { min: 1, max: 24 } },
|
|
110
115
|
Compare: {
|
|
111
116
|
group: 'content',
|
|
112
117
|
description: 'Two chat attachments in one frame with a handle to wipe between them; children are the caption.',
|
|
113
|
-
schema: z.strictObject({ before: name, after: name, beforeLabel: text.optional(), afterLabel: text.optional() })
|
|
118
|
+
schema: z.strictObject({ before: name, after: name, beforeLabel: text.optional(), afterLabel: text.optional() }),
|
|
119
|
+
text: 'caption'
|
|
114
120
|
},
|
|
115
121
|
Map: {
|
|
116
122
|
group: 'places',
|
|
@@ -136,7 +142,9 @@ export const UI_CATALOG = {
|
|
|
136
142
|
(props) => (props.lat === undefined) === (props.lon === undefined) && (props.lat !== undefined || props.address !== undefined),
|
|
137
143
|
'Give lat with lon, or an address.'
|
|
138
144
|
),
|
|
139
|
-
parent: 'Map'
|
|
145
|
+
parent: 'Map',
|
|
146
|
+
needs: 'lat lon|address',
|
|
147
|
+
text: 'name'
|
|
140
148
|
},
|
|
141
149
|
Sources: { group: 'content', description: 'Numbered sources without preloading.', schema: empty, children: ['Source'] },
|
|
142
150
|
Source: {
|
|
@@ -160,15 +168,16 @@ export const UI_CATALOG = {
|
|
|
160
168
|
children: ['Item'],
|
|
161
169
|
binding: 'value'
|
|
162
170
|
},
|
|
163
|
-
Item: { group: 'inputs', description: 'One selectable value.', schema: z.strictObject({ value: scalar }), parent: 'Checklist' },
|
|
164
|
-
Switch: { group: 'inputs', description: 'A local boolean.', schema: z.strictObject({ value: z.boolean() }), binding: 'value' },
|
|
171
|
+
Item: { group: 'inputs', description: 'One selectable value.', schema: z.strictObject({ value: scalar }), parent: 'Checklist', text: 'label' },
|
|
172
|
+
Switch: { group: 'inputs', description: 'A local boolean.', schema: z.strictObject({ value: z.boolean() }), binding: 'value', text: 'label' },
|
|
165
173
|
Slider: {
|
|
166
174
|
group: 'inputs',
|
|
167
175
|
description: 'A local number in a range.',
|
|
168
176
|
schema: z
|
|
169
177
|
.strictObject({ value: number, min: number, max: number, step: number.positive().optional(), unit: text.optional() })
|
|
170
178
|
.refine((props) => props.min < props.max && props.value >= props.min && props.value <= props.max, 'Value must lie within an increasing range.'),
|
|
171
|
-
binding: 'value'
|
|
179
|
+
binding: 'value',
|
|
180
|
+
text: 'label'
|
|
172
181
|
},
|
|
173
182
|
Segmented: { group: 'inputs', description: 'Choose one local value.', schema: z.strictObject({ value: scalar }), children: ['Option'], binding: 'value' },
|
|
174
183
|
Select: {
|
|
@@ -176,22 +185,31 @@ export const UI_CATALOG = {
|
|
|
176
185
|
description: 'Choose one local value from a dropdown, for more or longer options than Segmented holds; the text beside its Options is its label.',
|
|
177
186
|
schema: z.strictObject({ value: scalar }),
|
|
178
187
|
children: ['Option'],
|
|
179
|
-
binding: 'value'
|
|
188
|
+
binding: 'value',
|
|
189
|
+
text: 'label'
|
|
190
|
+
},
|
|
191
|
+
Option: {
|
|
192
|
+
group: 'inputs',
|
|
193
|
+
description: 'One local alternative.',
|
|
194
|
+
schema: z.strictObject({ value: scalar }),
|
|
195
|
+
parent: ['Segmented', 'Select'],
|
|
196
|
+
text: 'label'
|
|
180
197
|
},
|
|
181
|
-
Option: { group: 'inputs', description: 'One local alternative.', schema: z.strictObject({ value: scalar }), parent: ['Segmented', 'Select'] },
|
|
182
198
|
TextField: {
|
|
183
199
|
group: 'inputs',
|
|
184
200
|
description: 'A line of local text up to maxLength characters (256 without one, at most 1024); children are the label.',
|
|
185
201
|
schema: z
|
|
186
202
|
.strictObject({ value: text, maxLength: z.number().int().min(1).max(1024).optional(), placeholder: text.max(128).optional() })
|
|
187
203
|
.refine((props) => props.value.length <= (props.maxLength ?? 256), 'Value must not be longer than maxLength.'),
|
|
188
|
-
binding: 'value'
|
|
204
|
+
binding: 'value',
|
|
205
|
+
text: 'label'
|
|
189
206
|
},
|
|
190
207
|
Button: {
|
|
191
208
|
group: 'inputs',
|
|
192
209
|
description: 'Run one local state action when pressed; children are the label.',
|
|
193
210
|
schema: z.strictObject({ disabled: z.boolean().optional() }),
|
|
194
|
-
action: 'action'
|
|
211
|
+
action: 'action',
|
|
212
|
+
text: 'label'
|
|
195
213
|
},
|
|
196
214
|
Show: { group: 'inputs', description: 'Show children when the condition is true.', schema: z.strictObject({ when: z.boolean() }) },
|
|
197
215
|
Each: {
|
|
@@ -204,7 +222,8 @@ export const UI_CATALOG = {
|
|
|
204
222
|
group: 'choices',
|
|
205
223
|
description: 'Send its label and visible context once.',
|
|
206
224
|
schema: z.strictObject({ context: text.optional(), primary: z.boolean().optional(), disabled: z.boolean().optional() }),
|
|
207
|
-
parent: 'Choices'
|
|
225
|
+
parent: 'Choices',
|
|
226
|
+
text: 'label'
|
|
208
227
|
}
|
|
209
228
|
} as const;
|
|
210
229
|
|
package/src/compiler.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { safeKey, UI_LIMITS, UiBudget, UiFailure, type UiLimits } from './budget.ts';
|
|
2
2
|
import { isUiComponent, UI_CATALOG, UI_CATALOG_VERSION } from './catalog.ts';
|
|
3
3
|
import { uiActionProp, uiActionTarget, uiLocalState } from './actions.ts';
|
|
4
|
+
import { unreadableEventTime } from './event-warning.ts';
|
|
4
5
|
import { uiNodeFallback, uiPlainText } from './fallback.ts';
|
|
5
6
|
import { evaluateUiExpression, sameUiValue, type UiExpression, type UiValue } from './expression.ts';
|
|
6
7
|
import { parseUiSyntax, uiDiagnostic, type UiDiagnostic, type UiSyntaxNode } from './syntax.ts';
|
|
@@ -312,6 +313,10 @@ export function compileUiBlock(source: string, options: UiCompileOptions): UiBlo
|
|
|
312
313
|
throw new UiFailure('invalid_props', parsed.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; '));
|
|
313
314
|
}
|
|
314
315
|
}
|
|
316
|
+
const unreadable = !unresolved && syntax.complete && syntax.type === 'Event' ? unreadableEventTime(evaluated.at) : null;
|
|
317
|
+
if (unreadable !== null) {
|
|
318
|
+
diagnose(unreadable, syntax.start, syntax.end, syntax.id);
|
|
319
|
+
}
|
|
315
320
|
// The catalog promises six series, one per chart color; more still reach the values a person can open.
|
|
316
321
|
if (!unresolved && syntax.complete && syntax.type === 'Chart' && chartSeries(evaluated.data) > CHART_SERIES) {
|
|
317
322
|
diagnose(
|
package/src/event-time.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
const EVENT_TIME =
|
|
1
|
+
const EVENT_TIME =
|
|
2
|
+
/^(?:(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?|(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?(Z|[+-]\d{2}:\d{2})?)?)$/;
|
|
2
3
|
|
|
3
4
|
export interface UiEventTime {
|
|
4
5
|
kind: 'time' | 'date' | 'datetime';
|
|
5
6
|
date: Date;
|
|
6
7
|
// True when the text gave no offset: `date` then holds the parts as written in UTC, to be written in UTC, so no zone or clock change moves them. False is a real instant, to be written in the reader's zone.
|
|
7
8
|
written: boolean;
|
|
9
|
+
// True for a lone time that wrote seconds (`14:05:30`), which then shows them. A date-time shows its minutes only.
|
|
10
|
+
seconds: boolean;
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
function validClock(hour: number, minute: number, second = 0): boolean {
|
|
@@ -21,17 +24,23 @@ function zoneOffset(zone: string): number | null {
|
|
|
21
24
|
return minutes > 59 || offset < -12 * 60 || offset > 14 * 60 ? null : offset;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
/* The moment an Event's `at` names, or null when it is no `HH:MM`, `YYYY-MM-DD` or ISO date-time with an optional offset. */
|
|
27
|
+
/* The moment an Event's `at` names, or null when it is no `HH:MM[:SS]`, `YYYY-MM-DD` or ISO date-time with an optional offset. */
|
|
25
28
|
export function parseUiEventTime(at: string): UiEventTime | null {
|
|
26
29
|
const match = EVENT_TIME.exec(at);
|
|
27
30
|
if (match === null) {
|
|
28
31
|
return null;
|
|
29
32
|
}
|
|
30
|
-
const [, clockHour, clockMinute, year, month, day, hour, minute, second, zone] = match.map((part) => part as string | undefined);
|
|
33
|
+
const [, clockHour, clockMinute, clockSecond, year, month, day, hour, minute, second, zone] = match.map((part) => part as string | undefined);
|
|
31
34
|
if (clockHour !== undefined) {
|
|
32
35
|
// The day is not shown for a time, so any day will do.
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
const clockSeconds = Number(clockSecond ?? 0);
|
|
37
|
+
return validClock(Number(clockHour), Number(clockMinute), clockSeconds)
|
|
38
|
+
? {
|
|
39
|
+
kind: 'time',
|
|
40
|
+
date: new Date(Date.UTC(2000, 0, 1, Number(clockHour), Number(clockMinute), clockSeconds)),
|
|
41
|
+
written: true,
|
|
42
|
+
seconds: clockSecond !== undefined
|
|
43
|
+
}
|
|
35
44
|
: null;
|
|
36
45
|
}
|
|
37
46
|
const [y, m, d] = [Number(year), Number(month), Number(day)];
|
|
@@ -40,15 +49,15 @@ export function parseUiEventTime(at: string): UiEventTime | null {
|
|
|
40
49
|
return null;
|
|
41
50
|
}
|
|
42
51
|
if (hour === undefined) {
|
|
43
|
-
return { kind: 'date', date: calendar, written: true };
|
|
52
|
+
return { kind: 'date', date: calendar, written: true, seconds: false };
|
|
44
53
|
}
|
|
45
54
|
const [h, min, sec] = [Number(hour), Number(minute), Number(second ?? 0)];
|
|
46
55
|
if (!validClock(h, min, sec)) {
|
|
47
56
|
return null;
|
|
48
57
|
}
|
|
49
58
|
if (zone === undefined) {
|
|
50
|
-
return { kind: 'datetime', date: new Date(Date.UTC(y, m - 1, d, h, min, sec)), written: true };
|
|
59
|
+
return { kind: 'datetime', date: new Date(Date.UTC(y, m - 1, d, h, min, sec)), written: true, seconds: false };
|
|
51
60
|
}
|
|
52
61
|
const offset = zoneOffset(zone);
|
|
53
|
-
return offset === null ? null : { kind: 'datetime', date: new Date(Date.UTC(y, m - 1, d, h, min, sec) - offset * 60000), written: false };
|
|
62
|
+
return offset === null ? null : { kind: 'datetime', date: new Date(Date.UTC(y, m - 1, d, h, min, sec) - offset * 60000), written: false, seconds: false };
|
|
54
63
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { UiFailure } from './budget.ts';
|
|
2
|
+
import { parseUiEventTime } from './event-time.ts';
|
|
3
|
+
|
|
4
|
+
/* The warning for an Event whose `at` cannot be read, or null when it can. The Event still draws, with `at` as written. */
|
|
5
|
+
export function unreadableEventTime(at: unknown): UiFailure | null {
|
|
6
|
+
if (typeof at !== 'string' || parseUiEventTime(at) !== null) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const shown = at.length > 40 ? `${at.slice(0, 40)}...` : at;
|
|
10
|
+
return new UiFailure(
|
|
11
|
+
'unreadable_time',
|
|
12
|
+
`Event at="${shown}" is not HH:MM[:SS], YYYY-MM-DD or YYYY-MM-DDTHH:MM[:SS] with an optional Z or ±HH:MM, so it is shown as written.`
|
|
13
|
+
);
|
|
14
|
+
}
|
package/src/prop-list.ts
CHANGED
|
@@ -5,3 +5,21 @@ export function uiPropList(entry: { readonly schema: { readonly shape: Readonly<
|
|
|
5
5
|
const props = Object.entries(entry.schema.shape).map(([key, schema]) => `${key}${schema.isOptional() ? '?' : ''}`);
|
|
6
6
|
return entry.action ? [entry.action, ...props] : props;
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
/* What a component needs in the compact note: the props it cannot do without and, when its text is not the plain content, what that text is. */
|
|
10
|
+
export function uiRequirement(
|
|
11
|
+
name: string,
|
|
12
|
+
entry: {
|
|
13
|
+
readonly schema: { readonly shape: Readonly<Record<string, z.ZodType>> };
|
|
14
|
+
readonly action?: string;
|
|
15
|
+
readonly needs?: string;
|
|
16
|
+
readonly text?: string;
|
|
17
|
+
}
|
|
18
|
+
): string {
|
|
19
|
+
const required =
|
|
20
|
+
entry.needs ??
|
|
21
|
+
uiPropList(entry)
|
|
22
|
+
.filter((prop) => !prop.endsWith('?'))
|
|
23
|
+
.join(' ');
|
|
24
|
+
return `${name}${required ? ` ${required}` : ''}${entry.text ? ` (text: ${entry.text})` : ''}`;
|
|
25
|
+
}
|
package/src/protocol.ts
CHANGED
|
@@ -26,7 +26,8 @@ export const UiDiagnosticSchema = z.object({
|
|
|
26
26
|
message: z.string(),
|
|
27
27
|
start: position,
|
|
28
28
|
end: position,
|
|
29
|
-
nodeId: z.string().optional()
|
|
29
|
+
nodeId: z.string().optional(),
|
|
30
|
+
severity: z.literal('warning').optional()
|
|
30
31
|
});
|
|
31
32
|
|
|
32
33
|
export const UiBlockSchema: z.ZodType<UiBlock> = z.object({
|
package/src/runtime.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { isUiComponent, UI_CATALOG, UI_CATALOG_VERSION } from './catalog.ts';
|
|
|
4
4
|
import { type UiBlock, type UiNode } from './compiler.ts';
|
|
5
5
|
import { uiActionProp, uiActionTarget, uiLocalState } from './actions.ts';
|
|
6
6
|
import { uiCompiledNodes, uiVisibleInputs } from './inputs.ts';
|
|
7
|
+
import { unreadableEventTime } from './event-warning.ts';
|
|
7
8
|
import { uiDiagnostic, type UiDiagnostic } from './syntax.ts';
|
|
8
9
|
|
|
9
10
|
export interface UiBinding<Value> {
|
|
@@ -185,6 +186,16 @@ export function evaluateUiBlock(block: UiBlock, state = new UiState(block), limi
|
|
|
185
186
|
throw new UiFailure('invalid_props', parsed.error.issues.map((issue) => issue.message).join('; '));
|
|
186
187
|
}
|
|
187
188
|
view.props = parsed.data;
|
|
189
|
+
const unreadable = node.complete && node.type === 'Event' ? unreadableEventTime(props.at) : null;
|
|
190
|
+
// The compiler already warned about a time it could read from the source; this one waited on an Each or a query.
|
|
191
|
+
if (
|
|
192
|
+
unreadable !== null &&
|
|
193
|
+
![...block.diagnostics, ...result.diagnostics].some((item) => item.code === 'unreadable_time' && item.nodeId === node.id)
|
|
194
|
+
) {
|
|
195
|
+
if (result.diagnostics.length < budget.limits.diagnostics) {
|
|
196
|
+
result.diagnostics.push(uiDiagnostic(unreadable, node.start, node.end, node.id));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
188
199
|
if (node.type === 'Show') {
|
|
189
200
|
return props.when ? children(node.children, variables, depth + 1, suffix) : [];
|
|
190
201
|
}
|
package/src/syntax.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface UiDiagnostic {
|
|
|
7
7
|
start: number;
|
|
8
8
|
end: number;
|
|
9
9
|
nodeId?: string;
|
|
10
|
+
// Only a warning says so: the part it names still drew. Absent means an error, a part that could not be drawn.
|
|
11
|
+
severity?: 'warning';
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export interface UiSyntaxNode {
|
|
@@ -29,9 +31,13 @@ export interface UiSyntax {
|
|
|
29
31
|
diagnostics: UiDiagnostic[];
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
const WARNING_CODES = new Set(['refused_prop', 'too_many_series', 'too_many_children', 'unreadable_time']);
|
|
35
|
+
|
|
32
36
|
export function uiDiagnostic(error: unknown, start: number, end: number, nodeId?: string): UiDiagnostic {
|
|
37
|
+
const code = error instanceof UiFailure ? error.code : 'invalid_syntax';
|
|
33
38
|
return {
|
|
34
|
-
code
|
|
39
|
+
code,
|
|
40
|
+
...(WARNING_CODES.has(code) ? { severity: 'warning' as const } : {}),
|
|
35
41
|
message: error instanceof Error ? error.message : 'Invalid UI syntax.',
|
|
36
42
|
start,
|
|
37
43
|
end,
|
package/src/text.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { UI_CATALOG, UI_GROUPS, type UiGroupName } from './catalog.ts';
|
|
2
|
-
import { uiPropList } from './prop-list.ts';
|
|
2
|
+
import { uiPropList, uiRequirement } from './prop-list.ts';
|
|
3
3
|
import { UI_FENCE_LANGUAGE, type UiBlock } from './compiler.ts';
|
|
4
4
|
|
|
5
5
|
export function uiCompactCatalog(): string {
|
|
@@ -39,15 +39,15 @@ function uiGroupComponents(group: string) {
|
|
|
39
39
|
return Object.entries(UI_CATALOG).filter(([, entry]) => entry.group === group);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/* One line per group with
|
|
42
|
+
/* One line per group with its components and what each needs, for a host that hands the optional props out per group. */
|
|
43
43
|
function uiGroupNames(detailHint: string | undefined): string {
|
|
44
44
|
const lines = Object.keys(UI_GROUPS).map(
|
|
45
45
|
(group) =>
|
|
46
46
|
`${group}: ${uiGroupComponents(group)
|
|
47
|
-
.map(([name]) => name)
|
|
47
|
+
.map(([name, entry]) => uiRequirement(name, entry))
|
|
48
48
|
.join(', ')}`
|
|
49
49
|
);
|
|
50
|
-
return `Components by group${detailHint ? ` (${detailHint})` : ''}:\n${lines.join('\n')}`;
|
|
50
|
+
return `Components by group${detailHint ? ` (${detailHint})` : ''}, required props (text: words between the tags):\n${lines.join('\n')}`;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export function uiSessionNote({ fenceLanguage = UI_FENCE_LANGUAGE, catalog = 'props', detailHint }: UiTextOptions = {}): string {
|