Skip to content

Commit adf2136

Browse files
serpentbladeclaude
andcommitted
chore(fullcalendar): regen 6 leaves + READMEs for merged plugins + noEventsContent slot (10)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2200043 commit adf2136

13 files changed

Lines changed: 357 additions & 78 deletions

File tree

packages/ui/fullcalendar/packages/angular/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,4 @@ export class DemoComponent {
104104
| moreLink | arg |
105105
| allDayContent | arg |
106106
| slotLaneContent | arg |
107+
| noEventsContent | arg |

packages/ui/fullcalendar/packages/angular/src/FullCalendar.ts

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ interface SlotLaneContentCtx {
5252
arg: any;
5353
}
5454

55+
interface NoEventsContentCtx {
56+
$implicit: { arg: any };
57+
arg: any;
58+
}
59+
5560
@Component({
5661
selector: 'rozie-full-calendar',
5762
standalone: true,
@@ -69,6 +74,7 @@ interface SlotLaneContentCtx {
6974
7075
7176
77+
7278
<ng-container #rozie_portalAnchor></ng-container>
7379
`,
7480
styles: [`
@@ -125,6 +131,7 @@ export class FullCalendar {
125131
@ContentChild('moreLink', { read: TemplateRef }) moreLinkTpl?: TemplateRef<MoreLinkCtx>;
126132
@ContentChild('allDayContent', { read: TemplateRef }) allDayContentTpl?: TemplateRef<AllDayContentCtx>;
127133
@ContentChild('slotLaneContent', { read: TemplateRef }) slotLaneContentTpl?: TemplateRef<SlotLaneContentCtx>;
134+
@ContentChild('noEventsContent', { read: TemplateRef }) noEventsContentTpl?: TemplateRef<NoEventsContentCtx>;
128135
templates = input<Record<string, TemplateRef<unknown>> | undefined>(undefined);
129136
private _portalViews = new Set<EmbeddedViewRef<unknown>>();
130137
private _portalAnchor = viewChild('rozie_portalAnchor', { read: ViewContainerRef });
@@ -137,6 +144,7 @@ export class FullCalendar {
137144
private _moreLinkTpl = contentChild('moreLink', { read: TemplateRef });
138145
private _allDayContentTpl = contentChild('allDayContent', { read: TemplateRef });
139146
private _slotLaneContentTpl = contentChild('slotLaneContent', { read: TemplateRef });
147+
private _noEventsContentTpl = contentChild('noEventsContent', { read: TemplateRef });
140148
private __rozieDestroyRef = inject(DestroyRef);
141149
private __rozieWatchInitial_0 = true;
142150
private __rozieWatchInitial_1 = true;
@@ -315,13 +323,38 @@ export class FullCalendar {
315323
this._portalViews.delete(view as EmbeddedViewRef<unknown>);
316324
};
317325
},
326+
noEventsContent: (container: HTMLElement, scope: { arg: unknown }): (() => void) => {
327+
const tpl = this._noEventsContentTpl();
328+
const vcr = this._portalAnchor();
329+
if (!tpl || !vcr) return () => {};
330+
// Spike 004: portal-scope attribute injection.
331+
container.setAttribute('data-rozie-portal-noEventsContent', '5589629a');
332+
const view = vcr.createEmbeddedView(tpl, scope as unknown as Record<string, unknown>);
333+
view.detectChanges();
334+
for (const node of view.rootNodes as Node[]) container.appendChild(node);
335+
this._portalViews.add(view as EmbeddedViewRef<unknown>);
336+
return () => {
337+
view.destroy();
338+
this._portalViews.delete(view as EmbeddedViewRef<unknown>);
339+
};
340+
},
318341
};
342+
const __options = this.options();
319343
const opts = {
320344
// :options passthrough spread FIRST — the curated keys below + the portal
321345
// *Content handlers added after this object override any colliding key, so
322346
// an explicitly-bound prop (e.g. :height) wins over options.height.
323-
...this.options(),
324-
plugins: this.PLUGINS,
347+
//
348+
// EXCEPTION — `plugins` is the one curated key that AUGMENTS rather than
349+
// overrides: instead of clobbering a consumer-supplied `:options.plugins`,
350+
// it MERGES the always-on baked-in defaults (dayGrid + timeGrid +
351+
// interaction) with any consumer-added plugins. This makes the wrapper
352+
// consumer-extensible (opt-in) — a consumer can engage list/rrule/premium/
353+
// etc. via `:options="{ plugins: [listPlugin] }"` with NO bundle cost and NO
354+
// per-plugin wrapper code. FullCalendar dedupes plugins by identity, so a
355+
// consumer re-passing a default is harmless.
356+
...__options,
357+
plugins: [...this.PLUGINS, ...(__options?.plugins ?? [])],
325358
initialView: this.view(),
326359
weekends: this.weekends(),
327360
editable: this.editable(),
@@ -475,24 +508,24 @@ export class FullCalendar {
475508
};
476509
};
477510
}
478-
// The 8 remaining *Content portal-slots — wired identically to `event`, one
479-
// per FullCalendar per-cell content hook that fires with the bundled plugins
480-
// (core + daygrid + timegrid + interaction). Each guarded by its own slot so
481-
// unfilled slots keep FullCalendar's default rendering. (9 portal-slots total
511+
// The 9 remaining *Content portal-slots — wired identically to `event`, one
512+
// per FullCalendar per-cell content hook. Each guarded by its own slot so
513+
// unfilled slots keep FullCalendar's default rendering. (10 portal-slots total
482514
// counting `event` above; allDayContent + slotLaneContent are the two timeGrid
483-
// axis/lane hooks added last.)
515+
// axis/lane hooks, and noEventsContent is the list-view "no events" hook —
516+
// inert unless the consumer engages @fullcalendar/list via :options.plugins.)
484517
//
485518
// NOTE the `nowIndicatorContent` slot is named for its FullCalendar engine
486519
// hook (`nowIndicatorContent`) so it does NOT clash with the boolean
487520
// `nowIndicator` PROP — a slot name that equals a declared prop name is now a
488521
// hard compile error (ROZ127 SLOT_PROP_NAME_COLLISION), because Svelte 5
489522
// unifies snippets and props into one `$props` namespace.
490-
// The 8 remaining *Content portal-slots — wired identically to `event`, one
491-
// per FullCalendar per-cell content hook that fires with the bundled plugins
492-
// (core + daygrid + timegrid + interaction). Each guarded by its own slot so
493-
// unfilled slots keep FullCalendar's default rendering. (9 portal-slots total
523+
// The 9 remaining *Content portal-slots — wired identically to `event`, one
524+
// per FullCalendar per-cell content hook. Each guarded by its own slot so
525+
// unfilled slots keep FullCalendar's default rendering. (10 portal-slots total
494526
// counting `event` above; allDayContent + slotLaneContent are the two timeGrid
495-
// axis/lane hooks added last.)
527+
// axis/lane hooks, and noEventsContent is the list-view "no events" hook —
528+
// inert unless the consumer engages @fullcalendar/list via :options.plugins.)
496529
//
497530
// NOTE the `nowIndicatorContent` slot is named for its FullCalendar engine
498531
// hook (`nowIndicatorContent`) so it does NOT clash with the boolean
@@ -595,13 +628,30 @@ export class FullCalendar {
595628
};
596629
};
597630
}
598-
// The one still-excluded *Content slot (documented, not a gap): noEventsContent
599-
// — list-view only, and @fullcalendar/list is not a bundled engine peer. A
600-
// consumer needing it uses the :options passthrough + getApi().
601-
// The one still-excluded *Content slot (documented, not a gap): noEventsContent
602-
// — list-view only, and @fullcalendar/list is not a bundled engine peer. A
603-
// consumer needing it uses the :options passthrough + getApi().
604-
631+
// noEventsContent — the list-view "no events to display" hook. Pre-declared
632+
// and wired like the other 9 *Content slots, but INERT unless the consumer
633+
// (a) engages @fullcalendar/list via the now-merged :options.plugins AND
634+
// (b) shows a list view (listWeek/listDay/listMonth) with ZERO events. With
635+
// the bundled-only plugin set there is no list view, so this hook never fires
636+
// — by design, documented, zero bundle cost.
637+
// noEventsContent — the list-view "no events to display" hook. Pre-declared
638+
// and wired like the other 9 *Content slots, but INERT unless the consumer
639+
// (a) engages @fullcalendar/list via the now-merged :options.plugins AND
640+
// (b) shows a list view (listWeek/listDay/listMonth) with ZERO events. With
641+
// the bundled-only plugin set there is no list view, so this hook never fires
642+
// — by design, documented, zero bundle cost.
643+
if ((this.noEventsContentTpl ?? this.templates()?.['noEventsContent'])) {
644+
opts.noEventsContent = (arg: any) => {
645+
const node = document.createElement('div');
646+
const dispose = portals.noEventsContent(node, {
647+
arg
648+
});
649+
return {
650+
domNodes: [node],
651+
dispose
652+
};
653+
};
654+
}
605655
this.instance = new Calendar(this.__rozieRoot()!.nativeElement, opts);
606656
this.instance.render();
607657
this.__rozieDestroyRef.onDestroy(() => this.instance?.destroy());
@@ -672,7 +722,7 @@ export class FullCalendar {
672722
static ngTemplateContextGuard(
673723
_dir: FullCalendar,
674724
_ctx: unknown,
675-
): _ctx is EventCtx | DayCellCtx | DayHeaderCtx | SlotLabelCtx | WeekNumberCtx | NowIndicatorContentCtx | MoreLinkCtx | AllDayContentCtx | SlotLaneContentCtx {
725+
): _ctx is EventCtx | DayCellCtx | DayHeaderCtx | SlotLabelCtx | WeekNumberCtx | NowIndicatorContentCtx | MoreLinkCtx | AllDayContentCtx | SlotLaneContentCtx | NoEventsContentCtx {
676726
return true;
677727
}
678728
}

packages/ui/fullcalendar/packages/lit/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,4 @@ const api = el.getApi();
9898
| moreLink | arg |
9999
| allDayContent | arg |
100100
| slotLaneContent | arg |
101+
| noEventsContent | arg |

packages/ui/fullcalendar/packages/lit/src/FullCalendar.ts

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ interface RozieSlotLaneContentSlotCtx {
4343
arg: unknown;
4444
}
4545

46+
interface RozieNoEventsContentSlotCtx {
47+
arg: unknown;
48+
}
49+
4650
@customElement('rozie-full-calendar')
4751
export 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

packages/ui/fullcalendar/packages/react/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,4 @@ const api = cal.current?.getApi();
102102
| moreLink | arg |
103103
| allDayContent | arg |
104104
| slotLaneContent | arg |
105+
| noEventsContent | arg |

packages/ui/fullcalendar/packages/react/src/FullCalendar.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface FullCalendarProps {
3838
renderMoreLink?: (params: { arg: () => void }) => ReactNode;
3939
renderAllDayContent?: (params: { arg: () => void }) => ReactNode;
4040
renderSlotLaneContent?: (params: { arg: () => void }) => ReactNode;
41+
renderNoEventsContent?: (params: { arg: () => void }) => ReactNode;
4142
slots?: Record<string, () => ReactNode>;
4243
}
4344

0 commit comments

Comments
 (0)