@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,110 @@
1
+ import { A_Caller, A_Inject } from "@adaas/a-concept";
2
+ import { Are, AreNode } from "@adaas/are";
3
+ import { AreHTMLNode } from "src";
4
+ import { MarketingStore } from "./MarketingStore";
5
+ import { SelectionState } from "../../signals/SelectionState.signal";
6
+
7
+
8
+ /**
9
+ * PostPreview — the live LinkedIn-style preview of the Marketing app.
10
+ *
11
+ * It mirrors the app's own {@link MarketingStore} (updated by the editor) and
12
+ * also listens to the OS-wide {@link SelectionState} signal: whatever text the
13
+ * user selects anywhere on the desktop is offered as a hashtag. Updates are
14
+ * applied imperatively to the rendered DOM so the editor's textarea never loses
15
+ * focus and so there is no teardown to coordinate.
16
+ */
17
+ export class PostPreview extends Are {
18
+
19
+ protected _unsubscribe?: () => void;
20
+
21
+ @Are.Template
22
+ template(@A_Inject(A_Caller) node: AreNode) {
23
+ node.setContent(`
24
+ <article class="pp">
25
+ <header class="pp-top">
26
+ <div class="pp-avatar">A</div>
27
+ <div>
28
+ <div class="pp-name">ARE Platform</div>
29
+ <div class="pp-sub">Runtime · 1m · 🌐</div>
30
+ </div>
31
+ </header>
32
+ <p class="pp-body">${this.escape(MarketingStore.text)}</p>
33
+ <div class="pp-sel" hidden></div>
34
+ <footer class="pp-actions">
35
+ <span>👍 Like</span><span>💬 Comment</span><span>↪ Share</span>
36
+ </footer>
37
+ </article>
38
+ `);
39
+ }
40
+
41
+ @Are.onAfterMount
42
+ onMount() {
43
+ this._unsubscribe = MarketingStore.subscribe(() => {
44
+ const el = document.querySelector('.pp-body');
45
+ if (el) el.textContent = MarketingStore.text;
46
+ });
47
+ }
48
+
49
+ @Are.onBeforeUnmount
50
+ onUnmount() {
51
+ this._unsubscribe?.();
52
+ this._unsubscribe = undefined;
53
+ }
54
+
55
+ @Are.Signal(SelectionState)
56
+ onSelection(@A_Inject(SelectionState) signal: SelectionState) {
57
+ const el = document.querySelector('.pp-sel') as HTMLElement | null;
58
+ if (!el) return;
59
+
60
+ const text = signal.text.trim();
61
+ if (text.length) {
62
+ const tag = text.replace(/\s+/g, '').replace(/[^\w]/g, '').slice(0, 24);
63
+ el.hidden = false;
64
+ el.textContent = tag ? `Selected on desktop → add #${tag}` : `Selected ${signal.length} characters`;
65
+ } else {
66
+ el.hidden = true;
67
+ }
68
+ }
69
+
70
+ protected escape(text: string): string {
71
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
72
+ }
73
+
74
+ @Are.Styles
75
+ styles(@A_Inject(A_Caller) node: AreHTMLNode) {
76
+ node.setStyles(`
77
+ .pp {
78
+ margin: 22px 24px;
79
+ padding: 18px;
80
+ border-radius: 14px;
81
+ background: #ffffff;
82
+ color: #1d2226;
83
+ box-shadow: 0 12px 30px rgba(0,0,0,0.25);
84
+ align-self: start;
85
+ }
86
+ .pp-top { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
87
+ .pp-avatar {
88
+ width: 44px; height: 44px; border-radius: 50%;
89
+ display: flex; align-items: center; justify-content: center;
90
+ background: #2f6df6; color: white; font-weight: 700;
91
+ }
92
+ .pp-name { font-weight: 700; font-size: 14px; }
93
+ .pp-sub { font-size: 12px; color: #66707a; }
94
+ .pp-body { font-size: 14px; line-height: 1.55; white-space: pre-wrap; word-break: break-word; }
95
+ .pp-sel {
96
+ margin-top: 12px;
97
+ padding: 8px 12px;
98
+ border-radius: 8px;
99
+ background: #eaf1ff;
100
+ color: #2f6df6;
101
+ font-size: 12px;
102
+ font-weight: 600;
103
+ }
104
+ .pp-actions {
105
+ display: flex; gap: 18px; margin-top: 14px; padding-top: 12px;
106
+ border-top: 1px solid #e6e9ec; color: #66707a; font-size: 13px; font-weight: 600;
107
+ }
108
+ `);
109
+ }
110
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Marketing app bundle entry.
3
+ *
4
+ * This is the ESM module that the OS lazily `import()`s when the user installs
5
+ * or opens the Marketing app. It exports the app's ENTIRE component set — the
6
+ * AppComponentResolver picks each class out of this module by the `export` name
7
+ * declared in the app descriptor (see MarketingApp.backend.ts).
8
+ *
9
+ * One bundle, one backend, three components.
10
+ */
11
+ export { MarketingApp } from "./MarketingApp.component";
12
+ export { PostEditor } from "./PostEditor.component";
13
+ export { PostPreview } from "./PostPreview.component";
14
+
15
+ import { MarketingApp } from "./MarketingApp.component";
16
+ export default MarketingApp;
@@ -0,0 +1,126 @@
1
+ import { A_Concept, A_Context } from "@adaas/a-concept";
2
+ import { A_Config, ConfigReader } from "@adaas/a-utils/a-config";
3
+ import { A_Logger, A_LOGGER_ENV_KEYS } from "@adaas/a-utils/a-logger";
4
+ import { A_SignalBus, A_SignalState } from "@adaas/a-utils/a-signal";
5
+ import { A_Polyfill } from "@adaas/a-utils/a-polyfill";
6
+ import {
7
+ AreContainer,
8
+ AreInit,
9
+ AreSignalsContext,
10
+ } from "@adaas/are";
11
+ import {
12
+ AreRoot,
13
+ AreHTMLEngine,
14
+ AreHTMLEngineContext,
15
+ AreDirectiveIf,
16
+ AreDirectiveFor,
17
+ AreDirectiveShow,
18
+ AreRouteWatcher,
19
+ } from "src";
20
+
21
+ // ── OS shell components (eager) ─────────────────────────────────────────────
22
+ import { OsDesktop } from "./os/Desktop.component";
23
+ import { MenuBar } from "./os/MenuBar.component";
24
+ import { OsHud } from "./os/Hud.component";
25
+ import { OsDock } from "./os/Dock.component";
26
+ import { AppStage } from "./os/AppStage.component";
27
+ import { AppWindow } from "./os/AppWindow.component";
28
+ import { OsLaunchpad } from "./os/Launchpad.component";
29
+
30
+ // ── Runtime (app catalogue + lazy resolver) ─────────────────────────────────
31
+ import { AppRegistry, AppDescriptor } from "./runtime/AppRegistry.fragment";
32
+ import { AppComponentResolver } from "./runtime/AppComponentResolver.fragment";
33
+
34
+ // ── OS signals ──────────────────────────────────────────────────────────────
35
+ import { OSRoute } from "./signals/OSRoute.signal";
36
+ import { MouseState } from "./signals/MouseState.signal";
37
+ import { SelectionState } from "./signals/SelectionState.signal";
38
+
39
+
40
+ (async () => {
41
+ try {
42
+ // 1. Discover the installable apps from the OS kernel. Each descriptor
43
+ // carries the app's bundle URL + the component tags that bundle
44
+ // provides, so the resolver can lazily import it on demand.
45
+ const available: AppDescriptor[] = await fetch('/api/apps')
46
+ .then(res => res.json())
47
+ .catch(() => []);
48
+
49
+ // The catalogue starts with nothing installed — the user installs apps
50
+ // from the Launchpad, which is exactly the flow this example shows.
51
+ const registry = new AppRegistry({ available });
52
+
53
+ // The engine consults this whenever it renders an unregistered tag: it
54
+ // imports the owning app's bundle (one per app) and hands back the class.
55
+ const componentResolver = new AppComponentResolver({ registry });
56
+
57
+ // Empty signals context: <are-root id="os"> just renders its body
58
+ // (<os-desktop>); all OS routing is done by the AppStage reacting to
59
+ // OSRoute, not by are-root conditions.
60
+ const signalsContext = new AreSignalsContext({});
61
+
62
+ const container = new AreContainer({
63
+ name: 'ARE OS Desktop',
64
+ components: [
65
+ // ── OS shell ─────────────────────────────────────────────
66
+ OsDesktop,
67
+ MenuBar,
68
+ OsHud,
69
+ OsDock,
70
+ AppStage,
71
+ AppWindow,
72
+ OsLaunchpad,
73
+ // ── Directives ───────────────────────────────────────────
74
+ AreDirectiveIf,
75
+ AreDirectiveFor,
76
+ AreDirectiveShow,
77
+ // ── Engine ───────────────────────────────────────────────
78
+ A_SignalBus,
79
+ AreRoot,
80
+ AreRouteWatcher,
81
+ ConfigReader,
82
+ AreHTMLEngine,
83
+ A_Logger,
84
+ ],
85
+ entities: [
86
+ AreInit,
87
+ OSRoute,
88
+ MouseState,
89
+ SelectionState,
90
+ ],
91
+ fragments: [
92
+ // EVERY dispatched signal type must appear here too, or
93
+ // state.has() returns false and the bus drops the signal.
94
+ new A_SignalState([AreInit, OSRoute, MouseState, SelectionState]),
95
+ signalsContext,
96
+ new AreHTMLEngineContext({ container: document }),
97
+ registry,
98
+ componentResolver,
99
+ new A_Config({
100
+ defaults: {
101
+ [A_LOGGER_ENV_KEYS.LOG_LEVEL]: 'info',
102
+ },
103
+ }),
104
+ ],
105
+ });
106
+
107
+ const concept = new A_Concept({
108
+ name: 'adaas-are-example-os-desktop',
109
+ fragments: [
110
+ new A_Config({
111
+ variables: ['CONFIG_VERBOSE', 'DEV_MODE'] as const,
112
+ defaults: { CONFIG_VERBOSE: true, DEV_MODE: true },
113
+ }),
114
+ ],
115
+ components: [A_Logger, ConfigReader, A_Polyfill],
116
+ containers: [container],
117
+ });
118
+
119
+ await concept.load();
120
+ await concept.start();
121
+
122
+ } catch (error) {
123
+ const logger = A_Context.root.resolve<A_Logger>(A_Logger)!;
124
+ logger.error(error);
125
+ }
126
+ })();
@@ -0,0 +1,112 @@
1
+ import { A_Caller, A_Inject } from "@adaas/a-concept";
2
+ import { Are, AreNode, AreSignalsContext } from "@adaas/are";
3
+ import { AreHTMLNode } from "src";
4
+ import { OSRoute } from "../signals/OSRoute.signal";
5
+
6
+
7
+ /**
8
+ * AppStage — the routed surface of the desktop.
9
+ *
10
+ * This is the OS's custom signal outlet. It maps the current {@link OSRoute} to
11
+ * exactly one of three surfaces and swaps it in via the engine primitives
12
+ * (`clear()` + `render()`):
13
+ * - `/app/<id>` → `<app-window>` (the window chrome reads the route to pick
14
+ * the app and mounts its lazily-resolved root component).
15
+ * - `/launchpad` → `<os-launchpad>` (the App Store overlay).
16
+ * - anything else → nothing (the bare desktop).
17
+ *
18
+ * Switching between two apps keeps the tag (`app-window`) but is a different
19
+ * route, so the stage tears the old window down and builds a fresh one — which
20
+ * is why it keys on the full route, not just the tag. Before tearing a surface
21
+ * down it unsubscribes that subtree from the bus (same discipline AreRoot uses)
22
+ * so a detached app stops reacting to signals.
23
+ */
24
+ export class AppStage extends Are {
25
+
26
+ protected _renderedKey: string = '';
27
+
28
+ @Are.Template
29
+ template(@A_Inject(A_Caller) node: AreNode) {
30
+ const route = document.location.pathname || '/';
31
+ this._renderedKey = this.surfaceKey(route);
32
+ const tag = this.surfaceTag(route);
33
+ if (tag) {
34
+ node.setContent(`<${tag}></${tag}>`);
35
+ }
36
+ }
37
+
38
+ @Are.Signal(OSRoute)
39
+ async onRoute(
40
+ @A_Inject(A_Caller) node: AreNode,
41
+ @A_Inject(OSRoute) signal: OSRoute,
42
+ @A_Inject(AreSignalsContext) signalsContext?: AreSignalsContext,
43
+ ) {
44
+ const route = signal.path;
45
+ const key = this.surfaceKey(route);
46
+
47
+ // Same surface already on screen — nothing to swap.
48
+ if (key === this._renderedKey) {
49
+ return;
50
+ }
51
+
52
+ // Unsubscribe the outgoing surface (it may contain app components that
53
+ // subscribed to the bus) before detaching it, so a torn-down app never
54
+ // reacts to a later signal on a scope that no longer exists.
55
+ if (signalsContext) {
56
+ for (const child of [...node.children]) {
57
+ for (const subscriber of this.collectSubscribers(child, signalsContext)) {
58
+ signalsContext.unsubscribe(subscriber);
59
+ }
60
+ }
61
+ }
62
+
63
+ await node.clear();
64
+
65
+ const tag = this.surfaceTag(route);
66
+ if (tag) {
67
+ node.setContent(`<${tag}></${tag}>`);
68
+ }
69
+
70
+ this._renderedKey = key;
71
+ await node.render();
72
+ }
73
+
74
+ /** A stable key per visible surface (distinct apps must differ). */
75
+ protected surfaceKey(route: string): string {
76
+ if (route.startsWith('/app/')) return route;
77
+ if (route === '/launchpad') return '/launchpad';
78
+ return '/desktop';
79
+ }
80
+
81
+ /** The component tag to mount for a route ('' = empty desktop). */
82
+ protected surfaceTag(route: string): string {
83
+ if (route.startsWith('/app/')) return 'app-window';
84
+ if (route === '/launchpad') return 'os-launchpad';
85
+ return '';
86
+ }
87
+
88
+ protected collectSubscribers(
89
+ node: AreNode,
90
+ signalsContext: AreSignalsContext,
91
+ ): AreNode[] {
92
+ const result: AreNode[] = [];
93
+ const queue: AreNode[] = [node];
94
+
95
+ while (queue.length > 0) {
96
+ const current = queue.shift()!;
97
+ if (signalsContext.subscribers.has(current)) {
98
+ result.push(current);
99
+ }
100
+ queue.push(...current.children);
101
+ }
102
+
103
+ return result;
104
+ }
105
+
106
+ @Are.Styles
107
+ styles(@A_Inject(A_Caller) node: AreHTMLNode) {
108
+ node.setStyles(`
109
+ app-stage { display: contents; }
110
+ `);
111
+ }
112
+ }
@@ -0,0 +1,102 @@
1
+ import { A_Caller, A_Inject } from "@adaas/a-concept";
2
+ import { A_SignalBus } from "@adaas/a-utils/a-signal";
3
+ import { Are, AreEvent, AreNode } from "@adaas/are";
4
+ import { AreHTMLNode } from "src";
5
+ import { AppRegistry } from "../runtime/AppRegistry.fragment";
6
+ import { OSRoute } from "../signals/OSRoute.signal";
7
+
8
+
9
+ /**
10
+ * AppWindow — the macOS-style window chrome that hosts a running app.
11
+ *
12
+ * It reads the current `/app/<id>` route, looks the app up in the
13
+ * {@link AppRegistry}, draws the title bar (traffic-light controls + app name)
14
+ * and mounts the app's ROOT tag in its body. That root tag is (usually) not a
15
+ * registered component yet — the engine resolves it through the
16
+ * AppComponentResolver, which imports the app's bundle. The window neither
17
+ * knows nor cares where the app's code comes from.
18
+ *
19
+ * The red traffic-light closes the window by routing back to `/desktop`.
20
+ */
21
+ export class AppWindow extends Are {
22
+
23
+ @Are.Template
24
+ template(
25
+ @A_Inject(A_Caller) node: AreNode,
26
+ @A_Inject(AppRegistry) registry: AppRegistry,
27
+ ) {
28
+ const id = (document.location.pathname.match(/^\/app\/([^/]+)/) || [])[1];
29
+ const app = id ? registry.get(id) : undefined;
30
+
31
+ if (!app) {
32
+ node.setContent(`<div class="win win-missing">Application not found.</div>`);
33
+ return;
34
+ }
35
+
36
+ node.setContent(`
37
+ <div class="win" style="--accent:${app.accent}">
38
+ <header class="win-bar">
39
+ <div class="traffic">
40
+ <button class="tl tl-red" @click="$close()" title="Close"></button>
41
+ <span class="tl tl-amber"></span>
42
+ <span class="tl tl-green"></span>
43
+ </div>
44
+ <div class="win-title">${app.icon} ${app.name}</div>
45
+ <div class="win-spacer"></div>
46
+ </header>
47
+ <div class="win-body">
48
+ <${app.rootTag}></${app.rootTag}>
49
+ </div>
50
+ </div>
51
+ `);
52
+ }
53
+
54
+ @Are.EventHandler
55
+ close(
56
+ @A_Inject(AreEvent) event: AreEvent,
57
+ @A_Inject(A_SignalBus) bus: A_SignalBus,
58
+ ) {
59
+ (event.get('native') as MouseEvent)?.preventDefault();
60
+ history.pushState({}, '', '/desktop');
61
+ bus.next(new OSRoute('/desktop'));
62
+ }
63
+
64
+ @Are.Styles
65
+ styles(@A_Inject(A_Caller) node: AreHTMLNode) {
66
+ node.setStyles(`
67
+ .win {
68
+ width: min(960px, 86vw);
69
+ height: min(620px, 80vh);
70
+ display: flex;
71
+ flex-direction: column;
72
+ border-radius: 14px;
73
+ overflow: hidden;
74
+ background: #16131f;
75
+ border: 1px solid rgba(255,255,255,0.1);
76
+ box-shadow: 0 40px 120px rgba(0,0,0,0.6), 0 0 0 1px rgba(0,0,0,0.4);
77
+ animation: win-in 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
78
+ }
79
+ @keyframes win-in {
80
+ from { opacity: 0; transform: translateY(14px) scale(0.97); }
81
+ to { opacity: 1; transform: translateY(0) scale(1); }
82
+ }
83
+ .win-bar {
84
+ display: flex;
85
+ align-items: center;
86
+ height: 40px;
87
+ padding: 0 14px;
88
+ background: linear-gradient(180deg, color-mix(in srgb, var(--accent, #6d5fd0) 26%, #221b33), #1a1526);
89
+ border-bottom: 1px solid rgba(255,255,255,0.06);
90
+ }
91
+ .traffic { display: flex; gap: 8px; width: 80px; }
92
+ .tl { width: 12px; height: 12px; border-radius: 50%; border: none; padding: 0; cursor: default; }
93
+ .tl-red { background: #ff5f57; cursor: pointer; }
94
+ .tl-amber { background: #febc2e; }
95
+ .tl-green { background: #28c840; }
96
+ .win-title { flex: 1; text-align: center; font-size: 13px; font-weight: 600; color: rgba(245,245,247,0.9); }
97
+ .win-spacer { width: 80px; }
98
+ .win-body { flex: 1; overflow: auto; background: #14111d; }
99
+ .win-missing { padding: 40px; color: #a1a1aa; }
100
+ `);
101
+ }
102
+ }
@@ -0,0 +1,106 @@
1
+ import { A_Caller, A_Inject } from "@adaas/a-concept";
2
+ import { A_SignalBus } from "@adaas/a-utils/a-signal";
3
+ import { Are, AreNode } from "@adaas/are";
4
+ import { AreHTMLNode } from "src";
5
+ import { OSRoute } from "../signals/OSRoute.signal";
6
+ import { MouseState } from "../signals/MouseState.signal";
7
+ import { SelectionState } from "../signals/SelectionState.signal";
8
+
9
+
10
+ /**
11
+ * Desktop — the OS shell root.
12
+ *
13
+ * It only lays out the persistent surfaces; every dynamic behaviour lives in a
14
+ * child that reacts to signals on its own:
15
+ * - `<menu-bar>` — top bar, reacts to OSRoute + SelectionState.
16
+ * - `<app-stage>` — the routed surface (window / launchpad / empty desktop).
17
+ * - `<os-hud>` — corner read-out, reacts to MouseState + SelectionState.
18
+ * - `<os-dock>` — bottom dock, reacts to OSRoute.
19
+ *
20
+ * There is no central controller wiring these together — the signal bus is the
21
+ * only coupling, which is exactly what "signal-based routing" buys you.
22
+ *
23
+ * Tag: `<os-desktop>` (class name must be `toPascalCase('os-desktop')`).
24
+ */
25
+ export class OsDesktop extends Are {
26
+
27
+ protected _lastMouse: number = 0;
28
+
29
+ @Are.Template
30
+ template(@A_Inject(A_Caller) node: AreNode) {
31
+ node.setContent(`
32
+ <div class="os">
33
+ <menu-bar></menu-bar>
34
+ <div class="os-stage">
35
+ <app-stage></app-stage>
36
+ </div>
37
+ <os-hud></os-hud>
38
+ <os-dock></os-dock>
39
+ </div>
40
+ `);
41
+ }
42
+
43
+ /**
44
+ * Wire the three global signals into the bus once the shell is mounted.
45
+ * This is the single producer for the OS-wide signals:
46
+ * - pointer movement → {@link MouseState} (throttled),
47
+ * - text selection → {@link SelectionState},
48
+ * - browser navigation → {@link OSRoute} (back/forward).
49
+ *
50
+ * The listeners are attached on the next animation frame — i.e. AFTER the
51
+ * engine's atomic initial mount has fully settled. The browser fires a
52
+ * spurious `selectionchange` while the document is being built; dispatching
53
+ * a signal into the engine mid-mount would race the first paint, so we wait
54
+ * for the frame boundary before any signal can flow.
55
+ *
56
+ * There is no initial OSRoute dispatch: every route-aware surface
57
+ * (MenuBar / Dock / AppStage) already reads `location.pathname` in its
58
+ * template, so the first paint is correct without a signal.
59
+ */
60
+ @Are.onAfterMount
61
+ onMount(@A_Inject(A_SignalBus) bus: A_SignalBus) {
62
+ requestAnimationFrame(() => {
63
+ window.addEventListener('mousemove', (event: MouseEvent) => {
64
+ const now = Date.now();
65
+ if (now - this._lastMouse < 100) return; // ~10Hz throttle
66
+ this._lastMouse = now;
67
+ bus.next(new MouseState(event.clientX, event.clientY));
68
+ });
69
+
70
+ document.addEventListener('selectionchange', () => {
71
+ const text = (window.getSelection()?.toString() || '');
72
+ bus.next(new SelectionState(text));
73
+ });
74
+
75
+ window.addEventListener('popstate', () => {
76
+ bus.next(new OSRoute(window.location.pathname || '/'));
77
+ });
78
+ });
79
+ }
80
+
81
+ @Are.Styles
82
+ styles(@A_Inject(A_Caller) node: AreHTMLNode) {
83
+ node.setStyles(`
84
+ .os {
85
+ position: relative;
86
+ width: 100vw;
87
+ height: 100vh;
88
+ overflow: hidden;
89
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
90
+ color: #f5f5f7;
91
+ background:
92
+ radial-gradient(120% 120% at 20% 0%, #5b3fb0 0%, transparent 55%),
93
+ radial-gradient(120% 120% at 100% 30%, #b03f8a 0%, transparent 50%),
94
+ linear-gradient(160deg, #1d1033 0%, #0c0a1a 60%, #050410 100%);
95
+ user-select: none;
96
+ }
97
+ .os-stage {
98
+ position: absolute;
99
+ inset: 28px 0 0 0;
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ }
104
+ `);
105
+ }
106
+ }