@cynodia/axiom-compiler 0.4.1-alpha.1 → 0.5.2-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 +16 -0
- package/dist/codegen.d.ts +6 -1
- package/dist/codegen.js +5 -31
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/normalize.js +35 -1
- package/dist/stylesheet.d.ts +8 -0
- package/dist/stylesheet.js +453 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -8,6 +8,15 @@ framework.
|
|
|
8
8
|
Validates an Application Graph, normalizes it into a runtime-ready IR, and emits a
|
|
9
9
|
self-contained HTML page with the runtime inlined.
|
|
10
10
|
|
|
11
|
+
Normalization resolves presentation once — renderer defaults, theme, inheritance, semantic
|
|
12
|
+
inference, node declarations and responsive overrides — and puts the result in the IR, still
|
|
13
|
+
as roles and tokens rather than CSS. `createThemeStylesheet` is the web renderer's
|
|
14
|
+
translation of a theme into CSS custom properties and rules, and is the only place in the
|
|
15
|
+
framework that decides a role is a colour.
|
|
16
|
+
|
|
17
|
+
Main exports: `compileToIR`, `compileToHtml`, `createThemeStylesheet`,
|
|
18
|
+
`GraphValidationError`.
|
|
19
|
+
|
|
11
20
|
## Installation
|
|
12
21
|
|
|
13
22
|
```bash
|
|
@@ -20,6 +29,13 @@ Most applications should install the facade package instead, which re-exports th
|
|
|
20
29
|
npm install @cynodia/axiom@alpha
|
|
21
30
|
```
|
|
22
31
|
|
|
32
|
+
|
|
33
|
+
## Documentation
|
|
34
|
+
|
|
35
|
+
The canonical operational contract lives in the `docs/` directory of the
|
|
36
|
+
[`@cynodia/axiom`](https://www.npmjs.com/package/@cynodia/axiom) package, and in
|
|
37
|
+
[the repository](https://github.com/cynodia/axiom). Start with `docs/AGENT_REFERENCE.md`.
|
|
38
|
+
|
|
23
39
|
## License
|
|
24
40
|
|
|
25
41
|
MIT
|
package/dist/codegen.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import type { ApplicationGraph, ApplicationIR } from '@cynodia/axiom-core';
|
|
1
|
+
import type { ApplicationGraph, ApplicationIR, Appearance } from '@cynodia/axiom-core';
|
|
2
2
|
import type { CompileOptions } from './normalize.js';
|
|
3
3
|
export interface HtmlOptions extends CompileOptions {
|
|
4
4
|
title?: string;
|
|
5
|
+
/**
|
|
6
|
+
* Pins the appearance of the emitted page. Omitted, the theme decides — and a theme set
|
|
7
|
+
* to `system` follows the reader's own preference.
|
|
8
|
+
*/
|
|
9
|
+
appearance?: Appearance;
|
|
5
10
|
}
|
|
6
11
|
/**
|
|
7
12
|
* Emits a self-contained page: the normalized IR as data, plus the generic runtime. No
|
package/dist/codegen.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRuntimeModuleSource } from '@cynodia/axiom-runtime';
|
|
2
2
|
import { compileToIR } from './normalize.js';
|
|
3
|
+
import { createThemeStylesheet } from './stylesheet.js';
|
|
3
4
|
function escapeHtml(value) {
|
|
4
5
|
return value
|
|
5
6
|
.replace(/&/g, '&')
|
|
@@ -10,35 +11,6 @@ function escapeHtml(value) {
|
|
|
10
11
|
function escapeForScript(value) {
|
|
11
12
|
return value.replace(/<\/script/gi, '<\\/script');
|
|
12
13
|
}
|
|
13
|
-
/** Domain-neutral styling for the semantic UI vocabulary. */
|
|
14
|
-
const STYLESHEET = `
|
|
15
|
-
:root { color-scheme: light; font-family: Inter, system-ui, sans-serif; }
|
|
16
|
-
body { margin: 0; background: #f4f7fb; color: #1f2937; }
|
|
17
|
-
#app { padding: 24px; max-width: 1100px; margin: 0 auto; }
|
|
18
|
-
.axiom-view { display: grid; gap: 16px; }
|
|
19
|
-
.axiom-container { display: flex; gap: 12px; }
|
|
20
|
-
.axiom-layout-vertical { flex-direction: column; align-items: stretch; }
|
|
21
|
-
.axiom-layout-horizontal { flex-direction: row; align-items: center; flex-wrap: wrap; }
|
|
22
|
-
.axiom-layout-stack { flex-direction: column; gap: 4px; }
|
|
23
|
-
.axiom-view > .axiom-container,
|
|
24
|
-
.axiom-view > .axiom-form { background: #ffffff; border-radius: 16px; padding: 20px; box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08); }
|
|
25
|
-
.axiom-repeat { display: grid; gap: 10px; }
|
|
26
|
-
.axiom-field { display: flex; gap: 8px; align-items: baseline; }
|
|
27
|
-
.axiom-field-label { color: #64748b; font-size: 13px; }
|
|
28
|
-
.axiom-field-value { font-weight: 500; }
|
|
29
|
-
.axiom-form { display: grid; gap: 12px; }
|
|
30
|
-
.axiom-input { display: grid; gap: 6px; }
|
|
31
|
-
.axiom-input-label { font-size: 13px; color: #475569; }
|
|
32
|
-
.axiom-control { font: inherit; border: 1px solid #cbd5e1; border-radius: 10px; padding: 10px 12px; width: 100%; box-sizing: border-box; }
|
|
33
|
-
textarea.axiom-control { min-height: 120px; resize: vertical; }
|
|
34
|
-
input[type="checkbox"].axiom-control { width: auto; }
|
|
35
|
-
.axiom-button { font: inherit; border: none; border-radius: 10px; padding: 10px 14px; background: #2563eb; color: #ffffff; cursor: pointer; }
|
|
36
|
-
.axiom-button.axiom-destructive, .axiom-role-danger { background: #dc2626; }
|
|
37
|
-
.axiom-role-secondary { background: #64748b; }
|
|
38
|
-
.axiom-emphasis-strong { font-weight: 700; font-size: 18px; }
|
|
39
|
-
.axiom-density-compact { padding: 6px 10px; }
|
|
40
|
-
.axiom-no-route { padding: 20px; background: #ffffff; border-radius: 12px; }
|
|
41
|
-
`.trim();
|
|
42
14
|
/**
|
|
43
15
|
* Emits a self-contained page: the normalized IR as data, plus the generic runtime. No
|
|
44
16
|
* part of this output is derived from what the application is about.
|
|
@@ -47,15 +19,17 @@ export function compileIRToHtml(ir, options = {}) {
|
|
|
47
19
|
const runtimeSource = createRuntimeModuleSource();
|
|
48
20
|
const payload = escapeForScript(JSON.stringify(ir));
|
|
49
21
|
const title = options.title ?? ir.name;
|
|
22
|
+
const stylesheet = createThemeStylesheet(ir.theme);
|
|
23
|
+
const appearance = options.appearance ?? (ir.theme.appearance === 'system' ? undefined : ir.theme.appearance);
|
|
50
24
|
return [
|
|
51
25
|
'<!DOCTYPE html>',
|
|
52
|
-
|
|
26
|
+
`<html lang="en"${appearance ? ` data-axiom-appearance="${appearance}"` : ''}>`,
|
|
53
27
|
'<head>',
|
|
54
28
|
' <meta charset="utf-8" />',
|
|
55
29
|
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
56
30
|
` <title>${escapeHtml(title)}</title>`,
|
|
57
31
|
' <style>',
|
|
58
|
-
|
|
32
|
+
stylesheet,
|
|
59
33
|
' </style>',
|
|
60
34
|
'</head>',
|
|
61
35
|
'<body>',
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/normalize.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { actionGuards, inferLocationType, locationRootStateId, semanticContextFromGraph, validateGraph, } from '@cynodia/axiom-core';
|
|
1
|
+
import { actionGuards, inferExpressionType, inferLocationType, itemTypeOf, locationFieldIds, locationRootStateId, resolvePresentationMap, semanticContextFromGraph, validateGraph, } from '@cynodia/axiom-core';
|
|
2
2
|
import { isUINode } from '@cynodia/axiom-core';
|
|
3
3
|
export class GraphValidationError extends Error {
|
|
4
4
|
problems;
|
|
@@ -96,6 +96,7 @@ export function compileToIR(graph, options = {}) {
|
|
|
96
96
|
const semantics = semanticContextFromGraph(graph);
|
|
97
97
|
const locationTypes = {};
|
|
98
98
|
const locationRoots = {};
|
|
99
|
+
const locationRequired = {};
|
|
99
100
|
for (const node of Object.values(uiNodes)) {
|
|
100
101
|
if (node.kind !== 'input') {
|
|
101
102
|
continue;
|
|
@@ -105,7 +106,36 @@ export function compileToIR(graph, options = {}) {
|
|
|
105
106
|
locationTypes[node.id] = resolved;
|
|
106
107
|
}
|
|
107
108
|
locationRoots[node.id] = locationRootStateId(node.binding.location);
|
|
109
|
+
// Whether a value is required is already in the model; a renderer should not re-derive it.
|
|
110
|
+
const addressed = locationFieldIds(node.binding.location)[0];
|
|
111
|
+
locationRequired[node.id] = addressed
|
|
112
|
+
? graph.getField(addressed)?.field.required === true
|
|
113
|
+
: resolved !== undefined && resolved.kind !== 'optional';
|
|
108
114
|
}
|
|
115
|
+
// Which field distinguishes the members of each repeat, so the renderer can give every
|
|
116
|
+
// rendered instance a stable identity without inferring anything itself.
|
|
117
|
+
const repeatIdentityFields = {};
|
|
118
|
+
for (const node of Object.values(uiNodes)) {
|
|
119
|
+
if (node.kind !== 'repeat') {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const sourceType = inferExpressionType(node.source, semantics);
|
|
123
|
+
const item = itemTypeOf(sourceType);
|
|
124
|
+
const resolved = item?.kind === 'optional' ? item.valueType : item;
|
|
125
|
+
if (resolved?.kind !== 'entity') {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const identity = graph.getNode(resolved.entityId)?.identityFieldId;
|
|
129
|
+
if (identity) {
|
|
130
|
+
repeatIdentityFields[node.id] = identity;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Presentation is normalized here, once: renderer defaults, theme, inheritance,
|
|
134
|
+
// semantic inference, node declarations and responsive overrides are all decided before
|
|
135
|
+
// a renderer sees them. What lands in the IR is still semantic — roles and tokens, not
|
|
136
|
+
// CSS — so a second renderer stays possible.
|
|
137
|
+
const theme = graph.theme;
|
|
138
|
+
const presentation = resolvePresentationMap(graph.listNodes(), theme);
|
|
109
139
|
return {
|
|
110
140
|
id: graph.id,
|
|
111
141
|
name: graph.name,
|
|
@@ -122,6 +152,10 @@ export function compileToIR(graph, options = {}) {
|
|
|
122
152
|
edges: graph.semanticEdges(),
|
|
123
153
|
locationTypes,
|
|
124
154
|
locationRoots,
|
|
155
|
+
locationRequired,
|
|
156
|
+
repeatIdentityFields,
|
|
157
|
+
theme,
|
|
158
|
+
presentation,
|
|
125
159
|
};
|
|
126
160
|
}
|
|
127
161
|
export function serializeIR(ir) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Theme } from '@cynodia/axiom-core';
|
|
2
|
+
/**
|
|
3
|
+
* The complete stylesheet for a theme. It is derived entirely from theme data and the
|
|
4
|
+
* semantic class vocabulary, so a different theme is a different sheet and never a
|
|
5
|
+
* different application.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createThemeStylesheet(theme: Theme): string;
|
|
8
|
+
//# sourceMappingURL=stylesheet.d.ts.map
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { ALIGNMENTS, BOUNDED_SIZES, DENSITIES, EMPHASIS_LEVELS, JUSTIFICATIONS, LAYOUT_KINDS, PRESENTATION_ROLES, SPACING_TOKENS, SURFACE_ROLES, TEXT_ROLES, } from '@cynodia/axiom-core';
|
|
2
|
+
/**
|
|
3
|
+
* The web renderer's half of the presentation layer: it turns a theme and the semantic
|
|
4
|
+
* class vocabulary the runtime emits into CSS.
|
|
5
|
+
*
|
|
6
|
+
* Nothing here is reachable from an application graph. A graph says `role: 'destructive'`
|
|
7
|
+
* and `gap: 'medium'`; this file is the only place that decides those are a colour and a
|
|
8
|
+
* length, which is what keeps a second renderer possible.
|
|
9
|
+
*/
|
|
10
|
+
/** The maximum fixed column count the generated sheet supports. */
|
|
11
|
+
const MAX_FIXED_COLUMNS = 6;
|
|
12
|
+
const ELEVATION_SHADOWS_LIGHT = [
|
|
13
|
+
'none',
|
|
14
|
+
'0 1px 2px rgba(15, 23, 42, 0.06)',
|
|
15
|
+
'0 10px 28px rgba(15, 23, 42, 0.08)',
|
|
16
|
+
'0 24px 60px rgba(15, 23, 42, 0.14)',
|
|
17
|
+
];
|
|
18
|
+
const ELEVATION_SHADOWS_DARK = [
|
|
19
|
+
'none',
|
|
20
|
+
'0 1px 2px rgba(0, 0, 0, 0.35)',
|
|
21
|
+
'0 12px 30px rgba(0, 0, 0, 0.45)',
|
|
22
|
+
'0 26px 64px rgba(0, 0, 0, 0.55)',
|
|
23
|
+
];
|
|
24
|
+
/** Semantic alignment becomes flex/grid alignment here and nowhere else. */
|
|
25
|
+
const ALIGN_VALUES = {
|
|
26
|
+
start: 'flex-start',
|
|
27
|
+
center: 'center',
|
|
28
|
+
end: 'flex-end',
|
|
29
|
+
stretch: 'stretch',
|
|
30
|
+
};
|
|
31
|
+
const JUSTIFY_VALUES = {
|
|
32
|
+
start: 'flex-start',
|
|
33
|
+
center: 'center',
|
|
34
|
+
end: 'flex-end',
|
|
35
|
+
stretch: 'stretch',
|
|
36
|
+
between: 'space-between',
|
|
37
|
+
};
|
|
38
|
+
function kebab(name) {
|
|
39
|
+
return name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
/** A spacing token as a density-scaled length. */
|
|
42
|
+
function space(token) {
|
|
43
|
+
return token === 'none' ? '0px' : `calc(var(--axiom-space-${token}) * var(--axiom-density-scale))`;
|
|
44
|
+
}
|
|
45
|
+
function colorVariables(colors, shadows) {
|
|
46
|
+
const lines = Object.entries(colors).map(([role, value]) => ` --axiom-color-${kebab(role)}: ${value};`);
|
|
47
|
+
shadows.forEach((shadow, level) => {
|
|
48
|
+
lines.push(` --axiom-shadow-${level}: ${shadow};`);
|
|
49
|
+
});
|
|
50
|
+
return lines;
|
|
51
|
+
}
|
|
52
|
+
function appearanceBlocks(theme) {
|
|
53
|
+
const light = colorVariables(theme.colors.light, ELEVATION_SHADOWS_LIGHT).join('\n');
|
|
54
|
+
const dark = colorVariables(theme.colors.dark, ELEVATION_SHADOWS_DARK).join('\n');
|
|
55
|
+
const appearance = theme.appearance;
|
|
56
|
+
if (appearance === 'dark') {
|
|
57
|
+
return [`:root {\n color-scheme: dark;\n${dark}\n}`].join('\n');
|
|
58
|
+
}
|
|
59
|
+
const blocks = [`:root {\n color-scheme: light;\n${light}\n}`];
|
|
60
|
+
if (appearance === 'system') {
|
|
61
|
+
blocks.push(':root { color-scheme: light dark; }');
|
|
62
|
+
blocks.push(`@media (prefers-color-scheme: dark) {\n :root:not([data-axiom-appearance="light"]) {\n${dark}\n }\n}`);
|
|
63
|
+
}
|
|
64
|
+
// A host may pin an appearance without the graph changing at all.
|
|
65
|
+
blocks.push(`:root[data-axiom-appearance="dark"] {\n color-scheme: dark;\n${dark}\n}`);
|
|
66
|
+
blocks.push(`:root[data-axiom-appearance="light"] {\n color-scheme: light;\n${light}\n}`);
|
|
67
|
+
return blocks.join('\n');
|
|
68
|
+
}
|
|
69
|
+
function tokenVariables(theme) {
|
|
70
|
+
const lines = [];
|
|
71
|
+
for (const token of SPACING_TOKENS) {
|
|
72
|
+
lines.push(` --axiom-space-${token}: ${theme.spacing[token]}px;`);
|
|
73
|
+
}
|
|
74
|
+
for (const [token, value] of Object.entries(theme.radius)) {
|
|
75
|
+
lines.push(` --axiom-radius-${token}: ${value}px;`);
|
|
76
|
+
}
|
|
77
|
+
for (const size of BOUNDED_SIZES) {
|
|
78
|
+
lines.push(` --axiom-size-${size}: ${theme.sizes[size]}px;`);
|
|
79
|
+
}
|
|
80
|
+
lines.push(` --axiom-font-family: ${theme.typography.fontFamily};`);
|
|
81
|
+
lines.push(` --axiom-font-heading: ${theme.typography.headingFamily ?? theme.typography.fontFamily};`);
|
|
82
|
+
lines.push(` --axiom-font-mono: ${theme.typography.monoFamily};`);
|
|
83
|
+
lines.push(' --axiom-density-scale: 1;');
|
|
84
|
+
lines.push(' --axiom-weight-shift: 0;');
|
|
85
|
+
lines.push(` --axiom-control-height: ${theme.controls.height.comfortable}px;`);
|
|
86
|
+
lines.push(` --axiom-control-padding-x: ${theme.controls.paddingX.comfortable}px;`);
|
|
87
|
+
lines.push(` --axiom-control-radius: var(--axiom-radius-${theme.controls.radius});`);
|
|
88
|
+
lines.push(` --axiom-control-border: ${theme.controls.borderWidth}px;`);
|
|
89
|
+
lines.push(` --axiom-focus-width: ${theme.controls.focusWidth}px;`);
|
|
90
|
+
return `:root {\n${lines.join('\n')}\n}`;
|
|
91
|
+
}
|
|
92
|
+
/** Density is the one property that cascades, so it cascades as CSS variables. */
|
|
93
|
+
function densityRules(theme, prefix = '') {
|
|
94
|
+
return DENSITIES.map((density) => [
|
|
95
|
+
`.axiom-${prefix}density-${density} {`,
|
|
96
|
+
` --axiom-density-scale: ${theme.density[density].scale};`,
|
|
97
|
+
` --axiom-control-height: ${theme.controls.height[density]}px;`,
|
|
98
|
+
` --axiom-control-padding-x: ${theme.controls.paddingX[density]}px;`,
|
|
99
|
+
'}',
|
|
100
|
+
].join('\n')).join('\n');
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* One layout kind, plus how children of that layout interpret their own sizing intent.
|
|
104
|
+
* Sizing has to be scoped to the parent's direction: "fill" means grow along the main
|
|
105
|
+
* axis and stretch across the cross axis, and only the parent knows which is which.
|
|
106
|
+
*/
|
|
107
|
+
function layoutBlock(cls, kind) {
|
|
108
|
+
const rules = [];
|
|
109
|
+
switch (kind) {
|
|
110
|
+
case 'horizontal':
|
|
111
|
+
rules.push(`${cls} { display: flex; flex-direction: row; }`);
|
|
112
|
+
rules.push(`${cls} > .axiom-width-fill { flex: 1 1 0%; min-width: 0; }`);
|
|
113
|
+
rules.push(`${cls} > .axiom-width-fit { flex: 0 1 auto; min-width: 0; }`);
|
|
114
|
+
rules.push(`${cls} > .axiom-width-content { flex: 0 0 auto; }`);
|
|
115
|
+
rules.push(`${cls} > .axiom-height-fill { align-self: stretch; }`);
|
|
116
|
+
break;
|
|
117
|
+
case 'grid':
|
|
118
|
+
rules.push(`${cls} { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--axiom-size-narrow), 1fr)); }`);
|
|
119
|
+
rules.push(`${cls} > * { min-width: 0; }`);
|
|
120
|
+
break;
|
|
121
|
+
case 'stack':
|
|
122
|
+
case 'vertical':
|
|
123
|
+
default:
|
|
124
|
+
rules.push(`${cls} { display: flex; flex-direction: column; }`);
|
|
125
|
+
rules.push(`${cls} > .axiom-width-fill { align-self: stretch; }`);
|
|
126
|
+
rules.push(`${cls} > .axiom-width-fit { align-self: flex-start; max-width: 100%; }`);
|
|
127
|
+
rules.push(`${cls} > .axiom-width-content { align-self: flex-start; }`);
|
|
128
|
+
rules.push(`${cls} > .axiom-height-fill { flex: 1 1 0%; }`);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
return rules.join('\n');
|
|
132
|
+
}
|
|
133
|
+
function layoutRules(prefix = '') {
|
|
134
|
+
return LAYOUT_KINDS.map((kind) => layoutBlock(`.axiom-${prefix}layout-${kind}`, kind)).join('\n');
|
|
135
|
+
}
|
|
136
|
+
function spacingRules(prefix = '') {
|
|
137
|
+
const rules = [];
|
|
138
|
+
for (const token of SPACING_TOKENS) {
|
|
139
|
+
rules.push(`.axiom-${prefix}gap-${token} { gap: ${space(token)}; }`);
|
|
140
|
+
rules.push(`.axiom-${prefix}pad-x-${token} { padding-left: ${space(token)}; padding-right: ${space(token)}; }`);
|
|
141
|
+
rules.push(`.axiom-${prefix}pad-y-${token} { padding-top: ${space(token)}; padding-bottom: ${space(token)}; }`);
|
|
142
|
+
}
|
|
143
|
+
return rules.join('\n');
|
|
144
|
+
}
|
|
145
|
+
function alignmentRules() {
|
|
146
|
+
const rules = [];
|
|
147
|
+
for (const align of ALIGNMENTS) {
|
|
148
|
+
rules.push(`.axiom-align-${align} { align-items: ${ALIGN_VALUES[align]}; }`);
|
|
149
|
+
}
|
|
150
|
+
for (const justify of JUSTIFICATIONS) {
|
|
151
|
+
rules.push(`.axiom-justify-${justify} { justify-content: ${JUSTIFY_VALUES[justify]}; }`);
|
|
152
|
+
}
|
|
153
|
+
rules.push('.axiom-wrap { flex-wrap: wrap; }');
|
|
154
|
+
rules.push('.axiom-nowrap { flex-wrap: nowrap; }');
|
|
155
|
+
return rules.join('\n');
|
|
156
|
+
}
|
|
157
|
+
function sizingRules(prefix = '') {
|
|
158
|
+
const rules = [];
|
|
159
|
+
for (const size of BOUNDED_SIZES) {
|
|
160
|
+
rules.push(`.axiom-${prefix}width-${size} { flex: 0 0 auto; width: var(--axiom-size-${size}); max-width: 100%; }`);
|
|
161
|
+
rules.push(`.axiom-${prefix}minwidth-${size} { min-width: min(100%, var(--axiom-size-${size})); }`);
|
|
162
|
+
rules.push(`.axiom-${prefix}maxwidth-${size} { max-width: var(--axiom-size-${size}); }`);
|
|
163
|
+
}
|
|
164
|
+
if (!prefix) {
|
|
165
|
+
rules.push('.axiom-height-content { height: auto; }');
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// A responsive width override has to be able to undo a bounded base width.
|
|
169
|
+
rules.push(`.axiom-${prefix}width-fill { width: auto; align-self: stretch; flex: 1 1 0%; min-width: 0; }`);
|
|
170
|
+
rules.push(`.axiom-${prefix}width-content { width: auto; flex: 0 0 auto; }`);
|
|
171
|
+
rules.push(`.axiom-${prefix}width-fit { width: auto; flex: 0 1 auto; min-width: 0; }`);
|
|
172
|
+
}
|
|
173
|
+
return rules.join('\n');
|
|
174
|
+
}
|
|
175
|
+
function columnRules() {
|
|
176
|
+
const rules = [];
|
|
177
|
+
for (let count = 1; count <= MAX_FIXED_COLUMNS; count += 1) {
|
|
178
|
+
rules.push(`.axiom-columns-${count} { grid-template-columns: repeat(${count}, minmax(0, 1fr)); }`);
|
|
179
|
+
}
|
|
180
|
+
for (const size of BOUNDED_SIZES) {
|
|
181
|
+
rules.push(`.axiom-columns-adaptive-${size} { grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--axiom-size-${size})), 1fr)); }`);
|
|
182
|
+
}
|
|
183
|
+
return rules.join('\n');
|
|
184
|
+
}
|
|
185
|
+
function surfaceRules(theme) {
|
|
186
|
+
return SURFACE_ROLES.map((role) => {
|
|
187
|
+
const style = theme.surfaces[role];
|
|
188
|
+
const background = style.color === 'transparent' ? 'transparent' : `var(--axiom-color-${style.color})`;
|
|
189
|
+
const border = style.border === 'none'
|
|
190
|
+
? 'border: 0;'
|
|
191
|
+
: `border: 1px solid var(--axiom-color-border);${style.border === 'strong' ? ' border-width: 2px;' : ''}`;
|
|
192
|
+
return [
|
|
193
|
+
`.axiom-surface-${role} {`,
|
|
194
|
+
` background: ${background};`,
|
|
195
|
+
` ${border}`,
|
|
196
|
+
` border-radius: var(--axiom-radius-${style.radius});`,
|
|
197
|
+
` box-shadow: var(--axiom-shadow-${style.elevation});`,
|
|
198
|
+
'}',
|
|
199
|
+
].join('\n');
|
|
200
|
+
}).join('\n');
|
|
201
|
+
}
|
|
202
|
+
function typographyRules(theme) {
|
|
203
|
+
const rules = TEXT_ROLES.map((role) => {
|
|
204
|
+
const style = theme.typography.roles[role];
|
|
205
|
+
const heading = role === 'heading' || role === 'title' || role === 'display';
|
|
206
|
+
return [
|
|
207
|
+
`.axiom-text-${role} {`,
|
|
208
|
+
` --axiom-font-weight: ${style.weight};`,
|
|
209
|
+
` font-family: ${heading ? 'var(--axiom-font-heading)' : 'var(--axiom-font-family)'};`,
|
|
210
|
+
` font-size: ${style.size}px;`,
|
|
211
|
+
' font-weight: calc(var(--axiom-font-weight) + var(--axiom-weight-shift));',
|
|
212
|
+
` line-height: ${style.lineHeight};`,
|
|
213
|
+
style.tracking === undefined ? '' : ` letter-spacing: ${style.tracking / 100}em;`,
|
|
214
|
+
style.transform === undefined ? '' : ` text-transform: ${style.transform};`,
|
|
215
|
+
' margin: 0;',
|
|
216
|
+
'}',
|
|
217
|
+
]
|
|
218
|
+
.filter(Boolean)
|
|
219
|
+
.join('\n');
|
|
220
|
+
});
|
|
221
|
+
for (const level of EMPHASIS_LEVELS) {
|
|
222
|
+
const emphasis = theme.typography.emphasis[level];
|
|
223
|
+
rules.push(`.axiom-emphasis-${level} { --axiom-weight-shift: ${emphasis.weightShift}; opacity: ${emphasis.opacity}; }`);
|
|
224
|
+
}
|
|
225
|
+
rules.push('.axiom-text-label { color: var(--axiom-color-text-muted); }');
|
|
226
|
+
rules.push('.axiom-text-caption { color: var(--axiom-color-text-muted); }');
|
|
227
|
+
return rules.join('\n');
|
|
228
|
+
}
|
|
229
|
+
/** A filled role: background, its own text colour, no border. */
|
|
230
|
+
function filledRole(role) {
|
|
231
|
+
const token = kebab(role);
|
|
232
|
+
return `background: var(--axiom-color-${token}); color: var(--axiom-color-${token}-text); border-color: transparent;`;
|
|
233
|
+
}
|
|
234
|
+
function roleRules() {
|
|
235
|
+
const rules = [];
|
|
236
|
+
rules.push(`.axiom-button.axiom-role-primary { background: var(--axiom-color-accent); color: var(--axiom-color-accent-text); border-color: transparent; }`);
|
|
237
|
+
rules.push('.axiom-button.axiom-role-secondary { background: var(--axiom-color-surface); color: var(--axiom-color-text); border-color: var(--axiom-color-border); }');
|
|
238
|
+
rules.push('.axiom-button.axiom-role-tertiary { background: transparent; color: var(--axiom-color-accent); border-color: transparent; }');
|
|
239
|
+
rules.push('.axiom-button.axiom-role-muted { background: var(--axiom-color-muted); color: var(--axiom-color-muted-text); border-color: transparent; }');
|
|
240
|
+
for (const role of ['destructive', 'success', 'warning', 'informational']) {
|
|
241
|
+
rules.push(`.axiom-button.axiom-role-${role} { ${filledRole(role)} }`);
|
|
242
|
+
}
|
|
243
|
+
// Away from controls, a role tints text and separates a region from its ground.
|
|
244
|
+
for (const role of ['destructive', 'success', 'warning', 'informational']) {
|
|
245
|
+
const token = kebab(role);
|
|
246
|
+
rules.push(`.axiom-text.axiom-role-${role}, .axiom-field.axiom-role-${role} { color: var(--axiom-color-${token}); }`);
|
|
247
|
+
rules.push(`.axiom-container.axiom-role-${role}, .axiom-conditional.axiom-role-${role} {\n` +
|
|
248
|
+
` border-color: var(--axiom-color-${token});\n` +
|
|
249
|
+
` background: color-mix(in srgb, var(--axiom-color-${token}) 10%, var(--axiom-color-surface));\n` +
|
|
250
|
+
'}');
|
|
251
|
+
rules.push(`.axiom-role-${role} > .axiom-icon { color: var(--axiom-color-${token}); }`);
|
|
252
|
+
}
|
|
253
|
+
rules.push('.axiom-text.axiom-role-muted, .axiom-field.axiom-role-muted { color: var(--axiom-color-text-muted); }');
|
|
254
|
+
rules.push('.axiom-text.axiom-role-primary { color: var(--axiom-color-accent); }');
|
|
255
|
+
// Badges and pills, so a status value need not be a bare word.
|
|
256
|
+
rules.push([
|
|
257
|
+
'.axiom-treatment-badge > .axiom-field-value,',
|
|
258
|
+
'.axiom-treatment-pill > .axiom-field-value,',
|
|
259
|
+
'.axiom-text.axiom-treatment-badge,',
|
|
260
|
+
'.axiom-text.axiom-treatment-pill {',
|
|
261
|
+
' display: inline-block;',
|
|
262
|
+
` padding: 2px ${space('small')};`,
|
|
263
|
+
' background: var(--axiom-color-muted);',
|
|
264
|
+
' color: var(--axiom-color-muted-text);',
|
|
265
|
+
' font-weight: 600;',
|
|
266
|
+
' white-space: nowrap;',
|
|
267
|
+
'}',
|
|
268
|
+
].join('\n'));
|
|
269
|
+
rules.push('.axiom-treatment-badge > .axiom-field-value, .axiom-text.axiom-treatment-badge { border-radius: var(--axiom-radius-small); }');
|
|
270
|
+
rules.push('.axiom-treatment-pill > .axiom-field-value, .axiom-text.axiom-treatment-pill { border-radius: var(--axiom-radius-pill); }');
|
|
271
|
+
for (const role of PRESENTATION_ROLES) {
|
|
272
|
+
if (role === 'muted' || role === 'secondary' || role === 'tertiary') {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const token = role === 'primary' ? 'accent' : kebab(role);
|
|
276
|
+
rules.push(`.axiom-treatment-badge.axiom-role-${role} > .axiom-field-value,\n` +
|
|
277
|
+
`.axiom-treatment-pill.axiom-role-${role} > .axiom-field-value,\n` +
|
|
278
|
+
`.axiom-text.axiom-treatment-badge.axiom-role-${role},\n` +
|
|
279
|
+
`.axiom-text.axiom-treatment-pill.axiom-role-${role} {\n` +
|
|
280
|
+
` background: color-mix(in srgb, var(--axiom-color-${token}) 16%, var(--axiom-color-surface));\n` +
|
|
281
|
+
` color: var(--axiom-color-${token});\n` +
|
|
282
|
+
'}');
|
|
283
|
+
}
|
|
284
|
+
return rules.join('\n');
|
|
285
|
+
}
|
|
286
|
+
/** Landmarks, controls, forms and the rest of the visual baseline of §55. */
|
|
287
|
+
function baselineRules(theme) {
|
|
288
|
+
return `
|
|
289
|
+
* { box-sizing: border-box; }
|
|
290
|
+
body {
|
|
291
|
+
margin: 0;
|
|
292
|
+
background: var(--axiom-color-background);
|
|
293
|
+
color: var(--axiom-color-text);
|
|
294
|
+
font-family: var(--axiom-font-family);
|
|
295
|
+
font-size: ${theme.typography.roles.body.size}px;
|
|
296
|
+
line-height: ${theme.typography.roles.body.lineHeight};
|
|
297
|
+
-webkit-font-smoothing: antialiased;
|
|
298
|
+
}
|
|
299
|
+
#app { padding: ${space('large')}; max-width: 1180px; margin: 0 auto; }
|
|
300
|
+
.axiom-view { width: 100%; }
|
|
301
|
+
.axiom-repeat > * { min-width: 0; }
|
|
302
|
+
.axiom-icon { flex: 0 0 auto; font-style: normal; line-height: 1; }
|
|
303
|
+
|
|
304
|
+
.axiom-field-label { color: var(--axiom-color-text-muted); font-size: ${theme.typography.roles.label.size}px; font-weight: ${theme.typography.roles.label.weight}; }
|
|
305
|
+
.axiom-field-value { color: var(--axiom-color-text); }
|
|
306
|
+
|
|
307
|
+
.axiom-input-label { color: var(--axiom-color-text-muted); font-size: ${theme.typography.roles.label.size}px; font-weight: ${theme.typography.roles.label.weight}; }
|
|
308
|
+
.axiom-input-description { color: var(--axiom-color-text-muted); font-size: ${theme.typography.roles.caption.size}px; }
|
|
309
|
+
.axiom-required-marker { color: var(--axiom-color-destructive); margin-left: 2px; }
|
|
310
|
+
|
|
311
|
+
input.axiom-control, select.axiom-control, textarea.axiom-control, .axiom-control {
|
|
312
|
+
font: inherit;
|
|
313
|
+
color: var(--axiom-color-text);
|
|
314
|
+
background: var(--axiom-color-surface);
|
|
315
|
+
border: var(--axiom-control-border) solid var(--axiom-color-border);
|
|
316
|
+
border-radius: var(--axiom-control-radius);
|
|
317
|
+
min-height: var(--axiom-control-height);
|
|
318
|
+
padding: 0 var(--axiom-control-padding-x);
|
|
319
|
+
width: 100%;
|
|
320
|
+
}
|
|
321
|
+
select.axiom-control { appearance: auto; }
|
|
322
|
+
textarea.axiom-control { min-height: calc(var(--axiom-control-height) * 3); padding: ${space('small')} var(--axiom-control-padding-x); resize: vertical; line-height: inherit; }
|
|
323
|
+
input[type="checkbox"].axiom-control { width: 1.15em; height: 1.15em; min-height: 0; padding: 0; accent-color: var(--axiom-color-accent); }
|
|
324
|
+
.axiom-control-switch { display: inline-flex; align-items: center; gap: ${space('xsmall')}; }
|
|
325
|
+
.axiom-radio-group { display: flex; flex-wrap: wrap; gap: ${space('small')}; }
|
|
326
|
+
.axiom-radio-option { display: inline-flex; align-items: center; gap: ${space('xsmall')}; }
|
|
327
|
+
.axiom-control:disabled { opacity: 0.6; }
|
|
328
|
+
.axiom-control[aria-invalid="true"] { border-color: var(--axiom-color-destructive); }
|
|
329
|
+
|
|
330
|
+
button.axiom-button {
|
|
331
|
+
font: inherit;
|
|
332
|
+
font-weight: 560;
|
|
333
|
+
/* The internal arrangement comes from the theme's button metrics, so no application
|
|
334
|
+
has to restate layout and padding on every control. */
|
|
335
|
+
display: inline-flex;
|
|
336
|
+
flex-direction: ${theme.buttons.layout === 'vertical' ? 'column' : 'row'};
|
|
337
|
+
align-items: ${ALIGN_VALUES[theme.buttons.align]};
|
|
338
|
+
justify-content: ${JUSTIFY_VALUES[theme.buttons.justify]};
|
|
339
|
+
gap: ${space(theme.buttons.gap)};
|
|
340
|
+
min-height: var(--axiom-control-height);
|
|
341
|
+
padding: 0 calc(var(--axiom-control-padding-x) * ${theme.buttons.paddingScale});
|
|
342
|
+
border: var(--axiom-control-border) solid transparent;
|
|
343
|
+
border-radius: var(--axiom-control-radius);
|
|
344
|
+
background: var(--axiom-color-surface);
|
|
345
|
+
color: var(--axiom-color-text);
|
|
346
|
+
cursor: pointer;
|
|
347
|
+
white-space: nowrap;
|
|
348
|
+
text-align: center;
|
|
349
|
+
}
|
|
350
|
+
button.axiom-button:hover { filter: brightness(0.96); }
|
|
351
|
+
button.axiom-button:active { transform: translateY(1px); }
|
|
352
|
+
/* A button's own contents are laid out by the theme; a layout token on the node refines
|
|
353
|
+
the direction without giving up the control's metrics. */
|
|
354
|
+
button.axiom-button.axiom-layout-vertical { flex-direction: column; }
|
|
355
|
+
button.axiom-button.axiom-layout-horizontal { flex-direction: row; }
|
|
356
|
+
.axiom-button > .axiom-button-label { white-space: nowrap; }
|
|
357
|
+
|
|
358
|
+
:focus-visible { outline: var(--axiom-focus-width) solid var(--axiom-color-focus); outline-offset: 1px; }
|
|
359
|
+
|
|
360
|
+
.axiom-ux-header-region { position: sticky; top: 0; z-index: 5; backdrop-filter: blur(6px); }
|
|
361
|
+
.axiom-ux-empty-state { text-align: center; }
|
|
362
|
+
.axiom-ux-form-section + .axiom-ux-form-section { padding-top: ${space('small')}; border-top: 1px solid var(--axiom-color-border); }
|
|
363
|
+
|
|
364
|
+
.axiom-diagnostic[data-empty="true"] { display: none; }
|
|
365
|
+
.axiom-diagnostic-entry { color: inherit; }
|
|
366
|
+
.axiom-no-route { padding: ${space('large')}; background: var(--axiom-color-surface); border-radius: var(--axiom-radius-medium); }
|
|
367
|
+
.axiom-diagnostics { list-style: none; margin: 0; padding: 0; }
|
|
368
|
+
`.trim();
|
|
369
|
+
}
|
|
370
|
+
function mediaQuery(theme, device) {
|
|
371
|
+
const responsive = theme.responsive;
|
|
372
|
+
if (device === 'compact') {
|
|
373
|
+
return `@media (max-width: ${responsive.compact.maxWidth}px)`;
|
|
374
|
+
}
|
|
375
|
+
if (device === 'wide') {
|
|
376
|
+
return `@media (min-width: ${responsive.wide.minWidth}px)`;
|
|
377
|
+
}
|
|
378
|
+
return `@media (min-width: ${responsive.regular.minWidth}px) and (max-width: ${responsive.regular.maxWidth}px)`;
|
|
379
|
+
}
|
|
380
|
+
function indent(block) {
|
|
381
|
+
return block
|
|
382
|
+
.split('\n')
|
|
383
|
+
.map((line) => (line.trim() ? ` ${line}` : line))
|
|
384
|
+
.join('\n');
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Responsive behaviour a graph does not have to ask for: rows keep their wrapping, fixed
|
|
388
|
+
* grids give up columns, bounded widths stop being bounded, and controls go full width.
|
|
389
|
+
* An agent should not have to design every breakpoint by hand.
|
|
390
|
+
*/
|
|
391
|
+
function compactDefaults() {
|
|
392
|
+
const rules = [];
|
|
393
|
+
for (let count = 2; count <= MAX_FIXED_COLUMNS; count += 1) {
|
|
394
|
+
rules.push(`.axiom-columns-${count} { grid-template-columns: minmax(0, 1fr); }`);
|
|
395
|
+
}
|
|
396
|
+
for (const size of BOUNDED_SIZES) {
|
|
397
|
+
rules.push(`.axiom-width-${size} { width: 100%; }`);
|
|
398
|
+
rules.push(`.axiom-maxwidth-${size} { max-width: 100%; }`);
|
|
399
|
+
rules.push(`.axiom-minwidth-${size} { min-width: 0; }`);
|
|
400
|
+
}
|
|
401
|
+
rules.push('.axiom-layout-horizontal.axiom-nowrap { overflow-x: auto; }');
|
|
402
|
+
rules.push('.axiom-button { flex: 1 1 auto; }');
|
|
403
|
+
rules.push('.axiom-ux-action-group { justify-content: stretch; }');
|
|
404
|
+
rules.push('#app { padding: ' + space('medium') + '; }');
|
|
405
|
+
return rules.join('\n');
|
|
406
|
+
}
|
|
407
|
+
function regularDefaults() {
|
|
408
|
+
const rules = [];
|
|
409
|
+
for (let count = 3; count <= MAX_FIXED_COLUMNS; count += 1) {
|
|
410
|
+
rules.push(`.axiom-columns-${count} { grid-template-columns: repeat(2, minmax(0, 1fr)); }`);
|
|
411
|
+
}
|
|
412
|
+
return rules.join('\n');
|
|
413
|
+
}
|
|
414
|
+
/** The per-device overrides a graph asks for explicitly. */
|
|
415
|
+
function responsiveOverrides(theme, device) {
|
|
416
|
+
const prefix = `${device}-`;
|
|
417
|
+
return [
|
|
418
|
+
layoutRules(prefix),
|
|
419
|
+
spacingRules(prefix),
|
|
420
|
+
sizingRules(prefix),
|
|
421
|
+
densityRules(theme, prefix),
|
|
422
|
+
`.axiom-${prefix}hidden { display: none !important; }`,
|
|
423
|
+
].join('\n');
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* The complete stylesheet for a theme. It is derived entirely from theme data and the
|
|
427
|
+
* semantic class vocabulary, so a different theme is a different sheet and never a
|
|
428
|
+
* different application.
|
|
429
|
+
*/
|
|
430
|
+
export function createThemeStylesheet(theme) {
|
|
431
|
+
const sections = [
|
|
432
|
+
`/* ${theme.name} (${theme.id}) — generated from theme data; do not edit. */`,
|
|
433
|
+
appearanceBlocks(theme),
|
|
434
|
+
tokenVariables(theme),
|
|
435
|
+
baselineRules(theme),
|
|
436
|
+
densityRules(theme),
|
|
437
|
+
layoutRules(),
|
|
438
|
+
spacingRules(),
|
|
439
|
+
alignmentRules(),
|
|
440
|
+
sizingRules(),
|
|
441
|
+
columnRules(),
|
|
442
|
+
surfaceRules(theme),
|
|
443
|
+
typographyRules(theme),
|
|
444
|
+
roleRules(),
|
|
445
|
+
];
|
|
446
|
+
for (const device of ['compact', 'regular', 'wide']) {
|
|
447
|
+
const defaults = device === 'compact' ? compactDefaults() : device === 'regular' ? regularDefaults() : '';
|
|
448
|
+
const body = [defaults, responsiveOverrides(theme, device)].filter(Boolean).join('\n');
|
|
449
|
+
sections.push(`${mediaQuery(theme, device)} {\n${indent(body)}\n}`);
|
|
450
|
+
}
|
|
451
|
+
sections.push('@media (prefers-reduced-motion: reduce) {\n * { transition: none !important; animation: none !important; }\n}');
|
|
452
|
+
return sections.join('\n\n');
|
|
453
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cynodia/axiom-compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.2-alpha.1",
|
|
4
4
|
"description": "Normalizes an Axiom application graph and emits a self-contained page.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AskTech AS",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@cynodia/axiom-core": "0.
|
|
35
|
-
"@cynodia/axiom-runtime": "0.
|
|
34
|
+
"@cynodia/axiom-core": "0.5.2-alpha.1",
|
|
35
|
+
"@cynodia/axiom-runtime": "0.5.2-alpha.1"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsc -b tsconfig.json tsconfig.test.json",
|