@gramlot/gramlot-examples 0.2.16 → 0.3.1
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/README.md +8 -5
- package/package.json +4 -3
- package/src/gramlot_examples/catalog.json +156 -0
- package/src/gramlot_examples/gallery/collections.js +4 -0
- package/src/gramlot_examples/gallery/dist/frame.js +88 -28
- package/src/gramlot_examples/gallery/dist/gallery.js +211 -52
- package/src/gramlot_examples/gallery/dist/inputs.js +24456 -0
- package/src/gramlot_examples/gallery/dist/inspector.js +42 -18
- package/src/gramlot_examples/gallery/dist/layout.js +541 -0
- package/src/gramlot_examples/gallery/dist/theme-styles.js +2 -2
- package/src/gramlot_examples/gallery/gallery-page.js +55 -17
- package/src/gramlot_examples/gallery/gallery.css +52 -4
- package/src/gramlot_examples/index.js +3 -2
- package/src/gramlot_examples/pages/README.md +5 -2
- package/src/gramlot_examples/pages/binding/09_forms.js +1 -1
- package/src/gramlot_examples/pages/components/01_textbox.js +32 -0
- package/src/gramlot_examples/pages/components/01_textbox.md +14 -0
- package/src/gramlot_examples/pages/components/02_checkbox.js +25 -0
- package/src/gramlot_examples/pages/components/02_checkbox.md +14 -0
- package/src/gramlot_examples/pages/components/03_radio_button_text.js +30 -0
- package/src/gramlot_examples/pages/components/03_radio_button_text.md +16 -0
- package/src/gramlot_examples/pages/components/04_text_box_area.js +23 -0
- package/src/gramlot_examples/pages/components/04_text_box_area.md +14 -0
- package/src/gramlot_examples/pages/components/05_passwordbox.js +23 -0
- package/src/gramlot_examples/pages/components/05_passwordbox.md +14 -0
- package/src/gramlot_examples/pages/components/06_radio_button.js +28 -0
- package/src/gramlot_examples/pages/components/06_radio_button.md +14 -0
- package/src/gramlot_examples/pages/components/07_colorpicker.js +24 -0
- package/src/gramlot_examples/pages/components/07_colorpicker.md +14 -0
- package/src/gramlot_examples/pages/components/08_horizontal_slider.js +24 -0
- package/src/gramlot_examples/pages/components/08_horizontal_slider.md +14 -0
- package/src/gramlot_examples/pages/components/09_vertical_slider.js +24 -0
- package/src/gramlot_examples/pages/components/09_vertical_slider.md +14 -0
- package/src/gramlot_examples/pages/components/10_number_textbox.js +30 -0
- package/src/gramlot_examples/pages/components/10_number_textbox.md +16 -0
- package/src/gramlot_examples/pages/components/11_date_textbox.js +23 -0
- package/src/gramlot_examples/pages/components/11_date_textbox.md +18 -0
- package/src/gramlot_examples/pages/components/12_time_textbox.js +23 -0
- package/src/gramlot_examples/pages/components/12_time_textbox.md +16 -0
- package/src/gramlot_examples/pages/components/13_filtering_select.js +23 -0
- package/src/gramlot_examples/pages/components/13_filtering_select.md +20 -0
- package/src/gramlot_examples/pages/components/14_callback_select.js +45 -0
- package/src/gramlot_examples/pages/components/14_callback_select.md +24 -0
- package/src/gramlot_examples/pages/components/15_combo_box.js +23 -0
- package/src/gramlot_examples/pages/components/15_combo_box.md +22 -0
- package/src/gramlot_examples/pages/components/16_checkbox_text.js +23 -0
- package/src/gramlot_examples/pages/components/16_checkbox_text.md +23 -0
- package/src/gramlot_examples/pages/components/17_date_calendar.js +25 -0
- package/src/gramlot_examples/pages/components/17_date_calendar.md +22 -0
- package/src/gramlot_examples/pages/components/18_validation.js +31 -0
- package/src/gramlot_examples/pages/components/18_validation.md +29 -0
- package/src/gramlot_examples/pages/components/19_formlet.js +26 -0
- package/src/gramlot_examples/pages/components/19_formlet.md +12 -0
- package/src/gramlot_examples/pages/components/20_group_box.js +26 -0
- package/src/gramlot_examples/pages/components/20_group_box.md +12 -0
- package/src/gramlot_examples/pages/components/21_switch.js +19 -0
- package/src/gramlot_examples/pages/components/21_switch.md +12 -0
- package/src/gramlot_examples/pages/components/22_button.js +18 -0
- package/src/gramlot_examples/pages/components/22_button.md +12 -0
- package/src/gramlot_examples/pages/components/23_toggle_button.js +18 -0
- package/src/gramlot_examples/pages/components/23_toggle_button.md +12 -0
- package/src/gramlot_examples/pages/components/24_dropdown_button.js +21 -0
- package/src/gramlot_examples/pages/components/24_dropdown_button.md +12 -0
- package/src/gramlot_examples/pages/components/25_validation_engine.js +51 -0
- package/src/gramlot_examples/pages/components/25_validation_engine.md +26 -0
- package/src/gramlot_examples/pages/components/README.md +40 -0
- package/src/gramlot_examples/pages/controllers/16_attribute_projection.js +2 -2
- package/src/gramlot_examples/pages/controllers/16_attribute_projection.md +7 -7
- package/src/gramlot_examples/pages/html_svg/06_forms.js +3 -3
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {IndexPage} from '@gramlot/gramlot/page';
|
|
3
3
|
export class GalleryPage extends IndexPage {
|
|
4
4
|
static title = 'Gramlot examples';
|
|
5
|
+
static serverlessDownloadUrl = null;
|
|
5
6
|
static installable = true;
|
|
6
7
|
static appIcon = '/assets/branding/app-icon.png';
|
|
7
8
|
static shellId = 'gallery';
|
|
@@ -16,20 +17,55 @@ export class GalleryPage extends IndexPage {
|
|
|
16
17
|
intro.p('This gallery shows each example page beside the code that creates it. ' +
|
|
17
18
|
'The Python and JavaScript integrations have the same examples.');
|
|
18
19
|
intro.p('Choose an example from the list. Its tab stays open so you can return to it. ' +
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'Choose Base or
|
|
20
|
+
'Open Settings (⚙) in the footer and enable Keyboard navigation to move through tabs by keyboard. ' +
|
|
21
|
+
'Use the top bar to show or hide Navigation and Code; drag their dividers to resize them. ' +
|
|
22
|
+
'Choose Base, Compact, mimi or joanna independently of the color mode.');
|
|
22
23
|
|
|
23
24
|
}
|
|
24
25
|
groupPanel(pane, group) { pane.div(group.readme, {id: `readme-${group.key}`}); }
|
|
25
26
|
footer(footer) {
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
if (this.constructor.serverlessDownloadUrl) {
|
|
28
|
+
footer.a('↓ Serverless ZIP', {href: this.constructor.serverlessDownloadUrl,
|
|
29
|
+
download: 'gramlot-gallery-serverless.zip', class: 'gallery-download',
|
|
30
|
+
title: 'Download the offline gallery; unzip and open index.html'});
|
|
31
|
+
}
|
|
32
|
+
footer.html_button('⚙', {type: 'button', id: 'gallery-settings-toggle', class: 'gallery-settings-toggle',
|
|
33
|
+
title: 'Settings', aria_label: 'Settings', popovertarget: 'gallery-settings'});
|
|
34
|
+
const settings = footer.div({id: 'gallery-settings', popover: 'auto', aria_labelledby: 'gallery-settings-title'});
|
|
35
|
+
const heading = settings.div({class: 'gallery-settings-heading'});
|
|
36
|
+
heading.h2('Settings', {id: 'gallery-settings-title'});
|
|
37
|
+
heading.html_button('×', {type: 'button', aria_label: 'Close settings', popovertarget: 'gallery-settings', popovertargetaction: 'hide'});
|
|
38
|
+
super.footer(settings);
|
|
39
|
+
const labels = settings.div({class: 'index-theme'});
|
|
40
|
+
labels.html_label('Label position', {for: 'gallery-label-position'});
|
|
41
|
+
const position = labels.select({id: 'gallery-label-position', value: 'example'});
|
|
42
|
+
position.option('As in the example', {value: 'example', selected: true});
|
|
43
|
+
for (const code of ['TL', 'TC', 'TR', 'L', 'R', 'BL', 'BC', 'BR']) position.option(code, {value: code});
|
|
44
|
+
settings.p("Label position applies to input components in the previews. Choose As in the example to restore the lesson's layout.");
|
|
45
|
+
const inspector = footer.html_button({type: 'button', id: 'gallery-inspector', class: 'gallery-inspector',
|
|
28
46
|
title: 'Open inspector', aria_label: 'Inspector', aria_pressed: 'false', disabled: true});
|
|
29
47
|
inspector.span('🔍', {aria_hidden: 'true'});
|
|
30
48
|
|
|
31
49
|
}
|
|
32
|
-
|
|
50
|
+
header(shell) {
|
|
51
|
+
const header = shell.header({class: 'gallery-toolbar'});
|
|
52
|
+
header.html_button('☰ Navigation', {type: 'button', id: 'index-navigation-toggle',
|
|
53
|
+
aria_controls: 'index-sidebar', aria_expanded: 'true'});
|
|
54
|
+
header.h1(this.constructor.title);
|
|
55
|
+
header.html_button('Code ⟨/⟩', {type: 'button', id: 'index-code-toggle',
|
|
56
|
+
aria_controls: 'index-code', aria_expanded: 'false', disabled: true});
|
|
57
|
+
}
|
|
58
|
+
behavior(shell) {
|
|
59
|
+
shell.div({class: 'index-divider', id: 'index-code-divider', hidden: true,
|
|
60
|
+
title: 'Drag to resize code', role: 'separator', aria_orientation: 'vertical',
|
|
61
|
+
tabindex: 0, aria_label: 'Resize code', aria_controls: 'index-code',
|
|
62
|
+
aria_valuemin: 200, aria_valuemax: 800, aria_valuenow: 420});
|
|
63
|
+
const code = shell.aside({id: 'index-code', aria_label: 'Example source code', hidden: true});
|
|
64
|
+
for (const family of this.constructor.navigation) {
|
|
65
|
+
for (const example of family.children) this.codePanel(code, example);
|
|
66
|
+
}
|
|
67
|
+
shell.script({src: this.constructor.galleryScript});
|
|
68
|
+
}
|
|
33
69
|
|
|
34
70
|
panel(parent, example) {
|
|
35
71
|
const key = example.key;
|
|
@@ -38,21 +74,23 @@ export class GalleryPage extends IndexPage {
|
|
|
38
74
|
const explanation = panel.header({class: 'gallery-explanation'});
|
|
39
75
|
explanation.div(example.readme, {id: `readme-${key}`});
|
|
40
76
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
77
|
+
const preview = panel.div({class: 'gallery-preview', id: `preview-${key}`});
|
|
78
|
+
const modes = preview.div({class: 'gallery-preview-modes', role: 'group', aria_label: 'Preview mode'});
|
|
79
|
+
for (const [mode, caption] of [['normal', 'Normale'], ['mobile', 'Mobile']]) {
|
|
80
|
+
modes.html_button(caption, {type: 'button', id: `preview-${mode}-${key}`, aria_pressed: mode === 'normal' ? 'true' : 'false'});
|
|
81
|
+
}
|
|
82
|
+
const stage = preview.div({class: 'gallery-preview-stage', id: `preview-stage-${key}`});
|
|
83
|
+
stage.iframe({title: `${example.title} preview`, name: `example-${key}`,
|
|
45
84
|
id: `frame-${key}`});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const codepane = split.div({class: 'gallery-codepane', id: `codepane-${key}`});
|
|
85
|
+
}
|
|
86
|
+
codePanel(parent, example) {
|
|
87
|
+
const key = example.key;
|
|
88
|
+
const codepane = parent.div({class: 'gallery-codepane', id: `codepane-${key}`, hidden: true});
|
|
51
89
|
const tabs = codepane.div({class: 'gallery-source-tabs', role: 'tablist', aria_label: 'Source files'});
|
|
52
|
-
tabs.
|
|
90
|
+
tabs.html_button('JavaScript', {title: `${example.folder}.js`, type: 'button', role: 'tab',
|
|
53
91
|
id: `code-tab-${key}`, aria_controls: `code-panel-${key}`, aria_selected: 'true', tabindex: 0});
|
|
54
92
|
if (example.logic != null) {
|
|
55
|
-
tabs.
|
|
93
|
+
tabs.html_button('logic.js', {hidden: true, title: `${example.folder}_aux.js`, type: 'button', role: 'tab',
|
|
56
94
|
id: `logic-tab-${key}`, aria_controls: `logic-panel-${key}`, aria_selected: 'false', tabindex: -1});
|
|
57
95
|
}
|
|
58
96
|
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:
|
|
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
|
|
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,30 @@
|
|
|
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)}
|
|
86
|
+
|
|
87
|
+
.gallery-download { font-size: .8rem; color: inherit; white-space: nowrap; padding-inline: .3rem; }
|
|
@@ -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
|
|
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/):
|
|
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
|
|
@@ -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.
|
|
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>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = "Color and labelbox";
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
|
|
7
|
+
main(root) {
|
|
8
|
+
const pane = root.main({class: 'example-page stack'});
|
|
9
|
+
pane.h1("Color and labelbox");
|
|
10
|
+
pane.p("Choose a color in either picker. Both controls and the swatch follow the same Data value.");
|
|
11
|
+
const controls = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
|
|
12
|
+
controls.checkbox({id: "live", value: "^live", lbl: "Editing", label: "Update while editing"});
|
|
13
|
+
controls.checkbox({id: "locked", value: "^locked", lbl: "State", label: "Disable controls"});
|
|
14
|
+
const fields = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
|
|
15
|
+
fields.colorpicker({id: "color", value: "^color", lbl: "Accent color", live: "^live", disabled: "^locked", box_padding: "8px"});
|
|
16
|
+
fields.colorpicker({id: "color-copy", value: "^color", lbl: "Same color", live: "^live", disabled: "^locked"});
|
|
17
|
+
pane.div({background_color: "^color", width: "6rem", height: "3rem", border: "1px solid #888", aria_label: "Color sample"});
|
|
18
|
+
pane.p("^color", {id: "color-value"});
|
|
19
|
+
for (const [path, value] of [["color", "#315fa8"], ["locked", false], ["live", true]]) pane.dataSetter({destination_path: path, value});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** This page needs no named client methods. */
|
|
24
|
+
export class Logic {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# 07 · Color and labelbox
|
|
2
|
+
|
|
3
|
+
Choose a color in either picker. Both controls and the swatch follow the same Data value.
|
|
4
|
+
|
|
5
|
+
**Uses:** the 0.3 input collection, shared labelbox, Source, Data and bindings. No endpoint calls, stores or validation engine.
|
|
6
|
+
|
|
7
|
+
<details>
|
|
8
|
+
<summary>More detail</summary>
|
|
9
|
+
|
|
10
|
+
`colorpicker` uses the native browser color dialog and a `#rrggbb` string. Live mode uses the input events the browser supplies; otherwise the value commits on change. The swatch uses ordinary Source style binding. The dialog appearance can differ between browsers.
|
|
11
|
+
|
|
12
|
+
All controls use `lbl`, reactive `lbl_position` (TL by default), and the shared `lbl_`/`box_` style prefixes. Disable the controls to compare their common behavior. Label layout variants are in Settings (⚙) in the gallery footer.
|
|
13
|
+
|
|
14
|
+
</details>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = "Horizontal slider";
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
|
|
7
|
+
main(root) {
|
|
8
|
+
const pane = root.main({class: 'example-page stack'});
|
|
9
|
+
pane.h1("Horizontal slider");
|
|
10
|
+
pane.p("Two horizontal sliders share a numeric level. Drag or use the arrow keys; switch live editing to compare immediate and committed updates.");
|
|
11
|
+
const controls = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
|
|
12
|
+
controls.checkbox({id: "live", value: "^live", lbl: "Editing", label: "Update while editing"});
|
|
13
|
+
controls.checkbox({id: "locked", value: "^locked", lbl: "State", label: "Disable controls"});
|
|
14
|
+
const fields = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
|
|
15
|
+
fields.horizontalSlider({id: "level", value: "^level", lbl: "Level", min: 0, max: 100, step: 5,
|
|
16
|
+
live: "^live", disabled: "^locked", box_padding: "8px"});
|
|
17
|
+
fields.horizontalSlider({id: "level-copy", value: "^level", lbl: "Same level", min: 0, max: 100, step: 5, live: "^live", disabled: "^locked"});
|
|
18
|
+
pane.p("^level", {id: "level-value"});
|
|
19
|
+
for (const [path, value] of [["level", 50], ["locked", false], ["live", true]]) pane.dataSetter({destination_path: path, value});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** This page needs no named client methods. */
|
|
24
|
+
export class Logic {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# 08 · Horizontal slider
|
|
2
|
+
|
|
3
|
+
Two horizontal sliders share a numeric level. Drag or use the arrow keys; switch live editing to compare immediate and committed updates.
|
|
4
|
+
|
|
5
|
+
**Uses:** the 0.3 input collection, shared labelbox, Source, Data and bindings. No endpoint calls, stores or validation engine.
|
|
6
|
+
|
|
7
|
+
<details>
|
|
8
|
+
<summary>More detail</summary>
|
|
9
|
+
|
|
10
|
+
`horizontalSlider` wraps a native range input. `min`, `max` and `step` define its numeric range and can also use Data pointers. The Data value is a number, not text. With live editing off, releasing the slider commits the value. Native clamping affects display and does not rewrite an out-of-range Data value by itself.
|
|
11
|
+
|
|
12
|
+
All controls use `lbl`, reactive `lbl_position` (TL by default), and the shared `lbl_`/`box_` style prefixes. Disable the controls to compare their common behavior. Label layout variants are in Settings (⚙) in the gallery footer.
|
|
13
|
+
|
|
14
|
+
</details>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = "Vertical slider";
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
|
|
7
|
+
main(root) {
|
|
8
|
+
const pane = root.main({class: 'example-page stack'});
|
|
9
|
+
pane.h1("Vertical slider");
|
|
10
|
+
pane.p("Vertical and horizontal sliders share the same level. The vertical minimum is at the bottom and the maximum at the top.");
|
|
11
|
+
const controls = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
|
|
12
|
+
controls.checkbox({id: "live", value: "^live", lbl: "Editing", label: "Update while editing"});
|
|
13
|
+
controls.checkbox({id: "locked", value: "^locked", lbl: "State", label: "Disable controls"});
|
|
14
|
+
const fields = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
|
|
15
|
+
fields.verticalSlider({id: "level", value: "^level", lbl: "Vertical level", min: 0, max: 100, step: 5,
|
|
16
|
+
live: "^live", disabled: "^locked", box_padding: "8px"});
|
|
17
|
+
fields.horizontalSlider({id: "level-copy", value: "^level", lbl: "Same level horizontally", min: 0, max: 100, step: 5, live: "^live", disabled: "^locked"});
|
|
18
|
+
pane.p("^level", {id: "level-value"});
|
|
19
|
+
for (const [path, value] of [["level", 50], ["locked", false], ["live", true]]) pane.dataSetter({destination_path: path, value});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** This page needs no named client methods. */
|
|
24
|
+
export class Logic {}
|