@vortexm/vjt 0.1.4 → 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
@@ -3904,69 +3904,111 @@ var NetworkRuntime = class {
3904
3904
  if (!definition) {
3905
3905
  return null;
3906
3906
  }
3907
- const payload = await this.buildSchemaValue(definition.request, currentValue, void 0);
3908
- const requestEnvelope = {
3909
- jsonrpc: "2.0",
3910
- id: Date.now(),
3911
- method: requestName,
3912
- params: payload
3913
- };
3914
- const requestUrl = definition.url ?? this.backendUrl;
3915
- if (!requestUrl) {
3916
- throw new Error(`Request ${requestName} has no url in config`);
3917
- }
3918
- const safeRequestUrl = sanitizeUrl(requestUrl, { allowRelative: true });
3919
- if (!safeRequestUrl) {
3920
- throw new Error(`Request ${requestName} has unsafe url`);
3921
- }
3922
- const requestBody = JSON.stringify(requestEnvelope);
3923
- logRuntimeDebug(this.debugLogging, "request", {
3924
- requestName,
3925
- url: safeRequestUrl,
3926
- envelope: requestEnvelope,
3927
- body: requestBody
3928
- });
3929
- const response = await fetch(safeRequestUrl, {
3930
- method: "POST",
3931
- headers: {
3932
- "Content-Type": "application/json"
3933
- },
3934
- body: requestBody
3935
- });
3936
- const responseText = await response.text();
3937
- if (!response.ok) {
3938
- throw new Error(`Request ${requestName} failed: ${response.status} ${response.statusText || responseText}`);
3939
- }
3940
- if (!responseText.trim()) {
3941
- throw new Error(`Request ${requestName} returned empty response body`);
3942
- }
3943
- let json;
3944
3907
  try {
3945
- json = JSON.parse(responseText);
3946
- } catch {
3947
- throw new Error(`Request ${requestName} returned non-JSON response: ${responseText.slice(0, 120)}`);
3948
- }
3949
- if (json.error) {
3950
- throw new Error(`Request ${requestName} returned JSON-RPC error: ${this.stringifyPrimitive(json.error.message ?? "unknown error")}`);
3951
- }
3952
- if (!definition.response) {
3953
- throw new Error(`Request ${requestName} must define response schema`);
3954
- }
3955
- const result = sanitizeSchemaValue(definition.response, json.result, `response.${requestName}`);
3956
- logRuntimeDebug(this.debugLogging, "response", {
3957
- requestName,
3958
- url: safeRequestUrl,
3959
- status: response.status,
3960
- json,
3961
- result
3962
- });
3963
- if (definition.onResponse?.length) {
3964
- await this.runActions(definition.onResponse, null, {
3965
- currentValue: null,
3966
- responseValue: result
3908
+ const payload = await this.buildSchemaValue(definition.request, currentValue, void 0);
3909
+ const requestEnvelope = {
3910
+ jsonrpc: "2.0",
3911
+ id: Date.now(),
3912
+ method: requestName,
3913
+ params: payload
3914
+ };
3915
+ const requestUrl = definition.url ?? this.backendUrl;
3916
+ if (!requestUrl) {
3917
+ throw new Error(`Request ${requestName} has no url in config`);
3918
+ }
3919
+ const safeRequestUrl = sanitizeUrl(requestUrl, { allowRelative: true });
3920
+ if (!safeRequestUrl) {
3921
+ throw new Error(`Request ${requestName} has unsafe url`);
3922
+ }
3923
+ const requestBody = JSON.stringify(requestEnvelope);
3924
+ logRuntimeDebug(this.debugLogging, "request", {
3925
+ requestName,
3926
+ url: safeRequestUrl,
3927
+ envelope: requestEnvelope,
3928
+ body: requestBody
3929
+ });
3930
+ const response = await fetch(safeRequestUrl, {
3931
+ method: "POST",
3932
+ headers: {
3933
+ "Content-Type": "application/json"
3934
+ },
3935
+ body: requestBody
3967
3936
  });
3937
+ const responseText = await response.text();
3938
+ if (!response.ok) {
3939
+ throw this.createRequestError(requestName, {
3940
+ message: `Request ${requestName} failed: ${response.status} ${response.statusText || responseText}`,
3941
+ status: response.status,
3942
+ statusText: response.statusText,
3943
+ body: responseText,
3944
+ url: safeRequestUrl
3945
+ });
3946
+ }
3947
+ if (!responseText.trim()) {
3948
+ throw this.createRequestError(requestName, {
3949
+ message: `Request ${requestName} returned empty response body`,
3950
+ status: response.status,
3951
+ statusText: response.statusText,
3952
+ body: responseText,
3953
+ url: safeRequestUrl
3954
+ });
3955
+ }
3956
+ let json;
3957
+ try {
3958
+ json = JSON.parse(responseText);
3959
+ } catch {
3960
+ throw this.createRequestError(requestName, {
3961
+ message: `Request ${requestName} returned non-JSON response: ${responseText.slice(0, 120)}`,
3962
+ status: response.status,
3963
+ statusText: response.statusText,
3964
+ body: responseText,
3965
+ url: safeRequestUrl
3966
+ });
3967
+ }
3968
+ if (json.error) {
3969
+ throw this.createRequestError(requestName, {
3970
+ message: `Request ${requestName} returned JSON-RPC error: ${this.stringifyPrimitive(json.error.message ?? "unknown error")}`,
3971
+ status: response.status,
3972
+ statusText: response.statusText,
3973
+ body: responseText,
3974
+ url: safeRequestUrl,
3975
+ error: json.error
3976
+ });
3977
+ }
3978
+ if (!definition.response) {
3979
+ throw this.createRequestError(requestName, {
3980
+ message: `Request ${requestName} must define response schema`,
3981
+ status: response.status,
3982
+ statusText: response.statusText,
3983
+ body: responseText,
3984
+ url: safeRequestUrl
3985
+ });
3986
+ }
3987
+ const result = sanitizeSchemaValue(definition.response, json.result, `response.${requestName}`);
3988
+ logRuntimeDebug(this.debugLogging, "response", {
3989
+ requestName,
3990
+ url: safeRequestUrl,
3991
+ status: response.status,
3992
+ json,
3993
+ result
3994
+ });
3995
+ if (definition.onResponse?.length) {
3996
+ await this.runActions(definition.onResponse, null, {
3997
+ currentValue: null,
3998
+ responseValue: result
3999
+ });
4000
+ }
4001
+ return result;
4002
+ } catch (error) {
4003
+ if (definition.onError?.length) {
4004
+ const errorPayload = this.toRequestErrorPayload(requestName, error);
4005
+ await this.runActions(definition.onError, null, {
4006
+ currentValue: null,
4007
+ responseValue: errorPayload
4008
+ });
4009
+ }
4010
+ throw error;
3968
4011
  }
3969
- return result;
3970
4012
  }
3971
4013
  async buildSchemaValue(schema, currentValue, responseValue) {
3972
4014
  if (schema.type === "object") {
@@ -4004,6 +4046,36 @@ var NetworkRuntime = class {
4004
4046
  }
4005
4047
  return JSON.stringify(value);
4006
4048
  }
4049
+ createRequestError(requestName, payload) {
4050
+ const error = new Error(payload.message);
4051
+ error.requestPayload = {
4052
+ requestName,
4053
+ message: payload.message,
4054
+ status: payload.status ?? null,
4055
+ statusText: payload.statusText ?? "",
4056
+ body: payload.body ?? "",
4057
+ url: payload.url ?? "",
4058
+ error: payload.error ?? null
4059
+ };
4060
+ return error;
4061
+ }
4062
+ toRequestErrorPayload(requestName, error) {
4063
+ if (typeof error === "object" && error !== null && "requestPayload" in error) {
4064
+ const payload = error.requestPayload;
4065
+ if (typeof payload === "object" && payload !== null && !Array.isArray(payload)) {
4066
+ return payload;
4067
+ }
4068
+ }
4069
+ return {
4070
+ requestName,
4071
+ message: error instanceof Error ? error.message : this.stringifyPrimitive(error),
4072
+ status: null,
4073
+ statusText: "",
4074
+ body: "",
4075
+ url: "",
4076
+ error: null
4077
+ };
4078
+ }
4007
4079
  coercePrimitiveSchemaValue(type, value) {
4008
4080
  switch (type) {
4009
4081
  case "int": {
@@ -4186,6 +4258,9 @@ var ReferenceRuntime = class {
4186
4258
  if (reference.startsWith("url.")) {
4187
4259
  return getUrlParamValue(reference.slice(4));
4188
4260
  }
4261
+ if (reference.startsWith("vars.")) {
4262
+ return this.host.getVarValue(reference.slice(5));
4263
+ }
4189
4264
  const [widgetId, ...rest] = reference.split(".");
4190
4265
  const state = this.host.stateById.get(widgetId);
4191
4266
  if (!state) {
@@ -4311,6 +4386,10 @@ var ReferenceRuntime = class {
4311
4386
  this.host.setAppValue(reference.slice(4), inputValue);
4312
4387
  return;
4313
4388
  }
4389
+ if (reference.startsWith("vars.")) {
4390
+ this.host.setVarValue(reference.slice(5), inputValue);
4391
+ return;
4392
+ }
4314
4393
  const [widgetId, ...rest] = reference.split(".");
4315
4394
  const field = rest.join(".");
4316
4395
  const state = this.host.stateById.get(widgetId);
@@ -4489,6 +4568,7 @@ var ActionRuntime = class {
4489
4568
  actionFunctions;
4490
4569
  nodeById;
4491
4570
  stateById;
4571
+ stateByKey;
4492
4572
  rerenderRoot;
4493
4573
  dispatchWidgetEventImpl;
4494
4574
  executeRequest;
@@ -4498,6 +4578,17 @@ var ActionRuntime = class {
4498
4578
  resolveMappedValue;
4499
4579
  setWidgetEnabled;
4500
4580
  clearWidget;
4581
+ clearListElementState;
4582
+ focusWidget;
4583
+ playAudio;
4584
+ stopPlaying;
4585
+ copyToClipboard;
4586
+ selectFile;
4587
+ confirmModal;
4588
+ startRecording;
4589
+ stopRecording;
4590
+ startListening;
4591
+ stopListening;
4501
4592
  setUiReference;
4502
4593
  refreshWidgetTree;
4503
4594
  renderWidgetTree;
@@ -4514,6 +4605,7 @@ var ActionRuntime = class {
4514
4605
  this.actionFunctions = options.actionFunctions;
4515
4606
  this.nodeById = options.nodeById;
4516
4607
  this.stateById = options.stateById;
4608
+ this.stateByKey = options.stateByKey;
4517
4609
  this.rerenderRoot = options.rerenderRoot;
4518
4610
  this.dispatchWidgetEventImpl = options.dispatchWidgetEvent;
4519
4611
  this.executeRequest = options.executeRequest;
@@ -4523,6 +4615,17 @@ var ActionRuntime = class {
4523
4615
  this.resolveMappedValue = options.resolveMappedValue;
4524
4616
  this.setWidgetEnabled = options.setWidgetEnabled;
4525
4617
  this.clearWidget = options.clearWidget;
4618
+ this.clearListElementState = options.clearListElementState;
4619
+ this.focusWidget = options.focusWidget;
4620
+ this.playAudio = options.playAudio;
4621
+ this.stopPlaying = options.stopPlaying;
4622
+ this.copyToClipboard = options.copyToClipboard;
4623
+ this.selectFile = options.selectFile;
4624
+ this.confirmModal = options.confirmModal;
4625
+ this.startRecording = options.startRecording;
4626
+ this.stopRecording = options.stopRecording;
4627
+ this.startListening = options.startListening;
4628
+ this.stopListening = options.stopListening;
4526
4629
  this.setUiReference = options.setUiReference;
4527
4630
  this.refreshWidgetTree = options.refreshWidgetTree;
4528
4631
  this.renderWidgetTree = options.renderWidgetTree;
@@ -4557,7 +4660,7 @@ var ActionRuntime = class {
4557
4660
  async runActions(actions, inputValue, context) {
4558
4661
  let current = inputValue;
4559
4662
  for (const action of actions) {
4560
- current = await this.runSingleAction(action, current, context);
4663
+ current = await this.runSingleAction(action, inputValue, context);
4561
4664
  }
4562
4665
  return current;
4563
4666
  }
@@ -4569,19 +4672,7 @@ var ActionRuntime = class {
4569
4672
  try {
4570
4673
  let output = await this.executeAction(action, inputValue, context);
4571
4674
  if (action.andThen?.length) {
4572
- if (Array.isArray(output)) {
4573
- const collected = [];
4574
- for (const entry of output) {
4575
- if (entry === null || entry === void 0) {
4576
- continue;
4577
- }
4578
- const nestedOutput = await this.runActions(action.andThen, entry, { ...context, currentValue: entry });
4579
- if (nestedOutput !== null && nestedOutput !== void 0) {
4580
- collected.push(nestedOutput);
4581
- }
4582
- }
4583
- output = collected;
4584
- } else if (output !== null && output !== void 0) {
4675
+ if (output !== null && output !== void 0) {
4585
4676
  output = await this.runActions(action.andThen, output, { ...context, currentValue: output });
4586
4677
  } else {
4587
4678
  output = null;
@@ -4641,6 +4732,40 @@ var ActionRuntime = class {
4641
4732
  await this.navigateTo(name.slice(9));
4642
4733
  return null;
4643
4734
  }
4735
+ if (name.startsWith("play ")) {
4736
+ await this.playAudio(this.resolveReference(name.slice(5), context.currentValue, context.responseValue));
4737
+ return null;
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
+ }
4749
+ if (name === "stopPlaying") {
4750
+ await this.stopPlaying();
4751
+ return null;
4752
+ }
4753
+ if (name === "startRecording") {
4754
+ await this.startRecording();
4755
+ return null;
4756
+ }
4757
+ if (name === "stopRecording") {
4758
+ await this.stopRecording();
4759
+ return null;
4760
+ }
4761
+ if (name === "startListening") {
4762
+ await this.startListening();
4763
+ return null;
4764
+ }
4765
+ if (name === "stopListening") {
4766
+ await this.stopListening();
4767
+ return null;
4768
+ }
4644
4769
  if (name.startsWith("showMenu ")) {
4645
4770
  const menuId = name.slice(9);
4646
4771
  const menuState = this.stateById.get(menuId);
@@ -4651,7 +4776,9 @@ var ActionRuntime = class {
4651
4776
  this.setActiveContextMenu({
4652
4777
  id: menuId,
4653
4778
  x: position.x,
4654
- y: position.y
4779
+ y: position.y,
4780
+ currentValue: context.currentValue,
4781
+ openPath: []
4655
4782
  });
4656
4783
  return null;
4657
4784
  }
@@ -4680,6 +4807,10 @@ var ActionRuntime = class {
4680
4807
  this.clearWidget(name.slice(6));
4681
4808
  return null;
4682
4809
  }
4810
+ if (name.startsWith("setFocus ")) {
4811
+ this.focusWidget(name.slice(9));
4812
+ return null;
4813
+ }
4683
4814
  if (name.startsWith("setUi ")) {
4684
4815
  const widgetId = name.slice(6);
4685
4816
  const resourceRef = typeof inputValue === "string" ? inputValue : "";
@@ -4689,7 +4820,11 @@ var ActionRuntime = class {
4689
4820
  return inputValue;
4690
4821
  }
4691
4822
  if (name.startsWith("get ")) {
4692
- 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);
4693
4828
  }
4694
4829
  if (name.startsWith("equals ")) {
4695
4830
  return this.resolveReference(name.slice(7), context.currentValue, context.responseValue) === action.args;
@@ -4702,11 +4837,82 @@ var ActionRuntime = class {
4702
4837
  this.assignReference(name.slice(4), inputValue, context.currentValue, args);
4703
4838
  return inputValue;
4704
4839
  }
4840
+ if (name.startsWith("append ")) {
4841
+ const reference = name.slice(7);
4842
+ const currentText = this.resolveReference(reference, context.currentValue, context.responseValue);
4843
+ const nextValue = `${this.stringifyValue(currentText)}${this.stringifyValue(action.args)}`;
4844
+ this.assignReference(reference, nextValue, context.currentValue);
4845
+ return nextValue;
4846
+ }
4705
4847
  if (name.startsWith("filter ")) {
4706
- 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);
4707
4853
  return value ? inputValue : null;
4708
4854
  }
4855
+ if (name === "remove") {
4856
+ const listState = this.getCurrentListState(context.currentValue);
4857
+ if (listState && typeof context.currentValue === "object" && context.currentValue !== null && "index" in context.currentValue) {
4858
+ const index = context.currentValue.index;
4859
+ if (typeof index === "number" && Array.isArray(listState.elements)) {
4860
+ listState.elements = listState.elements.filter((_, elementIndex) => elementIndex !== index);
4861
+ this.clearListElementState(listState.key);
4862
+ return listState.elements;
4863
+ }
4864
+ }
4865
+ return null;
4866
+ }
4867
+ if (name.startsWith("add ")) {
4868
+ const valueToAdd = this.cloneValue(this.unwrapListValue(this.resolveReference(name.slice(4), context.currentValue, context.responseValue)));
4869
+ const listState = this.getCurrentListState(context.currentValue);
4870
+ if (listState && Array.isArray(listState.elements)) {
4871
+ listState.elements.push(valueToAdd);
4872
+ this.clearListElementState(listState.key);
4873
+ return listState.elements;
4874
+ }
4875
+ if (context.currentList && typeof context.currentIndex === "number") {
4876
+ return context.currentIndex === context.currentList.length - 1 ? [this.unwrapListValue(inputValue), valueToAdd] : this.unwrapListValue(inputValue);
4877
+ }
4878
+ if (Array.isArray(inputValue)) {
4879
+ const nextList = [];
4880
+ for (const item of inputValue) {
4881
+ nextList.push(item);
4882
+ }
4883
+ nextList.push(valueToAdd);
4884
+ return nextList;
4885
+ }
4886
+ return inputValue;
4887
+ }
4888
+ if (name.startsWith("insert ")) {
4889
+ const valueToInsert = this.cloneValue(this.unwrapListValue(this.resolveReference(name.slice(7), context.currentValue, context.responseValue)));
4890
+ const listState = this.getCurrentListState(context.currentValue);
4891
+ if (listState && Array.isArray(listState.elements) && typeof context.currentValue === "object" && context.currentValue !== null && "index" in context.currentValue) {
4892
+ const index = context.currentValue.index;
4893
+ if (typeof index === "number") {
4894
+ listState.elements.splice(index + 1, 0, valueToInsert);
4895
+ this.clearListElementState(listState.key);
4896
+ return listState.elements;
4897
+ }
4898
+ }
4899
+ if (context.currentList && typeof context.currentIndex === "number") {
4900
+ return [this.unwrapListValue(inputValue), valueToInsert];
4901
+ }
4902
+ if (Array.isArray(inputValue)) {
4903
+ const nextList = [];
4904
+ for (const item of inputValue) {
4905
+ nextList.push(item);
4906
+ }
4907
+ nextList.push(valueToInsert);
4908
+ return nextList;
4909
+ }
4910
+ return inputValue;
4911
+ }
4709
4912
  if (name === "map") {
4913
+ if (Array.isArray(context.currentValue)) {
4914
+ return context.currentValue.map((entry) => this.resolveMappedValue(action.args, entry, context.responseValue));
4915
+ }
4710
4916
  return this.resolveMappedValue(action.args, context.currentValue, context.responseValue);
4711
4917
  }
4712
4918
  if (name === "ifelse") {
@@ -4722,10 +4928,43 @@ var ActionRuntime = class {
4722
4928
  }
4723
4929
  return inputValue;
4724
4930
  }
4931
+ unwrapListValue(value) {
4932
+ return isListElementLike(value) ? value.descriptor : value;
4933
+ }
4934
+ getCurrentListState(currentValue) {
4935
+ if (!isListElementLike(currentValue) || typeof currentValue.listId !== "string") {
4936
+ return null;
4937
+ }
4938
+ return this.stateByKey.get(currentValue.listId) ?? this.stateById.get(currentValue.listId) ?? null;
4939
+ }
4940
+ cloneValue(value) {
4941
+ if (value === null || value === void 0) {
4942
+ return value;
4943
+ }
4944
+ if (typeof value === "object") {
4945
+ return structuredClone(value);
4946
+ }
4947
+ return value;
4948
+ }
4949
+ stringifyValue(value) {
4950
+ if (value == null) {
4951
+ return "";
4952
+ }
4953
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
4954
+ return String(value);
4955
+ }
4956
+ return JSON.stringify(value);
4957
+ }
4958
+ isCurrentScopedReference(reference) {
4959
+ return reference === "current" || reference.startsWith("current.") || reference.startsWith("this.") || reference === "this";
4960
+ }
4725
4961
  };
4726
4962
  function isIfElseArgs(value) {
4727
4963
  return typeof value === "object" && value !== null && !Array.isArray(value);
4728
4964
  }
4965
+ function isListElementLike(value) {
4966
+ return typeof value === "object" && value !== null && "kind" in value && value.kind === "list-element" && "listId" in value && typeof value.listId === "string" && "index" in value && typeof value.index === "number" && "descriptor" in value;
4967
+ }
4729
4968
 
4730
4969
  // src/lib/dom-state.ts
4731
4970
  function isPlainObject3(value) {
@@ -4754,7 +4993,7 @@ function isElementInsidePendingResetModal(element, pendingResetModalIds) {
4754
4993
  }
4755
4994
  return false;
4756
4995
  }
4757
- function captureElementState(root, pendingResetModalIds, stateByKey) {
4996
+ function captureElementState(root, pendingResetModalIds) {
4758
4997
  const state = {
4759
4998
  activeId: null,
4760
4999
  values: /* @__PURE__ */ new Map(),
@@ -4782,20 +5021,6 @@ function captureElementState(root, pendingResetModalIds, stateByKey) {
4782
5021
  if (element instanceof HTMLInputElement && element.type === "checkbox" && element.id) {
4783
5022
  state.checked.set(element.id, element.checked);
4784
5023
  }
4785
- const widgetKey = element.dataset.widgetKey;
4786
- if (!widgetKey) {
4787
- continue;
4788
- }
4789
- const widgetState = stateByKey.get(widgetKey);
4790
- if (!widgetState || element.readOnly || element.disabled) {
4791
- continue;
4792
- }
4793
- if (widgetState.widget === "edit" || widgetState.widget === "textarea") {
4794
- widgetState.text = element.value;
4795
- }
4796
- if (widgetState.widget === "checkbox" && element instanceof HTMLInputElement && element.type === "checkbox") {
4797
- widgetState.checked = element.checked;
4798
- }
4799
5024
  }
4800
5025
  for (const element of Array.from(root.querySelectorAll("[id], [data-widget-key]"))) {
4801
5026
  const top = element.scrollTop;
@@ -4817,16 +5042,44 @@ function captureElementState(root, pendingResetModalIds, stateByKey) {
4817
5042
  }
4818
5043
  return state;
4819
5044
  }
4820
- function restoreElementState(root, state) {
5045
+ function shouldRestoreTextValue(element, preservedValue, stateByKey) {
5046
+ const widgetKey = element.dataset.widgetKey;
5047
+ if (!widgetKey) {
5048
+ return true;
5049
+ }
5050
+ const widgetState = stateByKey.get(widgetKey);
5051
+ if (!widgetState || widgetState.widget !== "edit" && widgetState.widget !== "textarea") {
5052
+ return true;
5053
+ }
5054
+ return (widgetState.text ?? "") === preservedValue;
5055
+ }
5056
+ function shouldRestoreCheckedValue(element, preservedValue, stateByKey) {
5057
+ const widgetKey = element.dataset.widgetKey;
5058
+ if (!widgetKey) {
5059
+ return true;
5060
+ }
5061
+ const widgetState = stateByKey.get(widgetKey);
5062
+ if (!widgetState || widgetState.widget !== "checkbox") {
5063
+ return true;
5064
+ }
5065
+ return (widgetState.checked ?? false) === preservedValue;
5066
+ }
5067
+ function restoreElementState(root, state, stateByKey) {
4821
5068
  for (const [id, value] of state.values.entries()) {
4822
5069
  const element = root.querySelector(`#${CSS.escape(id)}`);
4823
5070
  if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
5071
+ if (!shouldRestoreTextValue(element, value, stateByKey)) {
5072
+ continue;
5073
+ }
4824
5074
  element.value = value;
4825
5075
  }
4826
5076
  }
4827
5077
  for (const [id, checked] of state.checked.entries()) {
4828
5078
  const element = root.querySelector(`#${CSS.escape(id)}`);
4829
5079
  if (element instanceof HTMLInputElement && element.type === "checkbox") {
5080
+ if (!shouldRestoreCheckedValue(element, checked, stateByKey)) {
5081
+ continue;
5082
+ }
4830
5083
  element.checked = checked;
4831
5084
  }
4832
5085
  }
@@ -5129,6 +5382,430 @@ var DEFAULT_STYLE_MAP = {
5129
5382
  employeeLinkDark: "display:flex; align-items:center; width:100%; height:100%; color:#b8d0ea; font-weight:600;"
5130
5383
  };
5131
5384
 
5385
+ // src/lib/voice-runtime.ts
5386
+ var MAX_CONCURRENT_PLAYERS = 5;
5387
+ var MAX_RECORDING_MS = 10 * 60 * 1e3;
5388
+ var MAX_LISTENING_SILENCE_MS = 60 * 60 * 1e3;
5389
+ var SILENCE_STOP_MS = 2e3;
5390
+ var SPEECH_THRESHOLD = 0.035;
5391
+ var RECORDING_MIME_CANDIDATES = [
5392
+ "audio/webm;codecs=opus",
5393
+ "audio/webm",
5394
+ "audio/mp4",
5395
+ "audio/mp4;codecs=mp4a.40.2",
5396
+ "audio/wav"
5397
+ ];
5398
+ function isPlayableAudioPayload(value) {
5399
+ return typeof value === "object" && value !== null;
5400
+ }
5401
+ function decodeBase64(base64) {
5402
+ const normalized = base64.includes(",") ? base64.slice(base64.indexOf(",") + 1) : base64;
5403
+ const binary = atob(normalized);
5404
+ const bytes = new Uint8Array(binary.length);
5405
+ for (let index = 0; index < binary.length; index += 1) {
5406
+ bytes[index] = binary.charCodeAt(index);
5407
+ }
5408
+ return bytes;
5409
+ }
5410
+ function detectMimeType(bytes) {
5411
+ if (bytes.length >= 4 && bytes[0] === 26 && bytes[1] === 69 && bytes[2] === 223 && bytes[3] === 163) {
5412
+ return "audio/webm";
5413
+ }
5414
+ if (bytes.length >= 12 && bytes[4] === 102 && bytes[5] === 116 && bytes[6] === 121 && bytes[7] === 112) {
5415
+ return "audio/mp4";
5416
+ }
5417
+ if (bytes.length >= 3 && bytes[0] === 73 && bytes[1] === 68 && bytes[2] === 51) {
5418
+ return "audio/mpeg";
5419
+ }
5420
+ if (bytes.length >= 2 && bytes[0] === 255 && (bytes[1] & 224) === 224) {
5421
+ return "audio/mpeg";
5422
+ }
5423
+ if (bytes.length >= 12 && bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 65 && bytes[10] === 86 && bytes[11] === 69) {
5424
+ return "audio/wav";
5425
+ }
5426
+ if (bytes.length >= 4 && bytes[0] === 79 && bytes[1] === 103 && bytes[2] === 103 && bytes[3] === 83) {
5427
+ return "audio/ogg";
5428
+ }
5429
+ return "audio/mpeg";
5430
+ }
5431
+ function blobToBase64(blob) {
5432
+ return new Promise((resolve, reject) => {
5433
+ const reader = new FileReader();
5434
+ reader.onerror = () => reject(reader.error ?? new Error("Failed to read recorded audio data"));
5435
+ reader.onloadend = () => {
5436
+ const result = typeof reader.result === "string" ? reader.result : "";
5437
+ const base64 = result.includes(",") ? result.slice(result.indexOf(",") + 1) : result;
5438
+ resolve(base64);
5439
+ };
5440
+ reader.readAsDataURL(blob);
5441
+ });
5442
+ }
5443
+ var VoiceRuntime = class {
5444
+ debugLogging;
5445
+ triggerSystemEvent;
5446
+ activePlayers = [];
5447
+ mediaStream = null;
5448
+ mediaRecorder = null;
5449
+ recordingChunks = [];
5450
+ recordingTimeoutId = null;
5451
+ listening = false;
5452
+ listenContext = null;
5453
+ listenAnalyser = null;
5454
+ listenSource = null;
5455
+ listenFrameId = null;
5456
+ lastSpeechAt = 0;
5457
+ listeningStartedAt = 0;
5458
+ recordingStartedFromListening = false;
5459
+ speechEventTriggered = false;
5460
+ visibilityHandler;
5461
+ pageHideHandler;
5462
+ constructor(options) {
5463
+ this.debugLogging = options.debugLogging;
5464
+ this.triggerSystemEvent = options.triggerSystemEvent;
5465
+ this.visibilityHandler = () => {
5466
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
5467
+ void this.handlePageDeactivation();
5468
+ }
5469
+ };
5470
+ this.pageHideHandler = () => {
5471
+ void this.handlePageDeactivation();
5472
+ };
5473
+ if (typeof document !== "undefined") {
5474
+ document.addEventListener("visibilitychange", this.visibilityHandler);
5475
+ }
5476
+ if (typeof window !== "undefined") {
5477
+ window.addEventListener("pagehide", this.pageHideHandler);
5478
+ }
5479
+ }
5480
+ async play(value) {
5481
+ const payload = this.normalizePlayablePayload(value);
5482
+ if (!payload) {
5483
+ return;
5484
+ }
5485
+ const bytes = decodeBase64(payload.audioData);
5486
+ const copy = new Uint8Array(bytes.byteLength);
5487
+ copy.set(bytes);
5488
+ const blob = new Blob([copy.buffer], { type: payload.mimeType });
5489
+ const url = URL.createObjectURL(blob);
5490
+ const audio = new Audio(url);
5491
+ audio.preload = "auto";
5492
+ const player = { audio, url, payload };
5493
+ audio.onended = () => {
5494
+ void this.finishPlayback(player, "finished");
5495
+ };
5496
+ audio.onerror = () => {
5497
+ this.detachPlayer(player);
5498
+ logRuntimeError("voice.play", new Error("Audio playback failed"), { mimeType: payload.mimeType });
5499
+ };
5500
+ this.activePlayers.push(player);
5501
+ while (this.activePlayers.length > MAX_CONCURRENT_PLAYERS) {
5502
+ const oldest = this.activePlayers.shift();
5503
+ if (!oldest) {
5504
+ break;
5505
+ }
5506
+ oldest.audio.pause();
5507
+ void this.finishPlayback(oldest, "stopped");
5508
+ }
5509
+ logRuntimeDebug(this.debugLogging, "voice-play", {
5510
+ mimeType: payload.mimeType,
5511
+ bytes: bytes.byteLength,
5512
+ activePlayers: this.activePlayers.length
5513
+ });
5514
+ await audio.play();
5515
+ }
5516
+ async stopPlaying() {
5517
+ const players = [...this.activePlayers];
5518
+ for (const player of players) {
5519
+ player.audio.pause();
5520
+ player.audio.currentTime = 0;
5521
+ await this.finishPlayback(player, "stopped");
5522
+ }
5523
+ }
5524
+ async startRecording() {
5525
+ try {
5526
+ const stream = await this.ensureInputStream();
5527
+ this.startRecordingInternal(stream, false);
5528
+ } catch (error) {
5529
+ await this.handleRecordingError(error);
5530
+ }
5531
+ }
5532
+ stopRecording() {
5533
+ if (!this.mediaRecorder || this.mediaRecorder.state === "inactive") {
5534
+ return;
5535
+ }
5536
+ this.clearRecordingTimeout();
5537
+ this.mediaRecorder.stop();
5538
+ }
5539
+ async startListening() {
5540
+ if (this.listening) {
5541
+ return;
5542
+ }
5543
+ try {
5544
+ const stream = await this.ensureInputStream();
5545
+ const context = new AudioContext();
5546
+ const analyser = context.createAnalyser();
5547
+ analyser.fftSize = 2048;
5548
+ const source = context.createMediaStreamSource(stream);
5549
+ source.connect(analyser);
5550
+ this.listening = true;
5551
+ this.listenContext = context;
5552
+ this.listenAnalyser = analyser;
5553
+ this.listenSource = source;
5554
+ this.lastSpeechAt = 0;
5555
+ this.listeningStartedAt = performance.now();
5556
+ this.speechEventTriggered = false;
5557
+ const sampleBuffer = new Uint8Array(analyser.fftSize);
5558
+ const step = async () => {
5559
+ if (!this.listening || !this.listenAnalyser) {
5560
+ return;
5561
+ }
5562
+ this.listenAnalyser.getByteTimeDomainData(sampleBuffer);
5563
+ let squareSum = 0;
5564
+ for (const sample of sampleBuffer) {
5565
+ const centered = (sample - 128) / 128;
5566
+ squareSum += centered * centered;
5567
+ }
5568
+ const rms = Math.sqrt(squareSum / sampleBuffer.length);
5569
+ const now = performance.now();
5570
+ if (rms >= SPEECH_THRESHOLD) {
5571
+ this.lastSpeechAt = now;
5572
+ this.listeningStartedAt = now;
5573
+ if (!this.speechEventTriggered) {
5574
+ this.speechEventTriggered = true;
5575
+ await this.triggerSystemEvent("onSpeechDetected", null);
5576
+ }
5577
+ if (!this.mediaRecorder || this.mediaRecorder.state === "inactive") {
5578
+ this.startRecordingInternal(stream, true);
5579
+ }
5580
+ } else if (this.recordingStartedFromListening && this.mediaRecorder && this.mediaRecorder.state !== "inactive" && this.lastSpeechAt > 0 && now - this.lastSpeechAt >= SILENCE_STOP_MS) {
5581
+ this.stopRecording();
5582
+ this.lastSpeechAt = 0;
5583
+ this.speechEventTriggered = false;
5584
+ } else if (!this.recordingStartedFromListening && now - this.listeningStartedAt >= MAX_LISTENING_SILENCE_MS) {
5585
+ await this.stopListening();
5586
+ return;
5587
+ }
5588
+ this.listenFrameId = window.requestAnimationFrame(() => {
5589
+ void step();
5590
+ });
5591
+ };
5592
+ void step();
5593
+ } catch (error) {
5594
+ await this.handleListeningError(error);
5595
+ }
5596
+ }
5597
+ async stopListening() {
5598
+ this.listening = false;
5599
+ this.speechEventTriggered = false;
5600
+ this.lastSpeechAt = 0;
5601
+ this.listeningStartedAt = 0;
5602
+ if (this.listenFrameId !== null) {
5603
+ window.cancelAnimationFrame(this.listenFrameId);
5604
+ this.listenFrameId = null;
5605
+ }
5606
+ try {
5607
+ await this.listenContext?.close();
5608
+ } catch (error) {
5609
+ logRuntimeError("voice.stopListening", error);
5610
+ }
5611
+ this.listenContext = null;
5612
+ this.listenAnalyser = null;
5613
+ this.listenSource = null;
5614
+ if (this.recordingStartedFromListening && this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
5615
+ this.stopRecording();
5616
+ }
5617
+ this.recordingStartedFromListening = false;
5618
+ this.releaseInputStreamIfIdle();
5619
+ }
5620
+ dispose() {
5621
+ void this.stopListening();
5622
+ this.stopRecording();
5623
+ this.clearRecordingTimeout();
5624
+ void this.stopPlaying();
5625
+ if (typeof document !== "undefined") {
5626
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
5627
+ }
5628
+ if (typeof window !== "undefined") {
5629
+ window.removeEventListener("pagehide", this.pageHideHandler);
5630
+ }
5631
+ this.stopStreamTracks();
5632
+ }
5633
+ startRecordingInternal(stream, fromListening) {
5634
+ if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
5635
+ return;
5636
+ }
5637
+ if (typeof MediaRecorder === "undefined") {
5638
+ throw new Error("MediaRecorder is not supported in this browser");
5639
+ }
5640
+ const preferredMimeType = this.getPreferredRecordingMimeType();
5641
+ const options = preferredMimeType ? { mimeType: preferredMimeType } : void 0;
5642
+ const recorder = options ? new MediaRecorder(stream, options) : new MediaRecorder(stream);
5643
+ this.mediaRecorder = recorder;
5644
+ this.recordingChunks = [];
5645
+ this.recordingStartedFromListening = fromListening;
5646
+ recorder.ondataavailable = (event) => {
5647
+ if (event.data && event.data.size > 0) {
5648
+ this.recordingChunks.push(event.data);
5649
+ }
5650
+ };
5651
+ recorder.onerror = (event) => {
5652
+ void this.handleRecordingError(event.error ?? new Error("Unknown recording error"));
5653
+ };
5654
+ recorder.onstart = () => {
5655
+ logRuntimeDebug(this.debugLogging, "voice-recording-started", {
5656
+ mimeType: recorder.mimeType || preferredMimeType || "unknown",
5657
+ fromListening
5658
+ });
5659
+ void this.triggerSystemEvent("onRecordingStarted", null);
5660
+ };
5661
+ recorder.onstop = () => {
5662
+ void this.handleRecorderStop(recorder);
5663
+ };
5664
+ recorder.start();
5665
+ this.clearRecordingTimeout();
5666
+ this.recordingTimeoutId = window.setTimeout(() => {
5667
+ void this.stopRecording();
5668
+ }, MAX_RECORDING_MS);
5669
+ }
5670
+ async handleRecorderStop(recorder) {
5671
+ this.clearRecordingTimeout();
5672
+ const mimeType = recorder.mimeType || this.getPreferredRecordingMimeType() || "audio/webm";
5673
+ const blob = new Blob(this.recordingChunks, { type: mimeType });
5674
+ this.recordingChunks = [];
5675
+ this.mediaRecorder = null;
5676
+ try {
5677
+ const audioData = await blobToBase64(blob);
5678
+ logRuntimeDebug(this.debugLogging, "voice-recording-stopped", {
5679
+ mimeType,
5680
+ size: blob.size,
5681
+ fromListening: this.recordingStartedFromListening
5682
+ });
5683
+ await this.triggerSystemEvent("onRecordingStopped", {
5684
+ audioData,
5685
+ mimeType
5686
+ });
5687
+ } catch (error) {
5688
+ await this.handleRecordingError(error);
5689
+ } finally {
5690
+ this.recordingStartedFromListening = false;
5691
+ this.releaseInputStreamIfIdle();
5692
+ }
5693
+ }
5694
+ async handleRecordingError(error) {
5695
+ logRuntimeError("voice.recording", error);
5696
+ await this.triggerSystemEvent("onRecordingError", this.normalizeErrorMessage(error));
5697
+ }
5698
+ async handleListeningError(error) {
5699
+ logRuntimeError("voice.listening", error);
5700
+ const message = this.normalizeErrorMessage(error);
5701
+ await this.triggerSystemEvent("onListeningError", message);
5702
+ await this.triggerSystemEvent("onListeringError", message);
5703
+ }
5704
+ normalizePlayablePayload(value) {
5705
+ if (typeof value === "string" && value.trim()) {
5706
+ const bytes = decodeBase64(value.trim());
5707
+ return {
5708
+ audioData: value.trim(),
5709
+ mimeType: detectMimeType(bytes)
5710
+ };
5711
+ }
5712
+ if (isPlayableAudioPayload(value) && typeof value.audioData === "string" && value.audioData.trim()) {
5713
+ const audioData = value.audioData.trim();
5714
+ const mimeType = typeof value.mimeType === "string" && value.mimeType.trim() ? value.mimeType.trim() : detectMimeType(decodeBase64(audioData));
5715
+ return { audioData, mimeType };
5716
+ }
5717
+ return null;
5718
+ }
5719
+ getPreferredRecordingMimeType() {
5720
+ if (typeof MediaRecorder === "undefined" || typeof MediaRecorder.isTypeSupported !== "function") {
5721
+ return null;
5722
+ }
5723
+ for (const candidate of RECORDING_MIME_CANDIDATES) {
5724
+ if (MediaRecorder.isTypeSupported(candidate)) {
5725
+ return candidate;
5726
+ }
5727
+ }
5728
+ return null;
5729
+ }
5730
+ async ensureInputStream() {
5731
+ if (this.mediaStream && this.mediaStream.active) {
5732
+ return this.mediaStream;
5733
+ }
5734
+ if (!navigator.mediaDevices?.getUserMedia) {
5735
+ throw new Error("Audio input is not supported in this browser");
5736
+ }
5737
+ this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
5738
+ return this.mediaStream;
5739
+ }
5740
+ clearRecordingTimeout() {
5741
+ if (this.recordingTimeoutId !== null) {
5742
+ window.clearTimeout(this.recordingTimeoutId);
5743
+ this.recordingTimeoutId = null;
5744
+ }
5745
+ }
5746
+ releaseInputStreamIfIdle() {
5747
+ if (this.listening) {
5748
+ return;
5749
+ }
5750
+ if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
5751
+ return;
5752
+ }
5753
+ this.stopStreamTracks();
5754
+ }
5755
+ stopStreamTracks() {
5756
+ if (!this.mediaStream) {
5757
+ return;
5758
+ }
5759
+ for (const track of this.mediaStream.getTracks()) {
5760
+ track.stop();
5761
+ }
5762
+ this.mediaStream = null;
5763
+ }
5764
+ normalizeErrorMessage(error) {
5765
+ if (error instanceof Error) {
5766
+ return error.message;
5767
+ }
5768
+ return String(error);
5769
+ }
5770
+ detachPlayer(player) {
5771
+ const index = this.activePlayers.indexOf(player);
5772
+ if (index >= 0) {
5773
+ this.activePlayers.splice(index, 1);
5774
+ }
5775
+ player.audio.onended = null;
5776
+ player.audio.onerror = null;
5777
+ URL.revokeObjectURL(player.url);
5778
+ }
5779
+ async finishPlayback(player, reason) {
5780
+ if (!this.activePlayers.includes(player)) {
5781
+ return;
5782
+ }
5783
+ const payload = {
5784
+ audioData: player.payload.audioData,
5785
+ mimeType: player.payload.mimeType
5786
+ };
5787
+ this.detachPlayer(player);
5788
+ await this.triggerSystemEvent(reason === "finished" ? "onPlayFinished" : "onPlayingStopped", payload);
5789
+ }
5790
+ async handlePageDeactivation() {
5791
+ const hadActiveRecording = Boolean(this.mediaRecorder && this.mediaRecorder.state !== "inactive");
5792
+ const hadActiveListening = this.listening;
5793
+ if (!hadActiveRecording && !hadActiveListening) {
5794
+ return;
5795
+ }
5796
+ if (hadActiveRecording) {
5797
+ await this.handleRecordingError(new Error("Recording stopped because the browser tab became inactive"));
5798
+ this.stopRecording();
5799
+ }
5800
+ if (hadActiveListening) {
5801
+ await this.stopListening();
5802
+ if (!hadActiveRecording) {
5803
+ await this.handleListeningError(new Error("Listening stopped because the browser tab became inactive"));
5804
+ }
5805
+ }
5806
+ }
5807
+ };
5808
+
5132
5809
  // src/lib/widgets/adaptive-layout.ts
5133
5810
  var renderAdaptiveLayout = (env, node, _state, key, path, itemContext) => {
5134
5811
  const mobile = env.isMobileViewport();
@@ -5587,7 +6264,7 @@ var renderOverlayContainer = (env, node, _state, key, path, itemContext) => {
5587
6264
  var renderTabs = (env, node, state, key, path, itemContext) => {
5588
6265
  const tabs = node.tabs ?? [];
5589
6266
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
5590
- const rawActiveTab = typeof node.activeTab === "number" ? node.activeTab : state.activeTab ?? 0;
6267
+ const rawActiveTab = typeof state.activeTab === "number" ? state.activeTab : typeof node.activeTab === "number" ? node.activeTab : 0;
5591
6268
  const activeTab = Math.max(0, Math.min(rawActiveTab, Math.max(tabs.length - 1, 0)));
5592
6269
  const headers = tabs.map((tab, index) => `<button class="vjt-tab-button${index === activeTab ? " is-active" : ""}" type="button" role="tab" aria-selected="${index === activeTab ? "true" : "false"}" data-widget="tabs" data-widget-key="${env.escapeHtml(key)}" data-tab-index="${index}"${enabled ? "" : " disabled"}>${env.escapeHtml(env.resolveI18nValue(tab.name))}</button>`).join("");
5593
6270
  const content = tabs[activeTab]?.content ? env.renderNode(tabs[activeTab].content, `${path}.tabs.${activeTab}`, itemContext) : "";
@@ -5654,6 +6331,21 @@ function renderModalOverlay(env, modalId) {
5654
6331
  }
5655
6332
 
5656
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
+ }
5657
6349
  function renderContextMenuOverlay(env, menu) {
5658
6350
  const node = env.nodeById.get(menu.id);
5659
6351
  if (!node || node.widget !== "context-menu") {
@@ -5661,8 +6353,52 @@ function renderContextMenuOverlay(env, menu) {
5661
6353
  }
5662
6354
  const state = env.ensureWidgetState(node, menu.id);
5663
6355
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
5664
- 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("");
5665
- 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>`;
5666
6402
  }
5667
6403
 
5668
6404
  // src/lib/widgets/bindings.ts
@@ -5854,7 +6590,7 @@ function bindWidgetEvents(root, env) {
5854
6590
  });
5855
6591
  continue;
5856
6592
  }
5857
- 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)) {
5858
6594
  element.addEventListener("pointerdown", (event) => {
5859
6595
  env.setLastPointer({ x: event.clientX, y: event.clientY });
5860
6596
  });
@@ -5924,6 +6660,45 @@ function bindDelegatedUi(root, env) {
5924
6660
  window.addEventListener("pointercancel", () => {
5925
6661
  env.stopSplitterDrag();
5926
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
+ });
5927
6702
  root.addEventListener("click", (event) => {
5928
6703
  void (async () => {
5929
6704
  const target = event.target;
@@ -5945,8 +6720,6 @@ function bindDelegatedUi(root, env) {
5945
6720
  const state = env.stateByKey.get(key);
5946
6721
  if (state) {
5947
6722
  state.activeTab = Number.parseInt(tabButton.dataset.tabIndex ?? "0", 10) || 0;
5948
- const tabsNode = node;
5949
- tabsNode.activeTab = state.activeTab;
5950
6723
  }
5951
6724
  await env.rerenderRoot();
5952
6725
  return;
@@ -6004,7 +6777,7 @@ function bindDelegatedUi(root, env) {
6004
6777
  }
6005
6778
  const menuItem = target.closest("[data-context-menu-item]");
6006
6779
  if (menuItem instanceof HTMLButtonElement) {
6007
- const [menuId, indexValue] = (menuItem.dataset.contextMenuItem ?? "").split(":");
6780
+ const [menuId, pathText = ""] = (menuItem.dataset.contextMenuItem ?? "").split(":");
6008
6781
  const node = env.nodeById.get(menuId);
6009
6782
  if (node?.widget !== "context-menu") {
6010
6783
  return;
@@ -6012,14 +6785,30 @@ function bindDelegatedUi(root, env) {
6012
6785
  if (!env.isWidgetEnabled(node, menuId)) {
6013
6786
  return;
6014
6787
  }
6015
- 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;
6016
6791
  env.setActiveContextMenu(null);
6017
6792
  if (item?.actions?.length) {
6018
- await env.runActions(item.actions, null, { currentValue: null });
6793
+ await env.runActions(item.actions, currentValue, { currentValue });
6019
6794
  }
6020
6795
  await env.rerenderRoot();
6021
6796
  return;
6022
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;
6804
+ }
6805
+ env.setActiveContextMenu({
6806
+ ...activeMenu,
6807
+ openPath: parseMenuPath(pathText)
6808
+ });
6809
+ await env.rerenderRoot();
6810
+ return;
6811
+ }
6023
6812
  const modalButton = target.closest("[data-modal-button]");
6024
6813
  if (modalButton instanceof HTMLButtonElement) {
6025
6814
  const [modalId, indexValue] = (modalButton.dataset.modalButton ?? "").split(":");
@@ -6035,10 +6824,32 @@ function bindDelegatedUi(root, env) {
6035
6824
  await env.runActions(item.actions, null, { currentValue: null });
6036
6825
  }
6037
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();
6038
6837
  }
6039
6838
  })();
6040
6839
  });
6041
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
+ }
6042
6853
 
6043
6854
  // src/lib/render.ts
6044
6855
  var DEFAULT_DATE_FORMAT = "dd.MM.yyyy";
@@ -6183,16 +6994,19 @@ var RuntimeRenderer = class {
6183
6994
  stateByKey = /* @__PURE__ */ new Map();
6184
6995
  nodeById = /* @__PURE__ */ new Map();
6185
6996
  stateById = /* @__PURE__ */ new Map();
6997
+ vars = /* @__PURE__ */ new Map();
6186
6998
  resizeHandler = null;
6187
6999
  pointerHandler = null;
6188
7000
  eventSources = [];
6189
7001
  referenceRuntime;
6190
7002
  networkRuntime;
6191
7003
  actionRuntime;
7004
+ voiceRuntime;
6192
7005
  splitterDragState = null;
6193
7006
  hasTriggeredInitialRefresh = false;
6194
7007
  activeModalId = null;
6195
7008
  activeContextMenu = null;
7009
+ activeConfirmModal = null;
6196
7010
  lastPointer = { x: 24, y: 24 };
6197
7011
  pendingResetModalIds = /* @__PURE__ */ new Set();
6198
7012
  constructor(description, options = {}) {
@@ -6255,6 +7069,10 @@ var RuntimeRenderer = class {
6255
7069
  break;
6256
7070
  }
6257
7071
  },
7072
+ getVarValue: (name) => this.vars.get(name),
7073
+ setVarValue: (name, value) => {
7074
+ this.vars.set(name, value);
7075
+ },
6258
7076
  ensureWidgetState: (node, key) => this.ensureWidgetState(node, key),
6259
7077
  resolveItemNodeKey: (node, listKey, index, fallbackPath) => this.resolveItemNodeKey(node, listKey, index, fallbackPath),
6260
7078
  indexListElementNodes: (listNode, element, index) => this.indexListElementNodes(listNode, element, index)
@@ -6268,12 +7086,17 @@ var RuntimeRenderer = class {
6268
7086
  runActions: (actions, inputValue, context) => this.actionRuntime.runActions(actions, inputValue, context),
6269
7087
  rerenderRoot: () => this.rerenderRoot()
6270
7088
  });
7089
+ this.voiceRuntime = new VoiceRuntime({
7090
+ debugLogging: this.debugLogging,
7091
+ triggerSystemEvent: (eventName, inputValue) => this.triggerRuntimeSystemEvent(eventName, inputValue)
7092
+ });
6271
7093
  this.actionRuntime = new ActionRuntime({
6272
7094
  debugLogging: this.debugLogging,
6273
7095
  actionsMap: this.actionsMap,
6274
7096
  actionFunctions: this.actionFunctions,
6275
7097
  nodeById: this.nodeById,
6276
7098
  stateById: this.stateById,
7099
+ stateByKey: this.stateByKey,
6277
7100
  rerenderRoot: () => this.rerenderRoot(),
6278
7101
  dispatchWidgetEvent: (node, eventName, key, inputValue, pointer) => this.actionRuntime.dispatchWidgetEvent(node, eventName, key, inputValue, pointer),
6279
7102
  executeRequest: (requestName, currentValue) => this.networkRuntime.executeRequest(requestName, currentValue),
@@ -6283,6 +7106,17 @@ var RuntimeRenderer = class {
6283
7106
  resolveMappedValue: (template, currentValue, responseValue) => this.referenceRuntime.resolveMappedValue(template, currentValue, responseValue),
6284
7107
  setWidgetEnabled: (widgetId, enabled) => this.setWidgetEnabled(widgetId, enabled),
6285
7108
  clearWidget: (widgetId) => this.clearWidget(widgetId),
7109
+ clearListElementState: (listKey) => this.clearListElementState(listKey),
7110
+ focusWidget: (reference) => this.focusWidget(reference),
7111
+ playAudio: (value) => this.voiceRuntime.play(value),
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),
7116
+ startRecording: () => this.voiceRuntime.startRecording(),
7117
+ stopRecording: () => Promise.resolve(this.voiceRuntime.stopRecording()),
7118
+ startListening: () => this.voiceRuntime.startListening(),
7119
+ stopListening: () => this.voiceRuntime.stopListening(),
6286
7120
  setUiReference: (widgetId, resourceRef) => {
6287
7121
  const node = this.nodeById.get(widgetId);
6288
7122
  if (!node || node.widget !== "ui-reference") {
@@ -6376,6 +7210,7 @@ var RuntimeRenderer = class {
6376
7210
  source.close();
6377
7211
  }
6378
7212
  this.eventSources.length = 0;
7213
+ this.voiceRuntime.dispose();
6379
7214
  };
6380
7215
  }
6381
7216
  rerenderRoot() {
@@ -6383,7 +7218,7 @@ var RuntimeRenderer = class {
6383
7218
  return Promise.resolve();
6384
7219
  }
6385
7220
  this.reindexStaticTree();
6386
- const preservedState = captureElementState(this.root, this.pendingResetModalIds, this.stateByKey);
7221
+ const preservedState = captureElementState(this.root, this.pendingResetModalIds);
6387
7222
  this.root.innerHTML = this.renderMarkup();
6388
7223
  this.root.classList.toggle("vjt-root--mobile", isMobileViewport());
6389
7224
  this.root.classList.toggle("vjt-root--desktop", !isMobileViewport());
@@ -6392,7 +7227,7 @@ var RuntimeRenderer = class {
6392
7227
  }
6393
7228
  this.attachInputBehavior(this.root);
6394
7229
  this.attachWidgetEvents(this.root);
6395
- restoreElementState(this.root, preservedState);
7230
+ restoreElementState(this.root, preservedState, this.stateByKey);
6396
7231
  this.activeContextMenu = adjustContextMenuPosition(this.root, this.activeContextMenu);
6397
7232
  this.pendingResetModalIds.clear();
6398
7233
  if (typeof this.onRuntimeSnapshot === "function") {
@@ -6407,6 +7242,17 @@ var RuntimeRenderer = class {
6407
7242
  }
6408
7243
  await this.actionRuntime.runActions(actions, null, { currentValue: null });
6409
7244
  }
7245
+ async triggerRuntimeSystemEvent(eventName, inputValue) {
7246
+ const actions = this.systemEvents[eventName];
7247
+ if (!actions?.length) {
7248
+ return;
7249
+ }
7250
+ await this.actionRuntime.runActions(actions, inputValue, {
7251
+ currentValue: inputValue,
7252
+ responseValue: inputValue
7253
+ });
7254
+ await this.rerenderRoot();
7255
+ }
6410
7256
  async navigateTo(url) {
6411
7257
  if (typeof window === "undefined" || !url.trim()) {
6412
7258
  return;
@@ -6425,6 +7271,138 @@ var RuntimeRenderer = class {
6425
7271
  await this.rerenderRoot();
6426
7272
  await this.runSystemEvent("onAfterNavigate");
6427
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
+ }
6428
7406
  reindexStaticTree() {
6429
7407
  this.nodeByKey.clear();
6430
7408
  this.nodeById.clear();
@@ -6461,6 +7439,12 @@ var RuntimeRenderer = class {
6461
7439
  this.stateById.set(restoredState.id, restoredState);
6462
7440
  }
6463
7441
  }
7442
+ this.vars.clear();
7443
+ for (const [name, value] of snapshot.vars ?? []) {
7444
+ if (typeof name === "string" && name.length > 0) {
7445
+ this.vars.set(name, deepClone(value));
7446
+ }
7447
+ }
6464
7448
  if (snapshot.activeModalId && this.nodeById.get(snapshot.activeModalId)?.widget === "modal-window") {
6465
7449
  this.activeModalId = snapshot.activeModalId;
6466
7450
  }
@@ -6471,8 +7455,13 @@ var RuntimeRenderer = class {
6471
7455
  createRuntimeSnapshot() {
6472
7456
  return {
6473
7457
  stateByKey: Array.from(this.stateByKey.entries(), ([key, state]) => [key, deepClone(state)]),
7458
+ vars: Array.from(this.vars.entries(), ([name, value]) => [name, deepClone(value)]),
6474
7459
  activeModalId: this.activeModalId,
6475
- 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
6476
7465
  };
6477
7466
  }
6478
7467
  indexStaticNodes(node, path) {
@@ -6982,7 +7971,8 @@ var RuntimeRenderer = class {
6982
7971
  const env = this.getWidgetRenderEnvironment();
6983
7972
  const modalMarkup = this.activeModalId ? renderModalOverlay(env, this.activeModalId) : "";
6984
7973
  const menuMarkup = this.activeContextMenu ? renderContextMenuOverlay(env, this.activeContextMenu) : "";
6985
- return `${modalMarkup}${menuMarkup}`;
7974
+ const confirmMarkup = this.activeConfirmModal ? renderConfirmModalOverlay(env, this.activeConfirmModal) : "";
7975
+ return `${modalMarkup}${menuMarkup}${confirmMarkup}`;
6986
7976
  }
6987
7977
  updatePointerFromEvent(event) {
6988
7978
  this.lastPointer = updatePointerFromMouseEvent(event);
@@ -7209,6 +8199,7 @@ var RuntimeRenderer = class {
7209
8199
  nodeByKey: this.nodeByKey,
7210
8200
  stateByKey: this.stateByKey,
7211
8201
  getLastPointer: () => this.lastPointer,
8202
+ getActiveContextMenu: () => this.activeContextMenu,
7212
8203
  setActiveContextMenu: (menu) => {
7213
8204
  this.activeContextMenu = menu;
7214
8205
  },
@@ -7218,6 +8209,7 @@ var RuntimeRenderer = class {
7218
8209
  updateSplitterDrag: (clientPosition) => this.updateSplitterDrag(clientPosition),
7219
8210
  stopSplitterDrag: () => this.stopSplitterDrag(),
7220
8211
  hasSplitterDrag: () => this.splitterDragState !== null,
8212
+ resolveConfirmModal: (confirmed) => this.resolveConfirmModal(confirmed),
7221
8213
  closeModal: (modalId) => this.closeModal(modalId),
7222
8214
  rerenderRoot: () => this.rerenderRoot(),
7223
8215
  redispatchUnderlyingClick: (x, y) => this.redispatchUnderlyingClick(x, y),
@@ -7414,6 +8406,28 @@ var RuntimeRenderer = class {
7414
8406
  clearListElementState(listKey) {
7415
8407
  this.referenceRuntime.clearListElementState(listKey);
7416
8408
  }
8409
+ focusWidget(reference) {
8410
+ if (!(this.root instanceof HTMLElement)) {
8411
+ return;
8412
+ }
8413
+ const widgetId = reference.split(".")[0]?.trim();
8414
+ if (!widgetId) {
8415
+ return;
8416
+ }
8417
+ const directTarget = this.root.querySelector(`#${CSS.escape(widgetId)}`);
8418
+ if (directTarget && typeof directTarget.focus === "function") {
8419
+ directTarget.focus({ preventScroll: true });
8420
+ return;
8421
+ }
8422
+ const widgetRoot = this.root.querySelector(`[data-widget-id="${CSS.escape(widgetId)}"]`);
8423
+ if (!widgetRoot) {
8424
+ return;
8425
+ }
8426
+ const nestedTarget = widgetRoot.matches("input, textarea, select, button, a[href], [tabindex]") ? widgetRoot : widgetRoot.querySelector("input, textarea, select, button, a[href], [tabindex]");
8427
+ if (nestedTarget && typeof nestedTarget.focus === "function") {
8428
+ nestedTarget.focus({ preventScroll: true });
8429
+ }
8430
+ }
7417
8431
  };
7418
8432
  function renderJson(description, options = {}) {
7419
8433
  const renderer = new RuntimeRenderer(description, options);