├── .editorconfig ├── .gitignore ├── .npmignore ├── .postcssrc ├── .prettierrc ├── API.md ├── LICENSE ├── README.md ├── dev ├── start.js └── webpack.config.js ├── examples ├── next-js │ ├── .gitignore │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ ├── index.js │ │ ├── index.module.css │ │ └── layout.css │ └── postcss.config.js └── webpack │ ├── .gitignore │ ├── .postcssrc │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── index.html │ ├── index.js │ ├── index.module.css │ └── layout.css │ └── webpack.config.js ├── package-lock.json ├── package.json ├── src ├── @dev │ ├── App.module.css │ ├── App.tsx │ ├── base.css │ ├── client.tsx │ ├── components │ │ ├── CenteredWrapper.module.css │ │ └── CenteredWrapper.tsx │ ├── lib │ │ └── schema │ │ │ ├── index.ts │ │ │ └── types.ts │ ├── matchRoute.ts │ ├── screens │ │ ├── root │ │ │ ├── IFrame.tsx │ │ │ ├── PropsForm.tsx │ │ │ ├── RootScreen.module.css │ │ │ ├── RootScreen.tsx │ │ │ ├── field │ │ │ │ ├── BooleanField.tsx │ │ │ │ ├── Field.module.css │ │ │ │ ├── Field.tsx │ │ │ │ ├── NumberField.tsx │ │ │ │ ├── StringField.tsx │ │ │ │ ├── TextField.tsx │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ └── index.tsx │ │ └── stage │ │ │ ├── StageScreen.module.css │ │ │ ├── StageScreen.tsx │ │ │ └── index.tsx │ ├── server.js │ ├── stories.ts │ ├── streams │ │ ├── history.ts │ │ └── message.ts │ └── types.ts ├── avatar │ ├── Avatar.module.css │ ├── Avatar.tsx │ ├── Placeholder.module.css │ ├── Placeholder.tsx │ ├── index.ts │ ├── stories.tsx │ └── story.tsx ├── button │ ├── Button.module.css │ ├── Button.tsx │ ├── index.ts │ ├── stories.tsx │ └── story.tsx ├── code │ ├── AnnotatedCodeSection.module.css │ ├── AnnotatedCodeSection.tsx │ ├── Code.module.css │ ├── Code.tsx │ ├── Lowlight.tsx │ ├── index.ts │ ├── languages │ │ └── groq.ts │ ├── stories.tsx │ └── story.tsx ├── icon │ ├── ChevronDownIcon.tsx │ ├── HamburgerIcon.tsx │ ├── Icon.module.css │ ├── index.tsx │ ├── stories.tsx │ └── story.tsx ├── index.ts ├── input │ ├── CheckboxInput.tsx │ ├── StringInput.tsx │ ├── index.ts │ ├── stories.tsx │ ├── story.module.css │ └── story.tsx ├── lib │ ├── classNameUtils.ts │ └── stringUtils.ts ├── login │ ├── GitHubLogo.tsx │ ├── GoogleLogo.tsx │ ├── Login.module.css │ ├── Login.tsx │ ├── index.ts │ ├── stories.tsx │ └── story.tsx ├── logo │ ├── GitHubLogo.module.css │ ├── GitHubLogo.tsx │ ├── NetlifyLogo.module.css │ ├── NetlifyLogo.tsx │ ├── SanityLogo.module.css │ ├── SanityLogo.tsx │ ├── ZeitLogo.module.css │ ├── ZeitLogo.tsx │ ├── index.tsx │ └── stories.tsx ├── message │ ├── Message.module.css │ ├── Message.tsx │ └── stories.tsx ├── siteFooter │ ├── SiteFooter.module.css │ ├── SiteFooter.tsx │ ├── index.tsx │ ├── stories.tsx │ └── story.tsx ├── siteHeader │ ├── SiteHeader.module.css │ ├── SiteHeader.tsx │ ├── index.tsx │ └── stories.tsx ├── spinner │ ├── Spinner.module.css │ ├── Spinner.tsx │ ├── index.ts │ ├── stories.tsx │ └── story.tsx ├── suggestSearch │ ├── SuggestSearch.module.css │ ├── SuggestSearch.tsx │ ├── index.ts │ ├── stories.module.css │ └── stories.tsx ├── typography │ └── Typography.module.css └── wizard │ ├── WizardStep.module.css │ ├── WizardStep.tsx │ ├── WizardSteps.module.css │ ├── WizardSteps.tsx │ ├── index.ts │ └── stories.tsx ├── ts ├── tsconfig.cjs.json ├── tsconfig.es.json └── types.d.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # Matches multiple files with brace expansion notation 14 | # Set default charset 15 | [*.{js, ts, tsx}] 16 | charset = utf-8 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /lib 3 | /node_modules 4 | /storybook-static 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .prettierrc 3 | examples 4 | src 5 | storybook 6 | storybook-static 7 | ts 8 | tsconfig.json 9 | -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "modules": true, 3 | "plugins": { 4 | "postcss-import": {}, 5 | "postcss-preset-env": { 6 | importFrom: [ 7 | "node_modules/sanity-web-styles/src/custom-media.css", 8 | "node_modules/sanity-web-styles/src/custom-properties.css" 9 | ], 10 | stage: 3, 11 | "features": { 12 | "color-mod-function": { "unresolved": "warn" }, 13 | "custom-media-queries": { 14 | "preserve": false 15 | }, 16 | "custom-properties": { 17 | "preserve": false 18 | }, 19 | "nesting-rules": true 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API documentation 2 | 3 | ## `` 4 | 5 | | Property | Type | Required | 6 | | -------- | -------------------------------- | -------- | 7 | | `image` | `{src: string, alt: string}` | No | 8 | | `size` | `"small" \| "normal" \| "large"` | No | 9 | 10 | ```jsx 11 | import { Avatar } from 'sanity-web-react-components' 12 | 13 | const vdom = 14 | ``` 15 | 16 | ## ` 29 | ``` 30 | 31 | ## `` 32 | 33 | | Property | Type | Required | 34 | | ---------- | --------------------------------------------------------------------------------------------- | -------- | 35 | | `code` | `String` | Yes | 36 | | `language` | `"css" \| "html" \| "markdown" \| "javascript" \| "json" \| "jsx" \| "php" \| "text" \| "sh"` | Yes | 37 | 38 | ```jsx 39 | import { Code } from 'sanity-web-react-components' 40 | 41 | const vdom = 42 | ``` 43 | 44 | ## `` 45 | 46 | | Property | Type | Required | 47 | | -------- | ------------- | -------- | 48 | | `symbol` | `"hamburger"` | Yes | 49 | 50 | ```jsx 51 | import { Icon } from 'sanity-web-react-components' 52 | 53 | const vdom = 54 | ``` 55 | 56 | ## `` 57 | 58 | ```jsx 59 | import { SanityLogo } from 'sanity-web-react-components' 60 | 61 | const vdom = 62 | ``` 63 | 64 | ## `` 65 | 66 | | Property | Type | Required | 67 | | ------------- | -------- | -------- | 68 | | `title` | `string` | Yes | 69 | | `description` | `string` | Yes | 70 | | `status` | `any` | Yes | 71 | 72 | ```jsx 73 | import { ServiceConnect } from 'sanity-web-react-components' 74 | 75 | const vdom = ( 76 | 81 | ) 82 | ``` 83 | 84 | ## `` 85 | 86 | | Property | Type | Required | 87 | | -------------- | ------------------------------------ | -------- | 88 | | `systemStatus` | `"partially-down" \| "down" \| "up"` | No | 89 | 90 | ```jsx 91 | import { SiteFooter } from 'sanity-web-react-components' 92 | 93 | const vdom = 94 | ``` 95 | 96 | ## `` 97 | 98 | | Property | Type | Required | 99 | | ------------ | ---------- | ------------------------- | 100 | | `onHideNav` | `Function` | Yes | 101 | | `onShowNav` | `Function` | Yes | 102 | | `renderLink` | `Function` | No | 103 | | `showNav` | `Boolean` | Yes | 104 | | `siteTitle` | `String` | No (defaults to "Sanity") | 105 | 106 | ```jsx 107 | import { useState } from 'react' 108 | import { SiteHeader } from 'sanity-web-react-components' 109 | 110 | function App() { 111 | const [showNav, setShowNav] = useState(false) 112 | return ( 113 | setShowNav(false)} 115 | onShowNav={() => setShowNav(true)} 116 | showNav={showNav} 117 | /> 118 | ) 119 | } 120 | ``` 121 | 122 | ## `` 123 | 124 | | Property | Type | Required | 125 | | ----------------- | ---------- | -------- | 126 | | `name` | `string` | Yes | 127 | | `hasResults` | `boolean` | No | 128 | | `id` | `string` | No | 129 | | `query` | `string` | No | 130 | | `onFocus` | `Function` | No | 131 | | `onQueryChange` | `Function` | Yes | 132 | | `open` | `boolean` | No | 133 | | `placeholder` | `string` | Yes | 134 | | `results` | `any` | No | 135 | | `setInputElement` | `Function` | No | 136 | 137 | ```jsx 138 | import { SuggestSearch } from 'sanity-web-react-components' 139 | 140 | const vdom = {}} placeholder="Search docs" /> 141 | ``` 142 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019, Sanity.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to 7 | deal in the Software without restriction, including without limitation the 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | sell copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | * The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sanity-web-react-components 2 | 3 | Shared React components for web apps on Sanity.io. 4 | 5 | ```sh 6 | npm install sanity-web-react-components 7 | ``` 8 | 9 | [![npm version](https://img.shields.io/npm/v/sanity-web-react-components.svg?style=flat-square)](https://www.npmjs.com/package/sanity-web-react-components) 10 | 11 | > NOTE: this project is at a very early stage. API and scope will change. 12 | 13 | ## Development 14 | 15 | ```sh 16 | git clone git@github.com:sanity-io/sanity-web-react-components.git 17 | cd sanity-web-react-components 18 | npm install 19 | npm run dev 20 | # Open http://localhost:6006 21 | ``` 22 | 23 | Build JS and CSS to `dist/`: 24 | 25 | ```sh 26 | npm run build 27 | ``` 28 | 29 | ## Documentation 30 | 31 | See [API documentation](API.md). 32 | 33 | ## License 34 | 35 | [MIT](LICENSE) © [Sanity.io](https://www.sanity.io) 36 | -------------------------------------------------------------------------------- /dev/start.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar') 2 | const express = require('express') 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | const webpackDevMiddleware = require('webpack-dev-middleware') 6 | const webpackHotMiddleware = require('webpack-hot-middleware') 7 | const webpackConfig = require('./webpack.config') 8 | 9 | const sourcePath = path.resolve(__dirname, '../src') 10 | 11 | // HOOKS 12 | require('module-alias/register') 13 | require('css-modules-require-hook')({ 14 | generateScopedName: '[name]__[local]___[hash:base64:5]', 15 | }) 16 | require('ts-node').register({ 17 | transpileOnly: true, 18 | }) 19 | 20 | const app = express() 21 | 22 | app.use(express.static(path.resolve(__dirname, '../node_modules/sanity-web-styles/dist'))) 23 | 24 | // WEBPACK 25 | const webpackCompiler = webpack(webpackConfig) 26 | app.use( 27 | webpackDevMiddleware(webpackCompiler, { 28 | hot: true, 29 | noInfo: true, 30 | publicPath: webpackConfig.output.publicPath, 31 | }), 32 | ) 33 | app.use(webpackHotMiddleware(webpackCompiler)) 34 | 35 | app.use((req, res, next) => { 36 | require('../src/@dev/server').create()(req, res, next) 37 | }) 38 | 39 | app.listen(8080, err => { 40 | if (err) panic(err) 41 | else console.log(`Listening at http://localhost:8080`) 42 | }) 43 | 44 | function panic(err) { 45 | console.error(err) 46 | process.exit(1) 47 | } 48 | 49 | // Setup server-side watcher 50 | const watcher = chokidar.watch(sourcePath) 51 | watcher.on('ready', () => { 52 | watcher.on('all', () => { 53 | console.log('Clearing require cache...') 54 | Object.keys(require.cache).forEach(key => { 55 | delete require.cache[key] 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /dev/webpack.config.js: -------------------------------------------------------------------------------- 1 | const genericNames = require('generic-names') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | 5 | const contextPath = path.resolve(__dirname, '../') 6 | 7 | const getLocalIdent = genericNames('[name]__[local]___[hash:base64:5]', { 8 | context: contextPath, 9 | }) 10 | 11 | module.exports = { 12 | context: contextPath, 13 | mode: 'development', 14 | devtool: 'eval-source-map', 15 | entry: { 16 | main: ['webpack-hot-middleware/client?reload=true', './src/@dev/client.tsx'], 17 | }, 18 | output: { 19 | publicPath: '/', 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.tsx?$/, 25 | use: 'ts-loader', 26 | exclude: /node_modules/, 27 | }, 28 | { 29 | test: /\.module\.css$/, 30 | use: [ 31 | 'style-loader', 32 | { 33 | loader: 'css-loader', 34 | options: { 35 | modules: { 36 | localIdentName: '[name]__[local]___[hash:base64:5]', 37 | getLocalIdent: (context, _, localName) => { 38 | return getLocalIdent(localName, context._module.resource) 39 | }, 40 | }, 41 | importLoaders: 1, 42 | }, 43 | }, 44 | 'postcss-loader', 45 | ], 46 | }, 47 | { 48 | // Match "*.css" that is not "*.module.css" 49 | test: /^(?![.*]\.module)([^.]+)\.css$/, 50 | use: ['style-loader', 'css-loader', 'postcss-loader'], 51 | }, 52 | ], 53 | }, 54 | resolve: { 55 | alias: { 56 | '@dev': path.resolve(contextPath, './src/@dev'), 57 | }, 58 | extensions: ['.tsx', '.ts', '.js'], 59 | }, 60 | plugins: [new webpack.HotModuleReplacementPlugin()], 61 | } 62 | -------------------------------------------------------------------------------- /examples/next-js/.gitignore: -------------------------------------------------------------------------------- 1 | /.next 2 | /node_modules 3 | -------------------------------------------------------------------------------- /examples/next-js/next.config.js: -------------------------------------------------------------------------------- 1 | const withCSS = require('@zeit/next-css') 2 | const withTranspileModules = require('next-transpile-modules') 3 | 4 | module.exports = withCSS( 5 | withTranspileModules({ 6 | cssModules: true, 7 | transpileModules: ['sanity-web-react-components', 'sanity-web-styles'], 8 | webpack(config, options) { 9 | return { 10 | ...config, 11 | module: { 12 | ...config.module, 13 | rules: config.module.rules.map(rule => { 14 | if (String(rule.test) === '/\\.css$/') { 15 | return { 16 | oneOf: [ 17 | { ...rule, test: /\.module\.css$/ }, 18 | { 19 | ...rule, 20 | test: /\.css$/, 21 | use: rule.use.map(conf => { 22 | if (typeof conf === 'object' && conf.loader === 'css-loader') { 23 | return { 24 | ...conf, 25 | options: { ...conf.options, modules: false }, 26 | } 27 | } 28 | return conf 29 | }), 30 | }, 31 | ], 32 | } 33 | } 34 | return rule 35 | }), 36 | }, 37 | } 38 | }, 39 | }), 40 | ) 41 | -------------------------------------------------------------------------------- /examples/next-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "example-next-js", 4 | "scripts": { 5 | "dev": "next", 6 | "build": "next build", 7 | "start": "next start" 8 | }, 9 | "dependencies": { 10 | "@zeit/next-css": "^1.0.1", 11 | "highlight.js": "^9.14.2", 12 | "next": "^8.1.0", 13 | "next-transpile-modules": "^2.3.1", 14 | "react": "^16.8.1", 15 | "react-dom": "^16.8.1", 16 | "react-lowlight": "^2.0.0", 17 | "sanity-web-react-components": "^1.0.0-alpha.5", 18 | "sanity-web-styles": "^1.0.0-alpha.5" 19 | }, 20 | "devDependencies": { 21 | "postcss-import": "^12.0.1", 22 | "postcss-preset-env": "^6.5.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/next-js/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // NOTE: Import base styles before view components 4 | import './layout.css' 5 | 6 | // NOTE: For an unknown reason, Next.js requires the full path of the root module 7 | import { SiteFooter, SiteHeader } from 'sanity-web-react-components/lib/es' 8 | 9 | import styles from './index.module.css' 10 | 11 | function Home() { 12 | return ( 13 | <> 14 | 15 |
Content
16 | 17 | 18 | ) 19 | } 20 | 21 | export default Home 22 | -------------------------------------------------------------------------------- /examples/next-js/pages/index.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | min-height: calc(100vh - 200px); 3 | padding: var(--padding-medium); 4 | box-sizing: border-box; 5 | max-width: var(--width-large); 6 | margin: 0 auto; 7 | 8 | @media (--media-min-small) { 9 | padding: var(--padding-large); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/next-js/pages/layout.css: -------------------------------------------------------------------------------- 1 | @import 'sanity-web-styles'; 2 | -------------------------------------------------------------------------------- /examples/next-js/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | 'postcss-preset-env': { 5 | importFrom: [ 6 | 'node_modules/sanity-web-styles/src/custom-media.css', 7 | 'node_modules/sanity-web-styles/src/custom-properties.css', 8 | ], 9 | stage: 3, 10 | features: { 11 | 'color-mod-function': { unresolved: 'warn' }, 12 | 'nesting-rules': true, 13 | 'custom-media-queries': { 14 | preserve: false, 15 | }, 16 | 'custom-properties': { 17 | preserve: false, 18 | }, 19 | }, 20 | }, 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /examples/webpack/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | -------------------------------------------------------------------------------- /examples/webpack/.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "modules": true, 3 | "plugins": { 4 | "postcss-import": {}, 5 | "postcss-preset-env": { 6 | importFrom: [ 7 | "node_modules/sanity-web-styles/src/custom-media.css", 8 | "node_modules/sanity-web-styles/src/custom-properties.css" 9 | ], 10 | stage: 3, 11 | "features": { 12 | "color-mod-function": { "unresolved": "warn" }, 13 | "custom-media-queries": { 14 | "preserve": false 15 | }, 16 | "custom-properties": { 17 | "preserve": false 18 | }, 19 | "nesting-rules": true 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "example-webpack", 4 | "scripts": { 5 | "dev": "webpack-dev-server --hot", 6 | "build": "rimraf dist && webpack", 7 | "start": "npm run dev" 8 | }, 9 | "dependencies": { 10 | "highlight.js": "^9.14.2", 11 | "react": "^16.8.1", 12 | "react-dom": "^16.8.1", 13 | "react-lowlight": "^2.0.0", 14 | "sanity-web-react-components": "^1.0.0-alpha.5", 15 | "sanity-web-styles": "^1.0.0-alpha.5" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.2.2", 19 | "@babel/preset-env": "^7.3.1", 20 | "@babel/preset-react": "^7.0.0", 21 | "babel-loader": "^8.0.5", 22 | "css-loader": "^2.1.0", 23 | "html-webpack-plugin": "^3.2.0", 24 | "postcss-loader": "^3.0.0", 25 | "rimraf": "^2.6.3", 26 | "style-loader": "^0.23.1", 27 | "webpack": "^4.29.3", 28 | "webpack-cli": "^3.2.3", 29 | "webpack-dev-server": "^3.1.14" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/webpack/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= htmlWebpackPlugin.options.title %> 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/webpack/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { SiteFooter, SiteHeader } from 'sanity-web-react-components' 4 | 5 | import './layout.css' 6 | import styles from './index.module.css' 7 | 8 | function App() { 9 | return ( 10 | <> 11 | 12 |
Content
13 | 14 | 15 | ) 16 | } 17 | 18 | ReactDOM.render(, document.getElementById('root')) 19 | -------------------------------------------------------------------------------- /examples/webpack/src/index.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | min-height: calc(100vh - 200px); 3 | padding: var(--padding-medium); 4 | box-sizing: border-box; 5 | max-width: var(--width-large); 6 | margin: 0 auto; 7 | 8 | @media (--media-min-small) { 9 | padding: var(--padding-large); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/webpack/src/layout.css: -------------------------------------------------------------------------------- 1 | @import 'sanity-web-styles'; 2 | -------------------------------------------------------------------------------- /examples/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin') 2 | const path = require('path') 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: { 7 | main: './src/index.js', 8 | }, 9 | output: { 10 | path: path.resolve(__dirname, 'dist'), 11 | filename: '[name].js', 12 | publicPath: '/', 13 | }, 14 | devServer: { 15 | contentBase: path.resolve(__dirname, 'dist'), 16 | publicPath: '/', 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.m?js$/, 22 | exclude: /node_modules/, 23 | use: { 24 | loader: 'babel-loader', 25 | options: { 26 | presets: ['@babel/preset-env', '@babel/preset-react'], 27 | }, 28 | }, 29 | }, 30 | { 31 | test: /\.css$/, 32 | oneOf: [ 33 | { 34 | test: /\.module\.css$/, 35 | use: [ 36 | 'style-loader', 37 | { 38 | loader: 'css-loader', 39 | options: { modules: true, importLoaders: 1 }, 40 | }, 41 | 'postcss-loader', 42 | ], 43 | }, 44 | { 45 | test: /\.css$/, 46 | use: [ 47 | 'style-loader', 48 | { 49 | loader: 'css-loader', 50 | options: { importLoaders: 1 }, 51 | }, 52 | 'postcss-loader', 53 | ], 54 | }, 55 | ], 56 | }, 57 | ], 58 | }, 59 | plugins: [ 60 | new HtmlWebpackPlugin({ 61 | title: 'example-webpack', 62 | filename: path.resolve(__dirname, 'dist/index.html'), 63 | template: 'src/index.html', 64 | }), 65 | ], 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sanity-web-react-components", 3 | "version": "1.0.0-alpha.24", 4 | "description": "Shared React components for web apps on Sanity.io.", 5 | "author": "Sanity.io ", 6 | "license": "MIT", 7 | "repository": "https://github.com/sanity-io/sanity-web-react-components", 8 | "homepage": "https://github.com/sanity-io/sanity-web-react-components#readme", 9 | "bugs": { 10 | "url": "https://github.com/sanity-io/sanity-web-react-components/issues" 11 | }, 12 | "keywords": [ 13 | "sanity.io", 14 | "web", 15 | "react", 16 | "components", 17 | "css modules" 18 | ], 19 | "main": "lib/cjs/index.js", 20 | "module": "lib/es/index.js", 21 | "scripts": { 22 | "build": "run-s clean tsc-cjs tsc-es postcss-cjs postcss-es", 23 | "clean": "rimraf lib", 24 | "dev": "run-p watch-*", 25 | "postcss-cjs": "mkdirp lib/cjs && postcss src/**/*.css --base src --dir lib/cjs/", 26 | "postcss-es": "mkdirp lib/es && postcss src/**/*.css --base src --dir lib/es/", 27 | "prepublishOnly": "npm run build", 28 | "test": "echo \"Error: no test specified\" && exit 1", 29 | "tsc-cjs": "tsc -p ts/tsconfig.cjs.json", 30 | "tsc-es": "tsc -p ts/tsconfig.es.json", 31 | "watch-dev-server": "node dev/start.js", 32 | "watch-postcss-cjs": "mkdirp lib/cjs && postcss -w src/**/*.css --base src --dir lib/cjs/", 33 | "watch-postcss-es": "mkdirp lib/es && postcss -w src/**/*.css --base src --dir lib/es/", 34 | "watch-tsc-cjs": "tsc -w -p ts/tsconfig.cjs.json", 35 | "watch-tsc-es": "tsc -w -p ts/tsconfig.es.json" 36 | }, 37 | "devDependencies": { 38 | "@types/qs": "^6.5.3", 39 | "@types/react": "^16.8.20", 40 | "@types/react-dom": "^16.8.4", 41 | "babel-loader": "^8.0.6", 42 | "css-loader": "^3.0.0", 43 | "css-modules-require-hook": "^4.2.3", 44 | "express": "^4.17.1", 45 | "highlight.js": "^9.15.8", 46 | "mkdirp": "^0.5.1", 47 | "module-alias": "^2.2.0", 48 | "npm-run-all": "^4.1.5", 49 | "postcss-cli": "^6.1.2", 50 | "postcss-import": "^12.0.1", 51 | "postcss-loader": "^3.0.0", 52 | "postcss-preset-env": "^6.6.0", 53 | "qs": "^6.7.0", 54 | "react": "^16.8.6", 55 | "react-dom": "^16.8.6", 56 | "react-icons": "^3.7.0", 57 | "react-lowlight": "^2.0.0", 58 | "react-props-stream": "^1.0.0", 59 | "rimraf": "^2.6.3", 60 | "rxjs": "^6.5.2", 61 | "sanity-web-styles": "^1.0.0-alpha.13", 62 | "style-loader": "^0.23.1", 63 | "ts-loader": "^6.0.2", 64 | "ts-node": "^8.2.0", 65 | "typescript": "^3.5.2", 66 | "webpack": "^4.34.0", 67 | "webpack-dev-middleware": "^3.7.0", 68 | "webpack-hot-middleware": "^2.25.0" 69 | }, 70 | "peerDependencies": { 71 | "highlight.js": "^9.15.8", 72 | "react": "^16.8.6", 73 | "react-icons": "^3.7.0", 74 | "react-lowlight": "^2.0.0", 75 | "sanity-web-styles": "^1.0.0-alpha.13" 76 | }, 77 | "_moduleAliases": { 78 | "@dev": "src/@dev" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/@dev/App.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | height: 100%; 3 | display: grid; 4 | grid-template-columns: 1fr 4fr; 5 | } 6 | 7 | .nav { 8 | background: #eee; 9 | min-width: 200px; 10 | padding: 1em; 11 | } 12 | 13 | .stage { 14 | padding: 1em; 15 | } 16 | -------------------------------------------------------------------------------- /src/@dev/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { withPropsStream } from 'react-props-stream' 3 | import { merge } from 'rxjs' 4 | import matchRoute from './matchRoute' 5 | import { location$ } from './streams/history' 6 | 7 | // Import screens 8 | import { RootScreen } from './screens/root' 9 | import { StageScreen } from './screens/stage' 10 | 11 | interface Props { 12 | path?: string 13 | query?: { 14 | [key: string]: string 15 | } 16 | } 17 | 18 | function App(props: Props) { 19 | const route = matchRoute(props.path) 20 | switch (route.name) { 21 | case 'root': 22 | return 23 | case 'stage': 24 | return 25 | default: 26 | return
Not found: {props.path}
27 | } 28 | } 29 | 30 | const StreamingApp = withPropsStream( 31 | (props$: any): any => merge(props$, location$), 32 | (props: Props) => , 33 | ) 34 | 35 | export default StreamingApp 36 | -------------------------------------------------------------------------------- /src/@dev/base.css: -------------------------------------------------------------------------------- 1 | @import url('//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/default.min.css'); 2 | @import url('//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.14.2/styles/xcode.min.css'); 3 | @import 'sanity-web-styles'; 4 | 5 | body { 6 | background: var(--color-very-light-gray); 7 | } 8 | 9 | html, 10 | body, 11 | #root { 12 | height: 100%; 13 | } 14 | -------------------------------------------------------------------------------- /src/@dev/client.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactDOM from 'react-dom' 3 | 4 | // Import base CSS before components 5 | import './base.css' 6 | 7 | import App from './App' 8 | 9 | function decode(str: string) { 10 | return JSON.parse(decodeURIComponent(str)) 11 | } 12 | 13 | const rootElm = document.getElementById('root') 14 | if (!rootElm) throw new Error('Missing #root element') 15 | 16 | const rawProps = rootElm.getAttribute('data-props') 17 | if (!rawProps) throw new Error('Missing data-props') 18 | 19 | const props = decode(rawProps) 20 | 21 | ReactDOM.hydrate(, rootElm) 22 | -------------------------------------------------------------------------------- /src/@dev/components/CenteredWrapper.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | height: 100%; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | padding: 1em; 7 | box-sizing: border-box; 8 | } 9 | -------------------------------------------------------------------------------- /src/@dev/components/CenteredWrapper.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import * as styles from './CenteredWrapper.module.css' 4 | 5 | function CenteredWrapper(props: any) { 6 | return ( 7 |
8 |
{props.children}
9 |
10 | ) 11 | } 12 | 13 | export default CenteredWrapper 14 | -------------------------------------------------------------------------------- /src/@dev/lib/schema/index.ts: -------------------------------------------------------------------------------- 1 | import { BooleanType, FieldType, NumberType, StringType, TextType } from './types' 2 | 3 | export function getDefaultData(fields: FieldType[]) { 4 | return fields.reduce( 5 | (acc, field) => { 6 | switch (field.type) { 7 | case 'boolean': { 8 | const booleanType = field as BooleanType 9 | if (booleanType.default !== undefined) { 10 | acc[field.name] = booleanType.default 11 | } 12 | return acc 13 | } 14 | case 'number': { 15 | const numberType = field as NumberType 16 | if (numberType.default !== undefined) { 17 | acc[field.name] = numberType.default 18 | } 19 | return acc 20 | } 21 | case 'string': { 22 | const stringType = field as StringType 23 | if (stringType.default !== undefined) { 24 | acc[field.name] = stringType.default 25 | } 26 | return acc 27 | } 28 | case 'text': { 29 | const textType = field as TextType 30 | if (textType.default !== undefined) { 31 | acc[field.name] = textType.default 32 | } 33 | return acc 34 | } 35 | default: 36 | return acc 37 | } 38 | }, 39 | {} as any, 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/@dev/lib/schema/types.ts: -------------------------------------------------------------------------------- 1 | export interface BooleanType { 2 | type: 'boolean' 3 | name: string 4 | label: string 5 | default: boolean 6 | } 7 | 8 | interface StringOption { 9 | label: string 10 | value: string 11 | } 12 | 13 | export interface StringType { 14 | type: 'string' 15 | name: string 16 | label: string 17 | options?: StringOption[] 18 | default?: string 19 | } 20 | 21 | interface NumberOption { 22 | label: string 23 | value: number 24 | } 25 | 26 | export interface NumberType { 27 | type: 'number' 28 | name: string 29 | label: string 30 | options?: NumberOption[] 31 | default?: number 32 | } 33 | 34 | export interface TextType { 35 | type: 'text' 36 | name: string 37 | label: string 38 | default?: string 39 | } 40 | 41 | export interface UnknownType { 42 | type: string 43 | label: string 44 | name: string 45 | } 46 | 47 | export type FieldType = BooleanType | StringType | TextType | UnknownType 48 | -------------------------------------------------------------------------------- /src/@dev/matchRoute.ts: -------------------------------------------------------------------------------- 1 | function matchRoute(path?: string) { 2 | switch (true) { 3 | case path === '/': 4 | return { name: 'root', params: {} } 5 | case path && path.startsWith('/story/'): { 6 | const parts = path ? path.split('/') : [] 7 | return { 8 | name: 'root', 9 | params: { 10 | storyName: parts[2], 11 | patternIndex: parseInt(parts[3]) || 0, 12 | }, 13 | } 14 | } 15 | case path && path.startsWith('/stage/'): { 16 | const parts = path ? path.split('/') : [] 17 | return { 18 | name: 'stage', 19 | params: { 20 | storyName: parts[2], 21 | patternIndex: parseInt(parts[3]) || 0, 22 | }, 23 | } 24 | } 25 | default: 26 | return { name: null } 27 | } 28 | } 29 | 30 | export default matchRoute 31 | -------------------------------------------------------------------------------- /src/@dev/screens/root/IFrame.tsx: -------------------------------------------------------------------------------- 1 | import * as qs from 'qs' 2 | import * as React from 'react' 3 | 4 | interface Props { 5 | path: string 6 | query?: { [key: string]: string } 7 | } 8 | 9 | class IFrame extends React.Component { 10 | iframe?: HTMLIFrameElement 11 | 12 | setIframe = (iframe: HTMLIFrameElement) => { 13 | this.iframe = iframe 14 | } 15 | 16 | shouldComponentUpdate(nextProps: Props) { 17 | if (this.iframe && this.iframe.contentWindow) { 18 | this.iframe.contentWindow.postMessage( 19 | JSON.stringify({ 20 | type: 'history/replaceState', 21 | path: nextProps.path, 22 | query: nextProps.query, 23 | }), 24 | '*', 25 | ) 26 | return false 27 | } 28 | return true 29 | } 30 | 31 | render() { 32 | const search = qs.stringify(this.props.query || {}) 33 | const src = this.props.path + (search ? `?${search}` : '') 34 | return