@rightkit/onboarding 0.2.1 → 0.2.3

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 CHANGED
@@ -24,6 +24,8 @@ import "@rightkit/onboarding/onboarding.css";
24
24
  | `beforeLegalGate()` | Awaited before the gate shows (e.g. size or reveal the window). The package does no window sizing itself. |
25
25
  | `requireModel` | Continue/Finish on the models step wait for an installed model. |
26
26
  | `autoStartDownload` | Start the first missing model download when the models step opens. |
27
+ | `shouldAutoStart(entry, models)` | Choose which missing model auto-starts when the models step first opens (first `missing` entry returning true). Replaces `autoStartDownload`'s rule, including its "a model is already installed" guard. Once per mount, across re-renders and Back/Next. |
28
+ | `onModelsStepEnter(ctx)` | Full control, called once when the step opens and the list has loaded: `ctx.models`, `ctx.start(id)`, `ctx.refresh()`. Replaces `shouldAutoStart` and `autoStartDownload`; a throw is shown in the step. |
27
29
  | `modelsIntro` | App copy shown at the top of the models step. |
28
30
  | `allowSkip` | Show "Set up later" (default `true`). |
29
31
  | `titlebar` | Node rendered in a strip along the top. |
@@ -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): boolean;
4
4
  export declare function onboardingStateKey(): string;
5
- export declare function Onboarding({ settings, legal, steps, permissions, models, license, onComplete, onSkip, className, requiredVersion, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, modelsIntro, allowSkip, titlebar, dragRegion, legalDragRegion, legalReceipt }: OnboardingProps): import("react").JSX.Element;
5
+ export declare function Onboarding({ settings, legal, steps, permissions, models, license, onComplete, onSkip, className, requiredVersion, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip, titlebar, dragRegion, legalDragRegion, legalReceipt }: OnboardingProps): import("react").JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { LegalGate } from "@rightkit/legal-ui";
4
4
  import { Button } from "@rightkit/app-shell/react";
5
5
  import { cx } from "./ui.js";
@@ -73,12 +73,14 @@ function TitleStrip({ titlebar, dragRegion }) {
73
73
  return null;
74
74
  return _jsx("div", { className: "rk-onboarding__titlebar", "data-testid": "onboarding-titlebar", ...(dragRegion ? { "data-tauri-drag-region": "" } : {}), children: titlebar });
75
75
  }
76
- export function Onboarding({ settings, legal, steps = [], permissions, models, license, onComplete, onSkip, className, requiredVersion, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, modelsIntro, allowSkip = true, titlebar, dragRegion, legalDragRegion = false, legalReceipt }) {
76
+ export function Onboarding({ settings, legal, steps = [], permissions, models, license, onComplete, onSkip, className, requiredVersion, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip = true, titlebar, dragRegion, legalDragRegion = false, legalReceipt }) {
77
77
  const [state, setState] = useState(null);
78
78
  const [saving, setSaving] = useState(false);
79
79
  const [error, setError] = useState(null);
80
80
  const [readyMap, setReadyMap] = useState({});
81
81
  const [gateCleared, setGateCleared] = useState(false);
82
+ /** One auto-start per mount, even when the models step unmounts (Back/Next) or the step list is rebuilt. */
83
+ const modelsStarted = useRef(false);
82
84
  /** Persists a patch; throws when the settings backend rejects it. */
83
85
  const persist = useCallback(async (patch) => {
84
86
  setSaving(true);
@@ -138,10 +140,10 @@ export function Onboarding({ settings, legal, steps = [], permissions, models, l
138
140
  const builtins = useMemo(() => [
139
141
  { id: "legal", label: "Agreement", title: "Review agreement", skippable: false, render: () => _jsx("p", { className: "rk-onboarding__muted", children: "Agreement accepted." }) },
140
142
  ...(permissions ? [permissionsStep(permissions)] : []),
141
- ...(models ? [modelsStep(models, { required: requireModel, autoStartDownload, intro: modelsIntro })] : []),
143
+ ...(models ? [modelsStep(models, { required: requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, intro: modelsIntro, startGuard: modelsStarted })] : []),
142
144
  ...(license ? [licenseStep(license)] : []),
143
145
  ...steps,
144
- ], [license, models, permissions, steps, requireModel, autoStartDownload, modelsIntro]);
146
+ ], [license, models, permissions, steps, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro]);
145
147
  const activeState = state ?? INITIAL;
146
148
  const accepted = isLegalAccepted(activeState, requiredVersion, isAccepted);
147
149
  const needsGate = state !== null && !accepted;
package/dist/steps.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { type ReactNode } from "react";
2
- import type { LicenseBackend, ModelsBackend, OnboardingStep, PermissionsBackend } from "./types.js";
2
+ import type { LicenseBackend, ModelsAutoStart, ModelsBackend, OnboardingStep, PermissionsBackend } from "./types.js";
3
3
  export declare function permissionsStep(backend: PermissionsBackend, options?: {
4
4
  label?: string;
5
5
  title?: string;
6
6
  description?: string;
7
7
  }): OnboardingStep;
8
- export interface ModelsStepOptions {
8
+ export interface ModelsStepOptions extends ModelsAutoStart {
9
9
  label?: string;
10
10
  title?: string;
11
11
  description?: string;
@@ -15,6 +15,10 @@ export interface ModelsStepOptions {
15
15
  autoStartDownload?: boolean;
16
16
  /** App copy rendered at the top of the step. */
17
17
  intro?: ReactNode;
18
+ /** Shared "already auto-started" flag. `Onboarding` supplies one that survives Back/Next and re-renders; omit it and the guard lives for the step's mount only. */
19
+ startGuard?: {
20
+ current: boolean;
21
+ };
18
22
  }
19
23
  export declare function modelsStep(backend: ModelsBackend, options?: ModelsStepOptions): OnboardingStep;
20
24
  export declare function licenseStep(backend: LicenseBackend, options?: {
package/dist/steps.js CHANGED
@@ -29,12 +29,13 @@ function PermissionsBody({ backend }) {
29
29
  export function permissionsStep(backend, options = {}) {
30
30
  return { id: "permissions", label: options.label ?? "Permissions", title: options.title ?? "Allow what this app needs", description: options.description ?? "You can change these permissions later in System Settings.", skippable: true, render: () => _jsx(PermissionsBody, { backend: backend }) };
31
31
  }
32
- function ModelsBody({ backend, onReady, autoStartDownload, intro }) {
32
+ function ModelsBody({ backend, onReady, autoStartDownload, shouldAutoStart, onModelsStepEnter, intro, startGuard }) {
33
33
  const [models, setModels] = useState([]);
34
34
  const [loaded, setLoaded] = useState(false);
35
35
  const [busy, setBusy] = useState(null);
36
36
  const [error, setError] = useState(null);
37
- const autoStarted = useRef(false);
37
+ const ownGuard = useRef(false);
38
+ const autoStarted = startGuard ?? ownGuard;
38
39
  const reload = useCallback(() => backend.list().then((list) => { setModels(list); setLoaded(true); }, (e) => setError(e instanceof Error ? e.message : String(e))), [backend]);
39
40
  useEffect(() => {
40
41
  void reload();
@@ -56,9 +57,23 @@ function ModelsBody({ backend, onReady, autoStartDownload, intro }) {
56
57
  void request.then(reload, (e) => setError(e instanceof Error ? e.message : String(e))).finally(() => setBusy(null));
57
58
  };
58
59
  useEffect(() => {
59
- if (!autoStartDownload || !loaded || autoStarted.current)
60
+ if (!loaded || autoStarted.current)
61
+ return;
62
+ if (!onModelsStepEnter && !shouldAutoStart && !autoStartDownload)
60
63
  return;
61
64
  autoStarted.current = true;
65
+ if (onModelsStepEnter) {
66
+ // The app owns which model starts and when; the hook runs once per Onboarding mount.
67
+ const ctx = { models, start: (id) => backend.install(id).then(reload), refresh: () => backend.list().then((list) => { setModels(list); return list; }) };
68
+ void Promise.resolve().then(() => onModelsStepEnter(ctx)).catch((e) => setError(e instanceof Error ? e.message : String(e)));
69
+ return;
70
+ }
71
+ if (shouldAutoStart) {
72
+ const pick = models.find((m) => m.state === "missing" && shouldAutoStart(m, models));
73
+ if (pick)
74
+ run(pick.id, backend.install(pick.id));
75
+ return;
76
+ }
62
77
  if (models.some((m) => m.state === "installed" || m.state === "downloading" || m.state === "verifying"))
63
78
  return;
64
79
  const first = models.find((m) => m.state === "missing");
@@ -66,7 +81,7 @@ function ModelsBody({ backend, onReady, autoStartDownload, intro }) {
66
81
  run(first.id, backend.install(first.id));
67
82
  // run is intentionally omitted: it only closes over setters.
68
83
  // eslint-disable-next-line react-hooks/exhaustive-deps
69
- }, [autoStartDownload, loaded, models, backend]);
84
+ }, [autoStartDownload, shouldAutoStart, onModelsStepEnter, loaded, models, backend]);
70
85
  return (_jsxs("div", { className: "rk-onboarding__card", "data-testid": "onboarding-models", children: [intro ? _jsx("div", { className: "rk-onboarding__intro", "data-testid": "onboarding-models-intro", children: intro }) : null, error ? _jsx("p", { className: "rk-onboarding__error", role: "alert", children: error }) : null, models.map((model) => {
71
86
  const progress = model.progress ?? (model.state === "installed" ? 1 : 0);
72
87
  const active = model.state === "downloading" || model.state === "verifying";
@@ -82,7 +97,7 @@ export function modelsStep(backend, options = {}) {
82
97
  id: "models", label: options.label ?? "Models", title: options.title ?? "Choose local models", description: options.description ?? "Downloads are verified before a model is activated.", skippable: true,
83
98
  canContinue: options.required ? (ctx) => ctx.ready.models === true : undefined,
84
99
  blockedHint: options.required ? "Download a model to continue." : undefined,
85
- render: (ctx) => _jsx(ModelsBody, { backend: backend, onReady: ctx.setReady, autoStartDownload: options.autoStartDownload, intro: options.intro }),
100
+ render: (ctx) => _jsx(ModelsBody, { backend: backend, onReady: ctx.setReady, autoStartDownload: options.autoStartDownload, shouldAutoStart: options.shouldAutoStart, onModelsStepEnter: options.onModelsStepEnter, intro: options.intro, startGuard: options.startGuard }),
86
101
  };
87
102
  }
88
103
  function LicenseBody({ backend }) {
package/dist/types.d.ts CHANGED
@@ -135,8 +135,12 @@ export interface OnboardingProps {
135
135
  beforeLegalGate?: () => void | Promise<void>;
136
136
  /** Require at least one downloaded model before Continue/Finish on the models step. Default false. */
137
137
  requireModel?: boolean;
138
- /** Start downloading the first missing model when the models step opens. Default false. */
138
+ /** Start downloading the first missing model when the models step opens (skipped when any model is installed or downloading). Default false. */
139
139
  autoStartDownload?: boolean;
140
+ /** Choose which missing model starts when the models step first opens: the first `state === "missing"` entry for which this returns true. Replaces the `autoStartDownload` rule (no "something is installed" guard; the app decides). Fires at most once per `Onboarding` mount, across re-renders and Back/Next. */
141
+ shouldAutoStart?: (entry: ModelEntry, models: readonly ModelEntry[]) => boolean;
142
+ /** Full control: called once, when the models step first opens and the list has loaded. Replaces `shouldAutoStart` and `autoStartDownload`. Start models through `ctx.start`. Fires at most once per `Onboarding` mount; a throw is shown in the step. */
143
+ onModelsStepEnter?: (ctx: ModelsStepEnterContext) => void | Promise<void>;
140
144
  /** App copy shown at the top of the models step. */
141
145
  modelsIntro?: ReactNode;
142
146
  /** Show "Set up later". Default true. */
@@ -151,3 +155,18 @@ export interface OnboardingProps {
151
155
  legalReceipt?: LegalReceiptPort;
152
156
  }
153
157
  export type { LegalGateDocument, LegalGateTheme };
158
+ /** Context handed to `onModelsStepEnter`. */
159
+ export interface ModelsStepEnterContext {
160
+ /** The model list as loaded when the step opened. */
161
+ models: readonly ModelEntry[];
162
+ /** Start installing one model, then refresh the list. */
163
+ start(id: string): Promise<void>;
164
+ /** Re-read the list from the backend (and update the step). */
165
+ refresh(): Promise<ModelEntry[]>;
166
+ }
167
+ /** The auto-start options shared by `Onboarding` and `modelsStep`. */
168
+ export interface ModelsAutoStart {
169
+ autoStartDownload?: boolean;
170
+ shouldAutoStart?: (entry: ModelEntry, models: readonly ModelEntry[]) => boolean;
171
+ onModelsStepEnter?: (ctx: ModelsStepEnterContext) => void | Promise<void>;
172
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/onboarding",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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",
@@ -29,8 +29,9 @@
29
29
  "react": ">=18 <20"
30
30
  },
31
31
  "devDependencies": {
32
- "@rightkit/app-shell": "0.2.0",
32
+ "@rightkit/app-shell": "0.2.1",
33
33
  "@rightkit/legal-ui": "0.1.3",
34
+ "@rightkit/qa": "0.2.2",
34
35
  "@testing-library/jest-dom": "^6.6.3",
35
36
  "@testing-library/react": "^16.1.0",
36
37
  "@testing-library/user-event": "^14.5.2",