Upgrade to nick_site commit f981fa57, adds alerts and inactive sidebar option master
authorNick Downing <nick@ndcode.org>
Fri, 11 Feb 2022 14:43:19 +0000 (01:43 +1100)
committerNick Downing <nick@ndcode.org>
Wed, 16 Feb 2022 14:02:01 +0000 (01:02 +1100)
26 files changed:
_config/n.sh
_config/navigation.json
_config/site.jst
_lib/blog_post.jst
_lib/get_nodemailer.jst
_lib/navbar.jst
_lib/sidebar.jst [new file with mode: 0644]
blog/index.html.jst
contact/index.html.jst
css/bootstrap/_custom.scss
css/bootstrap/_nav.scss
css/bootstrap/_reboot.scss
css/bootstrap/_variables.scss
css/ie10-viewport-bug-workaround.css.min [deleted file]
css/jsdoc-default.css.less [deleted file]
css/login.css.min [deleted file]
my_account/index.html.jst
my_account/password_reset/index.html.jst
my_account/send_verification_email/index.html.jst
my_account/sign_up/index.html.jst
my_account/verify_email/index.html.jst
my_account/verify_password/index.html.jst
projects/index.html.jst
projects/jst/index.html.jst [new file with mode: 0644]
projects/pitree/index.html.jst [new file with mode: 0644]
search/index.html.jst

index b89228d..5ac4469 100755 (executable)
@@ -1,4 +1,4 @@
 #!/bin/sh
 ./set.mjs /api/globals/set.json <globals.json
-./set.mjs /api/nodemailers/set.json <nodemailers.json
 ./set.mjs /api/navigation/set.json <navigation.json
+#./set.mjs /api/nodemailers/set.json <nodemailers.json
index d574b43..10f7294 100644 (file)
@@ -8,19 +8,25 @@
           "title": "14/01/2022",
           "children": {},
           "menu": [],
+          "images": [
+            "image.svg",
+            "image.svg",
+            "thumbnail.svg"
+          ],
           "description": "Log-structured JSON database",
-          "author": "Nick Downing",
-          "image": "image.svg",
-          "thumbnail": "thumbnail.svg"
+          "author": "Nick Downing"
         },
         "20220128": {
           "title": "28/01/2022",
           "children": {},
           "menu": [],
+          "images": [
+            "image.svg",
+            "image.svg",
+            "thumbnail.svg"
+          ],
           "description": "Refactoring",
-          "author": "Nick Downing",
-          "image": "image.svg",
-          "thumbnail": "thumbnail.svg"
+          "author": "Nick Downing"
         }
       },
       "menu": [
     },
     "projects": {
       "title": "Projects",
-      "children": {},
-      "menu": []
+      "children": {
+        "jst": {
+          "title": "JST",
+          "description": "Create websites efficiently",
+          "children": {},
+          "menu": [],
+          "icon": "/_svg/icon_jst.svg"
+        },
+        "pitree": {
+          "title": "Ï€tree",
+          "description": "Create languages efficiently",
+          "children": {},
+          "menu": [],
+          "icon": "/_svg/icon_pitree.svg"
+        }
+      },
+      "menu": [
+        "jst",
+        "pitree"
+      ]
     },
     "sphinx": {
       "title": "Documentation",
index a346221..5c0194c 100644 (file)
@@ -24,7 +24,7 @@ return async (resources, root, prev_site) => {
 
       assert(this.database === undefined)
       this.database = await this.resources.ref(
-        'database',
+        `database:${this.root}`,
         async () => {
           let database = new logjson.Database()
           await database.open(this.root + '/database.logjson')
index 401761b..9a95717 100644 (file)
@@ -16,12 +16,15 @@ return async (env, head, body, scripts) => {
       try {
         let root = await transaction.get()
         let p = await get_navigation(root, env.component_names)
+        let images = await p.get_json('images')
 
-        div.row.mb-3 {
-          div.col-sm-12 {
-            img.img-responsive(
-              src=await p.get_json('image') || 'image.jpg'
-            ) {}
+        // we can use higher resolution images when lower not available
+        while (images.length < 2)
+          images.push(images[images.length - 1])
+
+        div.my-3 {
+          a(href=images[0]) {
+            img.img-fluid(src=images[1]) {}
           }
         }
 
index fefa21a..4a1848a 100644 (file)
@@ -4,15 +4,16 @@ let nodemailer = require('nodemailer')
 return async (root, name) => {
   let nodemailers = await root.get('nodemailers')
   if (nodemailers === undefined)
-    throw new Problem(
+    throw new jst_server.Problem(
       'Nodemailer error',
       'Please import the nodemailers data into the database.',
       509
     )
   let _nodemailer = await nodemailers.get_json(name)
-    throw new Problem(
+  if (_nodemailer === undefined)
+    throw new jst_server.Problem(
       'Nodemailer error',
-      `Can't find the nodemailer "${name}" in the nodemailers data.',
+      `Can't find the nodemailer "${name}" in the nodemailers data.`,
       509
     )
   return nodemailer.createTransport(_nodemailer)
index 848488d..8988e57 100644 (file)
@@ -103,7 +103,7 @@ return async (env, head, body, scripts) => {
 
   // note: menu_titles.length === menu_names.length + 1
   // menu_titles[0] corresponds to /, is 'Home' or similar
-  // menu_titles[i] corresponds to menu_names[i - 1], i >= 1a
+  // menu_titles[i] corresponds to menu_names[i - 1], i >= 1
   // (navbar has Home appearing at same level as its immediate children)
 
   await page(
@@ -189,7 +189,7 @@ return async (env, head, body, scripts) => {
               //  }
               //}
             }
-            nav.navbar.navbar-expand-lg.navbar-dark.bg-primary.extend-background {
+            nav.navbar.navbar-expand-lg.navbar-dark.bg-primary.container-background {
               //a.navbar-brand(href="#") {'Navbar'}
               //' '
               button.navbar-toggler(type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation") {
@@ -259,7 +259,7 @@ return async (env, head, body, scripts) => {
 
             await body(_out)
 
-            footer.page-footer.extend-background.py-5 {
+            footer.page-footer.container-background.py-5 {
               a(rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/") {
                 img(alt="Creative Commons License" style="border-width:0;" src="/images/by-sa_3.0_88x31.png") {}
               }
@@ -291,7 +291,7 @@ return async (env, head, body, scripts) => {
               span.h4.modal-title {'Sign in'}
             }
             div.modal-body {
-              form#navbar-sign-in-form {
+              form.mb-2#navbar-sign-in-form {
                 div.row {
                   div.col-md-12 {
                     div.form-group {
@@ -312,7 +312,7 @@ return async (env, head, body, scripts) => {
                 }
               }
 
-              p.mt-2 {
+              p {
                 'No account yet? '
                 a(href="/my_account/sign_up/index.html") {'Sign up'}
               }
@@ -322,7 +322,7 @@ return async (env, head, body, scripts) => {
                 a(href="/my_account/password_reset/index.html") {'Password reset'}
               }
 
-              p.'mt-3'.mb-0#navbar-sign-in-message(hidden) {}
+              div.alert.alert-danger.'mt-3'.mb-0#navbar-sign-in-alert(hidden) {}
             }
             div.modal-footer {
               button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
@@ -368,8 +368,8 @@ return async (env, head, body, scripts) => {
                 div.row {
                   div.col-md-12 {
                     div.form-group {
-                      label.form-label(for="navbar-feedback-message1") {'Message'}
-                      textarea.form-control#navbar-feedback-message1(placeholder="I noticed that..." required rows=4 maxlength=65536) {
+                      label.form-label(for="navbar-feedback-message") {'Message'}
+                      textarea.form-control#navbar-feedback-message(placeholder="I noticed that..." required rows=4 maxlength=65536) {
                         if (feedback_draft)
                           `${feedback_draft.message}`
                       }
@@ -379,7 +379,7 @@ return async (env, head, body, scripts) => {
                 }
               }
 
-              p.'mt-3'.mb-0#navbar-feedback-message(hidden) {}
+              div.alert.alert-danger.'mt-3'.mb-0#navbar-feedback-alert(hidden) {}
             }
             div.modal-footer {
               button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
@@ -462,11 +462,11 @@ return async (env, head, body, scripts) => {
         document.addEventListener(
           'DOMContentLoaded',
           () => {
+            let id_navbar_feedback_alert = document.getElementById('navbar-feedback-alert')
             let id_navbar_feedback_cross = document.getElementById('navbar-feedback-cross')
             let id_navbar_feedback_form = document.getElementById('navbar-feedback-form')
             let id_navbar_feedback_icon = document.getElementById('navbar-feedback-icon')
             let id_navbar_feedback_message = document.getElementById('navbar-feedback-message')
-            let id_navbar_feedback_message1 = document.getElementById('navbar-feedback-message1')
             let id_navbar_feedback_modal = document.getElementById('navbar-feedback-modal')
             let id_navbar_feedback_send_message = document.getElementById('navbar-feedback-send-message')
             let id_navbar_feedback_spinner = document.getElementById('navbar-feedback-spinner')
@@ -476,11 +476,11 @@ return async (env, head, body, scripts) => {
             let id_navbar_message_modal_message = document.getElementById('navbar-message-modal-message')
             let id_navbar_search_button = document.getElementById('navbar-search-button')
             let id_navbar_sign_in = document.getElementById('navbar-sign-in')
+            let id_navbar_sign_in_alert = document.getElementById('navbar-sign-in-alert')
             let id_navbar_sign_in_cross = document.getElementById('navbar-sign-in-cross')
             let id_navbar_sign_in_email = document.getElementById('navbar-sign-in-email')
             let id_navbar_sign_in_form = document.getElementById('navbar-sign-in-form')
             let id_navbar_sign_in_icon = document.getElementById('navbar-sign-in-icon')
-            let id_navbar_sign_in_message = document.getElementById('navbar-sign-in-message')
             let id_navbar_sign_in_modal = document.getElementById('navbar-sign-in-modal')
             let id_navbar_sign_in_password = document.getElementById('navbar-sign-in-password')
             let id_navbar_sign_in_sign_in = document.getElementById('navbar-sign-in-sign-in')
@@ -516,7 +516,7 @@ return async (env, head, body, scripts) => {
               //id_navbar_sign_in_tick.hidden = true
               id_navbar_sign_in_cross.hidden = true
               id_navbar_sign_in_spinner.hidden = true
-              id_navbar_sign_in_message.hidden = true
+              id_navbar_sign_in_alert.hidden = true
             }
 
             id_navbar_sign_in_email.addEventListener(
@@ -535,8 +535,8 @@ return async (env, head, body, scripts) => {
                 //id_navbar_sign_in_tick.hidden = true
                 id_navbar_sign_in_cross.hidden = true
                 id_navbar_sign_in_spinner.hidden = true
-                // the below causes an ugly flicker, so just keep the message
-                //id_navbar_sign_in_message.hidden = true
+                // the below causes an ugly flicker, so just keep the alert
+                //id_navbar_sign_in_alert.hidden = true
 
                 if (!id_navbar_sign_in_form.checkValidity()) {
                   id_navbar_sign_in_form.classList.add('was-validated');
@@ -569,32 +569,32 @@ return async (env, head, body, scripts) => {
                   id_navbar_sign_in_cross.hidden = false
                   id_navbar_sign_in_spinner.hidden = true
 
-                  id_navbar_sign_in_message.textContent = problem.detail
-                  //id_navbar_sign_in_message.classList.remove('text-success')
-                  id_navbar_sign_in_message.classList.add('text-danger')
-                  id_navbar_sign_in_message.hidden = false
+                  id_navbar_sign_in_alert.textContent = problem.detail
+                  //id_navbar_sign_in_alert.classList.remove('alert-success')
+                  //id_navbar_sign_in_alert.classList.add('alert-danger')
+                  id_navbar_sign_in_alert.hidden = false
                   return
                 }
                 //id_navbar_sign_in_tick.hidden = false
                 //id_navbar_sign_in_spinner.hidden = true
-                //id_navbar_sign_in_message.textContent = `You are now signed in as "${email}".`
-                //id_navbar_sign_in_message.classList.add('text-success')
-                //id_navbar_sign_in_message.classList.remove('text-danger')
-                //id_navbar_sign_in_message.hidden = false
+                //id_navbar_sign_in_alert.textContent = `You are now signed in as "${email}".`
+                //id_navbar_sign_in_alert.classList.add('alert-success')
+                //id_navbar_sign_in_alert.classList.remove('alert-danger')
+                //id_navbar_sign_in_alert.hidden = false
 
                 if (sign_in_out(true))
                   // if location has been changed, leave the spinner and do
                   // not show status/dialog, as it causes an annoying flicker
                   return
 
-                id_navbar_signed_in_status.textContent = 'Signed in.' //`Signed in as ${email}.`
+                id_navbar_signed_in_status.textContent = 'Signed in.'
                 id_navbar_sign_in.hidden = true
                 id_navbar_sign_up.hidden = true
                 id_navbar_sign_out.hidden = false
 
                 id_navbar_sign_in_icon.hidden = false
                 id_navbar_sign_in_spinner.hidden = true
-                id_navbar_sign_in_message.hidden = true
+                id_navbar_sign_in_alert.hidden = true
                 id_navbar_message_modal_message.textContent = `You are now signed in as "${email}".`
                 $('#navbar-sign-in-modal').modal('hide')
                 $('#navbar-message-modal').modal('show')
@@ -624,7 +624,7 @@ return async (env, head, body, scripts) => {
                   // not show status/dialog, as it causes an annoying flicker
                   return
 
-                id_navbar_signed_in_status.textContent = 'Browsing as guest.'
+                id_navbar_signed_in_status.textContent = 'Signed out.'
                 id_navbar_sign_in.hidden = false
                 id_navbar_sign_up.hidden = false
                 id_navbar_sign_out.hidden = true
@@ -640,9 +640,9 @@ return async (env, head, body, scripts) => {
               'click',
               () => {
                 // hack to move cursor to end of textarea
-                let temp = id_navbar_feedback_message1.value
-                id_navbar_feedback_message1.value = ''
-                id_navbar_feedback_message1.value = temp
+                let temp = id_navbar_feedback_message.value
+                id_navbar_feedback_message.value = ''
+                id_navbar_feedback_message.value = temp
 
                 $('#navbar-feedback-modal').modal('show')
                 return false
@@ -651,7 +651,7 @@ return async (env, head, body, scripts) => {
 
             $('#navbar-feedback-modal').on(
               'shown.bs.modal',
-              () => {id_navbar_feedback_message1.focus()}
+              () => {id_navbar_feedback_message.focus()}
             )
 
             let feedback_input_semaphore = new BinarySemaphore(false)
@@ -663,10 +663,10 @@ return async (env, head, body, scripts) => {
                   feedback_input_semaphore.try_acquire()
                   await api_call(
                     '/api/feedback/set_draft.json',
-                    id_navbar_feedback_message1.value.length === 0 ?
+                    id_navbar_feedback_message.value.length === 0 ?
                       null :
                       {
-                        message: id_navbar_feedback_message1.value.slice(0, 65536)
+                        message: id_navbar_feedback_message.value.slice(0, 65536)
                       }
                   )
                 }
@@ -677,15 +677,15 @@ return async (env, head, body, scripts) => {
               feedback_input_semaphore.release()
 
               id_navbar_feedback_send_message.disabled =
-                id_navbar_feedback_message1.value.length === 0
+                id_navbar_feedback_message.value.length === 0
               id_navbar_feedback_icon.hidden = false
               //id_navbar_feedback_tick.hidden = true
               id_navbar_feedback_cross.hidden = true
               id_navbar_feedback_spinner.hidden = true
-              id_navbar_feedback_message.hidden = true
+              id_navbar_feedback_alert.hidden = true
             }
 
-            id_navbar_feedback_message1.addEventListener(
+            id_navbar_feedback_message.addEventListener(
               'input',
               feedback_edited
             )
@@ -697,8 +697,8 @@ return async (env, head, body, scripts) => {
                 //id_navbar_feedback_tick.hidden = true
                 id_navbar_feedback_cross.hidden = true
                 id_navbar_feedback_spinner.hidden = true
-                // the below causes an ugly flicker, so just keep the message
-                //id_navbar_feedback_message.hidden = true
+                // the below causes an ugly flicker, so just keep the alert
+                //id_navbar_feedback_alert.hidden = true
 
                 if (!id_navbar_feedback_form.checkValidity()) {
                   id_navbar_feedback_form.classList.add('was-validated');
@@ -715,7 +715,7 @@ return async (env, head, body, scripts) => {
                   await api_call(
                     '/api/feedback/send_message.json',
                     location.href,
-                    id_navbar_feedback_message1.value.slice(0, 65536)
+                    id_navbar_feedback_message.value.slice(0, 65536)
                   )
                 }
                 catch (error) {
@@ -724,22 +724,22 @@ return async (env, head, body, scripts) => {
                   id_navbar_feedback_cross.hidden = false
                   id_navbar_feedback_spinner.hidden = true
 
-                  id_navbar_feedback_message.textContent = problem.detail
-                  //id_navbar_feedback_message.classList.remove('text-success')
-                  id_navbar_feedback_message.classList.add('text-danger')
-                  id_navbar_feedback_message.hidden = false
+                  id_navbar_feedback_alert.textContent = problem.detail
+                  //id_navbar_feedback_alert.classList.remove('alert-success')
+                  //id_navbar_feedback_alert.classList.add('alert-danger')
+                  id_navbar_feedback_alert.hidden = false
                   return
                 }
                 //id_navbar_feedback_tick.hidden = false
                 //id_navbar_feedback_spinner.hidden = true
-                //id_navbar_feedback_message.textContent = 'We have received your message. We will be in touch as soon as possible.'
-                //id_navbar_feedback_message.classList.add('text-success')
-                //id_navbar_feedback_message.classList.remove('text-danger')
-                //id_navbar_feedback_message.hidden = false
+                //id_navbar_feedback_alert.alertContent = 'We have received your message. We will be in touch as soon as possible.'
+                //id_navbar_feedback_alert.classList.add('alert-success')
+                //id_navbar_feedback_alert.classList.remove('alert-danger')
+                //id_navbar_feedback_alert.hidden = false
 
                 id_navbar_feedback_icon.hidden = false
                 id_navbar_feedback_spinner.hidden = true
-                id_navbar_feedback_message.hidden = true
+                id_navbar_feedback_alert.hidden = true
                 id_navbar_message_modal_message.textContent = 'Thanks! We have received your feedback.'
                 $('#navbar-feedback-modal').modal('hide')
                 $('#navbar-message-modal').modal('show')
diff --git a/_lib/sidebar.jst b/_lib/sidebar.jst
new file mode 100644 (file)
index 0000000..5f108e2
--- /dev/null
@@ -0,0 +1,802 @@
+let assert = require('assert')
+let jst_server = (await import('@ndcode/jst_server')).default
+let XDate = require('xdate')
+
+let arrays_equal =
+  (a, b) =>
+    a.length === b.length && a.every((value, index) => value === b[index])
+
+return async (env, head, body, scripts) => {
+  //let cart = await _require('/online_store/cart.jst')
+  let fa_arrow_circle_left = await env.site.get_min_svg('/_svg/fa_arrow-circle-left.svg')
+  let fa_bars = await env.site.get_min_svg('/_svg/fa_bars.svg')
+  let fa_times_circle = await env.site.get_min_svg('/_svg/fa_times-circle.svg')
+  let fa_envelope = await env.site.get_min_svg('/_svg/fa_envelope.svg')
+  let fa_unlock_alt = await env.site.get_min_svg('/_svg/fa_unlock-alt.svg')
+  let fa_search = await env.site.get_min_svg('/_svg/fa_search.svg')
+  let get_session = await _require('/_lib/get_session.jst')
+  //let icon_cart_small = await env.site.get_min_svg('/_svg/icon_cart_small.svg')
+  let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
+  //let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
+  let avatar_maker = await env.site.get_min_svg('/_svg/AvatarMaker.svg')
+  let page = await _require('/_lib/page.jst')
+
+  // initialize env.cart
+  //await cart(env)
+
+  // compute breadcrumbs from directories of the path
+  let component_names = env.parsed_url.pathname.split('/')
+  assert(component_names.length >= 2)
+  assert(component_names[0].length === 0)
+  assert(component_names[component_names.length - 1].length)
+  component_names = component_names.slice(1, -1)
+
+  let transaction = await env.site.database.Transaction()
+  let signed_in_as
+  let site_title, copyright
+  let component_titles // collects breadcrumb titles for current page
+  let menu_names, menu_titles // collects top level of menu for the sidebar
+  let feedback_draft
+  try {
+    let root = await transaction.get()
+
+    let session = await get_session(env, root)
+    signed_in_as = await session.get_json('signed_in_as')
+
+    let globals = await root.get('globals')
+    site_title = await globals.get_json('site_title')
+    copyright = await globals.get_json('copyright')
+
+    let navigation = await root.get('navigation')
+    if (navigation === undefined)
+      throw new jst_server.Problem(
+        'Navigation error',
+        'Please import the navigation tree into the database.',
+        508
+      )
+
+    // this code is taken from get_navigation.jst and instrumented
+    let p = navigation
+    component_titles = [await p.get_json('title')] // Home
+    for (let i = 0; i < component_names.length; ++i) {
+      let children = await p.get('children')
+      p = await children.get(component_names[i])
+      if (navigation === undefined)
+        throw new jst_server.Problem(
+          'Navigation error',
+          `Can't find the path "${
+            component_names.slice(0, i + 1).map(name => '/' + name).join('')
+          }" in the navigation tree.`,
+          508
+        )
+      component_titles.push(await p.get_json('title'))
+    }
+
+    // similar to above but walks the top level laterally (not deeply)
+    menu_names = await navigation.get_json('menu')
+    let children = await navigation.get('children')
+    menu_titles = [await navigation.get_json('title')] // Home
+    for (let i = 0; i < menu_names.length; ++i) {
+      let child = await children.get(menu_names[i])
+      if (child === undefined)
+        throw new jst_server.Problem(
+          'Navigation error',
+          `Can't find the path "/${menu_names[i]}" in the navigation tree.`
+          508
+        )
+      menu_titles.push(await child.get('title'))
+    }
+
+    feedback_draft = await session.get_json('feedback_draft')
+    if (feedback_draft === undefined || env.now >= feedback_draft.expires)
+      feedback_draft = null
+  }
+  finally {
+    transaction.rollback()
+  }
+
+  // save breadcrumbs and their titles for breadcrumbs.jst
+  // note: component_titles.length === component_names.length + 1
+  // component_titles[0] corresponds to /, is 'Home' or similar
+  // component_titles[i] corresponds to component_names[i - 1], i >= 1
+  env.component_names = component_names
+  env.component_titles = component_titles
+
+  // note: menu_titles.length === menu_names.length + 1
+  // menu_titles[0] corresponds to /, is 'Home' or similar
+  // menu_titles[i] corresponds to menu_names[i - 1], i >= 1
+  // (sidebar has Home appearing at same level as its immediate children)
+
+  await page(
+    env,
+    // head
+    async _out => {
+      title {
+        `${site_title}: ${
+          component_titles[
+            component_names.length >= 2 ? 1 : component_names.length
+          ]
+        }`
+      }
+
+      await head(_out)
+    },
+    // body
+    async _out => {
+      // extract top-level directory name
+      assert(env.parsed_url.pathname.slice(0, 1) === '/')
+      let index = env.parsed_url.pathname.indexOf('/', 1)
+      let dir = index === -1 ? '' : env.parsed_url.pathname.slice(1, index)
+
+      div.container-fluid {
+        div.row {
+          div.col-md.sidebar-outer.sidebar-outer-collapsed#sidebar-outer {
+            nav.sidebar-inner.d-flex.flex-column#sidebar-inner {
+              div.mb-4 {
+                div(style="width: 128px; height: 128px;") {
+                  _out.push(avatar_maker)
+                }
+                b.h1 {
+                  `${site_title}`
+                }
+              }
+
+              div.mb-2 {
+                span#sidebar-signed-in-status {
+                  if (signed_in_as !== undefined)
+                    'Signed in.'
+                  else
+                    'Signed out.'
+                }
+                ' '
+                if (signed_in_as !== undefined)
+                  a#sidebar-sign-in(href="#" hidden) {'Sign in'}
+                else
+                  a#sidebar-sign-in(href="#") {'Sign in'}
+                ' '
+                if (signed_in_as !== undefined)
+                  a#sidebar-sign-up(href="/my_account/sign_up/index.html" hidden) {'Sign up'}
+                else
+                  a#sidebar-sign-up(href="/my_account/sign_up/index.html") {'Sign up'}
+                ' '
+                if (signed_in_as !== undefined)
+                  a#sidebar-sign-out(href="#") {'Sign out'}
+                else
+                  a#sidebar-sign-out(href="#" hidden) {'Sign out'}
+              }
+
+              form.mb-4(action="/search/index.html") {
+                div.input-group {
+                  input.form-control(name="query" type="text" placeholder="Search" aria-describedby="search-button") {}
+                  div.input-group-append {
+                    button.btn.btn-outline-secondary#sidebar-search-button(type="submit") {
+                      div.icon24-outer {
+                        div.icon24-inner {_out.push(fa_search)}
+                      }
+                    }
+                  }
+                }
+              }
+
+              // the active entry in the sidebar bar is based on which top-level
+              // page we are under, even if we are not directly on that page
+              // but one of its children, this may be unexpected as the active
+              // entry does not highlight on hover, but you can still click it;
+              // we determine here the path to the corresponding top-level page
+              let component_prefix = component_names.slice(0, 1)
+
+              for (let i = 0; i < menu_titles.length; ++i) {
+                // construct path to the top-level page about to be described
+                let menu_prefix =
+                  i === 0 ? [] : [menu_names[i - 1]]
+                let menu_prefix_path =
+                  menu_prefix.map(name => '/' + name).join('') + '/index.html'
+
+                if (arrays_equal(menu_prefix, component_prefix))
+                  div.nav-item.active {
+                    a.nav-link.nav-link2.grid-gutter-background(href=menu_prefix_path) {
+                      `${menu_titles[i]}`
+                      span.sr-only {' (current)'}
+                    }
+                  }
+                else
+                  div.nav-item {
+                    a.nav-link.nav-link2.grid-gutter-background(href=menu_prefix_path) {
+                      `${menu_titles[i]}`
+                    }
+                  }
+              }
+              div.nav-item.mt-auto {
+                a.nav-link.nav-link2.grid-gutter-background#sidebar-give-feedback(href="#") {'Give feedback'}
+              }
+            }
+          }
+
+          div.col-md.sidebar-content {
+            // the breadcrumbs have already been determined by sidebar.jst, as
+            // the HTML title is similar to the breadcrumbs (but without links)
+            let component_names = env.component_names
+            let component_titles = env.component_titles
+
+            // present component_titles as breadcrumbs, except last one as text
+            h2.page-header.grid-gutter-background.'py-2'.mb-0 {
+              button.btn.btn-outline-secondary.sidebar-toggle.mr-3#sidebar-toggle {
+                div.icon24-outer(style="top: -1px;") {
+                  div.icon24-inner {_out.push(fa_bars)}
+                }
+                span.sr-only {'Navbar toggle'}
+              }
+              for (let i = 0; i < component_names.length; ++i) {
+                a.h4(
+                  href=
+                    `${
+                      component_names.slice(0, i).map(name => '/' + name).join('')
+                    }/index.html`
+                ) {`${component_titles[i]}`}
+                ' '
+                span.h5 {'>'}
+                ' '
+              }
+              `${component_titles[component_names.length]}`
+            }
+
+            await body(_out)
+
+            footer.page-footer.grid-gutter-background.py-5 {
+              a(rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/") {
+                img(alt="Creative Commons License" style="border-width:0;" src="/images/by-sa_3.0_88x31.png") {}
+              }
+              p {
+                'This website is '
+                a(href="https://git.ndcode.org/public/nick_site.git") {
+                  'open source'
+                }
+                ' and licensed under a '
+                a(rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/") {
+                  'Creative Commons Attribution-ShareAlike 3.0 Unported License'
+                }
+                '.'
+              }
+
+              p.mb-0 {`Copyright © ${new XDate(env.now).getUTCFullYear()} ${copyright}.`}
+            }
+          }
+          div.col-md.sidebar-dummy {}
+        }
+      }
+
+      // hidden part
+      div.modal#sidebar-sign-in-modal(tabindex="-1") {
+        div.modal-dialog {
+          div.modal-content {
+            div.modal-header {
+              span.h4.modal-title {'Sign in'}
+            }
+            div.modal-body {
+              form.mb-2#sidebar-sign-in-form {
+                div.row {
+                  div.col-md-12 {
+                    div.form-group {
+                      label.form-label(for="sidebar-sign-in-email") {'Email'}
+                      input.form-control#sidebar-sign-in-email(type="email" required maxlength=256) {}
+                      div.invalid-feedback {'Please enter your account\'s email address.'}
+                    }
+                  }
+                }
+                div.row {
+                  div.col-md-12 {
+                    div.form-group {
+                      label.form-label(for="sidebar-sign-in-password") {'Password'}
+                      input.form-control#sidebar-sign-in-password(type="password" required minlength=8 maxlength=256) {}
+                      div.invalid-feedback {'Please enter at least 8 characters.'}
+                    }
+                  }
+                }
+              }
+
+              p {
+                'No account yet? '
+                a(href="/my_account/sign_up/index.html") {'Sign up'}
+              }
+
+              p.mb-0 {
+                'Forgot password? '
+                a(href="/my_account/password_reset/index.html") {'Password reset'}
+              }
+
+              div.alert.alert-danger.'mt-3'.mb-0#sidebar-sign-in-alert(hidden) {}
+            }
+            div.modal-footer {
+              button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
+                div.icon24-outer.mr-2 {
+                  div.icon24-inner {_out.push(fa_arrow_circle_left)}
+                }
+                'Back'
+              }
+              button.btn.btn-primary#sidebar-sign-in-sign-in(type="button") {
+                div.icon24-outer.mr-2#sidebar-sign-in-icon {
+                  div.icon24-inner {_out.push(fa_unlock_alt)}
+                }
+                //div.icon24-outer.mr-2#sidebar-sign-in-tick(hidden) {
+                //  div.icon24-inner {_out.push(icon_tick)}
+                //}
+                div.icon24-outer.mr-2#sidebar-sign-in-cross(hidden) {
+                  div.icon24-inner {_out.push(icon_cross)}
+                }
+                div.icon24-outer.mr-2#sidebar-sign-in-spinner(hidden) {
+                  div.icon24-inner {
+                    div.spinner-border.spinner-border-sm(role="status") {}
+                  }
+                }
+                'Sign in'
+              }
+            }
+          }
+        }
+      }
+
+      div.modal#sidebar-feedback-modal(tabindex="-1") {
+        div.modal-dialog {
+          div.modal-content {
+            div.modal-header {
+              span.h4.modal-title {'Give feedback'}
+            }
+            div.modal-body {
+              p {
+                'Did you notice something not quite right, or just want to share your impression of this page?'
+              }
+
+              form#sidebar-feedback-form {
+                div.row {
+                  div.col-md-12 {
+                    div.form-group {
+                      label.form-label(for="sidebar-feedback-message") {'Message'}
+                      textarea.form-control#sidebar-feedback-message(placeholder="I noticed that..." required rows=4 maxlength=65536) {
+                        if (feedback_draft)
+                          `${feedback_draft.message}`
+                      }
+                      div.invalid-feedback {'Please let us have your thoughts.'}
+                    }
+                  }
+                }
+              }
+
+              div.alert.alert-danger.'mt-3'.mb-0#sidebar-feedback-alert(hidden) {}
+            }
+            div.modal-footer {
+              button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
+                div.icon24-outer.mr-2 {
+                  div.icon24-inner {_out.push(fa_arrow_circle_left)}
+                }
+                'Back'
+              }
+              if (feedback_draft)
+                button.btn.btn-primary#sidebar-feedback-send-message(type="button") {
+                  div.icon24-outer.mr-2#sidebar-feedback-icon {
+                    div.icon24-inner {_out.push(fa_envelope)}
+                  }
+                  //div.icon24-outer.mr-2#sidebar-feedback-tick(hidden) {
+                  //  div.icon24-inner {_out.push(icon_tick)}
+                  //}
+                  div.icon24-outer.mr-2#sidebar-feedback-cross(hidden) {
+                    div.icon24-inner {_out.push(icon_cross)}
+                  }
+                  div.icon24-outer.mr-2#sidebar-feedback-spinner(hidden) {
+                    div.icon24-inner {
+                      div.spinner-border.spinner-border-sm(role="status") {}
+                    }
+                  }
+                  'Send message'
+                }
+              else
+                button.btn.btn-primary#sidebar-feedback-send-message(type="button" disabled) {
+                  div.icon24-outer.mr-2#sidebar-feedback-icon {
+                    div.icon24-inner {_out.push(fa_envelope)}
+                  }
+                  //div.icon24-outer.mr-2#sidebar-feedback-tick(hidden) {
+                  //  div.icon24-inner {_out.push(icon_tick)}
+                  //}
+                  div.icon24-outer.mr-2#sidebar-feedback-cross(hidden) {
+                    div.icon24-inner {_out.push(icon_cross)}
+                  }
+                  div.icon24-outer.mr-2#sidebar-feedback-spinner(hidden) {
+                    div.icon24-inner {
+                      div.spinner-border.spinner-border-sm(role="status") {}
+                    }
+                  }
+                  'Send message'
+                }
+            }
+          }
+        }
+      }
+
+      div.modal#sidebar-message-modal(tabindex="-1") {
+        div.modal-dialog {
+          div.modal-content {
+            div.modal-header {
+              span.h4.modal-title {'Message'}
+            }
+            div.modal-body#sidebar-message-modal-message {
+            }
+            div.modal-footer {
+              button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
+                div.icon24-outer.mr-2 {
+                  div.icon24-inner {_out.push(fa_times_circle)}
+                }
+                'Close'
+              }
+            }
+          }
+        }
+      }
+    },
+    // scripts
+    async _out => {
+      script(src="/js/utils.js") {}
+
+      script {
+        // this function can be overridden in a further script
+        function sign_in_out(status) {
+          return false
+        }
+
+        document.addEventListener(
+          'DOMContentLoaded',
+          () => {
+            let id_sidebar_feedback_alert = document.getElementById('sidebar-feedback-alert')
+            let id_sidebar_feedback_cross = document.getElementById('sidebar-feedback-cross')
+            let id_sidebar_feedback_form = document.getElementById('sidebar-feedback-form')
+            let id_sidebar_feedback_icon = document.getElementById('sidebar-feedback-icon')
+            let id_sidebar_feedback_message = document.getElementById('sidebar-feedback-message')
+            let id_sidebar_feedback_modal = document.getElementById('sidebar-feedback-modal')
+            let id_sidebar_feedback_send_message = document.getElementById('sidebar-feedback-send-message')
+            let id_sidebar_feedback_spinner = document.getElementById('sidebar-feedback-spinner')
+            let id_sidebar_feedback_tick = document.getElementById('sidebar-feedback-tick')
+            let id_sidebar_give_feedback = document.getElementById('sidebar-give-feedback')
+            let id_sidebar_inner = document.getElementById('sidebar-inner')
+            let id_sidebar_message_modal = document.getElementById('sidebar-message-modal')
+            let id_sidebar_message_modal_message = document.getElementById('sidebar-message-modal-message')
+            let id_sidebar_outer = document.getElementById('sidebar-outer')
+            let id_sidebar_search_button = document.getElementById('sidebar-search-button')
+            let id_sidebar_sign_in = document.getElementById('sidebar-sign-in')
+            let id_sidebar_sign_in_alert = document.getElementById('sidebar-sign-in-alert')
+            let id_sidebar_sign_in_cross = document.getElementById('sidebar-sign-in-cross')
+            let id_sidebar_sign_in_email = document.getElementById('sidebar-sign-in-email')
+            let id_sidebar_sign_in_form = document.getElementById('sidebar-sign-in-form')
+            let id_sidebar_sign_in_icon = document.getElementById('sidebar-sign-in-icon')
+            let id_sidebar_sign_in_modal = document.getElementById('sidebar-sign-in-modal')
+            let id_sidebar_sign_in_password = document.getElementById('sidebar-sign-in-password')
+            let id_sidebar_sign_in_sign_in = document.getElementById('sidebar-sign-in-sign-in')
+            let id_sidebar_sign_in_spinner = document.getElementById('sidebar-sign-in-spinner')
+            let id_sidebar_sign_in_tick = document.getElementById('sidebar-sign-in-tick')
+            let id_sidebar_sign_out = document.getElementById('sidebar-sign-out')
+            let id_sidebar_sign_up = document.getElementById('sidebar-sign-up')
+            let id_sidebar_signed_in_status = document.getElementById('sidebar-signed-in-status')
+            let id_sidebar_toggle = document.getElementById('sidebar-toggle')
+
+            // sign in form
+            id_sidebar_sign_in.addEventListener(
+              'click',
+              () => {
+                id_sidebar_sign_in_email.value = ''
+                id_sidebar_sign_in_password.value = ''
+                id_sidebar_sign_in_sign_in.disabled = true
+                $('#sidebar-sign-in-modal').modal('show')
+              }
+            )
+
+            $('#sidebar-sign-in-modal').on(
+              'shown.bs.modal',
+              () => {id_sidebar_sign_in_email.focus()}
+            )
+
+            let sign_in_edited = () => {
+              id_sidebar_sign_in_sign_in.disabled =
+                id_sidebar_sign_in_email.value.length === 0 &&
+                  id_sidebar_sign_in_password.value.length === 0
+              id_sidebar_sign_in_icon.hidden = false
+              //id_sidebar_sign_in_tick.hidden = true
+              id_sidebar_sign_in_cross.hidden = true
+              id_sidebar_sign_in_spinner.hidden = true
+              id_sidebar_sign_in_alert.hidden = true
+            }
+
+            id_sidebar_sign_in_email.addEventListener(
+              'input',
+              sign_in_edited
+            )
+            id_sidebar_sign_in_password.addEventListener(
+              'input',
+              sign_in_edited
+            )
+
+            id_sidebar_sign_in_sign_in.addEventListener(
+              'click',
+              async () => {
+                id_sidebar_sign_in_icon.hidden = false
+                //id_sidebar_sign_in_tick.hidden = true
+                id_sidebar_sign_in_cross.hidden = true
+                id_sidebar_sign_in_spinner.hidden = true
+                // the below causes an ugly flicker, so just keep the alert
+                //id_sidebar_sign_in_alert.hidden = true
+
+                if (!id_sidebar_sign_in_form.checkValidity()) {
+                  id_sidebar_sign_in_form.classList.add('was-validated');
+
+                  id_sidebar_sign_in_icon.hidden = true
+                  id_sidebar_sign_in_cross.hidden = false
+                  return
+                }
+                id_sidebar_sign_in_form.classList.remove('was-validated');
+
+                let email = id_sidebar_sign_in_email.value.slice(0, 256).toLowerCase()
+
+                id_sidebar_sign_in_icon.hidden = true
+                id_sidebar_sign_in_spinner.hidden = false
+                try {
+                  await api_call(
+                    '/api/account/sign_in.json',
+                    email,
+                    id_sidebar_sign_in_password.value.slice(0, 256)
+                  )
+                }
+                catch (error) {
+                  let problem = Problem.from(error)
+
+                  if (problem.title === 'Email not yet verified') {
+                    location.href = `/my_account/send_verification_email?email=${encodeURIComponent(email)}`
+                    return
+                  }
+
+                  id_sidebar_sign_in_cross.hidden = false
+                  id_sidebar_sign_in_spinner.hidden = true
+
+                  id_sidebar_sign_in_alert.textContent = problem.detail
+                  //id_sidebar_sign_in_alert.classList.remove('alert-success')
+                  //id_sidebar_sign_in_alert.classList.add('alert-danger')
+                  id_sidebar_sign_in_alert.hidden = false
+                  return
+                }
+                //id_sidebar_sign_in_tick.hidden = false
+                //id_sidebar_sign_in_spinner.hidden = true
+                //id_sidebar_sign_in_alert.textContent = `You are now signed in as "${email}".`
+                //id_sidebar_sign_in_alert.classList.add('alert-success')
+                //id_sidebar_sign_in_alert.classList.remove('alert-danger')
+                //id_sidebar_sign_in_alert.hidden = false
+
+                if (sign_in_out(true))
+                  // if location has been changed, leave the spinner and do
+                  // not show status/dialog, as it causes an annoying flicker
+                  return
+
+                id_sidebar_signed_in_status.textContent = 'Signed in.'
+                id_sidebar_sign_in.hidden = true
+                id_sidebar_sign_up.hidden = true
+                id_sidebar_sign_out.hidden = false
+
+                id_sidebar_sign_in_icon.hidden = false
+                id_sidebar_sign_in_spinner.hidden = true
+                id_sidebar_sign_in_alert.hidden = true
+                id_sidebar_message_modal_message.textContent = `You are now signed in as "${email}".`
+                $('#sidebar-sign-in-modal').modal('hide')
+                $('#sidebar-message-modal').modal('show')
+              }
+            )
+
+            // sign out button
+            id_sidebar_sign_out.addEventListener(
+              'click',
+              async () => {
+                try {
+                  await api_call(
+                    '/api/account/sign_out.json'
+                  )
+                }
+                catch (error) {
+                  let problem = Problem.from(error)
+
+                  id_sidebar_message_modal_message.textContent = problem.detail
+                  $('#sidebar-sign-in-modal').modal('hide')
+                  $('#sidebar-message-modal').modal('show')
+                  return
+                }
+
+                if (sign_in_out(false))
+                  // if location has been changed, leave the spinner and do
+                  // not show status/dialog, as it causes an annoying flicker
+                  return
+
+                id_sidebar_signed_in_status.textContent = 'Signed out.'
+                id_sidebar_sign_in.hidden = false
+                id_sidebar_sign_up.hidden = false
+                id_sidebar_sign_out.hidden = true
+
+                id_sidebar_message_modal_message.textContent = `You are now signed out.`
+                $('#sidebar-sign-in-modal').modal('hide')
+                $('#sidebar-message-modal').modal('show')
+              }
+            )
+
+            // feedback form
+            id_sidebar_give_feedback.addEventListener(
+              'click',
+              () => {
+                // hack to move cursor to end of textarea
+                let temp = id_sidebar_feedback_message.value
+                id_sidebar_feedback_message.value = ''
+                id_sidebar_feedback_message.value = temp
+
+                $('#sidebar-feedback-modal').modal('show')
+                return false
+              }
+            )
+
+            $('#sidebar-feedback-modal').on(
+              'shown.bs.modal',
+              () => {id_sidebar_feedback_message.focus()}
+            )
+
+            let feedback_input_semaphore = new BinarySemaphore(false)
+            ;(
+              async () => {
+                while (true) {
+                  await feedback_input_semaphore.acquire()
+                  await new Promise(resolve => setTimeout(resolve, 3000))
+                  feedback_input_semaphore.try_acquire()
+                  await api_call(
+                    '/api/feedback/set_draft.json',
+                    id_sidebar_feedback_message.value.length === 0 ?
+                      null :
+                      {
+                        message: id_sidebar_feedback_message.value.slice(0, 65536)
+                      }
+                  )
+                }
+              }
+            )() // ignore returned promise (start thread)
+
+            let feedback_edited = () => {
+              feedback_input_semaphore.release()
+
+              id_sidebar_feedback_send_message.disabled =
+                id_sidebar_feedback_message.value.length === 0
+              id_sidebar_feedback_icon.hidden = false
+              //id_sidebar_feedback_tick.hidden = true
+              id_sidebar_feedback_cross.hidden = true
+              id_sidebar_feedback_spinner.hidden = true
+              id_sidebar_feedback_alert.hidden = true
+            }
+
+            id_sidebar_feedback_message.addEventListener(
+              'input',
+              feedback_edited
+            )
+
+            id_sidebar_feedback_send_message.addEventListener(
+              'click',
+              async () => {
+                id_sidebar_feedback_icon.hidden = false
+                //id_sidebar_feedback_tick.hidden = true
+                id_sidebar_feedback_cross.hidden = true
+                id_sidebar_feedback_spinner.hidden = true
+                // the below causes an ugly flicker, so just keep the alert
+                //id_sidebar_feedback_alert.hidden = true
+
+                if (!id_sidebar_feedback_form.checkValidity()) {
+                  id_sidebar_feedback_form.classList.add('was-validated');
+
+                  id_sidebar_feedback_icon.hidden = true
+                  id_sidebar_feedback_cross.hidden = false
+                  return
+                }
+                id_sidebar_feedback_form.classList.remove('was-validated');
+
+                id_sidebar_feedback_icon.hidden = true
+                id_sidebar_feedback_spinner.hidden = false
+                try {
+                  await api_call(
+                    '/api/feedback/send_message.json',
+                    location.href,
+                    id_sidebar_feedback_message.value.slice(0, 65536)
+                  )
+                }
+                catch (error) {
+                  let problem = Problem.from(error)
+
+                  id_sidebar_feedback_cross.hidden = false
+                  id_sidebar_feedback_spinner.hidden = true
+
+                  id_sidebar_feedback_alert.textContent = problem.detail
+                  //id_sidebar_feedback_alert.classList.remove('alert-success')
+                  //id_sidebar_feedback_alert.classList.add('alert-danger')
+                  id_sidebar_feedback_alert.hidden = false
+                  return
+                }
+                //id_sidebar_feedback_tick.hidden = false
+                //id_sidebar_feedback_spinner.hidden = true
+                //id_sidebar_feedback_alert.alertContent = 'We have received your message. We will be in touch as soon as possible.'
+                //id_sidebar_feedback_alert.classList.add('alert-success')
+                //id_sidebar_feedback_alert.classList.remove('alert-danger')
+                //id_sidebar_feedback_alert.hidden = false
+
+                id_sidebar_feedback_icon.hidden = false
+                id_sidebar_feedback_spinner.hidden = true
+                id_sidebar_feedback_alert.hidden = true
+                id_sidebar_message_modal_message.textContent = 'Thanks! We have received your feedback.'
+                $('#sidebar-feedback-modal').modal('hide')
+                $('#sidebar-message-modal').modal('show')
+              }
+            )
+
+            // sidebar
+            let sidebar_outer_computed_style = window.getComputedStyle(
+              id_sidebar_outer
+            )
+            let sidebar_toggle_computed_style = window.getComputedStyle(
+              id_sidebar_toggle
+            )
+            let sidebar_is_collapsed =
+              () =>
+                sidebar_toggle_computed_style.display !== 'none' &&
+                  id_sidebar_outer.classList.contains(
+                    'sidebar-outer-collapsed'
+                  )
+            let sidebar_collapse_update = () => {
+              if (sidebar_outer_computed_style.position === 'sticky') { // md and up
+                id_sidebar_outer.style.flexBasis =
+                  sidebar_is_collapsed() ?
+                    '0px' :
+                    `${id_sidebar_inner.clientWidth}px`
+                id_sidebar_outer.style.removeProperty('height')
+              }
+              else {
+                id_sidebar_outer.style.height =
+                  sidebar_is_collapsed() ?
+                    '0px' :
+                    `${id_sidebar_inner.clientHeight}px`
+                id_sidebar_outer.style.removeProperty('flex-basis')
+              }
+            }
+            window.addEventListener('resize', sidebar_collapse_update)
+            sidebar_collapse_update()
+
+            id_sidebar_outer.addEventListener(
+              'transitionend',
+              () => {
+                // transitions are only allowed after clicking collapse button,
+                // otherwise they can be triggered by media queries on resize
+                id_sidebar_outer.style.removeProperty('transition-property')
+              }
+            )
+
+            id_sidebar_toggle.addEventListener(
+              'click',
+              () => {
+                if (
+                  id_sidebar_outer.classList.contains(
+                    'sidebar-outer-collapsed'
+                  )
+                )
+                  id_sidebar_outer.classList.remove(
+                    'sidebar-outer-collapsed'
+                  )
+                else
+                   id_sidebar_outer.classList.add(
+                    'sidebar-outer-collapsed'
+                  )
+                id_sidebar_outer.style.transitionProperty =
+                  sidebar_outer_computed_style.position === 'sticky' ?
+                    'flex-basis' : // md and up
+                    'height'
+                sidebar_collapse_update()
+              }
+            )
+          }
+        )
+      }
+
+      await scripts(_out)
+    }
+  )
+}
index cacd4a1..fb8996a 100644 (file)
@@ -16,29 +16,31 @@ return async env => {
         let children = await p.get('children')
         let menu = await p.get_json('menu')
 
-        ul.nav.flex-column {
+        nav {
           for (let i = 0; i < menu.length; ++i) {
             let name = menu[i]
             let q = await children.get(name)
+            let images = await q.get_json('images')
 
-            li.nav-item {
-              a.nav-link(href=`${name}/index.html`) {
-                table.icon-and-text {
-                  tr {
-                    td {
-                      img(
-                        src=`${name}/${await q.get_json('thumbnail') || 'thumbnail.jpg'}`
-                      ) {}
-                    }
-                    td {
-                      span.h2{
-                        `${await q.get_json('title')}`
-                      }
-                      br {}
-                      span.h4{
-                        `${await q.get_json('description')}—by ${await q.get_json('author')}`
-                      }
-                    }
+            // we can use higher resolution images when lower not available
+            while (images.length < 3)
+              images.push(images[images.length - 1])
+
+            // note: make the row a link to show hover background at edges
+            // (similar to the grid-gutter-background class I use elsewhere)
+            // note: position: relative sets the target for stretched link
+            div.row.flex-nowrap.align-items-center.nav-link-outer.position-relative {
+              div.col.col-icon256 {
+                img.img-fluid.py-3(src=`${name}/${images[2]}`) {}
+              }
+              div.col.col-text {
+                a.nav-link-inner.stretched-link(href=`${name}/index.html`) {
+                  span.h2{
+                    `${await q.get_json('title')}`
+                  }
+                  br {}
+                  span.h4{
+                    `${await q.get_json('description')}—by ${await q.get_json('author')}`
                   }
                 }
               }
index c12edde..bc04d21 100644 (file)
@@ -36,7 +36,7 @@ return async env => {
     async _out => {
       await breadcrumbs(env, _out)
 
-      p {'Do you require more information, or assistance with integrating the projects on this site? We’d love to hear from you.'}
+      p/*.mt-3*/ {'Do you require more information, or assistance with integrating the projects on this site? We’d love to hear from you.'}
 
       form#form {
         div.row {
@@ -72,8 +72,8 @@ return async env => {
         div.row {
           div.col-md-12 {
             div.form-group {
-              label.form-label(for="message1") {'Message *'}
-              textarea.form-control#message1(placeholder="I would like to..." required rows=6 maxlength=65536) {
+              label.form-label(for="message") {'Message *'}
+              textarea.form-control#message(placeholder="I am interested in..." required rows=6 maxlength=65536) {
                 if (contact_draft)
                   `${contact_draft.message}`
               }
@@ -84,7 +84,7 @@ return async env => {
       }
 
       if (contact_draft !== null)
-        button.btn.btn-success#send-enquiry(type="button") {
+        button.btn.btn-success.mb-3#send-enquiry(type="button") {
           div.icon24-outer.mr-2#icon {
             div.icon24-inner {_out.push(fa_envelope)}
           }
@@ -102,7 +102,7 @@ return async env => {
           'Send enquiry'
         }
       else
-        button.btn.btn-success#send-enquiry(type="button" disabled) {
+        button.btn.btn-success.mb-3#send-enquiry(type="button" disabled) {
           div.icon24-outer.mr-2#icon {
             div.icon24-inner {_out.push(fa_envelope)}
           }
@@ -120,9 +120,9 @@ return async env => {
           'Send enquiry'
         }
 
-      p.'mt-3'.mb-0#message(hidden) {}
+      div.alert#alert(hidden) {}
 
-      p.text-muted.mt-3 {'* These fields are required.'}
+      p.text-muted {'* These fields are required.'}
     },
     // scripts
     async _out => {
@@ -132,6 +132,7 @@ return async env => {
         document.addEventListener(
           'DOMContentLoaded',
           () => {
+            let id_alert = document.getElementById('alert')
             let id_company = document.getElementById('company')
             let id_cross = document.getElementById('cross')
             let id_email = document.getElementById('email')
@@ -140,7 +141,6 @@ return async env => {
             let id_given_names = document.getElementById('given-names')
             let id_icon = document.getElementById('icon')
             let id_message = document.getElementById('message')
-            let id_message1 = document.getElementById('message1')
             let id_send_enquiry = document.getElementById('send-enquiry')
             let id_spinner = document.getElementById('spinner')
             let id_tick = document.getElementById('tick')
@@ -158,14 +158,14 @@ return async env => {
                       id_family_name.value.length === 0 &&
                       id_company.value.length === 0 &&
                       id_email.value.length === 0 &&
-                      id_message1.value.length === 0 ?
+                      id_message.value.length === 0 ?
                       null :
                       {
                         given_names: id_given_names.value.slice(0, 256),
                         family_name: id_family_name.value.slice(0, 256),
                         company: id_company.value.slice(0, 256),
                         email: id_email.value.slice(0, 256).toLowerCase(),
-                        message: id_message1.value.slice(0, 65536)
+                        message: id_message.value.slice(0, 65536)
                       }
                   )
                 }
@@ -180,19 +180,19 @@ return async env => {
                   id_family_name.value.length === 0 &&
                   id_company.value.length === 0 &&
                   id_email.value.length === 0 &&
-                  id_message1.value.length === 0
+                  id_message.value.length === 0
               id_icon.hidden = false
               id_tick.hidden = true
               id_cross.hidden = true
               id_spinner.hidden = true
-              id_message.hidden = true
+              id_alert.hidden = true
             }
 
             id_given_names.addEventListener('input', edited)
             id_family_name.addEventListener('input', edited)
             id_company.addEventListener('input', edited)
             id_email.addEventListener('input', edited)
-            id_message1.addEventListener('input', edited)
+            id_message.addEventListener('input', edited)
 
             id_send_enquiry.addEventListener(
               'click',
@@ -201,8 +201,8 @@ return async env => {
                 id_tick.hidden = true
                 id_cross.hidden = true
                 id_spinner.hidden = true
-                // the below causes an ugly flicker, so just keep the message
-                //id_message.hidden = true
+                // the below causes an ugly flicker, so just keep the alert
+                //id_alert.hidden = true
 
                 if (!id_form.checkValidity()) {
                   id_form.classList.add('was-validated');
@@ -223,7 +223,7 @@ return async env => {
                       family_name: id_family_name.value.slice(0, 256),
                       company: id_company.value.slice(0, 256),
                       email: id_email.value.slice(0, 256).toLowerCase(),
-                      message: id_message1.value.slice(0, 65536)
+                      message: id_message.value.slice(0, 65536)
                     }
                   )
                 }
@@ -233,18 +233,18 @@ return async env => {
                   id_cross.hidden = false
                   id_spinner.hidden = true
 
-                  id_message.textContent = problem.detail
-                  //id_message.classList.remove('text-success')
-                  id_message.classList.add('text-danger')
-                  id_message.hidden = false
+                  id_alert.textContent = problem.detail
+                  id_alert.classList.remove('alert-success')
+                  id_alert.classList.add('alert-danger')
+                  id_alert.hidden = false
                   return
                 }
                 id_tick.hidden = false
                 id_spinner.hidden = true
-                id_message.textContent = 'We have received your enquiry. We will be in touch as soon as possible.'
-                //id_message.classList.add('text-success')
-                id_message.classList.remove('text-danger')
-                id_message.hidden = false
+                id_alert.textContent = 'We have received your enquiry. We will be in touch as soon as possible.'
+                id_alert.classList.add('alert-success')
+                id_alert.classList.remove('alert-danger')
+                id_alert.hidden = false
               }
             )
           }
index 6de27a1..14ab5de 100644 (file)
@@ -18,9 +18,8 @@ $footer-link-hover-color: darken($footer-link-color, 10%);
   }
 }
 
-// place a container div around entire page, and then use this
-// to make the background on navbar or footer appear full-width
-.extend-background {
+// use within a container to extend the background to left/right
+.container-background {
   margin-left: calc(-.5 * (100vw - 100%));
   margin-right: calc(-.5 * (100vw - 100%));
   padding-left: calc(.5 * (100vw - 100%));
@@ -35,13 +34,117 @@ $footer-link-hover-color: darken($footer-link-color, 10%);
   padding-right: .5 * $grid-gutter-width;
 }
 
-.sidebar {
-  position: sticky;
-  top: 0;
-  max-height: 100vh;
-  overflow-y: auto;
+// similar to bootstrap's navbar class, but appears to the side
+// responsive in a fairly sophisticated way, with 5 different modes:
+// >= xs: appears above content, toggleable, animates vertically
+// >= md: appears to left of content, toggleable, animates horizontally
+// >= xl: appears to left of content, always visible
+// >= 2xl: same as xl but has a right margin half the width of the sidebar
+// >= 3xl: same as xl but has a right margin the same width as the sidebar
+// (the last way gives a balanced look, as the content is along the middle)
+// note: flex item cannot be smaller than its contents with overflow visible,
+// so to be safe (when screen so small that a word cannot fit) make it hidden
+.sidebar-outer {
+  // note: the following properties are set via javascript using inline style:
+  // height: 0px if hidden and animation is vertical, otherwise content height
+  // flex-basis: 0px if hidden and animation is horizontal, otherwise 350px
+  // transition-property: flex-basis / height during animation, otherwise none
+  flex-grow: 0;
+  flex-shrink: 0;
+  padding-left: 0;
+  padding-right: 0;
+  overflow: hidden;
+  @include media-breakpoint-up(md) {
+    position: sticky;
+    top: 0;
+    max-height: 100vh;
+    overflow-y: auto;
+  }
+  transition: none .35s ease;
   background-color: $gray-200;
 }
+.sidebar-inner {
+  padding-left: $grid-gutter-width * .5;
+  padding-right: $grid-gutter-width * .5;
+  @include media-breakpoint-up(md) {
+    width: 350px;
+    min-height: 100vh;
+  }
+}
+.sidebar-toggle {
+  @include media-breakpoint-up(xl) {
+    display: none;
+  }
+}
+.sidebar-content {
+  overflow: hidden;
+  // the following gives an effect more like .container, as the text wrap
+  // becomes predictable after a certain screen size, but text is biased
+  // to the left on larger screens, I prefer to have it wider and centred
+  //max-width: 1280px;
+}
+.sidebar-dummy {
+  flex: 0 0 0px;
+  padding-left: 0px;
+  padding-right: 0px;
+  overflow: hidden;
+  @include media-breakpoint-up(2xl) {
+    flex-basis: 175px;
+  }
+  @include media-breakpoint-up(3xl) {
+    flex-basis: 350px;
+  }
+}
+
+// used in blog posts for a responsive N-column layout
+.rowN {
+  display: flex;
+  flex-wrap: wrap;
+  //margin-left: $grid-gutter-width * -.5;
+  //margin-right: $grid-gutter-width * -.5;
+}
+.rowN-header {
+  display: none;
+  @include media-breakpoint-up(2xl) {
+    display: flex;
+  }
+}
+.col7 {
+  padding-left: $grid-gutter-width * .5;
+  padding-right: $grid-gutter-width * .5;
+  flex: 1 0 100%;
+  @include media-breakpoint-up(2xl) {
+    flex-basis: 14.285714%;
+    max-width: 14.285714%;
+  }
+  border: 2px solid white;
+  hyphens: auto;
+}
+.col7-2 {
+  padding-left: $grid-gutter-width * .5;
+  padding-right: $grid-gutter-width * .5;
+  flex: 1 0 100%;
+  @include media-breakpoint-up(2xl) {
+    flex-basis: 28.571429%;
+    max-width: 28.571429%;
+  }
+  border: 2px solid white;
+  hyphens: auto;
+}
+.colN-data {
+  background-color: theme-color-level("primary", $alert-bg-level);
+}
+.colN-header {
+  background-color: theme-color-level("success", $alert-bg-level);
+}
+
+// use after .img-fluid
+.img-fluid-1-3 {
+  max-width: 33.333333%
+}
+.img-fluid-2-3 {
+  max-width: 66.666667%
+}
 
 // needed for svg icons inside buttons, card headers, etc
 // creates an inline element with correct width but no height
@@ -76,16 +179,26 @@ $icon-sizes: (
   font-family: $font-family-serif;
 }
 
-// apply this to table to get an icon with vertically centred text next to it
-.icon-and-text {
-  vertical-align: middle;
-  td {
-    padding: .25em .5em;
-  }
+// use this with bootstrap's grid system to make an icon with text beside it
+// the icon can take up to 1/2 of the width, but not more than its N-px width
+// note: flex item cannot be smaller than its contents with overflow visible,
+// so to be safe (when screen so small that a word cannot fit) make it hidden
+// note: col has position: relative, and I believe this is so that stretched
+// links will fill the column, but we want them to fill the row in this case
+.col-icon128 {
+  flex: 0 1 calc(128px + $grid-gutter-width);
+  overflow: hidden;
+  position: static;
 }
-
-.search-results li {
-  margin-bottom: .5em;
+.col-icon256 {
+  flex: 0 1 calc(256px + $grid-gutter-width);
+  overflow: hidden;
+  position: static;
+}
+.col-text {
+  flex: 1 0 50%;
+  overflow: hidden;
+  position: static;
 }
 
 .page-header {
index 102bf7a..187bd88 100644 (file)
   }
 }
 
+// Nick (for stretched links)
+.nav-link-inner {
+  display: block;
+  padding: $nav-link-padding-y $nav-link-padding-x;
+  text-decoration: if($link-decoration == none, null, none);
+}
+.nav-link-outer {
+  @include hover-focus() {
+    text-decoration: none;
+    background: $nav-link-hover-bg; // Nick
+  }
+
+  // Disabled state lightens text
+  &.disabled {
+    color: $nav-link-disabled-color;
+    pointer-events: none;
+    cursor: default;
+  }
+}
+
 //
 // Tabs
 //
index 5339e13..d93a6c5 100644 (file)
@@ -155,7 +155,7 @@ blockquote {
 
 b,
 strong {
-  font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari
+  font-weight: $font-weight-bold/*er*/; // Add the correct font weight in Chrome, Edge, and Safari
 }
 
 small {
index 23f1a51..49f87b2 100644 (file)
@@ -198,7 +198,9 @@ $grid-breakpoints: (
   md: 768px,
   lg: 992px,
   xl: 1200px,
-  //xxl: 1500px // Nick (for sidebar)
+  // Nick (for sidebar)
+  //2xl: 1500px,
+  //3xl: 1800px
 ) !default;
 
 @include _assert-ascending($grid-breakpoints, "$grid-breakpoints");
@@ -214,7 +216,9 @@ $container-max-widths: (
   md: 720px,
   lg: 960px,
   xl: 1140px,
-  //xxl: 1420px // Nick (for sidebar)
+  // Nick (for sidebar)
+  //2xl: 1420px,
+  //3xl: 1700px
 ) !default;
 
 @include _assert-ascending($container-max-widths, "$container-max-widths");
diff --git a/css/ie10-viewport-bug-workaround.css.min b/css/ie10-viewport-bug-workaround.css.min
deleted file mode 100644 (file)
index 4b9518e..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-/*!
- * IE10 viewport hack for Surface/desktop Windows 8 bug
- * Copyright 2014-2015 Twitter, Inc.
- * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
- */
-
-/*
- * See the Getting Started docs for more information:
- * http://getbootstrap.com/getting-started/#support-ie10-width
- */
-@-ms-viewport     { width: device-width; }
-@-o-viewport      { width: device-width; }
-@viewport         { width: device-width; }
diff --git a/css/jsdoc-default.css.less b/css/jsdoc-default.css.less
deleted file mode 100644 (file)
index b066acb..0000000
+++ /dev/null
@@ -1,364 +0,0 @@
-//@font-face {
-//    font-family: 'Open Sans';
-//    font-weight: normal;
-//    font-style: normal;
-//    src: url('../jsdoc/fonts/OpenSans-Regular-webfont.eot');
-//    src:
-//        local('Open Sans'),
-//        local('OpenSans'),
-//        url('../jsdoc/fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'),
-//        url('../jsdoc/fonts/OpenSans-Regular-webfont.woff') format('woff'),
-//        url('../jsdoc/fonts/OpenSans-Regular-webfont.svg#open_sansregular') format('svg');
-//}
-//
-//@font-face {
-//    font-family: 'Open Sans Light';
-//    font-weight: normal;
-//    font-style: normal;
-//    src: url('../jsdoc/fonts/OpenSans-Light-webfont.eot');
-//    src:
-//        local('Open Sans Light'),
-//        local('OpenSans Light'),
-//        url('../jsdoc/fonts/OpenSans-Light-webfont.eot?#iefix') format('embedded-opentype'),
-//        url('../jsdoc/fonts/OpenSans-Light-webfont.woff') format('woff'),
-//        url('../jsdoc/fonts/OpenSans-Light-webfont.svg#open_sanslight') format('svg');
-//}
-
-.jsdoc {
-
-//html
-//{
-//    overflow: auto;
-//    background-color: #fff;
-//    font-size: 14px;
-//}
-//
-//body
-//{
-//    font-family: 'Open Sans', sans-serif;
-//    line-height: 1.5;
-//    color: #4d4e53;
-//    background-color: white;
-//}
-//
-//a, a:visited, a:active {
-//    color: #0095dd;
-//    text-decoration: none;
-//}
-//
-//a:hover {
-//    text-decoration: underline;
-//}
-
-header
-{
-    display: block;
-    padding: 0px; //0px 4px;
-}
-
-tt, code, kbd, samp {
-    font-family: Consolas, Monaco, 'Andale Mono', monospace;
-}
-
-.class-description {
-    font-size: 130%;
-    line-height: 140%;
-    margin-bottom: 1em;
-    margin-top: 1em;
-}
-
-.class-description:empty {
-    margin: 0;
-}
-
-#main {
-    float: left;
-    width: 77.5%; //70%;
-}
-
-article dl {
-    margin-bottom: 40px;
-}
-
-article img {
-  max-width: 100%;
-}
-
-section
-{
-    display: block;
-    background-color: #fff;
-    padding: 12px 0px; //12px 24px;
-    border-bottom: 1px solid #ccc;
-//    margin-right: 30px;
-}
-
-.variation {
-    display: none;
-}
-
-.signature-attributes {
-    font-size: 60%;
-    color: #aaa;
-    font-style: italic;
-    font-weight: lighter;
-}
-
-nav
-{
-    display: block;
-    float: right;
-    margin-top: 28px;
-    width: 20%; //30%;
-    box-sizing: border-box;
-//    border-left: 1px solid #ccc;
-    padding-left: 16px;
- background-color: #eee;
- padding-bottom: 16px;
-}
-
-//nav ul {
-//    font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif;
-//    font-size: 100%;
-//    line-height: 17px;
-//    padding: 0;
-//    margin: 0;
-//    list-style-type: none;
-//}
-//
-//nav ul a, nav ul a:visited, nav ul a:active {
-//    font-family: Consolas, Monaco, 'Andale Mono', monospace;
-//    line-height: 18px;
-//    color: #4D4E53;
-//}
-
-nav h3 {
-    margin-top: 12px;
-}
-
-nav li {
-    margin-top: 6px;
-}
-
-footer {
-    display: block;
-    padding: 6px 0px; //6px;
-    margin-top: 12px;
-    font-style: italic;
-    font-size: 90%;
-}
-
-//h1, h2, h3, h4 {
-//    font-weight: 200;
-//    margin: 0;
-//}
-//
-//h1
-//{
-//    font-family: 'Open Sans Light', sans-serif;
-//    font-size: 48px;
-//    letter-spacing: -2px;
-//    margin: 12px 24px 20px;
-//}
-//
-//h2, h3.subsection-title
-//{
-//    font-size: 30px;
-//    font-weight: 700;
-//    letter-spacing: -1px;
-//    margin-bottom: 12px;
-//}
-//
-//h3
-//{
-//    font-size: 24px;
-//    letter-spacing: -0.5px;
-//    margin-bottom: 12px;
-//}
-//
-//h4
-//{
-//    font-size: 18px;
-//    letter-spacing: -0.33px;
-//    margin-bottom: 12px;
-//    color: #4d4e53;
-//}
-//
-//h5, .container-overview .subsection-title
-//{
-//    font-size: 120%;
-//    font-weight: bold;
-//    letter-spacing: -0.01em;
-//    margin: 8px 0 3px 0;
-//}
-//
-//h6
-//{
-//    font-size: 100%;
-//    letter-spacing: -0.01em;
-//    margin: 6px 0 3px 0;
-//    font-style: italic;
-//}
-
-table
-{
-    border-spacing: 0;
-    border: 0;
-    border-collapse: collapse;
-}
-
-td, th
-{
-    border: 1px solid #ddd;
-    margin: 0px;
-    text-align: left;
-    vertical-align: top;
-    padding: 4px 6px;
-    display: table-cell;
-}
-
-thead tr
-{
-    background-color: #ddd;
-    font-weight: bold;
-}
-
-th { border-right: 1px solid #aaa; }
-tr > th:last-child { border-right: 1px solid #ddd; }
-
-.ancestors, .attribs { color: #999; }
-.ancestors a, .attribs a
-{
-    color: #999 !important;
-    text-decoration: none;
-}
-
-.clear
-{
-    clear: both;
-}
-
-.important
-{
-    font-weight: bold;
-    color: #950B02;
-}
-
-.yes-def {
-    text-indent: -1000px;
-}
-
-.type-signature {
-    color: #aaa;
-}
-
-//.name, .signature {
-//    font-family: Consolas, Monaco, 'Andale Mono', monospace;
-//}
-
-.details { margin-top: 14px; border-left: 2px solid #DDD; }
-.details dt { width: 120px; float: left; padding-left: 10px;  padding-top: 6px; }
-.details dd { margin-left: 70px; }
-.details ul { margin: 0; }
-.details ul { list-style-type: none; }
-.details li { margin-left: 30px; padding-top: 6px; }
-.details pre.prettyprint { margin: 0 }
-.details .object-value { padding-top: 0; }
-
-.description {
-    margin-bottom: 1em;
-    margin-top: 1em;
-}
-
-.code-caption
-{
-    font-style: italic;
-    font-size: 107%;
-    margin: 0;
-}
-
-.source
-{
-    border: 1px solid #ddd;
-    width: 77.5%; //80%;
-    overflow: auto;
-}
-
-.prettyprint.source {
-    width: inherit;
-}
-
-.source code
-{
-    font-size: 100%;
-    line-height: 18px;
-    display: block;
-    padding: 4px 12px;
-    margin: 0;
-    background-color: #fff;
-    color: #4D4E53;
-}
-
-.prettyprint code span.line
-{
-  display: inline-block;
-}
-
-.prettyprint.linenums
-{
-  padding-left: 70px;
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  -ms-user-select: none;
-  user-select: none;
-}
-
-.prettyprint.linenums ol
-{
-  padding-left: 0;
-}
-
-.prettyprint.linenums li
-{
-  border-left: 3px #ddd solid;
-}
-
-.prettyprint.linenums li.selected,
-.prettyprint.linenums li.selected *
-{
-  background-color: lightyellow;
-}
-
-.prettyprint.linenums li *
-{
-  -webkit-user-select: text;
-  -moz-user-select: text;
-  -ms-user-select: text;
-  user-select: text;
-}
-
-.params .name, .props .name, .name code {
-    color: #4D4E53;
-    font-family: Consolas, Monaco, 'Andale Mono', monospace;
-    font-size: 100%;
-}
-
-.params td.description > p:first-child,
-.props td.description > p:first-child
-{
-    margin-top: 0;
-    padding-top: 0;
-}
-
-.params td.description > p:last-child,
-.props td.description > p:last-child
-{
-    margin-bottom: 0;
-    padding-bottom: 0;
-}
-
-.disabled {
-    color: #454545;
-}
-
-}
diff --git a/css/login.css.min b/css/login.css.min
deleted file mode 100644 (file)
index 7be2329..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-body {
-  padding-top: 40px;
-  padding-bottom: 40px;
-  background-color: #eee;
-}
-
-.form-login {
-  max-width: 330px;
-  padding: 15px;
-  margin: 0 auto;
-}
-.form-login .form-login-heading,
-.form-login .checkbox {
-  margin-bottom: 10px;
-}
-.form-login .checkbox {
-  font-weight: normal;
-}
-.form-login .form-control {
-  position: relative;
-  height: auto;
-  -webkit-box-sizing: border-box;
-     -moz-box-sizing: border-box;
-          box-sizing: border-box;
-  padding: 10px;
-  font-size: 16px;
-}
-.form-login .form-control:focus {
-  z-index: 2;
-}
-.form-login input[type="email"] {
-  margin-bottom: -1px;
-  border-bottom-right-radius: 0;
-  border-bottom-left-radius: 0;
-}
-.form-login input[type="password"] {
-  margin-bottom: 10px;
-  border-top-left-radius: 0;
-  border-top-right-radius: 0;
-}
index 11b72f1..2433544 100644 (file)
@@ -66,7 +66,7 @@ return async env => {
 
       if (signed_in_as !== undefined) {
         // signed in
-        p {'Your given names are visible to other users if you comment on our blog. Your email and family name remain private. If your name is one word or does not fit given names/family name pattern, then please enter given names only.'}
+        p/*.mt-3*/ {'Your given names are visible to other users if you comment on our blog. Your email and family name remain private. If your name is one word or does not fit given names/family name pattern, then please enter given names only.'}
 
         div.accordion#accordion(role="tablist" aria-multiselectable="true") {
           div.card#card-1 {
@@ -160,7 +160,7 @@ return async env => {
                     'Save'
                   }
 
-                p.'mt-3'.mb-0#card-1-message(hidden) {}
+                div.alert.alert-danger.'mt-3'.mb-0#card-1-alert(hidden) {}
               }
             }
           }
@@ -215,7 +215,7 @@ return async env => {
                   'Save'
                 }
 
-                p.'mt-3'.mb-0#card-2-message(hidden) {}
+                div.alert.alert-danger.'mt-3'.mb-0#card-2-alert(hidden) {}
               }
             }
           }
@@ -225,7 +225,7 @@ return async env => {
       }
       else {
         // signed out
-        p {'For account maintenance, please click on one of the options below.'}
+        p/*.mt-3*/ {'For account maintenance, please click on one of the options below.'}
 
         let transaction = await env.site.database.Transaction()
         try {
@@ -234,25 +234,26 @@ return async env => {
           let children = await p.get('children')
           let menu = await p.get_json('menu')
   
-          ul.nav.flex-column {
+          nav {
             for (let i = 0; i < menu.length; ++i) {
               let name = menu[i]
               let q = await children.get(name)
-  
-              li.nav-item {
-                a.nav-link(href=`${name}/index.html`) {
-                  table.icon-and-text {
-                    tr {
-                      td {
-                        _out.push(
-                          await env.site.get_min_svg(await q.get_json('icon'))
-                        )
-                      }
-                      td {
-                        span.h2{
-                          `${await q.get_json('title')}`
-                        }
-                      }
+
+              // note: make the row a link to show hover background at edges
+              // (similar to the grid-gutter-background class I use elsewhere)
+              // note: position: relative sets the target for stretched link
+              div.row.flex-nowrap.align-items-center.nav-link-outer.position-relative {
+                div.col.col-icon128 {
+                  div.img-fluid.py-3 {
+                    _out.push(
+                      await env.site.get_min_svg(await q.get_json('icon'))
+                    )
+                  }
+                }
+                div.col.col-text {
+                  a.nav-link-inner.stretched-link(href=`${name}/index.html`) {
+                    span.h2{
+                      `${await q.get_json('title')}`
                     }
                   }
                 }
@@ -285,24 +286,24 @@ return async env => {
             () => {
               let id_accordion = document.getElementById('accordion')
               let id_card_1 = document.getElementById('card-1')
+              let id_card_1_alert = document.getElementById('card-1-alert')
               let id_card_1_collapse = document.getElementById('card-1-collapse')
               let id_card_1_cross = document.getElementById('card-1-cross')
               let id_card_1_form = document.getElementById('card-1-form')
               let id_card_1_heading = document.getElementById('card-1-heading')
               let id_card_1_icon = document.getElementById('card-1-icon')
-              let id_card_1_message = document.getElementById('card-1-message')
               let id_card_1_revert = document.getElementById('card-1-revert')
               let id_card_1_save = document.getElementById('card-1-save')
               let id_card_1_spinner = document.getElementById('card-1-spinner')
               let id_card_1_tick = document.getElementById('card-1-tick')
               let id_card_2 = document.getElementById('card-2')
+              let id_card_2_alert = document.getElementById('card-2-alert')
               let id_card_2_clear = document.getElementById('card-2-clear')
               let id_card_2_collapse = document.getElementById('card-2-collapse')
               let id_card_2_cross = document.getElementById('card-2-cross')
               let id_card_2_form = document.getElementById('card-2-form')
               let id_card_2_heading = document.getElementById('card-2-heading')
               let id_card_2_icon = document.getElementById('card-2-icon')
-              let id_card_2_message = document.getElementById('card-2-message')
               let id_card_2_save = document.getElementById('card-2-save')
               let id_card_2_spinner = document.getElementById('card-2-spinner')
               let id_card_2_tick = document.getElementById('card-2-tick')
@@ -352,7 +353,7 @@ return async env => {
                 id_card_1_tick.hidden = true
                 id_card_1_cross.hidden = true
                 id_card_1_spinner.hidden = true
-                id_card_1_message.hidden = true
+                id_card_1_alert.hidden = true
               }
 
               id_given_names.addEventListener('input', card_1_edited)
@@ -375,7 +376,7 @@ return async env => {
                   id_card_1_tick.hidden = true
                   id_card_1_cross.hidden = true
                   id_card_1_spinner.hidden = true
-                  id_card_1_message.hidden = true
+                  id_card_1_alert.hidden = true
                 }
               )
 
@@ -386,8 +387,8 @@ return async env => {
                   id_card_1_tick.hidden = true
                   id_card_1_cross.hidden = true
                   id_card_1_spinner.hidden = true
-                  // the below causes an ugly flicker, so just keep the message
-                  //id_card_1_message.hidden = true
+                  // the below causes an ugly flicker, so just keep the alert
+                  //id_card_1_alert.hidden = true
 
                   if (!id_card_1_form.checkValidity()) {
                     id_card_1_form.classList.add('was-validated');
@@ -416,10 +417,10 @@ return async env => {
                     id_card_1_cross.hidden = false
                     id_card_1_spinner.hidden = true
 
-                    id_card_1_message.textContent = problem.detail
-                    //id_card_1_message.classList.remove('text-success')
-                    id_card_1_message.classList.add('text-danger')
-                    id_card_1_message.hidden = false
+                    id_card_1_alert.textContent = problem.detail
+                    //id_card_1_alert.classList.remove('alert-success')
+                    //id_card_1_alert.classList.add('alert-danger')
+                    id_card_1_alert.hidden = false
                     return
                   }
                   id_card_1_tick.hidden = false
@@ -438,7 +439,7 @@ return async env => {
                   //id_card_1_tick.hidden = true
                   //id_card_1_cross.hidden = true
                   //id_card_1_spinner.hidden = true
-                  id_card_1_message.hidden = true
+                  id_card_1_alert.hidden = true
                 }
               )
 
@@ -453,7 +454,7 @@ return async env => {
                 id_card_2_tick.hidden = true
                 id_card_2_cross.hidden = true
                 id_card_2_spinner.hidden = true
-                id_card_2_message.hidden = true
+                id_card_2_alert.hidden = true
               }
 
               id_old_password.addEventListener('input', card_2_edited)
@@ -472,7 +473,7 @@ return async env => {
                   id_card_2_tick.hidden = true
                   id_card_2_cross.hidden = true
                   id_card_2_spinner.hidden = true
-                  id_card_2_message.hidden = true
+                  id_card_2_alert.hidden = true
                 }
               )
 
@@ -483,8 +484,8 @@ return async env => {
                   id_card_2_tick.hidden = true
                   id_card_2_cross.hidden = true
                   id_card_2_spinner.hidden = true
-                  // the below causes an ugly flicker, so just keep the message
-                  //id_card_2_message.hidden = true
+                  // the below causes an ugly flicker, so just keep the alert
+                  //id_card_2_alert.hidden = true
 
                   if (!id_card_2_form.checkValidity()) {
                     id_card_2_form.classList.add('was-validated');
@@ -510,10 +511,10 @@ return async env => {
                     id_card_2_cross.hidden = false
                     id_card_2_spinner.hidden = true
 
-                    id_card_2_message.textContent = problem.detail
-                    //id_card_2_message.classList.remove('text-success')
-                    id_card_2_message.classList.add('text-danger')
-                    id_card_2_message.hidden = false
+                    id_card_2_alert.textContent = problem.detail
+                    //id_card_2_alert.classList.remove('alert-success')
+                    //id_card_2_alert.classList.add('alert-danger')
+                    id_card_2_alert.hidden = false
                     return
                   }
                   id_card_2_tick.hidden = false
@@ -529,7 +530,7 @@ return async env => {
                   //id_card_2_tick.hidden = true
                   //id_card_2_cross.hidden = true
                   //id_card_2_spinner.hidden = true
-                  id_card_2_message.hidden = true
+                  id_card_2_alert.hidden = true
                 }
               )
             }
index e06abce..348b3d2 100644 (file)
@@ -38,7 +38,7 @@ return async env => {
     async _out => {
       await breadcrumbs(env, _out)
 
-      p {'To reset your password, please enter new details below and we will send you a password reset link.'}
+      p/*.mt-3*/ {'To reset your password, please enter new details below and we will send you a password reset link.'}
 
       form#form {
         div.row {
@@ -60,7 +60,7 @@ return async env => {
       }
 
       if (email.length)
-        button.btn.btn-success#password-reset(type="button") {
+        button.btn.btn-success.mb-3#password-reset(type="button") {
           div.icon24-outer.mr-2#icon {
             div.icon24-inner {_out.push(fa_envelope)}
           }
@@ -78,7 +78,7 @@ return async env => {
           'Password reset'
         }
       else
-        button.btn.btn-success#password-reset(type="button" disabled) {
+        button.btn.btn-success.mb-3#password-reset(type="button" disabled) {
           div.icon24-outer.mr-2#icon {
             div.icon24-inner {_out.push(fa_envelope)}
           }
@@ -96,9 +96,9 @@ return async env => {
           'Password reset'
         }
 
-      p.'mt-3'.mb-0#message(hidden) {}
+      div.alert#alert(hidden) {}
 
-      p.text-muted.mt-3 {'* These fields are required.'}
+      p.text-muted {'* These fields are required.'}
     },
     // scripts
     async _out => {
@@ -108,11 +108,11 @@ return async env => {
         document.addEventListener(
           'DOMContentLoaded',
           () => {
+            let id_alert = document.getElementById('alert')
             let id_cross = document.getElementById('cross')
             let id_email = document.getElementById('email')
             let id_form = document.getElementById('form')
             let id_icon = document.getElementById('icon')
-            let id_message = document.getElementById('message')
             let id_new_password = document.getElementById('new-password')
             let id_password_reset = document.getElementById('password-reset')
             let id_spinner = document.getElementById('spinner')
@@ -126,7 +126,7 @@ return async env => {
               id_tick.hidden = true
               id_cross.hidden = true
               id_spinner.hidden = true
-              id_message.hidden = true
+              id_alert.hidden = true
             }
 
             id_email.addEventListener('input', edited)
@@ -139,8 +139,8 @@ return async env => {
                 id_tick.hidden = true
                 id_cross.hidden = true
                 id_spinner.hidden = true
-                // the below causes an ugly flicker, so just keep the message
-                //id_message.hidden = true
+                // the below causes an ugly flicker, so just keep the alert
+                //id_alert.hidden = true
 
                 if (!id_form.checkValidity()) {
                   id_form.classList.add('was-validated');
@@ -169,18 +169,18 @@ return async env => {
                   id_cross.hidden = false
                   id_spinner.hidden = true
 
-                  id_message.textContent = problem.detail
-                  //id_message.classList.remove('text-success')
-                  id_message.classList.add('text-danger')
-                  id_message.hidden = false
+                  id_alert.textContent = problem.detail
+                  id_alert.classList.remove('alert-success')
+                  id_alert.classList.add('alert-danger')
+                  id_alert.hidden = false
                   return
                 }
                 id_tick.hidden = false
                 id_spinner.hidden = true
-                id_message.textContent = `Password reset link has been sent to "${email}". Please check your email for next steps.`
-                //id_message.classList.add('text-success')
-                id_message.classList.remove('text-danger')
-                id_message.hidden = false
+                id_alert.textContent = `Password reset link has been sent to "${email}". Please check your email for next steps.`
+                id_alert.classList.add('alert-success')
+                id_alert.classList.remove('alert-danger')
+                id_alert.hidden = false
               }
             )
           }
index 935071e..5fbe706 100644 (file)
@@ -38,7 +38,7 @@ return async env => {
     async _out => {
       await breadcrumbs(env, _out)
 
-      p {'Your email is not yet verified. Check your email for next steps, or re-send the verification email below.'}
+      p/*.mt-3*/ {'Your email is not yet verified. Check your email for next steps, or re-send the verification email below.'}
 
       form#form {
         div.row.align-items-center {
@@ -92,7 +92,7 @@ return async env => {
         }
       }
 
-      p#message(hidden) {}
+      div.alert#alert(hidden) {}
     },
     // scripts
     async _out => {
@@ -102,11 +102,11 @@ return async env => {
         document.addEventListener(
           'DOMContentLoaded',
           () => {
+            let id_alert = document.getElementById('alert')
             let id_cross = document.getElementById('cross')
             let id_email = document.getElementById('email')
             let id_form = document.getElementById('form')
             let id_icon = document.getElementById('icon')
-            let id_message = document.getElementById('message')
             let id_send_verification_email = document.getElementById('send-verification-email')
             let id_spinner = document.getElementById('spinner')
             let id_tick = document.getElementById('tick')
@@ -118,7 +118,7 @@ return async env => {
               id_tick.hidden = true
               id_cross.hidden = true
               id_spinner.hidden = true
-              id_message.hidden = true
+              id_alert.hidden = true
             }
 
             id_email.addEventListener('input', edited)
@@ -130,8 +130,8 @@ return async env => {
                 id_tick.hidden = true
                 id_cross.hidden = true
                 id_spinner.hidden = true
-                // the below causes an ugly flicker, so just keep the message
-                //id_message.hidden = true
+                // the below causes an ugly flicker, so just keep the alert
+                //id_alert.hidden = true
 
                 if (!id_form.checkValidity()) {
                   id_form.classList.add('was-validated');
@@ -158,18 +158,18 @@ return async env => {
                   id_cross.hidden = false
                   id_spinner.hidden = true
 
-                  id_message.textContent = problem.detail
-                  //id_message.classList.remove('text-success')
-                  id_message.classList.add('text-danger')
-                  id_message.hidden = false
+                  id_alert.textContent = problem.detail
+                  id_alert.classList.remove('alert-success')
+                  id_alert.classList.add('alert-danger')
+                  id_alert.hidden = false
                   return
                 }
                 id_tick.hidden = false
                 id_spinner.hidden = true
-                id_message.textContent = `Email verification link has been sent to "${email}". Please check your email for next steps.`
-                //id_message.classList.add('text-success')
-                id_message.classList.remove('text-danger')
-                id_message.hidden = false
+                id_alert.textContent = `Email verification link has been sent to "${email}". Please check your email for next steps.`
+                id_alert.classList.add('alert-success')
+                id_alert.classList.remove('alert-danger')
+                id_alert.hidden = false
               }
             )
           }
index 27eb438..2f0e39c 100644 (file)
@@ -39,7 +39,7 @@ return async env => {
     async _out => {
       await breadcrumbs(env, _out)
 
-      p {'Signing up allows you to leave comments on our blog and receive communications from us.'}
+      p/*.mt-3*/ {'Signing up allows you to leave comments on our blog and receive communications from us.'}
 
       p {'Your given names are visible to other users if you comment on our blog. Your email and family name remain private. If your name is one word or does not fit given names/family name pattern, then please enter given names only.'}
 
@@ -146,7 +146,7 @@ return async env => {
                   'Create account'
                 }
 
-              p.'mt-3'.mb-0#card-1-message(hidden) {}
+              div.alert.'mt-3'.mb-0#card-1-alert(hidden) {}
             }
           }
         }
@@ -182,7 +182,7 @@ return async env => {
                 'Re-send email'
               }
 
-              p.'mt-3'.mb-0#card-2-message(hidden) {}
+              div.alert.'mt-3'.mb-0#card-2-alert(hidden) {}
             }
           }
         }
@@ -200,20 +200,20 @@ return async env => {
           () => {
             let id_accordion = document.getElementById('accordion')
             let id_card_1 = document.getElementById('card-1')
+            let id_card_1_alert = document.getElementById('card-1-alert')
             let id_card_1_collapse = document.getElementById('card-1-collapse')
             let id_card_1_create_account = document.getElementById('card-1-create-account')
             let id_card_1_cross = document.getElementById('card-1-cross')
             let id_card_1_heading = document.getElementById('card-1-heading')
-            let id_card_1_message = document.getElementById('card-1-message')
             let id_card_1_new = document.getElementById('card-1-new')
             let id_card_1_spinner = document.getElementById('card-1-spinner')
             let id_card_1_tick = document.getElementById('card-1-tick')
             let id_card_2 = document.getElementById('card-2')
+            let id_card_2_alert = document.getElementById('card-2-alert')
             let id_card_2_back = document.getElementById('card-2-back')
             let id_card_2_collapse = document.getElementById('card-2-collapse')
             let id_card_2_cross = document.getElementById('card-2-cross')
             let id_card_2_heading = document.getElementById('card-2-heading')
-            let id_card_2_message = document.getElementById('card-2-message')
             let id_card_2_resend_email = document.getElementById('card-2-resend-email')
             let id_card_2_spinner = document.getElementById('card-2-spinner')
             let id_card_2_tick = document.getElementById('card-2-tick')
@@ -266,13 +266,13 @@ return async env => {
               id_card_1_tick.hidden = true
               id_card_1_cross.hidden = true
               id_card_1_spinner.hidden = true
-              id_card_1_message.hidden = true
+              id_card_1_alert.hidden = true
 
               id_card_2_resend_email.disabled = true
               id_card_2_tick.hidden = true
               id_card_2_cross.hidden = true
               id_card_2_spinner.hidden = true
-              id_card_2_message.hidden = true
+              id_card_2_alert.hidden = true
             }
 
             let details
@@ -280,8 +280,8 @@ return async env => {
               id_card_1_tick.hidden = true
               id_card_1_cross.hidden = true
               id_card_1_spinner.hidden = true
-              // the below causes an ugly flicker, so just keep the message
-              //id_card_1_message.hidden = true
+              // the below causes an ugly flicker, so just keep the alert
+              //id_card_1_alert.hidden = true
 
               if (!id_form.checkValidity()) {
                 // workaround for https://github.com/twbs/bootstrap/issues/23454
@@ -325,20 +325,20 @@ return async env => {
                 id_card_1_cross.hidden = false
                 id_card_1_spinner.hidden = true
 
-                id_card_1_message.textContent = problem.detail
-                //id_card_1_message.classList.remove('text-success')
-                id_card_1_message.classList.add('text-danger')
-                id_card_1_message.hidden = false
+                id_card_1_alert.textContent = problem.detail
+                id_card_1_alert.classList.remove('alert-success')
+                id_card_1_alert.classList.add('alert-danger')
+                id_card_1_alert.hidden = false
 
                 $('#card-1-collapse').collapse('show')
                 return false
               }
               id_card_1_tick.hidden = false
               id_card_1_spinner.hidden = true
-              id_card_1_message.textContent = `Your account with email "${details.email}" has been created.`
-              //id_card_1_message.classList.add('text-success')
-              id_card_1_message.classList.remove('text-danger')
-              id_card_1_message.hidden = false
+              id_card_1_alert.textContent = `Your account with email "${details.email}" has been created.`
+              id_card_1_alert.classList.add('alert-success')
+              id_card_1_alert.classList.remove('alert-danger')
+              id_card_1_alert.hidden = false
 
               id_card_2_resend_email.disabled = false
               id_card_2.scrollIntoView()
@@ -349,8 +349,8 @@ return async env => {
               id_card_2_tick.hidden = true
               id_card_2_cross.hidden = true
               id_card_2_spinner.hidden = false
-              // the below causes an ugly flicker, so just keep the message
-              //id_card_2_message.hidden = true
+              // the below causes an ugly flicker, so just keep the alert
+              //id_card_2_alert.hidden = true
 
               try {
                 await api_call(
@@ -364,10 +364,10 @@ return async env => {
                 id_card_2_cross.hidden = false
                 id_card_2_spinner.hidden = true
 
-                id_card_2_message.textContent = problem.detail
-                //id_card_2_message.classList.remove('text-success')
-                id_card_2_message.classList.add('text-danger')
-                id_card_2_message.hidden = false
+                id_card_2_alert.textContent = problem.detail
+                id_card_2_alert.classList.remove('alert-success')
+                id_card_2_alert.classList.add('alert-danger')
+                id_card_2_alert.hidden = false
 
                 $('#card-2-collapse').collapse('show')
                 return false
@@ -375,10 +375,10 @@ return async env => {
               id_card_2_tick.hidden = false
               id_card_2_spinner.hidden = true
 
-              id_card_2_message.textContent = `Email verification link has been sent to "${details.email}". Please check your email for next steps.`
-              //id_card_2_message.classList.add('text-success')
-              id_card_2_message.classList.remove('text-danger')
-              id_card_2_message.hidden = false
+              id_card_2_alert.textContent = `Email verification link has been sent to "${details.email}". Please check your email for next steps.`
+              id_card_2_alert.classList.add('alert-success')
+              id_card_2_alert.classList.remove('alert-danger')
+              id_card_2_alert.hidden = false
               return true
             }
 
index e618f33..679a126 100644 (file)
@@ -45,7 +45,7 @@ return async env => {
     async _out => {
       await breadcrumbs(env, _out)
 
-      p {'You will need to verify your email address via an emailed link before you can sign in to your account.'}
+      p/*.mt-3*/ {'You will need to verify your email address via an emailed link before you can sign in to your account.'}
 
       form#form {
         div.row {
@@ -67,7 +67,7 @@ return async env => {
       }
 
       if (email.length || details.length)
-        button.btn.btn-success#verify-email(type="button") {
+        button.btn.btn-success.mb-3#verify-email(type="button") {
           div.icon24-outer.mr-2#icon {
             div.icon24-inner {_out.push(fa_user_check)}
           }
@@ -85,7 +85,7 @@ return async env => {
           'Verify email'
         }
       else
-        button.btn.btn-success#verify-email(type="button" disabled) {
+        button.btn.btn-success.mb-3#verify-email(type="button" disabled) {
           div.icon24-outer.mr-2#icon {
             div.icon24-inner {_out.push(fa_user_check)}
           }
@@ -103,9 +103,9 @@ return async env => {
           'Verify email'
         }
 
-      p.'mt-3'.mb-0#message(hidden) {}
+      div.alert#alert(hidden) {}
 
-      p.text-muted.mt-3 {'* These fields are required.'}
+      p.text-muted {'* These fields are required.'}
     },
     // scripts
     async _out => {
@@ -115,12 +115,12 @@ return async env => {
         document.addEventListener(
           'DOMContentLoaded',
           () => {
+            let id_alert = document.getElementById('alert')
             let id_cross = document.getElementById('cross')
             let id_email = document.getElementById('email')
             let id_form = document.getElementById('form')
             let id_icon = document.getElementById('icon')
             let id_link_code = document.getElementById('link-code')
-            let id_message = document.getElementById('message')
             let id_spinner = document.getElementById('spinner')
             let id_tick = document.getElementById('tick')
             let id_verify_email = document.getElementById('verify-email')
@@ -133,7 +133,7 @@ return async env => {
               id_tick.hidden = true
               id_cross.hidden = true
               id_spinner.hidden = true
-              id_message.hidden = true
+              id_alert.hidden = true
             }
 
             id_email.addEventListener('input', edited)
@@ -146,8 +146,8 @@ return async env => {
                 id_tick.hidden = true
                 id_cross.hidden = true
                 id_spinner.hidden = true
-                // the below causes an ugly flicker, so just keep the message
-                //id_message.hidden = true
+                // the below causes an ugly flicker, so just keep the alert
+                //id_alert.hidden = true
 
                 if (!id_form.checkValidity()) {
                   id_form.classList.add('was-validated');
@@ -176,18 +176,18 @@ return async env => {
                   id_cross.hidden = false
                   id_spinner.hidden = true
 
-                  id_message.textContent = problem.detail
-                  //id_message.classList.remove('text-success')
-                  id_message.classList.add('text-danger')
-                  id_message.hidden = false
+                  id_alert.textContent = problem.detail
+                  id_alert.classList.remove('alert-success')
+                  id_alert.classList.add('alert-danger')
+                  id_alert.hidden = false
                   return
                 }
                 id_tick.hidden = false
                 id_spinner.hidden = true
-                id_message.textContent = `Your email "${email}" has been verified. You can now sign in.`
-                //id_message.classList.add('text-success')
-                id_message.classList.remove('text-danger')
-                id_message.hidden = false
+                id_alert.textContent = `Your email "${email}" has been verified. You can now sign in.`
+                id_alert.classList.add('alert-success')
+                id_alert.classList.remove('alert-danger')
+                id_alert.hidden = false
               }
             )
           }
index 7b42eb1..38ee433 100644 (file)
@@ -45,7 +45,7 @@ return async env => {
     async _out => {
       await breadcrumbs(env, _out)
 
-      p {'You will need to verify your new password via an emailed link before you can use it to sign in to your account.'}
+      p/*.mt-3*/ {'You will need to verify your new password via an emailed link before you can use it to sign in to your account.'}
 
       form#form {
         div.row {
@@ -67,7 +67,7 @@ return async env => {
       }
 
       if (email.length || details.length)
-        button.btn.btn-success#verify-password(type="button") {
+        button.btn.btn-success.mb-3#verify-password(type="button") {
           div.icon24-outer.mr-2#icon {
             div.icon24-inner {_out.push(fa_user_check)}
           }
@@ -85,7 +85,7 @@ return async env => {
           'Verify password'
         }
       else
-        button.btn.btn-success#verify-password(type="button" disabled) {
+        button.btn.btn-success.mb-3#verify-password(type="button" disabled) {
           div.icon24-outer.mr-2#icon {
             div.icon24-inner {_out.push(fa_user_check)}
           }
@@ -103,9 +103,9 @@ return async env => {
           'Verify password'
         }
 
-      p.'mt-3'.mb-0#message(hidden) {}
+      div.alert#alert(hidden) {}
 
-      p.text-muted.mt-3 {'* These fields are required.'}
+      p.text-muted {'* These fields are required.'}
     },
     // scripts
     async _out => {
@@ -115,12 +115,12 @@ return async env => {
         document.addEventListener(
           'DOMContentLoaded',
           () => {
+            let id_alert = document.getElementById('alert')
             let id_cross = document.getElementById('cross')
             let id_email = document.getElementById('email')
             let id_form = document.getElementById('form')
             let id_icon = document.getElementById('icon')
             let id_link_code = document.getElementById('link-code')
-            let id_message = document.getElementById('message')
             let id_spinner = document.getElementById('spinner')
             let id_tick = document.getElementById('tick')
             let id_verify_password = document.getElementById('verify-password')
@@ -133,7 +133,7 @@ return async env => {
               id_tick.hidden = true
               id_cross.hidden = true
               id_spinner.hidden = true
-              id_message.hidden = true
+              id_alert.hidden = true
             }
 
             id_email.addEventListener('input', edited)
@@ -146,8 +146,8 @@ return async env => {
                 id_tick.hidden = true
                 id_cross.hidden = true
                 id_spinner.hidden = true
-                // the below causes an ugly flicker, so just keep the message
-                //id_message.hidden = true
+                // the below causes an ugly flicker, so just keep the alert
+                //id_alert.hidden = true
 
                 if (!id_form.checkValidity()) {
                   id_form.classList.add('was-validated');
@@ -176,18 +176,18 @@ return async env => {
                   id_cross.hidden = false
                   id_spinner.hidden = true
 
-                  id_message.textContent = problem.detail
-                  //id_message.classList.remove('text-success')
-                  id_message.classList.add('text-danger')
-                  id_message.hidden = false
+                  id_alert.textContent = problem.detail
+                  id_alert.classList.remove('alert-success')
+                  id_alert.classList.add('alert-danger')
+                  id_alert.hidden = false
                   return
                 }
                 id_tick.hidden = false
                 id_spinner.hidden = true
-                id_message.textContent = `New password for "${email}" has been verified. You can now sign in.`
-                //id_message.classList.add('text-success')
-                id_message.classList.remove('text-danger')
-                id_message.hidden = false
+                id_alert.textContent = `New password for "${email}" has been verified. You can now sign in.`
+                id_alert.classList.add('alert-success')
+                id_alert.classList.remove('alert-danger')
+                id_alert.hidden = false
               }
             )
           }
index f47050d..fd76436 100644 (file)
@@ -1,8 +1,7 @@
 return async env => {
   let breadcrumbs = await _require('/_lib/breadcrumbs.jst')
-  let icon_jst = await env.site.get_min_svg('/_svg/icon_jst.svg')
+  let get_navigation = await _require('/_lib/get_navigation.jst')
   let navbar = await _require('/_lib/navbar.jst')
-  let icon_pitree = await env.site.get_min_svg('/_svg/icon_pitree.svg')
 
   await navbar(
     env,
@@ -25,43 +24,48 @@ return async env => {
         i {'[Actually don\'t because it\'s still under construction]'}
       }
 
-      ul.nav.flex-column {
-        li.nav-item {
-          a.nav-link {
-            table.icon-and-text {
-              tr {
-                td {
-                  _out.push(icon_pitree)
-                }
-                td {
-                  span.h1 {
-                    span.serif {'Ï€'}
-                    'tree'
-                  }
-                  br {}
-                  span.h3 {'the source code analysis and transformation framework'}
+      let transaction = await env.site.database.Transaction()
+      try {
+        let root = await transaction.get()
+        let p = await get_navigation(root, env.component_names)
+        let children = await p.get('children')
+        let menu = await p.get_json('menu')
+
+        nav {
+          for (let i = 0; i < menu.length; ++i) {
+            let name = menu[i]
+            let q = await children.get(name)
+
+            // note: make the row a link to show hover background at edges
+            // (similar to the grid-gutter-background class I use elsewhere)
+            // note: position: relative sets the target for stretched link
+            div.row.flex-nowrap.align-items-center.nav-link-outer.position-relative {
+              div.col.col-icon128 {
+                div.img-fluid.py-3 {
+                  _out.push(
+                    await env.site.get_min_svg(await q.get_json('icon'))
+                  )
                 }
               }
-            }
-          }
-        }
-        li.nav-item {
-          a.nav-link {
-            table.icon-and-text {
-              tr {
-                td {
-                  _out.push(icon_jst)
-                }
-                td {
-                  span.h1{'JST'}
+              div.col.col-text {
+                a.nav-link-inner.stretched-link(href=`${name}/index.html`) {
+                  span.h2{
+                    `${await q.get_json('title')}`
+                  }
                   br {}
-                  span.h3{'the web development framework with JavaScript Templates'}
+                  span.h4{
+                    `${await q.get_json('description')}`
+                  }
                 }
               }
             }
           }
         }
       }
+      finally {
+        transaction.rollback()
+      }
     },
     // scripts
     async _out => {}
diff --git a/projects/jst/index.html.jst b/projects/jst/index.html.jst
new file mode 100644 (file)
index 0000000..1d75a21
--- /dev/null
@@ -0,0 +1,20 @@
+return async env => {
+  let breadcrumbs = await _require('/_lib/breadcrumbs.jst')
+  let navbar = await _require('/_lib/navbar.jst')
+
+  await navbar(
+    env,
+    // head
+    async _out => {},
+    // body
+    async _out => {
+      await breadcrumbs(env, _out)
+
+      p {
+        i {'Under construction.'}
+      }
+    },
+    // scripts
+    async _out => {}
+  )
+}
diff --git a/projects/pitree/index.html.jst b/projects/pitree/index.html.jst
new file mode 100644 (file)
index 0000000..1d75a21
--- /dev/null
@@ -0,0 +1,20 @@
+return async env => {
+  let breadcrumbs = await _require('/_lib/breadcrumbs.jst')
+  let navbar = await _require('/_lib/navbar.jst')
+
+  await navbar(
+    env,
+    // head
+    async _out => {},
+    // body
+    async _out => {
+      await breadcrumbs(env, _out)
+
+      p {
+        i {'Under construction.'}
+      }
+    },
+    // scripts
+    async _out => {}
+  )
+}
index a066789..bbb508d 100644 (file)
@@ -42,9 +42,9 @@ return async env => {
 
       await breadcrumbs(env, _out)
 
-      h4 {
+      h4/*.mt-3*/ {
         'Query: '
-        strong {`${query}`}
+        b {`${query}`}
       }
 
       if (search.results.length) {
@@ -53,10 +53,10 @@ return async env => {
         ol(start=first + 1) {
           for (let i = 0; i < search.results.length; ++i) {
             let page = search.results[i].auxiliary
-            li {
+            li.mb-3 {
               a(href=page) {`${await breadcrumbs_str(page)}`}
               br {}
-              p {_out.push(search.results[i].summary)} // note: contains HTML
+              _out.push(search.results[i].summary) // note: contains HTML
             }
           }
         }