@micha.bigler/survey-renderer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ // Empty by design: this pure package ships no host-specific discovery datasets. A consuming
2
+ // host registers its own datasets (e.g. before first render of any discovery-select question)
3
+ // via registerDiscoveryDataset(key, dataset) -- do not mutate discoveryDatasetRegistry directly,
4
+ // it is exported read-only for introspection only.
5
+ export const discoveryDatasetRegistry = {};
6
+ export function registerDiscoveryDataset(datasetKey, dataset) {
7
+ if (!datasetKey) {
8
+ throw new Error("registerDiscoveryDataset requires a non-empty datasetKey");
9
+ }
10
+ discoveryDatasetRegistry[datasetKey] = dataset;
11
+ }
12
+ export function getDiscoveryDataset(datasetKey) {
13
+ if (!datasetKey) {
14
+ return null;
15
+ }
16
+ return discoveryDatasetRegistry[datasetKey] || null;
17
+ }
@@ -0,0 +1,160 @@
1
+ import { normalizeLabel } from "./questionUiConfig";
2
+ function clamp(value, min = 0, max = 1) {
3
+ return Math.max(min, Math.min(max, value));
4
+ }
5
+ export function serializeDiscoveryValue(value) {
6
+ return String(value !== null && value !== void 0 ? value : "");
7
+ }
8
+ export function resolveDiscoveryStrategy(...strategySources) {
9
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
10
+ const merged = strategySources.filter(Boolean).reduce((current, next) => (Object.assign(Object.assign(Object.assign({}, current), next), { scoreFormula: Object.assign(Object.assign({}, (current.scoreFormula || {})), (next.scoreFormula || {})) })), {});
11
+ const adaptiveShare = clamp((_a = merged.adaptiveShare) !== null && _a !== void 0 ? _a : (merged.randomShare !== undefined ? 1 - merged.randomShare : 0.5));
12
+ const randomShare = clamp((_b = merged.randomShare) !== null && _b !== void 0 ? _b : 1 - adaptiveShare);
13
+ return {
14
+ adaptiveShare,
15
+ deckSize: Math.max(1, (_c = merged.deckSize) !== null && _c !== void 0 ? _c : 30),
16
+ maxSelections: Math.max(1, (_d = merged.maxSelections) !== null && _d !== void 0 ? _d : 5),
17
+ randomShare,
18
+ scoreFormula: {
19
+ popularityWeight: (_g = (_f = (_e = merged.scoreFormula) === null || _e === void 0 ? void 0 : _e.popularityWeight) !== null && _f !== void 0 ? _f : merged.popularityWeight) !== null && _g !== void 0 ? _g : 0.35,
20
+ randomWeight: (_k = (_j = (_h = merged.scoreFormula) === null || _h === void 0 ? void 0 : _h.randomWeight) !== null && _j !== void 0 ? _j : merged.randomWeight) !== null && _k !== void 0 ? _k : 0.2,
21
+ relatedWeight: (_o = (_m = (_l = merged.scoreFormula) === null || _l === void 0 ? void 0 : _l.relatedWeight) !== null && _m !== void 0 ? _m : merged.relatedWeight) !== null && _o !== void 0 ? _o : 0.45,
22
+ },
23
+ };
24
+ }
25
+ function shuffle(values) {
26
+ const result = [...values];
27
+ for (let index = result.length - 1; index > 0; index -= 1) {
28
+ const swapIndex = Math.floor(Math.random() * (index + 1));
29
+ [result[index], result[swapIndex]] = [result[swapIndex], result[index]];
30
+ }
31
+ return result;
32
+ }
33
+ function average(values) {
34
+ if (!values.length) {
35
+ return 0;
36
+ }
37
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
38
+ }
39
+ function buildSimilarity(left, right) {
40
+ if (!left || !right || left.valueKey === right.valueKey) {
41
+ return 0;
42
+ }
43
+ if (left.neighborValues.includes(right.valueKey) || right.neighborValues.includes(left.valueKey)) {
44
+ return 1;
45
+ }
46
+ const leftTags = new Set(left.tags || []);
47
+ const rightTags = new Set(right.tags || []);
48
+ const sharedTagCount = Array.from(leftTags).filter((tag) => rightTags.has(tag)).length;
49
+ const unionCount = new Set([...(left.tags || []), ...(right.tags || [])]).size;
50
+ const tagScore = unionCount > 0 ? sharedTagCount / unionCount : 0;
51
+ const categoryScore = left.category && left.category === right.category ? 0.35 : 0;
52
+ return clamp(Math.max(tagScore * 0.8, categoryScore));
53
+ }
54
+ export function buildDiscoveryItems(options, dataset) {
55
+ const baseItems = (options || []).map((option, index) => {
56
+ var _a, _b;
57
+ const metadata = ((_a = dataset === null || dataset === void 0 ? void 0 : dataset.resolveMetadata) === null || _a === void 0 ? void 0 : _a.call(dataset, option)) || {};
58
+ const normalizedFilters = Object.fromEntries(Object.entries(metadata.filters || {}).map(([filterKey, filterValues]) => [
59
+ filterKey,
60
+ Array.isArray(filterValues) ? filterValues.map(normalizeLabel).filter(Boolean) : [],
61
+ ]));
62
+ return {
63
+ background: metadata.background || null,
64
+ category: metadata.category || "general",
65
+ discoverable: metadata.discoverable !== false,
66
+ filters: normalizedFilters,
67
+ imageAlt: metadata.imageAlt || option.label || "",
68
+ imageUrl: metadata.imageUrl || "",
69
+ index,
70
+ label: option.label || "",
71
+ metadata,
72
+ neighborLabels: (metadata.neighbors || []).map((entry) => normalizeLabel(entry)),
73
+ popularity: clamp((_b = metadata.popularity) !== null && _b !== void 0 ? _b : 0.4),
74
+ tags: Array.from(new Set(metadata.tags || [])),
75
+ value: option.value,
76
+ valueKey: serializeDiscoveryValue(option.value),
77
+ };
78
+ });
79
+ const itemByLabel = new Map(baseItems.map((item) => [normalizeLabel(item.label), item]));
80
+ return baseItems.map((item) => (Object.assign(Object.assign({}, item), { neighborValues: item.neighborLabels
81
+ .map((entry) => { var _a; return (_a = itemByLabel.get(entry)) === null || _a === void 0 ? void 0 : _a.valueKey; })
82
+ .filter((value) => value !== undefined) })));
83
+ }
84
+ export function filterDiscoveryItemsByGate(items, gateLabels, gateConfig = {}) {
85
+ const filterKey = (gateConfig === null || gateConfig === void 0 ? void 0 : gateConfig.filterKey) || "quarters";
86
+ const normalizedGateLabels = (gateLabels || []).map(normalizeLabel).filter(Boolean);
87
+ if (!normalizedGateLabels.length) {
88
+ return [];
89
+ }
90
+ const gateSet = new Set(normalizedGateLabels);
91
+ return (items || []).filter((item) => {
92
+ var _a;
93
+ const itemFilterValues = ((_a = item === null || item === void 0 ? void 0 : item.filters) === null || _a === void 0 ? void 0 : _a[filterKey]) || [];
94
+ return itemFilterValues.some((entry) => gateSet.has(entry));
95
+ });
96
+ }
97
+ export function rankDiscoveryItems(items, state, strategy = {}) {
98
+ const resolvedStrategy = resolveDiscoveryStrategy(strategy);
99
+ const selectedSet = new Set((state.selectedValues || []).map(serializeDiscoveryValue));
100
+ const dismissedSet = new Set((state.dismissedValues || []).map(serializeDiscoveryValue));
101
+ const seenSet = new Set((state.seenValues || []).map(serializeDiscoveryValue));
102
+ const likedItems = items.filter((item) => selectedSet.has(item.valueKey));
103
+ const popularityWeight = resolvedStrategy.scoreFormula.popularityWeight;
104
+ const relatedWeight = resolvedStrategy.scoreFormula.relatedWeight;
105
+ const randomWeight = resolvedStrategy.scoreFormula.randomWeight;
106
+ const candidateItems = items.filter((item) => item.discoverable &&
107
+ !selectedSet.has(item.valueKey));
108
+ const freshCandidateItems = candidateItems.filter((item) => !dismissedSet.has(item.valueKey) &&
109
+ !seenSet.has(item.valueKey));
110
+ const rankedSource = freshCandidateItems.length > 0 ? freshCandidateItems : candidateItems;
111
+ return rankedSource
112
+ .map((item) => {
113
+ const relatedness = average(likedItems.map((likedItem) => buildSimilarity(likedItem, item)));
114
+ const randomScore = Math.random();
115
+ const score = item.popularity * popularityWeight +
116
+ relatedness * relatedWeight +
117
+ randomScore * randomWeight;
118
+ return Object.assign(Object.assign({}, item), { relatedness,
119
+ score });
120
+ })
121
+ .sort((left, right) => right.score - left.score || right.popularity - left.popularity || left.index - right.index);
122
+ }
123
+ export function buildDiscoveryDeck(items, state, strategy = {}) {
124
+ const resolvedStrategy = resolveDiscoveryStrategy(strategy);
125
+ const ranked = rankDiscoveryItems(items, state, resolvedStrategy);
126
+ const adaptiveShare = resolvedStrategy.adaptiveShare;
127
+ const deckSize = resolvedStrategy.deckSize;
128
+ const adaptiveCount = Math.round(deckSize * adaptiveShare);
129
+ const randomCount = Math.max(0, deckSize - adaptiveCount);
130
+ const randomItems = shuffle(ranked).slice(0, randomCount);
131
+ const randomSet = new Set(randomItems.map((item) => item.valueKey));
132
+ const adaptiveItems = ranked.filter((item) => !randomSet.has(item.valueKey)).slice(0, adaptiveCount);
133
+ const queue = [];
134
+ const maxLength = Math.min(deckSize, ranked.length);
135
+ while (queue.length < maxLength && (randomItems.length || adaptiveItems.length)) {
136
+ if (adaptiveItems.length) {
137
+ queue.push(adaptiveItems.shift());
138
+ }
139
+ if (queue.length >= maxLength) {
140
+ break;
141
+ }
142
+ if (randomItems.length) {
143
+ queue.push(randomItems.shift());
144
+ }
145
+ }
146
+ return queue.slice(0, deckSize);
147
+ }
148
+ export function summarizeDiscoveryDataset(items) {
149
+ const discoverable = items.filter((item) => item.discoverable);
150
+ const topPopularity = [...discoverable]
151
+ .sort((left, right) => right.popularity - left.popularity)
152
+ .slice(0, 8)
153
+ .map((item) => ({ label: item.label, popularity: item.popularity }));
154
+ return {
155
+ categories: Array.from(new Set(discoverable.map((item) => item.category))).sort(),
156
+ discoverableCount: discoverable.length,
157
+ itemCount: items.length,
158
+ topPopularity,
159
+ };
160
+ }
@@ -0,0 +1,17 @@
1
+ // Empty by design: this pure package ships no host-specific geo datasets. A consuming host
2
+ // registers its own datasets (e.g. before first render of any geo-select question) via
3
+ // registerGeoDataset(key, dataset) -- do not mutate geoDatasetRegistry directly, it is
4
+ // exported read-only for introspection only.
5
+ export const geoDatasetRegistry = {};
6
+ export function registerGeoDataset(datasetKey, dataset) {
7
+ if (!datasetKey) {
8
+ throw new Error("registerGeoDataset requires a non-empty datasetKey");
9
+ }
10
+ geoDatasetRegistry[datasetKey] = dataset;
11
+ }
12
+ export function getGeoDataset(datasetKey) {
13
+ if (!datasetKey) {
14
+ return null;
15
+ }
16
+ return geoDatasetRegistry[datasetKey] || null;
17
+ }
@@ -0,0 +1,242 @@
1
+ export const surveyRendererTranslations = {
2
+ "surveyRenderer.DISCOVERY_AUTOCOMPLETE_PLACEHOLDER": {
3
+ "de": "Weitere Orte suchen oder Auswahl anpassen",
4
+ "en": "Search more places or adjust your selection",
5
+ "fr": "Rechercher d'autres lieux ou ajuster votre selection",
6
+ "sw": "Tafuta maeneo zaidi au rekebisha uchaguzi wako"
7
+ },
8
+ "surveyRenderer.DISCOVERY_DONE": {
9
+ "de": "Alle vorgeschlagenen Orte wurden gezeigt. Du kannst deine Auswahl unten noch anpassen.",
10
+ "en": "All suggested places have been shown. You can still adjust your picks below.",
11
+ "fr": "Tous les lieux proposes ont ete affiches. Vous pouvez encore ajuster votre choix.",
12
+ "sw": "Maeneo yote yaliyopendekezwa yameonyeshwa. Unaweza bado kurekebisha uchaguzi wako hapa chini."
13
+ },
14
+ "surveyRenderer.DISCOVERY_GATE_EMPTY": {
15
+ "de": "Keine passenden Quartiere gefunden",
16
+ "en": "No matching districts found",
17
+ "fr": "Aucun quartier correspondant trouve",
18
+ "sw": "Hakuna mitaa inayolingana iliyopatikana"
19
+ },
20
+ "surveyRenderer.DISCOVERY_GATE_HINT": {
21
+ "de": "Danach zeigen wir dir passende Orte aus den gewählten Quartieren.",
22
+ "en": "After that, we show matching places from the selected districts.",
23
+ "fr": "Ensuite, nous affichons des lieux adaptes aux quartiers choisis.",
24
+ "sw": "Baada ya hapo, tutaonyesha maeneo yanayolingana kutoka mitaa iliyochaguliwa."
25
+ },
26
+ "surveyRenderer.DISCOVERY_GATE_LABEL": {
27
+ "de": "Wähle zuerst bis zu {{max}} Quartiere aus",
28
+ "en": "Choose up to {{max}} districts first",
29
+ "fr": "Choisissez d'abord jusqu'a {{max}} quartiers",
30
+ "sw": "Chagua kwanza mitaa hadi {{max}}"
31
+ },
32
+ "surveyRenderer.DISCOVERY_GATE_PLACEHOLDER": {
33
+ "de": "Quartiere suchen oder Auswahl anpassen",
34
+ "en": "Search districts or adjust your selection",
35
+ "fr": "Rechercher des quartiers ou ajuster votre selection",
36
+ "sw": "Tafuta mitaa au rekebisha uchaguzi wako"
37
+ },
38
+ "surveyRenderer.DISCOVERY_GATE_REQUIRED": {
39
+ "de": "Wähle zuerst mindestens ein Quartier aus, damit wir passende Orte anzeigen können.",
40
+ "en": "Choose at least one district first so we can show matching places.",
41
+ "fr": "Choisissez d'abord au moins un quartier pour voir des lieux adaptes.",
42
+ "sw": "Chagua angalau mtaa mmoja kwanza ili tuweze kuonyesha maeneo yanayolingana."
43
+ },
44
+ "surveyRenderer.DISCOVERY_HELP": {
45
+ "de": "Nach rechts merken, nach links skippen.",
46
+ "en": "Swipe right to keep, left to skip.",
47
+ "fr": "Glissez a droite pour enregistrer, a gauche pour passer.",
48
+ "sw": "Swipe kulia kuhifadhi, kushoto kuruka."
49
+ },
50
+ "surveyRenderer.DISCOVERY_MAX_REACHED": {
51
+ "de": "{{max}} Orte sind ausgewählt. Wenn du weiterswipen möchtest, entferne zuerst einen Ort aus deiner Auswahl.",
52
+ "en": "{{max}} places are selected. Remove one first if you want to keep swiping.",
53
+ "fr": "{{max}} lieux sont selectionnes. Retirez-en un si vous voulez continuer.",
54
+ "sw": "Maeneo {{max}} yamechaguliwa. Ondoa moja kwanza ikiwa unataka kuendelea kuswipe."
55
+ },
56
+ "surveyRenderer.DISCOVERY_REFRESH": {
57
+ "de": "Das aktuelle Deck ist durch. Passe deine Auswahl an oder nutze die Suche, um weitere Orte auszuwählen.",
58
+ "en": "This deck is done. Adjust your picks or use search to choose more places.",
59
+ "fr": "Cette pile est terminee. Ajustez vos choix ou utilisez la recherche.",
60
+ "sw": "Seti hii imekwisha. Rekebisha uchaguzi wako au tumia utafutaji kuchagua maeneo zaidi."
61
+ },
62
+ "surveyRenderer.DISCOVERY_SAVE": {
63
+ "de": "Merken",
64
+ "en": "Keep",
65
+ "fr": "Enregistrer",
66
+ "sw": "Hifadhi"
67
+ },
68
+ "surveyRenderer.DISCOVERY_SKIP": {
69
+ "de": "Skip",
70
+ "en": "Skip",
71
+ "fr": "Passer",
72
+ "sw": "Ruka"
73
+ },
74
+ "surveyRenderer.FILTERED_SELECT_EMPTY": {
75
+ "de": "Keine passenden Orte in den gewählten Quartieren gefunden.",
76
+ "en": "No matching places found in the selected districts.",
77
+ "fr": "Aucun lieu correspondant n'a ete trouve dans les quartiers choisis.",
78
+ "sw": "Hakuna maeneo yanayolingana yaliyopatikana katika mitaa iliyochaguliwa."
79
+ },
80
+ "surveyRenderer.FILTERED_SELECT_MAX_REACHED": {
81
+ "de": "{{max}} Orte sind ausgewählt. Entferne einen Ort, wenn du deine Auswahl ändern möchtest.",
82
+ "en": "{{max}} places are selected. Remove one if you want to change your selection.",
83
+ "fr": "{{max}} lieux sont selectionnes. Retirez-en un pour modifier votre selection.",
84
+ "sw": "Maeneo {{max}} yamechaguliwa. Ondoa moja ikiwa unataka kubadilisha uchaguzi wako."
85
+ },
86
+ "surveyRenderer.FILTERED_SELECT_REQUIRED": {
87
+ "de": "Wähle zuerst mindestens ein Quartier aus, damit wir passende Orte anzeigen können.",
88
+ "en": "Choose at least one district first so we can show matching places.",
89
+ "fr": "Choisissez d'abord au moins un quartier pour voir des lieux adaptes.",
90
+ "sw": "Chagua angalau mtaa mmoja kwanza ili tuweze kuonyesha maeneo yanayolingana."
91
+ },
92
+ "surveyRenderer.FILTERED_SELECT_SEARCH_PLACEHOLDER": {
93
+ "de": "Orte in den gewählten Quartieren suchen",
94
+ "en": "Search places in the selected districts",
95
+ "fr": "Rechercher des lieux dans les quartiers choisis",
96
+ "sw": "Tafuta maeneo katika mitaa iliyochaguliwa"
97
+ },
98
+ "surveyRenderer.FIX_ERRORS_BEFORE_CONTINUE": {
99
+ "de": "Bitte korrigiere zuerst die markierten Angaben, bevor du weitergehst.",
100
+ "en": "Please correct the highlighted answers before continuing.",
101
+ "fr": "Veuillez d'abord corriger les reponses signalees avant de continuer.",
102
+ "sw": "Tafadhali rekebisha majibu yaliyowekwa alama kabla ya kuendelea."
103
+ },
104
+ "surveyRenderer.MAP_AUTOCOMPLETE_EMPTY": {
105
+ "de": "Keine passenden Quartiere gefunden",
106
+ "en": "No matching districts found",
107
+ "fr": "Aucun quartier correspondant trouve",
108
+ "sw": "Hakuna mitaa inayolingana iliyopatikana"
109
+ },
110
+ "surveyRenderer.MAP_AUTOCOMPLETE_PLACEHOLDER": {
111
+ "de": "Quartier eingeben oder auswählen",
112
+ "en": "Type or choose a district",
113
+ "fr": "Saisir ou choisir un quartier",
114
+ "sw": "Andika au chagua mtaa"
115
+ },
116
+ "surveyRenderer.MAP_DATA_PENDING": {
117
+ "de": "Die Kartendaten für diese Frage sind noch nicht hinterlegt. Du kannst den Wohnort bereits über die Suche auswählen.",
118
+ "en": "The map data for this question is not connected yet. You can already choose the location through search.",
119
+ "fr": "Les donnees cartographiques pour cette question ne sont pas encore connectees. Vous pouvez deja choisir le lieu via la recherche.",
120
+ "sw": "Data ya ramani kwa swali hili haijaunganishwa bado. Unaweza tayari kuchagua mahali kupitia utafutaji."
121
+ },
122
+ "surveyRenderer.MAP_LOADING": {
123
+ "de": "Kartendaten werden geladen...",
124
+ "en": "Loading map data...",
125
+ "fr": "Chargement des donnees cartographiques...",
126
+ "sw": "Inapakia data ya ramani..."
127
+ },
128
+ "surveyRenderer.MAP_MAX_REACHED": {
129
+ "de": "Du hast bereits {{max}} Quartiere ausgewählt.",
130
+ "en": "You already selected {{max}} districts.",
131
+ "fr": "Vous avez deja selectionne {{max}} quartiers.",
132
+ "sw": "Umeshachagua mitaa {{max}}."
133
+ },
134
+ "surveyRenderer.MAP_MULTI_AUTOCOMPLETE_PLACEHOLDER": {
135
+ "de": "Quartiere eingeben oder auswählen",
136
+ "en": "Type or choose districts",
137
+ "fr": "Saisir ou choisir des quartiers",
138
+ "sw": "Andika au chagua mitaa"
139
+ },
140
+ "surveyRenderer.MAP_ZOOM_IN": {
141
+ "de": "Karte vergrössern",
142
+ "en": "Zoom in",
143
+ "fr": "Agrandir la carte",
144
+ "sw": "Kuza ramani"
145
+ },
146
+ "surveyRenderer.MAP_ZOOM_OUT": {
147
+ "de": "Karte verkleinern",
148
+ "en": "Zoom out",
149
+ "fr": "Reduire la carte",
150
+ "sw": "Punguza ramani"
151
+ },
152
+ "surveyRenderer.MAX_SELECTIONS": {
153
+ "de": "Max. {{count}} Auswahlmöglichkeiten",
154
+ "en": "Maximum {{count}} selections",
155
+ "fr": "Maximum de {{count}} sélections",
156
+ "sw": "Chaguo zisizozidi {{count}}"
157
+ },
158
+ "surveyRenderer.NEXT_QUESTION": {
159
+ "de": "Weiter zur nächsten Frage",
160
+ "en": "Continue to the next question",
161
+ "fr": "Continuer vers la question suivante",
162
+ "sw": "Endelea hadi swali lijalo"
163
+ },
164
+ "surveyRenderer.NPS_EXTREMELY_LIKELY": {
165
+ "de": "Äusserst wahrscheinlich",
166
+ "en": "Extremely likely",
167
+ "fr": "Extrêmement probable",
168
+ "sw": "Inawezekana sana"
169
+ },
170
+ "surveyRenderer.NPS_NOT_LIKELY": {
171
+ "de": "Überhaupt nicht wahrscheinlich",
172
+ "en": "Not at all likely",
173
+ "fr": "Pas du tout probable",
174
+ "sw": "Haiwezekani kabisa"
175
+ },
176
+ "surveyRenderer.PAGE_JUMP": {
177
+ "de": "Zu Abschnitt springen",
178
+ "en": "Jump to section",
179
+ "fr": "Aller a la section",
180
+ "sw": "Ruka hadi sehemu"
181
+ },
182
+ "surveyRenderer.PREVIOUS_QUESTION": {
183
+ "de": "Zurück zur letzten Frage",
184
+ "en": "Back to the previous question",
185
+ "fr": "Retour a la question precedente",
186
+ "sw": "Rudi swali lililopita"
187
+ },
188
+ "surveyRenderer.RANK_EXCLUDE": {
189
+ "de": "{{option}} aus der Rangliste entfernen",
190
+ "en": "Remove {{option}} from ranking",
191
+ "fr": "Retirer {{option}} du classement",
192
+ "sw": "Ondoa {{option}} kwenye mpangilio"
193
+ },
194
+ "surveyRenderer.RANK_INCLUDE": {
195
+ "de": "{{option}} in die Rangliste aufnehmen",
196
+ "en": "Include {{option}} in ranking",
197
+ "fr": "Inclure {{option}} dans le classement",
198
+ "sw": "Jumuisha {{option}} katika mpangilio"
199
+ },
200
+ "surveyRenderer.RANK_MOVE_DOWN": {
201
+ "de": "{{option}} nach unten verschieben",
202
+ "en": "Move {{option}} down",
203
+ "fr": "Déplacer {{option}} vers le bas",
204
+ "sw": "Hamisha {{option}} chini"
205
+ },
206
+ "surveyRenderer.RANK_MOVE_UP": {
207
+ "de": "{{option}} nach oben verschieben",
208
+ "en": "Move {{option}} up",
209
+ "fr": "Déplacer {{option}} vers le haut",
210
+ "sw": "Hamisha {{option}} juu"
211
+ },
212
+ "surveyRenderer.REPEAT_ADD": {
213
+ "de": "Eintrag hinzufügen",
214
+ "en": "Add entry",
215
+ "fr": "Ajouter une entree",
216
+ "sw": "Ongeza ingizo"
217
+ },
218
+ "surveyRenderer.REPEAT_REMOVE": {
219
+ "de": "Eintrag entfernen",
220
+ "en": "Remove",
221
+ "fr": "Supprimer",
222
+ "sw": "Ondoa"
223
+ },
224
+ "surveyRenderer.SECTION_DEFAULT": {
225
+ "de": "Abschnitt {{index}}",
226
+ "en": "Section {{index}}",
227
+ "fr": "Section {{index}}",
228
+ "sw": "Sehemu {{index}}"
229
+ },
230
+ "surveyRenderer.SUBMIT_ERRORS_BODY": {
231
+ "de": "Bitte gehe die betroffenen Abschnitte noch einmal durch. Die Zahl in Klammern zeigt, wie viele Felder dort noch problematisch sind.",
232
+ "en": "Please review the affected sections once more. The number in brackets shows how many fields still need attention there.",
233
+ "fr": "Veuillez revoir les sections concernees. Le nombre entre parentheses indique combien de champs demandent encore votre attention.",
234
+ "sw": "Tafadhali pitia sehemu zilizoathiriwa mara nyingine. Nambari kwenye mabano inaonyesha ni maeneo mangapi bado yanahitaji umakini."
235
+ },
236
+ "surveyRenderer.SUBMIT_ERRORS_TITLE": {
237
+ "de": "Vor dem Absenden fehlen noch Angaben.",
238
+ "en": "Some answers still need attention before submission.",
239
+ "fr": "Certaines reponses demandent encore votre attention avant l'envoi.",
240
+ "sw": "Majibu fulani bado yanahitaji umakini kabla ya kutuma."
241
+ }
242
+ };
@@ -0,0 +1,64 @@
1
+ const MAX_LONGEST_EDGE = 1600;
2
+ const OUTPUT_QUALITY = 0.8;
3
+ function isHeicOrHeif(file) {
4
+ const type = file.type.toLowerCase();
5
+ const filename = file.name.toLowerCase();
6
+ return type === "image/heic" || type === "image/heif" || /\.hei[cf]$/.test(filename);
7
+ }
8
+ function encodeCanvas(canvas, type) {
9
+ return new Promise((resolve, reject) => {
10
+ try {
11
+ canvas.toBlob((blob) => {
12
+ if (blob)
13
+ resolve(blob);
14
+ else
15
+ reject(new Error(`Unable to encode image as ${type}.`));
16
+ }, type, OUTPUT_QUALITY);
17
+ }
18
+ catch (error) {
19
+ reject(error);
20
+ }
21
+ });
22
+ }
23
+ function outputFilename(filename, mimeType) {
24
+ const extension = mimeType === "image/webp" ? "webp" : "jpg";
25
+ const dotIndex = filename.lastIndexOf(".");
26
+ const baseName = dotIndex > 0 ? filename.slice(0, dotIndex) : filename || "image";
27
+ return `${baseName}.${extension}`;
28
+ }
29
+ /**
30
+ * Downscale large browser-decodable images and return an upload-ready File.
31
+ * HEIC/HEIF resolves { ok: false, reason: "unsupported_format" } before decode work.
32
+ */
33
+ export async function downscaleImage(file) {
34
+ var _a;
35
+ if (isHeicOrHeif(file))
36
+ return { ok: false, reason: "unsupported_format" };
37
+ const bitmap = await createImageBitmap(file, { imageOrientation: "from-image" });
38
+ try {
39
+ const longestEdge = Math.max(bitmap.width, bitmap.height);
40
+ if (longestEdge <= MAX_LONGEST_EDGE)
41
+ return file;
42
+ const scale = MAX_LONGEST_EDGE / longestEdge;
43
+ const width = Math.round(bitmap.width * scale);
44
+ const height = Math.round(bitmap.height * scale);
45
+ const canvas = document.createElement("canvas");
46
+ canvas.width = width;
47
+ canvas.height = height;
48
+ canvas.getContext("2d").drawImage(bitmap, 0, 0, width, height);
49
+ const webp = await encodeCanvas(canvas, "image/webp").catch(() => null);
50
+ const encoded = (webp === null || webp === void 0 ? void 0 : webp.type) === "image/webp"
51
+ ? webp
52
+ : await encodeCanvas(canvas, "image/jpeg");
53
+ if (encoded.type !== "image/jpeg" && encoded.type !== "image/webp") {
54
+ throw new Error("The browser cannot encode this image as WebP or JPEG.");
55
+ }
56
+ return new File([encoded], outputFilename(file.name, encoded.type), {
57
+ type: encoded.type,
58
+ lastModified: file.lastModified,
59
+ });
60
+ }
61
+ finally {
62
+ (_a = bitmap.close) === null || _a === void 0 ? void 0 : _a.call(bitmap);
63
+ }
64
+ }
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { default as SurveyRenderer } from './SurveyRenderer';
2
+ export { default as SurveyCompletionLayout } from './SurveyCompletionLayout';
3
+ export { applyQuestionDefaults } from './questionDefaults';
4
+ export { resolveQuestionWidget, mergeQuestionUiConfig, normalizeLabel } from './questionUiConfig';
5
+ export { getGroupsByName, getStagedGroupQuestionNames, mergeStagedAnswers } from './stagedGroups';
6
+ export { interpolateSurveyText } from './localizeSurvey';
7
+ export { discoveryDatasetRegistry, registerDiscoveryDataset, getDiscoveryDataset } from './discoveryDatasets';
8
+ export { buildDiscoveryItems, rankDiscoveryItems, resolveDiscoveryStrategy, summarizeDiscoveryDataset } from './discoveryEngine';
9
+ export { geoDatasetRegistry, registerGeoDataset, getGeoDataset } from './geoDatasets';
10
+ export { downscaleImage } from './imageDownscale';
11
+ export { surveyRendererTranslations } from './i18n/surveyRendererTranslations';
@@ -0,0 +1,24 @@
1
+ export function localizeSurveyValue(value, language, fallbackLanguage = "de") {
2
+ if (!value) {
3
+ return "";
4
+ }
5
+ if (typeof value === "string") {
6
+ return value;
7
+ }
8
+ return value[language] || value[fallbackLanguage] || Object.values(value)[0] || "";
9
+ }
10
+ export function interpolateSurveyText(template, answers = {}) {
11
+ if (!template) {
12
+ return "";
13
+ }
14
+ return String(template).replace(/\$\{([^}]+)\}/g, (_match, fieldName) => {
15
+ const rawValue = answers[fieldName];
16
+ if (Array.isArray(rawValue)) {
17
+ return rawValue.join(", ");
18
+ }
19
+ if (rawValue === undefined || rawValue === null) {
20
+ return "";
21
+ }
22
+ return String(rawValue);
23
+ });
24
+ }
@@ -0,0 +1,13 @@
1
+ export function applyQuestionDefaults(definition, answers = {}) {
2
+ const nextAnswers = Object.assign({}, (answers || {}));
3
+ const questions = Array.isArray(definition === null || definition === void 0 ? void 0 : definition.questions) ? definition.questions : [];
4
+ questions.forEach((question) => {
5
+ if (!(question === null || question === void 0 ? void 0 : question.name) || question.default === undefined || question.default === null || question.default === "") {
6
+ return;
7
+ }
8
+ if (!Object.prototype.hasOwnProperty.call(nextAnswers, question.name)) {
9
+ nextAnswers[question.name] = question.default;
10
+ }
11
+ });
12
+ return nextAnswers;
13
+ }
@@ -0,0 +1,43 @@
1
+ function tokenizeAppearance(appearance) {
2
+ return String(appearance || "")
3
+ .split(/\s+/)
4
+ .map((token) => token.trim())
5
+ .filter(Boolean);
6
+ }
7
+ export function resolveQuestionWidget(question, configured = null) {
8
+ const appearanceTokens = tokenizeAppearance(question === null || question === void 0 ? void 0 : question.appearance);
9
+ if (appearanceTokens.includes("map-select")) {
10
+ return "map-select";
11
+ }
12
+ if (appearanceTokens.includes("discovery-select")) {
13
+ return "discovery-select";
14
+ }
15
+ if (appearanceTokens.includes("filtered-select")) {
16
+ return "filtered-select";
17
+ }
18
+ return (configured === null || configured === void 0 ? void 0 : configured.widget) || null;
19
+ }
20
+ export function resolveQuestionUiConfig(question, questionUiConfigByName = {}) {
21
+ const configured = (questionUiConfigByName === null || questionUiConfigByName === void 0 ? void 0 : questionUiConfigByName[question === null || question === void 0 ? void 0 : question.name]) || null;
22
+ const widget = resolveQuestionWidget(question, configured);
23
+ if (!configured && !widget) {
24
+ return null;
25
+ }
26
+ return Object.assign(Object.assign({ autocomplete: true, emptyStateMode: "options-only" }, configured), { widget });
27
+ }
28
+ export function mergeQuestionUiConfig(siteQuestionUiConfig = {}, persistedQuestionUiConfig = {}) {
29
+ const merged = Object.assign({}, siteQuestionUiConfig);
30
+ Object.entries(persistedQuestionUiConfig || {}).forEach(([questionName, overrideConfig]) => {
31
+ merged[questionName] = Object.assign(Object.assign(Object.assign({}, (merged[questionName] || {})), (overrideConfig || {})), { gating: Object.assign(Object.assign({}, ((merged[questionName] || {}).gating || {})), ((overrideConfig || {}).gating || {})), strategy: Object.assign(Object.assign(Object.assign({}, ((merged[questionName] || {}).strategy || {})), ((overrideConfig || {}).strategy || {})), { scoreFormula: Object.assign(Object.assign({}, (((merged[questionName] || {}).strategy || {}).scoreFormula || {})), ((((overrideConfig || {}).strategy) || {}).scoreFormula || {})) }) });
32
+ });
33
+ return merged;
34
+ }
35
+ export function normalizeLabel(value) {
36
+ return String(value || "")
37
+ .trim()
38
+ .toLowerCase()
39
+ .normalize("NFD")
40
+ .replace(/[\u0300-\u036f]/g, "")
41
+ .replace(/[^a-z0-9]+/g, " ")
42
+ .trim();
43
+ }