├── .gitignore
├── .npmignore
├── .prettierrc
├── CHANGELOG.md
├── README.md
├── examples
├── color-list.jsx
├── hero-schema.js
└── icons.jsx
├── images
├── preview-color-list.png
├── preview-large.png
├── preview-medium.png
├── preview-small.png
└── preview.png
├── package-lock.json
├── package.config.ts
├── package.json
├── sanity.json
├── src
├── components
│ ├── VisualOptions.css
│ ├── VisualOptions.jsx
│ ├── VisualOptionsItem.jsx
│ └── index.jsx
└── index.js
├── tsconfig.dist.json
├── tsconfig.json
├── tsconfig.settings.json
└── v2-incompatible.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | .idea/
3 | .vscode/
4 | .DS_Store
5 | node_modules/
6 | lib/
7 | dist/
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vscode/
3 | .DS_Store
4 | .prettierrc
5 | .babelrc
6 | /src
7 | /example
8 | /node_modules
9 | /images
10 | /examples
11 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "lf",
3 | "semi": false,
4 | "singleQuote": false,
5 | "tabWidth": 2,
6 | "trailingComma": "es5"
7 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | Notable changes are within this log. Not all patch releases will be detailed, see commit history for a greater insight.
4 |
5 | ## 0.8.3 - 9th November 2020
6 |
7 | ### Bug Fixes
8 |
9 | - Resolves CSS compilation issue with Sanity Studio 2.0 due to Sanity changing variable name from `--min-medium` to `--screen-medium`.
10 | ([Issue #5](https://github.com/fractaldimensions/sanity-plugin-visual-options/issues/5) - Thanks to [GarethChetwood](https://github.com/GarethChetwood) for reporting)
11 |
12 | ## 0.8.2 - 28th May 2020
13 |
14 | - Removes unnecessary dependency 'typescript'
15 |
16 | ## 0.8.0 - 4th May 2020
17 |
18 | ### Features
19 |
20 | - Labels can be shown as tool tips on hover by adding `showTooltip: true` to the options.
21 | - Option sizes can be set to `small` and `large` by adding `optionSize: "small"` to the options ([Issue #2](https://github.com/fractaldimensions/sanity-plugin-visual-options/issues/2))
22 |
23 | ## 0.7.0 - 9th April 2020
24 |
25 | Last of stability issues resolved.
26 |
27 | ### Bug Fixes
28 |
29 | - Change of control warning in console ([Issue #4](https://github.com/fractaldimensions/sanity-plugin-visual-options/issues/4))
30 |
31 | ## 0.6.7
32 |
33 | ### Bug Fixes
34 |
35 | - Grid drops to two columns on mobile view.
36 | - [react-icons](https://react-icons.netlify.com/#/) now fills full space.
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sanity Visual Option List
2 |
3 | > This is a **Sanity Studio v3** plugin.
4 | > For the v2 version, please refer to the [v2-branch](https://github.com/fractaldimensions/sanity-plugin-visual-options/tree/studio-v2/).
5 |
6 | [![NPM Version][npm-image]][npm-url]
7 | [![NPM Version][npm-dl-image]][npm-url]
8 |
9 | _For notable updates and bug fixes see [change log](https://github.com/fractaldimensions/sanity-plugin-visual-options/blob/next/CHANGELOG.md)_
10 |
11 | A visual way to show options to users, for example, what layout to apply for a text/image component (default layout):
12 |
13 | 
14 |
15 | Or with tooltip, and as smaller icons:
16 |
17 | 
18 |
19 | Or, also for color options:
20 |
21 | 
22 |
23 | See code at [over here](https://github.com/fractaldimensions/sanity-plugin-visual-options/blob/next/examples/color-list.jsx) for how to implement a color list.
24 |
25 |
26 | ## Installation
27 |
28 | From the terminal within the Sanity Studio directory:
29 |
30 | ```
31 | npm install sanity-plugin-visual-options
32 | ```
33 |
34 | Then add the plugin to your sanity.config.js/ts file:
35 |
36 | ```javascript
37 | import { visualOptions } from "sanity-plugin-visual-options";
38 |
39 | export default defineConfig({
40 | // ...
41 | plugins: [
42 | visualOptions(),
43 | ]
44 | })
45 | ```
46 |
47 | ## Usage
48 |
49 | Schema to produce the above screenshot can be found [here](https://github.com/fractaldimensions/sanity-plugin-visual-options/blob/next/examples/hero-schema.js) with the icons found [here](https://github.com/fractaldimensions/sanity-plugin-visual-options/blob/next/examples/icons.jsx)
50 |
51 | In your schema, you should add a field of type 'visualOptions', and the options property should contain a key of 'list'. Within this is another dictionary, with the key being the reference that will be saved against the item. Each item must contain an icon as a minimum, which is a React Component.
52 |
53 | In the example below, which produced the image above with small options, the icons are simple React components returning an SVG, therefore react-icons should work here too.
54 |
55 | _*NOTE: As of Sanity Studio V3 when using React within a schema the schema file must have the extension jsx or tsx not js/ts*_
56 |
57 | ```javascript
58 | {
59 | ...,
60 | fields: [
61 | {
62 | name: "blockLayout",
63 | title: "Block Layout",
64 | type: "visualOptions",
65 | options: {
66 | showTooltip: true,
67 | optionSize: "small",
68 | list: {
69 | left: {
70 | name: "Text Left / Image Right",
71 | icon: OITextLeftOverlap,
72 | default: true,
73 | },
74 | right: {
75 | name: "Text Right / Image Left",
76 | icon: OITextRightOverlap,
77 | },
78 | top: {
79 | name: "Text Top / Image Bottom",
80 | icon: OITextTopOverlap,
81 | },
82 | bottom: {
83 | name: "Text Botom / Image Top",
84 | icon: OITextBottomOverlap,
85 | },
86 | notext: {
87 | name: "Image, No Text",
88 | icon: OIImage,
89 | },
90 | noimage: {
91 | name: "Text, No Image",
92 | icon: OIText,
93 | },
94 | },
95 | },
96 | },
97 | ...
98 | ],
99 | }
100 | ```
101 |
102 | ## Options
103 |
104 | Within the `options` for the schema, there are the following options:
105 |
106 | - `showLabels: (true|false)` - Sets whether to show the labels for each item based on their name.
107 | - `showTooltip: (true|false)` - The name of the item will be turned into a tooltip and displayed on hover. Overrides `showLabels` above.
108 | - `optionSize: ("small"|"medium"|"large")` - Sets the size of the option items. Defaults to "medium" if omitted or and invalid option is provided.
109 | - `shape: ("circle"|"box")` - Optional, if omitted, default will be "box".
110 |
111 | ## Layout Options
112 |
113 | ## Small
114 |
115 |
116 |
117 | ## Medium
118 |
119 |
120 |
121 | ## Large
122 |
123 |
124 |
125 | ## Future Development/Considerations
126 |
127 | - Allow items multi selections with limits e.g. maximum of two.
128 | - Add a check mark to show selection and allow de-selection (moving of radio to checkboxes also solving the above item).
129 | - Allow standard images to be displayed rather than just SVGs.
130 |
131 | [npm-image]: https://badgen.net/npm/v/sanity-plugin-visual-options?2.0.0-beta.1
132 | [npm-url]: https://npmjs.org/package/sanity-plugin-visual-options
133 | [npm-dl-image]: https://badgen.net/npm/dt/sanity-plugin-visual-options
134 |
--------------------------------------------------------------------------------
/examples/color-list.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | // Color list that can be stored centrally or generated from another document
4 | const colorList = [
5 | {
6 | key: "one",
7 | title: "Blue",
8 | value: "#137380",
9 | },
10 | {
11 | key: "two",
12 | title: "Purple",
13 | value: "#7C1375",
14 | },
15 | {
16 | key: "three",
17 | title: "Mint",
18 | value: "#00FFA3",
19 | },
20 | {
21 | key: "gradient-one-two",
22 | title: "Gradient",
23 | value: "linear-gradient(110.72deg, #137380 0%, #7C1375 100.52%)"
24 | }
25 | ];
26 |
27 | // Component to appear inside the option list
28 | const Color = (color) => {
29 | return () =>
34 | }
35 |
36 | // Create option list using a combination of Color component and the color list.
37 | const colors = Object.assign(
38 | {},
39 | ...colorList.map((c) => ({
40 | [c.key]: {
41 | name: c.title,
42 | icon: Color(c.value),
43 | },
44 | }))
45 | );
46 |
47 | // Standard schema settings
48 | export default {
49 | name: "colorList",
50 | title: "Colour",
51 | type: "visualOptions",
52 | options: {
53 | showTooltip: false,
54 | optionSize: "small",
55 | shape: "circle",
56 | list: colors, // Set color options as list
57 | }
58 | }
--------------------------------------------------------------------------------
/examples/hero-schema.js:
--------------------------------------------------------------------------------
1 | import {
2 | OITextLeftOverlap,
3 | OITextRightOverlap,
4 | OIImage,
5 | OIText,
6 | OITextBottomOverlap,
7 | OITextTopOverlap,
8 | } from "./icons"
9 |
10 | export default {
11 | name: "hero",
12 | title: "Hero",
13 | type: "object",
14 | fields: [
15 | {
16 | name: "blockLayout",
17 | title: "Block Layout",
18 | type: "visualOptions",
19 | options: {
20 | list: {
21 | left: {
22 | name: "Text Left / Image Right",
23 | icon: OITextLeftOverlap,
24 | },
25 | right: {
26 | name: "Text Right / Image Left",
27 | icon: OITextRightOverlap,
28 | default: true,
29 | },
30 | top: {
31 | name: "Text Top / Image Bottom",
32 | icon: OITextTopOverlap,
33 | },
34 | bottom: {
35 | name: "Text Botom / Image Top",
36 | icon: OITextBottomOverlap,
37 | },
38 | notext: {
39 | name: "Image, No Text",
40 | icon: OIImage,
41 | },
42 | noimage: {
43 | name: "Text, No Image",
44 | icon: OIText,
45 | },
46 | },
47 | },
48 | },
49 | {
50 | name: "image",
51 | title: "Image",
52 | type: "image",
53 | },
54 | {
55 | name: "blockContent",
56 | title: "Content",
57 | type: "array",
58 | of: [
59 | {
60 | title: "Block",
61 | type: "block",
62 | styles: [
63 | { title: "Normal", value: "normal" },
64 | { title: "H1", value: "h1" },
65 | { title: "H2", value: "h2" },
66 | { title: "H3", value: "h3" },
67 | { title: "H4", value: "h4" },
68 | { title: "Quote", value: "blockquote" },
69 | ],
70 | lists: [{ title: "Bullet", value: "bullet" }],
71 | marks: {},
72 | },
73 | {
74 | type: "actionButton",
75 | },
76 | ],
77 | },
78 | ],
79 | preview: {
80 | select: {
81 | title: "blockContent",
82 | imageUrl: "image.asset.url",
83 | },
84 | prepare(selection) {
85 | const block = (selection.title || []).find(
86 | block => block._type === "block"
87 | )
88 | return {
89 | title: block.children
90 | ? block.children
91 | .filter(child => child._type == "span")
92 | .map(span => span.text)
93 | .join("")
94 | : "Hero",
95 | imageUrl: selection.imageUrl,
96 | }
97 | },
98 | },
99 | }
100 |
--------------------------------------------------------------------------------
/examples/icons.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const OIText = () =>
4 |
5 | export const OIImage = () =>
6 |
7 | export const OITextLeftOverlap = () =>
8 |
9 | export const OITextRightOverlap = () =>
10 |
11 | export const OITextTopOverlap = () =>
12 |
13 | export const OITextBottomOverlap = () =>
14 |
--------------------------------------------------------------------------------
/images/preview-color-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fddigital-uk/sanity-plugin-visual-options/79b2cca48b35133bc0e48032694cdeedda4a8dc1/images/preview-color-list.png
--------------------------------------------------------------------------------
/images/preview-large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fddigital-uk/sanity-plugin-visual-options/79b2cca48b35133bc0e48032694cdeedda4a8dc1/images/preview-large.png
--------------------------------------------------------------------------------
/images/preview-medium.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fddigital-uk/sanity-plugin-visual-options/79b2cca48b35133bc0e48032694cdeedda4a8dc1/images/preview-medium.png
--------------------------------------------------------------------------------
/images/preview-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fddigital-uk/sanity-plugin-visual-options/79b2cca48b35133bc0e48032694cdeedda4a8dc1/images/preview-small.png
--------------------------------------------------------------------------------
/images/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fddigital-uk/sanity-plugin-visual-options/79b2cca48b35133bc0e48032694cdeedda4a8dc1/images/preview.png
--------------------------------------------------------------------------------
/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 | import postcss from 'rollup-plugin-postcss'
3 |
4 | export default defineConfig({
5 | legacyExports: true,
6 | dist: 'dist',
7 | tsconfig: 'tsconfig.dist.json',
8 |
9 | // Remove this block to enable strict export validation
10 | extract: {
11 | rules: {
12 | 'ae-forgotten-export': 'off',
13 | 'ae-incompatible-release-tags': 'off',
14 | 'ae-internal-missing-underscore': 'off',
15 | 'ae-missing-release-tag': 'off',
16 | },
17 | },
18 |
19 | rollup: {
20 | // @ts-ignore ¯\_(ツ)_/¯
21 | plugins: [postcss({modules: true})],
22 | },
23 | })
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sanity-plugin-visual-options",
3 | "version": "2.0.2",
4 | "description": "A custom input for options to user by displaying a visual representation.",
5 | "keywords": [
6 | "sanity",
7 | "sanity-plugin",
8 | "options",
9 | "visual",
10 | "display",
11 | "sanityio",
12 | "plugin",
13 | "plugins",
14 | "sanity.io",
15 | "list"
16 | ],
17 | "license": "ISC",
18 | "author": "Nick Taylor",
19 | "exports": {
20 | ".": {
21 | "source": "./src/index.js",
22 | "import": "./dist/index.esm.js",
23 | "require": "./dist/index.js",
24 | "default": "./dist/index.esm.js"
25 | },
26 | "./package.json": "./package.json"
27 | },
28 | "main": "./dist/index.js",
29 | "module": "./dist/index.esm.js",
30 | "source": "./src/index.js",
31 | "files": [
32 | "dist",
33 | "sanity.json",
34 | "src",
35 | "v2-incompatible.js"
36 | ],
37 | "scripts": {
38 | "build": "run-s clean && plugin-kit verify-package --silent && pkg-utils build --strict && pkg-utils --strict",
39 | "clean": "rimraf dist",
40 | "format": "prettier --write --cache --ignore-unknown .",
41 | "link-watch": "plugin-kit link-watch",
42 | "lint": "eslint .",
43 | "prepublishOnly": "run-s build",
44 | "watch": "pkg-utils watch --strict"
45 | },
46 | "dependencies": {
47 | "@sanity/incompatible-plugin": "^1.0.4"
48 | },
49 | "devDependencies": {
50 | "@sanity/pkg-utils": "^3.2.3",
51 | "@sanity/plugin-kit": "^3.1.10",
52 | "@types/react": "^18.2.37",
53 | "@typescript-eslint/eslint-plugin": "^6.11.0",
54 | "@typescript-eslint/parser": "^6.11.0",
55 | "eslint": "^8.54.0",
56 | "eslint-config-prettier": "^9.0.0",
57 | "eslint-config-sanity": "^7.0.1",
58 | "eslint-plugin-prettier": "^5.0.1",
59 | "eslint-plugin-react": "^7.33.2",
60 | "eslint-plugin-react-hooks": "^4.6.0",
61 | "npm-run-all": "^4.1.5",
62 | "prettier": "^3.1.0",
63 | "prettier-plugin-packagejson": "^2.4.6",
64 | "react": "^18.2.0",
65 | "react-dom": "^18.2.0",
66 | "react-is": "^18.2.0",
67 | "rimraf": "^5.0.5",
68 | "rollup-plugin-postcss": "^4.0.2",
69 | "sanity": "^3.20.0",
70 | "styled-components": "^5.3.11",
71 | "typescript": "^5.2.2"
72 | },
73 | "peerDependencies": {
74 | "react": "^18",
75 | "sanity": "^3"
76 | },
77 | "engines": {
78 | "node": ">=14"
79 | },
80 | "repository": {
81 | "type": "git",
82 | "url": "git+https://github.com/fractaldimensions/sanity-plugin-visual-options.git"
83 | },
84 | "bugs": {
85 | "url": "https://github.com/fractaldimensions/sanity-plugin-visual-options/issues"
86 | },
87 | "homepage": "https://github.com/fractaldimensions/sanity-plugin-visual-options#readme"
88 | }
89 |
--------------------------------------------------------------------------------
/sanity.json:
--------------------------------------------------------------------------------
1 | {
2 | "parts": [
3 | {
4 | "implements": "part:@sanity/base/sanity-root",
5 | "path": "./v2-incompatible.js"
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/VisualOptions.css:
--------------------------------------------------------------------------------
1 | /* Support for Sanity < v2.0 - variable rename from --min-medium to --screen-medium */
2 | @custom-media --screen-medium (min-width: 512px);
3 |
4 | .container {
5 | --state-danger-color: maroon;
6 | --extra-small-padding: 0.4em;
7 | --small-padding: 0.6em;
8 | --medium-padding: 1em;
9 | --large-padding: 1.5em;
10 | --border-radius-large: 10px;
11 | --hairline-color: #07163f;
12 | --brand-primary--inverted: black;
13 | --brand-primary: #5998fc;
14 | }
15 |
16 | .error {
17 | color: var(--state-danger-color);
18 | font-weight: bold;
19 | }
20 |
21 | .grid {
22 | display: grid;
23 | grid-template-columns: 1fr 1fr;
24 | grid-gap: var(--medium-padding);
25 | }
26 |
27 | @media (min-width: 768px) {
28 | .grid {
29 | grid-template-columns: repeat(4, 1fr);
30 | }
31 | }
32 |
33 | .grid.small {
34 | grid-gap: var(--extra-small-padding);
35 | grid-template-columns: repeat(4, 1fr);
36 | }
37 |
38 | @media (min-width: 768px) {
39 | .grid.small {
40 | grid-template-columns: repeat(6, 1fr);
41 | }
42 | }
43 |
44 | .grid.large {
45 | grid-template-columns: repeat(1, 1fr);
46 | }
47 |
48 | @media (min-width: 768px) {
49 | .grid.large {
50 | grid-template-columns: repeat(3, 1fr);
51 | }
52 | }
53 |
54 | .item {
55 | position: relative;
56 | padding: var(--extra-small-padding);
57 | border-radius: var(--border-radius-large);
58 | border: 1px solid var(--hairline-color);
59 | box-sizing: border-box;
60 | cursor: pointer;
61 | background-color: white;
62 | }
63 |
64 | .item .icon {
65 | min-height: calc(var(--large-padding) * 4);
66 | }
67 |
68 | .item > small {
69 | display: block;
70 | text-align: center;
71 | }
72 |
73 | .grid:not(.circle):not(.tooltip) .item {
74 | display: flex;
75 | flex-direction: column;
76 | }
77 |
78 | .grid:not(.circle):not(.tooltip) .item .icon {
79 | flex-grow: 1;
80 | }
81 |
82 | .grid.circle .item, .grid.circle.small .item, .grid.circle.large .item {
83 | border-radius: 50%;
84 | }
85 |
86 | .grid.circle .item:before, .grid.circle.small .item:before, .grid.circle.large .item:before {
87 | border-radius: 50%;
88 | border-width: 3px;
89 | }
90 |
91 | .grid.circle .icon, .grid.circle.small .icon, .grid.circle.large .icon {
92 | border-radius: 50%;
93 | padding: 0;
94 | overflow: hidden;
95 | }
96 |
97 | .grid.small .icon {
98 | min-height: calc(var(--medium-padding) * 4);
99 | padding: var(--small-padding);
100 | }
101 |
102 | .grid.large .icon {
103 | min-height: calc(var(--large-padding) * 6);
104 | }
105 |
106 | .grid.circle {
107 | display: block;
108 | }
109 |
110 | .grid.circle .item {
111 | float: left;
112 | height: 5rem;
113 | width: 5rem;
114 | display: flex;
115 | margin-right: 0.5rem;
116 | margin-top: 0.5rem;
117 | }
118 |
119 | .grid.circle .item:not(.tooltip) .tip {
120 | display: none;
121 | }
122 |
123 | .grid.circle .item .icon {
124 | width: 100%;
125 | min-height: auto;
126 | }
127 |
128 | .grid.circle.large .item {
129 | height: 8rem;
130 | width: 8rem;
131 | }
132 |
133 | .grid.circle.small .item {
134 | height: 3rem;
135 | width: 3rem;
136 | }
137 |
138 | .hidden {
139 | position: absolute;
140 | left: -1000px;
141 | top: auto;
142 | width: 1px;
143 | height: 1px;
144 | overflow: hidden;
145 | }
146 |
147 | .icon {
148 | display: grid;
149 | align-items: center;
150 | padding: var(--medium-padding);
151 | box-sizing: border-box;
152 | }
153 |
154 | .icon > * {
155 | height: 100%;
156 | width: 100%;
157 | }
158 |
159 | .icon > img {
160 | object-fit: contain;
161 | }
162 |
163 | .tip {
164 | text-align: center;
165 | line-height: 1.1;
166 | padding: var(--small-padding);
167 | padding-top: 0;
168 | color: black;
169 | }
170 |
171 | .name {
172 | text-align: center;
173 | }
174 |
175 | .tooltip {
176 | position: relative;
177 | }
178 |
179 | .tooltip small {
180 | background-color: var(--brand-primary);
181 | color: var(--brand-primary--inverted);
182 | padding: var(--small-padding);
183 | border-radius: 5px;
184 | }
185 |
186 | .tooltip .tip {
187 | padding: 0;
188 | display: none;
189 | }
190 |
191 | .tooltip.over .tip {
192 | position: absolute;
193 | display: flex;
194 | width: calc(100% - 0.5rem);
195 | box-sizing: border-box;
196 | bottom: 0;
197 | justify-content: center;
198 | align-content: center;
199 | justify-items: center;
200 | align-items: center;
201 | }
202 |
203 | .tooltip.over .tip small {
204 | position: relative;
205 | box-sizing: border-box;
206 | z-index: 500;
207 | transform: translateY(calc(100% + 0.25rem));
208 | }
209 |
210 | .tooltip.over .tip small:before {
211 | position: absolute;
212 | top: -5px;
213 | left: calc(50% - 5px);
214 | border: solid var(--brand-primary);
215 | border-width: 0 10px 10px 0;
216 | transform: rotate(-135deg);
217 | content: " ";
218 | }
219 |
220 | .selected:before {
221 | content: "";
222 | position: absolute;
223 | top: 0;
224 | left: 0;
225 | right: 0;
226 | bottom: 0;
227 | border: 2px solid var(--brand-primary);
228 | border-radius: var(--border-radius-large);
229 | }
230 |
--------------------------------------------------------------------------------
/src/components/VisualOptions.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styles from './VisualOptions.css'
3 | import VisualOptionsItem from './VisualOptionsItem'
4 |
5 | class VisualOptions extends React.Component {
6 | focus() {
7 | if (this.inputElement) {
8 | this.inputElement.focus()
9 | }
10 | }
11 |
12 | render() {
13 | const {shape = '', options, value, size = '', showLabel, showTooltip, onChange} = this.props
14 |
15 | const extraClasses = []
16 |
17 | if (['large', 'small', 'tiny'].indexOf(size.toLowerCase()) > -1) {
18 | extraClasses.push(styles[size.toLowerCase()])
19 | }
20 |
21 | if (shape) {
22 | extraClasses.push(styles[shape.toLowerCase()])
23 | }
24 |
25 | return (
26 |
27 | {options && (
28 |
29 | {Object.keys(options).map((k, i) => (
30 |
38 | !this.inputElement && (k == value || (value == undefined && i == 0))
39 | ? (this.inputElement = element)
40 | : null
41 | }
42 | onChange={() => onChange(k)}
43 | />
44 | ))}
45 |
46 | )}
47 | {!options && (
48 |
49 | Options must be an array with at least a value for `key`
50 |
51 | )}
52 |
53 | )
54 | }
55 | }
56 |
57 | export default VisualOptions
58 |
--------------------------------------------------------------------------------
/src/components/VisualOptionsItem.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styles from './VisualOptions.css'
3 |
4 | class VisualOptionsItem extends React.Component {
5 | constructor(props) {
6 | super(props)
7 | this.state = {over: false}
8 | }
9 | focus() {
10 | if (this.inputElement) {
11 | this.inputElement.focus()
12 | }
13 | }
14 |
15 | render() {
16 | const {
17 | fieldName,
18 | name,
19 | value,
20 | selected,
21 | showLabel = true,
22 | showLabelAsTooltip = false,
23 | onChange,
24 | icon: Icon,
25 | } = this.props
26 |
27 | return (
28 | this.setState({over: true})}
33 | onMouseOut={() => this.setState({over: false})}
34 | onClick={(e) => {
35 | this.inputElement.click()
36 | e.preventDefault()
37 | this.focus()
38 | }}
39 | >
40 |
41 |
42 |
43 | {(showLabel || showLabelAsTooltip) && name && (
44 |
45 | {name}
46 |
47 | )}
48 |
(this.inputElement ? null : (this.inputElement = element))}
57 | />
58 |
59 | )
60 | }
61 | }
62 |
63 | export default VisualOptionsItem
64 |
--------------------------------------------------------------------------------
/src/components/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import {FormField, set, unset} from 'sanity'
4 | import VisualOptions from './VisualOptions'
5 |
6 | const createPatchFrom = (value) => {
7 | if (value === '') {
8 | return unset()
9 | }
10 | return set(value)
11 | }
12 |
13 | class VisualOptionsContainer extends React.Component {
14 | focus() {
15 | if (this.inputElement) {
16 | this.inputElement.focus()
17 | }
18 | }
19 |
20 | selectItem(item) {
21 | this.props.onChange(createPatchFrom(item))
22 | }
23 |
24 | render() {
25 | const {value, schemaType: type, level} = this.props
26 |
27 | return (
28 |
33 | (this.inputElement ? null : (this.inputElement = element))}
41 | onChange={(item) => this.selectItem(item)}
42 | />
43 |
44 | )
45 | }
46 | }
47 |
48 | VisualOptionsContainer.propTypes = {
49 | value: PropTypes.string,
50 | options: PropTypes.shape({
51 | list: PropTypes.object,
52 | }),
53 | onChange: PropTypes.func.isRequired,
54 | }
55 |
56 | export default VisualOptionsContainer
57 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import {createPlugin, defineType} from 'sanity'
2 | import VisualOptions from './components'
3 |
4 | export const visualOptions = createPlugin({
5 | schema: {
6 | types: [
7 | defineType({
8 | title: 'Visual Options',
9 | type: 'string',
10 | name: 'visualOptions',
11 | components: {
12 | input: VisualOptions,
13 | },
14 | }),
15 | ],
16 | },
17 | })
18 |
--------------------------------------------------------------------------------
/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings",
3 | "include": ["./src"],
4 | "exclude": [
5 | "./src/**/__fixtures__",
6 | "./src/**/__mocks__",
7 | "./src/**/*.test.ts",
8 | "./src/**/*.test.tsx"
9 | ],
10 | "compilerOptions": {
11 | "rootDir": ".",
12 | "outDir": "./dist",
13 | "jsx": "react-jsx",
14 | "emitDeclarationOnly": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings",
3 | "include": ["./src", "./package.config.ts"],
4 | "compilerOptions": {
5 | "rootDir": ".",
6 | "jsx": "react-jsx",
7 | "noEmit": true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "moduleResolution": "node",
4 | "target": "esnext",
5 | "module": "esnext",
6 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
7 | "esModuleInterop": true,
8 | "strict": true,
9 | "downlevelIteration": true,
10 | "declaration": true,
11 | "allowSyntheticDefaultImports": true,
12 | "skipLibCheck": true,
13 | "isolatedModules": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/v2-incompatible.js:
--------------------------------------------------------------------------------
1 | const {showIncompatiblePluginDialog} = require('@sanity/incompatible-plugin')
2 | const {name, version, sanityExchangeUrl} = require('./package.json')
3 |
4 | export default showIncompatiblePluginDialog({
5 | name: name,
6 | versions: {
7 | v3: version,
8 | v2: undefined,
9 | },
10 | sanityExchangeUrl,
11 | })
12 |
--------------------------------------------------------------------------------