@gramlot/gramlot-examples 0.3.3 → 0.3.4
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 +6 -3
- package/package.json +4 -3
- package/src/gramlot_examples/catalog.json +54 -0
- package/src/gramlot_examples/gallery/dist/frame.js +548 -36
- package/src/gramlot_examples/gallery/dist/gallery.js +2387 -16677
- package/src/gramlot_examples/gallery/dist/inputs.js +12 -6
- package/src/gramlot_examples/gallery/dist/inspector.js +547 -35
- package/src/gramlot_examples/gallery/dist/layout.js +847 -28
- package/src/gramlot_examples/gallery/dist/notices.json +0 -24
- package/src/gramlot_examples/gallery/dist/theme-styles.js +1 -1
- package/src/gramlot_examples/gallery/gallery-page.js +66 -45
- package/src/gramlot_examples/gallery/gallery.css +61 -38
- package/src/gramlot_examples/gallery/guided-demo.js +151 -0
- package/src/gramlot_examples/gallery/page.js +1 -1
- package/src/gramlot_examples/index.js +1 -0
- package/src/gramlot_examples/pages/layout/01_border.js +80 -0
- package/src/gramlot_examples/pages/layout/01_border.md +23 -0
- package/src/gramlot_examples/pages/layout/02_nested.js +99 -0
- package/src/gramlot_examples/pages/layout/02_nested.md +23 -0
- package/src/gramlot_examples/pages/layout/03_rounded.js +111 -0
- package/src/gramlot_examples/pages/layout/03_rounded.md +25 -0
- package/src/gramlot_examples/pages/layout/04_drawer_handles.js +140 -0
- package/src/gramlot_examples/pages/layout/04_drawer_handles.md +33 -0
- package/src/gramlot_examples/pages/layout/05_nested_lab.js +251 -0
- package/src/gramlot_examples/pages/layout/05_nested_lab.md +25 -0
- package/src/gramlot_examples/pages/layout/06_border_events.js +118 -0
- package/src/gramlot_examples/pages/layout/06_border_events.md +25 -0
- package/src/gramlot_examples/pages/layout/07_tabs.js +300 -0
- package/src/gramlot_examples/pages/layout/07_tabs.md +37 -0
- package/src/gramlot_examples/pages/layout/08_stack.js +142 -0
- package/src/gramlot_examples/pages/layout/08_stack.md +27 -0
- package/src/gramlot_examples/pages/layout/README.md +15 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Page as BasePage } from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = 'Drawers: handles, content and retained Data';
|
|
5
|
+
static devices = ['desktop', 'tablet', 'phone'];
|
|
6
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
7
|
+
|
|
8
|
+
main(root) {
|
|
9
|
+
const pane = root.main({
|
|
10
|
+
class: 'example-page stack',
|
|
11
|
+
height: '100dvh',
|
|
12
|
+
box_sizing: 'border-box',
|
|
13
|
+
display: 'flex',
|
|
14
|
+
flex_direction: 'column',
|
|
15
|
+
overflow: 'hidden',
|
|
16
|
+
});
|
|
17
|
+
pane.h1('Small handles, readable labels');
|
|
18
|
+
pane.p('Open and resize the regions; the fields share the same Data.');
|
|
19
|
+
const border = pane.borderContainer({
|
|
20
|
+
id: 'handles-demo',
|
|
21
|
+
height: '100%',
|
|
22
|
+
min_height: 0,
|
|
23
|
+
flex: '1',
|
|
24
|
+
max_width: '64rem',
|
|
25
|
+
border: '1px solid var(--gramlot-border-control, #aaa)',
|
|
26
|
+
rounded: 12,
|
|
27
|
+
});
|
|
28
|
+
const surface = 'var(--gramlot-surface-subtle, #eef1f5)';
|
|
29
|
+
const search = border.contentPane({
|
|
30
|
+
region: 'top',
|
|
31
|
+
node_id: 'drawerSearch',
|
|
32
|
+
height: '64px',
|
|
33
|
+
splitter: true,
|
|
34
|
+
drawer: '^drawerSearch',
|
|
35
|
+
drawer_icon: '⌕',
|
|
36
|
+
drawer_label: 'Search',
|
|
37
|
+
drawer_title: 'Search',
|
|
38
|
+
drawer_minimize: true,
|
|
39
|
+
minSize: 36,
|
|
40
|
+
padding: '10px',
|
|
41
|
+
background: surface,
|
|
42
|
+
});
|
|
43
|
+
search.textbox({
|
|
44
|
+
value: '^search',
|
|
45
|
+
lbl: 'Search customers',
|
|
46
|
+
placeholder: 'Type a name',
|
|
47
|
+
width: '100%',
|
|
48
|
+
max_width: '20rem',
|
|
49
|
+
});
|
|
50
|
+
const filters = border.contentPane({
|
|
51
|
+
region: 'left',
|
|
52
|
+
node_id: 'drawerFilters',
|
|
53
|
+
width: '^filtersWidth',
|
|
54
|
+
splitter: true,
|
|
55
|
+
drawer: '^drawerFilters',
|
|
56
|
+
drawer_label: 'Filters',
|
|
57
|
+
drawer_title: 'Filters',
|
|
58
|
+
minSize: 70,
|
|
59
|
+
padding: '10px',
|
|
60
|
+
background: surface,
|
|
61
|
+
});
|
|
62
|
+
filters.h3('Filters');
|
|
63
|
+
filters.checkbox({ value: '^active', label: 'Active only', mode: 'switch' });
|
|
64
|
+
filters.radioButtonText({
|
|
65
|
+
value: '^delivery',
|
|
66
|
+
values: 'standard:Standard,express:Express,pickup:Pick up',
|
|
67
|
+
cols: 1,
|
|
68
|
+
});
|
|
69
|
+
const details = border.contentPane({
|
|
70
|
+
region: 'right',
|
|
71
|
+
node_id: 'drawerDetails',
|
|
72
|
+
width: '180px',
|
|
73
|
+
splitter: true,
|
|
74
|
+
drawer: '^drawerDetails',
|
|
75
|
+
drawer_label: 'Details',
|
|
76
|
+
drawer_title: 'Details',
|
|
77
|
+
drawer_minimize: true,
|
|
78
|
+
minSize: 70,
|
|
79
|
+
padding: '10px',
|
|
80
|
+
background: surface,
|
|
81
|
+
});
|
|
82
|
+
details.h3('Customer details');
|
|
83
|
+
details.textbox({ value: '^customer', lbl: 'Customer', width: '100%' });
|
|
84
|
+
details.p('Pull the closed handle inward to open this panel.');
|
|
85
|
+
border
|
|
86
|
+
.contentPane({
|
|
87
|
+
region: 'bottom',
|
|
88
|
+
node_id: 'drawerOutput',
|
|
89
|
+
height: '64px',
|
|
90
|
+
splitter: true,
|
|
91
|
+
drawer: '^drawerOutput',
|
|
92
|
+
drawer_label: 'Output',
|
|
93
|
+
drawer_title: 'Output',
|
|
94
|
+
minSize: 36,
|
|
95
|
+
padding: '10px',
|
|
96
|
+
background: surface,
|
|
97
|
+
})
|
|
98
|
+
.p('^note', { mask: 'Shared note: {}' });
|
|
99
|
+
const center = border.contentPane({
|
|
100
|
+
region: 'center',
|
|
101
|
+
padding: '16px',
|
|
102
|
+
background: 'var(--gramlot-surface, #fff)',
|
|
103
|
+
});
|
|
104
|
+
center.h2('Customer workspace');
|
|
105
|
+
center.p('^customer', { mask: 'Customer: {}' });
|
|
106
|
+
center.p('^delivery', { mask: 'Delivery: {}' });
|
|
107
|
+
center.textbox({
|
|
108
|
+
id: 'handles-note',
|
|
109
|
+
value: '^note',
|
|
110
|
+
lbl: 'Retained value',
|
|
111
|
+
width: '100%',
|
|
112
|
+
max_width: '18rem',
|
|
113
|
+
});
|
|
114
|
+
center.p(
|
|
115
|
+
'Move over a handle or focus it with Tab. Its label grows over the panel; the fields do not move.',
|
|
116
|
+
);
|
|
117
|
+
center.p(
|
|
118
|
+
'Click to toggle. Drag to resize, even from closed. On touch devices the captions remain visible and compact at rest.',
|
|
119
|
+
);
|
|
120
|
+
for (const title of ['Search', 'Filters', 'Details', 'Output']) {
|
|
121
|
+
pane.dataSetter({
|
|
122
|
+
destination_path: `drawer${title}`,
|
|
123
|
+
value: title === 'Details' ? 'close' : true,
|
|
124
|
+
});
|
|
125
|
+
pane.dataController({
|
|
126
|
+
script: "this.SET(path, open ? true : 'close')",
|
|
127
|
+
path: `drawer${title}`,
|
|
128
|
+
...{ [`subscribe_drawer${title}_closable_change`]: true },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
pane.dataSetter({ destination_path: 'filtersWidth', value: '180px' });
|
|
132
|
+
pane.dataSetter({ destination_path: 'customer', value: 'Ada Lovelace' });
|
|
133
|
+
pane.dataSetter({ destination_path: 'search', value: '' });
|
|
134
|
+
pane.dataSetter({ destination_path: 'active', value: true });
|
|
135
|
+
pane.dataSetter({ destination_path: 'delivery', value: 'standard' });
|
|
136
|
+
pane.dataSetter({ destination_path: 'note', value: 'Same Data, every presentation' });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class Logic {}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Make room without losing your work
|
|
2
|
+
|
|
3
|
+
Imagine editing a customer while keeping filters, customer details and a short output summary nearby. You need those tools, but you do not need them all on screen all the time. A **drawer** is a panel you can tuck away and bring back with a small handle.
|
|
4
|
+
|
|
5
|
+
This example has one on each side. Search is above the workspace, Filters is on the left, Details is on the right, and Output is below. Details starts closed.
|
|
6
|
+
|
|
7
|
+
## Let the page show you
|
|
8
|
+
|
|
9
|
+
In the gallery, open the lower **Guide** handle and press **Start / Resume**. We will change a delivery option, hide its panel, and bring it back. Then we will change a customer name and a note, showing where those values appear elsewhere.
|
|
10
|
+
|
|
11
|
+
You can pause whenever you want to try something. **Next** performs one step at a time. **Back** lets you reread an explanation; it does not undo changes. **Stop** leaves the example as it is. Starting again begins the demonstration again, including its changes to the customer, delivery and note.
|
|
12
|
+
|
|
13
|
+
If you prefer listening, enable **Voice**. It reads the same English explanation using a voice available in your browser. You can follow the lesson with text alone. The guide is part of the gallery, so you will not see it if you open this example on its own.
|
|
14
|
+
|
|
15
|
+
## Try it
|
|
16
|
+
|
|
17
|
+
Choose **Express** in Filters and read the delivery line in the center. Now close Filters with its handle. The delivery line still says Express. Open Filters again: Express is still selected.
|
|
18
|
+
|
|
19
|
+
That is the key idea. Closing a drawer hides a view; it does not erase the values behind it. Gramlot calls those shared values **Data**. The radio group and the delivery line read the same value, so they stay in agreement.
|
|
20
|
+
|
|
21
|
+
Open Details and enter a different customer name. Leave the field and look at the center. You did not have to copy the name from one widget to another: their bindings connect them to the same Data.
|
|
22
|
+
|
|
23
|
+
For one more experiment, close Output, change the note in the center and leave its field, then reopen Output. It shows the new note even though it was hidden when you made the edit.
|
|
24
|
+
|
|
25
|
+
## Small handles, deliberate choices
|
|
26
|
+
|
|
27
|
+
A click opens or closes a drawer. When a splitter is enabled, you can also drag the handle to resize its panel, even starting from closed. The icon belongs to the caption. By default both stay hidden in a thin grip until you interact; on touch it remains readable without starting zoomed. An author can explicitly request an always-visible caption with drawer_minimize=False.
|
|
28
|
+
|
|
29
|
+
## Find the idea in Python
|
|
30
|
+
|
|
31
|
+
In **Code**, look for the contentPane declarations. The region chooses a side; drawer enables opening and closing; drawer_label gives the handle a name. The values beginning with a caret connect those choices to Data. The small controllers below keep that Data up to date when you operate a handle yourself.
|
|
32
|
+
|
|
33
|
+
You do not need to understand every line to use a drawer. Start with a panel that opens and closes, then add resizing and bindings when your application needs them.
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { Page as BasePage } from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = 'Nested regions playground';
|
|
5
|
+
static devices = ['desktop', 'tablet', 'phone'];
|
|
6
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
7
|
+
|
|
8
|
+
main(root) {
|
|
9
|
+
const pane = root.main({ class: 'example-page stack' });
|
|
10
|
+
pane.h1('Five containers, one live workspace');
|
|
11
|
+
pane.p(
|
|
12
|
+
'Move the sliders, drag separators and close drawers. The same widgets and their values stay in place.',
|
|
13
|
+
);
|
|
14
|
+
const settings = pane.section({
|
|
15
|
+
display: 'flex',
|
|
16
|
+
flex_wrap: 'wrap',
|
|
17
|
+
gap: '12px',
|
|
18
|
+
align_items: 'end',
|
|
19
|
+
});
|
|
20
|
+
settings.filteringSelect({
|
|
21
|
+
id: 'lab-design',
|
|
22
|
+
value: '^design',
|
|
23
|
+
lbl: 'Outer design',
|
|
24
|
+
width: '10rem',
|
|
25
|
+
values: 'headline:Headline,sidebar:Sidebar',
|
|
26
|
+
});
|
|
27
|
+
settings.checkbox({
|
|
28
|
+
id: 'lab-minimize',
|
|
29
|
+
value: '^minimize',
|
|
30
|
+
lbl: 'Drawer captions',
|
|
31
|
+
label: 'Minimize',
|
|
32
|
+
});
|
|
33
|
+
settings.horizontalSlider({
|
|
34
|
+
id: 'lab-radius',
|
|
35
|
+
value: '^radius',
|
|
36
|
+
lbl: 'Corner radius',
|
|
37
|
+
min: 0,
|
|
38
|
+
max: 28,
|
|
39
|
+
live: true,
|
|
40
|
+
width: '12rem',
|
|
41
|
+
});
|
|
42
|
+
const sliders = pane.section({ display: 'flex', flex_wrap: 'wrap', gap: '12px' });
|
|
43
|
+
const sizes = [
|
|
44
|
+
['outer.top', 'Header height', 40, 110, 58],
|
|
45
|
+
['outer.bottom', 'Footer height', 35, 130, 46],
|
|
46
|
+
['outer.left', 'Navigation width', 90, 240, 150],
|
|
47
|
+
['outer.right', 'Details width', 90, 240, 170],
|
|
48
|
+
['workspace.left', 'Tools width', 70, 200, 105],
|
|
49
|
+
['workspace.bottom', 'Preview height', 50, 180, 90],
|
|
50
|
+
['editor.bottom', 'Notes height', 60, 180, 100],
|
|
51
|
+
];
|
|
52
|
+
for (const [path, caption, minimum, maximum, initial] of sizes) {
|
|
53
|
+
sliders.horizontalSlider({
|
|
54
|
+
id: 'size-' + path.replace('.', '-'),
|
|
55
|
+
value: '^regions.' + path,
|
|
56
|
+
lbl: `^regions.${path}:${caption} · {} px`,
|
|
57
|
+
min: minimum,
|
|
58
|
+
max: maximum,
|
|
59
|
+
step: 1,
|
|
60
|
+
live: true,
|
|
61
|
+
width: '12rem',
|
|
62
|
+
});
|
|
63
|
+
pane.dataSetter({ destination_path: 'regions.' + path, value: initial });
|
|
64
|
+
}
|
|
65
|
+
const surface = 'var(--gramlot-surface, #fff)';
|
|
66
|
+
const edge = '1px solid var(--gramlot-border-control, #8192aa)';
|
|
67
|
+
const fills = {
|
|
68
|
+
nav: 'color-mix(in srgb, var(--gramlot-surface, #fff) 90%, #4488cc)',
|
|
69
|
+
tools: 'color-mix(in srgb, var(--gramlot-surface, #fff) 90%, #32a880)',
|
|
70
|
+
details: 'color-mix(in srgb, var(--gramlot-surface, #fff) 90%, #bb8844)',
|
|
71
|
+
preview: 'color-mix(in srgb, var(--gramlot-surface, #fff) 90%, #9977bb)',
|
|
72
|
+
};
|
|
73
|
+
const drawer = { splitter: true, drawer: true, drawer_minimize: '^minimize' };
|
|
74
|
+
const outer = pane.borderContainer({
|
|
75
|
+
id: 'lab-outer',
|
|
76
|
+
height: '700px',
|
|
77
|
+
max_width: '78rem',
|
|
78
|
+
design: '^design',
|
|
79
|
+
rounded: '^radius',
|
|
80
|
+
border: edge,
|
|
81
|
+
background: surface,
|
|
82
|
+
});
|
|
83
|
+
const header = outer.contentPane({
|
|
84
|
+
region: 'top',
|
|
85
|
+
height: '^regions.outer.top:{}px',
|
|
86
|
+
minSize: 40,
|
|
87
|
+
maxSize: 110,
|
|
88
|
+
drawer_label: 'Header',
|
|
89
|
+
padding: '10px',
|
|
90
|
+
background: fills['nav'],
|
|
91
|
+
...drawer,
|
|
92
|
+
});
|
|
93
|
+
header.strong('Customer workspace · ');
|
|
94
|
+
header.span('^customer');
|
|
95
|
+
const footer = outer.contentPane({
|
|
96
|
+
region: 'bottom',
|
|
97
|
+
height: '^regions.outer.bottom:{}px',
|
|
98
|
+
minSize: 35,
|
|
99
|
+
maxSize: 130,
|
|
100
|
+
drawer_label: 'Status',
|
|
101
|
+
padding: '10px',
|
|
102
|
+
background: fills['preview'],
|
|
103
|
+
...drawer,
|
|
104
|
+
});
|
|
105
|
+
footer.span('^customer', { mask: 'Customer: {}' });
|
|
106
|
+
footer.span('^delivery', { mask: ' · Delivery: {}' });
|
|
107
|
+
const nav = outer.borderContainer({
|
|
108
|
+
id: 'lab-navigation',
|
|
109
|
+
region: 'left',
|
|
110
|
+
width: '^regions.outer.left:{}px',
|
|
111
|
+
minSize: 90,
|
|
112
|
+
maxSize: 240,
|
|
113
|
+
drawer_label: 'Navigation',
|
|
114
|
+
background: fills['nav'],
|
|
115
|
+
...drawer,
|
|
116
|
+
});
|
|
117
|
+
nav.contentPane({ region: 'top', height: '40px', padding: '10px' }).strong('Navigation');
|
|
118
|
+
const navbody = nav.contentPane({ region: 'center', padding: '12px' });
|
|
119
|
+
navbody.textbox({ id: 'lab-search', value: '^search', lbl: 'Search', width: '100%' });
|
|
120
|
+
navbody.radioButtonText({
|
|
121
|
+
value: '^section',
|
|
122
|
+
lbl: 'Section',
|
|
123
|
+
values: 'profile:Profile,orders:Orders,history:History',
|
|
124
|
+
cols: 1,
|
|
125
|
+
});
|
|
126
|
+
nav.contentPane({ region: 'bottom', height: '54px', padding: '10px' }).span('^section', {
|
|
127
|
+
mask: 'Section: {}',
|
|
128
|
+
});
|
|
129
|
+
const details = outer.borderContainer({
|
|
130
|
+
id: 'lab-details',
|
|
131
|
+
region: 'right',
|
|
132
|
+
width: '^regions.outer.right:{}px',
|
|
133
|
+
minSize: 90,
|
|
134
|
+
maxSize: 240,
|
|
135
|
+
drawer_label: 'Details',
|
|
136
|
+
background: fills['details'],
|
|
137
|
+
...drawer,
|
|
138
|
+
});
|
|
139
|
+
details.contentPane({ region: 'top', height: '40px', padding: '10px' }).strong('Details');
|
|
140
|
+
const detailfields = details
|
|
141
|
+
.contentPane({ region: 'center', padding: '12px' })
|
|
142
|
+
.formlet({ columns: 1 });
|
|
143
|
+
detailfields.checkbox({ value: '^active', lbl: 'Active', mode: 'switch' });
|
|
144
|
+
detailfields.textbox({ value: '^customer', lbl: 'Same customer', width: '100%' });
|
|
145
|
+
detailfields.dateTextBox({
|
|
146
|
+
id: 'lab-date',
|
|
147
|
+
value: '^appointment',
|
|
148
|
+
lbl: 'Appointment',
|
|
149
|
+
width: '100%',
|
|
150
|
+
});
|
|
151
|
+
details
|
|
152
|
+
.contentPane({ region: 'bottom', height: '60px', padding: '10px' })
|
|
153
|
+
.p('An independent nested container.');
|
|
154
|
+
const work = outer.borderContainer({
|
|
155
|
+
id: 'lab-workspace',
|
|
156
|
+
region: 'center',
|
|
157
|
+
rounded: '^radius',
|
|
158
|
+
border: edge,
|
|
159
|
+
});
|
|
160
|
+
const tools = work.contentPane({
|
|
161
|
+
region: 'left',
|
|
162
|
+
width: '^regions.workspace.left:{}px',
|
|
163
|
+
minSize: 70,
|
|
164
|
+
maxSize: 200,
|
|
165
|
+
drawer_label: 'Tools',
|
|
166
|
+
padding: '10px',
|
|
167
|
+
background: fills['tools'],
|
|
168
|
+
...drawer,
|
|
169
|
+
});
|
|
170
|
+
tools.strong('Tools');
|
|
171
|
+
tools.checkbox({ value: '^active', lbl: 'Enabled', mode: 'switch' });
|
|
172
|
+
tools.checkbox({ value: '^readonly', lbl: 'Editor', label: 'Read only' });
|
|
173
|
+
const preview = work.contentPane({
|
|
174
|
+
region: 'bottom',
|
|
175
|
+
height: '^regions.workspace.bottom:{}px',
|
|
176
|
+
minSize: 50,
|
|
177
|
+
maxSize: 180,
|
|
178
|
+
drawer_label: 'Preview',
|
|
179
|
+
padding: '12px',
|
|
180
|
+
background: fills['preview'],
|
|
181
|
+
...drawer,
|
|
182
|
+
});
|
|
183
|
+
preview.strong('Shared note');
|
|
184
|
+
preview.p('^note');
|
|
185
|
+
const editor = work.borderContainer({
|
|
186
|
+
id: 'lab-editor',
|
|
187
|
+
region: 'center',
|
|
188
|
+
rounded: '^radius',
|
|
189
|
+
border: edge,
|
|
190
|
+
background: surface,
|
|
191
|
+
});
|
|
192
|
+
editor
|
|
193
|
+
.contentPane({ region: 'top', height: '40px', padding: '10px' })
|
|
194
|
+
.strong('Editor · third nesting level');
|
|
195
|
+
const fields = editor
|
|
196
|
+
.contentPane({ region: 'center', padding: '16px 24px' })
|
|
197
|
+
.formlet({ columns: 1, max_width: '22rem' });
|
|
198
|
+
fields.textbox({
|
|
199
|
+
id: 'lab-customer',
|
|
200
|
+
value: '^customer',
|
|
201
|
+
lbl: 'Customer (2–24 characters)',
|
|
202
|
+
validate_len: '2:24',
|
|
203
|
+
readonly: '^readonly',
|
|
204
|
+
width: '100%',
|
|
205
|
+
});
|
|
206
|
+
fields.filteringSelect({
|
|
207
|
+
id: 'lab-delivery',
|
|
208
|
+
value: '^delivery',
|
|
209
|
+
lbl: 'Delivery',
|
|
210
|
+
width: '100%',
|
|
211
|
+
values: 'standard:Standard,express:Express,pickup:Pick up',
|
|
212
|
+
});
|
|
213
|
+
fields.numberTextBox({ id: 'lab-quantity', value: '^quantity', lbl: 'Quantity', width: '100%' });
|
|
214
|
+
const notes = editor.contentPane({
|
|
215
|
+
region: 'bottom',
|
|
216
|
+
height: '^regions.editor.bottom:{}px',
|
|
217
|
+
minSize: 60,
|
|
218
|
+
maxSize: 180,
|
|
219
|
+
drawer_label: 'Notes',
|
|
220
|
+
padding: '10px 24px',
|
|
221
|
+
background: fills['tools'],
|
|
222
|
+
...drawer,
|
|
223
|
+
});
|
|
224
|
+
notes.textBoxArea({
|
|
225
|
+
id: 'lab-note',
|
|
226
|
+
value: '^note',
|
|
227
|
+
lbl: 'Retained draft',
|
|
228
|
+
rows: 2,
|
|
229
|
+
live: true,
|
|
230
|
+
width: '100%',
|
|
231
|
+
});
|
|
232
|
+
for (const [key, value] of [
|
|
233
|
+
['design', 'headline'],
|
|
234
|
+
['minimize', true],
|
|
235
|
+
['radius', 12],
|
|
236
|
+
['customer', 'Ada Lovelace'],
|
|
237
|
+
['search', ''],
|
|
238
|
+
['section', 'profile'],
|
|
239
|
+
['active', true],
|
|
240
|
+
['readonly', false],
|
|
241
|
+
['delivery', 'standard'],
|
|
242
|
+
['quantity', 3],
|
|
243
|
+
['appointment', new Date(Date.UTC(2026, 9, 18))],
|
|
244
|
+
['note', 'Resize any region: this note stays here.'],
|
|
245
|
+
]) {
|
|
246
|
+
pane.dataSetter({ destination_path: key, value: value });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export class Logic {}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Explore a layout by moving one thing at a time
|
|
2
|
+
|
|
3
|
+
This is a small customer workspace with several layouts inside it. It is deliberately busier than the first examples: navigation, details, an editor and output all need space. The sliders let you see which container owns which area.
|
|
4
|
+
|
|
5
|
+
If the page looks complicated at first, start with just one slider. You do not need to understand the whole arrangement at once.
|
|
6
|
+
|
|
7
|
+
## Try it
|
|
8
|
+
|
|
9
|
+
Move one region-size slider a little, then back again. Follow the boundary that moves. Does it belong to the outer workspace, or to a layout inside it? Repeat with a slider for a different level.
|
|
10
|
+
|
|
11
|
+
Now enter a customer name and a note. Change a date or delivery option too. Look for the places where those values are repeated. They are views of shared Data, not separate copies of the customer.
|
|
12
|
+
|
|
13
|
+
Move the sliders again. The regions become wider, narrower, taller or shorter while your edits remain. Close one drawer, rearrange another area, and reopen it. Hiding a panel has not discarded its contents.
|
|
14
|
+
|
|
15
|
+
Finally, try the design and corner-radius controls. These change presentation while the same customer workspace stays alive.
|
|
16
|
+
|
|
17
|
+
## How a number becomes a size
|
|
18
|
+
|
|
19
|
+
A slider produces a number. A region width needs a size such as 180px. The example connects them with a pointer template: it reads the slider's Data value and adds the unit. You can find these declarations by looking for the region-size paths in **Code**.
|
|
20
|
+
|
|
21
|
+
The nested containers still have a simple responsibility: each lays out its own immediate children. The outer container does not calculate the positions of every field inside the editor.
|
|
22
|
+
|
|
23
|
+
## What to take into your own page
|
|
24
|
+
|
|
25
|
+
Build the outer layout first, then give a region its own container only when it needs independent areas. Connect sizes to Data if users or application logic need to control them. Ordinary splitters are enough when you only want people to resize by dragging.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Page as BasePage } from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = 'Border events and controllers';
|
|
5
|
+
static devices = ['desktop', 'tablet', 'phone'];
|
|
6
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
7
|
+
|
|
8
|
+
main(root) {
|
|
9
|
+
const pane = root.main({ class: 'example-page stack', datapath: 'events' });
|
|
10
|
+
for (const [name, value] of [
|
|
11
|
+
['drawerCount', 0],
|
|
12
|
+
['resizeCount', 0],
|
|
13
|
+
['drawerMessage', 'No visibility change yet'],
|
|
14
|
+
['resizeMessage', 'No completed resize yet'],
|
|
15
|
+
['drawerPayload', '{}'],
|
|
16
|
+
['resizePayload', '{}'],
|
|
17
|
+
['hideFilters', false],
|
|
18
|
+
['name', 'Ada'],
|
|
19
|
+
['note', 'Keep this draft'],
|
|
20
|
+
]) {
|
|
21
|
+
pane.dataSetter({ destination_path: `.${name}`, value: value });
|
|
22
|
+
}
|
|
23
|
+
pane.dataController({
|
|
24
|
+
script: "this.SET('.drawerCount', count + 1); this.SET('.drawerMessage', region + ': ' + (open ? 'open' : 'closed')); this.SET('.drawerPayload', JSON.stringify(_subscription_kwargs));",
|
|
25
|
+
count: '=.drawerCount',
|
|
26
|
+
subscribe_eventFilters_closable_change: true,
|
|
27
|
+
});
|
|
28
|
+
pane.dataController({
|
|
29
|
+
script: "this.SET('.resizeCount', count + 1); this.SET('.resizeMessage', region + ': ' + previousSize + ' → ' + size); this.SET('.resizePayload', JSON.stringify(_subscription_kwargs));",
|
|
30
|
+
count: '=.resizeCount',
|
|
31
|
+
subscribe_eventWorkspace_splitter_resize: true,
|
|
32
|
+
});
|
|
33
|
+
pane.h1('Observe a drawer and a splitter');
|
|
34
|
+
pane.p(
|
|
35
|
+
'Toggle Filters, drag a separator, or focus a separator and use its arrow keys. The controllers below receive the published payloads.',
|
|
36
|
+
);
|
|
37
|
+
pane.checkbox({ id: 'events-hide', value: '^.hideFilters', label: 'Hide Filters from Data' });
|
|
38
|
+
const border = pane.borderContainer({
|
|
39
|
+
id: 'events-border',
|
|
40
|
+
node_id: 'eventWorkspace',
|
|
41
|
+
height: '360px',
|
|
42
|
+
max_width: '48rem',
|
|
43
|
+
rounded: 10,
|
|
44
|
+
border: '1px solid var(--gramlot-border-control, #aaa)',
|
|
45
|
+
});
|
|
46
|
+
const filters = border.contentPane({
|
|
47
|
+
node_id: 'eventFilters',
|
|
48
|
+
region: 'left',
|
|
49
|
+
width: '130px',
|
|
50
|
+
minSize: 90,
|
|
51
|
+
maxSize: 230,
|
|
52
|
+
splitter: true,
|
|
53
|
+
drawer: true,
|
|
54
|
+
drawer_label: 'Filters',
|
|
55
|
+
hidden: '^.hideFilters',
|
|
56
|
+
padding: '12px',
|
|
57
|
+
background: 'var(--gramlot-surface-subtle, #eef1f5)',
|
|
58
|
+
border_right: '1px solid var(--gramlot-border-control, #aaa)',
|
|
59
|
+
});
|
|
60
|
+
filters.p('This pane publishes closable_change.');
|
|
61
|
+
filters.textbox({
|
|
62
|
+
id: 'events-name',
|
|
63
|
+
value: '^.name',
|
|
64
|
+
lbl: 'Name (2–20)',
|
|
65
|
+
validate_len: '2:20',
|
|
66
|
+
width: '100%',
|
|
67
|
+
});
|
|
68
|
+
const center = border.contentPane({
|
|
69
|
+
region: 'center',
|
|
70
|
+
padding: '16px',
|
|
71
|
+
background: 'var(--gramlot-surface, #fff)',
|
|
72
|
+
});
|
|
73
|
+
center.textBoxArea({
|
|
74
|
+
id: 'events-note',
|
|
75
|
+
value: '^.note',
|
|
76
|
+
lbl: 'Retained draft',
|
|
77
|
+
rows: 3,
|
|
78
|
+
width: '100%',
|
|
79
|
+
max_width: '18rem',
|
|
80
|
+
});
|
|
81
|
+
center.p(
|
|
82
|
+
'Live resizing keeps these widgets mounted. The resize counter changes only when the gesture finishes.',
|
|
83
|
+
);
|
|
84
|
+
border
|
|
85
|
+
.contentPane({
|
|
86
|
+
region: 'bottom',
|
|
87
|
+
height: '52px',
|
|
88
|
+
minSize: 36,
|
|
89
|
+
maxSize: 120,
|
|
90
|
+
splitter: true,
|
|
91
|
+
padding: '12px',
|
|
92
|
+
border_top: '1px solid var(--gramlot-border-control, #aaa)',
|
|
93
|
+
background: 'var(--gramlot-surface-subtle, #eef1f5)',
|
|
94
|
+
})
|
|
95
|
+
.p('Resize this bottom region too.');
|
|
96
|
+
const report = pane.section({ class: 'card stack', max_width: '48rem' });
|
|
97
|
+
report.h2('Received by dataController');
|
|
98
|
+
report.p('^.drawerCount', { id: 'events-drawer-count', mask: 'Visibility events: {}' });
|
|
99
|
+
report.p('^.drawerMessage');
|
|
100
|
+
report.pre('^.drawerPayload', {
|
|
101
|
+
id: 'events-drawer-payload',
|
|
102
|
+
white_space: 'pre-wrap',
|
|
103
|
+
overflow_wrap: 'anywhere',
|
|
104
|
+
});
|
|
105
|
+
report.p('^.resizeCount', { id: 'events-resize-count', mask: 'Completed resizes: {}' });
|
|
106
|
+
report.p('^.resizeMessage');
|
|
107
|
+
report.pre('^.resizePayload', {
|
|
108
|
+
id: 'events-resize-payload',
|
|
109
|
+
white_space: 'pre-wrap',
|
|
110
|
+
overflow_wrap: 'anywhere',
|
|
111
|
+
});
|
|
112
|
+
pane.p(
|
|
113
|
+
"No events are emitted at startup. The Data checkbox changes visibility too; it does not change the drawer's own open/closed preference.",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export class Logic {}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Let another part of your page know what happened
|
|
2
|
+
|
|
3
|
+
Suppose you want a small status message whenever someone closes a panel. You should not have to watch the browser's HTML or write a second click handler for the drawer. The container can announce the change, and your page can listen.
|
|
4
|
+
|
|
5
|
+
Those announcements are **events**. A **dataController** is a small piece of page logic that runs when an event it subscribes to arrives. Here, controllers turn drawer and splitter events into counters and readable messages.
|
|
6
|
+
|
|
7
|
+
## Try it
|
|
8
|
+
|
|
9
|
+
Close Filters and look at **Visibility events**. The count increases and the message says that the left region is closed. Open it again: you receive another change.
|
|
10
|
+
|
|
11
|
+
Now drag a separator slowly. The panel follows your hand immediately, but **Completed resizes** changes when you finish dragging. This is useful if your application wants to react to the final size rather than every small movement.
|
|
12
|
+
|
|
13
|
+
Use **Hide Filters from Data**. The region disappears and the visibility report updates too. An event describes a change in the interface, not just a particular mouse click. Untick it to restore visibility; the drawer's own open or closed preference is a separate setting.
|
|
14
|
+
|
|
15
|
+
## Reading the extra details
|
|
16
|
+
|
|
17
|
+
The structured text below each message is the event's **payload**: the details sent with the announcement. For visibility, look for the region and whether it is open. For resizing, compare the previous size with the new size. You do not need to memorize the whole object.
|
|
18
|
+
|
|
19
|
+
## Connecting your own controller
|
|
20
|
+
|
|
21
|
+
The Filters pane has a node_id of eventFilters. The controller's subscribe_eventFilters_closable_change parameter listens to its visibility announcement. The other subscription names eventWorkspace, the container, and splitter_resize, its completed-resize event.
|
|
22
|
+
|
|
23
|
+
Read the name in three parts: subscribe, the component's identity, and the event. The controller receives the details and writes its result into Data. The counters on screen then update through normal bindings.
|
|
24
|
+
|
|
25
|
+
No event is counted just because the page starts. Try an action and watch the result: this is a conversation between existing components, not a replacement for their normal behavior.
|