From df0f736fd75458342a36490a123ac2213ca05aa3 Mon Sep 17 00:00:00 2001 From: Tal Funke-Bilu Date: Fri, 29 May 2026 17:48:15 -0700 Subject: [PATCH 1/6] MVP v1 - Reworked CSS to use custom properties/variables. - Created theme-switcher.js to dynamically switch between light and dark modes. - Single-line change to layout.cfm to reference new theme-switcher.js. --- assets/style.css | 516 ++++++++++++++++++++++++++++++++++++--- assets/theme-switcher.js | 110 +++++++++ views/layout.cfm | 18 ++ 3 files changed, 609 insertions(+), 35 deletions(-) create mode 100644 assets/theme-switcher.js diff --git a/assets/style.css b/assets/style.css index cb0d654f0..864f99a1e 100644 --- a/assets/style.css +++ b/assets/style.css @@ -1,17 +1,234 @@ +/* ======================================== + CSS Custom Properties - Light Mode (Default) + ======================================== */ +:root { + /* Primary Colors */ + --bg-primary: #ffffff; + --bg-secondary: #f8f9fa; + --text-primary: #333333; + --text-secondary: #666666; + --text-tertiary: #999999; + + /* Surfaces */ + --navbar-bg: #eeeeee; + --navbar-border: #cccccc; + --panel-bg: #f1f1f1; + --code-bg: #000000; + + /* Borders & Dividers */ + --border-light: #f1f1f1; + --border-medium: #cccccc; + --border-dark: #999999; + + /* Interactive Elements */ + --accent-primary: #0088cc; + --accent-dark: #0077b3; + --accent-light: #0081c2; + --hover-bg: #0088cc; + + /* UI Components */ + --input-bg: #ffffff; + --input-border: #cccccc; + --input-text: #333333; + --dropdown-bg: #ffffff; + --dropdown-border: #cccccc; + --dropdown-hover-bg: #0088cc; + --dropdown-hover-text: #cccccc; + + /* Semantic Colors */ + --link-color: #0088cc; + --fork-me-bg: #e3e3e3; + --fork-me-border: #c2c2c2; + --fork-me-text: #484848; + + /* Labels & Tags */ + --label-acf-bg: #069; + --label-acf-hover: #246; + --label-lucee-bg: #449caf; + --label-lucee-hover: #01798a; + --label-openbd-bg: #2fa5d7; + --label-openbd-hover: #1b6c8f; + --label-boxlang-bg: #04CD70; + --label-boxlang-hover: #08834C; + + /* Syntax */ + --syntax-highlight: #c7254e; + --code-text: #333333; + --code-inline-bg: #f5f5f5; + + /* Transitions */ + --transition-speed: 0.3s; +} + +/* ======================================== + Dark Mode (Respects prefers-color-scheme) + ======================================== */ +@media (prefers-color-scheme: dark) { + :root { + /* Primary Colors - VS Code Dark */ + --bg-primary: #1e1e1e; + --bg-secondary: #252526; + --text-primary: #d4d4d4; + --text-secondary: #a0a0a0; + --text-tertiary: #858585; + + /* Surfaces */ + --navbar-bg: #252526; + --navbar-border: #3e3e42; + --panel-bg: #2d2d30; + --code-bg: #1e1e1e; + + /* Borders & Dividers */ + --border-light: #3e3e42; + --border-medium: #3e3e42; + --border-dark: #5a5a5a; + + /* Interactive Elements */ + --accent-primary: #4fc1ff; + --accent-dark: #3fa7d6; + --accent-light: #5fcfff; + --hover-bg: #264f78; + + /* UI Components */ + --input-bg: #3c3c3c; + --input-border: #3e3e42; + --input-text: #d4d4d4; + --dropdown-bg: #2d2d30; + --dropdown-border: #3e3e42; + --dropdown-hover-bg: #264f78; + --dropdown-hover-text: #d4d4d4; + + /* Semantic Colors */ + --link-color: #4fc1ff; + --fork-me-bg: #3e3e42; + --fork-me-border: #555555; + --fork-me-text: #b0b0b0; + + /* Labels & Tags (adjusted for dark mode) */ + --label-acf-bg: #1a4d7a; + --label-acf-hover: #2d6ba3; + --label-lucee-bg: #2a5a66; + --label-lucee-hover: #3a7a85; + --label-openbd-bg: #2a5a8a; + --label-openbd-hover: #3a7aab; + --label-boxlang-bg: #1a5a3a; + --label-boxlang-hover: #2a7a50; + + /* Syntax */ + --syntax-highlight: #ff7f7f; + --code-text: #d4d4d4; + --code-inline-bg: #2b2b2b; + } +} + +/* ======================================== + Manual Theme Override via data-attribute + ======================================== */ +[data-theme="dark"] { + --bg-primary: #1e1e1e; + --bg-secondary: #252526; + --text-primary: #d4d4d4; + --text-secondary: #a0a0a0; + --text-tertiary: #858585; + --navbar-bg: #252526; + --navbar-border: #3e3e42; + --panel-bg: #2d2d30; + --code-bg: #1e1e1e; + --border-light: #3e3e42; + --border-medium: #3e3e42; + --border-dark: #5a5a5a; + --accent-primary: #4fc1ff; + --accent-dark: #3fa7d6; + --accent-light: #5fcfff; + --hover-bg: #264f78; + --input-bg: #3c3c3c; + --input-border: #3e3e42; + --input-text: #d4d4d4; + --dropdown-bg: #2d2d30; + --dropdown-border: #3e3e42; + --dropdown-hover-bg: #264f78; + --dropdown-hover-text: #d4d4d4; + --link-color: #4fc1ff; + --fork-me-bg: #3e3e42; + --fork-me-border: #555555; + --fork-me-text: #b0b0b0; + --label-acf-bg: #1a4d7a; + --label-acf-hover: #2d6ba3; + --label-lucee-bg: #2a5a66; + --label-lucee-hover: #3a7a85; + --label-openbd-bg: #2a5a8a; + --label-openbd-hover: #3a7aab; + --label-boxlang-bg: #1a5a3a; + --label-boxlang-hover: #2a7a50; + --syntax-highlight: #ff7f7f; + --code-text: #d4d4d4; +} + +[data-theme="light"] { + --bg-primary: #ffffff; + --bg-secondary: #f8f9fa; + --text-primary: #333333; + --text-secondary: #666666; + --text-tertiary: #999999; + --navbar-bg: #eeeeee; + --navbar-border: #cccccc; + --panel-bg: #f1f1f1; + --code-bg: #000000; + --border-light: #f1f1f1; + --border-medium: #cccccc; + --border-dark: #999999; + --accent-primary: #0088cc; + --accent-dark: #0077b3; + --accent-light: #0081c2; + --hover-bg: #0088cc; + --input-bg: #ffffff; + --input-border: #cccccc; + --input-text: #333333; + --dropdown-bg: #ffffff; + --dropdown-border: #cccccc; + --dropdown-hover-bg: #0088cc; + --dropdown-hover-text: #cccccc; + --link-color: #0088cc; + --fork-me-bg: #e3e3e3; + --fork-me-border: #c2c2c2; + --fork-me-text: #484848; + --label-acf-bg: #069; + --label-acf-hover: #246; + --label-lucee-bg: #449caf; + --label-lucee-hover: #01798a; + --label-openbd-bg: #2fa5d7; + --label-openbd-hover: #1b6c8f; + --label-boxlang-bg: #04CD70; + --label-boxlang-hover: #08834C; + --syntax-highlight: #c7254e; + --code-text: #333333; +} + +/* ======================================== + Smooth Transitions for Theme Switching + ======================================== */ +* { + transition: background-color var(--transition-speed) ease, + color var(--transition-speed) ease, + border-color var(--transition-speed) ease; +} + body { padding-top: 50px; padding-bottom: 20px; font-size: 16px; + background-color: var(--bg-primary); + color: var(--text-primary); } div.listing > h2 > a.btn { text-transform: none; } -nav.navbar { background-color:#eee; } -#docname,h4,.typewriter { font-family: Menlo,Monaco,Consolas,"Courier New",monospace; } -.param h4 { background-color: #f1f1f1; padding: 8px 8px; margin-bottom: 2px; } +nav.navbar { background-color: var(--navbar-bg); } +#docname,h4,.typewriter { font-family: Menlo,Monaco,Consolas,"Courier New",monospace; color: var(--text-primary) !important; } +.param h4 { background-color: var(--panel-bg); padding: 8px 8px; margin-bottom: 2px; color: var(--text-primary) !important; } .p-default { font-weight:normal; font-size: smaller; } -h2 { border-bottom:1px solid #f1f1f1; padding-bottom: 12px; } -h2 .item-name { font-weight:bold; font-family: Menlo,Monaco,Consolas,"Courier New",monospace; } +h2 { border-bottom: 1px solid var(--border-light); padding-bottom: 12px; color: var(--text-primary) !important; } +h2 .item-name { font-weight:bold; font-family: Menlo,Monaco,Consolas,"Courier New",monospace; color: var(--text-primary) !important; } .p-name { font-weight:bold; font-size:larger;} .p-desc { padding: 8px; margin: 0 5%; } .listing a { margin-right: 15px; margin-bottom: 15px; font-size: larger } @@ -33,9 +250,9 @@ h2 .item-name { font-weight:bold; font-family: Menlo,Monaco,Consolas,"Courier Ne } #forkme::before { content: ""; - background-color: #e3e3e3; - border-top: 1.5px dashed #c2c2c2; - border-bottom: 1.5px dashed #c2c2c2; + background-color: var(--fork-me-bg); + border-top: 1.5px dashed var(--fork-me-border); + border-bottom: 1.5px dashed var(--fork-me-border); pointer-events: auto; display: block; position: absolute; @@ -48,7 +265,7 @@ h2 .item-name { font-weight:bold; font-family: Menlo,Monaco,Consolas,"Courier Ne } #forkme::after { content: "Fork me on GitHub"; - color: #484848; + color: var(--fork-me-text); text-decoration: none; text-align: center; text-indent: 0; @@ -80,6 +297,11 @@ h2 .item-name { font-weight:bold; font-family: Menlo,Monaco,Consolas,"Courier Ne width: 70px; height:20px; } +html[data-theme="dark"] #foundeo img, +:root:not([data-theme]) #foundeo img { + filter: invert(1) brightness(1.2); +} + #foundeo { transition: all 2.5s ease-in-out; } @@ -122,7 +344,7 @@ nav.navbar.navbar-mini .navbar-header { nav.navbar.navbar-mini .navbar-brand { padding: 0px 10px 0px 7px; height: 24px; - border-right: 1px solid #ccc; + border-right: 1px solid var(--border-medium); margin: 13px 0 0; } nav.navbar.navbar-mini .navbar-nav>li { @@ -134,17 +356,17 @@ nav.navbar.navbar-mini .navbar-nav>li { } .headinglink { - color: #333333; + color: var(--text-primary); position: relative; } .headinglink:hover { - color: #333333; + color: var(--text-primary); text-decoration: none; } .headinglink:hover::before { content: "\e144"; font-family: "Glyphicons Halflings"; - color: #333333; + color: var(--text-primary); width: 0; position: absolute; left: -20px; @@ -156,8 +378,8 @@ nav.navbar.navbar-mini .navbar-nav>li { min-width: 160px; margin-top: 2px; padding: 5px 0; - background-color: #fff; - border: 1px solid #ccc; + background-color: var(--dropdown-bg); + border: 1px solid var(--border-medium); border: 1px solid rgba(0,0,0,.2); *border-right-width: 2px; *border-bottom-width: 2px; @@ -178,14 +400,14 @@ nav.navbar.navbar-mini .navbar-nav>li { } .tt-suggestion:hover, .tt-cursor { - color: #ccc; + color: var(--dropdown-hover-text); cursor: pointer; - background-color: #0081c2; - background-image: -moz-linear-gradient(top, #0088cc, #0077b3); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); - background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); - background-image: -o-linear-gradient(top, #0088cc, #0077b3); - background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-color: var(--accent-light); + background-image: -moz-linear-gradient(top, var(--accent-primary), var(--accent-dark)); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(var(--accent-primary)), to(var(--accent-dark))); + background-image: -webkit-linear-gradient(top, var(--accent-primary), var(--accent-dark)); + background-image: -o-linear-gradient(top, var(--accent-primary), var(--accent-dark)); + background-image: linear-gradient(to bottom, var(--accent-primary), var(--accent-dark)); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0) } @@ -193,7 +415,7 @@ nav.navbar.navbar-mini .navbar-nav>li { .twitter-typeahead .tt-hint { display: block; border: 1px solid transparent; - color:#ccc; + color: var(--text-secondary); } .breadcrumb li.pull-right:before { content: " "; } .breadcrumb li.divider:before { content: "|"; padding-right:0px; padding-left:8px; } @@ -202,42 +424,73 @@ footer { text-align: center; margin-top: 10px; font-size:smaller; } .navbar-brand { font-weight:bold; } .label-acf { - background-color: #069; + background-color: var(--label-acf-bg); } .label-acf:hover, .label-acf:focus { - background-color: #246; + background-color: var(--label-acf-hover); } .label-lucee { - background-color: #449caf; + background-color: var(--label-lucee-bg); } .label-lucee:hover, .label-lucee:focus { - background-color: #01798a; + background-color: var(--label-lucee-hover); } .label-openbd { - background-color: #2fa5d7; + background-color: var(--label-openbd-bg); } .label-openbd:hover, .label-openbd:focus { - background-color: #1b6c8f; + background-color: var(--label-openbd-hover); } .label-boxlang { - background-color: #04CD70; + background-color: var(--label-boxlang-bg); } .label-boxlang:hover, .label-boxlang:focus { - background-color: #08834C; + background-color: var(--label-boxlang-hover); } .syntax-highlight { - color: #c7254e; - font-weight:bold; + color: var(--syntax-highlight); + font-weight: bold; } .alert-warning a { color: #fff; } /* for use in page anchor - moves -100px to allow space for toolbar */ +/* Force headings color to theme */ +h1,h2,h3,h4,h5,h6 { + color: var(--text-primary) !important; +} + +/* Panel heading H4s */ +.panel-heading h4, +.panel .panel-heading h4 { + color: var(--text-primary) !important; +} + +/* Inline code styling: apply only in dark mode (or when data-theme=dark). + Exclude code inside
 so block code styling remains controlled by pre.prettyprint. */
+[data-theme="dark"] :not(pre) > code {
+  background-color: var(--code-inline-bg) !important;
+  color: var(--syntax-highlight) !important;
+  padding: 0 0.25em;
+  border-radius: 3px;
+  font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
+}
+
+@media (prefers-color-scheme: dark) {
+  :root:not([data-theme]) :not(pre) > code {
+    background-color: var(--code-inline-bg) !important;
+    color: var(--syntax-highlight) !important;
+    padding: 0 0.25em;
+    border-radius: 3px;
+    font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
+  }
+}
+
 .page-anchor {
   position:absolute;
   margin-top:-80px;
@@ -250,16 +503,42 @@ p.clearfix .page-anchor {
 
 iframe { border:0px; }
 
-.contributor { background-color: #f1f1f1; border-radius: 7px; margin: 5px 0; padding:15px 10px; }
+.contributor { background-color: var(--panel-bg); border-radius: 7px; margin: 5px 0; padding: 15px 10px; }
 .contributor img { width: 75px; height: 75px; display: block; margin: 0 auto; }
 
 pre.prettyprint {
   padding: 5px 8px !important;
-  border: 1px solid #CCC !important;
+  border: 1px solid var(--border-medium) !important;
+  background-color: var(--code-bg) !important;
+  color: var(--code-text) !important;
   font-size: 14px;
   line-height: 21px;
 }
 
+/* Light-mode override: use a black background for prettify code examples */
+html:not([data-theme]) pre.prettyprint,
+html[data-theme="light"] pre.prettyprint {
+  background-color: #000 !important;
+  border-color: #333 !important;
+}
+
+/* Dark-mode prettify blocks: remove inline code and span backgrounds inside the block */
+html[data-theme="dark"] pre.prettyprint code,
+:root:not([data-theme]) pre.prettyprint code {
+  background-color: transparent !important;
+  background-image: none !important;
+  background: transparent !important;
+}
+
+html[data-theme="dark"] pre.prettyprint span,
+html[data-theme="dark"] pre.prettyprint code span,
+:root:not([data-theme]) pre.prettyprint span,
+:root:not([data-theme]) pre.prettyprint code span {
+  background-color: transparent !important;
+  background-image: none !important;
+  background: transparent !important;
+}
+
 .alert-danger h4 { line-height: 1.5; }
 
 #search { margin-right:100px; }
@@ -296,3 +575,170 @@ nav.navbar ul.dropdown-menu {
   max-height: 80vh;
   overflow-y: auto;
 }
+
+/* ========================================
+   Theme Toggle Button Styling
+   ======================================== */
+.theme-toggle {
+  position: absolute;
+  top: 15px;
+  right: 110px;
+  cursor: pointer;
+  z-index: 1000;
+  user-select: none;
+}
+
+.theme-toggle svg {
+  width: 20px;
+  height: 20px;
+  stroke: var(--text-primary);
+  fill: none;
+  stroke-width: 2;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+  transition: opacity var(--transition-speed) ease, transform var(--transition-speed) ease;
+}
+
+.theme-toggle svg:hover {
+  opacity: 0.8;
+  transform: scale(1.1);
+}
+
+/* By default show moon, hide sun (icon indicates the mode that will be activated when clicked)
+   - Light mode (default): show moon (click => activate dark)
+   - Dark mode: show sun (click => activate light) */
+#svg-sun {
+  display: none;
+  position: absolute;
+  top: 0;
+  right: 0;
+}
+
+#svg-moon {
+  display: block;
+  position: absolute;
+  top: 0;
+  right: 0;
+}
+
+/* When dark mode is active, show sun and hide moon */
+[data-theme="dark"] #svg-sun {
+  display: block;
+}
+
+[data-theme="dark"] #svg-moon {
+  display: none;
+}
+
+/* OS dark mode preference (only affects when data-theme not set) */
+@media (prefers-color-scheme: dark) {
+  :root:not([data-theme]) #svg-sun {
+    display: block;
+  }
+
+  :root:not([data-theme]) #svg-moon {
+    display: none;
+  }
+}
+
+/* ========================================
+   Component overrides for dark mode
+   Ensure header search, jumbotron, breadcrumb and panels use variables
+   ======================================== */
+/* Header search input */
+#search .form-control,
+.navbar-form .form-control {
+  background-color: var(--input-bg);
+  border: 1px solid var(--input-border);
+  color: var(--input-text);
+}
+
+/* Jumbotron */
+.jumbotron,
+.jumbotron#cfbreak {
+  background-color: var(--bg-secondary) !important;
+  color: var(--text-primary) !important;
+  border: 1px solid var(--border-light) !important;
+}
+
+/* Breadcrumb container */
+.breadcrumb {
+  background-color: var(--panel-bg);
+  color: var(--text-secondary);
+  border-radius: 3px;
+  padding: 6px 12px;
+}
+
+/* Panels */
+.panel,
+.panel-default,
+.panel .panel-body,
+.panel-collapse {
+  background-color: var(--panel-bg) !important;
+  color: var(--text-primary) !important;
+  border-color: var(--border-medium) !important;
+}
+
+.panel-default > .panel-heading {
+  background-color: var(--panel-bg) !important;
+  color: var(--text-primary) !important;
+  border-bottom: 1px solid var(--border-medium) !important;
+}
+
+/* Ensure dropdowns and other surfaces inherit dropdown variables */
+.dropdown-menu,
+.navbar .dropdown-menu {
+  background-color: var(--dropdown-bg);
+  border-color: var(--dropdown-border);
+  color: var(--text-primary);
+}
+
+/* Navbar border & surfaces */
+.navbar,
+.navbar-default,
+.navbar-fixed-top {
+  border-color: var(--navbar-border) !important;
+  background-color: var(--navbar-bg) !important;
+}
+
+/* Breadcrumb border */
+.breadcrumb {
+  background-color: var(--panel-bg) !important;
+  color: var(--text-secondary) !important;
+  border: 1px solid var(--border-medium) !important;
+}
+
+/* Search and form inputs (header, jumbotron, footer) */
+.navbar-form .form-control,
+.jumbotron .form-control,
+.newsletter .form-control,
+footer .form-control,
+.container .form-control {
+  background-color: var(--input-bg) !important;
+  border: 1px solid var(--input-border) !important;
+  color: var(--input-text) !important;
+}
+
+/* Footer button (Get It) */
+.btn-secondary,
+.btn.btn-secondary {
+  background-color: var(--accent-primary) !important;
+  border-color: var(--accent-dark) !important;
+  color: #fff !important;
+}
+.btn-secondary:hover,
+.btn.btn-secondary:hover {
+  background-color: var(--accent-dark) !important;
+}
+
+/* Copy code / code block buttons */
+.prettyprint .btn,
+pre.prettyprint + .btn,
+.code-toolbar .btn,
+.copy-btn,
+.copy-code,
+.btn-copy {
+  background-color: var(--panel-bg) !important;
+  border-color: var(--border-medium) !important;
+  color: var(--text-primary) !important;
+}
\ No newline at end of file
diff --git a/assets/theme-switcher.js b/assets/theme-switcher.js
new file mode 100644
index 000000000..446e9fbc3
--- /dev/null
+++ b/assets/theme-switcher.js
@@ -0,0 +1,110 @@
+/**
+ * CFDocs Theme Switcher - Handles light/dark mode switching with localStorage persistence and OS preference detection
+ */
+
+(function() {
+  'use strict';
+
+  const LIGHT_THEME = 'light';
+  const DARK_THEME = 'dark';
+  const THEME_STORAGE_KEY = 'cfdocs-theme';
+  const HTML_ELEMENT = document.documentElement;
+
+  /**
+   * Get the user's preferred theme
+   * Priority: localStorage > OS preference > default to light
+   */
+  function getPreferredTheme() {
+    // Check localStorage first
+    const storedTheme = localStorage.getItem(THEME_STORAGE_KEY);
+    if (storedTheme === LIGHT_THEME || storedTheme === DARK_THEME) {
+      return storedTheme;
+    }
+
+    // Check OS preference
+    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+      return DARK_THEME;
+    }
+
+    // Default to light theme
+    return LIGHT_THEME;
+  }
+
+  /**
+   * Apply theme by setting data-theme attribute on HTML element
+   */
+  function applyTheme(theme) {
+    if (theme === DARK_THEME || theme === LIGHT_THEME) {
+      HTML_ELEMENT.setAttribute('data-theme', theme);
+      localStorage.setItem(THEME_STORAGE_KEY, theme);
+      updateToggleButton(theme);
+    }
+  }
+
+  /**
+   * Toggle between light and dark themes
+   */
+  function toggleTheme() {
+    const currentTheme = HTML_ELEMENT.getAttribute('data-theme') || getPreferredTheme();
+    const newTheme = currentTheme === LIGHT_THEME ? DARK_THEME : LIGHT_THEME;
+    applyTheme(newTheme);
+  }
+
+  /**
+   * Update the toggle button visual state (if needed for additional UI feedback)
+   */
+  function updateToggleButton(theme) {
+    // The CSS handles the icon visibility through opacity
+    // This function is here for future extensibility
+    const toggleButton = document.getElementById('theme-toggle');
+    if (toggleButton) {
+      toggleButton.setAttribute('data-current-theme', theme);
+    }
+  }
+
+  /**
+   * Initialize theme switcher
+   */
+  function init() {
+    // Apply initial theme
+    const initialTheme = getPreferredTheme();
+    applyTheme(initialTheme);
+
+    // Add click handler to theme toggle button
+    const toggleButton = document.getElementById('theme-toggle');
+    if (toggleButton) {
+      toggleButton.addEventListener('click', toggleTheme);
+      toggleButton.addEventListener('keydown', function(e) {
+        if (e.key === 'Enter' || e.key === ' ') {
+          e.preventDefault();
+          toggleTheme();
+        }
+      });
+      // Make toggle button keyboard accessible
+      toggleButton.setAttribute('role', 'button');
+      toggleButton.setAttribute('tabindex', '0');
+      toggleButton.setAttribute('aria-label', 'Toggle dark/light mode');
+    }
+
+    // Listen for OS theme changes (only if localStorage preference not set)
+    if (window.matchMedia) {
+      window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
+        // Only apply OS change if user hasn't set a preference in localStorage
+        if (!localStorage.getItem(THEME_STORAGE_KEY)) {
+          const newTheme = e.matches ? DARK_THEME : LIGHT_THEME;
+          applyTheme(newTheme);
+        }
+      });
+    }
+  }
+
+  // Initialize when DOM is ready
+  if (document.readyState === 'loading') {
+    document.addEventListener('DOMContentLoaded', init);
+  } else {
+    init();
+  }
+
+  // Expose toggle function globally for testing/debugging
+  window.toggleTheme = toggleTheme;
+})();
diff --git a/views/layout.cfm b/views/layout.cfm
index 786547687..4dd97eb3c 100644
--- a/views/layout.cfm
+++ b/views/layout.cfm
@@ -15,6 +15,7 @@
 	
 	
 	
+	
 	
 		
 		
@@ -115,6 +116,23 @@
 					
 				
 				
+				
+				
+ + + + + + + + + + + + + + +
From 7dfc14ced189aa0a5763a629e71060eb20df0c69 Mon Sep 17 00:00:00 2001 From: Tal Funke-Bilu Date: Fri, 29 May 2026 18:11:41 -0700 Subject: [PATCH 2/6] Menu and Button Fixes - Run Code and Default buttons now display correctly in dark mode. - Navigation menu mouseovers now display correctly in dark mode. --- assets/style.css | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/assets/style.css b/assets/style.css index 864f99a1e..c3c237a2c 100644 --- a/assets/style.css +++ b/assets/style.css @@ -187,7 +187,7 @@ --dropdown-bg: #ffffff; --dropdown-border: #cccccc; --dropdown-hover-bg: #0088cc; - --dropdown-hover-text: #cccccc; + --dropdown-hover-text: #000000; --link-color: #0088cc; --fork-me-bg: #e3e3e3; --fork-me-border: #c2c2c2; @@ -693,6 +693,23 @@ nav.navbar ul.dropdown-menu { color: var(--text-primary); } +/* Use theme variables for hover/focus on nav and dropdown items so hover colors follow the active theme */ +.navbar-nav > li > a:hover, +.navbar-nav > li > a:focus, +.navbar .dropdown-menu > li > a:hover, +.navbar .dropdown-menu > li > a:focus, +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + color: var(--dropdown-hover-text) !important; +} + +/* Ensure open/active dropdown anchors use hover colors too */ +.navbar-nav > .open > a, +.navbar-nav > .open > a:hover, +.navbar-nav > .open > a:focus { + color: var(--dropdown-hover-text) !important; +} + /* Navbar border & surfaces */ .navbar, .navbar-default, @@ -737,7 +754,9 @@ pre.prettyprint + .btn, .code-toolbar .btn, .copy-btn, .copy-code, -.btn-copy { +.btn-copy, +.example-btn, +.btn-default { background-color: var(--panel-bg) !important; border-color: var(--border-medium) !important; color: var(--text-primary) !important; From 90dea0b5fad60f1fe376e5a9b4142fff2923fae1 Mon Sep 17 00:00:00 2001 From: Tal Funke-Bilu Date: Fri, 29 May 2026 19:29:46 -0700 Subject: [PATCH 3/6] Added useLocalStorage Flag - applyTheme() was incorrectly setting local storage values when OS preference was identified on load. - this was preventing OS preference from being correctly identified in the future since local storage was already written. - if OS preference is detected, no local storage is used. - local storage is now only used when the user clicks the toggle. To Test: - Clear browser local storage - Load site - You should see your OS preference (light or dark mode) - In your OS, switch modes (light to dark, or dark to light). - The site in your browser should instantly change modes as well. - Now, click the toggle on the web page (mode will change). - Now, change OS theme (dark/light). Web page mode will not change since toggle was set. --- assets/theme-switcher.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/assets/theme-switcher.js b/assets/theme-switcher.js index 446e9fbc3..2a2cb23c9 100644 --- a/assets/theme-switcher.js +++ b/assets/theme-switcher.js @@ -33,10 +33,12 @@ /** * Apply theme by setting data-theme attribute on HTML element */ - function applyTheme(theme) { + function applyTheme(theme,useLocalStorage = true) { if (theme === DARK_THEME || theme === LIGHT_THEME) { HTML_ELEMENT.setAttribute('data-theme', theme); - localStorage.setItem(THEME_STORAGE_KEY, theme); + if (useLocalStorage == true) { + localStorage.setItem(THEME_STORAGE_KEY, theme); + } updateToggleButton(theme); } } @@ -68,7 +70,7 @@ function init() { // Apply initial theme const initialTheme = getPreferredTheme(); - applyTheme(initialTheme); + applyTheme(initialTheme,false); // Don't update localStorage on initial load since we're just applying the preferred theme // Add click handler to theme toggle button const toggleButton = document.getElementById('theme-toggle'); @@ -92,7 +94,7 @@ // Only apply OS change if user hasn't set a preference in localStorage if (!localStorage.getItem(THEME_STORAGE_KEY)) { const newTheme = e.matches ? DARK_THEME : LIGHT_THEME; - applyTheme(newTheme); + applyTheme(newTheme,false); // Don't update localStorage since this is an OS change } }); } From e81088d5598790cef204f073121cafb46da03d96 Mon Sep 17 00:00:00 2001 From: Tal Funke-Bilu Date: Wed, 3 Jun 2026 14:40:35 -0700 Subject: [PATCH 4/6] Right Click OS Theme Preference Added right click to toggle that displays option to use OS theme preference. This removes the localStorage used to save the user selected dark/light mode theme preference. -style.css: Added CSS for right click context menu. - theme-switcher.js: Added context menu build/show/hide functionality and associated event listeners (context menu is hidden by clicking off of it or pressing ESC). --- assets/style.css | 27 +++++++++++++++ assets/theme-switcher.js | 71 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/assets/style.css b/assets/style.css index c3c237a2c..108ae8117 100644 --- a/assets/style.css +++ b/assets/style.css @@ -604,6 +604,33 @@ nav.navbar ul.dropdown-menu { transform: scale(1.1); } +.theme-toggle-menu { + position: fixed; + z-index: 2000; + min-width: 200px; + background-color: var(--bg-primary); + border: 1px solid var(--border-medium); + border-radius: 5px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); + overflow: hidden; +} + +.theme-toggle-menu button { + width: 100%; + padding: 10px 14px; + border: none; + background: transparent; + color: var(--text-primary); + text-align: left; + font: inherit; + cursor: pointer; +} + +.theme-toggle-menu button:hover, +.theme-toggle-menu button:focus { + background-color: rgba(0, 0, 0, 0.05); +} + /* By default show moon, hide sun (icon indicates the mode that will be activated when clicked) - Light mode (default): show moon (click => activate dark) - Dark mode: show sun (click => activate light) */ diff --git a/assets/theme-switcher.js b/assets/theme-switcher.js index 2a2cb23c9..21a1ec09e 100644 --- a/assets/theme-switcher.js +++ b/assets/theme-switcher.js @@ -8,6 +8,7 @@ const LIGHT_THEME = 'light'; const DARK_THEME = 'dark'; const THEME_STORAGE_KEY = 'cfdocs-theme'; + const CONTEXT_MENU_ID = 'theme-toggle-reset-menu'; const HTML_ELEMENT = document.documentElement; /** @@ -64,6 +65,57 @@ } } + function buildContextMenu() { + let menu = document.getElementById(CONTEXT_MENU_ID); + if (menu) { + return menu; + } + + menu = document.createElement('div'); + menu.id = CONTEXT_MENU_ID; + menu.className = 'theme-toggle-menu'; + menu.innerHTML = ''; + menu.style.display = 'none'; + + const button = menu.querySelector('button'); + button.addEventListener('click', function() { + localStorage.removeItem(THEME_STORAGE_KEY); + applyTheme(getPreferredTheme(), false); + hideContextMenu(); + }); + + menu.addEventListener('contextmenu', function(event) { + event.preventDefault(); + }); + + document.body.appendChild(menu); + return menu; + } + + function showContextMenu(x, y) { + const menu = buildContextMenu(); + menu.style.display = 'block'; + menu.style.visibility = 'hidden'; + menu.style.left = '0px'; + menu.style.top = '0px'; + + const menuWidth = menu.offsetWidth; + const menuHeight = menu.offsetHeight; + const maxLeft = Math.max(window.innerWidth - menuWidth - 10, 10); + const maxTop = Math.max(window.innerHeight - menuHeight - 10, 10); + + menu.style.left = `${Math.min(x, maxLeft)}px`; + menu.style.top = `${Math.min(y, maxTop)}px`; + menu.style.visibility = 'visible'; + } + + function hideContextMenu() { + const menu = document.getElementById(CONTEXT_MENU_ID); + if (menu) { + menu.style.display = 'none'; + } + } + /** * Initialize theme switcher */ @@ -82,6 +134,25 @@ toggleTheme(); } }); + toggleButton.addEventListener('contextmenu', function(e) { + e.preventDefault(); + e.stopPropagation(); + showContextMenu(e.clientX, e.clientY); + }); + + document.addEventListener('click', function(e) { + const menu = document.getElementById(CONTEXT_MENU_ID); + if (menu && !menu.contains(e.target) && !toggleButton.contains(e.target)) { + hideContextMenu(); + } + }); + + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + hideContextMenu(); + } + }); + // Make toggle button keyboard accessible toggleButton.setAttribute('role', 'button'); toggleButton.setAttribute('tabindex', '0'); From 401e8ff607328547b4904ac3f9d008df253b6947 Mon Sep 17 00:00:00 2001 From: Tal Funke-Bilu Date: Sat, 6 Jun 2026 20:58:37 -0700 Subject: [PATCH 5/6] Table Background and Row Color Fixes - Fixed table nth-row alternating background color in dark mode. - Fixed table border background color in dark mode. --- assets/style.css | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/assets/style.css b/assets/style.css index 108ae8117..98a7cc932 100644 --- a/assets/style.css +++ b/assets/style.css @@ -539,6 +539,44 @@ html[data-theme="dark"] pre.prettyprint code span, background: transparent !important; } +/* Bootstrap striped table override for dark mode */ +[data-theme="dark"] .table-striped>tbody>tr:nth-of-type(odd) { + background-color: var(--bg-secondary); +} + +/* Bootstrap bordered table override for dark mode */ +[data-theme="dark"] .table-bordered { + border: 1px solid var(--border-medium); +} + +[data-theme="dark"] .table-bordered>tbody>tr>td, +[data-theme="dark"] .table-bordered>tbody>tr>th, +[data-theme="dark"] .table-bordered>thead>tr>td, +[data-theme="dark"] .table-bordered>thead>tr>th, +[data-theme="dark"] .table-bordered>tfoot>tr>td, +[data-theme="dark"] .table-bordered>tfoot>tr>th { + border: 1px solid var(--border-medium); +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme]) .table-striped>tbody>tr:nth-of-type(odd) { + background-color: var(--bg-secondary); + } + + :root:not([data-theme]) .table-bordered { + border: 1px solid var(--border-medium); + } + + :root:not([data-theme]) .table-bordered>tbody>tr>td, + :root:not([data-theme]) .table-bordered>tbody>tr>th, + :root:not([data-theme]) .table-bordered>thead>tr>td, + :root:not([data-theme]) .table-bordered>thead>tr>th, + :root:not([data-theme]) .table-bordered>tfoot>tr>td, + :root:not([data-theme]) .table-bordered>tfoot>tr>th { + border: 1px solid var(--border-medium); + } +} + .alert-danger h4 { line-height: 1.5; } #search { margin-right:100px; } From b06244dbd1030ca7543159b419c77b8cbd439902 Mon Sep 17 00:00:00 2001 From: Tal Funke-Bilu Date: Sat, 6 Jun 2026 22:37:25 -0700 Subject: [PATCH 6/6] Refactoring - Consolidated #foundeo rules from 3 separate blocks into 1 unified declaration with all properties (margin, position, transition, filter) - Merged duplicate .breadcrumb rules into a single definition with all properties combined and consistent !important flags --- assets/style.css | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/assets/style.css b/assets/style.css index 98a7cc932..2b919921c 100644 --- a/assets/style.css +++ b/assets/style.css @@ -292,25 +292,21 @@ h2 .item-name { font-weight:bold; font-family: Menlo,Monaco,Consolas,"Courier Ne #foundeo { margin-left: 100px; + position: absolute; + top: 15px; + right: 14px; + transition: all 2.5s ease-in-out; } + #foundeo img { - width: 70px; height:20px; + width: 70px; + height: 20px; } html[data-theme="dark"] #foundeo img, :root:not([data-theme]) #foundeo img { filter: invert(1) brightness(1.2); } - -#foundeo { - transition: all 2.5s ease-in-out; -} - -#foundeo { - position: absolute; - top: 15px; - right: 14px; -} nav.navbar.navbar-mini { min-height: 40px; margin-bottom: 0; @@ -728,8 +724,9 @@ nav.navbar ul.dropdown-menu { /* Breadcrumb container */ .breadcrumb { - background-color: var(--panel-bg); - color: var(--text-secondary); + background-color: var(--panel-bg) !important; + color: var(--text-secondary) !important; + border: 1px solid var(--border-medium) !important; border-radius: 3px; padding: 6px 12px; } @@ -783,13 +780,6 @@ nav.navbar ul.dropdown-menu { background-color: var(--navbar-bg) !important; } -/* Breadcrumb border */ -.breadcrumb { - background-color: var(--panel-bg) !important; - color: var(--text-secondary) !important; - border: 1px solid var(--border-medium) !important; -} - /* Search and form inputs (header, jumbotron, footer) */ .navbar-form .form-control, .jumbotron .form-control,