@sprlab/wccompiler 0.10.5 → 0.10.7

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
@@ -192,6 +192,11 @@ watch(count, (newVal, oldVal) => {
192
192
  watch(() => props.count, (newVal, oldVal) => {
193
193
  console.log(`Prop changed: ${oldVal} → ${newVal}`)
194
194
  })
195
+
196
+ // Watch a derived expression
197
+ watch(() => count() * 2, (newVal, oldVal) => {
198
+ console.log(`Doubled changed: ${oldVal} → ${newVal}`)
199
+ })
195
200
  ```
196
201
 
197
202
  `watch` observes a specific signal or getter and provides both old and new values. The callback does not run on initial mount — only on subsequent changes.
@@ -248,6 +253,58 @@ const emit = defineEmits<{ (e: 'change', value: number): void }>()
248
253
 
249
254
  The compiler validates emit calls against declared events at compile time.
250
255
 
256
+ ## defineModel (Two-Way Binding)
257
+
258
+ `defineModel` declares a prop that supports two-way binding across frameworks:
259
+
260
+ ```js
261
+ import { defineModel } from 'wcc'
262
+
263
+ const count = defineModel({ name: 'count', default: 0 })
264
+ const title = defineModel({ name: 'title', default: 'untitled' })
265
+ ```
266
+
267
+ Read and write like a signal:
268
+ ```js
269
+ count() // read current value
270
+ count.set(5) // write — updates internal state + emits events
271
+ ```
272
+
273
+ **Events emitted on write:**
274
+
275
+ | Event | Purpose |
276
+ |-------|---------|
277
+ | `count-changed` | Kebab-case — for Vue plugin, addEventListener |
278
+ | `countChanged` | camelCase — for Angular, direct binding |
279
+ | `countChange` | Angular `[(count)]` banana-box syntax |
280
+ | `wcc:model` | Generic — for vanilla JS and WCC-to-WCC |
281
+
282
+ **Usage per framework:**
283
+
284
+ ```vue
285
+ <!-- Vue (with plugin) -->
286
+ <wcc-counter v-model:count="ref"></wcc-counter>
287
+
288
+ <!-- Vue (without plugin) -->
289
+ <wcc-counter :count="ref" @count-changed="ref = $event.detail"></wcc-counter>
290
+ ```
291
+
292
+ ```jsx
293
+ // React (with wrapper)
294
+ <WccCounter count={count} onCountChange={(value) => setCount(value)} />
295
+
296
+ // React (native CE)
297
+ <wcc-counter count={count} oncountchanged={(e) => setCount(e.detail)} />
298
+ ```
299
+
300
+ ```html
301
+ <!-- Angular (zero-config two-way) -->
302
+ <wcc-counter [(count)]="signal"></wcc-counter>
303
+
304
+ <!-- Angular (manual) -->
305
+ <wcc-counter [count]="signal()" (countChange)="signal.set($event.detail)"></wcc-counter>
306
+ ```
307
+
251
308
  ## Template Directives
252
309
 
253
310
  ### Text Interpolation
@@ -638,63 +695,96 @@ Each standalone component has its own isolated reactive runtime. Signals from co
638
695
 
639
696
  ## Framework Integrations
640
697
 
641
- WCC components are native custom elements — they work in any framework. Optional integration helpers reduce configuration friction:
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.
642
699
 
643
- ### Vue 3 (Vite)
700
+ ### Zero-Config (works immediately)
701
+
702
+ Just import the compiled component and use it:
703
+
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)
644
730
 
645
731
  ```js
646
732
  // vite.config.js
647
733
  import { wccVuePlugin } from '@sprlab/wccompiler/integrations/vue'
734
+ export default defineConfig({ plugins: [wccVuePlugin()] })
735
+ ```
648
736
 
649
- export default defineConfig({
650
- plugins: [wccVuePlugin()]
651
- })
652
-
653
- // Custom prefix:
654
- // plugins: [wccVuePlugin({ prefix: 'my-' })]
737
+ ```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>
655
743
  ```
656
744
 
657
- ### React
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)
658
750
 
659
- React 19+ supports custom elements natively. For React 18, use the event hook to bridge CustomEvents:
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
+ ```
660
757
 
661
758
  ```jsx
662
- import { useRef } from 'react'
663
- import { useWccEvent } from '@sprlab/wccompiler/integrations/react'
664
-
665
- function App() {
666
- // Form 1: Pass an existing ref
667
- const counterRef = useRef(null)
668
- useWccEvent(counterRef, 'change', (e) => console.log(e.detail))
669
- return <wcc-counter ref={counterRef}></wcc-counter>
670
- }
759
+ // Auto-generated wrappers events unwrapped, React-idiomatic naming
760
+ import { createWccWrappers } from '@sprlab/wccompiler/adapters/react'
761
+ const { WccCounter, WccCard } = createWccWrappers()
671
762
 
672
- // Form 2: Let the hook create the ref
673
- function App2() {
674
- const ref = useWccEvent('change', (e) => console.log(e.detail))
675
- return <wcc-counter ref={ref}></wcc-counter>
676
- }
763
+ <WccCounter count={count} onCountChange={(value) => setCount(value)} />
764
+ <WccCard renderStats={(likes) => <span>{likes} likes</span>}>
765
+ <p>Body</p>
766
+ </WccCard>
677
767
  ```
678
768
 
679
- ### Angular
769
+ Wrappers read component metadata (`static __meta`) automatically — no manual event/model config needed.
770
+
771
+ ### Angular Directive (scoped slots only)
680
772
 
681
- Add `CUSTOM_ELEMENTS_SCHEMA` to your component or module this is Angular's built-in way to allow custom elements:
773
+ Angular needs no plugin for props, events, or two-way binding. The directive is only needed for scoped slots:
682
774
 
683
775
  ```ts
684
- import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
776
+ import { WccSlotsDirective, WccSlotDef } from '@sprlab/wccompiler/adapters/angular'
685
777
 
686
- // Standalone component (Angular 17+)
687
778
  @Component({
779
+ imports: [WccSlotsDirective, WccSlotDef],
688
780
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
689
- template: `<wcc-counter></wcc-counter>`
690
- })
691
- export class AppComponent {}
692
-
693
- // Or NgModule approach
694
- @NgModule({
695
- schemas: [CUSTOM_ELEMENTS_SCHEMA],
781
+ template: `
782
+ <wcc-card wccSlots>
783
+ <ng-template slot="header"><strong>Title</strong></ng-template>
784
+ <ng-template slot="stats" let-likes>{{ likes }} likes</ng-template>
785
+ </wcc-card>
786
+ `
696
787
  })
697
- export class AppModule {}
698
788
  ```
699
789
 
700
790
  ### Vanilla
package/adapters/react.js CHANGED
@@ -4,25 +4,24 @@
4
4
  *
5
5
  * @module @sprlab/wccompiler/adapters/react
6
6
  *
7
- * IMPORTANT: Import hooks from THIS file (not from integrations/react).
8
- * The integrations/react file is for vite.config.js only (contains Babel).
7
+ * NOTE: These hooks are optional utilities for React 18 or edge cases.
8
+ * With React 19 + wccReactPlugin, WCC components work natively:
9
9
  *
10
- * Usage:
11
- * import { useWccEvent, useWccModel, createWccWrapper } from '@sprlab/wccompiler/adapters/react'
10
+ * <WccCounter count={state} oncountchanged={(e) => setState(e.detail)} />
11
+ * <WccCard>
12
+ * <WccCard.Header><strong>Title</strong></WccCard.Header>
13
+ * </WccCard>
12
14
  *
13
- * // Option A: Low-level hooks (full control)
14
- * const ref = useWccEvent('change', (e) => console.log(e.detail))
15
- * <wcc-counter ref={ref}></wcc-counter>
15
+ * The plugin handles PascalCase → kebab-case, compound components, and
16
+ * scoped slots at build time. No runtime wrappers needed.
16
17
  *
17
- * // Option B: Wrapper components (idiomatic React DX)
18
- * const WccCounter = createWccWrapper('wcc-counter', {
19
- * events: ['change'],
20
- * models: ['count']
21
- * })
22
- * <WccCounter onChange={handler} count={count} onCountChanged={setCount} />
18
+ * These hooks are for cases where you need manual event bridging
19
+ * (e.g., React 18, non-Vite builds, or dynamic event names):
20
+ *
21
+ * import { useWccEvent, useWccModel } from '@sprlab/wccompiler/adapters/react'
23
22
  */
24
23
 
25
- import React, { useRef, useEffect } from 'react'
24
+ import { useRef, useEffect } from 'react'
26
25
 
27
26
  /**
28
27
  * Hook that attaches a CustomEvent listener to a DOM element via ref.
@@ -35,6 +34,16 @@ import React, { useRef, useEffect } from 'react'
35
34
  * @param {string | ((event: CustomEvent) => void)} eventNameOrHandler
36
35
  * @param {((event: CustomEvent) => void)} [handler]
37
36
  * @returns {import('react').RefObject<HTMLElement> | void}
37
+ *
38
+ * @example
39
+ * // Form 1: Let the hook create the ref
40
+ * const ref = useWccEvent('count-changed', (e) => setCount(e.detail))
41
+ * <wcc-counter ref={ref}></wcc-counter>
42
+ *
43
+ * // Form 2: Pass an existing ref
44
+ * const myRef = useRef(null)
45
+ * useWccEvent(myRef, 'count-changed', (e) => setCount(e.detail))
46
+ * <wcc-counter ref={myRef}></wcc-counter>
38
47
  */
39
48
  export function useWccEvent(refOrEventName, eventNameOrHandler, handler) {
40
49
  const isRefForm = typeof refOrEventName !== 'string'
@@ -103,258 +112,3 @@ export function useWccModel(propName, value, setValue, existingRef) {
103
112
 
104
113
  return elementRef
105
114
  }
106
-
107
-
108
- /**
109
- * Converts a kebab-case event name to a React-idiomatic prop name.
110
- *
111
- * Rules:
112
- * - 'change' → 'onChange'
113
- * - 'count-changed' → 'onCountChange' (strips trailing 'd' from past tense)
114
- * - 'value-updated' → 'onValueUpdate' (strips trailing 'd')
115
- * - 'reset' → 'onReset'
116
- * - 'item-click' → 'onItemClick'
117
- *
118
- * @param {string} eventName - kebab-case event name
119
- * @returns {string} React prop name (onCamelCase)
120
- */
121
- function toReactEventProp(eventName) {
122
- const parts = eventName.split('-')
123
- const camel = parts.map(s => s[0].toUpperCase() + s.slice(1)).join('')
124
- // Strip trailing 'd' from past tense verbs (changed→Change, updated→Update)
125
- const normalized = camel.replace(/(Changed|Updated|Removed|Added|Closed|Opened|Submitted|Cancelled)$/, (m) => m.slice(0, -1))
126
- return 'on' + normalized
127
- }
128
-
129
- /**
130
- * Creates a React wrapper component for a WCC custom element.
131
- *
132
- * The wrapper provides idiomatic React DX:
133
- * - Event props: `onChange`, `onCountChange` → automatically wired via addEventListener
134
- * Handlers receive the unwrapped value (event.detail), not the raw CustomEvent.
135
- * - Model props: two-way binding via attribute + event listener
136
- * - Regular props: passed as attributes on the custom element
137
- * - Children: passed through as-is (use `<div slot="name">` for named slots)
138
- * - Ref forwarding: supports React refs via forwardRef
139
- *
140
- * @param {string} tagName - The custom element tag name (e.g., 'wcc-card')
141
- * @param {Object} [config] - Configuration for the wrapper
142
- * @param {string[]} [config.events] - Custom event names to expose as onEventName props
143
- * Event names are converted: 'count-changed' → onCountChange prop (React convention)
144
- * @param {string[]} [config.models] - Model prop names for two-way binding
145
- * Each model 'name' creates: `name` prop (sets attribute) + `onNameChange` event
146
- * @returns {import('react').ForwardRefExoticComponent} A React component
147
- *
148
- * @example
149
- * const WccCounter = createWccWrapper('wcc-counter', {
150
- * events: ['change'],
151
- * models: ['count']
152
- * })
153
- *
154
- * function App() {
155
- * const [count, setCount] = useState(0)
156
- * return (
157
- * <WccCounter
158
- * count={count}
159
- * onCountChange={(value) => setCount(value)}
160
- * onChange={(value) => console.log('changed', value)}
161
- * label="Clicks"
162
- * >
163
- * <div slot="footer">Footer content</div>
164
- * </WccCounter>
165
- * )
166
- * }
167
- */
168
- export function createWccWrapper(tagName, config = {}) {
169
- const { events = [], models = [] } = config
170
-
171
- // Build a set of event prop names for quick lookup
172
- // Convention: kebab-case event → React onCamelCase (without trailing 'd' from 'changed')
173
- // 'count-changed' → 'onCountChange' (not 'onCountChanged')
174
- // 'change' → 'onChange'
175
- // 'value-updated' → 'onValueUpdate' (strips trailing 'd' from past tense)
176
- const eventPropMap = new Map()
177
- for (const eventName of events) {
178
- const propName = toReactEventProp(eventName)
179
- eventPropMap.set(propName, eventName)
180
- }
181
-
182
- // Model events: 'count' → 'count-changed' → 'onCountChange'
183
- const modelEventMap = new Map()
184
- for (const modelName of models) {
185
- const eventName = `${modelName}-changed`
186
- const propName = toReactEventProp(eventName)
187
- eventPropMap.set(propName, eventName)
188
- modelEventMap.set(modelName, eventName)
189
- }
190
-
191
- // Reserved prop names that should not be passed as attributes
192
- const SKIP_PROPS = new Set(['children', 'key', 'ref', 'style', 'className', 'dangerouslySetInnerHTML'])
193
-
194
- const WccWrapper = React.forwardRef(function WccWrapper(props, externalRef) {
195
- const internalRef = useRef(null)
196
- const ref = externalRef || internalRef
197
-
198
- // Store event handlers in a ref to avoid re-subscribing on every render
199
- const handlersRef = useRef({})
200
-
201
- // Collect event handlers and regular props
202
- const regularProps = {}
203
- const eventHandlers = {}
204
-
205
- for (const [key, value] of Object.entries(props)) {
206
- if (SKIP_PROPS.has(key)) continue
207
-
208
- if (eventPropMap.has(key)) {
209
- eventHandlers[eventPropMap.get(key)] = value
210
- } else if (key.startsWith('on') && key.length > 2 && key[2] >= 'A' && key[2] <= 'Z') {
211
- // Generic React event handler pattern: onClick, onFocus, etc.
212
- // Convert onSomething → 'something' (lowercase first char)
213
- const nativeEvent = key[2].toLowerCase() + key.slice(3)
214
- eventHandlers[nativeEvent] = value
215
- } else {
216
- regularProps[key] = value
217
- }
218
- }
219
-
220
- // Update handlers ref
221
- handlersRef.current = eventHandlers
222
-
223
- // Subscribe to custom events
224
- useEffect(() => {
225
- const el = typeof ref === 'function' ? null : ref?.current
226
- if (!el) return
227
-
228
- const listeners = []
229
- const allEvents = new Set([...eventPropMap.values(), ...Object.keys(eventHandlers)])
230
-
231
- for (const eventName of allEvents) {
232
- const listener = (e) => {
233
- const handler = handlersRef.current[eventName]
234
- if (handler) handler(e instanceof CustomEvent ? e.detail : e)
235
- }
236
- el.addEventListener(eventName, listener)
237
- listeners.push([eventName, listener])
238
- }
239
-
240
- return () => {
241
- for (const [name, listener] of listeners) {
242
- el.removeEventListener(name, listener)
243
- }
244
- }
245
- }, []) // eslint-disable-line react-hooks/exhaustive-deps
246
-
247
- // Sync regular props as attributes
248
- useEffect(() => {
249
- const el = typeof ref === 'function' ? null : ref?.current
250
- if (!el) return
251
-
252
- for (const [key, value] of Object.entries(regularProps)) {
253
- if (value == null || value === false) {
254
- el.removeAttribute(key)
255
- } else if (value === true) {
256
- el.setAttribute(key, '')
257
- } else {
258
- el.setAttribute(key, String(value))
259
- }
260
- }
261
- })
262
-
263
- // Build the element props for React's createElement
264
- const elementProps = { ref }
265
- if (props.style) elementProps.style = props.style
266
- if (props.className) elementProps.className = props.className
267
-
268
- return React.createElement(tagName, elementProps, props.children)
269
- })
270
-
271
- WccWrapper.displayName = tagName.split('-').map(s => s[0].toUpperCase() + s.slice(1)).join('')
272
-
273
- return WccWrapper
274
- }
275
-
276
-
277
-
278
- /**
279
- * Creates a React wrapper from a WCC component class that has `static __meta`.
280
- *
281
- * Unlike `createWccWrapper` which requires manual event/model configuration,
282
- * this function reads the metadata directly from the compiled component class.
283
- *
284
- * @param {Function} WccClass - The WCC custom element class (must have static __meta)
285
- * @returns {import('react').ForwardRefExoticComponent} A React component
286
- *
287
- * @example
288
- * import { wrapWccComponent } from '@sprlab/wccompiler/adapters/react'
289
- * import '../wcc-components/wcc-counter.js' // registers the custom element
290
- *
291
- * // Read metadata directly from the registered class
292
- * const WccCounter = wrapWccComponent(customElements.get('wcc-counter'))
293
- *
294
- * // Use idiomatically — no manual config needed
295
- * // Handlers receive the value directly (not the event)
296
- * <WccCounter count={count} onCountChange={setCount} onChange={(val) => console.log(val)} />
297
- */
298
- export function wrapWccComponent(WccClass) {
299
- const meta = WccClass?.__meta
300
- if (!meta) {
301
- throw new Error(`wrapWccComponent: class does not have static __meta. Is it a compiled WCC component?`)
302
- }
303
-
304
- return createWccWrapper(meta.tag, {
305
- events: meta.events || [],
306
- models: meta.models || [],
307
- })
308
- }
309
-
310
- /**
311
- * Creates React wrappers for all registered WCC custom elements matching a prefix.
312
- *
313
- * Scans the custom elements registry for components with `static __meta` and
314
- * generates typed wrapper components for each one.
315
- *
316
- * @param {Object} [options]
317
- * @param {string} [options.prefix='wcc-'] - Tag prefix to filter components
318
- * @returns {Record<string, import('react').ForwardRefExoticComponent>} Map of PascalCase name → React component
319
- *
320
- * @example
321
- * // In your app entry point, after importing all WCC components:
322
- * import '../wcc-components/wcc-counter.js'
323
- * import '../wcc-components/wcc-card.js'
324
- * import { createWccWrappers } from '@sprlab/wccompiler/adapters/react'
325
- *
326
- * export const { WccCounter, WccCard } = createWccWrappers()
327
- *
328
- * // Then use anywhere:
329
- * <WccCounter count={count} onCountChanged={setCount} />
330
- * <WccCard><div slot="header">Title</div></WccCard>
331
- */
332
- export function createWccWrappers(options = {}) {
333
- const { prefix = 'wcc-' } = options
334
- const wrappers = {}
335
-
336
- // Note: customElements registry doesn't have a list API,
337
- // so we need the component files to be imported first (which registers them).
338
- // This function is meant to be called after all component imports.
339
-
340
- // We'll use a Proxy that lazily creates wrappers on first access
341
- return new Proxy(wrappers, {
342
- get(target, prop) {
343
- if (typeof prop !== 'string') return undefined
344
- if (prop in target) return target[prop]
345
-
346
- // Convert PascalCase to kebab-case: WccCounter → wcc-counter
347
- const kebab = prop.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '')
348
-
349
- // Check if it starts with the prefix
350
- if (!kebab.startsWith(prefix)) return undefined
351
-
352
- const ctor = customElements.get(kebab)
353
- if (!ctor || !(ctor).__meta) return undefined
354
-
355
- const wrapper = wrapWccComponent(ctor)
356
- target[prop] = wrapper
357
- return wrapper
358
- }
359
- })
360
- }
@@ -643,9 +643,102 @@ export function wccReactPlugin(options = {}) {
643
643
  const openingElement = path.node.openingElement
644
644
  const nameNode = openingElement.name
645
645
 
646
+ // ── Compound component transform ──
647
+ // <WccCard.Header>children</WccCard.Header> → <div slot="header" style={{display:'contents'}}>children</div>
648
+ // <WccCard.Stats>{(likes) => <span>{likes}</span>}</WccCard.Stats> → scoped slot div
649
+ if (nameNode.type === 'JSXMemberExpression') {
650
+ const objectName = nameNode.object?.name // e.g., 'WccCard'
651
+ const propName = nameNode.property?.name // e.g., 'Header'
652
+ if (!objectName || !propName) return
653
+
654
+ // Convert PascalCase object to kebab-case and check if it's a custom element
655
+ const kebab = objectName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '')
656
+ if (!kebab.includes('-')) return
657
+ if (prefix && !kebab.startsWith(prefix)) return
658
+
659
+ // Derive slot name: Header → header, FooterNav → footerNav (lcfirst)
660
+ const slotName = propName[0].toLowerCase() + propName.slice(1)
661
+
662
+ // Check if children is a function (scoped slot / render prop pattern)
663
+ const children = path.node.children
664
+ const isScopedSlot = children.length === 1
665
+ && children[0].type === 'JSXExpressionContainer'
666
+ && children[0].expression.type === 'ArrowFunctionExpression'
667
+
668
+ if (isScopedSlot) {
669
+ // Scoped slot: <WccCard.Stats>{(likes) => <span>{likes}</span>}</WccCard.Stats>
670
+ const arrowFn = children[0].expression
671
+ const params = (arrowFn.params || []).map(p => p.name || '')
672
+ const body = arrowFn.body
673
+
674
+ // Warn on unsupported expressions
675
+ const renderWarnings = []
676
+ serializeJsxToHtml(body, params, renderWarnings)
677
+ if (renderWarnings.length > 0) {
678
+ pluginCtx.warn(`[wcc-react] ${id} — ${objectName}.${propName}: ${renderWarnings[0]}`)
679
+ return
680
+ }
681
+
682
+ // Replace with scoped slot element
683
+ const scopedEl = generateScopedSlotElement(slotName, params, body)
684
+ path.replaceWith(scopedEl)
685
+ transformed = true
686
+ } else {
687
+ // Named slot: <WccCard.Header><strong>Title</strong></WccCard.Header>
688
+ // → <div slot="header" style={{display:'contents'}}>children</div>
689
+ const slotAttr = {
690
+ type: 'JSXAttribute',
691
+ name: { type: 'JSXIdentifier', name: 'slot' },
692
+ value: { type: 'StringLiteral', value: slotName }
693
+ }
694
+ const styleAttr = {
695
+ type: 'JSXAttribute',
696
+ name: { type: 'JSXIdentifier', name: 'style' },
697
+ value: {
698
+ type: 'JSXExpressionContainer',
699
+ expression: {
700
+ type: 'ObjectExpression',
701
+ properties: [{
702
+ type: 'ObjectProperty',
703
+ key: { type: 'Identifier', name: 'display' },
704
+ value: { type: 'StringLiteral', value: 'contents' },
705
+ computed: false,
706
+ shorthand: false
707
+ }]
708
+ }
709
+ }
710
+ }
711
+
712
+ openingElement.name = { type: 'JSXIdentifier', name: 'div' }
713
+ openingElement.attributes = [...openingElement.attributes, slotAttr, styleAttr]
714
+ if (path.node.closingElement) {
715
+ path.node.closingElement.name = { type: 'JSXIdentifier', name: 'div' }
716
+ }
717
+ transformed = true
718
+ }
719
+ return
720
+ }
721
+
722
+ // ── PascalCase custom element transform ──
723
+ // <WccCard> → <wcc-card> (only if it maps to a hyphenated tag)
724
+ if (nameNode.type === 'JSXIdentifier' && /^[A-Z]/.test(nameNode.name)) {
725
+ const kebab = nameNode.name.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '')
726
+ if (!kebab.includes('-')) return
727
+ if (prefix && !kebab.startsWith(prefix)) return
728
+
729
+ // Rewrite tag name to kebab-case
730
+ openingElement.name = { type: 'JSXIdentifier', name: kebab }
731
+ if (path.node.closingElement) {
732
+ path.node.closingElement.name = { type: 'JSXIdentifier', name: kebab }
733
+ }
734
+ transformed = true
735
+ // Fall through to process props on this element
736
+ }
737
+
646
738
  // Only process elements with hyphenated tag names (custom elements)
647
- if (nameNode.type !== 'JSXIdentifier') return
648
- const tagName = nameNode.name
739
+ const currentName = openingElement.name
740
+ if (currentName.type !== 'JSXIdentifier') return
741
+ const tagName = currentName.name
649
742
  if (!tagName.includes('-')) return
650
743
 
651
744
  // Apply prefix filtering if set
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.10.5",
3
+ "version": "0.10.7",
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": {