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