@vortexm/vjt 0.1.7 → 0.1.9

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
@@ -4154,6 +4154,32 @@ function readPath(target, path) {
4154
4154
  }
4155
4155
  return current;
4156
4156
  }
4157
+ function writePath(target, path, value) {
4158
+ if (!isPlainObject2(target) || path.length === 0) {
4159
+ return false;
4160
+ }
4161
+ let current = target;
4162
+ for (let index = 0; index < path.length - 1; index += 1) {
4163
+ const segment = path[index];
4164
+ if (isBlockedObjectKey(segment)) {
4165
+ return false;
4166
+ }
4167
+ const next = current[segment];
4168
+ if (isPlainObject2(next)) {
4169
+ current = next;
4170
+ continue;
4171
+ }
4172
+ const created = {};
4173
+ current[segment] = created;
4174
+ current = created;
4175
+ }
4176
+ const leaf = path[path.length - 1];
4177
+ if (isBlockedObjectKey(leaf)) {
4178
+ return false;
4179
+ }
4180
+ current[leaf] = value;
4181
+ return true;
4182
+ }
4157
4183
  function sanitizeIdFragment(value) {
4158
4184
  return value.replace(/[^A-Za-z0-9_-]/g, "");
4159
4185
  }
@@ -4273,8 +4299,9 @@ var ReferenceRuntime = class {
4273
4299
  }
4274
4300
  resolveCurrentReference(reference, currentValue) {
4275
4301
  if (this.isListElementContext(currentValue)) {
4302
+ const resolvedDescriptor = this.resolveListDescriptorNode(currentValue.descriptor, currentValue);
4276
4303
  const firstPart = reference.split(".")[0];
4277
- const namedNode = this.findNamedWidget(currentValue.descriptor, firstPart);
4304
+ const namedNode = this.findNamedWidget(resolvedDescriptor, firstPart, currentValue);
4278
4305
  if (namedNode) {
4279
4306
  const widgetKey = this.host.resolveItemNodeKey(namedNode, currentValue.listId, currentValue.index, `named.${firstPart}`);
4280
4307
  const state = this.host.ensureWidgetState(namedNode, widgetKey);
@@ -4285,12 +4312,16 @@ var ReferenceRuntime = class {
4285
4312
  if (directDescriptorValue !== void 0) {
4286
4313
  return directDescriptorValue;
4287
4314
  }
4288
- if (typeof currentValue.descriptor.widget === "string") {
4289
- const descriptorKey = this.host.resolveItemNodeKey(currentValue.descriptor, currentValue.listId, currentValue.index, "item");
4290
- const descriptorState = this.host.ensureWidgetState(currentValue.descriptor, descriptorKey);
4315
+ const resolvedDescriptorValue = resolvedDescriptor !== currentValue.descriptor ? readPath(resolvedDescriptor, reference.split(".")) : void 0;
4316
+ if (resolvedDescriptorValue !== void 0) {
4317
+ return resolvedDescriptorValue;
4318
+ }
4319
+ if (typeof resolvedDescriptor.widget === "string") {
4320
+ const descriptorKey = this.host.resolveItemNodeKey(resolvedDescriptor, currentValue.listId, currentValue.index, "item");
4321
+ const descriptorState = this.host.ensureWidgetState(resolvedDescriptor, descriptorKey);
4291
4322
  return this.readWidgetField(descriptorState, reference);
4292
4323
  }
4293
- return readPath(currentValue.descriptor, reference.split("."));
4324
+ return readPath(resolvedDescriptor, reference.split("."));
4294
4325
  }
4295
4326
  if (isPlainObject2(currentValue) || Array.isArray(currentValue)) {
4296
4327
  return readPath(currentValue, reference.split("."));
@@ -4335,6 +4366,8 @@ var ReferenceRuntime = class {
4335
4366
  return state.isHidden ?? true;
4336
4367
  case "url":
4337
4368
  return state.url ?? "";
4369
+ case "base64":
4370
+ return state.base64 ?? "";
4338
4371
  case "elements":
4339
4372
  if (state.widget === "list") {
4340
4373
  return (state.elements ?? []).map((descriptor, index) => ({
@@ -4374,8 +4407,12 @@ var ReferenceRuntime = class {
4374
4407
  return readPath(state, field.split("."));
4375
4408
  }
4376
4409
  }
4377
- assignReference(reference, inputValue, _currentValue, options) {
4378
- if (reference.startsWith("current.") || hasBlockedReferenceSegment(reference)) {
4410
+ assignReference(reference, inputValue, currentValue, options) {
4411
+ if (hasBlockedReferenceSegment(reference)) {
4412
+ return;
4413
+ }
4414
+ if (reference.startsWith("current.")) {
4415
+ this.assignCurrentReference(reference.slice(8), inputValue, currentValue);
4379
4416
  return;
4380
4417
  }
4381
4418
  if (reference.startsWith("cookies.")) {
@@ -4418,6 +4455,9 @@ var ReferenceRuntime = class {
4418
4455
  case "url":
4419
4456
  state.url = stringifyReferenceValue(inputValue);
4420
4457
  break;
4458
+ case "base64":
4459
+ state.base64 = stringifyReferenceValue(inputValue);
4460
+ break;
4421
4461
  case "condition":
4422
4462
  state.condition = Boolean(inputValue);
4423
4463
  break;
@@ -4443,6 +4483,19 @@ var ReferenceRuntime = class {
4443
4483
  break;
4444
4484
  }
4445
4485
  }
4486
+ assignCurrentReference(reference, inputValue, currentValue) {
4487
+ const path = reference.split(".").filter(Boolean);
4488
+ if (path.length === 0) {
4489
+ return;
4490
+ }
4491
+ if (this.isListElementContext(currentValue)) {
4492
+ writePath(currentValue.descriptor, path, inputValue);
4493
+ return;
4494
+ }
4495
+ if (isPlainObject2(currentValue)) {
4496
+ writePath(currentValue, path, inputValue);
4497
+ }
4498
+ }
4446
4499
  clearListElementState(listKey) {
4447
4500
  const generatedPrefix = `${sanitizeIdFragment(listKey)}Element`;
4448
4501
  for (const key of Array.from(this.host.stateByKey.keys())) {
@@ -4493,7 +4546,7 @@ var ReferenceRuntime = class {
4493
4546
  isListElementContext(value) {
4494
4547
  return isPlainObject2(value) && value.kind === "list-element" && typeof value.listId === "string" && typeof value.index === "number" && isPlainObject2(value.descriptor) && typeof value.descriptor.widget === "string";
4495
4548
  }
4496
- findNamedWidget(node, name) {
4549
+ findNamedWidget(node, name, currentValue) {
4497
4550
  if (!node) {
4498
4551
  return null;
4499
4552
  }
@@ -4502,12 +4555,12 @@ var ReferenceRuntime = class {
4502
4555
  }
4503
4556
  switch (node.widget) {
4504
4557
  case "adaptive-layout":
4505
- return this.findNamedWidget(node.desktop, name) ?? this.findNamedWidget(node.mobile, name);
4558
+ return this.findNamedWidget(node.desktop, name, currentValue) ?? this.findNamedWidget(node.mobile, name, currentValue);
4506
4559
  case "container-layout":
4507
4560
  if (node.type === "grid") {
4508
4561
  for (const row of node.grid?.rows ?? []) {
4509
4562
  for (const column of row.columns ?? []) {
4510
- const match = this.findNamedWidget(column.child, name);
4563
+ const match = this.findNamedWidget(column.child, name, currentValue);
4511
4564
  if (match) {
4512
4565
  return match;
4513
4566
  }
@@ -4516,20 +4569,20 @@ var ReferenceRuntime = class {
4516
4569
  return null;
4517
4570
  }
4518
4571
  for (const cell of node.children ?? []) {
4519
- const match = this.findNamedWidget(cell.child, name);
4572
+ const match = this.findNamedWidget(cell.child, name, currentValue);
4520
4573
  if (match) {
4521
4574
  return match;
4522
4575
  }
4523
4576
  }
4524
4577
  return null;
4525
4578
  case "panel":
4526
- return this.findNamedWidget(node.child, name);
4579
+ return this.findNamedWidget(node.child, name, currentValue);
4527
4580
  case "conditional-container":
4528
- return this.findNamedWidget(node.default, name) ?? this.findNamedWidget(node.alternative, name);
4581
+ return this.findNamedWidget(node.default, name, currentValue) ?? this.findNamedWidget(node.alternative, name, currentValue);
4529
4582
  case "list":
4530
4583
  case "grid-view":
4531
4584
  for (const element of node.elements ?? []) {
4532
- const match = this.findNamedWidget(element, name);
4585
+ const match = this.findNamedWidget(element, name, currentValue);
4533
4586
  if (match) {
4534
4587
  return match;
4535
4588
  }
@@ -4537,7 +4590,7 @@ var ReferenceRuntime = class {
4537
4590
  return null;
4538
4591
  case "overlay-container":
4539
4592
  for (const layer of node.layers ?? []) {
4540
- const match = this.findNamedWidget(layer.child, name);
4593
+ const match = this.findNamedWidget(layer.child, name, currentValue);
4541
4594
  if (match) {
4542
4595
  return match;
4543
4596
  }
@@ -4545,20 +4598,40 @@ var ReferenceRuntime = class {
4545
4598
  return null;
4546
4599
  case "tabs":
4547
4600
  for (const tab of node.tabs ?? []) {
4548
- const match = this.findNamedWidget(tab.content, name);
4601
+ const match = this.findNamedWidget(tab.content, name, currentValue);
4549
4602
  if (match) {
4550
4603
  return match;
4551
4604
  }
4552
4605
  }
4553
4606
  return null;
4554
4607
  case "spoiler":
4555
- return typeof node.content === "string" ? null : this.findNamedWidget(node.content, name);
4608
+ return typeof node.content === "string" ? null : this.findNamedWidget(node.content, name, currentValue);
4609
+ case "ui-reference": {
4610
+ const referenced = currentValue ? this.resolveUiReferenceNode(node, currentValue) : null;
4611
+ return referenced ? this.findNamedWidget(referenced, name, currentValue) : null;
4612
+ }
4556
4613
  case "modal-window":
4557
- return this.findNamedWidget(node.child, name);
4614
+ return this.findNamedWidget(node.child, name, currentValue);
4558
4615
  default:
4559
4616
  return null;
4560
4617
  }
4561
4618
  }
4619
+ resolveListDescriptorNode(node, currentValue) {
4620
+ if (node.widget !== "ui-reference") {
4621
+ return node;
4622
+ }
4623
+ return this.resolveUiReferenceNode(node, currentValue) ?? node;
4624
+ }
4625
+ resolveUiReferenceNode(node, currentValue) {
4626
+ if (node.widget !== "ui-reference") {
4627
+ return null;
4628
+ }
4629
+ const resolvedValue = typeof node.ref === "string" && node.ref.startsWith("$ref:") ? this.resolveReference(node.ref.slice(5), currentValue, null) : node.ref;
4630
+ if (typeof resolvedValue !== "string" || resolvedValue.length === 0) {
4631
+ return null;
4632
+ }
4633
+ return this.host.getUiResource(resolvedValue);
4634
+ }
4562
4635
  };
4563
4636
 
4564
4637
  // src/lib/action-runtime.ts
@@ -4577,9 +4650,14 @@ var ActionRuntime = class {
4577
4650
  assignReference;
4578
4651
  resolveMappedValue;
4579
4652
  setWidgetEnabled;
4653
+ setWidgetVisible;
4580
4654
  clearWidget;
4581
4655
  clearListElementState;
4582
4656
  focusWidget;
4657
+ moveCursorToEnd;
4658
+ scrollWidgetToTop;
4659
+ scrollWidgetToBottom;
4660
+ scrollWidgetToElementByIndex;
4583
4661
  playAudio;
4584
4662
  stopPlaying;
4585
4663
  copyToClipboard;
@@ -4614,9 +4692,14 @@ var ActionRuntime = class {
4614
4692
  this.assignReference = options.assignReference;
4615
4693
  this.resolveMappedValue = options.resolveMappedValue;
4616
4694
  this.setWidgetEnabled = options.setWidgetEnabled;
4695
+ this.setWidgetVisible = options.setWidgetVisible;
4617
4696
  this.clearWidget = options.clearWidget;
4618
4697
  this.clearListElementState = options.clearListElementState;
4619
4698
  this.focusWidget = options.focusWidget;
4699
+ this.moveCursorToEnd = options.moveCursorToEnd;
4700
+ this.scrollWidgetToTop = options.scrollWidgetToTop;
4701
+ this.scrollWidgetToBottom = options.scrollWidgetToBottom;
4702
+ this.scrollWidgetToElementByIndex = options.scrollWidgetToElementByIndex;
4620
4703
  this.playAudio = options.playAudio;
4621
4704
  this.stopPlaying = options.stopPlaying;
4622
4705
  this.copyToClipboard = options.copyToClipboard;
@@ -4678,7 +4761,7 @@ var ActionRuntime = class {
4678
4761
  let output = rawOutput;
4679
4762
  if (action.andThen?.length) {
4680
4763
  if (output !== null && output !== void 0) {
4681
- output = await this.runActions(action.andThen, output, { ...context, currentValue: output });
4764
+ output = await this.runActions(action.andThen, output, context);
4682
4765
  } else {
4683
4766
  output = null;
4684
4767
  }
@@ -4765,7 +4848,7 @@ var ActionRuntime = class {
4765
4848
  return null;
4766
4849
  }
4767
4850
  if (name === "startListening") {
4768
- await this.startListening();
4851
+ await this.startListening(action.args);
4769
4852
  return null;
4770
4853
  }
4771
4854
  if (name === "stopListening") {
@@ -4809,6 +4892,14 @@ var ActionRuntime = class {
4809
4892
  this.setWidgetEnabled(name.slice(8), false);
4810
4893
  return null;
4811
4894
  }
4895
+ if (name.startsWith("makeVisible ")) {
4896
+ this.setWidgetVisible(name.slice(12), true);
4897
+ return null;
4898
+ }
4899
+ if (name.startsWith("makeInvisible ")) {
4900
+ this.setWidgetVisible(name.slice(14), false);
4901
+ return null;
4902
+ }
4812
4903
  if (name.startsWith("clear ")) {
4813
4904
  this.clearWidget(name.slice(6));
4814
4905
  return null;
@@ -4817,6 +4908,22 @@ var ActionRuntime = class {
4817
4908
  this.focusWidget(name.slice(9));
4818
4909
  return null;
4819
4910
  }
4911
+ if (name.startsWith("moveCursorToEnd ")) {
4912
+ this.moveCursorToEnd(name.slice(16));
4913
+ return null;
4914
+ }
4915
+ if (name.startsWith("scrollToTop ")) {
4916
+ this.scrollWidgetToTop(name.slice(12));
4917
+ return inputValue;
4918
+ }
4919
+ if (name.startsWith("scrollToBottom ")) {
4920
+ this.scrollWidgetToBottom(name.slice(15));
4921
+ return inputValue;
4922
+ }
4923
+ if (name.startsWith("scrollToElementByIndex ")) {
4924
+ this.scrollWidgetToElementByIndex(name.slice(23), inputValue);
4925
+ return inputValue;
4926
+ }
4820
4927
  if (name.startsWith("setUi ")) {
4821
4928
  const widgetId = name.slice(6);
4822
4929
  const resourceRef = typeof inputValue === "string" ? inputValue : "";
@@ -4827,14 +4934,62 @@ var ActionRuntime = class {
4827
4934
  }
4828
4935
  if (name.startsWith("get ")) {
4829
4936
  const reference = name.slice(4);
4830
- if (Array.isArray(context.currentValue) && this.isCurrentScopedReference(reference)) {
4831
- return context.currentValue.map((entry) => this.resolveReference(reference, entry, context.responseValue)).filter((entry) => entry !== null && entry !== void 0);
4937
+ if (Array.isArray(inputValue) && this.isCurrentScopedReference(reference)) {
4938
+ return inputValue.map((entry) => this.resolveReference(reference, entry, context.responseValue)).filter((entry) => entry !== null && entry !== void 0);
4832
4939
  }
4833
4940
  return this.resolveReference(reference, context.currentValue, context.responseValue);
4834
4941
  }
4942
+ if (name.startsWith("count ")) {
4943
+ return this.countValue(this.resolveReference(name.slice(6), context.currentValue, context.responseValue));
4944
+ }
4945
+ if (name === "indexOf") {
4946
+ if (isListElementLike(context.currentValue)) {
4947
+ return context.currentValue.index;
4948
+ }
4949
+ if (typeof context.currentIndex === "number") {
4950
+ return context.currentIndex;
4951
+ }
4952
+ return null;
4953
+ }
4954
+ if (name === "foreach") {
4955
+ if (Array.isArray(inputValue) && action.andThen?.length) {
4956
+ const currentList = inputValue;
4957
+ for (let index = 0; index < currentList.length; index += 1) {
4958
+ const entry = currentList[index];
4959
+ await this.runActions(action.andThen, entry, {
4960
+ ...context,
4961
+ currentValue: entry,
4962
+ currentList,
4963
+ currentIndex: index
4964
+ });
4965
+ }
4966
+ }
4967
+ return inputValue;
4968
+ }
4969
+ if (name === "inc") {
4970
+ return this.toNumber(inputValue) + 1;
4971
+ }
4972
+ if (name === "dec") {
4973
+ return this.toNumber(inputValue) - 1;
4974
+ }
4835
4975
  if (name.startsWith("equals ")) {
4836
4976
  return this.resolveReference(name.slice(7), context.currentValue, context.responseValue) === action.args;
4837
4977
  }
4978
+ if (name.startsWith("greaterThan ")) {
4979
+ return this.toNumber(inputValue) > this.toNumber(this.resolveReference(name.slice(12), context.currentValue, context.responseValue));
4980
+ }
4981
+ if (name.startsWith("greaterThanOrEquals ")) {
4982
+ return this.toNumber(inputValue) >= this.toNumber(this.resolveReference(name.slice(20), context.currentValue, context.responseValue));
4983
+ }
4984
+ if (name.startsWith("lessThan ")) {
4985
+ return this.toNumber(inputValue) < this.toNumber(this.resolveReference(name.slice(9), context.currentValue, context.responseValue));
4986
+ }
4987
+ if (name.startsWith("lessThanOrEquals ")) {
4988
+ return this.toNumber(inputValue) <= this.toNumber(this.resolveReference(name.slice(17), context.currentValue, context.responseValue));
4989
+ }
4990
+ if (name.startsWith("and ")) {
4991
+ return Boolean(inputValue) && Boolean(this.resolveReference(name.slice(4), context.currentValue, context.responseValue));
4992
+ }
4838
4993
  if (name.startsWith("isEmpty ")) {
4839
4994
  return this.isEmptyReference(name.slice(8), context.currentValue, context.responseValue);
4840
4995
  }
@@ -4852,8 +5007,8 @@ var ActionRuntime = class {
4852
5007
  }
4853
5008
  if (name.startsWith("filter ")) {
4854
5009
  const reference = name.slice(7);
4855
- if (Array.isArray(context.currentValue) && this.isCurrentScopedReference(reference)) {
4856
- return context.currentValue.filter((entry) => this.resolveReference(reference, entry, context.responseValue));
5010
+ if (Array.isArray(inputValue) && this.isCurrentScopedReference(reference)) {
5011
+ return inputValue.filter((entry) => this.resolveReference(reference, entry, context.responseValue));
4857
5012
  }
4858
5013
  const value = this.resolveReference(reference, context.currentValue, context.responseValue);
4859
5014
  return value ? inputValue : null;
@@ -4878,6 +5033,12 @@ var ActionRuntime = class {
4878
5033
  this.clearListElementState(listState.key);
4879
5034
  return listState.elements;
4880
5035
  }
5036
+ const inputListState = this.asListWidgetState(inputValue);
5037
+ if (inputListState && Array.isArray(inputListState.elements)) {
5038
+ inputListState.elements.push(valueToAdd);
5039
+ this.clearListElementState(inputListState.key);
5040
+ return inputListState.elements;
5041
+ }
4881
5042
  if (context.currentList && typeof context.currentIndex === "number") {
4882
5043
  return context.currentIndex === context.currentList.length - 1 ? [this.unwrapListValue(inputValue), valueToAdd] : this.unwrapListValue(inputValue);
4883
5044
  }
@@ -4902,6 +5063,12 @@ var ActionRuntime = class {
4902
5063
  return listState.elements;
4903
5064
  }
4904
5065
  }
5066
+ const inputListState = this.asListWidgetState(inputValue);
5067
+ if (inputListState && Array.isArray(inputListState.elements)) {
5068
+ inputListState.elements.push(valueToInsert);
5069
+ this.clearListElementState(inputListState.key);
5070
+ return inputListState.elements;
5071
+ }
4905
5072
  if (context.currentList && typeof context.currentIndex === "number") {
4906
5073
  return [this.unwrapListValue(inputValue), valueToInsert];
4907
5074
  }
@@ -4916,10 +5083,10 @@ var ActionRuntime = class {
4916
5083
  return inputValue;
4917
5084
  }
4918
5085
  if (name === "map") {
4919
- if (Array.isArray(context.currentValue)) {
4920
- return context.currentValue.map((entry) => this.resolveMappedValue(action.args, entry, context.responseValue));
5086
+ if (Array.isArray(inputValue)) {
5087
+ return inputValue.map((entry) => this.resolveMappedValue(action.args, entry, context.responseValue));
4921
5088
  }
4922
- return this.resolveMappedValue(action.args, context.currentValue, context.responseValue);
5089
+ return this.resolveMappedValue(action.args, inputValue, context.responseValue);
4923
5090
  }
4924
5091
  if (name === "ifelse") {
4925
5092
  const branches = isIfElseArgs(action.args) ? action.args : null;
@@ -4964,6 +5131,46 @@ var ActionRuntime = class {
4964
5131
  isCurrentScopedReference(reference) {
4965
5132
  return reference === "current" || reference.startsWith("current.") || reference.startsWith("this.") || reference === "this";
4966
5133
  }
5134
+ countValue(value) {
5135
+ if (Array.isArray(value) || typeof value === "string") {
5136
+ return value.length;
5137
+ }
5138
+ if (value && typeof value === "object") {
5139
+ const widgetState = value;
5140
+ if (widgetState.widget === "list" || widgetState.widget === "grid-view") {
5141
+ return Array.isArray(widgetState.elements) ? widgetState.elements.length : 0;
5142
+ }
5143
+ if (widgetState.widget === "listbox") {
5144
+ return Array.isArray(widgetState.listboxElements) ? widgetState.listboxElements.length : 0;
5145
+ }
5146
+ if (widgetState.widget === "combobox") {
5147
+ return Array.isArray(widgetState.comboboxElements) ? widgetState.comboboxElements.length : 0;
5148
+ }
5149
+ if ("length" in widgetState && typeof widgetState.length === "number") {
5150
+ return widgetState.length;
5151
+ }
5152
+ }
5153
+ return 0;
5154
+ }
5155
+ toNumber(value) {
5156
+ if (typeof value === "number" && Number.isFinite(value)) {
5157
+ return value;
5158
+ }
5159
+ if (typeof value === "string" && value.trim().length > 0) {
5160
+ const parsed = Number.parseFloat(value);
5161
+ if (Number.isFinite(parsed)) {
5162
+ return parsed;
5163
+ }
5164
+ }
5165
+ return 0;
5166
+ }
5167
+ asListWidgetState(value) {
5168
+ if (!value || typeof value !== "object") {
5169
+ return null;
5170
+ }
5171
+ const state = value;
5172
+ return state.widget === "list" || state.widget === "grid-view" ? state : null;
5173
+ }
4967
5174
  };
4968
5175
  function isIfElseArgs(value) {
4969
5176
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -5393,7 +5600,7 @@ var MAX_CONCURRENT_PLAYERS = 5;
5393
5600
  var MAX_RECORDING_MS = 10 * 60 * 1e3;
5394
5601
  var MAX_LISTENING_SILENCE_MS = 60 * 60 * 1e3;
5395
5602
  var SILENCE_STOP_MS = 2e3;
5396
- var SPEECH_THRESHOLD = 0.035;
5603
+ var DEFAULT_SPEECH_THRESHOLD = 0.035;
5397
5604
  var RECORDING_MIME_CANDIDATES = [
5398
5605
  "audio/webm;codecs=opus",
5399
5606
  "audio/webm",
@@ -5446,6 +5653,21 @@ function blobToBase64(blob) {
5446
5653
  reader.readAsDataURL(blob);
5447
5654
  });
5448
5655
  }
5656
+ function normalizeSpeechThreshold(value) {
5657
+ if (typeof value === "number" && Number.isFinite(value)) {
5658
+ return Math.max(0, Math.min(1, value));
5659
+ }
5660
+ if (typeof value === "string" && value.trim().length > 0) {
5661
+ const parsed = Number.parseFloat(value);
5662
+ if (Number.isFinite(parsed)) {
5663
+ return Math.max(0, Math.min(1, parsed));
5664
+ }
5665
+ }
5666
+ return DEFAULT_SPEECH_THRESHOLD;
5667
+ }
5668
+ function isStartListeningArgs(value) {
5669
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5670
+ }
5449
5671
  var VoiceRuntime = class {
5450
5672
  debugLogging;
5451
5673
  triggerSystemEvent;
@@ -5542,11 +5764,12 @@ var VoiceRuntime = class {
5542
5764
  this.clearRecordingTimeout();
5543
5765
  this.mediaRecorder.stop();
5544
5766
  }
5545
- async startListening() {
5767
+ async startListening(args) {
5546
5768
  if (this.listening) {
5547
5769
  return;
5548
5770
  }
5549
5771
  try {
5772
+ const silenceThreshold = isStartListeningArgs(args) ? normalizeSpeechThreshold(args.silenceThreshold) : DEFAULT_SPEECH_THRESHOLD;
5550
5773
  const stream = await this.ensureInputStream();
5551
5774
  const context = new AudioContext();
5552
5775
  const analyser = context.createAnalyser();
@@ -5573,7 +5796,7 @@ var VoiceRuntime = class {
5573
5796
  }
5574
5797
  const rms = Math.sqrt(squareSum / sampleBuffer.length);
5575
5798
  const now = performance.now();
5576
- if (rms >= SPEECH_THRESHOLD) {
5799
+ if (rms >= silenceThreshold) {
5577
5800
  this.lastSpeechAt = now;
5578
5801
  this.listeningStartedAt = now;
5579
5802
  if (!this.speechEventTriggered) {
@@ -5595,6 +5818,9 @@ var VoiceRuntime = class {
5595
5818
  void step();
5596
5819
  });
5597
5820
  };
5821
+ logRuntimeDebug(this.debugLogging, "voice-listening-started", {
5822
+ silenceThreshold
5823
+ });
5598
5824
  void step();
5599
5825
  } catch (error) {
5600
5826
  await this.handleListeningError(error);
@@ -6010,7 +6236,7 @@ var renderGridView = (env, node, state, key) => {
6010
6236
  index: realIndex,
6011
6237
  descriptor: element
6012
6238
  };
6013
- return `<div class="vjt-grid-cell">${env.renderNode(element, `${key}.elements.${realIndex}`, context)}</div>`;
6239
+ return `<div class="vjt-grid-cell" data-list-id="${env.escapeHtml(key)}" data-list-index="${realIndex}">${env.renderNode(element, `${key}.elements.${realIndex}`, context)}</div>`;
6014
6240
  }).join("");
6015
6241
  rows.push(`<div class="vjt-grid-row">${rowMarkup}</div>`);
6016
6242
  }
@@ -6056,13 +6282,13 @@ var renderCombobox = (env, node, state, key) => {
6056
6282
  var HEADING_TAGS = /* @__PURE__ */ new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
6057
6283
  var TEXT_ALIGN_MAP = { left: "left", center: "center", right: "right" };
6058
6284
  var VERTICAL_ALIGN_MAP = { top: "flex-start", center: "center", bottom: "flex-end" };
6059
- var renderStaticText = (env, node, state, key) => {
6285
+ var renderStaticText = (env, node, state, key, _path, itemContext) => {
6060
6286
  const resolvedHeading = node.heading && HEADING_TAGS.has(node.heading) ? node.heading : null;
6061
6287
  const tag = resolvedHeading ?? "div";
6062
6288
  const align = TEXT_ALIGN_MAP[node["horiz-align"] ?? "left"];
6063
6289
  const verticalAlign = VERTICAL_ALIGN_MAP[node["vert-align"] ?? "center"];
6064
6290
  const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
6065
- const text = env.escapeHtml(env.resolveI18nValue(state.text ?? node.text));
6291
+ const text = env.escapeHtml(env.resolveTextValue(state.text ?? node.text, itemContext));
6066
6292
  const inlineStyle = [
6067
6293
  "display:flex",
6068
6294
  `align-items:${verticalAlign}`,
@@ -6093,11 +6319,11 @@ var markdownConverter = new import_showdown.default.Converter({
6093
6319
  strikethrough: false,
6094
6320
  tasklists: false
6095
6321
  });
6096
- var renderMdText = (env, node, state, key) => {
6322
+ var renderMdText = (env, node, state, key, _path, itemContext) => {
6097
6323
  const align = TEXT_ALIGN_MAP2[node["horiz-align"] ?? "left"];
6098
6324
  const verticalAlign = VERTICAL_ALIGN_MAP2[node["vert-align"] ?? "center"];
6099
6325
  const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
6100
- const markdown = env.resolveI18nValue(state.text ?? node.text);
6326
+ const markdown = env.resolveTextValue(state.text ?? node.text, itemContext);
6101
6327
  const html = sanitizeGeneratedHtml(markdownConverter.makeHtml(sanitizeMarkdownSource(markdown)));
6102
6328
  const inlineStyle = [
6103
6329
  "display:flex",
@@ -6119,24 +6345,24 @@ var renderMdText = (env, node, state, key) => {
6119
6345
  };
6120
6346
 
6121
6347
  // src/lib/widgets/edit.ts
6122
- function resolvePlaceholder(env, node, statePlaceholder) {
6348
+ function resolvePlaceholder(env, node, itemContext, statePlaceholder) {
6123
6349
  if (statePlaceholder) {
6124
- return env.resolveI18nValue(statePlaceholder);
6350
+ return env.resolveTextValue(statePlaceholder, itemContext);
6125
6351
  }
6126
6352
  if (node.placeholder) {
6127
- return env.resolveI18nValue(node.placeholder);
6353
+ return env.resolveTextValue(node.placeholder, itemContext);
6128
6354
  }
6129
6355
  if ((node.type ?? "string") === "date") {
6130
6356
  return env.getDateFormat(node);
6131
6357
  }
6132
6358
  return "";
6133
6359
  }
6134
- var renderEdit = (env, node, state, key) => {
6360
+ var renderEdit = (env, node, state, key, _path, itemContext) => {
6135
6361
  const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
6136
6362
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
6137
6363
  const editable = state.editable ?? env.normalizeBoolean(node.editable, true);
6138
- const placeholder = env.escapeHtml(resolvePlaceholder(env, node, state.placeholder));
6139
- const value = env.escapeHtml(env.resolveI18nValue(state.text ?? node.text));
6364
+ const placeholder = env.escapeHtml(resolvePlaceholder(env, node, itemContext, state.placeholder));
6365
+ const value = env.escapeHtml(env.resolveTextValue(state.text ?? node.text, itemContext));
6140
6366
  const inputType = node.type === "password" ? "password" : "text";
6141
6367
  const disabledAttribute = enabled ? "" : " disabled";
6142
6368
  const readonlyAttribute = editable ? "" : " readonly";
@@ -6159,23 +6385,23 @@ var renderEdit = (env, node, state, key) => {
6159
6385
  };
6160
6386
 
6161
6387
  // src/lib/widgets/textarea.ts
6162
- var renderTextarea = (env, node, state, key) => {
6388
+ var renderTextarea = (env, node, state, key, _path, itemContext) => {
6163
6389
  const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
6164
6390
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
6165
6391
  const editable = state.editable ?? env.normalizeBoolean(node.editable, true);
6166
- const placeholder = env.escapeHtml(env.resolveI18nValue(state.placeholder ?? node.placeholder));
6167
- const value = env.escapeHtml(state.text ?? env.resolveI18nValue(node.text));
6392
+ const placeholder = env.escapeHtml(env.resolveTextValue(state.placeholder ?? node.placeholder, itemContext));
6393
+ const value = env.escapeHtml(state.text ?? env.resolveTextValue(node.text, itemContext));
6168
6394
  const styleString = env.buildStyleString(node);
6169
6395
  const styleAttribute = styleString ? ` style="${env.escapeHtml(styleString)}"` : "";
6170
6396
  return `<textarea${common}${env.buildWidgetDataAttributes(key, node)} class="vjt-textarea" data-widget="textarea" placeholder="${placeholder}"${enabled ? "" : " disabled"}${editable ? "" : " readonly"}${styleAttribute}>${value}</textarea>`;
6171
6397
  };
6172
6398
 
6173
6399
  // src/lib/widgets/button.ts
6174
- var renderButton = (env, node, state, key) => {
6400
+ var renderButton = (env, node, state, key, _path, itemContext) => {
6175
6401
  const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
6176
6402
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
6177
6403
  const disabledAttribute = enabled ? "" : " disabled";
6178
- const text = env.escapeHtml(env.resolveI18nValue(state.text ?? node.text));
6404
+ const text = env.escapeHtml(env.resolveTextValue(state.text ?? node.text, itemContext));
6179
6405
  const styleString = env.buildStyleString(node);
6180
6406
  const styleAttribute = styleString ? ` style="${env.escapeHtml(styleString)}"` : "";
6181
6407
  const buttonType = env.escapeHtml(node.type === "submit" ? "submit" : "button");
@@ -6204,7 +6430,10 @@ var renderCheckbox = (env, node, state, key) => {
6204
6430
 
6205
6431
  // src/lib/widgets/image.ts
6206
6432
  var renderImage = (env, node, state, key) => {
6207
- const url = env.escapeHtml(sanitizeUrl(state.url ?? node.url ?? "", { allowRelative: true }) ?? "");
6433
+ const rawBase64 = typeof state.base64 === "string" ? state.base64 : typeof node.base64 === "string" ? node.base64 : "";
6434
+ const rawUrl = sanitizeUrl(state.url ?? node.url ?? "", { allowRelative: true }) ?? "";
6435
+ const imageSource = rawBase64 ? rawBase64.startsWith("data:") ? rawBase64 : `data:image/png;base64,${rawBase64}` : rawUrl;
6436
+ const url = env.escapeHtml(imageSource);
6208
6437
  const styleParts = [
6209
6438
  "box-sizing:border-box",
6210
6439
  "width:100%",
@@ -6224,10 +6453,10 @@ var renderImage = (env, node, state, key) => {
6224
6453
  };
6225
6454
 
6226
6455
  // src/lib/widgets/link.ts
6227
- var renderLink = (env, node, state, key) => {
6456
+ var renderLink = (env, node, state, key, _path, itemContext) => {
6228
6457
  const type = node.type === "button" || node.type === "text" ? node.type : "link";
6229
6458
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
6230
- const text = env.escapeHtml(env.resolveI18nValue(state.text ?? node.text));
6459
+ const text = env.escapeHtml(env.resolveTextValue(state.text ?? node.text, itemContext));
6231
6460
  const classes = `vjt-link vjt-link--${type}${enabled ? "" : " is-disabled"}`;
6232
6461
  const styleString = env.buildStyleString(node);
6233
6462
  const styleAttribute = styleString ? ` style="${env.escapeHtml(styleString)}"` : "";
@@ -6408,6 +6637,32 @@ function renderConfirmModalOverlay(env, modal) {
6408
6637
  }
6409
6638
 
6410
6639
  // src/lib/widgets/bindings.ts
6640
+ function readBlobAsDataUrl(blob) {
6641
+ return new Promise((resolve, reject) => {
6642
+ const reader = new FileReader();
6643
+ reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : "");
6644
+ reader.onerror = () => reject(reader.error ?? new Error("Failed to read clipboard blob"));
6645
+ reader.readAsDataURL(blob);
6646
+ });
6647
+ }
6648
+ async function getPasteInputValue(event) {
6649
+ const clipboardData = event.clipboardData;
6650
+ if (!clipboardData) {
6651
+ return "";
6652
+ }
6653
+ for (const item of Array.from(clipboardData.items)) {
6654
+ if (!item.type.startsWith("image/")) {
6655
+ continue;
6656
+ }
6657
+ const file = item.getAsFile();
6658
+ if (!file) {
6659
+ continue;
6660
+ }
6661
+ const dataUrl = await readBlobAsDataUrl(file);
6662
+ return dataUrl;
6663
+ }
6664
+ return clipboardData.getData("text/plain");
6665
+ }
6411
6666
  function bindKeyboardSubmitEvents(element, node, key, env) {
6412
6667
  const hasKeyboardEvents = Boolean(
6413
6668
  node.events?.onEnter?.length || node.events?.onShiftEnter?.length || node.events?.onControlEnter?.length
@@ -6430,6 +6685,10 @@ function bindKeyboardSubmitEvents(element, node, key, env) {
6430
6685
  if (!eventName || !node.events?.[eventName]?.length) {
6431
6686
  return;
6432
6687
  }
6688
+ if (eventName !== "onShiftEnter") {
6689
+ event.preventDefault();
6690
+ event.stopPropagation();
6691
+ }
6433
6692
  void env.dispatchWidgetEvent(node, eventName, key, env.getActionInputValueForElement(element, node), null);
6434
6693
  });
6435
6694
  }
@@ -6501,6 +6760,11 @@ function bindWidgetEvents(root, env) {
6501
6760
  }
6502
6761
  if (element instanceof HTMLInputElement && element.dataset.widget === "edit") {
6503
6762
  bindKeyboardSubmitEvents(element, node, key, env);
6763
+ if (node.events?.onPaste?.length) {
6764
+ element.addEventListener("paste", (event) => {
6765
+ void getPasteInputValue(event).then((inputValue) => env.dispatchWidgetEvent(node, "onPaste", key, inputValue, null));
6766
+ });
6767
+ }
6504
6768
  element.addEventListener("input", () => {
6505
6769
  const state = env.stateByKey.get(key);
6506
6770
  if (state) {
@@ -6519,6 +6783,11 @@ function bindWidgetEvents(root, env) {
6519
6783
  }
6520
6784
  if (element instanceof HTMLTextAreaElement && element.dataset.widget === "textarea") {
6521
6785
  bindKeyboardSubmitEvents(element, node, key, env);
6786
+ if (node.events?.onPaste?.length) {
6787
+ element.addEventListener("paste", (event) => {
6788
+ void getPasteInputValue(event).then((inputValue) => env.dispatchWidgetEvent(node, "onPaste", key, inputValue, null));
6789
+ });
6790
+ }
6522
6791
  element.addEventListener("input", () => {
6523
6792
  const state = env.stateByKey.get(key);
6524
6793
  if (state) {
@@ -6984,6 +7253,7 @@ var RuntimeRenderer = class {
6984
7253
  i18n;
6985
7254
  actionsMap;
6986
7255
  requestsMap;
7256
+ routes;
6987
7257
  sseConfigs;
6988
7258
  systemEvents;
6989
7259
  resourceManager;
@@ -7003,6 +7273,7 @@ var RuntimeRenderer = class {
7003
7273
  vars = /* @__PURE__ */ new Map();
7004
7274
  resizeHandler = null;
7005
7275
  pointerHandler = null;
7276
+ popstateHandler = null;
7006
7277
  eventSources = [];
7007
7278
  referenceRuntime;
7008
7279
  networkRuntime;
@@ -7015,6 +7286,11 @@ var RuntimeRenderer = class {
7015
7286
  activeConfirmModal = null;
7016
7287
  lastPointer = { x: 24, y: 24 };
7017
7288
  pendingResetModalIds = /* @__PURE__ */ new Set();
7289
+ pendingScrollAction = null;
7290
+ pendingScrollFrameId = null;
7291
+ activeScrollAnimationFrameId = null;
7292
+ pendingCursorReference = null;
7293
+ pendingCursorFrameId = null;
7018
7294
  constructor(description, options = {}) {
7019
7295
  this.description = deepClone(description);
7020
7296
  this.resourceManager = options.resourceManager ?? null;
@@ -7028,6 +7304,8 @@ var RuntimeRenderer = class {
7028
7304
  this.theme = options.theme === "light" ? "light" : "dark";
7029
7305
  this.actionsMap = options.actionsMap ?? this.resourceManager?.getActionsMap() ?? {};
7030
7306
  this.requestsMap = options.requestsMap ?? this.resourceManager?.getRequestsMap() ?? {};
7307
+ const resolvedRoutes = options.routes ?? this.resourceManager?.getRoutes() ?? [];
7308
+ this.routes = Array.isArray(resolvedRoutes) ? resolvedRoutes.map((route) => ({ ...route })) : Array.isArray(resolvedRoutes?.routes) ? resolvedRoutes.routes.map((route) => ({ ...route })) : [];
7031
7309
  const resolvedSseConfigs = options.sseConfigs ?? this.resourceManager?.getSseConfigs() ?? [];
7032
7310
  this.sseConfigs = Array.isArray(resolvedSseConfigs) ? resolvedSseConfigs : resolvedSseConfigs ? [resolvedSseConfigs] : [];
7033
7311
  this.systemEvents = options.systemEvents ?? this.resourceManager?.getSystemEvents() ?? {};
@@ -7041,12 +7319,15 @@ var RuntimeRenderer = class {
7041
7319
  stateByKey: this.stateByKey,
7042
7320
  nodeById: this.nodeById,
7043
7321
  nodeByKey: this.nodeByKey,
7322
+ getUiResource: (ref) => this.resourceManager?.getUi(ref) ?? null,
7044
7323
  getAppValue: (name) => {
7045
7324
  switch (name) {
7046
7325
  case "language":
7047
7326
  return this.language;
7048
7327
  case "theme":
7049
7328
  return this.theme;
7329
+ case "isMobile":
7330
+ return typeof window === "undefined" ? false : isMobileViewport();
7050
7331
  case "urlPath":
7051
7332
  return typeof window === "undefined" ? "" : window.location.pathname;
7052
7333
  default:
@@ -7111,9 +7392,14 @@ var RuntimeRenderer = class {
7111
7392
  assignReference: (reference, inputValue, currentValue, options2) => this.referenceRuntime.assignReference(reference, inputValue, currentValue, options2),
7112
7393
  resolveMappedValue: (template, currentValue, responseValue) => this.referenceRuntime.resolveMappedValue(template, currentValue, responseValue),
7113
7394
  setWidgetEnabled: (widgetId, enabled) => this.setWidgetEnabled(widgetId, enabled),
7395
+ setWidgetVisible: (widgetId, visible) => this.setWidgetVisible(widgetId, visible),
7114
7396
  clearWidget: (widgetId) => this.clearWidget(widgetId),
7115
7397
  clearListElementState: (listKey) => this.clearListElementState(listKey),
7116
7398
  focusWidget: (reference) => this.focusWidget(reference),
7399
+ moveCursorToEnd: (reference) => this.moveCursorToEnd(reference),
7400
+ scrollWidgetToTop: (reference) => this.scrollWidgetToTop(reference),
7401
+ scrollWidgetToBottom: (reference) => this.scrollWidgetToBottom(reference),
7402
+ scrollWidgetToElementByIndex: (reference, index) => this.scrollWidgetToElementByIndex(reference, index),
7117
7403
  playAudio: (value) => this.voiceRuntime.play(value),
7118
7404
  stopPlaying: () => this.voiceRuntime.stopPlaying(),
7119
7405
  copyToClipboard: (value) => this.copyToClipboard(value),
@@ -7121,7 +7407,7 @@ var RuntimeRenderer = class {
7121
7407
  confirmModal: (args, inputValue) => this.confirmModal(args, inputValue),
7122
7408
  startRecording: () => this.voiceRuntime.startRecording(),
7123
7409
  stopRecording: () => Promise.resolve(this.voiceRuntime.stopRecording()),
7124
- startListening: () => this.voiceRuntime.startListening(),
7410
+ startListening: (args) => this.voiceRuntime.startListening(args),
7125
7411
  stopListening: () => this.voiceRuntime.stopListening(),
7126
7412
  setUiReference: (widgetId, resourceRef) => {
7127
7413
  const node = this.nodeById.get(widgetId);
@@ -7151,11 +7437,12 @@ var RuntimeRenderer = class {
7151
7437
  getInlineActions: (node) => this.getInlineActions(node),
7152
7438
  isWidgetEnabled: (node, key) => this.isWidgetEnabled(node, key)
7153
7439
  });
7154
- this.indexStaticNodes(this.description, "root");
7440
+ this.indexStaticNodes(this.getActiveDescription(), "root");
7155
7441
  this.applyRuntimeSnapshot(this.initialRuntimeSnapshot);
7156
7442
  }
7157
7443
  renderMarkup() {
7158
- const markup = `${this.renderNode(this.description, "root", null)}${this.renderGlobalOverlays()}`;
7444
+ const activeDescription = this.getActiveDescription();
7445
+ const markup = `${this.renderNode(activeDescription, "root", null)}${this.renderGlobalOverlays()}`;
7159
7446
  logRuntimeDebug(this.debugLogging, "html", markup);
7160
7447
  return markup;
7161
7448
  }
@@ -7205,6 +7492,17 @@ var RuntimeRenderer = class {
7205
7492
  };
7206
7493
  window.addEventListener("pointerdown", handlePointer);
7207
7494
  this.pointerHandler = handlePointer;
7495
+ const handlePopState = () => {
7496
+ void (async () => {
7497
+ await this.runSystemEvent("onBeforeNavigate");
7498
+ await this.rerenderRoot();
7499
+ await this.runSystemEvent("onAfterNavigate");
7500
+ })().catch((error) => {
7501
+ logRuntimeError("popstate", error);
7502
+ });
7503
+ };
7504
+ window.addEventListener("popstate", handlePopState);
7505
+ this.popstateHandler = handlePopState;
7208
7506
  return () => {
7209
7507
  if (this.resizeHandler) {
7210
7508
  window.removeEventListener("resize", this.resizeHandler);
@@ -7212,6 +7510,9 @@ var RuntimeRenderer = class {
7212
7510
  if (this.pointerHandler) {
7213
7511
  window.removeEventListener("pointerdown", this.pointerHandler);
7214
7512
  }
7513
+ if (this.popstateHandler) {
7514
+ window.removeEventListener("popstate", this.popstateHandler);
7515
+ }
7215
7516
  for (const source of this.eventSources) {
7216
7517
  source.close();
7217
7518
  }
@@ -7234,6 +7535,8 @@ var RuntimeRenderer = class {
7234
7535
  this.attachInputBehavior(this.root);
7235
7536
  this.attachWidgetEvents(this.root);
7236
7537
  restoreElementState(this.root, preservedState, this.stateByKey);
7538
+ this.applyPendingScrollAction();
7539
+ this.applyPendingCursorAction();
7237
7540
  this.activeContextMenu = adjustContextMenuPosition(this.root, this.activeContextMenu);
7238
7541
  this.pendingResetModalIds.clear();
7239
7542
  if (typeof this.onRuntimeSnapshot === "function") {
@@ -7413,7 +7716,113 @@ var RuntimeRenderer = class {
7413
7716
  this.nodeByKey.clear();
7414
7717
  this.nodeById.clear();
7415
7718
  this.stateById.clear();
7416
- this.indexStaticNodes(this.description, "root");
7719
+ this.indexStaticNodes(this.getActiveDescription(), "root");
7720
+ }
7721
+ getActiveDescription() {
7722
+ if (this.hasRoutes() && !this.isCurrentPathValid()) {
7723
+ return this.buildNotFoundDescription(this.getCurrentPathname());
7724
+ }
7725
+ return this.description;
7726
+ }
7727
+ hasRoutes() {
7728
+ return this.routes.length > 0;
7729
+ }
7730
+ isCurrentPathValid() {
7731
+ const pathname = this.getCurrentPathname();
7732
+ if (pathname === "/") {
7733
+ return true;
7734
+ }
7735
+ return this.routes.some((entry) => this.normalizePathname(entry.path) === pathname);
7736
+ }
7737
+ getCurrentPathname() {
7738
+ if (typeof window === "undefined") {
7739
+ return "/";
7740
+ }
7741
+ return this.normalizePathname(window.location.pathname);
7742
+ }
7743
+ normalizePathname(path) {
7744
+ if (!path || path === "/" || path === "/index.html") {
7745
+ return "/";
7746
+ }
7747
+ const normalized = path.startsWith("/") ? path : `/${path}`;
7748
+ return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
7749
+ }
7750
+ buildNotFoundDescription(pathname) {
7751
+ const dictionary = this.getBuiltinNotFoundTexts();
7752
+ return {
7753
+ widget: "panel",
7754
+ id: "vjt-not-found-panel",
7755
+ css: "position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:min(520px, calc(100vw - 32px));padding:28px;border-radius:20px;background:color-mix(in srgb, var(--vjt-surface) 92%, black 8%);border:1px solid var(--vjt-border);box-shadow:var(--vjt-shadow);",
7756
+ child: {
7757
+ widget: "container-layout",
7758
+ type: "vertical",
7759
+ children: [
7760
+ {
7761
+ minHeightPx: 64,
7762
+ maxHeightPx: 64,
7763
+ marginBottomPx: 8,
7764
+ child: {
7765
+ widget: "static-text",
7766
+ heading: "h1",
7767
+ text: dictionary.title,
7768
+ css: "font-size:42px;font-weight:800;letter-spacing:0.04em;text-align:center;justify-content:center;"
7769
+ }
7770
+ },
7771
+ {
7772
+ minHeightPx: 36,
7773
+ maxHeightPx: 36,
7774
+ marginBottomPx: 8,
7775
+ child: {
7776
+ widget: "static-text",
7777
+ heading: "h3",
7778
+ text: dictionary.caption,
7779
+ css: "text-align:center;justify-content:center;"
7780
+ }
7781
+ },
7782
+ {
7783
+ minHeightPx: 64,
7784
+ marginBottomPx: 16,
7785
+ child: {
7786
+ widget: "static-text",
7787
+ text: `${dictionary.note} ${pathname}`,
7788
+ css: "text-align:center;justify-content:center;color:var(--vjt-muted);"
7789
+ }
7790
+ },
7791
+ {
7792
+ minHeightPx: 52,
7793
+ maxHeightPx: 52,
7794
+ child: {
7795
+ widget: "button",
7796
+ text: dictionary.home,
7797
+ events: {
7798
+ onClick: [
7799
+ {
7800
+ action: "navigate /"
7801
+ }
7802
+ ]
7803
+ }
7804
+ }
7805
+ }
7806
+ ]
7807
+ }
7808
+ };
7809
+ }
7810
+ getBuiltinNotFoundTexts() {
7811
+ const language = this.language.toLowerCase();
7812
+ if (language.startsWith("ru")) {
7813
+ return {
7814
+ title: "404",
7815
+ caption: "\u0421\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430",
7816
+ note: "\u0417\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u043D\u044B\u0439 \u043F\u0443\u0442\u044C \u043D\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442:",
7817
+ home: "\u041D\u0430 \u0433\u043B\u0430\u0432\u043D\u0443\u044E"
7818
+ };
7819
+ }
7820
+ return {
7821
+ title: "404",
7822
+ caption: "Page not found",
7823
+ note: "The requested path does not exist:",
7824
+ home: "Go home"
7825
+ };
7417
7826
  }
7418
7827
  getUiReferenceTarget(node, key) {
7419
7828
  if (node.widget !== "ui-reference") {
@@ -7648,6 +8057,9 @@ var RuntimeRenderer = class {
7648
8057
  existing.id = stateId2;
7649
8058
  this.stateById.set(stateId2, existing);
7650
8059
  }
8060
+ if (existing.visible === void 0) {
8061
+ existing.visible = normalizeBoolean(node.visible, true);
8062
+ }
7651
8063
  this.syncStateDefinition(existing, node);
7652
8064
  return existing;
7653
8065
  }
@@ -7698,7 +8110,8 @@ var RuntimeRenderer = class {
7698
8110
  id: stateId,
7699
8111
  name: node.name,
7700
8112
  text: typeof node.text === "string" ? node.text : void 0,
7701
- url: "url" in node ? typeof node.url === "string" ? node.url : "" : void 0
8113
+ url: "url" in node ? typeof node.url === "string" ? node.url : "" : void 0,
8114
+ base64: "base64" in node ? typeof node.base64 === "string" ? node.base64 : "" : void 0
7702
8115
  };
7703
8116
  break;
7704
8117
  case "conditional-container":
@@ -7871,6 +8284,20 @@ var RuntimeRenderer = class {
7871
8284
  }
7872
8285
  return `??${key}??`;
7873
8286
  }
8287
+ resolveTextValue(value, itemContext) {
8288
+ if (typeof value !== "string") {
8289
+ return value ?? "";
8290
+ }
8291
+ if (value.startsWith("$ref:")) {
8292
+ const resolved = this.referenceRuntime.resolveReference(value.slice(5), itemContext, null);
8293
+ if (resolved == null) {
8294
+ return "";
8295
+ }
8296
+ const resolvedText = typeof resolved === "string" ? resolved : typeof resolved === "number" || typeof resolved === "boolean" || typeof resolved === "bigint" ? String(resolved) : JSON.stringify(resolved);
8297
+ return this.resolveI18nValue(resolvedText);
8298
+ }
8299
+ return this.resolveI18nValue(value);
8300
+ }
7874
8301
  buildWidgetDataAttributes(key, node) {
7875
8302
  const parts = [` data-widget-key="${escapeHtml(key)}"`];
7876
8303
  if (node.id) {
@@ -7889,6 +8316,7 @@ var RuntimeRenderer = class {
7889
8316
  escapeHtml,
7890
8317
  buildStyleString: (node) => this.buildStyleString(node),
7891
8318
  resolveI18nValue: (value) => this.resolveI18nValue(value),
8319
+ resolveTextValue: (value, itemContext) => this.resolveTextValue(value, itemContext),
7892
8320
  getUiResource: (ref) => this.resourceManager?.getUi(ref) ?? null,
7893
8321
  resolveReference: (reference, currentValue, responseValue) => this.referenceRuntime.resolveReference(reference, currentValue, responseValue),
7894
8322
  buildWidgetDataAttributes: (key, node) => this.buildWidgetDataAttributes(key, node),
@@ -7920,6 +8348,9 @@ var RuntimeRenderer = class {
7920
8348
  const key = itemContext ? this.resolveItemNodeKey(node, itemContext.listId, itemContext.index, path) : this.resolveNodeKey(node, path);
7921
8349
  this.nodeByKey.set(key, node);
7922
8350
  const state = this.ensureWidgetState(node, key);
8351
+ if ((state.visible ?? normalizeBoolean(node.visible, true)) === false) {
8352
+ return "";
8353
+ }
7923
8354
  switch (node.widget) {
7924
8355
  case "adaptive-layout":
7925
8356
  return renderAdaptiveLayout(env, node, state, key, path, itemContext);
@@ -8409,6 +8840,13 @@ var RuntimeRenderer = class {
8409
8840
  }
8410
8841
  state.enabled = enabled;
8411
8842
  }
8843
+ setWidgetVisible(widgetId, visible) {
8844
+ const state = this.stateById.get(widgetId);
8845
+ if (!state) {
8846
+ return;
8847
+ }
8848
+ state.visible = visible;
8849
+ }
8412
8850
  clearListElementState(listKey) {
8413
8851
  this.referenceRuntime.clearListElementState(listKey);
8414
8852
  }
@@ -8434,6 +8872,199 @@ var RuntimeRenderer = class {
8434
8872
  nestedTarget.focus({ preventScroll: true });
8435
8873
  }
8436
8874
  }
8875
+ moveCursorToEnd(reference) {
8876
+ this.pendingCursorReference = reference;
8877
+ if (typeof window === "undefined" || this.pendingCursorFrameId !== null) {
8878
+ return;
8879
+ }
8880
+ this.pendingCursorFrameId = window.requestAnimationFrame(() => {
8881
+ this.pendingCursorFrameId = null;
8882
+ this.applyPendingCursorAction();
8883
+ });
8884
+ }
8885
+ scrollWidgetToTop(reference) {
8886
+ this.pendingScrollAction = { kind: "top", reference };
8887
+ this.schedulePendingScrollAction();
8888
+ }
8889
+ scrollWidgetToBottom(reference) {
8890
+ this.pendingScrollAction = { kind: "bottom", reference };
8891
+ this.schedulePendingScrollAction();
8892
+ }
8893
+ scrollWidgetToElementByIndex(reference, indexValue) {
8894
+ this.pendingScrollAction = { kind: "index", reference, indexValue };
8895
+ this.schedulePendingScrollAction();
8896
+ }
8897
+ schedulePendingScrollAction() {
8898
+ if (typeof window === "undefined" || this.pendingScrollFrameId !== null) {
8899
+ return;
8900
+ }
8901
+ this.pendingScrollFrameId = window.requestAnimationFrame(() => {
8902
+ this.pendingScrollFrameId = null;
8903
+ this.applyPendingScrollAction();
8904
+ });
8905
+ }
8906
+ applyPendingScrollAction() {
8907
+ if (!this.pendingScrollAction) {
8908
+ return;
8909
+ }
8910
+ const pending = this.pendingScrollAction;
8911
+ if (pending.kind === "top") {
8912
+ this.animateScrollTop(pending.reference, 0);
8913
+ this.pendingScrollAction = null;
8914
+ return;
8915
+ }
8916
+ if (pending.kind === "bottom") {
8917
+ const element = this.findScrollableWidgetElement(pending.reference);
8918
+ if (!element) {
8919
+ return;
8920
+ }
8921
+ this.animateScrollTop(pending.reference, element.scrollHeight);
8922
+ this.pendingScrollAction = null;
8923
+ return;
8924
+ }
8925
+ const index = this.toScrollableIndex(pending.indexValue);
8926
+ if (index === null) {
8927
+ this.pendingScrollAction = null;
8928
+ return;
8929
+ }
8930
+ this.performScrollToElementByIndex(pending.reference, index);
8931
+ this.pendingScrollAction = null;
8932
+ }
8933
+ performScrollToElementByIndex(reference, index) {
8934
+ const element = this.findScrollableWidgetElement(reference);
8935
+ if (!element) {
8936
+ return;
8937
+ }
8938
+ if (element instanceof HTMLSelectElement) {
8939
+ const option = element.options.item(index);
8940
+ if (!option) {
8941
+ return;
8942
+ }
8943
+ const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
8944
+ this.animateScrollTop(reference, Math.max(0, Math.min(option.offsetTop, maxScrollTop)));
8945
+ return;
8946
+ }
8947
+ const listElement = element.querySelector(`.vjt-list-element[data-list-index="${index}"], .vjt-grid-cell[data-list-index="${index}"]`);
8948
+ if (listElement instanceof HTMLElement) {
8949
+ const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
8950
+ const containerRect = element.getBoundingClientRect();
8951
+ const itemRect = listElement.getBoundingClientRect();
8952
+ const targetTop = element.scrollTop + (itemRect.top - containerRect.top);
8953
+ const remainingContentHeight = element.scrollHeight - targetTop;
8954
+ const nextScrollTop = remainingContentHeight > element.clientHeight ? Math.max(0, Math.min(targetTop, maxScrollTop)) : maxScrollTop;
8955
+ this.animateScrollTop(reference, nextScrollTop);
8956
+ }
8957
+ }
8958
+ animateScrollTop(reference, targetTop) {
8959
+ const resolveLiveElement = () => {
8960
+ const resolved = this.findScrollableWidgetElement(reference);
8961
+ return resolved instanceof HTMLElement ? resolved : null;
8962
+ };
8963
+ const getMaxScrollTop = (element) => Math.max(
8964
+ 0,
8965
+ element.scrollHeight - element.clientHeight
8966
+ );
8967
+ const initialElement = resolveLiveElement();
8968
+ if (!initialElement) {
8969
+ return;
8970
+ }
8971
+ if (typeof window === "undefined") {
8972
+ initialElement.scrollTop = Math.max(0, Math.min(targetTop, getMaxScrollTop(initialElement)));
8973
+ return;
8974
+ }
8975
+ if (this.activeScrollAnimationFrameId !== null) {
8976
+ window.cancelAnimationFrame(this.activeScrollAnimationFrameId);
8977
+ this.activeScrollAnimationFrameId = null;
8978
+ }
8979
+ const startTop = initialElement.scrollTop;
8980
+ const clampedTargetTop = Math.max(0, Math.min(targetTop, getMaxScrollTop(initialElement)));
8981
+ const delta = clampedTargetTop - startTop;
8982
+ if (Math.abs(delta) < 1) {
8983
+ initialElement.scrollTop = clampedTargetTop;
8984
+ return;
8985
+ }
8986
+ const durationMs = 300;
8987
+ const startTime = window.performance.now();
8988
+ const easeInOutCubic = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
8989
+ const tick = (now) => {
8990
+ const liveElement = resolveLiveElement();
8991
+ if (!liveElement) {
8992
+ this.activeScrollAnimationFrameId = null;
8993
+ return;
8994
+ }
8995
+ const elapsed = now - startTime;
8996
+ const progress = Math.min(1, elapsed / durationMs);
8997
+ const liveTargetTop = Math.max(0, Math.min(clampedTargetTop, getMaxScrollTop(liveElement)));
8998
+ liveElement.scrollTop = startTop + (liveTargetTop - startTop) * easeInOutCubic(progress);
8999
+ if (progress < 1) {
9000
+ this.activeScrollAnimationFrameId = window.requestAnimationFrame(tick);
9001
+ } else {
9002
+ liveElement.scrollTop = liveTargetTop;
9003
+ this.activeScrollAnimationFrameId = null;
9004
+ }
9005
+ };
9006
+ this.activeScrollAnimationFrameId = window.requestAnimationFrame(tick);
9007
+ }
9008
+ applyPendingCursorAction() {
9009
+ if (!(this.root instanceof HTMLElement) || !this.pendingCursorReference) {
9010
+ return;
9011
+ }
9012
+ const reference = this.pendingCursorReference;
9013
+ const widgetId = reference.split(".")[0]?.trim();
9014
+ if (!widgetId) {
9015
+ this.pendingCursorReference = null;
9016
+ return;
9017
+ }
9018
+ const directTarget = this.root.querySelector(`#${CSS.escape(widgetId)}`);
9019
+ const widgetRoot = directTarget ?? this.root.querySelector(`[data-widget-id="${CSS.escape(widgetId)}"]`);
9020
+ if (!widgetRoot) {
9021
+ return;
9022
+ }
9023
+ const inputTarget = widgetRoot instanceof HTMLInputElement || widgetRoot instanceof HTMLTextAreaElement ? widgetRoot : widgetRoot.querySelector("input, textarea");
9024
+ if (!inputTarget || inputTarget.disabled) {
9025
+ this.pendingCursorReference = null;
9026
+ return;
9027
+ }
9028
+ if (typeof inputTarget.focus === "function") {
9029
+ inputTarget.focus({ preventScroll: true });
9030
+ }
9031
+ if (typeof inputTarget.setSelectionRange === "function") {
9032
+ const end = inputTarget.value.length;
9033
+ inputTarget.setSelectionRange(end, end);
9034
+ }
9035
+ this.pendingCursorReference = null;
9036
+ }
9037
+ findScrollableWidgetElement(reference) {
9038
+ if (!(this.root instanceof HTMLElement)) {
9039
+ return null;
9040
+ }
9041
+ const widgetId = reference.split(".")[0]?.trim();
9042
+ if (!widgetId) {
9043
+ return null;
9044
+ }
9045
+ const directTarget = this.root.querySelector(`#${CSS.escape(widgetId)}`);
9046
+ if (directTarget) {
9047
+ return directTarget;
9048
+ }
9049
+ for (const element of Array.from(this.root.querySelectorAll("[data-widget-id]"))) {
9050
+ if (element.dataset.widgetId === widgetId) {
9051
+ return element;
9052
+ }
9053
+ }
9054
+ return null;
9055
+ }
9056
+ toScrollableIndex(value) {
9057
+ if (typeof value === "number" && Number.isFinite(value)) {
9058
+ return Math.max(0, Math.trunc(value));
9059
+ }
9060
+ if (typeof value === "string" && value.trim().length > 0) {
9061
+ const parsed = Number.parseInt(value, 10);
9062
+ if (Number.isFinite(parsed)) {
9063
+ return Math.max(0, parsed);
9064
+ }
9065
+ }
9066
+ return null;
9067
+ }
8437
9068
  };
8438
9069
  function renderJson(description, options = {}) {
8439
9070
  const renderer = new RuntimeRenderer(description, options);
@@ -8449,6 +9080,7 @@ var ResourceManager = class {
8449
9080
  ui = /* @__PURE__ */ new Map();
8450
9081
  actions = {};
8451
9082
  requests = {};
9083
+ routes = [];
8452
9084
  sse = {};
8453
9085
  systemEvents = {};
8454
9086
  i18n = {};
@@ -8459,6 +9091,7 @@ var ResourceManager = class {
8459
9091
  }
8460
9092
  this.actions = { ...input.actions ?? {} };
8461
9093
  this.requests = { ...input.requests ?? {} };
9094
+ this.routes = Array.isArray(input.routes) ? input.routes.map((route) => ({ ...route })) : Array.isArray(input.routes?.routes) ? input.routes.routes.map((route) => ({ ...route })) : [];
8462
9095
  this.sse = { ...input.sse ?? {} };
8463
9096
  this.systemEvents = { ...input.systemEvents ?? {} };
8464
9097
  this.i18n = { ...input.i18n ?? {} };
@@ -8476,6 +9109,9 @@ var ResourceManager = class {
8476
9109
  getRequestsMap() {
8477
9110
  return this.requests;
8478
9111
  }
9112
+ getRoutes() {
9113
+ return this.routes.map((route) => ({ ...route }));
9114
+ }
8479
9115
  getSseConfigs() {
8480
9116
  return Object.values(this.sse);
8481
9117
  }