@roxyon/mcp 0.1.0
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 +63 -0
- package/bin/roxyon-mcp.js +2 -0
- package/dist/index.js +615 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
- package/resources/baas.md +61 -0
- package/resources/deploy.md +64 -0
- package/resources/lumenjs.md +1914 -0
|
@@ -0,0 +1,1914 @@
|
|
|
1
|
+
# LumenJS — Complete Reference for AI Models
|
|
2
|
+
|
|
3
|
+
> **Audience note for the reader (human or AI):** this document assumes you already know Vue and/or React well, and explains LumenJS **by contrast**. Wherever LumenJS syntax visually resembles Vue or React, assume it behaves like Vue/React **unless this document explicitly says otherwise** — the differences are usually small but breaking (e.g. `:if` looks like Vue's shorthand-bind but is actually Vue's `v-if`).
|
|
4
|
+
|
|
5
|
+
> **Target version: LumenJS V1.** Everything in this document describes V1,
|
|
6
|
+
> which is what the Rahimoun app and the Console are written in. V1 requires
|
|
7
|
+
> explicit `bind=` declarations for reactivity (§5.1) and `var` for every bound
|
|
8
|
+
> variable. A later V2 removes `bind` in favour of automatic reactivity — do not
|
|
9
|
+
> mix V2 idioms into V1 code.
|
|
10
|
+
|
|
11
|
+
**Sources used to compile this document:**
|
|
12
|
+
- The live `lumenjs.com` docs-site checkout (sparse — only a loops guide and a header widget survive in the current working copy).
|
|
13
|
+
- A fuller historical archive of the same docs site (`_ws17/lumenjs.com`, `lumenjs.com_bu`), which contains ~50 additional guide pages not present in the current checkout (routing, modals, storage, drag/sort, events, uploader, realtime API, etc).
|
|
14
|
+
- The compiled framework engine itself (`Rahimoun/src/js/bea.js`), reverse-read for directive names and exact runtime behavior.
|
|
15
|
+
- A large real production app (**Rahimoun**, a charity-management SPA) for real-world usage patterns across ~150 `.view` files.
|
|
16
|
+
|
|
17
|
+
Where the docs-site prose and the engine source disagreed or a feature was undocumented, the engine source (i.e. actual runtime behavior) was treated as authoritative.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. What LumenJS is
|
|
22
|
+
|
|
23
|
+
LumenJS is a small, **no-build, no-virtual-DOM** reactive framework created by a company called BEA (also the name behind `beacdn.com`, `beaapis.com`, `bea.sa`). It ships as a single client-side script (historically loaded from a CDN, e.g. `https://s.beacdn.com/e/`, plus a local compiled copy such as `bea.js`) built on top of jQuery. There is no compilation step for the framework itself — `.view` files are plain HTML+JS+CSS files fetched and interpreted directly in the browser.
|
|
24
|
+
|
|
25
|
+
It is paired with a companion CSS framework called **Mobius CSS**, which supplies the utility classes (`w50`, `p20`, `round`, `fs16`, `c`, `l`, `r`, etc.) and a handful of custom elements used purely for layout (`<tbl_nm>`, `<rw>`, `<cl>`, `<grid>`, `<grid_nm>`). **These layout tags are not LumenJS directives** — they're Mobius CSS's flexbox/table abstraction, analogous to Bootstrap's `<div class="row">`/`<div class="col">`. This document does not attempt to be a Mobius CSS reference; only the LumenJS-authored parts of markup are documented in full.
|
|
26
|
+
|
|
27
|
+
There is also a companion CLI (`@lmjs/cli`, invoked as `lm`, `lu`, or `lumen`) that scaffolds and serves projects.
|
|
28
|
+
|
|
29
|
+
### Mental model, contrasted with Vue/React up front
|
|
30
|
+
|
|
31
|
+
| Concept | Vue / React | LumenJS |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| Compilation | Vue SFCs / JSX compile to render functions | **None.** `.view` files are HTML fetched over the network and diffed/patched live by the runtime |
|
|
34
|
+
| Component identity | Explicit component classes/functions with lifecycle | A "view" is just an HTML file; a "component" (`comp=`) and a "template" (`tpl=`) are two *other*, different kinds of reusable HTML files — see §4 |
|
|
35
|
+
| State declaration | `data()`/`ref()`/`useState()` | **`var` (never `let`/`const`) in a `<script>` tag inside the view.** A variable becomes reactive only when named in `bind="varName"` on an ancestor element — see §5.1 |
|
|
36
|
+
| Routing | A router library (vue-router, react-router) with route config | **URL path segments map directly to nested view files on disk.** No route config; `nodes[n]` gives you the URL segments at runtime (§9) |
|
|
37
|
+
| Global store | Vuex/Pinia/Redux/Context | A single global `globals` object, auto-persisted to `localStorage` |
|
|
38
|
+
| List rendering | `v-for` / `.map()` | `:for` — similar syntax, very different semantics (arrays *or* a plain number to repeat N times) — see §6.1 |
|
|
39
|
+
| Conditional rendering | `v-if`/`v-else-if`/`v-else` or ternaries/`&&` | `:if` / `:else-if` / `:else` — **same names as Vue minus the `v-` prefix**, and mostly the same semantics |
|
|
40
|
+
| Two-way form binding | `v-model` | **No `:model` equivalent exists.** Forms are handled wholesale via `o-sub`/`b-sub` on the `<form>` (§7) |
|
|
41
|
+
| Event binding | `@click` (Vue) / `onClick` (React) | `@click` — **same syntax as Vue**, but the event catalog is much larger (touch/swipe/drag/sort/scroll — §6.3) and handlers receive `(ev, el)` where `el` is a **jQuery-wrapped** element, not a native DOM node |
|
|
42
|
+
| HTTP client | fetch/axios + your own code | Built-in global `API.get/post/put/delete/patch(path, data, headers, api_index)`, configured via `config.json` |
|
|
43
|
+
| Watchers | `watch()`/`useEffect()` | A single global `watch = { varName: fn }` object (§5.3) |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 2. Project anatomy
|
|
48
|
+
|
|
49
|
+
A LumenJS project (scaffolded by `lm create`) has this shape (from the real `Rahimoun` app):
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
src/
|
|
53
|
+
index.js # Reactor() bootstrap — the only mandatory JS entry point
|
|
54
|
+
index.html # shell HTML: <script> tags + <div main></div> mount point
|
|
55
|
+
config.json # API base URLs/headers + local dev server + (optionally) FTP deploy config
|
|
56
|
+
css/ # plain CSS
|
|
57
|
+
js/ # bea.js (the compiled engine) + any extra scripts
|
|
58
|
+
views/ # every .view file = one URL route (see §9)
|
|
59
|
+
home.view
|
|
60
|
+
login.view
|
|
61
|
+
account/
|
|
62
|
+
admin/
|
|
63
|
+
dashboard.view -> /account/admin/dashboard
|
|
64
|
+
modals/
|
|
65
|
+
user.view -> opened as a modal, not a route
|
|
66
|
+
widgets/
|
|
67
|
+
header.view -> shared chrome, not a route
|
|
68
|
+
tpls/ # .tpl files: list-item / row templates rendered via `tpl="name"` (see §4.3)
|
|
69
|
+
comps/ # .tpl files: reusable "components" rendered via `comp="name"` (see §4.2)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
There is no `dist/` step required during development; `lm build` produces a deployable bundle (optionally `--serverless`) and `lm build --deploy` / `lm build --serverless --deploy` push it directly.
|
|
73
|
+
|
|
74
|
+
### 2.1 Which files in THIS repo are production (important)
|
|
75
|
+
|
|
76
|
+
Most of this repo is **old UI kept for reference only**. Do not treat a stale file
|
|
77
|
+
as evidence of current conventions, schema, or stack.
|
|
78
|
+
|
|
79
|
+
**Production-ready:**
|
|
80
|
+
|
|
81
|
+
| Path | Notes |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `src/views/_dashboards/cloudhosting/**` | all files; finalized, Hetzner-Console-style UI |
|
|
84
|
+
| `src/views/_forms/` | only `pay.view`, `domains_create.view`, `cloudhosting_create.view` |
|
|
85
|
+
| `src/views/_lists/` | all |
|
|
86
|
+
| `src/views/modals/` | only `pay`, `done`, `confirm_payment` |
|
|
87
|
+
| `src/views/_account/` | `invoices`, `members`, `profile`, `settings` |
|
|
88
|
+
| `src/views/*.view` | top-level routing shells: `account`, `cloudhosting`, `domains`, `auth`, `dashboard` |
|
|
89
|
+
|
|
90
|
+
**Everything else is stale reference.** Notably `src/views/modals/add_domain.view`
|
|
91
|
+
is NOT production — its "System (Apache) Page" strings contradict the real stack.
|
|
92
|
+
|
|
93
|
+
### 2.2 Platform facts
|
|
94
|
+
|
|
95
|
+
- Web server is **nginx**. There is no `.htaccess`; per-site rules are generated
|
|
96
|
+
nginx config plus a reload.
|
|
97
|
+
- Each hosting subscription is an **Incus container** ("Roxyon Secure Isolation
|
|
98
|
+
Layer"), so per-account CPU/memory/process/inode metrics are obtainable.
|
|
99
|
+
- The realtime backend is **Swoole over WebSocket**.
|
|
100
|
+
- Production code calls the backend through the app wrappers **`rx.*` / `engine.*`**,
|
|
101
|
+
not the generic `API.*` global documented in §10.
|
|
102
|
+
|
|
103
|
+
### CLI commands (exhaustive, from the docs)
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm i @lmjs/cli -g # install the CLI
|
|
107
|
+
lm create My First Project # scaffold a new project
|
|
108
|
+
lm run # or: lm serve — dev server
|
|
109
|
+
lm create -v myview # scaffold a new top-level view
|
|
110
|
+
lm create -s mysubview # scaffold a sub-view
|
|
111
|
+
lm create -t mytpl # scaffold a template (tpls/ file)
|
|
112
|
+
lm purge # purge offline cache (PWA-style)
|
|
113
|
+
lm build # production build + Node.js server
|
|
114
|
+
lm build --deploy # ...and deploy
|
|
115
|
+
lm build --serverless # production build, no server
|
|
116
|
+
lm build --serverless --deploy # ...and deploy
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 3. Bootstrapping: `Reactor(...)`
|
|
122
|
+
|
|
123
|
+
`index.js` calls a single global function, `Reactor`, once:
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
Reactor({
|
|
127
|
+
App: "[main]", // name of the main view component (mount target)
|
|
128
|
+
defaultView: "home", // view shown at "/"
|
|
129
|
+
authView: "login", // view shown when `auth()` returns false
|
|
130
|
+
init: function () {
|
|
131
|
+
// runs once, before first render — do async bootstrap fetches here
|
|
132
|
+
},
|
|
133
|
+
tick: function () {
|
|
134
|
+
// runs after EVERY re-render (any reactive variable change), like a
|
|
135
|
+
// global post-render hook. No dependency array — it just always fires.
|
|
136
|
+
},
|
|
137
|
+
auth: function () {
|
|
138
|
+
return true; // gate for any view whose <settings> declares requireAuth: true
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Contrast with Vue/React: there is no equivalent of `createApp().mount()` with a component tree — `Reactor()` *is* the entire app definition. `tick` has no React/Vue analogue; it is closest to a global, un-scoped `useEffect` with no dependency array that runs after *every* DOM patch anywhere in the app.
|
|
144
|
+
|
|
145
|
+
`Reactor` also exposes methods for programmatic control: `render`, `navigate`, `authorize`, `deauthorize` (documented but usage examples were not recovered from the archives).
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 4. Three kinds of reusable HTML: views, components, templates
|
|
150
|
+
|
|
151
|
+
LumenJS has **three distinct file kinds**, easy to conflate with Vue/React "components" — they are not interchangeable:
|
|
152
|
+
|
|
153
|
+
| Kind | Folder | Invoked with | Maps to a URL? | Analogue |
|
|
154
|
+
|---|---|---|---|---|
|
|
155
|
+
| **View** | `views/` | automatically, by URL path, or `view="path/to/file"` | Yes (top-level) / No (sub-view) | Vue/React "page" component |
|
|
156
|
+
| **Component** | `comps/` | `<div comp="name" :data="varName"></div>` | Never | A reusable Vue/React component |
|
|
157
|
+
| **Template** | `tpls/` | `<div tpl="name">` — almost always paired with `:for` to render one template per array item | Never | A Vue `<slot>`-less render-prop / list-item partial |
|
|
158
|
+
|
|
159
|
+
### 4.1 Views and sub-views
|
|
160
|
+
|
|
161
|
+
A `.view` file can freely mix HTML, a `<script>` block, and a `<style>` block — order doesn't matter, and there's no single-file-component wrapper syntax (no `<template>` tag like Vue SFCs — the whole file body up to `<script>`/`<style>` *is* the template).
|
|
162
|
+
|
|
163
|
+
```html
|
|
164
|
+
<div class="textColor" bind="text">
|
|
165
|
+
This is your text: {{text}}
|
|
166
|
+
</div>
|
|
167
|
+
<script>
|
|
168
|
+
var text = "Hello World !";
|
|
169
|
+
</script>
|
|
170
|
+
<style>
|
|
171
|
+
.textColor { color: green; }
|
|
172
|
+
</style>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A **sub-view** is just another view file nested inside a parent via the `view` attribute:
|
|
176
|
+
|
|
177
|
+
```html
|
|
178
|
+
<div view="child"></div>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Pass data down to it with `:data` (the child must `bind` the same variable name to render it):
|
|
182
|
+
|
|
183
|
+
```html
|
|
184
|
+
<div bind="text,childName">
|
|
185
|
+
This is your text: {{text}}
|
|
186
|
+
<div view="child" :data="childName"></div>
|
|
187
|
+
</div>
|
|
188
|
+
<script>
|
|
189
|
+
var text = "Hello World !";
|
|
190
|
+
var childName = "Mike";
|
|
191
|
+
</script>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Inside `child.view`, the passed value shows up as a same-named variable (bound implicitly) — there is no `props` object for views the way there is for components (see below) or modals (see §8).
|
|
195
|
+
|
|
196
|
+
**Per-view settings** — a `<settings>` block (JSON) controls chrome and auth:
|
|
197
|
+
|
|
198
|
+
```html
|
|
199
|
+
<settings>
|
|
200
|
+
{
|
|
201
|
+
"hasNav": false,
|
|
202
|
+
"hasHeader": false,
|
|
203
|
+
"hasFooter": false,
|
|
204
|
+
"requireAuth": false
|
|
205
|
+
}
|
|
206
|
+
</settings>
|
|
207
|
+
```
|
|
208
|
+
`hasNav`/`hasHeader`/`hasFooter` accept `true`, `false`, or a string path to a custom widget view. `requireAuth: true` routes unauthenticated visitors to `authView` (via the `Reactor` `auth()` callback).
|
|
209
|
+
|
|
210
|
+
### 4.2 Components (`comps/*.tpl`, invoked with `comp=`)
|
|
211
|
+
|
|
212
|
+
```html
|
|
213
|
+
<!-- comps/comp.tpl -->
|
|
214
|
+
<div bind="compName">
|
|
215
|
+
Hello I'm a Component called ~ {{compName}} ~
|
|
216
|
+
</div>
|
|
217
|
+
```
|
|
218
|
+
```html
|
|
219
|
+
<!-- calling view -->
|
|
220
|
+
<div bind="compName">
|
|
221
|
+
Below is a Component
|
|
222
|
+
<div comp="comp" :data="compName"></div>
|
|
223
|
+
</div>
|
|
224
|
+
<script>
|
|
225
|
+
var compName = "Mike";
|
|
226
|
+
</script>
|
|
227
|
+
```
|
|
228
|
+
Functionally very similar to a sub-view invoked with `view=` — the docs themselves describe components as "a multi-used item," e.g. a card used in several places. In practice, real code (Rahimoun) leans on `tpl=` far more than `comp=` for this purpose.
|
|
229
|
+
|
|
230
|
+
### 4.3 Templates (`tpls/*.tpl`, invoked with `tpl=`)
|
|
231
|
+
|
|
232
|
+
Templates are almost always combined with `:for` to render one instance per collection item, with the loop item's own scope available inside — this is the real workhorse for list rows/cards in production code:
|
|
233
|
+
|
|
234
|
+
```html
|
|
235
|
+
<!-- tpls/item.tpl -->
|
|
236
|
+
<tbl_nm class="" fixed>
|
|
237
|
+
<rw>
|
|
238
|
+
<cl :for="lstOpts['columns'] as key,cl" :for-if="!cl['hide']">
|
|
239
|
+
<div :if="key=='Status'">{{ statuses[item[key]].name }}</div>
|
|
240
|
+
<div :else>{{ item[key] }}</div>
|
|
241
|
+
</cl>
|
|
242
|
+
</rw>
|
|
243
|
+
</tbl_nm>
|
|
244
|
+
```
|
|
245
|
+
```html
|
|
246
|
+
<!-- calling view/template -->
|
|
247
|
+
<li :for="lstOpts['items'] as item; lok" tpl="item"></li>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`tpl` also accepts an **inline attribute-value template literal** convention used pervasively for computed class strings — see §6.5.
|
|
251
|
+
|
|
252
|
+
### 4.4 The built-in list components: `<lst>` and `<lmlst>`
|
|
253
|
+
|
|
254
|
+
Two engine-provided custom elements exist purely to render paginated/data-bound lists without hand-rolled `:for` markup, seen throughout Rahimoun:
|
|
255
|
+
|
|
256
|
+
```html
|
|
257
|
+
<lst class="clear" tpl="list" :data="lstOpts"></lst>
|
|
258
|
+
```
|
|
259
|
+
```html
|
|
260
|
+
<lmlst @query="queryList" :config="myConfig" />
|
|
261
|
+
```
|
|
262
|
+
`<lst>` renders via a `tpls/list.tpl` template you supply (itself using `:for`/`:for-if`/`tpl="item"` internally, plus a `<lst-footer>` pager block driven by `lstOpts.page`/`lstOpts.pages`). `<lmlst>` is a newer/alternate variant driven by a `:config` object and a `@query` event instead of a pre-fetched `lstOpts` object. Neither has a public options reference recovered from the docs archive — treat them as app-level list-rendering sugar built on the primitives documented below, not a documented public API surface.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## 5. Reactivity
|
|
267
|
+
|
|
268
|
+
### 5.1 `bind` — declaring what's reactive
|
|
269
|
+
|
|
270
|
+
**This is the single biggest conceptual difference from Vue/React**, and the one most likely to trip up a model that "fills the gap with Vue by default":
|
|
271
|
+
|
|
272
|
+
- In Vue, *any* variable returned from `setup()`/`data()` is reactive everywhere it's used, automatically.
|
|
273
|
+
- In LumenJS, a variable in a view's `<script>` is **not tracked for re-rendering unless some ancestor element carries `bind="thatVariableName"`**.
|
|
274
|
+
|
|
275
|
+
```html
|
|
276
|
+
<div bind="name">
|
|
277
|
+
<p>My name is: {{name}}</p>
|
|
278
|
+
</div>
|
|
279
|
+
<script>
|
|
280
|
+
var name = "Raouf";
|
|
281
|
+
</script>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
#### Rule 1 — reactive state is declared with `var`, never `let` or `const`
|
|
285
|
+
|
|
286
|
+
Any variable named in a `bind=` attribute **must** be declared with `var` in the
|
|
287
|
+
view's `<script>`. This is not a style preference; `let` and `const` are
|
|
288
|
+
block-scoped and are not visible to the runtime's expression evaluator, so a
|
|
289
|
+
`bind`-ed `let` silently never renders and never updates. There is no error.
|
|
290
|
+
|
|
291
|
+
```html
|
|
292
|
+
<!-- CORRECT -->
|
|
293
|
+
<script>
|
|
294
|
+
var items = [];
|
|
295
|
+
var loading = false;
|
|
296
|
+
</script>
|
|
297
|
+
|
|
298
|
+
<!-- BROKEN — renders nothing, reports nothing -->
|
|
299
|
+
<script>
|
|
300
|
+
let items = [];
|
|
301
|
+
const loading = false;
|
|
302
|
+
</script>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`let`/`const` are fine for variables that are **never** named in a `bind=` and
|
|
306
|
+
never appear in `{{ }}` — loop counters inside a function body, intermediate
|
|
307
|
+
values, and so on. The moment a variable is rendered or bound, it must be `var`.
|
|
308
|
+
|
|
309
|
+
#### Rule 1b — `bind` renders DESCENDANTS only, never its own element
|
|
310
|
+
|
|
311
|
+
The element carrying `bind` does **not** get its own attributes interpolated —
|
|
312
|
+
only the subtree beneath it. So `bind` always goes on the **parent** of whatever
|
|
313
|
+
needs rendering.
|
|
314
|
+
|
|
315
|
+
```html
|
|
316
|
+
<!-- BROKEN — renders the literal {{nodes[1]}} into the href -->
|
|
317
|
+
<a href="/cloudhosting/{{nodes[1]}}/advanced" bind="nodes">Back</a>
|
|
318
|
+
|
|
319
|
+
<!-- CORRECT — bind on the parent -->
|
|
320
|
+
<flx bind="nodes">
|
|
321
|
+
<a href="/cloudhosting/{{nodes[1]}}/advanced">Back</a>
|
|
322
|
+
</flx>
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The bind list controls **when** to re-render, not what is resolvable: a subtree
|
|
326
|
+
under `bind="globals"` can still interpolate `{{nodes[1]}}` fine.
|
|
327
|
+
|
|
328
|
+
#### Rule 1c — never nest a `bind` inside another `bind`
|
|
329
|
+
|
|
330
|
+
A `bind` on a descendant of another `bind` silently stops the subtree rendering;
|
|
331
|
+
interpolations come out empty with no error. Use **sibling** binds for independent
|
|
332
|
+
regions, never nested ones. One bind per region is enough.
|
|
333
|
+
|
|
334
|
+
#### Rule 1d — put `bind` on the lowest element that needs it
|
|
335
|
+
|
|
336
|
+
A bound subtree is re-rendered **wholesale**, so everything inside is rebuilt:
|
|
337
|
+
a Select2 control is re-initialised, an input the user is typing into is emptied,
|
|
338
|
+
a `<select>` loses its selection. Keep static or stateful controls **outside** the
|
|
339
|
+
bound region, and bind only the part that actually changes.
|
|
340
|
+
|
|
341
|
+
#### Rule 1e — `:if` goes on a CHILD of the bound element
|
|
342
|
+
|
|
343
|
+
Never on the bound element itself (any depth below is fine):
|
|
344
|
+
|
|
345
|
+
```html
|
|
346
|
+
<div bind="alerts">
|
|
347
|
+
<flx :if="alerts.length > 0"> … </flx>
|
|
348
|
+
</div>
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Give such a wrapper `display: contents` so it does not become a flex item and
|
|
352
|
+
introduce a stray gap.
|
|
353
|
+
|
|
354
|
+
#### Rule 2 — every rendered variable needs a `bind` ancestor
|
|
355
|
+
|
|
356
|
+
`bind` takes a **comma-separated list** of variable names: `bind="items,index"`.
|
|
357
|
+
Nothing is inferred from usage inside `{{ }}` — every top-level variable you
|
|
358
|
+
mutate and expect to re-render must be listed explicitly on an enclosing
|
|
359
|
+
element.
|
|
360
|
+
|
|
361
|
+
Omitting `bind` is the single most common failure mode when writing LumenJS
|
|
362
|
+
with Vue habits: the initial value renders once and then never updates again,
|
|
363
|
+
with no console error and no visible symptom until a user reports stale data.
|
|
364
|
+
If a value appears in `{{ }}` or drives a `:if` / `:for`, it belongs in a
|
|
365
|
+
`bind`.
|
|
366
|
+
|
|
367
|
+
One `bind` on a wrapper covering the whole reactive region is idiomatic;
|
|
368
|
+
scattering many small `bind`s is not:
|
|
369
|
+
|
|
370
|
+
```html
|
|
371
|
+
<div bind="items,loading,filters">
|
|
372
|
+
<div :if="loading">Loading…</div>
|
|
373
|
+
<div :for="items as item">{{ item.Name }}</div>
|
|
374
|
+
</div>
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Contrast with Vue: there is no reactive-proxy wrapping of the variable itself —
|
|
378
|
+
mutating `items.push(x)` works because LumenJS re-diffs the DOM subtree under
|
|
379
|
+
the nearest `bind` ancestor, not because the array became a Proxy. Equally,
|
|
380
|
+
**reassigning** the whole variable (`items = newArray`) is fine and is the
|
|
381
|
+
normal pattern after a fetch; Vue's `.value` / immutable-update rules do not
|
|
382
|
+
apply.
|
|
383
|
+
|
|
384
|
+
### 5.2 `{{ }}` interpolation, and the backtick variant
|
|
385
|
+
|
|
386
|
+
Standard interpolation is `{{ expression }}` — this works in text nodes and (unlike Vue) directly inside quoted attribute values:
|
|
387
|
+
|
|
388
|
+
```html
|
|
389
|
+
<p>My name is: {{ Ourarray[`Ourindex`] }}</p>
|
|
390
|
+
```
|
|
391
|
+
Note the inner variable-in-a-variable syntax: to index an array/object with another *variable's* value inside `{{ }}`, wrap the index variable in backticks: `{{ Ourarray[\`Ourindex\`] }}`. This is **not** a JS template literal — it's LumenJS's own escape hatch inside the moustache parser to disambiguate "the literal key `Ourindex`" from "the value of the variable `Ourindex`."
|
|
392
|
+
|
|
393
|
+
A **second, different** backtick convention shows up in real production attribute values — wrapping an *entire* `{{ }}` expression (not just a sub-key) in backticks, seen extensively for computed class/style strings:
|
|
394
|
+
|
|
395
|
+
```html
|
|
396
|
+
<cl class="`{{lstOpts['columns'][key]['_mini'] ? 'lst-cl-mini' : 'lst-cl'}}`">
|
|
397
|
+
```
|
|
398
|
+
This is the safe way to interpolate an expression that itself contains quotes (ternaries with string literals) into an already-quoted attribute — plain `{{ }}` works for simple attribute interpolation, but reach for the backtick-wrapped form once the expression contains its own `'single quotes'`.
|
|
399
|
+
|
|
400
|
+
### 5.2b Function calls inside `{{ }}` are not re-evaluated
|
|
401
|
+
|
|
402
|
+
A function call in a moustache renders once and then goes **stale** — it is not
|
|
403
|
+
re-run when the subtree re-renders. Compute into a plain `var` and render that,
|
|
404
|
+
recomputing wherever the state changes:
|
|
405
|
+
|
|
406
|
+
```html
|
|
407
|
+
<!-- stale after the first render -->
|
|
408
|
+
<pre>{{buildNginx()}}</pre>
|
|
409
|
+
|
|
410
|
+
<!-- correct -->
|
|
411
|
+
<pre>{{confText}}</pre>
|
|
412
|
+
```
|
|
413
|
+
```js
|
|
414
|
+
function commit() {
|
|
415
|
+
rules = rules.slice();
|
|
416
|
+
confText = buildNginx(); // recompute on every mutation
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### 5.3 `watch` — the global watcher object
|
|
421
|
+
|
|
422
|
+
There is no per-component `watch()` call. There is exactly **one** global object, `watch`, keyed by variable name:
|
|
423
|
+
|
|
424
|
+
```js
|
|
425
|
+
watch = {
|
|
426
|
+
myVariable: function () {
|
|
427
|
+
// fires whenever `myVariable`'s value changes (deep — including nested object mutation)
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
```
|
|
431
|
+
**Gotcha (explicitly called out in the docs):** `watch = {...}` is a plain object *assignment*, so declaring it twice **overwrites** the first set of watchers entirely — there's no merging. Two safe patterns:
|
|
432
|
+
```js
|
|
433
|
+
// 1. one object literal, multiple keys
|
|
434
|
+
watch = {
|
|
435
|
+
myKey: function () { /* ... */ },
|
|
436
|
+
myKey2: function () { /* ... */ },
|
|
437
|
+
};
|
|
438
|
+
// 2. property assignment, anywhere, any number of times
|
|
439
|
+
watch["myKey3"] = function () { /* ... */ };
|
|
440
|
+
```
|
|
441
|
+
This is a sharp edge with no Vue/React equivalent — Vue's `watch: {}` option and multiple `watch()` calls never clobber each other like this.
|
|
442
|
+
|
|
443
|
+
### 5.4 `tick()` — global post-render hook
|
|
444
|
+
|
|
445
|
+
Declared once, inside `Reactor({...})` (§3). Fires after *every* re-render anywhere in the app — there's no scoping, no dependency array, and no per-component equivalent.
|
|
446
|
+
|
|
447
|
+
### 5.5 `nodes` — the URL, pre-split
|
|
448
|
+
|
|
449
|
+
```js
|
|
450
|
+
alert(nodes); // e.g. ["guide", "global-variables", "nodes"]
|
|
451
|
+
```
|
|
452
|
+
`nodes` is the URL path split on `/`. **`nodes[0]` is always the main/top-level view name.** Nested sub-view routing (e.g. `views/_guide/*.view` dispatching on `nodes[1]`) is done by hand, by `bind`-ing `nodes` and rendering `<div view="_guide/{{nodes[1]}}/{{nodes[2]}}"></div>` with a default fallback:
|
|
453
|
+
```html
|
|
454
|
+
<div bind="nodes">
|
|
455
|
+
<div view="_guide/{{nodes[1]}}/{{nodes[2]}}"></div>
|
|
456
|
+
</div>
|
|
457
|
+
<script>
|
|
458
|
+
if (!nodes[2]) nodes[2] = "structure";
|
|
459
|
+
</script>
|
|
460
|
+
```
|
|
461
|
+
This is the entirety of LumenJS's router — **there is no route-config file, no `<Route>` component, no `useParams()`.** Nesting is just string-building a `view=` path out of `nodes[n]` segments yourself.
|
|
462
|
+
|
|
463
|
+
### 5.6 `globalWatch()` — re-scanning DOM you inserted yourself
|
|
464
|
+
|
|
465
|
+
When you build HTML as a string and drop it in with `$(sel).html(...)`, `.append(...)`,
|
|
466
|
+
`el.insertAdjacentHTML(...)` etc., LumenJS **does not know it happened**. The engine
|
|
467
|
+
only scans on its own render passes. Anything in that fragment that relies on the
|
|
468
|
+
engine wiring something up stays dead until you call the global **`globalWatch()`**.
|
|
469
|
+
|
|
470
|
+
`globalWatch()` (defined in `bea.js`) runs, in order: re-executes any
|
|
471
|
+
`<script type="beajs">` blocks → binds every `[bind]:not(.binded)` → then
|
|
472
|
+
`renderViewsTpls()` (`[view]` / `[tpl]` / `[comp]`) → `renderPlugins()` → `loadPics()`
|
|
473
|
+
(`[lazy]`) → `reloads()` → `tick()`.
|
|
474
|
+
|
|
475
|
+
`renderPlugins()` is the important one — it is what initialises:
|
|
476
|
+
|
|
477
|
+
| selector | becomes |
|
|
478
|
+
|---|---|
|
|
479
|
+
| `[sl]:not(.select2-hidden-accessible)` | a Select2 control |
|
|
480
|
+
| `form:not(.binded,[norm])` | an `o-sub` / `b-sub` intercepted form |
|
|
481
|
+
| `[crslf]:not(.flickity-enabled)` / `[crsl]:not(.crsl-initialized)` | a carousel |
|
|
482
|
+
| `[up]:not([upid])` | a file-drop uploader (injects the hidden `<input type=file>`) |
|
|
483
|
+
|
|
484
|
+
Every one of those has a `:not(...)` guard, so **`globalWatch()` is idempotent** —
|
|
485
|
+
calling it again never double-initialises what's already live. Call it once, after
|
|
486
|
+
the DOM insert:
|
|
487
|
+
|
|
488
|
+
```js
|
|
489
|
+
$('.host').html('<select sl sl-prt="self" name="zone">' + opts + '</select>');
|
|
490
|
+
globalWatch(); // now it's a Select2, not a bare <select>
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
**What does NOT need `globalWatch()`:** `@click` / `@change` / other `@event`
|
|
494
|
+
directives in injected HTML already work — the engine delegates events from
|
|
495
|
+
`document`, so a handler on a `<span @click="fn">` fires even though the span was
|
|
496
|
+
added after render (this also survives Iconify swapping the `<span class="iconify">`
|
|
497
|
+
for an `<svg>`). It is only the *plugin-wrapping* attributes above, plus `[bind]`
|
|
498
|
+
and `{{ }}` interpolation, that need the re-scan.
|
|
499
|
+
|
|
500
|
+
**Iconify** specifically: `<span class="iconify" data-icon="…">` is picked up by
|
|
501
|
+
Iconify's own `MutationObserver`, so it usually renders on its own — but after a big
|
|
502
|
+
`innerHTML`/`outerHTML` swap call `Iconify.scan()` (guarded: `window.Iconify && Iconify.scan()`)
|
|
503
|
+
if an icon fails to appear. **Do not `addClass`/`removeClass` an iconify element to change
|
|
504
|
+
its state** (e.g. a green "on" class): Iconify replaces the `<span>` with a freshly built
|
|
505
|
+
`<svg>` and re-copies the *placeholder's original* class string, so a class you added a tick
|
|
506
|
+
earlier is silently dropped. Toggle the class on a non-iconify wrapper, or use a plain
|
|
507
|
+
element for the stateful control (a CSS pill switch, etc.).
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## 6. Directives — exhaustive reference
|
|
512
|
+
|
|
513
|
+
Attribute-directive syntax uses two prefixes, both borrowed visually from Vue: `:` for data/structural bindings, `@` for events. **Do not assume Vue semantics beyond the name** — cross-reference every entry below.
|
|
514
|
+
|
|
515
|
+
### 6.1 `:for` — loops (contrast: Vue `v-for`)
|
|
516
|
+
|
|
517
|
+
| Vue `v-for` | LumenJS `:for` |
|
|
518
|
+
|---|---|
|
|
519
|
+
| `v-for="item in items"` | `:for="items as item"` |
|
|
520
|
+
| `v-for="(item, i) in items"` | `:for="items as item; i"` (custom index name after `;`) |
|
|
521
|
+
| `v-for="(val, key) in obj"` | `:for="myObject as key, value"` |
|
|
522
|
+
| `v-for="n in 5"` (Vue supports N too) | `:for="5"` |
|
|
523
|
+
| implicit loop var `item` name required | **default item/index names exist**: bare `:for="items"` (no `as`) still gives you `{{ number }}`-style item fields directly in scope, plus a magic `index` variable, with no `as` clause at all |
|
|
524
|
+
| key required (`:key`) | no key attribute exists or is needed |
|
|
525
|
+
|
|
526
|
+
Forms, exhaustively:
|
|
527
|
+
```html
|
|
528
|
+
<li :for="items">The number is: {{number}}</li> <!-- array of objects, no alias -->
|
|
529
|
+
<div :for="3">Item {{index}}</div> <!-- fixed repeat count, no data -->
|
|
530
|
+
<li :for="cartItems as item">{{ item }}</li> <!-- array, aliased -->
|
|
531
|
+
<rw :for="myData as item">...</rw> <!-- array of objects, aliased -->
|
|
532
|
+
<li :for="myObject as key, value">{{ key }}: {{ value }}</li> <!-- object iteration -->
|
|
533
|
+
<li :for="newItems as item">{{ index + 1 }}. {{ item }}</li> <!-- magic `index`, 0-based -->
|
|
534
|
+
<li :for="newItems as item; myIndex">{{ myIndex + 1 }}. {{ item }}</li> <!-- custom index name -->
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
**Attributes on the `:for` element itself need backticks.** Interpolation in an
|
|
538
|
+
attribute of the looping element only resolves when the moustache is wrapped in
|
|
539
|
+
backticks — that is what tells the engine to evaluate it in loop scope. Object-key
|
|
540
|
+
iteration uses **double** backticks:
|
|
541
|
+
|
|
542
|
+
```html
|
|
543
|
+
<div :for="arrayOfElements as kuku" data-id="`{{kuku + ' ' + index}}`">
|
|
544
|
+
<div :for="ObjectData as k,v" data-id="``{{k}}``">
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Without the backticks the attribute renders empty. (Descendants of the loop element
|
|
548
|
+
interpolate normally, so the alternative is to put the loop on a bare wrapper and
|
|
549
|
+
the interpolated attributes on an inner element.)
|
|
550
|
+
|
|
551
|
+
**Never put `:for` inside a form control or anything holding live state.** A V1
|
|
552
|
+
re-render rebuilds the subtree and destroys that state — a `<select>` loses its
|
|
553
|
+
selection, and an input the user is mid-way through typing into is emptied. Build
|
|
554
|
+
those options as an HTML string in JS and inject them with `.html()`, or write them
|
|
555
|
+
out literally in the markup:
|
|
556
|
+
|
|
557
|
+
```js
|
|
558
|
+
// idiomatic: see modals/email_create.view
|
|
559
|
+
$('select[name="Domain"]').html(opts.join(''));
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
**Nested `:for` does not resolve the inner loop variable.** An inner loop inside an
|
|
563
|
+
outer one renders the right number of rows but every interpolation comes out empty
|
|
564
|
+
(the DOM shows `clone="x"` with blank values). Build nested structures as an HTML
|
|
565
|
+
string in JS and inject them, as `sites.view`'s `gridRender` and
|
|
566
|
+
`applications.view` do.
|
|
567
|
+
|
|
568
|
+
`:for` is also **slow for large loops** in V1 — prefer building rows as an HTML
|
|
569
|
+
string (as `lmlst`'s `gridRender` does) for anything long.
|
|
570
|
+
|
|
571
|
+
A `:for` array does **not** need to be in a `bind` unless it changes; bind it so the
|
|
572
|
+
DOM updates when it does.
|
|
573
|
+
|
|
574
|
+
**Loop modifiers** (siblings of `:for` on the same element — no Vue equivalent at all):
|
|
575
|
+
```html
|
|
576
|
+
<li :for="items2 as item"
|
|
577
|
+
:for-limit="5" <!-- render at most 5 -->
|
|
578
|
+
:for-offset="2" <!-- skip the first 2 -->
|
|
579
|
+
:for-if="item.value > 10"> <!-- per-item conditional filter, applied before limit/offset -->
|
|
580
|
+
{{ item.key }}: {{ item.value }}
|
|
581
|
+
</li>
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### 6.2 `:if` / `:else-if` / `:else` — conditionals (contrast: Vue `v-if`/`v-else-if`/`v-else`)
|
|
585
|
+
|
|
586
|
+
Naming and chaining semantics are essentially identical to Vue (adjacent-sibling chain, first true branch wins, `:else` has no value):
|
|
587
|
+
```html
|
|
588
|
+
<button :if="!isLoggedIn">Login</button>
|
|
589
|
+
<p :if="age < 18">...</p>
|
|
590
|
+
<p :else-if="age < 21">...</p>
|
|
591
|
+
<p :else>...</p>
|
|
592
|
+
```
|
|
593
|
+
Unlike React's `{cond && <X/>}` or `? :`, and like Vue, these are DOM-structural — the element (and, per the engine's `refItem`/`getRefTree` logic, matching `:for` blocks) is only materialized when true, not merely hidden.
|
|
594
|
+
|
|
595
|
+
### 6.3 Events — `@eventName="handler"` (contrast: Vue `@click`, React `onClick`)
|
|
596
|
+
|
|
597
|
+
Syntax matches Vue exactly: `@event="functionName"` (calls a global function) or `@event="inline js expression"`. **Handler signature differs from both Vue and React**: every handler receives `(ev, el)`:
|
|
598
|
+
- `ev` — the native-ish event object (`ev.type` tells you which concrete event fired for multiplexed handlers like drag/scroll/sort, see below).
|
|
599
|
+
- `el` — **a jQuery-wrapped element**, not a raw DOM node and not a component-ref. Use `el.css(...)`, `el.data(...)`, `el.val()`, etc. Get the raw DOM node with `el[0]`.
|
|
600
|
+
|
|
601
|
+
```html
|
|
602
|
+
<button @click="handleButtonClick">Click me</button>
|
|
603
|
+
<script>
|
|
604
|
+
function handleButtonClick(ev, el) { alert("clicked"); }
|
|
605
|
+
</script>
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
**Full event catalogue** (exhaustive, gathered from docs + engine):
|
|
609
|
+
|
|
610
|
+
*Click/tap:*
|
|
611
|
+
`@click`, `@dblclick`, `@clickhold`, `@click2` (two-finger click), `@click2hold`, `@singleclick` (debounced single click), `@tap`, `@dbltap`, `@taphold`, `@tap2`, `@tap2hold`, `@singletap`
|
|
612
|
+
|
|
613
|
+
*Swipe / orientation:*
|
|
614
|
+
`@swipe`, `@swipe-left`, `@swipe-right`, `@swipe-up`, `@swipe-down`, `orientationchange` (window-level, not per-element)
|
|
615
|
+
|
|
616
|
+
*Mouse:*
|
|
617
|
+
`@mouse-over`, `@mouse-enter`, `@mouse-move`, `@mouse-leave`, `@mouse-out`, `@mouse-up`, `@mouse-down` — note the **hyphenated** form (not `@mouseover` camelCase like a native DOM listener, and not Vue's `@mouseover`)
|
|
618
|
+
|
|
619
|
+
*Scroll:*
|
|
620
|
+
`@scroll`, `@scroll-start`, `@scroll-end`, `@scroll-bottom`, `@scroll-top`, `@scroll-right`, `@scroll-left` (the last four fire once when the scroll container hits that edge)
|
|
621
|
+
|
|
622
|
+
*Drag (requires the `drag` attribute on the element, see §6.4):*
|
|
623
|
+
`@drag` (continuous, while dragging), `@drag-start`, `@drag-stop`
|
|
624
|
+
|
|
625
|
+
*Sort (requires the `sort` attribute on the container, see §6.4):*
|
|
626
|
+
`@sort` (fires per reorder), `@sort-start`, `@sort-stop`, `@sort-change` (live, during drag), `@sort-update` (final, on drop) — handlers for the `-start/-stop/-change/-update` variants receive a third argument `ui` with `ui.originalPosition.{left,top}`
|
|
627
|
+
|
|
628
|
+
*Forms:* standard `@change`, `@blur`, `@focus`, `@keydown`, `@keyup`, `@keypress` (native-named, unlike the hyphenated mouse events)
|
|
629
|
+
|
|
630
|
+
*File drop / upload (see §6.6):* `@drop`, `@file-removed`, `@file-uploaded`, `@files-added`, `@upload-progress`
|
|
631
|
+
|
|
632
|
+
*Modal (see §8):* `@modal-open`, `@modal-close`, `@modal-beforeclose`
|
|
633
|
+
|
|
634
|
+
*Select2 wrapper (see §6.7):* `@sl-select`, `@sl-unselect`
|
|
635
|
+
|
|
636
|
+
*Custom list widget:* `@query` (used by `<lmlst>`, §4.4)
|
|
637
|
+
|
|
638
|
+
### 6.4 Draggable / sortable
|
|
639
|
+
|
|
640
|
+
No attribute value is required to turn on the behavior — presence of the bare attribute is enough:
|
|
641
|
+
```html
|
|
642
|
+
<div drag class="grab">Drag me</div>
|
|
643
|
+
<div drag drag-revert="false">Drag me!</div> <!-- default true: snaps back after drop -->
|
|
644
|
+
|
|
645
|
+
<ul sort>
|
|
646
|
+
<li :for="4">{{index+1}}</li>
|
|
647
|
+
</ul>
|
|
648
|
+
```
|
|
649
|
+
Combine with the drag/sort events from §6.3 to react to movement.
|
|
650
|
+
|
|
651
|
+
### 6.5 Modals
|
|
652
|
+
|
|
653
|
+
See §8 for the full write-up; attribute summary:
|
|
654
|
+
`modal="path/to/subview"`, `modal-class="css-class"`, `modal-title="..."`, `modal-description="..."`, `close-modal` (bare attribute on any element inside the modal, closes it like the X button).
|
|
655
|
+
|
|
656
|
+
CSS targeting hooks for the modal chrome (Mobius CSS classes, not directives, but only meaningful in this context): `pop-c` (content), `pop-x` (close X), `pop-h` (header), `pop-t` (title), `pop-d` (description), `pop-b` (body).
|
|
657
|
+
|
|
658
|
+
### 6.6 Forms — `o-sub`, `b-sub`, `norm`
|
|
659
|
+
|
|
660
|
+
**There is no `:model`/`v-model` two-way-binding directive in LumenJS.** Forms are handled wholesale on submit, not field-by-field:
|
|
661
|
+
|
|
662
|
+
| Attribute | Behavior |
|
|
663
|
+
|---|---|
|
|
664
|
+
| *(none — default)* | Submitting navigates to `action` (if present) or stays on the same view; input values land in `View.props` (or `View.params` if `method="get"`, as a query string) of the target view. Works with zero JS. |
|
|
665
|
+
| `o-sub="fnName"` | Fires `fnName(f, d)` **on submit**, where `f` is the form node and `d` is an object of `{name: value}` for every input. **Mutually exclusive with `action`/`method`/`target`** — combine with `pushURL(...)` inside the handler to navigate (see §9.2). |
|
|
666
|
+
| `b-sub="fnName"` | Fires `fnName(f, d)` **before** submit; `return false` cancels the submission (validation gate) — otherwise behaves like a normal form (respects `action`). |
|
|
667
|
+
| `norm` (bare) | Opts the `<form>` back out of all LumenJS interception — plain full-page-reload HTML form behavior. |
|
|
668
|
+
|
|
669
|
+
```html
|
|
670
|
+
<form o-sub="onSubmit">
|
|
671
|
+
<input type="text" name="name">
|
|
672
|
+
<input type="submit" value="submit">
|
|
673
|
+
</form>
|
|
674
|
+
<script>
|
|
675
|
+
function onSubmit(f, d) {
|
|
676
|
+
pushURL("/view-example", { "data": d });
|
|
677
|
+
}
|
|
678
|
+
</script>
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
Field-level validation: add `:required` to an `<input>`/`<select>`/`<textarea>` — the engine adds a `.required` class to the closest `<label>` and blocks submission (adding a `.validation-error` class to the form) if empty. This is the closest thing to Vue/React form validation primitives that exists — there's no per-field custom-validator directive beyond this boolean required check plus your own `b-sub` logic.
|
|
682
|
+
|
|
683
|
+
### 6.7 Select2 wrapper (`sl-*`)
|
|
684
|
+
|
|
685
|
+
A large family of attributes wraps the Select2 jQuery plugin onto `<select>`-like elements. Confirmed from the engine: `sl-id`, `sl-value` (preselect a value), `sl-text`, `sl-query` (name of a global function used as a custom async data source), `sl-class`, `sl-dir`, `sl-min`/`sl-mins` (`minimumResultsForSearch`, default 10 — set high to hide the search box), `sl-nrmsg` (no-results message), `sl-ntgs` (allow free-text tags), `sl-prt` (dropdown parent selector, or `"self"` → the select's `.parent()`; default `"body"`), `sl-nosrch`/`nosearch` (disable search box), plus events `@sl-select`/`@sl-unselect`. Treat this family as "Select2 configuration surface," not core reactivity — there is no dedicated guide page recovered for it; behavior above is reconstructed from the compiled engine.
|
|
686
|
+
|
|
687
|
+
**Verified from the compiled wrapper (`bea.js`):**
|
|
688
|
+
|
|
689
|
+
- The init selector is `$("[sl]:not(.select2-hidden-accessible)")`, run inside
|
|
690
|
+
`renderPlugins()`. A `[sl]` select you inject with jQuery/vanilla stays a raw
|
|
691
|
+
browser control until the next `globalWatch()` (§5.6) — or init it by hand,
|
|
692
|
+
`$(sel).select2({ dropdownParent: $(sel).parent(), ... })`, guarded by the same
|
|
693
|
+
`:not(.select2-hidden-accessible)` check.
|
|
694
|
+
- **Clear button is opt-in via the bare `clear` attribute** (changed 2026-08-31;
|
|
695
|
+
was hardcoded `allowClear:true` before). `<select sl>` → no clear ×;
|
|
696
|
+
`<select sl clear>` → shows it, styled in the Console as a solid dark-red (`#900`)
|
|
697
|
+
square button (`styles.css`, `.select2-selection__clear`). Only add `clear` when
|
|
698
|
+
an empty/unset value is actually meaningful for that field.
|
|
699
|
+
- After replacing a select's `<option>`s with `$(sel).html(opts)`, call
|
|
700
|
+
`$(sel).trigger('change')` so the Select2 display re-reads them.
|
|
701
|
+
|
|
702
|
+
### 6.8 DOM utility / class-toggle directives (no Vue/React equivalent at all)
|
|
703
|
+
|
|
704
|
+
These exist because LumenJS has no component-local state for simple UI toggles (open/closed menus, active tabs) — instead you point one element at another by CSS selector:
|
|
705
|
+
|
|
706
|
+
| Attribute | Behavior |
|
|
707
|
+
|---|---|
|
|
708
|
+
| `t="<selector>"` + `tcn="<class>"` | On click/touch of the element carrying `t`, **toggle** class `tcn` (default `"active"`) on the element(s) matched by the selector in `t`. Also toggles `.toggled` on the trigger itself. Seen used for mobile-menu hamburgers: `<div t=".mobileMenu,.header__btnmenu" tcn="active">`. |
|
|
709
|
+
| `acl="<selector>"` + `aclcn="<class>"` (add-class), `rcl="<selector>"` + `rclcn="<class>"` (remove-class) | Same idea, but split into an explicit add-target and remove-target instead of a single toggle target — used when opening one panel must simultaneously close a different one. |
|
|
710
|
+
| `actv` (value = selector or empty) | On click, removes `.active` from the selector's matches (if given) and adds `.active` to itself. A crude "only one of these is active" pattern with **no data model backing it** — purely a DOM class effect. |
|
|
711
|
+
| `actvt` (value = selector or empty) | Same as `actv` but *toggles* rather than unconditionally activating (won't re-activate an already-active element). |
|
|
712
|
+
| `stop` (bare) | Calls `event.stopPropagation()` for the triggering interaction — an escape hatch for nested `t`/`acl`/`actv` elements. |
|
|
713
|
+
| `confirm="message"` / `econfirm="js expr"` | Native `confirm()` gate before the element's action proceeds; `econfirm` evaluates a JS expression instead of a literal string. |
|
|
714
|
+
|
|
715
|
+
### 6.9 Navigation active-state matching: `n0`, `n1`, `n2`, …
|
|
716
|
+
|
|
717
|
+
```html
|
|
718
|
+
<a href="/guide" n0="guide">Guide</a>
|
|
719
|
+
```
|
|
720
|
+
The numeric suffix corresponds to the `nodes[n]` depth (§5.5) — the engine compares `nodes[N]` against the attribute's value to decide whether to mark that link/nav-item as the active one. This is LumenJS's entire "active nav link" story; there's no `<router-link active-class>` — you hand-annotate every link with which URL segment and value it corresponds to.
|
|
721
|
+
|
|
722
|
+
### 6.10 Misc single-purpose attributes
|
|
723
|
+
|
|
724
|
+
| Attribute | Behavior |
|
|
725
|
+
|---|---|
|
|
726
|
+
| `lazy` + `data-src="url"` | Lazy-loads a background/image source once in viewport (also `data-srcset`, `data-sizes` for responsive variants). |
|
|
727
|
+
| `tip="text"`, `tip-pos="top\|...`, `tip-class="..."` | Built-in tooltip. |
|
|
728
|
+
| `pinnable`, `pinnable-offset-up`, `pinnable-offset-down` | Sticky/pin-on-scroll behavior with configurable trigger offsets. |
|
|
729
|
+
| `crslf` + `crslf-opts='{...}'` | Turns a container into a carousel (Flickity-based). Options object (all optional, defaults noted): `draggable` (true), `freeScroll` (false), `wrapAround` (false), `groupCells` (1), `autoPlay` (false), `fullscreen` (false), `fade` (false), `adaptiveHeight` (false), `cellAlign` ("center"), `contain` (false), `rightToLeft` (true), `prevNextButtons` (true), `pageDots` (true). |
|
|
730
|
+
| `up` (bare, on a container) + `up-accept="mime/types"` | Marks a container as a file-uploader drop zone (auto-injects a hidden `<input type="file">`); pair with the `@drop`/`@files-added`/`@file-uploaded`/`@file-removed`/`@upload-progress` events (§6.3). |
|
|
731
|
+
| `:hover="css-declarations"` | Applies raw inline CSS text (`"color:red;font-weight:bold;"`) on hover/touch-move, removes it on leave — a directive-level substitute for a CSS `:hover` pseudoclass, used because scoped `<style>` inheritance across sub-views is otherwise all-or-nothing. |
|
|
732
|
+
| `:required` | See §6.6. |
|
|
733
|
+
| `:data` | See §4.1/§4.2 — passes a variable to a sub-view/component. |
|
|
734
|
+
| `:config` | Passes a config object, used by `<lmlst>` (§4.4). |
|
|
735
|
+
| `:props`, `:props-bind`, `:lookup-callback` | Present in the engine's attribute table; no worked examples survive in the docs archive. Likely component-prop-declaration and custom-datasource-callback hooks respectively — flagged here for completeness, use with caution/verify against engine behavior before relying on them. |
|
|
736
|
+
|
|
737
|
+
---
|
|
738
|
+
|
|
739
|
+
## 7. Global built-in objects and functions
|
|
740
|
+
|
|
741
|
+
None of these need an import — they are ambient globals, available in every view's `<script>` block.
|
|
742
|
+
|
|
743
|
+
| Name | What it is |
|
|
744
|
+
|---|---|
|
|
745
|
+
| `globals` | A plain object, **auto-persisted to `localStorage`** and reactive when `bind`-ed. Add/edit: `globals.data = {...}`. Delete: `delete globals.data`. This is LumenJS's entire global-store story — no actions/mutations/reducers, just direct property assignment. |
|
|
746
|
+
| `session` | Same API surface as `globals` (`session.key = value`, `delete session.key`), backed by `sessionStorage` instead of `localStorage` (cleared when the tab/browser closes). |
|
|
747
|
+
| `cookies` | Cookie helper object: `cookies.setItem(key, value, expiry, path, domain, secure)`, `cookies.getItem(key)`, `cookies.removeItem(key)`, `cookies.hasItem(key)`, `cookies.keys()`. |
|
|
748
|
+
| `nodes` | URL path split on `/`, see §5.5. |
|
|
749
|
+
| `View.params` | Query-string data (from `<a href="/x?name=Hana">` or `method="get"` forms) available in the *target* view. |
|
|
750
|
+
| `View.props` | Data pushed via `pushURL(url, data)` or a submitted `o-sub`/`b-sub`/default form, available in the target view. |
|
|
751
|
+
| `pushURL(path, data?)` | Programmatic SPA navigation (no full reload). Second argument populates `View.props` on arrival. This plus `<a href>` and `<form>` are the **entire routing API** — see §9. |
|
|
752
|
+
| `watch` | Global watcher object, §5.3. |
|
|
753
|
+
| `API.get/post/put/delete/patch(path, data, headers, api_index, opts)` | Built-in HTTP client, see §10. |
|
|
754
|
+
| `LIVE` | Global WebSocket client (`new _Live("wss://...")`) with an `_emit(event, payload, isBinary)` method returning a promise — used internally for realtime features (e.g. live production counters on lumenjs.com itself, and chunked file uploads) and available for app-level realtime channels. No public guide page for it survived in the archives; treat it as an advanced/internal-facing API. |
|
|
755
|
+
| `cl(...)` | Seen throughout real code as a `console.log` shorthand — appears to be a project/engine convenience, not `console.log` itself. |
|
|
756
|
+
|
|
757
|
+
---
|
|
758
|
+
|
|
759
|
+
## 8. Modals in depth
|
|
760
|
+
|
|
761
|
+
A modal renders a sub-view's body inside a popup, without a route change or losing the parent view's state.
|
|
762
|
+
|
|
763
|
+
```html
|
|
764
|
+
<button modal="modal_view"
|
|
765
|
+
modal-class="my-modal"
|
|
766
|
+
modal-title="Title Here"
|
|
767
|
+
modal-description="Optional subtitle">
|
|
768
|
+
Open
|
|
769
|
+
</button>
|
|
770
|
+
```
|
|
771
|
+
Any element (not just buttons) with a `modal` attribute opens that popup on click. Structure (targetable via Mobius classes, not directives): content `pop-c`, close-X `pop-x`, header `pop-h`, title `pop-t` (default text "New Modal"), description `pop-d`, body `pop-b`.
|
|
772
|
+
|
|
773
|
+
Close it from inside with a bare `close-modal` attribute on any descendant element, equivalent to clicking the X.
|
|
774
|
+
|
|
775
|
+
**Multiple/nested modals**: give each trigger its own `modal-class` to scope CSS (`.my-modal-a pop-c { ... }`); a modal's own body can contain further `modal=` triggers, opening a second modal stacked on the first.
|
|
776
|
+
|
|
777
|
+
**Events**: `@modal-open`, `@modal-close`, `@modal-beforeclose` on the *trigger* element. Handlers receive `(ev, el, modal)`. `@modal-beforeclose` can be used to gate closing (e.g. prompt-before-discard).
|
|
778
|
+
|
|
779
|
+
**Passing data parent → modal**: return an object from the `@modal-open` handler; inside the modal view, retrieve it via `view.scope`:
|
|
780
|
+
```js
|
|
781
|
+
// parent view
|
|
782
|
+
function myFunc(ev, el, modal) {
|
|
783
|
+
return { key1: "hello", key2: 1234 };
|
|
784
|
+
}
|
|
785
|
+
```
|
|
786
|
+
```js
|
|
787
|
+
// modal view
|
|
788
|
+
var myData;
|
|
789
|
+
view.scope(function (view, props) {
|
|
790
|
+
myData = props.modal.key2;
|
|
791
|
+
});
|
|
792
|
+
```
|
|
793
|
+
**Passing data modal → parent**: mutate `props.modal` from inside the modal's `view.scope` callback — new keys you add become visible to the parent's context.
|
|
794
|
+
|
|
795
|
+
This `view.scope(function(view, props){...})` pattern is the closest LumenJS gets to a component "props" object — it is **only** used for modals, not for ordinary sub-views (§4.1) or components (§4.2), which instead rely on implicit same-name variable binding via `:data`.
|
|
796
|
+
|
|
797
|
+
---
|
|
798
|
+
|
|
799
|
+
## 9. Routing
|
|
800
|
+
|
|
801
|
+
There is no router library, no route-config file, and no `<Route>`/`useNavigate()` — routing is three primitives, all covered above:
|
|
802
|
+
|
|
803
|
+
1. **`<a href="/path?query=val">`** — plain anchor; LumenJS intercepts the click, swaps the view without a full reload. Query-string data lands in the target view's `View.params`.
|
|
804
|
+
2. **`pushURL(path, data?)`** — programmatic equivalent; `data` lands in `View.props` (not `View.params`) on arrival.
|
|
805
|
+
3. **`<form>`** — see §6.6 for the full `o-sub`/`b-sub`/`norm` matrix; default (no special attribute) behavior still avoids a full reload and populates `View.props`/`View.params` on the target view.
|
|
806
|
+
|
|
807
|
+
File-system nesting: a URL like `/account/admin/dashboard` maps directly to `views/account/admin/dashboard.view` — there is no separate manifest mapping URLs to files (except where a view deliberately re-dispatches on `nodes[n]` itself, §5.5, for guide-style catch-all sections).
|
|
808
|
+
|
|
809
|
+
---
|
|
810
|
+
|
|
811
|
+
## 10. HTTP / data fetching
|
|
812
|
+
|
|
813
|
+
Configured once in `config.json`:
|
|
814
|
+
```json
|
|
815
|
+
{
|
|
816
|
+
"node": { "host": "127.0.0.1", "port": "3000" },
|
|
817
|
+
"apis": [
|
|
818
|
+
{
|
|
819
|
+
"baseURL": "YOUR_API_URL",
|
|
820
|
+
"headers": {
|
|
821
|
+
"X-BEA-Application-ID": "YOUR_APPLICATION_ID",
|
|
822
|
+
"X-BEA-Authorization": "YOUR_REST_API_KEY"
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
]
|
|
826
|
+
}
|
|
827
|
+
```
|
|
828
|
+
Then, anywhere, the global `API` object:
|
|
829
|
+
```js
|
|
830
|
+
API.get("path", data, headers, api_index)
|
|
831
|
+
.then((data) => { /* ... */ })
|
|
832
|
+
.catch((err) => { /* ... */ });
|
|
833
|
+
|
|
834
|
+
API.post("path", data, headers, api_index).then(...).catch(...);
|
|
835
|
+
API.put("path/" + objectId, data, headers, api_index).then(...).catch(...);
|
|
836
|
+
API.delete("path/" + objectId, data, headers, api_index).then(...).catch(...);
|
|
837
|
+
```
|
|
838
|
+
- `path` — a full URL, or just a table/resource name if `config.json` already has a matching `baseURL` configured.
|
|
839
|
+
- `data` — request body / query object.
|
|
840
|
+
- `headers` — normally omitted (pulled from `config.json`); override per-call if needed.
|
|
841
|
+
- `api_index` — which entry in the `apis[]` array to use, if you configured more than one backend.
|
|
842
|
+
|
|
843
|
+
In real production code (Rahimoun), apps commonly wrap a second, app-specific HTTP client (there called `engine`, an instance of a BEA-provided `Engine` class configured with app/API keys) alongside or instead of the generic `API` global — `API` is the documented, generic primitive; `engine`/`Engine` is that particular backend's SDK layered on top, not a LumenJS core concept.
|
|
844
|
+
|
|
845
|
+
---
|
|
846
|
+
|
|
847
|
+
## 10b. The Roxyon BaaS query DSL
|
|
848
|
+
|
|
849
|
+
Everything in §10 covers *how* to make a request. This section covers the
|
|
850
|
+
**request body**, which is where nearly all real Console/Rahimoun page logic
|
|
851
|
+
lives.
|
|
852
|
+
|
|
853
|
+
> **Do not invent query syntax.** This DSL is not Parse, not Firebase, not
|
|
854
|
+
> MongoDB, and not SQL — it resembles all of them in places and differs in the
|
|
855
|
+
> details. Every operator, key, and idiom below is exhaustive for the common
|
|
856
|
+
> path; if something you want is not listed here, it probably does not exist,
|
|
857
|
+
> and inventing it produces a query that returns *plausible wrong data* rather
|
|
858
|
+
> than an error.
|
|
859
|
+
|
|
860
|
+
### 10b.1 Mental model
|
|
861
|
+
|
|
862
|
+
A **class** is a MySQL table; an **object** is a row. `objectId` is a Hashids
|
|
863
|
+
string, never the raw integer. There are **no foreign keys** — a pointer column
|
|
864
|
+
holds the target row's `objectId` as a plain string, and the relationship only
|
|
865
|
+
exists in the schema metadata.
|
|
866
|
+
|
|
867
|
+
```js
|
|
868
|
+
API.get("/Patients", { fields: "objectId,firstName,lastName", limit: 50 })
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
The response is always:
|
|
872
|
+
|
|
873
|
+
```js
|
|
874
|
+
{ results: [ {...}, {...} ] } // plus "count" when count:1 is set
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
### 10b.2 Body keys
|
|
878
|
+
|
|
879
|
+
| Key | Purpose |
|
|
880
|
+
|---|---|
|
|
881
|
+
| `fields` | Comma-separated columns to select. Supports aggregates: `"Count(*) as c,Gender"` |
|
|
882
|
+
| `where` | Filter object — see §10b.3 |
|
|
883
|
+
| `limit` | Row cap. **`-1` does NOT mean unlimited — it means 1000.** Omitting it defaults to 10 |
|
|
884
|
+
| `offset` | Skip N rows (pagination) |
|
|
885
|
+
| `order` | `"createdAt"` ascending, `"-createdAt"` descending |
|
|
886
|
+
| `groupby` | Comma-separated columns; `"none"` collapses to a single aggregate row |
|
|
887
|
+
| `having` | Filter on aggregate results |
|
|
888
|
+
| `count` | `1` adds a `count` key to the response. Pair with `limit: 0` for a count with no rows |
|
|
889
|
+
| `include` | Resolve a pointer to its target row — see §10b.4 |
|
|
890
|
+
| `locale` | `"en,ar"` to pull localized columns |
|
|
891
|
+
| `media` | `"images"`, `"files"` — attach media sub-records |
|
|
892
|
+
| `crops` | `"ax300,ax600"` — which image crop sizes to return |
|
|
893
|
+
| `_then` | Chain a follow-up request — see §10b.6 |
|
|
894
|
+
| `on_duplicate` | On POST: what to do if the row already exists — see §10b.6 |
|
|
895
|
+
|
|
896
|
+
### 10b.3 `where` and its operators
|
|
897
|
+
|
|
898
|
+
A bare value means equality:
|
|
899
|
+
|
|
900
|
+
```js
|
|
901
|
+
where: { Status: "completed" }
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
An object means explicit operators:
|
|
905
|
+
|
|
906
|
+
```js
|
|
907
|
+
where: {
|
|
908
|
+
Status: { in: "new,inreview" },
|
|
909
|
+
createdAt: { gte: "2026-07-01", lte: "2026-07-31" },
|
|
910
|
+
Name: { contains: "حسن" }
|
|
911
|
+
}
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
**The complete operator list.** Anything not here does not exist:
|
|
915
|
+
|
|
916
|
+
| Operator | Meaning |
|
|
917
|
+
|---|---|
|
|
918
|
+
| `eq` / `ne` | equals / not equals (aliases: `equals`, `notequals`, `neq`) |
|
|
919
|
+
| `gt` `gte` `lt` `lte` | numeric and date comparison |
|
|
920
|
+
| `in` / `nin` | in / not in a comma-separated list |
|
|
921
|
+
| `contains` / `notcontains` | substring match |
|
|
922
|
+
| `startswith` / `notstartswith` | prefix match |
|
|
923
|
+
| `endswith` / `notendswith` | suffix match |
|
|
924
|
+
| `regex` (alias `REGEXP`) | regular expression |
|
|
925
|
+
| `month` / `year` | match a date part; accepts the literal `"current"` |
|
|
926
|
+
| `exists` | pointer is / is not set |
|
|
927
|
+
| `arrayKeyIn` / `arrayKeyAll` | match inside a `pointerArray` / `array` column |
|
|
928
|
+
|
|
929
|
+
Relative dates have dedicated operators — prefer them over building a literal
|
|
930
|
+
range, because they cannot go stale:
|
|
931
|
+
|
|
932
|
+
```js
|
|
933
|
+
where: { createdAt: { month: "current" } } // this month
|
|
934
|
+
where: { createdAt: { year: "current" } } // this year
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
#### OR groups
|
|
938
|
+
|
|
939
|
+
```js
|
|
940
|
+
where: {
|
|
941
|
+
or: [
|
|
942
|
+
{ Status: "new" },
|
|
943
|
+
{ Severity: "urgent" }
|
|
944
|
+
]
|
|
945
|
+
}
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
#### Subqueries (`select`)
|
|
949
|
+
|
|
950
|
+
The equivalent of SQL's `WHERE x IN (SELECT ...)`. This is how you filter one
|
|
951
|
+
class by a condition on another:
|
|
952
|
+
|
|
953
|
+
```js
|
|
954
|
+
where: {
|
|
955
|
+
Application: {
|
|
956
|
+
select: {
|
|
957
|
+
query: {
|
|
958
|
+
className: "Applications",
|
|
959
|
+
where: { Type: "hospital" }
|
|
960
|
+
},
|
|
961
|
+
field: "objectId"
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
#### The `published` idiom
|
|
968
|
+
|
|
969
|
+
**Every query silently appends `eye='1' AND published='1'` unless the `where`
|
|
970
|
+
mentions those keys.** Rows are soft-deleted by setting `published = 0`, so a
|
|
971
|
+
normal query never sees them. To include unpublished rows you must say so:
|
|
972
|
+
|
|
973
|
+
```js
|
|
974
|
+
where: { published: { REGEXP: "0|1" } } // include both states
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
This looks strange and is easy to mistake for dead code. It is not — omitting
|
|
978
|
+
it is why a query "loses" rows that visibly exist in the database.
|
|
979
|
+
|
|
980
|
+
### 10b.4 `include` — resolving pointers
|
|
981
|
+
|
|
982
|
+
A pointer column holds an `objectId` string. `include` fetches the target row
|
|
983
|
+
and attaches it as `_<ClassName>`:
|
|
984
|
+
|
|
985
|
+
```js
|
|
986
|
+
API.get("/Patients", {
|
|
987
|
+
fields: "objectId,firstName,Branch",
|
|
988
|
+
limit: 50,
|
|
989
|
+
include: {
|
|
990
|
+
className: "Branches",
|
|
991
|
+
field: "Branch", // the pointer column on Patients
|
|
992
|
+
fields: "Name"
|
|
993
|
+
}
|
|
994
|
+
})
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
Each result row then carries:
|
|
998
|
+
|
|
999
|
+
```js
|
|
1000
|
+
{ objectId: "...", firstName: "...", Branch: "9GQJl7MZKm",
|
|
1001
|
+
_Branch: { results: [ { objectId: "9GQJl7MZKm", Name: "فرع طرابلس" } ] } }
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
Note the shape: **`_<Field>.results[0]`**, an array, not a bare object. Real
|
|
1005
|
+
code almost always flattens it immediately:
|
|
1006
|
+
|
|
1007
|
+
```js
|
|
1008
|
+
for (var i = 0; i < r.results.length; i++) {
|
|
1009
|
+
var row = r.results[i];
|
|
1010
|
+
row.Branch = row._Branch.results[0];
|
|
1011
|
+
}
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
`include` may be a single object or an **array** of objects, and may nest.
|
|
1015
|
+
|
|
1016
|
+
#### Reverse includes
|
|
1017
|
+
|
|
1018
|
+
To query from the class being pointed *at*, add `reverse: true`. This lists each
|
|
1019
|
+
branch together with its patients:
|
|
1020
|
+
|
|
1021
|
+
```js
|
|
1022
|
+
API.get("/Branches", {
|
|
1023
|
+
fields: "Name",
|
|
1024
|
+
limit: 50,
|
|
1025
|
+
include: {
|
|
1026
|
+
className: "Patients",
|
|
1027
|
+
field: "Branch", // the pointer column on Patients
|
|
1028
|
+
reverse: true,
|
|
1029
|
+
fields: "firstName,lastName",
|
|
1030
|
+
where: { Gender: "female" }
|
|
1031
|
+
}
|
|
1032
|
+
})
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
A class may have several pointers to the same target, and may point at itself
|
|
1036
|
+
(`Domains.ParentDomain -> Domains`) — both work.
|
|
1037
|
+
|
|
1038
|
+
**Where the result lands.** Each include is returned on the parent row under
|
|
1039
|
+
`_ClassName`, always as `{ results: [...] }`, never as a bare array — so it is
|
|
1040
|
+
`row._Pricing.results[0]`, and an empty `results` is a row with no children
|
|
1041
|
+
rather than a missing key:
|
|
1042
|
+
|
|
1043
|
+
```js
|
|
1044
|
+
let pricing = pln._Pricing.results[0]; // reverse include
|
|
1045
|
+
let service = pln._Service.results[0]; // forward include
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
**Reverse includes nest.** An include inside a reverse include resolves against
|
|
1049
|
+
the child rows, which is how one request can reach a grandchild. This fetches
|
|
1050
|
+
every application with its processes, its routes, and the name of each route's
|
|
1051
|
+
domain — replacing four sequential round trips with one:
|
|
1052
|
+
|
|
1053
|
+
```js
|
|
1054
|
+
rx.get("/Applications", {
|
|
1055
|
+
fields: "objectId,Name,Status,SourcePath,ConfigRevision,AppliedRevision",
|
|
1056
|
+
limit: -1,
|
|
1057
|
+
where: { Subscription: sub },
|
|
1058
|
+
include: [
|
|
1059
|
+
{ className: "ApplicationProcesses", field: "Application",
|
|
1060
|
+
fields: "objectId,Type,Command,Status", reverse: 1 },
|
|
1061
|
+
{ className: "ApplicationRoutes", field: "Application",
|
|
1062
|
+
fields: "objectId,Domain,Path,Enabled", reverse: 1,
|
|
1063
|
+
include: { className: "Domains", field: "Domain",
|
|
1064
|
+
fields: "objectId,Name" } }
|
|
1065
|
+
]
|
|
1066
|
+
})
|
|
1067
|
+
// app._ApplicationRoutes.results[0]._Domain.results[0].Name
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
`field` is always the pointer column on the *child* class, in both directions.
|
|
1071
|
+
|
|
1072
|
+
#### The wire format, when calling the API by hand
|
|
1073
|
+
|
|
1074
|
+
`rx.get()` and `AsyncHttpClient::get()` serialise the query with
|
|
1075
|
+
`http_build_query`, so nested parameters go out as PHP bracket notation, not as
|
|
1076
|
+
JSON:
|
|
1077
|
+
|
|
1078
|
+
```
|
|
1079
|
+
include[0][className]=ApplicationProcesses&include[0][field]=Application
|
|
1080
|
+
&include[0][reverse]=1&include[1][include][className]=Domains
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
A hand-built request that JSON-encodes `include` or `where` into the query
|
|
1084
|
+
string is silently misread: `include` comes back as `Invalid Field Name`, and a
|
|
1085
|
+
JSON `where` is **dropped entirely** — the query then returns arbitrary rows
|
|
1086
|
+
rather than failing, which is how a `where` on `objectId` once returned a
|
|
1087
|
+
different record. Build query strings with `http_build_query` (PHP) or
|
|
1088
|
+
`urlencode` over bracketed keys (anything else), or go through `rx`.
|
|
1089
|
+
|
|
1090
|
+
#### Adding columns: what `/Schemas` accepts
|
|
1091
|
+
|
|
1092
|
+
`PUT /Schemas/{Class}` adds `data`, `options`, `integer`, `boolean` and string
|
|
1093
|
+
columns, and rejects anything beyond the minimum keys — `options` fields fail if
|
|
1094
|
+
given `dbcol`/`default_value`/`editable`, so send only `name`, `type` and
|
|
1095
|
+
`options`.
|
|
1096
|
+
|
|
1097
|
+
**Pointers use the nested shape** — the same one `POST /schemas/{Class}` takes
|
|
1098
|
+
when the class is first created:
|
|
1099
|
+
|
|
1100
|
+
```php
|
|
1101
|
+
"User" => [
|
|
1102
|
+
"name" => "User",
|
|
1103
|
+
"type" => "pointer",
|
|
1104
|
+
"pointer" => [
|
|
1105
|
+
"field" => "Email", // column to display
|
|
1106
|
+
"className" => "users", // the class KEY (lowercase, = dbtbl)
|
|
1107
|
+
"objectId" => $class_users->objectId // objectId of the class pointed AT
|
|
1108
|
+
],
|
|
1109
|
+
"required" => 1
|
|
1110
|
+
]
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
Note this differs from how a pointer is *read back*: `GET /schemas` returns it
|
|
1114
|
+
flattened, as `pointer: "users"` plus `pointerClassId` and `pointer_col`. Do not
|
|
1115
|
+
send it back in the shape you read it.
|
|
1116
|
+
|
|
1117
|
+
**Adding a pointer to an existing class via `PUT` did not work.** Every attempt
|
|
1118
|
+
returned `code 117, "Class Doesn't Exists"`, including the shape above with and
|
|
1119
|
+
without `required`, and with the pointed class's own objectId. `createFields()`
|
|
1120
|
+
in the BaaS clearly does understand pointers, so the capability exists and the
|
|
1121
|
+
rejection is happening somewhere earlier — the cause was not identified. The
|
|
1122
|
+
error names the class rather than the field, which sends you looking at the
|
|
1123
|
+
wrong thing. Declare pointers when the class is created, or use the schema UI.
|
|
1124
|
+
|
|
1125
|
+
Two details from the BaaS source worth knowing when hand-writing a field spec:
|
|
1126
|
+
the default value is read from `default`, **not** `default_value` (which is what
|
|
1127
|
+
`GET /schemas` returns it as), and `options` fields reject the extra keys a read
|
|
1128
|
+
returns — send only `name`, `type` and `options`.
|
|
1129
|
+
|
|
1130
|
+
### 10b.5 Aggregates and grouping
|
|
1131
|
+
|
|
1132
|
+
Aggregates go in `fields`, not a separate key:
|
|
1133
|
+
|
|
1134
|
+
```js
|
|
1135
|
+
API.get("/Patients", {
|
|
1136
|
+
fields: "Count(*) as c,Gender",
|
|
1137
|
+
groupby: "Gender",
|
|
1138
|
+
limit: 100
|
|
1139
|
+
})
|
|
1140
|
+
// -> [ { c: "3859", Gender: "male" }, { c: "5284", Gender: "female" } ]
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
Available functions: `Count`, `Sum`, `Avg`, `Min`, `Max`.
|
|
1144
|
+
|
|
1145
|
+
Three things that catch people out:
|
|
1146
|
+
|
|
1147
|
+
1. **Aggregate values come back as strings**, not numbers. `parseFloat()` before
|
|
1148
|
+
arithmetic; concatenating them by accident is a common bug.
|
|
1149
|
+
2. **`groupby: "none"`** removes grouping entirely and returns one aggregate row.
|
|
1150
|
+
Without any `groupby`, the engine defaults to grouping by `_pos`, i.e. per
|
|
1151
|
+
row.
|
|
1152
|
+
3. With `groupby` set, `count: 1` returns the **number of groups**, not the
|
|
1153
|
+
number of rows.
|
|
1154
|
+
|
|
1155
|
+
`objectId`, `createdAt` and `updatedAt` are injected into every select
|
|
1156
|
+
automatically. In a grouped query they hold an arbitrary member of each group
|
|
1157
|
+
and are meaningless — ignore them rather than displaying them.
|
|
1158
|
+
|
|
1159
|
+
### 10b.6 Chaining and upserts
|
|
1160
|
+
|
|
1161
|
+
#### `_then` — a follow-up request
|
|
1162
|
+
|
|
1163
|
+
Runs after the first query, with `row(FIELD)` interpolating values from the
|
|
1164
|
+
first result set:
|
|
1165
|
+
|
|
1166
|
+
```js
|
|
1167
|
+
API.get("/Subscriptions", {
|
|
1168
|
+
fields: "objectId,Plan",
|
|
1169
|
+
limit: 20,
|
|
1170
|
+
count: 1,
|
|
1171
|
+
_then: [
|
|
1172
|
+
{
|
|
1173
|
+
path: "/Domains",
|
|
1174
|
+
method: "get",
|
|
1175
|
+
body: {
|
|
1176
|
+
count: 1,
|
|
1177
|
+
limit: 0,
|
|
1178
|
+
where: { Subscription: "row(objectId)" }
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
]
|
|
1182
|
+
})
|
|
1183
|
+
```
|
|
1184
|
+
|
|
1185
|
+
`row(oType)` may also interpolate into the **path** (`"path": "/row(oType)"`)
|
|
1186
|
+
for polymorphic pointers.
|
|
1187
|
+
|
|
1188
|
+
#### `on_duplicate` — insert-or-update
|
|
1189
|
+
|
|
1190
|
+
On POST, if the row already exists, run this instead. Combined with the atomic
|
|
1191
|
+
increment string `"+=1"`, this is how the platform allocates sequential
|
|
1192
|
+
reference numbers:
|
|
1193
|
+
|
|
1194
|
+
```js
|
|
1195
|
+
API.post("/Stats", {
|
|
1196
|
+
Year: 2026, Month: 8, Type: "patient", Count: 1,
|
|
1197
|
+
on_duplicate: {
|
|
1198
|
+
method: "PUT",
|
|
1199
|
+
path: "/Stats",
|
|
1200
|
+
body: {
|
|
1201
|
+
where: { Year: 2026, Month: 8, Type: "patient" },
|
|
1202
|
+
Count: "+=1"
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
_then: {
|
|
1206
|
+
method: "GET",
|
|
1207
|
+
path: "/Stats",
|
|
1208
|
+
body: { limit: 1, fields: "*",
|
|
1209
|
+
where: { Year: 2026, Month: 8, Type: "patient" } }
|
|
1210
|
+
}
|
|
1211
|
+
})
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
### 10b.7 `/batch` — several queries in one round trip
|
|
1215
|
+
|
|
1216
|
+
```js
|
|
1217
|
+
var reqs = {
|
|
1218
|
+
requests: [
|
|
1219
|
+
{ path: "/Patients", method: "GET", body: { count: 1, limit: 0 } },
|
|
1220
|
+
{ path: "/Applications", method: "GET",
|
|
1221
|
+
body: { fields: "Count(*) as c,Status", groupby: "Status", limit: 100 } }
|
|
1222
|
+
]
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
API.post("/batch", reqs).then(function (r) {
|
|
1226
|
+
var patientCount = r[0].count;
|
|
1227
|
+
var byStatus = r[1].results;
|
|
1228
|
+
});
|
|
1229
|
+
```
|
|
1230
|
+
|
|
1231
|
+
The response is an **array** positionally matching `requests` — not an object.
|
|
1232
|
+
|
|
1233
|
+
### 10b.8 Field types and what comes back
|
|
1234
|
+
|
|
1235
|
+
| Schema type | JS value returned |
|
|
1236
|
+
|---|---|
|
|
1237
|
+
| `string`, `longText`, `email`, `key`, `color` | string |
|
|
1238
|
+
| `integer`, `number` | string (parse before arithmetic) |
|
|
1239
|
+
| `boolean` | `0` / `1` |
|
|
1240
|
+
| `date`, `datetime`, `time`, `year` | ISO-ish string |
|
|
1241
|
+
| `options` | string, one of the declared values |
|
|
1242
|
+
| `pointer` | `objectId` string, plus `_<Field>.results[]` when included |
|
|
1243
|
+
| `pointerArray`, `array` | comma-separated string of `objectId`s |
|
|
1244
|
+
| `data` | **a JSON string — call `JSON.parse()` yourself** |
|
|
1245
|
+
|
|
1246
|
+
The `data` type is the one that bites: it looks like an object in the schema and
|
|
1247
|
+
arrives as a string.
|
|
1248
|
+
|
|
1249
|
+
#### `Plans.Limits` and `Plans.Features` — the real shapes
|
|
1250
|
+
|
|
1251
|
+
`Limits` is the **internal, machine-readable** column (snake_case). The complete
|
|
1252
|
+
real shape, with storage values in **MB**:
|
|
1253
|
+
|
|
1254
|
+
```json
|
|
1255
|
+
{"max_processes":20,"memory_limit":512,"inodes":50000,
|
|
1256
|
+
"web_storage":40960,"mail_storage":61440}
|
|
1257
|
+
```
|
|
1258
|
+
|
|
1259
|
+
There is **no CPU key and no bandwidth key**. Do not invent `memory`, `cpu_cores`,
|
|
1260
|
+
`max_sites`, `max_databases` or `max_emails` — none exist. Read a limit with a
|
|
1261
|
+
documented fallback, and ask before assuming a new key.
|
|
1262
|
+
|
|
1263
|
+
`Features` is the **user-facing** column: nested display strings, marketing copy,
|
|
1264
|
+
never parse it for real limits.
|
|
1265
|
+
|
|
1266
|
+
```js
|
|
1267
|
+
Features['Websites & Storage']['Disk Space'] // "40 GB NVMe"
|
|
1268
|
+
Features['Email & Databases'].Databases // 10
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
Both are `data` columns, so `JSON.parse()` them (`_dashboards/cloudhosting.view`
|
|
1272
|
+
does this once and shares `Limits` with every tab).
|
|
1273
|
+
|
|
1274
|
+
Related real fields: `Datacenters` carry `Name`, `Region`, `Flag`, `Identifier`.
|
|
1275
|
+
`globals.phpversions` is keyed by version string (`"8.3"`) with `objectId`,
|
|
1276
|
+
`ReleaseDate` and `EOLDate` inside each record — so resolving a `PHPVersion`
|
|
1277
|
+
pointer needs a reverse scan, not a lookup. `EOLDate` is `ReleaseDate + 2 years`,
|
|
1278
|
+
i.e. **end of active support**; PHP gives a further year of security fixes, so do
|
|
1279
|
+
not label a version "end of life" from that date alone.
|
|
1280
|
+
|
|
1281
|
+
```js
|
|
1282
|
+
var plan = r.results[0];
|
|
1283
|
+
plan.Limits = JSON.parse(plan.Limits);
|
|
1284
|
+
plan.Features = JSON.parse(plan.Features);
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
### 10b.9 Writing
|
|
1288
|
+
|
|
1289
|
+
```js
|
|
1290
|
+
API.post("/Patients", { firstName: "...", Branch: branchObjectId });
|
|
1291
|
+
API.put("/Patients/" + objectId, { Mobile: "..." });
|
|
1292
|
+
API.delete("/Patients/" + objectId);
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
Deletion is normally **soft** — set `published: 0` rather than issuing a
|
|
1296
|
+
`DELETE`, so the row stays recoverable and drops out of every normal query
|
|
1297
|
+
automatically.
|
|
1298
|
+
|
|
1299
|
+
### 10b.9b Writes fail WITHOUT rejecting
|
|
1300
|
+
|
|
1301
|
+
The API resolves on a failed write instead of rejecting, so `.catch()` never runs
|
|
1302
|
+
and a failure looks exactly like success to a `.then()` handler:
|
|
1303
|
+
|
|
1304
|
+
```js
|
|
1305
|
+
// success
|
|
1306
|
+
{"results":[{"objectId":"9GQJl7MZKm"}]}
|
|
1307
|
+
// failure — still a RESOLVED promise
|
|
1308
|
+
{"results":[{"code":1054,"error":"Unknown column 'X' in 'SET'","type":"DBQueryError"}]}
|
|
1309
|
+
// top-level failures happen too
|
|
1310
|
+
{"code":105,"error":"Invalid Field Name","type":"InvalidFieldName"}
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
Always run a write's response through `rxError(r)` (in `index.js`) before telling
|
|
1314
|
+
the user it saved — it returns the message, or `''` on success:
|
|
1315
|
+
|
|
1316
|
+
```js
|
|
1317
|
+
rx.put("/Domains", {...}).then(function (r) {
|
|
1318
|
+
var err = rxError(r);
|
|
1319
|
+
if (err) { /* surface it */ return; }
|
|
1320
|
+
/* only now is it actually saved */
|
|
1321
|
+
});
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
One exception worth knowing: `db_create.view` relies on code **1062** (duplicate key)
|
|
1325
|
+
being returned for its `on_duplicate` flow, so it checks for that case before
|
|
1326
|
+
treating an error as fatal.
|
|
1327
|
+
|
|
1328
|
+
### 10b.9c A `select` subquery inside an `or` group crashes the API
|
|
1329
|
+
|
|
1330
|
+
`or` works, and `select` works, but a subquery nested inside an `or` returns an
|
|
1331
|
+
empty response (a server error). Resolve the ids in a first query, then use `in`:
|
|
1332
|
+
|
|
1333
|
+
```js
|
|
1334
|
+
// step 1: ids of the parents whose subdomains match
|
|
1335
|
+
// step 2:
|
|
1336
|
+
where: { Subscription: sub, Type: "primary",
|
|
1337
|
+
or: [ { Name: { contains: kw } }, { objectId: { in: ids.join(',') } } ] }
|
|
1338
|
+
```
|
|
1339
|
+
|
|
1340
|
+
### 10b.9d `/Schemas` — reading and changing the schema
|
|
1341
|
+
|
|
1342
|
+
`rx.get("/Schemas")` returns every class with its fields, types, pointers and
|
|
1343
|
+
options — the authoritative source for what exists. Use it instead of guessing at
|
|
1344
|
+
field names.
|
|
1345
|
+
|
|
1346
|
+
**Create a class** — `POST /schemas/{ClassName}`:
|
|
1347
|
+
|
|
1348
|
+
```js
|
|
1349
|
+
rx.post("/schemas/Ports", {
|
|
1350
|
+
className: "Ports",
|
|
1351
|
+
classType: "custom",
|
|
1352
|
+
fields: {
|
|
1353
|
+
Port: { name: "Port", type: "integer", required: 1 },
|
|
1354
|
+
Status: { name: "Status", type: "options", required: 1,
|
|
1355
|
+
options: "available,reserved,allocated", default: "reserved" },
|
|
1356
|
+
Subscription: {
|
|
1357
|
+
name: "Subscription", type: "pointer", required: 0,
|
|
1358
|
+
// objectId is the TARGET CLASS's objectId, read from /Schemas
|
|
1359
|
+
pointer: { field: "Name", className: "subscriptions", objectId: "aPbxP39VeL" }
|
|
1360
|
+
}
|
|
1361
|
+
},
|
|
1362
|
+
indexes: {
|
|
1363
|
+
Port: { name: "Port", field: "Port", unique: 1 },
|
|
1364
|
+
Status: { name: "Status", field: "Status" }
|
|
1365
|
+
},
|
|
1366
|
+
options: { images:0, videos:0, files:0, audios:0, variants:0, specs:0,
|
|
1367
|
+
categories:0, feature:0, MailOnAdd:0, MailOnEdit:0, locale:["en"] }
|
|
1368
|
+
});
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
**Add a column** — `PUT /Schemas/{ClassName}` carrying only the new field(s):
|
|
1372
|
+
|
|
1373
|
+
```js
|
|
1374
|
+
rx.put("/Schemas/Applications", {
|
|
1375
|
+
className: "Applications",
|
|
1376
|
+
fields: { Env: { name: "Env", type: "data", required: 0, unique: 0 } }
|
|
1377
|
+
});
|
|
1378
|
+
```
|
|
1379
|
+
|
|
1380
|
+
Field `type` values in use: `string`, `longText`, `integer`, `number`, `boolean`,
|
|
1381
|
+
`email`, `date`, `datetime`, `time`, `year`, `options`, `color`, `key`, `data`,
|
|
1382
|
+
`pointer`, `pointerArray`. A `pointer` needs `pointer: { className, field, objectId }`
|
|
1383
|
+
where `objectId` identifies the target **class** and `field` is the column to display.
|
|
1384
|
+
|
|
1385
|
+
**Gotcha:** the `indexes` block reads back **empty** from `/Schemas` even when the
|
|
1386
|
+
index exists and MySQL is enforcing it. Verify a unique index by attempting a
|
|
1387
|
+
duplicate insert and checking for code **1062**, not by reading the schema.
|
|
1388
|
+
|
|
1389
|
+
### 10b.10 Debugging
|
|
1390
|
+
|
|
1391
|
+
`debug_query: 1` in the body returns the generated SQL alongside the results.
|
|
1392
|
+
Use it when a query returns something unexpected; do not ship it.
|
|
1393
|
+
|
|
1394
|
+
---
|
|
1395
|
+
|
|
1396
|
+
## 11. A minimal complete example
|
|
1397
|
+
|
|
1398
|
+
```html
|
|
1399
|
+
<!-- views/patients.view -->
|
|
1400
|
+
<div bind="items,loading,total">
|
|
1401
|
+
<h2>Patients ({{ total }})</h2>
|
|
1402
|
+
|
|
1403
|
+
<div :if="loading">Loading…</div>
|
|
1404
|
+
|
|
1405
|
+
<div :else :for="items as item; i">
|
|
1406
|
+
{{ i + 1 }}. {{ item.firstName }} {{ item.lastName }}
|
|
1407
|
+
— {{ item._Branch.results[0].Name }}
|
|
1408
|
+
</div>
|
|
1409
|
+
</div>
|
|
1410
|
+
|
|
1411
|
+
<script>
|
|
1412
|
+
// every bound variable is `var`, never let/const (§5.1)
|
|
1413
|
+
var items = [];
|
|
1414
|
+
var loading = true;
|
|
1415
|
+
var total = 0;
|
|
1416
|
+
|
|
1417
|
+
API.get("/Patients", {
|
|
1418
|
+
fields: "objectId,firstName,lastName,Branch",
|
|
1419
|
+
where: { Gender: "female" },
|
|
1420
|
+
order: "-createdAt",
|
|
1421
|
+
limit: 50,
|
|
1422
|
+
count: 1,
|
|
1423
|
+
include: {
|
|
1424
|
+
className: "Branches",
|
|
1425
|
+
field: "Branch",
|
|
1426
|
+
fields: "Name"
|
|
1427
|
+
}
|
|
1428
|
+
}).then(function (r) {
|
|
1429
|
+
items = r.results; // reassignment is fine and idiomatic
|
|
1430
|
+
total = r.count;
|
|
1431
|
+
loading = false;
|
|
1432
|
+
}).catch(function (err) {
|
|
1433
|
+
loading = false;
|
|
1434
|
+
cl("patients load failed", err);
|
|
1435
|
+
});
|
|
1436
|
+
</script>
|
|
1437
|
+
|
|
1438
|
+
<style>
|
|
1439
|
+
h2 { color: teal; }
|
|
1440
|
+
</style>
|
|
1441
|
+
|
|
1442
|
+
<settings>
|
|
1443
|
+
{ "hasNav": true, "hasHeader": true, "hasFooter": true, "requireAuth": false }
|
|
1444
|
+
</settings>
|
|
1445
|
+
```
|
|
1446
|
+
|
|
1447
|
+
---
|
|
1448
|
+
|
|
1449
|
+
## 12. Full syntax-contrast table (Vue / React → LumenJS)
|
|
1450
|
+
|
|
1451
|
+
| Looks like… | In Vue/React it means | In LumenJS it actually means |
|
|
1452
|
+
|---|---|---|
|
|
1453
|
+
| `:for="items as item"` | (not valid Vue syntax, but visually close to `v-for`) | Loop directive; **also** accepts a bare integer (`:for="5"`) to repeat N times with no backing array — no Vue/React equivalent |
|
|
1454
|
+
| `:if` / `:else-if` / `:else` | N/A literally, but reads as `v-if` minus the `v-` | Same chaining semantics as Vue's `v-if`/`v-else-if`/`v-else` — this one **does** transfer cleanly |
|
|
1455
|
+
| `{{ expr }}` | Vue interpolation (text-only in Vue) | Works in text **and directly inside quoted HTML attributes** — Vue requires `:attr="expr"` for the latter; LumenJS does not distinguish |
|
|
1456
|
+
| `` `{{ expr }}` `` (backticks around the moustache) | Nothing — not valid Vue/React | LumenJS-specific "raw/safe" interpolation form for attribute values whose expression contains its own quotes (§5.2) |
|
|
1457
|
+
| `@click="handler"` | Vue: calls `handler($event)`; DOM `event` only | Calls `handler(ev, el)` — **second argument `el` is always a jQuery object**, not the native node and not a Vue/React synthetic-event target |
|
|
1458
|
+
| `@mouseover` | Native DOM event name | LumenJS spells it `@mouse-over` (hyphenated) — the un-hyphenated native form is not the documented directive name |
|
|
1459
|
+
| `v-model` / controlled `<input value={} onChange={}>` | Two-way field binding | **Does not exist.** Nearest equivalent is whole-form `o-sub`/`b-sub` handlers receiving a `d` object of all field values at submit time (§6.6) |
|
|
1460
|
+
| `bind="x"` | Looks like Vue's `v-bind` shorthand (`:x`) — **it is not that** | Declares `x` as a reactive variable for this subtree; unrelated to attribute binding. Required — without it the value renders once and never updates, silently |
|
|
1461
|
+
| `let x = ...` / `const x = ...` for state | Standard modern JS, works fine | **Breaks reactivity silently.** Bound variables must be `var` (§5.1) |
|
|
1462
|
+
| `:data="x"` | Looks like a Vue prop binding | Passes variable `x` into a sub-view/component by same-name convention, not a named-prop API |
|
|
1463
|
+
| `view="path"` | N/A | Renders another `.view` file inline — closest Vue analogue is `<component :is="...">`, but resolved against the filesystem, not a component registry |
|
|
1464
|
+
| `comp="name"` | N/A | Renders a file from `comps/` — a second, separate "component" concept from both views and templates |
|
|
1465
|
+
| `tpl="name"` | N/A | Renders a file from `tpls/`, almost always alongside `:for` — a third, separate reusable-HTML concept |
|
|
1466
|
+
| Component props object | `props` (Vue/React) | Only exists for **modals**, via `view.scope(function(view, props){...})` (§8) — ordinary sub-views/components use implicit same-name variable binding instead |
|
|
1467
|
+
| `watch: {...}` (Vue option) / `watch()`/`useEffect()` | Per-component, additive, dependency-scoped | One **global** `watch` object; reassigning it wholesale **overwrites** previously-registered watchers (§5.3) |
|
|
1468
|
+
| Router (`vue-router`/`react-router`) with a route table | Declarative route config | No route table at all — URL segments map 1:1 to nested view files on disk; deeper dispatch is hand-written against `nodes[n]` (§5.5, §9) |
|
|
1469
|
+
| Global store (Pinia/Redux) | Actions/mutations/reducers, often with devtools | Direct mutation of a single `globals` object, auto-mirrored to `localStorage` — no action layer |
|
|
1470
|
+
| `key` prop in lists | Required for diffing correctness | No `key` attribute exists on `:for` |
|
|
1471
|
+
| CSS scoping (`<style scoped>`) | Compiler-enforced style isolation per component | No compiler, so no true scoping — styles in a parent view's `<style>` block are inherited by sub-views rendered inside it; the `:hover` *directive* (§6.10) exists specifically to work around the lack of scoped `:hover` pseudo-class control |
|
|
1472
|
+
|
|
1473
|
+
---
|
|
1474
|
+
|
|
1475
|
+
## 13. Corrections and additions from live verification (2026-08-29, "Talabat" BaaS project)
|
|
1476
|
+
|
|
1477
|
+
Everything below was checked against a real, freshly-created BaaS project
|
|
1478
|
+
(not read from docs or reverse-engineered from an old app) while provisioning
|
|
1479
|
+
a schema for a new client. Two things in §10b.9d turned out to be wrong or
|
|
1480
|
+
incomplete; everything else here is new material the earlier sections didn't
|
|
1481
|
+
cover. Source: `_Talabat/schema/README.md` and `_Talabat/schema/provision.js`
|
|
1482
|
+
in this BEACDN tree have the full worked example.
|
|
1483
|
+
|
|
1484
|
+
### 13.1 `GET /Schemas`'s real response shape (corrects §10b.9d)
|
|
1485
|
+
|
|
1486
|
+
§10b.9d says `/Schemas` "returns every class with its fields, types,
|
|
1487
|
+
pointers and options" without specifying the envelope. The real shape is
|
|
1488
|
+
**one row per app**, not one row per class:
|
|
1489
|
+
|
|
1490
|
+
```js
|
|
1491
|
+
{ "results": [
|
|
1492
|
+
{ "objectId": "2OaQrLGxeq", "appName": "Talabat", "classes": { /* ... */ } }
|
|
1493
|
+
] }
|
|
1494
|
+
```
|
|
1495
|
+
|
|
1496
|
+
`classes` is an object keyed by the **lowercase `dbtbl` name**, not an array:
|
|
1497
|
+
|
|
1498
|
+
```js
|
|
1499
|
+
classes.restaurants = {
|
|
1500
|
+
objectId: "e2L8WE7V07", // <- use this as a pointer's target objectId
|
|
1501
|
+
className: "Restaurants", // display name, original casing
|
|
1502
|
+
dbtbl: "restaurants",
|
|
1503
|
+
Options: { images: 1, videos: 0, /* ... */ locale: ["en"] },
|
|
1504
|
+
fields: {
|
|
1505
|
+
Name: { objectId, name, dbcol, type: "string", required: "1", /* ... */ },
|
|
1506
|
+
Restaurant: { // a pointer field, once one exists
|
|
1507
|
+
type: "pointer",
|
|
1508
|
+
pointer: "restaurants", // target's dbtbl (lowercase)
|
|
1509
|
+
pointerClassId: "e2L8WE7V07", // target's objectId
|
|
1510
|
+
pointer_col: "Name", // the target's display field
|
|
1511
|
+
},
|
|
1512
|
+
},
|
|
1513
|
+
};
|
|
1514
|
+
```
|
|
1515
|
+
|
|
1516
|
+
A brand-new project's `classes` is `{}` — there is no default/built-in entry
|
|
1517
|
+
for anything, including `Users` (§13.2).
|
|
1518
|
+
|
|
1519
|
+
### 13.2 There is no pre-existing built-in `Users` class
|
|
1520
|
+
|
|
1521
|
+
On a fresh project, `/Users` doesn't exist (`GET /Users` → `code 103,
|
|
1522
|
+
"Invalid Class Name"`), and `POST /Auth/register` fails with
|
|
1523
|
+
`{"code":110,"error":"Invalid Class Request Method","type":"InvalidMethod"}`
|
|
1524
|
+
until a `Users` class is created **exactly like any other custom class**
|
|
1525
|
+
via `POST /schemas/Users`. Minimum viable shape that makes `/Auth/register`,
|
|
1526
|
+
`/Auth/login`, and `/Auth/me` work: `firstName`, `lastName`, `Email` (type
|
|
1527
|
+
`email`, required), `Password`, plus whatever else the app needs
|
|
1528
|
+
(`Mobile`, `authType`, `emailVerified`, `image`, ...).
|
|
1529
|
+
|
|
1530
|
+
### 13.3 The full `/Auth` flow, confirmed end-to-end
|
|
1531
|
+
|
|
1532
|
+
This is the sequence that actually works, with real responses:
|
|
1533
|
+
|
|
1534
|
+
```js
|
|
1535
|
+
// 1. App-level token exchange — Application ID + the mobile CLIENT KEY
|
|
1536
|
+
// (the console's "client key (for iOS/Android/Flutter)", a distinct
|
|
1537
|
+
// credential from the "js key (for JavaScript clients)" and the
|
|
1538
|
+
// "REST API key"), sent as X-BEA-JavaScript-Key regardless of the
|
|
1539
|
+
// "js"-sounding header name:
|
|
1540
|
+
POST /Auth
|
|
1541
|
+
X-BEA-Application-ID: <app id>
|
|
1542
|
+
X-BEA-JavaScript-Key: <CLIENT key, not the js key, for a mobile client>
|
|
1543
|
+
body: {"scope":"public"}
|
|
1544
|
+
→ {"access_token":"<jwt>","refresh_token":"<jwt>","token_type":"Bearer","expires_in":3700}
|
|
1545
|
+
|
|
1546
|
+
// 2. Register — needs the app access token from step 1, as x-bea-access-token:
|
|
1547
|
+
POST /Auth/register
|
|
1548
|
+
x-bea-access-token: <access_token from step 1>
|
|
1549
|
+
body: {"Email":"...", "Password":"...", "firstName":"...", "lastName":"...", "authType":"normal"}
|
|
1550
|
+
→ {"results":[{"objectId":"...", "createdAt":"...", "updatedAt":"..."}]}
|
|
1551
|
+
|
|
1552
|
+
// 3. Login — same auth as register, different response shape (flat, no `results`):
|
|
1553
|
+
POST /Auth/login
|
|
1554
|
+
x-bea-access-token: <access_token>
|
|
1555
|
+
body: {"Email":"...", "Password":"..."}
|
|
1556
|
+
→ {"session_token":"<jwt>","refresh_token":"<jwt>","token_type":"Bearer","expires_in":3700}
|
|
1557
|
+
// wrong credentials:
|
|
1558
|
+
→ {"Server":"...", "code":121, "error":"Invalid Email Or Password", "type":"InvalidUserData"}
|
|
1559
|
+
|
|
1560
|
+
// 4. Current user — use the SESSION token from login, not the access token:
|
|
1561
|
+
POST /Auth/me
|
|
1562
|
+
x-bea-session-token: <session_token from step 3>
|
|
1563
|
+
body: {}
|
|
1564
|
+
→ {"objectId":"...", "firstName":"...", "lastName":"...", "Email":"...",
|
|
1565
|
+
"Password":"...", // <- comes back in PLAINTEXT; do not log/display this response as-is
|
|
1566
|
+
"Mobile":"", "authType":"normal", "emailVerified":0, "image":""}
|
|
1567
|
+
|
|
1568
|
+
// 5. Logout:
|
|
1569
|
+
POST /Auth/logout
|
|
1570
|
+
x-bea-session-token: <session_token>
|
|
1571
|
+
→ {"success": true}
|
|
1572
|
+
```
|
|
1573
|
+
|
|
1574
|
+
**Critical gotcha, found by accident, not by reading anything**: calling
|
|
1575
|
+
`/Auth/register`, `/Auth/login`, or `/Auth/me` with the **static REST API
|
|
1576
|
+
key** (`X-BEA-Authorization` + `X-BEA-Application-ID`, the §10 `API`-global
|
|
1577
|
+
auth style) instead of an access/session token does **not** error. It
|
|
1578
|
+
returns HTTP 200 with a **fresh, unrelated app token** (the same shape as
|
|
1579
|
+
step 1) and silently ignores the request body entirely — no error, no
|
|
1580
|
+
rejection, and it is easy to mistake for a successful call since the shape
|
|
1581
|
+
looks plausible. `/Auth/*` endpoints must be driven by the access/session
|
|
1582
|
+
token flow above, never the REST API key.
|
|
1583
|
+
|
|
1584
|
+
### 13.4 §10b.9d's "adding a pointer to an existing class via PUT doesn't
|
|
1585
|
+
work" is not a blanket rule
|
|
1586
|
+
|
|
1587
|
+
§10b.9d documents `PUT /Schemas/{Class}` failing with `code 117, "Class
|
|
1588
|
+
Doesn't Exists"` when adding a pointer field to a class that already has
|
|
1589
|
+
data. Retested this exact operation live, twice, on two different
|
|
1590
|
+
already-existing classes with real rows in them:
|
|
1591
|
+
|
|
1592
|
+
```js
|
|
1593
|
+
PUT /Schemas/Restaurants
|
|
1594
|
+
body: { "className": "Restaurants", "fields": {
|
|
1595
|
+
"Location": { "name":"Location", "type":"pointer", "required":0,
|
|
1596
|
+
"pointer": { "field":"Name", "className":"locations", "objectId":"<Locations objectId>" } }
|
|
1597
|
+
}}
|
|
1598
|
+
→ {"results":{"fields":{"success":1,"message":"Fields updated successfully"}, "localization":false}}
|
|
1599
|
+
```
|
|
1600
|
+
|
|
1601
|
+
Confirmed genuinely applied (not a false-success — see §10b.9b's warning
|
|
1602
|
+
that success messages can lie) by reading it back via `GET /Schemas`: the
|
|
1603
|
+
field appeared with the correct `pointerClassId`. Did this twice
|
|
1604
|
+
(`Restaurants` and `Addresses`), both succeeded. Whatever caused the
|
|
1605
|
+
documented failure, it is not a standing rule on this project as of
|
|
1606
|
+
2026-08-29 — don't treat §10b.9d's warning as gospel without retesting
|
|
1607
|
+
against the project you're actually working with; do treat a `PUT`
|
|
1608
|
+
"success" as worth a read-back check regardless, per §10b.9b.
|
|
1609
|
+
|
|
1610
|
+
### 13.5 The built-in media/images subsystem — not documented anywhere in
|
|
1611
|
+
this file until now
|
|
1612
|
+
|
|
1613
|
+
Turning on a class's `images` option (`PUT /Schemas/{Class}` with
|
|
1614
|
+
`{"options": {"images": 1}}`) does **not** add a schema field — confirmed by
|
|
1615
|
+
reading the schema back before and after (field list unchanged). It
|
|
1616
|
+
activates a separate media subsystem: a dedicated sub-resource keyed by the
|
|
1617
|
+
record's own `objectId`, not a field in the record's body. Confirmed by
|
|
1618
|
+
reading real production code (`Rahimoun/src/views/account/admin/
|
|
1619
|
+
userprofile.view`), not by guessing from the option's name:
|
|
1620
|
+
|
|
1621
|
+
```js
|
|
1622
|
+
// Upload — note the body is an ARRAY, not an object:
|
|
1623
|
+
POST /{ClassName}/{objectId}/images
|
|
1624
|
+
body: [ { "title": "...", "album": "untitled", "file": "<dataURL string>" } ]
|
|
1625
|
+
|
|
1626
|
+
// Read — media:"images" (+ optional crops:"ax300,ax600") on a single-object GET:
|
|
1627
|
+
GET /{ClassName}/{objectId}?media=images
|
|
1628
|
+
→ { "results": [ { ..., "images": { "untitled": [
|
|
1629
|
+
{ "objectId": "...", "dir": "...", "imageax600": "...", /* one imageax<N> per requested crop */ }
|
|
1630
|
+
] } } ] }
|
|
1631
|
+
// build a URL as: entry.dir + entry.imageax600
|
|
1632
|
+
|
|
1633
|
+
// Delete:
|
|
1634
|
+
DELETE /{ClassName}/{objectId}/images/{imageObjectId}
|
|
1635
|
+
```
|
|
1636
|
+
|
|
1637
|
+
`album` groups images under a name (`"untitled"` is what production code
|
|
1638
|
+
defaults to) — a class can have more than one album. This is a *different*
|
|
1639
|
+
endpoint/shape than the list-query `media`/`crops` params in §10b.2 applied
|
|
1640
|
+
per-row in a multi-row response; this one is scoped to a single object.
|
|
1641
|
+
|
|
1642
|
+
### 13.6 Real confirmed write-failure and `where`-serialization examples
|
|
1643
|
+
|
|
1644
|
+
Concrete, from-the-wire versions of what §10b.3 and §10b.9b describe in the
|
|
1645
|
+
abstract:
|
|
1646
|
+
|
|
1647
|
+
```js
|
|
1648
|
+
// Bad column name — resolves (HTTP 200), doesn't reject, per §10b.9b:
|
|
1649
|
+
POST /Restaurants { "NoSuchColumn": "x" }
|
|
1650
|
+
→ {"results":[{"code":1054,"error":"Unknown column 'NoSuchColumn' in 'INSERT INTO'","type":"DBQueryError"}]}
|
|
1651
|
+
```
|
|
1652
|
+
|
|
1653
|
+
`where` bracket-notation vs. JSON-encoding (§10b.4's wire-format warning),
|
|
1654
|
+
confirmed side by side against two real rows named "Probe Diner" and
|
|
1655
|
+
"Second Spot":
|
|
1656
|
+
|
|
1657
|
+
```
|
|
1658
|
+
GET /Restaurants?fields=objectId,Name&where=%7B%22Name%22%3A%22Probe+Diner%22%7D (JSON-encoded)
|
|
1659
|
+
→ returns BOTH rows — the where clause was silently ignored, not an error
|
|
1660
|
+
|
|
1661
|
+
GET /Restaurants?fields=objectId,Name&where[Name]=Probe%20Diner (bracket notation)
|
|
1662
|
+
→ returns only "Probe Diner"
|
|
1663
|
+
```
|
|
1664
|
+
|
|
1665
|
+
One implementation detail worth knowing for anyone building a Dart/Flutter
|
|
1666
|
+
(or other Dio-based) client: **Dio's default `queryParameters` handling
|
|
1667
|
+
already serializes a nested `Map` value as bracket notation** — passing
|
|
1668
|
+
`{"where": {"Name": "Probe Diner"}}` as `queryParameters` produces
|
|
1669
|
+
`where%5BName%5D=Probe+Diner` on the wire with no extra code, confirmed by
|
|
1670
|
+
inspecting the actual outgoing request URI. Don't manually JSON-encode
|
|
1671
|
+
`where`/`include` before handing them to Dio; the naive-looking default
|
|
1672
|
+
already does the right thing.
|
|
1673
|
+
|
|
1674
|
+
### 13.7 Numeric field encoding: contradicts §10b.8 for this project
|
|
1675
|
+
|
|
1676
|
+
§10b.8 states `integer`/`number` columns always come back as strings. Not
|
|
1677
|
+
what was observed here, confirmed twice now: `Latitude`/`Longitude` via an
|
|
1678
|
+
`include`-resolved pointer, and `Rating`/`IsOpen` read directly (no
|
|
1679
|
+
`include`) on a real, freshly-seeded `Restaurants` row — both cases returned
|
|
1680
|
+
native JSON numbers (`4.5`, not `"4.5"`), not strings. Whichever project
|
|
1681
|
+
§10b.8 was originally written against, this project's BaaS version doesn't
|
|
1682
|
+
match it for numeric encoding. **Practical guidance regardless**: parse
|
|
1683
|
+
defensively either way — call `.toString()` on the value before
|
|
1684
|
+
`num.parse`/`parseFloat`, which is correct whether the wire value is a JSON
|
|
1685
|
+
number or a numeric string, and cheap insurance either way.
|
|
1686
|
+
|
|
1687
|
+
### 13.8 `fields` is not optional in practice; a wrong-token-type 500 on
|
|
1688
|
+
`/Auth/logout`
|
|
1689
|
+
|
|
1690
|
+
Omitting `fields` from a `GET` does **not** return every column the way a
|
|
1691
|
+
bare SQL `SELECT *` would — it returns only `objectId` plus
|
|
1692
|
+
`createdAt`/`updatedAt`, and every custom column is silently absent, not an
|
|
1693
|
+
error. Confirmed live against real, non-empty rows (seeded `Restaurants`
|
|
1694
|
+
data, queried back with no `fields` param — every custom field came back
|
|
1695
|
+
`undefined`). Every query needs its column list spelled out explicitly;
|
|
1696
|
+
there's no safe default to lean on, and the failure mode is silent missing
|
|
1697
|
+
data, not an exception.
|
|
1698
|
+
|
|
1699
|
+
Separately: `POST /Auth/logout` returns a genuine HTTP 500 when called with
|
|
1700
|
+
an app-level access token (`x-bea-access-token`) instead of a real user
|
|
1701
|
+
session token (`x-bea-session-token`) — this is NOT the same as an
|
|
1702
|
+
expired/garbage session token, which the endpoint handles gracefully
|
|
1703
|
+
(`HTTP 200, {"error":"Invalid token: Incomplete segments"}`). Reproduced via
|
|
1704
|
+
a real client app hitting this exact failure mode; the client-side root
|
|
1705
|
+
cause of sending the wrong token type in that instance wasn't nailed down
|
|
1706
|
+
with certainty (token-store persistence across repeated debug rebuilds on
|
|
1707
|
+
an iOS Simulator is one plausible culprit, not confirmed). Regardless of
|
|
1708
|
+
cause: treat any call that's only meaningful for a logged-in user
|
|
1709
|
+
(`/Auth/logout`, `/Auth/me`) as something to skip entirely client-side when
|
|
1710
|
+
no real session is stored, rather than trusting a generic access-token
|
|
1711
|
+
fallback to either work or fail cleanly.
|
|
1712
|
+
|
|
1713
|
+
---
|
|
1714
|
+
|
|
1715
|
+
## 14. Mobius CSS — the utility scale (verified against `src/css/bea.css`, 2026-08-30)
|
|
1716
|
+
|
|
1717
|
+
The spec deliberately doesn't try to be a full Mobius reference (§1), but the
|
|
1718
|
+
**spacing/size scale is not linear** and inventing an off-scale class is the most
|
|
1719
|
+
common way to break a Roxyon Console view. An off-scale class fails **silently** —
|
|
1720
|
+
no error, the rule just isn't there, and the element sits with 0 of whatever you
|
|
1721
|
+
asked for. `src/css/bea.css` is the ground truth; `src/css/styles.css` holds the
|
|
1722
|
+
Console's theme classes (`bdgc`, `_cgr`, `_cr`, `_cdg`, `_cgreen`, `iconify`,
|
|
1723
|
+
`secondary-nav*`, …) and any project-specific additions **go at the bottom of
|
|
1724
|
+
`styles.css`**.
|
|
1725
|
+
|
|
1726
|
+
### 14.1 The spacing scale — `p m` and every side/axis variant
|
|
1727
|
+
|
|
1728
|
+
Prefixes: `p pv ph pt pb pl pr` · `m mt mb ml mr` (there is **no `mv`**; `mh` only
|
|
1729
|
+
exists as `mh100 mh350` — use `mya`/`mxa`/`ma` for auto margins).
|
|
1730
|
+
|
|
1731
|
+
Allowed integers (px), **all families identical**:
|
|
1732
|
+
|
|
1733
|
+
```
|
|
1734
|
+
0 1 2 3 4 5 6 8 10 15 20 25 30 40 50 60 70 80 90 100 110 120 130 140 150 160 180 200
|
|
1735
|
+
```
|
|
1736
|
+
|
|
1737
|
+
Gaps between steps: 1 below 6, **no 7**, then 8, **no 9**, 10, then **+5 up to 30**,
|
|
1738
|
+
then **+10 up to 200**. So `p7 p11 p12 p13 p14 p16 p18 p22 …` **do not exist** —
|
|
1739
|
+
round to the nearest listed value (`p14`→`p15`, `p18`→`p20`, `mt12`→`mt10`).
|
|
1740
|
+
|
|
1741
|
+
### 14.2 Gap — `g`
|
|
1742
|
+
|
|
1743
|
+
```
|
|
1744
|
+
5 8 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100
|
|
1745
|
+
```
|
|
1746
|
+
|
|
1747
|
+
Note `g8` exists (added 2026-08-29) but **`g6` and `g12` do not** — use `g5`/`g10`.
|
|
1748
|
+
`<flx>` / `.df` is the flex container; `g<n>` sets its `gap`.
|
|
1749
|
+
|
|
1750
|
+
### 14.3 Font-size / line-height — `fs` / `lh`
|
|
1751
|
+
|
|
1752
|
+
Every integer `0`–`30`, then `32 34 36 38 40 42 44 46 48 50 55 60 65 70 75 80 85
|
|
1753
|
+
90 95 100 110 120 130 140 150 160`. `fs14`/`fs16` are fine; `fs31 fs33 …` are not.
|
|
1754
|
+
|
|
1755
|
+
### 14.4 Other scales
|
|
1756
|
+
|
|
1757
|
+
- `z` (z-index): `1 10 20…200 300…1000 2000…10000`
|
|
1758
|
+
- `op` (opacity ×%): `0 5 10 20 30 40 50 60 70 80 90 100`
|
|
1759
|
+
- `mw` (max-width ×%): `50 60 70 80 100`
|
|
1760
|
+
- radius: `round` (.25rem) `round2x` (.5) `round3x` (.75) `round4x` (1rem) `round0`
|
|
1761
|
+
`round100` `roundl` `roundr`
|
|
1762
|
+
- weight: `b`/`b700` (bold) `b400 b500 b600 b900` `n`
|
|
1763
|
+
|
|
1764
|
+
### 14.5 Layout utilities that trip people up
|
|
1765
|
+
|
|
1766
|
+
| class | actual rule | note |
|
|
1767
|
+
|---|---|---|
|
|
1768
|
+
| `.l` | `text-align: right` | **RTL-first framework — `l` is RIGHT, `r` is LEFT.** `.c` is centre. |
|
|
1769
|
+
| `.r` | `text-align: left` | |
|
|
1770
|
+
| `.h` | hide | part of a giant combined selector in bea.css; toggled at runtime via `addClass('h')`. Note the responsive variants `h-m_sb` etc. |
|
|
1771
|
+
| `.fh` | `display:none !important` | plain force-hide; use this for a hidden programmatic `modal=` trigger (`.h` sometimes keeps the element out of a `modal` binding — `.fh` is what existing code like `_forms/domains_create.view`'s `._checkModal` uses). |
|
|
1772
|
+
| `.ns` | `user-select:none` | |
|
|
1773
|
+
| `.oe` | `white-space:nowrap` | (not ellipsis) |
|
|
1774
|
+
| `.cc` | `position:absolute;top/left:50%` | centre-absolute; needs a `translate` or it's off by half its own size |
|
|
1775
|
+
| `.df .fc .fr .fw .f1` | flex shorthands | `<flx>` element == `.df` |
|
|
1776
|
+
| `.ac .as .ae` / `.jc .js .je .jsb .jsa` | align-items / justify-content | **inert on a plain `<div>`** — they only do anything once the element is `display:flex`. Use `<flx class="je g15 ac">`, not `<div class="je g15 ac">`. Same for `g<n>` (it's `gap`). |
|
|
1777
|
+
| `.bdgc` | card background+border (theme) | Console-specific, in `styles.css` |
|
|
1778
|
+
|
|
1779
|
+
### 14.6 Adding a class
|
|
1780
|
+
|
|
1781
|
+
If a value genuinely isn't on the scale and rounding hurts the design, append to
|
|
1782
|
+
the **bottom of `src/css/styles.css`** (never `bea.css` — that's the vendored
|
|
1783
|
+
framework). Prefer a scoped `<style>` block in the view with an explicit
|
|
1784
|
+
`gap:`/`padding:` when the exact px matters for one component only — see the
|
|
1785
|
+
comment in `_dashboards/dashboard.view` (`.dsh-act`) for the house pattern:
|
|
1786
|
+
*"A rule that is wrong fails visibly; a class that is not there fails quietly."*
|
|
1787
|
+
|
|
1788
|
+
---
|
|
1789
|
+
|
|
1790
|
+
## 15. Roxyon Console — house conventions (forms, inputs, modals, colour)
|
|
1791
|
+
|
|
1792
|
+
Design reference for the whole Console is **Hetzner Cloud Console, dark mode** — clone
|
|
1793
|
+
its layout, density and interaction patterns. The rules below are things the user has
|
|
1794
|
+
asked for more than once; treat them as required, not stylistic.
|
|
1795
|
+
|
|
1796
|
+
### 15.1 Forms
|
|
1797
|
+
|
|
1798
|
+
- `<form data-theme="dark" o-sub="handler">` — **always** `data-theme="dark"` on the
|
|
1799
|
+
`<form>`, and on any non-form container that holds inputs (a card, a list row with
|
|
1800
|
+
an inline editor). Without it the fields theme light and read as a bug.
|
|
1801
|
+
- Field-level validation is only `:required` (§6.6) + your own `b-sub`. There is no
|
|
1802
|
+
per-field validator directive and no `v-model`.
|
|
1803
|
+
- Close a modal form from JS with `$('[close-modal]:last').click()`.
|
|
1804
|
+
|
|
1805
|
+
### 15.2 Inputs
|
|
1806
|
+
|
|
1807
|
+
```html
|
|
1808
|
+
<label class="f1 field__input-wrap">
|
|
1809
|
+
<input type="text" name="Foo" class="ba0 bgt dark" placeholder="example.com" />
|
|
1810
|
+
</label>
|
|
1811
|
+
```
|
|
1812
|
+
|
|
1813
|
+
- `.dark` sets **white text only** — the wrapper paints the background. `.dark` + `.input`
|
|
1814
|
+
together = white text on white = invisible field. Never pair them.
|
|
1815
|
+
- Inside the older `.rx-input_wrap` labelled-field pattern the wrapper forces
|
|
1816
|
+
`background: transparent`, so there the correct combo is `hc-field__input input dark`
|
|
1817
|
+
— and *only* there.
|
|
1818
|
+
|
|
1819
|
+
### 15.3 Selects — always `sl`
|
|
1820
|
+
|
|
1821
|
+
`<select sl sl-prt="self" :required name="Foo">…</select>`. See §6.7 for the wrapper
|
|
1822
|
+
details. The two things that bite:
|
|
1823
|
+
|
|
1824
|
+
1. A select injected from JS after an async load is a **raw browser control** until
|
|
1825
|
+
the next `globalWatch()` (§5.6). Prefer `globalWatch()`; the manual fallback is
|
|
1826
|
+
`$(sel).select2({ dropdownParent: $(sel).parent(), … })` guarded by
|
|
1827
|
+
`:not(.select2-hidden-accessible)`.
|
|
1828
|
+
2. The clear-× is **opt-in** — `<select sl clear>` shows the dark-red (`#900`) clear
|
|
1829
|
+
button, `<select sl>` doesn't. Add `clear` only when unsetting the field is a real
|
|
1830
|
+
choice (rare in this app — most selects always hold a valid value).
|
|
1831
|
+
|
|
1832
|
+
### 15.4 Checkbox — the house pattern (not `input-check`)
|
|
1833
|
+
|
|
1834
|
+
```html
|
|
1835
|
+
<div class="round4x checkbox red p8">
|
|
1836
|
+
<span class="__nses"><input class="h" type="checkbox" name="Foo"></span>
|
|
1837
|
+
<span class="iconify" data-icon="mdi:checkbox-blank-outline"></span>
|
|
1838
|
+
<span class="iconify" data-icon="mdi:checkbox-marked"></span>
|
|
1839
|
+
</div>
|
|
1840
|
+
```
|
|
1841
|
+
|
|
1842
|
+
### 15.5 Modals — the edit-in-place pattern
|
|
1843
|
+
|
|
1844
|
+
Beyond §8's basics, the Console pattern for "edit a row from a list without a route
|
|
1845
|
+
change and without re-rendering the whole list":
|
|
1846
|
+
|
|
1847
|
+
1. One **hidden** trigger in the parent view:
|
|
1848
|
+
`<div class="fh _fooModal" modal="…/modals/foo" modal-class="gen-modal" @modal-open="_fooArgs"></div>`
|
|
1849
|
+
(`.fh`, never `.h` — see §14.5).
|
|
1850
|
+
2. Parent sets a module-scoped var, then clicks the trigger:
|
|
1851
|
+
`_fooPending = { id, record }; $('._fooModal').click();`
|
|
1852
|
+
`function _fooArgs() { return _fooPending; }`
|
|
1853
|
+
3. Modal reads it: `view.scope(function (view, props) { var p = props.modal; … });`
|
|
1854
|
+
4. **Modal → parent callback:** the parent attaches a function to `window`
|
|
1855
|
+
(`window._fooApply = function (row, mode) { … }`); the modal calls it on a
|
|
1856
|
+
successful save. (View `<script>`s share globals — this is the same mechanism as
|
|
1857
|
+
`myList.refresh()` being visible inside `modals/add_site.view`.)
|
|
1858
|
+
5. The save API returns the **stored row**, so the parent patches exactly one node
|
|
1859
|
+
(`document.querySelector('.row[data-id="'+id+'"]').outerHTML = rowHtml(row)`), or
|
|
1860
|
+
inserts one, or removes one. **Re-rendering the whole list on every save/cancel is
|
|
1861
|
+
the glitch to avoid** — a modal Cancel should touch nothing, a Save should touch
|
|
1862
|
+
one row. After an `outerHTML`/`innerHTML` swap that contains icons, call
|
|
1863
|
+
`Iconify.scan()`; `@click` bindings survive the swap on their own (§5.6).
|
|
1864
|
+
|
|
1865
|
+
`modal="path"` with a bare `close-modal` and `@modal-open`/`@modal-close`/
|
|
1866
|
+
`@modal-beforeclose` handlers is all still §8 — this is just how they're wired for
|
|
1867
|
+
list editing.
|
|
1868
|
+
|
|
1869
|
+
### 15.6 `rx.*` — no `rx.del`
|
|
1870
|
+
|
|
1871
|
+
The Console wraps only `rx.get`, `rx.post`, `rx.put` (`engine.*` is the same object).
|
|
1872
|
+
**There is no `rx.del` / `rx.delete`.** For a delete, POST to a `/delete` subpath the
|
|
1873
|
+
handler understands (the `_payment` block does this: `/_payment/card/delete`,
|
|
1874
|
+
`/_payment/card/default`, …) — or drop to the raw framework client `API.delete(path)`.
|
|
1875
|
+
|
|
1876
|
+
### 15.7 Escape anything you build as an HTML string
|
|
1877
|
+
|
|
1878
|
+
Any value interpolated into a `.html(...)` / `insertAdjacentHTML(...)` string must go
|
|
1879
|
+
through an escaper first (`& < > "`). A DKIM key, a php.ini description containing
|
|
1880
|
+
`<?`, a domain with odd characters — all have torn a section apart in the past.
|
|
1881
|
+
Never use `[ctc]`/attribute selectors that match every row for copy affordances — use
|
|
1882
|
+
`.rx-copy` + `data-copy="…"` + `@click="rxCopy"` (global, in `styles.css` / `index.js`).
|
|
1883
|
+
|
|
1884
|
+
### 15.8 Colour
|
|
1885
|
+
|
|
1886
|
+
Text-colour helpers (in `styles.css`): `_cr` (red / danger), `_cgreen`, `_camber`,
|
|
1887
|
+
`_corange`, `_cgr` (muted grey), `_cdg` (darker grey), `_cpurple`, `_cpink`, `_cb`,
|
|
1888
|
+
`_cw` (white), `_cmain`, `_cp`, `_cg`, `_cgl`, `_cicon`.
|
|
1889
|
+
|
|
1890
|
+
Theme CSS variables worth knowing (don't hard-code hex when one of these fits):
|
|
1891
|
+
`--color-border--dark`, `--color-card-background`, `--color-input--background`,
|
|
1892
|
+
`--color-box-code-background`, `--color-text`, `--color-text-secondary`,
|
|
1893
|
+
`--color-status-indicator--{green,amber,orange,red,grey}`,
|
|
1894
|
+
`--color-status-badge-{green,orange,red}--{background,color}`.
|
|
1895
|
+
|
|
1896
|
+
For a set of category badges (DNS record types, service states…) that each need their
|
|
1897
|
+
own accent, set a single custom prop per class and derive fill/border from it, with an
|
|
1898
|
+
rgba fallback before the `color-mix()`:
|
|
1899
|
+
|
|
1900
|
+
```css
|
|
1901
|
+
.badge { color: var(--tc, #9aa0a6);
|
|
1902
|
+
background: rgba(138,138,138,.13); border: 1px solid rgba(138,138,138,.38);
|
|
1903
|
+
background: color-mix(in srgb, var(--tc) 14%, transparent);
|
|
1904
|
+
border-color: color-mix(in srgb, var(--tc) 40%, transparent); }
|
|
1905
|
+
.badge-a { --tc: #3b82f6; } /* set --tc on the ROW too, so a hover accent can read it */
|
|
1906
|
+
```
|
|
1907
|
+
|
|
1908
|
+
### 15.9 "Editable?" — records the platform owns
|
|
1909
|
+
|
|
1910
|
+
When surfacing platform-managed data for editing (DNS is the worked example), lock the
|
|
1911
|
+
records the system re-derives and would silently break: **SOA** and **`*._domainkey.*`
|
|
1912
|
+
DKIM** (rspamd publishes the key). Leave the ones customers legitimately customise
|
|
1913
|
+
(SPF `@` TXT, DMARC `_dmarc` TXT, apex `NS` — with a warning). Enforce on the server;
|
|
1914
|
+
the `managed:true` flag from the API just drives the UI.
|