@vortexm/vjt 0.1.5 → 0.1.7

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.
package/dist/index.js CHANGED
@@ -4582,6 +4582,9 @@ var ActionRuntime = class {
4582
4582
  focusWidget;
4583
4583
  playAudio;
4584
4584
  stopPlaying;
4585
+ copyToClipboard;
4586
+ selectFile;
4587
+ confirmModal;
4585
4588
  startRecording;
4586
4589
  stopRecording;
4587
4590
  startListening;
@@ -4616,6 +4619,9 @@ var ActionRuntime = class {
4616
4619
  this.focusWidget = options.focusWidget;
4617
4620
  this.playAudio = options.playAudio;
4618
4621
  this.stopPlaying = options.stopPlaying;
4622
+ this.copyToClipboard = options.copyToClipboard;
4623
+ this.selectFile = options.selectFile;
4624
+ this.confirmModal = options.confirmModal;
4619
4625
  this.startRecording = options.startRecording;
4620
4626
  this.stopRecording = options.stopRecording;
4621
4627
  this.startListening = options.startListening;
@@ -4654,7 +4660,7 @@ var ActionRuntime = class {
4654
4660
  async runActions(actions, inputValue, context) {
4655
4661
  let current = inputValue;
4656
4662
  for (const action of actions) {
4657
- current = await this.runSingleAction(action, current, context);
4663
+ current = await this.runSingleAction(action, inputValue, context);
4658
4664
  }
4659
4665
  return current;
4660
4666
  }
@@ -4664,34 +4670,14 @@ var ActionRuntime = class {
4664
4670
  async runSingleAction(action, inputValue, context) {
4665
4671
  const actionName = action.action.trim();
4666
4672
  try {
4667
- let output = await this.executeAction(action, inputValue, context);
4673
+ const rawOutput = await this.executeAction(action, inputValue, context);
4674
+ logRuntimeDebug(this.debugLogging, "action-output", {
4675
+ action: actionName,
4676
+ output: rawOutput
4677
+ });
4678
+ let output = rawOutput;
4668
4679
  if (action.andThen?.length) {
4669
- if (Array.isArray(output)) {
4670
- const collected = [];
4671
- for (const [index, entry] of output.entries()) {
4672
- if (entry === null || entry === void 0) {
4673
- continue;
4674
- }
4675
- const nestedOutput = await this.runActions(action.andThen, entry, {
4676
- ...context,
4677
- currentValue: entry,
4678
- currentList: output,
4679
- currentIndex: index
4680
- });
4681
- if (nestedOutput !== null && nestedOutput !== void 0) {
4682
- if (Array.isArray(nestedOutput)) {
4683
- for (const item of nestedOutput) {
4684
- if (item !== null && item !== void 0) {
4685
- collected.push(item);
4686
- }
4687
- }
4688
- } else {
4689
- collected.push(nestedOutput);
4690
- }
4691
- }
4692
- }
4693
- output = collected;
4694
- } else if (output !== null && output !== void 0) {
4680
+ if (output !== null && output !== void 0) {
4695
4681
  output = await this.runActions(action.andThen, output, { ...context, currentValue: output });
4696
4682
  } else {
4697
4683
  output = null;
@@ -4699,6 +4685,7 @@ var ActionRuntime = class {
4699
4685
  }
4700
4686
  logRuntimeDebug(this.debugLogging, "action-result", {
4701
4687
  action: actionName,
4688
+ rawOutput,
4702
4689
  output
4703
4690
  });
4704
4691
  return output;
@@ -4716,7 +4703,7 @@ var ActionRuntime = class {
4716
4703
  }
4717
4704
  async executeAction(action, inputValue, context) {
4718
4705
  const name = action.action.trim();
4719
- logRuntimeDebug(this.debugLogging, "action", {
4706
+ logRuntimeDebug(this.debugLogging, "action-start", {
4720
4707
  action: name,
4721
4708
  args: action.args,
4722
4709
  inputValue,
@@ -4755,6 +4742,16 @@ var ActionRuntime = class {
4755
4742
  await this.playAudio(this.resolveReference(name.slice(5), context.currentValue, context.responseValue));
4756
4743
  return null;
4757
4744
  }
4745
+ if (name === "copyToClipboard") {
4746
+ await this.copyToClipboard(inputValue);
4747
+ return inputValue;
4748
+ }
4749
+ if (name === "selectFile") {
4750
+ return this.selectFile(action.args);
4751
+ }
4752
+ if (name === "confirmModal") {
4753
+ return await this.confirmModal(action.args, inputValue) ? inputValue : void 0;
4754
+ }
4758
4755
  if (name === "stopPlaying") {
4759
4756
  await this.stopPlaying();
4760
4757
  return null;
@@ -4785,7 +4782,9 @@ var ActionRuntime = class {
4785
4782
  this.setActiveContextMenu({
4786
4783
  id: menuId,
4787
4784
  x: position.x,
4788
- y: position.y
4785
+ y: position.y,
4786
+ currentValue: context.currentValue,
4787
+ openPath: []
4789
4788
  });
4790
4789
  return null;
4791
4790
  }
@@ -4827,7 +4826,11 @@ var ActionRuntime = class {
4827
4826
  return inputValue;
4828
4827
  }
4829
4828
  if (name.startsWith("get ")) {
4830
- return this.resolveReference(name.slice(4), context.currentValue, context.responseValue);
4829
+ const reference = name.slice(4);
4830
+ if (Array.isArray(context.currentValue) && this.isCurrentScopedReference(reference)) {
4831
+ return context.currentValue.map((entry) => this.resolveReference(reference, entry, context.responseValue)).filter((entry) => entry !== null && entry !== void 0);
4832
+ }
4833
+ return this.resolveReference(reference, context.currentValue, context.responseValue);
4831
4834
  }
4832
4835
  if (name.startsWith("equals ")) {
4833
4836
  return this.resolveReference(name.slice(7), context.currentValue, context.responseValue) === action.args;
@@ -4848,7 +4851,11 @@ var ActionRuntime = class {
4848
4851
  return nextValue;
4849
4852
  }
4850
4853
  if (name.startsWith("filter ")) {
4851
- const value = this.resolveReference(name.slice(7), context.currentValue, context.responseValue);
4854
+ const reference = name.slice(7);
4855
+ if (Array.isArray(context.currentValue) && this.isCurrentScopedReference(reference)) {
4856
+ return context.currentValue.filter((entry) => this.resolveReference(reference, entry, context.responseValue));
4857
+ }
4858
+ const value = this.resolveReference(reference, context.currentValue, context.responseValue);
4852
4859
  return value ? inputValue : null;
4853
4860
  }
4854
4861
  if (name === "remove") {
@@ -4909,6 +4916,9 @@ var ActionRuntime = class {
4909
4916
  return inputValue;
4910
4917
  }
4911
4918
  if (name === "map") {
4919
+ if (Array.isArray(context.currentValue)) {
4920
+ return context.currentValue.map((entry) => this.resolveMappedValue(action.args, entry, context.responseValue));
4921
+ }
4912
4922
  return this.resolveMappedValue(action.args, context.currentValue, context.responseValue);
4913
4923
  }
4914
4924
  if (name === "ifelse") {
@@ -4951,6 +4961,9 @@ var ActionRuntime = class {
4951
4961
  }
4952
4962
  return JSON.stringify(value);
4953
4963
  }
4964
+ isCurrentScopedReference(reference) {
4965
+ return reference === "current" || reference.startsWith("current.") || reference.startsWith("this.") || reference === "this";
4966
+ }
4954
4967
  };
4955
4968
  function isIfElseArgs(value) {
4956
4969
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -6324,6 +6337,21 @@ function renderModalOverlay(env, modalId) {
6324
6337
  }
6325
6338
 
6326
6339
  // src/lib/widgets/context-menu.ts
6340
+ var MENU_WIDTH = 220;
6341
+ var MENU_ITEM_HEIGHT = 44;
6342
+ var MENU_PADDING = 8;
6343
+ function getContextMenuItemAtPath(items, path) {
6344
+ let currentItems = items ?? [];
6345
+ let currentItem = null;
6346
+ for (const index of path) {
6347
+ currentItem = currentItems[index] ?? null;
6348
+ if (!currentItem) {
6349
+ return null;
6350
+ }
6351
+ currentItems = currentItem.items ?? [];
6352
+ }
6353
+ return currentItem;
6354
+ }
6327
6355
  function renderContextMenuOverlay(env, menu) {
6328
6356
  const node = env.nodeById.get(menu.id);
6329
6357
  if (!node || node.widget !== "context-menu") {
@@ -6331,8 +6359,52 @@ function renderContextMenuOverlay(env, menu) {
6331
6359
  }
6332
6360
  const state = env.ensureWidgetState(node, menu.id);
6333
6361
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
6334
- const items = (node.items ?? []).map((item, index) => `<button class="vjt-context-menu-item" type="button" data-context-menu-item="${env.escapeHtml(menu.id)}:${index}"${enabled ? "" : " disabled"}>${env.escapeHtml(env.resolveI18nValue(item.name))}</button>`).join("");
6335
- return `<div class="vjt-context-menu-backdrop" data-context-menu-backdrop="${env.escapeHtml(menu.id)}"><div class="vjt-context-menu" style="left:${menu.x}px;top:${menu.y}px">${items}</div></div>`;
6362
+ const openPath = Array.isArray(menu.openPath) ? menu.openPath : [];
6363
+ const layers = [];
6364
+ let items = node.items ?? [];
6365
+ let pathPrefix = [];
6366
+ let layerX = menu.x;
6367
+ let layerY = menu.y;
6368
+ while (items.length > 0) {
6369
+ layers.push(renderMenuLayer(env, menu.id, items, enabled, layerX, layerY, pathPrefix));
6370
+ const nextIndex = openPath[pathPrefix.length];
6371
+ if (typeof nextIndex !== "number") {
6372
+ break;
6373
+ }
6374
+ const nextItem = items[nextIndex];
6375
+ if (!nextItem?.items?.length) {
6376
+ break;
6377
+ }
6378
+ const proposedY = layerY + MENU_PADDING + nextIndex * MENU_ITEM_HEIGHT;
6379
+ const rightX = layerX + MENU_WIDTH - MENU_PADDING;
6380
+ const leftX = layerX - MENU_WIDTH + MENU_PADDING;
6381
+ layerX = rightX + MENU_WIDTH <= window.innerWidth ? rightX : Math.max(8, leftX);
6382
+ layerY = Math.max(8, Math.min(proposedY, window.innerHeight - (16 + nextItem.items.length * MENU_ITEM_HEIGHT) - 8));
6383
+ items = nextItem.items;
6384
+ pathPrefix = [...pathPrefix, nextIndex];
6385
+ }
6386
+ return `<div class="vjt-context-menu-backdrop" data-context-menu-backdrop="${env.escapeHtml(menu.id)}">${layers.join("")}</div>`;
6387
+ }
6388
+ function renderMenuLayer(env, menuId, items, enabled, x, y, pathPrefix) {
6389
+ const content = items.map((item, index) => {
6390
+ const path = [...pathPrefix, index];
6391
+ const pathText = path.join(".");
6392
+ const label = env.escapeHtml(env.resolveI18nValue(item.name));
6393
+ const disabled = enabled ? "" : " disabled";
6394
+ if (item.items?.length) {
6395
+ return `<button class="vjt-context-menu-item vjt-context-menu-item--parent" type="button" data-context-menu-expand="${env.escapeHtml(menuId)}:${env.escapeHtml(pathText)}"${disabled}><span>${label}</span><span class="vjt-context-menu-chevron">&gt;</span></button>`;
6396
+ }
6397
+ return `<button class="vjt-context-menu-item" type="button" data-context-menu-item="${env.escapeHtml(menuId)}:${env.escapeHtml(pathText)}"${disabled}>${label}</button>`;
6398
+ }).join("");
6399
+ return `<div class="vjt-context-menu" style="left:${x}px;top:${y}px">${content}</div>`;
6400
+ }
6401
+
6402
+ // src/lib/widgets/confirm-modal.ts
6403
+ function renderConfirmModalOverlay(env, modal) {
6404
+ const caption = env.escapeHtml(env.resolveI18nValue(modal.caption));
6405
+ const yes = env.escapeHtml(env.resolveI18nValue(modal.yes));
6406
+ const no = env.escapeHtml(env.resolveI18nValue(modal.no));
6407
+ return `<div class="vjt-confirm-backdrop" data-confirm-backdrop="true"><div class="vjt-confirm-window"><div class="vjt-confirm-caption">${caption}</div><div class="vjt-confirm-actions"><button class="vjt-button vjt-button--bright" type="button" data-confirm-yes="true">${yes}</button><button class="vjt-button vjt-button--regular" type="button" data-confirm-no="true">${no}</button></div></div></div>`;
6336
6408
  }
6337
6409
 
6338
6410
  // src/lib/widgets/bindings.ts
@@ -6524,7 +6596,7 @@ function bindWidgetEvents(root, env) {
6524
6596
  });
6525
6597
  continue;
6526
6598
  }
6527
- if (element instanceof HTMLAnchorElement && element.dataset.widget === "link" && env.getInlineActions(node)?.length) {
6599
+ if (element instanceof HTMLAnchorElement && element.dataset.widget === "link" && (node.events?.onClick?.length || env.getInlineActions(node)?.length)) {
6528
6600
  element.addEventListener("pointerdown", (event) => {
6529
6601
  env.setLastPointer({ x: event.clientX, y: event.clientY });
6530
6602
  });
@@ -6594,6 +6666,45 @@ function bindDelegatedUi(root, env) {
6594
6666
  window.addEventListener("pointercancel", () => {
6595
6667
  env.stopSplitterDrag();
6596
6668
  });
6669
+ root.addEventListener("pointerover", (event) => {
6670
+ void (async () => {
6671
+ const target = event.target;
6672
+ if (!(target instanceof Element)) {
6673
+ return;
6674
+ }
6675
+ const hoveredItem = target.closest("[data-context-menu-item], [data-context-menu-expand]");
6676
+ if (!(hoveredItem instanceof HTMLButtonElement)) {
6677
+ return;
6678
+ }
6679
+ const descriptor = hoveredItem.dataset.contextMenuExpand ?? hoveredItem.dataset.contextMenuItem;
6680
+ if (!descriptor) {
6681
+ return;
6682
+ }
6683
+ const [menuId, pathText = ""] = descriptor.split(":");
6684
+ const node = env.nodeById.get(menuId);
6685
+ if (node?.widget !== "context-menu") {
6686
+ return;
6687
+ }
6688
+ const activeMenu = env.getActiveContextMenu();
6689
+ if (!activeMenu || activeMenu.id !== menuId) {
6690
+ return;
6691
+ }
6692
+ const path = parseMenuPath(pathText);
6693
+ const item = getContextMenuItemAtPath(node.items, path);
6694
+ if (!item) {
6695
+ return;
6696
+ }
6697
+ const nextOpenPath = item.items?.length ? path : path.slice(0, -1);
6698
+ if (samePath(activeMenu.openPath ?? [], nextOpenPath)) {
6699
+ return;
6700
+ }
6701
+ env.setActiveContextMenu({
6702
+ ...activeMenu,
6703
+ openPath: nextOpenPath
6704
+ });
6705
+ await env.rerenderRoot();
6706
+ })();
6707
+ });
6597
6708
  root.addEventListener("click", (event) => {
6598
6709
  void (async () => {
6599
6710
  const target = event.target;
@@ -6672,7 +6783,7 @@ function bindDelegatedUi(root, env) {
6672
6783
  }
6673
6784
  const menuItem = target.closest("[data-context-menu-item]");
6674
6785
  if (menuItem instanceof HTMLButtonElement) {
6675
- const [menuId, indexValue] = (menuItem.dataset.contextMenuItem ?? "").split(":");
6786
+ const [menuId, pathText = ""] = (menuItem.dataset.contextMenuItem ?? "").split(":");
6676
6787
  const node = env.nodeById.get(menuId);
6677
6788
  if (node?.widget !== "context-menu") {
6678
6789
  return;
@@ -6680,11 +6791,27 @@ function bindDelegatedUi(root, env) {
6680
6791
  if (!env.isWidgetEnabled(node, menuId)) {
6681
6792
  return;
6682
6793
  }
6683
- const item = node.items?.[Number.parseInt(indexValue, 10) || 0];
6794
+ const item = getContextMenuItemAtPath(node.items, parseMenuPath(pathText));
6795
+ const menuContext = env.getActiveContextMenu();
6796
+ const currentValue = menuContext?.id === menuId ? menuContext.currentValue ?? null : null;
6684
6797
  env.setActiveContextMenu(null);
6685
6798
  if (item?.actions?.length) {
6686
- await env.runActions(item.actions, null, { currentValue: null });
6799
+ await env.runActions(item.actions, currentValue, { currentValue });
6800
+ }
6801
+ await env.rerenderRoot();
6802
+ return;
6803
+ }
6804
+ const menuParentItem = target.closest("[data-context-menu-expand]");
6805
+ if (menuParentItem instanceof HTMLButtonElement) {
6806
+ const [menuId, pathText = ""] = (menuParentItem.dataset.contextMenuExpand ?? "").split(":");
6807
+ const activeMenu = env.getActiveContextMenu();
6808
+ if (!activeMenu || activeMenu.id !== menuId) {
6809
+ return;
6687
6810
  }
6811
+ env.setActiveContextMenu({
6812
+ ...activeMenu,
6813
+ openPath: parseMenuPath(pathText)
6814
+ });
6688
6815
  await env.rerenderRoot();
6689
6816
  return;
6690
6817
  }
@@ -6703,10 +6830,32 @@ function bindDelegatedUi(root, env) {
6703
6830
  await env.runActions(item.actions, null, { currentValue: null });
6704
6831
  }
6705
6832
  await env.rerenderRoot();
6833
+ return;
6834
+ }
6835
+ if (target.closest("[data-confirm-yes]")) {
6836
+ env.resolveConfirmModal(true);
6837
+ await env.rerenderRoot();
6838
+ return;
6839
+ }
6840
+ if (target.closest("[data-confirm-no]")) {
6841
+ env.resolveConfirmModal(false);
6842
+ await env.rerenderRoot();
6706
6843
  }
6707
6844
  })();
6708
6845
  });
6709
6846
  }
6847
+ function parseMenuPath(value) {
6848
+ if (!value) {
6849
+ return [];
6850
+ }
6851
+ return value.split(".").map((part) => Number.parseInt(part, 10)).filter((part) => !Number.isNaN(part));
6852
+ }
6853
+ function samePath(left, right) {
6854
+ if (left.length !== right.length) {
6855
+ return false;
6856
+ }
6857
+ return left.every((value, index) => value === right[index]);
6858
+ }
6710
6859
 
6711
6860
  // src/lib/render.ts
6712
6861
  var DEFAULT_DATE_FORMAT = "dd.MM.yyyy";
@@ -6863,6 +7012,7 @@ var RuntimeRenderer = class {
6863
7012
  hasTriggeredInitialRefresh = false;
6864
7013
  activeModalId = null;
6865
7014
  activeContextMenu = null;
7015
+ activeConfirmModal = null;
6866
7016
  lastPointer = { x: 24, y: 24 };
6867
7017
  pendingResetModalIds = /* @__PURE__ */ new Set();
6868
7018
  constructor(description, options = {}) {
@@ -6966,6 +7116,9 @@ var RuntimeRenderer = class {
6966
7116
  focusWidget: (reference) => this.focusWidget(reference),
6967
7117
  playAudio: (value) => this.voiceRuntime.play(value),
6968
7118
  stopPlaying: () => this.voiceRuntime.stopPlaying(),
7119
+ copyToClipboard: (value) => this.copyToClipboard(value),
7120
+ selectFile: (args) => this.selectFile(args),
7121
+ confirmModal: (args, inputValue) => this.confirmModal(args, inputValue),
6969
7122
  startRecording: () => this.voiceRuntime.startRecording(),
6970
7123
  stopRecording: () => Promise.resolve(this.voiceRuntime.stopRecording()),
6971
7124
  startListening: () => this.voiceRuntime.startListening(),
@@ -7124,6 +7277,138 @@ var RuntimeRenderer = class {
7124
7277
  await this.rerenderRoot();
7125
7278
  await this.runSystemEvent("onAfterNavigate");
7126
7279
  }
7280
+ async copyToClipboard(value) {
7281
+ const text = value == null ? "" : typeof value === "string" ? value : JSON.stringify(value) ?? "";
7282
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
7283
+ await navigator.clipboard.writeText(text);
7284
+ return;
7285
+ }
7286
+ if (typeof document === "undefined") {
7287
+ throw new Error("Clipboard API is unavailable");
7288
+ }
7289
+ const textarea = document.createElement("textarea");
7290
+ textarea.value = text;
7291
+ textarea.setAttribute("readonly", "true");
7292
+ textarea.style.position = "fixed";
7293
+ textarea.style.left = "-9999px";
7294
+ textarea.style.top = "0";
7295
+ document.body.append(textarea);
7296
+ textarea.focus();
7297
+ textarea.select();
7298
+ const copied = document.execCommand("copy");
7299
+ textarea.remove();
7300
+ if (!copied) {
7301
+ throw new Error("Failed to copy value to clipboard");
7302
+ }
7303
+ }
7304
+ async selectFile(args) {
7305
+ if (typeof document === "undefined") {
7306
+ throw new Error("File selection is unavailable");
7307
+ }
7308
+ const options = isPlainObject4(args) ? args : {};
7309
+ const input = document.createElement("input");
7310
+ input.type = "file";
7311
+ input.style.position = "fixed";
7312
+ input.style.left = "-9999px";
7313
+ input.style.top = "0";
7314
+ if (typeof options.accept === "string" && options.accept.trim()) {
7315
+ input.accept = options.accept;
7316
+ }
7317
+ input.multiple = options.multiple === true;
7318
+ const selection = new Promise((resolve, reject) => {
7319
+ let settled = false;
7320
+ const finish = (value) => {
7321
+ if (settled) {
7322
+ return;
7323
+ }
7324
+ settled = true;
7325
+ resolve(value);
7326
+ };
7327
+ const fail = (error) => {
7328
+ if (settled) {
7329
+ return;
7330
+ }
7331
+ settled = true;
7332
+ reject(error instanceof Error ? error : new Error(String(error)));
7333
+ };
7334
+ const cleanup = () => {
7335
+ window.removeEventListener("focus", handleFocus, true);
7336
+ input.remove();
7337
+ };
7338
+ const handleFocus = () => {
7339
+ window.setTimeout(() => {
7340
+ if (!settled && (!input.files || input.files.length === 0)) {
7341
+ cleanup();
7342
+ finish(null);
7343
+ }
7344
+ }, 250);
7345
+ };
7346
+ input.addEventListener("change", () => {
7347
+ void (async () => {
7348
+ try {
7349
+ const files = Array.from(input.files ?? []);
7350
+ const converted = await Promise.all(files.map((file) => this.readSelectedFile(file)));
7351
+ cleanup();
7352
+ finish(input.multiple ? converted : converted[0] ?? null);
7353
+ } catch (error) {
7354
+ cleanup();
7355
+ fail(error);
7356
+ }
7357
+ })();
7358
+ }, { once: true });
7359
+ input.addEventListener("cancel", () => {
7360
+ cleanup();
7361
+ finish(null);
7362
+ }, { once: true });
7363
+ window.addEventListener("focus", handleFocus, true);
7364
+ document.body.append(input);
7365
+ input.click();
7366
+ });
7367
+ return selection;
7368
+ }
7369
+ readSelectedFile(file) {
7370
+ return new Promise((resolve, reject) => {
7371
+ const reader = new FileReader();
7372
+ reader.onerror = () => {
7373
+ reject(reader.error ?? new Error(`Failed to read file ${file.name}`));
7374
+ };
7375
+ reader.onload = () => {
7376
+ const result = typeof reader.result === "string" ? reader.result : "";
7377
+ const [, data = ""] = result.split(",", 2);
7378
+ resolve({
7379
+ name: file.name,
7380
+ mimeType: file.type || "application/octet-stream",
7381
+ size: file.size,
7382
+ data
7383
+ });
7384
+ };
7385
+ reader.readAsDataURL(file);
7386
+ });
7387
+ }
7388
+ async confirmModal(args, _inputValue) {
7389
+ const options = isPlainObject4(args) ? args : {};
7390
+ const caption = typeof options.caption === "string" && options.caption ? options.caption : "Confirm?";
7391
+ const yes = typeof options.yes === "string" && options.yes ? options.yes : "Yes";
7392
+ const no = typeof options.no === "string" && options.no ? options.no : "No";
7393
+ return new Promise((resolve) => {
7394
+ this.activeConfirmModal?.resolve(false);
7395
+ this.activeConfirmModal = {
7396
+ caption,
7397
+ yes,
7398
+ no,
7399
+ resolve
7400
+ };
7401
+ void this.rerenderRoot();
7402
+ });
7403
+ }
7404
+ resolveConfirmModal(confirmed) {
7405
+ const activeModal = this.activeConfirmModal;
7406
+ if (!activeModal) {
7407
+ return;
7408
+ }
7409
+ this.activeConfirmModal = null;
7410
+ activeModal.resolve(confirmed);
7411
+ }
7127
7412
  reindexStaticTree() {
7128
7413
  this.nodeByKey.clear();
7129
7414
  this.nodeById.clear();
@@ -7178,7 +7463,11 @@ var RuntimeRenderer = class {
7178
7463
  stateByKey: Array.from(this.stateByKey.entries(), ([key, state]) => [key, deepClone(state)]),
7179
7464
  vars: Array.from(this.vars.entries(), ([name, value]) => [name, deepClone(value)]),
7180
7465
  activeModalId: this.activeModalId,
7181
- activeContextMenu: this.activeContextMenu ? deepClone(this.activeContextMenu) : null
7466
+ activeContextMenu: this.activeContextMenu ? {
7467
+ id: this.activeContextMenu.id,
7468
+ x: this.activeContextMenu.x,
7469
+ y: this.activeContextMenu.y
7470
+ } : null
7182
7471
  };
7183
7472
  }
7184
7473
  indexStaticNodes(node, path) {
@@ -7688,7 +7977,8 @@ var RuntimeRenderer = class {
7688
7977
  const env = this.getWidgetRenderEnvironment();
7689
7978
  const modalMarkup = this.activeModalId ? renderModalOverlay(env, this.activeModalId) : "";
7690
7979
  const menuMarkup = this.activeContextMenu ? renderContextMenuOverlay(env, this.activeContextMenu) : "";
7691
- return `${modalMarkup}${menuMarkup}`;
7980
+ const confirmMarkup = this.activeConfirmModal ? renderConfirmModalOverlay(env, this.activeConfirmModal) : "";
7981
+ return `${modalMarkup}${menuMarkup}${confirmMarkup}`;
7692
7982
  }
7693
7983
  updatePointerFromEvent(event) {
7694
7984
  this.lastPointer = updatePointerFromMouseEvent(event);
@@ -7915,6 +8205,7 @@ var RuntimeRenderer = class {
7915
8205
  nodeByKey: this.nodeByKey,
7916
8206
  stateByKey: this.stateByKey,
7917
8207
  getLastPointer: () => this.lastPointer,
8208
+ getActiveContextMenu: () => this.activeContextMenu,
7918
8209
  setActiveContextMenu: (menu) => {
7919
8210
  this.activeContextMenu = menu;
7920
8211
  },
@@ -7924,6 +8215,7 @@ var RuntimeRenderer = class {
7924
8215
  updateSplitterDrag: (clientPosition) => this.updateSplitterDrag(clientPosition),
7925
8216
  stopSplitterDrag: () => this.stopSplitterDrag(),
7926
8217
  hasSplitterDrag: () => this.splitterDragState !== null,
8218
+ resolveConfirmModal: (confirmed) => this.resolveConfirmModal(confirmed),
7927
8219
  closeModal: (modalId) => this.closeModal(modalId),
7928
8220
  rerenderRoot: () => this.rerenderRoot(),
7929
8221
  redispatchUnderlyingClick: (x, y) => this.redispatchUnderlyingClick(x, y),
@@ -34,6 +34,9 @@ type ActionRuntimeOptions = {
34
34
  focusWidget: (reference: string) => void;
35
35
  playAudio: (value: unknown) => Promise<void>;
36
36
  stopPlaying: () => Promise<void>;
37
+ copyToClipboard: (value: unknown) => Promise<void>;
38
+ selectFile: (options: unknown) => Promise<unknown>;
39
+ confirmModal: (options: unknown, inputValue: unknown) => Promise<boolean>;
37
40
  startRecording: () => Promise<void>;
38
41
  stopRecording: () => Promise<void>;
39
42
  startListening: () => Promise<void>;
@@ -53,6 +56,8 @@ type ActionRuntimeOptions = {
53
56
  id: string;
54
57
  x: number;
55
58
  y: number;
59
+ currentValue?: unknown;
60
+ openPath?: number[];
56
61
  } | null) => void;
57
62
  setActiveModalId: (modalId: string | null) => void;
58
63
  closeModal: (modalId?: string | null) => void;
@@ -79,6 +84,9 @@ export declare class ActionRuntime {
79
84
  private readonly focusWidget;
80
85
  private readonly playAudio;
81
86
  private readonly stopPlaying;
87
+ private readonly copyToClipboard;
88
+ private readonly selectFile;
89
+ private readonly confirmModal;
82
90
  private readonly startRecording;
83
91
  private readonly stopRecording;
84
92
  private readonly startListening;
@@ -106,5 +114,6 @@ export declare class ActionRuntime {
106
114
  private getCurrentListState;
107
115
  private cloneValue;
108
116
  private stringifyValue;
117
+ private isCurrentScopedReference;
109
118
  }
110
119
  export {};
@@ -21,10 +21,14 @@ export declare function adjustContextMenuPosition(root: HTMLElement, activeConte
21
21
  id: string;
22
22
  x: number;
23
23
  y: number;
24
+ currentValue?: unknown;
25
+ openPath?: number[];
24
26
  } | null): {
25
27
  id: string;
26
28
  x: number;
27
29
  y: number;
30
+ currentValue?: unknown;
31
+ openPath?: number[];
28
32
  } | null;
29
33
  export declare function updatePointerFromMouseEvent(event: MouseEvent): {
30
34
  x: number;
@@ -0,0 +1,7 @@
1
+ import type { WidgetRenderEnvironment } from './context.js';
2
+ export type ConfirmModalState = {
3
+ caption: string;
4
+ yes: string;
5
+ no: string;
6
+ };
7
+ export declare function renderConfirmModalOverlay(env: WidgetRenderEnvironment, modal: ConfirmModalState): string;
@@ -1,14 +1,20 @@
1
1
  import type { ActionDefinition, BaseNode } from '../types.js';
2
2
  import type { WidgetRenderEnvironment } from './context.js';
3
+ export type ContextMenuItem = {
4
+ name: string;
5
+ actions?: ActionDefinition[];
6
+ items?: ContextMenuItem[];
7
+ };
3
8
  export type ContextMenuNode = BaseNode & {
4
9
  widget: 'context-menu';
5
- items?: Array<{
6
- name: string;
7
- actions?: ActionDefinition[];
8
- }>;
10
+ items?: ContextMenuItem[];
9
11
  };
10
- export declare function renderContextMenuOverlay(env: WidgetRenderEnvironment, menu: {
12
+ export type ActiveContextMenuState = {
11
13
  id: string;
12
14
  x: number;
13
15
  y: number;
14
- }): string;
16
+ currentValue?: unknown;
17
+ openPath?: number[];
18
+ };
19
+ export declare function getContextMenuItemAtPath(items: ContextMenuItem[] | undefined, path: number[]): ContextMenuItem | null;
20
+ export declare function renderContextMenuOverlay(env: WidgetRenderEnvironment, menu: ActiveContextMenuState): string;
@@ -5,7 +5,7 @@ import type { CheckboxNode } from './checkbox.js';
5
5
  import type { ComboboxNode } from './combobox.js';
6
6
  import type { ConditionalContainerNode } from './conditional-container.js';
7
7
  import type { ContainerCell, ContainerLayoutNode } from './container-layout.js';
8
- import type { ContextMenuNode } from './context-menu.js';
8
+ import type { ActiveContextMenuState, ContextMenuNode } from './context-menu.js';
9
9
  import type { GridViewNode } from './grid-view.js';
10
10
  import type { ImageNode } from './image.js';
11
11
  import type { LinkNode } from './link.js';
@@ -23,11 +23,7 @@ import type { TabsNode } from './tabs.js';
23
23
  import type { TextareaNode } from './textarea.js';
24
24
  import type { UiReferenceNode } from './ui-reference.js';
25
25
  export type WidgetRenderEnvironment = {
26
- readonly activeContextMenu: {
27
- id: string;
28
- x: number;
29
- y: number;
30
- } | null;
26
+ readonly activeContextMenu: ActiveContextMenuState | null;
31
27
  readonly activeModalId: string | null;
32
28
  readonly nodeById: Map<string, DescriptionNode>;
33
29
  escapeHtml: (value: unknown) => string;
@@ -7,10 +7,19 @@ type DelegationEnvironment = {
7
7
  x: number;
8
8
  y: number;
9
9
  };
10
+ getActiveContextMenu: () => {
11
+ id: string;
12
+ x: number;
13
+ y: number;
14
+ currentValue?: unknown;
15
+ openPath?: number[];
16
+ } | null;
10
17
  setActiveContextMenu: (menu: {
11
18
  id: string;
12
19
  x: number;
13
20
  y: number;
21
+ currentValue?: unknown;
22
+ openPath?: number[];
14
23
  } | null) => void;
15
24
  isWidgetEnabled: (node: DescriptionNode, key: string) => boolean;
16
25
  startSplitterDrag: (layoutKey: string, axis: 'horizontal' | 'vertical', splitterIndex: number, startPointer: number) => void;
@@ -18,6 +27,7 @@ type DelegationEnvironment = {
18
27
  updateSplitterDrag: (clientPosition: number) => void;
19
28
  stopSplitterDrag: () => void;
20
29
  hasSplitterDrag: () => boolean;
30
+ resolveConfirmModal: (confirmed: boolean) => void;
21
31
  closeModal: (modalId: string | null) => void;
22
32
  rerenderRoot: () => Promise<void>;
23
33
  redispatchUnderlyingClick: (clientX: number, clientY: number) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vortexm/vjt",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/vjt-styles.css CHANGED
@@ -685,12 +685,21 @@ body {
685
685
  }
686
686
 
687
687
  .vjt-modal-backdrop,
688
- .vjt-context-menu-backdrop {
688
+ .vjt-context-menu-backdrop,
689
+ .vjt-confirm-backdrop {
689
690
  position: fixed;
690
691
  inset: 0;
692
+ }
693
+
694
+ .vjt-modal-backdrop,
695
+ .vjt-context-menu-backdrop {
691
696
  z-index: 1000;
692
697
  }
693
698
 
699
+ .vjt-confirm-backdrop {
700
+ z-index: 1100;
701
+ }
702
+
694
703
  .vjt-modal-backdrop {
695
704
  display: flex;
696
705
  align-items: center;
@@ -759,16 +768,20 @@ body {
759
768
  }
760
769
 
761
770
  .vjt-context-menu {
762
- position: fixed;
763
- min-width: 180px;
764
- padding: 8px;
765
- border-radius: 14px;
771
+ position: fixed;
772
+ box-sizing: border-box;
773
+ width: 220px;
774
+ padding: 8px;
775
+ border-radius: 14px;
766
776
  border: 1px solid var(--vjt-border);
767
777
  background: var(--vjt-surface);
768
778
  box-shadow: var(--vjt-shadow);
769
779
  }
770
780
 
771
781
  .vjt-context-menu-item {
782
+ display: flex;
783
+ align-items: center;
784
+ justify-content: space-between;
772
785
  width: 100%;
773
786
  padding: 10px 12px;
774
787
  text-align: left;
@@ -781,3 +794,48 @@ body {
781
794
  .vjt-context-menu-item:hover {
782
795
  background: color-mix(in srgb, var(--vjt-accent) 14%, transparent);
783
796
  }
797
+
798
+ .vjt-context-menu-item--parent {
799
+ font-weight: 500;
800
+ }
801
+
802
+ .vjt-context-menu-chevron {
803
+ margin-left: 16px;
804
+ opacity: 0.7;
805
+ }
806
+
807
+ .vjt-confirm-backdrop {
808
+ display: flex;
809
+ align-items: center;
810
+ justify-content: center;
811
+ background: rgba(7, 10, 15, 0.42);
812
+ padding: 24px;
813
+ }
814
+
815
+ .vjt-confirm-window {
816
+ width: min(420px, calc(100vw - 32px));
817
+ display: flex;
818
+ flex-direction: column;
819
+ gap: 18px;
820
+ padding: 22px;
821
+ border-radius: 18px;
822
+ border: 1px solid var(--vjt-border);
823
+ background: var(--vjt-modal-surface);
824
+ color: var(--vjt-text);
825
+ box-shadow: var(--vjt-shadow);
826
+ }
827
+
828
+ .vjt-confirm-caption {
829
+ line-height: 1.45;
830
+ white-space: pre-wrap;
831
+ }
832
+
833
+ .vjt-confirm-actions {
834
+ display: flex;
835
+ justify-content: flex-end;
836
+ gap: 10px;
837
+ }
838
+
839
+ .vjt-confirm-actions .vjt-button {
840
+ min-width: 120px;
841
+ }