├── .babelrc ├── .gitignore ├── .npmignore ├── .storybook └── config.js ├── README.md ├── package.json ├── src ├── apps │ ├── Dialog.js │ ├── FileBrowser.js │ ├── Notepad.js │ └── WordPad.js ├── atoms │ ├── GreyBox.js │ ├── LightBorderBox.js │ ├── LightlyInsetBox.js │ ├── RidgedBox.js │ ├── RidgedButton.js │ └── Select.js ├── components │ ├── Desktop.js │ ├── IconArea.js │ ├── IconRegular.js │ ├── MenuOverlay.js │ ├── Shell.js │ ├── SystemTray.js │ ├── Taskbar.js │ ├── TaskbarItem.js │ ├── WindowLayer.js │ ├── WindowToolbar.js │ ├── startmenu │ │ ├── StartButton.js │ │ ├── StartMenu.js │ │ └── StartMenuItem.js │ ├── window │ │ ├── Window.js │ │ ├── WindowTitleBar.js │ │ └── WindowTitleBarTransition.js │ └── windowmenu │ │ ├── WindowMenu.js │ │ ├── WindowMenuButton.js │ │ ├── WindowMenuGroup.js │ │ └── WindowMenuItem.js ├── img │ ├── arrow-down.png │ ├── arrow-left.png │ ├── arrow-right.png │ ├── arrow-up.png │ ├── border-button-inset.png │ ├── border-button.png │ ├── border-inset.png │ ├── border-strong.png │ ├── border.png │ ├── close.png │ ├── dropdown.png │ ├── icon-default-24.png │ ├── icon-default.png │ ├── justify-center.png │ ├── justify-left.png │ ├── light-border-inset.png │ ├── light-border.png │ ├── logo-small.png │ ├── maximize.png │ ├── minimize.png │ ├── pointer.png │ ├── resize-handle.png │ ├── resize-se.png │ ├── ruler-marker-1.png │ ├── ruler-marker-2.png │ ├── ruler-marker-3.png │ ├── ruler.png │ ├── scrollbar-track.png │ ├── semi-busy.png │ ├── text-cursor.png │ ├── tick.png │ └── unmaximize.png ├── index.js └── util │ ├── focusButtonListener.js │ ├── getViewport.js │ ├── isObject.js │ └── underlinedLabel.js ├── stories ├── helpers │ └── WindowStateContainer.js ├── icon-folder.png └── index.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015", 5 | "stage-2" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | dist 4 | yarn-error.log 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react' 2 | 3 | function loadStories() { 4 | require('../stories/index.js') 5 | // You can require as many stories as you need. 6 | } 7 | 8 | configure(loadStories, module) 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reactows 95 2 | 3 | A set of React components for recreating everyone's favourite operating system as a webapp. 4 | 5 | [Demo app here](https://reactows-95-demo.herokuapp.com/) (not guaranteed to be up-to-date). 6 | 7 | To try it out using [Storybook](https://storybook.js.org/): 8 | 9 | ``` 10 | git clone https://github.com/Middlerun/reactows-95.git 11 | cd reactows-95 12 | yarn install 13 | yarn storybook 14 | ``` 15 | 16 | ## To do 17 | 18 | - More window types 19 | - State container for window management (currently being developed within the example app, but I'll probably move it into this library at some point) 20 | - Documentation of components and their props 21 | - Use TypeScript? 22 | 23 | ## License 24 | 25 | Copyright 2018 Eddie McLean 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 28 | 29 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactows-95", 3 | "version": "0.0.5", 4 | "description": "Everyone's favourite OS, now in React!", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "prepublish": "rm -rf ./dist && yarn webpack", 8 | "storybook": "start-storybook -p 9001 -c .storybook" 9 | }, 10 | "author": "Eddie McLean", 11 | "license": "MIT", 12 | "peerDependencies": { 13 | "react": "^16.0.0", 14 | "react-dom": "^16.0.0" 15 | }, 16 | "dependencies": { 17 | "prop-types": "^15.6.2", 18 | "react-overlays": "^0.8.3", 19 | "styled-components": "^3.3.2" 20 | }, 21 | "devDependencies": { 22 | "@storybook/react": "^3.4.6", 23 | "babel-core": "^6.26.3", 24 | "babel-loader": "^7.1.4", 25 | "babel-preset-es2015": "^6.24.1", 26 | "babel-preset-react": "^6.24.1", 27 | "babel-preset-stage-2": "^6.24.1", 28 | "file-loader": "^1.1.11", 29 | "react": "^16.4.0", 30 | "react-dom": "^16.4.0", 31 | "url-loader": "^1.0.1", 32 | "webpack": "^4.12.0", 33 | "webpack-cli": "^3.0.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/apps/Dialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from 'styled-components' 3 | 4 | import Window from '../components/window/Window' 5 | import RidgedButton from '../atoms/RidgedButton' 6 | import focusButtonListener from '../util/focusButtonListener' 7 | 8 | const MessageWrapper = styled.div` 9 | padding: 22px 10px 12px 10px; 10 | text-align: center; 11 | 12 | > * + * { 13 | margin-top: 25px; 14 | } 15 | ` 16 | 17 | class Dialog extends Component { 18 | componentDidMount() { 19 | this.keyListener = focusButtonListener(this, this.props.onRequestClose, { which: 13 }) 20 | } 21 | 22 | componentWillUnmount() { 23 | this.keyListener.remove() 24 | } 25 | 26 | render() { 27 | const { 28 | children, 29 | initialGeometry, 30 | onRequestClose, 31 | ...props 32 | } = this.props 33 | 34 | const windowInitialGeometry = { 35 | width: 296, 36 | height: 123, 37 | ...initialGeometry 38 | } 39 | 40 | return ( 41 | 47 | 48 |
49 | {children} 50 |
51 |
52 | 58 | OK 59 | 60 |
61 |
62 |
63 | ) 64 | } 65 | } 66 | 67 | export default Dialog 68 | -------------------------------------------------------------------------------- /src/apps/FileBrowser.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import Window from '../components/window/Window' 4 | import WindowMenuGroup from '../components/windowmenu/WindowMenuGroup' 5 | import RidgedBox from '../atoms/RidgedBox' 6 | import IconArea from '../components/IconArea' 7 | import LightlyInsetBox from '../atoms/LightlyInsetBox' 8 | 9 | const ContentRoot = RidgedBox.extend` 10 | flex: 1; 11 | display: flex; 12 | width: 100%; 13 | background-color: white; 14 | overflow: auto; 15 | ` 16 | 17 | const BottomContentArea = LightlyInsetBox.extend` 18 | height: 100%; 19 | padding: 0 3px; 20 | ` 21 | 22 | class FileBrowser extends Component { 23 | getMenus() { 24 | const { onRequestClose } = this.props 25 | return [ 26 | { label: 'File', underline: 0, items: [ 27 | { label: 'New', underline: 2, disabled: true }, 28 | 'divider', 29 | { label: 'Create Shortcut', underline: 7, disabled: true }, 30 | { label: 'Delete', underline: 0, disabled: true }, 31 | { label: 'Rename', underline: 4, disabled: true }, 32 | { label: 'Properties', underline: 1, disabled: true }, 33 | 'divider', 34 | { label: 'Close', underline: 0, onSelect: onRequestClose }, 35 | ] }, 36 | { label: 'Edit', underline: 0, items: [ 37 | { label: 'Undo', underline: 0, disabled: true }, 38 | 'divider', 39 | { label: 'Cut', underline: 2, disabled: true }, 40 | { label: 'Copy', underline: 0, disabled: true }, 41 | { label: 'Paste', underline: 0, disabled: true }, 42 | { label: 'Paste Shortcut', underline: 6, disabled: true }, 43 | 'divider', 44 | { label: 'Select All', underline: 7, disabled: true }, 45 | { label: 'Invert Selection', underline: 0, disabled: true }, 46 | ] }, 47 | { label: 'View', underline: 0, items: [ 48 | { label: 'Toolbar', underline: 0, disabled: true }, 49 | { label: 'Status Bar', underline: 7, disabled: true }, 50 | 'divider', 51 | { label: 'Large Icons', underline: 3, disabled: true }, 52 | { label: 'Small Icons', underline: 1, disabled: true }, 53 | { label: 'List', underline: 0, disabled: true }, 54 | { label: 'Details', underline: 0, disabled: true }, 55 | 'divider', 56 | { label: 'Arrange Icons', underline: 8, disabled: true }, 57 | { label: 'Line up Icons', underline: 3, disabled: true }, 58 | 'divider', 59 | { label: 'Refresh', underline: 0, disabled: true }, 60 | { label: 'Options...', underline: 0, disabled: true }, 61 | ] }, 62 | { label: 'Help', underline: 0, items: [ 63 | { label: 'Help Topics', underline: 0, disabled: true }, 64 | { label: 'About Reactows 95', underline: 0, disabled: true }, 65 | ] }, 66 | ] 67 | } 68 | 69 | render() { 70 | const { 71 | children, 72 | ...props 73 | } = this.props 74 | 75 | const bottomAreaContent = 76 | {(children || []).length} object(s) 77 | 78 | 79 | return ( 80 | 84 | 85 | 86 | 87 | {children} 88 | 89 | 90 | 91 | ) 92 | } 93 | } 94 | 95 | export default FileBrowser 96 | -------------------------------------------------------------------------------- /src/apps/Notepad.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { findDOMNode } from 'react-dom' 3 | import styled, { css } from 'styled-components' 4 | 5 | import Window from '../components/window/Window' 6 | import WindowMenuGroup from '../components/windowmenu/WindowMenuGroup' 7 | import RidgedBox from '../atoms/RidgedBox' 8 | import textCursor from '../img/text-cursor.png' 9 | import resizeHandleImage from '../img/resize-handle.png' 10 | 11 | const ContentRoot = RidgedBox.extend` 12 | flex: 1; 13 | display: flex; 14 | width: 100%; 15 | background-color: white; 16 | overflow: auto; 17 | ` 18 | 19 | const Content = styled.div` 20 | min-height: 100%; 21 | width: 100%; 22 | padding: 2px; 23 | cursor: url('${textCursor}') 3 9, auto; 24 | overflow-y: scroll; 25 | overflow-x: ${({wordWrap}) => wordWrap ? 'hidden' : 'scroll'}; 26 | user-select: text; 27 | 28 | > pre { 29 | margin: 0; 30 | font-family: Fixedsys, monospace; 31 | font-size: 12px; 32 | line-height: 15px; 33 | 34 | ${({wordWrap}) => wordWrap && css` 35 | max-width: 100%; 36 | white-space: pre-wrap; 37 | overflow-wrap: break-word; 38 | `} 39 | } 40 | ` 41 | 42 | const ResizeHandleContainer = styled.div` 43 | position: absolute; 44 | bottom: 2px; 45 | right: 2px; 46 | width: ${({size}) => size}px; 47 | height: ${({size}) => size}px; 48 | background-color: #c0c0c0; 49 | ${({showHandle}) => showHandle && css` 50 | background: #c0c0c0 url(${resizeHandleImage}) no-repeat; 51 | background-position: ${({size}) => size - 12}px ${({size}) => size - 12}px; 52 | `} 53 | ` 54 | 55 | class Notepad extends Component { 56 | constructor() { 57 | super() 58 | this.state = { 59 | wordWrap: true, 60 | scrollbarSize: null, 61 | } 62 | } 63 | 64 | setTitle() { 65 | const { onSetTitle } = this.props 66 | onSetTitle && onSetTitle(this.generateTitle()) 67 | } 68 | 69 | componentDidMount() { 70 | this.setTitle() 71 | this.updateScrollbarSizeIfNeeded() 72 | } 73 | 74 | componentDidUpdate(prevProps) { 75 | if (this.props.fileName !== prevProps.fileName) { 76 | this.setTitle() 77 | } 78 | this.updateScrollbarSizeIfNeeded() 79 | } 80 | 81 | updateScrollbarSizeIfNeeded() { 82 | if (!this.contentDiv || !this.contentPre) { 83 | return 84 | } 85 | const scrollbarSize = findDOMNode(this.contentDiv).getBoundingClientRect().width - 86 | findDOMNode(this.contentPre).getBoundingClientRect().width - 4 87 | this.setState(state => ( 88 | state.scrollbarSize !== scrollbarSize ? { scrollbarSize } : null 89 | )) 90 | } 91 | 92 | generateTitle() { 93 | return (this.props.fileName || 'Untitled') + ' - Notepad' 94 | } 95 | 96 | toggleWordWrap = () => { 97 | this.setState(state => ({ wordWrap: !state.wordWrap })) 98 | } 99 | 100 | getMenus() { 101 | const { onRequestClose } = this.props 102 | const { wordWrap } = this.state 103 | 104 | return [ 105 | { label: 'File', underline: 0, items: [ 106 | { label: 'New...', underline: 0, disabled: true }, 107 | { label: 'Open...', underline: 0, disabled: true }, 108 | { label: 'Save', underline: 0, disabled: true }, 109 | { label: 'Save As...', underline: 5, disabled: true }, 110 | 'divider', 111 | { label: 'Page Setup', underline: 6, disabled: true }, 112 | { label: 'Print...', underline: 0, disabled: true }, 113 | 'divider', 114 | { label: 'Exit', underline: 1, onSelect: onRequestClose }, 115 | ] }, 116 | { label: 'Edit', underline: 0, items: [ 117 | { label: 'Undo', underline: 0, disabled: true }, 118 | 'divider', 119 | { label: 'Cut', underline: 2, disabled: true }, 120 | { label: 'Copy', underline: 0, disabled: true }, 121 | { label: 'Paste', underline: 0, disabled: true }, 122 | { label: 'Delete', underline: 2, disabled: true }, 123 | 'divider', 124 | { label: 'Select All', underline: 7, disabled: true }, 125 | { label: 'Time/Date', underline: 5, disabled: true }, 126 | 'divider', 127 | { label: 'Word Wrap', underline: 0, checked: wordWrap, onSelect: this.toggleWordWrap }, 128 | ] }, 129 | { label: 'Search', underline: 0, items: [ 130 | { label: 'Find...', underline: 0, disabled: true }, 131 | { label: 'Find Next', underline: 5, disabled: true }, 132 | ] }, 133 | { label: 'Help', underline: 0, items: [ 134 | { label: 'Help Topics', underline: 0, disabled: true }, 135 | 'divider', 136 | { label: 'About Notepad', underline: 0, disabled: true }, 137 | ] }, 138 | ] 139 | } 140 | 141 | render() { 142 | const { 143 | children, 144 | title, 145 | fileName, 146 | onSetTitle, 147 | initialGeometry, 148 | ...props 149 | } = this.props 150 | 151 | const { 152 | maximized, 153 | resizable, 154 | } = this.props 155 | 156 | const { wordWrap, scrollbarSize } = this.state 157 | 158 | const windowInitialGeometry = { 159 | width: 600, 160 | height: 500, 161 | ...initialGeometry 162 | } 163 | 164 | console.log(resizable, maximized, this.props) 165 | 166 | // TODO: Remove bottom content area while keeping window resizable 167 | return ( 168 | 174 | 175 | 176 | 177 | this.contentDiv = el}> 178 |
 this.contentPre = el}>
179 |               {children}
180 |             
181 |
182 | 183 | {!wordWrap && } 187 |
188 |
189 | ) 190 | } 191 | } 192 | 193 | Notepad.defaultProps = { 194 | resizable: true, 195 | } 196 | 197 | export default Notepad 198 | -------------------------------------------------------------------------------- /src/apps/WordPad.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from 'styled-components' 3 | 4 | import Window from '../components/window/Window' 5 | import WindowMenuGroup from '../components/windowmenu/WindowMenuGroup' 6 | import RidgedBox from '../atoms/RidgedBox' 7 | import LightlyInsetBox from '../atoms/LightlyInsetBox' 8 | import LightBorderBox from '../atoms/LightBorderBox' 9 | import WindowToolbar, { Divider, Spacer, ToolbarButton } from '../components/WindowToolbar' 10 | import Select from '../atoms/Select' 11 | import textCursor from '../img/text-cursor.png' 12 | import rulerBg from '../img/ruler.png' 13 | import rulerMarker1 from '../img/ruler-marker-1.png' 14 | import rulerMarker2 from '../img/ruler-marker-2.png' 15 | import rulerMarker3 from '../img/ruler-marker-3.png' 16 | import justifyLeft from '../img/justify-left.png' 17 | import justifyCenter from '../img/justify-center.png' 18 | 19 | const ContentRoot = RidgedBox.extend` 20 | flex: 1; 21 | display: flex; 22 | width: 100%; 23 | background-color: white; 24 | overflow: auto; 25 | ` 26 | 27 | const Content = styled.div` 28 | min-height: 100%; 29 | width: 598px; 30 | max-width: 100%; 31 | padding: 10px; 32 | overflow-wrap: break-word; 33 | cursor: url('${textCursor}') 3 9, auto; 34 | user-select: text; 35 | 36 | > :first-child { 37 | margin-top: 0; 38 | } 39 | ` 40 | 41 | const BottomContentArea = styled.div` 42 | height: 100%; 43 | padding: 0 14px 0 3px; 44 | display: flex; 45 | justify-content: flex-end; 46 | 47 | > * + * { 48 | margin-left: 2px; 49 | } 50 | ` 51 | 52 | const RulerMain = styled(RidgedBox)` 53 | display: inline-block; 54 | position: relative; 55 | width: 576px; 56 | height: 17px; 57 | margin-left: 6px; 58 | border-left: 1px solid black; 59 | border-right: 0; 60 | background-color: white; 61 | background-image: url('${rulerBg}'); 62 | background-position: left center; 63 | background-repeat: repeat-x; 64 | padding-left: 47px; 65 | user-select: none; 66 | 67 | > div { 68 | display: inline-flex; 69 | width: 96px; 70 | height: 100%; 71 | justify-content: center; 72 | align-items: center; 73 | font-size: 11px; 74 | } 75 | ` 76 | 77 | const RulerExtra = styled(LightBorderBox)` 78 | display: inline-block; 79 | width: 120px; 80 | height: 17px; 81 | border-right: 0; 82 | background-image: url('${rulerBg}'); 83 | background-position: left center; 84 | background-repeat: repeat-x; 85 | padding-left: 47px; 86 | user-select: none; 87 | 88 | > div { 89 | display: inline-flex; 90 | width: 96px; 91 | height: 100%; 92 | justify-content: center; 93 | align-items: center; 94 | font-size: 11px; 95 | } 96 | ` 97 | 98 | const RulerRightBorder = styled.div` 99 | display: inline-block; 100 | width: 1px; 101 | height: 17px; 102 | background-color: #808080; 103 | border-bottom: 1px solid white; 104 | ` 105 | 106 | const RulerMarker = styled.img.attrs({ 107 | draggable: false, 108 | })` 109 | position: absolute; 110 | ` 111 | 112 | const fontCssMap = { 113 | 'Arial': 'Arial, sans-serif', 114 | 'Courier New': "'Courier New', monospace", 115 | 'Helvetica': 'Helvetica, sans-serif', 116 | 'Times New Roman': "'Times New Roman', serif", 117 | } 118 | const fonts = Object.keys(fontCssMap) 119 | const sizes = [8, 10, 12, 14, 16, 18, 20] 120 | 121 | class WordPad extends Component { 122 | constructor() { 123 | super() 124 | this.state = { 125 | font: 'Arial', 126 | fontSize: 12, 127 | bold: false, 128 | italic: false, 129 | underline: false, 130 | textAlign: 'left', 131 | } 132 | } 133 | 134 | setTitle() { 135 | const { onSetTitle } = this.props 136 | onSetTitle && onSetTitle(this.generateTitle()) 137 | } 138 | 139 | componentDidMount() { 140 | this.setTitle() 141 | } 142 | 143 | componentDidUpdate(prevProps) { 144 | if (this.props.fileName !== prevProps.fileName) { 145 | this.setTitle() 146 | } 147 | } 148 | 149 | generateTitle() { 150 | return (this.props.fileName || 'Untitled') + ' - WordPad' 151 | } 152 | 153 | getContentStyle() { 154 | const { 155 | font, 156 | fontSize, 157 | bold, 158 | italic, 159 | underline, 160 | textAlign, 161 | } = this.state 162 | 163 | const style = { 164 | fontFamily: fontCssMap[font], 165 | fontSize, 166 | textAlign, 167 | } 168 | if (bold) { 169 | style.fontWeight = 'bold' 170 | } 171 | if (italic) { 172 | style.fontStyle = 'italic' 173 | } 174 | if (underline) { 175 | style.textDecoration = 'underline' 176 | } 177 | 178 | return style 179 | } 180 | 181 | onFontChange = (e) => { 182 | this.setState({ font: e.target.value }) 183 | } 184 | 185 | onFontSizeChange = (e) => { 186 | this.setState({ fontSize: parseInt(e.target.value) }) 187 | } 188 | 189 | setTextAlign(textAlign) { 190 | this.setState({ textAlign }) 191 | } 192 | 193 | toggleState(stateProperty) { 194 | this.setState(state => ({ 195 | [stateProperty]: !state[stateProperty], 196 | })) 197 | } 198 | 199 | getMenus() { 200 | const { onRequestClose } = this.props 201 | return [ 202 | { label: 'File', underline: 0, items: [ 203 | { label: 'New...', underline: 0, disabled: true }, 204 | { label: 'Open...', underline: 0, disabled: true }, 205 | { label: 'Save', underline: 0, disabled: true }, 206 | { label: 'Save As...', underline: 5, disabled: true }, 207 | 'divider', 208 | { label: 'Print...', underline: 0, disabled: true }, 209 | { label: 'Print Preview', underline: 9, disabled: true }, 210 | { label: 'Page Setup', underline: 9, disabled: true }, 211 | 'divider', 212 | { label: 'Recent File', disabled: true }, 213 | 'divider', 214 | { label: 'Exit', underline: 1, onSelect: onRequestClose }, 215 | ] }, 216 | { label: 'Edit', underline: 0, items: [ 217 | { label: 'Undo', underline: 0, disabled: true }, 218 | 'divider', 219 | { label: 'Cut', underline: 2, disabled: true }, 220 | { label: 'Copy', underline: 0, disabled: true }, 221 | { label: 'Paste', underline: 0, disabled: true }, 222 | { label: 'Paste Special...', underline: 6, disabled: true }, 223 | { label: 'Clear', underline: 3, disabled: true }, 224 | { label: 'Select All', underline: 8, disabled: true }, 225 | 'divider', 226 | { label: 'Find...', underline: 0, disabled: true }, 227 | { label: 'Find Next', underline: 5, disabled: true }, 228 | { label: 'Replace...', underline: 1, disabled: true }, 229 | 'divider', 230 | { label: 'Links...', underline: 3, disabled: true }, 231 | { label: 'Object Properties', underline: 8, disabled: true }, 232 | { label: 'Object', underline: 0, disabled: true }, 233 | ] }, 234 | { label: 'View', underline: 0, items: [ 235 | { label: 'Toolbar', underline: 0, disabled: true }, 236 | { label: 'Format Bar', underline: 0, disabled: true }, 237 | { label: 'Ruler', underline: 0, disabled: true }, 238 | { label: 'Status Bar', underline: 0, disabled: true }, 239 | 'divider', 240 | { label: 'Options...', underline: 0, disabled: true }, 241 | ] }, 242 | { label: 'Insert', underline: 0, items: [ 243 | { label: 'Date and Time...', underline: 0, disabled: true }, 244 | { label: 'Object...', underline: 0, disabled: true }, 245 | ] }, 246 | { label: 'Format', underline: 1, items: [ 247 | { label: 'Font...', underline: 0, disabled: true }, 248 | { label: 'Bullet Style', underline: 0, disabled: true }, 249 | { label: 'Paragraph...', underline: 0, disabled: true }, 250 | { label: 'Tabs...', underline: 0, disabled: true }, 251 | ] }, 252 | { label: 'Help', underline: 0, items: [ 253 | { label: 'Help Topics', underline: 0, disabled: true }, 254 | { label: 'About WordPad', underline: 0, disabled: true }, 255 | ] }, 256 | ] 257 | } 258 | 259 | render() { 260 | const { 261 | children, 262 | title, 263 | fileName, 264 | onSetTitle, 265 | initialGeometry, 266 | ...props 267 | } = this.props 268 | 269 | const { 270 | font, 271 | fontSize, 272 | bold, 273 | italic, 274 | underline, 275 | textAlign, 276 | } = this.state 277 | 278 | const bottomAreaContent = 279 | 280 | 281 | 282 | 283 | const windowInitialGeometry = { 284 | width: 600, 285 | height: 500, 286 | ...initialGeometry 287 | } 288 | 289 | return ( 290 | 296 | 297 | 298 | 299 | 300 | 301 | 306 | 307 | 312 | 313 | {this.toggleState('bold')}} bold serif pressed={bold}> 314 | B 315 | 316 | {this.toggleState('italic')}} italic serif pressed={italic}> 317 | I 318 | 319 | {this.toggleState('underline')}} underline serif pressed={underline}> 320 | U 321 | 322 | 323 | {this.setTextAlign('left')}} pressed={textAlign === 'left'}> 324 | 325 | 326 | {this.setTextAlign('center')}} pressed={textAlign === 'center'}> 327 | 328 | 329 | {this.setTextAlign('right')}} pressed={textAlign === 'right'}> 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 |
1
339 |
2
340 |
3
341 |
4
342 |
5
343 | 344 | 345 | 346 | 347 |
348 | 349 | 350 |
7
351 |
352 | 353 |
354 | 355 | 356 | 357 | {children} 358 | 359 | 360 |
361 | ) 362 | } 363 | } 364 | 365 | export default WordPad 366 | -------------------------------------------------------------------------------- /src/atoms/GreyBox.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export default styled.div` 4 | background-color: #c0c0c0; 5 | ` 6 | -------------------------------------------------------------------------------- /src/atoms/LightBorderBox.js: -------------------------------------------------------------------------------- 1 | import GreyBox from './GreyBox' 2 | 3 | import borderImage from '../img/light-border.png' 4 | 5 | export default GreyBox.extend` 6 | border-width: 2px; 7 | border-style: solid; 8 | border-image: url('${borderImage}') 2; 9 | ` 10 | -------------------------------------------------------------------------------- /src/atoms/LightlyInsetBox.js: -------------------------------------------------------------------------------- 1 | import GreyBox from './GreyBox' 2 | 3 | import borderImage from '../img/light-border-inset.png' 4 | 5 | export default GreyBox.extend` 6 | border-width: 1px; 7 | border-style: solid; 8 | border-image: url('${borderImage}') 1; 9 | ` 10 | -------------------------------------------------------------------------------- /src/atoms/RidgedBox.js: -------------------------------------------------------------------------------- 1 | import GreyBox from './GreyBox' 2 | import PropTypes from 'prop-types' 3 | 4 | import borderImage from '../img/border.png' 5 | import borderImageStrong from '../img/border-strong.png' 6 | import borderImageInset from '../img/border-inset.png' 7 | 8 | function selectBorderImage({inset, strongBorder}) { 9 | if (inset) { 10 | return borderImageInset 11 | } else if (strongBorder) { 12 | return borderImageStrong 13 | } else { 14 | return borderImage 15 | } 16 | } 17 | 18 | function selectBorderWidth({inset, strongBorder}) { 19 | return !inset && strongBorder ? 3 : 2 20 | } 21 | 22 | const RidgedBox = GreyBox.extend` 23 | border-width: 2px; 24 | border-style: solid; 25 | border-image: url('${selectBorderImage}') ${selectBorderWidth}; 26 | ` 27 | 28 | RidgedBox.propTypes = { 29 | inset: PropTypes.bool, 30 | strongBorder: PropTypes.bool, 31 | } 32 | 33 | export { borderImage } 34 | export { borderImageInset } 35 | export default RidgedBox 36 | -------------------------------------------------------------------------------- /src/atoms/RidgedButton.js: -------------------------------------------------------------------------------- 1 | import GreyBox from './GreyBox' 2 | import PropTypes from 'prop-types' 3 | import { css } from 'styled-components' 4 | 5 | import borderImage from '../img/border-button.png' 6 | import borderImageStrong from '../img/border-strong.png' 7 | import borderImageInset from '../img/border-button-inset.png' 8 | import pressedBackground from '../img/scrollbar-track.png' 9 | 10 | const RidgedButton = GreyBox.extend` 11 | display: inline-flex; 12 | align-items: center; 13 | color: black; 14 | overflow: hidden; 15 | white-space: nowrap; 16 | text-overflow: ellipsis; 17 | font-family: "Microsoft Sans Serif", Arial, sans-serif; 18 | font-size: 12px; 19 | line-height: inherit; 20 | 21 | ${({standardFormat}) => standardFormat && css` 22 | width: 75px; 23 | height: 23px; 24 | justify-content: center; 25 | `} 26 | 27 | ${({ bold }) => bold && css`font-weight: bold;`} 28 | ${({ italic }) => italic && css`font-style: italic;`} 29 | ${({ underline }) => underline && css`text-decoration: underline;`} 30 | 31 | border-width: ${({inset, strongBorder}) => !inset && strongBorder ? '3px' : '2px'}; 32 | border-style: solid; 33 | border-image: 34 | url('${({inset, strongBorder}) => inset ? borderImageInset : (strongBorder ? borderImageStrong : borderImage)}') 35 | ${({inset, strongBorder}) => !inset && strongBorder ? 3 : 2}; 36 | 37 | img { 38 | pointer-events: none; 39 | } 40 | 41 | :disabled { 42 | color: #808080; 43 | text-shadow: white 1px 1px; 44 | 45 | img { 46 | filter: brightness(0%) invert(100%) brightness(50%) drop-shadow(1px 1px 0 white); 47 | } 48 | } 49 | 50 | > * { 51 | transform: translate(-1px, 0); 52 | } 53 | 54 | :focus { 55 | outline: 1px dotted black; 56 | outline-offset: -${({strongBorder}) => strongBorder ? 5 : 4}px; 57 | } 58 | 59 | ${({pressed}) => pressed && css` 60 | border-image: url('${borderImageInset}') 2; 61 | > * { 62 | transform: translate(0, 1px); 63 | } 64 | background-image: url('${pressedBackground}'); 65 | `} 66 | 67 | :active:enabled { 68 | border-image: url('${borderImageInset}') 2; 69 | > * { 70 | transform: translate(0, 1px); 71 | } 72 | } 73 | 74 | ::-moz-focus-inner { 75 | border: 0; 76 | } 77 | `.withComponent('button') 78 | 79 | RidgedButton.propTypes = { 80 | standardFormat: PropTypes.bool, 81 | bold: PropTypes.bool, 82 | italic: PropTypes.bool, 83 | underline: PropTypes.bool, 84 | inset: PropTypes.bool, 85 | strongBorder: PropTypes.bool, 86 | pressed: PropTypes.bool, 87 | } 88 | 89 | export default RidgedButton 90 | -------------------------------------------------------------------------------- /src/atoms/Select.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import PropTypes from 'prop-types' 3 | 4 | import dropdownImage from '../img/dropdown.png' 5 | import borderImageInset from '../img/border-inset.png' 6 | import pointer from '../img/pointer.png' 7 | 8 | const Select = styled.select` 9 | height: 21px; 10 | border-width: 2px; 11 | border-style: solid; 12 | border-image: url('${borderImageInset}') 2; 13 | appearance: none; 14 | background: white url('${dropdownImage}') no-repeat right; 15 | padding: 0 17px 0 1px; 16 | cursor: url('${pointer}') 0 0, auto; 17 | ${({width}) => width && `width: ${width};`}; 18 | 19 | :focus { 20 | outline: 0; 21 | } 22 | 23 | option { 24 | background-color: white; 25 | color: black; 26 | 27 | :hover { 28 | background-color: #000080; 29 | color: white; 30 | } 31 | } 32 | 33 | :-moz-focusring { 34 | color: transparent; 35 | text-shadow: 0 0 0 #000; 36 | } 37 | ` 38 | 39 | Select.propTypes = { 40 | width: PropTypes.string, 41 | } 42 | 43 | export default Select 44 | -------------------------------------------------------------------------------- /src/components/Desktop.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export default styled.div` 4 | flex: 1; 5 | position: relative; 6 | z-index: 1; 7 | 8 | > * { 9 | position: absolute; 10 | top: 0; 11 | bottom: 0; 12 | left: 0; 13 | right: 0; 14 | } 15 | ` 16 | -------------------------------------------------------------------------------- /src/components/IconArea.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import PropTypes from 'prop-types' 3 | 4 | const IconArea = styled.div` 5 | width: 100%; 6 | max-height: 100%; 7 | flex: 1; 8 | display: flex; 9 | flex-direction: ${({desktop}) => desktop ? 'column' : 'row'}; 10 | flex-wrap: wrap; 11 | align-content: flex-start; 12 | padding-top: 4px; 13 | overflow: ${({desktop}) => desktop ? 'hidden' : 'auto'}; 14 | 15 | > .reactows95-Icon { 16 | ${({ iconTextColor }) => iconTextColor && ` 17 | color: ${iconTextColor} 18 | `}; 19 | } 20 | ` 21 | 22 | IconArea.propTypes = { 23 | desktop: PropTypes.bool, 24 | iconTextColor: PropTypes.string, 25 | } 26 | 27 | export default IconArea 28 | -------------------------------------------------------------------------------- /src/components/IconRegular.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled, { css } from 'styled-components' 4 | 5 | import defaultIcon from '../img/icon-default.png' 6 | 7 | const Root = styled.div` 8 | width: 75px; 9 | height: 75px; 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | ` 14 | 15 | const ImageContainer = styled.div` 16 | position: relative; 17 | width: 32px; 18 | height: 32px; 19 | margin-top: 2px; 20 | image-rendering: pixelated; 21 | image-rendering: -moz-crisp-edges; 22 | ` 23 | 24 | const IconImage = styled.button` 25 | width: 32px; 26 | height: 32px; 27 | padding: 0; 28 | border: 0; 29 | background-color: transparent; 30 | background-image: url('${({image}) => image}'); 31 | 32 | :focus { 33 | outline: 0; 34 | 35 | :before { 36 | content: ""; 37 | display: block; 38 | position: absolute; 39 | top: 0; 40 | bottom: 0; 41 | left: 0; 42 | right: 0; 43 | background: rgba(0,0,127, 0.5); 44 | mask-image: url('${({image}) => image}'); 45 | mask-size: 32px 32px; 46 | } 47 | } 48 | 49 | ::-moz-focus-inner { 50 | border: 0; 51 | } 52 | ` 53 | 54 | const IconText = styled.div` 55 | display: inline-block; 56 | margin-top: 3px; 57 | padding: 1px 2px; 58 | max-width: calc(100% - 6px); 59 | line-height: 1.2em; 60 | max-height: calc(2.4em + 2px); 61 | text-align: center; 62 | overflow: hidden; 63 | background-color: ${({hasFocus}) => hasFocus ? '#00007b' : 'transparent'}; 64 | ${({hasFocus}) => hasFocus && css` 65 | color: white; 66 | outline: 1px dotted #ffff00; 67 | outline-offset: -1px; 68 | `}; 69 | ` 70 | 71 | const DOUBLE_CLICK_TIMEOUT = 500 72 | 73 | class IconRegular extends Component { 74 | constructor() { 75 | super() 76 | this.state = { hasFocus: false } 77 | this.doubleClickTimer = null 78 | } 79 | 80 | onClick = (e) => { 81 | this.props.onClick && this.props.onClick(e) 82 | 83 | if (this.doubleClickTimer) { 84 | clearTimeout(this.doubleClickTimer) 85 | this.doubleClickTimer = null 86 | this.props.onDoubleClick && this.props.onDoubleClick(e) 87 | } else { 88 | this.doubleClickTimer = setTimeout(() => { 89 | this.doubleClickTimer = null 90 | }, DOUBLE_CLICK_TIMEOUT) 91 | } 92 | } 93 | 94 | onFocus = (e) => { 95 | this.props.onFocus && this.props.onFocus(e) 96 | this.setState({ hasFocus: true }) 97 | } 98 | 99 | onBlur = (e) => { 100 | this.props.onBlur && this.props.onBlur(e) 101 | this.setState({ hasFocus: false }) 102 | } 103 | 104 | render() { 105 | const { label, icon } = this.props 106 | const { hasFocus } = this.state 107 | 108 | return ( 109 | 110 | 111 | this.button = el} 117 | /> 118 | 119 | { 122 | this.button.focus(); 123 | this.button.click(); 124 | }} 125 | > 126 | {label} 127 | 128 | 129 | ) 130 | } 131 | } 132 | 133 | IconRegular.propTypes = { 134 | label: PropTypes.string.isRequired, 135 | icon: PropTypes.string, 136 | onClick: PropTypes.func, 137 | onDoubleClick: PropTypes.func, 138 | onFocus: PropTypes.func, 139 | onBlur: PropTypes.func, 140 | } 141 | 142 | export default IconRegular 143 | -------------------------------------------------------------------------------- /src/components/MenuOverlay.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { findDOMNode } from 'react-dom' 4 | import styled from 'styled-components' 5 | import { RootCloseWrapper } from 'react-overlays' 6 | 7 | const Root = styled.div` 8 | position: absolute; 9 | ${({positioning}) => positioning.hasOwnProperty('top') && `top: ${positioning.top}px;`} 10 | ${({positioning}) => positioning.hasOwnProperty('bottom') && `bottom: ${positioning.bottom}px;`} 11 | ${({positioning}) => positioning.hasOwnProperty('left') && `left: ${positioning.left}px;`} 12 | ${({positioning}) => positioning.hasOwnProperty('right') && `right: ${positioning.right}px;`} 13 | z-index: 1; 14 | ` 15 | 16 | class MenuOverlay extends Component { 17 | constructor() { 18 | super() 19 | this.state = { containerRect: null } 20 | } 21 | 22 | componentDidMount() { 23 | this.updateContainerRect() 24 | } 25 | 26 | componentDidUpdate() { 27 | this.updateContainerRect() 28 | } 29 | 30 | updateContainerRect() { 31 | const oldRect = this.state.containerRect 32 | const containerDomNode = findDOMNode(this.props.container) 33 | if (containerDomNode) { 34 | const containerRect = containerDomNode.getBoundingClientRect() 35 | if (!oldRect || containerRect.width !== oldRect.width || containerRect.height !== oldRect.height) { 36 | this.setState({ containerRect }) 37 | } 38 | } 39 | } 40 | 41 | calculatePositioning(containerRect) { 42 | let { placement, placementOffset, alignEdge, alignOffset } = this.props 43 | const positioning = {} 44 | 45 | placementOffset = placementOffset || 0 46 | alignOffset = alignOffset || 0 47 | 48 | if (placement === 'top') { 49 | positioning.bottom = containerRect.height + placementOffset 50 | } else if (placement === 'bottom') { 51 | positioning.top = containerRect.height + placementOffset 52 | } else if (placement === 'right') { 53 | positioning.left = containerRect.width + placementOffset 54 | } else if (placement === 'left') { 55 | positioning.right = containerRect.width + placementOffset 56 | } 57 | 58 | if (alignEdge === 'top') { 59 | positioning.top = alignOffset 60 | } else if (alignEdge === 'bottom') { 61 | positioning.bottom = alignOffset 62 | } else if (alignEdge === 'right') { 63 | positioning.right = alignOffset 64 | } else if (alignEdge === 'left') { 65 | positioning.left = alignOffset 66 | } 67 | 68 | return positioning 69 | } 70 | 71 | render() { 72 | const { 73 | show, 74 | rootClose, 75 | rootCloseEvent, 76 | onHide, 77 | children, 78 | } = this.props 79 | 80 | const { containerRect } = this.state 81 | 82 | if (!show || !containerRect) { 83 | return null 84 | } 85 | 86 | let child = ( 87 | 88 | {children} 89 | 90 | ) 91 | 92 | if (rootClose) { 93 | child = ( 94 | 98 | {child} 99 | 100 | ) 101 | } 102 | 103 | return child 104 | } 105 | } 106 | 107 | MenuOverlay.propTypes = { 108 | placement: PropTypes.oneOf(['top', 'bottom', 'left', 'right']).isRequired, 109 | placementOffset: PropTypes.number, 110 | alignEdge: PropTypes.oneOf(['top', 'bottom', 'left', 'right']), 111 | alignOffset: PropTypes.number, 112 | show: PropTypes.bool, 113 | rootClose: PropTypes.bool, 114 | rootCloseEvent: PropTypes.oneOf(['click', 'mousedown']), 115 | onHide: PropTypes.func, 116 | container: PropTypes.any.isRequired, 117 | } 118 | 119 | export default MenuOverlay 120 | -------------------------------------------------------------------------------- /src/components/Shell.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled, { css } from 'styled-components' 3 | import PropTypes from 'prop-types' 4 | 5 | import borderImage from '../img/border.png' 6 | import borderImageInset from '../img/border-inset.png' 7 | import scrollbarTrack from '../img/scrollbar-track.png' 8 | import scrollUp from '../img/arrow-up.png' 9 | import scrollDown from '../img/arrow-down.png' 10 | import scrollLeft from '../img/arrow-left.png' 11 | import scrollRight from '../img/arrow-right.png' 12 | import pointer from '../img/pointer.png' 13 | import semiBusyCursor from '../img/semi-busy.png' 14 | import Select from '../atoms/Select' 15 | 16 | function getCursor(semiBusy) { 17 | if (semiBusy) return css`url('${semiBusyCursor}') 0 0, progress` 18 | return css`url('${pointer}') 0 0, auto` 19 | } 20 | 21 | const Root = styled.div` 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | bottom: 0; 26 | right: 0; 27 | background-color: #008080; 28 | display: flex; 29 | flex-direction: column; 30 | font-family: "Microsoft Sans Serif", Arial, sans-serif; 31 | font-size: 12px; 32 | overflow: hidden; 33 | user-select: none; 34 | cursor: ${({semiBusy}) => getCursor(semiBusy)}; 35 | 36 | button { 37 | cursor: ${({semiBusy}) => getCursor(semiBusy)}; 38 | } 39 | 40 | * { 41 | box-sizing: border-box; 42 | } 43 | 44 | *::-webkit-scrollbar { 45 | width: 16px; 46 | height: 16px; 47 | } 48 | 49 | *::-webkit-scrollbar-button:single-button { 50 | background-color: #c0c0c0; 51 | width: 16px; 52 | height: 16px; 53 | border-width: 2px; 54 | border-style: solid; 55 | border-image: url('${borderImage}') 2; 56 | } 57 | 58 | *::-webkit-scrollbar-button:single-button:active { 59 | border-image: url('${borderImageInset}') 2; 60 | } 61 | 62 | *::-webkit-scrollbar-button:single-button:vertical:decrement { 63 | background: #c0c0c0 url('${scrollUp}') center no-repeat; 64 | } 65 | 66 | *::-webkit-scrollbar-button:single-button:vertical:increment { 67 | background: #c0c0c0 url('${scrollDown}') center no-repeat; 68 | } 69 | 70 | *::-webkit-scrollbar-button:single-button:horizontal:decrement { 71 | background: #c0c0c0 url('${scrollLeft}') center no-repeat; 72 | } 73 | 74 | *::-webkit-scrollbar-button:single-button:horizontal:increment { 75 | background: #c0c0c0 url('${scrollRight}') center no-repeat; 76 | } 77 | 78 | *::-webkit-scrollbar-track { 79 | background: #f1f1f1; 80 | background-image: url('${scrollbarTrack}'); 81 | } 82 | 83 | *::-webkit-scrollbar-thumb { 84 | background-color: #c0c0c0; 85 | border-width: 2px; 86 | border-style: solid; 87 | border-image: url('${borderImage}') 2; 88 | } 89 | 90 | *::selection { 91 | background-color: #000080; 92 | color: white; 93 | } 94 | ` 95 | 96 | class Shell extends Component { 97 | render() { 98 | const { semiBusy, children } = this.props 99 | return ( 100 | 101 | {children} 102 | 103 | ) 104 | } 105 | } 106 | 107 | Select.propTypes = { 108 | semiBusy: PropTypes.bool, 109 | } 110 | 111 | export default Shell 112 | -------------------------------------------------------------------------------- /src/components/SystemTray.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import LightlyInsetBox from '../atoms/LightlyInsetBox' 4 | 5 | const Root = LightlyInsetBox.extend` 6 | display: flex; 7 | align-items: center; 8 | padding: 0 11px; 9 | user-select: none; 10 | white-space: nowrap; 11 | ` 12 | 13 | class SystemTray extends Component { 14 | static getTime() { 15 | const date = new Date() 16 | let hours = date.getHours() 17 | const amPm = hours >= 12 ? 'PM' : 'AM' 18 | hours = hours % 12 19 | if (hours === 0) { 20 | hours = 12 21 | } 22 | let minutes = date.getMinutes() 23 | if (minutes < 10) { 24 | minutes = `0${minutes}` 25 | } 26 | return `${hours}:${minutes} ${amPm}` 27 | } 28 | 29 | refresh = () => { 30 | this.forceUpdate() 31 | } 32 | 33 | componentDidMount() { 34 | this.scheduleUpdate() 35 | } 36 | 37 | scheduleUpdate() { 38 | const date = new Date() 39 | const timeToWait = (60 - date.getSeconds() + 1) * 1000 40 | this.updateTimeout = setTimeout(this.updateClock, timeToWait) 41 | } 42 | 43 | updateClock = () => { 44 | this.refresh() 45 | this.scheduleUpdate() 46 | } 47 | 48 | componentWillUnmount() { 49 | if (this.updateTimeout) { 50 | clearTimeout(this.updateTimeout) 51 | delete(this.updateTimeout) 52 | } 53 | } 54 | 55 | render() { 56 | return ( 57 | 58 | {SystemTray.getTime()} 59 | 60 | ) 61 | } 62 | } 63 | 64 | export default SystemTray 65 | -------------------------------------------------------------------------------- /src/components/Taskbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from 'styled-components' 4 | 5 | import GreyBox from '../atoms/GreyBox' 6 | import StartButton from './startmenu/StartButton' 7 | import SystemTray from './SystemTray' 8 | 9 | import borderImage from '../img/border.png' 10 | 11 | const Root = GreyBox.extend` 12 | width: 100%; 13 | height: 28px; 14 | border-top-width: 2px; 15 | border-top-style: solid; 16 | border-image: url('${borderImage}') 2; 17 | padding: 2px; 18 | display: flex; 19 | align-items: stretch; 20 | z-index: 2; 21 | ` 22 | 23 | const Inner = styled.div` 24 | display: flex; 25 | justify-content: flex-start; 26 | flex: 1; 27 | padding: 0 4px; 28 | 29 | > * { 30 | flex: 1; 31 | width: 0; 32 | max-width: 160px; 33 | 34 | + * { 35 | margin-left: 3px; 36 | } 37 | } 38 | ` 39 | 40 | class Taskbar extends Component { 41 | render() { 42 | const { 43 | startMenuItems, 44 | children, 45 | } = this.props 46 | 47 | return ( 48 | 49 | 50 | 51 | {children} 52 | 53 | 54 | 55 | ) 56 | } 57 | } 58 | 59 | Taskbar.propTypes = { 60 | startMenuItems: PropTypes.array, 61 | } 62 | 63 | export default Taskbar 64 | -------------------------------------------------------------------------------- /src/components/TaskbarItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from 'styled-components' 4 | 5 | import RidgedButton from '../atoms/RidgedButton' 6 | 7 | const Root = RidgedButton.extend` 8 | justify-content: flex-start; 9 | padding: 1px 4px; 10 | ` 11 | 12 | const IconImage = styled.img.attrs({ 13 | draggable: false, 14 | })` 15 | width: 16px; 16 | height: 16px; 17 | margin-right: 3px; 18 | image-rendering: pixelated; 19 | image-rendering: -moz-crisp-edges; 20 | ` 21 | 22 | const Text = styled.span` 23 | overflow: hidden; 24 | white-space: nowrap; 25 | text-overflow: ellipsis; 26 | ` 27 | 28 | class TaskbarItem extends Component { 29 | render() { 30 | const { 31 | label, 32 | icon, 33 | focused, 34 | ...props 35 | } = this.props 36 | 37 | return ( 38 | 39 | {icon && } 40 | {label} 41 | 42 | ) 43 | } 44 | } 45 | 46 | TaskbarItem.propTypes = { 47 | label: PropTypes.string.isRequired, 48 | icon: PropTypes.string, 49 | onClick: PropTypes.func, 50 | focused: PropTypes.bool, 51 | } 52 | 53 | export default TaskbarItem 54 | -------------------------------------------------------------------------------- /src/components/WindowLayer.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export default styled.div` 4 | pointer-events: none; 5 | ` 6 | -------------------------------------------------------------------------------- /src/components/WindowToolbar.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components' 2 | import PropTypes from 'prop-types' 3 | 4 | import LightlyInsetBox from '../atoms/LightlyInsetBox' 5 | import RidgedButton from '../atoms/RidgedButton' 6 | 7 | const WindowToolbar = styled.div` 8 | margin-bottom: 2px; 9 | padding-left: ${({noLeftPad}) => noLeftPad ? '0' : '6px'}; 10 | 11 | ${({noWrap}) => noWrap && css` 12 | overflow-x: hidden; 13 | white-space: nowrap; 14 | `} 15 | 16 | ${({verticalOverflowSpace}) => verticalOverflowSpace && css` 17 | margin-top: ${-verticalOverflowSpace}px; 18 | margin-bottom: ${-verticalOverflowSpace + 2}px; 19 | padding-top: ${verticalOverflowSpace}px; 20 | padding-bottom: ${verticalOverflowSpace}px; 21 | `} 22 | 23 | > * { 24 | vertical-align: top; 25 | } 26 | ` 27 | 28 | const Divider = LightlyInsetBox.extend` 29 | width: 100%; 30 | height: 2px; 31 | margin: 0; 32 | margin-top: 1px; 33 | margin-bottom: 3px; 34 | border-width: 1px 0; 35 | `.withComponent('hr') 36 | 37 | const Spacer = styled.div` 38 | display: inline-block; 39 | width: 8px; 40 | ` 41 | 42 | const ToolbarButton = RidgedButton.extend` 43 | width: 23px; 44 | height: 22px; 45 | display: inline-flex; 46 | padding: 0; 47 | justify-content: center; 48 | align-items: center; 49 | font-size: 14px; 50 | ${({serif}) => serif && `font-family: 'Times New Roman', serif;`} 51 | ` 52 | 53 | WindowToolbar.propTypes = { 54 | noWrap: PropTypes.bool, 55 | noLeftPad: PropTypes.bool, 56 | verticalOverflowSpace: PropTypes.number, 57 | } 58 | 59 | ToolbarButton.propTypes = { 60 | serif: PropTypes.bool, 61 | } 62 | 63 | export default WindowToolbar 64 | export { ToolbarButton, Spacer, Divider } 65 | -------------------------------------------------------------------------------- /src/components/startmenu/StartButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from 'styled-components' 4 | import { RootCloseWrapper } from 'react-overlays' 5 | 6 | import StartMenu from './StartMenu' 7 | import RidgedButton from '../../atoms/RidgedButton' 8 | 9 | import logo from '../../img/logo-small.png' 10 | 11 | const Root = styled.div` 12 | position: relative; 13 | ` 14 | 15 | const StyledStartButton = RidgedButton.extend` 16 | height: 100%; 17 | padding: 0 3px; 18 | font-weight: bold; 19 | white-space: nowrap; 20 | image-rendering: pixelated; 21 | image-rendering: -moz-crisp-edges; 22 | ` 23 | 24 | const LogoImage = styled.img.attrs({ 25 | draggable: false, 26 | })` 27 | vertical-align: bottom; 28 | ` 29 | 30 | class StartButton extends Component { 31 | constructor() { 32 | super() 33 | this.state = { startMenuOpen: false } 34 | } 35 | 36 | toggleStartMenuOpen = () => { 37 | this.setState(state => ({ 38 | startMenuOpen: !state.startMenuOpen, 39 | })) 40 | } 41 | 42 | closeStartMenu = () => { 43 | this.setState({ startMenuOpen: false }) 44 | } 45 | 46 | render() { 47 | const { startMenuItems } = this.props 48 | const { startMenuOpen } = this.state 49 | 50 | return 51 | this.root = el}> 52 | 57 | 58 | Start 59 | 60 | 61 | 62 | 68 | 69 | 70 | } 71 | } 72 | 73 | StartButton.propTypes = { 74 | startMenuItems: PropTypes.array, 75 | } 76 | 77 | export default StartButton 78 | -------------------------------------------------------------------------------- /src/components/startmenu/StartMenu.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { findDOMNode } from 'react-dom' 4 | import styled from 'styled-components' 5 | 6 | import MenuOverlay from '../MenuOverlay' 7 | import RidgedBox from '../../atoms/RidgedBox' 8 | import StartMenuItem, { Divider } from './StartMenuItem' 9 | import isObject from '../../util/isObject' 10 | import { getWidth, getHeight } from '../../util/getViewport' 11 | 12 | const Root = RidgedBox.extend` 13 | position: relative; 14 | display: flex; 15 | padding: 1px; 16 | max-height: 100vh; 17 | ` 18 | 19 | const LeftStripe = styled.div` 20 | position: relative; 21 | width: 21px; 22 | background-color: #808080; 23 | overflow: hidden; 24 | ` 25 | 26 | const OSNameText = styled.div` 27 | position: absolute; 28 | bottom: 0; 29 | transform: rotate(-90deg); 30 | transform-origin: 8px 8px; 31 | font-size: 20px; 32 | line-height: 22px; 33 | ` 34 | 35 | const OSName1 = styled.span` 36 | color: #c0c0c0; 37 | font-weight: 900; 38 | ` 39 | 40 | const OSName2 = styled.span` 41 | color: white; 42 | ` 43 | 44 | class StartMenu extends Component { 45 | constructor() { 46 | super() 47 | this.state = { 48 | highlightedItemKey: null, 49 | openedSubMenuItemKey: null, 50 | directionReversed: false, 51 | verticalAdjustment: 0, 52 | } 53 | this.positioningDetermined = false 54 | } 55 | 56 | static getDerivedStateFromProps(props) { 57 | if (!props.isOpen) { 58 | return { 59 | highlightedItemKey: null, 60 | openedSubMenuItemKey: null, 61 | } 62 | } 63 | return null 64 | } 65 | 66 | componentDidMount() { 67 | if (this.props.isOpen) { 68 | this.determinePositioning() 69 | } 70 | } 71 | 72 | componentDidUpdate(prevProps) { 73 | if (this.props.isOpen && !prevProps.isOpen 74 | && !this.positioningDetermined) { 75 | this.determinePositioning() 76 | } 77 | } 78 | 79 | determinePositioning() { 80 | const { defaultDirectionIsLeft } = this.props 81 | if (this.root) { 82 | const domNode = findDOMNode(this.root) 83 | const rect = domNode.getBoundingClientRect() 84 | 85 | // Determine whether to reverse direction 86 | if ((defaultDirectionIsLeft && rect.left < 0) 87 | || (!defaultDirectionIsLeft && rect.right > getWidth())) { 88 | this.setState({ directionReversed: true }) 89 | } 90 | 91 | // Determine if menu needs to be lifted up 92 | if (rect.bottom > getHeight()) { 93 | this.setState({ verticalAdjustment: getHeight() - rect.bottom }) 94 | } 95 | 96 | this.positioningDetermined = true 97 | } 98 | } 99 | 100 | onMouseEnterItem = (itemKey) => () => { 101 | this.setState({ highlightedItemKey: itemKey }) 102 | } 103 | 104 | onLingerItem = (itemKey) => () => { 105 | this.setState({ openedSubMenuItemKey: itemKey }) 106 | } 107 | 108 | generateMenuContent() { 109 | const { items, isSubMenu, onItemSelected } = this.props 110 | const { highlightedItemKey, openedSubMenuItemKey } = this.state 111 | 112 | if (!Array.isArray(items)) { 113 | return null 114 | } 115 | 116 | if (items.length === 0) { 117 | const i = 0 118 | return [ 119 | 130 | ] 131 | } 132 | 133 | return items.map((item, i) => { 134 | if (item === 'divider') { 135 | return 136 | } else if (!isObject(item)) { 137 | return null 138 | } else { 139 | const { icon, label, subMenuItems, onSelect } = item 140 | return { 153 | onItemSelected && onItemSelected() 154 | onSelect && onSelect(item) 155 | }} 156 | onItemSelected={onItemSelected} 157 | /> 158 | } 159 | }) 160 | } 161 | 162 | directionIsLeft() { 163 | const { defaultDirectionIsLeft } = this.props 164 | const { directionReversed } = this.state 165 | 166 | return !directionReversed !== !defaultDirectionIsLeft // Equivalent to XOR 167 | } 168 | 169 | render() { 170 | const { 171 | isOpen, 172 | isSubMenu, 173 | container, 174 | } = this.props 175 | const { verticalAdjustment } = this.state 176 | 177 | const subMenuPlacement = this.directionIsLeft() ? 'left' : 'right' 178 | 179 | return ( 180 | 188 | { 192 | if (el) { 193 | this.root = el 194 | } 195 | }} 196 | > 197 | {!isSubMenu && 198 | 199 | Reactows95 200 | 201 | } 202 |
203 | {this.generateMenuContent()} 204 |
205 |
206 |
207 | ) 208 | } 209 | } 210 | 211 | StartMenu.propTypes = { 212 | items: PropTypes.arrayOf(PropTypes.oneOfType([ 213 | PropTypes.object, 214 | PropTypes.string, 215 | ])), 216 | isOpen: PropTypes.bool, 217 | isSubMenu: PropTypes.bool, 218 | onItemSelected: PropTypes.func, 219 | defaultDirectionIsLeft: PropTypes.bool, 220 | container: MenuOverlay.propTypes.container, 221 | } 222 | 223 | StartMenu.defaultProps = { 224 | isOpen: false, 225 | defaultDirectionIsLeft: false, 226 | } 227 | 228 | export default StartMenu 229 | -------------------------------------------------------------------------------- /src/components/startmenu/StartMenuItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled, { css } from 'styled-components' 4 | 5 | import StartMenu from './StartMenu' 6 | import LightlyInsetBox from '../../atoms/LightlyInsetBox' 7 | 8 | import arrow from '../../img/arrow-right.png' 9 | import defaultIcon from '../../img/icon-default.png' 10 | import defaultIcon24 from '../../img/icon-default-24.png' 11 | 12 | const Root = styled.div` 13 | position: relative; 14 | width: 100%; 15 | height: ${({mainStartMenu}) => mainStartMenu ? '32px' : '20px'}; 16 | display: flex; 17 | align-items: center; 18 | white-space: nowrap; 19 | padding-right: 4px; 20 | 21 | ${({highlighted}) => highlighted && ` 22 | background-color: #000080; 23 | `} 24 | ` 25 | 26 | const IconContainer = styled.div` 27 | width: ${({mainStartMenu}) => mainStartMenu ? '32px' : '16px'}; 28 | height: ${({mainStartMenu}) => mainStartMenu ? '32px' : '16px'}; 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | margin-left: 3px; 33 | 34 | ` 35 | 36 | const IconImage = styled.img.attrs({ 37 | draggable: false, 38 | })` 39 | max-width: 100%; 40 | max-height: 100%; 41 | image-rendering: pixelated; 42 | image-rendering: -moz-crisp-edges; 43 | ` 44 | 45 | const Label = styled.div` 46 | flex: 1; 47 | max-width: 250px; 48 | margin-left: 3px; 49 | margin-right: 3px; 50 | overflow: hidden; 51 | white-space: nowrap; 52 | text-overflow: ellipsis; 53 | 54 | ${({ highlighted }) => highlighted && css` 55 | color: white; 56 | `} 57 | 58 | ${({ disabled }) => disabled && css` 59 | color: #808080; 60 | `} 61 | 62 | ${({ disabled, highlighted }) => disabled && !highlighted && css` 63 | text-shadow: white 1px 1px; 64 | `} 65 | ` 66 | 67 | const SubMenuArrow = styled.img` 68 | filter: ${({highlighted}) => highlighted ? 'invert(100%)' : 'none'}; 69 | margin-left: ${({mainStartMenu}) => mainStartMenu ? '26px' : '5px'}; 70 | ` 71 | 72 | export const Divider = LightlyInsetBox.extend` 73 | height: 2px; 74 | margin: 3px; 75 | border-width: 1px 0; 76 | `.withComponent('hr') 77 | 78 | class StartMenuItem extends Component { 79 | componentDidMount() { 80 | if (this.props.highlighted) { 81 | this.onGainHighlight() 82 | } 83 | } 84 | 85 | componentDidUpdate(prevProps) { 86 | if (!prevProps.highlighted && this.props.highlighted) { 87 | this.onGainHighlight() 88 | } else if (prevProps.highlighted && !this.props.highlighted) { 89 | this.onLoseHighlight() 90 | } 91 | } 92 | 93 | onGainHighlight() { 94 | const { onLinger } = this.props 95 | this.lingerTimeout = setTimeout(() => { 96 | onLinger && onLinger() 97 | delete this.lingerTimeout 98 | }, 500) 99 | } 100 | 101 | onLoseHighlight() { 102 | if (this.lingerTimeout) { 103 | clearTimeout(this.lingerTimeout) 104 | delete this.lingerTimeout 105 | } 106 | } 107 | 108 | onMouseEnter = (e) => { 109 | const { onMouseEnter } = this.props 110 | onMouseEnter && onMouseEnter(e) 111 | } 112 | 113 | onClick = (e) => { 114 | const { subMenuItems, onLinger, onSelect } = this.props 115 | onLinger && onLinger(e) 116 | !subMenuItems && onSelect && onSelect() 117 | } 118 | 119 | componentWillUnmount() { 120 | if (this.lingerTimeout) { 121 | clearTimeout(this.lingerTimeout) 122 | delete this.lingerTimeout 123 | } 124 | } 125 | 126 | render() { 127 | const { 128 | highlighted, 129 | mainStartMenu, 130 | subMenuItems, 131 | subMenuOpen, 132 | defaultDirectionIsLeft, 133 | icon, 134 | noIcon, 135 | label, 136 | disabled, 137 | onItemSelected, 138 | } = this.props 139 | 140 | return ( 141 | { 148 | if (el) { 149 | this.root = el 150 | } 151 | }} 152 | > 153 | 154 | {noIcon || } 155 | 156 | 157 | 158 | 159 | {subMenuItems && } 164 | 165 | {subMenuItems && 166 | 174 | } 175 | 176 | ) 177 | } 178 | } 179 | 180 | StartMenuItem.propTypes = { 181 | highlighted: PropTypes.bool, 182 | mainStartMenu: PropTypes.bool, 183 | subMenuItems: PropTypes.array, 184 | subMenuOpen: PropTypes.bool, 185 | defaultDirectionIsLeft: PropTypes.bool, 186 | icon: PropTypes.string, 187 | label: PropTypes.string.isRequired, 188 | onItemSelected: PropTypes.func, 189 | onLinger: PropTypes.func, 190 | onSelect: PropTypes.func, 191 | } 192 | 193 | export default StartMenuItem 194 | -------------------------------------------------------------------------------- /src/components/window/Window.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled, { css } from 'styled-components' 4 | 5 | import RidgedBox from '../../atoms/RidgedBox' 6 | import RidgedButton from '../../atoms/RidgedButton' 7 | import WindowTitleBarTransition from './WindowTitleBarTransition' 8 | import { TitleBar, TitleWrapper, IconImage } from './WindowTitleBar' 9 | 10 | import minimizeIcon from '../../img/minimize.png' 11 | import maximizeIcon from '../../img/maximize.png' 12 | import unmaximizeIcon from '../../img/unmaximize.png' 13 | import closeIcon from '../../img/close.png' 14 | import resizeHandleImage from '../../img/resize-handle.png' 15 | import resizeSW from '../../img/resize-se.png' 16 | 17 | const LEFT_MOUSE_BUTTON = 0 18 | const DEFAULT_MIN_WIDTH = 200 19 | const DEFAULT_MIN_HEIGHT = 200 20 | 21 | const Root = RidgedBox.extend` 22 | display: ${({minimized}) => minimized ? 'none' : 'flex'}; 23 | flex-direction: column; 24 | position: absolute; 25 | padding: ${({maximized}) => maximized ? '0' : '2px'}; 26 | pointer-events: all; 27 | ${({maximized}) => maximized && css` 28 | border: 0; 29 | `} 30 | ${({zIndex}) => (typeof zIndex !== 'undefined') && css` 31 | z-index: ${zIndex}; 32 | `} 33 | ` 34 | 35 | const WindowContent = styled.div` 36 | width: 100%; 37 | flex: 1; 38 | display: flex; 39 | flex-direction: column; 40 | position: relative; 41 | min-height: 0; 42 | ` 43 | 44 | const WindowButton = RidgedButton.extend` 45 | padding: 0 1px 1px 2px; 46 | margin-left: ${({leftMargin}) => leftMargin ? '2px' : '0'}; 47 | image-rendering: pixelated; 48 | image-rendering: -moz-crisp-edges; 49 | ` 50 | 51 | const BottomArea = styled.div` 52 | position: relative; 53 | height: 17px; 54 | margin-top: 2px; 55 | ` 56 | 57 | const ResizeHandle = styled.img.attrs({ 58 | src: resizeHandleImage, 59 | draggable: false, 60 | })` 61 | position: absolute; 62 | bottom: -2px; 63 | right: -2px; 64 | cursor: url('${resizeSW}') 7 7, se-resize; 65 | opacity: ${({visible}) => visible ? 1 : 0} 66 | ` 67 | 68 | const maximizedGeometry = { 69 | top: 0, 70 | bottom: 0, 71 | left: 0, 72 | right: 0, 73 | } 74 | 75 | class Window extends Component { 76 | constructor(props) { 77 | super(props) 78 | 79 | const initialGeometry = props.initialGeometry || {} 80 | 81 | this.state = { 82 | dragging: false, 83 | resizing: false, 84 | dragStart: {}, 85 | geometry: { 86 | left: initialGeometry.left || 100, 87 | top: initialGeometry.top || 100, 88 | width: initialGeometry.width || 420, 89 | height: initialGeometry.height || 400, 90 | }, 91 | transitionType: null, 92 | } 93 | } 94 | 95 | setTitle() { 96 | const { fileName, onSetTitle } = this.props 97 | fileName && onSetTitle && onSetTitle(fileName) 98 | } 99 | 100 | componentDidMount() { 101 | this.setTitle() 102 | } 103 | 104 | componentDidUpdate(prevProps) { 105 | if (!prevProps.minimized && prevProps.maximized && this.props.minimized) { 106 | this.setState({ transitionType: 'FromMaximizedToMinimized' }) 107 | } 108 | else if (prevProps.minimized && !this.props.minimized && this.props.maximized) { 109 | this.setState({ transitionType: 'FromMinimizedToMaximized' }) 110 | } 111 | else if (!prevProps.minimized && this.props.minimized) { 112 | this.setState({ transitionType: 'FromNormalToMinimized' }) 113 | } 114 | else if (prevProps.minimized && !this.props.minimized) { 115 | this.setState({ transitionType: 'FromMinimizedToNormal' }) 116 | } 117 | else if (!prevProps.maximized && this.props.maximized) { 118 | this.setState({ transitionType: 'FromNormalToMaximized' }) 119 | } 120 | else if (prevProps.maximized && !this.props.maximized) { 121 | this.setState({ transitionType: 'FromMaximizedToNormal' }) 122 | } 123 | 124 | if (this.props.fileName !== prevProps.fileName) { 125 | this.setTitle() 126 | } 127 | } 128 | 129 | dragStart = (e) => { 130 | const clickedWindowButton = !!e.target.dataset.button 131 | if (this.state.dragging || this.props.maximized || e.button !== LEFT_MOUSE_BUTTON || clickedWindowButton) { 132 | return 133 | } 134 | 135 | const mouseCoords = { x: e.screenX, y: e.screenY } 136 | this.setState(state => ({ 137 | dragging: true, 138 | dragStart: { 139 | mouseCoords, 140 | geometry: state.geometry, 141 | } 142 | })) 143 | 144 | addEventListener('mousemove', this.onDragMove) 145 | addEventListener('mouseup', this.onDragEnd) 146 | } 147 | 148 | onDragMove = (e) => { 149 | const newX = e.screenX 150 | const newY = e.screenY 151 | this.setState(state => ({ 152 | geometry: { 153 | ...state.geometry, 154 | left: state.dragStart.geometry.left + (newX - state.dragStart.mouseCoords.x), 155 | top: state.dragStart.geometry.top + (newY - state.dragStart.mouseCoords.y), 156 | }, 157 | })) 158 | } 159 | 160 | onDragEnd = () => { 161 | this.setState({ dragging: false }) 162 | 163 | removeEventListener('mousemove', this.onDragMove) 164 | removeEventListener('mouseup', this.onDragEnd) 165 | } 166 | 167 | resizeStart = (e) => { 168 | if (this.state.resizing || this.props.maximized || e.button !== LEFT_MOUSE_BUTTON) { 169 | return 170 | } 171 | e.preventDefault() 172 | 173 | const mouseCoords = { x: e.screenX, y: e.screenY } 174 | this.setState(state => ({ 175 | resizing: true, 176 | dragStart: { 177 | mouseCoords, 178 | geometry: state.geometry, 179 | } 180 | })) 181 | 182 | addEventListener('mousemove', this.onResizeMove) 183 | addEventListener('mouseup', this.onResizeEnd) 184 | } 185 | 186 | onResizeMove = (e) => { 187 | const { minWidth, minHeight } = this.props 188 | const newX = e.screenX 189 | const newY = e.screenY 190 | this.setState(state => { 191 | const draggedWidth = state.dragStart.geometry.width + (newX - state.dragStart.mouseCoords.x) 192 | const draggedHeight = state.dragStart.geometry.height + (newY - state.dragStart.mouseCoords.y) 193 | return { 194 | geometry: { 195 | ...state.geometry, 196 | width: Math.max((minWidth || DEFAULT_MIN_WIDTH), draggedWidth), 197 | height: Math.max((minHeight || DEFAULT_MIN_HEIGHT), draggedHeight), 198 | }, 199 | } 200 | }) 201 | } 202 | 203 | onResizeEnd = () => { 204 | this.setState({ resizing: false }) 205 | 206 | removeEventListener('mousemove', this.onResizeMove) 207 | removeEventListener('mouseup', this.onResizeEnd) 208 | } 209 | 210 | toggleMaximized = (e) => { 211 | e.target.blur() 212 | const { maximized, setMaximized } = this.props 213 | setMaximized && setMaximized(!maximized) 214 | } 215 | 216 | toggleMinimized = (e) => { 217 | e.target.blur() 218 | const { minimized, setMinimized } = this.props 219 | setMinimized && setMinimized(!minimized) 220 | } 221 | 222 | clearTransition = () => { 223 | this.setState({ transitionType: null }) 224 | } 225 | 226 | matchTransition(regex) { 227 | const { transitionType } = this.state 228 | return transitionType && transitionType.match(regex) 229 | } 230 | 231 | render() { 232 | const { 233 | fileName, 234 | title, 235 | icon, 236 | hasFocus, 237 | onRequestClose, 238 | bottomAreaContent, 239 | maximized, 240 | minimized, 241 | minimizable, 242 | resizable, 243 | taskbarItemId, 244 | children, 245 | ...otherProps 246 | } = this.props 247 | 248 | const { 249 | geometry, 250 | transitionType, 251 | } = this.state 252 | 253 | const displayAsMaximized = (maximized && !this.matchTransition(/ToMaximized/)) 254 | || this.matchTransition(/FromMaximized/) 255 | const displayAsMinimized = (minimized && !this.matchTransition(/ToMinimized/)) 256 | || this.matchTransition(/FromMinimized/) 257 | 258 | return 259 | 265 | 266 | {icon && } 267 | 268 | {title || fileName} 269 | 270 | {minimizable && 274 | 275 | } 276 | 277 | {resizable && 281 | 282 | } 283 | 284 | 290 | 291 | 292 | 293 | 294 | 295 | {children} 296 | 297 | 298 | {bottomAreaContent && 299 | {bottomAreaContent} 300 | } 301 | 302 | {resizable && !displayAsMaximized && } 306 | 307 | 308 | {transitionType && } 317 | 318 | } 319 | } 320 | 321 | Window.propTypes = { 322 | initialGeometry: PropTypes.shape({ 323 | left: PropTypes.number, 324 | top: PropTypes.number, 325 | width: PropTypes.number, 326 | height: PropTypes.number, 327 | }), 328 | maximized: PropTypes.bool, 329 | minimized: PropTypes.bool, 330 | minimizable: PropTypes.bool, 331 | resizable: PropTypes.bool, 332 | title: PropTypes.string.isRequired, 333 | icon: PropTypes.string, 334 | hasFocus: PropTypes.bool, 335 | zIndex: PropTypes.number, 336 | bottomAreaContent: PropTypes.node, 337 | setMaximized: PropTypes.func, 338 | setMinimized: PropTypes.func, 339 | minWidth: PropTypes.number, 340 | minHeight: PropTypes.number, 341 | onRequestClose: PropTypes.func, 342 | taskbarItemId: PropTypes.string, 343 | } 344 | 345 | Window.defaultProps = { 346 | minimizable: true, 347 | resizable: true, 348 | } 349 | 350 | export default Window 351 | -------------------------------------------------------------------------------- /src/components/window/WindowTitleBar.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const TitleBar = styled.div` 4 | height: 18px; 5 | width: 100%; 6 | padding: 0 2px; 7 | display: flex; 8 | align-items: center; 9 | background-color: ${({hasFocus}) => hasFocus ? '#000082' : '#808080'}; 10 | color: ${({hasFocus}) => hasFocus ? 'white' : '#c0c0c0'}; 11 | font-weight: bold; 12 | margin-bottom: 2px; 13 | ` 14 | 15 | export const IconImage = styled.img` 16 | width: 16px; 17 | height: 16px; 18 | margin-right: 2px; 19 | image-rendering: pixelated; 20 | image-rendering: -moz-crisp-edges; 21 | ` 22 | 23 | export const TitleWrapper = styled.div` 24 | flex: 1; 25 | overflow: hidden; 26 | white-space: nowrap; 27 | text-overflow: ellipsis; 28 | ` 29 | -------------------------------------------------------------------------------- /src/components/window/WindowTitleBarTransition.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { TitleBar, IconImage, TitleWrapper } from './WindowTitleBar' 5 | import { getWidth, getHeight } from '../../util/getViewport' 6 | 7 | const MAX_Z_INDEX = 2147483647 8 | 9 | const Root = TitleBar.extend` 10 | position: absolute; 11 | z-index: ${MAX_Z_INDEX}; 12 | pointer-events: none; 13 | transition: left .2s linear, top .2s linear, width .2s linear; 14 | ` 15 | 16 | class WindowTitleBarTransition extends Component { 17 | constructor(props) { 18 | super(props) 19 | let geometry 20 | 21 | if (props.type.match(/FromMaximized/)) { 22 | geometry = this.maximizedGeometry() 23 | } else if (props.type.match(/FromMinimized/)) { 24 | geometry = this.minimizedGeometry(props) 25 | } else { 26 | geometry = this.regularGeometry(props) 27 | } 28 | 29 | this.state = { 30 | geometry, 31 | } 32 | 33 | this.transitionCompleted = false 34 | } 35 | 36 | componentDidMount() { 37 | setTimeout(() => { 38 | let geometry 39 | 40 | if (this.props.type.match(/ToMaximized/)) { 41 | geometry = this.maximizedGeometry() 42 | } else if (this.props.type.match(/ToMinimized/)) { 43 | geometry = this.minimizedGeometry(this.props) 44 | } else { 45 | geometry = this.regularGeometry(this.props) 46 | } 47 | 48 | this.setState({ 49 | loaded: true, 50 | geometry, 51 | }) 52 | 53 | // Fallback, in case the CSS `transitionend` event somehow doesn't fire 54 | setTimeout(this.onTransitionEnd, 250) 55 | }, 10) 56 | } 57 | 58 | maximizedGeometry() { 59 | return { 60 | top: 0, 61 | left: 0, 62 | width: getWidth(), 63 | } 64 | } 65 | 66 | minimizedGeometry(props) { 67 | const domNode = document.getElementById(props.taskbarItemId) 68 | if (domNode) { 69 | const rect = domNode.getBoundingClientRect() 70 | const gap = Math.floor((rect.height - 18) / 2) 71 | 72 | return { 73 | top: rect.top + gap, 74 | left: rect.left + gap, 75 | width: rect.width - gap * 2, 76 | } 77 | } else { 78 | const standardTaskbarItemWidth = 160 79 | return { 80 | top: getHeight() - 23, 81 | left: props.windowGeometry.left + props.windowGeometry.width / 2 - (standardTaskbarItemWidth / 2 - 2), 82 | width: standardTaskbarItemWidth - 4, 83 | } 84 | } 85 | } 86 | 87 | regularGeometry(props) { 88 | const { windowGeometry } = props 89 | return { 90 | top: windowGeometry.top + 4, 91 | left: windowGeometry.left + 4, 92 | width: windowGeometry.width - 8, 93 | } 94 | } 95 | 96 | onTransitionEnd = () => { 97 | const { onTransitionEnd } = this.props 98 | const { loaded } = this.state 99 | 100 | if (!this.transitionCompleted && loaded) { 101 | onTransitionEnd && onTransitionEnd() 102 | this.transitionCompleted = true 103 | } 104 | } 105 | 106 | render() { 107 | const { icon, title } = this.props 108 | const { geometry } = this.state 109 | 110 | return ( 111 | 116 | {icon && } 117 | {title} 118 | 119 | ) 120 | } 121 | } 122 | 123 | WindowTitleBarTransition.propTypes = { 124 | windowGeometry: PropTypes.shape({ 125 | left: PropTypes.number, 126 | top: PropTypes.number, 127 | width: PropTypes.number, 128 | }), 129 | onTransitionEnd: PropTypes.func.isRequired, 130 | taskbarItemId: PropTypes.string, 131 | icon: PropTypes.string, 132 | title: PropTypes.string, 133 | type: PropTypes.oneOf([ 134 | 'FromNormalToMinimized', 135 | 'FromMinimizedToNormal', 136 | 'FromNormalToMaximized', 137 | 'FromMaximizedToNormal', 138 | 'FromMinimizedToMaximized', 139 | 'FromMaximizedToMinimized', 140 | ]).isRequired, 141 | } 142 | 143 | export default WindowTitleBarTransition 144 | -------------------------------------------------------------------------------- /src/components/windowmenu/WindowMenu.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import RidgedBox from '../../atoms/RidgedBox' 5 | import WindowMenuItem, { Divider } from './WindowMenuItem' 6 | 7 | import isObject from '../../util/isObject' 8 | 9 | const Root = RidgedBox.extend` 10 | display: flex; 11 | padding: 1px; 12 | ` 13 | 14 | class WindowMenu extends Component { 15 | constructor() { 16 | super() 17 | this.state = { 18 | highlightedItemKey: null, 19 | openedSubMenuItemKey: null, 20 | } 21 | } 22 | 23 | onMouseEnterItem = (itemKey) => () => { 24 | this.setState({ highlightedItemKey: itemKey }) 25 | } 26 | 27 | onLingerItem = (itemKey) => () => { 28 | this.setState({ openedSubMenuItemKey: itemKey }) 29 | } 30 | 31 | generateMenuContent() { 32 | const { items, onItemSelected } = this.props 33 | const { highlightedItemKey, openedSubMenuItemKey } = this.state 34 | 35 | if (!Array.isArray(items)) { 36 | return null 37 | } 38 | return items.map((item, i) => { 39 | if (item === 'divider') { 40 | return 41 | } else if (!isObject(item)) { 42 | return null 43 | } else { 44 | const { label, underline, items, disabled, checked, onSelect } = item 45 | return { 54 | onItemSelected() 55 | onSelect && onSelect(item) 56 | }} 57 | onItemSelected={onItemSelected} 58 | /> 59 | } 60 | }) 61 | } 62 | 63 | render() { 64 | return ( 65 | 66 |
67 | {this.generateMenuContent()} 68 |
69 |
70 | ) 71 | } 72 | } 73 | 74 | WindowMenu.propTypes = { 75 | items: PropTypes.arrayOf(PropTypes.oneOfType([ 76 | PropTypes.object, 77 | PropTypes.string, 78 | ])), 79 | onItemSelected: PropTypes.func, 80 | } 81 | 82 | export default WindowMenu 83 | -------------------------------------------------------------------------------- /src/components/windowmenu/WindowMenuButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled, { css } from 'styled-components' 4 | 5 | import WindowMenu from './WindowMenu' 6 | import MenuOverlay from '../MenuOverlay' 7 | 8 | import underlinedLabel from '../../util/underlinedLabel' 9 | 10 | const Root = styled.div` 11 | position: relative; 12 | display: inline-block; 13 | margin-top: -1px; 14 | margin-bottom: -2px; 15 | ` 16 | 17 | const StyledWindowMenuButton = styled.button` 18 | border: 0; 19 | outline: 0; 20 | height: 18px; 21 | padding: 0 6px; 22 | background-color: transparent; 23 | white-space: nowrap; 24 | 25 | ${({pressed}) => pressed && css` 26 | background-color: #000080; 27 | color: white; 28 | `} 29 | 30 | u { 31 | text-decoration: underline; 32 | } 33 | 34 | ::-moz-focus-inner { 35 | border: 0; 36 | } 37 | ` 38 | 39 | class WindowMenuButton extends Component { 40 | render() { 41 | const { label, underline, items, menuOpen, onMouseEnterButton, onItemSelected, onClick } = this.props 42 | 43 | return this.root = el}> 44 | 49 | {underlinedLabel(label, underline)} 50 | 51 | 52 | {menuOpen && 58 | 59 | } 60 | 61 | } 62 | } 63 | 64 | WindowMenuButton.propTypes = { 65 | label: PropTypes.string.isRequired, 66 | underline: PropTypes.number, 67 | items: PropTypes.array, 68 | menuOpen: PropTypes.bool, 69 | onMouseEnterButton: PropTypes.func, 70 | onItemSelected: PropTypes.func, 71 | onClick: PropTypes.func, 72 | } 73 | 74 | export default WindowMenuButton 75 | -------------------------------------------------------------------------------- /src/components/windowmenu/WindowMenuGroup.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { RootCloseWrapper } from 'react-overlays' 4 | 5 | import WindowToolbar from '../WindowToolbar' 6 | import WindowMenuButton from './WindowMenuButton' 7 | 8 | class WindowMenuGroup extends Component { 9 | constructor() { 10 | super() 11 | this.state = { 12 | openedMenuKey: null, 13 | } 14 | } 15 | 16 | onClickMenuButton = (menuKey) => () => { 17 | this.setState(state => ({ openedMenuKey: state.openedMenuKey === menuKey ? null : menuKey })) 18 | } 19 | 20 | onMouseEnterMenuButton = (menuKey) => () => { 21 | if (this.state.openedMenuKey !== null && this.state.openedMenuKey !== menuKey) { 22 | this.setState({ openedMenuKey: menuKey }) 23 | } 24 | } 25 | 26 | closeWindowMenu = () => { 27 | this.setState({ openedMenuKey: null }) 28 | } 29 | 30 | render() { 31 | let { menus } = this.props 32 | menus = menus || [] 33 | const { openedMenuKey } = this.state 34 | 35 | return ( 36 | 37 | 38 | {menus.map(({label, items, underline}, i) => )} 47 | 48 | 49 | ) 50 | } 51 | } 52 | 53 | WindowMenuGroup.propTypes = { 54 | menus: PropTypes.array, 55 | } 56 | 57 | export default WindowMenuGroup 58 | -------------------------------------------------------------------------------- /src/components/windowmenu/WindowMenuItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled, { css } from 'styled-components' 4 | 5 | import WindowMenu from './WindowMenu' 6 | import LightlyInsetBox from '../../atoms/LightlyInsetBox' 7 | import MenuOverlay from '../MenuOverlay' 8 | 9 | import underlinedLabel from '../../util/underlinedLabel' 10 | 11 | import tick from '../../img/tick.png' 12 | import arrow from '../../img/arrow-right.png' 13 | 14 | const Root = styled.div` 15 | position: relative; 16 | width: 100%; 17 | height: 17px; 18 | display: flex; 19 | align-items: center; 20 | white-space: nowrap; 21 | padding-right: 4px; 22 | 23 | ${({highlighted}) => highlighted && css` 24 | background-color: #000080; 25 | `} 26 | ` 27 | 28 | const Tick = styled.img.attrs({ 29 | src: tick, 30 | draggable: false, 31 | })` 32 | position: absolute; 33 | left: 6px; 34 | filter: ${({highlighted}) => highlighted ? 'invert(100%)' : 'none'}; 35 | ` 36 | 37 | const Label = styled.div` 38 | flex: 1; 39 | max-width: 250px; 40 | margin-left: 23px; 41 | margin-right: 23px; 42 | overflow: hidden; 43 | white-space: nowrap; 44 | text-overflow: ellipsis; 45 | 46 | ${({ highlighted }) => highlighted && css` 47 | color: white; 48 | `} 49 | 50 | ${({ disabled }) => disabled && css` 51 | color: #808080; 52 | `} 53 | ` 54 | 55 | const SubMenuArrow = styled.img` 56 | filter: ${({highlighted}) => highlighted ? 'invert(100%)' : 'none'}; 57 | margin-left: 5px; 58 | ` 59 | 60 | export const Divider = LightlyInsetBox.extend` 61 | height: 2px; 62 | margin: 3px 0; 63 | border-width: 1px 0; 64 | `.withComponent('hr') 65 | 66 | class WindowMenuItem extends Component { 67 | componentDidMount() { 68 | if (this.props.highlighted) { 69 | this.onGainHighlight() 70 | } 71 | } 72 | 73 | componentDidUpdate(prevProps) { 74 | if (!prevProps.highlighted && this.props.highlighted) { 75 | this.onGainHighlight() 76 | } else if (prevProps.highlighted && !this.props.highlighted) { 77 | this.onLoseHighlight() 78 | } 79 | } 80 | 81 | onGainHighlight() { 82 | const { onLinger } = this.props 83 | this.lingerTimeout = setTimeout(() => { 84 | onLinger && onLinger() 85 | delete this.lingerTimeout 86 | }, 500) 87 | } 88 | 89 | onLoseHighlight() { 90 | if (this.lingerTimeout) { 91 | clearTimeout(this.lingerTimeout) 92 | delete this.lingerTimeout 93 | } 94 | } 95 | 96 | onMouseEnter = (e) => { 97 | const { onMouseEnter, disabled } = this.props 98 | !disabled && onMouseEnter && onMouseEnter(e) 99 | } 100 | 101 | onClick = (e) => { 102 | const { items, onLinger, onSelect, disabled } = this.props 103 | if (disabled) { 104 | return 105 | } 106 | onLinger && onLinger(e) 107 | !items && onSelect && onSelect() 108 | } 109 | 110 | componentWillUnmount() { 111 | if (this.lingerTimeout) { 112 | clearTimeout(this.lingerTimeout) 113 | delete this.lingerTimeout 114 | } 115 | } 116 | 117 | render() { 118 | const { 119 | highlighted, 120 | checked, 121 | items, 122 | subMenuOpen, 123 | label, 124 | underline, 125 | onItemSelected, 126 | disabled, 127 | } = this.props 128 | 129 | return ( 130 | { 135 | if (el) { 136 | this.root = el 137 | } 138 | }} 139 | > 140 | {checked && } 141 | 142 | 145 | 146 | {items && } 150 | 151 | {items && 159 | 164 | } 165 | 166 | ) 167 | } 168 | } 169 | 170 | WindowMenuItem.propTypes = { 171 | highlighted: PropTypes.bool, 172 | checked: PropTypes.bool, 173 | items: PropTypes.array, 174 | subMenuOpen: PropTypes.bool, 175 | label: PropTypes.string.isRequired, 176 | underline: PropTypes.number, 177 | disabled: PropTypes.bool, 178 | onItemSelected: PropTypes.func, 179 | onLinger: PropTypes.func, 180 | onSelect: PropTypes.func, 181 | onMouseEnter: PropTypes.func, 182 | } 183 | 184 | export default WindowMenuItem 185 | -------------------------------------------------------------------------------- /src/img/arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/arrow-down.png -------------------------------------------------------------------------------- /src/img/arrow-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/arrow-left.png -------------------------------------------------------------------------------- /src/img/arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/arrow-right.png -------------------------------------------------------------------------------- /src/img/arrow-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/arrow-up.png -------------------------------------------------------------------------------- /src/img/border-button-inset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/border-button-inset.png -------------------------------------------------------------------------------- /src/img/border-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/border-button.png -------------------------------------------------------------------------------- /src/img/border-inset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/border-inset.png -------------------------------------------------------------------------------- /src/img/border-strong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/border-strong.png -------------------------------------------------------------------------------- /src/img/border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/border.png -------------------------------------------------------------------------------- /src/img/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/close.png -------------------------------------------------------------------------------- /src/img/dropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/dropdown.png -------------------------------------------------------------------------------- /src/img/icon-default-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/icon-default-24.png -------------------------------------------------------------------------------- /src/img/icon-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/icon-default.png -------------------------------------------------------------------------------- /src/img/justify-center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/justify-center.png -------------------------------------------------------------------------------- /src/img/justify-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/justify-left.png -------------------------------------------------------------------------------- /src/img/light-border-inset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/light-border-inset.png -------------------------------------------------------------------------------- /src/img/light-border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/light-border.png -------------------------------------------------------------------------------- /src/img/logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/logo-small.png -------------------------------------------------------------------------------- /src/img/maximize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/maximize.png -------------------------------------------------------------------------------- /src/img/minimize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/minimize.png -------------------------------------------------------------------------------- /src/img/pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/pointer.png -------------------------------------------------------------------------------- /src/img/resize-handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/resize-handle.png -------------------------------------------------------------------------------- /src/img/resize-se.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/resize-se.png -------------------------------------------------------------------------------- /src/img/ruler-marker-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/ruler-marker-1.png -------------------------------------------------------------------------------- /src/img/ruler-marker-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/ruler-marker-2.png -------------------------------------------------------------------------------- /src/img/ruler-marker-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/ruler-marker-3.png -------------------------------------------------------------------------------- /src/img/ruler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/ruler.png -------------------------------------------------------------------------------- /src/img/scrollbar-track.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/scrollbar-track.png -------------------------------------------------------------------------------- /src/img/semi-busy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/semi-busy.png -------------------------------------------------------------------------------- /src/img/text-cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/text-cursor.png -------------------------------------------------------------------------------- /src/img/tick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/tick.png -------------------------------------------------------------------------------- /src/img/unmaximize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/src/img/unmaximize.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import RidgedButton from './atoms/RidgedButton' 2 | 3 | import Desktop from './components/Desktop' 4 | import Dialog from './apps/Dialog' 5 | import FileBrowser from './apps/FileBrowser' 6 | import IconArea from './components/IconArea' 7 | import IconRegular from './components/IconRegular' 8 | import Notepad from './apps/Notepad' 9 | import Shell from './components/Shell' 10 | import StartButton from './components/startmenu/StartButton' 11 | import SystemTray from './components/SystemTray' 12 | import Taskbar from './components/Taskbar' 13 | import TaskbarItem from './components/TaskbarItem' 14 | import Window from './components/window/Window' 15 | import WindowLayer from './components/WindowLayer' 16 | import WordPad from './apps/WordPad' 17 | import defaultIcon from './img/icon-default.png' 18 | 19 | export { 20 | RidgedButton, 21 | Desktop, 22 | Dialog, 23 | FileBrowser, 24 | IconArea, 25 | IconRegular, 26 | Notepad, 27 | Shell, 28 | StartButton, 29 | SystemTray, 30 | Taskbar, 31 | TaskbarItem, 32 | Window, 33 | WindowLayer, 34 | WordPad, 35 | defaultIcon, 36 | } 37 | -------------------------------------------------------------------------------- /src/util/focusButtonListener.js: -------------------------------------------------------------------------------- 1 | export default function focusButtonListener(component, callback, keySelector, eventType = 'keydown') { 2 | let filteredCallback = (e) => { 3 | if ( 4 | component.props.hasFocus && 5 | ( 6 | !keySelector || 7 | keySelector.hasOwnProperty('key') && keySelector.key === e.key || 8 | keySelector.hasOwnProperty('which') && keySelector.which === e.which || 9 | keySelector.hasOwnProperty('code') && keySelector.code === e.code 10 | ) 11 | ) { 12 | callback(e) 13 | } 14 | } 15 | 16 | addEventListener(eventType, filteredCallback) 17 | console.log('added event listener') 18 | 19 | return { 20 | remove: () => { 21 | removeEventListener(eventType, filteredCallback) 22 | console.log('removed event listener') 23 | filteredCallback = null 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/util/getViewport.js: -------------------------------------------------------------------------------- 1 | export function getWidth() { 2 | if (!document) { 3 | return 0 4 | } 5 | 6 | return Math.max(document.documentElement.clientWidth, window.innerWidth || 0) 7 | } 8 | 9 | export function getHeight() { 10 | if (!document) { 11 | return 0 12 | } 13 | 14 | return Math.max(document.documentElement.clientHeight, window.innerHeight || 0) 15 | } 16 | -------------------------------------------------------------------------------- /src/util/isObject.js: -------------------------------------------------------------------------------- 1 | export default function isObject (value) { 2 | return value && typeof value === 'object' && value.constructor === Object; 3 | } 4 | -------------------------------------------------------------------------------- /src/util/underlinedLabel.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | 3 | export default function underlinedLabel(label, underline) { 4 | if (typeof underline === 'undefined') { 5 | return label 6 | } 7 | 8 | const piece1 = label.substr(0, underline) 9 | const piece2 = label.substr(underline, 1) 10 | const piece3 = label.substr(underline + 1) 11 | 12 | return {piece1}{piece2}{piece3} 13 | } 14 | -------------------------------------------------------------------------------- /stories/helpers/WindowStateContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import { 4 | Desktop, 5 | Shell, 6 | Taskbar, 7 | TaskbarItem, 8 | Window, 9 | WindowLayer, 10 | } from '../../src' 11 | 12 | class WindowStateContainer extends Component { 13 | constructor() { 14 | super() 15 | this.state = { 16 | minimized: false, 17 | maximized: false, 18 | } 19 | } 20 | 21 | setMinimized = (isMinimized) => { 22 | this.setState({ minimized: isMinimized }) 23 | } 24 | 25 | setMaximized = (isMaximized) => { 26 | this.setState({ maximized: isMaximized }) 27 | } 28 | 29 | render() { 30 | const { maximized, minimized } = this.state 31 | 32 | const onClick = () => this.setMinimized(!minimized) 33 | 34 | return ( 35 | 36 | 37 | 38 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ) 54 | } 55 | } 56 | 57 | export default WindowStateContainer 58 | -------------------------------------------------------------------------------- /stories/icon-folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Middlerun/reactows-95/d327aeced2c30938856111c8d930ea1558400f92/stories/icon-folder.png -------------------------------------------------------------------------------- /stories/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { action } from '@storybook/addon-actions' 4 | import { 5 | Desktop, 6 | Dialog, 7 | FileBrowser, 8 | IconArea, 9 | IconRegular, 10 | Notepad, 11 | RidgedButton, 12 | Shell, 13 | Taskbar, 14 | TaskbarItem, 15 | Window, 16 | WindowLayer, 17 | WordPad, 18 | defaultIcon, 19 | } from '../src' 20 | import WindowStateContainer from './helpers/WindowStateContainer' 21 | 22 | import folderIcon from './icon-folder.png' 23 | 24 | function startMenuItemSelected(item) { 25 | action(item.label + ' selected')() 26 | } 27 | 28 | const recursiveStartMenuFolder = { label: 'Recursive Folder' } 29 | recursiveStartMenuFolder.subMenuItems = [recursiveStartMenuFolder] 30 | 31 | const startMenuItems = [ 32 | { label: 'Programs', subMenuItems: [ 33 | { label: 'Program 1', onSelect: startMenuItemSelected }, 34 | { label: 'Program 2', onSelect: startMenuItemSelected }, 35 | { label: 'Folder', subMenuItems: [ 36 | { label: 'herp', onSelect: startMenuItemSelected }, 37 | { label: 'derp', onSelect: startMenuItemSelected }, 38 | ] }, 39 | { label: 'Empty Folder', subMenuItems: [] }, 40 | { label: 'Big Folder', subMenuItems: (new Array(20)).fill(1).map((val, i) => ( 41 | { label: `Thing ${i+1}`, onSelect: startMenuItemSelected } 42 | )) }, 43 | recursiveStartMenuFolder, 44 | ] }, 45 | { label: 'Documents', onSelect: startMenuItemSelected }, 46 | { label: 'Settings', onSelect: startMenuItemSelected }, 47 | { label: 'Find', onSelect: startMenuItemSelected }, 48 | { label: 'Help', onSelect: startMenuItemSelected }, 49 | { label: 'Run...', onSelect: startMenuItemSelected }, 50 | 'divider', 51 | { label: 'Shut down...', onSelect: startMenuItemSelected }, 52 | ] 53 | 54 | 55 | storiesOf('Shell', module) 56 | .add('with a window', () => ( 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | {(new Array(30)).fill(1).map((val, i) => )} 67 | 68 | 71 |

Content!

72 |

Here's some content. Here's some content. Here's some content. Here's some content. Here's some content. Here's some content.

73 | Obligatory cat photo 74 |
75 |
76 |
77 | 78 | 79 | 80 | 81 | 82 |
83 | )) 84 | .add('with lots of icons', () => ( 85 | 86 | 87 | 88 | {(new Array(30)).fill(1).map((val, i) => )} 89 | 90 | 91 | 92 | 93 | )) 94 | .add('with lots of taskbar items', () => ( 95 | 96 | 97 | 98 | 99 | 100 | {(new Array(30)).fill(1).map((val, i) => )} 101 | 102 | 103 | )) 104 | .add('with semi-busy state', () => ( 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | )) 113 | 114 | storiesOf('Window', module) 115 | .add('standard', () => ( 116 | 117 | 118 | 119 | 120 | Content 121 | 122 | 123 | 124 | 125 | )) 126 | .add('non-resizable', () => ( 127 | 128 | 129 | 130 | 131 | Content 132 | 133 | 134 | 135 | 136 | )) 137 | .add('maximized', () => ( 138 | 139 | 140 | 141 | 142 | Content 143 | 144 | 145 | 146 | 147 | )) 148 | .add('minimizable and maximizable', () => ( 149 | 150 | )) 151 | 152 | storiesOf('RidgedButton', module) 153 | .add('normal', () => ( 154 | 155 | Button 156 | 157 | )) 158 | .add('strong', () => ( 159 | 160 | Button 161 | 162 | )) 163 | .add('disabled', () => ( 164 | 165 | Button 166 | 167 | )) 168 | 169 | storiesOf('Taskbar', module) 170 | .add('empty', () => ( 171 | 172 | )) 173 | 174 | storiesOf('TaskbarItem', module) 175 | .add('normal', () => ( 176 | 177 | )) 178 | .add('focused', () => ( 179 | 180 | )) 181 | 182 | storiesOf('Window types', module) 183 | .add('generic Window', () => ( 184 | 185 | 186 | 187 | )) 188 | .add('FileBrowser', () => ( 189 | 190 | 191 | 192 | )) 193 | .add('WordPad', () => ( 194 | 195 | 196 | {(new Array(100)).fill(1).map((val, i) =>

197 | All work and no play makes Jack a dull boy 198 |

)} 199 |
200 |
201 | )) 202 | .add('Notepad', () => ( 203 | 204 | 205 | {'All work and no play makes Jack a dull boy\n'.repeat(100)} 206 | dlkfjal;sdkjf;laksjdl;fkja;sldkfjl;aksdjf;klajsd 207 | 208 | 209 | )) 210 | .add('Dialog', () => ( 211 | 212 | 213 | There was an error 214 | 215 | 216 | )) 217 | 218 | storiesOf('Scroll bars', module) 219 | .add('horizontal and vertical', () => ( 220 | 221 |
222 |
223 | content 224 |
225 |
226 |
227 | )) 228 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const pkg = require('./package.json') 3 | const path = require('path') 4 | const libraryName = pkg.name 5 | 6 | module.exports = { 7 | entry: path.join(__dirname, "./src/index.js"), 8 | output: { 9 | path: path.join(__dirname, './dist'), 10 | filename: 'index.js', 11 | library: libraryName, 12 | libraryTarget: 'umd', 13 | publicPath: '/dist/', 14 | umdNamedDefine: true, 15 | }, 16 | devtool: "source-map", 17 | optimization: { 18 | minimize: false, 19 | }, 20 | node: { 21 | net: 'empty', 22 | tls: 'empty', 23 | dns: 'empty', 24 | }, 25 | module: { 26 | rules : [ 27 | { 28 | test: /\.(png|svg|jpg|gif)$/, 29 | use: [ 30 | { 31 | loader: 'url-loader', 32 | options:{ 33 | fallback: "file-loader", 34 | name: "[name][md5:hash].[ext]", 35 | outputPath: 'assets/', 36 | publicPath: '/assets/', 37 | }, 38 | }, 39 | ], 40 | }, 41 | { 42 | test: /\.(js|jsx)$/, 43 | use: ["babel-loader"], 44 | include: path.resolve(__dirname, "src"), 45 | exclude: /node_modules/, 46 | }, 47 | ], 48 | }, 49 | resolve: { 50 | alias: { 51 | 'react': path.resolve(__dirname, './node_modules/react') , 52 | 'react-dom': path.resolve(__dirname, './node_modules/react-dom'), 53 | 'react-overlays': path.resolve(__dirname, './node_modules/react-overlays'), 54 | 'styled-components': path.resolve(__dirname, './node_modules/styled-components'), 55 | 'prop-types': path.resolve(__dirname, './node_modules/prop-types'), 56 | 'assets': path.resolve(__dirname, 'assets'), 57 | }, 58 | }, 59 | externals: { 60 | react: { 61 | commonjs: "react", 62 | commonjs2: "react", 63 | amd: "React", 64 | root: "React" 65 | }, 66 | "react-dom": { 67 | commonjs: "react-dom", 68 | commonjs2: "react-dom", 69 | amd: "ReactDOM", 70 | root: "ReactDOM" 71 | }, 72 | "react-overlays": "react-overlays", 73 | "styled-components": "styled-components", 74 | "prop-types": "prop-types", 75 | }, 76 | } 77 | --------------------------------------------------------------------------------