sanity-plugin-pte-interpolation 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # sanity-plugin-pte-interpolation
2
+
3
+ [![npm version](https://img.shields.io/npm/v/sanity-plugin-pte-interpolation.svg?style=flat-square)](https://www.npmjs.com/package/sanity-plugin-pte-interpolation)
4
+
5
+ Sanity Studio schema helper that adds dynamic variable picker inline blocks to the [Portable Text Editor](https://www.sanity.io/docs/portable-text-editor). Editors can insert named variables (like `{firstName}` or `{email}`) directly into rich text content, which are then resolved to real values at render time.
6
+
7
+ Part of [sanity-pte-interpolation](https://github.com/jordanl17/sanity-pte-interpolation). For rendering the variables in React, see [`pte-interpolation-react`](https://www.npmjs.com/package/pte-interpolation-react).
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install sanity-plugin-pte-interpolation
13
+ ```
14
+
15
+ ### Peer dependencies
16
+
17
+ - `sanity` ^3.0.0 || ^4.0.0 || ^5.0.0
18
+ - `react` ^18.0.0 || ^19.0.0
19
+ - `@sanity/ui` ^2.0.0 || ^3.0.0
20
+ - `@sanity/icons` ^3.0.0
21
+
22
+ ## Usage
23
+
24
+ Call `interpolationVariables()` inside the `of` array of a Portable Text field. It returns a `block` array member with the variable inline object type injected.
25
+
26
+ ```ts
27
+ import {defineType, defineField} from 'sanity'
28
+ import {interpolationVariables} from 'sanity-plugin-pte-interpolation'
29
+
30
+ export default defineType({
31
+ name: 'email',
32
+ title: 'Email',
33
+ type: 'document',
34
+ fields: [
35
+ defineField({
36
+ name: 'body',
37
+ title: 'Body',
38
+ type: 'array',
39
+ of: [
40
+ interpolationVariables([
41
+ {id: 'firstName', name: 'First name', description: 'First name of the recipient'},
42
+ {id: 'lastName', name: 'Last name', description: 'Last name of the recipient'},
43
+ {id: 'email', name: 'Email address', description: 'Email address of the recipient'},
44
+ ]),
45
+ ],
46
+ }),
47
+ ],
48
+ })
49
+ ```
50
+
51
+ Each variable requires an `id` (the lookup key used at render time) and a `name` (displayed in the Studio dropdown). The optional `description` appears as helper text below the picker when a variable is selected.
52
+
53
+ ### With a custom block definition
54
+
55
+ If you already have a customised `block` definition, pass it as the second argument and the variable type will be appended to its existing `of` array:
56
+
57
+ ```ts
58
+ import {defineArrayMember} from 'sanity'
59
+ import {interpolationVariables} from 'sanity-plugin-pte-interpolation'
60
+
61
+ const customBlock = defineArrayMember({
62
+ type: 'block',
63
+ styles: [{title: 'Normal', value: 'normal'}],
64
+ marks: {
65
+ decorators: [{title: 'Bold', value: 'strong'}],
66
+ },
67
+ })
68
+
69
+ interpolationVariables([{id: 'firstName', name: 'First name'}], customBlock)
70
+ ```
71
+
72
+ ## Rendering Variables
73
+
74
+ This package handles the **authoring** side. To resolve variables to actual values in your frontend, use [`pte-interpolation-react`](https://www.npmjs.com/package/pte-interpolation-react):
75
+
76
+ ```tsx
77
+ import {InterpolatedPortableText} from 'pte-interpolation-react'
78
+ ;<InterpolatedPortableText
79
+ value={body}
80
+ interpolationValues={{
81
+ firstName: 'Jo',
82
+ lastName: 'Smith',
83
+ email: 'jo@example.com',
84
+ }}
85
+ />
86
+ ```
87
+
88
+ For framework-agnostic use cases - plain string output, variable key extraction, server-side rendering, or any non-React environment - use [`pte-interpolation-core`](https://www.npmjs.com/package/pte-interpolation-core) directly:
89
+
90
+ ```ts
91
+ import {interpolateToString, extractVariableKeys} from 'pte-interpolation-core'
92
+
93
+ const keys = extractVariableKeys(blocks) // ['firstName', 'email']
94
+ const text = interpolateToString(blocks, {firstName: 'Jo', email: 'jo@example.com'})
95
+ // "Hello, Jo! Your email is jo@example.com."
96
+ ```
97
+
98
+ ## Data Shape
99
+
100
+ Variables are stored as inline objects within Portable Text blocks:
101
+
102
+ ```json
103
+ {
104
+ "_type": "block",
105
+ "children": [
106
+ {"_type": "span", "text": "Hello, "},
107
+ {"_type": "pteInterpolationVariable", "variableKey": "firstName"},
108
+ {"_type": "span", "text": "!"}
109
+ ]
110
+ }
111
+ ```
112
+
113
+ The `variableKey` maps to the `id` you defined in `interpolationVariables()` and the key in the `interpolationValues` record on the rendering side.
114
+
115
+ ## API Reference
116
+
117
+ ### `interpolationVariables(variables, block?)`
118
+
119
+ | Parameter | Type | Description |
120
+ | ----------- | -------------------------------------- | --------------------------------------------- |
121
+ | `variables` | `InterpolationVariable[]` | Array of variable definitions |
122
+ | `block` | `ReturnType<typeof defineArrayMember>` | Optional existing block definition to augment |
123
+
124
+ Returns a `block` array member with the `pteInterpolationVariable` inline object type added.
125
+
126
+ ### `InterpolationVariable`
127
+
128
+ ```ts
129
+ interface InterpolationVariable {
130
+ id: string // Lookup key used at render time
131
+ name: string // Display name shown in Studio
132
+ description?: string // Helper text shown below the picker
133
+ }
134
+ ```
135
+
136
+ ### `VARIABLE_TYPE_PREFIX`
137
+
138
+ The constant `'pteInterpolationVariable'` - the `_type` string used for variable inline blocks in stored Portable Text. Exported for advanced use cases.
139
+
140
+ ## License
141
+
142
+ MIT
package/dist/index.cjs CHANGED
@@ -1,8 +1,87 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: !0 });
3
- var sanity = require("sanity");
4
- const pteInterpolation = sanity.definePlugin((_config) => ({
5
- name: "sanity-plugin-pte-interpolation"
6
- }));
7
- exports.pteInterpolation = pteInterpolation;
3
+ var icons = require("@sanity/icons"), sanity = require("sanity"), jsxRuntime = require("react/jsx-runtime"), ui = require("@sanity/ui"), react = require("react");
4
+ function VariableKeyField(props) {
5
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: props.children });
6
+ }
7
+ function createVariableKeyInput(variables) {
8
+ const options = variables.map((variable) => ({
9
+ value: variable.id,
10
+ name: variable.name,
11
+ description: variable.description
12
+ }));
13
+ return function(props) {
14
+ const autocompleteId = react.useId(), selectedVariable = variables.find((variable) => variable.id === props.value), handleChange = react.useCallback(
15
+ (selectedValue) => {
16
+ props.onChange(selectedValue ? sanity.set(selectedValue) : sanity.unset());
17
+ },
18
+ [props]
19
+ ), filterOption = react.useCallback((query, option) => option.name.toLowerCase().includes(query.toLowerCase()), []), renderOption = react.useCallback((option) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Card, { as: "button", padding: 3, children: [
20
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "medium", children: option.name }),
21
+ option.description && /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { marginTop: 2, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, muted: !0, children: option.description }) })
22
+ ] }), []), renderValue = react.useCallback((_value, option) => option ? option.name : options.find((candidate) => candidate.value === _value)?.name ?? _value, []);
23
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
24
+ /* @__PURE__ */ jsxRuntime.jsx(
25
+ ui.Autocomplete,
26
+ {
27
+ id: autocompleteId,
28
+ options,
29
+ value: typeof props.value == "string" ? props.value : void 0,
30
+ onChange: handleChange,
31
+ filterOption,
32
+ renderOption,
33
+ renderValue,
34
+ openButton: !0,
35
+ icon: icons.SearchIcon,
36
+ placeholder: "Search variables...",
37
+ fontSize: 1,
38
+ padding: 3
39
+ }
40
+ ),
41
+ selectedVariable?.description && /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { marginTop: 2, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: selectedVariable.description }) })
42
+ ] });
43
+ };
44
+ }
45
+ function createVariableInlineBlock(variables) {
46
+ return function(props) {
47
+ const variableKey = props.value?.variableKey, variable = variables.find((candidate) => candidate.id === variableKey);
48
+ return props.renderDefault({
49
+ ...props,
50
+ renderPreview: () => /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 2, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, weight: "medium", children: variable?.name ?? variableKey ?? "Select variable" }) })
51
+ });
52
+ };
53
+ }
54
+ const VARIABLE_TYPE_PREFIX = "pteInterpolationVariable";
55
+ function interpolationVariables(variables, block) {
56
+ const variableType = sanity.defineArrayMember({
57
+ type: "object",
58
+ name: VARIABLE_TYPE_PREFIX,
59
+ title: "Variable",
60
+ icon: icons.TagIcon,
61
+ options: {
62
+ modal: { width: 0 }
63
+ },
64
+ fields: [
65
+ sanity.defineField({
66
+ name: "variableKey",
67
+ title: "Variable",
68
+ type: "string",
69
+ validation: (rule) => rule.required(),
70
+ components: {
71
+ field: VariableKeyField,
72
+ input: createVariableKeyInput(variables)
73
+ }
74
+ })
75
+ ],
76
+ components: {
77
+ inlineBlock: createVariableInlineBlock(variables)
78
+ }
79
+ }), baseBlock = block ?? sanity.defineArrayMember({ type: "block" });
80
+ return {
81
+ ...baseBlock,
82
+ of: [...baseBlock.of ?? [], variableType]
83
+ };
84
+ }
85
+ exports.VARIABLE_TYPE_PREFIX = VARIABLE_TYPE_PREFIX;
86
+ exports.interpolationVariables = interpolationVariables;
8
87
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":["../src/plugin.ts"],"sourcesContent":["import {definePlugin} from 'sanity'\nimport type {PteInterpolationPluginConfig} from './types'\n\n/** @public */\nexport const pteInterpolation = definePlugin<PteInterpolationPluginConfig | void>((_config) => {\n return {\n name: 'sanity-plugin-pte-interpolation',\n }\n})\n"],"names":["definePlugin"],"mappings":";;;AAIO,MAAM,mBAAmBA,OAAAA,aAAkD,CAAC,aAC1E;AAAA,EACL,MAAM;AACR,EACD;;"}
1
+ {"version":3,"file":"index.cjs","sources":["../src/components/VariableInlineBlock.tsx","../src/interpolationVariables.ts"],"sourcesContent":["import {SearchIcon} from '@sanity/icons'\nimport {Autocomplete, Box, Card, Text} from '@sanity/ui'\nimport {useCallback, useId} from 'react'\nimport {set, unset} from 'sanity'\nimport type {BlockProps, FieldProps, InputProps} from 'sanity'\nimport type {InterpolationVariable} from '../types'\n\ninterface VariableOption {\n value: string\n name: string\n description?: string\n}\n\nexport function VariableKeyField(props: FieldProps) {\n return <>{props.children}</>\n}\n\nexport function createVariableKeyInput(variables: InterpolationVariable[]) {\n const options: VariableOption[] = variables.map((variable) => ({\n value: variable.id,\n name: variable.name,\n description: variable.description,\n }))\n\n return function VariableKeyInput(props: InputProps) {\n const autocompleteId = useId()\n const selectedVariable = variables.find((variable) => variable.id === props.value)\n\n const handleChange = useCallback(\n (selectedValue: string) => {\n props.onChange(selectedValue ? set(selectedValue) : unset())\n },\n [props],\n )\n\n const filterOption = useCallback((query: string, option: VariableOption) => {\n return option.name.toLowerCase().includes(query.toLowerCase())\n }, [])\n\n const renderOption = useCallback((option: VariableOption) => {\n return (\n <Card as=\"button\" padding={3}>\n <Text size={1} weight=\"medium\">\n {option.name}\n </Text>\n {option.description && (\n <Box marginTop={2}>\n <Text size={0} muted>\n {option.description}\n </Text>\n </Box>\n )}\n </Card>\n )\n }, [])\n\n const renderValue = useCallback((_value: string, option?: VariableOption) => {\n if (option) return option.name\n const matchedOption = options.find((candidate) => candidate.value === _value)\n return matchedOption?.name ?? _value\n }, [])\n\n return (\n <>\n <Autocomplete\n id={autocompleteId}\n options={options}\n value={typeof props.value === 'string' ? props.value : undefined}\n onChange={handleChange}\n filterOption={filterOption}\n renderOption={renderOption}\n renderValue={renderValue}\n openButton\n icon={SearchIcon}\n placeholder=\"Search variables...\"\n fontSize={1}\n padding={3}\n />\n {selectedVariable?.description && (\n <Box marginTop={2}>\n <Text size={1} muted>\n {selectedVariable.description}\n </Text>\n </Box>\n )}\n </>\n )\n }\n}\n\nexport function createVariableInlineBlock(variables: InterpolationVariable[]) {\n return function VariableInlineBlock(props: BlockProps) {\n const value = props.value as {variableKey?: string}\n const variableKey = value?.variableKey\n const variable = variables.find((candidate) => candidate.id === variableKey)\n\n return props.renderDefault({\n ...props,\n renderPreview: () => (\n <Box padding={2}>\n <Text size={0} weight=\"medium\">\n {variable?.name ?? variableKey ?? 'Select variable'}\n </Text>\n </Box>\n ),\n })\n }\n}\n","import {TagIcon} from '@sanity/icons'\nimport {defineArrayMember, defineField} from 'sanity'\nimport {\n createVariableInlineBlock,\n createVariableKeyInput,\n VariableKeyField,\n} from './components/VariableInlineBlock'\nimport type {InterpolationVariable} from './types'\n\n/** @public */\nexport const VARIABLE_TYPE_PREFIX = 'pteInterpolationVariable'\n\n/** @public */\nexport function interpolationVariables(\n variables: InterpolationVariable[],\n block?: ReturnType<typeof defineArrayMember>,\n) {\n const variableType = defineArrayMember({\n type: 'object',\n name: VARIABLE_TYPE_PREFIX,\n title: 'Variable',\n icon: TagIcon,\n options: {\n modal: {width: 0},\n },\n fields: [\n defineField({\n name: 'variableKey',\n title: 'Variable',\n type: 'string',\n validation: (rule) => rule.required(),\n components: {\n field: VariableKeyField,\n input: createVariableKeyInput(variables),\n },\n }),\n ],\n components: {\n inlineBlock: createVariableInlineBlock(variables),\n },\n })\n\n const baseBlock = block ?? defineArrayMember({type: 'block'})\n\n return {\n ...baseBlock,\n of: [...((baseBlock as {of?: unknown[]}).of ?? []), variableType],\n }\n}\n"],"names":["jsx","Fragment","useId","useCallback","set","unset","jsxs","Card","Text","Box","Autocomplete","SearchIcon","defineArrayMember","TagIcon","defineField"],"mappings":"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"}