@hubpav/hwio-web-runtime 0.4.0 → 0.6.0

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.
package/dist/consent.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { hwioMount } from './mount.js';
2
2
  import { HWIO_CONSENT_COOKIE, LEGACY_CONSENT_COOKIE, hwioBaseCategories, hwioConsentToGoogle, hwioCookieString, hwioDisallowedCookieNames, hwioExpireCookieString, hwioHasTrackingConsent, hwioMakeReceipt, hwioParseReceipt, hwioReadCookie, } from './consent-core.js';
3
3
  const ATTRIBUTION_KEYS = ['hwio_attribution', 'hw_attribution']; // legacy-ok
4
- const THEME_KEYS = ['hwio_theme', 'hwio_theme_consent_version', 'theme', 'hw_theme_consent_version']; // legacy-ok
5
4
  /**
6
5
  * Consent runtime. Markup contract (rendered by HWioCookieConsent):
7
6
  * [data-hwio-consent data-version data-gtm? data-locale] banner root (hidden attr toggles it)
@@ -77,14 +76,6 @@ export function hwioConsentRuntime() {
77
76
  try {
78
77
  if (!categories.marketing)
79
78
  ATTRIBUTION_KEYS.forEach((k) => sessionStorage.removeItem(k));
80
- if (!categories.preferences)
81
- THEME_KEYS.forEach((k) => localStorage.removeItem(k));
82
- else {
83
- if (localStorage.getItem('hwio_theme'))
84
- localStorage.setItem('hwio_theme_consent_version', version);
85
- if (localStorage.getItem('theme'))
86
- localStorage.setItem('hw_theme_consent_version', version); // legacy-ok
87
- }
88
79
  }
89
80
  catch {
90
81
  // storage unavailable
package/dist/forms.js CHANGED
@@ -101,6 +101,22 @@ export function hwioFormsRuntime() {
101
101
  status.hidden = true;
102
102
  }
103
103
  await hwioEnsureTurnstileToken(form);
104
+ if (form.querySelector('.cf-turnstile') && !form.querySelector('input[name="cf-turnstile-response"]')?.value) {
105
+ // The widget produced no token (script blocked, unsupported browser, timeout): say so instead of
106
+ // sending a request the endpoint has to reject.
107
+ hwioResetTurnstile(form);
108
+ if (submitBtn) {
109
+ submitBtn.disabled = false;
110
+ submitBtn.textContent = original;
111
+ }
112
+ if (status) {
113
+ status.hidden = false;
114
+ status.textContent = form.dataset.hsTurnstileMsg || error;
115
+ status.classList.add('hs-status-error');
116
+ }
117
+ console.error('form submission blocked: no Turnstile token');
118
+ return;
119
+ }
104
120
  let fields = {};
105
121
  for (const [name, value] of new FormData(form).entries())
106
122
  if (typeof value === 'string')
package/dist/inline.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export const consentDefaults = { "code": "(function(){window.dataLayer=window.dataLayer||[];window.gtag=window.gtag||function(){window.dataLayer.push(arguments)};window.gtag('consent','default',{ad_storage:'denied',ad_user_data:'denied',ad_personalization:'denied',analytics_storage:'denied',functionality_storage:'denied',personalization_storage:'denied',security_storage:'granted'})})();", "hash": "sha256-8ypBBYlrPDUYAWyidrG8ZMBi91IYR2VYNNXABFu2aI8=" };
2
2
  export const fontGate = { "code": "(function(){var h=document.documentElement,d=h.dataset,fam=d.hwioFontFamily;if(!fam||!('fonts' in document)){h.classList.remove('fonts-loading');h.classList.add('fonts-ready');return}var T=5000;function load(s){return new Promise(function(res,rej){var id=setTimeout(function(){rej(new Error('font timeout'))},T);document.fonts.load('400 1em \"'+fam+'\"',s).then(function(f){clearTimeout(id);res(f)},function(e){clearTimeout(id);rej(e)})})}if(!window.__hwioFontGate){window.__hwioFontGate={state:'loading',ready:Promise.all([load(d.hwioFontSample||'HARDWARIO'),load(d.hwioFontSampleExt||'Řešení Vývojáři Odwiedź')])};document.addEventListener('astro:before-swap',function(ev){var n=ev.newDocument.documentElement;n.classList.add('js');n.classList.remove('fonts-loading');n.classList.toggle('fonts-failed',window.__hwioFontGate.state==='failed');n.classList.add('fonts-ready');var cur=h.getAttribute('data-theme');if(cur)n.setAttribute('data-theme',cur);else n.removeAttribute('data-theme')})}window.__hwioFontGate.ready.then(function(){window.__hwioFontGate.state='ready';h.classList.remove('fonts-loading');h.classList.add('fonts-ready')},function(){window.__hwioFontGate.state='failed';h.classList.remove('fonts-loading');h.classList.add('fonts-failed','fonts-ready')})})();", "hash": "sha256-yZ5zwDTjvBj25m7DRmqzRrznrnc2PqP3T7FWx0ftKO8=" };
3
- export const themeBootstrap = { "code": "(function(){var h=document.documentElement,d=h.dataset;h.classList.add('js');try{var v=d.hwioConsentVersion;var sv=localStorage.getItem('hwio_theme_consent_version')||localStorage.getItem('hw_theme_consent_version');if(v&&sv===v){var t=localStorage.getItem('hwio_theme')||localStorage.getItem('theme');if(t==='dark'&&d.hwioThemeDark)h.setAttribute('data-theme',d.hwioThemeDark);else if(t==='light'&&d.hwioThemeLight)h.setAttribute('data-theme',d.hwioThemeLight)}}catch(e){}if(!h.getAttribute('data-theme')&&d.hwioThemeLight){var dk=false;try{dk=matchMedia('(prefers-color-scheme: dark)').matches}catch(e){}h.setAttribute('data-theme',dk&&d.hwioThemeDark?d.hwioThemeDark:d.hwioThemeLight)}})();", "hash": "sha256-2j6kEk35lbk4ErIco/G03YuZwTwDNma2HXX466e1BlI=" }; // legacy-ok: reads the pre-design-system storage keys once
3
+ export const themeBootstrap = { "code": "(function(){var h=document.documentElement,d=h.dataset;h.classList.add('js');var t=null;try{if(d.hwioThemeCookieDomain){var m=document.cookie.match(/(?:^|; )hwio_theme=(dark|light)/);if(m)t=m[1]}if(!t)t=localStorage.getItem('hwio_theme')||localStorage.getItem('theme')}catch(e){}if(!h.getAttribute('data-theme')&&d.hwioThemeLight){h.setAttribute('data-theme',t==='dark'&&d.hwioThemeDark?d.hwioThemeDark:d.hwioThemeLight)}})();", "hash": "sha256-N+PCgo10dW74h2jKsCZVedLAmTLJeWIaNkhmjfY8r1Q=" };
4
4
  export const hwioInlineHashes = [consentDefaults.hash, fontGate.hash, themeBootstrap.hash];
package/dist/nav.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Header behaviours: keeps aria-expanded on the DaisyUI drawer label in sync with its checkbox,
3
- * closes the drawer when a link inside it is followed, and marks the nav link whose section is
4
- * in view ([data-hwio-scrollspy] nav with same-page anchors) with aria-current="location".
3
+ * closes the drawer when a link inside it is followed, opens HWioHeader's mega panels
4
+ * ([data-hwio-mega-trigger] buttons and their [data-hwio-mega-panel]; hover with intent on pointer
5
+ * devices, click, ArrowDown into the panel, Escape / outside click / focus leaving close; one at a
6
+ * time), and marks the nav link whose section is in view ([data-hwio-scrollspy] nav with same-page
7
+ * anchors) with aria-current="location".
5
8
  */
6
9
  export declare function hwioNavRuntime(): import("./mount.js").HWioBinder;
package/dist/nav.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import { hwioMount } from './mount.js';
2
2
  /**
3
3
  * Header behaviours: keeps aria-expanded on the DaisyUI drawer label in sync with its checkbox,
4
- * closes the drawer when a link inside it is followed, and marks the nav link whose section is
5
- * in view ([data-hwio-scrollspy] nav with same-page anchors) with aria-current="location".
4
+ * closes the drawer when a link inside it is followed, opens HWioHeader's mega panels
5
+ * ([data-hwio-mega-trigger] buttons and their [data-hwio-mega-panel]; hover with intent on pointer
6
+ * devices, click, ArrowDown into the panel, Escape / outside click / focus leaving close; one at a
7
+ * time), and marks the nav link whose section is in view ([data-hwio-scrollspy] nav with same-page
8
+ * anchors) with aria-current="location".
6
9
  */
7
10
  export function hwioNavRuntime() {
8
11
  let controller = null;
@@ -29,6 +32,54 @@ export function hwioNavRuntime() {
29
32
  sync();
30
33
  } }, { signal });
31
34
  });
35
+ const triggers = Array.from(document.querySelectorAll('[data-hwio-mega-trigger]'));
36
+ if (triggers.length) {
37
+ const panelOf = (trigger) => document.querySelector(`[data-hwio-mega-panel="${trigger.dataset.hwioMegaTrigger}"]`);
38
+ const pairs = triggers.map((trigger) => ({ trigger, panel: panelOf(trigger) })).filter((p) => !!p.panel);
39
+ const hoverable = window.matchMedia('(hover: hover)').matches;
40
+ let closeTimer = null;
41
+ const cancelClose = () => { if (closeTimer) {
42
+ clearTimeout(closeTimer);
43
+ closeTimer = null;
44
+ } };
45
+ const set = (pair, open) => { pair.trigger.setAttribute('aria-expanded', String(open)); pair.panel.hidden = !open; };
46
+ const closeAll = () => { cancelClose(); pairs.forEach((p) => set(p, false)); };
47
+ const openOne = (pair) => { cancelClose(); pairs.forEach((p) => set(p, p === pair)); };
48
+ const scheduleClose = () => { cancelClose(); closeTimer = setTimeout(closeAll, 180); };
49
+ const isOpen = (pair) => pair.trigger.getAttribute('aria-expanded') === 'true';
50
+ pairs.forEach((pair) => {
51
+ const { trigger, panel } = pair;
52
+ trigger.addEventListener('click', () => (isOpen(pair) ? closeAll() : openOne(pair)), { signal });
53
+ trigger.addEventListener('keydown', (e) => {
54
+ if (e.key !== 'ArrowDown')
55
+ return;
56
+ e.preventDefault();
57
+ openOne(pair);
58
+ panel.querySelector('a[href], button')?.focus();
59
+ }, { signal });
60
+ if (hoverable) {
61
+ trigger.addEventListener('mouseenter', () => openOne(pair), { signal });
62
+ trigger.addEventListener('mouseleave', scheduleClose, { signal });
63
+ panel.addEventListener('mouseenter', cancelClose, { signal });
64
+ panel.addEventListener('mouseleave', scheduleClose, { signal });
65
+ }
66
+ panel.addEventListener('keydown', (e) => { if (e.key === 'Escape') {
67
+ e.preventDefault();
68
+ closeAll();
69
+ trigger.focus();
70
+ } }, { signal });
71
+ panel.addEventListener('focusout', (e) => { const next = e.relatedTarget; if (next && !panel.contains(next) && next !== trigger)
72
+ closeAll(); }, { signal });
73
+ panel.querySelectorAll('a[href]').forEach((a) => a.addEventListener('click', closeAll, { signal }));
74
+ });
75
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape')
76
+ closeAll(); }, { signal });
77
+ document.addEventListener('click', (e) => {
78
+ const node = e.target;
79
+ if (!pairs.some((p) => p.trigger.contains(node) || p.panel.contains(node)))
80
+ closeAll();
81
+ }, { signal });
82
+ }
32
83
  const nav = document.querySelector('[data-hwio-scrollspy]');
33
84
  if (!nav || !('IntersectionObserver' in window))
34
85
  return;
package/dist/theme.d.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  export type HWioScheme = 'light' | 'dark';
2
- export type HWioThemeChoice = HWioScheme | 'system';
3
- /** The scheme actually in effect: explicit data-theme ending in "-dark", else the OS preference. */
2
+ export type HWioThemeChoice = HWioScheme;
3
+ /** The scheme actually in effect: the stamped data-theme ending in "-dark" means dark. */
4
4
  export declare function hwioResolvedScheme(): HWioScheme;
5
+ /**
6
+ * Applies and remembers the visitor's choice. Light is the default (owner decision 2026-09-06); the
7
+ * choice is a user-requested display preference, stored without a consent gate: localStorage on every
8
+ * site and, when <html data-hwio-theme-cookie-domain> is set, a first-party cookie shared by that
9
+ * domain's sites (www, docs, stem on .hardwario.com).
10
+ */
5
11
  export declare function hwioApplyTheme(choice: HWioThemeChoice): void;
6
12
  /**
7
13
  * Binds [data-hwio-theme-toggle] buttons (two-state light/dark) and keeps the runtime-only
package/dist/theme.js CHANGED
@@ -1,41 +1,35 @@
1
1
  import { hwioMount } from './mount.js';
2
2
  const THEME_KEY = 'hwio_theme';
3
- const THEME_VERSION_KEY = 'hwio_theme_consent_version';
4
- /** The scheme actually in effect: explicit data-theme ending in "-dark", else the OS preference. */
3
+ /** The scheme actually in effect: the stamped data-theme ending in "-dark" means dark. */
5
4
  export function hwioResolvedScheme() {
6
5
  const theme = document.documentElement.getAttribute('data-theme');
7
6
  if (theme)
8
7
  return theme.endsWith('-dark') ? 'dark' : 'light';
9
8
  if (document.documentElement.classList.contains('dark'))
10
9
  return 'dark'; // legacy-ok: www's class toggle until P2
11
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
10
+ return 'light';
12
11
  }
13
12
  function themeNames() {
14
13
  const d = document.documentElement.dataset;
15
14
  return { light: d.hwioThemeLight ?? 'hwio', dark: d.hwioThemeDark ?? 'hwio-dark' };
16
15
  }
16
+ /**
17
+ * Applies and remembers the visitor's choice. Light is the default (owner decision 2026-09-06); the
18
+ * choice is a user-requested display preference, stored without a consent gate: localStorage on every
19
+ * site and, when <html data-hwio-theme-cookie-domain> is set, a first-party cookie shared by that
20
+ * domain's sites (www, docs, stem on .hardwario.com).
21
+ */
17
22
  export function hwioApplyTheme(choice) {
18
23
  const html = document.documentElement;
19
24
  const names = themeNames();
20
- // 'system' stamps the resolved theme (never an empty attribute): non-default themes only apply when stamped.
21
- const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
22
- const dark = choice === 'system' ? systemDark : choice === 'dark';
23
- html.setAttribute('data-theme', dark ? names.dark : names.light);
25
+ html.setAttribute('data-theme', choice === 'dark' ? names.dark : names.light);
24
26
  html.classList.add('hwio-theme-transition');
25
27
  window.setTimeout(() => html.classList.remove('hwio-theme-transition'), 300);
26
28
  try {
27
- if (window.hwioConsent?.allows('preferences')) {
28
- if (choice === 'system') {
29
- localStorage.removeItem(THEME_KEY);
30
- localStorage.removeItem(THEME_VERSION_KEY);
31
- }
32
- else {
33
- localStorage.setItem(THEME_KEY, choice);
34
- const version = html.dataset.hwioConsentVersion;
35
- if (version)
36
- localStorage.setItem(THEME_VERSION_KEY, version);
37
- }
38
- }
29
+ localStorage.setItem(THEME_KEY, choice);
30
+ const domain = html.dataset.hwioThemeCookieDomain;
31
+ if (domain)
32
+ document.cookie = `${THEME_KEY}=${choice}; Domain=${domain}; Path=/; Max-Age=31536000; SameSite=Lax; Secure`;
39
33
  }
40
34
  catch {
41
35
  // storage unavailable
@@ -48,16 +42,6 @@ export function hwioApplyTheme(choice) {
48
42
  */
49
43
  export function hwioThemeRuntime() {
50
44
  let controller = null;
51
- // No stored choice: follow the OS scheme while the page is open.
52
- try {
53
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
54
- if (!localStorage.getItem(THEME_KEY))
55
- hwioApplyTheme('system');
56
- });
57
- }
58
- catch {
59
- // matchMedia unavailable
60
- }
61
45
  let themeBeforeSwap = null;
62
46
  document.addEventListener('astro:before-swap', () => { themeBeforeSwap = document.documentElement.getAttribute('data-theme'); });
63
47
  document.addEventListener('astro:after-swap', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubpav/hwio-web-runtime",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Framework-free browser runtime for the HARDWARIO websites: consent (Consent Mode v2 + GTM), HubSpot forms with Turnstile, attribution, theme, CTA tracking and small behaviours. View-transition safe.",
5
5
  "license": "MIT",
6
6
  "type": "module",