@rightkit/onboarding 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/Onboarding.d.ts +1 -1
- package/dist/Onboarding.js +32 -13
- package/dist/types.d.ts +2 -0
- package/onboarding.css +1 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Acceptance is stored as `legalAcceptance: { version, documentsHash, acceptedAt,
|
|
|
49
49
|
|
|
50
50
|
## When the legal gate re-prompts
|
|
51
51
|
|
|
52
|
-
`isLegalAccepted(state, requiredVersion?, isAccepted?, requiredDocumentsHash?)` decides it, in this order: a custom `isAccepted` wins outright; a `requiredDocumentsHash` must equal the stored acceptance's `documentsHash`; a `requiredVersion` must equal its `version`; with neither,
|
|
52
|
+
`isLegalAccepted(state, requiredVersion?, isAccepted?, requiredDocumentsHash?)` decides it, in this order: a custom `isAccepted` wins outright; a `requiredDocumentsHash` must equal the stored acceptance's `documentsHash`; a `requiredVersion` must equal its `version`; with neither, a recorded acceptance (`legalAcceptance`) is still required, so a bare legacy `legalAccepted` flag re-prompts once as a re-acceptance. Pass `requiredDocumentsHash` (the hash of the material documents, from `@rightkit/legal`) to catch a text change that did not bump the version: without it only the version is compared. `documentsHash` is recorded with the acceptance and never compared; by default it covers document titles and URLs, not their text.
|
|
53
53
|
|
|
54
54
|
## Known issues
|
|
55
55
|
|
package/dist/Onboarding.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ import type { OnboardingProps, PersistedOnboardingState } from "./types.js";
|
|
|
2
2
|
/** True when the stored acceptance satisfies the host's requirement. */
|
|
3
3
|
export declare function isLegalAccepted(state: PersistedOnboardingState, requiredVersion?: string, custom?: (state: PersistedOnboardingState) => boolean, requiredDocumentsHash?: string): boolean;
|
|
4
4
|
export declare function onboardingStateKey(): string;
|
|
5
|
-
export declare function Onboarding({ settings, legal, legalMode, appName, steps, permissions, models, license, onComplete, onSkip, className, requiredVersion, requiredDocumentsHash, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip, titlebar, dragRegion, legalDragRegion, legalReceipt, labels: labelOverrides }: OnboardingProps): import("react").JSX.Element | null;
|
|
5
|
+
export declare function Onboarding({ settings, legal, legalMode, appName, steps, permissions, models, license, onComplete, onSkip, className, layout, requiredVersion, requiredDocumentsHash, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip, titlebar, dragRegion, legalDragRegion, legalReceipt, labels: labelOverrides }: OnboardingProps): import("react").JSX.Element | null;
|
package/dist/Onboarding.js
CHANGED
|
@@ -7,11 +7,22 @@ import { licenseStep, modelsStep, permissionsStep } from "./steps.js";
|
|
|
7
7
|
const KEY = "onboarding.state";
|
|
8
8
|
const INITIAL = { version: 1, currentStep: 0, completed: [], skipped: false, legalAccepted: false };
|
|
9
9
|
const DEFAULT_LABELS = { skip: "Set up later", skipStep: "Skip", back: "Back", continue: "Continue", finish: "Finish setup", loading: "Loading setup…" };
|
|
10
|
+
/** Same canonical-UTC rule as `@rightkit/legal` receipts: `Date#toISOString()` form, round-tripping unchanged. */
|
|
11
|
+
function isCanonicalUtc(value) {
|
|
12
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value))
|
|
13
|
+
return false;
|
|
14
|
+
const parsed = Date.parse(value);
|
|
15
|
+
return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
|
|
16
|
+
}
|
|
10
17
|
function readAcceptance(value) {
|
|
11
|
-
if (!value || typeof value !== "object")
|
|
18
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12
19
|
return undefined;
|
|
13
20
|
const r = value;
|
|
14
|
-
if (typeof r.version !== "string" ||
|
|
21
|
+
if (typeof r.version !== "string" || !r.version.trim())
|
|
22
|
+
return undefined;
|
|
23
|
+
if (typeof r.documentsHash !== "string" || !r.documentsHash.trim())
|
|
24
|
+
return undefined;
|
|
25
|
+
if (!isCanonicalUtc(r.acceptedAt))
|
|
15
26
|
return undefined;
|
|
16
27
|
if (r.eligibilityBasis !== "individual" && r.eligibilityBasis !== "enterprise")
|
|
17
28
|
return undefined;
|
|
@@ -22,12 +33,15 @@ function readState(value) {
|
|
|
22
33
|
return INITIAL;
|
|
23
34
|
const candidate = value;
|
|
24
35
|
const legalAcceptance = readAcceptance(candidate.legalAcceptance);
|
|
36
|
+
// A present receipt is authoritative, including when it is malformed. Keep the
|
|
37
|
+
// legacy boolean only for state written before the receipt field existed.
|
|
38
|
+
const hasStoredAcceptance = Object.prototype.hasOwnProperty.call(candidate, "legalAcceptance");
|
|
25
39
|
return {
|
|
26
40
|
version: 1,
|
|
27
41
|
currentStep: typeof candidate.currentStep === "number" && candidate.currentStep >= 0 ? Math.floor(candidate.currentStep) : 0,
|
|
28
42
|
completed: Array.isArray(candidate.completed) ? candidate.completed.filter((x) => typeof x === "string") : [],
|
|
29
43
|
skipped: candidate.skipped === true,
|
|
30
|
-
legalAccepted: candidate.legalAccepted === true
|
|
44
|
+
legalAccepted: legalAcceptance !== undefined || (!hasStoredAcceptance && candidate.legalAccepted === true),
|
|
31
45
|
eligibilityBasis: legalAcceptance?.eligibilityBasis ?? (candidate.eligibilityBasis === "individual" || candidate.eligibilityBasis === "enterprise" ? candidate.eligibilityBasis : undefined),
|
|
32
46
|
...(legalAcceptance ? { legalAcceptance } : {}),
|
|
33
47
|
...(Array.isArray(candidate.skippedSteps) ? { skippedSteps: candidate.skippedSteps.filter((x) => typeof x === "string") } : {}),
|
|
@@ -44,15 +58,19 @@ function withReceipt(state, record) {
|
|
|
44
58
|
export function isLegalAccepted(state, requiredVersion, custom, requiredDocumentsHash) {
|
|
45
59
|
if (custom)
|
|
46
60
|
return custom(state);
|
|
61
|
+
// A stored record whose timestamp is not canonical UTC is not an acceptance,
|
|
62
|
+
// even when the host passes no version/hash requirement (same rule as legal receipts).
|
|
63
|
+
if (state.legalAcceptance !== undefined && !isCanonicalUtc(state.legalAcceptance.acceptedAt))
|
|
64
|
+
return false;
|
|
47
65
|
// A host that supplies the documents' hash is asking for the text itself to be what was accepted:
|
|
48
66
|
// a changed text re-prompts even when the version string did not move.
|
|
49
67
|
if (requiredDocumentsHash !== undefined && state.legalAcceptance?.documentsHash !== requiredDocumentsHash)
|
|
50
68
|
return false;
|
|
51
69
|
if (requiredVersion !== undefined)
|
|
52
70
|
return state.legalAcceptance?.version === requiredVersion;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return state.
|
|
71
|
+
// With no version or hash requirement a recorded acceptance is still required: a bare legacy
|
|
72
|
+
// `legalAccepted` flag carries no version, time or basis, so it re-prompts once (as re-acceptance).
|
|
73
|
+
return state.legalAcceptance !== undefined;
|
|
56
74
|
}
|
|
57
75
|
/** FNV-1a fallback when SubtleCrypto is unavailable. */
|
|
58
76
|
function fnv(text) {
|
|
@@ -86,7 +104,7 @@ function NoticeList({ documents }) {
|
|
|
86
104
|
return _jsx("p", { className: "rk-onboarding__muted", children: "No notices to show." });
|
|
87
105
|
return _jsx("ul", { className: "rk-onboarding__notices", children: documents.map((d) => _jsx("li", { children: d.onOpen ? _jsx("button", { type: "button", onClick: d.onOpen, children: d.title }) : d.url ? _jsx("a", { href: d.url, target: "_blank", rel: "noreferrer", children: d.title }) : d.title }, d.title)) });
|
|
88
106
|
}
|
|
89
|
-
export function Onboarding({ settings, legal, legalMode = "gate", appName, steps = [], permissions, models, license, onComplete, onSkip, className, requiredVersion, requiredDocumentsHash, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip = true, titlebar, dragRegion, legalDragRegion = false, legalReceipt, labels: labelOverrides }) {
|
|
107
|
+
export function Onboarding({ settings, legal, legalMode = "gate", appName, steps = [], permissions, models, license, onComplete, onSkip, className, layout = "window", requiredVersion, requiredDocumentsHash, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip = true, titlebar, dragRegion, legalDragRegion = false, legalReceipt, labels: labelOverrides }) {
|
|
90
108
|
if (legalMode === "gate" && !legal)
|
|
91
109
|
throw new Error("Onboarding: `legal` is required when legalMode is \"gate\"");
|
|
92
110
|
const L = { ...DEFAULT_LABELS, ...labelOverrides };
|
|
@@ -248,19 +266,20 @@ export function Onboarding({ settings, legal, legalMode = "gate", appName, steps
|
|
|
248
266
|
}
|
|
249
267
|
await markAndMove(index + 1, { completed: active.id });
|
|
250
268
|
};
|
|
269
|
+
const rootClass = cx("rk-onboarding", layout === "contained" && "rk-onboarding--contained", className);
|
|
251
270
|
if (error && !state)
|
|
252
|
-
return _jsxs("div", { className:
|
|
271
|
+
return _jsxs("div", { className: rootClass, children: [_jsx("p", { className: "rk-onboarding__error", role: "alert", children: error }), _jsx(Button, { onClick: () => window.location.reload(), children: "Retry" })] });
|
|
253
272
|
if (state && !active)
|
|
254
|
-
return _jsxs("main", { className:
|
|
273
|
+
return _jsxs("main", { className: rootClass, "data-testid": "onboarding", children: [_jsx("p", { className: "rk-onboarding__muted", children: "Nothing to set up." }), _jsx(Button, { variant: "primary", onClick: () => void runCallback(() => onComplete(resultOf(activeState.completed, activeState.skippedSteps ?? []))), children: L.finish })] });
|
|
255
274
|
if (!state || !active)
|
|
256
|
-
return _jsx("div", { className:
|
|
275
|
+
return _jsx("div", { className: rootClass, "aria-busy": "true", children: _jsx("p", { className: "rk-onboarding__muted", children: L.loading }) });
|
|
257
276
|
if (needsGate) {
|
|
258
277
|
if (!gateCleared)
|
|
259
|
-
return _jsxs("div", { className:
|
|
278
|
+
return _jsxs("div", { className: rootClass, "aria-busy": "true", children: [_jsx(TitleStrip, { titlebar: titlebar, dragRegion: legalDragRegion }), _jsx("p", { className: "rk-onboarding__muted", children: L.loading })] });
|
|
260
279
|
if (!legal)
|
|
261
280
|
return null;
|
|
262
281
|
const reacceptance = activeState.legalAccepted || activeState.legalAcceptance !== undefined;
|
|
263
|
-
return _jsxs("div", { className: cx("rk-onboarding rk-onboarding--legal", className), "data-testid": "onboarding-legal", "data-variant": reacceptance ? "re-acceptance" : "first-run", children: [_jsx(TitleStrip, { titlebar: titlebar, dragRegion: legalDragRegion }), _jsx(LegalGate, { ...legal, variant: reacceptance ? "re-acceptance" : "first-run", onQuit: () => void legal.onQuit?.(), onAgree: async ({ eligibilityBasis }) => {
|
|
282
|
+
return _jsxs("div", { className: cx("rk-onboarding rk-onboarding--legal", layout === "contained" && "rk-onboarding--contained", className), "data-testid": "onboarding-legal", "data-variant": reacceptance ? "re-acceptance" : "first-run", children: [_jsx(TitleStrip, { titlebar: titlebar, dragRegion: legalDragRegion }), _jsx(LegalGate, { ...legal, layout: layout, variant: reacceptance ? "re-acceptance" : "first-run", onQuit: () => void legal.onQuit?.(), onAgree: async ({ eligibilityBasis }) => {
|
|
264
283
|
const legalAcceptance = { version: requiredVersion ?? "1", documentsHash: documentsHash ?? requiredDocumentsHash ?? await hashDocuments(legal.documents), acceptedAt: new Date().toISOString(), eligibilityBasis };
|
|
265
284
|
if (legalReceipt) {
|
|
266
285
|
try {
|
|
@@ -278,6 +297,6 @@ export function Onboarding({ settings, legal, legalMode = "gate", appName, steps
|
|
|
278
297
|
} })] });
|
|
279
298
|
}
|
|
280
299
|
const body = active.render(context);
|
|
281
|
-
return _jsxs("main", { className:
|
|
300
|
+
return _jsxs("main", { className: rootClass, "data-testid": "onboarding", "data-step-id": active.id, children: [_jsx(TitleStrip, { titlebar: titlebar, dragRegion: dragRegion }), _jsxs("aside", { className: "rk-onboarding__rail", children: [_jsx("div", { className: "rk-onboarding__brand", children: appName ?? legal?.appName }), _jsx("nav", { "aria-label": "Setup progress", children: builtins.map((step, i) => _jsxs("button", { type: "button", className: cx("rk-onboarding__step", i === index && "is-active", activeState.completed.includes(step.id) && "is-done"), "aria-current": i === index ? "step" : undefined, disabled: (gated && i === 0 && builtins.length > 1) || continuing, onClick: () => { if (!continuing && i >= lead && (i <= index || canContinue))
|
|
282
301
|
void save({ currentStep: i }); }, children: [_jsx("span", { children: activeState.completed.includes(step.id) ? "✓" : i + 1 }), step.label] }, step.id)) })] }), _jsxs("section", { className: "rk-onboarding__main", children: [_jsxs("header", { children: [_jsxs("p", { className: "rk-onboarding__eyebrow", children: ["Step ", index + 1, " of ", builtins.length] }), active.title ? _jsx("h1", { children: active.title }) : null, active.description ? _jsx("p", { className: "rk-onboarding__description", children: active.description }) : null] }), _jsx("div", { className: "rk-onboarding__content", children: body }), error ? _jsx("p", { className: "rk-onboarding__error", role: "alert", children: error }) : null, _jsxs("footer", { children: [allowSkip ? _jsx(Button, { variant: "ghost", disabled: saving || continuing, onClick: () => { void save({ currentStep: index, skipped: true }).then((ok) => ok ? runCallback(onSkip) : undefined); }, children: L.skip }) : null, active.skippable ? _jsx(Button, { variant: "ghost", disabled: saving || continuing, onClick: () => void markAndMove(index + 1, { skipped: active.id }), children: L.skipStep }) : null, _jsx("span", { className: "rk-onboarding__hint", children: !canContinue && active.blockedHint ? active.blockedHint : null }), _jsx(Button, { variant: "secondary", disabled: index <= lead || saving || continuing, onClick: () => void save({ currentStep: index - 1 }), children: L.back }), _jsx(Button, { variant: "primary", disabled: saving || continuing || !canContinue, onClick: () => void advance(), children: active.continueLabel ?? (index === builtins.length - 1 ? L.finish : L.continue) })] })] })] });
|
|
283
302
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -178,6 +178,8 @@ export interface LegalStepConfig extends Omit<LegalGateProps, "variant" | "onAgr
|
|
|
178
178
|
*/
|
|
179
179
|
export type LegalMode = "gate" | "notice" | "none";
|
|
180
180
|
export interface OnboardingProps {
|
|
181
|
+
/** Window-sized by default; use `contained` when the host sizes the setup surface. */
|
|
182
|
+
layout?: "window" | "contained";
|
|
181
183
|
settings: SettingsBackend;
|
|
182
184
|
/** Required when `legalMode` is `gate` (default). Optional for `notice` (the documents shown) and `none`. */
|
|
183
185
|
legal?: LegalStepConfig;
|
package/onboarding.css
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* @rightkit/onboarding — uses app-shell tokens and legal-ui's locked light gate. */
|
|
2
2
|
.rk-onboarding { position: relative; min-height: 100vh; display: grid; grid-template-columns: 220px minmax(0, 1fr); background: var(--rk-plane, #fffcf5); color: var(--rk-ink, #231f1a); font: 13px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; }
|
|
3
|
+
.rk-onboarding--contained { min-height: 0; }
|
|
3
4
|
.rk-onboarding *, .rk-onboarding *::before, .rk-onboarding *::after { box-sizing: border-box; }
|
|
4
5
|
.rk-onboarding__rail { display: flex; flex-direction: column; gap: 22px; padding: 28px 14px; background: var(--rk-chrome, #eee9df); border-right: .5px solid var(--rk-hairline, rgba(35,31,26,.11)); }
|
|
5
6
|
.rk-onboarding__brand { padding: 0 10px; font-size: 19px; font-weight: 650; letter-spacing: -.02em; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/onboarding",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "First-run onboarding framework: ordered steps, progress, skip/resume, legal gate first, permissions, model download and licence/trial steps.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
],
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"@rightkit/react-shell": ">=0.2.0 <1",
|
|
28
|
-
"@rightkit/legal-ui": ">=0.1
|
|
28
|
+
"@rightkit/legal-ui": ">=0.4.1 <1",
|
|
29
29
|
"react": ">=18 <21"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@rightkit/react-shell": "0.7.
|
|
33
|
-
"@rightkit/legal-ui": "0.4.
|
|
32
|
+
"@rightkit/react-shell": "0.7.1",
|
|
33
|
+
"@rightkit/legal-ui": "0.4.1",
|
|
34
34
|
"@rightkit/qa-chrome": "0.1.8",
|
|
35
35
|
"@testing-library/jest-dom": "^7.0.1",
|
|
36
36
|
"@testing-library/dom": "^10.4.2",
|