@umituz/web-dashboard 6.1.0 → 6.2.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/README.md +47 -73
- package/package.json +5 -9
- package/src/config/DashboardConfig.ts +19 -68
- package/src/domains/analytics/components/index.ts +0 -6
- package/src/domains/analytics/hooks/index.ts +0 -6
- package/src/domains/analytics/hooks/useAnalytics.ts +2 -20
- package/src/domains/analytics/index.ts +0 -10
- package/src/domains/analytics/services/AnalyticsEngineService.ts +7 -74
- package/src/domains/analytics/services/PerformanceService.ts +27 -138
- package/src/domains/analytics/services/index.ts +0 -4
- package/src/domains/analytics/types/analytics.ts +100 -270
- package/src/domains/analytics/types/index.ts +0 -6
- package/src/domains/analytics/utils/analytics.test.ts +2 -9
- package/src/domains/analytics/utils/analytics.ts +1 -22
- package/src/domains/analytics/utils/index.ts +0 -6
- package/src/domains/calendar/config.ts +0 -7
- package/src/domains/calendar/hooks/index.ts +0 -4
- package/src/domains/calendar/hooks/useCalendar.ts +7 -27
- package/src/domains/calendar/index.ts +0 -4
- package/src/domains/calendar/services/CalendarService.ts +0 -21
- package/src/domains/calendar/services/index.ts +0 -4
- package/src/domains/calendar/types/calendar.types.ts +9 -53
- package/src/domains/calendar/utils/contentItemMapper.test.ts +0 -4
- package/src/domains/calendar/utils/contentItemMapper.ts +0 -31
- package/src/domains/calendar/utils/index.ts +0 -26
- package/src/domains/layouts/components/index.ts +0 -4
- package/src/domains/layouts/hooks/dashboard.ts +0 -21
- package/src/domains/layouts/hooks/index.ts +0 -4
- package/src/domains/layouts/index.ts +0 -6
- package/src/domains/layouts/theme/default.ts +0 -12
- package/src/domains/layouts/theme/index.ts +0 -4
- package/src/domains/layouts/theme/presets.ts +0 -9
- package/src/domains/layouts/theme/utils.ts +0 -15
- package/src/domains/layouts/types/index.ts +0 -4
- package/src/domains/layouts/types/layout.ts +18 -56
- package/src/domains/layouts/types/notification.ts +4 -17
- package/src/domains/layouts/types/sidebar.ts +8 -28
- package/src/domains/layouts/types/theme.ts +23 -59
- package/src/domains/layouts/types/user.ts +9 -30
- package/src/domains/layouts/utils/dashboard.ts +4 -48
- package/src/domains/layouts/utils/index.ts +0 -4
- package/src/domains/modules/build.tsx +2 -16
- package/src/domains/modules/types.ts +8 -23
- package/src/domains/onboarding/calc/calc.test.ts +1 -9
- package/src/domains/onboarding/calc/card-stack-motion-curve.ts +0 -6
- package/src/domains/onboarding/calc/count-with-locale.ts +0 -8
- package/src/domains/onboarding/calc/duration-equal-split.ts +0 -12
- package/src/domains/onboarding/calc/elapsed-millis.ts +0 -6
- package/src/domains/onboarding/calc/progress-percentage.ts +1 -11
- package/src/domains/onboarding/calc/slider-value-bounds.ts +1 -11
- package/src/domains/onboarding/calc/step-index-clamp.ts +0 -9
- package/src/domains/onboarding/calc/swipe-gesture-curve.ts +0 -13
- package/src/domains/onboarding/components/{shared/AnimationWrapper.tsx → AnimationWrapper.tsx} +0 -4
- package/src/domains/onboarding/components/{shared/OnboardingErrorBoundary.tsx → OnboardingErrorBoundary.tsx} +7 -27
- package/src/domains/onboarding/components/OnboardingFlow.test.tsx +8 -30
- package/src/domains/onboarding/components/OnboardingFlow.tsx +13 -61
- package/src/domains/onboarding/components/OnboardingIntl.tsx +1 -11
- package/src/domains/onboarding/components/PrimaryButton.tsx +40 -0
- package/src/domains/onboarding/components/ProgressBar.tsx +32 -0
- package/src/domains/onboarding/components/QuestionWrapper.tsx +28 -0
- package/src/domains/onboarding/components/ScreenContainer.tsx +19 -0
- package/src/domains/onboarding/components/{shared/SwipeCard.test.tsx → SwipeCard.test.tsx} +1 -3
- package/src/domains/onboarding/components/{shared/SwipeCard.tsx → SwipeCard.tsx} +10 -28
- package/src/domains/onboarding/components/renderBundledIcon.tsx +7 -0
- package/src/domains/onboarding/components/screens/AccountGateScreen.tsx +7 -17
- package/src/domains/onboarding/components/screens/AppDemoScreen.tsx +4 -10
- package/src/domains/onboarding/components/screens/ComparisonScreen.tsx +12 -22
- package/src/domains/onboarding/components/screens/GoalQuestionScreen.tsx +9 -6
- package/src/domains/onboarding/components/screens/PainPointsScreen.tsx +10 -10
- package/src/domains/onboarding/components/screens/PaywallScreen.tsx +22 -38
- package/src/domains/onboarding/components/screens/PermissionPrimingScreen.tsx +14 -29
- package/src/domains/onboarding/components/screens/PersonalizedSolutionScreen.tsx +10 -23
- package/src/domains/onboarding/components/screens/PreferencesScreen.tsx +13 -27
- package/src/domains/onboarding/components/screens/ProcessingScreen.tsx +11 -27
- package/src/domains/onboarding/components/screens/SocialProofScreen.tsx +22 -51
- package/src/domains/onboarding/components/screens/TinderCardsScreen.tsx +13 -30
- package/src/domains/onboarding/components/screens/ValueDeliveryScreen.tsx +13 -29
- package/src/domains/onboarding/components/screens/WelcomeScreen.tsx +12 -32
- package/src/domains/onboarding/{components/hooks → hooks}/useOnboardingAnalytics.test.tsx +1 -1
- package/src/domains/onboarding/{components/hooks → hooks}/useOnboardingAnalytics.ts +3 -16
- package/src/domains/onboarding/{components/hooks → hooks}/useOnboardingProgress.test.tsx +2 -2
- package/src/domains/onboarding/{components/hooks → hooks}/useOnboardingProgress.ts +10 -21
- package/src/domains/onboarding/{components/hooks → hooks}/usePermissionPriming.ts +3 -10
- package/src/domains/onboarding/{components/hooks → hooks}/useReducedMotion.ts +0 -5
- package/src/domains/onboarding/index.ts +16 -45
- package/src/domains/onboarding/predicates/has-handler.ts +0 -4
- package/src/domains/onboarding/predicates/icon-name-resolves.ts +1 -17
- package/src/domains/onboarding/predicates/permission-granted.ts +0 -5
- package/src/domains/onboarding/predicates/permission-supports.ts +1 -11
- package/src/domains/onboarding/predicates/predicates.test.ts +6 -20
- package/src/domains/onboarding/predicates/screen-emits-data.ts +0 -4
- package/src/domains/onboarding/predicates/storage-available.ts +0 -8
- package/src/domains/onboarding/templates/TemplateDemoFallback.tsx +19 -39
- package/src/domains/onboarding/templates/art-onboarding.ts +1 -11
- package/src/domains/onboarding/templates/health-onboarding.ts +0 -5
- package/src/domains/onboarding/templates/platform-onboarding.ts +0 -9
- package/src/domains/onboarding/templates/video-onboarding.ts +0 -5
- package/src/domains/onboarding/test/setup.ts +1 -8
- package/src/domains/onboarding/types/config.ts +7 -18
- package/src/domains/onboarding/types/labels.ts +25 -59
- package/src/domains/onboarding/types/screens.ts +3 -11
- package/src/domains/onboarding/utils/analytics-dispatch.ts +2 -21
- package/src/domains/onboarding/utils/default-storage-key.ts +0 -1
- package/src/domains/onboarding/utils/index.ts +0 -1
- package/src/domains/onboarding/utils/local-storage-bridge.ts +1 -21
- package/src/domains/onboarding/utils/permission-bridge.ts +0 -8
- package/src/domains/onboarding/validation/flow-structure-check.ts +0 -6
- package/src/domains/onboarding/validation/permission-status-check.ts +0 -5
- package/src/domains/onboarding/validation/screen-config-check.ts +0 -5
- package/src/domains/onboarding/validation/stored-progress-check.ts +1 -10
- package/src/domains/onboarding/validation/validation.test.ts +1 -6
- package/src/domains/permissions/hasPermission.ts +0 -8
- package/src/domains/permissions/types.ts +0 -20
- package/src/domains/settings/components/index.ts +0 -4
- package/src/domains/settings/hooks/index.ts +0 -4
- package/src/domains/settings/hooks/useSettings.ts +1 -26
- package/src/domains/settings/index.ts +0 -6
- package/src/domains/settings/types/index.ts +0 -4
- package/src/domains/settings/types/settings.ts +23 -67
- package/src/domains/settings/utils/index.ts +0 -4
- package/src/domains/settings/utils/settings.ts +0 -45
- package/src/styles/dashboard.css +818 -1
- package/src/domains/onboarding/components/shared/OptionCard.tsx +0 -73
- package/src/domains/onboarding/components/shared/PrimaryButton.tsx +0 -58
- package/src/domains/onboarding/components/shared/ProgressBar.tsx +0 -51
- package/src/domains/onboarding/components/shared/QuestionWrapper.tsx +0 -36
- package/src/domains/onboarding/components/shared/ScreenContainer.tsx +0 -29
- package/src/domains/onboarding/components/shared/index.ts +0 -16
- package/src/domains/onboarding/components/shared/renderBundledIcon.tsx +0 -20
- package/src/domains/onboarding/onboarding.css +0 -1
- package/src/domains/onboarding/theme/cn.ts +0 -10
- package/src/domains/onboarding/theme/index.css +0 -131
- package/src/domains/onboarding/theme/index.ts +0 -12
- package/src/domains/onboarding/theme/tokens.ts +0 -122
- /package/src/domains/onboarding/{components/hooks → hooks}/index.ts +0 -0
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Discriminated union: every screen is one of 14 strictly-typed shapes.
|
|
5
|
-
* Eliminates `Record<string, any>` masking (project rule: no `any`).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
3
|
export interface BaseScreen<T extends string, D = unknown> {
|
|
9
4
|
id: string;
|
|
10
5
|
type: T;
|
|
@@ -19,8 +14,7 @@ export interface OnboardingOption {
|
|
|
19
14
|
id: string;
|
|
20
15
|
label: string;
|
|
21
16
|
description?: string;
|
|
22
|
-
iconName?: string;
|
|
23
|
-
image?: string;
|
|
17
|
+
iconName?: string; image?: string;
|
|
24
18
|
}
|
|
25
19
|
|
|
26
20
|
export type WelcomeScreenConfig = BaseScreen<'welcome'> & {
|
|
@@ -131,8 +125,7 @@ export type ValueDeliveryScreenConfig<TResult = unknown> = BaseScreen<
|
|
|
131
125
|
> & {
|
|
132
126
|
title: string;
|
|
133
127
|
result: TResult;
|
|
134
|
-
|
|
135
|
-
shareConfig?: {
|
|
128
|
+
shareConfig?: {
|
|
136
129
|
text: string;
|
|
137
130
|
url?: string;
|
|
138
131
|
};
|
|
@@ -148,8 +141,7 @@ export type AccountGateScreenConfig = BaseScreen<'account-gate', { signedIn: boo
|
|
|
148
141
|
};
|
|
149
142
|
|
|
150
143
|
export type PaywallFeature = {
|
|
151
|
-
iconName: string;
|
|
152
|
-
text: string;
|
|
144
|
+
iconName: string; text: string;
|
|
153
145
|
highlighted?: boolean;
|
|
154
146
|
};
|
|
155
147
|
|
|
@@ -1,21 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Analytics dispatch service.
|
|
3
|
-
* Routes a typed onboarding event to the configured provider.
|
|
4
|
-
* Provider-specific dispatch is isolated here; callers stay provider-agnostic.
|
|
5
|
-
*
|
|
6
|
-
* Contract: dispatch NEVER throws. Analytics is an observation channel — a
|
|
7
|
-
* broken tracker must not take down the onboarding funnel. Failures are
|
|
8
|
-
* surfaced to the console (the library's documented error channel) and
|
|
9
|
-
* dropped.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
1
|
import type {
|
|
13
2
|
AnalyticsProvider,
|
|
14
3
|
OnboardingAnalyticsEvent,
|
|
15
4
|
} from '../types/config';
|
|
16
5
|
|
|
17
|
-
/** Provider globals are read through a LOCAL view of `window` — this package
|
|
18
|
-
* must never augment the global `Window` type for its consumers. */
|
|
19
6
|
type AnalyticsWindow = Window & {
|
|
20
7
|
gtag?: (...args: unknown[]) => void;
|
|
21
8
|
amplitude?: {
|
|
@@ -25,8 +12,7 @@ type AnalyticsWindow = Window & {
|
|
|
25
12
|
};
|
|
26
13
|
|
|
27
14
|
function dispatchToProvider(event: OnboardingAnalyticsEvent, provider: AnalyticsProvider): void {
|
|
28
|
-
if (typeof window === 'undefined') return;
|
|
29
|
-
const win = window as AnalyticsWindow;
|
|
15
|
+
if (typeof window === 'undefined') return; const win = window as AnalyticsWindow;
|
|
30
16
|
switch (provider) {
|
|
31
17
|
case 'google-analytics':
|
|
32
18
|
win.gtag?.('event', event.name, event as unknown as Record<string, unknown>);
|
|
@@ -40,8 +26,7 @@ function dispatchToProvider(event: OnboardingAnalyticsEvent, provider: Analytics
|
|
|
40
26
|
win.mixpanel?.track(event.name, event as unknown as Record<string, unknown>);
|
|
41
27
|
return;
|
|
42
28
|
case 'custom':
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
29
|
+
return; }
|
|
45
30
|
}
|
|
46
31
|
|
|
47
32
|
function dispatchToCustomTracker(
|
|
@@ -59,10 +44,6 @@ function dispatchToCustomTracker(
|
|
|
59
44
|
customTracker(event);
|
|
60
45
|
}
|
|
61
46
|
|
|
62
|
-
/**
|
|
63
|
-
* Routes an event to the configured provider. Never throws: provider failures
|
|
64
|
-
* are logged and dropped so an analytics outage cannot break the flow UI.
|
|
65
|
-
*/
|
|
66
47
|
export function dispatchAnalyticsEvent(
|
|
67
48
|
event: OnboardingAnalyticsEvent,
|
|
68
49
|
provider: AnalyticsProvider | undefined,
|
|
@@ -9,5 +9,4 @@ export type { RehydratedProgress } from './local-storage-bridge';
|
|
|
9
9
|
export { queryPermissionStatus, requestPermission } from './permission-bridge';
|
|
10
10
|
export { defaultStorageKey } from './default-storage-key';
|
|
11
11
|
|
|
12
|
-
// Re-export validation for convenience (the public API treats validation as a util).
|
|
13
12
|
export { checkFlowStructure, checkScreenConfig, checkStoredProgress } from '../validation';
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* localStorage bridge for onboarding progress.
|
|
3
|
-
* Owns the I/O — validation, parsing, and the schema version are delegated
|
|
4
|
-
* to the validation module so this file is a thin persistence layer.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
import { elapsedMillis } from '../calc/elapsed-millis';
|
|
8
2
|
import { isLocalStorageAvailable } from '../predicates/storage-available';
|
|
9
3
|
import { checkStoredProgress } from '../validation/stored-progress-check';
|
|
@@ -22,10 +16,6 @@ function buildKey(customKey: string | undefined): string {
|
|
|
22
16
|
return customKey ?? defaultStorageKey;
|
|
23
17
|
}
|
|
24
18
|
|
|
25
|
-
/**
|
|
26
|
-
* Save the current progress to localStorage. Returns true on success.
|
|
27
|
-
* Surfaces persistence failures to the console; never silently masks them.
|
|
28
|
-
*/
|
|
29
19
|
export function saveOnboardingProgress(
|
|
30
20
|
key: string | undefined,
|
|
31
21
|
payload: {
|
|
@@ -55,18 +45,13 @@ export function saveOnboardingProgress(
|
|
|
55
45
|
}
|
|
56
46
|
}
|
|
57
47
|
|
|
58
|
-
/**
|
|
59
|
-
* Load and validate a previously saved progress payload.
|
|
60
|
-
* Returns null if storage is unavailable, nothing is stored, or the payload is corrupt.
|
|
61
|
-
*/
|
|
62
48
|
export function loadOnboardingProgress(key: string | undefined): RehydratedProgress | null {
|
|
63
49
|
if (!isLocalStorageAvailable()) return null;
|
|
64
50
|
const raw = window.localStorage.getItem(buildKey(key));
|
|
65
51
|
const checked = checkStoredProgress(raw);
|
|
66
52
|
if (!checked.ok) {
|
|
67
53
|
if (raw !== null) {
|
|
68
|
-
|
|
69
|
-
window.localStorage.removeItem(buildKey(key));
|
|
54
|
+
window.localStorage.removeItem(buildKey(key));
|
|
70
55
|
}
|
|
71
56
|
return null;
|
|
72
57
|
}
|
|
@@ -79,16 +64,11 @@ export function loadOnboardingProgress(key: string | undefined): RehydratedProgr
|
|
|
79
64
|
};
|
|
80
65
|
}
|
|
81
66
|
|
|
82
|
-
/** Remove the stored payload. Safe to call when nothing is stored. */
|
|
83
67
|
export function clearOnboardingProgress(key: string | undefined): void {
|
|
84
68
|
if (!isLocalStorageAvailable()) return;
|
|
85
69
|
window.localStorage.removeItem(buildKey(key));
|
|
86
70
|
}
|
|
87
71
|
|
|
88
|
-
/**
|
|
89
|
-
* Build the final `OnboardingResult` value the consumer receives via
|
|
90
|
-
* `onComplete`. Owns the duration calculation; no magic numbers here.
|
|
91
|
-
*/
|
|
92
72
|
export function buildOnboardingResult(input: {
|
|
93
73
|
completedSteps: ReadonlyArray<string>;
|
|
94
74
|
collected: Record<string, unknown>;
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser permission API bridge.
|
|
3
|
-
* Owns the I/O — the predicates and the validation module decide whether
|
|
4
|
-
* the runtime supports the call, and how to normalize the raw status.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
import {
|
|
8
2
|
doesBrowserSupportMediaCapture,
|
|
9
3
|
doesBrowserSupportGeolocation,
|
|
@@ -14,7 +8,6 @@ import type { PermissionStatus, PermissionType } from '../types/screens';
|
|
|
14
8
|
|
|
15
9
|
const GEOLOCATION_TIMEOUT_MS = 5000;
|
|
16
10
|
|
|
17
|
-
/** Read the current permission status without prompting the user. */
|
|
18
11
|
export async function queryPermissionStatus(permission: PermissionType): Promise<PermissionStatus> {
|
|
19
12
|
if (typeof window === 'undefined' || typeof navigator === 'undefined') return 'unsupported';
|
|
20
13
|
|
|
@@ -51,7 +44,6 @@ export async function queryPermissionStatus(permission: PermissionType): Promise
|
|
|
51
44
|
}
|
|
52
45
|
}
|
|
53
46
|
|
|
54
|
-
/** Prompt the user for the given permission. Never throws — failures map to a status. */
|
|
55
47
|
export async function requestPermission(permission: PermissionType): Promise<PermissionStatus> {
|
|
56
48
|
if (typeof window === 'undefined' || typeof navigator === 'undefined') return 'unsupported';
|
|
57
49
|
|
|
@@ -5,12 +5,6 @@ export type ValidationOutcome = { ok: true } | { ok: false; reason: string };
|
|
|
5
5
|
|
|
6
6
|
const MIN_FLOW_LENGTH = 1;
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* Validates the structure of an entire onboarding flow:
|
|
10
|
-
* - At least one screen exists
|
|
11
|
-
* - Every screen passes its own check
|
|
12
|
-
* - Step IDs are unique
|
|
13
|
-
*/
|
|
14
8
|
export function checkFlowStructure(screens: ReadonlyArray<OnboardingScreen>): ValidationOutcome {
|
|
15
9
|
if (!Array.isArray(screens) || screens.length < MIN_FLOW_LENGTH) {
|
|
16
10
|
return { ok: false, reason: 'flow must contain at least one screen' };
|
|
@@ -7,15 +7,10 @@ const RECOGNIZED_STATUSES: ReadonlySet<PermissionStatus> = new Set<PermissionSta
|
|
|
7
7
|
'unsupported',
|
|
8
8
|
]);
|
|
9
9
|
|
|
10
|
-
/** Is the given string one of the recognized permission statuses? */
|
|
11
10
|
export function isRecognizedPermissionStatus(value: unknown): value is PermissionStatus {
|
|
12
11
|
return typeof value === 'string' && RECOGNIZED_STATUSES.has(value as PermissionStatus);
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
/**
|
|
16
|
-
* Normalize a browser-level permission string (which may be "default")
|
|
17
|
-
* to our internal PermissionStatus enum.
|
|
18
|
-
*/
|
|
19
14
|
export function normalizePermissionStatus(rawValue: string | null | undefined): PermissionStatus {
|
|
20
15
|
if (rawValue === 'granted' || rawValue === 'denied') return rawValue;
|
|
21
16
|
if (rawValue === 'default' || rawValue === 'prompt') return 'prompt';
|
|
@@ -66,10 +66,6 @@ function checkPaywall(screen: OnboardingScreen): ValidationOutcome {
|
|
|
66
66
|
return { ok: true };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/**
|
|
70
|
-
* Validates a single screen config. Per-screen rules live here so the file
|
|
71
|
-
* is the single place to look when a new screen type is added.
|
|
72
|
-
*/
|
|
73
69
|
export function checkScreenConfig(screen: OnboardingScreen): ValidationOutcome {
|
|
74
70
|
if (!isPlainObject(screen)) {
|
|
75
71
|
return { ok: false, reason: 'screen must be an object' };
|
|
@@ -99,7 +95,6 @@ export function checkScreenConfig(screen: OnboardingScreen): ValidationOutcome {
|
|
|
99
95
|
}
|
|
100
96
|
}
|
|
101
97
|
|
|
102
|
-
/** The set of screen types the orchestrator knows how to render. */
|
|
103
98
|
export function getRecognizedScreenTypes(): ReadonlySet<OnboardingScreenType> {
|
|
104
99
|
return RECOGNIZED_SCREEN_TYPES;
|
|
105
100
|
}
|
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Storage payload integrity check.
|
|
3
|
-
* Decides whether a localStorage string is something we can safely rehydrate
|
|
4
|
-
* from. The shape is versioned so future migrations stay clean.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
export interface StoredProgressShape {
|
|
8
2
|
version: 1;
|
|
9
3
|
currentStep: number;
|
|
10
4
|
completedSteps: string[];
|
|
11
5
|
collected: Record<string, unknown>;
|
|
12
|
-
startedAt: string;
|
|
13
|
-
lastUpdated: string; // ISO
|
|
14
|
-
}
|
|
6
|
+
startedAt: string; lastUpdated: string; }
|
|
15
7
|
|
|
16
8
|
export type StoredProgressOutcome =
|
|
17
9
|
| { ok: true; data: StoredProgressShape }
|
|
@@ -34,7 +26,6 @@ function isIsoDateString(value: unknown): value is string {
|
|
|
34
26
|
return Number.isFinite(ms);
|
|
35
27
|
}
|
|
36
28
|
|
|
37
|
-
/** Is the raw string a valid, current-version stored progress payload? */
|
|
38
29
|
export function checkStoredProgress(raw: string | null): StoredProgressOutcome {
|
|
39
30
|
if (raw === null) return { ok: false, reason: 'no stored progress' };
|
|
40
31
|
let parsed: unknown;
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
} from './permission-status-check';
|
|
9
9
|
import type { OnboardingScreen } from '../types/screens';
|
|
10
10
|
|
|
11
|
-
// TEST_SCENARIOS.md §23
|
|
12
11
|
const welcome = (id = 'a'): OnboardingScreen => ({
|
|
13
12
|
id,
|
|
14
13
|
type: 'welcome',
|
|
@@ -31,8 +30,7 @@ describe('flow-structure-check (§23.1)', () => {
|
|
|
31
30
|
|
|
32
31
|
it('accepts a valid flow and propagates per-screen failures', () => {
|
|
33
32
|
expect(checkFlowStructure([welcome('a'), welcome('b')])).toEqual({ ok: true });
|
|
34
|
-
|
|
35
|
-
const bad = checkFlowStructure([
|
|
33
|
+
const bad = checkFlowStructure([
|
|
36
34
|
welcome('a'),
|
|
37
35
|
{ id: 'g', type: 'goal-question', question: 'Q', options: [], onChange: () => undefined },
|
|
38
36
|
]);
|
|
@@ -40,7 +38,6 @@ describe('flow-structure-check (§23.1)', () => {
|
|
|
40
38
|
});
|
|
41
39
|
});
|
|
42
40
|
|
|
43
|
-
// TEST_SCENARIOS.md §23.2
|
|
44
41
|
describe('screen-config-check (§23.2)', () => {
|
|
45
42
|
it('rejects malformed screens', () => {
|
|
46
43
|
// @ts-expect-error -- deliberately malformed input (null is not a screen)
|
|
@@ -89,7 +86,6 @@ describe('screen-config-check (§23.2)', () => {
|
|
|
89
86
|
});
|
|
90
87
|
});
|
|
91
88
|
|
|
92
|
-
// TEST_SCENARIOS.md §23.3
|
|
93
89
|
describe('stored-progress-check (§23.3)', () => {
|
|
94
90
|
const validPayload = JSON.stringify({
|
|
95
91
|
version: 1,
|
|
@@ -157,7 +153,6 @@ describe('stored-progress-check (§23.3)', () => {
|
|
|
157
153
|
});
|
|
158
154
|
});
|
|
159
155
|
|
|
160
|
-
// TEST_SCENARIOS.md §23.4
|
|
161
156
|
describe('permission-status-check (§23.4)', () => {
|
|
162
157
|
it('normalizes browser-level permission strings', () => {
|
|
163
158
|
expect(normalizePermissionStatus('granted')).toBe('granted');
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* hasPermission — pure permission lookup against a matrix.
|
|
3
|
-
*
|
|
4
|
-
* Deny-by-default: unknown roles, unknown permissions and empty
|
|
5
|
-
* grants all resolve to `false`. Only an exact entry, a module
|
|
6
|
-
* wildcard ("sales.*") or the full wildcard ("*") grants access.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
1
|
import {
|
|
10
2
|
PERMISSION_WILDCARD,
|
|
11
3
|
type Permission,
|
|
@@ -1,33 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Permission model — generalized from the texora/visora fleet pattern.
|
|
3
|
-
*
|
|
4
|
-
* Hosts keep their own role and permission vocabularies (string unions
|
|
5
|
-
* in the app); the package treats them as opaque strings and stays
|
|
6
|
-
* camp-agnostic. A matrix maps each role to either the full-grant
|
|
7
|
-
* wildcard `"*"` or a list of permission strings, which may themselves
|
|
8
|
-
* end in `.*` to grant every permission of a module
|
|
9
|
-
* (e.g. `"sales.*"` grants `"sales.view"`).
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/** A single permission, e.g. "sales.view". Opaque to the package. */
|
|
13
1
|
export type Permission = string;
|
|
14
2
|
|
|
15
|
-
/** A role key, e.g. "owner", "operator". Opaque to the package. */
|
|
16
3
|
export type RoleKey = string;
|
|
17
4
|
|
|
18
|
-
/** Full-grant wildcard — grants every permission. */
|
|
19
5
|
export const PERMISSION_WILDCARD = "*" as const;
|
|
20
6
|
|
|
21
|
-
/**
|
|
22
|
-
* What a role grants: the wildcard or an explicit permission list
|
|
23
|
-
* (module wildcards like `"sales.*"` allowed).
|
|
24
|
-
*/
|
|
25
7
|
export type RoleGrant = typeof PERMISSION_WILDCARD | readonly string[];
|
|
26
8
|
|
|
27
|
-
/** Role x permission matrix. */
|
|
28
9
|
export type PermissionMatrix = Readonly<Record<RoleKey, RoleGrant>>;
|
|
29
10
|
|
|
30
|
-
/** Display metadata for a role (matrix screens). */
|
|
31
11
|
export interface RoleInfo {
|
|
32
12
|
key: RoleKey;
|
|
33
13
|
label: string;
|
|
@@ -1,26 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Settings Hooks
|
|
3
|
-
*
|
|
4
|
-
* Custom React hooks for settings functionality.
|
|
5
|
-
* Uses a single `useReducer` so the three update paths share one
|
|
6
|
-
* well-tested tree-walking helper instead of duplicating it.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
1
|
import { useReducer, useCallback } from "react";
|
|
10
2
|
import type { SettingsConfig, SettingsItem } from "../types/settings";
|
|
11
3
|
|
|
12
|
-
/**
|
|
13
|
-
* Action discriminated union for the settings reducer.
|
|
14
|
-
*/
|
|
15
4
|
type SettingsAction =
|
|
16
5
|
| { type: 'updateItem'; sectionKey: string; itemKey: string; updates: Partial<SettingsItem> }
|
|
17
6
|
| { type: 'toggleItem'; sectionKey: string; itemKey: string }
|
|
18
7
|
| { type: 'setItemBadge'; sectionKey: string; itemKey: string; badge: number | undefined };
|
|
19
8
|
|
|
20
|
-
/**
|
|
21
|
-
* Apply a transform to the item identified by (sectionKey, itemKey).
|
|
22
|
-
* Single source of truth for the tree-walking logic.
|
|
23
|
-
*/
|
|
24
9
|
const mapItemInTree = (
|
|
25
10
|
config: SettingsConfig,
|
|
26
11
|
sectionKey: string,
|
|
@@ -48,8 +33,7 @@ const settingsReducer = (state: SettingsConfig, action: SettingsAction): Setting
|
|
|
48
33
|
case 'toggleItem':
|
|
49
34
|
return mapItemInTree(state, action.sectionKey, action.itemKey, (item) => ({
|
|
50
35
|
...item,
|
|
51
|
-
|
|
52
|
-
enabled: item.enabled === undefined ? false : !item.enabled,
|
|
36
|
+
enabled: item.enabled === undefined ? false : !item.enabled,
|
|
53
37
|
}));
|
|
54
38
|
case 'setItemBadge':
|
|
55
39
|
return mapItemInTree(state, action.sectionKey, action.itemKey, (item) => ({
|
|
@@ -59,10 +43,6 @@ const settingsReducer = (state: SettingsConfig, action: SettingsAction): Setting
|
|
|
59
43
|
}
|
|
60
44
|
};
|
|
61
45
|
|
|
62
|
-
/**
|
|
63
|
-
* Public surface of the hook. Returned callbacks are stable
|
|
64
|
-
* (no deps that change between renders).
|
|
65
|
-
*/
|
|
66
46
|
export interface UseSettingsReturn {
|
|
67
47
|
config: SettingsConfig;
|
|
68
48
|
updateItem: (sectionKey: string, itemKey: string, updates: Partial<SettingsItem>) => void;
|
|
@@ -70,11 +50,6 @@ export interface UseSettingsReturn {
|
|
|
70
50
|
setItemBadge: (sectionKey: string, itemKey: string, badge: number | undefined) => void;
|
|
71
51
|
}
|
|
72
52
|
|
|
73
|
-
/**
|
|
74
|
-
* Use Settings Hook
|
|
75
|
-
*
|
|
76
|
-
* Manages settings configuration state and provides update actions.
|
|
77
|
-
*/
|
|
78
53
|
export function useSettings(initialConfig: SettingsConfig): UseSettingsReturn {
|
|
79
54
|
const [config, dispatch] = useReducer(settingsReducer, initialConfig);
|
|
80
55
|
|
|
@@ -1,83 +1,39 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Settings Types
|
|
3
|
-
*
|
|
4
|
-
* Type definitions for settings system
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
import type { LucideIcon } from "lucide-react";
|
|
8
2
|
|
|
9
|
-
/**
|
|
10
|
-
* Settings item configuration
|
|
11
|
-
*/
|
|
12
3
|
export interface SettingsItem {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
path?: string;
|
|
23
|
-
/** Click handler when no route is available */
|
|
24
|
-
onClick?: () => void;
|
|
25
|
-
/** Whether this item is enabled */
|
|
26
|
-
enabled?: boolean;
|
|
27
|
-
/** Required permission to access */
|
|
28
|
-
permission?: string;
|
|
29
|
-
/** Badge count (for notifications) */
|
|
30
|
-
badge?: number;
|
|
4
|
+
key: string;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
icon?: LucideIcon;
|
|
8
|
+
path?: string;
|
|
9
|
+
onClick?: () => void;
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
permission?: string;
|
|
12
|
+
badge?: number;
|
|
31
13
|
}
|
|
32
14
|
|
|
33
|
-
/**
|
|
34
|
-
* Settings section configuration
|
|
35
|
-
*/
|
|
36
15
|
export interface SettingsSection {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/** Items in this section */
|
|
42
|
-
items: SettingsItem[];
|
|
43
|
-
/** Optional: Section icon */
|
|
44
|
-
icon?: LucideIcon;
|
|
16
|
+
title: string;
|
|
17
|
+
key: string;
|
|
18
|
+
items: SettingsItem[];
|
|
19
|
+
icon?: LucideIcon;
|
|
45
20
|
}
|
|
46
21
|
|
|
47
|
-
/**
|
|
48
|
-
* Settings layout configuration
|
|
49
|
-
*/
|
|
50
22
|
export interface SettingsConfig {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
brandName?: string;
|
|
55
|
-
/** Default route */
|
|
56
|
-
defaultRoute?: string;
|
|
23
|
+
sections: SettingsSection[];
|
|
24
|
+
brandName?: string;
|
|
25
|
+
defaultRoute?: string;
|
|
57
26
|
}
|
|
58
27
|
|
|
59
|
-
/**
|
|
60
|
-
* Settings layout props
|
|
61
|
-
*/
|
|
62
28
|
export interface SettingsLayoutProps {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
currentPath?: string;
|
|
67
|
-
/** On navigate callback */
|
|
68
|
-
onNavigate?: (path: string) => void;
|
|
29
|
+
config: SettingsConfig;
|
|
30
|
+
currentPath?: string;
|
|
31
|
+
onNavigate?: (path: string) => void;
|
|
69
32
|
}
|
|
70
33
|
|
|
71
|
-
/**
|
|
72
|
-
* Settings section component props
|
|
73
|
-
*/
|
|
74
34
|
export interface SettingsSectionProps {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
/** On navigate callback */
|
|
80
|
-
onNavigate?: (path: string) => void;
|
|
81
|
-
/** Collapsed state */
|
|
82
|
-
collapsed?: boolean;
|
|
35
|
+
section: SettingsSection;
|
|
36
|
+
currentPath?: string;
|
|
37
|
+
onNavigate?: (path: string) => void;
|
|
38
|
+
collapsed?: boolean;
|
|
83
39
|
}
|
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Settings Utilities
|
|
3
|
-
*
|
|
4
|
-
* Utility functions for settings operations
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
import type { SettingsConfig, SettingsItem, SettingsSection } from "../types/settings";
|
|
8
2
|
|
|
9
|
-
/**
|
|
10
|
-
* Find settings item by key
|
|
11
|
-
*
|
|
12
|
-
* @param config - Settings configuration
|
|
13
|
-
* @param itemKey - Item key to find
|
|
14
|
-
* @returns Item or undefined
|
|
15
|
-
*/
|
|
16
3
|
export function findSettingsItem(config: SettingsConfig, itemKey: string): SettingsItem | undefined {
|
|
17
4
|
for (const section of config.sections) {
|
|
18
5
|
const item = section.items.find((i) => i.key === itemKey);
|
|
@@ -21,24 +8,10 @@ export function findSettingsItem(config: SettingsConfig, itemKey: string): Setti
|
|
|
21
8
|
return undefined;
|
|
22
9
|
}
|
|
23
10
|
|
|
24
|
-
/**
|
|
25
|
-
* Find settings section by key
|
|
26
|
-
*
|
|
27
|
-
* @param config - Settings configuration
|
|
28
|
-
* @param sectionKey - Section key to find
|
|
29
|
-
* @returns Section or undefined
|
|
30
|
-
*/
|
|
31
11
|
export function findSettingsSection(config: SettingsConfig, sectionKey: string): SettingsSection | undefined {
|
|
32
12
|
return config.sections.find((s) => s.key === sectionKey);
|
|
33
13
|
}
|
|
34
14
|
|
|
35
|
-
/**
|
|
36
|
-
* Filter settings items by permission
|
|
37
|
-
*
|
|
38
|
-
* @param items - Settings items to filter
|
|
39
|
-
* @param userPermissions - User permissions
|
|
40
|
-
* @returns Filtered items
|
|
41
|
-
*/
|
|
42
15
|
export function filterSettingsByPermission<T extends SettingsItem>(
|
|
43
16
|
items: T[],
|
|
44
17
|
userPermissions?: string[]
|
|
@@ -51,19 +24,8 @@ export function filterSettingsByPermission<T extends SettingsItem>(
|
|
|
51
24
|
});
|
|
52
25
|
}
|
|
53
26
|
|
|
54
|
-
/**
|
|
55
|
-
* Fallback route when no default or first available route can be resolved.
|
|
56
|
-
* Caller should treat this as an explicit "no route configured" signal
|
|
57
|
-
* rather than a silent success.
|
|
58
|
-
*/
|
|
59
27
|
export const SETTINGS_FALLBACK_ROUTE = "/settings";
|
|
60
28
|
|
|
61
|
-
/**
|
|
62
|
-
* Get default route from settings config
|
|
63
|
-
*
|
|
64
|
-
* Returns null when no default route is resolvable. The caller is
|
|
65
|
-
* responsible for handling that case (e.g. redirect, error).
|
|
66
|
-
*/
|
|
67
29
|
export function getSettingsDefaultRoute(config: SettingsConfig): string | null {
|
|
68
30
|
if (config.defaultRoute) return config.defaultRoute;
|
|
69
31
|
|
|
@@ -75,13 +37,6 @@ export function getSettingsDefaultRoute(config: SettingsConfig): string | null {
|
|
|
75
37
|
return null;
|
|
76
38
|
}
|
|
77
39
|
|
|
78
|
-
/**
|
|
79
|
-
* Generate settings item key
|
|
80
|
-
*
|
|
81
|
-
* @param prefix - Key prefix
|
|
82
|
-
* @param identifier - Unique identifier
|
|
83
|
-
* @returns Generated key
|
|
84
|
-
*/
|
|
85
40
|
export function generateSettingsKey(prefix: string, identifier: string): string {
|
|
86
41
|
return `${prefix}_${identifier}`;
|
|
87
42
|
}
|