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',
},
],
slug- URL segment appended under/a/[orgSlug]/settings/. Must be unique across all installed packages.label- text shown in the settings sidebar.component- subpath (no extension) to the.tsxthat renders the panel.
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.