@picked/svelte 0.0.1-dev.1765971320697 → 0.0.1-dev.1767586122445
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 -226
- 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.1767586122445",
|
|
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.1767586122445",
|
|
79
79
|
"effector": "^23.3.0",
|
|
80
80
|
"svelte": "^5.34.3"
|
|
81
81
|
},
|
|
@@ -1,226 +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 isShopifyDomain(url: URL): boolean {
|
|
75
|
-
try {
|
|
76
|
-
return url.hostname === 'cdn.shopify.com'
|
|
77
|
-
} catch {
|
|
78
|
-
return false
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Add dpl param to Convincely domain URLs for deployment-specific routing.
|
|
84
|
-
*/
|
|
85
|
-
function addDplParamIfNeeded(imageSrc: string): string {
|
|
86
|
-
try {
|
|
87
|
-
const url = new URL(imageSrc, import.meta.url)
|
|
88
|
-
|
|
89
|
-
if (isConvincelyDomain(url)) {
|
|
90
|
-
// Check for Vercel deployment ID (works in both browser and server contexts)
|
|
91
|
-
const vercelDeploymentId = typeof import.meta.env !== 'undefined'
|
|
92
|
-
? import.meta.env.VERCEL_DEPLOYMENT_ID
|
|
93
|
-
: undefined
|
|
94
|
-
const vercelSkewProtection = typeof import.meta.env !== 'undefined'
|
|
95
|
-
? import.meta.env.VERCEL_SKEW_PROTECTION_ENABLED
|
|
96
|
-
: undefined
|
|
97
|
-
|
|
98
|
-
if (vercelDeploymentId && vercelSkewProtection === '1') {
|
|
99
|
-
url.searchParams.set('dpl', vercelDeploymentId)
|
|
100
|
-
}
|
|
101
|
-
return url.toString()
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return imageSrc
|
|
105
|
-
} catch {
|
|
106
|
-
return imageSrc
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function getOptimizedImageUrl(imageSrc: string, targetWidth: number): string {
|
|
111
|
-
const width = findClosestSize(targetWidth)
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
const url = new URL(imageSrc, import.meta.url)
|
|
115
|
-
|
|
116
|
-
// If it's a Shopify domain and a custom loader is provided, use the loader
|
|
117
|
-
if (isShopifyDomain(url) && loader) {
|
|
118
|
-
const customUrl = loader({ src: imageSrc, width })
|
|
119
|
-
if (customUrl) return customUrl
|
|
120
|
-
}
|
|
121
|
-
} catch {
|
|
122
|
-
// If URL parsing fails, continue with default behavior
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Default optimization: use img.convincely.io for all images
|
|
126
|
-
const imgUrl = getImgUrl(imageSrc)
|
|
127
|
-
const encodedUrl = encodeURIComponent(imgUrl)
|
|
128
|
-
return `https://img.convincely.io/_next/image?url=${encodedUrl}&w=${width}&q=75`
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function getImgUrl(imageSrc: string): string {
|
|
132
|
-
if (imageSrc.startsWith('data:')) {
|
|
133
|
-
return imageSrc
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const baseUrl = new URL(import.meta.url).origin
|
|
137
|
-
return imageSrc.startsWith('http') ? imageSrc : `${baseUrl}${imageSrc}`
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Add dpl param to src if it's a Convincely domain
|
|
141
|
-
const processedSrc = $derived(addDplParamIfNeeded(src))
|
|
142
|
-
|
|
143
|
-
function handleLoad() {
|
|
144
|
-
isLoaded = true
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
onMount(() => {
|
|
148
|
-
if (loading === 'lazy') {
|
|
149
|
-
const elementToObserve = placeholderElement || imgElement
|
|
150
|
-
if (elementToObserve) {
|
|
151
|
-
observer = new IntersectionObserver(
|
|
152
|
-
(entries) => {
|
|
153
|
-
entries.forEach((entry) => {
|
|
154
|
-
if (entry.isIntersecting) {
|
|
155
|
-
isInView = true
|
|
156
|
-
observer?.unobserve(entry.target)
|
|
157
|
-
}
|
|
158
|
-
})
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
rootMargin: '50px',
|
|
162
|
-
},
|
|
163
|
-
)
|
|
164
|
-
observer.observe(elementToObserve)
|
|
165
|
-
|
|
166
|
-
return () => {
|
|
167
|
-
observer?.disconnect()
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
} else if (loading === 'eager') {
|
|
171
|
-
isInView = true
|
|
172
|
-
}
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
const shouldLoadImage = $derived(loading === 'eager' || isInView)
|
|
176
|
-
|
|
177
|
-
const widthCandidates = $derived.by(() => {
|
|
178
|
-
if (typeof width === 'number' && Number.isFinite(width)) {
|
|
179
|
-
const filtered = SUPPORTED_SIZES.filter((w) => w <= width)
|
|
180
|
-
return filtered.length ? filtered : [width]
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return SUPPORTED_SIZES
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
const srcSet = $derived.by(() =>
|
|
187
|
-
widthCandidates.map((w) => `${getOptimizedImageUrl(processedSrc, w)} ${w}w`).join(', '),
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
const fallbackSrc = $derived.by(() => getOptimizedImageUrl(processedSrc, widthCandidates[0]))
|
|
191
|
-
|
|
192
|
-
const fadeClasses = $derived.by(() => {
|
|
193
|
-
if (loading === 'eager') return ''
|
|
194
|
-
return `transition-opacity duration-300 ${isLoaded ? 'opacity-100' : 'opacity-0'}`
|
|
195
|
-
})
|
|
196
|
-
</script>
|
|
197
|
-
|
|
198
|
-
{#if shouldLoadImage}
|
|
199
|
-
<img
|
|
200
|
-
{...restProps}
|
|
201
|
-
bind:this={imgElement}
|
|
202
|
-
src={fallbackSrc}
|
|
203
|
-
srcset={srcSet}
|
|
204
|
-
{sizes}
|
|
205
|
-
width={fill ? undefined : width}
|
|
206
|
-
height={fill ? undefined : height}
|
|
207
|
-
{alt}
|
|
208
|
-
style:aspect-ratio={aspectRatio}
|
|
209
|
-
style:object-fit={objectFit}
|
|
210
|
-
style:position={fill ? 'absolute' : undefined}
|
|
211
|
-
style:inset={fill ? 0 : undefined}
|
|
212
|
-
{loading}
|
|
213
|
-
onload={handleLoad}
|
|
214
|
-
class="{fadeClasses} {restProps.class || ''}"
|
|
215
|
-
data-testid="lazy-image-element"
|
|
216
|
-
/>
|
|
217
|
-
{:else}
|
|
218
|
-
<div
|
|
219
|
-
bind:this={placeholderElement}
|
|
220
|
-
style:aspect-ratio={aspectRatio}
|
|
221
|
-
style:position={fill ? 'absolute' : undefined}
|
|
222
|
-
style:inset={fill ? 0 : undefined}
|
|
223
|
-
aria-label={alt}
|
|
224
|
-
data-testid="lazy-image-placeholder"
|
|
225
|
-
></div>
|
|
226
|
-
{/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};
|