canvasengine 2.0.0-beta.19 → 2.0.0-beta.20

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": "canvasengine",
3
- "version": "2.0.0-beta.19",
3
+ "version": "2.0.0-beta.20",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,229 @@
1
+ import { DOMContainer as PixiDOMContainer } from "pixi.js";
2
+ import {
3
+ createComponent,
4
+ Element,
5
+ registerComponent,
6
+ } from "../engine/reactive";
7
+ import { ComponentInstance, DisplayObject } from "./DisplayObject";
8
+ import { ComponentFunction } from "../engine/signal";
9
+ import { DisplayObjectProps } from "./types/DisplayObject";
10
+
11
+ interface DOMContainerProps extends DisplayObjectProps {
12
+ element:
13
+ | string
14
+ | {
15
+ value: HTMLElement;
16
+ };
17
+ textContent?: string;
18
+ attrs?: Record<string, any> & {
19
+ class?:
20
+ | string
21
+ | string[]
22
+ | Record<string, boolean>
23
+ | { items?: string[] }
24
+ | { value?: string | string[] | Record<string, boolean> };
25
+ style?:
26
+ | string
27
+ | Record<string, string | number>
28
+ | { value?: string | Record<string, string | number> };
29
+ };
30
+ sortableChildren?: boolean;
31
+ }
32
+
33
+ /**
34
+ * DOMContainer class for managing DOM elements within the canvas engine
35
+ *
36
+ * This class extends the DisplayObject functionality to handle DOM elements using
37
+ * PixiJS's native DOMContainer. It provides a bridge between the canvas rendering
38
+ * system and traditional DOM manipulation with proper transform hierarchy and visibility.
39
+ *
40
+ * The DOMContainer is especially useful for rendering standard DOM elements that handle
41
+ * user input, such as `<input>` or `<textarea>`. This is often simpler and more flexible
42
+ * than trying to implement text input directly in PixiJS.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * // Basic usage with input element
47
+ * const element = document.createElement('input');
48
+ * element.type = 'text';
49
+ * element.placeholder = 'Enter text...';
50
+ *
51
+ * const domContainer = new DOMContainer({
52
+ * element,
53
+ * x: 100,
54
+ * y: 50,
55
+ * anchor: { x: 0.5, y: 0.5 }
56
+ * });
57
+ *
58
+ * // Using different class and style formats
59
+ * const containerWithClasses = new DOMContainer({
60
+ * element: 'div',
61
+ * attrs: {
62
+ * // String format: space-separated classes
63
+ * class: 'container primary-theme',
64
+ *
65
+ * // Array format: array of class names
66
+ * // class: ['container', 'primary-theme'],
67
+ *
68
+ * // Object format: conditional classes
69
+ * // class: {
70
+ * // 'container': true,
71
+ * // 'primary-theme': true,
72
+ * // 'disabled': false
73
+ * // }
74
+ *
75
+ * // String format: CSS style string
76
+ * style: 'background-color: red; padding: 10px;',
77
+ *
78
+ * // Object format: style properties
79
+ * // style: {
80
+ * // backgroundColor: 'red',
81
+ * // padding: '10px',
82
+ * // fontSize: 16
83
+ * // }
84
+ * }
85
+ * });
86
+ * ```
87
+ */
88
+ const EVENTS = [
89
+ "click",
90
+ "mouseover",
91
+ "mouseout",
92
+ "mouseenter",
93
+ "mouseleave",
94
+ "mousemove",
95
+ "mouseup",
96
+ "mousedown",
97
+ "touchstart",
98
+ "touchend",
99
+ "touchmove",
100
+ "touchcancel",
101
+ "wheel",
102
+ "scroll",
103
+ "resize",
104
+ "focus",
105
+ "blur",
106
+ "change",
107
+ "input",
108
+ "submit",
109
+ "reset",
110
+ "keydown",
111
+ "keyup",
112
+ "keypress",
113
+ "contextmenu",
114
+ "drag",
115
+ "dragend",
116
+ "dragenter",
117
+ "dragleave",
118
+ "dragover",
119
+ "drop",
120
+ "dragstart",
121
+ "select",
122
+ "selectstart",
123
+ "selectend",
124
+ "selectall",
125
+ "selectnone",
126
+ ];
127
+
128
+ export class CanvasDOMContainer extends DisplayObject(PixiDOMContainer) {
129
+ disableLayout = true;
130
+ private eventListeners: Map<string, (e: Event) => void> = new Map();
131
+
132
+ onInit(props: DOMContainerProps) {
133
+ super.onInit(props);
134
+ if (props.element === undefined) {
135
+ throw new Error("DOMContainer: element is required");
136
+ }
137
+ if (typeof props.element === "string") {
138
+ this.element = document.createElement(props.element);
139
+ } else {
140
+ this.element = props.element.value;
141
+ }
142
+ for (const event of EVENTS) {
143
+ if (props.attrs?.[event]) {
144
+ const eventHandler = (e: Event) => {
145
+ props.attrs[event]?.(e);
146
+ };
147
+ this.eventListeners.set(event, eventHandler);
148
+ this.element.addEventListener(event, eventHandler, false);
149
+ }
150
+ }
151
+ }
152
+
153
+ onUpdate(props: DOMContainerProps) {
154
+ super.onUpdate(props);
155
+
156
+ for (const [key, value] of Object.entries(props.attrs || {})) {
157
+ if (key === "class") {
158
+ const classList = value.items || value.value || value;
159
+
160
+ // Clear existing classes first
161
+ this.element.className = "";
162
+
163
+ if (typeof classList === "string") {
164
+ // String: space-separated class names
165
+ this.element.className = classList;
166
+ } else if (Array.isArray(classList)) {
167
+ // Array: array of class names
168
+ this.element.classList.add(...classList);
169
+ } else if (typeof classList === "object" && classList !== null) {
170
+ // Object: { className: boolean }
171
+ for (const [className, shouldAdd] of Object.entries(classList)) {
172
+ if (shouldAdd) {
173
+ this.element.classList.add(className);
174
+ }
175
+ }
176
+ }
177
+ } else if (key === "style") {
178
+ const styleValue = value.items || value.value || value;
179
+
180
+ if (typeof styleValue === "string") {
181
+ // String: CSS style string
182
+ this.element.setAttribute("style", styleValue);
183
+ } else if (typeof styleValue === "object" && styleValue !== null) {
184
+ // Object: { property: value }
185
+ for (const [styleProp, styleVal] of Object.entries(styleValue)) {
186
+ if (styleVal !== null && styleVal !== undefined) {
187
+ (this.element.style as any)[styleProp] = styleVal;
188
+ }
189
+ }
190
+ }
191
+ } else if (!EVENTS.includes(key)) {
192
+ this.element.setAttribute(key, value);
193
+ }
194
+ }
195
+ if (props.textContent) {
196
+ this.element.textContent = props.textContent;
197
+ }
198
+
199
+ if (props.sortableChildren !== undefined) {
200
+ this.sortableChildren = props.sortableChildren;
201
+ }
202
+ }
203
+
204
+ async onDestroy(
205
+ parent: Element<ComponentInstance>,
206
+ afterDestroy: () => void
207
+ ): Promise<void> {
208
+ // Remove all event listeners from the DOM element
209
+ if (this.element) {
210
+ for (const [event, handler] of this.eventListeners) {
211
+ this.element.removeEventListener(event, handler, false);
212
+ }
213
+ this.eventListeners.clear();
214
+ }
215
+
216
+ const _afterDestroyCallback = async () => {
217
+ afterDestroy();
218
+ };
219
+ await super.onDestroy(parent, _afterDestroyCallback);
220
+ }
221
+ }
222
+
223
+ export interface CanvasDOMContainer extends DisplayObjectProps {}
224
+
225
+ registerComponent("DOMContainer", CanvasDOMContainer);
226
+
227
+ export const DOMContainer: ComponentFunction<DOMContainerProps> = (props) => {
228
+ return createComponent("DOMContainer", props);
229
+ };
@@ -9,9 +9,10 @@ import type {
9
9
  TransformOrigin,
10
10
  } from "./types/DisplayObject";
11
11
  import { signal } from "@signe/reactive";
12
- import { DropShadowFilter } from "pixi-filters";
13
12
  import { BlurFilter, ObservablePoint } from "pixi.js";
13
+ import * as FILTERS from "pixi-filters";
14
14
  import { isPercent } from "../utils/functions";
15
+ import { BehaviorSubject, filter, Subject } from "rxjs";
15
16
 
16
17
  export interface ComponentInstance extends PixiMixins.ContainerOptions {
17
18
  id?: string;
@@ -113,12 +114,19 @@ export function DisplayObject(extendClass) {
113
114
  layout = null;
114
115
  onBeforeDestroy: OnHook | null = null;
115
116
  onAfterMount: OnHook | null = null;
117
+ subjectInit = new BehaviorSubject(null);
118
+ disableLayout: boolean = false;
116
119
 
117
120
  get deltaRatio() {
118
121
  return this.#canvasContext?.scheduler?.tick.value.deltaRatio;
119
122
  }
120
123
 
121
- onInit(props) {
124
+ get parentIsFlex() {
125
+ if (this.disableLayout) return false;
126
+ return this.parent?.isFlex;
127
+ }
128
+
129
+ onInit(props: Props) {
122
130
  this._id = props.id;
123
131
  for (let event of EVENTS) {
124
132
  if (props[event] && !this.overrideProps.includes(event)) {
@@ -146,14 +154,20 @@ export function DisplayObject(extendClass) {
146
154
  this.layout = {};
147
155
  this.isFlex = true;
148
156
  }
157
+
158
+ this.subjectInit.next(this);
149
159
  }
150
160
 
151
161
  async onMount({ parent, props }: Element<DisplayObject>, index?: number) {
152
162
  this.#canvasContext = props.context;
153
163
  if (parent) {
154
164
  const instance = parent.componentInstance as DisplayObject;
155
- if (instance.isFlex && !this.layout) {
156
- this.layout = {};
165
+ if (instance.isFlex && !this.layout && !this.disableLayout) {
166
+ try {
167
+ this.layout = {};
168
+ } catch (error) {
169
+ console.warn('Failed to set layout:', error);
170
+ }
157
171
  }
158
172
  if (index === undefined) {
159
173
  instance.addChild(this);
@@ -168,7 +182,7 @@ export function DisplayObject(extendClass) {
168
182
  }
169
183
  }
170
184
 
171
- onUpdate(props) {
185
+ onUpdate(props: Props) {
172
186
  this.fullProps = {
173
187
  ...this.fullProps,
174
188
  ...props,
@@ -229,23 +243,24 @@ export function DisplayObject(extendClass) {
229
243
  if (props.filters) this.filters = props.filters;
230
244
  if (props.maskOf) {
231
245
  if (isElement(props.maskOf)) {
232
- props.maskOf.componentInstance.mask = this;
246
+ props.maskOf.componentInstance.mask = this as any;
233
247
  }
234
248
  }
235
249
  if (props.blendMode) this.blendMode = props.blendMode;
236
250
  if (props.filterArea) this.filterArea = props.filterArea;
237
251
  const currentFilters = this.filters || [];
238
252
 
239
- if (props.shadow) {
240
- let dropShadowFilter = currentFilters.find(
241
- (filter) => filter instanceof DropShadowFilter
242
- );
243
- if (!dropShadowFilter) {
244
- dropShadowFilter = new DropShadowFilter();
245
- currentFilters.push(dropShadowFilter);
246
- }
247
- Object.assign(dropShadowFilter, props.shadow);
248
- }
253
+ // TODO: Fix DropShadowFilter import issue
254
+ // if (props.shadow) {
255
+ // let dropShadowFilter = currentFilters.find(
256
+ // (filter) => filter instanceof FILTERS.DropShadowFilter
257
+ // );
258
+ // if (!dropShadowFilter) {
259
+ // dropShadowFilter = new FILTERS.DropShadowFilter();
260
+ // currentFilters.push(dropShadowFilter);
261
+ // }
262
+ // Object.assign(dropShadowFilter, props.shadow);
263
+ // }
249
264
 
250
265
  if (props.blur) {
251
266
  let blurFilter = currentFilters.find(
@@ -272,7 +287,7 @@ export function DisplayObject(extendClass) {
272
287
  await this.onBeforeDestroy();
273
288
  }
274
289
  super.destroy();
275
- if (this.onAfterDestroy) this.onAfterDestroy()
290
+ if (afterDestroy) afterDestroy();
276
291
  }
277
292
 
278
293
  setFlexDirection(direction: FlexDirection) {
@@ -328,7 +343,7 @@ export function DisplayObject(extendClass) {
328
343
 
329
344
  setX(x: number) {
330
345
  x = x + this.getWidth() * this._anchorPoints.x;
331
- if (!this.parent.isFlex) {
346
+ if (!this.parentIsFlex) {
332
347
  this.x = x;
333
348
  } else {
334
349
  this.x = x;
@@ -338,7 +353,7 @@ export function DisplayObject(extendClass) {
338
353
 
339
354
  setY(y: number) {
340
355
  y = y + this.getHeight() * this._anchorPoints.y;
341
- if (!this.parent.isFlex) {
356
+ if (!this.parentIsFlex) {
342
357
  this.y = y;
343
358
  } else {
344
359
  this.y = y;
@@ -416,7 +431,7 @@ export function DisplayObject(extendClass) {
416
431
 
417
432
  setWidth(width: number) {
418
433
  this.displayWidth.set(width);
419
- if (!this.parent?.isFlex) {
434
+ if (!this.parentIsFlex) {
420
435
  this.width = width;
421
436
  } else {
422
437
  this.layout = { width };
@@ -425,7 +440,7 @@ export function DisplayObject(extendClass) {
425
440
 
426
441
  setHeight(height: number) {
427
442
  this.displayHeight.set(height);
428
- if (!this.parent?.isFlex) {
443
+ if (!this.parentIsFlex) {
429
444
  this.height = height;
430
445
  } else {
431
446
  this.layout = { height };
@@ -1,13 +1,14 @@
1
- import { Effect, effect, Signal } from "@signe/reactive";
2
- import { Graphics as PixiGraphics } from "pixi.js";
1
+ import { Effect, effect, isSignal, signal, Signal, WritableSignal } from "@signe/reactive";
2
+ import { Assets, Graphics as PixiGraphics } from "pixi.js";
3
3
  import { createComponent, Element, registerComponent } from "../engine/reactive";
4
4
  import { ComponentInstance, DisplayObject } from "./DisplayObject";
5
5
  import { DisplayObjectProps } from "./types/DisplayObject";
6
6
  import { useProps } from "../hooks/useProps";
7
7
  import { SignalOrPrimitive } from "./types";
8
+ import { isPercent } from "../utils/functions";
8
9
 
9
10
  interface GraphicsProps extends DisplayObjectProps {
10
- draw?: (graphics: PixiGraphics) => void;
11
+ draw?: (graphics: PixiGraphics, width: number, height: number) => void;
11
12
  }
12
13
 
13
14
  interface RectProps extends DisplayObjectProps {
@@ -29,19 +30,135 @@ interface TriangleProps extends DisplayObjectProps {
29
30
  }
30
31
 
31
32
  interface SvgProps extends DisplayObjectProps {
32
- svg: string;
33
+ /** SVG content as string (legacy prop) */
34
+ svg?: string;
35
+ /** URL source of the SVG file to load */
36
+ src?: string;
37
+ /** Direct SVG content as string */
38
+ content?: string;
33
39
  }
34
40
 
35
41
  class CanvasGraphics extends DisplayObject(PixiGraphics) {
36
42
  clearEffect: Effect;
37
- onInit(props) {
38
- super.onInit(props);
43
+ width: WritableSignal<number>;
44
+ height: WritableSignal<number>;
45
+
46
+ /**
47
+ * Initializes the graphics component with reactive width and height handling.
48
+ *
49
+ * This method handles different types of width and height props:
50
+ * - **Numbers**: Direct pixel values
51
+ * - **Strings with %**: Percentage values that trigger flex layout and use layout box dimensions
52
+ * - **Signals**: Reactive values that update automatically
53
+ *
54
+ * When percentage values are detected, the component:
55
+ * 1. Sets `display: 'flex'` to enable layout calculations
56
+ * 2. Listens to layout events to get computed dimensions
57
+ * 3. Updates internal width/height signals with layout box values
58
+ *
59
+ * The draw function receives the reactive width and height signals as parameters.
60
+ *
61
+ * @param props - Component properties including width, height, and draw function
62
+ * @example
63
+ * ```typescript
64
+ * // With pixel values
65
+ * Graphics({ width: 100, height: 50, draw: (g, w, h) => g.rect(0, 0, w(), h()) });
66
+ *
67
+ * // With percentage values (uses layout box)
68
+ * Graphics({ width: "50%", height: "100%", draw: (g, w, h) => g.rect(0, 0, w(), h()) });
69
+ *
70
+ * // With signals
71
+ * const width = signal(100);
72
+ * Graphics({ width, height: 50, draw: (g, w, h) => g.rect(0, 0, w(), h()) });
73
+ * ```
74
+ */
75
+ async onInit(props) {
76
+ await super.onInit(props);
77
+ }
78
+
79
+ /**
80
+ * Called when the component is mounted to the scene graph.
81
+ * Creates the reactive effect for drawing using the original signals from propObservables.
82
+ * @param {Element<DisplayObject>} element - The element being mounted with props and propObservables.
83
+ * @param {number} [index] - The index of the component among its siblings.
84
+ */
85
+ async onMount(element: Element<any>, index?: number): Promise<void> {
86
+ await super.onMount(element, index);
87
+ const { props, propObservables } = element;
88
+
89
+ // Use original signals from propObservables if available, otherwise create new ones
90
+ const width = (isSignal(propObservables?.width) ? propObservables.width : signal(props.width || 0)) as WritableSignal<number>;
91
+ const height = (isSignal(propObservables?.height) ? propObservables.height : signal(props.height || 0)) as WritableSignal<number>;
92
+
93
+ // Store as class properties for access in other methods
94
+ this.width = width;
95
+ this.height = height;
96
+
97
+ // Check if width or height are percentages to set display flex
98
+ const isWidthPercentage = isPercent(width());
99
+ const isHeightPercentage = isPercent(height());
100
+
39
101
  if (props.draw) {
40
102
  this.clearEffect = effect(() => {
103
+ const w = width();
104
+ const h = height();
105
+ if (typeof w == 'string' || typeof h == 'string') {
106
+ return
107
+ }
108
+ if (w == 0 || h == 0) {
109
+ return
110
+ }
41
111
  this.clear();
42
- props.draw?.(this);
112
+ props.draw?.(this, w, h);
113
+ this.subjectInit.next(this)
43
114
  });
44
115
  }
116
+
117
+ this.on('layout', (event) => {
118
+ const layoutBox = event.computedLayout;
119
+ // Update width if it's a percentage
120
+ if (isWidthPercentage && isSignal(width)) {
121
+ width.set(layoutBox.width);
122
+ }
123
+
124
+ // Update height if it's a percentage
125
+ if (isHeightPercentage && isSignal(height)) {
126
+ height.set(layoutBox.height);
127
+ }
128
+ });
129
+ }
130
+
131
+ /**
132
+ * Called when component props are updated.
133
+ * Updates the internal width and height signals when props change.
134
+ * @param props - Updated properties
135
+ */
136
+ onUpdate(props: any) {
137
+ super.onUpdate(props);
138
+
139
+ // Update width signal if width prop changed
140
+ if (props.width !== undefined && this.width) {
141
+ if (isSignal(props.width)) {
142
+ // If the new prop is a signal, we need to replace our local signal
143
+ // This shouldn't happen in normal usage, but handle it just in case
144
+ this.width = props.width;
145
+ } else {
146
+ // Update our local signal with the new value
147
+ this.width.set(props.width);
148
+ }
149
+ }
150
+
151
+ // Update height signal if height prop changed
152
+ if (props.height !== undefined && this.height) {
153
+ if (isSignal(props.height)) {
154
+ // If the new prop is a signal, we need to replace our local signal
155
+ // This shouldn't happen in normal usage, but handle it just in case
156
+ this.height = props.height;
157
+ } else {
158
+ // Update our local signal with the new value
159
+ this.height.set(props.height);
160
+ }
161
+ }
45
162
  }
46
163
 
47
164
  /**
@@ -70,16 +187,17 @@ export function Graphics(props: GraphicsProps) {
70
187
  }
71
188
 
72
189
  export function Rect(props: RectProps) {
73
- const { width, height, color, borderRadius, border } = useProps(props, {
190
+ const { color, borderRadius, border } = useProps(props, {
74
191
  borderRadius: null,
75
192
  border: null
76
193
  })
194
+
77
195
  return Graphics({
78
- draw: (g) => {
196
+ draw: (g, width, height) => {
79
197
  if (borderRadius()) {
80
- g.roundRect(0, 0, width(), height(), borderRadius());
198
+ g.roundRect(0, 0, width, height, borderRadius());
81
199
  } else {
82
- g.rect(0, 0, width(), height());
200
+ g.rect(0, 0, width, height);
83
201
  }
84
202
  if (border) {
85
203
  g.stroke(border);
@@ -95,8 +213,8 @@ function drawShape(g: PixiGraphics, shape: 'circle' | 'ellipse', props: {
95
213
  color: Signal<string>;
96
214
  border: Signal<number>;
97
215
  } | {
98
- width: Signal<number>;
99
- height: Signal<number>;
216
+ width: WritableSignal<number>;
217
+ height: WritableSignal<number>;
100
218
  color: Signal<string>;
101
219
  border: Signal<number>;
102
220
  }) {
@@ -127,7 +245,7 @@ export function Ellipse(props: EllipseProps) {
127
245
  border: null
128
246
  })
129
247
  return Graphics({
130
- draw: (g) => drawShape(g, 'ellipse', { width, height, color, border }),
248
+ draw: (g, gWidth, gHeight) => drawShape(g, 'ellipse', { width: signal(gWidth), height: signal(gHeight), color, border }),
131
249
  ...props
132
250
  })
133
251
  }
@@ -138,11 +256,11 @@ export function Triangle(props: TriangleProps) {
138
256
  color: '#000'
139
257
  })
140
258
  return Graphics({
141
- draw: (g) => {
142
- g.moveTo(0, height());
143
- g.lineTo(width() / 2, 0);
144
- g.lineTo(width(), height());
145
- g.lineTo(0, height());
259
+ draw: (g, gWidth, gHeight) => {
260
+ g.moveTo(0, gHeight);
261
+ g.lineTo(gWidth / 2, 0);
262
+ g.lineTo(gWidth, gHeight);
263
+ g.lineTo(0, gHeight);
146
264
  g.fill(color());
147
265
  if (border) {
148
266
  g.stroke(border);
@@ -152,9 +270,55 @@ export function Triangle(props: TriangleProps) {
152
270
  })
153
271
  }
154
272
 
273
+ /**
274
+ * Creates an SVG component that can render SVG graphics from URL, content, or legacy svg prop.
275
+ *
276
+ * This component provides three ways to display SVG graphics:
277
+ * - **src**: Load SVG from a URL using Assets.load with parseAsGraphicsContext option
278
+ * - **content**: Render SVG directly from string content using Graphics.svg() method
279
+ * - **svg**: Legacy prop for SVG content (for backward compatibility)
280
+ *
281
+ * @param props - Component properties including src, content, or svg
282
+ * @returns A reactive SVG component
283
+ * @example
284
+ * ```typescript
285
+ * // Load from URL
286
+ * const svgFromUrl = Svg({ src: "/assets/logo.svg" });
287
+ *
288
+ * // Direct content
289
+ * const svgFromContent = Svg({
290
+ * content: `<svg viewBox="0 0 100 100">
291
+ * <circle cx="50" cy="50" r="40" fill="blue"/>
292
+ * </svg>`
293
+ * });
294
+ *
295
+ * // Legacy usage
296
+ * const svgLegacy = Svg({ svg: "<svg>...</svg>" });
297
+ * ```
298
+ */
155
299
  export function Svg(props: SvgProps) {
156
300
  return Graphics({
157
- draw: (g) => g.svg(props.svg),
301
+ draw: async (g) => {
302
+ if (props.src) {
303
+ // Load SVG from source URL with graphics context parsing
304
+ const svgData = await Assets.load({
305
+ src: props.src,
306
+ data: {
307
+ parseAsGraphicsContext: true,
308
+ },
309
+ });
310
+
311
+ // Apply the loaded graphics context
312
+ const graphics = new PixiGraphics(svgData);
313
+ g.context = graphics.context;
314
+ } else if (props.content) {
315
+ // Render SVG directly from content string
316
+ g.svg(props.content);
317
+ } else if (props.svg) {
318
+ // Legacy prop support
319
+ g.svg(props.svg);
320
+ }
321
+ },
158
322
  ...props
159
323
  })
160
324
  }