From 8ac0f6b93125fe6ed764e1b178c4bc6dbaa4dbb4 Mon Sep 17 00:00:00 2001
From: Nicola Clark <nicola@slottedspoon.dev>
Date: Sat, 12 Oct 2024 14:37:07 -0500
Subject: [PATCH] add transition to nav tree

user-agent detection is employed to ensure that the SSR renders the nav items by default for desktop (opposite for mobile). CSS media queries take over after the content loads on the device.

TODO: fix preview pane jump on desktop
---
 package-lock.json                  | 63 ++++++++++++++++++++++++++++++
 package.json                       |  6 +++
 src/lib/components/nav-tree.svelte | 20 +++++-----
 src/routes/+layout.server.ts       | 12 ++++++
 src/routes/+page.svelte            |  4 +-
 tests/mobile.test.ts               |  7 +++-
 6 files changed, 99 insertions(+), 13 deletions(-)
 create mode 100644 src/routes/+layout.server.ts

diff --git a/package-lock.json b/package-lock.json
index bc81984..649765c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,11 +7,16 @@
     "": {
       "name": "resumarkdown",
       "version": "0.0.1",
+      "dependencies": {
+        "ua-parser-js": "^1.0.39"
+      },
       "devDependencies": {
         "@playwright/test": "^1.28.1",
         "@sveltejs/adapter-auto": "^3.0.0",
         "@sveltejs/kit": "^2.0.0",
         "@sveltejs/vite-plugin-svelte": "^3.0.0",
+        "@types/ua-parser-js": "^0.7.39",
+        "@types/user-agents": "^1.0.4",
         "less": "^4.2.0",
         "prettier": "^3.1.1",
         "prettier-plugin-svelte": "^3.1.2",
@@ -19,6 +24,7 @@
         "svelte-check": "^4.0.0",
         "svelte-preprocess": "^6.0.3",
         "typescript": "^5.0.0",
+        "user-agents": "^1.1.325",
         "vite": "^5.0.3",
         "vitest": "^2.0.0"
       }
@@ -829,6 +835,20 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/ua-parser-js": {
+      "version": "0.7.39",
+      "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz",
+      "integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/user-agents": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@types/user-agents/-/user-agents-1.0.4.tgz",
+      "integrity": "sha512-AjeFc4oX5WPPflgKfRWWJfkEk7Wu82fnj1rROPsiqFt6yElpdGFg8Srtm/4PU4rA9UiDUZlruGPgcwTMQlwq4w==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@vitest/expect": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.2.tgz",
@@ -1357,6 +1377,13 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/lodash.clonedeep": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+      "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/loupe": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz",
@@ -1986,6 +2013,42 @@
         "node": ">=14.17"
       }
     },
+    "node_modules/ua-parser-js": {
+      "version": "1.0.39",
+      "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.39.tgz",
+      "integrity": "sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/ua-parser-js"
+        },
+        {
+          "type": "paypal",
+          "url": "https://paypal.me/faisalman"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/faisalman"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "ua-parser-js": "script/cli.js"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/user-agents": {
+      "version": "1.1.325",
+      "resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.1.325.tgz",
+      "integrity": "sha512-BmVDscJOZsBBztMPHg+wf65QwbT+N3C46YEsuW8BmcxRxwKh2AbdSaKEjzoC7rDlyuECXEUXO+C2eW3QAhbl2A==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "lodash.clonedeep": "^4.5.0"
+      }
+    },
     "node_modules/vite": {
       "version": "5.4.8",
       "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
diff --git a/package.json b/package.json
index c36f9ca..d942322 100644
--- a/package.json
+++ b/package.json
@@ -14,11 +14,16 @@
     "lint": "prettier --check .",
     "format": "prettier --write ."
   },
+  "dependencies": {
+    "ua-parser-js": "^1.0.39"
+  },
   "devDependencies": {
     "@playwright/test": "^1.28.1",
     "@sveltejs/adapter-auto": "^3.0.0",
     "@sveltejs/kit": "^2.0.0",
     "@sveltejs/vite-plugin-svelte": "^3.0.0",
+    "@types/ua-parser-js": "^0.7.39",
+    "@types/user-agents": "^1.0.4",
     "less": "^4.2.0",
     "prettier": "^3.1.1",
     "prettier-plugin-svelte": "^3.1.2",
@@ -26,6 +31,7 @@
     "svelte-check": "^4.0.0",
     "svelte-preprocess": "^6.0.3",
     "typescript": "^5.0.0",
+    "user-agents": "^1.1.325",
     "vite": "^5.0.3",
     "vitest": "^2.0.0"
   },
diff --git a/src/lib/components/nav-tree.svelte b/src/lib/components/nav-tree.svelte
index 34dfbde..d416cce 100644
--- a/src/lib/components/nav-tree.svelte
+++ b/src/lib/components/nav-tree.svelte
@@ -1,14 +1,14 @@
 <script lang="ts">
   import { onMount } from 'svelte';
+  import { slide } from 'svelte/transition';
 
   import { navHeight } from '$lib/stores/layout';
 
   import NavToggle from './nav-toggle.svelte';
 
-  let open: boolean = false;
+  export let mobile: boolean = true;
 
-  let hidden: boolean;
-  $: hidden = !open;
+  let open: boolean = false;
 
   // magic to offset preview pane on desktop
   let navRef: HTMLElement;
@@ -20,9 +20,11 @@
 
 <nav bind:this={navRef}>
   <NavToggle bind:navOpen={open} />
-  <ul class:hidden role="tablist">
-    <slot />
-  </ul>
+  {#if open || !mobile}
+    <ul role="tablist" transition:slide={{ duration: 300 }}>
+      <slot />
+    </ul>
+  {/if}
 </nav>
 
 <style lang="less">
@@ -35,14 +37,10 @@
     margin: 0;
     padding: 0;
 
-    &.hidden {
-      display: none;
-    }
-
     @media screen and (min-width: @sizes[lg]) {
       align-items: center;
       border-right: @border;
-      display: flex !important;
+      display: flex;
       flex-direction: row;
       justify-content: space-evenly;
       width: 50%;
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts
new file mode 100644
index 0000000..a77effe
--- /dev/null
+++ b/src/routes/+layout.server.ts
@@ -0,0 +1,12 @@
+import { UAParser } from 'ua-parser-js';
+
+export async function load({ request }) {
+  const ua = request.headers.get('user-agent');
+
+  if (!ua) {
+    return { mobile: true };
+  }
+
+  const { device } = UAParser(ua);
+  return { mobile: device.type === 'mobile' };
+}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 318da54..99fd55b 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -5,6 +5,8 @@
   import NavItem from '$lib/components/nav-item.svelte';
   import NavTree from '$lib/components/nav-tree.svelte';
 
+  export let data;
+
   let markdown: string = '';
   let stylesheet: string = '';
 </script>
@@ -15,7 +17,7 @@
 <div>
   <header>
     <Headline>resumarkdown</Headline>
-    <NavTree>
+    <NavTree mobile={data.mobile}>
       <NavItem destination="content">content</NavItem>
       <NavItem destination="style">style</NavItem>
       <NavItem destination="preview">preview</NavItem>
diff --git a/tests/mobile.test.ts b/tests/mobile.test.ts
index 03d5cee..3189fb5 100644
--- a/tests/mobile.test.ts
+++ b/tests/mobile.test.ts
@@ -1,6 +1,11 @@
 import { expect, test } from '@playwright/test';
 
-test.use({ viewport: { height: 2556, width: 1179 } });
+import UserAgent from 'user-agents';
+
+test.use({
+  viewport: { height: 2556, width: 1179 },
+  userAgent: new UserAgent({ deviceCategory: 'mobile' }).toString(),
+});
 
 test('mobile page has nav tree hidden by default', async ({ page }) => {
   await page.goto('/');
