// Point of entry for Vue
#app
window-manager
// Vindue Plugin Templates
template#taskbar
.taskbar
.start-menu(v-show='startMenuOpen')
.title Vindue98
.items
.item(@mousedown="open('help')") #[i.fa.fa-fw.fa-lg.fa-question]Help
.item(@mousedown="open('settings')") #[i.fa.fa-fw.fa-lg.fa-cog]Settings
.divider
.item #[i.fa.fa-fw.fa-lg.fa-power-off]Shut Down
button(@click='toggleStartMenu' @blur='closeStartMenu' v-bind:class='{pressed: startMenuOpen}') #[i.fa.fa-fw.fa-bars]Start
.divider
.section
button.item(v-for='(w, key) in windows' @click='focusWindow(key)' v-bind:title='w' v-bind:class='{pressed: focus === key}') {{$parent.$refs['key-' + key][0].title}}
.info
.time(v-bind:title="moment(now).format('dddd, MMMM D, YYYY')") {{moment(now).format(timeCode)}}
template#windowManager
.window-manager(@touchstart='onTouch' @touchmove='onTouch' @touchend='onTouch' v-bind:style='state.style')
taskbar(@open='open' @focus-window='focusWindow' v-bind:windows='windows' v-bind:focus='focus' v-bind:time-code='state.timeCode')
template(v-for='(w, key) in windows')
window(v-bind:component='w' v-bind:key-val='key' v-bind:top='start.top' v-bind:left='start.left' v-bind:class='{focus: focus === key}' v-bind:ref="'key-' + key" v-bind:vindue-state.sync='state')
template#help
.help {{text}}
// App Components
template#tabControl
.tab-control-wrapper
.tab-control
.anchor
.header
button(v-for='(value, key) in $slots' v-bind:class='{focus: focus === key}' @click='focusTab(key)'): span {{key}}
.pane(v-for="(value, key) in $slots" v-show='focus === key')
slot(:name='key')
template#settings
.settings
tab-control
template(slot='Background')
.color-box(v-bind:style='{background: color}')
.drop-down
select(v-model='color' v-on:change='changeBackground')
option(value='#1C1F2B') Blue
option(value='#1b5e20') Green
option(value='#7f0000') Red
template(slot='Theme') Theme Stuff
template(slot='Time')
.row
.col.labels
label(for='hours') Hours
label(for='minutes') Minutes
label(for='seconds') Seconds
label(for='month') Month
label(for='day') Day
label(for='year') Year
.col.inputs
.row
.drop-down(v-bind:class='{disabled: !hoursEnabled}')
select(id='hours' v-model='hours' @change='changeTime')
option(value='h') 12
option(value='k') 24
input(type='checkbox' v-model='hoursEnabled' @change='changeTime')
.row
.drop-down(v-bind:class='{disabled: !minutesEnabled}')
select(id='minutes' v-model='minutes' @change='changeTime')
option(value='mm') 00 01...
option(value='m') 0 1...
input(type='checkbox' v-model='minutesEnabled' @change='changeTime')
.row
.drop-down(v-bind:class='{disabled: !secondsEnabled}')
select(id='seconds' v-model='seconds' @change='changeTime')
option(value='ss') 00 01...
option(value='s') 0 1...
input(type='checkbox' v-model='secondsEnabled' @change='changeTime')
.row
.drop-down(v-bind:class='{disabled: !monthEnabled}')
select(id='month' v-model='month' @change='changeTime')
option(value='M') 1 2...
option(value='MM') 01 02...
option(value='Mo') 1st...
option(value='MMM') Jan
option(value='MMMM') January
input(type='checkbox' v-model='monthEnabled' @change='changeTime')
.row
.drop-down(v-bind:class='{disabled: !dayEnabled}')
select(id='day' v-model='day' @change='changeTime')
option(value='D') 1 2...
option(value='DD') 01 02...
input(type='checkbox' v-model='dayEnabled' @change='changeTime')
.row
.drop-down(v-bind:class='{disabled: !yearEnabled}')
select(id='year' v-model='year' @change='changeTime')
option(value='YY') 70 71...
option(value='YYYY') 1970 1971...
input(type='checkbox' v-model='yearEnabled' @change='changeTime')
.buttons
button(@click='$parent.close()') OK
// Profile link styling for self-shilling
a.shill(href='https://codepen.io/milesmanners' target='_blank' title='Made by Miles Manners')
View Compiled
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,700');
$clr-bg: #1C1F2B;
$clr-bg-light: lighten($clr-bg, 10%);
$clr-bg-lighter: lighten($clr-bg, 15%);
$clr-bg-dark: darken($clr-bg, 2.5%);
$clr-bg-darker: darken($clr-bg, 5%);
$clr-text: $clr-black;
$clr-text-hint: rgba($clr-text, .6);
$clr-primary: $clr-blue;
$clr-secondary: $clr-green;
$clr-taskbar: #C2C2C2;
$clr-taskbar-dark: #BEBEBE;
$clr-taskbar-darker: #A1A1A1;
$clr-taskbar-border: #E3E3E3;
$anim-dur: 200ms;
$border-radius: 4px;
*, *::before, *::after {
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
}
body {
background: $clr-bg;
//color: $clr-text;
margin: 0px;
font-family: 'Open Sans', sans-serif;
}
.anchor {
height: 0;
}
.row {
display: flex;
}
.fa {
display: inline-block;
}
.window-manager {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/726875/Vindue.png);
background-repeat: no-repeat;
background-position: center center;
background-size: 40vmin;
.window {
position: absolute;
background: $clr-taskbar;
border: 2px outset $clr-taskbar-border;
will-change: left, top;
z-index: 5;
display: flex;
flex-direction: column;
&.focus {
z-index: 6;
.title {
background: linear-gradient(to right, #00007F, #0F80CF);
color: $clr-white;
}
}
.border {
position: absolute;
&.top, &.bottom {
width: 100%;
height: 6px;
cursor: ns-resize;
}
&.left, &.right {
height: 100%;
width: 6px;
cursor: ew-resize;
}
&.top-right, &.bottom-right, &.bottom-left, &.top-left {
width: 6px;
height: 6px;
}
&.top-right, &.bottom-left {
cursor: nesw-resize;
}
&.bottom-right, &.top-left {
cursor: nwse-resize;
}
&.top { top: -6px }
&.right { right: -6px }
&.bottom { bottom: -6px }
&.left { left: -6px }
&.top-right {
top: -6px;
right: -6px;
}
&.bottom-right {
bottom: -6px;
right: -6px;
}
&.bottom-left {
bottom: -6px;
left: -6px;
}
&.top-left {
top: -6px;
left: -6px;
}
}
.title {
display: flex;
background: $clr-taskbar-darker;
padding: 2px;
padding-left: 4px;
user-select: none;
.actions {
display: flex;
margin-left: auto;
button {
padding: 0;
}
}
}
.content {
overflow: auto;
}
}
}
.taskbar {
position: absolute;
bottom: 0;
background: $clr-taskbar;
color: $clr-black;
width: 100%;
height: 38px;
border: 2px outset $clr-taskbar-border;
display: flex;
user-select: none;
.start-menu {
position: absolute;
bottom: 38px;
background: inherit;
border: inherit;
padding-right: 3px;
display: flex;
z-index: 999999;
.title {
background: linear-gradient(to top, #1715FB, #060579);
color: $clr-white;
writing-mode: tb-rl;
transform: scale(-1, -1);
width: 40px;
font-weight: 700;
font-size: 28px;
padding: 10px 0;
}
.divider {
width: 100%;
height: 0px;
border-width: 1px;
border-style: inset;
}
.item {
padding: 5px 10px 5px 0;
&:hover {
background: $clr-primary;
}
.fa {
padding-right: 5px;
}
}
}
button {
height: 30px;
margin-top: 2px;
padding: 0px 4px 0px 2px;
background: inherit;
font-size: 16px;
&.pressed {
border-style: inset;
}
&.item {
width: 100px;
text-align: left;
padding-left: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-left: 3px;
&:first-child {
margin-left: 0;
}
}
}
.divider {
background: inherit;
width: 2px;
height: 30px;
margin: 2px 2px 0 2px;
border: inherit;
}
.info {
margin-top: 2px;
padding: 0 3px;
height: 30px;
background: #BEBEBE;
border: 2px inset $clr-taskbar-border;
margin-left: auto;
}
}
.window .help {
padding: 5px;
}
.tab-control-wrapper {
padding: 35px 10px 5px;
.tab-control {
height: 100%;
border: 2px outset $clr-taskbar-border;
.header {
position: relative;
top: -26px;
white-space: nowrap;
display: inline-block;
button {
height: 24px;
background: $clr-taskbar;
border-radius: $border-radius $border-radius 0 0;
border-bottom: none;
&.focus {
height: 26px;
span {
position: relative;
top: -1px;
}
}
}
}
.pane {
margin: 10px;
}
}
}
.color-box {
height: 100px;
border: 2px inset $clr-taskbar-border;
margin-bottom: 5px;
}
.drop-down {
position: relative;
pointer-events: none;
&:after {
content: '>';
font: 17px "Consolas", monospace;
font-weight: bold;
color: $clr-black;
transform: rotate(90deg);
background: $clr-taskbar;
width: 21px;
top: -1px;
right: 5px;
text-align: center;
padding: 0 0 2px;
border: 2px outset $clr-taskbar-border;
position: absolute;
pointer-events: none;
}
select {
appearance: none;
pointer-events: auto;
border: 2px inset $clr-taskbar-border;
display: block;
width: 100%;
height: 25px;
float: right;
margin: 0;
padding: 0px;
font-size: 16px;
line-height: 1.75;
color: $clr-black;
background-color: $clr-white;
}
&.disabled {
&:after {
color: $clr-text-hint;
}
select {
background: $clr-taskbar;
color: $clr-text-hint;
}
}
}
.window .settings {
.pane {
display: flex;
flex-direction: column;
.col {
display: flex;
flex-direction: column;
}
.row {
display: flex;
}
.labels {
padding-right: 5px;
label {
height: 30px;
}
}
.inputs {
flex-grow: 1;
.row {
height: 30px;
}
.drop-down {
width: 100%;
}
}
}
.buttons {
margin-right: 10px;
button {
height: 30px;
padding: 0px 4px 0px 2px;
background: inherit;
font-size: 16px;
width: 80px;
padding-left: 4px;
white-space: nowrap;
overflow: hidden;
text-align: center;
float: right;
margin-bottom: 10px;
margin-left: 3px;
&:last-child {
margin-left: 0;
}
}
}
}
// Profile link styling for self-shilling
.shill {
position: fixed;
background-image: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/726875/profile/profile-512.jpg?100000');
background-size: cover;
width: 40px;
height: 40px;
bottom: 50px;
right: 10px;
border-radius: 20px;
opacity: .4;
filter: blur(.5px);
transition: opacity $anim-dur, blur $anim-dur;
&:hover {
opacity: 1;
filter: none;
}
}
View Compiled
// Plugin Definition
let Vindue = {
install (vue, options) {
Vue.component('window', {
props: [
'top',
'left',
'component',
'key-val',
'vindue-state'
],
data () {
return {
title: '',
resizable: false,
helpText: '',
style: {
top: 0,
left: 0,
'min-width': '150px',
'min-height': '100px',
width: 'auto',
height: 'auto',
'z-index': 1,
'user-select': 'auto'
}
}
},
render (h) {
let comps = []
// Resizable Borders
const border = (c, ...on) =>
h('div', {
style: { display: this.resizable ? 'block' : 'none' },
'class': { border: true, [c]: true },
on: { mousedown: (e) => on.forEach(f => f(e)) }
})
comps.push(border('top', this.resizeTop))
comps.push(border('right', this.resizeRight))
comps.push(border('bottom', this.resizeBottom))
comps.push(border('left', this.resizeLeft))
comps.push(border('top-right', this.resizeTop, this.resizeRight))
comps.push(border('bottom-right', this.resizeBottom, this.resizeRight))
comps.push(border('bottom-left', this.resizeBottom, this.resizeLeft))
comps.push(border('top-left', this.resizeTop, this.resizeLeft))
// Window Actions
let buttons = []
// Help Button
buttons.push(h('button', {
style: { display: this.helpText && this.helpText.length > 0 ? 'block' : 'none' },
on: {
click: e => {
e.stopPropagation()
this.help()
}
}
}, [
h('i', { 'class': { fa: true, 'fa-fw': true, 'fa-question': true } })
]))
// Close Button
buttons.push(h('button', {
on: {
click: e => {
e.stopPropagation()
this.close()
}
}
}, [
h('i', { 'class': { fa: true, 'fa-fw': true, 'fa-times': true } })
]))
// Title
comps.push(h('div', {
'class': { title: true },
on: {
mousedown: e => {
if (e.target !== e.currentTarget) return
this.startDrag(e)
}
}
}, [this.title, h('div', { 'class': { actions: true } }, buttons)]))
// Content
let dynamic = h(this.component, {
tag: 'component',
props: {
styleObj: this.style,
vindueState: this.vindueState
},
on: {
'update:title': val => { this.title = val },
'update:resizable': val => { this.resizable = val },
'update:help-text': val => { this.helpText = val },
'update:style-obj': val => { this.style = val },
'update:vindue-state': val => { this.vindueState = val }
},
ref: 'child'
})
// Content Container
comps.push(h('div', { 'class': { content: true } }, [dynamic]))
// Window
return h('div', {
'class': { window: true },
style: this.style,
on: { mousedown: e => { this.focusWindow() } }
}, comps)
},
methods: {
focusWindow () {
this.$parent.focusWindow(this.keyVal)
},
startDrag (e) {
let el = this.$el
let style = this.style
let startX = el.offsetLeft
let startY = el.offsetTop
let initialMouseX = e.clientX
let initialMouseY = e.clientY
function mouseMove (e) {
let dx = e.clientX - initialMouseX
let dy = e.clientY - initialMouseY
style.top = startY + dy + 'px'
style.left = startX + dx + 'px'
}
function mouseUp () {
document.removeEventListener('mousemove', mouseMove)
document.removeEventListener('mouseup', mouseUp)
if (parseInt(style.left) < 0) {
style.left = 0
} else if (parseInt(style.left) + el.offsetWidth > window.innerWidth) {
style.left = `${window.innerWidth - el.offsetWidth}px`
}
if (parseInt(style.top) < 0) {
style.top = 0
} else if (parseInt(style.top) + el.offsetHeight + 38 > window.innerHeight) {
style.top = `${window.innerHeight - el.offsetHeight - 38}px`
}
}
document.addEventListener('mousemove', mouseMove)
document.addEventListener('mouseup', mouseUp)
},
help () {
this.$parent.help(this.helpText)
},
close () {
this.$parent.close(this.keyVal)
},
resizeTop (e) {
this.style['user-select'] = 'none'
let el = this.$el
let style = this.style
let startY = el.offsetTop
let startH = parseInt(style.height)
let initialMouseY = e.clientY
function mouseMove (e) {
let dy = e.clientY - initialMouseY
if (startH - dy >= parseInt(style['min-height'])) {
style.height = startH - dy + 'px'
style.top = startY + dy + 'px'
}
}
function mouseUp () {
document.removeEventListener('mousemove', mouseMove)
document.removeEventListener('mouseup', mouseUp)
if (parseInt(style.top) < 0) {
style.height = parseInt(style.height) + parseInt(style.top) + 'px'
style.top = 0
}
style['user-select'] = 'auto'
}
document.addEventListener('mousemove', mouseMove)
document.addEventListener('mouseup', mouseUp)
},
resizeRight (e) {
this.style['user-select'] = 'none'
let el = this.$el
let style = this.style
let startX = el.offsetLeft
let startW = parseInt(style.width)
let initialMouseX = e.clientX
function mouseMove (e) {
let dx = e.clientX - initialMouseX
if (startW + dx >= parseInt(style['min-width'])) {
style.width = startW + dx + 'px'
}
}
function mouseUp () {
document.removeEventListener('mousemove', mouseMove)
document.removeEventListener('mouseup', mouseUp)
style['user-select'] = 'auto'
if (startX + parseInt(style.width) > window.innerWidth) {
style.width = window.innerWidth - startX + 'px'
}
}
document.addEventListener('mousemove', mouseMove)
document.addEventListener('mouseup', mouseUp)
},
resizeBottom (e) {
this.style['user-select'] = 'none'
let el = this.$el
let style = this.style
let startY = el.offsetTop
let startH = parseInt(style.height)
let initialMouseY = e.clientY
function mouseMove (e) {
let dy = e.clientY - initialMouseY
if (startH + dy >= parseInt(style['min-height'])) {
style.height = startH + dy + 'px'
}
}
function mouseUp () {
document.removeEventListener('mousemove', mouseMove)
document.removeEventListener('mouseup', mouseUp)
if (startY + parseInt(style.height) + 38 > window.innerHeight) {
style.height = window.innerHeight - startY - 38 + 'px'
}
style['user-select'] = 'auto'
}
document.addEventListener('mousemove', mouseMove)
document.addEventListener('mouseup', mouseUp)
},
resizeLeft (e) {
this.style['user-select'] = 'none'
let el = this.$el
let style = this.style
let startX = el.offsetLeft
let startW = parseInt(style.width)
let initialMouseX = e.clientX
function mouseMove (e) {
let dx = e.clientX - initialMouseX
if (startW - dx >= parseInt(style['min-width'])) {
style.width = startW - dx + 'px'
style.left = startX + dx + 'px'
}
}
function mouseUp () {
document.removeEventListener('mousemove', mouseMove)
document.removeEventListener('mouseup', mouseUp)
if (parseInt(style.left) < 0) {
style.width = parseInt(style.width) + parseInt(style.left) + 'px'
style.left = 0
}
style['user-select'] = 'auto'
}
document.addEventListener('mousemove', mouseMove)
document.addEventListener('mouseup', mouseUp)
}
},
mounted () {
this.title = this.$refs.child.title
this.resizable = this.$refs.child.resizable
this.helpText = this.$refs.child.helpText
this.style.top = this.top + 'px'
this.style.left = this.left + 'px'
this.$nextTick(function () {
this.style.width = this.$el.offsetWidth + 1 + 'px'
this.style.height = this.$el.offsetHeight + 1 + 'px'
})
this.style['z-index'] = this.keyVal
}
})
Vue.component('taskbar', {
template: '#taskbar',
props: [
'windows',
'focus',
'timeCode'
],
data () {
return {
startMenuOpen: false,
now: new Date()
}
},
created () {
setInterval(() => this.now = new Date, 1000)
},
methods: {
toggleStartMenu () {
this.startMenuOpen = !this.startMenuOpen
},
closeStartMenu () {
this.startMenuOpen = false
},
open (name) {
this.$emit('open', name)
},
focusWindow (w) {
this.$emit('focus-window', w)
}
}
})
Vue.component('window-manager', {
template: '#windowManager',
data () {
return {
focus: null,
windows: [],
start: {
top: 0,
left: 0
},
state: {
timeCode: 'h:mm A',
style: {
'background-color': '#1C1F2B'
}
},
mousedown: []
}
},
methods: {
open (w) {
this.focus = this.windows.length
this.windows.push(w)
this.start.top += 15
this.start.left += 15
},
close (key) {
this.windows.splice(key, 1)
this.$nextTick(function () {
let max = null
for (let k in this.$refs) {
let r = this.$refs[k][0]
if (r && (!max || (max.style['z-index'] < r.style['z-index']))) {
max = r
}
}
this.focus = max ? max.keyVal : null
})
},
help (text) {
this.open('help')
this.$nextTick(function () {
this.$refs['key-' + this.focus][0].$refs.child.text = text
})
},
focusWindow (key) {
this.focus = key
let ref = this.$refs['key-' + key][0]
for (let k in this.$refs) {
let r = this.$refs[k][0]
if (r && r.style['z-index'] >= ref.style['z-index']) {
r.style['z-index'] = parseInt(r.style['z-index']) - 1
}
}
ref.style['z-index'] = this.windows.length
},
onTouch (e) {
if (e.target.tagName === 'SELECT')
return true
e.preventDefault()
if (e.touches.length > 1 || (e.type == 'touchend' && e.touches.length > 0))
return
let type = null
let touch = e.changedTouches[0]
switch (e.type) {
case 'touchstart':
type = 'mousedown'
this.mousedown.push(e.target)
break
case 'touchmove':
type = 'mousemove'
break
case 'touchend':
type = 'mouseup'
break
}
if (type === 'mouseup') {
if (this.mousedown.includes(e.target)) {
this.mousedown.splice(this.mousedown.indexOf(e.target), 1)
e.target.dispatchEvent(new MouseEvent('click', {
screenX: touch.screenX,
screenY: touch.screenY,
clientX: touch.clientX,
clientY: touch.clientY,
ctrlKey: e.ctrlKey,
shiftKey: e.shiftKey,
altKey: e.altKey,
metaKey: e.metaKey,
button: 0,
bubbles: true,
cancelable: true
}))
}
}
e.target.dispatchEvent(new MouseEvent(type, {
screenX: touch.screenX,
screenY: touch.screenY,
clientX: touch.clientX,
clientY: touch.clientY,
ctrlKey: e.ctrlKey,
shiftKey: e.shiftKey,
altKey: e.altKey,
metaKey: e.metaKey,
button: 0,
bubbles: true,
cancelable: true
}))
}
}
})
Vue.component('help', {
template: '#help',
props: {
'style-obj': Object
},
data () {
return {
title: 'Help',
text: "Welcome to Vindue 98, the world's most advanced operating system."
}
},
mounted () {
Object.assign(this.styleObj, this.style, {
'width': '250px'
})
}
})
}
}
Vue.use(Vindue)
// Component Definitions
Vue.component('tab-control', {
template: '#tabControl',
data () {
return {
panes: [],
focus: ''
}
},
mounted () {
this.focus = Object.keys(this.$slots)[0]
},
methods: {
focusTab (key) {
this.focus = key
}
}
})
Vue.component('settings', {
template: '#settings',
props: {
title: { default: 'Settings' },
resizable: { default: true },
'help-text': { default: 'Change system settings' },
'style-obj': Object,
'vindue-state': Object
},
data () {
return {
color: '#1C1F2B',
hoursEnabled: true,
hours: 'h',
minutesEnabled: true,
minutes: 'mm',
secondsEnabled: false,
seconds: 'ss',
monthEnabled: false,
month: 'M',
dayEnabled: false,
day: 'D',
yearEnabled: false,
year: 'YYYY'
}
},
methods: {
changeBackground () {
this.vindueState.style['background-color'] = this.color
},
changeTime () {
let timeCode = ''
if (this.hoursEnabled)
timeCode += this.hours
if (this.hoursEnabled && this.minutesEnabled)
timeCode += ':'
if (this.minutesEnabled)
timeCode += this.minutes
if (this.minutesEnabled && this.secondsEnabled)
timeCode += ':'
if (this.secondsEnabled)
timeCode += this.seconds
if (this.hours === 'h')
timeCode += ' A'
if (timeCode.length > 0 && (this.monthEnabled || this.dayEnabled || this.yearEnabled))
timeCode += ' '
if (this.monthEnabled)
timeCode += this.month
if (this.monthEnabled && this.dayEnabled)
timeCode += '/'
if (this.dayEnabled)
timeCode += this.day
if (this.dayEnabled && this.yearEnabled)
timeCode += '/'
if (this.yearEnabled)
timeCode += this.year
this.vindueState.timeCode = timeCode
}
},
mounted () {
Object.assign(this.styleObj, this.style, {
'min-width': '250px',
'min-height': '250px'
})
this.color = this.vindueState.style['background-color']
}
})
// Point of entry
new Vue({
el: '#app'
})