volaro 0.1.0-alpha.4 → 0.1.0-alpha.5
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 +5 -6
- package/compiler/SOURCE_INFO.json +2 -2
- package/compiler/SOURCE_REV +1 -1
- package/compiler/validator/vlcheck/checks.py +6 -4
- package/compiler/vlbuild/vlbuild/assets/vlrt.css +107 -7
- package/compiler/vlbuild/vlbuild/emit.py +12 -4
- package/compiler/vlbuild/vlbuild/pages_build.py +35 -3
- package/compiler/vlbuild/vlbuild/style_config.py +30 -9
- package/compiler/vlbuild/vlbuild/styling.py +5 -2
- package/language/crib.md +22 -5
- package/language/spec.md +10 -4
- package/language/supported.md +14 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# Volaro
|
|
2
2
|
|
|
3
|
-
**Limited alpha
|
|
4
|
-
`
|
|
5
|
-
`0.0.x` placeholder) — always specify `@alpha` or the exact version.
|
|
3
|
+
**Limited alpha** (this is `0.1.0-alpha.5`), published under both the
|
|
4
|
+
`latest` and `alpha` dist-tags: `npm install volaro`.
|
|
6
5
|
|
|
7
6
|
Volaro is an experimental application language intended for AI authoring and
|
|
8
7
|
human review. This package ships the language reference **and a working
|
|
@@ -113,11 +112,11 @@ dynamic route), so `volaro dev` on a project directory runs that server.
|
|
|
113
112
|
## Scaffold a project
|
|
114
113
|
|
|
115
114
|
```bash
|
|
116
|
-
npm create volaro
|
|
115
|
+
npm create volaro my-app # the separate create-volaro package
|
|
117
116
|
```
|
|
118
117
|
|
|
119
|
-
(
|
|
120
|
-
|
|
118
|
+
(If `npm create volaro` prints "This release is a placeholder", npm is
|
|
119
|
+
reusing a cached copy of the old `0.0.1`: run `npm cache clean --force`.)
|
|
121
120
|
|
|
122
121
|
It writes one single-page starter, a `volaro.json`, and `package.json` scripts
|
|
123
122
|
(`dev` / `build` / `check`) that call `volaro`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"revision": "
|
|
2
|
+
"revision": "8c0952905f3449e5dc7c4fe75c2ba4b718b34148",
|
|
3
3
|
"source": "git",
|
|
4
4
|
"dirty": false,
|
|
5
|
-
"content_sha256": "
|
|
5
|
+
"content_sha256": "24266586ae4ce6a2122f6893b7b44cf1cf721c2e97785afe95751a8109a2cb7a"
|
|
6
6
|
}
|
package/compiler/SOURCE_REV
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
8c0952905f3449e5dc7c4fe75c2ba4b718b34148
|
|
@@ -488,13 +488,14 @@ class Checker:
|
|
|
488
488
|
if not valid:
|
|
489
489
|
self._d("E-THEME-SHAPE", Severity.ERROR,
|
|
490
490
|
"malformed %s token row" % category.head, category)
|
|
491
|
-
allowed = {"button", "input", "card"
|
|
491
|
+
allowed = {"button", "input", "card", "select", "textarea", "checkbox",
|
|
492
|
+
"radio", "link", "fieldset", "table", "details"}
|
|
492
493
|
seen_recipes: set[str] = set()
|
|
493
494
|
for recipe in (x for x in mod.items if isinstance(x, A.RecipeDecl)):
|
|
494
495
|
if recipe.primitive not in allowed:
|
|
495
496
|
self._d("E-RECIPE-UNKNOWN-PRIMITIVE", Severity.ERROR,
|
|
496
|
-
"
|
|
497
|
-
% recipe.primitive, recipe)
|
|
497
|
+
"recipes support %s; got %r"
|
|
498
|
+
% (", ".join(sorted(allowed)), recipe.primitive), recipe)
|
|
498
499
|
continue
|
|
499
500
|
if recipe.primitive in seen_recipes:
|
|
500
501
|
self._d("E-RECIPE-SHAPE", Severity.ERROR,
|
|
@@ -541,7 +542,8 @@ class Checker:
|
|
|
541
542
|
if value not in axes.get(axis, set()):
|
|
542
543
|
self._d("E-RECIPE-SHAPE", Severity.ERROR,
|
|
543
544
|
"default %s:%s is not declared" % (axis, value), recipe)
|
|
544
|
-
if recipe.primitive in ("button", "input"
|
|
545
|
+
if recipe.primitive in ("button", "input", "select", "textarea",
|
|
546
|
+
"checkbox", "radio", "link"):
|
|
545
547
|
all_tokens = " ".join(" ".join(r.tokens) for r in recipe.body)
|
|
546
548
|
if "focus-visible:" not in all_tokens:
|
|
547
549
|
self._d("E-RECIPE-FOCUS", Severity.ERROR,
|
|
@@ -15,8 +15,15 @@
|
|
|
15
15
|
/* Keep white action text at 5.99:1 in both schemes. The lighter dark-mode
|
|
16
16
|
* accent is for links/borders, not a white-text button background. */
|
|
17
17
|
--action-bg: #2f6f4f;
|
|
18
|
+
/* White text on a danger button needs the darker red in BOTH schemes;
|
|
19
|
+
* the lighter dark-mode --danger is for text on the dark surface. */
|
|
20
|
+
--danger-bg: #b91c1c;
|
|
18
21
|
--danger: #b91c1c;
|
|
19
22
|
--warn: #b45309;
|
|
23
|
+
/* Native controls (checkbox, select, scrollbars) follow the scheme; without
|
|
24
|
+
* this they stayed light-mode white boxes on the dark page. */
|
|
25
|
+
color-scheme: light dark;
|
|
26
|
+
accent-color: var(--accent);
|
|
20
27
|
}
|
|
21
28
|
@media (prefers-color-scheme: dark) {
|
|
22
29
|
:root {
|
|
@@ -56,10 +63,13 @@ code {
|
|
|
56
63
|
.vl-spacer { flex: 1; }
|
|
57
64
|
.vl-fragment { display: contents; }
|
|
58
65
|
|
|
59
|
-
a.
|
|
60
|
-
|
|
66
|
+
/* `a` (45.4) is the same link as the `link` convenience, so it looks the
|
|
67
|
+
* same; unstyled it was browser blue, unreadable on the dark surface. */
|
|
68
|
+
a.vl-link, a.vl-a { color: var(--accent); text-decoration: none; }
|
|
69
|
+
a.vl-link:hover, a.vl-a:hover { text-decoration: underline; }
|
|
61
70
|
|
|
62
|
-
|
|
71
|
+
/* The text-entry controls share one look. */
|
|
72
|
+
input.vl-input, select.vl-select, textarea.vl-textarea {
|
|
63
73
|
font: inherit;
|
|
64
74
|
padding: 4px 8px;
|
|
65
75
|
border: 1px solid var(--border);
|
|
@@ -67,13 +77,51 @@ input.vl-input {
|
|
|
67
77
|
background: var(--surface);
|
|
68
78
|
color: var(--ink);
|
|
69
79
|
}
|
|
70
|
-
|
|
80
|
+
select.vl-select { min-height: 32px; }
|
|
81
|
+
textarea.vl-textarea { line-height: 1.5; resize: vertical; }
|
|
82
|
+
input.vl-input[aria-invalid="true"], select.vl-select[aria-invalid="true"],
|
|
83
|
+
textarea.vl-textarea[aria-invalid="true"] { border-color: var(--danger); }
|
|
84
|
+
|
|
85
|
+
/* Checkbox, radio, range and colour are not text boxes: drop the box
|
|
86
|
+
* styling and let the native control (tinted by accent-color) show. */
|
|
87
|
+
input.vl-input:is([type="checkbox"], [type="radio"], [type="range"], [type="color"]) {
|
|
88
|
+
padding: 0;
|
|
89
|
+
border: 0;
|
|
90
|
+
background: none;
|
|
91
|
+
}
|
|
92
|
+
input.vl-input:is([type="checkbox"], [type="radio"]) { width: 16px; height: 16px; margin: 0; }
|
|
93
|
+
|
|
94
|
+
/* Disabled looks disabled, on every control. */
|
|
95
|
+
:is(input.vl-input, select.vl-select, textarea.vl-textarea, button.vl-button):disabled,
|
|
96
|
+
fieldset.vl-fieldset:disabled {
|
|
97
|
+
opacity: 0.55;
|
|
98
|
+
cursor: not-allowed;
|
|
99
|
+
}
|
|
100
|
+
/* ...and so does its label, so the whole field reads as unavailable. */
|
|
101
|
+
.vl-field:has(> :disabled) > .vl-label { opacity: 0.55; }
|
|
71
102
|
|
|
72
103
|
/* accessible input field (spec 8.9): label / help / error wrapper */
|
|
73
104
|
.vl-field { display: flex; flex-direction: column; gap: 4px; }
|
|
74
105
|
.vl-field > .vl-label { font-size: 13px; font-weight: 600; }
|
|
75
106
|
.vl-field > .vl-help { font-size: 12px; color: var(--muted); }
|
|
76
|
-
|
|
107
|
+
/* The field error shares the class name of the runtime crash box (.vl-error,
|
|
108
|
+
* monospace, below); inside a field it is ordinary text. */
|
|
109
|
+
.vl-field > .vl-error { font-size: 12px; font-family: inherit; white-space: normal; color: var(--danger); }
|
|
110
|
+
/* A checkbox or radio sits BESIDE its label, box first. The label stays
|
|
111
|
+
* first in the DOM (reading and tab order unchanged); only the grid places
|
|
112
|
+
* it. Help and error text line up under the label. */
|
|
113
|
+
.vl-field:has(> input.vl-input:is([type="checkbox"], [type="radio"])) {
|
|
114
|
+
display: grid;
|
|
115
|
+
grid-template-columns: auto 1fr;
|
|
116
|
+
column-gap: 8px;
|
|
117
|
+
row-gap: 2px;
|
|
118
|
+
align-items: center;
|
|
119
|
+
}
|
|
120
|
+
.vl-field:has(> input.vl-input:is([type="checkbox"], [type="radio"])) > input { grid-column: 1; grid-row: 1; }
|
|
121
|
+
.vl-field:has(> input.vl-input:is([type="checkbox"], [type="radio"])) > .vl-label {
|
|
122
|
+
grid-column: 2; grid-row: 1; font-size: inherit; font-weight: 400;
|
|
123
|
+
}
|
|
124
|
+
.vl-field:has(> input.vl-input:is([type="checkbox"], [type="radio"])) > :is(.vl-help, .vl-error) { grid-column: 2; }
|
|
77
125
|
button.vl-button {
|
|
78
126
|
font: inherit;
|
|
79
127
|
padding: 6px 12px;
|
|
@@ -88,12 +136,64 @@ button.vl-button[data-variant="primary"] {
|
|
|
88
136
|
border-color: transparent;
|
|
89
137
|
color: #fff;
|
|
90
138
|
}
|
|
91
|
-
button.vl-button
|
|
92
|
-
|
|
139
|
+
button.vl-button[data-variant="ghost"] {
|
|
140
|
+
background: transparent;
|
|
141
|
+
border-color: transparent;
|
|
142
|
+
color: var(--accent);
|
|
143
|
+
}
|
|
144
|
+
button.vl-button[data-variant="danger"] {
|
|
145
|
+
background: var(--danger-bg);
|
|
146
|
+
border-color: transparent;
|
|
147
|
+
color: #fff;
|
|
148
|
+
}
|
|
149
|
+
button.vl-button:not(:disabled):hover { border-color: var(--accent); }
|
|
150
|
+
:is(input.vl-input, select.vl-select, textarea.vl-textarea, button.vl-button,
|
|
151
|
+
a.vl-link, a.vl-a, summary.vl-summary):focus-visible {
|
|
93
152
|
outline: 3px solid var(--ink);
|
|
94
153
|
outline-offset: 3px;
|
|
95
154
|
}
|
|
96
155
|
|
|
156
|
+
/* Inside a layout primitive, `gap:` is the spacing; the browser's own
|
|
157
|
+
* margins on these would double it. Ordinary flow keeps them. */
|
|
158
|
+
:is(.vl-col, .vl-row, .vl-stack, .vl-grid, .vl-card) > :is(
|
|
159
|
+
.vl-p, .vl-h1, .vl-h2, .vl-h3, .vl-h4, .vl-h5, .vl-h6, .vl-ul, .vl-ol,
|
|
160
|
+
.vl-menu, .vl-dl, .vl-table, .vl-figure, .vl-blockquote, .vl-pre, .vl-hr,
|
|
161
|
+
.vl-fieldset, .vl-form, .vl-details) { margin: 0; }
|
|
162
|
+
|
|
163
|
+
/* fieldset groups its controls like a column; legend reads like a label. */
|
|
164
|
+
fieldset.vl-fieldset {
|
|
165
|
+
display: flex;
|
|
166
|
+
flex-direction: column;
|
|
167
|
+
gap: 10px;
|
|
168
|
+
min-width: 0;
|
|
169
|
+
padding: 12px 16px 16px;
|
|
170
|
+
border: 1px solid var(--border);
|
|
171
|
+
border-radius: 8px;
|
|
172
|
+
}
|
|
173
|
+
legend.vl-legend { padding: 0 4px; font-size: 13px; font-weight: 600; }
|
|
174
|
+
|
|
175
|
+
/* Tables: left-aligned, ruled rows, numbers that line up. */
|
|
176
|
+
table.vl-table { border-collapse: collapse; font-variant-numeric: tabular-nums; }
|
|
177
|
+
caption.vl-caption { text-align: start; font-weight: 600; padding-bottom: 6px; }
|
|
178
|
+
:is(th.vl-th, td.vl-td) {
|
|
179
|
+
padding: 6px 12px;
|
|
180
|
+
text-align: start;
|
|
181
|
+
border-bottom: 1px solid var(--border);
|
|
182
|
+
}
|
|
183
|
+
thead.vl-thead th.vl-th { border-bottom-width: 2px; }
|
|
184
|
+
|
|
185
|
+
/* Disclosure panel. */
|
|
186
|
+
details.vl-details {
|
|
187
|
+
border: 1px solid var(--border);
|
|
188
|
+
border-radius: 8px;
|
|
189
|
+
padding: 8px 12px;
|
|
190
|
+
background: var(--surface);
|
|
191
|
+
}
|
|
192
|
+
summary.vl-summary { cursor: pointer; font-weight: 600; }
|
|
193
|
+
details.vl-details[open] > summary.vl-summary { margin-bottom: 8px; }
|
|
194
|
+
|
|
195
|
+
progress.vl-progress, meter.vl-meter { width: 100%; max-width: 320px; }
|
|
196
|
+
|
|
97
197
|
/* overlay layer: a positioned mount point above the app. */
|
|
98
198
|
.vl-overlay { position: fixed; inset: 0; z-index: 1000; }
|
|
99
199
|
|
|
@@ -1332,8 +1332,7 @@ class Emitter:
|
|
|
1332
1332
|
for k, v in el.attrs:
|
|
1333
1333
|
if k in skip_attrs:
|
|
1334
1334
|
continue
|
|
1335
|
-
if k == "class" or
|
|
1336
|
-
and k in self.style_project.recipes[el.name].axes):
|
|
1335
|
+
if k == "class" or k in self._recipe_axes(el):
|
|
1337
1336
|
continue
|
|
1338
1337
|
self._apply_attr(el.name, k, v, style, attrs, handlers)
|
|
1339
1338
|
self._link_safety(el.name, attrs)
|
|
@@ -1589,8 +1588,7 @@ class Emitter:
|
|
|
1589
1588
|
attrs: dict[str, str] = {}
|
|
1590
1589
|
handlers: dict[str, str] = {}
|
|
1591
1590
|
for k, v in rest:
|
|
1592
|
-
if k == "class" or
|
|
1593
|
-
and k in self.style_project.recipes[el.name].axes):
|
|
1591
|
+
if k == "class" or k in self._recipe_axes(el):
|
|
1594
1592
|
continue
|
|
1595
1593
|
if k == "bind" and el.name == "input" and input_type in INPUT_TYPES_CHECKED:
|
|
1596
1594
|
# action-45.2: checkbox/radio bind a *checked* boolean, not a
|
|
@@ -1696,6 +1694,16 @@ class Emitter:
|
|
|
1696
1694
|
spec.append("error: %s" % self.expr(a11y["error"]))
|
|
1697
1695
|
return "VL.field({%s})" % ", ".join(spec)
|
|
1698
1696
|
|
|
1697
|
+
def _recipe_axes(self, el: A.Element) -> dict:
|
|
1698
|
+
"""The style axes of the recipe `el` uses (checkbox/radio resolve to
|
|
1699
|
+
their own recipe, not `input`'s); those attributes are styling, not
|
|
1700
|
+
DOM attributes. Imported lazily: style_config imports this module."""
|
|
1701
|
+
if self.style_project is None:
|
|
1702
|
+
return {}
|
|
1703
|
+
from .style_config import recipe_for
|
|
1704
|
+
recipe = recipe_for(self.style_project, el)
|
|
1705
|
+
return recipe.axes if recipe else {}
|
|
1706
|
+
|
|
1699
1707
|
def _style_classes(self, el: A.Element) -> list[str]:
|
|
1700
1708
|
if self.style_project is None:
|
|
1701
1709
|
# class: is a styling feature: if present, a default style project
|
|
@@ -172,6 +172,15 @@ def build_pages(root: Path, out_public: Path, styled: bool = False) -> dict:
|
|
|
172
172
|
check_no_module_id_collisions({path: module_id_for(str(root), path)
|
|
173
173
|
for path in files})
|
|
174
174
|
|
|
175
|
+
# Styling (theme.vl / recipes.vl): a project keeps them at its root,
|
|
176
|
+
# beside app/, exactly where a single-file build finds them beside
|
|
177
|
+
# app.vl. Every page, layout and boundary compiles against the same
|
|
178
|
+
# style project and one app.css covers them all. Multi-page builds used
|
|
179
|
+
# to ignore styling entirely.
|
|
180
|
+
from .style_config import load_style_project
|
|
181
|
+
style_project = load_style_project(root / "app.vl", A.Module(items=[]))
|
|
182
|
+
style_candidates: list[dict[str, str]] = []
|
|
183
|
+
|
|
175
184
|
compiled: dict[str, str] = {} # module id -> emitted JS
|
|
176
185
|
for path, info in files.items():
|
|
177
186
|
mod, parse_diags, src = _parse(path)
|
|
@@ -187,8 +196,16 @@ def build_pages(root: Path, out_public: Path, styled: bool = False) -> dict:
|
|
|
187
196
|
if check_errors:
|
|
188
197
|
problems.extend(f"{d.code}: {d.message} ({path})" for d in check_errors)
|
|
189
198
|
continue
|
|
199
|
+
if any(isinstance(x, A.RecipeDecl)
|
|
200
|
+
or (isinstance(x, A.Directive) and x.head == "theme")
|
|
201
|
+
for x in mod.items):
|
|
202
|
+
problems.append(
|
|
203
|
+
"E-STYLE-LOCATION: a project's theme and recipes live in theme.vl / "
|
|
204
|
+
"recipes.vl at the project root, beside app/, not in a page, layout "
|
|
205
|
+
"or boundary (%s)" % path)
|
|
206
|
+
continue
|
|
190
207
|
mid = module_id_for(str(root), path)
|
|
191
|
-
emitter = Emitter(src, os.path.basename(path))
|
|
208
|
+
emitter = Emitter(src, os.path.basename(path), style_project=style_project)
|
|
192
209
|
try:
|
|
193
210
|
if info["role"] == "layout":
|
|
194
211
|
js = emitter.build_module(mod, mid, path,
|
|
@@ -203,10 +220,24 @@ def build_pages(root: Path, out_public: Path, styled: bool = False) -> dict:
|
|
|
203
220
|
problems.append(f"{exc} ({path})")
|
|
204
221
|
continue
|
|
205
222
|
compiled[mid] = js
|
|
223
|
+
style_candidates.extend(emitter.style_candidates)
|
|
206
224
|
|
|
207
225
|
if problems:
|
|
208
226
|
raise BuildError("browser-page compilation failed:\n " + "\n ".join(sorted(set(problems))))
|
|
209
227
|
|
|
228
|
+
# Compiled before anything is written, so a CSS failure leaves no output.
|
|
229
|
+
app_css = None
|
|
230
|
+
if style_project.active or style_candidates:
|
|
231
|
+
from .styling import compile_css
|
|
232
|
+
app_css = compile_css(style_candidates, style_project.theme_css())
|
|
233
|
+
|
|
234
|
+
out_public.mkdir(parents=True, exist_ok=True)
|
|
235
|
+
stale_css = out_public / "app.css"
|
|
236
|
+
if app_css is not None:
|
|
237
|
+
stale_css.write_text(app_css, encoding="utf-8")
|
|
238
|
+
elif stale_css.exists():
|
|
239
|
+
stale_css.unlink() # a rebuild after the styling was removed
|
|
240
|
+
|
|
210
241
|
pages_dir = out_public / "pages"
|
|
211
242
|
pages_dir.mkdir(parents=True, exist_ok=True)
|
|
212
243
|
for mid, js in compiled.items():
|
|
@@ -258,7 +289,7 @@ def static_routes(client_manifest: dict) -> list[dict]:
|
|
|
258
289
|
_ASSETS = Path(__file__).resolve().parent / "assets"
|
|
259
290
|
|
|
260
291
|
|
|
261
|
-
def shell_html(title: str = "Volaro") -> str:
|
|
292
|
+
def shell_html(title: str = "Volaro", styled: bool = False) -> str:
|
|
262
293
|
"""The ONE application shell served for every route (static-generated or
|
|
263
294
|
server-fallback) -- byte-identical regardless of route, deliberately:
|
|
264
295
|
architecture decision §4, no SSR/hydration. The client router does the
|
|
@@ -271,6 +302,7 @@ def shell_html(title: str = "Volaro") -> str:
|
|
|
271
302
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
272
303
|
<title>{escape(title)}</title>
|
|
273
304
|
<link rel="stylesheet" href="/vlrt.css" />
|
|
305
|
+
{'<link rel="stylesheet" href="/app.css" />' if styled else ''}
|
|
274
306
|
</head>
|
|
275
307
|
<body>
|
|
276
308
|
<div id="vl-root">loading…</div>
|
|
@@ -328,7 +360,7 @@ def build_project(root: Path, out_dir: Path) -> dict:
|
|
|
328
360
|
(out_public / "vlrouter.js").write_text(
|
|
329
361
|
(_ASSETS / "vlrouter.js").read_text(encoding="utf-8"), encoding="utf-8")
|
|
330
362
|
(out_public / "bootstrap.js").write_text(bootstrap_js(), encoding="utf-8")
|
|
331
|
-
shell = shell_html()
|
|
363
|
+
shell = shell_html(styled=(out_public / "app.css").is_file())
|
|
332
364
|
(out_public / "index.html").write_text(shell, encoding="utf-8")
|
|
333
365
|
|
|
334
366
|
for page in static_routes(client_manifest):
|
|
@@ -32,7 +32,26 @@ DEFAULT_THEME = {
|
|
|
32
32
|
}
|
|
33
33
|
RESERVED_COLORS = {"white", "black", "transparent", "current"}
|
|
34
34
|
RESERVED_ROOTS = {"container", "columns", "break", "box", "object", "overflow"}
|
|
35
|
-
STYLEABLE = {"button", "input", "card"
|
|
35
|
+
STYLEABLE = {"button", "input", "card", "select", "textarea", "checkbox", "radio",
|
|
36
|
+
"link", "fieldset", "table", "details"}
|
|
37
|
+
# Interactive targets must declare visible focus (E-RECIPE-FOCUS).
|
|
38
|
+
FOCUS_REQUIRED = {"button", "input", "select", "textarea", "checkbox", "radio", "link"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def recipe_key(el: A.Element) -> str:
|
|
42
|
+
"""The recipe target an element uses. `checkbox` and `radio` are
|
|
43
|
+
`input type:checkbox|radio` in source but are their own targets: a
|
|
44
|
+
`recipe input` is a text box ("w-full px-sm ..."), which would stretch
|
|
45
|
+
and pad a checkbox. Every other element uses its own name."""
|
|
46
|
+
if el.name == "input":
|
|
47
|
+
for k, v in el.attrs:
|
|
48
|
+
if k == "type" and isinstance(v, A.Name) and v.id in ("checkbox", "radio"):
|
|
49
|
+
return v.id
|
|
50
|
+
return el.name
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def recipe_for(project: "StyleProject", el: A.Element):
|
|
54
|
+
return project.recipes.get(recipe_key(el)) if project else None
|
|
36
55
|
|
|
37
56
|
|
|
38
57
|
def _string(raw: str) -> str:
|
|
@@ -174,7 +193,8 @@ def _parse_theme(node: A.Directive) -> dict:
|
|
|
174
193
|
|
|
175
194
|
def _parse_recipe(node: A.RecipeDecl) -> Recipe:
|
|
176
195
|
if node.primitive not in STYLEABLE:
|
|
177
|
-
raise BuildError("E-RECIPE-UNKNOWN-PRIMITIVE: %r; valid:
|
|
196
|
+
raise BuildError("E-RECIPE-UNKNOWN-PRIMITIVE: %r; valid: %s"
|
|
197
|
+
% (node.primitive, ", ".join(sorted(STYLEABLE))))
|
|
178
198
|
recipe = Recipe(node.primitive)
|
|
179
199
|
for row in node.body:
|
|
180
200
|
toks = row.tokens
|
|
@@ -207,7 +227,7 @@ def _parse_recipe(node: A.RecipeDecl) -> Recipe:
|
|
|
207
227
|
if axis not in recipe.axes or value not in recipe.axes[axis]:
|
|
208
228
|
raise BuildError("E-RECIPE-SHAPE: default %s:%s is not declared" % (axis, value))
|
|
209
229
|
all_classes = recipe.base + [c for vals in recipe.axes.values() for cs in vals.values() for c in cs]
|
|
210
|
-
if node.primitive in
|
|
230
|
+
if node.primitive in FOCUS_REQUIRED and not any("focus-visible:" in c for c in all_classes):
|
|
211
231
|
raise BuildError("E-RECIPE-FOCUS: interactive recipe %r needs focus-visible styling" % node.primitive)
|
|
212
232
|
if (any(_utility(c).startswith(("transition", "animate-")) for c in all_classes)
|
|
213
233
|
and not any("motion-reduce:" in c for c in all_classes)):
|
|
@@ -291,7 +311,8 @@ INLINE_GROUPS = {"pad": "padding", "pad_x": "padding", "pad_y": "padding",
|
|
|
291
311
|
|
|
292
312
|
|
|
293
313
|
def resolve_classes(project: StyleProject, el: A.Element) -> tuple[list[str], list[tuple[str, str]]]:
|
|
294
|
-
recipe = project
|
|
314
|
+
recipe = recipe_for(project, el)
|
|
315
|
+
key = recipe_key(el)
|
|
295
316
|
selected: dict[str, str] = {}
|
|
296
317
|
custom = ""
|
|
297
318
|
for key, value in el.attrs:
|
|
@@ -313,20 +334,20 @@ def resolve_classes(project: StyleProject, el: A.Element) -> tuple[list[str], li
|
|
|
313
334
|
continue
|
|
314
335
|
if choice not in values:
|
|
315
336
|
raise BuildError("E-RECIPE-VARIANT: recipe %r has no %s %r; valid: %s" %
|
|
316
|
-
(
|
|
337
|
+
(key, axis, choice, ", ".join(sorted(values))))
|
|
317
338
|
classes.extend(values[choice])
|
|
318
339
|
occupied = {g for c in classes if (g := conflict_group(c))}
|
|
319
340
|
for key, _value in el.attrs:
|
|
320
341
|
if (key in INLINE_GROUPS and key not in recipe.axes
|
|
321
342
|
and any(group == INLINE_GROUPS[key] and state == ""
|
|
322
343
|
for state, group in occupied)):
|
|
323
|
-
raise BuildError("E-STYLE-RECIPE-MIX: %s recipe and inline %s: set the same property" % (el
|
|
344
|
+
raise BuildError("E-STYLE-RECIPE-MIX: %s recipe and inline %s: set the same property" % (recipe_key(el), key))
|
|
324
345
|
for c in custom.split():
|
|
325
346
|
if _utility(c).startswith("!") or _utility(c).endswith("!"):
|
|
326
347
|
raise BuildError("E-CLASS-IMPORTANT: class: cannot force precedence with !important")
|
|
327
348
|
group = conflict_group(c)
|
|
328
349
|
if group and group in occupied:
|
|
329
|
-
raise BuildError("E-CLASS-RECIPE-CONFLICT: %r conflicts with recipe %r" % (c,
|
|
350
|
+
raise BuildError("E-CLASS-RECIPE-CONFLICT: %r conflicts with recipe %r" % (c, key))
|
|
330
351
|
custom_classes = custom.split()
|
|
331
352
|
all_classes = classes + custom_classes
|
|
332
353
|
if any("[" in c and "]" in c for c in custom_classes):
|
|
@@ -336,12 +357,12 @@ def resolve_classes(project: StyleProject, el: A.Element) -> tuple[list[str], li
|
|
|
336
357
|
raise BuildError("E-CLASS-LIVE-HIDDEN: a live region must remain exposed")
|
|
337
358
|
if el.name == "status" and "sr-only" in utilities:
|
|
338
359
|
project.warnings.append("W-CLASS-LIVE-SR-ONLY: status feedback should remain visually present")
|
|
339
|
-
if el.name in ("button", "input", "link") and any(c in ("outline-none", "outline-0", "ring-0") for c in utilities):
|
|
360
|
+
if el.name in ("button", "input", "link", "select", "textarea") and any(c in ("outline-none", "outline-0", "ring-0") for c in utilities):
|
|
340
361
|
if not any("focus-visible:" in c and _utility(c) not in ("outline-none", "outline-0", "ring-0") for c in all_classes):
|
|
341
362
|
raise BuildError("E-CLASS-FOCUS-REMOVED: removed focus needs a focus-visible replacement")
|
|
342
363
|
seen: set[str] = set(); result = []
|
|
343
364
|
for c in all_classes:
|
|
344
365
|
if c not in seen:
|
|
345
366
|
seen.add(c); result.append(c)
|
|
346
|
-
origins.append((c, "class on %s" % el.name if c in custom_classes else "recipe %s" %
|
|
367
|
+
origins.append((c, "class on %s" % el.name if c in custom_classes else "recipe %s" % key))
|
|
347
368
|
return result, origins
|
|
@@ -18,8 +18,11 @@ def compile_css(candidates: list[dict[str, str]], theme_css: str = "") -> str:
|
|
|
18
18
|
"""
|
|
19
19
|
if not (_TOOLS / "node_modules" / "tailwindcss" / "package.json").is_file():
|
|
20
20
|
raise BuildError(
|
|
21
|
-
"E-CSS-TOOLCHAIN:
|
|
22
|
-
"
|
|
21
|
+
"E-CSS-TOOLCHAIN: theme / recipe / class: styling needs the Tailwind "
|
|
22
|
+
"toolchain, which the volaro npm package does not include yet. "
|
|
23
|
+
"Remove theme.vl, recipes.vl and class: to build with the default "
|
|
24
|
+
"styles, which cover every control. (In the Volaro repository, run "
|
|
25
|
+
"npm ci --prefix prototype/vlbuild/styling.)")
|
|
23
26
|
try:
|
|
24
27
|
result = subprocess.run(
|
|
25
28
|
["node", str(_TOOLS / "build-css.mjs")],
|
package/language/crib.md
CHANGED
|
@@ -320,7 +320,15 @@ select label:"Size" bind:size
|
|
|
320
320
|
`select`/`optgroup`/`datalist` only. `datalist id:"…"` holds `option`
|
|
321
321
|
children; an `input list:"…"` names that same id (a plain string match, not a
|
|
322
322
|
compiler-verified link). `fieldset` needs exactly one `legend` **first**
|
|
323
|
-
child — that legend is the group's accessible name.
|
|
323
|
+
child — that legend is the group's accessible name. `legend` is a container,
|
|
324
|
+
so its text is a child:
|
|
325
|
+
|
|
326
|
+
```vl
|
|
327
|
+
fieldset
|
|
328
|
+
legend
|
|
329
|
+
text "Size"
|
|
330
|
+
input type:radio value:"s" label:"Small" bind:size
|
|
331
|
+
```
|
|
324
332
|
|
|
325
333
|
Attributes and events are **closed**, like element names. An attribute with no
|
|
326
334
|
Volaro contract is `E-ATTR-UNKNOWN`; a real one on the wrong element is
|
|
@@ -342,8 +350,8 @@ need a real checked state).
|
|
|
342
350
|
|
|
343
351
|
`variant:` takes a **bare style name only** — a
|
|
344
352
|
computed value is `E-VARIANT-LITERAL` (vary the positional label instead).
|
|
345
|
-
|
|
346
|
-
|
|
353
|
+
The minimal build styles `variant:primary`, `ghost` and `danger`; other names
|
|
354
|
+
are valid and emit `data-variant=` but render as a plain button until a
|
|
347
355
|
`theme` / `recipe` styles them. The page mounts client-side (`#vl-root` shows
|
|
348
356
|
`loading…` until `app.js` runs). `text ... pre:true` (`text` only,
|
|
349
357
|
`E-PRE-TARGET` elsewhere) preserves whitespace exactly, including embedded
|
|
@@ -759,7 +767,14 @@ Client calls: `auth.sign_in(email:, password:)`, `auth.sign_up(...)`,
|
|
|
759
767
|
|
|
760
768
|
## Styling
|
|
761
769
|
|
|
762
|
-
|
|
770
|
+
**From an npm install, skip this section:** `theme`, `recipe` and `class:` need
|
|
771
|
+
the Tailwind toolchain, which the volaro package does not ship yet
|
|
772
|
+
(`E-CSS-TOOLCHAIN`). The default styles cover every control in light and dark
|
|
773
|
+
mode; use layout attributes (`gap:`, `pad:`, `max_w:` …) for spacing.
|
|
774
|
+
|
|
775
|
+
One `theme.vl` (beside `app.vl`, or at a project's root beside `app/`, with
|
|
776
|
+
`recipes.vl` beside it; a page may not declare either: `E-STYLE-LOCATION`) groups
|
|
777
|
+
`color`, `space`, `radius`, `font`, `screen`, and
|
|
763
778
|
`motion reduce respect`; colors may add positional `dark "…"`. Numeric
|
|
764
779
|
`gap:`/`pad:` values are px, while `gap:sm` resolves `theme.space.sm`.
|
|
765
780
|
|
|
@@ -773,7 +788,9 @@ card variant:raised
|
|
|
773
788
|
button "Save" variant:primary class:"w-full sm:w-auto"
|
|
774
789
|
```
|
|
775
790
|
|
|
776
|
-
|
|
791
|
+
Recipe targets: `button input card select textarea checkbox radio link fieldset
|
|
792
|
+
table details`. `checkbox`/`radio` style `input type:checkbox|radio` (they never
|
|
793
|
+
take `recipe input`, which is for text boxes). A recipe has one literal
|
|
777
794
|
`base`, orthogonal `axis value "classes"` rows, and `default axis:value`.
|
|
778
795
|
`class:` is one complete literal and additive only: no interpolation,
|
|
779
796
|
`!important`, recipe property conflict, or recipe-plus-inline override.
|
package/language/spec.md
CHANGED
|
@@ -683,10 +683,15 @@ Numbers in `gap:` / `pad:` remain pixels; a name such as `gap:sm` resolves to
|
|
|
683
683
|
`theme.space.sm`. `color:muted` resolves to `var(--vl-color-muted)`. Dark values
|
|
684
684
|
apply through both `prefers-color-scheme` and `html[data-theme="dark"]`.
|
|
685
685
|
|
|
686
|
-
`recipe
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
686
|
+
A `recipe` declares literal Tailwind classes as one `base`, orthogonal axes,
|
|
687
|
+
and defaults, for one of these targets: `button`, `input`, `card`, `select`,
|
|
688
|
+
`textarea`, `checkbox`, `radio`, `link`, `fieldset`, `table`, `details`
|
|
689
|
+
(anything else is `E-RECIPE-UNKNOWN-PRIMITIVE`). `checkbox` and `radio` style
|
|
690
|
+
`input type:checkbox` / `input type:radio`, which never take `recipe input`:
|
|
691
|
+
that recipe is for text boxes. `card` is a grouping `<div>` with a child slot.
|
|
692
|
+
The interactive targets (`button`, `input`, `select`, `textarea`, `checkbox`,
|
|
693
|
+
`radio`, `link`) require a `focus-visible:` treatment (`E-RECIPE-FOCUS`), and
|
|
694
|
+
motion utilities require a `motion-reduce:` counterpart:
|
|
690
695
|
|
|
691
696
|
```vl
|
|
692
697
|
recipe button
|
|
@@ -2498,6 +2503,7 @@ multi-owner shared state remains open — not decided by this amendment.
|
|
|
2498
2503
|
26. **Media and image maps (§8.9.5)**, action-45.6, the sixth slice of the safe-UI-catalogue programme. Added `audio`, `video`, `source`, `track`, `picture`, `map` and `area` to the closed lowercase element set; `img` kept §8.10's `alt:` contract unchanged and gained the resource-loading and intrinsic-size surface. Every element here is a network request the browser makes on the page's behalf, so the policy is resource-loading policy: a literal `src:`/`srcset:`/`poster:` must be `http`, `https` or a relative path (`E-MEDIA-SRC`), a **narrower** set than §8.9.3's navigation one because `mailto:`/`tel:` are navigation targets rather than resources and a fetch has no user-visible confirmation step — `data:` is refused despite the legitimate tiny-inline-PNG case, since the same URL form carries arbitrary bytes of any type the browser will sniff, is unreviewable in source, and defeats every cache and size budget — with a computed URL running the same policy in the browser through `VL.fetchUrl`, which neutralises an unsafe value to the empty string. `autoplay:true` is now only expressible alongside `muted:true` (`E-AUTOPLAY-UNMUTED`): every current browser blocks unmuted autoplay, so `autoplay` alone read as media that starts on load and shipped media that did not start at all. `audio`/`video` need `controls:true` or `muted:true` (`E-MEDIA-CONTROLS`) — with sound and no controls a keyboard user cannot pause what they hear — and must **declare** their text alternative the way an `img` declares `alt:`, either a `track kind:captions` child or `captions:none` (`E-MEDIA-CAPTIONS`), because uncaptioned speech is simply unavailable to a deaf viewer and nothing downstream can recover it. An `area` with an `href:` needs an `alt:` (`E-AREA-ALT`), its `shape:`/`coords:` must match in count (`E-AREA-SHAPE` / `E-AREA-COORDS`; a browser silently ignores a mismatched area, so that region is not clickable and nothing says so), and `area` shares §8.9.3's link policy rather than getting a second one. `picture` needs exactly one `img` last (`E-PICTURE-SHAPE`), `source` uses `srcset:` in a picture and `src:` in a media element (`E-SOURCE-SHAPE`), `track` needs `kind:`/`src:` plus `srclang:`/`label:` when it carries speech (`E-TRACK-KIND` / `E-TRACK-SHAPE`), `map` needs a `name:` and at least one `area` (`E-MAP-NAME` / `E-MAP-CHILD`), and an orphaned `source`/`track`/`area` is `E-MEDIA-PARENT`. Typed values are closed with `E-MEDIA-VALUE`, `E-MEDIA-PRELOAD`, `E-MEDIA-SIZE` and `E-MEDIA-FLAG`. Verifying this in real Chromium (part 29, against the new `test-only/media-maps-45-6-acceptance` page, whose assets are a real decodable PNG and a genuine 8-bit PCM WAV) caught a third real defect in the programme: `muted` is the same trap as `checked` — the content attribute sets `defaultMuted` and the live property is initialised from it at creation — so autoplaying media the compiler had proved muted in source was shipping **unmuted**, defeating `E-AUTOPLAY-UNMUTED` entirely. `VL.el` now sets `muted` as a live property, and the emitter emits a real `true` rather than `""`, which is falsy on that path. Part 29 also proves the bytes genuinely fetched and decoded, `<picture>` having Chromium resolve `currentSrc` to the media-matched candidate, both `<track>` children becoming real `TextTrack`s whose cues come from Chromium's own WebVTT parser, a real WAV playing so `on play`/`on pause` observe genuine playback, an image-map area being a real focusable link whose `alt:` is its accessible name, and a computed `data:` resource URL neutralised with Chromium's own network log showing it was never requested.
|
|
2499
2504
|
27. **Global attributes, the layout surface, and refused elements (§8.9.6)**, action-45.7, the closure slice of the safe-UI-catalogue programme. §8.9.1 closed control attributes and events and §8.9.2-§8.9.5 closed each family's surface; this closes what applies to every element. The global attributes each got an outcome: `dir:` became genuinely global (it was scoped to `bdi`/`bdo` by §8.9.3, but it is element-agnostic by definition and a `lang:"ar"` subtree needs a direction on whatever element carries it), `lang:` must be a parseable tag (`E-LANG-TAG`; an unparseable one is ignored, so a screen reader keeps the page language), `tabindex:` is restricted to `0` and `-1` (`E-TABINDEX`; a **positive** tabindex pulls the element ahead of everything with a natural tab order, for the whole document, invisibly to whoever wrote it), `hidden:` is a literal initial state (`E-HIDDEN-VALUE`) and refused on a live region (`E-HIDDEN-LIVE`, since a hidden element leaves the accessibility tree and a hidden live region announces nothing), `spellcheck:` is scoped to where a viewer types, and `title:` outside `abbr` is `W-TITLE-TOOLTIP` — a warning, because a tooltip is invisible on touch, unreachable by keyboard, and read *instead* of the element's own text by most screen readers. Refused globally with `E-ATTR-REFUSED` and a stated reason: `contenteditable` (an editor with no state binding, no paste sanitisation, no undo), `draggable` (a gesture that starts and cannot finish), `style` (the compiler owns the style surface), `role` (it can only contradict its element), any inline handler attribute, `srcdoc`, and `action:`/`method:`/`formaction:` — the **form-submission bypass**, a second unchecked path straight past every guard, auth rule and pending-state check `on submit` provides. There is no `data-*` or `aria-*` passthrough, stated as policy: `aria_label:` is the single aria surface, and the `data-*` passthrough was the fail-open path §8.9.1 removed. The layout/style surface, which §8.9.1 deliberately left element-agnostic and deferred here, is now scoped to three families (`E-LAYOUT-TARGET`): flex/grid container properties only on the elements this compiler gives `display: flex`/`grid`, and box/text styling on anything except the twelve elements that generate no styleable box of their own — with the table `col` following §8.9.4's name-by-position decision, so the flex-column convenience keeps its full style surface. Every element the catalogue refuses now carries its own diagnostic and reason rather than a bare `E-UNKNOWN-ELEMENT`: `E-ELEMENT-COMPILER-OWNED` (10 names), `E-ELEMENT-UNSAFE` (10), `E-ELEMENT-DEFERRED` (13, each naming the contract it waits on) and `E-ELEMENT-OBSOLETE` (17, each naming its replacement). A custom element needs none: the hyphen is not part of a Volaro identifier, so the name cannot be lexed as an element name at all. The closure claim is a **test**, not a sentence — the validator suite carries the whole HTML element set and asserts that no name is both supported and refused, that every refusal has a stated reason, and that no HTML element has neither outcome (88 supported, 46 refused, 0 unclassified). Writing that assertion immediately found four real gaps (`basefont`, `noembed`, `noframes`, and the `style` *element*, dropped from the compiler-owned table while the style *attribute* was handled separately), all now closed. Verified in real Chromium (part 30, against the new `test-only/global-closure-45-7-acceptance` page) for the permitted half — the refusals are proved by the negative controls, since each stops the build and cannot reach a browser: `lang:` reaching the real property, `dir:rtl` genuinely changing the layout against an otherwise pixel-identical `ltr` subtree, a real Tab sequence landing on the `tabindex:0` element at its document position and never on the `tabindex:-1` one (which stays focusable programmatically), `hidden:true` removing the element from Chromium's own accessibility tree rather than merely from view, and every permitted layout/style attribute still resolving to a real computed style.
|
|
2500
2505
|
28. **Conditional `where` predicates** (§13.1), action-44. A `where` value written as an `if` expression, such as `where(title: if query == "" nil else contains(query))`, now lowers branch by branch: a `nil` branch skips the clause and a predicate branch filters. Before, the whole expression went through generic lowering, so the predicate name was emitted as a call to a function that does not exist; it built clean and threw on every request. Precheck now validates each branch, so a non-predicate call inside one fails the build. Verified against a real seeded node+sqlite server (`test_where_predicate_conditional`). The unconditional sentinel workaround (`contains("")`) still works and is no longer required.
|
|
2506
|
+
29. **Recipe targets beyond button, input and card** (§ styling), 2026-09-24. `recipe` now also targets `select`, `textarea`, `checkbox`, `radio`, `link`, `fieldset`, `table` and `details`. `checkbox` and `radio` resolve to their own recipe: before, `recipe input` applied to every `input`, so a text-box recipe (`w-full px-sm ...`) stretched and padded checkboxes and radios. The interactive new targets must declare visible focus, like `button` and `input`. In a multi-page project, `theme.vl` and `recipes.vl` live at the project root beside `app/`; a page, layout or boundary that declares a theme or recipe is `E-STYLE-LOCATION`. Verified by a full build with the CSS compile (`test_styling_new_recipe_targets`) and, for projects, real Chromium (pages-routing e2e part 16).
|
|
2501
2507
|
|
|
2502
2508
|
**Dated correction, 2026-09-23 — amendments 19, 20 and 21 carry the wrong action ID.**
|
|
2503
2509
|
Those three amendments attribute the pages/layouts/router work to "action-20". The
|
package/language/supported.md
CHANGED
|
@@ -354,8 +354,14 @@ audio video source track picture map area
|
|
|
354
354
|
`aria-modal` / Escape are carried for you.
|
|
355
355
|
- `variant:` takes a **bare style name only** (`variant:primary`). A computed
|
|
356
356
|
value, or a `state` / loop variable, is `E-VARIANT-LITERAL`. The minimal
|
|
357
|
-
build styles `variant:primary
|
|
357
|
+
build styles `variant:primary`, `ghost` and `danger`; other names emit
|
|
358
358
|
`data-variant=` but render plain until a `theme` / `recipe` styles them.
|
|
359
|
+
- The default stylesheet styles every control and content element in light
|
|
360
|
+
and dark mode: text inputs, `select` and `textarea` share one look;
|
|
361
|
+
`checkbox`/`radio` sit beside their label; disabled controls (and their
|
|
362
|
+
labels) are faded; `fieldset`, tables, `details`, `progress`/`meter` and
|
|
363
|
+
`a` links are themed. It sits in a low cascade layer, so a `theme` /
|
|
364
|
+
`recipe` overrides it.
|
|
359
365
|
- `text ... pre:true` (`text` only — any other element is `E-PRE-TARGET`)
|
|
360
366
|
emits inline `white-space: pre-wrap` on that element, so embedded
|
|
361
367
|
newlines and runs of whitespace render and copy exactly as given instead
|
|
@@ -615,15 +621,16 @@ shell is the same regardless of route; the client does the first render).
|
|
|
615
621
|
through 45.7 in `NATIVE-ELEMENT-COVERAGE.md`. Document structure, block
|
|
616
622
|
content and lists landed with 45.3 (above); `details`/`summary` are part
|
|
617
623
|
of 45.5, not of it.
|
|
618
|
-
- **Multi-page project builds do not apply `theme` / `recipe` styling.** The
|
|
619
|
-
pipeline emits no `app.css`; styled output is single-entry only. Not a
|
|
620
|
-
diagnostic — the project simply builds unstyled.
|
|
621
624
|
- **No server-side rendering or hydration** for a multi-page project. The
|
|
622
625
|
application shell is the same for every route and the client does the first
|
|
623
626
|
render. Deliberate, not a gap being worked around.
|
|
624
|
-
- No migrations, no OAuth / TOTP, no deployable production output, no
|
|
625
|
-
|
|
626
|
-
|
|
627
|
+
- No migrations, no OAuth / TOTP, no deployable production output, no npm
|
|
628
|
+
escape hatches (email / payments / storage).
|
|
629
|
+
- **`theme` / `recipe` / `class:` styling does not work from an npm
|
|
630
|
+
install.** It needs the Tailwind toolchain, which this package does not
|
|
631
|
+
ship; a build that uses any of them fails with `E-CSS-TOOLCHAIN`. The
|
|
632
|
+
default styles (above) cover every control; the styling features work
|
|
633
|
+
only inside the Volaro repository.
|
|
627
634
|
- Real screen-reader behaviour is **not certified** (build-time a11y rules
|
|
628
635
|
are enforced; a full SR pass is owed). The manual SR-1–SR-11 protocol has
|
|
629
636
|
**not** been walked against the client router either — its results template
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "volaro",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.5",
|
|
4
4
|
"description": "Volaro \u2014 an application language written to be authored by an AI agent and read by a person. Ships the language reference and a working compiler for a supported subset (volaro check / build / dev). CLI: `volaro` (alias `vl`).",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|