@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.
- package/README.md +335 -6
- package/dist/gobj-ui.cjs.js +10930 -5027
- package/dist/gobj-ui.es.js +10948 -5080
- package/index.js +45 -1
- package/package.json +11 -11
- package/src/c_g6_nodes_tree.js +128 -40
- package/src/c_yui_form.js +112 -53
- package/src/c_yui_gobj_tree_js.js +58 -36
- package/src/c_yui_json.css +139 -0
- package/src/c_yui_json.js +928 -0
- package/src/c_yui_json_graph.js +110 -20
- package/src/c_yui_map.js +19 -37
- package/src/c_yui_nav.js +6 -9
- package/src/c_yui_period.css +184 -0
- package/src/c_yui_period.js +1441 -0
- package/src/c_yui_shell.css +33 -0
- package/src/c_yui_shell.js +672 -111
- package/src/c_yui_treedb_graph.js +639 -37
- package/src/c_yui_treedb_schema.js +478 -0
- package/src/c_yui_treedb_topic_with_form.css +7 -42
- package/src/c_yui_treedb_topic_with_form.js +152 -103
- package/src/c_yui_treedb_topics.css +73 -0
- package/src/c_yui_treedb_topics.js +1070 -29
- package/src/c_yui_window.js +180 -97
- package/src/c_yui_window_manager.js +38 -4
- package/src/json_view_helpers.js +214 -0
- package/src/json_view_helpers.test.js +135 -0
- package/src/route_map_model.js +317 -0
- package/src/route_map_model.test.js +209 -0
- package/src/route_resolver.js +24 -1
- package/src/route_resolver.test.js +40 -1
- package/src/shell_modals.js +117 -39
- package/src/shell_route_map.css +225 -0
- package/src/shell_route_map.js +363 -0
- package/src/tabulator.css +67 -0
- package/src/yui_dev.js +130 -8
- package/src/yui_frontend_view.js +106 -0
- package/src/yui_icons.css +62 -0
- package/src/yui_inputs.css +8 -3
- package/src/yui_inputs.js +51 -8
- package/src/yui_tabulator_i18n.js +128 -0
- package/src/yui_theme.js +147 -0
- package/src/yui_time.js +666 -0
- package/src/yui_time.test.js +330 -0
- 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.
|
|
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**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.
|