How to — add a customization option
Adding a new toggle, slider, or picker that appears in the Finder Builder and propagates to the visitor-facing widget. This touches all three repos. The canonical worked example for this pattern is any existing field in V2Config.design.
Step 1 — Extend V2Config in finderBuilderConfig.ts
V2Config is the TypeScript type that represents the full in-memory state of the builder. Add the new field to the correct sub-object:
// dropafinder-app-nextjs/src/components/finders/v2/finderBuilderConfig.ts
export type V2Config = {
// ...
design: {
// ...
search: {
autoclosePanelOnSelect: boolean; // ← add here
};
};
// ...
};Then update buildDefaultV2Config() (or equivalent default factory) in the same file to include the new field’s default value:
design: {
search: {
autoclosePanelOnSelect: true,
},
}💡 Tip: Because
designis stored as a JSON column on thefinderstable, old saved finders won’t have the new key. The default factory value is what the builder falls back to for those finders — choose it to preserve existing behavior.
Step 2 — Add the control to the appropriate Section component
Section components live in dropafinder-app-nextjs/src/components/finders/v2/sections/. Pick the section that semantically owns the new option:
| Option category | Section file |
|---|---|
| Colors, borders | DesignSection.tsx / ThemeSection.tsx |
| Fonts, sizes | TypographySection.tsx |
| Layout | DesignSection.tsx |
| Shape / radius | ShapeSection.tsx |
| Map settings | MapsSection.tsx |
| Search / behavior | BehaviorSection.tsx |
| Refinements | RefinementsSection.tsx |
| Content / fields | ContentSection.tsx |
Inside the section component, every control calls the update callback, which wraps dispatch({ type: 'SET', payload }) in FinderBuilderV2.tsx. The pattern is a functional updater — you receive the current config and return the next config:
// Inside BehaviorSection.tsx
type Props = {
config: V2Config;
update: (fn: (c: V2Config) => V2Config) => void;
};
// Toggle control
<Toggle
label="Auto-close panel on select"
checked={config.design.search.autoclosePanelOnSelect}
onChange={(val) =>
update((c) => ({
...c,
design: {
...c.design,
search: { ...c.design.search, autoclosePanelOnSelect: val },
},
}))
}
/>Never mutate config directly — always spread into a new object tree. The history reducer (historyReducer in FinderBuilderV2.tsx) pushes this into the undo stack via dispatch({ type: 'SET', payload }).
Step 3 — Update the Live Preview payload
The Live Preview iframe receives an updated payload every time config changes. The payload is assembled in previewPayload.ts (dropafinder-app-nextjs/src/components/finders/v2/previewPayload.ts). If your new field maps to a settings.* entry that the widget reads, check that previewPayload.ts includes it in the serialized settings array.
Most design.* fields serialize automatically through the existing settings-serialization loop. Verify by opening the Finder Builder locally, toggling your new option, and confirming the Live Preview iframe reflects the change.
Step 4 — Persist via the API payload transformer
When the user saves, the config is transformed to the wire format by src/lib/api/payloads.ts (toFinderPayload). Ensure your new field is written into the payload:
// dropafinder-app-nextjs/src/lib/api/payloads.ts
function serializeDesignSettings(design: V2Config['design']): SettingsEntry[] {
return [
// ...existing entries...
{ name: 'design.search.autoclosePanelOnSelect', value: String(design.search.autoclosePanelOnSelect) },
];
}The backend stores this as an entry in the settings JSON array on the finders table — no migration needed.
Step 5 — Read the value in the widget
The widget reads design.* fields from designStore after initializeFinderStores() parses the flat settings array into nested objects. In the relevant Svelte component:
<script>
import { designStore } from '../lib/stores.js';
// Always use a default fallback — old finders won't have the key
$: autoclosePanelOnSelect = $designStore?.search?.autoclosePanelOnSelect ?? true;
</script>Apply the value in the component’s behavior:
{#if autoclosePanelOnSelect}
<!-- close the panel on location select -->
{/if}Step 6 — Backend validation (optional)
If the new value should be validated server-side (e.g., it affects billing or security), add a validation rule to FinderController::update in dropafinder-app-backend/app/Http/Controllers/FinderController.php. For most design/settings values, validation is handled by the TypeScript type system on the frontend — no backend rule is needed.
Step 7 — Verify end-to-end
- Open the Finder Builder locally (
http://localhost:3000). - Find the new control in the builder UI. Toggle it.
- Confirm the Live Preview iframe updates immediately.
- Save the finder. Confirm the value persists when you re-open the builder.
- Build the widget (
npm run build:dev) and open the public embed. Confirm the widget respects the setting.
Step 8 — Deploy in order
- Widget deploy first (
npm run deploy:02) if the widget reads the new field. - Dashboard deploy second (
git push origin main). - Backend deploy only if validation rules were added (
make deploy).
See deploy for the full deploy ritual.
Where to next
- The full three-repo change flow: add-api-endpoint-end-to-end
- Adding a color preset that uses this option: add-theme-preset
- Ship to production: deploy