@yuneta/gobj-ui 5.3.3 → 5.5.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/README.md CHANGED
@@ -162,7 +162,11 @@ seen*:
162
162
  "content": {"gclass": "C_MY_LANDING", "kw": {}},
163
163
  "children": [
164
164
  {"id": "energy", "label": "Energy", "icon": "yi-bolt",
165
- "projection": {"index": {"layout": "cards"}},
165
+ "projection": {
166
+ "index": {"layout": "cards"},
167
+ "chrome": [{"layout": "tabs", "show_on": ">=tablet"},
168
+ {"layout": "backbar", "show_on": "<tablet"}]
169
+ },
166
170
  "children": [ /* … any depth … */ ]}
167
171
  ]
168
172
  }
@@ -173,6 +177,16 @@ seen*:
173
177
  icon-bar/backbar and `show_on` all work unchanged) in two modes: `index`
174
178
  when the node is the tip of the path — the projection IS the page — and
175
179
  `chrome` when a child is showing — the projection is the strip around it.
180
+ - **Chrome belongs to the node that declares it — so every branch declares its
181
+ own.** A node's `chrome` strip lists *that* node's children, and its backbar
182
+ goes back to *that* node's route (`back_route = my_route`). A branch that
183
+ declares no `chrome` therefore contributes no strip, and the only ← the user
184
+ can reach is the nearest ancestor that did declare one. Declaring the pair
185
+ **only at the root** is the mistake this rule exists to name: the whole
186
+ subtree then shows one "← root" that says the same thing at every depth,
187
+ instead of one ← per level going up exactly one level. Repeat the same
188
+ `chrome` on every branch that can have a tip below it — that is what
189
+ `test-app`'s `/cards` does, and what makes its ← hierarchical.
176
190
  - **`content` and `children` are not exclusive**: a section with its own page
177
191
  and sub-pages is one node.
178
192
  - **The route table does not grow.** The host declares ONE route; everything
@@ -187,6 +201,30 @@ seen*:
187
201
  `projection.path`) while the rest of the tree keeps its tabs. Note the
188
202
  asymmetry that makes it a third mode and not a layout: `index` and `chrome`
189
203
  project a node's CHILDREN; `path` projects the way in.
204
+ - **`nav_mode` — the three shapes as one runtime knob.** The two bullets above
205
+ describe what a tree *declares*; `nav_mode` is how a user *chooses* between
206
+ the shapes without the app rewriting anything:
207
+
208
+ | mode | what it shows | equivalent declaration |
209
+ |---|---|---|
210
+ | `"stack"` (default) | one strip per ancestor | whatever each branch declares |
211
+ | `"back"` | only the tip's parent, as a `← parent` | `chrome: {"layout":"backbar"}` everywhere + `chrome_depth: 1` |
212
+ | `"path"` | the trail as ONE line | `path: {"layout":"breadcrumb"}` on the root + `chrome_depth: 0` |
213
+
214
+ It belongs to the **root** (`yui_node_set_nav_mode(root, "path")`, or
215
+ `"nav_mode"` in its declaration) and the whole tree reads it from there —
216
+ ancestors stacking strips while a descendant drew a breadcrumb would be
217
+ saying the same thing twice in two languages. Set on a middle node it is
218
+ refused, loudly, rather than accepted and ignored.
219
+
220
+ A mode **filters the renders as they are asked for; it never rewrites what
221
+ the app declared.** That is what makes `"stack"` an exact restore: a branch
222
+ that declared `vertical` chrome comes back as `vertical`, not as the tabs a
223
+ canonical "stacked" shape would have imposed. The `index` projection is
224
+ never touched by a mode — how a node shows its own children when it IS the
225
+ page is not a statement about depth. Modes are per tree, so an app can run
226
+ `/admin` as a breadcrumb and `/alarms` as a backbar; `test-app`'s node lab
227
+ cycles all three on the live tree.
190
228
  - **`chrome_depth`** caps the stacked chrome: with every ancestor painting its
191
229
  own strip, depth N shows N-1 of them. A node declares how many its corner of
192
230
  the tree deserves (`0` = none, omit = all), the **deepest declaration on the
@@ -199,6 +237,7 @@ seen*:
199
237
  yui_node_add(node, spec, index) yui_node_remove(node, node_id)
200
238
  yui_node_set_projection(node, proj) yui_node_set_content(node, content)
201
239
  yui_node_set_chrome_depth(node, n) yui_node_tree_version(node)
240
+ yui_node_set_nav_mode(root, mode) yui_node_nav_mode(node)
202
241
  yui_node_find(node, "energy/north") yui_node_route(node)
203
242
  ```
204
243
 
@@ -283,6 +322,61 @@ Runnable reference: the **Cards** chapter of `test-app` (four levels plus a
283
322
  panel that mutates the live tree), driven by `test-app/_qa_nodetree.mjs` and
284
323
  `test-app/_qa_extra.mjs`.
285
324
 
325
+ ### C_YUI_SERVICE_VIEW — mounting a view that talks to a backend
326
+
327
+ A view asks the backend for data with `gobj_command(remote, …, src = itself)`,
328
+ and `C_IEVENT_CLI` routes the answer back with
329
+ `gobj_find_service(gobj_name(src))` — **which only finds registered services**.
330
+ Neither host creates one, so a backend-talking view mounted directly at a route
331
+ never receives a single answer: it sits empty while the ievent logs *"service
332
+ not found"* once per answer. And a route's `target.kw` is static JSON, so it
333
+ cannot carry the live transport pointer either.
334
+
335
+ Two shapes, because the callers are not alike:
336
+
337
+ ```js
338
+ /* A route with NO extras: declare the host, name the view it hosts. */
339
+ { gclass: "C_YUI_SERVICE_VIEW", kw: {
340
+ view_gclass: "C_MY_VIEW",
341
+ service_name: "#my-view", // UNIQUE per mount — see below
342
+ view_kw: { title: "…" }
343
+ }}
344
+
345
+ /* A wrapper that keeps its own extras (url segments into the hosted view,
346
+ * rebinding it when a connection drops): drop only the boilerplate. */
347
+ let view = yui_mount_service_view(gobj, {
348
+ gclass: "C_YUI_TREEDB_TOPICS",
349
+ name: service_name(gobj),
350
+ kw: {...},
351
+ transport: remote // already resolved (e.g. per connection);
352
+ }); // omit it for "__remote_service__"
353
+ ```
354
+
355
+ The hosted gclass must declare the attr the transport is injected under
356
+ (`gobj_remote_yuno` by default), build its `$container` in `mt_create`, and flag
357
+ `EVF_PUBLIC_EVENT` on whatever arrives from the backend — the ievent drops
358
+ events that are not public.
359
+
360
+ **The service name must be unique per mount**, and a duplicate is dangerous
361
+ precisely because it is not fatal: gobj-js logs *"service ALREADY REGISTERED.
362
+ Will be UPDATED"* and **rebinds the name**, so two mounts of one route would
363
+ cross their answers. Derive it from the route (or from whatever else makes the
364
+ mount unique — a connection id, a workspace), never from the gclass alone.
365
+
366
+ > **Why the hosts do not just create services.** It would make every routed view
367
+ > an inter-yuno endpoint by default, against the framework's rule that only
368
+ > named services are; most views never talk to a backend; and the collision
369
+ > above would become the default failure mode. Opt-in per route instead.
370
+ >
371
+ > **Known asymmetry, deliberately left alone:** `C_YUI_SHELL` mounts a view with
372
+ > `gobj_create()` (a plain child) and `C_YUI_NODE` with
373
+ > `gobj_create_pure_child()`. The flag decides whether a gclass that consults
374
+ > `gobj_is_pure_child()` sends its output event straight to the parent or
375
+ > publishes it. Today nothing consults it *on a view* (only `c_ievent_cli` and
376
+ > `c_timer` do, and always about themselves), so the difference has no observed
377
+ > consequence — but the same view gclass does get a different flag depending on
378
+ > who mounted it. Align it the day it bites, with the case that bit.
379
+
286
380
  ### C_YUI_JSON — lazy JSON tree viewer
287
381
 
288
382
  Indentation follows the house rule: four characters per level, plus a **guide
@@ -401,6 +495,25 @@ shapes, and the fix for each:
401
495
  | A composed string (`` `${key} · ${t(mode)}` ``) | carries no key at all | split it: the translatable halves get their own key. (Note `createElement2` **trims** text nodes — space a `·` separator with CSS, not with spaces.) |
402
496
  | `title` / `aria-label` set with `t()` | tooltip stuck in the old language | `data-i18n-title` / `data-i18n-aria-label` |
403
497
  | Anything a WIDGET renders (a Tabulator header, its paginator, a formatter) | drawn once; no attribute reaches it | subscribe to the shell and re-render (below) |
498
+ | DOM built AFTER start up (a node's strips, a dropdown panel) | renders the raw key — indistinguishable from a MISSING key | `yui_shell_translate(shell, $el)` right after building it (below) |
499
+
500
+ **Carrying the key is not enough for the FIRST render.** A node is born holding
501
+ the raw English key, and the app's `refresh_language()` passes only walk what
502
+ already exists: the shell tree at start up, `document.body` on a language
503
+ switch. Anything built later — a `C_YUI_NODE` strip rendered when you walk into
504
+ it, a lazily-built toolbar panel — is reached by neither, so it renders the key:
505
+ lower-case English that never changes language, which is exactly what a missing
506
+ key looks like. Division of labour:
507
+
508
+ ```js
509
+ yui_shell_translate(shell, $el); // LIBRARY-built DOM, right after building it
510
+ ```
511
+
512
+ and **app view gclasses translate their own DOM** — they own a `t`, so they call
513
+ `refresh_language($container, t)` at the end of their build (this is why the
514
+ shell does not translate a mounted view: see `mount_view` in `c_yui_shell.js`).
515
+ `yui_shell_translate` is a no-op when the app registered no translator, so
516
+ behaviour is unchanged for apps that never call `yui_shell_set_translator`.
404
517
 
405
518
  **The contract.** The app owns the locales: it switches its i18next and calls
406
519