@guideify/react 0.1.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/LICENSE +122 -0
- package/README.md +79 -0
- package/dist/index.cjs +141 -0
- package/dist/index.d.cts +413 -0
- package/dist/index.d.ts +413 -0
- package/dist/index.js +136 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Guideify SDK License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Guideify. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software is proprietary. It is published to the public npm registry so a
|
|
6
|
+
build tool can install it, not because it is open source. The permission you
|
|
7
|
+
have to use it is written below, and there is no other.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
1. Definitions
|
|
11
|
+
|
|
12
|
+
"Software" means this package and every file it publishes.
|
|
13
|
+
|
|
14
|
+
"Service" means the Guideify hosted product at https://guideify.in.
|
|
15
|
+
|
|
16
|
+
"You" means the person or organisation installing the Software, and any
|
|
17
|
+
organisation You install it on behalf of.
|
|
18
|
+
|
|
19
|
+
"Your Application" means a product You develop and operate that integrates
|
|
20
|
+
the Service.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
2. Grant
|
|
24
|
+
|
|
25
|
+
Subject to Your compliance with this license, Guideify grants You a
|
|
26
|
+
non-exclusive, worldwide, royalty-free, non-transferable, revocable license
|
|
27
|
+
to:
|
|
28
|
+
|
|
29
|
+
(a) install, copy and use the Software in order to integrate the Service
|
|
30
|
+
into Your Application;
|
|
31
|
+
|
|
32
|
+
(b) incorporate the Software into Your Application, including compiling,
|
|
33
|
+
bundling, minifying and otherwise transforming it as an ordinary build
|
|
34
|
+
step does; and
|
|
35
|
+
|
|
36
|
+
(c) distribute the Software as an incorporated part of Your Application to
|
|
37
|
+
Your users, including by serving it from Your own origin or from a
|
|
38
|
+
content delivery network You use.
|
|
39
|
+
|
|
40
|
+
Paragraph (c) is the point of the whole document. A browser SDK reaches its
|
|
41
|
+
audience only by being served from Your pages, and a proprietary license that
|
|
42
|
+
omitted it would forbid the one thing every user of this package must do.
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
3. Conditions
|
|
46
|
+
|
|
47
|
+
(a) You may not distribute the Software on its own, or inside anything whose
|
|
48
|
+
purpose is to make the Software available to others. It travels inside
|
|
49
|
+
Your Application or not at all.
|
|
50
|
+
|
|
51
|
+
(b) You may not remove or alter a copyright, license or attribution notice in
|
|
52
|
+
the Software. A build step that strips comments from bundled output is
|
|
53
|
+
not a breach of this paragraph.
|
|
54
|
+
|
|
55
|
+
(c) You may not sublicense the Software except as an inseparable part of Your
|
|
56
|
+
Application, on terms no more permissive than these.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
4. Restrictions
|
|
60
|
+
|
|
61
|
+
(a) You may not modify the Software, or use it, to connect to or interoperate
|
|
62
|
+
with any service other than the Service.
|
|
63
|
+
|
|
64
|
+
(b) You may not use the Software, or knowledge gained from studying it, to
|
|
65
|
+
build a product that competes with the Service.
|
|
66
|
+
|
|
67
|
+
(c) You may not reverse engineer, decompile or disassemble the Software,
|
|
68
|
+
except where that restriction is prohibited by applicable law, or where
|
|
69
|
+
it is necessary to exercise a right applicable law grants You that cannot
|
|
70
|
+
be waived by contract.
|
|
71
|
+
|
|
72
|
+
Nothing in this section restricts what You may do with the Software's public
|
|
73
|
+
API, with its documented behaviour, or with the data the Service returns to
|
|
74
|
+
You about Your own product.
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
5. Reservation of rights
|
|
78
|
+
|
|
79
|
+
All rights not expressly granted are reserved. The Software is licensed,
|
|
80
|
+
never sold, and this license transfers no ownership.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
6. Term and termination
|
|
84
|
+
|
|
85
|
+
This license takes effect when You install the Software. It ends when You
|
|
86
|
+
stop using the Service, or if You breach a term and do not remedy the breach
|
|
87
|
+
within thirty days of being told about it.
|
|
88
|
+
|
|
89
|
+
On termination You must stop distributing new copies of the Software. Copies
|
|
90
|
+
already embedded in versions of Your Application that You have already
|
|
91
|
+
distributed are unaffected, and You do not have to recall them.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
7. No warranty
|
|
95
|
+
|
|
96
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
97
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
98
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
8. Limitation of liability
|
|
102
|
+
|
|
103
|
+
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL THE
|
|
104
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
105
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
106
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
107
|
+
|
|
108
|
+
Nothing here excludes a liability that applicable law does not permit to be
|
|
109
|
+
excluded.
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
9. Relationship to the Terms of Service
|
|
113
|
+
|
|
114
|
+
The Guideify Terms of Service at https://guideify.in/terms govern the
|
|
115
|
+
Service. This license governs the Software.
|
|
116
|
+
|
|
117
|
+
It stands on its own: it is granted by the copyright holder directly, and it
|
|
118
|
+
does not depend on those terms being in force. Copyright exists without
|
|
119
|
+
incorporation, and so does a license over it.
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
Questions about this license: hello@guideify.in
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @guideify/react
|
|
2
|
+
|
|
3
|
+
Tier 3 of the three ways to install Guideify — BUILD_REPORT §6.3. Tier 1 is a
|
|
4
|
+
script tag and Tier 2 is `identify()`; this is for teams who would rather say
|
|
5
|
+
both in the component tree, plus the one thing neither tier offers:
|
|
6
|
+
`<GuideifyTarget>`.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm i @guideify/react
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { GuideifyProvider, GuideifyTarget, useGuideify } from '@guideify/react';
|
|
14
|
+
|
|
15
|
+
<GuideifyProvider apiKey={process.env.NEXT_PUBLIC_GUIDEIFY_KEY}>
|
|
16
|
+
<App />
|
|
17
|
+
</GuideifyProvider>;
|
|
18
|
+
|
|
19
|
+
// anywhere below it
|
|
20
|
+
const { start, track, identify } = useGuideify();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## `<GuideifyTarget>`
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
<GuideifyTarget id="create-project">
|
|
27
|
+
<button onClick={create}>New project</button>
|
|
28
|
+
</GuideifyTarget>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The paved path from §3.6, as one import. `data-guideify-id` is the resolver's
|
|
32
|
+
first tier: a step aimed at one never scores candidates, never lands on the
|
|
33
|
+
wrong element, and never drifts when the class names around it change.
|
|
34
|
+
|
|
35
|
+
It renders no element of its own — it clones the single child and adds the
|
|
36
|
+
attribute — so it cannot change your layout. Given anything else (several
|
|
37
|
+
children, a bare string) it renders them untouched rather than throwing.
|
|
38
|
+
|
|
39
|
+
## What it does not bundle
|
|
40
|
+
|
|
41
|
+
**The core is not in this package.** The provider injects the loader tag and the
|
|
42
|
+
runtime arrives from the CDN, which is what keeps a canary (R-03) and the kill
|
|
43
|
+
switch (R-04) able to reach you. Every import from `@guideify/js` here is
|
|
44
|
+
`import type`, and `test/boundary.test.mjs` fails the build if that changes.
|
|
45
|
+
|
|
46
|
+
Two consequences worth knowing:
|
|
47
|
+
|
|
48
|
+
- **Already have the script tag?** Keep it. The provider never injects over an
|
|
49
|
+
existing `window.guideify`, so having both is the documented upgrade path
|
|
50
|
+
rather than a mistake — and it is why you do not get two cores.
|
|
51
|
+
- **Calls before the core lands are queued, not lost.** `track()` in a mount
|
|
52
|
+
effect needs no guard. The one exception is `on()`, which owes you an
|
|
53
|
+
unsubscribe immediately; this package holds the subscription and attaches it
|
|
54
|
+
on arrival, and unsubscribing first really does mean it is never attached.
|
|
55
|
+
|
|
56
|
+
## Props
|
|
57
|
+
|
|
58
|
+
`GuideifyProvider` takes exactly what a script tag can carry, because the tag is
|
|
59
|
+
what initialises the core:
|
|
60
|
+
|
|
61
|
+
| Prop | |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `apiKey` | required — your `pk_live_…` |
|
|
64
|
+
| `cdnHost` | self-hosted or CNAME'd bundle origin |
|
|
65
|
+
| `ingestHost` | analytics origin |
|
|
66
|
+
| `publicKey` | turns on config signature verification |
|
|
67
|
+
| `debug` | |
|
|
68
|
+
| `nonce` | CSP `nonce` for the injected tag — §6.4 |
|
|
69
|
+
|
|
70
|
+
`manualOnly`, `apiHost`, `dashboardOrigin` and `config` have no script-tag
|
|
71
|
+
equivalent, so they are deliberately absent from the type rather than accepted
|
|
72
|
+
and ignored. Use `@guideify/js` directly if you need them.
|
|
73
|
+
|
|
74
|
+
Next.js App Router users: see `@guideify/next`, which reads the CSP nonce for you.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
Proprietary — see [LICENSE](./LICENSE). You may bundle this into your own
|
|
79
|
+
application and serve it to your users; you may not redistribute it on its own.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
|
|
7
|
+
// ../snippet/src/index.ts
|
|
8
|
+
var DEFAULT_CDN = "https://cdn.guideify.in";
|
|
9
|
+
function isCore(candidate) {
|
|
10
|
+
return typeof candidate?.on === "function";
|
|
11
|
+
}
|
|
12
|
+
function current() {
|
|
13
|
+
if (typeof window === "undefined") return void 0;
|
|
14
|
+
return window.guideify;
|
|
15
|
+
}
|
|
16
|
+
function coreReady() {
|
|
17
|
+
return isCore(current());
|
|
18
|
+
}
|
|
19
|
+
function installLoader(options) {
|
|
20
|
+
if (typeof document === "undefined") return null;
|
|
21
|
+
const existing = document.querySelector("script[data-guideify-key]");
|
|
22
|
+
if (current() || existing) return existing;
|
|
23
|
+
const cdn = options.cdnHost ?? DEFAULT_CDN;
|
|
24
|
+
const tag = document.createElement("script");
|
|
25
|
+
tag.async = true;
|
|
26
|
+
tag.src = `${cdn}/v1/loader.js`;
|
|
27
|
+
tag.setAttribute("data-guideify-key", options.apiKey);
|
|
28
|
+
if (options.cdnHost) tag.setAttribute("data-cdn", options.cdnHost);
|
|
29
|
+
if (options.ingestHost) tag.setAttribute("data-ingest", options.ingestHost);
|
|
30
|
+
if (options.publicKey) tag.setAttribute("data-public-key", options.publicKey);
|
|
31
|
+
if (options.debug) tag.setAttribute("data-debug", "");
|
|
32
|
+
if (options.nonce) tag.setAttribute("nonce", options.nonce);
|
|
33
|
+
(document.head || document.documentElement).appendChild(tag);
|
|
34
|
+
return tag;
|
|
35
|
+
}
|
|
36
|
+
var POLL_MS = 50;
|
|
37
|
+
var LOAD_TIMEOUT_MS = 15e3;
|
|
38
|
+
function whenCoreReady(tag, then) {
|
|
39
|
+
if (coreReady()) {
|
|
40
|
+
then();
|
|
41
|
+
return () => void 0;
|
|
42
|
+
}
|
|
43
|
+
let done = false;
|
|
44
|
+
const finish = () => {
|
|
45
|
+
if (done) return;
|
|
46
|
+
done = true;
|
|
47
|
+
stop();
|
|
48
|
+
then();
|
|
49
|
+
};
|
|
50
|
+
const onLoad = () => {
|
|
51
|
+
if (coreReady()) finish();
|
|
52
|
+
};
|
|
53
|
+
const started = Date.now();
|
|
54
|
+
const timer = setInterval(() => {
|
|
55
|
+
if (coreReady()) return finish();
|
|
56
|
+
if (Date.now() - started > LOAD_TIMEOUT_MS) stop();
|
|
57
|
+
}, POLL_MS);
|
|
58
|
+
function stop() {
|
|
59
|
+
clearInterval(timer);
|
|
60
|
+
tag?.removeEventListener("load", onLoad);
|
|
61
|
+
}
|
|
62
|
+
tag?.addEventListener("load", onLoad);
|
|
63
|
+
return stop;
|
|
64
|
+
}
|
|
65
|
+
function callCore(method, args) {
|
|
66
|
+
const target = current();
|
|
67
|
+
const fn = target?.[method];
|
|
68
|
+
if (typeof fn !== "function") return void 0;
|
|
69
|
+
try {
|
|
70
|
+
return fn.apply(target, args);
|
|
71
|
+
} catch {
|
|
72
|
+
return void 0;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
var GuideifyContext = react.createContext(null);
|
|
76
|
+
var pending = /* @__PURE__ */ new Set();
|
|
77
|
+
function attach(event, listener) {
|
|
78
|
+
const off = callCore("on", [event, listener]);
|
|
79
|
+
return typeof off === "function" ? off : () => void 0;
|
|
80
|
+
}
|
|
81
|
+
function subscribe(event, listener) {
|
|
82
|
+
if (coreReady()) return attach(event, listener);
|
|
83
|
+
const intent = { event, listener, live: null, cancelled: false };
|
|
84
|
+
pending.add(intent);
|
|
85
|
+
return () => {
|
|
86
|
+
intent.cancelled = true;
|
|
87
|
+
pending.delete(intent);
|
|
88
|
+
intent.live?.();
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function drainPendingSubscriptions() {
|
|
92
|
+
if (!coreReady()) return;
|
|
93
|
+
for (const intent of [...pending]) {
|
|
94
|
+
pending.delete(intent);
|
|
95
|
+
if (!intent.cancelled) intent.live = attach(intent.event, intent.listener);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function useGuideify() {
|
|
99
|
+
const context = react.useContext(GuideifyContext);
|
|
100
|
+
if (!context) {
|
|
101
|
+
throw new Error("useGuideify must be called inside a <GuideifyProvider>");
|
|
102
|
+
}
|
|
103
|
+
const { ready } = context;
|
|
104
|
+
return react.useMemo(
|
|
105
|
+
() => ({
|
|
106
|
+
start: (flowId) => void callCore("start", [flowId]),
|
|
107
|
+
stop: () => void callCore("stop", []),
|
|
108
|
+
track: (name, props) => void callCore("track", props === void 0 ? [name] : [name, props]),
|
|
109
|
+
identify: (userId, traits, opts) => void callCore("identify", [userId, traits ?? {}, opts]),
|
|
110
|
+
reset: () => void callCore("reset", []),
|
|
111
|
+
on: subscribe,
|
|
112
|
+
ready
|
|
113
|
+
}),
|
|
114
|
+
[ready]
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
function GuideifyProvider({ children, ...options }) {
|
|
118
|
+
const [ready, setReady] = react.useState(false);
|
|
119
|
+
const { apiKey, cdnHost, ingestHost, publicKey, debug, nonce } = options;
|
|
120
|
+
react.useEffect(() => {
|
|
121
|
+
if (!apiKey) return;
|
|
122
|
+
const tag = installLoader({ apiKey, cdnHost, ingestHost, publicKey, debug, nonce });
|
|
123
|
+
return whenCoreReady(tag, () => {
|
|
124
|
+
drainPendingSubscriptions();
|
|
125
|
+
setReady(true);
|
|
126
|
+
});
|
|
127
|
+
}, [apiKey, cdnHost, ingestHost, publicKey, debug, nonce]);
|
|
128
|
+
const value = react.useMemo(() => ({ apiKey, ready }), [apiKey, ready]);
|
|
129
|
+
return /* @__PURE__ */ jsxRuntime.jsx(GuideifyContext.Provider, { value, children });
|
|
130
|
+
}
|
|
131
|
+
function GuideifyTarget({ id, children }) {
|
|
132
|
+
if (!react.isValidElement(children)) return children;
|
|
133
|
+
const child = children;
|
|
134
|
+
if (child.props["data-guideify-id"] !== void 0) return child;
|
|
135
|
+
return react.cloneElement(child, { "data-guideify-id": id });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
exports.DEFAULT_CDN = DEFAULT_CDN;
|
|
139
|
+
exports.GuideifyProvider = GuideifyProvider;
|
|
140
|
+
exports.GuideifyTarget = GuideifyTarget;
|
|
141
|
+
exports.useGuideify = useGuideify;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The published config contract.
|
|
5
|
+
*
|
|
6
|
+
* These types are the wire format between the publisher (which compiles them
|
|
7
|
+
* into an immutable CDN artifact) and the SDK (which parses it in the end
|
|
8
|
+
* user's browser). ARCHITECTURE §14: it must be impossible for those two to
|
|
9
|
+
* drift, which is why they live in one package imported by both.
|
|
10
|
+
*
|
|
11
|
+
* Changes here are BREAKING unless purely additive with a safe default. Bump
|
|
12
|
+
* `GuideifyConfig.version` and keep the SDK tolerant of unknown fields.
|
|
13
|
+
*/
|
|
14
|
+
interface AncestorHint {
|
|
15
|
+
tag: string;
|
|
16
|
+
id?: string;
|
|
17
|
+
role?: string;
|
|
18
|
+
classes?: string[];
|
|
19
|
+
landmark?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Multi-signal element identity. Deliberately records far more than is needed
|
|
23
|
+
* so the resolver can degrade gracefully as the host app changes.
|
|
24
|
+
* See BUILD_REPORT §3.2.
|
|
25
|
+
*/
|
|
26
|
+
interface ElementFingerprint {
|
|
27
|
+
/** Tier 1: the host app opted in with data-guideify-id. Always wins. */
|
|
28
|
+
explicitId?: string;
|
|
29
|
+
testId?: string;
|
|
30
|
+
domId?: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
ariaLabel?: string;
|
|
33
|
+
role?: string;
|
|
34
|
+
tag: string;
|
|
35
|
+
cssPath?: string;
|
|
36
|
+
stableClasses?: string[];
|
|
37
|
+
ancestors?: AncestorHint[];
|
|
38
|
+
siblingIndex?: number;
|
|
39
|
+
siblingCount?: number;
|
|
40
|
+
text?: string;
|
|
41
|
+
href?: string;
|
|
42
|
+
rectRatio?: {
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
w: number;
|
|
46
|
+
h: number;
|
|
47
|
+
};
|
|
48
|
+
captureViewport?: {
|
|
49
|
+
w: number;
|
|
50
|
+
h: number;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Content is a structured document, never an HTML string. The renderer walks
|
|
55
|
+
* this tree with createElement/textContent, which removes the injection class
|
|
56
|
+
* rather than filtering it. See BUILD_REPORT §9.3.
|
|
57
|
+
*/
|
|
58
|
+
type Mark = {
|
|
59
|
+
type: 'bold';
|
|
60
|
+
} | {
|
|
61
|
+
type: 'italic';
|
|
62
|
+
} | {
|
|
63
|
+
type: 'code';
|
|
64
|
+
} | {
|
|
65
|
+
type: 'link';
|
|
66
|
+
href: string;
|
|
67
|
+
};
|
|
68
|
+
type ContentNode = {
|
|
69
|
+
type: 'text';
|
|
70
|
+
text: string;
|
|
71
|
+
marks?: Mark[];
|
|
72
|
+
} | {
|
|
73
|
+
type: 'paragraph';
|
|
74
|
+
content?: ContentNode[];
|
|
75
|
+
} | {
|
|
76
|
+
type: 'heading';
|
|
77
|
+
level?: 1 | 2 | 3;
|
|
78
|
+
content?: ContentNode[];
|
|
79
|
+
} | {
|
|
80
|
+
type: 'bulletList';
|
|
81
|
+
content?: ContentNode[];
|
|
82
|
+
} | {
|
|
83
|
+
type: 'orderedList';
|
|
84
|
+
content?: ContentNode[];
|
|
85
|
+
} | {
|
|
86
|
+
type: 'listItem';
|
|
87
|
+
content?: ContentNode[];
|
|
88
|
+
} | {
|
|
89
|
+
type: 'code';
|
|
90
|
+
text: string;
|
|
91
|
+
} | {
|
|
92
|
+
type: 'image';
|
|
93
|
+
src: string;
|
|
94
|
+
alt?: string;
|
|
95
|
+
} | {
|
|
96
|
+
type: 'doc';
|
|
97
|
+
content?: ContentNode[];
|
|
98
|
+
};
|
|
99
|
+
interface StepButton {
|
|
100
|
+
label: string;
|
|
101
|
+
action: 'next' | 'prev' | 'dismiss' | 'complete' | 'url';
|
|
102
|
+
href?: string;
|
|
103
|
+
variant?: 'primary' | 'secondary' | 'ghost';
|
|
104
|
+
}
|
|
105
|
+
interface StepContent {
|
|
106
|
+
title?: string;
|
|
107
|
+
body?: ContentNode;
|
|
108
|
+
media?: {
|
|
109
|
+
type: 'image';
|
|
110
|
+
src: string;
|
|
111
|
+
alt?: string;
|
|
112
|
+
};
|
|
113
|
+
buttons?: StepButton[];
|
|
114
|
+
}
|
|
115
|
+
type StepType = 'tooltip' | 'modal';
|
|
116
|
+
type Placement = 'auto' | 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
|
|
117
|
+
type AdvanceOn = {
|
|
118
|
+
type: 'next_button';
|
|
119
|
+
} | {
|
|
120
|
+
type: 'target_click';
|
|
121
|
+
} | {
|
|
122
|
+
type: 'url_match';
|
|
123
|
+
pattern: string;
|
|
124
|
+
} | {
|
|
125
|
+
type: 'event';
|
|
126
|
+
name: string;
|
|
127
|
+
} | {
|
|
128
|
+
type: 'element_appears';
|
|
129
|
+
target: ElementFingerprint;
|
|
130
|
+
};
|
|
131
|
+
/** What to do when a step's target cannot be resolved. */
|
|
132
|
+
type StepFallback = 'skip' | 'modal' | 'abort';
|
|
133
|
+
interface Step {
|
|
134
|
+
id: string;
|
|
135
|
+
type: StepType;
|
|
136
|
+
/** Absent for modal steps, which are not anchored to anything. */
|
|
137
|
+
target?: ElementFingerprint;
|
|
138
|
+
content: StepContent;
|
|
139
|
+
placement?: Placement;
|
|
140
|
+
advanceOn?: AdvanceOn;
|
|
141
|
+
/** Only show once the URL matches; otherwise wait for navigation. */
|
|
142
|
+
urlPattern?: string;
|
|
143
|
+
/** Let clicks through to the highlighted element. */
|
|
144
|
+
interactive?: boolean;
|
|
145
|
+
fallback?: StepFallback;
|
|
146
|
+
/** ms to wait for the target before giving up. */
|
|
147
|
+
timeout?: number;
|
|
148
|
+
/** Extra px of breathing room around the spotlight hole. */
|
|
149
|
+
padding?: number;
|
|
150
|
+
}
|
|
151
|
+
type RuleOperator = 'eq' | 'neq' | 'in' | 'not_in' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'starts_with' | 'matches_regex' | 'within_last' | 'before' | 'after' | 'exists' | 'not_exists';
|
|
152
|
+
interface Condition {
|
|
153
|
+
field: string;
|
|
154
|
+
operator: RuleOperator;
|
|
155
|
+
value?: unknown;
|
|
156
|
+
}
|
|
157
|
+
interface RuleGroup {
|
|
158
|
+
op: 'and' | 'or';
|
|
159
|
+
rules: RuleNode[];
|
|
160
|
+
}
|
|
161
|
+
type RuleNode = Condition | RuleGroup;
|
|
162
|
+
type Trigger = {
|
|
163
|
+
type: 'auto';
|
|
164
|
+
} | {
|
|
165
|
+
type: 'manual';
|
|
166
|
+
} | {
|
|
167
|
+
type: 'url';
|
|
168
|
+
pattern: string;
|
|
169
|
+
} | {
|
|
170
|
+
type: 'event';
|
|
171
|
+
name: string;
|
|
172
|
+
} | {
|
|
173
|
+
type: 'element_appears';
|
|
174
|
+
target: ElementFingerprint;
|
|
175
|
+
};
|
|
176
|
+
type Frequency = 'once' | 'session' | 'until_complete' | 'always';
|
|
177
|
+
interface Flow {
|
|
178
|
+
id: string;
|
|
179
|
+
name: string;
|
|
180
|
+
version: number;
|
|
181
|
+
trigger: Trigger;
|
|
182
|
+
audience?: RuleNode;
|
|
183
|
+
frequency: Frequency;
|
|
184
|
+
priority?: number;
|
|
185
|
+
/** Can the end user close this flow? Use false sparingly. */
|
|
186
|
+
dismissible?: boolean;
|
|
187
|
+
steps: Step[];
|
|
188
|
+
}
|
|
189
|
+
interface ThemeTokens {
|
|
190
|
+
accent?: string;
|
|
191
|
+
scrim?: string;
|
|
192
|
+
radius?: string;
|
|
193
|
+
fontFamily?: string;
|
|
194
|
+
}
|
|
195
|
+
interface ConfigSettings {
|
|
196
|
+
/** Max flows auto-started per session. Stops customers spamming their users. */
|
|
197
|
+
maxFlowsPerSession?: number;
|
|
198
|
+
maxFlowsPerDay?: number;
|
|
199
|
+
theme?: ThemeTokens;
|
|
200
|
+
}
|
|
201
|
+
interface GuideifyConfig {
|
|
202
|
+
version: number;
|
|
203
|
+
envKey: string;
|
|
204
|
+
flows: Flow[];
|
|
205
|
+
settings?: ConfigSettings;
|
|
206
|
+
}
|
|
207
|
+
interface UserTraits {
|
|
208
|
+
[key: string]: unknown;
|
|
209
|
+
}
|
|
210
|
+
interface InitOptions {
|
|
211
|
+
apiKey: string;
|
|
212
|
+
/** Origin serving the hash-addressed config blobs. */
|
|
213
|
+
cdnHost?: string;
|
|
214
|
+
/** Origin receiving batched analytics. */
|
|
215
|
+
ingestHost?: string;
|
|
216
|
+
/**
|
|
217
|
+
* Origin of the Guideify API. Reached only by design mode, to spend a nonce
|
|
218
|
+
* for a token — the end-user hot path never touches this service (§0).
|
|
219
|
+
*/
|
|
220
|
+
apiHost?: string;
|
|
221
|
+
/**
|
|
222
|
+
* Origin of the Guideify dashboard, pinned on both halves of the design-mode
|
|
223
|
+
* handshake. Overridden only for self-hosted deployments and for the demo,
|
|
224
|
+
* and it is a *security* parameter: it decides which window may vouch for a
|
|
225
|
+
* nonce, so it is compiled in rather than read from the page.
|
|
226
|
+
*/
|
|
227
|
+
dashboardOrigin?: string;
|
|
228
|
+
/**
|
|
229
|
+
* Base64 Ed25519 public key for this environment, from `GET /v1/envs/:id`.
|
|
230
|
+
*
|
|
231
|
+
* Supplying it turns on config verification: the pointer must be signed by the
|
|
232
|
+
* matching key and the artifact must hash to what that signature covers, or no
|
|
233
|
+
* flows run at all. It belongs in the snippet rather than in the config
|
|
234
|
+
* because the point is that it does not come from the bucket. §5.1.
|
|
235
|
+
*/
|
|
236
|
+
publicKey?: string;
|
|
237
|
+
/** Bypass the network entirely and run against a literal config (tests, demos). */
|
|
238
|
+
config?: GuideifyConfig;
|
|
239
|
+
debug?: boolean;
|
|
240
|
+
/** Disable automatic flow evaluation; only `start()` will run anything. */
|
|
241
|
+
manualOnly?: boolean;
|
|
242
|
+
}
|
|
243
|
+
interface IdentifyOptions {
|
|
244
|
+
/** HMAC of the user id, proving the host's backend vouches for it. §9.2 */
|
|
245
|
+
userHash?: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @guideify/snippet — how the core gets onto the page, for every framework
|
|
250
|
+
* wrapper that has to do it. F-06.
|
|
251
|
+
*
|
|
252
|
+
* Extracted rather than copied into each wrapper, on D-259's precedent: two
|
|
253
|
+
* packages owning two copies of one contract is how the two come to disagree
|
|
254
|
+
* about it, and everything here is a contract with something else. The attribute
|
|
255
|
+
* names are the loader's, the default origin is the loader's, and the readiness
|
|
256
|
+
* test is a fact about the core. None of it is framework-shaped, which is why
|
|
257
|
+
* there is nothing to specialise per wrapper.
|
|
258
|
+
*
|
|
259
|
+
* Private and source-only, like `@guideify/shared`: consumers inline it, because
|
|
260
|
+
* a bare `@guideify/snippet` import left in a published `dist` is one an npm
|
|
261
|
+
* consumer could never resolve.
|
|
262
|
+
*
|
|
263
|
+
* **The wrapper injects the loader; it does not bundle the core.** npm-installing
|
|
264
|
+
* `@guideify/js` beside this package would be one line shorter and would pin every
|
|
265
|
+
* customer to whatever core they built against. Two release-readiness items
|
|
266
|
+
* depend on that not being true: R-03 canaries 1% of environments onto a new core
|
|
267
|
+
* hash, and R-04 is a kill switch that pins a per-environment version in Workers
|
|
268
|
+
* KV and expects it live within 60 seconds. Neither reaches a copy sitting in
|
|
269
|
+
* somebody's webpack output. The loader exists precisely so the core is
|
|
270
|
+
* "versioned separately so we can ship SDK updates without customers touching
|
|
271
|
+
* this line ever again", and a framework wrapper is not a reason to give that up.
|
|
272
|
+
*
|
|
273
|
+
* So every import from `@guideify/js` in this package is `import type`, and
|
|
274
|
+
* `test/boundary.test.mjs` is what keeps it that way.
|
|
275
|
+
*/
|
|
276
|
+
/**
|
|
277
|
+
* The subset of `InitOptions` the pasted snippet can actually carry.
|
|
278
|
+
*
|
|
279
|
+
* Deliberately not all of it. `standalone.ts` auto-initialises from the script
|
|
280
|
+
* tag's data attributes, and these five are the ones it reads — so these are the
|
|
281
|
+
* five a tag can express. `manualOnly`, `apiHost`, `dashboardOrigin` and `config`
|
|
282
|
+
* have no attribute and are not silently dropped here: they are absent from the
|
|
283
|
+
* type, so asking for one is a compile error rather than a setting that appears
|
|
284
|
+
* to be applied and is not.
|
|
285
|
+
*/
|
|
286
|
+
interface SnippetOptions extends Pick<InitOptions, 'apiKey'> {
|
|
287
|
+
cdnHost?: string | undefined;
|
|
288
|
+
ingestHost?: string | undefined;
|
|
289
|
+
publicKey?: string | undefined;
|
|
290
|
+
debug?: boolean | undefined;
|
|
291
|
+
/**
|
|
292
|
+
* CSP `nonce` for the injected tag — BUILD_REPORT §6.4.
|
|
293
|
+
*
|
|
294
|
+
* A page served with `script-src 'nonce-…'` refuses an injected script that
|
|
295
|
+
* does not carry it, and the failure is silent from the host app's point of
|
|
296
|
+
* view: no tours, no error anybody connects to us.
|
|
297
|
+
*/
|
|
298
|
+
nonce?: string | undefined;
|
|
299
|
+
}
|
|
300
|
+
/** Where the loader lives when nothing says otherwise. Mirrors `loader.ts`. */
|
|
301
|
+
declare const DEFAULT_CDN = "https://cdn.guideify.in";
|
|
302
|
+
|
|
303
|
+
interface GuideifyProviderProps extends SnippetOptions {
|
|
304
|
+
children: ReactNode;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Mounts Guideify for a React tree — BUILD_REPORT §6.3.
|
|
308
|
+
*
|
|
309
|
+
* ```tsx
|
|
310
|
+
* <GuideifyProvider apiKey={process.env.NEXT_PUBLIC_GUIDEIFY_KEY}>
|
|
311
|
+
* <App />
|
|
312
|
+
* </GuideifyProvider>
|
|
313
|
+
* ```
|
|
314
|
+
*
|
|
315
|
+
* **Nothing touches `window` during render**, which is the Next.js App Router
|
|
316
|
+
* pitfall §6.4 names: the tag goes in from an effect, so a server render
|
|
317
|
+
* produces the same markup as the client's first pass and nothing hydrates
|
|
318
|
+
* mismatched.
|
|
319
|
+
*
|
|
320
|
+
* **This does not call `init`.** The loader's tag carries `data-guideify-key` and
|
|
321
|
+
* `standalone.ts` initialises from it on arrival, so a provider that also called
|
|
322
|
+
* `init` would be the second call and the core would warn and ignore it. The
|
|
323
|
+
* props are therefore exactly what a tag can express (`SnippetOptions`) rather
|
|
324
|
+
* than all of `InitOptions`.
|
|
325
|
+
*
|
|
326
|
+
* Mounted twice, the second one adds nothing: `installLoader` refuses to inject
|
|
327
|
+
* over an existing `window.guideify`, so a nested provider is a no-op rather than
|
|
328
|
+
* a second core.
|
|
329
|
+
*/
|
|
330
|
+
declare function GuideifyProvider({ children, ...options }: GuideifyProviderProps): ReactNode;
|
|
331
|
+
|
|
332
|
+
interface GuideifyTargetProps {
|
|
333
|
+
/** The stable name a flow step aims at. Yours to choose; never changes. */
|
|
334
|
+
id: string;
|
|
335
|
+
children: ReactNode;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* The paved path from BUILD_REPORT §3.6, as one import.
|
|
339
|
+
*
|
|
340
|
+
* ```tsx
|
|
341
|
+
* <GuideifyTarget id="create-project">
|
|
342
|
+
* <button onClick={create}>New project</button>
|
|
343
|
+
* </GuideifyTarget>
|
|
344
|
+
* ```
|
|
345
|
+
*
|
|
346
|
+
* An element marked this way resolves exactly and for free: `data-guideify-id` is
|
|
347
|
+
* the resolver's first tier, so it never scores candidates, never lands on the
|
|
348
|
+
* wrong one, and never drifts when the class names around it change. §3.6's
|
|
349
|
+
* finding is that the customers who do this do not churn over broken tours —
|
|
350
|
+
* which makes the cost of doing it the thing worth attacking, and one import
|
|
351
|
+
* beats a code review about a magic attribute.
|
|
352
|
+
*
|
|
353
|
+
* **Renders no element of its own.** It clones the single child and adds the
|
|
354
|
+
* attribute, because a wrapper `<div>` would change the host app's layout to
|
|
355
|
+
* suit our targeting — exactly the "zero impact on host app" §6.5 promises. The
|
|
356
|
+
* cost is the constraint: one child, and it has to be an element that puts its
|
|
357
|
+
* props on a DOM node.
|
|
358
|
+
*
|
|
359
|
+
* Given anything else — several children, a bare string, a fragment — it renders
|
|
360
|
+
* them untouched rather than throwing. A missing attribute costs a step its
|
|
361
|
+
* exact match and the resolver falls back to fingerprinting; an exception costs
|
|
362
|
+
* the customer their page.
|
|
363
|
+
*/
|
|
364
|
+
declare function GuideifyTarget({ id, children }: GuideifyTargetProps): ReactNode;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The four public events the core emits. `PublicEvent` is module-private in
|
|
368
|
+
* `core/guideify.ts`, so it is restated rather than imported — and
|
|
369
|
+
* `test/surface.test.mjs` reads that file to prove the two lists still agree.
|
|
370
|
+
*/
|
|
371
|
+
type GuideifyEvent = 'flow_started' | 'flow_completed' | 'flow_dismissed' | 'step_viewed';
|
|
372
|
+
type Unsubscribe = () => void;
|
|
373
|
+
interface GuideifyApi {
|
|
374
|
+
/** Starts one flow by id, regardless of its trigger or frequency rules. */
|
|
375
|
+
start(flowId: string): void;
|
|
376
|
+
/** Ends whatever is running. Safe when nothing is. */
|
|
377
|
+
stop(): void;
|
|
378
|
+
track(name: string, props?: Record<string, unknown>): void;
|
|
379
|
+
identify(userId: string, traits?: UserTraits, opts?: IdentifyOptions): void;
|
|
380
|
+
/** Forgets this browser's identity and progress — call it on sign-out. */
|
|
381
|
+
reset(): void;
|
|
382
|
+
/**
|
|
383
|
+
* Subscribes to a flow lifecycle event.
|
|
384
|
+
*
|
|
385
|
+
* The one method that cannot be queued: the loader's stub hands back nothing,
|
|
386
|
+
* and `on` owes its caller an unsubscribe immediately. So before the core
|
|
387
|
+
* arrives this registers the intent and returns a canceller that honours it —
|
|
388
|
+
* unsubscribing between mount and arrival really does mean the listener is
|
|
389
|
+
* never attached.
|
|
390
|
+
*/
|
|
391
|
+
on(event: GuideifyEvent, listener: (payload: unknown) => void): Unsubscribe;
|
|
392
|
+
/** Whether the core has arrived. Calls made before it do not need this. */
|
|
393
|
+
ready: boolean;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* The hook — BUILD_REPORT §6.3.
|
|
397
|
+
*
|
|
398
|
+
* ```tsx
|
|
399
|
+
* const { start, track, identify } = useGuideify();
|
|
400
|
+
* ```
|
|
401
|
+
*
|
|
402
|
+
* **Every method is safe to call before the core arrives.** The loader installs
|
|
403
|
+
* a queue for exactly this, so `track()` in a mount effect is not a race the
|
|
404
|
+
* caller has to think about — which is most of what this package is for.
|
|
405
|
+
*
|
|
406
|
+
* Throws when there is no provider above it, and that is the one thing here that
|
|
407
|
+
* does throw: it is a mistake in the component tree, it is the same on every
|
|
408
|
+
* render, and the alternative is a hook that silently does nothing for the life
|
|
409
|
+
* of the application.
|
|
410
|
+
*/
|
|
411
|
+
declare function useGuideify(): GuideifyApi;
|
|
412
|
+
|
|
413
|
+
export { DEFAULT_CDN, type GuideifyApi, type GuideifyEvent, GuideifyProvider, type GuideifyProviderProps, GuideifyTarget, type GuideifyTargetProps, type SnippetOptions, type Unsubscribe, useGuideify };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The published config contract.
|
|
5
|
+
*
|
|
6
|
+
* These types are the wire format between the publisher (which compiles them
|
|
7
|
+
* into an immutable CDN artifact) and the SDK (which parses it in the end
|
|
8
|
+
* user's browser). ARCHITECTURE §14: it must be impossible for those two to
|
|
9
|
+
* drift, which is why they live in one package imported by both.
|
|
10
|
+
*
|
|
11
|
+
* Changes here are BREAKING unless purely additive with a safe default. Bump
|
|
12
|
+
* `GuideifyConfig.version` and keep the SDK tolerant of unknown fields.
|
|
13
|
+
*/
|
|
14
|
+
interface AncestorHint {
|
|
15
|
+
tag: string;
|
|
16
|
+
id?: string;
|
|
17
|
+
role?: string;
|
|
18
|
+
classes?: string[];
|
|
19
|
+
landmark?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Multi-signal element identity. Deliberately records far more than is needed
|
|
23
|
+
* so the resolver can degrade gracefully as the host app changes.
|
|
24
|
+
* See BUILD_REPORT §3.2.
|
|
25
|
+
*/
|
|
26
|
+
interface ElementFingerprint {
|
|
27
|
+
/** Tier 1: the host app opted in with data-guideify-id. Always wins. */
|
|
28
|
+
explicitId?: string;
|
|
29
|
+
testId?: string;
|
|
30
|
+
domId?: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
ariaLabel?: string;
|
|
33
|
+
role?: string;
|
|
34
|
+
tag: string;
|
|
35
|
+
cssPath?: string;
|
|
36
|
+
stableClasses?: string[];
|
|
37
|
+
ancestors?: AncestorHint[];
|
|
38
|
+
siblingIndex?: number;
|
|
39
|
+
siblingCount?: number;
|
|
40
|
+
text?: string;
|
|
41
|
+
href?: string;
|
|
42
|
+
rectRatio?: {
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
w: number;
|
|
46
|
+
h: number;
|
|
47
|
+
};
|
|
48
|
+
captureViewport?: {
|
|
49
|
+
w: number;
|
|
50
|
+
h: number;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Content is a structured document, never an HTML string. The renderer walks
|
|
55
|
+
* this tree with createElement/textContent, which removes the injection class
|
|
56
|
+
* rather than filtering it. See BUILD_REPORT §9.3.
|
|
57
|
+
*/
|
|
58
|
+
type Mark = {
|
|
59
|
+
type: 'bold';
|
|
60
|
+
} | {
|
|
61
|
+
type: 'italic';
|
|
62
|
+
} | {
|
|
63
|
+
type: 'code';
|
|
64
|
+
} | {
|
|
65
|
+
type: 'link';
|
|
66
|
+
href: string;
|
|
67
|
+
};
|
|
68
|
+
type ContentNode = {
|
|
69
|
+
type: 'text';
|
|
70
|
+
text: string;
|
|
71
|
+
marks?: Mark[];
|
|
72
|
+
} | {
|
|
73
|
+
type: 'paragraph';
|
|
74
|
+
content?: ContentNode[];
|
|
75
|
+
} | {
|
|
76
|
+
type: 'heading';
|
|
77
|
+
level?: 1 | 2 | 3;
|
|
78
|
+
content?: ContentNode[];
|
|
79
|
+
} | {
|
|
80
|
+
type: 'bulletList';
|
|
81
|
+
content?: ContentNode[];
|
|
82
|
+
} | {
|
|
83
|
+
type: 'orderedList';
|
|
84
|
+
content?: ContentNode[];
|
|
85
|
+
} | {
|
|
86
|
+
type: 'listItem';
|
|
87
|
+
content?: ContentNode[];
|
|
88
|
+
} | {
|
|
89
|
+
type: 'code';
|
|
90
|
+
text: string;
|
|
91
|
+
} | {
|
|
92
|
+
type: 'image';
|
|
93
|
+
src: string;
|
|
94
|
+
alt?: string;
|
|
95
|
+
} | {
|
|
96
|
+
type: 'doc';
|
|
97
|
+
content?: ContentNode[];
|
|
98
|
+
};
|
|
99
|
+
interface StepButton {
|
|
100
|
+
label: string;
|
|
101
|
+
action: 'next' | 'prev' | 'dismiss' | 'complete' | 'url';
|
|
102
|
+
href?: string;
|
|
103
|
+
variant?: 'primary' | 'secondary' | 'ghost';
|
|
104
|
+
}
|
|
105
|
+
interface StepContent {
|
|
106
|
+
title?: string;
|
|
107
|
+
body?: ContentNode;
|
|
108
|
+
media?: {
|
|
109
|
+
type: 'image';
|
|
110
|
+
src: string;
|
|
111
|
+
alt?: string;
|
|
112
|
+
};
|
|
113
|
+
buttons?: StepButton[];
|
|
114
|
+
}
|
|
115
|
+
type StepType = 'tooltip' | 'modal';
|
|
116
|
+
type Placement = 'auto' | 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
|
|
117
|
+
type AdvanceOn = {
|
|
118
|
+
type: 'next_button';
|
|
119
|
+
} | {
|
|
120
|
+
type: 'target_click';
|
|
121
|
+
} | {
|
|
122
|
+
type: 'url_match';
|
|
123
|
+
pattern: string;
|
|
124
|
+
} | {
|
|
125
|
+
type: 'event';
|
|
126
|
+
name: string;
|
|
127
|
+
} | {
|
|
128
|
+
type: 'element_appears';
|
|
129
|
+
target: ElementFingerprint;
|
|
130
|
+
};
|
|
131
|
+
/** What to do when a step's target cannot be resolved. */
|
|
132
|
+
type StepFallback = 'skip' | 'modal' | 'abort';
|
|
133
|
+
interface Step {
|
|
134
|
+
id: string;
|
|
135
|
+
type: StepType;
|
|
136
|
+
/** Absent for modal steps, which are not anchored to anything. */
|
|
137
|
+
target?: ElementFingerprint;
|
|
138
|
+
content: StepContent;
|
|
139
|
+
placement?: Placement;
|
|
140
|
+
advanceOn?: AdvanceOn;
|
|
141
|
+
/** Only show once the URL matches; otherwise wait for navigation. */
|
|
142
|
+
urlPattern?: string;
|
|
143
|
+
/** Let clicks through to the highlighted element. */
|
|
144
|
+
interactive?: boolean;
|
|
145
|
+
fallback?: StepFallback;
|
|
146
|
+
/** ms to wait for the target before giving up. */
|
|
147
|
+
timeout?: number;
|
|
148
|
+
/** Extra px of breathing room around the spotlight hole. */
|
|
149
|
+
padding?: number;
|
|
150
|
+
}
|
|
151
|
+
type RuleOperator = 'eq' | 'neq' | 'in' | 'not_in' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'starts_with' | 'matches_regex' | 'within_last' | 'before' | 'after' | 'exists' | 'not_exists';
|
|
152
|
+
interface Condition {
|
|
153
|
+
field: string;
|
|
154
|
+
operator: RuleOperator;
|
|
155
|
+
value?: unknown;
|
|
156
|
+
}
|
|
157
|
+
interface RuleGroup {
|
|
158
|
+
op: 'and' | 'or';
|
|
159
|
+
rules: RuleNode[];
|
|
160
|
+
}
|
|
161
|
+
type RuleNode = Condition | RuleGroup;
|
|
162
|
+
type Trigger = {
|
|
163
|
+
type: 'auto';
|
|
164
|
+
} | {
|
|
165
|
+
type: 'manual';
|
|
166
|
+
} | {
|
|
167
|
+
type: 'url';
|
|
168
|
+
pattern: string;
|
|
169
|
+
} | {
|
|
170
|
+
type: 'event';
|
|
171
|
+
name: string;
|
|
172
|
+
} | {
|
|
173
|
+
type: 'element_appears';
|
|
174
|
+
target: ElementFingerprint;
|
|
175
|
+
};
|
|
176
|
+
type Frequency = 'once' | 'session' | 'until_complete' | 'always';
|
|
177
|
+
interface Flow {
|
|
178
|
+
id: string;
|
|
179
|
+
name: string;
|
|
180
|
+
version: number;
|
|
181
|
+
trigger: Trigger;
|
|
182
|
+
audience?: RuleNode;
|
|
183
|
+
frequency: Frequency;
|
|
184
|
+
priority?: number;
|
|
185
|
+
/** Can the end user close this flow? Use false sparingly. */
|
|
186
|
+
dismissible?: boolean;
|
|
187
|
+
steps: Step[];
|
|
188
|
+
}
|
|
189
|
+
interface ThemeTokens {
|
|
190
|
+
accent?: string;
|
|
191
|
+
scrim?: string;
|
|
192
|
+
radius?: string;
|
|
193
|
+
fontFamily?: string;
|
|
194
|
+
}
|
|
195
|
+
interface ConfigSettings {
|
|
196
|
+
/** Max flows auto-started per session. Stops customers spamming their users. */
|
|
197
|
+
maxFlowsPerSession?: number;
|
|
198
|
+
maxFlowsPerDay?: number;
|
|
199
|
+
theme?: ThemeTokens;
|
|
200
|
+
}
|
|
201
|
+
interface GuideifyConfig {
|
|
202
|
+
version: number;
|
|
203
|
+
envKey: string;
|
|
204
|
+
flows: Flow[];
|
|
205
|
+
settings?: ConfigSettings;
|
|
206
|
+
}
|
|
207
|
+
interface UserTraits {
|
|
208
|
+
[key: string]: unknown;
|
|
209
|
+
}
|
|
210
|
+
interface InitOptions {
|
|
211
|
+
apiKey: string;
|
|
212
|
+
/** Origin serving the hash-addressed config blobs. */
|
|
213
|
+
cdnHost?: string;
|
|
214
|
+
/** Origin receiving batched analytics. */
|
|
215
|
+
ingestHost?: string;
|
|
216
|
+
/**
|
|
217
|
+
* Origin of the Guideify API. Reached only by design mode, to spend a nonce
|
|
218
|
+
* for a token — the end-user hot path never touches this service (§0).
|
|
219
|
+
*/
|
|
220
|
+
apiHost?: string;
|
|
221
|
+
/**
|
|
222
|
+
* Origin of the Guideify dashboard, pinned on both halves of the design-mode
|
|
223
|
+
* handshake. Overridden only for self-hosted deployments and for the demo,
|
|
224
|
+
* and it is a *security* parameter: it decides which window may vouch for a
|
|
225
|
+
* nonce, so it is compiled in rather than read from the page.
|
|
226
|
+
*/
|
|
227
|
+
dashboardOrigin?: string;
|
|
228
|
+
/**
|
|
229
|
+
* Base64 Ed25519 public key for this environment, from `GET /v1/envs/:id`.
|
|
230
|
+
*
|
|
231
|
+
* Supplying it turns on config verification: the pointer must be signed by the
|
|
232
|
+
* matching key and the artifact must hash to what that signature covers, or no
|
|
233
|
+
* flows run at all. It belongs in the snippet rather than in the config
|
|
234
|
+
* because the point is that it does not come from the bucket. §5.1.
|
|
235
|
+
*/
|
|
236
|
+
publicKey?: string;
|
|
237
|
+
/** Bypass the network entirely and run against a literal config (tests, demos). */
|
|
238
|
+
config?: GuideifyConfig;
|
|
239
|
+
debug?: boolean;
|
|
240
|
+
/** Disable automatic flow evaluation; only `start()` will run anything. */
|
|
241
|
+
manualOnly?: boolean;
|
|
242
|
+
}
|
|
243
|
+
interface IdentifyOptions {
|
|
244
|
+
/** HMAC of the user id, proving the host's backend vouches for it. §9.2 */
|
|
245
|
+
userHash?: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @guideify/snippet — how the core gets onto the page, for every framework
|
|
250
|
+
* wrapper that has to do it. F-06.
|
|
251
|
+
*
|
|
252
|
+
* Extracted rather than copied into each wrapper, on D-259's precedent: two
|
|
253
|
+
* packages owning two copies of one contract is how the two come to disagree
|
|
254
|
+
* about it, and everything here is a contract with something else. The attribute
|
|
255
|
+
* names are the loader's, the default origin is the loader's, and the readiness
|
|
256
|
+
* test is a fact about the core. None of it is framework-shaped, which is why
|
|
257
|
+
* there is nothing to specialise per wrapper.
|
|
258
|
+
*
|
|
259
|
+
* Private and source-only, like `@guideify/shared`: consumers inline it, because
|
|
260
|
+
* a bare `@guideify/snippet` import left in a published `dist` is one an npm
|
|
261
|
+
* consumer could never resolve.
|
|
262
|
+
*
|
|
263
|
+
* **The wrapper injects the loader; it does not bundle the core.** npm-installing
|
|
264
|
+
* `@guideify/js` beside this package would be one line shorter and would pin every
|
|
265
|
+
* customer to whatever core they built against. Two release-readiness items
|
|
266
|
+
* depend on that not being true: R-03 canaries 1% of environments onto a new core
|
|
267
|
+
* hash, and R-04 is a kill switch that pins a per-environment version in Workers
|
|
268
|
+
* KV and expects it live within 60 seconds. Neither reaches a copy sitting in
|
|
269
|
+
* somebody's webpack output. The loader exists precisely so the core is
|
|
270
|
+
* "versioned separately so we can ship SDK updates without customers touching
|
|
271
|
+
* this line ever again", and a framework wrapper is not a reason to give that up.
|
|
272
|
+
*
|
|
273
|
+
* So every import from `@guideify/js` in this package is `import type`, and
|
|
274
|
+
* `test/boundary.test.mjs` is what keeps it that way.
|
|
275
|
+
*/
|
|
276
|
+
/**
|
|
277
|
+
* The subset of `InitOptions` the pasted snippet can actually carry.
|
|
278
|
+
*
|
|
279
|
+
* Deliberately not all of it. `standalone.ts` auto-initialises from the script
|
|
280
|
+
* tag's data attributes, and these five are the ones it reads — so these are the
|
|
281
|
+
* five a tag can express. `manualOnly`, `apiHost`, `dashboardOrigin` and `config`
|
|
282
|
+
* have no attribute and are not silently dropped here: they are absent from the
|
|
283
|
+
* type, so asking for one is a compile error rather than a setting that appears
|
|
284
|
+
* to be applied and is not.
|
|
285
|
+
*/
|
|
286
|
+
interface SnippetOptions extends Pick<InitOptions, 'apiKey'> {
|
|
287
|
+
cdnHost?: string | undefined;
|
|
288
|
+
ingestHost?: string | undefined;
|
|
289
|
+
publicKey?: string | undefined;
|
|
290
|
+
debug?: boolean | undefined;
|
|
291
|
+
/**
|
|
292
|
+
* CSP `nonce` for the injected tag — BUILD_REPORT §6.4.
|
|
293
|
+
*
|
|
294
|
+
* A page served with `script-src 'nonce-…'` refuses an injected script that
|
|
295
|
+
* does not carry it, and the failure is silent from the host app's point of
|
|
296
|
+
* view: no tours, no error anybody connects to us.
|
|
297
|
+
*/
|
|
298
|
+
nonce?: string | undefined;
|
|
299
|
+
}
|
|
300
|
+
/** Where the loader lives when nothing says otherwise. Mirrors `loader.ts`. */
|
|
301
|
+
declare const DEFAULT_CDN = "https://cdn.guideify.in";
|
|
302
|
+
|
|
303
|
+
interface GuideifyProviderProps extends SnippetOptions {
|
|
304
|
+
children: ReactNode;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Mounts Guideify for a React tree — BUILD_REPORT §6.3.
|
|
308
|
+
*
|
|
309
|
+
* ```tsx
|
|
310
|
+
* <GuideifyProvider apiKey={process.env.NEXT_PUBLIC_GUIDEIFY_KEY}>
|
|
311
|
+
* <App />
|
|
312
|
+
* </GuideifyProvider>
|
|
313
|
+
* ```
|
|
314
|
+
*
|
|
315
|
+
* **Nothing touches `window` during render**, which is the Next.js App Router
|
|
316
|
+
* pitfall §6.4 names: the tag goes in from an effect, so a server render
|
|
317
|
+
* produces the same markup as the client's first pass and nothing hydrates
|
|
318
|
+
* mismatched.
|
|
319
|
+
*
|
|
320
|
+
* **This does not call `init`.** The loader's tag carries `data-guideify-key` and
|
|
321
|
+
* `standalone.ts` initialises from it on arrival, so a provider that also called
|
|
322
|
+
* `init` would be the second call and the core would warn and ignore it. The
|
|
323
|
+
* props are therefore exactly what a tag can express (`SnippetOptions`) rather
|
|
324
|
+
* than all of `InitOptions`.
|
|
325
|
+
*
|
|
326
|
+
* Mounted twice, the second one adds nothing: `installLoader` refuses to inject
|
|
327
|
+
* over an existing `window.guideify`, so a nested provider is a no-op rather than
|
|
328
|
+
* a second core.
|
|
329
|
+
*/
|
|
330
|
+
declare function GuideifyProvider({ children, ...options }: GuideifyProviderProps): ReactNode;
|
|
331
|
+
|
|
332
|
+
interface GuideifyTargetProps {
|
|
333
|
+
/** The stable name a flow step aims at. Yours to choose; never changes. */
|
|
334
|
+
id: string;
|
|
335
|
+
children: ReactNode;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* The paved path from BUILD_REPORT §3.6, as one import.
|
|
339
|
+
*
|
|
340
|
+
* ```tsx
|
|
341
|
+
* <GuideifyTarget id="create-project">
|
|
342
|
+
* <button onClick={create}>New project</button>
|
|
343
|
+
* </GuideifyTarget>
|
|
344
|
+
* ```
|
|
345
|
+
*
|
|
346
|
+
* An element marked this way resolves exactly and for free: `data-guideify-id` is
|
|
347
|
+
* the resolver's first tier, so it never scores candidates, never lands on the
|
|
348
|
+
* wrong one, and never drifts when the class names around it change. §3.6's
|
|
349
|
+
* finding is that the customers who do this do not churn over broken tours —
|
|
350
|
+
* which makes the cost of doing it the thing worth attacking, and one import
|
|
351
|
+
* beats a code review about a magic attribute.
|
|
352
|
+
*
|
|
353
|
+
* **Renders no element of its own.** It clones the single child and adds the
|
|
354
|
+
* attribute, because a wrapper `<div>` would change the host app's layout to
|
|
355
|
+
* suit our targeting — exactly the "zero impact on host app" §6.5 promises. The
|
|
356
|
+
* cost is the constraint: one child, and it has to be an element that puts its
|
|
357
|
+
* props on a DOM node.
|
|
358
|
+
*
|
|
359
|
+
* Given anything else — several children, a bare string, a fragment — it renders
|
|
360
|
+
* them untouched rather than throwing. A missing attribute costs a step its
|
|
361
|
+
* exact match and the resolver falls back to fingerprinting; an exception costs
|
|
362
|
+
* the customer their page.
|
|
363
|
+
*/
|
|
364
|
+
declare function GuideifyTarget({ id, children }: GuideifyTargetProps): ReactNode;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The four public events the core emits. `PublicEvent` is module-private in
|
|
368
|
+
* `core/guideify.ts`, so it is restated rather than imported — and
|
|
369
|
+
* `test/surface.test.mjs` reads that file to prove the two lists still agree.
|
|
370
|
+
*/
|
|
371
|
+
type GuideifyEvent = 'flow_started' | 'flow_completed' | 'flow_dismissed' | 'step_viewed';
|
|
372
|
+
type Unsubscribe = () => void;
|
|
373
|
+
interface GuideifyApi {
|
|
374
|
+
/** Starts one flow by id, regardless of its trigger or frequency rules. */
|
|
375
|
+
start(flowId: string): void;
|
|
376
|
+
/** Ends whatever is running. Safe when nothing is. */
|
|
377
|
+
stop(): void;
|
|
378
|
+
track(name: string, props?: Record<string, unknown>): void;
|
|
379
|
+
identify(userId: string, traits?: UserTraits, opts?: IdentifyOptions): void;
|
|
380
|
+
/** Forgets this browser's identity and progress — call it on sign-out. */
|
|
381
|
+
reset(): void;
|
|
382
|
+
/**
|
|
383
|
+
* Subscribes to a flow lifecycle event.
|
|
384
|
+
*
|
|
385
|
+
* The one method that cannot be queued: the loader's stub hands back nothing,
|
|
386
|
+
* and `on` owes its caller an unsubscribe immediately. So before the core
|
|
387
|
+
* arrives this registers the intent and returns a canceller that honours it —
|
|
388
|
+
* unsubscribing between mount and arrival really does mean the listener is
|
|
389
|
+
* never attached.
|
|
390
|
+
*/
|
|
391
|
+
on(event: GuideifyEvent, listener: (payload: unknown) => void): Unsubscribe;
|
|
392
|
+
/** Whether the core has arrived. Calls made before it do not need this. */
|
|
393
|
+
ready: boolean;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* The hook — BUILD_REPORT §6.3.
|
|
397
|
+
*
|
|
398
|
+
* ```tsx
|
|
399
|
+
* const { start, track, identify } = useGuideify();
|
|
400
|
+
* ```
|
|
401
|
+
*
|
|
402
|
+
* **Every method is safe to call before the core arrives.** The loader installs
|
|
403
|
+
* a queue for exactly this, so `track()` in a mount effect is not a race the
|
|
404
|
+
* caller has to think about — which is most of what this package is for.
|
|
405
|
+
*
|
|
406
|
+
* Throws when there is no provider above it, and that is the one thing here that
|
|
407
|
+
* does throw: it is a mistake in the component tree, it is the same on every
|
|
408
|
+
* render, and the alternative is a hook that silently does nothing for the life
|
|
409
|
+
* of the application.
|
|
410
|
+
*/
|
|
411
|
+
declare function useGuideify(): GuideifyApi;
|
|
412
|
+
|
|
413
|
+
export { DEFAULT_CDN, type GuideifyApi, type GuideifyEvent, GuideifyProvider, type GuideifyProviderProps, GuideifyTarget, type GuideifyTargetProps, type SnippetOptions, type Unsubscribe, useGuideify };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { createContext, useContext, useMemo, useState, useEffect, isValidElement, cloneElement } from 'react';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// ../snippet/src/index.ts
|
|
6
|
+
var DEFAULT_CDN = "https://cdn.guideify.in";
|
|
7
|
+
function isCore(candidate) {
|
|
8
|
+
return typeof candidate?.on === "function";
|
|
9
|
+
}
|
|
10
|
+
function current() {
|
|
11
|
+
if (typeof window === "undefined") return void 0;
|
|
12
|
+
return window.guideify;
|
|
13
|
+
}
|
|
14
|
+
function coreReady() {
|
|
15
|
+
return isCore(current());
|
|
16
|
+
}
|
|
17
|
+
function installLoader(options) {
|
|
18
|
+
if (typeof document === "undefined") return null;
|
|
19
|
+
const existing = document.querySelector("script[data-guideify-key]");
|
|
20
|
+
if (current() || existing) return existing;
|
|
21
|
+
const cdn = options.cdnHost ?? DEFAULT_CDN;
|
|
22
|
+
const tag = document.createElement("script");
|
|
23
|
+
tag.async = true;
|
|
24
|
+
tag.src = `${cdn}/v1/loader.js`;
|
|
25
|
+
tag.setAttribute("data-guideify-key", options.apiKey);
|
|
26
|
+
if (options.cdnHost) tag.setAttribute("data-cdn", options.cdnHost);
|
|
27
|
+
if (options.ingestHost) tag.setAttribute("data-ingest", options.ingestHost);
|
|
28
|
+
if (options.publicKey) tag.setAttribute("data-public-key", options.publicKey);
|
|
29
|
+
if (options.debug) tag.setAttribute("data-debug", "");
|
|
30
|
+
if (options.nonce) tag.setAttribute("nonce", options.nonce);
|
|
31
|
+
(document.head || document.documentElement).appendChild(tag);
|
|
32
|
+
return tag;
|
|
33
|
+
}
|
|
34
|
+
var POLL_MS = 50;
|
|
35
|
+
var LOAD_TIMEOUT_MS = 15e3;
|
|
36
|
+
function whenCoreReady(tag, then) {
|
|
37
|
+
if (coreReady()) {
|
|
38
|
+
then();
|
|
39
|
+
return () => void 0;
|
|
40
|
+
}
|
|
41
|
+
let done = false;
|
|
42
|
+
const finish = () => {
|
|
43
|
+
if (done) return;
|
|
44
|
+
done = true;
|
|
45
|
+
stop();
|
|
46
|
+
then();
|
|
47
|
+
};
|
|
48
|
+
const onLoad = () => {
|
|
49
|
+
if (coreReady()) finish();
|
|
50
|
+
};
|
|
51
|
+
const started = Date.now();
|
|
52
|
+
const timer = setInterval(() => {
|
|
53
|
+
if (coreReady()) return finish();
|
|
54
|
+
if (Date.now() - started > LOAD_TIMEOUT_MS) stop();
|
|
55
|
+
}, POLL_MS);
|
|
56
|
+
function stop() {
|
|
57
|
+
clearInterval(timer);
|
|
58
|
+
tag?.removeEventListener("load", onLoad);
|
|
59
|
+
}
|
|
60
|
+
tag?.addEventListener("load", onLoad);
|
|
61
|
+
return stop;
|
|
62
|
+
}
|
|
63
|
+
function callCore(method, args) {
|
|
64
|
+
const target = current();
|
|
65
|
+
const fn = target?.[method];
|
|
66
|
+
if (typeof fn !== "function") return void 0;
|
|
67
|
+
try {
|
|
68
|
+
return fn.apply(target, args);
|
|
69
|
+
} catch {
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
var GuideifyContext = createContext(null);
|
|
74
|
+
var pending = /* @__PURE__ */ new Set();
|
|
75
|
+
function attach(event, listener) {
|
|
76
|
+
const off = callCore("on", [event, listener]);
|
|
77
|
+
return typeof off === "function" ? off : () => void 0;
|
|
78
|
+
}
|
|
79
|
+
function subscribe(event, listener) {
|
|
80
|
+
if (coreReady()) return attach(event, listener);
|
|
81
|
+
const intent = { event, listener, live: null, cancelled: false };
|
|
82
|
+
pending.add(intent);
|
|
83
|
+
return () => {
|
|
84
|
+
intent.cancelled = true;
|
|
85
|
+
pending.delete(intent);
|
|
86
|
+
intent.live?.();
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function drainPendingSubscriptions() {
|
|
90
|
+
if (!coreReady()) return;
|
|
91
|
+
for (const intent of [...pending]) {
|
|
92
|
+
pending.delete(intent);
|
|
93
|
+
if (!intent.cancelled) intent.live = attach(intent.event, intent.listener);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function useGuideify() {
|
|
97
|
+
const context = useContext(GuideifyContext);
|
|
98
|
+
if (!context) {
|
|
99
|
+
throw new Error("useGuideify must be called inside a <GuideifyProvider>");
|
|
100
|
+
}
|
|
101
|
+
const { ready } = context;
|
|
102
|
+
return useMemo(
|
|
103
|
+
() => ({
|
|
104
|
+
start: (flowId) => void callCore("start", [flowId]),
|
|
105
|
+
stop: () => void callCore("stop", []),
|
|
106
|
+
track: (name, props) => void callCore("track", props === void 0 ? [name] : [name, props]),
|
|
107
|
+
identify: (userId, traits, opts) => void callCore("identify", [userId, traits ?? {}, opts]),
|
|
108
|
+
reset: () => void callCore("reset", []),
|
|
109
|
+
on: subscribe,
|
|
110
|
+
ready
|
|
111
|
+
}),
|
|
112
|
+
[ready]
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
function GuideifyProvider({ children, ...options }) {
|
|
116
|
+
const [ready, setReady] = useState(false);
|
|
117
|
+
const { apiKey, cdnHost, ingestHost, publicKey, debug, nonce } = options;
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (!apiKey) return;
|
|
120
|
+
const tag = installLoader({ apiKey, cdnHost, ingestHost, publicKey, debug, nonce });
|
|
121
|
+
return whenCoreReady(tag, () => {
|
|
122
|
+
drainPendingSubscriptions();
|
|
123
|
+
setReady(true);
|
|
124
|
+
});
|
|
125
|
+
}, [apiKey, cdnHost, ingestHost, publicKey, debug, nonce]);
|
|
126
|
+
const value = useMemo(() => ({ apiKey, ready }), [apiKey, ready]);
|
|
127
|
+
return /* @__PURE__ */ jsx(GuideifyContext.Provider, { value, children });
|
|
128
|
+
}
|
|
129
|
+
function GuideifyTarget({ id, children }) {
|
|
130
|
+
if (!isValidElement(children)) return children;
|
|
131
|
+
const child = children;
|
|
132
|
+
if (child.props["data-guideify-id"] !== void 0) return child;
|
|
133
|
+
return cloneElement(child, { "data-guideify-id": id });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export { DEFAULT_CDN, GuideifyProvider, GuideifyTarget, useGuideify };
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@guideify/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test": "node --test \"test/*.test.mjs\"",
|
|
24
|
+
"verify": "npm run typecheck && npm run build && npm run test",
|
|
25
|
+
"verify:package": "node ../../scripts/verify-package.mjs react",
|
|
26
|
+
"prepublishOnly": "npm run verify && npm run verify:package"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@guideify/js": "*",
|
|
33
|
+
"@guideify/snippet": "*",
|
|
34
|
+
"@types/react": "^19.2.0",
|
|
35
|
+
"jsdom": "^26.1.0",
|
|
36
|
+
"react": "^19.2.8",
|
|
37
|
+
"react-dom": "^19.2.8",
|
|
38
|
+
"tsup": "^8.5.0",
|
|
39
|
+
"typescript": "^5.7.3"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/vikasOzark/guidely.git",
|
|
47
|
+
"directory": "packages/react"
|
|
48
|
+
}
|
|
49
|
+
}
|