@yuneta/gobj-ui 5.4.0 → 5.6.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
@@ -322,6 +322,61 @@ Runnable reference: the **Cards** chapter of `test-app` (four levels plus a
322
322
  panel that mutates the live tree), driven by `test-app/_qa_nodetree.mjs` and
323
323
  `test-app/_qa_extra.mjs`.
324
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
+
325
380
  ### C_YUI_JSON — lazy JSON tree viewer
326
381
 
327
382
  Indentation follows the house rule: four characters per level, plus a **guide
@@ -409,6 +464,46 @@ host can use it to toggle. The tree is a **pure child of the window**, so every
409
464
  teardown path (the ✕, or the host destroying the window to toggle the entry
410
465
  off) takes it down too.
411
466
 
467
+ ### Toolbar badge — a count pinned to an item's icon
468
+
469
+ ```js
470
+ /* app_config.json — seeds the FIRST paint only */
471
+ { "id": "alarms", "icon": "yi-triangle-exclamation", "align": "end",
472
+ "aria_label": "alarms", "badge": 0,
473
+ "action": {"type": "navigate", "route": "/alarms"} }
474
+
475
+ /* the interface that matters: a count is a RUNTIME fact */
476
+ yui_shell_set_toolbar_item_badge(shell, "alarms", 3);
477
+ yui_shell_set_toolbar_item_badge(shell, "alarms", 0); // clears it
478
+ ```
479
+
480
+ An icon-only toolbar button is a link; the badge is what makes it a
481
+ **signal**. Without a number, an alarm bell cannot say whether anything needs
482
+ you — which is the reason to look at it at all.
483
+
484
+ Rules baked in, all for the same reason (a badge that lies costs more than no
485
+ badge):
486
+
487
+ - **`0`, `""`, `null` and `false` all clear it.** A badge reading "0" is worse
488
+ than none: it draws the eye to say nothing.
489
+ - **Over 99 renders `99+`.** The toolbar is a fixed-width row and a four-digit
490
+ pill pushes its neighbours off a phone screen.
491
+ - **A string passes through** for the states that are not counts (`"!"`, `"…"`).
492
+ - **Unknown item id is a silent no-op**, so an app whose toolbar has no such
493
+ item does not log an error on every tick of whatever feeds the number.
494
+ - Writing the **same** value again touches no DOM: the badge is a
495
+ `role="status"` live region, and rewriting it would have a screen reader
496
+ announce the same number on every tick.
497
+
498
+ `role="status"` and not `aria-hidden`, deliberately: the button carries an
499
+ explicit `aria-label`, and an explicit label **replaces** an element's content
500
+ for a screen reader — a badge inside it would otherwise be silent. As its own
501
+ live region it is both read and announced when it changes.
502
+
503
+ > `C_YUI_NAV` items do **not** have this. Its item contract listed `badge` for a
504
+ > long time and nothing ever rendered it; the claim is gone. Implement it there
505
+ > the day a menu entry needs one.
506
+
412
507
  ### Modals — `yui_shell_show_modal` and the `before_close` veto
413
508
 
414
509
  `yui_shell_show_modal(shell, $box, opts)` is the standard popup: pass
@@ -440,6 +535,25 @@ shapes, and the fix for each:
440
535
  | 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.) |
441
536
  | `title` / `aria-label` set with `t()` | tooltip stuck in the old language | `data-i18n-title` / `data-i18n-aria-label` |
442
537
  | 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) |
538
+ | 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) |
539
+
540
+ **Carrying the key is not enough for the FIRST render.** A node is born holding
541
+ the raw English key, and the app's `refresh_language()` passes only walk what
542
+ already exists: the shell tree at start up, `document.body` on a language
543
+ switch. Anything built later — a `C_YUI_NODE` strip rendered when you walk into
544
+ it, a lazily-built toolbar panel — is reached by neither, so it renders the key:
545
+ lower-case English that never changes language, which is exactly what a missing
546
+ key looks like. Division of labour:
547
+
548
+ ```js
549
+ yui_shell_translate(shell, $el); // LIBRARY-built DOM, right after building it
550
+ ```
551
+
552
+ and **app view gclasses translate their own DOM** — they own a `t`, so they call
553
+ `refresh_language($container, t)` at the end of their build (this is why the
554
+ shell does not translate a mounted view: see `mount_view` in `c_yui_shell.js`).
555
+ `yui_shell_translate` is a no-op when the app registered no translator, so
556
+ behaviour is unchanged for apps that never call `yui_shell_set_translator`.
443
557
 
444
558
  **The contract.** The app owns the locales: it switches its i18next and calls
445
559