@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.
Files changed (32) hide show
  1. package/README.md +6 -3
  2. package/package.json +4 -3
  3. package/src/gramlot_examples/catalog.json +54 -0
  4. package/src/gramlot_examples/gallery/dist/frame.js +548 -36
  5. package/src/gramlot_examples/gallery/dist/gallery.js +2387 -16677
  6. package/src/gramlot_examples/gallery/dist/inputs.js +12 -6
  7. package/src/gramlot_examples/gallery/dist/inspector.js +547 -35
  8. package/src/gramlot_examples/gallery/dist/layout.js +847 -28
  9. package/src/gramlot_examples/gallery/dist/notices.json +0 -24
  10. package/src/gramlot_examples/gallery/dist/theme-styles.js +1 -1
  11. package/src/gramlot_examples/gallery/gallery-page.js +66 -45
  12. package/src/gramlot_examples/gallery/gallery.css +61 -38
  13. package/src/gramlot_examples/gallery/guided-demo.js +151 -0
  14. package/src/gramlot_examples/gallery/page.js +1 -1
  15. package/src/gramlot_examples/index.js +1 -0
  16. package/src/gramlot_examples/pages/layout/01_border.js +80 -0
  17. package/src/gramlot_examples/pages/layout/01_border.md +23 -0
  18. package/src/gramlot_examples/pages/layout/02_nested.js +99 -0
  19. package/src/gramlot_examples/pages/layout/02_nested.md +23 -0
  20. package/src/gramlot_examples/pages/layout/03_rounded.js +111 -0
  21. package/src/gramlot_examples/pages/layout/03_rounded.md +25 -0
  22. package/src/gramlot_examples/pages/layout/04_drawer_handles.js +140 -0
  23. package/src/gramlot_examples/pages/layout/04_drawer_handles.md +33 -0
  24. package/src/gramlot_examples/pages/layout/05_nested_lab.js +251 -0
  25. package/src/gramlot_examples/pages/layout/05_nested_lab.md +25 -0
  26. package/src/gramlot_examples/pages/layout/06_border_events.js +118 -0
  27. package/src/gramlot_examples/pages/layout/06_border_events.md +25 -0
  28. package/src/gramlot_examples/pages/layout/07_tabs.js +300 -0
  29. package/src/gramlot_examples/pages/layout/07_tabs.md +37 -0
  30. package/src/gramlot_examples/pages/layout/08_stack.js +142 -0
  31. package/src/gramlot_examples/pages/layout/08_stack.md +27 -0
  32. package/src/gramlot_examples/pages/layout/README.md +15 -0
@@ -0,0 +1,300 @@
1
+ import { Page as BasePage } from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Tabs, selection and closing';
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: 'tabsDemo' });
10
+ for (const [key, value] of [
11
+ ['page', 'customer'],
12
+ ['index', 0],
13
+ ['name', 'Ada'],
14
+ ['notes', 'These Data survive closing Notes.'],
15
+ ['variantMessage', 'Please confirm the delivery address.'],
16
+ ['mode', 'tabs'],
17
+ ['position', 'top'],
18
+ ['align', 'start'],
19
+ ['unread', 3],
20
+ ['badgeColor', 'crimson'],
21
+ ['notesIcon', '✉'],
22
+ ['locked', false],
23
+ ['events', ''],
24
+ ['closed', 'None'],
25
+ ]) {
26
+ pane.dataSetter({ destination_path: `.${key}`, value: value });
27
+ }
28
+ for (const topic of ['hiding', 'showing', 'selected', 'closing', 'closed']) {
29
+ pane.dataController({
30
+ script: "this.SET('.events', history + topic + ': ' + JSON.stringify(_subscription_kwargs) + '\\n');",
31
+ history: '=.events',
32
+ topic: topic,
33
+ ...{ [`subscribe_editor_${topic}`]: true },
34
+ });
35
+ }
36
+ pane.dataController({ script: "this.SET('.closed', pageName)", subscribe_editor_closed: true });
37
+ this.tab_variants(pane);
38
+ this.tab_alignment(pane);
39
+ pane.h2('Try the controls, selection and closing');
40
+ pane.p(
41
+ 'Try a one-letter name, switch away and return: the invalid draft remains. Close Notes: its Source disappears, but the saved notes below remain.',
42
+ );
43
+ const controls = pane.formlet({ col_min_width: '14rem', max_width: '36rem' });
44
+ controls.filteringSelect({
45
+ id: 'tabs-choice',
46
+ value: '^.page',
47
+ lbl: 'Selected page',
48
+ values: 'customer:Customer,notes:Notes,layout:Nested border',
49
+ });
50
+ controls.filteringSelect({
51
+ id: 'tabs-mode',
52
+ value: '^.mode',
53
+ lbl: 'Presentation',
54
+ values: 'tabs:Tabs,underline:Underline',
55
+ });
56
+ controls.filteringSelect({
57
+ id: 'tabs-position',
58
+ value: '^.position',
59
+ lbl: 'Tab position',
60
+ values: 'top:Top,bottom:Bottom,left:Left,right:Right',
61
+ });
62
+ controls.filteringSelect({
63
+ id: 'tabs-align',
64
+ value: '^.align',
65
+ lbl: 'Alignment along the strip',
66
+ values: 'start:Start,center:Center,end:End',
67
+ });
68
+ controls.horizontalSlider({
69
+ id: 'tabs-unread',
70
+ value: '^.unread',
71
+ lbl: 'Unread messages',
72
+ min: 0,
73
+ max: 12,
74
+ step: 1,
75
+ live: true,
76
+ width: '14rem',
77
+ max_width: '100%',
78
+ });
79
+ controls.filteringSelect({
80
+ id: 'tabs-badge-color',
81
+ value: '^.badgeColor',
82
+ lbl: 'Badge color',
83
+ values: 'crimson:Crimson,seagreen:Green,royalblue:Blue',
84
+ });
85
+ controls.filteringSelect({
86
+ id: 'tabs-icon',
87
+ value: '^.notesIcon',
88
+ lbl: 'Notes icon',
89
+ values: '✉:Envelope,●:Dot,★:Star',
90
+ });
91
+ controls.checkbox({
92
+ id: 'tabs-lock',
93
+ value: '^.locked',
94
+ label: 'Prevent closing Notes',
95
+ mode: 'switch',
96
+ });
97
+ pane.p(
98
+ 'Change position and presentation while keeping a draft. Move the unread slider while Notes is inactive: its badge updates immediately.',
99
+ );
100
+ pane.p('^.index', { id: 'tabs-index', mask: 'Selected index: {}' });
101
+ const tabs = pane.tabContainer({
102
+ id: 'tabs-demo',
103
+ node_id: 'editor',
104
+ selectedPage: '^.page',
105
+ selected: '^.index',
106
+ tabMode: '^.mode',
107
+ tabPosition: '^.position',
108
+ tabAlign: '^.align',
109
+ height: '300px',
110
+ max_width: '44rem',
111
+ border: '1px solid var(--gramlot-border-control, #aaa)',
112
+ rounded: 6,
113
+ });
114
+ const customer = tabs.contentPane({
115
+ id: 'customer-panel',
116
+ pageName: 'customer',
117
+ title: 'Customer',
118
+ icon: '👤',
119
+ padding: '12px',
120
+ });
121
+ customer.textbox({
122
+ id: 'tabs-name',
123
+ value: '^.name',
124
+ lbl: 'Name (2–20)',
125
+ validate_len: '2:20',
126
+ live: true,
127
+ width: '15rem',
128
+ });
129
+ customer.p('The accepted Data value:');
130
+ customer.p('^.name', { id: 'tabs-accepted' });
131
+ const notes = tabs.contentPane({
132
+ id: 'notes-panel',
133
+ pageName: 'notes',
134
+ title: 'Notes',
135
+ icon: '^.notesIcon',
136
+ badge: '^.unread',
137
+ badge_color: '^.badgeColor',
138
+ closable: true,
139
+ onClose: "return !this.GET('.locked')",
140
+ padding: '12px',
141
+ });
142
+ notes.textBoxArea({
143
+ id: 'tabs-notes',
144
+ value: '^.notes',
145
+ lbl: 'Notes',
146
+ rows: 3,
147
+ width: '100%',
148
+ max_width: '24rem',
149
+ });
150
+ const nested = tabs.borderContainer({
151
+ id: 'tabs-border',
152
+ pageName: 'layout',
153
+ title: 'Nested border',
154
+ icon: '▦',
155
+ });
156
+ nested
157
+ .contentPane({
158
+ region: 'left',
159
+ width: '100px',
160
+ splitter: true,
161
+ drawer: true,
162
+ drawer_label: 'Tools',
163
+ padding: '10px',
164
+ background: 'var(--gramlot-surface-subtle, #eef1f5)',
165
+ })
166
+ .p('Resize me');
167
+ nested
168
+ .contentPane({ region: 'center', padding: '12px' })
169
+ .textbox({ value: '^.name', lbl: 'Same accepted name', width: '15rem' });
170
+ pane.p('^.closed', { id: 'tabs-closed', mask: 'Last closed: {}' });
171
+ pane.p('^.notes', { id: 'tabs-saved-notes', mask: 'Saved notes: {}' });
172
+ pane.h2('Container events');
173
+ pane.pre('^.events', {
174
+ id: 'tabs-events',
175
+ max_height: '12rem',
176
+ overflow: 'auto',
177
+ white_space: 'pre-wrap',
178
+ overflow_wrap: 'anywhere',
179
+ });
180
+ }
181
+
182
+ tab_variants(pane) {
183
+ pane.h1('See the tab variants side by side');
184
+ pane.p(
185
+ 'Compare shaped tabs with a light underline, on each of the four sides. Every card is a working editor. Change the name in one: the others show the same Data.',
186
+ );
187
+ pane.p(
188
+ 'Top and bottom use a horizontal row. Left and right use a vertical list; the words remain horizontal so they are easy to read.',
189
+ );
190
+ const grid = pane.section({
191
+ id: 'tab-variants',
192
+ display: 'grid',
193
+ grid_template_columns: 'repeat(auto-fit, minmax(min(100%, 24rem), 1fr))',
194
+ gap: '16px',
195
+ width: '100%',
196
+ max_width: '72rem',
197
+ });
198
+ for (const [position, caption] of [
199
+ ['top', 'Top'],
200
+ ['bottom', 'Bottom'],
201
+ ['left', 'Left'],
202
+ ['right', 'Right'],
203
+ ]) {
204
+ for (const [mode, presentation] of [
205
+ ['tabs', 'Shaped tabs'],
206
+ ['underline', 'Underline'],
207
+ ]) {
208
+ const key = `${mode}-${position}`;
209
+ this.variant_card(grid, key, `${presentation} · ${caption}`, mode, position);
210
+ }
211
+ }
212
+ }
213
+
214
+ tab_alignment(pane) {
215
+ pane.h2('Align the labels along the strip');
216
+ pane.p(
217
+ 'Position chooses the side of the panel. Alignment chooses where the labels sit along that side. Compare the three horizontal rows, then the three vertical lists.',
218
+ );
219
+ const grid = pane.section({
220
+ id: 'tab-alignment',
221
+ display: 'grid',
222
+ grid_template_columns: 'repeat(auto-fit, minmax(min(100%, 22rem), 1fr))',
223
+ gap: '16px',
224
+ width: '100%',
225
+ max_width: '72rem',
226
+ });
227
+ for (const [position, axis, captions] of [
228
+ ['top', 'Horizontal', ['Left', 'Center', 'Right']],
229
+ ['left', 'Vertical', ['Top', 'Center', 'Bottom']],
230
+ ]) {
231
+ for (const [align, caption] of ['start', 'center', 'end'].map((value, i) => [
232
+ value,
233
+ captions[i],
234
+ ])) {
235
+ this.variant_card(
236
+ grid,
237
+ `align-${position}-${align}`,
238
+ `${axis} · ${caption}`,
239
+ 'underline',
240
+ position,
241
+ align,
242
+ );
243
+ }
244
+ }
245
+ }
246
+
247
+ variant_card(grid, key, title, mode, position, align = 'start') {
248
+ const card = grid.section({ min_width: 0 });
249
+ card.h2(title, { font_size: '1rem', margin: '0 0 6px' });
250
+ card.p(`tabMode="${mode}", tabPosition="${position}", tabAlign="${align}"`, {
251
+ font_family: 'var(--gramlot-font-code)',
252
+ font_size: '.8rem',
253
+ margin: '0 0 8px',
254
+ });
255
+ const tabs = card.tabContainer({
256
+ id: `tabs-variant-${key}`,
257
+ tabMode: mode,
258
+ tabPosition: position,
259
+ tabAlign: align,
260
+ height: '240px',
261
+ width: '100%',
262
+ border: '1px solid var(--gramlot-border-control)',
263
+ rounded: 6,
264
+ });
265
+ const profile = tabs.contentPane({ pageName: 'profile', title: 'Profile', padding: '12px' });
266
+ profile.textbox({
267
+ id: `variant-name-${key}`,
268
+ value: '^.name',
269
+ lbl: 'Customer name',
270
+ width: '100%',
271
+ max_width: '15rem',
272
+ live: true,
273
+ });
274
+ profile.p('Switch to Messages and back. Your edit stays here.', { font_size: '.9rem' });
275
+ const messages = tabs.contentPane({
276
+ pageName: 'messages',
277
+ title: 'Messages',
278
+ icon: '✉',
279
+ badge: '^.unread',
280
+ badge_color: '^.badgeColor',
281
+ padding: '12px',
282
+ });
283
+ messages.textBoxArea({
284
+ value: '^.variantMessage',
285
+ lbl: 'Message',
286
+ rows: 2,
287
+ width: '100%',
288
+ live: true,
289
+ });
290
+ messages.p('The badge remains visible when this panel is hidden.', { font_size: '.9rem' });
291
+ const activity = tabs.contentPane({ pageName: 'activity', title: 'Activity', padding: '12px' });
292
+ activity.h3('Customer updated', { font_size: '1rem' });
293
+ activity.p('^.name', { mask: 'Latest name: {}' });
294
+ activity.p('Each card selects its own panel; only the business values are shared.', {
295
+ font_size: '.9rem',
296
+ });
297
+ }
298
+ }
299
+
300
+ export class Logic {}
@@ -0,0 +1,37 @@
1
+ # Choose the presentation by looking at it
2
+
3
+ The first eight cards compare two presentations on all four sides. **Shaped tabs** give each label a visible outline; **Underline** leaves a lighter row or list of text, with a line marking the selected panel. You can click every card: these are real containers, not pictures.
4
+
5
+ **Top** and **Bottom** place the labels in a horizontal row. **Left** and **Right** place them in a vertical list. The text itself stays horizontal: a vertical tab list does not require rotated words. Each card prints the mode, position and alignment options that create it.
6
+
7
+ Type a name in one **Profile** panel. The other cards update because their editors share the same Data path. Open **Messages**, edit its text, and return to Profile. Changing panels keeps the controls and their values. Each card has independent selection, so you can compare different panels at the same time.
8
+
9
+ The next six cards separate **position** from **alignment**. Position chooses a side; `tabAlign` chooses where the labels sit along that side. With a horizontal bar, `start`, `center`, `end` mean left, middle, right. With a vertical bar they mean top, middle, bottom. The default is `start`. If the labels no longer fit, the strip keeps its beginning reachable and lets you scroll through the entries.
10
+
11
+ The controls below the comparison let you change the presentation of one larger editor, adjust icons and unread badges, and explore closing. The unread-message slider also updates the badges in the cards above.
12
+
13
+ ## Keep several tasks within reach
14
+
15
+ Tabs let someone move between parts of a task without opening a new page. Here you can edit a customer, write notes, and visit a nested layout. Only one panel is visible at a time, but changing tabs keeps the others ready for your return.
16
+
17
+ There is an important distinction to learn: **switching away from a panel is not the same as closing it**.
18
+
19
+ ## Try it
20
+
21
+ Write a note in **Notes**, select **Customer**, then return. Your note is still there. In Customer, try a one-letter name: it is too short, so it stays as an unfinished edit while the accepted name below remains unchanged. Switch away and return again. Even that unfinished edit survives.
22
+
23
+ Now use **Presentation** and **Tab position**. Underline gives you a lighter row of labels; Tabs gives you shaped tabs. Move the row to the bottom or to either side. These are different presentations of the same panels.
24
+
25
+ While Notes is hidden, move **Unread messages**. The number beside Notes changes without opening the panel. That little number is a **badge**: a way to show that something deserves attention elsewhere. Try its color and icon controls as well.
26
+
27
+ ## Closing deserves a separate experiment
28
+
29
+ Turn on **Prevent closing Notes** and try its close button. The panel stays. Turn the lock off and try again: this time the tab and its panel are removed. The **Saved notes** line still contains the note, because removing an editor does not automatically erase the Data it was editing.
30
+
31
+ After closing Notes, it is no longer a selectable tab. Reload this example when you want to repeat that part of the lesson.
32
+
33
+ ## How selection connects to your page
34
+
35
+ Each panel has a pageName, a stable name such as customer or notes. The container's selectedPage can bind to Data, which is why **Selected page** and clicking a tab can control the same selection. The numeric selected value is its position in the list, starting at zero.
36
+
37
+ The event report lets you see which panel is being hidden, shown or closed. It is useful when another part of your application needs to react, but you do not need a controller just to switch ordinary tabs: the container already does that.
@@ -0,0 +1,142 @@
1
+ import { Page as BasePage } from '@gramlot/gramlot/page';
2
+ import catalog from '../../catalog.json' with { type: 'json' };
3
+
4
+ export class Page extends BasePage {
5
+ // The static exporter supplies its file routes; the hosted gallery uses route keys.
6
+ static galleryUrls = Object.fromEntries(
7
+ catalog.families.flatMap((family) => family.examples).map((demo) => [demo.key, demo.key]),
8
+ );
9
+ static title = 'Demo carousel and live preview';
10
+ static devices = ['desktop', 'tablet', 'phone'];
11
+ static css = ['/themes/gramlot-base/theme.css'];
12
+
13
+ main(root) {
14
+ const demos = catalog.families
15
+ .flatMap((family) => family.examples)
16
+ .filter((demo) => demo.key !== 'l08');
17
+ const pane = root.main({ class: 'example-page stack', datapath: 'stackDemo' });
18
+ for (const [key, value] of [
19
+ ['panel', 0],
20
+ ['page', demos[0]['key']],
21
+ ['caption', demos[0]['title']],
22
+ ['active', demos[0]['key']],
23
+ ['activeCaption', demos[0]['title']],
24
+ ['event', ''],
25
+ ]) {
26
+ pane.dataSetter({ destination_path: `.${key}`, value: value });
27
+ }
28
+ for (const demo of demos) {
29
+ pane.dataSetter({
30
+ destination_path: `.urls.${demo.key}`,
31
+ value: this.constructor.galleryUrls[demo.key],
32
+ });
33
+ pane.dataSetter({ destination_path: `.titles.${demo['key']}`, value: demo['title'] });
34
+ pane.dataSetter({ destination_path: `.opened.${demo['key']}`, value: null });
35
+ }
36
+ pane.dataController({
37
+ script: "if (selected) { this.SET('.caption', this.GET('.titles.' + page)); } this.SET('.event', JSON.stringify(_subscription_kwargs));",
38
+ subscribe_demoStack_selected: true,
39
+ });
40
+ pane.dataController({
41
+ script: "this.SET('.opened.' + demo, this.GET('.urls.' + demo)); this.SET('.active', demo); this.SET('.activeCaption', this.GET('.titles.' + demo));",
42
+ demo: '^.page',
43
+ _delay: 350,
44
+ _onStart: true,
45
+ });
46
+ pane.h1('Browse, pause and try the demo');
47
+ pane.p(
48
+ `Slide through ${demos.length} thumbnails in the drawer. Pause to open the full-size demo in the center. Close the drawer for more space.`,
49
+ );
50
+ const border = pane.borderContainer({
51
+ id: 'carousel-layout',
52
+ height: 'min(76vh, 680px)',
53
+ min_height: '420px',
54
+ rounded: 6,
55
+ border: '1px solid var(--gramlot-border-control, #aaa)',
56
+ });
57
+ border
58
+ .contentPane({
59
+ region: 'top',
60
+ height: '36px',
61
+ padding: '8px 12px',
62
+ border_bottom: '1px solid var(--gramlot-border, #ccc)',
63
+ background: 'var(--gramlot-surface-subtle, #eef1f5)',
64
+ })
65
+ .strong('^.activeCaption', { id: 'stack-active-title', mask: 'Live demo: {}' });
66
+ const drawer = border.contentPane({
67
+ id: 'carousel-drawer',
68
+ region: 'left',
69
+ width: '260px',
70
+ splitter: true,
71
+ minSize: 180,
72
+ maxSize: 400,
73
+ drawer: true,
74
+ drawer_label: 'Demos',
75
+ drawer_icon: '▦',
76
+ padding: '12px',
77
+ background: 'var(--gramlot-surface-subtle, #eef1f5)',
78
+ border_right: '1px solid var(--gramlot-border, #ccc)',
79
+ overflow: 'auto',
80
+ });
81
+ drawer.horizontalSlider({
82
+ id: 'stack-panel',
83
+ value: '^.panel',
84
+ lbl: `Demo index (0–${demos.length - 1})`,
85
+ min: 0,
86
+ max: demos.length - 1,
87
+ step: 1,
88
+ live: true,
89
+ width: '100%',
90
+ min_width: 0,
91
+ });
92
+ drawer.h2('^.caption', { id: 'stack-caption', margin_top: '12px' });
93
+ drawer.p('^.page', { id: 'stack-page', mask: 'Demo: {}' });
94
+ const thumbnails = drawer.stackContainer({
95
+ id: 'stack-demo',
96
+ node_id: 'demoStack',
97
+ selectedPage: '^.page',
98
+ selected: '^.panel',
99
+ height: '300px',
100
+ rounded: 4,
101
+ border: '1px solid var(--gramlot-border-control, #aaa)',
102
+ });
103
+ const center = border.contentPane({ region: 'center', background: 'var(--gramlot-surface, #fff)' });
104
+ const full = center.stackContainer({ id: 'stack-full', selectedPage: '^.active', height: '100%' });
105
+ for (const demo of demos) {
106
+ const panel = thumbnails.contentPane({
107
+ pageName: demo['key'],
108
+ position: 'relative',
109
+ overflow: 'hidden',
110
+ });
111
+ panel.iframe({
112
+ id: `mini-${demo['key']}`,
113
+ src: this.constructor.galleryUrls[demo.key],
114
+ title: demo['title'],
115
+ loading: 'lazy',
116
+ position: 'absolute',
117
+ top: 0,
118
+ left: 0,
119
+ width: '200%',
120
+ height: '200%',
121
+ border: 0,
122
+ transform: 'scale(0.5)',
123
+ max_width: 'none',
124
+ style: 'transform-origin:top left',
125
+ });
126
+ full.contentPane({ pageName: demo['key'] }).iframe({
127
+ id: `live-${demo['key']}`,
128
+ src: `^.opened.${demo['key']}`,
129
+ title: `Live: ${demo['title']}`,
130
+ width: '100%',
131
+ height: '100%',
132
+ border: 0,
133
+ display: 'block',
134
+ });
135
+ }
136
+ pane.p(
137
+ 'The center loads a demo only after a pause and keeps visited pages mounted. Thumbnail and full-size views have independent Data. This overview excludes itself.',
138
+ );
139
+ }
140
+ }
141
+
142
+ export class Logic {}
@@ -0,0 +1,27 @@
1
+ # Browse a gallery without losing your place
2
+
3
+ A **stackContainer** holds several panels but shows just one at a time. Unlike a tabContainer, it does not add a row of tabs. You choose how people move between panels. This example uses a slider, so you can quickly browse many demos.
4
+
5
+ There are two viewing areas. The left drawer is a small preview browser. The center is a full-size demo you can actually use.
6
+
7
+ ## Try it
8
+
9
+ Move the slider slowly. Watch the title and miniature change as you move. Stop on a demo that interests you. After a short pause, the center opens that demo at its normal size.
10
+
11
+ Now sweep quickly past several entries. The center waits until you settle instead of opening every demo you pass. This small delay makes browsing calmer and avoids loading pages you never intended to try.
12
+
13
+ Choose a demo with a text field. Enter something in the full-size view, visit another demo, then return. The first page is still there with your edit. Choosing a different stack panel hid it; it did not reload it.
14
+
15
+ Close the carousel drawer when you want more room for the center. Open it again when you want to browse further.
16
+
17
+ ## Why the miniature and the large view differ
18
+
19
+ Each is a separate embedded page, called an **iframe**. They show the same example, but they do not share its Data. Typing in the large view will not type into the miniature. Each view keeps its own edits when you leave and return.
20
+
21
+ Full-size demos are loaded on their first settled visit. A demo you skip quickly is not loaded in the center. Miniatures use the browser's lazy loading, so the browser decides when to fetch them. This carousel excludes itself to avoid opening another carousel inside every preview.
22
+
23
+ ## How the pause works
24
+
25
+ The slider changes the preview selection immediately. A dataController waits 350 milliseconds after the latest selection change before updating the center. This is called **debouncing**: wait until the user pauses, then act on their final choice.
26
+
27
+ In **Code**, look for the two stackContainers and the controller with _delay=350. Read them as three jobs: browse now, wait briefly, then show the chosen page. You can use the same idea for a search field that waits for someone to stop typing before searching.
@@ -0,0 +1,15 @@
1
+ # Learn to arrange a workspace
2
+
3
+ A useful screen needs room for the task in front of you, while keeping other tools within reach. In these lessons you will build that idea one step at a time: divide the space, put one layout inside another, tuck panels away, and move between tasks without losing your edits.
4
+
5
+ You can start by playing with a page. Its **Explanation** tab tells you what to try and what the result means. Open **Code** when you want to connect that behavior to Python or JavaScript. There is no need to understand every declaration before your first experiment.
6
+
7
+ ## A route through the lessons
8
+
9
+ Start with **Border regions and splitters**. Learn what a region is and how the center makes room when you resize a side panel. Continue with **Nested borders** to give part of a page its own layout, then **Rounded borders** to combine that layout with calendars and other popups.
10
+
11
+ **Drawers** shows how to put tools away without losing their values. Its **Guide** handle opens a step-by-step demonstration, with optional voice. Once you are comfortable, **Nested regions playground** gives you a larger workspace to explore.
12
+
13
+ **Border events** explains how your page can hear about a change and react. **Tabs** introduces switching between panels and the different meaning of closing one. Finally, **Demo carousel** uses a slider to browse a stack of pages, with a short pause before opening the chosen page at full size.
14
+
15
+ The same lessons run in the Python, JavaScript and offline galleries. Code shows the language of the gallery you are using.