@effindomv2/create-fui-as-app 0.1.24 → 0.1.25

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.
@@ -3,6 +3,71 @@ import { dirname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { FUI_AS_VERSION, RUNTIME_VERSION } from "./versions.js";
5
5
  const TEMPLATE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "templates");
6
+ const SHARED_LOADING_OVERLAY_STYLES = `.effindom-loading-overlay {
7
+ position: absolute;
8
+ inset: 0;
9
+ display: grid;
10
+ place-items: center;
11
+ padding: 24px;
12
+ box-sizing: border-box;
13
+ background: linear-gradient(180deg, rgba(2, 6, 23, 0.72), rgba(10, 20, 34, 0.82));
14
+ backdrop-filter: blur(10px);
15
+ z-index: 2;
16
+ }
17
+
18
+ .effindom-loading-overlay[hidden] {
19
+ display: none;
20
+ }
21
+
22
+ .effindom-loading-card {
23
+ max-width: 420px;
24
+ padding: 22px 24px;
25
+ border: 1px solid rgba(148, 163, 184, 0.32);
26
+ border-radius: 18px;
27
+ background: rgba(6, 12, 21, 0.80);
28
+ text-align: center;
29
+ box-shadow: 0 18px 48px rgba(2, 6, 23, 0.30);
30
+ }
31
+
32
+ .effindom-loading-overlay[data-state="error"] .effindom-loading-card {
33
+ border-color: rgba(248, 113, 113, 0.50);
34
+ background: rgba(69, 10, 10, 0.78);
35
+ }
36
+
37
+ .effindom-loading-kicker {
38
+ margin: 0 0 10px;
39
+ font-size: 11px;
40
+ font-weight: 700;
41
+ letter-spacing: 0.12em;
42
+ text-transform: uppercase;
43
+ color: #7dd3fc;
44
+ }
45
+
46
+ .effindom-loading-title {
47
+ margin: 0;
48
+ font-size: 28px;
49
+ line-height: 1.15;
50
+ }
51
+
52
+ .effindom-loading-detail {
53
+ margin: 12px 0 0;
54
+ font-size: 14px;
55
+ line-height: 1.55;
56
+ color: #cbd5e1;
57
+ }`;
58
+ const SHARED_LOADING_OVERLAY_BODY = `<div
59
+ class="effindom-loading-overlay"
60
+ id="effindom-loading-overlay"
61
+ data-state="loading"
62
+ aria-live="polite"
63
+ aria-hidden="true"
64
+ hidden>
65
+ <div class="effindom-loading-card">
66
+ <p class="effindom-loading-kicker">EffinDom backstage</p>
67
+ <h2 class="effindom-loading-title" id="effindom-loading-title">Teaching the pixels their lines...</h2>
68
+ <p class="effindom-loading-detail" id="effindom-loading-detail">The runtime orchestra is tuning up behind the canvas.</p>
69
+ </div>
70
+ </div>`;
6
71
  function collectTemplateFiles(root, relativePath = "") {
7
72
  const absolutePath = resolve(root, relativePath);
8
73
  const stats = statSync(absolutePath);
@@ -32,12 +97,18 @@ function replaceTemplateTokens(value, context) {
32
97
  .replaceAll("__FUI_AS_VERSION__", FUI_AS_VERSION)
33
98
  .replaceAll("__RUNTIME_VERSION__", RUNTIME_VERSION);
34
99
  }
100
+ function expandSharedLoadingOverlay(value) {
101
+ return value
102
+ .replaceAll("{{LOADING_OVERLAY_STYLES}}", SHARED_LOADING_OVERLAY_STYLES)
103
+ .replaceAll("{{LOADING_OVERLAY_BODY}}", SHARED_LOADING_OVERLAY_BODY);
104
+ }
35
105
  export function createTemplateFiles(template, context) {
36
106
  const templateDirectory = resolve(TEMPLATE_ROOT, template);
37
107
  const files = collectTemplateFiles(templateDirectory);
38
108
  const output = new Map();
39
109
  for (const [filePath, contents] of files) {
40
- output.set(filePath, replaceTemplateTokens(contents, context));
110
+ const replaced = replaceTemplateTokens(contents, context);
111
+ output.set(filePath, filePath.endsWith(".html") ? expandSharedLoadingOverlay(replaced) : replaced);
41
112
  }
42
113
  return output;
43
114
  }
@@ -1,2 +1,2 @@
1
- export declare const FUI_AS_VERSION = "0.1.21";
1
+ export declare const FUI_AS_VERSION = "0.1.22";
2
2
  export declare const RUNTIME_VERSION = "0.1.8";
@@ -1,2 +1,2 @@
1
- export const FUI_AS_VERSION = "0.1.21";
1
+ export const FUI_AS_VERSION = "0.1.22";
2
2
  export const RUNTIME_VERSION = "0.1.8";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effindomv2/create-fui-as-app",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Scaffold a minimal EffinDom v2 FUI-AS app",
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="FUI-AS">
2
+ <rect width="64" height="64" rx="16" fill="#0f172a"/>
3
+ <path
4
+ d="M8 38C11 20 26 10 36 13C43 15 43 30 34 41C28 49 21 54 17 51C13 48 15 39 23 30C34 18 47 11 57 11M27 42C34 37 42 31 50 28"
5
+ fill="none"
6
+ stroke="#f8fafc"
7
+ stroke-linecap="round"
8
+ stroke-linejoin="round"
9
+ stroke-width="6.5"
10
+ />
11
+ </svg>
@@ -2,6 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
+ <link rel="icon" type="image/svg+xml" href="./favicon.svg" />
5
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
7
  <title>FUI-AS Hello World</title>
7
8
  <style>
@@ -34,62 +35,13 @@
34
35
  outline: none;
35
36
  }
36
37
 
37
- .effindom-loading-overlay {
38
- position: absolute;
39
- inset: 0;
40
- display: grid;
41
- place-items: center;
42
- padding: 18px;
43
- box-sizing: border-box;
44
- background: color-mix(in srgb, Canvas 35%, transparent);
45
- backdrop-filter: blur(10px);
46
- }
47
-
48
- .effindom-loading-overlay[hidden] {
49
- display: none;
50
- }
51
-
52
- .effindom-loading-card {
53
- max-width: 420px;
54
- padding: 20px;
55
- border-radius: 14px;
56
- border: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
57
- background: color-mix(in srgb, Canvas 85%, transparent);
58
- text-align: center;
59
- }
60
-
61
- .effindom-loading-kicker {
62
- margin: 0 0 8px;
63
- font-size: 11px;
64
- letter-spacing: 0.1em;
65
- text-transform: uppercase;
66
- }
67
-
68
- .effindom-loading-title {
69
- margin: 0;
70
- }
71
-
72
- .effindom-loading-detail {
73
- margin: 8px 0 0;
74
- opacity: 0.85;
75
- }
38
+ {{LOADING_OVERLAY_STYLES}}
76
39
  </style>
77
40
  </head>
78
41
  <body>
79
42
  <section class="app-shell" data-effindom-canvas-size-source>
80
43
  <canvas id="fui-canvas"></canvas>
81
- <div
82
- class="effindom-loading-overlay"
83
- id="effindom-loading-overlay"
84
- data-state="loading"
85
- aria-live="polite"
86
- aria-hidden="false">
87
- <div class="effindom-loading-card">
88
- <p class="effindom-loading-kicker">EffinDom backstage</p>
89
- <h2 class="effindom-loading-title" id="effindom-loading-title">Teaching the pixels their lines...</h2>
90
- <p class="effindom-loading-detail" id="effindom-loading-detail">The runtime orchestra is tuning up behind the canvas.</p>
91
- </div>
92
- </div>
44
+ {{LOADING_OVERLAY_BODY}}
93
45
  </section>
94
46
  <script src="./effindom-runtime-config.js"></script>
95
47
  <script src="./bridge.js"></script>
@@ -1,4 +1,4 @@
1
- import { copyFileSync, cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
1
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
 
3
3
  const outputDir = "public";
4
4
  rmSync(outputDir, { recursive: true, force: true });
@@ -15,4 +15,14 @@ writeFileSync(
15
15
  'window.__effindomRuntime = Object.assign({}, window.__effindomRuntime, { manifestUrl: "./runtime/dist/effindom.v2.manifest.json" });\n',
16
16
  "utf8",
17
17
  );
18
- copyFileSync("index.html", `${outputDir}/index.html`);
18
+ const indexTemplate = readFileSync("index.html", "utf8");
19
+ const loadingOverlayStyles = readFileSync("node_modules/@effindomv2/fui-as/browser/loading-overlay-styles.html", "utf8");
20
+ const loadingOverlayBody = readFileSync("node_modules/@effindomv2/fui-as/browser/loading-overlay-body.html", "utf8");
21
+ writeFileSync(
22
+ `${outputDir}/index.html`,
23
+ indexTemplate
24
+ .replace("{{LOADING_OVERLAY_STYLES}}", loadingOverlayStyles)
25
+ .replace("{{LOADING_OVERLAY_BODY}}", loadingOverlayBody),
26
+ "utf8",
27
+ );
28
+ copyFileSync("favicon.svg", `${outputDir}/favicon.svg`);
@@ -2,10 +2,13 @@
2
2
 
3
3
  This template scaffolds a multi-route FUI-AS app with explicit page-level MVC.
4
4
 
5
+ - `src/route-config.ts` is the single route manifest used by wasm builds, runtime prep, smoke checks, and routed harness.
6
+ - Route `title` now drives the browser tab/window title for each shell, and `routeHead("name", "content", ...)` from `@effindomv2/fui-as/browser/routed-app-conventions` can optionally add generic page metadata.
7
+ - `src/routes.ts` holds the route path helpers used by nav/UI code.
5
8
  - `src/routes/HomeApp.ts` and `src/routes/SettingsApp.ts` are route app entrypoints.
6
9
  - `src/routes/home/*` and `src/routes/settings/*` hold each route's model, view, and controller.
7
10
  - `src/routes/shared/*` holds shared route UI primitives (for example nav pills/nav bar) and shared route helpers.
8
- - `harness.ts` wires routed host boot, route table, and host-services/events registration.
11
+ - `harness.ts` wires routed host boot, route table, and host-services/events registration from the route manifest.
9
12
  - `route-shell.html` is the per-route host shell copied for each route path.
10
13
  - `src/host/host-services.ts` and `src/host/host-events.ts` are app-owned bridge contracts; `src/host/generated/*` is generated output.
11
14
 
@@ -22,7 +25,7 @@ The routed harness is optimized for fast navigation:
22
25
  ## Typical workflow
23
26
 
24
27
  1. Add/modify page behavior inside `home/` or `settings/` controllers/models/views.
25
- 2. Add new routes by creating another `*App.ts` entrypoint plus a new route folder and route record in `harness.ts`.
28
+ 2. Add new routes by updating `src/route-config.ts`, then add the matching route folder and `*App.ts` entrypoint.
26
29
  3. Keep shared browser bridge capabilities in `src/host/*.ts`, then regenerate with `npm run generate:host`.
27
30
  4. Use `npm run dev` for local routed iteration (uses debug AssemblyScript builds for faster rebuilds).
28
31
  5. Use `npm run publish` for an optimized release build staged under `published/`.
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="FUI-AS">
2
+ <rect width="64" height="64" rx="16" fill="#0f172a"/>
3
+ <path
4
+ d="M8 38C11 20 26 10 36 13C43 15 43 30 34 41C28 49 21 54 17 51C13 48 15 39 23 30C34 18 47 11 57 11M27 42C34 37 42 31 50 28"
5
+ fill="none"
6
+ stroke="#f8fafc"
7
+ stroke-linecap="round"
8
+ stroke-linejoin="round"
9
+ stroke-width="6.5"
10
+ />
11
+ </svg>
@@ -1,34 +1,21 @@
1
1
  import { startRoutedHarness, type HarnessExports, type RoutedHarnessRoute } from './src/fui/FuiBrowser';
2
2
  import { appHostEvents } from './src/host/host-events';
3
3
  import { appHostServices } from './src/host/host-services';
4
+ import { routeManifest } from './src/route-config';
5
+ import { buildRoutedHarnessRoutes } from '@effindomv2/fui-as/browser/routed-app-conventions';
4
6
 
5
7
  type RouteExports = HarnessExports & {
6
8
  __runApp(): void;
7
9
  __disposeApp?(): void;
8
10
  };
9
11
 
10
- const routePrefix = window.location.pathname.startsWith('/v2/fui-as/demo-mvc/') ? '/v2/fui-as/demo-mvc' : '';
11
- const routes: readonly RoutedHarnessRoute[] = [
12
- {
13
- routePath: `${routePrefix}/home/`,
14
- wasmPath: `${routePrefix}/home.wasm`,
15
- title: 'Home',
16
- },
17
- {
18
- routePath: `${routePrefix}/settings/`,
19
- wasmPath: `${routePrefix}/settings.wasm`,
20
- title: 'Settings',
21
- },
22
- ];
12
+ const routedRoutes: readonly RoutedHarnessRoute[] = buildRoutedHarnessRoutes(routeManifest, window.location.pathname);
23
13
 
24
14
  startRoutedHarness<RouteExports>({
25
15
  shellId: 'fui-routes',
26
- routeBase: routes[0].routePath,
27
- routes,
16
+ routeBase: routedRoutes[0].routePath,
17
+ routes: routedRoutes,
28
18
  recreateRuntimeOnWarmRouteSwap: true,
29
- showLoadingOverlay(isWarmRouteSwap): boolean {
30
- return !isWarmRouteSwap;
31
- },
32
19
  run(exports): void {
33
20
  exports.__runApp();
34
21
  },
@@ -1,11 +1,174 @@
1
1
  <!doctype html>
2
2
  <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta http-equiv="refresh" content="0; url=/home/" />
6
- <title>FUI-AS Routed App</title>
7
- </head>
8
- <body>
9
- <p>Redirecting to <a href="/home/">/home/</a>…</p>
10
- </body>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" type="image/svg+xml" href="favicon.svg" />
6
+ {{HEAD}}
7
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8
+ <base href="../" />
9
+ <style>
10
+ :root {
11
+ font-family: system-ui, sans-serif;
12
+ color-scheme: dark;
13
+ --page-background:
14
+ radial-gradient(circle at top left, rgba(56, 189, 248, 0.14), transparent 32%),
15
+ linear-gradient(180deg, #020611, #060d18 55%, #0a1422);
16
+ --page-text: #e2e8f0;
17
+ --canvas-border: #263449;
18
+ --canvas-background: #0d1424;
19
+ --canvas-shadow: 0 24px 60px rgba(2, 6, 23, 0.45);
20
+ --sidebar-border: #18324b;
21
+ --sidebar-background: rgba(6, 12, 21, 0.94);
22
+ --sidebar-label: #cbd5e1;
23
+ --button-background: linear-gradient(180deg, #0f3c66, #0f5e8a);
24
+ --button-text: #f8fbff;
25
+ --code-color: #93c5fd;
26
+ }
27
+
28
+ :root[data-demo-theme="light"] {
29
+ color-scheme: light;
30
+ --page-background:
31
+ radial-gradient(circle at top left, rgba(14, 165, 233, 0.10), transparent 30%),
32
+ linear-gradient(180deg, #f8fbff, #eef4fb 55%, #e5eef8);
33
+ --page-text: #102033;
34
+ --canvas-border: #c5d2e2;
35
+ --canvas-background: #f8fbff;
36
+ --canvas-shadow: 0 20px 48px rgba(15, 23, 42, 0.12);
37
+ --sidebar-border: #c5d2e2;
38
+ --sidebar-background: rgba(255, 255, 255, 0.92);
39
+ --sidebar-label: #334155;
40
+ --button-background: linear-gradient(180deg, #2563eb, #1d4ed8);
41
+ --button-text: #f8fbff;
42
+ --code-color: #1d4ed8;
43
+ }
44
+
45
+ body {
46
+ margin: 0;
47
+ min-height: 100vh;
48
+ display: grid;
49
+ grid-template-columns: minmax(320px, 1fr) 340px;
50
+ gap: 24px;
51
+ padding: 24px;
52
+ box-sizing: border-box;
53
+ background: var(--page-background);
54
+ color: var(--page-text);
55
+ }
56
+
57
+ .viewport {
58
+ display: grid;
59
+ min-width: 0;
60
+ min-height: 0;
61
+ }
62
+
63
+ .canvas-shell {
64
+ position: relative;
65
+ min-width: 0;
66
+ height: calc(100vh - 48px);
67
+ border-radius: 16px;
68
+ overflow: hidden;
69
+ }
70
+
71
+ canvas {
72
+ box-sizing: border-box;
73
+ display: block;
74
+ width: 100%;
75
+ height: 100%;
76
+ border: 1px solid var(--canvas-border);
77
+ border-radius: 16px;
78
+ background: var(--canvas-background);
79
+ box-shadow: var(--canvas-shadow);
80
+ outline: none;
81
+ }
82
+
83
+ canvas:focus,
84
+ canvas:focus-visible {
85
+ outline: none;
86
+ }
87
+
88
+ {{LOADING_OVERLAY_STYLES}}
89
+
90
+ .sidebar {
91
+ display: grid;
92
+ gap: 16px;
93
+ align-content: start;
94
+ padding: 20px;
95
+ border: 1px solid var(--sidebar-border);
96
+ border-radius: 16px;
97
+ background: var(--sidebar-background);
98
+ }
99
+
100
+ .control-group {
101
+ display: grid;
102
+ gap: 8px;
103
+ }
104
+
105
+ .control-group label {
106
+ font-size: 14px;
107
+ color: var(--sidebar-label);
108
+ }
109
+
110
+ input[type="range"] {
111
+ width: 100%;
112
+ }
113
+
114
+ button {
115
+ padding: 10px 14px;
116
+ border: 0;
117
+ border-radius: 10px;
118
+ background: var(--button-background);
119
+ color: var(--button-text);
120
+ font: inherit;
121
+ cursor: pointer;
122
+ }
123
+
124
+ code {
125
+ color: var(--code-color);
126
+ }
127
+
128
+ @media (max-width: 1120px) {
129
+ body {
130
+ grid-template-columns: 1fr;
131
+ }
132
+
133
+ .canvas-shell {
134
+ height: clamp(360px, 60vh, 620px);
135
+ }
136
+ }
137
+ </style>
138
+ </head>
139
+ <body>
140
+ <section class="viewport">
141
+ <div class="canvas-shell" data-effindom-canvas-size-source>
142
+ <canvas id="fui-canvas"></canvas>
143
+ {{LOADING_OVERLAY_BODY}}
144
+ </div>
145
+ </section>
146
+
147
+ <aside class="sidebar">
148
+ <div>
149
+ <h1>FUI-AS demo</h1>
150
+ <p>Use this routed demo shell to tweak the dashboard route and jump to the advanced-controls playground without leaving the shared runtime.</p>
151
+ <p><code>Dashboard route</code></p>
152
+ </div>
153
+
154
+ <div class="control-group">
155
+ <label for="hue">Accent hue <span id="hue-value">210 deg</span></label>
156
+ <input id="hue" type="range" min="0" max="359" step="1" value="210" />
157
+ </div>
158
+
159
+ <div class="control-group">
160
+ <label>Theme mode <span id="theme-mode-value">Dark</span></label>
161
+ <button id="toggle-theme-mode" type="button">Switch to light mode</button>
162
+ </div>
163
+
164
+ <div class="control-group">
165
+ <label>Clock tick <span id="tick-value">0 s</span></label>
166
+ <button id="pulse-hue" type="button">Jump hue</button>
167
+ </div>
168
+ </aside>
169
+
170
+ <script src="./effindom-runtime-config.js"></script>
171
+ <script src="./bridge.js"></script>
172
+ <script type="module" src="./harness.js"></script>
173
+ </body>
11
174
  </html>
@@ -10,14 +10,14 @@
10
10
  "publish": "npm run generate:host && npm run build:assets && npm run build:wasm:publish && npm run build:harness && npm run publish:stage",
11
11
  "build:assets": "tsx scripts/prepare-runtime.ts",
12
12
  "build:wasm": "npm run build:wasm:home && npm run build:wasm:settings",
13
- "build:wasm:dev": "npm run build:wasm:dev:home && npm run build:wasm:dev:settings",
14
- "build:wasm:publish": "npm run build:wasm:publish:home && npm run build:wasm:publish:settings",
15
13
  "build:wasm:home": "asc src/routes/HomeApp.ts --config asconfig.json --target release --outFile public/home.wasm",
16
14
  "build:wasm:settings": "asc src/routes/SettingsApp.ts --config asconfig.json --target release --outFile public/settings.wasm",
15
+ "build:wasm:dev": "npm run build:wasm:dev:home && npm run build:wasm:dev:settings",
17
16
  "build:wasm:dev:home": "asc src/routes/HomeApp.ts --config asconfig.json --target debug --outFile public/home.wasm",
18
17
  "build:wasm:dev:settings": "asc src/routes/SettingsApp.ts --config asconfig.json --target debug --outFile public/settings.wasm",
19
- "build:wasm:publish:home": "asc src/routes/HomeApp.ts --config asconfig.json --target release --outFile public/home.wasm --optimizeLevel 3 --shrinkLevel 2 --noAssert",
20
- "build:wasm:publish:settings": "asc src/routes/SettingsApp.ts --config asconfig.json --target release --outFile public/settings.wasm --optimizeLevel 3 --shrinkLevel 2 --noAssert",
18
+ "build:wasm:publish": "npm run build:wasm:publish:home && npm run build:wasm:publish:settings",
19
+ "build:wasm:publish:home": "asc src/routes/HomeApp.ts --config asconfig.json --target release --optimizeLevel 3 --shrinkLevel 2 --noAssert --outFile public/home.wasm",
20
+ "build:wasm:publish:settings": "asc src/routes/SettingsApp.ts --config asconfig.json --target release --optimizeLevel 3 --shrinkLevel 2 --noAssert --outFile public/settings.wasm",
21
21
  "build:harness": "esbuild harness.ts --bundle --format=esm --platform=browser --outfile=public/harness.js",
22
22
  "generate:host-services": "tsx ./node_modules/@effindomv2/fui-as/scripts/generate-host-services.ts src/host/host-services.ts appHostServices src/host/generated/HostServices.ts ../../fui/FuiPrimitives",
23
23
  "generate:host-events": "tsx ./node_modules/@effindomv2/fui-as/scripts/generate-host-events.ts src/host/host-events.ts appHostEvents src/host/generated/HostEvents.ts ../../fui/FuiPrimitives",
@@ -4,51 +4,168 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <base href="../" />
7
- <title>FUI-AS Routed Demo</title>
7
+ <title>EffinDom FUI AssemblyScript Demo</title>
8
8
  <style>
9
- html, body {
9
+ :root {
10
+ font-family: system-ui, sans-serif;
11
+ color-scheme: dark;
12
+ --page-background:
13
+ radial-gradient(circle at top left, rgba(56, 189, 248, 0.14), transparent 32%),
14
+ linear-gradient(180deg, #020611, #060d18 55%, #0a1422);
15
+ --page-text: #e2e8f0;
16
+ --canvas-border: #263449;
17
+ --canvas-background: #0d1424;
18
+ --canvas-shadow: 0 24px 60px rgba(2, 6, 23, 0.45);
19
+ --sidebar-border: #18324b;
20
+ --sidebar-background: rgba(6, 12, 21, 0.94);
21
+ --sidebar-label: #cbd5e1;
22
+ --button-background: linear-gradient(180deg, #0f3c66, #0f5e8a);
23
+ --button-text: #f8fbff;
24
+ --code-color: #93c5fd;
25
+ }
26
+
27
+ :root[data-demo-theme="light"] {
28
+ color-scheme: light;
29
+ --page-background:
30
+ radial-gradient(circle at top left, rgba(14, 165, 233, 0.10), transparent 30%),
31
+ linear-gradient(180deg, #f8fbff, #eef4fb 55%, #e5eef8);
32
+ --page-text: #102033;
33
+ --canvas-border: #c5d2e2;
34
+ --canvas-background: #f8fbff;
35
+ --canvas-shadow: 0 20px 48px rgba(15, 23, 42, 0.12);
36
+ --sidebar-border: #c5d2e2;
37
+ --sidebar-background: rgba(255, 255, 255, 0.92);
38
+ --sidebar-label: #334155;
39
+ --button-background: linear-gradient(180deg, #2563eb, #1d4ed8);
40
+ --button-text: #f8fbff;
41
+ --code-color: #1d4ed8;
42
+ }
43
+
44
+ body {
10
45
  margin: 0;
11
- width: 100%;
12
- height: 100%;
13
- overflow: hidden;
14
- background: #020617;
46
+ min-height: 100vh;
47
+ display: grid;
48
+ grid-template-columns: minmax(320px, 1fr) 340px;
49
+ gap: 24px;
50
+ padding: 24px;
51
+ box-sizing: border-box;
52
+ background: var(--page-background);
53
+ color: var(--page-text);
15
54
  }
16
55
 
17
- main {
18
- width: 100%;
19
- height: 100%;
56
+ .viewport {
57
+ display: grid;
58
+ min-width: 0;
59
+ min-height: 0;
60
+ }
61
+
62
+ .canvas-shell {
63
+ position: relative;
64
+ min-width: 0;
65
+ height: calc(100vh - 48px);
66
+ border-radius: 16px;
67
+ overflow: hidden;
20
68
  }
21
69
 
22
70
  canvas {
71
+ box-sizing: border-box;
23
72
  display: block;
24
73
  width: 100%;
25
74
  height: 100%;
26
- background: #020617;
75
+ border: 1px solid var(--canvas-border);
76
+ border-radius: 16px;
77
+ background: var(--canvas-background);
78
+ box-shadow: var(--canvas-shadow);
79
+ outline: none;
27
80
  }
28
81
 
29
- .effindom-loading-overlay {
30
- position: fixed;
31
- inset: 0;
82
+ canvas:focus,
83
+ canvas:focus-visible {
84
+ outline: none;
85
+ }
86
+
87
+ {{LOADING_OVERLAY_STYLES}}
88
+
89
+ .sidebar {
32
90
  display: grid;
33
- place-items: center;
34
- color: #e2e8f0;
35
- background: rgba(2, 6, 23, 0.75);
36
- z-index: 2;
37
- font-family: system-ui, sans-serif;
91
+ gap: 16px;
92
+ align-content: start;
93
+ padding: 20px;
94
+ border: 1px solid var(--sidebar-border);
95
+ border-radius: 16px;
96
+ background: var(--sidebar-background);
38
97
  }
39
98
 
40
- .effindom-loading-overlay[hidden] {
41
- display: none;
99
+ .control-group {
100
+ display: grid;
101
+ gap: 8px;
102
+ }
103
+
104
+ .control-group label {
105
+ font-size: 14px;
106
+ color: var(--sidebar-label);
107
+ }
108
+
109
+ input[type="range"] {
110
+ width: 100%;
111
+ }
112
+
113
+ button {
114
+ padding: 10px 14px;
115
+ border: 0;
116
+ border-radius: 10px;
117
+ background: var(--button-background);
118
+ color: var(--button-text);
119
+ font: inherit;
120
+ cursor: pointer;
121
+ }
122
+
123
+ code {
124
+ color: var(--code-color);
125
+ }
126
+
127
+ @media (max-width: 1120px) {
128
+ body {
129
+ grid-template-columns: 1fr;
130
+ }
131
+
132
+ .canvas-shell {
133
+ height: clamp(360px, 60vh, 620px);
134
+ }
42
135
  }
43
136
  </style>
44
137
  </head>
45
138
  <body>
46
- <main data-effindom-canvas-size-source>
47
- <canvas id="fui-canvas"></canvas>
48
- </main>
49
- <div class="effindom-loading-overlay" id="effindom-loading-overlay" data-state="loading">
50
- <div>Loading routed demo…</div>
51
- </div>
139
+ <section class="viewport">
140
+ <div class="canvas-shell" data-effindom-canvas-size-source>
141
+ <canvas id="fui-canvas"></canvas>
142
+ {{LOADING_OVERLAY_BODY}}
143
+ </div>
144
+ </section>
145
+
146
+ <aside class="sidebar">
147
+ <div>
148
+ <h1>FUI-AS Routed Demo</h1>
149
+ <p>The routed demo shell keeps the dashboard, advanced-controls, and templated-controls playgrounds on the same bridge runtime.</p>
150
+ <p><code>Demo MFE route</code></p>
151
+ </div>
152
+
153
+ <div class="control-group">
154
+ <label for="hue">Accent hue <span id="hue-value">210 deg</span></label>
155
+ <input id="hue" type="range" min="0" max="359" step="1" value="210" />
156
+ </div>
157
+
158
+ <div class="control-group">
159
+ <label>Theme mode <span id="theme-mode-value">Dark</span></label>
160
+ <button id="toggle-theme-mode" type="button">Switch to light mode</button>
161
+ </div>
162
+
163
+ <div class="control-group">
164
+ <label>Clock tick <span id="tick-value">0 s</span></label>
165
+ <button id="pulse-hue" type="button">Jump hue</button>
166
+ </div>
167
+ </aside>
168
+
52
169
  <script src="./effindom-runtime-config.js"></script>
53
170
  <script src="./bridge.js"></script>
54
171
  <script type="module" src="./harness.js"></script>
@@ -0,0 +1,68 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { mkdirSync } from "node:fs";
3
+ import { routeManifest } from "../src/route-config";
4
+ import { resolveRouteManifest } from "@effindomv2/fui-as/browser/routed-app-conventions";
5
+
6
+ const args = process.argv.slice(2);
7
+ let target = "release";
8
+ let publish = false;
9
+ let routeKey = "";
10
+ let buildAll = false;
11
+
12
+ for (let index = 0; index < args.length; index += 1) {
13
+ const arg = args[index];
14
+ switch (arg) {
15
+ case "--all":
16
+ buildAll = true;
17
+ break;
18
+ case "--route":
19
+ routeKey = args[++index] ?? "";
20
+ break;
21
+ case "--target":
22
+ target = args[++index] ?? "release";
23
+ break;
24
+ case "--publish":
25
+ publish = true;
26
+ break;
27
+ default:
28
+ if (routeKey === "") {
29
+ routeKey = arg;
30
+ } else {
31
+ throw new Error(`Unknown argument: ${arg}`);
32
+ }
33
+ break;
34
+ }
35
+ }
36
+
37
+ const resolvedManifest = resolveRouteManifest(routeManifest);
38
+ const selectedRoutes = buildAll
39
+ ? resolvedManifest.routes
40
+ : resolvedManifest.routes.filter((route): boolean => route.key === routeKey);
41
+
42
+ if (!buildAll && selectedRoutes.length === 0) {
43
+ throw new Error(`Unknown route key: ${routeKey}`);
44
+ }
45
+
46
+ mkdirSync("public", { recursive: true });
47
+
48
+ for (const route of selectedRoutes) {
49
+ const ascArgs = [
50
+ route.entrypoint,
51
+ "--config",
52
+ "asconfig.json",
53
+ "--target",
54
+ target,
55
+ "--outFile",
56
+ `public/${route.wasmFile}`,
57
+ ];
58
+
59
+ if (publish) {
60
+ ascArgs.push("--optimizeLevel", "3", "--shrinkLevel", "2", "--noAssert");
61
+ }
62
+
63
+ console.log(`Building ${route.title} -> ${route.wasmFile}`);
64
+ const result = spawnSync("asc", ascArgs, { stdio: "inherit" });
65
+ if (result.status !== 0) {
66
+ process.exit(result.status ?? 1);
67
+ }
68
+ }
@@ -1,10 +1,24 @@
1
- import { copyFileSync, cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
1
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { renderRoutedPageHead, resolveRouteManifest, routeHead } from "@effindomv2/fui-as/browser/routed-app-conventions";
3
+ import { routeManifest } from "../src/route-config";
2
4
 
3
5
  const outputDir = "public";
6
+ const resolvedManifest = resolveRouteManifest(routeManifest);
7
+ const indexTemplate = readFileSync("index.html", "utf8");
8
+ const routeShellTemplate = readFileSync("route-shell.html", "utf8");
9
+ const loadingOverlayStyles = readFileSync("node_modules/@effindomv2/fui-as/browser/loading-overlay-styles.html", "utf8");
10
+ const loadingOverlayBody = readFileSync("node_modules/@effindomv2/fui-as/browser/loading-overlay-body.html", "utf8");
11
+
12
+ function renderLoadingOverlay(template: string): string {
13
+ return template
14
+ .replace("{{LOADING_OVERLAY_STYLES}}", loadingOverlayStyles)
15
+ .replace("{{LOADING_OVERLAY_BODY}}", loadingOverlayBody);
16
+ }
4
17
  rmSync(outputDir, { recursive: true, force: true });
5
18
  mkdirSync(`${outputDir}/runtime`, { recursive: true });
6
- mkdirSync(`${outputDir}/home`, { recursive: true });
7
- mkdirSync(`${outputDir}/settings`, { recursive: true });
19
+ for (const route of resolvedManifest.routes) {
20
+ mkdirSync(`${outputDir}/${route.shellDir}`, { recursive: true });
21
+ }
8
22
 
9
23
  cpSync("node_modules/@effindomv2/runtime/dist", `${outputDir}/runtime/dist`, { recursive: true });
10
24
  cpSync("node_modules/@effindomv2/runtime/dist/fonts", `${outputDir}/runtime/fonts`, { recursive: true });
@@ -17,6 +31,21 @@ writeFileSync(
17
31
  'window.__effindomRuntime = Object.assign({}, window.__effindomRuntime, { manifestUrl: "./runtime/dist/effindom.v2.manifest.json" });\n',
18
32
  "utf8",
19
33
  );
20
- copyFileSync("index.html", `${outputDir}/index.html`);
21
- copyFileSync("route-shell.html", `${outputDir}/home/index.html`);
22
- copyFileSync("route-shell.html", `${outputDir}/settings/index.html`);
34
+ copyFileSync("favicon.svg", `${outputDir}/favicon.svg`);
35
+ const defaultRoute = resolvedManifest.routes.length == 0 ? undefined : resolvedManifest.routes[0];
36
+ const defaultRouteTitle = defaultRoute == null ? "FUI-AS Routed App" : defaultRoute.title;
37
+ const defaultRouteHref = defaultRoute == null ? "/home/" : defaultRoute.publishedRoutePath;
38
+ writeFileSync(
39
+ `${outputDir}/index.html`,
40
+ renderLoadingOverlay(indexTemplate)
41
+ .replace("{{HEAD}}", renderRoutedPageHead(defaultRouteTitle, defaultRoute == null ? routeHead() : defaultRoute.headTags))
42
+ .replaceAll("{{REDIRECT_HREF}}", defaultRouteHref),
43
+ "utf8",
44
+ );
45
+ for (const route of resolvedManifest.routes) {
46
+ writeFileSync(
47
+ `${outputDir}/${route.shellDir}/index.html`,
48
+ renderLoadingOverlay(routeShellTemplate).replace("{{HEAD}}", renderRoutedPageHead(route.title, route.headTags)),
49
+ "utf8",
50
+ );
51
+ }
@@ -1,18 +1,24 @@
1
1
  import { accessSync } from "node:fs";
2
+ import { resolveRouteManifest } from "@effindomv2/fui-as/browser/routed-app-conventions";
3
+ import { routeManifest } from "../src/route-config";
2
4
 
5
+ const resolvedManifest = resolveRouteManifest(routeManifest);
3
6
  const expectedFiles = [
4
7
  "public/index.html",
5
8
  "public/harness.js",
6
- "public/home/index.html",
7
- "public/settings/index.html",
8
- "public/home.wasm",
9
- "public/settings.wasm",
10
9
  "public/bridge.js",
11
10
  "public/effindom-runtime-config.js",
12
11
  "public/runtime/dist/effindom.v2.manifest.json",
13
12
  "public/runtime/fonts/NotoSans-Regular.ttf",
14
13
  ];
15
14
 
15
+ for (const route of resolvedManifest.routes) {
16
+ expectedFiles.push(
17
+ `public/${route.shellDir}/index.html`,
18
+ `public/${route.wasmFile}`,
19
+ );
20
+ }
21
+
16
22
  for (const filePath of expectedFiles) {
17
23
  accessSync(filePath);
18
24
  }
@@ -0,0 +1,16 @@
1
+ import { defineRoutedAppManifest, routeDef, routeHead } from "@effindomv2/fui-as/browser/routed-app-conventions";
2
+
3
+ export const sourceRouteBase = "";
4
+
5
+ export const routeManifest = defineRoutedAppManifest(sourceRouteBase, [
6
+ routeDef("home", "Home", routeHead(
7
+ "description", "Home page for the routed MVC starter.",
8
+ )),
9
+ routeDef("settings", "Settings", routeHead(
10
+ "description", "Settings page for the routed MVC starter.",
11
+ )),
12
+ ]);
13
+
14
+ const routes = routeManifest.routes;
15
+ export const homeRouteConfig = routes[0];
16
+ export const settingsRouteConfig = routes[1];
@@ -1,6 +1,6 @@
1
1
  import { FlexBox, JustifyContent, Row, Unit } from "../../../fui/Fui";
2
2
  import { NavPill } from "./NavPill";
3
- import { homeRoute, settingsRoute } from "../routes";
3
+ import { homeRoute, settingsRoute } from "../../../routes";
4
4
 
5
5
  function navSpacer(): FlexBox {
6
6
  return new FlexBox().width(10.0, Unit.Pixel).height(1.0, Unit.Pixel);
@@ -0,0 +1,11 @@
1
+ import { currentRoute } from "./fui/Fui";
2
+ import { routeManifest } from "./route-config";
3
+ import { resolveRoutePath } from "@effindomv2/fui-as/browser/routed-app-conventions";
4
+
5
+ export function homeRoute(): string {
6
+ return resolveRoutePath(routeManifest, "home", currentRoute.value);
7
+ }
8
+
9
+ export function settingsRoute(): string {
10
+ return resolveRoutePath(routeManifest, "settings", currentRoute.value);
11
+ }
@@ -1,22 +0,0 @@
1
- import { currentRoute } from "../../fui/Fui";
2
-
3
- const SOURCE_DEMO_BASE: string = "/v2/fui-as/demo-mvc";
4
- const SOURCE_HOME_ROUTE: string = "/v2/fui-as/demo-mvc/home/";
5
- const SOURCE_SETTINGS_ROUTE: string = "/v2/fui-as/demo-mvc/settings/";
6
- const PUBLISHED_HOME_ROUTE: string = "/home/";
7
- const PUBLISHED_SETTINGS_ROUTE: string = "/settings/";
8
-
9
- function isSourceDemoRoute(route: string): bool {
10
- if (route.length == 0) {
11
- return true;
12
- }
13
- return route.startsWith(SOURCE_DEMO_BASE);
14
- }
15
-
16
- export function homeRoute(): string {
17
- return isSourceDemoRoute(currentRoute.value) ? SOURCE_HOME_ROUTE : PUBLISHED_HOME_ROUTE;
18
- }
19
-
20
- export function settingsRoute(): string {
21
- return isSourceDemoRoute(currentRoute.value) ? SOURCE_SETTINGS_ROUTE : PUBLISHED_SETTINGS_ROUTE;
22
- }