@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 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
- '<html lang="en">',
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
- STYLESHEET,
32
+ stylesheet,
59
33
  ' </style>',
60
34
  '</head>',
61
35
  '<body>',
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './normalize.js';
2
+ export * from './stylesheet.js';
2
3
  export * from './codegen.js';
3
4
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './normalize.js';
2
+ export * from './stylesheet.js';
2
3
  export * from './codegen.js';
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.4.1-alpha.1",
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.4.1-alpha.1",
35
- "@cynodia/axiom-runtime": "0.4.1-alpha.1"
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",