jsgui3-server 0.0.140 → 0.0.141

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/.github/agents/jsgui3-server.agent.md +699 -0
  2. package/.github/instructions/copilot.instructions.md +180 -0
  3. package/.playwright-mcp/page-2025-11-29T23-39-18-629Z.png +0 -0
  4. package/.playwright-mcp/page-2025-11-29T23-39-31-903Z.png +0 -0
  5. package/.playwright-mcp/page-2025-11-30T00-33-56-265Z.png +0 -0
  6. package/.playwright-mcp/page-2025-11-30T00-34-06-619Z.png +0 -0
  7. package/docs/agent-development-guide.md +108 -4
  8. package/docs/api-reference.md +116 -0
  9. package/docs/controls-development.md +127 -0
  10. package/docs/css/luxuryObsidianCss.js +1203 -0
  11. package/docs/css/obsidian-scrollbars.css +370 -0
  12. package/docs/diagrams/jsgui3-stack.svg +568 -0
  13. package/docs/guides/JSGUI3_UI_ARCHITECTURE_GUIDE.md +2527 -0
  14. package/docs/guides/OBSIDIAN_LUXURY_DESIGN_GUIDE.md +847 -0
  15. package/docs/jsgui3-vs-express-comparison.svg +542 -0
  16. package/docs/jsgui3-vs-nestjs-comparison.svg +550 -0
  17. package/docs/publishers-guide.md +76 -0
  18. package/docs/troubleshooting.md +51 -0
  19. package/examples/controls/15) window, observable SSE/README.md +125 -0
  20. package/examples/controls/15) window, observable SSE/check.js +144 -0
  21. package/examples/controls/15) window, observable SSE/client.js +395 -0
  22. package/examples/controls/15) window, observable SSE/server.js +111 -0
  23. package/http/responders/static/Static_Route_HTTP_Responder.js +16 -16
  24. package/module.js +7 -0
  25. package/package.json +7 -6
  26. package/port-utils.js +112 -0
  27. package/serve-factory.js +27 -5
  28. package/tests/README.md +40 -26
  29. package/tests/examples-controls.e2e.test.js +164 -0
  30. package/tests/observable-sse.test.js +363 -0
  31. package/tests/port-utils.test.js +114 -0
  32. package/tests/test-runner.js +13 -12
@@ -0,0 +1,2527 @@
1
+ # jsgui3 UI Architecture Guide for AI Agents
2
+
3
+ **Target Audience**: AI coding agents working on jsgui3 UIs
4
+ **Scope**: Component-based architecture, control composition, isomorphic patterns, and SSR
5
+ **Last Updated**: November 2025
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ 1. [Overview](#overview)
12
+ 2. [Isomorphic Architecture](#isomorphic-architecture)
13
+ 3. [Core Concepts](#core-concepts)
14
+ 4. [Control Architecture](#control-architecture)
15
+ 5. [Creating Controls](#creating-controls)
16
+ 6. [Composition Patterns](#composition-patterns)
17
+ 7. [Project Structure](#project-structure)
18
+ 8. [Verification Scripts](#verification-scripts)
19
+ 9. [Development Server & Detached Mode](#development-server--detached-mode)
20
+ 10. [Common Patterns](#common-patterns)
21
+ 11. [**Extending jsgui3 (Plugins, Mixins, Extensions)**](#extending-jsgui3-plugins-mixins-extensions)
22
+ 12. [Dashboard Server Performance Patterns](#dashboard-server-performance-patterns)
23
+ 13. [Anti-Patterns to Avoid](#anti-patterns-to-avoid)
24
+ 14. [Quick Reference](#quick-reference)
25
+ 15. [**Client-Side Activation Flow (CRITICAL)**](#client-side-activation-flow-critical)
26
+ 16. [Troubleshooting](#troubleshooting)
27
+
28
+ ---
29
+
30
+ ## Workflow Discovery Quickstart
31
+
32
+ 1. Before touching any jsgui3 surface, run `node tools/dev/md-scan.js --dir docs --search "jsgui3 workflow" --json` (tweak the search term: `activation`, `control registration`, `dashboard wiring`, etc.).
33
+ 2. Record which sections you rely on inside your session notes (`WORKING_NOTES.md`) so the next agent can pick up the same trail.
34
+ 3. If the workflow you find is incomplete, expand this guide (or the relevant workflow doc) and mention the new section in your summary so md-scan keeps surfacing it.
35
+
36
+ > **Tip:** Use `--find-sections "Client-Side Activation"` when you already know the section title you need.
37
+
38
+ ## Overview
39
+
40
+ > **Visual Guide**: See [`jsgui3-architecture-diagram.svg`](../diagrams/jsgui3-architecture-diagram.svg) for an illustrated overview of the isomorphic architecture.
41
+
42
+ **jsgui3-html** is an **isomorphic** component library that works on both server and client. Controls are JavaScript classes that build DOM structures programmatically. On the server, they render to HTML strings; on the client, they can activate existing DOM and bind events (called "hydration" in other frameworks).
43
+
44
+ ### When to Use jsgui3
45
+
46
+ - Server-rendered pages with structured, reusable components
47
+ - Admin dashboards, data explorers, documentation viewers
48
+ - Pages where SEO and initial load performance matter
49
+ - UIs that need consistent structure across multiple views
50
+
51
+ ### Key Characteristics
52
+
53
+ ```javascript
54
+ // Server: controls render to HTML strings
55
+ const control = new MyControl({ context, data });
56
+ const html = control.all_html_render(); // → "<div class=\"my-control\">...</div>"
57
+
58
+ // Client: controls activate existing DOM and bind events
59
+ const control = new MyControl({ context, el: existingElement });
60
+ control.activate(); // Binds click handlers, etc.
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Isomorphic Architecture
66
+
67
+ ### Library Relationship
68
+
69
+ ```
70
+ jsgui3-html (core isomorphic library)
71
+
72
+ ├── Used directly on server for SSR
73
+
74
+ └── jsgui3-client (extends jsgui3-html)
75
+
76
+ └── Adds browser-specific features:
77
+ • Client_Page_Context
78
+ • activate() lifecycle
79
+ • Event binding
80
+ • DOM activation
81
+ ```
82
+
83
+ **Key insight**: `jsgui3-client` **requires** `jsgui3-html` internally. The core Control API is identical on both server and client.
84
+
85
+ ### Same Code, Different Environments
86
+
87
+ Controls using `require("jsgui3-html")` work in both environments:
88
+
89
+ ```javascript
90
+ // This control works on BOTH server and client
91
+ const jsgui = require("jsgui3-html");
92
+
93
+ class MyButtonControl extends jsgui.Control {
94
+ constructor(spec = {}) {
95
+ super({ ...spec, tagName: "button", __type_name: "my_button" });
96
+ this.label = spec.label || "Click me";
97
+ if (!spec.el) this.compose();
98
+ }
99
+
100
+ compose() {
101
+ this.add(new jsgui.String_Control({ context: this.context, text: this.label }));
102
+ }
103
+ }
104
+ ```
105
+
106
+ - **Server**: `node` resolves `jsgui3-html` → renders HTML
107
+ - **Client**: Bundler (esbuild) includes `jsgui3-html` → control works in browser
108
+
109
+ ### When to Use jsgui3-client
110
+
111
+ Use `require("jsgui3-client")` only when you need **client-specific features**:
112
+
113
+ ```javascript
114
+ // Client-only control with activation lifecycle
115
+ const jsgui = require("jsgui3-client");
116
+
117
+ class DocsThemeToggleControl extends jsgui.Control {
118
+ constructor(spec = {}) {
119
+ super({ ...spec, tagName: "button" });
120
+ this.__type_name = "docs_theme_toggle";
121
+ if (!spec.el) this.compose();
122
+ }
123
+
124
+ // Client-side activation - binds events to existing DOM
125
+ activate() {
126
+ if (this.__active) return;
127
+ this.__active = true;
128
+
129
+ const el = this.dom?.el;
130
+ if (!el) return;
131
+
132
+ el.addEventListener("click", (e) => {
133
+ e.preventDefault();
134
+ this.toggleTheme();
135
+ });
136
+ }
137
+
138
+ toggleTheme() {
139
+ // Client-side theme switching logic
140
+ }
141
+ }
142
+ ```
143
+
144
+ ### Bundling for Browser
145
+
146
+ The build script (`scripts/build-docs-viewer-client.js`) uses esbuild to bundle controls:
147
+
148
+ ```javascript
149
+ await esbuild.build({
150
+ entryPoints: [entryPoint],
151
+ bundle: true,
152
+ platform: "browser",
153
+ format: "iife",
154
+ // jsgui3-client from npm (v0.0.121+) has browser compatibility fixes
155
+ // No special aliasing needed - just require("jsgui3-client")
156
+ });
157
+ ```
158
+
159
+ The bundler resolves all `require()` statements and produces a single browser-ready file.
160
+
161
+ ### Activation Pattern
162
+
163
+ Server renders HTML with data attributes, client activates:
164
+
165
+ > **Terminology note**: jsgui3 calls this process "activation". Other frameworks (React, Vue, Svelte) call the equivalent process "hydration".
166
+
167
+ ```javascript
168
+ // Server-side: render with marker attribute
169
+ class MyControl extends jsgui.Control {
170
+ constructor(spec) {
171
+ super(spec);
172
+ this.dom.attributes["data-jsgui-control"] = "my_control";
173
+ // ...
174
+ }
175
+ }
176
+
177
+ // Client-side: find and activate marked elements
178
+ const elements = document.querySelectorAll("[data-jsgui-control]");
179
+ elements.forEach(el => {
180
+ const controlType = el.getAttribute("data-jsgui-control");
181
+ const ControlClass = CONTROL_TYPES[controlType];
182
+
183
+ // Create control with existing DOM element (skips compose)
184
+ const control = new ControlClass({ context, el });
185
+ control.activate(); // Bind events
186
+ });
187
+ ```
188
+
189
+ ### Project Structure for Isomorphic Apps
190
+
191
+ ```
192
+ src/ui/server/myApp/
193
+ ├── controls/ # Server-side controls (use jsgui3-html)
194
+ │ ├── MyAppControl.js
195
+ │ └── index.js
196
+ ├── client/ # Client-side entry + controls
197
+ │ ├── index.js # Bundle entry point
198
+ │ ├── controls/ # Client controls (use jsgui3-client)
199
+ │ │ └── MyInteractiveControl.js
200
+ │ └── shims/ # Browser shims for Node modules
201
+ └── public/ # Built assets
202
+ └── my-app-client.js # Bundled client code
203
+ ```
204
+
205
+ ---
206
+
207
+ ## Core Concepts
208
+
209
+ ### 1. Context
210
+
211
+ Every control requires a **context** object that manages rendering state. Create it once per page render:
212
+
213
+ ```javascript
214
+ const jsgui = require("jsgui3-html");
215
+
216
+ // Create context for a page render
217
+ const context = new jsgui.Page_Context();
218
+
219
+ // Pass context to all controls
220
+ const app = new MyAppControl({ context, ...props });
221
+ ```
222
+
223
+ **Critical Rule**: Context flows down through composition. Parent controls pass their `this.context` to child controls.
224
+
225
+ ### 2. Controls
226
+
227
+ Controls are classes that extend `jsgui.Control`. They:
228
+ - Accept a `spec` object in the constructor
229
+ - Store state as instance properties
230
+ - Build their DOM structure in a `compose()` method
231
+ - Render to HTML via `all_html_render()`
232
+
233
+ ```javascript
234
+ class MyControl extends jsgui.Control {
235
+ constructor(spec = {}) {
236
+ super({
237
+ ...spec,
238
+ tagName: "div", // Root element tag
239
+ __type_name: "my_control" // Internal type identifier
240
+ });
241
+
242
+ // Store state from spec
243
+ this.title = spec.title || "Default";
244
+
245
+ // Compose after state is set
246
+ if (!spec.el) {
247
+ this.compose();
248
+ }
249
+ }
250
+
251
+ compose() {
252
+ // Build child elements here
253
+ }
254
+ }
255
+ ```
256
+
257
+ ### 3. Adding Text Content
258
+
259
+ jsgui3 **automatically wraps raw strings** when you call `.add()`. Both approaches work:
260
+
261
+ ```javascript
262
+ // ✅ Simple - jsgui3 auto-wraps strings
263
+ element.add("Hello World");
264
+
265
+ // ✅ Explicit - useful when you need the control reference
266
+ const StringControl = jsgui.String_Control;
267
+ element.add(new StringControl({ context: this.context, text: "Hello World" }));
268
+ ```
269
+
270
+ **When to use explicit `String_Control`**:
271
+ - When you need a reference to the text node for later manipulation
272
+ - When building reusable components where explicitness aids clarity
273
+ - Legacy code compatibility (older jsgui patterns)
274
+
275
+ For most cases, just use `.add("text")` directly.
276
+
277
+ ---
278
+
279
+ ## Control Architecture
280
+
281
+ ### Inheritance Hierarchy
282
+
283
+ ```
284
+ jsgui.Control (base)
285
+ └── BaseAppControl (shared app-level features)
286
+ ├── DocAppControl (docs viewer)
287
+ ├── ExplorerAppControl (data explorer)
288
+ ├── DiagramAtlasAppControl (diagram atlas)
289
+ └── GazetteerAppControl (gazetteer)
290
+ ```
291
+
292
+ ### BaseAppControl Pattern
293
+
294
+ Create a shared base class for app-level controls that provides:
295
+ - Common header/footer structure
296
+ - Navigation building
297
+ - Section creation helpers
298
+
299
+ ```javascript
300
+ class BaseAppControl extends jsgui.Control {
301
+ constructor(spec = {}) {
302
+ super({
303
+ ...spec,
304
+ tagName: "div",
305
+ __type_name: "base_app"
306
+ });
307
+
308
+ this.appName = spec.appName || "App";
309
+ this.appClass = spec.appClass || "app";
310
+ this.title = spec.title || this.appName;
311
+
312
+ this.add_class(this.appClass);
313
+
314
+ // DO NOT call compose() here - subclasses must call it
315
+ // after setting their own properties
316
+ }
317
+
318
+ compose() {
319
+ this.headerContainer = this._buildHeader();
320
+ this.add(this.headerContainer);
321
+
322
+ this.mainContainer = new jsgui.Control({
323
+ context: this.context,
324
+ tagName: "main"
325
+ });
326
+ this.mainContainer.add_class(`${this.appClass}__main`);
327
+ this.add(this.mainContainer);
328
+
329
+ // Hook for subclasses
330
+ this.composeMainContent();
331
+
332
+ this.footerContainer = this._buildFooter();
333
+ this.add(this.footerContainer);
334
+ }
335
+
336
+ // Override in subclasses
337
+ composeMainContent() {}
338
+
339
+ _buildHeader() { /* ... */ }
340
+ _buildFooter() { /* ... */ }
341
+ }
342
+ ```
343
+
344
+ ### Critical: Constructor Timing
345
+
346
+ **JavaScript class inheritance runs the parent constructor before the child constructor.**
347
+
348
+ This means properties set in a child constructor are NOT available when the parent constructor runs.
349
+
350
+ ```javascript
351
+ // ❌ WRONG - compose() called before child properties exist
352
+ class BaseControl extends jsgui.Control {
353
+ constructor(spec) {
354
+ super(spec);
355
+ this.compose(); // Child's this.data is undefined here!
356
+ }
357
+ }
358
+
359
+ class ChildControl extends BaseControl {
360
+ constructor(spec) {
361
+ super(spec); // Parent's constructor runs FIRST
362
+ this.data = spec.data; // This runs AFTER parent's compose()
363
+ }
364
+ }
365
+ ```
366
+
367
+ ```javascript
368
+ // ✅ CORRECT - child calls compose() after setting properties
369
+ class BaseControl extends jsgui.Control {
370
+ constructor(spec) {
371
+ super(spec);
372
+ // DO NOT call compose() here
373
+ }
374
+ }
375
+
376
+ class ChildControl extends BaseControl {
377
+ constructor(spec) {
378
+ super(spec);
379
+ this.data = spec.data; // Set properties first
380
+
381
+ if (!spec.el) {
382
+ this.compose(); // Now compose with all properties available
383
+ }
384
+ }
385
+ }
386
+ ```
387
+
388
+ ---
389
+
390
+ ## Creating Controls
391
+
392
+ ### Step 1: Define the Control Class
393
+
394
+ ```javascript
395
+ "use strict";
396
+
397
+ const jsgui = require("jsgui3-html");
398
+ const StringControl = jsgui.String_Control;
399
+
400
+ /**
401
+ * PlaceBadgeControl - Reusable badge for place metadata
402
+ *
403
+ * @example
404
+ * const badge = new PlaceBadgeControl({
405
+ * context,
406
+ * text: "city",
407
+ * variant: "kind"
408
+ * });
409
+ */
410
+ class PlaceBadgeControl extends jsgui.Control {
411
+ /**
412
+ * @param {Object} spec - Control specification
413
+ * @param {Object} spec.context - jsgui context (required)
414
+ * @param {string} spec.text - Badge text content
415
+ * @param {string} [spec.variant] - Visual variant: "kind", "country", "default"
416
+ */
417
+ constructor(spec = {}) {
418
+ super({
419
+ ...spec,
420
+ tagName: "span",
421
+ __type_name: "place_badge"
422
+ });
423
+
424
+ this.add_class("gazetteer__badge");
425
+
426
+ this.text = spec.text || "";
427
+ this.variant = spec.variant || "default";
428
+
429
+ if (this.variant !== "default") {
430
+ this.add_class(`gazetteer__badge--${this.variant}`);
431
+ }
432
+
433
+ if (!spec.el) {
434
+ this.compose();
435
+ }
436
+ }
437
+
438
+ compose() {
439
+ this.add(new StringControl({ context: this.context, text: this.text }));
440
+ }
441
+ }
442
+
443
+ module.exports = { PlaceBadgeControl };
444
+ ```
445
+
446
+ ### Step 2: Key Patterns
447
+
448
+ #### Adding CSS Classes
449
+
450
+ ```javascript
451
+ // Single class
452
+ control.add_class("my-class");
453
+
454
+ // BEM naming convention
455
+ control.add_class("block__element");
456
+ control.add_class("block__element--modifier");
457
+ ```
458
+
459
+ #### Setting Attributes
460
+
461
+ ```javascript
462
+ // Standard attributes
463
+ control.dom.attributes.href = "/path";
464
+ control.dom.attributes.type = "submit";
465
+ control.dom.attributes.value = "Search";
466
+
467
+ // Data attributes
468
+ control.dom.attributes["data-role"] = "toolbar";
469
+ control.dom.attributes["data-metric"] = "count";
470
+
471
+ // Boolean attributes
472
+ control.dom.attributes.disabled = "disabled";
473
+ control.dom.attributes.selected = "selected";
474
+ ```
475
+
476
+ #### Creating Child Elements
477
+
478
+ ```javascript
479
+ compose() {
480
+ // Create a child element
481
+ const header = new jsgui.Control({
482
+ context: this.context,
483
+ tagName: "header"
484
+ });
485
+ header.add_class("my-control__header");
486
+
487
+ // Add text content
488
+ const title = new jsgui.Control({ context: this.context, tagName: "h1" });
489
+ title.add(new StringControl({ context: this.context, text: this.title }));
490
+ header.add(title);
491
+
492
+ // Add to parent
493
+ this.add(header);
494
+ }
495
+ ```
496
+
497
+ #### Building Methods Pattern
498
+
499
+ For complex controls, break composition into private builder methods:
500
+
501
+ ```javascript
502
+ compose() {
503
+ const header = this._buildHeader();
504
+ this.add(header);
505
+
506
+ const content = this._buildContent();
507
+ this.add(content);
508
+
509
+ const footer = this._buildFooter();
510
+ this.add(footer);
511
+ }
512
+
513
+ _buildHeader() {
514
+ const header = new jsgui.Control({ context: this.context, tagName: "header" });
515
+ // ... build header structure
516
+ return header;
517
+ }
518
+
519
+ _buildContent() {
520
+ const content = new jsgui.Control({ context: this.context, tagName: "main" });
521
+ // ... build content structure
522
+ return content;
523
+ }
524
+
525
+ _buildFooter() {
526
+ const footer = new jsgui.Control({ context: this.context, tagName: "footer" });
527
+ // ... build footer structure
528
+ return footer;
529
+ }
530
+ ```
531
+
532
+ ---
533
+
534
+ ## Composition Patterns
535
+
536
+ ### Pattern 1: Direct Instantiation
537
+
538
+ When a component is simple and self-contained:
539
+
540
+ ```javascript
541
+ _buildHeader() {
542
+ const header = new jsgui.Control({ context: this.context, tagName: "header" });
543
+
544
+ // Use a dedicated control
545
+ const toolbar = new DiagramToolbarControl({
546
+ context: this.context,
547
+ snapshotTime: this.generatedAt,
548
+ status: "complete"
549
+ });
550
+ header.add(toolbar);
551
+
552
+ return header;
553
+ }
554
+ ```
555
+
556
+ ### Pattern 2: Iterative Composition
557
+
558
+ When rendering lists of similar items:
559
+
560
+ ```javascript
561
+ _buildResultsList() {
562
+ const list = new jsgui.Control({ context: this.context, tagName: "div" });
563
+ list.add_class("results-list");
564
+
565
+ for (const item of this.results) {
566
+ const itemControl = new ResultItemControl({
567
+ context: this.context,
568
+ data: item
569
+ });
570
+ list.add(itemControl);
571
+ }
572
+
573
+ return list;
574
+ }
575
+ ```
576
+
577
+ ### Pattern 3: Conditional Composition
578
+
579
+ When structure depends on state:
580
+
581
+ ```javascript
582
+ composeMainContent() {
583
+ if (!this.data) {
584
+ // Empty state
585
+ const placeholder = new jsgui.Control({ context: this.context, tagName: "div" });
586
+ placeholder.add_class("placeholder");
587
+ placeholder.add(new StringControl({ context: this.context, text: "No data available" }));
588
+ this.mainContainer.add(placeholder);
589
+ return;
590
+ }
591
+
592
+ // Normal state with data
593
+ const content = this._buildDataContent();
594
+ this.mainContainer.add(content);
595
+ }
596
+ ```
597
+
598
+ ### Pattern 4: View Type Switching
599
+
600
+ When a single control handles multiple view types:
601
+
602
+ ```javascript
603
+ const VIEW_TYPES = Object.freeze({
604
+ SEARCH: "search",
605
+ DETAIL: "detail",
606
+ DASHBOARD: "dashboard"
607
+ });
608
+
609
+ class MyAppControl extends BaseAppControl {
610
+ constructor(spec) {
611
+ super(spec);
612
+ this.viewType = spec.viewType || VIEW_TYPES.SEARCH;
613
+ // ...
614
+ }
615
+
616
+ composeMainContent() {
617
+ switch (this.viewType) {
618
+ case VIEW_TYPES.SEARCH:
619
+ this._composeSearchView();
620
+ break;
621
+ case VIEW_TYPES.DETAIL:
622
+ this._composeDetailView();
623
+ break;
624
+ case VIEW_TYPES.DASHBOARD:
625
+ this._composeDashboardView();
626
+ break;
627
+ }
628
+ }
629
+ }
630
+
631
+ MyAppControl.VIEW_TYPES = VIEW_TYPES;
632
+ ```
633
+
634
+ ---
635
+
636
+ ## Project Structure
637
+
638
+ ### Recommended Directory Layout
639
+
640
+ ```
641
+ src/ui/
642
+ ├── controls/ # Shared/generic controls
643
+ │ ├── Table.js
644
+ │ ├── PagerButton.js
645
+ │ └── checks/ # Check scripts for shared controls
646
+ │ └── Table.check.js
647
+
648
+ ├── server/
649
+ │ ├── shared/ # Shared app-level infrastructure
650
+ │ │ ├── BaseAppControl.js
651
+ │ │ └── index.js
652
+ │ │
653
+ │ ├── dataExplorer/ # Data Explorer app
654
+ │ │ ├── controls/
655
+ │ │ │ ├── ExplorerAppControl.js
656
+ │ │ │ ├── ExplorerHomeCardControl.js
657
+ │ │ │ ├── ExplorerPaginationControl.js
658
+ │ │ │ └── index.js
659
+ │ │ ├── checks/
660
+ │ │ │ └── ExplorerAppControl.check.js
661
+ │ │ └── dataExplorerServer.js
662
+ │ │
663
+ │ ├── gazetteer/ # Gazetteer app
664
+ │ │ ├── controls/
665
+ │ │ │ ├── GazetteerAppControl.js
666
+ │ │ │ ├── GazetteerSearchFormControl.js
667
+ │ │ │ ├── GazetteerBreadcrumbControl.js
668
+ │ │ │ ├── GazetteerResultItemControl.js
669
+ │ │ │ ├── PlaceBadgeControl.js
670
+ │ │ │ └── index.js
671
+ │ │ └── checks/
672
+ │ │ └── GazetteerAppControl.check.js
673
+ │ │
674
+ │ └── diagramAtlas/ # Diagram Atlas app
675
+ │ ├── controls/
676
+ │ │ ├── DiagramAtlasAppControl.js
677
+ │ │ ├── DiagramToolbarControl.js
678
+ │ │ ├── DiagramDiagnosticsControl.js
679
+ │ │ └── index.js
680
+ │ └── checks/
681
+ │ ├── DiagramAtlasAppControl.check.js
682
+ │ └── DiagramToolbarControl.check.js
683
+ ```
684
+
685
+ ### Index Files
686
+
687
+ Each controls directory should have an `index.js` that exports all controls:
688
+
689
+ ```javascript
690
+ "use strict";
691
+
692
+ const { GazetteerAppControl, VIEW_TYPES } = require("./GazetteerAppControl");
693
+ const { GazetteerSearchFormControl, KIND_OPTIONS } = require("./GazetteerSearchFormControl");
694
+ const { GazetteerBreadcrumbControl } = require("./GazetteerBreadcrumbControl");
695
+ const { GazetteerResultItemControl } = require("./GazetteerResultItemControl");
696
+ const { PlaceBadgeControl, BADGE_VARIANTS } = require("./PlaceBadgeControl");
697
+
698
+ module.exports = {
699
+ GazetteerAppControl,
700
+ GazetteerSearchFormControl,
701
+ GazetteerBreadcrumbControl,
702
+ GazetteerResultItemControl,
703
+ PlaceBadgeControl,
704
+ VIEW_TYPES,
705
+ KIND_OPTIONS,
706
+ BADGE_VARIANTS
707
+ };
708
+ ```
709
+
710
+ ---
711
+
712
+ ## Verification Scripts
713
+
714
+ ### Purpose
715
+
716
+ Check scripts verify that controls render correctly without running the full application. They:
717
+ - Instantiate controls with sample data
718
+ - Render to HTML
719
+ - Assert expected content/structure exists
720
+ - Provide quick feedback during development
721
+
722
+ ### Check Script Template
723
+
724
+ ```javascript
725
+ "use strict";
726
+
727
+ /**
728
+ * MyControl Check Script
729
+ *
730
+ * Run with: node src/ui/server/myApp/checks/MyControl.check.js
731
+ */
732
+
733
+ const jsgui = require("jsgui3-html");
734
+ const { MyControl } = require("../controls/MyControl");
735
+
736
+ // Create context for rendering
737
+ function createContext() {
738
+ return new jsgui.Page_Context();
739
+ }
740
+
741
+ // Sample data
742
+ const SAMPLE_DATA = {
743
+ title: "Test Title",
744
+ items: [
745
+ { id: 1, name: "Item 1" },
746
+ { id: 2, name: "Item 2" }
747
+ ]
748
+ };
749
+
750
+ console.log("MyControl Verification");
751
+ console.log("========================================\n");
752
+
753
+ let totalPassed = 0;
754
+ let totalFailed = 0;
755
+
756
+ function check(condition, name) {
757
+ if (condition) {
758
+ console.log(` ✅ ${name}`);
759
+ totalPassed++;
760
+ } else {
761
+ console.log(` ❌ ${name}`);
762
+ totalFailed++;
763
+ }
764
+ }
765
+
766
+ // Test 1: Basic rendering
767
+ console.log("📋 Testing basic rendering...");
768
+ {
769
+ const ctx = createContext();
770
+ const control = new MyControl({
771
+ context: ctx,
772
+ title: SAMPLE_DATA.title,
773
+ items: SAMPLE_DATA.items
774
+ });
775
+
776
+ const html = control.all_html_render();
777
+
778
+ check(html.includes("my-control"), "has my-control class");
779
+ check(html.includes("Test Title"), "contains title");
780
+ check(html.includes("Item 1"), "contains first item");
781
+ check(html.includes("Item 2"), "contains second item");
782
+ }
783
+
784
+ // Test 2: Empty state
785
+ console.log("\n📋 Testing empty state...");
786
+ {
787
+ const ctx = createContext();
788
+ const control = new MyControl({
789
+ context: ctx,
790
+ title: "Empty",
791
+ items: []
792
+ });
793
+
794
+ const html = control.all_html_render();
795
+
796
+ check(html.includes("my-control"), "has my-control class");
797
+ check(html.includes("no-items") || html.includes("No items"), "shows empty state");
798
+ }
799
+
800
+ // Summary
801
+ console.log("\n========================================");
802
+ if (totalFailed === 0) {
803
+ console.log(`✅ All checks passed! (${totalPassed}/${totalPassed})`);
804
+ process.exit(0);
805
+ } else {
806
+ console.log(`❌ ${totalFailed} checks failed`);
807
+ process.exit(1);
808
+ }
809
+ ```
810
+
811
+ ### Running Check Scripts
812
+
813
+ ```bash
814
+ # Single control
815
+ node src/ui/server/gazetteer/checks/GazetteerAppControl.check.js
816
+
817
+ # Multiple controls (PowerShell)
818
+ node src/ui/server/diagramAtlas/checks/DiagramAtlasAppControl.check.js; `
819
+ node src/ui/server/dataExplorer/checks/ExplorerAppControl.check.js
820
+ ```
821
+
822
+ ---
823
+
824
+ ## Development Server & Detached Mode
825
+
826
+ ### Server Overview
827
+
828
+ jsgui3 UI applications run on Express servers that serve SSR pages and API endpoints. The primary server for the Data Explorer is `dataExplorerServer.js`.
829
+
830
+ ### The Problem: Servers Die When Terminal Commands Run
831
+
832
+ **Critical for AI Agents**: When you start a server in a terminal and then run another command, the server process often terminates. This happens because:
833
+
834
+ 1. Running a new command in the same terminal may send signals to child processes
835
+ 2. PowerShell/shell may propagate interrupt signals
836
+ 3. The agent's next action kills the previous process
837
+
838
+ **Symptoms**:
839
+ - Server starts successfully, shows "listening on port 4600"
840
+ - Agent runs another command (e.g., `npm run build`)
841
+ - Subsequent requests to `http://127.0.0.1:4600/` fail with connection refused
842
+ - Agent wastes time debugging "why isn't the server working"
843
+
844
+ ### Solution: Detached Mode
845
+
846
+ The `dataExplorerServer.js` supports **detached mode** for running independently of the terminal:
847
+
848
+ ```bash
849
+ # Start server in detached mode (runs in background)
850
+ node src/ui/server/dataExplorerServer.js --detached --port 4600
851
+
852
+ # Check if server is running
853
+ node src/ui/server/dataExplorerServer.js --status
854
+
855
+ # Stop detached server
856
+ node src/ui/server/dataExplorerServer.js --stop
857
+ ```
858
+
859
+ **Output examples**:
860
+ ```
861
+ # --detached
862
+ 🔍 Data Explorer started in background (PID: 12345)
863
+ URL: http://127.0.0.1:4600/domains
864
+ Stop with: node src\ui\server\dataExplorerServer.js --stop
865
+
866
+ # --status (running)
867
+ 🔍 Data Explorer: running (PID: 12345)
868
+
869
+ # --status (not running)
870
+ 🔍 Data Explorer: not running
871
+
872
+ # --stop
873
+ 🔍 Data Explorer stopped (was PID: 12345)
874
+ ```
875
+
876
+ ### Agent Workflow: Server Management
877
+
878
+ **Before starting a server**:
879
+ ```bash
880
+ # Always stop any existing detached server first
881
+ node src/ui/server/dataExplorerServer.js --stop 2>$null
882
+
883
+ # Then start fresh in detached mode
884
+ node src/ui/server/dataExplorerServer.js --detached --port 4600
885
+ ```
886
+
887
+ **When debugging server issues**:
888
+ ```bash
889
+ # Check if server is actually running
890
+ node src/ui/server/dataExplorerServer.js --status
891
+
892
+ # If not running, start it
893
+ node src/ui/server/dataExplorerServer.js --detached --port 4600
894
+ ```
895
+
896
+ **After making server-side changes**:
897
+ ```bash
898
+ # Restart to pick up code changes
899
+ node src/ui/server/dataExplorerServer.js --stop
900
+ node src/ui/server/dataExplorerServer.js --detached --port 4600
901
+ ```
902
+
903
+ ### PID File Location
904
+
905
+ Detached mode uses a PID file to track the running process:
906
+ - **Location**: `tmp/.data-explorer.pid`
907
+ - **Contents**: Process ID of the detached server
908
+ - **Cleanup**: Automatically deleted when `--stop` succeeds
909
+
910
+ ### When NOT to Use Detached Mode
911
+
912
+ - **Debugging with console.log**: Use foreground mode to see output
913
+ - **Watching for errors**: Foreground mode shows stack traces immediately
914
+ - **Development iteration**: Sometimes easier to Ctrl+C and restart
915
+
916
+ For debugging, run in foreground in a dedicated terminal:
917
+ ```bash
918
+ node src/ui/server/dataExplorerServer.js --port 4600
919
+ ```
920
+
921
+ ### Quick Reference
922
+
923
+ | Command | Purpose |
924
+ |---------|---------|
925
+ | `--detached` | Start server in background, survives terminal commands |
926
+ | `--status` | Check if detached server is running |
927
+ | `--stop` | Stop detached server |
928
+ | `--port <n>` | Specify port (default: 4600) |
929
+
930
+ ### Adding Detached Mode to Other Servers
931
+
932
+ If you need to add detached mode to another server, the pattern is:
933
+
934
+ ```javascript
935
+ const { spawn } = require("child_process");
936
+ const fs = require("fs");
937
+ const path = require("path");
938
+
939
+ const PID_FILE = path.join(__dirname, "../../../tmp/.my-server.pid");
940
+
941
+ function spawnDetached(port) {
942
+ const child = spawn(process.execPath, [__filename, "--port", String(port)], {
943
+ detached: true,
944
+ stdio: "ignore",
945
+ cwd: process.cwd()
946
+ });
947
+ child.unref();
948
+ fs.mkdirSync(path.dirname(PID_FILE), { recursive: true });
949
+ fs.writeFileSync(PID_FILE, String(child.pid));
950
+ console.log(`Server started in background (PID: ${child.pid})`);
951
+ }
952
+
953
+ function stopDetached() {
954
+ if (!fs.existsSync(PID_FILE)) {
955
+ console.log("No detached server found");
956
+ return;
957
+ }
958
+ const pid = parseInt(fs.readFileSync(PID_FILE, "utf8"), 10);
959
+ try {
960
+ process.kill(pid, "SIGTERM");
961
+ fs.unlinkSync(PID_FILE);
962
+ console.log(`Server stopped (was PID: ${pid})`);
963
+ } catch (e) {
964
+ fs.unlinkSync(PID_FILE);
965
+ console.log("Server was not running (stale PID file cleaned)");
966
+ }
967
+ }
968
+
969
+ function checkStatus() {
970
+ if (!fs.existsSync(PID_FILE)) {
971
+ console.log("Server: not running");
972
+ return;
973
+ }
974
+ const pid = parseInt(fs.readFileSync(PID_FILE, "utf8"), 10);
975
+ try {
976
+ process.kill(pid, 0); // Check if process exists
977
+ console.log(`Server: running (PID: ${pid})`);
978
+ } catch {
979
+ console.log("Server: not running (stale PID file)");
980
+ }
981
+ }
982
+ ```
983
+
984
+ ---
985
+
986
+ ## Common Patterns
987
+
988
+ ### 1. Form Controls
989
+
990
+ ```javascript
991
+ class SearchFormControl extends jsgui.Control {
992
+ constructor(spec = {}) {
993
+ super({ ...spec, tagName: "form", __type_name: "search_form" });
994
+
995
+ this.add_class("search-form");
996
+ this.dom.attributes.action = spec.action || "/search";
997
+ this.dom.attributes.method = "get";
998
+
999
+ this.query = spec.query || "";
1000
+ this.placeholder = spec.placeholder || "Search...";
1001
+
1002
+ if (!spec.el) this.compose();
1003
+ }
1004
+
1005
+ compose() {
1006
+ // Text input
1007
+ const input = new jsgui.Control({ context: this.context, tagName: "input" });
1008
+ input.dom.attributes.type = "text";
1009
+ input.dom.attributes.name = "q";
1010
+ input.dom.attributes.placeholder = this.placeholder;
1011
+ if (this.query) {
1012
+ input.dom.attributes.value = this.query;
1013
+ }
1014
+ input.add_class("search-form__input");
1015
+ this.add(input);
1016
+
1017
+ // Submit button
1018
+ const button = new jsgui.Control({ context: this.context, tagName: "button" });
1019
+ button.dom.attributes.type = "submit";
1020
+ button.add_class("search-form__button");
1021
+ button.add(new StringControl({ context: this.context, text: "Search" }));
1022
+ this.add(button);
1023
+ }
1024
+ }
1025
+ ```
1026
+
1027
+ ### 2. Select/Dropdown Controls
1028
+
1029
+ ```javascript
1030
+ const OPTIONS = [
1031
+ { value: "", label: "All" },
1032
+ { value: "type1", label: "Type 1" },
1033
+ { value: "type2", label: "Type 2" }
1034
+ ];
1035
+
1036
+ class FilterSelectControl extends jsgui.Control {
1037
+ constructor(spec = {}) {
1038
+ super({ ...spec, tagName: "select", __type_name: "filter_select" });
1039
+
1040
+ this.add_class("filter-select");
1041
+ this.dom.attributes.name = spec.name || "filter";
1042
+
1043
+ this.options = spec.options || OPTIONS;
1044
+ this.selectedValue = spec.selectedValue || "";
1045
+
1046
+ if (!spec.el) this.compose();
1047
+ }
1048
+
1049
+ compose() {
1050
+ for (const opt of this.options) {
1051
+ const option = new jsgui.Control({ context: this.context, tagName: "option" });
1052
+ option.dom.attributes.value = opt.value;
1053
+
1054
+ if (opt.value === this.selectedValue) {
1055
+ option.dom.attributes.selected = "selected";
1056
+ }
1057
+
1058
+ option.add(new StringControl({ context: this.context, text: opt.label }));
1059
+ this.add(option);
1060
+ }
1061
+ }
1062
+ }
1063
+ ```
1064
+
1065
+ ### 3. Breadcrumb Navigation
1066
+
1067
+ ```javascript
1068
+ class BreadcrumbControl extends jsgui.Control {
1069
+ constructor(spec = {}) {
1070
+ super({ ...spec, tagName: "nav", __type_name: "breadcrumb" });
1071
+
1072
+ this.add_class("breadcrumb");
1073
+
1074
+ this.items = spec.items || []; // [{ label, href }, ...]
1075
+ this.separator = spec.separator || " › ";
1076
+
1077
+ if (!spec.el) this.compose();
1078
+ }
1079
+
1080
+ compose() {
1081
+ this.items.forEach((item, index) => {
1082
+ // Add separator (except before first item)
1083
+ if (index > 0) {
1084
+ this.add(new StringControl({ context: this.context, text: this.separator }));
1085
+ }
1086
+
1087
+ if (item.href) {
1088
+ // Linked item
1089
+ const link = new jsgui.Control({ context: this.context, tagName: "a" });
1090
+ link.dom.attributes.href = item.href;
1091
+ link.add(new StringControl({ context: this.context, text: item.label }));
1092
+ this.add(link);
1093
+ } else {
1094
+ // Current page (no link)
1095
+ const span = new jsgui.Control({ context: this.context, tagName: "span" });
1096
+ span.add_class("breadcrumb__current");
1097
+ span.add(new StringControl({ context: this.context, text: item.label }));
1098
+ this.add(span);
1099
+ }
1100
+ });
1101
+ }
1102
+ }
1103
+ ```
1104
+
1105
+ ### 4. Card/Tile Components
1106
+
1107
+ ```javascript
1108
+ class StatCardControl extends jsgui.Control {
1109
+ constructor(spec = {}) {
1110
+ super({ ...spec, tagName: "div", __type_name: "stat_card" });
1111
+
1112
+ this.add_class("stat-card");
1113
+ if (spec.variant) {
1114
+ this.add_class(`stat-card--${spec.variant}`);
1115
+ }
1116
+
1117
+ this.label = spec.label || "Stat";
1118
+ this.value = spec.value;
1119
+ this.detail = spec.detail || null;
1120
+ this.icon = spec.icon || null;
1121
+
1122
+ if (!spec.el) this.compose();
1123
+ }
1124
+
1125
+ compose() {
1126
+ // Icon (optional)
1127
+ if (this.icon) {
1128
+ const iconEl = new jsgui.Control({ context: this.context, tagName: "span" });
1129
+ iconEl.add_class("stat-card__icon");
1130
+ iconEl.add(new StringControl({ context: this.context, text: this.icon }));
1131
+ this.add(iconEl);
1132
+ }
1133
+
1134
+ // Label
1135
+ const labelEl = new jsgui.Control({ context: this.context, tagName: "div" });
1136
+ labelEl.add_class("stat-card__label");
1137
+ labelEl.add(new StringControl({ context: this.context, text: this.label }));
1138
+ this.add(labelEl);
1139
+
1140
+ // Value
1141
+ const valueEl = new jsgui.Control({ context: this.context, tagName: "div" });
1142
+ valueEl.add_class("stat-card__value");
1143
+ valueEl.add(new StringControl({ context: this.context, text: String(this.value ?? "—") }));
1144
+ this.add(valueEl);
1145
+
1146
+ // Detail (optional)
1147
+ if (this.detail) {
1148
+ const detailEl = new jsgui.Control({ context: this.context, tagName: "div" });
1149
+ detailEl.add_class("stat-card__detail");
1150
+ detailEl.add(new StringControl({ context: this.context, text: this.detail }));
1151
+ this.add(detailEl);
1152
+ }
1153
+ }
1154
+ }
1155
+ ```
1156
+
1157
+ ### 5. Table Components
1158
+
1159
+ ```javascript
1160
+ class SimpleTableControl extends jsgui.Control {
1161
+ constructor(spec = {}) {
1162
+ super({ ...spec, tagName: "table", __type_name: "simple_table" });
1163
+
1164
+ this.add_class("ui-table");
1165
+
1166
+ this.columns = spec.columns || []; // [{ key, label, width? }, ...]
1167
+ this.rows = spec.rows || []; // [{ key1: val1, key2: val2 }, ...]
1168
+
1169
+ if (!spec.el) this.compose();
1170
+ }
1171
+
1172
+ compose() {
1173
+ // Header
1174
+ const thead = new jsgui.Control({ context: this.context, tagName: "thead" });
1175
+ const headerRow = new jsgui.Control({ context: this.context, tagName: "tr" });
1176
+
1177
+ for (const col of this.columns) {
1178
+ const th = new jsgui.Control({ context: this.context, tagName: "th" });
1179
+ if (col.width) {
1180
+ th.dom.attributes.style = `width: ${col.width}`;
1181
+ }
1182
+ th.add(new StringControl({ context: this.context, text: col.label }));
1183
+ headerRow.add(th);
1184
+ }
1185
+
1186
+ thead.add(headerRow);
1187
+ this.add(thead);
1188
+
1189
+ // Body
1190
+ const tbody = new jsgui.Control({ context: this.context, tagName: "tbody" });
1191
+
1192
+ for (const row of this.rows) {
1193
+ const tr = new jsgui.Control({ context: this.context, tagName: "tr" });
1194
+
1195
+ for (const col of this.columns) {
1196
+ const td = new jsgui.Control({ context: this.context, tagName: "td" });
1197
+ const value = row[col.key];
1198
+ td.add(new StringControl({
1199
+ context: this.context,
1200
+ text: value != null ? String(value) : ""
1201
+ }));
1202
+ tr.add(td);
1203
+ }
1204
+
1205
+ tbody.add(tr);
1206
+ }
1207
+
1208
+ this.add(tbody);
1209
+ }
1210
+ }
1211
+ ```
1212
+
1213
+ ### 6. Pagination
1214
+
1215
+ ```javascript
1216
+ class PaginationControl extends jsgui.Control {
1217
+ constructor(spec = {}) {
1218
+ super({ ...spec, tagName: "div", __type_name: "pagination" });
1219
+
1220
+ this.add_class("pagination");
1221
+
1222
+ this.currentPage = spec.currentPage || 1;
1223
+ this.totalPages = spec.totalPages || 1;
1224
+ this.basePath = spec.basePath || "";
1225
+
1226
+ if (!spec.el) this.compose();
1227
+ }
1228
+
1229
+ compose() {
1230
+ // Previous button
1231
+ if (this.currentPage > 1) {
1232
+ const prev = new jsgui.Control({ context: this.context, tagName: "a" });
1233
+ prev.dom.attributes.href = `${this.basePath}?page=${this.currentPage - 1}`;
1234
+ prev.add_class("pagination__btn");
1235
+ prev.add(new StringControl({ context: this.context, text: "← Previous" }));
1236
+ this.add(prev);
1237
+ }
1238
+
1239
+ // Page info
1240
+ const info = new jsgui.Control({ context: this.context, tagName: "span" });
1241
+ info.add_class("pagination__info");
1242
+ info.add(new StringControl({
1243
+ context: this.context,
1244
+ text: `Page ${this.currentPage} of ${this.totalPages}`
1245
+ }));
1246
+ this.add(info);
1247
+
1248
+ // Next button
1249
+ if (this.currentPage < this.totalPages) {
1250
+ const next = new jsgui.Control({ context: this.context, tagName: "a" });
1251
+ next.dom.attributes.href = `${this.basePath}?page=${this.currentPage + 1}`;
1252
+ next.add_class("pagination__btn");
1253
+ next.add(new StringControl({ context: this.context, text: "Next →" }));
1254
+ this.add(next);
1255
+ }
1256
+ }
1257
+ }
1258
+ ```
1259
+
1260
+ ---
1261
+
1262
+ ## Extending jsgui3 (Plugins, Mixins, Extensions)
1263
+
1264
+ When jsgui3 doesn't provide functionality you need, you have options. The key principle: **write extensions that could be merged back into jsgui3** without major refactoring.
1265
+
1266
+ ### Philosophy: jsgui3 Abstracts the DOM
1267
+
1268
+ jsgui3's job is to abstract over the DOM. If you find yourself writing direct DOM manipulation code repeatedly, that's a signal that:
1269
+
1270
+ 1. **jsgui3 should handle it** - Consider writing an extension
1271
+ 2. **It's a jsgui3 bug** - Report it and document a temporary workaround
1272
+ 3. **It's genuinely client-specific** - Put it in `activate()` with clear comments
1273
+
1274
+ ### Extension Location
1275
+
1276
+ Place extensions in a dedicated directory that mirrors jsgui3's structure:
1277
+
1278
+ ```
1279
+ src/ui/
1280
+ ├── jsgui-extensions/ # Extensions designed for upstream contribution
1281
+ │ ├── controls/ # New control types
1282
+ │ │ └── ContextMenuControl.js
1283
+ │ ├── mixins/ # Behavior mixins (drag, resize, etc.)
1284
+ │ │ └── DraggableMixin.js
1285
+ │ ├── plugins/ # Context-level plugins
1286
+ │ │ └── TooltipPlugin.js
1287
+ │ └── index.js # Exports all extensions
1288
+ ```
1289
+
1290
+ ### Pattern 1: Control Extensions
1291
+
1292
+ Extend `jsgui.Control` for new component types:
1293
+
1294
+ ```javascript
1295
+ // src/ui/jsgui-extensions/controls/ContextMenuControl.js
1296
+ "use strict";
1297
+
1298
+ /**
1299
+ * ContextMenuControl - Reusable context menu component
1300
+ *
1301
+ * Designed for upstream contribution to jsgui3.
1302
+ *
1303
+ * @example
1304
+ * const menu = new ContextMenuControl({
1305
+ * context,
1306
+ * items: [
1307
+ * { label: "Edit", icon: "✏️", value: "edit" },
1308
+ * { label: "Delete", icon: "🗑️", value: "delete" }
1309
+ * ],
1310
+ * onSelect: (value) => handleAction(value)
1311
+ * });
1312
+ */
1313
+ class ContextMenuControl extends jsgui.Control {
1314
+ constructor(spec = {}) {
1315
+ super({
1316
+ ...spec,
1317
+ tagName: "div",
1318
+ __type_name: "context_menu"
1319
+ });
1320
+
1321
+ this.add_class("jsgui-context-menu");
1322
+
1323
+ this._items = spec.items || [];
1324
+ this._onSelect = spec.onSelect || null;
1325
+ this._visible = false;
1326
+ this._position = { x: 0, y: 0 };
1327
+
1328
+ if (!spec.el) {
1329
+ this.compose();
1330
+ }
1331
+ }
1332
+
1333
+ compose() {
1334
+ for (const item of this._items) {
1335
+ const menuItem = new ContextMenuItemControl({
1336
+ context: this.context,
1337
+ ...item,
1338
+ onSelect: (value) => this._handleSelect(value)
1339
+ });
1340
+ this.add(menuItem);
1341
+ }
1342
+ }
1343
+
1344
+ // Public API - these methods should work pre and post render
1345
+ show(x, y) {
1346
+ this._visible = true;
1347
+ this._position = { x, y };
1348
+ this.remove_class("jsgui-context-menu--hidden");
1349
+ // Position is applied in activate() or via CSS custom properties
1350
+ this._applyPosition();
1351
+ }
1352
+
1353
+ hide() {
1354
+ this._visible = false;
1355
+ this.add_class("jsgui-context-menu--hidden");
1356
+ }
1357
+
1358
+ _applyPosition() {
1359
+ // Use CSS custom properties for positioning (works with jsgui3 abstraction)
1360
+ this.dom.attributes.style = `--menu-x: ${this._position.x}px; --menu-y: ${this._position.y}px;`;
1361
+ }
1362
+
1363
+ _handleSelect(value) {
1364
+ this.hide();
1365
+ if (this._onSelect) {
1366
+ this._onSelect(value);
1367
+ }
1368
+ }
1369
+
1370
+ activate() {
1371
+ if (this.__activated) return;
1372
+ this.__activated = true;
1373
+
1374
+ // Client-side: bind keyboard navigation, click-outside-to-close
1375
+ this._bindKeyboardNav();
1376
+ this._bindClickOutside();
1377
+ }
1378
+
1379
+ _bindKeyboardNav() {
1380
+ // Keyboard handling is inherently client-side
1381
+ if (!this.dom.el) return;
1382
+ this.dom.el.addEventListener("keydown", (e) => {
1383
+ if (e.key === "Escape") this.hide();
1384
+ // ... arrow key navigation
1385
+ });
1386
+ }
1387
+
1388
+ _bindClickOutside() {
1389
+ // Click-outside is inherently client-side
1390
+ document.addEventListener("click", (e) => {
1391
+ if (this._visible && this.dom.el && !this.dom.el.contains(e.target)) {
1392
+ this.hide();
1393
+ }
1394
+ });
1395
+ }
1396
+ }
1397
+
1398
+ module.exports = { ContextMenuControl };
1399
+ ```
1400
+
1401
+ ### Pattern 2: Behavior Mixins
1402
+
1403
+ For behaviors that can apply to multiple controls (drag, resize, sortable):
1404
+
1405
+ ```javascript
1406
+ // src/ui/jsgui-extensions/mixins/DraggableMixin.js
1407
+ "use strict";
1408
+
1409
+ /**
1410
+ * DraggableMixin - Adds drag behavior to any control
1411
+ *
1412
+ * Designed for upstream contribution to jsgui3.
1413
+ * Apply via: Object.assign(MyControl.prototype, DraggableMixin);
1414
+ *
1415
+ * Requirements:
1416
+ * - Control must have this.dom.el after activation
1417
+ * - Control should have a drag handle element (or uses whole control)
1418
+ */
1419
+ const DraggableMixin = {
1420
+ /**
1421
+ * Initialize draggable behavior
1422
+ * Call this in your control's activate() method
1423
+ *
1424
+ * @param {Object} options
1425
+ * @param {string} [options.handleSelector] - CSS selector for drag handle
1426
+ * @param {Function} [options.onDragStart] - Called when drag starts
1427
+ * @param {Function} [options.onDragEnd] - Called when drag ends
1428
+ */
1429
+ initDraggable(options = {}) {
1430
+ if (!this.dom.el) return;
1431
+
1432
+ this._dragOptions = options;
1433
+ this._isDragging = false;
1434
+ this._dragStart = { x: 0, y: 0 };
1435
+ this._posStart = { x: 0, y: 0 };
1436
+
1437
+ const handle = options.handleSelector
1438
+ ? this.dom.el.querySelector(options.handleSelector)
1439
+ : this.dom.el;
1440
+
1441
+ if (handle) {
1442
+ handle.addEventListener("mousedown", (e) => this._onDragMouseDown(e));
1443
+ }
1444
+
1445
+ // These must be on document to track mouse outside element
1446
+ document.addEventListener("mousemove", (e) => this._onDragMouseMove(e));
1447
+ document.addEventListener("mouseup", (e) => this._onDragMouseUp(e));
1448
+ },
1449
+
1450
+ _onDragMouseDown(e) {
1451
+ if (e.button !== 0) return; // Left click only
1452
+
1453
+ this._isDragging = true;
1454
+ this._dragStart = { x: e.clientX, y: e.clientY };
1455
+
1456
+ // Get current position from control state (not DOM)
1457
+ this._posStart = {
1458
+ x: this._position?.x || 0,
1459
+ y: this._position?.y || 0
1460
+ };
1461
+
1462
+ this.add_class("jsgui-dragging");
1463
+
1464
+ if (this._dragOptions.onDragStart) {
1465
+ this._dragOptions.onDragStart();
1466
+ }
1467
+
1468
+ e.preventDefault();
1469
+ },
1470
+
1471
+ _onDragMouseMove(e) {
1472
+ if (!this._isDragging) return;
1473
+
1474
+ const dx = e.clientX - this._dragStart.x;
1475
+ const dy = e.clientY - this._dragStart.y;
1476
+
1477
+ // Update control state
1478
+ this._position = {
1479
+ x: Math.max(0, this._posStart.x + dx),
1480
+ y: Math.max(0, this._posStart.y + dy)
1481
+ };
1482
+
1483
+ // Apply via style attribute (jsgui3 compatible)
1484
+ this._applyDragPosition();
1485
+ },
1486
+
1487
+ _onDragMouseUp(e) {
1488
+ if (!this._isDragging) return;
1489
+
1490
+ this._isDragging = false;
1491
+ this.remove_class("jsgui-dragging");
1492
+
1493
+ if (this._dragOptions.onDragEnd) {
1494
+ this._dragOptions.onDragEnd(this._position);
1495
+ }
1496
+ },
1497
+
1498
+ _applyDragPosition() {
1499
+ // Use style attribute which jsgui3 handles
1500
+ this.dom.attributes.style = `left: ${this._position.x}px; top: ${this._position.y}px;`;
1501
+ // Sync to DOM if rendered
1502
+ if (this.dom.el) {
1503
+ this.dom.el.style.left = `${this._position.x}px`;
1504
+ this.dom.el.style.top = `${this._position.y}px`;
1505
+ }
1506
+ }
1507
+ };
1508
+
1509
+ module.exports = { DraggableMixin };
1510
+ ```
1511
+
1512
+ ### Pattern 3: Context Plugins
1513
+
1514
+ For features that need access to all controls (tooltips, focus management):
1515
+
1516
+ ```javascript
1517
+ // src/ui/jsgui-extensions/plugins/TooltipPlugin.js
1518
+ "use strict";
1519
+
1520
+ /**
1521
+ * TooltipPlugin - Automatic tooltip management for jsgui3 contexts
1522
+ *
1523
+ * Designed for upstream contribution to jsgui3.
1524
+ *
1525
+ * @example
1526
+ * const context = new jsgui.Page_Context();
1527
+ * TooltipPlugin.install(context);
1528
+ *
1529
+ * // Then in any control:
1530
+ * myControl.dom.attributes["data-tooltip"] = "Helpful text";
1531
+ */
1532
+ const TooltipPlugin = {
1533
+ install(context) {
1534
+ // Store plugin state on context
1535
+ context._tooltipPlugin = {
1536
+ activeTooltip: null,
1537
+ tooltipEl: null
1538
+ };
1539
+
1540
+ // Hook into context's activation lifecycle
1541
+ const originalActivate = context.activate?.bind(context) || (() => {});
1542
+ context.activate = function() {
1543
+ originalActivate();
1544
+ TooltipPlugin._initTooltips(context);
1545
+ };
1546
+ },
1547
+
1548
+ _initTooltips(context) {
1549
+ // Create tooltip element once
1550
+ if (!context._tooltipPlugin.tooltipEl) {
1551
+ const tooltip = document.createElement("div");
1552
+ tooltip.className = "jsgui-tooltip jsgui-tooltip--hidden";
1553
+ document.body.appendChild(tooltip);
1554
+ context._tooltipPlugin.tooltipEl = tooltip;
1555
+ }
1556
+
1557
+ // Use event delegation on document
1558
+ document.addEventListener("mouseenter", (e) => {
1559
+ const target = e.target.closest("[data-tooltip]");
1560
+ if (target) {
1561
+ TooltipPlugin._showTooltip(context, target);
1562
+ }
1563
+ }, true);
1564
+
1565
+ document.addEventListener("mouseleave", (e) => {
1566
+ const target = e.target.closest("[data-tooltip]");
1567
+ if (target) {
1568
+ TooltipPlugin._hideTooltip(context);
1569
+ }
1570
+ }, true);
1571
+ },
1572
+
1573
+ _showTooltip(context, target) {
1574
+ const text = target.getAttribute("data-tooltip");
1575
+ const tooltip = context._tooltipPlugin.tooltipEl;
1576
+
1577
+ tooltip.textContent = text;
1578
+ tooltip.classList.remove("jsgui-tooltip--hidden");
1579
+
1580
+ // Position near target
1581
+ const rect = target.getBoundingClientRect();
1582
+ tooltip.style.left = `${rect.left + rect.width / 2}px`;
1583
+ tooltip.style.top = `${rect.bottom + 8}px`;
1584
+ },
1585
+
1586
+ _hideTooltip(context) {
1587
+ const tooltip = context._tooltipPlugin.tooltipEl;
1588
+ tooltip.classList.add("jsgui-tooltip--hidden");
1589
+ }
1590
+ };
1591
+
1592
+ module.exports = { TooltipPlugin };
1593
+ ```
1594
+
1595
+ ### Guidelines for Upstream-Ready Extensions
1596
+
1597
+ 1. **Use jsgui3 APIs wherever possible**
1598
+ - `add_class()` / `remove_class()` instead of `classList`
1599
+ - `this.dom.attributes` instead of `setAttribute()`
1600
+ - `this.add()` for child controls
1601
+
1602
+ 2. **Keep DOM access in `activate()`**
1603
+ - Event listeners: always in `activate()`
1604
+ - Document-level listeners: in `activate()`
1605
+ - Element measurements: only when needed, in methods called post-activation
1606
+
1607
+ 3. **Document the contract**
1608
+ - JSDoc with `@example`
1609
+ - Note any requirements (must call `initX()` in `activate()`)
1610
+ - Explain what's client-only vs isomorphic
1611
+
1612
+ 4. **Use consistent naming**
1613
+ - Classes: `jsgui-*` prefix for CSS
1614
+ - Types: `__type_name` follows existing patterns
1615
+ - Methods: match jsgui3 conventions (`add_class`, not `addClass`)
1616
+
1617
+ 5. **Test isomorphically**
1618
+ - Check script should work (server-side render)
1619
+ - Manual test should work (client-side activation)
1620
+ - Document any client-only features
1621
+
1622
+ 6. **Keep dependencies minimal**
1623
+ - No external libraries in extensions
1624
+ - If you need a utility, write it or reference jsgui3's internals
1625
+
1626
+ ### Filing Upstream
1627
+
1628
+ When an extension is stable and useful:
1629
+
1630
+ 1. Open issue in jsgui3 repo describing the use case
1631
+ 2. Reference this repo's implementation
1632
+ 3. Propose API changes if the extension reveals jsgui3 gaps
1633
+ 4. Be prepared to adapt to jsgui3's code style
1634
+
1635
+ ---
1636
+
1637
+ ## Dashboard Server Performance Patterns
1638
+
1639
+ Dashboards often need to query databases or aggregate data. Poor patterns here cause "waiting for ages" load times.
1640
+
1641
+ ### ✅ 1. Cache Expensive Queries
1642
+
1643
+ **Problem**: Listing databases requires scanning directories and opening each DB to check tables - slow for many DBs.
1644
+
1645
+ **Solution**: Cache the result with a TTL (Time To Live).
1646
+
1647
+ ```javascript
1648
+ // Server-side caching pattern
1649
+ let databaseListCache = { data: null, timestamp: 0 };
1650
+ const DATABASE_CACHE_TTL_MS = 30000; // 30 seconds
1651
+
1652
+ async function listDatabases() {
1653
+ const now = Date.now();
1654
+
1655
+ // Return cached data if still valid
1656
+ if (databaseListCache.data && (now - databaseListCache.timestamp) < DATABASE_CACHE_TTL_MS) {
1657
+ return databaseListCache.data;
1658
+ }
1659
+
1660
+ // Expensive operation: scan directory, check each DB
1661
+ const dataDir = path.resolve(__dirname, "../../../data");
1662
+ const files = fs.readdirSync(dataDir).filter(f => f.endsWith(".db"));
1663
+
1664
+ const databases = [];
1665
+ for (const file of files) {
1666
+ const info = await getBasicDbInfo(path.join(dataDir, file));
1667
+ databases.push(info);
1668
+ }
1669
+
1670
+ // Update cache
1671
+ databaseListCache = { data: databases, timestamp: now };
1672
+ return databases;
1673
+ }
1674
+ ```
1675
+
1676
+ **API Enhancement**: Add `?refresh=true` to force cache refresh:
1677
+
1678
+ ```javascript
1679
+ app.get("/api/databases", async (req, res) => {
1680
+ if (req.query.refresh === "true") {
1681
+ databaseListCache = { data: null, timestamp: 0 }; // Invalidate
1682
+ }
1683
+ const databases = await listDatabases();
1684
+ res.json(databases);
1685
+ });
1686
+ ```
1687
+
1688
+ ### ✅ 2. Optimize Database Info Queries
1689
+
1690
+ **Problem**: Counting all rows in large tables (millions of rows) is slow.
1691
+
1692
+ **Solution**: Use existence checks (`LIMIT 1`) and approximate counts for large DBs.
1693
+
1694
+ ```javascript
1695
+ async function getBasicDbInfo(dbPath) {
1696
+ const stats = fs.statSync(dbPath);
1697
+ const sizeBytes = stats.size;
1698
+
1699
+ // For large DBs (>100MB), use approximate count
1700
+ const isLarge = sizeBytes > 100 * 1024 * 1024;
1701
+
1702
+ const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY);
1703
+
1704
+ try {
1705
+ // Quick existence check - faster than COUNT(*)
1706
+ const hasGazetteerTables = await new Promise((resolve) => {
1707
+ db.get(
1708
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='places' LIMIT 1",
1709
+ (err, row) => resolve(!err && !!row)
1710
+ );
1711
+ });
1712
+
1713
+ let placeCount = 0;
1714
+ if (hasGazetteerTables) {
1715
+ if (isLarge) {
1716
+ // Approximate count for large DBs - instant
1717
+ const approx = await dbGet(db,
1718
+ "SELECT MAX(rowid) as approx FROM places");
1719
+ placeCount = approx?.approx || 0;
1720
+ } else {
1721
+ // Exact count for small DBs
1722
+ const exact = await dbGet(db, "SELECT COUNT(*) as cnt FROM places");
1723
+ placeCount = exact?.cnt || 0;
1724
+ }
1725
+ }
1726
+
1727
+ return {
1728
+ name: path.basename(dbPath),
1729
+ path: dbPath,
1730
+ sizeBytes,
1731
+ hasGazetteerTables,
1732
+ placeCount,
1733
+ isApproximate: isLarge
1734
+ };
1735
+ } finally {
1736
+ db.close();
1737
+ }
1738
+ }
1739
+ ```
1740
+
1741
+ ### ✅ 3. Timeout Guards for DB Operations
1742
+
1743
+ **Problem**: Corrupt or locked DBs can hang queries indefinitely.
1744
+
1745
+ **Solution**: Wrap in Promise.race with timeout.
1746
+
1747
+ ```javascript
1748
+ async function safeDbQuery(db, sql, timeout = 5000) {
1749
+ return Promise.race([
1750
+ new Promise((resolve, reject) => {
1751
+ db.get(sql, (err, row) => err ? reject(err) : resolve(row));
1752
+ }),
1753
+ new Promise((_, reject) =>
1754
+ setTimeout(() => reject(new Error("Query timeout")), timeout)
1755
+ )
1756
+ ]);
1757
+ }
1758
+
1759
+ // Usage
1760
+ try {
1761
+ const result = await safeDbQuery(db, "SELECT COUNT(*) FROM large_table", 3000);
1762
+ } catch (err) {
1763
+ if (err.message === "Query timeout") {
1764
+ return { count: "unknown", timedOut: true };
1765
+ }
1766
+ throw err;
1767
+ }
1768
+ ```
1769
+
1770
+ ### ✅ 4. Invalidate Cache on Mutations
1771
+
1772
+ When creating/deleting databases, invalidate the cache:
1773
+
1774
+ ```javascript
1775
+ app.post("/api/databases", async (req, res) => {
1776
+ // ... create database logic ...
1777
+
1778
+ // Invalidate cache so next list reflects new DB
1779
+ databaseListCache = { data: null, timestamp: 0 };
1780
+
1781
+ res.json({ success: true, database: newDb });
1782
+ });
1783
+ ```
1784
+
1785
+ ### ✅ 5. Show All Items, Style Differently
1786
+
1787
+ **Problem**: Users may want to add features to items that don't have them yet (e.g., add gazetteer tables to a non-gazetteer database).
1788
+
1789
+ **Solution**: Show all items, but style non-applicable ones differently.
1790
+
1791
+ ```javascript
1792
+ // In DatabaseItem control
1793
+ compose() {
1794
+ const hasGazetteer = this.database.hasGazetteerTables;
1795
+
1796
+ if (!hasGazetteer) {
1797
+ this.add_class("non-gazetteer"); // Grayed out
1798
+
1799
+ // Add badge
1800
+ const badge = new jsgui.Control({ context: this.context, tagName: "span" });
1801
+ badge.add_class("init-gazetteer-badge");
1802
+ badge.add("📦 No Gazetteer");
1803
+ this.add(badge);
1804
+
1805
+ // Add action button
1806
+ const initBtn = new jsgui.Control({ context: this.context, tagName: "button" });
1807
+ initBtn.add_class("init-gazetteer-btn");
1808
+ initBtn.add("Init Gazetteer");
1809
+ this.add(initBtn);
1810
+ }
1811
+ }
1812
+ ```
1813
+
1814
+ **CSS**:
1815
+ ```css
1816
+ .non-gazetteer {
1817
+ opacity: 0.7;
1818
+ filter: grayscale(30%);
1819
+ }
1820
+
1821
+ .init-gazetteer-badge {
1822
+ background: linear-gradient(135deg, #1e40af, #3b82f6);
1823
+ color: white;
1824
+ padding: 2px 8px;
1825
+ border-radius: 4px;
1826
+ font-size: 0.75rem;
1827
+ }
1828
+
1829
+ .init-gazetteer-btn {
1830
+ background: var(--theme-surface, #2d2d2d);
1831
+ color: var(--theme-text-secondary, #888);
1832
+ border: 1px solid var(--theme-border, #3d3d3d);
1833
+ padding: 4px 12px;
1834
+ border-radius: 4px;
1835
+ cursor: pointer;
1836
+ }
1837
+ ```
1838
+
1839
+ ---
1840
+
1841
+ ## Anti-Patterns to Avoid
1842
+
1843
+ ### ❌ 1. Inline Compositions in App Controls
1844
+
1845
+ **Bad**: Building complex structures inline in `_build*` methods.
1846
+
1847
+ ```javascript
1848
+ // ❌ Avoid: 50+ line inline composition
1849
+ _buildToolbar() {
1850
+ const toolbar = new jsgui.Control({ context: this.context, tagName: "div" });
1851
+ toolbar.add_class("toolbar");
1852
+
1853
+ const statusCard = new jsgui.Control({ context: this.context, tagName: "div" });
1854
+ statusCard.add_class("toolbar__status");
1855
+ // ... 40 more lines of inline building
1856
+
1857
+ return toolbar;
1858
+ }
1859
+ ```
1860
+
1861
+ **Good**: Extract to a dedicated control.
1862
+
1863
+ ```javascript
1864
+ // ✅ Better: Use a dedicated control
1865
+ _buildToolbar() {
1866
+ return new ToolbarControl({
1867
+ context: this.context,
1868
+ status: this.status,
1869
+ timestamp: this.timestamp
1870
+ });
1871
+ }
1872
+ ```
1873
+
1874
+ ### ❌ 2. Calling compose() in Base Class Constructor
1875
+
1876
+ **Bad**: Base class calls `compose()`, breaking child property initialization.
1877
+
1878
+ ```javascript
1879
+ // ❌ Avoid
1880
+ class BaseControl extends jsgui.Control {
1881
+ constructor(spec) {
1882
+ super(spec);
1883
+ this.compose(); // Child properties not set yet!
1884
+ }
1885
+ }
1886
+ ```
1887
+
1888
+ **Good**: Let child classes call `compose()` after setting their properties.
1889
+
1890
+ ```javascript
1891
+ // ✅ Better
1892
+ class BaseControl extends jsgui.Control {
1893
+ constructor(spec) {
1894
+ super(spec);
1895
+ // Don't compose here
1896
+ }
1897
+ }
1898
+
1899
+ class ChildControl extends BaseControl {
1900
+ constructor(spec) {
1901
+ super(spec);
1902
+ this.myProp = spec.myProp; // Set first
1903
+ if (!spec.el) this.compose(); // Then compose
1904
+ }
1905
+ }
1906
+ ```
1907
+
1908
+ ### ✅ 3. Raw Strings Work Fine
1909
+
1910
+ jsgui3 automatically handles raw strings - you don't need to wrap them:
1911
+
1912
+ ```javascript
1913
+ // ✅ This works - jsgui3 auto-wraps strings
1914
+ element.add("Hello World"); // Renders correctly!
1915
+
1916
+ // ✅ Also fine - explicit StringControl
1917
+ element.add(new StringControl({ context: this.context, text: "Hello World" }));
1918
+ ```
1919
+
1920
+ **Note**: Some older code in this repo uses explicit `String_Control` everywhere. That's fine but not required.
1921
+
1922
+ ### ❌ 4. Forgetting to Pass Context
1923
+
1924
+ **Bad**: Creating child controls without context.
1925
+
1926
+ ```javascript
1927
+ // ❌ Avoid
1928
+ const child = new MyChildControl({ data: this.data }); // Missing context!
1929
+ ```
1930
+
1931
+ **Good**: Always pass `this.context` to child controls.
1932
+
1933
+ ```javascript
1934
+ // ✅ Better
1935
+ const child = new MyChildControl({ context: this.context, data: this.data });
1936
+ ```
1937
+
1938
+ ### ❌ 5. God Controls
1939
+
1940
+ **Bad**: One massive control that handles everything.
1941
+
1942
+ ```javascript
1943
+ // ❌ Avoid: 500+ line control with 15 different view types
1944
+ class EverythingControl extends jsgui.Control {
1945
+ compose() {
1946
+ // Hundreds of lines mixing unrelated concerns
1947
+ }
1948
+ }
1949
+ ```
1950
+
1951
+ **Good**: Small, focused controls composed together.
1952
+
1953
+ ```javascript
1954
+ // ✅ Better: Focused controls
1955
+ // - HeaderControl (~50 lines)
1956
+ // - SearchFormControl (~40 lines)
1957
+ // - ResultsListControl (~60 lines)
1958
+ // - PaginationControl (~40 lines)
1959
+ // - AppControl composes them all (~80 lines)
1960
+ ```
1961
+
1962
+ ### ❌ 6. Missing Check Scripts
1963
+
1964
+ **Bad**: No verification for new controls.
1965
+
1966
+ **Good**: Every control has a check script that verifies rendering.
1967
+
1968
+ ### ❌ 7. Interactive Components as Plain JS/CSS (Context Menu Case Study)
1969
+
1970
+ **Bad**: Building interactive components (context menus, tooltips, modals) as plain JavaScript functions with CSS.
1971
+
1972
+ ```javascript
1973
+ // ❌ Avoid: Context menu as plain JS
1974
+ function showColumnContextMenu(x, y) {
1975
+ const menu = document.querySelector("[data-context-menu='columns']");
1976
+ menu.style.display = "block";
1977
+ menu.style.left = x + "px";
1978
+ menu.style.top = y + "px";
1979
+
1980
+ // 40 more lines of positioning, event handling, keyboard nav...
1981
+ document.addEventListener("click", closeOnClickOutside);
1982
+ document.addEventListener("keydown", closeOnEscape);
1983
+ }
1984
+
1985
+ function hideColumnContextMenu() {
1986
+ const menu = document.querySelector("[data-context-menu='columns']");
1987
+ menu.style.display = "none";
1988
+ }
1989
+ ```
1990
+
1991
+ **Why this is wrong**:
1992
+ - Not testable with check scripts
1993
+ - Event handlers scattered across global scope
1994
+ - Can't reuse for other context menus (row actions, toolbar options)
1995
+ - No clear ownership of state (open/closed)
1996
+ - `js-scan` can't track dependencies
1997
+
1998
+ **Good**: Extract to a dedicated `ContextMenuControl`.
1999
+
2000
+ ```javascript
2001
+ // ✅ Better: Dedicated control class
2002
+ // src/ui/controls/ContextMenuControl.js
2003
+
2004
+ class ContextMenuControl extends jsgui.Control {
2005
+ constructor(spec = {}) {
2006
+ super({ ...spec, tagName: "div", __type_name: "context_menu" });
2007
+ this.add_class("context-menu");
2008
+ this.items = spec.items || [];
2009
+ this.onSelect = spec.onSelect || (() => {});
2010
+ if (!spec.el) this.compose();
2011
+ }
2012
+
2013
+ compose() {
2014
+ for (const item of this.items) {
2015
+ const menuItem = new ContextMenuItemControl({
2016
+ context: this.context,
2017
+ label: item.label,
2018
+ icon: item.icon, // 🔍 for search, ⚙️ for settings, etc.
2019
+ checked: item.checked,
2020
+ value: item.value
2021
+ });
2022
+ this.add(menuItem);
2023
+ }
2024
+ }
2025
+
2026
+ activate() {
2027
+ if (this.__active) return;
2028
+ this.__active = true;
2029
+ this._bindClickOutside();
2030
+ this._bindKeyboardNav();
2031
+ this._bindItemSelection();
2032
+ }
2033
+
2034
+ show(x, y) {
2035
+ const el = this.dom?.el;
2036
+ if (!el) return;
2037
+ el.style.display = "block";
2038
+ this._positionAt(x, y);
2039
+ }
2040
+
2041
+ hide() {
2042
+ const el = this.dom?.el;
2043
+ if (el) el.style.display = "none";
2044
+ }
2045
+ }
2046
+ ```
2047
+
2048
+ **Benefits of extraction**:
2049
+ - ✅ Testable: Check script verifies menu renders with correct items
2050
+ - ✅ Reusable: Same control for column options, row actions, any dropdown
2051
+ - ✅ Maintainable: Event handlers live with the component
2052
+ - ✅ Discoverable: `js-scan --what-imports ContextMenuControl.js` shows all usages
2053
+ - ✅ Type-safe: Props documented in JSDoc
2054
+
2055
+ ### ❌ 8. Missing Visual Affordances (No Icons)
2056
+
2057
+ **Bad**: Interactive elements with text-only labels that users can't quickly scan.
2058
+
2059
+ ```javascript
2060
+ // ❌ Avoid: No visual cue for action type
2061
+ const button = new jsgui.Control({ context: this.context, tagName: "button" });
2062
+ button.add("Options"); // User must read to understand
2063
+ ```
2064
+
2065
+ **Good**: Use emoji icons for instant visual recognition.
2066
+
2067
+ ```javascript
2068
+ // ✅ Better: Emoji provides instant recognition
2069
+ const button = new jsgui.Control({ context: this.context, tagName: "button" });
2070
+ button.add("⚙️ Options"); // Gear = settings, instantly clear
2071
+
2072
+ // Or with dedicated icon element
2073
+ const icon = new jsgui.Control({ context: this.context, tagName: "span" });
2074
+ icon.add_class("btn__icon");
2075
+ icon.add("🔍");
2076
+ button.add(icon);
2077
+ button.add(" Search");
2078
+ ```
2079
+
2080
+ **Standard emoji vocabulary for UI**:
2081
+
2082
+ | Action | Emoji | Notes |
2083
+ |--------|-------|-------|
2084
+ | Search | 🔍 | Magnifying glass - universal |
2085
+ | Settings/Options | ⚙️ | Gear - configuration |
2086
+ | Add/Create | ➕ | Plus sign |
2087
+ | Delete | 🗑️ | Trash can |
2088
+ | Edit | ✏️ | Pencil |
2089
+ | Refresh | 🔄 | Circular arrows |
2090
+ | Sort asc | ▲ | Triangle up |
2091
+ | Sort desc | ▼ | Triangle down |
2092
+ | Menu | ☰ | Hamburger |
2093
+ | More options | ⋮ | Kebab (vertical dots) |
2094
+ | Close | ✕ | X mark |
2095
+ | Success | ✅ | Check mark |
2096
+ | Error | ❌ | X in circle |
2097
+ | Warning | ⚠️ | Triangle alert |
2098
+ | Info | ℹ️ | Information |
2099
+ | Folder | 📁 | Directory |
2100
+ | File | 📄 | Document |
2101
+
2102
+ ### ❌ 9. Shadowing Reserved Property Names (CRITICAL: `this.content`)
2103
+
2104
+ **⚠️ CRITICAL BUG**: Never use `this.content` as a property name in jsgui3 controls!
2105
+
2106
+ **Bad**: Using `content` as a property name shadows the base class Collection.
2107
+
2108
+ ```javascript
2109
+ // ❌ CRITICAL BUG - DO NOT DO THIS
2110
+ class TwoColumnLayout extends jsgui.Control {
2111
+ constructor(spec) {
2112
+ super(spec);
2113
+ this.sidebar = null;
2114
+ this.content = null; // 💥 SHADOWS jsgui.Control.content (a Collection)!
2115
+ if (!spec.el) this.compose();
2116
+ }
2117
+
2118
+ compose() {
2119
+ // This will CRASH because this.add() tries to use this.content
2120
+ // which is now null instead of the Collection!
2121
+ const container = new jsgui.Control({ ... });
2122
+ this.add(container); // 💥 TypeError: Cannot read property 'push' of null
2123
+ }
2124
+ }
2125
+ ```
2126
+
2127
+ **Why this breaks**: In jsgui3, `Control.content` is a **Collection** that holds child controls. When you call `this.add(child)`, it internally calls `this.content.push(child)`. If you shadow `this.content` with `null` or any non-Collection value, `this.add()` crashes.
2128
+
2129
+ **Good**: Use a different property name.
2130
+
2131
+ ```javascript
2132
+ // ✅ CORRECT - use a unique property name
2133
+ class TwoColumnLayout extends jsgui.Control {
2134
+ constructor(spec) {
2135
+ super(spec);
2136
+ this.sidebar = null;
2137
+ this.contentArea = null; // ✅ Unique name, doesn't shadow base class
2138
+ if (!spec.el) this.compose();
2139
+ }
2140
+
2141
+ compose() {
2142
+ const container = new jsgui.Control({ ... });
2143
+ this.add(container); // ✅ Works - this.content is still the Collection
2144
+
2145
+ this.contentArea = new jsgui.Control({ context: this.context, tagName: 'div' });
2146
+ this.contentArea.add_class('content-area');
2147
+ container.add(this.contentArea);
2148
+ }
2149
+
2150
+ addContent(control) {
2151
+ if (this.contentArea) {
2152
+ this.contentArea.add(control); // ✅ Uses our custom property
2153
+ }
2154
+ }
2155
+ }
2156
+ ```
2157
+
2158
+ **Reserved property names to avoid**:
2159
+ - `content` - The Collection holding child controls
2160
+ - `context` - The jsgui context object
2161
+ - `dom` - DOM element references
2162
+ - `__type_name` - Internal type identifier
2163
+
2164
+ **How to spot this bug**: If you see errors like:
2165
+ - `TypeError: Cannot read property 'push' of null`
2166
+ - `TypeError: this.content.push is not a function`
2167
+ - Controls don't render any children despite calling `this.add()`
2168
+
2169
+ Check if you've accidentally shadowed `this.content` in your constructor.
2170
+
2171
+ ### ❌ 10. Direct DOM Manipulation Instead of jsgui3 Methods
2172
+
2173
+ **Bad**: Using `classList.add/remove` or `this.dom.el.style` directly when jsgui3 methods exist.
2174
+
2175
+ ```javascript
2176
+ // ❌ Avoid: Direct DOM manipulation
2177
+ setVisible(visible) {
2178
+ if (this.dom.el) {
2179
+ if (visible) {
2180
+ this.dom.el.classList.remove("hidden");
2181
+ } else {
2182
+ this.dom.el.classList.add("hidden");
2183
+ }
2184
+ }
2185
+ }
2186
+ ```
2187
+
2188
+ **Good**: Use jsgui3's `add_class` and `remove_class` methods - they work whether the control is in the DOM or not.
2189
+
2190
+ ```javascript
2191
+ // ✅ Correct: Use jsgui3 methods
2192
+ setVisible(visible) {
2193
+ if (visible) {
2194
+ this.remove_class("hidden");
2195
+ } else {
2196
+ this.add_class("hidden");
2197
+ }
2198
+ }
2199
+ ```
2200
+
2201
+ **Why jsgui3 methods are better**:
2202
+ - **Work pre-render**: Classes are tracked internally and applied when HTML is generated
2203
+ - **Work post-render**: If `this.dom.el` exists, jsgui3 syncs to the DOM automatically
2204
+ - **Consistent API**: Same code works on server and client
2205
+ - **Maintainable**: State stays in the control, not scattered in DOM
2206
+
2207
+ **When direct DOM manipulation IS appropriate**:
2208
+ - **Client-side activation**: Setting up event listeners in `activate()`
2209
+ - **Dynamic innerHTML updates**: When replacing content after initial render (see [Dynamic Control Updates](#dynamic-control-updates))
2210
+ - **Scroll position**: `this.dom.el.scrollTop = this.dom.el.scrollHeight`
2211
+ - **Focus management**: `this.dom.el.focus()`
2212
+ - **Measurements**: Reading `offsetWidth`, `getBoundingClientRect()`, etc.
2213
+
2214
+ **If jsgui3 methods don't work as expected** (e.g., `add_class` doesn't update the DOM), **report it as a bug** in jsgui3. Don't work around it with direct DOM manipulation unless absolutely necessary, and document the workaround with a `// WORKAROUND: jsgui3 bug - <description>` comment.
2215
+
2216
+ ---
2217
+
2218
+ ## Quick Reference
2219
+
2220
+ ### Creating a New Control
2221
+
2222
+ ```javascript
2223
+ "use strict";
2224
+
2225
+ const jsgui = require("jsgui3-html");
2226
+ const StringControl = jsgui.String_Control;
2227
+
2228
+ class MyControl extends jsgui.Control {
2229
+ constructor(spec = {}) {
2230
+ super({
2231
+ ...spec,
2232
+ tagName: "div",
2233
+ __type_name: "my_control"
2234
+ });
2235
+
2236
+ this.add_class("my-control");
2237
+
2238
+ // 1. Store state from spec
2239
+ this.title = spec.title || "Default";
2240
+
2241
+ // 2. Compose after state is set
2242
+ if (!spec.el) {
2243
+ this.compose();
2244
+ }
2245
+ }
2246
+
2247
+ compose() {
2248
+ // 3. Build child elements
2249
+ const heading = new jsgui.Control({ context: this.context, tagName: "h2" });
2250
+ heading.add(new StringControl({ context: this.context, text: this.title }));
2251
+ this.add(heading);
2252
+ }
2253
+ }
2254
+
2255
+ module.exports = { MyControl };
2256
+ ```
2257
+
2258
+ ### Rendering to HTML
2259
+
2260
+ ```javascript
2261
+ const jsgui = require("jsgui3-html");
2262
+ const { MyControl } = require("./MyControl");
2263
+
2264
+ const context = new jsgui.Page_Context();
2265
+ const control = new MyControl({ context, title: "Hello" });
2266
+ const html = control.all_html_render();
2267
+ // → '<div class="my-control"><h2>Hello</h2></div>'
2268
+ ```
2269
+
2270
+ ### Common Element Patterns
2271
+
2272
+ ```javascript
2273
+ // Link
2274
+ const link = new jsgui.Control({ context: this.context, tagName: "a" });
2275
+ link.dom.attributes.href = "/path";
2276
+ link.add(new StringControl({ context: this.context, text: "Click me" }));
2277
+
2278
+ // Button
2279
+ const btn = new jsgui.Control({ context: this.context, tagName: "button" });
2280
+ btn.dom.attributes.type = "submit";
2281
+ btn.add_class("btn");
2282
+ btn.add(new StringControl({ context: this.context, text: "Submit" }));
2283
+
2284
+ // Input
2285
+ const input = new jsgui.Control({ context: this.context, tagName: "input" });
2286
+ input.dom.attributes.type = "text";
2287
+ input.dom.attributes.name = "query";
2288
+ input.dom.attributes.placeholder = "Search...";
2289
+ input.dom.attributes.value = this.value;
2290
+
2291
+ // Image
2292
+ const img = new jsgui.Control({ context: this.context, tagName: "img" });
2293
+ img.dom.attributes.src = "/image.png";
2294
+ img.dom.attributes.alt = "Description";
2295
+ ```
2296
+
2297
+ ### BEM Naming Convention
2298
+
2299
+ ```javascript
2300
+ // Block
2301
+ control.add_class("toolbar");
2302
+
2303
+ // Element
2304
+ control.add_class("toolbar__button");
2305
+ control.add_class("toolbar__status");
2306
+
2307
+ // Modifier
2308
+ control.add_class("toolbar--compact");
2309
+ control.add_class("toolbar__button--active");
2310
+ ```
2311
+
2312
+ ---
2313
+
2314
+ ## Summary
2315
+
2316
+ 1. **jsgui3-html is isomorphic** - same code works on server (SSR) and client (bundled)
2317
+ 2. **jsgui3-client extends jsgui3-html** - use it only for client-specific features like `activate()`
2318
+ 3. **Controls are classes** that extend `jsgui.Control` and build DOM programmatically
2319
+ 4. **Context flows down** - always pass `context` to child controls
2320
+ 5. **Text auto-wraps** - `.add("text")` works directly, `String_Control` is optional
2321
+ 6. **Compose timing matters** - set properties before calling `compose()`
2322
+ 7. **Activation via `spec.el`** - pass existing DOM element to skip `compose()` and bind events (aka "hydration" in other frameworks)
2323
+ 8. **Extract reusable pieces** - prefer small, focused controls over inline compositions
2324
+ 9. **Verify with check scripts** - every control should have a verification script
2325
+ 10. **Use BEM naming** - consistent CSS class naming improves maintainability
2326
+
2327
+ ---
2328
+
2329
+ ## Client-Side Activation Flow (CRITICAL)
2330
+
2331
+ > ⚠️ **This section documents hard-won knowledge from debugging jsgui3 activation issues. Future agents: READ THIS BEFORE working on client-side jsgui3 code.**
2332
+
2333
+ ### The Problem
2334
+
2335
+ When rendering jsgui3 controls on the client side (e.g., in Electron or browser), calling `app.activate()` after `innerHTML = html` often **fails silently**. Event handlers don't bind, and `this.dom.el` is `null` in controls.
2336
+
2337
+ ### Root Cause
2338
+
2339
+ jsgui3 requires a specific activation sequence:
2340
+
2341
+ 1. Controls must be **registered** in `context.map_controls`
2342
+ 2. Control instances must be **linked** to their DOM elements (`this.dom.el`)
2343
+ 3. Only then will `activate()` properly bind events
2344
+
2345
+ The `all_html_render()` method generates HTML with `data-jsgui-id` attributes, but it does NOT automatically link controls to DOM elements.
2346
+
2347
+ ### The Solution: Proper Activation Sequence
2348
+
2349
+ ```javascript
2350
+ // ❌ WRONG - This will NOT work properly
2351
+ const html = app.all_html_render();
2352
+ rootEl.innerHTML = html;
2353
+ app.activate(); // DOM refs are null!
2354
+
2355
+ // ✅ CORRECT - Proper activation sequence
2356
+ const html = app.all_html_render();
2357
+ rootEl.innerHTML = html;
2358
+
2359
+ // Step 1: Register ALL controls in context.map_controls
2360
+ app.register_this_and_subcontrols();
2361
+
2362
+ // Step 2: Find and link the root control's DOM element
2363
+ const appEl = rootEl.querySelector('[data-jsgui-id="' + app._id() + '"]');
2364
+ app.dom.el = appEl;
2365
+
2366
+ // Step 3: Recursively link ALL child controls to their DOM elements
2367
+ app.rec_desc_ensure_ctrl_el_refs(appEl);
2368
+
2369
+ // Step 4: NOW activate (event binding will work)
2370
+ app.activate();
2371
+ ```
2372
+
2373
+ ### Key Methods Explained
2374
+
2375
+ | Method | What it does | Why it's needed |
2376
+ |--------|-------------|------------------|
2377
+ | `register_this_and_subcontrols()` | Adds control + children to `context.map_controls` | Required for `activate()` to find controls |
2378
+ | `rec_desc_ensure_ctrl_el_refs(el)` | Recursively links `ctrl.dom.el` to DOM elements by `data-jsgui-id` | Without this, `this.dom.el` is null |
2379
+ | `_id()` | Returns the control's jsgui id (e.g., `"zserver_app_0"`) | Used to find DOM element |
2380
+
2381
+ ### Dynamic Control Updates
2382
+
2383
+ When creating new controls after initial render (e.g., populating a list):
2384
+
2385
+ ```javascript
2386
+ setItems(items) {
2387
+ this._items = items;
2388
+
2389
+ if (this.dom.el) {
2390
+ // Already rendered - must update DOM properly
2391
+ this.dom.el.innerHTML = '';
2392
+
2393
+ items.forEach(item => {
2394
+ const ctrl = new ItemControl({ context: this.context, item });
2395
+
2396
+ // Step 1: Register new control
2397
+ ctrl.register_this_and_subcontrols();
2398
+
2399
+ // Step 2: Render and insert HTML
2400
+ const itemHtml = ctrl.all_html_render();
2401
+ this.dom.el.insertAdjacentHTML('beforeend', itemHtml);
2402
+
2403
+ // Step 3: Find and link DOM element
2404
+ const itemEl = this.dom.el.querySelector('[data-jsgui-id="' + ctrl._id() + '"]');
2405
+ ctrl.dom.el = itemEl;
2406
+ this.context.map_els[ctrl._id()] = itemEl;
2407
+
2408
+ // Step 4: Link children
2409
+ if (ctrl.rec_desc_ensure_ctrl_el_refs) {
2410
+ ctrl.rec_desc_ensure_ctrl_el_refs(itemEl);
2411
+ }
2412
+
2413
+ // Step 5: Activate
2414
+ ctrl.activate();
2415
+ });
2416
+ }
2417
+ }
2418
+ ```
2419
+
2420
+ ### How jsgui3 Finds DOM Elements (Internal Detail)
2421
+
2422
+ Inside `activate()` and `pre_activate()`, jsgui3 tries to find DOM elements via:
2423
+
2424
+ ```javascript
2425
+ let found_el = this.context.get_ctrl_el(this)
2426
+ || this.context.map_els[this._id()]
2427
+ || document.querySelectorAll('[data-jsgui-id="' + this._id() + '"]')[0];
2428
+ ```
2429
+
2430
+ The DOM query fallback exists but is unreliable for nested controls. Always use `rec_desc_ensure_ctrl_el_refs()` for proper initialization.
2431
+
2432
+ ### Complete Working Example (Electron App)
2433
+
2434
+ ```javascript
2435
+ // renderer.src.js - Electron renderer process entry
2436
+ "use strict";
2437
+
2438
+ // Fix for jsgui3-client bug
2439
+ if (typeof window !== 'undefined') {
2440
+ window.page_context = null;
2441
+ }
2442
+
2443
+ const jsgui = require("jsgui3-client");
2444
+ const { MyAppControl } = require("./controls");
2445
+
2446
+ document.addEventListener("DOMContentLoaded", async () => {
2447
+ const context = new jsgui.Client_Page_Context();
2448
+
2449
+ const app = new MyAppControl({
2450
+ context,
2451
+ api: window.electronAPI
2452
+ });
2453
+
2454
+ const rootEl = document.getElementById("app-root");
2455
+ const html = app.all_html_render();
2456
+ rootEl.innerHTML = html;
2457
+
2458
+ // CRITICAL: Proper activation sequence
2459
+ app.register_this_and_subcontrols();
2460
+ console.log("Controls registered:", Object.keys(context.map_controls).length);
2461
+
2462
+ const appEl = rootEl.querySelector('[data-jsgui-id="' + app._id() + '"]');
2463
+ if (appEl) {
2464
+ app.dom.el = appEl;
2465
+ app.rec_desc_ensure_ctrl_el_refs(appEl);
2466
+ console.log("DOM elements linked:", Object.keys(context.map_els).length);
2467
+ }
2468
+
2469
+ app.activate();
2470
+ await app.init();
2471
+ });
2472
+ ```
2473
+
2474
+ ### Debugging Checklist
2475
+
2476
+ If controls aren't working after render:
2477
+
2478
+ - [ ] Did you call `register_this_and_subcontrols()` before `activate()`?
2479
+ - [ ] Did you link the root element: `app.dom.el = appEl`?
2480
+ - [ ] Did you call `rec_desc_ensure_ctrl_el_refs(appEl)`?
2481
+ - [ ] Check `console.log(Object.keys(context.map_controls).length)` - should match control count
2482
+ - [ ] Check `console.log(Object.keys(context.map_els).length)` - should be close to control count
2483
+ - [ ] In your control's method, check `console.log("DOM el:", this.dom.el)` - should NOT be null
2484
+
2485
+ ---
2486
+
2487
+ ## Troubleshooting
2488
+
2489
+ ### Known Issues
2490
+
2491
+ #### Controls don't respond to clicks / `this.dom.el` is null
2492
+
2493
+ **Symptom**: Controls render visually but event handlers don't fire. `this.dom.el` is `null` or `undefined` inside control methods.
2494
+
2495
+ **Cause**: Missing activation sequence steps. The `all_html_render()` method generates HTML but does NOT link Control instances to DOM elements.
2496
+
2497
+ **Solution**: Follow the complete activation sequence in the [Client-Side Activation Flow](#client-side-activation-flow-critical) section above.
2498
+
2499
+ ```javascript
2500
+ // ALWAYS do all 4 steps:
2501
+ app.register_this_and_subcontrols(); // 1. Register
2502
+ app.dom.el = rootEl.querySelector('[data-jsgui-id="' + app._id() + '"]'); // 2. Link root
2503
+ app.rec_desc_ensure_ctrl_el_refs(app.dom.el); // 3. Link children
2504
+ app.activate(); // 4. Activate
2505
+ ```
2506
+
2507
+ #### jsgui3-client: `page_context` ReferenceError
2508
+
2509
+ **Symptom**: `ReferenceError: page_context is not defined` during client-side activation.
2510
+ **Cause**: A bug in `jsgui3-client` (v0.0.121+) where `page_context` is assigned without declaration in the `activate` function.
2511
+ **Workaround**: Define `page_context` globally before loading `jsgui3-client`.
2512
+
2513
+ ```javascript
2514
+ // In your client entry point (e.g., renderer.src.js)
2515
+ if (typeof window !== 'undefined') {
2516
+ window.page_context = null; // Fix for jsgui3-client bug
2517
+ }
2518
+ const jsgui = require("jsgui3-client");
2519
+ ```
2520
+
2521
+ #### "Missing context.map_Controls for type X"
2522
+
2523
+ **Symptom**: Console logs `"Missing context.map_Controls for type my_control, using generic Control"`
2524
+
2525
+ **Cause**: jsgui3's internal activation tries to reconstruct controls from DOM but can't find the custom control types. This is informational - your controls are still registered via `register_this_and_subcontrols()`.
2526
+
2527
+ **Solution**: This warning is harmless if you're using the proper activation sequence. The custom controls are already instantiated - jsgui3 is just warning that it can't re-instantiate them from DOM alone.