ab-nextjs-components 0.1.7 → 0.1.9
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/CHANGELOG.md +18 -0
- package/README.md +13 -0
- package/ab-linear-progress/index.tsx +125 -0
- package/ab-linear-progress/styles.module.css +121 -0
- package/ab-logo/index.tsx +88 -30
- package/ab-logo/styles.module.css +79 -11
- package/ab-splash/csp.d.ts +54 -0
- package/ab-splash/csp.mjs +82 -0
- package/ab-splash/csp.ts +60 -0
- package/ab-splash/gate.ts +216 -0
- package/ab-splash/index.tsx +138 -0
- package/ab-splash/styles.module.css +106 -0
- package/assets/logos/ab-logo.svg +1 -0
- package/assets/spinners/12-dots-scale-rotate.svg +1 -0
- package/assets/spinners/3-dots-scale.svg +1 -0
- package/assets/spinners/LICENSE +20 -0
- package/css/chrome-mq.css +121 -0
- package/css/dialog.css +248 -0
- package/css/doodle.css +41 -0
- package/css/logo.css +52 -0
- package/css/spinner.css +28 -0
- package/css/toast.css +70 -0
- package/index.ts +6 -0
- package/package.json +25 -6
- package/server/ab-logo/index.tsx +88 -29
- package/server/ab-logo/styles.module.css +79 -11
- package/styles.css +14 -0
- package/types/css.d.ts +43 -0
- package/types/index.d.ts +2 -0
|
@@ -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
|
+
};
|
package/ab-splash/csp.ts
ADDED
|
@@ -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,138 @@
|
|
|
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
|
+
|
|
64
|
+
// 📦 re-export the gate so `import { abSplashGateScript } from 'ab-nextjs-components/ab-splash'` works
|
|
65
|
+
export * from './gate';
|
|
66
|
+
|
|
67
|
+
// 📐 props
|
|
68
|
+
export interface AbSplashProps {
|
|
69
|
+
// 🏷️ consumer classes
|
|
70
|
+
className?: string;
|
|
71
|
+
// 🔤 wordmark text (default "Elements")
|
|
72
|
+
wordmark?: string;
|
|
73
|
+
// ⏱️ hard cap in ms (default 1200)
|
|
74
|
+
maxMs?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 💦 AbSplash — brand overlay shown only when the gate script set `html[data-ab-splash]`. */
|
|
78
|
+
export default function AbSplash({ className, wordmark = 'Elements', maxMs }: AbSplashProps): ReactElement {
|
|
79
|
+
// 📶 progress (25% until hydration)
|
|
80
|
+
const [progress, setProgress] = useState<number>(25);
|
|
81
|
+
// 📌 the overlay element
|
|
82
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
// 🔁 StrictMode remount? cancel the teardown the previous cleanup scheduled (keeps the flag on)
|
|
86
|
+
cancelAbSplashFinish();
|
|
87
|
+
// 🌳 the <html> element
|
|
88
|
+
const root = document.documentElement;
|
|
89
|
+
// 😴 gate said no → nothing to do
|
|
90
|
+
if (root.getAttribute(AB_SPLASH_ATTRIBUTE) !== 'on') return;
|
|
91
|
+
// 🛟 unmount guard
|
|
92
|
+
let alive = true;
|
|
93
|
+
// ⏲️ fade timer
|
|
94
|
+
let fadeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
95
|
+
// 🙈 make the page behind inert while we show
|
|
96
|
+
const restoreInert = ref.current ? setAbSplashBackgroundInert(ref.current as unknown as AbInertNode) : () => {};
|
|
97
|
+
// 👋 final cleanup: flag off + page interactive again
|
|
98
|
+
const finish = () => { root.removeAttribute(AB_SPLASH_ATTRIBUTE); restoreInert(); };
|
|
99
|
+
// ➡️ hydrated: move the progress on (next frame)
|
|
100
|
+
const frame = requestAnimationFrame(() => setProgress(60));
|
|
101
|
+
// ♿ reduced motion?
|
|
102
|
+
const reduced = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
|
103
|
+
// ⏳ time left before the cap
|
|
104
|
+
const remaining = getAbSplashRemainingMs(performance.now(), maxMs);
|
|
105
|
+
// 🔤 wait for fonts (or the cap), then hide
|
|
106
|
+
waitForAbSplashReady(document.fonts?.ready, remaining).then(() => {
|
|
107
|
+
// 🛟 unmounted meanwhile
|
|
108
|
+
if (!alive) return;
|
|
109
|
+
// ✅ fill the bar
|
|
110
|
+
setProgress(100);
|
|
111
|
+
// 🌫️ start the CSS fade
|
|
112
|
+
root.setAttribute(AB_SPLASH_ATTRIBUTE, 'leaving');
|
|
113
|
+
// 👋 remove for good (at once with reduced motion)
|
|
114
|
+
fadeTimer = setTimeout(finish, getAbSplashFadeMs(reduced));
|
|
115
|
+
});
|
|
116
|
+
// 🧹 cleanup
|
|
117
|
+
return () => {
|
|
118
|
+
alive = false; // 🛟 dead
|
|
119
|
+
cancelAnimationFrame(frame); // 🚫 frame
|
|
120
|
+
if (fadeTimer) clearTimeout(fadeTimer); // 🚫 timer
|
|
121
|
+
restoreInert(); // 🙉 page interactive again right away (a remount re-applies inert)
|
|
122
|
+
scheduleAbSplashFinish(finish); // 👋 remove data-ab-splash on the next task unless we remount (StrictMode)
|
|
123
|
+
};
|
|
124
|
+
}, [maxMs]);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
// 💦 the overlay (decorative → hidden from assistive tech)
|
|
128
|
+
<div ref={ref} className={clsx('AbSplash', styles.abSplash, className)} aria-hidden="true">
|
|
129
|
+
{/* 🅰️ mark + wordmark */}
|
|
130
|
+
<AbLogo wordmark={wordmark} alt="" className={styles.abSplash__brand} />
|
|
131
|
+
{/* 📶 progress */}
|
|
132
|
+
<AbLinearProgress className={styles.abSplash__progress} progress={progress} label="" />
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 📦 named export too
|
|
138
|
+
export { AbSplash };
|
|
@@ -0,0 +1,106 @@
|
|
|
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 - Component Stylesheet (plain CSS, Tailwind-independent)
|
|
29
|
+
* @file: ab-splash/styles.module.css
|
|
30
|
+
* @type: CSS Module
|
|
31
|
+
* @authors: Abraham Ukachi <abrahamukachi@gmail.com>
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
36
|
+
* MOTTO: We'll always do more 😜!!!
|
|
37
|
+
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/* 💦 overlay: hidden by default (JS off = no Splash 🙈) */
|
|
41
|
+
.abSplash {
|
|
42
|
+
display: none; /* 🙈 off unless the gate turned it on */
|
|
43
|
+
position: fixed; /* 📌 cover the viewport */
|
|
44
|
+
inset: 0; /* 📐 edge to edge */
|
|
45
|
+
z-index: 1000; /* 🔝 above everything */
|
|
46
|
+
flex-direction: column; /* ⬇️ brand above progress */
|
|
47
|
+
align-items: center; /* ↔️ centered */
|
|
48
|
+
justify-content: center; /* ↕️ centered */
|
|
49
|
+
gap: 2.25rem; /* ↕️ brand ↔ bar */
|
|
50
|
+
background-color: #FFF8F3; /* ☀️ light surface (matches the splash mocks) */
|
|
51
|
+
color: #A67C52; /* 🎨 bronze brand */
|
|
52
|
+
transition: opacity 200ms ease; /* 🌫️ fade-out */
|
|
53
|
+
animation: abSplashFailsafe 0s linear 1.2s forwards; /* ⏱️ never longer than 1.2 s, even without hydration */
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* 🌙 dark */
|
|
57
|
+
:global(html.dark) .abSplash {
|
|
58
|
+
background-color: #1A1310; /* 🌙 dark surface (matches the splash mocks) */
|
|
59
|
+
color: #FFF8F5; /* 🤍 light brand */
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* 🚦 gate said "on" (or leaving) */
|
|
63
|
+
:global(html[data-ab-splash]) .abSplash {
|
|
64
|
+
display: flex;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* 🔒 no scrolling behind the overlay — with the same 1.2 s failsafe as the overlay itself */
|
|
68
|
+
:global(html[data-ab-splash='on']) {
|
|
69
|
+
overflow: hidden; /* 🔒 lock */
|
|
70
|
+
animation: abSplashUnlock 0s linear 1.2s forwards; /* ⏱️ unlock even if hydration never happens */
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* ⏱️ failsafe unlock keyframes (overflow animates discretely) */
|
|
74
|
+
@keyframes abSplashUnlock {
|
|
75
|
+
to { overflow: visible; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* 👋 leaving: fade out */
|
|
79
|
+
:global(html[data-ab-splash='leaving']) .abSplash {
|
|
80
|
+
opacity: 0;
|
|
81
|
+
pointer-events: none;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* 🅰️ brand lockup size */
|
|
85
|
+
.abSplash__brand {
|
|
86
|
+
font-size: clamp(2.5rem, 6vw, 3.25rem); /* 📏 as in the mock */
|
|
87
|
+
color: inherit; /* 🎨 follow light/dark */
|
|
88
|
+
--ab-logo-color: currentColor; /* 🎨 mark + wordmark follow the overlay color */
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* 📶 progress track */
|
|
92
|
+
.abSplash__progress {
|
|
93
|
+
width: min(18rem, 28vw); /* ↔️ mobile + laptop, as in the mock */
|
|
94
|
+
--ab-linear-progress-height: 0.375rem; /* ↕️ 6 px */
|
|
95
|
+
--ab-linear-progress-color: #A67C52; /* 🎨 bronze bar in both themes */
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* ♿ reduced motion: no fade */
|
|
99
|
+
@media (prefers-reduced-motion: reduce) {
|
|
100
|
+
.abSplash { transition: none; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ⏱️ failsafe keyframes */
|
|
104
|
+
@keyframes abSplashFailsafe {
|
|
105
|
+
to { visibility: hidden; pointer-events: none; }
|
|
106
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 24 24"><path fill="#A67C52" d="m1.8 20.5 9.3-17v17zM22.2 7.7l-9.3-4.2v17l9.3-4.2-4.7-4.3z"/><path d="m12.9 20.5 9.3-4.2-9.3-8.6z" opacity=".1"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>@keyframes spinner_grm3{0%,50%{animation-timing-function:cubic-bezier(.27,.42,.37,.99);r:1px}25%{animation-timing-function:cubic-bezier(.53,0,.61,.73);r:2px}}@keyframes spinner_T3O6{0%{transform:rotate(360deg)}}.spinner_EUy1{animation:spinner_grm3 1.2s infinite}.spinner_f6oS{animation-delay:.1s}.spinner_g3nX{animation-delay:.2s}.spinner_nvEs{animation-delay:.3s}.spinner_MaNM{animation-delay:.4s}.spinner_4nle{animation-delay:.5s}.spinner_ZETM{animation-delay:.6s}.spinner_HXuO{animation-delay:.7s}.spinner_YaQo{animation-delay:.8s}.spinner_GOx1{animation-delay:.9s}.spinner_4vv9{animation-delay:1s}.spinner_NTs9{animation-delay:1.1s}.spinner_auJJ{transform-origin:center;animation:spinner_T3O6 6s linear infinite}</style><g class="spinner_auJJ"><circle cx="12" cy="3" r="1" class="spinner_EUy1"/><circle cx="16.5" cy="4.21" r="1" class="spinner_EUy1 spinner_f6oS"/><circle cx="7.5" cy="4.21" r="1" class="spinner_EUy1 spinner_NTs9"/><circle cx="19.79" cy="7.5" r="1" class="spinner_EUy1 spinner_g3nX"/><circle cx="4.21" cy="7.5" r="1" class="spinner_EUy1 spinner_4vv9"/><circle cx="21" cy="12" r="1" class="spinner_EUy1 spinner_nvEs"/><circle cx="3" cy="12" r="1" class="spinner_EUy1 spinner_GOx1"/><circle cx="19.79" cy="16.5" r="1" class="spinner_EUy1 spinner_MaNM"/><circle cx="4.21" cy="16.5" r="1" class="spinner_EUy1 spinner_YaQo"/><circle cx="16.5" cy="19.79" r="1" class="spinner_EUy1 spinner_4nle"/><circle cx="7.5" cy="19.79" r="1" class="spinner_EUy1 spinner_HXuO"/><circle cx="12" cy="21" r="1" class="spinner_EUy1 spinner_ZETM"/></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>@keyframes spinner_xe7Q{93.75%{r:3px}46.875%{r:.2px}}.spinner_b2T7{animation:spinner_xe7Q .8s linear infinite}.spinner_YRVV{animation-delay:-.65s}.spinner_c9oY{animation-delay:-.5s}</style><circle cx="4" cy="12" r="3" class="spinner_b2T7"/><circle cx="12" cy="12" r="3" class="spinner_b2T7 spinner_YRVV"/><circle cx="20" cy="12" r="3" class="spinner_b2T7 spinner_c9oY"/></svg>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) Utkarsh Verma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|