content/static: update version page symbol UI

Refactors versions page template to meet updated
design spec. The page now has a responsive layout
and symbol history content visibility can be toggled
with expand and collapse buttons at versions page
header or by clicking on the commit time text.

For golang/go#37102

Change-Id: I61487c51ac76ee02495c22d1d798bfa1712a9e35
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/306132
Trust: Jamal Carvalho <jamal@golang.org>
Run-TryBot: Jamal Carvalho <jamal@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/content/static/css/versions.css b/content/static/css/versions.css
new file mode 100644
index 0000000..2f6e554
--- /dev/null
+++ b/content/static/css/versions.css
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2021 The Go Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file.
+ */
+
+.Unit-content .Versions {
+  margin-top: 1rem;
+  max-width: unset;
+}
+.Versions-title {
+  align-items: center;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 1rem 2.5rem;
+  margin-bottom: 1rem;
+}
+.Versions-title h2 {
+  margin: 0;
+}
+.Versions-titleButtonGroup {
+  display: none;
+}
+.Versions-titleButtonGroup button {
+  background-color: transparent;
+  border: none;
+  bottom: 1rem;
+  color: var(--turq-dark);
+  cursor: pointer;
+  font-size: 0.875rem;
+  text-decoration: none;
+}
+.Versions-titleButtonGroup button:disabled {
+  color: var(--gray-8);
+  cursor: initial;
+}
+.Versions-list {
+  gap: 0 1rem;
+  line-height: 2.25rem;
+}
+@media only screen and (min-width: 37.5rem) {
+  .Versions-list {
+    display: grid;
+    grid-template-columns: fit-content(8rem) fit-content(20rem) min-content auto;
+  }
+}
+.Version-major {
+  margin-bottom: 1rem;
+  min-width: 4rem;
+}
+@media only screen and (min-width: 37.5rem) {
+  .Version-major {
+    margin-bottom: 0;
+  }
+}
+.Version-tag {
+  text-align: left;
+}
+@media only screen and (min-width: 37.5rem) {
+  .Version-tag {
+    text-align: right;
+  }
+}
+.Version-dot {
+  border: 0.0625rem solid var(--gray-9);
+  color: var(--gray-8);
+  display: none;
+  font-size: 2.75rem;
+  justify-content: center;
+  line-height: 1.75rem;
+  -webkit-text-stroke: 0.125rem var(--white);
+  width: 0;
+}
+.Version-dot::before {
+  content: '•';
+}
+@media only screen and (min-width: 37.5rem) {
+  .Version-dot {
+    display: flex;
+  }
+}
+.Version-dot--minor {
+  color: var(--turq-dark);
+}
+.Version-commitTime {
+  margin-left: 1rem;
+  white-space: nowrap;
+}
+.Version-details {
+  line-height: 1.25rem;
+  margin-left: -0.5rem;
+}
+.Version-summary {
+  align-items: center;
+  cursor: pointer;
+  display: flex;
+  line-height: 2.25rem;
+  padding-right: 0.5rem;
+  white-space: nowrap;
+  width: min-content;
+}
+details.Version-details img {
+  position: relative;
+  top: 0.0625rem;
+}
+details.Version-details[open] img {
+  transform: rotate(90deg);
+}
diff --git a/content/static/html/helpers/_versions.tmpl b/content/static/html/helpers/_versions.tmpl
index d8748cb..4f68034 100644
--- a/content/static/html/helpers/_versions.tmpl
+++ b/content/static/html/helpers/_versions.tmpl
@@ -8,15 +8,18 @@
 
 {{define "versions"}}
   <div class="Versions">
-    <table>
-      <tr><th colspan="3"><h2>Versions in this module</h2></th></tr>
-      {{template "module_list" .ThisModule}}
-      {{if .IncompatibleModules}}
-        <tr><th colspan="3"><h2>Incompatible versions in this module</h2></th></tr>
-        {{template "module_list_incompatible" .IncompatibleModules}}
-      {{end}}
-    </table>
-
+    <div class="Versions-title">
+      <h2>Versions in this module</h2>
+      <div class="Versions-titleButtonGroup js-buttonGroup">
+        <button class="js-versionsExpand">Expand all</button>
+        <button class="js-versionsCollapse">Collapse all</button>
+      </div>
+    </div>
+    {{template "version_list" .ThisModule}}
+    {{if .IncompatibleModules}}
+      <h2>Incompatible versions in this module</h2>
+      {{template "version_list" .IncompatibleModules}}
+    {{end}}
     {{if .OtherModules}}
       <h2>Other modules containing this package</h2>
       {{range .OtherModules}}
@@ -26,87 +29,74 @@
   </div>
 {{end}}
 
-
 {{/* . is []*internal/frontend.VersionList */}}
 
-{{define "module_list_incompatible"}}
-  {{range $major := .}}
-     {{range $i, $v := $major.Versions}}
-       <tr>
-         <td>
-           {{if eq $i 0 }}
-             <div class="Versions-major">
-               {{if $major.Deprecated}}(Deprecated{{with $major.DeprecationComment}}: {{.}}{{end}}){{end}}
-             </div>
-           {{end}}
-         </td>
-         <td>
-           <a href="{{$v.Link}}">{{$v.Version}}</a>
-           {{if $v.Retracted}}(Retracted{{with .RetractionRationale}}: {{.}}){{end}}{{end}}
-         </td>
-         <td>
-           <div class="Versions-commitTime">{{$v.CommitTime}}</div>
-         </td>
-       </tr>
-     {{end}}
-  {{end}}
+{{define "version_list"}}
+  <div class="Versions-list">
+    {{range $major := .}}
+      {{range $i, $v := $major.Versions}}
+        <div class="Version-major">
+          {{if and (eq $i 0) (not $major.Incompatible)}}
+            <strong>{{$major.Major}}</strong>
+            <div>{{if $major.Deprecated}}(Deprecated{{with $major.DeprecationComment}}: {{.}}{{end}}){{end}}</div>
+          {{end}}
+        </div>
+        <div class="Version-tag">
+          <a class="js-versionLink" href="{{$v.Link}}">{{$v.Version}}</a>
+          <div>{{if $v.Retracted}}(Retracted{{with $v.RetractionRationale}}: {{.}}{{end}}){{end}}</div>
+        </div>
+        <div class="Version-dot{{if and $v.IsMinor (not $major.Incompatible)}} Version-dot--minor{{end}}"></div>
+        {{if and $v.Symbols (not $major.Incompatible)}}
+          {{template "symbol_history" $v}}
+        {{else}}
+          <div class="Version-commitTime">{{$v.CommitTime}}</div>
+        {{end}}
+      {{end}}
+    {{end}}
+  </div>
 {{end}}
 
-{{define "module_list"}}
-  {{range $major := .}}
-     {{range $i, $v := $major.Versions}}
-       <tr>
-         <td>
-           {{if eq $i 0 }}
-             <div class="Versions-major">
-               {{$major.Major}}
-               {{if $major.Deprecated}}(Deprecated{{with $major.DeprecationComment}}: {{.}}{{end}}){{end}}
-             </div>
-           {{end}}
-         </td>
-         <td>
-           <a href="{{$v.Link}}">{{$v.Version}}</a>
-           {{if $v.Retracted}}(Retracted{{with .RetractionRationale}}: {{.}}){{end}}{{end}}
-         </td>
-         <td>
-           <div class="Versions-commitTime">{{$v.CommitTime}}</div>
-           {{if $v.Symbols }}
-             <div class="Versions-symbols">
-             <div class="Versions-symbolsHeader">Changes in this version</div>
-             {{range $v.Symbols}}
-               <div class="Versions-symbolSection">
-               {{range .}}
-                 {{if eq .Kind "Type"}}
-                   <div class="Versions-symbolType">
-                   {{template "symbol" .}}
-                   {{range .Children}}
-                     <div class="Versions-symbolChild">{{template "symbol" .}}</div>
-                   {{end}}
-                   </div>
-                 {{else}}
-                   <div>{{template "symbol" .}}</div>
-                 {{end}}
-               {{end}}
-               </div>
-             {{end}}
-           </div>
-           {{end}}
-         </td>
-       </tr>
-     {{end}}
-  {{end}}
+{{define "symbol_history"}}
+  <details class="Version-details js-versionDetails">
+    <summary class="Version-summary">
+      <img alt="" height="24" width="24" src="/static/img/pkg-icon-arrowRight_24x24.svg">
+      {{.CommitTime}}
+    </summary>
+    <div class="Versions-symbols">
+      <div class="Versions-symbolsHeader">Changes in this version</div>
+      {{range .Symbols}}
+        <div class="Versions-symbolSection">
+          {{range .}}
+            {{if eq .Kind "Type"}}
+              <div class="Versions-symbolType">
+                {{template "symbol" .}}
+                {{range .Children}}
+                  <div class="Versions-symbolChild">{{template "symbol" .}}</div>
+                {{end}}
+              </div>
+            {{else}}
+              <div>{{template "symbol" .}}</div>
+            {{end}}
+          {{end}}
+        </div>
+      {{end}}
+    </div>
+  </details>
 {{end}}
 
 {{define "symbol"}}
   <div>
-     {{if .New}}
-       <span class="Versions-symbolBulletNew">+</span><a class="Versions-symbolSynopsis" href="{{.Link}}">{{.Synopsis}}</a>
-     {{else}}
-       <span class="Versions-symbolOld Versions-symbolSynopsis">{{.Synopsis}}</span>
-     {{end}}
-     {{if .Builds}}
-       <span class="Versions-symbolBuildsDash">—</span>
-       <span class="Versions-symbolBuilds">{{range $i, $b := .Builds}}{{if $i}}, {{end}}{{$b}}{{end}}</span>
-     {{end}}
+    {{if .New}}
+      <span class="Versions-symbolBulletNew">+</span>
+      <a class="Versions-symbolSynopsis" href="{{.Link}}">{{.Synopsis}}</a>
+    {{else}}
+      <span class="Versions-symbolOld Versions-symbolSynopsis">{{.Synopsis}}</span>
+    {{end}}
+    {{if .Builds}}
+      <span class="Versions-symbolBuildsDash">—</span>
+      <span class="Versions-symbolBuilds">
+        {{range $i, $b := .Builds}}{{if $i}}, {{end}}{{$b}}{{end}}
+      </span>
+    {{end}}
   </div>
 {{end}}
diff --git a/content/static/html/pages/unit_versions.tmpl b/content/static/html/pages/unit_versions.tmpl
index c628773..c345499 100644
--- a/content/static/html/pages/unit_versions.tmpl
+++ b/content/static/html/pages/unit_versions.tmpl
@@ -4,8 +4,22 @@
   license that can be found in the LICENSE file.
 -->
 
+{{define "unit_pre_content"}}
+  <link href="/static/css/versions.css?version={{.AppVersionLabel}}" rel="stylesheet">
+{{end}}
+
 {{define "unit_content"}}
   <div class="Unit-content" role="main" data-test-id="UnitVersions">
-    {{block "versions" .Details}}{{end}}
+    {{if (.Experiments.IsActive "symbol-history-versions-page")}}
+      {{block "versions" .Details}}{{end}}
+    {{else}}
+      {{block "legacy_versions" .Details}}{{end}}
+    {{end}}
   </div>
 {{end}}
+
+{{define "unit_post_content"}}
+  <script>
+    loadScript("/static/js/versions.js", {type: 'module', async: true, defer: true})
+  </script>
+{{end}}
diff --git a/content/static/js/versions.js b/content/static/js/versions.js
new file mode 100644
index 0000000..2165e32
--- /dev/null
+++ b/content/static/js/versions.js
@@ -0,0 +1,7 @@
+/*!
+ * @license
+ * Copyright 2021 The Go Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file.
+ */class VersionsController{constructor(){this.expand=document.querySelector(".js-versionsExpand");this.collapse=document.querySelector(".js-versionsCollapse");this.details=[...document.querySelectorAll(".js-versionDetails")];if(!!this.expand?.parentElement){this.details.some(e=>e.tagName==="DETAILS")&&(this.expand.parentElement.style.display="block");for(const e of this.details)e.addEventListener("click",()=>{this.updateButtons()});this.expand?.addEventListener("click",()=>{this.details.map(e=>e.open=!0),this.updateButtons()}),this.collapse?.addEventListener("click",()=>{this.details.map(e=>e.open=!1),this.updateButtons()}),this.updateButtons(),this.setCurrent()}}setCurrent(){const e=document.querySelector(".js-canonicalURLPath")?.dataset?.canonicalUrlPath,t=document.querySelector(`.js-versionLink[href="${e}"]`);t&&(t.style.fontWeight="bold")}updateButtons(){setTimeout(()=>{if(!this.expand||!this.collapse)return;let e,t;for(const s of this.details)e=e||s.open,t=t||!s.open;this.expand.style.display=t?"inline-block":"none",this.collapse.style.display=t?"none":"inline-block"})}}new VersionsController;
+//# sourceMappingURL=versions.js.map
diff --git a/content/static/js/versions.js.map b/content/static/js/versions.js.map
new file mode 100644
index 0000000..028d44d
--- /dev/null
+++ b/content/static/js/versions.js.map
@@ -0,0 +1,7 @@
+{
+  "version": 3,
+  "sources": ["versions.ts"],
+  "sourcesContent": ["/*!\n * @license\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n/**\n * VersionsController encapsulates event listeners and UI updates\n * for the versions page. As the the expandable sections containing\n * the symbol history for a package are opened and closed it toggles\n * visiblity of the buttons to expand or collapse them. On page load\n * it adds an indicator to the version that matches the version request\n * by the user for the page or the canonical url path.\n */\nclass VersionsController {\n  private expand = document.querySelector<HTMLButtonElement>('.js-versionsExpand');\n  private collapse = document.querySelector<HTMLButtonElement>('.js-versionsCollapse');\n  private details = [...document.querySelectorAll<HTMLDetailsElement>('.js-versionDetails')];\n\n  constructor() {\n    if (!this.expand?.parentElement) return;\n    if (this.details.some(d => d.tagName === 'DETAILS')) {\n      this.expand.parentElement.style.display = 'block';\n    }\n\n    for (const d of this.details) {\n      d.addEventListener('click', () => {\n        this.updateButtons();\n      });\n    }\n\n    this.expand?.addEventListener('click', () => {\n      this.details.map(d => (d.open = true));\n      this.updateButtons();\n    });\n\n    this.collapse?.addEventListener('click', () => {\n      this.details.map(d => (d.open = false));\n      this.updateButtons();\n    });\n\n    this.updateButtons();\n    this.setCurrent();\n  }\n\n  /**\n   * setCurrent applies the active style to the version dot\n   * for the version that matches the canonical URL path.\n   */\n  private setCurrent() {\n    const canonicalPath = document.querySelector<HTMLElement>('.js-canonicalURLPath')?.dataset\n      ?.canonicalUrlPath;\n    const versionLink = document.querySelector<HTMLElement>(\n      `.js-versionLink[href=\"${canonicalPath}\"]`\n    );\n    if (versionLink) {\n      versionLink.style.fontWeight = 'bold';\n    }\n  }\n\n  private updateButtons() {\n    setTimeout(() => {\n      if (!this.expand || !this.collapse) return;\n      let someOpen, someClosed;\n      for (const d of this.details) {\n        someOpen = someOpen || d.open;\n        someClosed = someClosed || !d.open;\n      }\n      this.expand.style.display = someClosed ? 'inline-block' : 'none';\n      this.collapse.style.display = someClosed ? 'none' : 'inline-block';\n    });\n  }\n}\n\nnew VersionsController();\n"],
+  "mappings": "AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GAeA,wBAAyB,CAKvB,aAAc,CAJN,YAAS,SAAS,cAAiC,sBACnD,cAAW,SAAS,cAAiC,wBACrD,aAAU,CAAC,GAAG,SAAS,iBAAqC,uBAGlE,GAAI,EAAC,KAAK,QAAQ,cAClB,CAAI,KAAK,QAAQ,KAAK,GAAK,EAAE,UAAY,YACvC,MAAK,OAAO,cAAc,MAAM,QAAU,SAG5C,SAAW,KAAK,MAAK,QACnB,EAAE,iBAAiB,QAAS,IAAM,CAChC,KAAK,kBAIT,KAAK,QAAQ,iBAAiB,QAAS,IAAM,CAC3C,KAAK,QAAQ,IAAI,GAAM,EAAE,KAAO,IAChC,KAAK,kBAGP,KAAK,UAAU,iBAAiB,QAAS,IAAM,CAC7C,KAAK,QAAQ,IAAI,GAAM,EAAE,KAAO,IAChC,KAAK,kBAGP,KAAK,gBACL,KAAK,cAOC,YAAa,CACnB,KAAM,GAAgB,SAAS,cAA2B,yBAAyB,SAC/E,iBACE,EAAc,SAAS,cAC3B,yBAAyB,OAE3B,AAAI,GACF,GAAY,MAAM,WAAa,QAI3B,eAAgB,CACtB,WAAW,IAAM,CACf,GAAI,CAAC,KAAK,QAAU,CAAC,KAAK,SAAU,OACpC,GAAI,GAAU,EACd,SAAW,KAAK,MAAK,QACnB,EAAW,GAAY,EAAE,KACzB,EAAa,GAAc,CAAC,EAAE,KAEhC,KAAK,OAAO,MAAM,QAAU,EAAa,eAAiB,OAC1D,KAAK,SAAS,MAAM,QAAU,EAAa,OAAS,kBAK1D,GAAI",
+  "names": []
+}
diff --git a/content/static/js/versions.ts b/content/static/js/versions.ts
new file mode 100644
index 0000000..6ee0cd9
--- /dev/null
+++ b/content/static/js/versions.ts
@@ -0,0 +1,76 @@
+/*!
+ * @license
+ * Copyright 2021 The Go Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file.
+ */
+
+/**
+ * VersionsController encapsulates event listeners and UI updates
+ * for the versions page. As the the expandable sections containing
+ * the symbol history for a package are opened and closed it toggles
+ * visiblity of the buttons to expand or collapse them. On page load
+ * it adds an indicator to the version that matches the version request
+ * by the user for the page or the canonical url path.
+ */
+class VersionsController {
+  private expand = document.querySelector<HTMLButtonElement>('.js-versionsExpand');
+  private collapse = document.querySelector<HTMLButtonElement>('.js-versionsCollapse');
+  private details = [...document.querySelectorAll<HTMLDetailsElement>('.js-versionDetails')];
+
+  constructor() {
+    if (!this.expand?.parentElement) return;
+    if (this.details.some(d => d.tagName === 'DETAILS')) {
+      this.expand.parentElement.style.display = 'block';
+    }
+
+    for (const d of this.details) {
+      d.addEventListener('click', () => {
+        this.updateButtons();
+      });
+    }
+
+    this.expand?.addEventListener('click', () => {
+      this.details.map(d => (d.open = true));
+      this.updateButtons();
+    });
+
+    this.collapse?.addEventListener('click', () => {
+      this.details.map(d => (d.open = false));
+      this.updateButtons();
+    });
+
+    this.updateButtons();
+    this.setCurrent();
+  }
+
+  /**
+   * setCurrent applies the active style to the version dot
+   * for the version that matches the canonical URL path.
+   */
+  private setCurrent() {
+    const canonicalPath = document.querySelector<HTMLElement>('.js-canonicalURLPath')?.dataset
+      ?.canonicalUrlPath;
+    const versionLink = document.querySelector<HTMLElement>(
+      `.js-versionLink[href="${canonicalPath}"]`
+    );
+    if (versionLink) {
+      versionLink.style.fontWeight = 'bold';
+    }
+  }
+
+  private updateButtons() {
+    setTimeout(() => {
+      if (!this.expand || !this.collapse) return;
+      let someOpen, someClosed;
+      for (const d of this.details) {
+        someOpen = someOpen || d.open;
+        someClosed = someClosed || !d.open;
+      }
+      this.expand.style.display = someClosed ? 'inline-block' : 'none';
+      this.collapse.style.display = someClosed ? 'none' : 'inline-block';
+    });
+  }
+}
+
+new VersionsController();
diff --git a/internal/middleware/secureheaders.go b/internal/middleware/secureheaders.go
index 2ff8b5f..b4a8379 100644
--- a/internal/middleware/secureheaders.go
+++ b/internal/middleware/secureheaders.go
@@ -27,6 +27,8 @@
 	"'sha256-nF5UdhqQFxB95DCaw1XdSQCEkIjoMhorTCQ+nQ4+Lq4='",
 	"'sha256-L+G1K2BEWa+o2vPy1pwdabLjINBByPWi1NkRwvASUq8='",
 	"'sha256-hb8VdkRSeBmkNlbshYmBnkYWC/BYHCPiz5s7liRcZNM='",
+	// From content/static/html/pages/unit_versions.tmpl
+	"'sha256-KBdPSv2Ajjw3jsa29qBhRW49nNx3jXxOLZIWX545FCA='",
 }
 
 // SecureHeaders adds a content-security-policy and other security-related