ab-nextjs-components 0.1.8 โ†’ 0.1.10

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/ab-brand/index.tsx +30 -1
  3. package/ab-brand/styles.module.css +40 -1
  4. package/ab-button/helpers.ts +90 -0
  5. package/ab-button/index.tsx +164 -87
  6. package/ab-button/styles.module.css +79 -1
  7. package/ab-carousel/helpers.ts +146 -0
  8. package/ab-carousel/index.tsx +643 -0
  9. package/ab-carousel/styles.module.css +206 -0
  10. package/ab-chip/index.tsx +75 -0
  11. package/ab-chip/styles.module.css +87 -0
  12. package/ab-collapsible/index.tsx +30 -1
  13. package/ab-collapsible/styles.module.css +40 -1
  14. package/ab-flow-diagram/helpers.ts +77 -0
  15. package/ab-flow-diagram/index.tsx +141 -0
  16. package/ab-flow-diagram/styles.module.css +141 -0
  17. package/ab-icon/builtin.ts +61 -0
  18. package/ab-icon/helpers.ts +102 -0
  19. package/ab-icon/index.tsx +77 -16
  20. package/ab-icon/styles.module.css +53 -9
  21. package/ab-icon-button/index.tsx +110 -46
  22. package/ab-icon-button/styles.module.css +28 -10
  23. package/ab-install-snippet/helpers.ts +62 -0
  24. package/ab-install-snippet/index.tsx +170 -0
  25. package/ab-install-snippet/styles.module.css +104 -0
  26. package/ab-like-button/index.tsx +33 -1
  27. package/ab-like-button/styles.module.css +41 -6
  28. package/ab-linear-progress/index.tsx +129 -0
  29. package/ab-linear-progress/styles.module.css +105 -0
  30. package/ab-locale-switcher/index.tsx +213 -0
  31. package/ab-locale-switcher/styles.module.css +127 -0
  32. package/ab-logo/index.tsx +88 -30
  33. package/ab-logo/styles.module.css +79 -11
  34. package/ab-pill/index.tsx +74 -0
  35. package/ab-progress-chips/index.tsx +30 -1
  36. package/ab-progress-chips/styles.module.css +40 -1
  37. package/ab-searchbar/index.tsx +33 -2
  38. package/ab-searchbar/styles.module.css +41 -5
  39. package/ab-splash/csp.d.ts +54 -0
  40. package/ab-splash/csp.mjs +82 -0
  41. package/ab-splash/csp.ts +60 -0
  42. package/ab-splash/gate.ts +216 -0
  43. package/ab-splash/index.tsx +140 -0
  44. package/ab-splash/styles.module.css +103 -0
  45. package/ab-theme-toggle/index.tsx +188 -0
  46. package/ab-theme-toggle/styles.module.css +89 -0
  47. package/css/dialog.css +38 -3
  48. package/index.ts +47 -0
  49. package/package.json +46 -18
  50. package/server/ab-brand/index.tsx +29 -1
  51. package/server/ab-brand/styles.module.css +40 -1
  52. package/server/ab-button/index.tsx +96 -79
  53. package/server/ab-button/styles.module.css +79 -1
  54. package/server/ab-icon/index.tsx +68 -16
  55. package/server/ab-icon/styles.module.css +53 -9
  56. package/server/ab-logo/index.tsx +88 -29
  57. package/server/ab-logo/styles.module.css +79 -11
  58. package/types/css.d.ts +43 -0
  59. package/types/index.d.ts +2 -0
@@ -1,3 +1,42 @@
1
+ /*
2
+ * @license MIT
3
+ * ~~~~~~~~~~~~
4
+ * ab-nextjs-components
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the 'Software'), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
26
+ * @project: ab-nextjs-components
27
+ * @name: ab-searchbar - styles
28
+ * @file: ab-searchbar/styles.module.css
29
+ * @type: CSS Module
30
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
31
+ */
32
+
33
+ /*
34
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
35
+ * MOTTO: We'll always do more ๐Ÿ˜œ!!!
36
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
37
+ */
38
+
39
+
1
40
  .abSearchbar {
2
41
  position: relative;
3
42
  display: flex;
@@ -10,7 +49,7 @@
10
49
  border-radius: 9999px;
11
50
  overflow: hidden;
12
51
  background: color-mix(in srgb, var(--ab-surface-container-high, #e6e0e9) 90%, transparent);
13
- transition: background 0.3s ease;
52
+ --ab-motion-duration: 300ms; /* ๐ŸŽจ focus fade = .ab-motion-colors (ab-nextjs-animations) */
14
53
  }
15
54
  .abSearchbar:focus-within {
16
55
  background: var(--ab-surface-container-high, #e6e0e9);
@@ -65,10 +104,7 @@
65
104
  border: 2px solid color-mix(in srgb, currentColor 25%, transparent);
66
105
  border-top-color: currentColor;
67
106
  border-radius: 50%;
68
- animation: abSearchbarSpin 0.7s linear infinite;
69
- }
70
- @keyframes abSearchbarSpin {
71
- to { transform: rotate(360deg); }
107
+ /* ๐ŸŒ€ rotation = .ab-spin (ab-nextjs-animations) */
72
108
  }
73
109
  .isDisabled {
74
110
  opacity: 0.55;
@@ -0,0 +1,54 @@
1
+ /*
2
+ * @license MIT
3
+ * ~~~~~~~~~~~~
4
+ * ab-nextjs-components
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the 'Software'), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+
26
+ *
27
+ * @project: ab-nextjs-components
28
+ * @name: AbSplash - CSP hash helpers (types for csp.mjs)
29
+ * @file: ab-splash/csp.d.ts
30
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
31
+ */
32
+
33
+ /*
34
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
35
+ * MOTTO: We'll always do more ๐Ÿ˜œ!!!
36
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
37
+ */
38
+
39
+ // โš™๏ธ gate options (mirror of `AbSplashGateOptions` in ./gate.ts)
40
+ export interface AbSplashGateOptions {
41
+ // ๐ŸŒ every configured locale
42
+ locales?: readonly string[]
43
+ // ๐Ÿ  the default (unprefixed) locale
44
+ defaultLocale?: string
45
+ // ๐Ÿšช extra root sub-paths (default ['welcome'])
46
+ rootSubPaths?: readonly string[]
47
+ }
48
+
49
+ // ๐Ÿ” `'sha256-<base64>'` CSP source for an inline script
50
+ export declare function getAbSplashGateSha256(script: string): string
51
+ // ๐Ÿ” the gate script + its hash, from the same options
52
+ export declare function buildAbSplashGate(options?: AbSplashGateOptions): { script: string; sha256: string }
53
+ // ๐Ÿ” hash of the default `abSplashGateScript`
54
+ export declare const AB_SPLASH_GATE_SHA256: string
@@ -0,0 +1,82 @@
1
+ /*
2
+ * @license MIT
3
+ * ~~~~~~~~~~~~
4
+ * ab-nextjs-components
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ * (MIT license text: see LICENSE)
8
+ *
9
+ * @project: ab-nextjs-components
10
+ * @name: AbSplash - CSP hash helpers (COMPILED, do not edit โ€” run `node scripts/build-csp.mjs`)
11
+ * @file: ab-splash/csp.mjs
12
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
13
+ */
14
+ // ๐Ÿค– generated from ab-splash/csp.ts + ab-splash/gate.ts (bundled, ESM, Node)
15
+
16
+ // ab-splash/csp.ts
17
+ import { createHash } from "node:crypto";
18
+
19
+ // ab-splash/gate.ts
20
+ var AB_SPLASH_ATTRIBUTE = "data-ab-splash";
21
+ var AB_SPLASH_VISITED_KEY = "ab-splash-visited";
22
+ var AB_SPLASH_SESSION_KEY = "ab-splash-session";
23
+ var AB_SPLASH_BOT_PATTERN = "bot|crawl|spider|slurp|bingpreview|facebookexternalhit|embedly|quora link preview|whatsapp|telegrambot|discordbot|headlesschrome|lighthouse|pagespeed|prerender";
24
+ var escapeRe = (value) => value.replace(/[.*+?^${}()|[\]\\/-]/g, "\\$&");
25
+ function buildAbSplashRootPathPattern(options = {}) {
26
+ const prefixed = (options.locales ?? []).filter((locale) => locale && locale !== options.defaultLocale).map(escapeRe);
27
+ const subPaths = (options.rootSubPaths ?? ["welcome"]).filter(Boolean).map(escapeRe);
28
+ const localePart = prefixed.length ? `(?:\\/(?:${prefixed.join("|")}))?` : "";
29
+ const subPart = subPaths.length ? `(?:\\/(?:${subPaths.join("|")}))?` : "";
30
+ return `^${localePart}${subPart}\\/?$`;
31
+ }
32
+ function buildAbSplashGateScript(options = {}) {
33
+ const pattern = JSON.stringify(buildAbSplashRootPathPattern(options));
34
+ const bots = JSON.stringify(AB_SPLASH_BOT_PATTERN);
35
+ return `(function () {
36
+ // \u{1F9ED} shortcuts
37
+ var w = window, d = document, nav = w.navigator || {};
38
+ // \u{1F916} skip automation and bots (no Splash for crawlers, previews or headless tests)
39
+ if (nav.webdriver || new RegExp(${bots}, 'i').test(nav.userAgent || '')) return;
40
+ // \u{1F4D6} safe storage read: a failing get counts as "already seen" ('1')
41
+ function get(store, key) { try { return w[store].getItem(key); } catch (e) { return '1'; } }
42
+ // \u270D\uFE0F safe storage write: a failing set is ignored
43
+ function set(store, key) { try { w[store].setItem(key, '1'); } catch (e) {} }
44
+ // \u{1F4F1} installed PWA? (matchMedia OR iOS navigator.standalone)
45
+ var mm = false;
46
+ try { mm = !!(w.matchMedia && w.matchMedia('(display-mode: standalone)').matches); } catch (e) {}
47
+ var isStandalone = mm || nav.standalone === true;
48
+ // \u{1F3E0} a root page? (/, /<locale>, /welcome, /<locale>/welcome \u2014 never the default locale prefix)
49
+ var isRootPage = false;
50
+ try { isRootPage = new RegExp(${pattern}).test(w.location.pathname); } catch (e) {}
51
+ // \u{1F6A6} decision
52
+ var show = false;
53
+ // \u{1F4F1} standalone: once per session
54
+ if (isStandalone) {
55
+ if (!get('sessionStorage', ${JSON.stringify(AB_SPLASH_SESSION_KEY)})) show = true;
56
+ set('sessionStorage', ${JSON.stringify(AB_SPLASH_SESSION_KEY)});
57
+ }
58
+ // \u{1F463} first ever visit, on a root page only
59
+ if (!get('localStorage', ${JSON.stringify(AB_SPLASH_VISITED_KEY)})) {
60
+ if (isRootPage) show = true;
61
+ set('localStorage', ${JSON.stringify(AB_SPLASH_VISITED_KEY)});
62
+ }
63
+ // \u{1F6A9} last step: turn the Splash on
64
+ if (show) { try { d.documentElement.setAttribute(${JSON.stringify(AB_SPLASH_ATTRIBUTE)}, 'on'); } catch (e) {} }
65
+ })();`;
66
+ }
67
+ var abSplashGateScript = buildAbSplashGateScript();
68
+
69
+ // ab-splash/csp.ts
70
+ function getAbSplashGateSha256(script) {
71
+ return `'sha256-${createHash("sha256").update(script, "utf8").digest("base64")}'`;
72
+ }
73
+ function buildAbSplashGate(options = {}) {
74
+ const script = buildAbSplashGateScript(options);
75
+ return { script, sha256: getAbSplashGateSha256(script) };
76
+ }
77
+ var AB_SPLASH_GATE_SHA256 = getAbSplashGateSha256(abSplashGateScript);
78
+ export {
79
+ AB_SPLASH_GATE_SHA256,
80
+ buildAbSplashGate,
81
+ getAbSplashGateSha256
82
+ };
@@ -0,0 +1,60 @@
1
+ /*
2
+ * @license MIT
3
+ * ~~~~~~~~~~~~
4
+ * ab-nextjs-components
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the 'Software'), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+
26
+ *
27
+ * @project: ab-nextjs-components
28
+ * @name: AbSplash - CSP hash helpers
29
+ * @file: ab-splash/csp.ts
30
+ * @type: TypeScript (Node only: use it in next.config / proxy, never in the browser)
31
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
32
+ */
33
+
34
+ /*
35
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
36
+ * MOTTO: We'll always do more ๐Ÿ˜œ!!!
37
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
38
+ */
39
+
40
+ // ๐Ÿ” Node's crypto (sync sha256 for build-time CSP)
41
+ import { createHash } from 'node:crypto';
42
+ // ๐Ÿšฆ the gate script builders
43
+ import { abSplashGateScript, buildAbSplashGateScript, type AbSplashGateOptions } from './gate';
44
+
45
+ // ๐Ÿ” CSP source for an inline script: `'sha256-<base64>'`
46
+ export function getAbSplashGateSha256(script: string): string {
47
+ // ๐Ÿงฎ hash the exact bytes that go into the <script> tag
48
+ return `'sha256-${createHash('sha256').update(script, 'utf8').digest('base64')}'`;
49
+ }
50
+
51
+ // ๐Ÿ” build the script AND its CSP hash in one go (keep both from the same options!)
52
+ export function buildAbSplashGate(options: AbSplashGateOptions = {}): { script: string; sha256: string } {
53
+ // ๐Ÿšฆ the script
54
+ const script = buildAbSplashGateScript(options);
55
+ // ๐Ÿ“ฆ script + hash
56
+ return { script, sha256: getAbSplashGateSha256(script) };
57
+ }
58
+
59
+ // ๐Ÿ” hash of the default `abSplashGateScript` (static CSP)
60
+ export const AB_SPLASH_GATE_SHA256: string = getAbSplashGateSha256(abSplashGateScript);
@@ -0,0 +1,216 @@
1
+ /*
2
+ * @license MIT
3
+ * ~~~~~~~~~~~~
4
+ * ab-nextjs-components
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the 'Software'), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+
26
+ *
27
+ * @project: ab-nextjs-components
28
+ * @name: AbSplash - Gate script & timing helpers
29
+ * @file: ab-splash/gate.ts
30
+ * @type: TypeScript (isomorphic, no Node or DOM imports)
31
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
32
+ */
33
+
34
+ /*
35
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
36
+ * MOTTO: We'll always do more ๐Ÿ˜œ!!!
37
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
38
+ */
39
+
40
+ // ๐Ÿšฉ the `<html>` data attribute that turns the Splash on (set by the gate script, before the first paint โšก๏ธ)
41
+ export const AB_SPLASH_ATTRIBUTE = 'data-ab-splash';
42
+ // ๐Ÿ‘ฃ the localStorage key that remembers the first ever visit
43
+ export const AB_SPLASH_VISITED_KEY = 'ab-splash-visited';
44
+ // ๐Ÿ“ฑ the sessionStorage key that remembers the Splash was shown in this (standalone) session
45
+ export const AB_SPLASH_SESSION_KEY = 'ab-splash-session';
46
+ // โฑ๏ธ the Splash never stays longer than this (in ms)
47
+ export const AB_SPLASH_MAX_MS = 1200;
48
+ // ๐ŸŒซ๏ธ the fade-out duration (in ms), when motion is allowed
49
+ export const AB_SPLASH_FADE_MS = 200;
50
+ // ๐Ÿค– user agents that never get a Splash (crawlers, previews, headless tools)
51
+ export const AB_SPLASH_BOT_PATTERN = 'bot|crawl|spider|slurp|bingpreview|facebookexternalhit|embedly|quora link preview|whatsapp|telegrambot|discordbot|headlesschrome|lighthouse|pagespeed|prerender';
52
+
53
+ // โš™๏ธ gate options
54
+ export interface AbSplashGateOptions {
55
+ // ๐ŸŒ every configured locale (e.g. ['en', 'fr', 'es', 'ru'])
56
+ locales?: readonly string[];
57
+ // ๐Ÿ  the default locale (served without a prefix, so `/en` is NOT a root page)
58
+ defaultLocale?: string;
59
+ // ๐Ÿšช extra "root" sub-paths that also show the Splash on first visit (default: ['welcome'])
60
+ rootSubPaths?: readonly string[];
61
+ }
62
+
63
+ // ๐Ÿ›ก๏ธ escape a string for use inside a RegExp
64
+ const escapeRe = (value: string): string => value.replace(/[.*+?^${}()|[\]\\/-]/g, '\\$&');
65
+
66
+ /**
67
+ * ๐Ÿงญ Build the root-path RegExp source from the configured locales minus the default.
68
+ * e.g. locales en/fr/es/ru + default en โ†’ `^(?:\/(?:fr|es|ru))?(?:\/(?:welcome))?\/?$`
69
+ */
70
+ export function buildAbSplashRootPathPattern(options: AbSplashGateOptions = {}): string {
71
+ // ๐ŸŒ prefixed locales only (the default one has no prefix)
72
+ const prefixed = (options.locales ?? []).filter((locale) => locale && locale !== options.defaultLocale).map(escapeRe);
73
+ // ๐Ÿšช sub-paths (welcome by default)
74
+ const subPaths = (options.rootSubPaths ?? ['welcome']).filter(Boolean).map(escapeRe);
75
+ // ๐Ÿงฑ optional locale prefix
76
+ const localePart = prefixed.length ? `(?:\\/(?:${prefixed.join('|')}))?` : '';
77
+ // ๐Ÿงฑ optional sub-path
78
+ const subPart = subPaths.length ? `(?:\\/(?:${subPaths.join('|')}))?` : '';
79
+ // ๐Ÿ anchored pattern (optional trailing slash)
80
+ return `^${localePart}${subPart}\\/?$`;
81
+ }
82
+
83
+ /*
84
+ * ๐Ÿ’ก Why the gate does its own storage + the CSS does its own fade/failsafe:
85
+ * this script runs inline in <head>, BEFORE React hydrates. Hooks (useAbLocalStorage)
86
+ * and ab-nextjs-animations can't run there yet, and waiting for them would flash the page.
87
+ * So it stays tiny, dependency-free ES5 with its own try-wrapped storage access.
88
+ */
89
+
90
+ /**
91
+ * ๐Ÿšฆ Build the inline gate script (runs in <head>, before the first paint).
92
+ * Inject with `<script dangerouslySetInnerHTML={{ __html: script }} />` and allow its hash in the CSP
93
+ * (see `getAbSplashGateSha256` in `ab-nextjs-components/ab-splash/csp`).
94
+ */
95
+ export function buildAbSplashGateScript(options: AbSplashGateOptions = {}): string {
96
+ // ๐Ÿงญ root-path pattern, injected as a JSON string literal (never raw-concatenated)
97
+ const pattern = JSON.stringify(buildAbSplashRootPathPattern(options));
98
+ // ๐Ÿค– bot pattern, also JSON-injected
99
+ const bots = JSON.stringify(AB_SPLASH_BOT_PATTERN);
100
+ // ๐Ÿงพ the script (plain ES5, comments allowed: newlines are kept)
101
+ return `(function () {
102
+ // ๐Ÿงญ shortcuts
103
+ var w = window, d = document, nav = w.navigator || {};
104
+ // ๐Ÿค– skip automation and bots (no Splash for crawlers, previews or headless tests)
105
+ if (nav.webdriver || new RegExp(${bots}, 'i').test(nav.userAgent || '')) return;
106
+ // ๐Ÿ“– safe storage read: a failing get counts as "already seen" ('1')
107
+ function get(store, key) { try { return w[store].getItem(key); } catch (e) { return '1'; } }
108
+ // โœ๏ธ safe storage write: a failing set is ignored
109
+ function set(store, key) { try { w[store].setItem(key, '1'); } catch (e) {} }
110
+ // ๐Ÿ“ฑ installed PWA? (matchMedia OR iOS navigator.standalone)
111
+ var mm = false;
112
+ try { mm = !!(w.matchMedia && w.matchMedia('(display-mode: standalone)').matches); } catch (e) {}
113
+ var isStandalone = mm || nav.standalone === true;
114
+ // ๐Ÿ  a root page? (/, /<locale>, /welcome, /<locale>/welcome โ€” never the default locale prefix)
115
+ var isRootPage = false;
116
+ try { isRootPage = new RegExp(${pattern}).test(w.location.pathname); } catch (e) {}
117
+ // ๐Ÿšฆ decision
118
+ var show = false;
119
+ // ๐Ÿ“ฑ standalone: once per session
120
+ if (isStandalone) {
121
+ if (!get('sessionStorage', ${JSON.stringify(AB_SPLASH_SESSION_KEY)})) show = true;
122
+ set('sessionStorage', ${JSON.stringify(AB_SPLASH_SESSION_KEY)});
123
+ }
124
+ // ๐Ÿ‘ฃ first ever visit, on a root page only
125
+ if (!get('localStorage', ${JSON.stringify(AB_SPLASH_VISITED_KEY)})) {
126
+ if (isRootPage) show = true;
127
+ set('localStorage', ${JSON.stringify(AB_SPLASH_VISITED_KEY)});
128
+ }
129
+ // ๐Ÿšฉ last step: turn the Splash on
130
+ if (show) { try { d.documentElement.setAttribute(${JSON.stringify(AB_SPLASH_ATTRIBUTE)}, 'on'); } catch (e) {} }
131
+ })();`;
132
+ }
133
+
134
+ // ๐Ÿšฆ the default gate script (no locale prefixes; build your own with `buildAbSplashGateScript`)
135
+ export const abSplashGateScript: string = buildAbSplashGateScript();
136
+
137
+ // โณ time left before the cap (never below 0, never above the max)
138
+ export function getAbSplashRemainingMs(elapsedMs: number, maxMs: number = AB_SPLASH_MAX_MS): number {
139
+ return Math.min(maxMs, Math.max(0, maxMs - elapsedMs));
140
+ }
141
+
142
+ // ๐ŸŒซ๏ธ fade duration (0 with reduced motion โ™ฟ)
143
+ export function getAbSplashFadeMs(prefersReducedMotion: boolean): number {
144
+ return prefersReducedMotion ? 0 : AB_SPLASH_FADE_MS;
145
+ }
146
+
147
+ // ๐Ÿ resolve when fonts are ready or the timeout hits, whichever comes first
148
+ export function waitForAbSplashReady(fontsReady: Promise<unknown> | undefined, timeoutMs: number): Promise<'fonts' | 'timeout'> {
149
+ // โฑ๏ธ the timeout racer
150
+ const timeout = new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), timeoutMs));
151
+ // ๐Ÿคท no Font Loading API โ†’ timeout only
152
+ if (!fontsReady) return timeout;
153
+ // ๐Ÿ”ค a failing font load counts as ready too
154
+ const fonts = fontsReady.then(() => 'fonts' as const, () => 'fonts' as const);
155
+ // ๐Ÿ first one wins
156
+ return Promise.race([fonts, timeout]);
157
+ }
158
+
159
+ // ๐Ÿ™ˆ minimal element shape used by the inert helper (keeps it testable without a DOM)
160
+ export interface AbInertNode {
161
+ parentElement: AbInertNode | null;
162
+ children: ArrayLike<AbInertNode>;
163
+ inert?: boolean;
164
+ tagName?: string;
165
+ }
166
+
167
+ /**
168
+ * ๐Ÿ™ˆ Make everything behind `overlay` inert (siblings on the path up to <body>).
169
+ * Returns a restore function that only un-inerts what it changed.
170
+ */
171
+ export function setAbSplashBackgroundInert(overlay: AbInertNode): () => void {
172
+ // ๐Ÿ“ nodes we changed
173
+ const changed: AbInertNode[] = [];
174
+ // โฌ†๏ธ walk up from the overlay to <body>
175
+ let node: AbInertNode | null = overlay;
176
+ while (node && node.parentElement) {
177
+ // ๐Ÿ‘ช the parent
178
+ const parent: AbInertNode = node.parentElement;
179
+ // ๐Ÿ” every sibling of the current node
180
+ for (const sibling of Array.from(parent.children)) {
181
+ // โœ… skip the overlay's own branch, scripts/styles and already-inert nodes
182
+ if (sibling === node || sibling.inert || /^(SCRIPT|STYLE|LINK|TEMPLATE)$/.test(sibling.tagName ?? '')) continue;
183
+ // ๐Ÿ™ˆ make it inert
184
+ sibling.inert = true;
185
+ // ๐Ÿ“ remember it
186
+ changed.push(sibling);
187
+ }
188
+ // ๐Ÿ›‘ stop at <body>
189
+ if ((parent.tagName ?? '').toUpperCase() === 'BODY') break;
190
+ // โฌ†๏ธ go up
191
+ node = parent;
192
+ }
193
+ // โ†ฉ๏ธ restore
194
+ return () => { for (const el of changed) el.inert = false; };
195
+ }
196
+
197
+ // โฒ๏ธ pending teardown (module-level: survives React StrictMode's dev unmount โ†’ remount)
198
+ let abSplashPendingFinish: ReturnType<typeof setTimeout> | undefined
199
+
200
+ /**
201
+ * ๐Ÿ” Schedule the final teardown (flag off + inert restored) on the next task instead of running it
202
+ * synchronously in an effect cleanup. StrictMode (dev) unmounts and immediately remounts: the remount
203
+ * cancels this with `cancelAbSplashFinish()`, so the Splash isn't killed by its own cleanup.
204
+ */
205
+ export function scheduleAbSplashFinish(finish: () => void, delayMs: number = 0): void {
206
+ cancelAbSplashFinish() // ๐Ÿงน only one pending teardown
207
+ abSplashPendingFinish = setTimeout(() => { abSplashPendingFinish = undefined; finish() }, delayMs) // โฒ๏ธ
208
+ }
209
+
210
+ /** ๐Ÿ›‘ Cancel a pending teardown; returns true when one was pending (i.e. this is a remount). */
211
+ export function cancelAbSplashFinish(): boolean {
212
+ if (abSplashPendingFinish === undefined) return false // ๐Ÿ˜ด nothing pending
213
+ clearTimeout(abSplashPendingFinish) // ๐Ÿ›‘ cancel
214
+ abSplashPendingFinish = undefined // ๐Ÿงน reset
215
+ return true // ๐Ÿ” remount
216
+ }
@@ -0,0 +1,140 @@
1
+ /*
2
+ * @license MIT
3
+ * ~~~~~~~~~~~~
4
+ * ab-nextjs-components
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the 'Software'), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+
26
+ *
27
+ * @project: ab-nextjs-components
28
+ * @name: AbSplash - Client
29
+ * @file: ab-splash/index.tsx
30
+ * @type: Client Component
31
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
32
+ */
33
+
34
+ /*
35
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
36
+ * MOTTO: We'll always do more ๐Ÿ˜œ!!!
37
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
38
+ */
39
+
40
+ 'use client'; // ๐Ÿ’ง hides itself after hydration
41
+
42
+ // โš›๏ธ React hooks
43
+ import { useEffect, useRef, useState, type ReactElement } from 'react';
44
+ // ๐Ÿงต class-name joiner
45
+ import clsx from 'clsx';
46
+ // ๐Ÿ…ฐ๏ธ the Ab mark + "Elements" wordmark (this package)
47
+ import AbLogo from '../ab-logo';
48
+ // ๐Ÿ“ถ the linear progress (this package)
49
+ import AbLinearProgress from '../ab-linear-progress';
50
+ // ๐Ÿšฆ gate constants & helpers
51
+ import {
52
+ AB_SPLASH_ATTRIBUTE,
53
+ getAbSplashFadeMs,
54
+ getAbSplashRemainingMs,
55
+ setAbSplashBackgroundInert,
56
+ scheduleAbSplashFinish,
57
+ cancelAbSplashFinish,
58
+ waitForAbSplashReady,
59
+ type AbInertNode,
60
+ } from './gate';
61
+ // ๐ŸŽจ package-shipped styles
62
+ import styles from './styles.module.css';
63
+ import { AB_MOTION } from 'ab-nextjs-animations/motion'; // ๐ŸŽฌ fade-out transition (ab-nextjs-animations >= 0.2.4)
64
+ import 'ab-nextjs-animations/motion/styles.css'; // ๐ŸŒซ๏ธ .ab-motion-colors (covers opacity)
65
+
66
+ // ๐Ÿ“ฆ re-export the gate so `import { abSplashGateScript } from 'ab-nextjs-components/ab-splash'` works
67
+ export * from './gate';
68
+
69
+ // ๐Ÿ“ props
70
+ export interface AbSplashProps {
71
+ // ๐Ÿท๏ธ consumer classes
72
+ className?: string;
73
+ // ๐Ÿ”ค wordmark text (default "Elements")
74
+ wordmark?: string;
75
+ // โฑ๏ธ hard cap in ms (default 1200)
76
+ maxMs?: number;
77
+ }
78
+
79
+ /** ๐Ÿ’ฆ AbSplash โ€” brand overlay shown only when the gate script set `html[data-ab-splash]`. */
80
+ export default function AbSplash({ className, wordmark = 'Elements', maxMs }: AbSplashProps): ReactElement {
81
+ // ๐Ÿ“ถ progress (25% until hydration)
82
+ const [progress, setProgress] = useState<number>(25);
83
+ // ๐Ÿ“Œ the overlay element
84
+ const ref = useRef<HTMLDivElement>(null);
85
+
86
+ useEffect(() => {
87
+ // ๐Ÿ” StrictMode remount? cancel the teardown the previous cleanup scheduled (keeps the flag on)
88
+ cancelAbSplashFinish();
89
+ // ๐ŸŒณ the <html> element
90
+ const root = document.documentElement;
91
+ // ๐Ÿ˜ด gate said no โ†’ nothing to do
92
+ if (root.getAttribute(AB_SPLASH_ATTRIBUTE) !== 'on') return;
93
+ // ๐Ÿ›Ÿ unmount guard
94
+ let alive = true;
95
+ // โฒ๏ธ fade timer
96
+ let fadeTimer: ReturnType<typeof setTimeout> | undefined;
97
+ // ๐Ÿ™ˆ make the page behind inert while we show
98
+ const restoreInert = ref.current ? setAbSplashBackgroundInert(ref.current as unknown as AbInertNode) : () => {};
99
+ // ๐Ÿ‘‹ final cleanup: flag off + page interactive again
100
+ const finish = () => { root.removeAttribute(AB_SPLASH_ATTRIBUTE); restoreInert(); };
101
+ // โžก๏ธ hydrated: move the progress on (next frame)
102
+ const frame = requestAnimationFrame(() => setProgress(60));
103
+ // โ™ฟ reduced motion?
104
+ const reduced = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
105
+ // โณ time left before the cap
106
+ const remaining = getAbSplashRemainingMs(performance.now(), maxMs);
107
+ // ๐Ÿ”ค wait for fonts (or the cap), then hide
108
+ waitForAbSplashReady(document.fonts?.ready, remaining).then(() => {
109
+ // ๐Ÿ›Ÿ unmounted meanwhile
110
+ if (!alive) return;
111
+ // โœ… fill the bar
112
+ setProgress(100);
113
+ // ๐ŸŒซ๏ธ start the CSS fade
114
+ root.setAttribute(AB_SPLASH_ATTRIBUTE, 'leaving');
115
+ // ๐Ÿ‘‹ remove for good (at once with reduced motion)
116
+ fadeTimer = setTimeout(finish, getAbSplashFadeMs(reduced));
117
+ });
118
+ // ๐Ÿงน cleanup
119
+ return () => {
120
+ alive = false; // ๐Ÿ›Ÿ dead
121
+ cancelAnimationFrame(frame); // ๐Ÿšซ frame
122
+ if (fadeTimer) clearTimeout(fadeTimer); // ๐Ÿšซ timer
123
+ restoreInert(); // ๐Ÿ™‰ page interactive again right away (a remount re-applies inert)
124
+ scheduleAbSplashFinish(finish); // ๐Ÿ‘‹ remove data-ab-splash on the next task unless we remount (StrictMode)
125
+ };
126
+ }, [maxMs]);
127
+
128
+ return (
129
+ // ๐Ÿ’ฆ the overlay (decorative โ†’ hidden from assistive tech)
130
+ <div ref={ref} className={clsx('AbSplash', styles.abSplash, AB_MOTION.colors, className)} aria-hidden="true">
131
+ {/* ๐Ÿ…ฐ๏ธ mark + wordmark */}
132
+ <AbLogo wordmark={wordmark} alt="" className={styles.abSplash__brand} />
133
+ {/* ๐Ÿ“ถ progress */}
134
+ <AbLinearProgress className={styles.abSplash__progress} progress={progress} label="" />
135
+ </div>
136
+ );
137
+ }
138
+
139
+ // ๐Ÿ“ฆ named export too
140
+ export { AbSplash };