@picked/svelte 0.0.1-dev.1774224866171 → 0.0.1-dev.1774244905386

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.
@@ -1,2 +1,25 @@
1
- export type { FormField } from '@picked/client/form';
2
- export { getForm } from './get-form';
1
+ import { ValidationError } from '@picked/client';
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
+ /**
12
+ *
13
+ * @param root0
14
+ * @param root0.id
15
+ * @param root0.title
16
+ * @param root0.onError
17
+ * @param root0.onSuccess
18
+ * @example
19
+ */
20
+ export declare function getForm({ id: formId, title, onError, onSuccess }: GetFormOptions): {
21
+ form: {
22
+ onsubmit: import('effector').EventCallable<SubmitEventWithCurrentTarget>;
23
+ };
24
+ };
25
+ export {};
@@ -1 +1 @@
1
- import{getForm as e}from"./get-form.js";export{e as getForm};
1
+ import{getExperience as e,getPicked as t}from"../hooks/client.js";import{getStep as n}from"../hooks/step.js";import"../hooks/index.js";import{attach as r,createEffect as i,createEvent as a,sample as o,split as s}from"effector";function c({id:c,title:l,onError:u,onSuccess:d}){let f=t(),p=n(),m=e(),{currentStepStore:h}=m,g=a(),_=i(e=>f.responses.commit(e)),v=r({source:{step:h},effect:i(async({step:e})=>{f.eventBus({type:`form_submit_success`,form:{id:c,title:l},experience:m,step:e??void 0}),await d?.()})}),y=r({source:{step:h},mapParams:(e,t)=>({step:t.step,errors:e.errors}),effect:i(async({step:e,errors:t})=>{f.eventBus({type:`form_submit_error`,form:{id:c,title:l},exception:{reason:t},experience:m,step:e??void 0}),t.forEach(t=>{f.eventBus({type:`field_validation_error`,exception:{reason:[t]},experience:m,step:e??void 0})}),await u?.(t)})});return o({clock:g,source:{step:h},fn:({step:e})=>({formId:c,title:l,step:e}),target:i(({formId:e,title:t,step:n})=>{f.eventBus({type:`form_submit`,form:{id:e,title:t},experience:m,step:n??void 0})})}),o({clock:g,source:{fields:f.fields.listStore,stepFields:p.fields,step:h},fn:({stepFields:e},t)=>{t.preventDefault();let n=new FormData(t.currentTarget),r=Array.from(n.entries()),i={data:{},push(e){let t=e.questionId,n=this.data[t];n&&Array.isArray(n)&&Array.isArray(e.value)?Array.isArray(n)&&Array.isArray(e.value)&&(typeof n[0]==`string`?this.data[t]=[...n,...e.value].map(String):typeof n[0]==`number`?this.data[t]=[...n,...e.value].map(Number):typeof n[0]==`boolean`?this.data[t]=[...n,...e.value].map(e=>!!e):this.data[t]=e.value):this.data[t]=e.value}};return e.forEach(e=>{let t=r.filter(([t])=>t===e.key||t===e.id);t.length===0&&(t=[[e.id,``]]),t.forEach(([,t])=>{let n=e.id;if(t===``){i.push({questionId:n,value:null});return}switch(e.type){case`text`:i.push({questionId:n,value:e.multiple?[String(t)]:String(t)});break;case`number`:i.push({questionId:n,value:e.multiple?[Number(t)]:Number(t)});break;case`boolean`:i.push({questionId:n,value:e.multiple?[t===`true`]:t===`true`});break;case`object`:break}})}),i.data},target:_}),s({source:_.doneData,match:{hasErrors:({errors:e})=>e.length>0},cases:{hasErrors:y,__:v}}),{form:{onsubmit:g}}}export{c as getForm};
@@ -1,7 +1,8 @@
1
1
  import { Field, FieldValue } from '@picked/client';
2
2
  import { Store } from 'effector';
3
- export interface GetFieldResponse {
3
+ interface GetFieldResponse {
4
4
  value: Store<FieldValue>;
5
5
  field: Store<Field | null>;
6
6
  }
7
- export declare function getField(id: string): GetFieldResponse;
7
+ export declare function getField(key: string): GetFieldResponse;
8
+ export {};
@@ -1 +1 @@
1
- import{getPicked as e}from"./client.js";function t(t){let n=e();return{field:n.fields.getById(t),value:n.responses.getByFieldId(t)}}export{t as getField};
1
+ import{getPicked as e}from"./client.js";function t(t){let n=e();return{field:n.fields.getByKey(t),value:n.responses.getByFieldKey(t)}}export{t as getField};
@@ -1,13 +1,13 @@
1
1
  import { Store } from 'effector';
2
- import { ExperienceStep } from '@picked/client';
3
- import { FormInstance, FormField } from '../form/create-form.svelte';
4
- export interface StepContext extends FormInstance {
2
+ import { ExperienceStep, Field, Response } from '@picked/client';
3
+ type FieldWithValue = Field & {
4
+ value: Response | undefined;
5
+ };
6
+ export interface StepContext {
5
7
  step: Store<ExperienceStep>;
6
- /** Directly accessible fields with current response values — usable without a form */
7
- fields: Store<FormField[]>;
8
- isActive: Store<boolean>;
9
- /** @deprecated Use `isActive` instead */
8
+ fields: Store<FieldWithValue[]>;
10
9
  isActiveStep: Store<boolean>;
11
10
  }
12
- export declare function setStepContext(stepId: string): StepContext;
13
11
  export declare function getStep(): StepContext;
12
+ export declare function setStepContext(stepId: string): StepContext;
13
+ export {};
@@ -1 +1 @@
1
- import{STEP_CONTEXT_KEY as e}from"../context.js";import{getExperience as t}from"./client.js";import{getStepRouter as n}from"../router/context.js";import{getContext as r,setContext as i}from"svelte";import{createForm as a}from"../form/create-form.svelte";function o(e){let n=t().treeStore.map(t=>{let n=t.steps.find(t=>t.id===e);if(!n)throw Error(`Step with id "${e}" not found in the experience tree.`);return n});return{stepStore:n,form:a(e,n.map(e=>e.label),n.map(e=>e.fieldIds))}}function s(t){let{activeStep:r}=n(),a=r.map(e=>e?.id===t),{stepStore:s,form:c}=o(t);return i(e,{step:s,isActive:a,isActiveStep:a,...c})}function c(){return r(e)}export{c as getStep,s as setStepContext};
1
+ import{STEP_CONTEXT_KEY as e}from"../context.js";import{getPicked as t}from"./client.js";import{getStepFields as n}from"../utils/get-step-fields.js";import{getStepRouter as r}from"../router/context.js";import{attach as i,combine as a,createEffect as o,createStore as s,sample as c}from"effector";import{getContext as l,setContext as u}from"svelte";function d(){return l(e)}function f(l){let d=t(),{activeStep:f}=r(),p=f.map(e=>e?.id===l),m=d.steps.listStore.map(e=>{let t=e.find(e=>(e.key??e.id)===l);if(!t)throw Error(`Step with id "${l}" not found in the step registry.`);return t}),h=s([]),g=a({fields:h,responses:d.responses.listStore},({fields:e,responses:t})=>e.map(e=>{let n=t.find(t=>t.fieldKey===e.key||t.fieldId===e.id)?.value;return{...e,value:n}})),_=i({source:a({step:m,fields:d.fields.listStore}),effect:o(async({step:e,fields:t})=>await n(d,e.fieldKeys&&e.fieldKeys.length>0?t.filter(t=>(e.fieldKeys??[]).includes(t.key)):t.filter(t=>(e.fieldIds??[]).includes(t.id))))});return c({source:{isActiveStep:p},filter:({isActiveStep:e})=>e,target:_}),c({clock:d.responses.listStore,source:{isActiveStep:p},filter:({isActiveStep:e})=>e,target:_}),c({source:_.doneData,target:h}),u(e,{step:m,fields:g,isActiveStep:p})}export{d as getStep,f as setStepContext};
@@ -1,9 +1,14 @@
1
1
  <script lang="ts">
2
+ /** @file List item row displaying an experience instance's current state in the devtools panel. */
2
3
  import type { ExperienceInstance } from '@picked/client'
3
4
  import { Button } from '../ui'
5
+ import { getPicked } from '../../../hooks'
4
6
 
5
7
  export let experienceInstance: ExperienceInstance
6
8
 
9
+ const client = getPicked()
10
+ const experienceLookup = client.experiences.get(experienceInstance.experienceId)
11
+
7
12
  const currentNode = experienceInstance.currentNodeStore
8
13
  const currentStep = experienceInstance.currentStepStore
9
14
  const nextStepNodes = experienceInstance.nextReachableStepNodesStore
@@ -12,15 +17,22 @@
12
17
  const isOpen = experienceInstance.isOpenStore
13
18
 
14
19
  $: totalNextStepNodes = $nextStepNodes.length
20
+ $: experienceName = $experienceLookup?.name ?? experienceInstance.experienceId
21
+ $: currentNodeDisplay = $currentNode
22
+ ? (('name' in $currentNode && $currentNode.name) ||
23
+ ('stepKey' in $currentNode && $currentNode.stepKey) ||
24
+ $currentNode.id)
25
+ : ''
26
+ $: currentStepDisplay = $currentStep?.name ?? ''
15
27
  </script>
16
28
 
17
29
  <tr>
18
- <td>{experienceInstance.experienceId}</td>
30
+ <td>{experienceName}</td>
19
31
  <td><Button variant="ghost" size="small" onclick={() => experienceInstance.toggleOpen()}>{$isOpen}</Button></td>
20
- <td>{$currentNode?.id || ''}</td>
32
+ <td>{currentNodeDisplay}</td>
21
33
  <td>{$currentNode?.type || ''}</td>
22
- <td>{$currentStep?.id || ''}</td>
23
- <td>{$currentStep?.label || ''}</td>
34
+ <td>{currentStepDisplay}</td>
35
+ <td>{$currentStep?.title || ''}</td>
24
36
  <td>{totalNextStepNodes}</td>
25
37
  <td>{$stepsToOrigin[0]} - {$stepsToOrigin[1]}</td>
26
38
  <td>{$stepsToLeaf[0]} - {$stepsToLeaf[1]}</td>
@@ -6,7 +6,7 @@
6
6
  export let experienceInstances: ExperienceInstance[]
7
7
 
8
8
  const columns = [
9
- { key: 'experience', header: 'Experience' },
9
+ { key: 'experience', header: 'Name' },
10
10
  { key: 'isOpen', header: 'Is Open' },
11
11
  { key: 'currentNodeId', header: 'Current node ID' },
12
12
  { key: 'currentNodeType', header: 'Current node Type' },
@@ -11,5 +11,14 @@ export interface SvelteDevtoolsPluginOptions {
11
11
  /**
12
12
  * Registers the devtools experience for on-demand loading via the Chrome DevTools extension.
13
13
  * The devtools component is lazily loaded — it is never included in the initial bundle.
14
+ *
15
+ * @param options - Optional configuration for the devtools plugin.
16
+ * @returns An array of plugins to register with the Picked client.
17
+ * @example
18
+ * ```typescript
19
+ * const client = new Picked({
20
+ * plugins: [...svelteDevtoolsPlugin({ enabled: true })],
21
+ * })
22
+ * ```
14
23
  */
15
24
  export declare const svelteDevtoolsPlugin: (options?: SvelteDevtoolsPluginOptions) => Plugin[];
@@ -1 +1 @@
1
- import{adaptSvelteComponent as e}from"../../component.js";import{__vitePreload as t}from"../../_virtual/_vite/preload-helper.js";import{lazyComponent as n}from"@picked/client";import{createExperience as r,createExperiencePlugin as i}from"@picked/client/plugins/create-experience";var a=a=>{let o=r({id:`devtools`,name:`Picked Devtools`,condition:!1,initializeOpen:!1,targets:[{selector:`body`,placement:`append`}],component:e(n(()=>t(()=>import(`./DevToolsExperience.svelte`),[])))});return o.addStep({label:`Tools`}),[{name:`svelte-devtools`,setup:e=>{a?.enabled&&e.eventBus.watch(t=>{t.type===`ready`&&e.experiences.launch({id:o.id,options:{bypassCondition:!0}})})}},i({experiences:[o]})]};export{a as svelteDevtoolsPlugin};
1
+ import{adaptSvelteComponent as e}from"../../component.js";import{__vitePreload as t}from"../../_virtual/_vite/preload-helper.js";import{lazyComponent as n}from"@picked/client";import{createExperience as r,createExperiencePlugin as i}from"@picked/client/plugins/create-experience";var a=a=>{let o=r({id:`devtools`,name:`Picked Devtools`,condition:!1,initializeOpen:!1,targets:[{selector:`body`,placement:`append`}],component:e(n(()=>t(()=>import(`./DevToolsExperience.svelte`),[])))});return o.addStep({label:`Tools`}),[{name:`svelte-devtools`,setup:e=>{a?.enabled&&e.eventBus.watch(t=>{t.type===`ready`&&e.experiences.launch({key:o.key,options:{bypassCondition:!0}})})}},i({experiences:[o]})]};export{a as svelteDevtoolsPlugin};
@@ -1,11 +1,33 @@
1
1
  <script lang="ts">
2
+ /** @file Expandable list item row for displaying an event bus history entry in the devtools events screen. */
2
3
  import type { EventBusPayload, EventHistoryEntry } from '@picked/client'
3
4
  import { Badge, Card, CodeBlock, Stack, Text } from '../../ui'
5
+ import { getPicked } from '../../../../hooks'
4
6
 
5
7
  export let event: EventHistoryEntry
6
8
  export let isExpanded: boolean
7
9
  export let onToggle: () => void
8
10
 
11
+ const client = getPicked()
12
+
13
+ const eventExpId =
14
+ 'experience' in event.payload && event.payload.experience
15
+ ? event.payload.experience.experienceId
16
+ : undefined
17
+ const eventExpLookup = client.experiences.get(eventExpId ?? '')
18
+ $: eventExpName = eventExpId
19
+ ? ($eventExpLookup?.name ?? eventExpId)
20
+ : '-'
21
+
22
+ /**
23
+ * Formats a Date as a 24-hour time string with milliseconds.
24
+ *
25
+ * @param date - The date to format.
26
+ * @example
27
+ * ```typescript
28
+ * formatTime(new Date()) // '14:30:05.123'
29
+ * ```
30
+ */
9
31
  function formatTime(date: Date): string {
10
32
  return date.toLocaleTimeString('en-US', {
11
33
  hour12: false,
@@ -21,62 +43,102 @@
21
43
  value: unknown
22
44
  }
23
45
 
24
- function getPayloadFields(payload: EventBusPayload): PayloadField[] {
46
+ /**
47
+ * Extracts experience-related fields from an event payload's experience object.
48
+ *
49
+ * @param exp - The experience object from the event payload.
50
+ * @example
51
+ * ```typescript
52
+ * getExperienceFields({ id: 'abc', experienceId: 'quiz' })
53
+ * // [{ label: 'Experience ID', value: 'abc' }, { label: 'Experience Type', value: 'quiz' }]
54
+ * ```
55
+ */
56
+ function getExperienceFields(exp: unknown): PayloadField[] {
57
+ if (!exp || typeof exp !== 'object') return []
25
58
  const fields: PayloadField[] = []
59
+ if ('id' in exp && exp.id) fields.push({ label: 'Experience ID', value: exp.id })
60
+ if ('experienceId' in exp && exp.experienceId)
61
+ fields.push({ label: 'Experience Type', value: exp.experienceId })
62
+ return fields
63
+ }
26
64
 
27
- fields.push({ label: 'Type', value: payload.type })
65
+ /**
66
+ * Extracts step-related fields from an event payload's step object.
67
+ *
68
+ * @param step - The step object from the event payload.
69
+ * @example
70
+ * ```typescript
71
+ * getStepFields({ id: 's1', name: 'Welcome' })
72
+ * // [{ label: 'Step ID', value: 's1' }, { label: 'Step Name', value: 'Welcome' }]
73
+ * ```
74
+ */
75
+ function getStepFields(step: unknown): PayloadField[] {
76
+ if (!step || typeof step !== 'object') return []
77
+ const fields: PayloadField[] = []
78
+ if ('id' in step && step.id) fields.push({ label: 'Step ID', value: step.id })
79
+ // eslint-disable-next-line sonarjs/deprecation -- backwards-compat fallback for label
80
+ const stepName = ('name' in step && step.name) || ('label' in step && step.label)
81
+ if (stepName) fields.push({ label: 'Step Name', value: stepName })
82
+ return fields
83
+ }
28
84
 
29
- if ('value' in payload && payload.value !== undefined) {
85
+ /**
86
+ * Extracts scalar (non-nested) fields from an event bus payload.
87
+ *
88
+ * @param payload - The event bus payload to extract fields from.
89
+ * @example
90
+ * ```typescript
91
+ * getScalarFields({ type: 'step', value: 42 })
92
+ * // [{ label: 'Value', value: 42 }]
93
+ * ```
94
+ */
95
+ function getScalarFields(payload: EventBusPayload): PayloadField[] {
96
+ const fields: PayloadField[] = []
97
+ if ('value' in payload && payload.value !== undefined)
30
98
  fields.push({ label: 'Value', value: payload.value })
31
- }
32
-
33
- if ('experience' in payload && payload.experience) {
34
- const exp = payload.experience
35
- if (exp && typeof exp === 'object') {
36
- if ('id' in exp && exp.id) {
37
- fields.push({ label: 'Experience ID', value: exp.id })
38
- }
39
- if ('experienceId' in exp && exp.experienceId) {
40
- fields.push({ label: 'Experience Type', value: exp.experienceId })
41
- }
42
- }
43
- }
44
-
45
- if ('step' in payload && payload.step) {
46
- const step = payload.step
47
- if (step && typeof step === 'object') {
48
- if ('id' in step && step.id) {
49
- fields.push({ label: 'Step ID', value: step.id })
50
- }
51
- if ('label' in step && step.label) {
52
- fields.push({ label: 'Step Label', value: step.label })
53
- }
54
- }
55
- }
56
-
57
- if ('form' in payload && payload.form) {
99
+ if ('form' in payload && payload.form)
58
100
  fields.push({ label: 'Form', value: JSON.stringify(payload.form) })
59
- }
60
-
61
- if ('feature' in payload && payload.feature) {
101
+ if ('feature' in payload && payload.feature)
62
102
  fields.push({ label: 'Feature', value: JSON.stringify(payload.feature) })
63
- }
64
-
65
- if ('exception' in payload && payload.exception) {
103
+ if ('exception' in payload && payload.exception)
66
104
  fields.push({ label: 'Exception', value: JSON.stringify(payload.exception) })
67
- }
68
-
69
- if ('version' in payload && payload.version) {
105
+ if ('version' in payload && payload.version)
70
106
  fields.push({ label: 'Version', value: payload.version })
71
- }
107
+ return fields
108
+ }
72
109
 
73
- if ('id' in payload && payload.id && payload.type === 'session') {
110
+ /**
111
+ * Builds a complete list of display fields from an event bus payload.
112
+ *
113
+ * @param payload - The event bus payload to extract all fields from.
114
+ * @example
115
+ * ```typescript
116
+ * getPayloadFields({ type: 'session', id: 'sess-1' })
117
+ * // [{ label: 'Type', value: 'session' }, { label: 'Session ID', value: 'sess-1' }]
118
+ * ```
119
+ */
120
+ function getPayloadFields(payload: EventBusPayload): PayloadField[] {
121
+ const fields: PayloadField[] = [{ label: 'Type', value: payload.type }]
122
+ fields.push(...getScalarFields(payload))
123
+ if ('experience' in payload && payload.experience)
124
+ fields.push(...getExperienceFields(payload.experience))
125
+ if ('step' in payload && payload.step)
126
+ fields.push(...getStepFields(payload.step))
127
+ if ('id' in payload && payload.id && payload.type === 'session')
74
128
  fields.push({ label: 'Session ID', value: payload.id })
75
- }
76
-
77
129
  return fields
78
130
  }
79
131
 
132
+ /**
133
+ * Formats a field value for display as a string.
134
+ *
135
+ * @param value - The value to format.
136
+ * @example
137
+ * ```typescript
138
+ * formatValue(42) // '42'
139
+ * formatValue(null) // 'null'
140
+ * ```
141
+ */
80
142
  function formatValue(value: unknown): string {
81
143
  if (value === null) return 'null'
82
144
  if (value === undefined) return 'undefined'
@@ -97,9 +159,7 @@
97
159
  <Badge status="info">{event.payload.type}</Badge>
98
160
  </td>
99
161
  <td>
100
- <Text size="small">
101
- {'experience' in event.payload && event.payload.experience ? event.payload.experience.experienceId : '-'}
102
- </Text>
162
+ <Text size="small">{eventExpName}</Text>
103
163
  </td>
104
164
  <td>
105
165
  <Text size="caption" color="subdued">
@@ -114,7 +174,7 @@
114
174
  <Text size="small" weight="semibold">Event Payload</Text>
115
175
  <Card>
116
176
  <Stack direction="vertical" gap="small">
117
- {#each getPayloadFields(event.payload) as field}
177
+ {#each getPayloadFields(event.payload) as field (field.label)}
118
178
  <Text size="caption" inline>
119
179
  <Text size="caption" weight="semibold" color="subdued" inline>{field.label}:</Text>
120
180
  <CodeBlock code={formatValue(field.value)} inline />
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ /** @file Detail screen for a single experience, showing its config, target selectors, and decision logic. */
2
3
  import type { ExperienceStore } from '@picked/client'
3
4
  import { getPicked } from '../../../../hooks'
4
5
  import { Button, Card, Text, CodeBlock, Stack } from '../../ui'
@@ -11,7 +12,7 @@
11
12
 
12
13
  <Stack direction="vertical" gap="medium" padding="medium">
13
14
  <Stack direction="horizontal" gap="small">
14
- <Button variant="primary" size="small" onclick={() => client.launchExperienceById(experience.id)}>
15
+ <Button variant="primary" size="small" onclick={() => client.experiences.open({ key: experience.key, options: { bypassCondition: true } })}>
15
16
  Launch experience
16
17
  </Button>
17
18
  </Stack>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ /** @file D3-based tree visualization of an experience's node graph in the devtools panel. */
2
3
  import * as d3 from 'd3'
3
4
  import type { ExperienceEdge, ExperienceNode, ExperienceStore } from '@picked/client'
4
5
  import { Badge, Button, Spinner, Stack, Text, VisualizationContainer } from '../../ui'
@@ -7,19 +8,20 @@
7
8
 
8
9
  let svgEl: SVGSVGElement
9
10
  let zoom = 1
11
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- reserved for future node details panel
10
12
  let selectedNode = null
13
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, sonarjs/no-unused-collection -- reserved for future path highlighting
11
14
  let highlightedPath: string[][] = []
12
- let error: string | null = null
15
+ let error = null
13
16
  const loading = false
14
17
 
15
18
  let treeRenderingDone = false
16
19
 
17
20
  const tree = experience.treeStore
18
21
 
19
- $: steps = $tree.steps
20
22
  $: nodes = $tree.nodes
21
23
  $: edges = $tree.edges
22
- $: conditionals = $tree.conditionals
24
+ $: conditions = $tree.conditions
23
25
 
24
26
  let zoomBehavior: d3.ZoomBehavior<SVGSVGElement, unknown>
25
27
 
@@ -35,10 +37,30 @@
35
37
  _referenceTo?: string
36
38
  }
37
39
 
40
+ /**
41
+ * Type guard that checks if a value is a non-null, non-array object.
42
+ *
43
+ * @param val - The value to check.
44
+ * @example
45
+ * ```typescript
46
+ * isRecord({ key: 'value' }) // true
47
+ * isRecord([1, 2]) // false
48
+ * ```
49
+ */
38
50
  function isRecord(val: unknown): val is Record<string, unknown> {
39
51
  return val !== null && typeof val === 'object' && !Array.isArray(val)
40
52
  }
41
53
 
54
+ /**
55
+ * Recursively formats a JSON Logic rule into a human-readable string.
56
+ *
57
+ * @param rule - The JSON Logic rule object to format.
58
+ * @example
59
+ * ```typescript
60
+ * formatLogicRule({ '==': [{ var: 'age' }, 18] }) // 'age = 18'
61
+ * ```
62
+ */
63
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- recursive logic rule formatter handles all operator types
42
64
  function formatLogicRule(rule: unknown): string {
43
65
  if (rule === null || rule === undefined) return '?'
44
66
  if (typeof rule === 'string') return rule
@@ -46,6 +68,7 @@
46
68
  if (Array.isArray(rule)) {
47
69
  return rule.map(formatLogicRule).join(', ')
48
70
  }
71
+
49
72
  if (isRecord(rule)) {
50
73
  const keys = Object.keys(rule)
51
74
  if (keys.length === 0) return 'Condition'
@@ -69,28 +92,43 @@
69
92
  if (op in compOps && Array.isArray(args) && args.length >= 2) {
70
93
  return `${formatLogicRule(args[0])} ${compOps[op]} ${formatLogicRule(args[1])}`
71
94
  }
95
+
72
96
  if (op === 'and' && Array.isArray(args)) {
73
97
  return args.map(formatLogicRule).join(' & ')
74
98
  }
99
+
75
100
  if (op === 'or' && Array.isArray(args)) {
76
101
  return args.map(formatLogicRule).join(' | ')
77
102
  }
103
+
78
104
  if ((op === '!' || op === 'not') && Array.isArray(args)) {
79
105
  return `!${formatLogicRule(args[0])}`
80
106
  }
107
+
81
108
  if (op === 'in' && Array.isArray(args) && args.length >= 2) {
82
109
  return `${formatLogicRule(args[0])} in ${formatLogicRule(args[1])}`
83
110
  }
111
+
84
112
  return `${op}(?)`
85
113
  }
114
+
86
115
  return '?'
87
116
  }
88
117
 
118
+ /**
119
+ * Renders the experience node graph as an interactive D3 tree visualization.
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * renderGraph() // builds and renders the SVG tree
124
+ * ```
125
+ */
89
126
  function renderGraph() {
90
127
  d3.select(svgEl).selectAll('*').remove()
91
128
 
92
- const stepsMap = new Map(steps.map((s) => [s.id, s]))
93
- const conditionalsMap = new Map(conditionals.map((c) => [c.id, c]))
129
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- D3 visualization runs imperatively outside Svelte reactivity
130
+ const conditionsMap = new Map(conditions.map((c) => [c.id, c]))
131
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- D3 visualization runs imperatively outside Svelte reactivity
94
132
  const outgoingEdges = new Map<string, ExperienceEdge[]>()
95
133
  edges.forEach((e) => {
96
134
  if (!outgoingEdges.has(e.sourceNodeId)) outgoingEdges.set(e.sourceNodeId, [])
@@ -99,17 +137,20 @@
99
137
 
100
138
  const getNodeLabel = (node: ExperienceNode): string => {
101
139
  if (node.type === 'origin') return 'Start'
102
- if (node.type === 'step' && node.stepId)
103
- return stepsMap.get(node.stepId)?.label ?? 'Unknown Step'
104
- if (node.type === 'conditional' && node.conditionalId) {
105
- const conditional = conditionalsMap.get(node.conditionalId)
106
- if (conditional?.label) return conditional.label
107
- if (conditional?.condition) return formatLogicRule(conditional.condition)
140
+ if (node.type === 'step')
141
+ return node.name ?? node.stepKey ?? node.stepId ?? 'Unknown Step'
142
+ if (node.type === 'condition') {
143
+ const conditionId = node.conditionId
144
+ const conditionItem = conditionId ? conditionsMap.get(conditionId) : undefined
145
+ if (conditionItem?.name) return conditionItem.name
146
+ if (conditionItem?.logic) return formatLogicRule(conditionItem.logic)
108
147
  return 'Condition'
109
148
  }
149
+
110
150
  return 'Unknown Node'
111
151
  }
112
152
 
153
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- D3 visualization runs imperatively outside Svelte reactivity
113
154
  const processedNodes = new Map<string, ProcessedNode>()
114
155
  nodes.forEach((n) => {
115
156
  processedNodes.set(n.id, {
@@ -141,6 +182,7 @@
141
182
  // Use a global visited set so join nodes (multiple incoming branches converging
142
183
  // to the same node) only appear once in the tree, with reference nodes on
143
184
  // subsequent visits to indicate the convergence point.
185
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- D3 visualization runs imperatively outside Svelte reactivity
144
186
  const globalVisited = new Set<string>()
145
187
  const convertGraphToTree = (node: ProcessedNode): ProcessedNode => {
146
188
  if (globalVisited.has(node.id)) {
@@ -196,8 +238,9 @@
196
238
  .append('path')
197
239
  .attr('class', 'link')
198
240
  .attr('fill', 'none')
241
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
199
242
  .attr('stroke', (d: any) => {
200
- if (d.source.data.type !== 'conditional') return '#94a3b8'
243
+ if (d.source.data.type !== 'condition') return '#94a3b8'
201
244
  const targetId = d.target.data._isReference
202
245
  ? d.target.data._referenceTo
203
246
  : d.target.data.id
@@ -211,17 +254,21 @@
211
254
  .attr('stroke-width', 2)
212
255
  .attr('d', linkGenerator)
213
256
 
214
- // Label true/false branches from conditional nodes
257
+ // Label true/false branches from condition nodes
215
258
  g.selectAll('.edge-label')
216
- .data(treeRoot.links().filter((d: any) => d.source.data.type === 'conditional'))
259
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
260
+ .data(treeRoot.links().filter((d: any) => d.source.data.type === 'condition'))
217
261
  .enter()
218
262
  .append('text')
219
263
  .attr('class', 'edge-label')
220
264
  .attr('text-anchor', 'middle')
221
265
  .attr('font-size', '10px')
222
266
  .attr('font-weight', 'bold')
267
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
223
268
  .attr('x', (d: any) => (d.source.y + d.target.y) / 2)
269
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
224
270
  .attr('y', (d: any) => (d.source.x + d.target.x) / 2 - 6)
271
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
225
272
  .attr('fill', (d: any) => {
226
273
  const targetId = d.target.data._isReference
227
274
  ? d.target.data._referenceTo
@@ -233,6 +280,7 @@
233
280
  if (edge?.sourceNodeHandle === 'false') return '#ef4444'
234
281
  return '#64748b'
235
282
  })
283
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
236
284
  .text((d: any) => {
237
285
  const targetId = d.target.data._isReference
238
286
  ? d.target.data._referenceTo
@@ -251,15 +299,18 @@
251
299
  .enter()
252
300
  .append('g')
253
301
  .attr('class', 'node')
302
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
254
303
  .attr('transform', (d: any) => `translate(${d.y},${d.x})`)
255
304
 
256
305
  nodesSelection
257
306
  .append('circle')
307
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
258
308
  .attr('r', (d: any) => (d.data.type === 'origin' ? 15 : 10))
309
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
259
310
  .attr('fill', (d: any) => {
260
311
  if (d.data._isReference) return '#38bdf8'
261
312
  if (d.data.type === 'origin') return '#00cd90'
262
- if (d.data.type === 'conditional') return '#f59e0b'
313
+ if (d.data.type === 'condition') return '#f59e0b'
263
314
  return '#1748ff'
264
315
  })
265
316
  .attr('stroke', '#fff')
@@ -267,10 +318,12 @@
267
318
 
268
319
  nodesSelection
269
320
  .append('text')
321
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
270
322
  .attr('dy', (d: any) => (d.children ? -15 : 25))
271
323
  .attr('text-anchor', 'middle')
272
324
  .attr('font-size', '12px')
273
325
  .attr('fill', '#1e293b')
326
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
274
327
  .text((d: any) =>
275
328
  d.data.name.length > 30 ? `${d.data.name.slice(0, 27)}...` : d.data.name,
276
329
  )
@@ -301,11 +354,27 @@
301
354
  treeRenderingDone = true
302
355
  }
303
356
 
357
+ /**
358
+ * Zooms the tree visualization in by 20%.
359
+ *
360
+ * @example
361
+ * ```typescript
362
+ * handleZoomIn() // increases zoom level
363
+ * ```
364
+ */
304
365
  function handleZoomIn() {
305
366
  if (!svgEl || !zoomBehavior) return
306
367
  d3.select(svgEl).transition().duration(250).call(zoomBehavior.scaleBy, 1.2)
307
368
  }
308
369
 
370
+ /**
371
+ * Zooms the tree visualization out by 20%.
372
+ *
373
+ * @example
374
+ * ```typescript
375
+ * handleZoomOut() // decreases zoom level
376
+ * ```
377
+ */
309
378
  function handleZoomOut() {
310
379
  if (!svgEl || !zoomBehavior) return
311
380
  d3.select(svgEl)
@@ -314,6 +383,14 @@
314
383
  .call(zoomBehavior.scaleBy, 1 / 1.2)
315
384
  }
316
385
 
386
+ /**
387
+ * Resets the tree visualization zoom and position to the default centered view.
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * handleReset() // resets zoom to default
392
+ * ```
393
+ */
317
394
  function handleReset() {
318
395
  if (!svgEl || !zoomBehavior) return
319
396
  const width = svgEl.clientWidth
@@ -327,6 +404,15 @@
327
404
  d3.select(svgEl).transition().duration(750).call(zoomBehavior.transform, initialTransform)
328
405
  }
329
406
 
407
+ /**
408
+ * Closes the node details panel and resets all link/node highlights.
409
+ *
410
+ * @example
411
+ * ```typescript
412
+ * closeNodeDetails() // resets selection and highlighting
413
+ * ```
414
+ */
415
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- reserved for future node details panel
330
416
  function closeNodeDetails() {
331
417
  selectedNode = null
332
418
  if (svgEl) {
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ /** @file Screen listing all registered experiences as tabs in the devtools panel. */
2
3
  import Tabs from '../../components/Tabs.svelte'
3
4
  import { getPicked } from '../../../../hooks'
4
5
  import ExperienceScreen from './ExperienceScreen.svelte'
@@ -9,7 +10,7 @@
9
10
  $: tabs = $experiences.map((experience) => {
10
11
  return {
11
12
  id: experience.id,
12
- label: experience.id,
13
+ label: experience.name,
13
14
  component: ExperienceScreen,
14
15
  props: {
15
16
  experience,
@@ -1,12 +1,18 @@
1
1
  <script lang="ts">
2
+ /** @file List item row displaying a navigation history entry in the devtools history screen. */
2
3
  import type { HistoryEntry } from '@picked/client'
4
+ import { getPicked } from '../../../../hooks'
3
5
 
4
6
  export let historyEntry: HistoryEntry
7
+
8
+ const client = getPicked()
9
+ const experienceLookup = client.experiences.get(historyEntry.experienceId)
10
+ $: experienceName = $experienceLookup?.name ?? historyEntry.experienceId
5
11
  </script>
6
12
 
7
13
  <tr>
8
14
  <td>{historyEntry.id}</td>
9
- <td>{historyEntry.experienceId}</td>
15
+ <td>{experienceName}</td>
10
16
  <td>{historyEntry.node.id}</td>
11
17
  <td>{historyEntry.node.type}</td>
12
18
  <td>{historyEntry.at}</td>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
- import type { ResponseListItem, PrimitiveFieldValue, Field, FieldValue } from '@picked/client'
2
+ /** @file Editable table row for a single field response in the devtools responses screen. */
3
+ import type { ResponseListItem, PrimitiveFieldValue, Field } from '@picked/client'
3
4
  import { getPicked } from '../../../../hooks'
4
5
  import { Button, Text, CodeBlock, Banner, Stack } from '../../ui'
5
6
 
@@ -11,25 +12,46 @@
11
12
  let isEditing = false
12
13
  let editValue = ''
13
14
  let editError = ''
14
- let selectedOptions: Set<FieldValue> = new Set()
15
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- Set is reassigned for Svelte reactivity via `selectedOptions = selectedOptions`
16
+ let selectedOptions: Set<PrimitiveFieldValue> = new Set()
15
17
 
16
- $: hasOptions = Array.isArray(field?.options) && field.options.length > 0
17
- $: staticOptions = Array.isArray(field?.options) ? field.options : null
18
+ $: hasOptions = field?.options && field.options.length > 0
18
19
  $: currentValue = response?.value ?? null
19
20
 
21
+ /**
22
+ * Formats a response value for display, handling arrays, objects, and primitives.
23
+ *
24
+ * @param value - The response value to format.
25
+ * @example
26
+ * ```typescript
27
+ * formatValue(['a', 'b']) // 'a, b'
28
+ * formatValue(null) // '-'
29
+ * ```
30
+ */
20
31
  function formatValue(value: unknown): string {
21
32
  if (value === null || value === undefined) {
22
33
  return '-'
23
34
  }
35
+
24
36
  if (Array.isArray(value) && value.every((item) => typeof item === 'string')) {
25
37
  return value.join(', ')
26
38
  }
39
+
27
40
  if (typeof value === 'object') {
28
41
  return JSON.stringify(value, null, 2)
29
42
  }
43
+
30
44
  return String(value)
31
45
  }
32
46
 
47
+ /**
48
+ * Enters edit mode for this field, initializing the edit state from the current value.
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * startEdit() // opens the inline editor
53
+ * ```
54
+ */
33
55
  function startEdit() {
34
56
  isEditing = true
35
57
  editError = ''
@@ -46,6 +68,14 @@
46
68
  }
47
69
  }
48
70
 
71
+ /**
72
+ * Cancels the current edit, discarding changes and resetting edit state.
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * cancelEdit() // closes editor without saving
77
+ * ```
78
+ */
49
79
  function cancelEdit() {
50
80
  isEditing = false
51
81
  editValue = ''
@@ -53,61 +83,90 @@
53
83
  selectedOptions = new Set()
54
84
  }
55
85
 
56
- function toggleOption(optionValue: FieldValue) {
86
+ /**
87
+ * Toggles selection of a field option, supporting both single and multi-select modes.
88
+ *
89
+ * @param optionValue - The option value to toggle.
90
+ * @example
91
+ * ```typescript
92
+ * toggleOption('red') // selects or deselects the 'red' option
93
+ * ```
94
+ */
95
+ function toggleOption(optionValue: PrimitiveFieldValue) {
57
96
  if (field?.multiple) {
58
97
  if (selectedOptions.has(optionValue)) {
59
98
  selectedOptions.delete(optionValue)
60
99
  } else {
61
100
  selectedOptions.add(optionValue)
62
101
  }
102
+
63
103
  selectedOptions = selectedOptions
64
104
  } else {
65
105
  selectedOptions = new Set([optionValue])
66
106
  }
67
107
  }
68
108
 
69
- function parseValue(input: string): FieldValue {
70
- input = input.trim()
109
+ /**
110
+ * Parses a string input into a typed value based on the field's type.
111
+ *
112
+ * @param input - The raw string input to parse.
113
+ * @example
114
+ * ```typescript
115
+ * parseValue('42') // 42 (when field.type is 'number')
116
+ * parseValue('true') // true (when field.type is 'boolean')
117
+ * ```
118
+ */
119
+ function parseValue(input: string): unknown {
120
+ const trimmed = input.trim()
71
121
 
72
- if (input === '' || input === '-') {
122
+ if (trimmed === '' || trimmed === '-') {
73
123
  return null
74
124
  }
75
125
 
76
- if (input.startsWith('{') || input.startsWith('[')) {
126
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
77
127
  try {
78
- return JSON.parse(input)
128
+ return JSON.parse(trimmed)
79
129
  } catch {
80
130
  throw new Error('Invalid JSON format')
81
131
  }
82
132
  }
83
133
 
84
134
  if (field.type === 'boolean') {
85
- if (input === 'true') return true
86
- if (input === 'false') return false
135
+ if (trimmed === 'true') return true
136
+ if (trimmed === 'false') return false
87
137
  throw new Error('Boolean must be "true" or "false"')
88
138
  }
89
139
 
90
140
  if (field.type === 'number') {
91
- const num = Number(input)
141
+ const num = Number(trimmed)
92
142
  if (isNaN(num)) {
93
143
  throw new Error('Invalid number format')
94
144
  }
145
+
95
146
  return num
96
147
  }
97
148
 
98
- return input
149
+ return trimmed
99
150
  }
100
151
 
152
+ /**
153
+ * Commits the edited value via the client responses API and exits edit mode.
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * await saveEdit() // saves the current edit value
158
+ * ```
159
+ */
101
160
  async function saveEdit() {
102
161
  try {
103
- let newValue: FieldValue
162
+ let newValue: unknown
104
163
 
105
164
  if (hasOptions) {
106
165
  const values = Array.from(selectedOptions)
107
166
  if (field?.multiple) {
108
167
  newValue = values
109
168
  } else {
110
- newValue = values.at(0) ?? null
169
+ newValue = values[0] ?? null
111
170
  }
112
171
  } else {
113
172
  newValue = parseValue(editValue)
@@ -128,6 +187,14 @@
128
187
  }
129
188
  }
130
189
 
190
+ /**
191
+ * Clears the response value for this field via the client responses API.
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * handleClear() // removes the response for this field
196
+ * ```
197
+ */
131
198
  function handleClear() {
132
199
  client.responses.clear(field.id)
133
200
  }
@@ -135,7 +202,7 @@
135
202
 
136
203
  <tr>
137
204
  <td>
138
- <CodeBlock code={field.name} inline />
205
+ <CodeBlock code={field.key} inline />
139
206
  </td>
140
207
  <td>
141
208
  <Text size="small" weight="semibold">{field.label}</Text>
@@ -143,9 +210,9 @@
143
210
  <td>
144
211
  {#if isEditing}
145
212
  <Stack direction="vertical" gap="small">
146
- {#if hasOptions && staticOptions}
213
+ {#if hasOptions && field?.options}
147
214
  <Stack direction="vertical" gap="small">
148
- {#each staticOptions as option (option.id)}
215
+ {#each field.options as option (option.id)}
149
216
  <label class="option-label">
150
217
  {#if field.multiple}
151
218
  <input
@@ -1,8 +1,9 @@
1
1
  <script lang="ts">
2
- import type { ResponseListItem, Field } from '@picked/client'
2
+ /** @file Table displaying all field responses with filtering and clear-all functionality in the devtools panel. */
3
+ import type { ResponseListItem } from '@picked/client'
3
4
  import { getPicked } from '../../../../hooks'
4
- import ResponseTableRow from './ResponseTableRow.svelte'
5
5
  import { Card, DataTable, EmptyState, Button, RadioGroup, Stack } from '../../ui'
6
+ import ResponseTableRow from './ResponseTableRow.svelte'
6
7
 
7
8
  export let responses: ResponseListItem[] = []
8
9
 
@@ -10,7 +11,7 @@
10
11
 
11
12
  let filter = 'set'
12
13
 
13
- $: fieldsStore = client.fields.listStore
14
+ const fieldsStore = client.fields.listStore
14
15
  $: allFields = $fieldsStore ?? []
15
16
 
16
17
  $: responseMap = new Map(responses.map((r) => [r.fieldId, r]))
@@ -29,12 +30,20 @@
29
30
  return true
30
31
  })
31
32
 
33
+ /**
34
+ * Clears all committed field responses via the client responses API.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * handleClearAll() // removes all responses
39
+ * ```
40
+ */
32
41
  function handleClearAll() {
33
42
  client.responses.clearAll()
34
43
  }
35
44
 
36
45
  const columns = [
37
- { key: 'id', header: 'ID' },
46
+ { key: 'key', header: 'Key' },
38
47
  { key: 'field', header: 'Field' },
39
48
  { key: 'value', header: 'Value' },
40
49
  { key: 'type', header: 'Type' },
@@ -13,6 +13,7 @@
13
13
 
14
14
  const step = {
15
15
  id,
16
+ key: id
16
17
  }
17
18
 
18
19
  const { registerStep, unregisterStep } = getStepRouter()
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ /** @file Root router component that manages step registration and active step detection via Effector. */
2
3
  import { createEvent, createStore, sample } from 'effector'
3
4
  import { supportsWindow } from '@picked/client'
4
5
  import { getExperience } from '../hooks'
@@ -51,7 +52,7 @@
51
52
 
52
53
  sample({
53
54
  source: { steps: stepsStore, currentStep: currentStepStore },
54
- fn: ({ steps, currentStep }) => steps.find((s) => s.id === currentStep?.id) || null,
55
+ fn: ({ steps, currentStep }) => steps.find((s) => s.id === currentStep?.key) || null,
55
56
  target: setActiveStepEvent,
56
57
  })
57
58
 
@@ -0,0 +1,2 @@
1
+ import { Field, Picked } from '@picked/client';
2
+ export declare function getStepFields(client: Picked, fields: Field[]): Promise<Field[]>;
@@ -0,0 +1 @@
1
+ import{evaluateRuleLogic as e}from"@picked/client";function t(e){return e.options?.some(e=>`condition`in e)??!1}function n(e){return e.some(e=>e.condition||e.conditionKey||t(e))}async function r(e,t){return t.conditionKey?e.conditions.getByKey(t.conditionKey).getState()?.logic??null:t.condition??null}async function i(i,a){return a.length===0||!n(a)?a:(await Promise.all(a.map(async n=>{let a=await r(i,n);if(!(!a||await e(i,a)))return null;if(!n.options||!t(n))return n;let o=(await Promise.all(n.options.map(async t=>t.condition?await e(i,t.condition)?t:null:t))).filter(e=>e!==null);return o.length===n.options.length?n:o.length===0?null:{...n,options:o}}))).filter(e=>e!==null)}export{i as getStepFields};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@picked/svelte",
3
- "version": "0.0.1-dev.1774224866171",
3
+ "version": "0.0.1-dev.1774244905386",
4
4
  "dependencies": {
5
5
  "d3": "^7.9.0"
6
6
  },
@@ -88,7 +88,7 @@
88
88
  },
89
89
  "module": "./dist/index.js",
90
90
  "peerDependencies": {
91
- "@picked/client": "0.0.1-dev.1774224866171",
91
+ "@picked/client": "0.0.1-dev.1774244905386",
92
92
  "effector": "^23.3.0",
93
93
  "svelte": "^5.46.4"
94
94
  },
@@ -1 +0,0 @@
1
- import{getExperience as e,getPicked as t}from"../hooks/client.js";import{createForm as n}from"@picked/client/form";import*as r from"svelte/internal/client";function i(i,a,o){let s=n({client:t(),experienceInstance:e()},i,a,o);return{...s,form:{...s.form,onSubmit:e=>{r.user_effect(()=>s.form.onSubmit(e))},onSuccess:e=>{r.user_effect(()=>s.form.onSuccess(e))},onError:e=>{r.user_effect(()=>s.form.onError(e))}}}}export{i as createForm};
@@ -1,24 +0,0 @@
1
- import { Store } from 'effector';
2
- import { FormInstance as CoreFormInstance, ErrorCallback, SubmitCallback, SuccessCallback } from '@picked/client/form';
3
- export type { FormField } from '@picked/client/form';
4
- /**
5
- * Svelte FormInstance — identical to core but callback registration returns void
6
- * (cleanup is handled automatically via $effect, not by the consumer).
7
- */
8
- export interface FormInstance<TFieldIds extends string = string> extends Omit<CoreFormInstance<TFieldIds>, 'form'> {
9
- form: Omit<CoreFormInstance<TFieldIds>['form'], 'onSubmit' | 'onSuccess' | 'onError'> & {
10
- onSubmit: (callback: SubmitCallback<TFieldIds>) => void;
11
- onSuccess: (callback: SuccessCallback<TFieldIds>) => void;
12
- onError: (callback: ErrorCallback) => void;
13
- };
14
- }
15
- /**
16
- * Creates a form instance with Svelte lifecycle management.
17
- *
18
- * Wraps the framework-agnostic createForm from @picked/client/form:
19
- * - Injects client and experienceInstance from Svelte context
20
- * - Auto-wires callback cleanup via $effect so consumers don't need to manage lifecycle
21
- *
22
- * Used internally by setStepContext — access via getStep() in components.
23
- */
24
- export declare function createForm(formId: string, title: Store<string>, fieldIds: Store<string[]>): FormInstance;
@@ -1 +0,0 @@
1
- export * from './create-form.js';
@@ -1,16 +0,0 @@
1
- import { ValidationError } from '@picked/client';
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 +0,0 @@
1
- import{getExperience as e,getPicked as t}from"../hooks/client.js";import{getStep as n}from"../hooks/step.js";import"../hooks/index.js";import{buildResponsesFromFormData as r}from"@picked/client/form";import{attach as i,createEffect as a,createEvent as o,sample as s,split as c}from"effector";function l({id:l,title:u,onError:d,onSuccess:f}){let p=t(),m=n(),h=e(),{currentStepStore:g}=h,_=o(),v=a(e=>p.responses.commit(e)),y=i({source:{step:g},effect:a(({step:e})=>{p.eventBus({type:`form_submit_success`,form:{id:l,title:u},experience:h,step:e??void 0}),f?.()})}),b=i({source:{step:g},mapParams:(e,t)=>({step:t.step,errors:e.errors}),effect:a(({step:e,errors:t})=>{p.eventBus({type:`form_submit_error`,form:{id:l,title:u},exception:{reason:t},experience:h,step:e??void 0}),t.forEach(t=>{p.eventBus({type:`field_validation_error`,exception:{reason:[t]},experience:h,step:e??void 0})}),d?.(t)})});return s({clock:_,source:{step:g},fn:({step:e})=>({formId:l,title:u,step:e}),target:a(({formId:e,title:t,step:n})=>{p.eventBus({type:`form_submit`,form:{id:e,title:t},experience:h,step:n??void 0})})}),s({clock:_,source:{fields:p.fields.listStore,stepFields:m.fields,step:g},fn:({stepFields:e},t)=>(t.preventDefault(),r(e,new FormData(t.currentTarget))),target:v}),c({source:v.doneData,match:{hasErrors:({errors:e})=>e.length>0},cases:{hasErrors:b,__:y}}),{form:{onsubmit:_}}}export{l as getForm};