@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
@@ -16,20 +16,50 @@ export class GalleryPage extends IndexPage {
16
16
  intro.p('This gallery shows each example page beside the code that creates it. ' +
17
17
  'The Python and JavaScript integrations have the same examples.');
18
18
  intro.p('Choose an example from the list. Its tab stays open so you can return to it. ' +
19
- 'Enable Keyboard navigation in the footer to move through tabs by keyboard. ' +
20
- 'Drag the dividers to resize the sidebar or the preview and code. ' +
21
- 'Choose Base or Compact independently of the color mode.');
19
+ 'Open Settings (⚙) in the footer and enable Keyboard navigation to move through tabs by keyboard. ' +
20
+ 'Use the top bar to show or hide Navigation and Code; drag their dividers to resize them. ' +
21
+ 'Choose Base, Compact, mimi or joanna independently of the color mode.');
22
22
 
23
23
  }
24
24
  groupPanel(pane, group) { pane.div(group.readme, {id: `readme-${group.key}`}); }
25
25
  footer(footer) {
26
- super.footer(footer);
27
- const inspector = footer.button({type: 'button', id: 'gallery-inspector', class: 'gallery-inspector',
26
+ footer.html_button('⚙', {type: 'button', id: 'gallery-settings-toggle', class: 'gallery-settings-toggle',
27
+ title: 'Settings', aria_label: 'Settings', popovertarget: 'gallery-settings'});
28
+ const settings = footer.div({id: 'gallery-settings', popover: 'auto', aria_labelledby: 'gallery-settings-title'});
29
+ const heading = settings.div({class: 'gallery-settings-heading'});
30
+ heading.h2('Settings', {id: 'gallery-settings-title'});
31
+ heading.html_button('×', {type: 'button', aria_label: 'Close settings', popovertarget: 'gallery-settings', popovertargetaction: 'hide'});
32
+ super.footer(settings);
33
+ const labels = settings.div({class: 'index-theme'});
34
+ labels.html_label('Label position', {for: 'gallery-label-position'});
35
+ const position = labels.select({id: 'gallery-label-position', value: 'example'});
36
+ position.option('As in the example', {value: 'example', selected: true});
37
+ for (const code of ['TL', 'TC', 'TR', 'L', 'R', 'BL', 'BC', 'BR']) position.option(code, {value: code});
38
+ settings.p("Label position applies to input components in the previews. Choose As in the example to restore the lesson's layout.");
39
+ const inspector = footer.html_button({type: 'button', id: 'gallery-inspector', class: 'gallery-inspector',
28
40
  title: 'Open inspector', aria_label: 'Inspector', aria_pressed: 'false', disabled: true});
29
41
  inspector.span('🔍', {aria_hidden: 'true'});
30
42
 
31
43
  }
32
- behavior(shell) { shell.script({src: this.constructor.galleryScript}); }
44
+ header(shell) {
45
+ const header = shell.header({class: 'gallery-toolbar'});
46
+ header.html_button('☰ Navigation', {type: 'button', id: 'index-navigation-toggle',
47
+ aria_controls: 'index-sidebar', aria_expanded: 'true'});
48
+ header.h1(this.constructor.title);
49
+ header.html_button('Code ⟨/⟩', {type: 'button', id: 'index-code-toggle',
50
+ aria_controls: 'index-code', aria_expanded: 'false', disabled: true});
51
+ }
52
+ behavior(shell) {
53
+ shell.div({class: 'index-divider', id: 'index-code-divider', hidden: true,
54
+ title: 'Drag to resize code', role: 'separator', aria_orientation: 'vertical',
55
+ tabindex: 0, aria_label: 'Resize code', aria_controls: 'index-code',
56
+ aria_valuemin: 200, aria_valuemax: 800, aria_valuenow: 420});
57
+ const code = shell.aside({id: 'index-code', aria_label: 'Example source code', hidden: true});
58
+ for (const family of this.constructor.navigation) {
59
+ for (const example of family.children) this.codePanel(code, example);
60
+ }
61
+ shell.script({src: this.constructor.galleryScript});
62
+ }
33
63
 
34
64
  panel(parent, example) {
35
65
  const key = example.key;
@@ -38,21 +68,23 @@ export class GalleryPage extends IndexPage {
38
68
  const explanation = panel.header({class: 'gallery-explanation'});
39
69
  explanation.div(example.readme, {id: `readme-${key}`});
40
70
 
41
- const split = panel.div({class: 'gallery-split', id: `split-${key}`,
42
- style: '--split-position: 65%'});
43
- const preview = split.div({class: 'gallery-preview', id: `preview-${key}`});
44
- preview.iframe({title: `${example.title} preview`, name: `example-${key}`,
71
+ const preview = panel.div({class: 'gallery-preview', id: `preview-${key}`});
72
+ const modes = preview.div({class: 'gallery-preview-modes', role: 'group', aria_label: 'Preview mode'});
73
+ for (const [mode, caption] of [['normal', 'Normale'], ['mobile', 'Mobile']]) {
74
+ modes.html_button(caption, {type: 'button', id: `preview-${mode}-${key}`, aria_pressed: mode === 'normal' ? 'true' : 'false'});
75
+ }
76
+ const stage = preview.div({class: 'gallery-preview-stage', id: `preview-stage-${key}`});
77
+ stage.iframe({title: `${example.title} preview`, name: `example-${key}`,
45
78
  id: `frame-${key}`});
46
- split.div({class: 'gallery-divider', id: `divider-${key}`,
47
- role: 'separator', aria_orientation: 'vertical', tabindex: 0,
48
- aria_label: 'Resize preview and code', aria_valuemin: 20,
49
- aria_valuemax: 80, aria_valuenow: 65});
50
- const codepane = split.div({class: 'gallery-codepane', id: `codepane-${key}`});
79
+ }
80
+ codePanel(parent, example) {
81
+ const key = example.key;
82
+ const codepane = parent.div({class: 'gallery-codepane', id: `codepane-${key}`, hidden: true});
51
83
  const tabs = codepane.div({class: 'gallery-source-tabs', role: 'tablist', aria_label: 'Source files'});
52
- tabs.button('JavaScript', {title: `${example.folder}.js`, type: 'button', role: 'tab',
84
+ tabs.html_button('JavaScript', {title: `${example.folder}.js`, type: 'button', role: 'tab',
53
85
  id: `code-tab-${key}`, aria_controls: `code-panel-${key}`, aria_selected: 'true', tabindex: 0});
54
86
  if (example.logic != null) {
55
- tabs.button('logic.js', {hidden: true, title: `${example.folder}_aux.js`, type: 'button', role: 'tab',
87
+ tabs.html_button('logic.js', {hidden: true, title: `${example.folder}_aux.js`, type: 'button', role: 'tab',
56
88
  id: `logic-tab-${key}`, aria_controls: `logic-panel-${key}`, aria_selected: 'false', tabindex: -1});
57
89
  }
58
90
  codepane.pre({id: `code-panel-${key}`, role: 'tabpanel', aria_labelledby: `code-tab-${key}`, tabindex: 0})
@@ -1,3 +1,25 @@
1
+ /* The source viewer is a sibling of navigation and content, never an iframe overlay. */
2
+ #gallery.index-shell {
3
+ grid-template-rows: auto minmax(0, 1fr) auto;
4
+ grid-template-columns: var(--navigation-size, min(var(--sidebar-width, 240px), 60vw))
5
+ var(--navigation-divider, 8px) minmax(0, 1fr) var(--code-divider, 0px) var(--code-size, 0px);
6
+ }
7
+ .gallery-toolbar { grid-column: 1 / -1; grid-row: 1; display: flex; align-items: center;
8
+ gap: .6rem; padding: .35rem .6rem; background: var(--gramlot-surface-subtle);
9
+ border-block-start: 3px solid var(--gramlot-brand-yellow); border-block-end: 1px solid var(--gramlot-border); }
10
+ .gallery-toolbar h1 { flex: 1; min-inline-size: 0; margin: 0; font-size: 1rem;
11
+ text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
12
+ .gallery-toolbar button { margin: 0; padding: .25rem .5rem; min-block-size: 28px; font-size: .875rem; }
13
+ #gallery > .index-sidebar { grid-column: 1; grid-row: 2; border-block-start: 0; }
14
+ #gallery > .index-sidebar header h1 { display: none; }
15
+ #gallery > #index-sidebar-divider { grid-column: 2; grid-row: 2; }
16
+ #gallery > .index-main { grid-column: 3; grid-row: 2; }
17
+ #gallery > #index-code-divider { grid-column: 4; grid-row: 2; }
18
+ #index-code { grid-column: 5; grid-row: 2; min-inline-size: 0; min-block-size: 0; overflow: hidden; background: #1e1e1e; }
19
+ #gallery > .index-footer { grid-row: 3; }
20
+ #gallery[data-navigation-hidden="true"] { --navigation-size: 0px; --navigation-divider: 0px; }
21
+ #gallery[data-code-visible="true"] { --code-size: min(var(--code-width, 420px), 60vw); --code-divider: 8px; }
22
+ #gallery [hidden] { display: none; }
1
23
  .gallery-panel { block-size: 100%; min-block-size: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); }
2
24
  .gallery-explanation { max-block-size: 28dvh; overflow: auto; padding: .5rem .75rem; background: var(--gramlot-surface); border-block-end: 1px solid var(--gramlot-border); font-size: 1rem; }
3
25
  .gallery-explanation h1 { font-size: 1.25rem; margin-block: 0 .35rem; }
@@ -7,11 +29,10 @@
7
29
  .gallery-explanation :last-child { margin-block-end: 0; }
8
30
  .gallery-description { font-weight: 600; }
9
31
  .gallery-resources { display: flex; gap: .6rem; font-size: .95rem; }
10
- .gallery-split { min-inline-size: 0; min-block-size: 0; display: grid; grid-template-columns: minmax(0, calc(var(--split-position) - 4px)) 8px minmax(0, 1fr); background: var(--gramlot-surface); }
11
32
  .gallery-preview { min-inline-size: 0; min-block-size: 0; overflow: hidden; }
12
33
  .gallery-preview iframe { display: block; inline-size: 100%; block-size: 100%; min-block-size: 0; border: 0; background: var(--gramlot-surface); }
13
- .gallery-codepane { min-inline-size: 0; min-block-size: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); background: #1e1e1e; }
14
- .gallery-codepane pre { min-inline-size: 0; min-block-size: 0; margin: 0; padding: .4rem .4rem .4rem 0; border: 0; border-radius: 0; overflow: auto; font: 12px/1.4 var(--gramlot-font-code); background: #1e1e1e; color: #dcdcdc; }
34
+ .gallery-codepane { block-size: 100%; min-inline-size: 0; min-block-size: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); background: #1e1e1e; }
35
+ .gallery-codepane pre { min-inline-size: 0; min-block-size: 0; margin: 0 8px 8px; padding: 12px 16px 16px 10px; border: 0; border-radius: 0; overflow: auto; font: 11px/1.5 var(--gramlot-font-code); background: #1e1e1e; color: #dcdcdc; }
15
36
  .gallery-codepane code { white-space: pre; font-size: inherit; }
16
37
  .gallery-codepane .hljs { color: #dcdcdc; counter-reset: code-line; }
17
38
  .gallery-codepane .code-line::before { counter-increment: code-line; content: counter(code-line); display: inline-block; inline-size: 2.5em; margin-inline-end: .75em; padding-inline-end: .25em; text-align: end; color: #6e7681; user-select: none; }
@@ -28,7 +49,7 @@
28
49
  .gallery-codepane :is(.hljs-attr, .hljs-attribute) { color: #9cdcfe; }
29
50
  .gallery-codepane > pre[hidden] { display: none; }
30
51
  .gallery-source-tabs { display: flex; min-inline-size: 0; overflow-x: auto; background: #2d2d30; }
31
- .gallery-source-tabs button { flex: none; min-block-size: 28px; margin: 0; padding: .2rem .6rem; border: 0; border-radius: 0; background: transparent; color: #ccc; font: 600 12px/1.35 var(--gramlot-font-code); white-space: nowrap; }
52
+ .gallery-source-tabs button { flex: none; min-block-size: 28px; margin: 0; padding: .2rem .6rem; border: 0; border-radius: 0; background: transparent; color: #ccc; font: 600 11px/1.35 var(--gramlot-font-code); white-space: nowrap; }
32
53
  .gallery-source-tabs button[aria-selected="true"] { background: #1e1e1e; color: #fff; box-shadow: inset 0 2px var(--gramlot-brand-blue); }
33
54
  .gallery-source-tabs button:focus-visible { outline: 2px solid var(--gramlot-focus-ring); outline-offset: -2px; }
34
55
  .gallery-source-tabs button[hidden] { display: none; }
@@ -37,3 +58,28 @@
37
58
  .gallery-inspector:hover { background: var(--gramlot-surface); }
38
59
  .gallery-inspector:focus-visible { outline: 2px solid var(--gramlot-focus-ring); outline-offset: 2px; }
39
60
  .gallery-inspector[aria-pressed="true"] { background: var(--gramlot-action); color: var(--gramlot-navigation-text); }
61
+
62
+ /* Native popover keeps settings out of both the footer and lesson layout. */
63
+ .gallery-settings-toggle { inline-size: 28px; block-size: 28px; min-block-size: 28px;
64
+ padding: 0; margin: 0; border: 0; background: transparent; color: inherit; font-size: 18px; }
65
+ #gallery-settings { inset: auto auto 42px 10px; margin: 0; box-sizing: border-box;
66
+ inline-size: min(320px, calc(100vw - 20px)); max-block-size: calc(100dvh - 60px); overflow: auto;
67
+ padding: 12px; border: 1px solid var(--gramlot-border); border-radius: var(--gramlot-radius);
68
+ background: var(--gramlot-surface); color: var(--gramlot-ink); box-shadow: 0 4px 20px #0003; }
69
+ #gallery-settings:popover-open { display: grid; gap: 10px; }
70
+ .gallery-settings-heading { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
71
+ .gallery-settings-heading h2 { margin: 0; font-size: 1rem; }
72
+ .gallery-settings-heading button { padding: 0 6px; min-block-size: 24px; font-size: 1rem; }
73
+ #gallery-settings .index-theme { display: grid; gap: 4px; }
74
+ #gallery-settings .index-theme select { inline-size: 100%; }
75
+ #gallery-settings .index-install { order: 2; }
76
+ #gallery-settings .index-install[open] { position: static; }
77
+ #gallery-settings p { margin: 0; font-size: .85rem; line-height: 1.4; }
78
+
79
+ /* Resize the existing preview; switching modes never replaces the iframe. */
80
+ .gallery-preview{display:grid;grid-template-rows:auto minmax(0,1fr)}
81
+ .gallery-preview-modes{display:flex;gap:4px;border-block-end:1px solid var(--gramlot-border);background:var(--gramlot-surface)}
82
+ .gallery-preview-modes button{font:inherit;font-size:12px;background:transparent;color:var(--gramlot-text-secondary);border:0;border-radius:0;border-block-end:2px solid transparent;padding:5px 12px;min-block-size:28px}
83
+ .gallery-preview-modes button[aria-pressed=true]{color:var(--gramlot-action);border-block-end-color:currentColor}
84
+ .gallery-preview-stage{min-block-size:0;min-inline-size:0;overflow:hidden;background:var(--gramlot-surface-subtle);display:flex;justify-content:center}
85
+ .gallery-preview-stage[data-preview-mode=mobile] iframe{inline-size:390px;max-inline-size:100%;flex:none;border-inline:1px solid var(--gramlot-border)}
@@ -1,3 +1,4 @@
1
+ import {collections} from './gallery/collections.js';
1
2
  /** Gramlot example pages, the gallery page and its catalogue. */
2
3
  import {existsSync, readFileSync} from 'node:fs';
3
4
  import {basename, extname, join} from 'node:path';
@@ -6,7 +7,7 @@ import {fileURLToPath} from 'node:url';
6
7
  const PACKAGE = fileURLToPath(new URL('./', import.meta.url));
7
8
  const MEDIA_TYPES = {'.css': 'text/css', '.svg': 'image/svg+xml', '.png': 'image/png', '.json': 'application/json'};
8
9
  const SUFFIXES = ['.py', '.js', '.css', '.md', '_aux.js'];
9
- const DIST = ['gallery.js', 'frame.js', 'notices.json', 'LICENSE', 'NOTICE'];
10
+ const DIST = ['gallery.js', 'frame.js', 'inputs.js', 'layout.js', 'notices.json', 'LICENSE', 'NOTICE'];
10
11
 
11
12
  /** Media type of a gallery asset; only the built gallery scripts are served as JavaScript. */
12
13
  function mediaType(path, script = false) {
@@ -77,5 +78,5 @@ export function buildGallery({catalogs = []} = {}) {
77
78
  }
78
79
  }
79
80
  }
80
- return {families, routes, assets};
81
+ return {families, routes, assets, collections};
81
82
  }
@@ -1,17 +1,20 @@
1
1
  # Gramlot examples
2
2
 
3
3
  The [gallery](https://github.com/gramlot-org/gramlot-examples/blob/main/docs/010-gallery.md) presents the same examples in Python and
4
- JavaScript, in three families:
4
+ JavaScript, in four families:
5
5
 
6
6
  - [`html_svg/`](html_svg/): thirteen HTML and SVG pages, without binding.
7
7
  - [`binding/`](binding/): eleven pages whose DOM follows the Data through pointers,
8
8
  editing, booleans, style, SVG and freeze. The last three take the content of
9
9
  `html_svg` examples 06, 08 and 12 and add the binding.
10
- - [`controllers/`](controllers/): seventeen pages with formulas, controllers, named
10
+ - [`controllers/`](controllers/): twenty-two pages with formulas, controllers, named
11
11
  logic in the `Logic` class of the page module, inline expressions, node methods, buttons,
12
12
  events, the end-to-end story, `dataRpc` calls to the endpoints of the page, `remote`
13
13
  fragments and `dataRemote` values.
14
14
 
15
+ - [`components/`](components/): twenty-five lessons for inputs, labelbox, layout,
16
+ command buttons and validation, with compact desktop/touch presentation.
17
+
15
18
  Each example is a file page: `NN_name.py`, its JavaScript equivalent `NN_name.js`,
16
19
  its README `NN_name.md`, and where needed a same-name stylesheet `NN_name.css`.
17
20
  `NN_name.js` exports `Page` and `Logic`: the JavaScript page uses both, the Python
@@ -16,29 +16,94 @@ Controls change an element’s appearance and visibility. You learn how Data dri
16
16
 
17
17
  **Open and edit:** select **b06 · Style and visibility** 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
18
 
19
- ## Read the page
19
+ ## From Python to Source to DOM
20
20
 
21
- The box binds class, style, color, background, size, radius and visibility to Data. Supported class/style values are strings or null. CSS shortcuts compose the style and take precedence over a duplicate property in `style`.
21
+ The controls above change Data. The preview box reads that Data through its bindings.
22
+ Follow three steps side by side; these are **excerpts of the existing page**, not
23
+ additional declarations to paste into it. Source cells list attributes, not serialized
24
+ Bags; DOM cells show only relevant CSS and omit other styles/classes. Browsers may
25
+ normalize colors to `rgb(...)` and change declaration spacing/order.
22
26
 
23
- `visible=False` sets hidden visibility but retains space; native `hidden=True` removes the box from layout. Use CSS units such as `1.5rem` for font size.
27
+ | Python declaration | Source attributes | DOM inline style at startup |
28
+ | --- | --- | --- |
29
+ | `color="^.color"` | `color: "^.color"` | `color: #ffffff` |
30
+ | `background_color="^.background"` | `background_color: "^.background"` | `background-color: #456bc4` |
31
+ | `font_size="^.size"` | `font_size: "^.size"` | `font-size: 1.5rem` |
32
+ | `padding="1rem"` | `padding: "1rem"` | `padding: 1rem` |
33
+ | `style="^.style"` | `style: "^.style"` | `border: 4px dashed #ffc400` |
24
34
 
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.
35
+ All these attributes belong to the **same** preview div. Its enclosing pane has
36
+ `datapath="look"`, so `^.color` follows `look.color`. The renderer combines the
37
+ resolved values into one HTML `style` attribute. It does not put a `color` HTML
38
+ attribute on the div or replace the Source pointer with its current value.
26
39
 
27
- ## Try it and predict the result
40
+ For comparison, a literal declaration has no Data dependency:
28
41
 
29
- 1. Expect a blue box, white text, dashed border, 1.5rem font and rounded corners.
30
- 2. Replace the style field with `color: red` and leave it: the color shortcut still makes the text white; replacing the style string also removes its old border declaration.
31
- 3. Uncheck visible: the space remains. Then check hidden: the space collapses.
42
+ | Python | Source attributes | DOM |
43
+ | --- | --- | --- |
44
+ | `pane.div("Hello", color="red", font_size="18px")` | `color: "red"`, `font_size: "18px"` | `<div style="color: red; font-size: 18px;">Hello</div>` |
32
45
 
33
- ## Change it and check yourself
46
+ This comparison uses the literal styling foundation; the controls in this lesson
47
+ add the core 0.2.0 Data binding layer. No later controller or component feature is needed.
34
48
 
35
- Restore visible, clear hidden and choose 2rem with 24px corners.
49
+ ## Try the binding
36
50
 
37
- **Answer:** the same box changes appearance from Data; its text and Source structure are unchanged. A null shortcut removes its own property, not every other style declaration.
51
+ 1. Choose a new **Text color**, for example `#ff0000`.
52
+ 2. The color input writes its value to `look.color`.
53
+ 3. The preview's `color="^.color"` reads that value and becomes red.
54
+ 4. Inspect its Source: `color` still contains `^.color`. Inspect the preview element
55
+ in browser DevTools: the inline style now has the resolved red color.
56
+
57
+ The input writes Data; the div displays it. This is not the div's CSS writing back
58
+ to Data. Python built the Source; these edits and projections execute in the browser.
59
+
60
+ ## Which style wins?
61
+
62
+ Restore **Text color** to white, then replace the **style** field with
63
+ `color: red; border: 4px dashed #ffc400` and leave the field to commit the edit.
64
+
65
+ | Declaration retained in Source | Current Data | Resulting inline style (excerpt) |
66
+ | --- | --- | --- |
67
+ | `style="^.style"`, `color="^.color"` | `look.style = "color: red; border: 4px dashed #ffc400"`; `look.color = "#ffffff"` | `color: #ffffff; border: 4px dashed #ffc400` |
68
+
69
+ **The text stays white:** the explicit `color` shortcut wins over the same property
70
+ inside `style`. The border survives because it has no conflicting shortcut. Now
71
+ replace the style field with only `color: red`: the border disappears, while text
72
+ stays white. Replacing the style string removes its old declarations; other shortcut
73
+ properties still compose into the result.
74
+
75
+ This is Gramlot's composition of inline style. The browser then applies the CSS
76
+ cascade: an applicable `!important` stylesheet rule can change the computed result.
77
+ A resolved null shortcut removes its property; it does not restore an overwritten
78
+ property from the style string. Class/style values accept strings or null, not objects.
79
+
80
+ ## What about the reverse direction?
81
+
82
+ As a temporary debugging exercise, select the preview's `#box` element in browser
83
+ DevTools and change its inline `color` to green. Source still has `^.color`, and
84
+ Data still has the selected color: **editing CSS does not update either one**.
85
+ Choose another Text color in the page: the Data-driven projection takes over again.
86
+ Reloading also rebuilds the page from its declarations.
87
+
88
+ Editing a literal Source attribute through the Gramlot inspector does project it to
89
+ the DOM, but does not save it to the Python/JS source file. The inspector's **Styles**
90
+ tab previews theme variables, not reverse conversion of element CSS into Source;
91
+ use **Download CSS** there to export applied theme overrides. Computed CSS may come
92
+ from classes, inheritance or variables, so there is no single reverse mapping.
93
+
94
+ ## Visibility and a small exercise
95
+
96
+ Uncheck **visible**: the box keeps its space. Restore it and check **hidden**: the
97
+ space collapses. Restore visible and clear hidden, then choose **2rem** font size
98
+ and **24px** corners. The same box changes appearance; its Source structure stays
99
+ unchanged. Use explicit CSS units for sizes.
100
+
101
+ For the broader theme and cascade explanation, see
102
+ [Python styles and a reactive example](https://github.com/gramlot-org/gramlot/blob/main/docs/public/096-themes-and-css.md#gc-096-040).
38
103
 
39
104
  ## Limits and next step
40
105
 
41
- Stay with the agreed string/null contract. The development review has a pending renderer-parity correction for rejection of unsupported class/style types; this lesson does not teach object coercions or rely on them.
106
+ Stay with the agreed string/null contract. This lesson does not teach object coercions or rely on them.
42
107
 
43
108
  Continue with [b07 · Bound SVG](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/07_bound_svg.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) explains the full contract.
44
109
 
@@ -56,7 +56,7 @@ export class Page extends BasePage {
56
56
  options.input({type: 'text', id: 'unavailable', value: 'Disabled', disabled: true});
57
57
  options.html_label('Attachment', {for: 'attachment'});
58
58
  options.input({type: 'file', id: 'attachment', name: 'attachment'});
59
- form.button('Submit unavailable', {type: 'submit', disabled: true});
59
+ form.html_button('Submit unavailable', {type: 'submit', disabled: true});
60
60
 
61
61
  const record = layout.aside({class: 'card stack'});
62
62
  record.h2('The Data record');
@@ -0,0 +1,32 @@
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 = "Textbox and labelbox";
6
+ static css = ["/themes/gramlot-base/theme.css"];
7
+
8
+ main(root) {
9
+ const pane = root.main({class: "example-page stack"});
10
+ pane.h1("One value, two textboxes");
11
+ pane.p("Type in either field. Both share the same Data value.");
12
+ const controls = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
13
+ controls.html_label("Label position", {for: "position"});
14
+ const position = controls.select({id: "position", value: "^position", width: "8rem", max_width: "100%"});
15
+ for (const code of ["TL", "TC", "TR", "L", "R", "BL", "BC", "BR"]) {
16
+ position.option(code, {value: code});
17
+ }
18
+ controls.checkbox({value: "^live", lbl: "Editing", label: "Update while typing"});
19
+ controls.checkbox({value: "^readonly", lbl: "State", label: "Read only"});
20
+ const fields = pane.formlet({class: "card stack", width: "100%", max_width: "32rem"});
21
+ fields.textbox({id: "customer", value: "^customer", lbl: "Customer", lbl_position: "^position", live: "^live", readonly: "^readonly", placeholder: "Enter a name", lbl_color: "#315fa8", box_padding: "8px", box_l_padding: "4px", box_c_width: "240px"});
22
+ fields.textbox({id: "customer-copy", value: "^customer", lbl: "Same customer", live: "^live", readonly: "^readonly"});
23
+ pane.h2("The Data");
24
+ pane.p("^customer", {id: "customer-value"});
25
+ for (const [path, value] of [["customer", "Ada"], ["position", "TL"], ["live", true], ["readonly", false]]) {
26
+ pane.dataSetter({destination_path: path, value});
27
+ }
28
+ }
29
+ }
30
+
31
+ /** This page needs no named client methods. */
32
+ export class Logic {}
@@ -0,0 +1,14 @@
1
+ # 01 · Textbox and labelbox
2
+
3
+ Two textboxes share a customer name. Type in either one, switch live editing off and on, and move the label without rebuilding the field.
4
+
5
+ **Uses:** the first 0.3 input components, shared labelbox, Source, Data and bindings. No endpoints or validation engine.
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+ `lbl_` styles the label; `box_` styles the whole labelbox. `box_l_` and `box_c_` address its label and control containers. Read only stops editing while Data updates can still reach the field.
11
+
12
+ **Try it:** choose each of `TL`, `TC`, `TR`, `L`, `R`, `BL`, `BC`, `BR`. The default is `TL`. Open the inspector to see the same values in Data.
13
+
14
+ </details>
@@ -0,0 +1,25 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = "Checkbox 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("A boolean value");
10
+ pane.p("lbl names the field; label is the caption beside its checkbox.");
11
+ const fields = pane.formlet({class: "card stack", width: "100%", max_width: "32rem"});
12
+ fields.checkbox({id: "newsletter", value: "^newsletter", lbl: "Contact preferences", label: "Send me the newsletter", lbl_color: "#315fa8", box_padding: "8px", disabled: "^locked"});
13
+ fields.checkbox({id: "newsletter-copy", value: "^newsletter", lbl: "Same preference", label: "Newsletter", disabled: "^locked"});
14
+ const controls = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
15
+ controls.checkbox({value: "^locked", lbl: "State", label: "Disable the preference"});
16
+ pane.p("Subscribed", {hidden: "^newsletter:=!{}", id: "subscribed"});
17
+ pane.p("Not subscribed", {hidden: "^newsletter", id: "not-subscribed"});
18
+ for (const [path, value] of [["newsletter", false], ["locked", false], ]) {
19
+ pane.dataSetter({destination_path: path, value});
20
+ }
21
+ }
22
+ }
23
+
24
+ /** This page needs no named client methods. */
25
+ export class Logic {}
@@ -0,0 +1,14 @@
1
+ # 02 · Checkbox and labelbox
2
+
3
+ Two checkboxes share one boolean preference. Click either caption, then disable both using the State checkbox.
4
+
5
+ **Uses:** the first 0.3 input components, shared labelbox, Source, Data and bindings. No endpoints or validation engine.
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+ `lbl` names the field; `label` is the caption beside the checkbox. The stored value is `True`/`False` in Python and `true`/`false` in JavaScript.
11
+
12
+ **Try it:** click either checkbox and inspect the shared boolean in Data. For layout variants, use 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 = "RadioButtonText 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("One code for each group");
10
+ pane.p("Choose a delivery option: the Data stores its code, not its caption. The second group has its own value. Arrow keys move between options.");
11
+ const controls = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
12
+ controls.html_label("Columns (1–3): ", {for: "columns"}).span("^columns", {id: "columns-value"});
13
+ controls.input({id: "columns", type: "range", min: 1, max: 3, value: "^columns", live: true, width: "16rem", max_width: "100%"});
14
+ const fields = pane.formlet({class: "card stack", width: "100%", max_width: "36rem"});
15
+ const choices = "standard:Standard,express:Express,pickup:Pick up";
16
+ fields.radioButtonText({id: "shipping", value: "^shipping", values: choices, lbl: "Delivery", cols: "^columns", lbl_color: "#315fa8", box_padding: "8px"});
17
+ fields.radioButtonText({id: "alternative", value: "^alternative", values: choices, lbl: "Alternative delivery", cols: 3, box_padding: "8px"});
18
+ const facts = pane.dl();
19
+ facts.dt("Delivery code");
20
+ facts.dd("^shipping", {id: "shipping-value"});
21
+ facts.dt("Alternative code");
22
+ facts.dd("^alternative", {id: "alternative-value"});
23
+ for (const [path, value] of [["shipping", "standard"], ["alternative", "pickup"], ["columns", 3]]) {
24
+ pane.dataSetter({destination_path: path, value});
25
+ }
26
+ }
27
+ }
28
+
29
+ /** This page needs no named client methods. */
30
+ export class Logic {}
@@ -0,0 +1,16 @@
1
+ # 03 · RadioButtonText and labelbox
2
+
3
+ Choose one delivery option in each group. Change the column count, and try the arrow keys inside a group.
4
+
5
+ **Uses:** the first 0.3 input components, shared labelbox, Source, Data and bindings. No endpoints or validation engine.
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+ `values="standard:Standard,express:Express,pickup:Pick up"` separates stored codes from visible captions. Each group writes only its own Data path; `cols` and `lbl_position` can follow pointers.
11
+
12
+ The controls stay above the groups so changing the number of rows does not move the slider while you drag it. The number beside **Columns** shows the current Data value, from 1 to 3.
13
+
14
+ **Try it:** click either checkbox and inspect the shared boolean in Data. For layout variants, use Settings (⚙) in the gallery footer.
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 = "Multiline text 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("Multiline text and labelbox");
10
+ pane.p("Two multiline fields share one note. Switch live editing and move the label while keeping the text and selection.");
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.textBoxArea({id: "notes", value: "^notes", lbl: "Notes", rows: 4, cols: 24,
16
+ placeholder: "Write a few lines", live: "^live", disabled: "^locked", box_padding: "8px"});
17
+ fields.textBoxArea({id: "notes-copy", value: "^notes", lbl: "Same notes", rows: 3, cols: 24, live: "^live", disabled: "^locked"});
18
+ for (const [path, value] of [["notes", "First line\nSecond line"], ["locked", false], ["live", true]]) pane.dataSetter({destination_path: path, value});
19
+ }
20
+ }
21
+
22
+ /** This page needs no named client methods. */
23
+ export class Logic {}
@@ -0,0 +1,14 @@
1
+ # 04 · Multiline text and labelbox
2
+
3
+ Two multiline fields share one note. Switch live editing and move the label while keeping the text and selection.
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
+ `textBoxArea` wraps a native textarea; `rows` and `cols` set its visible size. With live editing off, leave the field to commit. With it on, the second field follows each edit. Native `textarea` remains available in the earlier HTML lessons.
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,23 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = "Password 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("Password and labelbox");
10
+ pane.p("Two password fields share a sample value. The browser masks the characters; changing either field updates the other.");
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.passwordbox({id: "secret", value: "^secret", lbl: "Sample password", live: "^live",
16
+ disabled: "^locked", placeholder: "Use a sample value", box_padding: "8px"});
17
+ fields.passwordbox({id: "secret-copy", value: "^secret", lbl: "Same sample password", live: "^live", disabled: "^locked"});
18
+ for (const [path, value] of [["secret", "sample-only"], ["locked", false], ["live", true]]) pane.dataSetter({destination_path: path, value});
19
+ }
20
+ }
21
+
22
+ /** This page needs no named client methods. */
23
+ export class Logic {}
@@ -0,0 +1,14 @@
1
+ # 05 · Password and labelbox
2
+
3
+ Two password fields share a sample value. The browser masks the characters; changing either field updates the other.
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
+ `passwordbox` wraps a native password input. Masking changes its display, not the value in Data: this lesson uses a sample, and the inspector can read it. It has the same string, live editing and labelbox behavior as `textbox`.
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,28 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = "Single radio buttons";
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+
7
+ main(root) {
8
+ const pane = root.main({class: 'example-page stack'});
9
+ pane.h1("Single radio buttons");
10
+ pane.p("Each radio binds its own boolean. A shared group makes the choices exclusive; a separate group stays independent.");
11
+ const controls = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
12
+ controls.checkbox({id: "locked", value: "^locked", lbl: "State", label: "Disable controls"});
13
+ const fields = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
14
+ for (const [code, caption] of [["standard", "Standard"], ["express", "Express"], ["pickup", "Pick up"]]) {
15
+ fields.radioButton({id: code, value: `^${code}`, group: "delivery", label: caption,
16
+ lbl: caption, disabled: "^locked", box_padding: "4px"});
17
+ }
18
+ fields.radioButton({id: "independent", value: "^independent", group: "other", label: "Independent choice", lbl: "Another group", disabled: "^locked"});
19
+ const facts = pane.dl();
20
+ for (const code of ["standard", "express", "pickup", "independent"]) {
21
+ facts.dt(code); facts.dd(`^${code}`, {id: `${code}-value`});
22
+ }
23
+ for (const [path, value] of [["standard", true], ["express", false], ["pickup", false], ["independent", false], ["locked", false]]) pane.dataSetter({destination_path: path, value});
24
+ }
25
+ }
26
+
27
+ /** This page needs no named client methods. */
28
+ export class Logic {}
@@ -0,0 +1,14 @@
1
+ # 06 · Single radio buttons
2
+
3
+ Each radio binds its own boolean. A shared group makes the choices exclusive; a separate group stays independent.
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
+ `radioButton` uses one boolean per control; `group` names the exclusive group within the current Source form (or page). Selecting Express writes `express=True` and clears the selected peer. Use arrow keys between choices or click the captions. Compare w03: `radioButtonText` stores one selected code instead. These are two different value contracts.
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>