@adaas/are-html 0.0.23 → 0.0.24

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 (81) hide show
  1. package/dist/browser/index.d.mts +18 -2
  2. package/dist/browser/index.mjs +35 -10
  3. package/dist/browser/index.mjs.map +1 -1
  4. package/dist/node/directives/AreDirectiveIf.directive.d.mts +17 -1
  5. package/dist/node/directives/AreDirectiveIf.directive.d.ts +17 -1
  6. package/dist/node/directives/AreDirectiveIf.directive.js +29 -6
  7. package/dist/node/directives/AreDirectiveIf.directive.js.map +1 -1
  8. package/dist/node/directives/AreDirectiveIf.directive.mjs +29 -6
  9. package/dist/node/directives/AreDirectiveIf.directive.mjs.map +1 -1
  10. package/dist/node/engine/AreHTML.compiler.d.mts +3 -1
  11. package/dist/node/engine/AreHTML.compiler.d.ts +3 -1
  12. package/dist/node/engine/AreHTML.compiler.js +7 -4
  13. package/dist/node/engine/AreHTML.compiler.js.map +1 -1
  14. package/dist/node/engine/AreHTML.compiler.mjs +7 -4
  15. package/dist/node/engine/AreHTML.compiler.mjs.map +1 -1
  16. package/examples/for-perf/dist/index.html +1 -1
  17. package/examples/for-perf/dist/{mqj1mpf2-z4aokv.js → mqp8i2py-vltsx0.js} +2488 -2373
  18. package/examples/lazy-loading/README.md +76 -0
  19. package/examples/lazy-loading/concept.ts +55 -0
  20. package/examples/lazy-loading/containers/UI.container.ts +215 -0
  21. package/examples/lazy-loading/dist/app.js +3803 -0
  22. package/examples/{for-perf/dist/mqj1mpff-4fr7mw.js → lazy-loading/dist/chunks/chunk-6K72IBO4.js} +2688 -5897
  23. package/examples/lazy-loading/dist/index.html +36 -0
  24. package/examples/lazy-loading/dist/lazy/about-page.js +59 -0
  25. package/examples/lazy-loading/dist/lazy/reports-page.js +65 -0
  26. package/examples/lazy-loading/dist/lazy/settings-page.js +54 -0
  27. package/examples/lazy-loading/public/index.html +36 -0
  28. package/examples/lazy-loading/src/components/AppShell.component.ts +44 -0
  29. package/examples/lazy-loading/src/components/HomePage.component.ts +59 -0
  30. package/examples/lazy-loading/src/components/LazyOutlet.component.ts +108 -0
  31. package/examples/lazy-loading/src/components/NavBar.component.ts +98 -0
  32. package/examples/lazy-loading/src/concept.ts +116 -0
  33. package/examples/lazy-loading/src/lazy/AboutPage.component.ts +54 -0
  34. package/examples/lazy-loading/src/lazy/ReportsPage.component.ts +56 -0
  35. package/examples/lazy-loading/src/lazy/SettingsPage.component.ts +45 -0
  36. package/examples/lazy-loading/src/runtime/ComponentManifest.fragment.ts +61 -0
  37. package/examples/lazy-loading/src/runtime/LazyComponentResolver.fragment.ts +77 -0
  38. package/examples/os-desktop/README.md +91 -0
  39. package/examples/os-desktop/concept.ts +54 -0
  40. package/examples/os-desktop/containers/OS.container.ts +198 -0
  41. package/examples/os-desktop/containers/apps/AppBackend.ts +29 -0
  42. package/examples/os-desktop/containers/apps/GanttApp.backend.ts +56 -0
  43. package/examples/os-desktop/containers/apps/MarketingApp.backend.ts +68 -0
  44. package/examples/os-desktop/dist/app.js +4410 -0
  45. package/examples/os-desktop/dist/apps/gantt/app.js +271 -0
  46. package/examples/os-desktop/dist/apps/marketing/app.js +346 -0
  47. package/examples/os-desktop/dist/chunks/chunk-6K72IBO4.js +12455 -0
  48. package/examples/os-desktop/dist/chunks/chunk-EIIGUL6N.js +30 -0
  49. package/examples/os-desktop/dist/chunks/chunk-WOH7L5UR.js +30 -0
  50. package/examples/os-desktop/dist/index.html +33 -0
  51. package/examples/os-desktop/public/index.html +33 -0
  52. package/examples/os-desktop/src/apps/gantt/GanttApp.component.ts +41 -0
  53. package/examples/os-desktop/src/apps/gantt/GanttChart.component.ts +126 -0
  54. package/examples/os-desktop/src/apps/gantt/GanttStore.ts +47 -0
  55. package/examples/os-desktop/src/apps/gantt/GanttToolbar.component.ts +73 -0
  56. package/examples/os-desktop/src/apps/gantt/index.ts +13 -0
  57. package/examples/os-desktop/src/apps/marketing/MarketingApp.component.ts +53 -0
  58. package/examples/os-desktop/src/apps/marketing/MarketingStore.ts +34 -0
  59. package/examples/os-desktop/src/apps/marketing/PostEditor.component.ts +153 -0
  60. package/examples/os-desktop/src/apps/marketing/PostPreview.component.ts +110 -0
  61. package/examples/os-desktop/src/apps/marketing/index.ts +16 -0
  62. package/examples/os-desktop/src/concept.ts +126 -0
  63. package/examples/os-desktop/src/os/AppStage.component.ts +112 -0
  64. package/examples/os-desktop/src/os/AppWindow.component.ts +102 -0
  65. package/examples/os-desktop/src/os/Desktop.component.ts +106 -0
  66. package/examples/os-desktop/src/os/Dock.component.ts +174 -0
  67. package/examples/os-desktop/src/os/Hud.component.ts +83 -0
  68. package/examples/os-desktop/src/os/Launchpad.component.ts +191 -0
  69. package/examples/os-desktop/src/os/MenuBar.component.ts +156 -0
  70. package/examples/os-desktop/src/runtime/AppComponentResolver.fragment.ts +121 -0
  71. package/examples/os-desktop/src/runtime/AppRegistry.fragment.ts +104 -0
  72. package/examples/os-desktop/src/signals/MouseState.signal.ts +34 -0
  73. package/examples/os-desktop/src/signals/OSRoute.signal.ts +37 -0
  74. package/examples/os-desktop/src/signals/SelectionState.signal.ts +34 -0
  75. package/examples/signal-routing/dist/index.html +1 -1
  76. package/examples/signal-routing/dist/{mqiwo23h-bhcolu.js → mqp8hgce-4d6rh0.js} +2911 -2708
  77. package/package.json +11 -7
  78. package/src/directives/AreDirectiveIf.directive.ts +33 -4
  79. package/src/engine/AreHTML.compiler.ts +12 -2
  80. package/tests/PropPropagation.test.ts +181 -0
  81. package/tests/jest.setup.ts +11 -0
@@ -0,0 +1,30 @@
1
+ import {
2
+ AreSignal,
3
+ R2 as R,
4
+ __decorateClass,
5
+ __name
6
+ } from "./chunk-6K72IBO4.js";
7
+
8
+ // examples/os-desktop/src/signals/MouseState.signal.ts
9
+ var MouseState = class extends AreSignal {
10
+ constructor(x, y) {
11
+ super({ data: { x, y } });
12
+ }
13
+ get x() {
14
+ return this.data.x;
15
+ }
16
+ get y() {
17
+ return this.data.y;
18
+ }
19
+ };
20
+ __name(MouseState, "MouseState");
21
+ MouseState = __decorateClass([
22
+ R.Define({
23
+ namespace: "a-are-os-desktop",
24
+ description: "Live pointer-position state signal {x, y}. Emitted throttled from a global mousemove listener and consumed by the HUD via a typed @Are.Signal(MouseState) handler."
25
+ })
26
+ ], MouseState);
27
+
28
+ export {
29
+ MouseState
30
+ };
@@ -0,0 +1,30 @@
1
+ import {
2
+ AreSignal,
3
+ R2 as R,
4
+ __decorateClass,
5
+ __name
6
+ } from "./chunk-6K72IBO4.js";
7
+
8
+ // examples/os-desktop/src/signals/SelectionState.signal.ts
9
+ var SelectionState = class extends AreSignal {
10
+ constructor(text) {
11
+ super({ data: { text, length: text.length } });
12
+ }
13
+ get text() {
14
+ return this.data.text;
15
+ }
16
+ get length() {
17
+ return this.data.length;
18
+ }
19
+ };
20
+ __name(SelectionState, "SelectionState");
21
+ SelectionState = __decorateClass([
22
+ R.Define({
23
+ namespace: "a-are-os-desktop",
24
+ description: "Current text-selection state signal {text, length}. Emitted from a global selectionchange listener and consumed independently by the OS HUD and the Marketing app."
25
+ })
26
+ ], SelectionState);
27
+
28
+ export {
29
+ SelectionState
30
+ };
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ARE · OS Desktop</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+ html, body { height: 100%; }
10
+ body { background: #050410; color: #f5f5f7; overflow: hidden; }
11
+
12
+ /* The mount root is a real block-level container. */
13
+ are-root { display: block; height: 100%; }
14
+
15
+ /*
16
+ * ARE wraps every component (and interpolated text) in a custom tag. Left
17
+ * as the default `display: inline`, those wrappers — and the empty
18
+ * <are-text> whitespace nodes between siblings — would become the actual
19
+ * fl/grid items and break layout. `display: contents` makes each wrapper's
20
+ * box disappear while keeping its children in the parent's flow.
21
+ */
22
+ os-desktop, menu-bar, app-stage, os-hud, os-dock, os-launchpad,
23
+ app-window, marketing-app, post-editor, post-preview,
24
+ gantt-app, gantt-toolbar, gantt-chart, are-text { display: contents; }
25
+ </style>
26
+ </head>
27
+ <body>
28
+ <!-- Outer ARE root renders the persistent OS shell (desktop + dock + bars). -->
29
+ <are-root id="os"><os-desktop></os-desktop></are-root>
30
+
31
+ <script type="module" src="./app.js"></script>
32
+ </body>
33
+ </html>
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ARE · OS Desktop</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+ html, body { height: 100%; }
10
+ body { background: #050410; color: #f5f5f7; overflow: hidden; }
11
+
12
+ /* The mount root is a real block-level container. */
13
+ are-root { display: block; height: 100%; }
14
+
15
+ /*
16
+ * ARE wraps every component (and interpolated text) in a custom tag. Left
17
+ * as the default `display: inline`, those wrappers — and the empty
18
+ * <are-text> whitespace nodes between siblings — would become the actual
19
+ * fl/grid items and break layout. `display: contents` makes each wrapper's
20
+ * box disappear while keeping its children in the parent's flow.
21
+ */
22
+ os-desktop, menu-bar, app-stage, os-hud, os-dock, os-launchpad,
23
+ app-window, marketing-app, post-editor, post-preview,
24
+ gantt-app, gantt-toolbar, gantt-chart, are-text { display: contents; }
25
+ </style>
26
+ </head>
27
+ <body>
28
+ <!-- Outer ARE root renders the persistent OS shell (desktop + dock + bars). -->
29
+ <are-root id="os"><os-desktop></os-desktop></are-root>
30
+
31
+ <script type="module" src="./app.js"></script>
32
+ </body>
33
+ </html>
@@ -0,0 +1,41 @@
1
+ import { A_Caller, A_Inject } from "@adaas/a-concept";
2
+ import { Are, AreNode } from "@adaas/are";
3
+ import { AreHTMLNode } from "src";
4
+
5
+
6
+ /**
7
+ * GanttApp — root of the Gantt app bundle. Composes the app's own component set:
8
+ * a `<gantt-toolbar>` and a `<gantt-chart>`. Both are resolved lazily out of the
9
+ * same bundle by the AppComponentResolver.
10
+ */
11
+ export class GanttApp extends Are {
12
+
13
+ @Are.Template
14
+ template(@A_Inject(A_Caller) node: AreNode) {
15
+ node.setContent(`
16
+ <div class="gt">
17
+ <div class="gt-head">
18
+ <h2>Project Timeline</h2>
19
+ <span class="gt-be">backend · <code>/apps/gantt/api</code></span>
20
+ </div>
21
+ <gantt-toolbar></gantt-toolbar>
22
+ <gantt-chart></gantt-chart>
23
+ </div>
24
+ `);
25
+ }
26
+
27
+ @Are.Styles
28
+ styles(@A_Inject(A_Caller) node: AreHTMLNode) {
29
+ node.setStyles(`
30
+ gantt-toolbar, gantt-chart { display: block; }
31
+ .gt { display: flex; flex-direction: column; height: 100%; color: #ececf1; }
32
+ .gt-head {
33
+ display: flex; align-items: baseline; justify-content: space-between;
34
+ padding: 18px 24px; border-bottom: 1px solid rgba(255,255,255,0.07);
35
+ }
36
+ .gt-head h2 { font-size: 18px; font-weight: 700; }
37
+ .gt-be { font-size: 11px; color: #7fa0c8; }
38
+ .gt-be code { color: #b8d4ff; }
39
+ `);
40
+ }
41
+ }
@@ -0,0 +1,126 @@
1
+ import { A_Caller, A_Inject } from "@adaas/a-concept";
2
+ import { A_Logger } from "@adaas/a-utils/a-logger";
3
+ import { Are, AreNode } from "@adaas/are";
4
+ import { AreHTMLNode } from "src";
5
+ import { GanttStore } from "./GanttStore";
6
+ import { MouseState } from "../../signals/MouseState.signal";
7
+
8
+
9
+ const TOTAL_DAYS = 30;
10
+ const ROW_H = 30;
11
+
12
+
13
+ /**
14
+ * GanttChart — the Gantt app's visualization.
15
+ *
16
+ * On mount it loads tasks from the app's OWN backend (`/apps/gantt/api/tasks`),
17
+ * mirrors the app's {@link GanttStore}, and re-paints its bars imperatively so
18
+ * the toolbar can add tasks without a full re-render. It also consumes the
19
+ * OS-wide {@link MouseState} signal to glide a vertical guide line under the
20
+ * pointer — the same signal the HUD reads, proving signals fan out to any
21
+ * component that cares.
22
+ */
23
+ export class GanttChart extends Are {
24
+
25
+ protected _unsubscribe?: () => void;
26
+
27
+ @Are.Template
28
+ template(@A_Inject(A_Caller) node: AreNode) {
29
+ const cols = Array.from({ length: TOTAL_DAYS / 5 }, (_, i) =>
30
+ `<div class="gc-col"><span>d${i * 5}</span></div>`).join('');
31
+
32
+ node.setContent(`
33
+ <div class="gc">
34
+ <div class="gc-grid">${cols}</div>
35
+ <div class="gc-bars"></div>
36
+ <div class="gc-cursor"></div>
37
+ </div>
38
+ `);
39
+ }
40
+
41
+ @Are.onAfterMount
42
+ async onMount(@A_Inject(A_Logger) logger: A_Logger) {
43
+ this._unsubscribe = GanttStore.subscribe(() => this.paint());
44
+
45
+ try {
46
+ const res = await fetch('/apps/gantt/api/tasks');
47
+ const data = await res.json();
48
+ GanttStore.set(Array.isArray(data.tasks) ? data.tasks : []);
49
+ } catch (error) {
50
+ logger.error(error);
51
+ this.paint();
52
+ }
53
+ }
54
+
55
+ @Are.onBeforeUnmount
56
+ onUnmount() {
57
+ this._unsubscribe?.();
58
+ this._unsubscribe = undefined;
59
+ }
60
+
61
+ @Are.Signal(MouseState)
62
+ onMouse(@A_Inject(MouseState) signal: MouseState) {
63
+ const cursor = document.querySelector('.gc-cursor') as HTMLElement | null;
64
+ if (!cursor || !cursor.parentElement) return;
65
+ const rect = cursor.parentElement.getBoundingClientRect();
66
+ const x = Math.max(0, Math.min(signal.x - rect.left, rect.width));
67
+ cursor.style.left = `${x}px`;
68
+ }
69
+
70
+ protected paint() {
71
+ const bars = document.querySelector('.gc-bars') as HTMLElement | null;
72
+ if (!bars) return;
73
+
74
+ bars.style.height = `${Math.max(1, GanttStore.tasks.length) * ROW_H + 8}px`;
75
+ bars.innerHTML = GanttStore.tasks.map(task => {
76
+ const left = (task.start / TOTAL_DAYS) * 100;
77
+ const width = ((task.end - task.start) / TOTAL_DAYS) * 100;
78
+ const top = task.track * ROW_H + 4;
79
+ return `
80
+ <div class="gc-bar" style="left:${left}%;width:${width}%;top:${top}px;background:${task.color}">
81
+ <span>${task.name}</span>
82
+ </div>
83
+ `;
84
+ }).join('');
85
+ }
86
+
87
+ @Are.Styles
88
+ styles(@A_Inject(A_Caller) node: AreHTMLNode) {
89
+ node.setStyles(`
90
+ .gc {
91
+ position: relative;
92
+ flex: 1;
93
+ margin: 18px 24px;
94
+ border-radius: 12px;
95
+ background: rgba(0,0,0,0.22);
96
+ border: 1px solid rgba(255,255,255,0.08);
97
+ overflow: hidden;
98
+ }
99
+ .gc-grid { position: absolute; inset: 0; display: flex; }
100
+ .gc-col {
101
+ flex: 1;
102
+ border-right: 1px dashed rgba(255,255,255,0.07);
103
+ padding: 6px 8px;
104
+ }
105
+ .gc-col span { font-size: 10px; color: #6f7a8c; font-family: ui-monospace, monospace; }
106
+ .gc-bars { position: relative; margin-top: 26px; }
107
+ .gc-bar {
108
+ position: absolute;
109
+ height: ${ROW_H - 8}px;
110
+ border-radius: 6px;
111
+ display: flex; align-items: center;
112
+ padding: 0 10px;
113
+ color: white; font-size: 12px; font-weight: 600;
114
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
115
+ overflow: hidden; white-space: nowrap;
116
+ transition: left 0.2s, width 0.2s;
117
+ }
118
+ .gc-cursor {
119
+ position: absolute; top: 0; bottom: 0; left: 0; width: 2px;
120
+ background: rgba(255,255,255,0.55);
121
+ pointer-events: none;
122
+ box-shadow: 0 0 10px rgba(255,255,255,0.5);
123
+ }
124
+ `);
125
+ }
126
+ }
@@ -0,0 +1,47 @@
1
+ export interface GanttTask {
2
+ id: string;
3
+ name: string;
4
+ start: number; // day offset from project start
5
+ end: number; // day offset (exclusive)
6
+ color: string;
7
+ track: number; // row index
8
+ }
9
+
10
+
11
+ /**
12
+ * GanttStore — the Gantt app's OWN internal state (its task list).
13
+ *
14
+ * Like the Marketing app's store, this is private to the Gantt bundle and is
15
+ * how the toolbar (writer) and the chart (reader) stay in sync without touching
16
+ * the OS signal bus.
17
+ */
18
+ class GanttStoreImpl {
19
+
20
+ protected _tasks: GanttTask[] = [];
21
+ protected _listeners: Set<() => void> = new Set();
22
+
23
+ get tasks(): GanttTask[] {
24
+ return this._tasks;
25
+ }
26
+
27
+ set(tasks: GanttTask[]): void {
28
+ this._tasks = tasks;
29
+ this.emit();
30
+ }
31
+
32
+ add(task: GanttTask): void {
33
+ this._tasks = [...this._tasks, task];
34
+ this.emit();
35
+ }
36
+
37
+ subscribe(listener: () => void): () => void {
38
+ this._listeners.add(listener);
39
+ return () => this._listeners.delete(listener);
40
+ }
41
+
42
+ protected emit(): void {
43
+ for (const listener of this._listeners) listener();
44
+ }
45
+ }
46
+
47
+ export const GanttStore = new GanttStoreImpl();
@@ -0,0 +1,73 @@
1
+ import { A_Caller, A_Inject } from "@adaas/a-concept";
2
+ import { A_Logger } from "@adaas/a-utils/a-logger";
3
+ import { Are, AreNode } from "@adaas/are";
4
+ import { AreHTMLNode } from "src";
5
+ import { GanttStore, GanttTask } from "./GanttStore";
6
+
7
+
8
+ const COLORS = ['#5b8def', '#34c759', '#ff9f0a', '#bf5af2', '#ff375f', '#64d2ff'];
9
+ const NAMES = ['Discovery', 'Design', 'Build', 'Review', 'QA', 'Launch', 'Retro'];
10
+
11
+
12
+ /**
13
+ * GanttToolbar — the Gantt app's controls. Writes into the app's own
14
+ * {@link GanttStore} (add a task) and can reload the task list from the app's
15
+ * OWN backend (`/apps/gantt/api/tasks`).
16
+ */
17
+ export class GanttToolbar extends Are {
18
+
19
+ @Are.Template
20
+ template(@A_Inject(A_Caller) node: AreNode) {
21
+ node.setContent(`
22
+ <div class="tb">
23
+ <button class="tb-btn tb-add" @click="$add()">+ Add task</button>
24
+ <button class="tb-btn" @click="$reload()">↻ Reload from backend</button>
25
+ <span class="tb-hint">Tasks are served by the Gantt app's own backend.</span>
26
+ </div>
27
+ `);
28
+ }
29
+
30
+ @Are.EventHandler
31
+ add() {
32
+ const tracks = GanttStore.tasks.length;
33
+ const start = Math.floor(Math.random() * 20);
34
+ const task: GanttTask = {
35
+ id: `t-${Date.now()}`,
36
+ name: NAMES[Math.floor(Math.random() * NAMES.length)],
37
+ start,
38
+ end: start + 3 + Math.floor(Math.random() * 6),
39
+ color: COLORS[tracks % COLORS.length],
40
+ track: tracks,
41
+ };
42
+ GanttStore.add(task);
43
+ }
44
+
45
+ @Are.EventHandler
46
+ async reload(@A_Inject(A_Logger) logger: A_Logger) {
47
+ try {
48
+ const res = await fetch('/apps/gantt/api/tasks');
49
+ const data = await res.json();
50
+ if (Array.isArray(data.tasks)) GanttStore.set(data.tasks);
51
+ } catch (error) {
52
+ logger.error(error);
53
+ }
54
+ }
55
+
56
+ @Are.Styles
57
+ styles(@A_Inject(A_Caller) node: AreHTMLNode) {
58
+ node.setStyles(`
59
+ .tb {
60
+ display: flex; align-items: center; gap: 10px;
61
+ padding: 14px 24px; border-bottom: 1px solid rgba(255,255,255,0.07);
62
+ }
63
+ .tb-btn {
64
+ padding: 7px 13px; border: 1px solid rgba(255,255,255,0.14);
65
+ border-radius: 9px; background: rgba(255,255,255,0.06);
66
+ color: #ececf1; font-size: 13px; font-weight: 600; cursor: pointer;
67
+ }
68
+ .tb-btn:hover { background: rgba(255,255,255,0.12); }
69
+ .tb-add { background: #2f6df6; border-color: transparent; }
70
+ .tb-hint { font-size: 11px; color: #7d889a; margin-left: auto; }
71
+ `);
72
+ }
73
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Gantt app bundle entry.
3
+ *
4
+ * The OS lazily `import()`s this module when the Gantt app is installed/opened.
5
+ * It exports the app's full component set; the AppComponentResolver selects each
6
+ * class by the `export` name listed in the descriptor (see GanttApp.backend.ts).
7
+ */
8
+ export { GanttApp } from "./GanttApp.component";
9
+ export { GanttToolbar } from "./GanttToolbar.component";
10
+ export { GanttChart } from "./GanttChart.component";
11
+
12
+ import { GanttApp } from "./GanttApp.component";
13
+ export default GanttApp;
@@ -0,0 +1,53 @@
1
+ import { A_Caller, A_Inject } from "@adaas/a-concept";
2
+ import { Are, AreNode } from "@adaas/are";
3
+ import { AreHTMLNode } from "src";
4
+
5
+
6
+ /**
7
+ * MarketingApp — the root component of the Marketing app bundle.
8
+ *
9
+ * It only composes the app's own component set: a `<post-editor>` and a
10
+ * `<post-preview>`. Those tags are not registered with the OS at boot — the
11
+ * engine resolves them through the AppComponentResolver, which pulls them from
12
+ * THIS same bundle (one bundle, three components).
13
+ */
14
+ export class MarketingApp extends Are {
15
+
16
+ @Are.Template
17
+ template(@A_Inject(A_Caller) node: AreNode) {
18
+ node.setContent(`
19
+ <div class="mk">
20
+ <div class="mk-head">
21
+ <h2>LinkedIn Post Builder</h2>
22
+ <span class="mk-be">backend · <code>/apps/marketing/api</code></span>
23
+ </div>
24
+ <div class="mk-grid">
25
+ <post-editor></post-editor>
26
+ <post-preview></post-preview>
27
+ </div>
28
+ </div>
29
+ `);
30
+ }
31
+
32
+ @Are.Styles
33
+ styles(@A_Inject(A_Caller) node: AreHTMLNode) {
34
+ node.setStyles(`
35
+ post-editor, post-preview { display: contents; }
36
+ .mk { display: flex; flex-direction: column; height: 100%; color: #ececf1; }
37
+ .mk-head {
38
+ display: flex; align-items: baseline; justify-content: space-between;
39
+ padding: 18px 24px; border-bottom: 1px solid rgba(255,255,255,0.07);
40
+ }
41
+ .mk-head h2 { font-size: 18px; font-weight: 700; }
42
+ .mk-be { font-size: 11px; color: #8b7fb0; }
43
+ .mk-be code { color: #c9b8ff; }
44
+ .mk-grid {
45
+ flex: 1;
46
+ display: grid;
47
+ grid-template-columns: 1fr 1fr;
48
+ gap: 0;
49
+ min-height: 0;
50
+ }
51
+ `);
52
+ }
53
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * MarketingStore — the Marketing app's OWN internal state.
3
+ *
4
+ * It lives entirely inside the marketing bundle and has nothing to do with the
5
+ * OS signal bus. This demonstrates that an app manages its own state however it
6
+ * likes: here a tiny observable holding the post draft, shared between the
7
+ * editor (writer) and the preview (reader). Both components ship in the same
8
+ * bundle, so they share this singleton.
9
+ */
10
+ class MarketingStoreImpl {
11
+
12
+ protected _text: string =
13
+ "Thrilled to share that we shipped runtime app loading in our OS shell! 🚀\n\nEach app is its own bundle + backend, loaded on demand. No redeploys, no reloads.";
14
+
15
+ protected _listeners: Set<() => void> = new Set();
16
+
17
+ get text(): string {
18
+ return this._text;
19
+ }
20
+
21
+ set(text: string): void {
22
+ this._text = text;
23
+ for (const listener of this._listeners) {
24
+ listener();
25
+ }
26
+ }
27
+
28
+ subscribe(listener: () => void): () => void {
29
+ this._listeners.add(listener);
30
+ return () => this._listeners.delete(listener);
31
+ }
32
+ }
33
+
34
+ export const MarketingStore = new MarketingStoreImpl();
@@ -0,0 +1,153 @@
1
+ import { A_Caller, A_Inject } from "@adaas/a-concept";
2
+ import { A_Logger } from "@adaas/a-utils/a-logger";
3
+ import { Are, AreEvent, AreNode } from "@adaas/are";
4
+ import { AreHTMLNode } from "src";
5
+ import { MarketingStore } from "./MarketingStore";
6
+
7
+
8
+ /**
9
+ * PostEditor — the writing surface of the Marketing app.
10
+ *
11
+ * It writes the draft into the app's own {@link MarketingStore} on every
12
+ * keystroke (so the preview can mirror it) and talks to the app's OWN backend
13
+ * (`/apps/marketing/api/hashtags`) to fetch suggested hashtags — proving each
14
+ * app carries its own server, not just its own UI.
15
+ */
16
+ export class PostEditor extends Are {
17
+
18
+ protected _hashtags: string[] = [];
19
+
20
+ @Are.Template
21
+ template(@A_Inject(A_Caller) node: AreNode) {
22
+ node.setContent(this.build());
23
+ }
24
+
25
+ @Are.EventHandler
26
+ edit(@A_Inject(AreEvent) event: AreEvent) {
27
+ const el = event.get('native')?.target as HTMLTextAreaElement;
28
+ if (el) MarketingStore.set(el.value);
29
+ }
30
+
31
+ @Are.EventHandler
32
+ async suggest(
33
+ @A_Inject(A_Logger) logger: A_Logger,
34
+ ) {
35
+ const btn = document.getElementById('pe-suggest') as HTMLButtonElement | null;
36
+ if (btn) { btn.disabled = true; btn.textContent = 'Asking backend…'; }
37
+
38
+ try {
39
+ const topic = encodeURIComponent(MarketingStore.text.slice(0, 60));
40
+ const res = await fetch(`/apps/marketing/api/hashtags?topic=${topic}`);
41
+ const data = await res.json();
42
+ this._hashtags = Array.isArray(data.hashtags) ? data.hashtags : [];
43
+ } catch (error) {
44
+ logger.error(error);
45
+ this._hashtags = [];
46
+ }
47
+
48
+ if (btn) { btn.disabled = false; btn.textContent = 'Suggest hashtags'; }
49
+ this.paintChips();
50
+ }
51
+
52
+ /**
53
+ * Renders the suggested-hashtag chips imperatively. Each chip appends its
54
+ * tag to the app's store (and the textarea) via a native listener — the
55
+ * editor never re-renders, so the textarea keeps its caret and focus.
56
+ */
57
+ protected paintChips() {
58
+ const host = document.getElementById('pe-chips');
59
+ if (!host) return;
60
+ host.innerHTML = '';
61
+ if (!this._hashtags.length) {
62
+ const empty = document.createElement('span');
63
+ empty.className = 'pe-empty';
64
+ empty.textContent = 'Click “Suggest” to ask the marketing backend for hashtags.';
65
+ host.appendChild(empty);
66
+ return;
67
+ }
68
+ for (const tag of this._hashtags) {
69
+ const chip = document.createElement('button');
70
+ chip.className = 'pe-chip';
71
+ chip.textContent = `#${tag}`;
72
+ chip.addEventListener('click', () => this.appendTag(tag));
73
+ host.appendChild(chip);
74
+ }
75
+ }
76
+
77
+ protected appendTag(tag: string) {
78
+ MarketingStore.set(`${MarketingStore.text} #${tag}`);
79
+ const ta = document.querySelector('.pe-text') as HTMLTextAreaElement | null;
80
+ if (ta) ta.value = MarketingStore.text;
81
+ }
82
+
83
+ protected build(): string {
84
+ return `
85
+ <section class="pe">
86
+ <label class="pe-label">Draft</label>
87
+ <textarea class="pe-text" @input="$edit()" spellcheck="false">${this.escape(MarketingStore.text)}</textarea>
88
+ <div class="pe-tools">
89
+ <button class="pe-suggest" id="pe-suggest" @click="$suggest()">Suggest hashtags</button>
90
+ </div>
91
+ <div class="pe-chips" id="pe-chips">
92
+ <span class="pe-empty">Click “Suggest” to ask the marketing backend for hashtags.</span>
93
+ </div>
94
+ </section>
95
+ `;
96
+ }
97
+
98
+ protected escape(text: string): string {
99
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
100
+ }
101
+
102
+ @Are.Styles
103
+ styles(@A_Inject(A_Caller) node: AreHTMLNode) {
104
+ node.setStyles(`
105
+ .pe {
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 12px;
109
+ padding: 22px 24px;
110
+ border-right: 1px solid rgba(255,255,255,0.07);
111
+ min-height: 0;
112
+ }
113
+ .pe-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: #8b7fb0; }
114
+ .pe-text {
115
+ flex: 1;
116
+ min-height: 200px;
117
+ resize: none;
118
+ padding: 14px 16px;
119
+ border-radius: 12px;
120
+ border: 1px solid rgba(255,255,255,0.1);
121
+ background: rgba(0,0,0,0.25);
122
+ color: #f2f2f5;
123
+ font-size: 14px;
124
+ line-height: 1.6;
125
+ font-family: inherit;
126
+ }
127
+ .pe-text:focus { outline: none; border-color: #7c6fd6; }
128
+ .pe-suggest {
129
+ padding: 9px 16px;
130
+ border: none;
131
+ border-radius: 10px;
132
+ background: #2f6df6;
133
+ color: white;
134
+ font-size: 13px;
135
+ font-weight: 600;
136
+ cursor: pointer;
137
+ }
138
+ .pe-suggest:disabled { opacity: 0.6; cursor: default; }
139
+ .pe-chips { display: flex; flex-wrap: wrap; gap: 6px; min-height: 24px; }
140
+ .pe-chip {
141
+ padding: 4px 10px;
142
+ border: 1px solid rgba(124,111,214,0.5);
143
+ border-radius: 999px;
144
+ background: rgba(124,111,214,0.14);
145
+ color: #c9b8ff;
146
+ font-size: 12px;
147
+ cursor: pointer;
148
+ }
149
+ .pe-chip:hover { background: rgba(124,111,214,0.28); }
150
+ .pe-empty { font-size: 12px; color: #6f6790; }
151
+ `);
152
+ }
153
+ }