Settings

Packages can add entries to the Personal Settings panel without declaring routes or navigation. Each entry is a link in the settings list; clicking it renders a component the package ships. This is how settings-only packages like @tinycld/google-takeout-import integrate - no routes, no nav, just a settings array and a component file.

Declaring settings entries

Add a settings array to the manifest. Each entry has three fields:

settings: [
    {
        slug: 'example',
        label: 'Example settings',
        component: 'settings/example',
    },
],

A package can declare any number of settings entries. @tinycld/mail ships two: one for the mail provider, one for mailboxes.

settings: [
    { slug: 'provider', label: 'Provider', component: 'settings/provider' },
    { slug: 'mailboxes', label: 'Mailboxes', component: 'settings/mailboxes' },
],

The component

A settings component is a plain React component. It renders into the settings layout that core already provides - no chrome, no title bar, just the panel body. Use the same hooks and patterns as any other screen:

import { useForm } from 'react-hook-form'
import { useStore, useOrgLiveQuery } from '@tinycld/core/lib/pocketbase'
import { useMutation } from '@tinycld/core/lib/mutations'

export default function ExampleSettings() {
    const [settingsCollection] = useStore('example_settings')
    const { data } = useOrgLiveQuery((query, { orgId }) =>
        query.from({ s: settingsCollection }).where(({ s }) => eq(s.org, orgId)),
    )
    // render your form
    return null
}

Default-export the component. The generator imports it by default name - a named export will not be picked up.

package.json exports

The wildcard export for settings/* must be present or the generator can’t resolve the component path:

{
    "exports": {
        "./settings/*": "./settings/*.tsx"
    }
}

Settings-only packages

A package with no nav entry, no screens, and no public routes is valid. @tinycld/google-takeout-import is the canonical example:

const manifest = {
    name: 'Google Takeout Import',
    slug: 'google-takeout-import',
    version: '0.1.0',
    description: 'Import data from Google Takeout .zip files.',
    settings: [
        {
            slug: 'google-takeout',
            component: 'settings/takeout',
            label: 'Import from Google',
        },
    ],
}

export default manifest

When this package is linked, a single “Import from Google” link appears in Personal Settings. Unlinking it removes the entry with no trace.

Runtime gating from a settings panel

Settings panels often need to know which other packages are installed - e.g. the takeout importer only offers “Import mail” if @tinycld/mail is linked. Use usePackages() for this:

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

export default function TakeoutSettings() {
    const installed = new Set(usePackages().map((p) => p.slug))
    const canImportMail = installed.has('mail')
    // conditionally render import options
}

Do not add a hard import from @tinycld/mail into the dependent package. That would turn the dependency into a build-time requirement and break the app shell’s lean-shell guarantee. Filter by slug at runtime instead.