@picked/svelte 0.0.1-dev.1765991816856 → 0.0.1-dev.1767591456821
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.
- package/dist/form/form.svelte.d.ts +25 -0
- package/dist/form/form.svelte.ts +340 -0
- package/dist/form/index.d.ts +1 -16
- package/dist/form/index.js +1 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/step.d.ts +18 -3
- package/dist/hooks/step.js +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -1
- package/dist/render/browser.js +1 -1
- package/dist/utils/ErrorBoundary.svelte +27 -0
- package/package.json +2 -2
- package/dist/components/LazyImage.svelte +0 -196
- package/dist/components/index.d.ts +0 -1
- package/dist/components/index.js +0 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CommitResponses, Field, ValidationError } from '@picked/client';
|
|
2
|
+
import { EventCallable, Store } from 'effector';
|
|
3
|
+
import { FieldWithValue } from '../hooks';
|
|
4
|
+
interface GetFormOptions {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
fields: Store<Field[] | FieldWithValue[]>;
|
|
8
|
+
}
|
|
9
|
+
type SuccessCallback = (options: {
|
|
10
|
+
stopNavigation: EventCallable<void>;
|
|
11
|
+
}) => void | Promise<void>;
|
|
12
|
+
type ErrorCallback = (errors: ValidationError[]) => void | Promise<void>;
|
|
13
|
+
type SubmitCallback = (data: CommitResponses) => CommitResponses | void | Promise<CommitResponses | void>;
|
|
14
|
+
type SubmitEventWithCurrentTarget = SubmitEvent & {
|
|
15
|
+
currentTarget: EventTarget & HTMLFormElement;
|
|
16
|
+
};
|
|
17
|
+
export declare function getForm({ id: formId, title, fields }: GetFormOptions): {
|
|
18
|
+
form: {
|
|
19
|
+
onsubmit: EventCallable<SubmitEventWithCurrentTarget>;
|
|
20
|
+
};
|
|
21
|
+
onFormSubmit: (callback: SubmitCallback) => void;
|
|
22
|
+
onFormSuccess: (callback: SuccessCallback) => void;
|
|
23
|
+
onFormError: (callback: ErrorCallback) => void;
|
|
24
|
+
};
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CommitResponses,
|
|
3
|
+
ExperienceStep,
|
|
4
|
+
Field,
|
|
5
|
+
FieldValue,
|
|
6
|
+
ValidationError,
|
|
7
|
+
ExperienceInstance,
|
|
8
|
+
} from '@picked/client'
|
|
9
|
+
import {
|
|
10
|
+
attach,
|
|
11
|
+
createEffect,
|
|
12
|
+
createEvent,
|
|
13
|
+
createStore,
|
|
14
|
+
sample,
|
|
15
|
+
split,
|
|
16
|
+
type EventCallable,
|
|
17
|
+
type Store,
|
|
18
|
+
} from 'effector'
|
|
19
|
+
import { getPicked, getExperience, type FieldWithValue } from '../hooks'
|
|
20
|
+
|
|
21
|
+
interface Response {
|
|
22
|
+
questionId: Field['id']
|
|
23
|
+
value: FieldValue
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface GetFormOptions {
|
|
27
|
+
id: string
|
|
28
|
+
title: string
|
|
29
|
+
fields: Store<Field[] | FieldWithValue[]>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type SuccessCallback = (options: { stopNavigation: EventCallable<void> }) => void | Promise<void>
|
|
33
|
+
type ErrorCallback = (errors: ValidationError[]) => void | Promise<void>
|
|
34
|
+
type SubmitCallback = (
|
|
35
|
+
data: CommitResponses,
|
|
36
|
+
) => CommitResponses | void | Promise<CommitResponses | void>
|
|
37
|
+
|
|
38
|
+
type SubmitEventWithCurrentTarget = SubmitEvent & { currentTarget: EventTarget & HTMLFormElement }
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Factory function to create a callback observer with automatic cleanup
|
|
42
|
+
* Reduces duplication across success/error/submit callback patterns
|
|
43
|
+
*/
|
|
44
|
+
function createObserver<TCallback extends (...args: any[]) => any>(
|
|
45
|
+
executor: (callbacks: TCallback[], ...params: Parameters<TCallback>) => Promise<any>,
|
|
46
|
+
) {
|
|
47
|
+
let callbacks: TCallback[] = []
|
|
48
|
+
|
|
49
|
+
const executeFx = createEffect(async (...params: Parameters<TCallback>) => {
|
|
50
|
+
return await executor(callbacks, ...params)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const register = (callback: TCallback) => {
|
|
54
|
+
$effect(() => {
|
|
55
|
+
callbacks.push(callback)
|
|
56
|
+
return () => {
|
|
57
|
+
callbacks = callbacks.filter((cb) => cb !== callback)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { executeFx, register }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getForm({ id: formId, title, fields }: GetFormOptions) {
|
|
66
|
+
const client = getPicked()
|
|
67
|
+
const experienceInstance: ExperienceInstance = getExperience()
|
|
68
|
+
const { currentStepStore: stepStore, goToNextStep } = experienceInstance
|
|
69
|
+
|
|
70
|
+
const submitEvent = createEvent<SubmitEventWithCurrentTarget>()
|
|
71
|
+
const formSuccessEvent = createEvent<void>()
|
|
72
|
+
const formErrorEvent = createEvent<ValidationError[]>()
|
|
73
|
+
const stopNavigationEvent = createEvent<void>()
|
|
74
|
+
const allCallbacksCompleteEvent = createEvent<void>()
|
|
75
|
+
|
|
76
|
+
const stopNavigationStore = createStore(false)
|
|
77
|
+
stopNavigationStore
|
|
78
|
+
.on(stopNavigationEvent, () => true) // Set to true when stopNavigation is used in onFormSuccess
|
|
79
|
+
.on(submitEvent, () => false) // Reset to false on new submit
|
|
80
|
+
|
|
81
|
+
// Create observers for each callback type
|
|
82
|
+
const successObserver = createObserver<SuccessCallback>(async (callbacks, options) => {
|
|
83
|
+
await Promise.all(callbacks.map((callback) => Promise.resolve(callback(options))))
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const errorObserver = createObserver<ErrorCallback>(async (callbacks, errors) => {
|
|
87
|
+
await Promise.all(callbacks.map((callback) => Promise.resolve(callback(errors))))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const submitObserver = createObserver<SubmitCallback>(async (callbacks, data) => {
|
|
91
|
+
let transformedData = data
|
|
92
|
+
|
|
93
|
+
// Apply each callback's transformation sequentially
|
|
94
|
+
for (const callback of callbacks) {
|
|
95
|
+
const result = await Promise.resolve(callback(transformedData))
|
|
96
|
+
// If callback returns data, use it; otherwise keep current
|
|
97
|
+
if (result !== undefined) {
|
|
98
|
+
transformedData = result
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return transformedData
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const commitFx = createEffect((responses: CommitResponses) => client.responses.commit(responses))
|
|
106
|
+
|
|
107
|
+
const onSuccessFx = attach({
|
|
108
|
+
source: { step: stepStore },
|
|
109
|
+
effect: createEffect(async ({ step }: { step: ExperienceStep | null }) => {
|
|
110
|
+
client.eventBus({
|
|
111
|
+
type: 'form_submit_success',
|
|
112
|
+
form: { id: formId, title },
|
|
113
|
+
experience: experienceInstance,
|
|
114
|
+
step: step ?? undefined,
|
|
115
|
+
})
|
|
116
|
+
}),
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const onErrorFx = attach({
|
|
120
|
+
source: { step: stepStore },
|
|
121
|
+
mapParams: (params: { errors: ValidationError[] }, source) => ({
|
|
122
|
+
step: source.step,
|
|
123
|
+
errors: params.errors,
|
|
124
|
+
}),
|
|
125
|
+
effect: createEffect(
|
|
126
|
+
async ({ step, errors }: { step: ExperienceStep | null; errors: ValidationError[] }) => {
|
|
127
|
+
client.eventBus({
|
|
128
|
+
type: 'form_submit_error',
|
|
129
|
+
form: { id: formId, title },
|
|
130
|
+
exception: { reason: errors },
|
|
131
|
+
experience: experienceInstance,
|
|
132
|
+
step: step ?? undefined,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
errors.forEach((exception) => {
|
|
136
|
+
client.eventBus({
|
|
137
|
+
type: 'field_validation_error',
|
|
138
|
+
exception: { reason: [exception] },
|
|
139
|
+
experience: experienceInstance,
|
|
140
|
+
step: step ?? undefined,
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
return errors
|
|
145
|
+
},
|
|
146
|
+
),
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
sample({
|
|
150
|
+
clock: submitEvent,
|
|
151
|
+
source: { step: stepStore },
|
|
152
|
+
fn: ({ step }) => ({ formId, title, step }),
|
|
153
|
+
target: createEffect(
|
|
154
|
+
({
|
|
155
|
+
formId: formIdParam,
|
|
156
|
+
title: titleParam,
|
|
157
|
+
step,
|
|
158
|
+
}: {
|
|
159
|
+
formId: string
|
|
160
|
+
title: string
|
|
161
|
+
step: ExperienceStep | null
|
|
162
|
+
}) => {
|
|
163
|
+
client.eventBus({
|
|
164
|
+
type: 'form_submit',
|
|
165
|
+
form: { id: formIdParam, title: titleParam },
|
|
166
|
+
experience: experienceInstance,
|
|
167
|
+
step: step ?? undefined,
|
|
168
|
+
})
|
|
169
|
+
},
|
|
170
|
+
),
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
sample({
|
|
174
|
+
clock: submitEvent,
|
|
175
|
+
source: {
|
|
176
|
+
fields: client.fields.listStore,
|
|
177
|
+
stepFields: fields,
|
|
178
|
+
},
|
|
179
|
+
fn: ({ stepFields }, event) => {
|
|
180
|
+
event.preventDefault()
|
|
181
|
+
|
|
182
|
+
const formData = new FormData(event.currentTarget)
|
|
183
|
+
const formDataEntries = Array.from(formData.entries())
|
|
184
|
+
|
|
185
|
+
const responses: { data: Record<Field['id'], FieldValue>; push: (item: Response) => void } = {
|
|
186
|
+
data: {},
|
|
187
|
+
push(item: Response) {
|
|
188
|
+
const questionId = item.questionId
|
|
189
|
+
const existing = this.data[questionId]
|
|
190
|
+
|
|
191
|
+
if (!existing) {
|
|
192
|
+
// No matching questionId, just add the new item
|
|
193
|
+
this.data[questionId] = item.value
|
|
194
|
+
// If both values are arrays, merge them
|
|
195
|
+
} else if (Array.isArray(existing) && Array.isArray(item.value)) {
|
|
196
|
+
if (Array.isArray(existing) && Array.isArray(item.value)) {
|
|
197
|
+
// Check what type FieldValue expects and convert accordingly
|
|
198
|
+
if (typeof existing[0] === 'string') {
|
|
199
|
+
this.data[questionId] = [...existing, ...item.value].map(String)
|
|
200
|
+
} else if (typeof existing[0] === 'number') {
|
|
201
|
+
this.data[questionId] = [...existing, ...item.value].map(Number)
|
|
202
|
+
} else if (typeof existing[0] === 'boolean') {
|
|
203
|
+
this.data[questionId] = [...existing, ...item.value].map((v) => Boolean(v))
|
|
204
|
+
} else {
|
|
205
|
+
this.data[questionId] = item.value
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
// Otherwise, override the existing value
|
|
210
|
+
this.data[questionId] = item.value
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
stepFields.forEach((field) => {
|
|
216
|
+
let fieldEntries = formDataEntries.filter(([key]) => key === field.id)
|
|
217
|
+
|
|
218
|
+
if (fieldEntries.length === 0) {
|
|
219
|
+
fieldEntries = [[field.id, '']]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fieldEntries.forEach(([, value]) => {
|
|
223
|
+
const questionId = field.id
|
|
224
|
+
|
|
225
|
+
if (value === '') {
|
|
226
|
+
responses.push({
|
|
227
|
+
questionId,
|
|
228
|
+
value: null,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
switch (field.type) {
|
|
235
|
+
case 'text':
|
|
236
|
+
responses.push({
|
|
237
|
+
questionId,
|
|
238
|
+
value: field.multiple ? [String(value)] : String(value),
|
|
239
|
+
})
|
|
240
|
+
break
|
|
241
|
+
case 'number':
|
|
242
|
+
responses.push({
|
|
243
|
+
questionId,
|
|
244
|
+
value: field.multiple ? [Number(value)] : Number(value),
|
|
245
|
+
})
|
|
246
|
+
break
|
|
247
|
+
case 'boolean':
|
|
248
|
+
responses.push({
|
|
249
|
+
questionId,
|
|
250
|
+
value: field.multiple ? [value === 'true'] : value === 'true',
|
|
251
|
+
})
|
|
252
|
+
break
|
|
253
|
+
// case 'object':
|
|
254
|
+
// try {
|
|
255
|
+
// responses.push({
|
|
256
|
+
// questionId,
|
|
257
|
+
// value: field.multiple ? [JSON.parse(String(value))] : JSON.parse(String(value)),
|
|
258
|
+
// })
|
|
259
|
+
// } catch {
|
|
260
|
+
// responses.push({
|
|
261
|
+
// questionId,
|
|
262
|
+
// value: field.multiple ? [null] : null,
|
|
263
|
+
// })
|
|
264
|
+
// }
|
|
265
|
+
|
|
266
|
+
// break
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
return responses.data
|
|
272
|
+
},
|
|
273
|
+
target: submitObserver.executeFx,
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
// Pass transformed data from submit callbacks to commit
|
|
277
|
+
sample({
|
|
278
|
+
clock: submitObserver.executeFx.doneData,
|
|
279
|
+
target: commitFx,
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
// Errors are returned from the commit even if it is a success, split to handle errors array
|
|
283
|
+
split({
|
|
284
|
+
source: commitFx.doneData,
|
|
285
|
+
match: {
|
|
286
|
+
hasErrors: ({ errors }) => errors.length > 0,
|
|
287
|
+
},
|
|
288
|
+
cases: {
|
|
289
|
+
hasErrors: onErrorFx,
|
|
290
|
+
__: onSuccessFx,
|
|
291
|
+
},
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
sample({
|
|
295
|
+
clock: onSuccessFx.done,
|
|
296
|
+
target: formSuccessEvent,
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
// When form succeeds, execute all callbacks
|
|
300
|
+
sample({
|
|
301
|
+
clock: formSuccessEvent,
|
|
302
|
+
fn: () => ({ stopNavigation: stopNavigationEvent }), // Pass event reference to callbacks (not calling it)
|
|
303
|
+
target: successObserver.executeFx,
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// When all callbacks complete, trigger navigation check
|
|
307
|
+
sample({
|
|
308
|
+
clock: successObserver.executeFx.done,
|
|
309
|
+
target: allCallbacksCompleteEvent,
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
// Check navigation after all callbacks complete
|
|
313
|
+
sample({
|
|
314
|
+
clock: allCallbacksCompleteEvent,
|
|
315
|
+
source: stopNavigationStore,
|
|
316
|
+
filter: (stop) => !stop,
|
|
317
|
+
target: goToNextStep,
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
// Declarative event wiring for error flow
|
|
321
|
+
sample({
|
|
322
|
+
clock: onErrorFx.doneData,
|
|
323
|
+
target: formErrorEvent,
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
// When form errors, execute all error callbacks
|
|
327
|
+
sample({
|
|
328
|
+
clock: formErrorEvent,
|
|
329
|
+
target: errorObserver.executeFx,
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
form: {
|
|
334
|
+
onsubmit: submitEvent,
|
|
335
|
+
},
|
|
336
|
+
onFormSubmit: submitObserver.register,
|
|
337
|
+
onFormSuccess: successObserver.register,
|
|
338
|
+
onFormError: errorObserver.register,
|
|
339
|
+
}
|
|
340
|
+
}
|
package/dist/form/index.d.ts
CHANGED
|
@@ -1,16 +1 @@
|
|
|
1
|
-
|
|
2
|
-
interface GetFormOptions {
|
|
3
|
-
id: string;
|
|
4
|
-
title: string;
|
|
5
|
-
onSuccess?: () => void | Promise<void>;
|
|
6
|
-
onError?: (errors: ValidationError[]) => void | Promise<void>;
|
|
7
|
-
}
|
|
8
|
-
type SubmitEventWithCurrentTarget = SubmitEvent & {
|
|
9
|
-
currentTarget: EventTarget & HTMLFormElement;
|
|
10
|
-
};
|
|
11
|
-
export declare function getForm({ id: formId, title, onError, onSuccess }: GetFormOptions): {
|
|
12
|
-
form: {
|
|
13
|
-
onsubmit: import('effector').EventCallable<SubmitEventWithCurrentTarget>;
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
export {};
|
|
1
|
+
export { getForm } from './form.svelte';
|
package/dist/form/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{getForm as e}from"./form.svelte";export{e as getForm};
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { getPicked, getExperience } from './client';
|
|
2
|
-
export { getStep, setStepContext } from './step';
|
|
2
|
+
export { getStep, setStepContext, type FieldWithValue } from './step';
|
|
3
3
|
export { useUnit, getContextScope, setContextScope } from './stores';
|
|
4
4
|
export { createSessionStorageStore, createLocalStorageStore } from './storage.js';
|
|
5
5
|
export { getTrackRecommendationPresentEvent } from './events';
|
package/dist/hooks/step.d.ts
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import { Store } from 'effector';
|
|
2
2
|
import { ExperienceStep, Field, Response } from '@picked/client';
|
|
3
|
-
|
|
3
|
+
import { getForm } from '../form';
|
|
4
|
+
export type FieldWithValue = Field & {
|
|
4
5
|
value: Response | undefined;
|
|
5
6
|
};
|
|
6
7
|
export interface StepContext {
|
|
7
8
|
step: Store<ExperienceStep>;
|
|
8
9
|
fields: Store<FieldWithValue[]>;
|
|
9
10
|
isActiveStep: Store<boolean>;
|
|
11
|
+
form: ReturnType<typeof getForm>;
|
|
10
12
|
}
|
|
11
|
-
export declare function getStep():
|
|
13
|
+
export declare function getStep(): {
|
|
14
|
+
form: {
|
|
15
|
+
onsubmit: import('effector').EventCallable<SubmitEvent & {
|
|
16
|
+
currentTarget: EventTarget & HTMLFormElement;
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
19
|
+
onFormSubmit: (callback: (data: import('@picked/client').CommitResponses) => import('@picked/client').CommitResponses | void | Promise<import('@picked/client').CommitResponses | void>) => void;
|
|
20
|
+
onFormSuccess: (callback: (options: {
|
|
21
|
+
stopNavigation: import('effector').EventCallable<void>;
|
|
22
|
+
}) => void | Promise<void>) => void;
|
|
23
|
+
onFormError: (callback: (errors: import('@picked/client').ValidationError[]) => void | Promise<void>) => void;
|
|
24
|
+
step: Store<ExperienceStep>;
|
|
25
|
+
fields: Store<FieldWithValue[]>;
|
|
26
|
+
isActiveStep: Store<boolean>;
|
|
27
|
+
};
|
|
12
28
|
export declare function setStepContext(stepId: string): StepContext;
|
|
13
|
-
export {};
|
package/dist/hooks/step.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getContext as
|
|
1
|
+
import{getContext as h,setContext as w}from"svelte";import{createStore as C,combine as l,attach as F,createEffect as A,sample as m}from"effector";import{getStepFields as T}from"@picked/client";import{STEP_CONTEXT_KEY as d}from"../context.js";import{getStepRouter as b}from"../router/context.js";import{getForm as P}from"../form/form.svelte";import{getExperience as _,getPicked as $}from"./client.js";function V(){const t=h(d),{form:i}=t;return{...t,...i}}function W(t){const i=_(),n=$(),{activeStep:u}=b(),f=u.map(e=>(e==null?void 0:e.id)===t),s=i.treeStore.map(e=>{const r=e.steps.find(o=>o.id===t);if(!r)throw new Error(`Step with id "${t}" not found in the experience tree.`);return r}),c=C([]),S=l({fields:c,responses:n.responses.listStore},({fields:e,responses:r})=>e.map(o=>{var p;const v=(p=r.find(E=>E.fieldId===o.id))==null?void 0:p.value;return{...o,value:v}})),a=F({source:l({step:s,fields:n.fields.listStore}),effect:A(async({step:{fieldIds:e},fields:r})=>await T(n,r.filter(o=>e.includes(o.id))))}),{label:g}=s.getState(),x=P({id:`${t}`,title:g||t,fields:c});return m({source:{isActiveStep:f},filter:({isActiveStep:e})=>e,target:a}),m({source:a.doneData,target:c}),w(d,{step:s,fields:S,isActiveStep:f,form:x})}export{V as getStep,W as setStepContext};
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{adaptSvelteComponent as
|
|
1
|
+
import{adaptSvelteComponent as t,isValidAdaptedComponent as p}from"./component.js";export{t as adaptSvelteComponent,p as isValidAdaptedComponent};
|
package/dist/render/browser.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{mount as
|
|
1
|
+
import{mount as d}from"svelte";import{isValidAdaptedComponent as E}from"../component.js";import{CLIENT_CONTEXT_KEY as f,EXPERIENCE_CONTEXT_KEY as y}from"../context.js";import{mergeArrays as u}from"../utils/array.js";import h from"../utils/ErrorBoundary.svelte";function C(n,e){return new Map([[f,n],[y,e]])}const k=async(n,e,s,o,i)=>{var p,a;if(!E(e))throw new Error("The component is invalid.");const c=u((p=e.style)==null?void 0:p.sheets,i.stylesheets),m=u((a=e.style)==null?void 0:a.css,i.css);c.length&&c.forEach(r=>{const t=document.createElement("link");t.setAttribute("rel","stylesheet"),t.setAttribute("href",r),t.setAttribute("type","text/css"),t.setAttribute("picked-injected","true"),n.appendChild(t)}),m.length&&m.forEach(r=>{const t=document.createElement("style");t.textContent=r,t.setAttribute("type","text/css"),t.setAttribute("picked-injected","true"),n.appendChild(t)});const l=r=>{d(h,{target:n,props:{component:r,componentProps:{client:s,experience:o}},context:C(s,o)})};if(e.component instanceof Promise){const{default:r}=await e.component;l(r);return}l(e.component)},N=n=>{};export{C as createContext,k as renderSvelteComponent,N as unRenderSvelteComponent};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ComponentWithProps } from '../component'
|
|
3
|
+
import { getPicked } from '../hooks'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
component: ComponentWithProps
|
|
7
|
+
componentProps?: Record<string, unknown>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { component: InnerComponent, componentProps }: Props = $props()
|
|
11
|
+
|
|
12
|
+
function handleError(error: unknown) {
|
|
13
|
+
const client = getPicked()
|
|
14
|
+
|
|
15
|
+
// We still log to console for visibility during development
|
|
16
|
+
// eslint-disable-next-line no-console
|
|
17
|
+
console.error(error)
|
|
18
|
+
|
|
19
|
+
client.eventBus({
|
|
20
|
+
type: 'exception',
|
|
21
|
+
level: 'error',
|
|
22
|
+
data: error,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<svelte:boundary onerror={handleError}><InnerComponent {...componentProps} /></svelte:boundary>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@picked/svelte",
|
|
3
|
-
"version": "0.0.1-dev.
|
|
3
|
+
"version": "0.0.1-dev.1767591456821",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"d3": "^7.9.0"
|
|
6
6
|
},
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
],
|
|
76
76
|
"module": "./dist/index.js",
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@picked/client": "0.0.1-dev.
|
|
78
|
+
"@picked/client": "0.0.1-dev.1767591456821",
|
|
79
79
|
"effector": "^23.3.0",
|
|
80
80
|
"svelte": "^5.34.3"
|
|
81
81
|
},
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { HTMLImgAttributes } from 'svelte/elements'
|
|
3
|
-
import { onMount } from 'svelte'
|
|
4
|
-
|
|
5
|
-
type CommonProps = Omit<HTMLImgAttributes, 'src' | 'loading' | 'width' | 'height'> & {
|
|
6
|
-
src: string
|
|
7
|
-
loading?: 'eager' | 'lazy'
|
|
8
|
-
alt: string
|
|
9
|
-
sizes?: string
|
|
10
|
-
objectFit?: string
|
|
11
|
-
loader?: (params: { src: string; width: number }) => string | undefined
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type FillProps = {
|
|
15
|
-
fill: true
|
|
16
|
-
width?: never
|
|
17
|
-
height?: never
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
type FixedSizeProps = {
|
|
21
|
-
fill?: false
|
|
22
|
-
width: number
|
|
23
|
-
height: number
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
type Props = (CommonProps & FillProps) | (CommonProps & FixedSizeProps)
|
|
27
|
-
|
|
28
|
-
const {
|
|
29
|
-
src,
|
|
30
|
-
width,
|
|
31
|
-
height,
|
|
32
|
-
loading = 'lazy',
|
|
33
|
-
alt,
|
|
34
|
-
sizes = '100vw',
|
|
35
|
-
objectFit = 'cover',
|
|
36
|
-
fill = false,
|
|
37
|
-
loader,
|
|
38
|
-
...restProps
|
|
39
|
-
}: Props = $props()
|
|
40
|
-
|
|
41
|
-
let imgElement = $state<HTMLImageElement | undefined>(undefined)
|
|
42
|
-
let placeholderElement = $state<HTMLDivElement | undefined>(undefined)
|
|
43
|
-
let isLoaded = $state(false)
|
|
44
|
-
let isInView = $state(false)
|
|
45
|
-
let observer: IntersectionObserver | undefined
|
|
46
|
-
|
|
47
|
-
const aspectRatio = $derived.by(() =>
|
|
48
|
-
!fill && typeof width === 'number' && typeof height === 'number' && height !== 0
|
|
49
|
-
? width / height
|
|
50
|
-
: undefined,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
const SUPPORTED_SIZES = [
|
|
54
|
-
16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840,
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
function findClosestSize(targetSize: number): number {
|
|
58
|
-
const roundedTargetSize = Math.round(targetSize)
|
|
59
|
-
return SUPPORTED_SIZES.reduce(
|
|
60
|
-
(prev, curr) =>
|
|
61
|
-
Math.abs(curr - roundedTargetSize) < Math.abs(prev - roundedTargetSize) ? curr : prev,
|
|
62
|
-
SUPPORTED_SIZES[0],
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function isConvincelyDomain(url: URL): boolean {
|
|
67
|
-
try {
|
|
68
|
-
return url.hostname.endsWith('.convincely.io')
|
|
69
|
-
} catch {
|
|
70
|
-
return false
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function getOptimizedImageUrl(imageSrc: string, targetWidth: number): string {
|
|
75
|
-
try {
|
|
76
|
-
// If a custom loader is provided, use it first
|
|
77
|
-
if (loader) {
|
|
78
|
-
const result = loader({ src: imageSrc, width: targetWidth })
|
|
79
|
-
if (result !== undefined) {
|
|
80
|
-
return result
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const url = new URL(imageSrc, import.meta.url)
|
|
85
|
-
|
|
86
|
-
// Handle Convincely domain optimization through the image optimization service
|
|
87
|
-
if (isConvincelyDomain(url)) {
|
|
88
|
-
const encodedUrl = encodeURIComponent(url.toString())
|
|
89
|
-
const optimizedSize = findClosestSize(targetWidth)
|
|
90
|
-
return `https://img.convincely.io/_next/image?url=${encodedUrl}&w=${optimizedSize}&q=75`
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// For other domains, return the original URL
|
|
94
|
-
return getImgUrl(imageSrc)
|
|
95
|
-
} catch {
|
|
96
|
-
// If URL parsing fails, fall back to original behavior
|
|
97
|
-
return getImgUrl(imageSrc)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function getImgUrl(imageSrc: string): string {
|
|
102
|
-
// Skip base64 images
|
|
103
|
-
if (imageSrc.startsWith('data:')) {
|
|
104
|
-
return imageSrc
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const baseUrl = new URL(import.meta.url).origin
|
|
108
|
-
return imageSrc.startsWith('http') ? imageSrc : `${baseUrl}${imageSrc}`
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function handleLoad() {
|
|
112
|
-
isLoaded = true
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
onMount(() => {
|
|
116
|
-
if (loading === 'lazy') {
|
|
117
|
-
const elementToObserve = placeholderElement || imgElement
|
|
118
|
-
if (elementToObserve) {
|
|
119
|
-
observer = new IntersectionObserver(
|
|
120
|
-
(entries) => {
|
|
121
|
-
entries.forEach((entry) => {
|
|
122
|
-
if (entry.isIntersecting) {
|
|
123
|
-
isInView = true
|
|
124
|
-
observer?.unobserve(entry.target)
|
|
125
|
-
}
|
|
126
|
-
})
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
rootMargin: '50px',
|
|
130
|
-
},
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
observer.observe(elementToObserve)
|
|
134
|
-
|
|
135
|
-
return () => {
|
|
136
|
-
observer?.disconnect()
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
} else if (loading === 'eager') {
|
|
140
|
-
isInView = true
|
|
141
|
-
}
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
const shouldLoadImage = $derived(loading === 'eager' || isInView)
|
|
145
|
-
|
|
146
|
-
const widthCandidates = $derived.by(() => {
|
|
147
|
-
if (typeof width === 'number' && Number.isFinite(width)) {
|
|
148
|
-
const filtered = SUPPORTED_SIZES.filter((w) => w <= width)
|
|
149
|
-
return filtered.length ? filtered : [width]
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return SUPPORTED_SIZES
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
const srcSet = $derived.by(() =>
|
|
156
|
-
widthCandidates.map((w) => `${getOptimizedImageUrl(src, w)} ${w}w`).join(', '),
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
const fallbackSrc = $derived.by(() => getOptimizedImageUrl(src, widthCandidates[0]))
|
|
160
|
-
|
|
161
|
-
// Only apply fade-in when loading is not eager
|
|
162
|
-
const fadeClasses = $derived.by(() => {
|
|
163
|
-
if (loading === 'eager') return ''
|
|
164
|
-
return `transition-opacity duration-300 ${isLoaded ? 'opacity-100' : 'opacity-0'}`
|
|
165
|
-
})
|
|
166
|
-
</script>
|
|
167
|
-
|
|
168
|
-
{#if shouldLoadImage}
|
|
169
|
-
<img
|
|
170
|
-
{...restProps}
|
|
171
|
-
bind:this={imgElement}
|
|
172
|
-
src={fallbackSrc}
|
|
173
|
-
srcset={srcSet}
|
|
174
|
-
{sizes}
|
|
175
|
-
width={fill ? undefined : width}
|
|
176
|
-
height={fill ? undefined : height}
|
|
177
|
-
{alt}
|
|
178
|
-
style:aspect-ratio={aspectRatio}
|
|
179
|
-
style:object-fit={objectFit}
|
|
180
|
-
style:position={fill ? 'absolute' : undefined}
|
|
181
|
-
style:inset={fill ? 0 : undefined}
|
|
182
|
-
{loading}
|
|
183
|
-
onload={handleLoad}
|
|
184
|
-
class="{fadeClasses} {restProps.class || ''}"
|
|
185
|
-
data-testid="lazy-image-element"
|
|
186
|
-
/>
|
|
187
|
-
{:else}
|
|
188
|
-
<div
|
|
189
|
-
bind:this={placeholderElement}
|
|
190
|
-
style:aspect-ratio={aspectRatio}
|
|
191
|
-
style:position={fill ? 'absolute' : undefined}
|
|
192
|
-
style:inset={fill ? 0 : undefined}
|
|
193
|
-
aria-label={alt}
|
|
194
|
-
data-testid="lazy-image-placeholder"
|
|
195
|
-
></div>
|
|
196
|
-
{/if}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { default as LazyImage } from './LazyImage.svelte';
|
package/dist/components/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{default as f}from"./LazyImage.svelte";export{f as LazyImage};
|