├── .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 | ![preview of default](https://github.com/fractaldimensions/sanity-plugin-visual-options/raw/next/images/preview.png) 14 | 15 | Or with tooltip, and as smaller icons: 16 | 17 | ![preview of small with tooltip](https://github.com/fractaldimensions/sanity-plugin-visual-options/raw/next/images/preview-small.png) 18 | 19 | Or, also for color options: 20 | 21 | ![preview of large with tooltip](https://github.com/fractaldimensions/sanity-plugin-visual-options/raw/next/images/preview-color-list.png) 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 | preview of small with tooltip 116 | 117 | ## Medium 118 | 119 | preview of medium (default) with tooltip 120 | 121 | ## Large 122 | 123 | preview of large with tooltip 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 | --------------------------------------------------------------------------------