@cynodia/axiom-runtime 0.4.0-alpha.1 → 0.5.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/dom.d.ts +17 -0
- package/dist/format.d.ts +8 -0
- package/dist/format.js +122 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/memory-host.d.ts +3 -1
- package/dist/memory-host.js +5 -0
- package/dist/mutation/transaction.d.ts +6 -0
- package/dist/mutation/transaction.js +1 -0
- package/dist/mutation/values.d.ts +16 -1
- package/dist/mutation/values.js +40 -6
- package/dist/presentation-classes.d.ts +18 -0
- package/dist/presentation-classes.js +147 -0
- package/dist/runtime.d.ts +9 -1
- package/dist/runtime.js +448 -141
- package/dist/source.js +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,6 +9,10 @@ The domain-independent runtime: the state store, expression evaluation, the muta
|
|
|
9
9
|
engine, constraint checking, the semantic UI renderer and routing. It takes its whole
|
|
10
10
|
environment through a `HostEnvironment`, so it runs in a browser or headlessly.
|
|
11
11
|
|
|
12
|
+
The renderer reads presentation that the compiler has already resolved and turns it into
|
|
13
|
+
semantic class names, landmark and heading elements, and formatted values. It writes no
|
|
14
|
+
styles and computes no lengths — what those classes mean is the theme's business.
|
|
15
|
+
|
|
12
16
|
## Installation
|
|
13
17
|
|
|
14
18
|
```bash
|
package/dist/dom.d.ts
CHANGED
|
@@ -27,6 +27,21 @@ export interface StorageAdapter {
|
|
|
27
27
|
read(key: string): string | null;
|
|
28
28
|
write(key: string, value: string): void;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* A confirmation, described rather than drawn. A host may present it however its platform
|
|
32
|
+
* presents such things; nothing here is browser-specific, and the graph never constructs
|
|
33
|
+
* dialog markup.
|
|
34
|
+
*/
|
|
35
|
+
export interface ConfirmationRequest {
|
|
36
|
+
actionId: string;
|
|
37
|
+
title: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
confirmLabel: string;
|
|
40
|
+
cancelLabel: string;
|
|
41
|
+
severity: 'informational' | 'warning' | 'destructive';
|
|
42
|
+
/** A single line for hosts that can only ask a plain question. */
|
|
43
|
+
message: string;
|
|
44
|
+
}
|
|
30
45
|
/** Everything the runtime needs from its environment, so nothing is read from globals. */
|
|
31
46
|
export interface HostEnvironment {
|
|
32
47
|
document: DomDocument;
|
|
@@ -34,6 +49,8 @@ export interface HostEnvironment {
|
|
|
34
49
|
pushPath(path: string): void;
|
|
35
50
|
onPathChange(listener: () => void): void;
|
|
36
51
|
confirm(message: string): boolean;
|
|
52
|
+
/** Preferred over `confirm` when the host can present a structured confirmation. */
|
|
53
|
+
confirmRequest?(request: ConfirmationRequest): boolean;
|
|
37
54
|
now(): string;
|
|
38
55
|
uuid(): string;
|
|
39
56
|
storage?: StorageAdapter;
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ValueFormat } from '@cynodia/axiom-core';
|
|
2
|
+
/**
|
|
3
|
+
* Renders a value for display. A value the format cannot describe — a date field holding
|
|
4
|
+
* something that is not a date — falls back to its plain text rather than inventing a
|
|
5
|
+
* plausible-looking result.
|
|
6
|
+
*/
|
|
7
|
+
export declare function formatValue(value: unknown, format: ValueFormat | undefined, locale?: string): string;
|
|
8
|
+
//# sourceMappingURL=format.d.ts.map
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { isPresent, toText } from './mutation/values.js';
|
|
2
|
+
/**
|
|
3
|
+
* Semantic value formatting. Formatting is presentation: the stored value never changes,
|
|
4
|
+
* and the graph describes what it wants with a structured `ValueFormat` rather than a
|
|
5
|
+
* function, so the description survives serialization and can be reasoned about.
|
|
6
|
+
*/
|
|
7
|
+
const DATE_STYLES = {
|
|
8
|
+
short: 'short',
|
|
9
|
+
medium: 'medium',
|
|
10
|
+
long: 'long',
|
|
11
|
+
};
|
|
12
|
+
function numberFormat(locale, options) {
|
|
13
|
+
const intl = globalThis.Intl;
|
|
14
|
+
if (!intl?.NumberFormat) {
|
|
15
|
+
return (value) => String(value);
|
|
16
|
+
}
|
|
17
|
+
const formatter = new intl.NumberFormat(locale, options);
|
|
18
|
+
return (value) => formatter.format(value);
|
|
19
|
+
}
|
|
20
|
+
function asNumber(value) {
|
|
21
|
+
if (typeof value === 'number') {
|
|
22
|
+
return Number.isFinite(value) ? value : null;
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
25
|
+
const parsed = Number(value);
|
|
26
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function asDate(value) {
|
|
31
|
+
if (value instanceof Date) {
|
|
32
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
35
|
+
const parsed = new Date(value);
|
|
36
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function formatDate(value, locale, style, withTime) {
|
|
41
|
+
const date = asDate(value);
|
|
42
|
+
if (!date) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const intl = globalThis.Intl;
|
|
46
|
+
if (!intl?.DateTimeFormat) {
|
|
47
|
+
return date.toISOString();
|
|
48
|
+
}
|
|
49
|
+
const dateStyle = DATE_STYLES[style ?? 'medium'] ?? 'medium';
|
|
50
|
+
const options = withTime
|
|
51
|
+
? { dateStyle, timeStyle: dateStyle === 'long' ? 'medium' : 'short' }
|
|
52
|
+
: { dateStyle };
|
|
53
|
+
return new intl.DateTimeFormat(locale, options).format(date);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Renders a value for display. A value the format cannot describe — a date field holding
|
|
57
|
+
* something that is not a date — falls back to its plain text rather than inventing a
|
|
58
|
+
* plausible-looking result.
|
|
59
|
+
*/
|
|
60
|
+
export function formatValue(value, format, locale = 'en-US') {
|
|
61
|
+
if (!format) {
|
|
62
|
+
return toText(value);
|
|
63
|
+
}
|
|
64
|
+
if (format.kind !== 'boolean' && !isPresent(value)) {
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
switch (format.kind) {
|
|
68
|
+
case 'text':
|
|
69
|
+
return toText(value);
|
|
70
|
+
case 'number': {
|
|
71
|
+
const numeric = asNumber(value);
|
|
72
|
+
if (numeric === null) {
|
|
73
|
+
return toText(value);
|
|
74
|
+
}
|
|
75
|
+
return numberFormat(locale, {
|
|
76
|
+
useGrouping: format.grouping !== false,
|
|
77
|
+
...(format.decimals === undefined
|
|
78
|
+
? {}
|
|
79
|
+
: { minimumFractionDigits: format.decimals, maximumFractionDigits: format.decimals }),
|
|
80
|
+
})(numeric);
|
|
81
|
+
}
|
|
82
|
+
case 'currency': {
|
|
83
|
+
const numeric = asNumber(value);
|
|
84
|
+
if (numeric === null) {
|
|
85
|
+
return toText(value);
|
|
86
|
+
}
|
|
87
|
+
return numberFormat(locale, {
|
|
88
|
+
style: 'currency',
|
|
89
|
+
currency: format.currency,
|
|
90
|
+
...(format.decimals === undefined
|
|
91
|
+
? {}
|
|
92
|
+
: { minimumFractionDigits: format.decimals, maximumFractionDigits: format.decimals }),
|
|
93
|
+
})(numeric);
|
|
94
|
+
}
|
|
95
|
+
case 'percentage': {
|
|
96
|
+
const numeric = asNumber(value);
|
|
97
|
+
if (numeric === null) {
|
|
98
|
+
return toText(value);
|
|
99
|
+
}
|
|
100
|
+
// A percentage may be stored as a fraction or as a whole number of percent.
|
|
101
|
+
const fraction = format.scale === 'percent' ? numeric / 100 : numeric;
|
|
102
|
+
return numberFormat(locale, {
|
|
103
|
+
style: 'percent',
|
|
104
|
+
...(format.decimals === undefined
|
|
105
|
+
? {}
|
|
106
|
+
: { minimumFractionDigits: format.decimals, maximumFractionDigits: format.decimals }),
|
|
107
|
+
})(fraction);
|
|
108
|
+
}
|
|
109
|
+
case 'boolean': {
|
|
110
|
+
if (!isPresent(value)) {
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
return value ? format.trueLabel ?? 'Yes' : format.falseLabel ?? 'No';
|
|
114
|
+
}
|
|
115
|
+
case 'date':
|
|
116
|
+
return formatDate(value, locale, format.style, false) ?? toText(value);
|
|
117
|
+
case 'datetime':
|
|
118
|
+
return formatDate(value, locale, format.style, true) ?? toText(value);
|
|
119
|
+
default:
|
|
120
|
+
return toText(value);
|
|
121
|
+
}
|
|
122
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export * from './mutation/store.js';
|
|
|
4
4
|
export * from './mutation/transaction.js';
|
|
5
5
|
export * from './mutation/resolve-location.js';
|
|
6
6
|
export * from './mutation/mutation-engine.js';
|
|
7
|
+
export * from './format.js';
|
|
8
|
+
export * from './presentation-classes.js';
|
|
7
9
|
export * from './runtime.js';
|
|
8
10
|
export * from './memory-host.js';
|
|
9
11
|
export * from './source.js';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ export * from './mutation/store.js';
|
|
|
4
4
|
export * from './mutation/transaction.js';
|
|
5
5
|
export * from './mutation/resolve-location.js';
|
|
6
6
|
export * from './mutation/mutation-engine.js';
|
|
7
|
+
export * from './format.js';
|
|
8
|
+
export * from './presentation-classes.js';
|
|
7
9
|
export * from './runtime.js';
|
|
8
10
|
export * from './memory-host.js';
|
|
9
11
|
export * from './source.js';
|
package/dist/memory-host.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DomDocument, DomElement, DomEvent, DomListener, HostEnvironment, StorageAdapter } from './dom.js';
|
|
1
|
+
import type { ConfirmationRequest, DomDocument, DomElement, DomEvent, DomListener, HostEnvironment, StorageAdapter } from './dom.js';
|
|
2
2
|
/**
|
|
3
3
|
* An in-memory DOM and host. The runtime never touches browser globals directly, so the
|
|
4
4
|
* same renderer that drives a real page can be driven headlessly in tests.
|
|
@@ -36,6 +36,8 @@ export interface MemoryHost extends HostEnvironment {
|
|
|
36
36
|
path: string;
|
|
37
37
|
reports: string[];
|
|
38
38
|
confirmations: string[];
|
|
39
|
+
/** The structured confirmations asked for, so a test can assert what was presented. */
|
|
40
|
+
confirmationRequests: ConfirmationRequest[];
|
|
39
41
|
storage?: StorageAdapter;
|
|
40
42
|
}
|
|
41
43
|
export declare function createMemoryHost(options?: MemoryHostOptions): MemoryHost;
|
package/dist/memory-host.js
CHANGED
|
@@ -61,6 +61,7 @@ export function createMemoryHost(options = {}) {
|
|
|
61
61
|
path: options.path ?? '/',
|
|
62
62
|
reports: [],
|
|
63
63
|
confirmations: [],
|
|
64
|
+
confirmationRequests: [],
|
|
64
65
|
document: new MemoryDocument(),
|
|
65
66
|
getPath: () => host.path,
|
|
66
67
|
pushPath: (next) => {
|
|
@@ -73,6 +74,10 @@ export function createMemoryHost(options = {}) {
|
|
|
73
74
|
host.confirmations.push(message);
|
|
74
75
|
return typeof confirmResult === 'function' ? confirmResult() : confirmResult;
|
|
75
76
|
},
|
|
77
|
+
confirmRequest: (request) => {
|
|
78
|
+
host.confirmationRequests.push(request);
|
|
79
|
+
return host.confirm(request.message);
|
|
80
|
+
},
|
|
76
81
|
now: () => {
|
|
77
82
|
counter += 1;
|
|
78
83
|
return `2026-01-01T00:00:${String(counter).padStart(2, '0')}.000Z`;
|
|
@@ -16,6 +16,12 @@ export interface RuntimeTransaction {
|
|
|
16
16
|
export interface TransactionManager {
|
|
17
17
|
begin(): RuntimeTransaction;
|
|
18
18
|
currentId(): string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Committed state as it was immediately before the outermost open transaction began —
|
|
21
|
+
* what a transition rule means by "previous". Not the previous operation, not the
|
|
22
|
+
* previous iteration.
|
|
23
|
+
*/
|
|
24
|
+
entrySnapshot(): unknown;
|
|
19
25
|
}
|
|
20
26
|
export declare function createTransactionManager(store: StoreSnapshot, nextId: () => string): TransactionManager;
|
|
21
27
|
//# sourceMappingURL=transaction.d.ts.map
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raised when an expression cannot be evaluated — a collection operator applied to
|
|
3
|
+
* something that is not a collection, an aggregation over non-numeric data, a reference
|
|
4
|
+
* that does not resolve. Failing loudly is deliberate: an expression must never return a
|
|
5
|
+
* plausible-looking value and report a failure at the same time.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ExpressionEvaluationError extends Error {
|
|
8
|
+
readonly details: Record<string, unknown>;
|
|
9
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
10
|
+
}
|
|
1
11
|
/**
|
|
2
12
|
* Value helpers shared by the runtime and the mutation subsystem.
|
|
3
13
|
*
|
|
@@ -12,8 +22,13 @@
|
|
|
12
22
|
export declare function cloneValue<T>(value: T): T;
|
|
13
23
|
export declare function deepFreeze<T>(value: T): T;
|
|
14
24
|
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
15
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Presence answers one question: does a value exist? It says nothing about whether the
|
|
27
|
+
* value is empty. An empty collection, an empty string, zero and false are all present.
|
|
28
|
+
*/
|
|
16
29
|
export declare function isPresent(value: unknown): boolean;
|
|
30
|
+
/** Emptiness of a collection or a string. Anything else is never empty. */
|
|
31
|
+
export declare function isEmptyValue(value: unknown): boolean;
|
|
17
32
|
export declare function toBoolean(value: unknown): boolean;
|
|
18
33
|
export declare function toText(value: unknown): string;
|
|
19
34
|
export declare function compareValues(left: unknown, right: unknown): number;
|
package/dist/mutation/values.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raised when an expression cannot be evaluated — a collection operator applied to
|
|
3
|
+
* something that is not a collection, an aggregation over non-numeric data, a reference
|
|
4
|
+
* that does not resolve. Failing loudly is deliberate: an expression must never return a
|
|
5
|
+
* plausible-looking value and report a failure at the same time.
|
|
6
|
+
*/
|
|
7
|
+
export class ExpressionEvaluationError extends Error {
|
|
8
|
+
details;
|
|
9
|
+
constructor(message, details = {}) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'ExpressionEvaluationError';
|
|
12
|
+
this.details = details;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
1
15
|
/**
|
|
2
16
|
* Value helpers shared by the runtime and the mutation subsystem.
|
|
3
17
|
*
|
|
@@ -24,18 +38,25 @@ export function deepFreeze(value) {
|
|
|
24
38
|
export function isRecord(value) {
|
|
25
39
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
26
40
|
}
|
|
27
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Presence answers one question: does a value exist? It says nothing about whether the
|
|
43
|
+
* value is empty. An empty collection, an empty string, zero and false are all present.
|
|
44
|
+
*/
|
|
28
45
|
export function isPresent(value) {
|
|
46
|
+
return value !== null && value !== undefined;
|
|
47
|
+
}
|
|
48
|
+
/** Emptiness of a collection or a string. Anything else is never empty. */
|
|
49
|
+
export function isEmptyValue(value) {
|
|
29
50
|
if (value === null || value === undefined) {
|
|
30
|
-
return
|
|
51
|
+
return true;
|
|
31
52
|
}
|
|
32
53
|
if (typeof value === 'string') {
|
|
33
|
-
return value.trim().length
|
|
54
|
+
return value.trim().length === 0;
|
|
34
55
|
}
|
|
35
56
|
if (Array.isArray(value)) {
|
|
36
|
-
return value.length
|
|
57
|
+
return value.length === 0;
|
|
37
58
|
}
|
|
38
|
-
return
|
|
59
|
+
return false;
|
|
39
60
|
}
|
|
40
61
|
export function toBoolean(value) {
|
|
41
62
|
if (Array.isArray(value)) {
|
|
@@ -63,6 +84,19 @@ export function compareValues(left, right) {
|
|
|
63
84
|
const rightText = toText(right);
|
|
64
85
|
return leftText === rightText ? 0 : leftText < rightText ? -1 : 1;
|
|
65
86
|
}
|
|
87
|
+
/** A stable serialization, so record comparison does not depend on key order. */
|
|
88
|
+
function canonical(value) {
|
|
89
|
+
if (value === null || typeof value !== 'object') {
|
|
90
|
+
return JSON.stringify(value) ?? 'null';
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
94
|
+
}
|
|
95
|
+
const entries = Object.entries(value)
|
|
96
|
+
.filter(([, entry]) => entry !== undefined)
|
|
97
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
98
|
+
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonical(entry)}`).join(',')}}`;
|
|
99
|
+
}
|
|
66
100
|
export function valuesEqual(left, right) {
|
|
67
101
|
if (left === right) {
|
|
68
102
|
return true;
|
|
@@ -71,7 +105,7 @@ export function valuesEqual(left, right) {
|
|
|
71
105
|
return (left ?? null) === (right ?? null);
|
|
72
106
|
}
|
|
73
107
|
if (typeof left === 'object' || typeof right === 'object') {
|
|
74
|
-
return
|
|
108
|
+
return canonical(left) === canonical(right);
|
|
75
109
|
}
|
|
76
110
|
return false;
|
|
77
111
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ResolvedPresentation, TextRole, UxRole } from '@cynodia/axiom-core';
|
|
2
|
+
/** Every class a node's resolved presentation implies, in a stable order. */
|
|
3
|
+
export declare function presentationClassList(resolved: ResolvedPresentation | undefined): string[];
|
|
4
|
+
export declare function presentationClasses(resolved: ResolvedPresentation | undefined, ...extra: string[]): string;
|
|
5
|
+
/**
|
|
6
|
+
* Landmarks. A UX role that names a region of the page becomes the element that means
|
|
7
|
+
* that region, so assistive technology gets the structure the graph declared.
|
|
8
|
+
*/
|
|
9
|
+
export declare function landmarkTag(uxRole: UxRole | undefined): string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Headings are real headings, so the document has an outline rather than a set of large
|
|
12
|
+
* words. The three heading text roles are three levels: an application title, a title
|
|
13
|
+
* within it, and a section heading under that.
|
|
14
|
+
*/
|
|
15
|
+
export declare function headingTag(textRole: TextRole | undefined): string | undefined;
|
|
16
|
+
/** The ARIA role a UX role implies, where one is warranted. */
|
|
17
|
+
export declare function ariaRoleFor(uxRole: UxRole | undefined): string | undefined;
|
|
18
|
+
//# sourceMappingURL=presentation-classes.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The web renderer's translation of resolved presentation into class names.
|
|
3
|
+
*
|
|
4
|
+
* The renderer emits semantic classes and nothing else — no inline styles, no computed
|
|
5
|
+
* lengths, no colours. What those classes mean is decided by the generated stylesheet, so
|
|
6
|
+
* the same resolved presentation can drive an entirely different renderer.
|
|
7
|
+
*/
|
|
8
|
+
function layoutClasses(prefix, layout) {
|
|
9
|
+
const classes = [`axiom-${prefix}layout-${layout.kind}`, `axiom-${prefix}gap-${layout.gap}`];
|
|
10
|
+
if (!prefix) {
|
|
11
|
+
classes.push(`axiom-align-${layout.align}`, `axiom-justify-${layout.justify}`);
|
|
12
|
+
classes.push(layout.wrap ? 'axiom-wrap' : 'axiom-nowrap');
|
|
13
|
+
if (layout.columns !== undefined) {
|
|
14
|
+
classes.push(typeof layout.columns === 'number'
|
|
15
|
+
? `axiom-columns-${layout.columns}`
|
|
16
|
+
: `axiom-columns-adaptive-${layout.columns.minimum}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return classes;
|
|
20
|
+
}
|
|
21
|
+
function sizingClasses(prefix, sizing) {
|
|
22
|
+
const classes = [`axiom-${prefix}width-${sizing.width}`];
|
|
23
|
+
if (!prefix) {
|
|
24
|
+
classes.push(`axiom-height-${sizing.height}`);
|
|
25
|
+
if (sizing.minWidth) {
|
|
26
|
+
classes.push(`axiom-minwidth-${sizing.minWidth}`);
|
|
27
|
+
}
|
|
28
|
+
if (sizing.maxWidth) {
|
|
29
|
+
classes.push(`axiom-maxwidth-${sizing.maxWidth}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return classes;
|
|
33
|
+
}
|
|
34
|
+
function paddingClasses(prefix, padding) {
|
|
35
|
+
return [`axiom-${prefix}pad-x-${padding.horizontal}`, `axiom-${prefix}pad-y-${padding.vertical}`];
|
|
36
|
+
}
|
|
37
|
+
function responsiveClasses(device, override) {
|
|
38
|
+
const prefix = `${device}-`;
|
|
39
|
+
const classes = [];
|
|
40
|
+
if (override.hidden) {
|
|
41
|
+
classes.push(`axiom-${prefix}hidden`);
|
|
42
|
+
}
|
|
43
|
+
if (override.layout) {
|
|
44
|
+
classes.push(...layoutClasses(prefix, override.layout));
|
|
45
|
+
}
|
|
46
|
+
if (override.sizing) {
|
|
47
|
+
classes.push(...sizingClasses(prefix, override.sizing));
|
|
48
|
+
}
|
|
49
|
+
if (override.padding) {
|
|
50
|
+
classes.push(...paddingClasses(prefix, override.padding));
|
|
51
|
+
}
|
|
52
|
+
if (override.density) {
|
|
53
|
+
classes.push(`axiom-${prefix}density-${override.density}`);
|
|
54
|
+
}
|
|
55
|
+
return classes;
|
|
56
|
+
}
|
|
57
|
+
/** Every class a node's resolved presentation implies, in a stable order. */
|
|
58
|
+
export function presentationClassList(resolved) {
|
|
59
|
+
if (!resolved) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const classes = [
|
|
63
|
+
`axiom-density-${resolved.density}`,
|
|
64
|
+
`axiom-emphasis-${resolved.emphasis}`,
|
|
65
|
+
`axiom-surface-${resolved.surface}`,
|
|
66
|
+
`axiom-text-${resolved.textRole}`,
|
|
67
|
+
`axiom-treatment-${resolved.treatment}`,
|
|
68
|
+
];
|
|
69
|
+
if (resolved.role) {
|
|
70
|
+
classes.push(`axiom-role-${resolved.role}`);
|
|
71
|
+
}
|
|
72
|
+
if (resolved.uxRole) {
|
|
73
|
+
classes.push(`axiom-ux-${resolved.uxRole}`);
|
|
74
|
+
}
|
|
75
|
+
classes.push(...layoutClasses('', resolved.layout));
|
|
76
|
+
classes.push(...sizingClasses('', resolved.sizing));
|
|
77
|
+
classes.push(...paddingClasses('', resolved.padding));
|
|
78
|
+
for (const device of ['compact', 'regular', 'wide']) {
|
|
79
|
+
const override = resolved.responsive[device];
|
|
80
|
+
if (override) {
|
|
81
|
+
classes.push(...responsiveClasses(device, override));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// The escape hatch: a renderer-specific class, attached and otherwise not understood.
|
|
85
|
+
const override = resolved.rendererOverrides?.web;
|
|
86
|
+
if (override && typeof override.className === 'string' && override.className.trim()) {
|
|
87
|
+
classes.push('axiom-opaque', ...override.className.trim().split(/\s+/));
|
|
88
|
+
}
|
|
89
|
+
return classes;
|
|
90
|
+
}
|
|
91
|
+
export function presentationClasses(resolved, ...extra) {
|
|
92
|
+
return [...extra, ...presentationClassList(resolved)].filter(Boolean).join(' ');
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Landmarks. A UX role that names a region of the page becomes the element that means
|
|
96
|
+
* that region, so assistive technology gets the structure the graph declared.
|
|
97
|
+
*/
|
|
98
|
+
export function landmarkTag(uxRole) {
|
|
99
|
+
switch (uxRole) {
|
|
100
|
+
case 'header-region':
|
|
101
|
+
return 'header';
|
|
102
|
+
case 'footer-region':
|
|
103
|
+
return 'footer';
|
|
104
|
+
case 'navigation-group':
|
|
105
|
+
return 'nav';
|
|
106
|
+
case 'content-region':
|
|
107
|
+
return 'main';
|
|
108
|
+
case 'sidebar':
|
|
109
|
+
return 'aside';
|
|
110
|
+
case 'form-section':
|
|
111
|
+
return 'section';
|
|
112
|
+
default:
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Headings are real headings, so the document has an outline rather than a set of large
|
|
118
|
+
* words. The three heading text roles are three levels: an application title, a title
|
|
119
|
+
* within it, and a section heading under that.
|
|
120
|
+
*/
|
|
121
|
+
export function headingTag(textRole) {
|
|
122
|
+
switch (textRole) {
|
|
123
|
+
case 'display':
|
|
124
|
+
return 'h1';
|
|
125
|
+
case 'title':
|
|
126
|
+
return 'h2';
|
|
127
|
+
case 'heading':
|
|
128
|
+
return 'h3';
|
|
129
|
+
default:
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** The ARIA role a UX role implies, where one is warranted. */
|
|
134
|
+
export function ariaRoleFor(uxRole) {
|
|
135
|
+
switch (uxRole) {
|
|
136
|
+
case 'toolbar':
|
|
137
|
+
return 'toolbar';
|
|
138
|
+
case 'error-state':
|
|
139
|
+
return 'alert';
|
|
140
|
+
case 'warning-state':
|
|
141
|
+
case 'success-state':
|
|
142
|
+
case 'informational-state':
|
|
143
|
+
return 'status';
|
|
144
|
+
default:
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare const RUNTIME_DIAGNOSTIC_CODES: {
|
|
|
20
20
|
readonly MUTATION_FAILED: "MUTATION_FAILED";
|
|
21
21
|
readonly DERIVED_STATE_WRITE: "DERIVED_STATE_WRITE";
|
|
22
22
|
readonly UNKNOWN_STATE: "UNKNOWN_STATE";
|
|
23
|
+
readonly TRANSITION_CONSTRAINT_VIOLATION: "TRANSITION_CONSTRAINT_VIOLATION";
|
|
23
24
|
readonly UNSUPPORTED_EXPRESSION: "UNSUPPORTED_EXPRESSION";
|
|
24
25
|
readonly UNSUPPORTED_OPERATION: "UNSUPPORTED_OPERATION";
|
|
25
26
|
readonly ROUTE_NOT_FOUND: "ROUTE_NOT_FOUND";
|
|
@@ -69,7 +70,14 @@ export interface AxiomRuntime {
|
|
|
69
70
|
start(): void;
|
|
70
71
|
render(): void;
|
|
71
72
|
getState(id: NodeId): unknown;
|
|
72
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Replaces a state value outright, for hosts, tests and seeding.
|
|
75
|
+
*
|
|
76
|
+
* This is an administrative facility, not a semantic write: it does not evaluate
|
|
77
|
+
* preconditions, entity constraints or transition constraints. Application behaviour
|
|
78
|
+
* belongs in actions and input bindings, which are governed.
|
|
79
|
+
*/
|
|
80
|
+
hydrateState(id: NodeId, value: unknown): void;
|
|
73
81
|
invokeAction(id: NodeId, args?: Record<string, unknown>): ActionResult;
|
|
74
82
|
navigate(path: string): void;
|
|
75
83
|
currentRoute(): RouteMatch | null;
|