@vortexm/vjt 0.1.7 → 0.1.8

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.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { renderApp, renderJson } from './lib/render.js';
2
2
  export { ResourceManager } from './lib/resource-manager.js';
3
3
  export { DEFAULT_STYLE_MAP } from './lib/default-styles.js';
4
- export type { ActionDefinition, ActionMap, AdaptiveLayoutNode, BaseNode, ButtonNode, CheckboxNode, ComboboxNode, ConditionExpression, ConditionalContainerNode, ContextMenuNode, ContainerCell, ContainerLayoutNode, CellConstraints, DescriptionNode, GridViewNode, HeadingTag, I18nMap, ImageNode, LayoutType, LinkNode, ListElementContext, ListNode, ListboxNode, MdTextNode, ModalWindowNode, MountJsonOptions, OverlayContainerNode, OverlayLayer, PanelNode, PrimitiveRequestType, RadioGroupNode, RequestDefinition, RequestMap, RequestSchema, RenderJsonOptions, RuntimeSnapshot, SseConfig, SseConfigInput, SseEventDefinition, SpoilerNode, SplitterNode, StaticTextNode, StyleMap, SystemEventName, TabsNode, TextAlign, TextareaNode, UiReferenceNode, Theme, VerticalAlign, WidgetEventName, WidgetState } from './lib/types.js';
4
+ export type { ActionDefinition, ActionMap, AdaptiveLayoutNode, BaseNode, ButtonNode, CheckboxNode, ComboboxNode, ConditionExpression, ConditionalContainerNode, ContextMenuNode, ContainerCell, ContainerLayoutNode, CellConstraints, DescriptionNode, GridViewNode, HeadingTag, I18nMap, ImageNode, LayoutType, LinkNode, ListElementContext, ListNode, ListboxNode, MdTextNode, ModalWindowNode, MountJsonOptions, OverlayContainerNode, OverlayLayer, PanelNode, PrimitiveRequestType, RadioGroupNode, RouteDefinition, RoutesConfigInput, RequestDefinition, RequestMap, RequestSchema, RenderJsonOptions, RuntimeSnapshot, SseConfig, SseConfigInput, SseEventDefinition, SpoilerNode, SplitterNode, StaticTextNode, StyleMap, SystemEventName, TabsNode, TextAlign, TextareaNode, UiReferenceNode, Theme, VerticalAlign, WidgetEventName, WidgetState } from './lib/types.js';
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("."));
@@ -4374,8 +4405,12 @@ var ReferenceRuntime = class {
4374
4405
  return readPath(state, field.split("."));
4375
4406
  }
4376
4407
  }
4377
- assignReference(reference, inputValue, _currentValue, options) {
4378
- if (reference.startsWith("current.") || hasBlockedReferenceSegment(reference)) {
4408
+ assignReference(reference, inputValue, currentValue, options) {
4409
+ if (hasBlockedReferenceSegment(reference)) {
4410
+ return;
4411
+ }
4412
+ if (reference.startsWith("current.")) {
4413
+ this.assignCurrentReference(reference.slice(8), inputValue, currentValue);
4379
4414
  return;
4380
4415
  }
4381
4416
  if (reference.startsWith("cookies.")) {
@@ -4443,6 +4478,19 @@ var ReferenceRuntime = class {
4443
4478
  break;
4444
4479
  }
4445
4480
  }
4481
+ assignCurrentReference(reference, inputValue, currentValue) {
4482
+ const path = reference.split(".").filter(Boolean);
4483
+ if (path.length === 0) {
4484
+ return;
4485
+ }
4486
+ if (this.isListElementContext(currentValue)) {
4487
+ writePath(currentValue.descriptor, path, inputValue);
4488
+ return;
4489
+ }
4490
+ if (isPlainObject2(currentValue)) {
4491
+ writePath(currentValue, path, inputValue);
4492
+ }
4493
+ }
4446
4494
  clearListElementState(listKey) {
4447
4495
  const generatedPrefix = `${sanitizeIdFragment(listKey)}Element`;
4448
4496
  for (const key of Array.from(this.host.stateByKey.keys())) {
@@ -4493,7 +4541,7 @@ var ReferenceRuntime = class {
4493
4541
  isListElementContext(value) {
4494
4542
  return isPlainObject2(value) && value.kind === "list-element" && typeof value.listId === "string" && typeof value.index === "number" && isPlainObject2(value.descriptor) && typeof value.descriptor.widget === "string";
4495
4543
  }
4496
- findNamedWidget(node, name) {
4544
+ findNamedWidget(node, name, currentValue) {
4497
4545
  if (!node) {
4498
4546
  return null;
4499
4547
  }
@@ -4502,12 +4550,12 @@ var ReferenceRuntime = class {
4502
4550
  }
4503
4551
  switch (node.widget) {
4504
4552
  case "adaptive-layout":
4505
- return this.findNamedWidget(node.desktop, name) ?? this.findNamedWidget(node.mobile, name);
4553
+ return this.findNamedWidget(node.desktop, name, currentValue) ?? this.findNamedWidget(node.mobile, name, currentValue);
4506
4554
  case "container-layout":
4507
4555
  if (node.type === "grid") {
4508
4556
  for (const row of node.grid?.rows ?? []) {
4509
4557
  for (const column of row.columns ?? []) {
4510
- const match = this.findNamedWidget(column.child, name);
4558
+ const match = this.findNamedWidget(column.child, name, currentValue);
4511
4559
  if (match) {
4512
4560
  return match;
4513
4561
  }
@@ -4516,20 +4564,20 @@ var ReferenceRuntime = class {
4516
4564
  return null;
4517
4565
  }
4518
4566
  for (const cell of node.children ?? []) {
4519
- const match = this.findNamedWidget(cell.child, name);
4567
+ const match = this.findNamedWidget(cell.child, name, currentValue);
4520
4568
  if (match) {
4521
4569
  return match;
4522
4570
  }
4523
4571
  }
4524
4572
  return null;
4525
4573
  case "panel":
4526
- return this.findNamedWidget(node.child, name);
4574
+ return this.findNamedWidget(node.child, name, currentValue);
4527
4575
  case "conditional-container":
4528
- return this.findNamedWidget(node.default, name) ?? this.findNamedWidget(node.alternative, name);
4576
+ return this.findNamedWidget(node.default, name, currentValue) ?? this.findNamedWidget(node.alternative, name, currentValue);
4529
4577
  case "list":
4530
4578
  case "grid-view":
4531
4579
  for (const element of node.elements ?? []) {
4532
- const match = this.findNamedWidget(element, name);
4580
+ const match = this.findNamedWidget(element, name, currentValue);
4533
4581
  if (match) {
4534
4582
  return match;
4535
4583
  }
@@ -4537,7 +4585,7 @@ var ReferenceRuntime = class {
4537
4585
  return null;
4538
4586
  case "overlay-container":
4539
4587
  for (const layer of node.layers ?? []) {
4540
- const match = this.findNamedWidget(layer.child, name);
4588
+ const match = this.findNamedWidget(layer.child, name, currentValue);
4541
4589
  if (match) {
4542
4590
  return match;
4543
4591
  }
@@ -4545,20 +4593,40 @@ var ReferenceRuntime = class {
4545
4593
  return null;
4546
4594
  case "tabs":
4547
4595
  for (const tab of node.tabs ?? []) {
4548
- const match = this.findNamedWidget(tab.content, name);
4596
+ const match = this.findNamedWidget(tab.content, name, currentValue);
4549
4597
  if (match) {
4550
4598
  return match;
4551
4599
  }
4552
4600
  }
4553
4601
  return null;
4554
4602
  case "spoiler":
4555
- return typeof node.content === "string" ? null : this.findNamedWidget(node.content, name);
4603
+ return typeof node.content === "string" ? null : this.findNamedWidget(node.content, name, currentValue);
4604
+ case "ui-reference": {
4605
+ const referenced = currentValue ? this.resolveUiReferenceNode(node, currentValue) : null;
4606
+ return referenced ? this.findNamedWidget(referenced, name, currentValue) : null;
4607
+ }
4556
4608
  case "modal-window":
4557
- return this.findNamedWidget(node.child, name);
4609
+ return this.findNamedWidget(node.child, name, currentValue);
4558
4610
  default:
4559
4611
  return null;
4560
4612
  }
4561
4613
  }
4614
+ resolveListDescriptorNode(node, currentValue) {
4615
+ if (node.widget !== "ui-reference") {
4616
+ return node;
4617
+ }
4618
+ return this.resolveUiReferenceNode(node, currentValue) ?? node;
4619
+ }
4620
+ resolveUiReferenceNode(node, currentValue) {
4621
+ if (node.widget !== "ui-reference") {
4622
+ return null;
4623
+ }
4624
+ const resolvedValue = typeof node.ref === "string" && node.ref.startsWith("$ref:") ? this.resolveReference(node.ref.slice(5), currentValue, null) : node.ref;
4625
+ if (typeof resolvedValue !== "string" || resolvedValue.length === 0) {
4626
+ return null;
4627
+ }
4628
+ return this.host.getUiResource(resolvedValue);
4629
+ }
4562
4630
  };
4563
4631
 
4564
4632
  // src/lib/action-runtime.ts
@@ -4580,6 +4648,9 @@ var ActionRuntime = class {
4580
4648
  clearWidget;
4581
4649
  clearListElementState;
4582
4650
  focusWidget;
4651
+ scrollWidgetToTop;
4652
+ scrollWidgetToBottom;
4653
+ scrollWidgetToElementByIndex;
4583
4654
  playAudio;
4584
4655
  stopPlaying;
4585
4656
  copyToClipboard;
@@ -4617,6 +4688,9 @@ var ActionRuntime = class {
4617
4688
  this.clearWidget = options.clearWidget;
4618
4689
  this.clearListElementState = options.clearListElementState;
4619
4690
  this.focusWidget = options.focusWidget;
4691
+ this.scrollWidgetToTop = options.scrollWidgetToTop;
4692
+ this.scrollWidgetToBottom = options.scrollWidgetToBottom;
4693
+ this.scrollWidgetToElementByIndex = options.scrollWidgetToElementByIndex;
4620
4694
  this.playAudio = options.playAudio;
4621
4695
  this.stopPlaying = options.stopPlaying;
4622
4696
  this.copyToClipboard = options.copyToClipboard;
@@ -4678,7 +4752,7 @@ var ActionRuntime = class {
4678
4752
  let output = rawOutput;
4679
4753
  if (action.andThen?.length) {
4680
4754
  if (output !== null && output !== void 0) {
4681
- output = await this.runActions(action.andThen, output, { ...context, currentValue: output });
4755
+ output = await this.runActions(action.andThen, output, context);
4682
4756
  } else {
4683
4757
  output = null;
4684
4758
  }
@@ -4765,7 +4839,7 @@ var ActionRuntime = class {
4765
4839
  return null;
4766
4840
  }
4767
4841
  if (name === "startListening") {
4768
- await this.startListening();
4842
+ await this.startListening(action.args);
4769
4843
  return null;
4770
4844
  }
4771
4845
  if (name === "stopListening") {
@@ -4817,6 +4891,18 @@ var ActionRuntime = class {
4817
4891
  this.focusWidget(name.slice(9));
4818
4892
  return null;
4819
4893
  }
4894
+ if (name.startsWith("scrollToTop ")) {
4895
+ this.scrollWidgetToTop(name.slice(12));
4896
+ return inputValue;
4897
+ }
4898
+ if (name.startsWith("scrollToBottom ")) {
4899
+ this.scrollWidgetToBottom(name.slice(15));
4900
+ return inputValue;
4901
+ }
4902
+ if (name.startsWith("scrollToElementByIndex ")) {
4903
+ this.scrollWidgetToElementByIndex(name.slice(23), inputValue);
4904
+ return inputValue;
4905
+ }
4820
4906
  if (name.startsWith("setUi ")) {
4821
4907
  const widgetId = name.slice(6);
4822
4908
  const resourceRef = typeof inputValue === "string" ? inputValue : "";
@@ -4827,14 +4913,62 @@ var ActionRuntime = class {
4827
4913
  }
4828
4914
  if (name.startsWith("get ")) {
4829
4915
  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);
4916
+ if (Array.isArray(inputValue) && this.isCurrentScopedReference(reference)) {
4917
+ return inputValue.map((entry) => this.resolveReference(reference, entry, context.responseValue)).filter((entry) => entry !== null && entry !== void 0);
4832
4918
  }
4833
4919
  return this.resolveReference(reference, context.currentValue, context.responseValue);
4834
4920
  }
4921
+ if (name.startsWith("count ")) {
4922
+ return this.countValue(this.resolveReference(name.slice(6), context.currentValue, context.responseValue));
4923
+ }
4924
+ if (name === "indexOf") {
4925
+ if (isListElementLike(context.currentValue)) {
4926
+ return context.currentValue.index;
4927
+ }
4928
+ if (typeof context.currentIndex === "number") {
4929
+ return context.currentIndex;
4930
+ }
4931
+ return null;
4932
+ }
4933
+ if (name === "foreach") {
4934
+ if (Array.isArray(inputValue) && action.andThen?.length) {
4935
+ const currentList = inputValue;
4936
+ for (let index = 0; index < currentList.length; index += 1) {
4937
+ const entry = currentList[index];
4938
+ await this.runActions(action.andThen, entry, {
4939
+ ...context,
4940
+ currentValue: entry,
4941
+ currentList,
4942
+ currentIndex: index
4943
+ });
4944
+ }
4945
+ }
4946
+ return inputValue;
4947
+ }
4948
+ if (name === "inc") {
4949
+ return this.toNumber(inputValue) + 1;
4950
+ }
4951
+ if (name === "dec") {
4952
+ return this.toNumber(inputValue) - 1;
4953
+ }
4835
4954
  if (name.startsWith("equals ")) {
4836
4955
  return this.resolveReference(name.slice(7), context.currentValue, context.responseValue) === action.args;
4837
4956
  }
4957
+ if (name.startsWith("greaterThan ")) {
4958
+ return this.toNumber(inputValue) > this.toNumber(this.resolveReference(name.slice(12), context.currentValue, context.responseValue));
4959
+ }
4960
+ if (name.startsWith("greaterThanOrEquals ")) {
4961
+ return this.toNumber(inputValue) >= this.toNumber(this.resolveReference(name.slice(20), context.currentValue, context.responseValue));
4962
+ }
4963
+ if (name.startsWith("lessThan ")) {
4964
+ return this.toNumber(inputValue) < this.toNumber(this.resolveReference(name.slice(9), context.currentValue, context.responseValue));
4965
+ }
4966
+ if (name.startsWith("lessThanOrEquals ")) {
4967
+ return this.toNumber(inputValue) <= this.toNumber(this.resolveReference(name.slice(17), context.currentValue, context.responseValue));
4968
+ }
4969
+ if (name.startsWith("and ")) {
4970
+ return Boolean(inputValue) && Boolean(this.resolveReference(name.slice(4), context.currentValue, context.responseValue));
4971
+ }
4838
4972
  if (name.startsWith("isEmpty ")) {
4839
4973
  return this.isEmptyReference(name.slice(8), context.currentValue, context.responseValue);
4840
4974
  }
@@ -4852,8 +4986,8 @@ var ActionRuntime = class {
4852
4986
  }
4853
4987
  if (name.startsWith("filter ")) {
4854
4988
  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));
4989
+ if (Array.isArray(inputValue) && this.isCurrentScopedReference(reference)) {
4990
+ return inputValue.filter((entry) => this.resolveReference(reference, entry, context.responseValue));
4857
4991
  }
4858
4992
  const value = this.resolveReference(reference, context.currentValue, context.responseValue);
4859
4993
  return value ? inputValue : null;
@@ -4878,6 +5012,12 @@ var ActionRuntime = class {
4878
5012
  this.clearListElementState(listState.key);
4879
5013
  return listState.elements;
4880
5014
  }
5015
+ const inputListState = this.asListWidgetState(inputValue);
5016
+ if (inputListState && Array.isArray(inputListState.elements)) {
5017
+ inputListState.elements.push(valueToAdd);
5018
+ this.clearListElementState(inputListState.key);
5019
+ return inputListState.elements;
5020
+ }
4881
5021
  if (context.currentList && typeof context.currentIndex === "number") {
4882
5022
  return context.currentIndex === context.currentList.length - 1 ? [this.unwrapListValue(inputValue), valueToAdd] : this.unwrapListValue(inputValue);
4883
5023
  }
@@ -4902,6 +5042,12 @@ var ActionRuntime = class {
4902
5042
  return listState.elements;
4903
5043
  }
4904
5044
  }
5045
+ const inputListState = this.asListWidgetState(inputValue);
5046
+ if (inputListState && Array.isArray(inputListState.elements)) {
5047
+ inputListState.elements.push(valueToInsert);
5048
+ this.clearListElementState(inputListState.key);
5049
+ return inputListState.elements;
5050
+ }
4905
5051
  if (context.currentList && typeof context.currentIndex === "number") {
4906
5052
  return [this.unwrapListValue(inputValue), valueToInsert];
4907
5053
  }
@@ -4916,8 +5062,8 @@ var ActionRuntime = class {
4916
5062
  return inputValue;
4917
5063
  }
4918
5064
  if (name === "map") {
4919
- if (Array.isArray(context.currentValue)) {
4920
- return context.currentValue.map((entry) => this.resolveMappedValue(action.args, entry, context.responseValue));
5065
+ if (Array.isArray(inputValue)) {
5066
+ return inputValue.map((entry) => this.resolveMappedValue(action.args, entry, context.responseValue));
4921
5067
  }
4922
5068
  return this.resolveMappedValue(action.args, context.currentValue, context.responseValue);
4923
5069
  }
@@ -4964,6 +5110,46 @@ var ActionRuntime = class {
4964
5110
  isCurrentScopedReference(reference) {
4965
5111
  return reference === "current" || reference.startsWith("current.") || reference.startsWith("this.") || reference === "this";
4966
5112
  }
5113
+ countValue(value) {
5114
+ if (Array.isArray(value) || typeof value === "string") {
5115
+ return value.length;
5116
+ }
5117
+ if (value && typeof value === "object") {
5118
+ const widgetState = value;
5119
+ if (widgetState.widget === "list" || widgetState.widget === "grid-view") {
5120
+ return Array.isArray(widgetState.elements) ? widgetState.elements.length : 0;
5121
+ }
5122
+ if (widgetState.widget === "listbox") {
5123
+ return Array.isArray(widgetState.listboxElements) ? widgetState.listboxElements.length : 0;
5124
+ }
5125
+ if (widgetState.widget === "combobox") {
5126
+ return Array.isArray(widgetState.comboboxElements) ? widgetState.comboboxElements.length : 0;
5127
+ }
5128
+ if ("length" in widgetState && typeof widgetState.length === "number") {
5129
+ return widgetState.length;
5130
+ }
5131
+ }
5132
+ return 0;
5133
+ }
5134
+ toNumber(value) {
5135
+ if (typeof value === "number" && Number.isFinite(value)) {
5136
+ return value;
5137
+ }
5138
+ if (typeof value === "string" && value.trim().length > 0) {
5139
+ const parsed = Number.parseFloat(value);
5140
+ if (Number.isFinite(parsed)) {
5141
+ return parsed;
5142
+ }
5143
+ }
5144
+ return 0;
5145
+ }
5146
+ asListWidgetState(value) {
5147
+ if (!value || typeof value !== "object") {
5148
+ return null;
5149
+ }
5150
+ const state = value;
5151
+ return state.widget === "list" || state.widget === "grid-view" ? state : null;
5152
+ }
4967
5153
  };
4968
5154
  function isIfElseArgs(value) {
4969
5155
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -5393,7 +5579,7 @@ var MAX_CONCURRENT_PLAYERS = 5;
5393
5579
  var MAX_RECORDING_MS = 10 * 60 * 1e3;
5394
5580
  var MAX_LISTENING_SILENCE_MS = 60 * 60 * 1e3;
5395
5581
  var SILENCE_STOP_MS = 2e3;
5396
- var SPEECH_THRESHOLD = 0.035;
5582
+ var DEFAULT_SPEECH_THRESHOLD = 0.035;
5397
5583
  var RECORDING_MIME_CANDIDATES = [
5398
5584
  "audio/webm;codecs=opus",
5399
5585
  "audio/webm",
@@ -5446,6 +5632,21 @@ function blobToBase64(blob) {
5446
5632
  reader.readAsDataURL(blob);
5447
5633
  });
5448
5634
  }
5635
+ function normalizeSpeechThreshold(value) {
5636
+ if (typeof value === "number" && Number.isFinite(value)) {
5637
+ return Math.max(0, Math.min(1, value));
5638
+ }
5639
+ if (typeof value === "string" && value.trim().length > 0) {
5640
+ const parsed = Number.parseFloat(value);
5641
+ if (Number.isFinite(parsed)) {
5642
+ return Math.max(0, Math.min(1, parsed));
5643
+ }
5644
+ }
5645
+ return DEFAULT_SPEECH_THRESHOLD;
5646
+ }
5647
+ function isStartListeningArgs(value) {
5648
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5649
+ }
5449
5650
  var VoiceRuntime = class {
5450
5651
  debugLogging;
5451
5652
  triggerSystemEvent;
@@ -5542,11 +5743,12 @@ var VoiceRuntime = class {
5542
5743
  this.clearRecordingTimeout();
5543
5744
  this.mediaRecorder.stop();
5544
5745
  }
5545
- async startListening() {
5746
+ async startListening(args) {
5546
5747
  if (this.listening) {
5547
5748
  return;
5548
5749
  }
5549
5750
  try {
5751
+ const silenceThreshold = isStartListeningArgs(args) ? normalizeSpeechThreshold(args.silenceThreshold) : DEFAULT_SPEECH_THRESHOLD;
5550
5752
  const stream = await this.ensureInputStream();
5551
5753
  const context = new AudioContext();
5552
5754
  const analyser = context.createAnalyser();
@@ -5573,7 +5775,7 @@ var VoiceRuntime = class {
5573
5775
  }
5574
5776
  const rms = Math.sqrt(squareSum / sampleBuffer.length);
5575
5777
  const now = performance.now();
5576
- if (rms >= SPEECH_THRESHOLD) {
5778
+ if (rms >= silenceThreshold) {
5577
5779
  this.lastSpeechAt = now;
5578
5780
  this.listeningStartedAt = now;
5579
5781
  if (!this.speechEventTriggered) {
@@ -5595,6 +5797,9 @@ var VoiceRuntime = class {
5595
5797
  void step();
5596
5798
  });
5597
5799
  };
5800
+ logRuntimeDebug(this.debugLogging, "voice-listening-started", {
5801
+ silenceThreshold
5802
+ });
5598
5803
  void step();
5599
5804
  } catch (error) {
5600
5805
  await this.handleListeningError(error);
@@ -6010,7 +6215,7 @@ var renderGridView = (env, node, state, key) => {
6010
6215
  index: realIndex,
6011
6216
  descriptor: element
6012
6217
  };
6013
- return `<div class="vjt-grid-cell">${env.renderNode(element, `${key}.elements.${realIndex}`, context)}</div>`;
6218
+ 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
6219
  }).join("");
6015
6220
  rows.push(`<div class="vjt-grid-row">${rowMarkup}</div>`);
6016
6221
  }
@@ -6056,13 +6261,13 @@ var renderCombobox = (env, node, state, key) => {
6056
6261
  var HEADING_TAGS = /* @__PURE__ */ new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
6057
6262
  var TEXT_ALIGN_MAP = { left: "left", center: "center", right: "right" };
6058
6263
  var VERTICAL_ALIGN_MAP = { top: "flex-start", center: "center", bottom: "flex-end" };
6059
- var renderStaticText = (env, node, state, key) => {
6264
+ var renderStaticText = (env, node, state, key, _path, itemContext) => {
6060
6265
  const resolvedHeading = node.heading && HEADING_TAGS.has(node.heading) ? node.heading : null;
6061
6266
  const tag = resolvedHeading ?? "div";
6062
6267
  const align = TEXT_ALIGN_MAP[node["horiz-align"] ?? "left"];
6063
6268
  const verticalAlign = VERTICAL_ALIGN_MAP[node["vert-align"] ?? "center"];
6064
6269
  const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
6065
- const text = env.escapeHtml(env.resolveI18nValue(state.text ?? node.text));
6270
+ const text = env.escapeHtml(env.resolveTextValue(state.text ?? node.text, itemContext));
6066
6271
  const inlineStyle = [
6067
6272
  "display:flex",
6068
6273
  `align-items:${verticalAlign}`,
@@ -6093,11 +6298,11 @@ var markdownConverter = new import_showdown.default.Converter({
6093
6298
  strikethrough: false,
6094
6299
  tasklists: false
6095
6300
  });
6096
- var renderMdText = (env, node, state, key) => {
6301
+ var renderMdText = (env, node, state, key, _path, itemContext) => {
6097
6302
  const align = TEXT_ALIGN_MAP2[node["horiz-align"] ?? "left"];
6098
6303
  const verticalAlign = VERTICAL_ALIGN_MAP2[node["vert-align"] ?? "center"];
6099
6304
  const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
6100
- const markdown = env.resolveI18nValue(state.text ?? node.text);
6305
+ const markdown = env.resolveTextValue(state.text ?? node.text, itemContext);
6101
6306
  const html = sanitizeGeneratedHtml(markdownConverter.makeHtml(sanitizeMarkdownSource(markdown)));
6102
6307
  const inlineStyle = [
6103
6308
  "display:flex",
@@ -6119,24 +6324,24 @@ var renderMdText = (env, node, state, key) => {
6119
6324
  };
6120
6325
 
6121
6326
  // src/lib/widgets/edit.ts
6122
- function resolvePlaceholder(env, node, statePlaceholder) {
6327
+ function resolvePlaceholder(env, node, itemContext, statePlaceholder) {
6123
6328
  if (statePlaceholder) {
6124
- return env.resolveI18nValue(statePlaceholder);
6329
+ return env.resolveTextValue(statePlaceholder, itemContext);
6125
6330
  }
6126
6331
  if (node.placeholder) {
6127
- return env.resolveI18nValue(node.placeholder);
6332
+ return env.resolveTextValue(node.placeholder, itemContext);
6128
6333
  }
6129
6334
  if ((node.type ?? "string") === "date") {
6130
6335
  return env.getDateFormat(node);
6131
6336
  }
6132
6337
  return "";
6133
6338
  }
6134
- var renderEdit = (env, node, state, key) => {
6339
+ var renderEdit = (env, node, state, key, _path, itemContext) => {
6135
6340
  const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
6136
6341
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
6137
6342
  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));
6343
+ const placeholder = env.escapeHtml(resolvePlaceholder(env, node, itemContext, state.placeholder));
6344
+ const value = env.escapeHtml(env.resolveTextValue(state.text ?? node.text, itemContext));
6140
6345
  const inputType = node.type === "password" ? "password" : "text";
6141
6346
  const disabledAttribute = enabled ? "" : " disabled";
6142
6347
  const readonlyAttribute = editable ? "" : " readonly";
@@ -6159,23 +6364,23 @@ var renderEdit = (env, node, state, key) => {
6159
6364
  };
6160
6365
 
6161
6366
  // src/lib/widgets/textarea.ts
6162
- var renderTextarea = (env, node, state, key) => {
6367
+ var renderTextarea = (env, node, state, key, _path, itemContext) => {
6163
6368
  const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
6164
6369
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
6165
6370
  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));
6371
+ const placeholder = env.escapeHtml(env.resolveTextValue(state.placeholder ?? node.placeholder, itemContext));
6372
+ const value = env.escapeHtml(state.text ?? env.resolveTextValue(node.text, itemContext));
6168
6373
  const styleString = env.buildStyleString(node);
6169
6374
  const styleAttribute = styleString ? ` style="${env.escapeHtml(styleString)}"` : "";
6170
6375
  return `<textarea${common}${env.buildWidgetDataAttributes(key, node)} class="vjt-textarea" data-widget="textarea" placeholder="${placeholder}"${enabled ? "" : " disabled"}${editable ? "" : " readonly"}${styleAttribute}>${value}</textarea>`;
6171
6376
  };
6172
6377
 
6173
6378
  // src/lib/widgets/button.ts
6174
- var renderButton = (env, node, state, key) => {
6379
+ var renderButton = (env, node, state, key, _path, itemContext) => {
6175
6380
  const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
6176
6381
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
6177
6382
  const disabledAttribute = enabled ? "" : " disabled";
6178
- const text = env.escapeHtml(env.resolveI18nValue(state.text ?? node.text));
6383
+ const text = env.escapeHtml(env.resolveTextValue(state.text ?? node.text, itemContext));
6179
6384
  const styleString = env.buildStyleString(node);
6180
6385
  const styleAttribute = styleString ? ` style="${env.escapeHtml(styleString)}"` : "";
6181
6386
  const buttonType = env.escapeHtml(node.type === "submit" ? "submit" : "button");
@@ -6224,10 +6429,10 @@ var renderImage = (env, node, state, key) => {
6224
6429
  };
6225
6430
 
6226
6431
  // src/lib/widgets/link.ts
6227
- var renderLink = (env, node, state, key) => {
6432
+ var renderLink = (env, node, state, key, _path, itemContext) => {
6228
6433
  const type = node.type === "button" || node.type === "text" ? node.type : "link";
6229
6434
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
6230
- const text = env.escapeHtml(env.resolveI18nValue(state.text ?? node.text));
6435
+ const text = env.escapeHtml(env.resolveTextValue(state.text ?? node.text, itemContext));
6231
6436
  const classes = `vjt-link vjt-link--${type}${enabled ? "" : " is-disabled"}`;
6232
6437
  const styleString = env.buildStyleString(node);
6233
6438
  const styleAttribute = styleString ? ` style="${env.escapeHtml(styleString)}"` : "";
@@ -6984,6 +7189,7 @@ var RuntimeRenderer = class {
6984
7189
  i18n;
6985
7190
  actionsMap;
6986
7191
  requestsMap;
7192
+ routes;
6987
7193
  sseConfigs;
6988
7194
  systemEvents;
6989
7195
  resourceManager;
@@ -7003,6 +7209,7 @@ var RuntimeRenderer = class {
7003
7209
  vars = /* @__PURE__ */ new Map();
7004
7210
  resizeHandler = null;
7005
7211
  pointerHandler = null;
7212
+ popstateHandler = null;
7006
7213
  eventSources = [];
7007
7214
  referenceRuntime;
7008
7215
  networkRuntime;
@@ -7015,6 +7222,9 @@ var RuntimeRenderer = class {
7015
7222
  activeConfirmModal = null;
7016
7223
  lastPointer = { x: 24, y: 24 };
7017
7224
  pendingResetModalIds = /* @__PURE__ */ new Set();
7225
+ pendingScrollAction = null;
7226
+ pendingScrollFrameId = null;
7227
+ activeScrollAnimationFrameId = null;
7018
7228
  constructor(description, options = {}) {
7019
7229
  this.description = deepClone(description);
7020
7230
  this.resourceManager = options.resourceManager ?? null;
@@ -7028,6 +7238,8 @@ var RuntimeRenderer = class {
7028
7238
  this.theme = options.theme === "light" ? "light" : "dark";
7029
7239
  this.actionsMap = options.actionsMap ?? this.resourceManager?.getActionsMap() ?? {};
7030
7240
  this.requestsMap = options.requestsMap ?? this.resourceManager?.getRequestsMap() ?? {};
7241
+ const resolvedRoutes = options.routes ?? this.resourceManager?.getRoutes() ?? [];
7242
+ this.routes = Array.isArray(resolvedRoutes) ? resolvedRoutes.map((route) => ({ ...route })) : Array.isArray(resolvedRoutes?.routes) ? resolvedRoutes.routes.map((route) => ({ ...route })) : [];
7031
7243
  const resolvedSseConfigs = options.sseConfigs ?? this.resourceManager?.getSseConfigs() ?? [];
7032
7244
  this.sseConfigs = Array.isArray(resolvedSseConfigs) ? resolvedSseConfigs : resolvedSseConfigs ? [resolvedSseConfigs] : [];
7033
7245
  this.systemEvents = options.systemEvents ?? this.resourceManager?.getSystemEvents() ?? {};
@@ -7041,6 +7253,7 @@ var RuntimeRenderer = class {
7041
7253
  stateByKey: this.stateByKey,
7042
7254
  nodeById: this.nodeById,
7043
7255
  nodeByKey: this.nodeByKey,
7256
+ getUiResource: (ref) => this.resourceManager?.getUi(ref) ?? null,
7044
7257
  getAppValue: (name) => {
7045
7258
  switch (name) {
7046
7259
  case "language":
@@ -7114,6 +7327,9 @@ var RuntimeRenderer = class {
7114
7327
  clearWidget: (widgetId) => this.clearWidget(widgetId),
7115
7328
  clearListElementState: (listKey) => this.clearListElementState(listKey),
7116
7329
  focusWidget: (reference) => this.focusWidget(reference),
7330
+ scrollWidgetToTop: (reference) => this.scrollWidgetToTop(reference),
7331
+ scrollWidgetToBottom: (reference) => this.scrollWidgetToBottom(reference),
7332
+ scrollWidgetToElementByIndex: (reference, index) => this.scrollWidgetToElementByIndex(reference, index),
7117
7333
  playAudio: (value) => this.voiceRuntime.play(value),
7118
7334
  stopPlaying: () => this.voiceRuntime.stopPlaying(),
7119
7335
  copyToClipboard: (value) => this.copyToClipboard(value),
@@ -7121,7 +7337,7 @@ var RuntimeRenderer = class {
7121
7337
  confirmModal: (args, inputValue) => this.confirmModal(args, inputValue),
7122
7338
  startRecording: () => this.voiceRuntime.startRecording(),
7123
7339
  stopRecording: () => Promise.resolve(this.voiceRuntime.stopRecording()),
7124
- startListening: () => this.voiceRuntime.startListening(),
7340
+ startListening: (args) => this.voiceRuntime.startListening(args),
7125
7341
  stopListening: () => this.voiceRuntime.stopListening(),
7126
7342
  setUiReference: (widgetId, resourceRef) => {
7127
7343
  const node = this.nodeById.get(widgetId);
@@ -7151,11 +7367,12 @@ var RuntimeRenderer = class {
7151
7367
  getInlineActions: (node) => this.getInlineActions(node),
7152
7368
  isWidgetEnabled: (node, key) => this.isWidgetEnabled(node, key)
7153
7369
  });
7154
- this.indexStaticNodes(this.description, "root");
7370
+ this.indexStaticNodes(this.getActiveDescription(), "root");
7155
7371
  this.applyRuntimeSnapshot(this.initialRuntimeSnapshot);
7156
7372
  }
7157
7373
  renderMarkup() {
7158
- const markup = `${this.renderNode(this.description, "root", null)}${this.renderGlobalOverlays()}`;
7374
+ const activeDescription = this.getActiveDescription();
7375
+ const markup = `${this.renderNode(activeDescription, "root", null)}${this.renderGlobalOverlays()}`;
7159
7376
  logRuntimeDebug(this.debugLogging, "html", markup);
7160
7377
  return markup;
7161
7378
  }
@@ -7205,6 +7422,17 @@ var RuntimeRenderer = class {
7205
7422
  };
7206
7423
  window.addEventListener("pointerdown", handlePointer);
7207
7424
  this.pointerHandler = handlePointer;
7425
+ const handlePopState = () => {
7426
+ void (async () => {
7427
+ await this.runSystemEvent("onBeforeNavigate");
7428
+ await this.rerenderRoot();
7429
+ await this.runSystemEvent("onAfterNavigate");
7430
+ })().catch((error) => {
7431
+ logRuntimeError("popstate", error);
7432
+ });
7433
+ };
7434
+ window.addEventListener("popstate", handlePopState);
7435
+ this.popstateHandler = handlePopState;
7208
7436
  return () => {
7209
7437
  if (this.resizeHandler) {
7210
7438
  window.removeEventListener("resize", this.resizeHandler);
@@ -7212,6 +7440,9 @@ var RuntimeRenderer = class {
7212
7440
  if (this.pointerHandler) {
7213
7441
  window.removeEventListener("pointerdown", this.pointerHandler);
7214
7442
  }
7443
+ if (this.popstateHandler) {
7444
+ window.removeEventListener("popstate", this.popstateHandler);
7445
+ }
7215
7446
  for (const source of this.eventSources) {
7216
7447
  source.close();
7217
7448
  }
@@ -7234,6 +7465,7 @@ var RuntimeRenderer = class {
7234
7465
  this.attachInputBehavior(this.root);
7235
7466
  this.attachWidgetEvents(this.root);
7236
7467
  restoreElementState(this.root, preservedState, this.stateByKey);
7468
+ this.applyPendingScrollAction();
7237
7469
  this.activeContextMenu = adjustContextMenuPosition(this.root, this.activeContextMenu);
7238
7470
  this.pendingResetModalIds.clear();
7239
7471
  if (typeof this.onRuntimeSnapshot === "function") {
@@ -7413,7 +7645,113 @@ var RuntimeRenderer = class {
7413
7645
  this.nodeByKey.clear();
7414
7646
  this.nodeById.clear();
7415
7647
  this.stateById.clear();
7416
- this.indexStaticNodes(this.description, "root");
7648
+ this.indexStaticNodes(this.getActiveDescription(), "root");
7649
+ }
7650
+ getActiveDescription() {
7651
+ if (this.hasRoutes() && !this.isCurrentPathValid()) {
7652
+ return this.buildNotFoundDescription(this.getCurrentPathname());
7653
+ }
7654
+ return this.description;
7655
+ }
7656
+ hasRoutes() {
7657
+ return this.routes.length > 0;
7658
+ }
7659
+ isCurrentPathValid() {
7660
+ const pathname = this.getCurrentPathname();
7661
+ if (pathname === "/") {
7662
+ return true;
7663
+ }
7664
+ return this.routes.some((entry) => this.normalizePathname(entry.path) === pathname);
7665
+ }
7666
+ getCurrentPathname() {
7667
+ if (typeof window === "undefined") {
7668
+ return "/";
7669
+ }
7670
+ return this.normalizePathname(window.location.pathname);
7671
+ }
7672
+ normalizePathname(path) {
7673
+ if (!path || path === "/" || path === "/index.html") {
7674
+ return "/";
7675
+ }
7676
+ const normalized = path.startsWith("/") ? path : `/${path}`;
7677
+ return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
7678
+ }
7679
+ buildNotFoundDescription(pathname) {
7680
+ const dictionary = this.getBuiltinNotFoundTexts();
7681
+ return {
7682
+ widget: "panel",
7683
+ id: "vjt-not-found-panel",
7684
+ 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);",
7685
+ child: {
7686
+ widget: "container-layout",
7687
+ type: "vertical",
7688
+ children: [
7689
+ {
7690
+ minHeightPx: 64,
7691
+ maxHeightPx: 64,
7692
+ marginBottomPx: 8,
7693
+ child: {
7694
+ widget: "static-text",
7695
+ heading: "h1",
7696
+ text: dictionary.title,
7697
+ css: "font-size:42px;font-weight:800;letter-spacing:0.04em;text-align:center;justify-content:center;"
7698
+ }
7699
+ },
7700
+ {
7701
+ minHeightPx: 36,
7702
+ maxHeightPx: 36,
7703
+ marginBottomPx: 8,
7704
+ child: {
7705
+ widget: "static-text",
7706
+ heading: "h3",
7707
+ text: dictionary.caption,
7708
+ css: "text-align:center;justify-content:center;"
7709
+ }
7710
+ },
7711
+ {
7712
+ minHeightPx: 64,
7713
+ marginBottomPx: 16,
7714
+ child: {
7715
+ widget: "static-text",
7716
+ text: `${dictionary.note} ${pathname}`,
7717
+ css: "text-align:center;justify-content:center;color:var(--vjt-muted);"
7718
+ }
7719
+ },
7720
+ {
7721
+ minHeightPx: 52,
7722
+ maxHeightPx: 52,
7723
+ child: {
7724
+ widget: "button",
7725
+ text: dictionary.home,
7726
+ events: {
7727
+ onClick: [
7728
+ {
7729
+ action: "navigate /"
7730
+ }
7731
+ ]
7732
+ }
7733
+ }
7734
+ }
7735
+ ]
7736
+ }
7737
+ };
7738
+ }
7739
+ getBuiltinNotFoundTexts() {
7740
+ const language = this.language.toLowerCase();
7741
+ if (language.startsWith("ru")) {
7742
+ return {
7743
+ title: "404",
7744
+ caption: "\u0421\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430",
7745
+ 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:",
7746
+ home: "\u041D\u0430 \u0433\u043B\u0430\u0432\u043D\u0443\u044E"
7747
+ };
7748
+ }
7749
+ return {
7750
+ title: "404",
7751
+ caption: "Page not found",
7752
+ note: "The requested path does not exist:",
7753
+ home: "Go home"
7754
+ };
7417
7755
  }
7418
7756
  getUiReferenceTarget(node, key) {
7419
7757
  if (node.widget !== "ui-reference") {
@@ -7871,6 +8209,20 @@ var RuntimeRenderer = class {
7871
8209
  }
7872
8210
  return `??${key}??`;
7873
8211
  }
8212
+ resolveTextValue(value, itemContext) {
8213
+ if (typeof value !== "string") {
8214
+ return value ?? "";
8215
+ }
8216
+ if (value.startsWith("$ref:")) {
8217
+ const resolved = this.referenceRuntime.resolveReference(value.slice(5), itemContext, null);
8218
+ if (resolved == null) {
8219
+ return "";
8220
+ }
8221
+ const resolvedText = typeof resolved === "string" ? resolved : typeof resolved === "number" || typeof resolved === "boolean" || typeof resolved === "bigint" ? String(resolved) : JSON.stringify(resolved);
8222
+ return this.resolveI18nValue(resolvedText);
8223
+ }
8224
+ return this.resolveI18nValue(value);
8225
+ }
7874
8226
  buildWidgetDataAttributes(key, node) {
7875
8227
  const parts = [` data-widget-key="${escapeHtml(key)}"`];
7876
8228
  if (node.id) {
@@ -7889,6 +8241,7 @@ var RuntimeRenderer = class {
7889
8241
  escapeHtml,
7890
8242
  buildStyleString: (node) => this.buildStyleString(node),
7891
8243
  resolveI18nValue: (value) => this.resolveI18nValue(value),
8244
+ resolveTextValue: (value, itemContext) => this.resolveTextValue(value, itemContext),
7892
8245
  getUiResource: (ref) => this.resourceManager?.getUi(ref) ?? null,
7893
8246
  resolveReference: (reference, currentValue, responseValue) => this.referenceRuntime.resolveReference(reference, currentValue, responseValue),
7894
8247
  buildWidgetDataAttributes: (key, node) => this.buildWidgetDataAttributes(key, node),
@@ -8434,6 +8787,138 @@ var RuntimeRenderer = class {
8434
8787
  nestedTarget.focus({ preventScroll: true });
8435
8788
  }
8436
8789
  }
8790
+ scrollWidgetToTop(reference) {
8791
+ this.pendingScrollAction = { kind: "top", reference };
8792
+ this.schedulePendingScrollAction();
8793
+ }
8794
+ scrollWidgetToBottom(reference) {
8795
+ this.pendingScrollAction = { kind: "bottom", reference };
8796
+ this.schedulePendingScrollAction();
8797
+ }
8798
+ scrollWidgetToElementByIndex(reference, indexValue) {
8799
+ this.pendingScrollAction = { kind: "index", reference, indexValue };
8800
+ this.schedulePendingScrollAction();
8801
+ }
8802
+ schedulePendingScrollAction() {
8803
+ if (typeof window === "undefined" || this.pendingScrollFrameId !== null) {
8804
+ return;
8805
+ }
8806
+ this.pendingScrollFrameId = window.requestAnimationFrame(() => {
8807
+ this.pendingScrollFrameId = null;
8808
+ this.applyPendingScrollAction();
8809
+ });
8810
+ }
8811
+ applyPendingScrollAction() {
8812
+ if (!this.pendingScrollAction) {
8813
+ return;
8814
+ }
8815
+ const pending = this.pendingScrollAction;
8816
+ const element = this.findScrollableWidgetElement(pending.reference);
8817
+ if (!element) {
8818
+ return;
8819
+ }
8820
+ if (pending.kind === "top") {
8821
+ this.animateScrollTop(element, 0);
8822
+ this.pendingScrollAction = null;
8823
+ return;
8824
+ }
8825
+ if (pending.kind === "bottom") {
8826
+ this.animateScrollTop(element, element.scrollHeight);
8827
+ this.pendingScrollAction = null;
8828
+ return;
8829
+ }
8830
+ const index = this.toScrollableIndex(pending.indexValue);
8831
+ if (index === null) {
8832
+ this.pendingScrollAction = null;
8833
+ return;
8834
+ }
8835
+ this.performScrollToElementByIndex(element, index);
8836
+ this.pendingScrollAction = null;
8837
+ }
8838
+ performScrollToElementByIndex(element, index) {
8839
+ if (element instanceof HTMLSelectElement) {
8840
+ const option = element.options.item(index);
8841
+ if (!option) {
8842
+ return;
8843
+ }
8844
+ const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
8845
+ this.animateScrollTop(element, Math.max(0, Math.min(option.offsetTop, maxScrollTop)));
8846
+ return;
8847
+ }
8848
+ const listElement = element.querySelector(`.vjt-list-element[data-list-index="${index}"], .vjt-grid-cell[data-list-index="${index}"]`);
8849
+ if (listElement instanceof HTMLElement) {
8850
+ const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
8851
+ const containerRect = element.getBoundingClientRect();
8852
+ const itemRect = listElement.getBoundingClientRect();
8853
+ const targetTop = element.scrollTop + (itemRect.top - containerRect.top);
8854
+ const remainingContentHeight = element.scrollHeight - targetTop;
8855
+ const nextScrollTop = remainingContentHeight > element.clientHeight ? Math.max(0, Math.min(targetTop, maxScrollTop)) : maxScrollTop;
8856
+ this.animateScrollTop(element, nextScrollTop);
8857
+ }
8858
+ }
8859
+ animateScrollTop(element, targetTop) {
8860
+ if (typeof window === "undefined") {
8861
+ element.scrollTop = targetTop;
8862
+ return;
8863
+ }
8864
+ if (this.activeScrollAnimationFrameId !== null) {
8865
+ window.cancelAnimationFrame(this.activeScrollAnimationFrameId);
8866
+ this.activeScrollAnimationFrameId = null;
8867
+ }
8868
+ const startTop = element.scrollTop;
8869
+ const clampedTargetTop = Math.max(0, targetTop);
8870
+ const delta = clampedTargetTop - startTop;
8871
+ if (Math.abs(delta) < 1) {
8872
+ element.scrollTop = clampedTargetTop;
8873
+ return;
8874
+ }
8875
+ const durationMs = 300;
8876
+ const startTime = window.performance.now();
8877
+ const easeInOutCubic = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
8878
+ const tick = (now) => {
8879
+ const elapsed = now - startTime;
8880
+ const progress = Math.min(1, elapsed / durationMs);
8881
+ element.scrollTop = startTop + delta * easeInOutCubic(progress);
8882
+ if (progress < 1) {
8883
+ this.activeScrollAnimationFrameId = window.requestAnimationFrame(tick);
8884
+ } else {
8885
+ element.scrollTop = clampedTargetTop;
8886
+ this.activeScrollAnimationFrameId = null;
8887
+ }
8888
+ };
8889
+ this.activeScrollAnimationFrameId = window.requestAnimationFrame(tick);
8890
+ }
8891
+ findScrollableWidgetElement(reference) {
8892
+ if (!(this.root instanceof HTMLElement)) {
8893
+ return null;
8894
+ }
8895
+ const widgetId = reference.split(".")[0]?.trim();
8896
+ if (!widgetId) {
8897
+ return null;
8898
+ }
8899
+ const directTarget = this.root.querySelector(`#${CSS.escape(widgetId)}`);
8900
+ if (directTarget) {
8901
+ return directTarget;
8902
+ }
8903
+ for (const element of Array.from(this.root.querySelectorAll("[data-widget-id]"))) {
8904
+ if (element.dataset.widgetId === widgetId) {
8905
+ return element;
8906
+ }
8907
+ }
8908
+ return null;
8909
+ }
8910
+ toScrollableIndex(value) {
8911
+ if (typeof value === "number" && Number.isFinite(value)) {
8912
+ return Math.max(0, Math.trunc(value));
8913
+ }
8914
+ if (typeof value === "string" && value.trim().length > 0) {
8915
+ const parsed = Number.parseInt(value, 10);
8916
+ if (Number.isFinite(parsed)) {
8917
+ return Math.max(0, parsed);
8918
+ }
8919
+ }
8920
+ return null;
8921
+ }
8437
8922
  };
8438
8923
  function renderJson(description, options = {}) {
8439
8924
  const renderer = new RuntimeRenderer(description, options);
@@ -8449,6 +8934,7 @@ var ResourceManager = class {
8449
8934
  ui = /* @__PURE__ */ new Map();
8450
8935
  actions = {};
8451
8936
  requests = {};
8937
+ routes = [];
8452
8938
  sse = {};
8453
8939
  systemEvents = {};
8454
8940
  i18n = {};
@@ -8459,6 +8945,7 @@ var ResourceManager = class {
8459
8945
  }
8460
8946
  this.actions = { ...input.actions ?? {} };
8461
8947
  this.requests = { ...input.requests ?? {} };
8948
+ this.routes = Array.isArray(input.routes) ? input.routes.map((route) => ({ ...route })) : Array.isArray(input.routes?.routes) ? input.routes.routes.map((route) => ({ ...route })) : [];
8462
8949
  this.sse = { ...input.sse ?? {} };
8463
8950
  this.systemEvents = { ...input.systemEvents ?? {} };
8464
8951
  this.i18n = { ...input.i18n ?? {} };
@@ -8476,6 +8963,9 @@ var ResourceManager = class {
8476
8963
  getRequestsMap() {
8477
8964
  return this.requests;
8478
8965
  }
8966
+ getRoutes() {
8967
+ return this.routes.map((route) => ({ ...route }));
8968
+ }
8479
8969
  getSseConfigs() {
8480
8970
  return Object.values(this.sse);
8481
8971
  }
@@ -32,6 +32,9 @@ type ActionRuntimeOptions = {
32
32
  clearWidget: (widgetId: string) => void;
33
33
  clearListElementState: (listKey: string) => void;
34
34
  focusWidget: (reference: string) => void;
35
+ scrollWidgetToTop: (reference: string) => void;
36
+ scrollWidgetToBottom: (reference: string) => void;
37
+ scrollWidgetToElementByIndex: (reference: string, index: unknown) => void;
35
38
  playAudio: (value: unknown) => Promise<void>;
36
39
  stopPlaying: () => Promise<void>;
37
40
  copyToClipboard: (value: unknown) => Promise<void>;
@@ -39,7 +42,7 @@ type ActionRuntimeOptions = {
39
42
  confirmModal: (options: unknown, inputValue: unknown) => Promise<boolean>;
40
43
  startRecording: () => Promise<void>;
41
44
  stopRecording: () => Promise<void>;
42
- startListening: () => Promise<void>;
45
+ startListening: (args?: unknown) => Promise<void>;
43
46
  stopListening: () => Promise<void>;
44
47
  setUiReference: (widgetId: string, resourceRef: string) => void;
45
48
  refreshWidgetTree: (widgetId: string) => Promise<void>;
@@ -82,6 +85,9 @@ export declare class ActionRuntime {
82
85
  private readonly clearWidget;
83
86
  private readonly clearListElementState;
84
87
  private readonly focusWidget;
88
+ private readonly scrollWidgetToTop;
89
+ private readonly scrollWidgetToBottom;
90
+ private readonly scrollWidgetToElementByIndex;
85
91
  private readonly playAudio;
86
92
  private readonly stopPlaying;
87
93
  private readonly copyToClipboard;
@@ -115,5 +121,8 @@ export declare class ActionRuntime {
115
121
  private cloneValue;
116
122
  private stringifyValue;
117
123
  private isCurrentScopedReference;
124
+ private countValue;
125
+ private toNumber;
126
+ private asListWidgetState;
118
127
  }
119
128
  export {};
@@ -4,6 +4,7 @@ type ReferenceRuntimeHost = {
4
4
  readonly stateByKey: Map<string, WidgetState>;
5
5
  readonly nodeById: Map<string, DescriptionNode>;
6
6
  readonly nodeByKey: Map<string, DescriptionNode>;
7
+ getUiResource: (ref: string) => DescriptionNode | null;
7
8
  getAppValue: (name: string) => unknown;
8
9
  setAppValue: (name: string, value: unknown) => void;
9
10
  getVarValue: (name: string) => unknown;
@@ -19,12 +20,15 @@ export declare class ReferenceRuntime {
19
20
  resolveReference(reference: string, currentValue: unknown, responseValue: unknown): unknown;
20
21
  resolveCurrentReference(reference: string, currentValue: unknown): unknown;
21
22
  readWidgetField(state: WidgetState, field: string): unknown;
22
- assignReference(reference: string, inputValue: unknown, _currentValue: unknown, options?: {
23
+ assignReference(reference: string, inputValue: unknown, currentValue: unknown, options?: {
23
24
  cookieTtl?: unknown;
24
25
  }): void;
26
+ private assignCurrentReference;
25
27
  clearListElementState(listKey: string): void;
26
28
  resolveMappedValue(template: unknown, currentValue: unknown, responseValue: unknown): unknown;
27
29
  isListElementContext(value: unknown): value is ListElementContext;
28
- findNamedWidget(node: DescriptionNode | undefined, name: string): DescriptionNode | null;
30
+ findNamedWidget(node: DescriptionNode | undefined, name: string, currentValue?: unknown): DescriptionNode | null;
31
+ private resolveListDescriptorNode;
32
+ private resolveUiReferenceNode;
29
33
  }
30
34
  export {};
@@ -1,8 +1,9 @@
1
- import type { ActionMap, DescriptionNode, I18nMap, RequestMap, SseConfig, SystemEventsMap, StyleMap } from './types.js';
1
+ import type { ActionMap, DescriptionNode, I18nMap, RequestMap, RouteDefinition, RoutesConfigInput, SseConfig, SystemEventsMap, StyleMap } from './types.js';
2
2
  export type ResourceManagerInput = {
3
3
  ui?: Record<string, DescriptionNode>;
4
4
  actions?: ActionMap;
5
5
  requests?: RequestMap;
6
+ routes?: RoutesConfigInput;
6
7
  sse?: Record<string, SseConfig>;
7
8
  systemEvents?: SystemEventsMap;
8
9
  i18n?: I18nMap;
@@ -12,6 +13,7 @@ export declare class ResourceManager {
12
13
  private readonly ui;
13
14
  private actions;
14
15
  private requests;
16
+ private routes;
15
17
  private sse;
16
18
  private systemEvents;
17
19
  private i18n;
@@ -21,6 +23,7 @@ export declare class ResourceManager {
21
23
  setUi(name: string, description: DescriptionNode): void;
22
24
  getActionsMap(): ActionMap;
23
25
  getRequestsMap(): RequestMap;
26
+ getRoutes(): RouteDefinition[];
24
27
  getSseConfigs(): SseConfig[];
25
28
  getSystemEvents(): SystemEventsMap;
26
29
  getI18n(): I18nMap;
@@ -6,6 +6,13 @@ export type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
6
6
  export type WidgetEventName = 'onClick' | 'onUserValueChange' | 'onRefresh' | 'onEnter' | 'onShiftEnter' | 'onControlEnter';
7
7
  export type SystemEventName = 'onBeforeRender' | 'onAfterRender' | 'onBeforeNavigate' | 'onAfterNavigate' | 'onSpeechDetected' | 'onRecordingStarted' | 'onRecordingStopped' | 'onRecordingError' | 'onListeningError' | 'onListeringError' | 'onPlayFinished' | 'onPlayingStopped';
8
8
  export type PrimitiveRequestType = 'int' | 'float' | 'boolean' | 'string';
9
+ export type RouteDefinition = {
10
+ path: string;
11
+ ui: string;
12
+ };
13
+ export type RoutesConfigInput = RouteDefinition[] | {
14
+ routes: RouteDefinition[];
15
+ };
9
16
  export type ActionDefinition = {
10
17
  action: string;
11
18
  andThen?: ActionDefinition[];
@@ -139,6 +146,7 @@ export type RenderJsonOptions = {
139
146
  requestsMap?: RequestMap;
140
147
  sseConfigs?: SseConfigInput;
141
148
  systemEvents?: SystemEventsMap;
149
+ routes?: RoutesConfigInput;
142
150
  actionFunctions?: Record<string, () => unknown>;
143
151
  backendUrl?: string;
144
152
  resourceManager?: ResourceManager;
@@ -27,7 +27,7 @@ export declare class VoiceRuntime {
27
27
  stopPlaying(): Promise<void>;
28
28
  startRecording(): Promise<void>;
29
29
  stopRecording(): void;
30
- startListening(): Promise<void>;
30
+ startListening(args?: unknown): Promise<void>;
31
31
  stopListening(): Promise<void>;
32
32
  dispose(): void;
33
33
  private startRecordingInternal;
@@ -29,6 +29,7 @@ export type WidgetRenderEnvironment = {
29
29
  escapeHtml: (value: unknown) => string;
30
30
  buildStyleString: (node: DescriptionNode) => string;
31
31
  resolveI18nValue: (value: string | undefined) => string;
32
+ resolveTextValue: (value: string | undefined, itemContext: ListElementContext | null) => string;
32
33
  getUiResource: (ref: string) => DescriptionNode | null;
33
34
  resolveReference: (reference: string, currentValue: unknown, responseValue: unknown) => unknown;
34
35
  buildWidgetDataAttributes: (key: string, node: DescriptionNode) => string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vortexm/vjt",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",