@@ -43,6 +43,10 @@ interface RozieSlotLaneContentSlotCtx {
4343 arg : unknown ;
4444}
4545
46+ interface RozieNoEventsContentSlotCtx {
47+ arg : unknown ;
48+ }
49+
4650@customElement ( 'rozie-full-calendar' )
4751export default class FullCalendar extends SignalWatcher ( LitElement ) {
4852 static styles = css `
@@ -102,6 +106,9 @@ private _portalContainers = new Set<HTMLElement>();
102106 @state ( ) private _hasSlotSlotLaneContent = false ;
103107 @queryAssignedElements ( { slot : 'slotLaneContent' , flatten : true } ) private _slotSlotLaneContentElements ! : Element [ ] ;
104108 @property ( { attribute : false } ) slotLaneContent ?: ( scope : { arg : unknown } ) => unknown ;
109+ @state ( ) private _hasSlotNoEventsContent = false ;
110+ @queryAssignedElements ( { slot : 'noEventsContent' , flatten : true } ) private _slotNoEventsContentElements ! : Element [ ] ;
111+ @property ( { attribute : false } ) noEventsContent ?: ( scope : { arg : unknown } ) => unknown ;
105112
106113 private _disconnectCleanups : Array < ( ) => void > = [ ] ;
107114 // Re-parenting guard: set true once the deferred teardown has actually
@@ -207,6 +214,17 @@ private _portalContainers = new Set<HTMLElement>();
207214 update ( ) ;
208215 }
209216 }
217+
218+ {
219+ const slotEl = this . shadowRoot ?. querySelector ( 'slot[name="noEventsContent"]' ) ;
220+ if ( slotEl !== null && slotEl !== undefined ) {
221+ const update = ( ) => { this . _hasSlotNoEventsContent = this . _slotNoEventsContentElements . length > 0 ; } ;
222+ slotEl . addEventListener ( 'slotchange' , update ) ;
223+ // CR-05 fix: push cleanup so the listener is removed on disconnectedCallback.
224+ this . _disconnectCleanups . push ( ( ) => slotEl . removeEventListener ( 'slotchange' , update ) ) ;
225+ update ( ) ;
226+ }
227+ }
210228 }
211229
212230 connectedCallback ( ) : void {
@@ -220,6 +238,7 @@ private _portalContainers = new Set<HTMLElement>();
220238 this . _hasSlotMoreLink = Array . from ( this . children ) . some ( ( el ) => el . getAttribute ( 'slot' ) === 'moreLink' ) ;
221239 this . _hasSlotAllDayContent = Array . from ( this . children ) . some ( ( el ) => el . getAttribute ( 'slot' ) === 'allDayContent' ) ;
222240 this . _hasSlotSlotLaneContent = Array . from ( this . children ) . some ( ( el ) => el . getAttribute ( 'slot' ) === 'slotLaneContent' ) ;
241+ this . _hasSlotNoEventsContent = Array . from ( this . children ) . some ( ( el ) => el . getAttribute ( 'slot' ) === 'noEventsContent' ) ;
223242 super . connectedCallback ( ) ;
224243 if ( this . hasUpdated && this . _rozieTornDown ) { this . _rozieTornDown = false ; this . _armListeners ( ) ; }
225244 }
@@ -336,6 +355,18 @@ private _portalContainers = new Set<HTMLElement>();
336355 this . _portalContainers . delete ( container ) ;
337356 } ;
338357 } ,
358+ noEventsContent : ( container : HTMLElement , scope : { arg : unknown } ) : ( ( ) => void ) => {
359+ const tpl = this . noEventsContent ;
360+ if ( typeof tpl !== 'function' ) return ( ) => { } ;
361+ // Spike 004: portal-scope attribute injection.
362+ container . setAttribute ( 'data-rozie-portal-noEventsContent' , '5589629a' ) ;
363+ render ( tpl ( scope ) , container ) ;
364+ this . _portalContainers . add ( container ) ;
365+ return ( ) => {
366+ render ( nothing , container ) ;
367+ this . _portalContainers . delete ( container ) ;
368+ } ;
369+ } ,
339370 } ;
340371
341372 this . _disconnectCleanups . push ( ( ( ) => this . instance ?. destroy ( ) ) ) ;
@@ -351,8 +382,17 @@ private _portalContainers = new Set<HTMLElement>();
351382 // :options passthrough spread FIRST — the curated keys below + the portal
352383 // *Content handlers added after this object override any colliding key, so
353384 // an explicitly-bound prop (e.g. :height) wins over options.height.
385+ //
386+ // EXCEPTION — `plugins` is the one curated key that AUGMENTS rather than
387+ // overrides: instead of clobbering a consumer-supplied `:options.plugins`,
388+ // it MERGES the always-on baked-in defaults (dayGrid + timeGrid +
389+ // interaction) with any consumer-added plugins. This makes the wrapper
390+ // consumer-extensible (opt-in) — a consumer can engage list/rrule/premium/
391+ // etc. via `:options="{ plugins: [listPlugin] }"` with NO bundle cost and NO
392+ // per-plugin wrapper code. FullCalendar dedupes plugins by identity, so a
393+ // consumer re-passing a default is harmless.
354394 ...this . options ,
355- plugins : this . PLUGINS ,
395+ plugins : [ ... this . PLUGINS , ... ( this . options ?. plugins ?? [ ] ) ] ,
356396 initialView : this . view ,
357397 weekends : this . weekends ,
358398 editable : this . editable ,
@@ -550,24 +590,24 @@ private _portalContainers = new Set<HTMLElement>();
550590 } ;
551591 } ;
552592 }
553- // The 8 remaining *Content portal-slots — wired identically to `event`, one
554- // per FullCalendar per-cell content hook that fires with the bundled plugins
555- // (core + daygrid + timegrid + interaction). Each guarded by its own slot so
556- // unfilled slots keep FullCalendar's default rendering. (9 portal-slots total
593+ // The 9 remaining *Content portal-slots — wired identically to `event`, one
594+ // per FullCalendar per-cell content hook. Each guarded by its own slot so
595+ // unfilled slots keep FullCalendar's default rendering. (10 portal-slots total
557596 // counting `event` above; allDayContent + slotLaneContent are the two timeGrid
558- // axis/lane hooks added last.)
597+ // axis/lane hooks, and noEventsContent is the list-view "no events" hook —
598+ // inert unless the consumer engages @fullcalendar /list via :options.plugins.)
559599 //
560600 // NOTE the `nowIndicatorContent` slot is named for its FullCalendar engine
561601 // hook (`nowIndicatorContent`) so it does NOT clash with the boolean
562602 // `nowIndicator` PROP — a slot name that equals a declared prop name is now a
563603 // hard compile error (ROZ127 SLOT_PROP_NAME_COLLISION), because Svelte 5
564604 // unifies snippets and props into one `$props` namespace.
565- // The 8 remaining *Content portal-slots — wired identically to `event`, one
566- // per FullCalendar per-cell content hook that fires with the bundled plugins
567- // (core + daygrid + timegrid + interaction). Each guarded by its own slot so
568- // unfilled slots keep FullCalendar's default rendering. (9 portal-slots total
605+ // The 9 remaining *Content portal-slots — wired identically to `event`, one
606+ // per FullCalendar per-cell content hook. Each guarded by its own slot so
607+ // unfilled slots keep FullCalendar's default rendering. (10 portal-slots total
569608 // counting `event` above; allDayContent + slotLaneContent are the two timeGrid
570- // axis/lane hooks added last.)
609+ // axis/lane hooks, and noEventsContent is the list-view "no events" hook —
610+ // inert unless the consumer engages @fullcalendar /list via :options.plugins.)
571611 //
572612 // NOTE the `nowIndicatorContent` slot is named for its FullCalendar engine
573613 // hook (`nowIndicatorContent`) so it does NOT clash with the boolean
@@ -670,13 +710,30 @@ private _portalContainers = new Set<HTMLElement>();
670710 } ;
671711 } ;
672712 }
673- // The one still-excluded *Content slot (documented, not a gap): noEventsContent
674- // — list-view only, and @fullcalendar/list is not a bundled engine peer. A
675- // consumer needing it uses the :options passthrough + getApi().
676- // The one still-excluded *Content slot (documented, not a gap): noEventsContent
677- // — list-view only, and @fullcalendar/list is not a bundled engine peer. A
678- // consumer needing it uses the :options passthrough + getApi().
679-
713+ // noEventsContent — the list-view "no events to display" hook. Pre-declared
714+ // and wired like the other 9 *Content slots, but INERT unless the consumer
715+ // (a) engages @fullcalendar/list via the now-merged :options.plugins AND
716+ // (b) shows a list view (listWeek/listDay/listMonth) with ZERO events. With
717+ // the bundled-only plugin set there is no list view, so this hook never fires
718+ // — by design, documented, zero bundle cost.
719+ // noEventsContent — the list-view "no events to display" hook. Pre-declared
720+ // and wired like the other 9 *Content slots, but INERT unless the consumer
721+ // (a) engages @fullcalendar/list via the now-merged :options.plugins AND
722+ // (b) shows a list view (listWeek/listDay/listMonth) with ZERO events. With
723+ // the bundled-only plugin set there is no list view, so this hook never fires
724+ // — by design, documented, zero bundle cost.
725+ if ( this . noEventsContent !== undefined ) {
726+ opts . noEventsContent = ( arg : any ) => {
727+ const node = document . createElement ( 'div' ) ;
728+ const dispose = portals . noEventsContent ( node , {
729+ arg
730+ } ) ;
731+ return {
732+ domNodes : [ node ] ,
733+ dispose
734+ } ;
735+ } ;
736+ }
680737 this . instance = new Calendar ( this . _ref__rozieRoot , opts ) ;
681738 this . instance . render ( ) ;
682739 }
@@ -733,6 +790,7 @@ private _portalContainers = new Set<HTMLElement>();
733790< slot name ="moreLink "> </ slot >
734791< slot name ="allDayContent "> </ slot >
735792< slot name ="slotLaneContent "> </ slot >
793+ < slot name ="noEventsContent "> </ slot >
736794` ;
737795 }
738796
0 commit comments