@gramlot/gramlot-examples 0.3.3 → 0.3.5

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 +2467 -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 +77 -47
  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 +6 -6
  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,151 @@
1
+ const STEPS = [
2
+ {
3
+ title: 'Meet your workspace',
4
+ text: 'Imagine you are editing a customer. Search is above the form, Filters is on the left, and Output is below. Details is tucked away on the right. Each small handle lets you bring a tool into view or put it away. We will try that together.',
5
+ changes: {
6
+ drawerSearch: true,
7
+ drawerFilters: true,
8
+ drawerDetails: 'close',
9
+ drawerOutput: true,
10
+ },
11
+ seconds: 14,
12
+ },
13
+ {
14
+ title: 'One choice, shown in two places',
15
+ text: 'Look at Filters: Express is now selected. The delivery line in the center also says express. These are two views of the same stored value. Gramlot calls these shared values Data. You do not need to copy the choice from the radio group into the summary.',
16
+ changes: {
17
+ delivery: 'express',
18
+ },
19
+ seconds: 14,
20
+ },
21
+ {
22
+ title: 'Put the filters away',
23
+ text: 'We have closed Filters to give the form more room. Notice that the delivery line still says express. Hiding the panel has not cancelled your choice. A drawer is useful when you need a tool nearby, but do not want it taking up space all the time.',
24
+ changes: {
25
+ drawerFilters: 'close',
26
+ },
27
+ seconds: 14,
28
+ },
29
+ {
30
+ title: 'Bring your choice back',
31
+ text: 'Filters is open again, and Express is still selected. We have also made the panel a little wider. Nothing had to be filled in again. You can pause here and try dragging its thin separator or its handle to choose a comfortable width yourself.',
32
+ changes: {
33
+ drawerFilters: true,
34
+ filtersWidth: '220px',
35
+ },
36
+ seconds: 14,
37
+ },
38
+ {
39
+ title: 'Edit once, see it wherever you need it',
40
+ text: 'Details has opened on the right, and the customer is now Grace Hopper. Look at the name in the center too. Both places are connected to the same customer value. This connection is called a binding: when the value changes, the places reading it update.',
41
+ changes: {
42
+ drawerDetails: true,
43
+ customer: 'Grace Hopper',
44
+ },
45
+ seconds: 14,
46
+ },
47
+ {
48
+ title: 'The customer stays, even when the editor is hidden',
49
+ text: 'Details is closed, but the center still shows Grace Hopper. We have hidden the place where you edit the customer, not removed the customer. You can open Details again whenever you need it; its field will still have that name.',
50
+ changes: {
51
+ drawerDetails: 'close',
52
+ },
53
+ seconds: 14,
54
+ },
55
+ {
56
+ title: 'Change something while its summary is hidden',
57
+ text: 'We have closed Output and changed the note in the center. Read the field: it now says Written while Output was closed. Can a panel that was hidden keep up with that change? In the next step we will open it and see.',
58
+ changes: {
59
+ drawerOutput: 'close',
60
+ note: 'Written while Output was closed',
61
+ },
62
+ seconds: 14,
63
+ },
64
+ {
65
+ title: 'Your work is still here',
66
+ text: 'Output is back, and it shows the new note. That is the main idea: drawers let you rearrange what you see while your work stays in place. Try it yourself now. Change the note, leave its field, close Output, and open it again. There is no extra save or copy step in this example.',
67
+ changes: {
68
+ drawerOutput: true,
69
+ },
70
+ seconds: 14,
71
+ },
72
+ ];
73
+
74
+ export function guidedDemo(parent) {
75
+ for (const [name, value] of Object.entries({
76
+ steps: STEPS,
77
+ status: 'ready',
78
+ position: -1,
79
+ executed: -1,
80
+ count: STEPS.length,
81
+ progress: `Ready · ${STEPS.length} steps`,
82
+ title: 'Let’s explore drawers together',
83
+ text: 'Press Start to watch the workspace change, or Next to take one step at a time. You can pause and try the handles yourself whenever you like.',
84
+ voice: false,
85
+ rate: '1',
86
+ voiceAvailable: false,
87
+ voiceMessage: '',
88
+ available: false,
89
+ })) {
90
+ parent.dataSetter({ destination_path: `galleryGuide.${name}`, value: value });
91
+ }
92
+ const panel = parent.contentPane({
93
+ region: 'bottom',
94
+ height: '190px',
95
+ minSize: 140,
96
+ maxSize: 300,
97
+ splitter: true,
98
+ drawer: true,
99
+ drawer_label: 'Guide',
100
+ drawer_icon: '▶',
101
+ node_id: 'galleryGuidePanel',
102
+ id: 'gallery-guide',
103
+ class: 'gallery-guide',
104
+ padding: '12px 16px',
105
+ border_top: '1px solid var(--gramlot-border)',
106
+ background: 'var(--gramlot-surface-subtle)',
107
+ });
108
+ const toolbar = panel.div({ class: 'gallery-guide-toolbar' });
109
+ for (const [name, caption, disabled] of [
110
+ ['Play', '▶ Start / Resume', "^galleryGuide.status:= {} === 'running' || !{available}"],
111
+ ['Pause', 'Ⅱ Pause', "^galleryGuide.status:= {} !== 'running'"],
112
+ ['Stop', '■ Stop', "^galleryGuide.status:= !['running','paused'].includes({})"],
113
+ ['Previous', '← Back', '^galleryGuide.position:= {} <= 0'],
114
+ ['Next', 'Next →', '^galleryGuide.position:= {} >= {count} - 1 || !{available}'],
115
+ ]) {
116
+ toolbar.button(caption, {
117
+ node_id: `galleryGuide${name}`,
118
+ id: `gallery-guide-${name.toLowerCase()}`,
119
+ action: "this.publish('command')",
120
+ disabled: disabled,
121
+ available: '^galleryGuide.available',
122
+ count: '^galleryGuide.count',
123
+ });
124
+ }
125
+ toolbar.checkbox({
126
+ value: '^galleryGuide.voice',
127
+ label: 'Voice',
128
+ id: 'gallery-guide-voice',
129
+ disabled: '^galleryGuide.voiceAvailable:=!{}',
130
+ });
131
+ const speed = toolbar.select({
132
+ value: '^galleryGuide.rate',
133
+ aria_label: 'Narration speed',
134
+ id: 'gallery-guide-rate',
135
+ width: '5em',
136
+ });
137
+ for (const [value, caption] of [
138
+ ['0.8', '0.8×'],
139
+ ['1', '1×'],
140
+ ['1.2', '1.2×'],
141
+ ]) {
142
+ speed.option(caption, { value: value });
143
+ }
144
+ toolbar.span('^galleryGuide.progress', { id: 'gallery-guide-progress', role: 'status' });
145
+ panel.strong('^galleryGuide.title', { id: 'gallery-guide-title' });
146
+ panel.p('^galleryGuide.text', { id: 'gallery-guide-text', aria_live: 'polite' });
147
+ panel.p('^galleryGuide.voiceMessage', { id: 'gallery-guide-voice-message', role: 'status' });
148
+ panel.small(
149
+ 'The tour changes delivery, customer and note. Stop keeps the current values. Back reviews the explanation only.',
150
+ );
151
+ }
@@ -14,14 +14,14 @@ export class Page extends GalleryPage {
14
14
  return families.map(({key, title, path, examples}) => {
15
15
  const read = name => readFileSync(join(path, name), 'utf8');
16
16
  return {
17
- key, title, readme: read('README.md'),
17
+ key, title, get readme() { return read('README.md'); },
18
18
  examples: examples.map(example => ({
19
- key: example.key, title: example.title, folder: example.folder, frameUrl: example.key,
20
- readme: read(`${example.folder}.md`),
21
- source: read(`${example.folder}.js`),
19
+ key: example.key, title: example.title, description: example.description, folder: example.folder, frameUrl: example.key,
20
+ get readme() { return read(`${example.folder}.md`); },
21
+ get source() { return read(`${example.folder}.js`); },
22
22
  // The page module shows its own Logic; only a companion NN_name_aux.js is shown apart.
23
- logic: routes[example.key].logic === routes[example.key].page
24
- ? null : readFileSync(routes[example.key].logic, 'utf8'),
23
+ get logic() { return routes[example.key].logic === routes[example.key].page
24
+ ? null : readFileSync(routes[example.key].logic, 'utf8'); },
25
25
  })),
26
26
  };
27
27
  });
@@ -52,6 +52,7 @@ export function buildGallery({catalogs = []} = {}) {
52
52
  const resolve = specifier => fileURLToPath(import.meta.resolve(specifier));
53
53
  const routes = {index: {page: join(gallery, 'page.js'), stylesheet: null, logic: null}};
54
54
  const filesByUrl = [
55
+ ['/catalog.json', join(PACKAGE, 'catalog.json')],
55
56
  ['/themes/gramlot-base/theme.css', resolve('@gramlot/gramlot/themes/gramlot-base/theme.css')],
56
57
  ['/assets/branding/gramlot-logo-dark.svg', resolve('@gramlot/gramlot/assets/branding/gramlot-logo-dark.svg')],
57
58
  ['/assets/branding/app-icon.png', resolve('@gramlot/gramlot/assets/branding/app-icon.png')],
@@ -0,0 +1,80 @@
1
+ import { Page as BasePage } from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Border regions and splitters';
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('Space for content');
11
+ pane.p(
12
+ 'Drag a separator or use its arrow keys. Resize Filters and Details while editing the customer.',
13
+ );
14
+ const bc = pane.borderContainer({
15
+ id: 'border-demo',
16
+ height: '440px',
17
+ max_width: '64rem',
18
+ border: '1px solid var(--gramlot-border-control, #aaa)',
19
+ });
20
+ bc.contentPane({
21
+ region: 'top',
22
+ border_bottom: '1px solid var(--gramlot-border-control, #aaa)',
23
+ height: '40px',
24
+ background: 'var(--gramlot-surface-subtle, #eef1f5)',
25
+ padding: '8px',
26
+ }).strong('Customer workspace');
27
+ const left = bc.contentPane({
28
+ region: 'left',
29
+ border_right: '1px solid var(--gramlot-border-control, #aaa)',
30
+ width: '160px',
31
+ splitter: true,
32
+ minSize: 100,
33
+ maxSize: 300,
34
+ padding: '12px',
35
+ background: 'var(--gramlot-surface-subtle, #eef1f5)',
36
+ });
37
+ left.textbox({ value: '^filter', lbl: 'Filter', width: '100%' });
38
+ const right = bc.contentPane({
39
+ region: 'right',
40
+ border_left: '1px solid var(--gramlot-border-control, #aaa)',
41
+ width: '220px',
42
+ splitter: true,
43
+ minSize: 120,
44
+ maxSize: 400,
45
+ padding: '12px',
46
+ background: 'var(--gramlot-surface-subtle, #eef1f5)',
47
+ });
48
+ right.h3('Customer summary');
49
+ right.p('^customer.name', { mask: 'Customer: {}' });
50
+ right.checkbox({ value: '^customer.active', label: 'Active customer', mode: 'switch' });
51
+ right.p('^customer.notes');
52
+ const center = bc.contentPane({
53
+ region: 'center',
54
+ background: 'var(--gramlot-surface, #fff)',
55
+ padding: '16px 48px',
56
+ });
57
+ const fields = center.formlet({ columns: 1, max_width: '22rem', gap: '12px' });
58
+ fields.textbox({
59
+ id: 'customer-name',
60
+ value: '^customer.name',
61
+ lbl: 'Name (2–8 characters)',
62
+ validate_len: '2:8',
63
+ width: '100%',
64
+ });
65
+ fields.textBoxArea({ value: '^customer.notes', lbl: 'Notes', rows: 3, width: '100%' });
66
+ center.p('Accepted name:');
67
+ center.p('^customer.name', { id: 'accepted-name' });
68
+ center.p('Try an invalid one-character name, then resize the panes. The draft stays in the field.');
69
+ for (const [path, value] of [
70
+ ['customer.active', true],
71
+ ['filter', ''],
72
+ ['customer.name', 'Ada'],
73
+ ['customer.notes', 'Same fields, same Data'],
74
+ ]) {
75
+ pane.dataSetter({ destination_path: path, value: value });
76
+ }
77
+ }
78
+ }
79
+
80
+ export class Logic {}
@@ -0,0 +1,23 @@
1
+ # Give each part of your page its own space
2
+
3
+ Imagine a customer screen: filters on the left, a form in the middle, and a summary on the right. You want people to give the form more room without having to close their work. This is what a **borderContainer** helps you build.
4
+
5
+ A *region* is simply one of its areas: top, bottom, left, right or center. The center takes the space that remains. You do not have to calculate its width yourself.
6
+
7
+ ## Try it
8
+
9
+ Start by dragging the thin line beside the filters. This line is a **splitter**. As the filters get wider, the form gets narrower. Release it where you are comfortable: you have changed the layout, not the form.
10
+
11
+ Now enter a customer name of at least two characters and leave the field. Look at the summary. It shows your new name because both places use the same stored value. In Gramlot, those shared values are called **Data**.
12
+
13
+ For a useful contrast, enter just one letter. The field reports that the name is too short, while the summary keeps the last accepted name. Resize the pane again: even the unfinished, invalid edit stays in the field. Moving a separator does not rebuild the editor.
14
+
15
+ ## How you would build this
16
+
17
+ First create a borderContainer. Add a contentPane for each region you need. For a left-hand pane, set region to left, choose a starting width and enable splitter. Add another pane with region set to center for the main form. That is the layout: no resize formula for the center is needed.
18
+
19
+ Minimum and maximum sizes keep a pane useful when someone drags too far. A keyboard user can focus a splitter and use its arrow keys.
20
+
21
+ ## Take one idea with you
22
+
23
+ A container organizes space; the fields continue to own their editing state. Use **Code** when you want to find the region declarations, then return to the example and try a different width. Next, **Nested borders** shows how one area can contain a layout of its own.
@@ -0,0 +1,99 @@
1
+ import { Page as BasePage } from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Nested borders and layout designs';
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('One layout inside another');
11
+ const controls = pane.formlet({ max_width: '24rem', columns: 1 });
12
+ controls.filteringSelect({
13
+ id: 'design',
14
+ value: '^design',
15
+ lbl: 'Outer design',
16
+ values: 'headline:Headline,sidebar:Sidebar',
17
+ width: '10rem',
18
+ });
19
+ const bc = pane.borderContainer({
20
+ id: 'nested-demo',
21
+ height: '440px',
22
+ design: '^design',
23
+ max_width: '64rem',
24
+ border: '1px solid var(--gramlot-border-control, #aaa)',
25
+ });
26
+ bc.contentPane({
27
+ region: 'top',
28
+ border_bottom: '1px solid var(--gramlot-border-control, #aaa)',
29
+ height: '48px',
30
+ splitter: true,
31
+ minSize: 30,
32
+ maxSize: 100,
33
+ padding: '10px',
34
+ background: 'var(--gramlot-surface-subtle, #eef1f5)',
35
+ }).p('Outer header');
36
+ const left = bc.contentPane({
37
+ region: 'left',
38
+ border_right: '1px solid var(--gramlot-border-control, #aaa)',
39
+ width: '130px',
40
+ drawer: true,
41
+ splitter: false,
42
+ drawer_title: 'Side',
43
+ padding: '10px',
44
+ background: 'var(--gramlot-surface-subtle, #eef1f5)',
45
+ });
46
+ left.h3('Workspace');
47
+ left.radioButtonText({ value: '^section', values: 'profile:Profile,history:History', cols: 1 });
48
+ left.p('A fixed-width drawer: open and close without dragging.');
49
+ const nested = bc.borderContainer({ region: 'center' });
50
+ nested
51
+ .contentPane({
52
+ region: 'top',
53
+ border_bottom: '1px solid var(--gramlot-border-control, #aaa)',
54
+ height: '40px',
55
+ padding: '10px',
56
+ background: 'var(--gramlot-surface-subtle, #eef1f5)',
57
+ })
58
+ .p('Inner header');
59
+ const editor = nested.contentPane({
60
+ region: 'center',
61
+ padding: '12px 40px',
62
+ background: 'var(--gramlot-surface, #fff)',
63
+ });
64
+ editor.textbox({ value: '^name', lbl: 'Customer', width: '100%', max_width: '24rem' });
65
+ editor.checkbox({ value: '^active', label: 'Active', mode: 'switch' });
66
+ editor.textBoxArea({
67
+ id: 'nested-note',
68
+ value: '^note',
69
+ lbl: 'Retained draft',
70
+ rows: 3,
71
+ width: '100%',
72
+ max_width: '24rem',
73
+ });
74
+ nested
75
+ .contentPane({
76
+ region: 'bottom',
77
+ border_top: '1px solid var(--gramlot-border-control, #aaa)',
78
+ height: '80px',
79
+ splitter: true,
80
+ minSize: 36,
81
+ maxSize: 180,
82
+ drawer: true,
83
+ drawer_title: 'Output',
84
+ padding: '10px',
85
+ background: 'var(--gramlot-surface-subtle, #eef1f5)',
86
+ })
87
+ .p('^note');
88
+ pane.dataSetter({ destination_path: 'name', value: 'Ada Lovelace' });
89
+ pane.dataSetter({ destination_path: 'active', value: true });
90
+ pane.dataSetter({ destination_path: 'section', value: 'profile' });
91
+ pane.dataSetter({ destination_path: 'design', value: 'headline' });
92
+ pane.dataSetter({
93
+ destination_path: 'note',
94
+ value: 'Resize the inner bottom region, then change the outer design.',
95
+ });
96
+ }
97
+ }
98
+
99
+ export class Logic {}
@@ -0,0 +1,23 @@
1
+ # Put a workspace inside a workspace
2
+
3
+ A whole application and one of its editors rarely need the same layout. The application may need navigation and a header; the editor may need its own tools and output. You can give each its own **borderContainer**, placing the editor's container in the center of the application's container.
4
+
5
+ Think of it as rooms inside a house: changing the arrangement of one room does not require rebuilding the house.
6
+
7
+ ## Try it
8
+
9
+ Find the customer and note fields and enter something you will recognize. Now change **Outer design** from Headline to Sidebar. Look at the corners: Headline gives the header the full width, while Sidebar lets the side region run along the height. Your text remains in the editor.
10
+
11
+ Next, drag the separator above the inner output area. Only that inner layout changes. The outer header and navigation still belong to the outer container.
12
+
13
+ Close **Side** using its little handle, then open it again. This particular drawer has a fixed width: you can open and close it, but it is not a resizable splitter. A drawer and a splitter are two independent choices.
14
+
15
+ ## How the pieces fit
16
+
17
+ Each container arranges its direct children. A child can be an ordinary contentPane holding fields, or another borderContainer holding more regions. The inner container only needs to know about its own children.
18
+
19
+ The design selector writes a shared Data value. The outer container reads that value through a **binding**, a connection that updates an attribute when its Data changes. Changing the design rearranges the existing screen; it does not create a new customer form.
20
+
21
+ ## When this is useful
22
+
23
+ Use nesting when a section needs its own layout rules. Avoid adding another container just to add spacing: padding is enough for that. The next lesson combines nested layouts with rounded edges and popups.
@@ -0,0 +1,111 @@
1
+ import { Page as BasePage } from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Rounded borders and popups';
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('Rounded outside, continuous inside');
11
+ pane.p(
12
+ 'Drag all four separators and toggle their drawers. Open the calendar and choices inside the rounded inner panel.',
13
+ );
14
+ const controls = pane.formlet({ max_width: '24rem', columns: 1 });
15
+ controls.filteringSelect({
16
+ id: 'rounded-design',
17
+ value: '^design',
18
+ lbl: 'Outer design',
19
+ values: 'headline:Headline,sidebar:Sidebar',
20
+ width: '10rem',
21
+ });
22
+ controls.horizontalSlider({
23
+ id: 'rounded-slider',
24
+ value: '^rounded',
25
+ lbl: 'Corner radius (px)',
26
+ min: 0,
27
+ max: 48,
28
+ step: 1,
29
+ live: true,
30
+ width: '14rem',
31
+ });
32
+ const border = pane.borderContainer({
33
+ id: 'rounded-border',
34
+ height: '560px',
35
+ max_width: '64rem',
36
+ design: '^design',
37
+ rounded: '^rounded',
38
+ border: '2px solid var(--gramlot-border-control, #8192aa)',
39
+ background: 'var(--gramlot-surface, #fff)',
40
+ });
41
+ for (const [side, size, label] of [
42
+ ['top', '64px', 'Header'],
43
+ ['bottom', '64px', 'Footer'],
44
+ ['left', '140px', 'Filters'],
45
+ ['right', '140px', 'Details'],
46
+ ]) {
47
+ const dimensions = {
48
+ [`border_${{ top: 'bottom', bottom: 'top', left: 'right', right: 'left' }[side]}`]:
49
+ '1px solid var(--gramlot-border-control, #aaa)',
50
+ [['top', 'bottom'].includes(side) ? 'height' : 'width']: size,
51
+ };
52
+ const region = border.contentPane({
53
+ id: `rounded-${side}`,
54
+ region: side,
55
+ splitter: true,
56
+ drawer: true,
57
+ minSize: 40,
58
+ maxSize: 240,
59
+ drawer_title: label,
60
+ padding: '16px',
61
+ background: 'var(--gramlot-surface-subtle, #e5ebf3)',
62
+ ...dimensions,
63
+ });
64
+ region.strong(label);
65
+ }
66
+ const center = border.contentPane({ region: 'center', padding: '24px 32px' });
67
+ const inner = center.borderContainer({
68
+ id: 'rounded-inner',
69
+ height: '100%',
70
+ rounded: '^rounded',
71
+ background: 'var(--gramlot-surface, #fff)',
72
+ border: '2px solid var(--gramlot-border-control, #8192aa)',
73
+ });
74
+ inner
75
+ .contentPane({
76
+ region: 'top',
77
+ border_bottom: '1px solid var(--gramlot-border-control, #aaa)',
78
+ height: '40px',
79
+ padding: '8px 12px',
80
+ background: 'var(--gramlot-surface-subtle, #e5ebf3)',
81
+ })
82
+ .strong('Shared reactive radius');
83
+ const fields = inner.contentPane({ region: 'center', padding: '12px' });
84
+ const form = fields.formlet({ columns: 1, gap: '10px', max_width: '20rem' });
85
+ form.textbox({
86
+ id: 'rounded-name',
87
+ value: '^name',
88
+ lbl: 'Name (2–8 characters)',
89
+ validate_len: '2:8',
90
+ width: '100%',
91
+ });
92
+ form.dateTextBox({ id: 'rounded-date', value: '^date', lbl: 'Appointment', width: '100%' });
93
+ form.filteringSelect({
94
+ id: 'rounded-choice',
95
+ value: '^choice',
96
+ lbl: 'Delivery',
97
+ values: 'standard:Standard,express:Express,pickup:Pick up,scheduled:Scheduled,evening:Evening,weekend:Weekend',
98
+ width: '100%',
99
+ });
100
+ pane.p(
101
+ "The rounded outline clips pane backgrounds. Calendar, choices and validation messages use the browser's top layer.",
102
+ );
103
+ pane.dataSetter({ destination_path: 'design', value: 'headline' });
104
+ pane.dataSetter({ destination_path: 'rounded', value: 24 });
105
+ pane.dataSetter({ destination_path: 'name', value: 'Ada' });
106
+ pane.dataSetter({ destination_path: 'date', value: new Date(Date.UTC(2026, 9, 18)) });
107
+ pane.dataSetter({ destination_path: 'choice', value: 'standard' });
108
+ }
109
+ }
110
+
111
+ export class Logic {}
@@ -0,0 +1,25 @@
1
+ # Rounded panels that still let you work
2
+
3
+ Rounded corners can make a workspace feel softer, but they should never get in the way of a calendar, a choice list or a helpful error message. This example lets you try those things together.
4
+
5
+ The outer layout and the layout inside it share a corner-radius setting. You will see both respond to one control.
6
+
7
+ ## Try it
8
+
9
+ Move **Corner radius** slowly from zero toward 48. At zero the corners are square. As the value increases, watch the inner boundaries as well as the outside outline. The page is changing an existing layout, not loading another version of it.
10
+
11
+ Open the appointment calendar, then open the Delivery list. Try this with a generous corner radius and with a narrow editor. A popup needs to remain usable even when the field sits near a rounded edge.
12
+
13
+ Enter a one-letter name. The error message floats beside the invalid field rather than pushing the following fields down. You can keep reading the form without losing your place.
14
+
15
+ Finally, resize a region or close a drawer and reopen it. Your edits should still be there.
16
+
17
+ ## Why one slider changes both layouts
18
+
19
+ Both containers read the same Data value for their rounded attribute. This is a binding: instead of assigning a radius once, you tell the container where to read it. When the slider changes that value, both containers update.
20
+
21
+ The rounded background and the popup have different jobs. The background stays within the panel's shape. The calendar and choice popup can appear above it, so the rounded edge does not cut off the choices.
22
+
23
+ ## Your next experiment
24
+
25
+ Try mimi and joanna from Settings. The appearance changes, but the layout and the values you entered remain the same. Then visit **Drawers** to explore how a panel can get out of your way without throwing away your work.