External widget — stores and state
The widget uses Svelte’s built-in reactive store primitives. All stores are defined in src/lib/stores.js. There are no derived stores — every store is a writable. State flows in one direction: the CDN payload (or preview bridge payload) is applied via initializeFinderStores, which sets all stores in a single coordinated pass. Components subscribe reactively; they do not write to stores directly except through two exported helper functions.
Store roster
| Export | Initial value | Purpose |
|---|---|---|
payloadStore | null | Full enriched payload object (finder + locations + parsed sub-objects) |
designStore | {} | Design tokens extracted from payload.finder.settings (keys prefixed design.) |
settingsStore | {} | Runtime settings extracted from payload.finder.settings (keys prefixed settings.) |
localizationStore | buildLocalizationRuntime() | Locale string + translation overrides; derived from design.localization |
searchQueryStore | '' | Current text in the search input |
filterPanelOpenStore | false | Whether the filter/refinements panel is open |
refinementsStore | See below | All filter, sort, and refinement state |
cachedLocationsStore | [] | Full location array from the payload (never filtered) |
locationsStore | [] | Visible (filtered + sorted + distance-annotated) locations |
activeLocationStore | null | Currently selected/highlighted location |
userLocationStore | { lat: null, lng: null, isGeolocated: false, error: null } | Visitor’s resolved coordinates |
All stores are declared at module scope with writable() (stores.js:5–40). There are no Svelte derived stores in this file — reactivity is triggered by explicit .set() and .update() calls inside initializeFinderStores and refreshVisibleLocations.
refinementsStore initial shape
stores.js:11–31:
export const refinementsStore = writable({
selectedTags: [],
selectedCountries: [],
selectedAdvancedRefinements: {},
radius: 500,
radiusEnabled: true,
sortBy: 'distance',
tag: true,
countries: true,
sort: true,
distance: true,
radiusRange: true,
popup: true,
quickFiltersRow: true,
advanced_refinements: [],
filterOptions: [],
sortOptions: [],
selectedHoursOpen: false,
selectedRatingMin: null,
sortDirection: 'asc',
});The tag, countries, sort, distance, radiusRange, popup, and quickFiltersRow fields are visibility toggles that control which UI controls render. They are overwritten from payload.finder.settings entries with the refinements. prefix during initialization.
Initialization: initializeFinderStores
stores.js:229–348 — The single entry point for loading a payload into all stores. Called by App.svelte:190 via applyPayload. It accepts the raw payload and an options object { preserveUiState }.
Settings parsing
stores.js:235–251 — Every entry in payload.finder.settings is a flat { name, value } object. initializeFinderStores splits on . and routes entries into three nested objects by prefix:
design.*→designobject →designStoresettings.*→settingsobject →settingsStorerefinements.*→refinementsobject → merged intorefinementsStore
Values are normalized by normalizeSettingValue (stores.js:107–112): the strings "true" and "false" become booleans; numeric strings become numbers.
Rules config parsing
stores.js:252–265 — After settings are parsed, parseRulesConfig processes design.refinements.rulesConfig (a JSON string) to build the filterOptions and sortOptions arrays. These arrays control which filter panels and sort options are visible and in what order. The default options are defined by defaultFilterOptions and defaultSortOptions (stores.js:114–134).
The default sort is determined by finding the first sortOptions entry that is both enabled and isDefault; its id and direction are written into refinements.sortBy and refinements.sortDirection.
Payload enrichment
stores.js:267–279 — The raw payload is re-assembled with the parsed sub-objects attached under payload.finder:
const enrichedPayload = {
...payload,
finder: {
...payload.finder,
design, // parsed design tokens
parsedSettings: settings,
parsedRefinements: refinements,
},
};
payloadStore.set(enrichedPayload);App.svelte subscribes to payloadStore (App.svelte:96–104) to extract customCSS.
preserveUiState branching
stores.js:281–347 — When preserveUiState is true (Live Preview hot-reload, see preview-bridge.md), initializeFinderStores keeps visitor UI state intact:
searchQueryStoreandfilterPanelOpenStoreare not reset.refinementsStoreis updated with payload-sourced values but selected tags, countries, advanced refinements, hours filter, rating filter, radius, and sort are preserved from the current store value.- A validity check confirms the preserved
sortByis still enabled in the newsortOptions; if not, it falls back to the payload default. pruneSelections(stores.js:211–227) removes any selected tags, countries, or advanced refinement names that no longer exist in the new locations array.
When preserveUiState is false (initial load or full remount), all selections are reset to defaults.
Location change detection
stores.js:290–347 — Locations are compared by a stable JSON signature (getLocationSignature, stores.js:193–209) that covers id, title, latitude, longitude, address, tags, and custom_fields. If the signature is unchanged from the last call (e.g., a design-only change in preview mode), cachedLocationsStore is not updated and activeLocationStore is preserved without scroll-to-top behavior.
// stores.js:291
const nextLocationSignature = getLocationSignature(locations);
const locationsChanged = nextLocationSignature !== lastLocationSignature;lastLocationSignature is module-level state (stores.js:42) — it persists across calls to initializeFinderStores within the same widget lifecycle.
refreshVisibleLocations
stores.js:44–91 — Private function called by initializeFinderStores and setUserLocation. It reads the current values of all stores via get(), calls applyFinderFilters from filtering.js, writes the result to locationsStore, then resolves the correct activeLocationStore value:
- If the previously active location still exists in the filtered list, it stays active.
- Otherwise, if
preserveActiveLocationis true, the oldactiveLocationStorevalue is kept even if it’s not in the visible list (edge case: radius filter hides the active location). - Otherwise, the first location in the filtered list becomes active.
setUserLocation (exported)
stores.js:350–359 — The only public write path for userLocationStore. Called by App.svelte after the geolocation or IP-fallback resolves. After updating userLocationStore, it triggers refreshVisibleLocations() so distance is recalculated and the list re-sorts immediately.
getCurrentLocations (exported)
stores.js:361–363 — Synchronous snapshot of locationsStore via get(). Used by analytics and the map component to read the current visible set without subscribing.
App.svelte subscriptions
App.svelte subscribes to three stores at module scope using Svelte’s $: syntax shorthand (.subscribe call):
payloadStore.subscribe(App.svelte:96) — extractscustomCSSlocalizationStore.subscribe(App.svelte:106) — keepslocalizationlocal variable in sync for template helpersdesignStore.subscribe(App.svelte:110) — keepsdesignlocal variable in sync for layout reactive declarations
Components (Search, Map, List, MobileDetail) subscribe to individual stores via Svelte’s $store auto-subscription syntax in their own <script> blocks.
Filtering logic (filtering.js)
src/lib/filtering.js — applyFinderFilters is called by refreshVisibleLocations. It:
- Annotates every location with a
distancefield (Haversine formula,filtering.js:3–26). - Applies text search (simple substring or natural-language mode).
- Applies tag, country, radius, hours-open, and minimum-rating filters.
- Applies advanced refinement rules (show/hide actions with field conditions).
- Sorts via
sortLocations(distance, name, country, state, city, postal, hours).
No filtering state lives in filtering.js itself — it is a pure function that takes all inputs as arguments and returns a new array.
Persistence
There is no localStorage or sessionStorage persistence for store values. The localStorage layer in cdn-and-pointer.md caches the raw CDN payload by hash, but that is read once at boot (in api.js) and never written by store code. All store state is ephemeral — it resets on page reload.
Related pages
- overview.md — widget architecture overview
- init-flow.md — when
initializeFinderStoresis called in the boot sequence - preview-bridge.md — how
preserveUiStateis set - cdn-and-pointer.md — payload fetch before store initialization
- directory-structure.md — file map for all
src/lib/modules