├── .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 |
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 |
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 |
10 |
--------------------------------------------------------------------------------
/assets/viola_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 | [](https://circleci.com/gh/violapub/workflows/viola/tree/master)
6 | []()
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