Jelajahi Sumber

Get basic track changes list working

James Allen 12 tahun lalu
induk
melakukan
60e5542f35

+ 7 - 111
services/web/app/views/project/editor.jade

@@ -33,121 +33,17 @@ block content
 			.alert.alert-warning.small(ng-if="connection.reconnecting")
 			.alert.alert-warning.small(ng-if="connection.reconnecting")
 				strong Reconnecting...
 				strong Reconnecting...
 
 
-		aside#left-menu(
-			ng-class="{ 'shown': ui.leftMenuShown }"
-			ng-cloak
-		)
-			h4 Settings
-			form
-				.form-controls
-					label(for="compiler") Compiler
-					select.form-control(
-						name="compiler"
-						ng-model="project.compiler"
-					)
-						option(value='pdflatex') pdfLaTeX
-						option(value='latex') LaTeX
-						option(value='xelatex') XeLaTeX
-						option(value='lualatex') LuaLaTeX
-
-				.form-controls
-					label(for="spellCheckLanguage") Spell Check
-					select.form-control(
-						name="spellCheckLanguage"
-						ng-model="project.spellCheckLanguage"
-					)
-						option(value="") Off
-						optgroup(label="Language")
-							for language in languages
-								option(
-									value=language.code
-								)= language.name
-
-				.form-controls
-					label(for="autoComplete") Auto-Complete
-					input.form-control(
-						type="checkbox"
-						name="autoComplete"
-						ng-model="settings.autoComplete"
-					)
-
-				.form-controls
-					label(for="theme") Theme
-					select.form-control(
-						name="theme"
-						ng-model="settings.theme"
-					)
-						each theme in themes
-							option(value=theme) #{theme}
-							
-				.form-controls
-					label(for="mode") Keybindings
-					select.form-control(
-						name="mode"
-						ng-model="settings.mode"
-					)
-						option(value='default') None
-						option(value='vim') Vim
-						option(value='emacs') Emacs
-
-				.form-controls
-					label(for="fontSize") Font Size
-					select.form-control(
-						name="fontSize"
-						ng-model="settings.fontSize"
-					)
-						each size in ['10','11','12','13','14','16','20','24']
-							option(value=size) #{size}px
-
-		#left-menu-mask(
-			ng-show="ui.leftMenuShown",
-			ng-click="ui.leftMenuShown = false"
-			ng-cloak
-		)
-
-		header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading")
-			a.btn.btn-full-height(
-				href,
-				ng-click="ui.leftMenuShown = true"
-				tooltip="Menu",
-				tooltip-placement="bottom"
-			)
-				i.fa.fa-bars
-
-			span.name {{ project.name }}
-
-			a(href='#', data-toggle="tooltip", title="Rename")
-				i.fa.fa-pencil
-
-			.toolbar-right
-				a.btn.btn-full-height(href='#', tooltip="Share", tooltip-placement="bottom")
-					i.fa.fa-group
-				a.btn.btn-full-height(href='#', tooltip="Recent Changes", tooltip-placement="bottom")
-					i.fa.fa-history
-				a.btn.btn-full-height(href='#', tooltip="Chat", tooltip-placement="bottom")
-					i.fa.fa-comment
+		include ./editor/left-menu
+
+		include ./editor/header
 
 
 		#ide-body(ng-cloak, layout="main", ng-hide="state.loading")
 		#ide-body(ng-cloak, layout="main", ng-hide="state.loading")
-			include ./editor/file-tree
+			.ui-layout-west
+				include ./editor/file-tree
 
 
 			.ui-layout-center
 			.ui-layout-center
-				.loading(ng-show="!editor.sharejs_doc || editor.opening")
-					i.fa.fa-spin.fa-refresh
-					|   Loading...
-				#editor(
-					ace-editor,
-					ng-show="!!editor.sharejs_doc && !editor.opening"
-					theme="settings.theme",
-					keybindings="settings.mode",
-					font-size="settings.fontSize",
-					auto-complete="settings.autoComplete",
-					spell-check-language="project.spellCheckLanguage",
-					annotations="onlineUserCursorAnnotations[editor.open_doc_id]"
-					show-print-margin="false",
-					sharejs-doc="editor.sharejs_doc",
-					last-updated="editor.last_updated",
-					cursor-position="editor.cursorPosition"
-				)
+				include ./editor/editor
+				include ./editor/track-changes
 
 
 	//- #loadingScreen
 	//- #loadingScreen
 	//- 	h3 Loading...
 	//- 	h3 Loading...

+ 19 - 0
services/web/app/views/project/editor/editor.jade

@@ -0,0 +1,19 @@
+div.full-size(ng-show="ui.view == 'editor'")
+	.loading(ng-show="!editor.sharejs_doc || editor.opening")
+		i.fa.fa-spin.fa-refresh
+		|   Loading...
+
+	#editor(
+		ace-editor,
+		ng-show="!!editor.sharejs_doc && !editor.opening"
+		theme="settings.theme",
+		keybindings="settings.mode",
+		font-size="settings.fontSize",
+		auto-complete="settings.autoComplete",
+		spell-check-language="project.spellCheckLanguage",
+		annotations="onlineUserCursorAnnotations[editor.open_doc_id]"
+		show-print-margin="false",
+		sharejs-doc="editor.sharejs_doc",
+		last-updated="editor.last_updated",
+		cursor-position="editor.cursorPosition"
+	)

+ 1 - 1
services/web/app/views/project/editor/file-tree.jade

@@ -1,4 +1,4 @@
-aside#file-tree.ui-layout-west(ng-controller="FileTreeController")
+aside#file-tree(ng-controller="FileTreeController")
 	.toolbar.toolbar-small
 	.toolbar.toolbar-small
 		a(
 		a(
 			href,
 			href,

+ 27 - 0
services/web/app/views/project/editor/header.jade

@@ -0,0 +1,27 @@
+header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading")
+	a.btn.btn-full-height(
+		href,
+		ng-click="ui.leftMenuShown = true"
+		tooltip="Menu",
+		tooltip-placement="bottom"
+	)
+		i.fa.fa-fw.fa-bars
+
+	span.name {{ project.name }}
+
+	a(href='#', data-toggle="tooltip", title="Rename")
+		i.fa.fa-pencil
+
+	.toolbar-right
+		a.btn.btn-full-height(href='#', tooltip="Share", tooltip-placement="bottom")
+			i.fa.fa-fw.fa-group
+		a.btn.btn-full-height(
+			href,
+			ng-click="toggleTrackChanges()",
+			ng-class="{ active: (ui.view == 'track-changes') }"
+			tooltip="Recent Changes",
+			tooltip-placement="bottom"
+		)
+			i.fa.fa-fw.fa-history
+		a.btn.btn-full-height(href='#', tooltip="Chat", tooltip-placement="bottom")
+			i.fa.fa-fw.fa-comment

+ 71 - 0
services/web/app/views/project/editor/left-menu.jade

@@ -0,0 +1,71 @@
+aside#left-menu.full-size(
+	ng-class="{ 'shown': ui.leftMenuShown }"
+	ng-cloak
+)
+	h4 Settings
+	form
+		.form-controls
+			label(for="compiler") Compiler
+			select.form-control(
+				name="compiler"
+				ng-model="project.compiler"
+			)
+				option(value='pdflatex') pdfLaTeX
+				option(value='latex') LaTeX
+				option(value='xelatex') XeLaTeX
+				option(value='lualatex') LuaLaTeX
+
+		.form-controls
+			label(for="spellCheckLanguage") Spell Check
+			select.form-control(
+				name="spellCheckLanguage"
+				ng-model="project.spellCheckLanguage"
+			)
+				option(value="") Off
+				optgroup(label="Language")
+					for language in languages
+						option(
+							value=language.code
+						)= language.name
+
+		.form-controls
+			label(for="autoComplete") Auto-Complete
+			input.form-control(
+				type="checkbox"
+				name="autoComplete"
+				ng-model="settings.autoComplete"
+			)
+
+		.form-controls
+			label(for="theme") Theme
+			select.form-control(
+				name="theme"
+				ng-model="settings.theme"
+			)
+				each theme in themes
+					option(value=theme) #{theme}
+					
+		.form-controls
+			label(for="mode") Keybindings
+			select.form-control(
+				name="mode"
+				ng-model="settings.mode"
+			)
+				option(value='default') None
+				option(value='vim') Vim
+				option(value='emacs') Emacs
+
+		.form-controls
+			label(for="fontSize") Font Size
+			select.form-control(
+				name="fontSize"
+				ng-model="settings.fontSize"
+			)
+				each size in ['10','11','12','13','14','16','20','24']
+					option(value=size) #{size}px
+
+#left-menu-mask(
+	ng-show="ui.leftMenuShown",
+	ng-click="ui.leftMenuShown = false"
+	ng-cloak
+)

+ 40 - 0
services/web/app/views/project/editor/track-changes.jade

@@ -0,0 +1,40 @@
+div#trackChanges(ng-show="ui.view == 'track-changes'")
+	aside.change-list
+		ul.list-unstyled(ng-controller="TrackChangesListController")
+			li.change(
+				ng-repeat="update in trackChanges.updates"
+				ng-class="{\
+					'first-in-day': update.meta.first_in_day,\
+					'selected': update.inSelection,\
+					'selected-to': update.selectedTo,\
+					'selected-from': update.selectedFrom\
+				}"
+				ng-controller="TrackChangesListItemController"
+			)
+
+				div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }}
+
+				div.selectors
+					div.range
+					form
+						input.selector-from(
+							type="radio"
+							name="fromVersion"
+							ng-model="update.selectedFrom"
+							ng-value="true"
+						)
+					form
+						input.selector-to(
+							type="radio"
+							name="toVersion"
+							ng-model="update.selectedTo"
+							ng-value="true"
+						)
+
+				div.description(ng-click="select()")
+					div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
+					div.docs(ng-repeat="(doc_id, doc) in update.docs")
+						span.doc {{ doc.entity.name }}
+					div.users
+						span.user(ng-repeat="user in update.meta.users")
+							| {{user.first_name}} {{user.last_name}}

+ 18 - 0
services/web/public/coffee/app/filters/formatDate.coffee

@@ -0,0 +1,18 @@
+define [
+	"base"
+], (App) ->
+	moment.lang "en", calendar:
+		lastDay : '[Yesterday]'
+		sameDay : '[Today]'
+		nextDay : '[Tomorrow]'
+		lastWeek : "ddd, Do MMM YY"
+		nextWeek : "ddd, Do MMM YY"
+		sameElse : 'ddd, Do MMM YY'
+
+	App.filter "formatDate", () ->
+		(date, format = "Do MMM YYYY, h:mm a") ->
+			moment(date).format(format)
+
+	App.filter "relativeDate", () ->
+		(date) ->
+			moment(date).calendar()

+ 5 - 0
services/web/public/coffee/app/ide.coffee

@@ -5,11 +5,13 @@ define [
 	"ide/editor/EditorManager"
 	"ide/editor/EditorManager"
 	"ide/settings/SettingsManager"
 	"ide/settings/SettingsManager"
 	"ide/online-users/OnlineUsersManager"
 	"ide/online-users/OnlineUsersManager"
+	"ide/track-changes/TrackChangesManager"
 	"ide/directives/layout"
 	"ide/directives/layout"
 	"ide/services/ide"
 	"ide/services/ide"
 	"directives/focus"
 	"directives/focus"
 	"directives/fineUpload"
 	"directives/fineUpload"
 	"directives/onEnter"
 	"directives/onEnter"
+	"filters/formatDate"
 ], (
 ], (
 	App
 	App
 	FileTreeManager
 	FileTreeManager
@@ -17,6 +19,7 @@ define [
 	EditorManager
 	EditorManager
 	SettingsManager
 	SettingsManager
 	OnlineUsersManager
 	OnlineUsersManager
+	TrackChangesManager
 ) ->
 ) ->
 	App.controller "IdeController", ["$scope", "$timeout", "ide", ($scope, $timeout, ide) ->
 	App.controller "IdeController", ["$scope", "$timeout", "ide", ($scope, $timeout, ide) ->
 		# Don't freak out if we're already in an apply callback
 		# Don't freak out if we're already in an apply callback
@@ -34,6 +37,7 @@ define [
 		}
 		}
 		$scope.ui = {
 		$scope.ui = {
 			leftMenuShown: false
 			leftMenuShown: false
+			view: "editor"
 		}
 		}
 
 
 		window._ide = ide
 		window._ide = ide
@@ -46,6 +50,7 @@ define [
 		ide.editorManager = new EditorManager(ide, $scope)
 		ide.editorManager = new EditorManager(ide, $scope)
 		ide.settingsManager = new SettingsManager(ide, $scope)
 		ide.settingsManager = new SettingsManager(ide, $scope)
 		ide.onlineUsersManager = new OnlineUsersManager(ide, $scope)
 		ide.onlineUsersManager = new OnlineUsersManager(ide, $scope)
+		ide.trackChangesManager = new TrackChangesManager(ide, $scope)
 	]
 	]
 
 
 	angular.bootstrap(document.body, ["SharelatexApp"])
 	angular.bootstrap(document.body, ["SharelatexApp"])

+ 34 - 0
services/web/public/coffee/app/ide/track-changes/TrackChangesListController.coffee

@@ -0,0 +1,34 @@
+define [
+	"base"
+], (App) ->
+	App.controller "TrackChangesListController", ["$scope", ($scope) ->
+		$scope.recalculateSelectedUpdates = () ->
+			inSelection = false
+			for update in $scope.trackChanges.updates
+				if update.selectedTo
+					inSelection = true
+				update.inSelection = inSelection
+				if update.selectedFrom
+					inSelection = false
+	]
+
+	App.controller "TrackChangesListItemController", ["$scope", ($scope) ->
+		$scope.$watch "update.selectedFrom", (selectedFrom) ->
+			if selectedFrom? 
+				if selectedFrom
+					for update in $scope.trackChanges.updates
+						update.selectedFrom = false unless update == $scope.update
+				$scope.recalculateSelectedUpdates()		
+
+		$scope.$watch "update.selectedTo", (selectedTo) ->
+			if selectedTo?
+				if selectedTo
+					for update in $scope.trackChanges.updates
+						update.selectedTo = false unless update == $scope.update
+				$scope.recalculateSelectedUpdates()
+
+		$scope.select = () ->
+			$scope.update.selectedTo = true
+			$scope.update.selectedFrom = true
+
+	]

+ 54 - 0
services/web/public/coffee/app/ide/track-changes/TrackChangesManager.coffee

@@ -0,0 +1,54 @@
+define [
+	"ide/track-changes/TrackChangesListController"
+], () ->
+	class TrackChangesManager
+		constructor: (@ide, @$scope) ->
+			@$scope.trackChanges = {
+				updates: []
+				nextBeforeTimestamp: null
+				atEnd: false
+			}
+
+			@$scope.toggleTrackChanges = () =>
+				if @$scope.ui.view == "track-changes"
+					@$scope.ui.view = "editor"
+				else
+					@$scope.ui.view = "track-changes"
+
+			@$scope.$on "file-tree:initialized", () =>
+				@fetchNextBatchOfChanges()
+
+		BATCH_SIZE: 4
+		fetchNextBatchOfChanges: () ->
+			url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
+			if @nextBeforeTimestamp?
+				url += "&before=#{@$scope.trackChanges.nextBeforeTimestamp}"
+			@ide.$http
+				.get(url)
+				.success (data) =>
+					@_loadUpdates(data.updates)
+					@$scope.trackChanges.nextBeforeTimestamp = data.nextBeforeTimestamp
+					if !data.nextBeforeTimestamp?
+						@$scope.trackChanges.atEnd = true
+
+		_loadUpdates: (updates = []) ->
+			previousUpdate = @$scope.trackChanges.updates[@$scope.trackChanges.updates.length - 1]
+
+			for update in updates
+				for doc_id, doc of update.docs or {}
+					doc.entity = @ide.fileTreeManager.findEntityById(doc_id)
+
+				for user in update.meta.users or []
+					user.hue = @ide.onlineUsersManager.getHueForUserId(user.id)
+
+				if !previousUpdate? or !moment(previousUpdate.meta.end_ts).isSame(update.meta.end_ts, "day")
+					update.meta.first_in_day = true
+
+				update.selectedFrom = false
+				update.selectedTo = false
+				update.inSelection = false
+
+				previousUpdate = update
+
+			@$scope.trackChanges.updates =
+				@$scope.trackChanges.updates.concat(updates)

+ 1 - 0
services/web/public/coffee/app/main.coffee

@@ -8,5 +8,6 @@ define [
 	"directives/equals"
 	"directives/equals"
 	"directives/fineUpload"
 	"directives/fineUpload"
 	"directives/onEnter"
 	"directives/onEnter"
+	"filters/formatDate"
 ], () ->
 ], () ->
 	angular.bootstrap(document.body, ["SharelatexApp"])
 	angular.bootstrap(document.body, ["SharelatexApp"])

+ 0 - 4
services/web/public/coffee/app/main/project-list.coffee

@@ -1,10 +1,6 @@
 define [
 define [
 	"base"
 	"base"
 ], (App) ->
 ], (App) ->
-	App.filter "formatDate", () ->
-		(date, format = "Do MMM YYYY, h:mm a") ->
-			moment(date).format(format)
-
 	App.factory "queuedHttp", ["$http", "$q", ($http, $q) ->
 	App.factory "queuedHttp", ["$http", "$q", ($http, $q) ->
 		pendingRequests = []
 		pendingRequests = []
 		inflight = false
 		inflight = false

+ 12 - 183
services/web/public/stylesheets/app/editor.less

@@ -1,3 +1,8 @@
+@import "./editor/file-tree.less";
+@import "./editor/track-changes.less";
+@import "./editor/toolbar.less";
+@import "./editor/left-menu.less";
+
 .full-size {
 .full-size {
 	position: absolute;
 	position: absolute;
 	top: 0;
 	top: 0;
@@ -15,147 +20,6 @@
 	margin-left: -200px;
 	margin-left: -200px;
 }
 }
 
 
-.toolbar {
-	height: 40px;
-	border-bottom: 1px solid @toolbar-border-color;
-
-	a {
-		display: inline-block;
-		color: @gray-light;
-		padding: 6px 12px 8px;
-		&:hover {
-			text-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
-			color: @gray-dark;
-		}
-	}
-
-	.btn-full-height {
-		border: none;
-		border-radius: 0;
-		border-right: 1px solid @toolbar-border-color;
-		color: @link-color;
-		padding: 6px 12px 8px;
-		&:hover {
-			text-shadow: 0 1px 0 rgba(0, 0, 0, 0.15);
-			background-color: darken(white, 10%);
-			color: @link-hover-color;
-		}
-	}
-
-	.toolbar-right {
-		float: right;
-		.btn-full-height {
-			border-right: 0;
-			border-left: 1px solid @toolbar-border-color;
-		}
-	}
-
-	&.toolbar-header {
-		box-shadow: 0 0 2px #ccc;
-		position: absolute;
-		top: 0;
-		left: 0;
-		right: 0;
-		z-index: 1;
-	}
-
-	&.toolbar-small {
-		height: 32px;
-		a {
-			padding: 4px 2px 2px;
-			margin-left: 6px;
-		}
-		.toolbar-right {
-			a {
-				margin-left: 0;
-				margin-right: 6px;
-			}
-		}
-	}
-}
-
-#file-tree {
-	background-color: #fafafa;
-
-	ul.file-tree-list {
-		font-size: 0.8rem;
-		margin: 0;
-		padding: (@line-height-computed / 4) 0;
-		position: absolute;
-		top: 32px;
-		bottom: 0;
-		left: 0;
-		right: 0;
-		overflow-y: scroll;
-
-		ul {
-			margin-left: (@line-height-computed / 2);
-		}
-
-		li {
-			line-height: 2.6;
-
-			.entity-name {
-				color: @gray-dark;
-				cursor: pointer;
-				white-space: nowrap;
-				overflow: hidden;
-				text-overflow: ellipsis;
-				&:hover {
-					background-color: @gray-lightest;
-				}
-				input {
-					line-height: 1.6;
-				}
-				&.droppable-hover {
-					background-color: @file-tree-droppable-background-color;
-				}
-			}
-
-			i.fa-folder-open, i.fa-folder {
-				color: lighten(desaturate(@link-color, 10%), 5%);
-				font-size: 14px;
-			}
-
-			i.fa-file, i.fa-image {
-				color: @gray-light;
-				font-size: 14px;
-			}
-
-			i.toggle {
-				width: 24px;
-				padding: 6px;
-				font-size: 0.7rem;
-				color: @gray
-			}
-
-			&.selected {
-				> .entity > .entity-name {
-					color: @link-color;
-					border-right: 4px solid @link-color;
-					font-weight: bold;
-					i.fa-folder-open, i.fa-folder, i.fa-file, i.fa-image {
-						color: @link-color;
-					}
-					padding-right: 32px;
-				}
-			}
-
-			.dropdown {
-				position: absolute;
-				right: 0;
-				> a {
-					padding: 0 12px;
-				}
-			}
-		}
-	}
-
-	ul.droppable-hover {
-		background-color: @file-tree-droppable-background-color;
-	}
-}
-
 #ide-body {
 #ide-body {
 	.full-size;
 	.full-size;
 	top: 40px;
 	top: 40px;
@@ -163,14 +27,14 @@
 
 
 #editor {
 #editor {
 	.full-size;
 	.full-size;
-}
 
 
-.loading {
-	.full-size;
-	padding-top: 10rem;
-	font-family: @font-family-serif;
-	text-align: center;
-	background-color: #fafafa;
+	.loading {
+		.full-size;
+		padding-top: 10rem;
+		font-family: @font-family-serif;
+		text-align: center;
+		background-color: #fafafa;
+	}
 }
 }
 
 
 // The internal components of the aceEditor directive
 // The internal components of the aceEditor directive
@@ -258,38 +122,3 @@
 	z-index: 100;
 	z-index: 100;
 }
 }
 
 
-#left-menu {
-	position: absolute;
-	width: 210px;
-	padding: 10px;
-	top: 0;
-	bottom: 0;
-	background-color: #f4f4f4;
-	z-index: 100;
-	overflow: auto;
-	-webkit-transition: left ease-in-out 0.35s;
-	transition: left ease-in-out 0.35s;
-
-	left: -280px;
-	&.shown {
-		left: 0;
-	}
-
-	h4 {
-		font-family: @font-family-serif;
-		font-weight: 400;
-		font-size: 1rem;
-		margin: (@line-height-computed / 2) 0;
-		padding-bottom: (@line-height-computed / 4);
-		color: @gray-light;
-		border-bottom: 1px solid @gray-light;
-		text-transform: uppercase;
-	}
-}
-
-#left-menu-mask {
-	.full-size;
-	opacity: 0.4;
-	background-color: #999;
-	z-index: 99;
-}

+ 82 - 0
services/web/public/stylesheets/app/editor/file-tree.less

@@ -0,0 +1,82 @@
+
+#file-tree {
+	background-color: #fafafa;
+
+	ul.file-tree-list {
+		font-size: 0.8rem;
+		margin: 0;
+		padding: (@line-height-computed / 4) 0;
+		position: absolute;
+		top: 32px;
+		bottom: 0;
+		left: 0;
+		right: 0;
+		overflow-y: auto;
+
+		ul {
+			margin-left: (@line-height-computed / 2);
+		}
+
+		li {
+			line-height: 2.6;
+
+			.entity-name {
+				color: @gray-dark;
+				cursor: pointer;
+				white-space: nowrap;
+				overflow: hidden;
+				text-overflow: ellipsis;
+				&:hover {
+					background-color: @gray-lightest;
+				}
+				input {
+					line-height: 1.6;
+				}
+				&.droppable-hover {
+					background-color: @file-tree-droppable-background-color;
+				}
+			}
+
+			i.fa-folder-open, i.fa-folder {
+				color: lighten(desaturate(@link-color, 10%), 5%);
+				font-size: 14px;
+			}
+
+			i.fa-file, i.fa-image {
+				color: @gray-light;
+				font-size: 14px;
+			}
+
+			i.toggle {
+				width: 24px;
+				padding: 6px;
+				font-size: 0.7rem;
+				color: @gray
+			}
+
+			&.selected {
+				> .entity > .entity-name {
+					color: @link-color;
+					border-right: 4px solid @link-color;
+					font-weight: bold;
+					i.fa-folder-open, i.fa-folder, i.fa-file, i.fa-image {
+						color: @link-color;
+					}
+					padding-right: 32px;
+				}
+			}
+
+			.dropdown {
+				position: absolute;
+				right: 0;
+				> a {
+					padding: 0 12px;
+				}
+			}
+		}
+	}
+
+	ul.droppable-hover {
+		background-color: @file-tree-droppable-background-color;
+	}
+}

+ 35 - 0
services/web/public/stylesheets/app/editor/left-menu.less

@@ -0,0 +1,35 @@
+#left-menu {
+	position: absolute;
+	width: 210px;
+	padding: 10px;
+	top: 0;
+	bottom: 0;
+	background-color: #f4f4f4;
+	z-index: 100;
+	overflow: auto;
+	-webkit-transition: left ease-in-out 0.35s;
+	transition: left ease-in-out 0.35s;
+
+	left: -280px;
+	&.shown {
+		left: 0;
+	}
+
+	h4 {
+		font-family: @font-family-serif;
+		font-weight: 400;
+		font-size: 1rem;
+		margin: (@line-height-computed / 2) 0;
+		padding-bottom: (@line-height-computed / 4);
+		color: @gray-light;
+		border-bottom: 1px solid @gray-light;
+		text-transform: uppercase;
+	}
+}
+
+#left-menu-mask {
+	.full-size;
+	opacity: 0.4;
+	background-color: #999;
+	z-index: 99;
+}

+ 63 - 0
services/web/public/stylesheets/app/editor/toolbar.less

@@ -0,0 +1,63 @@
+.toolbar {
+	height: 40px;
+	border-bottom: 1px solid @toolbar-border-color;
+
+	a {
+		display: inline-block;
+		color: @gray-light;
+		padding: 6px 12px 8px;
+		&:hover {
+			text-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
+			color: @gray-dark;
+		}
+	}
+
+	.btn-full-height {
+		border: none;
+		border-radius: 0;
+		border-right: 1px solid @toolbar-border-color;
+		color: @link-color;
+		padding: 6px 12px 8px;
+		&:hover {
+			text-shadow: 0 1px 0 rgba(0, 0, 0, 0.15);
+			background-color: darken(white, 10%);
+			color: @link-hover-color;
+		}
+		&.active, &:active {
+			color: white;
+			background-color: @link-color;
+			.box-shadow(inset 0 3px 5px rgba(0, 0, 0, 0.225));
+		}
+	}
+
+	.toolbar-right {
+		float: right;
+		.btn-full-height {
+			border-right: 0;
+			border-left: 1px solid @toolbar-border-color;
+		}
+	}
+
+	&.toolbar-header {
+		box-shadow: 0 0 2px #ccc;
+		position: absolute;
+		top: 0;
+		left: 0;
+		right: 0;
+		z-index: 1;
+	}
+
+	&.toolbar-small {
+		height: 32px;
+		a {
+			padding: 4px 2px 2px;
+			margin-left: 6px;
+		}
+		.toolbar-right {
+			a {
+				margin-left: 0;
+				margin-right: 6px;
+			}
+		}
+	}
+}

+ 318 - 0
services/web/public/stylesheets/app/editor/track-changes.less

@@ -0,0 +1,318 @@
+@changesListWidth:   250px;
+@changesListPadding: @line-height-computed / 2;
+
+@selector-padding-vertical: @line-height-computed / 3;
+@selector-padding-horizontal: @line-height-computed / 2;
+@day-header-height: 24px;
+
+@range-bar-color: @link-color;
+
+#trackChanges {
+	// .track-changes-diff {
+	// 	position: absolute;
+	// 	right: @changesListWidth + 1px;
+	// 	left: 0;
+	// 	top: 0;
+	// 	bottom: 0;
+	// 	height: 100%;
+	// 	.ace_editor {
+	// 		position: absolute;
+	// 		top: 42px;
+	// 		left: 0;
+	// 		right: 0;
+	// 		bottom: 0;
+	// 		.ace_active-line, .ace_cursor-layer, .ace_gutter-active-line {
+	// 			display: none;
+	// 		}
+	// 	}
+	// 	.track-changes-diff-toolbar {
+	// 		position: absolute;
+	// 		top: 0;
+	// 		left: 0;
+	// 		right: -1px;
+	// 		height: 32px;
+	// 		padding: 5px 5px 5px 5px;
+	// 		margin: 0;
+	// 		background-color: #282828;
+	// 		color: white;
+	// 		border-right: 1px solid white;
+	// 		.number-of-changes, .restore {
+	// 			position: absolute;
+	// 		}
+	// 		.number-of-changes {
+	// 			left: 10px;
+	// 			bottom: 7px;
+	// 		}
+	// 		.restore {
+	// 			right: 10px;
+	// 			bottom: 5px;
+	// 			padding: 3px 9px;
+	// 		}
+	// 	}
+	// }
+
+
+	// .track-changes-upgrade-control, .track-changes-upgrade-popup {
+	// 	position: absolute;
+	// 	top: 0;
+	// 	bottom: 0;
+	// 	left: 0;
+	// 	right: 0;
+	// 	z-index: 100;
+	// }
+
+	// .track-changes-upgrade-popup {
+	// 	background-color: rgba(128,128,128,0.4);
+	// 	.message {
+	// 		margin: auto;
+	// 		margin-top: 200px;
+	// 		padding: 10px 10px 14px 10px;
+	// 		width: 400px;
+	// 		font-weight: bold;
+	// 		text-align: center;
+	// 		background-color: white;
+	// 		.border-radius(8px);
+	// 	}
+	// }
+
+	// .track-changes-upgrade-control {
+	// 	background-color: #eeeeee;
+	// 	text-align: center;
+	// 	.message {
+	// 		font-size: 18px;
+	// 		margin: 12px;
+	// 		margin-top: 36px;
+	// 	}
+	// }
+
+	// .deleted-background,
+	// .deleted-foreground,
+	// .inserted-background,
+	// .name-marker,
+	// .changes-before,
+	// .changes-after {
+	// 	position: absolute;
+	// 	z-index: 2;
+	// }
+
+	// .name-marker {
+	// 	font-size: 0.8em;
+	// 	padding: 2px 6px;
+	// 	.border-radius(3px 3px 3px 3px);
+	// 	position: absolute;
+	// 	border: 1px solid #999;
+	// 	left: 0;
+	// 	white-space: pre;
+	// }
+
+	// .changes-before {
+	// 	top: 6px;
+	// 	right: 6px;
+	// }
+	// .changes-after {
+	// 	bottom: 6px;
+	// 	right: 6px;
+	// }
+	// .changes-before, .changes-after {
+	// 	padding: 4px 8px;
+	// 	background-color: #eee;
+	// 	border: 1px solid #999;
+	// 	.border-radius(3px);
+	// }
+
+
+	aside.change-list {
+		border-left: 1px solid @toolbar-border-color;
+		height: 100%;
+		width: @changesListWidth;
+		position: absolute;
+		right: 0;
+		background-color: white;
+
+		ul {
+			li.change {
+				position: relative;
+				cursor: pointer;
+				user-select: none;
+				-ms-user-select: none;
+				-moz-user-select: none;
+				-webkit-user-select: none;
+
+				.day {
+					background-color: #fafafa;
+					border-bottom: 1px solid @toolbar-border-color;
+					padding: 4px;
+					font-weight: bold;
+					text-align: center;
+					height: @day-header-height;
+					font-size: 14px;
+					line-height: 1;
+				}
+				.selectors {
+					input {
+						margin: 0;
+					}
+					position: absolute;
+					left: @selector-padding-horizontal;
+					top: 0;
+					bottom: 0;
+					width: 24px;
+					.selector-from {
+						position: absolute;
+						bottom: @selector-padding-vertical;
+						left: 0;
+						opacity: 0.8;
+					}
+					.selector-to {
+						position: absolute;
+						top: @selector-padding-vertical;
+						left: 0;
+						opacity: 0.8;
+					}
+					.range {
+						position: absolute;
+						left: 5px;
+						width: 4px;
+						top: 0;
+						bottom: 0;
+					}
+				}
+				.description {
+					padding: (@line-height-computed / 4);
+					padding-left: 38px;
+					min-height: 38px;
+					border-bottom: 1px solid @toolbar-border-color;
+				}
+				.user {
+					font-size: 0.8rem;
+					color: @gray;
+					text-transform: capitalize;
+				}
+				.time {
+					float: right;
+					color: @gray;
+					display: inline-block;
+					padding-right: (@line-height-computed / 2);
+					font-size: 0.8rem;
+					line-height: @line-height-computed;
+				}
+				// .color-square {
+				// 	display: inline-block;
+				// 	height: 10px;
+				// 	width: 10px;
+				// 	margin-right: 4px;
+				// 	margin-bottom: -1px;
+				// }
+				.docs {
+					font-weight: bold;
+					font-size: 0.9rem;
+				}
+				&:hover {
+					background-color: @gray-lightest;
+				}
+			}
+			li.loading-changes, li.empty-message {
+				padding: 6px;
+				cursor: default;
+				&:hover {
+					background-color: inherit;
+				}
+			}
+			li.selected {
+				border-left: 4px solid @range-bar-color;
+				.description {
+					padding-left: 34px;
+				}
+				.selectors {
+					left: @selector-padding-horizontal - 4px;
+					.range {
+						background-color: @range-bar-color;
+					}
+				}
+			}
+			li.selected-to {
+				.selectors {
+					.range {
+						top: 10px;
+					}
+					.selector-to {
+						opacity: 1;
+					}
+				}
+			}
+			li.selected-from {
+				.selectors {
+					.range {
+						bottom: 10px;
+					}
+					.selector-from {
+						opacity: 1;
+					}
+				}
+			}
+			li.first-in-day {
+				.day {
+					display: block;
+				}
+				.selectors {
+					.selector-to {
+						top: @day-header-height + @selector-padding-vertical;
+					}
+				}
+			}
+			li.first-in-day.selected-to {
+				.selectors {
+					.range {
+						top: 33px;
+					}
+				}
+			}
+		}
+		ul.list.hover-state {
+			li {
+				.selectors {
+					.range {
+						background-color: transparent;
+						top: 0;
+						bottom: 0;
+					}
+				}
+			}
+			li.hover-selected {
+				.selectors {
+					.range {
+						top: 0;
+						background-color: #999;
+					}
+				}
+			}
+			li.hover-selected-to {
+				.selectors {
+					.range {
+						top: 10px;
+					}
+					.selector-to {
+						opacity: 1;
+					}
+				}
+			}
+			li.hover-selected-from {
+				.selectors {
+					.range {
+						bottom: 10px;
+					}
+					.selector-from {
+						opacity: 1;
+					}
+				}
+			}
+			li.first-in-day.hover-selected-to {
+				.selectors {
+					.range {
+						top: 33px;
+					}
+				}
+			}
+		}
+	}
+}