@vortexm/vjt 0.1.6 → 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;
@@ -4670,16 +4744,22 @@ var ActionRuntime = class {
4670
4744
  async runSingleAction(action, inputValue, context) {
4671
4745
  const actionName = action.action.trim();
4672
4746
  try {
4673
- let output = await this.executeAction(action, inputValue, context);
4747
+ const rawOutput = await this.executeAction(action, inputValue, context);
4748
+ logRuntimeDebug(this.debugLogging, "action-output", {
4749
+ action: actionName,
4750
+ output: rawOutput
4751
+ });
4752
+ let output = rawOutput;
4674
4753
  if (action.andThen?.length) {
4675
4754
  if (output !== null && output !== void 0) {
4676
- output = await this.runActions(action.andThen, output, { ...context, currentValue: output });
4755
+ output = await this.runActions(action.andThen, output, context);
4677
4756
  } else {
4678
4757
  output = null;
4679
4758
  }
4680
4759
  }
4681
4760
  logRuntimeDebug(this.debugLogging, "action-result", {
4682
4761
  action: actionName,
4762
+ rawOutput,
4683
4763
  output
4684
4764
  });
4685
4765
  return output;
@@ -4697,7 +4777,7 @@ var ActionRuntime = class {
4697
4777
  }
4698
4778
  async executeAction(action, inputValue, context) {
4699
4779
  const name = action.action.trim();
4700
- logRuntimeDebug(this.debugLogging, "action", {
4780
+ logRuntimeDebug(this.debugLogging, "action-start", {
4701
4781
  action: name,
4702
4782
  args: action.args,
4703
4783
  inputValue,
@@ -4759,7 +4839,7 @@ var ActionRuntime = class {
4759
4839
  return null;
4760
4840
  }
4761
4841
  if (name === "startListening") {
4762
- await this.startListening();
4842
+ await this.startListening(action.args);
4763
4843
  return null;
4764
4844
  }
4765
4845
  if (name === "stopListening") {
@@ -4811,6 +4891,18 @@ var ActionRuntime = class {
4811
4891
  this.focusWidget(name.slice(9));
4812
4892
  return null;
4813
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
+ }
4814
4906
  if (name.startsWith("setUi ")) {
4815
4907
  const widgetId = name.slice(6);
4816
4908
  const resourceRef = typeof inputValue === "string" ? inputValue : "";
@@ -4821,14 +4913,62 @@ var ActionRuntime = class {
4821
4913
  }
4822
4914
  if (name.startsWith("get ")) {
4823
4915
  const reference = name.slice(4);
4824
- if (Array.isArray(context.currentValue) && this.isCurrentScopedReference(reference)) {
4825
- return context.currentValue.map((entry) => this.resolveReference(reference, entry, context.responseValue)).filter((entry) => entry !== null && entry !== void 0);
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);
4826
4918
  }
4827
4919
  return this.resolveReference(reference, context.currentValue, context.responseValue);
4828
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
+ }
4829
4954
  if (name.startsWith("equals ")) {
4830
4955
  return this.resolveReference(name.slice(7), context.currentValue, context.responseValue) === action.args;
4831
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
+ }
4832
4972
  if (name.startsWith("isEmpty ")) {
4833
4973
  return this.isEmptyReference(name.slice(8), context.currentValue, context.responseValue);
4834
4974
  }
@@ -4846,8 +4986,8 @@ var ActionRuntime = class {
4846
4986
  }
4847
4987
  if (name.startsWith("filter ")) {
4848
4988
  const reference = name.slice(7);
4849
- if (Array.isArray(context.currentValue) && this.isCurrentScopedReference(reference)) {
4850
- return context.currentValue.filter((entry) => this.resolveReference(reference, entry, context.responseValue));
4989
+ if (Array.isArray(inputValue) && this.isCurrentScopedReference(reference)) {
4990
+ return inputValue.filter((entry) => this.resolveReference(reference, entry, context.responseValue));
4851
4991
  }
4852
4992
  const value = this.resolveReference(reference, context.currentValue, context.responseValue);
4853
4993
  return value ? inputValue : null;
@@ -4872,6 +5012,12 @@ var ActionRuntime = class {
4872
5012
  this.clearListElementState(listState.key);
4873
5013
  return listState.elements;
4874
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
+ }
4875
5021
  if (context.currentList && typeof context.currentIndex === "number") {
4876
5022
  return context.currentIndex === context.currentList.length - 1 ? [this.unwrapListValue(inputValue), valueToAdd] : this.unwrapListValue(inputValue);
4877
5023
  }
@@ -4896,6 +5042,12 @@ var ActionRuntime = class {
4896
5042
  return listState.elements;
4897
5043
  }
4898
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
+ }
4899
5051
  if (context.currentList && typeof context.currentIndex === "number") {
4900
5052
  return [this.unwrapListValue(inputValue), valueToInsert];
4901
5053
  }
@@ -4910,8 +5062,8 @@ var ActionRuntime = class {
4910
5062
  return inputValue;
4911
5063
  }
4912
5064
  if (name === "map") {
4913
- if (Array.isArray(context.currentValue)) {
4914
- 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));
4915
5067
  }
4916
5068
  return this.resolveMappedValue(action.args, context.currentValue, context.responseValue);
4917
5069
  }
@@ -4958,6 +5110,46 @@ var ActionRuntime = class {
4958
5110
  isCurrentScopedReference(reference) {
4959
5111
  return reference === "current" || reference.startsWith("current.") || reference.startsWith("this.") || reference === "this";
4960
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
+ }
4961
5153
  };
4962
5154
  function isIfElseArgs(value) {
4963
5155
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -5387,7 +5579,7 @@ var MAX_CONCURRENT_PLAYERS = 5;
5387
5579
  var MAX_RECORDING_MS = 10 * 60 * 1e3;
5388
5580
  var MAX_LISTENING_SILENCE_MS = 60 * 60 * 1e3;
5389
5581
  var SILENCE_STOP_MS = 2e3;
5390
- var SPEECH_THRESHOLD = 0.035;
5582
+ var DEFAULT_SPEECH_THRESHOLD = 0.035;
5391
5583
  var RECORDING_MIME_CANDIDATES = [
5392
5584
  "audio/webm;codecs=opus",
5393
5585
  "audio/webm",
@@ -5440,6 +5632,21 @@ function blobToBase64(blob) {
5440
5632
  reader.readAsDataURL(blob);
5441
5633
  });
5442
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
+ }
5443
5650
  var VoiceRuntime = class {
5444
5651
  debugLogging;
5445
5652
  triggerSystemEvent;
@@ -5536,11 +5743,12 @@ var VoiceRuntime = class {
5536
5743
  this.clearRecordingTimeout();
5537
5744
  this.mediaRecorder.stop();
5538
5745
  }
5539
- async startListening() {
5746
+ async startListening(args) {
5540
5747
  if (this.listening) {
5541
5748
  return;
5542
5749
  }
5543
5750
  try {
5751
+ const silenceThreshold = isStartListeningArgs(args) ? normalizeSpeechThreshold(args.silenceThreshold) : DEFAULT_SPEECH_THRESHOLD;
5544
5752
  const stream = await this.ensureInputStream();
5545
5753
  const context = new AudioContext();
5546
5754
  const analyser = context.createAnalyser();
@@ -5567,7 +5775,7 @@ var VoiceRuntime = class {
5567
5775
  }
5568
5776
  const rms = Math.sqrt(squareSum / sampleBuffer.length);
5569
5777
  const now = performance.now();
5570
- if (rms >= SPEECH_THRESHOLD) {
5778
+ if (rms >= silenceThreshold) {
5571
5779
  this.lastSpeechAt = now;
5572
5780
  this.listeningStartedAt = now;
5573
5781
  if (!this.speechEventTriggered) {
@@ -5589,6 +5797,9 @@ var VoiceRuntime = class {
5589
5797
  void step();
5590
5798
  });
5591
5799
  };
5800
+ logRuntimeDebug(this.debugLogging, "voice-listening-started", {
5801
+ silenceThreshold
5802
+ });
5592
5803
  void step();
5593
5804
  } catch (error) {
5594
5805
  await this.handleListeningError(error);
@@ -6004,7 +6215,7 @@ var renderGridView = (env, node, state, key) => {
6004
6215
  index: realIndex,
6005
6216
  descriptor: element
6006
6217
  };
6007
- 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>`;
6008
6219
  }).join("");
6009
6220
  rows.push(`<div class="vjt-grid-row">${rowMarkup}</div>`);
6010
6221
  }
@@ -6050,13 +6261,13 @@ var renderCombobox = (env, node, state, key) => {
6050
6261
  var HEADING_TAGS = /* @__PURE__ */ new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
6051
6262
  var TEXT_ALIGN_MAP = { left: "left", center: "center", right: "right" };
6052
6263
  var VERTICAL_ALIGN_MAP = { top: "flex-start", center: "center", bottom: "flex-end" };
6053
- var renderStaticText = (env, node, state, key) => {
6264
+ var renderStaticText = (env, node, state, key, _path, itemContext) => {
6054
6265
  const resolvedHeading = node.heading && HEADING_TAGS.has(node.heading) ? node.heading : null;
6055
6266
  const tag = resolvedHeading ?? "div";
6056
6267
  const align = TEXT_ALIGN_MAP[node["horiz-align"] ?? "left"];
6057
6268
  const verticalAlign = VERTICAL_ALIGN_MAP[node["vert-align"] ?? "center"];
6058
6269
  const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
6059
- const text = env.escapeHtml(env.resolveI18nValue(state.text ?? node.text));
6270
+ const text = env.escapeHtml(env.resolveTextValue(state.text ?? node.text, itemContext));
6060
6271
  const inlineStyle = [
6061
6272
  "display:flex",
6062
6273
  `align-items:${verticalAlign}`,
@@ -6087,11 +6298,11 @@ var markdownConverter = new import_showdown.default.Converter({
6087
6298
  strikethrough: false,
6088
6299
  tasklists: false
6089
6300
  });
6090
- var renderMdText = (env, node, state, key) => {
6301
+ var renderMdText = (env, node, state, key, _path, itemContext) => {
6091
6302
  const align = TEXT_ALIGN_MAP2[node["horiz-align"] ?? "left"];
6092
6303
  const verticalAlign = VERTICAL_ALIGN_MAP2[node["vert-align"] ?? "center"];
6093
6304
  const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
6094
- const markdown = env.resolveI18nValue(state.text ?? node.text);
6305
+ const markdown = env.resolveTextValue(state.text ?? node.text, itemContext);
6095
6306
  const html = sanitizeGeneratedHtml(markdownConverter.makeHtml(sanitizeMarkdownSource(markdown)));
6096
6307
  const inlineStyle = [
6097
6308
  "display:flex",
@@ -6113,24 +6324,24 @@ var renderMdText = (env, node, state, key) => {
6113
6324
  };
6114
6325
 
6115
6326
  // src/lib/widgets/edit.ts
6116
- function resolvePlaceholder(env, node, statePlaceholder) {
6327
+ function resolvePlaceholder(env, node, itemContext, statePlaceholder) {
6117
6328
  if (statePlaceholder) {
6118
- return env.resolveI18nValue(statePlaceholder);
6329
+ return env.resolveTextValue(statePlaceholder, itemContext);
6119
6330
  }
6120
6331
  if (node.placeholder) {
6121
- return env.resolveI18nValue(node.placeholder);
6332
+ return env.resolveTextValue(node.placeholder, itemContext);
6122
6333
  }
6123
6334
  if ((node.type ?? "string") === "date") {
6124
6335
  return env.getDateFormat(node);
6125
6336
  }
6126
6337
  return "";
6127
6338
  }
6128
- var renderEdit = (env, node, state, key) => {
6339
+ var renderEdit = (env, node, state, key, _path, itemContext) => {
6129
6340
  const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
6130
6341
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
6131
6342
  const editable = state.editable ?? env.normalizeBoolean(node.editable, true);
6132
- const placeholder = env.escapeHtml(resolvePlaceholder(env, node, state.placeholder));
6133
- 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));
6134
6345
  const inputType = node.type === "password" ? "password" : "text";
6135
6346
  const disabledAttribute = enabled ? "" : " disabled";
6136
6347
  const readonlyAttribute = editable ? "" : " readonly";
@@ -6153,23 +6364,23 @@ var renderEdit = (env, node, state, key) => {
6153
6364
  };
6154
6365
 
6155
6366
  // src/lib/widgets/textarea.ts
6156
- var renderTextarea = (env, node, state, key) => {
6367
+ var renderTextarea = (env, node, state, key, _path, itemContext) => {
6157
6368
  const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
6158
6369
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
6159
6370
  const editable = state.editable ?? env.normalizeBoolean(node.editable, true);
6160
- const placeholder = env.escapeHtml(env.resolveI18nValue(state.placeholder ?? node.placeholder));
6161
- 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));
6162
6373
  const styleString = env.buildStyleString(node);
6163
6374
  const styleAttribute = styleString ? ` style="${env.escapeHtml(styleString)}"` : "";
6164
6375
  return `<textarea${common}${env.buildWidgetDataAttributes(key, node)} class="vjt-textarea" data-widget="textarea" placeholder="${placeholder}"${enabled ? "" : " disabled"}${editable ? "" : " readonly"}${styleAttribute}>${value}</textarea>`;
6165
6376
  };
6166
6377
 
6167
6378
  // src/lib/widgets/button.ts
6168
- var renderButton = (env, node, state, key) => {
6379
+ var renderButton = (env, node, state, key, _path, itemContext) => {
6169
6380
  const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
6170
6381
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
6171
6382
  const disabledAttribute = enabled ? "" : " disabled";
6172
- const text = env.escapeHtml(env.resolveI18nValue(state.text ?? node.text));
6383
+ const text = env.escapeHtml(env.resolveTextValue(state.text ?? node.text, itemContext));
6173
6384
  const styleString = env.buildStyleString(node);
6174
6385
  const styleAttribute = styleString ? ` style="${env.escapeHtml(styleString)}"` : "";
6175
6386
  const buttonType = env.escapeHtml(node.type === "submit" ? "submit" : "button");
@@ -6218,10 +6429,10 @@ var renderImage = (env, node, state, key) => {
6218
6429
  };
6219
6430
 
6220
6431
  // src/lib/widgets/link.ts
6221
- var renderLink = (env, node, state, key) => {
6432
+ var renderLink = (env, node, state, key, _path, itemContext) => {
6222
6433
  const type = node.type === "button" || node.type === "text" ? node.type : "link";
6223
6434
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
6224
- const text = env.escapeHtml(env.resolveI18nValue(state.text ?? node.text));
6435
+ const text = env.escapeHtml(env.resolveTextValue(state.text ?? node.text, itemContext));
6225
6436
  const classes = `vjt-link vjt-link--${type}${enabled ? "" : " is-disabled"}`;
6226
6437
  const styleString = env.buildStyleString(node);
6227
6438
  const styleAttribute = styleString ? ` style="${env.escapeHtml(styleString)}"` : "";
@@ -6978,6 +7189,7 @@ var RuntimeRenderer = class {
6978
7189
  i18n;
6979
7190
  actionsMap;
6980
7191
  requestsMap;
7192
+ routes;
6981
7193
  sseConfigs;
6982
7194
  systemEvents;
6983
7195
  resourceManager;
@@ -6997,6 +7209,7 @@ var RuntimeRenderer = class {
6997
7209
  vars = /* @__PURE__ */ new Map();
6998
7210
  resizeHandler = null;
6999
7211
  pointerHandler = null;
7212
+ popstateHandler = null;
7000
7213
  eventSources = [];
7001
7214
  referenceRuntime;
7002
7215
  networkRuntime;
@@ -7009,6 +7222,9 @@ var RuntimeRenderer = class {
7009
7222
  activeConfirmModal = null;
7010
7223
  lastPointer = { x: 24, y: 24 };
7011
7224
  pendingResetModalIds = /* @__PURE__ */ new Set();
7225
+ pendingScrollAction = null;
7226
+ pendingScrollFrameId = null;
7227
+ activeScrollAnimationFrameId = null;
7012
7228
  constructor(description, options = {}) {
7013
7229
  this.description = deepClone(description);
7014
7230
  this.resourceManager = options.resourceManager ?? null;
@@ -7022,6 +7238,8 @@ var RuntimeRenderer = class {
7022
7238
  this.theme = options.theme === "light" ? "light" : "dark";
7023
7239
  this.actionsMap = options.actionsMap ?? this.resourceManager?.getActionsMap() ?? {};
7024
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 })) : [];
7025
7243
  const resolvedSseConfigs = options.sseConfigs ?? this.resourceManager?.getSseConfigs() ?? [];
7026
7244
  this.sseConfigs = Array.isArray(resolvedSseConfigs) ? resolvedSseConfigs : resolvedSseConfigs ? [resolvedSseConfigs] : [];
7027
7245
  this.systemEvents = options.systemEvents ?? this.resourceManager?.getSystemEvents() ?? {};
@@ -7035,6 +7253,7 @@ var RuntimeRenderer = class {
7035
7253
  stateByKey: this.stateByKey,
7036
7254
  nodeById: this.nodeById,
7037
7255
  nodeByKey: this.nodeByKey,
7256
+ getUiResource: (ref) => this.resourceManager?.getUi(ref) ?? null,
7038
7257
  getAppValue: (name) => {
7039
7258
  switch (name) {
7040
7259
  case "language":
@@ -7108,6 +7327,9 @@ var RuntimeRenderer = class {
7108
7327
  clearWidget: (widgetId) => this.clearWidget(widgetId),
7109
7328
  clearListElementState: (listKey) => this.clearListElementState(listKey),
7110
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),
7111
7333
  playAudio: (value) => this.voiceRuntime.play(value),
7112
7334
  stopPlaying: () => this.voiceRuntime.stopPlaying(),
7113
7335
  copyToClipboard: (value) => this.copyToClipboard(value),
@@ -7115,7 +7337,7 @@ var RuntimeRenderer = class {
7115
7337
  confirmModal: (args, inputValue) => this.confirmModal(args, inputValue),
7116
7338
  startRecording: () => this.voiceRuntime.startRecording(),
7117
7339
  stopRecording: () => Promise.resolve(this.voiceRuntime.stopRecording()),
7118
- startListening: () => this.voiceRuntime.startListening(),
7340
+ startListening: (args) => this.voiceRuntime.startListening(args),
7119
7341
  stopListening: () => this.voiceRuntime.stopListening(),
7120
7342
  setUiReference: (widgetId, resourceRef) => {
7121
7343
  const node = this.nodeById.get(widgetId);
@@ -7145,11 +7367,12 @@ var RuntimeRenderer = class {
7145
7367
  getInlineActions: (node) => this.getInlineActions(node),
7146
7368
  isWidgetEnabled: (node, key) => this.isWidgetEnabled(node, key)
7147
7369
  });
7148
- this.indexStaticNodes(this.description, "root");
7370
+ this.indexStaticNodes(this.getActiveDescription(), "root");
7149
7371
  this.applyRuntimeSnapshot(this.initialRuntimeSnapshot);
7150
7372
  }
7151
7373
  renderMarkup() {
7152
- 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()}`;
7153
7376
  logRuntimeDebug(this.debugLogging, "html", markup);
7154
7377
  return markup;
7155
7378
  }
@@ -7199,6 +7422,17 @@ var RuntimeRenderer = class {
7199
7422
  };
7200
7423
  window.addEventListener("pointerdown", handlePointer);
7201
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;
7202
7436
  return () => {
7203
7437
  if (this.resizeHandler) {
7204
7438
  window.removeEventListener("resize", this.resizeHandler);
@@ -7206,6 +7440,9 @@ var RuntimeRenderer = class {
7206
7440
  if (this.pointerHandler) {
7207
7441
  window.removeEventListener("pointerdown", this.pointerHandler);
7208
7442
  }
7443
+ if (this.popstateHandler) {
7444
+ window.removeEventListener("popstate", this.popstateHandler);
7445
+ }
7209
7446
  for (const source of this.eventSources) {
7210
7447
  source.close();
7211
7448
  }
@@ -7228,6 +7465,7 @@ var RuntimeRenderer = class {
7228
7465
  this.attachInputBehavior(this.root);
7229
7466
  this.attachWidgetEvents(this.root);
7230
7467
  restoreElementState(this.root, preservedState, this.stateByKey);
7468
+ this.applyPendingScrollAction();
7231
7469
  this.activeContextMenu = adjustContextMenuPosition(this.root, this.activeContextMenu);
7232
7470
  this.pendingResetModalIds.clear();
7233
7471
  if (typeof this.onRuntimeSnapshot === "function") {
@@ -7407,7 +7645,113 @@ var RuntimeRenderer = class {
7407
7645
  this.nodeByKey.clear();
7408
7646
  this.nodeById.clear();
7409
7647
  this.stateById.clear();
7410
- 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
+ };
7411
7755
  }
7412
7756
  getUiReferenceTarget(node, key) {
7413
7757
  if (node.widget !== "ui-reference") {
@@ -7865,6 +8209,20 @@ var RuntimeRenderer = class {
7865
8209
  }
7866
8210
  return `??${key}??`;
7867
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
+ }
7868
8226
  buildWidgetDataAttributes(key, node) {
7869
8227
  const parts = [` data-widget-key="${escapeHtml(key)}"`];
7870
8228
  if (node.id) {
@@ -7883,6 +8241,7 @@ var RuntimeRenderer = class {
7883
8241
  escapeHtml,
7884
8242
  buildStyleString: (node) => this.buildStyleString(node),
7885
8243
  resolveI18nValue: (value) => this.resolveI18nValue(value),
8244
+ resolveTextValue: (value, itemContext) => this.resolveTextValue(value, itemContext),
7886
8245
  getUiResource: (ref) => this.resourceManager?.getUi(ref) ?? null,
7887
8246
  resolveReference: (reference, currentValue, responseValue) => this.referenceRuntime.resolveReference(reference, currentValue, responseValue),
7888
8247
  buildWidgetDataAttributes: (key, node) => this.buildWidgetDataAttributes(key, node),
@@ -8428,6 +8787,138 @@ var RuntimeRenderer = class {
8428
8787
  nestedTarget.focus({ preventScroll: true });
8429
8788
  }
8430
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
+ }
8431
8922
  };
8432
8923
  function renderJson(description, options = {}) {
8433
8924
  const renderer = new RuntimeRenderer(description, options);
@@ -8443,6 +8934,7 @@ var ResourceManager = class {
8443
8934
  ui = /* @__PURE__ */ new Map();
8444
8935
  actions = {};
8445
8936
  requests = {};
8937
+ routes = [];
8446
8938
  sse = {};
8447
8939
  systemEvents = {};
8448
8940
  i18n = {};
@@ -8453,6 +8945,7 @@ var ResourceManager = class {
8453
8945
  }
8454
8946
  this.actions = { ...input.actions ?? {} };
8455
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 })) : [];
8456
8949
  this.sse = { ...input.sse ?? {} };
8457
8950
  this.systemEvents = { ...input.systemEvents ?? {} };
8458
8951
  this.i18n = { ...input.i18n ?? {} };
@@ -8470,6 +8963,9 @@ var ResourceManager = class {
8470
8963
  getRequestsMap() {
8471
8964
  return this.requests;
8472
8965
  }
8966
+ getRoutes() {
8967
+ return this.routes.map((route) => ({ ...route }));
8968
+ }
8473
8969
  getSseConfigs() {
8474
8970
  return Object.values(this.sse);
8475
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.6",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",