@selvajs/ui 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@
14
14
  import AppLayout from './AppLayout.svelte';
15
15
  import StateDisplay from '../primitives/StateDisplay.svelte';
16
16
  import { getExternalInputs, readExternalValue } from '../../external/storage';
17
+ import { setClientSlot, type ClientSlot } from '../../contexts/clientSlotContext.svelte';
17
18
 
18
19
  import type { Snippet } from 'svelte';
19
20
 
@@ -57,6 +58,14 @@
57
58
  * values. If absent, falls back to definitionKey, then to schema.id.
58
59
  */
59
60
  externalScopeKey?: string;
61
+ /**
62
+ * Host-rendered element for client-sourced inputs whose schema sets
63
+ * source.client.presentation === 'slot'. Selva reserves the input's cell and
64
+ * invokes this snippet in its place, passing { inputId, displayName, slotLabel,
65
+ * value }. Selva renders nothing itself and never interprets what the host puts
66
+ * here (e.g. an "Edit JSON" button that navigates back to a producer page).
67
+ */
68
+ clientSlot?: ClientSlot;
60
69
  }
61
70
 
62
71
  let {
@@ -83,9 +92,14 @@
83
92
  headerRight,
84
93
  header,
85
94
  onReady,
86
- externalScopeKey
95
+ externalScopeKey,
96
+ clientSlot
87
97
  }: Props = $props();
88
98
 
99
+ // Make the host's client-input slot available to InputControl deep in the tree.
100
+ // svelte-ignore state_referenced_locally
101
+ setClientSlot(clientSlot);
102
+
89
103
  const resolvedScopeKey = $derived(externalScopeKey || definitionKey || schema?.id || '');
90
104
 
91
105
  function createInitialValues(s: UISchema, scopeKey: string) {
@@ -2,6 +2,7 @@ import type { UISchema, ParameterPreset } from '@selvajs/schemas';
2
2
  import type { ActionButton } from '../../types/actionButton';
3
3
  import type { SolveFn } from '../../types/solveFn';
4
4
  import type { PresetLabels } from '../../types/presetLabels';
5
+ import { type ClientSlot } from '../../contexts/clientSlotContext.svelte';
5
6
  import type { Snippet } from 'svelte';
6
7
  interface Props {
7
8
  schema: UISchema;
@@ -45,6 +46,14 @@ interface Props {
45
46
  * values. If absent, falls back to definitionKey, then to schema.id.
46
47
  */
47
48
  externalScopeKey?: string;
49
+ /**
50
+ * Host-rendered element for client-sourced inputs whose schema sets
51
+ * source.client.presentation === 'slot'. Selva reserves the input's cell and
52
+ * invokes this snippet in its place, passing { inputId, displayName, slotLabel,
53
+ * value }. Selva renders nothing itself and never interprets what the host puts
54
+ * here (e.g. an "Edit JSON" button that navigates back to a producer page).
55
+ */
56
+ clientSlot?: ClientSlot;
48
57
  }
49
58
  declare const ComputeApp: import("svelte").Component<Props, {}, "">;
50
59
  type ComputeApp = ReturnType<typeof ComputeApp>;
@@ -20,6 +20,7 @@
20
20
  NumberInput,
21
21
  TextInput
22
22
  } from './inputs';
23
+ import { getClientSlot } from '../../contexts/clientSlotContext.svelte';
23
24
 
24
25
  interface Props {
25
26
  item: InputLayoutItem;
@@ -40,6 +41,18 @@
40
41
  const inputId = $derived(`input-${item.paramId}`);
41
42
  const label = $derived(displayName || item.displayName || item.paramId);
42
43
 
44
+ // Client-sourced input set to render a host element in its place. The 'hidden'
45
+ // presentation never reaches here (visible:false filters it out upstream), so a
46
+ // client slot source means presentation === 'slot'.
47
+ const clientSlotConfig = $derived.by(() => {
48
+ const source = (
49
+ item as { source?: { kind?: string; client?: { presentation?: string; slotLabel?: string } } }
50
+ ).source;
51
+ if (source?.kind !== 'client' || source.client?.presentation !== 'slot') return null;
52
+ return { slotLabel: source.client.slotLabel };
53
+ });
54
+ const clientSlot = getClientSlot();
55
+
43
56
  // Number range hint — shown next to label for sliders, under the input for plain number fields.
44
57
  const numberRangeHint = $derived.by(() => {
45
58
  if (!isNumberWidget(item)) return null;
@@ -61,87 +74,98 @@
61
74
  }
62
75
  </script>
63
76
 
64
- <Field.Field>
65
- <Field.Label for={inputId} class="gap-2 flex items-center">
66
- {label}
67
- {#if item.description}
68
- <Dialog.Root>
69
- <Dialog.Trigger class="p-1 cursor-help opacity-60 transition-opacity hover:opacity-100">
70
- <HelpCircle size={16} />
71
- </Dialog.Trigger>
72
- <Dialog.Content class="sm:max-w-md">
73
- <Dialog.Header>
74
- <Dialog.Title>{label}</Dialog.Title>
75
- <Dialog.Description>{item.description}</Dialog.Description>
76
- </Dialog.Header>
77
- </Dialog.Content>
78
- </Dialog.Root>
79
- {/if}
80
- {#if showRangeInLabel}
81
- <span class="text-xs font-normal text-muted-foreground">{numberRangeHint}</span>
82
- {/if}
83
- </Field.Label>
77
+ {#if clientSlotConfig}
78
+ {#if clientSlot}
79
+ {@render clientSlot({
80
+ inputId: item.paramId,
81
+ displayName: label,
82
+ slotLabel: clientSlotConfig.slotLabel,
83
+ value
84
+ })}
85
+ {/if}
86
+ {:else}
87
+ <Field.Field>
88
+ <Field.Label for={inputId} class="gap-2 flex items-center">
89
+ {label}
90
+ {#if item.description}
91
+ <Dialog.Root>
92
+ <Dialog.Trigger class="p-1 cursor-help opacity-60 transition-opacity hover:opacity-100">
93
+ <HelpCircle size={16} />
94
+ </Dialog.Trigger>
95
+ <Dialog.Content class="sm:max-w-md">
96
+ <Dialog.Header>
97
+ <Dialog.Title>{label}</Dialog.Title>
98
+ <Dialog.Description>{item.description}</Dialog.Description>
99
+ </Dialog.Header>
100
+ </Dialog.Content>
101
+ </Dialog.Root>
102
+ {/if}
103
+ {#if showRangeInLabel}
104
+ <span class="text-xs font-normal text-muted-foreground">{numberRangeHint}</span>
105
+ {/if}
106
+ </Field.Label>
84
107
 
85
- {#if isNumberWidget(item)}
86
- {@const config = item.config}
87
- <NumberInput
88
- {inputId}
89
- value={typeof value === 'number' ? value : undefined}
90
- {config}
91
- onChange={commit}
92
- {disabled}
93
- />
94
- {:else if isCheckboxWidget(item)}
95
- <CheckboxInput
96
- {inputId}
97
- value={typeof value === 'boolean' ? value : undefined}
98
- onChange={commit}
99
- {disabled}
100
- />
101
- {:else if isTextWidget(item)}
102
- {@const config = item.config}
103
- <TextInput
104
- {inputId}
105
- value={typeof value === 'string' ? value : ''}
106
- {config}
107
- onChange={commit}
108
- {disabled}
109
- />
110
- {:else if isDropdownWidget(item)}
111
- {@const config = item.config}
112
- {#if config.displayAs === 'checklist'}
113
- <ChecklistInput
108
+ {#if isNumberWidget(item)}
109
+ {@const config = item.config}
110
+ <NumberInput
114
111
  {inputId}
115
- value={Array.isArray(value)
116
- ? (value as string[])
117
- : typeof value === 'string' && value
118
- ? [value]
119
- : []}
112
+ value={typeof value === 'number' ? value : undefined}
120
113
  {config}
121
114
  onChange={commit}
122
115
  {disabled}
123
116
  />
124
- {:else}
125
- <DropdownInput
117
+ {:else if isCheckboxWidget(item)}
118
+ <CheckboxInput
119
+ {inputId}
120
+ value={typeof value === 'boolean' ? value : undefined}
121
+ onChange={commit}
122
+ {disabled}
123
+ />
124
+ {:else if isTextWidget(item)}
125
+ {@const config = item.config}
126
+ <TextInput
127
+ {inputId}
126
128
  value={typeof value === 'string' ? value : ''}
127
129
  {config}
128
130
  onChange={commit}
129
131
  {disabled}
130
132
  />
133
+ {:else if isDropdownWidget(item)}
134
+ {@const config = item.config}
135
+ {#if config.displayAs === 'checklist'}
136
+ <ChecklistInput
137
+ {inputId}
138
+ value={Array.isArray(value)
139
+ ? (value as string[])
140
+ : typeof value === 'string' && value
141
+ ? [value]
142
+ : []}
143
+ {config}
144
+ onChange={commit}
145
+ {disabled}
146
+ />
147
+ {:else}
148
+ <DropdownInput
149
+ value={typeof value === 'string' ? value : ''}
150
+ {config}
151
+ onChange={commit}
152
+ {disabled}
153
+ />
154
+ {/if}
155
+ {:else if isFileWidget(item)}
156
+ {@const config = item.config as FileInputWidgetConfig}
157
+ <FileInput
158
+ value={typeof value === 'string' ? value : ''}
159
+ acceptedFormats={config?.acceptedFormats ?? []}
160
+ onChange={(newValue) => commit(newValue)}
161
+ defaultInputMode={config?.defaultInputMode}
162
+ allowedInputModes={config?.allowedInputModes}
163
+ />
164
+ {:else if isColorWidget(item)}
165
+ <ColorInput
166
+ value={typeof value === 'string' ? value : '#000000'}
167
+ onChange={(newValue) => commit(newValue)}
168
+ />
131
169
  {/if}
132
- {:else if isFileWidget(item)}
133
- {@const config = item.config as FileInputWidgetConfig}
134
- <FileInput
135
- value={typeof value === 'string' ? value : ''}
136
- acceptedFormats={config?.acceptedFormats ?? []}
137
- onChange={(newValue) => commit(newValue)}
138
- defaultInputMode={config?.defaultInputMode}
139
- allowedInputModes={config?.allowedInputModes}
140
- />
141
- {:else if isColorWidget(item)}
142
- <ColorInput
143
- value={typeof value === 'string' ? value : '#000000'}
144
- onChange={(newValue) => commit(newValue)}
145
- />
146
- {/if}
147
- </Field.Field>
170
+ </Field.Field>
171
+ {/if}
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ export interface ClientSlotArgs {
3
+ /** Grasshopper parameter instance GUID (LayoutItem.paramId / SchemaInput.id). */
4
+ inputId: string;
5
+ displayName: string;
6
+ /** Author-set label from the schema, passed through untouched. May be undefined. */
7
+ slotLabel?: string;
8
+ /** The current value held for this input (e.g. the prefilled JSON), if any. */
9
+ value: unknown;
10
+ }
11
+ export type ClientSlot = Snippet<[ClientSlotArgs]>;
12
+ export declare function setClientSlot(slot: ClientSlot | undefined): void;
13
+ export declare function getClientSlot(): ClientSlot | undefined;
@@ -0,0 +1,8 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ const CLIENT_SLOT_CONTEXT_KEY = Symbol('client-slot-context');
3
+ export function setClientSlot(slot) {
4
+ setContext(CLIENT_SLOT_CONTEXT_KEY, slot);
5
+ }
6
+ export function getClientSlot() {
7
+ return getContext(CLIENT_SLOT_CONTEXT_KEY);
8
+ }
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export * from './schema/defaults';
9
9
  export * from './compute/solving.svelte';
10
10
  export * from './external/storage';
11
11
  export * from './contexts/footerContext.svelte';
12
+ export * from './contexts/clientSlotContext.svelte';
12
13
  export * from './composables/useFooterItem.svelte';
13
14
  export * from './utils';
14
15
  export { randomId } from './utils/randomId';
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ export * from './compute/solving.svelte';
17
17
  export * from './external/storage';
18
18
  // Contexts & Composables
19
19
  export * from './contexts/footerContext.svelte';
20
+ export * from './contexts/clientSlotContext.svelte';
20
21
  export * from './composables/useFooterItem.svelte';
21
22
  // Utils (cn function)
22
23
  export * from './utils';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selvajs/ui",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "Shared UI components and utilities for Selva applications",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -59,7 +59,7 @@
59
59
  "svelte": "^5",
60
60
  "tailwind-variants": "^3.2.2",
61
61
  "three": "^0.184.0",
62
- "@selvajs/schemas": "4.2.0"
62
+ "@selvajs/schemas": "^4.3.0"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "three": {
@@ -84,8 +84,8 @@
84
84
  "bits-ui": "^2.18.0",
85
85
  "svelte": "5.55.5",
86
86
  "tailwind-variants": "^3.2.2",
87
- "@selvajs/schemas": "4.2.0",
88
- "@selvajs/config": "0.0.0"
87
+ "@selvajs/config": "0.0.0",
88
+ "@selvajs/schemas": "4.3.0"
89
89
  },
90
90
  "scripts": {
91
91
  "dev": "vite dev",
@@ -14,6 +14,7 @@
14
14
  import AppLayout from './AppLayout.svelte';
15
15
  import StateDisplay from '../primitives/StateDisplay.svelte';
16
16
  import { getExternalInputs, readExternalValue } from '../../external/storage';
17
+ import { setClientSlot, type ClientSlot } from '../../contexts/clientSlotContext.svelte';
17
18
 
18
19
  import type { Snippet } from 'svelte';
19
20
 
@@ -57,6 +58,14 @@
57
58
  * values. If absent, falls back to definitionKey, then to schema.id.
58
59
  */
59
60
  externalScopeKey?: string;
61
+ /**
62
+ * Host-rendered element for client-sourced inputs whose schema sets
63
+ * source.client.presentation === 'slot'. Selva reserves the input's cell and
64
+ * invokes this snippet in its place, passing { inputId, displayName, slotLabel,
65
+ * value }. Selva renders nothing itself and never interprets what the host puts
66
+ * here (e.g. an "Edit JSON" button that navigates back to a producer page).
67
+ */
68
+ clientSlot?: ClientSlot;
60
69
  }
61
70
 
62
71
  let {
@@ -83,9 +92,14 @@
83
92
  headerRight,
84
93
  header,
85
94
  onReady,
86
- externalScopeKey
95
+ externalScopeKey,
96
+ clientSlot
87
97
  }: Props = $props();
88
98
 
99
+ // Make the host's client-input slot available to InputControl deep in the tree.
100
+ // svelte-ignore state_referenced_locally
101
+ setClientSlot(clientSlot);
102
+
89
103
  const resolvedScopeKey = $derived(externalScopeKey || definitionKey || schema?.id || '');
90
104
 
91
105
  function createInitialValues(s: UISchema, scopeKey: string) {
@@ -20,6 +20,7 @@
20
20
  NumberInput,
21
21
  TextInput
22
22
  } from '$lib/components/preview/inputs';
23
+ import { getClientSlot } from '$lib/contexts/clientSlotContext.svelte';
23
24
 
24
25
  interface Props {
25
26
  item: InputLayoutItem;
@@ -40,6 +41,18 @@
40
41
  const inputId = $derived(`input-${item.paramId}`);
41
42
  const label = $derived(displayName || item.displayName || item.paramId);
42
43
 
44
+ // Client-sourced input set to render a host element in its place. The 'hidden'
45
+ // presentation never reaches here (visible:false filters it out upstream), so a
46
+ // client slot source means presentation === 'slot'.
47
+ const clientSlotConfig = $derived.by(() => {
48
+ const source = (
49
+ item as { source?: { kind?: string; client?: { presentation?: string; slotLabel?: string } } }
50
+ ).source;
51
+ if (source?.kind !== 'client' || source.client?.presentation !== 'slot') return null;
52
+ return { slotLabel: source.client.slotLabel };
53
+ });
54
+ const clientSlot = getClientSlot();
55
+
43
56
  // Number range hint — shown next to label for sliders, under the input for plain number fields.
44
57
  const numberRangeHint = $derived.by(() => {
45
58
  if (!isNumberWidget(item)) return null;
@@ -61,87 +74,98 @@
61
74
  }
62
75
  </script>
63
76
 
64
- <Field.Field>
65
- <Field.Label for={inputId} class="gap-2 flex items-center">
66
- {label}
67
- {#if item.description}
68
- <Dialog.Root>
69
- <Dialog.Trigger class="p-1 cursor-help opacity-60 transition-opacity hover:opacity-100">
70
- <HelpCircle size={16} />
71
- </Dialog.Trigger>
72
- <Dialog.Content class="sm:max-w-md">
73
- <Dialog.Header>
74
- <Dialog.Title>{label}</Dialog.Title>
75
- <Dialog.Description>{item.description}</Dialog.Description>
76
- </Dialog.Header>
77
- </Dialog.Content>
78
- </Dialog.Root>
79
- {/if}
80
- {#if showRangeInLabel}
81
- <span class="text-xs font-normal text-muted-foreground">{numberRangeHint}</span>
82
- {/if}
83
- </Field.Label>
77
+ {#if clientSlotConfig}
78
+ {#if clientSlot}
79
+ {@render clientSlot({
80
+ inputId: item.paramId,
81
+ displayName: label,
82
+ slotLabel: clientSlotConfig.slotLabel,
83
+ value
84
+ })}
85
+ {/if}
86
+ {:else}
87
+ <Field.Field>
88
+ <Field.Label for={inputId} class="gap-2 flex items-center">
89
+ {label}
90
+ {#if item.description}
91
+ <Dialog.Root>
92
+ <Dialog.Trigger class="p-1 cursor-help opacity-60 transition-opacity hover:opacity-100">
93
+ <HelpCircle size={16} />
94
+ </Dialog.Trigger>
95
+ <Dialog.Content class="sm:max-w-md">
96
+ <Dialog.Header>
97
+ <Dialog.Title>{label}</Dialog.Title>
98
+ <Dialog.Description>{item.description}</Dialog.Description>
99
+ </Dialog.Header>
100
+ </Dialog.Content>
101
+ </Dialog.Root>
102
+ {/if}
103
+ {#if showRangeInLabel}
104
+ <span class="text-xs font-normal text-muted-foreground">{numberRangeHint}</span>
105
+ {/if}
106
+ </Field.Label>
84
107
 
85
- {#if isNumberWidget(item)}
86
- {@const config = item.config}
87
- <NumberInput
88
- {inputId}
89
- value={typeof value === 'number' ? value : undefined}
90
- {config}
91
- onChange={commit}
92
- {disabled}
93
- />
94
- {:else if isCheckboxWidget(item)}
95
- <CheckboxInput
96
- {inputId}
97
- value={typeof value === 'boolean' ? value : undefined}
98
- onChange={commit}
99
- {disabled}
100
- />
101
- {:else if isTextWidget(item)}
102
- {@const config = item.config}
103
- <TextInput
104
- {inputId}
105
- value={typeof value === 'string' ? value : ''}
106
- {config}
107
- onChange={commit}
108
- {disabled}
109
- />
110
- {:else if isDropdownWidget(item)}
111
- {@const config = item.config}
112
- {#if config.displayAs === 'checklist'}
113
- <ChecklistInput
108
+ {#if isNumberWidget(item)}
109
+ {@const config = item.config}
110
+ <NumberInput
114
111
  {inputId}
115
- value={Array.isArray(value)
116
- ? (value as string[])
117
- : typeof value === 'string' && value
118
- ? [value]
119
- : []}
112
+ value={typeof value === 'number' ? value : undefined}
120
113
  {config}
121
114
  onChange={commit}
122
115
  {disabled}
123
116
  />
124
- {:else}
125
- <DropdownInput
117
+ {:else if isCheckboxWidget(item)}
118
+ <CheckboxInput
119
+ {inputId}
120
+ value={typeof value === 'boolean' ? value : undefined}
121
+ onChange={commit}
122
+ {disabled}
123
+ />
124
+ {:else if isTextWidget(item)}
125
+ {@const config = item.config}
126
+ <TextInput
127
+ {inputId}
126
128
  value={typeof value === 'string' ? value : ''}
127
129
  {config}
128
130
  onChange={commit}
129
131
  {disabled}
130
132
  />
133
+ {:else if isDropdownWidget(item)}
134
+ {@const config = item.config}
135
+ {#if config.displayAs === 'checklist'}
136
+ <ChecklistInput
137
+ {inputId}
138
+ value={Array.isArray(value)
139
+ ? (value as string[])
140
+ : typeof value === 'string' && value
141
+ ? [value]
142
+ : []}
143
+ {config}
144
+ onChange={commit}
145
+ {disabled}
146
+ />
147
+ {:else}
148
+ <DropdownInput
149
+ value={typeof value === 'string' ? value : ''}
150
+ {config}
151
+ onChange={commit}
152
+ {disabled}
153
+ />
154
+ {/if}
155
+ {:else if isFileWidget(item)}
156
+ {@const config = item.config as FileInputWidgetConfig}
157
+ <FileInput
158
+ value={typeof value === 'string' ? value : ''}
159
+ acceptedFormats={config?.acceptedFormats ?? []}
160
+ onChange={(newValue) => commit(newValue)}
161
+ defaultInputMode={config?.defaultInputMode}
162
+ allowedInputModes={config?.allowedInputModes}
163
+ />
164
+ {:else if isColorWidget(item)}
165
+ <ColorInput
166
+ value={typeof value === 'string' ? value : '#000000'}
167
+ onChange={(newValue) => commit(newValue)}
168
+ />
131
169
  {/if}
132
- {:else if isFileWidget(item)}
133
- {@const config = item.config as FileInputWidgetConfig}
134
- <FileInput
135
- value={typeof value === 'string' ? value : ''}
136
- acceptedFormats={config?.acceptedFormats ?? []}
137
- onChange={(newValue) => commit(newValue)}
138
- defaultInputMode={config?.defaultInputMode}
139
- allowedInputModes={config?.allowedInputModes}
140
- />
141
- {:else if isColorWidget(item)}
142
- <ColorInput
143
- value={typeof value === 'string' ? value : '#000000'}
144
- onChange={(newValue) => commit(newValue)}
145
- />
146
- {/if}
147
- </Field.Field>
170
+ </Field.Field>
171
+ {/if}
@@ -0,0 +1,33 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ import type { Snippet } from 'svelte';
3
+
4
+ // Carries the host app's slot renderer down to InputControl without threading a
5
+ // prop through every layout layer (ComputeApp → AppLayout → TabLayout →
6
+ // TabContent → Group → InputControl).
7
+ //
8
+ // An input with source.kind === 'client' and source.client.presentation === 'slot'
9
+ // reserves its cell but renders nothing itself. Instead Selva invokes this snippet
10
+ // so the host can render its own element (e.g. an "Edit JSON" button). Selva never
11
+ // interprets what the host renders; `slotLabel` is passed through untouched.
12
+
13
+ export interface ClientSlotArgs {
14
+ /** Grasshopper parameter instance GUID (LayoutItem.paramId / SchemaInput.id). */
15
+ inputId: string;
16
+ displayName: string;
17
+ /** Author-set label from the schema, passed through untouched. May be undefined. */
18
+ slotLabel?: string;
19
+ /** The current value held for this input (e.g. the prefilled JSON), if any. */
20
+ value: unknown;
21
+ }
22
+
23
+ export type ClientSlot = Snippet<[ClientSlotArgs]>;
24
+
25
+ const CLIENT_SLOT_CONTEXT_KEY = Symbol('client-slot-context');
26
+
27
+ export function setClientSlot(slot: ClientSlot | undefined): void {
28
+ setContext(CLIENT_SLOT_CONTEXT_KEY, slot);
29
+ }
30
+
31
+ export function getClientSlot(): ClientSlot | undefined {
32
+ return getContext<ClientSlot | undefined>(CLIENT_SLOT_CONTEXT_KEY);
33
+ }
package/src/lib/index.ts CHANGED
@@ -24,6 +24,7 @@ export * from './external/storage';
24
24
 
25
25
  // Contexts & Composables
26
26
  export * from './contexts/footerContext.svelte';
27
+ export * from './contexts/clientSlotContext.svelte';
27
28
  export * from './composables/useFooterItem.svelte';
28
29
 
29
30
  // Utils (cn function)