@networkpro/web 1.10.1 → 1.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,222 @@
1
+ /* ==========================================================================
2
+ src/lib/stores/trackingPreferences.js
3
+
4
+ Copyright © 2025 Network Pro Strategies (Network Pro™)
5
+ SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
6
+ This file is part of Network Pro.
7
+ ========================================================================== */
8
+
9
+ /**
10
+ * NOTE: Legacy logic from `trackingCookies.js` was merged here in June 2025.
11
+ * That file has been removed to consolidate stateful tracking logic into a
12
+ * reactive store with SSR compatibility.
13
+ */
14
+
15
+ /**
16
+ * @file trackingPreferences.js
17
+ * @description Reactive store for tracking preferences derived from
18
+ * cookies and browser signals (DNT / GPC). Safe for SSR.
19
+ * @module src/lib/stores
20
+ */
21
+
22
+ import { browser } from "$app/environment";
23
+ import { derived, writable } from "svelte/store";
24
+
25
+ /**
26
+ * @typedef {object} TrackingState
27
+ * @property {boolean} optedIn - User explicitly opted in via cookie
28
+ * @property {boolean} optedOut - User explicitly opted out via cookie
29
+ * @property {boolean} dnt - Do Not Track browser signal
30
+ * @property {boolean} gpc - Global Privacy Control browser signal
31
+ * @property {boolean} enabled - Whether tracking is permitted
32
+ * @property {string} status - Human-readable description of tracking state
33
+ */
34
+
35
+ /**
36
+ * @returns {string} Raw document.cookie or empty string (SSR-safe)
37
+ */
38
+ function readCookies() {
39
+ return browser ? document.cookie || "" : "";
40
+ }
41
+
42
+ /**
43
+ * Check if a specific cookie exists
44
+ * @param {string} name
45
+ * @returns {boolean}
46
+ */
47
+ function cookieExists(name) {
48
+ return readCookies().includes(`${name}=true`);
49
+ }
50
+
51
+ /**
52
+ * Set a cookie with boolean true, 1-year duration
53
+ * @param {string} name
54
+ * @returns {void}
55
+ */
56
+ function setCookie(name) {
57
+ if (browser) {
58
+ document.cookie = `${name}=true; path=/; max-age=31536000; samesite=lax`;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Remove a cookie by setting zero max-age
64
+ * @param {string} name
65
+ * @returns {void}
66
+ */
67
+ function removeCookie(name) {
68
+ if (browser) {
69
+ document.cookie = `${name}=; Max-Age=0; path=/; samesite=lax`;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Write tracking_consent_timestamp cookie
75
+ * @returns {void}
76
+ */
77
+ function setConsentTimestamp() {
78
+ if (browser) {
79
+ document.cookie = `tracking_consent_timestamp=${Date.now()}; path=/; max-age=31536000; samesite=lax`;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Remove tracking_consent_timestamp cookie
85
+ * @returns {void}
86
+ */
87
+ function removeConsentTimestamp() {
88
+ if (browser) {
89
+ document.cookie = `tracking_consent_timestamp=; Max-Age=0; path=/; samesite=lax`;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * @returns {{ dnt: boolean, gpc: boolean }}
95
+ */
96
+ function getPrivacySignals() {
97
+ if (!browser) return { dnt: false, gpc: false };
98
+
99
+ const dnt = navigator.doNotTrack === "1";
100
+ // @ts-expect-error: Non-standard GPC property
101
+ const gpc = navigator.globalPrivacyControl === true;
102
+
103
+ return { dnt, gpc };
104
+ }
105
+
106
+ /**
107
+ * @param {object} args
108
+ * @param {boolean} args.optedOut
109
+ * @param {boolean} args.optedIn
110
+ * @param {boolean} args.dnt
111
+ * @param {boolean} args.gpc
112
+ * @returns {string}
113
+ */
114
+ function deriveStatus({ optedOut, optedIn, dnt, gpc }) {
115
+ if (optedOut) return "🔒 Tracking disabled (manual opt-out)";
116
+ if (optedIn) return "✅ Tracking enabled (manual opt-in)";
117
+ if (dnt || gpc) return "🛑 Tracking disabled (via browser signal)";
118
+ return "⚙️ Using default settings (tracking enabled)";
119
+ }
120
+
121
+ /**
122
+ * @returns {TrackingState}
123
+ */
124
+ function computePreferences() {
125
+ const optedOut = cookieExists("disable_tracking");
126
+ const optedIn = cookieExists("enable_tracking");
127
+ const { dnt, gpc } = getPrivacySignals();
128
+
129
+ const enabled = optedIn || (!optedOut && !dnt && !gpc);
130
+ const status = deriveStatus({ optedOut, optedIn, dnt, gpc });
131
+
132
+ return { optedIn, optedOut, dnt, gpc, enabled, status };
133
+ }
134
+
135
+ // --- Writable store ---
136
+ /** @type {import('svelte/store').Writable<TrackingState>} */
137
+ export const trackingPreferences = writable(
138
+ browser
139
+ ? computePreferences()
140
+ : {
141
+ optedIn: false,
142
+ optedOut: false,
143
+ dnt: false,
144
+ gpc: false,
145
+ enabled: false,
146
+ status: "⏳ Checking tracking preferences...",
147
+ },
148
+ );
149
+
150
+ /**
151
+ * Returns true if the user manually set a tracking preference cookie.
152
+ * @returns {boolean}
153
+ */
154
+ function hasUserManuallySetTrackingPreference() {
155
+ const cookies = readCookies();
156
+ return (
157
+ cookies.includes("enable_tracking=true") ||
158
+ cookies.includes("disable_tracking=true")
159
+ );
160
+ }
161
+
162
+ /**
163
+ * Determines if user should be reminded to reconsent (after 6 months).
164
+ *
165
+ * @param {TrackingState} $prefs - The current tracking preferences.
166
+ * @returns {boolean}
167
+ * @type {import("svelte/store").Readable<boolean>}
168
+ */
169
+ export const remindUserToReconsent = derived(trackingPreferences, (_prefs) => {
170
+ if (!browser) return false;
171
+ if (!hasUserManuallySetTrackingPreference()) return false;
172
+
173
+ const match = readCookies().match(/tracking_consent_timestamp=(\d+)/);
174
+ if (!match) return true;
175
+
176
+ const timestamp = Number(match[1]);
177
+ if (isNaN(timestamp)) return true;
178
+
179
+ const age = Date.now() - timestamp;
180
+ return age > 1000 * 60 * 60 * 24 * 180; // 6 months
181
+ });
182
+
183
+ /**
184
+ * Force-refresh current preferences
185
+ * @returns {void}
186
+ */
187
+ export function refreshTrackingPreferences() {
188
+ if (browser) trackingPreferences.set(computePreferences());
189
+ }
190
+
191
+ /**
192
+ * Enable tracking by setting opt-in cookie
193
+ * @returns {void}
194
+ */
195
+ export function setOptIn() {
196
+ setCookie("enable_tracking");
197
+ removeCookie("disable_tracking");
198
+ setConsentTimestamp();
199
+ refreshTrackingPreferences();
200
+ }
201
+
202
+ /**
203
+ * Disable tracking by setting opt-out cookie
204
+ * @returns {void}
205
+ */
206
+ export function setOptOut() {
207
+ setCookie("disable_tracking");
208
+ removeCookie("enable_tracking");
209
+ setConsentTimestamp();
210
+ refreshTrackingPreferences();
211
+ }
212
+
213
+ /**
214
+ * Clear all tracking preference cookies
215
+ * @returns {void}
216
+ */
217
+ export function clearPrefs() {
218
+ removeCookie("enable_tracking");
219
+ removeCookie("disable_tracking");
220
+ removeConsentTimestamp();
221
+ refreshTrackingPreferences();
222
+ }
@@ -506,3 +506,15 @@ footer .container {
506
506
  font-family: inherit; /* Ensure it uses the same font-family as normal text */
507
507
  font-style: normal; /* Remove italic for the description */
508
508
  }
509
+
510
+ .sr-only {
511
+ position: absolute;
512
+ width: 1px;
513
+ height: 1px;
514
+ padding: 0;
515
+ margin: -1px;
516
+ border: 0;
517
+ clip: rect(0, 0, 0, 0);
518
+ overflow: hidden;
519
+ white-space: nowrap;
520
+ }
@@ -3,4 +3,4 @@ Copyright © 2025 Network Pro Strategies (Network Pro™)
3
3
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
4
4
  This file is part of Network Pro.
5
5
  ========================================================================== */
6
- html{-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{margin:.67em 0;font-size:2em}hr{box-sizing:content-box}pre{font-family:monospace;font-size:1em}a{background-color:#0000}abbr[title]{border-bottom:none;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted buttontext}fieldset{padding:.35em .75em .625em}legend{color:inherit;box-sizing:border-box;white-space:normal;max-width:100%;padding:0;display:table}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}html{color:#222;scroll-behavior:smooth;font-size:1em;line-height:1.4}::-moz-selection{text-shadow:none;background:#191919}::selection{text-shadow:none;background:#191919}hr{border:0;border-top:1px solid #ccc;height:1px;margin:1em 0;padding:0;display:block;overflow:visible}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}body{color:#fafafa;background-color:#191919;margin:10px;font-family:Arial,Helvetica,sans-serif}a{text-decoration:none}a:link{color:#ffc627}a:hover,a:active{color:#ffc627;text-decoration:underline}a:focus{color:#191919;background-color:#ffc627}a:visited,a:visited:hover{color:#7f6227}.hidden,[hidden]{display:none!important}.visually-hidden{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.visually-hidden.focusable:active,.visually-hidden.focusable:focus{clip:auto;width:auto;height:auto;white-space:inherit;margin:0;position:static;overflow:visible}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}@media print{*,:before,:after{color:#000!important;box-shadow:none!important;text-shadow:none!important;background:#fff!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^=\#]:after,a[href^=javascript\:]:after{content:""}pre{white-space:pre-wrap!important}pre,blockquote{page-break-inside:avoid;border:1px solid #999}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.full-width-section{background-position:50%;background-size:cover;width:100%;max-width:1920px;margin:0 auto}.container{max-width:1200px;margin:0 auto;padding:0 12px}.readable{max-width:900px;margin:0 auto}header,footer{width:100%}header .container,footer .container{max-width:1200px;margin:0 auto;padding:20px 12px}.gh{border-collapse:collapse;border-spacing:0;margin:0 auto}.gh td,.gh th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.gh .gh-tcell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.gh,.gh col{width:auto!important}.gh-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.soc{border-collapse:collapse;border-spacing:0;margin:0 auto}.soc td,.soc th{border-collapse:collapse;word-break:normal;padding:8px;overflow:hidden}.soc .soc-fa{text-align:center;vertical-align:middle}@media screen and (width<=767px){.soc,.soc col{width:auto!important}.soc-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.foss{border-collapse:collapse;border-spacing:0}.foss td,.foss th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.foss .foss-cell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.foss,.foss col{width:auto!important}.foss-wrap{-webkit-overflow-scrolling:touch;overflow-x:auto}}.bnav{text-align:center;border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav td,.bnav th{text-align:center;vertical-align:middle;word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav .bnav-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav,.bnav col{width:auto!important}.bnav-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.bnav2{border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav2 td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav2 th{word-break:normal;border-style:none;padding:12px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.bnav2 .bnav2-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav2,.bnav2 col{width:auto!important}.bnav2-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.pgp{border-collapse:collapse;border-spacing:0;margin:0 auto}.pgp td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp th{word-break:normal;border:1px solid #000;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp .pgp-col1{text-align:right;vertical-align:middle;padding-right:1rem}.pgp .pgp-col2{text-align:left;vertical-align:middle;padding-left:1rem}@media screen and (width<=767px){.pgp,.pgp col{width:auto!important}.pgp-wrap{-webkit-overflow-scrolling:touch;margin:2rem 0 auto;overflow-x:auto}}.logo{margin-left:auto;margin-right:auto;display:block}.index-title1{text-align:center;font-style:italic;font-weight:700}.index-title2{letter-spacing:-.015em;text-align:center;font-variant:small-caps;font-size:1.25rem;line-height:1.625rem}.index1{letter-spacing:-.035em;text-align:center;font-style:italic;font-weight:700;line-height:2.125rem}.index2{letter-spacing:-.035em;text-align:center;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.index3{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem}.index4{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem;text-decoration:underline}.subhead{letter-spacing:-.035em;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.bold{font-weight:700}.emphasis{font-style:italic}.uline{text-decoration:underline}.bolditalic{font-style:italic;font-weight:700}.bquote{border-left:3px solid #9e9e9e;margin-left:30px;padding-left:10px;font-style:italic}.small-text{font-size:.75rem;line-height:1.125rem}.large-text-center{text-align:center;font-size:1.25rem;line-height:1.75rem}.prewrap{white-space:pre-wrap;display:block}.hr-styled{width:75%;margin:auto}.center-text{text-align:center}.copyright{text-align:center;font-size:.75rem;line-height:1.125rem}.gold{color:#ffc627}.visited{color:#7f6227}.goldseparator{color:#ffc627;margin:0 .5rem}.center-nav{text-align:center;padding:5px;font-size:1rem;line-height:1.5rem}.block{resize:none;background:0 0;border:none;border-radius:0;outline:none;width:100%;font-size:.75rem;line-height:1.125rem}.fingerprint{white-space:pre-line;font-weight:700;display:block}.pgp-image{width:150px;height:150px}.spacer{margin:2rem 0}.separator{margin:0 .5rem}.emoji{margin-right:8px}.headline{margin-bottom:4px;font-style:italic;font-weight:700;display:block}.label{font-family:inherit;font-weight:700}.description{font-family:inherit;font-style:normal;font-weight:400;display:inline}
6
+ html{-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{margin:.67em 0;font-size:2em}hr{box-sizing:content-box}pre{font-family:monospace;font-size:1em}a{background-color:#0000}abbr[title]{border-bottom:none;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted buttontext}fieldset{padding:.35em .75em .625em}legend{color:inherit;box-sizing:border-box;white-space:normal;max-width:100%;padding:0;display:table}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}html{color:#222;scroll-behavior:smooth;font-size:1em;line-height:1.4}::-moz-selection{text-shadow:none;background:#191919}::selection{text-shadow:none;background:#191919}hr{border:0;border-top:1px solid #ccc;height:1px;margin:1em 0;padding:0;display:block;overflow:visible}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}body{color:#fafafa;background-color:#191919;margin:10px;font-family:Arial,Helvetica,sans-serif}a{text-decoration:none}a:link{color:#ffc627}a:hover,a:active{color:#ffc627;text-decoration:underline}a:focus{color:#191919;background-color:#ffc627}a:visited,a:visited:hover{color:#7f6227}.hidden,[hidden]{display:none!important}.visually-hidden{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.visually-hidden.focusable:active,.visually-hidden.focusable:focus{clip:auto;width:auto;height:auto;white-space:inherit;margin:0;position:static;overflow:visible}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}@media print{*,:before,:after{color:#000!important;box-shadow:none!important;text-shadow:none!important;background:#fff!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^=\#]:after,a[href^=javascript\:]:after{content:""}pre{white-space:pre-wrap!important}pre,blockquote{page-break-inside:avoid;border:1px solid #999}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.full-width-section{background-position:50%;background-size:cover;width:100%;max-width:1920px;margin:0 auto}.container{max-width:1200px;margin:0 auto;padding:0 12px}.readable{max-width:900px;margin:0 auto}header,footer{width:100%}header .container,footer .container{max-width:1200px;margin:0 auto;padding:20px 12px}.gh{border-collapse:collapse;border-spacing:0;margin:0 auto}.gh td,.gh th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.gh .gh-tcell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.gh,.gh col{width:auto!important}.gh-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.soc{border-collapse:collapse;border-spacing:0;margin:0 auto}.soc td,.soc th{border-collapse:collapse;word-break:normal;padding:8px;overflow:hidden}.soc .soc-fa{text-align:center;vertical-align:middle}@media screen and (width<=767px){.soc,.soc col{width:auto!important}.soc-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.foss{border-collapse:collapse;border-spacing:0}.foss td,.foss th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.foss .foss-cell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.foss,.foss col{width:auto!important}.foss-wrap{-webkit-overflow-scrolling:touch;overflow-x:auto}}.bnav{text-align:center;border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav td,.bnav th{text-align:center;vertical-align:middle;word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav .bnav-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav,.bnav col{width:auto!important}.bnav-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.bnav2{border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav2 td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav2 th{word-break:normal;border-style:none;padding:12px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.bnav2 .bnav2-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav2,.bnav2 col{width:auto!important}.bnav2-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.pgp{border-collapse:collapse;border-spacing:0;margin:0 auto}.pgp td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp th{word-break:normal;border:1px solid #000;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp .pgp-col1{text-align:right;vertical-align:middle;padding-right:1rem}.pgp .pgp-col2{text-align:left;vertical-align:middle;padding-left:1rem}@media screen and (width<=767px){.pgp,.pgp col{width:auto!important}.pgp-wrap{-webkit-overflow-scrolling:touch;margin:2rem 0 auto;overflow-x:auto}}.logo{margin-left:auto;margin-right:auto;display:block}.index-title1{text-align:center;font-style:italic;font-weight:700}.index-title2{letter-spacing:-.015em;text-align:center;font-variant:small-caps;font-size:1.25rem;line-height:1.625rem}.index1{letter-spacing:-.035em;text-align:center;font-style:italic;font-weight:700;line-height:2.125rem}.index2{letter-spacing:-.035em;text-align:center;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.index3{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem}.index4{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem;text-decoration:underline}.subhead{letter-spacing:-.035em;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.bold{font-weight:700}.emphasis{font-style:italic}.uline{text-decoration:underline}.bolditalic{font-style:italic;font-weight:700}.bquote{border-left:3px solid #9e9e9e;margin-left:30px;padding-left:10px;font-style:italic}.small-text{font-size:.75rem;line-height:1.125rem}.large-text-center{text-align:center;font-size:1.25rem;line-height:1.75rem}.prewrap{white-space:pre-wrap;display:block}.hr-styled{width:75%;margin:auto}.center-text{text-align:center}.copyright{text-align:center;font-size:.75rem;line-height:1.125rem}.gold{color:#ffc627}.visited{color:#7f6227}.goldseparator{color:#ffc627;margin:0 .5rem}.center-nav{text-align:center;padding:5px;font-size:1rem;line-height:1.5rem}.block{resize:none;background:0 0;border:none;border-radius:0;outline:none;width:100%;font-size:.75rem;line-height:1.125rem}.fingerprint{white-space:pre-line;font-weight:700;display:block}.pgp-image{width:150px;height:150px}.spacer{margin:2rem 0}.separator{margin:0 .5rem}.emoji{margin-right:8px}.headline{margin-bottom:4px;font-style:italic;font-weight:700;display:block}.label{font-family:inherit;font-weight:700}.description{font-family:inherit;font-style:normal;font-weight:400;display:inline}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}
@@ -0,0 +1,60 @@
1
+ /* ==========================================================================
2
+ src/lib/types/appConstants.js
3
+
4
+ Copyright © 2025 Network Pro Strategies (Network Pro™)
5
+ SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
6
+ This file is part of Network Pro.
7
+ ========================================================================== */
8
+
9
+ /**
10
+ * @file appConstants.js
11
+ * @description Type definitions for app constants in src/lib/index.js
12
+ * @module src/lib/types
13
+ * @author SunDevil311
14
+ * @updated 2025-06-03
15
+ */
16
+
17
+ /**
18
+ * @typedef {object} CompanyInfo
19
+ * @property {string} NAME - Full company name
20
+ * @property {string} APP_NAME - Application name
21
+ * @property {string} YEAR - Current copyright year
22
+ */
23
+
24
+ /**
25
+ * @typedef {object} ContactInfo
26
+ * @property {string} EMAIL - Primary contact email
27
+ * @property {string} SECURE - Secure contact email
28
+ * @property {string} PRIVACY - Privacy policy email
29
+ * @property {string} PHONE - Support phone number
30
+ */
31
+
32
+ /**
33
+ * @typedef {object} PageTargets
34
+ * @property {string} BLANK - Value for `target="_blank"`
35
+ * @property {string} SELF - Value for `target="_self"`
36
+ * @property {string} REL - Value for `rel="noopener noreferrer"`
37
+ */
38
+
39
+ /**
40
+ * @typedef {object} NavigationLabels
41
+ * @property {string} BACKTOP
42
+ * @property {string} HREFTOP
43
+ */
44
+
45
+ /**
46
+ * @typedef {object} Links
47
+ * @property {string} HOME - Main website URL
48
+ * @property {string} BLOG - External blog URL
49
+ */
50
+
51
+ /**
52
+ * @typedef {object} AppConstants
53
+ * @property {CompanyInfo} COMPANY_INFO
54
+ * @property {ContactInfo} CONTACT
55
+ * @property {PageTargets} PAGE
56
+ * @property {NavigationLabels} NAV
57
+ * @property {Links} LINKS
58
+ */
59
+
60
+ export {};
@@ -1,3 +1,20 @@
1
+ /* ==========================================================================
2
+ src/lib/types/fossTypes.js
3
+
4
+ Copyright © 2025 Network Pro Strategies (Network Pro™)
5
+ SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
6
+ This file is part of Network Pro.
7
+ ========================================================================== */
8
+
9
+ /**
10
+ * @file fossTypes.js
11
+ * @description Type definitions for `fossItem` in
12
+ * src/lib/components/foss/FossItemContent.svelte
13
+ * @module src/lib/types
14
+ * @author SunDevil311
15
+ * @updated 2025-06-03
16
+ */
17
+
1
18
  /**
2
19
  * @typedef {object} FossLink
3
20
  * @property {string} [label]
@@ -42,5 +42,3 @@ export function load({ url }) {
42
42
  meta: currentMeta, // Return the meta data (either from the route or the fallback)
43
43
  };
44
44
  }
45
-
46
- // cspell:ignore posthog
@@ -9,61 +9,59 @@ This file is part of Network Pro.
9
9
  <script>
10
10
  export let data;
11
11
 
12
+ import { onMount } from "svelte";
13
+ import { afterNavigate } from "$app/navigation";
14
+ import { initPostHog, showReminder, capture } from "$lib/stores/posthog";
15
+ import { registerServiceWorker } from "$lib/registerServiceWorker.js";
16
+ import { browser } from "$app/environment";
17
+
12
18
  import ContainerSection from "$lib/components/ContainerSection.svelte";
13
19
  import Footer from "$lib/components/layout/Footer.svelte";
14
20
  import HeaderDefault from "$lib/components/layout/HeaderDefault.svelte";
15
21
  import HeaderHome from "$lib/components/layout/HeaderHome.svelte";
16
22
  import PWAInstallButton from "$lib/components/PWAInstallButton.svelte";
17
- import { shouldTrackUser } from "$lib/utils/privacy.js";
18
- import { onMount } from "svelte";
19
- import { registerServiceWorker } from "$lib/registerServiceWorker.js";
20
- import { browser } from "$app/environment";
23
+
21
24
  import "$lib/styles/global.min.css";
22
25
  import "$lib/styles/fa-global.css";
23
26
 
24
- // Import favicon images
25
27
  import logoPng from "$lib/img/logo-web.png";
26
28
  import logoWbp from "$lib/img/logo-web.webp";
27
29
  import faviconSvg from "$lib/img/favicon.svg";
28
30
  import appleTouchIcon from "$lib/img/icon-180x180.png";
29
31
 
30
- // Declare PostHog as null initially
31
- /** @type {typeof import('$lib/components/PostHog.svelte').default | null} */
32
- let PostHog = null;
32
+ $: shouldShowReminder = $showReminder;
33
33
 
34
- if (browser) {
35
- // Preload all core images (logos + apple touch)
36
- [logoPng, logoWbp, appleTouchIcon].forEach((src) => {
37
- const img = new Image();
38
- img.src = src;
39
- });
34
+ onMount(() => {
35
+ console.log("[APP] onMount triggered in +layout.svelte");
36
+
37
+ registerServiceWorker();
38
+ initPostHog();
40
39
 
41
- // Run setup when component mounts (only in browser)
42
- onMount(() => {
43
- console.log("[APP] onMount triggered in +layout.svelte");
44
- registerServiceWorker();
40
+ // Register navigation tracking only on client
41
+ afterNavigate(() => {
42
+ capture("$pageview");
43
+ });
45
44
 
45
+ if (browser) {
46
46
  const isDev = import.meta.env.MODE === "development";
47
47
 
48
- console.log("ENV MODE =", import.meta.env.MODE); // Should be "development"
49
- console.log("isDev =", isDev);
50
- console.log("shouldTrackUser =", shouldTrackUser());
51
-
52
- if (isDev || shouldTrackUser()) {
53
- import("$lib/components/PostHog.svelte").then((module) => {
54
- PostHog = module.default;
55
-
56
- if (isDev) {
57
- console.log("[Dev] ✅ PostHog component loaded (tracking enabled)");
58
- }
59
- });
60
- } else {
61
- console.log(
62
- "[Privacy] ⛔ Skipping PostHog component due to DNT or GPC signal.",
63
- );
48
+ // Check for ?debug=true in URL (no persistence)
49
+ const urlParams = new URLSearchParams(window.location.search);
50
+ const debug = urlParams.get("debug") === "true";
51
+
52
+ if (isDev || debug) {
53
+ console.log("ENV MODE =", import.meta.env.MODE);
54
+ console.log("isDev =", isDev);
55
+ console.log("debug param =", debug);
64
56
  }
65
- });
66
- }
57
+
58
+ // Preload logo assets
59
+ [logoPng, logoWbp, appleTouchIcon].forEach((src) => {
60
+ const img = new Image();
61
+ img.src = src;
62
+ });
63
+ }
64
+ });
67
65
 
68
66
  // fallback values if data.meta not set
69
67
  const metaTitle =
@@ -103,10 +101,6 @@ This file is part of Network Pro.
103
101
  </header>
104
102
  <!-- END HEADER -->
105
103
 
106
- {#if PostHog}
107
- <PostHog /> <!-- Add PostHog component when it's loaded -->
108
- {/if}
109
-
110
104
  <main>
111
105
  <slot />
112
106
  </main>
@@ -0,0 +1,63 @@
1
+ # Tracking Preferences & Privacy Signals
2
+
3
+ <!-- markdownlint-disable MD018 -->
4
+
5
+ ## Overview
6
+
7
+ This document explains how Network Pro handles analytics tracking in a privacy-aware, user-consented, and standards-compliant manner. It covers:
8
+
9
+ - Tracking preference storage (cookies)
10
+ - Browser signals (DNT and GPC)
11
+ - Reconsent logic
12
+ - Reactive store architecture
13
+
14
+ ### 🔐 Principles
15
+
16
+ - **Privacy by default**: Tracking is disabled when browser signals indicate user preference (DNT/GPC).
17
+ - **Explicit consent**: Users may opt-in or opt-out manually, overriding signals.
18
+ - **Persistent choice**: Consent state is remembered via first-party cookies.
19
+ - **Transparency**: The tracking status is shown to users in the UI.
20
+
21
+ ### 🧠 Architecture Summary
22
+
23
+ - **Store**: `src/lib/stores/trackingPreferences.js`
24
+ - Consolidates logic for cookie preferences, browser signals, and consent state.
25
+ - SSR-safe, reactive, and fully declarative.
26
+ - **Deprecated**:
27
+ - `utils/privacy.js` → replaced by derived store logic.
28
+ - `utils/trackingCookies.js` → merged into the store with SSR-safe cookie APIs.
29
+
30
+ ### Reactive State
31
+
32
+ | Store | Type | Description |
33
+ | ----------------------- | ------------------------- | ----------------------------------------------------------------------- |
34
+ | `trackingPreferences` | `Readable<TrackingState>` | Contains current tracking metadata (opt-in/out, DNT, GPC, status, etc). |
35
+ | `trackingEnabled` | `Writable<boolean>` | Exposed to toggle or query PostHog tracking state reactively. |
36
+ | `remindUserToReconsent` | `Readable<boolean>` | Indicates whether a consent renewal prompt should be shown. |
37
+ | `showReminder` | `Writable<boolean>` | Used by PostHog to conditionally display a reminder or banner. |
38
+
39
+ ### ⏳ Reconsent Logic
40
+
41
+ The derived store `remindUserToReconsent` evaluates whether a user should be reminded to re-consent to tracking.
42
+
43
+ It checks for:
44
+
45
+ - Manual opt-in or opt-out
46
+ - A valid `tracking_consent_timestamp` cookie
47
+ - Whether 6+ months have elapsed since that timestamp
48
+
49
+ ### ⚙️ Developer Notes
50
+
51
+ - Changes to tracking preferences update cookies and reactive state
52
+ - Reconsent timestamp is written/cleared via store utility functions
53
+ - Use `$trackingPreferences` and `remindUserToReconsent` wherever reactive values are needed
54
+
55
+ ### 💡 Related Components
56
+
57
+ | File | Purpose |
58
+ | ------------------------------- | -------------------------------------------------------------------------------- |
59
+ | `+layout.svelte` | Initializes PostHog client and service worker; references `trackingPreferences`. |
60
+ | `PrivacyDashboard.svelte` | UI control panel for opt-in/out toggles and consent status display. |
61
+ | `PrivacyContent.svelte` | Informational content rendered in modals, footers, and standalone pages. |
62
+ | `stores/trackingPreferences.js` | Primary source of truth; tracks and derives tracking state. |
63
+ | `stores/posthog.js` | Encapsulates privacy-safe analytics setup and event capture logic. |
Binary file
@@ -1,7 +1,6 @@
1
1
  /*! ==========================================================================
2
- src/lib/styles/css/offline.css
3
-
2
+ Copyright © 2025 Network Pro Strategies (Network Pro™)
4
3
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
5
4
  This file is part of Network Pro.
6
5
  =========================================================================== */
7
- html,body{height:100%;margin:0;padding:0}body{color:#fafafa;text-align:center;background-color:#191919;flex-direction:column;justify-content:center;align-items:center;margin:10px;padding:0 1rem;font-family:Arial,Helvetica,sans-serif;display:flex}.container{max-width:600px;margin:0 auto}h1{color:#fafafa;margin-bottom:1rem;font-size:2rem}p{margin-bottom:1.5rem;font-size:1.1rem;line-height:1.5}.icon{color:#ff5252;margin-bottom:1rem;font-size:4rem}.retry-button{color:#fafafa;cursor:pointer;background-color:#2e7d32;border:none;border-radius:4px;margin-top:1rem;padding:12px 24px;font-family:Arial,Helvetica,sans-serif;font-size:1rem;transition:background-color .2s}.retry-button:hover{background-color:#388e3c}.status{color:#bdbdbd;background-color:#ffffff0d;border-radius:4px;margin:1rem 0;padding:1rem;font-size:.9rem}a{text-decoration:none}a:link{color:#ffc627}a:hover,a:active{color:#ffc627;text-decoration:underline}a:focus{color:#191919;background-color:#ffc627}a:visited,a:visited:hover{color:#7f6227}.help-text{color:#bdbdbd;margin-top:2rem;font-size:.9rem}
6
+ html,body{height:100%;margin:0;padding:0}body{color:#fafafa;text-align:center;background-color:#191919;flex-direction:column;justify-content:center;align-items:center;margin:10px;padding:0 1rem;font-family:Arial,Helvetica,sans-serif;display:flex}.container{max-width:600px;margin:0 auto}h1{color:#fafafa;margin-bottom:1rem;font-size:2rem}p{margin-bottom:1.5rem;font-size:1.1rem;line-height:1.5}.icon{color:#ff5252;margin-bottom:1rem;font-size:4rem}.retry-button{color:#fafafa;cursor:pointer;background-color:#2e7d32;border:none;border-radius:4px;margin-top:1rem;padding:12px 24px;font-family:Arial,Helvetica,sans-serif;font-size:1rem;transition:background-color .2s}.retry-button:hover{background-color:#388e3c}.status{color:#bdbdbd;background-color:#ffffff0d;border-radius:4px;margin:1rem 0;padding:1rem;font-size:.9rem}a{text-decoration:none}a:link{color:#ffc627}a:hover,a:active{color:#ffc627;text-decoration:underline}a:focus{color:#191919;background-color:#ffc627}a:visited,a:visited:hover{color:#7f6227}.help-text{color:#bdbdbd;margin-top:2rem;font-size:.9rem}
package/svelte.config.js CHANGED
@@ -24,12 +24,14 @@ const config = {
24
24
  // Netlify adapter configuration
25
25
  adapter: adapter({
26
26
  edge: false, // Disable edge functions (optional, enable if needed)
27
- split: false, // Disable splitting function files (optional, enable if needed),
27
+ split: false, // Disable splitting function files (optional, enable if needed)
28
28
  }),
29
+
29
30
  // Paths configuration for deployment
30
31
  paths: {
31
32
  base: "", // Always deploy to the root of the domain
32
33
  },
34
+
33
35
  prerender: {
34
36
  // Handle HTTP errors during prerendering
35
37
  handleHttpError: ({ path, _referrer, message }) => {
@@ -46,6 +48,7 @@ const config = {
46
48
  },
47
49
  },
48
50
  },
51
+
49
52
  // File extensions for Svelte and mdsvex
50
53
  extensions: [".svelte", ".svx", ".md"], // Added .md for Markdown support
51
54
  };
package/vite.config.js CHANGED
@@ -9,19 +9,17 @@ This file is part of Network Pro.
9
9
  import { sveltekit } from "@sveltejs/kit/vite";
10
10
  import { defineConfig } from "vite";
11
11
  import lightningcssPlugin from "vite-plugin-lightningcss";
12
+ import tsconfigPaths from "vite-tsconfig-paths"; // NEW: tsconfig/jsconfig alias support
12
13
 
13
14
  export default defineConfig({
14
15
  plugins: [
16
+ tsconfigPaths(), // Insert before sveltekit()
15
17
  sveltekit(),
16
18
  lightningcssPlugin({
17
19
  minify: process.env.NODE_ENV === "production",
18
20
  pruneUnusedFontFaceRules: true,
19
21
  pruneUnusedKeyframes: true,
20
22
  removeUnusedFontFaces: true,
21
- // Enables nesting support in Lightning CSS
22
- //drafts: {
23
- // nesting: true
24
- //}
25
23
  }),
26
24
  ],
27
25
  });