@gramlot/gramlot-examples 0.2.15 → 0.3.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 (74) hide show
  1. package/README.md +12 -5
  2. package/package.json +4 -3
  3. package/src/gramlot_examples/catalog.json +163 -1
  4. package/src/gramlot_examples/gallery/collections.js +4 -0
  5. package/src/gramlot_examples/gallery/dist/frame.js +88 -28
  6. package/src/gramlot_examples/gallery/dist/gallery.js +211 -52
  7. package/src/gramlot_examples/gallery/dist/inputs.js +24456 -0
  8. package/src/gramlot_examples/gallery/dist/inspector.js +42 -18
  9. package/src/gramlot_examples/gallery/dist/layout.js +541 -0
  10. package/src/gramlot_examples/gallery/dist/theme-styles.js +2 -2
  11. package/src/gramlot_examples/gallery/gallery-page.js +49 -17
  12. package/src/gramlot_examples/gallery/gallery.css +50 -4
  13. package/src/gramlot_examples/index.js +3 -2
  14. package/src/gramlot_examples/pages/README.md +5 -2
  15. package/src/gramlot_examples/pages/binding/06_style_and_visibility.md +77 -12
  16. package/src/gramlot_examples/pages/binding/09_forms.js +1 -1
  17. package/src/gramlot_examples/pages/components/01_textbox.js +32 -0
  18. package/src/gramlot_examples/pages/components/01_textbox.md +14 -0
  19. package/src/gramlot_examples/pages/components/02_checkbox.js +25 -0
  20. package/src/gramlot_examples/pages/components/02_checkbox.md +14 -0
  21. package/src/gramlot_examples/pages/components/03_radio_button_text.js +30 -0
  22. package/src/gramlot_examples/pages/components/03_radio_button_text.md +16 -0
  23. package/src/gramlot_examples/pages/components/04_text_box_area.js +23 -0
  24. package/src/gramlot_examples/pages/components/04_text_box_area.md +14 -0
  25. package/src/gramlot_examples/pages/components/05_passwordbox.js +23 -0
  26. package/src/gramlot_examples/pages/components/05_passwordbox.md +14 -0
  27. package/src/gramlot_examples/pages/components/06_radio_button.js +28 -0
  28. package/src/gramlot_examples/pages/components/06_radio_button.md +14 -0
  29. package/src/gramlot_examples/pages/components/07_colorpicker.js +24 -0
  30. package/src/gramlot_examples/pages/components/07_colorpicker.md +14 -0
  31. package/src/gramlot_examples/pages/components/08_horizontal_slider.js +24 -0
  32. package/src/gramlot_examples/pages/components/08_horizontal_slider.md +14 -0
  33. package/src/gramlot_examples/pages/components/09_vertical_slider.js +24 -0
  34. package/src/gramlot_examples/pages/components/09_vertical_slider.md +14 -0
  35. package/src/gramlot_examples/pages/components/10_number_textbox.js +30 -0
  36. package/src/gramlot_examples/pages/components/10_number_textbox.md +16 -0
  37. package/src/gramlot_examples/pages/components/11_date_textbox.js +23 -0
  38. package/src/gramlot_examples/pages/components/11_date_textbox.md +18 -0
  39. package/src/gramlot_examples/pages/components/12_time_textbox.js +23 -0
  40. package/src/gramlot_examples/pages/components/12_time_textbox.md +16 -0
  41. package/src/gramlot_examples/pages/components/13_filtering_select.js +23 -0
  42. package/src/gramlot_examples/pages/components/13_filtering_select.md +20 -0
  43. package/src/gramlot_examples/pages/components/14_callback_select.js +45 -0
  44. package/src/gramlot_examples/pages/components/14_callback_select.md +24 -0
  45. package/src/gramlot_examples/pages/components/15_combo_box.js +23 -0
  46. package/src/gramlot_examples/pages/components/15_combo_box.md +22 -0
  47. package/src/gramlot_examples/pages/components/16_checkbox_text.js +23 -0
  48. package/src/gramlot_examples/pages/components/16_checkbox_text.md +23 -0
  49. package/src/gramlot_examples/pages/components/17_date_calendar.js +25 -0
  50. package/src/gramlot_examples/pages/components/17_date_calendar.md +22 -0
  51. package/src/gramlot_examples/pages/components/18_validation.js +31 -0
  52. package/src/gramlot_examples/pages/components/18_validation.md +29 -0
  53. package/src/gramlot_examples/pages/components/19_formlet.js +26 -0
  54. package/src/gramlot_examples/pages/components/19_formlet.md +12 -0
  55. package/src/gramlot_examples/pages/components/20_group_box.js +26 -0
  56. package/src/gramlot_examples/pages/components/20_group_box.md +12 -0
  57. package/src/gramlot_examples/pages/components/21_switch.js +19 -0
  58. package/src/gramlot_examples/pages/components/21_switch.md +12 -0
  59. package/src/gramlot_examples/pages/components/22_button.js +18 -0
  60. package/src/gramlot_examples/pages/components/22_button.md +12 -0
  61. package/src/gramlot_examples/pages/components/23_toggle_button.js +18 -0
  62. package/src/gramlot_examples/pages/components/23_toggle_button.md +12 -0
  63. package/src/gramlot_examples/pages/components/24_dropdown_button.js +21 -0
  64. package/src/gramlot_examples/pages/components/24_dropdown_button.md +12 -0
  65. package/src/gramlot_examples/pages/components/25_validation_engine.js +51 -0
  66. package/src/gramlot_examples/pages/components/25_validation_engine.md +26 -0
  67. package/src/gramlot_examples/pages/components/README.md +40 -0
  68. package/src/gramlot_examples/pages/controllers/02_controller.js +1 -1
  69. package/src/gramlot_examples/pages/controllers/16_attribute_projection.js +3 -3
  70. package/src/gramlot_examples/pages/controllers/16_attribute_projection.md +8 -8
  71. package/src/gramlot_examples/pages/controllers/22_pointer_transforms.js +39 -0
  72. package/src/gramlot_examples/pages/controllers/22_pointer_transforms.md +64 -0
  73. package/src/gramlot_examples/pages/controllers/README.md +3 -0
  74. package/src/gramlot_examples/pages/html_svg/06_forms.js +3 -3
@@ -0,0 +1,24 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = "Color and labelbox";
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+
7
+ main(root) {
8
+ const pane = root.main({class: 'example-page stack'});
9
+ pane.h1("Color and labelbox");
10
+ pane.p("Choose a color in either picker. Both controls and the swatch follow the same Data value.");
11
+ const controls = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
12
+ controls.checkbox({id: "live", value: "^live", lbl: "Editing", label: "Update while editing"});
13
+ controls.checkbox({id: "locked", value: "^locked", lbl: "State", label: "Disable controls"});
14
+ const fields = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
15
+ fields.colorpicker({id: "color", value: "^color", lbl: "Accent color", live: "^live", disabled: "^locked", box_padding: "8px"});
16
+ fields.colorpicker({id: "color-copy", value: "^color", lbl: "Same color", live: "^live", disabled: "^locked"});
17
+ pane.div({background_color: "^color", width: "6rem", height: "3rem", border: "1px solid #888", aria_label: "Color sample"});
18
+ pane.p("^color", {id: "color-value"});
19
+ for (const [path, value] of [["color", "#315fa8"], ["locked", false], ["live", true]]) pane.dataSetter({destination_path: path, value});
20
+ }
21
+ }
22
+
23
+ /** This page needs no named client methods. */
24
+ export class Logic {}
@@ -0,0 +1,14 @@
1
+ # 07 · Color and labelbox
2
+
3
+ Choose a color in either picker. Both controls and the swatch follow the same Data value.
4
+
5
+ **Uses:** the 0.3 input collection, shared labelbox, Source, Data and bindings. No endpoint calls, stores or validation engine.
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+ `colorpicker` uses the native browser color dialog and a `#rrggbb` string. Live mode uses the input events the browser supplies; otherwise the value commits on change. The swatch uses ordinary Source style binding. The dialog appearance can differ between browsers.
11
+
12
+ All controls use `lbl`, reactive `lbl_position` (TL by default), and the shared `lbl_`/`box_` style prefixes. Disable the controls to compare their common behavior. Label layout variants are in Settings (⚙) in the gallery footer.
13
+
14
+ </details>
@@ -0,0 +1,24 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = "Horizontal slider";
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+
7
+ main(root) {
8
+ const pane = root.main({class: 'example-page stack'});
9
+ pane.h1("Horizontal slider");
10
+ pane.p("Two horizontal sliders share a numeric level. Drag or use the arrow keys; switch live editing to compare immediate and committed updates.");
11
+ const controls = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
12
+ controls.checkbox({id: "live", value: "^live", lbl: "Editing", label: "Update while editing"});
13
+ controls.checkbox({id: "locked", value: "^locked", lbl: "State", label: "Disable controls"});
14
+ const fields = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
15
+ fields.horizontalSlider({id: "level", value: "^level", lbl: "Level", min: 0, max: 100, step: 5,
16
+ live: "^live", disabled: "^locked", box_padding: "8px"});
17
+ fields.horizontalSlider({id: "level-copy", value: "^level", lbl: "Same level", min: 0, max: 100, step: 5, live: "^live", disabled: "^locked"});
18
+ pane.p("^level", {id: "level-value"});
19
+ for (const [path, value] of [["level", 50], ["locked", false], ["live", true]]) pane.dataSetter({destination_path: path, value});
20
+ }
21
+ }
22
+
23
+ /** This page needs no named client methods. */
24
+ export class Logic {}
@@ -0,0 +1,14 @@
1
+ # 08 · Horizontal slider
2
+
3
+ Two horizontal sliders share a numeric level. Drag or use the arrow keys; switch live editing to compare immediate and committed updates.
4
+
5
+ **Uses:** the 0.3 input collection, shared labelbox, Source, Data and bindings. No endpoint calls, stores or validation engine.
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+ `horizontalSlider` wraps a native range input. `min`, `max` and `step` define its numeric range and can also use Data pointers. The Data value is a number, not text. With live editing off, releasing the slider commits the value. Native clamping affects display and does not rewrite an out-of-range Data value by itself.
11
+
12
+ All controls use `lbl`, reactive `lbl_position` (TL by default), and the shared `lbl_`/`box_` style prefixes. Disable the controls to compare their common behavior. Label layout variants are in Settings (⚙) in the gallery footer.
13
+
14
+ </details>
@@ -0,0 +1,24 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = "Vertical slider";
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+
7
+ main(root) {
8
+ const pane = root.main({class: 'example-page stack'});
9
+ pane.h1("Vertical slider");
10
+ pane.p("Vertical and horizontal sliders share the same level. The vertical minimum is at the bottom and the maximum at the top.");
11
+ const controls = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
12
+ controls.checkbox({id: "live", value: "^live", lbl: "Editing", label: "Update while editing"});
13
+ controls.checkbox({id: "locked", value: "^locked", lbl: "State", label: "Disable controls"});
14
+ const fields = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
15
+ fields.verticalSlider({id: "level", value: "^level", lbl: "Vertical level", min: 0, max: 100, step: 5,
16
+ live: "^live", disabled: "^locked", box_padding: "8px"});
17
+ fields.horizontalSlider({id: "level-copy", value: "^level", lbl: "Same level horizontally", min: 0, max: 100, step: 5, live: "^live", disabled: "^locked"});
18
+ pane.p("^level", {id: "level-value"});
19
+ for (const [path, value] of [["level", 50], ["locked", false], ["live", true]]) pane.dataSetter({destination_path: path, value});
20
+ }
21
+ }
22
+
23
+ /** This page needs no named client methods. */
24
+ export class Logic {}
@@ -0,0 +1,14 @@
1
+ # 09 · Vertical slider
2
+
3
+ Vertical and horizontal sliders share the same level. The vertical minimum is at the bottom and the maximum at the top.
4
+
5
+ **Uses:** the 0.3 input collection, shared labelbox, Source, Data and bindings. No endpoint calls, stores or validation engine.
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+ `verticalSlider` shares the numeric and editing contract of `horizontalSlider`. Standard CSS writing mode gives the native range its vertical orientation; the value increases upwards. Try ArrowUp and ArrowDown, then move the label. Home/End follow the browser's native writing-mode behavior: WebKit and Chromium can select opposite endpoints. The horizontal companion shows that orientation does not change the Data value.
11
+
12
+ All controls use `lbl`, reactive `lbl_position` (TL by default), and the shared `lbl_`/`box_` style prefixes. Disable the controls to compare their common behavior. Label layout variants are in Settings (⚙) in the gallery footer.
13
+
14
+ </details>
@@ -0,0 +1,30 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = "Number editing";
5
+ static locale = "it-IT";
6
+ static css = ["/themes/gramlot-base/theme.css"];
7
+ async main(root) {
8
+ const {Bag} = await import('@gramlot/gramlot');
9
+ const pane = root.main({class:"example-page stack"});
10
+ pane.h1("Number editing");
11
+ pane.p("Try a comma decimal, an integer and an exact Decimal. Invalid text stays in the field without changing Data.");
12
+ const controls = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
13
+ controls.checkbox({id:"live",value:"^live",lbl:"Editing",label:"Update while typing"});
14
+ controls.checkbox({id:"locked",value:"^locked",lbl:"State",label:"Read only"});
15
+ const fields = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
16
+ fields.numberTextBox({id:"amount", value:"^amount", lbl:"Amount (0–100)", format:"0.00", min:0, max:100, live:"^live", readonly:"^locked", box_c_width:"14rem"});
17
+ fields.numberTextBox({id:"amount-copy", value:"^amount", lbl:"Same amount", format:"0.00", readonly:"^locked", box_c_width:"14rem"});
18
+ fields.numberTextBox({id:"quantity", value:"^quantity", lbl:"Integer", dtype:"L", readonly:"^locked", box_c_width:"10rem"});
19
+ fields.numberTextBox({id:"exact", value:"^exact", lbl:"Exact Decimal", dtype:"N", format:"0.00", readonly:"^locked", box_c_width:"16rem"});
20
+ pane.p("^amount", {id:"amount-value", format:"0.00", dtype:"R"});
21
+ pane.p("^exact", {id:"exact-value", format:"0.00", dtype:"N"});
22
+ pane.dataSetter({destination_path:"amount",value:12.5});
23
+ pane.dataSetter({destination_path:"quantity",value:3});
24
+ pane.dataSetter({destination_path:"exact",value:Bag.fromTytx('{"exact":"9007199254740993.25::N"}').getItem('exact')});
25
+ for (const [path,value] of [["live",false],["locked",false]]) pane.dataSetter({destination_path:path,value});
26
+ }
27
+ }
28
+
29
+
30
+ export class Logic {}
@@ -0,0 +1,16 @@
1
+ # 10 · Number editing
2
+
3
+ Try a comma decimal, an integer and an exact Decimal. Invalid text stays in the field without changing Data.
4
+
5
+ **Uses:** the 0.3 input collection, labelbox, typed Data, page locale and Gramlot formatting. No endpoint calls or validation engine.
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+ The page locale is `it-IT`: enter `38,25`, without thousands separators. The amount accepts 0–100; the integer field rejects fractions. `dtype="N"` keeps a Decimal, including digits beyond JavaScript's safe integer range. `format="0.00"` controls display, not the stored precision. In Python the initial exact value is a `Decimal`; JavaScript initializes a typed Bag with `Bag.fromTytx` and reads its Decimal.
11
+
12
+ Try `38x`: it stays visible with an error while the previous Data value remains intact. Clear a field to write null. Enter or leaving the field commits a valid draft; Escape restores the last accepted value. With live editing, each complete valid input updates Data. Partial or invalid input never writes a replacement value.
13
+
14
+ Make the fields read-only; try label layout variants from Settings (⚙) in the gallery footer. Both controls keep the shared labelbox contract (`lbl_` and `box_`). The cards stay within 24rem.
15
+
16
+ </details>
@@ -0,0 +1,23 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = "Date editing";
5
+ static locale = "it-IT";
6
+ static css = ["/themes/gramlot-base/theme.css"];
7
+ main(root) {
8
+ const pane = root.main({class:"example-page stack"});
9
+ pane.h1("Date editing");
10
+ pane.p("Enter a calendar date or a word such as oggi or domani. Two fields share one civil date.");
11
+ const controls = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
12
+ controls.checkbox({id:"live",value:"^live",lbl:"Editing",label:"Update while typing"});
13
+ controls.checkbox({id:"locked",value:"^locked",lbl:"State",label:"Read only"});
14
+ const fields = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
15
+ fields.dateTextBox({id:"date", value:"^date", lbl:"Appointment", format:"long", live:"^live", readonly:"^locked", box_c_width:"16rem"});
16
+ fields.dateTextBox({id:"date-copy", value:"^date", lbl:"Same date, short display", calendar:false, format:"short", readonly:"^locked", box_c_width:"14rem"});
17
+ pane.p("^date", {id:"date-value", format:"long", dtype:"D"});
18
+ pane.dataSetter({destination_path:"date",value:new Date('2026-10-16T00:00:00Z')});
19
+ for (const [path,value] of [["live",false],["locked",false]]) pane.dataSetter({destination_path:path,value});
20
+ }
21
+ }
22
+
23
+ export class Logic {}
@@ -0,0 +1,18 @@
1
+ # 11 · Date editing
2
+
3
+ Enter a calendar date or a word such as oggi or domani. Two fields share one civil date.
4
+
5
+ **Uses:** the 0.3 input collection, labelbox, typed Data, page locale and Gramlot formatting. No endpoint calls or validation engine.
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+ The page locale is `it-IT`. Focus a field to edit its short date; leave it to see its chosen `format`. Try `18/10/2026`, `oggi` and `domani`. Words such as `ieri`, `oggi` and `domani` are relative to today in the browser, independently of the date already in the field. An explicit `workdate="YYYY-MM-DD"` can be used when an application deliberately needs a different reference day.
11
+
12
+ The Data value is a Python `date`, transported as TYTX `D`; JavaScript represents it as a Date at UTC midnight. It remains a civil date, independent of the page timezone. `31/02/2026` is invalid and does not change Data. Period expressions are rejected: this field edits one complete date. Clearing writes null; Escape cancels the draft. This is a text editor, not a calendar popup.
13
+
14
+ Make the fields read-only; try label layout variants from Settings (⚙) in the gallery footer. Both controls keep the shared labelbox contract (`lbl_` and `box_`). The cards stay within 24rem.
15
+
16
+ </details>
17
+
18
+ The first field also opens a calendar (Alt+Down or its button). The second uses `calendar=False` to keep text editing only. See **Date calendar** for the standalone view and shared binding.
@@ -0,0 +1,23 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = "Time editing";
5
+ static locale = "it-IT";
6
+ static css = ["/themes/gramlot-base/theme.css"];
7
+ main(root) {
8
+ const pane = root.main({class:"example-page stack"});
9
+ pane.h1("Time editing");
10
+ pane.p("Edit a civil time with hours, minutes and optional seconds. The two fields keep the same typed value.");
11
+ const controls = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
12
+ controls.checkbox({id:"live",value:"^live",lbl:"Editing",label:"Update while typing"});
13
+ controls.checkbox({id:"locked",value:"^locked",lbl:"State",label:"Read only"});
14
+ const fields = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
15
+ fields.timeTextBox({id:"time", value:"^time", lbl:"Departure", format:"medium", live:"^live", readonly:"^locked", box_c_width:"14rem"});
16
+ fields.timeTextBox({id:"time-copy", value:"^time", lbl:"Same time, short display", format:"short", readonly:"^locked", box_c_width:"14rem"});
17
+ pane.p("^time", {id:"time-value", format:"medium", dtype:"H"});
18
+ pane.dataSetter({destination_path:"time",value:new Date('1970-01-01T09:30:00Z')});
19
+ for (const [path,value] of [["live",false],["locked",false]]) pane.dataSetter({destination_path:path,value});
20
+ }
21
+ }
22
+
23
+ export class Logic {}
@@ -0,0 +1,16 @@
1
+ # 12 · Time editing
2
+
3
+ Edit a civil time with hours, minutes and optional seconds. The two fields keep the same typed value.
4
+
5
+ **Uses:** the 0.3 input collection, labelbox, typed Data, page locale and Gramlot formatting. No endpoint calls or validation engine.
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+ Type `14:35`, `14:35:20` or `14:35:20.125`. Editing uses the 24-hour clock; display uses the page locale and `format` (`short` or `medium`). A short display can hide seconds, but it does not remove them from Data. Focus the field to see the complete time again.
11
+
12
+ Python starts with `datetime.time`; TYTX `H` carries a civil time, represented in JavaScript on 1970-01-01 UTC. No timezone shift is applied. Midnight is a valid value; an empty field writes null. `25:00` stays as an invalid draft without overwriting Data. Enter or leaving the field commits; Escape cancels. Millisecond precision is supported; this editor does not provide a time picker popup.
13
+
14
+ Make the fields read-only; try label layout variants from Settings (⚙) in the gallery footer. Both controls keep the shared labelbox contract (`lbl_` and `box_`). The cards stay within 24rem.
15
+
16
+ </details>
@@ -0,0 +1,23 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = "Filtering select";
5
+ static css = ["/themes/gramlot-base/theme.css"];
6
+ main(root) {
7
+ const pane = root.main({class:"example-page stack"});
8
+ pane.h1("Filtering select");
9
+ pane.p("Search a caption, store its code. Two fields share the same choice and the same list.");
10
+ const controls = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
11
+ controls.checkbox({id:"locked",value:"^locked",lbl:"State",label:"Read only"});
12
+ const fields = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
13
+ fields.filteringSelect({id:"choice",value:"^choice",values:"^choices",lbl:"Delivery",readonly:"^locked",box_c_width:"16rem",placeholder:"Type part of a caption"});
14
+ fields.filteringSelect({id:"choice-copy",value:"^choice",values:"^choices",lbl:"Same choice",readonly:"^locked",box_c_width:"16rem"});
15
+ pane.dataSetter({destination_path:"choices",value:"standard:Standard,express:Express,pickup:Pick up"});
16
+ pane.dataSetter({destination_path:"choice",value:"express"});
17
+ pane.p("Selected code:");
18
+ pane.p("^choice",{id:"choice-value"});
19
+ pane.dataSetter({destination_path:"locked",value:false});
20
+ }
21
+ }
22
+
23
+ export class Logic {}
@@ -0,0 +1,20 @@
1
+ # 13 · Filtering select
2
+
3
+ Search a caption, store its code. Two fields share the same choice and the same list.
4
+
5
+ **Uses:** the 0.3 input collection, shared labelbox, Source and Data bindings. No endpoints or validation engine.
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+ Type `Exp`, then press Enter or click **Express**. Data stores `express`, while both fields show **Express**. The arrow button opens all choices; use Up/Down to move and Enter to select. Escape restores the committed caption. Emptying and leaving the field writes null. An unknown caption stays as an invalid draft and does not overwrite Data.
11
+
12
+ `values="^choices"` reads `code:Caption` pairs separated by commas from Data. Updating that value refreshes the choices. `filteringSelect` is a component; plain `select` remains the native HTML element.
13
+
14
+ ```python
15
+ fields.filteringSelect(value="^choice", values="^choices", lbl="Delivery")
16
+ ```
17
+
18
+ Move the label or enable read-only mode without replacing the input. Both lessons use the same `lbl_` and `box_` contract as the earlier controls, with compact cards and bounded field widths.
19
+
20
+ </details>
@@ -0,0 +1,45 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = "Callback select";
5
+ static css = ["/themes/gramlot-base/theme.css"];
6
+ main(root) {
7
+ const pane = root.main({class:"example-page stack"});
8
+ pane.h1("Callback select");
9
+ pane.p("Search 100 random samples generated once in this page. The callback reads a Data Bag, without a server request.");
10
+ const controls = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
11
+ controls.checkbox({id:"locked",value:"^locked",lbl:"State",label:"Read only"});
12
+ const fields = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
13
+ fields.callbackSelect({id:"choice",value:"^choice",callback:"searchChoices",lbl:"Sample",readonly:"^locked",box_c_width:"18rem",placeholder:"Try Sample 02 or value 7"});
14
+ fields.callbackSelect({id:"choice-copy",value:"^choice",callback:"searchChoices",lbl:"Same sample",readonly:"^locked",box_c_width:"18rem"});
15
+ pane.p("100 stable codes; random values stay unchanged until the page reloads.");
16
+ pane.dataController({func:"createSamples",_onStart:true});
17
+ pane.p("Selected code:");
18
+ pane.p("^choice",{id:"choice-value"});
19
+ pane.dataSetter({destination_path:"locked",value:false});
20
+ }
21
+ }
22
+
23
+ export class Logic {
24
+ createSamples(node) {
25
+ for (let i = 1; i <= 100; i++) {
26
+ const number = String(i).padStart(3, '0');
27
+ const id = `item_${number}`;
28
+ const value = Math.floor(Math.random() * 100);
29
+ node.SET(`samples.${id}.id`, id);
30
+ node.SET(`samples.${id}.name`, `Sample ${number} — value ${value}`);
31
+ }
32
+ node.SET('choice', 'item_001');
33
+ }
34
+ searchChoices(node, kw) {
35
+ const rows = node.GET('samples').getNodes().map(item => ({
36
+ id: item.getValue().getItem('id'),
37
+ name: item.getValue().getItem('name')
38
+ }));
39
+ return {
40
+ rows: rows.filter(row => kw._id != null ? row.id === kw._id
41
+ : row.name.toLocaleLowerCase().includes(kw._querystring.toLocaleLowerCase())),
42
+ identifier: 'id', caption: 'name'
43
+ };
44
+ }
45
+ }
@@ -0,0 +1,24 @@
1
+ # 14 · Callback select
2
+
3
+ Search 100 random samples generated once in this page. The callback reads a Data Bag, without a server request.
4
+
5
+ **Uses:** the 0.3 input collection, shared labelbox, Source and Data bindings and named browser Logic. No endpoints or validation engine.
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+ Search `Sample 02` to see samples 020–029, then choose one. The second field recovers its caption from the selected code. Searching again does not regenerate the random values: inspect `samples` in the Data inspector to see the same 100 records.
11
+
12
+ Python declares the control and names its browser Logic method:
13
+
14
+ ```python
15
+ fields.callbackSelect(value="^choice", callback="searchChoices", lbl="Sample")
16
+ ```
17
+
18
+ The Logic method receives `(node, kw)`. `_querystring` asks for matches; `_id` asks for the record of a code already in Data. It returns `{rows, identifier: 'id', caption: 'name'}`. Rows are objects with string codes and captions. The example reads them from a Data Bag initialized by an ordinary startup controller. No endpoint, manual DOM code or second application state is needed.
19
+
20
+ A callback may also return a Promise. The control shows pending state and discards responses from superseded searches or removed controls. Callback failures appear below the input. The same code/caption, keyboard and invalid-draft rules apply as in Filtering select.
21
+
22
+ Move the label or enable read-only mode without replacing the input. Both lessons use the same `lbl_` and `box_` contract as the earlier controls, with compact cards and bounded field widths.
23
+
24
+ </details>
@@ -0,0 +1,23 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = "Combo box";
5
+ static css = ["/themes/gramlot-base/theme.css"];
6
+ main(root) {
7
+ const pane = root.main({class:"example-page stack"});
8
+ pane.h1("Combo box");
9
+ pane.p("Choose a suggested city or enter a different one. Two fields share the same text; suggestions do not restrict what you can write.");
10
+ const controls = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
11
+ controls.checkbox({id:"locked",value:"^locked",lbl:"State",label:"Read only"});
12
+ const fields = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
13
+ fields.comboBox({id:"choice",value:"^choice",values:"^choices",lbl:"City",readonly:"^locked",box_c_width:"16rem",placeholder:"Choose or type a city"});
14
+ fields.comboBox({id:"choice-copy",value:"^choice",values:"^choices",lbl:"Same city",readonly:"^locked",box_c_width:"16rem"});
15
+ pane.dataSetter({destination_path:"choices",value:"Rome,Paris,Berlin"});
16
+ pane.dataSetter({destination_path:"choice",value:"Rome"});
17
+ pane.p("Stored text:");
18
+ pane.p("^choice",{id:"choice-value"});
19
+ pane.dataSetter({destination_path:"locked",value:false});
20
+ }
21
+ }
22
+
23
+ export class Logic {}
@@ -0,0 +1,22 @@
1
+ # 15 · Combo box
2
+
3
+ Choose a suggested city or enter a different one. Two fields share the same text; suggestions do not restrict what you can write.
4
+
5
+ **Uses:** the 0.3 input collection, labelbox, Source and Data bindings. No endpoints or validation engine.
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+ Type `Pa` and choose **Paris** from the suggestions. Then type a city absent from the list and press Enter or leave the field: the new text is accepted and appears in both controls.
11
+
12
+ ```python
13
+ fields.comboBox(value="^choice", values="Rome,Paris,Berlin", lbl="City")
14
+ ```
15
+
16
+ `comboBox` stores the text, including the caption of a chosen suggestion. Compare **Filtering select**, which accepts only listed choices and stores their codes. If `values` contains `code:Caption` pairs, comboBox still stores the caption, not the code.
17
+
18
+ The arrow opens all suggestions. Up/Down moves the highlight; Enter or a click chooses it. Escape cancels an uncommitted draft. Empty text writes an empty string; an untouched initial null stays null. `live=True` enables text updates while typing, through the same binding and IME handling as textbox.
19
+
20
+ `values`, `lbl_position` and readonly can follow Data pointers. Changing the label preserves the input and caret. Cards and fields remain compact, using the shared `lbl_` and `box_` styling contract.
21
+
22
+ </details>
@@ -0,0 +1,23 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = "Checkbox group";
5
+ static css = ["/themes/gramlot-base/theme.css"];
6
+ main(root) {
7
+ const pane = root.main({class:"example-page stack"});
8
+ pane.h1("Several choices, one value");
9
+ pane.p("Choose several notification channels. Two groups share the same comma-separated codes; clearing all choices writes null.");
10
+ const controls = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
11
+ controls.html_label("Columns (1–3): ",{for:"columns"}).span("^columns",{id:"columns-value"});
12
+ controls.input({id:"columns",type:"range",min:1,max:3,value:"^columns",live:true,width:"16rem",max_width:"100%"});
13
+ controls.checkbox({id:"locked",value:"^locked",lbl:"State",label:"Read only"});
14
+ const fields = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
15
+ fields.checkBoxText({id:"channels",value:"^channels",values:"^choices",lbl:"Notifications",cols:"^columns",readonly:"^locked",box_padding:"8px"});
16
+ fields.checkBoxText({id:"channels-copy",value:"^channels",values:"^choices",lbl:"Same channels",cols:1,readonly:"^locked",box_padding:"8px"});
17
+ pane.p("Selected codes:");
18
+ pane.p("^channels",{id:"channels-value"});
19
+ for (const [destination_path,value] of [["choices","email:Email,sms:SMS,push:Push"],["channels","email,push"],["columns",2],["locked",false]]) pane.dataSetter({destination_path,value});
20
+ }
21
+ }
22
+
23
+ export class Logic {}
@@ -0,0 +1,23 @@
1
+ # 16 · Checkbox group
2
+
3
+ Choose several notification channels. Two groups share the same comma-separated codes; clearing all choices writes null.
4
+
5
+ **Uses:** the 0.3 input collection, labelbox, Source and Data bindings. No endpoints, stores or validation engine.
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+ Check **SMS** in either group: both show the same selection. The Data value is a string such as `email,push,sms`, not the visible captions. Uncheck every option: Data becomes null. Existing selected codes keep their order; new choices are appended.
11
+
12
+ ```python
13
+ fields.checkBoxText(value="^channels", values="email:Email,sms:SMS,push:Push",
14
+ lbl="Notifications", cols=2)
15
+ ```
16
+
17
+ Compare **RadioButtonText**: it uses the same option and labelbox structure, but selects only one code. Here every checkbox is independent. Tab or arrow keys move focus and Space changes the focused checkbox. Home/End move to the first/last option.
18
+
19
+ Change the number of columns; label layout variants are in Settings (⚙) in the gallery footer. The group keeps its controls and selection. `values`, `cols`, `lbl_position` and `readonly` can follow Data pointers; the readonly switch locks both groups. Shared `lbl_` and `box_` attributes style the same labelbox parts as earlier lessons.
20
+
21
+ This lesson uses an inline group. Popup selection and Bag/store-backed options are separate features. Cards stay within 24rem and the column slider within 16rem.
22
+
23
+ </details>
@@ -0,0 +1,25 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static devices = ['desktop', 'tablet', 'phone'];
5
+ static title = "Date calendar";
6
+ static locale = "it-IT";
7
+ static css = ["/themes/gramlot-base/theme.css"];
8
+ main(root) {
9
+ const pane = root.main({class:"example-page stack"});
10
+ pane.h1("One date, three editors");
11
+ pane.p("Pick a day or type a date. The inline calendar, popup and text-only field share the same Data value.");
12
+ const controls = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
13
+ controls.checkbox({id:"locked",value:"^locked",lbl:"State",label:"Read only"});
14
+ const fields = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
15
+ fields.dateTextBox({id:"date",value:"^date",lbl:"With popup",
16
+ readonly:"^locked",box_c_width:"14rem",box_c_max_width:"100%"});
17
+ fields.dateTextBox({id:"plain",value:"^date",lbl:"Text only",calendar:false,
18
+ readonly:"^locked",box_c_width:"14rem",box_c_max_width:"100%"});
19
+ fields.dateCalendar({id:"calendar",value:"^date",lbl:"Choose a day",readonly:"^locked",lbl_color:"#4369c5",box_c_max_width:"100%"});
20
+ pane.p("^date",{id:"date-value",format:"long",dtype:"D"});
21
+ pane.dataSetter({destination_path:"date",value:new Date('2026-10-16T00:00:00Z')});
22
+ pane.dataSetter({destination_path:"locked",value:false});
23
+ }
24
+ }
25
+ export class Logic {}
@@ -0,0 +1,22 @@
1
+ # 17 · Date calendar
2
+
3
+ Pick a day or type a date. The inline calendar, popup and text-only field share the same Data value. Learn when to use a calendar beside the page or inside a date field.
4
+
5
+ **Uses:** the 0.3 input collection, labelbox and typed Data bindings. No endpoints, stores or validation engine.
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+ ```python
11
+ fields.dateTextBox(value="^date", lbl="With popup")
12
+ fields.dateTextBox(value="^date", lbl="Text only", calendar=False)
13
+ fields.dateCalendar(value="^date", lbl="Choose a day")
14
+ ```
15
+
16
+ `dateTextBox` has a calendar button by default (`calendar=True`). `dateCalendar` displays the same calendar directly in the page. All three declarations use the same pointer: choosing or typing a date updates every view. The Data holds a civil date (`D`), never the formatted caption. **Clear** writes null; **Today** and natural-language input use today's local date in the browser. The initial sample value does not set the reference day. An application may explicitly provide `workdate` when a different working date is intended.
17
+
18
+ Use the arrows to browse months. In the day grid, arrow keys move by day/week, Home/End move to Monday/Sunday, PageUp/PageDown change month, and Enter or Space selects. Navigation alone leaves Data unchanged. Alt+Down opens the text field's calendar; Escape closes it and returns focus to the field. A popup selection commits the date immediately. Invalid text remains a draft until corrected or cancelled.
19
+
20
+ The calendar has Monday-first weeks; month/day captions follow the page locale (`it-IT` here). **Read only** locks all three controls. Use Settings (⚙) in the gallery footer to compare label positions. The calendar shares the `lbl_position`, `lbl_` and `box_` contract of the other inputs. Cards stay within 24rem and text fields within 14rem.
21
+
22
+ </details>
@@ -0,0 +1,31 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static devices = ['desktop', 'tablet', 'phone'];
5
+ static title = "Required values and length";
6
+ static css = ["/themes/gramlot-base/theme.css"];
7
+ main(root) {
8
+ const pane = root.main({class:"example-page stack"});
9
+ pane.h1("The draft and the accepted value");
10
+ pane.p("Try one letter, more than eight, or an empty name. The draft stays in the field; the Data below keeps the last valid value.");
11
+ const controls = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
12
+ controls.checkbox({id:"live",value:"^live",lbl:"Timing",label:"Validate while typing"});
13
+ const fields = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
14
+ fields.textbox({id:"name",value:"^name",lbl:"Name (2–8 characters)",live:"^live",
15
+ validate_notnull:true,validate_len:"2:8",validate_notnull_error:"Please enter a name.",
16
+ validate_len_error:"Use between 2 and 8 characters.",box_c_width:"16rem",box_c_max_width:"100%"});
17
+ fields.p("Last accepted name:");fields.p("^name",{id:"accepted-name"});
18
+ fields.textbox({id:"name-copy",value:"^name",lbl:"Another view of the Data",readonly:true,
19
+ box_c_width:"16rem",box_c_max_width:"100%"});
20
+ const notes = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
21
+ notes.textBoxArea({id:"notes",value:"^notes",lbl:"Optional note (up to 20 characters)",rows:2,
22
+ validate_len:":20",live:"^live",box_c_width:"18rem",box_c_max_width:"100%"});
23
+ notes.p("^notes",{id:"accepted-notes"});
24
+ const numbers = pane.formlet({class:"card stack",width:"100%",max_width:"24rem"});
25
+ numbers.numberTextBox({id:"quantity",value:"^quantity",lbl:"Required quantity — zero is valid",
26
+ validate_notnull:true,box_c_width:"10rem",box_c_max_width:"100%"});
27
+ numbers.p("^quantity",{id:"accepted-quantity"});
28
+ for (const [destination_path,value] of [["name","Ada"],["notes","Hello"],["quantity",0],["live",false]]) pane.dataSetter({destination_path,value});
29
+ }
30
+ }
31
+ export class Logic {}
@@ -0,0 +1,29 @@
1
+ # 18 · Required values and length
2
+
3
+ Try a missing, short or long name. You learn the difference between the draft visible in a field and the last accepted value in Data.
4
+
5
+ **Uses:** the 0.3 input collection, shared validation, labelbox and Data bindings. No form/save service, callbacks or remote validators.
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+ ```python
11
+ fields.textbox(value="^name", lbl="Name", validate_notnull=True,
12
+ validate_len="2:8", validate_len_error="Use 2 to 8 characters.")
13
+ ```
14
+
15
+ Type `A`, then leave the field: the message appears, while the accepted name stays `Ada`. Try more than eight characters: nothing is truncated. Type `Grace` and leave the field: both Data views update and the message disappears. Clear the field: the required message appears and Data stays `Grace`.
16
+
17
+ Turn **Validate while typing** on to use `live=True`. The same rules now run on each edit, after IME composition has finished. Otherwise text fields validate on their normal change/blur event; typed number/date/time editors also confirm on Enter.
18
+
19
+ `validate_notnull` rejects null and the empty string. Zero and false are values. `validate_len="2:8"` accepts 2–8 characters, `"2:"` sets only a minimum, `":20"` only a maximum, and `"4"` requires an exact length. Length counts JavaScript string units, as in the existing PoC; it is not a grapheme counter. Optional empty text passes the length rule, as the note demonstrates. No trimming or normalization is added.
20
+
21
+ The error messages can be customized with `validate_notnull_error` and `validate_len_error`; they are marked as localizable strings for the future localization service. Rules and messages may follow Data pointers. The next edit uses the current rules. Changing a label does not discard an invalid draft; a different external Data value replaces it. Validation does not filter writes performed directly by application logic.
22
+
23
+ The shared message also drives ARIA and native custom validity. Gramlot remains the rule owner; `maxlength` is not imposed because that would prevent the invalid draft from being entered and inspected. Required quantity demonstrates that clearing a typed editor is rejected while zero is accepted.
24
+
25
+ </details>
26
+
27
+ An invalid field keeps a pale red background. Its message floats without moving the other fields,
28
+ then fades out after about two seconds (no animation with reduced motion). Focus or hover the field to read it again;
29
+ Escape dismisses it. The error and last accepted Data value remain until corrected.