@shipfox/client-onboarding 23.0.0 → 25.0.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.
Files changed (91) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +28 -0
  3. package/README.md +26 -7
  4. package/dist/components/setup-checklist-body.d.ts +3 -0
  5. package/dist/components/setup-checklist-body.d.ts.map +1 -0
  6. package/dist/components/setup-checklist-body.js +25 -0
  7. package/dist/components/setup-checklist-body.js.map +1 -0
  8. package/dist/components/setup-checklist-completion.d.ts +6 -0
  9. package/dist/components/setup-checklist-completion.d.ts.map +1 -0
  10. package/dist/components/setup-checklist-completion.js +156 -0
  11. package/dist/components/setup-checklist-completion.js.map +1 -0
  12. package/dist/components/setup-checklist-confetti.d.ts +13 -0
  13. package/dist/components/setup-checklist-confetti.d.ts.map +1 -0
  14. package/dist/components/setup-checklist-confetti.js +24 -0
  15. package/dist/components/setup-checklist-confetti.js.map +1 -0
  16. package/dist/components/setup-checklist-host-primitives.d.ts +11 -0
  17. package/dist/components/setup-checklist-host-primitives.d.ts.map +1 -0
  18. package/dist/components/setup-checklist-host-primitives.js +71 -0
  19. package/dist/components/setup-checklist-host-primitives.js.map +1 -0
  20. package/dist/components/setup-checklist-row.d.ts +7 -0
  21. package/dist/components/setup-checklist-row.d.ts.map +1 -0
  22. package/dist/components/setup-checklist-row.js +177 -0
  23. package/dist/components/setup-checklist-row.js.map +1 -0
  24. package/dist/components/setup-checklist-types.d.ts +19 -0
  25. package/dist/components/setup-checklist-types.d.ts.map +1 -0
  26. package/dist/components/setup-checklist-types.js +3 -0
  27. package/dist/components/setup-checklist-types.js.map +1 -0
  28. package/dist/components/setup-checklist.d.ts +5 -0
  29. package/dist/components/setup-checklist.d.ts.map +1 -0
  30. package/dist/components/setup-checklist.js +5 -0
  31. package/dist/components/setup-checklist.js.map +1 -0
  32. package/dist/components/setup-checklist.stories.d.ts +19 -0
  33. package/dist/components/setup-checklist.stories.d.ts.map +1 -0
  34. package/dist/components/setup-checklist.stories.js +305 -0
  35. package/dist/components/setup-checklist.stories.js.map +1 -0
  36. package/dist/components/workspace-setup-checklist.d.ts +3 -0
  37. package/dist/components/workspace-setup-checklist.d.ts.map +1 -0
  38. package/dist/components/workspace-setup-checklist.js +80 -0
  39. package/dist/components/workspace-setup-checklist.js.map +1 -0
  40. package/dist/components/workspace-setup-indicator.d.ts +3 -0
  41. package/dist/components/workspace-setup-indicator.d.ts.map +1 -0
  42. package/dist/components/workspace-setup-indicator.js +133 -0
  43. package/dist/components/workspace-setup-indicator.js.map +1 -0
  44. package/dist/core/setup-checklist.d.ts +2 -0
  45. package/dist/core/setup-checklist.d.ts.map +1 -1
  46. package/dist/core/setup-checklist.js +11 -6
  47. package/dist/core/setup-checklist.js.map +1 -1
  48. package/dist/feature.d.ts +6 -0
  49. package/dist/feature.d.ts.map +1 -0
  50. package/dist/feature.js +19 -0
  51. package/dist/feature.js.map +1 -0
  52. package/dist/hooks/api/setup-checklist.d.ts +13 -0
  53. package/dist/hooks/api/setup-checklist.d.ts.map +1 -0
  54. package/dist/hooks/api/setup-checklist.js +103 -0
  55. package/dist/hooks/api/setup-checklist.js.map +1 -0
  56. package/dist/hooks/use-checklist-analytics.d.ts +6 -0
  57. package/dist/hooks/use-checklist-analytics.d.ts.map +1 -0
  58. package/dist/hooks/use-checklist-analytics.js +51 -0
  59. package/dist/hooks/use-checklist-analytics.js.map +1 -0
  60. package/dist/hooks/use-checklist-dismissal.d.ts +5 -0
  61. package/dist/hooks/use-checklist-dismissal.d.ts.map +1 -0
  62. package/dist/hooks/use-checklist-dismissal.js +31 -0
  63. package/dist/hooks/use-checklist-dismissal.js.map +1 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +1 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/tsconfig.test.tsbuildinfo +1 -1
  69. package/package.json +12 -5
  70. package/src/components/setup-checklist-body.tsx +35 -0
  71. package/src/components/setup-checklist-completion.test.tsx +124 -0
  72. package/src/components/setup-checklist-completion.tsx +165 -0
  73. package/src/components/setup-checklist-confetti.test.ts +19 -0
  74. package/src/components/setup-checklist-confetti.ts +39 -0
  75. package/src/components/setup-checklist-host-primitives.tsx +65 -0
  76. package/src/components/setup-checklist-row.tsx +189 -0
  77. package/src/components/setup-checklist-types.ts +21 -0
  78. package/src/components/setup-checklist.stories.tsx +281 -0
  79. package/src/components/setup-checklist.test.tsx +564 -0
  80. package/src/components/setup-checklist.tsx +8 -0
  81. package/src/components/workspace-setup-checklist.tsx +91 -0
  82. package/src/components/workspace-setup-indicator.tsx +126 -0
  83. package/src/core/setup-checklist.test.ts +2 -2
  84. package/src/core/setup-checklist.ts +19 -10
  85. package/src/feature.ts +16 -0
  86. package/src/hooks/api/setup-checklist.ts +144 -0
  87. package/src/hooks/use-checklist-analytics.ts +60 -0
  88. package/src/hooks/use-checklist-dismissal.ts +30 -0
  89. package/src/index.ts +8 -0
  90. package/tsconfig.build.tsbuildinfo +1 -1
  91. package/vitest.config.ts +2 -0
@@ -0,0 +1,126 @@
1
+ import {useClientAnalytics, useMaybeActiveWorkspace} from '@shipfox/client-shell/runtime';
2
+ import {Button} from '@shipfox/react-ui/button';
3
+ import {Icon} from '@shipfox/react-ui/icon';
4
+ import {Panel, PanelBody} from '@shipfox/react-ui/panel';
5
+ import {Popover, PopoverContent, PopoverTrigger} from '@shipfox/react-ui/popover';
6
+ import {cn} from '@shipfox/react-ui/utils';
7
+ import {useCallback, useId, useState} from 'react';
8
+ import type {SetupChecklistItem} from '#core/setup-checklist.js';
9
+ import {useSetupChecklistQueryState} from '#hooks/api/setup-checklist.js';
10
+ import {useCompletionTransition, useShownAnalytics} from '#hooks/use-checklist-analytics.js';
11
+ import {useChecklistDismissal} from '#hooks/use-checklist-dismissal.js';
12
+ import {SetupChecklistBody} from './setup-checklist-body.js';
13
+ import {ChecklistDismissAction, checklistCountLabel} from './setup-checklist-host-primitives.js';
14
+ import type {WorkspaceReference, WorkspaceSetupHostProps} from './setup-checklist-types.js';
15
+
16
+ export function WorkspaceSetupIndicator(props: WorkspaceSetupHostProps = {}) {
17
+ if (props.workspace) {
18
+ return (
19
+ <WorkspaceSetupIndicatorForWorkspace key={props.workspace.id} workspace={props.workspace} />
20
+ );
21
+ }
22
+
23
+ return <WorkspaceSetupIndicatorFromShell />;
24
+ }
25
+
26
+ function WorkspaceSetupIndicatorFromShell() {
27
+ const workspace = useMaybeActiveWorkspace();
28
+ return workspace ? (
29
+ <WorkspaceSetupIndicatorForWorkspace key={workspace.id} workspace={workspace} />
30
+ ) : null;
31
+ }
32
+
33
+ function WorkspaceSetupIndicatorForWorkspace({workspace}: {workspace: WorkspaceReference}) {
34
+ const dismissal = useChecklistDismissal(workspace.id);
35
+ const queryState = useSetupChecklistQueryState(workspace.id, !dismissal.dismissed);
36
+ const analytics = useClientAnalytics();
37
+ const [open, setOpen] = useState(false);
38
+ const [pulseKey, setPulseKey] = useState(0);
39
+ const [burstPending, setBurstPending] = useState(false);
40
+ const handleCompleted = useCallback(
41
+ (completed: boolean) => {
42
+ if (!completed) return;
43
+ setBurstPending(true);
44
+ if (!open) setPulseKey((key) => key + 1);
45
+ },
46
+ [open],
47
+ );
48
+ const showCompletion = useCompletionTransition(queryState, 'popover', handleCompleted);
49
+ const consumeBurst = useCallback(() => setBurstPending(false), []);
50
+ const triggerId = useId();
51
+
52
+ const dismiss = useCallback(() => {
53
+ dismissal.dismiss();
54
+ setOpen(false);
55
+ analytics.capture('onboarding_checklist_dismissed', {host: 'popover'});
56
+ }, [analytics, dismissal]);
57
+ const handleAction = useCallback(
58
+ (item: SetupChecklistItem) => {
59
+ analytics.capture('onboarding_checklist_row_clicked', {row_id: item.id});
60
+ setOpen(false);
61
+ },
62
+ [analytics],
63
+ );
64
+
65
+ const indicatorReady = queryState.completionReady || queryState.checklist.openCount > 0;
66
+ const isVisible =
67
+ !dismissal.dismissed &&
68
+ queryState.baseSettled &&
69
+ indicatorReady &&
70
+ (!queryState.checklist.complete || showCompletion);
71
+ useShownAnalytics('popover', isVisible);
72
+
73
+ if (dismissal.dismissed || !queryState.baseSettled || !indicatorReady) return null;
74
+ if (queryState.checklist.complete && !showCompletion) return null;
75
+
76
+ const countLabel = checklistCountLabel(queryState.checklist);
77
+ const ariaLabel = `Get started, ${countLabel}`;
78
+
79
+ return (
80
+ <Popover modal={false} open={open} onOpenChange={setOpen}>
81
+ <PopoverTrigger asChild>
82
+ <Button
83
+ type="button"
84
+ variant="transparent"
85
+ size="sm"
86
+ id={triggerId}
87
+ aria-label={ariaLabel}
88
+ className="gap-inline"
89
+ >
90
+ <span
91
+ key={pulseKey}
92
+ className={cn(
93
+ 'inline-flex items-center',
94
+ pulseKey > 0 && 'motion-safe:animate-[pulse_1s_ease-in-out_1]',
95
+ )}
96
+ >
97
+ <Icon name="circleDottedLine" size={16} aria-hidden="true" />
98
+ </span>
99
+ <span>Get started · {countLabel}</span>
100
+ </Button>
101
+ </PopoverTrigger>
102
+ <PopoverContent
103
+ align="end"
104
+ className="w-[360px] max-w-[calc(100vw-32px)] p-0"
105
+ aria-labelledby={triggerId}
106
+ >
107
+ <Panel asChild>
108
+ <section aria-label="Get started">
109
+ <PanelBody>
110
+ <SetupChecklistBody
111
+ checklist={queryState.checklist}
112
+ workspaceSlug={workspace.slug}
113
+ completion={showCompletion}
114
+ showBurst={burstPending}
115
+ onBurstComplete={consumeBurst}
116
+ onAction={handleAction}
117
+ onDone={dismiss}
118
+ />
119
+ </PanelBody>
120
+ <ChecklistDismissAction onDismiss={dismiss} />
121
+ </section>
122
+ </Panel>
123
+ </PopoverContent>
124
+ </Popover>
125
+ );
126
+ }
@@ -277,6 +277,7 @@ describe('deriveSetupChecklist', () => {
277
277
  expect(checklist.items.find((item) => item.id === 'tools')).toMatchObject({
278
278
  status: 'done',
279
279
  title: 'Connect your tools',
280
+ attention: false,
280
281
  });
281
282
  });
282
283
 
@@ -284,8 +285,7 @@ describe('deriveSetupChecklist', () => {
284
285
  const checklist = deriveSetupChecklist(input());
285
286
 
286
287
  expect(checklist.items.find((item) => item.id === 'tools')).toMatchObject({
287
- purpose:
288
- 'Connect issue tracking, messaging, observability, or any Shipfox integration so workflows can react to events and agents can use them',
288
+ purpose: 'Connect issue tracking, messaging, observability, or any Shipfox integration',
289
289
  action: {href: '/settings/integrations'},
290
290
  });
291
291
  });
@@ -22,6 +22,8 @@ export interface SetupChecklistItem {
22
22
  status: SetupChecklistItemStatus;
23
23
  /** Tracked items count toward completion; pointers never do. */
24
24
  tracked: boolean;
25
+ /** The row's underlying integration has a connection that needs attention. */
26
+ attention?: boolean;
25
27
  /** One line of purpose, rendered for open and pointer rows. */
26
28
  purpose?: string;
27
29
  /** Primary action, a link to the settings page where the work happens. */
@@ -49,7 +51,7 @@ export interface SetupChecklistInput {
49
51
 
50
52
  const TOOLS_TITLE = 'Connect your tools';
51
53
  const TOOLS_PURPOSE =
52
- 'Connect issue tracking, messaging, observability, or any Shipfox integration so workflows can react to events and agents can use them';
54
+ 'Connect issue tracking, messaging, observability, or any Shipfox integration';
53
55
  const RUNNER_PURPOSE = 'Jobs wait in `pending` until a runner is online';
54
56
  const MODEL_PROVIDER_PURPOSE = 'Agent steps need a model provider to run';
55
57
  const FIRST_WORKFLOW_PURPOSE =
@@ -69,6 +71,8 @@ export function deriveSetupChecklist({
69
71
  modelProvider,
70
72
  membership,
71
73
  }: SetupChecklistInput): SetupChecklist {
74
+ const toolsAttention =
75
+ !readiness.hasToolIntegration && attentionToolProviders(readiness).length > 0;
72
76
  const items: SetupChecklistItem[] = [
73
77
  {id: 'source-control', title: 'Connect source control', status: 'done', tracked: true},
74
78
  {id: 'project', title: 'Create a project', status: 'done', tracked: true},
@@ -77,6 +81,7 @@ export function deriveSetupChecklist({
77
81
  title: toolsTitle(readiness),
78
82
  status: readiness.hasToolIntegration ? 'done' : 'open',
79
83
  tracked: true,
84
+ attention: toolsAttention,
80
85
  purpose: TOOLS_PURPOSE,
81
86
  action: {label: 'Connect', href: '/settings/integrations'},
82
87
  },
@@ -137,20 +142,24 @@ export function deriveSetupChecklist({
137
142
 
138
143
  function toolsTitle(readiness: WorkspaceIntegrationReadiness): string {
139
144
  if (!readiness.hasToolIntegration) {
140
- const attentionToolProviders = readiness.attentionProviders
141
- .map((providerKey) => readiness.providers.find(({provider}) => provider === providerKey))
142
- .filter(
143
- (provider) => provider !== undefined && !provider.capabilities.includes('source_control'),
144
- );
145
- if (attentionToolProviders.length === 1) {
146
- const provider = attentionToolProviders[0];
145
+ const providers = attentionToolProviders(readiness);
146
+ if (providers.length === 1) {
147
+ const provider = providers[0];
147
148
  if (provider !== undefined) {
148
149
  return `${provider.displayName || provider.provider} needs attention`;
149
150
  }
150
151
  }
151
- if (attentionToolProviders.length > 1) {
152
- return `${attentionToolProviders.length} integrations need attention`;
152
+ if (providers.length > 1) {
153
+ return `${providers.length} integrations need attention`;
153
154
  }
154
155
  }
155
156
  return TOOLS_TITLE;
156
157
  }
158
+
159
+ function attentionToolProviders(readiness: WorkspaceIntegrationReadiness) {
160
+ return readiness.attentionProviders
161
+ .map((providerKey) => readiness.providers.find(({provider}) => provider === providerKey))
162
+ .filter(
163
+ (provider) => provider !== undefined && !provider.capabilities.includes('source_control'),
164
+ );
165
+ }
package/src/feature.ts ADDED
@@ -0,0 +1,16 @@
1
+ import {defineClientFeature} from '@shipfox/client-shell';
2
+ import {lazy} from 'react';
3
+
4
+ export const onboardingFeature = defineClientFeature({
5
+ id: 'shipfox.onboarding',
6
+ });
7
+
8
+ export const WorkspaceSetupChecklist = lazy(async () => {
9
+ const module = await import('./components/setup-checklist.js');
10
+ return {default: module.WorkspaceSetupChecklist};
11
+ });
12
+
13
+ export const WorkspaceSetupIndicator = lazy(async () => {
14
+ const module = await import('./components/setup-checklist.js');
15
+ return {default: module.WorkspaceSetupIndicator};
16
+ });
@@ -0,0 +1,144 @@
1
+ import {
2
+ modelProviderCatalogQueryOptions,
3
+ modelProviderConfigsQueryOptions,
4
+ } from '@shipfox/client-agent';
5
+ import {
6
+ integrationConnectionsQueryOptions,
7
+ integrationProvidersQueryOptions,
8
+ } from '@shipfox/client-integrations';
9
+ import {
10
+ activeProvisionersQueryOptions,
11
+ installationRunnersStatusQueryOptions,
12
+ } from '@shipfox/client-runners';
13
+ import {
14
+ listInvitationsQueryOptions,
15
+ listMembersQueryOptions,
16
+ } from '@shipfox/client-workspace-settings';
17
+ import {useQuery} from '@tanstack/react-query';
18
+ import {deriveIntegrationReadiness} from '#core/integration-readiness.js';
19
+ import {
20
+ deriveSetupChecklist,
21
+ type SetupChecklist,
22
+ type SetupChecklistItemId,
23
+ } from '#core/setup-checklist.js';
24
+
25
+ const CHECKLIST_STALE_TIME_MS = 5 * 60 * 1000;
26
+
27
+ export interface ChecklistQueryState {
28
+ checklist: SetupChecklist;
29
+ baseSettled: boolean;
30
+ completionReady: boolean;
31
+ }
32
+
33
+ /**
34
+ * Composes the server state needed by both checklist hosts. Keeping this hook
35
+ * in the API adapter boundary gives each query family one shared cache policy
36
+ * while allowing dismissed hosts to remain unsubscribed.
37
+ */
38
+ export function useSetupChecklistQueryState(
39
+ workspaceId: string,
40
+ subscribed: boolean,
41
+ ): ChecklistQueryState {
42
+ const queryEnabled = subscribed && Boolean(workspaceId);
43
+ const queryPolicy = {
44
+ enabled: queryEnabled,
45
+ subscribed: queryEnabled,
46
+ refetchInterval: false,
47
+ retry: false,
48
+ staleTime: CHECKLIST_STALE_TIME_MS,
49
+ refetchOnWindowFocus: false,
50
+ } as const;
51
+
52
+ const providersQuery = useQuery({
53
+ ...integrationProvidersQueryOptions(),
54
+ ...queryPolicy,
55
+ });
56
+ const connectionsQuery = useQuery({
57
+ ...integrationConnectionsQueryOptions(workspaceId),
58
+ ...queryPolicy,
59
+ });
60
+ const activeProvisionersQuery = useQuery({
61
+ ...activeProvisionersQueryOptions(workspaceId),
62
+ ...queryPolicy,
63
+ });
64
+ const runnersStatusQuery = useQuery({
65
+ ...installationRunnersStatusQueryOptions(workspaceId),
66
+ ...queryPolicy,
67
+ });
68
+ const catalogQuery = useQuery({
69
+ ...modelProviderCatalogQueryOptions(),
70
+ ...queryPolicy,
71
+ });
72
+ const configsQuery = useQuery({
73
+ ...modelProviderConfigsQueryOptions(workspaceId),
74
+ ...queryPolicy,
75
+ });
76
+ const membersQuery = useQuery({
77
+ ...listMembersQueryOptions(workspaceId),
78
+ ...queryPolicy,
79
+ });
80
+ const invitationsQuery = useQuery({
81
+ ...listInvitationsQueryOptions(workspaceId),
82
+ ...queryPolicy,
83
+ });
84
+
85
+ const baseSettled = isSettled(providersQuery) && isSettled(connectionsQuery);
86
+ const runnerSettled = isSettled(activeProvisionersQuery) && isSettled(runnersStatusQuery);
87
+ const runnerReady = activeProvisionersQuery.isSuccess && runnersStatusQuery.isSuccess;
88
+ const catalogInstallationProvided =
89
+ catalogQuery.data !== undefined &&
90
+ (catalogQuery.data.managedProviderId !== null ||
91
+ catalogQuery.data.instanceDefaultProviderId !== null);
92
+ const modelReady = catalogQuery.isSuccess && configsQuery.isSuccess;
93
+ const membersReady = membersQuery.isSuccess && invitationsQuery.isSuccess;
94
+ const modelSettled = isSettled(catalogQuery) && isSettled(configsQuery);
95
+ const membersSettled = isSettled(membersQuery) && isSettled(invitationsQuery);
96
+ const completionReady =
97
+ providersQuery.isSuccess &&
98
+ connectionsQuery.isSuccess &&
99
+ runnerSettled &&
100
+ modelSettled &&
101
+ membersSettled;
102
+
103
+ const rawChecklist = deriveSetupChecklist({
104
+ readiness: deriveIntegrationReadiness({
105
+ providers: providersQuery.data ?? [],
106
+ connections: connectionsQuery.data ?? [],
107
+ }),
108
+ installationRunners: runnersStatusQuery.data ?? 'managed',
109
+ workspaceRunnerCapacity: (activeProvisionersQuery.data?.length ?? 0) > 0,
110
+ modelProvider: {
111
+ installationProvided: catalogQuery.isSuccess ? catalogInstallationProvided : true,
112
+ configured: (configsQuery.data?.configs.length ?? 0) > 0,
113
+ },
114
+ membership: {
115
+ memberCount: membersQuery.data?.length ?? 0,
116
+ pendingInvitationCount: invitationsQuery.data?.length ?? 0,
117
+ },
118
+ });
119
+
120
+ const hiddenRows = new Set<SetupChecklistItemId>();
121
+ if (!providersQuery.isSuccess || !connectionsQuery.isSuccess) hiddenRows.add('tools');
122
+ if (!runnerReady) hiddenRows.add('runner');
123
+ if (!modelReady) hiddenRows.add('model-provider');
124
+ if (!membersReady) hiddenRows.add('teammates');
125
+
126
+ const items = rawChecklist.items.filter((item) => !hiddenRows.has(item.id));
127
+ const trackedItems = items.filter((item) => item.tracked);
128
+ const openCount = trackedItems.filter((item) => item.status === 'open').length;
129
+
130
+ return {
131
+ baseSettled,
132
+ completionReady,
133
+ checklist: {
134
+ items,
135
+ openCount,
136
+ trackedCount: trackedItems.length,
137
+ complete: completionReady && openCount === 0,
138
+ },
139
+ };
140
+ }
141
+
142
+ function isSettled(query: {isError: boolean; isSuccess: boolean}) {
143
+ return query.isSuccess || query.isError;
144
+ }
@@ -0,0 +1,60 @@
1
+ import {useClientAnalytics} from '@shipfox/client-shell/runtime';
2
+ import {useEffect, useRef, useState} from 'react';
3
+ import type {ChecklistQueryState} from '#hooks/api/setup-checklist.js';
4
+
5
+ type ChecklistHost = 'panel' | 'popover';
6
+
7
+ export function useCompletionTransition(
8
+ queryState: ChecklistQueryState,
9
+ host: ChecklistHost,
10
+ onCompleted?: (completed: boolean) => void,
11
+ ) {
12
+ const analytics = useClientAnalytics();
13
+ const observedIncomplete = useRef(false);
14
+ const completionHandled = useRef(false);
15
+ const [showCompletion, setShowCompletion] = useState(false);
16
+
17
+ useEffect(() => {
18
+ if (
19
+ queryState.baseSettled &&
20
+ !queryState.checklist.complete &&
21
+ queryState.checklist.openCount > 0
22
+ ) {
23
+ observedIncomplete.current = true;
24
+ }
25
+
26
+ if (!queryState.completionReady) return;
27
+
28
+ if (!queryState.checklist.complete) {
29
+ setShowCompletion(false);
30
+ return;
31
+ }
32
+
33
+ if (!observedIncomplete.current || completionHandled.current) return;
34
+ completionHandled.current = true;
35
+ setShowCompletion(true);
36
+ onCompleted?.(true);
37
+ analytics.capture('onboarding_checklist_completed', {host});
38
+ }, [
39
+ analytics,
40
+ host,
41
+ onCompleted,
42
+ queryState.baseSettled,
43
+ queryState.checklist.complete,
44
+ queryState.checklist.openCount,
45
+ queryState.completionReady,
46
+ ]);
47
+
48
+ return showCompletion;
49
+ }
50
+
51
+ export function useShownAnalytics(host: ChecklistHost, visible: boolean) {
52
+ const analytics = useClientAnalytics();
53
+ const shown = useRef(false);
54
+
55
+ useEffect(() => {
56
+ if (!visible || shown.current) return;
57
+ shown.current = true;
58
+ analytics.capture('onboarding_checklist_shown', {host});
59
+ }, [analytics, host, visible]);
60
+ }
@@ -0,0 +1,30 @@
1
+ import {
2
+ dismissWorkspaceSetupChecklist,
3
+ isWorkspaceSetupChecklistDismissed,
4
+ WORKSPACE_SETUP_CHECKLIST_DISMISSAL_EVENT,
5
+ } from '@shipfox/client-shell/runtime';
6
+ import {useCallback, useEffect, useState} from 'react';
7
+
8
+ export function useChecklistDismissal(workspaceId: string) {
9
+ const [dismissed, setDismissed] = useState(() => isWorkspaceSetupChecklistDismissed(workspaceId));
10
+
11
+ useEffect(() => {
12
+ const refresh = () => setDismissed(isWorkspaceSetupChecklistDismissed(workspaceId));
13
+ refresh();
14
+ window.addEventListener('storage', refresh);
15
+ window.addEventListener('focus', refresh);
16
+ window.addEventListener(WORKSPACE_SETUP_CHECKLIST_DISMISSAL_EVENT, refresh);
17
+ return () => {
18
+ window.removeEventListener('storage', refresh);
19
+ window.removeEventListener('focus', refresh);
20
+ window.removeEventListener(WORKSPACE_SETUP_CHECKLIST_DISMISSAL_EVENT, refresh);
21
+ };
22
+ }, [workspaceId]);
23
+
24
+ const dismiss = useCallback(() => {
25
+ dismissWorkspaceSetupChecklist(workspaceId);
26
+ setDismissed(true);
27
+ }, [workspaceId]);
28
+
29
+ return {dismissed, dismiss};
30
+ }
package/src/index.ts CHANGED
@@ -13,6 +13,14 @@ export {
13
13
  type SetupChecklistItemId,
14
14
  type SetupChecklistItemStatus,
15
15
  } from '#core/setup-checklist.js';
16
+ export {
17
+ SetupChecklistBody,
18
+ type SetupChecklistBodyProps,
19
+ type WorkspaceReference,
20
+ WorkspaceSetupChecklist,
21
+ type WorkspaceSetupHostProps,
22
+ WorkspaceSetupIndicator,
23
+ } from './components/setup-checklist.js';
16
24
  export {
17
25
  loadWorkspaceSetupRoute,
18
26
  type WorkspaceSetupRouteOptions,