@gramlot/gramlot-examples 0.2.2

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 (98) hide show
  1. package/LICENSE +176 -0
  2. package/NOTICE +8 -0
  3. package/README.md +49 -0
  4. package/package.json +52 -0
  5. package/src/gramlot_examples/catalog.json +253 -0
  6. package/src/gramlot_examples/gallery/dist/LICENSE +176 -0
  7. package/src/gramlot_examples/gallery/dist/NOTICE +8 -0
  8. package/src/gramlot_examples/gallery/dist/frame.js +28 -0
  9. package/src/gramlot_examples/gallery/dist/gallery.js +21239 -0
  10. package/src/gramlot_examples/gallery/dist/notices.json +38 -0
  11. package/src/gramlot_examples/gallery/gallery-page.js +97 -0
  12. package/src/gramlot_examples/gallery/gallery.css +110 -0
  13. package/src/gramlot_examples/gallery/page.js +26 -0
  14. package/src/gramlot_examples/index.js +79 -0
  15. package/src/gramlot_examples/pages/README.md +21 -0
  16. package/src/gramlot_examples/pages/binding/01_pointers.js +35 -0
  17. package/src/gramlot_examples/pages/binding/01_pointers.md +14 -0
  18. package/src/gramlot_examples/pages/binding/02_variable_datapath.js +33 -0
  19. package/src/gramlot_examples/pages/binding/02_variable_datapath.md +9 -0
  20. package/src/gramlot_examples/pages/binding/03_setters_and_defaults.js +42 -0
  21. package/src/gramlot_examples/pages/binding/03_setters_and_defaults.md +10 -0
  22. package/src/gramlot_examples/pages/binding/04_native_editing.js +53 -0
  23. package/src/gramlot_examples/pages/binding/04_native_editing.md +11 -0
  24. package/src/gramlot_examples/pages/binding/05_checkbox_radio.js +41 -0
  25. package/src/gramlot_examples/pages/binding/05_checkbox_radio.md +9 -0
  26. package/src/gramlot_examples/pages/binding/06_style_and_visibility.js +51 -0
  27. package/src/gramlot_examples/pages/binding/06_style_and_visibility.md +11 -0
  28. package/src/gramlot_examples/pages/binding/07_bound_svg.js +33 -0
  29. package/src/gramlot_examples/pages/binding/07_bound_svg.md +7 -0
  30. package/src/gramlot_examples/pages/binding/08_freeze.css +1 -0
  31. package/src/gramlot_examples/pages/binding/08_freeze.js +26 -0
  32. package/src/gramlot_examples/pages/binding/08_freeze.md +11 -0
  33. package/src/gramlot_examples/pages/binding/08_freeze_aux.js +26 -0
  34. package/src/gramlot_examples/pages/binding/09_forms.css +8 -0
  35. package/src/gramlot_examples/pages/binding/09_forms.js +79 -0
  36. package/src/gramlot_examples/pages/binding/09_forms.md +12 -0
  37. package/src/gramlot_examples/pages/binding/10_svg_shapes.js +42 -0
  38. package/src/gramlot_examples/pages/binding/10_svg_shapes.md +9 -0
  39. package/src/gramlot_examples/pages/binding/11_complete_page.css +5 -0
  40. package/src/gramlot_examples/pages/binding/11_complete_page.js +127 -0
  41. package/src/gramlot_examples/pages/binding/11_complete_page.md +12 -0
  42. package/src/gramlot_examples/pages/binding/README.md +9 -0
  43. package/src/gramlot_examples/pages/controllers/01_formula.js +34 -0
  44. package/src/gramlot_examples/pages/controllers/01_formula.md +12 -0
  45. package/src/gramlot_examples/pages/controllers/02_controller.js +44 -0
  46. package/src/gramlot_examples/pages/controllers/02_controller.md +10 -0
  47. package/src/gramlot_examples/pages/controllers/03_named_logic.js +28 -0
  48. package/src/gramlot_examples/pages/controllers/03_named_logic.md +12 -0
  49. package/src/gramlot_examples/pages/controllers/03_named_logic_aux.js +19 -0
  50. package/src/gramlot_examples/pages/controllers/04_inline_expressions.js +38 -0
  51. package/src/gramlot_examples/pages/controllers/04_inline_expressions.md +12 -0
  52. package/src/gramlot_examples/pages/controllers/05_node_methods.css +1 -0
  53. package/src/gramlot_examples/pages/controllers/05_node_methods.js +39 -0
  54. package/src/gramlot_examples/pages/controllers/05_node_methods.md +11 -0
  55. package/src/gramlot_examples/pages/controllers/06_button_controller.js +36 -0
  56. package/src/gramlot_examples/pages/controllers/06_button_controller.md +11 -0
  57. package/src/gramlot_examples/pages/controllers/07_events.js +38 -0
  58. package/src/gramlot_examples/pages/controllers/07_events.md +10 -0
  59. package/src/gramlot_examples/pages/controllers/08_remote_source.js +34 -0
  60. package/src/gramlot_examples/pages/controllers/08_remote_source.md +10 -0
  61. package/src/gramlot_examples/pages/controllers/08_remote_source_aux.js +8 -0
  62. package/src/gramlot_examples/pages/controllers/09_end_to_end.css +1 -0
  63. package/src/gramlot_examples/pages/controllers/09_end_to_end.js +67 -0
  64. package/src/gramlot_examples/pages/controllers/09_end_to_end.md +14 -0
  65. package/src/gramlot_examples/pages/controllers/09_end_to_end_aux.js +35 -0
  66. package/src/gramlot_examples/pages/controllers/README.md +11 -0
  67. package/src/gramlot_examples/pages/html_svg/01_hello_world.js +12 -0
  68. package/src/gramlot_examples/pages/html_svg/01_hello_world.md +9 -0
  69. package/src/gramlot_examples/pages/html_svg/02_text_and_links.js +28 -0
  70. package/src/gramlot_examples/pages/html_svg/02_text_and_links.md +9 -0
  71. package/src/gramlot_examples/pages/html_svg/03_lists.js +29 -0
  72. package/src/gramlot_examples/pages/html_svg/03_lists.md +9 -0
  73. package/src/gramlot_examples/pages/html_svg/04_semantic_page.js +25 -0
  74. package/src/gramlot_examples/pages/html_svg/04_semantic_page.md +9 -0
  75. package/src/gramlot_examples/pages/html_svg/05_tables.js +27 -0
  76. package/src/gramlot_examples/pages/html_svg/05_tables.md +9 -0
  77. package/src/gramlot_examples/pages/html_svg/06_forms.css +5 -0
  78. package/src/gramlot_examples/pages/html_svg/06_forms.js +63 -0
  79. package/src/gramlot_examples/pages/html_svg/06_forms.md +9 -0
  80. package/src/gramlot_examples/pages/html_svg/07_native_disclosure.js +27 -0
  81. package/src/gramlot_examples/pages/html_svg/07_native_disclosure.md +9 -0
  82. package/src/gramlot_examples/pages/html_svg/08_svg_shapes.js +23 -0
  83. package/src/gramlot_examples/pages/html_svg/08_svg_shapes.md +9 -0
  84. package/src/gramlot_examples/pages/html_svg/09_svg_composition.js +42 -0
  85. package/src/gramlot_examples/pages/html_svg/09_svg_composition.md +9 -0
  86. package/src/gramlot_examples/pages/html_svg/10_cards_with_icons.css +6 -0
  87. package/src/gramlot_examples/pages/html_svg/10_cards_with_icons.js +40 -0
  88. package/src/gramlot_examples/pages/html_svg/10_cards_with_icons.md +20 -0
  89. package/src/gramlot_examples/pages/html_svg/11_static_report.css +3 -0
  90. package/src/gramlot_examples/pages/html_svg/11_static_report.js +68 -0
  91. package/src/gramlot_examples/pages/html_svg/11_static_report.md +9 -0
  92. package/src/gramlot_examples/pages/html_svg/12_complete_page.css +5 -0
  93. package/src/gramlot_examples/pages/html_svg/12_complete_page.js +120 -0
  94. package/src/gramlot_examples/pages/html_svg/12_complete_page.md +9 -0
  95. package/src/gramlot_examples/pages/html_svg/13_live_source.css +3 -0
  96. package/src/gramlot_examples/pages/html_svg/13_live_source.js +64 -0
  97. package/src/gramlot_examples/pages/html_svg/13_live_source.md +18 -0
  98. package/src/gramlot_examples/pages/html_svg/README.md +10 -0
@@ -0,0 +1,42 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Setters and defaults';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+
7
+ main(root) {
8
+ const pane = root.div({class: 'example-page stack', datapath: 'order'});
9
+ pane.h1('Setters and defaults');
10
+ pane.p('Every value below was in the Data before the first render.', {class: 'muted'});
11
+
12
+ const form = pane.form({class: 'card stack'});
13
+ form.html_label('Customer', {for: 'customer'});
14
+ form.input({value: '^.customer', id: 'customer'});
15
+ form.html_label('Quantity (default 1)', {for: 'quantity'});
16
+ form.input({type: 'number', value: '^.quantity', default: 1, id: 'quantity'});
17
+ form.html_label('Note (default_value wins over default)', {for: 'note'});
18
+ form.input({value: '^.note', default_value: 'No note', default: 'Never used', id: 'note'});
19
+ form.html_label('Price', {for: 'price'});
20
+ form.input({type: 'number', value: '^.price', attr_currency: 'EUR', id: 'price'});
21
+
22
+ const summary = pane.section({class: 'card stack'});
23
+ summary.h2('The Data');
24
+ const facts = summary.dl();
25
+ for (const [label, pointer, key] of [['Customer', '^.customer', 'out-customer'],
26
+ ['Quantity', '^.quantity', 'out-quantity'],
27
+ ['Note', '^.note', 'out-note'],
28
+ ['Price', '^.price', 'out-price'],
29
+ ['Currency (price?currency)', '^.price?currency', 'out-currency'],
30
+ ['Shipping city', '^.shipping.city', 'out-city']]) {
31
+ facts.dt(label);
32
+ facts.dd(pointer, {id: key});
33
+ }
34
+
35
+ pane.dataSetter({destination_path: '.customer', value: 'Ada Lovelace'});
36
+ pane.dataSetter({destination_path: '.price', value: 12.5});
37
+ const later = pane.section({class: 'muted'});
38
+ later.p('This section holds the second setters: a descendant writes after its ancestors.');
39
+ later.dataSetter({destination_path: '.customer', value: 'Grace Hopper'});
40
+ later.dataSetter({destination_path: '.shipping', value: {city: 'London', zone: 'EU'}});
41
+ }
42
+ }
@@ -0,0 +1,10 @@
1
+ # 03 · Setters and defaults
2
+
3
+ `dataSetter` writes a value in the Data. All the setters of a branch run before its first render, wherever they sit: here they come after the form and in a later section, yet the first DOM already shows their values.
4
+
5
+ - Setters run parents before descendants, in document order. Two setters write `.customer`; the one in the later section runs last, so the customer is Grace Hopper.
6
+ - A dictionary (Python) or a plain object (JavaScript) becomes a Bag: `.shipping.city` reads inside it.
7
+ - A control fills an empty value path with its `default`. `default_value` wins over `default`. Only a null or missing value is empty: `0`, `false` and `''` are values.
8
+ - `attr_currency='EUR'` sets the attribute `currency` on the Data node of the control's value, after the defaults. `^.price?currency` reads it.
9
+
10
+ **Try it:** Remove the setter in the later section: the customer becomes Ada Lovelace. Add a setter of `.quantity` with `0`: the default no longer applies.
@@ -0,0 +1,53 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ const FIELDS = [['Text', 'text', 'text'], ['Number', 'number', 'number'], ['Range', 'range', 'range'],
4
+ ['Date', 'date', 'date'], ['Color', 'color', 'color']];
5
+
6
+ export class Page extends BasePage {
7
+ static title = 'Native editing';
8
+ static css = ['/themes/gramlot-base/theme.css'];
9
+
10
+ main(root) {
11
+ const pane = root.div({class: 'example-page stack', datapath: 'edit'});
12
+ pane.h1('Native editing');
13
+ pane.html_label('Write at every keystroke (live)', {for: 'live'});
14
+ pane.input({type: 'checkbox', value: '^.live', id: 'live'});
15
+
16
+ const grid = pane.div({class: 'grid'});
17
+ const form = grid.form({class: 'card stack'});
18
+ for (const [label, key, kind] of FIELDS) {
19
+ form.html_label(label, {for: key});
20
+ form.input({type: kind, value: `^.${key}`, live: '^.live', id: key});
21
+ }
22
+ form.html_label('Note', {for: 'note'});
23
+ form.textarea({value: '^.note', live: '^.live', rows: 3, id: 'note'});
24
+ form.html_label('Size', {for: 'size'});
25
+ const size = form.select({value: '^.size', id: 'size'});
26
+ for (const option of ['S', 'M', 'L']) size.option(option, {value: option});
27
+ form.html_label('Topics', {for: 'topics'});
28
+ const topics = form.select({value: '^.topics', multiple: true, size: 3, id: 'topics'});
29
+ for (const option of ['HTML', 'SVG', 'Data']) topics.option(option, {value: option.toLowerCase()});
30
+
31
+ const values = grid.section({class: 'card stack'});
32
+ values.h2('The Data');
33
+ const facts = values.dl();
34
+ for (const [label, key] of FIELDS) {
35
+ facts.dt(label);
36
+ facts.dd(`^.${key}`, {id: `out-${key}`});
37
+ }
38
+ for (const [label, key] of [['Note', 'note'], ['Size', 'size'], ['Topics', 'topics']]) {
39
+ facts.dt(label);
40
+ facts.dd(`^.${key}`, {id: `out-${key}`});
41
+ }
42
+
43
+ pane.dataSetter({destination_path: '.live', value: false});
44
+ pane.dataSetter({destination_path: '.text', value: 'Edit me'});
45
+ pane.dataSetter({destination_path: '.number', value: 42});
46
+ pane.dataSetter({destination_path: '.range', value: 30});
47
+ pane.dataSetter({destination_path: '.date', value: '2026-09-30'});
48
+ pane.dataSetter({destination_path: '.color', value: '#456bc4'});
49
+ pane.dataSetter({destination_path: '.note', value: 'Two\nlines'});
50
+ pane.dataSetter({destination_path: '.size', value: 'M'});
51
+ pane.dataSetter({destination_path: '.topics', value: ['html', 'data']});
52
+ }
53
+ }
@@ -0,0 +1,11 @@
1
+ # 04 · Native editing
2
+
3
+ A control whose `value` is a `^` pointer shows the Data and writes it back when you edit.
4
+
5
+ - Text and textarea write a string; number and range write a number, or null when the field is empty; date writes its text such as `2026-09-30`; color writes the color string.
6
+ - A single select writes the chosen value; a multiple select writes a list.
7
+ - With `live` false a field writes at `change`, when you leave it or press Enter. With `live` true text, textarea, number and range write at every `input`. Here `live='^.live'` follows the checkbox, without rebuilding the fields.
8
+
9
+ The list on the right reads the same Data paths.
10
+
11
+ **Try it:** Type in the text field with the checkbox off, then on, and compare when the list changes.
@@ -0,0 +1,41 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ const PLANS = [['basic', 'Basic'], ['plus', 'Plus'], ['team', 'Team']];
4
+
5
+ export class Page extends BasePage {
6
+ static title = 'Checkbox and radio';
7
+ static css = ['/themes/gramlot-base/theme.css'];
8
+
9
+ main(root) {
10
+ const pane = root.div({class: 'example-page stack', datapath: 'choice'});
11
+ pane.h1('Checkbox and radio');
12
+
13
+ const options = pane.fieldset({class: 'card stack'});
14
+ options.legend('Options');
15
+ for (const [key, label] of [['newsletter', 'Newsletter'], ['invoice', 'Paper invoice']]) {
16
+ const row = options.div();
17
+ row.input({type: 'checkbox', value: `^.${key}`, id: key});
18
+ row.html_label(label, {for: key});
19
+ }
20
+
21
+ const plans = pane.fieldset({class: 'card stack'});
22
+ plans.legend('Plan');
23
+ for (const [key, label] of PLANS) {
24
+ const row = plans.div();
25
+ row.input({type: 'radio', group: 'plan', value: `^.plan.${key}`, id: key});
26
+ row.html_label(label, {for: key});
27
+ }
28
+
29
+ const values = pane.section({class: 'card stack'});
30
+ values.h2('The Data');
31
+ const facts = values.dl();
32
+ for (const key of ['newsletter', 'invoice', 'plan.basic', 'plan.plus', 'plan.team']) {
33
+ facts.dt(key);
34
+ facts.dd(`^.${key}`, {id: `out-${key.replace('.', '-')}`});
35
+ }
36
+
37
+ pane.dataSetter({destination_path: '.newsletter', value: true});
38
+ pane.dataSetter({destination_path: '.invoice', value: false});
39
+ pane.dataSetter({destination_path: '.plan.plus', value: true});
40
+ }
41
+ }
@@ -0,0 +1,9 @@
1
+ # 05 · Checkbox and radio
2
+
3
+ A checkbox bound with `value='^path'` reads a boolean and writes `true` or `false`, never the text `on`.
4
+
5
+ Radio buttons with the same `group` form one choice. Each button owns its own boolean path. Choosing one writes `true` in its path and `false` in the path of the button that was on. Gramlot generates the DOM `name` of the group, so two pages or two groups never mix.
6
+
7
+ Only `plan.plus` has a setter. `plan.basic` and `plan.team` start empty; a button that is already off is not written, so an empty path stays empty until its button is chosen once.
8
+
9
+ **Try it:** Choose Team, then Basic, and watch which Data values change; then add a fourth plan.
@@ -0,0 +1,51 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ const CHOICES = [
4
+ ['Class', 'tone', ['card', 'card status status--success', 'card status status--warning']],
5
+ ['Font size', 'size', ['1rem', '1.5rem', '2rem']],
6
+ ['Corners (rounded)', 'radius', ['0', '8px', '24px']],
7
+ ];
8
+
9
+ export class Page extends BasePage {
10
+ static title = 'Style and visibility';
11
+ static css = ['/themes/gramlot-base/theme.css'];
12
+
13
+ main(root) {
14
+ const pane = root.div({class: 'example-page stack', datapath: 'look'});
15
+ pane.h1('Style and visibility');
16
+
17
+ const controls = pane.form({class: 'card stack'});
18
+ for (const [label, key] of [['visible', 'show'], ['hidden', 'hide']]) {
19
+ const row = controls.div();
20
+ row.input({type: 'checkbox', value: `^.${key}`, id: key});
21
+ row.html_label(label, {for: key});
22
+ }
23
+ for (const [label, key] of [['Text color', 'color'], ['Background color', 'background']]) {
24
+ controls.html_label(label, {for: key});
25
+ controls.input({type: 'color', value: `^.${key}`, id: key});
26
+ }
27
+ for (const [label, key, options] of CHOICES) {
28
+ controls.html_label(label, {for: key});
29
+ const select = controls.select({value: `^.${key}`, id: key});
30
+ for (const option of options) select.option(option, {value: option});
31
+ }
32
+ controls.html_label('style', {for: 'style'});
33
+ controls.input({value: '^.style', id: 'style'});
34
+
35
+ const stage = pane.div({class: 'stack'});
36
+ stage.p('The box keeps its place when it is not visible.', {class: 'muted'});
37
+ stage.div('A styled box', {id: 'box', class: '^.tone', style: '^.style',
38
+ color: '^.color', background_color: '^.background', font_size: '^.size',
39
+ rounded: '^.radius', padding: '1rem', visible: '^.show', hidden: '^.hide'});
40
+ stage.p('This line follows the box.', {class: 'muted'});
41
+
42
+ pane.dataSetter({destination_path: '.show', value: true});
43
+ pane.dataSetter({destination_path: '.hide', value: false});
44
+ pane.dataSetter({destination_path: '.color', value: '#ffffff'});
45
+ pane.dataSetter({destination_path: '.background', value: '#456bc4'});
46
+ pane.dataSetter({destination_path: '.tone', value: 'card'});
47
+ pane.dataSetter({destination_path: '.size', value: '1.5rem'});
48
+ pane.dataSetter({destination_path: '.radius', value: '8px'});
49
+ pane.dataSetter({destination_path: '.style', value: 'border: 4px dashed #ffc400'});
50
+ }
51
+ }
@@ -0,0 +1,11 @@
1
+ # 06 · Style and visibility
2
+
3
+ Every attribute of the box is a `^` pointer, so the controls restyle it through the Data.
4
+
5
+ - `class` and `style` take a string, or null to remove the attribute.
6
+ - The style shortcuts `color`, `background_color`, `font_size`, `padding` and the macro `rounded` compose one `style` attribute with `style` itself. A shortcut wins over the same property written in `style`; a null shortcut drops only its own property.
7
+ - `visible` false sets `visibility: hidden`: the box disappears and keeps its space. `hidden` is the native attribute: the box leaves the layout.
8
+
9
+ Values need their CSS unit: `font_size='1.5rem'`, not `1.5`.
10
+
11
+ **Try it:** Write `color: red` in the style field: the shortcut `color` still wins. Then clear the visible checkbox, and afterwards check hidden.
@@ -0,0 +1,33 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Bound SVG';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+
7
+ main(root) {
8
+ const pane = root.div({class: 'example-page stack', datapath: 'drawing'});
9
+ pane.h1('Bound SVG');
10
+
11
+ const controls = pane.form({class: 'card stack'});
12
+ controls.html_label('Radius', {for: 'radius'});
13
+ controls.input({type: 'range', min: 5, max: 80, value: '^.radius', live: true, id: 'radius'});
14
+ controls.html_label('Circle fill', {for: 'fill'});
15
+ controls.input({type: 'color', value: '^.fill', id: 'fill'});
16
+ controls.html_label('Bar width', {for: 'width'});
17
+ controls.input({type: 'range', min: 10, max: 260, value: '^.width', live: true, id: 'width'});
18
+ controls.html_label('Bar fill', {for: 'bar'});
19
+ controls.input({type: 'color', value: '^.bar', id: 'bar'});
20
+
21
+ const figure = pane.figure({class: 'card'});
22
+ const art = figure.svg({viewBox: '0 0 480 200', role: 'img', aria_label: 'A circle and a bar', class: 'example-art'});
23
+ art.circle({cx: 100, cy: 100, r: '^.radius', fill: '^.fill', id: 'circle'});
24
+ art.rect({x: 200, y: 80, height: 40, rx: 6, width: '^.width', fill: '^.bar', id: 'bar-shape'});
25
+ art.text('^.radius', {x: 100, y: 190, text_anchor: 'middle', fill: 'currentColor', id: 'radius-label'});
26
+ figure.figcaption('The circle and the bar read their geometry and paint from the Data.');
27
+
28
+ pane.dataSetter({destination_path: '.radius', value: 40});
29
+ pane.dataSetter({destination_path: '.fill', value: '#ffc400'});
30
+ pane.dataSetter({destination_path: '.width', value: 160});
31
+ pane.dataSetter({destination_path: '.bar', value: '#456bc4'});
32
+ }
33
+ }
@@ -0,0 +1,7 @@
1
+ # 07 · Bound SVG
2
+
3
+ SVG attributes take `^` pointers as HTML attributes do. The circle's `r` and `fill`, the bar's `width` and `fill`, and the label text read Data paths edited by the controls.
4
+
5
+ The ranges have `live` true, so the drawing follows the slider while you drag it. SVG has no style shortcuts: `fill` and `stroke` are SVG presentation attributes.
6
+
7
+ **Try it:** Bind the circle's `cy` to a third range.
@@ -0,0 +1 @@
1
+ .actions { display: flex; flex-wrap: wrap; gap: .5rem; }
@@ -0,0 +1,26 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Freeze';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+
7
+ main(root) {
8
+ const pane = root.div({class: 'example-page stack', datapath: 'board'});
9
+ pane.h1('Freeze');
10
+ const actions = pane.div({class: 'actions'});
11
+ for (const [label, method] of [['Freeze', 'freeze'], ['Add one', 'add'],
12
+ ['Remove last item', 'removeLast'], ['Thaw', 'thaw']]) {
13
+ actions.button(label, {id: method}).dataController({func: method});
14
+ }
15
+ pane.p('^.frozen', {id: 'frozen', class: 'muted'});
16
+
17
+ const board = pane.section({node_id: 'board', class: 'card stack'});
18
+ board.h2('A branch that can be frozen');
19
+ board.p('^.count', {id: 'count'});
20
+ const items = board.ul({node_id: 'items', id: 'items'});
21
+ for (const number of [1, 2, 3]) items.li(`Item ${number}`);
22
+
23
+ pane.dataSetter({destination_path: '.count', value: 0});
24
+ pane.dataSetter({destination_path: '.frozen', value: false});
25
+ }
26
+ }
@@ -0,0 +1,11 @@
1
+ # 08 · Freeze
2
+
3
+ `renderer.freeze(node)` suspends the DOM work of a branch; `renderer.unfreeze(node)` resumes it and builds the current Source of the branch once.
4
+
5
+ The four buttons call methods of the companion `08_freeze_aux.js`, through a `dataController` nested in each button.
6
+
7
+ - While the board is frozen, **Add one** still changes the counter: the elements already built keep following the Data.
8
+ - **Remove last item** removes a Source node at once, but its `<li>` stays on screen until the thaw.
9
+ - **Thaw** builds the branch once, with every removal applied.
10
+
11
+ **Try it:** Freeze, remove two items, add one, then thaw.
@@ -0,0 +1,26 @@
1
+ /** Logic of 08_freeze.py and 08_freeze.js: each button's nested controller calls one method. */
2
+ export class Logic {
3
+ sourceNode(nodeId) {
4
+ return this.page.source.getNodeByAttr('node_id', nodeId);
5
+ }
6
+
7
+ freeze(node) {
8
+ this.page.renderer.freeze(this.sourceNode('board'));
9
+ node.SET('.frozen', true);
10
+ }
11
+
12
+ add(node) {
13
+ node.SET('.count', node.GET('.count') + 1);
14
+ }
15
+
16
+ removeLast() {
17
+ const items = this.sourceNode('items').value;
18
+ const last = items.getNodes().at(-1);
19
+ if (last) items.popNode(last.label);
20
+ }
21
+
22
+ thaw(node) {
23
+ this.page.renderer.unfreeze(this.sourceNode('board'));
24
+ node.SET('.frozen', false);
25
+ }
26
+ }
@@ -0,0 +1,8 @@
1
+ .form-grid, .form-options { display: grid; grid-template-columns: minmax(9rem, 13rem) minmax(0, 1fr); gap: .7rem 1.2rem; align-items: center; margin: 1.3rem 0; }
2
+ .form-grid legend, .form-options legend { font-weight: 700; padding: 0 .45rem; }
3
+ .form-grid input, .form-grid select, .form-grid textarea, .form-options input:not([type='checkbox']):not([type='radio']) { max-width: 28rem; }
4
+ .form-actions { display: flex; flex-wrap: wrap; gap: .75rem; }
5
+ @media (max-width: 38rem) { .form-grid, .form-options { grid-template-columns: 1fr; } }
6
+ .bound-form { display: grid; grid-template-columns: minmax(0, 2fr) minmax(12rem, 1fr); gap: 1.5rem; align-items: start; }
7
+ .bound-form dd { overflow-wrap: anywhere; }
8
+ @media (max-width: 52rem) { .bound-form { grid-template-columns: 1fr; } }
@@ -0,0 +1,79 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ const RECORD = {name: 'Ada Lovelace', quantity: 3, accent: '#456bc4', intensity: 60,
4
+ region: 'south', topics: ['html'], note: 'A native multiline field.', updates: true};
5
+ const SHOWN = ['name', 'email', 'search', 'website', 'telephone', 'quantity', 'date', 'time', 'month',
6
+ 'week', 'accent', 'intensity', 'region', 'topics', 'note', 'updates'];
7
+
8
+ export class Page extends BasePage {
9
+ static title = 'Forms with binding';
10
+ static css = ['/themes/gramlot-base/theme.css'];
11
+
12
+ main(root) {
13
+ const pane = root.div({class: 'example-page stack', datapath: 'form'});
14
+ pane.h1('Native form controls, bound');
15
+ pane.p('The same fields as HTML / SVG example 06. Every value lives in the Data record on the right.');
16
+ const layout = pane.div({class: 'bound-form'});
17
+ const form = layout.form();
18
+ const identity = form.fieldset({class: 'form-grid'});
19
+ identity.legend('Identity');
20
+ this.field(identity, 'Name', 'name', 'text', {required: true});
21
+ this.field(identity, 'Email', 'email', 'email', {placeholder: 'name@example.org'});
22
+ this.field(identity, 'Search', 'search', 'search', {placeholder: 'Find a topic'});
23
+ this.field(identity, 'Website', 'website', 'url', {placeholder: 'https://example.org'});
24
+ this.field(identity, 'Telephone', 'telephone', 'tel', {placeholder: '+1 555 0100'});
25
+ this.field(identity, 'Password', 'password', 'password', {minlength: 8});
26
+
27
+ const choices = form.fieldset({class: 'form-grid'});
28
+ choices.legend('Choices and ranges');
29
+ this.field(choices, 'Quantity', 'quantity', 'number', {min: 1, max: 20});
30
+ this.field(choices, 'Date', 'date', 'date');
31
+ this.field(choices, 'Time', 'time', 'time');
32
+ this.field(choices, 'Month', 'month', 'month');
33
+ this.field(choices, 'Week', 'week', 'week');
34
+ this.field(choices, 'Accent', 'accent', 'color');
35
+ this.field(choices, 'Intensity', 'intensity', 'range', {min: 0, max: 100, step: 5, live: true});
36
+ choices.html_label('Region', {for: 'region'});
37
+ const select = choices.select({id: 'region', name: 'region', value: '^.region'});
38
+ for (const value of ['North', 'South', 'West']) select.option(value, {value: value.toLowerCase()});
39
+ choices.html_label('Topics (multiple)', {for: 'topics'});
40
+ const topics = choices.select({id: 'topics', name: 'topics', multiple: true, size: 3, value: '^.topics'});
41
+ for (const topic of ['HTML', 'SVG', 'Source']) topics.option(topic, {value: topic.toLowerCase()});
42
+ choices.html_label('Note', {for: 'note'});
43
+ choices.textarea({id: 'note', name: 'note', rows: 3, value: '^.note'});
44
+
45
+ const options = form.fieldset({class: 'form-options'});
46
+ options.legend('Flags and states');
47
+ options.html_label('Receive updates', {for: 'updates'});
48
+ options.input({type: 'checkbox', id: 'updates', name: 'updates', value: '^.updates'});
49
+ for (const value of ['daily', 'weekly']) {
50
+ options.html_label(value[0].toUpperCase() + value.slice(1), {for: value});
51
+ options.input({type: 'radio', id: value, group: 'frequency', value: `^.frequency.${value}`});
52
+ }
53
+ options.html_label('Read-only code', {for: 'code'});
54
+ options.input({type: 'text', id: 'code', value: 'DEMO-01', readonly: true});
55
+ options.html_label('Unavailable choice', {for: 'unavailable'});
56
+ options.input({type: 'text', id: 'unavailable', value: 'Disabled', disabled: true});
57
+ options.html_label('Attachment', {for: 'attachment'});
58
+ options.input({type: 'file', id: 'attachment', name: 'attachment'});
59
+ form.button('Submit unavailable', {type: 'submit', disabled: true});
60
+
61
+ const record = layout.aside({class: 'card stack'});
62
+ record.h2('The Data record');
63
+ const facts = record.dl();
64
+ for (const key of SHOWN) {
65
+ facts.dt(key);
66
+ facts.dd(`^.${key}`, {id: `out-${key}`});
67
+ }
68
+ facts.dt('frequency.weekly');
69
+ facts.dd('^.frequency.weekly', {id: 'out-weekly'});
70
+
71
+ pane.dataSetter({destination_path: 'form', value: RECORD});
72
+ pane.dataSetter({destination_path: '.frequency.weekly', value: true});
73
+ }
74
+
75
+ field(parent, label, key, kind, attrs = {}) {
76
+ parent.html_label(label, {for: key});
77
+ parent.input({type: kind, id: key, name: key, value: `^.${key}`, ...attrs});
78
+ }
79
+ }
@@ -0,0 +1,12 @@
1
+ # 09 · Forms with binding
2
+
3
+ The fieldsets of HTML / SVG example 06, with each `value` bound to a field of the Data record `form`.
4
+
5
+ - One `dataSetter` writes the whole record from a dictionary (Python) or a plain object (JavaScript); a second setter adds `frequency.weekly`.
6
+ - Each field writes its own type: numbers for quantity and intensity, a list for the topics, booleans for the checkbox and the two radio buttons.
7
+ - The record on the right reads the same paths, so it changes while you edit. Intensity has `live` true and follows the slider.
8
+ - The password is bound but not shown, and the file field stays native: a file has no Data value.
9
+
10
+ **Limit:** Reset is not offered: a native reset changes the fields without an input event, so the Data would not follow.
11
+
12
+ **Try it:** Compare this page with example 06: the fieldsets are the same, and the initial values moved from the attributes to the Data record.
@@ -0,0 +1,42 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'SVG shapes with binding';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+
7
+ main(root) {
8
+ const pane = root.div({class: 'example-page stack', datapath: 'shapes'});
9
+ pane.h1('SVG shape vocabulary, bound');
10
+ pane.p('The six shapes of HTML / SVG example 08. Their size, stroke, color and label come from the Data.');
11
+ const controls = pane.form({class: 'card stack'});
12
+ for (const [label, key, low, high] of [['Rectangle width', 'width', 20, 95], ['Circle radius', 'radius', 10, 45],
13
+ ['Ellipse height', 'ry', 10, 45], ['Stroke width', 'stroke', 1, 14]]) {
14
+ controls.html_label(label, {for: key});
15
+ controls.input({type: 'range', min: low, max: high, value: `^.${key}`, live: true, id: key});
16
+ }
17
+ controls.html_label('Triangle color', {for: 'triangle'});
18
+ controls.input({type: 'color', value: '^.triangle', id: 'triangle'});
19
+ controls.html_label('Label', {for: 'label'});
20
+ controls.input({value: '^.label', live: true, id: 'label'});
21
+
22
+ const figure = pane.figure({class: 'card'});
23
+ const art = figure.svg({viewBox: '0 0 480 200', role: 'img', aria_labelledby: 'shapes-title', class: 'example-art'});
24
+ art.title('Six geometric shapes', {id: 'shapes-title'});
25
+ art.rect({x: 15, y: 20, width: '^.width', height: 75, rx: 14, fill: 'var(--gramlot-action)', id: 'rect'});
26
+ art.circle({cx: 172, cy: 58, r: '^.radius', fill: 'var(--gramlot-warning)', id: 'circle'});
27
+ art.ellipse({cx: 263, cy: 58, rx: 26, ry: '^.ry', fill: 'var(--gramlot-selected)', id: 'ellipse'});
28
+ art.line({x1: 294, y1: 20, x2: 315, y2: 98, stroke: 'currentColor', stroke_width: '^.stroke', stroke_linecap: 'round'});
29
+ art.polyline({points: '325,95 345,25 365,70 385,25 405,95', fill: 'none', stroke: 'var(--gramlot-success)',
30
+ stroke_width: '^.stroke', stroke_linejoin: 'round', id: 'polyline'});
31
+ art.polygon({points: '430,95 450,20 470,95', fill: '^.triangle', id: 'polygon'});
32
+ art.text('^.label', {x: 18, y: 150, fill: 'currentColor', font_size: 17, id: 'caption'});
33
+ figure.figcaption('The shape attributes are pointers into the Data.');
34
+
35
+ pane.dataSetter({destination_path: '.width', value: 95});
36
+ pane.dataSetter({destination_path: '.radius', value: 38});
37
+ pane.dataSetter({destination_path: '.ry', value: 38});
38
+ pane.dataSetter({destination_path: '.stroke', value: 7});
39
+ pane.dataSetter({destination_path: '.triangle', value: '#456bc4'});
40
+ pane.dataSetter({destination_path: '.label', value: 'rectangle · circle · ellipse · line · polyline · polygon'});
41
+ }
42
+ }
@@ -0,0 +1,9 @@
1
+ # 10 · SVG shapes with binding
2
+
3
+ The SVG of HTML / SVG example 08, with six attributes turned into `^` pointers.
4
+
5
+ - The ranges change the rectangle width, the circle radius, the ellipse height and one `stroke_width` shared by the line and the polyline.
6
+ - The color field paints the triangle; the text field rewrites the label.
7
+ - Every control has `live` true, so the drawing follows while you drag or type.
8
+
9
+ **Try it:** Bind the circle's `cx` to a new range and move the circle along the row.
@@ -0,0 +1,5 @@
1
+ .complete-page { display: grid; gap: 2rem; }
2
+ .complete-header { display: grid; gap: .55rem; }
3
+ .complete-header nav { display: flex; flex-wrap: wrap; gap: 1rem; padding-top: .5rem; }
4
+ .complete-hero { display: grid; gap: 1rem; text-align: center; }
5
+ .complete-grid { grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); }