namespace_browser: Bookmarks and Top Recommendation UI polish.

This CL refactor the UI and the code around Bookmarks/Recommendations
and Items views.

Main UI Refactors:
-View switcher for Grid View, Tree View, Visualize View, Bookmarks,
Recommendations
https://screenshot.googleplex.com/4BagmtKoVG.png
https://screenshot.googleplex.com/bsoME6UJ6G.png
https://screenshot.googleplex.com/f2a8Rf9qJV.png
https://screenshot.googleplex.com/9bT6TMfe3X.png
https://screenshot.googleplex.com/exJOkG7vLV.png
https://screenshot.googleplex.com/rj1tpydVmu.png

-Bookmark action moved to the side panel with UNDO-able toast.
https://screenshot.googleplex.com/hyZoMWN32N.png

Main code refactors:
-Splitting browse component into several sub components
-Moving bookmark and recommendation business logic to a service layer

Also includes random bug fixes (UI and logic) as I noticed them during testing

Change-Id: Ic9dfd3267bbd3e71733d06e53ea40ef67ccd8f61
diff --git a/package.json b/package.json
index 2ff5b19..4dc04e3 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
     "mercury": "^12.0.0",
     "routes": "^1.2.0",
     "lodash": "~2.4.1",
-    "xtend": "~4.0.0",
-    "vis": "~3.7.1"
+    "vis": "~3.8.0",
+    "extend": "~2.0.0"
   }
 }
diff --git a/public/index.html b/public/index.html
index 77becb8..b202d1d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -7,10 +7,9 @@
   <meta name="apple-mobile-web-app-capable" content="yes">
   <meta name="description" content="">
   <link href='//fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'>
-  <title>Veyron Browser</title>
+  <title>Viz - Vanadium Viewer</title>
   <link rel="import" href="bundle.html">
 <head>
 <body fullbleed>
-<div id="mynetwork"></div>
   <script src="bundle.js"></script>
 </body>
\ No newline at end of file
diff --git a/src/components/browse/bookmarks/index.js b/src/components/browse/bookmarks/index.js
new file mode 100644
index 0000000..f131ec2
--- /dev/null
+++ b/src/components/browse/bookmarks/index.js
@@ -0,0 +1,66 @@
+var mercury = require('mercury');
+
+var ItemCardList = require('../item-card-list/index');
+
+var bookmarksService = require('../../../services/bookmarks/service');
+
+var log = require('../../../lib/log')('components:browse:bookmarks');
+
+module.exports = create;
+module.exports.render = render;
+module.exports.load = load;
+
+/*
+ * Bookmark view
+ */
+function create() {
+
+  var state = mercury.varhash({
+    /*
+     * List of user-specified bookmark items to display
+     * @see services/namespace/item
+     * @type {Array<namespaceitem>}
+     */
+    bookmarkItems: mercury.array([])
+  });
+
+  return {
+    state: state
+  };
+}
+
+/*
+ * Renders the bookmark view
+ */
+function render(state, browseState, browseEvents, navEvents) {
+  return ItemCardList.render(
+    state.bookmarkItems,
+    browseState,
+    browseEvents,
+    navEvents, {
+      title: 'Bookmarks',
+      emptyText: 'No bookmarks.'
+    }
+  );
+}
+
+/*
+ * Does the initialization and loading of the data necessary to display the
+ * bookmarks.
+ * Called and used by the parent browse view to initialize the view on
+ * request.
+ * Returns a promise that will be resolved when loading is finished. Promise
+ * is used by the parent browse view to display a loading progressbar.
+ */
+function load(state) {
+  return new Promise(function(resolve, reject) {
+    bookmarksService.getAll()
+      .then(function bookmarksReceived(items) {
+        state.put('bookmarkItems', items);
+        items.events.on('end', resolve);
+      }).catch(function(err) {
+        log.error(err);
+        reject();
+      });
+  });
+}
\ No newline at end of file
diff --git a/src/components/browse/browse-namespace.js b/src/components/browse/browse-namespace.js
index eede491..c172a54 100644
--- a/src/components/browse/browse-namespace.js
+++ b/src/components/browse/browse-namespace.js
@@ -1,13 +1,17 @@
-var mercury = require('mercury');
-var guid = require('guid');
-var handleShortcuts = require('./handle-shortcuts');
-var recommendShortcuts = require('./recommend-shortcuts');
-var exists = require('../../lib/exists');
+var extend = require('extend');
+
+var Bookmarks = require('./bookmarks/index.js');
+var Recommendations = require('./recommendations/index.js');
+var Items = require('./items/index.js');
+
 var log = require('../../lib/log')('components:browse:browse-namespace');
-var namespaceService = require('../../services/namespace/service');
 
 module.exports = browseNamespace;
 
+// We keep track of previous namespace that was browsed to so we can
+// know when navigating to a different namespace happens.
+var previousNamespace;
+
 /*
  * Default event handler for the browseNamespace event.
  * Updates the necessary states when browseNamespace is triggered
@@ -18,75 +22,81 @@
  * }
  */
 function browseNamespace(browseState, browseEvents, data) {
-  if (exists(data.namespace)) {
-    browseState.namespace.set(data.namespace);
+
+  var defaults = {
+    namespace: '',
+    globQuery: '',
+    subPage: 'items',
+    viewType: 'grid'
+  };
+
+  data = extend(defaults, data);
+
+  if (!Items.trySetViewType(browseState.items, data.viewType)) {
+    error404('Invalid view type: ' + data.viewType);
+    return;
   }
 
-  if (exists(data.globQuery)) {
-    browseState.globQuery.set(data.globQuery);
-  }
+  browseState.namespace.set(data.namespace);
+  browseState.globQuery.set(data.globQuery);
+  browseState.subPage.set(data.subPage);
 
   var namespace = browseState.namespace();
-
-  // Search the namespace and update the browseState's items.
-  var requestId = guid.create().value;
-  browseState.isFinishedLoadingItems.set(false);
-  browseState.currentRequestId.set(requestId);
-  browseState.put('items', mercury.array([]));
-
   var globQuery = browseState.globQuery() || '*';
-  namespaceService.search(namespace, globQuery).
-  then(function globResultsReceived(items) {
-    if (!isCurrentRequest()) {
-      return;
-    }
-    browseState.put('items', items);
-    items.events.on('end', searchFinished);
-    items.events.on('streamError', searchFinished);
-  }).catch(function(err) {
-    searchFinished();
-    browseEvents.error(err);
-    log.error(err);
-  });
+  var subPage = browseState.subPage();
 
-  // Reload the user's shortcuts.
-  handleShortcuts.load(browseState).catch(function(err) {
+  // When navigating to a different namespace, reset the currently selected item
+  if (previousNamespace !== namespace) {
+    browseState.selectedItemName.set(namespace);
+  }
+  previousNamespace = namespace;
+
+  browseState.isFinishedLoadingItems.set(false);
+
+  switch (subPage) {
+    case 'items':
+      Items.load(browseState.items, namespace, globQuery)
+        .then(loadingFinished)
+        .catch(onError.bind(null, 'items'));
+      break;
+    case 'bookmarks':
+      Bookmarks.load(browseState.bookmarks)
+        .then(loadingFinished)
+        .catch(onError.bind(null, 'bookmarks'));
+      break;
+    case 'recommendations':
+      Recommendations.load(browseState.recommendations)
+        .then(loadingFinished)
+        .catch(onError.bind(null, 'recommendations'));
+      break;
+    default:
+      browseState.subPage.set(defaults.subPage);
+      error404('Invalid page: ' + browseState.subPage());
+      return;
+  }
+
+  function onError(subject, err) {
+    var message = 'Could not load ' + subject;
     browseEvents.toast({
-      text: 'Could not load shortcuts',
+      text: message,
       type: 'error'
     });
-    // TODO(alexfandrianto): I'd like to toast here, but our toasting mechanism
-    // would only allow for 1 toast. The toast below would override this one.
-    // Perhaps we should allow an array of toasts to be set?
-    log.error('Could not load user shortcuts', err);
-  });
+    log.error(message, err);
+    loadingFinished();
+  }
 
-  // Update our shortcuts, as they may have changed.
-  recommendShortcuts(browseState);
+  function error404(errMessage) {
+    log.error(errMessage);
+    //TODO(aghassemi) Needs to be 404 error when we have support for 404
+    browseEvents.error(new Error(errMessage));
+  }
 
-  // Trigger display items event
-  browseEvents.selectedItemDetails.displayItemDetails({
-    name: data.namespace
-  });
-
-  // TODO(alexfandrianto): Example toast. Consider removing.
-  browseEvents.toast({
-    text: 'Browsing ' + data.namespace,
-    action: browseNamespace.bind(null, browseState, browseEvents, data),
-    actionText: 'REFRESH'
-  });
-
-  function searchFinished() {
-    if (!isCurrentRequest()) {
-      return;
-    }
+  function loadingFinished() {
     browseState.isFinishedLoadingItems.set(true);
   }
 
-  // Whether were are still the current request. This is used to ignore out of
-  // order return of async calls where user has moved on to another item
-  // by the time previous requests result comes back.
-  function isCurrentRequest() {
-    return browseState.currentRequestId() === requestId;
-  }
+  // Update the right side
+  browseEvents.selectedItemDetails.displayItemDetails({
+    name: browseState.selectedItemName()
+  });
 }
\ No newline at end of file
diff --git a/src/components/browse/get-service-icon.js b/src/components/browse/get-service-icon.js
deleted file mode 100644
index e58b392..0000000
--- a/src/components/browse/get-service-icon.js
+++ /dev/null
@@ -1,15 +0,0 @@
-module.exports = getServiceIcon;
-
-var serviceIconMap = Object.freeze({
-  'veyron-mounttable': ['social:circles-extended', 'social:circles-extended'],
-  'veyron-unknown': ['cloud-queue', 'cloud'],
-  '': ['folder-open', 'folder']
-});
-
-/*
- * Given the type of a service and whether the element should be filled or not,
- * return the name of the corresponding core-icon to use for rendering.
- */
-function getServiceIcon(type, fill) {
-  return serviceIconMap[type][fill ? 1 : 0];
-}
\ No newline at end of file
diff --git a/src/components/browse/handle-shortcuts.js b/src/components/browse/handle-shortcuts.js
deleted file mode 100644
index 9bc4709..0000000
--- a/src/components/browse/handle-shortcuts.js
+++ /dev/null
@@ -1,102 +0,0 @@
-var mercury = require('mercury');
-var _ = require('lodash');
-var arraySet = require('../../lib/arraySet');
-var recommendShortcuts = require('./recommend-shortcuts');
-var log = require('../../lib/log')('components:browse:handle-shortcuts');
-var store = require('../../lib/store');
-var namespaceService = require('../../services/namespace/service');
-
-module.exports = {
-  load: loadShortcuts,
-  set: setShortcut,
-  find: findShortcut
-};
-
-// Data is loaded from and saved to this key in the store.
-var userShortcutsID = 'user-shortcuts';
-
-/*
- * Returns a promise that loads the user's shortcuts into the browse state.
- */
-function loadShortcuts(browseState) {
-  return loadShortcutKeys().then(function getShortcuts(rawShortcuts) {
-    rawShortcuts = rawShortcuts || [];
-
-    // Clear out the old shortcuts and fill them with new ones.
-    browseState.put('userShortcuts', mercury.array([]));
-
-    rawShortcuts.forEach(function(rawShortcut, i) {
-      namespaceService.getNamespaceItem(rawShortcut).then(
-        function(shortcut) {
-          browseState.userShortcuts.put(i, shortcut);
-        }
-      ).catch(function(err) {
-        // TODO(alexfandrianto): We should find a way to indicate that the
-        // service is not accessible at the moment. A toast is not enough.
-        log.error('Could not load shortcut', rawShortcut, err);
-      });
-    });
-  }).catch(function(err) {
-    log.error('Unable to load user shortcuts', err);
-    return Promise.reject(err);
-  });
-}
-
-/*
- * Returns a promise that resolves to an array of user-defined shortcut keys.
- */
-function loadShortcutKeys() {
-  return store.getValue(userShortcutsID);
-}
-
-/*
- * Returns a promise that saves the new status of the shortcut key to the store.
- */
-function saveShortcutKey(key, shouldSet) {
-  return loadShortcutKeys().then(function(keys) {
-    keys = keys || []; // Initialize the shortcuts, if none were loaded.
-    arraySet.set(keys, key, shouldSet);
-    return store.setValue(userShortcutsID, keys);
-  });
-}
-
-/*
- * Update the given browseState with the shortcut information in the given data.
- * data should have 'save' (boolean) and 'item' (@see services/namespace/item).
- *
- * This update is done asynchronously; to maintain consistency, shortcuts are
- * refreshed before they are persisted. Additionally, what is rendered in the
- * browseState may not perfectly match the data present in the store.
- */
-function setShortcut(browseState, browseEvents, data) {
-  // Update the user shortcuts, as the data specifies.
-  arraySet.set(
-    browseState.userShortcuts,
-    data.item,
-    data.save,
-    findShortcut.bind(null, browseState())
-  );
-
-  // The recommended shortcuts may have changed too.
-  recommendShortcuts(browseState);
-
-  // Persist the updated user shortcut.
-  return saveShortcutKey(data.item.objectName, data.save).catch(function(err) {
-    browseEvents.toast({
-      text: 'Error while modifying shortcut',
-      type: 'error'
-    });
-    log.error('Error while modifying shortcut', err);
-  });
-}
-
-/*
- * Check the browseState for the index of the given item. -1 if not present.
- * Note: browseState should be observed.
- */
-function findShortcut(browseState, item) {
-  return _.findIndex(browseState.userShortcuts, function(shortcut) {
-    // Since shortcuts can be assigned out of order, check for undefined.
-    return shortcut !== undefined && item.objectName === shortcut.objectName;
-  });
-}
\ No newline at end of file
diff --git a/src/components/browse/index.css b/src/components/browse/index.css
index bc1afd6..fb7337b 100644
--- a/src/components/browse/index.css
+++ b/src/components/browse/index.css
@@ -32,86 +32,27 @@
   background-color: var(--color-grey-very-light);
   border-left: solid 1px var(--color-divider);
 }
-.items-container {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  border-bottom: var(--border);
-  padding-bottom: 0.5em;
+
+.browse-main-wrapper paper-progress {
+  position: absolute;
+  z-index: 500;
+  opacity: 0.6;
 }
-.items-container:last-child {
-  border-bottom: none;
-}
-.items-container paper-progress{
+
+.browse-main-wrapper core-tooltip {
   position: absolute;
 }
-.items-container h2 {
+
+.browse-main-wrapper h2 {
   width: 100%;
   font-size: var(--size-font-large);
   color: var(--color-text-heading);
   padding: 0.5em 0em 0em 0.75em;
   text-decoration: none;
 }
-.item.card {
-  box-sizing: border-box;
-  background-color: var(--color-white);
-  display: flex;
-  flex-shrink: 0;
-  flex-grow: 0;
-  height: 2.5em;
-  min-width: 8em;
-  margin: 0.75em;
-  border-radius: 3px;
-  overflow: hidden;
-  position: relative;
-  box-shadow: var(--shadow-all-around);
-  border: var(--border);
-}
-.item .label, .item .drill {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  padding: 0.5em;
-}
-.item .label {
-  text-decoration: none;
-  flex: 1;
-  overflow: hidden;
-  white-space: nowrap;
-}
-.item .drill {
-  width: 1.5em;
-  background-color: var(--color-grey-light);
-  border-left: var(--border);
-}
 
-.item.selected .drill {
-  background-color: var(--color-bright);
-}
-
-.item a:hover, .item a:focus{
-  opacity: 0.7;
-}
-
-.item.card .icon {
-  align-self: center;
-  padding-right: 0.5em;
-}
-
-.item.inaccessible {
-  opacity: 0.5;
-}
-
-.item.selected.card {
-  background-color: var(--color-bright);
-  color: var(--color-text-primary-invert);
-}
-
-.tooltip::shadow .core-tooltip {
-  width: 36em;
-  line-height: 0.9em; /* overrides Polymer's 6px line-height */
-  white-space: pre-wrap;
-  font-size: var(--size-font-xsmall);
+.progress-tooltip {
+  width: 100%;
 }
 
 .breadcrumbs {
@@ -124,6 +65,7 @@
 }
 
 .breadcrumb-item {
+  font-size: var(--size-font-small);
   overflow: hidden;
   flex-shrink: 1;
   flex-grow: 0;
@@ -144,27 +86,21 @@
 .breadcrumb-item:before
 {
   content: '/';
-  padding:0 0.5em;
+  padding:0 var(--size-space-xxsmall);
   display: inline-block;
   color: var(--color-text-secondary);
 }
+
 .breadcrumb-item:first-child:before
 {
   content: ' ';
 }
 
-.empty {
-  padding: 1em;
-  text-align: center;
-  color: var(--color-text-secondary);
-}
-
 .namespace-box {
   background: var(--color-white-transparent);
   font-size: var(--size-font-xsmall);
-  height: 2.7em;
-  padding: 0 0.8em;
-  margin-left: 3em !important;
+  padding: var(--size-space-xxsmall) var(--size-space-small);
+  margin-left: var(--size-space-xxlarge) !important;
   border-radius: 1px;
 }
 
@@ -172,20 +108,43 @@
   color: inherit
 }
 
+.icon-group {
+  white-space: nowrap;
+}
+
+.vertical-ruler {
+  display: inline-block;
+  width: 1px;
+  background-color: var(--color-divider);
+  margin:0 var(--size-space-xxsmall);
+}
+
+.icon-group paper-icon-button {
+  vertical-align: middle;
+  color: var(--color-text-secondary);
+  padding: var(--size-space-xxsmall);
+  margin:0 var(--size-space-xxsmall);
+}
+
+.icon-group paper-icon-button:hover, .icon-group paper-icon-button:focus {
+  background: #eee;
+  border-radius: 50%;
+}
+
 .search-box {
   white-space: nowrap;
-  width: var(--size-input-width-normal);
+  width: var(--size-input-width-small);
+  margin-left: var(--size-space-xxsmall);
 }
 
 .search-box core-tooltip {
   width: 100%;
-  font-size: var(--size-font-xsmall);
 }
 
 .search-box .icon, .namespace-box .icon {
   align-self: flex-end;
-  margin-right: 0.5em;
   color: var(--color-text-secondary);
+  margin-right: var(--size-space-xxsmall);
 }
 
 .search-box .input {
@@ -204,21 +163,6 @@
   transform: scale(0.7);
   padding-right: 0.75em;
 }
-/* The service icon is colored brightly if active. */
-.service-type-icon:hover,
-.service-type-icon.shortcut {
-  color: var(--color-bright);
-}
-/* If the service item is selected and is also active, make the icon's colors deeper. */
-.item.selected.card .service-type-icon:hover,
-.item.selected.card .service-type-icon.shortcut {
-  color: var(--color-bright-deep);
-}
-/* If hovering over a shortcut, use the parent's color. */
-.service-type-icon.shortcut:hover,
-.item.selected.card .service-type-icon.shortcut:hover {
-  color: inherit;
-}
 
 paper-autocomplete {
   width: var(--size-input-width-normal);
diff --git a/src/components/browse/index.js b/src/components/browse/index.js
index c68f78d..9bfc059 100644
--- a/src/components/browse/index.js
+++ b/src/components/browse/index.js
@@ -1,19 +1,29 @@
 var mercury = require('mercury');
 var insertCss = require('insert-css');
+
 var AttributeHook = require('../../lib/mercury/attribute-hook');
 var PropertyValueEvent = require('../../lib/mercury/property-value-event');
+
 var exists = require('../../lib/exists');
-var log = require('../../lib/log')('components:browse');
-var browseRoute = require('../../routes/browse');
-var browseNamespace = require('./browse-namespace');
-var getNamespaceSuggestions = require('./get-namespace-suggestions');
-var getServiceIcon = require('./get-service-icon');
-var handleShortcuts = require('./handle-shortcuts');
-var itemDetailsComponent = require('./item-details/index');
+
 var namespaceService = require('../../services/namespace/service');
 var smartService = require('../../services/smart/service');
-var css = require('./index.css');
 
+var browseRoute = require('../../routes/browse');
+var bookmarksRoute = require('../../routes/bookmarks');
+var recommendationsRoute = require('../../routes/recommendations');
+
+var ItemDetails = require('./item-details/index');
+var Items = require('./items/index');
+var Bookmarks = require('./bookmarks/index');
+var Recommendations = require('./recommendations/index');
+
+var browseNamespace = require('./browse-namespace');
+var getNamespaceSuggestions = require('./get-namespace-suggestions');
+
+var log = require('../../lib/log')('components:browse');
+
+var css = require('./index.css');
 var h = mercury.h;
 
 module.exports = create;
@@ -26,14 +36,17 @@
 function create() {
   loadLearners();
 
-  var selectedItemDetails = itemDetailsComponent();
+  var selectedItemDetails = new ItemDetails();
+  var bookmarks = new Bookmarks();
+  var recommendations = new Recommendations();
+  var items = new Items();
 
   var state = mercury.varhash({
     /*
      * Veyron namespace being displayed and queried
      * @type {string}
      */
-    namespace: mercury.value(''), //TODO(aghassemi) temp
+    namespace: mercury.value(''),
 
     /*
      * Glob query applied to the Veyron namespace
@@ -60,38 +73,19 @@
     namespacePrefix: mercury.value(''),
 
     /*
-     * List of namespace items to display
-     * @see services/namespace/item
-     * @type {Array<namespaceitem>}
+     * State of the bookmarks component
      */
-    items: mercury.array([]),
+    bookmarks: bookmarks.state,
 
     /*
-     * Whether loading items has finished.
-     * @type {Boolean}
+     * State of the recommendation component
      */
-    isFinishedLoadingItems: mercury.value(false),
+    recommendations: recommendations.state,
 
     /*
-     * uuid for the current browse-namespace request.
-     * Needed to handle out-of-order return of async calls.
-     * @type {String}
+     * State of the items component
      */
-    currentRequestId: mercury.value(''),
-
-    /*
-     * List of user-specified shortcuts to display
-     * @see services/namespace/item
-     * @type {Array<namespaceitem>}
-     */
-    userShortcuts: mercury.array([]),
-
-    /*
-     * List of recommended shortcuts to display
-     * @see services/namespace/item
-     * @type {Array<namespaceitem>}
-     */
-    recShortcuts: mercury.array([]),
+    items: items.state,
 
     /*
      * State of the selected item-details component
@@ -101,7 +95,19 @@
     /*
      * Name of currently selected item
      */
-    selectedItemName:  mercury.value(''),
+    selectedItemName: mercury.value(''),
+
+    /*
+     * Whether loading items has finished.
+     * @type {Boolean}
+     */
+    isFinishedLoadingItems: mercury.value(false),
+
+    /*
+     * Specifies what sub page is currently displayed.
+     * One of: items, bookmarks, recommendations
+     */
+    subPage: mercury.value('items')
 
   });
 
@@ -110,7 +116,7 @@
      * Indicates a request to browse the Veyron namespace
      * Data of form:
      * {
-     *   namespace: '/veyron/name/space',
+     *   namespace: '/namespace-root:8881/name/space',
      *   globQuery: '*',
      * }
      * is expected as data for the event
@@ -123,16 +129,35 @@
     'getNamespaceSuggestions',
 
     /*
-     * Indicates that the user is setting/removing a shortcut.
+     * Selects an items.
+     * Data of form:
+     * {
+     *    name: 'object/name'
+     * }
      */
-    'setShortcut',
-
-    'selectedItemDetails',
-
     'selectItem',
 
+    /*
+     * Events for the ItemDetails component
+     */
+    'selectedItemDetails',
+
+    /*
+     * Displays an error
+     * Data of should be an Error object.
+     */
     'error',
 
+    /*
+     * Displays a toast
+     * Data of form:
+     * {
+          text: 'Saved',
+          type: 'error',
+          action: function undo(){ // },
+          actionText: 'UNDO'
+     * }
+     */
     'toast'
   ]);
 
@@ -148,17 +173,11 @@
 
 /*
  * Loads the learners into the smart service upon creation of this component.
+ * TODO(aghassemi), TODO(alexfandrianto) Move this into service layers, similar
+ * to how `learner-shortcut` is now loaded in the recommendations service.
  */
 function loadLearners() {
   smartService.loadOrCreate(
-    'learner-shortcut',
-    smartService.constants.LEARNER_SHORTCUT, {
-      k: 3
-    }
-  ).catch(function(err) {
-    log.error(err);
-  });
-  smartService.loadOrCreate(
     'learner-method-input',
     smartService.constants.LEARNER_METHOD_INPUT, {
       minThreshold: 0.2,
@@ -183,6 +202,102 @@
  * namespace root.
  */
 function renderHeader(browseState, browseEvents, navEvents) {
+  return h('div', [
+    renderNamespaceBox(browseState, browseEvents, navEvents)
+  ]);
+}
+
+/*
+ * Renders the main body of the namespace browser.
+ * A toolbar is rendered on top of the mainView and sideView showing the current
+ * position in the namespace as well as a globquery searchbox.
+ * The mainView contains the shortcuts and names at this point in the namespace.
+ * The sideView displays the detail information of the selected name.
+ */
+function render(browseState, browseEvents, navEvents) {
+  insertCss(css);
+
+  var sideView = [
+    ItemDetails.render(
+      browseState.selectedItemDetails,
+      browseEvents.selectedItemDetails
+    )
+  ];
+
+  var mainView;
+  switch (browseState.subPage) {
+    case 'items':
+      mainView = Items.render(browseState.items, browseState,
+        browseEvents, navEvents);
+      break;
+    case 'bookmarks':
+      mainView = Bookmarks.render(browseState.bookmarks,
+        browseState, browseEvents, navEvents);
+      break;
+    case 'recommendations':
+      mainView = Recommendations.render(browseState.recommendations,
+        browseState, browseEvents, navEvents);
+      break;
+    default:
+      log.error('Unsupported subPage ' + browseState.subPage);
+  }
+
+  // add progressbar and wrap in a container
+  var progressbar;
+  if (!browseState.isFinishedLoadingItems) {
+    progressbar = h('core-tooltip.progress-tooltip', {
+      'label': new AttributeHook('Loading items...'),
+      'position': new AttributeHook('bottom')
+    }, h('paper-progress.delayed', {
+      'indeterminate': new AttributeHook(true),
+      'aria-label': new AttributeHook('Loading items')
+    }));
+  }
+
+  mainView = h('div.browse-main-wrapper', [
+    progressbar,
+    mainView
+  ]);
+
+  var sideViewWidth = '50%';
+  var view = [
+    h('core-toolbar.browse-toolbar', [
+      renderBreadcrumbs(browseState, navEvents),
+      renderViewActions(browseState, navEvents)
+    ]),
+    h('core-drawer-panel', {
+      'rightDrawer': new AttributeHook(true),
+      'drawerWidth': new AttributeHook(sideViewWidth),
+      'responsiveWidth': new AttributeHook('0px')
+    }, [
+      h('core-header-panel.browse-main-panel', {
+        'main': new AttributeHook(true)
+      }, [
+        mainView
+      ]),
+      h('core-header-panel.browse-details-sidebar', {
+        'drawer': new AttributeHook(true)
+      }, [
+        sideView
+      ])
+    ])
+  ];
+
+  return h('core-drawer-panel', {
+    'drawerWidth': new AttributeHook('0px')
+  }, [
+    h('core-header-panel', {
+      'main': new AttributeHook(true)
+    }, [
+      view
+    ])
+  ]);
+}
+
+/*
+ * Renders the addressbar for entering namespace
+ */
+function renderNamespaceBox(browseState, browseEvents, navEvents) {
   // Trigger an actual navigation event when value of the inputs change
   var changeEvent = new PropertyValueEvent(function(val) {
     var namespace = browseState.namespace;
@@ -190,7 +305,9 @@
       namespace = val;
     }
     navEvents.navigate({
-      path: browseRoute.createUrl(namespace)
+      path: browseRoute.createUrl(browseState, {
+        namespace: namespace
+      })
     });
   }, 'value', true);
 
@@ -231,86 +348,65 @@
   );
 }
 
+function createActionIcon(tooltip, icon, href) {
+  var view = h('core-tooltip', {
+      'label': tooltip,
+      'position': 'bottom'
+    },
+    h('a', {
+      'href': new AttributeHook(href)
+    }, h('paper-icon-button.icon', {
+      'icon': new AttributeHook(icon)
+    }))
+  );
+
+  return view;
+}
+
 /*
- * Renders the main body of the namespace browser.
- * A toolbar is rendered on top of the mainView and sideView showing the current
- * position in the namespace as well as a globquery searchbox.
- * The mainView contains the shortcuts and names at this point in the namespace.
- * The sideView displays the detail information of the selected name.
+ * Renders the view switchers for different views and bookmarks, recommendations
  */
-function render(browseState, browseEvents, navEvents) {
-  insertCss(css);
+function renderViewActions(browseState, navEvents) {
 
-  var sideView = [
-    itemDetailsComponent.render(
-      browseState.selectedItemDetails,
-      browseEvents.selectedItemDetails
+  var switchGroup = h('div.icon-group', [
+    createActionIcon('Grid view', 'apps',
+      browseRoute.createUrl(browseState, {
+        viewType: 'grid'
+      })
+    ),
+    createActionIcon('Tree view', 'list',
+      browseRoute.createUrl(browseState, {
+        viewType: 'tree'
+      })
+    ),
+    createActionIcon('Visualize view', 'social:circles-extended',
+      browseRoute.createUrl(browseState, {
+        viewType: 'visualize'
+      })
     )
-  ];
-
-  var mainView = [
-    h('div.items-container', [
-      h('h2', 'Bookmarks'),
-      renderUserShortcuts(browseState, browseEvents, navEvents)
-    ]),
-    h('div.items-container', [
-      h('h2', 'Top Recommendations'),
-      renderRecommendedShortcuts(browseState, browseEvents, navEvents)
-    ])
-  ];
-
-  var sideViewWidth = '50%';
-  var progressbar;
-  if( !browseState.isFinishedLoadingItems ) {
-    progressbar = h('paper-progress.delayed', {
-      'indeterminate': new AttributeHook(true),
-      'aria-label': new AttributeHook('Loading namespace items')
-    });
-  }
-  if (browseState.isFinishedLoadingItems && browseState.items.length === 0) {
-    mainView.push(h('div.empty',
-      h('span',(browseState.globQuery ? 'No search results' : 'No children')))
-    );
-  } else {
-    mainView.push(h('div.items-container', [
-      progressbar,
-      h('h2', (browseState.globQuery ? 'Search results' : 'Children')),
-      renderItems(browseState, browseEvents, navEvents)
-    ]));
-  }
-
-  var view = [
-    h('core-toolbar.browse-toolbar', [
-      renderBreadcrumbs(browseState, navEvents),
-      renderSearch(browseState, navEvents)
-    ]),
-    h('core-drawer-panel', {
-      'rightDrawer': new AttributeHook(true),
-      'drawerWidth': new AttributeHook(sideViewWidth),
-      'responsiveWidth': new AttributeHook('0px')
-    }, [
-      h('core-header-panel.browse-main-panel', {
-        'main': new AttributeHook(true)
-      }, [
-        mainView
-      ]),
-      h('core-header-panel.browse-details-sidebar', {
-        'drawer': new AttributeHook(true)
-      }, [
-        sideView
-      ])
-    ])
-  ];
-
-  return h('core-drawer-panel', {
-    'drawerWidth': new AttributeHook('0px')
-  }, [
-    h('core-header-panel', {
-      'main': new AttributeHook(true)
-    }, [
-      view
-    ])
   ]);
+  var ruler = h('div.vertical-ruler');
+  var bookmarkGroup = h('div.icon-group', [
+    createActionIcon('Bookmarks', 'bookmark-outline',
+      bookmarksRoute.createUrl()
+    ),
+    createActionIcon('Recommendations', 'social:whatshot',
+      recommendationsRoute.createUrl()
+    )
+  ]);
+  var searchGroup = renderSearch(browseState, navEvents);
+  var view = h('div', {
+    'layout': new AttributeHook('true'),
+    'horizontal': new AttributeHook('true')
+  }, [
+    switchGroup,
+    ruler,
+    bookmarkGroup,
+    ruler,
+    searchGroup
+  ]);
+
+  return view;
 }
 
 /*
@@ -319,22 +415,23 @@
 function renderSearch(browseState, navEvents) {
   // Trigger an actual navigation event when value of the inputs change
   var changeEvent = new PropertyValueEvent(function(val) {
-    var globQuery = browseState.globQuery;
-    if (exists(val)) {
-      globQuery = val;
-    }
     navEvents.navigate({
-      path: browseRoute.createUrl(browseState.namespace, globQuery)
+      path: browseRoute.createUrl(browseState, {
+        globQuery: val,
+        //TODO(aghassemi) We only supprt grid view for search, we could
+        //potentially support other views such as tree too but it's tricky.
+        viewType: 'grid'
+      })
     });
   }, 'value', true);
 
   var clearSearch;
-  if(browseState.globQuery) {
+  if (browseState.globQuery) {
     clearSearch = h('paper-icon-button.icon.clear-search', {
       'icon': new AttributeHook('clear'),
       'label': new AttributeHook('Clear search'),
       'ev-click': mercury.event(navEvents.navigate, {
-        path: browseRoute.createUrl(browseState.namespace)
+        path: browseRoute.createUrl(browseState)
       })
     });
   }
@@ -343,7 +440,7 @@
         'label': new AttributeHook(
           'Enter Glob query for searching, e.g. */*/a*'
         ),
-        'position': 'left'
+        'position': 'bottom'
       },
       h('div', {
         'layout': new AttributeHook('true'),
@@ -356,7 +453,8 @@
           'flex': new AttributeHook('true'),
           'name': 'globQuery',
           'value': browseState.globQuery,
-          'ev-change': changeEvent
+          'ev-change': changeEvent,
+          'label': new AttributeHook('Glob Search')
         }),
         clearSearch
       ])
@@ -365,118 +463,19 @@
 }
 
 /*
- * The shortcuts chosen by the user are rendered with renderItem.
- */
-function renderUserShortcuts(browseState, browseEvents, navEvents) {
-  return browseState.userShortcuts.map(function(shortcut) {
-    return renderItem(browseState, browseEvents, navEvents, shortcut, true);
-  });
-}
-
-/*
- * The shortcuts recommended by the smartService are rendered with renderItem.
- * A shortcut is no longer recommended if it is already a user shortcut.
- * TODO(alexfandrianto): Should we really filter out these repeats?
- */
-function renderRecommendedShortcuts(browseState, browseEvents, navEvents) {
-  return browseState.recShortcuts.filter(function(shortcut) {
-    return shortcut !== undefined &&
-      handleShortcuts.find(browseState, shortcut) === -1;
-  }).map(function(shortcut) {
-    return renderItem(browseState, browseEvents, navEvents, shortcut, false);
-  });
-}
-
-/*
- * The items (obtained by globbing) are rendered with renderItem.
- */
-function renderItems(browseState, browseEvents, navEvents) {
-  return browseState.items.map(function(item) {
-    var isShortcut = handleShortcuts.find(browseState, item) !== -1;
-    return renderItem(browseState, browseEvents, navEvents, item, isShortcut);
-  });
-}
-
-/*
- * Render a browse item card. The card consists of a service icon, a mounted
- * name, and if globbable, a drill icon.
- */
-function renderItem(browseState, browseEvents, navEvents, item, isShortcut) {
-  var selected = false;
-
-  if (browseState.selectedItemName === item.objectName) {
-    selected = true;
-  }
-
-  // Prepare the drill if this item happens to be globbable.
-  var expandAction = null;
-  if (item.isGlobbable) {
-    expandAction = h('a.drill', {
-      'href': browseRoute.createUrl(item.objectName),
-      'ev-click': mercury.event(navEvents.navigate, {
-        path: browseRoute.createUrl(item.objectName)
-      })
-    }, h('core-icon.icon', {
-      'icon': new AttributeHook('chevron-right')
-    }));
-  }
-
-  // Prepare tooltip and service icon information for the item.
-  var isAccessible = true;
-  var itemTooltip = item.objectName;
-  var iconCssClass = '.service-type-icon' + (isShortcut ? '.shortcut' : '');
-  var iconAttributes = {
-    'ev-click': mercury.event(browseEvents.setShortcut, {
-      'item': item,
-      'save': !isShortcut,
-    })
-  };
-
-  if (item.isServer) {
-    isAccessible = item.serverInfo.isAccessible;
-    if (!isAccessible) {
-      itemTooltip += ' - Service seems to be offline or inaccessible';
-    }
-    iconAttributes.title = new AttributeHook(item.serverInfo.typeInfo.typeName);
-    iconAttributes.icon = new AttributeHook(
-      getServiceIcon(item.serverInfo.typeInfo.key, isShortcut)
-    );
-  } else {
-    iconAttributes.title = new AttributeHook('Intermediary Name');
-    iconAttributes.icon = new AttributeHook(getServiceIcon('', isShortcut));
-  }
-
-  // Construct the service icon.
-  var iconNode = h('core-icon' + iconCssClass, iconAttributes);
-
-  // Put the item card's pieces together.
-  var itemClassNames = 'item.card' +
-    (selected ? '.selected' : '') +
-    (!isAccessible ? '.inaccessible' : '');
-
-  return h('div.' + itemClassNames, {
-    'title': itemTooltip
-  }, [
-    h('a.label', {
-      'href': 'javascript:;',
-      'ev-click': mercury.event(
-        browseEvents.selectItem, {
-          name: item.objectName
-        })
-    }, [
-      iconNode,
-      h('span', item.mountedName)
-    ]),
-    expandAction
-  ]);
-}
-
-/*
  * Renders the current name being browsed, split into parts.
  * Each name part is a link to a parent.
  */
 function renderBreadcrumbs(browseState, navEvents) {
 
+  // only render the breadcrumbs for items and not bookmarks/recommendations
+  if (browseState.subPage !== 'items') {
+    // use a flex div to leave white-space inplace of breadcrumbs
+    return h('div', {
+      'flex': new AttributeHook('true')
+    });
+  }
+
   var isRooted = namespaceService.util.isRooted(browseState.namespace);
   var namespaceParts = browseState.namespace.split('/').filter(
     function(n) {
@@ -485,12 +484,16 @@
   );
   var breadCrumbs = [];
   if (!isRooted) {
+    // Add a relative root (empty namespace)
+    var rootUrl = browseRoute.createUrl(browseState, {
+      namespace: ''
+    });
     breadCrumbs.push(h('li.breadcrumb-item', [
       //TODO(aghassemi) refactor link generation code
       h('a', {
-        'href': browseRoute.createUrl(),
+        'href': rootUrl,
         'ev-click': mercury.event(navEvents.navigate, {
-          path: browseRoute.createUrl()
+          path: rootUrl
         })
       }, 'Home')
     ]));
@@ -501,11 +504,15 @@
     var fullName = (isRooted ? '/' : '') +
       namespaceService.util.join(namespaceParts.slice(0, i + 1));
 
+    var url = browseRoute.createUrl(browseState, {
+      namespace: fullName
+    });
+
     var listItem = h('li.breadcrumb-item', [
       h('a', {
-        'href': browseRoute.createUrl(fullName),
+        'href': url,
         'ev-click': mercury.event(navEvents.navigate, {
-          path: browseRoute.createUrl(fullName)
+          path: url
         })
       }, namePart)
     ]);
@@ -513,14 +520,15 @@
     breadCrumbs.push(listItem);
   }
 
-  return h('ul.breadcrumbs', breadCrumbs);
+  return h('ul.breadcrumbs', {
+    'flex': new AttributeHook('true')
+  }, breadCrumbs);
 }
 
 // Wire up events that we know how to handle
 function wireUpEvents(state, events) {
   events.browseNamespace(browseNamespace.bind(null, state, events));
   events.getNamespaceSuggestions(getNamespaceSuggestions.bind(null, state));
-  events.setShortcut(handleShortcuts.set.bind(null, state, events));
   events.selectItem(function(data) {
     state.selectedItemName.set(data.name);
     events.selectedItemDetails.displayItemDetails(data);
diff --git a/src/components/browse/item-card-list/index.css b/src/components/browse/item-card-list/index.css
new file mode 100644
index 0000000..fd012ab
--- /dev/null
+++ b/src/components/browse/item-card-list/index.css
@@ -0,0 +1,13 @@
+@import "common-style/theme.css";
+
+.items-container {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  border-bottom: var(--border);
+  padding-bottom: 0.5em;
+}
+
+.items-container:last-child {
+  border-bottom: none;
+}
\ No newline at end of file
diff --git a/src/components/browse/item-card-list/index.js b/src/components/browse/item-card-list/index.js
new file mode 100644
index 0000000..b4f6bac
--- /dev/null
+++ b/src/components/browse/item-card-list/index.js
@@ -0,0 +1,33 @@
+var mercury = require('mercury');
+var insertCss = require('insert-css');
+
+var ItemCard = require('./item-card/index');
+
+var css = require('./index.css');
+var h = mercury.h;
+
+module.exports.render = render;
+
+/*
+ * Renders a list of namespace items as cards in a list.
+ * @param opts.title {string} Title for the list. e.g "Bookmarks"
+ * @param opts.emptyText {string} Text to render when items is empty.
+ *  e.g No Bookmarks found.
+ * @param items {Array<namespaceitem>} @see services/namespace/item
+ */
+function render(items, browseState, browseEvents, navEvents, opts) {
+  insertCss(css);
+
+  var view;
+  if (browseState.isFinishedLoadingItems && items.length === 0) {
+    view = h('div.empty', h('span', opts.emptyText));
+  } else {
+    view = items.map(function(item) {
+      return ItemCard.render(item, browseState, browseEvents, navEvents);
+    });
+  }
+
+  var heading = h('h2', opts.title);
+
+  return h('div.items-container', [heading, view]);
+}
\ No newline at end of file
diff --git a/src/components/browse/item-card-list/item-card/get-service-icon.js b/src/components/browse/item-card-list/item-card/get-service-icon.js
new file mode 100644
index 0000000..0c18e71
--- /dev/null
+++ b/src/components/browse/item-card-list/item-card/get-service-icon.js
@@ -0,0 +1,15 @@
+module.exports = getServiceIcon;
+
+var serviceIconMap = Object.freeze({
+  'veyron-mounttable': 'social:circles-extended',
+  'veyron-unknown': 'cloud-queue',
+  '': 'folder-open'
+});
+
+/*
+ * Given the type of a service and whether the element should be filled or not,
+ * return the name of the corresponding core-icon to use for rendering.
+ */
+function getServiceIcon(type) {
+  return serviceIconMap[type];
+}
\ No newline at end of file
diff --git a/src/components/browse/item-card-list/item-card/index.css b/src/components/browse/item-card-list/item-card/index.css
new file mode 100644
index 0000000..68f5629
--- /dev/null
+++ b/src/components/browse/item-card-list/item-card/index.css
@@ -0,0 +1,5 @@
+@import "common-style/card.css";
+
+.item.inaccessible {
+  opacity: 0.5;
+}
diff --git a/src/components/browse/item-card-list/item-card/index.js b/src/components/browse/item-card-list/item-card/index.js
new file mode 100644
index 0000000..627edd8
--- /dev/null
+++ b/src/components/browse/item-card-list/item-card/index.js
@@ -0,0 +1,84 @@
+var mercury = require('mercury');
+var insertCss = require('insert-css');
+var getServiceIcon = require('./get-service-icon');
+
+var AttributeHook = require('../../../../lib/mercury/attribute-hook');
+
+var browseRoute = require('../../../../routes/browse');
+
+var css = require('./index.css');
+var h = mercury.h;
+
+module.exports.render = render;
+
+/*
+ * Renders a namespace item in a card view.
+ * @param item {namespaceitem} @see services/namespace/item
+ */
+function render(item, browseState, browseEvents, navEvents) {
+  insertCss(css);
+
+  var selected = (browseState.selectedItemName === item.objectName);
+
+  var url = browseRoute.createUrl(browseState, {
+    namespace: item.objectName,
+    viewType: 'grid'
+  });
+
+  // Prepare the drill if this item happens to be globbable.
+  var expandAction = null;
+  if (item.isGlobbable) {
+    expandAction = h('a.drill', {
+      'href': url,
+      'ev-click': mercury.event(navEvents.navigate, {
+        path: url
+      })
+    }, h('core-icon.icon', {
+      'icon': new AttributeHook('chevron-right')
+    }));
+  }
+
+  // Prepare tooltip and service icon information for the item.
+  var isAccessible = true;
+  var itemTooltip = item.objectName;
+  var iconCssClass = '.service-type-icon';
+  var iconAttributes = {};
+
+  if (item.isServer) {
+    isAccessible = item.serverInfo.isAccessible;
+    if (!isAccessible) {
+      itemTooltip += ' - Service seems to be offline or inaccessible';
+    }
+    iconAttributes.title = new AttributeHook(item.serverInfo.typeInfo.typeName);
+    iconAttributes.icon = new AttributeHook(
+      getServiceIcon(item.serverInfo.typeInfo.key)
+    );
+  } else {
+    iconAttributes.title = new AttributeHook('Intermediary Name');
+    iconAttributes.icon = new AttributeHook(getServiceIcon(''));
+  }
+
+  // Construct the service icon.
+  var iconNode = h('core-icon' + iconCssClass, iconAttributes);
+
+  // Put the item card's pieces together.
+  var itemClassNames = 'item.card' +
+    (selected ? '.selected' : '') +
+    (!isAccessible ? '.inaccessible' : '');
+
+  return h('div.' + itemClassNames, {
+    'title': itemTooltip
+  }, [
+    h('a.label', {
+      'href': 'javascript:;',
+      'ev-click': mercury.event(
+        browseEvents.selectItem, {
+          name: item.objectName
+        })
+    }, [
+      iconNode,
+      h('span', item.mountedName || '<root>')
+    ]),
+    expandAction
+  ]);
+}
\ No newline at end of file
diff --git a/src/components/browse/item-details/bookmark.js b/src/components/browse/item-details/bookmark.js
new file mode 100644
index 0000000..303a7c6
--- /dev/null
+++ b/src/components/browse/item-details/bookmark.js
@@ -0,0 +1,41 @@
+var bookmarksService = require('../../../services/bookmarks/service');
+
+var log = require('../../../lib/log')(
+  'components:browse:item-details:bookmark');
+
+module.exports = bookmark;
+
+function bookmark(state, events, data) {
+  var wasBookmarked = state.isBookmarked();
+  state.isBookmarked.set(data.bookmark);
+
+  bookmarksService.bookmark(data.name, data.bookmark).then(function() {
+    var toastText = 'Bookmark ' +
+      (data.bookmark ? 'added' : 'removed') +
+      ' for ' + data.name;
+
+    var undoAction = bookmark.bind(null, state, events, {
+      name: data.name,
+      bookmark: !data.bookmark
+    });
+
+    events.toast({
+      text: toastText,
+      action: undoAction,
+      actionText: 'UNDO'
+    });
+  }).catch(function(err) {
+    var errText = 'Failed to ' +
+      (data.bookmark ? 'add ' : 'remove') +
+      'bookmark for ' + data.name;
+
+    log.error(errText, err);
+
+    // reset state on error back to what it used to be
+    state.isBookmarked.set(wasBookmarked);
+    events.toast({
+      text: errText,
+      type: 'error'
+    });
+  });
+}
\ No newline at end of file
diff --git a/src/components/browse/item-details/display-item-details.js b/src/components/browse/item-details/display-item-details.js
index 2431c3d..e2a89f8 100644
--- a/src/components/browse/item-details/display-item-details.js
+++ b/src/components/browse/item-details/display-item-details.js
@@ -1,13 +1,18 @@
 var mercury = require('mercury');
-var namespaceService = require('../../../services/namespace/service');
-var smartService = require('../../../services/smart/service');
+
 var methodNameToVarHashKey = require('./methodNameToVarHashKey');
+var methodStart = require('./method-start.js');
+var methodEnd = require('./method-end.js');
+
+var methodForm = require('./method-form/index.js');
+
+var namespaceService = require('../../../services/namespace/service');
+var bookmarkService = require('../../../services/bookmarks/service');
+var smartService = require('../../../services/smart/service');
+
 var log = require('../../../lib/log')(
   'components:browse:item-details:display-item-details'
 );
-var methodForm = require('./method-form/index.js');
-var methodStart = require('./method-start.js');
-var methodEnd = require('./method-end.js');
 
 module.exports = displayItemDetails;
 
@@ -40,8 +45,12 @@
     state.showLoadingIndicator.set(true);
   }, SHOW_LOADING_THRESHOLD);
 
-  namespaceService.getNamespaceItem(name).then(function(itemObs) {
+  var resultsPromise = Promise.all([
+    bookmarkService.isBookmarked(name),
+    namespaceService.getNamespaceItem(name)
+  ]);
 
+  resultsPromise.then(function(results) {
     /*
      * Since async call, by the time we are here, a different name
      * might be selected.
@@ -51,11 +60,16 @@
       return;
     }
 
+    var isBookmarked = results[0];
+    var itemObs = results[1];
+
     // Indicate we finished loading
     setIsLoaded();
 
     state.put('item', itemObs);
 
+    state.isBookmarked.set(isBookmarked);
+
     mercury.watch(itemObs, function(item) {
       if (!item.isServer) {
         return;
diff --git a/src/components/browse/item-details/index.css b/src/components/browse/item-details/index.css
index 2ff892f..c3f0710 100644
--- a/src/components/browse/item-details/index.css
+++ b/src/components/browse/item-details/index.css
@@ -1,5 +1,6 @@
 @import "common-style/sizes.css";
 @import "common-style/theme.css";
+@import "common-style/card.css";
 
 .field {
   font-size: var(--size-font-small);
@@ -11,6 +12,17 @@
   color: var(--color-text-secondary);
 }
 
+paper-icon-button.bookmarked {
+  color: var(--color-bright);
+}
+
+.item-actions {
+  margin: var(--size-space-xxsmall) -var(--size-space-xsmall);
+  margin-top: -var(--size-space-xxsmall);
+  padding: var(--size-space-xxsmall) 0;
+  border-bottom: var(--border);
+}
+
 .method-input {
   display: inline-block;
   vertical-align: top;
@@ -18,10 +30,20 @@
   overflow: hidden;
 }
 
-.tooltip.method-tooltip::shadow .core-tooltip {
+.tooltip.field-tooltip::shadow .core-tooltip, .tooltip.method-tooltip::shadow .core-tooltip {
+  line-height: 0.9em; /* overrides Polymer's 6px line-height */
+  white-space: pre-wrap;
+  font-size: var(--size-font-xsmall);
+}
+
+.tooltip.field-tooltip::shadow .core-tooltip {
   width: 24em;
 }
 
+.tooltip.field-tooltip::shadow .core-tooltip {
+  width: 36em;
+}
+
 .method-input .label {
   align-items: center;
   overflow: hidden;
diff --git a/src/components/browse/item-details/index.js b/src/components/browse/item-details/index.js
index b0f83e9..94fb9fa 100644
--- a/src/components/browse/item-details/index.js
+++ b/src/components/browse/item-details/index.js
@@ -1,12 +1,18 @@
 var mercury = require('mercury');
-var AttributeHook = require('../../../lib/mercury/attribute-hook');
 var insertCss = require('insert-css');
-var displayItemDetails = require('./display-item-details');
-var h = mercury.h;
-var css = require('./index.css');
+
+var AttributeHook = require('../../../lib/mercury/attribute-hook');
+
 var methodNameToVarHashKey = require('./methodNameToVarHashKey.js');
+
+var displayItemDetails = require('./display-item-details');
+var bookmark = require('./bookmark');
+
 var methodForm = require('./method-form/index.js');
 
+var css = require('./index.css');
+var h = mercury.h;
+
 module.exports = create;
 module.exports.render = render;
 
@@ -52,10 +58,17 @@
      * Whether a loading indicator should be displayed instead of content
      * @type {mercury.value<boolean>}
      */
-    showLoadingIndicator: mercury.value(false)
+    showLoadingIndicator: mercury.value(false),
+
+    /*
+     * Whether item is bookmarked
+     * @type {mercury.value<boolean>}
+     */
+    isBookmarked: mercury.value(false)
   });
 
   var events = mercury.input([
+    'bookmark',
     'displayItemDetails',
     'tabSelected',
     'methodForm',
@@ -79,12 +92,12 @@
 
   var tabContent;
 
-  if(state.showLoadingIndicator) {
+  if (state.showLoadingIndicator) {
     tabContent = h('paper-spinner', {
       'active': new AttributeHook(true),
       'aria-label': new AttributeHook('Loading')
     });
-  } else if(state.item) {
+  } else if (state.item) {
     var detailsContent = renderDetailsContent(state, events);
 
     var methodsContent;
@@ -114,6 +127,33 @@
 }
 
 /*
+ * Renders an action bar on top of the details panel page.
+ */
+function renderActions(state, events) {
+  var item = state.item;
+
+  // Bookmark action
+  var isBookmarked = state.isBookmarked;
+  var bookmarkIcon = 'bookmark' + (!isBookmarked ? '-outline' : '');
+  var bookmarkTitle = (isBookmarked ? 'Remove bookmark ' : 'Bookmark');
+  var bookmarkAction = h('core-tooltip', {
+      'label': new AttributeHook(bookmarkTitle),
+      'position': new AttributeHook('right'),
+    },
+    h('paper-icon-button' + (isBookmarked ? '.bookmarked' : ''), {
+      'icon': new AttributeHook(bookmarkIcon),
+      'alt': new AttributeHook(bookmarkTitle),
+      'ev-click': mercury.event(events.bookmark, {
+        bookmark: !isBookmarked,
+        name: item.objectName
+      })
+    })
+  );
+
+  return h('div.icon-group.item-actions', [bookmarkAction]);
+}
+
+/*
  * Renders details about the current service object.
  * Note: Currently renders in the same tab as renderMethodsContent.
  */
@@ -128,7 +168,10 @@
     typeName = 'Intermediary Name';
   }
 
+  var actions = renderActions(state, events);
+
   var displayItems = [
+    actions,
     renderFieldItem('Name', (item.objectName || '<root>')),
     renderFieldItem('Type', typeName, typeDescription)
   ];
@@ -142,7 +185,7 @@
 
       // Use an info icon whose tooltip reveals the description.
       serviceDescs.push(h('div', [
-        h('core-tooltip.tooltip', {
+        h('core-tooltip.tooltip.field-tooltip', {
           'label': desc || '<no description>',
           'position': 'right'
         }, h('core-icon.icon.info', {
@@ -199,9 +242,9 @@
   methodNames.sort().forEach(function(methodName) {
     var methodKey = methodNameToVarHashKey(methodName);
     methods.push(methodForm.render(
-        state.methodForm[methodKey],
-        events.methodForm[methodKey]
-      ));
+      state.methodForm[methodKey],
+      events.methodForm[methodKey]
+    ));
   });
 
   return h('div', methods); // Note: allows 0 method signatures
@@ -249,7 +292,7 @@
   content = h('span', content);
   if (tooltip) {
     // If there is a tooltip, wrap the content in it
-    content = h('core-tooltip.tooltip', {
+    content = h('core-tooltip.tooltip.field-tooltip', {
       'label': new AttributeHook(tooltip),
       'position': 'right'
     }, content);
@@ -267,4 +310,5 @@
   events.tabSelected(function(data) {
     state.selectedTabIndex.set(data.index);
   });
+  events.bookmark(bookmark.bind(null, state, events));
 }
\ No newline at end of file
diff --git a/src/components/browse/item-details/method-end.js b/src/components/browse/item-details/method-end.js
index 2a25f1f..3cdd315 100644
--- a/src/components/browse/item-details/method-end.js
+++ b/src/components/browse/item-details/method-end.js
@@ -1,8 +1,11 @@
-var h = require('mercury').h;
+var formatDetail = require('./format-detail');
+
 var smartService = require('../../../services/smart/service');
+
 var log = require('../../../lib/log')(
   'components:browse:item-details:method-end');
-var formatDetail = require('./format-detail');
+
+var h = require('mercury').h;
 
 module.exports = methodEnd;
 
diff --git a/src/components/browse/item-details/method-form/index.js b/src/components/browse/item-details/method-form/index.js
index c459f14..85c16cd 100644
--- a/src/components/browse/item-details/method-form/index.js
+++ b/src/components/browse/item-details/method-form/index.js
@@ -1,19 +1,26 @@
 var mercury = require('mercury');
-var h = mercury.h;
 var _ = require('lodash');
 var guid = require('guid');
+
+var makeRPC = require('./make-rpc.js');
+
 var arraySet = require('../../../../lib/arraySet');
 var setMercuryArray = require('../../../../lib/mercury/setMercuryArray');
 var AttributeHook = require('../../../../lib/mercury/attribute-hook');
 var PropertyValueEvent =
   require('../../../../lib/mercury/property-value-event');
+
 var store = require('../../../../lib/store');
-var log = require('../../../../lib/log')(
-  'components:browse:item-details:method-form');
+
 var smartService = require('../../../../services/smart/service');
 var hashSignature =
   require('../../../../services/namespace/service').hashSignature;
-var makeRPC = require('./make-rpc.js');
+
+var log = require('../../../../lib/log')(
+  'components:browse:item-details:method-form');
+
+var h = mercury.h;
+
 
 module.exports = create;
 module.exports.render = render;
diff --git a/src/components/browse/item-details/method-form/make-rpc.js b/src/components/browse/item-details/method-form/make-rpc.js
index d1399e1..7a93500 100644
--- a/src/components/browse/item-details/method-form/make-rpc.js
+++ b/src/components/browse/item-details/method-form/make-rpc.js
@@ -1,4 +1,5 @@
 var namespaceService = require('../../../../services/namespace/service');
+
 var log = require('../../../../lib/log')(
   'components:browse:item-details:method-form:make-rpc'
 );
diff --git a/src/components/browse/item-details/methodNameToVarHashKey.js b/src/components/browse/item-details/methodNameToVarHashKey.js
index 61b3b1d..dc8e670 100644
--- a/src/components/browse/item-details/methodNameToVarHashKey.js
+++ b/src/components/browse/item-details/methodNameToVarHashKey.js
@@ -1,4 +1,3 @@
-
 /*
  * Mercury VarHash has an issue where reserved keywords like 'delete', 'put'
  * can not be used a hash keys :`(
diff --git a/src/components/browse/items/grid-view/index.js b/src/components/browse/items/grid-view/index.js
new file mode 100644
index 0000000..3e07496
--- /dev/null
+++ b/src/components/browse/items/grid-view/index.js
@@ -0,0 +1,22 @@
+var ItemCardList = require('../../item-card-list/index');
+
+module.exports = create;
+module.exports.render = render;
+
+function create() {}
+
+function render(itemsState, browseState, browseEvents, navEvents) {
+  var isSearch = !!browseState.globQuery;
+  var emptyText = (isSearch ? 'No glob search results' : 'No children');
+  var title = (isSearch ? 'Glob Search Results' : 'Grid View');
+
+  return ItemCardList.render(
+    itemsState.items,
+    browseState,
+    browseEvents,
+    navEvents, {
+      title: title,
+      emptyText: emptyText
+    }
+  );
+}
\ No newline at end of file
diff --git a/src/components/browse/items/index.css b/src/components/browse/items/index.css
new file mode 100644
index 0000000..bdcccb3
--- /dev/null
+++ b/src/components/browse/items/index.css
@@ -0,0 +1,11 @@
+.items-container {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  border-bottom: var(--border);
+  padding-bottom: 0.5em;
+}
+
+.items-container:last-child {
+  border-bottom: none;
+}
\ No newline at end of file
diff --git a/src/components/browse/items/index.js b/src/components/browse/items/index.js
new file mode 100644
index 0000000..6849a92
--- /dev/null
+++ b/src/components/browse/items/index.js
@@ -0,0 +1,131 @@
+var mercury = require('mercury');
+var guid = require('guid');
+
+var GridView = require('./grid-view/index');
+var TreeView = require('./tree-view/index');
+var VisualizeView = require('./visualize-view/index');
+
+var namespaceService = require('../../../services/namespace/service');
+
+var log = require('../../../lib/log')('components:browse:items');
+
+module.exports = create;
+module.exports.render = render;
+module.exports.load = load;
+module.exports.trySetViewType = trySetViewType;
+
+var VALID_VIEW_TYPES = ['grid', 'tree', 'visualize'];
+
+/*
+ * Items view.
+ * Renders one of: Grid, Tree or Visualize views depending on the state
+ */
+function create() {
+
+  var state = mercury.varhash({
+
+    /*
+     * List of namespace items to display
+     * @see services/namespace/item
+     * @type {Array<namespaceitem>}
+     */
+    items: mercury.array([]),
+
+    /*
+     * Specifies the current view type of the items.
+     * One of: grid, tree, visualize
+     */
+    viewType: mercury.value('grid'),
+
+    /*
+     * uuid for the current browse-namespace request.
+     * Needed to handle out-of-order return of async calls.
+     * @type {String}
+     */
+    currentRequestId: mercury.value('')
+  });
+
+  return {
+    state: state
+  };
+}
+
+function trySetViewType(state, viewType) {
+  var isValid = VALID_VIEW_TYPES.indexOf(viewType) >= 0;
+  if (!isValid) {
+    return false;
+  }
+
+  state.viewType.set(viewType);
+  return true;
+}
+
+/*
+ * Does the initialization and loading of the data necessary to display the
+ * namespace items.
+ * Called and used by the parent browse view to initialize the view on
+ * request.
+ * Returns a promise that will be resolved when loading is finished. Promise
+ * is used by the parent browse view to display a loading progressbar.
+ */
+function load(state, namespace, globQuery) {
+
+  // TODO(aghassemi)
+  // -Rename the concept to "init", not every component may have it.
+  // does not return anything, we can have showLoadingIndicator(bool) be an
+  // functionality that can be requested of the browse view.
+  // -Move items to "GridView"
+  // -Have a common component between tree and vis to share the childrens map
+
+  if (state.viewType() !== 'grid') {
+    return Promise.resolve();
+  }
+
+  // Search the namespace and update the browseState's items.
+  var requestId = guid.create().value;
+  state.currentRequestId.set(requestId);
+  state.put('items', mercury.array([]));
+
+  return new Promise(function(resolve, reject) {
+    namespaceService.search(namespace, globQuery).
+    then(function globResultsReceived(items) {
+      if (!isCurrentRequest()) {
+        resolve();
+        return;
+      }
+      state.put('items', items);
+      items.events.on('end', loadingFinished);
+      items.events.on('streamError', loadingFinished);
+    }).catch(function(err) {
+      log.error(err);
+      reject();
+    });
+
+    function loadingFinished() {
+      if (!isCurrentRequest()) {
+        return;
+      }
+      resolve();
+    }
+  });
+
+  // Whether we are still the current request. This is used to ignore out of
+  // order return of async calls where user has moved on to another item
+  // by the time previous requests result comes back.
+  function isCurrentRequest() {
+    return state.currentRequestId() === requestId;
+  }
+}
+
+function render(state, browseState, browseEvents, navEvents) {
+  switch (state.viewType) {
+    case 'grid':
+      return GridView.render(state, browseState, browseEvents, navEvents);
+    case 'tree':
+      return TreeView.render(state, browseState, browseEvents, navEvents);
+    case 'visualize':
+      return VisualizeView.render(state, browseState, browseEvents, navEvents);
+    default:
+      log.error('Unsupported viewType: ' + state.viewType);
+  }
+}
\ No newline at end of file
diff --git a/src/components/browse/items/tree-view/index.js b/src/components/browse/items/tree-view/index.js
new file mode 100644
index 0000000..6a2a104
--- /dev/null
+++ b/src/components/browse/items/tree-view/index.js
@@ -0,0 +1,12 @@
+var mercury = require('mercury');
+
+var h = mercury.h;
+
+module.exports = create;
+module.exports.render = render;
+
+function create() {}
+
+function render(itemsState, browseState, browseEvents, navEvents) {
+  return h('h2', 'Tree View (TODO)');
+}
\ No newline at end of file
diff --git a/src/components/visualize/index.css b/src/components/browse/items/visualize-view/index.css
similarity index 100%
rename from src/components/visualize/index.css
rename to src/components/browse/items/visualize-view/index.css
diff --git a/src/components/visualize/index.js b/src/components/browse/items/visualize-view/index.js
similarity index 82%
rename from src/components/visualize/index.js
rename to src/components/browse/items/visualize-view/index.js
index 0ce6d1f..602af98 100644
--- a/src/components/visualize/index.js
+++ b/src/components/browse/items/visualize-view/index.js
@@ -1,34 +1,39 @@
 var mercury = require('mercury');
 var insertCss = require('insert-css');
 var vis = require('vis');
+
+var namespaceService = require('../../../../services/namespace/service');
+
+var log = require('../../../../lib/log')('components:browse:items:tree-view');
+
 var css = require('./index.css');
-var namespaceService = require('../../services/namespace/service');
-var log = require('../../lib/log')('components:visualize');
+var h = mercury.h;
 
 module.exports = create;
 module.exports.render = render;
 
-// Maximum number of levels that are automatically shown
-var MAX_AUTO_LOAD_DEPTH = 3;
-
-/*
- * Visualize view
- */
 function create() {}
 
-function render(browseState) {
+function render(itemsState, browseState, browseEvents, navEvents) {
+
   insertCss(css);
+
   return [
-    new TreeWidget(browseState)
+    h('h2', 'Visualize View'),
+    new TreeWidget(browseState, browseEvents)
   ];
 }
 
-function TreeWidget(browseState) {
+// Maximum number of levels that are automatically shown
+var MAX_AUTO_LOAD_DEPTH = 3;
+
+function TreeWidget(browseState, browseEvents) {
   if (!(this instanceof TreeWidget)) {
     return new TreeWidget(browseState);
   }
 
   this.browseState = browseState;
+  this.browseEvents = browseEvents;
   this.nodes = new vis.DataSet();
   this.edges = new vis.DataSet();
 }
@@ -62,7 +67,7 @@
   var options = {
     hover: false,
     selectable: true, // Need this or nodes won't be click-able
-    smoothCurves: true,
+    smoothCurves: false,
     stabilize: false,
     edges: {
       width: 1
@@ -87,6 +92,19 @@
 
   // Event listeners.
   network.on('click', function onClick(data) {
+    // refresh side view
+    var nodeId = data.nodes[0];
+    var node = network.nodes[nodeId];
+
+    if (node) {
+      self.browseEvents.selectItem({
+        name: nodeId
+      });
+    }
+  });
+
+  network.on('doubleClick', function onClick(data) {
+    // drill
     var nodeId = data.nodes[0];
     var node = network.nodes[nodeId];
 
@@ -94,7 +112,6 @@
       self.loadSubNodes(node);
     }
   });
-
   return network;
 };
 
@@ -144,7 +161,7 @@
         if (item.level - self.rootNode.level < MAX_AUTO_LOAD_DEPTH) {
           self.loadSubNodes(item);
         } else {
-          item.title = 'Click to expand';
+          item.title = 'Double-click to expand';
         }
       });
       self.nodes.add(newNodes);
diff --git a/src/components/browse/recommend-shortcuts.js b/src/components/browse/recommend-shortcuts.js
deleted file mode 100644
index 5bf541f..0000000
--- a/src/components/browse/recommend-shortcuts.js
+++ /dev/null
@@ -1,30 +0,0 @@
-var mercury = require('mercury');
-var _ = require('lodash');
-var log = require('../../lib/log')('components:browse:recommend-shortcuts');
-var namespaceService = require('../../services/namespace/service');
-var smartService = require('../../services/smart/service');
-
-module.exports = recommendShortcuts;
-
-/*
- * Asks the smartService to asynchronously update the browseState with the
- * associated recommendations.
- */
-function recommendShortcuts(browseState) {
-  var input = {
-    'name': '',
-    'exclude': _.pluck(browseState.userShortcuts(), 'objectName')
-  };
-  smartService.predict('learner-shortcut', input).then(function(predictions) {
-    browseState.put('recShortcuts', mercury.array([]));
-    predictions.forEach(function(prediction, i) {
-      namespaceService.getNamespaceItem(prediction.item).then(function(item) {
-        browseState.recShortcuts.put(i, item);
-      }).catch(function(err) {
-        log.error('Failed to get recommended shortcut:', prediction, err);
-      });
-    });
-  }).catch(function(err) {
-    log.error('Could not load recommended shortcuts', err);
-  });
-}
\ No newline at end of file
diff --git a/src/components/browse/recommendations/index.js b/src/components/browse/recommendations/index.js
new file mode 100644
index 0000000..4362fdf
--- /dev/null
+++ b/src/components/browse/recommendations/index.js
@@ -0,0 +1,64 @@
+var mercury = require('mercury');
+var ItemCardList = require('../item-card-list/index');
+
+var recommendationsService =
+  require('../../../services/recommendations/service');
+
+var log = require('../../../lib/log')('components:browse:recommendation');
+
+module.exports = create;
+module.exports.render = render;
+module.exports.load = load;
+
+/*
+ * Recommendation view
+ */
+function create() {
+
+  var state = mercury.varhash({
+    /*
+     * List of recommended shortcuts to display
+     * @see services/namespace/item
+     * @type {Array<namespaceitem>}
+     */
+    recShortcuts: mercury.array([]),
+
+  });
+
+  return {
+    state: state
+  };
+}
+
+function render(state, browseState, browseEvents, navEvents) {
+  return ItemCardList.render(
+    state.recShortcuts,
+    browseState,
+    browseEvents,
+    navEvents, {
+      title: 'Recommendations',
+      emptyText: 'No recommendations'
+    }
+  );
+}
+
+/*
+ * Does the initialization and loading of the data necessary to display the
+ * recommendations.
+ * Called and used by the parent browse view to initialize the view on
+ * request.
+ * Returns a promise that will be resolved when loading is finished. Promise
+ * is used by the parent browse view to display a loading progressbar.
+ */
+function load(state) {
+  return new Promise(function(resolve, reject) {
+    recommendationsService.getAll()
+      .then(function recReceived(items) {
+        state.put('recShortcuts', items);
+        items.events.on('end', resolve);
+      }).catch(function(err) {
+        log.error(err);
+        reject();
+      });
+  });
+}
\ No newline at end of file
diff --git a/src/components/common-style/card.css b/src/components/common-style/card.css
new file mode 100644
index 0000000..bc47054
--- /dev/null
+++ b/src/components/common-style/card.css
@@ -0,0 +1,53 @@
+@import "common-style/theme.css";
+@import "common-style/sizes.css";
+
+.item.card {
+  box-sizing: border-box;
+  background-color: var(--color-white);
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 0;
+  height: 2.5em;
+  min-width: 8em;
+  margin: 0.75em;
+  border-radius: 3px;
+  overflow: hidden;
+  position: relative;
+  box-shadow: var(--shadow-all-around);
+  border: var(--border);
+}
+.item .label, .item .drill {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 0.5em;
+}
+.item .label {
+  text-decoration: none;
+  flex: 1;
+  overflow: hidden;
+  white-space: nowrap;
+}
+.item .drill {
+  width: 1.5em;
+  background-color: var(--color-grey-light);
+  border-left: var(--border);
+}
+
+.item.selected .drill {
+  background-color: var(--color-bright);
+}
+
+.item a:hover, .item a:focus{
+  opacity: 0.7;
+}
+
+.item.card .icon {
+  align-self: center;
+  padding-right: 0.5em;
+}
+
+.item.selected.card {
+  background-color: var(--color-bright);
+  color: var(--color-text-primary-invert);
+}
diff --git a/src/components/common-style/defaults.css b/src/components/common-style/defaults.css
index 33efad7..f9195a9 100644
--- a/src/components/common-style/defaults.css
+++ b/src/components/common-style/defaults.css
@@ -36,7 +36,7 @@
 
 paper-progress {
   width: 100%;
-  height: 2px;
+  height: 4px;
 }
 
 /*
@@ -79,6 +79,13 @@
   margin-right: var(--size-space-xsmall);
 }
 
+.empty {
+  width: 100%;
+  padding: 1em;
+  text-align: center;
+  color: var(--color-text-secondary);
+}
+
 /*
  * Applies a tiny margin to the left of the element.
  */
diff --git a/src/components/common-style/sizes.css b/src/components/common-style/sizes.css
index e8d37d2..b32f372 100644
--- a/src/components/common-style/sizes.css
+++ b/src/components/common-style/sizes.css
@@ -9,12 +9,14 @@
   --size-font-xlarge: 1.2em;
 
   /* margin and padding size */
-  --size-space-xxsmall: 0.25em;
+  --size-space-xxsmall: 0.2em;
   --size-space-xsmall: 0.5em;
   --size-space-small: 0.75em;
   --size-space-normal: 1em;
   --size-space-large: 1.25em;
   --size-space-xlarge: 1.5em;
+  --size-space-xxlarge: 3em;
 
   --size-input-width-normal: 16em;
+  --size-input-width-small: 8em;
 }
diff --git a/src/components/help/selectTab.js b/src/components/help/selectTab.js
index f992b7b..ddb4eb0 100644
--- a/src/components/help/selectTab.js
+++ b/src/components/help/selectTab.js
@@ -9,8 +9,7 @@
 function selectTab(state, events, tabKey) {
   // If the tab is invalid, go to the error page.
   if (sections.get(tabKey) === undefined) {
-    // TODO(aghassemi): Add 404 error.
-    // events.error(type.404);
+    //TODO(aghassemi) Needs to be 404 error when we have support for 404
     events.error(new Error('Invalid help page: ' + tabKey));
   } else {
     // Since the tabKey is valid, the selectedTab can be updated.
diff --git a/src/components/main-content/index.js b/src/components/main-content/index.js
index ebaa676..acc5c03 100644
--- a/src/components/main-content/index.js
+++ b/src/components/main-content/index.js
@@ -3,7 +3,6 @@
 var Browse = require('../browse/index');
 var Help = require('../help/index');
 var ErrorPage = require('../error/index');
-var Visualize = require('../visualize/index');
 var css = require('./index.css');
 
 var h = mercury.h;
@@ -47,8 +46,6 @@
       return Help.render(state.help, events.help);
     case 'error':
       return ErrorPage.render(state.error);
-    case 'visualize':
-      return Visualize.render(state.browse);
     default:
       // We shouldn't get here with proper route handlers, so it's an error(bug)
       throw new Error('Could not find page ' + pageKey);
diff --git a/src/components/sidebar/index.js b/src/components/sidebar/index.js
index 3679dee..4ffffa8 100644
--- a/src/components/sidebar/index.js
+++ b/src/components/sidebar/index.js
@@ -2,7 +2,6 @@
 var insertCss = require('insert-css');
 var browseRoute = require('../../routes/browse');
 var helpRoute = require('../../routes/help');
-var visualizeRoute = require('../../routes/visualize');
 var css = require('./index.css');
 
 var h = mercury.h;
@@ -28,25 +27,9 @@
   function renderNavigationItems() {
     var navigationItems = [{
       key: 'browse',
-      label: 'Iconview',
-      icon: 'apps',
-      href: browseRoute.createUrl(
-        state.browse.namespace,
-        state.browse.globQuery
-      )
-    }, {
-      key: 'tree',
-      label: 'Treeview',
-      icon: 'chevron-left',
-      href: browseRoute.createUrl(
-        state.browse.namespace,
-        state.browse.globQuery
-      )
-    }, {
-      key: 'visualize',
-      label: 'Visualize',
-      icon: 'social:circles-extended',
-      href: visualizeRoute.createUrl()
+      label: 'Browse',
+      icon: 'explore',
+      href: browseRoute.createUrl(state.browse)
     }, {
       key: 'help',
       label: 'Help',
diff --git a/src/lib/log.js b/src/lib/log.js
index 0be661f..344689c 100644
--- a/src/lib/log.js
+++ b/src/lib/log.js
@@ -17,7 +17,7 @@
  *  as DeLogger? DeLog is taken :(
  */
 var debug = require('debug');
-var extend = require('xtend');
+var extend = require('extend');
 
 module.exports = log;
 module.exports.disable = disable;
diff --git a/src/router.js b/src/router.js
index 36fe972..da0f12b 100644
--- a/src/router.js
+++ b/src/router.js
@@ -18,7 +18,7 @@
     var path = normalizePath(data.path);
     var route = routes.match(path);
     if (!route) {
-      //TOOD(aghassemi) redirect to 404 error view?
+      //TODO(aghassemi) Needs to be 404 error when we have support for 404
       return;
     }
     if (!data.skipHistoryPush) {
diff --git a/src/routes/bookmarks.js b/src/routes/bookmarks.js
new file mode 100644
index 0000000..5d37da7
--- /dev/null
+++ b/src/routes/bookmarks.js
@@ -0,0 +1,20 @@
+module.exports = function(routes) {
+  routes.addRoute('/bookmarks', handleBookmarksRoute);
+};
+
+module.exports.createUrl = function() {
+    return '#/bookmarks';
+};
+
+function handleBookmarksRoute(state, events, params) {
+
+  // Set the page to browse
+  state.navigation.pageKey.set('browse');
+  state.viewport.title.set('Browse');
+
+  // Trigger browse components browseNamespace event
+  events.browse.browseNamespace({
+    'namespace': state.browse.namespace(),
+    'subPage': 'bookmarks'
+  });
+}
\ No newline at end of file
diff --git a/src/routes/browse.js b/src/routes/browse.js
index 3f56fde..a3ec64b 100644
--- a/src/routes/browse.js
+++ b/src/routes/browse.js
@@ -1,25 +1,42 @@
 var urlUtil = require('url');
 var qsUtil = require('querystring');
+
 var exists = require('../lib/exists');
-var log = require('../lib/log');
 var store = require('../lib/store');
 
+var log = require('../lib/log')('routes:browse');
+
 module.exports = function(routes) {
-  // Url pattern: /browse/veyronNameSpace?glob=*
+  // Url pattern: /browse/veyronNameSpace?glob=*&viewType=grid
   routes.addRoute('/browse/:namespace?', handleBrowseRoute);
 };
 
-module.exports.createUrl = function(namespace, globquery) {
+module.exports.createUrl = function(browseState, opts) {
+  var globQuery;
+  var viewType;
+  var namespace;
+  if (opts) {
+    globQuery = opts.globQuery;
+    viewType = opts.viewType;
+    namespace = opts.namespace;
+  }
+
+  // We preserve namespace and viewtype if they are not provided
+  // We reset globquery unless provided
+  namespace = (namespace === undefined ? browseState.namespace : namespace);
+  viewType = (viewType === undefined ? browseState.viewType : viewType);
+
   var path = '/browse';
   if (exists(namespace)) {
     namespace = encodeURIComponent(namespace);
     path += '/' + namespace;
   }
-  var query;
-  if (exists(globquery)) {
-    query = {
-      'glob': globquery
-    };
+  var query = {};
+  if (exists(globQuery)) {
+    query['glob'] = globQuery;
+  }
+  if (exists(viewType)) {
+    query['viewtype'] = viewType;
   }
   return '#' + urlUtil.format({
     pathname: path,
@@ -33,13 +50,19 @@
   state.navigation.pageKey.set('browse');
   state.viewport.title.set('Browse');
 
-  var namespace = '';
-  var globquery = '';
+  var namespace;
+  var globquery;
+  var viewtype;
   if (params.namespace) {
     var parsed = urlUtil.parse(params.namespace);
-    namespace = parsed.pathname || '';
+    if (parsed.pathname) {
+      namespace = parsed.pathname;
+    }
+
     if (parsed.query) {
-      globquery = qsUtil.parse(parsed.query).glob;
+      var queryString = qsUtil.parse(parsed.query);
+      globquery = queryString.glob;
+      viewtype = queryString.viewtype;
     }
   }
 
@@ -51,6 +74,8 @@
   // Trigger browse components browseNamespace event
   events.browse.browseNamespace({
     'namespace': namespace,
-    'globQuery': globquery
+    'globQuery': globquery,
+    'viewType': viewtype,
+    'subPage': 'items'
   });
 }
\ No newline at end of file
diff --git a/src/routes/index.js b/src/routes/index.js
index bc56277..273720a 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -1,7 +1,8 @@
 var browseRoute = require('./browse');
-var log = require('../lib/log');
 var store = require('../lib/store');
 
+var log = require('../lib/log')('routes:index');
+
 module.exports = function(routes) {
   routes.addRoute('/', handleIndexRoute);
 };
@@ -17,14 +18,18 @@
 
     // Redirect to browse
     events.navigation.navigate({
-      path: browseRoute.createUrl(index)
+      path: browseRoute.createUrl(state.browse, {
+        namespace: index
+      })
     });
   }).catch(function(err) {
     log.warn('Unable to access stored index', err);
 
     // Redirect to browse
     events.navigation.navigate({
-      path: browseRoute.createUrl(index)
+      path: browseRoute.createUrl(state.browse, {
+        namespace: index
+      })
     });
   });
 }
diff --git a/src/routes/recommendations.js b/src/routes/recommendations.js
new file mode 100644
index 0000000..0c10dbf
--- /dev/null
+++ b/src/routes/recommendations.js
@@ -0,0 +1,20 @@
+module.exports = function(routes) {
+  routes.addRoute('/recommendation', handleRecommendationRoute);
+};
+
+module.exports.createUrl = function() {
+    return '#/recommendation';
+};
+
+function handleRecommendationRoute(state, events, params) {
+
+  // Set the page to browse
+  state.navigation.pageKey.set('browse');
+  state.viewport.title.set('Browse');
+
+  // Trigger browse components browseNamespace event
+  events.browse.browseNamespace({
+    'namespace': state.browse.namespace(),
+    'subPage': 'recommendations'
+  });
+}
\ No newline at end of file
diff --git a/src/routes/register-routes.js b/src/routes/register-routes.js
index b91c99f..0c01d6c 100644
--- a/src/routes/register-routes.js
+++ b/src/routes/register-routes.js
@@ -8,5 +8,6 @@
   require('./help')(routes);
   require('./browse')(routes);
   require('./error')(routes);
-  require('./visualize')(routes);
+  require('./recommendations')(routes);
+  require('./bookmarks')(routes);
 }
\ No newline at end of file
diff --git a/src/routes/visualize.js b/src/routes/visualize.js
deleted file mode 100644
index ba0b398..0000000
--- a/src/routes/visualize.js
+++ /dev/null
@@ -1,14 +0,0 @@
-module.exports = function(routes) {
-  routes.addRoute('/visualize', handleVisualizeRoute);
-};
-
-module.exports.createUrl = function() {
-  return '#/visualize';
-};
-
-function handleVisualizeRoute(state) {
-
-  // Set the page to visualize
-  state.navigation.pageKey.set('visualize');
-  state.viewport.title.set('Visualize');
-}
\ No newline at end of file
diff --git a/src/services/bookmarks/service.js b/src/services/bookmarks/service.js
new file mode 100644
index 0000000..36e2214
--- /dev/null
+++ b/src/services/bookmarks/service.js
@@ -0,0 +1,130 @@
+var mercury = require('mercury');
+var _ = require('lodash');
+var EventEmitter = require('events').EventEmitter;
+
+var arraySet = require('../../lib/arraySet');
+var store = require('../../lib/store');
+var freeze = require('../../lib/mercury/freeze');
+
+var namespaceService = require('../namespace/service');
+
+var log = require('../../lib/log')('services:bookmarks:service');
+
+module.exports = {
+  getAll: getAll,
+  bookmark: bookmark,
+  isBookmarked: isBookmarked
+};
+
+// Data is loaded from and saved to this key in the store.
+var USER_BOOKMARKS_KEY = 'bookmarks-store-key';
+
+// Singleton state for all the bookmarks.
+var bookmarksObs = mercury.array([]);
+
+/*
+ * Gets all the namespace items that are bookmarked
+ * As new bookmarks become available/removed the observable array will change
+ * to reflect the changes.
+ *
+ * The observable result has an events property which is an EventEmitter
+ * and emits 'end', 'itemError' events.
+ *
+ * @return {Promise.<mercury.array>} Promise of an observable array
+ * of bookmark items
+ */
+function getAll() {
+  // Empty out the array
+  bookmarksObs.splice(0, bookmarksObs.getLength());
+  var immutableBookmarksObs = freeze(bookmarksObs);
+  immutableBookmarksObs.events = new EventEmitter();
+
+  return loadKeys().then(getBookmarkItems);
+
+  function getBookmarkItems(names) {
+    var allItems = names.map(function(name) {
+      return addNamespaceItem(name).catch(function(err) {
+        immutableBookmarksObs.events.emit('itemError', {
+          name: name,
+          error: err
+        });
+        log.error('Failed to create item for "' + name + '"', err);
+      });
+    });
+
+    Promise.all(allItems).then(function() {
+      immutableBookmarksObs.events.emit('end');
+    }).catch(function() {
+      immutableBookmarksObs.events.emit('end');
+    });
+
+    return immutableBookmarksObs;
+  }
+}
+
+/*
+ * Gets the namespace items for a name and adds it to the observable array
+ */
+function addNamespaceItem(name) {
+  return namespaceService.getNamespaceItem(name)
+    .then(function(item) {
+      bookmarksObs.push(item);
+    });
+}
+
+/*
+ * Whether a specific name is bookmarked or not
+ * @return {Promise.<boolean>} Promise indicating a name is bookmarked
+ */
+function isBookmarked(name) {
+  return loadKeys().then(function(keys) {
+    return (keys && keys.indexOf(name) >= 0);
+  });
+}
+
+/*
+ * Bookmarks/Unbookmarks a name.
+ * @return {Promise.<void>} Promise indicating whether operation succeeded.
+ */
+function bookmark(name, isBookmarked) {
+  if (isBookmarked) {
+    // new bookmark, add it to the state
+    addNamespaceItem(name);
+  } else {
+    // remove bookmark
+    arraySet.set(bookmarksObs, null, false, indexOf.bind(null, name));
+  }
+
+  // update store
+  return loadKeys().then(function(keys) {
+    keys = keys || []; // Initialize the bookmarks, if none were loaded.
+    arraySet.set(keys, name, isBookmarked);
+    return store.setValue(USER_BOOKMARKS_KEY, keys);
+  });
+}
+
+/*
+ * Check the browseState for the index of the given item. -1 if not present.
+ * Note: browseState should be observed.
+ */
+function indexOf(name) {
+  return _.findIndex(bookmarksObs(), function(bookmark) {
+    // Since bookmarks can be assigned out of order, check for undefined.
+    return bookmark !== undefined && name === bookmark.objectName;
+  });
+}
+
+/*
+ * Loads all the bookmarked names from the store
+ */
+function loadKeys() {
+  return store.getValue(USER_BOOKMARKS_KEY).then(function(keys) {
+    keys = keys || [];
+    return keys.filter(function(key, index, self) {
+      // only return unique and existing values
+      return key !== null &&
+        key !== undefined &&
+        self.indexOf(key) === index;
+    });
+  });
+}
\ No newline at end of file
diff --git a/src/services/recommendations/service.js b/src/services/recommendations/service.js
new file mode 100644
index 0000000..7def416
--- /dev/null
+++ b/src/services/recommendations/service.js
@@ -0,0 +1,80 @@
+var mercury = require('mercury');
+var EventEmitter = require('events').EventEmitter;
+
+var freeze = require('../../lib/mercury/freeze');
+
+var namespaceService = require('../namespace/service');
+var smartService = require('../smart/service');
+
+var log = require('../../lib/log')('services:recommendations:service');
+
+var LEARNER_KEY = 'learner-shortcut';
+var MAX_NUM_RECOMMENDATIONS = 10;
+
+module.exports = {
+  getAll: getAll
+};
+
+// Singleton state for all the bookmarks.
+var recommendationsObs = mercury.array([]);
+
+smartService.loadOrCreate(
+  LEARNER_KEY,
+  smartService.constants.LEARNER_SHORTCUT, {
+    k: MAX_NUM_RECOMMENDATIONS
+  }
+).catch(function(err) {
+  log.error(err);
+});
+
+/*
+ * Gets all the namespace items that are recommended based on our learning agent
+ * As new recommendations become available/removed the observable array will
+ * change to reflect the changes.
+ *
+ * The observable result has an events property which is an EventEmitter
+ * and emits 'end', 'itemError' events.
+ *
+ * @return {Promise.<mercury.array>} Promise of an observable array
+ * of recommended items
+ */
+function getAll() {
+
+  // Empty out the array
+  recommendationsObs.splice(0, recommendationsObs.getLength());
+  var immutableRecommendationsObs = freeze(recommendationsObs);
+  immutableRecommendationsObs.events = new EventEmitter();
+
+  return smartService.predict(LEARNER_KEY).then(getRecommendationItems);
+
+  function getRecommendationItems(recs) {
+    var allItems = recs.map(function(rec) {
+      var name = rec.item;
+      return addNamespaceItem(name).catch(function(err) {
+        immutableRecommendationsObs.events.emit('itemError', {
+          name: name,
+          error: err
+        });
+        log.error('Failed to create item for "' + name + '"', err);
+      });
+    });
+
+    Promise.all(allItems).then(function() {
+      immutableRecommendationsObs.events.emit('end');
+    }).catch(function() {
+      immutableRecommendationsObs.events.emit('end');
+    });
+
+    return immutableRecommendationsObs;
+  }
+}
+
+/*
+ * Gets the namespace items for a name and adds it to the observable array
+ */
+function addNamespaceItem(name) {
+  return namespaceService.getNamespaceItem(name)
+    .then(function(item) {
+      recommendationsObs.push(item);
+    });
+}
\ No newline at end of file
diff --git a/src/services/smart/service.js b/src/services/smart/service.js
index 9292e1e..e5ea83d 100644
--- a/src/services/smart/service.js
+++ b/src/services/smart/service.js
@@ -8,6 +8,7 @@
 var store = require('../../lib/store');
 var log = require('../../lib/log')('services:smart-service');
 var constants = require('./service-implementation');
+var extend = require('extend');
 
 // Export methods and constants
 module.exports = {
@@ -39,6 +40,7 @@
   // Store the learner right away. Calls to get should use this promise.
   learners[id] = load(id).then(function loadSuccess(learner) {
     log.debug('loaded learner', id);
+    learner.params = extend(learner.params, params);
     return Promise.resolve(learner);
   }, function loadFailure() {
     return create(id, type, params).then(function(learner) {
diff --git a/test/unit/components/browse/browse-namespace.js b/test/unit/components/browse/browse-namespace.js
index ecd27f9..1e8cf84 100644
--- a/test/unit/components/browse/browse-namespace.js
+++ b/test/unit/components/browse/browse-namespace.js
@@ -24,14 +24,7 @@
     return Promise.resolve(mercury.array([mockItem]));
   }
 };
-var namespaceServiceMockWithFailure = {
-  isGlobbable: function(name) {
-    return Promise.resolve(true);
-  },
-  search: function(name, globQuery) {
-    return Promise.reject();
-  }
-};
+
 function itemDetailsComponentMock() {
   return {
     state: {},
@@ -49,11 +42,6 @@
   '../../services/namespace/service': namespaceServiceMock
 });
 
-var browseNamespaceWithFailure =
-proxyquire('../../../../src/components/browse/browse-namespace',{
-  '../../services/namespace/service': namespaceServiceMockWithFailure
-});
-
 test('Updates state.namespace', function(t) {
   t.plan(4);
 
@@ -66,15 +54,15 @@
   });
   t.equal(state.namespace(), 'foo/bar');
 
-  // Should not update state.namespace if data.namespace is null
+  // Should reset state.namespace if data.namespace is null
   browseNamespace(state, events, {
     namespace: null
   });
-  t.equal(state.namespace(), 'foo/bar');
+  t.equal(state.namespace(), '');
 
-  // Should not update state.namespace if data.namespace is undefined
+  // Should reset state.namespace if data.namespace is undefined
   browseNamespace(state, events, {});
-  t.equal(state.namespace(), 'foo/bar');
+  t.equal(state.namespace(), '');
 
   // Should update state.namespace if data.namespace is empty string
   browseNamespace(state, events, {
@@ -95,15 +83,15 @@
   });
   t.equal(state.globQuery(), '**/*');
 
-  // Should not update state.globQuery if data.globQuery is null
+  // Should reset state.globQuery if data.globQuery is null
   browseNamespace(state, events, {
     globQuery: null
   });
-  t.equal(state.globQuery(), '**/*');
+  t.equal(state.globQuery(), '');
 
-  // Should not update state.globQuery if data.globQuery is undefined
+  // Should reset state.globQuery if data.globQuery is undefined
   browseNamespace(state, events, {});
-  t.equal(state.globQuery(), '**/*');
+  t.equal(state.globQuery(), '');
 
   // Empty glob keeps it empty in the state but behind the scenes does a '*'
   browseNamespace(state, events, {
@@ -111,46 +99,3 @@
   });
   t.equal(state.globQuery(), '');
 });
-
-test('Updates state.items', function(t) {
-  t.plan(1);
-
-  var state = browseComponent().state;
-  var events = browseComponent().events;
-
-  // Should update the items to items returned by glob method call (async)
-  browseNamespace(state, events, {
-    globQuery: '*',
-    namespace: 'foo/bar'
-  });
-
-  // Wait until next tick and assert the expected state change
-  process.nextTick( function() {
-    var items = state.items();
-    t.deepEqual(items, [mockItem]);
-  });
-
-});
-
-test('Updates state.items to empty on failure', function(t) {
-  t.plan(1);
-
-  var state = browseComponent().state;
-  var events = browseComponent().events;
-
-  // Give initial non-empty value
-  state.items.push([mockItem]);
-
-  //Should reset the items to empty on failed glob method call (async)
-  browseNamespaceWithFailure(state, events, {
-    globQuery: '*',
-    namespace: 'foo/bar'
-  });
-
-  // Wait until next tick and assert the expected state change
-  process.nextTick( function() {
-    var items = state.items();
-    t.deepEqual(items, []);
-  });
-
-});