@gramlot/gramlot-examples 0.3.5 → 0.3.7

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 CHANGED
@@ -12,7 +12,7 @@ npm install @gramlot/gramlot-examples
12
12
  [![PyPI](https://img.shields.io/pypi/v/gramlot-examples)](https://pypi.org/project/gramlot-examples/)
13
13
  [![npm](https://img.shields.io/npm/v/@gramlot/gramlot-examples)](https://www.npmjs.com/package/@gramlot/gramlot-examples)
14
14
 
15
- Current release: 0.3.5.
15
+ Current release: 0.3.7.
16
16
 
17
17
  [Documentation and learning sequence](https://gramlot-examples.readthedocs.io/en/latest/)
18
18
  include every lesson, its Python and JavaScript source, and the gallery guide.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gramlot/gramlot-examples",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Gramlot example pages, the gallery page and its catalogue.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -14,7 +14,7 @@
14
14
  "prepack": "node scripts/build-browser.mjs"
15
15
  },
16
16
  "dependencies": {
17
- "@gramlot/gramlot": ">=0.3.3"
17
+ "@gramlot/gramlot": ">=0.3.4"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@genrojs/bag": ">=0.11.0",
@@ -6879,11 +6879,101 @@ Please report this to https://github.com/markedjs/marked.`, e) {
6879
6879
  element.innerHTML = purifier.sanitize(markdown.parse(element.textContent), { USE_PROFILES: { html: true } });
6880
6880
  }
6881
6881
 
6882
+ // src/gramlot_examples/gallery/browser/gallery-content.js
6883
+ var pending = /* @__PURE__ */ new WeakMap();
6884
+ function mountGalleryContent(renderer, root) {
6885
+ const document2 = renderer.destination.ownerDocument;
6886
+ const byId = (id) => document2.getElementById(id);
6887
+ const node = (element) => renderer.elements.get(element).node;
6888
+ const entries2 = /* @__PURE__ */ new Map();
6889
+ let disposed = false;
6890
+ function format({ key, view }) {
6891
+ if (view === "description") {
6892
+ const readme = byId(`readme-${key}`);
6893
+ readme.textContent = node(readme).value;
6894
+ renderReadme(readme);
6895
+ return;
6896
+ }
6897
+ const code = byId(`code-${key}`);
6898
+ code.textContent = node(code).value;
6899
+ highlightCode(code);
6900
+ const logic = byId(`logic-${key}`);
6901
+ if (logic) {
6902
+ const source = logicSource(node(logic).value);
6903
+ node(byId(`logic-panel-${key}`)).setAttr({ hidden: source === null });
6904
+ logic.textContent = source ?? "";
6905
+ if (source !== null) highlightCode(logic);
6906
+ }
6907
+ }
6908
+ async function load(entry) {
6909
+ if (entry.loading) return;
6910
+ entry.loading = true;
6911
+ const targetId = entry.view === "code" ? `code-tabs-${entry.key}` : entry.element.id;
6912
+ const target = node(byId(targetId));
6913
+ const current = () => {
6914
+ const element = byId(targetId);
6915
+ return element && renderer.elements.get(element)?.node === target ? element : null;
6916
+ };
6917
+ let request = pending.get(target);
6918
+ if (!request) {
6919
+ request = renderer.binding.gramlot.src.remoteSource(target, "lesson", { key: entry.key, view: entry.view });
6920
+ pending.set(target, request);
6921
+ }
6922
+ try {
6923
+ const mounted = await request;
6924
+ if (disposed || !current() || !mounted) return;
6925
+ format(entry);
6926
+ observer.unobserve(entry.element);
6927
+ entries2.delete(entry.element);
6928
+ } catch {
6929
+ if (disposed || !current()) return;
6930
+ const status = current().querySelector('[role="status"]');
6931
+ node(status).setValue("Could not load this content. Close and reopen the panel to retry.");
6932
+ } finally {
6933
+ if (pending.get(target) === request) pending.delete(target);
6934
+ entry.loading = false;
6935
+ }
6936
+ }
6937
+ const observer = new document2.defaultView.IntersectionObserver((records) => {
6938
+ for (const record of records) {
6939
+ const entry = entries2.get(record.target);
6940
+ if (!entry) continue;
6941
+ const visible = record.isIntersecting && record.intersectionRect.width > 0 && record.intersectionRect.height > 0;
6942
+ if (visible && !entry.visible) void load(entry);
6943
+ entry.visible = visible;
6944
+ }
6945
+ });
6946
+ const dispose = () => {
6947
+ disposed = true;
6948
+ observer.disconnect();
6949
+ entries2.clear();
6950
+ };
6951
+ renderer.onDispose(root, dispose);
6952
+ return {
6953
+ observe(key) {
6954
+ for (const [view, id, loaded] of [
6955
+ ["code", `codepane-${key}`, `code-${key}`],
6956
+ ["description", `description-${key}`, `readme-${key}`]
6957
+ ]) {
6958
+ const element = byId(id);
6959
+ const entry = { key, view, element, visible: false, loading: false };
6960
+ if (byId(loaded)) format(entry);
6961
+ else if (!entries2.has(element)) {
6962
+ entries2.set(element, entry);
6963
+ observer.observe(element);
6964
+ }
6965
+ }
6966
+ },
6967
+ dispose
6968
+ };
6969
+ }
6970
+
6882
6971
  // src/gramlot_examples/gallery/browser/gallery.js
6883
6972
  function mountGallery(renderer, root, inspector) {
6884
6973
  const state = root.builder.data.getItem("galleryIndex");
6885
6974
  const settings = mountGallerySettings(renderer, root, state);
6886
6975
  let guide = null;
6976
+ const content = mountGalleryContent(renderer, root);
6887
6977
  const mobile = mountGalleryMobile(renderer, root, inspector, state, (visible) => {
6888
6978
  if (!visible) guide?.pause();
6889
6979
  });
@@ -6891,19 +6981,14 @@ Please report this to https://github.com/markedjs/marked.`, e) {
6891
6981
  inspector.update(entries2, active);
6892
6982
  mobile.update(entries2, active);
6893
6983
  guide?.update(entries2, active);
6894
- }, (entry, { byId, node, set, listen }) => {
6984
+ }, (entry, { byId }) => {
6895
6985
  const { key } = entry;
6896
6986
  if (key === "l04" && !guide) guide = mountGuidedDemo(renderer, root);
6897
- renderReadme(byId(`readme-${key}`));
6898
- if (entry.panel.classList.contains("index-category")) return;
6899
- highlightCode(byId(`code-${key}`));
6900
- const logic = byId(`logic-${key}`);
6901
- if (logic) {
6902
- const source = logicSource(node(logic).value);
6903
- set(byId(`logic-panel-${key}`), { hidden: source === null });
6904
- logic.textContent = source ?? "";
6905
- if (source !== null) highlightCode(logic);
6987
+ if (entry.panel.classList.contains("index-category")) {
6988
+ renderReadme(byId(`readme-${key}`));
6989
+ return;
6906
6990
  }
6991
+ content.observe(entry.key);
6907
6992
  }, {
6908
6993
  containerLayout: true,
6909
6994
  state,
@@ -6919,20 +7004,22 @@ Please report this to https://github.com/markedjs/marked.`, e) {
6919
7004
  hidden: true,
6920
7005
  class: category ? "index-category" : "gallery-panel"
6921
7006
  });
6922
- const code = category ? null : source("gallery-code-pages").tabContainer({
7007
+ const code = category ? null : source("gallery-code-pages").div({
6923
7008
  id: `codepane-${entry.key}`,
6924
- class: "gallery-codepane gallery-source-tabs",
7009
+ class: "gallery-codepane",
6925
7010
  hidden: true,
7011
+ height: "100%"
7012
+ });
7013
+ if (code) code.tabContainer({
7014
+ id: `code-tabs-${entry.key}`,
7015
+ class: "gallery-source-tabs",
6926
7016
  height: "100%",
6927
7017
  tabMode: "underline"
6928
- });
7018
+ }).contentPane({ title: "Code" }).p("Loading code\u2026", { role: "status" });
6929
7019
  try {
6930
7020
  const runtime = renderer.binding.gramlot;
6931
- const results = await Promise.all([
6932
- runtime.src.remoteSource(node(`panel-${entry.key}`), "lesson", { key: entry.key }),
6933
- ...code ? [runtime.src.remoteSource(node(`codepane-${entry.key}`), "lesson", { key: entry.key, view: "code" })] : []
6934
- ]);
6935
- if (results.some((mounted) => !mounted)) throw new Error("Lesson loading was cancelled");
7021
+ const mounted = await runtime.src.remoteSource(node(`panel-${entry.key}`), "lesson", { key: entry.key });
7022
+ if (!mounted) throw new Error("Lesson loading was cancelled");
6936
7023
  } catch (error) {
6937
7024
  for (const target of [pane, code]) if (target?.parentBag) target.parentBag.popNode(target.label);
6938
7025
  throw error;
@@ -6945,6 +7032,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
6945
7032
  });
6946
7033
  return { dispose() {
6947
7034
  guide?.dispose();
7035
+ content.dispose();
6948
7036
  settings.dispose();
6949
7037
  index.dispose();
6950
7038
  mobile.dispose();
@@ -7550,11 +7638,11 @@ Please report this to https://github.com/markedjs/marked.`, e) {
7550
7638
  this.error = error.message;
7551
7639
  }
7552
7640
  if (message.type === "result") {
7553
- const pending = this.#pending.get(message.id);
7554
- if (pending) {
7641
+ const pending2 = this.#pending.get(message.id);
7642
+ if (pending2) {
7555
7643
  this.#pending.delete(message.id);
7556
- if (message.ok && !this.error) pending.resolve(message);
7557
- else pending.reject(new Error(message.error || this.error));
7644
+ if (message.ok && !this.error) pending2.resolve(message);
7645
+ else pending2.reject(new Error(message.error || this.error));
7558
7646
  }
7559
7647
  }
7560
7648
  this.#notify();
@@ -7611,7 +7699,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
7611
7699
  this.#port.removeEventListener("message", this.#receive);
7612
7700
  this.#port.close();
7613
7701
  this.state = "disposed";
7614
- for (const pending of this.#pending.values()) pending.reject(new Error("Inspected page disconnected"));
7702
+ for (const pending2 of this.#pending.values()) pending2.reject(new Error("Inspected page disconnected"));
7615
7703
  this.#pending.clear();
7616
7704
  this.#notify();
7617
7705
  }
@@ -7690,9 +7778,9 @@ Please report this to https://github.com/markedjs/marked.`, e) {
7690
7778
  _subscribe() {
7691
7779
  if (this._store) this._store.subscribe(this._subId, { any: () => {
7692
7780
  if (this._pendingRender) return;
7693
- const pending = this._pendingRender = {};
7781
+ const pending2 = this._pendingRender = {};
7694
7782
  queueMicrotask(() => {
7695
- if (this._pendingRender === pending && this.isConnected) this._render();
7783
+ if (this._pendingRender === pending2 && this.isConnected) this._render();
7696
7784
  });
7697
7785
  } });
7698
7786
  }
@@ -253,7 +253,7 @@ var SIDES = ["top", "bottom", "left", "right"];
253
253
  var REGIONS = [...SIDES, "center"];
254
254
  var borderRegions = /* @__PURE__ */ new WeakMap();
255
255
  var horizontal = (side) => side === "top" || side === "bottom";
256
- function projectBorderChild(element, attrs, { adapt, invoke, publish, publishParent }) {
256
+ function projectBorderChild(element, attrs, { adapt, invoke, publish, publishParent, writeParameter }) {
257
257
  const rest = { ...attrs }, splitter = {}, drawer = {};
258
258
  for (const [name, value] of Object.entries(rest)) {
259
259
  for (const [prefix, group] of [["splitter_", splitter], ["drawer_", drawer]]) {
@@ -272,7 +272,15 @@ function projectBorderChild(element, attrs, { adapt, invoke, publish, publishPar
272
272
  if (![void 0, null, false, true, "close"].includes(rest.drawer)) throw new TypeError("drawer must be true, false or close");
273
273
  const probe = element.ownerDocument.createElement("div");
274
274
  probe.style.cssText = rest.style ?? "";
275
- const size = probe.style[horizontal(region) ? "height" : "width"];
275
+ const dimension = horizontal(region) ? "height" : "width";
276
+ const numeric = typeof rest[dimension] === "number";
277
+ if (numeric) {
278
+ if (!Number.isFinite(rest[dimension]) || rest[dimension] < 0) throw new TypeError("Border numeric dimensions must be finite nonnegative pixels");
279
+ probe.style[dimension] = `${rest[dimension]}px`;
280
+ }
281
+ const size = probe.style[dimension];
282
+ delete rest.width;
283
+ delete rest.height;
276
284
  probe.style.width = "100%";
277
285
  probe.style.height = "100%";
278
286
  rest.style = probe.style.cssText;
@@ -297,6 +305,9 @@ function projectBorderChild(element, attrs, { adapt, invoke, publish, publishPar
297
305
  label,
298
306
  icon,
299
307
  minimize,
308
+ writeParameter: (size2) => {
309
+ if (numeric) writeParameter(dimension, size2);
310
+ },
300
311
  notifications: {
301
312
  closableChange: (payload) => publish("closable_change", payload),
302
313
  splitterResize: (payload) => publishParent("splitter_resize", payload)
@@ -334,7 +345,7 @@ var BorderContainer = defineComponent({
334
345
  tag: "gramlot-border-container",
335
346
  base: Container,
336
347
  subTags: "*",
337
- childParameters: { region: { required: true, enum: REGIONS, unique: true }, drawer_onclick: { inline: ["evt", "show"] } },
348
+ childParameters: { region: { required: true, enum: REGIONS, unique: true }, width: {}, height: {}, drawer_onclick: { inline: ["evt", "show"] } },
338
349
  parameters: {
339
350
  design: { dtype: "T", default: "headline", binding: "read", enum: ["headline", "sidebar"] },
340
351
  persist: { dtype: "B", default: false, binding: "read" }
@@ -636,6 +647,7 @@ var BorderContainer = defineComponent({
636
647
  const size = Math.max(Math.min(c.min, available), Math.min(desired, c.max, available));
637
648
  state.size = `${size}px`;
638
649
  this.#render(side);
650
+ c.writeParameter(size);
639
651
  }
640
652
  #keyResize(side, evt) {
641
653
  const state = this.#regions.get(side);
@@ -102,6 +102,7 @@ export class GalleryPage extends IndexPage {
102
102
  lesson(root, {key, view = 'example'}) {
103
103
  const item = this.galleryItem(key);
104
104
  if (view === 'code' && !item.examples) this.codePanel(root, item);
105
+ else if (view === 'description' && !item.examples) root.div(item.readme, {id: `readme-${key}`});
105
106
  else if (view === 'example') {
106
107
  if (item.examples) this.groupPanel(root, item);
107
108
  else this.panel(root, item);
@@ -124,8 +125,8 @@ export class GalleryPage extends IndexPage {
124
125
  guidedDemo(layout);
125
126
  }
126
127
  stage.iframe({title: `${example.title} preview`, name: `example-${key}`, id: `frame-${key}`});
127
- preview.contentPane({title: 'Explanation', pageName: 'explanation', class: 'gallery-lesson', overflow: 'auto'})
128
- .div(example.readme, {id: `readme-${key}`});
128
+ preview.contentPane({title: 'Explanation', pageName: 'explanation', class: 'gallery-lesson', overflow: 'auto', id: `description-${key}`})
129
+ .p('Loading explanation…', {role: 'status'});
129
130
  }
130
131
  codePanel(parent, example) {
131
132
  const key = example.key;
@@ -82,7 +82,7 @@ export class Page extends BasePage {
82
82
  });
83
83
  const header = outer.contentPane({
84
84
  region: 'top',
85
- height: '^regions.outer.top:{}px',
85
+ height: '^regions.outer.top',
86
86
  minSize: 40,
87
87
  maxSize: 110,
88
88
  drawer_label: 'Header',
@@ -94,7 +94,7 @@ export class Page extends BasePage {
94
94
  header.span('^customer');
95
95
  const footer = outer.contentPane({
96
96
  region: 'bottom',
97
- height: '^regions.outer.bottom:{}px',
97
+ height: '^regions.outer.bottom',
98
98
  minSize: 35,
99
99
  maxSize: 130,
100
100
  drawer_label: 'Status',
@@ -107,7 +107,7 @@ export class Page extends BasePage {
107
107
  const nav = outer.borderContainer({
108
108
  id: 'lab-navigation',
109
109
  region: 'left',
110
- width: '^regions.outer.left:{}px',
110
+ width: '^regions.outer.left',
111
111
  minSize: 90,
112
112
  maxSize: 240,
113
113
  drawer_label: 'Navigation',
@@ -129,7 +129,7 @@ export class Page extends BasePage {
129
129
  const details = outer.borderContainer({
130
130
  id: 'lab-details',
131
131
  region: 'right',
132
- width: '^regions.outer.right:{}px',
132
+ width: '^regions.outer.right',
133
133
  minSize: 90,
134
134
  maxSize: 240,
135
135
  drawer_label: 'Details',
@@ -159,7 +159,7 @@ export class Page extends BasePage {
159
159
  });
160
160
  const tools = work.contentPane({
161
161
  region: 'left',
162
- width: '^regions.workspace.left:{}px',
162
+ width: '^regions.workspace.left',
163
163
  minSize: 70,
164
164
  maxSize: 200,
165
165
  drawer_label: 'Tools',
@@ -172,7 +172,7 @@ export class Page extends BasePage {
172
172
  tools.checkbox({ value: '^readonly', lbl: 'Editor', label: 'Read only' });
173
173
  const preview = work.contentPane({
174
174
  region: 'bottom',
175
- height: '^regions.workspace.bottom:{}px',
175
+ height: '^regions.workspace.bottom',
176
176
  minSize: 50,
177
177
  maxSize: 180,
178
178
  drawer_label: 'Preview',
@@ -213,7 +213,7 @@ export class Page extends BasePage {
213
213
  fields.numberTextBox({ id: 'lab-quantity', value: '^quantity', lbl: 'Quantity', width: '100%' });
214
214
  const notes = editor.contentPane({
215
215
  region: 'bottom',
216
- height: '^regions.editor.bottom:{}px',
216
+ height: '^regions.editor.bottom',
217
217
  minSize: 60,
218
218
  maxSize: 180,
219
219
  drawer_label: 'Notes',
@@ -8,6 +8,8 @@ If the page looks complicated at first, start with just one slider. You do not n
8
8
 
9
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
10
 
11
+ Next, drag the same separator. Its slider moves immediately: both controls update the same number. Open the Inspector on Data to follow `regions.outer.left` while moving either the Navigation width slider or its separator. Try a nested region too.
12
+
11
13
  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
14
 
13
15
  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.
@@ -16,7 +18,15 @@ Finally, try the design and corner-radius controls. These change presentation wh
16
18
 
17
19
  ## How a number becomes a size
18
20
 
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**.
21
+ The slider and the region both use a plain pointer to the same number. Numeric region widths and heights are pixels; the borderContainer converts them to CSS internally. Dragging the splitter writes back the constrained number through the binding, so the slider follows immediately.
22
+
23
+ ```python
24
+ sliders.horizontalSlider(value="^regions.outer.left", min=90, max=240, live=True)
25
+ nav = outer.borderContainer(region="left", width="^regions.outer.left",
26
+ splitter=True, minSize=90, maxSize=240)
27
+ ```
28
+
29
+ There is no formatting suffix or synchronization controller. `regions` is just the Data branch chosen by this example; it is not a special container parameter. You can find all seven shared size bindings in **Code**.
20
30
 
21
31
  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
32