@portel/photon 1.11.0 → 1.12.0

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 (56) hide show
  1. package/README.md +81 -72
  2. package/dist/auto-ui/beam/photon-management.d.ts.map +1 -1
  3. package/dist/auto-ui/beam/photon-management.js +5 -0
  4. package/dist/auto-ui/beam/photon-management.js.map +1 -1
  5. package/dist/auto-ui/beam/routes/api-browse.d.ts +1 -2
  6. package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
  7. package/dist/auto-ui/beam/routes/api-browse.js +140 -191
  8. package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
  9. package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -1
  10. package/dist/auto-ui/beam/routes/api-config.js +44 -1
  11. package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
  12. package/dist/auto-ui/beam.d.ts.map +1 -1
  13. package/dist/auto-ui/beam.js +874 -20
  14. package/dist/auto-ui/beam.js.map +1 -1
  15. package/dist/auto-ui/frontend/index.html +83 -60
  16. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  17. package/dist/auto-ui/streamable-http-transport.js +16 -2
  18. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  19. package/dist/auto-ui/types.d.ts +1 -1
  20. package/dist/auto-ui/types.d.ts.map +1 -1
  21. package/dist/auto-ui/types.js.map +1 -1
  22. package/dist/beam.bundle.js +2794 -322
  23. package/dist/beam.bundle.js.map +4 -4
  24. package/dist/cli/commands/package-app.d.ts.map +1 -1
  25. package/dist/cli/commands/package-app.js +116 -35
  26. package/dist/cli/commands/package-app.js.map +1 -1
  27. package/dist/context-store.d.ts +5 -0
  28. package/dist/context-store.d.ts.map +1 -1
  29. package/dist/context-store.js +9 -0
  30. package/dist/context-store.js.map +1 -1
  31. package/dist/daemon/server.js +303 -6
  32. package/dist/daemon/server.js.map +1 -1
  33. package/dist/loader.d.ts +4 -0
  34. package/dist/loader.d.ts.map +1 -1
  35. package/dist/loader.js +186 -1
  36. package/dist/loader.js.map +1 -1
  37. package/dist/photon-cli-runner.d.ts.map +1 -1
  38. package/dist/photon-cli-runner.js +21 -1
  39. package/dist/photon-cli-runner.js.map +1 -1
  40. package/dist/photon-doc-extractor.d.ts +6 -0
  41. package/dist/photon-doc-extractor.d.ts.map +1 -1
  42. package/dist/photon-doc-extractor.js +22 -0
  43. package/dist/photon-doc-extractor.js.map +1 -1
  44. package/dist/photons/tunnel.photon.d.ts +5 -9
  45. package/dist/photons/tunnel.photon.d.ts.map +1 -1
  46. package/dist/photons/tunnel.photon.js +36 -96
  47. package/dist/photons/tunnel.photon.js.map +1 -1
  48. package/dist/photons/tunnel.photon.ts +40 -112
  49. package/dist/server.d.ts.map +1 -1
  50. package/dist/server.js +27 -2
  51. package/dist/server.js.map +1 -1
  52. package/dist/test-runner.d.ts +13 -1
  53. package/dist/test-runner.d.ts.map +1 -1
  54. package/dist/test-runner.js +529 -122
  55. package/dist/test-runner.js.map +1 -1
  56. package/package.json +22 -6
@@ -15357,10 +15357,14 @@ var theme = i`
15357
15357
  bottom: 0;
15358
15358
  background-color: var(--bg-glass);
15359
15359
  border: 1px solid var(--border-glass);
15360
- transition: 0.4s;
15360
+ transition: 0.2s;
15361
15361
  border-radius: var(--radius-full);
15362
15362
  }
15363
15363
 
15364
+ .slider:hover {
15365
+ border-color: var(--accent-secondary);
15366
+ }
15367
+
15364
15368
  .slider:before {
15365
15369
  position: absolute;
15366
15370
  content: '';
@@ -15369,7 +15373,7 @@ var theme = i`
15369
15373
  left: 2px;
15370
15374
  bottom: 2px;
15371
15375
  background-color: var(--t-muted);
15372
- transition: 0.4s;
15376
+ transition: 0.2s;
15373
15377
  border-radius: 50%;
15374
15378
  }
15375
15379
 
@@ -15544,7 +15548,7 @@ ToastManager.styles = [
15544
15548
  pointer-events: auto;
15545
15549
  animation: slideIn 0.3s ease-out;
15546
15550
  max-width: 350px;
15547
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
15551
+ box-shadow: var(--shadow-lg);
15548
15552
  }
15549
15553
 
15550
15554
  .toast.exiting {
@@ -15622,7 +15626,7 @@ ToastManager.styles = [
15622
15626
 
15623
15627
  .close:hover {
15624
15628
  color: var(--t-primary);
15625
- background: hsla(220, 10%, 80%, 0.1);
15629
+ background: var(--bg-glass);
15626
15630
  }
15627
15631
 
15628
15632
  .toast-action {
@@ -15822,7 +15826,8 @@ var iconPaths = {
15822
15826
  docs: `<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>`,
15823
15827
  expand: `<path d="M15 3h6v6"/><path d="m21 3-7 7"/><path d="m3 21 7-7"/><path d="M9 21H3v-6"/>`,
15824
15828
  plus: `<path d="M5 12h14"/><path d="M12 5v14"/>`,
15825
- clone: `<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>`
15829
+ clone: `<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>`,
15830
+ installApp: `<path d="M12 3v12"/><path d="m17 11-5 5-5-5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>`
15826
15831
  };
15827
15832
 
15828
15833
  // node_modules/@portel/photon-core/dist/design-system/oklch.js
@@ -16509,7 +16514,8 @@ var buttons = i`
16509
16514
  color: var(--t-primary);
16510
16515
  }
16511
16516
 
16512
- .btn-primary:disabled {
16517
+ .btn-primary:disabled,
16518
+ .btn-secondary:disabled {
16513
16519
  opacity: 0.6;
16514
16520
  cursor: not-allowed;
16515
16521
  transform: none;
@@ -16549,7 +16555,21 @@ var forms = i`
16549
16555
  padding: var(--space-sm);
16550
16556
  border-radius: var(--radius-sm);
16551
16557
  font-family: var(--font-sans);
16558
+ font-size: var(--text-md);
16552
16559
  box-sizing: border-box;
16560
+ transition:
16561
+ border-color 0.15s ease,
16562
+ box-shadow 0.15s ease;
16563
+ }
16564
+
16565
+ input:hover,
16566
+ textarea:hover,
16567
+ select:hover {
16568
+ border-color: var(--accent-secondary);
16569
+ }
16570
+
16571
+ textarea {
16572
+ resize: vertical;
16553
16573
  }
16554
16574
 
16555
16575
  ::placeholder {
@@ -16565,12 +16585,12 @@ var forms = i`
16565
16585
  display: block;
16566
16586
  margin-bottom: var(--space-xs);
16567
16587
  font-weight: 500;
16568
- font-size: 0.9rem;
16588
+ font-size: var(--text-md);
16569
16589
  }
16570
16590
 
16571
16591
  .error-text {
16572
16592
  color: var(--color-error);
16573
- font-size: 0.75rem;
16593
+ font-size: var(--text-xs);
16574
16594
  margin-top: var(--space-xs);
16575
16595
  }
16576
16596
 
@@ -16590,7 +16610,7 @@ var forms = i`
16590
16610
  var badges = i`
16591
16611
  .type-badge {
16592
16612
  display: inline-block;
16593
- font-size: 0.65rem;
16613
+ font-size: var(--text-2xs);
16594
16614
  font-weight: 600;
16595
16615
  text-transform: uppercase;
16596
16616
  letter-spacing: 0.04em;
@@ -16624,7 +16644,7 @@ var badges = i`
16624
16644
  }
16625
16645
 
16626
16646
  .param-tag {
16627
- font-size: 0.65rem;
16647
+ font-size: var(--text-2xs);
16628
16648
  padding: 1px 6px;
16629
16649
  border-radius: var(--radius-xs);
16630
16650
  background: var(--param-tag-bg, hsla(220, 10%, 80%, 0.08));
@@ -17200,6 +17220,307 @@ function loadSavedThemeConfig() {
17200
17220
  return null;
17201
17221
  }
17202
17222
 
17223
+ // src/auto-ui/frontend/services/photon-instance-manager.ts
17224
+ var SimpleEventEmitter = class {
17225
+ constructor() {
17226
+ this._listeners = /* @__PURE__ */ new Map();
17227
+ }
17228
+ on(event, callback2) {
17229
+ if (!this._listeners.has(event)) {
17230
+ this._listeners.set(event, /* @__PURE__ */ new Set());
17231
+ }
17232
+ this._listeners.get(event).add(callback2);
17233
+ }
17234
+ off(event, callback2) {
17235
+ const listeners = this._listeners.get(event);
17236
+ if (listeners) {
17237
+ listeners.delete(callback2);
17238
+ }
17239
+ }
17240
+ emit(event, ...args) {
17241
+ const listeners = this._listeners.get(event);
17242
+ if (listeners) {
17243
+ for (const callback2 of listeners) {
17244
+ callback2(...args);
17245
+ }
17246
+ }
17247
+ }
17248
+ };
17249
+ var PhotonSessionProxy = class extends SimpleEventEmitter {
17250
+ constructor(options) {
17251
+ super();
17252
+ this._paginationState = /* @__PURE__ */ new Map();
17253
+ this._pendingPatches = [];
17254
+ this._isApplyingPatches = false;
17255
+ this._name = options.name;
17256
+ this._state = { ...options.initialState };
17257
+ }
17258
+ /**
17259
+ * Get the photon name
17260
+ */
17261
+ get name() {
17262
+ return this._name;
17263
+ }
17264
+ /**
17265
+ * Get current state
17266
+ */
17267
+ get state() {
17268
+ return this._state;
17269
+ }
17270
+ /**
17271
+ * Apply JSON Patch array to instance state
17272
+ * Patches from: https://tools.ietf.org/html/rfc6902
17273
+ */
17274
+ applyPatches(patches) {
17275
+ if (!Array.isArray(patches) || patches.length === 0) {
17276
+ return;
17277
+ }
17278
+ this._pendingPatches.push(...patches);
17279
+ if (!this._isApplyingPatches) {
17280
+ this._isApplyingPatches = true;
17281
+ queueMicrotask(() => this._processPendingPatches());
17282
+ }
17283
+ }
17284
+ /**
17285
+ * Update pagination state for a property (e.g., 'items')
17286
+ */
17287
+ setPaginationState(property2, state) {
17288
+ const current = this._paginationState.get(property2) || {
17289
+ pageSize: 20,
17290
+ currentPage: 0,
17291
+ hasMore: true,
17292
+ isLoading: false
17293
+ };
17294
+ this._paginationState.set(property2, { ...current, ...state });
17295
+ }
17296
+ /**
17297
+ * Get pagination state for a property
17298
+ */
17299
+ getPaginationState(property2) {
17300
+ return this._paginationState.get(property2) || {
17301
+ pageSize: 20,
17302
+ currentPage: 0,
17303
+ hasMore: true,
17304
+ isLoading: false
17305
+ };
17306
+ }
17307
+ /**
17308
+ * Create a getter/setter for property access
17309
+ */
17310
+ _createPropertyDescriptor(key) {
17311
+ return {
17312
+ configurable: true,
17313
+ enumerable: true,
17314
+ get: () => this._state[key],
17315
+ set: (value) => {
17316
+ this._state[key] = value;
17317
+ this.emit("propertyChanged", { property: key, value });
17318
+ }
17319
+ };
17320
+ }
17321
+ /**
17322
+ * Make a property accessible on the proxy
17323
+ */
17324
+ makeProperty(key) {
17325
+ Object.defineProperty(this, key, this._createPropertyDescriptor(key));
17326
+ }
17327
+ /**
17328
+ * Get all top-level properties (useful for initialization)
17329
+ */
17330
+ getProperties() {
17331
+ return Object.keys(this._state);
17332
+ }
17333
+ /**
17334
+ * Process pending patches
17335
+ */
17336
+ _processPendingPatches() {
17337
+ const patches = this._pendingPatches.splice(0);
17338
+ try {
17339
+ for (const patch of patches) {
17340
+ this._applyPatch(patch);
17341
+ }
17342
+ this.emit("state-changed", patches);
17343
+ } finally {
17344
+ this._isApplyingPatches = false;
17345
+ if (this._pendingPatches.length > 0) {
17346
+ queueMicrotask(() => this._processPendingPatches());
17347
+ }
17348
+ }
17349
+ }
17350
+ /**
17351
+ * Apply a single JSON Patch operation
17352
+ */
17353
+ _applyPatch(patch) {
17354
+ const { op, path, value } = patch;
17355
+ if (!path || typeof path !== "string") {
17356
+ console.warn("Invalid patch: missing or invalid path", patch);
17357
+ return;
17358
+ }
17359
+ const parts = path.split("/").filter((p5) => p5 !== "");
17360
+ let current = this._state;
17361
+ try {
17362
+ for (let i7 = 0; i7 < parts.length - 1; i7++) {
17363
+ const part = this._unescapePath(parts[i7]);
17364
+ if (!(part in current)) {
17365
+ current[part] = {};
17366
+ }
17367
+ current = current[part];
17368
+ }
17369
+ const lastPart = this._unescapePath(parts[parts.length - 1]);
17370
+ switch (op) {
17371
+ case "add":
17372
+ if (Array.isArray(current) && !isNaN(Number(lastPart))) {
17373
+ const index2 = Number(lastPart);
17374
+ current.splice(index2, 0, value);
17375
+ } else {
17376
+ current[lastPart] = value;
17377
+ }
17378
+ break;
17379
+ case "remove":
17380
+ if (Array.isArray(current) && !isNaN(Number(lastPart))) {
17381
+ const index2 = Number(lastPart);
17382
+ current.splice(index2, 1);
17383
+ } else {
17384
+ delete current[lastPart];
17385
+ }
17386
+ break;
17387
+ case "replace":
17388
+ current[lastPart] = value;
17389
+ break;
17390
+ case "move":
17391
+ const fromParts = patch.from.split("/").filter((p5) => p5 !== "");
17392
+ let fromCurrent = this._state;
17393
+ for (let i7 = 0; i7 < fromParts.length - 1; i7++) {
17394
+ fromCurrent = fromCurrent[this._unescapePath(fromParts[i7])];
17395
+ }
17396
+ const fromLast = this._unescapePath(fromParts[fromParts.length - 1]);
17397
+ const movedValue = fromCurrent[fromLast];
17398
+ if (Array.isArray(fromCurrent)) {
17399
+ fromCurrent.splice(Number(fromLast), 1);
17400
+ } else {
17401
+ delete fromCurrent[fromLast];
17402
+ }
17403
+ if (Array.isArray(current)) {
17404
+ current.splice(Number(lastPart), 0, movedValue);
17405
+ } else {
17406
+ current[lastPart] = movedValue;
17407
+ }
17408
+ break;
17409
+ case "copy":
17410
+ const copyFromParts = patch.from.split("/").filter((p5) => p5 !== "");
17411
+ let copyCurrent = this._state;
17412
+ for (let i7 = 0; i7 < copyFromParts.length - 1; i7++) {
17413
+ copyCurrent = copyCurrent[this._unescapePath(copyFromParts[i7])];
17414
+ }
17415
+ const copyFromLast = this._unescapePath(copyFromParts[copyFromParts.length - 1]);
17416
+ const copiedValue = JSON.parse(JSON.stringify(copyCurrent[copyFromLast]));
17417
+ current[lastPart] = copiedValue;
17418
+ break;
17419
+ case "test":
17420
+ if (current[lastPart] !== value) {
17421
+ throw new Error(`Test failed at ${path}: expected ${value}, got ${current[lastPart]}`);
17422
+ }
17423
+ break;
17424
+ default:
17425
+ console.warn(`Unknown patch operation: ${op}`);
17426
+ }
17427
+ } catch (error2) {
17428
+ console.error("Failed to apply patch", { patch, error: error2 });
17429
+ throw error2;
17430
+ }
17431
+ }
17432
+ /**
17433
+ * Unescape JSON Pointer path component
17434
+ * See: https://tools.ietf.org/html/rfc6901
17435
+ */
17436
+ _unescapePath(part) {
17437
+ return part.replace(/~1/g, "/").replace(/~0/g, "~");
17438
+ }
17439
+ /**
17440
+ * Clear all state
17441
+ */
17442
+ reset(newState) {
17443
+ this._state = { ...newState };
17444
+ this._paginationState.clear();
17445
+ this.emit("reset");
17446
+ }
17447
+ };
17448
+ var GlobalSessionManager = class {
17449
+ constructor() {
17450
+ this._sessions = /* @__PURE__ */ new Map();
17451
+ }
17452
+ /**
17453
+ * Create or get a photon session
17454
+ */
17455
+ createOrGetSession(name2, initialState) {
17456
+ if (this._sessions.has(name2)) {
17457
+ return this._sessions.get(name2);
17458
+ }
17459
+ const session = new PhotonSessionProxy({
17460
+ name: name2,
17461
+ initialState
17462
+ });
17463
+ Object.keys(initialState).forEach((key) => {
17464
+ session.makeProperty(key);
17465
+ });
17466
+ this._sessions.set(name2, session);
17467
+ return session;
17468
+ }
17469
+ /**
17470
+ * Get existing session
17471
+ */
17472
+ getSession(name2) {
17473
+ return this._sessions.get(name2);
17474
+ }
17475
+ /**
17476
+ * Apply patches to a session
17477
+ */
17478
+ applyPatches(name2, patches) {
17479
+ const session = this._sessions.get(name2);
17480
+ if (!session) {
17481
+ console.warn(`No session found for: ${name2}`);
17482
+ return false;
17483
+ }
17484
+ session.applyPatches(patches);
17485
+ return true;
17486
+ }
17487
+ /**
17488
+ * Remove session
17489
+ */
17490
+ removeSession(name2) {
17491
+ this._sessions.delete(name2);
17492
+ }
17493
+ /**
17494
+ * Get all session names
17495
+ */
17496
+ getSessionNames() {
17497
+ return Array.from(this._sessions.keys());
17498
+ }
17499
+ };
17500
+ var globalSessionManager = null;
17501
+ function getGlobalSessionManager() {
17502
+ if (!globalSessionManager) {
17503
+ globalSessionManager = new GlobalSessionManager();
17504
+ if (typeof window !== "undefined") {
17505
+ window.__photonSessionManager = globalSessionManager;
17506
+ }
17507
+ }
17508
+ return globalSessionManager;
17509
+ }
17510
+ function initializeGlobalPhotonSession(photonName, initialState) {
17511
+ const manager = getGlobalSessionManager();
17512
+ const session = manager.createOrGetSession(photonName, initialState);
17513
+ if (typeof window !== "undefined") {
17514
+ const varName = photonName.charAt(0).toLocaleLowerCase() + photonName.slice(1);
17515
+ window[varName] = session;
17516
+ if (photonName === "Boards") {
17517
+ window.boards = session;
17518
+ window.boardsSession = session;
17519
+ }
17520
+ }
17521
+ return session;
17522
+ }
17523
+
17203
17524
  // src/auto-ui/frontend/services/mcp-client.ts
17204
17525
  var MCPClientService = class {
17205
17526
  // Discard operations older than 30s
@@ -17904,9 +18225,21 @@ var MCPClientService = class {
17904
18225
  case "photon/refresh-needed":
17905
18226
  this.emit("refresh-needed", notification.params);
17906
18227
  break;
17907
- case "photon/state-changed":
18228
+ case "state-changed":
18229
+ const params = notification.params;
18230
+ if (params?.instance && params?.patches) {
18231
+ try {
18232
+ const manager = getGlobalSessionManager();
18233
+ manager.applyPatches(params.instance, params.patches);
18234
+ } catch (error2) {
18235
+ console.error("Failed to apply patches to session", error2);
18236
+ }
18237
+ }
17908
18238
  this.emit("state-changed", notification.params);
17909
18239
  break;
18240
+ case "photon/notification":
18241
+ this.emit("photon-notification", notification.params);
18242
+ break;
17910
18243
  // MCP Apps standard notifications
17911
18244
  case "ui/notifications/tool-result":
17912
18245
  this.emit("ui-tool-result", notification.params);
@@ -17931,6 +18264,439 @@ var MCPClientService = class {
17931
18264
  };
17932
18265
  var mcpClient = new MCPClientService();
17933
18266
 
18267
+ // src/auto-ui/frontend/services/viewport-aware-proxy.ts
18268
+ var ViewportAwareProxy = class {
18269
+ constructor(photonName, methodName, mcpClient2, options = {}) {
18270
+ // Cache management
18271
+ this._cache = /* @__PURE__ */ new Map();
18272
+ this._viewport = { start: 0, end: 20 };
18273
+ this._pendingRanges = /* @__PURE__ */ new Set();
18274
+ this._pagination = {
18275
+ totalCount: 0,
18276
+ start: 0,
18277
+ end: 0,
18278
+ hasMore: false
18279
+ };
18280
+ // Event listeners
18281
+ this._listeners = /* @__PURE__ */ new Map();
18282
+ this._photonName = photonName;
18283
+ this._methodName = methodName;
18284
+ this._mcpClient = mcpClient2;
18285
+ this._fetchOptions = {
18286
+ pageSize: options.pageSize ?? 20,
18287
+ bufferSize: options.bufferSize ?? 5,
18288
+ maxCacheSize: options.maxCacheSize ?? 1e3
18289
+ };
18290
+ }
18291
+ /**
18292
+ * Initialize with paginated response from server
18293
+ * Server should return: { items: [...], _pagination: {...} }
18294
+ */
18295
+ initializeWithResponse(response) {
18296
+ if (!response || typeof response !== "object") {
18297
+ return;
18298
+ }
18299
+ const { items = [], _pagination } = response;
18300
+ if (_pagination) {
18301
+ this._pagination = _pagination;
18302
+ items.forEach((item, offset) => {
18303
+ this._cache.set(_pagination.start + offset, item);
18304
+ });
18305
+ this._emit("initialized", { pagination: this._pagination });
18306
+ }
18307
+ }
18308
+ /**
18309
+ * Set visible viewport (what user sees on screen)
18310
+ * Automatically fetches data for this range + buffer
18311
+ */
18312
+ async setViewport(start, end) {
18313
+ this._viewport = { start, end };
18314
+ const bufferedStart = Math.max(0, start - this._fetchOptions.bufferSize);
18315
+ const bufferedEnd = Math.min(this._pagination.totalCount, end + this._fetchOptions.bufferSize);
18316
+ const missingRanges = this._findMissingRanges(bufferedStart, bufferedEnd);
18317
+ await Promise.all(
18318
+ missingRanges.map(
18319
+ (range) => this._fetchRange(range.start, range.end).catch((err) => {
18320
+ console.error(`Failed to fetch range [${range.start}, ${range.end}]`, err);
18321
+ })
18322
+ )
18323
+ );
18324
+ }
18325
+ /**
18326
+ * Get items in current viewport (array-like interface)
18327
+ */
18328
+ get items() {
18329
+ const items = [];
18330
+ for (let i7 = this._viewport.start; i7 < this._viewport.end; i7++) {
18331
+ if (this._cache.has(i7)) {
18332
+ items.push(this._cache.get(i7));
18333
+ }
18334
+ }
18335
+ return items;
18336
+ }
18337
+ /**
18338
+ * Get item by index
18339
+ */
18340
+ getItem(index2) {
18341
+ return this._cache.get(index2);
18342
+ }
18343
+ /**
18344
+ * Get total count of items (if available)
18345
+ */
18346
+ get totalCount() {
18347
+ return this._pagination.totalCount;
18348
+ }
18349
+ /**
18350
+ * Get current pagination state
18351
+ */
18352
+ get pagination() {
18353
+ return { ...this._pagination };
18354
+ }
18355
+ /**
18356
+ * Get current viewport
18357
+ */
18358
+ get viewport() {
18359
+ return { ...this._viewport };
18360
+ }
18361
+ /**
18362
+ * Get cache size (for debugging)
18363
+ */
18364
+ get cacheSize() {
18365
+ return this._cache.size;
18366
+ }
18367
+ /**
18368
+ * Check if a range is cached
18369
+ */
18370
+ isCached(start, end) {
18371
+ for (let i7 = start; i7 < end; i7++) {
18372
+ if (!this._cache.has(i7)) {
18373
+ return false;
18374
+ }
18375
+ }
18376
+ return true;
18377
+ }
18378
+ /**
18379
+ * Subscribe to events
18380
+ */
18381
+ on(event, callback2) {
18382
+ if (!this._listeners.has(event)) {
18383
+ this._listeners.set(event, /* @__PURE__ */ new Set());
18384
+ }
18385
+ this._listeners.get(event).add(callback2);
18386
+ }
18387
+ /**
18388
+ * Unsubscribe from events
18389
+ */
18390
+ off(event, callback2) {
18391
+ const listeners = this._listeners.get(event);
18392
+ if (listeners) {
18393
+ listeners.delete(callback2);
18394
+ }
18395
+ }
18396
+ /**
18397
+ * Clear cache
18398
+ */
18399
+ clearCache() {
18400
+ this._cache.clear();
18401
+ this._emit("cache-cleared");
18402
+ }
18403
+ /**
18404
+ * Apply patches from state-changed events
18405
+ * Updates cache with new/modified items
18406
+ */
18407
+ applyPatches(patches) {
18408
+ for (const patch of patches) {
18409
+ this._applyPatch(patch);
18410
+ }
18411
+ this._emit("patched", { patches });
18412
+ }
18413
+ /**
18414
+ * Find which ranges need to be fetched
18415
+ */
18416
+ _findMissingRanges(start, end) {
18417
+ const missing = [];
18418
+ for (let i7 = start; i7 < end; i7++) {
18419
+ if (!this._cache.has(i7)) {
18420
+ missing.push(i7);
18421
+ }
18422
+ }
18423
+ const ranges = [];
18424
+ if (missing.length === 0) return ranges;
18425
+ let rangeStart = missing[0];
18426
+ let rangeEnd2 = missing[0];
18427
+ for (let i7 = 1; i7 < missing.length; i7++) {
18428
+ if (missing[i7] === rangeEnd2 + 1) {
18429
+ rangeEnd2 = missing[i7];
18430
+ } else {
18431
+ ranges.push({ start: rangeStart, end: rangeEnd2 + 1 });
18432
+ rangeStart = missing[i7];
18433
+ rangeEnd2 = missing[i7];
18434
+ }
18435
+ }
18436
+ ranges.push({ start: rangeStart, end: rangeEnd2 + 1 });
18437
+ return ranges;
18438
+ }
18439
+ /**
18440
+ * Fetch a range of items via MCP
18441
+ */
18442
+ async _fetchRange(start, end) {
18443
+ const rangeKey = `${start}-${end}`;
18444
+ if (this._pendingRanges.has(rangeKey)) {
18445
+ return;
18446
+ }
18447
+ this._pendingRanges.add(rangeKey);
18448
+ try {
18449
+ const toolName = `${this._photonName}/${this._methodName}`;
18450
+ const result = await this._mcpClient.callTool(toolName, {
18451
+ start,
18452
+ limit: Math.min(end - start, this._fetchOptions.pageSize)
18453
+ });
18454
+ if (result.isError) {
18455
+ console.error(`Failed to fetch ${toolName}[${start}:${end}]`, result);
18456
+ return;
18457
+ }
18458
+ const parsed = this._mcpClient.parseToolResult(result);
18459
+ if (parsed && typeof parsed === "object") {
18460
+ const { items = [], _pagination } = parsed;
18461
+ if (_pagination) {
18462
+ this._pagination = _pagination;
18463
+ }
18464
+ items.forEach((item, offset) => {
18465
+ this._cache.set(start + offset, item);
18466
+ });
18467
+ if (this._cache.size > this._fetchOptions.maxCacheSize) {
18468
+ this._pruneCache();
18469
+ }
18470
+ this._emit("fetched", { start, end, itemCount: items.length });
18471
+ }
18472
+ } finally {
18473
+ this._pendingRanges.delete(rangeKey);
18474
+ }
18475
+ }
18476
+ /**
18477
+ * Apply a single JSON Patch to cache
18478
+ */
18479
+ _applyPatch(patch) {
18480
+ const { op, path } = patch;
18481
+ const match = path.match(/\/items\/(\d+)/);
18482
+ if (!match) return;
18483
+ const index2 = parseInt(match[1], 10);
18484
+ switch (op) {
18485
+ case "add": {
18486
+ const newCache = /* @__PURE__ */ new Map();
18487
+ for (const [key, value] of this._cache) {
18488
+ if (key >= index2) {
18489
+ newCache.set(key + 1, value);
18490
+ } else {
18491
+ newCache.set(key, value);
18492
+ }
18493
+ }
18494
+ newCache.set(index2, patch.value);
18495
+ this._cache = newCache;
18496
+ this._pagination.totalCount++;
18497
+ break;
18498
+ }
18499
+ case "remove": {
18500
+ const newCache = /* @__PURE__ */ new Map();
18501
+ for (const [key, value] of this._cache) {
18502
+ if (key > index2) {
18503
+ newCache.set(key - 1, value);
18504
+ } else if (key < index2) {
18505
+ newCache.set(key, value);
18506
+ }
18507
+ }
18508
+ this._cache = newCache;
18509
+ this._pagination.totalCount--;
18510
+ break;
18511
+ }
18512
+ case "replace": {
18513
+ this._cache.set(index2, patch.value);
18514
+ break;
18515
+ }
18516
+ }
18517
+ }
18518
+ /**
18519
+ * Remove least-recently-used items from cache when it gets too large
18520
+ */
18521
+ _pruneCache() {
18522
+ const toRemove = this._cache.size - this._fetchOptions.maxCacheSize;
18523
+ if (toRemove <= 0) return;
18524
+ const viewportCenter = this._viewport.start + (this._viewport.end - this._viewport.start) / 2;
18525
+ const sortedKeys = Array.from(this._cache.keys()).sort(
18526
+ (a5, b3) => Math.abs(a5 - viewportCenter) - Math.abs(b3 - viewportCenter)
18527
+ );
18528
+ for (let i7 = 0; i7 < toRemove && i7 < sortedKeys.length; i7++) {
18529
+ this._cache.delete(sortedKeys[i7]);
18530
+ }
18531
+ }
18532
+ /**
18533
+ * Emit event to listeners
18534
+ */
18535
+ _emit(event, data) {
18536
+ const listeners = this._listeners.get(event);
18537
+ if (listeners) {
18538
+ for (const callback2 of listeners) {
18539
+ try {
18540
+ callback2(data);
18541
+ } catch (err) {
18542
+ console.error(`Error in ${event} listener:`, err);
18543
+ }
18544
+ }
18545
+ }
18546
+ }
18547
+ };
18548
+
18549
+ // src/auto-ui/frontend/services/viewport-manager.ts
18550
+ function getPageSizeForClient() {
18551
+ if (typeof window !== "undefined" && window.innerWidth < 600) {
18552
+ return 10;
18553
+ }
18554
+ if (typeof window !== "undefined" && window.innerWidth < 1024) {
18555
+ return 25;
18556
+ }
18557
+ return 50;
18558
+ }
18559
+ var ViewportManager = class {
18560
+ constructor(config3) {
18561
+ this.observer = null;
18562
+ this.visibleSentinels = /* @__PURE__ */ new Set();
18563
+ this.lastVisibleRange = { start: 0, end: 0 };
18564
+ this.lastScrollDirection = "none";
18565
+ this.changeCallbacks = [];
18566
+ this.element = config3.element;
18567
+ this.pageSize = config3.pageSize;
18568
+ this.paddingAbove = config3.paddingAbove ?? this.pageSize;
18569
+ this.paddingBelow = config3.paddingBelow ?? this.pageSize * 2;
18570
+ this.debug = config3.debug ?? false;
18571
+ this.log("ViewportManager initialized", {
18572
+ pageSize: this.pageSize,
18573
+ paddingAbove: this.paddingAbove,
18574
+ paddingBelow: this.paddingBelow
18575
+ });
18576
+ this.initializeObserver();
18577
+ }
18578
+ initializeObserver() {
18579
+ const options = {
18580
+ root: null,
18581
+ rootMargin: "100px",
18582
+ threshold: 0
18583
+ };
18584
+ this.observer = new IntersectionObserver((entries) => {
18585
+ for (const entry of entries) {
18586
+ const index2 = this.getSentinelIndex(entry.target);
18587
+ if (index2 !== null) {
18588
+ if (entry.isIntersecting) {
18589
+ this.visibleSentinels.add(index2);
18590
+ } else {
18591
+ this.visibleSentinels.delete(index2);
18592
+ }
18593
+ }
18594
+ }
18595
+ this.updateVisibleRange();
18596
+ }, options);
18597
+ }
18598
+ createSentinel(index2) {
18599
+ const sentinel = document.createElement("div");
18600
+ sentinel.setAttribute("data-viewport-sentinel", String(index2));
18601
+ sentinel.style.height = "0px";
18602
+ sentinel.style.overflow = "hidden";
18603
+ return sentinel;
18604
+ }
18605
+ observeSentinel(sentinel) {
18606
+ if (this.observer) {
18607
+ this.observer.observe(sentinel);
18608
+ }
18609
+ }
18610
+ unobserveSentinel(sentinel) {
18611
+ if (this.observer) {
18612
+ this.observer.unobserve(sentinel);
18613
+ }
18614
+ }
18615
+ getSentinelIndex(element) {
18616
+ const attr = element.getAttribute("data-viewport-sentinel");
18617
+ return attr !== null ? parseInt(attr, 10) : null;
18618
+ }
18619
+ updateVisibleRange() {
18620
+ if (this.visibleSentinels.size === 0) {
18621
+ return;
18622
+ }
18623
+ const indices = Array.from(this.visibleSentinels).sort((a5, b3) => a5 - b3);
18624
+ const visibleStart = indices[0];
18625
+ const visibleEnd = indices[indices.length - 1] + 1;
18626
+ const direction = this.determineScrollDirection(visibleStart);
18627
+ const bufferStart = Math.max(
18628
+ 0,
18629
+ visibleStart - Math.ceil(this.paddingAbove / this.pageSize) * this.pageSize
18630
+ );
18631
+ const bufferEnd = visibleEnd + Math.ceil(this.paddingBelow / this.pageSize) * this.pageSize;
18632
+ const rangeChanged = visibleStart !== this.lastVisibleRange.start || visibleEnd !== this.lastVisibleRange.end;
18633
+ if (rangeChanged) {
18634
+ this.lastVisibleRange = { start: visibleStart, end: visibleEnd };
18635
+ this.lastScrollDirection = direction;
18636
+ const event = {
18637
+ visibleRange: { start: visibleStart, end: visibleEnd },
18638
+ bufferRange: { start: bufferStart, end: bufferEnd },
18639
+ scrollDirection: direction,
18640
+ timestamp: Date.now()
18641
+ };
18642
+ this.log("Viewport changed", event);
18643
+ for (const callback2 of this.changeCallbacks) {
18644
+ callback2(event);
18645
+ }
18646
+ }
18647
+ }
18648
+ determineScrollDirection(newStart) {
18649
+ if (newStart < this.lastVisibleRange.start) {
18650
+ return "up";
18651
+ } else if (newStart > this.lastVisibleRange.start) {
18652
+ return "down";
18653
+ }
18654
+ return "none";
18655
+ }
18656
+ getVisibleRange() {
18657
+ return this.lastVisibleRange;
18658
+ }
18659
+ getBufferRange(totalItems) {
18660
+ const { start, end } = this.lastVisibleRange;
18661
+ const bufferStart = Math.max(0, start - this.paddingAbove);
18662
+ const bufferEnd = Math.min(totalItems, end + this.paddingBelow);
18663
+ return { start: bufferStart, end: bufferEnd };
18664
+ }
18665
+ getScrollDirection() {
18666
+ return this.lastScrollDirection;
18667
+ }
18668
+ getPageSize() {
18669
+ return this.pageSize;
18670
+ }
18671
+ onChange(callback2) {
18672
+ this.changeCallbacks.push(callback2);
18673
+ }
18674
+ offChange(callback2) {
18675
+ this.changeCallbacks = this.changeCallbacks.filter((cb) => cb !== callback2);
18676
+ }
18677
+ setPageSize(newPageSize) {
18678
+ if (newPageSize !== this.pageSize) {
18679
+ this.pageSize = newPageSize;
18680
+ this.log("Page size updated", { newPageSize });
18681
+ this.updateVisibleRange();
18682
+ }
18683
+ }
18684
+ destroy() {
18685
+ if (this.observer) {
18686
+ this.observer.disconnect();
18687
+ this.observer = null;
18688
+ }
18689
+ this.changeCallbacks = [];
18690
+ this.visibleSentinels.clear();
18691
+ this.log("ViewportManager destroyed");
18692
+ }
18693
+ log(message, data) {
18694
+ if (this.debug) {
18695
+ console.log(`[ViewportManager] ${message}`, data);
18696
+ }
18697
+ }
18698
+ };
18699
+
17934
18700
  // src/auto-ui/frontend/components/beam-app.ts
17935
18701
  var THEME_STORAGE_KEY = "beam-theme";
17936
18702
  var PROTOCOL_STORAGE_KEY = "beam-protocol";
@@ -17994,6 +18760,10 @@ var BeamApp = class extends i4 {
17994
18760
  this._promptArguments = {};
17995
18761
  this._renderedPrompt = "";
17996
18762
  this._resourceContent = "";
18763
+ this._splitPanels = [];
18764
+ this._methodPickerOpen = false;
18765
+ this._methodPickerPanelId = null;
18766
+ this._nextPanelId = 0;
17997
18767
  // Collection auto-subscription for ReactiveArray/Map/Set events
17998
18768
  this._collectionUnsubscribes = [];
17999
18769
  this._currentCollectionName = null;
@@ -18004,6 +18774,10 @@ var BeamApp = class extends i4 {
18004
18774
  this._fileIdCounter = 0;
18005
18775
  // Deep link URL for setOpenInAppUrl
18006
18776
  this._openInAppUrl = null;
18777
+ // PWA install state
18778
+ this._pwaInstallPrompt = null;
18779
+ this._pwaIsStandalone = false;
18780
+ this._pwaCurrentPhoton = null;
18007
18781
  this._handleDocumentClick = (e8) => {
18008
18782
  const path = e8.composedPath();
18009
18783
  if (this._showSettingsMenu) {
@@ -18021,11 +18795,11 @@ var BeamApp = class extends i4 {
18021
18795
  }
18022
18796
  };
18023
18797
  this._initialConnectDone = false;
18024
- this._handleHashChange = () => {
18798
+ this._handleRouteChange = () => {
18025
18799
  void (async () => {
18026
- const fullHash = window.location.hash.slice(1);
18027
- const [pathPart, queryPart] = fullHash.split("?");
18028
- const [photonName, methodName] = pathPart.split("/");
18800
+ const fullPath = window.location.pathname.slice(1);
18801
+ const queryPart = window.location.search.slice(1);
18802
+ const [photonName, methodName] = fullPath.split("/");
18029
18803
  let sharedParams = {};
18030
18804
  if (queryPart) {
18031
18805
  const params = new URLSearchParams(queryPart);
@@ -18042,6 +18816,9 @@ var BeamApp = class extends i4 {
18042
18816
  }
18043
18817
  }
18044
18818
  }
18819
+ this._methodPickerOpen = false;
18820
+ this._methodPickerPanelId = null;
18821
+ this._splitPanels = [];
18045
18822
  if (!photonName || photonName === "home") {
18046
18823
  this._selectedPhoton = null;
18047
18824
  this._selectedMethod = null;
@@ -18068,7 +18845,8 @@ var BeamApp = class extends i4 {
18068
18845
  return;
18069
18846
  }
18070
18847
  if (methodName && photon.methods) {
18071
- const method = photon.methods.find((m3) => m3.name === methodName);
18848
+ const [firstMethodName, secondMethodName] = methodName.split("+");
18849
+ const method = photon.methods.find((m3) => m3.name === firstMethodName);
18072
18850
  if (method) {
18073
18851
  if (Object.keys(sharedParams).length > 0) {
18074
18852
  this._sharedFormParams = sharedParams;
@@ -18079,6 +18857,22 @@ var BeamApp = class extends i4 {
18079
18857
  }
18080
18858
  this._selectedMethod = method;
18081
18859
  this._view = "form";
18860
+ if (secondMethodName) {
18861
+ const urlPath = location.pathname;
18862
+ const methodPart = urlPath.split("/").pop() || "";
18863
+ const methodNames = methodPart.split("+");
18864
+ for (let i7 = 1; i7 < methodNames.length; i7++) {
18865
+ const name2 = methodNames[i7];
18866
+ if (name2 === "source") {
18867
+ this._addPanel("source");
18868
+ } else {
18869
+ const panelMethod = photon.methods.find((m3) => m3.name === name2);
18870
+ if (panelMethod) {
18871
+ this._addPanel("method", panelMethod);
18872
+ }
18873
+ }
18874
+ }
18875
+ }
18082
18876
  if (Object.keys(sharedParams).length === 0) {
18083
18877
  this._maybeAutoInvoke(method);
18084
18878
  }
@@ -18098,18 +18892,15 @@ var BeamApp = class extends i4 {
18098
18892
  }
18099
18893
  })();
18100
18894
  };
18101
- this._handlePopState = () => {
18102
- void this._handleHashChange();
18103
- };
18104
18895
  this._goHome = () => {
18105
18896
  this._selectedPhoton = null;
18106
18897
  this._selectedMethod = null;
18107
18898
  this._lastResult = null;
18108
- this._updateHash();
18899
+ this._updateRoute();
18109
18900
  };
18110
18901
  this._runTests = async () => {
18111
18902
  if (!this._selectedPhoton || this._runningTests) return;
18112
- const testMethods = this._getTestMethods();
18903
+ const testMethods = await this._fetchTestList(this._selectedPhoton.name);
18113
18904
  if (testMethods.length === 0) return;
18114
18905
  this._runningTests = true;
18115
18906
  this._testResults = [];
@@ -18195,6 +18986,7 @@ var BeamApp = class extends i4 {
18195
18986
  `All photon tests: ${passed}/${total} passed`
18196
18987
  );
18197
18988
  };
18989
+ this._pickerDismissHandler = null;
18198
18990
  this._toggleSettingsMenu = () => {
18199
18991
  this._showSettingsMenu = !this._showSettingsMenu;
18200
18992
  };
@@ -18213,8 +19005,45 @@ var BeamApp = class extends i4 {
18213
19005
  };
18214
19006
  this._launchAsApp = () => {
18215
19007
  this._closeSettingsMenu();
18216
- if (this._selectedPhoton) {
18217
- window.open(`/api/pwa/app?photon=${encodeURIComponent(this._selectedPhoton.name)}`, "_blank");
19008
+ if (!this._selectedPhoton) return;
19009
+ if (this._pwaInstallPrompt) {
19010
+ void (async () => {
19011
+ try {
19012
+ await fetch("/api/pwa/configure", {
19013
+ method: "POST",
19014
+ headers: { "Content-Type": "application/json" },
19015
+ body: JSON.stringify({
19016
+ photon: this._selectedPhoton.name,
19017
+ port: location.port || "4100"
19018
+ }),
19019
+ signal: AbortSignal.timeout(5e3)
19020
+ });
19021
+ } catch {
19022
+ }
19023
+ const photonName = this._selectedPhoton.name;
19024
+ const onInstalled = () => {
19025
+ window.open(`/app/${encodeURIComponent(photonName)}`, "_blank");
19026
+ };
19027
+ window.addEventListener("appinstalled", onInstalled, { once: true });
19028
+ const result = await this._pwaInstallPrompt.prompt();
19029
+ this._log("info", `PWA install prompt result: ${result?.outcome}`);
19030
+ if (result?.outcome !== "accepted") {
19031
+ window.removeEventListener("appinstalled", onInstalled);
19032
+ }
19033
+ this._pwaInstallPrompt = null;
19034
+ })();
19035
+ } else {
19036
+ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
19037
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
19038
+ if (isSafari || isIOS) {
19039
+ const hint = isIOS ? 'Tap the Share button, then "Add to Home Screen"' : "Use File > Add to Dock to install this app";
19040
+ showToast(hint, "info");
19041
+ } else {
19042
+ showToast(
19043
+ "Install not ready yet \u2014 the service worker needs a moment to generate icons. Try again in a few seconds.",
19044
+ "info"
19045
+ );
19046
+ }
18218
19047
  }
18219
19048
  };
18220
19049
  this._handleRemove = async () => {
@@ -18380,7 +19209,7 @@ var BeamApp = class extends i4 {
18380
19209
  }
18381
19210
  }
18382
19211
  this._view = "config";
18383
- this._updateHash();
19212
+ this._updateRoute();
18384
19213
  };
18385
19214
  this._toggleRememberValues = () => {
18386
19215
  this._rememberFormValues = !this._rememberFormValues;
@@ -18965,7 +19794,7 @@ var BeamApp = class extends i4 {
18965
19794
  }
18966
19795
  if (e8.key === "Enter" && this._selectedMethod && this._view === "list") {
18967
19796
  this._view = "form";
18968
- this._updateHash();
19797
+ this._updateRoute();
18969
19798
  return;
18970
19799
  }
18971
19800
  if (e8.key === "r") {
@@ -19001,12 +19830,12 @@ var BeamApp = class extends i4 {
19001
19830
  this._focusMode = !this._focusMode;
19002
19831
  if (this._focusMode) {
19003
19832
  this.classList.add("focus-mode");
19004
- const hash2 = window.location.hash.replace(/\?focus=1/, "");
19005
- history.replaceState(null, "", hash2 + "?focus=1");
19833
+ const path = window.location.pathname;
19834
+ history.replaceState(null, "", path + "?focus=1");
19006
19835
  } else {
19007
19836
  this.classList.remove("focus-mode");
19008
- const hash2 = window.location.hash.replace(/\?focus=1/, "");
19009
- history.replaceState(null, "", hash2 || "#");
19837
+ const path = window.location.pathname;
19838
+ history.replaceState(null, "", path);
19010
19839
  }
19011
19840
  };
19012
19841
  this._handleFullscreen = () => {
@@ -19105,9 +19934,25 @@ var BeamApp = class extends i4 {
19105
19934
  case "fullscreen":
19106
19935
  this._handleFullscreen();
19107
19936
  break;
19937
+ case "install-app":
19938
+ this._launchAsApp();
19939
+ break;
19940
+ }
19941
+ break;
19942
+ }
19943
+ case "split-view:add":
19944
+ this._showMethodPicker();
19945
+ break;
19946
+ case "split-view:change": {
19947
+ const method = this._selectedPhoton?.methods?.find((m3) => m3.name === e8.detail.method);
19948
+ if (method && this._splitPanels.length > 0) {
19949
+ this._changePanelMethod(this._splitPanels[0].id, method);
19108
19950
  }
19109
19951
  break;
19110
19952
  }
19953
+ case "split-view:remove":
19954
+ this._closeSecondPanel();
19955
+ break;
19111
19956
  }
19112
19957
  };
19113
19958
  this._startEditingDescription = () => {
@@ -19307,7 +20152,7 @@ var BeamApp = class extends i4 {
19307
20152
  if (method) {
19308
20153
  this._selectedMethod = method;
19309
20154
  this._view = "form";
19310
- this._updateHash();
20155
+ this._updateRoute();
19311
20156
  this._log("info", `Starting ${targetName}/${action}...`);
19312
20157
  showToast(`Starting ${action}...`, "info");
19313
20158
  } else {
@@ -19334,15 +20179,21 @@ var BeamApp = class extends i4 {
19334
20179
  */
19335
20180
  _handleGlobalMethodSelect(photon, method) {
19336
20181
  this._teardownActiveCustomUI();
20182
+ if (this._selectedPhoton?.name !== photon.name) {
20183
+ this._closeSecondPanel();
20184
+ }
19337
20185
  this._selectedPhoton = photon;
19338
20186
  if (this._willAutoInvoke(method)) {
19339
20187
  this._isExecuting = true;
19340
20188
  }
19341
20189
  this._selectedMethod = method;
19342
20190
  this._view = "form";
19343
- this._updateHash();
20191
+ this._updateRoute();
19344
20192
  this._maybeAutoInvoke(method);
19345
20193
  }
20194
+ get _splitViewEnabled() {
20195
+ return this._splitPanels.length > 0;
20196
+ }
19346
20197
  connectedCallback() {
19347
20198
  super.connectedCallback();
19348
20199
  const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
@@ -19374,20 +20225,114 @@ var BeamApp = class extends i4 {
19374
20225
  }
19375
20226
  document.addEventListener("click", this._handleDocumentClick);
19376
20227
  void this._connectMCP();
19377
- window.addEventListener("hashchange", this._handleHashChange);
19378
- window.addEventListener("popstate", this._handlePopState);
20228
+ this._setupNotificationHandlers();
20229
+ window.addEventListener("popstate", this._handleRouteChange);
19379
20230
  window.addEventListener("message", this._handleBridgeMessage);
19380
20231
  window.addEventListener("keydown", this._handleKeydown);
20232
+ this._initPWA();
19381
20233
  }
19382
20234
  disconnectedCallback() {
19383
20235
  super.disconnectedCallback();
19384
- window.removeEventListener("hashchange", this._handleHashChange);
19385
- window.removeEventListener("popstate", this._handlePopState);
20236
+ window.removeEventListener("popstate", this._handleRouteChange);
19386
20237
  window.removeEventListener("message", this._handleBridgeMessage);
19387
20238
  window.removeEventListener("keydown", this._handleKeydown);
19388
20239
  document.removeEventListener("click", this._handleDocumentClick);
19389
20240
  this._cleanupCollectionSubscriptions();
19390
20241
  }
20242
+ willUpdate(changedProperties) {
20243
+ super.willUpdate(changedProperties);
20244
+ if (changedProperties.has("_selectedPhoton")) {
20245
+ const name2 = this._selectedPhoton?.name || null;
20246
+ if (name2 !== this._pwaCurrentPhoton) {
20247
+ this._pwaCurrentPhoton = name2;
20248
+ if (name2) {
20249
+ this._setupPWAManifest(name2);
20250
+ }
20251
+ }
20252
+ }
20253
+ }
20254
+ // ---------------------------------------------------------------------------
20255
+ // PWA — Progressive Web App support
20256
+ // ---------------------------------------------------------------------------
20257
+ _initPWA() {
20258
+ this._pwaIsStandalone = window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true;
20259
+ if ("serviceWorker" in navigator) {
20260
+ navigator.serviceWorker.register("/sw.js", { scope: "/" }).then((reg) => this._log("info", `PWA SW registered: ${reg.scope}`, true)).catch((err) => this._log("warn", `PWA SW registration failed: ${err}`, true));
20261
+ }
20262
+ window.addEventListener("beforeinstallprompt", (e8) => {
20263
+ this._pwaInstallPrompt = e8;
20264
+ });
20265
+ window.addEventListener("appinstalled", () => {
20266
+ this._pwaInstallPrompt = null;
20267
+ this._log("info", "PWA app installed");
20268
+ });
20269
+ }
20270
+ /**
20271
+ * Set up notification handlers for photon notifications.
20272
+ * When a notification arrives for a photon that cares about that event type,
20273
+ * bring the window to focus (especially important for PWAs/background tabs).
20274
+ */
20275
+ _setupNotificationHandlers() {
20276
+ mcpClient.on("photon-notification", (notification) => {
20277
+ const { photon, type, priority } = notification;
20278
+ if (!photon || !type) return;
20279
+ const sidebarElement = this.querySelector("beam-sidebar");
20280
+ if (sidebarElement && sidebarElement.isNotificationWatched) {
20281
+ const isWatched = sidebarElement.isNotificationWatched(photon, type);
20282
+ if (isWatched) {
20283
+ window.focus();
20284
+ console.log(`\u{1F4E1} Window focused for ${photon} notification: ${type}`);
20285
+ sidebarElement.updatePhotonWarmth?.(photon);
20286
+ }
20287
+ }
20288
+ });
20289
+ }
20290
+ /**
20291
+ * Set up the PWA manifest & icons for the currently selected photon.
20292
+ * Called whenever the selected photon changes. The manifest points to
20293
+ * /api/pwa/icon-png URLs which the service worker intercepts and renders
20294
+ * as real PNGs via OffscreenCanvas — no client-side blob URL needed.
20295
+ */
20296
+ _setupPWAManifest(photonName) {
20297
+ let manifestLink = document.head.querySelector('link[rel="manifest"]');
20298
+ if (!manifestLink) {
20299
+ manifestLink = document.createElement("link");
20300
+ manifestLink.rel = "manifest";
20301
+ document.head.appendChild(manifestLink);
20302
+ }
20303
+ manifestLink.href = `/api/pwa/manifest.json?photon=${encodeURIComponent(photonName)}`;
20304
+ let appleIcon = document.head.querySelector('link[rel="apple-touch-icon"]');
20305
+ if (!appleIcon) {
20306
+ appleIcon = document.createElement("link");
20307
+ appleIcon.rel = "apple-touch-icon";
20308
+ document.head.appendChild(appleIcon);
20309
+ }
20310
+ appleIcon.href = `/api/pwa/icon?photon=${encodeURIComponent(photonName)}`;
20311
+ let appleTitleMeta = document.head.querySelector(
20312
+ 'meta[name="apple-mobile-web-app-title"]'
20313
+ );
20314
+ if (!appleTitleMeta) {
20315
+ appleTitleMeta = document.createElement("meta");
20316
+ appleTitleMeta.name = "apple-mobile-web-app-title";
20317
+ document.head.appendChild(appleTitleMeta);
20318
+ }
20319
+ appleTitleMeta.content = photonName;
20320
+ this._prewarmIconCache(photonName);
20321
+ this._log("info", `PWA manifest set for "${photonName}"`, true);
20322
+ }
20323
+ /**
20324
+ * Pre-warm the service worker icon cache by fetching the PNG icon URLs.
20325
+ * This ensures the rendered emoji icons are cached before the user
20326
+ * attempts to install the PWA, improving the install dialog appearance.
20327
+ */
20328
+ _prewarmIconCache(photonName) {
20329
+ const sizes = [192, 512];
20330
+ sizes.forEach((size) => {
20331
+ const url2 = `/api/pwa/icon-png?photon=${encodeURIComponent(photonName)}&size=${size}`;
20332
+ fetch(url2, { mode: "no-cors" }).catch(() => {
20333
+ });
20334
+ });
20335
+ }
19391
20336
  async _connectMCP() {
19392
20337
  try {
19393
20338
  mcpClient.on("connect", () => {
@@ -19415,12 +20360,24 @@ var BeamApp = class extends i4 {
19415
20360
  }
19416
20361
  this._initialConnectDone = true;
19417
20362
  void this._checkForUpdates();
19418
- if (window.location.hash) {
19419
- void this._handleHashChange();
20363
+ if (window.location.pathname !== "/") {
20364
+ void this._handleRouteChange();
19420
20365
  } else if (!this._selectedPhoton && this._photons.length > 0) {
19421
20366
  const firstUserPhoton = this._photons.find((p5) => !p5.internal);
19422
- if (firstUserPhoton) this._selectedPhoton = firstUserPhoton;
19423
- this._updateHash(true);
20367
+ if (firstUserPhoton) {
20368
+ this._selectedPhoton = firstUserPhoton;
20369
+ if (firstUserPhoton.isApp && firstUserPhoton.appEntry) {
20370
+ if (this._willAutoInvoke(firstUserPhoton.appEntry)) {
20371
+ this._isExecuting = true;
20372
+ }
20373
+ this._selectedMethod = firstUserPhoton.appEntry;
20374
+ this._view = "form";
20375
+ this._maybeAutoInvoke(firstUserPhoton.appEntry);
20376
+ } else {
20377
+ this._view = "list";
20378
+ }
20379
+ }
20380
+ this._updateRoute(true);
19424
20381
  }
19425
20382
  })();
19426
20383
  });
@@ -19439,15 +20396,37 @@ var BeamApp = class extends i4 {
19439
20396
  this._photons = photons;
19440
20397
  this._externalMCPs = externalMCPs;
19441
20398
  this._addUnconfiguredPhotons();
19442
- if (!this._selectedPhoton && window.location.hash !== "#home") {
19443
- const newUserPhoton = this._photons.find(
19444
- (p5) => !p5.internal && p5.configured && !prevNames.has(p5.name)
19445
- );
19446
- if (newUserPhoton) {
19447
- this._selectedPhoton = newUserPhoton;
19448
- this._welcomePhase = "welcome";
19449
- this._view = "list";
19450
- this._updateHash(true);
20399
+ if (this._selectedPhoton) {
20400
+ const updated = this._photons.find((p5) => p5.name === this._selectedPhoton?.name);
20401
+ if (updated) {
20402
+ const wasApp = this._selectedPhoton.isApp;
20403
+ this._selectedPhoton = updated;
20404
+ if (!wasApp && updated.isApp && updated.appEntry && this._view === "list") {
20405
+ if (this._willAutoInvoke(updated.appEntry)) {
20406
+ this._isExecuting = true;
20407
+ }
20408
+ this._selectedMethod = updated.appEntry;
20409
+ this._view = "form";
20410
+ this._updateRoute(true);
20411
+ this._maybeAutoInvoke(updated.appEntry);
20412
+ }
20413
+ }
20414
+ }
20415
+ if (!this._selectedPhoton && window.location.pathname !== "/") {
20416
+ const pathPhotonName = window.location.pathname.slice(1).split("/")[0];
20417
+ const routePhoton = pathPhotonName ? this._photons.find((p5) => p5.name === pathPhotonName) : null;
20418
+ if (routePhoton) {
20419
+ void this._handleRouteChange();
20420
+ } else {
20421
+ const newUserPhoton = this._photons.find(
20422
+ (p5) => !p5.internal && p5.configured && !prevNames.has(p5.name)
20423
+ );
20424
+ if (newUserPhoton) {
20425
+ this._selectedPhoton = newUserPhoton;
20426
+ this._welcomePhase = "welcome";
20427
+ this._view = "list";
20428
+ this._updateRoute(true);
20429
+ }
19451
20430
  }
19452
20431
  }
19453
20432
  })();
@@ -19511,7 +20490,7 @@ var BeamApp = class extends i4 {
19511
20490
  this._selectedPhoton = updated;
19512
20491
  }
19513
20492
  } else {
19514
- if (window.location.hash === "#home") return;
20493
+ if (window.location.pathname === "/") return;
19515
20494
  const newUserPhoton = this._photons.find(
19516
20495
  (p5) => !p5.internal && p5.configured && !prevNames.has(p5.name)
19517
20496
  );
@@ -19540,7 +20519,7 @@ var BeamApp = class extends i4 {
19540
20519
  } else {
19541
20520
  this._view = "list";
19542
20521
  }
19543
- this._updateHash(true);
20522
+ this._updateRoute(true);
19544
20523
  }
19545
20524
  }
19546
20525
  }
@@ -19592,7 +20571,7 @@ var BeamApp = class extends i4 {
19592
20571
  } else {
19593
20572
  this._view = "list";
19594
20573
  }
19595
- this._updateHash(true);
20574
+ this._updateRoute(true);
19596
20575
  }
19597
20576
  }
19598
20577
  });
@@ -19703,20 +20682,27 @@ var BeamApp = class extends i4 {
19703
20682
  }
19704
20683
  }
19705
20684
  }
19706
- _updateHash(replace2 = false) {
19707
- let hash2;
20685
+ _updateRoute(replace2 = false) {
20686
+ let path;
19708
20687
  if (!this._selectedPhoton) {
19709
- hash2 = "home";
20688
+ path = "/";
19710
20689
  } else {
19711
- hash2 = this._selectedPhoton.name;
20690
+ path = "/" + this._selectedPhoton.name;
19712
20691
  if (this._selectedMethod) {
19713
- hash2 += `/${this._selectedMethod.name}`;
20692
+ path += `/${this._selectedMethod.name}`;
20693
+ for (const panel of this._splitPanels) {
20694
+ if (panel.type === "method" && panel.method) {
20695
+ path += `+${panel.method.name}`;
20696
+ } else if (panel.type === "source") {
20697
+ path += `+source`;
20698
+ }
20699
+ }
19714
20700
  }
19715
20701
  }
19716
20702
  if (replace2) {
19717
- history.replaceState(null, "", `#${hash2}`);
20703
+ history.replaceState(null, "", path);
19718
20704
  } else {
19719
- history.pushState(null, "", `#${hash2}`);
20705
+ history.pushState(null, "", path);
19720
20706
  }
19721
20707
  }
19722
20708
  /**
@@ -19767,6 +20753,21 @@ var BeamApp = class extends i4 {
19767
20753
  if (!this._selectedPhoton?.methods) return [];
19768
20754
  return this._selectedPhoton.methods.filter((m3) => m3.name.startsWith("test_") || m3.name.startsWith("test")).map((m3) => m3.name);
19769
20755
  }
20756
+ /** Fetch all tests (external .test.ts + inline) from the server */
20757
+ async _fetchTestList(photonName) {
20758
+ try {
20759
+ const res = await fetch(`/api/test/list?photon=${encodeURIComponent(photonName)}`, {
20760
+ signal: AbortSignal.timeout(5e3)
20761
+ });
20762
+ if (!res.ok) return this._getTestMethods();
20763
+ const data = await res.json();
20764
+ if (data.tests && data.tests.length > 0) {
20765
+ return data.tests.map((t8) => t8.name);
20766
+ }
20767
+ } catch {
20768
+ }
20769
+ return this._getTestMethods();
20770
+ }
19770
20771
  _getAllTestMethods() {
19771
20772
  const results = [];
19772
20773
  for (const p5 of this._photons) {
@@ -20090,7 +21091,7 @@ var BeamApp = class extends i4 {
20090
21091
  }}
20091
21092
  @diagnostics=${() => {
20092
21093
  this._view = "diagnostics";
20093
- this._updateHash();
21094
+ this._updateRoute();
20094
21095
  }}
20095
21096
  @open-studio=${(e8) => {
20096
21097
  const photon = this._photons.find((p5) => p5.name === e8.detail.photonName);
@@ -20110,7 +21111,39 @@ var BeamApp = class extends i4 {
20110
21111
  ></beam-sidebar>
20111
21112
  </nav>
20112
21113
 
20113
- <main class="main-area" id="main-content" tabindex="-1" aria-label="Main content">
21114
+ <main
21115
+ class="main-area"
21116
+ id="main-content"
21117
+ tabindex="-1"
21118
+ aria-label="Main content"
21119
+ style="${this._splitViewEnabled ? "overflow: hidden !important;" : ""}"
21120
+ >
21121
+ ${this._selectedPhoton && this._selectedMethod ? b2`<button
21122
+ class="beam-back-btn"
21123
+ @click=${() => this._handleBackFromMethod()}
21124
+ @mouseenter=${(e8) => {
21125
+ e8.target.style.color = "var(--t-primary)";
21126
+ e8.target.style.borderColor = "var(--accent-primary)";
21127
+ }}
21128
+ @mouseleave=${(e8) => {
21129
+ e8.target.style.color = "var(--t-muted)";
21130
+ e8.target.style.borderColor = "var(--border-glass)";
21131
+ }}
21132
+ title="Back to ${this._selectedPhoton.name}"
21133
+ >
21134
+ <svg
21135
+ width="16"
21136
+ height="16"
21137
+ viewBox="0 0 24 24"
21138
+ fill="none"
21139
+ stroke="currentColor"
21140
+ stroke-width="2"
21141
+ stroke-linecap="round"
21142
+ stroke-linejoin="round"
21143
+ >
21144
+ <path d="m15 18-6-6 6-6" />
21145
+ </svg>
21146
+ </button>` : ""}
20114
21147
  ${this._selectedPhoton ? b2`<button
20115
21148
  class="beam-fullscreen-btn"
20116
21149
  @click=${this._toggleFocusMode}
@@ -20546,7 +21579,7 @@ var BeamApp = class extends i4 {
20546
21579
  }}
20547
21580
  @click=${() => {
20548
21581
  this._view = "diagnostics";
20549
- this._updateHash();
21582
+ this._updateRoute();
20550
21583
  }}
20551
21584
  >
20552
21585
  <span style="font-size: 1rem; width: 20px; text-align: center;">🔍</span>
@@ -20755,7 +21788,7 @@ ${photon.errorMessage || "Unknown error"}</pre
20755
21788
  @select=${(e8) => {
20756
21789
  this._selectedMethod = e8.detail.method;
20757
21790
  this._view = "form";
20758
- this._updateHash();
21791
+ this._updateRoute();
20759
21792
  }}
20760
21793
  ></method-card>
20761
21794
  `
@@ -20772,7 +21805,7 @@ ${photon.errorMessage || "Unknown error"}</pre
20772
21805
  if (this._selectedMethod.linkedUi) {
20773
21806
  const isAppMain = this._selectedPhoton.isApp && this._selectedMethod.name === "main";
20774
21807
  const otherMethods = isAppMain ? this._getVisibleMethods().filter((m3) => m3.name !== "main") : [];
20775
- const isExternalMCP2 = this._selectedPhoton.isExternalMCP;
21808
+ const isExternalMCP = this._selectedPhoton.isExternalMCP;
20776
21809
  const appRenderer = this._isExecuting ? b2`
20777
21810
  <div
20778
21811
  style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; height: calc(100vh - 140px);"
@@ -20782,7 +21815,7 @@ ${photon.errorMessage || "Unknown error"}</pre
20782
21815
  ${this._progress?.message || "Starting up\u2026"}
20783
21816
  </span>
20784
21817
  </div>
20785
- ` : isExternalMCP2 ? b2`
21818
+ ` : isExternalMCP ? b2`
20786
21819
  <mcp-app-renderer
20787
21820
  .mcpName=${this._selectedPhoton.name}
20788
21821
  .appUri=${`ui://${this._selectedPhoton.name}/${this._selectedMethod.linkedUi}`}
@@ -20801,7 +21834,48 @@ ${photon.errorMessage || "Unknown error"}</pre
20801
21834
  ></custom-ui-renderer>
20802
21835
  `;
20803
21836
  if (isAppMain) {
21837
+ if (this._splitPanels.length > 0) {
21838
+ return b2`
21839
+ <div style="display: flex; gap: 1px; height: calc(100vh - 60px); overflow: hidden;">
21840
+ <!-- App Panel (primary) — no title bar, app has its own chrome -->
21841
+ <div style="flex: 1; min-height: 0; overflow: hidden;">${appRenderer}</div>
21842
+
21843
+ <!-- Additional Panels -->
21844
+ ${this._splitPanels.map(
21845
+ (panel) => b2`
21846
+ <div style="flex: 1; min-height: 0; background: var(--bg-panel);">
21847
+ ${panel.type === "method" ? this._renderSinglePanel(this._buildAdditionalPanelOpts(panel)) : this._renderSourcePanel(panel.id)}
21848
+ </div>
21849
+ `
21850
+ )}
21851
+ </div>
21852
+ `;
21853
+ }
20804
21854
  return b2`
21855
+ <!-- Floating add-panel button for app view — sits next to focus button -->
21856
+ <div
21857
+ style="position: sticky; top: calc(-1 * var(--space-lg)); float: right; z-index: 100; margin-top: calc(-1 * var(--space-lg)); margin-right: 6px;"
21858
+ >
21859
+ <button
21860
+ @click=${() => {
21861
+ this._methodPickerOpen = !this._methodPickerOpen;
21862
+ this._methodPickerPanelId = null;
21863
+ }}
21864
+ style="width: 28px; height: 28px; border-radius: var(--radius-sm); background: var(--bg-glass); border: 1px solid var(--border-glass); color: var(--t-muted); cursor: pointer; font-size: 14px; font-weight: 700; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; backdrop-filter: blur(8px);"
21865
+ @mouseenter=${(e8) => {
21866
+ e8.target.style.color = "var(--accent-secondary)";
21867
+ e8.target.style.borderColor = "var(--accent-secondary)";
21868
+ }}
21869
+ @mouseleave=${(e8) => {
21870
+ e8.target.style.color = "var(--t-muted)";
21871
+ e8.target.style.borderColor = "var(--border-glass)";
21872
+ }}
21873
+ title="Add panel"
21874
+ >
21875
+ +
21876
+ </button>
21877
+ ${this._methodPickerOpen && this._methodPickerPanelId === null ? this._renderMethodPickerPopover() : ""}
21878
+ </div>
20805
21879
  <app-layout
20806
21880
  .photonName=${this._selectedPhoton.name}
20807
21881
  .photonIcon=${this._selectedPhoton.appEntry?.icon || "\u{1F4F1}"}
@@ -20844,61 +21918,72 @@ ${photon.errorMessage || "Unknown error"}</pre
20844
21918
  </app-layout>
20845
21919
  `;
20846
21920
  }
21921
+ if (this._splitPanels.length > 0) {
21922
+ return b2`
21923
+ <div style="display: flex; gap: 1px; height: calc(100vh - 60px); overflow: hidden;">
21924
+ <!-- Linked UI Panel (primary) -->
21925
+ <div
21926
+ style="flex: 1; min-height: 0; display: flex; flex-direction: column; position: relative;"
21927
+ >
21928
+ <div
21929
+ style="display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid var(--border-glass); background: var(--bg-glass); flex-shrink: 0;"
21930
+ >
21931
+ <span style="font-size: 12px; font-weight: 500; color: var(--t-primary);"
21932
+ >${this._selectedMethod.name}</span
21933
+ >
21934
+ <div style="position: relative; flex-shrink: 0;">
21935
+ <button
21936
+ @click=${() => {
21937
+ this._methodPickerOpen = !this._methodPickerOpen;
21938
+ this._methodPickerPanelId = null;
21939
+ }}
21940
+ style="padding: 4px 8px; background: none; color: var(--accent-secondary); border: 1px solid var(--accent-secondary); border-radius: 3px; cursor: pointer; font-size: 14px; font-weight: 700; transition: all 0.2s ease;"
21941
+ title="Add panel"
21942
+ >
21943
+ +
21944
+ </button>
21945
+ ${this._methodPickerOpen && this._methodPickerPanelId === null ? this._renderMethodPickerPopover() : ""}
21946
+ </div>
21947
+ </div>
21948
+ <div style="flex: 1; min-height: 0; overflow: hidden;">${appRenderer}</div>
21949
+ </div>
21950
+
21951
+ <!-- Additional Panels -->
21952
+ ${this._splitPanels.map(
21953
+ (panel) => b2`
21954
+ <div style="flex: 1; min-height: 0; background: var(--bg-panel);">
21955
+ ${panel.type === "method" ? this._renderSinglePanel(this._buildAdditionalPanelOpts(panel)) : this._renderSourcePanel(panel.id)}
21956
+ </div>
21957
+ `
21958
+ )}
21959
+ </div>
21960
+ `;
21961
+ }
20847
21962
  return b2`
20848
- <context-bar
20849
- .photon=${this._selectedPhoton}
20850
- .breadcrumbs=${[
20851
- {
20852
- label: this._currentInstance !== "default" ? `${this._selectedPhoton.name}:${this._currentInstance}` : this._selectedPhoton.name,
20853
- action: "back"
20854
- },
20855
- { label: this._selectedMethod.name }
20856
- ]}
20857
- .live=${this._currentCollectionName !== null}
20858
- .showEdit=${false}
20859
- .showConfigure=${false}
20860
- .showCopyConfig=${false}
20861
- .overflowItems=${[]}
20862
- .instanceSelectorMode=${this._instanceSelectorMode}
20863
- .autoInstance=${this._autoInstance}
20864
- @context-action=${this._handleContextAction}
20865
- ></context-bar>
20866
- <div
20867
- class="glass-panel"
20868
- style="padding: 0; overflow: hidden; min-height: calc(100vh - 80px); margin-top: var(--space-md);"
20869
- >
20870
- ${appRenderer}
21963
+ <div style="position: relative;">
21964
+ <div style="position: absolute; top: 16px; right: 16px; z-index: 50;">
21965
+ <button
21966
+ @click=${() => {
21967
+ this._methodPickerOpen = !this._methodPickerOpen;
21968
+ this._methodPickerPanelId = null;
21969
+ }}
21970
+ style="padding: 4px 8px; background: var(--bg-glass); color: var(--accent-secondary); border: 1px solid var(--accent-secondary); border-radius: 3px; cursor: pointer; font-size: 14px; font-weight: 700; transition: all 0.2s ease; backdrop-filter: blur(8px);"
21971
+ title="Add panel"
21972
+ >
21973
+ +
21974
+ </button>
21975
+ ${this._methodPickerOpen && this._methodPickerPanelId === null ? this._renderMethodPickerPopover() : ""}
21976
+ </div>
21977
+ <div
21978
+ class="glass-panel"
21979
+ style="padding: 0; overflow: hidden; min-height: calc(100vh - 80px); margin-top: var(--space-md);"
21980
+ >
21981
+ ${appRenderer}
21982
+ </div>
20871
21983
  </div>
20872
21984
  `;
20873
21985
  }
20874
- const isExternalMCP = this._selectedPhoton.isExternalMCP;
20875
- return b2`
20876
- <context-bar
20877
- .photon=${this._selectedPhoton}
20878
- .breadcrumbs=${[
20879
- {
20880
- label: this._currentInstance !== "default" ? `${this._selectedPhoton.name}:${this._currentInstance}` : this._selectedPhoton.name,
20881
- action: "back"
20882
- },
20883
- { label: this._selectedMethod.name }
20884
- ]}
20885
- .live=${this._currentCollectionName !== null}
20886
- .showEdit=${false}
20887
- .showConfigure=${false}
20888
- .showCopyConfig=${false}
20889
- .overflowItems=${this._buildOverflowItems({
20890
- showRefresh: !isExternalMCP,
20891
- showRename: false,
20892
- showViewSource: false,
20893
- showDelete: false,
20894
- showHelp: !isExternalMCP
20895
- })}
20896
- .instanceSelectorMode=${this._instanceSelectorMode}
20897
- .autoInstance=${this._autoInstance}
20898
- @context-action=${this._handleContextAction}
20899
- ></context-bar>
20900
- ${this._renderMethodContent()}
20901
- `;
21986
+ return b2` ${this._renderMethodContent()} `;
20902
21987
  }
20903
21988
  return b2`
20904
21989
  ${this._renderPhotonToolbar()} ${this._editingIcon ? this._renderEmojiPicker() : ""}
@@ -21026,6 +22111,7 @@ ${photon.errorMessage || "Unknown error"}</pre
21026
22111
  }
21027
22112
  }
21028
22113
  async _handlePhotonSelect(e8) {
22114
+ this._closeSecondPanel();
21029
22115
  this._selectedPhoton = e8.detail.photon;
21030
22116
  this._selectedMethod = null;
21031
22117
  this._lastResult = null;
@@ -21033,12 +22119,12 @@ ${photon.errorMessage || "Unknown error"}</pre
21033
22119
  this._currentInstance = "default";
21034
22120
  if (this._selectedPhoton.configured === false) {
21035
22121
  this._view = "config";
21036
- this._updateHash();
22122
+ this._updateRoute();
21037
22123
  return;
21038
22124
  }
21039
22125
  if (this._selectedPhoton.isExternalMCP && this._selectedPhoton.hasMcpApp) {
21040
22126
  this._view = "mcp-app";
21041
- this._updateHash();
22127
+ this._updateRoute();
21042
22128
  return;
21043
22129
  }
21044
22130
  if (this._selectedPhoton.stateful && this._selectedPhoton.configured) {
@@ -21065,13 +22151,13 @@ ${photon.errorMessage || "Unknown error"}</pre
21065
22151
  }
21066
22152
  this._selectedMethod = this._selectedPhoton.appEntry;
21067
22153
  this._view = "form";
21068
- this._updateHash();
22154
+ this._updateRoute();
21069
22155
  this._maybeAutoInvoke(this._selectedPhoton.appEntry);
21070
22156
  return;
21071
22157
  } else {
21072
22158
  this._view = "list";
21073
22159
  }
21074
- this._updateHash();
22160
+ this._updateRoute();
21075
22161
  }
21076
22162
  /** Fetch available instances for a stateful photon from the server */
21077
22163
  async _fetchInstances(photonName) {
@@ -21114,7 +22200,7 @@ ${photon.errorMessage || "Unknown error"}</pre
21114
22200
  /**
21115
22201
  * Trigger instance switch via MCP elicitation (calls _use without name).
21116
22202
  * The transport layer shows an elicitation modal with available instances.
21117
- * After _use succeeds, the transport broadcasts photon/state-changed which
22203
+ * After _use succeeds, the transport broadcasts state-changed which
21118
22204
  * triggers _silentRefresh() to update the result and notifies custom UIs.
21119
22205
  */
21120
22206
  async _switchInstance() {
@@ -21150,7 +22236,7 @@ ${photon.errorMessage || "Unknown error"}</pre
21150
22236
  this._selectedMethod = e8.detail.method;
21151
22237
  this._lastResult = null;
21152
22238
  this._view = "form";
21153
- this._updateHash();
22239
+ this._updateRoute();
21154
22240
  this._maybeAutoInvoke(e8.detail.method);
21155
22241
  }
21156
22242
  /**
@@ -21212,57 +22298,507 @@ ${photon.errorMessage || "Unknown error"}</pre
21212
22298
  </div>
21213
22299
  `;
21214
22300
  }
22301
+ if (this._splitPanels.length > 0) {
22302
+ return b2`
22303
+ <div style="display: flex; gap: 1px; height: 100%; overflow: hidden;">
22304
+ <!-- Primary Panel -->
22305
+ <div style="flex: 1; min-height: 0; background: var(--bg-panel);">
22306
+ ${this._renderSinglePanel({
22307
+ photon: this._selectedPhoton,
22308
+ method: this._selectedMethod,
22309
+ result: this._lastResult,
22310
+ executing: this._isExecuting,
22311
+ progress: this._progress,
22312
+ formParams: this._lastFormParams,
22313
+ onSubmit: (e8) => void this._handleExecute(e8),
22314
+ onCancel: () => this._handleBackFromMethod(),
22315
+ panelLabel: "Primary",
22316
+ instance: this._currentInstance,
22317
+ instances: this._instances,
22318
+ onInstanceChange: (instance) => this._handleLeftPanelInstanceChange(instance),
22319
+ allMethods: this._selectedPhoton?.methods || [],
22320
+ onMethodChange: (method) => this._handleLeftPanelMethodChange(method),
22321
+ panelSide: "primary",
22322
+ onPanelAction: (action) => this._handlePrimaryPanelAction(action),
22323
+ onInstanceAction: (detail) => void this._handleInstanceAction(detail),
22324
+ isStateful: !!this._selectedPhoton?.stateful,
22325
+ isLive: this._currentCollectionName !== null,
22326
+ overflowItems: this._buildOverflowItems({
22327
+ showRefresh: !this._selectedPhoton?.isExternalMCP,
22328
+ showRename: false,
22329
+ showViewSource: false,
22330
+ showDelete: false,
22331
+ showHelp: !this._selectedPhoton?.isExternalMCP
22332
+ }),
22333
+ onOverflowSelect: (id2) => this._handleOverflowAction(id2)
22334
+ })}
22335
+ </div>
22336
+
22337
+ <!-- Additional Panels -->
22338
+ ${this._splitPanels.map(
22339
+ (panel) => b2`
22340
+ <div style="flex: 1; min-height: 0; background: var(--bg-panel);">
22341
+ ${panel.type === "method" ? this._renderSinglePanel(this._buildAdditionalPanelOpts(panel)) : this._renderSourcePanel(panel.id)}
22342
+ </div>
22343
+ `
22344
+ )}
22345
+ </div>
22346
+ `;
22347
+ }
22348
+ return this._renderSinglePanel({
22349
+ photon: this._selectedPhoton,
22350
+ method: this._selectedMethod,
22351
+ result: this._lastResult,
22352
+ executing: this._isExecuting,
22353
+ progress: this._progress,
22354
+ formParams: this._lastFormParams,
22355
+ onSubmit: (e8) => void this._handleExecute(e8),
22356
+ onCancel: () => this._handleBackFromMethod(),
22357
+ panelLabel: "Primary",
22358
+ instance: this._currentInstance,
22359
+ instances: this._instances,
22360
+ allMethods: this._selectedPhoton?.methods || [],
22361
+ onMethodChange: (method) => {
22362
+ this._selectedMethod = method;
22363
+ this._lastResult = null;
22364
+ this._lastFormParams = {};
22365
+ if (this._willAutoInvoke(method)) {
22366
+ void this._handleExecute(new CustomEvent("execute", { detail: { args: {} } }));
22367
+ }
22368
+ this._updateRoute();
22369
+ },
22370
+ panelSide: "primary",
22371
+ onPanelAction: (action) => this._handlePrimaryPanelAction(action),
22372
+ onInstanceAction: (detail) => void this._handleInstanceAction(detail),
22373
+ isStateful: !!this._selectedPhoton?.stateful,
22374
+ isLive: this._currentCollectionName !== null,
22375
+ overflowItems: this._buildOverflowItems({
22376
+ showRefresh: !this._selectedPhoton?.isExternalMCP,
22377
+ showRename: false,
22378
+ showViewSource: false,
22379
+ showDelete: false,
22380
+ showHelp: !this._selectedPhoton?.isExternalMCP
22381
+ }),
22382
+ onOverflowSelect: (id2) => this._handleOverflowAction(id2)
22383
+ });
22384
+ }
22385
+ /** Render a single panel with self-contained header */
22386
+ _renderSinglePanel(opts) {
22387
+ const isSplit = this._splitPanels.length > 0;
21215
22388
  return b2`
21216
- <div class="glass-panel method-detail">
21217
- <h2>${this._selectedMethod.name}</h2>
21218
- ${this._renderDescription(this._selectedMethod.description)}
21219
- <invoke-form
21220
- .params=${this._selectedMethod.params}
21221
- .loading=${this._isExecuting}
21222
- .photonName=${this._selectedPhoton.name}
21223
- .methodName=${this._selectedMethod.name}
21224
- .rememberValues=${this._rememberFormValues}
21225
- .sharedValues=${this._sharedFormParams}
21226
- @submit=${(e8) => {
21227
- void this._handleExecute(e8);
22389
+ <div
22390
+ class="glass-panel method-detail"
22391
+ style="${isSplit ? "border-radius: 0; height: 100%;" : ""} display: flex; flex-direction: column;"
22392
+ >
22393
+ <!-- Panel Header: LED + Method ▼ + instance-panel + [+] + [⋯] + [×] -->
22394
+ <div
22395
+ style="display: flex; align-items: center; gap: 8px; padding-bottom: 12px; margin-bottom: 12px; border-bottom: 1px solid var(--border-glass); flex-shrink: 0; position: relative;"
22396
+ >
22397
+ <!-- LED dot (stateful/live indicator) -->
22398
+ ${opts.isStateful ? b2`<span
22399
+ style="width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; background: ${opts.isLive ? "var(--color-success, #22c55e)" : "var(--t-tertiary, #666)"}; box-shadow: ${opts.isLive ? "0 0 6px var(--color-success, #22c55e)" : "none"};"
22400
+ title="${opts.isLive ? "Live \u2014 stateful photon with active subscription" : "Stateful photon"}"
22401
+ ></span>` : ""}
22402
+
22403
+ <!-- Method Selector (name as dropdown) -->
22404
+ <select
22405
+ .value=${opts.method.name}
22406
+ @change=${(e8) => {
22407
+ const methodName = e8.target.value;
22408
+ const method = opts.allMethods?.find((m3) => m3.name === methodName);
22409
+ if (method && opts.onMethodChange) {
22410
+ opts.onMethodChange(method);
22411
+ }
21228
22412
  }}
21229
- @cancel=${() => this._handleBackFromMethod()}
21230
- ></invoke-form>
22413
+ style="padding: 6px 8px; border-radius: 4px; border: 1px solid var(--border-glass); background: var(--bg-glass); color: var(--t-primary); font-size: 13px; font-weight: 600; flex-shrink: 0;"
22414
+ >
22415
+ ${opts.allMethods?.map(
22416
+ (m3) => b2`<option .selected=${m3.name === opts.method.name} value=${m3.name}>
22417
+ ${m3.name}
22418
+ </option>`
22419
+ ) || b2`<option value=${opts.method.name}>${opts.method.name}</option>`}
22420
+ </select>
21231
22421
 
21232
- ${this._progress ? b2`
21233
- <div class="progress-container">
21234
- <div class="progress-bar-wrapper">
21235
- <div
21236
- class="progress-bar ${this._progress.value < 0 ? "indeterminate" : ""}"
21237
- style="width: ${this._progress.value < 0 ? "30%" : Math.round(this._progress.value * 100) + "%"}"
21238
- ></div>
22422
+ <div style="flex: 1;"></div>
22423
+
22424
+ <!-- Rich Instance Selector -->
22425
+ ${opts.isStateful && opts.instances && opts.instances.length > 0 && opts.onInstanceAction ? b2`
22426
+ <instance-panel
22427
+ .instanceName=${opts.instance || "default"}
22428
+ .instances=${opts.instances}
22429
+ .photonName=${opts.photon.name}
22430
+ .selectorMode=${this._instanceSelectorMode}
22431
+ .autoInstance=${this._autoInstance}
22432
+ @instance-action=${(e8) => opts.onInstanceAction?.(e8.detail)}
22433
+ ></instance-panel>
22434
+ ` : ""}
22435
+
22436
+ <!-- Add panel button (primary only) -->
22437
+ ${opts.panelSide === "primary" ? b2`
22438
+ <div style="position: relative; flex-shrink: 0;">
22439
+ <button
22440
+ @click=${() => opts.onPanelAction?.("toggle-picker")}
22441
+ style="padding: 4px 8px; background: none; color: var(--accent-secondary); border: 1px solid var(--accent-secondary); border-radius: 3px; cursor: pointer; font-size: 14px; font-weight: 700; transition: all 0.2s ease;"
22442
+ title="Add panel"
22443
+ >
22444
+ +
22445
+ </button>
22446
+ ${this._methodPickerOpen && this._methodPickerPanelId === null ? this._renderMethodPickerPopover() : ""}
21239
22447
  </div>
21240
- <div class="progress-text">
21241
- <span>${this._progress.message}</span>
21242
- ${this._progress.value >= 0 ? b2`
21243
- <span class="progress-percentage"
21244
- >${Math.round(this._progress.value * 100)}%</span
21245
- >
21246
- ` : ""}
22448
+ ` : ""}
22449
+
22450
+ <!-- Overflow menu -->
22451
+ ${opts.overflowItems && opts.overflowItems.length > 0 ? b2`
22452
+ <overflow-menu
22453
+ .items=${opts.overflowItems}
22454
+ @menu-select=${(e8) => opts.onOverflowSelect?.(e8.detail.id)}
22455
+ ></overflow-menu>
22456
+ ` : ""}
22457
+
22458
+ <!-- Close button (additional panels only) -->
22459
+ ${opts.panelSide === "additional" ? b2`
22460
+ <button
22461
+ @click=${() => opts.onPanelAction?.("close")}
22462
+ style="padding: 4px 8px; background: none; color: var(--color-error); border: 1px solid var(--color-error); border-radius: 3px; cursor: pointer; font-size: 14px; font-weight: 700; flex-shrink: 0; transition: all 0.2s ease;"
22463
+ title="Close panel"
22464
+ >
22465
+ ×
22466
+ </button>
22467
+ ` : ""}
22468
+ </div>
22469
+ <!-- Panel Content (scrollable) -->
22470
+ <div
22471
+ style="display: flex; flex-direction: column; flex: 1; min-height: 0; overflow-y: auto;"
22472
+ >
22473
+ ${this._renderDescription(opts.method.description)}
22474
+ <invoke-form
22475
+ .params=${opts.method.params}
22476
+ .loading=${opts.executing}
22477
+ .photonName=${opts.photon.name}
22478
+ .methodName=${opts.method.name}
22479
+ .rememberValues=${this._rememberFormValues}
22480
+ .sharedValues=${opts.formParams}
22481
+ @submit=${opts.onSubmit}
22482
+ @cancel=${opts.onCancel}
22483
+ ></invoke-form>
22484
+
22485
+ ${opts.progress ? b2`
22486
+ <div class="progress-container">
22487
+ <div class="progress-bar-wrapper">
22488
+ <div
22489
+ class="progress-bar ${opts.progress.value < 0 ? "indeterminate" : ""}"
22490
+ style="width: ${opts.progress.value < 0 ? "30%" : Math.round(opts.progress.value * 100) + "%"}"
22491
+ ></div>
22492
+ </div>
22493
+ <div class="progress-text">
22494
+ <span>${opts.progress.message}</span>
22495
+ </div>
22496
+ </div>
22497
+ ` : ""}
22498
+ ${opts.result !== null ? b2`
22499
+ <result-viewer
22500
+ .result=${opts.result}
22501
+ .outputFormat=${opts.method?.outputFormat}
22502
+ .layoutHints=${opts.method?.layoutHints}
22503
+ .theme=${this._theme}
22504
+ .live=${this._currentCollectionName !== null}
22505
+ .resultKey=${opts.photon && opts.method ? `${opts.photon.name}/${opts.method.name}` : void 0}
22506
+ @share=${() => this._handleShareResult()}
22507
+ ></result-viewer>
22508
+ ` : b2`
22509
+ <div class="empty-state-inline result-empty">
22510
+ <span class="empty-state-icon">${play}</span>
22511
+ <span>Run the method to see results here</span>
21247
22512
  </div>
22513
+ `}
22514
+ </div>
22515
+ </div>
22516
+ `;
22517
+ }
22518
+ /** Close all split panels and return to single-panel mode */
22519
+ _closeSecondPanel() {
22520
+ this._splitPanels = [];
22521
+ this._methodPickerOpen = false;
22522
+ this._methodPickerPanelId = null;
22523
+ this._updateRoute();
22524
+ }
22525
+ /** Handle instance change for primary panel */
22526
+ _handleLeftPanelInstanceChange(instance) {
22527
+ this._currentInstance = instance;
22528
+ sessionStorage.setItem(`photon-instance:${this._selectedPhoton.name}`, instance);
22529
+ this._lastResult = null;
22530
+ }
22531
+ /** Add a new split panel */
22532
+ _addPanel(type, method) {
22533
+ if (this._splitPanels.length >= 2) return;
22534
+ const panel = {
22535
+ id: `panel-${++this._nextPanelId}`,
22536
+ type,
22537
+ method,
22538
+ result: null,
22539
+ executing: false,
22540
+ progress: null,
22541
+ formParams: {},
22542
+ instance: this._currentInstance || "default"
22543
+ };
22544
+ this._splitPanels = [...this._splitPanels, panel];
22545
+ if (type === "method" && method && this._willAutoInvoke(method)) {
22546
+ void this._executePanelMethod(panel.id, {});
22547
+ }
22548
+ this._methodPickerOpen = false;
22549
+ this._methodPickerPanelId = null;
22550
+ this._cleanupPickerDismiss();
22551
+ this._updateRoute();
22552
+ }
22553
+ /** Remove a split panel by id */
22554
+ _removePanel(panelId) {
22555
+ this._splitPanels = this._splitPanels.filter((p5) => p5.id !== panelId);
22556
+ this._updateRoute();
22557
+ }
22558
+ /** Update a split panel's state */
22559
+ _updatePanel(panelId, updates) {
22560
+ this._splitPanels = this._splitPanels.map((p5) => p5.id === panelId ? { ...p5, ...updates } : p5);
22561
+ }
22562
+ /** Execute a method in a split panel */
22563
+ async _executePanelMethod(panelId, args) {
22564
+ const panel = this._splitPanels.find((p5) => p5.id === panelId);
22565
+ if (!panel || panel.type !== "method" || !panel.method) return;
22566
+ this._updatePanel(panelId, { executing: true, result: null, progress: null, formParams: args });
22567
+ try {
22568
+ const toolName = `${this._selectedPhoton.name}/${panel.method.name}`;
22569
+ const result = await mcpClient.callTool(
22570
+ toolName,
22571
+ args,
22572
+ void 0,
22573
+ panel.instance || this._currentInstance,
22574
+ (progress) => {
22575
+ this._updatePanel(panelId, { progress });
22576
+ }
22577
+ );
22578
+ this._updatePanel(panelId, {
22579
+ result: mcpClient.parseToolResult(result),
22580
+ executing: false,
22581
+ progress: null
22582
+ });
22583
+ } catch (error2) {
22584
+ showToast(`Error: ${error2.message}`, { type: "error" });
22585
+ this._updatePanel(panelId, {
22586
+ result: { error: error2.message },
22587
+ executing: false,
22588
+ progress: null
22589
+ });
22590
+ }
22591
+ }
22592
+ /** Change the method in a split panel */
22593
+ _changePanelMethod(panelId, method) {
22594
+ this._updatePanel(panelId, { method, result: null, formParams: {} });
22595
+ if (this._willAutoInvoke(method)) {
22596
+ void this._executePanelMethod(panelId, {});
22597
+ }
22598
+ this._updateRoute();
22599
+ }
22600
+ /** Change the instance in a split panel */
22601
+ _changePanelInstance(panelId, instance) {
22602
+ this._updatePanel(panelId, { instance, result: null });
22603
+ }
22604
+ /** Open a method in a new split panel (backwards-compatible entry point) */
22605
+ _openInSecondPanel(photon, method) {
22606
+ this._addPanel("method", method);
22607
+ }
22608
+ /** Show method picker popover */
22609
+ _showMethodPicker() {
22610
+ this._methodPickerOpen = !this._methodPickerOpen;
22611
+ this._methodPickerPanelId = null;
22612
+ }
22613
+ /** Clean up picker dismiss listener */
22614
+ _cleanupPickerDismiss() {
22615
+ if (this._pickerDismissHandler) {
22616
+ window.removeEventListener("mousedown", this._pickerDismissHandler, true);
22617
+ this._pickerDismissHandler = null;
22618
+ }
22619
+ }
22620
+ /** Handle method change in primary panel */
22621
+ _handleLeftPanelMethodChange(method) {
22622
+ this._selectedMethod = method;
22623
+ this._lastResult = null;
22624
+ this._lastFormParams = {};
22625
+ this._updateRoute();
22626
+ }
22627
+ /** Handle panel action for primary panel */
22628
+ _handlePrimaryPanelAction(action) {
22629
+ if (action === "toggle-picker") {
22630
+ this._methodPickerOpen = !this._methodPickerOpen;
22631
+ this._methodPickerPanelId = null;
22632
+ }
22633
+ }
22634
+ /** Build opts for an additional (non-primary) split panel */
22635
+ _buildAdditionalPanelOpts(panel) {
22636
+ return {
22637
+ photon: this._selectedPhoton,
22638
+ method: panel.method,
22639
+ result: panel.result,
22640
+ executing: panel.executing || false,
22641
+ progress: panel.progress,
22642
+ formParams: panel.formParams || {},
22643
+ onSubmit: (e8) => {
22644
+ const args = e8.detail?.args || {};
22645
+ void this._executePanelMethod(panel.id, args);
22646
+ },
22647
+ onCancel: () => this._removePanel(panel.id),
22648
+ panelLabel: panel.id,
22649
+ instance: panel.instance,
22650
+ instances: this._instances,
22651
+ onInstanceChange: (inst) => this._changePanelInstance(panel.id, inst),
22652
+ allMethods: this._selectedPhoton?.methods || [],
22653
+ onMethodChange: (m3) => this._changePanelMethod(panel.id, m3),
22654
+ panelSide: "additional",
22655
+ onPanelAction: (action) => {
22656
+ if (action === "close") this._removePanel(panel.id);
22657
+ },
22658
+ onInstanceAction: (detail) => {
22659
+ if (detail.action === "switch") {
22660
+ this._changePanelInstance(panel.id, detail.instance);
22661
+ }
22662
+ },
22663
+ isStateful: !!this._selectedPhoton?.stateful,
22664
+ isLive: this._currentCollectionName !== null,
22665
+ overflowItems: this._buildOverflowItems({
22666
+ showRefresh: false,
22667
+ showRename: false,
22668
+ showViewSource: false,
22669
+ showDelete: false,
22670
+ showHelp: false,
22671
+ showRunTests: false
22672
+ }),
22673
+ onOverflowSelect: (id2) => this._handleOverflowAction(id2)
22674
+ };
22675
+ }
22676
+ /** Handle overflow menu actions from panel headers */
22677
+ _handleOverflowAction(id2) {
22678
+ switch (id2) {
22679
+ case "refresh":
22680
+ if (this._selectedMethod && this._willAutoInvoke(this._selectedMethod)) {
22681
+ void this._handleExecute(new CustomEvent("execute", { detail: { args: {} } }));
22682
+ }
22683
+ break;
22684
+ case "remember-values":
22685
+ this._rememberFormValues = !this._rememberFormValues;
22686
+ break;
22687
+ case "verbose-logging":
22688
+ this._verboseLogging = !this._verboseLogging;
22689
+ break;
22690
+ case "run-tests":
22691
+ void this._runTests();
22692
+ break;
22693
+ case "help":
22694
+ this._showPhotonHelp = true;
22695
+ break;
22696
+ default:
22697
+ this._handleContextAction(new CustomEvent("context-action", { detail: { action: id2 } }));
22698
+ break;
22699
+ }
22700
+ }
22701
+ /** Render source panel for split view */
22702
+ _renderSourcePanel(panelId) {
22703
+ return b2`
22704
+ <div style="height: 100%; overflow: hidden;">
22705
+ <photon-studio
22706
+ .photonName=${this._selectedPhoton?.name}
22707
+ .theme=${this._theme}
22708
+ @studio-saved=${() => this._handleStudioSaved?.()}
22709
+ @studio-close=${() => this._removePanel(panelId)}
22710
+ ></photon-studio>
22711
+ </div>
22712
+ `;
22713
+ }
22714
+ /** Render the method picker popover for adding new panels */
22715
+ _renderMethodPickerPopover() {
22716
+ const methods = this._getVisibleMethods();
22717
+ const openMethodNames = /* @__PURE__ */ new Set();
22718
+ if (this._selectedMethod) openMethodNames.add(this._selectedMethod.name);
22719
+ for (const p5 of this._splitPanels) {
22720
+ if (p5.type === "method" && p5.method) openMethodNames.add(p5.method.name);
22721
+ }
22722
+ const availableMethods = methods.filter((m3) => !openMethodNames.has(m3.name));
22723
+ const hasSourcePanel = this._splitPanels.some((p5) => p5.type === "source");
22724
+ const canAddMore = this._splitPanels.length < 2;
22725
+ if (!canAddMore) return "";
22726
+ if (!this._pickerDismissHandler) {
22727
+ const handler = (e8) => {
22728
+ const path = e8.composedPath();
22729
+ const popover = this.renderRoot.querySelector(".method-picker-popover");
22730
+ if (popover && path.includes(popover)) return;
22731
+ this._methodPickerOpen = false;
22732
+ this._pickerDismissHandler = null;
22733
+ window.removeEventListener("mousedown", handler, true);
22734
+ };
22735
+ this._pickerDismissHandler = handler;
22736
+ setTimeout(() => window.addEventListener("mousedown", handler, true), 0);
22737
+ }
22738
+ return b2`
22739
+ <div
22740
+ class="method-picker-popover"
22741
+ @click=${(e8) => e8.stopPropagation()}
22742
+ style="position: absolute; top: 100%; right: 0; z-index: 9999; min-width: 200px; max-height: 360px; overflow-y: auto; background: var(--bg-glass); border: 1px solid var(--border-glass); border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.3); backdrop-filter: blur(20px); margin-top: 4px;"
22743
+ >
22744
+ <!-- Header -->
22745
+ <div style="padding: 10px 12px 8px; border-bottom: 1px solid var(--border-glass);">
22746
+ <div
22747
+ style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--t-tertiary);"
22748
+ >
22749
+ Split Pane
22750
+ </div>
22751
+ </div>
22752
+
22753
+ <!-- Methods section -->
22754
+ ${availableMethods.length > 0 ? b2`
22755
+ <div style="padding: 4px 0;">
22756
+ <div
22757
+ style="padding: 4px 12px 2px; font-size: 10px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; color: var(--t-tertiary);"
22758
+ >
22759
+ Methods
22760
+ </div>
22761
+ ${availableMethods.map(
22762
+ (m3) => b2`
22763
+ <div
22764
+ @click=${() => this._addPanel("method", m3)}
22765
+ style="padding: 6px 12px 6px 16px; cursor: pointer; font-size: 12px; color: var(--t-primary); transition: background 0.15s ease; display: flex; align-items: center; gap: 6px;"
22766
+ @mouseenter=${(e8) => e8.target.style.background = "var(--bg-hover)"}
22767
+ @mouseleave=${(e8) => e8.target.style.background = "transparent"}
22768
+ >
22769
+ <span style="color: var(--t-tertiary); font-size: 10px;">&#9656;</span>
22770
+ ${m3.name}
22771
+ </div>
22772
+ `
22773
+ )}
21248
22774
  </div>
21249
- ` : ""}
21250
- ${this._lastResult !== null ? b2`
21251
- <result-viewer
21252
- .result=${this._lastResult}
21253
- .outputFormat=${this._selectedMethod?.outputFormat}
21254
- .layoutHints=${this._selectedMethod?.layoutHints}
21255
- .theme=${this._theme}
21256
- .live=${this._currentCollectionName !== null}
21257
- .resultKey=${this._selectedPhoton && this._selectedMethod ? `${this._selectedPhoton.name}/${this._selectedMethod.name}` : void 0}
21258
- @share=${() => this._handleShareResult()}
21259
- ></result-viewer>
21260
22775
  ` : b2`
21261
- <div class="empty-state-inline result-empty">
21262
- <span class="empty-state-icon">${play}</span>
21263
- <span>Run the method to see results here</span>
22776
+ <div
22777
+ style="padding: 8px 12px; font-size: 11px; color: var(--t-tertiary); font-style: italic;"
22778
+ >
22779
+ All methods already open
21264
22780
  </div>
21265
22781
  `}
22782
+
22783
+ <!-- Tools section -->
22784
+ ${!hasSourcePanel ? b2`
22785
+ <div style="border-top: 1px solid var(--border-glass); padding: 4px 0;">
22786
+ <div
22787
+ style="padding: 4px 12px 2px; font-size: 10px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; color: var(--t-tertiary);"
22788
+ >
22789
+ Tools
22790
+ </div>
22791
+ <div
22792
+ @click=${() => this._addPanel("source")}
22793
+ style="padding: 6px 12px 6px 16px; cursor: pointer; font-size: 12px; color: var(--t-primary); transition: background 0.15s ease; display: flex; align-items: center; gap: 6px;"
22794
+ @mouseenter=${(e8) => e8.target.style.background = "var(--bg-hover)"}
22795
+ @mouseleave=${(e8) => e8.target.style.background = "transparent"}
22796
+ >
22797
+ <span style="font-size: 11px; color: var(--accent-primary);">&lt;/&gt;</span>
22798
+ Source Editor
22799
+ </div>
22800
+ </div>
22801
+ ` : ""}
21266
22802
  </div>
21267
22803
  `;
21268
22804
  }
@@ -21291,10 +22827,11 @@ ${photon.errorMessage || "Unknown error"}</pre
21291
22827
  if (form?.isDirty && !confirm("You have unsaved changes. Discard them?")) {
21292
22828
  return;
21293
22829
  }
22830
+ this._closeSecondPanel();
21294
22831
  if (this._selectedPhoton.isApp && this._selectedPhoton.appEntry) {
21295
22832
  this._selectedMethod = this._selectedPhoton.appEntry;
21296
22833
  this._view = "form";
21297
- this._updateHash(true);
22834
+ this._updateRoute(true);
21298
22835
  void this.updateComplete.then(() => {
21299
22836
  setTimeout(() => {
21300
22837
  const mainArea = this.shadowRoot?.querySelector(".main-area");
@@ -21308,11 +22845,11 @@ ${photon.errorMessage || "Unknown error"}</pre
21308
22845
  this._selectedPhoton = null;
21309
22846
  this._welcomePhase = "welcome";
21310
22847
  this._view = "list";
21311
- this._updateHash(true);
22848
+ this._updateRoute(true);
21312
22849
  } else {
21313
22850
  this._view = "list";
21314
22851
  this._selectedMethod = null;
21315
- this._updateHash(true);
22852
+ this._updateRoute(true);
21316
22853
  }
21317
22854
  }
21318
22855
  async _openForkDialog() {
@@ -21386,7 +22923,17 @@ ${photon.errorMessage || "Unknown error"}</pre
21386
22923
  }
21387
22924
  } else {
21388
22925
  this._lastResult = mcpClient.parseToolResult(result);
22926
+ if (this._selectedPhoton?.stateful && Array.isArray(this._lastResult) && this._selectedMethod) {
22927
+ this._lastResult = this._autoWrapPaginationIfNeeded(
22928
+ this._lastResult,
22929
+ this._selectedPhoton.name,
22930
+ this._selectedMethod
22931
+ );
22932
+ }
21389
22933
  this._log("success", "Execution completed", false, execDuration);
22934
+ if (this._selectedPhoton?.stateful && this._lastResult != null && typeof this._lastResult === "object") {
22935
+ this._initializeGlobalInstance(this._selectedPhoton.name, this._lastResult);
22936
+ }
21390
22937
  void this.updateComplete.then(() => {
21391
22938
  const rv2 = this.shadowRoot?.querySelector("result-viewer");
21392
22939
  if (rv2 && window.innerWidth <= 768) {
@@ -21421,6 +22968,16 @@ ${photon.errorMessage || "Unknown error"}</pre
21421
22968
  const result = await mcpClient.callTool(toolName, this._lastFormParams || {});
21422
22969
  if (!result.isError) {
21423
22970
  this._lastResult = mcpClient.parseToolResult(result);
22971
+ if (this._selectedPhoton.stateful && Array.isArray(this._lastResult) && this._selectedMethod) {
22972
+ this._lastResult = this._autoWrapPaginationIfNeeded(
22973
+ this._lastResult,
22974
+ this._selectedPhoton.name,
22975
+ this._selectedMethod
22976
+ );
22977
+ }
22978
+ if (this._selectedPhoton.stateful && this._lastResult != null && typeof this._lastResult === "object") {
22979
+ this._initializeGlobalInstance(this._selectedPhoton.name, this._lastResult);
22980
+ }
21424
22981
  }
21425
22982
  } catch {
21426
22983
  } finally {
@@ -21471,6 +23028,162 @@ ${photon.errorMessage || "Unknown error"}</pre
21471
23028
  this._collectionUnsubscribes = [];
21472
23029
  this._currentCollectionName = null;
21473
23030
  }
23031
+ /**
23032
+ * Initialize global photon instance for @stateful photons
23033
+ * Makes the photon instance available as window.{photonName}
23034
+ * and keeps it in sync with server state via state-changed patches
23035
+ * Wraps paginated array properties with ViewportAwareProxy for smart fetching
23036
+ */
23037
+ /**
23038
+ * Auto-wrap array results with pagination metadata for @stateful photons
23039
+ * If method has (start, limit) parameters, detect them and use global instance
23040
+ * to calculate pagination metadata from the full array
23041
+ */
23042
+ _autoWrapPaginationIfNeeded(items, photonName, method) {
23043
+ const hasStartLimitParams = method.parameters?.some(
23044
+ (p5) => (p5.name === "start" || p5.name === "limit") && p5.required !== true
23045
+ );
23046
+ if (!hasStartLimitParams || items.length === 0) {
23047
+ return items;
23048
+ }
23049
+ try {
23050
+ const manager = getGlobalInstanceManager();
23051
+ const instance = manager.getInstance(photonName);
23052
+ if (!instance) {
23053
+ return items;
23054
+ }
23055
+ const arrayProp = Object.keys(instance).find((key) => {
23056
+ const value = instance[key];
23057
+ return Array.isArray(value) && value.length > 0;
23058
+ });
23059
+ if (!arrayProp) {
23060
+ return items;
23061
+ }
23062
+ const fullArray = instance[arrayProp];
23063
+ const totalCount = fullArray.length;
23064
+ const start = 0;
23065
+ const end = items.length;
23066
+ return {
23067
+ items,
23068
+ _pagination: {
23069
+ totalCount,
23070
+ start,
23071
+ end,
23072
+ hasMore: end < totalCount
23073
+ }
23074
+ };
23075
+ } catch (error2) {
23076
+ if (this._verboseLogging) {
23077
+ const msg = error2 instanceof Error ? error2.message : "Unknown error";
23078
+ this._log("warn", `Could not auto-wrap pagination: ${msg}`);
23079
+ }
23080
+ return items;
23081
+ }
23082
+ }
23083
+ _initializeGlobalInstance(photonName, initialState) {
23084
+ try {
23085
+ const paginatedProps = this._detectPaginatedProperties(initialState);
23086
+ const instance = initializeGlobalPhotonSession(photonName, initialState);
23087
+ for (const [propName, paginationMeta] of Object.entries(paginatedProps)) {
23088
+ this._wrapWithViewportProxy(instance, propName, paginationMeta);
23089
+ }
23090
+ if (this._verboseLogging) {
23091
+ const message = paginatedProps.size > 0 ? `\u{1F4E1} Global instance initialized with ${paginatedProps.size} paginated array(s): window.${photonName.toLowerCase()}` : `\u{1F4E1} Global instance initialized: window.${photonName.toLowerCase()}`;
23092
+ this._log("info", message);
23093
+ }
23094
+ } catch (error2) {
23095
+ console.error("Failed to initialize global photon instance", error2);
23096
+ }
23097
+ }
23098
+ /**
23099
+ * Detect which properties have pagination metadata
23100
+ */
23101
+ _detectPaginatedProperties(initialState) {
23102
+ const paginated = /* @__PURE__ */ new Map();
23103
+ if (!initialState || typeof initialState !== "object") {
23104
+ return paginated;
23105
+ }
23106
+ for (const [key, value] of Object.entries(initialState)) {
23107
+ if (value && typeof value === "object" && value._pagination) {
23108
+ paginated.set(key, value._pagination);
23109
+ }
23110
+ }
23111
+ return paginated;
23112
+ }
23113
+ /**
23114
+ * Wrap a property with ViewportAwareProxy for smart pagination
23115
+ */
23116
+ _wrapWithViewportProxy(instance, propertyName2, paginationMeta) {
23117
+ try {
23118
+ const pageSize = getPageSizeForClient();
23119
+ const proxy = new ViewportAwareProxy(
23120
+ this._selectedPhoton?.name || "unknown",
23121
+ this._selectedMethod?.name || propertyName2,
23122
+ mcpClient,
23123
+ {
23124
+ pageSize,
23125
+ bufferSize: 5,
23126
+ // Items to buffer above/below viewport
23127
+ maxCacheSize: 500
23128
+ // Max items to keep in cache
23129
+ }
23130
+ );
23131
+ proxy.initializeWithResponse({
23132
+ items: instance[propertyName2] || [],
23133
+ _pagination: paginationMeta
23134
+ });
23135
+ instance.makeProperty(propertyName2);
23136
+ Object.defineProperty(instance, propertyName2, {
23137
+ configurable: true,
23138
+ enumerable: true,
23139
+ get: () => proxy.items,
23140
+ set: (value) => {
23141
+ proxy.clearCache();
23142
+ proxy.initializeWithResponse({
23143
+ items: value || [],
23144
+ _pagination: paginationMeta
23145
+ });
23146
+ }
23147
+ });
23148
+ const patchHandler = (data) => {
23149
+ if (data?.patches) {
23150
+ proxy.applyPatches(data.patches);
23151
+ }
23152
+ };
23153
+ instance.on("state-changed", patchHandler);
23154
+ queueMicrotask(() => {
23155
+ try {
23156
+ const resultViewer = this._resultViewer;
23157
+ if (resultViewer?.shadowRoot) {
23158
+ const scrollContainer = resultViewer.shadowRoot.querySelector(
23159
+ ".result-content"
23160
+ );
23161
+ if (scrollContainer) {
23162
+ const manager = new ViewportManager(proxy, {
23163
+ container: scrollContainer,
23164
+ itemSelector: "[data-index]",
23165
+ pageSize,
23166
+ bufferSize: 5
23167
+ });
23168
+ manager.start();
23169
+ }
23170
+ }
23171
+ } catch (error2) {
23172
+ if (this._verboseLogging) {
23173
+ this._log("warn", `Could not set up automatic viewport tracking for ${propertyName2}`);
23174
+ }
23175
+ }
23176
+ });
23177
+ if (this._verboseLogging) {
23178
+ this._log(
23179
+ "info",
23180
+ `\u2728 Paginated proxy enabled for ${propertyName2} (page size: ${pageSize})`
23181
+ );
23182
+ }
23183
+ } catch (error2) {
23184
+ console.error(`Failed to wrap ${propertyName2} with ViewportAwareProxy`, error2);
23185
+ }
23186
+ }
21474
23187
  async _handleConfigure(e8) {
21475
23188
  const { photon, config: config3 } = e8.detail;
21476
23189
  this._log("info", `Configuring ${photon}...`);
@@ -21505,8 +23218,7 @@ ${photon.errorMessage || "Unknown error"}</pre
21505
23218
  showToast("No method selected to share", "error");
21506
23219
  return;
21507
23220
  }
21508
- const baseUrl = window.location.origin + window.location.pathname;
21509
- const hash2 = `${this._selectedPhoton.name}/${this._selectedMethod.name}`;
23221
+ const pathSegment2 = `${this._selectedPhoton.name}/${this._selectedMethod.name}`;
21510
23222
  const params = new URLSearchParams();
21511
23223
  for (const [key, value] of Object.entries(this._lastFormParams)) {
21512
23224
  if (value !== void 0 && value !== null && value !== "") {
@@ -21517,7 +23229,7 @@ ${photon.errorMessage || "Unknown error"}</pre
21517
23229
  }
21518
23230
  }
21519
23231
  }
21520
- let shareUrl = `${baseUrl}#${hash2}`;
23232
+ let shareUrl = `${window.location.origin}/${pathSegment2}`;
21521
23233
  if (params.toString()) {
21522
23234
  shareUrl += `?${params.toString()}`;
21523
23235
  }
@@ -21751,7 +23463,8 @@ ${photon.errorMessage || "Unknown error"}</pre
21751
23463
  showHelp = true,
21752
23464
  showRunTests = this._getTestMethods().length > 0,
21753
23465
  showRemove = false,
21754
- showFullscreen = false
23466
+ showFullscreen = false,
23467
+ showInstallApp = !this._pwaIsStandalone && !!this._selectedPhoton?.isApp
21755
23468
  } = opts;
21756
23469
  const items = [];
21757
23470
  if (showFullscreen) {
@@ -21797,6 +23510,13 @@ ${photon.errorMessage || "Unknown error"}</pre
21797
23510
  toggle: true,
21798
23511
  toggleActive: this._verboseLogging
21799
23512
  });
23513
+ if (showInstallApp) {
23514
+ items.push({
23515
+ id: "install-app",
23516
+ label: "Install as App",
23517
+ iconSvg: iconSvgString(iconPaths.installApp)
23518
+ });
23519
+ }
21800
23520
  if (showRename || showViewSource || showFork || showContribute || showDelete || showRemove) {
21801
23521
  const first = [
21802
23522
  showRename,
@@ -22722,6 +24442,10 @@ BeamApp.styles = [
22722
24442
  display: none;
22723
24443
  }
22724
24444
 
24445
+ :host(.focus-mode) .main-area {
24446
+ padding: var(--space-sm);
24447
+ }
24448
+
22725
24449
  .main-area {
22726
24450
  flex: 1;
22727
24451
  position: relative;
@@ -22730,10 +24454,10 @@ BeamApp.styles = [
22730
24454
  padding: var(--space-lg);
22731
24455
  }
22732
24456
 
24457
+ .beam-back-btn,
22733
24458
  .beam-fullscreen-btn {
22734
24459
  position: sticky;
22735
24460
  top: calc(-1 * var(--space-lg));
22736
- float: right;
22737
24461
  z-index: 100;
22738
24462
  width: 28px;
22739
24463
  height: 28px;
@@ -22747,6 +24471,18 @@ BeamApp.styles = [
22747
24471
  align-items: center;
22748
24472
  justify-content: center;
22749
24473
  transition: all 0.2s ease;
24474
+ backdrop-filter: blur(8px);
24475
+ }
24476
+
24477
+ .beam-back-btn {
24478
+ float: left;
24479
+ margin-top: calc(-1 * var(--space-lg));
24480
+ margin-left: calc(-1 * var(--space-lg) + 1px);
24481
+ margin-bottom: calc(-28px + var(--space-lg));
24482
+ }
24483
+
24484
+ .beam-fullscreen-btn {
24485
+ float: right;
22750
24486
  margin-top: calc(-1 * var(--space-lg));
22751
24487
  margin-right: calc(-1 * var(--space-lg) + 1px);
22752
24488
  margin-bottom: calc(-28px + var(--space-lg));
@@ -23777,7 +25513,7 @@ BeamApp.styles = [
23777
25513
  min-width: 120px;
23778
25514
  font-family: var(--font-mono);
23779
25515
  font-size: var(--text-md);
23780
- color: hsl(45, 80%, 60%);
25516
+ color: var(--color-warning);
23781
25517
  }
23782
25518
 
23783
25519
  .variable-input input {
@@ -23966,7 +25702,7 @@ BeamApp.styles = [
23966
25702
 
23967
25703
  .progress-bar-wrapper {
23968
25704
  height: 8px;
23969
- background: rgba(0, 0, 0, 0.2);
25705
+ background: var(--bg-glass);
23970
25706
  border-radius: var(--radius-xs);
23971
25707
  overflow: hidden;
23972
25708
  }
@@ -24084,6 +25820,25 @@ BeamApp.styles = [
24084
25820
  padding-top: calc(var(--space-md) + 60px); /* Space for mobile menu button */
24085
25821
  }
24086
25822
 
25823
+ .beam-back-btn,
25824
+ .beam-fullscreen-btn {
25825
+ position: fixed;
25826
+ top: var(--space-md);
25827
+ float: none;
25828
+ margin: 0;
25829
+ width: 44px;
25830
+ height: 44px;
25831
+ box-shadow: var(--shadow-md);
25832
+ }
25833
+
25834
+ .beam-back-btn {
25835
+ left: calc(var(--space-md) + 44px + var(--space-sm));
25836
+ }
25837
+
25838
+ .beam-fullscreen-btn {
25839
+ right: var(--space-md);
25840
+ }
25841
+
24087
25842
  .photon-header {
24088
25843
  flex-direction: column;
24089
25844
  align-items: flex-start;
@@ -24098,10 +25853,9 @@ BeamApp.styles = [
24098
25853
  grid-template-columns: 1fr;
24099
25854
  }
24100
25855
 
24101
- /* Touch-friendly targets - min 44px */
25856
+ /* Touch-friendly targets - min 44px (exclude small inline buttons) */
24102
25857
  .asset-card,
24103
25858
  .method-card,
24104
- button,
24105
25859
  .filter-btn {
24106
25860
  min-height: 44px;
24107
25861
  }
@@ -24382,6 +26136,15 @@ __decorateClass([
24382
26136
  __decorateClass([
24383
26137
  r5()
24384
26138
  ], BeamApp.prototype, "_resourceContent", 2);
26139
+ __decorateClass([
26140
+ r5()
26141
+ ], BeamApp.prototype, "_splitPanels", 2);
26142
+ __decorateClass([
26143
+ r5()
26144
+ ], BeamApp.prototype, "_methodPickerOpen", 2);
26145
+ __decorateClass([
26146
+ r5()
26147
+ ], BeamApp.prototype, "_methodPickerPanelId", 2);
24385
26148
  __decorateClass([
24386
26149
  e7("beam-sidebar")
24387
26150
  ], BeamApp.prototype, "_sidebar", 2);
@@ -24420,6 +26183,7 @@ var BeamSidebar = class extends i4 {
24420
26183
  this._favorites = /* @__PURE__ */ new Set();
24421
26184
  this._collapsedSections = /* @__PURE__ */ new Set();
24422
26185
  this._recentPhotons = [];
26186
+ this._notificationWarmth = /* @__PURE__ */ new Map();
24423
26187
  }
24424
26188
  connectedCallback() {
24425
26189
  super.connectedCallback();
@@ -24790,7 +26554,7 @@ var BeamSidebar = class extends i4 {
24790
26554
  }
24791
26555
  return b2`
24792
26556
  <li
24793
- class="photon-item ${this.selectedPhoton === photon.name ? "active" : ""} ${photon.internal ? "internal" : ""}"
26557
+ class="photon-item ${this.selectedPhoton === photon.name ? "active" : ""} ${photon.internal ? "internal" : ""} ${this._isPhotonWarm(photon.name) ? "warmth" : ""}"
24794
26558
  role="option"
24795
26559
  aria-selected="${this.selectedPhoton === photon.name}"
24796
26560
  tabindex="0"
@@ -24987,6 +26751,28 @@ ${photon.path}` : ""}"
24987
26751
  }
24988
26752
  });
24989
26753
  }
26754
+ /**
26755
+ * Check if a photon currently has a warmth indicator (notification received within last 5 seconds)
26756
+ */
26757
+ _isPhotonWarm(photonName) {
26758
+ const lastNotificationTime = this._notificationWarmth.get(photonName);
26759
+ if (!lastNotificationTime) return false;
26760
+ return Date.now() - lastNotificationTime < 5e3;
26761
+ }
26762
+ /**
26763
+ * Update the warmth indicator for a photon when a notification arrives
26764
+ */
26765
+ updatePhotonWarmth(photonName) {
26766
+ this._notificationWarmth.set(photonName, Date.now());
26767
+ this.requestUpdate();
26768
+ }
26769
+ /**
26770
+ * Check if a notification type is watched by a photon
26771
+ * This will be called by beam-app to determine if window.focus() should be triggered
26772
+ */
26773
+ isNotificationWatched(photonName, notificationType) {
26774
+ return true;
26775
+ }
24990
26776
  };
24991
26777
  BeamSidebar.styles = [
24992
26778
  theme,
@@ -25221,7 +27007,7 @@ BeamSidebar.styles = [
25221
27007
  }
25222
27008
 
25223
27009
  .section-header:hover {
25224
- color: var(--t-secondary);
27010
+ color: var(--t-primary);
25225
27011
  background: var(--bg-panel);
25226
27012
  }
25227
27013
 
@@ -25283,7 +27069,7 @@ BeamSidebar.styles = [
25283
27069
  }
25284
27070
 
25285
27071
  .photon-item:hover {
25286
- background: hsla(220, 10%, 80%, 0.1);
27072
+ background: var(--bg-glass);
25287
27073
  }
25288
27074
 
25289
27075
  .photon-item.active {
@@ -25297,13 +27083,28 @@ BeamSidebar.styles = [
25297
27083
 
25298
27084
  @keyframes flash-highlight {
25299
27085
  0% {
25300
- background: hsla(260, 100%, 65%, 0.2);
27086
+ background: var(--glow-primary);
25301
27087
  }
25302
27088
  100% {
25303
27089
  background: transparent;
25304
27090
  }
25305
27091
  }
25306
27092
 
27093
+ .photon-item.warmth {
27094
+ animation: warmth-fade 5s ease-out forwards;
27095
+ }
27096
+
27097
+ @keyframes warmth-fade {
27098
+ 0% {
27099
+ background: var(--color-warning);
27100
+ opacity: 0.15;
27101
+ }
27102
+ 100% {
27103
+ background: var(--color-warning);
27104
+ opacity: 0;
27105
+ }
27106
+ }
27107
+
25307
27108
  .photon-icon {
25308
27109
  width: 28px;
25309
27110
  height: 28px;
@@ -25394,7 +27195,7 @@ BeamSidebar.styles = [
25394
27195
  gap: 3px;
25395
27196
  font-size: var(--text-2xs);
25396
27197
  padding: 2px 5px;
25397
- background: rgba(255, 255, 255, 0.05);
27198
+ background: var(--bg-glass);
25398
27199
  border: 1px solid var(--border-glass);
25399
27200
  border-radius: var(--radius-full);
25400
27201
  font-weight: 500;
@@ -25477,7 +27278,7 @@ BeamSidebar.styles = [
25477
27278
  .marketplace-btn:hover {
25478
27279
  background: var(--bg-panel);
25479
27280
  border-color: var(--accent-secondary);
25480
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
27281
+ box-shadow: var(--shadow-sm);
25481
27282
  transform: translateY(-1px);
25482
27283
  }
25483
27284
 
@@ -25709,6 +27510,9 @@ __decorateClass([
25709
27510
  __decorateClass([
25710
27511
  r5()
25711
27512
  ], BeamSidebar.prototype, "_recentPhotons", 2);
27513
+ __decorateClass([
27514
+ r5()
27515
+ ], BeamSidebar.prototype, "_notificationWarmth", 2);
25712
27516
  BeamSidebar = __decorateClass([
25713
27517
  t4("beam-sidebar")
25714
27518
  ], BeamSidebar);
@@ -25798,34 +27602,16 @@ var MethodCard = class extends i4 {
25798
27602
  </div>
25799
27603
  ${this.method.isTemplate ? b2`<span class="badge prompt">Prompt</span>` : ""}
25800
27604
  ${isCron ? b2`<span
25801
- class="badge"
25802
- style="background:hsla(215,80%,60%,0.15);color:hsl(215,80%,65%)"
27605
+ class="badge scheduled"
25803
27606
  title="Runs automatically on schedule: ${this.method.scheduled}"
25804
27607
  >⏱ Scheduled</span
25805
27608
  >` : ""}
25806
- ${isDeprecated ? b2`<span
25807
- class="badge"
25808
- style="background:hsla(0,0%,50%,0.15);color:hsl(0,0%,60%);text-decoration:line-through"
25809
- >Deprecated</span
25810
- >` : ""}
25811
- ${isCached ? b2`<span
25812
- class="badge"
25813
- style="background:hsla(280,60%,50%,0.15);color:hsl(280,60%,65%)"
25814
- >Cached</span
25815
- >` : ""}
25816
- ${isThrottled ? b2`<span
25817
- class="badge"
25818
- style="background:hsla(30,80%,50%,0.15);color:hsl(30,80%,60%)"
25819
- >Throttled</span
25820
- >` : ""}
25821
- ${isQueued ? b2`<span
25822
- class="badge"
25823
- style="background:hsla(200,70%,50%,0.15);color:hsl(200,70%,60%)"
25824
- >Queued</span
25825
- >` : ""}
27609
+ ${isDeprecated ? b2`<span class="badge deprecated">Deprecated</span>` : ""}
27610
+ ${isCached ? b2`<span class="badge cached">Cached</span>` : ""}
27611
+ ${isThrottled ? b2`<span class="badge throttled">Throttled</span>` : ""}
27612
+ ${isQueued ? b2`<span class="badge queued">Queued</span>` : ""}
25826
27613
  ${emitsEvent ? b2`<span
25827
- class="badge"
25828
- style="background:hsla(100,70%,50%,0.15);color:hsl(100,70%,60%)"
27614
+ class="badge event"
25829
27615
  title="Automatically emits event: ${this.method.eventName}"
25830
27616
  >📡 Event</span
25831
27617
  >` : ""}
@@ -26071,7 +27857,7 @@ MethodCard.styles = [
26071
27857
 
26072
27858
  .card:hover {
26073
27859
  transform: translateY(-2px);
26074
- box-shadow: 0 8px 32px -4px rgba(0, 0, 0, 0.3);
27860
+ box-shadow: var(--shadow-lg);
26075
27861
  border-left-color: var(--accent-primary);
26076
27862
  }
26077
27863
 
@@ -26249,7 +28035,7 @@ MethodCard.styles = [
26249
28035
  font-size: var(--text-xs);
26250
28036
  padding: 2px 8px;
26251
28037
  border-radius: var(--radius-md);
26252
- background: hsla(220, 10%, 80%, 0.1);
28038
+ background: var(--bg-glass);
26253
28039
  color: var(--t-muted);
26254
28040
  flex-shrink: 0;
26255
28041
  }
@@ -26259,6 +28045,37 @@ MethodCard.styles = [
26259
28045
  color: hsl(45, 80%, 60%);
26260
28046
  }
26261
28047
 
28048
+ .badge.scheduled {
28049
+ background: hsla(215, 80%, 60%, 0.15);
28050
+ color: hsl(215, 80%, 65%);
28051
+ }
28052
+
28053
+ .badge.deprecated {
28054
+ background: hsla(0, 0%, 50%, 0.15);
28055
+ color: hsl(0, 0%, 60%);
28056
+ text-decoration: line-through;
28057
+ }
28058
+
28059
+ .badge.cached {
28060
+ background: hsla(280, 60%, 50%, 0.15);
28061
+ color: hsl(280, 60%, 65%);
28062
+ }
28063
+
28064
+ .badge.throttled {
28065
+ background: hsla(30, 80%, 50%, 0.15);
28066
+ color: hsl(30, 80%, 60%);
28067
+ }
28068
+
28069
+ .badge.queued {
28070
+ background: hsla(200, 70%, 50%, 0.15);
28071
+ color: hsl(200, 70%, 60%);
28072
+ }
28073
+
28074
+ .badge.event {
28075
+ background: hsla(100, 70%, 50%, 0.15);
28076
+ color: hsl(100, 70%, 60%);
28077
+ }
28078
+
26262
28079
  .param-tags {
26263
28080
  display: flex;
26264
28081
  flex-wrap: wrap;
@@ -26272,9 +28089,9 @@ MethodCard.styles = [
26272
28089
  font-size: var(--text-xs);
26273
28090
  padding: 4px 10px;
26274
28091
  border-radius: var(--radius-xs);
26275
- background: hsla(260, 60%, 50%, 0.12);
26276
- color: var(--t-primary);
26277
- border: 1px solid hsla(260, 60%, 50%, 0.2);
28092
+ background: var(--param-tag-bg, hsla(260, 60%, 50%, 0.12));
28093
+ color: var(--param-tag-color, var(--t-primary));
28094
+ border: 1px solid var(--param-tag-border, hsla(260, 60%, 50%, 0.2));
26278
28095
  font-weight: 500;
26279
28096
  }
26280
28097
 
@@ -26283,8 +28100,8 @@ MethodCard.styles = [
26283
28100
  min-width: 20px;
26284
28101
  height: 20px;
26285
28102
  border-radius: var(--radius-sm);
26286
- background: hsla(45, 80%, 50%, 0.2);
26287
- color: hsl(45, 80%, 60%);
28103
+ background: var(--color-warning-bg, hsla(45, 80%, 50%, 0.2));
28104
+ color: var(--color-warning, hsl(45, 80%, 60%));
26288
28105
  display: inline-flex;
26289
28106
  align-items: center;
26290
28107
  justify-content: center;
@@ -26314,7 +28131,7 @@ MethodCard.styles = [
26314
28131
  align-items: center;
26315
28132
  justify-content: center;
26316
28133
  border-radius: var(--radius-sm);
26317
- background: hsla(220, 10%, 80%, 0.1);
28134
+ background: var(--bg-glass);
26318
28135
  }
26319
28136
 
26320
28137
  .card:hover .action-icon {
@@ -26329,7 +28146,7 @@ MethodCard.styles = [
26329
28146
  border: 1px solid var(--border-glass);
26330
28147
  border-radius: var(--radius-md);
26331
28148
  padding: var(--space-sm);
26332
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
28149
+ box-shadow: var(--shadow-lg);
26333
28150
  z-index: 100;
26334
28151
  display: grid;
26335
28152
  grid-template-columns: repeat(6, 1fr);
@@ -26498,6 +28315,30 @@ function singularize(word) {
26498
28315
  if (lower.endsWith("s") && !lower.endsWith("ss")) return word.slice(0, -1);
26499
28316
  return word;
26500
28317
  }
28318
+ function isValidCharForFormat(char, format, position) {
28319
+ if (!format) return true;
28320
+ switch (format.toLowerCase()) {
28321
+ case "email":
28322
+ return /[a-zA-Z0-9@.\-_+]/.test(char);
28323
+ case "url":
28324
+ case "uri":
28325
+ return /[a-zA-Z0-9:\/\-_.?=&#%@+~;,!]/.test(char);
28326
+ case "uuid":
28327
+ return /[0-9a-fA-F\-]/.test(char);
28328
+ case "ipv4":
28329
+ return /[0-9.]/.test(char);
28330
+ case "ipv6":
28331
+ return /[0-9a-fA-F:]/.test(char);
28332
+ case "slug":
28333
+ return /[a-z0-9\-]/.test(char);
28334
+ case "hex":
28335
+ return position === 0 && char === "#" ? true : /[0-9a-fA-F]/.test(char);
28336
+ case "phone":
28337
+ return /[0-9+\-() ]/.test(char);
28338
+ default:
28339
+ return true;
28340
+ }
28341
+ }
26501
28342
  var InvokeForm = class extends i4 {
26502
28343
  constructor() {
26503
28344
  super(...arguments);
@@ -26752,9 +28593,7 @@ var InvokeForm = class extends i4 {
26752
28593
  this._handleChange(key, arr.length > 0 ? arr : raw);
26753
28594
  }}
26754
28595
  />
26755
- <div class="hint" style="font-size:0.75rem;color:var(--t-muted);margin-top:2px;">
26756
- Comma-separated values
26757
- </div>
28596
+ <div class="hint">Comma-separated values</div>
26758
28597
  </div>
26759
28598
  `;
26760
28599
  }
@@ -26805,9 +28644,12 @@ var InvokeForm = class extends i4 {
26805
28644
  min="${min}"
26806
28645
  max="${max}"
26807
28646
  step="${step2}"
28647
+ style="${this._sliderFillStyle(Number(currentValue2), min, max)}"
26808
28648
  .value=${String(currentValue2)}
26809
28649
  @input=${(e8) => {
26810
- const v2 = Number(e8.target.value);
28650
+ const el2 = e8.target;
28651
+ const v2 = this._sanitizeSliderValue(Number(el2.value), min, max, step2);
28652
+ el2.style.cssText = this._sliderFillStyle(v2, min, max);
26811
28653
  this._handleChange(key, v2);
26812
28654
  }}
26813
28655
  />
@@ -26820,7 +28662,14 @@ var InvokeForm = class extends i4 {
26820
28662
  step="${step2}"
26821
28663
  .value=${displayValue2}
26822
28664
  @input=${(e8) => {
26823
- const v2 = Number(e8.target.value);
28665
+ const raw = e8.target.value;
28666
+ if (raw === "" || raw === "-") return;
28667
+ this._handleChange(key, Number(raw));
28668
+ }}
28669
+ @change=${(e8) => {
28670
+ const el2 = e8.target;
28671
+ const v2 = this._sanitizeSliderValue(Number(el2.value), min, max, step2);
28672
+ el2.value = String(v2);
26824
28673
  this._handleChange(key, v2);
26825
28674
  }}
26826
28675
  />
@@ -26839,14 +28688,31 @@ var InvokeForm = class extends i4 {
26839
28688
  return b2`
26840
28689
  <input
26841
28690
  id=${o8(inputId)}
26842
- type="number"
26843
- class="${errorClass}"
28691
+ type="text"
28692
+ class="number-input-clean ${errorClass}"
28693
+ inputmode=${isInteger ? "numeric" : "decimal"}
26844
28694
  ${hasMin ? `min="${schema.minimum}"` : ""}
26845
- step="${step}"
26846
28695
  placeholder="${defaultVal2}"
26847
28696
  .value=${displayValue}
28697
+ @keypress=${(e8) => {
28698
+ const char = e8.key;
28699
+ const isDigit = /\d/.test(char);
28700
+ const isMinus = char === "-";
28701
+ const isDecimal = char === "." && !isInteger;
28702
+ if (!isDigit && !isMinus && !isDecimal) {
28703
+ e8.preventDefault();
28704
+ }
28705
+ }}
26848
28706
  @input=${(e8) => {
26849
- const v2 = Number(e8.target.value);
28707
+ let text = e8.target.value;
28708
+ if (isInteger) {
28709
+ const cleaned = text.replace(/[^\d-]/g, "");
28710
+ text = cleaned.match(/-.*-/) ? cleaned.replace(/-/g, "").replace(/^/, "-") : cleaned;
28711
+ }
28712
+ let v2 = text === "" || text === "-" ? defaultVal2 : Number(text);
28713
+ if (isNaN(v2)) {
28714
+ v2 = defaultVal2;
28715
+ }
26850
28716
  this._handleChange(key, v2);
26851
28717
  }}
26852
28718
  />
@@ -26934,6 +28800,8 @@ var InvokeForm = class extends i4 {
26934
28800
  }
26935
28801
  const defaultVal = schema.default;
26936
28802
  const placeholder = defaultVal != null ? String(defaultVal) : "";
28803
+ const format = schema.format;
28804
+ const pattern = schema.pattern;
26937
28805
  return b2`
26938
28806
  <input
26939
28807
  id=${o8(inputId)}
@@ -26941,6 +28809,23 @@ var InvokeForm = class extends i4 {
26941
28809
  class="${errorClass}"
26942
28810
  placeholder="${placeholder}"
26943
28811
  .value=${this._values[key] || ""}
28812
+ @keypress=${(e8) => {
28813
+ if (!isValidCharForFormat(e8.key, format)) {
28814
+ e8.preventDefault();
28815
+ return;
28816
+ }
28817
+ if (pattern) {
28818
+ try {
28819
+ const currentValue = e8.target.value;
28820
+ const newValue = currentValue + e8.key;
28821
+ const regex = new RegExp(pattern);
28822
+ if (!regex.test(newValue)) {
28823
+ e8.preventDefault();
28824
+ }
28825
+ } catch (err) {
28826
+ }
28827
+ }
28828
+ }}
26944
28829
  @input=${(e8) => this._handleChange(key, e8.target.value)}
26945
28830
  />
26946
28831
  `;
@@ -27065,7 +28950,11 @@ var InvokeForm = class extends i4 {
27065
28950
  */
27066
28951
  _cleanDescription(desc, schema) {
27067
28952
  if (!desc) return desc;
27068
- let cleaned = desc.replace(
28953
+ let cleaned = desc;
28954
+ const lines = cleaned.split("\n").filter((line) => !line.trim().startsWith("@"));
28955
+ cleaned = lines.join("\n").trim();
28956
+ cleaned = cleaned.replace(/\s*@\w+[\s\S]*?(?=[.!?\n]|$)/g, "").trim();
28957
+ cleaned = cleaned.replace(
27069
28958
  /['"][\w-]+['"]\s*(?:\([^)]*\)\s*)?(?:\|\s*['"][\w-]+['"]\s*(?:\([^)]*\)\s*)?)+/g,
27070
28959
  ""
27071
28960
  );
@@ -27101,6 +28990,23 @@ var InvokeForm = class extends i4 {
27101
28990
  this._savePersistedValues();
27102
28991
  }
27103
28992
  }
28993
+ _sliderFillStyle(value, min, max) {
28994
+ const pct = max > min ? (value - min) / (max - min) * 100 : 0;
28995
+ return `background: linear-gradient(to right, var(--accent-primary) ${pct}%, var(--border-glass) ${pct}%)`;
28996
+ }
28997
+ /** Clamp to [min, max] and round to nearest step (integer-safe). */
28998
+ _sanitizeSliderValue(raw, min, max, step) {
28999
+ let v2 = Number.isFinite(raw) ? raw : min;
29000
+ v2 = Math.min(max, Math.max(min, v2));
29001
+ if (step > 0) {
29002
+ v2 = min + Math.round((v2 - min) / step) * step;
29003
+ if (v2 > max) v2 = max;
29004
+ }
29005
+ if (Number.isInteger(step) && step >= 1) {
29006
+ v2 = Math.round(v2);
29007
+ }
29008
+ return v2;
29009
+ }
27104
29010
  _handleChange(key, value) {
27105
29011
  this._values = { ...this._values, [key]: value };
27106
29012
  if (this.rememberValues) {
@@ -27108,7 +29014,7 @@ var InvokeForm = class extends i4 {
27108
29014
  }
27109
29015
  }
27110
29016
  _buildCliCommand() {
27111
- const parts = ["photon", "cli", this.photonName, this.methodName];
29017
+ const parts = [this.photonName, this.methodName];
27112
29018
  for (const [key, value] of Object.entries(this._values)) {
27113
29019
  if (value === void 0 || value === null || value === "") continue;
27114
29020
  const strVal = typeof value === "object" ? JSON.stringify(value) : String(value);
@@ -27232,7 +29138,9 @@ var InvokeForm = class extends i4 {
27232
29138
  >
27233
29139
  <option value="">Select...</option>
27234
29140
  ${schema.enum.map(
27235
- (opt) => b2` <option value=${opt} ?selected=${opt === value}>${opt}</option> `
29141
+ (opt) => b2`
29142
+ <option value=${opt} ?selected=${opt === value}>${capitalizeEnumValue(opt)}</option>
29143
+ `
27236
29144
  )}
27237
29145
  </select>
27238
29146
  `;
@@ -27268,8 +29176,14 @@ var InvokeForm = class extends i4 {
27268
29176
  min="${min}"
27269
29177
  max="${max}"
27270
29178
  step="${step2}"
29179
+ style="${this._sliderFillStyle(Number(currentVal2), min, max)}"
27271
29180
  .value=${String(currentVal2)}
27272
- @input=${(e8) => onChange(propKey, Number(e8.target.value))}
29181
+ @input=${(e8) => {
29182
+ const el2 = e8.target;
29183
+ const v2 = this._sanitizeSliderValue(Number(el2.value), min, max, step2);
29184
+ el2.style.cssText = this._sliderFillStyle(v2, min, max);
29185
+ onChange(propKey, v2);
29186
+ }}
27273
29187
  />
27274
29188
  <input
27275
29189
  type="number"
@@ -27278,7 +29192,17 @@ var InvokeForm = class extends i4 {
27278
29192
  max="${max}"
27279
29193
  step="${step2}"
27280
29194
  .value=${displayVal2}
27281
- @input=${(e8) => onChange(propKey, Number(e8.target.value))}
29195
+ @input=${(e8) => {
29196
+ const raw = e8.target.value;
29197
+ if (raw === "" || raw === "-") return;
29198
+ onChange(propKey, Number(raw));
29199
+ }}
29200
+ @change=${(e8) => {
29201
+ const el2 = e8.target;
29202
+ const v2 = this._sanitizeSliderValue(Number(el2.value), min, max, step2);
29203
+ el2.value = String(v2);
29204
+ onChange(propKey, v2);
29205
+ }}
27282
29206
  />
27283
29207
  </div>
27284
29208
  <div class="range-labels">
@@ -27445,18 +29369,23 @@ var InvokeForm = class extends i4 {
27445
29369
  >
27446
29370
  <option value="">Select...</option>
27447
29371
  ${schema.enum.map(
27448
- (opt) => b2` <option value=${opt} ?selected=${opt === value}>${opt}</option> `
29372
+ (opt) => b2`
29373
+ <option value=${opt} ?selected=${opt === value}>${capitalizeEnumValue(opt)}</option>
29374
+ `
27449
29375
  )}
27450
29376
  </select>
27451
29377
  `;
27452
29378
  }
27453
29379
  if (schema.type === "boolean") {
27454
29380
  return b2`
27455
- <input
27456
- type="checkbox"
27457
- .checked=${!!value}
27458
- @change=${(e8) => handleNestedChange(e8.target.checked)}
27459
- />
29381
+ <label class="switch">
29382
+ <input
29383
+ type="checkbox"
29384
+ .checked=${!!value}
29385
+ @change=${(e8) => handleNestedChange(e8.target.checked)}
29386
+ />
29387
+ <span class="slider"></span>
29388
+ </label>
27460
29389
  `;
27461
29390
  }
27462
29391
  if (schema.type === "number" || schema.type === "integer") {
@@ -27478,8 +29407,14 @@ var InvokeForm = class extends i4 {
27478
29407
  min="${min}"
27479
29408
  max="${max}"
27480
29409
  step="${step2}"
29410
+ style="${this._sliderFillStyle(Number(currentVal2), min, max)}"
27481
29411
  .value=${String(currentVal2)}
27482
- @input=${(e8) => handleNestedChange(Number(e8.target.value))}
29412
+ @input=${(e8) => {
29413
+ const el2 = e8.target;
29414
+ const v2 = this._sanitizeSliderValue(Number(el2.value), min, max, step2);
29415
+ el2.style.cssText = this._sliderFillStyle(v2, min, max);
29416
+ handleNestedChange(v2);
29417
+ }}
27483
29418
  />
27484
29419
  <input
27485
29420
  type="number"
@@ -27488,7 +29423,17 @@ var InvokeForm = class extends i4 {
27488
29423
  max="${max}"
27489
29424
  step="${step2}"
27490
29425
  .value=${displayVal2}
27491
- @input=${(e8) => handleNestedChange(Number(e8.target.value))}
29426
+ @input=${(e8) => {
29427
+ const raw = e8.target.value;
29428
+ if (raw === "" || raw === "-") return;
29429
+ handleNestedChange(Number(raw));
29430
+ }}
29431
+ @change=${(e8) => {
29432
+ const el2 = e8.target;
29433
+ const v2 = this._sanitizeSliderValue(Number(el2.value), min, max, step2);
29434
+ el2.value = String(v2);
29435
+ handleNestedChange(v2);
29436
+ }}
27492
29437
  />
27493
29438
  </div>
27494
29439
  <div class="range-labels">
@@ -27839,6 +29784,7 @@ InvokeForm.styles = [
27839
29784
  font-size: var(--text-xs);
27840
29785
  padding: 2px 6px;
27841
29786
  border-radius: var(--radius-xs);
29787
+ transition: background 0.15s ease;
27842
29788
  }
27843
29789
 
27844
29790
  .array-item-remove:hover {
@@ -27915,11 +29861,17 @@ InvokeForm.styles = [
27915
29861
  margin-top: var(--space-xs);
27916
29862
  }
27917
29863
 
29864
+ .hint {
29865
+ font-size: var(--text-xs);
29866
+ color: var(--t-muted);
29867
+ margin-top: 2px;
29868
+ }
29869
+
27918
29870
  /* Slider-First Numeric Input */
27919
29871
  .slider-group {
27920
29872
  display: flex;
27921
29873
  flex-direction: column;
27922
- gap: 6px;
29874
+ gap: 4px;
27923
29875
  }
27924
29876
 
27925
29877
  .slider-row {
@@ -27930,57 +29882,140 @@ InvokeForm.styles = [
27930
29882
 
27931
29883
  .slider-row input[type='range'] {
27932
29884
  flex: 1;
27933
- height: 6px;
27934
- background: rgba(255, 255, 255, 0.12);
27935
- border: 1px solid rgba(255, 255, 255, 0.08);
27936
- border-radius: 3px;
29885
+ height: 4px;
29886
+ border-radius: var(--radius-full, 9999px);
27937
29887
  -webkit-appearance: none;
27938
29888
  cursor: pointer;
27939
29889
  margin: 8px 0;
29890
+ border: none;
29891
+ outline: none;
29892
+ /* Default unfilled track — overridden by inline style for fill effect */
29893
+ background: var(--border-glass);
27940
29894
  }
27941
29895
 
27942
29896
  .slider-row input[type='range']::-webkit-slider-thumb {
27943
29897
  -webkit-appearance: none;
27944
- width: 20px;
27945
- height: 20px;
29898
+ width: 16px;
29899
+ height: 16px;
27946
29900
  background: var(--accent-primary);
29901
+ border: 2px solid var(--bg-panel, var(--bg-glass));
27947
29902
  border-radius: 50%;
27948
29903
  cursor: pointer;
27949
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
29904
+ box-shadow:
29905
+ 0 0 0 2px var(--accent-primary),
29906
+ 0 1px 4px rgba(0, 0, 0, 0.3);
29907
+ transition:
29908
+ box-shadow 0.15s ease,
29909
+ transform 0.15s ease;
29910
+ }
29911
+
29912
+ .slider-row input[type='range']::-webkit-slider-thumb:hover {
29913
+ box-shadow:
29914
+ 0 0 0 3px var(--accent-primary),
29915
+ 0 0 8px var(--glow-primary);
29916
+ transform: scale(1.1);
29917
+ }
29918
+
29919
+ .slider-row input[type='range']:active::-webkit-slider-thumb {
29920
+ transform: scale(0.95);
27950
29921
  }
27951
29922
 
27952
29923
  .slider-row input[type='range']::-moz-range-track {
27953
- height: 6px;
27954
- background: rgba(255, 255, 255, 0.12);
27955
- border-radius: 3px;
27956
- border: 1px solid rgba(255, 255, 255, 0.08);
29924
+ height: 4px;
29925
+ background: var(--border-glass);
29926
+ border-radius: var(--radius-full, 9999px);
29927
+ border: none;
29928
+ }
29929
+
29930
+ .slider-row input[type='range']::-moz-range-progress {
29931
+ height: 4px;
29932
+ background: var(--accent-primary);
29933
+ border-radius: var(--radius-full, 9999px);
27957
29934
  }
27958
29935
 
27959
29936
  .slider-row input[type='range']::-moz-range-thumb {
27960
- width: 20px;
27961
- height: 20px;
29937
+ width: 12px;
29938
+ height: 12px;
27962
29939
  background: var(--accent-primary);
29940
+ border: 2px solid var(--bg-panel, var(--bg-glass));
27963
29941
  border-radius: 50%;
27964
29942
  cursor: pointer;
27965
- border: none;
27966
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
29943
+ box-shadow:
29944
+ 0 0 0 2px var(--accent-primary),
29945
+ 0 1px 4px rgba(0, 0, 0, 0.3);
29946
+ transition:
29947
+ box-shadow 0.15s ease,
29948
+ transform 0.15s ease;
27967
29949
  }
27968
29950
 
27969
- .slider-value {
27970
- min-width: 60px;
29951
+ .slider-row input[type='range']::-moz-range-thumb:hover {
29952
+ box-shadow:
29953
+ 0 0 0 3px var(--accent-primary),
29954
+ 0 0 8px var(--glow-primary);
29955
+ }
29956
+
29957
+ .slider-number-input {
29958
+ width: 64px;
27971
29959
  text-align: center;
29960
+ font-size: var(--text-sm);
29961
+ font-weight: 500;
29962
+ font-variant-numeric: tabular-nums;
29963
+ padding: 4px 6px;
29964
+ background: var(--bg-glass);
29965
+ border: 1px solid var(--border-glass) !important;
29966
+ border-radius: var(--radius-sm);
29967
+ color: var(--t-primary);
29968
+ -moz-appearance: textfield;
29969
+ transition:
29970
+ border-color 0.15s ease,
29971
+ box-shadow 0.15s ease;
29972
+ }
29973
+
29974
+ .slider-number-input::-webkit-outer-spin-button,
29975
+ .slider-number-input::-webkit-inner-spin-button {
29976
+ -webkit-appearance: none;
29977
+ margin: 0;
29978
+ }
29979
+
29980
+ .slider-number-input:focus-visible {
29981
+ outline: none;
29982
+ border-color: var(--accent-primary) !important;
29983
+ box-shadow: 0 0 0 2px var(--glow-primary);
29984
+ }
29985
+
29986
+ /* Clean numeric input — no spinner, mouse wheel support */
29987
+ .number-input-clean {
29988
+ width: 100%;
29989
+ padding: 8px 12px;
27972
29990
  font-size: var(--text-md);
27973
- font-weight: 600;
29991
+ background: var(--bg-glass);
29992
+ border: 1px solid var(--border-glass);
29993
+ border-radius: var(--radius-sm);
27974
29994
  color: var(--t-primary);
27975
- font-variant-numeric: tabular-nums;
29995
+ appearance: none;
29996
+ -moz-appearance: textfield;
29997
+ -webkit-appearance: none;
29998
+ transition:
29999
+ border-color 0.15s ease,
30000
+ box-shadow 0.15s ease;
27976
30001
  }
27977
30002
 
27978
- .slider-number-input {
27979
- width: 80px;
27980
- text-align: right;
27981
- font-size: var(--text-sm);
27982
- padding: 4px 8px;
27983
- border: 1px solid rgba(255, 255, 255, 0.08) !important;
30003
+ .number-input-clean::-webkit-outer-spin-button,
30004
+ .number-input-clean::-webkit-inner-spin-button {
30005
+ -webkit-appearance: none;
30006
+ appearance: none;
30007
+ display: none;
30008
+ margin: 0;
30009
+ }
30010
+
30011
+ .number-input-clean:hover {
30012
+ border-color: var(--accent-primary);
30013
+ }
30014
+
30015
+ .number-input-clean:focus-visible {
30016
+ outline: none;
30017
+ border-color: var(--accent-primary);
30018
+ box-shadow: 0 0 0 2px var(--glow-primary);
27984
30019
  }
27985
30020
 
27986
30021
  .range-labels {
@@ -28029,6 +30064,15 @@ InvokeForm.styles = [
28029
30064
  flex-shrink: 0;
28030
30065
  }
28031
30066
 
30067
+ select {
30068
+ appearance: none;
30069
+ -webkit-appearance: none;
30070
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E");
30071
+ background-repeat: no-repeat;
30072
+ background-position: right 12px center;
30073
+ padding-right: 32px;
30074
+ }
30075
+
28032
30076
  /* ===== Responsive Design ===== */
28033
30077
  @media (max-width: 768px) {
28034
30078
  .form-container {
@@ -28231,7 +30275,7 @@ ActivityLog.styles = [
28231
30275
  font-size: var(--text-sm);
28232
30276
  text-transform: uppercase;
28233
30277
  letter-spacing: 0.1em;
28234
- color: var(--t-secondary); /* Brighter */
30278
+ color: var(--t-primary);
28235
30279
  font-weight: 600;
28236
30280
  }
28237
30281
 
@@ -28246,7 +30290,7 @@ ActivityLog.styles = [
28246
30290
  }
28247
30291
 
28248
30292
  .clear-btn:hover {
28249
- background: hsla(220, 10%, 80%, 0.1);
30293
+ background: var(--bg-glass);
28250
30294
  color: var(--t-primary);
28251
30295
  }
28252
30296
 
@@ -28265,14 +30309,14 @@ ActivityLog.styles = [
28265
30309
  }
28266
30310
 
28267
30311
  .filter-btn:hover {
28268
- background: hsla(220, 10%, 80%, 0.1);
30312
+ background: var(--bg-glass);
28269
30313
  color: var(--t-primary);
28270
30314
  }
28271
30315
 
28272
30316
  .filter-btn.active {
28273
30317
  border-color: var(--accent-secondary);
28274
30318
  color: var(--accent-secondary);
28275
- background: hsla(210, 80%, 65%, 0.08);
30319
+ background: var(--bg-glass);
28276
30320
  }
28277
30321
 
28278
30322
  .visually-hidden {
@@ -28332,7 +30376,7 @@ ActivityLog.styles = [
28332
30376
  display: inline-block;
28333
30377
  font-size: var(--text-xs);
28334
30378
  color: var(--t-muted);
28335
- background: hsla(220, 10%, 80%, 0.08);
30379
+ background: var(--bg-glass);
28336
30380
  padding: 1px 6px;
28337
30381
  border-radius: var(--radius-full);
28338
30382
  margin-left: var(--space-sm);
@@ -28491,6 +30535,8 @@ var ResultViewer = class extends i4 {
28491
30535
  this._previousResult = null;
28492
30536
  // Recency heat: track when items were last added/updated
28493
30537
  this._itemHeatTimestamps = /* @__PURE__ */ new Map();
30538
+ // Audit trail expansion state: track which items have expanded audit trails
30539
+ this._expandedAuditTrails = /* @__PURE__ */ new Set();
28494
30540
  // The detected ID field for the current result (shared across diff, animation, warmth)
28495
30541
  this._activeIdField = "id";
28496
30542
  // Chart.js instance for reactive updates
@@ -28793,19 +30839,32 @@ var ResultViewer = class extends i4 {
28793
30839
  /**
28794
30840
  * Get the warmth class based on recency heat.
28795
30841
  * Reads timestamp from item data first (survives refresh), falls back to in-memory map.
30842
+ * Prioritizes __meta timestamps (most recent), then standard timestamp fields.
28796
30843
  */
28797
30844
  _getItemWarmthClass(item) {
28798
30845
  const idField = this._activeIdField;
28799
30846
  let timestamp;
28800
30847
  if (item && typeof item === "object") {
28801
30848
  const rec = item;
28802
- for (const field of ResultViewer._TIMESTAMP_FIELDS) {
28803
- const val = rec[field];
28804
- if (val !== void 0 && val !== null) {
28805
- const parsed = typeof val === "number" ? val : new Date(typeof val === "string" ? val : String(val)).getTime();
28806
- if (!isNaN(parsed)) {
28807
- timestamp = parsed;
28808
- break;
30849
+ const meta4 = rec.__meta;
30850
+ if (meta4 && typeof meta4 === "object") {
30851
+ if (meta4.modifiedAt) {
30852
+ const parsed = new Date(meta4.modifiedAt).getTime();
30853
+ if (!isNaN(parsed)) timestamp = parsed;
30854
+ } else if (meta4.createdAt) {
30855
+ const parsed = new Date(meta4.createdAt).getTime();
30856
+ if (!isNaN(parsed)) timestamp = parsed;
30857
+ }
30858
+ }
30859
+ if (timestamp === void 0) {
30860
+ for (const field of ResultViewer._TIMESTAMP_FIELDS) {
30861
+ const val = rec[field];
30862
+ if (val !== void 0 && val !== null) {
30863
+ const parsed = typeof val === "number" ? val : new Date(typeof val === "string" ? val : String(val)).getTime();
30864
+ if (!isNaN(parsed)) {
30865
+ timestamp = parsed;
30866
+ break;
30867
+ }
28809
30868
  }
28810
30869
  }
28811
30870
  }
@@ -29314,7 +31373,8 @@ var ResultViewer = class extends i4 {
29314
31373
  "tabs",
29315
31374
  "accordion",
29316
31375
  "stack",
29317
- "columns"
31376
+ "columns",
31377
+ "qr"
29318
31378
  ].includes(format)) {
29319
31379
  return format;
29320
31380
  }
@@ -29481,6 +31541,8 @@ var ResultViewer = class extends i4 {
29481
31541
  return this._renderStack(filteredData);
29482
31542
  case "columns":
29483
31543
  return this._renderColumns(filteredData);
31544
+ case "qr":
31545
+ return this._renderQR(filteredData);
29484
31546
  case "mermaid":
29485
31547
  return this._renderMermaid(filteredData);
29486
31548
  case "json":
@@ -29723,6 +31785,136 @@ var ResultViewer = class extends i4 {
29723
31785
  </div>
29724
31786
  `
29725
31787
  )}
31788
+ ${this._renderAuditTrail(data)}
31789
+ `;
31790
+ }
31791
+ /**
31792
+ * Render audit trail from __meta object if present
31793
+ */
31794
+ _renderAuditTrail(data) {
31795
+ if (!data || typeof data !== "object") return "";
31796
+ const meta4 = data.__meta;
31797
+ if (!meta4 || typeof meta4 !== "object") return "";
31798
+ const idField = this._activeIdField;
31799
+ const itemId = data[idField] ? String(data[idField]) : Math.random().toString(36);
31800
+ const auditKey = `audit-${itemId}`;
31801
+ const isExpanded = this._expandedAuditTrails.has(auditKey);
31802
+ const formatTime = (isoString) => {
31803
+ if (!isoString) return "N/A";
31804
+ const date4 = new Date(isoString);
31805
+ return date4.toLocaleString();
31806
+ };
31807
+ const createdAt = formatTime(meta4.createdAt);
31808
+ const modifiedAt = meta4.modifiedAt ? formatTime(meta4.modifiedAt) : null;
31809
+ return b2`
31810
+ <div
31811
+ style="margin-top: var(--space-md); border-top: 1px solid var(--border-glass); padding-top: var(--space-md);"
31812
+ >
31813
+ <details
31814
+ ?open="${isExpanded}"
31815
+ @toggle="${(e8) => {
31816
+ const detail = e8.target;
31817
+ if (detail.open) {
31818
+ this._expandedAuditTrails.add(auditKey);
31819
+ } else {
31820
+ this._expandedAuditTrails.delete(auditKey);
31821
+ }
31822
+ }}"
31823
+ >
31824
+ <summary
31825
+ style="cursor: pointer; font-weight: 500; display: flex; align-items: center; gap: var(--space-sm);"
31826
+ >
31827
+ <span
31828
+ style="display: inline-block; width: 0.5em; height: 0.5em; border-radius: 50%; background: var(--text-secondary); margin-right: var(--space-xs);"
31829
+ ></span>
31830
+ Audit Trail
31831
+ ${meta4.modifications?.length ? b2`<span style="font-size: 0.85em; color: var(--text-secondary);"
31832
+ >(${meta4.modifications.length} changes)</span
31833
+ >` : ""}
31834
+ </summary>
31835
+
31836
+ <div
31837
+ style="margin-top: var(--space-md); padding: var(--space-sm) var(--space-md); background: var(--bg-secondary); border-radius: var(--radius-sm);"
31838
+ >
31839
+ <table style="width: 100%; font-size: 0.9em;">
31840
+ <tbody>
31841
+ <tr style="border-bottom: 1px solid var(--border-subtle);">
31842
+ <td
31843
+ style="padding: var(--space-xs); color: var(--text-secondary); font-weight: 500;"
31844
+ >
31845
+ Created
31846
+ </td>
31847
+ <td style="padding: var(--space-xs);">${createdAt}</td>
31848
+ ${meta4.createdBy ? b2`<td
31849
+ style="padding: var(--space-xs); color: var(--text-secondary); font-size: 0.85em;"
31850
+ >
31851
+ (by: ${meta4.createdBy})
31852
+ </td>` : ""}
31853
+ </tr>
31854
+ ${modifiedAt ? b2`
31855
+ <tr style="border-bottom: 1px solid var(--border-subtle);">
31856
+ <td
31857
+ style="padding: var(--space-xs); color: var(--text-secondary); font-weight: 500;"
31858
+ >
31859
+ Modified
31860
+ </td>
31861
+ <td style="padding: var(--space-xs);">${modifiedAt}</td>
31862
+ ${meta4.modifiedBy ? b2`<td
31863
+ style="padding: var(--space-xs); color: var(--text-secondary); font-size: 0.85em;"
31864
+ >
31865
+ (by: ${meta4.modifiedBy})
31866
+ </td>` : ""}
31867
+ </tr>
31868
+ ` : ""}
31869
+ </tbody>
31870
+ </table>
31871
+
31872
+ ${meta4.modifications && Array.isArray(meta4.modifications) && meta4.modifications.length > 0 ? b2`
31873
+ <div style="margin-top: var(--space-md);">
31874
+ <div
31875
+ style="font-weight: 500; margin-bottom: var(--space-sm); color: var(--text-secondary);"
31876
+ >
31877
+ Changes
31878
+ </div>
31879
+ <ul style="list-style: none; padding: 0; margin: 0;">
31880
+ ${meta4.modifications.map(
31881
+ (mod, idx) => b2`
31882
+ <li
31883
+ key="${idx}"
31884
+ style="padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs); background: var(--bg-hover); border-left: 3px solid var(--primary); border-radius: 2px; font-size: 0.9em;"
31885
+ >
31886
+ <div style="margin-bottom: 2px;">
31887
+ <span style="font-weight: 500; color: var(--text-primary);"
31888
+ >${mod.field}</span
31889
+ >
31890
+ <span style="color: var(--text-secondary); margin: 0 var(--space-xs);"
31891
+ >→</span
31892
+ >
31893
+ <span style="color: var(--text-secondary); font-size: 0.85em;"
31894
+ >${formatTime(mod.timestamp || (/* @__PURE__ */ new Date()).toISOString())}</span
31895
+ >
31896
+ </div>
31897
+ <div
31898
+ style="font-family: monospace; font-size: 0.85em; color: var(--text-secondary); margin-left: var(--space-sm);"
31899
+ >
31900
+ <span style="color: #d87070;">${JSON.stringify(mod.oldValue)}</span>
31901
+ <span style="margin: 0 4px;">→</span>
31902
+ <span style="color: #7cb342;">${JSON.stringify(mod.newValue)}</span>
31903
+ </div>
31904
+ ${mod.modifiedBy ? b2`<div
31905
+ style="font-size: 0.8em; color: var(--text-secondary); margin-top: 2px;"
31906
+ >
31907
+ <em>by: ${mod.modifiedBy}</em>
31908
+ </div>` : ""}
31909
+ </li>
31910
+ `
31911
+ )}
31912
+ </ul>
31913
+ </div>
31914
+ ` : ""}
31915
+ </div>
31916
+ </details>
31917
+ </div>
29726
31918
  `;
29727
31919
  }
29728
31920
  /** Returns true for arrays of objects or large nested objects that deserve their own section */
@@ -30181,6 +32373,163 @@ ${code}</pre>`;
30181
32373
  style="position: relative; background: ${bgColor}; border-radius: var(--radius-sm); padding: 16px; margin: 16px 0; min-height: 120px;"
30182
32374
  ></div>`;
30183
32375
  }
32376
+ _renderQR(data) {
32377
+ const text = typeof data === "object" && data !== null ? String(data.url || data.link || data.value || JSON.stringify(data)) : String(data);
32378
+ const isUrl = /^https?:\/\//i.test(text);
32379
+ const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text);
32380
+ const isPhone = /^[+]?[\d\s\-().]{7,}$/.test(text.trim());
32381
+ let href = "";
32382
+ let linkLabel = text;
32383
+ if (isUrl) {
32384
+ href = text;
32385
+ try {
32386
+ linkLabel = new URL(text).hostname + new URL(text).pathname;
32387
+ } catch {
32388
+ }
32389
+ } else if (isEmail) {
32390
+ href = `mailto:${text}`;
32391
+ } else if (isPhone) {
32392
+ href = `tel:${text.replace(/[\s\-().]/g, "")}`;
32393
+ }
32394
+ setTimeout(() => {
32395
+ if (this._qrContainer) {
32396
+ this._qrContainer.innerHTML = "";
32397
+ try {
32398
+ const containerWidth = this._qrContainer.clientWidth;
32399
+ const qrSize = Math.max(200, Math.min(containerWidth - 48, 400));
32400
+ new window.QRCode(this._qrContainer, {
32401
+ text,
32402
+ width: qrSize,
32403
+ height: qrSize,
32404
+ correctLevel: window.QRCode?.CorrectLevel?.H,
32405
+ colorDark: "#000000",
32406
+ colorLight: "#ffffff"
32407
+ });
32408
+ } catch (error2) {
32409
+ console.error("Failed to generate QR code:", error2);
32410
+ }
32411
+ }
32412
+ }, 0);
32413
+ const message = typeof data === "object" && data !== null ? data.message : void 0;
32414
+ const extraFields = [];
32415
+ if (typeof data === "object" && data !== null) {
32416
+ for (const [k3, v2] of Object.entries(data)) {
32417
+ if (["url", "link", "value", "message", "port"].includes(k3) || v2 == null || typeof v2 === "object")
32418
+ continue;
32419
+ extraFields.push({ label: k3, value: `${v2}` });
32420
+ }
32421
+ }
32422
+ return b2`<div
32423
+ style="
32424
+ display: flex; flex-direction: column; align-items: center; gap: 0;
32425
+ padding: 0; border-radius: var(--radius-md);
32426
+ border: 1px solid var(--border-glass);
32427
+ background: var(--bg-subtle); overflow: hidden;
32428
+ width: 40%; min-width: 280px; max-width: 480px;
32429
+ margin: 16px auto;
32430
+ "
32431
+ >
32432
+ <div
32433
+ id="qr-container"
32434
+ style="
32435
+ width: 100%; padding: 24px;
32436
+ display: flex; justify-content: center; align-items: center;
32437
+ background: #ffffff; border-radius: var(--radius-md) var(--radius-md) 0 0;
32438
+ "
32439
+ ></div>
32440
+
32441
+ <div
32442
+ style="
32443
+ width: 100%; padding: 16px 20px;
32444
+ display: flex; flex-direction: column; gap: 10px;
32445
+ border-top: 1px solid var(--border-glass);
32446
+ "
32447
+ >
32448
+ ${message ? b2`<div
32449
+ style="
32450
+ font-size: 0.8rem; color: var(--t-secondary, #a0a0a0);
32451
+ text-align: center; font-weight: 500;
32452
+ "
32453
+ >
32454
+ ${message}
32455
+ </div>` : ""}
32456
+ ${href ? b2`<div
32457
+ style="display: flex; align-items: center; gap: 10px; justify-content: center;"
32458
+ >
32459
+ <a
32460
+ href="${href}"
32461
+ target="_blank"
32462
+ rel="noopener noreferrer"
32463
+ style="
32464
+ font-size: 0.85rem; color: var(--accent, #3b82f6);
32465
+ text-decoration: none; font-weight: 500;
32466
+ text-align: center; word-break: break-word;
32467
+ "
32468
+ @mouseenter=${(e8) => e8.target.style.textDecoration = "underline"}
32469
+ @mouseleave=${(e8) => e8.target.style.textDecoration = "none"}
32470
+ >${linkLabel}</a
32471
+ >
32472
+ <button
32473
+ title="Copy to clipboard"
32474
+ style="
32475
+ background: var(--bg-subtle, rgba(255,255,255,0.06));
32476
+ border: 1px solid var(--border-glass);
32477
+ border-radius: 6px; padding: 6px 10px; cursor: pointer;
32478
+ color: var(--t-muted); font-size: 0.85rem; flex-shrink: 0;
32479
+ transition: all 0.15s; line-height: 1;
32480
+ "
32481
+ @mouseenter=${(e8) => {
32482
+ e8.target.style.color = "var(--accent, #3b82f6)";
32483
+ e8.target.style.borderColor = "var(--accent, #3b82f6)";
32484
+ }}
32485
+ @mouseleave=${(e8) => {
32486
+ e8.target.style.color = "var(--t-muted)";
32487
+ e8.target.style.borderColor = "var(--border-glass)";
32488
+ }}
32489
+ @click=${(e8) => {
32490
+ void navigator.clipboard.writeText(text);
32491
+ const btn = e8.target;
32492
+ const orig = btn.textContent;
32493
+ btn.textContent = "\u2713 Copied";
32494
+ btn.style.color = "var(--accent, #3b82f6)";
32495
+ setTimeout(() => {
32496
+ btn.textContent = orig;
32497
+ btn.style.color = "var(--t-muted)";
32498
+ }, 1500);
32499
+ }}
32500
+ >
32501
+ Copy
32502
+ </button>
32503
+ </div>` : b2`<div
32504
+ style="
32505
+ font-size: 0.875rem; color: var(--t-muted);
32506
+ text-align: center; word-break: break-all;
32507
+ "
32508
+ >
32509
+ ${text}
32510
+ </div>`}
32511
+ ${extraFields.length > 0 ? b2`<div
32512
+ style="
32513
+ display: flex; flex-direction: column; gap: 6px;
32514
+ padding-top: 10px; border-top: 1px solid var(--border-glass);
32515
+ font-size: 0.8rem;
32516
+ "
32517
+ >
32518
+ ${extraFields.map(
32519
+ (f5) => b2`<div style="display: flex; justify-content: space-between; gap: 12px;">
32520
+ <span style="color: var(--t-muted); text-transform: capitalize;"
32521
+ >${f5.label}</span
32522
+ >
32523
+ <span
32524
+ style="color: var(--t-primary); font-family: var(--font-mono, monospace); word-break: break-all; text-align: right;"
32525
+ >${f5.value}</span
32526
+ >
32527
+ </div>`
32528
+ )}
32529
+ </div>` : ""}
32530
+ </div>
32531
+ </div>`;
32532
+ }
30184
32533
  _renderText(data) {
30185
32534
  const text = String(data);
30186
32535
  if (this._isMermaidString(text)) {
@@ -31440,7 +33789,7 @@ ResultViewer.styles = [
31440
33789
  }
31441
33790
 
31442
33791
  button:hover {
31443
- background: hsla(220, 10%, 80%, 0.1);
33792
+ background: var(--bg-glass);
31444
33793
  color: var(--t-primary);
31445
33794
  }
31446
33795
 
@@ -31591,7 +33940,7 @@ ResultViewer.styles = [
31591
33940
  }
31592
33941
 
31593
33942
  .smart-table tr:hover td {
31594
- background: hsla(220, 10%, 80%, 0.05);
33943
+ background: var(--bg-glass);
31595
33944
  }
31596
33945
 
31597
33946
  /* Key-Value Table (single object) */
@@ -32278,7 +34627,7 @@ ResultViewer.styles = [
32278
34627
  }
32279
34628
 
32280
34629
  .tree-item:hover {
32281
- background: hsla(220, 10%, 80%, 0.1);
34630
+ background: var(--bg-glass);
32282
34631
  }
32283
34632
 
32284
34633
  .tree-toggle {
@@ -32307,16 +34656,16 @@ ResultViewer.styles = [
32307
34656
  }
32308
34657
 
32309
34658
  .tree-value.string {
32310
- color: #a5d6ff;
34659
+ color: var(--syntax-string);
32311
34660
  }
32312
34661
  .tree-value.number {
32313
- color: #ff9e64;
34662
+ color: var(--syntax-number);
32314
34663
  }
32315
34664
  .tree-value.boolean {
32316
- color: #ff007c;
34665
+ color: var(--syntax-boolean);
32317
34666
  }
32318
34667
  .tree-value.null {
32319
- color: #79c0ff;
34668
+ color: var(--syntax-null);
32320
34669
  }
32321
34670
 
32322
34671
  .tree-type {
@@ -32578,9 +34927,9 @@ ResultViewer.styles = [
32578
34927
  }
32579
34928
 
32580
34929
  .expand-btn:hover {
32581
- background: var(--primary);
34930
+ background: var(--accent-primary);
32582
34931
  color: white;
32583
- border-color: var(--primary);
34932
+ border-color: var(--accent-primary);
32584
34933
  }
32585
34934
 
32586
34935
  /* Markdown items (array rendering with filter transitions) */
@@ -32818,13 +35167,13 @@ ResultViewer.styles = [
32818
35167
  }
32819
35168
 
32820
35169
  .metric-delta.up {
32821
- color: #16a34a;
32822
- background: rgba(22, 163, 74, 0.12);
35170
+ color: var(--color-success);
35171
+ background: var(--color-success-bg);
32823
35172
  }
32824
35173
 
32825
35174
  .metric-delta.down {
32826
- color: #dc2626;
32827
- background: rgba(220, 38, 38, 0.12);
35175
+ color: var(--color-error);
35176
+ background: var(--color-error-bg);
32828
35177
  }
32829
35178
 
32830
35179
  .metric-delta.neutral {
@@ -32959,7 +35308,7 @@ ResultViewer.styles = [
32959
35308
 
32960
35309
  .timeline-description {
32961
35310
  font-size: var(--text-md);
32962
- color: var(--t-secondary);
35311
+ color: var(--t-muted);
32963
35312
  margin-top: 2px;
32964
35313
  line-height: 1.4;
32965
35314
  }
@@ -33106,7 +35455,7 @@ ResultViewer.styles = [
33106
35455
  justify-content: space-between;
33107
35456
  padding: 4px 0;
33108
35457
  font-size: var(--text-md);
33109
- color: var(--t-secondary);
35458
+ color: var(--t-muted);
33110
35459
  }
33111
35460
 
33112
35461
  .cart-summary-row.total {
@@ -33384,6 +35733,9 @@ __decorateClass([
33384
35733
  __decorateClass([
33385
35734
  r5()
33386
35735
  ], ResultViewer.prototype, "_internalResult", 2);
35736
+ __decorateClass([
35737
+ e7("#qr-container")
35738
+ ], ResultViewer.prototype, "_qrContainer", 2);
33387
35739
  __decorateClass([
33388
35740
  n4({ type: String })
33389
35741
  ], ResultViewer.prototype, "collectionProperty", 2);
@@ -83453,9 +85805,13 @@ var ElicitationModal = class extends i4 {
83453
85805
  min="${min}"
83454
85806
  max="${max}"
83455
85807
  step="${step}"
85808
+ style="${this._sliderFillStyle(Number(currentValue), min, max)}"
83456
85809
  .value=${String(currentValue)}
83457
85810
  @input=${(e8) => {
83458
- this._inputValue = Number(e8.target.value);
85811
+ const el2 = e8.target;
85812
+ const v2 = this._sanitizeSliderValue(Number(el2.value), min, max, step);
85813
+ el2.style.cssText = this._sliderFillStyle(v2, min, max);
85814
+ this._inputValue = v2;
83459
85815
  }}
83460
85816
  />
83461
85817
  <input
@@ -83466,7 +85822,15 @@ var ElicitationModal = class extends i4 {
83466
85822
  step="${step}"
83467
85823
  .value=${String(currentValue)}
83468
85824
  @input=${(e8) => {
83469
- this._inputValue = Number(e8.target.value);
85825
+ const raw = e8.target.value;
85826
+ if (raw === "" || raw === "-") return;
85827
+ this._inputValue = Number(raw);
85828
+ }}
85829
+ @change=${(e8) => {
85830
+ const el2 = e8.target;
85831
+ const v2 = this._sanitizeSliderValue(Number(el2.value), min, max, step);
85832
+ el2.value = String(v2);
85833
+ this._inputValue = v2;
83470
85834
  }}
83471
85835
  @keydown=${(e8) => this._handleKeydown(e8)}
83472
85836
  autofocus
@@ -83710,9 +86074,13 @@ var ElicitationModal = class extends i4 {
83710
86074
  min="${min}"
83711
86075
  max="${max}"
83712
86076
  step="${step}"
86077
+ style="${this._sliderFillStyle(Number(currentValue), min, max)}"
83713
86078
  .value=${String(currentValue)}
83714
86079
  @input=${(e8) => {
83715
- this._updateFormValue(field.name, Number(e8.target.value));
86080
+ const el2 = e8.target;
86081
+ const v2 = this._sanitizeSliderValue(Number(el2.value), min, max, step);
86082
+ el2.style.cssText = this._sliderFillStyle(v2, min, max);
86083
+ this._updateFormValue(field.name, v2);
83716
86084
  }}
83717
86085
  />
83718
86086
  <input
@@ -83723,7 +86091,15 @@ var ElicitationModal = class extends i4 {
83723
86091
  step="${step}"
83724
86092
  .value=${String(currentValue)}
83725
86093
  @input=${(e8) => {
83726
- this._updateFormValue(field.name, Number(e8.target.value));
86094
+ const raw = e8.target.value;
86095
+ if (raw === "" || raw === "-") return;
86096
+ this._updateFormValue(field.name, Number(raw));
86097
+ }}
86098
+ @change=${(e8) => {
86099
+ const el2 = e8.target;
86100
+ const v2 = this._sanitizeSliderValue(Number(el2.value), min, max, step);
86101
+ el2.value = String(v2);
86102
+ this._updateFormValue(field.name, v2);
83727
86103
  }}
83728
86104
  />
83729
86105
  </div>
@@ -83744,6 +86120,23 @@ var ElicitationModal = class extends i4 {
83744
86120
  />
83745
86121
  `;
83746
86122
  }
86123
+ _sliderFillStyle(value, min, max) {
86124
+ const pct = max > min ? (value - min) / (max - min) * 100 : 0;
86125
+ return `background: linear-gradient(to right, var(--accent-primary) ${pct}%, var(--border-glass) ${pct}%)`;
86126
+ }
86127
+ /** Clamp to [min, max] and round to nearest step (integer-safe). */
86128
+ _sanitizeSliderValue(raw, min, max, step) {
86129
+ let v2 = Number.isFinite(raw) ? raw : min;
86130
+ v2 = Math.min(max, Math.max(min, v2));
86131
+ if (step > 0) {
86132
+ v2 = min + Math.round((v2 - min) / step) * step;
86133
+ if (v2 > max) v2 = max;
86134
+ }
86135
+ if (Number.isInteger(step) && step >= 1) {
86136
+ v2 = Math.round(v2);
86137
+ }
86138
+ return v2;
86139
+ }
83747
86140
  _updateFormValue(name2, value) {
83748
86141
  this._formValues = { ...this._formValues, [name2]: value };
83749
86142
  }
@@ -83868,11 +86261,19 @@ ElicitationModal.styles = [
83868
86261
  color: white;
83869
86262
  }
83870
86263
 
86264
+ .btn-success:hover {
86265
+ filter: brightness(1.1);
86266
+ }
86267
+
83871
86268
  .btn-danger {
83872
86269
  background: var(--color-error);
83873
86270
  color: white;
83874
86271
  }
83875
86272
 
86273
+ .btn-danger:hover {
86274
+ filter: brightness(1.1);
86275
+ }
86276
+
83876
86277
  /* Confirm buttons */
83877
86278
  .confirm-actions {
83878
86279
  display: flex;
@@ -83982,7 +86383,7 @@ ElicitationModal.styles = [
83982
86383
  .slider-group {
83983
86384
  display: flex;
83984
86385
  flex-direction: column;
83985
- gap: 6px;
86386
+ gap: 4px;
83986
86387
  }
83987
86388
 
83988
86389
  .slider-row {
@@ -83993,47 +86394,104 @@ ElicitationModal.styles = [
83993
86394
 
83994
86395
  .slider-row input[type='range'] {
83995
86396
  flex: 1;
83996
- height: 6px;
83997
- background: rgba(255, 255, 255, 0.12);
83998
- border: 1px solid rgba(255, 255, 255, 0.08);
83999
- border-radius: 3px;
86397
+ height: 4px;
86398
+ border-radius: var(--radius-full, 9999px);
84000
86399
  -webkit-appearance: none;
84001
86400
  cursor: pointer;
84002
86401
  margin: 8px 0;
86402
+ border: none;
86403
+ outline: none;
86404
+ background: var(--border-glass);
84003
86405
  }
84004
86406
 
84005
86407
  .slider-row input[type='range']::-webkit-slider-thumb {
84006
86408
  -webkit-appearance: none;
84007
- width: 20px;
84008
- height: 20px;
86409
+ width: 16px;
86410
+ height: 16px;
84009
86411
  background: var(--accent-primary);
86412
+ border: 2px solid var(--bg-panel, var(--bg-glass));
84010
86413
  border-radius: 50%;
84011
86414
  cursor: pointer;
84012
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
86415
+ box-shadow:
86416
+ 0 0 0 2px var(--accent-primary),
86417
+ 0 1px 4px rgba(0, 0, 0, 0.3);
86418
+ transition:
86419
+ box-shadow 0.15s ease,
86420
+ transform 0.15s ease;
86421
+ }
86422
+
86423
+ .slider-row input[type='range']::-webkit-slider-thumb:hover {
86424
+ box-shadow:
86425
+ 0 0 0 3px var(--accent-primary),
86426
+ 0 0 8px var(--glow-primary);
86427
+ transform: scale(1.1);
86428
+ }
86429
+
86430
+ .slider-row input[type='range']:active::-webkit-slider-thumb {
86431
+ transform: scale(0.95);
84013
86432
  }
84014
86433
 
84015
86434
  .slider-row input[type='range']::-moz-range-track {
84016
- height: 6px;
84017
- background: rgba(255, 255, 255, 0.12);
84018
- border-radius: 3px;
84019
- border: 1px solid rgba(255, 255, 255, 0.08);
86435
+ height: 4px;
86436
+ background: var(--border-glass);
86437
+ border-radius: var(--radius-full, 9999px);
86438
+ border: none;
86439
+ }
86440
+
86441
+ .slider-row input[type='range']::-moz-range-progress {
86442
+ height: 4px;
86443
+ background: var(--accent-primary);
86444
+ border-radius: var(--radius-full, 9999px);
84020
86445
  }
84021
86446
 
84022
86447
  .slider-row input[type='range']::-moz-range-thumb {
84023
- width: 20px;
84024
- height: 20px;
86448
+ width: 12px;
86449
+ height: 12px;
84025
86450
  background: var(--accent-primary);
86451
+ border: 2px solid var(--bg-panel, var(--bg-glass));
84026
86452
  border-radius: 50%;
84027
86453
  cursor: pointer;
84028
- border: none;
84029
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
86454
+ box-shadow:
86455
+ 0 0 0 2px var(--accent-primary),
86456
+ 0 1px 4px rgba(0, 0, 0, 0.3);
86457
+ transition:
86458
+ box-shadow 0.15s ease,
86459
+ transform 0.15s ease;
86460
+ }
86461
+
86462
+ .slider-row input[type='range']::-moz-range-thumb:hover {
86463
+ box-shadow:
86464
+ 0 0 0 3px var(--accent-primary),
86465
+ 0 0 8px var(--glow-primary);
84030
86466
  }
84031
86467
 
84032
86468
  .slider-number-input {
84033
- width: 80px;
84034
- text-align: right;
86469
+ width: 64px;
86470
+ text-align: center;
84035
86471
  font-size: var(--text-sm);
84036
- padding: 4px 8px;
86472
+ font-weight: 500;
86473
+ font-variant-numeric: tabular-nums;
86474
+ padding: 4px 6px;
86475
+ background: var(--bg-glass);
86476
+ border: 1px solid var(--border-glass) !important;
86477
+ border-radius: var(--radius-sm);
86478
+ color: var(--t-primary);
86479
+ -moz-appearance: textfield;
86480
+ transition:
86481
+ border-color 0.15s ease,
86482
+ box-shadow 0.15s ease;
86483
+ }
86484
+
86485
+ .slider-number-input::-webkit-outer-spin-button,
86486
+ .slider-number-input::-webkit-inner-spin-button {
86487
+ -webkit-appearance: none;
86488
+ margin: 0;
86489
+ }
86490
+
86491
+ .slider-number-input:focus-visible {
86492
+ outline: none;
86493
+ border-color: var(--accent-primary) !important;
86494
+ box-shadow: 0 0 0 2px var(--glow-primary);
84037
86495
  }
84038
86496
 
84039
86497
  .range-labels {
@@ -84092,7 +86550,13 @@ ElicitationModal.styles = [
84092
86550
  background: var(--bg-glass-strong);
84093
86551
  border-radius: 12px;
84094
86552
  cursor: pointer;
84095
- transition: background 0.2s;
86553
+ transition:
86554
+ background 0.2s,
86555
+ box-shadow 0.2s;
86556
+ }
86557
+
86558
+ .toggle-track:hover {
86559
+ background: var(--bg-glass);
84096
86560
  }
84097
86561
 
84098
86562
  .toggle-track::after {
@@ -84111,6 +86575,14 @@ ElicitationModal.styles = [
84111
86575
  background: var(--accent-primary);
84112
86576
  }
84113
86577
 
86578
+ .toggle-switch input:checked + .toggle-track:hover {
86579
+ background: var(--accent-secondary);
86580
+ }
86581
+
86582
+ .toggle-switch input:focus-visible + .toggle-track {
86583
+ box-shadow: 0 0 0 2px var(--glow-primary);
86584
+ }
86585
+
84114
86586
  .toggle-switch input:checked + .toggle-track::after {
84115
86587
  transform: translateX(20px);
84116
86588
  }
@@ -84246,7 +86718,7 @@ var OverflowMenu = class extends i4 {
84246
86718
  return b2`
84247
86719
  <button
84248
86720
  class="trigger"
84249
- @click=${() => this._toggle()}
86721
+ @click=${(e8) => this._toggle(e8)}
84250
86722
  title="More actions"
84251
86723
  aria-label="More actions"
84252
86724
  >
@@ -85753,7 +88225,7 @@ ContextBar.styles = [
85753
88225
  backdrop-filter: blur(20px);
85754
88226
  border: 1px solid var(--border-glass);
85755
88227
  border-radius: var(--radius-md);
85756
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
88228
+ box-shadow: var(--shadow-lg);
85757
88229
  padding: 4px;
85758
88230
  z-index: 100;
85759
88231
  }
@@ -85764,7 +88236,7 @@ ContextBar.styles = [
85764
88236
  padding: 6px 10px;
85765
88237
  border: none;
85766
88238
  background: none;
85767
- color: var(--t-secondary);
88239
+ color: var(--t-muted);
85768
88240
  font-size: var(--text-sm);
85769
88241
  font-family: inherit;
85770
88242
  text-align: left;
@@ -85790,7 +88262,7 @@ ContextBar.styles = [
85790
88262
  font-size: var(--text-2xs);
85791
88263
  font-weight: 700;
85792
88264
  letter-spacing: 0.08em;
85793
- color: #4ade80;
88265
+ color: var(--color-success);
85794
88266
  text-transform: uppercase;
85795
88267
  margin-left: auto;
85796
88268
  }
@@ -85799,8 +88271,8 @@ ContextBar.styles = [
85799
88271
  width: 6px;
85800
88272
  height: 6px;
85801
88273
  border-radius: 50%;
85802
- background: #4ade80;
85803
- box-shadow: 0 0 6px 2px rgba(74, 222, 128, 0.6);
88274
+ background: var(--color-success);
88275
+ box-shadow: 0 0 6px 2px var(--color-success-glow);
85804
88276
  animation: live-pulse 2s ease-in-out infinite;
85805
88277
  }
85806
88278
 
@@ -85808,11 +88280,11 @@ ContextBar.styles = [
85808
88280
  0%,
85809
88281
  100% {
85810
88282
  opacity: 1;
85811
- box-shadow: 0 0 6px 2px rgba(74, 222, 128, 0.6);
88283
+ box-shadow: 0 0 6px 2px var(--color-success-glow);
85812
88284
  }
85813
88285
  50% {
85814
88286
  opacity: 0.5;
85815
- box-shadow: 0 0 3px 1px rgba(74, 222, 128, 0.3);
88287
+ box-shadow: 0 0 3px 1px var(--color-success-glow);
85816
88288
  }
85817
88289
  }
85818
88290