Sidebar slots

A sidebar slot is a named insertion point in a host package’s sidebar. Other packages declare contributions targeting a slot, and the host renders them inline alongside its own UI. This is the mechanism for extending an existing package — for example, a booking-page package that adds “My Booking Pages” to the calendar sidebar instead of becoming its own top-level nav entry.

The contract is intentionally narrow: the host owns the slot’s position; the contributor owns what renders inside it.

When to use a sidebar slot vs. a new package

You want…Use…
A new top-level feature with its own nav entry and screensA normal package with nav + routes
To add a section inside an existing package’s sidebarA sidebar contribution
To add a panel to Personal Settingssettings

Contributions are static and bundled-only — they ship at build time through the same generator pipeline as sidebar, provider, and settings. Runtime-installed packages (from the in-app package registry) cannot contribute slots.

Host side: exposing a slot

A host package does two things: declares the slot in its manifest, and renders <SidebarSlot> where contributions should appear.

const manifest = {
    name: 'Calendar',
    slug: 'calendar',
    version: '0.1.0',
    description: 'Shared calendar for your organization',
    routes: { directory: 'screens' },
    sidebar: { component: 'sidebar' },
    slots: ['sidebar.after-calendars'],
    // ...
}

Slot names are free-form strings — by convention, namespace them as sidebar.<descriptive-anchor> so contributors can read where their content lands without having to open the host’s sidebar code. Each name must be unique within the manifest; the generator errors on duplicates.

In the host’s sidebar component, drop <SidebarSlot> at the position the slot’s name promises:

import {
    SidebarDivider,
    SidebarItem,
    SidebarNav,
    SidebarSlot,
} from '@tinycld/core/components/sidebar-primitives'

export default function CalendarSidebar() {
    return (
        <SidebarNav>
            {/* ... mini calendar, calendar list ... */}
            <SidebarDivider />

            <SidebarSlot target="calendar" slot="sidebar.after-calendars" />

            <SidebarItem label="Subscribe to calendar" /* ... */ />
        </SidebarNav>
    )
}

When no contributions target the slot, <SidebarSlot> renders nothing — no extra divider, no empty container. The host’s existing layout is unchanged for users in a lean checkout.

Contributor side: rendering into a slot

A contributor package declares a sidebarContributions array in its manifest. Each entry says which host’s slot to target, and which component to render.

const manifest = {
    name: 'Calendar Slots',
    slug: 'calendar-slots',
    version: '0.1.0',
    description: 'Calendly-style booking pages for the calendar.',
    sidebarContributions: [
        {
            target: 'calendar',
            slot: 'sidebar.after-calendars',
            component: 'sidebar-contributions/booking-pages',
        },
    ],
    // ...
}

Add the matching wildcard to your package.json so the generator can resolve the import:

{
    "exports": {
        "./sidebar-contributions/*": "./tinycld/calendar-slots/sidebar-contributions/*.tsx"
    }
}

The component: contributor owns the chrome

<SidebarSlot> renders each contribution back-to-back with no wrapper. Your component is responsible for its own structure — heading, items, dividers, collapsible state, action buttons. This is deliberate: a “My Booking Pages” section and a “Quick Filters” section want very different layouts, and the slot stays out of the way.

import { SidebarHeading, SidebarItem } from '@tinycld/core/components/sidebar-primitives'
import { useOrgHref } from '@tinycld/core/lib/org-routes'
import { useRouter } from 'expo-router'
import { Calendar } from 'lucide-react-native'
import { useBookingPages } from '../hooks/use-booking-pages'

export default function BookingPagesContribution() {
    const router = useRouter()
    const orgHref = useOrgHref()
    const { pages } = useBookingPages()

    return (
        <>
            <SidebarHeading>My Booking Pages</SidebarHeading>
            {pages.map((p) => (
                <SidebarItem
                    key={p.id}
                    label={p.name}
                    icon={Calendar}
                    onPress={() => router.push(orgHref('calendar-slots/[id]', { id: p.id }))}
                />
            ))}
        </>
    )
}

The component must default-export. The generator imports by default name — a named export will not be picked up.

Ordering between contributions

When multiple packages target the same slot, contributions sort by order ascending (default 0). Ties break alphabetically by contributor slug. Set order explicitly only when relative position matters between two contributions you don’t control — for most cases the default is fine.

What the generator validates

Validation runs at generate time (tinycld/scripts/generate.ts, invoked by pnpm install and pnpm run packages:generate):

Typos surface immediately. There’s no silent “contribution declared but never rendered” failure mode for present hosts.

Runtime gating: “only contribute if the host is present”

The generator already handles the absent-host case by skipping registration with a warning. If your contributor needs to vary its own behavior — for example, hide a “Sync with mail” feature when @tinycld/mail isn’t installed — use the runtime package registry:

import { usePackages } from '@tinycld/core/lib/packages/use-packages'

export default function MyContribution() {
    const installed = new Set(usePackages().map((p) => p.slug))
    const mailAvailable = installed.has('mail')
    // ...
}

Do not add a hard import '@tinycld/mail/...' to your contributor. That turns the host into a build-time dependency and breaks the lean-shell guarantee (a workspace without mail must still typecheck and run). Filter by slug at runtime instead.

Built-in slots

HostSlotPosition
calendarsidebar.after-calendarsBelow the “My calendars” / “Other calendars” / “Subscribed calendars” group, above “Subscribe to calendar”
mailsidebar.after-labelsBelow the Labels section, above the Help link
drivesidebar.after-treeBelow the folder tree, above “Shared with me”

To add a new slot to a host, declare its name in the host’s slots array and render a <SidebarSlot> at the corresponding position.

Troubleshooting

SymptomLikely cause
Contribution doesn’t appear after pnpm installGenerator hasn’t re-run, or package.json exports is missing the wildcard for the component subpath.
Build fails: “sidebarContribution targets unknown slot”Typo in slot or the host hasn’t declared that slot in manifest.slots. The error lists the host’s actual slots.
Build warns: “not installed in this workspace”The target host isn’t a present member — normal in a partial checkout. Install the host or remove the contribution.
Contribution renders but with no contentComponent file exists but uses a named export. The generator only picks up default exports.