@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.
- package/LICENSE +176 -0
- package/NOTICE +8 -0
- package/README.md +49 -0
- package/package.json +52 -0
- package/src/gramlot_examples/catalog.json +253 -0
- package/src/gramlot_examples/gallery/dist/LICENSE +176 -0
- package/src/gramlot_examples/gallery/dist/NOTICE +8 -0
- package/src/gramlot_examples/gallery/dist/frame.js +28 -0
- package/src/gramlot_examples/gallery/dist/gallery.js +21239 -0
- package/src/gramlot_examples/gallery/dist/notices.json +38 -0
- package/src/gramlot_examples/gallery/gallery-page.js +97 -0
- package/src/gramlot_examples/gallery/gallery.css +110 -0
- package/src/gramlot_examples/gallery/page.js +26 -0
- package/src/gramlot_examples/index.js +79 -0
- package/src/gramlot_examples/pages/README.md +21 -0
- package/src/gramlot_examples/pages/binding/01_pointers.js +35 -0
- package/src/gramlot_examples/pages/binding/01_pointers.md +14 -0
- package/src/gramlot_examples/pages/binding/02_variable_datapath.js +33 -0
- package/src/gramlot_examples/pages/binding/02_variable_datapath.md +9 -0
- package/src/gramlot_examples/pages/binding/03_setters_and_defaults.js +42 -0
- package/src/gramlot_examples/pages/binding/03_setters_and_defaults.md +10 -0
- package/src/gramlot_examples/pages/binding/04_native_editing.js +53 -0
- package/src/gramlot_examples/pages/binding/04_native_editing.md +11 -0
- package/src/gramlot_examples/pages/binding/05_checkbox_radio.js +41 -0
- package/src/gramlot_examples/pages/binding/05_checkbox_radio.md +9 -0
- package/src/gramlot_examples/pages/binding/06_style_and_visibility.js +51 -0
- package/src/gramlot_examples/pages/binding/06_style_and_visibility.md +11 -0
- package/src/gramlot_examples/pages/binding/07_bound_svg.js +33 -0
- package/src/gramlot_examples/pages/binding/07_bound_svg.md +7 -0
- package/src/gramlot_examples/pages/binding/08_freeze.css +1 -0
- package/src/gramlot_examples/pages/binding/08_freeze.js +26 -0
- package/src/gramlot_examples/pages/binding/08_freeze.md +11 -0
- package/src/gramlot_examples/pages/binding/08_freeze_aux.js +26 -0
- package/src/gramlot_examples/pages/binding/09_forms.css +8 -0
- package/src/gramlot_examples/pages/binding/09_forms.js +79 -0
- package/src/gramlot_examples/pages/binding/09_forms.md +12 -0
- package/src/gramlot_examples/pages/binding/10_svg_shapes.js +42 -0
- package/src/gramlot_examples/pages/binding/10_svg_shapes.md +9 -0
- package/src/gramlot_examples/pages/binding/11_complete_page.css +5 -0
- package/src/gramlot_examples/pages/binding/11_complete_page.js +127 -0
- package/src/gramlot_examples/pages/binding/11_complete_page.md +12 -0
- package/src/gramlot_examples/pages/binding/README.md +9 -0
- package/src/gramlot_examples/pages/controllers/01_formula.js +34 -0
- package/src/gramlot_examples/pages/controllers/01_formula.md +12 -0
- package/src/gramlot_examples/pages/controllers/02_controller.js +44 -0
- package/src/gramlot_examples/pages/controllers/02_controller.md +10 -0
- package/src/gramlot_examples/pages/controllers/03_named_logic.js +28 -0
- package/src/gramlot_examples/pages/controllers/03_named_logic.md +12 -0
- package/src/gramlot_examples/pages/controllers/03_named_logic_aux.js +19 -0
- package/src/gramlot_examples/pages/controllers/04_inline_expressions.js +38 -0
- package/src/gramlot_examples/pages/controllers/04_inline_expressions.md +12 -0
- package/src/gramlot_examples/pages/controllers/05_node_methods.css +1 -0
- package/src/gramlot_examples/pages/controllers/05_node_methods.js +39 -0
- package/src/gramlot_examples/pages/controllers/05_node_methods.md +11 -0
- package/src/gramlot_examples/pages/controllers/06_button_controller.js +36 -0
- package/src/gramlot_examples/pages/controllers/06_button_controller.md +11 -0
- package/src/gramlot_examples/pages/controllers/07_events.js +38 -0
- package/src/gramlot_examples/pages/controllers/07_events.md +10 -0
- package/src/gramlot_examples/pages/controllers/08_remote_source.js +34 -0
- package/src/gramlot_examples/pages/controllers/08_remote_source.md +10 -0
- package/src/gramlot_examples/pages/controllers/08_remote_source_aux.js +8 -0
- package/src/gramlot_examples/pages/controllers/09_end_to_end.css +1 -0
- package/src/gramlot_examples/pages/controllers/09_end_to_end.js +67 -0
- package/src/gramlot_examples/pages/controllers/09_end_to_end.md +14 -0
- package/src/gramlot_examples/pages/controllers/09_end_to_end_aux.js +35 -0
- package/src/gramlot_examples/pages/controllers/README.md +11 -0
- package/src/gramlot_examples/pages/html_svg/01_hello_world.js +12 -0
- package/src/gramlot_examples/pages/html_svg/01_hello_world.md +9 -0
- package/src/gramlot_examples/pages/html_svg/02_text_and_links.js +28 -0
- package/src/gramlot_examples/pages/html_svg/02_text_and_links.md +9 -0
- package/src/gramlot_examples/pages/html_svg/03_lists.js +29 -0
- package/src/gramlot_examples/pages/html_svg/03_lists.md +9 -0
- package/src/gramlot_examples/pages/html_svg/04_semantic_page.js +25 -0
- package/src/gramlot_examples/pages/html_svg/04_semantic_page.md +9 -0
- package/src/gramlot_examples/pages/html_svg/05_tables.js +27 -0
- package/src/gramlot_examples/pages/html_svg/05_tables.md +9 -0
- package/src/gramlot_examples/pages/html_svg/06_forms.css +5 -0
- package/src/gramlot_examples/pages/html_svg/06_forms.js +63 -0
- package/src/gramlot_examples/pages/html_svg/06_forms.md +9 -0
- package/src/gramlot_examples/pages/html_svg/07_native_disclosure.js +27 -0
- package/src/gramlot_examples/pages/html_svg/07_native_disclosure.md +9 -0
- package/src/gramlot_examples/pages/html_svg/08_svg_shapes.js +23 -0
- package/src/gramlot_examples/pages/html_svg/08_svg_shapes.md +9 -0
- package/src/gramlot_examples/pages/html_svg/09_svg_composition.js +42 -0
- package/src/gramlot_examples/pages/html_svg/09_svg_composition.md +9 -0
- package/src/gramlot_examples/pages/html_svg/10_cards_with_icons.css +6 -0
- package/src/gramlot_examples/pages/html_svg/10_cards_with_icons.js +40 -0
- package/src/gramlot_examples/pages/html_svg/10_cards_with_icons.md +20 -0
- package/src/gramlot_examples/pages/html_svg/11_static_report.css +3 -0
- package/src/gramlot_examples/pages/html_svg/11_static_report.js +68 -0
- package/src/gramlot_examples/pages/html_svg/11_static_report.md +9 -0
- package/src/gramlot_examples/pages/html_svg/12_complete_page.css +5 -0
- package/src/gramlot_examples/pages/html_svg/12_complete_page.js +120 -0
- package/src/gramlot_examples/pages/html_svg/12_complete_page.md +9 -0
- package/src/gramlot_examples/pages/html_svg/13_live_source.css +3 -0
- package/src/gramlot_examples/pages/html_svg/13_live_source.js +64 -0
- package/src/gramlot_examples/pages/html_svg/13_live_source.md +18 -0
- package/src/gramlot_examples/pages/html_svg/README.md +10 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {Page as BasePage, source} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = 'End-to-end story';
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
|
|
7
|
+
main(root) {
|
|
8
|
+
const pane = root.div({class: 'example-page stack', datapath: 'story'});
|
|
9
|
+
pane.h1('^.settings.caption', {id: 'caption'});
|
|
10
|
+
|
|
11
|
+
const form = pane.form({class: 'card stack', node_id: 'form'});
|
|
12
|
+
form.html_label('Caption', {for: 'caption-field'});
|
|
13
|
+
form.input({value: '^.settings.caption', live: '^.live', id: 'caption-field'});
|
|
14
|
+
const row = form.div();
|
|
15
|
+
row.input({type: 'checkbox', value: '^.live', id: 'live'});
|
|
16
|
+
row.html_label('live', {for: 'live'});
|
|
17
|
+
form.html_label('Quantity', {for: 'quantity'});
|
|
18
|
+
form.input({type: 'number', min: 0, value: '^.quantity', default: 2, id: 'quantity'});
|
|
19
|
+
form.dataSetter({destination_path: '.settings.caption', value: 'A first caption'});
|
|
20
|
+
const sizes = form.fieldset();
|
|
21
|
+
sizes.legend('Size');
|
|
22
|
+
for (const [key, label] of [['small', 'Small'], ['large', 'Large']]) {
|
|
23
|
+
sizes.input({type: 'radio', group: 'size', value: `^.size.${key}`, id: key});
|
|
24
|
+
sizes.html_label(label, {for: key});
|
|
25
|
+
}
|
|
26
|
+
const gift = form.div();
|
|
27
|
+
gift.input({type: 'checkbox', value: '^.gift', id: 'gift'});
|
|
28
|
+
gift.html_label('Gift wrap', {for: 'gift'});
|
|
29
|
+
|
|
30
|
+
const result = pane.section({class: 'card stack'});
|
|
31
|
+
result.p('^.total', {id: 'total'});
|
|
32
|
+
const drawing = result.svg({viewBox: '0 0 200 200', role: 'img', aria_label: 'Total as a circle', class: 'example-art'});
|
|
33
|
+
drawing.circle({cx: 100, cy: 100, r: '^.radius', fill: '^.color', id: 'circle'});
|
|
34
|
+
result.button('Press, also with Shift', {id: 'press'}).dataController({func: 'press'});
|
|
35
|
+
result.p('^.presses', {id: 'presses'});
|
|
36
|
+
result.p('^.modifiers', {id: 'modifiers', class: 'muted'});
|
|
37
|
+
|
|
38
|
+
pane.dataFormula({result_path: '.total', formula: 'quantity * (large ? 3 : 1) + (gift ? 5 : 0)', quantity: '^.quantity',
|
|
39
|
+
large: '^.size.large', gift: '^.gift', _init: true});
|
|
40
|
+
pane.dataFormula({result_path: '.radius', formula: 'Math.min(20 + total * 4, 90)', total: '^.total', _init: true});
|
|
41
|
+
pane.dataController({func: 'paint', total: '^.total', _init: true});
|
|
42
|
+
|
|
43
|
+
const later = pane.section({class: 'card stack', node_id: 'later'});
|
|
44
|
+
later.h2('A later branch');
|
|
45
|
+
const notes = later.ul({node_id: 'notes', id: 'notes'});
|
|
46
|
+
for (const text of ['Setters live here too', 'This note can be removed under freeze']) notes.li(text);
|
|
47
|
+
const actions = later.div({class: 'actions'});
|
|
48
|
+
for (const [label, method] of [['Load extras', 'loadExtras'], ['Freeze', 'freeze'],
|
|
49
|
+
['Remove a note', 'removeNote'], ['Thaw', 'thaw']]) {
|
|
50
|
+
actions.button(label, {id: method}).dataController({func: method});
|
|
51
|
+
}
|
|
52
|
+
later.section({node_id: 'extras', id: 'extras'}).p('No extras yet.', {class: 'muted'});
|
|
53
|
+
later.dataSetter({destination_path: '.settings.caption', value: 'The story of a page'});
|
|
54
|
+
later.dataSetter({destination_path: '.size', value: {small: true, large: false}});
|
|
55
|
+
later.dataSetter({destination_path: '.gift', value: false});
|
|
56
|
+
later.dataSetter({destination_path: '.live', value: false});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
extras(root) {
|
|
60
|
+
const branch = root.div({datapath: 'extras', class: 'stack'});
|
|
61
|
+
branch.h3('^.title', {id: 'extras-title'});
|
|
62
|
+
branch.p('^.items', {id: 'extras-items'});
|
|
63
|
+
branch.dataSetter({destination_path: '.title', value: 'Extras from the server'});
|
|
64
|
+
branch.dataSetter({destination_path: '.items', value: 'Ribbon, card, envelope'});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
source(Page.prototype.extras);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# 09 · End-to-end story
|
|
2
|
+
|
|
3
|
+
The acceptance page of Gramlot 0.2.0 (unified plan §8.1), in Python and JavaScript with one companion.
|
|
4
|
+
|
|
5
|
+
1. **First render.** The `dataSetter` nodes sit after some controls and in the later branch; `.settings.caption` is written twice and the later setter wins. The quantity has no setter and starts from its `default`. The first DOM already shows every final value.
|
|
6
|
+
2. **Editing.** The caption field writes at `change`; after checking `live` it writes at every keystroke.
|
|
7
|
+
3. **Formula and controller.** The formulas compute the total and the circle's radius; the named controller `paint` colors the circle.
|
|
8
|
+
4. **Radio.** Choosing Large writes `size.large` true and `size.small` false.
|
|
9
|
+
5. **Button.** The nested named controller `press` runs once per click, with `button_counter` and `button_shift`.
|
|
10
|
+
6. **Remote Source.** **Load extras** replaces the extras section with a branch built on the server; its own setters are visible at its first render.
|
|
11
|
+
7. **Freeze.** Freeze the later branch, change the quantity and remove a note, then thaw once: the removal appears at the thaw.
|
|
12
|
+
8. **Close.** Closing the page disposes the Gramlot instance: providers, timers, listeners and pending requests stop.
|
|
13
|
+
|
|
14
|
+
**Try it:** Walk the eight steps in order, then reload and walk them in another order.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Companion of 09_end_to_end.py and 09_end_to_end.js: the named logic of the story. */
|
|
2
|
+
export class Logic {
|
|
3
|
+
sourceNode(nodeId) {
|
|
4
|
+
return this.page.source.getNodeByAttr('node_id', nodeId);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Controller of the total: the circle turns red above 10. */
|
|
8
|
+
paint(node, kwargs) {
|
|
9
|
+
node.SET('.color', kwargs.total > 10 ? '#b13d48' : '#23745b');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** The button's nested controller: one call per click, with its counter and modifiers. */
|
|
13
|
+
press(node, kwargs) {
|
|
14
|
+
node.SET('.presses', `Pressed ${kwargs.button_counter} times`);
|
|
15
|
+
node.SET('.modifiers', kwargs.button_shift ? 'with Shift' : 'without modifiers');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
loadExtras() {
|
|
19
|
+
return this.page.remoteSource(this.sourceNode('extras'), 'extras');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
freeze() {
|
|
23
|
+
this.page.renderer.freeze(this.sourceNode('later'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
removeNote() {
|
|
27
|
+
const notes = this.sourceNode('notes').value;
|
|
28
|
+
const last = notes.getNodes().at(-1);
|
|
29
|
+
if (last) notes.popNode(last.label);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
thaw() {
|
|
33
|
+
this.page.renderer.unfreeze(this.sourceNode('later'));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Controllers
|
|
2
|
+
|
|
3
|
+
The pages of this family run code when the Data changes.
|
|
4
|
+
|
|
5
|
+
`dataFormula` computes a value and writes it to its result path. `dataController` runs code for its effects. A button runs a nested controller at every click; `connect_on<event>` attributes connect other DOM events. The code is inline in the page, or a method of the `Logic` class in the page companion `NN_name_aux.js`.
|
|
6
|
+
|
|
7
|
+
Inline code needs a Content Security Policy that allows it. Named logic in the companion also runs under a strict policy.
|
|
8
|
+
|
|
9
|
+
Start with **Formula** and **Controller**, then move the code to the companion with **Named logic**. The last page is the end-to-end story of Gramlot 0.2.0: every piece of the two families on one page.
|
|
10
|
+
|
|
11
|
+
Each example has a Python page and its JavaScript equivalent. Both share the same companion.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = 'Hello, Gramlot';
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
|
|
7
|
+
main(root) {
|
|
8
|
+
const page = root.main({class: 'example-page stack'});
|
|
9
|
+
page.h1('Hello, Gramlot!');
|
|
10
|
+
page.p('This heading and paragraph come from a Source tree.');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 01 · Hello world
|
|
2
|
+
|
|
3
|
+
The smallest Gramlot page: one main landmark, heading and paragraph.
|
|
4
|
+
|
|
5
|
+
`main(root)` declares each element as a typed Source node. The Python and JavaScript Pages produce the same visible structure.
|
|
6
|
+
|
|
7
|
+
**Try it:** Add a second paragraph and compare the two Page modules.
|
|
8
|
+
|
|
9
|
+
**Limit:** The page has no state or interaction.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = 'Text and links';
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
|
|
7
|
+
main(root) {
|
|
8
|
+
const page = root.main({class: 'example-page stack'});
|
|
9
|
+
page.h1('Words with meaning');
|
|
10
|
+
const intro = page.p('A paragraph can contain ');
|
|
11
|
+
intro.strong('strong emphasis');
|
|
12
|
+
intro.span(', ');
|
|
13
|
+
intro.em('gentle emphasis');
|
|
14
|
+
intro.span(', and a ');
|
|
15
|
+
intro.a('link to the HTML standard', {href: 'https://html.spec.whatwg.org/', target: '_blank', rel: 'noopener noreferrer'});
|
|
16
|
+
intro.span('.');
|
|
17
|
+
page.p('Text is escaped by the Source renderer: <tag> is shown as text.', {class: 'muted'});
|
|
18
|
+
const quote = page.blockquote();
|
|
19
|
+
quote.p('Clarity grows from small, explicit choices.');
|
|
20
|
+
quote.footer('A note from this example');
|
|
21
|
+
page.h2('Code as text');
|
|
22
|
+
page.p('The expression below is shown literally:');
|
|
23
|
+
page.pre().code("root.h1('Hello')");
|
|
24
|
+
page.h3('A local destination', {id: 'destination'});
|
|
25
|
+
page.p('Headings give the link above a place to land.');
|
|
26
|
+
page.a('Jump back to the heading', {href: '#destination'});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 02 · Text and links
|
|
2
|
+
|
|
3
|
+
Text hierarchy, mixed inline content, quotation, code and two kinds of link.
|
|
4
|
+
|
|
5
|
+
A paragraph combines literal text with strong, emphasis, span and anchor children. The pre/code pair displays escaped text.
|
|
6
|
+
|
|
7
|
+
**Try it:** Add an abbreviation and another local destination.
|
|
8
|
+
|
|
9
|
+
**Limit:** The external standards link needs network access; the page itself fetches nothing.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
const STEPS = ['Describe a page with Source', 'Add HTML elements', 'Review the rendered document'];
|
|
4
|
+
const TERMS = [['Source', 'The declared structure of a page.'], ['Data', 'A separate home for application state.']];
|
|
5
|
+
|
|
6
|
+
export class Page extends BasePage {
|
|
7
|
+
static title = 'Lists';
|
|
8
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
9
|
+
|
|
10
|
+
main(root) {
|
|
11
|
+
const page = root.main({class: 'example-page stack'});
|
|
12
|
+
page.h1('Lists tell a sequence');
|
|
13
|
+
page.p('Loops add one Source node for each item.');
|
|
14
|
+
const ordered = page.ol();
|
|
15
|
+
for (const step of STEPS) ordered.li(step);
|
|
16
|
+
page.h2('Two useful terms');
|
|
17
|
+
const definitions = page.dl();
|
|
18
|
+
for (const [term, description] of TERMS) {
|
|
19
|
+
definitions.dt(term);
|
|
20
|
+
definitions.dd(description);
|
|
21
|
+
}
|
|
22
|
+
page.h2('A nested list');
|
|
23
|
+
const groups = page.ul();
|
|
24
|
+
const first = groups.li('HTML elements');
|
|
25
|
+
const nested = first.ul();
|
|
26
|
+
for (const item of ['headings', 'lists', 'links']) nested.li(item);
|
|
27
|
+
groups.li('SVG');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 03 · Lists
|
|
2
|
+
|
|
3
|
+
Ordered, unordered and description lists built from local data.
|
|
4
|
+
|
|
5
|
+
Loops add ordered steps, definition pairs and a nested list while Source preserves their structure.
|
|
6
|
+
|
|
7
|
+
**Try it:** Add a fourth step and another glossary term.
|
|
8
|
+
|
|
9
|
+
**Limit:** The list content is fixed when the Page builds Source.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = 'A semantic page';
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
|
|
7
|
+
main(root) {
|
|
8
|
+
const page = root.div({class: 'example-page stack'});
|
|
9
|
+
const header = page.header();
|
|
10
|
+
header.p('GRAMLOT JOURNAL', {class: 'eyebrow'});
|
|
11
|
+
header.h1('A page with landmarks');
|
|
12
|
+
const nav = page.nav({aria_label: 'Page sections'});
|
|
13
|
+
nav.a('Story', {href: '#story'});
|
|
14
|
+
nav.span(' · ');
|
|
15
|
+
nav.a('Notes', {href: '#notes'});
|
|
16
|
+
const main = page.main();
|
|
17
|
+
const article = main.article({id: 'story', class: 'stack'});
|
|
18
|
+
article.h2('The article');
|
|
19
|
+
article.p('Header, navigation, main, article and footer each describe a role.');
|
|
20
|
+
const aside = main.aside({id: 'notes', class: 'card'});
|
|
21
|
+
aside.h2('A side note');
|
|
22
|
+
aside.p('The markup stays meaningful even without decoration.');
|
|
23
|
+
page.footer().small('Built with Gramlot Source.');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 04 · A semantic page
|
|
2
|
+
|
|
3
|
+
Landmarks and sections give a small page a meaningful reading order.
|
|
4
|
+
|
|
5
|
+
Header, navigation, main, article, aside and footer are authored as HTML Source. Section IDs serve as anchor targets.
|
|
6
|
+
|
|
7
|
+
**Try it:** Add another article and a matching navigation link.
|
|
8
|
+
|
|
9
|
+
**Limit:** Navigation uses native anchors; there is no client router.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
const ROWS = [['North', 12, 8], ['South', 9, 11], ['West', 15, 6]];
|
|
4
|
+
|
|
5
|
+
export class Page extends BasePage {
|
|
6
|
+
static title = 'Tables';
|
|
7
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
8
|
+
|
|
9
|
+
main(root) {
|
|
10
|
+
const page = root.main({class: 'example-page stack'});
|
|
11
|
+
page.h1('A small regional report');
|
|
12
|
+
page.p('Rows come from data; totals are computed before Source is built.');
|
|
13
|
+
const table = page.table();
|
|
14
|
+
table.caption('Orders by region and quarter');
|
|
15
|
+
const heading = table.thead().tr();
|
|
16
|
+
for (const label of ['Region', 'Q1', 'Q2', 'Total']) heading.th(label, {scope: 'col'});
|
|
17
|
+
const body = table.tbody();
|
|
18
|
+
for (const [region, q1, q2] of ROWS) {
|
|
19
|
+
const row = body.tr();
|
|
20
|
+
row.th(region, {scope: 'row'});
|
|
21
|
+
for (const value of [q1, q2, q1 + q2]) row.td(String(value));
|
|
22
|
+
}
|
|
23
|
+
const total = table.tfoot().tr();
|
|
24
|
+
total.th('All regions', {scope: 'row'});
|
|
25
|
+
for (const value of [ROWS.reduce((n, row) => n + row[1], 0), ROWS.reduce((n, row) => n + row[2], 0), ROWS.reduce((n, row) => n + row[1] + row[2], 0)]) total.td(String(value));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 05 · Tables
|
|
2
|
+
|
|
3
|
+
A data table with a caption, scoped headings and a calculated total.
|
|
4
|
+
|
|
5
|
+
One local row set feeds the table body and footer arithmetic.
|
|
6
|
+
|
|
7
|
+
**Try it:** Add a region and confirm the total changes in both languages.
|
|
8
|
+
|
|
9
|
+
**Limit:** Values are fixed when Source is built.
|
|
@@ -0,0 +1,5 @@
|
|
|
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; } }
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = 'Native form controls';
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
|
|
7
|
+
main(root) {
|
|
8
|
+
const page = root.main({class: 'example-page stack'});
|
|
9
|
+
page.h1('Native form controls');
|
|
10
|
+
page.p('Try the controls, then use Reset to restore their initial values. Submit is disabled in this static example.');
|
|
11
|
+
const form = page.form();
|
|
12
|
+
const identity = form.fieldset({class: 'form-grid'});
|
|
13
|
+
identity.legend('Identity');
|
|
14
|
+
this.field(identity, 'Name', 'name', 'text', {value: 'Ada Lovelace', required: true});
|
|
15
|
+
this.field(identity, 'Email', 'email', 'email', {placeholder: 'name@example.org'});
|
|
16
|
+
this.field(identity, 'Search', 'search', 'search', {placeholder: 'Find a topic'});
|
|
17
|
+
this.field(identity, 'Website', 'website', 'url', {placeholder: 'https://example.org'});
|
|
18
|
+
this.field(identity, 'Telephone', 'telephone', 'tel', {placeholder: '+1 555 0100'});
|
|
19
|
+
this.field(identity, 'Password', 'password', 'password', {minlength: 8});
|
|
20
|
+
|
|
21
|
+
const choices = form.fieldset({class: 'form-grid'});
|
|
22
|
+
choices.legend('Choices and ranges');
|
|
23
|
+
this.field(choices, 'Quantity', 'quantity', 'number', {min: 1, max: 20, value: 3});
|
|
24
|
+
this.field(choices, 'Date', 'date', 'date');
|
|
25
|
+
this.field(choices, 'Time', 'time', 'time');
|
|
26
|
+
this.field(choices, 'Month', 'month', 'month');
|
|
27
|
+
this.field(choices, 'Week', 'week', 'week');
|
|
28
|
+
this.field(choices, 'Accent', 'accent', 'color', {value: '#456bc4'});
|
|
29
|
+
this.field(choices, 'Intensity', 'intensity', 'range', {min: 0, max: 100, step: 5, value: 60});
|
|
30
|
+
choices.html_label('Region', {for: 'region'});
|
|
31
|
+
const select = choices.select({id: 'region', name: 'region'});
|
|
32
|
+
for (const value of ['North', 'South', 'West']) select.option(value, {value: value.toLowerCase(), selected: value === 'South'});
|
|
33
|
+
choices.html_label('Topics (multiple)', {for: 'topics'});
|
|
34
|
+
const topics = choices.select({id: 'topics', name: 'topics', multiple: true, size: 3});
|
|
35
|
+
for (const topic of ['HTML', 'SVG', 'Source']) topics.option(topic, {value: topic.toLowerCase(), selected: topic === 'HTML'});
|
|
36
|
+
choices.html_label('Note', {for: 'note'});
|
|
37
|
+
choices.textarea('A native multiline field.', {id: 'note', name: 'note', rows: 3});
|
|
38
|
+
|
|
39
|
+
const options = form.fieldset({class: 'form-options'});
|
|
40
|
+
options.legend('Flags and states');
|
|
41
|
+
options.html_label('Receive updates', {for: 'updates'});
|
|
42
|
+
options.input({type: 'checkbox', id: 'updates', name: 'updates', checked: true});
|
|
43
|
+
for (const value of ['daily', 'weekly']) {
|
|
44
|
+
options.html_label(value[0].toUpperCase() + value.slice(1), {for: value});
|
|
45
|
+
options.input({type: 'radio', id: value, name: 'frequency', value, checked: value === 'weekly'});
|
|
46
|
+
}
|
|
47
|
+
options.html_label('Read-only code', {for: 'code'});
|
|
48
|
+
options.input({type: 'text', id: 'code', value: 'DEMO-01', readonly: true});
|
|
49
|
+
options.html_label('Unavailable choice', {for: 'unavailable'});
|
|
50
|
+
options.input({type: 'text', id: 'unavailable', value: 'Disabled', disabled: true});
|
|
51
|
+
options.html_label('Attachment', {for: 'attachment'});
|
|
52
|
+
options.input({type: 'file', id: 'attachment', name: 'attachment'});
|
|
53
|
+
const actions = form.div({class: 'form-actions'});
|
|
54
|
+
actions.button('Button without an action', {type: 'button'});
|
|
55
|
+
actions.button('Reset values', {type: 'reset'});
|
|
56
|
+
actions.button('Submit unavailable', {type: 'submit', disabled: true});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
field(parent, label, key, kind, attrs = {}) {
|
|
60
|
+
parent.html_label(label, {for: key});
|
|
61
|
+
parent.input({type: kind, id: key, name: key, ...attrs});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 06 · Native forms
|
|
2
|
+
|
|
3
|
+
Native fields, choices and buttons grouped in fieldsets.
|
|
4
|
+
|
|
5
|
+
The browser owns editing and Reset. The Source grammar authors the HTML label with `html_label(...)`, because `label` is also a node property.
|
|
6
|
+
|
|
7
|
+
**Try it:** Change a default value, use Reset and add a select option.
|
|
8
|
+
|
|
9
|
+
**Limit:** Submit is disabled because this page has no save endpoint or declared form handler.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = 'Native disclosure and measures';
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
|
|
7
|
+
main(root) {
|
|
8
|
+
const page = root.main({class: 'example-page stack'});
|
|
9
|
+
page.h1('Built-in disclosure and measures');
|
|
10
|
+
page.p("Open the questions with the browser's native details control.");
|
|
11
|
+
for (const [question, answer] of [
|
|
12
|
+
['What is Source?', 'A declarative tree describing the page.'],
|
|
13
|
+
['Can a page contain SVG?', 'Yes. The SVG dialect nests under an HTML Source node.'],
|
|
14
|
+
]) {
|
|
15
|
+
const details = page.details({class: 'card'});
|
|
16
|
+
details.summary(question);
|
|
17
|
+
details.p(answer);
|
|
18
|
+
}
|
|
19
|
+
const measures = page.section({class: 'card stack'});
|
|
20
|
+
measures.h2('Measured values');
|
|
21
|
+
measures.html_label('Preparation: 70%', {for: 'preparation'});
|
|
22
|
+
measures.progress({id: 'preparation', value: 70, max: 100});
|
|
23
|
+
measures.html_label('Quality score: 8 of 10', {for: 'quality'});
|
|
24
|
+
measures.meter({id: 'quality', min: 0, max: 10, low: 4, high: 8, optimum: 10, value: 8});
|
|
25
|
+
measures.p('These numbers are fixed page content; no live progress is implied.', {class: 'muted'});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 07 · Disclosure and measures
|
|
2
|
+
|
|
3
|
+
Expandable questions with static progress and meter values.
|
|
4
|
+
|
|
5
|
+
A loop creates details/summary pairs. Native browser behavior handles expansion; the measurements have explicit values and labels.
|
|
6
|
+
|
|
7
|
+
**Try it:** Add a question and change the fixed meter value.
|
|
8
|
+
|
|
9
|
+
**Limit:** The measurements do not update automatically.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = 'SVG shapes';
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
|
|
7
|
+
main(root) {
|
|
8
|
+
const page = root.main({class: 'example-page stack'});
|
|
9
|
+
page.h1('SVG shape vocabulary');
|
|
10
|
+
page.p('One SVG viewport contains six shapes and a text label.');
|
|
11
|
+
const figure = page.figure({class: 'card'});
|
|
12
|
+
const art = figure.svg({viewBox: '0 0 480 200', role: 'img', aria_labelledby: 'shapes-title', class: 'example-art'});
|
|
13
|
+
art.title('Six geometric shapes', {id: 'shapes-title'});
|
|
14
|
+
art.rect({x: 15, y: 20, width: 95, height: 75, rx: 14, fill: 'var(--gramlot-action)'});
|
|
15
|
+
art.circle({cx: 172, cy: 58, r: 38, fill: 'var(--gramlot-warning)'});
|
|
16
|
+
art.ellipse({cx: 263, cy: 58, rx: 26, ry: 38, fill: 'var(--gramlot-selected)'});
|
|
17
|
+
art.line({x1: 294, y1: 20, x2: 315, y2: 98, stroke: 'currentColor', stroke_width: 8, stroke_linecap: 'round'});
|
|
18
|
+
art.polyline({points: '325,95 345,25 365,70 385,25 405,95', fill: 'none', stroke: 'var(--gramlot-success)', stroke_width: 7, stroke_linejoin: 'round'});
|
|
19
|
+
art.polygon({points: '430,95 450,20 470,95', fill: 'var(--gramlot-brand-blue)'});
|
|
20
|
+
art.text('rectangle · circle · ellipse · line · polyline · polygon', {x: 18, y: 150, fill: 'currentColor', font_size: 17});
|
|
21
|
+
figure.figcaption('The shape attributes are part of the Source tree.');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 08 · SVG shapes
|
|
2
|
+
|
|
3
|
+
Six SVG primitives in one accessible viewport.
|
|
4
|
+
|
|
5
|
+
The SVG dialect nests below an HTML figure. Coordinates, fill and stroke are declared as SVG attributes.
|
|
6
|
+
|
|
7
|
+
**Try it:** Move a shape and compare its coordinates in both languages.
|
|
8
|
+
|
|
9
|
+
**Limit:** This illustration has no animation or pointer behavior.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = 'SVG composition';
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
|
|
7
|
+
main(root) {
|
|
8
|
+
const page = root.main({class: 'example-page stack'});
|
|
9
|
+
page.h1('Compose a small SVG scene');
|
|
10
|
+
page.p('Groups and loops arrange repeated Source nodes in a single viewport.');
|
|
11
|
+
const figure = page.figure({class: 'card'});
|
|
12
|
+
const drawing = figure.svg({viewBox: '0 0 560 240', role: 'img', aria_labelledby: 'scene-title', class: 'example-art'});
|
|
13
|
+
drawing.title('A row of houses and trees in a garden', {id: 'scene-title'});
|
|
14
|
+
drawing.rect({x: 0, y: 0, width: 560, height: 240, fill: 'var(--gramlot-surface-subtle)'});
|
|
15
|
+
drawing.rect({x: 0, y: 177, width: 560, height: 63, fill: 'var(--gramlot-border)'});
|
|
16
|
+
for (const x of [115, 330]) this.house(drawing, x);
|
|
17
|
+
for (const x of [40, 260, 505]) this.tree(drawing, x);
|
|
18
|
+
for (const [x, color] of [[74, 'var(--gramlot-action)'], [282, 'var(--gramlot-warning)'], [465, 'var(--gramlot-brand-blue)']]) this.flower(drawing, x, color);
|
|
19
|
+
figure.figcaption('House, tree and flower methods compose one scene through groups and loops.');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
house(drawing, x) {
|
|
23
|
+
const group = drawing.g({transform: `translate(${x} 0)`});
|
|
24
|
+
group.rect({x: 0, y: 92, width: 125, height: 85, fill: 'var(--gramlot-surface)', stroke: 'var(--gramlot-action)', stroke_width: 3});
|
|
25
|
+
group.path({d: 'M-10 92 L62 42 L135 92 Z', fill: 'var(--gramlot-action)'});
|
|
26
|
+
group.rect({x: 47, y: 127, width: 30, height: 50, fill: 'var(--gramlot-selected)'});
|
|
27
|
+
for (const windowX of [17, 91]) group.rect({x: windowX, y: 110, width: 17, height: 18, fill: 'var(--gramlot-brand-yellow)'});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
tree(drawing, x) {
|
|
31
|
+
const group = drawing.g({transform: `translate(${x} 0)`});
|
|
32
|
+
group.rect({x: -4, y: 103, width: 8, height: 74, fill: 'var(--gramlot-success)'});
|
|
33
|
+
for (const [cx, cy] of [[-14, 111], [12, 109], [0, 88]]) group.circle({cx, cy, r: 22, fill: 'var(--gramlot-success)'});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
flower(drawing, x, color) {
|
|
37
|
+
const group = drawing.g({transform: `translate(${x} 0)`});
|
|
38
|
+
group.line({x1: 0, y1: 207, x2: 0, y2: 187, stroke: 'var(--gramlot-success)', stroke_width: 3});
|
|
39
|
+
for (const [dx, dy] of [[-7, 0], [7, 0], [0, -7], [0, 7]]) group.circle({cx: dx, cy: 184 + dy, r: 6, fill: color});
|
|
40
|
+
group.circle({cx: 0, cy: 184, r: 4, fill: 'var(--gramlot-brand-yellow)'});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 09 · SVG composition
|
|
2
|
+
|
|
3
|
+
A scene assembled from repeated groups, paths and helper methods.
|
|
4
|
+
|
|
5
|
+
House, tree and flower helpers add grouped shapes. Loops place repetitions, while theme tokens color the scene.
|
|
6
|
+
|
|
7
|
+
**Try it:** Add a third house or vary flower positions.
|
|
8
|
+
|
|
9
|
+
**Limit:** The scene is decorative and static.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
.icon-cards { grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); }
|
|
2
|
+
.icon-cards article { min-width: 0; }
|
|
3
|
+
|
|
4
|
+
.icon-cards article { position: relative; }
|
|
5
|
+
.card-remove { position: absolute; inset-block-start: .5rem; inset-inline-end: .5rem;
|
|
6
|
+
padding: .1rem .45rem; line-height: 1.2; font-size: 1rem; }
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
const CARDS = [
|
|
4
|
+
['Structure', 'Source describes the page tree.', 'structure'],
|
|
5
|
+
['Drawing', 'SVG sits naturally beside HTML.', 'drawing'],
|
|
6
|
+
['Reading', 'Semantic elements give content a role.', 'reading'],
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export class Page extends BasePage {
|
|
10
|
+
static title = 'Cards with SVG icons';
|
|
11
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
12
|
+
|
|
13
|
+
main(root) {
|
|
14
|
+
const page = root.main({node_label: 'page', class: 'example-page stack'});
|
|
15
|
+
page.h1('Three ideas, three icons');
|
|
16
|
+
page.p('Click × to remove a card from Source. Gramlot updates the DOM without reloading.');
|
|
17
|
+
const cards = page.section({node_label: 'cards', class: 'grid icon-cards', aria_label: 'Gramlot concepts'});
|
|
18
|
+
for (const [title, description, kind] of CARDS) {
|
|
19
|
+
const card = cards.article({node_label: kind, class: 'card stack'});
|
|
20
|
+
card.button('×', {type: 'button', class: 'card-remove', aria_label: `Remove ${title}`,
|
|
21
|
+
onclick: `window.gramlot.builder.source.getItem('main.page.cards').popNode('${kind}')`});
|
|
22
|
+
const icon = card.svg({viewBox: '0 0 64 64', width: 64, height: 64, aria_hidden: 'true'});
|
|
23
|
+
this.icon(icon, kind);
|
|
24
|
+
card.h2(title);
|
|
25
|
+
card.p(description);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
icon(icon, kind) {
|
|
30
|
+
icon.rect({x: 2, y: 2, width: 60, height: 60, rx: 16, fill: 'var(--gramlot-surface-subtle)'});
|
|
31
|
+
if (kind === 'structure') {
|
|
32
|
+
for (const [x, y] of [[18, 18], [36, 18], [27, 38]]) icon.rect({x, y, width: 11, height: 11, rx: 2, fill: 'var(--gramlot-action)'});
|
|
33
|
+
} else if (kind === 'drawing') {
|
|
34
|
+
icon.circle({cx: 32, cy: 32, r: 17, fill: 'none', stroke: 'var(--gramlot-warning)', stroke_width: 5});
|
|
35
|
+
icon.circle({cx: 32, cy: 32, r: 5, fill: 'var(--gramlot-warning)'});
|
|
36
|
+
} else {
|
|
37
|
+
icon.path({d: 'M17 18h30v28H17z M25 26h14 M25 32h14 M25 38h10', fill: 'none', stroke: 'var(--gramlot-success)', stroke_width: 3, stroke_linecap: 'round'});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 10 · Cards with icons and live Source
|
|
2
|
+
|
|
3
|
+
Responsive HTML cards with SVG drawings. Click a card's × button to remove it
|
|
4
|
+
without reloading the page.
|
|
5
|
+
|
|
6
|
+
The Builder gives the page, card container and each card a stable `node_label`.
|
|
7
|
+
The button's short JavaScript action finds `main.page.cards` in the browser
|
|
8
|
+
Builder's Source and calls `popNode` with the card label. Gramlot observes the
|
|
9
|
+
Source deletion and removes the corresponding DOM branch, including its SVG.
|
|
10
|
+
The action never removes a DOM element directly.
|
|
11
|
+
|
|
12
|
+
Python and JavaScript declare the same page and browser action. It also works
|
|
13
|
+
in the generated standalone directory opened directly from disk, offline.
|
|
14
|
+
|
|
15
|
+
**Try it:** Remove the middle card, then the others. Reload to restore all three.
|
|
16
|
+
Add a fourth card to the local dataset and give it a matching icon branch.
|
|
17
|
+
|
|
18
|
+
**Limit:** This is a small, explicit JavaScript action demonstrating live Source,
|
|
19
|
+
not Data binding, persistence or server synchronization. The icons are decorative;
|
|
20
|
+
card text and the labelled remove buttons carry the meaning.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
.report-summary { grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); }
|
|
2
|
+
.report-number { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 700; margin: .2rem 0; }
|
|
3
|
+
.report-legend { display: flex; flex-wrap: wrap; gap: .4rem 1.2rem; list-style: none; padding: 0; margin: 0; }
|