jsgui3-server 0.0.144 → 0.0.145

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 (47) hide show
  1. package/docs/jsgui3-html-improvement-ideas.md +162 -0
  2. package/docs/jsgui3-html-improvement-ideas.svg +151 -0
  3. package/examples/controls/14d) window, canvas globe/EarthGlobeRenderer.js +19 -14
  4. package/examples/controls/14d) window, canvas globe/pipeline/TransformStage.js +5 -5
  5. package/examples/jsgui3-html/01) mvvm-counter/client.js +648 -0
  6. package/examples/jsgui3-html/01) mvvm-counter/server.js +21 -0
  7. package/examples/jsgui3-html/02) date-transform/client.js +764 -0
  8. package/examples/jsgui3-html/02) date-transform/server.js +21 -0
  9. package/examples/jsgui3-html/03) form-validation/client.js +1045 -0
  10. package/examples/jsgui3-html/03) form-validation/server.js +21 -0
  11. package/examples/jsgui3-html/04) data-grid/client.js +738 -0
  12. package/examples/jsgui3-html/04) data-grid/server.js +21 -0
  13. package/examples/jsgui3-html/05) master-detail/client.js +649 -0
  14. package/examples/jsgui3-html/05) master-detail/server.js +21 -0
  15. package/examples/jsgui3-html/06) theming/client.js +514 -0
  16. package/examples/jsgui3-html/06) theming/server.js +21 -0
  17. package/examples/jsgui3-html/07) mixins/client.js +465 -0
  18. package/examples/jsgui3-html/07) mixins/server.js +21 -0
  19. package/examples/jsgui3-html/08) router/client.js +372 -0
  20. package/examples/jsgui3-html/08) router/server.js +21 -0
  21. package/examples/jsgui3-html/09) resource-transform/client.js +692 -0
  22. package/examples/jsgui3-html/09) resource-transform/server.js +21 -0
  23. package/examples/jsgui3-html/10) binding-debugger/client.js +810 -0
  24. package/examples/jsgui3-html/10) binding-debugger/server.js +21 -0
  25. package/examples/jsgui3-html/README.md +48 -0
  26. package/http/responders/static/Static_Route_HTTP_Responder.js +25 -20
  27. package/package.json +3 -3
  28. package/publishers/http-webpageorsite-publisher.js +3 -1
  29. package/serve-factory.js +12 -5
  30. package/server.js +103 -85
  31. package/tests/README.md +7 -0
  32. package/tests/end-to-end.test.js +336 -365
  33. package/tests/examples-controls.e2e.test.js +13 -1
  34. package/tests/fixtures/end-to-end-client.js +54 -0
  35. package/tests/fixtures/jsgui3-html/binding_debugger_expectations.json +15 -0
  36. package/tests/fixtures/jsgui3-html/counter_expectations.json +31 -0
  37. package/tests/fixtures/jsgui3-html/data_grid_expectations.json +26 -0
  38. package/tests/fixtures/jsgui3-html/date_transform_expectations.json +26 -0
  39. package/tests/fixtures/jsgui3-html/form_validation_expectations.json +27 -0
  40. package/tests/fixtures/jsgui3-html/master_detail_expectations.json +15 -0
  41. package/tests/fixtures/jsgui3-html/mixins_expectations.json +10 -0
  42. package/tests/fixtures/jsgui3-html/resource_transform_expectations.json +11 -0
  43. package/tests/fixtures/jsgui3-html/router_expectations.json +10 -0
  44. package/tests/fixtures/jsgui3-html/theming_expectations.json +10 -0
  45. package/tests/jsgui3-html-examples.puppeteer.test.js +537 -0
  46. package/tests/test-runner.js +1 -0
  47. package/tests/window-examples.puppeteer.test.js +217 -1
@@ -0,0 +1,514 @@
1
+ const jsgui = require('jsgui3-client');
2
+ const Active_HTML_Document = require('../../../controls/Active_HTML_Document');
3
+ const { Data_Object } = jsgui;
4
+ const { apply_theme_overrides } = require('jsgui3-html/control_mixins/theme');
5
+
6
+ const THEME_OPTIONS = Object.freeze({
7
+ copper: {
8
+ label: 'Copper Studio',
9
+ tokens: {
10
+ bg: '#fdf6ec',
11
+ surface: '#ffffff',
12
+ accent: '#d05f3b',
13
+ ink: '#2a2018',
14
+ muted: '#6f5b4b'
15
+ }
16
+ },
17
+ tide: {
18
+ label: 'Tide Atlas',
19
+ tokens: {
20
+ bg: '#eef4f7',
21
+ surface: '#ffffff',
22
+ accent: '#2b6cb0',
23
+ ink: '#10253b',
24
+ muted: '#5c6b7a'
25
+ }
26
+ }
27
+ });
28
+
29
+ class Theming_Control extends jsgui.Control {
30
+ constructor(spec = {}) {
31
+ spec.__type_name = spec.__type_name || 'theming_control';
32
+ super(spec);
33
+
34
+ this.data.model = new Data_Object({
35
+ theme_name: 'copper'
36
+ });
37
+
38
+ this.view.data.model = new Data_Object({
39
+ theme_label: '',
40
+ accent_value: '',
41
+ surface_value: '',
42
+ bg_value: '',
43
+ ink_value: '',
44
+ muted_value: ''
45
+ });
46
+
47
+ this.apply_theme_from_name(this.data.model.theme_name);
48
+
49
+ if (!spec.el) {
50
+ this.compose();
51
+ }
52
+ }
53
+
54
+ apply_theme_from_name(theme_name, options = {}) {
55
+ const next_theme = THEME_OPTIONS[theme_name] || THEME_OPTIONS.copper;
56
+ const tokens = next_theme.tokens || {};
57
+
58
+ apply_theme_overrides(this, tokens);
59
+
60
+ if (options.dom_el) {
61
+ const dom_el = options.dom_el;
62
+ Object.keys(tokens).forEach((token_name) => {
63
+ dom_el.style.setProperty(`--theme-${token_name}`, tokens[token_name]);
64
+ });
65
+ dom_el.setAttribute('data-theme', theme_name);
66
+ }
67
+
68
+ this.view.data.model.theme_label = next_theme.label;
69
+ this.view.data.model.accent_value = tokens.accent || '';
70
+ this.view.data.model.surface_value = tokens.surface || '';
71
+ this.view.data.model.bg_value = tokens.bg || '';
72
+ this.view.data.model.ink_value = tokens.ink || '';
73
+ this.view.data.model.muted_value = tokens.muted || '';
74
+ }
75
+
76
+ update_theme_buttons(button_map, active_theme) {
77
+ Object.keys(button_map).forEach((theme_key) => {
78
+ const button_el = button_map[theme_key];
79
+ if (!button_el) return;
80
+ const is_active = theme_key === active_theme;
81
+ button_el.classList.toggle('is-active', is_active);
82
+ button_el.setAttribute('aria-pressed', is_active ? 'true' : 'false');
83
+ });
84
+ }
85
+
86
+ activate() {
87
+ if (this.__active) return;
88
+ super.activate();
89
+
90
+ if (this._dom_bound) return;
91
+ const root_el = this.dom.el;
92
+ if (!root_el) return;
93
+ this._dom_bound = true;
94
+
95
+ const theme_label_el = root_el.querySelector('[data-test="theme-name"]');
96
+ const accent_el = root_el.querySelector('[data-test="token-accent"]');
97
+ const surface_el = root_el.querySelector('[data-test="token-surface"]');
98
+ const bg_el = root_el.querySelector('[data-test="token-bg"]');
99
+ const ink_el = root_el.querySelector('[data-test="token-ink"]');
100
+ const muted_el = root_el.querySelector('[data-test="token-muted"]');
101
+
102
+ const button_map = {
103
+ copper: root_el.querySelector('[data-test="theme-copper"]'),
104
+ tide: root_el.querySelector('[data-test="theme-tide"]')
105
+ };
106
+
107
+ Object.keys(button_map).forEach((theme_key) => {
108
+ const button_el = button_map[theme_key];
109
+ if (!button_el) return;
110
+ button_el.addEventListener('click', () => {
111
+ this.data.model.theme_name = theme_key;
112
+ });
113
+ });
114
+
115
+ this.watch(
116
+ this.data.model,
117
+ 'theme_name',
118
+ (theme_name) => {
119
+ this.apply_theme_from_name(theme_name, { dom_el: root_el });
120
+ this.update_theme_buttons(button_map, theme_name);
121
+ },
122
+ { immediate: true }
123
+ );
124
+
125
+ this.watch(
126
+ this.view.data.model,
127
+ 'theme_label',
128
+ (theme_label) => {
129
+ if (theme_label_el) theme_label_el.textContent = theme_label || '';
130
+ },
131
+ { immediate: true }
132
+ );
133
+
134
+ this.watch(
135
+ this.view.data.model,
136
+ 'accent_value',
137
+ (accent_value) => {
138
+ if (accent_el) accent_el.textContent = accent_value || '';
139
+ },
140
+ { immediate: true }
141
+ );
142
+
143
+ this.watch(
144
+ this.view.data.model,
145
+ 'surface_value',
146
+ (surface_value) => {
147
+ if (surface_el) surface_el.textContent = surface_value || '';
148
+ },
149
+ { immediate: true }
150
+ );
151
+
152
+ this.watch(
153
+ this.view.data.model,
154
+ 'bg_value',
155
+ (bg_value) => {
156
+ if (bg_el) bg_el.textContent = bg_value || '';
157
+ },
158
+ { immediate: true }
159
+ );
160
+
161
+ this.watch(
162
+ this.view.data.model,
163
+ 'ink_value',
164
+ (ink_value) => {
165
+ if (ink_el) ink_el.textContent = ink_value || '';
166
+ },
167
+ { immediate: true }
168
+ );
169
+
170
+ this.watch(
171
+ this.view.data.model,
172
+ 'muted_value',
173
+ (muted_value) => {
174
+ if (muted_el) muted_el.textContent = muted_value || '';
175
+ },
176
+ { immediate: true }
177
+ );
178
+ }
179
+
180
+ compose() {
181
+ // Framework expects the method name `compose`.
182
+ const page_context = this.context;
183
+
184
+ this.add_class('theming-control');
185
+ this.dom.attributes['data-test'] = 'theming-control';
186
+ this.dom.attributes['data-theme'] = this.data.model.theme_name;
187
+
188
+ const header = new jsgui.Control({
189
+ context: page_context,
190
+ tagName: 'header',
191
+ class: 'theme-header'
192
+ });
193
+
194
+ header.add(new jsgui.Control({
195
+ context: page_context,
196
+ tagName: 'h1',
197
+ class: 'theme-title',
198
+ content: 'Theme Tokens'
199
+ }));
200
+ header.add(new jsgui.Control({
201
+ context: page_context,
202
+ tagName: 'p',
203
+ class: 'theme-subtitle',
204
+ content: 'Switch token sets and watch the UI update.'
205
+ }));
206
+
207
+ const button_row = new jsgui.Control({
208
+ context: page_context,
209
+ tagName: 'div',
210
+ class: 'theme-buttons'
211
+ });
212
+
213
+ const copper_button = new jsgui.Control({
214
+ context: page_context,
215
+ tagName: 'button',
216
+ class: 'theme-button',
217
+ content: 'Copper'
218
+ });
219
+ copper_button.dom.attributes['data-test'] = 'theme-copper';
220
+
221
+ const tide_button = new jsgui.Control({
222
+ context: page_context,
223
+ tagName: 'button',
224
+ class: 'theme-button',
225
+ content: 'Tide'
226
+ });
227
+ tide_button.dom.attributes['data-test'] = 'theme-tide';
228
+
229
+ button_row.add(copper_button);
230
+ button_row.add(tide_button);
231
+
232
+ const layout = new jsgui.Control({
233
+ context: page_context,
234
+ tagName: 'div',
235
+ class: 'theme-layout'
236
+ });
237
+
238
+ const token_panel = new jsgui.Control({
239
+ context: page_context,
240
+ tagName: 'section',
241
+ class: 'theme-panel'
242
+ });
243
+
244
+ const label = new jsgui.Control({
245
+ context: page_context,
246
+ tagName: 'div',
247
+ class: 'theme-label',
248
+ content: this.view.data.model.theme_label
249
+ });
250
+ label.dom.attributes['data-test'] = 'theme-name';
251
+
252
+ const token_list = new jsgui.Control({
253
+ context: page_context,
254
+ tagName: 'div',
255
+ class: 'theme-token-list'
256
+ });
257
+
258
+ const make_token_row = (label_text, test_id, value) => {
259
+ const row = new jsgui.Control({
260
+ context: page_context,
261
+ tagName: 'div',
262
+ class: 'theme-token'
263
+ });
264
+ row.add(new jsgui.Control({
265
+ context: page_context,
266
+ tagName: 'span',
267
+ class: 'token-label',
268
+ content: label_text
269
+ }));
270
+ const value_span = new jsgui.Control({
271
+ context: page_context,
272
+ tagName: 'span',
273
+ class: 'token-value',
274
+ content: value
275
+ });
276
+ value_span.dom.attributes['data-test'] = test_id;
277
+ row.add(value_span);
278
+ return row;
279
+ };
280
+
281
+ token_list.add(make_token_row('Accent', 'token-accent', this.view.data.model.accent_value));
282
+ token_list.add(make_token_row('Surface', 'token-surface', this.view.data.model.surface_value));
283
+ token_list.add(make_token_row('Background', 'token-bg', this.view.data.model.bg_value));
284
+ token_list.add(make_token_row('Ink', 'token-ink', this.view.data.model.ink_value));
285
+ token_list.add(make_token_row('Muted', 'token-muted', this.view.data.model.muted_value));
286
+
287
+ token_panel.add(label);
288
+ token_panel.add(token_list);
289
+
290
+ const preview_panel = new jsgui.Control({
291
+ context: page_context,
292
+ tagName: 'section',
293
+ class: 'theme-panel theme-preview'
294
+ });
295
+
296
+ const preview_card = new jsgui.Control({
297
+ context: page_context,
298
+ tagName: 'div',
299
+ class: 'preview-card'
300
+ });
301
+ preview_card.dom.attributes['data-test'] = 'preview-card';
302
+
303
+ preview_card.add(new jsgui.Control({
304
+ context: page_context,
305
+ tagName: 'h2',
306
+ class: 'preview-title',
307
+ content: 'Preview Card'
308
+ }));
309
+ preview_card.add(new jsgui.Control({
310
+ context: page_context,
311
+ tagName: 'p',
312
+ class: 'preview-body',
313
+ content: 'Tokens drive the colors, borders, and emphasis styles.'
314
+ }));
315
+ preview_card.add(new jsgui.Control({
316
+ context: page_context,
317
+ tagName: 'button',
318
+ class: 'preview-action',
319
+ content: 'Primary action'
320
+ }));
321
+
322
+ preview_panel.add(preview_card);
323
+
324
+ layout.add(token_panel);
325
+ layout.add(preview_panel);
326
+
327
+ this.add(header);
328
+ this.add(button_row);
329
+ this.add(layout);
330
+ }
331
+ }
332
+
333
+ class Demo_UI extends Active_HTML_Document {
334
+ constructor(spec = {}) {
335
+ spec.__type_name = spec.__type_name || 'theming_demo_ui';
336
+ super(spec);
337
+
338
+ if (!spec.el) {
339
+ this.compose();
340
+ }
341
+ }
342
+
343
+ compose() {
344
+ // Framework expects the method name `compose`.
345
+ const page_context = this.context;
346
+ this.body.add_class('theming-demo');
347
+
348
+ const theme_control = new Theming_Control({
349
+ context: page_context
350
+ });
351
+
352
+ this.body.add(theme_control);
353
+ }
354
+ }
355
+
356
+ Demo_UI.css = `
357
+ :root {
358
+ --theme-bg: #fdf6ec;
359
+ --theme-surface: #ffffff;
360
+ --theme-accent: #d05f3b;
361
+ --theme-ink: #2a2018;
362
+ --theme-muted: #6f5b4b;
363
+ }
364
+
365
+ * {
366
+ box-sizing: border-box;
367
+ }
368
+
369
+ body {
370
+ margin: 0;
371
+ padding: 0;
372
+ background: var(--theme-bg);
373
+ color: var(--theme-ink);
374
+ font-family: "Sora", "Trebuchet MS", sans-serif;
375
+ }
376
+
377
+ .theming-demo {
378
+ min-height: 100vh;
379
+ display: flex;
380
+ align-items: center;
381
+ justify-content: center;
382
+ padding: 42px 24px;
383
+ }
384
+
385
+ .theming-control {
386
+ width: min(980px, 100%);
387
+ background: var(--theme-surface);
388
+ border-radius: 24px;
389
+ padding: 32px 36px 40px;
390
+ border: 1px solid rgba(0, 0, 0, 0.08);
391
+ box-shadow: 0 26px 60px rgba(30, 30, 40, 0.15);
392
+ }
393
+
394
+ .theme-header {
395
+ margin-bottom: 18px;
396
+ }
397
+
398
+ .theme-title {
399
+ margin: 0 0 8px;
400
+ font-size: 30px;
401
+ }
402
+
403
+ .theme-subtitle {
404
+ margin: 0;
405
+ color: var(--theme-muted);
406
+ font-size: 15px;
407
+ }
408
+
409
+ .theme-buttons {
410
+ display: flex;
411
+ gap: 10px;
412
+ margin-bottom: 18px;
413
+ }
414
+
415
+ .theme-button {
416
+ border: 1px solid rgba(0, 0, 0, 0.15);
417
+ background: transparent;
418
+ padding: 8px 16px;
419
+ border-radius: 999px;
420
+ font-size: 13px;
421
+ cursor: pointer;
422
+ }
423
+
424
+ .theme-button.is-active {
425
+ background: var(--theme-accent);
426
+ border-color: var(--theme-accent);
427
+ color: #ffffff;
428
+ }
429
+
430
+ .theme-layout {
431
+ display: grid;
432
+ grid-template-columns: 1fr 1fr;
433
+ gap: 20px;
434
+ }
435
+
436
+ .theme-panel {
437
+ background: #fbfaf7;
438
+ border-radius: 16px;
439
+ border: 1px solid rgba(0, 0, 0, 0.08);
440
+ padding: 18px 20px;
441
+ }
442
+
443
+ .theme-label {
444
+ font-size: 18px;
445
+ font-weight: 600;
446
+ margin-bottom: 12px;
447
+ }
448
+
449
+ .theme-token-list {
450
+ display: grid;
451
+ gap: 10px;
452
+ }
453
+
454
+ .theme-token {
455
+ display: flex;
456
+ justify-content: space-between;
457
+ font-size: 13px;
458
+ color: var(--theme-muted);
459
+ }
460
+
461
+ .token-label {
462
+ text-transform: uppercase;
463
+ letter-spacing: 0.08em;
464
+ }
465
+
466
+ .token-value {
467
+ font-weight: 600;
468
+ color: var(--theme-ink);
469
+ }
470
+
471
+ .theme-preview {
472
+ background: linear-gradient(140deg, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.6));
473
+ }
474
+
475
+ .preview-card {
476
+ background: var(--theme-bg);
477
+ border-radius: 16px;
478
+ padding: 20px;
479
+ border: 1px solid rgba(0, 0, 0, 0.06);
480
+ }
481
+
482
+ .preview-title {
483
+ margin: 0 0 8px;
484
+ font-size: 20px;
485
+ }
486
+
487
+ .preview-body {
488
+ margin: 0 0 16px;
489
+ color: var(--theme-muted);
490
+ font-size: 14px;
491
+ }
492
+
493
+ .preview-action {
494
+ background: var(--theme-accent);
495
+ border: none;
496
+ color: #ffffff;
497
+ padding: 10px 16px;
498
+ border-radius: 999px;
499
+ cursor: pointer;
500
+ font-size: 13px;
501
+ }
502
+
503
+ @media (max-width: 900px) {
504
+ .theme-layout {
505
+ grid-template-columns: 1fr;
506
+ }
507
+ }
508
+ `;
509
+
510
+ jsgui.controls.Theming_Control = Theming_Control;
511
+ jsgui.controls.Demo_UI = Demo_UI;
512
+ jsgui.controls.theming_demo_ui = Demo_UI;
513
+
514
+ module.exports = jsgui;
@@ -0,0 +1,21 @@
1
+ const jsgui = require('./client');
2
+ const Server = require('../../../server');
3
+ const { Demo_UI } = jsgui.controls;
4
+
5
+ if (require.main === module) {
6
+ const server_instance = new Server({
7
+ Ctrl: Demo_UI,
8
+ src_path_client_js: require.resolve('./client.js')
9
+ });
10
+
11
+ server_instance.allowed_addresses = ['127.0.0.1'];
12
+
13
+ server_instance.on('ready', () => {
14
+ server_instance.start(52000, (err) => {
15
+ if (err) {
16
+ throw err;
17
+ }
18
+ console.log('server started on port 52000');
19
+ });
20
+ });
21
+ }