Ver código fonte

UI improvements

Thibaut Courouble 8 anos atrás
pai
commit
0f238609da
39 arquivos alterados com 303 adições e 342 exclusões
  1. BIN
      assets/images/icons.png
  2. BIN
      assets/images/icons@2x.png
  3. 6 6
      assets/javascripts/templates/pages/about_tmpl.coffee
  4. 5 3
      assets/javascripts/templates/pages/help_tmpl.coffee
  5. 1 1
      assets/javascripts/templates/pages/news_tmpl.coffee.erb
  6. 1 1
      assets/javascripts/templates/pages/offline_tmpl.coffee
  7. 0 9
      assets/javascripts/templates/pages/root_tmpl.coffee.erb
  8. 0 1
      assets/javascripts/views/content/root_page.coffee
  9. 11 37
      assets/javascripts/views/layout/document.coffee
  10. 24 0
      assets/javascripts/views/layout/menu.coffee
  11. 17 18
      assets/javascripts/views/layout/mobile.coffee
  12. 0 26
      assets/javascripts/views/layout/nav.coffee
  13. 1 1
      assets/javascripts/views/layout/resizer.coffee
  14. 6 6
      assets/javascripts/views/list/list_focus.coffee
  15. 0 1
      assets/javascripts/views/misc/notice.coffee
  16. 1 1
      assets/javascripts/views/search/search.coffee
  17. 1 1
      assets/javascripts/views/sidebar/doc_list.coffee
  18. 2 2
      assets/javascripts/views/sidebar/results.coffee
  19. 41 14
      assets/javascripts/views/sidebar/sidebar.coffee
  20. 7 0
      assets/javascripts/views/view.coffee
  21. 2 3
      assets/stylesheets/components/_app.scss
  22. 29 14
      assets/stylesheets/components/_content.scss
  23. 104 51
      assets/stylesheets/components/_header.scss
  24. 14 108
      assets/stylesheets/components/_mobile.scss
  25. 1 11
      assets/stylesheets/components/_notice.scss
  26. 1 1
      assets/stylesheets/components/_sidebar.scss
  27. 0 3
      assets/stylesheets/global/_icons.scss
  28. 2 2
      assets/stylesheets/global/_variables-dark.scss
  29. 4 4
      assets/stylesheets/global/_variables.scss
  30. 1 1
      lib/app.rb
  31. BIN
      public/icons/ui/back/16.png
  32. BIN
      public/icons/ui/back/16@2x.png
  33. BIN
      public/icons/ui/home/16.png
  34. BIN
      public/icons/ui/home/16@2x.png
  35. 0 1
      public/icons/ui/home/SOURCE
  36. BIN
      public/icons/ui/menu/16.png
  37. BIN
      public/icons/ui/menu/16@2x.png
  38. 2 2
      test/app_test.rb
  39. 19 13
      views/app.erb

BIN
assets/images/icons.png


BIN
assets/images/icons@2x.png


+ 6 - 6
assets/javascripts/templates/pages/about_tmpl.coffee

@@ -10,7 +10,7 @@ app.templates.aboutPage = -> """
     </ul>
   </nav>
 
-  <h1 class="_lined-heading">API Documentation Browser</h1>
+  <h1 class="_lined-heading">DevDocs: API Documentation Browser</h1>
   <p>DevDocs combines multiple API documentations in a fast, organized, and searchable interface.
   <ul>
     <li>Created and maintained by <a href="http://thibaut.me">Thibaut Courouble</a>
@@ -26,7 +26,7 @@ app.templates.aboutPage = -> """
   <p class="_note _note-green">If you like DevDocs, please consider supporting my work on
     <a href="https://gratipay.com/devdocs/">Gratipay</a>. Thanks!<br>
 
-  <h2 class="_lined-heading" id="credits">Credits</h2>
+  <h2 class="_block-heading" id="credits">Credits</h2>
 
   <p><strong>Special thanks to:</strong>
   <ul>
@@ -45,7 +45,7 @@ app.templates.aboutPage = -> """
     #{("<tr><td>#{c[0]}<td>&copy; #{c[1]}<td><a href=\"#{c[3]}\">#{c[2]}</a>" for c in credits).join('')}
   </table>
 
-  <h2 class="_lined-heading" id="faq">Questions & Answers</h2>
+  <h2 class="_block-heading" id="faq">Questions & Answers</h2>
   <dl>
     <dt>Where can I suggest new docs and features?
     <dd>You can suggest and vote for new docs on the <a href="https://trello.com/b/6BmTulfx/devdocs-documentation">Trello board</a>.<br>
@@ -56,7 +56,7 @@ app.templates.aboutPage = -> """
   </dl>
   <p>For anything else, feel free to email me at <a href="mailto:thibaut@devdocs.io">thibaut@devdocs.io</a>.
 
-  <h2 class="_lined-heading" id="copyright">Copyright and License</h2>
+  <h2 class="_block-heading" id="copyright">Copyright and License</h2>
   <p class="_note">
     <strong>Copyright 2013&ndash;2017 Thibaut Courouble and <a href="https://github.com/Thibaut/devdocs/graphs/contributors">other contributors</a></strong><br>
     This software is licensed under the terms of the Mozilla Public License v2.0.<br>
@@ -64,7 +64,7 @@ app.templates.aboutPage = -> """
     For more information, see the <a href="https://github.com/Thibaut/devdocs/blob/master/COPYRIGHT">COPYRIGHT</a>
     and <a href="https://github.com/Thibaut/devdocs/blob/master/LICENSE">LICENSE</a> files.
 
-  <h2 class="_lined-heading" id="plugins">Plugins and Extensions</h2>
+  <h2 class="_block-heading" id="plugins">Plugins and Extensions</h2>
   <ul>
     <li><a href="https://chrome.google.com/webstore/detail/devdocs/mnfehgbmkapmjnhcnbodoamcioleeooe">Chrome web app</a>
     <li><a href="https://sublime.wbond.net/packages/DevDocs">Sublime Text plugin</a>
@@ -73,7 +73,7 @@ app.templates.aboutPage = -> """
     <li><a href="https://github.com/xuchunyang/DevDocs.el">Emacs Package</a>
   </ul>
 
-  <h2 class="_lined-heading" id="privacy">Privacy Policy</h2>
+  <h2 class="_block-heading" id="privacy">Privacy Policy</h2>
   <ul>
     <li><a href="http://devdocs.io">devdocs.io</a> ("App") is operated by Thibaut Courouble ("We").
     <li>We do not collect personal information.

+ 5 - 3
assets/javascripts/templates/pages/help_tmpl.coffee

@@ -11,7 +11,9 @@ app.templates.helpPage = """
     </ul>
   </nav>
 
-  <h2 class="_lined-heading" id="search">Search</h2>
+  <h1 class="_lined-heading" id="search">Help</h2>
+
+  <h2 class="_block-heading" id="search">Search</h2>
   <p>
     The search is case-insensitive and supports fuzzy matching (for queries longer than two characters).
     For example, searching <code class="_label">bgcp</code> brings up <code class="_label">background-clip</code>.<br>
@@ -42,7 +44,7 @@ app.templates.helpPage = """
             <a href="https://support.mozilla.org/en-US/kb/how-search-from-address-bar">these instructions</a>.
   </dl>
 
-  <h2 class="_lined-heading" id="shortcuts">Keyboard Shortcuts</h2>
+  <h2 class="_block-heading" id="shortcuts">Keyboard Shortcuts</h2>
   <h3 class="_shortcuts-title">Selection</h3>
   <dl class="_shortcuts-dl">
     <dt class="_shortcuts-dt">
@@ -110,7 +112,7 @@ app.templates.helpPage = """
     <strong>Tip:</strong> If the cursor is no longer in the search field, press <code class="_label">/</code> or
     continue to type and it will refocus the search field and start showing new results.
 
-  <h2 class="_lined-heading" id="abbreviations">Abbreviations</h2>
+  <h2 class="_block-heading" id="abbreviations">Abbreviations</h2>
   <p>Feel free to suggest new abbreviations on <a href="https://github.com/Thibaut/devdocs/issues/new">GitHub</a>.
   <table class="_abbreviations">
     <tr>

+ 1 - 1
assets/javascripts/templates/pages/news_tmpl.coffee.erb

@@ -15,7 +15,7 @@ app.templates.newsList = (news, options = {}) ->
     date = new Date(value[0])
     if options.years isnt false and year isnt date.getUTCFullYear()
       year = date.getUTCFullYear()
-      result += "<h4>#{year}</h4>"
+      result += """<h2 class="_block-heading">#{year}</h2>"""
     result += newsItem(date, value[1..])
 
   result

+ 1 - 1
assets/javascripts/templates/pages/offline_tmpl.coffee

@@ -21,7 +21,7 @@ app.templates.offlinePage = (docs) -> """
     #{docs}
   </table>
   <p class="_note"><strong>Note:</strong> your browser may delete DevDocs's offline data if your computer is running low on disk space and you haven't used the app in a while. Load this page before going offline to make sure the data is still there.
-  <h1 class="_lined-heading">Questions & Answers</h1>
+  <h2 class="_block-heading">Questions & Answers</h2>
   <dl>
     <dt>How does this work?
     <dd>Each page is cached as a key-value pair in <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a> (downloaded from a single file).<br>

+ 0 - 9
assets/javascripts/templates/pages/root_tmpl.coffee.erb

@@ -48,15 +48,6 @@ app.templates.intro = """
 """
 <% end %>
 
-app.templates.mobileNav = """
-  <nav class="_mobile-nav">
-    <a href="/offline" class="_mobile-nav-link">Offline</a>
-    <a href="/about" class="_mobile-nav-link">About</a>
-    <a href="/news" class="_mobile-nav-link">News</a>
-    <a href="/help" class="_mobile-nav-link">Help</a>
-  </nav>
-"""
-
 app.templates.mobileIntro = """
   <div class="_mobile-intro">
     <h2 class="_intro-title">Welcome!</h2>

+ 0 - 1
assets/javascripts/views/content/root_page.coffee

@@ -9,7 +9,6 @@ class app.views.RootPage extends app.View
 
   render: ->
     @empty()
-    @append @tmpl('mobileNav') if app.isMobile()
     if app.isAndroidWebview()
       @append @tmpl('androidWarning')
     else

+ 11 - 37
assets/javascripts/views/layout/document.coffee

@@ -1,6 +1,6 @@
 class app.views.Document extends app.View
-  MAX_WIDTH_CLASS = '_max-width'
-  HIDE_SIDEBAR_CLASS = '_sidebar-hidden'
+  MAX_WIDTH_LAYOUT = '_max-width'
+  SIDEBAR_HIDDEN_LAYOUT = '_sidebar-hidden'
 
   @el: document
 
@@ -14,16 +14,12 @@ class app.views.Document extends app.View
     superRight: 'onForward'
 
   init: ->
-    @addSubview @nav     = new app.views.Nav,
+    @addSubview @menu    = new app.views.Menu,
     @addSubview @sidebar = new app.views.Sidebar
     @addSubview @resizer = new app.views.Resizer if app.views.Resizer.isSupported()
     @addSubview @content = new app.views.Content
     @addSubview @path    = new app.views.Path unless app.isSingleDoc() or app.isMobile()
 
-    @sidebar.search
-      .on 'searching', @onSearching
-      .on 'clear', @onSearchClear
-
     $.on document.body, 'click', @onClick
 
     @activate()
@@ -39,39 +35,17 @@ class app.views.Document extends app.View
     return
 
   toggleLayout: ->
-    wantsMaxWidth = !app.el.classList.contains(MAX_WIDTH_CLASS)
-    app.el.classList[if wantsMaxWidth then 'add' else 'remove'](MAX_WIDTH_CLASS)
-    app.settings.setLayout(MAX_WIDTH_CLASS, wantsMaxWidth)
+    wantsMaxWidth = !app.el.classList.contains(MAX_WIDTH_LAYOUT)
+    app.el.classList[if wantsMaxWidth then 'add' else 'remove'](MAX_WIDTH_LAYOUT)
+    app.settings.setLayout(MAX_WIDTH_LAYOUT, wantsMaxWidth)
     app.appCache?.updateInBackground()
     return
 
-  showSidebar: (options = {}) ->
-    @toggleSidebar(options, true)
-    return
-
-  hideSidebar: (options = {}) ->
-    @toggleSidebar(options, false)
-    return
-
-  toggleSidebar: (options = {}, shouldShow) ->
-    shouldShow ?= if options.save then !@hasSidebar() else app.el.classList.contains(HIDE_SIDEBAR_CLASS)
-    app.el.classList[if shouldShow then 'remove' else 'add'](HIDE_SIDEBAR_CLASS)
-    if options.save
-      app.settings.setLayout(HIDE_SIDEBAR_CLASS, !shouldShow)
-      app.appCache?.updateInBackground()
-    return
-
-  hasSidebar: ->
-    !app.settings.hasLayout(HIDE_SIDEBAR_CLASS)
-
-  onSearching: =>
-    unless @hasSidebar()
-      @showSidebar()
-    return
-
-  onSearchClear: =>
-    unless @hasSidebar()
-      @hideSidebar()
+  toggleSidebarLayout: ->
+    shouldHide = !app.settings.hasLayout(SIDEBAR_HIDDEN_LAYOUT)
+    app.el.classList[if shouldHide then 'add' else 'remove'](SIDEBAR_HIDDEN_LAYOUT)
+    app.settings.setLayout(SIDEBAR_HIDDEN_LAYOUT, shouldHide)
+    app.appCache?.updateInBackground()
     return
 
   setTitle: (title) ->

+ 24 - 0
assets/javascripts/views/layout/menu.coffee

@@ -0,0 +1,24 @@
+class app.views.Menu extends app.View
+  @el: '._menu'
+  @activeClass: 'active'
+
+  @events:
+    click: 'onClick'
+
+  init: ->
+    $.on document.body, 'click', @onGlobalClick
+    return
+
+  onClick: =>
+    prev = @el.previousElementSibling
+    $.remove @el
+    $.requestAnimationFrame => $.after(prev, @el)
+    return
+
+  onGlobalClick: (event) =>
+    return if event.which isnt 1
+    if event.target.hasAttribute?('data-toggle-menu')
+      @toggleClass @constructor.activeClass
+    else if @hasClass @constructor.activeClass
+      @removeClass @constructor.activeClass
+    return

+ 17 - 18
assets/javascripts/views/layout/mobile.coffee

@@ -35,44 +35,48 @@ class app.views.Mobile extends app.View
     FastClick.attach @body
 
     $.on @body, 'click', @onClick
-    $.on $('._home-btn'), 'click', @onClickHome
-    $.on $('._menu-btn'), 'click', @onClickMenu
     $.on $('._search'), 'touchend', @onTapSearch
 
-    @back = $('._back-btn')
+    @toggleSidebar = $('button[data-toggle-sidebar]')
+    @toggleSidebar.removeAttribute('hidden')
+    $.on @toggleSidebar, 'click', @onClickToggleSidebar
+
+    @back = $('button[data-back]')
+    @back.removeAttribute('hidden')
     $.on @back, 'click', @onClickBack
 
-    @forward = $('._forward-btn')
+    @forward = $('button[data-forward]')
+    @forward.removeAttribute('hidden')
     $.on @forward, 'click', @onClickForward
 
     app.document.sidebar.search
       .on 'searching', @showSidebar
-      .on 'clear', @hideSidebar
 
     @activate()
     return
 
   showSidebar: =>
     if @isSidebarShown()
-      @body.scrollTop = 0
+      window.scrollTo 0, 0
       return
 
-    @contentTop = @body.scrollTop
+    @contentTop = window.scrollY
     @content.style.display = 'none'
     @sidebar.style.display = 'block'
 
     if selection = @findByClass app.views.ListSelect.activeClass
-      $.scrollTo selection, @body, 'center'
+      scrollContainer = if window.scrollY is @body.scrollTop then @body else document.documentElement
+      $.scrollTo selection, scrollContainer, 'center'
     else
-      @body.scrollTop = @findByClass(app.views.ListFold.activeClass) and @sidebarTop or 0
+      window.scrollTo 0, @findByClass(app.views.ListFold.activeClass) and @sidebarTop or 0
     return
 
   hideSidebar: =>
     return unless @isSidebarShown()
-    @sidebarTop = @body.scrollTop
+    @sidebarTop = window.scrollY
     @sidebar.style.display = 'none'
     @content.style.display = 'block'
-    @body.scrollTop = @contentTop or 0
+    window.scrollTo 0, @contentTop or 0
     return
 
   isSidebarShown: ->
@@ -89,17 +93,12 @@ class app.views.Mobile extends app.View
   onClickForward: =>
     history.forward()
 
-  onClickHome: =>
-    app.shortcuts.trigger 'escape'
-    @hideSidebar()
-    return
-
-  onClickMenu: =>
+  onClickToggleSidebar: =>
     if @isSidebarShown() then @hideSidebar() else @showSidebar()
     return
 
   onTapSearch: =>
-    @body.scrollTop = 0
+    window.scrollTo 0, 0
 
   afterRoute: =>
     @hideSidebar()

+ 0 - 26
assets/javascripts/views/layout/nav.coffee

@@ -1,26 +0,0 @@
-class app.views.Nav extends app.View
-  @el: '._nav'
-  @activeClass: '_nav-current'
-
-  @routes:
-    after: 'afterRoute'
-
-  select: (href) ->
-    @deselect()
-    if @current = @find "a[href='#{href}']"
-      @current.classList.add @constructor.activeClass
-      @current.setAttribute 'tabindex', '-1'
-    return
-
-  deselect: ->
-    if @current
-      @current.classList.remove @constructor.activeClass
-      @current.removeAttribute 'tabindex'
-      @current = null
-    return
-
-  afterRoute: (route, context) =>
-    if route in ['page', 'offline']
-      @select context.pathname
-    else
-      @deselect()

+ 1 - 1
assets/javascripts/views/layout/resizer.coffee

@@ -35,7 +35,7 @@ class app.views.Resizer extends app.View
     return
 
   onClick: ->
-    app.document.toggleSidebar(save: true)
+    app.document.toggleSidebarLayout()
     return
 
   onDragStart: (event) =>

+ 6 - 6
assets/javascripts/views/list/list_focus.coffee

@@ -14,7 +14,7 @@ class app.views.ListFocus extends app.View
 
   constructor: (@el) ->
     super
-    @focus = $.framify(@focus, @)
+    @focusOnNextFrame = $.framify(@focus, @)
 
   focus: (el) ->
     if el and not el.classList.contains @constructor.activeClass
@@ -87,22 +87,22 @@ class app.views.ListFocus extends app.View
 
   onDown: =>
     if cursor = @getCursor()
-      @focus @findNext(cursor)
+      @focusOnNextFrame @findNext(cursor)
     else
-      @focus @findByTag('a')
+      @focusOnNextFrame @findByTag('a')
     return
 
   onUp: =>
     if cursor = @getCursor()
-      @focus @findPrev(cursor)
+      @focusOnNextFrame @findPrev(cursor)
     else
-      @focus @findLastByTag('a')
+      @focusOnNextFrame @findLastByTag('a')
     return
 
   onLeft: =>
     cursor = @getCursor()
     if cursor and not cursor.classList.contains(app.views.ListFold.activeClass) and cursor.parentElement isnt @el
-      @focus cursor.parentElement.previousSibling
+      @focusOnNextFrame cursor.parentElement.previousSibling
     return
 
   onEnter: =>

+ 0 - 1
assets/javascripts/views/misc/notice.coffee

@@ -18,7 +18,6 @@ class app.views.Notice extends app.View
     return
 
   show: ->
-    @addClass '_top' if @type is 'disabledDoc'
     @html @tmpl("#{@type}Notice", @args...)
     @prependTo $('._app')
     return

+ 1 - 1
assets/javascripts/views/search/search.coffee

@@ -130,7 +130,7 @@ class app.views.Search extends app.View
     if event.target is @resetLink
       $.stopEvent(event)
       @reset()
-      @focus()
+      app.document.onEscape()
     return
 
   onSubmit: (event) ->

+ 1 - 1
assets/javascripts/views/sidebar/doc_list.coffee

@@ -148,7 +148,7 @@ class app.views.DocList extends app.View
     return
 
   scrollTo: (model) ->
-    $.scrollTo @find("a[href='#{model.fullPath()}']"), null, 'top', margin: 0
+    $.scrollTo @find("a[href='#{model.fullPath()}']"), null, 'top', margin: if app.isMobile() then 48 else 0
     return
 
   toggleDisabled: ->

+ 2 - 2
assets/javascripts/views/sidebar/results.coffee

@@ -15,8 +15,8 @@ class app.views.Results extends app.View
     return
 
   init: ->
-    @addSubview @listSelect = new app.views.ListSelect @el
     @addSubview @listFocus  = new app.views.ListFocus @el unless app.isMobile()
+    @addSubview @listSelect = new app.views.ListSelect @el
 
     @search
       .on 'results', @onResults
@@ -42,7 +42,7 @@ class app.views.Results extends app.View
     return
 
   focusFirst: ->
-    @listFocus?.focus @el.firstElementChild
+    @listFocus?.focusOnNextFrame @el.firstElementChild
     return
 
   openFirst: ->

+ 41 - 14
assets/javascripts/views/sidebar/sidebar.coffee

@@ -3,6 +3,7 @@ class app.views.Sidebar extends app.View
 
   @events:
     focus: 'onFocus'
+    select: 'onSelect'
     click: 'onClick'
 
   @shortcuts:
@@ -14,8 +15,8 @@ class app.views.Sidebar extends app.View
     @addSubview @search = new app.views.Search
 
     @search
-      .on 'searching', @showResults
-      .on 'clear', @showDocList
+      .on 'searching', @onSearching
+      .on 'clear', @onSearchClear
     .scope
       .on 'change', @onScopeChange
 
@@ -27,7 +28,15 @@ class app.views.Sidebar extends app.View
     $.on document, 'click', @onGlobalClick if @docPicker
     return
 
-  show: (view) ->
+  display: ->
+    @el.style.display = 'block'
+    return
+
+  resetDisplay: ->
+    @el.style.display = '' unless @el.style.display is 'none'
+    return
+
+  showView: (view) ->
     unless @view is view
       @hover?.hide()
       @saveScrollPosition()
@@ -44,28 +53,29 @@ class app.views.Sidebar extends app.View
     @append @tmpl('sidebarSettings') if @view is @docList and @docPicker
     return
 
-  showDocList: (reset) =>
-    @show @docList
-    if reset is true
-      @docList.reset(revealCurrent: true)
-      @search.reset()
+  showDocList: ->
+    @showView @docList
     return
 
   showDocPicker: =>
-    @show @docPicker
+    @showView @docPicker
     return
 
   showResults: =>
-    @show @results
+    @showView @results
+    return
+
+  reset: ->
+    @display()
+    @showDocList()
+    @docList.reset()
+    @search.reset()
     return
 
   onReady: =>
     @view = @docList
     @render()
     @view.activate()
-
-  reset: ->
-    @showDocList true
     return
 
   onScopeChange: (newDoc, previousDoc) =>
@@ -90,15 +100,30 @@ class app.views.Sidebar extends app.View
     @el.scrollTop = 0
     return
 
+  onSearching: =>
+    @display()
+    @showResults()
+    return
+
+  onSearchClear: =>
+    @resetDisplay()
+    @showDocList()
+    return
+
   onFocus: (event) =>
+    @display()
     $.scrollTo event.target, @el, 'continuous', bottomGap: 2 unless event.target is @el
     return
 
+  onSelect: =>
+    @resetDisplay()
+    return
+
   onClick: (event) =>
     return if event.which isnt 1
     if event.target.hasAttribute? 'data-reset-list'
       $.stopEvent(event)
-      @reset()
+      @onAltR()
     else if event.target.hasAttribute? 'data-light'
       $.stopEvent(event)
       document.activeElement?.blur()
@@ -120,6 +145,8 @@ class app.views.Sidebar extends app.View
 
   onAltR: =>
     @reset()
+    @docList.reset(revealCurrent: true)
+    @display()
     return
 
   onEscape: =>

+ 7 - 0
assets/javascripts/views/view.coffee

@@ -35,6 +35,13 @@ class app.View
     @el.classList.remove(name)
     return
 
+  toggleClass: (name) ->
+    @el.classList.toggle(name)
+    return
+
+  hasClass: (name) ->
+    @el.classList.contains(name)
+
   resetClass: ->
     @el.className = @originalClassName or ''
     if @constructor.className

+ 2 - 3
assets/stylesheets/components/_app.scss

@@ -2,7 +2,6 @@
   position: relative;
   z-index: 1;
   height: 100%;
-  padding-top: $headerHeight;
   overflow: hidden;
   background: $contentBackground;
   -webkit-transition: opacity .2s;
@@ -12,7 +11,7 @@
   ._booting > & { opacity: 0; }
   ._noscript > & { display: none; }
 
-  &._max-width {
+  &._max-width  {
     margin: 0 auto;
     max-width: $maxWidth;
     box-shadow: 1px 0 $headerBorder, -1px 0 $headerBorder;
@@ -31,7 +30,7 @@
     left: 0;
     right: 0;
     line-height: 1;
-    margin-top: -.75em;
+    margin-top: -.6em;
     font-size: 4rem;
     font-weight: 300;
     letter-spacing: -.125rem;

+ 29 - 14
assets/stylesheets/components/_content.scss

@@ -20,18 +20,24 @@
   height: 100%;
   overflow-y: scroll;
   margin-left: .875rem;
-  padding: 1.25rem 1.5rem 0;
+  padding: 1.125rem 1.5rem 0;
   font-size: .875rem;
   pointer-events: auto;
   -webkit-overflow-scrolling: touch;
   @extend %border-box;
 
-  -webkit-padding-start: .75rem;
-  -webkit-padding-end: 1rem;
+  -webkit-padding-start: .625rem;
+  -webkit-padding-end: .75rem;
 
-  @media (-moz-overlay-scrollbars) { padding-left: .75rem; }
+  @media (-moz-overlay-scrollbars) { padding-left: .625rem; }
   @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { margin-left: 0; }
 
+  ._sidebar-hidden &:before {
+    content: '';
+    display: block;
+    margin-top: $headerHeight;
+  }
+
   &:after { // padding bottom
     content: '';
     display: block;
@@ -77,15 +83,22 @@
 // Intro
 //
 
-._intro { text-align: center; }
+._intro {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  min-height: calc(100vh - 2.375rem);
+
+  ._sidebar-hidden & {
+    min-height: calc(100vh - 2.375rem - #{$headerHeight});
+  }
+}
 
 ._intro-message {
-  position: relative;
-  display: inline-block;
-  vertical-align: top;
   max-width: 37rem;
+  margin: .5rem 0;
   padding: 1rem 1.25rem;
-  text-align: left;
   @extend %note, %note-green;
 }
 
@@ -146,23 +159,25 @@
 
 ._lined-heading,
 %lined-heading {
-  white-space: nowrap;
-  overflow: hidden;
-  word-wrap: normal;
-  overflow-wrap: normal;
+  display: flex;
+  justify-content: center;
+  align-items: center;
 
   &:after {
     content: '';
     display: inline-block;
     vertical-align: middle;
-    width: 100%;
+    flex-grow: 1;
     height: 1px;
     line-height: 0;
+    margin-top: .25rem;
     margin-left: 1rem;
     background: $boxBorderLight;
   }
 }
 
+._block-heading { @extend %block-heading; }
+
 ._heading-links {
   float: right;
   font-weight: normal;

+ 104 - 51
assets/stylesheets/components/_header.scss

@@ -7,79 +7,131 @@
   z-index: $headerZ;
   top: 0;
   left: 0;
-  right: 0;
+  display: flex;
+  width: $sidebarWidth;
   height: $headerHeight;
-  line-height: $headerHeight;
   background: $headerBackground;
   border-bottom: 1px solid $headerBorder;
   @extend %user-select-none;
+
+  @media #{$mediumScreen} { width: $sidebarMediumWidth; }
 }
 
-//
-// Navigation menu
-//
+._header-left {
+  float: left;
+  height: 100%;
+}
 
-._nav {
+._header-right {
   float: right;
-  margin-right: .5rem;
-  font-size: .875rem;
-  color: $textColor;
+  height: 100%;
 }
 
-._nav-link,
-._nav-link:hover {
+._header-btn {
   position: relative;
-  float: left;
-  padding: 0 1.25rem;
-  color: inherit;
-  text-decoration: none;
+  width: 2.25rem;
+  height: 100%;
+  color: $textColorLight;
+  text-align: center;
 
-  @media #{$mediumScreen} { padding: 0 .75rem; }
-}
+  &[hidden] { display: none; }
 
-._nav-link {
-  &:before, &:after {
-    position: absolute;
-    left: 50%;
-    bottom: 0;
-    width: 0;
-    height: 0;
-    margin-left: -.375rem;
-    border: .375rem solid transparent;
-    border-bottom-color: darken($headerBorder, 2%);
+  &[disabled] {
+    opacity: .3;
+    cursor: not-allowed;
   }
 
-  &:after {
-    bottom: -1px;
-    border-bottom-color: $contentBackground;
+  > svg {
+    display: inline-block;
+    vertical-align: top;
+    width: 1.5rem;
+    height: 1.5rem;
+    fill: currentColor;
+    pointer-events: none;
   }
 }
 
-._nav-current {
-  outline: 0;
+//
+// Menu
+//
 
-  &:before, &:after { content: ''; }
+._menu-btn {
+  border-right: 1px solid $headerBorder;
 }
 
-//
-// Logo
-//
+._menu {
+  position: absolute;
+  z-index: 1;
+  top: .25rem;
+  right: .25rem;
+  width: 8rem;
+  height: calc(11.5rem + 1px);
+  font-size: .875rem;
+  background: $contentBackground;
+  border: 1px solid $headerBorder;
+  border-radius: 3px;
+  box-shadow: -1px 1px 1px rgba(black, .05);
+  transition: all 250ms cubic-bezier(0.23, 1, 0.32, 1);
+  opacity: 0;
+  -webkit-transform: scale(0, 0);
+          transform: scale(0, 0);
+  -webkit-transform-origin: 100% 0;
+          transform-origin: 100% 0;
+
+  &:hover,
+  ._menu-btn:hover + & {
+    transition-delay: 100ms;
+  }
 
-._logo {
-  position: relative;
-  float: left;
-  height: $headerHeight;
+  &:hover,
+  &.active,
+  ._menu-btn:hover + &,
+  ._menu-btn:focus + & {
+    opacity: 1;
+    -webkit-transform: scale(1, 1);
+            transform: scale(1, 1);
+  }
+
+  &:focus-within {
+    opacity: 1;
+    -webkit-transform: scale(1, 1);
+            transform: scale(1, 1);
+  }
+}
+
+._menu-title {
   margin: 0;
-  line-height: inherit;
-  font-size: inherit;
+  line-height: 1.5rem;
+  font-size: 1rem;
   font-weight: $boldFontWeight;
-  cursor: default;
+  letter-spacing: -.5px;
+  background: $sidebarBackground;
+  border-bottom: 1px solid $sidebarBorder;
+  border-radius: 2px 2px 0 0;
+}
+
+._menu-title-link,
+._menu-title-link:hover {
+  display: block;
+  padding: .5rem 1rem;
+  color: $focusText;
+  text-decoration: none;
+}
 
-  > ._nav-link {
-    float: none;
-    margin-left: .75rem;
-    padding: 0 .25rem;
+._menu-link {
+  display: block;
+  padding: 0 1rem;
+  line-height: 2.25rem;
+  color: inherit;
+  text-decoration: none;
+
+  &:hover {
+    color: $focusText;
+    text-decoration: none;
+    background: $sidebarBackground;
   }
+
+  &:last-child { border-radius: 0 0 2px 2px; }
 }
 
 //
@@ -87,20 +139,19 @@
 //
 
 ._search {
+  flex-grow: 1;
   position: relative;
-  float: left;
-  width: $sidebarWidth;
   height: 100%;
   padding: .5rem 0 .5rem .5rem;
   @extend %border-box;
 
-  @media #{$mediumScreen} { width: $sidebarMediumWidth; }
-
   &:before {
     position: absolute;
+    z-index: 1;
     top: 1rem;
     left: 1rem;
     opacity: .4;
+    pointer-events: none;
     @if $style == 'dark' {
       @extend %icon, %icon-search-white;
     } @else {
@@ -110,6 +161,7 @@
 }
 
 ._search-input {
+  position: relative;
   display: block;
   width: 100%;
   height: 100%;
@@ -164,6 +216,7 @@
 ._search-tag {
   display: none;
   position: absolute;
+  z-index: 2;
   top: .875rem;
   left: .875rem;
   padding: 0 .5rem;

+ 14 - 108
assets/stylesheets/components/_mobile.scss

@@ -10,18 +10,17 @@
 
   body { -ms-overflow-style: -ms-autohiding-scrollbar; }
 
-  ._app, ._container, ._content { overflow: visible; }
-
-  ._container {
-    margin: 0;
-    border: 0;
-  }
+  ._app, ._content { overflow: visible; }
+  ._app { padding-top: $headerHeight; }
+  ._container { margin: 0; }
 
   ._content {
     position: static;
     height: auto;
     margin: 0;
     padding: .75rem 1rem 2.5rem;
+
+    &:before { content: none; }
   }
 
   ._booting:before, ._content-loading:before { font-size: 3rem; }
@@ -33,16 +32,13 @@
     max-width: 100vw;
   }
 
-  ._logo, ._nav { display: none; }
-  ._mobile-btn { display: block; }
+  ._header-btn { width: 2.5rem; }
+  ._header-btn[hidden] { display: block; }
+  ._menu-btn { border-right: 0; }
 
   ._search {
-    float: none;
-    width: auto;
-    overflow: hidden;
-    padding-left: 2px;
-    padding-right: 2px;
-    border-right: 0;
+    padding-right: .125rem;
+    padding-left: .125rem;
 
     &:before { left: .5rem; }
   }
@@ -57,7 +53,7 @@
     overflow: visible;
   }
 
-  ._list, ._sidebar-footer { width: 100%; }
+  ._header, ._list, ._sidebar-footer { width: 100%; }
 
   ._list-item {
     white-space: normal;
@@ -85,33 +81,12 @@
     box-shadow: 0 1px $noteGreenBorder, 0 -1px $noteGreenBorder;
   }
 
-  // Splash
-
-  ._splash-sponsors { margin-top: 1rem; }
-
-  ._splash-sponsor {
-    position: static;
-
-    ._logo-info {
-      left: 1rem;
-      right: 1rem;
-      width: auto;
-      max-width: none;
-      margin: 0;
-    }
-  }
-
   // Notice
 
   ._notice {
     position: fixed;
     left: 0;
     padding: 0 .5rem;
-
-    ~ ._sidebar {
-      margin-top: 2.5rem;
-      padding-bottom: 4rem;
-    }
   }
 
   ._notice-text { font-size: .75em; }
@@ -143,76 +118,10 @@
 // Header buttons
 //
 
-._mobile-btn {
-  display: none;
-  position: relative;
-  float: left;
-  width: 2.5rem;
-  height: 100%;
-  @extend %hide-text;
-
-  &[disabled] {
-    opacity: .3;
-    cursor: not-allowed;
-  }
-
-  &:before {
-    position: absolute;
-    top: 50%;
-    left: 50%;
-    margin: -.5rem 0 0 -.5rem;
-    @extend %icon;
-  }
-}
-
-._back-btn {
-  &:before { @extend %icon-back; }
-}
-
 ._forward-btn {
-  width: 2.25rem;
-  -webkit-transform: rotate(180deg);
-          transform: rotate(180deg);
-
-  &:before {
-    margin-left: -.375rem;
-    @extend %icon-back;
-  }
-}
-
-._home-btn {
-  float: right;
-  width: 2rem;
-
-  &:before {
-    margin-left: -.375rem;
-    @extend %icon-home;
-  }
-}
+  margin-right: -.5rem;
 
-._menu-btn {
-  float: right;
-
-  &:before { @extend %icon-menu; }
-}
-
-//
-// Navigation menu
-//
-
-._mobile-nav {
-  margin: .25rem 0 1.25rem;
-  padding: 0;
-  line-height: 2.8;
-  overflow: hidden;
-  @extend %box;
-}
-
-._mobile-nav-link {
-  float: left;
-  width: 25%;
-  text-align: center;
-  font-weight: $boldFontWeight;
+  > svg { margin-left: -.375rem; }
 }
 
 //
@@ -222,14 +131,11 @@
 ._mobile-intro {
   > ._intro-list { padding-left: 1.5rem; }
 
-  > ._intro-hide,
-  > ._intro-sponsors {
+  ._intro-hide {
     position: static;
     float: none;
     display: block;
     margin-top: .75rem;
     text-align: center;
   }
-
-  ._intro-sponsor { margin: .5em .75em; }
 }

+ 1 - 11
assets/stylesheets/components/_notice.scss

@@ -12,17 +12,7 @@
   @media #{$mediumScreen} { left: $sidebarMediumWidth; }
 
   ._sidebar-hidden & { left: $sidebarHiddenWidth; }
-
-  &:not(._top) ~ ._container { padding-bottom: 2.5rem; }
-
-  &._top {
-    bottom: auto;
-    top: $headerHeight;
-    margin-top: 1px;
-    box-shadow: inset 0 -1px $noticeBorder;
-
-    ~ ._container { padding-top: 2.5rem; }
-  }
+  ~ ._container { padding-bottom: 2.5rem; }
 }
 
 ._notice-text {

+ 1 - 1
assets/stylesheets/components/_sidebar.scss

@@ -40,7 +40,7 @@
 
 ._resizer {
   position: absolute;
-  z-index: $sidebarZ + 1;
+  z-index: $sidebarZ;
   top: $headerHeight;
   bottom: 0;
   left: $sidebarWidth;

+ 0 - 3
assets/stylesheets/global/_icons.scss

@@ -37,8 +37,6 @@
 %icon-clear                 { background-position: -3rem 0; }
 %icon-settings              { background-position: 0 -1rem; }
 %icon-check                 { background-position: -1rem -1rem; }
-%icon-menu                  { background-position: -2rem -1rem; @extend %darkIconFix !optional; }
-%icon-home                  { background-position: -3rem -1rem; @extend %darkIconFix !optional; }
 %icon-path                  { background-position: 0 -2rem; }
 %icon-search-white          { background-position: -1rem -2rem; }
 %icon-dir-white             { background-position: -2rem -2rem; }
@@ -54,7 +52,6 @@
 %icon-clipboard             { background-position: 0 -5rem; }
 %icon-clipboard-white       { background-position: -1rem -5rem; }
 %icon-close-white           { background-position: -2rem -5rem; }
-%icon-back                  { background-position: -3rem -5rem; @extend %darkIconFix !optional; }
 
 ._icon-codeceptjs:before    { background-position: -3rem 0; }
 ._icon-codeception:before   { background-position: -4rem 0; }

+ 2 - 2
assets/stylesheets/global/_variables-dark.scss

@@ -7,7 +7,7 @@ $style: 'dark';
 
 $maxWidth: 80rem;
 $headerHeight: 3rem;
-$sidebarWidth: 18rem;
+$sidebarWidth: 20rem;
 $sidebarMediumWidth: 16rem;
 $sidebarHiddenWidth: 9px;
 
@@ -37,7 +37,7 @@ $linkColor: $textColor;
 $linkColorHover: white;
 $linkTextDecoration: underline;
 
-$headerBackground: #1e1e1e;
+$headerBackground: #1c1c1c;
 $headerBorder: #000;
 
 $sidebarBackground: #24282a;

+ 4 - 4
assets/stylesheets/global/_variables.scss

@@ -7,7 +7,7 @@ $style: 'light';
 
 $maxWidth: 80rem;
 $headerHeight: 3rem;
-$sidebarWidth: 18rem;
+$sidebarWidth: 20rem;
 $sidebarMediumWidth: 16rem;
 $sidebarHiddenWidth: 9px;
 
@@ -37,11 +37,11 @@ $linkColor: #3377c0;
 $linkColorHover: #2f6cb6;
 $linkTextDecoration: none;
 
-$headerBackground: #f0f0f0;
-$headerBorder: #d9d9d9;
+$headerBackground: #eee;
+$headerBorder: #d7d7d7;
 
 $sidebarBackground: #f9f9f9;
-$sidebarBorder: #e3e3e3;
+$sidebarBorder: #e1e1e1;
 
 $scrollbarColor: #d2d2d2;
 $scrollbarColorHover: #aaa;

+ 1 - 1
lib/app.rb

@@ -184,7 +184,7 @@ class App < Sinatra::Application
     end
 
     def app_size
-      @app_size ||= cookies[:size].nil? ? '18rem' : "#{cookies[:size]}px"
+      @app_size ||= cookies[:size].nil? ? '20rem' : "#{cookies[:size]}px"
     end
 
     def app_layout

BIN
public/icons/ui/back/16.png


BIN
public/icons/ui/back/16@2x.png


BIN
public/icons/ui/home/16.png


BIN
public/icons/ui/home/16@2x.png


+ 0 - 1
public/icons/ui/home/SOURCE

@@ -1 +0,0 @@
-http://www.entypo.com/

BIN
public/icons/ui/menu/16.png


BIN
public/icons/ui/menu/16@2x.png


+ 2 - 2
test/app_test.rb

@@ -31,7 +31,7 @@ class AppTest < MiniTest::Spec
 
     it "sets default size" do
       get '/'
-      assert_includes last_response.body, 'data-size="18rem"'
+      assert_includes last_response.body, 'data-size="20rem"'
     end
 
     it "sets size from cookie" do
@@ -115,7 +115,7 @@ class AppTest < MiniTest::Spec
 
     it "sets default size" do
       get '/manifest.appcache'
-      assert_includes last_response.body, '18rem'
+      assert_includes last_response.body, '20rem'
     end
 
     it "sets size from cookie" do

+ 19 - 13
views/app.erb

@@ -1,22 +1,28 @@
 <div class="_app<%= " #{app_layout}" if app_layout %>" role="application">
   <header class="_header" role="banner">
-    <button type="button" class="_mobile-btn _back-btn">Back</button>
-    <button type="button" class="_mobile-btn _forward-btn">Forward</button>
-    <button type="button" class="_mobile-btn _menu-btn">Menu</button>
-    <button type="button" class="_mobile-btn _home-btn">Home</button>
+    <button type="button" aria-label="Toggle navigation" class="_header-btn" data-toggle-sidebar hidden>
+      <svg viewBox="0 0 24 24"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"></path></svg>
+    </button>
     <form class="_search" role="search">
       <input type="search" name="q" class="_search-input" placeholder="Search&hellip;" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" maxlength="30" aria-label="Search">
       <button type="reset" class="_search-clear" title="Clear search">Clear search</button>
       <div class="_search-tag"></div>
     </form>
-    <h1 class="_logo">
-      <a href="/" class="_nav-link" title="Offline API Documentation Browser">DevDocs</a><%= "/ #{@doc['full_name']}" if @doc %>
-    </h1>
-    <nav class="_nav" role="navigation">
-      <a href="/offline" class="_nav-link">Offline</a>
-      <a href="/about" class="_nav-link">About</a>
-      <a href="/news" class="_nav-link">News</a>
-      <a href="/help" class="_nav-link">Tips</a>
+    <button type="button" aria-label="Back" class="_header-btn" data-back hidden>
+      <svg viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"></svg>
+    </button>
+    <button type="button" aria-label="Forward" class="_header-btn _forward-btn" data-forward hidden>
+      <svg viewBox="0 0 24 24"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"></svg>
+    </button>
+    <button type="button" aria-label="Toggle menu" title="Toggle menu" class="_header-btn _menu-btn" data-toggle-menu>
+      <svg viewBox="0 0 24 24"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"></path></svg>
+    </button>
+    <nav class="_menu" role="navigation">
+      <h1 class="_menu-title"><a href="/" class="_menu-title-link">DevDocs</a></h1>
+      <a href="/offline" class="_menu-link">Offline</a>
+      <a href="/news" class="_menu-link">Changelog</a>
+      <a href="/help" class="_menu-link">Help</a>
+      <a href="/about" class="_menu-link">About</a>
     </nav>
   </header>
   <section class="_sidebar" tabindex="-1">
@@ -34,7 +40,7 @@
 </div>
 <style data-size="<%= app_size %>" data-resizer>
   ._container { margin-left: <%= app_size %>; }
-  ._search, ._list, ._sidebar-footer { width: <%= app_size %>; }
+  ._header, ._list, ._sidebar-footer { width: <%= app_size %>; }
   ._list-hover.clone { min-width: <%= app_size %>; }
   ._notice, ._path, ._resizer { left: <%= app_size %>; }
 </style>