Skip to content

Commit dad80f5

Browse files
serpentbladeclaude
andcommitted
chore(fullcalendar): regen 6 leaves + READMEs for allDayContent + slotLaneContent slots
node scripts/codegen.mjs — all 6 targets carry the 2 new slot members (React render-prop renderAllDayContent/renderSlotLaneContent in .tsx + .d.ts; Vue/Svelte/Angular/Solid/Lit slot/snippet/template/bridge forms). Props-table validation PASS (13 rows, no drift). No collisions — names are unique. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 44a3238 commit dad80f5

13 files changed

Lines changed: 469 additions & 49 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,5 @@ export class DemoComponent {
102102
| weekNumber | arg |
103103
| nowIndicatorContent | arg |
104104
| moreLink | arg |
105+
| allDayContent | arg |
106+
| slotLaneContent | arg |

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

Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ interface MoreLinkCtx {
4242
arg: any;
4343
}
4444

45+
interface AllDayContentCtx {
46+
$implicit: { arg: any };
47+
arg: any;
48+
}
49+
50+
interface SlotLaneContentCtx {
51+
$implicit: { arg: any };
52+
arg: any;
53+
}
54+
4555
@Component({
4656
selector: 'rozie-full-calendar',
4757
standalone: true,
@@ -57,6 +67,8 @@ interface MoreLinkCtx {
5767
5868
5969
70+
71+
6072
<ng-container #rozie_portalAnchor></ng-container>
6173
`,
6274
styles: [`
@@ -111,6 +123,8 @@ export class FullCalendar {
111123
@ContentChild('weekNumber', { read: TemplateRef }) weekNumberTpl?: TemplateRef<WeekNumberCtx>;
112124
@ContentChild('nowIndicatorContent', { read: TemplateRef }) nowIndicatorContentTpl?: TemplateRef<NowIndicatorContentCtx>;
113125
@ContentChild('moreLink', { read: TemplateRef }) moreLinkTpl?: TemplateRef<MoreLinkCtx>;
126+
@ContentChild('allDayContent', { read: TemplateRef }) allDayContentTpl?: TemplateRef<AllDayContentCtx>;
127+
@ContentChild('slotLaneContent', { read: TemplateRef }) slotLaneContentTpl?: TemplateRef<SlotLaneContentCtx>;
114128
templates = input<Record<string, TemplateRef<unknown>> | undefined>(undefined);
115129
private _portalViews = new Set<EmbeddedViewRef<unknown>>();
116130
private _portalAnchor = viewChild('rozie_portalAnchor', { read: ViewContainerRef });
@@ -121,6 +135,8 @@ export class FullCalendar {
121135
private _weekNumberTpl = contentChild('weekNumber', { read: TemplateRef });
122136
private _nowIndicatorContentTpl = contentChild('nowIndicatorContent', { read: TemplateRef });
123137
private _moreLinkTpl = contentChild('moreLink', { read: TemplateRef });
138+
private _allDayContentTpl = contentChild('allDayContent', { read: TemplateRef });
139+
private _slotLaneContentTpl = contentChild('slotLaneContent', { read: TemplateRef });
124140
private __rozieDestroyRef = inject(DestroyRef);
125141
private __rozieWatchInitial_0 = true;
126142
private __rozieWatchInitial_1 = true;
@@ -269,6 +285,36 @@ export class FullCalendar {
269285
this._portalViews.delete(view as EmbeddedViewRef<unknown>);
270286
};
271287
},
288+
allDayContent: (container: HTMLElement, scope: { arg: unknown }): (() => void) => {
289+
const tpl = this._allDayContentTpl();
290+
const vcr = this._portalAnchor();
291+
if (!tpl || !vcr) return () => {};
292+
// Spike 004: portal-scope attribute injection.
293+
container.setAttribute('data-rozie-portal-allDayContent', '5589629a');
294+
const view = vcr.createEmbeddedView(tpl, scope as unknown as Record<string, unknown>);
295+
view.detectChanges();
296+
for (const node of view.rootNodes as Node[]) container.appendChild(node);
297+
this._portalViews.add(view as EmbeddedViewRef<unknown>);
298+
return () => {
299+
view.destroy();
300+
this._portalViews.delete(view as EmbeddedViewRef<unknown>);
301+
};
302+
},
303+
slotLaneContent: (container: HTMLElement, scope: { arg: unknown }): (() => void) => {
304+
const tpl = this._slotLaneContentTpl();
305+
const vcr = this._portalAnchor();
306+
if (!tpl || !vcr) return () => {};
307+
// Spike 004: portal-scope attribute injection.
308+
container.setAttribute('data-rozie-portal-slotLaneContent', '5589629a');
309+
const view = vcr.createEmbeddedView(tpl, scope as unknown as Record<string, unknown>);
310+
view.detectChanges();
311+
for (const node of view.rootNodes as Node[]) container.appendChild(node);
312+
this._portalViews.add(view as EmbeddedViewRef<unknown>);
313+
return () => {
314+
view.destroy();
315+
this._portalViews.delete(view as EmbeddedViewRef<unknown>);
316+
};
317+
},
272318
};
273319
const opts = {
274320
// :options passthrough spread FIRST — the curated keys below + the portal
@@ -429,20 +475,24 @@ export class FullCalendar {
429475
};
430476
};
431477
}
432-
// The 6 remaining *Content portal-slots — wired identically to `event`, one
478+
// The 8 remaining *Content portal-slots — wired identically to `event`, one
433479
// per FullCalendar per-cell content hook that fires with the bundled plugins
434480
// (core + daygrid + timegrid + interaction). Each guarded by its own slot so
435-
// unfilled slots keep FullCalendar's default rendering.
481+
// unfilled slots keep FullCalendar's default rendering. (9 portal-slots total
482+
// counting `event` above; allDayContent + slotLaneContent are the two timeGrid
483+
// axis/lane hooks added last.)
436484
//
437485
// NOTE the `nowIndicatorContent` slot is named for its FullCalendar engine
438486
// hook (`nowIndicatorContent`) so it does NOT clash with the boolean
439487
// `nowIndicator` PROP — a slot name that equals a declared prop name is now a
440488
// hard compile error (ROZ127 SLOT_PROP_NAME_COLLISION), because Svelte 5
441489
// unifies snippets and props into one `$props` namespace.
442-
// The 6 remaining *Content portal-slots — wired identically to `event`, one
490+
// The 8 remaining *Content portal-slots — wired identically to `event`, one
443491
// per FullCalendar per-cell content hook that fires with the bundled plugins
444492
// (core + daygrid + timegrid + interaction). Each guarded by its own slot so
445-
// unfilled slots keep FullCalendar's default rendering.
493+
// unfilled slots keep FullCalendar's default rendering. (9 portal-slots total
494+
// counting `event` above; allDayContent + slotLaneContent are the two timeGrid
495+
// axis/lane hooks added last.)
446496
//
447497
// NOTE the `nowIndicatorContent` slot is named for its FullCalendar engine
448498
// hook (`nowIndicatorContent`) so it does NOT clash with the boolean
@@ -521,14 +571,36 @@ export class FullCalendar {
521571
};
522572
};
523573
}
524-
// Excluded *Content slots (documented, not gaps): noEventsContent (list-view
525-
// only — @fullcalendar/list is not a bundled peer), slotLaneContent (background
526-
// lane, no demand), allDayContent (trivial label). Consumers needing those use
527-
// the :options passthrough + getApi().
528-
// Excluded *Content slots (documented, not gaps): noEventsContent (list-view
529-
// only — @fullcalendar/list is not a bundled peer), slotLaneContent (background
530-
// lane, no demand), allDayContent (trivial label). Consumers needing those use
531-
// the :options passthrough + getApi().
574+
if ((this.allDayContentTpl ?? this.templates()?.['allDayContent'])) {
575+
opts.allDayContent = (arg: any) => {
576+
const node = document.createElement('div');
577+
const dispose = portals.allDayContent(node, {
578+
arg
579+
});
580+
return {
581+
domNodes: [node],
582+
dispose
583+
};
584+
};
585+
}
586+
if ((this.slotLaneContentTpl ?? this.templates()?.['slotLaneContent'])) {
587+
opts.slotLaneContent = (arg: any) => {
588+
const node = document.createElement('div');
589+
const dispose = portals.slotLaneContent(node, {
590+
arg
591+
});
592+
return {
593+
domNodes: [node],
594+
dispose
595+
};
596+
};
597+
}
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().
532604

533605
this.instance = new Calendar(this.__rozieRoot()!.nativeElement, opts);
534606
this.instance.render();
@@ -600,7 +672,7 @@ export class FullCalendar {
600672
static ngTemplateContextGuard(
601673
_dir: FullCalendar,
602674
_ctx: unknown,
603-
): _ctx is EventCtx | DayCellCtx | DayHeaderCtx | SlotLabelCtx | WeekNumberCtx | NowIndicatorContentCtx | MoreLinkCtx {
675+
): _ctx is EventCtx | DayCellCtx | DayHeaderCtx | SlotLabelCtx | WeekNumberCtx | NowIndicatorContentCtx | MoreLinkCtx | AllDayContentCtx | SlotLaneContentCtx {
604676
return true;
605677
}
606678
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,5 @@ const api = el.getApi();
9696
| weekNumber | arg |
9797
| nowIndicatorContent | arg |
9898
| moreLink | arg |
99+
| allDayContent | arg |
100+
| slotLaneContent | arg |

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

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ interface RozieMoreLinkSlotCtx {
3535
arg: unknown;
3636
}
3737

38+
interface RozieAllDayContentSlotCtx {
39+
arg: unknown;
40+
}
41+
42+
interface RozieSlotLaneContentSlotCtx {
43+
arg: unknown;
44+
}
45+
3846
@customElement('rozie-full-calendar')
3947
export default class FullCalendar extends SignalWatcher(LitElement) {
4048
static styles = css`
@@ -88,6 +96,12 @@ private _portalContainers = new Set<HTMLElement>();
8896
@state() private _hasSlotMoreLink = false;
8997
@queryAssignedElements({ slot: 'moreLink', flatten: true }) private _slotMoreLinkElements!: Element[];
9098
@property({ attribute: false }) moreLink?: (scope: { arg: unknown }) => unknown;
99+
@state() private _hasSlotAllDayContent = false;
100+
@queryAssignedElements({ slot: 'allDayContent', flatten: true }) private _slotAllDayContentElements!: Element[];
101+
@property({ attribute: false }) allDayContent?: (scope: { arg: unknown }) => unknown;
102+
@state() private _hasSlotSlotLaneContent = false;
103+
@queryAssignedElements({ slot: 'slotLaneContent', flatten: true }) private _slotSlotLaneContentElements!: Element[];
104+
@property({ attribute: false }) slotLaneContent?: (scope: { arg: unknown }) => unknown;
91105

92106
private _disconnectCleanups: Array<() => void> = [];
93107
// Re-parenting guard: set true once the deferred teardown has actually
@@ -171,6 +185,28 @@ private _portalContainers = new Set<HTMLElement>();
171185
update();
172186
}
173187
}
188+
189+
{
190+
const slotEl = this.shadowRoot?.querySelector('slot[name="allDayContent"]');
191+
if (slotEl !== null && slotEl !== undefined) {
192+
const update = () => { this._hasSlotAllDayContent = this._slotAllDayContentElements.length > 0; };
193+
slotEl.addEventListener('slotchange', update);
194+
// CR-05 fix: push cleanup so the listener is removed on disconnectedCallback.
195+
this._disconnectCleanups.push(() => slotEl.removeEventListener('slotchange', update));
196+
update();
197+
}
198+
}
199+
200+
{
201+
const slotEl = this.shadowRoot?.querySelector('slot[name="slotLaneContent"]');
202+
if (slotEl !== null && slotEl !== undefined) {
203+
const update = () => { this._hasSlotSlotLaneContent = this._slotSlotLaneContentElements.length > 0; };
204+
slotEl.addEventListener('slotchange', update);
205+
// CR-05 fix: push cleanup so the listener is removed on disconnectedCallback.
206+
this._disconnectCleanups.push(() => slotEl.removeEventListener('slotchange', update));
207+
update();
208+
}
209+
}
174210
}
175211

176212
connectedCallback(): void {
@@ -182,6 +218,8 @@ private _portalContainers = new Set<HTMLElement>();
182218
this._hasSlotWeekNumber = Array.from(this.children).some((el) => el.getAttribute('slot') === 'weekNumber');
183219
this._hasSlotNowIndicatorContent = Array.from(this.children).some((el) => el.getAttribute('slot') === 'nowIndicatorContent');
184220
this._hasSlotMoreLink = Array.from(this.children).some((el) => el.getAttribute('slot') === 'moreLink');
221+
this._hasSlotAllDayContent = Array.from(this.children).some((el) => el.getAttribute('slot') === 'allDayContent');
222+
this._hasSlotSlotLaneContent = Array.from(this.children).some((el) => el.getAttribute('slot') === 'slotLaneContent');
185223
super.connectedCallback();
186224
if (this.hasUpdated && this._rozieTornDown) { this._rozieTornDown = false; this._armListeners(); }
187225
}
@@ -274,6 +312,30 @@ private _portalContainers = new Set<HTMLElement>();
274312
this._portalContainers.delete(container);
275313
};
276314
},
315+
allDayContent: (container: HTMLElement, scope: { arg: unknown }): (() => void) => {
316+
const tpl = this.allDayContent;
317+
if (typeof tpl !== 'function') return () => {};
318+
// Spike 004: portal-scope attribute injection.
319+
container.setAttribute('data-rozie-portal-allDayContent', '5589629a');
320+
render(tpl(scope), container);
321+
this._portalContainers.add(container);
322+
return () => {
323+
render(nothing, container);
324+
this._portalContainers.delete(container);
325+
};
326+
},
327+
slotLaneContent: (container: HTMLElement, scope: { arg: unknown }): (() => void) => {
328+
const tpl = this.slotLaneContent;
329+
if (typeof tpl !== 'function') return () => {};
330+
// Spike 004: portal-scope attribute injection.
331+
container.setAttribute('data-rozie-portal-slotLaneContent', '5589629a');
332+
render(tpl(scope), container);
333+
this._portalContainers.add(container);
334+
return () => {
335+
render(nothing, container);
336+
this._portalContainers.delete(container);
337+
};
338+
},
277339
};
278340

279341
this._disconnectCleanups.push((() => this.instance?.destroy()));
@@ -488,20 +550,24 @@ private _portalContainers = new Set<HTMLElement>();
488550
};
489551
};
490552
}
491-
// The 6 remaining *Content portal-slots — wired identically to `event`, one
553+
// The 8 remaining *Content portal-slots — wired identically to `event`, one
492554
// per FullCalendar per-cell content hook that fires with the bundled plugins
493555
// (core + daygrid + timegrid + interaction). Each guarded by its own slot so
494-
// unfilled slots keep FullCalendar's default rendering.
556+
// unfilled slots keep FullCalendar's default rendering. (9 portal-slots total
557+
// counting `event` above; allDayContent + slotLaneContent are the two timeGrid
558+
// axis/lane hooks added last.)
495559
//
496560
// NOTE the `nowIndicatorContent` slot is named for its FullCalendar engine
497561
// hook (`nowIndicatorContent`) so it does NOT clash with the boolean
498562
// `nowIndicator` PROP — a slot name that equals a declared prop name is now a
499563
// hard compile error (ROZ127 SLOT_PROP_NAME_COLLISION), because Svelte 5
500564
// unifies snippets and props into one `$props` namespace.
501-
// The 6 remaining *Content portal-slots — wired identically to `event`, one
565+
// The 8 remaining *Content portal-slots — wired identically to `event`, one
502566
// per FullCalendar per-cell content hook that fires with the bundled plugins
503567
// (core + daygrid + timegrid + interaction). Each guarded by its own slot so
504-
// unfilled slots keep FullCalendar's default rendering.
568+
// unfilled slots keep FullCalendar's default rendering. (9 portal-slots total
569+
// counting `event` above; allDayContent + slotLaneContent are the two timeGrid
570+
// axis/lane hooks added last.)
505571
//
506572
// NOTE the `nowIndicatorContent` slot is named for its FullCalendar engine
507573
// hook (`nowIndicatorContent`) so it does NOT clash with the boolean
@@ -580,14 +646,36 @@ private _portalContainers = new Set<HTMLElement>();
580646
};
581647
};
582648
}
583-
// Excluded *Content slots (documented, not gaps): noEventsContent (list-view
584-
// only — @fullcalendar/list is not a bundled peer), slotLaneContent (background
585-
// lane, no demand), allDayContent (trivial label). Consumers needing those use
586-
// the :options passthrough + getApi().
587-
// Excluded *Content slots (documented, not gaps): noEventsContent (list-view
588-
// only — @fullcalendar/list is not a bundled peer), slotLaneContent (background
589-
// lane, no demand), allDayContent (trivial label). Consumers needing those use
590-
// the :options passthrough + getApi().
649+
if (this.allDayContent !== undefined) {
650+
opts.allDayContent = (arg: any) => {
651+
const node = document.createElement('div');
652+
const dispose = portals.allDayContent(node, {
653+
arg
654+
});
655+
return {
656+
domNodes: [node],
657+
dispose
658+
};
659+
};
660+
}
661+
if (this.slotLaneContent !== undefined) {
662+
opts.slotLaneContent = (arg: any) => {
663+
const node = document.createElement('div');
664+
const dispose = portals.slotLaneContent(node, {
665+
arg
666+
});
667+
return {
668+
domNodes: [node],
669+
dispose
670+
};
671+
};
672+
}
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().
591679

592680
this.instance = new Calendar(this._ref__rozieRoot, opts);
593681
this.instance.render();
@@ -643,6 +731,8 @@ private _portalContainers = new Set<HTMLElement>();
643731
<slot name="weekNumber"></slot>
644732
<slot name="nowIndicatorContent"></slot>
645733
<slot name="moreLink"></slot>
734+
<slot name="allDayContent"></slot>
735+
<slot name="slotLaneContent"></slot>
646736
`;
647737
}
648738

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,5 @@ const api = cal.current?.getApi();
100100
| weekNumber | arg |
101101
| nowIndicatorContent | arg |
102102
| moreLink | arg |
103+
| allDayContent | arg |
104+
| slotLaneContent | arg |

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export interface FullCalendarProps {
3636
renderWeekNumber?: (params: { arg: () => void }) => ReactNode;
3737
renderNowIndicatorContent?: (params: { arg: () => void }) => ReactNode;
3838
renderMoreLink?: (params: { arg: () => void }) => ReactNode;
39+
renderAllDayContent?: (params: { arg: () => void }) => ReactNode;
40+
renderSlotLaneContent?: (params: { arg: () => void }) => ReactNode;
3941
slots?: Record<string, () => ReactNode>;
4042
}
4143

0 commit comments

Comments
 (0)