@selvajs/ui 4.9.0 → 4.11.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.
@@ -54,7 +54,7 @@
54
54
  header?: Snippet;
55
55
  // Scopes sessionStorage for external-input values; falls back to definitionKey then schema.id.
56
56
  externalScopeKey?: string;
57
- // Renders client-sourced inputs with presentation === 'slot'; receives { inputId, displayName, slotLabel, value }.
57
+ // Renders client-sourced inputs with presentation === 'slot'; receives { inputId, displayName, value, onValueChange }.
58
58
  clientSlot?: ClientSlot;
59
59
  /**
60
60
  * UI language for the app's own chrome (viewer, panels, status text).
@@ -59,12 +59,10 @@
59
59
  // Client-sourced input set to render a host element in its place. The 'hidden'
60
60
  // presentation never reaches here (visible:false filters it out upstream), so a
61
61
  // client slot source means presentation === 'slot'.
62
- const clientSlotConfig = $derived.by(() => {
63
- const source = (
64
- item as { source?: { kind?: string; client?: { presentation?: string; slotLabel?: string } } }
65
- ).source;
66
- if (source?.kind !== 'client' || source.client?.presentation !== 'slot') return null;
67
- return { slotLabel: source.client.slotLabel };
62
+ const isClientSlot = $derived.by(() => {
63
+ const source = (item as { source?: { kind?: string; client?: { presentation?: string } } })
64
+ .source;
65
+ return source?.kind === 'client' && source.client?.presentation === 'slot';
68
66
  });
69
67
  const clientSlot = getClientSlot();
70
68
 
@@ -144,14 +142,32 @@
144
142
  }
145
143
  </script>
146
144
 
147
- {#if clientSlotConfig}
145
+ {#if isClientSlot}
148
146
  {#if clientSlot}
149
- {@render clientSlot({
150
- inputId: item.paramId,
151
- displayName: label,
152
- slotLabel: clientSlotConfig.slotLabel,
153
- value
154
- })}
147
+ <Field.Field>
148
+ <Field.Label class="gap-2 flex items-center">
149
+ {label}
150
+ {#if item.description}
151
+ <Dialog.Root>
152
+ <Dialog.Trigger class="p-1 cursor-help opacity-60 transition-opacity hover:opacity-100">
153
+ <HelpCircle size={16} />
154
+ </Dialog.Trigger>
155
+ <Dialog.Content class="sm:max-w-md">
156
+ <Dialog.Header>
157
+ <Dialog.Title>{label}</Dialog.Title>
158
+ <Dialog.Description>{item.description}</Dialog.Description>
159
+ </Dialog.Header>
160
+ </Dialog.Content>
161
+ </Dialog.Root>
162
+ {/if}
163
+ </Field.Label>
164
+ {@render clientSlot({
165
+ inputId: item.paramId,
166
+ displayName: label,
167
+ value,
168
+ onValueChange: commit
169
+ })}
170
+ </Field.Field>
155
171
  {/if}
156
172
  {:else if !hideDynamicListWhenEmpty}
157
173
  <Field.Field>
@@ -1,12 +1,18 @@
1
1
  import type { Snippet } from 'svelte';
2
+ import type { SupportedTypes } from '@selvajs/schemas';
2
3
  export interface ClientSlotArgs {
3
4
  /** Grasshopper parameter instance GUID (LayoutItem.paramId / SchemaInput.id). */
4
5
  inputId: string;
5
6
  displayName: string;
6
- /** Author-set label from the schema, passed through untouched. May be undefined. */
7
- slotLabel?: string;
8
7
  /** The current value held for this input (e.g. the prefilled JSON), if any. */
9
8
  value: unknown;
9
+ /**
10
+ * Commit a value for this input. Identical channel to a built-in widget's change
11
+ * — the value lands in the solve session and is sent to Compute on the next solve.
12
+ * `forceSolve` requests a solve even in manual-solve mode (system reconciliation).
13
+ * Lets a slot be an interactive control (a custom picker), not just a display cell.
14
+ */
15
+ onValueChange: (value: SupportedTypes, forceSolve?: boolean) => void;
10
16
  }
11
17
  export type ClientSlot = Snippet<[ClientSlotArgs]>;
12
18
  export declare function setClientSlot(slot: ClientSlot | undefined): void;
package/dist/public.js CHANGED
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // Scope: the compute-app SDK — everything an external host app needs to embed a
9
9
  // Grasshopper-driven app (ComputeApp), drive solves, and wire pre-step
10
- // producers. Verified against real consumers (parafa, parapet): they import
10
+ // producers. Verified against real external host apps: they import
11
11
  // ComputeApp + its types, the solve seam, and external/storage. Nothing else.
12
12
  //
13
13
  // Deliberately NOT public: design-system primitives (Button, Card, Dialog, …),
@@ -33,7 +33,7 @@ export { default as ErrorScreen } from './components/ErrorScreen.svelte';
33
33
  // driver interface). Exported so transports outside this package can satisfy
34
34
  // SolveDriver and drive a session. See CONTEXT.md.
35
35
  export { createSolveSession, createRequestResponseDriver } from './compute/createSolveSession.svelte';
36
- // Pre-step producer transit storage (parafa wires producers via these).
36
+ // Pre-step producer transit storage (host apps wire producers via these).
37
37
  export * from './external/storage';
38
38
  // Schema utilities a ComputeApp host reasonably needs to read/shape values.
39
39
  export * from './schema/defaults';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selvajs/ui",
3
- "version": "4.9.0",
3
+ "version": "4.11.0",
4
4
  "description": "Shared UI components and utilities for Selva applications",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -43,7 +43,7 @@
43
43
  "svelte": "^5",
44
44
  "tailwind-variants": "^3.2.2",
45
45
  "three": "^0.184.0",
46
- "@selvajs/schemas": "^4.5.0"
46
+ "@selvajs/schemas": "^4.6.0"
47
47
  },
48
48
  "peerDependenciesMeta": {
49
49
  "three": {
@@ -73,7 +73,7 @@
73
73
  "tailwind-variants": "^3.2.2",
74
74
  "vitest": "^3.2.6",
75
75
  "@selvajs/config": "0.0.1",
76
- "@selvajs/schemas": "4.5.0"
76
+ "@selvajs/schemas": "4.6.0"
77
77
  },
78
78
  "scripts": {
79
79
  "dev": "vite dev",
@@ -54,7 +54,7 @@
54
54
  header?: Snippet;
55
55
  // Scopes sessionStorage for external-input values; falls back to definitionKey then schema.id.
56
56
  externalScopeKey?: string;
57
- // Renders client-sourced inputs with presentation === 'slot'; receives { inputId, displayName, slotLabel, value }.
57
+ // Renders client-sourced inputs with presentation === 'slot'; receives { inputId, displayName, value, onValueChange }.
58
58
  clientSlot?: ClientSlot;
59
59
  /**
60
60
  * UI language for the app's own chrome (viewer, panels, status text).
@@ -59,12 +59,10 @@
59
59
  // Client-sourced input set to render a host element in its place. The 'hidden'
60
60
  // presentation never reaches here (visible:false filters it out upstream), so a
61
61
  // client slot source means presentation === 'slot'.
62
- const clientSlotConfig = $derived.by(() => {
63
- const source = (
64
- item as { source?: { kind?: string; client?: { presentation?: string; slotLabel?: string } } }
65
- ).source;
66
- if (source?.kind !== 'client' || source.client?.presentation !== 'slot') return null;
67
- return { slotLabel: source.client.slotLabel };
62
+ const isClientSlot = $derived.by(() => {
63
+ const source = (item as { source?: { kind?: string; client?: { presentation?: string } } })
64
+ .source;
65
+ return source?.kind === 'client' && source.client?.presentation === 'slot';
68
66
  });
69
67
  const clientSlot = getClientSlot();
70
68
 
@@ -144,14 +142,32 @@
144
142
  }
145
143
  </script>
146
144
 
147
- {#if clientSlotConfig}
145
+ {#if isClientSlot}
148
146
  {#if clientSlot}
149
- {@render clientSlot({
150
- inputId: item.paramId,
151
- displayName: label,
152
- slotLabel: clientSlotConfig.slotLabel,
153
- value
154
- })}
147
+ <Field.Field>
148
+ <Field.Label class="gap-2 flex items-center">
149
+ {label}
150
+ {#if item.description}
151
+ <Dialog.Root>
152
+ <Dialog.Trigger class="p-1 cursor-help opacity-60 transition-opacity hover:opacity-100">
153
+ <HelpCircle size={16} />
154
+ </Dialog.Trigger>
155
+ <Dialog.Content class="sm:max-w-md">
156
+ <Dialog.Header>
157
+ <Dialog.Title>{label}</Dialog.Title>
158
+ <Dialog.Description>{item.description}</Dialog.Description>
159
+ </Dialog.Header>
160
+ </Dialog.Content>
161
+ </Dialog.Root>
162
+ {/if}
163
+ </Field.Label>
164
+ {@render clientSlot({
165
+ inputId: item.paramId,
166
+ displayName: label,
167
+ value,
168
+ onValueChange: commit
169
+ })}
170
+ </Field.Field>
155
171
  {/if}
156
172
  {:else if !hideDynamicListWhenEmpty}
157
173
  <Field.Field>
@@ -1,5 +1,6 @@
1
1
  import { getContext, setContext } from 'svelte';
2
2
  import type { Snippet } from 'svelte';
3
+ import type { SupportedTypes } from '@selvajs/schemas';
3
4
 
4
5
  // Carries the host app's slot renderer down to InputControl without threading a
5
6
  // prop through every layout layer (ComputeApp → AppLayout → TabLayout →
@@ -7,17 +8,24 @@ import type { Snippet } from 'svelte';
7
8
  //
8
9
  // An input with source.kind === 'client' and source.client.presentation === 'slot'
9
10
  // 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.
11
+ // so the host can render its own element (e.g. an "Edit JSON" button, or a custom
12
+ // picker). Selva never interprets what the host renders. The host may COMMIT a value
13
+ // back via `onValueChange`, which flows into the solve exactly like any built-in
14
+ // widget's change.
12
15
 
13
16
  export interface ClientSlotArgs {
14
17
  /** Grasshopper parameter instance GUID (LayoutItem.paramId / SchemaInput.id). */
15
18
  inputId: string;
16
19
  displayName: string;
17
- /** Author-set label from the schema, passed through untouched. May be undefined. */
18
- slotLabel?: string;
19
20
  /** The current value held for this input (e.g. the prefilled JSON), if any. */
20
21
  value: unknown;
22
+ /**
23
+ * Commit a value for this input. Identical channel to a built-in widget's change
24
+ * — the value lands in the solve session and is sent to Compute on the next solve.
25
+ * `forceSolve` requests a solve even in manual-solve mode (system reconciliation).
26
+ * Lets a slot be an interactive control (a custom picker), not just a display cell.
27
+ */
28
+ onValueChange: (value: SupportedTypes, forceSolve?: boolean) => void;
21
29
  }
22
30
 
23
31
  export type ClientSlot = Snippet<[ClientSlotArgs]>;
package/src/lib/public.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // Scope: the compute-app SDK — everything an external host app needs to embed a
9
9
  // Grasshopper-driven app (ComputeApp), drive solves, and wire pre-step
10
- // producers. Verified against real consumers (parafa, parapet): they import
10
+ // producers. Verified against real external host apps: they import
11
11
  // ComputeApp + its types, the solve seam, and external/storage. Nothing else.
12
12
  //
13
13
  // Deliberately NOT public: design-system primitives (Button, Card, Dialog, …),
@@ -56,10 +56,10 @@ export {
56
56
  } from './compute/createSolveSession.svelte';
57
57
 
58
58
  // Client-slot context type (host apps render their own cell for client-sourced
59
- // inputs; parafa uses ClientSlotArgs).
59
+ // inputs, and may commit a value back via ClientSlotArgs.onValueChange).
60
60
  export type { ClientSlotArgs, ClientSlot } from './contexts/clientSlotContext.svelte';
61
61
 
62
- // Pre-step producer transit storage (parafa wires producers via these).
62
+ // Pre-step producer transit storage (host apps wire producers via these).
63
63
  export * from './external/storage';
64
64
 
65
65
  // Schema utilities a ComputeApp host reasonably needs to read/shape values.