@pipe0/react 0.0.2 → 0.0.4

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.
Files changed (25) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/components/defaults/adapters/key-value-list-input.mjs +3 -1
  3. package/dist/components/defaults/adapters/key-value-list-input.mjs.map +1 -1
  4. package/dist/components/defaults/adapters/prompt-input.mjs +1 -0
  5. package/dist/components/defaults/adapters/prompt-input.mjs.map +1 -1
  6. package/dist/components/defaults/adapters/tagged-text-input.mjs +1 -0
  7. package/dist/components/defaults/adapters/tagged-text-input.mjs.map +1 -1
  8. package/dist/components/defaults/adapters/template-input.mjs +1 -0
  9. package/dist/components/defaults/adapters/template-input.mjs.map +1 -1
  10. package/dist/components/internal/LiquidEditor/LiquidEditor.mjs +61 -37
  11. package/dist/components/internal/LiquidEditor/LiquidEditor.mjs.map +1 -1
  12. package/dist/components/internal/LiquidEditor/UnifiedReferencePicker.mjs +45 -31
  13. package/dist/components/internal/LiquidEditor/UnifiedReferencePicker.mjs.map +1 -1
  14. package/dist/components/internal/suggestion-menu/suggestion-menu.mjs +17 -9
  15. package/dist/components/internal/suggestion-menu/suggestion-menu.mjs.map +1 -1
  16. package/dist/components/ui/button.d.mts +2 -2
  17. package/dist/hooks/use-async-remote-source.mjs +93 -0
  18. package/dist/hooks/use-async-remote-source.mjs.map +1 -0
  19. package/dist/hooks/use-pipe-catalog-table.d.mts +8 -8
  20. package/dist/styles/pipe0-form.css +0 -48
  21. package/dist/types/field-props.d.mts +26 -2
  22. package/dist/types/field-props.d.mts.map +1 -1
  23. package/dist/utils/build-section-handlers.mjs +8 -3
  24. package/dist/utils/build-section-handlers.mjs.map +1 -1
  25. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @pipe0/elements-react
2
2
 
3
+ ## 0.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - @pipe0/base@0.0.4
8
+
9
+ ## 0.0.3
10
+
11
+ ### Patch Changes
12
+
13
+ - Update pipe_ids
14
+ - Updated dependencies
15
+ - @pipe0/base@0.0.3
16
+
3
17
  ## 0.0.2
4
18
 
5
19
  ### Patch Changes
@@ -19,7 +19,7 @@ import { Plus, X } from "lucide-react";
19
19
  * single legend renders once below the entire list.
20
20
  */
21
21
  function KeyValueListInputAdapter(field) {
22
- const { rows, addRow, removeRow, setKey, setValue, searchSecrets, meta } = field;
22
+ const { rows, addRow, removeRow, setKey, setValue, searchSecrets, searchConstants, meta } = field;
23
23
  const maxItems = meta.maxItems ?? 50;
24
24
  const canAdd = rows.length < maxItems;
25
25
  const [ids, setIds] = useState(() => rows.map(() => crypto.randomUUID()));
@@ -50,6 +50,7 @@ function KeyValueListInputAdapter(field) {
50
50
  onChange: (v) => setKey(index, v),
51
51
  inputFields: meta.inputFields ?? [],
52
52
  searchSecrets,
53
+ searchConstants,
53
54
  placeholder: meta.keyPlaceholder ?? meta.keyLabel ?? "Key",
54
55
  hideLegend: true
55
56
  })
@@ -61,6 +62,7 @@ function KeyValueListInputAdapter(field) {
61
62
  onChange: (v) => setValue(index, v),
62
63
  inputFields: meta.inputFields ?? [],
63
64
  searchSecrets,
65
+ searchConstants,
64
66
  placeholder: meta.valuePlaceholder ?? meta.valueLabel ?? "Value",
65
67
  hideLegend: true
66
68
  })
@@ -1 +1 @@
1
- {"version":3,"file":"key-value-list-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/key-value-list-input.tsx"],"sourcesContent":["import { Plus, X } from \"lucide-react\";\nimport { useState } from \"react\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport { FieldLegend } from \"../../internal/field-legend.js\";\nimport { LiquidEditor } from \"../../internal/LiquidEditor/LiquidEditor.js\";\nimport { Button } from \"../../ui/button.js\";\n\n/**\n * Adapter for `pipesKeyValueListInput`. Each row's key and value cells use\n * `LiquidEditor` so they support `/` references (input fields + secrets +\n * constants in one picker). The kvl is used for things like HTTP headers\n * and query params where either column may legitimately reference a field\n * (e.g. a per-record `pageToken`) or a secret (e.g. an Authorization\n * value).\n *\n * The unified legend would otherwise render twice per row (once per\n * cell) — too noisy. The per-cell editors run with `hideLegend`; a\n * single legend renders once below the entire list.\n */\nexport function KeyValueListInputAdapter(field: FieldHandle<\"key_value_list_input\">) {\n const { rows, addRow, removeRow, setKey, setValue, searchSecrets, meta } = field;\n const maxItems = meta.maxItems ?? 50;\n const canAdd = rows.length < maxItems;\n\n // Stable per-row identifiers, kept in lockstep with `rows`. The form value\n // is positional — `[{key, value}, ...]` — so reordering or removing the\n // row at index N would alias N's editor onto another row's content if we\n // keyed by index. We mirror mutations through `handleAdd`/`handleRemove`,\n // and lazily pad if `rows` was extended externally (e.g. form reset).\n const [ids, setIds] = useState<string[]>(() => rows.map(() => crypto.randomUUID()));\n if (ids.length < rows.length) {\n setIds((prev) => [\n ...prev,\n ...Array.from({ length: rows.length - prev.length }, () => crypto.randomUUID()),\n ]);\n }\n\n const handleAdd = () => {\n setIds((prev) => [...prev, crypto.randomUUID()]);\n addRow();\n };\n const handleRemove = (index: number) => {\n setIds((prev) => prev.filter((_, i) => i !== index));\n removeRow(index);\n };\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-2\">\n {rows.length === 0 && (\n <p className=\"pz:text-xs pz:text-muted-foreground\">\n {meta.keyLabel ? `No ${meta.keyLabel.toLowerCase()}s yet.` : \"No entries yet.\"}\n </p>\n )}\n {rows.map((row, index) => (\n <div key={ids[index]} className=\"pz:flex pz:items-start pz:gap-2\">\n <div className=\"pz:basis-1/3 pz:rounded-md pz:border pz:border-input pz:bg-transparent\">\n <LiquidEditor\n value={row.key}\n onChange={(v) => setKey(index, v)}\n inputFields={meta.inputFields ?? []}\n searchSecrets={searchSecrets}\n placeholder={meta.keyPlaceholder ?? meta.keyLabel ?? \"Key\"}\n hideLegend\n />\n </div>\n <div className=\"pz:flex-1 pz:rounded-md pz:border pz:border-input pz:bg-transparent\">\n <LiquidEditor\n value={row.value}\n onChange={(v) => setValue(index, v)}\n inputFields={meta.inputFields ?? []}\n searchSecrets={searchSecrets}\n placeholder={meta.valuePlaceholder ?? meta.valueLabel ?? \"Value\"}\n hideLegend\n />\n </div>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n aria-label=\"Remove row\"\n onClick={() => handleRemove(index)}\n >\n <X className=\"pz:size-4\" />\n </Button>\n </div>\n ))}\n {rows.length > 0 && <FieldLegend entries={[{ key: \"/\", label: \"to insert a reference\" }]} />}\n <div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={handleAdd} disabled={!canAdd}>\n <Plus className=\"pz:size-4 pz:mr-1\" />\n Add {meta.keyLabel ?? \"row\"}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,yBAAyB,OAA4C;CACnF,MAAM,EAAE,MAAM,QAAQ,WAAW,QAAQ,UAAU,eAAe,SAAS;CAC3E,MAAM,WAAW,KAAK,YAAY;CAClC,MAAM,SAAS,KAAK,SAAS;CAO7B,MAAM,CAAC,KAAK,UAAU,eAAyB,KAAK,UAAU,OAAO,YAAY,CAAC,CAAC;AACnF,KAAI,IAAI,SAAS,KAAK,OACpB,SAAQ,SAAS,CACf,GAAG,MACH,GAAG,MAAM,KAAK,EAAE,QAAQ,KAAK,SAAS,KAAK,QAAQ,QAAQ,OAAO,YAAY,CAAC,CAChF,CAAC;CAGJ,MAAM,kBAAkB;AACtB,UAAQ,SAAS,CAAC,GAAG,MAAM,OAAO,YAAY,CAAC,CAAC;AAChD,UAAQ;;CAEV,MAAM,gBAAgB,UAAkB;AACtC,UAAQ,SAAS,KAAK,QAAQ,GAAG,MAAM,MAAM,MAAM,CAAC;AACpD,YAAU,MAAM;;AAGlB,QACE,qBAAC,OAAD;EAAK,WAAQ;EAAQ,WAAU;YAA/B;GACG,KAAK,WAAW,KACf,oBAAC,KAAD;IAAG,WAAU;cACV,KAAK,WAAW,MAAM,KAAK,SAAS,aAAa,CAAC,UAAU;IAC3D;GAEL,KAAK,KAAK,KAAK,UACd,qBAAC,OAAD;IAAsB,WAAU;cAAhC;KACE,oBAAC,OAAD;MAAK,WAAU;gBACb,oBAAC,cAAD;OACE,OAAO,IAAI;OACX,WAAW,MAAM,OAAO,OAAO,EAAE;OACjC,aAAa,KAAK,eAAe,EAAE;OACpB;OACf,aAAa,KAAK,kBAAkB,KAAK,YAAY;OACrD;OACA;MACE;KACN,oBAAC,OAAD;MAAK,WAAU;gBACb,oBAAC,cAAD;OACE,OAAO,IAAI;OACX,WAAW,MAAM,SAAS,OAAO,EAAE;OACnC,aAAa,KAAK,eAAe,EAAE;OACpB;OACf,aAAa,KAAK,oBAAoB,KAAK,cAAc;OACzD;OACA;MACE;KACN,oBAAC,QAAD;MACE,MAAK;MACL,SAAQ;MACR,MAAK;MACL,cAAW;MACX,eAAe,aAAa,MAAM;gBAElC,oBAAC,GAAD,EAAG,WAAU,aAAc;MACpB;KACL;MA9BI,IAAI,OA8BR,CACN;GACD,KAAK,SAAS,KAAK,oBAAC,aAAD,EAAa,SAAS,CAAC;IAAE,KAAK;IAAK,OAAO;IAAyB,CAAC,EAAI;GAC5F,oBAAC,OAAD,YACE,qBAAC,QAAD;IAAQ,MAAK;IAAS,SAAQ;IAAU,MAAK;IAAK,SAAS;IAAW,UAAU,CAAC;cAAjF;KACE,oBAAC,MAAD,EAAM,WAAU,qBAAsB;;KACjC,KAAK,YAAY;KACf;OACL;GACF"}
1
+ {"version":3,"file":"key-value-list-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/key-value-list-input.tsx"],"sourcesContent":["import { Plus, X } from \"lucide-react\";\nimport { useState } from \"react\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport { FieldLegend } from \"../../internal/field-legend.js\";\nimport { LiquidEditor } from \"../../internal/LiquidEditor/LiquidEditor.js\";\nimport { Button } from \"../../ui/button.js\";\n\n/**\n * Adapter for `pipesKeyValueListInput`. Each row's key and value cells use\n * `LiquidEditor` so they support `/` references (input fields + secrets +\n * constants in one picker). The kvl is used for things like HTTP headers\n * and query params where either column may legitimately reference a field\n * (e.g. a per-record `pageToken`) or a secret (e.g. an Authorization\n * value).\n *\n * The unified legend would otherwise render twice per row (once per\n * cell) — too noisy. The per-cell editors run with `hideLegend`; a\n * single legend renders once below the entire list.\n */\nexport function KeyValueListInputAdapter(field: FieldHandle<\"key_value_list_input\">) {\n const { rows, addRow, removeRow, setKey, setValue, searchSecrets, searchConstants, meta } = field;\n const maxItems = meta.maxItems ?? 50;\n const canAdd = rows.length < maxItems;\n\n // Stable per-row identifiers, kept in lockstep with `rows`. The form value\n // is positional — `[{key, value}, ...]` — so reordering or removing the\n // row at index N would alias N's editor onto another row's content if we\n // keyed by index. We mirror mutations through `handleAdd`/`handleRemove`,\n // and lazily pad if `rows` was extended externally (e.g. form reset).\n const [ids, setIds] = useState<string[]>(() => rows.map(() => crypto.randomUUID()));\n if (ids.length < rows.length) {\n setIds((prev) => [\n ...prev,\n ...Array.from({ length: rows.length - prev.length }, () => crypto.randomUUID()),\n ]);\n }\n\n const handleAdd = () => {\n setIds((prev) => [...prev, crypto.randomUUID()]);\n addRow();\n };\n const handleRemove = (index: number) => {\n setIds((prev) => prev.filter((_, i) => i !== index));\n removeRow(index);\n };\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-2\">\n {rows.length === 0 && (\n <p className=\"pz:text-xs pz:text-muted-foreground\">\n {meta.keyLabel ? `No ${meta.keyLabel.toLowerCase()}s yet.` : \"No entries yet.\"}\n </p>\n )}\n {rows.map((row, index) => (\n <div key={ids[index]} className=\"pz:flex pz:items-start pz:gap-2\">\n <div className=\"pz:basis-1/3 pz:rounded-md pz:border pz:border-input pz:bg-transparent\">\n <LiquidEditor\n value={row.key}\n onChange={(v) => setKey(index, v)}\n inputFields={meta.inputFields ?? []}\n searchSecrets={searchSecrets}\n searchConstants={searchConstants}\n placeholder={meta.keyPlaceholder ?? meta.keyLabel ?? \"Key\"}\n hideLegend\n />\n </div>\n <div className=\"pz:flex-1 pz:rounded-md pz:border pz:border-input pz:bg-transparent\">\n <LiquidEditor\n value={row.value}\n onChange={(v) => setValue(index, v)}\n inputFields={meta.inputFields ?? []}\n searchSecrets={searchSecrets}\n searchConstants={searchConstants}\n placeholder={meta.valuePlaceholder ?? meta.valueLabel ?? \"Value\"}\n hideLegend\n />\n </div>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n aria-label=\"Remove row\"\n onClick={() => handleRemove(index)}\n >\n <X className=\"pz:size-4\" />\n </Button>\n </div>\n ))}\n {rows.length > 0 && <FieldLegend entries={[{ key: \"/\", label: \"to insert a reference\" }]} />}\n <div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={handleAdd} disabled={!canAdd}>\n <Plus className=\"pz:size-4 pz:mr-1\" />\n Add {meta.keyLabel ?? \"row\"}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,yBAAyB,OAA4C;CACnF,MAAM,EAAE,MAAM,QAAQ,WAAW,QAAQ,UAAU,eAAe,iBAAiB,SAAS;CAC5F,MAAM,WAAW,KAAK,YAAY;CAClC,MAAM,SAAS,KAAK,SAAS;CAO7B,MAAM,CAAC,KAAK,UAAU,eAAyB,KAAK,UAAU,OAAO,YAAY,CAAC,CAAC;AACnF,KAAI,IAAI,SAAS,KAAK,OACpB,SAAQ,SAAS,CACf,GAAG,MACH,GAAG,MAAM,KAAK,EAAE,QAAQ,KAAK,SAAS,KAAK,QAAQ,QAAQ,OAAO,YAAY,CAAC,CAChF,CAAC;CAGJ,MAAM,kBAAkB;AACtB,UAAQ,SAAS,CAAC,GAAG,MAAM,OAAO,YAAY,CAAC,CAAC;AAChD,UAAQ;;CAEV,MAAM,gBAAgB,UAAkB;AACtC,UAAQ,SAAS,KAAK,QAAQ,GAAG,MAAM,MAAM,MAAM,CAAC;AACpD,YAAU,MAAM;;AAGlB,QACE,qBAAC,OAAD;EAAK,WAAQ;EAAQ,WAAU;YAA/B;GACG,KAAK,WAAW,KACf,oBAAC,KAAD;IAAG,WAAU;cACV,KAAK,WAAW,MAAM,KAAK,SAAS,aAAa,CAAC,UAAU;IAC3D;GAEL,KAAK,KAAK,KAAK,UACd,qBAAC,OAAD;IAAsB,WAAU;cAAhC;KACE,oBAAC,OAAD;MAAK,WAAU;gBACb,oBAAC,cAAD;OACE,OAAO,IAAI;OACX,WAAW,MAAM,OAAO,OAAO,EAAE;OACjC,aAAa,KAAK,eAAe,EAAE;OACpB;OACE;OACjB,aAAa,KAAK,kBAAkB,KAAK,YAAY;OACrD;OACA;MACE;KACN,oBAAC,OAAD;MAAK,WAAU;gBACb,oBAAC,cAAD;OACE,OAAO,IAAI;OACX,WAAW,MAAM,SAAS,OAAO,EAAE;OACnC,aAAa,KAAK,eAAe,EAAE;OACpB;OACE;OACjB,aAAa,KAAK,oBAAoB,KAAK,cAAc;OACzD;OACA;MACE;KACN,oBAAC,QAAD;MACE,MAAK;MACL,SAAQ;MACR,MAAK;MACL,cAAW;MACX,eAAe,aAAa,MAAM;gBAElC,oBAAC,GAAD,EAAG,WAAU,aAAc;MACpB;KACL;MAhCI,IAAI,OAgCR,CACN;GACD,KAAK,SAAS,KAAK,oBAAC,aAAD,EAAa,SAAS,CAAC;IAAE,KAAK;IAAK,OAAO;IAAyB,CAAC,EAAI;GAC5F,oBAAC,OAAD,YACE,qBAAC,QAAD;IAAQ,MAAK;IAAS,SAAQ;IAAU,MAAK;IAAK,SAAS;IAAW,UAAU,CAAC;cAAjF;KACE,oBAAC,MAAD,EAAM,WAAU,qBAAsB;;KACjC,KAAK,YAAY;KACf;OACL;GACF"}
@@ -124,6 +124,7 @@ function PromptInputAdapter(field) {
124
124
  }),
125
125
  inputFields: meta.inputFields ?? [],
126
126
  searchSecrets: field.searchSecrets,
127
+ searchConstants: field.searchConstants,
127
128
  multiline: true,
128
129
  autoGrow: true,
129
130
  directives: (meta.supportedTags ?? ["input", "output"]).includes("output") ? ["output"] : []
@@ -1 +1 @@
1
- {"version":3,"file":"prompt-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/prompt-input.tsx"],"sourcesContent":["import { useCallback, useMemo, useState } from \"react\";\nimport { useFieldError } from \"../../../hooks/use-field-error.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport type { BaseFieldProps } from \"../../../types/field-props.js\";\nimport { generateRandomString } from \"../../../utils/generate-random-string.js\";\nimport { IconCheck, IconPencil, IconPlus, IconTrash } from \"../../internal/icons.js\";\nimport { LiquidEditor } from \"../../internal/LiquidEditor/LiquidEditor.js\";\nimport { SchemaEditor } from \"../../internal/schema-editor/SchemaEditor.js\";\nimport { Button } from \"../../ui/button.js\";\nimport { Input } from \"../../ui/input.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../../ui/popover.js\";\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"../../ui/table.js\";\n\nfunction ChangeSchemaNameForm({\n defaultName,\n existingNames,\n onSubmit,\n onClose,\n}: {\n defaultName: string;\n existingNames: string[];\n onSubmit: (newName: string) => void;\n onClose: () => void;\n}) {\n const [inputValue, setInputValue] = useState(defaultName);\n const trimmed = inputValue.trim();\n const isDuplicate = trimmed !== defaultName && existingNames.includes(trimmed);\n const isInvalid = !trimmed || isDuplicate;\n\n const handleSubmit = () => {\n if (isInvalid) return;\n if (defaultName !== trimmed) onSubmit(trimmed);\n onClose();\n };\n\n return (\n <div className=\"pz:flex pz:flex-col pz:gap-1\">\n <div className=\"pz:flex pz:items-center pz:gap-2\">\n <Input\n placeholder=\"Change name…\"\n value={inputValue}\n onChange={(e) => setInputValue(e.target.value)}\n minLength={1}\n aria-invalid={isDuplicate}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n handleSubmit();\n }\n }}\n />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={handleSubmit}\n disabled={isInvalid}\n >\n <IconCheck width={14} height={14} />\n </Button>\n </div>\n {isDuplicate && (\n <span className=\"pz:text-destructive pz:text-xs pz:font-medium\" role=\"alert\">\n A schema with that name already exists.\n </span>\n )}\n </div>\n );\n}\n\ntype PromptValue = BaseFieldProps<\"prompt_input\">[\"value\"];\n\n/**\n * Prompt editor with Tiptap rich text, @/​ autocomplete, and JSON schema management.\n * For a more customized editor, provide a custom adapter via FormProvider.\n */\nconst EMPTY_PROMPT: PromptValue = { template: \"\", json_schemas: {} };\n\nexport function PromptInputAdapter(field: FieldHandle<\"prompt_input\">) {\n const meta = field.meta;\n const value = field.value ?? EMPTY_PROMPT;\n\n const templateError = useFieldError(field.form, `${field.path}.template`);\n\n const tableData = useMemo(() => Object.entries(value.json_schemas || {}), [value.json_schemas]);\n\n // Mutation callbacks read the freshest value from RHF at call time instead\n // of closing over `value` — so they depend only on stable identifiers\n // (`field.form` and `field.path`) and don't re-create on every keystroke\n // into the prompt template.\n const formRef = field.form;\n const path = field.path;\n\n const readCurrent = useCallback(\n (): PromptValue => (formRef.getValues(path as any) as PromptValue | undefined) ?? EMPTY_PROMPT,\n [formRef, path],\n );\n\n const handleChange = useCallback((v: PromptValue) => field.setValue(v), [field]);\n\n const handleAddSchema = useCallback(\n (schemaName: string) => {\n const current = readCurrent();\n if (current.json_schemas?.[schemaName]) return;\n handleChange({\n ...current,\n json_schemas: {\n ...current.json_schemas,\n [schemaName]: { type: \"object\", properties: {} },\n },\n });\n },\n [handleChange, readCurrent],\n );\n\n const handleChangeSchemaName = useCallback(\n (oldName: string, newName: string) => {\n const current = readCurrent();\n const schemas = current.json_schemas ?? {};\n if (!schemas[oldName] || oldName === newName || schemas[newName]) return;\n const { [oldName]: oldSchema, ...rest } = schemas;\n handleChange({ template: current.template, json_schemas: { ...rest, [newName]: oldSchema } });\n },\n [handleChange, readCurrent],\n );\n\n const handleDeleteSchema = useCallback(\n (schemaName: string) => {\n const current = readCurrent();\n if (!current.json_schemas?.[schemaName]) return;\n const { [schemaName]: _removed, ...rest } = current.json_schemas;\n handleChange({ template: current.template, json_schemas: rest });\n },\n [handleChange, readCurrent],\n );\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-3\">\n <div\n aria-invalid={!!templateError}\n className={cn(\n \"pz:rounded-md pz:border pz:border-input pz:bg-transparent pz:shadow-xs\",\n templateError && \"pz:border-destructive\",\n )}\n >\n <LiquidEditor\n value={value.template}\n onChange={(v) => handleChange({ ...value, template: v })}\n inputFields={meta.inputFields ?? []}\n searchSecrets={field.searchSecrets}\n multiline\n autoGrow\n directives={\n (meta.supportedTags ?? [\"input\", \"output\"]).includes(\"output\") ? [\"output\"] : []\n }\n />\n </div>\n {templateError && (\n <span className=\"pz:text-destructive pz:text-xs pz:font-medium\" role=\"alert\">\n {templateError}\n </span>\n )}\n\n {/* JSON Schemas section */}\n <div className=\"pz:flex pz:flex-col pz:gap-2\">\n <div className=\"pz:flex pz:items-center pz:justify-between\">\n <span className=\"pz:text-sm pz:font-medium\">JSON Schemas</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => handleAddSchema(generateRandomString(5))}\n >\n <IconPlus width={14} height={14} /> Add\n </Button>\n </div>\n {tableData.length > 0 && (\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead>Schema</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {tableData.map(([schemaName, schema]) => (\n <TableRow key={schemaName}>\n <TableCell className=\"pz:flex pz:items-center pz:gap-1\">\n <span>{schemaName}</span>\n <Popover>\n <PopoverTrigger\n render={\n <Button type=\"button\" variant=\"ghost\" size=\"icon\" title=\"Rename\">\n <IconPencil width={12} height={12} />\n </Button>\n }\n />\n <PopoverContent sideOffset={4} className=\"pz:w-auto pz:p-2\">\n <ChangeSchemaNameForm\n defaultName={schemaName}\n existingNames={Object.keys(value.json_schemas || {})}\n onSubmit={(newName) => handleChangeSchemaName(schemaName, newName)}\n onClose={() => {}}\n />\n </PopoverContent>\n </Popover>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => handleDeleteSchema(schemaName)}\n title=\"Delete schema\"\n >\n <IconTrash width={12} height={12} />\n </Button>\n </TableCell>\n <TableCell>\n <SchemaEditor\n value={schema}\n onChange={(newSchema) => {\n handleChange({\n ...value,\n json_schemas: {\n ...value.json_schemas,\n [schemaName]: newSchema,\n },\n });\n }}\n />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"prompt-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/prompt-input.tsx"],"sourcesContent":["import { useCallback, useMemo, useState } from \"react\";\nimport { useFieldError } from \"../../../hooks/use-field-error.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport type { BaseFieldProps } from \"../../../types/field-props.js\";\nimport { generateRandomString } from \"../../../utils/generate-random-string.js\";\nimport { IconCheck, IconPencil, IconPlus, IconTrash } from \"../../internal/icons.js\";\nimport { LiquidEditor } from \"../../internal/LiquidEditor/LiquidEditor.js\";\nimport { SchemaEditor } from \"../../internal/schema-editor/SchemaEditor.js\";\nimport { Button } from \"../../ui/button.js\";\nimport { Input } from \"../../ui/input.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../../ui/popover.js\";\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"../../ui/table.js\";\n\nfunction ChangeSchemaNameForm({\n defaultName,\n existingNames,\n onSubmit,\n onClose,\n}: {\n defaultName: string;\n existingNames: string[];\n onSubmit: (newName: string) => void;\n onClose: () => void;\n}) {\n const [inputValue, setInputValue] = useState(defaultName);\n const trimmed = inputValue.trim();\n const isDuplicate = trimmed !== defaultName && existingNames.includes(trimmed);\n const isInvalid = !trimmed || isDuplicate;\n\n const handleSubmit = () => {\n if (isInvalid) return;\n if (defaultName !== trimmed) onSubmit(trimmed);\n onClose();\n };\n\n return (\n <div className=\"pz:flex pz:flex-col pz:gap-1\">\n <div className=\"pz:flex pz:items-center pz:gap-2\">\n <Input\n placeholder=\"Change name…\"\n value={inputValue}\n onChange={(e) => setInputValue(e.target.value)}\n minLength={1}\n aria-invalid={isDuplicate}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n handleSubmit();\n }\n }}\n />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={handleSubmit}\n disabled={isInvalid}\n >\n <IconCheck width={14} height={14} />\n </Button>\n </div>\n {isDuplicate && (\n <span className=\"pz:text-destructive pz:text-xs pz:font-medium\" role=\"alert\">\n A schema with that name already exists.\n </span>\n )}\n </div>\n );\n}\n\ntype PromptValue = BaseFieldProps<\"prompt_input\">[\"value\"];\n\n/**\n * Prompt editor with Tiptap rich text, @/​ autocomplete, and JSON schema management.\n * For a more customized editor, provide a custom adapter via FormProvider.\n */\nconst EMPTY_PROMPT: PromptValue = { template: \"\", json_schemas: {} };\n\nexport function PromptInputAdapter(field: FieldHandle<\"prompt_input\">) {\n const meta = field.meta;\n const value = field.value ?? EMPTY_PROMPT;\n\n const templateError = useFieldError(field.form, `${field.path}.template`);\n\n const tableData = useMemo(() => Object.entries(value.json_schemas || {}), [value.json_schemas]);\n\n // Mutation callbacks read the freshest value from RHF at call time instead\n // of closing over `value` — so they depend only on stable identifiers\n // (`field.form` and `field.path`) and don't re-create on every keystroke\n // into the prompt template.\n const formRef = field.form;\n const path = field.path;\n\n const readCurrent = useCallback(\n (): PromptValue => (formRef.getValues(path as any) as PromptValue | undefined) ?? EMPTY_PROMPT,\n [formRef, path],\n );\n\n const handleChange = useCallback((v: PromptValue) => field.setValue(v), [field]);\n\n const handleAddSchema = useCallback(\n (schemaName: string) => {\n const current = readCurrent();\n if (current.json_schemas?.[schemaName]) return;\n handleChange({\n ...current,\n json_schemas: {\n ...current.json_schemas,\n [schemaName]: { type: \"object\", properties: {} },\n },\n });\n },\n [handleChange, readCurrent],\n );\n\n const handleChangeSchemaName = useCallback(\n (oldName: string, newName: string) => {\n const current = readCurrent();\n const schemas = current.json_schemas ?? {};\n if (!schemas[oldName] || oldName === newName || schemas[newName]) return;\n const { [oldName]: oldSchema, ...rest } = schemas;\n handleChange({ template: current.template, json_schemas: { ...rest, [newName]: oldSchema } });\n },\n [handleChange, readCurrent],\n );\n\n const handleDeleteSchema = useCallback(\n (schemaName: string) => {\n const current = readCurrent();\n if (!current.json_schemas?.[schemaName]) return;\n const { [schemaName]: _removed, ...rest } = current.json_schemas;\n handleChange({ template: current.template, json_schemas: rest });\n },\n [handleChange, readCurrent],\n );\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-3\">\n <div\n aria-invalid={!!templateError}\n className={cn(\n \"pz:rounded-md pz:border pz:border-input pz:bg-transparent pz:shadow-xs\",\n templateError && \"pz:border-destructive\",\n )}\n >\n <LiquidEditor\n value={value.template}\n onChange={(v) => handleChange({ ...value, template: v })}\n inputFields={meta.inputFields ?? []}\n searchSecrets={field.searchSecrets}\n searchConstants={field.searchConstants}\n multiline\n autoGrow\n directives={\n (meta.supportedTags ?? [\"input\", \"output\"]).includes(\"output\") ? [\"output\"] : []\n }\n />\n </div>\n {templateError && (\n <span className=\"pz:text-destructive pz:text-xs pz:font-medium\" role=\"alert\">\n {templateError}\n </span>\n )}\n\n {/* JSON Schemas section */}\n <div className=\"pz:flex pz:flex-col pz:gap-2\">\n <div className=\"pz:flex pz:items-center pz:justify-between\">\n <span className=\"pz:text-sm pz:font-medium\">JSON Schemas</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => handleAddSchema(generateRandomString(5))}\n >\n <IconPlus width={14} height={14} /> Add\n </Button>\n </div>\n {tableData.length > 0 && (\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Name</TableHead>\n <TableHead>Schema</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {tableData.map(([schemaName, schema]) => (\n <TableRow key={schemaName}>\n <TableCell className=\"pz:flex pz:items-center pz:gap-1\">\n <span>{schemaName}</span>\n <Popover>\n <PopoverTrigger\n render={\n <Button type=\"button\" variant=\"ghost\" size=\"icon\" title=\"Rename\">\n <IconPencil width={12} height={12} />\n </Button>\n }\n />\n <PopoverContent sideOffset={4} className=\"pz:w-auto pz:p-2\">\n <ChangeSchemaNameForm\n defaultName={schemaName}\n existingNames={Object.keys(value.json_schemas || {})}\n onSubmit={(newName) => handleChangeSchemaName(schemaName, newName)}\n onClose={() => {}}\n />\n </PopoverContent>\n </Popover>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => handleDeleteSchema(schemaName)}\n title=\"Delete schema\"\n >\n <IconTrash width={12} height={12} />\n </Button>\n </TableCell>\n <TableCell>\n <SchemaEditor\n value={schema}\n onChange={(newSchema) => {\n handleChange({\n ...value,\n json_schemas: {\n ...value.json_schemas,\n [schemaName]: newSchema,\n },\n });\n }}\n />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -79,6 +79,7 @@ function TaggedTextInputAdapter(field) {
79
79
  onChange: (v) => field.setValue(v),
80
80
  inputFields: meta.inputFields ?? [],
81
81
  searchSecrets: field.searchSecrets,
82
+ searchConstants: field.searchConstants,
82
83
  placeholder: meta.placeholder,
83
84
  multiline: meta.multiline,
84
85
  expectedFieldType: meta.expectedTagType,
@@ -1 +1 @@
1
- {"version":3,"file":"tagged-text-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/tagged-text-input.tsx"],"sourcesContent":["import {\n autoUpdate,\n flip,\n offset,\n shift,\n size,\n useDismiss,\n useFloating,\n useInteractions,\n useTransitionStyles,\n} from \"@floating-ui/react\";\nimport type { StoreOption, TaggedTextMeta } from \"@pipe0/base\";\nimport { ChevronDown } from \"lucide-react\";\nimport { useMemo, useState } from \"react\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport { WidgetStrip } from \"../../../widgets/widget-strip.js\";\nimport { LiquidEditor } from \"../../internal/LiquidEditor/LiquidEditor.js\";\n\n/**\n * Adapter for `pipesTaggedTextInput`. Renders a tiptap-backed editor that\n * supports `/` to open a unified reference picker (input fields filtered\n * by `expectedTagType`, secrets, constants).\n *\n * When `meta.optionsDef` is present, the input also gains a value-suggestions\n * popover (e.g. listing the user's existing pipe0 sheets). The popover opens\n * on focus, hides automatically when the user is composing a `{{ tag }}`,\n * and is gated by `meta.optionsDef.enabledIf` (sub-feature gate — the input\n * itself stays editable in every state). Picking an option REPLACES the\n * input value with the option's value.\n */\nexport function TaggedTextInputAdapter(field: FieldHandle<\"tagged_text_input\">) {\n const meta = field.meta as TaggedTextMeta;\n const hasError = !!field.error;\n const hasSuggestions = !!meta.optionsDef;\n\n const [open, setOpen] = useState(false);\n\n // Hide the popover whenever the user is composing a `{{ tag }}` — the\n // LiquidEditor SuggestionMenu owns that interaction.\n const valueIsTag = useMemo(() => /\\{\\{[^}]*\\}?\\}?/.test(field.value ?? \"\"), [field.value]);\n\n const hasQuery = (field.value ?? \"\").trim().length > 0;\n\n const popoverShow = hasSuggestions && open && !valueIsTag;\n\n // Floating UI reads positioning straight from the wrapper element via\n // `refs.setReference`; `whileElementsMounted: autoUpdate` subscribes to the\n // ancestor scroll/resize that actually matters (no global window listener,\n // no React state for the rect).\n const { refs, floatingStyles, context } = useFloating({\n open: popoverShow,\n onOpenChange: (next) => {\n if (!next) setOpen(false);\n },\n placement: \"bottom-start\",\n whileElementsMounted: autoUpdate,\n middleware: [\n offset(4),\n flip({ mainAxis: true, crossAxis: false }),\n shift(),\n size({\n apply({ elements, rects }) {\n elements.floating.style.minWidth = `${rects.reference.width}px`;\n elements.floating.style.maxWidth = \"32rem\";\n },\n }),\n ],\n });\n\n const { isMounted, styles: transitionStyles } = useTransitionStyles(context);\n const dismiss = useDismiss(context, {\n outsidePress: true,\n outsidePressEvent: \"mousedown\",\n });\n const { getFloatingProps } = useInteractions([dismiss]);\n\n // Filter options client-side by current input text. Empty input → all options.\n const visibleOptions = useMemo(() => {\n const q = (field.value ?? \"\").trim().toLowerCase();\n if (!q) return field.options;\n return field.options.filter(\n (o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q),\n );\n }, [field.options, field.value]);\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-1\">\n <div\n ref={refs.setReference}\n className={cn(\n \"pz:flex pz:rounded-lg pz:border pz:border-input pz:bg-transparent\",\n meta.multiline ? \"pz:items-start\" : \"pz:items-center\",\n hasError && \"pz:border-destructive/60\",\n )}\n aria-invalid={hasError || undefined}\n onFocusCapture={() => {\n if (hasSuggestions) setOpen(true);\n }}\n onBlurCapture={(e) => {\n // Only close when focus leaves both the wrapper AND the popover.\n const next = e.relatedTarget as Node | null;\n if (next && e.currentTarget.contains(next)) return;\n if (next && document.querySelector(\"[data-p0='tagged-suggestions']\")?.contains(next)) {\n return;\n }\n setOpen(false);\n }}\n >\n <div className=\"pz:flex-1 pz:min-w-0\">\n <LiquidEditor\n value={field.value ?? \"\"}\n onChange={(v) => field.setValue(v)}\n inputFields={meta.inputFields ?? []}\n searchSecrets={field.searchSecrets}\n placeholder={meta.placeholder}\n multiline={meta.multiline}\n expectedFieldType={meta.expectedTagType}\n directives={[]}\n />\n </div>\n {hasSuggestions && (\n <div\n className={cn(\n \"pz:flex pz:items-center pz:gap-1 pz:pr-2 pz:text-muted-foreground\",\n meta.multiline && \"pz:pt-1\",\n )}\n >\n <ChevronDown className=\"pz:size-4 pz:pointer-events-none\" aria-hidden=\"true\" />\n </div>\n )}\n </div>\n\n {hasSuggestions && isMounted && (\n <div\n ref={refs.setFloating}\n data-p0=\"tagged-suggestions\"\n style={{ ...floatingStyles, ...transitionStyles, zIndex: 50 }}\n {...getFloatingProps({\n // Prevent stealing focus from the editor when the user clicks an option.\n onMouseDown: (e) => e.preventDefault(),\n })}\n className=\"pz:relative pz:rounded-lg pz:border pz:border-input pz:bg-popover pz:text-popover-foreground pz:shadow-md pz:overflow-hidden\"\n >\n {field.pending && visibleOptions.length > 0 && (\n // biome-ignore lint/a11y/useAriaPropsSupportedByRole: not relevant\n <span\n aria-label=\"Loading\"\n className=\"pz:absolute pz:right-2 pz:top-2 pz:z-10 pz:inline-block pz:h-3 pz:w-3 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\"\n />\n )}\n <SuggestionsBody\n suggestionsDisabled={field.suggestionsDisabled}\n reason={field.suggestionsDisabledReason}\n pending={field.pending}\n options={visibleOptions}\n hasQuery={hasQuery}\n onPick={(value) => {\n field.setValue(value);\n setOpen(false);\n }}\n />\n </div>\n )}\n </div>\n );\n}\n\nfunction SuggestionsBody({\n suggestionsDisabled,\n reason,\n pending,\n options,\n hasQuery,\n onPick,\n}: {\n suggestionsDisabled: boolean;\n reason?: string;\n pending: boolean;\n options: StoreOption[];\n hasQuery: boolean;\n onPick: (value: string) => void;\n}) {\n if (suggestionsDisabled) {\n return (\n <div className=\"pz:px-3 pz:py-2 pz:text-xs pz:text-muted-foreground\">\n {reason ?? \"Suggestions unavailable.\"}\n </div>\n );\n }\n if (pending && options.length === 0) {\n // Initial load — render a visible spinner so the popover has feedback.\n return (\n <div\n role=\"status\"\n aria-label=\"Loading\"\n className=\"pz:flex pz:items-center pz:justify-center pz:py-6\"\n >\n <span className=\"pz:inline-block pz:h-5 pz:w-5 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\" />\n </div>\n );\n }\n if (options.length === 0) {\n return (\n <div className=\"pz:px-3 pz:py-2 pz:text-xs pz:text-muted-foreground\">\n {hasQuery ? \"No matches\" : \"No options available\"}\n </div>\n );\n }\n return (\n <ul\n className=\"pz:flex pz:flex-col pz:gap-0.5 pz:p-1 pz:transition-opacity\"\n style={pending ? { opacity: 0.5, pointerEvents: \"none\" } : undefined}\n >\n {options.map((opt) => (\n <li key={opt.value}>\n <button\n type=\"button\"\n role=\"option\"\n className=\"pz:flex pz:w-full pz:items-center pz:gap-2 pz:rounded-sm pz:px-2 pz:py-1.5 pz:text-sm pz:text-left pz:cursor-pointer pz:hover:bg-accent pz:hover:text-accent-foreground\"\n onClick={() => onPick(opt.value)}\n >\n <WidgetStrip widgets={opt.widgets} />\n <span>{opt.label}</span>\n </button>\n </li>\n ))}\n </ul>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tagged-text-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/tagged-text-input.tsx"],"sourcesContent":["import {\n autoUpdate,\n flip,\n offset,\n shift,\n size,\n useDismiss,\n useFloating,\n useInteractions,\n useTransitionStyles,\n} from \"@floating-ui/react\";\nimport type { StoreOption, TaggedTextMeta } from \"@pipe0/base\";\nimport { ChevronDown } from \"lucide-react\";\nimport { useMemo, useState } from \"react\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport { WidgetStrip } from \"../../../widgets/widget-strip.js\";\nimport { LiquidEditor } from \"../../internal/LiquidEditor/LiquidEditor.js\";\n\n/**\n * Adapter for `pipesTaggedTextInput`. Renders a tiptap-backed editor that\n * supports `/` to open a unified reference picker (input fields filtered\n * by `expectedTagType`, secrets, constants).\n *\n * When `meta.optionsDef` is present, the input also gains a value-suggestions\n * popover (e.g. listing the user's existing pipe0 sheets). The popover opens\n * on focus, hides automatically when the user is composing a `{{ tag }}`,\n * and is gated by `meta.optionsDef.enabledIf` (sub-feature gate — the input\n * itself stays editable in every state). Picking an option REPLACES the\n * input value with the option's value.\n */\nexport function TaggedTextInputAdapter(field: FieldHandle<\"tagged_text_input\">) {\n const meta = field.meta as TaggedTextMeta;\n const hasError = !!field.error;\n const hasSuggestions = !!meta.optionsDef;\n\n const [open, setOpen] = useState(false);\n\n // Hide the popover whenever the user is composing a `{{ tag }}` — the\n // LiquidEditor SuggestionMenu owns that interaction.\n const valueIsTag = useMemo(() => /\\{\\{[^}]*\\}?\\}?/.test(field.value ?? \"\"), [field.value]);\n\n const hasQuery = (field.value ?? \"\").trim().length > 0;\n\n const popoverShow = hasSuggestions && open && !valueIsTag;\n\n // Floating UI reads positioning straight from the wrapper element via\n // `refs.setReference`; `whileElementsMounted: autoUpdate` subscribes to the\n // ancestor scroll/resize that actually matters (no global window listener,\n // no React state for the rect).\n const { refs, floatingStyles, context } = useFloating({\n open: popoverShow,\n onOpenChange: (next) => {\n if (!next) setOpen(false);\n },\n placement: \"bottom-start\",\n whileElementsMounted: autoUpdate,\n middleware: [\n offset(4),\n flip({ mainAxis: true, crossAxis: false }),\n shift(),\n size({\n apply({ elements, rects }) {\n elements.floating.style.minWidth = `${rects.reference.width}px`;\n elements.floating.style.maxWidth = \"32rem\";\n },\n }),\n ],\n });\n\n const { isMounted, styles: transitionStyles } = useTransitionStyles(context);\n const dismiss = useDismiss(context, {\n outsidePress: true,\n outsidePressEvent: \"mousedown\",\n });\n const { getFloatingProps } = useInteractions([dismiss]);\n\n // Filter options client-side by current input text. Empty input → all options.\n const visibleOptions = useMemo(() => {\n const q = (field.value ?? \"\").trim().toLowerCase();\n if (!q) return field.options;\n return field.options.filter(\n (o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q),\n );\n }, [field.options, field.value]);\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-1\">\n <div\n ref={refs.setReference}\n className={cn(\n \"pz:flex pz:rounded-lg pz:border pz:border-input pz:bg-transparent\",\n meta.multiline ? \"pz:items-start\" : \"pz:items-center\",\n hasError && \"pz:border-destructive/60\",\n )}\n aria-invalid={hasError || undefined}\n onFocusCapture={() => {\n if (hasSuggestions) setOpen(true);\n }}\n onBlurCapture={(e) => {\n // Only close when focus leaves both the wrapper AND the popover.\n const next = e.relatedTarget as Node | null;\n if (next && e.currentTarget.contains(next)) return;\n if (next && document.querySelector(\"[data-p0='tagged-suggestions']\")?.contains(next)) {\n return;\n }\n setOpen(false);\n }}\n >\n <div className=\"pz:flex-1 pz:min-w-0\">\n <LiquidEditor\n value={field.value ?? \"\"}\n onChange={(v) => field.setValue(v)}\n inputFields={meta.inputFields ?? []}\n searchSecrets={field.searchSecrets}\n searchConstants={field.searchConstants}\n placeholder={meta.placeholder}\n multiline={meta.multiline}\n expectedFieldType={meta.expectedTagType}\n directives={[]}\n />\n </div>\n {hasSuggestions && (\n <div\n className={cn(\n \"pz:flex pz:items-center pz:gap-1 pz:pr-2 pz:text-muted-foreground\",\n meta.multiline && \"pz:pt-1\",\n )}\n >\n <ChevronDown className=\"pz:size-4 pz:pointer-events-none\" aria-hidden=\"true\" />\n </div>\n )}\n </div>\n\n {hasSuggestions && isMounted && (\n <div\n ref={refs.setFloating}\n data-p0=\"tagged-suggestions\"\n style={{ ...floatingStyles, ...transitionStyles, zIndex: 50 }}\n {...getFloatingProps({\n // Prevent stealing focus from the editor when the user clicks an option.\n onMouseDown: (e) => e.preventDefault(),\n })}\n className=\"pz:relative pz:rounded-lg pz:border pz:border-input pz:bg-popover pz:text-popover-foreground pz:shadow-md pz:overflow-hidden\"\n >\n {field.pending && visibleOptions.length > 0 && (\n // biome-ignore lint/a11y/useAriaPropsSupportedByRole: not relevant\n <span\n aria-label=\"Loading\"\n className=\"pz:absolute pz:right-2 pz:top-2 pz:z-10 pz:inline-block pz:h-3 pz:w-3 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\"\n />\n )}\n <SuggestionsBody\n suggestionsDisabled={field.suggestionsDisabled}\n reason={field.suggestionsDisabledReason}\n pending={field.pending}\n options={visibleOptions}\n hasQuery={hasQuery}\n onPick={(value) => {\n field.setValue(value);\n setOpen(false);\n }}\n />\n </div>\n )}\n </div>\n );\n}\n\nfunction SuggestionsBody({\n suggestionsDisabled,\n reason,\n pending,\n options,\n hasQuery,\n onPick,\n}: {\n suggestionsDisabled: boolean;\n reason?: string;\n pending: boolean;\n options: StoreOption[];\n hasQuery: boolean;\n onPick: (value: string) => void;\n}) {\n if (suggestionsDisabled) {\n return (\n <div className=\"pz:px-3 pz:py-2 pz:text-xs pz:text-muted-foreground\">\n {reason ?? \"Suggestions unavailable.\"}\n </div>\n );\n }\n if (pending && options.length === 0) {\n // Initial load — render a visible spinner so the popover has feedback.\n return (\n <div\n role=\"status\"\n aria-label=\"Loading\"\n className=\"pz:flex pz:items-center pz:justify-center pz:py-6\"\n >\n <span className=\"pz:inline-block pz:h-5 pz:w-5 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\" />\n </div>\n );\n }\n if (options.length === 0) {\n return (\n <div className=\"pz:px-3 pz:py-2 pz:text-xs pz:text-muted-foreground\">\n {hasQuery ? \"No matches\" : \"No options available\"}\n </div>\n );\n }\n return (\n <ul\n className=\"pz:flex pz:flex-col pz:gap-0.5 pz:p-1 pz:transition-opacity\"\n style={pending ? { opacity: 0.5, pointerEvents: \"none\" } : undefined}\n >\n {options.map((opt) => (\n <li key={opt.value}>\n <button\n type=\"button\"\n role=\"option\"\n className=\"pz:flex pz:w-full pz:items-center pz:gap-2 pz:rounded-sm pz:px-2 pz:py-1.5 pz:text-sm pz:text-left pz:cursor-pointer pz:hover:bg-accent pz:hover:text-accent-foreground\"\n onClick={() => onPick(opt.value)}\n >\n <WidgetStrip widgets={opt.widgets} />\n <span>{opt.label}</span>\n </button>\n </li>\n ))}\n </ul>\n );\n}\n"],"mappings":"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"}
@@ -22,6 +22,7 @@ function TemplateInputAdapter(field) {
22
22
  onChange: (v) => field.setValue(v),
23
23
  inputFields: meta.inputFields ?? [],
24
24
  searchSecrets: field.searchSecrets,
25
+ searchConstants: field.searchConstants,
25
26
  multiline: true,
26
27
  directives: []
27
28
  })
@@ -1 +1 @@
1
- {"version":3,"file":"template-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/template-input.tsx"],"sourcesContent":["import type { TemplateInputMeta } from \"@pipe0/base\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport { LiquidEditor } from \"../../internal/LiquidEditor/LiquidEditor.js\";\n\n/**\n * Template editor with Liquid syntax support — `/` opens a unified\n * reference picker (input fields, secrets, constants). Templates cannot\n * declare outputs, so the directive picker is disabled.\n */\nexport function TemplateInputAdapter(field: FieldHandle<\"template_input\">) {\n const meta = field.meta as TemplateInputMeta;\n const hasError = !!field.error;\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-1\">\n <div\n className={cn(\n \"pz:rounded-md pz:border pz:border-input pz:bg-transparent pz:shadow-xs\",\n hasError && \"pz:border-destructive/60\",\n )}\n aria-invalid={hasError || undefined}\n >\n <LiquidEditor\n value={field.value ?? \"\"}\n onChange={(v) => field.setValue(v)}\n inputFields={meta.inputFields ?? []}\n searchSecrets={field.searchSecrets}\n multiline\n directives={[]}\n />\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAgB,qBAAqB,OAAsC;CACzE,MAAM,OAAO,MAAM;CACnB,MAAM,WAAW,CAAC,CAAC,MAAM;AAEzB,QACE,oBAAC,OAAD;EAAK,WAAQ;EAAQ,WAAU;YAC7B,oBAAC,OAAD;GACE,WAAW,GACT,0EACA,YAAY,2BACb;GACD,gBAAc,YAAY;aAE1B,oBAAC,cAAD;IACE,OAAO,MAAM,SAAS;IACtB,WAAW,MAAM,MAAM,SAAS,EAAE;IAClC,aAAa,KAAK,eAAe,EAAE;IACnC,eAAe,MAAM;IACrB;IACA,YAAY,EAAE;IACd;GACE;EACF"}
1
+ {"version":3,"file":"template-input.mjs","names":[],"sources":["../../../../src/components/defaults/adapters/template-input.tsx"],"sourcesContent":["import type { TemplateInputMeta } from \"@pipe0/base\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { FieldHandle } from \"../../../types/field-handle.js\";\nimport { LiquidEditor } from \"../../internal/LiquidEditor/LiquidEditor.js\";\n\n/**\n * Template editor with Liquid syntax support — `/` opens a unified\n * reference picker (input fields, secrets, constants). Templates cannot\n * declare outputs, so the directive picker is disabled.\n */\nexport function TemplateInputAdapter(field: FieldHandle<\"template_input\">) {\n const meta = field.meta as TemplateInputMeta;\n const hasError = !!field.error;\n\n return (\n <div data-p0=\"input\" className=\"pz:flex pz:flex-col pz:gap-1\">\n <div\n className={cn(\n \"pz:rounded-md pz:border pz:border-input pz:bg-transparent pz:shadow-xs\",\n hasError && \"pz:border-destructive/60\",\n )}\n aria-invalid={hasError || undefined}\n >\n <LiquidEditor\n value={field.value ?? \"\"}\n onChange={(v) => field.setValue(v)}\n inputFields={meta.inputFields ?? []}\n searchSecrets={field.searchSecrets}\n searchConstants={field.searchConstants}\n multiline\n directives={[]}\n />\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAgB,qBAAqB,OAAsC;CACzE,MAAM,OAAO,MAAM;CACnB,MAAM,WAAW,CAAC,CAAC,MAAM;AAEzB,QACE,oBAAC,OAAD;EAAK,WAAQ;EAAQ,WAAU;YAC7B,oBAAC,OAAD;GACE,WAAW,GACT,0EACA,YAAY,2BACb;GACD,gBAAc,YAAY;aAE1B,oBAAC,cAAD;IACE,OAAO,MAAM,SAAS;IACtB,WAAW,MAAM,MAAM,SAAS,EAAE;IAClC,aAAa,KAAK,eAAe,EAAE;IACnC,eAAe,MAAM;IACrB,iBAAiB,MAAM;IACvB;IACA,YAAY,EAAE;IACd;GACE;EACF"}
@@ -1,17 +1,17 @@
1
1
  import { cn } from "../../../lib/utils.mjs";
2
2
  import { FieldLegend } from "../field-legend.mjs";
3
+ import { useAsyncRemoteSource } from "../../../hooks/use-async-remote-source.mjs";
3
4
  import { SuggestionMenu } from "../suggestion-menu/suggestion-menu.mjs";
4
5
  import { TagChipDecoration } from "../tag-chip-decoration.mjs";
5
6
  import { ChipEditPopover } from "./ChipEditPopover.mjs";
6
- import { UnifiedReferencePicker, buildReferenceItems } from "./UnifiedReferencePicker.mjs";
7
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
+ import { UnifiedReferencePicker, buildReferenceItems, parseQuery } from "./UnifiedReferencePicker.mjs";
8
+ import { useCallback, useEffect, useMemo, useState } from "react";
8
9
  import { jsx, jsxs } from "react/jsx-runtime";
9
10
  import { RECORD_FIELD_TYPES } from "@pipe0/base";
10
11
  import { EditorContent, EditorContext, useEditor } from "@tiptap/react";
11
12
  import StarterKit from "@tiptap/starter-kit";
12
13
 
13
14
  //#region src/components/internal/LiquidEditor/LiquidEditor.tsx
14
- const SECRET_SEARCH_DEBOUNCE_MS = 150;
15
15
  function escapeHtml(s) {
16
16
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
17
17
  }
@@ -53,11 +53,19 @@ const OUTPUT_TEMPLATE = (fieldType) => {
53
53
  * byte-identical to what the legacy `TagEditor` / `TextPromptEditor`
54
54
  * produced. No engine, analyzer, or metadata changes.
55
55
  */
56
- function LiquidEditor({ value, onChange, inputFields, searchSecrets, constantSuggestions, multiline = false, autoGrow = true, directives = [], expectedFieldType, placeholder, className, editorProps, hideLegend = false }) {
56
+ function LiquidEditor({ value, onChange, inputFields, searchSecrets, searchConstants, multiline = false, autoGrow = true, directives = [], expectedFieldType, placeholder, className, editorProps, hideLegend = false }) {
57
57
  const blockSeparator = multiline ? "\n" : "";
58
- const visibleConstants = constantSuggestions ?? [];
59
58
  const supportsOutput = directives.includes("output");
60
59
  const hasSecretsResolver = !!searchSecrets;
60
+ const hasConstantsResolver = !!searchConstants;
61
+ const secretSource = useAsyncRemoteSource({ search: searchSecrets });
62
+ const constantSource = useAsyncRemoteSource({ search: searchConstants });
63
+ useEffect(() => {
64
+ secretSource.prefetch();
65
+ }, [secretSource.prefetch]);
66
+ useEffect(() => {
67
+ constantSource.prefetch();
68
+ }, [constantSource.prefetch]);
61
69
  const [chipTarget, setChipTarget] = useState(null);
62
70
  const tagChipExtension = useMemo(() => TagChipDecoration.configure({ onChipClick: (hit, el) => {
63
71
  setChipTarget({
@@ -100,43 +108,55 @@ function LiquidEditor({ value, onChange, inputFields, searchSecrets, constantSug
100
108
  blockSeparator
101
109
  ]);
102
110
  const insertField = useCallback((name) => `{{ ${name} }}`, []);
103
- const lastSecretsRef = useRef([]);
104
- const latestQueryRef = useRef("");
105
- const referenceItems = useCallback(({ query }) => {
106
- latestQueryRef.current = query;
107
- const myQuery = query;
108
- const buildWith = (secrets) => buildReferenceItems(myQuery, {
111
+ const [referenceQuery, setReferenceQuery] = useState("");
112
+ const buildVisibleItems = useCallback((query) => {
113
+ const all = buildReferenceItems(query, {
109
114
  inputFields,
110
- secretSuggestions: secrets,
111
- constantSuggestions: visibleConstants
115
+ secretSuggestions: secretSource.items,
116
+ constantSuggestions: constantSource.items
112
117
  }, expectedFieldType, insertField);
113
- if (!searchSecrets) return Promise.resolve(buildWith([]));
114
- return new Promise((resolve) => {
115
- setTimeout(async () => {
116
- if (latestQueryRef.current !== myQuery) {
117
- resolve(buildWith(lastSecretsRef.current));
118
- return;
119
- }
120
- try {
121
- const secrets = await searchSecrets(myQuery);
122
- if (latestQueryRef.current !== myQuery) {
123
- resolve(buildWith(lastSecretsRef.current));
124
- return;
125
- }
126
- lastSecretsRef.current = secrets;
127
- resolve(buildWith(secrets));
128
- } catch {
129
- resolve(buildWith(lastSecretsRef.current));
130
- }
131
- }, SECRET_SEARCH_DEBOUNCE_MS);
132
- });
118
+ const { prefix, residual } = parseQuery(query);
119
+ if (!!residual.trim() || prefix !== null) return {
120
+ items: all,
121
+ overflow: {
122
+ field: 0,
123
+ secret: 0,
124
+ constant: 0
125
+ }
126
+ };
127
+ const fields = all.filter((i) => i.context?.kind === "field");
128
+ const secrets = all.filter((i) => i.context?.kind === "secret");
129
+ const constants = all.filter((i) => i.context?.kind === "constant");
130
+ return {
131
+ items: [
132
+ ...fields.slice(0, 5),
133
+ ...secrets.slice(0, 5),
134
+ ...constants.slice(0, 5)
135
+ ],
136
+ overflow: {
137
+ field: Math.max(0, fields.length - 5),
138
+ secret: Math.max(0, secrets.length - 5),
139
+ constant: Math.max(0, constants.length - 5)
140
+ }
141
+ };
133
142
  }, [
134
- searchSecrets,
135
143
  inputFields,
136
- visibleConstants,
137
144
  expectedFieldType,
138
- insertField
145
+ insertField,
146
+ secretSource.items,
147
+ constantSource.items
148
+ ]);
149
+ const referenceItems = useCallback(({ query }) => {
150
+ setReferenceQuery(query);
151
+ secretSource.ensure(query);
152
+ constantSource.ensure(query);
153
+ return buildVisibleItems(query).items;
154
+ }, [
155
+ secretSource.ensure,
156
+ constantSource.ensure,
157
+ buildVisibleItems
139
158
  ]);
159
+ const { items: liveReferenceItems, overflow: liveOverflow } = useMemo(() => buildVisibleItems(referenceQuery), [referenceQuery, buildVisibleItems]);
140
160
  const directiveItems = useCallback(() => RECORD_FIELD_TYPES.map((fieldType) => ({
141
161
  title: `output: ${fieldType}`,
142
162
  keywords: [fieldType, "output"],
@@ -163,14 +183,18 @@ function LiquidEditor({ value, onChange, inputFields, searchSecrets, constantSug
163
183
  char: "/",
164
184
  pluginKey: "liquidEditorReferencePicker",
165
185
  items: referenceItems,
186
+ liveItems: liveReferenceItems,
166
187
  children: (props) => /* @__PURE__ */ jsx(UnifiedReferencePicker, {
167
188
  items: props.items,
168
189
  selectedIndex: props.selectedIndex,
169
190
  onSelect: props.onSelect,
170
191
  query: props.query,
171
192
  hasSecretsResolver,
193
+ hasConstantsResolver,
172
194
  inputFieldsCount: inputFields.length,
173
- constantsCount: visibleConstants.length
195
+ secretsPending: secretSource.pending,
196
+ constantsPending: constantSource.pending,
197
+ overflow: liveOverflow
174
198
  })
175
199
  }),
176
200
  supportsOutput && /* @__PURE__ */ jsx(SuggestionMenu, {
@@ -1 +1 @@
1
- {"version":3,"file":"LiquidEditor.mjs","names":[],"sources":["../../../../src/components/internal/LiquidEditor/LiquidEditor.tsx"],"sourcesContent":["import {\n type PipesFieldDefinitionWithName,\n RECORD_FIELD_TYPES,\n type RecordFieldType,\n} from \"@pipe0/base\";\nimport type { EditorProps } from \"@tiptap/pm/view\";\nimport { EditorContent, EditorContext, useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { ConstantSuggestion, SecretSuggestion } from \"../../../types/field-props.js\";\nimport { FieldLegend, type LegendEntry } from \"../field-legend.js\";\nimport { SuggestionMenu } from \"../suggestion-menu/suggestion-menu.js\";\nimport type { SuggestionItem } from \"../suggestion-menu/suggestion-menu-types.js\";\nimport { type ChipHit, TagChipDecoration } from \"../tag-chip-decoration.js\";\nimport { ChipEditPopover, type ChipEditTarget } from \"./ChipEditPopover.js\";\nimport {\n buildReferenceItems,\n type ReferenceContext,\n UnifiedReferencePicker,\n} from \"./UnifiedReferencePicker.js\";\n\nexport type DirectiveKind = \"output\";\n\nconst SECRET_SEARCH_DEBOUNCE_MS = 150;\n\nexport interface LiquidEditorProps {\n value: string;\n onChange: (v: string) => void;\n inputFields: PipesFieldDefinitionWithName[];\n /**\n * Per-keystroke async searcher for secrets. Called from the `/` reference\n * picker as the user types. Calls are debounced and race-guarded — only\n * the response for the latest query is rendered.\n */\n searchSecrets?: (query: string) => Promise<SecretSuggestion[]>;\n constantSuggestions?: ConstantSuggestion[];\n multiline?: boolean;\n /**\n * When true, the editor renders within `min-h`/`max-h` bounds. When\n * false, callers control the height via `className`. Default: true.\n */\n autoGrow?: boolean;\n directives?: DirectiveKind[];\n expectedFieldType?: RecordFieldType;\n placeholder?: string;\n className?: string;\n editorProps?: EditorProps;\n /**\n * Suppress the per-editor legend. Used by `key_value_list_input` which\n * renders a single legend below the entire list rather than per-cell.\n */\n hideLegend?: boolean;\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\n/**\n * Reconciles single- and multi-line text into Tiptap-friendly HTML.\n * - Single-line: collapses whitespace runs containing newlines into a\n * single space and renders as one paragraph.\n * - Multi-line: paragraphs separated by blank lines (`\\n\\n+`); soft\n * breaks within a paragraph render as `<br>`. Leading spaces on each\n * line are preserved with `&nbsp;` so JSON-shaped payloads round-trip\n * through the editor.\n */\nfunction textToTiptapHTML(text: string, multiline: boolean): string {\n if (!multiline) {\n return `<p>${escapeHtml(text.replace(/\\s*\\n+\\s*/g, \" \"))}</p>`;\n }\n return escapeHtml(text)\n .split(/\\n\\n+/)\n .map((block) => {\n const lines = block.split(/\\n/);\n const htmlLines = lines\n .map((line) => line.replace(/^ +/, (spaces) => \"&nbsp;\".repeat(spaces.length)))\n .join(\"<br>\");\n return `<p>${htmlLines}</p>`;\n })\n .join(\"\");\n}\n\nconst OUTPUT_TEMPLATE = (fieldType: RecordFieldType): string => {\n if (fieldType === \"json\") {\n return `{% output FIELD_NAME, type: \"json\", schema: \"SCHEMA_NAME\", description: \"\" %}`;\n }\n return `{% output FIELD_NAME, type: \"${fieldType}\", description: \"\" %}`;\n};\n\n/**\n * Single editor surface for every form field that accepts Liquid tags.\n * Two triggers, two cognitive categories:\n *\n * - `/` opens a unified reference picker (Fields, Secrets, Constants).\n * Prefix syntax `/f/`, `/s/`, `/c/` filters to one source. Picking\n * emits the corresponding `{{ … }}` Liquid Output.\n *\n * - `@` opens a directives picker (only when `directives.length > 0`).\n * Today the only directive is `output`, which inserts a templated\n * `{% output FIELD_NAME, type: \"...\", description: \"\" %}` block.\n *\n * Chips render via the `TagChipDecoration` extension. Click or\n * Enter/Space on a chip mounts the contextual `ChipEditPopover`.\n *\n * Backwards compat: the persisted value is plain Liquid text,\n * byte-identical to what the legacy `TagEditor` / `TextPromptEditor`\n * produced. No engine, analyzer, or metadata changes.\n */\nexport function LiquidEditor({\n value,\n onChange,\n inputFields,\n searchSecrets,\n constantSuggestions,\n multiline = false,\n autoGrow = true,\n directives = [],\n expectedFieldType,\n placeholder,\n className,\n editorProps,\n hideLegend = false,\n}: LiquidEditorProps) {\n const blockSeparator = multiline ? \"\\n\" : \"\";\n const visibleConstants = constantSuggestions ?? [];\n const supportsOutput = directives.includes(\"output\");\n const hasSecretsResolver = !!searchSecrets;\n\n const [chipTarget, setChipTarget] = useState<ChipEditTarget | null>(null);\n\n const tagChipExtension = useMemo(\n () =>\n TagChipDecoration.configure({\n onChipClick: (hit: ChipHit, el: HTMLElement) => {\n setChipTarget({\n kind: hit.kind,\n raw: hit.raw,\n from: hit.from,\n to: hit.to,\n rect: el.getBoundingClientRect(),\n });\n },\n }),\n [],\n );\n\n const editor = useEditor({\n extensions: [\n StarterKit.configure({ hardBreak: !multiline ? false : undefined }),\n tagChipExtension,\n ],\n parseOptions: { preserveWhitespace: \"full\" },\n editorProps: {\n ...editorProps,\n attributes: {\n class: cn(\n \"pz:px-2.5 pz:py-1 pz:text-sm pz:outline-none pz:break-words\",\n multiline\n ? autoGrow\n ? \"pz:min-h-24 pz:max-h-96 pz:overflow-auto pz:whitespace-pre-wrap\"\n : \"pz:overflow-auto pz:whitespace-pre-wrap\"\n : \"pz:min-h-8\",\n className,\n ),\n ...(placeholder ? { \"data-placeholder\": placeholder } : {}),\n ...editorProps?.attributes,\n },\n handleKeyDown(view, event) {\n if (editorProps?.handleKeyDown?.(view, event)) return true;\n // Single-line: Enter must not split the document. Multi-line: let\n // StarterKit handle Enter so paragraphs and blocks behave normally.\n if (!multiline && event.key === \"Enter\") return true;\n return false;\n },\n },\n content: textToTiptapHTML(value || \"\", multiline),\n onUpdate({ editor }) {\n onChange(editor.getText({ blockSeparator }));\n },\n });\n\n // Sync the editor's document when `value` changes from outside (e.g.\n // form reset, programmatic setValue). `useEditor` only reads `content`\n // at mount; without this hook the visible UI drifts from form state.\n // The early-return short-circuits the loop that the editor's own\n // `onUpdate` would otherwise create.\n useEffect(() => {\n if (!editor) return;\n const current = editor.getText({ blockSeparator });\n if (current === (value ?? \"\")) return;\n editor.commands.setContent(textToTiptapHTML(value ?? \"\", multiline), false);\n }, [editor, value, multiline, blockSeparator]);\n\n const insertField = useCallback((name: string) => `{{ ${name} }}`, []);\n\n // Live-query state for secrets. The picker calls `referenceItems(query)` on\n // every keystroke; we debounce the underlying fetch and race-guard via\n // `latestQueryRef` so only the response for the most recent query lands.\n // `lastSecretsRef` carries the previous fetch's results forward so stale\n // calls render something sensible instead of an empty list.\n const lastSecretsRef = useRef<SecretSuggestion[]>([]);\n const latestQueryRef = useRef(\"\");\n\n const referenceItems = useCallback(\n ({ query }: { query: string }): Promise<SuggestionItem<ReferenceContext>[]> => {\n latestQueryRef.current = query;\n const myQuery = query;\n\n const buildWith = (secrets: SecretSuggestion[]) =>\n buildReferenceItems(\n myQuery,\n {\n inputFields,\n secretSuggestions: secrets,\n constantSuggestions: visibleConstants,\n },\n expectedFieldType,\n insertField,\n );\n\n if (!searchSecrets) {\n return Promise.resolve(buildWith([]));\n }\n\n return new Promise<SuggestionItem<ReferenceContext>[]>((resolve) => {\n setTimeout(async () => {\n if (latestQueryRef.current !== myQuery) {\n resolve(buildWith(lastSecretsRef.current));\n return;\n }\n try {\n const secrets = await searchSecrets(myQuery);\n if (latestQueryRef.current !== myQuery) {\n resolve(buildWith(lastSecretsRef.current));\n return;\n }\n lastSecretsRef.current = secrets;\n resolve(buildWith(secrets));\n } catch {\n resolve(buildWith(lastSecretsRef.current));\n }\n }, SECRET_SEARCH_DEBOUNCE_MS);\n });\n },\n [searchSecrets, inputFields, visibleConstants, expectedFieldType, insertField],\n );\n\n const directiveItems = useCallback(\n () =>\n RECORD_FIELD_TYPES.map<SuggestionItem>((fieldType) => ({\n title: `output: ${fieldType}`,\n keywords: [fieldType, \"output\"],\n onSelect: ({ editor: ed, range }) => {\n ed.chain().focus().insertContentAt(range, OUTPUT_TEMPLATE(fieldType)).run();\n },\n })),\n [],\n );\n\n const legendEntries: LegendEntry[] = [];\n legendEntries.push({ key: \"/\", label: \"to insert a reference\" });\n if (supportsOutput) legendEntries.push({ key: \"@\", label: \"to insert a directive\" });\n\n return (\n <EditorContext.Provider value={{ editor }}>\n <EditorContent editor={editor} />\n\n {!hideLegend && <FieldLegend entries={legendEntries} />}\n\n <SuggestionMenu\n editor={editor}\n char=\"/\"\n pluginKey=\"liquidEditorReferencePicker\"\n items={referenceItems}\n >\n {(props) => (\n <UnifiedReferencePicker\n items={props.items as SuggestionItem<ReferenceContext>[]}\n selectedIndex={props.selectedIndex}\n onSelect={props.onSelect as (item: SuggestionItem<ReferenceContext>) => void}\n query={props.query}\n hasSecretsResolver={hasSecretsResolver}\n inputFieldsCount={inputFields.length}\n constantsCount={visibleConstants.length}\n />\n )}\n </SuggestionMenu>\n\n {supportsOutput && (\n <SuggestionMenu\n editor={editor}\n char=\"@\"\n pluginKey=\"liquidEditorDirectivePicker\"\n items={directiveItems}\n >\n {({ items, selectedIndex, onSelect }) => (\n <div className=\"pz:flex pz:flex-col pz:gap-0.5 pz:p-1 pz:min-w-56\" role=\"listbox\">\n <div className=\"pz:px-2 pz:pt-1 pz:pb-0.5 pz:text-[10px] pz:font-medium pz:uppercase pz:tracking-wide pz:text-muted-foreground\">\n Add output declaration\n </div>\n {items.map((item, index) => (\n <button\n key={item.title}\n type=\"button\"\n role=\"option\"\n aria-selected={selectedIndex === index}\n onClick={() => onSelect(item)}\n className={cn(\n \"pz:flex pz:w-full pz:items-center pz:rounded-sm pz:px-2 pz:py-1.5 pz:text-sm pz:text-left pz:cursor-pointer\",\n selectedIndex === index\n ? \"pz:bg-accent pz:text-accent-foreground\"\n : \"pz:hover:bg-accent pz:hover:text-accent-foreground\",\n )}\n >\n {item.title}\n </button>\n ))}\n </div>\n )}\n </SuggestionMenu>\n )}\n\n {chipTarget && editor && (\n <ChipEditPopover\n editor={editor}\n target={chipTarget}\n onClose={() => setChipTarget(null)}\n />\n )}\n </EditorContext.Provider>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"LiquidEditor.mjs","names":[],"sources":["../../../../src/components/internal/LiquidEditor/LiquidEditor.tsx"],"sourcesContent":["import {\n type PipesFieldDefinitionWithName,\n RECORD_FIELD_TYPES,\n type RecordFieldType,\n} from \"@pipe0/base\";\nimport type { EditorProps } from \"@tiptap/pm/view\";\nimport { EditorContent, EditorContext, useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useAsyncRemoteSource } from \"../../../hooks/use-async-remote-source.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { ConstantSuggestion, SecretSuggestion } from \"../../../types/field-props.js\";\nimport { FieldLegend, type LegendEntry } from \"../field-legend.js\";\nimport { SuggestionMenu } from \"../suggestion-menu/suggestion-menu.js\";\nimport type { SuggestionItem } from \"../suggestion-menu/suggestion-menu-types.js\";\nimport { type ChipHit, TagChipDecoration } from \"../tag-chip-decoration.js\";\nimport { ChipEditPopover, type ChipEditTarget } from \"./ChipEditPopover.js\";\nimport {\n buildReferenceItems,\n parseQuery,\n type ReferenceContext,\n SECTION_CAP,\n UnifiedReferencePicker,\n} from \"./UnifiedReferencePicker.js\";\n\nexport type DirectiveKind = \"output\";\n\nexport interface LiquidEditorProps {\n value: string;\n onChange: (v: string) => void;\n inputFields: PipesFieldDefinitionWithName[];\n /**\n * Per-keystroke async searcher for secrets. Called from the `/` reference\n * picker as the user types. Calls are debounced and race-guarded — only\n * the response for the latest query is rendered.\n */\n searchSecrets?: (query: string) => Promise<SecretSuggestion[]>;\n /**\n * Per-keystroke async searcher for constants. Mirrors `searchSecrets`.\n */\n searchConstants?: (query: string) => Promise<ConstantSuggestion[]>;\n multiline?: boolean;\n /**\n * When true, the editor renders within `min-h`/`max-h` bounds. When\n * false, callers control the height via `className`. Default: true.\n */\n autoGrow?: boolean;\n directives?: DirectiveKind[];\n expectedFieldType?: RecordFieldType;\n placeholder?: string;\n className?: string;\n editorProps?: EditorProps;\n /**\n * Suppress the per-editor legend. Used by `key_value_list_input` which\n * renders a single legend below the entire list rather than per-cell.\n */\n hideLegend?: boolean;\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\n/**\n * Reconciles single- and multi-line text into Tiptap-friendly HTML.\n * - Single-line: collapses whitespace runs containing newlines into a\n * single space and renders as one paragraph.\n * - Multi-line: paragraphs separated by blank lines (`\\n\\n+`); soft\n * breaks within a paragraph render as `<br>`. Leading spaces on each\n * line are preserved with `&nbsp;` so JSON-shaped payloads round-trip\n * through the editor.\n */\nfunction textToTiptapHTML(text: string, multiline: boolean): string {\n if (!multiline) {\n return `<p>${escapeHtml(text.replace(/\\s*\\n+\\s*/g, \" \"))}</p>`;\n }\n return escapeHtml(text)\n .split(/\\n\\n+/)\n .map((block) => {\n const lines = block.split(/\\n/);\n const htmlLines = lines\n .map((line) => line.replace(/^ +/, (spaces) => \"&nbsp;\".repeat(spaces.length)))\n .join(\"<br>\");\n return `<p>${htmlLines}</p>`;\n })\n .join(\"\");\n}\n\nconst OUTPUT_TEMPLATE = (fieldType: RecordFieldType): string => {\n if (fieldType === \"json\") {\n return `{% output FIELD_NAME, type: \"json\", schema: \"SCHEMA_NAME\", description: \"\" %}`;\n }\n return `{% output FIELD_NAME, type: \"${fieldType}\", description: \"\" %}`;\n};\n\n/**\n * Single editor surface for every form field that accepts Liquid tags.\n * Two triggers, two cognitive categories:\n *\n * - `/` opens a unified reference picker (Fields, Secrets, Constants).\n * Prefix syntax `/f/`, `/s/`, `/c/` filters to one source. Picking\n * emits the corresponding `{{ … }}` Liquid Output.\n *\n * - `@` opens a directives picker (only when `directives.length > 0`).\n * Today the only directive is `output`, which inserts a templated\n * `{% output FIELD_NAME, type: \"...\", description: \"\" %}` block.\n *\n * Chips render via the `TagChipDecoration` extension. Click or\n * Enter/Space on a chip mounts the contextual `ChipEditPopover`.\n *\n * Backwards compat: the persisted value is plain Liquid text,\n * byte-identical to what the legacy `TagEditor` / `TextPromptEditor`\n * produced. No engine, analyzer, or metadata changes.\n */\nexport function LiquidEditor({\n value,\n onChange,\n inputFields,\n searchSecrets,\n searchConstants,\n multiline = false,\n autoGrow = true,\n directives = [],\n expectedFieldType,\n placeholder,\n className,\n editorProps,\n hideLegend = false,\n}: LiquidEditorProps) {\n const blockSeparator = multiline ? \"\\n\" : \"\";\n const supportsOutput = directives.includes(\"output\");\n const hasSecretsResolver = !!searchSecrets;\n const hasConstantsResolver = !!searchConstants;\n\n const secretSource = useAsyncRemoteSource<SecretSuggestion>({ search: searchSecrets });\n const constantSource = useAsyncRemoteSource<ConstantSuggestion>({ search: searchConstants });\n\n // Warm caches once per resolver identity so the empty-query open is hot.\n useEffect(() => {\n secretSource.prefetch();\n }, [secretSource.prefetch]);\n\n useEffect(() => {\n constantSource.prefetch();\n }, [constantSource.prefetch]);\n\n const [chipTarget, setChipTarget] = useState<ChipEditTarget | null>(null);\n\n const tagChipExtension = useMemo(\n () =>\n TagChipDecoration.configure({\n onChipClick: (hit: ChipHit, el: HTMLElement) => {\n setChipTarget({\n kind: hit.kind,\n raw: hit.raw,\n from: hit.from,\n to: hit.to,\n rect: el.getBoundingClientRect(),\n });\n },\n }),\n [],\n );\n\n const editor = useEditor({\n extensions: [\n StarterKit.configure({ hardBreak: !multiline ? false : undefined }),\n tagChipExtension,\n ],\n parseOptions: { preserveWhitespace: \"full\" },\n editorProps: {\n ...editorProps,\n attributes: {\n class: cn(\n \"pz:px-2.5 pz:py-1 pz:text-sm pz:outline-none pz:break-words\",\n multiline\n ? autoGrow\n ? \"pz:min-h-24 pz:max-h-96 pz:overflow-auto pz:whitespace-pre-wrap\"\n : \"pz:overflow-auto pz:whitespace-pre-wrap\"\n : \"pz:min-h-8\",\n className,\n ),\n ...(placeholder ? { \"data-placeholder\": placeholder } : {}),\n ...editorProps?.attributes,\n },\n handleKeyDown(view, event) {\n if (editorProps?.handleKeyDown?.(view, event)) return true;\n // Single-line: Enter must not split the document. Multi-line: let\n // StarterKit handle Enter so paragraphs and blocks behave normally.\n if (!multiline && event.key === \"Enter\") return true;\n return false;\n },\n },\n content: textToTiptapHTML(value || \"\", multiline),\n onUpdate({ editor }) {\n onChange(editor.getText({ blockSeparator }));\n },\n });\n\n // Sync the editor's document when `value` changes from outside (e.g.\n // form reset, programmatic setValue). `useEditor` only reads `content`\n // at mount; without this hook the visible UI drifts from form state.\n // The early-return short-circuits the loop that the editor's own\n // `onUpdate` would otherwise create.\n useEffect(() => {\n if (!editor) return;\n const current = editor.getText({ blockSeparator });\n if (current === (value ?? \"\")) return;\n editor.commands.setContent(textToTiptapHTML(value ?? \"\", multiline), false);\n }, [editor, value, multiline, blockSeparator]);\n\n const insertField = useCallback((name: string) => `{{ ${name} }}`, []);\n\n // Tracks the current query in the open reference picker. Drives the\n // `liveItems` recomputation below — when the async sources resolve, we\n // re-render the picker against this query without waiting for Tiptap to\n // re-call items().\n const [referenceQuery, setReferenceQuery] = useState(\"\");\n\n // In sectioned (empty-query) view, cap each kind at SECTION_CAP visible\n // rows. Users see a compact summary and type to filter; the act of\n // filtering switches to flat mode where all matches are shown.\n // Caps are applied at this layer (not inside the picker) so the array\n // passed to SuggestionMenu matches what's rendered — keyboard nav\n // operates on the visible set and never lands on a hidden row.\n const buildVisibleItems = useCallback(\n (\n query: string,\n ): {\n items: SuggestionItem<ReferenceContext>[];\n overflow: Record<ReferenceContext[\"kind\"], number>;\n } => {\n const all = buildReferenceItems(\n query,\n {\n inputFields,\n secretSuggestions: secretSource.items,\n constantSuggestions: constantSource.items,\n },\n expectedFieldType,\n insertField,\n );\n const { prefix, residual } = parseQuery(query);\n const isFlat = !!residual.trim() || prefix !== null;\n if (isFlat) {\n return { items: all, overflow: { field: 0, secret: 0, constant: 0 } };\n }\n const fields = all.filter((i) => i.context?.kind === \"field\");\n const secrets = all.filter((i) => i.context?.kind === \"secret\");\n const constants = all.filter((i) => i.context?.kind === \"constant\");\n return {\n items: [\n ...fields.slice(0, SECTION_CAP),\n ...secrets.slice(0, SECTION_CAP),\n ...constants.slice(0, SECTION_CAP),\n ],\n overflow: {\n field: Math.max(0, fields.length - SECTION_CAP),\n secret: Math.max(0, secrets.length - SECTION_CAP),\n constant: Math.max(0, constants.length - SECTION_CAP),\n },\n };\n },\n [inputFields, expectedFieldType, insertField, secretSource.items, constantSource.items],\n );\n\n const referenceItems = useCallback(\n ({ query }: { query: string }): SuggestionItem<ReferenceContext>[] => {\n setReferenceQuery(query);\n // Fire-and-forget: results land via React state in `secretSource.items`\n // / `constantSource.items` and trigger a re-render. The popup reads\n // them through `liveItems` (see below).\n secretSource.ensure(query);\n constantSource.ensure(query);\n return buildVisibleItems(query).items;\n },\n [secretSource.ensure, constantSource.ensure, buildVisibleItems],\n );\n\n // Live items derived from the current query + cached source items. This is\n // recomputed whenever an async source resolves (which updates\n // `secretSource.items` / `constantSource.items` via setState). Passed to\n // SuggestionMenu via `liveItems` so the open popup re-renders without\n // needing the user to type — Tiptap's items() callback alone cannot\n // deliver async results that arrive between keystrokes.\n const { items: liveReferenceItems, overflow: liveOverflow } = useMemo(\n () => buildVisibleItems(referenceQuery),\n [referenceQuery, buildVisibleItems],\n );\n\n const directiveItems = useCallback(\n () =>\n RECORD_FIELD_TYPES.map<SuggestionItem>((fieldType) => ({\n title: `output: ${fieldType}`,\n keywords: [fieldType, \"output\"],\n onSelect: ({ editor: ed, range }) => {\n ed.chain().focus().insertContentAt(range, OUTPUT_TEMPLATE(fieldType)).run();\n },\n })),\n [],\n );\n\n const legendEntries: LegendEntry[] = [];\n legendEntries.push({ key: \"/\", label: \"to insert a reference\" });\n if (supportsOutput) legendEntries.push({ key: \"@\", label: \"to insert a directive\" });\n\n return (\n <EditorContext.Provider value={{ editor }}>\n <EditorContent editor={editor} />\n\n {!hideLegend && <FieldLegend entries={legendEntries} />}\n\n <SuggestionMenu\n editor={editor}\n char=\"/\"\n pluginKey=\"liquidEditorReferencePicker\"\n items={referenceItems}\n liveItems={liveReferenceItems}\n >\n {(props) => (\n <UnifiedReferencePicker\n items={props.items as SuggestionItem<ReferenceContext>[]}\n selectedIndex={props.selectedIndex}\n onSelect={props.onSelect as (item: SuggestionItem<ReferenceContext>) => void}\n query={props.query}\n hasSecretsResolver={hasSecretsResolver}\n hasConstantsResolver={hasConstantsResolver}\n inputFieldsCount={inputFields.length}\n secretsPending={secretSource.pending}\n constantsPending={constantSource.pending}\n overflow={liveOverflow}\n />\n )}\n </SuggestionMenu>\n\n {supportsOutput && (\n <SuggestionMenu\n editor={editor}\n char=\"@\"\n pluginKey=\"liquidEditorDirectivePicker\"\n items={directiveItems}\n >\n {({ items, selectedIndex, onSelect }) => (\n <div className=\"pz:flex pz:flex-col pz:gap-0.5 pz:p-1 pz:min-w-56\" role=\"listbox\">\n <div className=\"pz:px-2 pz:pt-1 pz:pb-0.5 pz:text-[10px] pz:font-medium pz:uppercase pz:tracking-wide pz:text-muted-foreground\">\n Add output declaration\n </div>\n {items.map((item, index) => (\n <button\n key={item.title}\n type=\"button\"\n role=\"option\"\n aria-selected={selectedIndex === index}\n onClick={() => onSelect(item)}\n className={cn(\n \"pz:flex pz:w-full pz:items-center pz:rounded-sm pz:px-2 pz:py-1.5 pz:text-sm pz:text-left pz:cursor-pointer\",\n selectedIndex === index\n ? \"pz:bg-accent pz:text-accent-foreground\"\n : \"pz:hover:bg-accent pz:hover:text-accent-foreground\",\n )}\n >\n {item.title}\n </button>\n ))}\n </div>\n )}\n </SuggestionMenu>\n )}\n\n {chipTarget && editor && (\n <ChipEditPopover editor={editor} target={chipTarget} onClose={() => setChipTarget(null)} />\n )}\n </EditorContext.Provider>\n );\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { Hash, Key } from "lucide-react";
3
3
 
4
4
  //#region src/components/internal/LiquidEditor/UnifiedReferencePicker.tsx
@@ -135,18 +135,7 @@ function ItemRow({ item, active, onSelect }) {
135
135
  ]
136
136
  });
137
137
  }
138
- /**
139
- * Picker UI rendered inside the SuggestionMenu's children render-prop.
140
- *
141
- * - Empty query → sectioned view (Fields, Secrets, Constants).
142
- * - Non-empty query → flat ranked list.
143
- *
144
- * The selected-index pointer from `SuggestionMenuRenderProps` always maps
145
- * to the flat `items` order. Sectioned mode uses the same flat order but
146
- * inserts headers between the boundaries, so highlight tracking still
147
- * works without bespoke navigation.
148
- */
149
- function UnifiedReferencePicker({ items, selectedIndex, onSelect, query, hasSecretsResolver, inputFieldsCount, constantsCount }) {
138
+ function UnifiedReferencePicker({ items, selectedIndex, onSelect, query, hasSecretsResolver, hasConstantsResolver, inputFieldsCount, secretsPending, constantsPending, overflow }) {
150
139
  const { prefix, residual } = parseQuery(query);
151
140
  if (!!residual.trim() || prefix !== null) {
152
141
  if (items.length === 0) return /* @__PURE__ */ jsxs("div", {
@@ -154,7 +143,7 @@ function UnifiedReferencePicker({ items, selectedIndex, onSelect, query, hasSecr
154
143
  role: "listbox",
155
144
  children: [prefix && /* @__PURE__ */ jsx(SectionHeader, { children: prefix === "field" ? "Fields" : prefix === "secret" ? "Secrets" : "Constants" }), /* @__PURE__ */ jsx("div", {
156
145
  className: "pz:px-2 pz:py-1.5 pz:text-sm pz:text-muted-foreground",
157
- children: describeEmpty(prefix)
146
+ children: prefix === "secret" && secretsPending || prefix === "constant" && constantsPending || prefix === null && (secretsPending || constantsPending) ? "Searching…" : describeEmpty(prefix)
158
147
  })]
159
148
  });
160
149
  return /* @__PURE__ */ jsxs("div", {
@@ -181,7 +170,9 @@ function UnifiedReferencePicker({ items, selectedIndex, onSelect, query, hasSecr
181
170
  startIndex: 0,
182
171
  selectedIndex,
183
172
  onSelect,
184
- emptyHint: inputFieldsCount === 0 ? "No input fields available." : "No matching input fields."
173
+ emptyHint: inputFieldsCount === 0 ? "No input fields available." : "No matching input fields.",
174
+ overflow: overflow.field,
175
+ overflowHelper: "Type to filter."
185
176
  }),
186
177
  hasSecretsResolver && /* @__PURE__ */ jsx(Section, {
187
178
  label: "Secrets",
@@ -190,42 +181,65 @@ function UnifiedReferencePicker({ items, selectedIndex, onSelect, query, hasSecr
190
181
  startIndex: fieldItems.length,
191
182
  selectedIndex,
192
183
  onSelect,
193
- emptyHint: "No matching secrets.",
194
- helperHint: "Type /s/ to filter to secrets only."
184
+ emptyHint: secretsPending ? "Searching…" : "No matching secrets.",
185
+ helperHint: "Type /s/ to filter to secrets only.",
186
+ pending: secretsPending,
187
+ overflow: overflow.secret,
188
+ overflowHelper: "Type to filter, or /s/ for secrets only."
195
189
  }),
196
- constantsCount > 0 && /* @__PURE__ */ jsx(Section, {
190
+ hasConstantsResolver && /* @__PURE__ */ jsx(Section, {
197
191
  label: "Constants",
198
192
  kind: "constant",
199
193
  items: constantItems,
200
194
  startIndex: fieldItems.length + secretItems.length,
201
195
  selectedIndex,
202
196
  onSelect,
203
- emptyHint: "No matching constants."
197
+ emptyHint: constantsPending ? "Searching…" : "No matching constants.",
198
+ helperHint: "Type /c/ to filter to constants only.",
199
+ pending: constantsPending,
200
+ overflow: overflow.constant,
201
+ overflowHelper: "Type to filter, or /c/ for constants only."
204
202
  })
205
203
  ]
206
204
  });
207
205
  }
208
- function Section({ label, kind, items, startIndex, selectedIndex, onSelect, emptyHint, helperHint }) {
206
+ function Section({ label, kind, items, startIndex, selectedIndex, onSelect, emptyHint, helperHint, pending, overflow = 0, overflowHelper }) {
209
207
  return /* @__PURE__ */ jsxs("div", {
210
208
  className: "pz:flex pz:flex-col",
211
- children: [/* @__PURE__ */ jsxs(SectionHeader, { children: [label, items.length > 0 && /* @__PURE__ */ jsxs("span", {
212
- className: "pz:ml-1 pz:normal-case pz:tracking-normal",
213
- children: [
214
- "(",
215
- items.length,
216
- ")"
217
- ]
218
- })] }), items.length === 0 ? /* @__PURE__ */ jsxs("div", {
209
+ children: [/* @__PURE__ */ jsxs(SectionHeader, { children: [
210
+ label,
211
+ items.length > 0 && /* @__PURE__ */ jsxs("span", {
212
+ className: "pz:ml-1 pz:normal-case pz:tracking-normal",
213
+ children: [
214
+ "(",
215
+ items.length,
216
+ overflow > 0 && ` of ${items.length + overflow}`,
217
+ ")"
218
+ ]
219
+ }),
220
+ pending && /* @__PURE__ */ jsx("span", {
221
+ className: "pz:ml-1 pz:normal-case pz:tracking-normal pz:opacity-70",
222
+ children: "· Searching…"
223
+ })
224
+ ] }), items.length === 0 ? /* @__PURE__ */ jsxs("div", {
219
225
  className: "pz:px-2 pz:py-1 pz:text-xs pz:text-muted-foreground",
220
226
  children: [emptyHint, helperHint && /* @__PURE__ */ jsx("span", {
221
227
  className: "pz:block pz:opacity-70",
222
228
  children: helperHint
223
229
  })]
224
- }) : items.map((item, i) => /* @__PURE__ */ jsx(ItemRow, {
230
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [items.map((item, i) => /* @__PURE__ */ jsx(ItemRow, {
225
231
  item,
226
232
  active: selectedIndex === startIndex + i,
227
233
  onSelect
228
- }, `${kind}-${item.title}`))]
234
+ }, `${kind}-${item.title}`)), overflow > 0 && overflowHelper && /* @__PURE__ */ jsxs("div", {
235
+ className: "pz:px-2 pz:py-0.5 pz:text-[11px] pz:text-muted-foreground pz:opacity-70",
236
+ children: [
237
+ "+",
238
+ overflow,
239
+ " more — ",
240
+ overflowHelper
241
+ ]
242
+ })] })]
229
243
  });
230
244
  }
231
245
  function describeEmpty(prefix) {
@@ -236,5 +250,5 @@ function describeEmpty(prefix) {
236
250
  }
237
251
 
238
252
  //#endregion
239
- export { UnifiedReferencePicker, buildReferenceItems };
253
+ export { UnifiedReferencePicker, buildReferenceItems, parseQuery };
240
254
  //# sourceMappingURL=UnifiedReferencePicker.mjs.map