@cynodia/axiom-compiler 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 CHANGED
@@ -8,6 +8,12 @@ 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,
13
+ still 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
+
11
17
  ## Installation
12
18
 
13
19
  ```bash
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 { inferLocationType, locationRootStateId, semanticContextFromGraph, validateGraph, } from '@cynodia/axiom-core';
1
+ import { actionGuards, inferLocationType, 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;
@@ -49,6 +49,7 @@ export function compileToIR(graph, options = {}) {
49
49
  const entities = [];
50
50
  const states = [];
51
51
  const constraints = [];
52
+ const transitionConstraints = [];
52
53
  const routes = [];
53
54
  for (const node of graph.listNodes()) {
54
55
  nodes[node.id] = node;
@@ -63,12 +64,23 @@ export function compileToIR(graph, options = {}) {
63
64
  case 'state':
64
65
  states.push(node);
65
66
  break;
66
- case 'action':
67
- actions[node.id] = node;
67
+ case 'action': {
68
+ // Guards are authoring sugar: the IR carries conditions and failures aligned.
69
+ const guards = actionGuards(node);
70
+ actions[node.id] = {
71
+ ...node,
72
+ preconditions: guards.map((guard) => guard.condition),
73
+ failureModes: guards.map((guard) => guard.failureMode ?? { code: 'precondition-failed' }),
74
+ };
75
+ nodes[node.id] = actions[node.id];
68
76
  break;
77
+ }
69
78
  case 'constraint':
70
79
  constraints.push(node);
71
80
  break;
81
+ case 'transition-constraint':
82
+ transitionConstraints.push(node);
83
+ break;
72
84
  case 'route':
73
85
  routes.push(compileRoute(node));
74
86
  break;
@@ -84,6 +96,7 @@ export function compileToIR(graph, options = {}) {
84
96
  const semantics = semanticContextFromGraph(graph);
85
97
  const locationTypes = {};
86
98
  const locationRoots = {};
99
+ const locationRequired = {};
87
100
  for (const node of Object.values(uiNodes)) {
88
101
  if (node.kind !== 'input') {
89
102
  continue;
@@ -93,7 +106,18 @@ export function compileToIR(graph, options = {}) {
93
106
  locationTypes[node.id] = resolved;
94
107
  }
95
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';
96
114
  }
115
+ // Presentation is normalized here, once: renderer defaults, theme, inheritance,
116
+ // semantic inference, node declarations and responsive overrides are all decided before
117
+ // a renderer sees them. What lands in the IR is still semantic — roles and tokens, not
118
+ // CSS — so a second renderer stays possible.
119
+ const theme = graph.theme;
120
+ const presentation = resolvePresentationMap(graph.listNodes(), theme);
97
121
  return {
98
122
  id: graph.id,
99
123
  name: graph.name,
@@ -105,10 +129,14 @@ export function compileToIR(graph, options = {}) {
105
129
  actions,
106
130
  uiNodes,
107
131
  constraints,
132
+ transitionConstraints,
108
133
  routes,
109
134
  edges: graph.semanticEdges(),
110
135
  locationTypes,
111
136
  locationRoots,
137
+ locationRequired,
138
+ theme,
139
+ presentation,
112
140
  };
113
141
  }
114
142
  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,442 @@
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
+ .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
+ .axiom-button {
331
+ font: inherit;
332
+ font-weight: 560;
333
+ display: inline-flex;
334
+ align-items: center;
335
+ justify-content: center;
336
+ gap: ${space('xsmall')};
337
+ min-height: var(--axiom-control-height);
338
+ padding: 0 calc(var(--axiom-control-padding-x) * 1.15);
339
+ border: var(--axiom-control-border) solid transparent;
340
+ border-radius: var(--axiom-control-radius);
341
+ background: var(--axiom-color-surface);
342
+ color: var(--axiom-color-text);
343
+ cursor: pointer;
344
+ white-space: nowrap;
345
+ }
346
+ .axiom-button:hover { filter: brightness(0.96); }
347
+ .axiom-button:active { transform: translateY(1px); }
348
+
349
+ :focus-visible { outline: var(--axiom-focus-width) solid var(--axiom-color-focus); outline-offset: 1px; }
350
+
351
+ .axiom-ux-header-region { position: sticky; top: 0; z-index: 5; backdrop-filter: blur(6px); }
352
+ .axiom-ux-empty-state { text-align: center; }
353
+ .axiom-ux-form-section + .axiom-ux-form-section { padding-top: ${space('small')}; border-top: 1px solid var(--axiom-color-border); }
354
+
355
+ .axiom-no-route { padding: ${space('large')}; background: var(--axiom-color-surface); border-radius: var(--axiom-radius-medium); }
356
+ .axiom-diagnostics { list-style: none; margin: 0; padding: 0; }
357
+ `.trim();
358
+ }
359
+ function mediaQuery(theme, device) {
360
+ const responsive = theme.responsive;
361
+ if (device === 'compact') {
362
+ return `@media (max-width: ${responsive.compact.maxWidth}px)`;
363
+ }
364
+ if (device === 'wide') {
365
+ return `@media (min-width: ${responsive.wide.minWidth}px)`;
366
+ }
367
+ return `@media (min-width: ${responsive.regular.minWidth}px) and (max-width: ${responsive.regular.maxWidth}px)`;
368
+ }
369
+ function indent(block) {
370
+ return block
371
+ .split('\n')
372
+ .map((line) => (line.trim() ? ` ${line}` : line))
373
+ .join('\n');
374
+ }
375
+ /**
376
+ * Responsive behaviour a graph does not have to ask for: rows keep their wrapping, fixed
377
+ * grids give up columns, bounded widths stop being bounded, and controls go full width.
378
+ * An agent should not have to design every breakpoint by hand.
379
+ */
380
+ function compactDefaults() {
381
+ const rules = [];
382
+ for (let count = 2; count <= MAX_FIXED_COLUMNS; count += 1) {
383
+ rules.push(`.axiom-columns-${count} { grid-template-columns: minmax(0, 1fr); }`);
384
+ }
385
+ for (const size of BOUNDED_SIZES) {
386
+ rules.push(`.axiom-width-${size} { width: 100%; }`);
387
+ rules.push(`.axiom-maxwidth-${size} { max-width: 100%; }`);
388
+ rules.push(`.axiom-minwidth-${size} { min-width: 0; }`);
389
+ }
390
+ rules.push('.axiom-layout-horizontal.axiom-nowrap { overflow-x: auto; }');
391
+ rules.push('.axiom-button { flex: 1 1 auto; }');
392
+ rules.push('.axiom-ux-action-group { justify-content: stretch; }');
393
+ rules.push('#app { padding: ' + space('medium') + '; }');
394
+ return rules.join('\n');
395
+ }
396
+ function regularDefaults() {
397
+ const rules = [];
398
+ for (let count = 3; count <= MAX_FIXED_COLUMNS; count += 1) {
399
+ rules.push(`.axiom-columns-${count} { grid-template-columns: repeat(2, minmax(0, 1fr)); }`);
400
+ }
401
+ return rules.join('\n');
402
+ }
403
+ /** The per-device overrides a graph asks for explicitly. */
404
+ function responsiveOverrides(theme, device) {
405
+ const prefix = `${device}-`;
406
+ return [
407
+ layoutRules(prefix),
408
+ spacingRules(prefix),
409
+ sizingRules(prefix),
410
+ densityRules(theme, prefix),
411
+ `.axiom-${prefix}hidden { display: none !important; }`,
412
+ ].join('\n');
413
+ }
414
+ /**
415
+ * The complete stylesheet for a theme. It is derived entirely from theme data and the
416
+ * semantic class vocabulary, so a different theme is a different sheet and never a
417
+ * different application.
418
+ */
419
+ export function createThemeStylesheet(theme) {
420
+ const sections = [
421
+ `/* ${theme.name} (${theme.id}) — generated from theme data; do not edit. */`,
422
+ appearanceBlocks(theme),
423
+ tokenVariables(theme),
424
+ baselineRules(theme),
425
+ densityRules(theme),
426
+ layoutRules(),
427
+ spacingRules(),
428
+ alignmentRules(),
429
+ sizingRules(),
430
+ columnRules(),
431
+ surfaceRules(theme),
432
+ typographyRules(theme),
433
+ roleRules(),
434
+ ];
435
+ for (const device of ['compact', 'regular', 'wide']) {
436
+ const defaults = device === 'compact' ? compactDefaults() : device === 'regular' ? regularDefaults() : '';
437
+ const body = [defaults, responsiveOverrides(theme, device)].filter(Boolean).join('\n');
438
+ sections.push(`${mediaQuery(theme, device)} {\n${indent(body)}\n}`);
439
+ }
440
+ sections.push('@media (prefers-reduced-motion: reduce) {\n * { transition: none !important; animation: none !important; }\n}');
441
+ return sections.join('\n\n');
442
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cynodia/axiom-compiler",
3
- "version": "0.4.0-alpha.1",
3
+ "version": "0.5.0-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.0-alpha.1",
35
- "@cynodia/axiom-runtime": "0.4.0-alpha.1"
34
+ "@cynodia/axiom-core": "0.5.0-alpha.1",
35
+ "@cynodia/axiom-runtime": "0.5.0-alpha.1"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "tsc -b tsconfig.json tsconfig.test.json",