├── .env ├── assets ├── icons.psd ├── favicon.ico └── viola_logo.svg ├── public ├── favicon.ico ├── manifest.json ├── index.prod.html └── index.dev.html ├── .gitmodules ├── .storybook ├── addons.js ├── StorybookContainer.js ├── config.js └── webpack.config.js ├── src ├── misc │ ├── noun.js │ ├── error.js │ ├── router.js │ ├── __tests__ │ │ └── project.spec.js │ └── project.js ├── ui │ ├── Logo │ │ ├── Logo.css │ │ ├── index.js │ │ └── viola_logo.svg │ ├── ProgressBar │ │ ├── index.js │ │ ├── ProgressBar-story.js │ │ └── ProgressBar.css │ ├── Modal │ │ ├── Modal-story.js │ │ ├── index.js │ │ └── Modal.css │ ├── ContextMenu │ │ ├── ContextMenu-story.js │ │ ├── index.js │ │ └── ContextMenu.css │ ├── Button │ │ ├── Button-story.js │ │ ├── index.js │ │ └── Button.css │ ├── StatusIndicator │ │ ├── StatusIndicator.css │ │ └── index.js │ └── Header │ │ ├── Header-story.js │ │ ├── Header.css │ │ └── index.js ├── index.css ├── components │ ├── SideNav │ │ ├── SideNav.css │ │ └── index.js │ ├── ToolBar │ │ ├── ToolBar-story.js │ │ ├── ToolBar.css │ │ └── index.js │ └── App │ │ ├── App.css │ │ └── index.js ├── locale │ ├── ja.json │ └── en.json ├── index.js ├── service-worker-import.js ├── withIntl.js └── registerServiceWorker.js ├── .editorconfig ├── .env.production ├── .env.development ├── config ├── jest │ ├── fileTransform.js │ └── cssTransform.js ├── polyfills.js ├── paths.js ├── env.js ├── webpackDevServer.config.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── .gitignore ├── scripts ├── test.js ├── start.js └── build.js ├── README.md ├── docker-compose.yml ├── package.json ├── .circleci └── config.yml └── LICENSE /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_VIOLA_REPOSITORY=https://github.com/violapub/viola 2 | -------------------------------------------------------------------------------- /assets/icons.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/violapub/viola/HEAD/assets/icons.psd -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/violapub/viola/HEAD/assets/favicon.ico -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/violapub/viola/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "brackets"] 2 | path = brackets 3 | url = https://github.com/violapub/brackets.git 4 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | import '@storybook/addon-knobs/register'; 4 | -------------------------------------------------------------------------------- /src/misc/noun.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl'; 2 | 3 | export default defineMessages({ 4 | error: 'Error', 5 | login: 'Login', 6 | logout: 'Logout', 7 | signUp: 'Sign up', 8 | projectList: 'Project list', 9 | }); 10 | -------------------------------------------------------------------------------- /src/ui/Logo/Logo.css: -------------------------------------------------------------------------------- 1 | .ViolaLogo { 2 | display: inline-block; 3 | height: 100%; 4 | } 5 | .ViolaLogo.white { 6 | fill: #dcdcdc; 7 | } 8 | .ViolaLogo.black { 9 | fill: #222222; 10 | } 11 | 12 | .ViolaLogo_svg { 13 | height: 100%; 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:400,600'); 2 | 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | overflow: hidden; 9 | } 10 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_BRAMBLE_HOST_URL=http://localhost:8000/dist/index.html 2 | REACT_APP_PRINT_PAGE_HOST_URL=http://localhost:8000/dist/thirdparty/viola-savepdf/index.html 3 | REACT_APP_VFS_ROOT_URL=http://localhost:8000/dist/vfs/ 4 | REACT_APP_VIOLA_HOMEPAGE=http://localhost:5000 5 | REACT_APP_CELLO_HOST_URL=http://localhost:7770 6 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_BRAMBLE_HOST_URL=http://localhost:3000/bramble/index.html 2 | REACT_APP_PRINT_PAGE_HOST_URL=http://localhost:3000/bramble/thirdparty/viola-savepdf/index.html 3 | REACT_APP_VFS_ROOT_URL=http://localhost:3000/bramble/vfs/ 4 | REACT_APP_VIOLA_HOMEPAGE=http://localhost:3000 5 | REACT_APP_CELLO_HOST_URL=http://localhost:7770 6 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | yarn.lock 24 | package-lock.json 25 | 26 | -------------------------------------------------------------------------------- /src/ui/ProgressBar/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | import './ProgressBar.css'; 4 | 5 | const ProgressBar = ({ 6 | className, value, max, 7 | ...other, 8 | }) => { 9 | const classes = classnames(className, 'ProgressBar'); 10 | 11 | return ( 12 |
13 | 14 |
15 | ); 16 | }; 17 | 18 | export { 19 | ProgressBar, 20 | }; 21 | -------------------------------------------------------------------------------- /.storybook/StorybookContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './../src/index.css'; 3 | 4 | export default class StorybookContainer extends React.Component { 5 | render() { 6 | const { story } = this.props; 7 | 8 | return ( 9 |
17 | {story()} 18 |
19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ui/Logo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | import ViolaLogoSVG from './viola_logo.svg'; 4 | import './Logo.css'; 5 | 6 | const ViolaLogo = ({ 7 | className, black, white, 8 | ...other 9 | }) => { 10 | const classes = classnames(className, 'ViolaLogo', { 11 | black: black, 12 | white: white, 13 | }); 14 | 15 | return ( 16 |
17 | 18 |
19 | ); 20 | }; 21 | 22 | export { 23 | ViolaLogo, 24 | }; 25 | -------------------------------------------------------------------------------- /src/ui/ProgressBar/ProgressBar-story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | import { action } from '@storybook/addon-actions'; 5 | import { withKnobs, number } from '@storybook/addon-knobs/react'; 6 | 7 | import { ProgressBar } from './index.js'; 8 | 9 | storiesOf('ProgressBar', module) 10 | .addDecorator(withKnobs) 11 | .add('ProgressBar', () => ( 12 |
13 | 17 |
18 | )); 19 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { configure, addDecorator } from '@storybook/react'; 3 | import StorybookContainer from './StorybookContainer'; 4 | 5 | addDecorator(story => ); 6 | 7 | function loadStories() { 8 | const components = require.context('../src/components', true, /\-story\.js$/); 9 | components.keys().forEach(filename => components(filename)); 10 | 11 | const ui = require.context('../src/ui', true, /\-story\.js$/); 12 | ui.keys().forEach(filename => ui(filename)); 13 | } 14 | 15 | configure(loadStories, module); 16 | -------------------------------------------------------------------------------- /src/ui/Modal/Modal-story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | import { withKnobs, boolean } from '@storybook/addon-knobs/react'; 5 | 6 | import { Modal, ModalHeader, ModalBody, ModalFooter } from './index'; 7 | 8 | storiesOf('Modal', module) 9 | .addDecorator(withKnobs) 10 | .add('Modal', () => ( 11 |
12 | 13 | 14 |

Header

15 |
16 | Body 17 | Footer 18 |
19 |
20 | )); 21 | -------------------------------------------------------------------------------- /src/misc/error.js: -------------------------------------------------------------------------------- 1 | export class NotLoggedInError extends Error { 2 | constructor(msg) { 3 | super(msg); 4 | this.name = 'NotLoggedInError'; 5 | } 6 | } 7 | 8 | export class ProjectNotFoundError extends Error { 9 | constructor(msg) { 10 | super(msg); 11 | this.name = 'ProjectNotFoundError'; 12 | } 13 | } 14 | 15 | export class TemplateNotFoundError extends Error { 16 | constructor(msg) { 17 | super(msg); 18 | this.name = 'TemplateNotFoundError'; 19 | } 20 | } 21 | 22 | export class CelloServerConnectionError extends Error { 23 | constructor(msg) { 24 | super(msg); 25 | this.name = 'CelloServerConnectionError'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ui/ProgressBar/ProgressBar.css: -------------------------------------------------------------------------------- 1 | .ProgressBar { 2 | display: flex; 3 | width: 100%; 4 | } 5 | 6 | .ProgressBar_progress { 7 | width: 100%; 8 | } 9 | 10 | progress.ProgressBar_progress { 11 | -webkit-appearance: none; 12 | border: none; 13 | height: 6px; 14 | background-color: rgba(0, 0, 0, .1); 15 | border-radius: 3px; 16 | } 17 | progress.ProgressBar_progress::-webkit-progress-bar { 18 | background-color: transparent; 19 | } 20 | progress.ProgressBar_progress::-webkit-progress-value { 21 | background: #222222; 22 | border-radius: 3px; 23 | } 24 | progress.ProgressBar_progress::-moz-progress-bar { 25 | background: #222222; 26 | border-radius: 3px; 27 | } 28 | -------------------------------------------------------------------------------- /src/components/SideNav/SideNav.css: -------------------------------------------------------------------------------- 1 | .SideNav { 2 | position: fixed; 3 | bottom: 0; 4 | left: 0; 5 | height: 34px; 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: stretch; 9 | } 10 | .fullscreen .SideNav, 11 | .sidebar-hidden .SideNav, 12 | .modal-open .SideNav { 13 | display: none; 14 | } 15 | 16 | .SideNav-item { 17 | display: inline-flex; 18 | align-items: center; 19 | padding: 0 0.8em 8px; 20 | color: rgba(255,255,255,0.4); 21 | font-size: 13px; 22 | } 23 | a.SideNav-item, 24 | a.SideNav-item:visited { 25 | color: #dcdcdc; 26 | text-decoration: none; 27 | } 28 | a.SideNav-item:hover { 29 | color: #ffffff; 30 | text-decoration: underline; 31 | } 32 | -------------------------------------------------------------------------------- /src/locale/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "components.App.faliedToLoad": "プロジェクトの読み込みに失敗しました。 ({errorName})", 3 | "components.App.loginInstruction": "ログインはお済みですか?", 4 | "components.ToolBar.download": "プロジェクトをダウンロード", 5 | "components.ToolBar.printPage": "印刷ページを開く", 6 | "misc.error": "エラー", 7 | "misc.login": "ログイン", 8 | "misc.logout": "ログアウト", 9 | "misc.projectList": "プロジェクト一覧", 10 | "misc.signUp": "アカウント登録", 11 | "ui.Header.connectionFailed": "Violaサーバーとの接続に失敗したため、編集内容は同期されません。接続状況を確認して、後ほど再読込してください。", 12 | "ui.Header.loginInstruction": "アカウントをお持ちの場合", 13 | "ui.Header.notConnected": "サーバー未接続", 14 | "ui.Header.notLoggedIn": "未ログイン", 15 | "ui.Header.signUpInstruction": "アカウント登録で、プロジェクトのバックアップを作成できます。" 16 | } 17 | -------------------------------------------------------------------------------- /src/ui/ContextMenu/ContextMenu-story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | import { action } from '@storybook/addon-actions'; 5 | 6 | import { ContextMenu, ContextMenuItem, ContextMenuDivider } from './index.js'; 7 | 8 | storiesOf('ContextMenu', module) 9 | .add('ContextMenu & ContextMenuItem', () => ( 10 |
11 | 12 | Clickable 13 | Non-clickable 14 | 15 | HogeHoge 16 | 17 |
18 | )); 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/App'; 4 | import route from './misc/router'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | import withIntl from './withIntl'; 7 | import './index.css'; 8 | 9 | const dataDOM = document.getElementById('viola-data'); 10 | let data = {}; 11 | if (dataDOM && dataDOM.dataset['json']) { 12 | try { 13 | data = JSON.parse(decodeURIComponent(dataDOM.dataset['json'])); 14 | } catch (e) { 15 | // do nothing 16 | } 17 | } 18 | const routeAction = route(); 19 | const IntlApp = withIntl(App); 20 | 21 | ReactDOM.render( 22 | , 23 | document.getElementById('root') 24 | ); 25 | registerServiceWorker(); 26 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | const argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /src/service-worker-import.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: 0 */ 2 | /* eslint no-restricted-globals: 0 */ 3 | /* eslint no-use-before-define: 0 */ 4 | 5 | // WebWorker script imported from servicw-worker.js 6 | 7 | var totalFiles = precacheConfig.length; 8 | var processedFiles = 0; 9 | //Original cleanResponse function from service-worker 10 | var originalCleanResponseFunc = cleanResponse; 11 | 12 | var cleanResponse = function(originalResponse) { 13 | processedFiles++; 14 | self.clients.matchAll({ 15 | includeUncontrolled: true, 16 | type: 'window' 17 | }).then(function(clients) { 18 | clients.forEach(function(client) { 19 | client.postMessage({ 20 | progress: processedFiles / totalFiles 21 | }); 22 | }); 23 | }); 24 | //finally call the original cleanResponseFunc 25 | return originalCleanResponseFunc(originalResponse); 26 | }; 27 | -------------------------------------------------------------------------------- /src/ui/Button/Button-story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | import { action } from '@storybook/addon-actions'; 5 | 6 | import { IconButton, SelectiveRippleButton, SelectiveRippleButtonItem } from './index.js'; 7 | 8 | storiesOf('Button', module) 9 | .add('IconButton', () => ( 10 |
11 | 12 | 13 | 14 |
15 | )) 16 | .add('SelectiveRippleButton', () => ( 17 | 26 | )); 27 | -------------------------------------------------------------------------------- /src/locale/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "components.App.faliedToLoad": "Failed to load the project. ({errorName})", 3 | "components.App.loginInstruction": "Have you logged in yet?", 4 | "components.ToolBar.download": "Download this project", 5 | "components.ToolBar.printPage": "Open the print page", 6 | "misc.error": "Error", 7 | "misc.login": "Login", 8 | "misc.logout": "Logout", 9 | "misc.projectList": "Project list", 10 | "misc.signUp": "Sign up", 11 | "ui.Header.connectionFailed": "Editing content won't be synchronized because a connection to Viola server has failed. Please check your connection status and reload it later.", 12 | "ui.Header.loginInstruction": "If you have the account...", 13 | "ui.Header.notConnected": "Not connected", 14 | "ui.Header.notLoggedIn": "Not logged in", 15 | "ui.Header.signUpInstruction": "You can make backups of your project by signing up." 16 | } 17 | -------------------------------------------------------------------------------- /src/ui/StatusIndicator/StatusIndicator.css: -------------------------------------------------------------------------------- 1 | .StatusIndicator { 2 | display: block; 3 | width: 70px; 4 | } 5 | 6 | .StatusIndicator_connection-info { 7 | display: flex; 8 | align-items: center; 9 | } 10 | 11 | .StatusIndicator_connection-dot { 12 | margin-right: 6px; 13 | width: 10px; 14 | height: 10px; 15 | border-radius: 50%; 16 | background-color: transparent; 17 | box-shadow: none; 18 | } 19 | 20 | .StatusIndicator_connection-info.connected .StatusIndicator_connection-dot { 21 | background-color: #00f109; 22 | box-shadow: 0 0 2px 1px rgba(0, 241, 9, 0.3); 23 | } 24 | 25 | .StatusIndicator_connection-info.disconnected .StatusIndicator_connection-dot { 26 | background-color: #fd3939; 27 | box-shadow: 0 0 2px 1px rgba(253, 57, 57, 0.3); 28 | } 29 | 30 | .StatusIndicator_connection-message { 31 | flex-grow: 1; 32 | font-size: 13px; 33 | line-height: 10px; 34 | color: #aaa; 35 | } 36 | -------------------------------------------------------------------------------- /src/ui/Logo/viola_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /assets/viola_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | 18 | // In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet. 19 | // We don't polyfill it in the browser--this is user's responsibility. 20 | if (process.env.NODE_ENV === 'test') { 21 | require('raf').polyfill(global); 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Viola 4 | 5 | [![CircleCI](https://img.shields.io/circleci/project/github/violapub/viola/master.svg?style=flat-square)](https://circleci.com/gh/violapub/workflows/viola/tree/master) 6 | [![license](https://img.shields.io/github/license/violapub/viola.svg?style=flat-square)]() 7 | 8 | Online editor for printing and publishing. 9 | 10 | The editor UI of this project consists [Bramble](https://github.com/mozilla/brackets) by Mozilla that is forked from [Brackets](http://brackets.io/). 11 | 12 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 13 | 14 | ## Development 15 | 16 | 1. Cloned this repository 17 | 1. Run `git submodule update --init --recursive` 18 | 1. Run `npm install` 19 | 1. Run `npm run bramble:build` 20 | 1. Run `npm start` 21 | 22 | Browsers are automatically updated as you change codes. 23 | 24 | ## Dependency 25 | 26 | - [Bramble](https://github.com/mozilla/brackets) 27 | -------------------------------------------------------------------------------- /src/components/SideNav/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | import './SideNav.css'; 4 | 5 | const { REACT_APP_VERSION, REACT_APP_VIOLA_REPOSITORY } = process.env; 6 | 7 | export default class SideNav extends React.Component { 8 | 9 | props: { 10 | bramble: any, 11 | }; 12 | 13 | initBramble = (bramble) => { 14 | bramble.on('layout', this.updateLayout); 15 | } 16 | 17 | updateLayout = (data) => { 18 | if (this.sideNavElement) { 19 | this.sideNavElement.style.width = `${data.sidebarWidth}px`; 20 | } 21 | }; 22 | 23 | componentWillMount() { 24 | this.initBramble(this.props.bramble); 25 | } 26 | 27 | render() { 28 | return ( 29 | 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/withIntl.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { addLocaleData, IntlProvider } from 'react-intl'; 3 | import enLocaleData from 'react-intl/locale-data/en'; 4 | import jaLocaleData from 'react-intl/locale-data/ja'; 5 | import enMessage from './locale/en.json'; 6 | import jaMessage from './locale/ja.json'; 7 | 8 | addLocaleData(enLocaleData); 9 | addLocaleData(jaLocaleData); 10 | const messageDefs = { 11 | en: enMessage, 12 | ja: jaMessage, 13 | }; 14 | 15 | export function getLocaleKeyByApplicableObj(obj) { 16 | const localeTag = window.navigator.language; 17 | return localeTag in obj 18 | ? localeTag 19 | : localeTag.split('-')[0] in obj 20 | ? localeTag.split('-')[0] 21 | : null; 22 | } 23 | 24 | export default ComposedComponent => { 25 | return class WithIntl extends React.Component { 26 | render() { 27 | const localeTag = getLocaleKeyByApplicableObj(messageDefs) || 'en'; 28 | return ( 29 | 30 | 31 | 32 | ); 33 | } 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/ToolBar/ToolBar-story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | import { action } from '@storybook/addon-actions'; 5 | 6 | import ToolBar from './index'; 7 | 8 | const brambleStub = { 9 | getFilename: () => null, 10 | getPreviewURL: () => null, 11 | on: () => {}, 12 | hideSidebar: () => {}, 13 | showSidebar: () => {}, 14 | showUploadFilesDialog: () => {}, 15 | createNewFile: () => {}, 16 | addNewFolder: () => {}, 17 | useMobilePreview: () => {}, 18 | useDesktopPreview: () => {}, 19 | usePrintPreview: () => {}, 20 | enableFullscreenPreview: () => {}, 21 | disableFullscreenPreview: () => {}, 22 | export: () => {}, 23 | }; 24 | 25 | storiesOf('ToolBar', module) 26 | .add('ToolBar', () => { 27 | 28 | return ( 29 |
30 | 36 |
37 | ); 38 | }) 39 | -------------------------------------------------------------------------------- /src/misc/router.js: -------------------------------------------------------------------------------- 1 | import { getLocaleKeyByApplicableObj } from '../withIntl'; 2 | 3 | // prettier-ignore 4 | const demoProjectMetaFiles = { 5 | ja: 'https://raw.githubusercontent.com/violapub/templates/master/welcome-ja/meta.toml', 6 | }; 7 | const defaultProjectMeta = 8 | 'https://raw.githubusercontent.com/violapub/templates/master/welcome-en/meta.toml'; 9 | 10 | export default function route() { 11 | const { pathname } = window.location; 12 | 13 | let match; 14 | 15 | match = pathname.match(/^\/project\/([0-9a-zA-Z]+)\/?$/); 16 | if (match) { 17 | return { 18 | role: 'project', 19 | projectId: match[1], 20 | }; 21 | } 22 | 23 | match = pathname.match(/^\/template\/(https?:\/\/.+)$/); 24 | if (match) { 25 | return { 26 | role: 'template-unofficial', 27 | url: match[1], 28 | }; 29 | } 30 | 31 | match = pathname.match(/^\/template\/([0-9a-zA-Z_-]+)\/?$/); 32 | if (match) { 33 | return { 34 | role: 'template-official', 35 | templateName: match[1], 36 | }; 37 | } 38 | 39 | // fallback to root page 40 | else if (pathname !== '/') { 41 | window.history.replaceState('', null, '/'); 42 | } 43 | const localeTag = getLocaleKeyByApplicableObj(demoProjectMetaFiles); 44 | const projectMeta = localeTag? demoProjectMetaFiles[localeTag] : defaultProjectMeta; 45 | return { 46 | role: 'demo', 47 | projectMeta, 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /src/ui/ContextMenu/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | import './ContextMenu.css'; 4 | 5 | const ContextMenu = ({ 6 | className, children, onOverlayClick, alignRight, black, 7 | ...other, 8 | }) => { 9 | const classes = classnames(className, 'ContextMenu_container', { 10 | 'align-right': alignRight, 11 | black, 12 | }); 13 | return ( 14 |
15 |
    { 17 | e.stopPropagation(); 18 | }} 19 | > 20 | {children} 21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | const ContextMenuItem = ({ 28 | className, children, onClick, href, 29 | ...other, 30 | }) => { 31 | const classes = classnames(className, 'ContextMenuItem_container', { 32 | 'clickable': onClick || href, 33 | }); 34 | 35 | const content = ( 36 |
  • 37 | {children} 38 |
  • 39 | ); 40 | return href 41 | ? {content} 42 | : content; 43 | }; 44 | 45 | const ContextMenuDivider = ({ 46 | ...other, 47 | }) => { 48 | return ( 49 |
  • 50 |
    51 |
  • 52 | ) 53 | }; 54 | 55 | export { 56 | ContextMenu, 57 | ContextMenuItem, 58 | ContextMenuDivider, 59 | }; 60 | -------------------------------------------------------------------------------- /src/ui/Header/Header-story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | import { action } from '@storybook/addon-actions'; 5 | import { withKnobs, boolean, text, select } from '@storybook/addon-knobs/react'; 6 | 7 | import { Header } from './index'; 8 | 9 | storiesOf('Header', module) 10 | .addDecorator(withKnobs) 11 | .add('Header', () => { 12 | const menuStatus = select('menuStatus', { 13 | LOADING: 'LOADING', 14 | LOADED: 'LOADED', 15 | DISCONNECTED: 'DISCONNECTED', 16 | }, 'LOADED'); 17 | const loggedIn = boolean('logged in', true); 18 | const projectName = text('projectName', 'Untitled Project'); 19 | const homepageURL = text('homepageURL', 'http://example.com/homepage'); 20 | const loginURL = text('loginURL', 'http://example.com/login'); 21 | const signupURL = text('signupURL', 'http://example.com/signup'); 22 | const projectListURL = text('projectListURL', 'http://example.com/projectList'); 23 | const logoutURL = text('signupURL', 'http://example.com/logout'); 24 | const user = { 25 | id: text('user.id', 'testtesttest'), 26 | name: text('user.name', 'foo'), 27 | email: text('user.email', 'foo@example.com'), 28 | }; 29 | return ( 30 |
    31 |
    34 |
    35 | ) 36 | }) 37 | -------------------------------------------------------------------------------- /src/ui/Modal/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CSSTransition } from 'react-transition-group'; 3 | import classnames from 'classnames'; 4 | import './Modal.css'; 5 | 6 | const ModalHeader = ({ className, children, ...other }) => ( 7 |
    8 | {children} 9 |
    10 | ); 11 | 12 | const ModalBody = ({ className, children, ...other }) => ( 13 |
    14 | {children} 15 |
    16 | ); 17 | 18 | const ModalFooter = ({ className, children, ...other }) => ( 19 |
    20 | {children} 21 |
    22 | ); 23 | 24 | class Modal extends React.PureComponent { 25 | render() { 26 | const { show, className, children, ...other } = this.props; 27 | 28 | return ( 29 | 32 | {state => ( 33 |
    34 |
    35 |
    36 |
    37 | {children} 38 |
    39 |
    40 |
    41 | )} 42 |
    43 | ); 44 | } 45 | } 46 | 47 | export { 48 | Modal, 49 | ModalHeader, 50 | ModalBody, 51 | ModalFooter, 52 | }; 53 | -------------------------------------------------------------------------------- /src/ui/StatusIndicator/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | import './StatusIndicator.css'; 4 | 5 | class StatusIndicator extends React.PureComponent { 6 | state = { 7 | connectionStatus: '', 8 | } 9 | 10 | handleOnline = () => { 11 | this.setState({ connectionStatus: 'connected' }); 12 | }; 13 | 14 | handleOffline = () => { 15 | this.setState({ connectionStatus: 'disconnected' }); 16 | }; 17 | 18 | componentDidMount() { 19 | window.addEventListener('online', this.handleOnline); 20 | window.addEventListener('offline', this.handleOffline); 21 | this.setState({ 22 | connectionStatus: window.navigator.onLine? 'connected' : 'disconnected', 23 | }); 24 | } 25 | 26 | componentWillUnmount() { 27 | window.removeEventListener('online', this.handleOnline); 28 | window.removeEventListener('offline', this.handleOffline); 29 | } 30 | 31 | render() { 32 | const { connectionStatus } = this.state; 33 | const connectionMessage = connectionStatus === 'connected' ? 'Online' 34 | : connectionStatus === 'disconnected' ? 'Offline' 35 | : ''; 36 | 37 | return ( 38 |
    39 |
    40 | 41 |
    {connectionMessage}
    42 |
    43 |
    44 | ); 45 | } 46 | } 47 | 48 | export { 49 | StatusIndicator, 50 | }; 51 | -------------------------------------------------------------------------------- /src/ui/Header/Header.css: -------------------------------------------------------------------------------- 1 | .Header { 2 | box-sizing: border-box; 3 | position: relative; 4 | background-color: #222; 5 | height: 40px; 6 | padding: 0; 7 | color: #dcdcdc; 8 | } 9 | 10 | .Header-title { 11 | height: 100%; 12 | margin: 0; 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | } 17 | 18 | .Header-title-logo { 19 | margin: 0 12px; 20 | height: 24px; 21 | } 22 | 23 | .Header-title h1 { 24 | display: inline; 25 | margin: 0; 26 | line-height: 40px; 27 | font-size: 16px; 28 | font-weight: 400; 29 | } 30 | 31 | .Header-lr { 32 | position: absolute; 33 | top: 0; 34 | bottom: 0; 35 | left: 0; 36 | right: 0; 37 | display: flex; 38 | justify-content: space-between; 39 | align-items: stretch; 40 | pointer-events: none; 41 | user-select: none; 42 | } 43 | 44 | .Header-left, 45 | .Header-right { 46 | display: flex; 47 | align-items: stretch; 48 | position: relative; 49 | pointer-events: all; 50 | } 51 | 52 | .Header-menu_knob { 53 | padding: 0 20px; 54 | display: flex; 55 | align-items: center; 56 | font-size: 13px; 57 | cursor: pointer; 58 | } 59 | 60 | .Header-user_avatar { 61 | width: 24px; 62 | height: 24px; 63 | overflow: hidden; 64 | border: 1px solid #aaa; 65 | border-radius: 50%; 66 | background-color: #fff; 67 | } 68 | .Header-user_avatar object { 69 | width: 100%; 70 | height: 100%; 71 | } 72 | 73 | .Header-right .ContextMenu_container { 74 | top: 34px; 75 | } 76 | 77 | .Header-context_label { 78 | margin-bottom: 6px; 79 | font-size: 13px; 80 | line-height: 20px; 81 | color: #aaa; 82 | max-width: 210px; 83 | } 84 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # 2 | # A docker-compose sample file for hosting over TLS. 3 | # 4 | # To enable TLS over hosting, 5 | # 6 | # 1. Update /etc/hosts 7 | # ``` 8 | # 127.0.0.1 writing.violadev.localdomain 9 | # 127.0.0.1 editor.violadev.localdomain 10 | # ```` 11 | # 12 | # 2. Start docker-proxy container 13 | # ``` 14 | # $ docker run -d -p 80:80 -p 443:443 \ 15 | # -v /path/to/certs:/etc/nginx/certs:ro \ 16 | # -v /var/run/docker.sock:/tmp/docker.sock:ro \ 17 | # jwilder/nginx-proxy 18 | # ``` 19 | # 20 | # 3. Run docker-compose up 21 | # 22 | 23 | version: '3' 24 | services: 25 | app: 26 | container_name: viola_app 27 | image: node:10-slim 28 | network_mode: bridge 29 | expose: 30 | - 3000 31 | volumes: 32 | - .:/viola 33 | working_dir: /viola 34 | environment: 35 | VIRTUAL_HOST: writing.violadev.localdomain 36 | VIRTUAL_PORT: 3000 37 | REACT_APP_BRAMBLE_HOST_URL: https://editor.violadev.localdomain/index.html 38 | REACT_APP_PRINT_PAGE_HOST_URL: https://editor.violadev.localdomain/thirdparty/viola-savepdf/index.html 39 | REACT_APP_VFS_ROOT_URL: https://editor.violadev.localdomain/vfs/ 40 | REACT_APP_VIOLA_HOMEPAGE: https://violadev.localdomain 41 | REACT_APP_CELLO_HOST_URL: https://violadev.localdomain 42 | command: yarn start 43 | 44 | bramble: 45 | container_name: viola_bramble 46 | image: nginx:latest 47 | network_mode: bridge 48 | expose: 49 | - 80 50 | volumes: 51 | - ./brackets/dist:/usr/share/nginx/html:ro 52 | environment: 53 | VIRTUAL_HOST: editor.violadev.localdomain 54 | VIRTUAL_PORT: 80 55 | -------------------------------------------------------------------------------- /src/components/ToolBar/ToolBar.css: -------------------------------------------------------------------------------- 1 | .ToolBar, 2 | .ToolBar-filetree_pane, 3 | .ToolBar-editor_pane, 4 | .ToolBar-preview_pane { 5 | height: 50px; 6 | } 7 | 8 | .ToolBar { 9 | display: flex; 10 | flex-direction: row; 11 | } 12 | .ToolBar > * { 13 | box-sizing: border-box; 14 | } 15 | 16 | .ToolBar-filetree_pane { 17 | display: flex; 18 | flex-direction: row; 19 | flex-grow: 1; 20 | align-items: center; 21 | justify-content: space-between; 22 | padding-left: 4px; 23 | padding-right: 14px; 24 | background-color: #222; 25 | } 26 | 27 | .ToolBar-filetree_left, 28 | .ToolBar-filetree_right { 29 | display: inline-flex; 30 | flex-direction: row; 31 | align-items: center; 32 | } 33 | 34 | .ToolBar-editor_pane { 35 | display: flex; 36 | flex-direction: row; 37 | flex-grow: 2; 38 | align-items: center; 39 | justify-content: space-between; 40 | padding-left: 20px; 41 | padding-right: 14px; 42 | background-color: #3F3F3F; 43 | color: #dcdcdc; 44 | } 45 | .sidebar-hidden .ToolBar-editor_pane { 46 | padding-left: 4px; 47 | } 48 | 49 | .ToolBar-editor_left, 50 | .ToolBar-editor_right { 51 | position: relative; 52 | display: inline-flex; 53 | flex-direction: row; 54 | align-items: center; 55 | } 56 | 57 | .ToolBar-editor_right .ContextMenu_container { 58 | top: 38px; 59 | left: 0px; 60 | } 61 | 62 | .ToolBar-preview_pane { 63 | display: flex; 64 | flex-direction: row; 65 | flex-grow: 3; 66 | align-items: center; 67 | justify-content: space-between; 68 | padding-left: 20px; 69 | padding-right: 20px; 70 | background-color: #EAEAEA; 71 | margin-left: 1px; /* fit to bramble second pane */ 72 | } 73 | 74 | .ToolBar-preview_left, 75 | .ToolBar-preview_right { 76 | position: relative; 77 | display: inline-flex; 78 | flex-direction: row; 79 | align-items: center; 80 | } 81 | 82 | .ToolBar-disable_fullscreen_button { 83 | position: fixed; 84 | top: 20px; 85 | right: 20px; 86 | z-index: 10000; 87 | } 88 | -------------------------------------------------------------------------------- /src/ui/Modal/Modal.css: -------------------------------------------------------------------------------- 1 | .Modal { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | } 6 | 7 | .Modal_underlay { 8 | position: fixed; 9 | top: 0; 10 | bottom: 0; 11 | left: 0; 12 | right: 0; 13 | background-color: rgba(0,0,0,0.5); 14 | } 15 | 16 | .Modal_container { 17 | position: fixed; 18 | top: 0; 19 | bottom: 0; 20 | left: 0; 21 | right: 0; 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | 26 | perspective: 500px; 27 | transform-style: preserve-3d; 28 | } 29 | 30 | .Modal_content { 31 | min-width: 200px; 32 | overflow: hidden; 33 | 34 | display: flex; 35 | flex-direction: column; 36 | 37 | background: white; 38 | border-radius: 2px; 39 | box-shadow: 0 3px 7px rgba(0,0,0,0.3); 40 | 41 | perspective: 500px; 42 | transform-style: preserve-3d; 43 | animation: showModal 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28) 0s 1; 44 | user-select: none; 45 | } 46 | 47 | .ModalHeader { 48 | padding: 20px 20px 0; 49 | } 50 | .ModalHeader h1, 51 | .ModalHeader h2 { 52 | margin: 0; 53 | } 54 | 55 | .ModalBody { 56 | padding: 20px; 57 | } 58 | 59 | .ModalFooter { 60 | background: rgba(0,0,0,0.1); 61 | padding: 15px 20px; 62 | } 63 | 64 | /* Annotated with React CSSTransition */ 65 | 66 | .modalanim-enter { 67 | transition: opacity 0.1s ease-out; 68 | opacity: 0.01; 69 | } 70 | /* .modalanim-enter .Modal_content { 71 | transition: transform 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28); 72 | transform: rotateX(-50deg); 73 | } */ 74 | .modalanim-enter-active { 75 | opacity: 1; 76 | } 77 | /* .modalanim-enter-active .Modal_content { 78 | transform: rotateX(0); 79 | } */ 80 | 81 | .modalanim-exit { 82 | transition: opacity 0.1s ease-out; 83 | opacity: 1; 84 | } 85 | .modalanim-exit-active { 86 | opacity: 0.01; 87 | } 88 | 89 | @keyframes showModal { 90 | 0% { 91 | transform: rotateX(-50deg); 92 | } 93 | 94 | 100% { 95 | transform: rotateX(0); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/components/App/App.css: -------------------------------------------------------------------------------- 1 | .App.modal-open:after { 2 | content: ''; 3 | display: block; 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | height: 90px; 9 | background-color: rgba(0, 0, 0, 0.7); 10 | animation: showModalBackdrop 0.1s ease-out; 11 | } 12 | @keyframes showModalBackdrop { 13 | 0% { 14 | opacity: 0; 15 | } 16 | 100% { 17 | opacity: .8; 18 | } 19 | } 20 | 21 | .App-brambleroot { 22 | position: absolute; 23 | top: 90px; 24 | bottom: 0; 25 | left: 0; 26 | right: 0; 27 | } 28 | 29 | .App.fullscreen .App-brambleroot { 30 | top: 0; 31 | } 32 | 33 | .App-loading_container { 34 | pointer-events: none; 35 | position: fixed; 36 | top: 40px; 37 | bottom: 0; 38 | left: 0; 39 | right: 0; 40 | margin-right: -24px;/* Align to top-center nav icon */ 41 | display: flex; 42 | align-items: center; 43 | justify-content: center; 44 | flex-direction: column; 45 | background-color: rgba(212, 208, 202, 0.8); 46 | opacity: 1; 47 | transition: opacity 0.4s linear; 48 | } 49 | 50 | .App-loading_container.hidden { 51 | opacity: 0; 52 | } 53 | 54 | .App-loading_container_lr { 55 | display: flex; 56 | align-items: center; 57 | justify-content: center; 58 | flex-direction: row; 59 | } 60 | 61 | .App-loading_logo { 62 | height: 48px; 63 | margin-right: 10px; 64 | animation: loading .9s cubic-bezier(0.1, 0.03, 0.1, 1.17) infinite; 65 | } 66 | 67 | .App-loading_message { 68 | white-space: nowrap; 69 | overflow: visible; 70 | font-size: 18px; 71 | } 72 | 73 | .App-loading_progress_bar { 74 | margin: 20px 0; 75 | width: 114px; 76 | } 77 | 78 | @keyframes spinner { 79 | 0% { transform: rotate(0deg); } 80 | 50% { transform: rotate(360deg); } 81 | 100% { transform: rotate(0deg); } 82 | } 83 | 84 | @keyframes loading { 85 | 0% { transform: translateX(0); } 86 | 94% { transform: translateX(-6px); } 87 | 100% { transform: translateX(0); } 88 | } 89 | 90 | .App-error_instruction { 91 | margin-top: 18px; 92 | } 93 | -------------------------------------------------------------------------------- /src/ui/Button/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Icon} from 'react-fa'; 3 | import classnames from 'classnames'; 4 | import './Button.css'; 5 | 6 | const IconButton = ({ 7 | className, name, black, opaque, 8 | ...other 9 | }) => { 10 | const buttonClasses = classnames(className, 'IconButton_button', { 11 | black: black, 12 | opaque: opaque, 13 | }); 14 | 15 | return ( 16 | 19 | ); 20 | }; 21 | 22 | class SelectiveRippleButton extends React.Component { 23 | 24 | props: { 25 | data: Array<{ 26 | name: string, 27 | onClick: ?() => void, 28 | }>, 29 | initialActiveIndex: ?number, 30 | onChange: ?(number) => void, 31 | }; 32 | 33 | constructor(props) { 34 | super(props); 35 | this.state = { 36 | active: props.initialActiveIndex || 0, 37 | } 38 | } 39 | 40 | onItemClick = (idx) => () => { 41 | const { data, onChange } = this.props; 42 | 43 | this.setState({ 44 | active: idx, 45 | }); 46 | if (data[idx].onClick) { 47 | data[idx].onClick(); 48 | } 49 | if (onChange) { 50 | onChange(idx); 51 | } 52 | }; 53 | 54 | render() { 55 | const { className, data, initialActive, onChange, ...other } = this.props; 56 | const { active } = this.state; 57 | 58 | return ( 59 |
    60 | 63 | {data.map((d, idx) => 64 |
    67 | 68 |
    69 | )} 70 |
    71 | ); 72 | } 73 | } 74 | 75 | export { 76 | IconButton, 77 | SelectiveRippleButton, 78 | }; 79 | -------------------------------------------------------------------------------- /src/ui/ContextMenu/ContextMenu.css: -------------------------------------------------------------------------------- 1 | .ContextMenu_container { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | } 6 | .ContextMenu_container.align-right { 7 | left: auto; 8 | right: 0; 9 | } 10 | 11 | .ContextMenu_ul { 12 | position: absolute; 13 | z-index: 1000; 14 | top: 0; 15 | left: 0; 16 | margin: 0; 17 | padding: 6px 0; 18 | list-style: none; 19 | width: max-content; 20 | min-width: 160px; 21 | background-color: white; 22 | color: #666666; 23 | border-radius: 2px; 24 | box-shadow: 0 3px 9px rgba(0,0,0,0.24); 25 | animation: showDropdown 90ms cubic-bezier(0, .97, .2, .99) 0ms 1; 26 | transform-origin: left top; 27 | user-select: none; 28 | } 29 | .ContextMenu_container.align-right .ContextMenu_ul { 30 | left: auto; 31 | right: 0; 32 | transform-origin: right top; 33 | } 34 | .ContextMenu_container.black .ContextMenu_ul { 35 | background-color: #222; 36 | color: #dcdcdc; 37 | } 38 | 39 | .ContextMenu_ul > a { 40 | color: inherit; 41 | text-decoration: none; 42 | } 43 | 44 | .ContextMenu_underlay { 45 | position: fixed; 46 | z-index: 999; 47 | top: 0; 48 | bottom: 0; 49 | left: 0; 50 | right: 0; 51 | } 52 | 53 | @keyframes showDropdown { 54 | 0% { 55 | opacity: .5; 56 | transform: translate3d(0, 0, 0) scale(.5) 57 | } 58 | 59 | 100% { 60 | opacity: 1; 61 | transform: translate3d(0, 0, 0) scale(1) 62 | } 63 | } 64 | 65 | .ContextMenuItem_container { 66 | line-height: 18px; 67 | padding: 12px 20px 12px 40px; 68 | } 69 | .ContextMenuItem_container.clickable { 70 | cursor: pointer; 71 | } 72 | .ContextMenuItem_container.clickable:hover { 73 | background-color: rgba(0, 0, 0, 0.08); 74 | /* color: #ffffff; */ 75 | } 76 | .ContextMenu_container.black .ContextMenuItem_container.ContextMenuItem_container.clickable:hover { 77 | background-color: rgba(0, 0, 0, 0.3); 78 | } 79 | 80 | .ContextMenuDivider_divider { 81 | border: none; 82 | height: 1px; 83 | background-color: rgba(0, 0, 0, 0.08); 84 | margin: 5px 0; 85 | } 86 | .ContextMenu_container.black .ContextMenuDivider_divider { 87 | background-color: rgba(255, 255, 255, 0.15); 88 | } 89 | -------------------------------------------------------------------------------- /src/ui/Button/Button.css: -------------------------------------------------------------------------------- 1 | .IconButton_button { 2 | position: relative; 3 | width: 32px; 4 | height: 32px; 5 | border-radius: 50%; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | margin: 0 3px; 10 | font-size: 16px; 11 | border: none; 12 | background-color: transparent; 13 | cursor: pointer; 14 | transition: background-color 0.1s ease-out; 15 | background-color: transarent; 16 | color: #dcdcdc; 17 | } 18 | .IconButton_button:hover, 19 | .IconButton_button:active { 20 | background-color: rgba(0, 0, 0, 0.3); 21 | } 22 | .IconButton_button:focus { 23 | outline: none; 24 | } 25 | .IconButton_button::before { 26 | content: ''; 27 | position: absolute; 28 | top: -4px; 29 | bottom: -4px; 30 | left: -4px; 31 | right: -4px; 32 | } 33 | 34 | .IconButton_button.black { 35 | color: #848484; 36 | } 37 | .IconButton_button.black:hover, 38 | .IconButton_button.black:active { 39 | background-color: rgba(160, 160, 160, 0.3); 40 | } 41 | 42 | .IconButton_button.opaque { 43 | color: #ffffff; 44 | background-color: rgba(0, 0, 0, 0.45); 45 | } 46 | .IconButton_button.opaque:hover, 47 | .IconButton_button.opaque:active { 48 | background-color: rgba(0, 0, 0, 0.9); 49 | } 50 | 51 | .SelectiveRippleButton { 52 | position: relative; 53 | height: 38px; 54 | background: #cccccc; 55 | border-radius: 21px; 56 | } 57 | 58 | .SelectiveRippleButton_paper { 59 | content: ''; 60 | position: absolute; 61 | top: 2px; 62 | left: 2px; 63 | width: 34px; 64 | height: 34px; 65 | border-radius: 50%; 66 | background-color: #fafafa; 67 | box-shadow: 0px 1px 4px 2px rgba(148, 148, 148, 0.5); 68 | transition: transform .2s ease; 69 | pointer-events: none; 70 | } 71 | 72 | .SelectiveRippleButton_item { 73 | position: relative; 74 | display: inline-flex; 75 | width: 38px; 76 | height: 38px; 77 | align-items: center; 78 | justify-content: center; 79 | color: #7c7c7c; 80 | cursor: pointer; 81 | } 82 | 83 | .SelectiveRippleButton_item.active, 84 | .SelectiveRippleButton_item:hover, 85 | .SelectiveRippleButton_item:active { 86 | color: #444444; 87 | } 88 | 89 | .SelectiveRippleButton_item:focus { 90 | outline: none; 91 | } 92 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | // you can use this file to add your custom webpack plugins, loaders and anything you like. 2 | // This is just the basic way to add additional webpack configurations. 3 | // For more information refer the docs: https://storybook.js.org/configurations/custom-webpack-config 4 | 5 | // IMPORTANT 6 | // When you add this file, we won't add the default configurations which is similar 7 | // to "React Create App". This only has babel loader to load JavaScript. 8 | 9 | const autoprefixer = require('autoprefixer'); 10 | const paths = require('./../config/paths'); 11 | 12 | module.exports = { 13 | plugins: [ 14 | // your custom plugins 15 | ], 16 | module: { 17 | rules: [ 18 | { 19 | exclude: [ 20 | /\.html$/, 21 | /\.ejs$/, 22 | /\.(js|jsx)$/, 23 | /\.css$/, 24 | /\.json$/, 25 | new RegExp(`${paths.appSrc}.+\\.svg`), 26 | ], 27 | loader: require.resolve('file-loader'), 28 | }, 29 | { 30 | test: /\.css$/, 31 | use: [ 32 | require.resolve('style-loader'), 33 | { 34 | loader: require.resolve('css-loader'), 35 | options: { 36 | importLoaders: 1, 37 | }, 38 | }, 39 | { 40 | loader: require.resolve('postcss-loader'), 41 | options: { 42 | ident: 'postcss', // https://webpack.js.org/guides/migrating/#complex-options 43 | plugins: () => [ 44 | require('postcss-flexbugs-fixes'), 45 | autoprefixer({ 46 | browsers: [ 47 | '>1%', 48 | 'last 4 versions', 49 | 'Firefox ESR', 50 | 'not ie < 9', // React doesn't support IE8 anyway 51 | ], 52 | flexbox: 'no-2009', 53 | }), 54 | ], 55 | }, 56 | }, 57 | ], 58 | }, 59 | { 60 | test: new RegExp(`${paths.appSrc}.+\\.svg`), 61 | use: [ 62 | require.resolve('react-svg-loader'), 63 | ], 64 | }, 65 | ], 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /public/index.prod.html: -------------------------------------------------------------------------------- 1 | <% 2 | // obtain bramble.xxxxxx.js path 3 | const brambleJsFile = webpack.assets.map(assets => assets['name']).filter(name => name.includes('bramble'))[0]; 4 | const violaDataStr = encodeURIComponent(JSON.stringify({ 5 | projectMeta: 'https://raw.githubusercontent.com/violapub/templates/master/welcome-ja/meta.toml', 6 | })); 7 | %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 30 | Viola 31 | 32 | 33 | 34 | 35 | 38 |
    39 | 40 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /public/index.dev.html: -------------------------------------------------------------------------------- 1 | <% 2 | const brambleRoot = process.env.REACT_APP_BRAMBLE_HOST_URL 3 | .match(/^(.+)\/.*$/)[1]; 4 | const violaDataStr = encodeURIComponent(JSON.stringify({ 5 | projectMeta: 'https://raw.githubusercontent.com/violapub/templates/master/welcome-ja/meta.toml', 6 | })); 7 | %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 30 | Viola 31 | 32 | 35 | 36 | 37 | 38 | 41 |
    42 | 43 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | export default function register() { 12 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 13 | window.addEventListener('load', () => { 14 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 15 | navigator.serviceWorker 16 | .register(swUrl) 17 | .then(registration => { 18 | registration.onupdatefound = () => { 19 | const installingWorker = registration.installing; 20 | installingWorker.onstatechange = () => { 21 | if (installingWorker.state === 'installed') { 22 | if (navigator.serviceWorker.controller) { 23 | // At this point, the old content will have been purged and 24 | // the fresh content will have been added to the cache. 25 | // It's the perfect time to display a "New content is 26 | // available; please refresh." message in your web app. 27 | console.log('New content is available; please refresh.'); 28 | } else { 29 | // At this point, everything has been precached. 30 | // It's the perfect time to display a 31 | // "Content is cached for offline use." message. 32 | console.log('Content is cached for offline use.'); 33 | } 34 | } 35 | }; 36 | }; 37 | }) 38 | .catch(error => { 39 | console.error('Error during service worker registration:', error); 40 | }); 41 | }); 42 | } 43 | } 44 | 45 | export function unregister() { 46 | if ('serviceWorker' in navigator) { 47 | navigator.serviceWorker.ready.then(registration => { 48 | registration.unregister(); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right