@assistant-ui/react-generative-ui 0.0.24 → 0.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.
- package/README.md +2 -0
- package/dist/a2ui.d.ts +1 -4
- package/dist/a2ui.d.ts.map +1 -1
- package/dist/a2ui.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -10
- package/dist/index.server.d.ts +1 -2
- package/dist/index.server.d.ts.map +1 -1
- package/dist/index.server.js +1 -10
- package/dist/ir.d.ts +1 -111
- package/dist/ir.d.ts.map +1 -1
- package/dist/ir.js +1 -175
- package/dist/slack.d.ts +1 -4
- package/dist/slack.d.ts.map +1 -1
- package/dist/slack.js +1 -4
- package/dist/teams.d.ts +1 -4
- package/dist/teams.d.ts.map +1 -1
- package/dist/teams.js +1 -4
- package/package.json +4 -5
- package/src/a2ui.ts +1 -22
- package/src/index.server.ts +1 -2
- package/src/index.test.ts +75 -0
- package/src/index.ts +1 -2
- package/src/ir.ts +1 -279
- package/src/slack.ts +1 -39
- package/src/teams.ts +1 -35
- package/dist/JSONGenerativeUI.client.d.ts +0 -28
- package/dist/JSONGenerativeUI.client.d.ts.map +0 -1
- package/dist/JSONGenerativeUI.client.js +0 -101
- package/dist/JSONGenerativeUI.client.js.map +0 -1
- package/dist/JSONGenerativeUI.server.d.ts +0 -21
- package/dist/JSONGenerativeUI.server.d.ts.map +0 -1
- package/dist/JSONGenerativeUI.server.js +0 -32
- package/dist/JSONGenerativeUI.server.js.map +0 -1
- package/dist/JSONGenerativeUI.shared.d.ts +0 -63
- package/dist/JSONGenerativeUI.shared.d.ts.map +0 -1
- package/dist/JSONGenerativeUI.shared.js +0 -31
- package/dist/JSONGenerativeUI.shared.js.map +0 -1
- package/dist/RadioGroupScope.d.ts +0 -6
- package/dist/RadioGroupScope.d.ts.map +0 -1
- package/dist/RadioGroupScope.js +0 -25
- package/dist/RadioGroupScope.js.map +0 -1
- package/dist/a2ui/BindingContext.d.ts +0 -11
- package/dist/a2ui/BindingContext.d.ts.map +0 -1
- package/dist/a2ui/BindingContext.js +0 -12
- package/dist/a2ui/BindingContext.js.map +0 -1
- package/dist/a2ui/PresentRenderer.d.ts +0 -11
- package/dist/a2ui/PresentRenderer.d.ts.map +0 -1
- package/dist/a2ui/PresentRenderer.js +0 -39
- package/dist/a2ui/PresentRenderer.js.map +0 -1
- package/dist/a2ui/convert.d.ts +0 -15
- package/dist/a2ui/convert.d.ts.map +0 -1
- package/dist/a2ui/convert.js +0 -728
- package/dist/a2ui/convert.js.map +0 -1
- package/dist/a2ui/dataModel.d.ts +0 -9
- package/dist/a2ui/dataModel.d.ts.map +0 -1
- package/dist/a2ui/dataModel.js +0 -44
- package/dist/a2ui/dataModel.js.map +0 -1
- package/dist/a2ui/reducer.d.ts +0 -8
- package/dist/a2ui/reducer.d.ts.map +0 -1
- package/dist/a2ui/reducer.js +0 -242
- package/dist/a2ui/reducer.js.map +0 -1
- package/dist/a2ui/snapshot.d.ts +0 -3
- package/dist/a2ui/snapshot.d.ts.map +0 -1
- package/dist/a2ui/snapshot.js +0 -33
- package/dist/a2ui/snapshot.js.map +0 -1
- package/dist/a2ui/types.d.ts +0 -88
- package/dist/a2ui/types.d.ts.map +0 -1
- package/dist/a2ui/types.js +0 -6
- package/dist/a2ui/types.js.map +0 -1
- package/dist/a2ui/valueFunctions.d.ts +0 -16
- package/dist/a2ui/valueFunctions.d.ts.map +0 -1
- package/dist/a2ui/valueFunctions.js +0 -332
- package/dist/a2ui/valueFunctions.js.map +0 -1
- package/dist/actionRegistry.d.ts +0 -29
- package/dist/actionRegistry.d.ts.map +0 -1
- package/dist/actionRegistry.js +0 -41
- package/dist/actionRegistry.js.map +0 -1
- package/dist/answeredValues.d.ts +0 -16
- package/dist/answeredValues.d.ts.map +0 -1
- package/dist/answeredValues.js +0 -37
- package/dist/answeredValues.js.map +0 -1
- package/dist/buildPresentParameters.d.ts +0 -21
- package/dist/buildPresentParameters.d.ts.map +0 -1
- package/dist/buildPresentParameters.js +0 -95
- package/dist/buildPresentParameters.js.map +0 -1
- package/dist/constants.d.ts +0 -15
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -19
- package/dist/constants.js.map +0 -1
- package/dist/convert/boundSpec.d.ts +0 -48
- package/dist/convert/boundSpec.d.ts.map +0 -1
- package/dist/convert/boundSpec.js +0 -108
- package/dist/convert/boundSpec.js.map +0 -1
- package/dist/convert/copyBounded.d.ts +0 -25
- package/dist/convert/copyBounded.d.ts.map +0 -1
- package/dist/convert/copyBounded.js +0 -38
- package/dist/convert/copyBounded.js.map +0 -1
- package/dist/convert/isElement.d.ts +0 -3
- package/dist/convert/isElement.d.ts.map +0 -1
- package/dist/convert/isElement.js +0 -6
- package/dist/convert/isElement.js.map +0 -1
- package/dist/convert/takeRun.d.ts +0 -6
- package/dist/convert/takeRun.d.ts.map +0 -1
- package/dist/convert/takeRun.js +0 -20
- package/dist/convert/takeRun.js.map +0 -1
- package/dist/defineGenerativeComponents.d.ts +0 -29
- package/dist/defineGenerativeComponents.d.ts.map +0 -1
- package/dist/defineGenerativeComponents.js +0 -34
- package/dist/defineGenerativeComponents.js.map +0 -1
- package/dist/fieldReferences.d.ts +0 -9
- package/dist/fieldReferences.d.ts.map +0 -1
- package/dist/fieldReferences.js +0 -53
- package/dist/fieldReferences.js.map +0 -1
- package/dist/generativeUIToJSX.d.ts +0 -18
- package/dist/generativeUIToJSX.d.ts.map +0 -1
- package/dist/generativeUIToJSX.js +0 -76
- package/dist/generativeUIToJSX.js.map +0 -1
- package/dist/index.shared.d.ts +0 -15
- package/dist/index.shared.d.ts.map +0 -1
- package/dist/index.shared.js +0 -9
- package/dist/ir.js.map +0 -1
- package/dist/renderGenerativeUI.d.ts +0 -14
- package/dist/renderGenerativeUI.d.ts.map +0 -1
- package/dist/renderGenerativeUI.js +0 -83
- package/dist/renderGenerativeUI.js.map +0 -1
- package/dist/scopeSchema.d.ts +0 -6
- package/dist/scopeSchema.d.ts.map +0 -1
- package/dist/scopeSchema.js +0 -56
- package/dist/scopeSchema.js.map +0 -1
- package/dist/slack/constants.d.ts +0 -93
- package/dist/slack/constants.d.ts.map +0 -1
- package/dist/slack/constants.js +0 -146
- package/dist/slack/constants.js.map +0 -1
- package/dist/slack/decodeBlockAction.d.ts +0 -10
- package/dist/slack/decodeBlockAction.d.ts.map +0 -1
- package/dist/slack/decodeBlockAction.js +0 -129
- package/dist/slack/decodeBlockAction.js.map +0 -1
- package/dist/slack/fromSlackBlocks.d.ts +0 -27
- package/dist/slack/fromSlackBlocks.d.ts.map +0 -1
- package/dist/slack/fromSlackBlocks.js +0 -483
- package/dist/slack/fromSlackBlocks.js.map +0 -1
- package/dist/slack/toSlackBlocks.d.ts +0 -4
- package/dist/slack/toSlackBlocks.d.ts.map +0 -1
- package/dist/slack/toSlackBlocks.js +0 -901
- package/dist/slack/toSlackBlocks.js.map +0 -1
- package/dist/slack/types.d.ts +0 -204
- package/dist/slack/types.d.ts.map +0 -1
- package/dist/slack/types.js +0 -0
- package/dist/teams/constants.d.ts +0 -34
- package/dist/teams/constants.d.ts.map +0 -1
- package/dist/teams/constants.js +0 -68
- package/dist/teams/constants.js.map +0 -1
- package/dist/teams/decodeSubmitData.d.ts +0 -26
- package/dist/teams/decodeSubmitData.d.ts.map +0 -1
- package/dist/teams/decodeSubmitData.js +0 -70
- package/dist/teams/decodeSubmitData.js.map +0 -1
- package/dist/teams/temporalId.d.ts +0 -5
- package/dist/teams/temporalId.d.ts.map +0 -1
- package/dist/teams/temporalId.js +0 -28
- package/dist/teams/temporalId.js.map +0 -1
- package/dist/teams/toAdaptiveCard.d.ts +0 -45
- package/dist/teams/toAdaptiveCard.d.ts.map +0 -1
- package/dist/teams/toAdaptiveCard.js +0 -642
- package/dist/teams/toAdaptiveCard.js.map +0 -1
- package/dist/teams/toTeamsAttachments.d.ts +0 -10
- package/dist/teams/toTeamsAttachments.d.ts.map +0 -1
- package/dist/teams/toTeamsAttachments.js +0 -99
- package/dist/teams/toTeamsAttachments.js.map +0 -1
- package/dist/teams/types.d.ts +0 -224
- package/dist/teams/types.d.ts.map +0 -1
- package/dist/teams/types.js +0 -0
- package/dist/temporal.d.ts +0 -33
- package/dist/temporal.d.ts.map +0 -1
- package/dist/temporal.js +0 -153
- package/dist/temporal.js.map +0 -1
- package/dist/types.d.ts +0 -99
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -0
- package/dist/vocabulary/alert.d.ts +0 -32
- package/dist/vocabulary/alert.d.ts.map +0 -1
- package/dist/vocabulary/alert.js +0 -160
- package/dist/vocabulary/alert.js.map +0 -1
- package/dist/vocabulary/collectFormValues.d.ts +0 -23
- package/dist/vocabulary/collectFormValues.d.ts.map +0 -1
- package/dist/vocabulary/collectFormValues.js +0 -44
- package/dist/vocabulary/collectFormValues.js.map +0 -1
- package/dist/vocabulary/data.d.ts +0 -66
- package/dist/vocabulary/data.d.ts.map +0 -1
- package/dist/vocabulary/data.js +0 -503
- package/dist/vocabulary/data.js.map +0 -1
- package/dist/vocabulary/dispatch.d.ts +0 -9
- package/dist/vocabulary/dispatch.d.ts.map +0 -1
- package/dist/vocabulary/dispatch.js +0 -50
- package/dist/vocabulary/dispatch.js.map +0 -1
- package/dist/vocabulary/fact.d.ts +0 -19
- package/dist/vocabulary/fact.d.ts.map +0 -1
- package/dist/vocabulary/fact.js +0 -51
- package/dist/vocabulary/fact.js.map +0 -1
- package/dist/vocabulary/form.d.ts +0 -11
- package/dist/vocabulary/form.d.ts.map +0 -1
- package/dist/vocabulary/form.js +0 -23
- package/dist/vocabulary/form.js.map +0 -1
- package/dist/vocabulary/formatValue.d.ts +0 -7
- package/dist/vocabulary/formatValue.d.ts.map +0 -1
- package/dist/vocabulary/formatValue.js +0 -76
- package/dist/vocabulary/formatValue.js.map +0 -1
- package/dist/vocabulary/icon.d.ts +0 -41
- package/dist/vocabulary/icon.d.ts.map +0 -1
- package/dist/vocabulary/icon.js +0 -272
- package/dist/vocabulary/icon.js.map +0 -1
- package/dist/vocabulary/index.d.ts +0 -12
- package/dist/vocabulary/index.d.ts.map +0 -1
- package/dist/vocabulary/index.js +0 -36
- package/dist/vocabulary/index.js.map +0 -1
- package/dist/vocabulary/interactive.d.ts +0 -230
- package/dist/vocabulary/interactive.d.ts.map +0 -1
- package/dist/vocabulary/interactive.js +0 -584
- package/dist/vocabulary/interactive.js.map +0 -1
- package/dist/vocabulary/layout.d.ts +0 -79
- package/dist/vocabulary/layout.d.ts.map +0 -1
- package/dist/vocabulary/layout.js +0 -139
- package/dist/vocabulary/layout.js.map +0 -1
- package/dist/vocabulary/list.d.ts +0 -14
- package/dist/vocabulary/list.d.ts.map +0 -1
- package/dist/vocabulary/list.js +0 -50
- package/dist/vocabulary/list.js.map +0 -1
- package/dist/vocabulary/media.d.ts +0 -25
- package/dist/vocabulary/media.d.ts.map +0 -1
- package/dist/vocabulary/media.js +0 -42
- package/dist/vocabulary/media.js.map +0 -1
- package/dist/vocabulary/text.d.ts +0 -57
- package/dist/vocabulary/text.d.ts.map +0 -1
- package/dist/vocabulary/text.js +0 -49
- package/dist/vocabulary/text.js.map +0 -1
- package/dist/vocabulary/toTextContent.d.ts +0 -2
- package/dist/vocabulary/toTextContent.d.ts.map +0 -1
- package/dist/vocabulary/toTextContent.js +0 -6
- package/dist/vocabulary/toTextContent.js.map +0 -1
- package/src/JSONGenerativeUI.client.tsx +0 -168
- package/src/JSONGenerativeUI.server.tsx +0 -39
- package/src/JSONGenerativeUI.shared.ts +0 -90
- package/src/JSONGenerativeUI.test.tsx +0 -1298
- package/src/RadioGroupScope.server.test.tsx +0 -24
- package/src/RadioGroupScope.tsx +0 -34
- package/src/a2ui/BindingContext.ts +0 -26
- package/src/a2ui/PresentRenderer.tsx +0 -83
- package/src/a2ui/convert.test.ts +0 -2923
- package/src/a2ui/convert.ts +0 -1207
- package/src/a2ui/dataModel.ts +0 -69
- package/src/a2ui/reducer.test.ts +0 -422
- package/src/a2ui/reducer.ts +0 -363
- package/src/a2ui/snapshot.test.ts +0 -52
- package/src/a2ui/snapshot.ts +0 -52
- package/src/a2ui/types.ts +0 -114
- package/src/a2ui/valueFunctions.test.ts +0 -271
- package/src/a2ui/valueFunctions.ts +0 -523
- package/src/actionRegistry.test.ts +0 -90
- package/src/actionRegistry.ts +0 -88
- package/src/answeredValues.test.ts +0 -33
- package/src/answeredValues.tsx +0 -70
- package/src/buildPresentParameters.test.ts +0 -246
- package/src/buildPresentParameters.ts +0 -130
- package/src/constants.ts +0 -17
- package/src/convert/boundSpec.test.ts +0 -192
- package/src/convert/boundSpec.ts +0 -134
- package/src/convert/copyBounded.test.ts +0 -131
- package/src/convert/copyBounded.ts +0 -38
- package/src/convert/isElement.ts +0 -6
- package/src/convert/takeRun.test.ts +0 -64
- package/src/convert/takeRun.ts +0 -19
- package/src/defineGenerativeComponents.ts +0 -38
- package/src/fieldReferences.test.ts +0 -66
- package/src/fieldReferences.ts +0 -83
- package/src/generativeUIToJSX.test.ts +0 -268
- package/src/generativeUIToJSX.ts +0 -152
- package/src/index.server.test.ts +0 -106
- package/src/index.shared.ts +0 -61
- package/src/ir.test.ts +0 -203
- package/src/renderGenerativeUI.test.tsx +0 -373
- package/src/renderGenerativeUI.tsx +0 -165
- package/src/scopeSchema.test.ts +0 -72
- package/src/scopeSchema.ts +0 -71
- package/src/slack/a2uiSlack.test.ts +0 -198
- package/src/slack/constants.ts +0 -194
- package/src/slack/datePicker.test.ts +0 -612
- package/src/slack/decodeBlockAction.test.ts +0 -233
- package/src/slack/decodeBlockAction.ts +0 -232
- package/src/slack/fromSlackBlocks.test.ts +0 -1465
- package/src/slack/fromSlackBlocks.ts +0 -789
- package/src/slack/toSlackBlocks.test.ts +0 -3065
- package/src/slack/toSlackBlocks.ts +0 -1818
- package/src/slack/types.ts +0 -264
- package/src/teams/constants.ts +0 -93
- package/src/teams/decodeSubmitData.test.ts +0 -397
- package/src/teams/decodeSubmitData.ts +0 -110
- package/src/teams/temporal.test.ts +0 -766
- package/src/teams/temporalId.ts +0 -56
- package/src/teams/toAdaptiveCard.test.ts +0 -1897
- package/src/teams/toAdaptiveCard.ts +0 -1057
- package/src/teams/toTeamsAttachments.test.ts +0 -74
- package/src/teams/toTeamsAttachments.ts +0 -133
- package/src/teams/types.ts +0 -266
- package/src/temporal.test.ts +0 -337
- package/src/temporal.ts +0 -263
- package/src/types.ts +0 -119
- package/src/vocabulary/alert.test.tsx +0 -427
- package/src/vocabulary/alert.tsx +0 -217
- package/src/vocabulary/collectFormValues.test.ts +0 -389
- package/src/vocabulary/collectFormValues.ts +0 -116
- package/src/vocabulary/data.test.tsx +0 -858
- package/src/vocabulary/data.tsx +0 -819
- package/src/vocabulary/datepicker.dom.test.tsx +0 -1457
- package/src/vocabulary/dispatch.ts +0 -83
- package/src/vocabulary/fact.test.tsx +0 -94
- package/src/vocabulary/fact.tsx +0 -54
- package/src/vocabulary/form.test.tsx +0 -113
- package/src/vocabulary/form.tsx +0 -39
- package/src/vocabulary/formatValue.test.ts +0 -72
- package/src/vocabulary/formatValue.ts +0 -127
- package/src/vocabulary/icon.test.tsx +0 -64
- package/src/vocabulary/icon.tsx +0 -174
- package/src/vocabulary/index.test.ts +0 -65
- package/src/vocabulary/index.ts +0 -33
- package/src/vocabulary/interactive.dispatch.test.tsx +0 -454
- package/src/vocabulary/interactive.dom.test.tsx +0 -1747
- package/src/vocabulary/interactive.test.tsx +0 -377
- package/src/vocabulary/interactive.tsx +0 -1109
- package/src/vocabulary/layout.test.tsx +0 -323
- package/src/vocabulary/layout.tsx +0 -247
- package/src/vocabulary/list.test.tsx +0 -222
- package/src/vocabulary/list.tsx +0 -50
- package/src/vocabulary/media.test.tsx +0 -85
- package/src/vocabulary/media.tsx +0 -56
- package/src/vocabulary/text.test.tsx +0 -102
- package/src/vocabulary/text.tsx +0 -60
- package/src/vocabulary/toTextContent.ts +0 -2
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { hasFieldReference, resolveFieldReferences } from "../fieldReferences";
|
|
2
|
-
import type { Action } from "../ir";
|
|
3
|
-
import type { GenerativeUIDispatch } from "../types";
|
|
4
|
-
import type { A2uiBinding } from "../a2ui/BindingContext";
|
|
5
|
-
import { equalData } from "../a2ui/dataModel";
|
|
6
|
-
import { CHECKBOX_GROUP_ATTR, FIELD_NAME_ATTR } from "../constants";
|
|
7
|
-
import {
|
|
8
|
-
collectFormValues,
|
|
9
|
-
type FormControlElementLike,
|
|
10
|
-
} from "./collectFormValues";
|
|
11
|
-
|
|
12
|
-
export const actionAttr = (a: Action | undefined): string | undefined =>
|
|
13
|
-
a ? JSON.stringify(a) : undefined;
|
|
14
|
-
|
|
15
|
-
const fieldValues = (
|
|
16
|
-
source: Element,
|
|
17
|
-
bindings?: ReadonlyMap<string, A2uiBinding>,
|
|
18
|
-
): Record<string, unknown> => {
|
|
19
|
-
const scope = source.closest('form[data-aui], [data-aui="root"]');
|
|
20
|
-
if (!scope) return {};
|
|
21
|
-
const root = scope.closest('[data-aui="root"]');
|
|
22
|
-
const controls = Array.from(
|
|
23
|
-
scope.querySelectorAll<
|
|
24
|
-
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
|
25
|
-
>("input, select, textarea"),
|
|
26
|
-
).filter((control) => control.closest('[data-aui="root"]') === root);
|
|
27
|
-
const values = collectFormValues(
|
|
28
|
-
controls as ArrayLike<FormControlElementLike>,
|
|
29
|
-
);
|
|
30
|
-
if (!bindings) return values;
|
|
31
|
-
const groups = new Map<string, Map<Element, FormControlElementLike[]>>();
|
|
32
|
-
for (const control of controls) {
|
|
33
|
-
const name = control.getAttribute(FIELD_NAME_ATTR) ?? control.name;
|
|
34
|
-
if (!bindings.has(name)) continue;
|
|
35
|
-
const field =
|
|
36
|
-
groups.get(name) ?? new Map<Element, FormControlElementLike[]>();
|
|
37
|
-
groups.set(name, field);
|
|
38
|
-
const group =
|
|
39
|
-
control.type === "radio" || control.hasAttribute(CHECKBOX_GROUP_ATTR)
|
|
40
|
-
? (control.closest("fieldset") ?? control)
|
|
41
|
-
: control;
|
|
42
|
-
const elements = field.get(group) ?? [];
|
|
43
|
-
elements.push(control);
|
|
44
|
-
field.set(group, elements);
|
|
45
|
-
}
|
|
46
|
-
for (const [name, field] of groups) {
|
|
47
|
-
const candidates = [...field.values()]
|
|
48
|
-
.map((elements) => collectFormValues(elements)[name])
|
|
49
|
-
.filter((value) => value !== undefined);
|
|
50
|
-
if (candidates.length > 0)
|
|
51
|
-
values[name] =
|
|
52
|
-
candidates.find(
|
|
53
|
-
(value) => !equalData(value, bindings.get(name)!.value),
|
|
54
|
-
) ?? candidates[0];
|
|
55
|
-
}
|
|
56
|
-
return values;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Fires `$action` through `$dispatch` when both are present, merging a runtime value into the payload under the reserved `$input` key (not `value`) so the user's input never clobbers a model-supplied `value` field. Each `{ "$field": name }` inside `$action` first resolves to the current value of the control with that `name` in the vocabulary form or generative UI root nearest to `source`, collected the way a `Form` collects it; a reference whose control is missing or holds no value resolves to its `fallback`, or is dropped without one. No-op when no registry is wired. The returned promise from an async handler is caught and re-thrown on a microtask so rejections surface rather than going unhandled.
|
|
61
|
-
*/
|
|
62
|
-
export const fire = (
|
|
63
|
-
$action: Action | undefined,
|
|
64
|
-
$dispatch: GenerativeUIDispatch | undefined,
|
|
65
|
-
input?: unknown,
|
|
66
|
-
source?: Element,
|
|
67
|
-
bindings?: ReadonlyMap<string, A2uiBinding>,
|
|
68
|
-
) => {
|
|
69
|
-
if (!$action || !$dispatch) return;
|
|
70
|
-
const action =
|
|
71
|
-
source && hasFieldReference($action)
|
|
72
|
-
? (resolveFieldReferences(
|
|
73
|
-
$action,
|
|
74
|
-
fieldValues(source, bindings),
|
|
75
|
-
) as Action)
|
|
76
|
-
: $action;
|
|
77
|
-
const payload = input === undefined ? action : { ...action, $input: input };
|
|
78
|
-
void Promise.resolve($dispatch(payload)).catch((error) => {
|
|
79
|
-
queueMicrotask(() => {
|
|
80
|
-
throw error;
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
};
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
-
import { renderGenerativeUI } from "../renderGenerativeUI";
|
|
4
|
-
import { factVocabulary } from "./fact";
|
|
5
|
-
|
|
6
|
-
const render = (node: unknown) =>
|
|
7
|
-
renderToStaticMarkup(<>{renderGenerativeUI(node, factVocabulary)}</>);
|
|
8
|
-
|
|
9
|
-
describe("factVocabulary", () => {
|
|
10
|
-
it("Fact renders a label/value pair inside a dl", () => {
|
|
11
|
-
expect(render({ $type: "Fact", label: "Status", value: "open" })).toBe(
|
|
12
|
-
'<dl data-aui="fact"><dt data-aui="fact-label">Status</dt><dd data-aui="fact-value">open</dd></dl>',
|
|
13
|
-
);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("Fact ignores malformed text properties", () => {
|
|
17
|
-
expect(
|
|
18
|
-
render({
|
|
19
|
-
$type: "Fact",
|
|
20
|
-
label: { unexpected: true },
|
|
21
|
-
value: { unexpected: true },
|
|
22
|
-
}),
|
|
23
|
-
).toBe(
|
|
24
|
-
'<dl data-aui="fact"><dt data-aui="fact-label"></dt><dd data-aui="fact-value"></dd></dl>',
|
|
25
|
-
);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("Fact renders a good or bad delta with an arrow", () => {
|
|
29
|
-
expect(
|
|
30
|
-
render({
|
|
31
|
-
$type: "Fact",
|
|
32
|
-
label: "Revenue",
|
|
33
|
-
value: "$12.4k",
|
|
34
|
-
delta: "+8%",
|
|
35
|
-
}),
|
|
36
|
-
).toContain(
|
|
37
|
-
'<span data-aui="fact-delta" data-aui-trend="up" data-aui-tone="good">↑ 8%</span>',
|
|
38
|
-
);
|
|
39
|
-
expect(
|
|
40
|
-
render({
|
|
41
|
-
$type: "Fact",
|
|
42
|
-
label: "Cost",
|
|
43
|
-
value: "$12.4k",
|
|
44
|
-
delta: "-8%",
|
|
45
|
-
upIsGood: false,
|
|
46
|
-
}),
|
|
47
|
-
).toContain(
|
|
48
|
-
'<span data-aui="fact-delta" data-aui-trend="down" data-aui-tone="good">↓ 8%</span>',
|
|
49
|
-
);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("Fact reads a typographic minus as a downward delta", () => {
|
|
53
|
-
expect(
|
|
54
|
-
render({
|
|
55
|
-
$type: "Fact",
|
|
56
|
-
label: "Latency",
|
|
57
|
-
value: "120 ms",
|
|
58
|
-
delta: "\u22123%",
|
|
59
|
-
}),
|
|
60
|
-
).toContain('data-aui-trend="down" data-aui-tone="bad">↓ 3%</span>');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("Fact infers a zero delta as flat", () => {
|
|
64
|
-
expect(
|
|
65
|
-
render({
|
|
66
|
-
$type: "Fact",
|
|
67
|
-
label: "Revenue",
|
|
68
|
-
value: "$12.4k",
|
|
69
|
-
delta: "+0.0%",
|
|
70
|
-
}),
|
|
71
|
-
).toContain('data-aui-trend="flat" data-aui-tone="neutral">→ +0.0%</span>');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("Fact lets trend override the delta sign and renders flat deltas neutrally", () => {
|
|
75
|
-
expect(
|
|
76
|
-
render({
|
|
77
|
-
$type: "Fact",
|
|
78
|
-
label: "Revenue",
|
|
79
|
-
value: "$12.4k",
|
|
80
|
-
delta: "-8%",
|
|
81
|
-
trend: "up",
|
|
82
|
-
}),
|
|
83
|
-
).toContain('data-aui-trend="up" data-aui-tone="good">↑ -8%</span>');
|
|
84
|
-
expect(
|
|
85
|
-
render({
|
|
86
|
-
$type: "Fact",
|
|
87
|
-
label: "Revenue",
|
|
88
|
-
value: "$12.4k",
|
|
89
|
-
delta: "0%",
|
|
90
|
-
trend: "flat",
|
|
91
|
-
}),
|
|
92
|
-
).toContain('data-aui-trend="flat" data-aui-tone="neutral">→ 0%</span>');
|
|
93
|
-
});
|
|
94
|
-
});
|
package/src/vocabulary/fact.tsx
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { GenerativeUILibrary } from "../types";
|
|
3
|
-
import { factTrend, formatFactDelta } from "./formatValue";
|
|
4
|
-
import { toTextContent } from "./toTextContent";
|
|
5
|
-
|
|
6
|
-
const factTone = (
|
|
7
|
-
trend: "up" | "down" | "flat",
|
|
8
|
-
upIsGood: unknown,
|
|
9
|
-
): "good" | "bad" | "neutral" => {
|
|
10
|
-
if (trend === "flat") return "neutral";
|
|
11
|
-
const positive = upIsGood !== false;
|
|
12
|
-
const isGood = trend === "up" ? positive : !positive;
|
|
13
|
-
return isGood ? "good" : "bad";
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export const factVocabulary = {
|
|
17
|
-
Fact: {
|
|
18
|
-
description:
|
|
19
|
-
"A label/value pair, rendered as a key followed by its value. Use for compact metadata.",
|
|
20
|
-
properties: z.object({
|
|
21
|
-
label: z.string().describe("The fact label (key)."),
|
|
22
|
-
value: z.string().describe("The fact value."),
|
|
23
|
-
delta: z.string().optional().describe("Change from before."),
|
|
24
|
-
trend: z
|
|
25
|
-
.enum(["up", "down", "flat"])
|
|
26
|
-
.optional()
|
|
27
|
-
.describe("Change direction."),
|
|
28
|
-
upIsGood: z.boolean().optional().describe("Whether up is good."),
|
|
29
|
-
}),
|
|
30
|
-
render: ({ label, value, delta, trend, upIsGood, children }) => {
|
|
31
|
-
const direction = factTrend(delta, trend);
|
|
32
|
-
const tone = factTone(direction, upIsGood);
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<dl data-aui="fact">
|
|
36
|
-
<dt data-aui="fact-label">{toTextContent(label)}</dt>
|
|
37
|
-
<dd data-aui="fact-value">
|
|
38
|
-
{toTextContent(value)}
|
|
39
|
-
{typeof delta === "string" ? (
|
|
40
|
-
<span
|
|
41
|
-
data-aui="fact-delta"
|
|
42
|
-
data-aui-trend={direction}
|
|
43
|
-
data-aui-tone={tone}
|
|
44
|
-
>
|
|
45
|
-
{formatFactDelta(delta, direction)}
|
|
46
|
-
</span>
|
|
47
|
-
) : null}
|
|
48
|
-
{children}
|
|
49
|
-
</dd>
|
|
50
|
-
</dl>
|
|
51
|
-
);
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
} satisfies GenerativeUILibrary;
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import type { ReactElement } from "react";
|
|
3
|
-
import { renderToStaticMarkup } from "react-dom/server";
|
|
4
|
-
import { renderGenerativeUI } from "../renderGenerativeUI";
|
|
5
|
-
import { createActionRegistry } from "../actionRegistry";
|
|
6
|
-
import { formVocabulary } from "./form";
|
|
7
|
-
import type { FormControlElementLike } from "./collectFormValues";
|
|
8
|
-
|
|
9
|
-
const render = (node: unknown) =>
|
|
10
|
-
renderToStaticMarkup(<>{renderGenerativeUI(node, formVocabulary)}</>);
|
|
11
|
-
|
|
12
|
-
const el = (
|
|
13
|
-
partial: Partial<FormControlElementLike>,
|
|
14
|
-
): FormControlElementLike => ({
|
|
15
|
-
name: "",
|
|
16
|
-
type: "text",
|
|
17
|
-
value: "",
|
|
18
|
-
disabled: false,
|
|
19
|
-
hasAttribute: () => false,
|
|
20
|
-
...partial,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const fakeSubmitEvent = (elements: FormControlElementLike[]) => ({
|
|
24
|
-
preventDefault: vi.fn(),
|
|
25
|
-
currentTarget: { elements },
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe("formVocabulary", () => {
|
|
29
|
-
it("Form renders a form element with children and a gap hook", () => {
|
|
30
|
-
const html = render({
|
|
31
|
-
$type: "Form",
|
|
32
|
-
gap: 2,
|
|
33
|
-
children: { $type: "Fact", label: "a", value: "b" } as never,
|
|
34
|
-
});
|
|
35
|
-
expect(html).toContain('<form data-aui="form" data-aui-gap="2">');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("Form without gap or $action renders bare", () => {
|
|
39
|
-
expect(render({ $type: "Form" })).toBe('<form data-aui="form"></form>');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("Form onSubmit prevents default and dispatches collected values keyed by name", () => {
|
|
43
|
-
const handler = vi.fn();
|
|
44
|
-
const registry = createActionRegistry({ save: handler });
|
|
45
|
-
const out = formVocabulary.Form.render({
|
|
46
|
-
$status: "done",
|
|
47
|
-
$action: { type: "save" },
|
|
48
|
-
$dispatch: registry.dispatch,
|
|
49
|
-
}) as ReactElement;
|
|
50
|
-
const onSubmit = (out.props as { onSubmit: (e: unknown) => void }).onSubmit;
|
|
51
|
-
const event = fakeSubmitEvent([
|
|
52
|
-
el({ name: "email", type: "email", value: "a@example.com" }),
|
|
53
|
-
el({ name: "agree", type: "checkbox", checked: true }),
|
|
54
|
-
el({ name: "size", type: "radio", value: "sm", checked: false }),
|
|
55
|
-
el({ name: "size", type: "radio", value: "lg", checked: true }),
|
|
56
|
-
el({ name: "", type: "text", value: "ignored" }),
|
|
57
|
-
el({ name: "off", type: "text", value: "ignored", disabled: true }),
|
|
58
|
-
]);
|
|
59
|
-
onSubmit(event);
|
|
60
|
-
expect(event.preventDefault).toHaveBeenCalled();
|
|
61
|
-
expect(handler).toHaveBeenCalledWith({
|
|
62
|
-
payload: {
|
|
63
|
-
type: "save",
|
|
64
|
-
$input: { email: "a@example.com", agree: true, size: "lg" },
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("Form onSubmit collects repeated non-radio names into an array", () => {
|
|
70
|
-
const handler = vi.fn();
|
|
71
|
-
const registry = createActionRegistry({ save: handler });
|
|
72
|
-
const out = formVocabulary.Form.render({
|
|
73
|
-
$status: "done",
|
|
74
|
-
$action: { type: "save" },
|
|
75
|
-
$dispatch: registry.dispatch,
|
|
76
|
-
}) as ReactElement;
|
|
77
|
-
const onSubmit = (out.props as { onSubmit: (e: unknown) => void }).onSubmit;
|
|
78
|
-
onSubmit(
|
|
79
|
-
fakeSubmitEvent([
|
|
80
|
-
el({ name: "tag", type: "text", value: "a" }),
|
|
81
|
-
el({ name: "tag", type: "text", value: "b" }),
|
|
82
|
-
]),
|
|
83
|
-
);
|
|
84
|
-
expect(handler).toHaveBeenCalledWith({
|
|
85
|
-
payload: { type: "save", $input: { tag: ["a", "b"] } },
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("Form onSubmit is silent (but still prevents default) when no $action is wired", () => {
|
|
90
|
-
const out = formVocabulary.Form.render({
|
|
91
|
-
$status: "done",
|
|
92
|
-
}) as ReactElement;
|
|
93
|
-
const onSubmit = (out.props as { onSubmit: (e: unknown) => void }).onSubmit;
|
|
94
|
-
const event = fakeSubmitEvent([
|
|
95
|
-
el({ name: "x", type: "text", value: "y" }),
|
|
96
|
-
]);
|
|
97
|
-
expect(() => onSubmit(event)).not.toThrow();
|
|
98
|
-
expect(event.preventDefault).toHaveBeenCalled();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("Form onSubmit is silent (but still prevents default) when $action is present but no $dispatch is wired", () => {
|
|
102
|
-
const out = formVocabulary.Form.render({
|
|
103
|
-
$status: "done",
|
|
104
|
-
$action: { type: "save" },
|
|
105
|
-
}) as ReactElement;
|
|
106
|
-
const onSubmit = (out.props as { onSubmit: (e: unknown) => void }).onSubmit;
|
|
107
|
-
const event = fakeSubmitEvent([
|
|
108
|
-
el({ name: "x", type: "text", value: "y" }),
|
|
109
|
-
]);
|
|
110
|
-
expect(() => onSubmit(event)).not.toThrow();
|
|
111
|
-
expect(event.preventDefault).toHaveBeenCalled();
|
|
112
|
-
});
|
|
113
|
-
});
|
package/src/vocabulary/form.tsx
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { GenerativeUILibrary } from "../types";
|
|
3
|
-
import { actionAttr, fire } from "./dispatch";
|
|
4
|
-
import { collectFormValuesFromEvent } from "./collectFormValues";
|
|
5
|
-
|
|
6
|
-
export const formVocabulary = {
|
|
7
|
-
Form: {
|
|
8
|
-
description:
|
|
9
|
-
"Wraps named child controls (Select/Input/Checkbox/RadioGroup/CheckboxGroup/DatePicker with a `name`). Carries `$action`; on submit it fires with every named control's value, keyed by `name`.",
|
|
10
|
-
properties: z.object({
|
|
11
|
-
gap: z
|
|
12
|
-
.number()
|
|
13
|
-
.min(0)
|
|
14
|
-
.max(8)
|
|
15
|
-
.optional()
|
|
16
|
-
.describe(
|
|
17
|
-
"Gap between children in 4px units. 0 to 8 is the supported range.",
|
|
18
|
-
),
|
|
19
|
-
}),
|
|
20
|
-
render: ({ gap, $action, $dispatch, children }) => (
|
|
21
|
-
<form
|
|
22
|
-
data-aui="form"
|
|
23
|
-
data-aui-gap={gap}
|
|
24
|
-
data-aui-action={actionAttr($action)}
|
|
25
|
-
onSubmit={(event) => {
|
|
26
|
-
event.preventDefault();
|
|
27
|
-
fire(
|
|
28
|
-
$action,
|
|
29
|
-
$dispatch,
|
|
30
|
-
collectFormValuesFromEvent(event),
|
|
31
|
-
event.currentTarget,
|
|
32
|
-
);
|
|
33
|
-
}}
|
|
34
|
-
>
|
|
35
|
-
{children}
|
|
36
|
-
</form>
|
|
37
|
-
),
|
|
38
|
-
},
|
|
39
|
-
} satisfies GenerativeUILibrary;
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { factTrend, formatValue } from "./formatValue";
|
|
3
|
-
|
|
4
|
-
afterEach(() => {
|
|
5
|
-
vi.restoreAllMocks();
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
describe("formatValue", () => {
|
|
9
|
-
it.each(["0", "0%", "+0.0%", "−0", "$0"])(
|
|
10
|
-
"infers %s as a flat fact delta",
|
|
11
|
-
(delta) => {
|
|
12
|
-
expect(factTrend(delta, undefined)).toBe("flat");
|
|
13
|
-
},
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
it("formats numbers, currency, percentages, and dates", () => {
|
|
17
|
-
expect(formatValue(1234.567, { kind: "number", decimals: 2 })).toBe(
|
|
18
|
-
"1,234.57",
|
|
19
|
-
);
|
|
20
|
-
expect(
|
|
21
|
-
formatValue(1234.5, { kind: "currency", currency: "USD", decimals: 2 }),
|
|
22
|
-
).toBe("$1,234.50");
|
|
23
|
-
expect(formatValue(0.1234, { kind: "percent", decimals: 1 })).toBe("12.3%");
|
|
24
|
-
expect(formatValue("2024-01-02", { kind: "date" })).toBe("Jan 2, 2024");
|
|
25
|
-
expect(formatValue(0, { kind: "date" })).toBe(
|
|
26
|
-
new Intl.DateTimeFormat("en-US", { dateStyle: "medium" }).format(0),
|
|
27
|
-
);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("keeps a date-only value on its calendar day west of UTC", () => {
|
|
31
|
-
const DateTimeFormat = Intl.DateTimeFormat;
|
|
32
|
-
vi.spyOn(Intl, "DateTimeFormat").mockImplementation(
|
|
33
|
-
function (locales, options) {
|
|
34
|
-
return new DateTimeFormat(locales, {
|
|
35
|
-
timeZone: "America/Los_Angeles",
|
|
36
|
-
...options,
|
|
37
|
-
});
|
|
38
|
-
},
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
expect(formatValue("2026-09-26", { kind: "date" })).toBe("Sep 26, 2026");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("keeps impossible ISO calendar dates as raw text", () => {
|
|
45
|
-
expect(formatValue("2024-02-30", { kind: "date" })).toBe("2024-02-30");
|
|
46
|
-
expect(formatValue("2023-02-29", { kind: "date" })).toBe("2023-02-29");
|
|
47
|
-
expect(formatValue("2024-04-31", { kind: "date" })).toBe("2024-04-31");
|
|
48
|
-
expect(formatValue("2024-02-29", { kind: "date" })).toBe("Feb 29, 2024");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("clamps decimal precision", () => {
|
|
52
|
-
expect(formatValue(1.234, { kind: "number", decimals: -2 })).toBe("1");
|
|
53
|
-
expect(formatValue(1.234, { kind: "number", decimals: 30 })).toBe(
|
|
54
|
-
new Intl.NumberFormat("en-US", {
|
|
55
|
-
minimumFractionDigits: 20,
|
|
56
|
-
maximumFractionDigits: 20,
|
|
57
|
-
}).format(1.234),
|
|
58
|
-
);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("keeps wrong-typed values and invalid formats as raw text", () => {
|
|
62
|
-
expect(formatValue("1234", { kind: "number" })).toBe("1234");
|
|
63
|
-
expect(formatValue("0.2", { kind: "percent" })).toBe("0.2");
|
|
64
|
-
expect(formatValue("not a date", { kind: "date" })).toBe("not a date");
|
|
65
|
-
expect(() =>
|
|
66
|
-
formatValue(12, { kind: "currency", currency: "not-a-currency" }),
|
|
67
|
-
).not.toThrow();
|
|
68
|
-
expect(
|
|
69
|
-
formatValue(12, { kind: "currency", currency: "not-a-currency" }),
|
|
70
|
-
).toBe("12");
|
|
71
|
-
});
|
|
72
|
-
});
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
type TableFormat = {
|
|
2
|
-
kind: "number" | "currency" | "percent" | "date";
|
|
3
|
-
currency?: string;
|
|
4
|
-
decimals?: number;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
8
|
-
|
|
9
|
-
const ISO_DATE_PATTERN =
|
|
10
|
-
/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
11
|
-
|
|
12
|
-
type FactTrend = "up" | "down" | "flat";
|
|
13
|
-
|
|
14
|
-
const toRawText = (value: unknown): string =>
|
|
15
|
-
typeof value === "string" ||
|
|
16
|
-
typeof value === "number" ||
|
|
17
|
-
typeof value === "boolean"
|
|
18
|
-
? String(value)
|
|
19
|
-
: "";
|
|
20
|
-
|
|
21
|
-
const isTableFormat = (format: unknown): format is TableFormat =>
|
|
22
|
-
format !== null &&
|
|
23
|
-
typeof format === "object" &&
|
|
24
|
-
"kind" in format &&
|
|
25
|
-
(format.kind === "number" ||
|
|
26
|
-
format.kind === "currency" ||
|
|
27
|
-
format.kind === "percent" ||
|
|
28
|
-
format.kind === "date");
|
|
29
|
-
|
|
30
|
-
const fractionDigits = (decimals: unknown): number | undefined => {
|
|
31
|
-
if (typeof decimals !== "number" || !Number.isFinite(decimals)) {
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
return Math.min(20, Math.max(0, Math.trunc(decimals)));
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const isCalendarDate = (value: string): boolean => {
|
|
38
|
-
const [year, month, day] = value.slice(0, 10).split("-").map(Number);
|
|
39
|
-
if (year === undefined || month === undefined || day === undefined) {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
const daysInMonth =
|
|
43
|
-
month === 2
|
|
44
|
-
? year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
|
|
45
|
-
? 29
|
|
46
|
-
: 28
|
|
47
|
-
: [4, 6, 9, 11].includes(month)
|
|
48
|
-
? 30
|
|
49
|
-
: 31;
|
|
50
|
-
return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const dateFromValue = (value: unknown): Date | undefined => {
|
|
54
|
-
if (typeof value === "number") {
|
|
55
|
-
const date = new Date(value);
|
|
56
|
-
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
57
|
-
}
|
|
58
|
-
if (typeof value !== "string" || !ISO_DATE_PATTERN.test(value)) {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
if (!isCalendarDate(value)) return undefined;
|
|
62
|
-
const date = new Date(value);
|
|
63
|
-
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export const factTrend = (delta: unknown, trend: unknown): FactTrend => {
|
|
67
|
-
if (trend === "down" || trend === "flat" || trend === "up") return trend;
|
|
68
|
-
if (typeof delta === "string" && /\d/.test(delta) && !/[1-9]/.test(delta)) {
|
|
69
|
-
return "flat";
|
|
70
|
-
}
|
|
71
|
-
return typeof delta === "string" && /^[-\u2212]/.test(delta) ? "down" : "up";
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export const formatFactDelta = (delta: string, trend: FactTrend): string => {
|
|
75
|
-
const arrow = trend === "up" ? "↑" : trend === "down" ? "↓" : "→";
|
|
76
|
-
const text =
|
|
77
|
-
trend === "up"
|
|
78
|
-
? delta.replace(/^\+/, "")
|
|
79
|
-
: trend === "down"
|
|
80
|
-
? delta.replace(/^[-\u2212]/, "")
|
|
81
|
-
: delta;
|
|
82
|
-
return `${arrow} ${text}`;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
export const isNumericTableFormat = (format: unknown): boolean =>
|
|
86
|
-
isTableFormat(format) && format.kind !== "date";
|
|
87
|
-
|
|
88
|
-
export const formatValue = (value: unknown, format: unknown): string => {
|
|
89
|
-
const raw = toRawText(value);
|
|
90
|
-
try {
|
|
91
|
-
if (!isTableFormat(format)) return raw;
|
|
92
|
-
|
|
93
|
-
if (format.kind === "date") {
|
|
94
|
-
const date = dateFromValue(value);
|
|
95
|
-
return date === undefined
|
|
96
|
-
? raw
|
|
97
|
-
: new Intl.DateTimeFormat("en-US", {
|
|
98
|
-
dateStyle: "medium",
|
|
99
|
-
...(typeof value === "string" && DATE_ONLY_PATTERN.test(value)
|
|
100
|
-
? { timeZone: "UTC" }
|
|
101
|
-
: {}),
|
|
102
|
-
}).format(date);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (typeof value !== "number") return raw;
|
|
106
|
-
|
|
107
|
-
const decimals = fractionDigits(format.decimals);
|
|
108
|
-
const digitOptions =
|
|
109
|
-
decimals === undefined
|
|
110
|
-
? {}
|
|
111
|
-
: { minimumFractionDigits: decimals, maximumFractionDigits: decimals };
|
|
112
|
-
if (format.kind === "currency") {
|
|
113
|
-
if (typeof format.currency !== "string") return raw;
|
|
114
|
-
return new Intl.NumberFormat("en-US", {
|
|
115
|
-
...digitOptions,
|
|
116
|
-
style: "currency",
|
|
117
|
-
currency: format.currency,
|
|
118
|
-
}).format(value);
|
|
119
|
-
}
|
|
120
|
-
return new Intl.NumberFormat("en-US", {
|
|
121
|
-
...digitOptions,
|
|
122
|
-
...(format.kind === "percent" ? { style: "percent" } : {}),
|
|
123
|
-
}).format(value);
|
|
124
|
-
} catch {
|
|
125
|
-
return raw;
|
|
126
|
-
}
|
|
127
|
-
};
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
-
import { renderGenerativeUI } from "../renderGenerativeUI";
|
|
4
|
-
import { buildPresentParameters } from "../buildPresentParameters";
|
|
5
|
-
import { iconVocabulary } from "./icon";
|
|
6
|
-
import { ICON_NAMES } from "../ir";
|
|
7
|
-
|
|
8
|
-
const render = (node: unknown) =>
|
|
9
|
-
renderToStaticMarkup(<>{renderGenerativeUI(node, iconVocabulary)}</>);
|
|
10
|
-
|
|
11
|
-
describe("iconVocabulary", () => {
|
|
12
|
-
it("a known name renders an svg with the data-aui hooks and default size", () => {
|
|
13
|
-
const html = render({ $type: "Icon", name: "check" });
|
|
14
|
-
expect(html).toContain('data-aui="icon"');
|
|
15
|
-
expect(html).toContain('data-aui-name="check"');
|
|
16
|
-
expect(html).toContain('viewBox="0 0 24 24"');
|
|
17
|
-
expect(html).toContain('width="16"');
|
|
18
|
-
expect(html).toContain('height="16"');
|
|
19
|
-
expect(html).toContain('aria-hidden="true"');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("every built-in name renders without throwing and without a data-aui-size attribute when size is omitted", () => {
|
|
23
|
-
for (const name of ICON_NAMES) {
|
|
24
|
-
const html = render({ $type: "Icon", name });
|
|
25
|
-
expect(html, name).toContain("<svg");
|
|
26
|
-
expect(html, name).not.toContain("data-aui-size=");
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("maps size tokens to pixel dimensions", () => {
|
|
31
|
-
expect(render({ $type: "Icon", name: "star", size: "sm" })).toContain(
|
|
32
|
-
'width="14"',
|
|
33
|
-
);
|
|
34
|
-
expect(render({ $type: "Icon", name: "star", size: "md" })).toContain(
|
|
35
|
-
'width="16"',
|
|
36
|
-
);
|
|
37
|
-
expect(render({ $type: "Icon", name: "star", size: "lg" })).toContain(
|
|
38
|
-
'width="20"',
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("emits the size token as a data-aui-size hook when given", () => {
|
|
43
|
-
expect(render({ $type: "Icon", name: "bell", size: "lg" })).toContain(
|
|
44
|
-
'data-aui-size="lg"',
|
|
45
|
-
);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("an unknown name renders nothing rather than throwing", () => {
|
|
49
|
-
expect(() =>
|
|
50
|
-
render({ $type: "Icon", name: "not-a-real-icon" }),
|
|
51
|
-
).not.toThrow();
|
|
52
|
-
expect(render({ $type: "Icon", name: "not-a-real-icon" })).toBe("");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("exposes the closed name enum in buildPresentParameters", () => {
|
|
56
|
-
const schema = buildPresentParameters(iconVocabulary) as Record<
|
|
57
|
-
string,
|
|
58
|
-
unknown
|
|
59
|
-
>;
|
|
60
|
-
const properties = schema["properties"] as Record<string, unknown>;
|
|
61
|
-
const nameProp = properties["name"] as { enum?: unknown[] };
|
|
62
|
-
expect(nameProp.enum).toEqual([...ICON_NAMES]);
|
|
63
|
-
});
|
|
64
|
-
});
|