1- import { Q , A , el } from "./utils.mjs" ;
1+ import { A , el } from "./utils.mjs" ;
22import { setStyle } from "./styling.mjs" ;
33import { CG } from "./CG.mjs" ;
44import { pageHandlers } from "./router.mjs" ;
@@ -8,24 +8,31 @@ import { logger } from "./logger.mjs";
88// static imports
99import { config } from "../data/config.mjs" ;
1010
11- /**
12- * Update all links on the page to use either the production or staging domain.
13- * @param {boolean } useTestDomain - If true, update links to use the staging domain; otherwise, use the production domain.
14- * @returns {void }
15- * @example
16- * // Update links to use the staging domain
17- * updateLinks(true);
18- *
19- * // Update links to use the production domain
20- * updateLinks(false);
21- */
11+ // ── DEBUG_FLAGS ─────────────────────────────────────────────────────────────
12+ const FLAGS_KEY = "DEBUG_FLAGS" ;
13+ const DEFAULT_FLAGS = {
14+ disabled : false ,
15+ logging : false ,
16+ expose : false ,
17+ useTestDomain : false ,
18+ } ;
19+
20+ function getFlags ( ) {
21+ try {
22+ return { ...DEFAULT_FLAGS , ...JSON . parse ( localStorage . getItem ( FLAGS_KEY ) || "{}" ) } ;
23+ } catch {
24+ return { ...DEFAULT_FLAGS } ;
25+ }
26+ }
27+
28+ function setFlag ( key , value ) {
29+ localStorage . setItem ( FLAGS_KEY , JSON . stringify ( { ...getFlags ( ) , [ key ] : value } ) ) ;
30+ }
31+
32+ // ── domain swapper ──────────────────────────────────────────────────────────
2233function updateLinks ( useTestDomain ) {
2334 A (
24- 'a[href*="' +
25- config . domains . prod . url +
26- '"], a[href*="' +
27- config . domains . stage . url +
28- '"]'
35+ `a[href*="${ config . domains . prod . url } "], a[href*="${ config . domains . stage . url } "]`
2936 ) . forEach ( ( link ) => {
3037 const url = new URL ( link . href ) ;
3138 if ( useTestDomain && url . hostname === config . domains . prod . url ) {
@@ -37,106 +44,263 @@ function updateLinks(useTestDomain) {
3744 } ) ;
3845}
3946
40- /**
41- * Adds a debug heading with environment information and a staging toggle.
42- * @returns {void }
43- */
44- export function debugHeading ( ) {
45- // adding a dropdown info circle
46- const infoCircle = el (
47- "div" ,
48- { class : "align-vertical info-circle-wrapper" } ,
49- [ el ( "div" , { class : "info-circle" , text : "I" } ) ]
50- ) ;
51- CG . dom . headerRight . insertBefore ( infoCircle , CG . dom . headerRight . firstChild ) ;
52-
53- let dropdownOptions = [
54- el ( "span" , {
55- text : "Handler: " + pageHandlers . find ( ( { test } ) => test ) . handler . name ,
56- } ) ,
57- el ( "input" , {
58- type : "checkbox" ,
59- id : "cg-baseurl-staging" ,
60- checked : CG . env . isStaging ? true : false ,
61- } ) ,
62-
63- // Add course edit link
64- CG . state . course . id
65- ? el ( "a" , { href : CG . state . course . edit , text : "Edit Course" } )
66- : null ,
47+ // ── global exposure ─────────────────────────────────────────────────────────
48+ const GLOBALS = { logger, animateCompletion, shoot, el, setStyle, CG } ;
6749
68- // Add path edit link
69- CG . state . course . path . id && CG . state . domain
70- ? el ( "a" , { href : CG . state . course . path . edit , text : "Edit Path" } )
71- : null ,
72- ]
73- . filter ( Boolean )
74- . map ( ( html ) => el ( "li" , { } , [ html ] ) ) ;
75-
76- const dropdownMenu = el (
77- "ul" ,
78- { class : "info-circle-menu" , hidden : true } ,
79- dropdownOptions
80- ) ;
50+ function exposeGlobals ( ) { Object . assign ( window , GLOBALS ) ; }
51+ function unexposeGlobals ( ) { Object . keys ( GLOBALS ) . forEach ( ( k ) => delete window [ k ] ) ; }
8152
82- CG . dom . headerRight . parentElement . insertBefore (
83- dropdownMenu ,
84- CG . dom . headerRight . parentElement . firstChild
85- ) ;
53+ // ── floating debug FAB ──────────────────────────────────────────────────────
54+ function buildDebugFab ( ) {
55+ const styleEl = document . createElement ( "style" ) ;
56+ styleEl . textContent = `
57+ #cg-debug-fab {
58+ position: fixed;
59+ bottom: 20px;
60+ right: 20px;
61+ z-index: 99999;
62+ display: flex;
63+ align-items: center;
64+ justify-content: center;
65+ width: 36px;
66+ height: 36px;
67+ padding: 0;
68+ border: none;
69+ border-radius: 50%;
70+ background: #6226fb;
71+ color: #ffffff;
72+ font-size: 14px;
73+ font-weight: 800;
74+ font-family: Gellix, system-ui, sans-serif;
75+ letter-spacing: 0;
76+ cursor: pointer;
77+ box-shadow: 0 2px 10px rgba(98, 38, 251, 0.40);
78+ transition: transform 0.12s ease, box-shadow 0.12s ease;
79+ }
80+ #cg-debug-fab:hover {
81+ transform: scale(1.1);
82+ box-shadow: 0 4px 16px rgba(98, 38, 251, 0.55);
83+ }
84+ #cg-debug-panel {
85+ position: fixed;
86+ bottom: 64px;
87+ right: 20px;
88+ z-index: 99998;
89+ width: 232px;
90+ background: #ffffff;
91+ border: 1px solid #e2e8f0;
92+ border-radius: 12px;
93+ overflow: hidden;
94+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.12), 0 1px 4px rgba(0, 0, 0, 0.06);
95+ color: #1e293b;
96+ font-family: Gellix, system-ui, sans-serif;
97+ font-size: 13px;
98+ }
99+ #cg-debug-panel[hidden] { display: none; }
100+ .cg-dbg-hd {
101+ display: flex;
102+ align-items: center;
103+ justify-content: space-between;
104+ padding: 10px 12px 9px;
105+ border-bottom: 1px solid #f1f5f9;
106+ }
107+ .cg-dbg-title {
108+ font-size: 11px;
109+ font-weight: 700;
110+ letter-spacing: 0.06em;
111+ text-transform: uppercase;
112+ color: #64748b;
113+ }
114+ .cg-dbg-close {
115+ display: flex;
116+ align-items: center;
117+ justify-content: center;
118+ width: 20px;
119+ height: 20px;
120+ padding: 0;
121+ border: none;
122+ border-radius: 4px;
123+ background: none;
124+ color: #94a3b8;
125+ font-size: 16px;
126+ line-height: 1;
127+ cursor: pointer;
128+ }
129+ .cg-dbg-close:hover { background: #f1f5f9; color: #1e293b; }
130+ .cg-dbg-section {
131+ padding: 6px 0;
132+ border-bottom: 1px solid #f1f5f9;
133+ }
134+ .cg-dbg-section:last-child { border-bottom: none; }
135+ .cg-dbg-row {
136+ display: flex;
137+ align-items: center;
138+ gap: 8px;
139+ padding: 5px 12px;
140+ cursor: pointer;
141+ user-select: none;
142+ }
143+ .cg-dbg-row:hover { background: #f8fafc; }
144+ .cg-dbg-row input[type="checkbox"] {
145+ flex-shrink: 0;
146+ width: 13px;
147+ height: 13px;
148+ margin: 0;
149+ accent-color: #6226fb;
150+ cursor: pointer;
151+ }
152+ .cg-dbg-row-lbl { font-size: 12px; }
153+ .cg-dbg-note {
154+ padding: 1px 12px 5px 33px;
155+ font-size: 10px;
156+ color: #94a3b8;
157+ line-height: 1.4;
158+ }
159+ .cg-dbg-info {
160+ display: flex;
161+ flex-direction: column;
162+ gap: 3px;
163+ padding: 8px 12px 6px;
164+ }
165+ .cg-dbg-kv { font-size: 11px; color: #94a3b8; }
166+ .cg-dbg-kv strong { color: #475569; font-weight: 600; }
167+ .cg-dbg-links {
168+ display: flex;
169+ flex-direction: column;
170+ gap: 2px;
171+ padding: 4px 12px 8px;
172+ }
173+ .cg-dbg-links a { font-size: 12px; color: #6226fb; text-decoration: none; }
174+ .cg-dbg-links a:hover { text-decoration: underline; }
175+ ` ;
176+ document . head . appendChild ( styleEl ) ;
86177
87- const trigger = Q ( ".info-circle-wrapper" ) ;
88- const dropdown = Q ( ".info-circle-menu" ) ;
178+ const flags = getFlags ( ) ;
179+ const handler = pageHandlers . find ( ( { test } ) => test ) ?. handler . name ?? "unknown" ;
180+ const envLabel = CG . env . isStaging ? "staging" : CG . env . isAdmin ? "admin" : "prod" ;
89181
90- trigger . addEventListener ( "click" , ( ) => {
91- const x = trigger . getBoundingClientRect ( ) . x ;
182+ function makeToggle ( label , flagKey , note , onChange ) {
183+ const id = `cg-dbg-${ flagKey } ` ;
184+ const checkbox = el ( "input" , { type : "checkbox" , id } ) ;
185+ checkbox . checked = flags [ flagKey ] ;
186+ checkbox . addEventListener ( "change" , ( ) => {
187+ setFlag ( flagKey , checkbox . checked ) ;
188+ onChange ?. ( checkbox . checked ) ;
189+ } ) ;
190+ const row = el ( "label" , { className : "cg-dbg-row" , for : id } , [
191+ checkbox ,
192+ el ( "span" , { className : "cg-dbg-row-lbl" , textContent : label } ) ,
193+ ] ) ;
194+ return note
195+ ? [ row , el ( "div" , { className : "cg-dbg-note" , textContent : note } ) ]
196+ : [ row ] ;
197+ }
92198
93- const dropdownWidth = 200 ;
94- const alignmentFactor = 0.7 ;
199+ const editLinks = [
200+ CG . state . course ?. id
201+ ? el ( "a" , { href : CG . state . course . edit , textContent : "Edit Course" , target : "_blank" } )
202+ : null ,
203+ CG . state . course ?. path ?. id && CG . state . domain
204+ ? el ( "a" , { href : CG . state . course . path . edit , textContent : "Edit Path" , target : "_blank" } )
205+ : null ,
206+ ] . filter ( Boolean ) ;
95207
96- const left = x - dropdownWidth * alignmentFactor ;
208+ const panel = el ( "div" , { id : "cg-debug-panel" } , [
209+ el ( "div" , { className : "cg-dbg-hd" } , [
210+ el ( "span" , { className : "cg-dbg-title" , textContent : "CG Debug" } ) ,
211+ el ( "button" , { className : "cg-dbg-close" , textContent : "×" , aria : { label : "Close" } } ) ,
212+ ] ) ,
213+ el ( "div" , { className : "cg-dbg-section" } , [
214+ ...makeToggle ( "Disable debug" , "disabled" , "Takes effect on next page load" ) ,
215+ ...makeToggle ( "Enable logging" , "logging" , null , ( on ) => {
216+ if ( on ) console . info ( "[CG] Logging enabled — reload for full output" ) ;
217+ } ) ,
218+ ...makeToggle ( "Expose globals" , "expose" , "logger, el, CG, shoot, …" , ( on ) =>
219+ on ? exposeGlobals ( ) : unexposeGlobals ( )
220+ ) ,
221+ ...makeToggle ( "Use test domain" , "useTestDomain" , null , updateLinks ) ,
222+ ] ) ,
223+ el ( "div" , { className : "cg-dbg-section" } , [
224+ el ( "div" , { className : "cg-dbg-info" } , [
225+ el ( "div" , { className : "cg-dbg-kv" } , [
226+ el ( "strong" , { textContent : "Handler: " } ) ,
227+ document . createTextNode ( handler ) ,
228+ ] ) ,
229+ el ( "div" , { className : "cg-dbg-kv" } , [
230+ el ( "strong" , { textContent : "Env: " } ) ,
231+ document . createTextNode ( envLabel ) ,
232+ ] ) ,
233+ ] ) ,
234+ ...( editLinks . length ? [ el ( "div" , { className : "cg-dbg-links" } , editLinks ) ] : [ ] ) ,
235+ ] ) ,
236+ ] ) ;
237+ panel . hidden = true ;
97238
98- dropdown . style . left = `${ left } px` ;
239+ panel . querySelector ( ".cg-dbg-close" ) . addEventListener ( "click" , ( ) => {
240+ panel . hidden = true ;
241+ } ) ;
99242
100- dropdown . hidden = ! dropdown . hidden ;
243+ const fab = el ( "button" , {
244+ id : "cg-debug-fab" ,
245+ textContent : "D" ,
246+ aria : { label : "Open debug panel" } ,
247+ } ) ;
248+ fab . addEventListener ( "click" , ( e ) => {
249+ e . stopPropagation ( ) ;
250+ panel . hidden = ! panel . hidden ;
101251 } ) ;
102252
103- const checkbox = Q ( "#cg-baseurl-staging" ) ;
253+ document . body . append ( fab , panel ) ;
104254
105- // initial state update if needed
106- updateLinks ( checkbox . checked ) ;
255+ // apply initial domain state
256+ updateLinks ( flags . useTestDomain ) ;
107257
108- // toggle behavior
109- checkbox . addEventListener ( "change" , function ( ) {
110- updateLinks ( this . checked ) ;
258+ // close on outside click or Escape
259+ document . addEventListener (
260+ "click" ,
261+ ( e ) => {
262+ if ( ! panel . hidden && ! panel . contains ( e . target ) && e . target !== fab ) {
263+ panel . hidden = true ;
264+ }
265+ } ,
266+ { capture : true }
267+ ) ;
268+ document . addEventListener ( "keydown" , ( e ) => {
269+ if ( e . key === "Escape" && ! panel . hidden ) panel . hidden = true ;
111270 } ) ;
112271}
113272
114- /*
115- * Sets up logging based on the environment. Logging is enabled by default for staging and admin users, but can be toggled via localStorage.
116- * This function also logs the current environment and state for debugging purposes.
117- * It checks the environment variables to determine if logging should be enabled and stores this preference in localStorage.
118- * Finally, it logs the current environment and state using the logger instance.
273+ /**
274+ * Adds a debug heading with environment information and a staging toggle.
275+ * @deprecated Use setupDebug() — the floating FAB supersedes this.
276+ * @returns {void }
277+ */
278+ export function debugHeading ( ) { }
279+
280+ /**
281+ * Sets up debug tooling. Reads and initialises DEBUG_FLAGS in localStorage,
282+ * builds the floating debug FAB, and (unless disabled) wires up logging,
283+ * global exposure, and environment logging.
284+ * @returns {void }
119285 */
120286export function setupDebug ( ) {
121- // setup logging based on environment - enabled for staging and admin users by default, but can be toggled
122- if ( ( CG . env . isStaging || CG . env . isAdmin ) && ! localStorage . getItem ( "cg-logger-enabled" ) ) {
123- localStorage . setItem ( "cg-logger-enabled" , "true" ) ;
124- } else if ( ! localStorage . getItem ( "cg-logger-enabled" ) ) {
125- localStorage . setItem ( "cg-logger-enabled" , "false" ) ;
287+ // first visit: seed flags based on environment
288+ if ( localStorage . getItem ( FLAGS_KEY ) === null ) {
289+ const auto = CG . env . isStaging || CG . env . isAdmin ;
290+ localStorage . setItem (
291+ FLAGS_KEY ,
292+ JSON . stringify ( { ...DEFAULT_FLAGS , logging : auto , expose : auto } )
293+ ) ;
126294 }
127295
128- // log environment info + state
296+ buildDebugFab ( ) ;
297+
298+ const flags = getFlags ( ) ;
299+ if ( flags . disabled ) return ;
300+
129301 logger . info ( "Environment" , CG . env ) ;
130302 logger . info ( "State" , CG . state ) ;
131303 logger . info ( "Page" , CG . page ) ;
132304
133- if ( CG . env . isStaging || CG . env . isAdmin ) {
134- // Expose logger and animateCompletion to the global scope for debugging and external triggers
135- window . logger = logger ;
136- window . animateCompletion = animateCompletion ;
137- window . shoot = shoot ;
138- window . el = el ;
139- window . setStyle = setStyle ;
140- window . CG = CG ; // Expose CG for easier debugging access to state and environment
141- }
142- }
305+ if ( flags . expose ) exposeGlobals ( ) ;
306+ }
0 commit comments