Index: src/wp-admin/upload.php
===================================================================
--- src/wp-admin/upload.php	(revision 28973)
+++ src/wp-admin/upload.php	(working copy)
@@ -22,16 +22,9 @@
 
 if ( 'grid' === $mode ) {
 	wp_enqueue_media();
+	wp_enqueue_script( 'media-grid' );
 	wp_enqueue_script( 'media' );
 	require_once( ABSPATH . 'wp-admin/admin-header.php' );
-	?><div class="view-switch media-grid-view-switch">
-		<a href="<?php echo esc_url( add_query_arg( 'mode', 'list', $_SERVER['REQUEST_URI'] ) ) ?>" class="view-list">
-			<img id="view-switch-list" src="<?php echo includes_url( 'images/blank.gif' ) ?>" width="20" height="20" title="List View" alt="List View"/>
-		</a>
-		<a href="<?php echo esc_url( add_query_arg( 'mode', 'grid', $_SERVER['REQUEST_URI'] ) ) ?>" class="view-grid current">
-			<img id="view-switch-excerpt" src="<?php echo includes_url( 'images/blank.gif' ) ?>" width="20" height="20" title="Grid View" alt="Grid View"/>
-		</a>
-	</div><?php
 	include( ABSPATH . 'wp-admin/admin-footer.php' );
 	exit;
 }
Index: src/wp-includes/css/media-views.css
===================================================================
--- src/wp-includes/css/media-views.css	(revision 28973)
+++ src/wp-includes/css/media-views.css	(working copy)
@@ -750,6 +750,30 @@
 	max-height: 100%;
 }
 
+.attachment-preview.type-audio .thumbnail,
+.attachment-preview.type-video .thumbnail {
+	z-index: 1;
+	margin: 5%;
+	max-width: 90%;
+	max-height: 90%;
+}
+
+.media-frame-content .attachment-preview.type-audio .icon,
+.media-frame-content .attachment-preview.type-video .icon {
+	z-index: 2;
+	background: #f1f1f1;
+	position: relative;
+	padding: 0;
+	top: 15%;
+	left: auto;
+	right: auto;
+}
+
+.attachment-preview.type-audio .filename,
+.attachment-preview.type-video .filename {
+	z-index: 3;
+}
+
 .attachment-preview .thumbnail:after {
 	content: '';
 	display: block;
@@ -909,6 +933,22 @@
 	border-radius: 0;
 }
 
+.attachment .data-fields {
+	margin: 5px 0 0;
+}
+
+.attachment .data-field {
+	white-space: nowrap;
+	text-overflow: ellipsis;
+	overflow: hidden;
+	display: block;
+	line-height: 19px;
+	height: 19px;
+	text-align: left;
+	width: 90%;
+	margin: 0 5%;
+}
+
 /**
  * Attachments Browser
  */
@@ -924,6 +964,10 @@
 	height: 50px;
 }
 
+.attachments-browser.hide-sidebar .media-toolbar {
+	right: 0;
+}
+
 .attachments-browser .media-toolbar-primary > .media-button,
 .attachments-browser .media-toolbar-primary > .media-button-group,
 .attachments-browser .media-toolbar-secondary > .media-button,
@@ -942,6 +986,43 @@
 	outline: none;
 }
 
+/**
+ * Copied styles from the theme browser view.
+ *
+ * This should be OOCSS'd so both use a shared selector.
+ */
+.attachment .edit-media {
+	-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
+	opacity: 0;
+	position: absolute;
+	z-index: 4;
+	top: 25%;
+	right: 25%;
+	left: 25%;
+	background: #222;
+	background: rgba(0,0,0,0.7);
+	color: #fff;
+	font-size: 15px;
+	text-shadow: 0 1px 0 rgba(0,0,0,0.6);
+	-webkit-font-smoothing: antialiased;
+	font-weight: 600;
+	padding: 10px 0;
+	text-align: center;
+	-webkit-border-radius: 3px;
+	border-radius: 3px;
+	-webkit-transition: opacity 0.1s ease-in-out;
+	transition: opacity 0.1s ease-in-out;
+}
+
+.attachment:hover .edit-media {
+	-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
+	opacity: 1;
+}
+
+.attachments-browser.hide-sidebar .attachments {
+	right: 0;
+}
+
 .attachments-browser .instructions {
 	display: inline-block;
 	margin-top: 16px;
@@ -2388,11 +2469,11 @@
 	line-height: 29px;
 }
 
-.media-grid-view-switch {
-	position: fixed;
-	right: 10px;
-	top: 44px;
-	z-index: 300;
+.media-grid-view .view-switch {
+	display: inline-block;
+	float: none;
+	margin-top: 13px;
+	vertical-align: middle;
 }
 
 /**
@@ -2427,7 +2508,217 @@
 	display: none;
 }
 
+/**
+ * Copied styles from the Add theme toolbar.
+ *
+ * This should be OOCSS'd so both use a shared selector.
+ */
+.media-grid-view .media-toolbar {
+	background: #fff;
+	-webkit-box-shadow: 0 1px 1px 0 rgba(0,0,0,.1);
+	box-shadow: 0 1px 1px 0 rgba(0,0,0,.1);
+	-webkit-box-sizing: border-box;
+	-moz-box-sizing: border-box;
+	box-sizing: border-box;
+	color: #555;
+	display: inline-block;
+	font-size: 13px;
+	padding: 0 20px;
+	position: relative;
+	width: 100%;
+}
+
+/**
+ * The left and right buttons are copied from the expanded theme details modal.
+ *
+ * This should be OOCSS'd so both use a shared selector.
+ */
+.edit-attachment-frame .edit-media-header .left,
+.edit-attachment-frame .edit-media-header .right {
+	cursor: pointer;
+	color: #777;
+	background-color: transparent;
+	height: 48px;
+	width: 54px;
+	float: left;
+	text-align: center;
+	border: 0;
+	border-right: 1px solid #ddd;
+}
+
+.edit-attachment-frame .edit-media-header .right:before,
+.edit-attachment-frame .edit-media-header .left:before {
+	font: normal 20px/50px 'dashicons' !important;
+	display: inline;
+	font-weight: 300;
+}
+
+
+.edit-attachment-frame .edit-media-header .left:before {
+	content: '\f340';
+}
+
+.edit-attachment-frame .edit-media-header .right:before {
+	content: '\f344';
+}
+
+.edit-attachment-frame .edit-media-header .left.disabled,
+.edit-attachment-frame .edit-media-header .right.disabled,
+.edit-attachment-frame .edit-media-header .left.disabled:hover,
+.edit-attachment-frame .edit-media-header .right.disabled:hover {
+	color: #ccc;
+	background: inherit;
+	cursor: inherit;
+}
+
+.edit-attachment-frame .edit-media-header .close:hover,
+.edit-attachment-frame .edit-media-header .right:hover,
+.edit-attachment-frame .edit-media-header .left:hover,
+.edit-attachment-frame .edit-media-header .close:focus,
+.edit-attachment-frame .edit-media-header .right:focus,
+.edit-attachment-frame .edit-media-header .left:focus {
+	background: #0074a2;
+	color: #fff;
+}
+
+.edit-attachment-frame .media-frame-content,
+.edit-attachment-frame .media-frame-router {
+	left: 0;
+}
+
+/* Hiding this for the moment instead of removing it from the template. */
+.edit-attachment-frame h3 {
+	display: none;
+}
+
+.edit-attachment-frame .attachment-details {
+	position: absolute;
+	overflow: auto;
+	top: 0;
+	bottom: 0;
+	right: 0;
+	left: 0;
+}
+
+.edit-attachment-frame .attachment-info {
+	border-bottom: 0;
+	border-right: 1px solid #ddd;
+	bottom: 0;
+	position: absolute;
+	top: 0;
+	left: 0;
+	margin-bottom: 0;
+	padding: 2% 4%;
+	right: 50%;
+}
+
+.edit-attachment-frame .attachment-info .thumbnail {
+	max-width: none;
+	max-height: none;
+}
+
+.edit-attachment-frame .attachment-info .thumbnail-image img {
+	margin: 0;
+}
+
+.edit-attachment-frame .attachment-info .thumbnail-image:after {
+	-webkit-box-shadow: none;
+	        box-shadow: none;
+}
+
+.edit-attachment-frame .attachment-info .thumbnail img {
+	max-width: none;
+	max-height: 50%;
+}
+
+.edit-attachment-frame .attachment-info .details {
+	float: none;
+}
+
+.edit-attachment-frame .attachment-fields {
+	bottom: 0;
+	padding: 2% 4%;
+	position: absolute;
+	top: 0;
+	left: 50%;
+	right: 0;
+}
+
+.edit-attachment-frame .attachment-fields .setting {
+	display: block;
+	float: left;
+	width: 100%;
+	margin: 1px 0;
+}
+
+.edit-attachment-frame .attachment-fields .setting label {
+	display: block;
+}
+
+.edit-attachment-frame .attachment-fields .setting .link-to-custom {
+	margin: 3px 0;
+}
+
+.edit-attachment-frame .attachment-fields .setting .name {
+	min-width: 30%;
+	margin-right: 4%;
+	font-size: 12px;
+	text-align: right;
+}
+
+.edit-attachment-frame .attachment-fields .setting select {
+	max-width: 65%;
+}
+
+.edit-attachment-frame .attachment-fields .setting input[type="checkbox"],
+.edit-attachment-frame .attachment-fields .field input[type="checkbox"] {
+	width: 16px;
+	float: none;
+	margin: 8px 3px 0;
+	padding: 0;
+}
+
+.edit-attachment-frame .attachment-fields .setting span {
+	float: left;
+	min-height: 22px;
+	padding-top: 8px;
+	line-height: 16px;
+	font-weight: normal;
+	color: #666;
+}
+
+.edit-attachment-frame .attachment-fields .setting input[type="text"],
+.edit-attachment-frame .attachment-fields .setting input[type="password"],
+.edit-attachment-frame .attachment-fields .setting input[type="number"],
+.edit-attachment-frame .attachment-fields .setting input[type="search"],
+.edit-attachment-frame .attachment-fields .setting input[type="email"],
+.edit-attachment-frame .attachment-fields .setting input[type="url"],
+.edit-attachment-frame .attachment-fields .setting textarea,
+.edit-attachment-frame .attachment-fields .setting .value {
+	margin: 1px;
+	width: 65%;
+	float: right;
+	padding: 6px 8px;
+	-webkit-box-sizing: border-box;
+	-moz-box-sizing: border-box;
+	box-sizing: border-box;
+}
+
+.edit-attachment-frame .attachment-fields .setting textarea {
+	height: 62px;
+	resize: vertical;
+}
+
+.edit-attachment-frame .attachment-fields select {
+	margin-top: 3px;
+}
+
+.media-grid-view.hide-router .media-frame-title {
+	box-shadow: none;
+}
+
 .media-grid-view .media-frame-content {
+	background-color: transparent;
 	bottom: 40px;
 }
 @media screen and (max-width: 782px) {
Index: src/wp-includes/js/media-grid.js
===================================================================
--- src/wp-includes/js/media-grid.js	(revision 0)
+++ src/wp-includes/js/media-grid.js	(working copy)
@@ -0,0 +1,532 @@
+(function($, _, Backbone, wp) {
+	var media = wp.media, l10n;
+
+	// Link any localized strings.
+	if ( media.view.l10n ) {
+		l10n = media.view.l10n;
+	} else {
+		l10n = media.view.l10n = typeof _wpMediaViewsL10n === 'undefined' ? {} : _wpMediaViewsL10n;
+		delete l10n.settings;
+	}
+
+	/**
+	 * A more abstracted state, because media.controller.State expects
+	 * specific regions (menu, title, etc.) to exist on the frame, which do not
+	 * exist in media.view.Frame.EditAttachment.
+	 */
+	media.controller._State = Backbone.Model.extend({
+		constructor: function() {
+			this.on( 'activate', this._preActivate, this );
+			this.on( 'activate', this.activate, this );
+			this.on( 'activate', this._postActivate, this );
+			this.on( 'deactivate', this._deactivate, this );
+			this.on( 'deactivate', this.deactivate, this );
+			this.on( 'reset', this.reset, this );
+			this.on( 'ready', this.ready, this );
+			/**
+			 * Call parent constructor with passed arguments
+			 */
+			Backbone.Model.apply( this, arguments );
+		},
+
+		/**
+		 * @abstract
+		 */
+		ready: function() {},
+		/**
+		 * @abstract
+		 */
+		activate: function() {},
+		/**
+		 * @abstract
+		 */
+		deactivate: function() {},
+		/**
+		 * @abstract
+		 */
+		reset: function() {},
+		/**
+		 * @access private
+		 */
+		_preActivate: function() {
+			this.active = true;
+		},
+		/**
+		 * @access private
+		 */
+		_postActivate: function() {},
+		/**
+		 * @access private
+		 */
+		_deactivate: function() {
+			this.active = false;
+		}
+	});
+
+	/**
+	 * A state for editing (cropping, etc.) an image.
+	 *
+	 * @constructor
+	 * @augments wp.media.controller.State
+	 * @augments Backbone.Model
+	 */
+	media.controller.EditImageNoFrame = media.controller._State.extend({
+		defaults: {
+			id:      'edit-attachment',
+			title:   l10n.editImage,
+			// Region mode defaults.
+			menu:    false,
+			router:  'edit-metadata',
+			content: 'edit-metadata',
+			toolbar: 'toolbar',
+
+			url:     ''
+		},
+
+		initialize: function() {
+			media.controller._State.prototype.initialize.apply( this, arguments );
+		},
+
+		activate: function() {
+			this.listenTo( this.frame, 'toolbar:render:edit-image', this.toolbar );
+		},
+
+		_postActivate: function() {
+			this._content();
+			this._router();
+		},
+
+		deactivate: function() {
+			this.stopListening( this.frame );
+		},
+
+		toolbar: function() {
+			var frame = this.frame,
+				lastState = frame.lastState(),
+				previous = lastState && lastState.id;
+
+			frame.toolbar.set( new media.view.Toolbar({
+				controller: frame,
+				items: {
+					back: {
+						style: 'primary',
+						text:     l10n.back,
+						priority: 20,
+						click:    function() {
+							if ( previous ) {
+								frame.setState( previous );
+							} else {
+								frame.close();
+							}
+						}
+					}
+				}
+			}) );
+		},
+
+		/**
+		 * @access private
+		 */
+		_router: function() {
+			var router = this.frame.router,
+				mode = this.get('router'),
+				view;
+
+			this.frame.$el.toggleClass( 'hide-router', ! mode );
+			if ( ! mode ) {
+				return;
+			}
+
+			this.frame.router.render( mode );
+
+			view = router.get();
+			if ( view && view.select ) {
+				view.select( this.frame.content.mode() );
+			}
+		},
+
+		_content: function() {
+			var mode = this.get( 'content' );
+			if ( mode ) {
+				this.frame[ 'content' ].render( mode );
+			}
+		}
+	});
+
+	/**
+	 * wp.media.view.MediaFrame.Manage
+	 *
+	 * A generic management frame workflow.
+	 *
+	 * Used in the media grid view.
+	 *
+	 * @constructor
+	 * @augments wp.media.view.MediaFrame
+	 * @augments wp.media.view.Frame
+	 * @augments wp.media.View
+	 * @augments wp.Backbone.View
+	 * @augments Backbone.View
+	 * @mixes wp.media.controller.StateMachine
+	 */
+	media.view.MediaFrame.Manage = media.view.MediaFrame.extend({
+		/**
+		 * @global wp.Uploader
+		 */
+		initialize: function() {
+			_.defaults( this.options, {
+				title:     l10n.mediaLibraryTitle,
+				modal:     false,
+				selection: [],
+				library:   {},
+				multiple:  false,
+				state:     'library',
+				uploader:  true,
+				mode:      [ 'grid', 'edit' ]
+			});
+
+			// Ensure core and media grid view UI is enabled.
+			this.$el.addClass('wp-core-ui media-grid-view');
+
+			// Force the uploader off if the upload limit has been exceeded or
+			// if the browser isn't supported.
+			if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported ) {
+				this.options.uploader = false;
+			}
+
+			// Initialize a window-wide uploader.
+			if ( this.options.uploader ) {
+				this.uploader = new media.view.UploaderWindow({
+					controller: this,
+					uploader: {
+						dropzone:  $('body'),
+						container: $('body')
+					}
+				}).render();
+				this.uploader.ready();
+				$('body').append( this.uploader.el );
+
+				this.options.uploader = false;
+			}
+
+			/**
+			 * call 'initialize' directly on the parent class
+			 */
+			media.view.MediaFrame.prototype.initialize.apply( this, arguments );
+
+			// Since we're not using the default modal built into
+			// a media frame, append our $element to the supplied container.
+			this.$el.appendTo( this.options.container );
+
+			this.createSelection();
+			this.createStates();
+			this.bindHandlers();
+			this.render();
+		},
+
+		createSelection: function() {
+			var selection = this.options.selection;
+
+			if ( ! (selection instanceof media.model.Selection) ) {
+				this.options.selection = new media.model.Selection( selection, {
+					multiple: this.options.multiple
+				});
+			}
+
+			this._selection = {
+				attachments: new media.model.Attachments(),
+				difference: []
+			};
+		},
+
+		createStates: function() {
+			var options = this.options;
+
+			if ( this.options.states ) {
+				return;
+			}
+
+			// Add the default states.
+			this.states.add([
+				new media.controller.Library({
+					library:    media.query( options.library ),
+					multiple:   options.multiple,
+					title:      options.title,
+					priority:   20,
+					toolbar:    false,
+					router:     false,
+					content:    'browse',
+					filterable: 'mime-types'
+				})
+			]);
+		},
+
+		bindHandlers: function() {
+			this.on( 'content:create:browse', this.browseContent, this );
+			this.on( 'content:render:edit-image', this.editImageContent, this );
+
+			// Handle a frame-level event for editing an attachment.
+			this.on( 'edit:attachment', this.editAttachment, this );
+			this.on( 'edit:attachment:next', this.editNextAttachment, this );
+			this.on( 'edit:attachment:previous', this.editPreviousAttachment, this );
+		},
+
+		editPreviousAttachment: function( currentModel ) {
+			var library = this.state().get('library'),
+			    currentModelIndex = library.indexOf( currentModel );
+			this.trigger( 'edit:attachment', library.at( currentModelIndex - 1 ) );
+		},
+
+		editNextAttachment: function( currentModel ) {
+			var library = this.state().get('library'),
+			    currentModelIndex = library.indexOf( currentModel );
+			this.trigger( 'edit:attachment', library.at( currentModelIndex + 1 ) );
+		},
+
+		/**
+		 * Open the Edit Attachment modal.
+		 */
+		editAttachment: function( model ) {
+			var library = this.state().get('library'), hasPrevious, hasNext;
+			if ( library.indexOf( model ) > 0 ) {
+				hasPrevious = true;
+			}
+			else {
+				hasPrevious = false;
+			}
+			if ( library.indexOf( model ) < library.length - 1 ) {
+				hasNext = true;
+			}
+			else {
+				hasNext = false;
+			}
+
+			new media.view.Frame.EditAttachment({
+				hasPrevious:    hasPrevious,
+				hasNext:        hasNext,
+				model:          model,
+				gridController: this
+			});
+		},
+
+		/**
+		 * Content
+		 *
+		 * @param {Object} content
+		 * @this wp.media.controller.Region
+		 */
+		browseContent: function( content ) {
+			var state = this.state();
+
+			// Browse our library of attachments.
+			content.view = new media.view.AttachmentsBrowser({
+				controller: this,
+				collection: state.get('library'),
+				selection:  state.get('selection'),
+				model:      state,
+				sortable:   state.get('sortable'),
+				search:     state.get('searchable'),
+				filters:    state.get('filterable'),
+				display:    state.get('displaySettings'),
+				dragInfo:   state.get('dragInfo'),
+				bulkEdit:   true,
+				sidebar:    false,
+
+				suggestedWidth:  state.get('suggestedWidth'),
+				suggestedHeight: state.get('suggestedHeight'),
+
+				AttachmentView: state.get('AttachmentView')
+			});
+		},
+
+		editImageContent: function() {
+			var image = this.state().get('image'),
+				view = new media.view.EditImage( { model: image, controller: this } ).render();
+
+			this.content.set( view );
+
+			// after creating the wrapper view, load the actual editor via an ajax call
+			view.loadEditor();
+
+		}
+	});
+
+	media.view.Attachment.Details.TwoColumn = media.view.Attachment.Details.extend({
+		template: wp.template( 'attachment-details-two-column' ),
+
+		initialize: function() {
+			this.$el.attr('aria-label', this.model.attributes.title).attr('aria-checked', false);
+			this.model.on( 'change:sizes change:uploading', this.render, this );
+			this.model.on( 'change:title', this._syncTitle, this );
+			this.model.on( 'change:caption', this._syncCaption, this );
+			this.model.on( 'change:percent', this.progress, this );
+
+			// Update the selection.
+			this.model.on( 'add', this.select, this );
+			this.model.on( 'remove', this.deselect, this );
+		}
+	});
+
+	/**
+	 * A frame for editing the details of a specific media item.
+	 *
+	 * Opens in a modal by default.
+	 *
+	 * Requires an attachment model to be passed in the options hash under `model`.
+	 */
+	media.view.Frame.EditAttachment = media.view.Frame.extend({
+
+		className: 'edit-attachment-frame',
+		template: media.template( 'edit-attachment-frame' ),
+		regions:   [ 'router', 'content' ],
+
+		events: {
+			'click':                    'collapse',
+			'click .delete-media-item': 'deleteMediaItem',
+			'click .left':              'previousMediaItem',
+			'click .right':             'nextMediaItem'
+		},
+
+		initialize: function( options ) {
+			var self = this;
+			media.view.Frame.prototype.initialize.apply( this, arguments );
+
+			_.defaults( this.options, {
+				modal:    true,
+				state: 'edit-attachment'
+			});
+
+			this.createStates();
+
+			this.on( 'content:render:edit-metadata', this.editMetadataContent, this );
+			this.on( 'content:render:edit-image', this.editImageContentUgh, this );
+
+			// Only need a tab to Edit Image for images.
+			if ( this.model.get( 'type' ) === 'image' ) {
+				this.on( 'router:create', this.createRouter, this );
+				this.on( 'router:render', this.browseRouter, this );
+			}
+
+			// Initialize modal container view.
+			if ( this.options.modal ) {
+				this.modal = new media.view.Modal({
+					controller: this,
+					title:      this.options.title
+				});
+
+				// Completely destroy the modal DOM element when closing it.
+				this.modal.close = function() {
+					self.modal.remove();
+				};
+
+				this.modal.content( this );
+				this.modal.open();
+			}
+		},
+
+		/**
+		 * Add the default states to the frame.
+		 */
+		createStates: function() {
+			this.states.add([
+				new media.controller.EditImageNoFrame( { model: this.model } )
+			]);
+		},
+
+		/**
+		 * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining
+		 */
+		render: function() {
+			// Activate the default state if no active state exists.
+			if ( ! this.state() && this.options.state ) {
+				this.setState( this.options.state );
+			}
+			/**
+			 * call 'render' directly on the parent class
+			 */
+			return media.view.Frame.prototype.render.apply( this, arguments );
+		},
+
+		/**
+		 * Content region rendering callback for the `edit-metadata` mode.
+		 */
+		editMetadataContent: function() {
+			var view = new media.view.Attachment.Details.TwoColumn({
+				controller: this,
+				model:      this.model
+			});
+			this.content.set( view );
+		},
+
+		/**
+		 * For some reason the view doesn't exist in the DOM yet, don't have the
+		 * patience to track this down right now.
+		 */
+		editImageContentUgh: function() {
+			_.defer( _.bind( this.editImageContent, this ) );
+		},
+
+		/**
+		 * Render the EditImage view into the frame's content region.
+		 */
+		editImageContent: function() {
+			var view = new media.view.EditImage( { model: this.model, controller: this } ).render();
+
+			this.content.set( view );
+
+			// after creating the wrapper view, load the actual editor via an ajax call
+			view.loadEditor();
+		},
+
+		/**
+		 * Create the router view.
+		 *
+		 * @param {Object} router
+		 * @this wp.media.controller.Region
+		 */
+		createRouter: function( router ) {
+			router.view = new media.view.Router({
+				controller: this
+			});
+		},
+
+		/**
+		 * Router rendering callback.
+		 *
+		 * @param  media.view.Router view Instantiated in this.createRouter()
+		 */
+		browseRouter: function( view ) {
+			view.set({
+				'edit-metadata': {
+					text:     'Edit Metadata',
+					priority: 20
+				},
+				'edit-image': {
+					text:     'Edit Image',
+					priority: 40
+				}
+			});
+		},
+
+		/**
+		 * Click handler to switch to the previous media item.
+		 */
+		previousMediaItem: function() {
+			if ( ! this.options.hasPrevious )
+				return;
+			this.modal.close();
+			this.options.gridController.trigger( 'edit:attachment:previous', this.model );
+		},
+
+		/**
+		 * Click handler to switch to the next media item.
+		 */
+		nextMediaItem: function() {
+			if ( ! this.options.hasNext )
+				return;
+			this.modal.close();
+			this.options.gridController.trigger( 'edit:attachment:next', this.model );
+		}
+
+	});
+
+}(jQuery, _, Backbone, wp));
\ No newline at end of file
Index: src/wp-includes/js/media-views.js
===================================================================
--- src/wp-includes/js/media-views.js	(revision 28973)
+++ src/wp-includes/js/media-views.js	(working copy)
@@ -1758,7 +1758,8 @@
 			_.defaults( this.options, {
 				title:    '',
 				modal:    true,
-				uploader: true
+				uploader: true,
+				mode:     ['select']
 			});
 
 			// Ensure core UI is enabled.
@@ -1955,160 +1956,6 @@
 	});
 
 	/**
-	 * wp.media.view.MediaFrame.Manage
-	 *
-	 * A generic management frame workflow.
-	 *
-	 * Used in the media grid view.
-	 *
-	 * @constructor
-	 * @augments wp.media.view.MediaFrame
-	 * @augments wp.media.view.Frame
-	 * @augments wp.media.View
-	 * @augments wp.Backbone.View
-	 * @augments Backbone.View
-	 * @mixes wp.media.controller.StateMachine
-	 */
-	media.view.MediaFrame.Manage = media.view.MediaFrame.extend({
-		/**
-		 * @global wp.Uploader
-		 */
-		initialize: function() {
-			_.defaults( this.options, {
-				title:     l10n.mediaLibraryTitle,
-				modal:     false,
-				selection: [],
-				library:   {},
-				multiple:  false,
-				state:     'library',
-				uploader:  true
-			});
-
-			// Ensure core and media grid view UI is enabled.
-			this.$el.addClass('wp-core-ui media-grid-view');
-
-			// Force the uploader off if the upload limit has been exceeded or
-			// if the browser isn't supported.
-			if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported ) {
-				this.options.uploader = false;
-			}
-
-			// Initialize a window-wide uploader.
-			if ( this.options.uploader ) {
-				this.uploader = new media.view.UploaderWindow({
-					controller: this,
-					uploader: {
-						dropzone:  $('body'),
-						container: $('body')
-					}
-				}).render();
-				this.uploader.ready();
-				$('body').append( this.uploader.el );
-
-				this.options.uploader = false;
-			}
-
-			/**
-			 * call 'initialize' directly on the parent class
-			 */
-			media.view.MediaFrame.prototype.initialize.apply( this, arguments );
-
-			// Since we're not using the default modal built into
-			// a media frame, append our $element to the supplied container.
-			this.$el.appendTo( this.options.container );
-
-			this.createSelection();
-			this.createStates();
-			this.bindHandlers();
-			this.render();
-		},
-
-		createSelection: function() {
-			var selection = this.options.selection;
-
-			if ( ! (selection instanceof media.model.Selection) ) {
-				this.options.selection = new media.model.Selection( selection, {
-					multiple: this.options.multiple
-				});
-			}
-
-			this._selection = {
-				attachments: new media.model.Attachments(),
-				difference: []
-			};
-		},
-
-		createStates: function() {
-			var options = this.options;
-
-			if ( this.options.states ) {
-				return;
-			}
-
-			// Add the default states.
-			this.states.add([
-				new media.controller.Library({
-					library:    media.query( options.library ),
-					multiple:   options.multiple,
-					title:      options.title,
-					priority:   20,
-					toolbar:    false,
-					router:     false,
-					content:    'browse',
-					filterable: 'mime-types'
-				}),
-
-				new media.controller.EditImage( { model: options.editImage } )
-			]);
-		},
-
-		bindHandlers: function() {
-			this.on( 'content:create:browse', this.browseContent, this );
-			this.on( 'content:render:edit-image', this.editImageContent, this );
-		},
-
-		/**
-		 * Content
-		 *
-		 * @param {Object} content
-		 * @this wp.media.controller.Region
-		 */
-		browseContent: function( content ) {
-			var state = this.state();
-
-			// Browse our library of attachments.
-			content.view = new media.view.AttachmentsBrowser({
-				controller: this,
-				collection: state.get('library'),
-				selection:  state.get('selection'),
-				model:      state,
-				sortable:   state.get('sortable'),
-				search:     state.get('searchable'),
-				filters:    state.get('filterable'),
-				display:    state.get('displaySettings'),
-				dragInfo:   state.get('dragInfo'),
-				bulkEdit:   true,
-
-				suggestedWidth:  state.get('suggestedWidth'),
-				suggestedHeight: state.get('suggestedHeight'),
-
-				AttachmentView: state.get('AttachmentView')
-			});
-		},
-
-		editImageContent: function() {
-			var image = this.state().get('image'),
-				view = new media.view.EditImage( { model: image, controller: this } ).render();
-
-			this.content.set( view );
-
-			// after creating the wrapper view, load the actual editor via an ajax call
-			view.loadEditor();
-
-		}
-	});
-
-	/**
 	 * wp.media.view.MediaFrame.Select
 	 *
 	 * Type of media frame that is used to select an item or items from the media library
@@ -4665,7 +4512,7 @@
 			var selection = this.options.selection;
 
 			this.$el.attr('aria-label', this.model.attributes.title).attr('aria-checked', false);
-			this.model.on( 'change:sizes change:uploading', this.render, this );
+			this.model.on( 'change', this.render, this );
 			this.model.on( 'change:title', this._syncTitle, this );
 			this.model.on( 'change:caption', this._syncCaption, this );
 			this.model.on( 'change:percent', this.progress, this );
@@ -4718,7 +4565,7 @@
 					compat:        false,
 					alt:           '',
 					description:   ''
-				});
+				}, this.options );
 
 			options.buttons  = this.buttons;
 			options.describe = this.controller.state().get('describe');
@@ -4768,11 +4615,17 @@
 		 */
 		toggleSelectionHandler: function( event ) {
 			var method;
-
 			// Catch enter and space events
 			if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) {
 				return;
 			}
+
+			// In the grid view, bubble up an edit:attachment event to the controller.
+			if ( _.contains( this.controller.options.mode, 'grid' ) ) {
+				this.controller.trigger( 'edit:attachment', this.model );
+				return;
+			}
+
 			if ( event.shiftKey ) {
 				method = 'between';
 			} else if ( event.ctrlKey || event.metaKey ) {
@@ -5303,10 +5156,11 @@
 		 */
 		createAttachmentView: function( attachment ) {
 			var view = new this.options.AttachmentView({
-				controller: this.controller,
-				model:      attachment,
-				collection: this.collection,
-				selection:  this.options.selection
+				controller:           this.controller,
+				model:                attachment,
+				collection:           this.collection,
+				selection:            this.options.selection,
+				showAttachmentFields: this.options.showAttachmentFields
 			});
 
 			return this._viewsByCid[ attachment.cid ] = view;
@@ -5603,7 +5457,6 @@
 		}
 	});
 
-
 	/**
 	 * wp.media.view.AttachmentsBrowser
 	 *
@@ -5621,13 +5474,18 @@
 				filters: false,
 				search:  true,
 				display: false,
-
+				sidebar: true,
+				showAttachmentFields: getUserSetting( 'showAttachmentFields', [ 'title', 'uploadedTo', 'dateFormatted', 'mime' ] ),
 				AttachmentView: media.view.Attachment.Library
 			});
 
 			this.createToolbar();
 			this.updateContent();
-			this.createSidebar();
+			if ( this.options.sidebar ) {
+				this.createSidebar();
+			} else {
+				this.$el.addClass( 'hide-sidebar' );
+			}
 
 			this.collection.on( 'add remove reset', this.updateContent, this );
 		},
@@ -5652,6 +5510,20 @@
 
 			this.views.add( this.toolbar );
 
+			// Feels odd to bring the global media library switcher into the Attachment
+			// browser view. Is this a use case for doAction( 'add:toolbar-items:attachments-browser', this.toolbar );
+			// which the controller can tap into and add this view?
+			if ( _.contains( this.controller.options.mode, 'grid' ) ) {
+				var libraryViewSwitcherConstructor = media.View.extend({
+					className: 'view-switch media-grid-view-switch',
+					template: media.template( 'media-library-view-switcher')
+				});
+				this.toolbar.set( 'libraryViewSwitcher', new libraryViewSwitcherConstructor({
+					controller: this.controller,
+					priority: -90
+				}).render() );
+			}
+
 			filters = this.options.filters;
 			if ( 'uploaded' === filters ) {
 				FiltersConstructor = media.view.AttachmentFilters.Uploaded;
@@ -5746,11 +5618,12 @@
 			this.removeContent();
 
 			this.attachments = new media.view.Attachments({
-				controller: this.controller,
-				collection: this.collection,
-				selection:  this.options.selection,
-				model:      this.model,
-				sortable:   this.options.sortable,
+				controller:           this.controller,
+				collection:           this.collection,
+				selection:            this.options.selection,
+				model:                this.model,
+				sortable:             this.options.sortable,
+				showAttachmentFields: this.options.showAttachmentFields,
 
 				// The single `Attachment` view to be used in the `Attachments` view.
 				AttachmentView: this.options.AttachmentView
Index: src/wp-includes/media-template.php
===================================================================
--- src/wp-includes/media-template.php	(revision 28973)
+++ src/wp-includes/media-template.php	(working copy)
@@ -220,6 +220,15 @@
 		</div>
 	</script>
 
+	<script type="text/html" id="tmpl-media-library-view-switcher">
+		<a href="<?php echo esc_url( add_query_arg( 'mode', 'list', $_SERVER['REQUEST_URI'] ) ) ?>" class="view-list">
+			<img id="view-switch-list" src="<?php echo includes_url( 'images/blank.gif' ) ?>" width="20" height="20" title="List View" alt="List View"/>
+		</a>
+		<a href="<?php echo esc_url( add_query_arg( 'mode', 'grid', $_SERVER['REQUEST_URI'] ) ) ?>" class="view-grid current">
+			<img id="view-switch-excerpt" src="<?php echo includes_url( 'images/blank.gif' ) ?>" width="20" height="20" title="Grid View" alt="Grid View"/>
+		</a>
+	</script>
+
 	<script type="text/html" id="tmpl-uploader-status">
 		<h3><?php _e( 'Uploading' ); ?></h3>
 		<a class="upload-dismiss-errors" href="#"><?php _e('Dismiss Errors'); ?></a>
@@ -241,6 +250,107 @@
 		<span class="upload-error-message">{{ data.message }}</span>
 	</script>
 
+	<script type="text/html" id="tmpl-edit-attachment-frame">
+		<div class="edit-media-header">
+			<button class="left dashicons dashicons-no<# if ( ! data.hasPrevious ) { #> disabled <# } #>"><span class="screen-reader-text"><?php _e( 'Edit previous media item' ); ?></span></button>
+			<button class="right dashicons dashicons-no<# if ( ! data.hasNext ) { #> disabled <# } #>"><span class="screen-reader-text"><?php _e( 'Edit next media item' ); ?></span></button>
+		</div>
+		<div class="media-frame-router"></div>
+		<div class="media-frame-content"></div>
+		<div class="media-frame-toolbar"></div>
+	</script>
+
+	<script type="text/html" id="tmpl-attachment-details-two-column">
+		<h3>
+			<?php _e('Attachment Details'); ?>
+
+			<span class="settings-save-status">
+				<span class="spinner"></span>
+				<span class="saved"><?php esc_html_e('Saved.'); ?></span>
+			</span>
+		</h3>
+		<div class="attachment-info">
+			<div class="thumbnail thumbnail-{{ data.type }}">
+				<# if ( data.uploading ) { #>
+					<div class="media-progress-bar"><div></div></div>
+				<# } else if ( 'image' === data.type ) { #>
+					<img src="{{ data.sizes.full.url }}" draggable="false" />
+				<# } else { #>
+					<img src="{{ data.icon }}" class="icon" draggable="false" />
+				<# } #>
+			</div>
+			<div class="details">
+				<div class="filename">{{ data.filename }}</div>
+				<div class="uploaded">{{ data.dateFormatted }}</div>
+
+				<div class="file-size">{{ data.filesizeHumanReadable }}</div>
+				<# if ( 'image' === data.type && ! data.uploading ) { #>
+					<# if ( data.width && data.height ) { #>
+						<div class="dimensions">{{ data.width }} &times; {{ data.height }}</div>
+					<# } #>
+
+					<# if ( data.can.save ) { #>
+						<a class="edit-attachment" href="{{ data.editLink }}&amp;image-editor" target="_blank"><?php _e( 'Edit Image' ); ?></a>
+						<a class="refresh-attachment" href="#"><?php _e( 'Refresh' ); ?></a>
+					<# } #>
+				<# } #>
+
+				<# if ( data.fileLength ) { #>
+					<div class="file-length"><?php _e( 'Length:' ); ?> {{ data.fileLength }}</div>
+				<# } #>
+
+				<# if ( ! data.uploading && data.can.remove ) { #>
+					<?php if ( MEDIA_TRASH ): ?>
+						<a class="trash-attachment" href="#"><?php _e( 'Trash' ); ?></a>
+					<?php else: ?>
+						<a class="delete-attachment" href="#"><?php _e( 'Delete Permanently' ); ?></a>
+					<?php endif; ?>
+				<# } #>
+
+				<div class="compat-meta">
+					<# if ( data.compat && data.compat.meta ) { #>
+						{{{ data.compat.meta }}}
+					<# } #>
+				</div>
+			</div>
+		</div>
+		<div class="attachment-fields">
+			<label class="setting" data-setting="url">
+				<span class="name"><?php _e('URL'); ?></span>
+				<input type="text" value="{{ data.url }}" readonly />
+			</label>
+			<# var maybeReadOnly = data.can.save || data.allowLocalEdits ? '' : 'readonly'; #>
+			<label class="setting" data-setting="title">
+				<span class="name"><?php _e('Title'); ?></span>
+				<input type="text" value="{{ data.title }}" {{ maybeReadOnly }} />
+			</label>
+			<label class="setting" data-setting="caption">
+				<span class="name"><?php _e('Caption'); ?></span>
+				<textarea {{ maybeReadOnly }}>{{ data.caption }}</textarea>
+			</label>
+			<# if ( 'image' === data.type ) { #>
+				<label class="setting" data-setting="alt">
+					<span class="name"><?php _e('Alt Text'); ?></span>
+					<input type="text" value="{{ data.alt }}" {{ maybeReadOnly }} />
+				</label>
+			<# } #>
+			<label class="setting" data-setting="description">
+				<span class="name"><?php _e('Description'); ?></span>
+				<textarea {{ maybeReadOnly }}>{{ data.description }}</textarea>
+			</label>
+			<label class="setting">
+					<span class="name"><?php _e( 'Uploaded By' ); ?></span>
+					<span class="value">{{ data.authorName }}</span>
+				</label>
+			<# if ( data.uploadedTo ) { #>
+				<label class="setting">
+					<span class="name"><?php _e('Uploaded To'); ?></span>
+					<span class="value"><a href="{{ data.uploadedToLink }}">{{ data.uploadedToTitle }}</a></span>
+				</label>
+			<# } #>
+		</div>
+	</script>
+
 	<script type="text/html" id="tmpl-attachment">
 		<div class="attachment-preview type-{{ data.type }} subtype-{{ data.subtype }} {{ data.orientation }}">
 			<# if ( data.uploading ) { #>
@@ -251,13 +361,18 @@
 						<img src="{{ data.size.url }}" draggable="false" alt="" />
 					</div>
 				</div>
-			<# } else { #>
+			<# } else {
+				if ( data.thumb && data.thumb.src && data.thumb.src !== data.icon ) {
+				#><img src="{{ data.thumb.src }}" class="thumbnail" draggable="false" /><#
+				} #>
 				<img src="{{ data.icon }}" class="icon" draggable="false" />
 				<div class="filename">
 					<div>{{ data.filename }}</div>
 				</div>
 			<# } #>
-
+			<# if ( _.contains( data.controller.options.mode, 'grid' ) ) { #>
+				<span class="edit-media">Edit</span>
+			<# } #>
 			<# if ( data.buttons.close ) { #>
 				<a class="close media-modal-icon" href="#" title="<?php esc_attr_e('Remove'); ?>"></a>
 			<# } #>
@@ -268,8 +383,8 @@
 		</div>
 		<#
 		var maybeReadOnly = data.can.save || data.allowLocalEdits ? '' : 'readonly';
-		if ( data.describe ) { #>
-			<# if ( 'image' === data.type ) { #>
+		if ( data.describe ) {
+			if ( 'image' === data.type ) { #>
 				<input type="text" value="{{ data.caption }}" class="describe" data-setting="caption"
 					placeholder="<?php esc_attr_e('Caption this image&hellip;'); ?>" {{ maybeReadOnly }} />
 			<# } else { #>
@@ -281,8 +396,31 @@
 					<# } else { #>
 						placeholder="<?php esc_attr_e('Describe this media file&hellip;'); ?>"
 					<# } #> {{ maybeReadOnly }} />
-			<# } #>
+			<# }
+		}
+
+		if ( _.contains( data.controller.options.mode, 'grid' ) ) { #>
+		<div class="data-fields">
+		<# _.each( data.showAttachmentFields, function( field ) { #>
+			<div class="data-field data-{{ field }}"><#
+				if ( 'uploadedTo' === field ) {
+					if ( data[field] ) {
+					#><?php _e( 'Uploaded To:' ) ?><#
+					} else {
+					#><?php _e( 'Unattached' ) ?><#
+					}
+				} else if ( 'title' === field && ! data[ field ] ) {
+				#><?php _e( '(No title)' ) ?><#
+				}
+
+				if ( data[ field ] ) {
+					#>{{ data[ field ] }}<#
+				}
+			#></div>
+		<# }); #>
+		</div>
 		<# } #>
+
 	</script>
 
 	<script type="text/html" id="tmpl-attachment-details">
Index: src/wp-includes/script-loader.php
===================================================================
--- src/wp-includes/script-loader.php	(revision 28973)
+++ src/wp-includes/script-loader.php	(working copy)
@@ -506,7 +506,7 @@
 		$scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox' ), false, 1 );
 
 		$scripts->add( 'list-revisions', "/wp-includes/js/wp-list-revisions$suffix.js" );
-
+		$scripts->add( 'media-grid', "/wp-includes/js/media-grid$suffix.js", array( 'media-editor' ), false, 1 );
 		$scripts->add( 'media', "/wp-admin/js/media$suffix.js", array( 'jquery' ), false, 1 );
 		did_action( 'init' ) && $scripts->localize( 'media', 'attachMediaBoxL10n', array(
 			'error' => __( 'An error has occurred. Please reload the page and try again.' ),
