├── .gitignore
├── LICENSE
├── README.md
├── api
└── index.js
├── bin
└── whiteroom
├── index.js
├── logo.svg
├── package-lock.json
├── package.json
├── src
├── client
│ ├── api.js
│ ├── app.vue
│ ├── index.js
│ ├── layouts
│ │ ├── centered.vue
│ │ ├── fluid.vue
│ │ ├── index.js
│ │ └── simple.vue
│ ├── store.js
│ └── types.js
├── connector.js
└── server
│ ├── app.vue
│ ├── components
│ ├── agents
│ │ ├── agent.vue
│ │ ├── boolean.vue
│ │ ├── enum.vue
│ │ ├── index.js
│ │ ├── number.vue
│ │ ├── range.vue
│ │ ├── text.vue
│ │ └── trigger.vue
│ ├── group.vue
│ └── search.vue
│ ├── index.js
│ ├── modules
│ ├── agents.vue
│ ├── devices.vue
│ ├── frame.vue
│ ├── headline.vue
│ ├── layout.vue
│ ├── log.vue
│ ├── navigation.vue
│ ├── panel.vue
│ ├── rotate.vue
│ ├── scale.vue
│ └── settings.vue
│ ├── router.js
│ ├── store.js
│ ├── transitions
│ └── expand.vue
│ └── vars.styl
├── webpack.config.js
└── www
├── index.html
└── microscope.html
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Julien Barbay
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Vue Whiteroom
4 |
5 | ### white what?
6 |
7 | `vue-whiteroom` is a javascript web-application which aim is to display vue components in a safely sealed environment where they can be manipulated while developing them.
8 |
9 | > I personally think that the "whiteroom" vocabulary is more appropriate, since nowadays not so many people use storybook to display user stories, rather as a safe way to isolate components, as if they were bacterias to observe ; which explains the name of the project. The UI may not look fancy but the main idea behind it is to provide the maximum space available to display your component, as it is the main function of this application. Panels come on overlay and are foldable on purpose ; that said, i'd be more than happy to have some inputs to improve.
10 |
11 | #### features
12 |
13 | - [x] hot reload
14 | - [x] _experiments_ (like "story objects") as plain Vue files
15 | - [x] automatically generated knobs from `props` inspection
16 | - [x] `props` can be extended to enhance knobs easily
17 | - [x] multiple _layouts_
18 | - [x] multiple _devices_
19 | - [x] extensible _devices_ and _layouts_ declaration
20 | - [x] multi level navigation
21 |
22 | ### why not reuse?
23 |
24 | while [storybook](https://github.com/storybooks/storybook) is an incredible project, it has been written with React for React. The abstraction layer is great, but doesn't work well with Vue.js (as for now, at the very least). An other project, [vue-play](https://github.com/vue-play/vue-play), was completing the task, but lacked of "knobs", and also doesn't look maintained for a year or so.
25 |
26 | ### under the hood?
27 |
28 | This is a basic "storybook" application, so the architecture is quite similar. The application is split in two parts, one rendered in the main frame, and the second one rendered in an iframe.
29 |
30 | Your components are rendered in the iframe, and fed with property values from the url of the iframe. The main application has some knobs which are serialized and passed to the iframe's url, and voila.
31 |
32 | ### how to run?
33 |
34 | 1. Install it from npm `npm install --save-dev vue-whiteroom`
35 | 2. Add `./node_modules/vue-whiteroom/bin/whiteroom` to your npm scripts
36 | 3. Use the default entrypoint `./experiments` in your project
37 |
38 | A basic entrypoint looks like this :
39 |
40 | ```js
41 | import { inspect } from 'vue-whiteroom/api'
42 |
43 | import Demo1 from './demo1.vue'
44 | inspect(Demo1)
45 | ```
46 |
47 | ### `vue-whiteroom/api` functions
48 |
49 | - `inspect(VueObject, { path = VueObject.__file__, group = '', name = path, description = null })` : this function registers a new object to be inspected. If you come from other projects such as vue-play or storybook, it's almost similar as `.add(() => VueObject)`. The "group" property provides a simple way to add foldable navigation by categories.
50 |
51 | - `device(name, css)` : this function adds the `name` device in the devices definition. `css` must contain at least `width` and `height` as normal css declaration.
52 |
53 | - `layout(name, VueObject)` : this function adds the `name` layout in the list of available layouts. see next chapter about how to write a proper layout (there's a trick)
54 |
55 | - `log(...parameters)` : this function will pass `parameters` to the cute log box, instead of using `console.log`. It's not overriden by default on purpose, but you can do it so easily it's almost mandatory.
56 |
57 | ```
58 | import { log } from 'vue-whiteroom/api'
59 | const _log = console.log;
60 | console.log = function() {
61 | log(Array.from(arguments))
62 | _log.apply(console, arguments)
63 | }
64 | ```
65 |
66 | - `wrap(VueComponent)` : this function will return an auto-generated Vue file to handle your component. It will pass props along (but can't really guess for enums) ; injections will be passed, events will be _all_ listened to, and public functions (not starting with `_` or `$`) will be automatically registered as trigger.
67 |
68 | ### configure my experiment
69 |
70 | A simple experiment should be a Vue file rendering at least an element.
71 |
72 | ```
73 |
74 |
75 |
76 |
77 |
78 |
79 |
111 | ```
112 |
113 | - Only the default slot is customizable (for now). It is available as a string.
114 |
115 | - Properties will be automatically parsed and will match type and default value accordingly. If you want to customize the property to limit the values to a limited set, you can use a special `enum` property and your whiteroom will display a `` box instead of the corresponding input.
116 |
117 | ```
118 |
119 |
120 | {{ label }}
121 |
122 |
123 |
124 |
145 |
146 |
147 | ```
148 |
149 | - If you wish to use injections, they are also parsed and available automatically.
150 |
151 | ```
152 |
153 |
154 | {{ $t(_button_label) }}
155 |
156 |
157 |
158 |
187 | ```
188 |
189 | - If you want to call methods on your experiment, all _public_ methods (non `$`, `_`) can be automatically called. It should be noted that no arguments can be passed.
190 |
191 | ```
192 |
193 |
194 | {{ $t(_button_label) }}
195 |
196 |
197 |
198 |
224 | ```
225 |
226 |
227 | ### layouts
228 |
229 | The real component will be passed to you in a `proxy` prop, along with props. Injections are automatically resolved, so there's no so much other things to care about.
230 |
231 | You'll need to make it `abstract` and `functional` to avoid messing with parenting and connections with the rest of the application. After that, render the child as many times as necessary in the `render(h)` function. Don't forget to pass `children` along, as it contains the default `slot` string.
232 |
233 | Here's the most basic example :
234 |
235 | ```
236 |
248 | ```
249 |
250 | Or, if you wanted something more sophisticated :
251 |
252 | ```
253 |
267 |
268 |
285 | ```
286 |
--------------------------------------------------------------------------------
/api/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | const client = (command, parameters, context = window) => {
4 | if (!context.__whiteroom__)
5 | context.__whiteroom__ = [{ command, parameters }]
6 | else if (Array.isArray(context.__whiteroom__))
7 | context.__whiteroom__.push({ command, parameters })
8 | else if (command in context.__whiteroom__)
9 | context.__whiteroom__[command](parameters)
10 | }
11 |
12 | export const inspect = (experiment, { path, group, ...parameters } = {}) => {
13 | if (experiment.__file && !path)
14 | path = experiment.__file.replace(/.vue$/, '')
15 |
16 | if (!group)
17 | group = ''
18 |
19 | client('inspect', { path, experiment, group, ...parameters })
20 | }
21 |
22 | export const device = (name, dimensions) => client('device', { name, dimensions })
23 | export const layout = (name, file) => client('layout', { name, file })
24 |
25 | export const log = (...parameters) => client('log', parameters)
26 |
27 | export const wrap = component => {
28 | return {
29 | abstract: true,
30 | functional: true,
31 |
32 | inject: component.inject,
33 | props: component.props,
34 |
35 | triggers: component.triggers || Object
36 | .keys(component.methods || {})
37 | .filter(method => !method.startsWith('_') && !method.startsWith('$'))
38 | .reduce((object, method) => ({ ...object, [method]: {} }), {}),
39 |
40 | render(h, { props, parent, children }) {
41 | const { $store } = parent
42 | const child = h(component, { props }, children)
43 |
44 | // we use internal Vue insert hook to get into the instance
45 | const insert = child.data.hook.insert;
46 | child.data.hook.insert = function(vnode) {
47 | const vm = vnode.componentInstance;
48 |
49 | // and hijack $emit to add store dispatch and event pop in the console
50 | const emit = vm.$emit;
51 | vm.$emit = function() {
52 | const event = Array.from(arguments)
53 | $store.dispatch('log', ['[EVENT TRIGGER]', event.shift(), ...event])
54 | return emit.apply(vm, arguments)
55 | }
56 |
57 | return insert(vnode)
58 | }
59 |
60 | return child
61 | },
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/bin/whiteroom:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const { existsSync } = require('fs')
3 | const { resolve } = require('path')
4 |
5 | // convenient argv
6 | let options = require('minimist')(process.argv.slice(2))
7 |
8 | // ignore argv if config file is here or provided
9 | if (existsSync(resolve(process.env.PWD, '.whiteroomrc')))
10 | options = require(resolve(process.env.PWD, '.whiteroomrc'))
11 | else if (!!options.config)
12 | options = require(resolve(process.env.PWD, options.config))
13 |
14 | const { entrypoint, host, port } = options
15 |
16 | require('../index')({ entrypoint, host, port })
17 |
18 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path')
2 | const Webpack = require('webpack')
3 | const Server = require('webpack-dev-server')
4 |
5 | module.exports = ({ entrypoint, host, port }) => {
6 | const config = require('./webpack.config')
7 |
8 | if (!!entrypoint)
9 | config.entry.experiments = resolve(process.env.PWD, entrypoint)
10 |
11 | if (!!host)
12 | config.devServer.host = host
13 |
14 | if (!!port)
15 | config.devServer.port = parseInt(port)
16 |
17 | Server.addDevServerEntrypoints(config, config.devServer)
18 |
19 | const compiler = Webpack(config);
20 |
21 | const server = new Server(compiler, config.devServer);
22 |
23 | server.listen(
24 | config.devServer.port, config.devServer.host,
25 | () => {
26 | console.log(`Starting server on http://${config.devServer.host}:${config.devServer.port}`);
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-whiteroom",
3 | "version": "1.3.6",
4 | "description": "storybook copycat with Vue.js in mind",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "MIT",
11 | "dependencies": {
12 | "@babel/core": "^7.1.2",
13 | "@babel/preset-env": "^7.1.0",
14 | "babel-loader": "^8.0.4",
15 | "clean-terminal-webpack-plugin": "^1.1.0",
16 | "clean-webpack-plugin": "^0.1.19",
17 | "css-loader": "^1.0.0",
18 | "gsap": "^2.0.2",
19 | "html-webpack-plugin": "^3.2.0",
20 | "minimist": "^1.2.0",
21 | "query-string": "^6.2.0",
22 | "stylus": "^0.54.5",
23 | "stylus-loader": "^3.0.2",
24 | "vue": "^2.5.17",
25 | "vue-loader": "^15.4.2",
26 | "vue-router": "^3.0.1",
27 | "vue-style-loader": "^4.1.2",
28 | "vue-template-compiler": "^2.5.17",
29 | "vuex": "^3.0.1",
30 | "webpack": "^4.20.2",
31 | "webpack-dev-server": "^3.4.1"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/client/api.js:
--------------------------------------------------------------------------------
1 | export default api => {
2 | // execute message cue
3 | if (Array.isArray(window.__whiteroom__)) {
4 | window.__whiteroom__
5 | .filter(({ command }) => command in api)
6 | .forEach(({ command, parameters }) => api[command](parameters))
7 | }
8 |
9 | // deliver api to userland
10 | window.__whiteroom__ = api
11 | }
12 |
--------------------------------------------------------------------------------
/src/client/app.vue:
--------------------------------------------------------------------------------
1 |
112 |
113 |
115 |
--------------------------------------------------------------------------------
/src/client/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import store from './store'
3 |
4 | import App from './app.vue'
5 |
6 | new Vue({
7 | store,
8 | render: h => h(App)
9 | }).$mount("#microscope")
10 |
--------------------------------------------------------------------------------
/src/client/layouts/centered.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
25 |
--------------------------------------------------------------------------------
/src/client/layouts/fluid.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
52 |
53 |
--------------------------------------------------------------------------------
/src/client/layouts/index.js:
--------------------------------------------------------------------------------
1 | import simple from './simple.vue'
2 | import centered from './centered.vue'
3 | import fluid from './fluid.vue'
4 |
5 | export default {
6 | centered, fluid, simple
7 | }
8 |
--------------------------------------------------------------------------------
/src/client/layouts/simple.vue:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/src/client/store.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue"
2 | import Vuex from "vuex"
3 | import qs from 'query-string'
4 |
5 | import connect from '../connector'
6 | import expose from './api'
7 |
8 | import { convert } from './types'
9 |
10 | import layouts from './layouts'
11 |
12 | Vue.use(Vuex);
13 |
14 | // reactive routing
15 | window.addEventListener('hashchange', () => store.commit('hash', window.location.hash))
16 |
17 | // whiteroom <-> microscope communication
18 | const whiteroom = connect(window.parent)
19 |
20 | whiteroom.on(({ command, parameters }) => (
21 | command in store._mutations ? store.commit(command, parameters) : console.log(`${command} not found`)
22 | ))
23 |
24 | // pass the list of layouts to whiteroom
25 | whiteroom.emit('layouts', Object.keys(layouts))
26 |
27 | // userland api
28 | expose({
29 | inspect: parameters => {
30 | store.commit('register', parameters)
31 |
32 | const { experiment, ...others } = parameters
33 | let { mixins, props, inject, events, triggers, methods } = experiment
34 |
35 | // merge mixins for props
36 | if (mixins)
37 | props = mixins.reduce((props, mixin) => ({ ...props, ...mixin.props }), props)
38 |
39 | // cannot clone validator functions
40 | if (props)
41 | Object.values(props).forEach(prop => delete prop.validator)
42 |
43 | // use public methods as triggers
44 | if (methods)
45 | triggers = Object
46 | .keys(methods)
47 | .filter(method => !method.startsWith('$') && !method.startsWith('_'))
48 | .map(method => ({ [method]: {} }))
49 | .reduce((triggers, method) => ({ ...triggers, ...method }), triggers || {})
50 |
51 | // pass the command upstairs
52 | whiteroom.emit('register', {
53 | ...others,
54 |
55 | // cast away prop types
56 | agents: Object.entries(props || {})
57 | .map(([name, def]) => ({
58 | ...def, name, type: convert(def)
59 | })),
60 |
61 | inject: Object.entries(inject || {})
62 | .map(([name, def]) => ({ ...def, name })),
63 |
64 | events: events || [],
65 |
66 | triggers: Object.entries(triggers || {})
67 | .map(([name, def]) => ({ ...def, name })),
68 | })
69 | },
70 |
71 | device: ({ name, dimensions }) => {
72 | whiteroom.emit('devices', { [name]: dimensions })
73 | },
74 |
75 | layout: ({ name, file }) => {
76 | store.commit('layouts', { [name]: file })
77 | whiteroom.emit('layouts', [name])
78 | },
79 |
80 | log: parameters => {
81 | whiteroom.emit('log', parameters)
82 | }
83 | })
84 |
85 | // global scale helper function
86 | const scale = fontsize => document
87 | .querySelectorAll('[scalable]')
88 | .forEach(el => el.style.fontSize = fontsize !== 1 ? `${fontsize}em` : null)
89 |
90 | // store
91 | const store = new Vuex.Store({
92 | state: {
93 | layouts: { ...layouts },
94 | layout: Object.values(layouts).shift(),
95 |
96 | db: {},
97 | path : '',
98 | query: {},
99 |
100 | trigger: '',
101 | fontsize: 1,
102 | },
103 |
104 | mutations: {
105 | hash(state, hash = '') {
106 | const path = hash
107 | .split('?')
108 | .shift()
109 | .replace(/^#\//, '')
110 |
111 | const query = hash.indexOf('?') !== -1
112 | ? qs.parse(hash.substr(hash.indexOf('?')))
113 | : {}
114 |
115 | if (state.path !== path) {
116 | state.path = path
117 | state.query = { ...query }
118 | }
119 | else {
120 | state.query = { ...state.query, ...query }
121 | }
122 |
123 | scale(state.fontsize)
124 | },
125 |
126 | register(state, { path, experiment, ...parameters }) {
127 | state.db = { ...state.db, [path]: experiment }
128 | },
129 |
130 | layouts(state, payload) {
131 | state.layouts = { ...state.layouts, ...payload }
132 | },
133 |
134 | layout(state, name = '') {
135 | if (name in state.layouts) {
136 | state.layout = state.layouts[name]
137 | setTimeout(scale, 0, state.fontsize)
138 | }
139 | },
140 |
141 | scale: (state, fontsize) => {
142 | state.fontsize = fontsize
143 | scale(state.fontsize)
144 | },
145 |
146 | trigger(state, name = '') {
147 | state.trigger = name
148 | },
149 | },
150 |
151 | actions: {
152 | log(context, parameters) {
153 | whiteroom.emit('log', parameters)
154 | }
155 | }
156 | });
157 |
158 | // initialize store hash
159 | store.commit('hash', window.location.hash)
160 |
161 | export default store;
162 |
--------------------------------------------------------------------------------
/src/client/types.js:
--------------------------------------------------------------------------------
1 | /*
2 | this converts a typed type of a prop to a string type definition,
3 | because we cannot serialize "String" or any function to pass in
4 | postMessage...
5 | */
6 |
7 | export const convert = definition => {
8 | if (!!definition.enum)
9 | return 'enum'
10 |
11 | switch(definition.type) {
12 | case Number:
13 | return definition.range ? 'range' : 'number'
14 |
15 | case Boolean:
16 | return 'boolean'
17 |
18 | case Date:
19 | return 'date'
20 |
21 | default:
22 | case String:
23 | return 'text'
24 | }
25 | }
26 |
27 | /*
28 | values come from querystring,
29 | so we should consider string as input value
30 | and output a casted value according to property definition
31 | */
32 |
33 | export const cast = (definition, value) => {
34 | if (definition.type === Number)
35 | return parseFloat(value)
36 | else if (definition.type === Date)
37 | return new Date(value)
38 | else if (definition.type === Boolean)
39 | return value === 'true'
40 |
41 | return value
42 | }
43 |
--------------------------------------------------------------------------------
/src/connector.js:
--------------------------------------------------------------------------------
1 | export default bridge => {
2 | let listeners = []
3 |
4 | window.addEventListener('message', event => {
5 | if (event.data.whiteroom) {
6 | listeners.forEach(({ cb }) => cb(event.data))
7 | listeners = listeners.filter(l => l.i === -1 || --l.i > 0)
8 | }
9 | })
10 |
11 | return {
12 | bridge,
13 |
14 | on(cb) {
15 | listeners.push({ cb, i: -1 })
16 | return this
17 | },
18 |
19 | once(cb) {
20 | listeners.push({ cb, i: 1 })
21 | return this
22 | },
23 |
24 | off(cb) {
25 | const i = listeners.find(l => l.cb === cb)
26 |
27 | if (i !== -1)
28 | listeners.splice(i, 1)
29 |
30 | return this
31 | },
32 |
33 | emit(command, parameters) {
34 | if (!this.bridge)
35 | return
36 |
37 | try {
38 | this.bridge.postMessage({
39 | whiteroom: true, command, parameters,
40 | }, window.origin)
41 | }
42 | catch(err) { console.log(err) }
43 |
44 | return this
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/server/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
34 |
35 |
53 |
--------------------------------------------------------------------------------
/src/server/components/agents/agent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ name }}
6 |
7 |
8 |
9 |
10 |
11 | {{ description }}
12 |
13 |
14 |
15 |
16 |
28 |
29 |
87 |
--------------------------------------------------------------------------------
/src/server/components/agents/boolean.vue:
--------------------------------------------------------------------------------
1 |
2 | $emit('input', e.target.checked.toString())" v-bind="$attrs" />
3 |
4 |
5 |
15 |
16 |
21 |
--------------------------------------------------------------------------------
/src/server/components/agents/enum.vue:
--------------------------------------------------------------------------------
1 |
2 | $emit('input', e.target.value)" v-bind="$attrs">
3 | {{ v }}
4 |
5 |
6 |
7 |
22 |
--------------------------------------------------------------------------------
/src/server/components/agents/index.js:
--------------------------------------------------------------------------------
1 | import boolean from './boolean.vue'
2 | import number from './number.vue'
3 | import range from './range.vue'
4 | import text from './text.vue'
5 |
6 | import _enum from './enum.vue'
7 |
8 | export default {
9 | boolean,
10 | number,
11 | range,
12 | text,
13 | 'enum': _enum
14 | }
15 |
--------------------------------------------------------------------------------
/src/server/components/agents/number.vue:
--------------------------------------------------------------------------------
1 |
2 | $emit('input', e.target.value)" v-bind="$attrs" />
3 |
4 |
5 |
15 |
--------------------------------------------------------------------------------
/src/server/components/agents/range.vue:
--------------------------------------------------------------------------------
1 |
2 | $emit('input', e.target.value)" v-bind="$attrs" />
3 |
4 |
5 |
15 |
--------------------------------------------------------------------------------
/src/server/components/agents/text.vue:
--------------------------------------------------------------------------------
1 |
2 | $emit('input', e.target.value)" v-bind="$attrs" />
3 |
5 |
6 |
21 |
22 |
29 |
30 |
--------------------------------------------------------------------------------
/src/server/components/agents/trigger.vue:
--------------------------------------------------------------------------------
1 |
2 | $root.$emit('frame', 'trigger', trigger)">trigger
3 |
4 |
5 |
12 |
13 |
31 |
--------------------------------------------------------------------------------
/src/server/components/group.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ name }}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
26 |
27 |
67 |
--------------------------------------------------------------------------------
/src/server/components/search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ⨯
6 |
7 |
8 |
9 |
10 |
27 |
28 |
29 |
68 |
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import store from './store'
3 | import router from './router'
4 |
5 | new Vue({
6 | store,
7 | router,
8 | render: h => h(Vue.component('RouterView'))
9 | }).$mount("#whiteroom")
10 |
--------------------------------------------------------------------------------
/src/server/modules/agents.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | set('slot', value)" />
6 |
7 |
injections
8 |
10 | set(inject.name, value)" />
11 |
12 |
props
13 |
15 | set(prop.name, value)"
19 | v-bind="prop.properties" />
20 |
21 |
methods
22 |
24 |
25 |
26 |
27 |
28 |
29 |
110 |
111 |
142 |
--------------------------------------------------------------------------------
/src/server/modules/devices.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
$store.commit('device', e.target.value)">
8 | {{ device }}
9 |
10 |
11 |
12 |
13 |
25 |
26 |
55 |
--------------------------------------------------------------------------------
/src/server/modules/frame.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
35 |
36 |
54 |
--------------------------------------------------------------------------------
/src/server/modules/headline.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ title }}
5 | {{ path }}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
46 |
47 |
117 |
--------------------------------------------------------------------------------
/src/server/modules/layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {{ layout }}
22 |
23 |
24 |
25 |
26 |
45 |
46 |
75 |
--------------------------------------------------------------------------------
/src/server/modules/log.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
36 |
37 |
88 |
--------------------------------------------------------------------------------
/src/server/modules/navigation.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ link.name }}
9 |
10 |
11 |
12 |
13 |
14 |
54 |
55 |
87 |
--------------------------------------------------------------------------------
/src/server/modules/panel.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
39 |
40 |
183 |
--------------------------------------------------------------------------------
/src/server/modules/rotate.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
$store.commit('rotate')">
5 |
6 |
7 |
8 |
36 |
--------------------------------------------------------------------------------
/src/server/modules/scale.vue:
--------------------------------------------------------------------------------
1 |
2 | e.buttons > 0 ? update(e.offsetX / e.target.clientWidth) : null"
4 | @dblclick="reset()">
5 |
8 |
9 |
font-size {{fontsize.toFixed(3)}}em
10 |
11 |
12 |
13 |
43 |
44 |
77 |
--------------------------------------------------------------------------------
/src/server/modules/settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
18 |
19 |
29 |
--------------------------------------------------------------------------------
/src/server/router.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Router from "vue-router";
3 |
4 | import App from "./app.vue";
5 |
6 | Vue.use(Router);
7 |
8 | export default new Router({
9 | routes: [{
10 | path: "*",
11 | component: App
12 | }]
13 | });
14 |
--------------------------------------------------------------------------------
/src/server/store.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Vuex from "vuex";
3 | import connect from '../connector';
4 |
5 | Vue.use(Vuex);
6 |
7 | // whiteroom <-> microscope communication
8 | const microscope = connect()
9 |
10 | microscope.on(({ command, parameters }) => {
11 | const commands = ['register', 'devices', 'layouts', 'log']
12 |
13 | if (commands.includes(command))
14 | store.commit(command, parameters)
15 | })
16 |
17 | // store
18 | const store = new Vuex.Store({
19 | state: {
20 | title: '',
21 | fullscreen: 0,
22 |
23 | db: {},
24 | log: [],
25 |
26 | layouts: [],
27 |
28 | devices: {
29 | 'responsive' : { width: '100%', height: '100%' },
30 | 'Galaxy S5' : { width: '360px', height: '640px' },
31 | 'Pixel 2' : { width: '411px', height: '731px' },
32 | 'Pixel 2 XL' : { width: '411px', height: '823px' },
33 | 'iPhone 5/SE' : { width: '320px', height: '568px' },
34 | 'iPhone 6/7/8' : { width: '375px', height: '667px' },
35 | 'iPhone 6/7/8 +': { width: '414px', height: '736px' },
36 | 'iPhone X' : { width: '375px', height: '812px' },
37 | 'iPad' : { width: '768px', height: '1024px' },
38 | 'iPad Pro' : { width: '1024px', height: '1366px' },
39 | },
40 |
41 | device: 'responsive',
42 |
43 | rotate: false,
44 |
45 | fontsize: 1,
46 | },
47 |
48 | mutations: {
49 | register(state, { path, ...parameters }) {
50 | state.db = { ...state.db, [path]: {
51 | ...parameters,
52 |
53 | // we have to manipulate agents data structure a little bit...
54 | agents: (parameters.agents || []).map(agent => {
55 | const { name, type, description, ...properties } = agent
56 |
57 | // we cant deconstruct 'default' property, so we manually handle it
58 | delete properties.default
59 |
60 | return {
61 | name, type, description, properties, default: agent.default
62 | }
63 | }),
64 |
65 | injects: (parameters.inject || {})
66 | } }
67 | },
68 |
69 | log(state, parameters) {
70 | if (parameters)
71 | state.log = [parameters.join(' ')].concat(state.log).slice(0, 50)
72 | else
73 | state.log = []
74 | },
75 |
76 | layouts(state, parameters) {
77 | state.layouts = [...state.layouts, ...parameters]
78 | .filter((e, i, a) => a.indexOf(e) === i)
79 | },
80 |
81 | devices(state, parameters) {
82 | state.devices = { ...state.devices, ...parameters }
83 | },
84 |
85 | device(state, parameters) {
86 | state.device = parameters
87 | },
88 |
89 | rotate(state) {
90 | state.rotate = !state.rotate
91 | },
92 |
93 | fullscreen(state, value) {
94 | state.fullscreen += value
95 | },
96 |
97 | scale(state, parameters) {
98 | state.fontsize = parameters
99 | },
100 | },
101 |
102 | actions: {
103 | clear(context) {
104 | context.commit('log', null)
105 | },
106 | }
107 | });
108 |
109 |
110 | export default store;
111 |
--------------------------------------------------------------------------------
/src/server/transitions/expand.vue:
--------------------------------------------------------------------------------
1 |
42 |
--------------------------------------------------------------------------------
/src/server/vars.styl:
--------------------------------------------------------------------------------
1 | $fg=#00c300
2 | $fg=#2dd28d
3 | $bg=#ffffff
4 | $vp=#f6f6f6
5 | $bd=darken($vp, 5%)
6 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path')
2 | const { HotModuleReplacementPlugin } = require('webpack')
3 | const HtmlWebpackPlugin = require('html-webpack-plugin')
4 | const CleanWebpackPlugin = require('clean-webpack-plugin')
5 | const { VueLoaderPlugin } = require("vue-loader")
6 | const CleanTerminalPlugin = require('clean-terminal-webpack-plugin')
7 |
8 | module.exports = {
9 | mode: 'development',
10 | node: { fs: 'empty' },
11 |
12 | resolveLoader: {
13 | modules: [ resolve(__dirname, './node_modules') ],
14 | },
15 |
16 | resolve: {
17 | alias: {
18 | '@': resolve(process.env.PWD, './src'),
19 | },
20 | },
21 |
22 | entry: {
23 | 'whiteroom' : resolve(__dirname, './src/server/'),
24 | 'microscope' : resolve(__dirname, './src/client/'),
25 | 'experiments' : resolve(process.env.PWD, './experiments'),
26 | },
27 |
28 | output: {
29 | path: resolve(__dirname, 'dist'),
30 | filename: '[name].bundle.js',
31 | publicPath: '',
32 | },
33 |
34 | module: {
35 | rules: [
36 | {
37 | test: /\.vue$/,
38 | loader: require.resolve('vue-loader')
39 | },
40 | {
41 | test: /\.js$/,
42 | exclude: /(node_modules)/,
43 | use: {
44 | loader: require.resolve('babel-loader'),
45 | options: {
46 | presets: [
47 | require.resolve('@babel/preset-env')
48 | ]
49 | }
50 | }
51 | },
52 | {
53 | test: /\.css$/,
54 | use: [
55 | require.resolve('vue-style-loader'),
56 | require.resolve('css-loader'),
57 | ]
58 | },
59 | {
60 | test: /\.styl(us)?$/,
61 | use: [
62 | require.resolve('vue-style-loader'),
63 | require.resolve('css-loader'),
64 | require.resolve('stylus-loader'),
65 | ]
66 | }
67 | ]
68 | },
69 |
70 | plugins: [
71 | new CleanWebpackPlugin(['dist']),
72 |
73 | new HotModuleReplacementPlugin(),
74 |
75 | new VueLoaderPlugin(),
76 |
77 | new HtmlWebpackPlugin({
78 | filename: 'index.html',
79 | chunks: ['whiteroom'],
80 | template: resolve(__dirname, './www/index.html'),
81 | inject: true,
82 | }),
83 |
84 | new HtmlWebpackPlugin({
85 | filename: 'microscope.html',
86 | chunks: ['microscope', 'experiments'],
87 | template: resolve(__dirname, './www/microscope.html'),
88 | inject: true,
89 | }),
90 |
91 | new CleanTerminalPlugin()
92 | ],
93 |
94 | devServer: {
95 | contentBase: './dist',
96 | host: '0.0.0.0',
97 | port: 9999,
98 | hot: true,
99 | watchOptions: {
100 | poll: true
101 | }
102 | },
103 |
104 | stats: { colors: true },
105 |
106 | watch: true,
107 | }
108 |
--------------------------------------------------------------------------------
/www/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | whiteroom
7 |
8 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/www/microscope.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | microscope
6 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------