@sprlab/wccompiler 0.10.11 → 0.11.0

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/README.md CHANGED
@@ -695,38 +695,20 @@ Each standalone component has its own isolated reactive runtime. Signals from co
695
695
 
696
696
  ## Framework Integrations
697
697
 
698
- WCC components are native custom elements — they work in any framework. Props, events, and named slots work natively with zero WCC-specific config. Two-way binding is zero-config in Angular; Vue and React require a lightweight plugin. Scoped slots require a framework plugin for idiomatic syntax.
698
+ WCC components are native custom elements — they work in any framework. Props, events, and named slots work natively with zero WCC-specific config. Two-way binding is zero-config in Angular; Vue requires a plugin. Scoped slots require a framework plugin or directive for idiomatic syntax.
699
699
 
700
- ### Zero-Config (works immediately)
700
+ ### Feature Support Matrix
701
701
 
702
- Just import the compiled component and use it:
702
+ | Feature | Vue (plugin) | Angular (directive) | React 19 (plugin) |
703
+ |---------|--------------|--------------------|--------------------|
704
+ | Props | ✅ `:count="ref"` | ✅ `[count]="signal()"` | ✅ `count={state}` |
705
+ | Events | ✅ `@count-changed="handler($event.detail)"` | ✅ `(count-changed)="handler($event.detail)"` | ✅ `oncountchanged={(e) => handler(e.detail)}` |
706
+ | Two-way binding | ✅ `v-model:count="ref"` | ✅ `[(count)]="signal"` | ❌ Not applicable |
707
+ | Default slot | ✅ children | ✅ children | ✅ children |
708
+ | Named slots | ✅ `<template #name>` | ✅ `<div slot-name>` | ✅ `<WccCard.Header>` |
709
+ | Scoped slots | ✅ `<template #name="{ prop }">` | ✅ `<ng-template slot="name" let-prop>` | ✅ `<WccList.Item>{(prop) => jsx}</WccList.Item>` |
703
710
 
704
- ```html
705
- <script type="module" src="dist/wcc-counter.js"></script>
706
- ```
707
-
708
- **Vue:**
709
- ```vue
710
- <wcc-counter :count="ref" @count-changed="handler($event.detail)">
711
- <div slot="footer">Footer content</div>
712
- </wcc-counter>
713
- ```
714
-
715
- **React 19:**
716
- ```jsx
717
- <wcc-counter count={state} onCountchanged={(e) => handler(e.detail)}>
718
- <div slot="footer">Footer content</div>
719
- </wcc-counter>
720
- ```
721
-
722
- **Angular:**
723
- ```html
724
- <wcc-counter [count]="signal()" (count-changed)="handler($event.detail)" [(count)]="signal">
725
- <div slot="footer">Footer content</div>
726
- </wcc-counter>
727
- ```
728
-
729
- ### Vue Plugin (v-model, modifiers, scoped slots)
711
+ ### Vue (with `wccVuePlugin`)
730
712
 
731
713
  ```js
732
714
  // vite.config.js
@@ -735,65 +717,184 @@ export default defineConfig({ plugins: [wccVuePlugin()] })
735
717
  ```
736
718
 
737
719
  ```vue
738
- <wcc-input v-model="text" v-model:count.number="count"></wcc-input>
739
- <wcc-card>
740
- <template #header><strong>Title</strong></template>
741
- <template #stats="{ likes }">{{ likes }} likes</template>
742
- </wcc-card>
743
- ```
744
-
745
- The plugin is needed for: v-model:prop (Vue assigns raw Event, not detail), v-model modifiers (.trim, .number), and scoped slot syntax (`{{prop}}` → `{%prop%}` escape).
746
-
747
- Without the plugin, use `:prop` + `@prop-changed` for two-way binding manually.
748
-
749
- ### React Plugin + Wrappers (scoped slots, typed events, two-way)
750
-
751
- ```js
752
- // vite.config.js
753
- import { wccReactPlugin } from '@sprlab/wccompiler/integrations/react'
754
- import react from '@vitejs/plugin-react'
755
- export default defineConfig({ plugins: [wccReactPlugin(), react()] })
756
- ```
757
-
758
- ```jsx
759
- // Auto-generated wrappers — events unwrapped, React-idiomatic naming
760
- import { createWccWrappers } from '@sprlab/wccompiler/adapters/react'
761
- const { WccCounter, WccCard } = createWccWrappers()
720
+ <script setup>
721
+ import { ref } from 'vue'
722
+ const count = ref(0)
723
+ const text = ref('')
724
+ </script>
762
725
 
763
- <WccCounter count={count} onCountChange={(value) => setCount(value)} />
764
- <WccCard renderStats={(likes) => <span>{likes} likes</span>}>
765
- <p>Body</p>
766
- </WccCard>
726
+ <template>
727
+ <!-- Props -->
728
+ <wcc-counter :count="count" label="Clicks"></wcc-counter>
729
+
730
+ <!-- Events -->
731
+ <wcc-counter @count-changed="count = $event.detail"></wcc-counter>
732
+
733
+ <!-- Two-way binding (v-model) -->
734
+ <wcc-counter v-model:count="count"></wcc-counter>
735
+ <wcc-input v-model.trim="text"></wcc-input>
736
+
737
+ <!-- Default slot -->
738
+ <wcc-card>
739
+ <p>Body content</p>
740
+ </wcc-card>
741
+
742
+ <!-- Named slots -->
743
+ <wcc-card>
744
+ <template #header><strong>Title</strong></template>
745
+ <p>Body</p>
746
+ <template #footer>Footer text</template>
747
+ </wcc-card>
748
+
749
+ <!-- Scoped slots -->
750
+ <wcc-list>
751
+ <template #item="{ item, index }">
752
+ <li>{{ index }}: {{ item }}</li>
753
+ </template>
754
+ </wcc-list>
755
+ </template>
767
756
  ```
768
757
 
769
- Wrappers read component metadata (`static __meta`) automatically no manual event/model config needed.
770
-
771
- ### Angular Directive (scoped slots only)
758
+ The plugin provides: `isCustomElement` config, `v-model:prop` support, v-model modifiers (`.trim`, `.number`), and scoped slot syntax (`{{prop}}` `{%prop%}` escape).
772
759
 
773
- Angular needs no plugin for props, events, or two-way binding. The directive is only needed for scoped slots:
760
+ ### Angular (with `WccSlotsDirective`)
774
761
 
775
762
  ```ts
763
+ import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
776
764
  import { WccSlotsDirective, WccSlotDef } from '@sprlab/wccompiler/adapters/angular'
777
765
 
778
766
  @Component({
779
767
  imports: [WccSlotsDirective, WccSlotDef],
780
768
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
781
769
  template: `
770
+ <!-- Props -->
771
+ <wcc-counter [count]="count" label="Clicks"></wcc-counter>
772
+
773
+ <!-- Events -->
774
+ <wcc-counter (count-changed)="onCount($event.detail)"></wcc-counter>
775
+
776
+ <!-- Two-way binding (banana-box) -->
777
+ <wcc-counter [(count)]="count"></wcc-counter>
778
+
779
+ <!-- Default slot -->
780
+ <wcc-card>
781
+ <p>Body content</p>
782
+ </wcc-card>
783
+
784
+ <!-- Named slots -->
782
785
  <wcc-card wccSlots>
783
- <ng-template slot="header"><strong>Title</strong></ng-template>
784
- <ng-template slot="stats" let-likes>{{ likes }} likes</ng-template>
786
+ <strong slot-header>Title</strong>
787
+ <p>Body</p>
788
+ <span slot-footer>Footer text</span>
785
789
  </wcc-card>
790
+
791
+ <!-- Scoped slots -->
792
+ <wcc-list wccSlots>
793
+ <ng-template slot="item" let-item let-index="index">
794
+ <li>{{ index }}: {{ item }}</li>
795
+ </ng-template>
796
+ </wcc-list>
786
797
  `
787
798
  })
799
+ export class AppComponent {
800
+ count = 0
801
+ onCount(value: number) { this.count = value }
802
+ }
788
803
  ```
789
804
 
790
- ### Vanilla
805
+ Angular needs no plugin for props, events, or two-way binding — only `CUSTOM_ELEMENTS_SCHEMA`. The directive is only needed for named slots (with `slot-name` syntax) and scoped slots.
806
+
807
+ ### React 19 (with `wccReactPlugin`)
808
+
809
+ ```js
810
+ // vite.config.js
811
+ import { wccReactPlugin } from '@sprlab/wccompiler/integrations/react'
812
+ import react from '@vitejs/plugin-react'
813
+ export default defineConfig({ plugins: [wccReactPlugin({ prefix: 'wcc-' }), react()] })
814
+ ```
815
+
816
+ ```jsx
817
+ import { useState } from 'react'
818
+ import { WccCard, WccList } from './dist/wcc-react'
819
+
820
+ export default function App() {
821
+ const [count, setCount] = useState(0)
822
+
823
+ return (
824
+ <>
825
+ {/* Props */}
826
+ <wcc-counter count={count} label="Clicks"></wcc-counter>
827
+
828
+ {/* Events */}
829
+ <wcc-counter oncountchanged={(e) => setCount(e.detail)}></wcc-counter>
830
+
831
+ {/* Default slot */}
832
+ <WccCard>
833
+ <p>Body content</p>
834
+ </WccCard>
835
+
836
+ {/* Named slots (compound pattern) */}
837
+ <WccCard>
838
+ <WccCard.Header><strong>Title</strong></WccCard.Header>
839
+ <p>Body</p>
840
+ <WccCard.Footer>Footer text</WccCard.Footer>
841
+ </WccCard>
842
+
843
+ {/* Named slots (props pattern) */}
844
+ <wcc-card header={<strong>Title</strong>} footer="Footer text">
845
+ <p>Body</p>
846
+ </wcc-card>
847
+
848
+ {/* Scoped slots (compound pattern) */}
849
+ <WccList>
850
+ <WccList.Item>{(item, index) => <li>{index}: {item}</li>}</WccList.Item>
851
+ </WccList>
852
+
853
+ {/* Scoped slots (render prop pattern) */}
854
+ <wcc-list renderItem={(item, index) => <li>{index}: {item}</li>} />
855
+ </>
856
+ )
857
+ }
858
+ ```
859
+
860
+ The plugin transforms PascalCase tags, compound components, props-as-slots, and render props at build time. Import stubs from `./dist/wcc-react` (auto-generated by `wcc build`).
861
+
862
+ ### Vanilla (no framework)
791
863
 
792
864
  No configuration needed:
793
865
 
794
866
  ```html
795
867
  <script type="module" src="dist/wcc-counter.js"></script>
796
- <wcc-counter></wcc-counter>
868
+ <script type="module" src="dist/wcc-card.js"></script>
869
+ <script type="module" src="dist/wcc-list.js"></script>
870
+
871
+ <!-- Props (attributes) -->
872
+ <wcc-counter count="0" label="Clicks"></wcc-counter>
873
+
874
+ <!-- Events -->
875
+ <script>
876
+ document.querySelector('wcc-counter')
877
+ .addEventListener('count-changed', (e) => console.log(e.detail))
878
+ </script>
879
+
880
+ <!-- Default slot -->
881
+ <wcc-card>
882
+ <p>Body content</p>
883
+ </wcc-card>
884
+
885
+ <!-- Named slots -->
886
+ <wcc-card>
887
+ <strong slot="header">Title</strong>
888
+ <p>Body</p>
889
+ <span slot="footer">Footer text</span>
890
+ </wcc-card>
891
+
892
+ <!-- Scoped slots -->
893
+ <wcc-list>
894
+ <template #item="{ item, index }">
895
+ <li>{{index}}: {{item}}</li>
896
+ </template>
897
+ </wcc-list>
797
898
  ```
798
899
 
799
900
  ## Editor Support
@@ -6,6 +6,7 @@
6
6
  * - WccSlotsDirective: Main directive activated via [wccSlots] attribute
7
7
  * - WccEvent: Single-event directive (wccEvent="name" + wccEmit output)
8
8
  * - WccEvents: Multi-event bridging directive (kebab-case → camelCase)
9
+ * - WccModel: Two-way binding bridge for [(prop)] banana-box syntax
9
10
  * - SlotContext: Interface for template context typing
10
11
  *
11
12
  * Usage:
@@ -547,3 +548,83 @@ export class WccEvents implements OnInit, OnDestroy {
547
548
  this.listeners = [];
548
549
  }
549
550
  }
551
+
552
+
553
+ // ─── WccModel — Two-way Binding Bridge (OPTIONAL) ───────────────────────────
554
+
555
+ /**
556
+ * Optional directive for Angular's [(prop)] banana-box syntax on WCC elements.
557
+ *
558
+ * NOTE: As of WCC v0.11+, the compiled component emits `propChange` directly,
559
+ * so [(prop)] works zero-config without this directive. This directive is kept
560
+ * as an alternative that uses the structured wcc:model event instead.
561
+ *
562
+ * Angular's [(prop)] expands to:
563
+ * [prop]="value" (propChange)="value = $event.detail"
564
+ *
565
+ * The component already emits `propChange` natively, so this works out of the box.
566
+ * This directive provides an alternative path via wcc:model for advanced use cases
567
+ * (e.g., when you need access to oldValue or want to handle multiple models centrally).
568
+ *
569
+ * Usage (optional):
570
+ * <wcc-input wccModel [(value)]="text"></wcc-input>
571
+ */
572
+ @Directive({
573
+ selector: '[wccModel]',
574
+ standalone: true,
575
+ })
576
+ export class WccModel implements OnInit, OnDestroy {
577
+ /** Optional explicit list of model prop names to bridge */
578
+ @Input() wccModel: string[] | '' = '';
579
+
580
+ private el = inject<ElementRef<HTMLElement>>(ElementRef);
581
+ private listener: ((e: Event) => void) | null = null;
582
+
583
+ ngOnInit(): void {
584
+ const hostEl = this.el.nativeElement;
585
+ const tagName = hostEl.tagName.toLowerCase();
586
+ if (!tagName.includes('-')) return;
587
+
588
+ this.setupModelBridge(hostEl, tagName);
589
+ }
590
+
591
+ private async setupModelBridge(hostEl: HTMLElement, tagName: string): Promise<void> {
592
+ // Determine which model props to bridge
593
+ let modelNames: string[];
594
+
595
+ if (Array.isArray(this.wccModel) && this.wccModel.length > 0) {
596
+ modelNames = this.wccModel;
597
+ } else {
598
+ // Auto-discover from component metadata
599
+ await customElements.whenDefined(tagName);
600
+ const ctor = customElements.get(tagName) as any;
601
+ modelNames = ctor?.__meta?.models || [];
602
+ }
603
+
604
+ if (modelNames.length === 0) return;
605
+
606
+ const modelSet = new Set(modelNames);
607
+
608
+ // Listen for wcc:model and re-dispatch as propChange
609
+ this.listener = (e: Event) => {
610
+ const detail = (e as CustomEvent).detail;
611
+ if (!detail || !modelSet.has(detail.prop)) return;
612
+
613
+ // Dispatch propChange (Angular banana-box convention)
614
+ hostEl.dispatchEvent(new CustomEvent(`${detail.prop}Change`, {
615
+ detail: detail.value,
616
+ bubbles: false,
617
+ cancelable: false,
618
+ }));
619
+ };
620
+
621
+ hostEl.addEventListener('wcc:model', this.listener);
622
+ }
623
+
624
+ ngOnDestroy(): void {
625
+ if (this.listener) {
626
+ this.el.nativeElement.removeEventListener('wcc:model', this.listener);
627
+ this.listener = null;
628
+ }
629
+ }
630
+ }
package/adapters/vue.js CHANGED
@@ -20,11 +20,11 @@
20
20
  */
21
21
 
22
22
  // ── Vue directive: v-wcc-model ──────────────────────────────────────
23
- // Fallback for non-Vite setups. Listens for propName-changed events directly.
23
+ // Fallback for non-Vite setups. Listens for wcc:model and filters by prop name.
24
24
 
25
25
  /**
26
26
  * Vue custom directive for two-way binding with WCC defineModel props.
27
- * Listens for `propName-changed` CustomEvent (emitted by WCC _modelSet).
27
+ * Listens for `wcc:model` CustomEvent and filters by prop name.
28
28
  */
29
29
  export const vWccModel = {
30
30
  mounted(el, binding) {
@@ -39,9 +39,10 @@ export const vWccModel = {
39
39
  el.setAttribute(propName, String(binding.value));
40
40
  }
41
41
 
42
- // Listen for propName-changed (WCC → Vue)
43
- const kebabName = propName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
42
+ // Listen for wcc:model (WCC → Vue), filter by prop name
44
43
  const handler = (e) => {
44
+ if (e.detail?.prop !== propName) return;
45
+ const newVal = e.detail.value;
45
46
  // Try to update the Vue ref via setupState
46
47
  const instance = binding.instance;
47
48
  if (instance) {
@@ -51,9 +52,9 @@ export const vWccModel = {
51
52
  const val = setupState[key];
52
53
  if (val === binding.value || val?.value === binding.value) {
53
54
  if (val?.value !== undefined) {
54
- val.value = e.detail;
55
+ val.value = newVal;
55
56
  } else {
56
- setupState[key] = e.detail;
57
+ setupState[key] = newVal;
57
58
  }
58
59
  break;
59
60
  }
@@ -61,9 +62,9 @@ export const vWccModel = {
61
62
  }
62
63
  };
63
64
 
64
- el.addEventListener(`${kebabName}-changed`, handler);
65
+ el.addEventListener('wcc:model', handler);
65
66
  el.__wccModelHandlers = el.__wccModelHandlers || {};
66
- el.__wccModelHandlers[propName] = { handler, eventName: `${kebabName}-changed` };
67
+ el.__wccModelHandlers[propName] = { handler, eventName: 'wcc:model' };
67
68
  },
68
69
 
69
70
  updated(el, binding) {
@@ -44,12 +44,12 @@
44
44
  * ```
45
45
  *
46
46
  * @example Two-way binding with [(prop)]
47
- * The adapter translates wcc:model events to Angular's propChange convention.
47
+ * The WccModel directive translates wcc:model events to Angular's propChange convention.
48
48
  * Angular's banana-in-a-box [(prop)] expands to:
49
49
  * [prop]="value" (propChange)="value = $event.detail"
50
50
  *
51
51
  * So when the WCC component emits wcc:model with { prop: 'value', value: 'new' },
52
- * the adapter re-dispatches as 'valueChange' CustomEvent, which Angular picks up.
52
+ * the WccModel directive re-dispatches as 'valueChange' CustomEvent, which Angular picks up.
53
53
  *
54
54
  * @example ngModel support (requires ControlValueAccessor)
55
55
  * For ngModel/ReactiveForms, see the WccValueAccessor guide in:
@@ -65,11 +65,9 @@ export function wccVuePlugin(options = {}) {
65
65
 
66
66
  let result = code
67
67
 
68
- // NOTE: As of WCC 0.10.3+, basic events work WITHOUT this plugin because
69
- // the compiled component emits events in multiple formats (kebab, camelCase, lowercase).
70
- // However, v-model:propName STILL REQUIRES this plugin because Vue assigns the raw
71
- // Event object to the ref — it doesn't extract .detail automatically.
72
- // This transform rewrites v-model:prop to @prop-changed="ref = $event.detail".
68
+ // NOTE: The compiled WCC component emits only `wcc:model` as its single
69
+ // canonical model change event. Framework-specific event formats are handled
70
+ // by each framework's adapter/plugin.
73
71
  //
74
72
  // This plugin is needed for:
75
73
  // 1. v-model:propName (Vue can't unwrap CustomEvent.detail natively)
@@ -78,11 +76,11 @@ export function wccVuePlugin(options = {}) {
78
76
 
79
77
  // Transform v-model:propName="expr" on custom elements (tags with hyphens)
80
78
  // Also handles modifiers: v-model:propName.trim.number="expr"
81
- // → :propName="expr" @propName-changed="expr = $event.detail"
82
- // with modifiers applied to the event handler value:
83
- // .trim → $event.detail.trim() (for string values)
84
- // .number → Number($event.detail)
85
- // .lazy → uses @propName-changed (same event, no difference for CE)
79
+ // → :propName="expr" @wcc:model="$event.detail.prop === 'propName' && (expr = value)"
80
+ // with modifiers applied to the extracted value:
81
+ // .trim → value.trim() (for string values)
82
+ // .number → Number(value)
83
+ // .lazy → no-op for custom elements
86
84
  // Run in a loop to handle multiple v-model on the same element
87
85
  let prev = ''
88
86
  while (prev !== result) {
@@ -91,7 +89,7 @@ export function wccVuePlugin(options = {}) {
91
89
  /(<[\w]+-[\w-]*(?:\s[^>]*?)?)\bv-model:(\w+)((?:\.\w+)*)="([^"]+)"/,
92
90
  (match, prefix, prop, modifiersStr, expr) => {
93
91
  const modifiers = modifiersStr ? modifiersStr.slice(1).split('.') : []
94
- let value = '$event.detail'
92
+ let value = '$event.detail.value'
95
93
  // Apply modifiers in order
96
94
  for (const mod of modifiers) {
97
95
  if (mod === 'trim') {
@@ -101,14 +99,14 @@ export function wccVuePlugin(options = {}) {
101
99
  }
102
100
  // .lazy is a no-op for custom elements (they already use change events)
103
101
  }
104
- return `${prefix}:${prop}="${expr}" @${prop}-changed="${expr} = ${value}"`
102
+ return `${prefix}:${prop}="${expr}" @wcc:model="$event.detail.prop === '${prop}' && (${expr} = ${value})"`
105
103
  }
106
104
  )
107
105
  }
108
106
 
109
107
  // Transform v-model="expr" (without argument) on custom elements
110
108
  // Also handles modifiers: v-model.trim.lazy="expr"
111
- // → :model-value="expr" @model-value-changed="expr = $event.detail"
109
+ // → :model-value="expr" @wcc:model="$event.detail.prop === 'modelValue' && (expr = value)"
112
110
  prev = ''
113
111
  while (prev !== result) {
114
112
  prev = result
@@ -116,7 +114,7 @@ export function wccVuePlugin(options = {}) {
116
114
  /(<[\w]+-[\w-]*(?:\s[^>]*?)?)\bv-model((?:\.\w+)*)="([^"]+)"/,
117
115
  (match, prefix, modifiersStr, expr) => {
118
116
  const modifiers = modifiersStr ? modifiersStr.slice(1).split('.') : []
119
- let value = '$event.detail'
117
+ let value = '$event.detail.value'
120
118
  for (const mod of modifiers) {
121
119
  if (mod === 'trim') {
122
120
  value = `(typeof ${value} === 'string' ? (${value}).trim() : ${value})`
@@ -124,7 +122,7 @@ export function wccVuePlugin(options = {}) {
124
122
  value = `Number(${value})`
125
123
  }
126
124
  }
127
- return `${prefix}:model-value="${expr}" @model-value-changed="${expr} = ${value}"`
125
+ return `${prefix}:model-value="${expr}" @wcc:model="$event.detail.prop === 'modelValue' && (${expr} = ${value})"`
128
126
  }
129
127
  )
130
128
  }
package/lib/codegen.js CHANGED
@@ -1773,43 +1773,23 @@ export function generateComponent(parseResult, options = {}) {
1773
1773
  }
1774
1774
 
1775
1775
  // _emit method (if emits declared)
1776
- // Emits the event in multiple formats for cross-framework compatibility:
1777
- // 1. Original name (kebab-case) for direct addEventListener and Angular (event-name)
1778
- // 2. camelCase — for Angular WccEvents directive
1779
- // 3. lowercase — for React 19 (onEventName → addEventListener('eventname'))
1776
+ // Emits the original event name + lowercase-no-hyphens for React 19 compatibility.
1777
+ // React 19 maps `oncountchanged` addEventListener('countchanged').
1780
1778
  if (emits.length > 0) {
1781
1779
  lines.push(' _emit(name, detail) {');
1782
1780
  lines.push(' const evt = { detail, bubbles: true, composed: true };');
1783
1781
  lines.push(' this.dispatchEvent(new CustomEvent(name, evt));');
1784
- // camelCase version (only if name contains hyphens)
1785
- lines.push(" if (name.includes('-')) {");
1786
- lines.push(" const camel = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());");
1787
- lines.push(' this.dispatchEvent(new CustomEvent(camel, evt));');
1788
- // lowercase version for React 19
1789
- lines.push(' this.dispatchEvent(new CustomEvent(camel.toLowerCase(), evt));');
1790
- lines.push(' } else {');
1791
- // If no hyphens, just emit lowercase (for React 19)
1792
- lines.push(' const lower = name.toLowerCase();');
1793
- lines.push(' if (lower !== name) this.dispatchEvent(new CustomEvent(lower, evt));');
1794
- lines.push(' }');
1782
+ lines.push(" const lower = name.replace(/-/g, '').toLowerCase();");
1783
+ lines.push(' if (lower !== name) this.dispatchEvent(new CustomEvent(lower, evt));');
1795
1784
  lines.push(' }');
1796
1785
  lines.push('');
1797
1786
  }
1798
1787
 
1799
1788
  // _modelSet methods (one per defineModel prop — emits events on internal write)
1800
1789
  // Emits:
1801
- // 1. wcc:model — generic event for vanilla JS and WCC-to-WCC binding
1802
- // 2. propName-changedkebab-case for direct addEventListener and Vue plugin
1803
- // 3. propNameChanged — camelCase for Angular WccEvents / direct binding
1804
- // 4. propnamechanged — lowercase for React 19 (onPropnameChanged → 'propnamechanged')
1805
- // 5. propNameChange — for Angular [(prop)] banana-box syntax
1806
- //
1807
- // NOTE: Vue v-model:prop requires the wccVuePlugin because Vue assigns the raw
1808
- // Event object to the ref (not event.detail). The plugin transforms v-model:prop
1809
- // to @prop-changed="ref = $event.detail" which correctly extracts the value.
1790
+ // 1. wcc:model — canonical event for vanilla JS, WCC-to-WCC, React adapter, Vue plugin
1791
+ // 2. propNameChange — for Angular [(prop)] banana-box syntax (zero-config)
1810
1792
  for (const md of modelDefs) {
1811
- const kebabName = camelToKebab(md.name);
1812
- const camelChanged = `${md.name}Changed`;
1813
1793
  lines.push(` _modelSet_${md.name}(newVal) {`);
1814
1794
  lines.push(` const oldVal = this._m_${md.name}();`);
1815
1795
  lines.push(` this._m_${md.name}(newVal);`);
@@ -1818,13 +1798,6 @@ export function generateComponent(parseResult, options = {}) {
1818
1798
  lines.push(` bubbles: true,`);
1819
1799
  lines.push(` composed: true`);
1820
1800
  lines.push(` }));`);
1821
- // Kebab-case: prop-name-changed (Vue plugin, addEventListener)
1822
- lines.push(` this.dispatchEvent(new CustomEvent('${kebabName}-changed', { detail: newVal, bubbles: true }));`);
1823
- // camelCase: propNameChanged (Angular, addEventListener)
1824
- lines.push(` this.dispatchEvent(new CustomEvent('${camelChanged}', { detail: newVal, bubbles: true }));`);
1825
- // lowercase: propnamechanged (React 19)
1826
- lines.push(` this.dispatchEvent(new CustomEvent('${camelChanged.toLowerCase()}', { detail: newVal, bubbles: true }));`);
1827
- // Angular banana-box: propNameChange
1828
1801
  lines.push(` this.dispatchEvent(new CustomEvent('${md.name}Change', { detail: newVal, bubbles: true }));`);
1829
1802
  lines.push(' }');
1830
1803
  lines.push('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.10.11",
3
+ "version": "0.11.0",
4
4
  "description": "Zero-runtime compiler that transforms .wcc single-file components into native web components with signals-based reactivity",
5
5
  "type": "module",
6
6
  "exports": {