@vortexm/vjt 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3683,6 +3683,159 @@ function logRuntimeError(scope, error, details) {
3683
3683
  console.error("[VJT error]", payload);
3684
3684
  }
3685
3685
 
3686
+ // src/lib/security.ts
3687
+ var BLOCKED_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
3688
+ var TRUSTED_CONFIG_ONLY_KEYS = /* @__PURE__ */ new Set(["widget", "style", "css", "actions", "events", "onClick", "onRefresh"]);
3689
+ function isPlainObject(value) {
3690
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3691
+ }
3692
+ function escapeHtmlAttribute(value) {
3693
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
3694
+ }
3695
+ function coerceFiniteNumber(value, kind, path) {
3696
+ if (typeof value === "number" && Number.isFinite(value)) {
3697
+ return kind === "int" ? Math.trunc(value) : value;
3698
+ }
3699
+ if (typeof value === "string" && value.trim().length > 0) {
3700
+ const parsed = kind === "int" ? Number.parseInt(value, 10) : Number.parseFloat(value);
3701
+ if (Number.isFinite(parsed)) {
3702
+ return kind === "int" ? Math.trunc(parsed) : parsed;
3703
+ }
3704
+ }
3705
+ throw new Error(`Invalid ${kind} at ${path}`);
3706
+ }
3707
+ function isBlockedObjectKey(key) {
3708
+ return BLOCKED_OBJECT_KEYS.has(key);
3709
+ }
3710
+ function hasBlockedReferenceSegment(reference) {
3711
+ return reference.split(".").some((segment) => isBlockedObjectKey(segment));
3712
+ }
3713
+ function isTrustedConfigOnlyKey(key) {
3714
+ return TRUSTED_CONFIG_ONLY_KEYS.has(key);
3715
+ }
3716
+ function templateValueUsesReference(value) {
3717
+ if (typeof value === "string") {
3718
+ return value.includes("$ref:");
3719
+ }
3720
+ if (Array.isArray(value)) {
3721
+ return value.some((entry) => templateValueUsesReference(entry));
3722
+ }
3723
+ if (isPlainObject(value)) {
3724
+ return Object.values(value).some((entry) => templateValueUsesReference(entry));
3725
+ }
3726
+ return false;
3727
+ }
3728
+ function sanitizeConfigStyleMap(styleMap) {
3729
+ const safe = {};
3730
+ for (const [key, value] of Object.entries(styleMap)) {
3731
+ if (isBlockedObjectKey(key) || typeof value !== "string") {
3732
+ continue;
3733
+ }
3734
+ safe[key] = value;
3735
+ }
3736
+ return safe;
3737
+ }
3738
+ function sanitizeUrl(rawValue, options = {}) {
3739
+ if (typeof rawValue !== "string") {
3740
+ return null;
3741
+ }
3742
+ const value = rawValue.trim();
3743
+ if (!value) {
3744
+ return null;
3745
+ }
3746
+ if (/^(javascript|data|vbscript):/i.test(value)) {
3747
+ return null;
3748
+ }
3749
+ if (value.startsWith("//")) {
3750
+ return null;
3751
+ }
3752
+ const schemeMatch = value.match(/^([A-Za-z][A-Za-z\d+\-.]*):/);
3753
+ if (!schemeMatch) {
3754
+ return options.allowRelative !== false ? value : null;
3755
+ }
3756
+ const scheme = schemeMatch[1].toLowerCase();
3757
+ if (scheme === "http" || scheme === "https") {
3758
+ return value;
3759
+ }
3760
+ if (scheme === "mailto" && options.allowMailto) {
3761
+ return value;
3762
+ }
3763
+ return null;
3764
+ }
3765
+ function sanitizeMarkdownSource(markdown) {
3766
+ return markdown.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
3767
+ }
3768
+ function sanitizeGeneratedHtml(html) {
3769
+ const withoutDangerousTags = html.replace(/<\s*(script|style|iframe|object|embed|link|meta|base|form)\b[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, "").replace(/<\s*(script|style|iframe|object|embed|link|meta|base|form)\b[^>]*\/?>/gi, "").replace(/\son[a-z]+\s*=\s*(".*?"|'.*?'|[^\s>]+)/gi, "");
3770
+ return withoutDangerousTags.replace(/\s(href|src)\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/gi, (_match, attributeName, _full, dq, sq, bare) => {
3771
+ const rawUrl = dq ?? sq ?? bare ?? "";
3772
+ const safeUrl = sanitizeUrl(rawUrl, {
3773
+ allowRelative: true,
3774
+ allowMailto: attributeName.toLowerCase() === "href"
3775
+ });
3776
+ const fallback = attributeName.toLowerCase() === "href" ? "#" : "";
3777
+ return ` ${attributeName}="${escapeHtmlAttribute(safeUrl ?? fallback)}"`;
3778
+ });
3779
+ }
3780
+ function sanitizeSchemaValue(schema, value, path = "value") {
3781
+ if (value === void 0 || value === null) {
3782
+ return void 0;
3783
+ }
3784
+ if (schema.type === "object") {
3785
+ if (!isPlainObject(value)) {
3786
+ throw new Error(`Expected object at ${path}`);
3787
+ }
3788
+ const result = {};
3789
+ for (const property of schema.properties ?? []) {
3790
+ if (!property.name || isBlockedObjectKey(property.name)) {
3791
+ continue;
3792
+ }
3793
+ result[property.name] = sanitizeSchemaValue(property, value[property.name], `${path}.${property.name}`);
3794
+ }
3795
+ return result;
3796
+ }
3797
+ if (schema.type === "array") {
3798
+ if (!Array.isArray(value)) {
3799
+ throw new Error(`Expected array at ${path}`);
3800
+ }
3801
+ return value.map((entry, index) => sanitizeSchemaValue(schema.elements, entry, `${path}[${index}]`));
3802
+ }
3803
+ switch (schema.type) {
3804
+ case "int":
3805
+ return coerceFiniteNumber(value, "int", path);
3806
+ case "float":
3807
+ return coerceFiniteNumber(value, "float", path);
3808
+ case "boolean":
3809
+ if (typeof value === "boolean") {
3810
+ return value;
3811
+ }
3812
+ if (value === "true" || value === "1" || value === 1) {
3813
+ return true;
3814
+ }
3815
+ if (value === "false" || value === "0" || value === 0) {
3816
+ return false;
3817
+ }
3818
+ throw new Error(`Invalid boolean at ${path}`);
3819
+ case "string": {
3820
+ const stringValue = typeof value === "string" ? value : typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" ? String(value) : (() => {
3821
+ throw new Error(`Invalid string at ${path}`);
3822
+ })();
3823
+ if (typeof schema.maxLength === "number" && stringValue.length > schema.maxLength) {
3824
+ throw new Error(`String too long at ${path}`);
3825
+ }
3826
+ if (typeof schema.regexp === "string" && schema.regexp) {
3827
+ const regexp = new RegExp(schema.regexp);
3828
+ if (!regexp.test(stringValue)) {
3829
+ throw new Error(`String does not match regexp at ${path}`);
3830
+ }
3831
+ }
3832
+ return stringValue;
3833
+ }
3834
+ default:
3835
+ return value;
3836
+ }
3837
+ }
3838
+
3686
3839
  // src/lib/network.ts
3687
3840
  var NetworkRuntime = class {
3688
3841
  requestsMap;
@@ -3707,7 +3860,12 @@ var NetworkRuntime = class {
3707
3860
  if (!config?.url) {
3708
3861
  continue;
3709
3862
  }
3710
- const source = new EventSource(config.url);
3863
+ const safeUrl = sanitizeUrl(config.url, { allowRelative: true });
3864
+ if (!safeUrl) {
3865
+ logRuntimeError("sseConfig", new Error("Blocked unsafe SSE url"), { url: config.url });
3866
+ continue;
3867
+ }
3868
+ const source = new EventSource(safeUrl);
3711
3869
  this.eventSources.push(source);
3712
3870
  for (const eventDefinition of config.events ?? []) {
3713
3871
  if (!eventDefinition?.name) {
@@ -3717,7 +3875,7 @@ var NetworkRuntime = class {
3717
3875
  void this.handleSseEvent(eventDefinition, event).catch((error) => {
3718
3876
  logRuntimeError("handleSseEvent", error, {
3719
3877
  eventName: eventDefinition.name,
3720
- url: config.url
3878
+ url: safeUrl
3721
3879
  });
3722
3880
  });
3723
3881
  });
@@ -3745,7 +3903,11 @@ var NetworkRuntime = class {
3745
3903
  if (!requestUrl) {
3746
3904
  throw new Error(`Request ${requestName} has no url in config`);
3747
3905
  }
3748
- const response = await fetch(requestUrl, {
3906
+ const safeRequestUrl = sanitizeUrl(requestUrl, { allowRelative: true });
3907
+ if (!safeRequestUrl) {
3908
+ throw new Error(`Request ${requestName} has unsafe url`);
3909
+ }
3910
+ const response = await fetch(safeRequestUrl, {
3749
3911
  method: "POST",
3750
3912
  headers: {
3751
3913
  "Content-Type": "application/json"
@@ -3765,7 +3927,13 @@ var NetworkRuntime = class {
3765
3927
  } catch {
3766
3928
  throw new Error(`Request ${requestName} returned non-JSON response: ${responseText.slice(0, 120)}`);
3767
3929
  }
3768
- const result = json.result;
3930
+ if (json.error) {
3931
+ throw new Error(`Request ${requestName} returned JSON-RPC error: ${String(json.error.message ?? "unknown error")}`);
3932
+ }
3933
+ if (!definition.response) {
3934
+ throw new Error(`Request ${requestName} must define response schema`);
3935
+ }
3936
+ const result = sanitizeSchemaValue(definition.response, json.result, `response.${requestName}`);
3769
3937
  if (definition.onResponse?.length) {
3770
3938
  await this.runActions(definition.onResponse, null, {
3771
3939
  currentValue: null,
@@ -3778,7 +3946,7 @@ var NetworkRuntime = class {
3778
3946
  if (schema.type === "object") {
3779
3947
  const result = {};
3780
3948
  for (const property of schema.properties ?? []) {
3781
- if (!property.name) {
3949
+ if (!property.name || isBlockedObjectKey(property.name)) {
3782
3950
  continue;
3783
3951
  }
3784
3952
  result[property.name] = await this.buildSchemaValue(property, currentValue, responseValue);
@@ -3834,12 +4002,15 @@ var NetworkRuntime = class {
3834
4002
  }
3835
4003
  }
3836
4004
  async handleSseEvent(eventDefinition, event) {
3837
- let message = {};
4005
+ let parsedMessage = {};
3838
4006
  try {
3839
- message = event.data ? JSON.parse(event.data) : {};
4007
+ parsedMessage = event.data ? JSON.parse(event.data) : {};
3840
4008
  } catch {
3841
- message = { value: event.data ?? "" };
4009
+ parsedMessage = { value: event.data ?? "" };
3842
4010
  }
4011
+ const message = eventDefinition.message ? sanitizeSchemaValue(eventDefinition.message, parsedMessage, `sse.${eventDefinition.name}`) : (() => {
4012
+ throw new Error(`SSE event ${eventDefinition.name} must define message schema`);
4013
+ })();
3843
4014
  if (!eventDefinition.onEvent?.length) {
3844
4015
  return;
3845
4016
  }
@@ -3852,12 +4023,15 @@ var NetworkRuntime = class {
3852
4023
  };
3853
4024
 
3854
4025
  // src/lib/references.ts
3855
- function isPlainObject(value) {
4026
+ function isPlainObject2(value) {
3856
4027
  return typeof value === "object" && value !== null && !Array.isArray(value);
3857
4028
  }
3858
4029
  function readPath(target, path) {
3859
4030
  let current = target;
3860
4031
  for (const segment of path) {
4032
+ if (isBlockedObjectKey(segment)) {
4033
+ return void 0;
4034
+ }
3861
4035
  if (current == null) {
3862
4036
  return void 0;
3863
4037
  }
@@ -3869,6 +4043,9 @@ function readPath(target, path) {
3869
4043
  if (typeof current !== "object") {
3870
4044
  return void 0;
3871
4045
  }
4046
+ if (!Object.prototype.hasOwnProperty.call(current, segment)) {
4047
+ return void 0;
4048
+ }
3872
4049
  current = current[segment];
3873
4050
  }
3874
4051
  return current;
@@ -3895,6 +4072,18 @@ function stringifyReferenceValue(value) {
3895
4072
  }
3896
4073
  return JSON.stringify(value);
3897
4074
  }
4075
+ function isReferenceValueEmpty(value) {
4076
+ if (value === null || value === void 0) {
4077
+ return true;
4078
+ }
4079
+ if (typeof value === "string") {
4080
+ return value.length === 0;
4081
+ }
4082
+ if (Array.isArray(value)) {
4083
+ return value.length === 0;
4084
+ }
4085
+ return false;
4086
+ }
3898
4087
  function getCookieValue(name) {
3899
4088
  if (typeof document === "undefined") {
3900
4089
  return void 0;
@@ -3919,16 +4108,27 @@ function getCookieValue(name) {
3919
4108
  }
3920
4109
  return void 0;
3921
4110
  }
3922
- function setCookieValue(name, value) {
4111
+ function setCookieValue(name, value, ttlDays) {
3923
4112
  if (typeof document === "undefined") {
3924
4113
  return;
3925
4114
  }
3926
- document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; path=/`;
4115
+ const ttlValue = typeof ttlDays === "number" && Number.isFinite(ttlDays) ? ttlDays : null;
4116
+ const expires = ttlValue !== null ? `; max-age=${Math.max(0, Math.round(ttlValue * 24 * 60 * 60))}` : "";
4117
+ document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; path=/${expires}`;
4118
+ }
4119
+ function getUrlParamValue(name) {
4120
+ if (typeof window === "undefined") {
4121
+ return void 0;
4122
+ }
4123
+ return new URLSearchParams(window.location.search).get(name) ?? void 0;
3927
4124
  }
3928
4125
  var ReferenceRuntime = class {
3929
4126
  constructor(host) {
3930
4127
  this.host = host;
3931
4128
  }
4129
+ isEmptyReference(reference, currentValue, responseValue) {
4130
+ return isReferenceValueEmpty(this.resolveReference(reference, currentValue, responseValue));
4131
+ }
3932
4132
  resolveReference(reference, currentValue, responseValue) {
3933
4133
  if (reference === "current") {
3934
4134
  return currentValue;
@@ -3951,6 +4151,9 @@ var ReferenceRuntime = class {
3951
4151
  if (reference.startsWith("app.")) {
3952
4152
  return this.host.getAppValue(reference.slice(4));
3953
4153
  }
4154
+ if (reference.startsWith("url.")) {
4155
+ return getUrlParamValue(reference.slice(4));
4156
+ }
3954
4157
  const [widgetId, ...rest] = reference.split(".");
3955
4158
  const state = this.host.stateById.get(widgetId);
3956
4159
  if (!state) {
@@ -3982,7 +4185,7 @@ var ReferenceRuntime = class {
3982
4185
  }
3983
4186
  return readPath(currentValue.descriptor, reference.split("."));
3984
4187
  }
3985
- if (isPlainObject(currentValue) || Array.isArray(currentValue)) {
4188
+ if (isPlainObject2(currentValue) || Array.isArray(currentValue)) {
3986
4189
  return readPath(currentValue, reference.split("."));
3987
4190
  }
3988
4191
  return void 0;
@@ -4007,7 +4210,7 @@ var ReferenceRuntime = class {
4007
4210
  if (state.widget === "combobox") {
4008
4211
  const index = state.selected ?? 0;
4009
4212
  const entry = (state.comboboxElements ?? [])[index];
4010
- if (isPlainObject(entry)) {
4213
+ if (isPlainObject2(entry)) {
4011
4214
  return entry.value ?? entry.text ?? index;
4012
4215
  }
4013
4216
  return index;
@@ -4015,7 +4218,7 @@ var ReferenceRuntime = class {
4015
4218
  if (state.widget === "listbox") {
4016
4219
  const index = state.selected ?? 0;
4017
4220
  const entry = (state.listboxElements ?? [])[index];
4018
- if (isPlainObject(entry)) {
4221
+ if (isPlainObject2(entry)) {
4019
4222
  return entry.value ?? entry.text ?? index;
4020
4223
  }
4021
4224
  return index;
@@ -4064,12 +4267,12 @@ var ReferenceRuntime = class {
4064
4267
  return readPath(state, field.split("."));
4065
4268
  }
4066
4269
  }
4067
- assignReference(reference, inputValue, _currentValue) {
4068
- if (reference.startsWith("current.")) {
4270
+ assignReference(reference, inputValue, _currentValue, options) {
4271
+ if (reference.startsWith("current.") || hasBlockedReferenceSegment(reference)) {
4069
4272
  return;
4070
4273
  }
4071
4274
  if (reference.startsWith("cookies.")) {
4072
- setCookieValue(reference.slice(8), stringifyReferenceValue(inputValue));
4275
+ setCookieValue(reference.slice(8), stringifyReferenceValue(inputValue), toFiniteNumber(options?.cookieTtl) ?? void 0);
4073
4276
  return;
4074
4277
  }
4075
4278
  if (reference.startsWith("app.")) {
@@ -4110,7 +4313,7 @@ var ReferenceRuntime = class {
4110
4313
  case "elements":
4111
4314
  if (state.widget === "list" || state.widget === "grid-view") {
4112
4315
  this.clearListElementState(state.key);
4113
- state.elements = Array.isArray(inputValue) ? inputValue.filter((item) => isPlainObject(item) && typeof item.widget === "string") : [];
4316
+ state.elements = Array.isArray(inputValue) ? inputValue.filter((item) => isPlainObject2(item) && typeof item.widget === "string") : [];
4114
4317
  state.elements.forEach((element, index) => {
4115
4318
  const listNode = this.host.nodeById.get(widgetId);
4116
4319
  if (listNode?.widget === "list" || listNode?.widget === "grid-view") {
@@ -4119,10 +4322,10 @@ var ReferenceRuntime = class {
4119
4322
  });
4120
4323
  }
4121
4324
  if (state.widget === "listbox") {
4122
- state.listboxElements = Array.isArray(inputValue) ? inputValue.filter((item) => isPlainObject(item)) : [];
4325
+ state.listboxElements = Array.isArray(inputValue) ? inputValue.filter((item) => isPlainObject2(item)) : [];
4123
4326
  }
4124
4327
  if (state.widget === "combobox") {
4125
- state.comboboxElements = Array.isArray(inputValue) ? inputValue.filter((item) => isPlainObject(item)) : [];
4328
+ state.comboboxElements = Array.isArray(inputValue) ? inputValue.filter((item) => isPlainObject2(item)) : [];
4126
4329
  }
4127
4330
  break;
4128
4331
  default:
@@ -4161,9 +4364,15 @@ var ReferenceRuntime = class {
4161
4364
  if (Array.isArray(template)) {
4162
4365
  return template.map((entry) => this.resolveMappedValue(entry, currentValue, responseValue));
4163
4366
  }
4164
- if (isPlainObject(template)) {
4367
+ if (isPlainObject2(template)) {
4165
4368
  const result = {};
4166
4369
  for (const [key, value] of Object.entries(template)) {
4370
+ if (isBlockedObjectKey(key)) {
4371
+ continue;
4372
+ }
4373
+ if (isTrustedConfigOnlyKey(key) && templateValueUsesReference(value)) {
4374
+ continue;
4375
+ }
4167
4376
  result[key] = this.resolveMappedValue(value, currentValue, responseValue);
4168
4377
  }
4169
4378
  return result;
@@ -4171,7 +4380,7 @@ var ReferenceRuntime = class {
4171
4380
  return template;
4172
4381
  }
4173
4382
  isListElementContext(value) {
4174
- return isPlainObject(value) && value.kind === "list-element" && typeof value.listId === "string" && typeof value.index === "number" && isPlainObject(value.descriptor) && typeof value.descriptor.widget === "string";
4383
+ return isPlainObject2(value) && value.kind === "list-element" && typeof value.listId === "string" && typeof value.index === "number" && isPlainObject2(value.descriptor) && typeof value.descriptor.widget === "string";
4175
4384
  }
4176
4385
  findNamedWidget(node, name) {
4177
4386
  if (!node) {
@@ -4251,6 +4460,7 @@ var ActionRuntime = class {
4251
4460
  dispatchWidgetEventImpl;
4252
4461
  executeRequest;
4253
4462
  resolveReference;
4463
+ isEmptyReference;
4254
4464
  assignReference;
4255
4465
  resolveMappedValue;
4256
4466
  setWidgetEnabled;
@@ -4258,6 +4468,7 @@ var ActionRuntime = class {
4258
4468
  setUiReference;
4259
4469
  refreshWidgetTree;
4260
4470
  renderWidgetTree;
4471
+ navigateTo;
4261
4472
  getContextMenuPosition;
4262
4473
  setActiveContextMenu;
4263
4474
  setActiveModalId;
@@ -4273,6 +4484,7 @@ var ActionRuntime = class {
4273
4484
  this.dispatchWidgetEventImpl = options.dispatchWidgetEvent;
4274
4485
  this.executeRequest = options.executeRequest;
4275
4486
  this.resolveReference = options.resolveReference;
4487
+ this.isEmptyReference = options.isEmptyReference;
4276
4488
  this.assignReference = options.assignReference;
4277
4489
  this.resolveMappedValue = options.resolveMappedValue;
4278
4490
  this.setWidgetEnabled = options.setWidgetEnabled;
@@ -4280,6 +4492,7 @@ var ActionRuntime = class {
4280
4492
  this.setUiReference = options.setUiReference;
4281
4493
  this.refreshWidgetTree = options.refreshWidgetTree;
4282
4494
  this.renderWidgetTree = options.renderWidgetTree;
4495
+ this.navigateTo = options.navigateTo;
4283
4496
  this.getContextMenuPosition = options.getContextMenuPosition;
4284
4497
  this.setActiveContextMenu = options.setActiveContextMenu;
4285
4498
  this.setActiveModalId = options.setActiveModalId;
@@ -4289,7 +4502,7 @@ var ActionRuntime = class {
4289
4502
  }
4290
4503
  async dispatchWidgetEvent(node, eventName, key, inputValue = null, pointer = null) {
4291
4504
  try {
4292
- if ((eventName === "onClick" || eventName === "onUserValueChange") && !this.isWidgetEnabled(node, key)) {
4505
+ if ((eventName === "onClick" || eventName === "onUserValueChange" || eventName === "onEnter" || eventName === "onShiftEnter" || eventName === "onControlEnter") && !this.isWidgetEnabled(node, key)) {
4293
4506
  return;
4294
4507
  }
4295
4508
  const actions = node.events?.[eventName] ?? (eventName === "onClick" ? this.getInlineActions(node) : void 0);
@@ -4365,6 +4578,10 @@ var ActionRuntime = class {
4365
4578
  const requestName = name.slice(8);
4366
4579
  return this.executeRequest(requestName, context.currentValue);
4367
4580
  }
4581
+ if (name.startsWith("navigate ")) {
4582
+ await this.navigateTo(name.slice(9));
4583
+ return null;
4584
+ }
4368
4585
  if (name.startsWith("showMenu ")) {
4369
4586
  const menuId = name.slice(9);
4370
4587
  const menuState = this.stateById.get(menuId);
@@ -4418,8 +4635,12 @@ var ActionRuntime = class {
4418
4635
  if (name.startsWith("equals ")) {
4419
4636
  return this.resolveReference(name.slice(7), context.currentValue, context.responseValue) === action.args;
4420
4637
  }
4638
+ if (name.startsWith("isEmpty ")) {
4639
+ return this.isEmptyReference(name.slice(8), context.currentValue, context.responseValue);
4640
+ }
4421
4641
  if (name.startsWith("set ")) {
4422
- this.assignReference(name.slice(4), inputValue, context.currentValue);
4642
+ const args = typeof action.args === "object" && action.args !== null && !Array.isArray(action.args) ? action.args : void 0;
4643
+ this.assignReference(name.slice(4), inputValue, context.currentValue, args);
4423
4644
  return inputValue;
4424
4645
  }
4425
4646
  if (name.startsWith("filter ")) {
@@ -4448,7 +4669,7 @@ function isIfElseArgs(value) {
4448
4669
  }
4449
4670
 
4450
4671
  // src/lib/dom-state.ts
4451
- function isPlainObject2(value) {
4672
+ function isPlainObject3(value) {
4452
4673
  return typeof value === "object" && value !== null && !Array.isArray(value);
4453
4674
  }
4454
4675
  function supportsTextSelection(element) {
@@ -4618,7 +4839,7 @@ function getListElementContextFromElement(element, stateByKey) {
4618
4839
  }
4619
4840
  const listState = stateByKey.get(listId);
4620
4841
  const descriptor = listState?.elements?.[index];
4621
- if (!descriptor || !isPlainObject2(descriptor) || typeof descriptor.widget !== "string") {
4842
+ if (!descriptor || !isPlainObject3(descriptor) || typeof descriptor.widget !== "string") {
4622
4843
  return null;
4623
4844
  }
4624
4845
  return {
@@ -5135,7 +5356,7 @@ var renderMdText = (env, node, state, key) => {
5135
5356
  const verticalAlign = VERTICAL_ALIGN_MAP2[node["vert-align"] ?? "center"];
5136
5357
  const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
5137
5358
  const markdown = env.resolveI18nValue(state.text ?? node.text);
5138
- const html = markdownConverter.makeHtml(markdown);
5359
+ const html = sanitizeGeneratedHtml(markdownConverter.makeHtml(sanitizeMarkdownSource(markdown)));
5139
5360
  const inlineStyle = [
5140
5361
  "display:flex",
5141
5362
  `align-items:${verticalAlign}`,
@@ -5241,7 +5462,7 @@ var renderCheckbox = (env, node, state, key) => {
5241
5462
 
5242
5463
  // src/lib/widgets/image.ts
5243
5464
  var renderImage = (env, node, state, key) => {
5244
- const url = env.escapeHtml(state.url ?? node.url ?? "");
5465
+ const url = env.escapeHtml(sanitizeUrl(state.url ?? node.url ?? "", { allowRelative: true }) ?? "");
5245
5466
  const styleParts = [
5246
5467
  "box-sizing:border-box",
5247
5468
  "width:100%",
@@ -5268,7 +5489,7 @@ var renderLink = (env, node, state, key) => {
5268
5489
  const classes = `vjt-link vjt-link--${type}${enabled ? "" : " is-disabled"}`;
5269
5490
  const styleString = env.buildStyleString(node);
5270
5491
  const styleAttribute = styleString ? ` style="${env.escapeHtml(styleString)}"` : "";
5271
- const href = enabled && typeof node.url === "string" ? node.url : "#";
5492
+ const href = enabled ? sanitizeUrl(state.url ?? node.url, { allowRelative: true, allowMailto: true }) ?? "#" : "#";
5272
5493
  const tabIndex = enabled ? "" : ' tabindex="-1" aria-disabled="true"';
5273
5494
  return `<a${state.id ? ` id="${env.escapeHtml(state.id)}"` : ""}${env.buildWidgetDataAttributes(key, node)} class="${classes}" data-widget="link" href="${env.escapeHtml(href)}"${tabIndex}${styleAttribute}>${text}</a>`;
5274
5495
  };
@@ -5386,6 +5607,31 @@ function renderContextMenuOverlay(env, menu) {
5386
5607
  }
5387
5608
 
5388
5609
  // src/lib/widgets/bindings.ts
5610
+ function bindKeyboardSubmitEvents(element, node, key, env) {
5611
+ const hasKeyboardEvents = Boolean(
5612
+ node.events?.onEnter?.length || node.events?.onShiftEnter?.length || node.events?.onControlEnter?.length
5613
+ );
5614
+ if (!hasKeyboardEvents) {
5615
+ return;
5616
+ }
5617
+ element.addEventListener("keydown", (event) => {
5618
+ if (event.key !== "Enter") {
5619
+ return;
5620
+ }
5621
+ let eventName = null;
5622
+ if (event.ctrlKey) {
5623
+ eventName = "onControlEnter";
5624
+ } else if (event.shiftKey) {
5625
+ eventName = "onShiftEnter";
5626
+ } else {
5627
+ eventName = "onEnter";
5628
+ }
5629
+ if (!eventName || !node.events?.[eventName]?.length) {
5630
+ return;
5631
+ }
5632
+ void env.dispatchWidgetEvent(node, eventName, key, env.getActionInputValueForElement(element, node), null);
5633
+ });
5634
+ }
5389
5635
  function bindWidgetInputBehavior(root, env) {
5390
5636
  for (const input of Array.from(root.querySelectorAll(".vjt-edit"))) {
5391
5637
  if (!(input instanceof HTMLInputElement)) {
@@ -5453,6 +5699,7 @@ function bindWidgetEvents(root, env) {
5453
5699
  continue;
5454
5700
  }
5455
5701
  if (element instanceof HTMLInputElement && element.dataset.widget === "edit") {
5702
+ bindKeyboardSubmitEvents(element, node, key, env);
5456
5703
  element.addEventListener("input", () => {
5457
5704
  const state = env.stateByKey.get(key);
5458
5705
  if (state) {
@@ -5470,6 +5717,7 @@ function bindWidgetEvents(root, env) {
5470
5717
  continue;
5471
5718
  }
5472
5719
  if (element instanceof HTMLTextAreaElement && element.dataset.widget === "textarea") {
5720
+ bindKeyboardSubmitEvents(element, node, key, env);
5473
5721
  element.addEventListener("input", () => {
5474
5722
  const state = env.stateByKey.get(key);
5475
5723
  if (state) {
@@ -5487,6 +5735,7 @@ function bindWidgetEvents(root, env) {
5487
5735
  continue;
5488
5736
  }
5489
5737
  if (element instanceof HTMLInputElement && element.dataset.widget === "checkbox") {
5738
+ bindKeyboardSubmitEvents(element, node, key, env);
5490
5739
  element.addEventListener("change", () => {
5491
5740
  const state = env.stateByKey.get(key);
5492
5741
  if (state) {
@@ -5504,6 +5753,7 @@ function bindWidgetEvents(root, env) {
5504
5753
  continue;
5505
5754
  }
5506
5755
  if (element instanceof HTMLSelectElement && (element.dataset.widget === "listbox" || element.dataset.widget === "combobox")) {
5756
+ bindKeyboardSubmitEvents(element, node, key, env);
5507
5757
  element.addEventListener("change", () => {
5508
5758
  const state = env.stateByKey.get(key);
5509
5759
  if (state) {
@@ -5521,6 +5771,7 @@ function bindWidgetEvents(root, env) {
5521
5771
  continue;
5522
5772
  }
5523
5773
  if (element instanceof HTMLInputElement && element.dataset.widget === "radio-group") {
5774
+ bindKeyboardSubmitEvents(element, node, key, env);
5524
5775
  element.addEventListener("change", () => {
5525
5776
  const state = env.stateByKey.get(key);
5526
5777
  if (state) {
@@ -5800,7 +6051,7 @@ function toFiniteNumber3(value) {
5800
6051
  }
5801
6052
  return null;
5802
6053
  }
5803
- function isPlainObject3(value) {
6054
+ function isPlainObject4(value) {
5804
6055
  return typeof value === "object" && value !== null && !Array.isArray(value);
5805
6056
  }
5806
6057
  function deepClone(value) {
@@ -5810,6 +6061,9 @@ function sanitizeIdFragment2(value) {
5810
6061
  const cleaned = value.replaceAll(/[^A-Za-z0-9_-]+/g, "");
5811
6062
  return cleaned || "item";
5812
6063
  }
6064
+ function buildDefinitionSignature(value) {
6065
+ return JSON.stringify(value) ?? "null";
6066
+ }
5813
6067
  function isGeneratedElementId(value) {
5814
6068
  return !value.includes(".") && !value.includes(":") && /Element\d+/.test(value);
5815
6069
  }
@@ -5855,6 +6109,7 @@ var RuntimeRenderer = class {
5855
6109
  actionsMap;
5856
6110
  requestsMap;
5857
6111
  sseConfigs;
6112
+ systemEvents;
5858
6113
  resourceManager;
5859
6114
  actionFunctions;
5860
6115
  backendUrl;
@@ -5883,7 +6138,7 @@ var RuntimeRenderer = class {
5883
6138
  constructor(description, options = {}) {
5884
6139
  this.description = deepClone(description);
5885
6140
  this.resourceManager = options.resourceManager ?? null;
5886
- const customStyleMap = options.styleMap ?? this.resourceManager?.getStyleMap() ?? {};
6141
+ const customStyleMap = sanitizeConfigStyleMap(options.styleMap ?? this.resourceManager?.getStyleMap() ?? {});
5887
6142
  this.styleMap = {
5888
6143
  ...DEFAULT_STYLE_MAP,
5889
6144
  ...customStyleMap
@@ -5895,6 +6150,7 @@ var RuntimeRenderer = class {
5895
6150
  this.requestsMap = options.requestsMap ?? this.resourceManager?.getRequestsMap() ?? {};
5896
6151
  const resolvedSseConfigs = options.sseConfigs ?? this.resourceManager?.getSseConfigs() ?? [];
5897
6152
  this.sseConfigs = Array.isArray(resolvedSseConfigs) ? resolvedSseConfigs : resolvedSseConfigs ? [resolvedSseConfigs] : [];
6153
+ this.systemEvents = options.systemEvents ?? this.resourceManager?.getSystemEvents() ?? {};
5898
6154
  this.actionFunctions = options.actionFunctions ?? {};
5899
6155
  this.backendUrl = options.backendUrl;
5900
6156
  this.initialRuntimeSnapshot = options.runtimeSnapshot ? deepClone(options.runtimeSnapshot) : null;
@@ -5910,6 +6166,8 @@ var RuntimeRenderer = class {
5910
6166
  return this.language;
5911
6167
  case "theme":
5912
6168
  return this.theme;
6169
+ case "urlPath":
6170
+ return typeof window === "undefined" ? "" : window.location.pathname;
5913
6171
  default:
5914
6172
  return void 0;
5915
6173
  }
@@ -5923,6 +6181,15 @@ var RuntimeRenderer = class {
5923
6181
  this.theme = typeof value === "boolean" ? value ? "light" : "dark" : value === "light" || value === "dark" ? value : "dark";
5924
6182
  document.documentElement.dataset.vjtTheme = this.theme;
5925
6183
  break;
6184
+ case "urlPath": {
6185
+ if (typeof window === "undefined") {
6186
+ break;
6187
+ }
6188
+ const nextPath = typeof value === "string" && value.trim() ? value.trim() : "/";
6189
+ const normalizedPath = nextPath.startsWith("/") ? nextPath : `/${nextPath}`;
6190
+ window.history.replaceState(window.history.state, "", `${normalizedPath}${window.location.search}${window.location.hash}`);
6191
+ break;
6192
+ }
5926
6193
  default:
5927
6194
  break;
5928
6195
  }
@@ -5948,7 +6215,8 @@ var RuntimeRenderer = class {
5948
6215
  dispatchWidgetEvent: (node, eventName, key, inputValue, pointer) => this.actionRuntime.dispatchWidgetEvent(node, eventName, key, inputValue, pointer),
5949
6216
  executeRequest: (requestName, currentValue) => this.networkRuntime.executeRequest(requestName, currentValue),
5950
6217
  resolveReference: (reference, currentValue, responseValue) => this.referenceRuntime.resolveReference(reference, currentValue, responseValue),
5951
- assignReference: (reference, inputValue, currentValue) => this.referenceRuntime.assignReference(reference, inputValue, currentValue),
6218
+ isEmptyReference: (reference, currentValue, responseValue) => this.referenceRuntime.isEmptyReference(reference, currentValue, responseValue),
6219
+ assignReference: (reference, inputValue, currentValue, options2) => this.referenceRuntime.assignReference(reference, inputValue, currentValue, options2),
5952
6220
  resolveMappedValue: (template, currentValue, responseValue) => this.referenceRuntime.resolveMappedValue(template, currentValue, responseValue),
5953
6221
  setWidgetEnabled: (widgetId, enabled) => this.setWidgetEnabled(widgetId, enabled),
5954
6222
  clearWidget: (widgetId) => this.clearWidget(widgetId),
@@ -5968,6 +6236,7 @@ var RuntimeRenderer = class {
5968
6236
  },
5969
6237
  refreshWidgetTree: (widgetId) => this.refreshWidgetTree(widgetId),
5970
6238
  renderWidgetTree: (widgetId) => this.renderWidgetTree(widgetId),
6239
+ navigateTo: (url) => this.navigateTo(url),
5971
6240
  getContextMenuPosition: (menuId, pointer) => getContextMenuPosition(menuId, pointer ?? null, this.lastPointer, this.nodeById),
5972
6241
  setActiveContextMenu: (menu) => {
5973
6242
  this.activeContextMenu = menu;
@@ -5996,7 +6265,9 @@ var RuntimeRenderer = class {
5996
6265
  this.afterRender = options.afterRender;
5997
6266
  this.onRuntimeSnapshot = options.onRuntimeSnapshot ?? this.onRuntimeSnapshot;
5998
6267
  const render = async () => {
6268
+ await this.runSystemEvent("onBeforeRender");
5999
6269
  await this.rerenderRoot();
6270
+ await this.runSystemEvent("onAfterRender");
6000
6271
  };
6001
6272
  const renderSync = () => {
6002
6273
  void render().catch((error) => {
@@ -6064,6 +6335,31 @@ var RuntimeRenderer = class {
6064
6335
  }
6065
6336
  return Promise.resolve();
6066
6337
  }
6338
+ async runSystemEvent(eventName) {
6339
+ const actions = this.systemEvents[eventName];
6340
+ if (!actions?.length) {
6341
+ return;
6342
+ }
6343
+ await this.actionRuntime.runActions(actions, null, { currentValue: null });
6344
+ }
6345
+ async navigateTo(url) {
6346
+ if (typeof window === "undefined" || !url.trim()) {
6347
+ return;
6348
+ }
6349
+ const safeUrl = sanitizeUrl(url, { allowRelative: true });
6350
+ if (!safeUrl) {
6351
+ throw new Error("Blocked unsafe navigation url");
6352
+ }
6353
+ const nextUrl = new URL(safeUrl, window.location.href);
6354
+ if (nextUrl.origin !== window.location.origin) {
6355
+ window.location.assign(nextUrl.toString());
6356
+ return;
6357
+ }
6358
+ window.history.pushState(window.history.state, "", `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`);
6359
+ await this.runSystemEvent("onBeforeNavigate");
6360
+ await this.rerenderRoot();
6361
+ await this.runSystemEvent("onAfterNavigate");
6362
+ }
6067
6363
  reindexStaticTree() {
6068
6364
  this.nodeByKey.clear();
6069
6365
  this.nodeById.clear();
@@ -6292,6 +6588,7 @@ var RuntimeRenderer = class {
6292
6588
  existing.id = stateId2;
6293
6589
  this.stateById.set(stateId2, existing);
6294
6590
  }
6591
+ this.syncStateDefinition(existing, node);
6295
6592
  return existing;
6296
6593
  }
6297
6594
  const stateId = node.id ?? (isGeneratedElementId(key) ? key : void 0);
@@ -6361,6 +6658,7 @@ var RuntimeRenderer = class {
6361
6658
  id: stateId,
6362
6659
  name: node.name,
6363
6660
  enabled: normalizeBoolean(node.enabled, true),
6661
+ definitionSignature: buildDefinitionSignature(node.elements ?? []),
6364
6662
  elements: deepClone(node.elements ?? [])
6365
6663
  };
6366
6664
  break;
@@ -6371,6 +6669,7 @@ var RuntimeRenderer = class {
6371
6669
  id: stateId,
6372
6670
  name: node.name,
6373
6671
  enabled: normalizeBoolean(node.enabled, true),
6672
+ definitionSignature: buildDefinitionSignature(node.elements ?? []),
6374
6673
  listboxElements: deepClone(node.elements ?? []),
6375
6674
  selected: toFiniteNumber3(node.selected) ?? 0
6376
6675
  };
@@ -6382,6 +6681,7 @@ var RuntimeRenderer = class {
6382
6681
  id: stateId,
6383
6682
  name: node.name,
6384
6683
  enabled: normalizeBoolean(node.enabled, true),
6684
+ definitionSignature: buildDefinitionSignature(node.elements ?? []),
6385
6685
  comboboxElements: deepClone(node.elements ?? []),
6386
6686
  selected: toFiniteNumber3(node.selected) ?? 0
6387
6687
  };
@@ -6445,6 +6745,45 @@ var RuntimeRenderer = class {
6445
6745
  }
6446
6746
  return state;
6447
6747
  }
6748
+ syncStateDefinition(state, node) {
6749
+ if (state.widget !== node.widget) {
6750
+ return;
6751
+ }
6752
+ switch (node.widget) {
6753
+ case "list":
6754
+ case "grid-view": {
6755
+ const nextSignature = buildDefinitionSignature(node.elements ?? []);
6756
+ if (state.definitionSignature !== nextSignature) {
6757
+ if (state.widget === "list" || state.widget === "grid-view") {
6758
+ this.clearListElementState(state.key);
6759
+ state.elements = deepClone(node.elements ?? []);
6760
+ state.definitionSignature = nextSignature;
6761
+ }
6762
+ }
6763
+ break;
6764
+ }
6765
+ case "listbox": {
6766
+ const nextSignature = buildDefinitionSignature(node.elements ?? []);
6767
+ if (state.definitionSignature !== nextSignature) {
6768
+ state.listboxElements = deepClone(node.elements ?? []);
6769
+ state.selected = toFiniteNumber3(node.selected) ?? 0;
6770
+ state.definitionSignature = nextSignature;
6771
+ }
6772
+ break;
6773
+ }
6774
+ case "combobox": {
6775
+ const nextSignature = buildDefinitionSignature(node.elements ?? []);
6776
+ if (state.definitionSignature !== nextSignature) {
6777
+ state.comboboxElements = deepClone(node.elements ?? []);
6778
+ state.selected = toFiniteNumber3(node.selected) ?? 0;
6779
+ state.definitionSignature = nextSignature;
6780
+ }
6781
+ break;
6782
+ }
6783
+ default:
6784
+ break;
6785
+ }
6786
+ }
6448
6787
  getStateForNode(node, fallbackPath) {
6449
6788
  const key = fallbackPath ? this.resolveNodeKey(node, fallbackPath) : node.id ?? this.resolveNodeKey(node, `auto:${node.widget}`);
6450
6789
  return this.ensureWidgetState(node, key);
@@ -6835,7 +7174,7 @@ var RuntimeRenderer = class {
6835
7174
  return;
6836
7175
  }
6837
7176
  const descriptor = listState.elements[indexValue];
6838
- if (!descriptor || !isPlainObject3(descriptor) || typeof descriptor.widget !== "string") {
7177
+ if (!descriptor || !isPlainObject4(descriptor) || typeof descriptor.widget !== "string") {
6839
7178
  return;
6840
7179
  }
6841
7180
  mutate(descriptor);
@@ -7026,6 +7365,7 @@ var ResourceManager = class {
7026
7365
  actions = {};
7027
7366
  requests = {};
7028
7367
  sse = {};
7368
+ systemEvents = {};
7029
7369
  i18n = {};
7030
7370
  styles = {};
7031
7371
  constructor(input = {}) {
@@ -7035,6 +7375,7 @@ var ResourceManager = class {
7035
7375
  this.actions = { ...input.actions ?? {} };
7036
7376
  this.requests = { ...input.requests ?? {} };
7037
7377
  this.sse = { ...input.sse ?? {} };
7378
+ this.systemEvents = { ...input.systemEvents ?? {} };
7038
7379
  this.i18n = { ...input.i18n ?? {} };
7039
7380
  this.styles = { ...input.styles ?? {} };
7040
7381
  }
@@ -7053,6 +7394,9 @@ var ResourceManager = class {
7053
7394
  getSseConfigs() {
7054
7395
  return Object.values(this.sse);
7055
7396
  }
7397
+ getSystemEvents() {
7398
+ return this.systemEvents;
7399
+ }
7056
7400
  getI18n() {
7057
7401
  return this.i18n;
7058
7402
  }
@@ -19,13 +19,17 @@ type ActionRuntimeOptions = {
19
19
  } | null) => Promise<void>;
20
20
  executeRequest: (requestName: string, currentValue: unknown) => Promise<unknown>;
21
21
  resolveReference: (reference: string, currentValue: unknown, responseValue: unknown) => unknown;
22
- assignReference: (reference: string, inputValue: unknown, currentValue: unknown) => void;
22
+ isEmptyReference: (reference: string, currentValue: unknown, responseValue: unknown) => boolean;
23
+ assignReference: (reference: string, inputValue: unknown, currentValue: unknown, options?: {
24
+ cookieTtl?: unknown;
25
+ }) => void;
23
26
  resolveMappedValue: (template: unknown, currentValue: unknown, responseValue: unknown) => unknown;
24
27
  setWidgetEnabled: (widgetId: string, enabled: boolean) => void;
25
28
  clearWidget: (widgetId: string) => void;
26
29
  setUiReference: (widgetId: string, resourceRef: string) => void;
27
30
  refreshWidgetTree: (widgetId: string) => Promise<void>;
28
31
  renderWidgetTree: (widgetId: string) => Promise<void>;
32
+ navigateTo: (url: string) => Promise<void>;
29
33
  getContextMenuPosition: (menuId: string, pointer?: {
30
34
  x: number;
31
35
  y: number;
@@ -52,6 +56,7 @@ export declare class ActionRuntime {
52
56
  private readonly dispatchWidgetEventImpl;
53
57
  private readonly executeRequest;
54
58
  private readonly resolveReference;
59
+ private readonly isEmptyReference;
55
60
  private readonly assignReference;
56
61
  private readonly resolveMappedValue;
57
62
  private readonly setWidgetEnabled;
@@ -59,6 +64,7 @@ export declare class ActionRuntime {
59
64
  private readonly setUiReference;
60
65
  private readonly refreshWidgetTree;
61
66
  private readonly renderWidgetTree;
67
+ private readonly navigateTo;
62
68
  private readonly getContextMenuPosition;
63
69
  private readonly setActiveContextMenu;
64
70
  private readonly setActiveModalId;
@@ -13,10 +13,13 @@ type ReferenceRuntimeHost = {
13
13
  export declare class ReferenceRuntime {
14
14
  private readonly host;
15
15
  constructor(host: ReferenceRuntimeHost);
16
+ isEmptyReference(reference: string, currentValue: unknown, responseValue: unknown): boolean;
16
17
  resolveReference(reference: string, currentValue: unknown, responseValue: unknown): unknown;
17
18
  resolveCurrentReference(reference: string, currentValue: unknown): unknown;
18
19
  readWidgetField(state: WidgetState, field: string): unknown;
19
- assignReference(reference: string, inputValue: unknown, _currentValue: unknown): void;
20
+ assignReference(reference: string, inputValue: unknown, _currentValue: unknown, options?: {
21
+ cookieTtl?: unknown;
22
+ }): void;
20
23
  clearListElementState(listKey: string): void;
21
24
  resolveMappedValue(template: unknown, currentValue: unknown, responseValue: unknown): unknown;
22
25
  isListElementContext(value: unknown): value is ListElementContext;
@@ -1,9 +1,10 @@
1
- import type { ActionMap, DescriptionNode, I18nMap, RequestMap, SseConfig, StyleMap } from './types.js';
1
+ import type { ActionMap, DescriptionNode, I18nMap, RequestMap, 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
6
  sse?: Record<string, SseConfig>;
7
+ systemEvents?: SystemEventsMap;
7
8
  i18n?: I18nMap;
8
9
  styles?: StyleMap;
9
10
  };
@@ -12,6 +13,7 @@ export declare class ResourceManager {
12
13
  private actions;
13
14
  private requests;
14
15
  private sse;
16
+ private systemEvents;
15
17
  private i18n;
16
18
  private styles;
17
19
  constructor(input?: ResourceManagerInput);
@@ -20,6 +22,7 @@ export declare class ResourceManager {
20
22
  getActionsMap(): ActionMap;
21
23
  getRequestsMap(): RequestMap;
22
24
  getSseConfigs(): SseConfig[];
25
+ getSystemEvents(): SystemEventsMap;
23
26
  getI18n(): I18nMap;
24
27
  getStyleMap(): StyleMap;
25
28
  }
@@ -0,0 +1,13 @@
1
+ import type { RequestSchema, StyleMap } from './types.js';
2
+ export declare function isBlockedObjectKey(key: string): boolean;
3
+ export declare function hasBlockedReferenceSegment(reference: string): boolean;
4
+ export declare function isTrustedConfigOnlyKey(key: string): boolean;
5
+ export declare function templateValueUsesReference(value: unknown): boolean;
6
+ export declare function sanitizeConfigStyleMap(styleMap: StyleMap): StyleMap;
7
+ export declare function sanitizeUrl(rawValue: unknown, options?: {
8
+ allowRelative?: boolean;
9
+ allowMailto?: boolean;
10
+ }): string | null;
11
+ export declare function sanitizeMarkdownSource(markdown: string): string;
12
+ export declare function sanitizeGeneratedHtml(html: string): string;
13
+ export declare function sanitizeSchemaValue(schema: RequestSchema, value: unknown, path?: string): unknown;
@@ -3,7 +3,8 @@ export type LayoutType = 'vertical' | 'horizontal' | 'grid';
3
3
  export type TextAlign = 'left' | 'center' | 'right';
4
4
  export type VerticalAlign = 'top' | 'center' | 'bottom';
5
5
  export type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
6
- export type WidgetEventName = 'onClick' | 'onUserValueChange' | 'onRefresh';
6
+ export type WidgetEventName = 'onClick' | 'onUserValueChange' | 'onRefresh' | 'onEnter' | 'onShiftEnter' | 'onControlEnter';
7
+ export type SystemEventName = 'onBeforeRender' | 'onAfterRender' | 'onBeforeNavigate' | 'onAfterNavigate';
7
8
  export type PrimitiveRequestType = 'int' | 'float' | 'boolean' | 'string';
8
9
  export type ActionDefinition = {
9
10
  action: string;
@@ -84,11 +85,13 @@ export type I18nMap = Record<string, Record<string, string>>;
84
85
  export type ActionMap = Record<string, ActionDefinition[]>;
85
86
  export type RequestMap = Record<string, RequestDefinition>;
86
87
  export type SseConfigInput = SseConfig | SseConfig[];
88
+ export type SystemEventsMap = Partial<Record<SystemEventName, ActionDefinition[]>>;
87
89
  export type WidgetState = {
88
90
  key: string;
89
91
  widget: DescriptionNode['widget'];
90
92
  id?: string;
91
93
  name?: string;
94
+ definitionSignature?: string;
92
95
  currentRef?: string;
93
96
  text?: string;
94
97
  url?: string;
@@ -132,6 +135,7 @@ export type RenderJsonOptions = {
132
135
  actionsMap?: ActionMap;
133
136
  requestsMap?: RequestMap;
134
137
  sseConfigs?: SseConfigInput;
138
+ systemEvents?: SystemEventsMap;
135
139
  actionFunctions?: Record<string, () => unknown>;
136
140
  backendUrl?: string;
137
141
  resourceManager?: ResourceManager;
@@ -12,7 +12,7 @@ type BindingEnvironment = {
12
12
  }) => void;
13
13
  updatePointerFromEvent: (event: MouseEvent) => void;
14
14
  updateOwningListElementDescriptor: (element: HTMLElement, mutate: (descriptor: DescriptionNode) => void) => void;
15
- dispatchWidgetEvent: (node: DescriptionNode, eventName: 'onClick' | 'onUserValueChange', key: string, inputValue?: unknown, pointer?: {
15
+ dispatchWidgetEvent: (node: DescriptionNode, eventName: 'onClick' | 'onUserValueChange' | 'onEnter' | 'onShiftEnter' | 'onControlEnter', key: string, inputValue?: unknown, pointer?: {
16
16
  x: number;
17
17
  y: number;
18
18
  } | null) => Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vortexm/vjt",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -25,12 +25,12 @@
25
25
  "scripts": {
26
26
  "build": "npm run build:lib && npm run build:examples && npm run build:types",
27
27
  "build:lib": "esbuild src/index.ts --bundle --format=esm --outfile=dist/index.js --loader:.json=json",
28
- "build:examples": "esbuild src/examples.ts --bundle --format=esm --outfile=dist/examples.js --loader:.json=json",
28
+ "build:examples": "esbuild examples/app/examples-app.ts --bundle --format=esm --outfile=dist/examples.js --loader:.json=json",
29
29
  "build:types": "tsc -p tsconfig.build.json",
30
- "watch": "esbuild src/index.ts src/examples.ts --bundle --format=esm --outdir=dist --loader:.json=json --watch",
30
+ "watch": "esbuild src/index.ts examples/app/examples-app.ts --bundle --format=esm --outdir=dist --loader:.json=json --watch",
31
31
  "typecheck": "tsc -p tsconfig.json",
32
32
  "lint": "eslint \"src/**/*.ts\"",
33
- "serve": "http-server . -c-1 -p 4173",
33
+ "serve": "node ./demo/examples-server.mjs",
34
34
  "serve:mock-backend": "node ./mock-backend/server.mjs",
35
35
  "dev": "concurrently \"npm:watch\" \"npm:serve\" \"npm:serve:mock-backend\"",
36
36
  "prepack": "npm run build"
package/vjt-styles.css CHANGED
@@ -33,6 +33,33 @@ html {
33
33
  --vjt-font-heading: Georgia, "Times New Roman", serif;
34
34
  }
35
35
 
36
+ :root {
37
+ background: var(--vjt-bg);
38
+ color: var(--vjt-text);
39
+ }
40
+
41
+ * {
42
+ box-sizing: border-box;
43
+ }
44
+
45
+ html,
46
+ body {
47
+ min-height: 100%;
48
+ }
49
+
50
+ body {
51
+ margin: 0;
52
+ min-height: 100vh;
53
+ background:
54
+ radial-gradient(circle at top left, var(--vjt-bg-glow), transparent 28%),
55
+ linear-gradient(180deg, color-mix(in srgb, var(--vjt-bg) 88%, white) 0%, var(--vjt-bg) 100%);
56
+ }
57
+
58
+ #app {
59
+ width: 100%;
60
+ min-height: 100vh;
61
+ }
62
+
36
63
  .vjt-static-text,
37
64
  .vjt-md-text,
38
65
  .vjt-edit,