@gramlot/gramlot-examples 0.2.10 → 0.2.12

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 (67) hide show
  1. package/README.md +4 -3
  2. package/package.json +3 -3
  3. package/src/gramlot_examples/catalog.json +42 -3
  4. package/src/gramlot_examples/gallery/dist/frame.js +1473 -5
  5. package/src/gramlot_examples/gallery/dist/gallery.js +3822 -17162
  6. package/src/gramlot_examples/gallery/dist/notices.json +2 -22
  7. package/src/gramlot_examples/gallery/dist/theme-styles.js +2 -0
  8. package/src/gramlot_examples/gallery/gallery-page.js +38 -67
  9. package/src/gramlot_examples/gallery/gallery.css +22 -94
  10. package/src/gramlot_examples/index.js +2 -1
  11. package/src/gramlot_examples/pages/README.md +87 -4
  12. package/src/gramlot_examples/pages/binding/01_pointers.js +9 -0
  13. package/src/gramlot_examples/pages/binding/01_pointers.md +56 -9
  14. package/src/gramlot_examples/pages/binding/02_variable_datapath.md +50 -4
  15. package/src/gramlot_examples/pages/binding/03_setters_and_defaults.md +45 -6
  16. package/src/gramlot_examples/pages/binding/04_native_editing.md +48 -6
  17. package/src/gramlot_examples/pages/binding/05_checkbox_radio.md +44 -4
  18. package/src/gramlot_examples/pages/binding/06_style_and_visibility.md +40 -6
  19. package/src/gramlot_examples/pages/binding/07_bound_svg.md +48 -3
  20. package/src/gramlot_examples/pages/binding/08_freeze.js +1 -1
  21. package/src/gramlot_examples/pages/binding/08_freeze.md +44 -6
  22. package/src/gramlot_examples/pages/binding/09_forms.js +2 -1
  23. package/src/gramlot_examples/pages/binding/09_forms.md +53 -7
  24. package/src/gramlot_examples/pages/binding/10_svg_shapes.md +50 -5
  25. package/src/gramlot_examples/pages/binding/11_complete_page.md +53 -7
  26. package/src/gramlot_examples/pages/binding/README.md +34 -0
  27. package/src/gramlot_examples/pages/controllers/01_formula.md +48 -7
  28. package/src/gramlot_examples/pages/controllers/02_controller.js +24 -2
  29. package/src/gramlot_examples/pages/controllers/02_controller.md +61 -6
  30. package/src/gramlot_examples/pages/controllers/03_named_logic.md +63 -7
  31. package/src/gramlot_examples/pages/controllers/04_inline_expressions.md +50 -7
  32. package/src/gramlot_examples/pages/controllers/05_node_methods.md +41 -7
  33. package/src/gramlot_examples/pages/controllers/06_button_controller.md +49 -6
  34. package/src/gramlot_examples/pages/controllers/07_events.md +45 -6
  35. package/src/gramlot_examples/pages/controllers/08_end_to_end.js +26 -1
  36. package/src/gramlot_examples/pages/controllers/08_end_to_end.md +138 -9
  37. package/src/gramlot_examples/pages/controllers/09_endpoint.md +47 -7
  38. package/src/gramlot_examples/pages/controllers/10_call_on_change.md +48 -8
  39. package/src/gramlot_examples/pages/controllers/11_call_on_click.js +6 -0
  40. package/src/gramlot_examples/pages/controllers/11_call_on_click.md +58 -6
  41. package/src/gramlot_examples/pages/controllers/12_remote.js +33 -0
  42. package/src/gramlot_examples/pages/controllers/12_remote.md +60 -0
  43. package/src/gramlot_examples/pages/controllers/13_cached_remote.js +44 -0
  44. package/src/gramlot_examples/pages/controllers/13_cached_remote.md +67 -0
  45. package/src/gramlot_examples/pages/controllers/14_data_remote.js +39 -0
  46. package/src/gramlot_examples/pages/controllers/14_data_remote.md +69 -0
  47. package/src/gramlot_examples/pages/controllers/15_lifecycle.js +51 -0
  48. package/src/gramlot_examples/pages/controllers/15_lifecycle.md +59 -0
  49. package/src/gramlot_examples/pages/controllers/16_attribute_projection.js +25 -0
  50. package/src/gramlot_examples/pages/controllers/16_attribute_projection.md +58 -0
  51. package/src/gramlot_examples/pages/controllers/17_data_exchange.js +51 -0
  52. package/src/gramlot_examples/pages/controllers/17_data_exchange.md +61 -0
  53. package/src/gramlot_examples/pages/controllers/README.md +57 -3
  54. package/src/gramlot_examples/pages/html_svg/01_hello_world.md +48 -4
  55. package/src/gramlot_examples/pages/html_svg/02_text_and_links.md +42 -4
  56. package/src/gramlot_examples/pages/html_svg/03_lists.md +45 -4
  57. package/src/gramlot_examples/pages/html_svg/04_semantic_page.md +44 -4
  58. package/src/gramlot_examples/pages/html_svg/05_tables.md +46 -4
  59. package/src/gramlot_examples/pages/html_svg/06_forms.md +48 -4
  60. package/src/gramlot_examples/pages/html_svg/07_native_disclosure.md +41 -4
  61. package/src/gramlot_examples/pages/html_svg/08_svg_shapes.md +45 -4
  62. package/src/gramlot_examples/pages/html_svg/09_svg_composition.md +44 -4
  63. package/src/gramlot_examples/pages/html_svg/10_cards_with_icons.md +42 -15
  64. package/src/gramlot_examples/pages/html_svg/11_static_report.md +37 -4
  65. package/src/gramlot_examples/pages/html_svg/12_complete_page.md +41 -4
  66. package/src/gramlot_examples/pages/html_svg/13_live_source.md +47 -16
  67. package/src/gramlot_examples/pages/html_svg/README.md +36 -0
@@ -1,12 +1,58 @@
1
1
  # 11 · A complete page with binding
2
2
 
3
- The event page of HTML / SVG example 12, with its visible state in the Data record `event`.
3
+ The event page from **A complete page** now follows shared Data. Change one value and see it update in several places. You learn how to make an existing page interactive while retaining its structure.
4
4
 
5
- - The heading, the hero caption and the footer read `^.title` and `^.footer`; the field in the header edits the title while you type.
6
- - The two checkboxes write `show.mira` and `show.theo`. Schedule rows and speaker cards bind `visible` to them; the joint session has no pointer and stays.
7
- - The radio buttons above the map own `stop.entrance`, `stop.studio` and `stop.hall`; each map stop shows its ring through `visible`.
8
- - The FAQ `details` bind `open` to one checkbox.
5
+ **Uses:** Source, Data, pointers and native binding — core 0.2.0. No endpoint calls.
9
6
 
10
- The main landmark is written `html_main`, held in the variable `main_content`, because the page also has a header and a footer.
7
+ <details>
8
+ <summary>More detail</summary>
11
9
 
12
- **Try it:** Hide Theo, move to the hall, then open all answers.
10
+
11
+ **Release stage:** Data binding and browser behavior (core 0.2.0).
12
+
13
+ **Goal:** Revisit the event page with shared Data driving several destinations.
14
+
15
+ **Before this lesson:** [e12 · Complete page](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/html_svg/12_complete_page.md), [b05 · Checkbox and radio](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/05_checkbox_radio.md), [b06 · Style and visibility](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/06_style_and_visibility.md), [b10 · SVG shapes with binding](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/10_svg_shapes.md). These prerequisites support the release sequence; later comparisons are explicitly labelled below.
16
+
17
+ **Open and edit:** select **b11 · Complete page with binding** in your environment’s gallery. For source changes, copy/edit the matching page in your own project using the [setup guide](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md); the gallery is a source viewer. Save, then reload/reopen as that guide explains. The snippets below are excerpts/edits within that Page, not standalone launch commands.
18
+
19
+ ## Read the page
20
+
21
+ The `event` Data record drives the title, speaker visibility, map selection and FAQ. `html_main`, stored in `main_content`, is the HTML landmark.
22
+
23
+ ```python
24
+ header.h1("^.title", id="title")
25
+ header.input(value="^.title", live=True, id="title-field")
26
+ # In the hero:
27
+ hero.p("^.title")
28
+ ```
29
+
30
+ One speaker flag controls the schedule row and speaker card. The joint session has no visibility pointer and stays visible. Radio booleans control the map rings; both FAQ disclosures bind `open` to one flag.
31
+
32
+ Python builds Source on the server; its JavaScript counterpart builds it on Node/Bun or in a standalone Worker. Bindings, actions and any Logic execute in the browser; Python strings containing JavaScript are not executed by Python.
33
+
34
+ ## Try it and predict the result
35
+
36
+ 1. Edit Event title: heading and hero text change together.
37
+ 2. Uncheck Sessions of Theo: his solo row and card become invisible while retaining space; the joint session remains.
38
+ 3. Choose Hall: its ring appears and Studio’s disappears. Check Open all answers: both FAQ answers open.
39
+
40
+ ## Change it and check yourself
41
+
42
+ Change the initial `.title` setter and start with `.show.theo` false in the dictionary.
43
+
44
+ **Answer:** both title destinations get the new value and both Theo destinations start invisible. You change initial Data once for each concept, not every affected Source node.
45
+
46
+ ## The same example across stages
47
+
48
+ **Earlier e12:** schedule, cards and FAQ are static, and the map has no selection.
49
+
50
+ **Later 0.2.0:** `visible="^.show.theo"` can be reused for both Theo destinations; `open="^.faq_open"` controls both answers. A set of declarative pointers replaces separate per-destination update routines. The title is deliberately reused in the hero, whose earlier static description differed. The page also omits the earlier Highlights section: this is a teaching variation, not a pixel-identical rewrite.
51
+
52
+ ## Limits and next step
53
+
54
+ The map has no routing service. Manually toggling a native disclosure is not declared as a write-back to `.faq_open`; the shared checkbox is the state-control demonstration. No server save or booking is performed.
55
+
56
+ Continue with [c01 · Formula](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/01_formula.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) explains the full contract.
57
+
58
+ </details>
@@ -1,5 +1,17 @@
1
1
  # Binding
2
2
 
3
+ Make fields, text and drawings share the same values. Edit a value once and let every bound view follow it.
4
+
5
+ **This section uses Source, Data Bags, pointers, native input binding and initialization with setters/defaults — core 0.2.0.** It makes no endpoint calls and loads no remote content.
6
+
7
+ Start with **Pointers**. The final three pages revisit **Native forms**, **SVG shapes** and **A complete page**: binding replaces code that would copy values into each view.
8
+
9
+ **Later follow-up:** **Freeze** also uses local button controllers and named Logic. Return after **Named logic** and **Button controller**. Its Page/Logic module layout is from 0.2.5.
10
+
11
+ <details>
12
+ <summary>More detail</summary>
13
+
14
+
3
15
  The pages of this family keep their content in the Data and let the DOM follow it.
4
16
 
5
17
  A `^` pointer in a text or an attribute reads a Data path and keeps the element up to date. A native control whose `value` is a `^` pointer also writes the Data when you edit it. `dataSetter` puts the initial values in the Data before the first render.
@@ -7,3 +19,25 @@ A `^` pointer in a text or an attribute reads a Data path and keeps the element
7
19
  Start with **Pointers**, then follow the order of the list: a context that changes, initial values and defaults, editing, booleans, style and SVG, freeze. The last three pages take the content of HTML / SVG examples 06, 08 and 12 and add the binding, so the same page shows the difference.
8
20
 
9
21
  Each example has a Python page and its JavaScript equivalent. Both run on the same packaged runtime.
22
+
23
+ ## Prerequisites within the release sequence
24
+
25
+ The family belongs to the 0.2.0 binding stage. Use b01's pointers to read b02–b04;
26
+ separate the first trial from the advanced symbolic-path exercises. b05/b06/b09
27
+ cover forms and layout; b07/b10 follow e08's SVG introduction. b08 uses named
28
+ Logic and nested button controllers: read c03/c06 and e10 first. Freeze suspends
29
+ structural rendering, not existing Data reactions; removed nodes stop handling
30
+ events immediately. See the [release-by-release route](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md#gc-025-015). Prerequisites support the
31
+ chronological sequence; they do not replace it. The maintained files may use
32
+ later APIs, so they are not snapshots of the earliest release.
33
+
34
+ ## How to work through a lesson
35
+
36
+ Read its core release stage first, then its goal and prerequisites. Open the
37
+ existing preview, predict the guided result, and only then try the source-change
38
+ exercise in your own editable page. Each exercise includes an answer. Follow
39
+ "The same example across stages" links to compare earlier/later declarations;
40
+ keep both stages visible. A newer interactive page can contain more total code
41
+ while requiring less explicit state synchronization or request coordination.
42
+
43
+ </details>
@@ -1,12 +1,53 @@
1
1
  # 01 · Formula
2
2
 
3
- `dataFormula(result_path, formula, **parameters)` computes an expression and writes the result at `result_path`. The names of the parameters are the names inside the expression.
3
+ A formula calculates a result from the fields. You learn which inputs trigger a calculation and which are read only when it runs: `^` follows changes, while `=` supplies the current value.
4
4
 
5
- - A `^` parameter triggers the formula when its Data changes: editing the price or the quantity recomputes the total.
6
- - A `=` parameter is read at each run but does not trigger it. The gross uses `vat='=.vat'`: changing the rate alone does not recompute it; the next change of the total does.
7
- - `_if` is a condition: when it is false the formula writes the `_else` expression instead.
8
- - `_init=True` runs each formula once when the page starts, after the setters.
5
+ **Uses:** Data binding and formulas — core 0.2.0.
9
6
 
10
- A formula result is itself Data: the gross and the size are formulas of the total.
7
+ <details>
8
+ <summary>More detail</summary>
11
9
 
12
- **Try it:** Change the VAT rate, then the quantity, and watch when the gross follows.
10
+
11
+ **Release stage:** Data binding and browser behavior (core 0.2.0).
12
+
13
+ **Goal:** Separate calculation dependencies from values read only at execution.
14
+
15
+ **Before this lesson:** [b01 · Pointers](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/01_pointers.md), [b03 · Setters and defaults](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/03_setters_and_defaults.md), [b04 · Native editing](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/04_native_editing.md). These prerequisites support the release sequence; later comparisons are explicitly labelled below.
16
+
17
+ **Open and edit:** select **c01 · Formula** in your environment’s gallery. For source changes, copy/edit the matching page in your own project using the [setup guide](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md); the gallery is a source viewer. Save, then reload/reopen as that guide explains. The snippets below are excerpts/edits within that Page, not standalone launch commands. This lesson executes authored inline JavaScript and needs the gallery’s permissive CSP profile; a strict named-Logic starter needs the corresponding named form.
18
+
19
+ ## Read the page
20
+
21
+ The first `dataFormula` computes price × quantity into `.total`; gross depends on total but only **reads** VAT. The formula for size uses `_if`/`_else`. `_init=True` evaluates after setters.
22
+
23
+ ```python
24
+ pane.dataFormula(".total", "price * quantity", price="^.price", quantity="^.quantity", _init=True)
25
+ ```
26
+
27
+ A formula returns its result; the provider writes it. The JavaScript expression runs in the browser, even inside a Python string. The result path is bare, not `^.total`.
28
+
29
+ Python builds Source on the server; its JavaScript counterpart builds it on Node/Bun or in a standalone Worker. Bindings, actions and any Logic execute in the browser; Python strings containing JavaScript are not executed by Python.
30
+
31
+ ## Try it and predict the result
32
+
33
+ 1. Initially price 12.5 × quantity 4 gives total 50, gross 60 and “small order”.
34
+ 2. Change VAT from 0.2 to 0.1: gross remains 60.
35
+ 3. Change quantity to 8: total becomes 100 and gross 110; size stays “small order” because the condition is **greater than** 100.
36
+
37
+ ## Change it and check yourself
38
+
39
+ Change `vat="=.vat"` to `vat="^.vat"`, reload and change only VAT to 0.1.
40
+
41
+ **Answer:** gross now becomes 55 immediately. VAT became a trigger. The formula body is unchanged; one declaration changes dependency tracking.
42
+
43
+ ## The same example across stages
44
+
45
+ Earlier e05 computed totals once while building Source. In 0.2.0, `dataFormula` declares what to recompute and which paths trigger it. Compared with a controller that explicitly calls `this.SET('.total', price * quantity)`, the formula body only returns `price * quantity`; the destination declaration owns the write. These are two forms within the same release, not a fabricated later release.
46
+
47
+ ## Limits and next step
48
+
49
+ A formula is for a derived value; use a controller for effects. VAT here is a fraction (0.2), while c09 later uses a percentage (22); do not copy the number unchanged.
50
+
51
+ Continue with [c02 · Controller](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/02_controller.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) explains the full contract.
52
+
53
+ </details>
@@ -37,11 +37,33 @@ export class Page extends BasePage {
37
37
  timed.dataController({script: "this.SET('.ticks', (ticks ?? 0) + 1)", ticks: '=.ticks', _timing: '^.interval',
38
38
  _onStart: true});
39
39
 
40
+ const filtered = pane.section({class: 'card stack'});
41
+ filtered.h2('Advanced: _userChanges filters paths, not input origin');
42
+ filtered.html_label('Tracked name', {for: 'tracked-name'});
43
+ filtered.input({value: '^.filter.name', id: 'tracked-name'});
44
+ for (const [label, method] of [['SET the name', 'setName'], ['Replace containing Bag', 'replaceFilter'],
45
+ ['Change address child', 'changeCity']]) {
46
+ filtered.button(label, {id: method, type: 'button'}).dataController({func: method});
47
+ }
48
+ for (const [key, path, exact] of [['name_exact', '.filter.name', true], ['name_all', '.filter.name', false],
49
+ ['address_exact', '.filter.address', true], ['address_all', '.filter.address', false]]) {
50
+ filtered.p(`^.${key}`, {mask: `${key}: %s`, id: key});
51
+ filtered.dataController({func: 'countPath', watched: `^${path}`, counter: `.${key}`, _userChanges: exact});
52
+ pane.dataSetter({destination_path: `.${key}`, value: 0});
53
+ }
54
+ pane.dataSetter({destination_path: '.filter', value: {name: 'Ada', address: {city: 'Rome'}}});
55
+ pane.dataSetter({destination_path: '.replacement', value: {name: 'Container', address: {city: 'Paris'}}});
56
+
40
57
  pane.dataSetter({destination_path: '.name', value: 'Ada'});
41
58
  pane.dataSetter({destination_path: '.requests', value: 0});
42
59
  pane.dataSetter({destination_path: '.interval', value: '1'});
43
60
  }
44
61
  }
45
62
 
46
- /** The page logic: this page names no methods. */
47
- export class Logic {}
63
+ /** Named methods for the optional path-filtering exercise. */
64
+ export class Logic {
65
+ countPath(node, {counter}) { node.SET(counter, node.GET(counter) + 1); }
66
+ setName(node) { node.SET('.filter.name', 'Grace'); }
67
+ replaceFilter(node) { node.SET('.filter', node.GET('.replacement').deepcopy()); }
68
+ changeCity(node) { node.SET('.filter.address.city', 'Milan'); }
69
+ }
@@ -1,10 +1,65 @@
1
1
  # 02 · Controller
2
2
 
3
- `dataController(script, **parameters)` runs code for its effects. Inside the script `this` is the controller's Source node: `this.SET('.path', value)` writes the Data relative to its context.
3
+ A controller reacts to Data changes and writes other values. You learn when to use a controller for an action rather than a formula for a result. Its path filter distinguishes where a change occurred, not who made it.
4
4
 
5
- - **React to a value.** `name='^.name'` triggers the controller at every change; it writes `.invalid`, and the warning binds `visible` to it. `_init=True` runs it once at the start.
6
- - **React to a fired path.** The button's `fire='.request'` fires the path at every click, also with an equal value. The controller counts the requests; `requests='=.requests'` is read without triggering.
7
- - **Wait for a pause.** `_delay=400` runs the controller 400 ms after the last change, once.
8
- - **Run at start and at intervals.** `_onStart=True` runs the controller when the page has started; `_timing='^.interval'` repeats it every *n* seconds, and 0 stops it.
5
+ **Uses:** Data binding and local controllers — core 0.2.0.
9
6
 
10
- **Try it:** Clear the name, type a search quickly, then change the interval to 5.
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+
11
+ **Release stage:** Data binding and browser behavior (core 0.2.0).
12
+
13
+ **Goal:** Choose a controller for an effect and identify its trigger.
14
+
15
+ **Before this lesson:** [c01 · Formula](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/01_formula.md). These prerequisites support the release sequence; later comparisons are explicitly labelled below.
16
+
17
+ **Open and edit:** select **c02 · Controller** in your environment’s gallery. For source changes, copy/edit the matching page in your own project using the [setup guide](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md); the gallery is a source viewer. Save, then reload/reopen as that guide explains. The snippets below are excerpts/edits within that Page, not standalone launch commands. This lesson executes authored inline JavaScript and needs the gallery’s permissive CSP profile; a strict named-Logic starter needs the corresponding named form.
18
+
19
+ ## Read the page
20
+
21
+ A `dataController` runs code for effects. In inline code, `this` is its Source node; `this.SET` writes relative Data. The first controller writes `.invalid`, whose pointer controls the warning.
22
+
23
+ ```python
24
+ check.dataController("this.SET('.invalid', !name.trim())", name="^.name", _init=True)
25
+ ```
26
+
27
+ The request button fires a Data path, not an HTTP request. The search controller waits 400 ms after the last edit. The clock runs at `_onStart` and repeats every `_timing` seconds; 0 disables repetition.
28
+
29
+ Python builds Source on the server; its JavaScript counterpart builds it on Node/Bun or in a standalone Worker. Bindings, actions and any Logic execute in the browser; Python strings containing JavaScript are not executed by Python.
30
+
31
+ ## Try it and predict the result
32
+
33
+ 1. Clear Name: the warning appears; type Ada: it disappears.
34
+ 2. Press Send a request twice: the local counter becomes 2, without a server call.
35
+ 3. Type a search quickly and pause: “Searching for …” appears after the debounce. Set Interval to 0: subsequent periodic ticks stop.
36
+
37
+ ## Change it and check yourself
38
+
39
+ Change `_delay=400` to `_delay=800`, reload and type another query.
40
+
41
+ **Answer:** the last text appears after about 800 ms of quiet, once; no real search service is contacted. Timing is approximate browser scheduling, not a precision clock.
42
+
43
+ ## The same example across stages
44
+
45
+ Compare e13’s `setInterval` plus explicit disposal with this page’s `_timing` on a controller. For a repeated controller effect, the provider owns timer cleanup, so the page no longer authors a timer handle and `clearInterval` pair. Keep e13’s Source animation as its own earlier/advanced example; no animation behavior has been silently replaced.
46
+
47
+
48
+ ## Advanced: filter the changed path (0.2.0)
49
+
50
+ The optional section uses named Logic from `02_controller.js`. Two controllers watch `.filter.name`, and two watch the Bag `.filter.address`; each pair differs only in `_userChanges`. Start from a reload: all counters are zero.
51
+
52
+ 1. Edit Tracked name, then leave the field: name_exact/name_all become 1/1.
53
+ 2. Press SET the name: both become 2/2. Programmatic writes to the exact path qualify too.
54
+ 3. Replace containing Bag: name_all becomes 3 and address_all becomes 1, while the exact counters stay 2 and 0.
55
+ 4. Change address child: address_all becomes 2; address_exact stays 0 because `.filter.address.city` lies below the watched `.filter.address`.
56
+
57
+ Exercise: change `_userChanges=True` to False on an exact watcher, reload and repeat. **Answer:** that watcher now accepts container/child events as its paired unfiltered watcher does. The flag is not a human-versus-code detector. The Data Bag copy used for replacement is still ordinary Data, not a parallel state store.
58
+
59
+ ## Limits and next step
60
+
61
+ `requests` in this page counts fired local events. The advanced `_userChanges` counters are separate from `requests`: they filter exact-path changes, including programmatic writes, not human versus code identity.
62
+
63
+ Continue with [c03 · Named logic](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/03_named_logic.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) explains the full contract.
64
+
65
+ </details>
@@ -1,12 +1,68 @@
1
1
  # 03 · Named logic
2
2
 
3
- The code of this page lives in `class Logic`, which `03_named_logic.js` exports beside `Page`. The core GramlotFileServer serves that module as the page logic for the JavaScript page and for the Python page `03_named_logic.py` alike.
3
+ A price calculation and a change counter call methods in a `Logic` class. You learn how to reuse named browser behavior instead of repeating inline code. In a Python gallery, open **logic.js** to see those methods.
4
4
 
5
- - `func='finalPrice'` names a method of `Logic`. A formula method receives one object with the resolved parameters and returns the result: 10% off from the `threshold`.
6
- - A controller method receives its Source node and the parameters. `node.SET('.changes', …)` writes relative to the node's context.
7
- - `this` is the logic group: `this.changes` keeps a count between calls, and `this.page` is the Gramlot page.
8
- - The parameters also carry `_reason`, `_node` and `_triggerpars`.
5
+ **Uses:** Local formulas, controllers and named Logic (0.2.0); shared Page/Logic module layout from 0.2.5.
9
6
 
10
- Named logic compiles nothing at run time, so it also works under a Content Security Policy without `'unsafe-eval'`.
7
+ <details>
8
+ <summary>More detail</summary>
11
9
 
12
- **Try it:** Raise the price above 100, then add a second formula that names `finalPrice` with another threshold.
10
+
11
+ **Release stage:** Data binding and browser behavior (core 0.2.0). The maintained Page/Logic module arrangement is from 0.2.5; it does not move the original behavior into a later release.
12
+
13
+ **Goal:** Reuse browser behavior by naming a Logic method.
14
+
15
+ **Before this lesson:** [c01 · Formula](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/01_formula.md), [c02 · Controller](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/02_controller.md). These prerequisites support the release sequence; later comparisons are explicitly labelled below.
16
+
17
+ **Open and edit:** select **c03 · Named logic** in your environment’s gallery. For source changes, copy/edit the matching page in your own project using the [setup guide](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md); the gallery is a source viewer. Save, then reload/reopen as that guide explains. The snippets below are excerpts/edits within that Page, not standalone launch commands.
18
+
19
+ ## Read the page
20
+
21
+ The Python Page declares `func="finalPrice"`; `03_named_logic.js` exports the browser `Logic` used by both languages. A formula method takes resolved parameters and returns a value. A controller method receives `(node, kwargs)` and writes Data with `node.SET`.
22
+
23
+ ```python
24
+ pane.dataFormula(".final", func="finalPrice", price="^.price", threshold=100, _init=True)
25
+ ```
26
+
27
+ `finalPrice` applies 10% off at/above the threshold. In Logic, `this` is the logic group, `this.page` is the browser Gramlot instance, and `this.changes` is local browser state. It is not a server Page field.
28
+
29
+ Python builds Source on the server; its JavaScript counterpart builds it on Node/Bun or in a standalone Worker. Bindings, actions and any Logic execute in the browser; Python strings containing JavaScript are not executed by Python. Read the gallery’s **logic.js** tab (the Logic class from `03_named_logic.js`) beside the Python source.
30
+
31
+ ## Try it and predict the result
32
+
33
+ 1. Initial price 80 produces final 80; the change controller has not yet counted an edit.
34
+ 2. Replace Price with 120: final becomes 108. The change count advances for each emitted input edit; `_reason` explains the trigger.
35
+
36
+ ## Change it and check yourself
37
+
38
+ Add a second formula using `func="finalPrice"`, another result path `.other_final`, and `threshold=200`; add a paragraph bound to `.other_final`.
39
+
40
+ **Answer:** at price 120, the original formula gives 108 and the second gives 120. The same method is reused; only the declarations and threshold differ.
41
+
42
+ ## The same example across stages
43
+
44
+ **0.2.0 behavior:** move reusable calculations/effects from inline strings to named methods; this removes repeated formula bodies when multiple declarations share one method.
45
+
46
+ **0.2.5 arrangement:** the same exported `Logic` can live beside `Page` in the page module; Python takes it from its same-name `.js` file. A separate `*_aux.js` is then unnecessary for this page. Do not claim the method body disappeared or that named Logic itself first arrived in 0.2.5.
47
+
48
+ ## Resource exercise: keep styling and Logic with the page
49
+
50
+ `Page.css` already selects the shared theme. In your editable project, create
51
+ `03_named_logic.css` beside the two page modules and put `h1 { color: teal; }` in it.
52
+ Reload the page and inspect its stylesheet requests. **Answer:** the matching
53
+ stylesheet is served as a companion and the heading changes without adding a
54
+ resource group or browser style-writing code. The Python page already obtains
55
+ its root Logic from `03_named_logic.js`. Remove the temporary style afterwards.
56
+
57
+ This common path covers Page.css, the same-name stylesheet and root Logic.
58
+ `css_requires`/`js_requires` and dotted named native-event handlers need the
59
+ separate genro-kajenn resource host; FileServer and current adapters do not supply
60
+ that host. Do not simulate it by loading scripts in the example.
61
+
62
+ ## Limits and next step
63
+
64
+ Named Logic works without runtime inline compilation. A root `func` method does not become a named `connect_on` group: dotted event names need a resource group unavailable in the common FileServer gallery. Server-only imports cannot be placed in a module that the browser imports for Logic.
65
+
66
+ Continue with [c04 · Inline expressions](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/04_inline_expressions.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) explains the full contract.
67
+
68
+ </details>
@@ -1,12 +1,55 @@
1
1
  # 04 · Inline expressions
2
2
 
3
- A text or an attribute that starts with `==` is a JavaScript expression. Its names are the other attributes of the same node, which are often `^` pointers: the expression is computed again when one of them changes.
3
+ Small expressions calculate what an element displays directly from its inputs. You learn how to format a local result without adding another calculated Data field.
4
4
 
5
- - The heading is `==first + ' ' + last`, with `first='^.first'` and `last='^.last'`.
6
- - The verdict computes its text, its `class` and its `title` from `score`.
7
- - The SVG bar computes its `width`; the button computes `disabled` from the checkbox.
8
- - The button's `action` is an inline script run at the click, with `this` = the button node.
5
+ **Uses:** Data binding and inline expressions — core 0.2.0.
9
6
 
10
- Inline code needs a Content Security Policy that allows `'unsafe-eval'`; under a strict policy it stops with a clear error naming the node and the attribute. The legacy macros `SET path = value` and `$1` still work and log a deprecation warning: write `this.SET(path, value)` and `arguments[0]`.
7
+ <details>
8
+ <summary>More detail</summary>
11
9
 
12
- **Try it:** Move the score across 50, accept the rules, then press Start.
10
+
11
+ **Release stage:** Data binding and browser behavior (core 0.2.0).
12
+
13
+ **Goal:** Compute a local display value without storing another derived Data field.
14
+
15
+ **Before this lesson:** [c01 · Formula](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/01_formula.md), [c03 · Named logic](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/03_named_logic.md). These prerequisites support the release sequence; later comparisons are explicitly labelled below.
16
+
17
+ **Open and edit:** select **c04 · Inline expressions** in your environment’s gallery. For source changes, copy/edit the matching page in your own project using the [setup guide](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md); the gallery is a source viewer. Save, then reload/reopen as that guide explains. The snippets below are excerpts/edits within that Page, not standalone launch commands. This lesson executes authored inline JavaScript and needs the gallery’s permissive CSP profile; a strict named-Logic starter needs the corresponding named form.
18
+
19
+ ## Read the page
20
+
21
+ `==` starts a browser expression. Its names come from other attributes on that same Source node: `first="^.first"`, `last="^.last"`, or `score="^.score"`.
22
+
23
+ ```python
24
+ result.h2("==first + ' ' + last", first="^.first", last="^.last", id="full-name")
25
+ ```
26
+
27
+ The verdict uses score for text, class and title; the SVG bar uses score × 3 for width. The button’s `action` sets `.started` after `disabled` becomes false.
28
+
29
+ Python builds Source on the server; its JavaScript counterpart builds it on Node/Bun or in a standalone Worker. Bindings, actions and any Logic execute in the browser; Python strings containing JavaScript are not executed by Python.
30
+
31
+ ## Try it and predict the result
32
+
33
+ 1. Expect Ada Lovelace, score 40, “Not yet”, bar width 120 and Start disabled.
34
+ 2. Move score to 60: verdict becomes Passed and width becomes 180.
35
+ 3. Accept the rules, then Start: the Started text appears.
36
+
37
+ ## Change it and check yourself
38
+
39
+ Predict the result at exactly score 50, then try it.
40
+
41
+ **Answer:** Passed and width 150, because the condition uses `>= 50`. Changing it to `> 50` changes that boundary, not the binding mechanism.
42
+
43
+ ## The same example across stages
44
+
45
+ For a value used only by one display, compare a `.full_name` dataFormula plus a `^.full_name` heading with the `==` heading above: it removes the extra destination and provider. Both are 0.2.0 choices. Keep a named/shared formula when the result is needed elsewhere; fewer lines is not the only criterion.
46
+
47
+ ## Limits and next step
48
+
49
+ Inline expressions/scripts require the permitted CSP profile and are not code loaded from Data. Do not teach deprecated SET macros. An expression cannot use another `==` attribute as a computed dependency; use a Data formula when several nodes need one result.
50
+
51
+ For attribute templates, compare [c16 · Attribute projection](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/16_attribute_projection.md): `${name}` consumes a declared sibling parameter; it does not permit chains of `==` attributes.
52
+
53
+ Continue with [c05 · Node methods](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/05_node_methods.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) explains the full contract.
54
+
55
+ </details>
@@ -1,11 +1,45 @@
1
1
  # 05 · Node methods
2
2
 
3
- Every Source node offers five methods for the Data. A path starting with `.` is relative to the node's context; here each button's `action` calls them with `this` = the button node.
3
+ Buttons read and change Data in different ways. You learn the difference between notifying writes, quiet writes, reads and fired events.
4
4
 
5
- - `this.SET(path, value)` writes and notifies: the value on screen changes and the observer counts one change.
6
- - `this.GET(path)` reads. **GET value** copies the current value into `.read`.
7
- - `this.PUT(path, value)` writes silently: nothing is notified, so the value on screen keeps the old number until the next SET. **GET value** shows the real one.
8
- - `this.FIRE(path, value)` writes the value and resets the path silently afterwards, so every FIRE is delivered, also with an equal value.
9
- - `this.FIRE_AFTER(path, value, delay)` fires after `delay` ms. The timer belongs to the node: removing the node cancels it.
5
+ **Uses:** Source, Data and local node methods — core 0.2.0.
10
6
 
11
- **Try it:** Press PUT, then GET value, then SET, and compare the three numbers.
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+
11
+ **Release stage:** Data binding and browser behavior (core 0.2.0).
12
+
13
+ **Goal:** Distinguish notifying writes, quiet writes, reads and fired events.
14
+
15
+ **Before this lesson:** [b01 · Pointers](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/01_pointers.md), [c02 · Controller](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/02_controller.md), [c04 · Inline expressions](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/04_inline_expressions.md). These prerequisites support the release sequence; later comparisons are explicitly labelled below.
16
+
17
+ **Open and edit:** select **c05 · Node methods** in your environment’s gallery. For source changes, copy/edit the matching page in your own project using the [setup guide](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md); the gallery is a source viewer. Save, then reload/reopen as that guide explains. The snippets below are excerpts/edits within that Page, not standalone launch commands. This lesson executes authored inline JavaScript and needs the gallery’s permissive CSP profile; a strict named-Logic starter needs the corresponding named form.
18
+
19
+ ## Read the page
20
+
21
+ The actions call methods of their Source node: `SET` writes and notifies, `GET` reads, `PUT` writes quietly, `FIRE` notifies even for the same fired value and silently resets afterwards. `FIRE_AFTER` schedules a fire in milliseconds and belongs to the node’s lifetime.
22
+
23
+ The notification counter is an observer of `.value`; `.read` is only a displayed snapshot made by GET value.
24
+
25
+ Python builds Source on the server; its JavaScript counterpart builds it on Node/Bun or in a standalone Worker. Bindings, actions and any Logic execute in the browser; Python strings containing JavaScript are not executed by Python.
26
+
27
+ ## Try it and predict the result
28
+
29
+ 1. Start with value 0 and notification count 0. Click PUT: the displayed value stays 0.
30
+ 2. Click GET value: its snapshot shows 10. Click SET: it reads 10, writes 11 and the displayed value becomes 11; notifications become 1.
31
+ 3. Fire “now” twice: pings advance twice. FIRE_AFTER reports “later” after about one second.
32
+
33
+ ## Change it and check yourself
34
+
35
+ Reload and click PUT twice, then GET value, then SET.
36
+
37
+ **Answer:** snapshot 20, then displayed value 21 and one notification. Quiet writes change Data even though subscribed displays have not been notified.
38
+
39
+ ## Limits and next step
40
+
41
+ PUT is not a second state store and does not mean the write failed. Do not use it when other views must update immediately. A delayed fire is cancelled when its owning node is removed.
42
+
43
+ Continue with [c06 · Button controller](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/06_button_controller.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) explains the full contract.
44
+
45
+ </details>
@@ -1,11 +1,54 @@
1
1
  # 06 · Button controller
2
2
 
3
- A button runs one click mechanism: a nested `dataController`, an `action`, or the `fire` family. Declaring two of them on one button is an error.
3
+ A controller inside a button runs when you click it. You learn how the button supplies click parameters, including modifier keys, to the action.
4
4
 
5
- - **Nested controller.** The `dataController` inside the button runs once per click. Its code reads `button_counter`, which counts the clicks of this button and survives a rebuild, and `button_shift`, `button_ctrl`, `button_alt`, `button_meta`; `_evt` is the DOM event.
6
- - **fire.** `fire='.saved'` fires the path at each click. The value is the modifier string, such as `Shift`, or `true` without modifiers.
7
- - **fire_<name>.** `fire_save` and `fire_close` fire their paths with the values `save` and `close`, in attribute order: the log gets two entries per click.
5
+ **Uses:** Data binding and local button controllers — core 0.2.0.
8
6
 
9
- A button with a mechanism gets `type="button"`, so it never submits a form by itself.
7
+ <details>
8
+ <summary>More detail</summary>
10
9
 
11
- **Try it:** Press the first button with Shift, then Save with Alt.
10
+
11
+ **Release stage:** Data binding and browser behavior (core 0.2.0).
12
+
13
+ **Goal:** Use one declared button mechanism and inspect its click parameters.
14
+
15
+ **Before this lesson:** [c02 · Controller](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/02_controller.md), [c05 · Node methods](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/05_node_methods.md). These prerequisites support the release sequence; later comparisons are explicitly labelled below.
16
+
17
+ **Open and edit:** select **c06 · Button controller** in your environment’s gallery. For source changes, copy/edit the matching page in your own project using the [setup guide](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md); the gallery is a source viewer. Save, then reload/reopen as that guide explains. The snippets below are excerpts/edits within that Page, not standalone launch commands. This lesson executes authored inline JavaScript and needs the gallery’s permissive CSP profile; a strict named-Logic starter needs the corresponding named form.
18
+
19
+ ## Read the page
20
+
21
+ The first button has a nested controller; the others use `fire` or `fire_<name>`. Mechanism precedence is not a choice: combining mechanisms on one button is an error. A mechanism makes it a non-submit button.
22
+
23
+ ```python
24
+ button = nested.button("Press me, also with Shift or Alt", id="press")
25
+ button.dataController("this.SET('.clicks', button_counter)")
26
+ ```
27
+
28
+ This reduced excerpt focuses on the counter; the provided page also writes modifier keys. `button_counter`, modifier booleans and `_evt` are supplied by Gramlot.
29
+
30
+ Python builds Source on the server; its JavaScript counterpart builds it on Node/Bun or in a standalone Worker. Bindings, actions and any Logic execute in the browser; Python strings containing JavaScript are not executed by Python.
31
+
32
+ ## Try it and predict the result
33
+
34
+ 1. Click the first button with Shift: counter 1 and keys Shift.
35
+ 2. Click Save with Alt: the result says “fired with Alt”.
36
+ 3. Click Save and close without modifiers: the log receives “save, close” in declaration order.
37
+
38
+ ## Change it and check yourself
39
+
40
+ Add another click to the first button without modifiers.
41
+
42
+ **Answer:** the counter is 2 and keys becomes none. You did not write a counter-increment routine or extract modifiers from the browser event yourself.
43
+
44
+ ## The same example across stages
45
+
46
+ In the later **0.2.14** [c11 · Call on click](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/11_call_on_click.md), the same nested-button pattern uses `dataRpc` for a server call. The framework supplies dispatch, response/error handling and result placement, so a click controller does not have to coordinate the request itself. Both stages remain explicit; local actions do not suddenly imply server persistence.
47
+
48
+ ## Limits and next step
49
+
50
+ The counter belongs to the button and survives a rebuild of that node; a newly created button is not the same identity. Do not add an `action` to the existing nested-controller button.
51
+
52
+ Continue with [c07 · Events](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/07_events.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) explains the full contract.
53
+
54
+ </details>
@@ -1,10 +1,49 @@
1
1
  # 07 · Events
2
2
 
3
- `connect_on<event>` adds one DOM listener to the element of its node. The event name is the part after `connect_on`, lower-cased: `connect_onclick` listens to `click`, `connect_onkeydown` to `keydown`.
3
+ Elements declare how they respond to browser events. You learn where an event is handled and how the declaring Source node updates Data.
4
4
 
5
- - The value is inline code with `this` = the declaring node and the parameter `event`. `this.SET('.path', …)` writes relative to the node's context.
6
- - The handler runs with the declaring node also when the event hits a descendant: a click on a chip reaches the section's handler, and `event.target` names the chip.
7
- - A value of dotted names, such as `gui.focused`, is named logic of a `js_requires` group instead. `js_requires` needs a server with a resource system: the core `GramlotFileServer` and the current adapters have none and raise an error for any name, so this page uses inline code. The resource system comes with genro-kajenn, part of Genro.
8
- - Removing the node removes its listeners.
5
+ **Uses:** Data and inline event handlers — core 0.2.0. Named event groups need the separate Kajenn resource host.
9
6
 
10
- **Try it:** Click the green chip, type a word, then add `connect_onfocus` to the field.
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+
11
+ **Release stage:** Data binding and browser behavior (core 0.2.0).
12
+
13
+ **Goal:** Follow a declared event to the declaring node and its Data write.
14
+
15
+ **Before this lesson:** [c02 · Controller](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/02_controller.md), [c06 · Button controller](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/06_button_controller.md). These prerequisites support the release sequence; later comparisons are explicitly labelled below.
16
+
17
+ **Open and edit:** select **c07 · Events** in your environment’s gallery. For source changes, copy/edit the matching page in your own project using the [setup guide](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md); the gallery is a source viewer. Save, then reload/reopen as that guide explains. The snippets below are excerpts/edits within that Page, not standalone launch commands. This lesson executes authored inline JavaScript and needs the gallery’s permissive CSP profile; a strict named-Logic starter needs the corresponding named form.
18
+
19
+ ## Read the page
20
+
21
+ `connect_onclick` declares a listener owned by the Source node. Inline `this` is the declaring node, while `event.target` may be a descendant. The section records the clicked chip’s ID; typing records length and the last key; hover writes a class.
22
+
23
+ The input’s own `event.target.value.length` is used only to explain that event’s payload, not to scrape unrelated controls or create a parallel form state. Normal application field state uses a `value` binding as in b04.
24
+
25
+ Python builds Source on the server; its JavaScript counterpart builds it on Node/Bun or in a standalone Worker. Bindings, actions and any Logic execute in the browser; Python strings containing JavaScript are not executed by Python.
26
+
27
+ ## Try it and predict the result
28
+
29
+ 1. Click green: the section display shows `chip-green`, because the child was the event target.
30
+ 2. Type “cat”: length becomes 3 and the last key reflects your last keypress.
31
+ 3. Move over/out of the box: its class changes and then returns to card.
32
+
33
+ ## Change it and check yourself
34
+
35
+ Add `connect_onfocus="this.SET('.key', 'focused')"` to the input, reload and focus it before typing.
36
+
37
+ **Answer:** Last key first shows focused; typing then replaces it with the keyboard event’s key. The same Source context resolves `.key`.
38
+
39
+ ## The same example across stages
40
+
41
+ For most field editing, b04’s `value` pointer replaces an authored input-to-Data handler. This event lesson remains useful when the event payload itself matters. Both are 0.2.0 mechanisms, not separate release milestones.
42
+
43
+ ## Limits and next step
44
+
45
+ Removing a node removes its listeners. Named handlers require a dotted `group.method` from `js_requires`; a root Logic method used by `func` is not a substitute. The common gallery has no resource host; the named-group lesson belongs with genro-kajenn. Inline code needs the permissive CSP profile.
46
+
47
+ Continue with [c08 · End-to-end story](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/08_end_to_end.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) explains the full contract.
48
+
49
+ </details>