From c77cdb4b79a7c888c4a9f64043318d373c21ab9d Mon Sep 17 00:00:00 2001
From: Ari <ariadna@hey.com>
Date: Thu, 29 May 2025 08:38:03 +0000
Subject: [PATCH] Custom themes and config overrides (#9)

Co-authored-by: Astra <me@astrra.space>
Reviewed-on: https://git.witchcraft.systems/scientific-witchery/pds-dash/pulls/9
Reviewed-by: Astra <me@astrra.space>
Co-authored-by: Ari <ariadna@hey.com>
Co-committed-by: Ari <ariadna@hey.com>
---
 .forgejo/workflows/deploy.yaml  |  13 +-
 .gitignore                      |   5 +-
 README.md                       |   4 +-
 config.ts                       |  37 ---
 config.ts.example               |  44 ++++
 src/App.svelte                  | 109 ++------
 src/app.css                     |  90 +------
 src/lib/AccountComponent.svelte |  25 --
 src/lib/PostComponent.svelte    | 164 +------------
 themes/default/theme.css        | 423 ++++++++++++++++++++++++++++++++
 themes/express/theme.css        | 370 ++++++++++++++++++++++++++++
 themes/witchcraft/theme.css     | 367 +++++++++++++++++++++++++++
 theming.ts                      |  32 +++
 vite.config.ts                  |   6 +-
 14 files changed, 1286 insertions(+), 403 deletions(-)
 delete mode 100644 config.ts
 create mode 100644 config.ts.example
 create mode 100644 themes/default/theme.css
 create mode 100644 themes/express/theme.css
 create mode 100644 themes/witchcraft/theme.css
 create mode 100644 theming.ts

diff --git a/.forgejo/workflows/deploy.yaml b/.forgejo/workflows/deploy.yaml
index c6a05d0..7f20d99 100644
--- a/.forgejo/workflows/deploy.yaml
+++ b/.forgejo/workflows/deploy.yaml
@@ -6,15 +6,26 @@ on:
       - main
       - astra/ci
 
+
 jobs:
   deploy:
     name: Deploy
     runs-on: ubuntu-latest
 
     steps:
-      - name: Checkout repo
+      - name: Checkout main repo
         uses: actions/checkout@v4
 
+      - name: Checkout overrides repo
+        uses: actions/checkout@v4
+        with:
+          repository: scientific-witchery/pds-dash-overrides
+          token: ${{ secrets.OVERRIDES_TOKEN}}
+          path: overrides
+
+      - name: Copy config file to root
+        run: cp overrides/config.ts ./config.ts
+      
       - name: Setup Node.js
         uses: actions/setup-node@v3
         with:
diff --git a/.gitignore b/.gitignore
index 42a517f..991a625 100644
--- a/.gitignore
+++ b/.gitignore
@@ -149,4 +149,7 @@ dist
 .yarn/unplugged
 .yarn/build-state.yml
 .yarn/install-state.gz
-.pnp.*
\ No newline at end of file
+.pnp.*
+
+# Config files
+config.ts
\ No newline at end of file
diff --git a/README.md b/README.md
index d9eb2ea..45276cb 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,9 @@ a frontend dashboard with stats for your ATProto PDS.
 
 ### installing
 
-clone the repo, install dependencies using deno:
+clone the repo, copy `config.ts.example` to `config.ts` and edit it to your liking.
+
+then, install dependencies using deno:
 
 ```sh
 deno install
diff --git a/config.ts b/config.ts
deleted file mode 100644
index 8d09cf6..0000000
--- a/config.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Configuration module for the PDS Dashboard
- */
-export class Config {
-  /**
-   * The base URL of the PDS (Personal Data Server)
-   * @default "https://pds.witchcraft.systems"
-   */
-  static readonly PDS_URL: string = "https://pds.witchcraft.systems";
-
-  /**
-   * The base URL of the frontend service for linking to replies/quotes/accounts etc.
-   * @default "https://deer.social"
-   */
-  static readonly FRONTEND_URL: string = "https://deer.social";
-
-  /**
-   * Maximum number of posts to fetch from the PDS per request
-   * Should be around 20 for about 10 users on the pds
-   * The more users you have, the lower the number should be
-   * since sorting is slow and is done on the frontend
-   * @default 20
-   */
-  static readonly MAX_POSTS: number = 20;
-
-  /**
-   * Footer text for the dashboard, you probably want to change this
-   */
-  static readonly FOOTER_TEXT: string =
-    "Astrally projected from <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a><br><br><a href='https://git.witchcraft.systems/scientific-witchery/pds-dash' target='_blank'>Source</a> (<a href='https://github.com/witchcraft-systems/pds-dash/' target='_blank'>github mirror</a>)";
-
-  /**
-   * Whether to show the posts that are in the future
-   * @default false
-   */
-  static readonly SHOW_FUTURE_POSTS: boolean = false;
-}
diff --git a/config.ts.example b/config.ts.example
new file mode 100644
index 0000000..87894d3
--- /dev/null
+++ b/config.ts.example
@@ -0,0 +1,44 @@
+/**
+ * Configuration module for the PDS Dashboard
+ */
+export class Config {
+  /**
+   * The base URL of the PDS (Personal Data Server).
+   * @default none
+   */
+  static readonly PDS_URL: string = "";
+
+  /**
+   * Theme to be used
+   * @default "default"
+   */
+  static readonly THEME: string = "default";
+
+  /**
+   * The base URL of the frontend service for linking to replies/quotes/accounts etc.
+   * @default "https://deer.social" // or https://bsky.app if you're boring
+   */
+  static readonly FRONTEND_URL: string = "https://deer.social";
+
+  /**
+   * Maximum number of posts to fetch from the PDS per request
+   * Should be around 20 for about 10 users on the pds
+   * The more users you have, the lower the number should be
+   * since sorting is slow and is done on the frontend
+   * @default 20
+   */
+  static readonly MAX_POSTS: number = 20;
+
+  /**
+   * Footer text for the dashboard, you probably want to change this. Supports HTML.
+   * @default "<a href='https://git.witchcraft.systems/scientific-witchery/pds-dash' target='_blank'>Source</a> (<a href='https://github.com/witchcraft-systems/pds-dash/' target='_blank'>github mirror</a>)"
+   */
+  static readonly FOOTER_TEXT: string =
+    "<a href='https://git.witchcraft.systems/scientific-witchery/pds-dash' target='_blank'>Source</a> (<a href='https://github.com/witchcraft-systems/pds-dash/' target='_blank'>github mirror</a>)";
+
+  /**
+   * Whether to show the posts with timestamps that are in the future.
+   * @default false
+   */
+  static readonly SHOW_FUTURE_POSTS: boolean = false;
+}
diff --git a/src/App.svelte b/src/App.svelte
index c6e7534..798b8a7 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -9,6 +9,26 @@
 
   let posts: Post[] = [];
 
+  let hue: number = 1;
+  const cycleColors = async () => {
+    while (true) {  
+      hue += 1;
+      if (hue > 360) {
+        hue = 0;
+      }
+      document.documentElement.style.setProperty("--primary-h", hue.toString());
+      await new Promise((resolve) => setTimeout(resolve, 10));
+    }
+  }
+  let clickCounter = 0;
+  const carameldansenfusion = async () => {
+    clickCounter++;
+    if (clickCounter >= 10) {
+      clickCounter = 0;
+      cycleColors();
+    }
+  };
+
   onMount(() => {
     // Fetch initial posts
     getNextPosts().then((initialPosts) => {
@@ -39,7 +59,7 @@
       <p>Loading...</p>
     {:then accountsData}
       <div id="Account">
-        <h1 id="Header">ATProto PDS</h1>
+        <h1 onclick={carameldansenfusion} id="Header">ATProto PDS</h1>
         <p>Home to {accountsData.length} accounts</p>
         <div id="accountsList">
           {#each accountsData as accountObject}
@@ -64,90 +84,5 @@
 </main>
 
 <style>
-  /* desktop style */
-
-  #Content {
-    display: flex;
-    /* split the screen in half, left for accounts, right for posts */
-    width: 100%;
-    height: 100%;
-    flex-direction: row;
-    justify-content: space-between;
-    align-items: center;
-    background-color: var(--background-color);
-    color: var(--text-color);
-  }
-  #Feed {
-    overflow-y: scroll;
-    width: 65%;
-    height: 100vh;
-    padding: 20px;
-    padding-bottom: 0;
-    padding-top: 0;
-    margin-top: 0;
-    margin-bottom: 0;
-  }
-  #spacer {
-    padding: 0;
-    margin: 0;
-    height: 10vh;
-    width: 100%;
-  }
-  #Account {
-    width: 35%;
-    display: flex;
-    flex-direction: column;
-    border: 1px solid var(--border-color);
-    background-color: var(--content-background-color);
-    height: 80vh;
-    padding: 20px;
-    margin-left: 20px;
-  }
-  #accountsList {
-    display: flex;
-    flex-direction: column;
-    overflow-y: scroll;
-    height: 100%;
-    width: 100%;
-    padding: 0px;
-    margin: 0px;
-  }
-
-  #Header {
-    text-align: center;
-    font-size: 2em;
-    margin-bottom: 20px;
-  }
-
-  /* mobile style */
-  @media screen and (max-width: 600px) {
-    #Content {
-      flex-direction: column;
-      width: auto;
-      padding-left: 0px;
-      padding-right: 0px;
-      margin-top: 5%;
-    }
-    #Account {
-      width: 85%;
-      padding-left: 5%;
-      padding-right: 5%;
-      margin-bottom: 20px;
-      margin-left: 5%;
-      margin-right: 5%;
-      height: auto;
-    }
-    #Feed {
-      width: 95%;
-      margin: 0px;
-      margin-left: 10%;
-      margin-right: 10%;
-      padding: 0px;
-      overflow-y: visible;
-    }
-
-    #spacer {
-      height: 0;
-    }
-  }
+ 
 </style>
diff --git a/src/app.css b/src/app.css
index 50da734..cb315e4 100644
--- a/src/app.css
+++ b/src/app.css
@@ -1,88 +1,4 @@
-@font-face {
-  font-family: "ProggyClean";
-  src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf);
-}
-
-:root {
-  --link-color: #646cff;
-  --link-hover-color: #535bf2;
-  --background-color: #12082b;
-  --header-background-color: #1f1145;
-  --content-background-color: #0d0620;
-  --text-color: white;
-  --border-color: #8054f0;
-  --indicator-inactive-color: #4a4a4a;
-  --indicator-active-color: #8054f0;
-}
-
-::-webkit-scrollbar {
-  width: 0px;
-  background: transparent;
-  padding: 0;
-  margin: 0;
-}
-::-webkit-scrollbar-thumb {
-  background: transparent;
-  border-radius: 0;
-}
-::-webkit-scrollbar-track {
-  background: transparent;
-  border-radius: 0;
-}
-::-webkit-scrollbar-corner {
-  background: transparent;
-  border-radius: 0;
-}
-::-webkit-scrollbar-button {
-  background: transparent;
-  border-radius: 0;
-}
-* {
-  scrollbar-width: none;
-  scrollbar-color: transparent transparent;
-  -ms-overflow-style: none; /* IE and Edge */
-  -webkit-overflow-scrolling: touch;
-  -webkit-scrollbar: none; /* Safari */
-}
-
-a {
-  font-weight: 500;
-  color: var(--link-color);
-  text-decoration: inherit;
-}
-a:hover {
-  color: var(--link-hover-color);
-  text-decoration: underline;
-}
-
+@import url('./themes/colors.css');
 body {
-  margin: 0;
-  display: flex;
-  place-items: center;
-  min-width: 320px;
-  min-height: 100vh;
-  background-color: var(--background-color);
-  font-family: "ProggyClean", monospace;
-  font-size: 24px;
-  color: var(--text-color);
-  border-color: var(--border-color);
-  overflow-wrap: break-word;
-  word-wrap: normal;
-  word-break: break-word;
-  hyphens: none;
-}
-
-h1 {
-  font-size: 3.2em;
-  line-height: 1.1;
-}
-
-#app {
-  max-width: 1400px;
-  width: 100%;
-  margin: 0;
-  padding: 0;
-  margin-left: auto;
-  margin-right: auto;
-  text-align: center;
-}
+  background-color: red;
+}
\ No newline at end of file
diff --git a/src/lib/AccountComponent.svelte b/src/lib/AccountComponent.svelte
index 880db3f..3f12cf7 100644
--- a/src/lib/AccountComponent.svelte
+++ b/src/lib/AccountComponent.svelte
@@ -20,30 +20,5 @@
 </a>
 
 <style>
-  #accountContainer {
-    display: flex;
-    text-align: start;
-    align-items: center;
-    background-color: var(--background-color);
-    padding: 0px;
-    margin-bottom: 15px;
-    border: 1px solid var(--border-color);
-  }
-  #accountName {
-    margin-left: 10px;
-    font-size: 0.9em;
-    max-width: 80%;
 
-    /* replace overflow with ellipsis */
-    overflow: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-  }
-  #avatar {
-    width: 50px;
-    height: 50px;
-    margin: 0px;
-    object-fit: cover;
-    border-right: var(--border-color) 1px solid;
-  }
 </style>
diff --git a/src/lib/PostComponent.svelte b/src/lib/PostComponent.svelte
index 43ad667..1cacc28 100644
--- a/src/lib/PostComponent.svelte
+++ b/src/lib/PostComponent.svelte
@@ -71,7 +71,7 @@
       >
       <p id="handle">
         <a href="{Config.FRONTEND_URL}/profile/{post.authorHandle}"
-          >{post.authorHandle}</a
+          >@{post.authorHandle}</a
         >
 
         <a
@@ -152,167 +152,5 @@
 </div>
 
 <style>
-  a:hover {
-    text-decoration: underline;
-  }
-  #postContainer {
-    display: flex;
-    flex-direction: column;
-    border: 1px solid var(--border-color);
-    background-color: var(--background-color);
-    margin-bottom: 15px;
-    overflow-wrap: break-word;
-  }
-  #postHeader {
-    display: flex;
-    flex-direction: row;
-    align-items: center;
-    justify-content: start;
-    background-color: var(--header-background-color);
-    padding: 0px 0px;
-    height: fit-content;
-    border-bottom: 1px solid var(--border-color);
-    font-weight: bold;
-    overflow-wrap: break-word;
-    height: 60px;
-  }
-  #displayName {
-    display: block;
-    color: var(--text-color);
-    font-size: 1.2em;
-    padding: 0;
-    margin: 0;
-    overflow-wrap:normal;
-    word-wrap: break-word;
-    word-break: break-word;
-    text-overflow: ellipsis;
-    overflow: hidden;
-    white-space: nowrap;
-    width: 100%;
-  }
-  #handle {
-    display: block;
-    color: var(--border-color);
-    font-size: 0.8em;
-    padding: 0;
-    margin: 0;
-  }
 
-  #postLink {
-    color: var(--border-color);
-    font-size: 0.8em;
-    padding: 0;
-    margin: 0;
-  }
-  #postContent {
-    display: flex;
-    text-align: start;
-    flex-direction: column;
-    padding: 10px;
-    background-color: var(--content-background-color);
-    color: var(--text-color);
-    overflow-wrap: break-word;
-    white-space: pre-line;
-  }
-  #replyingText {
-    font-size: 0.7em;
-    margin: 0;
-    padding: 0;
-    padding-bottom: 5px;
-  }
-  #quotingText {
-    font-size: 0.7em;
-    margin: 0;
-    padding: 0;
-    padding-bottom: 5px;
-  }
-  #postText {
-    margin: 0;
-    padding: 0;
-    overflow-wrap: break-word;
-    word-wrap: normal;
-    word-break: break-word;
-    hyphens: none;
-  }
-  #headerText {
-    margin-left: 10px;
-    font-size: 0.9em;
-    text-align: start;
-    word-break: break-word;
-    max-width: 80%;
-    max-height: 95%;
-    overflow: hidden;
-    align-self: flex-start;
-    margin-top: auto;
-    margin-bottom: auto;
-  }
-  #avatar {
-    height: 60px;
-    width: 60px;
-    margin: 0px;
-    margin-left: 0px;
-    overflow: hidden;
-    object-fit: cover;
-    border-right: var(--border-color) 1px solid;
-  }
-  #carouselContainer {
-    position: relative;
-    width: 100%;
-    margin-top: 10px;
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-  }
-  #carouselControls {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    width: 100%;
-    max-width: 500px;
-    margin-top: 5px;
-  }
-  #carouselIndicators {
-    display: flex;
-    gap: 5px;
-  }
-  .indicator {
-    width: 8px;
-    height: 8px;
-    background-color: var(--indicator-inactive-color);
-  }
-  .indicator.active {
-    background-color: var(--indicator-active-color);
-  }
-  #prevBtn,
-  #nextBtn {
-    background-color: rgba(31, 17, 69, 0.7);
-    color: var(--text-color);
-    border: 1px solid var(--border-color);
-    width: 30px;
-    height: 30px;
-    cursor: pointer;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-  #prevBtn:disabled,
-  #nextBtn:disabled {
-    opacity: 0.5;
-    cursor: not-allowed;
-  }
-  #embedVideo {
-    width: 100%;
-    max-width: 500px;
-    margin-top: 10px;
-    align-self: center;
-  }
-
-  #embedImages {
-    min-width: min(100%, 500px);
-    max-width: min(100%, 500px);
-    max-height: 500px;
-    object-fit: contain;
-
-    margin: 0;
-  }
 </style>
diff --git a/themes/default/theme.css b/themes/default/theme.css
new file mode 100644
index 0000000..e4061ec
--- /dev/null
+++ b/themes/default/theme.css
@@ -0,0 +1,423 @@
+/* Modern Theme for pds-dash */
+
+:root {
+  /* Modern color palette */
+  --primary-h: 243;
+  --link-color: hsl(var(--primary-h), 73%, 59%);
+  --link-hover-color: #4338ca;
+  --time-color: #8b5cf6;
+  --background-color: #f8fafc;
+  --header-background-color: #ffffff;
+  --content-background-color: #ffffff;
+  --text-color: #111827;
+  --text-secondary-color: #4b5563;
+  --border-color: #e2e8f0;
+  --indicator-inactive-color: #cbd5e1;
+  --indicator-active-color: #6366f1;
+  
+  /* Modern shadows */
+  --button-hover: #f3f4f6;
+}
+
+
+body {
+  margin: 0;
+  display: flex;
+  place-items: center;
+  min-width: 320px;
+  min-height: 100vh;
+  background-color: var(--background-color);
+  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
+  font-size: 18px;
+  line-height: 1.5;
+  color: var(--text-color);
+  border-color: var(--border-color);
+  overflow-wrap: break-word;
+  word-break: break-word;
+  hyphens: none;
+}
+
+a {
+  font-weight: 500;
+  color: var(--link-color);
+  text-decoration: none;
+  transition: color 0.15s ease;
+}
+a:hover {
+  color: var(--link-hover-color);
+}
+
+h1 {
+  font-size: 2.5em;
+  line-height: 1.2;
+  font-weight: 700;
+}
+
+#app {
+  max-width: 1400px;
+  width: 100%;
+  margin: 0 auto;
+  padding: 0;
+  text-align: center;
+}
+
+/* Post Component */
+#postContainer {
+  display: flex;
+  flex-direction: column;
+  border-radius: 12px;
+  border: 1px solid var(--border-color);
+  background-color: var(--content-background-color);
+  margin-bottom: 20px;
+  overflow-wrap: break-word;
+  overflow: hidden;
+  box-shadow: var(--card-shadow);
+  transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+#postContainer:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+}
+
+#postHeader {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: start;
+  background-color: var(--header-background-color);
+  padding: 12px 16px;
+  height: 60px;
+  border-bottom: 1px solid var(--border-color);
+  font-weight: 600;
+  overflow-wrap: break-word;
+}
+
+#displayName {
+  display: block;
+  color: var(--text-color);
+  font-size: 1.1em;
+  padding: 0;
+  margin: 0 0 2px 0;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  white-space: nowrap;
+  width: 100%;
+  letter-spacing: -0.01em;
+}
+
+#handle {
+  display: flex;
+  align-items: center;
+  color: #6b7280;
+  font-size: 0.85em;
+  font-weight: 400;
+  padding: 0;
+  margin: 0;
+  gap: 8px;
+}
+
+#postLink {
+  color: var(--time-color);
+  font-size: 0.85em;
+  padding: 0;
+  margin: 0;
+  opacity: 0.9;
+}
+
+#postContent {
+  display: flex;
+  text-align: start;
+  flex-direction: column;
+  padding: 16px;
+  background-color: var(--content-background-color);
+  color: var(--text-color);
+  overflow-wrap: break-word;
+  white-space: pre-line;
+  line-height: 1.6;
+}
+
+#replyingText, #quotingText {
+  font-size: 0.8em;
+  margin: 0;
+  padding: 0 0 10px 0;
+  color: #6b7280;
+}
+
+#postText {
+  margin: 0 0 8px 0;
+  padding: 0;
+  overflow-wrap: break-word;
+  word-break: break-word;
+  hyphens: none;
+  font-size: 1.05em;
+}
+
+#headerText {
+  margin-left: 12px;
+  font-size: 0.9em;
+  text-align: start;
+  word-break: break-word;
+  max-width: 80%;
+  max-height: 95%;
+  overflow: hidden;
+  align-self: flex-start;
+  margin-top: auto;
+  margin-bottom: auto;
+}
+
+#carouselContainer {
+  position: relative;
+  width: 100%;
+  margin-top: 12px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+#carouselControls {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  width: 100%;
+  max-width: 500px;
+  margin-top: 10px;
+}
+
+#carouselIndicators {
+  display: flex;
+  gap: 6px;
+}
+
+.indicator {
+  width: 6px;
+  height: 6px;
+  background-color: var(--indicator-inactive-color);
+  border-radius: 50%;
+  transition: background-color 0.2s ease, transform 0.2s ease;
+}
+
+.indicator.active {
+  background-color: var(--indicator-active-color);
+  transform: scale(1.3);
+}
+
+#prevBtn,
+#nextBtn {
+  background-color: var(--button-bg);
+  color: var(--text-color);
+  border: 1px solid var(--border-color);
+  width: 32px;
+  height: 32px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 50%;
+  transition: background-color 0.15s ease, transform 0.15s ease;
+  font-size: 16px;
+}
+
+#prevBtn:hover:not(:disabled),
+#nextBtn:hover:not(:disabled) {
+  background-color: var(--button-hover);
+  transform: scale(1.05);
+}
+
+#prevBtn:disabled,
+#nextBtn:disabled {
+  opacity: 0.4;
+  cursor: not-allowed;
+}
+
+#embedVideo {
+  width: 100%;
+  max-width: 500px;
+  margin-top: 12px;
+  align-self: center;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+#embedImages {
+  min-width: min(100%, 500px);
+  max-width: min(100%, 500px);
+  max-height: 500px;
+  object-fit: contain;
+  margin: 0;
+  border-radius: 8px;
+}
+
+/* Account Component */
+#accountContainer {
+  display: flex;
+  text-align: start;
+  align-items: center;
+  background-color: var(--content-background-color);
+  padding: 12px;
+  margin-bottom: 15px;
+  border: 1px solid var(--border-color);
+  border-radius: 12px;
+  transition: background-color 0.15s ease;
+}
+
+#accountContainer:hover {
+  background-color: var(--hover-bg);
+}
+
+#accountName {
+  margin-left: 12px;
+  font-size: 0.95em;
+  max-width: 80%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-weight: 500;
+}
+
+#avatar {
+  width: 48px;
+  height: 48px;
+  margin: 0;
+  object-fit: cover;
+  border-radius: 50%;
+  border: 2px solid white;
+  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+}
+
+/* App.Svelte Layout */
+#Content {
+  display: flex;
+  width: 100%;
+  height: 100%;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  background-color: var(--background-color);
+  color: var(--text-color);
+  gap: 24px;
+}
+
+#Feed {
+  overflow-y: auto;
+  width: 65%;
+  height: 100vh;
+  padding-right: 16px;
+  align-self: flex-start;
+}
+
+#spacer {
+  padding: 0;
+  margin: 0;
+  height: 10vh;
+  width: 100%;
+}
+
+#Account {
+  width: 35%;
+  display: flex;
+  flex-direction: column;
+  border: 1px solid var(--border-color);
+  background-color: var(--content-background-color);
+  max-height: 80vh;
+  padding: 24px;
+  margin-left: 16px;
+  border-radius: 12px;
+  box-shadow: var(--card-shadow);
+}
+
+#accountsList {
+  display: flex;
+  flex-direction: column;
+  overflow-y: auto;
+  height: 100%;
+  width: 100%;
+  padding: 8px 0;
+  margin: 0;
+}
+
+#Header {
+  text-align: center;
+  font-size: 1.8em;
+  margin-bottom: 16px;
+  font-weight: 700;
+  background: linear-gradient(to right, var(--link-color), #8b5cf6);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+}
+
+/* Mobile Styles */
+@media screen and (max-width: 768px) {
+  #Content {
+    flex-direction: column;
+    width: auto;
+    padding: 12px;
+    margin-top: 0;
+  }
+  
+  #Account {
+    width: calc(100% - 32px);
+    padding: 16px;
+    margin-bottom: 20px;
+    margin-left: 0;
+    margin-right: 0;
+    height: auto;
+    order: -1;
+  }
+  
+  #Feed {
+    width: 100%;
+    margin: 0;
+    padding: 0;
+    overflow-y: visible;
+  }
+  
+  #spacer {
+    height: 5vh;
+  }
+
+  body {
+    font-size: 16px;
+  }
+
+  #postHeader {
+    padding: 10px;
+    height: auto;
+    min-height: 50px;
+  }
+}
+
+/* Scrollbar Styles */
+::-webkit-scrollbar {
+  width: 0px;
+  background: transparent;
+  padding: 0;
+  margin: 0;
+}
+::-webkit-scrollbar-thumb {
+  background: transparent;
+  border-radius: 0;
+}
+::-webkit-scrollbar-track {
+  background: transparent;
+  border-radius: 0;
+}
+::-webkit-scrollbar-corner {
+  background: transparent;
+  border-radius: 0;
+}
+::-webkit-scrollbar-button {
+  background: transparent;
+  border-radius: 0;
+}
+
+* {
+  scrollbar-width: none;
+  scrollbar-color: transparent transparent;
+  -ms-overflow-style: none; /* IE and Edge */
+  -webkit-overflow-scrolling: touch;
+  -webkit-scrollbar: none; /* Safari */
+}
\ No newline at end of file
diff --git a/themes/express/theme.css b/themes/express/theme.css
new file mode 100644
index 0000000..cbc2336
--- /dev/null
+++ b/themes/express/theme.css
@@ -0,0 +1,370 @@
+@import url("https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap");
+
+:root {
+  /* Color overrides, edit to whatever you want */
+  --primary-h: 341; /* Hue */
+  --background-color: hsl(var(--primary-h), 62%, 30%);
+  --text-color: hsl(var(--primary-h), 69%, 18%);
+  --link-color: hsl(var(--primary-h), 100%, 20%);
+  --link-hover-color: hsl(var(--primary-h), 20%, 20%);
+  --border-color: hsl(var(--primary-h), 59%, 52%);
+  --content-background-color: hsl(var(--primary-h), 97%, 73%);
+
+  --header-background-color: hsl(var(--primary-h), 97%, 73%);
+  --indicator-inactive-color: #4a4a4a;
+  --indicator-active-color: var(--border-color);
+}
+
+a {
+  font-weight: 500;
+  color: var(--link-color);
+  text-decoration: inherit;
+}
+a:hover {
+  color: var(--link-hover-color);
+  text-decoration: underline;
+}
+
+body {
+  margin: 0;
+  display: flex;
+  place-items: center;
+  min-width: 320px;
+  min-height: 100vh;
+  background-color: var(--background-color);
+  font-family: "Share Tech Mono", monospace;
+  font-size: 24px;
+  color: var(--text-color);
+  border-color: var(--border-color);
+  overflow-wrap: break-word;
+  word-wrap: normal;
+  word-break: break-word;
+  hyphens: none;
+}
+
+h1 {
+  font-size: 3.2em;
+  line-height: 1.1;
+}
+
+#app {
+  max-width: 1400px;
+  width: 100%;
+  margin: 0;
+  padding: 0;
+  margin-left: auto;
+  margin-right: auto;
+  text-align: center;
+}
+
+/* Post Component */
+a:hover {
+  text-decoration: underline;
+}
+#postContainer {
+  display: flex;
+  flex-direction: column;
+  border: 4px solid var(--border-color);
+  background-color: var(--background-color);
+  margin-bottom: 15px;
+  overflow-wrap: break-word;
+  box-shadow: var(--border-color) 10px 10px;
+}
+#postHeader {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: start;
+  background-color: var(--header-background-color);
+  padding: 0px 0px;
+  height: fit-content;
+
+  font-weight: bold;
+  overflow-wrap: break-word;
+  height: 64px;
+}
+#displayName {
+  display: block;
+  color: var(--text-color);
+  font-size: 1.2em;
+  padding: 0;
+  margin: 0;
+  overflow-wrap: normal;
+  word-wrap: break-word;
+  word-break: break-word;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  white-space: nowrap;
+  width: 100%;
+}
+#handle {
+  display: block;
+  color: var(--border-color);
+  font-size: 0.8em;
+  padding: 0;
+  margin: 0;
+}
+
+#postLink {
+  color: var(--link-hover-color);
+  font-size: 0.8em;
+  padding: 0;
+  margin: 0;
+}
+#postContent {
+  display: flex;
+  text-align: start;
+  flex-direction: column;
+  padding: 10px;
+  background-color: var(--content-background-color);
+  color: var(--text-color);
+  overflow-wrap: break-word;
+  white-space: pre-line;
+}
+#replyingText {
+  font-size: 0.7em;
+  margin: 0;
+  padding: 0;
+  padding-bottom: 5px;
+}
+#quotingText {
+  font-size: 0.7em;
+  margin: 0;
+  padding: 0;
+  padding-bottom: 5px;
+}
+#postText {
+  margin: 0;
+  padding: 0;
+  overflow-wrap: break-word;
+  word-wrap: normal;
+  word-break: break-word;
+  hyphens: none;
+}
+#headerText {
+  margin-left: 10px;
+  font-size: 0.9em;
+  text-align: start;
+  word-break: break-word;
+  max-width: 80%;
+  max-height: 95%;
+  overflow: hidden;
+  align-self: flex-start;
+  margin-top: auto;
+  margin-bottom: auto;
+}
+#avatar {
+  height: 30px;
+  width: 30px;
+  overflow: hidden;
+  object-fit: cover;
+}
+#postContainer #avatar {
+  height: 60px;
+  width: 60px;
+  border-right: var(--border-color) 4px solid;
+  border-bottom: var(--border-color) 4px solid;
+}
+#carouselContainer {
+  position: relative;
+  width: 100%;
+  margin-top: 10px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+#carouselControls {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  width: 100%;
+  max-width: 500px;
+  margin-top: 5px;
+}
+#carouselIndicators {
+  display: flex;
+  gap: 5px;
+}
+.indicator {
+  width: 8px;
+  height: 8px;
+  background-color: var(--indicator-inactive-color);
+}
+.indicator.active {
+  background-color: var(--indicator-active-color);
+}
+#prevBtn,
+#nextBtn {
+  background-color: rgba(31, 17, 69, 0.7);
+  color: var(--text-color);
+  border: 4px solid var(--border-color);
+  width: 30px;
+  height: 30px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+#prevBtn:disabled,
+#nextBtn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+#embedVideo {
+  width: 100%;
+  max-width: 500px;
+  margin-top: 10px;
+  align-self: center;
+}
+
+#embedImages {
+  min-width: min(100%, 500px);
+  max-width: min(100%, 500px);
+  max-height: 500px;
+  object-fit: contain;
+
+  margin: 0;
+}
+
+/* Account Component */
+#accountContainer {
+  display: flex;
+  text-align: start;
+  align-items: center;
+  background-color: var(--header-background-color);
+  padding: 0px;
+  margin-bottom: 15px;
+  margin-right: 4px;
+  border: 4px solid var(--border-color);
+  box-shadow: var(--border-color) 10px 10px;
+}
+#accountName {
+  margin-left: 10px;
+  font-size: 1em;
+  max-width: 80%;
+
+  /* replace overflow with ellipsis */
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+/* App.Svelte */
+/* desktop style */
+
+#Content {
+  display: flex;
+  /* split the screen in half, left for accounts, right for posts */
+  width: 100%;
+  height: 100%;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  background-color: var(--background-color);
+  color: var(--text-color);
+}
+#Feed {
+  overflow-y: scroll;
+  width: 65%;
+  height: 100vh;
+  padding: 20px;
+  padding-bottom: 0;
+  padding-top: 0;
+  margin-top: 0;
+  margin-bottom: 0;
+}
+#spacer {
+  padding: 0;
+  margin: 0;
+  height: 10vh;
+  width: 100%;
+}
+#Account {
+  width: 35%;
+  display: flex;
+  flex-direction: column;
+  border: 4px solid var(--border-color);
+  background-color: var(--content-background-color);
+  box-shadow: var(--border-color) 10px 10px;
+  height: 80vh;
+  padding: 20px;
+  margin-left: 20px;
+}
+#accountsList {
+  display: flex;
+  flex-direction: column;
+  overflow-y: scroll;
+  height: 100%;
+  width: 100%;
+  padding: 0px;
+  margin: 0px;
+}
+
+#Header {
+  text-align: center;
+  font-size: 2em;
+  margin-bottom: 20px;
+}
+
+/* mobile style */
+@media screen and (max-width: 600px) {
+  #Content {
+    flex-direction: column;
+    width: auto;
+    padding-left: 0px;
+    padding-right: 0px;
+    margin-top: 5%;
+  }
+  #Account {
+    width: 85%;
+    padding-left: 5%;
+    padding-right: 5%;
+    margin-bottom: 20px;
+    margin-left: 5%;
+    margin-right: 5%;
+    height: auto;
+  }
+  #Feed {
+    width: 95%;
+    margin: 0px;
+    margin-left: 10%;
+    margin-right: 10%;
+    padding: 0px;
+    overflow-y: visible;
+  }
+
+  #spacer {
+    height: 0;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 0px;
+  background: transparent;
+  padding: 0;
+  margin: 0;
+}
+::-webkit-scrollbar-thumb {
+  background: transparent;
+  border-radius: 0;
+}
+::-webkit-scrollbar-track {
+  background: transparent;
+  border-radius: 0;
+}
+::-webkit-scrollbar-corner {
+  background: transparent;
+  border-radius: 0;
+}
+::-webkit-scrollbar-button {
+  background: transparent;
+  border-radius: 0;
+}
+
+* {
+  scrollbar-width: none;
+  scrollbar-color: transparent transparent;
+  -ms-overflow-style: none; /* IE and Edge */
+  -webkit-overflow-scrolling: touch;
+  -webkit-scrollbar: none; /* Safari */
+}
diff --git a/themes/witchcraft/theme.css b/themes/witchcraft/theme.css
new file mode 100644
index 0000000..cd15805
--- /dev/null
+++ b/themes/witchcraft/theme.css
@@ -0,0 +1,367 @@
+@font-face {
+  font-family: "ProggyClean";
+  src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf);
+}
+
+:root {
+  /* Color overrides, edit to whatever you want */
+  --primary-h: 260; /* Hue */
+  
+  --link-color: hsl(calc(var(--primary-h) - 30), 75%, 60%);
+  --link-hover-color: hsl(calc(var(--primary-h) - 30), 75%, 50%);
+  --background-color: hsl(var(--primary-h), 75%, 10%);
+  --header-background-color: hsl(var(--primary-h), 75%, 18%);
+  --content-background-color: hsl(var(--primary-h), 75%, 8%);
+  --text-color: #fff;
+  --border-color: hsl(var(--primary-h), 75%, 60%);
+  --indicator-inactive-color: #4a4a4a;
+  --indicator-active-color: var(--border-color);
+}
+
+
+a {
+  font-weight: 500;
+  color: var(--link-color);
+  text-decoration: inherit;
+}
+a:hover {
+  color: var(--link-hover-color);
+  text-decoration: underline;
+}
+
+body {
+  margin: 0;
+  display: flex;
+  place-items: center;
+  min-width: 320px;
+  min-height: 100vh;
+  background-color: var(--background-color);
+  font-family: "ProggyClean", monospace;
+  font-size: 24px;
+  color: var(--text-color);
+  border-color: var(--border-color);
+  overflow-wrap: break-word;
+  word-wrap: normal;
+  word-break: break-word;
+  hyphens: none;
+}
+
+h1 {
+  font-size: 3.2em;
+  line-height: 1.1;
+}
+
+#app {
+  max-width: 1400px;
+  width: 100%;
+  margin: 0;
+  padding: 0;
+  margin-left: auto;
+  margin-right: auto;
+  text-align: center;
+}
+
+/* Post Component */
+a:hover {
+  text-decoration: underline;
+}
+#postContainer {
+  display: flex;
+  flex-direction: column;
+  border: 1px solid var(--border-color);
+  background-color: var(--background-color);
+  margin-bottom: 15px;
+  overflow-wrap: break-word;
+}
+#postHeader {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: start;
+  background-color: var(--header-background-color);
+  padding: 0px 0px;
+  height: fit-content;
+  border-bottom: 1px solid var(--border-color);
+  font-weight: bold;
+  overflow-wrap: break-word;
+  height: 60px;
+}
+#displayName {
+  display: block;
+  color: var(--text-color);
+  font-size: 1.2em;
+  padding: 0;
+  margin: 0;
+  overflow-wrap: normal;
+  word-wrap: break-word;
+  word-break: break-word;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  white-space: nowrap;
+  width: 100%;
+}
+#handle {
+  display: block;
+  color: var(--border-color);
+  font-size: 0.8em;
+  padding: 0;
+  margin: 0;
+}
+
+#postLink {
+  color: var(--border-color);
+  font-size: 0.8em;
+  padding: 0;
+  margin: 0;
+}
+#postContent {
+  display: flex;
+  text-align: start;
+  flex-direction: column;
+  padding: 10px;
+  background-color: var(--content-background-color);
+  color: var(--text-color);
+  overflow-wrap: break-word;
+  white-space: pre-line;
+}
+#replyingText {
+  font-size: 0.7em;
+  margin: 0;
+  padding: 0;
+  padding-bottom: 5px;
+}
+#quotingText {
+  font-size: 0.7em;
+  margin: 0;
+  padding: 0;
+  padding-bottom: 5px;
+}
+#postText {
+  margin: 0;
+  padding: 0;
+  overflow-wrap: break-word;
+  word-wrap: normal;
+  word-break: break-word;
+  hyphens: none;
+}
+#headerText {
+  margin-left: 10px;
+  font-size: 0.9em;
+  text-align: start;
+  word-break: break-word;
+  max-width: 80%;
+  max-height: 95%;
+  overflow: hidden;
+  align-self: flex-start;
+  margin-top: auto;
+  margin-bottom: auto;
+}
+#avatar {
+  height: 60px;
+  width: 60px;
+  margin: 0px;
+  margin-left: 0px;
+  overflow: hidden;
+  object-fit: cover;
+  border-right: var(--border-color) 1px solid;
+}
+#carouselContainer {
+  position: relative;
+  width: 100%;
+  margin-top: 10px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+#carouselControls {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  width: 100%;
+  max-width: 500px;
+  margin-top: 5px;
+}
+#carouselIndicators {
+  display: flex;
+  gap: 5px;
+}
+.indicator {
+  width: 8px;
+  height: 8px;
+  background-color: var(--indicator-inactive-color);
+}
+.indicator.active {
+  background-color: var(--indicator-active-color);
+}
+#prevBtn,
+#nextBtn {
+  background-color: rgba(31, 17, 69, 0.7);
+  color: var(--text-color);
+  border: 1px solid var(--border-color);
+  width: 30px;
+  height: 30px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+#prevBtn:disabled,
+#nextBtn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+#embedVideo {
+  width: 100%;
+  max-width: 500px;
+  margin-top: 10px;
+  align-self: center;
+}
+
+#embedImages {
+  min-width: min(100%, 500px);
+  max-width: min(100%, 500px);
+  max-height: 500px;
+  object-fit: contain;
+
+  margin: 0;
+}
+
+/* Account Component */
+#accountContainer {
+  display: flex;
+  text-align: start;
+  align-items: center;
+  background-color: var(--background-color);
+  padding: 0px;
+  margin-bottom: 15px;
+  border: 1px solid var(--border-color);
+}
+#accountName {
+  margin-left: 10px;
+  font-size: 1em;
+  max-width: 80%;
+
+  /* replace overflow with ellipsis */
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+/* App.Svelte */
+/* desktop style */
+
+#Content {
+  display: flex;
+  /* split the screen in half, left for accounts, right for posts */
+  width: 100%;
+  height: 100%;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  background-color: var(--background-color);
+  color: var(--text-color);
+}
+#Feed {
+  overflow-y: scroll;
+  width: 65%;
+  height: 100vh;
+  padding: 20px;
+  padding-bottom: 0;
+  padding-top: 0;
+  margin-top: 0;
+  margin-bottom: 0;
+}
+#spacer {
+  padding: 0;
+  margin: 0;
+  height: 10vh;
+  width: 100%;
+}
+#Account {
+  width: 35%;
+  display: flex;
+  flex-direction: column;
+  border: 1px solid var(--border-color);
+  background-color: var(--content-background-color);
+  height: 80vh;
+  padding: 20px;
+  margin-left: 20px;
+}
+#accountsList {
+  display: flex;
+  flex-direction: column;
+  overflow-y: scroll;
+  height: 100%;
+  width: 100%;
+  padding: 0px;
+  margin: 0px;
+}
+
+#Header {
+  text-align: center;
+  font-size: 2em;
+  margin-bottom: 20px;
+}
+
+/* mobile style */
+@media screen and (max-width: 600px) {
+  #Content {
+    flex-direction: column;
+    width: auto;
+    padding-left: 0px;
+    padding-right: 0px;
+    margin-top: 5%;
+  }
+  #Account {
+    width: 85%;
+    padding-left: 5%;
+    padding-right: 5%;
+    margin-bottom: 20px;
+    margin-left: 5%;
+    margin-right: 5%;
+    height: auto;
+  }
+  #Feed {
+    width: 95%;
+    margin: 0px;
+    margin-left: 10%;
+    margin-right: 10%;
+    padding: 0px;
+    overflow-y: visible;
+  }
+
+  #spacer {
+    height: 0;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 0px;
+  background: transparent;
+  padding: 0;
+  margin: 0;
+}
+::-webkit-scrollbar-thumb {
+  background: transparent;
+  border-radius: 0;
+}
+::-webkit-scrollbar-track {
+  background: transparent;
+  border-radius: 0;
+}
+::-webkit-scrollbar-corner {
+  background: transparent;
+  border-radius: 0;
+}
+::-webkit-scrollbar-button {
+  background: transparent;
+  border-radius: 0;
+}
+
+* {
+  scrollbar-width: none;
+  scrollbar-color: transparent transparent;
+  -ms-overflow-style: none; /* IE and Edge */
+  -webkit-overflow-scrolling: touch;
+  -webkit-scrollbar: none; /* Safari */
+}
\ No newline at end of file
diff --git a/theming.ts b/theming.ts
new file mode 100644
index 0000000..3b5964b
--- /dev/null
+++ b/theming.ts
@@ -0,0 +1,32 @@
+import { Plugin } from 'vite';
+import { Config } from './config';
+
+
+// Replaces app.css with the contents of the file specified in the
+// config file.
+export const themePlugin = (): Plugin => {
+    const themeFolder = Config.THEME;
+    console.log(`Using theme folder: ${themeFolder}`);
+    return {
+        name: 'theme-generator',
+        enforce: 'pre', // Ensure this plugin runs first
+        transform(code, id) {
+            if (id.endsWith('app.css')) {
+                // Read the theme file and replace the contents of app.css with it
+                // Needs full path to the file
+                const themeCode = Deno.readTextFileSync(Deno.cwd() + '/themes/' + themeFolder + '/theme.css');
+                // Replace the contents of app.css with the theme code
+
+                // and add a comment at the top
+                const themeComment = `/* Generated from ${themeFolder} */\n`;
+                const themeCodeWithComment = themeComment + themeCode;
+                // Return the theme code as the new contents of app.css
+                return {
+                    code: themeCodeWithComment,
+                    map: null,
+                };
+            }
+            return null;
+        }
+    };
+};
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
index 20d2272..96d3c8c 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,7 +1,11 @@
 import { defineConfig } from "vite";
 import { svelte } from "@sveltejs/vite-plugin-svelte";
+import { themePlugin } from "./theming";
 
 // https://vite.dev/config/
 export default defineConfig({
-  plugins: [svelte()],
+  plugins: [
+    themePlugin(),
+    svelte(),
+  ],
 });