diff --git a/assets/style.css b/assets/style.css index cb0d654f0..2b919921c 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: #000000; + --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; @@ -75,19 +292,20 @@ h2 .item-name { font-weight:bold; font-family: Menlo,Monaco,Consolas,"Courier Ne #foundeo { margin-left: 100px; -} -#foundeo img { - width: 70px; height:20px; + position: absolute; + top: 15px; + right: 14px; + transition: all 2.5s ease-in-out; } -#foundeo { - transition: all 2.5s ease-in-out; +#foundeo img { + width: 70px; + height: 20px; } -#foundeo { - position: absolute; - top: 15px; - right: 14px; +html[data-theme="dark"] #foundeo img, +:root:not([data-theme]) #foundeo img { + filter: invert(1) brightness(1.2); } nav.navbar.navbar-mini { min-height: 40px; @@ -122,7 +340,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 +352,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 +374,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 +396,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 +411,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 +420,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 +499,80 @@ 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;
+}
+
+/* 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; }
@@ -296,3 +609,210 @@ 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);
+}
+
+.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) */
+#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) !important;
+ color: var(--text-secondary) !important;
+ border: 1px solid var(--border-medium) !important;
+ 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);
+}
+
+/* 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,
+.navbar-fixed-top {
+ border-color: var(--navbar-border) !important;
+ background-color: var(--navbar-bg) !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,
+.example-btn,
+.btn-default {
+ 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..21a1ec09e
--- /dev/null
+++ b/assets/theme-switcher.js
@@ -0,0 +1,183 @@
+/**
+ * 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 CONTEXT_MENU_ID = 'theme-toggle-reset-menu';
+ 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,useLocalStorage = true) {
+ if (theme === DARK_THEME || theme === LIGHT_THEME) {
+ HTML_ELEMENT.setAttribute('data-theme', theme);
+ if (useLocalStorage == true) {
+ 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);
+ }
+ }
+
+ 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
+ */
+ function init() {
+ // Apply initial theme
+ const initialTheme = getPreferredTheme();
+ 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');
+ if (toggleButton) {
+ toggleButton.addEventListener('click', toggleTheme);
+ toggleButton.addEventListener('keydown', function(e) {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ 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');
+ 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,false); // Don't update localStorage since this is an OS change
+ }
+ });
+ }
+ }
+
+ // 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 @@
+
+
+
+
+