jsgui3-server 0.0.144 → 0.0.146

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 (71) hide show
  1. package/docs/jsgui3-html-improvement-ideas.md +162 -0
  2. package/docs/jsgui3-html-improvement-ideas.svg +151 -0
  3. package/docs/jsgui3-sass-patterns-book/01-vision-and-goals.md +31 -0
  4. package/docs/jsgui3-sass-patterns-book/02-stack-map.md +40 -0
  5. package/docs/jsgui3-sass-patterns-book/03-control-local-sass.md +60 -0
  6. package/docs/jsgui3-sass-patterns-book/04-extension-and-variants.md +76 -0
  7. package/docs/jsgui3-sass-patterns-book/05-theming-and-tokens.md +54 -0
  8. package/docs/jsgui3-sass-patterns-book/06-workspace-overrides.md +45 -0
  9. package/docs/jsgui3-sass-patterns-book/07-resource-and-bundling.md +46 -0
  10. package/docs/jsgui3-sass-patterns-book/08-examples.md +62 -0
  11. package/docs/jsgui3-sass-patterns-book/09-testing-and-adoption.md +27 -0
  12. package/docs/jsgui3-sass-patterns-book/README.md +23 -0
  13. package/docs/troubleshooting.md +44 -4
  14. package/examples/controls/14) window, canvas/client.js +1 -1
  15. package/examples/controls/14b) window, canvas (improved renderer)/client.js +1 -1
  16. package/examples/controls/14d) window, canvas globe/EarthGlobeRenderer.js +19 -14
  17. package/examples/controls/14d) window, canvas globe/client.js +1 -1
  18. package/examples/controls/14d) window, canvas globe/pipeline/TransformStage.js +5 -5
  19. package/examples/controls/14e) window, canvas multithreaded/client.js +1 -1
  20. package/examples/controls/14f) window, canvas polyglobe/client.js +1 -1
  21. package/examples/controls/16) window, form container/client.js +254 -0
  22. package/examples/controls/16) window, form container/server.js +20 -0
  23. package/examples/controls/17) window, mvvm binding/client.js +530 -0
  24. package/examples/controls/17) window, mvvm binding/server.js +20 -0
  25. package/examples/jsgui3-html/01) mvvm-counter/client.js +648 -0
  26. package/examples/jsgui3-html/01) mvvm-counter/server.js +21 -0
  27. package/examples/jsgui3-html/02) date-transform/client.js +764 -0
  28. package/examples/jsgui3-html/02) date-transform/server.js +21 -0
  29. package/examples/jsgui3-html/03) form-validation/client.js +1045 -0
  30. package/examples/jsgui3-html/03) form-validation/server.js +21 -0
  31. package/examples/jsgui3-html/04) data-grid/client.js +738 -0
  32. package/examples/jsgui3-html/04) data-grid/server.js +21 -0
  33. package/examples/jsgui3-html/05) master-detail/client.js +649 -0
  34. package/examples/jsgui3-html/05) master-detail/server.js +21 -0
  35. package/examples/jsgui3-html/06) theming/client.js +514 -0
  36. package/examples/jsgui3-html/06) theming/server.js +21 -0
  37. package/examples/jsgui3-html/07) mixins/client.js +465 -0
  38. package/examples/jsgui3-html/07) mixins/server.js +21 -0
  39. package/examples/jsgui3-html/08) router/client.js +372 -0
  40. package/examples/jsgui3-html/08) router/server.js +21 -0
  41. package/examples/jsgui3-html/09) resource-transform/client.js +692 -0
  42. package/examples/jsgui3-html/09) resource-transform/server.js +21 -0
  43. package/examples/jsgui3-html/10) binding-debugger/client.js +810 -0
  44. package/examples/jsgui3-html/10) binding-debugger/server.js +21 -0
  45. package/examples/jsgui3-html/README.md +48 -0
  46. package/http/responders/static/Static_Route_HTTP_Responder.js +25 -20
  47. package/package.json +3 -3
  48. package/publishers/http-webpageorsite-publisher.js +3 -1
  49. package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +87 -100
  50. package/resources/processors/bundlers/js/esbuild/Core_JS_Single_File_Minifying_Bundler_Using_ESBuild.js +89 -60
  51. package/serve-factory.js +12 -5
  52. package/server.js +103 -85
  53. package/tests/README.md +52 -9
  54. package/tests/bundlers.test.js +53 -47
  55. package/tests/end-to-end.test.js +336 -365
  56. package/tests/examples-controls.e2e.test.js +15 -1
  57. package/tests/fixtures/end-to-end-client.js +54 -0
  58. package/tests/fixtures/jsgui3-html/binding_debugger_expectations.json +15 -0
  59. package/tests/fixtures/jsgui3-html/counter_expectations.json +31 -0
  60. package/tests/fixtures/jsgui3-html/data_grid_expectations.json +26 -0
  61. package/tests/fixtures/jsgui3-html/date_transform_expectations.json +26 -0
  62. package/tests/fixtures/jsgui3-html/form_validation_expectations.json +27 -0
  63. package/tests/fixtures/jsgui3-html/master_detail_expectations.json +15 -0
  64. package/tests/fixtures/jsgui3-html/mixins_expectations.json +10 -0
  65. package/tests/fixtures/jsgui3-html/resource_transform_expectations.json +11 -0
  66. package/tests/fixtures/jsgui3-html/router_expectations.json +10 -0
  67. package/tests/fixtures/jsgui3-html/theming_expectations.json +10 -0
  68. package/tests/jsgui3-html-examples.puppeteer.test.js +537 -0
  69. package/tests/sass-controls.e2e.test.js +123 -9
  70. package/tests/test-runner.js +1 -0
  71. package/tests/window-examples.puppeteer.test.js +217 -1
@@ -0,0 +1,738 @@
1
+ const jsgui = require('jsgui3-client');
2
+ const Active_HTML_Document = require('../../../controls/Active_HTML_Document');
3
+ const { Data_Object } = jsgui;
4
+
5
+ const DEFAULT_ITEMS = Object.freeze([
6
+ { id: 1, name: 'Ada Lovelace', role: 'Engineer', score: 95, joined: '2024-03-01' },
7
+ { id: 2, name: 'Grace Hopper', role: 'Admiral', score: 98, joined: '2024-01-15' },
8
+ { id: 3, name: 'Katherine Johnson', role: 'Analyst', score: 90, joined: '2024-02-20' },
9
+ { id: 4, name: 'Margaret Hamilton', role: 'Lead', score: 92, joined: '2024-04-10' },
10
+ { id: 5, name: 'Dorothy Vaughan', role: 'Supervisor', score: 88, joined: '2024-05-05' },
11
+ { id: 6, name: 'Mary Jackson', role: 'Specialist', score: 85, joined: '2024-06-12' },
12
+ { id: 7, name: 'Hedy Lamarr', role: 'Inventor', score: 91, joined: '2024-07-08' },
13
+ { id: 8, name: 'Radia Perlman', role: 'Architect', score: 94, joined: '2024-08-17' },
14
+ { id: 9, name: 'Frances Allen', role: 'Researcher', score: 89, joined: '2024-09-22' },
15
+ { id: 10, name: 'Annie Easley', role: 'Mathematician', score: 87, joined: '2024-10-30' }
16
+ ]);
17
+
18
+ class Data_Grid_Control extends jsgui.Control {
19
+ constructor(spec = {}) {
20
+ spec.__type_name = spec.__type_name || 'data_grid_control';
21
+ super(spec);
22
+
23
+ this.data.model = new Data_Object({
24
+ search_text: '',
25
+ sort_key: 'name',
26
+ sort_dir: 'asc',
27
+ page_index: 0,
28
+ page_size: 4
29
+ });
30
+ this.data.model.set('items', spec.items || DEFAULT_ITEMS, true);
31
+
32
+ this.view.data.model = new Data_Object({
33
+ paged_items: [],
34
+ result_count: 0,
35
+ range_text: '',
36
+ page_text: '',
37
+ total_pages: 1
38
+ });
39
+
40
+ this.setup_watchers();
41
+ this.recompute_view();
42
+
43
+ if (!spec.el) {
44
+ this.compose();
45
+ }
46
+ }
47
+
48
+ format_score(score) {
49
+ if (!Number.isFinite(score)) return '';
50
+ return this.transforms.number.toFixed(score, 0);
51
+ }
52
+
53
+ format_joined(iso_date) {
54
+ return this.transforms.date.format_iso_to_locale(iso_date, 'en-US');
55
+ }
56
+
57
+ get_items_array() {
58
+ let raw_items = this.data.model && typeof this.data.model.get === 'function'
59
+ ? this.data.model.get('items')
60
+ : this.data.model.items;
61
+
62
+ if (raw_items && raw_items.__data_value) {
63
+ raw_items = raw_items.value;
64
+ }
65
+
66
+ if (raw_items && typeof raw_items.value === 'function') {
67
+ raw_items = raw_items.value();
68
+ }
69
+
70
+ if (Array.isArray(raw_items)) {
71
+ return raw_items.slice();
72
+ }
73
+
74
+ if (raw_items && typeof raw_items.toArray === 'function') {
75
+ return raw_items.toArray().slice();
76
+ }
77
+
78
+ if (raw_items && Array.isArray(raw_items._arr)) {
79
+ return raw_items._arr.slice();
80
+ }
81
+
82
+ return [];
83
+ }
84
+
85
+ recompute_view() {
86
+ const items = this.get_items_array();
87
+ const search_text = this.transforms.string.trim(this.data.model.search_text).toLowerCase();
88
+ const sort_key = this.data.model.sort_key || 'name';
89
+ const sort_dir = this.data.model.sort_dir === 'desc' ? 'desc' : 'asc';
90
+ const page_size = Number.isFinite(this.data.model.page_size) ? this.data.model.page_size : 4;
91
+
92
+ const filtered_items = search_text
93
+ ? items.filter((item) => {
94
+ const haystack = `${item.name || ''} ${item.role || ''}`.toLowerCase();
95
+ return haystack.includes(search_text);
96
+ })
97
+ : items;
98
+
99
+ const sorted_items = filtered_items.slice().sort((a_item, b_item) => {
100
+ const a_value = a_item[sort_key];
101
+ const b_value = b_item[sort_key];
102
+ let comparison = 0;
103
+
104
+ if (sort_key === 'score') {
105
+ comparison = Number(a_value || 0) - Number(b_value || 0);
106
+ } else if (sort_key === 'joined') {
107
+ comparison = String(a_value || '').localeCompare(String(b_value || ''));
108
+ } else {
109
+ comparison = String(a_value || '').localeCompare(String(b_value || ''));
110
+ }
111
+
112
+ return sort_dir === 'asc' ? comparison : -comparison;
113
+ });
114
+
115
+ const total_pages = Math.max(1, Math.ceil(sorted_items.length / page_size));
116
+ let page_index = Number.isFinite(this.data.model.page_index) ? this.data.model.page_index : 0;
117
+ if (page_index > total_pages - 1) {
118
+ page_index = total_pages - 1;
119
+ this.data.model.page_index = page_index;
120
+ }
121
+
122
+ const start_index = page_index * page_size;
123
+ const end_index = Math.min(start_index + page_size, sorted_items.length);
124
+ const paged_items = sorted_items.slice(start_index, end_index);
125
+
126
+ const range_text = sorted_items.length
127
+ ? `Showing ${start_index + 1}-${end_index} of ${sorted_items.length}`
128
+ : 'No results.';
129
+
130
+ this.view.data.model.paged_items = paged_items;
131
+ this.view.data.model.result_count = sorted_items.length;
132
+ this.view.data.model.range_text = range_text;
133
+ this.view.data.model.page_text = `Page ${page_index + 1} of ${total_pages}`;
134
+ this.view.data.model.total_pages = total_pages;
135
+ }
136
+
137
+ setup_watchers() {
138
+ const recompute = () => this.recompute_view();
139
+
140
+ this.watch(this.data.model, 'items', () => recompute());
141
+ this.watch(this.data.model, 'search_text', () => recompute());
142
+ this.watch(this.data.model, 'sort_key', () => recompute());
143
+ this.watch(this.data.model, 'sort_dir', () => recompute());
144
+ this.watch(this.data.model, 'page_index', () => recompute());
145
+ this.watch(this.data.model, 'page_size', () => recompute());
146
+ }
147
+
148
+ update_search_text(raw_value) {
149
+ this.data.model.search_text = raw_value === null || raw_value === undefined
150
+ ? ''
151
+ : String(raw_value);
152
+ this.data.model.page_index = 0;
153
+ }
154
+
155
+ set_sort(next_key) {
156
+ if (this.data.model.sort_key === next_key) {
157
+ this.data.model.sort_dir = this.data.model.sort_dir === 'asc' ? 'desc' : 'asc';
158
+ } else {
159
+ this.data.model.sort_key = next_key;
160
+ this.data.model.sort_dir = 'asc';
161
+ }
162
+ this.data.model.page_index = 0;
163
+ }
164
+
165
+ set_page_index(next_index) {
166
+ const total_pages = this.view.data.model.total_pages || 1;
167
+ const clamped_index = Math.max(0, Math.min(total_pages - 1, next_index));
168
+ this.data.model.page_index = clamped_index;
169
+ }
170
+
171
+ render_rows_control(table_body, paged_items) {
172
+ table_body.clear();
173
+
174
+ paged_items.forEach((item) => {
175
+ const row = new jsgui.Control({
176
+ context: this.context,
177
+ tagName: 'tr',
178
+ class: 'grid-row'
179
+ });
180
+
181
+ const name_cell = new jsgui.Control({
182
+ context: this.context,
183
+ tagName: 'td',
184
+ content: item.name
185
+ });
186
+ name_cell.dom.attributes['data-col'] = 'name';
187
+
188
+ const role_cell = new jsgui.Control({
189
+ context: this.context,
190
+ tagName: 'td',
191
+ content: item.role
192
+ });
193
+ role_cell.dom.attributes['data-col'] = 'role';
194
+
195
+ const score_cell = new jsgui.Control({
196
+ context: this.context,
197
+ tagName: 'td',
198
+ content: this.format_score(item.score)
199
+ });
200
+ score_cell.dom.attributes['data-col'] = 'score';
201
+
202
+ const joined_cell = new jsgui.Control({
203
+ context: this.context,
204
+ tagName: 'td',
205
+ content: this.format_joined(item.joined)
206
+ });
207
+ joined_cell.dom.attributes['data-col'] = 'joined';
208
+
209
+ row.add(name_cell);
210
+ row.add(role_cell);
211
+ row.add(score_cell);
212
+ row.add(joined_cell);
213
+
214
+ table_body.add(row);
215
+ });
216
+ }
217
+
218
+ render_rows_dom(table_body_el, paged_items) {
219
+ if (!table_body_el) return;
220
+ table_body_el.innerHTML = '';
221
+
222
+ paged_items.forEach((item) => {
223
+ const row_el = document.createElement('tr');
224
+ row_el.className = 'grid-row';
225
+
226
+ const make_cell = (value, col_name) => {
227
+ const cell_el = document.createElement('td');
228
+ cell_el.setAttribute('data-col', col_name);
229
+ cell_el.textContent = value || '';
230
+ return cell_el;
231
+ };
232
+
233
+ row_el.appendChild(make_cell(item.name, 'name'));
234
+ row_el.appendChild(make_cell(item.role, 'role'));
235
+ row_el.appendChild(make_cell(this.format_score(item.score), 'score'));
236
+ row_el.appendChild(make_cell(this.format_joined(item.joined), 'joined'));
237
+
238
+ table_body_el.appendChild(row_el);
239
+ });
240
+ }
241
+
242
+ activate() {
243
+ if (this.__active) return;
244
+ super.activate();
245
+
246
+ if (this._dom_bound) return;
247
+ const root_el = this.dom.el;
248
+ if (!root_el) return;
249
+ this._dom_bound = true;
250
+
251
+ const search_input_el = root_el.querySelector('[data-test="search-input"]');
252
+ const table_body_el = root_el.querySelector('[data-test="grid-body"]');
253
+ const range_text_el = root_el.querySelector('[data-test="range-text"]');
254
+ const page_text_el = root_el.querySelector('[data-test="page-text"]');
255
+ const prev_button_el = root_el.querySelector('[data-test="prev-page"]');
256
+ const next_button_el = root_el.querySelector('[data-test="next-page"]');
257
+ const sort_name_el = root_el.querySelector('[data-test="sort-name"]');
258
+ const sort_role_el = root_el.querySelector('[data-test="sort-role"]');
259
+ const sort_score_el = root_el.querySelector('[data-test="sort-score"]');
260
+ const sort_joined_el = root_el.querySelector('[data-test="sort-joined"]');
261
+
262
+ const update_sort_labels = () => {
263
+ const sort_key = this.data.model.sort_key || 'name';
264
+ const sort_dir = this.data.model.sort_dir === 'desc' ? 'desc' : 'asc';
265
+
266
+ const label_for = (key, label) => {
267
+ if (sort_key !== key) return label;
268
+ return `${label} (${sort_dir})`;
269
+ };
270
+
271
+ if (sort_name_el) sort_name_el.textContent = label_for('name', 'Name');
272
+ if (sort_role_el) sort_role_el.textContent = label_for('role', 'Role');
273
+ if (sort_score_el) sort_score_el.textContent = label_for('score', 'Score');
274
+ if (sort_joined_el) sort_joined_el.textContent = label_for('joined', 'Joined');
275
+ };
276
+
277
+ const update_page_buttons = () => {
278
+ const page_index = Number.isFinite(this.data.model.page_index) ? this.data.model.page_index : 0;
279
+ const total_pages = this.view.data.model.total_pages || 1;
280
+ const has_prev = page_index > 0;
281
+ const has_next = page_index < total_pages - 1;
282
+
283
+ if (prev_button_el) {
284
+ prev_button_el.disabled = !has_prev;
285
+ prev_button_el.classList.toggle('is-disabled', !has_prev);
286
+ }
287
+
288
+ if (next_button_el) {
289
+ next_button_el.disabled = !has_next;
290
+ next_button_el.classList.toggle('is-disabled', !has_next);
291
+ }
292
+ };
293
+
294
+ if (search_input_el) {
295
+ search_input_el.addEventListener('input', (event) => {
296
+ const raw_value = event && event.target ? event.target.value : '';
297
+ this.update_search_text(raw_value);
298
+ });
299
+ }
300
+
301
+ if (prev_button_el) {
302
+ prev_button_el.addEventListener('click', () => {
303
+ this.set_page_index(this.data.model.page_index - 1);
304
+ });
305
+ }
306
+
307
+ if (next_button_el) {
308
+ next_button_el.addEventListener('click', () => {
309
+ this.set_page_index(this.data.model.page_index + 1);
310
+ });
311
+ }
312
+
313
+ if (sort_name_el) {
314
+ sort_name_el.addEventListener('click', () => this.set_sort('name'));
315
+ }
316
+
317
+ if (sort_role_el) {
318
+ sort_role_el.addEventListener('click', () => this.set_sort('role'));
319
+ }
320
+
321
+ if (sort_score_el) {
322
+ sort_score_el.addEventListener('click', () => this.set_sort('score'));
323
+ }
324
+
325
+ if (sort_joined_el) {
326
+ sort_joined_el.addEventListener('click', () => this.set_sort('joined'));
327
+ }
328
+
329
+ this.watch(
330
+ this.data.model,
331
+ 'search_text',
332
+ (search_text) => {
333
+ if (search_input_el) {
334
+ search_input_el.value = search_text || '';
335
+ }
336
+ },
337
+ { immediate: true }
338
+ );
339
+
340
+ this.watch(
341
+ this.view.data.model,
342
+ 'paged_items',
343
+ (paged_items) => {
344
+ this.render_rows_dom(table_body_el, Array.isArray(paged_items) ? paged_items : []);
345
+ },
346
+ { immediate: true }
347
+ );
348
+
349
+ this.watch(
350
+ this.view.data.model,
351
+ 'range_text',
352
+ (range_text) => {
353
+ if (range_text_el) {
354
+ range_text_el.textContent = range_text || '';
355
+ }
356
+ },
357
+ { immediate: true }
358
+ );
359
+
360
+ this.watch(
361
+ this.view.data.model,
362
+ 'page_text',
363
+ (page_text) => {
364
+ if (page_text_el) {
365
+ page_text_el.textContent = page_text || '';
366
+ }
367
+ },
368
+ { immediate: true }
369
+ );
370
+
371
+ this.watch(
372
+ this.data.model,
373
+ 'sort_key',
374
+ () => update_sort_labels(),
375
+ { immediate: true }
376
+ );
377
+
378
+ this.watch(
379
+ this.data.model,
380
+ 'sort_dir',
381
+ () => update_sort_labels(),
382
+ { immediate: true }
383
+ );
384
+
385
+ this.watch(
386
+ this.data.model,
387
+ 'page_index',
388
+ () => update_page_buttons(),
389
+ { immediate: true }
390
+ );
391
+
392
+ this.watch(
393
+ this.view.data.model,
394
+ 'total_pages',
395
+ () => update_page_buttons(),
396
+ { immediate: true }
397
+ );
398
+ }
399
+
400
+ compose() {
401
+ // Framework expects the method name `compose`.
402
+ const page_context = this.context;
403
+
404
+ this.add_class('data-grid-control');
405
+ this.dom.attributes['data-test'] = 'data-grid-control';
406
+
407
+ const card = new jsgui.Control({
408
+ context: page_context,
409
+ tagName: 'div',
410
+ class: 'grid-card'
411
+ });
412
+
413
+ const header = new jsgui.Control({
414
+ context: page_context,
415
+ tagName: 'div',
416
+ class: 'grid-header'
417
+ });
418
+
419
+ const title = new jsgui.Control({
420
+ context: page_context,
421
+ tagName: 'h1',
422
+ class: 'grid-title',
423
+ content: 'Team Directory'
424
+ });
425
+
426
+ const search_wrap = new jsgui.Control({
427
+ context: page_context,
428
+ tagName: 'div',
429
+ class: 'grid-search'
430
+ });
431
+
432
+ const search_input = new jsgui.Control({
433
+ context: page_context,
434
+ tagName: 'input',
435
+ class: 'grid-input'
436
+ });
437
+ search_input.dom.attributes.type = 'search';
438
+ search_input.dom.attributes.placeholder = 'Search by name or role';
439
+ search_input.dom.attributes['data-test'] = 'search-input';
440
+
441
+ search_wrap.add(search_input);
442
+ header.add(title);
443
+ header.add(search_wrap);
444
+
445
+ const meta = new jsgui.Control({
446
+ context: page_context,
447
+ tagName: 'div',
448
+ class: 'grid-meta'
449
+ });
450
+
451
+ const range_text = new jsgui.Control({
452
+ context: page_context,
453
+ tagName: 'div',
454
+ class: 'grid-range',
455
+ content: this.view.data.model.range_text
456
+ });
457
+ range_text.dom.attributes['data-test'] = 'range-text';
458
+
459
+ const page_text = new jsgui.Control({
460
+ context: page_context,
461
+ tagName: 'div',
462
+ class: 'grid-page',
463
+ content: this.view.data.model.page_text
464
+ });
465
+ page_text.dom.attributes['data-test'] = 'page-text';
466
+
467
+ meta.add(range_text);
468
+ meta.add(page_text);
469
+
470
+ const table = new jsgui.Control({
471
+ context: page_context,
472
+ tagName: 'table',
473
+ class: 'grid-table'
474
+ });
475
+
476
+ const table_head = new jsgui.Control({
477
+ context: page_context,
478
+ tagName: 'thead'
479
+ });
480
+
481
+ const header_row = new jsgui.Control({
482
+ context: page_context,
483
+ tagName: 'tr'
484
+ });
485
+
486
+ const make_sort_button = (label_text, test_id) => {
487
+ const button = new jsgui.Control({
488
+ context: page_context,
489
+ tagName: 'button',
490
+ class: 'grid-sort',
491
+ content: label_text
492
+ });
493
+ button.dom.attributes['data-test'] = test_id;
494
+ return button;
495
+ };
496
+
497
+ const name_header = new jsgui.Control({
498
+ context: page_context,
499
+ tagName: 'th'
500
+ });
501
+ const role_header = new jsgui.Control({
502
+ context: page_context,
503
+ tagName: 'th'
504
+ });
505
+ const score_header = new jsgui.Control({
506
+ context: page_context,
507
+ tagName: 'th'
508
+ });
509
+ const joined_header = new jsgui.Control({
510
+ context: page_context,
511
+ tagName: 'th'
512
+ });
513
+
514
+ const label_for = (key, label) => {
515
+ if (this.data.model.sort_key !== key) return label;
516
+ return `${label} (${this.data.model.sort_dir})`;
517
+ };
518
+
519
+ name_header.add(make_sort_button(label_for('name', 'Name'), 'sort-name'));
520
+ role_header.add(make_sort_button(label_for('role', 'Role'), 'sort-role'));
521
+ score_header.add(make_sort_button(label_for('score', 'Score'), 'sort-score'));
522
+ joined_header.add(make_sort_button(label_for('joined', 'Joined'), 'sort-joined'));
523
+
524
+ header_row.add(name_header);
525
+ header_row.add(role_header);
526
+ header_row.add(score_header);
527
+ header_row.add(joined_header);
528
+ table_head.add(header_row);
529
+
530
+ const table_body = new jsgui.Control({
531
+ context: page_context,
532
+ tagName: 'tbody'
533
+ });
534
+ table_body.dom.attributes['data-test'] = 'grid-body';
535
+
536
+ this.render_rows_control(table_body, this.view.data.model.paged_items);
537
+
538
+ table.add(table_head);
539
+ table.add(table_body);
540
+
541
+ const footer = new jsgui.Control({
542
+ context: page_context,
543
+ tagName: 'div',
544
+ class: 'grid-footer'
545
+ });
546
+
547
+ const prev_button = new jsgui.Control({
548
+ context: page_context,
549
+ tagName: 'button',
550
+ class: 'grid-button',
551
+ content: 'Prev'
552
+ });
553
+ prev_button.dom.attributes['data-test'] = 'prev-page';
554
+
555
+ const next_button = new jsgui.Control({
556
+ context: page_context,
557
+ tagName: 'button',
558
+ class: 'grid-button',
559
+ content: 'Next'
560
+ });
561
+ next_button.dom.attributes['data-test'] = 'next-page';
562
+
563
+ footer.add(prev_button);
564
+ footer.add(next_button);
565
+
566
+ card.add(header);
567
+ card.add(meta);
568
+ card.add(table);
569
+ card.add(footer);
570
+
571
+ this.add(card);
572
+ }
573
+ }
574
+
575
+ class Demo_UI extends Active_HTML_Document {
576
+ constructor(spec = {}) {
577
+ spec.__type_name = spec.__type_name || 'data_grid_demo_ui';
578
+ super(spec);
579
+
580
+ if (!spec.el) {
581
+ this.compose();
582
+ }
583
+ }
584
+
585
+ compose() {
586
+ // Framework expects the method name `compose`.
587
+ const page_context = this.context;
588
+ this.body.add_class('data-grid-demo');
589
+
590
+ const grid_control = new Data_Grid_Control({
591
+ context: page_context,
592
+ items: DEFAULT_ITEMS
593
+ });
594
+
595
+ this.body.add(grid_control);
596
+ }
597
+ }
598
+
599
+ Demo_UI.css = `
600
+ * {
601
+ box-sizing: border-box;
602
+ }
603
+
604
+ body {
605
+ margin: 0;
606
+ padding: 0;
607
+ font-family: "Garamond", "Georgia", serif;
608
+ background: linear-gradient(140deg, #f7efe3 0%, #eef4fb 55%, #dde4f2 100%);
609
+ color: #1d2430;
610
+ }
611
+
612
+ .data-grid-demo {
613
+ min-height: 100vh;
614
+ display: flex;
615
+ align-items: center;
616
+ justify-content: center;
617
+ padding: 36px;
618
+ }
619
+
620
+ .data-grid-control {
621
+ width: 100%;
622
+ max-width: 820px;
623
+ }
624
+
625
+ .grid-card {
626
+ background: #ffffff;
627
+ border-radius: 20px;
628
+ padding: 30px 34px;
629
+ border: 1px solid #e3d6c5;
630
+ box-shadow: 0 30px 60px rgba(19, 26, 38, 0.18);
631
+ display: grid;
632
+ gap: 18px;
633
+ }
634
+
635
+ .grid-header {
636
+ display: flex;
637
+ justify-content: space-between;
638
+ align-items: center;
639
+ gap: 16px;
640
+ }
641
+
642
+ .grid-title {
643
+ margin: 0;
644
+ font-size: 26px;
645
+ }
646
+
647
+ .grid-search {
648
+ min-width: 220px;
649
+ }
650
+
651
+ .grid-input {
652
+ width: 100%;
653
+ padding: 10px 12px;
654
+ border-radius: 999px;
655
+ border: 1px solid #c7ccd8;
656
+ font-size: 14px;
657
+ }
658
+
659
+ .grid-meta {
660
+ display: flex;
661
+ justify-content: space-between;
662
+ font-size: 13px;
663
+ color: #4a5568;
664
+ }
665
+
666
+ .grid-table {
667
+ width: 100%;
668
+ border-collapse: collapse;
669
+ }
670
+
671
+ .grid-table th,
672
+ .grid-table td {
673
+ text-align: left;
674
+ padding: 10px 8px;
675
+ border-bottom: 1px solid #e5e9f2;
676
+ font-size: 14px;
677
+ }
678
+
679
+ .grid-table th {
680
+ font-size: 12px;
681
+ text-transform: uppercase;
682
+ letter-spacing: 0.08em;
683
+ color: #6a5f53;
684
+ }
685
+
686
+ .grid-sort {
687
+ background: none;
688
+ border: none;
689
+ font-size: 12px;
690
+ text-transform: uppercase;
691
+ letter-spacing: 0.08em;
692
+ color: #6a5f53;
693
+ cursor: pointer;
694
+ padding: 0;
695
+ }
696
+
697
+ .grid-row:hover {
698
+ background: #f8fafc;
699
+ }
700
+
701
+ .grid-footer {
702
+ display: flex;
703
+ justify-content: flex-end;
704
+ gap: 12px;
705
+ }
706
+
707
+ .grid-button {
708
+ padding: 8px 16px;
709
+ border-radius: 999px;
710
+ border: 1px solid #c7ccd8;
711
+ background: #f4f6fb;
712
+ cursor: pointer;
713
+ font-size: 13px;
714
+ }
715
+
716
+ .grid-button.is-disabled {
717
+ opacity: 0.5;
718
+ cursor: not-allowed;
719
+ }
720
+
721
+ @media (max-width: 720px) {
722
+ .grid-header {
723
+ flex-direction: column;
724
+ align-items: flex-start;
725
+ }
726
+
727
+ .grid-meta {
728
+ flex-direction: column;
729
+ gap: 6px;
730
+ }
731
+ }
732
+ `;
733
+
734
+ jsgui.controls.Data_Grid_Control = Data_Grid_Control;
735
+ jsgui.controls.Demo_UI = Demo_UI;
736
+ jsgui.controls.data_grid_demo_ui = Demo_UI;
737
+
738
+ module.exports = jsgui;