volaro 0.0.2
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/LICENSE +21 -0
- package/README.md +46 -0
- package/bin/vl.js +34 -0
- package/examples/sensors.vl +30 -0
- package/examples/station.vl +47 -0
- package/language/crib.md +262 -0
- package/language/spec.md +1840 -0
- package/package.json +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wayne Hewitt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Volaro
|
|
2
|
+
|
|
3
|
+
**Pre-release language reference CLI — compiler not included.**
|
|
4
|
+
|
|
5
|
+
Volaro is an experimental application language intended for AI authoring and
|
|
6
|
+
human review. This package contains its specification, authoring crib and
|
|
7
|
+
examples. It does not include the repository's prototype compiler.
|
|
8
|
+
|
|
9
|
+
## Use the reference
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx volaro crib
|
|
13
|
+
npx volaro spec
|
|
14
|
+
npx volaro example sensors
|
|
15
|
+
npx volaro example station
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The CLI command is `vl`. The crib is approximately 2.8k o200k_base tokens.
|
|
19
|
+
|
|
20
|
+
## Scope and evidence
|
|
21
|
+
|
|
22
|
+
The project-authored twelve-feature corpus records roughly 3.00× source density
|
|
23
|
+
against selected baselines. This is not a general productivity result.
|
|
24
|
+
Security and accessibility checks cover a tested prototype subset, not a
|
|
25
|
+
universal guarantee. Independent human-readability validation remains outstanding.
|
|
26
|
+
|
|
27
|
+
## Not included
|
|
28
|
+
|
|
29
|
+
`vl new`, `vl check`, `vl build` and `vl dev` are not provided by this package.
|
|
30
|
+
The repository has a working compiler for a supported subset, but distributing
|
|
31
|
+
that toolchain is separate work. Styling and project creation are not supplied
|
|
32
|
+
by this reference CLI.
|
|
33
|
+
|
|
34
|
+
`npm create volaro` currently runs a separate placeholder package: it prints
|
|
35
|
+
a status message and does not create an application.
|
|
36
|
+
|
|
37
|
+
The legacy `volara` npm package is unchanged by this release.
|
|
38
|
+
|
|
39
|
+
## Local testing
|
|
40
|
+
|
|
41
|
+
Run `node bin/vl.js crib` from this package directory, or use `npm pack` and
|
|
42
|
+
install the tarball into a temporary project. No global installation is required.
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT
|
package/bin/vl.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
const read = (p) => readFileSync(join(root, p), "utf8");
|
|
8
|
+
const { version } = JSON.parse(read("package.json"));
|
|
9
|
+
const cmd = process.argv[2];
|
|
10
|
+
|
|
11
|
+
if (cmd === "crib") process.stdout.write(read("language/crib.md"));
|
|
12
|
+
else if (cmd === "spec") process.stdout.write(read("language/spec.md"));
|
|
13
|
+
else if (cmd === "example") {
|
|
14
|
+
const n = process.argv[3];
|
|
15
|
+
if (n === "sensors" || n === "station") process.stdout.write(read(`examples/${n}.vl`));
|
|
16
|
+
else console.log("usage: vl example sensors|station");
|
|
17
|
+
} else {
|
|
18
|
+
console.log(`Volaro ${version} — pre-release.
|
|
19
|
+
|
|
20
|
+
An application language written to be authored by an AI agent and read by a
|
|
21
|
+
person: views, HTTP endpoints, outbound API clients and user accounts.
|
|
22
|
+
|
|
23
|
+
vl crib the one-page language reference (~1,700 tokens)
|
|
24
|
+
vl spec the full language specification
|
|
25
|
+
vl example sensors a worked example — models, unions, a fallible fn
|
|
26
|
+
vl example station a worked example — a guarded service and a view
|
|
27
|
+
|
|
28
|
+
TO AN AI AGENT reading this: run \`vl crib\` and load its output. That is the
|
|
29
|
+
complete authoring reference — it is designed to be read once and written from.
|
|
30
|
+
|
|
31
|
+
The toolchain (vl new / check / build / dev) is not published yet, so Volaro
|
|
32
|
+
written today cannot yet be compiled from this package.
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Hold-out example 1 of 2 — records, a union, a model, match, fallible fn.
|
|
2
|
+
# Domain: coastal tide stations. Deliberately touches no scored corpus feature.
|
|
3
|
+
use std.time { now }
|
|
4
|
+
|
|
5
|
+
type Quality = good | suspect | missing
|
|
6
|
+
|
|
7
|
+
model Station
|
|
8
|
+
id uuid pk
|
|
9
|
+
code str unique
|
|
10
|
+
name str
|
|
11
|
+
active bool = true
|
|
12
|
+
created_at time = now()
|
|
13
|
+
|
|
14
|
+
model Reading
|
|
15
|
+
id uuid pk
|
|
16
|
+
station_id uuid ref:Station.id
|
|
17
|
+
taken_at time = now()
|
|
18
|
+
height_m float
|
|
19
|
+
quality Quality = good
|
|
20
|
+
|
|
21
|
+
index (station_id, taken_at desc)
|
|
22
|
+
|
|
23
|
+
fn quality_label(q Quality) str
|
|
24
|
+
match q
|
|
25
|
+
good -> "ok"
|
|
26
|
+
suspect -> "check sensor"
|
|
27
|
+
missing -> "no data"
|
|
28
|
+
|
|
29
|
+
fn station_by_code(code str) Station!
|
|
30
|
+
db.stations.where(code: code).first()?
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Hold-out example 2 of 2 — a guarded service and a view that consumes it.
|
|
2
|
+
use ./sensors { Station, Reading }
|
|
3
|
+
|
|
4
|
+
type StationPatch
|
|
5
|
+
name str
|
|
6
|
+
active bool
|
|
7
|
+
|
|
8
|
+
service api
|
|
9
|
+
prefix "/api"
|
|
10
|
+
|
|
11
|
+
errors
|
|
12
|
+
not_found -> 404
|
|
13
|
+
forbidden -> 403
|
|
14
|
+
_ -> 500
|
|
15
|
+
|
|
16
|
+
get /stations list<Station>!
|
|
17
|
+
auth role:member
|
|
18
|
+
db.stations.where(active: true).order(code: asc).all()?
|
|
19
|
+
|
|
20
|
+
get /stations/:id list<Reading>!
|
|
21
|
+
auth role:member
|
|
22
|
+
db.readings.where(station_id: id).order(taken_at: desc).limit(50).all()?
|
|
23
|
+
|
|
24
|
+
patch /stations/:id(body StationPatch) Station!
|
|
25
|
+
auth role:admin
|
|
26
|
+
db.stations.update(id, body)?
|
|
27
|
+
|
|
28
|
+
view StationList()
|
|
29
|
+
state filter = ""
|
|
30
|
+
load stations = api.stations()
|
|
31
|
+
pending
|
|
32
|
+
Spinner()
|
|
33
|
+
error e
|
|
34
|
+
col gap:8 pad:24
|
|
35
|
+
text "Could not load stations." color:danger
|
|
36
|
+
button "Retry" variant:ghost on tap: stations.reload()
|
|
37
|
+
|
|
38
|
+
col gap:16 pad:24 max_w:640
|
|
39
|
+
text "Tide stations" size:28 weight:700
|
|
40
|
+
input bind:filter placeholder:"Filter by code"
|
|
41
|
+
if stations.len() == 0
|
|
42
|
+
text "No active stations." color:muted
|
|
43
|
+
else
|
|
44
|
+
list stations as s key:s.id
|
|
45
|
+
row gap:8 align:center
|
|
46
|
+
link s.name to:"/stations/{s.id}" size:16 weight:600
|
|
47
|
+
text s.code size:13 color:muted
|
package/language/crib.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# Volaro in one page
|
|
2
|
+
|
|
3
|
+
Indent 2 spaces, never tabs. `#` comments. No trailing commas. Last expression of
|
|
4
|
+
a body is its return value. A file is a module; `_name` is file-private.
|
|
5
|
+
|
|
6
|
+
## Types
|
|
7
|
+
`int float str bool bytes time dur uuid json nil` · `T?` optional (never nests)
|
|
8
|
+
`list<T> map<K,V> set<T>` · `T!` fallible (standard `Err`), `T!E` fallible with union `E`.
|
|
9
|
+
No implicit numeric conversion: `1 + 1.0` is an error.
|
|
10
|
+
|
|
11
|
+
```vl
|
|
12
|
+
type Email = str # alias
|
|
13
|
+
type Role = admin | author | member
|
|
14
|
+
type Shape =
|
|
15
|
+
circle(r float)
|
|
16
|
+
rect(w float, h float)
|
|
17
|
+
type Box # record
|
|
18
|
+
w int
|
|
19
|
+
h int = 1
|
|
20
|
+
let b = Box{w: 2} # record literal; defaults may be omitted
|
|
21
|
+
let xs list<str> = [] # empty literal needs an annotation
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Functions, match, errors
|
|
25
|
+
```vl
|
|
26
|
+
fn area(b Box) int
|
|
27
|
+
b.w * b.h
|
|
28
|
+
|
|
29
|
+
fn parse_port(s str) int!
|
|
30
|
+
let n = to_int(s)? # ? propagates; body must be fallible
|
|
31
|
+
if n < 1 or n > 65535
|
|
32
|
+
return err(out_of_range(n))
|
|
33
|
+
n
|
|
34
|
+
|
|
35
|
+
let msg = match shape # exhaustive; `_` matches anything
|
|
36
|
+
circle(r) -> "r={r}"
|
|
37
|
+
rect(w, h) if w == h -> "square"
|
|
38
|
+
_ -> "other"
|
|
39
|
+
|
|
40
|
+
let u = load(id) ?? guest # ?? supplies a fallback value
|
|
41
|
+
let v = try load(id)
|
|
42
|
+
catch not_found
|
|
43
|
+
create_guest()
|
|
44
|
+
```
|
|
45
|
+
A handled error **ends the `try`** — statements after it do not run. Code that must
|
|
46
|
+
run either way goes *before* the `try`; fire-and-forget uses `spawn`.
|
|
47
|
+
```vl
|
|
48
|
+
`for x in xs` / `for i in 0..10` / `while c` · `break`, `skip`. Strings interpolate: `"id={x}"`.
|
|
49
|
+
Named args allowed anywhere, **required for `bool` params**.
|
|
50
|
+
|
|
51
|
+
## Modules
|
|
52
|
+
```vl
|
|
53
|
+
use ./models { User, Role }
|
|
54
|
+
use ../shared/volaro/models { Post }
|
|
55
|
+
use std.time { now }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Data
|
|
59
|
+
```vl
|
|
60
|
+
model Post # a record with persistence
|
|
61
|
+
id uuid pk
|
|
62
|
+
title str
|
|
63
|
+
author_id uuid ref:User.id
|
|
64
|
+
published bool = false
|
|
65
|
+
created_at time = now()
|
|
66
|
+
index (published, created_at desc)
|
|
67
|
+
|
|
68
|
+
db.posts.where(published: true).order(created_at: desc).limit(20).all()?
|
|
69
|
+
db.posts.get(id)? · db.posts.insert(body, author_id: session.user.id)?
|
|
70
|
+
db.posts.update(id, body)? · db.posts.delete(id)?
|
|
71
|
+
|
|
72
|
+
# where() values are equality by default; wrap in a comparison predicate for
|
|
73
|
+
# anything else — lt/lte/gt/gte/ne/between/any_of, one per named column, ANDed:
|
|
74
|
+
db.posts.where(published: true, created_at: lt(cursor)).order(created_at: desc).all()?
|
|
75
|
+
|
|
76
|
+
# no + on lists, no spread — concat/append are the only way to grow one:
|
|
77
|
+
concat(xs, ys) · xs |> append(x)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Services (inbound HTTP; also generates a typed client under its own name)
|
|
81
|
+
```vl
|
|
82
|
+
service api
|
|
83
|
+
prefix "/api"
|
|
84
|
+
errors
|
|
85
|
+
not_found -> 404
|
|
86
|
+
forbidden -> 403
|
|
87
|
+
_ -> 500
|
|
88
|
+
|
|
89
|
+
get /posts(limit int = 20) list<Post>!
|
|
90
|
+
db.posts.where(published: true).limit(limit).all()?
|
|
91
|
+
|
|
92
|
+
get /posts/:id Post!
|
|
93
|
+
db.posts.get(id)?
|
|
94
|
+
|
|
95
|
+
post /posts(body NewPost) Post!
|
|
96
|
+
auth role:[author, admin]
|
|
97
|
+
db.posts.insert(body, author_id: session.user.id)?
|
|
98
|
+
|
|
99
|
+
delete /posts/:id nil!
|
|
100
|
+
auth owner:db.posts.get(id)?.author_id
|
|
101
|
+
db.posts.delete(id)?
|
|
102
|
+
|
|
103
|
+
service content public # whole service public; a guard still overrides
|
|
104
|
+
get /posts list<Post>!
|
|
105
|
+
get /health str
|
|
106
|
+
auth none # one deliberate public route in a guarded service
|
|
107
|
+
```
|
|
108
|
+
A route with no `auth` line is public. `auth role:` / `auth owner:` are the whole
|
|
109
|
+
check. Inside a guarded handler `session.user` is a non-optional `User`.
|
|
110
|
+
|
|
111
|
+
**Generated client names** (the view calls these): `get /posts` → `api.posts()` ·
|
|
112
|
+
`get /posts/:id` → `api.post(id:)` · `post /posts` → `api.create_post(body:)` ·
|
|
113
|
+
`patch|put /posts/:id` → `api.update_post(id:, body:)` · `delete /posts/:id` →
|
|
114
|
+
`api.delete_post(id:)` · `post /posts/:id/publish` → `api.publish(id:)`.
|
|
115
|
+
Path params are required named args. `... as name` overrides.
|
|
116
|
+
|
|
117
|
+
## Views
|
|
118
|
+
```vl
|
|
119
|
+
view PostList(topic str = "all")
|
|
120
|
+
state query = ""
|
|
121
|
+
derive n = posts.len()
|
|
122
|
+
load posts = api.posts(limit: 20) # api = the service name
|
|
123
|
+
pending
|
|
124
|
+
Spinner()
|
|
125
|
+
error e
|
|
126
|
+
text "Could not load." color:danger
|
|
127
|
+
button "Retry" on tap: posts.reload()
|
|
128
|
+
|
|
129
|
+
col gap:16 pad:24 max_w:720
|
|
130
|
+
text "Latest" size:28 weight:700
|
|
131
|
+
input label:"Filter" bind:query # every input needs label: or aria_label:
|
|
132
|
+
if n == 0
|
|
133
|
+
text "Nothing yet." color:muted
|
|
134
|
+
else
|
|
135
|
+
list posts as p key:p.id # key: is REQUIRED
|
|
136
|
+
link p.title to:"/posts/{p.id}" size:18
|
|
137
|
+
button "Save" variant:primary
|
|
138
|
+
on tap
|
|
139
|
+
route.to("/")
|
|
140
|
+
```
|
|
141
|
+
Elements are `Name attr:value` + indented children; `Name(a: 1)` in expression
|
|
142
|
+
position. `col row stack grid text img button input link spacer` are auto-imported.
|
|
143
|
+
|
|
144
|
+
`on mount` runs its body once (use it for a fetch `load` doesn't fit — one that
|
|
145
|
+
accumulates instead of replacing, e.g. paginated results: `state`, `on mount`,
|
|
146
|
+
append the result, repeat from a button's `on tap`). `on change` / `on unmount`
|
|
147
|
+
are not implemented yet — don't reach for them.
|
|
148
|
+
|
|
149
|
+
Accessible primitives (roles / focus / keyboard are carried, not written):
|
|
150
|
+
```vl
|
|
151
|
+
if confirming # a dialog is shown by rendering it
|
|
152
|
+
dialog title:"Discard?" on dismiss: confirming = false # Esc + backdrop -> dismiss
|
|
153
|
+
text "Cannot be undone." color:muted
|
|
154
|
+
button "Discard" variant:danger on tap: discard()
|
|
155
|
+
|
|
156
|
+
tabs bind:tab # or: selected:tab + `on select t: ...`
|
|
157
|
+
tab "overview" label:"Overview"
|
|
158
|
+
text "Overview panel"
|
|
159
|
+
tab "settings" label:"Settings"
|
|
160
|
+
text "Settings panel"
|
|
161
|
+
```
|
|
162
|
+
`dialog` needs `title` + `on dismiss`; no `open` attr. `tab` needs an id + `label`,
|
|
163
|
+
only inside `tabs`. Raw `role:` / `aria-*` are not a thing — use the primitive.
|
|
164
|
+
|
|
165
|
+
Every `input` needs an accessible name — *exactly one* of `label:"Email"`
|
|
166
|
+
(visible) or `aria_label:"Search"` (name only); both is `E-INPUT-NAME-DUP`, an
|
|
167
|
+
empty or `placeholder:`-only name is `E-INPUT-NAME`. Optional `help:` and
|
|
168
|
+
`error:` are wired to the control for you:
|
|
169
|
+
```vl
|
|
170
|
+
derive email_err = if touched and not ok "Enter a valid email." else nil
|
|
171
|
+
input label:"Email" bind:email type:email help:"For the receipt." error:email_err
|
|
172
|
+
```
|
|
173
|
+
`help:` links via `aria-describedby`; a non-nil `error:` also sets `aria-invalid`
|
|
174
|
+
and shows the message. `error:` is usually a `derive` that is `nil` when valid.
|
|
175
|
+
Errors are polite live regions. For other feedback use `status message`:
|
|
176
|
+
one string-or-nil expression, no attributes/children. Keep it mounted; nil/""
|
|
177
|
+
is idle. Changes update in place without moving focus; unchanged messages
|
|
178
|
+
are not repeated. Ordinary dynamic `text` is not automatically live.
|
|
179
|
+
|
|
180
|
+
Every `img` needs one `alt:` form — `alt:"real text"` (a dynamic one that goes
|
|
181
|
+
empty is `render.empty_alt`), `alt:decorative` (emits `alt=""`), or `alt:todo` /
|
|
182
|
+
`alt_todo:"note"` (builds + warns; `--release` makes it `E-IMG-ALT-TODO`).
|
|
183
|
+
No `alt` is `E-IMG-ALT`.
|
|
184
|
+
|
|
185
|
+
Every build that renders a page needs a module-level `page title:"…"` (becomes
|
|
186
|
+
`<title>` + `document.title`; missing or empty is `E-PAGE-TITLE`, runtime-empty
|
|
187
|
+
is `render.empty_page_title`). `h1`–`h6` are real headings — level is semantic,
|
|
188
|
+
not `size:`; an intra-view level skip warns. `main` / `nav` are landmarks: one
|
|
189
|
+
`main` per page, and sibling `nav`s need distinct `aria_label:`. A focus-visible
|
|
190
|
+
"Skip to main content" link is injected automatically when there is a `main`.
|
|
191
|
+
|
|
192
|
+
## Auth, outbound, concurrency
|
|
193
|
+
```vl
|
|
194
|
+
auth
|
|
195
|
+
user User
|
|
196
|
+
identity email
|
|
197
|
+
provider password
|
|
198
|
+
min_len 12
|
|
199
|
+
session
|
|
200
|
+
store cookie
|
|
201
|
+
ttl 30d
|
|
202
|
+
roles admin, author, member
|
|
203
|
+
default_role member
|
|
204
|
+
|
|
205
|
+
api blog # outbound client; block name = the namespace
|
|
206
|
+
base "https://api.example.com/v1"
|
|
207
|
+
auth bearer env.BLOG_TOKEN # optional; timeout defaults to 30s
|
|
208
|
+
get post(id uuid, fields str) Post!
|
|
209
|
+
path "/posts/{id}" # {id} interpolates, URL-escaped
|
|
210
|
+
query fields # GET params MUST be named in query (or {..} the path)
|
|
211
|
+
post check(body Report) nil! # post/put/patch: unrouted params are the body
|
|
212
|
+
|
|
213
|
+
par # run together, fail together
|
|
214
|
+
let user = db.users.get(id)?
|
|
215
|
+
let quota = billing.quota(id)?
|
|
216
|
+
spawn # fire and forget; legal in a view, not cancelled on unmount
|
|
217
|
+
mailer.send_welcome(user.email)
|
|
218
|
+
```
|
|
219
|
+
A response decodes into the return type; a missing required field is `decode.missing_field`.
|
|
220
|
+
Client calls: `auth.sign_in(email:, password:)`, `auth.sign_up(...)`,
|
|
221
|
+
`auth.sign_out()`, `auth.verify_totp(code:)`, `route.to("/path")`.
|
|
222
|
+
|
|
223
|
+
## Styling
|
|
224
|
+
|
|
225
|
+
One sibling `theme.vl` groups `color`, `space`, `radius`, `font`, `screen`, and
|
|
226
|
+
`motion reduce respect`; colors may add positional `dark "…"`. Numeric
|
|
227
|
+
`gap:`/`pad:` values are px, while `gap:sm` resolves `theme.space.sm`.
|
|
228
|
+
|
|
229
|
+
```vl
|
|
230
|
+
recipe button
|
|
231
|
+
base "rounded-md transition-colors motion-reduce:transition-none focus-visible:outline focus-visible:outline-accent"
|
|
232
|
+
variant primary "bg-accent text-white"
|
|
233
|
+
default variant:primary
|
|
234
|
+
|
|
235
|
+
card variant:raised
|
|
236
|
+
button "Save" variant:primary class:"w-full sm:w-auto"
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Slice 1 recipes are `button`, `input`, and `card`. A recipe has one literal
|
|
240
|
+
`base`, orthogonal `axis value "classes"` rows, and `default axis:value`.
|
|
241
|
+
`class:` is one complete literal and additive only: no interpolation,
|
|
242
|
+
`!important`, recipe property conflict, or recipe-plus-inline override.
|
|
243
|
+
Unknown utilities fail the CSS build. Interactive recipes need visible-focus
|
|
244
|
+
classes; motion needs `motion-reduce:`. Generated CSS, not class-token order,
|
|
245
|
+
decides the cascade.
|
|
246
|
+
|
|
247
|
+
## Project preferences and automation IDs
|
|
248
|
+
|
|
249
|
+
Build/check discover nearest `volaro.json` up to the Git root (the old
|
|
250
|
+
`volara.json` name is a hard error, not a fallback); `--config PATH` overrides
|
|
251
|
+
discovery. Example: `{"schema_version":1,"testing":{"emit_ids":true}}`.
|
|
252
|
+
No config defaults to emitting authored hooks. Unknown options/types/versions
|
|
253
|
+
and duplicate JSON keys are errors. No secrets or accessibility opt-out.
|
|
254
|
+
|
|
255
|
+
`button "Save" test_id:"save"` emits data-testid. Local ID: static lowercase
|
|
256
|
+
slug, 1–80 chars. Scope: `Field test_scope:"editor"` makes its local
|
|
257
|
+
`test_id:"name"` become `editor/name`. Row scope may be an explicit public
|
|
258
|
+
string expression: `row test_scope:item.public_slug`. Never expose a list key
|
|
259
|
+
automatically or use private identifiers. Scopes propagate into local views and
|
|
260
|
+
dialog content; duplicate live IDs fail before the next tree is committed.
|
|
261
|
+
Use scopes, not test_id, on component/dialog/tabs calls; tab hooks unsupported.
|
|
262
|
+
Disabling emit_ids removes hooks without changing labels, events or behaviour.
|