@aigne/afs-ui 1.11.0-beta.12

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 (196) hide show
  1. package/LICENSE.md +26 -0
  2. package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
  4. package/dist/aup-protocol.cjs +235 -0
  5. package/dist/aup-protocol.d.cts +78 -0
  6. package/dist/aup-protocol.d.cts.map +1 -0
  7. package/dist/aup-protocol.d.mts +78 -0
  8. package/dist/aup-protocol.d.mts.map +1 -0
  9. package/dist/aup-protocol.mjs +235 -0
  10. package/dist/aup-protocol.mjs.map +1 -0
  11. package/dist/aup-registry.cjs +2489 -0
  12. package/dist/aup-registry.mjs +2487 -0
  13. package/dist/aup-registry.mjs.map +1 -0
  14. package/dist/aup-spec.cjs +1467 -0
  15. package/dist/aup-spec.mjs +1466 -0
  16. package/dist/aup-spec.mjs.map +1 -0
  17. package/dist/aup-types.cjs +165 -0
  18. package/dist/aup-types.d.cts +157 -0
  19. package/dist/aup-types.d.cts.map +1 -0
  20. package/dist/aup-types.d.mts +157 -0
  21. package/dist/aup-types.d.mts.map +1 -0
  22. package/dist/aup-types.mjs +157 -0
  23. package/dist/aup-types.mjs.map +1 -0
  24. package/dist/backend.cjs +14 -0
  25. package/dist/backend.d.cts +104 -0
  26. package/dist/backend.d.cts.map +1 -0
  27. package/dist/backend.d.mts +104 -0
  28. package/dist/backend.d.mts.map +1 -0
  29. package/dist/backend.mjs +13 -0
  30. package/dist/backend.mjs.map +1 -0
  31. package/dist/degradation.cjs +85 -0
  32. package/dist/degradation.d.cts +17 -0
  33. package/dist/degradation.d.cts.map +1 -0
  34. package/dist/degradation.d.mts +17 -0
  35. package/dist/degradation.d.mts.map +1 -0
  36. package/dist/degradation.mjs +84 -0
  37. package/dist/degradation.mjs.map +1 -0
  38. package/dist/index.cjs +36 -0
  39. package/dist/index.d.cts +12 -0
  40. package/dist/index.d.mts +12 -0
  41. package/dist/index.mjs +13 -0
  42. package/dist/runtime.cjs +117 -0
  43. package/dist/runtime.d.cts +59 -0
  44. package/dist/runtime.d.cts.map +1 -0
  45. package/dist/runtime.d.mts +59 -0
  46. package/dist/runtime.d.mts.map +1 -0
  47. package/dist/runtime.mjs +118 -0
  48. package/dist/runtime.mjs.map +1 -0
  49. package/dist/session.cjs +159 -0
  50. package/dist/session.d.cts +80 -0
  51. package/dist/session.d.cts.map +1 -0
  52. package/dist/session.d.mts +80 -0
  53. package/dist/session.d.mts.map +1 -0
  54. package/dist/session.mjs +159 -0
  55. package/dist/session.mjs.map +1 -0
  56. package/dist/snapshot.cjs +162 -0
  57. package/dist/snapshot.mjs +163 -0
  58. package/dist/snapshot.mjs.map +1 -0
  59. package/dist/term-page.cjs +264 -0
  60. package/dist/term-page.mjs +264 -0
  61. package/dist/term-page.mjs.map +1 -0
  62. package/dist/term.cjs +295 -0
  63. package/dist/term.d.cts +84 -0
  64. package/dist/term.d.cts.map +1 -0
  65. package/dist/term.d.mts +84 -0
  66. package/dist/term.d.mts.map +1 -0
  67. package/dist/term.mjs +296 -0
  68. package/dist/term.mjs.map +1 -0
  69. package/dist/tty.cjs +136 -0
  70. package/dist/tty.d.cts +53 -0
  71. package/dist/tty.d.cts.map +1 -0
  72. package/dist/tty.d.mts +53 -0
  73. package/dist/tty.d.mts.map +1 -0
  74. package/dist/tty.mjs +135 -0
  75. package/dist/tty.mjs.map +1 -0
  76. package/dist/ui-provider.cjs +4615 -0
  77. package/dist/ui-provider.d.cts +307 -0
  78. package/dist/ui-provider.d.cts.map +1 -0
  79. package/dist/ui-provider.d.mts +307 -0
  80. package/dist/ui-provider.d.mts.map +1 -0
  81. package/dist/ui-provider.mjs +4616 -0
  82. package/dist/ui-provider.mjs.map +1 -0
  83. package/dist/web-page/core.cjs +1388 -0
  84. package/dist/web-page/core.mjs +1387 -0
  85. package/dist/web-page/core.mjs.map +1 -0
  86. package/dist/web-page/css.cjs +1699 -0
  87. package/dist/web-page/css.mjs +1698 -0
  88. package/dist/web-page/css.mjs.map +1 -0
  89. package/dist/web-page/icons.cjs +248 -0
  90. package/dist/web-page/icons.mjs +248 -0
  91. package/dist/web-page/icons.mjs.map +1 -0
  92. package/dist/web-page/overlay-themes.cjs +514 -0
  93. package/dist/web-page/overlay-themes.mjs +513 -0
  94. package/dist/web-page/overlay-themes.mjs.map +1 -0
  95. package/dist/web-page/renderers/action.cjs +72 -0
  96. package/dist/web-page/renderers/action.mjs +72 -0
  97. package/dist/web-page/renderers/action.mjs.map +1 -0
  98. package/dist/web-page/renderers/broadcast.cjs +160 -0
  99. package/dist/web-page/renderers/broadcast.mjs +160 -0
  100. package/dist/web-page/renderers/broadcast.mjs.map +1 -0
  101. package/dist/web-page/renderers/calendar.cjs +137 -0
  102. package/dist/web-page/renderers/calendar.mjs +137 -0
  103. package/dist/web-page/renderers/calendar.mjs.map +1 -0
  104. package/dist/web-page/renderers/canvas.cjs +173 -0
  105. package/dist/web-page/renderers/canvas.mjs +173 -0
  106. package/dist/web-page/renderers/canvas.mjs.map +1 -0
  107. package/dist/web-page/renderers/cdn-loader.cjs +25 -0
  108. package/dist/web-page/renderers/cdn-loader.mjs +25 -0
  109. package/dist/web-page/renderers/cdn-loader.mjs.map +1 -0
  110. package/dist/web-page/renderers/chart.cjs +101 -0
  111. package/dist/web-page/renderers/chart.mjs +101 -0
  112. package/dist/web-page/renderers/chart.mjs.map +1 -0
  113. package/dist/web-page/renderers/deck.cjs +390 -0
  114. package/dist/web-page/renderers/deck.mjs +390 -0
  115. package/dist/web-page/renderers/deck.mjs.map +1 -0
  116. package/dist/web-page/renderers/device.cjs +1015 -0
  117. package/dist/web-page/renderers/device.mjs +1015 -0
  118. package/dist/web-page/renderers/device.mjs.map +1 -0
  119. package/dist/web-page/renderers/editor.cjs +127 -0
  120. package/dist/web-page/renderers/editor.mjs +127 -0
  121. package/dist/web-page/renderers/editor.mjs.map +1 -0
  122. package/dist/web-page/renderers/finance-chart.cjs +178 -0
  123. package/dist/web-page/renderers/finance-chart.mjs +178 -0
  124. package/dist/web-page/renderers/finance-chart.mjs.map +1 -0
  125. package/dist/web-page/renderers/frame.cjs +274 -0
  126. package/dist/web-page/renderers/frame.mjs +274 -0
  127. package/dist/web-page/renderers/frame.mjs.map +1 -0
  128. package/dist/web-page/renderers/globe.cjs +119 -0
  129. package/dist/web-page/renderers/globe.mjs +119 -0
  130. package/dist/web-page/renderers/globe.mjs.map +1 -0
  131. package/dist/web-page/renderers/input.cjs +137 -0
  132. package/dist/web-page/renderers/input.mjs +137 -0
  133. package/dist/web-page/renderers/input.mjs.map +1 -0
  134. package/dist/web-page/renderers/list.cjs +1243 -0
  135. package/dist/web-page/renderers/list.mjs +1243 -0
  136. package/dist/web-page/renderers/list.mjs.map +1 -0
  137. package/dist/web-page/renderers/map.cjs +126 -0
  138. package/dist/web-page/renderers/map.mjs +126 -0
  139. package/dist/web-page/renderers/map.mjs.map +1 -0
  140. package/dist/web-page/renderers/media.cjs +106 -0
  141. package/dist/web-page/renderers/media.mjs +106 -0
  142. package/dist/web-page/renderers/media.mjs.map +1 -0
  143. package/dist/web-page/renderers/moonphase.cjs +105 -0
  144. package/dist/web-page/renderers/moonphase.mjs +105 -0
  145. package/dist/web-page/renderers/moonphase.mjs.map +1 -0
  146. package/dist/web-page/renderers/natal-chart.cjs +222 -0
  147. package/dist/web-page/renderers/natal-chart.mjs +222 -0
  148. package/dist/web-page/renderers/natal-chart.mjs.map +1 -0
  149. package/dist/web-page/renderers/overlay.cjs +531 -0
  150. package/dist/web-page/renderers/overlay.mjs +531 -0
  151. package/dist/web-page/renderers/overlay.mjs.map +1 -0
  152. package/dist/web-page/renderers/table.cjs +74 -0
  153. package/dist/web-page/renderers/table.mjs +74 -0
  154. package/dist/web-page/renderers/table.mjs.map +1 -0
  155. package/dist/web-page/renderers/terminal.cjs +30 -0
  156. package/dist/web-page/renderers/terminal.mjs +30 -0
  157. package/dist/web-page/renderers/terminal.mjs.map +1 -0
  158. package/dist/web-page/renderers/text.cjs +109 -0
  159. package/dist/web-page/renderers/text.mjs +109 -0
  160. package/dist/web-page/renderers/text.mjs.map +1 -0
  161. package/dist/web-page/renderers/ticker.cjs +133 -0
  162. package/dist/web-page/renderers/ticker.mjs +133 -0
  163. package/dist/web-page/renderers/ticker.mjs.map +1 -0
  164. package/dist/web-page/renderers/time.cjs +69 -0
  165. package/dist/web-page/renderers/time.mjs +69 -0
  166. package/dist/web-page/renderers/time.mjs.map +1 -0
  167. package/dist/web-page/renderers/unknown.cjs +20 -0
  168. package/dist/web-page/renderers/unknown.mjs +20 -0
  169. package/dist/web-page/renderers/unknown.mjs.map +1 -0
  170. package/dist/web-page/renderers/view.cjs +161 -0
  171. package/dist/web-page/renderers/view.mjs +161 -0
  172. package/dist/web-page/renderers/view.mjs.map +1 -0
  173. package/dist/web-page/renderers/wm.cjs +669 -0
  174. package/dist/web-page/renderers/wm.mjs +669 -0
  175. package/dist/web-page/renderers/wm.mjs.map +1 -0
  176. package/dist/web-page/skeleton.cjs +103 -0
  177. package/dist/web-page/skeleton.mjs +103 -0
  178. package/dist/web-page/skeleton.mjs.map +1 -0
  179. package/dist/web-page.cjs +114 -0
  180. package/dist/web-page.d.cts +19 -0
  181. package/dist/web-page.d.cts.map +1 -0
  182. package/dist/web-page.d.mts +19 -0
  183. package/dist/web-page.d.mts.map +1 -0
  184. package/dist/web-page.mjs +115 -0
  185. package/dist/web-page.mjs.map +1 -0
  186. package/dist/web.cjs +827 -0
  187. package/dist/web.d.cts +144 -0
  188. package/dist/web.d.cts.map +1 -0
  189. package/dist/web.d.mts +144 -0
  190. package/dist/web.d.mts.map +1 -0
  191. package/dist/web.mjs +828 -0
  192. package/dist/web.mjs.map +1 -0
  193. package/dist/wm-state.cjs +172 -0
  194. package/dist/wm-state.mjs +171 -0
  195. package/dist/wm-state.mjs.map +1 -0
  196. package/package.json +59 -0
@@ -0,0 +1,1015 @@
1
+
2
+ //#region src/web-page/renderers/device.ts
3
+ const DEVICE_JS = `
4
+ // ── Device Renderer (universal surface — AFS path or remote WebSocket) ──
5
+
6
+ function _resolveDeviceUrl(url) {
7
+ if (!url || typeof url !== "string") return null;
8
+ url = url.trim();
9
+ if (url.indexOf("ws://") === 0 || url.indexOf("wss://") === 0) return url;
10
+ if (url.indexOf("https://") === 0) return "wss://" + url.slice(8);
11
+ if (url.indexOf("http://") === 0) return "ws://" + url.slice(7);
12
+ // Shorthand: ":3300" or "localhost:3300"
13
+ if (/^:\\\\d+/.test(url)) return "ws://localhost" + url;
14
+ if (/^localhost:\\\\d+/.test(url)) return "ws://" + url;
15
+ return null;
16
+ }
17
+
18
+ function renderDeviceFallback(node) {
19
+ var el = document.createElement("div");
20
+ el.className = "aup-device-fallback";
21
+ el.innerHTML = '<span style="font-size:1.2em">\\\\u26a0</span> '
22
+ + '<strong>' + _escapeHtml(String(node.type)) + '</strong>'
23
+ + ' (not supported on this device)';
24
+ return el;
25
+ }
26
+
27
+ function renderAupDevice(node) {
28
+ var el = document.createElement("div");
29
+ el.className = "aup-device";
30
+ var p = node.props || {};
31
+ var src = node.src || p.url || p.src || "";
32
+ var showStatus = p.showStatus !== false;
33
+
34
+ // Sizing modes: "fixed" (default if height set) — scroll internally;
35
+ // "fit" — auto-size to content within min/maxHeight range
36
+ var sizing = p.sizing || (p.height ? "fixed" : "fit");
37
+ if (sizing === "fixed") {
38
+ if (p.height) el.style.height = p.height;
39
+ el.style.overflow = "hidden";
40
+ el.setAttribute("data-aup-device-sizing", "fixed");
41
+ } else {
42
+ // "fit" mode — content determines height, constrained by min/maxHeight
43
+ el.style.height = "auto";
44
+ el.style.overflow = "visible";
45
+ if (p.minHeight) el.style.minHeight = p.minHeight;
46
+ if (p.maxHeight) { el.style.maxHeight = p.maxHeight; el.style.overflow = "auto"; }
47
+ el.setAttribute("data-aup-device-sizing", "fit");
48
+ }
49
+ if (p.capabilities && p.capabilities.maxWidth) el.style.maxWidth = p.capabilities.maxWidth;
50
+
51
+ // Route 1: WebSocket URL → remote AUP connection
52
+ var wsUrl = _resolveDeviceUrl(src);
53
+ if (wsUrl) {
54
+ return _renderDeviceWs(el, node, p, wsUrl, showStatus);
55
+ }
56
+
57
+ // Route 2: AFS path → introspect and auto-render
58
+ if (src && src.indexOf("/") === 0 && window.afs) {
59
+ return _renderDeviceAfs(el, node, p, src, showStatus);
60
+ }
61
+
62
+ // No valid source
63
+ var placeholder = document.createElement("div");
64
+ placeholder.className = "aup-device-fallback";
65
+ placeholder.innerHTML = '<span style="font-size:1.2em">\\\\u26a0</span> '
66
+ + '<span>Device: set <code>src</code> (AFS path) or <code>url</code> (ws://)</span>';
67
+ el.appendChild(placeholder);
68
+ return el;
69
+ }
70
+
71
+ // ── Route 1: Remote AUP via WebSocket (existing behavior) ──
72
+
73
+ function _renderDeviceWs(el, node, p, wsUrl, showStatus) {
74
+ // Status indicator
75
+ var statusEl = document.createElement("div");
76
+ statusEl.className = "aup-device-status";
77
+ if (showStatus) el.appendChild(statusEl);
78
+
79
+ // Content container
80
+ var contentEl = document.createElement("div");
81
+ contentEl.className = "aup-device-content";
82
+ el.appendChild(contentEl);
83
+
84
+ // Disconnected message
85
+ var disconnectEl = document.createElement("div");
86
+ disconnectEl.className = "aup-device-fallback";
87
+ disconnectEl.style.display = "none";
88
+ disconnectEl.textContent = "Device disconnected";
89
+ el.appendChild(disconnectEl);
90
+
91
+ // Capability filter — recursively replaces unsupported nodes before rendering
92
+ var allowedPrimitives = p.capabilities && p.capabilities.primitives;
93
+ function _filterTree(n) {
94
+ if (!n) return n;
95
+ if (allowedPrimitives && n.type && allowedPrimitives.indexOf(n.type) < 0) {
96
+ return {
97
+ id: n.id, type: "view", props: {},
98
+ children: [{ id: (n.id || "x") + "-fb", type: "text",
99
+ props: { content: "\\\\u26a0 " + n.type + " (not supported on this device)", intent: "info", scale: "sm" } }]
100
+ };
101
+ }
102
+ if (n.children) {
103
+ var fc = [];
104
+ for (var ci = 0; ci < n.children.length; ci++) fc.push(_filterTree(n.children[ci]));
105
+ var copy = {}; for (var k in n) copy[k] = n[k];
106
+ copy.children = fc;
107
+ return copy;
108
+ }
109
+ return n;
110
+ }
111
+ function renderFiltered(n) {
112
+ if (!n || !n.type) return null;
113
+ if (allowedPrimitives) n = _filterTree(n);
114
+ return renderAupNode(n);
115
+ }
116
+
117
+ // Store device WS on the element for event routing
118
+ el.setAttribute("data-aup-device-id", node.id || ("device-" + Math.random().toString(36).slice(2)));
119
+ var deviceWs = null;
120
+ var reconnectTimer = null;
121
+ var reconnectDelay = 1000;
122
+ var destroyed = false;
123
+ var deviceNodeTree = null;
124
+
125
+ function connectDevice() {
126
+ if (destroyed) return;
127
+ try {
128
+ deviceWs = new WebSocket(wsUrl);
129
+ } catch (ex) {
130
+ statusEl.className = "aup-device-status error";
131
+ return;
132
+ }
133
+ el._aupDeviceWs = deviceWs;
134
+
135
+ deviceWs.onopen = function() {
136
+ statusEl.className = "aup-device-status connected";
137
+ disconnectEl.style.display = "none";
138
+ reconnectDelay = 1000;
139
+ // Handshake
140
+ var handshake = { type: "join_session" };
141
+ if (allowedPrimitives) {
142
+ handshake.caps = { primitives: allowedPrimitives };
143
+ }
144
+ deviceWs.send(JSON.stringify(handshake));
145
+ // Fire connect event
146
+ if (node.events && node.events.connect) {
147
+ _fireAupEvent(node.id, "connect", {});
148
+ }
149
+ };
150
+
151
+ deviceWs.onmessage = function(e) {
152
+ var msg;
153
+ try { msg = JSON.parse(e.data); } catch (_ex) { return; }
154
+
155
+ if (msg.type === "aup" && msg.action === "render") {
156
+ deviceNodeTree = msg.root;
157
+ contentEl.innerHTML = "";
158
+ var rendered = renderFiltered(msg.root);
159
+ if (rendered) {
160
+ contentEl.appendChild(rendered);
161
+ // Trigger animations for device-rendered content immediately
162
+ var anims = contentEl.querySelectorAll("[data-animate]");
163
+ for (var ai = 0; ai < anims.length; ai++) anims[ai].classList.add("aup-animated");
164
+ }
165
+ } else if (msg.type === "aup" && msg.action === "patch") {
166
+ // Apply patches to device node tree and re-render affected nodes
167
+ if (deviceNodeTree && msg.ops) {
168
+ _applyDevicePatches(contentEl, deviceNodeTree, msg.ops, renderFiltered);
169
+ }
170
+ }
171
+ // Ignore other message types (session, etc.)
172
+ };
173
+
174
+ deviceWs.onclose = function() {
175
+ statusEl.className = "aup-device-status";
176
+ el._aupDeviceWs = null;
177
+ deviceWs = null;
178
+ if (!destroyed) {
179
+ disconnectEl.style.display = "";
180
+ // Fire disconnect event
181
+ if (node.events && node.events.disconnect) {
182
+ _fireAupEvent(node.id, "disconnect", {});
183
+ }
184
+ // Auto-reconnect with exponential backoff
185
+ reconnectTimer = setTimeout(function() {
186
+ reconnectDelay = Math.min(reconnectDelay * 2, 30000);
187
+ connectDevice();
188
+ }, reconnectDelay);
189
+ }
190
+ };
191
+
192
+ deviceWs.onerror = function() {
193
+ statusEl.className = "aup-device-status error";
194
+ if (node.events && node.events.error) {
195
+ _fireAupEvent(node.id, "error", { url: wsUrl });
196
+ }
197
+ };
198
+ }
199
+
200
+ connectDevice();
201
+
202
+ // Cleanup on DOM removal
203
+ var observer = new MutationObserver(function(mutations) {
204
+ for (var i = 0; i < mutations.length; i++) {
205
+ for (var j = 0; j < mutations[i].removedNodes.length; j++) {
206
+ if (mutations[i].removedNodes[j] === el || mutations[i].removedNodes[j].contains(el)) {
207
+ destroyed = true;
208
+ if (reconnectTimer) clearTimeout(reconnectTimer);
209
+ if (deviceWs) { try { deviceWs.close(); } catch (_ex) {} }
210
+ el._aupDeviceWs = null;
211
+ observer.disconnect();
212
+ return;
213
+ }
214
+ }
215
+ }
216
+ });
217
+ if (el.parentNode) {
218
+ observer.observe(el.parentNode, { childList: true, subtree: true });
219
+ } else {
220
+ // Defer until appended
221
+ setTimeout(function() {
222
+ if (el.parentNode) observer.observe(el.parentNode, { childList: true, subtree: true });
223
+ }, 0);
224
+ }
225
+
226
+ return el;
227
+ }
228
+
229
+ // ── Route 2: AFS path → introspect and auto-render ──
230
+
231
+ function _renderDeviceAfs(el, node, p, src, showStatus) {
232
+ el.setAttribute("data-aup-id", node.id || "");
233
+ el.setAttribute("data-aup-device-src", src);
234
+
235
+ // Status dot
236
+ var statusEl = document.createElement("div");
237
+ statusEl.className = "aup-device-status";
238
+ if (showStatus) el.appendChild(statusEl);
239
+
240
+ // Navigation breadcrumb (device-level, above content)
241
+ var breadcrumbEl = document.createElement("div");
242
+ breadcrumbEl.className = "aup-device-breadcrumb";
243
+ breadcrumbEl.style.display = "none";
244
+ el.appendChild(breadcrumbEl);
245
+
246
+ // View selector container (tabs or dropdown for multi-view .aup/)
247
+ var viewSelectorEl = document.createElement("div");
248
+ viewSelectorEl.className = "aup-device-view-selector";
249
+ viewSelectorEl.style.display = "none";
250
+ el.appendChild(viewSelectorEl);
251
+
252
+ // Content container
253
+ var contentEl = document.createElement("div");
254
+ contentEl.className = "aup-device-content";
255
+ el.appendChild(contentEl);
256
+
257
+ var prefix = node.id || "dev";
258
+ var rootSrc = src;
259
+ var currentSrc = src;
260
+ // Navigation stack: [{path, isLeaf}]
261
+ var navStack = [{ path: src, isLeaf: false }];
262
+ // Track current active view name and all discovered views
263
+ var activeViewName = (node.state && node.state.activeView) || "default";
264
+ var discoveredViews = null;
265
+ // Track in-flight view switch to prevent races
266
+ var viewSwitchSeq = 0;
267
+
268
+ function updateBreadcrumb() {
269
+ breadcrumbEl.innerHTML = "";
270
+ if (navStack.length <= 1) {
271
+ breadcrumbEl.style.display = "none";
272
+ return;
273
+ }
274
+ breadcrumbEl.style.display = "";
275
+ for (var i = 0; i < navStack.length; i++) {
276
+ if (i > 0) {
277
+ var sep = document.createElement("span");
278
+ sep.className = "aup-device-breadcrumb-sep";
279
+ sep.textContent = "/";
280
+ breadcrumbEl.appendChild(sep);
281
+ }
282
+ var seg = document.createElement("span");
283
+ var segPath = navStack[i].path;
284
+ var segName = segPath === rootSrc ? (rootSrc.split("/").filter(Boolean).pop() || "/") : segPath.split("/").filter(Boolean).pop() || "/";
285
+ if (i < navStack.length - 1) {
286
+ seg.className = "aup-device-breadcrumb-seg";
287
+ seg.textContent = segName;
288
+ (function(targetIdx) {
289
+ seg.onclick = function() { navigateToStack(targetIdx); };
290
+ })(i);
291
+ } else {
292
+ seg.className = "aup-device-breadcrumb-cur";
293
+ seg.textContent = segName;
294
+ }
295
+ breadcrumbEl.appendChild(seg);
296
+ }
297
+ }
298
+
299
+ function navigateToStack(idx) {
300
+ navStack = navStack.slice(0, idx + 1);
301
+ currentSrc = navStack[navStack.length - 1].path;
302
+ resolveAndRender(currentSrc);
303
+ updateBreadcrumb();
304
+ }
305
+
306
+ function navigateToPath(path, isLeaf) {
307
+ // Don't navigate above root
308
+ if (path.indexOf(rootSrc) !== 0 && rootSrc.indexOf(path) !== 0) return;
309
+ currentSrc = path;
310
+ navStack.push({ path: path, isLeaf: !!isLeaf });
311
+ resolveAndRender(path);
312
+ updateBreadcrumb();
313
+ }
314
+
315
+ function resolveAndRender(targetSrc) {
316
+ contentEl.innerHTML = "";
317
+ var loader = document.createElement("div");
318
+ loader.className = "aup-src-loading";
319
+ loader.innerHTML = '<div class="aup-src-loading-bar"></div>';
320
+ contentEl.appendChild(loader);
321
+
322
+ window.afs.stat(targetSrc).then(function(statResult) {
323
+ loader.remove();
324
+ statusEl.className = "aup-device-status connected";
325
+ var entry = statResult || {};
326
+ var meta = entry.meta || {};
327
+ var kind = meta.kind || "";
328
+ var childrenCount = meta.childrenCount;
329
+ var isDir = childrenCount != null && childrenCount >= 0;
330
+
331
+ // Priority 1: Check for .aup/ recipe (with multi-view discovery)
332
+ _tryAupRecipe(targetSrc, p).then(function(recipe) {
333
+ if (recipe) {
334
+ // Check for multi-view metadata attached by _discoverAupViews
335
+ var views = recipe._aupViews;
336
+ if (views && views.length > 1) {
337
+ discoveredViews = views;
338
+ _renderViewSelector(viewSelectorEl, views, activeViewName, function(viewName) {
339
+ _switchToView(targetSrc, viewName);
340
+ });
341
+ } else {
342
+ discoveredViews = null;
343
+ viewSelectorEl.style.display = "none";
344
+ viewSelectorEl.innerHTML = "";
345
+ }
346
+ // Clean up internal metadata before rendering
347
+ delete recipe._aupViews;
348
+ var rendered = renderAupNode(recipe);
349
+ if (rendered) contentEl.appendChild(rendered);
350
+ } else if (isDir) {
351
+ discoveredViews = null;
352
+ viewSelectorEl.style.display = "none";
353
+ viewSelectorEl.innerHTML = "";
354
+ _renderDeviceAsDir(contentEl, el, prefix, targetSrc, p, kind, navigateToPath);
355
+ } else {
356
+ discoveredViews = null;
357
+ viewSelectorEl.style.display = "none";
358
+ viewSelectorEl.innerHTML = "";
359
+ _renderDeviceAsLeaf(contentEl, prefix, targetSrc, meta);
360
+ }
361
+ });
362
+
363
+ // Fire connect event on initial load
364
+ if (targetSrc === rootSrc && node.events && node.events.connect) {
365
+ _fireAupEvent(node.id, "connect", { src: targetSrc, kind: kind });
366
+ }
367
+ }).catch(function(err) {
368
+ loader.remove();
369
+ statusEl.className = "aup-device-status error";
370
+ var errEl = document.createElement("div");
371
+ errEl.className = "aup-device-fallback";
372
+ errEl.textContent = "Failed to resolve: " + targetSrc + " (" + (err.message || "error") + ")";
373
+ contentEl.appendChild(errEl);
374
+
375
+ if (node.events && node.events.error) {
376
+ _fireAupEvent(node.id, "error", { src: targetSrc, message: err.message });
377
+ }
378
+ });
379
+ }
380
+
381
+ /**
382
+ * Switch to a different .aup/ view by name.
383
+ * Loads the recipe from the selected view and re-renders content.
384
+ */
385
+ function _switchToView(targetSrc, viewName) {
386
+ if (!discoveredViews) return;
387
+ var seq = ++viewSwitchSeq;
388
+ activeViewName = viewName;
389
+ // Persist selection in node.state
390
+ if (node.state) node.state.activeView = viewName;
391
+ else node.state = { activeView: viewName };
392
+
393
+ // Find the view entry
394
+ var view = null;
395
+ for (var i = 0; i < discoveredViews.length; i++) {
396
+ if (discoveredViews[i].name === viewName) {
397
+ view = discoveredViews[i];
398
+ break;
399
+ }
400
+ }
401
+ if (!view) return;
402
+
403
+ // Determine preferred variant
404
+ var preferCompact = false;
405
+ if (p && p.capabilities) {
406
+ var caps = p.capabilities;
407
+ if (caps.maxWidth || (caps.primitives && caps.primitives.length <= 4)) {
408
+ preferCompact = true;
409
+ }
410
+ }
411
+ var variants = preferCompact ? ["compact.json", "default.json"] : ["default.json"];
412
+
413
+ contentEl.innerHTML = "";
414
+ var loader = document.createElement("div");
415
+ loader.className = "aup-src-loading";
416
+ loader.innerHTML = '<div class="aup-src-loading-bar"></div>';
417
+ contentEl.appendChild(loader);
418
+
419
+ _tryViewVariants(view, variants, 0).then(function(recipe) {
420
+ // Check if a newer switch has been initiated
421
+ if (seq !== viewSwitchSeq) return;
422
+ loader.remove();
423
+ if (recipe) {
424
+ var rendered = renderAupNode(recipe);
425
+ if (rendered) contentEl.appendChild(rendered);
426
+ } else {
427
+ var errEl = document.createElement("div");
428
+ errEl.className = "aup-device-fallback";
429
+ errEl.textContent = "No recipe found for view: " + viewName;
430
+ contentEl.appendChild(errEl);
431
+ }
432
+ }).catch(function(err) {
433
+ if (seq !== viewSwitchSeq) return;
434
+ loader.remove();
435
+ var errEl = document.createElement("div");
436
+ errEl.className = "aup-device-fallback";
437
+ errEl.textContent = "Failed to load view: " + viewName + " (" + (err.message || "error") + ")";
438
+ contentEl.appendChild(errEl);
439
+ });
440
+
441
+ // Update selector UI (mark active)
442
+ _updateViewSelectorActive(viewSelectorEl, viewName);
443
+ }
444
+
445
+ // Initial resolve
446
+ resolveAndRender(src);
447
+
448
+ // Live subscription — re-resolve when data changes
449
+ if (window.afs.subscribe) {
450
+ window.afs.subscribe({ type: "afs:write", path: src }, function() {
451
+ resolveAndRender(currentSrc);
452
+ });
453
+ }
454
+
455
+ return el;
456
+ }
457
+
458
+ // ── AFS rendering strategies ──
459
+
460
+ function _renderDeviceAsDir(contentEl, deviceEl, prefix, src, props, kind, onNavigate) {
461
+ // Generate an afs-list node and render it inline
462
+ var listNode = {
463
+ id: prefix + "-list",
464
+ type: "afs-list",
465
+ src: src,
466
+ props: {
467
+ layout: "list",
468
+ itemStyle: "row",
469
+ showBreadcrumb: false,
470
+ clickMode: "both"
471
+ }
472
+ };
473
+ // Kind-aware layout hints
474
+ if (kind === "gallery" || kind === "media") {
475
+ listNode.props.layout = "masonry";
476
+ listNode.props.itemStyle = "media";
477
+ } else if (kind === "themes-directory" || kind === "overlay-themes-directory") {
478
+ listNode.props.layout = "grid";
479
+ listNode.props.itemStyle = "card";
480
+ }
481
+ var rendered = renderAupNode(listNode);
482
+ if (rendered) {
483
+ contentEl.appendChild(rendered);
484
+ // Listen for select events on files — navigate device to show leaf detail
485
+ if (onNavigate) {
486
+ rendered.addEventListener("aup-list:select", function(e) {
487
+ var d = e.detail;
488
+ if (d && d.path) onNavigate(d.path, true);
489
+ });
490
+ }
491
+ }
492
+ }
493
+
494
+ function _renderDeviceAsLeaf(contentEl, prefix, src, meta) {
495
+ window.afs.read(src).then(function(result) {
496
+ var content = result;
497
+ var tree;
498
+
499
+ if (typeof content === "string") {
500
+ tree = {
501
+ id: prefix + "-text",
502
+ type: "text",
503
+ props: { content: content, format: "markdown" }
504
+ };
505
+ } else if (content && typeof content === "object") {
506
+ tree = _buildDeviceDetailView(prefix, content, meta);
507
+ } else {
508
+ tree = {
509
+ id: prefix + "-empty",
510
+ type: "text",
511
+ props: { content: "(empty)", intent: "info" }
512
+ };
513
+ }
514
+
515
+ var rendered = renderAupNode(tree);
516
+ if (rendered) contentEl.appendChild(rendered);
517
+ }).catch(function(err) {
518
+ var errEl = document.createElement("div");
519
+ errEl.className = "aup-device-fallback";
520
+ errEl.textContent = "Read failed: " + (err.message || "error");
521
+ contentEl.appendChild(errEl);
522
+ });
523
+ }
524
+
525
+ function _buildDeviceDetailView(prefix, obj, meta) {
526
+ var children = [];
527
+ // Title from meta if available
528
+ if (meta && meta.description) {
529
+ children.push({
530
+ id: prefix + "-desc",
531
+ type: "text",
532
+ props: { content: String(meta.description), intent: "info", scale: "sm" }
533
+ });
534
+ }
535
+ var keys = Object.keys(obj);
536
+ for (var i = 0; i < keys.length; i++) {
537
+ var k = keys[i];
538
+ var v = obj[k];
539
+ var valStr = v === null ? "null" : typeof v === "object" ? JSON.stringify(v) : String(v);
540
+ children.push({
541
+ id: prefix + "-kv-" + i,
542
+ type: "view",
543
+ props: { layout: { direction: "row", gap: "sm" }, mode: "inline" },
544
+ children: [
545
+ { id: prefix + "-k-" + i, type: "text", props: { content: k + ":", intent: "info", scale: "sm" } },
546
+ { id: prefix + "-v-" + i, type: "text", props: { content: valStr, scale: "sm" } }
547
+ ]
548
+ });
549
+ }
550
+ return {
551
+ id: prefix + "-detail",
552
+ type: "view",
553
+ props: { layout: { gap: "xs" }, mode: "card" },
554
+ children: children
555
+ };
556
+ }
557
+
558
+ // ── Device patch helpers ──
559
+ function _applyDevicePatches(contentEl, tree, ops, renderFn) {
560
+ for (var i = 0; i < ops.length; i++) {
561
+ var op = ops[i];
562
+ if (op.op === "update") {
563
+ var target = _findDeviceNode(tree, op.id);
564
+ if (target) {
565
+ if (op.props) target.props = Object.assign(target.props || {}, op.props);
566
+ if (op.state) target.state = Object.assign(target.state || {}, op.state);
567
+ if (op.events !== undefined) target.events = op.events;
568
+ }
569
+ // Re-render the specific node in the DOM
570
+ var domEl = contentEl.querySelector('[data-aup-id="' + op.id + '"]');
571
+ if (domEl && target) {
572
+ var newEl = renderFn(target);
573
+ if (newEl) domEl.replaceWith(newEl);
574
+ }
575
+ } else if (op.op === "create") {
576
+ var parent = _findDeviceNode(tree, op.parentId);
577
+ if (parent) {
578
+ if (!parent.children) parent.children = [];
579
+ var newNode = Object.assign({}, op.node, { id: op.id });
580
+ if (typeof op.index === "number") parent.children.splice(op.index, 0, newNode);
581
+ else parent.children.push(newNode);
582
+ }
583
+ var parentDom = contentEl.querySelector('[data-aup-id="' + op.parentId + '"]');
584
+ var createdNode = _findDeviceNode(tree, op.id);
585
+ if (parentDom && createdNode) {
586
+ var createdEl = renderFn(createdNode);
587
+ if (createdEl) {
588
+ if (typeof op.index === "number" && parentDom.children[op.index]) {
589
+ parentDom.insertBefore(createdEl, parentDom.children[op.index]);
590
+ } else {
591
+ parentDom.appendChild(createdEl);
592
+ }
593
+ }
594
+ }
595
+ } else if (op.op === "remove") {
596
+ _removeDeviceNode(tree, op.id);
597
+ var removeDom = contentEl.querySelector('[data-aup-id="' + op.id + '"]');
598
+ if (removeDom) removeDom.remove();
599
+ }
600
+ }
601
+ }
602
+
603
+ function _findDeviceNode(node, id) {
604
+ if (!node) return null;
605
+ if (node.id === id) return node;
606
+ if (node.children) {
607
+ for (var i = 0; i < node.children.length; i++) {
608
+ var found = _findDeviceNode(node.children[i], id);
609
+ if (found) return found;
610
+ }
611
+ }
612
+ return null;
613
+ }
614
+
615
+ function _removeDeviceNode(root, id) {
616
+ if (!root.children) return;
617
+ for (var i = 0; i < root.children.length; i++) {
618
+ if (root.children[i].id === id) { root.children.splice(i, 1); return; }
619
+ _removeDeviceNode(root.children[i], id);
620
+ }
621
+ }
622
+
623
+ // ── .aup/ view selector ──
624
+
625
+ /**
626
+ * Render the view selector (tabs for <=5 views, dropdown for >5).
627
+ * @param container - The view selector container element
628
+ * @param views - Array of { name, path } view entries
629
+ * @param activeView - Currently active view name
630
+ * @param onSwitch - Callback when user selects a different view
631
+ */
632
+ function _renderViewSelector(container, views, activeView, onSwitch) {
633
+ container.innerHTML = "";
634
+ if (!views || views.length <= 1) {
635
+ container.style.display = "none";
636
+ return;
637
+ }
638
+ container.style.display = "";
639
+
640
+ // Resolve display labels: try meta.json for each view, fall back to name
641
+ // For now, use directory name directly (meta.json loading is async, done lazily)
642
+ var labels = {};
643
+ for (var i = 0; i < views.length; i++) {
644
+ labels[views[i].name] = _viewDisplayLabel(views[i].name);
645
+ }
646
+
647
+ // Determine if the active view exists; if not, pick "default" or first
648
+ var found = false;
649
+ for (var j = 0; j < views.length; j++) {
650
+ if (views[j].name === activeView) { found = true; break; }
651
+ }
652
+ if (!found) {
653
+ // Try "default", then first alphabetically
654
+ activeView = "default";
655
+ found = false;
656
+ for (var k = 0; k < views.length; k++) {
657
+ if (views[k].name === activeView) { found = true; break; }
658
+ }
659
+ if (!found) activeView = views[0].name;
660
+ }
661
+
662
+ if (views.length <= 5) {
663
+ // Tab bar mode
664
+ for (var ti = 0; ti < views.length; ti++) {
665
+ var tab = document.createElement("span");
666
+ tab.className = "aup-device-view-tab" + (views[ti].name === activeView ? " active" : "");
667
+ tab.textContent = labels[views[ti].name] || views[ti].name;
668
+ tab.setAttribute("data-view-name", views[ti].name);
669
+ (function(viewName) {
670
+ tab.onclick = function() {
671
+ if (!tab.classList.contains("active")) onSwitch(viewName);
672
+ };
673
+ })(views[ti].name);
674
+ container.appendChild(tab);
675
+ }
676
+ } else {
677
+ // Dropdown mode
678
+ var select = document.createElement("select");
679
+ select.className = "aup-device-view-dropdown";
680
+ for (var di = 0; di < views.length; di++) {
681
+ var option = document.createElement("option");
682
+ option.value = views[di].name;
683
+ option.textContent = labels[views[di].name] || views[di].name;
684
+ if (views[di].name === activeView) option.selected = true;
685
+ select.appendChild(option);
686
+ }
687
+ select.onchange = function() {
688
+ onSwitch(select.value);
689
+ };
690
+ container.appendChild(select);
691
+ }
692
+
693
+ // Async: try loading meta.json for each view to get better labels
694
+ _loadViewMetaLabels(views, function(updatedLabels) {
695
+ if (!updatedLabels) return;
696
+ var changed = false;
697
+ for (var m in updatedLabels) {
698
+ if (updatedLabels[m] && updatedLabels[m] !== labels[m]) {
699
+ labels[m] = updatedLabels[m];
700
+ changed = true;
701
+ }
702
+ }
703
+ if (changed) {
704
+ // Update displayed labels
705
+ if (views.length <= 5) {
706
+ var tabs = container.querySelectorAll(".aup-device-view-tab");
707
+ for (var t = 0; t < tabs.length; t++) {
708
+ var vn = tabs[t].getAttribute("data-view-name");
709
+ if (vn && labels[vn]) tabs[t].textContent = labels[vn];
710
+ }
711
+ } else {
712
+ var options = container.querySelectorAll("option");
713
+ for (var o = 0; o < options.length; o++) {
714
+ var ov = options[o].value;
715
+ if (ov && labels[ov]) options[o].textContent = labels[ov];
716
+ }
717
+ }
718
+ }
719
+ });
720
+ }
721
+
722
+ /**
723
+ * Generate a human-readable display label from a view directory name.
724
+ */
725
+ function _viewDisplayLabel(name) {
726
+ if (!name) return "";
727
+ // Capitalize first letter, replace hyphens/underscores with spaces
728
+ var label = name.replace(/[-_]/g, " ");
729
+ return label.charAt(0).toUpperCase() + label.slice(1);
730
+ }
731
+
732
+ /**
733
+ * Load meta.json from each view to get display labels.
734
+ * Calls callback with { viewName: label } map, or null if nothing found.
735
+ */
736
+ function _loadViewMetaLabels(views, callback) {
737
+ if (!window.afs || !window.afs.read) { callback(null); return; }
738
+ var results = {};
739
+ var pending = views.length;
740
+ var anyFound = false;
741
+
742
+ for (var i = 0; i < views.length; i++) {
743
+ (function(view) {
744
+ var metaPath = view.path + "/meta.json";
745
+ window.afs.read(metaPath).then(function(result) {
746
+ var meta = result;
747
+ if (meta && meta.content !== undefined) meta = meta.content;
748
+ if (typeof meta === "string") {
749
+ try { meta = JSON.parse(meta); } catch(_e) { meta = null; }
750
+ }
751
+ if (meta && typeof meta === "object" && meta.label) {
752
+ results[view.name] = meta.label;
753
+ anyFound = true;
754
+ }
755
+ if (--pending === 0) callback(anyFound ? results : null);
756
+ }).catch(function() {
757
+ if (--pending === 0) callback(anyFound ? results : null);
758
+ });
759
+ })(views[i]);
760
+ }
761
+ }
762
+
763
+ /**
764
+ * Update the active state on the view selector (tab bar or dropdown).
765
+ */
766
+ function _updateViewSelectorActive(container, activeViewName) {
767
+ // Tab mode
768
+ var tabs = container.querySelectorAll(".aup-device-view-tab");
769
+ for (var i = 0; i < tabs.length; i++) {
770
+ var vn = tabs[i].getAttribute("data-view-name");
771
+ if (vn === activeViewName) {
772
+ tabs[i].className = "aup-device-view-tab active";
773
+ } else {
774
+ tabs[i].className = "aup-device-view-tab";
775
+ }
776
+ }
777
+ // Dropdown mode
778
+ var select = container.querySelector(".aup-device-view-dropdown");
779
+ if (select) {
780
+ select.value = activeViewName;
781
+ }
782
+ }
783
+
784
+ // ── .aup/ recipe discovery ──
785
+
786
+ /**
787
+ * Discover all .aup/ views via list(), then load the best recipe.
788
+ * Returns { recipe, views } where views is the full list of discovered entries.
789
+ * Falls back to probing hardcoded variant filenames if list is unavailable.
790
+ */
791
+ function _tryAupRecipe(src, props) {
792
+ if (!window.afs || !window.afs.read) {
793
+ return Promise.resolve(null);
794
+ }
795
+
796
+ // Determine preferred variants based on capabilities
797
+ var preferCompact = false;
798
+ if (props && props.capabilities) {
799
+ var caps = props.capabilities;
800
+ if (caps.maxWidth || (caps.primitives && caps.primitives.length <= 4)) {
801
+ preferCompact = true;
802
+ }
803
+ }
804
+
805
+ // Strategy 1: list-based discovery (supports supplementary providers)
806
+ if (window.afs.list) {
807
+ return _discoverAupViews(src, preferCompact);
808
+ }
809
+
810
+ // Strategy 2: fallback to probing hardcoded variant filenames
811
+ var variants = preferCompact ? ["compact", "default"] : ["default"];
812
+ return _tryAupVariants(src, variants, 0);
813
+ }
814
+
815
+ /**
816
+ * List .aup/ directory to discover all available view entries.
817
+ * Returns the recipe for the best matching view, or null if none found.
818
+ * Stores discovered views on the result for Phase 1 view switching.
819
+ */
820
+ function _discoverAupViews(src, preferCompact) {
821
+ var aupPath = src + "/.aup";
822
+ return window.afs.list(aupPath).then(function(result) {
823
+ // list() returns {data: [...]} — unwrap
824
+ var entries = result && result.data ? result.data : (Array.isArray(result) ? result : null);
825
+ if (!entries || !Array.isArray(entries) || entries.length === 0) {
826
+ // No .aup/ entries — fall back to probe-based
827
+ var variants = preferCompact ? ["compact", "default"] : ["default"];
828
+ return _tryAupVariants(src, variants, 0);
829
+ }
830
+
831
+ // Separate flat recipe files (.json leaves) from view directories.
832
+ // AFSJSON stores recipes as stringified JSON leaves — they appear as
833
+ // ".aup/default.json" (a file), not ".aup/default/" (a directory).
834
+ var views = [];
835
+ var flatRecipes = []; // { name, path, content? }
836
+ for (var i = 0; i < entries.length; i++) {
837
+ var entry = entries[i];
838
+ var entryPath = entry.path || "";
839
+ var name = entryPath.split("/").filter(Boolean).pop() || "";
840
+ if (!name) continue;
841
+ var isLeaf = name.indexOf(".json") === name.length - 5;
842
+ var meta = entry.meta || {};
843
+ // Also detect leaves by childrenCount (directories have childrenCount >= 0)
844
+ if (isLeaf || (meta.childrenCount == null && entry.content != null)) {
845
+ flatRecipes.push({ name: name.replace(/\\.json$/, ""), path: entryPath, content: entry.content });
846
+ } else {
847
+ views.push({ name: name, path: entryPath });
848
+ }
849
+ }
850
+
851
+ // If we found flat recipe files, try reading them directly
852
+ if (flatRecipes.length > 0 && views.length === 0) {
853
+ // Pure flat structure — pick best recipe by preference order
854
+ var preferred = preferCompact ? ["compact", "default"] : ["default"];
855
+ return _tryFlatRecipes(flatRecipes, preferred, 0);
856
+ }
857
+
858
+ if (views.length === 0 && flatRecipes.length === 0) {
859
+ var fallbackVariants = preferCompact ? ["compact", "default"] : ["default"];
860
+ return _tryAupVariants(src, fallbackVariants, 0);
861
+ }
862
+
863
+ // Sort views: "default" first, then "compact", then alphabetical
864
+ views.sort(function(a, b) {
865
+ if (a.name === "default") return -1;
866
+ if (b.name === "default") return 1;
867
+ if (a.name === "compact") return preferCompact ? -1 : 1;
868
+ if (b.name === "compact") return preferCompact ? 1 : -1;
869
+ return a.name < b.name ? -1 : 1;
870
+ });
871
+
872
+ // Try loading recipe from the best view
873
+ // For each view, try reading {viewPath}/default.json
874
+ return _tryViewRecipes(views, preferCompact ? "compact" : "default", 0).then(function(result) {
875
+ if (result && result.recipe) {
876
+ // Attach views metadata to the recipe for Phase 1 view switching
877
+ result.recipe._aupViews = views;
878
+ return result.recipe;
879
+ }
880
+ return null;
881
+ });
882
+ }).catch(function() {
883
+ // List failed — fall back to probe-based discovery
884
+ var variants = preferCompact ? ["compact", "default"] : ["default"];
885
+ return _tryAupVariants(src, variants, 0);
886
+ });
887
+ }
888
+
889
+ /**
890
+ * Try loading a recipe from each discovered view in order.
891
+ * For each view, tries {viewPath}/default.json (and compact.json if preferred).
892
+ */
893
+ function _tryViewRecipes(views, preferredVariant, idx) {
894
+ if (idx >= views.length) return Promise.resolve(null);
895
+ var view = views[idx];
896
+
897
+ // For the selected view, try variant files
898
+ var variants = preferredVariant === "compact"
899
+ ? ["compact.json", "default.json"]
900
+ : ["default.json"];
901
+
902
+ return _tryViewVariants(view, variants, 0).then(function(recipe) {
903
+ if (recipe) {
904
+ return { recipe: recipe, viewName: view.name };
905
+ }
906
+ return _tryViewRecipes(views, preferredVariant, idx + 1);
907
+ });
908
+ }
909
+
910
+ /**
911
+ * Try reading recipe variant files from a specific view directory.
912
+ */
913
+ function _tryViewVariants(view, variants, idx) {
914
+ if (idx >= variants.length) return Promise.resolve(null);
915
+ var recipePath = view.path + "/" + variants[idx];
916
+ return window.afs.read(recipePath).then(function(result) {
917
+ var recipe = result;
918
+ if (recipe && recipe.content !== undefined) {
919
+ recipe = recipe.content;
920
+ }
921
+ if (typeof recipe === "string") {
922
+ try { recipe = JSON.parse(recipe); } catch(_e) { recipe = null; }
923
+ }
924
+ if (recipe && typeof recipe === "object" && recipe.type) {
925
+ return recipe;
926
+ }
927
+ return _tryViewVariants(view, variants, idx + 1);
928
+ }).catch(function() {
929
+ return _tryViewVariants(view, variants, idx + 1);
930
+ });
931
+ }
932
+
933
+ /**
934
+ * Legacy fallback: probe specific variant filenames without list().
935
+ */
936
+ function _tryAupVariants(src, variants, idx) {
937
+ if (idx >= variants.length) return Promise.resolve(null);
938
+ var recipePath = src + "/.aup/" + variants[idx] + ".json";
939
+ return window.afs.read(recipePath).then(function(result) {
940
+ var recipe = result;
941
+ // If read returns an entry wrapper, unwrap content
942
+ if (recipe && recipe.content !== undefined) {
943
+ recipe = recipe.content;
944
+ }
945
+ // If content is a JSON string (e.g. from AFSJSON leaf), parse it
946
+ if (typeof recipe === "string") {
947
+ try { recipe = JSON.parse(recipe); } catch(_e) { recipe = null; }
948
+ }
949
+ // Validate: must be an AUP node (has type field)
950
+ if (recipe && typeof recipe === "object" && recipe.type) {
951
+ return recipe;
952
+ }
953
+ // Invalid recipe — try next variant
954
+ return _tryAupVariants(src, variants, idx + 1);
955
+ }).catch(function() {
956
+ // Recipe doesn't exist — try next variant
957
+ return _tryAupVariants(src, variants, idx + 1);
958
+ });
959
+ }
960
+
961
+ /**
962
+ * Try flat recipe files from .aup/ list results.
963
+ * These are leaf entries (e.g. default.json) rather than view directories.
964
+ * Picks the best match from the preferred order, then falls back to first available.
965
+ */
966
+ function _tryFlatRecipes(flatRecipes, preferred, idx) {
967
+ // First try preferred names in order
968
+ if (idx < preferred.length) {
969
+ var target = preferred[idx];
970
+ for (var i = 0; i < flatRecipes.length; i++) {
971
+ if (flatRecipes[i].name === target) {
972
+ return _parseFlatRecipe(flatRecipes[i]).then(function(recipe) {
973
+ if (recipe) return recipe;
974
+ return _tryFlatRecipes(flatRecipes, preferred, idx + 1);
975
+ });
976
+ }
977
+ }
978
+ return _tryFlatRecipes(flatRecipes, preferred, idx + 1);
979
+ }
980
+ // No preferred match — try first available
981
+ if (flatRecipes.length > 0) {
982
+ return _parseFlatRecipe(flatRecipes[0]);
983
+ }
984
+ return Promise.resolve(null);
985
+ }
986
+
987
+ /**
988
+ * Parse a flat recipe entry — may have inline content or need a read().
989
+ */
990
+ function _parseFlatRecipe(entry) {
991
+ // If content was included in the list result, use it directly
992
+ if (entry.content != null) {
993
+ var recipe = entry.content;
994
+ if (typeof recipe === "string") {
995
+ try { recipe = JSON.parse(recipe); } catch(_e) { return Promise.resolve(null); }
996
+ }
997
+ if (recipe && typeof recipe === "object" && recipe.type) {
998
+ return Promise.resolve(recipe);
999
+ }
1000
+ }
1001
+ // Otherwise read the file
1002
+ return window.afs.read(entry.path).then(function(result) {
1003
+ var r = result;
1004
+ if (r && r.content !== undefined) r = r.content;
1005
+ if (typeof r === "string") {
1006
+ try { r = JSON.parse(r); } catch(_e) { r = null; }
1007
+ }
1008
+ if (r && typeof r === "object" && r.type) return r;
1009
+ return null;
1010
+ }).catch(function() { return null; });
1011
+ }
1012
+ `;
1013
+
1014
+ //#endregion
1015
+ exports.DEVICE_JS = DEVICE_JS;