@vortexm/vjt 0.1.5 → 0.1.6

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
  }
@@ -4666,32 +4672,7 @@ var ActionRuntime = class {
4666
4672
  try {
4667
4673
  let output = await this.executeAction(action, inputValue, context);
4668
4674
  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) {
4675
+ if (output !== null && output !== void 0) {
4695
4676
  output = await this.runActions(action.andThen, output, { ...context, currentValue: output });
4696
4677
  } else {
4697
4678
  output = null;
@@ -4755,6 +4736,16 @@ var ActionRuntime = class {
4755
4736
  await this.playAudio(this.resolveReference(name.slice(5), context.currentValue, context.responseValue));
4756
4737
  return null;
4757
4738
  }
4739
+ if (name === "copyToClipboard") {
4740
+ await this.copyToClipboard(inputValue);
4741
+ return inputValue;
4742
+ }
4743
+ if (name === "selectFile") {
4744
+ return this.selectFile(action.args);
4745
+ }
4746
+ if (name === "confirmModal") {
4747
+ return await this.confirmModal(action.args, inputValue) ? inputValue : void 0;
4748
+ }
4758
4749
  if (name === "stopPlaying") {
4759
4750
  await this.stopPlaying();
4760
4751
  return null;
@@ -4785,7 +4776,9 @@ var ActionRuntime = class {
4785
4776
  this.setActiveContextMenu({
4786
4777
  id: menuId,
4787
4778
  x: position.x,
4788
- y: position.y
4779
+ y: position.y,
4780
+ currentValue: context.currentValue,
4781
+ openPath: []
4789
4782
  });
4790
4783
  return null;
4791
4784
  }
@@ -4827,7 +4820,11 @@ var ActionRuntime = class {
4827
4820
  return inputValue;
4828
4821
  }
4829
4822
  if (name.startsWith("get ")) {
4830
- return this.resolveReference(name.slice(4), context.currentValue, context.responseValue);
4823
+ const reference = name.slice(4);
4824
+ if (Array.isArray(context.currentValue) && this.isCurrentScopedReference(reference)) {
4825
+ return context.currentValue.map((entry) => this.resolveReference(reference, entry, context.responseValue)).filter((entry) => entry !== null && entry !== void 0);
4826
+ }
4827
+ return this.resolveReference(reference, context.currentValue, context.responseValue);
4831
4828
  }
4832
4829
  if (name.startsWith("equals ")) {
4833
4830
  return this.resolveReference(name.slice(7), context.currentValue, context.responseValue) === action.args;
@@ -4848,7 +4845,11 @@ var ActionRuntime = class {
4848
4845
  return nextValue;
4849
4846
  }
4850
4847
  if (name.startsWith("filter ")) {
4851
- const value = this.resolveReference(name.slice(7), context.currentValue, context.responseValue);
4848
+ const reference = name.slice(7);
4849
+ if (Array.isArray(context.currentValue) && this.isCurrentScopedReference(reference)) {
4850
+ return context.currentValue.filter((entry) => this.resolveReference(reference, entry, context.responseValue));
4851
+ }
4852
+ const value = this.resolveReference(reference, context.currentValue, context.responseValue);
4852
4853
  return value ? inputValue : null;
4853
4854
  }
4854
4855
  if (name === "remove") {
@@ -4909,6 +4910,9 @@ var ActionRuntime = class {
4909
4910
  return inputValue;
4910
4911
  }
4911
4912
  if (name === "map") {
4913
+ if (Array.isArray(context.currentValue)) {
4914
+ return context.currentValue.map((entry) => this.resolveMappedValue(action.args, entry, context.responseValue));
4915
+ }
4912
4916
  return this.resolveMappedValue(action.args, context.currentValue, context.responseValue);
4913
4917
  }
4914
4918
  if (name === "ifelse") {
@@ -4951,6 +4955,9 @@ var ActionRuntime = class {
4951
4955
  }
4952
4956
  return JSON.stringify(value);
4953
4957
  }
4958
+ isCurrentScopedReference(reference) {
4959
+ return reference === "current" || reference.startsWith("current.") || reference.startsWith("this.") || reference === "this";
4960
+ }
4954
4961
  };
4955
4962
  function isIfElseArgs(value) {
4956
4963
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -6324,6 +6331,21 @@ function renderModalOverlay(env, modalId) {
6324
6331
  }
6325
6332
 
6326
6333
  // src/lib/widgets/context-menu.ts
6334
+ var MENU_WIDTH = 220;
6335
+ var MENU_ITEM_HEIGHT = 44;
6336
+ var MENU_PADDING = 8;
6337
+ function getContextMenuItemAtPath(items, path) {
6338
+ let currentItems = items ?? [];
6339
+ let currentItem = null;
6340
+ for (const index of path) {
6341
+ currentItem = currentItems[index] ?? null;
6342
+ if (!currentItem) {
6343
+ return null;
6344
+ }
6345
+ currentItems = currentItem.items ?? [];
6346
+ }
6347
+ return currentItem;
6348
+ }
6327
6349
  function renderContextMenuOverlay(env, menu) {
6328
6350
  const node = env.nodeById.get(menu.id);
6329
6351
  if (!node || node.widget !== "context-menu") {
@@ -6331,8 +6353,52 @@ function renderContextMenuOverlay(env, menu) {
6331
6353
  }
6332
6354
  const state = env.ensureWidgetState(node, menu.id);
6333
6355
  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>`;
6356
+ const openPath = Array.isArray(menu.openPath) ? menu.openPath : [];
6357
+ const layers = [];
6358
+ let items = node.items ?? [];
6359
+ let pathPrefix = [];
6360
+ let layerX = menu.x;
6361
+ let layerY = menu.y;
6362
+ while (items.length > 0) {
6363
+ layers.push(renderMenuLayer(env, menu.id, items, enabled, layerX, layerY, pathPrefix));
6364
+ const nextIndex = openPath[pathPrefix.length];
6365
+ if (typeof nextIndex !== "number") {
6366
+ break;
6367
+ }
6368
+ const nextItem = items[nextIndex];
6369
+ if (!nextItem?.items?.length) {
6370
+ break;
6371
+ }
6372
+ const proposedY = layerY + MENU_PADDING + nextIndex * MENU_ITEM_HEIGHT;
6373
+ const rightX = layerX + MENU_WIDTH - MENU_PADDING;
6374
+ const leftX = layerX - MENU_WIDTH + MENU_PADDING;
6375
+ layerX = rightX + MENU_WIDTH <= window.innerWidth ? rightX : Math.max(8, leftX);
6376
+ layerY = Math.max(8, Math.min(proposedY, window.innerHeight - (16 + nextItem.items.length * MENU_ITEM_HEIGHT) - 8));
6377
+ items = nextItem.items;
6378
+ pathPrefix = [...pathPrefix, nextIndex];
6379
+ }
6380
+ return `<div class="vjt-context-menu-backdrop" data-context-menu-backdrop="${env.escapeHtml(menu.id)}">${layers.join("")}</div>`;
6381
+ }
6382
+ function renderMenuLayer(env, menuId, items, enabled, x, y, pathPrefix) {
6383
+ const content = items.map((item, index) => {
6384
+ const path = [...pathPrefix, index];
6385
+ const pathText = path.join(".");
6386
+ const label = env.escapeHtml(env.resolveI18nValue(item.name));
6387
+ const disabled = enabled ? "" : " disabled";
6388
+ if (item.items?.length) {
6389
+ 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>`;
6390
+ }
6391
+ return `<button class="vjt-context-menu-item" type="button" data-context-menu-item="${env.escapeHtml(menuId)}:${env.escapeHtml(pathText)}"${disabled}>${label}</button>`;
6392
+ }).join("");
6393
+ return `<div class="vjt-context-menu" style="left:${x}px;top:${y}px">${content}</div>`;
6394
+ }
6395
+
6396
+ // src/lib/widgets/confirm-modal.ts
6397
+ function renderConfirmModalOverlay(env, modal) {
6398
+ const caption = env.escapeHtml(env.resolveI18nValue(modal.caption));
6399
+ const yes = env.escapeHtml(env.resolveI18nValue(modal.yes));
6400
+ const no = env.escapeHtml(env.resolveI18nValue(modal.no));
6401
+ 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
6402
  }
6337
6403
 
6338
6404
  // src/lib/widgets/bindings.ts
@@ -6524,7 +6590,7 @@ function bindWidgetEvents(root, env) {
6524
6590
  });
6525
6591
  continue;
6526
6592
  }
6527
- if (element instanceof HTMLAnchorElement && element.dataset.widget === "link" && env.getInlineActions(node)?.length) {
6593
+ if (element instanceof HTMLAnchorElement && element.dataset.widget === "link" && (node.events?.onClick?.length || env.getInlineActions(node)?.length)) {
6528
6594
  element.addEventListener("pointerdown", (event) => {
6529
6595
  env.setLastPointer({ x: event.clientX, y: event.clientY });
6530
6596
  });
@@ -6594,6 +6660,45 @@ function bindDelegatedUi(root, env) {
6594
6660
  window.addEventListener("pointercancel", () => {
6595
6661
  env.stopSplitterDrag();
6596
6662
  });
6663
+ root.addEventListener("pointerover", (event) => {
6664
+ void (async () => {
6665
+ const target = event.target;
6666
+ if (!(target instanceof Element)) {
6667
+ return;
6668
+ }
6669
+ const hoveredItem = target.closest("[data-context-menu-item], [data-context-menu-expand]");
6670
+ if (!(hoveredItem instanceof HTMLButtonElement)) {
6671
+ return;
6672
+ }
6673
+ const descriptor = hoveredItem.dataset.contextMenuExpand ?? hoveredItem.dataset.contextMenuItem;
6674
+ if (!descriptor) {
6675
+ return;
6676
+ }
6677
+ const [menuId, pathText = ""] = descriptor.split(":");
6678
+ const node = env.nodeById.get(menuId);
6679
+ if (node?.widget !== "context-menu") {
6680
+ return;
6681
+ }
6682
+ const activeMenu = env.getActiveContextMenu();
6683
+ if (!activeMenu || activeMenu.id !== menuId) {
6684
+ return;
6685
+ }
6686
+ const path = parseMenuPath(pathText);
6687
+ const item = getContextMenuItemAtPath(node.items, path);
6688
+ if (!item) {
6689
+ return;
6690
+ }
6691
+ const nextOpenPath = item.items?.length ? path : path.slice(0, -1);
6692
+ if (samePath(activeMenu.openPath ?? [], nextOpenPath)) {
6693
+ return;
6694
+ }
6695
+ env.setActiveContextMenu({
6696
+ ...activeMenu,
6697
+ openPath: nextOpenPath
6698
+ });
6699
+ await env.rerenderRoot();
6700
+ })();
6701
+ });
6597
6702
  root.addEventListener("click", (event) => {
6598
6703
  void (async () => {
6599
6704
  const target = event.target;
@@ -6672,7 +6777,7 @@ function bindDelegatedUi(root, env) {
6672
6777
  }
6673
6778
  const menuItem = target.closest("[data-context-menu-item]");
6674
6779
  if (menuItem instanceof HTMLButtonElement) {
6675
- const [menuId, indexValue] = (menuItem.dataset.contextMenuItem ?? "").split(":");
6780
+ const [menuId, pathText = ""] = (menuItem.dataset.contextMenuItem ?? "").split(":");
6676
6781
  const node = env.nodeById.get(menuId);
6677
6782
  if (node?.widget !== "context-menu") {
6678
6783
  return;
@@ -6680,11 +6785,27 @@ function bindDelegatedUi(root, env) {
6680
6785
  if (!env.isWidgetEnabled(node, menuId)) {
6681
6786
  return;
6682
6787
  }
6683
- const item = node.items?.[Number.parseInt(indexValue, 10) || 0];
6788
+ const item = getContextMenuItemAtPath(node.items, parseMenuPath(pathText));
6789
+ const menuContext = env.getActiveContextMenu();
6790
+ const currentValue = menuContext?.id === menuId ? menuContext.currentValue ?? null : null;
6684
6791
  env.setActiveContextMenu(null);
6685
6792
  if (item?.actions?.length) {
6686
- await env.runActions(item.actions, null, { currentValue: null });
6793
+ await env.runActions(item.actions, currentValue, { currentValue });
6794
+ }
6795
+ await env.rerenderRoot();
6796
+ return;
6797
+ }
6798
+ const menuParentItem = target.closest("[data-context-menu-expand]");
6799
+ if (menuParentItem instanceof HTMLButtonElement) {
6800
+ const [menuId, pathText = ""] = (menuParentItem.dataset.contextMenuExpand ?? "").split(":");
6801
+ const activeMenu = env.getActiveContextMenu();
6802
+ if (!activeMenu || activeMenu.id !== menuId) {
6803
+ return;
6687
6804
  }
6805
+ env.setActiveContextMenu({
6806
+ ...activeMenu,
6807
+ openPath: parseMenuPath(pathText)
6808
+ });
6688
6809
  await env.rerenderRoot();
6689
6810
  return;
6690
6811
  }
@@ -6703,10 +6824,32 @@ function bindDelegatedUi(root, env) {
6703
6824
  await env.runActions(item.actions, null, { currentValue: null });
6704
6825
  }
6705
6826
  await env.rerenderRoot();
6827
+ return;
6828
+ }
6829
+ if (target.closest("[data-confirm-yes]")) {
6830
+ env.resolveConfirmModal(true);
6831
+ await env.rerenderRoot();
6832
+ return;
6833
+ }
6834
+ if (target.closest("[data-confirm-no]")) {
6835
+ env.resolveConfirmModal(false);
6836
+ await env.rerenderRoot();
6706
6837
  }
6707
6838
  })();
6708
6839
  });
6709
6840
  }
6841
+ function parseMenuPath(value) {
6842
+ if (!value) {
6843
+ return [];
6844
+ }
6845
+ return value.split(".").map((part) => Number.parseInt(part, 10)).filter((part) => !Number.isNaN(part));
6846
+ }
6847
+ function samePath(left, right) {
6848
+ if (left.length !== right.length) {
6849
+ return false;
6850
+ }
6851
+ return left.every((value, index) => value === right[index]);
6852
+ }
6710
6853
 
6711
6854
  // src/lib/render.ts
6712
6855
  var DEFAULT_DATE_FORMAT = "dd.MM.yyyy";
@@ -6863,6 +7006,7 @@ var RuntimeRenderer = class {
6863
7006
  hasTriggeredInitialRefresh = false;
6864
7007
  activeModalId = null;
6865
7008
  activeContextMenu = null;
7009
+ activeConfirmModal = null;
6866
7010
  lastPointer = { x: 24, y: 24 };
6867
7011
  pendingResetModalIds = /* @__PURE__ */ new Set();
6868
7012
  constructor(description, options = {}) {
@@ -6966,6 +7110,9 @@ var RuntimeRenderer = class {
6966
7110
  focusWidget: (reference) => this.focusWidget(reference),
6967
7111
  playAudio: (value) => this.voiceRuntime.play(value),
6968
7112
  stopPlaying: () => this.voiceRuntime.stopPlaying(),
7113
+ copyToClipboard: (value) => this.copyToClipboard(value),
7114
+ selectFile: (args) => this.selectFile(args),
7115
+ confirmModal: (args, inputValue) => this.confirmModal(args, inputValue),
6969
7116
  startRecording: () => this.voiceRuntime.startRecording(),
6970
7117
  stopRecording: () => Promise.resolve(this.voiceRuntime.stopRecording()),
6971
7118
  startListening: () => this.voiceRuntime.startListening(),
@@ -7124,6 +7271,138 @@ var RuntimeRenderer = class {
7124
7271
  await this.rerenderRoot();
7125
7272
  await this.runSystemEvent("onAfterNavigate");
7126
7273
  }
7274
+ async copyToClipboard(value) {
7275
+ const text = value == null ? "" : typeof value === "string" ? value : JSON.stringify(value) ?? "";
7276
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
7277
+ await navigator.clipboard.writeText(text);
7278
+ return;
7279
+ }
7280
+ if (typeof document === "undefined") {
7281
+ throw new Error("Clipboard API is unavailable");
7282
+ }
7283
+ const textarea = document.createElement("textarea");
7284
+ textarea.value = text;
7285
+ textarea.setAttribute("readonly", "true");
7286
+ textarea.style.position = "fixed";
7287
+ textarea.style.left = "-9999px";
7288
+ textarea.style.top = "0";
7289
+ document.body.append(textarea);
7290
+ textarea.focus();
7291
+ textarea.select();
7292
+ const copied = document.execCommand("copy");
7293
+ textarea.remove();
7294
+ if (!copied) {
7295
+ throw new Error("Failed to copy value to clipboard");
7296
+ }
7297
+ }
7298
+ async selectFile(args) {
7299
+ if (typeof document === "undefined") {
7300
+ throw new Error("File selection is unavailable");
7301
+ }
7302
+ const options = isPlainObject4(args) ? args : {};
7303
+ const input = document.createElement("input");
7304
+ input.type = "file";
7305
+ input.style.position = "fixed";
7306
+ input.style.left = "-9999px";
7307
+ input.style.top = "0";
7308
+ if (typeof options.accept === "string" && options.accept.trim()) {
7309
+ input.accept = options.accept;
7310
+ }
7311
+ input.multiple = options.multiple === true;
7312
+ const selection = new Promise((resolve, reject) => {
7313
+ let settled = false;
7314
+ const finish = (value) => {
7315
+ if (settled) {
7316
+ return;
7317
+ }
7318
+ settled = true;
7319
+ resolve(value);
7320
+ };
7321
+ const fail = (error) => {
7322
+ if (settled) {
7323
+ return;
7324
+ }
7325
+ settled = true;
7326
+ reject(error instanceof Error ? error : new Error(String(error)));
7327
+ };
7328
+ const cleanup = () => {
7329
+ window.removeEventListener("focus", handleFocus, true);
7330
+ input.remove();
7331
+ };
7332
+ const handleFocus = () => {
7333
+ window.setTimeout(() => {
7334
+ if (!settled && (!input.files || input.files.length === 0)) {
7335
+ cleanup();
7336
+ finish(null);
7337
+ }
7338
+ }, 250);
7339
+ };
7340
+ input.addEventListener("change", () => {
7341
+ void (async () => {
7342
+ try {
7343
+ const files = Array.from(input.files ?? []);
7344
+ const converted = await Promise.all(files.map((file) => this.readSelectedFile(file)));
7345
+ cleanup();
7346
+ finish(input.multiple ? converted : converted[0] ?? null);
7347
+ } catch (error) {
7348
+ cleanup();
7349
+ fail(error);
7350
+ }
7351
+ })();
7352
+ }, { once: true });
7353
+ input.addEventListener("cancel", () => {
7354
+ cleanup();
7355
+ finish(null);
7356
+ }, { once: true });
7357
+ window.addEventListener("focus", handleFocus, true);
7358
+ document.body.append(input);
7359
+ input.click();
7360
+ });
7361
+ return selection;
7362
+ }
7363
+ readSelectedFile(file) {
7364
+ return new Promise((resolve, reject) => {
7365
+ const reader = new FileReader();
7366
+ reader.onerror = () => {
7367
+ reject(reader.error ?? new Error(`Failed to read file ${file.name}`));
7368
+ };
7369
+ reader.onload = () => {
7370
+ const result = typeof reader.result === "string" ? reader.result : "";
7371
+ const [, data = ""] = result.split(",", 2);
7372
+ resolve({
7373
+ name: file.name,
7374
+ mimeType: file.type || "application/octet-stream",
7375
+ size: file.size,
7376
+ data
7377
+ });
7378
+ };
7379
+ reader.readAsDataURL(file);
7380
+ });
7381
+ }
7382
+ async confirmModal(args, _inputValue) {
7383
+ const options = isPlainObject4(args) ? args : {};
7384
+ const caption = typeof options.caption === "string" && options.caption ? options.caption : "Confirm?";
7385
+ const yes = typeof options.yes === "string" && options.yes ? options.yes : "Yes";
7386
+ const no = typeof options.no === "string" && options.no ? options.no : "No";
7387
+ return new Promise((resolve) => {
7388
+ this.activeConfirmModal?.resolve(false);
7389
+ this.activeConfirmModal = {
7390
+ caption,
7391
+ yes,
7392
+ no,
7393
+ resolve
7394
+ };
7395
+ void this.rerenderRoot();
7396
+ });
7397
+ }
7398
+ resolveConfirmModal(confirmed) {
7399
+ const activeModal = this.activeConfirmModal;
7400
+ if (!activeModal) {
7401
+ return;
7402
+ }
7403
+ this.activeConfirmModal = null;
7404
+ activeModal.resolve(confirmed);
7405
+ }
7127
7406
  reindexStaticTree() {
7128
7407
  this.nodeByKey.clear();
7129
7408
  this.nodeById.clear();
@@ -7178,7 +7457,11 @@ var RuntimeRenderer = class {
7178
7457
  stateByKey: Array.from(this.stateByKey.entries(), ([key, state]) => [key, deepClone(state)]),
7179
7458
  vars: Array.from(this.vars.entries(), ([name, value]) => [name, deepClone(value)]),
7180
7459
  activeModalId: this.activeModalId,
7181
- activeContextMenu: this.activeContextMenu ? deepClone(this.activeContextMenu) : null
7460
+ activeContextMenu: this.activeContextMenu ? {
7461
+ id: this.activeContextMenu.id,
7462
+ x: this.activeContextMenu.x,
7463
+ y: this.activeContextMenu.y
7464
+ } : null
7182
7465
  };
7183
7466
  }
7184
7467
  indexStaticNodes(node, path) {
@@ -7688,7 +7971,8 @@ var RuntimeRenderer = class {
7688
7971
  const env = this.getWidgetRenderEnvironment();
7689
7972
  const modalMarkup = this.activeModalId ? renderModalOverlay(env, this.activeModalId) : "";
7690
7973
  const menuMarkup = this.activeContextMenu ? renderContextMenuOverlay(env, this.activeContextMenu) : "";
7691
- return `${modalMarkup}${menuMarkup}`;
7974
+ const confirmMarkup = this.activeConfirmModal ? renderConfirmModalOverlay(env, this.activeConfirmModal) : "";
7975
+ return `${modalMarkup}${menuMarkup}${confirmMarkup}`;
7692
7976
  }
7693
7977
  updatePointerFromEvent(event) {
7694
7978
  this.lastPointer = updatePointerFromMouseEvent(event);
@@ -7915,6 +8199,7 @@ var RuntimeRenderer = class {
7915
8199
  nodeByKey: this.nodeByKey,
7916
8200
  stateByKey: this.stateByKey,
7917
8201
  getLastPointer: () => this.lastPointer,
8202
+ getActiveContextMenu: () => this.activeContextMenu,
7918
8203
  setActiveContextMenu: (menu) => {
7919
8204
  this.activeContextMenu = menu;
7920
8205
  },
@@ -7924,6 +8209,7 @@ var RuntimeRenderer = class {
7924
8209
  updateSplitterDrag: (clientPosition) => this.updateSplitterDrag(clientPosition),
7925
8210
  stopSplitterDrag: () => this.stopSplitterDrag(),
7926
8211
  hasSplitterDrag: () => this.splitterDragState !== null,
8212
+ resolveConfirmModal: (confirmed) => this.resolveConfirmModal(confirmed),
7927
8213
  closeModal: (modalId) => this.closeModal(modalId),
7928
8214
  rerenderRoot: () => this.rerenderRoot(),
7929
8215
  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.6",
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
+ }