Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/apollo-wind/apollo-ai-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ Put the control inside `InputGroup`. Apollo's controls detect the group and beco
- A custom control that also renders standalone can read `useInputGroup()` (`inGroup`, `layout`, `anchor`).
- `FieldMenu` is the trailing menu: the field's modes, then its actions (`actions`), or an overflow menu alone (`modesDisabled`). Built-in modes are listed by id (`modes={['literal', 'expression', 'variable', 'prompt']}`; default `['literal', 'expression']`); `builtInValueModes(strings)` returns their options. `ValueModeIndicator` shows `=` in expression mode; inside MetadataForm a mode definition's `indicator` marks any other mode.
- `InsertVariableAction` renders `variables` (`VariablePickerItem[]`) as supplied, with no root of its own; pass a function to resolve them when the picker opens. Its strings include the picker's search placeholder and empty state. `AiAssistAction`'s `onGenerate(prompt)` may return a promise (any thenable): the prompt stays busy until it settles and shows `strings.error` on rejection. `disabled` disables the trigger.
- Host picker: `VariablePicker`, `InsertVariableAction` and `VariableValueControl` take `renderPicker: RenderVariablePicker` (`({ value?, onSelect(value, item?), close }) => ReactNode`; `onSelect` commits and closes; `variables` is then optional), keeping wind's trigger, popover, focus return and strings. `VariableValueControl` also takes `describeReference(reference) => { label, icon? }` for its chip. The slot is called only while the popover is open.
- Localization: every addon and action takes a `strings` prop merged over its `DEFAULT_*_STRINGS`. There is no provider.

#### Tabs
Expand Down Expand Up @@ -399,8 +400,8 @@ Exports (all from `@/components/forms`): MetadataForm, ModeAwareField, FormField
FieldControl, FIELD_CONTROL_GEOMETRY, FormDesigner, FormStateViewer, StringListField, RulesEngine,
RuleBuilder, ExpressionBuilder, DataFetcher, DataSourceBuilder, the built-in plugins
(analyticsPlugin, autoSavePlugin, workflowPlugin, auditPlugin, validationPlugin, formattingPlugin),
createInsertVariableAction, createAiAssistAction, createClearAction, envelopeCodec, literalValues,
VALUE_MODE_OPAQUE, DEFAULT_METADATA_FORM_STRINGS.
createInsertVariableAction, createAiAssistAction, createClearAction, createVariableModeControl,
envelopeCodec, literalValues, VALUE_MODE_OPAQUE, DEFAULT_METADATA_FORM_STRINGS.

Field types: text, email, textarea, number, select, multiselect, radio, checkbox, switch, boolean
(True / False radios with an unset state, `boolean | null`), slider (`maxRef` reads its max from
Expand All @@ -423,6 +424,9 @@ registered unless replaced), built from `createInsertVariableAction`, `createAiA
`FormPlugin.variables` (Insert variable falls back to them and hides itself in Variable mode; the
Variable control's picker reads them); and
strings for every built-in through `FormPlugin.strings` (`valueModes`, `boolean`, `insertVariable`, `aiAssist`, `clear`, `validation`).
A host with its own picker passes it as `renderPicker` to `createInsertVariableAction` and to
`createVariableModeControl` (registered as `FormPlugin.valueModes.definitions.variable.control`, with
`describeReference` for chip labels).
Keep plugin objects stable (module constants or memoized), pass `variables` as a function when the
list is long or changes, and keep codecs pure. The default codec stores every value as `{ $mode, value }` (no `value`
once cleared) and reads a raw value as literal, so code that reads form values must expect the
Expand Down
10 changes: 7 additions & 3 deletions packages/apollo-wind/docs/AI_CONSUMER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@ Same props as Row.
| `BooleanRadioGroup` | True / False / not set | `value`: boolean\|null, `onValueChange`, `strings` |
| `FieldMenu` | Mode and actions menu | `mode`, `onSelect`, `modes`, `actions`, `modesDisabled`, `strings` |
| `ValueModeIndicator` | `=` before an expression | `mode`, `strings` |
| `InsertVariableAction` | Header variable picker | `variables`, `onInsert`, `compact`, `strings` |
| `InsertVariableAction` | Header variable picker | `variables`, `onInsert`, `compact`, `strings`. `renderPicker` renders the host's own picker instead, and `variables` is then optional |
| `AiAssistAction` | Header AI prompt | `onGenerate`, `hint`, `strings` |
| `VariableValueControl` | Variable-bound control | `value`, `onChange`, `variables`, `strings` |
| `VariableValueControl` | Variable-bound control | `value`, `onChange`, `variables`, `strings`. `renderPicker` renders the host's own picker; `describeReference(reference)` gives the chip a `{ label, icon? }` |
| `PromptValueControl` | Prompt for an agent | `value`, `onChange`, `strings`. Render in `InputGroup` with `variant="agent"` `layout="grow"` |
| `QuickFormField` | Quick Form builder field | Only where end users configure fields in a HITL Quick Form. `fieldTypes`, `renderModeControl`, `variables`, `strings` |
| `Label` | Bare label primitive | `htmlFor`. For a field use `FormFieldLabel` |
Expand Down Expand Up @@ -926,7 +926,7 @@ an expression, a variable or a prompt, switched from a trailing menu.
| ------------ | ----------- | ----------------------------------------------------------- |
| `literal` | Fixed value | The field type's own control |
| `expression` | Expression | A plain input behind an `=` indicator |
| `variable` | Variable | `VariableValueControl`, picking from `FormPlugin.variables` |
| `variable` | Variable | `VariableValueControl`, picking from `FormPlugin.variables`, or the host's control from `createVariableModeControl` |
| `prompt` | Prompt | `PromptValueControl` in an agent-tinted `InputGroup` |

| Key | Description |
Expand Down Expand Up @@ -959,6 +959,10 @@ Field actions:
against `FormPlugin.fieldActions`; unregistered ids are skipped.
- Insert variable writes at the caret of a text control, otherwise it switches the field to
`expression` or `variable` mode, asking before it replaces a value.
- A host with its own picker passes it as `renderPicker` to `createInsertVariableAction` (it wins
over `variables`, and its pick takes the same path) and to `createVariableModeControl`, registered
as `FormPlugin.valueModes.definitions.variable.control`. That control also takes
`describeReference(reference, { field })` for the chip's label and icon.
- `badge` adds text after the label, such as the value's type.

Any of `valueModes`, `headerActions`, `menuActions` or `badge` renders the field through
Expand Down
12 changes: 11 additions & 1 deletion packages/apollo-wind/src/components/forms/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -916,7 +916,7 @@ Validation: a fixed value is checked by its field type's `validation`; any other

| Factory | Action |
| --------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `createInsertVariableAction({ variables?, formatReference?, strings?, id? })` | Inserts a variable. An `insertable` control takes it at the caret; otherwise the field switches to `expression`, then `variable`, asking before it replaces a value. Hidden in Variable mode. |
| `createInsertVariableAction({ variables?, renderPicker?, formatReference?, strings?, id? })` | Inserts a variable. An `insertable` control takes it at the caret; otherwise the field switches to `expression`, then `variable`, asking before it replaces a value. Hidden in Variable mode. `renderPicker` renders the host's own picker and wins over `variables`. |
| `createAiAssistAction({ generate, hint?, placeholder?, strings?, id? })` | Asks for a prompt and writes `generate`'s result in the field's mode, or the `mode` it returns. Skips the write if the field became disabled; closing the prompt does not cancel the request. |
| `createClearAction({ strings?, id? })` | Clears the value, keeping the mode. Registered as `clear` by default. |

Expand All @@ -926,6 +926,15 @@ Register an action under the same id to replace a built-in.

- `FormPlugin.variables` feeds both Insert variable and the `variable` mode: a list, or a function
called each time a picker opens (never during render). The last plugin that gives them wins.
- A host with its own picker passes it as `renderPicker` to `createInsertVariableAction` and to
`createVariableModeControl`, whose registration goes in
`FormPlugin.valueModes.definitions.variable.control`. The picker, a `FormVariablePicker`, is
called only while a picker is open, with `{ field, value, onSelect, close }`: `value` is the
bound reference in the `variable` mode, and `onSelect(reference, item?)` commits it and closes,
so Insert still writes at the caret or asks before replacing. Wind keeps the trigger, popover,
focus return and strings.
- `createVariableModeControl({ describeReference })` gives the `variable` mode's chip the host's
`{ label, icon? }` for a bound reference; returning nothing shows the reference as stored.
- `FormPlugin.strings` overrides any built-in string by group (`valueModes`, `boolean`,
`insertVariable`, `aiAssist`, `clear`, `validation`); later plugins win per string. The English
defaults are `DEFAULT_METADATA_FORM_STRINGS`.
Expand All @@ -935,6 +944,7 @@ Register an action under the same id to replace a built-in.
- Keep plugin objects stable: a module constant, or memoized. A new plugin object rebuilds the
registries and re-renders every field; a new array of the same plugins is fine.
- Pass `variables` as a function when the list is long or changes.
- Call `createVariableModeControl` once, at module scope: each call makes a new control component.
- Keep codecs pure: no live values captured in a codec.

## 📊 Data Sources
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createAiAssistAction,
createClearAction,
createInsertVariableAction,
type FormVariablePicker,
} from './field-actions';
import type { FormPlugin, FormSchema } from './form-schema';
import { MetadataForm } from './metadata-form';
Expand All @@ -23,6 +24,10 @@ per-field placeholder) and \`createClearAction\`, which is registered by default
the caret of a control that takes text; otherwise it switches the field to an expression, asking
first before it replaces a value other than a number or boolean. Their strings come from
\`FormPlugin.strings\`, and a factory's own \`strings\` win.

A host can render its own panels in these popovers.
\`createInsertVariableAction({ renderPicker })\` replaces the picker and wins over \`variables\`; a
pick is written as a picked variable would be.
`,
},
},
Expand Down Expand Up @@ -146,3 +151,41 @@ const plugin: FormPlugin = {
export const Actions: Story = {
render: () => <Demo schema={schema} plugins={[plugin]} />,
};

// Stand-in for a host's own picker, such as a virtualized tree with an Assets tab.
const hostPicker: FormVariablePicker = ({ onSelect, close }) => (
<div className="grid gap-1 p-2 text-xs">
<span className="px-1 text-foreground-muted">Host picker</span>
{['$vars.orderId', '$assets.apiKey'].map((reference) => (
<button
key={reference}
type="button"
onClick={() => onSelect(reference)}
className="rounded px-2 py-1 text-left font-mono hover:bg-surface-overlay"
>
{reference}
</button>
))}
<button type="button" onClick={close} className="px-1 text-left text-foreground-muted">
Cancel
</button>
</div>
);

const hostPlugin: FormPlugin = {
name: 'host',
fieldActions: {
header: {
'insert-variable': createInsertVariableAction({
renderPicker: hostPicker,
formatReference: (reference, { mode }) =>
mode === 'literal' ? `{{ ${reference} }}` : reference,
}),
},
},
};

/** Insert variable rendering the host's own picker, through its factory. */
export const HostPanels: Story = {
render: () => <Demo schema={schema} plugins={[hostPlugin]} />,
};
54 changes: 49 additions & 5 deletions packages/apollo-wind/src/components/forms/field-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import {
InsertVariableAction,
type InsertVariableActionStrings,
} from '@/components/ui/field-actions/insert-variable-action';
import type { VariablePickerItem } from '@/components/ui/variable-picker';
import type {
RenderVariablePicker,
VariablePickerItem,
VariablePickerSlotProps,
} from '@/components/ui/variable-picker';
import type { FieldMetadata, FormPlugin, FormVariables, ValueModeId } from './form-schema';
import type { ClearActionStrings, MetadataFormStrings } from './form-strings';
import type { DecodedValue, ValueModeControlHandle } from './value-modes';
Expand Down Expand Up @@ -76,6 +80,29 @@ export interface FieldMenuAction {
/** A variable Insert variable offers, rendered as supplied. */
export type ValueModeVariable = VariablePickerItem;

/**
* A host's own variable picker for a form field, rendered in place of the built-in tree. Called
* only while the picker is open, with the field it picks for.
*/
export type FormVariablePicker = (
props: VariablePickerSlotProps & { field: FieldMetadata }
) => React.ReactNode;

/** Binds `picker` to each field once, so the action's props stay stable across renders. */
export function bindVariablePicker(
picker: FormVariablePicker
): (field: FieldMetadata) => RenderVariablePicker {
const bound = new WeakMap<FieldMetadata, RenderVariablePicker>();
return (field) => {
let render = bound.get(field);
if (!render) {
render = (props) => picker({ ...props, field });
bound.set(field, render);
}
return render;
};
}

/** `source` for `field`: a function is called now, so only call this when a picker opens. */
export function resolveVariables(
source: FormVariables | undefined,
Expand Down Expand Up @@ -115,6 +142,11 @@ export interface FieldActionRegistry {
export interface InsertVariableActionOptions {
/** Defaults to the form's `FormPlugin.variables`. */
variables?: FormVariables;
/**
* The host's own picker, rendered in place of a picker over `variables`, which it wins over. Its
* pick is written like a picked variable: at the caret, or as the value once confirmed.
*/
renderPicker?: FormVariablePicker;
/**
* How a picked reference is written at the caret, such as `{{ ref }}` inside fixed-value text.
* A field that switches to `expression` or `variable` to take the reference gets it as picked.
Expand Down Expand Up @@ -171,28 +203,40 @@ function insertTarget(
/** Insert variable: a variable picker behind the label that writes the picked reference. */
export function createInsertVariableAction({
variables,
renderPicker,
formatReference,
strings,
id = 'insert-variable',
}: InsertVariableActionOptions): FieldHeaderAction {
const pickerFor = renderPicker && bindVariablePicker(renderPicker);
return {
id,
render: (ctx) => {
// A variable's own control already picks the variable.
if (ctx.mode?.mode === 'variable') return null;
const text = { ...ctx.strings.insertVariable, ...strings };
const insert = insertTarget(ctx, formatReference, text);
// Resolved at pick time: the mode or control may have changed since render.
const onInsert =
insert && ((reference: string) => insertTarget(ctx, formatReference, text)?.(reference));
if (pickerFor) {
return (
<InsertVariableAction
renderPicker={pickerFor(ctx.field)}
onInsert={onInsert}
disabled={ctx.disabled}
strings={text}
/>
);
}
return (
<InsertVariableAction
variables={
Array.isArray(variables)
? variables
: () => (variables ? resolveVariables(variables, ctx.field) : ctx.variables())
}
// Resolved at pick time: the mode or control may have changed since render.
onInsert={
insert && ((reference) => insertTarget(ctx, formatReference, text)?.(reference))
}
onInsert={onInsert}
disabled={ctx.disabled}
strings={text}
/>
Expand Down
5 changes: 5 additions & 0 deletions packages/apollo-wind/src/components/forms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export {
type FieldActionsPluginConfig,
type FieldHeaderAction,
type FieldMenuAction,
type FormVariablePicker,
type InsertVariableActionOptions,
resolveVariables,
type ValueModeVariable,
Expand Down Expand Up @@ -112,3 +113,7 @@ export {
type ValueModeRegistry,
type ValueModesPluginConfig,
} from './value-modes';
export {
createVariableModeControl,
type VariableModeControlOptions,
} from './variable-mode-control';
Loading
Loading