@vertesia/ui 0.79.1 → 0.80.0-dev-20251118

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertesia/ui",
3
- "version": "0.79.1",
3
+ "version": "0.80.0-dev-20251118",
4
4
  "description": "Vertesia UI components and and hooks",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -168,5 +168,10 @@
168
168
  "./lib/types/shell/index.d.ts"
169
169
  ]
170
170
  }
171
+ },
172
+ "repository": {
173
+ "type": "git",
174
+ "url": "https://github.com/vertesia/composableai.git",
175
+ "directory": "packages/ui"
171
176
  }
172
177
  }
@@ -90,7 +90,7 @@ export function SelectBox<T>({ clearTitle, ClearIcon = X, showFilter, filterBy,
90
90
  }
91
91
  }
92
92
  return (
93
- <Listbox value={value || null} onChange={onChange} by={by} disabled={disabled}>
93
+ <Listbox value={value ?? undefined} onChange={onChange} by={by} disabled={disabled}>
94
94
  {() => (
95
95
  <div className={'overflow-y-visible ' + className}>
96
96
  <div className="relative">
@@ -0,0 +1,388 @@
1
+ import clsx from 'clsx';
2
+ import { X } from 'lucide-react';
3
+ import { useContext, useEffect, useRef, useState } from 'react';
4
+ import { Popover, PopoverContent, PopoverContext, PopoverTrigger } from './shadcn/popover';
5
+
6
+ interface TagsInputProps {
7
+ options: string[];
8
+ value: string[];
9
+ onChange: (selected: string[]) => void;
10
+ onOptionsChange?: (options: string[]) => void;
11
+ placeholder?: string;
12
+ className?: string;
13
+ disabled?: boolean;
14
+ layout?: 'horizontal' | 'vertical';
15
+ creatable?: boolean;
16
+ createText?: string;
17
+ maxDropdownHeight?: number;
18
+ }
19
+
20
+ function TagsInputContent({
21
+ options,
22
+ value,
23
+ onChange,
24
+ onOptionsChange,
25
+ placeholder,
26
+ className,
27
+ disabled,
28
+ layout = 'horizontal',
29
+ creatable = false,
30
+ createText = 'Create "%value%"',
31
+ maxDropdownHeight = 200
32
+ }: TagsInputProps) {
33
+ const popoverContext = useContext(PopoverContext);
34
+ const [searchTerm, setSearchTerm] = useState('');
35
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
36
+ const [width, setWidth] = useState<number>(0);
37
+ const [pendingDeleteIndex, setPendingDeleteIndex] = useState<number | null>(null);
38
+ const inputRef = useRef<HTMLInputElement>(null);
39
+ const triggerRef = useRef<HTMLDivElement>(null);
40
+ const dropdownRef = useRef<HTMLDivElement>(null);
41
+ const highlightedItemRef = useRef<HTMLElement | null>(null);
42
+
43
+ const isOpen = popoverContext?.open ?? false;
44
+ const setIsOpen = popoverContext?.setOpen ?? (() => { });
45
+
46
+ // Measure trigger width for popover
47
+ useEffect(() => {
48
+ const element = triggerRef.current;
49
+ if (!element) return;
50
+
51
+ const updateWidth = () => {
52
+ const contentWidth = element.getBoundingClientRect().width;
53
+ setWidth(contentWidth);
54
+ };
55
+
56
+ const resizeObserver = new ResizeObserver(() => {
57
+ updateWidth();
58
+ });
59
+
60
+ updateWidth();
61
+ resizeObserver.observe(element);
62
+
63
+ return () => {
64
+ resizeObserver.disconnect();
65
+ };
66
+ }, []);
67
+
68
+ // Filter options based on search term and exclude already selected
69
+ const filteredOptions = options.filter(
70
+ option =>
71
+ !value.includes(option) &&
72
+ option.toLowerCase().includes(searchTerm.toLowerCase())
73
+ );
74
+
75
+ // Check if create option should be shown
76
+ const showCreateOption = creatable && searchTerm && !value.includes(searchTerm) && !options.includes(searchTerm);
77
+
78
+ // Total number of items (filtered options + create option if shown)
79
+ const totalItems = filteredOptions.length + (showCreateOption ? 1 : 0);
80
+
81
+ // Reset highlighted index when filtered options change
82
+ useEffect(() => {
83
+ setHighlightedIndex(0);
84
+ }, [searchTerm, showCreateOption]);
85
+
86
+ // Clear pending delete when user starts typing
87
+ useEffect(() => {
88
+ if (searchTerm !== '') {
89
+ setPendingDeleteIndex(null);
90
+ }
91
+ }, [searchTerm]);
92
+
93
+ // Scroll highlighted item into view
94
+ useEffect(() => {
95
+ if (isOpen && highlightedItemRef.current && dropdownRef.current) {
96
+ highlightedItemRef.current.scrollIntoView({
97
+ block: 'nearest',
98
+ behavior: 'smooth'
99
+ });
100
+ }
101
+ }, [highlightedIndex, isOpen]);
102
+
103
+ // Clear search term when popover closes and refocus input when it opens
104
+ useEffect(() => {
105
+ if (!isOpen) {
106
+ setSearchTerm('');
107
+ } else {
108
+ // Ensure input stays focused when popover opens
109
+ inputRef.current?.focus();
110
+ }
111
+ }, [isOpen]);
112
+
113
+ const handleSelect = (option: string) => {
114
+ onChange([...value, option]);
115
+ setSearchTerm('');
116
+ setIsOpen(false);
117
+ setHighlightedIndex(0);
118
+ setPendingDeleteIndex(null);
119
+ };
120
+
121
+ const handleCreate = (newTag: string) => {
122
+ // Add to value
123
+ onChange([...value, newTag]);
124
+ // Add to options if callback provided
125
+ if (onOptionsChange && !options.includes(newTag)) {
126
+ onOptionsChange([...options, newTag]);
127
+ }
128
+ setSearchTerm('');
129
+ setIsOpen(false);
130
+ setHighlightedIndex(0);
131
+ setPendingDeleteIndex(null);
132
+ };
133
+
134
+ const handleRemove = (option: string, e: React.MouseEvent) => {
135
+ e.stopPropagation();
136
+ onChange(value.filter(v => v !== option));
137
+ setPendingDeleteIndex(null);
138
+ };
139
+
140
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
141
+ // Handle special keys
142
+ if (e.key === 'Enter' && isOpen && totalItems > 0) {
143
+ e.preventDefault();
144
+ e.stopPropagation();
145
+ // Check if we're selecting the create option
146
+ if (highlightedIndex === filteredOptions.length && showCreateOption) {
147
+ handleCreate(searchTerm);
148
+ } else if (highlightedIndex < filteredOptions.length) {
149
+ handleSelect(filteredOptions[highlightedIndex]);
150
+ }
151
+ return;
152
+ }
153
+
154
+ if (e.key === 'ArrowDown') {
155
+ e.preventDefault();
156
+ e.stopPropagation();
157
+ if (!isOpen) {
158
+ setIsOpen(true);
159
+ } else {
160
+ setHighlightedIndex(prev =>
161
+ prev < totalItems - 1 ? prev + 1 : prev
162
+ );
163
+ }
164
+ return;
165
+ }
166
+
167
+ if (e.key === 'ArrowUp') {
168
+ e.preventDefault();
169
+ e.stopPropagation();
170
+ if (!isOpen) {
171
+ setIsOpen(true);
172
+ } else {
173
+ setHighlightedIndex(prev => (prev > 0 ? prev - 1 : 0));
174
+ }
175
+ return;
176
+ }
177
+
178
+ if (e.key === 'Escape') {
179
+ e.preventDefault();
180
+ e.stopPropagation();
181
+ setIsOpen(false);
182
+ return;
183
+ }
184
+
185
+ if (e.key === 'Backspace' && searchTerm === '' && value.length > 0) {
186
+ // Two-step deletion: first backspace marks for deletion, second backspace deletes
187
+ e.stopPropagation();
188
+ const lastIndex = value.length - 1;
189
+
190
+ if (pendingDeleteIndex === lastIndex) {
191
+ // Second backspace: actually delete the item
192
+ onChange(value.slice(0, -1));
193
+ setPendingDeleteIndex(null);
194
+ } else {
195
+ // First backspace: mark for deletion
196
+ setPendingDeleteIndex(lastIndex);
197
+ }
198
+ return;
199
+ }
200
+
201
+ // For any other key (typing characters), open dropdown
202
+ if (!isOpen && e.key.length === 1) {
203
+ setIsOpen(true);
204
+ }
205
+ };
206
+
207
+ const handleInputClick = (e: React.MouseEvent<HTMLInputElement>) => {
208
+ e.stopPropagation();
209
+ if (!disabled) {
210
+ setIsOpen(true);
211
+ }
212
+ };
213
+
214
+ const handleInputFocus = () => {
215
+ if (!disabled) {
216
+ setIsOpen(true);
217
+ }
218
+ };
219
+
220
+ const handleContainerClick = () => {
221
+ if (!disabled) {
222
+ inputRef.current?.focus();
223
+ }
224
+ };
225
+
226
+ return (
227
+ <div className={clsx('relative', className)}>
228
+ <PopoverTrigger asChild>
229
+ <div
230
+ ref={triggerRef}
231
+ className={clsx(
232
+ 'min-h-[40px] w-full rounded-md border border-input bg-background px-3 py-2',
233
+ 'flex items-center gap-1',
234
+ layout === 'horizontal' ? 'flex-wrap' : 'flex-col items-stretch',
235
+ 'cursor-text',
236
+ 'ring-offset-background',
237
+ disabled && 'opacity-50 cursor-not-allowed',
238
+ isOpen ? 'ring-1 ring-inset ring-ring' : ''
239
+ )}
240
+ onClick={handleContainerClick}
241
+ >
242
+ {/* Selected Items Badges - Vertical Layout */}
243
+ {layout === 'vertical' && value.length > 0 && (
244
+ <div className="flex flex-col gap-1 w-full">
245
+ {value.map((item, index) => (
246
+ <span
247
+ key={item}
248
+ className={clsx(
249
+ "inline-flex items-center justify-between gap-2 px-2 py-1 text-sm bg-primary/10 text-primary rounded-md w-full transition-all",
250
+ pendingDeleteIndex === index && "ring-2 ring-red-300 shadow-[0_0_8px_rgba(252,165,165,0.5)]"
251
+ )}
252
+ >
253
+ <span className="truncate">{item}</span>
254
+ <button
255
+ type="button"
256
+ onClick={(e) => handleRemove(item, e)}
257
+ disabled={disabled}
258
+ className="hover:bg-primary/20 rounded-sm transition-colors flex-shrink-0"
259
+ >
260
+ <X className="h-3 w-3" />
261
+ </button>
262
+ </span>
263
+ ))}
264
+ </div>
265
+ )}
266
+
267
+ {/* Selected Items Badges - Horizontal Layout */}
268
+ {layout === 'horizontal' && value.map((item, index) => (
269
+ <span
270
+ key={item}
271
+ className={clsx(
272
+ "inline-flex items-center gap-1 px-2 py-1 text-sm bg-primary/10 text-primary rounded-md transition-all",
273
+ pendingDeleteIndex === index && "ring-2 ring-red-300 shadow-[0_0_8px_rgba(252,165,165,0.5)]"
274
+ )}
275
+ >
276
+ {item}
277
+ <button
278
+ type="button"
279
+ onClick={(e) => handleRemove(item, e)}
280
+ disabled={disabled}
281
+ className="hover:bg-primary/20 rounded-sm transition-colors"
282
+ >
283
+ <X className="h-3 w-3" />
284
+ </button>
285
+ </span>
286
+ ))}
287
+
288
+ {/* Search Input */}
289
+ <input
290
+ ref={inputRef}
291
+ type="text"
292
+ value={searchTerm}
293
+ onChange={(e) => setSearchTerm(e.target.value)}
294
+ onKeyDown={handleKeyDown}
295
+ onClick={handleInputClick}
296
+ onFocus={handleInputFocus}
297
+ disabled={disabled}
298
+ placeholder={value.length === 0 ? placeholder : ''}
299
+ className={clsx(
300
+ 'flex-1 min-w-[120px] bg-transparent text-sm',
301
+ 'placeholder:text-muted-foreground',
302
+ 'border-none outline-none focus:outline-none focus:ring-0 p-0 m-0',
303
+ layout === 'vertical' && 'w-full'
304
+ )}
305
+ />
306
+ </div>
307
+ </PopoverTrigger>
308
+
309
+ <PopoverContent
310
+ style={{ width: `${width}px` }}
311
+ className="p-0 bg-popover border border-border shadow-lg"
312
+ align="start"
313
+ side="bottom"
314
+ onOpenAutoFocus={(e) => {
315
+ // Prevent the popover from stealing focus from the input
316
+ e.preventDefault();
317
+ }}
318
+ >
319
+ <div ref={dropdownRef} className="overflow-y-auto" style={{ maxHeight: `${maxDropdownHeight}px` }}>
320
+ {filteredOptions.length === 0 && !showCreateOption ? (
321
+ <div className="px-3 py-2 text-sm text-muted-foreground">
322
+ {searchTerm ? 'No options found' : 'No more options available'}
323
+ </div>
324
+ ) : (
325
+ <>
326
+ {filteredOptions.length > 0 && (
327
+ <ul className="py-1">
328
+ {filteredOptions.map((option, index) => (
329
+ <li
330
+ key={option}
331
+ ref={(el) => {
332
+ if (index === highlightedIndex) {
333
+ highlightedItemRef.current = el;
334
+ }
335
+ }}
336
+ onClick={() => handleSelect(option)}
337
+ onMouseEnter={() => setHighlightedIndex(index)}
338
+ className={clsx(
339
+ 'px-3 py-2 text-sm cursor-pointer transition-colors',
340
+ index === highlightedIndex
341
+ ? 'bg-blue-500/20 text-foreground'
342
+ : 'hover:bg-accent/50'
343
+ )}
344
+ >
345
+ {option}
346
+ </li>
347
+ ))}
348
+ </ul>
349
+ )}
350
+ {showCreateOption && (
351
+ <>
352
+ {filteredOptions.length > 0 && (
353
+ <div className="border-t border-border" />
354
+ )}
355
+ <div
356
+ ref={(el) => {
357
+ if (highlightedIndex === filteredOptions.length) {
358
+ highlightedItemRef.current = el;
359
+ }
360
+ }}
361
+ onClick={() => handleCreate(searchTerm)}
362
+ onMouseEnter={() => setHighlightedIndex(filteredOptions.length)}
363
+ className={clsx(
364
+ 'px-3 py-2 text-sm cursor-pointer transition-colors text-primary',
365
+ highlightedIndex === filteredOptions.length
366
+ ? 'bg-blue-500/20'
367
+ : 'hover:bg-accent/50'
368
+ )}
369
+ >
370
+ {createText.replace('%value%', searchTerm)}
371
+ </div>
372
+ </>
373
+ )}
374
+ </>
375
+ )}
376
+ </div>
377
+ </PopoverContent>
378
+ </div>
379
+ );
380
+ }
381
+
382
+ export function TagsInput(props: TagsInputProps) {
383
+ return (
384
+ <Popover click>
385
+ <TagsInputContent {...props} />
386
+ </Popover>
387
+ );
388
+ }
@@ -18,6 +18,7 @@ export * from "./MessageBox.js";
18
18
  export * from "./Modal.js";
19
19
  export * from "./NumberInput.js";
20
20
  export * from "./Overlay.js";
21
+ export * from "./Panel.js";
21
22
  export * from "./popup/index.js";
22
23
  export * from "./Portal.js";
23
24
  export * from "./RadioGroup.js";
@@ -26,12 +27,12 @@ export * from "./SelectList.js";
26
27
  export * from "./SelectStack.js";
27
28
  export * from "./shadcn/index.js";
28
29
  export * from "./SidePanel.js";
29
- export * from "./Panel.js";
30
30
  export * from "./Spinner.js";
31
31
  export * from "./styles.js";
32
32
  export * from "./Switch.js";
33
33
  export * from "./table/index.js";
34
34
  export * from "./tabs/index.js";
35
+ export * from "./TagsInput.js";
35
36
  export * from "./toast/index.js";
36
37
 
37
38
  export type HeroIcon = React.ForwardRefExoticComponent<Omit<React.SVGProps<SVGSVGElement>, "ref"> & {
@@ -5,13 +5,13 @@ import { cn } from "../libs/utils"
5
5
  import { JSX } from "react";
6
6
  import { useIsInModal } from "./dialog";
7
7
 
8
- interface PopoverContextValue {
8
+ export interface PopoverContextValue {
9
9
  open: boolean;
10
10
  setOpen: React.Dispatch<React.SetStateAction<boolean>>;
11
11
  hover: boolean;
12
12
  click: boolean;
13
13
  }
14
- const PopoverContext = React.createContext<PopoverContextValue | null>(null);
14
+ export const PopoverContext = React.createContext<PopoverContextValue | null>(null);
15
15
 
16
16
  interface PopoverProps {
17
17
  _open?: boolean;
@@ -4,17 +4,28 @@ import React, { ReactNode, useContext, useEffect, useState } from "react";
4
4
  //type KeysNotOfType<T, V> = { [K in keyof T]-?: T[K] extends V ? never : K }[keyof T];
5
5
 
6
6
  export class Property<V = any> {
7
- _value?: V;
8
- watchers: ((value: V) => void)[] = [];
9
- constructor(value?: V) {
7
+ private _value?: V;
8
+ private watchers: ((value: V | undefined) => void)[] = [];
9
+ /**
10
+ * Optional name for debugging purposes.
11
+ * When provided, changes to this property will be logged to the console in development mode,
12
+ * making it easier to track state changes and debug reactive updates.
13
+ *
14
+ * Example: new Property<string>('', 'streamingText')
15
+ * Will log: [CompositeState] streamingText: "" → "new value"
16
+ */
17
+ readonly name?: string;
18
+
19
+ constructor(value?: V, name?: string) {
10
20
  this._value = value;
21
+ this.name = name;
11
22
  }
12
23
 
13
24
  get value() {
14
25
  return this._value;
15
26
  }
16
27
 
17
- set value(value: any) {
28
+ set value(value: V | undefined) {
18
29
  if (value !== this._value) {
19
30
  this._value = value;
20
31
  for (const watcher of this.watchers) {
@@ -23,11 +34,11 @@ export class Property<V = any> {
23
34
  }
24
35
  }
25
36
 
26
- watch(watcher: (value: any) => void) {
37
+ watch(watcher: (value: V | undefined) => void) {
27
38
  this.watchers.push(watcher);
28
39
  return () => {
29
40
  this.watchers = this.watchers.filter(w => w !== watcher);
30
- }
41
+ };
31
42
  }
32
43
  }
33
44
 
@@ -140,3 +151,142 @@ export function useDefineSlot(slot: Slot, value: ReactNode | undefined) {
140
151
  }
141
152
  }, [slot, value])
142
153
  }
154
+
155
+ /**
156
+ * Computed property that derives its value from other properties.
157
+ * Automatically recalculates when any of its dependencies change.
158
+ *
159
+ * Think of it like a spreadsheet formula: if cell A1 = 5 and A2 = 10,
160
+ * then A3 = A1 + A2 will automatically update to 15. If A1 changes to 7,
161
+ * A3 automatically becomes 17.
162
+ *
163
+ * @example Basic usage
164
+ * ```typescript
165
+ * class MyState {
166
+ * count = new Property(0, 'count');
167
+ * multiplier = new Property(2, 'multiplier');
168
+ *
169
+ * // Automatically recalculates when count or multiplier changes
170
+ * result = new ComputedProperty(
171
+ * () => (this.count.value || 0) * (this.multiplier.value || 0),
172
+ * [this.count, this.multiplier],
173
+ * 'result'
174
+ * );
175
+ * }
176
+ *
177
+ * state.count.value = 5; // result automatically becomes 10
178
+ * state.multiplier.value = 3; // result automatically becomes 15
179
+ * ```
180
+ *
181
+ * @example Derived state
182
+ * ```typescript
183
+ * class EditorState {
184
+ * workingCopy = new Property<Interaction>();
185
+ * sourceInteraction = new Property<Interaction>();
186
+ *
187
+ * // Automatically true when workingCopy differs from source
188
+ * isDirty = new ComputedProperty(
189
+ * () => JSON.stringify(this.workingCopy.value) !==
190
+ * JSON.stringify(this.sourceInteraction.value),
191
+ * [this.workingCopy, this.sourceInteraction],
192
+ * 'isDirty'
193
+ * );
194
+ * }
195
+ * ```
196
+ *
197
+ * @example Cascading computed properties
198
+ * ```typescript
199
+ * class State {
200
+ * a = new Property(1);
201
+ * b = new Property(2);
202
+ * sum = new ComputedProperty(() => a.value + b.value, [a, b]);
203
+ * doubled = new ComputedProperty(() => sum.value * 2, [sum]);
204
+ * }
205
+ * ```
206
+ *
207
+ * Benefits:
208
+ * - ✅ Automatic updates when dependencies change
209
+ * - ✅ Memoization - only recalculates when needed
210
+ * - ✅ Composable - can depend on other ComputedProperties
211
+ * - ✅ Type-safe with full TypeScript support
212
+ *
213
+ * When to use:
214
+ * - ✅ For derived state (values calculated from other values)
215
+ * - ✅ When dependencies are other Properties or ComputedProperties
216
+ * - ❌ NOT for async operations (use regular methods instead)
217
+ * - ❌ NOT if compute function has side effects
218
+ *
219
+ * @important Remember to call dispose() when the ComputedProperty is no longer needed
220
+ * to prevent memory leaks by unsubscribing from all dependencies.
221
+ */
222
+ export class ComputedProperty<V = any> {
223
+ private _value?: V;
224
+ private watchers: ((value: V | undefined) => void)[] = [];
225
+ private unsubscribers: (() => void)[] = [];
226
+ /**
227
+ * Optional name for debugging purposes.
228
+ * When provided, recalculations will be logged to the console in development mode.
229
+ */
230
+ readonly name?: string;
231
+
232
+ /**
233
+ * @param compute - Function that calculates the derived value
234
+ * @param dependencies - Array of Properties this computed value depends on
235
+ * @param name - Optional name for debugging
236
+ */
237
+ constructor(
238
+ private compute: () => V,
239
+ dependencies: Property<any>[],
240
+ name?: string
241
+ ) {
242
+ this.name = name;
243
+ this.recalculate();
244
+
245
+ // Watch all dependencies - when any changes, recalculate
246
+ for (const dep of dependencies) {
247
+ this.unsubscribers.push(
248
+ dep.watch(() => this.recalculate())
249
+ );
250
+ }
251
+ }
252
+
253
+ private recalculate() {
254
+ const newValue = this.compute();
255
+ if (newValue !== this._value) {
256
+ this._value = newValue;
257
+ for (const watcher of this.watchers) {
258
+ watcher(newValue);
259
+ }
260
+ }
261
+ }
262
+
263
+ get value() {
264
+ return this._value;
265
+ }
266
+
267
+ watch(watcher: (value: V | undefined) => void) {
268
+ this.watchers.push(watcher);
269
+ return () => {
270
+ this.watchers = this.watchers.filter(w => w !== watcher);
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Dispose of this ComputedProperty by unsubscribing from all dependencies.
276
+ * Call this when the ComputedProperty is no longer needed to prevent memory leaks.
277
+ *
278
+ * @example
279
+ * ```typescript
280
+ * const computed = new ComputedProperty(...);
281
+ *
282
+ * // Later, when cleaning up:
283
+ * computed.dispose();
284
+ * ```
285
+ */
286
+ dispose() {
287
+ for (const unsub of this.unsubscribers) {
288
+ unsub();
289
+ }
290
+ this.watchers = [];
291
+ }
292
+ }
package/src/env/index.ts CHANGED
@@ -12,6 +12,7 @@ export interface EnvProps {
12
12
  endpoints: {
13
13
  zeno: string,
14
14
  studio: string,
15
+ sts: string, // Security Token Service endpoint
15
16
  },
16
17
  firebase?: {
17
18
  apiKey: string,
@@ -20,7 +21,7 @@ export interface EnvProps {
20
21
  appId?: string,
21
22
  providerType?: string,
22
23
  },
23
- datadog: boolean,
24
+ datadog?: boolean,
24
25
  logger?: {
25
26
  info: (msg: string, ...args: any) => void,
26
27
  warn: (msg: string, ...args: any) => void,
@@ -93,7 +94,7 @@ export class VertesiaEnvironment implements Readonly<EnvProps> {
93
94
  }
94
95
 
95
96
  get datadog() {
96
- return this.prop("datadog");
97
+ return this._props?.datadog ?? false;
97
98
  }
98
99
 
99
100
  get logger() {
@@ -117,4 +118,4 @@ export class VertesiaEnvironment implements Readonly<EnvProps> {
117
118
 
118
119
  const Env = new VertesiaEnvironment();
119
120
 
120
- export { Env };
121
+ export { Env };