@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.
- package/dist/components/compute/ComputeApp.svelte +15 -1
- package/dist/components/compute/ComputeApp.svelte.d.ts +9 -0
- package/dist/components/preview/InputControl.svelte +96 -72
- package/dist/contexts/clientSlotContext.svelte.d.ts +13 -0
- package/dist/contexts/clientSlotContext.svelte.js +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +4 -4
- package/src/lib/components/compute/ComputeApp.svelte +15 -1
- package/src/lib/components/preview/InputControl.svelte +96 -72
- package/src/lib/contexts/clientSlotContext.svelte.ts +33 -0
- package/src/lib/index.ts +1 -0
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
{
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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={
|
|
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
|
-
<
|
|
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
|
-
|
|
133
|
-
|
|
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.
|
|
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.
|
|
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/
|
|
88
|
-
"@selvajs/
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
{
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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={
|
|
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
|
-
<
|
|
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
|
-
|
|
133
|
-
|
|
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