@yuneta/gobj-ui 3.0.0 → 5.0.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.
Files changed (45) hide show
  1. package/README.md +335 -6
  2. package/dist/gobj-ui.cjs.js +10930 -5027
  3. package/dist/gobj-ui.es.js +10948 -5080
  4. package/index.js +45 -1
  5. package/package.json +11 -11
  6. package/src/c_g6_nodes_tree.js +128 -40
  7. package/src/c_yui_form.js +112 -53
  8. package/src/c_yui_gobj_tree_js.js +58 -36
  9. package/src/c_yui_json.css +139 -0
  10. package/src/c_yui_json.js +928 -0
  11. package/src/c_yui_json_graph.js +110 -20
  12. package/src/c_yui_map.js +19 -37
  13. package/src/c_yui_nav.js +6 -9
  14. package/src/c_yui_period.css +184 -0
  15. package/src/c_yui_period.js +1441 -0
  16. package/src/c_yui_shell.css +33 -0
  17. package/src/c_yui_shell.js +672 -111
  18. package/src/c_yui_treedb_graph.js +639 -37
  19. package/src/c_yui_treedb_schema.js +478 -0
  20. package/src/c_yui_treedb_topic_with_form.css +7 -42
  21. package/src/c_yui_treedb_topic_with_form.js +152 -103
  22. package/src/c_yui_treedb_topics.css +73 -0
  23. package/src/c_yui_treedb_topics.js +1070 -29
  24. package/src/c_yui_window.js +180 -97
  25. package/src/c_yui_window_manager.js +38 -4
  26. package/src/json_view_helpers.js +214 -0
  27. package/src/json_view_helpers.test.js +135 -0
  28. package/src/route_map_model.js +317 -0
  29. package/src/route_map_model.test.js +209 -0
  30. package/src/route_resolver.js +24 -1
  31. package/src/route_resolver.test.js +40 -1
  32. package/src/shell_modals.js +117 -39
  33. package/src/shell_route_map.css +225 -0
  34. package/src/shell_route_map.js +363 -0
  35. package/src/tabulator.css +67 -0
  36. package/src/yui_dev.js +130 -8
  37. package/src/yui_frontend_view.js +106 -0
  38. package/src/yui_icons.css +62 -0
  39. package/src/yui_inputs.css +8 -3
  40. package/src/yui_inputs.js +51 -8
  41. package/src/yui_tabulator_i18n.js +128 -0
  42. package/src/yui_theme.js +147 -0
  43. package/src/yui_time.js +666 -0
  44. package/src/yui_time.test.js +330 -0
  45. package/src/yui_toolbar.js +86 -43
package/README.md CHANGED
@@ -8,6 +8,56 @@ this line in `3.0.0` — the frozen v1 npm line still ships it.
8
8
 
9
9
  Published as `@yuneta/gobj-ui`. Built on top of [`@yuneta/gobj-js`](https://github.com/artgins/gobj-js).
10
10
 
11
+ > **Routing & navigation:** every navigable state is a URL. Before adding any
12
+ > view or navigable element, read **[`ROUTING.md`](ROUTING.md)** — the shell's
13
+ > routing contract (URL = source of truth, push/replace history, the
14
+ > position/preference/transient litmus).
15
+ >
16
+ > **BREAKING (5.0.0):** a **dependency-only major** — no component API moved.
17
+ > The peer floors are now `maplibre-gl` `^6.0.0`, `@yuneta/gobj-js` `^7.8.7`,
18
+ > `i18next` `^26.3.6`, `tom-select` `^2.6.2`, `vanilla-jsoneditor` `^3.13.0`.
19
+ > maplibre v6 is ESM-only with no default export, so a consumer imports it as
20
+ > `import * as maplibregl`; bundling the map with Vite 8 also means emitting
21
+ > maplibre's worker + shared chunk yourself and pointing `setWorkerUrl()` at
22
+ > them (Vite cannot statically follow v6's dynamic worker URL, and a `.mjs`
23
+ > worker is refused when the host serves it as `application/octet-stream`).
24
+ > The test-app's `vite.config.js` (`maplibre_worker_assets`) + `src/main.js`
25
+ > are the reference wiring.
26
+ >
27
+ > **BREAKING (4.0.0):** `yui_shell_navigate(shell, route)` now **pushes** a
28
+ > history entry by default; pass `{replace:true}` for a redirect / normalization
29
+ > / F5-restore — anything *code* decided rather than the user. It used to replace
30
+ > unless given `{push:true}` (still accepted, now redundant). A call left
31
+ > unmigrated only leaves a spurious Back entry; the default is the
32
+ > failure-tolerant direction, since a forgotten `{push}` silently broke Back.
33
+ > See ROUTING.md §7/§9.1.
34
+ >
35
+ > **BREAKING (4.0.0):** the legacy `__yui_main__` theme/resize service is
36
+ > **gone from v2**. Components are self-contained: the theme lives in
37
+ > `<html data-theme>` and gclasses follow it through **`src/yui_theme.js`**
38
+ > (`yui_theme_now()` / `yui_is_dark()` / `yui_watch_theme(gobj)`, all
39
+ > barrel-exported — the watcher translates the DOM mutation *and* the OS
40
+ > `prefers-color-scheme` flip into `EV_THEME`); reflow uses each component's
41
+ > own `ResizeObserver`. An app that registered a `__yui_main__` service for
42
+ > gobj-ui's benefit can delete it; do not re-add one.
43
+ >
44
+ > **BREAKING (4.0.0):** a window/modal **`title` is now an i18n KEY**,
45
+ > rendered with `data-i18n` so it re-translates on language change. Pass the
46
+ > key, never `t(key)`, and never compose data into it — the DATA half (a
47
+ > topic/service/marker name) travels in the new **`title_prefix`** attr/opt,
48
+ > shown before the title and never translated (`C_YUI_WINDOW`,
49
+ > `yui_shell_show_modal`, the dock chip). The old `title_fn`/`retitle_modal`
50
+ > hooks are removed.
51
+ >
52
+ > **BREAKING (4.0.0):** **minimize requires a window manager.**
53
+ > `C_YUI_WINDOW` paints its minimize button only when the window has a
54
+ > `manager` (`C_YUI_WINDOW_MANAGER`): minimize means "send to the dock", and
55
+ > without a manager there is nowhere to send it — so `showMin` is now **ignored**
56
+ > when there is no manager, and a manager-less window shows only
57
+ > maximize/restore + close. The self-contained **"shade"** fallback (roll up to
58
+ > the title bar in place) and its `is-shaded` CSS are **removed**; an app that
59
+ > relied on shading needs to register a manager.
60
+
11
61
  ## Two maintained lines
12
62
 
13
63
  This repository carries **two parallel lines** with different layouts and
@@ -15,15 +65,17 @@ consumers. They are independent snapshots (no shared git ancestry):
15
65
 
16
66
  | Line | Branch | Tag | Layout | Consumed by | How | Status |
17
67
  |------|--------|-----|--------|-------------|-----|--------|
18
- | **v2** | `main` | `2.0.0`+ | `src/` subdir | **wattyzer** | local `file:` dep on the yunetas submodule | active development |
19
- | **v1** | `v1` | `1.0.0` | `src/` subdir | **estadodelaire**, **hidraulia** | published npm `@yuneta/gobj-ui@^1.0.0` | frozen, maintenance-only |
68
+ | **v2** | `main` | `2.0.0`+ | `src/` subdir | **wattyzer**, **gui_agent**, **gui_treedb** | local `file:` dep on the yunetas submodule | active development |
69
+ | **v1** | `v1` | `1.0.1` | `src/` subdir | **estadodelaire**, **hidraulia** | published npm `@yuneta/gobj-ui@^1.0.1` (dist-tag `legacy`) | frozen, maintenance-only |
20
70
 
21
71
  - **v2 / `main`** is the active development line: the declarative shell
22
72
  (legacy-stack-free since `3.0.0`). It is embedded as a git submodule in **yunetas** at
23
- `kernel/js/gobj-ui`, and **wattyzer** consumes that checkout as a `file:`
24
- dependency (`@yuneta/gobj-ui` → `../../../yunetas/kernel/js/gobj-ui`),
25
- importing by package specifier (`@yuneta/gobj-ui/src/*.js`, exports map
26
- `"./src/*"`; the `index.js` barrel and the vite plugin stay at the package root).
73
+ `kernel/js/gobj-ui`, and **wattyzer** plus the in-repo JS yunos
74
+ (**`yunos/js/gui_agent`**, **`yunos/js/gui_treedb`**) consume that checkout as a
75
+ `file:` dependency (`@yuneta/gobj-ui` → `../../../kernel/js/gobj-ui` from a
76
+ yuno; `../../../yunetas/kernel/js/gobj-ui` from wattyzer), importing by package
77
+ specifier (`@yuneta/gobj-ui/src/*.js`, exports map `"./src/*"`; the `index.js`
78
+ barrel and the vite plugin stay at the package root).
27
79
  - **v1 / `v1`** is the frozen legacy-only stack (the declarative shell is not on
28
80
  this line). It is **published to npm**; estadodelaire and hidraulia depend on
29
81
  `@yuneta/gobj-ui@^1.0.0` from the registry. Land only maintenance fixes here,
@@ -58,8 +110,257 @@ npm test # vitest (v2/main only; v1 has no test target)
58
110
  tarball; v2 (wattyzer) imports source files by specifier. Rebuild `dist/` to
59
111
  validate and before publishing a v1 release.
60
112
 
113
+ ## Components
114
+
115
+ ### Site map — `yui_shell_show_route_map`
116
+
117
+ Every declarative-shell app can render its WHOLE navigation surface —
118
+ toolbar + account menu + every declared menu + live dynamic tabs + each
119
+ view's contributed sub-routes + the routes declared only in the route table —
120
+ as a printable, filterable, clickable tree (a floating `C_YUI_WINDOW`, modal
121
+ fallback) that doubles as the app's basic documentation. The current route is
122
+ marked "you are here". Wire it from an account-menu entry
123
+ (`type:"event"` → `EV_OPEN_SITEMAP`, or a deep-linkable `/sitemap` action
124
+ route with `redirect:"back"`) and call
125
+ `yui_shell_show_route_map(shell, {t})` from the handler; a second call
126
+ toggles it closed. The tree model is `yui_shell_nav_map()` /
127
+ `route_map_model.js` (pure, unit-tested). Semantics and the contributor
128
+ protocols (`yui_shell_set_sub_routes`, `yui_shell_register_event_handler`)
129
+ live in [`ROUTING.md`](ROUTING.md).
130
+
131
+ ### C_YUI_JSON — lazy JSON tree viewer
132
+
133
+ A container-agnostic viewer (like `C_YUI_PAGER`): it owns only a toolbar +
134
+ scrollable tree body and exposes a `$container` the parent mounts wherever it
135
+ wants (a `C_YUI_WINDOW` body, a `yui_shell_show_modal` card, or inline). It is
136
+ built to show **arbitrarily large** JSON, so it never assumes the whole
137
+ document fits in memory or the DOM.
138
+
139
+ **Server-driven lazy expansion.** The C kernel's `kw_collapse()` (`kwid.c`,
140
+ used by the `print-tranger` command) truncates over-limit dicts/arrays into a
141
+ sentinel — `{ "__collapsed__": { "path": …, "size": N } }` (dict) or
142
+ `[ { "__collapsed__": … } ]` (array). `C_YUI_JSON` renders each sentinel as an
143
+ expandable stub and, when the user opens it, **does not fetch anything itself**:
144
+ it publishes `EV_EXPAND_PATH {path, size}` to its subscriber. The subscriber is
145
+ the only party that knows the backend (it re-issues `print-tranger path=<path>`
146
+ with limits, or any equivalent), and hands the subtree back via
147
+ `EV_SUBTREE_LOADED {path, json}`. Only expanded containers are materialised in
148
+ the DOM, so the tree stays bounded regardless of document size. With no
149
+ sentinels present it degrades to a plain client-side collapsible tree.
150
+
151
+ **Contract:**
152
+
153
+ - Attributes: `subscriber`, `title` (i18n key, optional), `json_data` (initial
154
+ JSON, optional), `$container` (mounted by the parent).
155
+ - Input events: `EV_SET_JSON {json}` (replace the whole document; `ST_EMPTY` →
156
+ `ST_READY`), `EV_SUBTREE_LOADED {path, json}` (splice a fetched subtree),
157
+ `EV_SUBTREE_ERROR {path, error}`, plus `EV_REFRESH` / `EV_SHOW` / `EV_HIDE` /
158
+ `EV_LANGUAGE_CHANGED`.
159
+ - Output event: `EV_EXPAND_PATH {path, size}` (`EVF_OUTPUT_EVENT`) — the parent
160
+ must declare it in its own FSM (CHILD subscription model).
161
+ - Internal (DOM → FSM): `EV_TOGGLE_NODE`, `EV_EXPAND_COLLAPSED`, `EV_SEARCH`,
162
+ `EV_EXPAND_ALL`, `EV_COLLAPSE_ALL`, `EV_COPY_ALL`. Every kw carries only a
163
+ `path` string — never a DOM node or gobj.
164
+ - Paths use the kernel delimiter (backtick) and index arrays numerically, so a
165
+ path emitted by the viewer round-trips through `kw_find_path` on the backend.
166
+
167
+ **Backend note.** The Raw JSON feed is `print-tranger`, which serves the tranger
168
+ with both dict- and array-drill (via `kw_collapse()`): `c_tranger.c` for a
169
+ `C_TRANGER` service, and `C_NODE` (it holds `priv->tranger`) for a **treedb**.
170
+ A document that arrives with no `__collapsed__` sentinels is simply rendered
171
+ client-side (no lazy drill).
172
+
173
+ Logical DOM classes: `JSON_VIEWER`, `JSON_TOOLBAR`, `JSON_SEARCH`, `JSON_TREE`,
174
+ `JSON_ROW`, `JSON_KEY`, `JSON_VALUE`, `JSON_SUMMARY`, `JSON_COLLAPSED`,
175
+ `JSON_TIME`. The gclass imports its own `c_yui_json.css`.
176
+
177
+ ### C_YUI_TREEDB_SCHEMA — the treedb as a graph of topics (prototype)
178
+
179
+ A landing view that draws a treedb as a **graph of topics** — one node per
180
+ topic, one edge per hook/fkey relationship — built from the schema `descs`
181
+ **alone**: no data, no backend calls. It is the "every treedb is a graph" rule
182
+ applied to the schema itself, and an alternate landing to the topic cards. A
183
+ node click opens that topic's table through a real hash navigation, so the
184
+ graph is a navigation surface rather than a picture.
185
+
186
+ **Contract:**
187
+
188
+ - Attributes: `subscriber`, `descs` (`{topic_name: desc}`, the schema),
189
+ `node_route` (a hash-route template carrying a `{topic}` placeholder, e.g.
190
+ `#/topics/db/<sel>/{topic}` — a node click resolves it and navigates),
191
+ `system` (include the `__*__` system topics too, default `false`),
192
+ `$container` (mounted by the parent).
193
+ - Events: `EV_SHOW`, `EV_REBUILD`, `EV_THEME` (restyle — it repaints the G6
194
+ graph in place, preserving the user's zoom/pan), plus the internal
195
+ `EV_NODE_CLICK` a node click sends into the FSM.
196
+
197
+ Marked a **prototype**: it is barrel-exported and public from 4.0.0, but its
198
+ shape may still move. Renders with `@antv/g6` (no CSS of its own).
199
+
200
+ ### Frontend view — `setup_frontend_view`
201
+
202
+ `setup_frontend_view(self)` opens the **gobj tree of the app's own yuno** in a
203
+ floating `C_YUI_WINDOW` — the browser-side peer of the Developer window
204
+ (`setup_dev` / `build_dev_panel` / `apply_dev_traces` / `dev_window_was_open`,
205
+ `yui_dev.js`), and the JS answer to `view-gobj-tree` on a C yuno. Wire it to an
206
+ account-menu entry. It returns `null` when the window is already open, so the
207
+ host can use it to toggle. The tree is a **pure child of the window**, so every
208
+ teardown path (the ✕, or the host destroying the window to toggle the entry
209
+ off) takes it down too.
210
+
211
+ ### Modals — `yui_shell_show_modal` and the `before_close` veto
212
+
213
+ `yui_shell_show_modal(shell, $box, opts)` is the standard popup: pass
214
+ `{dialog:true}` for the adaptive dialog (centered card with the X top-right on
215
+ desktop, full-screen sheet with a back arrow on mobile), and the shell wires
216
+ Escape / backdrop / browser Back for you. It returns a `close()`.
217
+
218
+ **`opts.before_close`** guards the dismiss. It is consulted on every
219
+ *user-driven* close — Escape, backdrop, the X / back-arrow, browser Back — and
220
+ returning **`false` vetoes** it, so the caller can run its own flow instead (the
221
+ canonical case is an unsaved-changes prompt that closes the modal itself once
222
+ confirmed). On a vetoed browser-Back the history entry is re-armed, so Back
223
+ keeps working afterwards. With no guard a modal closes exactly as it always
224
+ did, and the returned `close()` always closes **unconditionally** — the veto is
225
+ for the user's dismiss, not for the code's.
226
+
61
227
  ## Conventions
62
228
 
229
+ ### i18n: a string must be able to CHANGE language, not just be translated once
230
+
231
+ Passing a string through `t()` is **not** enough. `refresh_language()` only
232
+ re-translates a node that **carries its key**, so anything a view composed with
233
+ `t()` at render time stays in the old language for the rest of its life. Three
234
+ shapes, and the fix for each:
235
+
236
+ | Shape | Symptom | Fix |
237
+ |---|---|---|
238
+ | Text built with `t()` | never changes language | `i18n` / `data-i18n` on the element (`["span", {i18n: "rows"}, t("rows")]`) |
239
+ | 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.) |
240
+ | `title` / `aria-label` set with `t()` | tooltip stuck in the old language | `data-i18n-title` / `data-i18n-aria-label` |
241
+ | 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) |
242
+
243
+ **The contract.** The app owns the locales: it switches its i18next and calls
244
+
245
+ ```js
246
+ yui_shell_language_changed(shell); // c_yui_shell.js
247
+ ```
248
+
249
+ which re-translates the document and publishes **`EV_LANGUAGE_CHANGED`**. Any
250
+ view that builds DOM imperatively subscribes to its shell (`yui_shell_of(gobj)`)
251
+ and re-renders in the ACTION — a language change is an OS notification like any
252
+ other, so it crosses the FSM, never a raw `i18next.on("languageChanged")`.
253
+
254
+ **Tabulator** renders its own chrome (the paginator, the placeholder, the
255
+ loading/error notices) and it never went through i18n. Use:
256
+
257
+ ```js
258
+ new Tabulator($el, {...settings, ...yui_tabulator_lang(t)}); // at build
259
+ yui_tabulator_relocalize(table, t); // on the event
260
+ ```
261
+
262
+ Every key falls back to the English string Tabulator used to render
263
+ (`defaultValue`), so an app that defines none of them sees no change. Two traps
264
+ the implementation already handles: `setLocale()` with the locale name already
265
+ in force is a **no-op** (hence a fresh name per switch), and re-applying a locale
266
+ makes Tabulator re-run a title formatter on the EXISTING header cell, which
267
+ **appends** to it — rebuild the columns from their definitions.
268
+
269
+ **A missing key is invisible:** i18next answers an unknown key **with the key
270
+ itself**, so it renders (lower-case English) and simply never changes language.
271
+ A **duplicate** key in a locale file is silent too — an object literal keeps the
272
+ last one. Both are caught by the apps' `scripts/validate-locales.mjs`, which
273
+ also scans the gobj-ui modules the app mounts: **the library translates through
274
+ the APP's i18next**, so every key it asks for must be defined by the app.
275
+
276
+ ### Dates: never hand-roll them again
277
+
278
+ Every date UI in the projects had grown its own copy of the same two things —
279
+ "epoch → the local wall clock" and "what are the bounds of this week" — and the
280
+ copies disagreed (one rendered UTC, another local; one closed a range on the
281
+ next bucket's first instant, another on its last). Both now live here, and
282
+ nothing else should.
283
+
284
+ **`yui_time.js` — the pure half** (no DOM, no dependency):
285
+
286
+ - `epoch_to_local_input` / `local_input_to_epoch` / `fmt_epoch` / `epoch_to_ms`
287
+ / `ms_to_epoch` — every conversion crosses the producer's unit flag
288
+ (`ms`: seconds unless a topic's `system_flag` says milliseconds).
289
+ - `period_bounds` / `period_shift` / `period_start` / `period_label` /
290
+ `infer_period` / `is_current_period` — the algebra of **periods**.
291
+
292
+ A period is **`(unit, count)`**, not a name from a fixed list:
293
+
294
+ ```js
295
+ {id: "quarter", unit: "month", count: 3} // and semester is count 6,
296
+ {id: "bimester", unit: "month", count: 2} // bimester 2, decade year×10,
297
+ {id: "15min", unit: "minute", count: 15} // …
298
+ ```
299
+
300
+ so an app that reports by quarter DECLARES a quarter — it does not ask for a new
301
+ component. `YUI_PERIODS` is the catalog of the named ones; anything an app
302
+ invents labels itself by its own edges (`1 jul – 31 aug 2026`).
303
+
304
+ Three invariants worth knowing before touching it:
305
+
306
+ - **Buckets are aligned**, never counted back from now: months to the year (so
307
+ 2/3/4/6/12 fall on calendar boundaries), weeks to Monday (ISO), hours to local
308
+ midnight. A window that ends at `now` is a **rolling** window (`YUI_ROLLING`),
309
+ a different animal — it has no previous, and its upper end stays **open**.
310
+ - **The upper bound is inclusive** — the bucket's last millisecond, not the next
311
+ one's first. Both ends of a match condition are inclusive, and an exclusive end
312
+ handed to one silently swallows the record that landed on the boundary.
313
+ - **Stepping is calendar arithmetic**, never `+86400000`: a DST day is 23 or 25
314
+ hours long, and `31 jan + 1 month` is february, not "3 march".
315
+
316
+ **`C_YUI_PERIOD` — the UI half**: a granularity strip + `‹ label › >|` + a
317
+ calendar on the label (day / month / year grid, chosen by the granularity's own
318
+ unit). It publishes `EV_PERIOD_CHANGED {mode, anchor, from, to}` and mirrors
319
+ `from`/`to` in read-only attrs, in the consumer's unit, `0` = unbounded. Modes
320
+ that cannot be walked (`span`, `custom`, a rolling window) live in `ST_FLAT`, so
321
+ an arrow arriving there fails loudly. `with_custom` reveals a `$custom` slot the
322
+ HOST fills (its own from/to inputs): the component shows and hides it with the
323
+ mode, the host owns what is in it. Reference consumer: the Rows options of
324
+ `gui_treedb`'s `C_TRANGER_VIEW`; live demo in `test-app` (chapter **Period**).
325
+
326
+ The library asks the APP's i18next for its keys, so a consumer must define them
327
+ (`day`, `week`, `quarter`, `today`, `week {{n}}`, `quarter {{n}} {{y}}`,
328
+ `previous period`, …) — copy the block from `test-app/src/locales.js`, which is
329
+ the complete one: it is the only consumer that declares every mode, `rolling`
330
+ included (`last 24h`, `last 7 days`), and a missing key is **invisible** —
331
+ i18next answers it with the key itself.
332
+ The picker subscribes ITSELF to the shell's `EV_LANGUAGE_CHANGED` (its labels
333
+ are composed at render time), so a host has nothing to forward — a host that
334
+ forwards the event anyway just repaints it twice, harmlessly. All Intl
335
+ formatting (month names, weekday initials, the parked-bucket label) follows
336
+ i18next's ACTIVE language, not `navigator.language` — the calendar never mixes
337
+ scripts with the UI around it.
338
+
339
+ ### Inputs: a clear (✕) is the norm on free-text fields
340
+
341
+ Every editable free-text field carries a clear button — a big help on mobile,
342
+ and `C_YUI_FORM` wires it into its field factory automatically (text / password
343
+ / url / tel and the text-backed numerics; excluded: color, datetime-local,
344
+ readonly). Build a bespoke one-off clear and it will look different from every
345
+ other one, so use the helper:
346
+
347
+ ```js
348
+ import {attach_clear, refresh_clear} from "@yuneta/gobj-ui";
349
+
350
+ attach_clear($control, $input, on_clear); // Bulma .delete inside the control
351
+ ```
352
+
353
+ `attach_clear($control, $input, on_clear)` appends a Bulma `.delete` that is
354
+ visible only while the field has content, hides itself while the input is
355
+ `readonly`/`disabled`, dispatches a **synthetic `input` event** so existing
356
+ handlers re-run on their own (which is why a component rarely needs a dedicated
357
+ "cleared" event), then refocuses. Its tooltip carries `data-i18n-title` /
358
+ `data-i18n-aria-label`, so it re-translates on a language change.
359
+
360
+ `refresh_clear($input)` re-syncs the button's visibility after a change that
361
+ fires **no** `input` event — a value loaded into the form, or `readonly` toggled
362
+ by the form mode. No-op on an input that never got a clear.
363
+
63
364
  ### Logical class names on important DOM blocks
64
365
 
65
366
  When a gclass builds DOM, tag its elements so the tree is self-describing in
@@ -88,4 +389,32 @@ renames a logical class, and adding a logical class never requires a CSS rule.
88
389
  you can't tell it's "the comment line". These are primarily debug aids, but
89
390
  they **may** double as real CSS hooks; styling them is fine when useful.
90
391
 
392
+ #### Naming a window / modal from the app: `logical_class`
393
+
394
+ The library's own chrome carries its block names — a window is tagged
395
+ `WINDOW_HEADER` / `WINDOW_CONTROLS` / `WINDOW_MIN` / `WINDOW_MAX` /
396
+ `WINDOW_CLOSE` / `WINDOW_BODY` / `WINDOW_FOOTER` / `WINDOW_RESIZE` and its
397
+ default title bar `WINDOW_TITLE` / `WINDOW_TITLE_PREFIX` / `WINDOW_TITLE_KIND`,
398
+ a modal
399
+ `MODAL` / `MODAL_BACKDROP` / `MODAL_CONTENT` / `MODAL_HEADER` / `MODAL_BACK` /
400
+ `MODAL_TITLE` (+ `MODAL_TITLE_PREFIX` / `MODAL_TITLE_KIND`) / `MODAL_CLOSE` /
401
+ `MODAL_BODY`, a confirm `CONFIRM*` and a toast
402
+ `TOAST*`.
403
+
404
+ Those names identify the *kind* of block, not the *instance*: every window in
405
+ the app is a `C_YUI_WINDOW`, every popup is a `MODAL`. To target **one**
406
+ exactly, the caller passes its own name:
407
+
408
+ ```js
409
+ gobj_create_service("keys", "C_YUI_WINDOW",
410
+ {logical_class: "TRANGER_KEYS_WINDOW", ...}, gobj);
411
+
412
+ yui_shell_show_modal(shell, $box,
413
+ {logical_class: "TRANGER_KEYS_SHEET", dialog: true, ...});
414
+
415
+ yui_shell_confirm_yesno(shell, msg, {logical_class: "...", ...});
416
+ ```
417
+
418
+ It lands on the root element, alongside `C_YUI_WINDOW` / `MODAL` / `CONFIRM`.
419
+
91
420
  Copyright (c) 2024-2026, ArtGins. All Rights Reserved.