@hawsen-the-first/interactiv 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +326 -0
  2. package/dist/animations.css +160 -0
  3. package/dist/index.d.ts +20 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +26 -0
  6. package/dist/src/animationBus.d.ts +30 -0
  7. package/dist/src/animationBus.d.ts.map +1 -0
  8. package/dist/src/animationBus.js +125 -0
  9. package/dist/src/appBuilder.d.ts +173 -0
  10. package/dist/src/appBuilder.d.ts.map +1 -0
  11. package/dist/src/appBuilder.js +957 -0
  12. package/dist/src/eventBus.d.ts +100 -0
  13. package/dist/src/eventBus.d.ts.map +1 -0
  14. package/dist/src/eventBus.js +326 -0
  15. package/dist/src/eventManager.d.ts +87 -0
  16. package/dist/src/eventManager.d.ts.map +1 -0
  17. package/dist/src/eventManager.js +455 -0
  18. package/dist/src/garbageCollector.d.ts +68 -0
  19. package/dist/src/garbageCollector.d.ts.map +1 -0
  20. package/dist/src/garbageCollector.js +169 -0
  21. package/dist/src/logger.d.ts +11 -0
  22. package/dist/src/logger.d.ts.map +1 -0
  23. package/dist/src/logger.js +15 -0
  24. package/dist/src/navigationManager.d.ts +105 -0
  25. package/dist/src/navigationManager.d.ts.map +1 -0
  26. package/dist/src/navigationManager.js +533 -0
  27. package/dist/src/screensaverManager.d.ts +66 -0
  28. package/dist/src/screensaverManager.d.ts.map +1 -0
  29. package/dist/src/screensaverManager.js +417 -0
  30. package/dist/src/settingsManager.d.ts +48 -0
  31. package/dist/src/settingsManager.d.ts.map +1 -0
  32. package/dist/src/settingsManager.js +317 -0
  33. package/dist/src/stateManager.d.ts +58 -0
  34. package/dist/src/stateManager.d.ts.map +1 -0
  35. package/dist/src/stateManager.js +278 -0
  36. package/dist/src/types.d.ts +32 -0
  37. package/dist/src/types.d.ts.map +1 -0
  38. package/dist/src/types.js +1 -0
  39. package/dist/utils/generateGuid.d.ts +2 -0
  40. package/dist/utils/generateGuid.d.ts.map +1 -0
  41. package/dist/utils/generateGuid.js +19 -0
  42. package/dist/utils/logger.d.ts +9 -0
  43. package/dist/utils/logger.d.ts.map +1 -0
  44. package/dist/utils/logger.js +42 -0
  45. package/dist/utils/template-helpers.d.ts +32 -0
  46. package/dist/utils/template-helpers.d.ts.map +1 -0
  47. package/dist/utils/template-helpers.js +24 -0
  48. package/package.json +59 -0
@@ -0,0 +1,957 @@
1
+ /* eslint-disable @typescript-eslint/no-this-alias */
2
+ import { EventBus } from "./eventBus";
3
+ import { NavigationManager } from "./navigationManager";
4
+ import { ScreensaverManager, } from "./screensaverManager";
5
+ import { SettingsManager } from "./settingsManager";
6
+ import { EventManager } from "./eventManager";
7
+ import { ComponentStateManager } from "./stateManager";
8
+ import { logger } from "./logger";
9
+ class RenderableComponent {
10
+ componentId;
11
+ state;
12
+ renderBus;
13
+ shadowRoot;
14
+ bubbleChanges;
15
+ children = [];
16
+ parent;
17
+ template = "";
18
+ styles = "";
19
+ lastRenderedHTML = "";
20
+ properties = new Map();
21
+ hostElement;
22
+ eventManager;
23
+ stateManager;
24
+ orchestrator;
25
+ globalSheets;
26
+ constructor(id, orchestrator, bubbleChanges = false) {
27
+ this.componentId = id;
28
+ this.bubbleChanges = bubbleChanges;
29
+ this.orchestrator = orchestrator;
30
+ this.globalSheets = [];
31
+ this.renderBus = orchestrator.registerEventBus(`render-${id}`);
32
+ this.setupRenderListeners();
33
+ this.attachShadowDOM();
34
+ this.initializeEventManager();
35
+ this.initializeStateManager();
36
+ }
37
+ setupRenderListeners() {
38
+ this.renderBus.on("render-request", (e) => {
39
+ const { changeType, source } = e.detail;
40
+ if (this.shouldRerender(changeType, source)) {
41
+ this.performRender();
42
+ }
43
+ });
44
+ this.renderBus.on("child-changed", (e) => {
45
+ const { childId, changeType } = e.detail;
46
+ this.handleChildChange(childId, changeType);
47
+ });
48
+ this.renderBus.on("parent-changed", (e) => {
49
+ const { changeType } = e.detail;
50
+ this.handleParentChange(changeType);
51
+ });
52
+ }
53
+ attachShadowDOM() {
54
+ this.hostElement = document.createElement("div");
55
+ this.hostElement.id = this.componentId;
56
+ this.shadowRoot = this.hostElement.attachShadow({ mode: "open" });
57
+ if (this.globalSheets?.length > 0) {
58
+ this.shadowRoot.adoptedStyleSheets = [...this.shadowRoot.adoptedStyleSheets, ...this.globalSheets];
59
+ }
60
+ }
61
+ async useGlobalStyles(text) {
62
+ const sheet = new CSSStyleSheet();
63
+ await sheet.replace(text);
64
+ this.globalSheets.push(sheet);
65
+ }
66
+ initializeEventManager() {
67
+ this.eventManager = new EventManager(this.shadowRoot, this.componentId);
68
+ }
69
+ /**
70
+ * Clean up event listeners before re-render to prevent duplicates
71
+ */
72
+ // private cleanupEventListeners(): void {
73
+ // // Clear all selector-based listeners to prevent duplicates
74
+ // // The EventManager will handle this automatically now with deduplication
75
+ // // but we can add explicit cleanup if needed
76
+ // }
77
+ initializeStateManager() {
78
+ this.stateManager = new ComponentStateManager(this.componentId, (key, value, isLocal) => {
79
+ this.handleStateChange(key, value, isLocal);
80
+ });
81
+ // Attach the state proxy to this component
82
+ this.state = this.stateManager.getStateProxy();
83
+ }
84
+ handleStateChange(key, value, isLocal) {
85
+ // Update properties map for template compilation
86
+ this.properties.set(key, value);
87
+ // Trigger re-render directly instead of through event bus to avoid loops
88
+ this.performRender();
89
+ // Handle bubbling for local state changes
90
+ if (isLocal && this.bubbleChanges && this.parent) {
91
+ this.parent.renderBus.emit("child-changed", {
92
+ childId: this.componentId,
93
+ changeType: "state-change",
94
+ stateKey: key,
95
+ isLocal: true,
96
+ });
97
+ }
98
+ }
99
+ performRender(options) {
100
+ const fadeConfig = options?.fade;
101
+ if (fadeConfig?.enabled) {
102
+ this.performRenderWithFade(fadeConfig.duration ?? 300, options);
103
+ }
104
+ else {
105
+ this.performRenderImmediate(options);
106
+ }
107
+ }
108
+ async performRenderWithFade(duration, options) {
109
+ // Fade out
110
+ this.hostElement.style.transition = `opacity ${duration}ms ease`;
111
+ this.hostElement.style.opacity = '0';
112
+ // Wait for fade out to complete
113
+ await new Promise(resolve => setTimeout(resolve, duration));
114
+ // Perform actual render
115
+ this.performRenderImmediate(options);
116
+ // Fade in
117
+ this.hostElement.style.opacity = '1';
118
+ }
119
+ performRenderImmediate(options) {
120
+ this.onBeforeRender();
121
+ const compiledHTML = this.compileTemplate();
122
+ if (compiledHTML !== this.lastRenderedHTML) {
123
+ this.updateShadowDOM(compiledHTML);
124
+ this.lastRenderedHTML = compiledHTML;
125
+ this.attachEventListeners();
126
+ this.renderChildren(options);
127
+ }
128
+ this.onAfterRender();
129
+ }
130
+ compileTemplate() {
131
+ let compiled = this.template;
132
+ // Handle ifnot/else blocks first (inverted if) - process these before regular if blocks
133
+ compiled = compiled.replace(/\{\{#ifnot\s+(\w+)\s*\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/ifnot\}\}/g, (_, condition, ifnotContent, elseContent) => {
134
+ const conditionValue = this.getProperty(condition);
135
+ if (!conditionValue) {
136
+ return ifnotContent || "";
137
+ }
138
+ else if (elseContent !== undefined) {
139
+ return elseContent;
140
+ }
141
+ return "";
142
+ });
143
+ // Handle if/else blocks
144
+ compiled = compiled.replace(/\{\{#if\s+(\w+)\s*\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}/g, (_, condition, ifContent, elseContent) => {
145
+ const conditionValue = this.getProperty(condition);
146
+ if (conditionValue) {
147
+ return ifContent || "";
148
+ }
149
+ else if (elseContent !== undefined) {
150
+ return elseContent;
151
+ }
152
+ return "";
153
+ });
154
+ // Handle property interpolation last
155
+ compiled = compiled.replace(/\{\{(\w+)\}\}/g, (_, propName) => {
156
+ return this.getProperty(propName) || "";
157
+ });
158
+ return compiled;
159
+ }
160
+ updateShadowDOM(html) {
161
+ this.shadowRoot.innerHTML = `
162
+ <style>${this.styles}</style>
163
+ ${html}
164
+ `;
165
+ }
166
+ renderChildren(options) {
167
+ const childrenContainer = this.shadowRoot.querySelector(".children-container");
168
+ if (childrenContainer && this.children.length > 0) {
169
+ childrenContainer.innerHTML = "";
170
+ this.children.forEach((child) => {
171
+ // Ensure child is rendered before appending
172
+ // Pass fade options to children so they fade together
173
+ child.performRender(options);
174
+ // Ensure child host element is visible
175
+ //child.hostElement.style.display = "block";
176
+ childrenContainer.appendChild(child.hostElement);
177
+ });
178
+ }
179
+ }
180
+ getProperty(key) {
181
+ return this.properties.get(key);
182
+ }
183
+ getPublicProperty(key) {
184
+ return this.properties.get(key);
185
+ }
186
+ setProperty(key, value) {
187
+ const oldValue = this.properties.get(key);
188
+ if (oldValue !== value) {
189
+ this.properties.set(key, value);
190
+ // Special handling for template changes
191
+ if (key === "template") {
192
+ this.template = value;
193
+ this.performRender();
194
+ return;
195
+ }
196
+ // Special handling for styles changes
197
+ if (key === "styles") {
198
+ this.styles = value;
199
+ this.performRender();
200
+ return;
201
+ }
202
+ this.renderBus.emit("render-request", {
203
+ changeType: "property-change",
204
+ source: this.componentId,
205
+ property: key,
206
+ oldValue,
207
+ newValue: value,
208
+ });
209
+ if (this.bubbleChanges && this.parent) {
210
+ this.parent.renderBus.emit("child-changed", {
211
+ childId: this.componentId,
212
+ changeType: "property-change",
213
+ property: key,
214
+ });
215
+ }
216
+ }
217
+ }
218
+ addChild(child) {
219
+ child.parent = this;
220
+ this.children.push(child);
221
+ // Force immediate re-render when child is added
222
+ this.updateShadowDOM(this.compileTemplate());
223
+ this.attachEventListeners();
224
+ this.renderChildren();
225
+ this.notifyChildrenOfStructureChange();
226
+ }
227
+ removeChild(childId) {
228
+ const childIndex = this.children.findIndex((c) => c.componentId === childId);
229
+ if (childIndex !== -1) {
230
+ const child = this.children[childIndex];
231
+ child.parent = undefined;
232
+ this.children.splice(childIndex, 1);
233
+ this.renderBus.emit("render-request", {
234
+ changeType: "child-removed",
235
+ source: this.componentId,
236
+ childId: childId,
237
+ });
238
+ this.notifyChildrenOfStructureChange();
239
+ }
240
+ }
241
+ addSibling(sibling) {
242
+ sibling.parent = this.parent;
243
+ this.parent?.children.push(sibling);
244
+ // Force immediate re-render when child is added
245
+ this.updateShadowDOM(this.compileTemplate());
246
+ this.attachEventListeners();
247
+ this.renderChildren();
248
+ this.notifyChildrenOfStructureChange();
249
+ }
250
+ removeSibling(siblingId) {
251
+ const me = this;
252
+ if (me.parent) {
253
+ const siblingIndex = me.parent?.children.findIndex((c) => c.componentId === siblingId);
254
+ if (siblingIndex !== -1) {
255
+ const sibling = me.parent?.children[siblingIndex];
256
+ if (sibling) {
257
+ sibling.parent = undefined;
258
+ me.parent.children.splice(siblingIndex, 1);
259
+ }
260
+ this.renderBus.emit("render-request", {
261
+ changeType: "child-removed",
262
+ source: me.componentId,
263
+ childId: siblingId,
264
+ });
265
+ this.notifyChildrenOfStructureChange();
266
+ }
267
+ }
268
+ }
269
+ notifyChildrenOfStructureChange() {
270
+ this.children.forEach((child) => {
271
+ child.renderBus.emit("parent-changed", {
272
+ changeType: "structure-change",
273
+ parentId: this.componentId,
274
+ });
275
+ });
276
+ }
277
+ attachEventListeners() {
278
+ const actionElements = this.shadowRoot.querySelectorAll("[data-action]");
279
+ actionElements.forEach((element) => {
280
+ const action = element.getAttribute("data-action");
281
+ element.addEventListener("click", (e) => {
282
+ this.handleAction(action, e);
283
+ });
284
+ });
285
+ }
286
+ handleAction(action, event) {
287
+ this.renderBus.emit("action-triggered", {
288
+ action,
289
+ componentId: this.componentId,
290
+ event,
291
+ });
292
+ }
293
+ onBeforeRender() { }
294
+ onAfterRender() { }
295
+ shouldRerender(changeType, source) {
296
+ if (changeType && source) {
297
+ return true; // Default: always re-render
298
+ }
299
+ return true; // Default: always re-render
300
+ }
301
+ handleParentChange(changeType) {
302
+ // Default: re-render when parent changes
303
+ if (changeType) {
304
+ this.performRender();
305
+ }
306
+ this.performRender();
307
+ }
308
+ handleChildChange(childId, changeType) {
309
+ // Default: re-render when children change
310
+ if (childId && changeType) {
311
+ this.performRender();
312
+ }
313
+ this.performRender();
314
+ }
315
+ // Initial render method
316
+ performInitialRender() {
317
+ this.defineTemplate();
318
+ this.defineStyles();
319
+ this.performRender();
320
+ }
321
+ getHostElement() {
322
+ return this.hostElement;
323
+ }
324
+ getChildrenCount() {
325
+ return this.children.length;
326
+ }
327
+ // Event Manager convenience methods
328
+ /**
329
+ * Add a point interaction (click + tap) to elements matching the selector
330
+ */
331
+ point(selector, callback) {
332
+ this.eventManager.point(selector, callback);
333
+ }
334
+ /**
335
+ * Add an un-point interaction (click + tap) to elements matching the selector
336
+ */
337
+ unpoint(selector, callback) {
338
+ this.eventManager.unpoint(selector, callback);
339
+ }
340
+ /**
341
+ * Add drag interaction (mouse drag + touch drag) to elements matching the selector
342
+ */
343
+ drag(selector, callbacks) {
344
+ this.eventManager.drag(selector, callbacks);
345
+ }
346
+ /**
347
+ * Add hover interaction (mouse enter/leave + touch fallback) to elements matching the selector
348
+ */
349
+ hover(selector, callbacks) {
350
+ this.eventManager.hover(selector, callbacks);
351
+ }
352
+ /**
353
+ * Add long press interaction to elements matching the selector
354
+ */
355
+ longPress(selector, callback, duration = 500) {
356
+ this.eventManager.longPress(selector, callback, duration);
357
+ }
358
+ /**
359
+ * Add swipe gesture detection to elements matching the selector
360
+ */
361
+ swipe(selector, callbacks, threshold = 50) {
362
+ this.eventManager.swipe(selector, callbacks, threshold);
363
+ }
364
+ /**
365
+ * Add a custom event listener with automatic cleanup
366
+ */
367
+ addEventListener(element, type, listener, options) {
368
+ this.eventManager.addEventListener(element, type, listener, options);
369
+ }
370
+ /**
371
+ * Remove a specific event listener
372
+ */
373
+ removeEventListener(element, type, listener) {
374
+ this.eventManager.removeEventListener(element, type, listener);
375
+ }
376
+ // State Management convenience methods
377
+ /**
378
+ * Create and manage local component state
379
+ */
380
+ useState(key, initialValue) {
381
+ return this.stateManager.useState(key, initialValue);
382
+ }
383
+ /**
384
+ * Subscribe to and manage global application state
385
+ */
386
+ useGlobalState(key, initialValue) {
387
+ return this.stateManager.useGlobalState(key, initialValue);
388
+ }
389
+ /**
390
+ * Get current local state value without subscribing
391
+ */
392
+ getLocalState(key) {
393
+ return this.stateManager.getLocalState(key);
394
+ }
395
+ /**
396
+ * Define multiple state properties at once with initial values
397
+ * This creates reactive state properties accessible via this.state
398
+ */
399
+ defineState(initialState) {
400
+ this.stateManager.defineState(initialState);
401
+ }
402
+ /**
403
+ * Destroy the component and clean up all event listeners
404
+ */
405
+ destroy() {
406
+ if (this.stateManager) {
407
+ this.stateManager.destroy();
408
+ }
409
+ if (this.eventManager) {
410
+ this.eventManager.destroy();
411
+ }
412
+ // Clean up children
413
+ this.children.forEach((child) => child.destroy());
414
+ this.children.length = 0;
415
+ // Remove from parent
416
+ if (this.parent) {
417
+ this.parent.removeChild(this.componentId);
418
+ }
419
+ // Remove from DOM
420
+ if (this.hostElement && this.hostElement.parentNode) {
421
+ this.hostElement.parentNode.removeChild(this.hostElement);
422
+ }
423
+ }
424
+ }
425
+ // AppBuilder - Root of the hierarchy
426
+ class AppBuilder extends RenderableComponent {
427
+ pages = [];
428
+ navigationManager;
429
+ constructor(orchestrator) {
430
+ super("app-builder", orchestrator, false); // Don't bubble by default
431
+ this.navigationManager = new NavigationManager(orchestrator);
432
+ new ScreensaverManager(orchestrator, this.navigationManager);
433
+ new SettingsManager(orchestrator, this.navigationManager);
434
+ this.performInitialRender();
435
+ this.setupNavigationContainer();
436
+ }
437
+ setupNavigationContainer() {
438
+ // Make the app content container a navigation container
439
+ const appContent = this.shadowRoot.querySelector(".app-content");
440
+ if (appContent) {
441
+ appContent.classList.add("nav-container");
442
+ }
443
+ }
444
+ defineTemplate() {
445
+ this.template = /* html */ `
446
+ <div class="app">
447
+ <main class="app-content">
448
+ <div class="children-container">
449
+ <!-- Pages will be rendered here -->
450
+ </div>
451
+ </main>
452
+ </div>
453
+ `;
454
+ }
455
+ defineStyles() {
456
+ this.styles = `
457
+ :host {
458
+ display: block;
459
+ width: 100%;
460
+ height: 100vh;
461
+ }
462
+
463
+ .app {
464
+ display: flex;
465
+ flex-direction: column;
466
+ height: 100%;
467
+ font-family: Arial, sans-serif;
468
+ }
469
+
470
+ `;
471
+ }
472
+ addPage(page) {
473
+ this.pages.push(page);
474
+ this.addChild(page);
475
+ // Register page with navigation manager
476
+ this.navigationManager.registerPage(page);
477
+ }
478
+ removePage(pageId) {
479
+ const pageIndex = this.pages.findIndex((p) => p.componentId === pageId);
480
+ if (pageIndex !== -1) {
481
+ this.pages.splice(pageIndex, 1);
482
+ this.removeChild(pageId);
483
+ }
484
+ }
485
+ navigateToPage(pageId, config) {
486
+ this.orchestrator.navigateToPage(pageId, config);
487
+ }
488
+ navigateToView(viewId, config) {
489
+ this.orchestrator.navigateToView(viewId, config);
490
+ }
491
+ getCurrentPageId() {
492
+ return this.navigationManager.getCurrentPageId();
493
+ }
494
+ getCurrentViewId() {
495
+ return this.navigationManager.getCurrentViewId();
496
+ }
497
+ isTransitioning() {
498
+ return this.navigationManager.isTransitioning();
499
+ }
500
+ /**
501
+ * Add a screensaver page with the specified configuration
502
+ * The screensaver page can contain multiple views for rich interactive experiences
503
+ * @param screensaverPage The page to use as the screensaver (can contain multiple views)
504
+ * @param config Configuration object for the screensaver (excluding 'page' property)
505
+ */
506
+ addScreensaver(screensaverPage, config) {
507
+ // Add the screensaver page to the app
508
+ this.addPage(screensaverPage);
509
+ // Register the screensaver with the screensaver manager via event bus
510
+ const screensaverBus = this.orchestrator.getEventBus("screensaver-manager");
511
+ if (screensaverBus) {
512
+ screensaverBus.emit("register-screensaver", {
513
+ config: {
514
+ ...config,
515
+ page: screensaverPage,
516
+ },
517
+ });
518
+ }
519
+ else {
520
+ throw new Error("Screensaver manager not found. Ensure AppBuilder is properly initialized.");
521
+ }
522
+ }
523
+ attachToDom() {
524
+ const appContainer = document.getElementById("app");
525
+ if (appContainer) {
526
+ appContainer.appendChild(this.getHostElement());
527
+ }
528
+ else {
529
+ // Fallback to body if #app doesn't exist
530
+ document.body.appendChild(this.getHostElement());
531
+ }
532
+ }
533
+ }
534
+ // Page - Contains Views
535
+ class Page extends RenderableComponent {
536
+ views = [];
537
+ constructor(id, orchestrator, bubbleChanges = true, customTemplate, customStyles) {
538
+ super(id, orchestrator, bubbleChanges);
539
+ // Set custom template/styles before initial render if provided
540
+ if (customTemplate) {
541
+ this.properties.set("template", customTemplate);
542
+ }
543
+ if (customStyles) {
544
+ this.properties.set("styles", customStyles);
545
+ }
546
+ this.performInitialRender();
547
+ }
548
+ defineTemplate() {
549
+ // Check if a custom template was set via setProperty
550
+ const customTemplate = this.getProperty("template");
551
+ if (customTemplate) {
552
+ this.template = customTemplate;
553
+ }
554
+ else {
555
+ // Use default template
556
+ this.template = `
557
+ <div class="page" data-page-id="${this.componentId}">
558
+ <div class="page-header">
559
+ <h2>{{pageTitle}}</h2>
560
+ <div class="page-actions">
561
+ {{#if showActions}}
562
+ <button data-action="edit" class="btn">Edit Page</button>
563
+ <button data-action="delete" class="btn danger">Delete Page</button>
564
+ {{/if}}
565
+ </div>
566
+ </div>
567
+ <div class="page-content">
568
+ <div class="children-container">
569
+ <!-- Views will be rendered here -->
570
+ </div>
571
+ </div>
572
+ </div>
573
+ `;
574
+ }
575
+ }
576
+ defineStyles() {
577
+ // Check if custom styles were set via setProperty
578
+ const customStyles = this.getProperty("styles");
579
+ if (customStyles) {
580
+ this.styles = customStyles;
581
+ }
582
+ else {
583
+ // Use default styles
584
+ this.styles = `
585
+ :host {
586
+ display: block;
587
+ margin-bottom: 2rem;
588
+ }
589
+
590
+ .page {
591
+ border: 2px solid #3498db;
592
+ border-radius: 8px;
593
+ padding: 1rem;
594
+ background: #ecf0f1;
595
+ }
596
+
597
+ .page-header {
598
+ display: flex;
599
+ justify-content: space-between;
600
+ align-items: center;
601
+ margin-bottom: 1rem;
602
+ padding-bottom: 0.5rem;
603
+ border-bottom: 1px solid #bdc3c7;
604
+ }
605
+
606
+ .page-actions {
607
+ display: flex;
608
+ gap: 0.5rem;
609
+ }
610
+
611
+ .btn {
612
+ padding: 0.5rem 1rem;
613
+ border: none;
614
+ border-radius: 4px;
615
+ cursor: pointer;
616
+ background: #3498db;
617
+ color: white;
618
+ }
619
+
620
+ .btn.danger {
621
+ background: #e74c3c;
622
+ }
623
+
624
+ .btn:hover {
625
+ opacity: 0.8;
626
+ }
627
+ `;
628
+ }
629
+ }
630
+ addView(view) {
631
+ this.views.push(view);
632
+ this.addChild(view);
633
+ // Register view with navigation manager via event bus
634
+ const navBus = this.orchestrator.getEventBus("navigation-manager");
635
+ if (navBus) {
636
+ navBus.emit("register-view", { view });
637
+ }
638
+ }
639
+ removeView(viewId) {
640
+ const viewIndex = this.views.findIndex((v) => v.componentId === viewId);
641
+ if (viewIndex !== -1) {
642
+ this.views.splice(viewIndex, 1);
643
+ this.removeChild(viewId);
644
+ }
645
+ }
646
+ /**
647
+ * Add a screensaver page with the specified configuration
648
+ * @param screensaverPage The page to use as the screensaver (can contain multiple views)
649
+ * @param config Configuration object for the screensaver (excluding 'page' property)
650
+ */
651
+ addScreensaver(screensaverPage, config) {
652
+ // Register the screensaver with the screensaver manager via event bus
653
+ const screensaverBus = this.orchestrator.getEventBus("screensaver-manager");
654
+ if (screensaverBus) {
655
+ screensaverBus.emit("register-screensaver", {
656
+ config: {
657
+ ...config,
658
+ page: screensaverPage,
659
+ },
660
+ });
661
+ }
662
+ else {
663
+ throw new Error("Screensaver manager not found. Ensure AppBuilder is properly initialized.");
664
+ }
665
+ }
666
+ /**
667
+ * Add a hidden settings page to this page with the specified configuration
668
+ * The settings page is activated by touching corners in sequence: top-left, top-right, bottom-right
669
+ * @param view The view to use as the settings page
670
+ * @param config Configuration object for the settings page
671
+ */
672
+ addSettings(view, config) {
673
+ // First add the view to this page (but don't register it with navigation yet)
674
+ this.views.push(view);
675
+ this.addChild(view);
676
+ // Register the settings with the settings manager via event bus
677
+ const settingsBus = this.orchestrator.getEventBus("settings-manager");
678
+ if (settingsBus) {
679
+ settingsBus.emit("register-settings", {
680
+ config: {
681
+ ...config,
682
+ view: view,
683
+ },
684
+ });
685
+ }
686
+ else {
687
+ throw new Error("Settings manager not found. Ensure AppBuilder is properly initialized.");
688
+ }
689
+ }
690
+ }
691
+ // View - Contains Components
692
+ class View extends RenderableComponent {
693
+ components = [];
694
+ constructor(id, orchestrator, bubbleChanges = false, customTemplate, customStyles) {
695
+ super(id, orchestrator, bubbleChanges);
696
+ // Set custom template/styles before initial render if provided
697
+ if (customTemplate) {
698
+ this.properties.set("template", customTemplate);
699
+ }
700
+ if (customStyles) {
701
+ this.properties.set("styles", customStyles);
702
+ }
703
+ this.performInitialRender();
704
+ }
705
+ defineTemplate() {
706
+ // Check if a custom template was set via setProperty
707
+ const customTemplate = this.getProperty("template");
708
+ if (customTemplate) {
709
+ this.template = customTemplate;
710
+ }
711
+ else {
712
+ // Use default template
713
+ this.template = `
714
+ <div class="view" data-view-id="${this.componentId}">
715
+ <div class="view-header">
716
+ <h3>{{viewTitle}}</h3>
717
+ <span class="view-type">{{viewType}}</span>
718
+ </div>
719
+ <div class="view-content">
720
+ <div class="children-container">
721
+ <!-- Components will be rendered here -->
722
+ </div>
723
+ </div>
724
+ </div>
725
+ `;
726
+ }
727
+ }
728
+ defineStyles() {
729
+ // Check if custom styles were set via setProperty
730
+ const customStyles = this.getProperty("styles");
731
+ if (customStyles) {
732
+ this.styles = customStyles;
733
+ }
734
+ else {
735
+ // Use default styles
736
+ this.styles = `
737
+ :host {
738
+ display: block;
739
+ margin-bottom: 1rem;
740
+ }
741
+
742
+ .view {
743
+ border: 1px solid #95a5a6;
744
+ border-radius: 6px;
745
+ padding: 0.75rem;
746
+ background: white;
747
+ }
748
+
749
+ .view-header {
750
+ display: flex;
751
+ justify-content: space-between;
752
+ align-items: center;
753
+ margin-bottom: 0.75rem;
754
+ padding-bottom: 0.25rem;
755
+ border-bottom: 1px solid #ecf0f1;
756
+ }
757
+
758
+ .view-type {
759
+ background: #95a5a6;
760
+ color: white;
761
+ padding: 0.25rem 0.5rem;
762
+ border-radius: 3px;
763
+ font-size: 0.8rem;
764
+ }
765
+ `;
766
+ }
767
+ }
768
+ addComponent(component) {
769
+ this.components.push(component);
770
+ this.addChild(component);
771
+ }
772
+ // TODO: Make this actually remove the component from the DOM.
773
+ removeComponent(componentId) {
774
+ logger.trace(`Removing component with ID: ${componentId}`);
775
+ const componentIndex = this.components.findIndex((c) => {
776
+ c.componentId === componentId;
777
+ });
778
+ if (componentIndex !== -1) {
779
+ this.components.splice(componentIndex, 1);
780
+ this.removeChild(componentId);
781
+ }
782
+ }
783
+ }
784
+ // Component - Leaf nodes in the hierarchy
785
+ class Component extends RenderableComponent {
786
+ constructor(id, orchestrator, bubbleChanges = false, customTemplate, customStyles) {
787
+ super(id, orchestrator, bubbleChanges);
788
+ if (customTemplate) {
789
+ this.properties.set("template", customTemplate);
790
+ }
791
+ if (customStyles) {
792
+ this.properties.set("styles", customStyles);
793
+ }
794
+ this.performInitialRender();
795
+ }
796
+ defineTemplate() {
797
+ // Check if a custom template was set via setProperty
798
+ const customTemplate = this.getProperty("template");
799
+ if (customTemplate) {
800
+ this.template = customTemplate;
801
+ }
802
+ else {
803
+ // Use default template
804
+ this.template = `
805
+ <div class="component-container" data-component-id="${this.componentId}">
806
+ <div class="component-header">
807
+ <h4>{{title}}</h4>
808
+ <span class="component-status {{statusClass}}">{{status}}</span>
809
+ </div>
810
+ <div class="component-body">
811
+ <p class="description">{{description}}</p>
812
+ {{#if showContent}}
813
+ <div class="dynamic-content">
814
+ {{content}}
815
+ </div>
816
+ {{else}}
817
+ <div class="no-content-message">
818
+ No content available
819
+ </div>
820
+ {{/if}}
821
+ {{#ifnot isDisabled}}
822
+ <div class="component-controls">
823
+ <button class="control-btn" data-action="toggle">Toggle</button>
824
+ </div>
825
+ {{else}}
826
+ <div class="disabled-message">
827
+ Component is disabled
828
+ </div>
829
+ {{/ifnot}}
830
+ </div>
831
+ <div class="component-actions">
832
+ <button class="action-btn" data-action="{{buttonAction}}">
833
+ {{buttonText}}
834
+ </button>
835
+ </div>
836
+ <div class="children-container">
837
+ <!-- Child components will be inserted here -->
838
+ </div>
839
+ </div>
840
+ `;
841
+ }
842
+ }
843
+ defineStyles() {
844
+ // Check if custom styles were set via setProperty
845
+ const customStyles = this.getProperty("styles");
846
+ if (customStyles) {
847
+ this.styles = customStyles;
848
+ }
849
+ else {
850
+ // Use default styles
851
+ this.styles = `
852
+ :host {
853
+ display: block;
854
+ margin-bottom: 0.5rem;
855
+ }
856
+
857
+ .component-container {
858
+ border: 1px solid #ddd;
859
+ border-radius: 4px;
860
+ padding: 0.5rem;
861
+ background: #f9f9f9;
862
+ font-family: Arial, sans-serif;
863
+ }
864
+
865
+ .component-header {
866
+ display: flex;
867
+ justify-content: space-between;
868
+ align-items: center;
869
+ margin-bottom: 0.5rem;
870
+ }
871
+
872
+ .component-header h4 {
873
+ margin: 0;
874
+ color: #2c3e50;
875
+ }
876
+
877
+ .component-status {
878
+ padding: 0.2rem 0.4rem;
879
+ border-radius: 3px;
880
+ font-size: 0.7rem;
881
+ font-weight: bold;
882
+ }
883
+
884
+ .component-status.active {
885
+ background: #2ecc71;
886
+ color: white;
887
+ }
888
+
889
+ .component-status.inactive {
890
+ background: #95a5a6;
891
+ color: white;
892
+ }
893
+
894
+ .dynamic-content {
895
+ background: #f5f5f5;
896
+ padding: 0.5rem;
897
+ border-radius: 4px;
898
+ margin: 0.5rem 0;
899
+ }
900
+
901
+ .action-btn {
902
+ background: #3498db;
903
+ color: white;
904
+ border: none;
905
+ padding: 0.4rem 0.8rem;
906
+ border-radius: 4px;
907
+ cursor: pointer;
908
+ font-size: 0.8rem;
909
+ }
910
+
911
+ .action-btn:hover {
912
+ background: #2980b9;
913
+ }
914
+
915
+ .no-content-message {
916
+ background: #f8f9fa;
917
+ color: #6c757d;
918
+ padding: 0.5rem;
919
+ border-radius: 4px;
920
+ font-style: italic;
921
+ text-align: center;
922
+ margin: 0.5rem 0;
923
+ }
924
+
925
+ .component-controls {
926
+ margin: 0.5rem 0;
927
+ }
928
+
929
+ .control-btn {
930
+ background: #28a745;
931
+ color: white;
932
+ border: none;
933
+ padding: 0.3rem 0.6rem;
934
+ border-radius: 3px;
935
+ cursor: pointer;
936
+ font-size: 0.75rem;
937
+ }
938
+
939
+ .control-btn:hover {
940
+ background: #218838;
941
+ }
942
+
943
+ .disabled-message {
944
+ background: #f8d7da;
945
+ color: #721c24;
946
+ padding: 0.5rem;
947
+ border-radius: 4px;
948
+ font-style: italic;
949
+ text-align: center;
950
+ margin: 0.5rem 0;
951
+ border: 1px solid #f5c6cb;
952
+ }
953
+ `;
954
+ }
955
+ }
956
+ }
957
+ export { AppBuilder, Page, View, Component, RenderableComponent, EventBus };