@mintjamsinc/ichigojs 0.1.54 → 0.1.55

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/ichigo.cjs CHANGED
@@ -7,6 +7,7 @@
7
7
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
8
8
  /**
9
9
  * Represents a reusable component definition.
10
+ * @deprecated This class is deprecated and will be removed in a future release. Please use the new component registration system instead.
10
11
  */
11
12
  class VComponent {
12
13
  /**
@@ -44,6 +45,7 @@
44
45
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
45
46
  /**
46
47
  * A registry for managing component definitions.
48
+ * @deprecated This class is deprecated and will be removed in a future release. Please use the new component registration system instead.
47
49
  */
48
50
  class VComponentRegistry {
49
51
  /**
@@ -7474,6 +7476,14 @@
7474
7476
  else if (attributeName === 'style') {
7475
7477
  this.#updateStyle(element, value);
7476
7478
  }
7479
+ else if (this.#isCustomElementProperty(element, attributeName, value)) {
7480
+ // Custom elements: set objects/arrays (and declared props) as properties
7481
+ // so complex values are not serialized to strings via setAttribute.
7482
+ // HTML attributes are lowercased by the browser, so resolve to the
7483
+ // actual camelCase property name declared on the custom element.
7484
+ const propName = this.#resolveCustomElementPropertyName(element, attributeName);
7485
+ element[propName] = value;
7486
+ }
7477
7487
  else if (this.#isDOMProperty(attributeName)) {
7478
7488
  this.#updateProperty(element, attributeName, value);
7479
7489
  }
@@ -7557,6 +7567,46 @@
7557
7567
  #camelToKebab(str) {
7558
7568
  return str.replace(/([A-Z])/g, '-$1').toLowerCase();
7559
7569
  }
7570
+ /**
7571
+ * Returns true when the target is a custom element and the binding should be
7572
+ * delivered as a property rather than an HTML attribute.
7573
+ *
7574
+ * Two conditions trigger property delivery:
7575
+ * 1. The value is an object or array — serialising these to a string attribute
7576
+ * would lose type information.
7577
+ * 2. The element exposes a matching property accessor (e.g. a prop declared via
7578
+ * defineComponent), even when the value is a primitive.
7579
+ */
7580
+ #isCustomElementProperty(element, name, value) {
7581
+ if (!element.tagName.includes('-')) {
7582
+ return false;
7583
+ }
7584
+ const isObjectOrArray = Array.isArray(value) || (typeof value === 'object' && value !== null);
7585
+ return isObjectOrArray || name in element;
7586
+ }
7587
+ /**
7588
+ * Resolves the actual property name on a custom element for a given
7589
+ * (lowercased) HTML attribute name. HTML attributes are always lowercase,
7590
+ * but custom element props are typically camelCase. This method checks the
7591
+ * element's declared _props (set by defineComponent) for a case-insensitive
7592
+ * match, falling back to scanning the prototype's own property descriptors.
7593
+ */
7594
+ #resolveCustomElementPropertyName(element, name) {
7595
+ // Fast path: exact match already exists
7596
+ if (name in element) {
7597
+ return name;
7598
+ }
7599
+ // Check declared _props from defineComponent / IchigoElement
7600
+ const props = element.constructor._props;
7601
+ if (Array.isArray(props)) {
7602
+ const lowerName = name.toLowerCase();
7603
+ const match = props.find(p => p.toLowerCase() === lowerName);
7604
+ if (match) {
7605
+ return match;
7606
+ }
7607
+ }
7608
+ return name;
7609
+ }
7560
7610
  /**
7561
7611
  * Checks if the attribute should be set as a DOM property.
7562
7612
  */
@@ -8937,6 +8987,18 @@
8937
8987
  });
8938
8988
  }
8939
8989
  }
8990
+ else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
8991
+ // If the node is a DocumentFragment, create child VNodes for its children
8992
+ // DocumentFragment is not an Element so it has no directives itself.
8993
+ this.#childVNodes = [];
8994
+ for (const childNode of Array.from(this.#node.childNodes)) {
8995
+ new VNode({
8996
+ node: childNode,
8997
+ vApplication: this.#vApplication,
8998
+ parentVNode: this
8999
+ });
9000
+ }
9001
+ }
8940
9002
  // Register this node as a dependent of the parent node, if any
8941
9003
  this.#closers = this.#parentVNode?.addDependent(this);
8942
9004
  }
@@ -9191,6 +9253,16 @@
9191
9253
  });
9192
9254
  }
9193
9255
  }
9256
+ else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
9257
+ // For document fragments (e.g. from <template v-if>), propagate updates
9258
+ // to dependent virtual nodes so that child bindings stay reactive.
9259
+ this.#dependents?.forEach(dependentNode => {
9260
+ const changed = dependentNode.dependentIdentifiers.some(id => changes.some(change => dependentNode.bindings.doesChangeMatchIdentifier(change, id)));
9261
+ if (changed) {
9262
+ dependentNode.update();
9263
+ }
9264
+ });
9265
+ }
9194
9266
  }
9195
9267
  /**
9196
9268
  * Forces an update of the virtual node and its children, regardless of changed identifiers.
@@ -9240,6 +9312,12 @@
9240
9312
  });
9241
9313
  }
9242
9314
  }
9315
+ else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
9316
+ // For document fragments, recursively force update child VNodes
9317
+ this.#childVNodes?.forEach(childVNode => {
9318
+ childVNode.forceUpdate();
9319
+ });
9320
+ }
9243
9321
  }
9244
9322
  /**
9245
9323
  * Adds a child virtual node to this virtual node.
@@ -9617,9 +9695,11 @@
9617
9695
  }
9618
9696
  // Clone the original node and create a new VNode for it
9619
9697
  const clone = this.#cloneNode();
9620
- // Insert the cloned node after the anchor node, or as a child of the parent if no anchor
9621
- this.#vNode.anchorNode?.parentNode?.insertBefore(clone, this.#vNode.anchorNode.nextSibling);
9622
- // Create a new VNode for the cloned element
9698
+ // Create a new VNode for the cloned element BEFORE inserting into DOM.
9699
+ // This prevents custom elements (Web Components) from having their
9700
+ // connectedCallback fire before VNode processes their children, which
9701
+ // would cause the parent VApplication to incorrectly adopt the custom
9702
+ // element's internal template content as its own VNode tree.
9623
9703
  // Pass the current bindings to ensure loop variables from v-for are available
9624
9704
  const vNode = new VNode({
9625
9705
  node: clone,
@@ -9627,6 +9707,29 @@
9627
9707
  parentVNode: this.#vNode,
9628
9708
  bindings: this.#vNode.bindings
9629
9709
  });
9710
+ // Insert after the anchor node AFTER VNode creation
9711
+ const anchorParent = this.#vNode.anchorNode?.parentNode;
9712
+ const nextSibling = this.#vNode.anchorNode?.nextSibling ?? null;
9713
+ if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE && anchorParent) {
9714
+ const startMarker = document.createComment('#vif-fragment-start');
9715
+ const endMarker = document.createComment('#vif-fragment-end');
9716
+ if (nextSibling) {
9717
+ anchorParent.insertBefore(startMarker, nextSibling);
9718
+ }
9719
+ else {
9720
+ anchorParent.appendChild(startMarker);
9721
+ }
9722
+ anchorParent.insertBefore(endMarker, startMarker.nextSibling);
9723
+ anchorParent.insertBefore(clone, endMarker);
9724
+ // Store markers for later removal
9725
+ vNode.userData.set('vif_fragment_start', startMarker);
9726
+ vNode.userData.set('vif_fragment_end', endMarker);
9727
+ this.#renderedVNode = vNode;
9728
+ this.#renderedVNode.forceUpdate();
9729
+ return;
9730
+ }
9731
+ const nodeToInsert = vNode.anchorNode || clone;
9732
+ anchorParent?.insertBefore(nodeToInsert, nextSibling);
9630
9733
  this.#renderedVNode = vNode;
9631
9734
  this.#renderedVNode.forceUpdate();
9632
9735
  }
@@ -9641,17 +9744,40 @@
9641
9744
  }
9642
9745
  // Destroy VNode first (calls @unmount hooks while DOM is still accessible)
9643
9746
  this.#renderedVNode.destroy();
9644
- // Then remove from DOM
9645
- this.#renderedVNode.node.parentNode?.removeChild(this.#renderedVNode.node);
9747
+ // Then remove from DOM. Handle fragment markers if present
9748
+ const startMarker = this.#renderedVNode.userData.get?.('vif_fragment_start');
9749
+ const endMarker = this.#renderedVNode.userData.get?.('vif_fragment_end');
9750
+ if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
9751
+ const parentNode = startMarker.parentNode;
9752
+ let node = startMarker;
9753
+ while (node) {
9754
+ const next = node.nextSibling;
9755
+ parentNode.removeChild(node);
9756
+ if (node === endMarker)
9757
+ break;
9758
+ node = next;
9759
+ }
9760
+ this.#renderedVNode = undefined;
9761
+ return;
9762
+ }
9763
+ const parent = this.#renderedVNode.node.parentNode;
9764
+ if (parent) {
9765
+ parent.removeChild(this.#renderedVNode.node);
9766
+ }
9646
9767
  this.#renderedVNode = undefined;
9647
9768
  }
9648
9769
  /**
9649
9770
  * Clones the original node of the directive's virtual node.
9650
- * This is used to create a new instance of the node for rendering.
9651
- * @returns The cloned HTMLElement.
9771
+ * When the source element is a <template>, returns a DocumentFragment
9772
+ * cloned from the template's content.
9773
+ * @returns The cloned Node (HTMLElement or DocumentFragment).
9652
9774
  */
9653
9775
  #cloneNode() {
9654
9776
  const element = this.#vNode.node;
9777
+ if (element instanceof HTMLTemplateElement) {
9778
+ // Return a DocumentFragment cloned from the template content
9779
+ return element.content.cloneNode(true);
9780
+ }
9655
9781
  return element.cloneNode(true);
9656
9782
  }
9657
9783
  /**
@@ -9969,10 +10095,28 @@
9969
10095
  vNode.destroy();
9970
10096
  }
9971
10097
  }
9972
- // Then remove from DOM
10098
+ // Then remove from DOM. Handle both Element nodes and fragment-marked ranges.
9973
10099
  for (const vNode of nodesToRemove) {
9974
- if (vNode.node.parentNode) {
9975
- vNode.node.parentNode.removeChild(vNode.node);
10100
+ // If this VNode stored fragment markers, remove the range between them
10101
+ const startMarker = vNode.userData.get?.('vfor_fragment_start');
10102
+ const endMarker = vNode.userData.get?.('vfor_fragment_end');
10103
+ if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
10104
+ const parentNode = startMarker.parentNode;
10105
+ let node = startMarker;
10106
+ // Remove nodes from startMarker up to and including endMarker
10107
+ while (node) {
10108
+ const next = node.nextSibling;
10109
+ parentNode.removeChild(node);
10110
+ if (node === endMarker)
10111
+ break;
10112
+ node = next;
10113
+ }
10114
+ continue;
10115
+ }
10116
+ // Fallback: remove the node itself if it's attached
10117
+ const parentOfNode = vNode.node.parentNode;
10118
+ if (parentOfNode) {
10119
+ parentOfNode.removeChild(vNode.node);
9976
10120
  }
9977
10121
  }
9978
10122
  // Add or reorder items
@@ -10006,6 +10150,28 @@
10006
10150
  bindings,
10007
10151
  dependentIdentifiers: depIds,
10008
10152
  });
10153
+ // If clone is a DocumentFragment, insert it between start/end comment markers
10154
+ if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
10155
+ const startMarker = document.createComment('#vfor-fragment-start');
10156
+ const endMarker = document.createComment('#vfor-fragment-end');
10157
+ // Insert start and end markers and the fragment's children between them
10158
+ if (prevNode.nextSibling) {
10159
+ parent.insertBefore(startMarker, prevNode.nextSibling);
10160
+ }
10161
+ else {
10162
+ parent.appendChild(startMarker);
10163
+ }
10164
+ parent.insertBefore(endMarker, startMarker.nextSibling);
10165
+ parent.insertBefore(clone, endMarker);
10166
+ // Store markers on the VNode for later removal/movement
10167
+ vNode.userData.set('vfor_fragment_start', startMarker);
10168
+ vNode.userData.set('vfor_fragment_end', endMarker);
10169
+ newRenderedItems.set(key, vNode);
10170
+ vNode.forceUpdate();
10171
+ // Use endMarker as prevNode for subsequent insertions
10172
+ prevNode = endMarker;
10173
+ continue;
10174
+ }
10009
10175
  // Determine what to insert: anchor node (if exists) or the clone itself
10010
10176
  const nodeToInsert = vNode.anchorNode || clone;
10011
10177
  // Insert after previous node
@@ -10023,8 +10189,9 @@
10023
10189
  newRenderedItems.set(key, vNode);
10024
10190
  // Update bindings
10025
10191
  this.#updateItemBindings(vNode, context);
10026
- // Determine the actual node in DOM: anchor node (if exists) or vNode.node
10027
- const actualNode = vNode.anchorNode || vNode.node;
10192
+ // Determine the actual node in DOM: prefer fragment end marker, then anchor node, then vNode.node
10193
+ const fragmentEnd = vNode.userData.get?.('vfor_fragment_end');
10194
+ const actualNode = fragmentEnd || vNode.anchorNode || vNode.node;
10028
10195
  // Move to correct position if needed
10029
10196
  if (prevNode.nextSibling !== actualNode) {
10030
10197
  if (prevNode.nextSibling) {
@@ -10035,8 +10202,9 @@
10035
10202
  }
10036
10203
  }
10037
10204
  }
10038
- // Use anchor node as prevNode if it exists, otherwise use vNode.node
10039
- prevNode = vNode.anchorNode || vNode.node;
10205
+ // Use fragment end marker > anchor node > vNode.node as prevNode
10206
+ const fragmentEndForPrev = vNode.userData.get?.('vfor_fragment_end');
10207
+ prevNode = fragmentEndForPrev || vNode.anchorNode || vNode.node;
10040
10208
  }
10041
10209
  // Update rendered items map
10042
10210
  this.#renderedItems = newRenderedItems;
@@ -10215,12 +10383,17 @@
10215
10383
  }
10216
10384
  /**
10217
10385
  * Clones the original node of the directive's virtual node.
10218
- * This is used to create a new instance of the node for rendering.
10219
- * @returns The cloned HTMLElement.
10386
+ * When the source element is a <template>, its children (stored in .content)
10387
+ * are cloned into a DocumentFragment so that multiple root nodes can be
10388
+ * managed without adding an extra wrapper element.
10389
+ * @returns The cloned Node (Element or DocumentFragment).
10220
10390
  */
10221
10391
  #cloneNode() {
10222
- // Clone the original element
10223
10392
  const element = this.#vNode.node;
10393
+ if (element instanceof HTMLTemplateElement) {
10394
+ // Return a cloned DocumentFragment containing the template content
10395
+ return element.content.cloneNode(true);
10396
+ }
10224
10397
  return element.cloneNode(true);
10225
10398
  }
10226
10399
  /**
@@ -13026,6 +13199,7 @@
13026
13199
  /**
13027
13200
  * Gets the component registry.
13028
13201
  * @return {VComponentRegistry} The component registry.
13202
+ * @deprecated This method is deprecated and will be removed in a future release. Please use the new component registration system instead.
13029
13203
  */
13030
13204
  static get componentRegistry() {
13031
13205
  return this.#componentRegistry;
@@ -13069,11 +13243,268 @@
13069
13243
  }
13070
13244
  }
13071
13245
 
13246
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
13247
+ /**
13248
+ * Base class for ichigo.js-backed Web Components (Light DOM, no Shadow DOM).
13249
+ *
13250
+ * Mount timing:
13251
+ * - If the component declares no props, the VApplication is mounted synchronously
13252
+ * at the end of connectedCallback (the template is known, no props to wait for).
13253
+ * - If the component declares props, the VApplication is mounted the first time
13254
+ * _setProp() is called after connectedCallback has prepared the DOM. This
13255
+ * guarantees that the parent framework (e.g. ichigo.js VBindDirective) has
13256
+ * already delivered at least one prop value before data() is evaluated.
13257
+ *
13258
+ * Subclasses must set the static fields _template, _props and _buildOptions before
13259
+ * calling customElements.define(). defineComponent() handles this automatically.
13260
+ */
13261
+ class IchigoElement extends HTMLElement {
13262
+ /**
13263
+ * The mounted VApplication instance, present only while connected to the DOM.
13264
+ */
13265
+ #app;
13266
+ /**
13267
+ * Stores prop values received at any time (before or after mount).
13268
+ */
13269
+ #propValues = {};
13270
+ /**
13271
+ * The root element cloned from the template, ready for mounting.
13272
+ * Set by connectedCallback; cleared on disconnect.
13273
+ */
13274
+ #mountRoot;
13275
+ /**
13276
+ * Whether a mount microtask is already queued to avoid double-mounting.
13277
+ */
13278
+ #mountScheduled = false;
13279
+ connectedCallback() {
13280
+ // --- 0. Guard: skip if template not yet loaded ---
13281
+ // customElements.define() may run before loadComponent() completes (e.g. when
13282
+ // dynamic imports are inlined by the bundler). The static placeholder element
13283
+ // in the HTML will be removed and replaced by v-for once the template is ready,
13284
+ // so it is safe to do nothing here.
13285
+ const ctor = this.constructor;
13286
+ const templateEl = document.querySelector(ctor._template);
13287
+ if (!templateEl || !(templateEl instanceof HTMLTemplateElement)) {
13288
+ return;
13289
+ }
13290
+ // --- 1. Capture slot content before clearing children ---
13291
+ const defaultSlotNodes = [];
13292
+ const namedSlotNodes = new Map();
13293
+ for (const child of Array.from(this.childNodes)) {
13294
+ if (child.nodeType === Node.ELEMENT_NODE) {
13295
+ const el = child;
13296
+ const slotName = el.getAttribute('slot');
13297
+ if (slotName) {
13298
+ if (!namedSlotNodes.has(slotName)) {
13299
+ namedSlotNodes.set(slotName, []);
13300
+ }
13301
+ namedSlotNodes.get(slotName).push(el);
13302
+ // Remove slot attribute so ichigo.js doesn't try to bind it
13303
+ el.removeAttribute('slot');
13304
+ }
13305
+ else {
13306
+ defaultSlotNodes.push(el);
13307
+ }
13308
+ }
13309
+ else if (child.nodeType === Node.TEXT_NODE) {
13310
+ if ((child.textContent ?? '').trim()) {
13311
+ defaultSlotNodes.push(child);
13312
+ }
13313
+ }
13314
+ }
13315
+ // Clear host element so we can append the cloned template
13316
+ while (this.firstChild) {
13317
+ this.removeChild(this.firstChild);
13318
+ }
13319
+ // --- 2. Clone the component template ---
13320
+ const fragment = templateEl.content.cloneNode(true);
13321
+ const root = this.#findRootElement(fragment);
13322
+ // --- 3. Distribute named slot content ---
13323
+ for (const [name, nodes] of namedSlotNodes) {
13324
+ const slot = root.querySelector(`slot[name="${name}"]`);
13325
+ if (slot) {
13326
+ slot.replaceWith(...nodes);
13327
+ }
13328
+ }
13329
+ // --- 4. Distribute default slot content ---
13330
+ const defaultSlot = root.querySelector('slot:not([name])');
13331
+ if (defaultSlot && defaultSlotNodes.length > 0) {
13332
+ defaultSlot.replaceWith(...defaultSlotNodes);
13333
+ }
13334
+ // Attach the populated template to the host element
13335
+ this.appendChild(root);
13336
+ this.#mountRoot = root;
13337
+ // If props were set before connectedCallback, ensure mount is scheduled
13338
+ this.#scheduleMountIfNeeded();
13339
+ // --- 5. Mount strategy ---
13340
+ // If this component has no declared props, mount immediately.
13341
+ // If it has props, mount will be triggered from _setProp() once the parent
13342
+ // delivers the first prop value (via VBindDirective / forceUpdate).
13343
+ if (ctor._props.length === 0) {
13344
+ this.#doMount();
13345
+ }
13346
+ // else: wait for _setProp() to trigger #scheduleMountIfNeeded()
13347
+ }
13348
+ disconnectedCallback() {
13349
+ this.#mountRoot = undefined;
13350
+ this.#mountScheduled = false;
13351
+ if (this.#app) {
13352
+ this.#app.unmount();
13353
+ this.#app = undefined;
13354
+ }
13355
+ }
13356
+ /**
13357
+ * Called by the property setters generated by defineComponent().
13358
+ * Before mount: stores the value and schedules a mount microtask.
13359
+ * After mount: pushes the value directly into the reactive bindings.
13360
+ */
13361
+ _setProp(name, value) {
13362
+ this.#propValues[name] = value;
13363
+ if (this.#app) {
13364
+ this.#app.bindings?.set(name, value);
13365
+ }
13366
+ else {
13367
+ this.#scheduleMountIfNeeded();
13368
+ }
13369
+ }
13370
+ /**
13371
+ * Called by the property getters generated by defineComponent().
13372
+ */
13373
+ _getProp(name) {
13374
+ return this.#propValues[name];
13375
+ }
13376
+ // --- Static fields set by defineComponent() ---
13377
+ /**
13378
+ * CSS selector for the component's <template> element (e.g. '#my-card').
13379
+ */
13380
+ static _template;
13381
+ /**
13382
+ * List of declared prop names. Used to decide whether to defer mounting.
13383
+ */
13384
+ static _props = [];
13385
+ /**
13386
+ * Factory that builds VApplicationOptions from the current prop values.
13387
+ * Implemented by defineComponent() as a closure that captures the user's options.
13388
+ */
13389
+ static _buildOptions;
13390
+ // --- Private helpers ---
13391
+ /**
13392
+ * Schedules a mount microtask if the DOM root is ready and no mount is pending.
13393
+ * Called from _setProp() so the mount happens after the prop value is stored.
13394
+ */
13395
+ #scheduleMountIfNeeded() {
13396
+ if (this.#mountScheduled || this.#app || !this.#mountRoot) {
13397
+ return;
13398
+ }
13399
+ this.#mountScheduled = true;
13400
+ queueMicrotask(() => {
13401
+ this.#mountScheduled = false;
13402
+ this.#doMount();
13403
+ });
13404
+ }
13405
+ #doMount() {
13406
+ if (this.#app || !this.#mountRoot) {
13407
+ return;
13408
+ }
13409
+ const ctor = this.constructor;
13410
+ const options = ctor._buildOptions(this.#propValues);
13411
+ this.#app = VDOM.createApp(options);
13412
+ this.#app.mount(this.#mountRoot);
13413
+ }
13414
+ #findRootElement(fragment) {
13415
+ for (const node of Array.from(fragment.childNodes)) {
13416
+ if (node.nodeType === Node.ELEMENT_NODE) {
13417
+ return node;
13418
+ }
13419
+ }
13420
+ throw new Error(`IchigoElement: no root element found in template '${this.constructor._template}'`);
13421
+ }
13422
+ }
13423
+
13424
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
13425
+ /**
13426
+ * Defines and registers a custom element backed by ichigo.js reactivity.
13427
+ *
13428
+ * Usage:
13429
+ * ```html
13430
+ * <template id="my-list">
13431
+ * <div>
13432
+ * <ul v-if="items.length > 0">
13433
+ * <li v-for="item of items">{{item.name}}</li>
13434
+ * </ul>
13435
+ * <slot></slot>
13436
+ * </div>
13437
+ * </template>
13438
+ * ```
13439
+ * ```typescript
13440
+ * defineComponent('my-list', {
13441
+ * template: '#my-list',
13442
+ * props: ['items'],
13443
+ * data() {
13444
+ * return { items: this.items ?? [] };
13445
+ * },
13446
+ * });
13447
+ * ```
13448
+ * ```html
13449
+ * <my-list :items="searchResults">
13450
+ * <span slot="empty">No results.</span>
13451
+ * </my-list>
13452
+ * ```
13453
+ *
13454
+ * @param tagName Custom element tag name (must contain a hyphen, e.g. 'my-card').
13455
+ * @param options Component options including template selector and optional props.
13456
+ */
13457
+ function defineComponent(tagName, options) {
13458
+ const { props = [], template, data, computed, methods, watch, logLevel } = options;
13459
+ // Build a subclass of IchigoElement specific to this component
13460
+ class ComponentElement extends IchigoElement {
13461
+ static _template = template;
13462
+ static _props = props;
13463
+ static _buildOptions(propValues) {
13464
+ return {
13465
+ data() {
13466
+ // 'this' is the $ctx object provided by VApplication ({ $markRaw }).
13467
+ // We extend it with prop values so the user's data() can reference them
13468
+ // via 'this.propName' and supply defaults (e.g. `this.items ?? []`).
13469
+ const ctx = { $markRaw: ReactiveProxy.markRaw.bind(ReactiveProxy), ...propValues };
13470
+ const userData = data
13471
+ ? data.call(ctx)
13472
+ : {};
13473
+ // Props are always included in data so they are reactive from the start.
13474
+ // User-returned values take precedence (allow transforming/defaulting props).
13475
+ return { ...propValues, ...userData };
13476
+ },
13477
+ computed,
13478
+ methods,
13479
+ watch,
13480
+ logLevel,
13481
+ };
13482
+ }
13483
+ }
13484
+ // Generate a property getter/setter for each declared prop.
13485
+ // This enables the parent VApplication to push updates via `element.propName = value`.
13486
+ for (const prop of props) {
13487
+ Object.defineProperty(ComponentElement.prototype, prop, {
13488
+ get() {
13489
+ return this._getProp(prop);
13490
+ },
13491
+ set(value) {
13492
+ this._setProp(prop, value);
13493
+ },
13494
+ configurable: true,
13495
+ enumerable: true,
13496
+ });
13497
+ }
13498
+ customElements.define(tagName, ComponentElement);
13499
+ }
13500
+
13072
13501
  exports.ExpressionUtils = ExpressionUtils;
13502
+ exports.IchigoElement = IchigoElement;
13073
13503
  exports.ReactiveProxy = ReactiveProxy;
13074
13504
  exports.VComponent = VComponent;
13075
13505
  exports.VComponentRegistry = VComponentRegistry;
13076
13506
  exports.VDOM = VDOM;
13507
+ exports.defineComponent = defineComponent;
13077
13508
 
13078
13509
  }));
13079
13510
  //# sourceMappingURL=ichigo.cjs.map