zauberflote 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/ui.js +136 -9
  3. package/.env +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zauberflote",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Shared UI components for the working examples book.",
5
5
  "type": "module",
6
6
  "main": "src/ui.js",
package/src/ui.js CHANGED
@@ -82,7 +82,7 @@ const ui = (() => {
82
82
  this.app = app;
83
83
  this.parent = parent || null;
84
84
  this.title = title || 'Section';
85
- this.id = slugify(this.title);
85
+ this.sectionId = slugify(this.title);
86
86
  this.readConfig = null;
87
87
  this.listTemplate = '';
88
88
  this.actions = [];
@@ -101,13 +101,26 @@ const ui = (() => {
101
101
  this.textConfig = null;
102
102
  this.markdownConfig = null;
103
103
  this.customRenderer = null;
104
+ this.renderHook = null;
104
105
  this.autoConfig = null;
106
+ this.mockConfig = null;
105
107
  this.suppressOutput = false;
106
108
  this.noAutoRefresh = false;
107
109
  this.layoutConfig = null;
110
+ this.templateMode = null;
108
111
  }
109
112
  id(value) {
110
- this.id = value;
113
+ this.sectionId = value;
114
+ return this;
115
+ }
116
+ mock(generator) {
117
+ // Signature: Smart Mocking System - show placeholder data when backend is offline.
118
+ this.mockConfig = generator;
119
+ return this;
120
+ }
121
+ onRender(callback) {
122
+ // Signature: Lifecycle hook for custom library initialization (Charts, Maps).
123
+ this.renderHook = callback;
111
124
  return this;
112
125
  }
113
126
  read(path) {
@@ -127,6 +140,11 @@ const ui = (() => {
127
140
  this.listTemplate = template || '';
128
141
  return this;
129
142
  }
143
+ template(name) {
144
+ // Signature: chapter-9 geo-search, live-poll (table display mode).
145
+ this.templateMode = name || 'default';
146
+ return this;
147
+ }
130
148
  listFrom(path) {
131
149
  // Signature: chapter-4/4-4-pagination (items list from query results).
132
150
  this.listFromPath = path;
@@ -501,7 +519,7 @@ const ui = (() => {
501
519
 
502
520
  if (section.actions.length > 0) {
503
521
  section.actions.forEach((action) => {
504
- actionWrap.appendChild(renderActionForm(action, output, section.appStore, sectionInputs, section));
522
+ actionWrap.appendChild(renderActionForm(action, output, section.appStore, sectionInputs, section, selectBindings));
505
523
  });
506
524
  root.appendChild(actionWrap);
507
525
  }
@@ -592,6 +610,9 @@ const ui = (() => {
592
610
  customWrap.appendChild(result);
593
611
  }
594
612
  }
613
+ if (section.renderHook) {
614
+ section.renderHook({ data, store: section.appStore, element: root });
615
+ }
595
616
  };
596
617
 
597
618
  const refresh = async () => {
@@ -599,16 +620,65 @@ const ui = (() => {
599
620
  if (section.readConfig) {
600
621
  const payload = buildQueryPayload(section, sectionInputs, section.appStore);
601
622
  const path = buildQueryPath(section.readConfig.path, payload);
602
- const data = await request(section.readConfig, null, false, payload, section.appStore, path);
623
+ let data;
624
+ try {
625
+ data = await request(section.readConfig, null, false, payload, section.appStore, path);
626
+ if (!data.ok && section.mockConfig) {
627
+ throw new Error("Backend error");
628
+ }
629
+ root.classList.remove('is-mocked');
630
+ root.style.borderStyle = '';
631
+ } catch (e) {
632
+ if (section.mockConfig || section.readConfig) {
633
+ console.warn(`⚠️ Mocking data for section "${section.title}"`);
634
+ let mockData;
635
+ if (section.mockConfig) {
636
+ mockData = typeof section.mockConfig === 'function' ? section.mockConfig(section.appStore) : section.mockConfig;
637
+ } else {
638
+ // Auto-inference logic
639
+ if (section.listTemplate) {
640
+ mockData = [
641
+ { id: 1, name: "Sample Item 1", amount: 100, description: "Mocked data" },
642
+ { id: 2, name: "Sample Item 2", amount: 200, description: "Mocked data" }
643
+ ];
644
+ } else if (section.kpiConfig) {
645
+ mockData = {};
646
+ section.kpiConfig.items.forEach(item => { if (item.key) mockData[item.key] = 0; });
647
+ } else {
648
+ mockData = { message: "Mocked response" };
649
+ }
650
+ }
651
+ data = { ok: true, status: 200, json: { data: mockData }, data: mockData };
652
+ root.classList.add('is-mocked');
653
+ root.style.borderStyle = 'dashed';
654
+ root.style.borderColor = '#cbd5e1'; // slate-300
655
+ if (!root.querySelector('.mock-badge')) {
656
+ const badge = document.createElement('span');
657
+ badge.className = 'mock-badge ml-2 inline-flex items-center rounded-md bg-amber-50 px-2 py-1 text-xs font-medium text-amber-700 ring-1 ring-inset ring-amber-600/20';
658
+ badge.textContent = 'MOCKED';
659
+ root.querySelector('h2').appendChild(badge);
660
+ }
661
+ } else {
662
+ root.classList.remove('is-mocked');
663
+ root.style.borderStyle = '';
664
+ const badge = root.querySelector('.mock-badge');
665
+ if (badge) badge.remove();
666
+ throw e;
667
+ }
668
+ }
669
+ if (!root.classList.contains('is-mocked')) {
670
+ const badge = root.querySelector('.mock-badge');
671
+ if (badge) badge.remove();
672
+ }
603
673
  section.lastData = data.json || null;
604
674
  applyStore(section.appStore, section.storeMap, data.json);
605
- const hasExplicitView = section.listFromPath || section.listTemplate || section.kpiConfig || section.jsonConfig || section.textConfig || section.markdownConfig || section.customRenderer;
675
+ const hasExplicitView = section.listFromPath || section.listTemplate || section.templateMode || section.kpiConfig || section.jsonConfig || section.textConfig || section.markdownConfig || section.customRenderer;
606
676
  if (!hasExplicitView && !section.autoConfig) {
607
677
  section.autoConfig = { path: section.listFromPath || 'data' };
608
678
  }
609
679
  if (section.listFromPath) {
610
680
  renderList(listWrap, section, { data: getPath(section.appStore, section.listFromPath) });
611
- } else if (section.listTemplate) {
681
+ } else if (section.listTemplate || section.templateMode) {
612
682
  renderList(listWrap, section, data);
613
683
  }
614
684
  if (metaEl && section.metaConfig) {
@@ -649,6 +719,8 @@ const ui = (() => {
649
719
 
650
720
  section.refresh = refresh;
651
721
 
722
+ window.addEventListener(`ui:refresh:${section.sectionId}`, refresh);
723
+
652
724
  return { root, refresh };
653
725
  }
654
726
 
@@ -830,12 +902,37 @@ const ui = (() => {
830
902
  target.appendChild(empty);
831
903
  return;
832
904
  }
905
+ // Table template mode
906
+ if (section.templateMode === 'table' && rows.length > 0) {
907
+ const table = document.createElement('table');
908
+ table.className = 'w-full text-sm border-collapse';
909
+ const keys = Object.keys(rows[0]);
910
+ const thead = document.createElement('thead');
911
+ thead.innerHTML = '<tr class="border-b border-slate-200">' + keys.map(k => `<th class="text-left py-2 px-3 font-medium text-slate-600">${k}</th>`).join('') + '</tr>';
912
+ table.appendChild(thead);
913
+ const tbody = document.createElement('tbody');
914
+ rows.forEach(row => {
915
+ const tr = document.createElement('tr');
916
+ tr.className = 'border-b border-slate-100 hover:bg-slate-50';
917
+ tr.innerHTML = keys.map(k => `<td class="py-2 px-3 text-slate-700">${row[k] ?? ''}</td>`).join('');
918
+ tbody.appendChild(tr);
919
+ });
920
+ table.appendChild(tbody);
921
+ target.appendChild(table);
922
+ return;
923
+ }
833
924
  rows.forEach((row) => {
834
925
  const card = document.createElement('div');
835
926
  card.className = 'rounded border border-slate-200 px-3 py-2 space-y-2';
836
927
  if (section.listTemplate) {
837
928
  const label = document.createElement('div');
838
- label.textContent = renderTemplate(section.listTemplate, row);
929
+ const rendered = renderTemplate(section.listTemplate, row);
930
+ // Use innerHTML if template contains HTML tags, otherwise textContent
931
+ if (/<[^>]+>/.test(rendered)) {
932
+ label.innerHTML = rendered;
933
+ } else {
934
+ label.textContent = rendered;
935
+ }
839
936
  card.appendChild(label);
840
937
  } else {
841
938
  const label = document.createElement('div');
@@ -878,13 +975,16 @@ const ui = (() => {
878
975
  });
879
976
  }
880
977
 
881
- function renderActionForm(action, output, appStore, sectionInputs, section) {
978
+ function renderActionForm(action, output, appStore, sectionInputs, section, selectBindings) {
882
979
  const wrap = document.createElement('div');
883
980
  wrap.className = 'grid gap-2 sm:grid-cols-4';
884
981
  const fieldInputs = {};
885
982
  action.fieldList.forEach((field) => {
886
983
  const input = renderField(field, appStore);
887
984
  fieldInputs[field.key] = input;
985
+ if (field.optionsFrom && selectBindings) {
986
+ selectBindings.push({ field, input });
987
+ }
888
988
  wrap.appendChild(input);
889
989
  });
890
990
  const btn = document.createElement('button');
@@ -1477,9 +1577,36 @@ const ui = (() => {
1477
1577
  }
1478
1578
  }
1479
1579
 
1580
+ function loadScript(url) {
1581
+ return new Promise((resolve, reject) => {
1582
+ if (document.querySelector(`script[src="${url}"]`)) return resolve();
1583
+ const s = document.createElement('script');
1584
+ s.src = url;
1585
+ s.onload = resolve;
1586
+ s.onerror = reject;
1587
+ document.head.appendChild(s);
1588
+ });
1589
+ }
1590
+
1591
+ function loadCSS(url) {
1592
+ if (document.querySelector(`link[href="${url}"]`)) return;
1593
+ const l = document.createElement('link');
1594
+ l.rel = 'stylesheet';
1595
+ l.href = url;
1596
+ document.head.appendChild(l);
1597
+ }
1598
+
1480
1599
  autoMountPending();
1481
1600
 
1482
- return { app, mount };
1601
+ // Global listener to bridge ui-refresh events to section-specific events
1602
+ window.addEventListener('ui-refresh', (e) => {
1603
+ const id = e.detail && e.detail.id;
1604
+ if (id) {
1605
+ window.dispatchEvent(new CustomEvent(`ui:refresh:${id}`));
1606
+ }
1607
+ });
1608
+
1609
+ return { app, mount, loadScript, loadCSS };
1483
1610
  })();
1484
1611
 
1485
1612
  export default ui;
package/.env DELETED
@@ -1 +0,0 @@
1
- TOKEN=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