@umituz/web-dashboard 6.0.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 +15 -7
- 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 +173 -0
- package/src/domains/onboarding/calc/card-stack-motion-curve.ts +27 -0
- package/src/domains/onboarding/calc/count-with-locale.ts +8 -0
- package/src/domains/onboarding/calc/duration-equal-split.ts +12 -0
- package/src/domains/onboarding/calc/elapsed-millis.ts +6 -0
- package/src/domains/onboarding/calc/index.ts +36 -0
- package/src/domains/onboarding/calc/progress-percentage.ts +27 -0
- package/src/domains/onboarding/calc/slider-value-bounds.ts +34 -0
- package/src/domains/onboarding/calc/step-index-clamp.ts +19 -0
- package/src/domains/onboarding/calc/swipe-gesture-curve.ts +54 -0
- package/src/domains/onboarding/components/AnimationWrapper.tsx +69 -0
- package/src/domains/onboarding/components/OnboardingErrorBoundary.tsx +61 -0
- package/src/domains/onboarding/components/OnboardingFlow.test.tsx +459 -0
- package/src/domains/onboarding/components/OnboardingFlow.tsx +313 -0
- package/src/domains/onboarding/components/OnboardingIntl.tsx +98 -0
- 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/SwipeCard.test.tsx +49 -0
- package/src/domains/onboarding/components/SwipeCard.tsx +88 -0
- package/src/domains/onboarding/components/renderBundledIcon.tsx +7 -0
- package/src/domains/onboarding/components/screens/AccountGateScreen.tsx +79 -0
- package/src/domains/onboarding/components/screens/AppDemoScreen.tsx +33 -0
- package/src/domains/onboarding/components/screens/ComparisonScreen.tsx +49 -0
- package/src/domains/onboarding/components/screens/GoalQuestionScreen.tsx +59 -0
- package/src/domains/onboarding/components/screens/PainPointsScreen.tsx +65 -0
- package/src/domains/onboarding/components/screens/PaywallScreen.tsx +122 -0
- package/src/domains/onboarding/components/screens/PermissionPrimingScreen.tsx +98 -0
- package/src/domains/onboarding/components/screens/PersonalizedSolutionScreen.tsx +40 -0
- package/src/domains/onboarding/components/screens/PreferencesScreen.tsx +180 -0
- package/src/domains/onboarding/components/screens/ProcessingScreen.tsx +63 -0
- package/src/domains/onboarding/components/screens/SocialProofScreen.tsx +87 -0
- package/src/domains/onboarding/components/screens/TinderCardsScreen.tsx +91 -0
- package/src/domains/onboarding/components/screens/ValueDeliveryScreen.tsx +69 -0
- package/src/domains/onboarding/components/screens/WelcomeScreen.tsx +32 -0
- package/src/domains/onboarding/components/screens/index.ts +28 -0
- package/src/domains/onboarding/hooks/index.ts +7 -0
- package/src/domains/onboarding/hooks/useOnboardingAnalytics.test.tsx +74 -0
- package/src/domains/onboarding/hooks/useOnboardingAnalytics.ts +49 -0
- package/src/domains/onboarding/hooks/useOnboardingProgress.test.tsx +150 -0
- package/src/domains/onboarding/hooks/useOnboardingProgress.ts +249 -0
- package/src/domains/onboarding/hooks/usePermissionPriming.ts +45 -0
- package/src/domains/onboarding/hooks/useReducedMotion.ts +19 -0
- package/src/domains/onboarding/index.ts +153 -0
- package/src/domains/onboarding/predicates/has-handler.ts +21 -0
- package/src/domains/onboarding/predicates/icon-name-resolves.ts +97 -0
- package/src/domains/onboarding/predicates/index.ts +26 -0
- package/src/domains/onboarding/predicates/permission-granted.ts +21 -0
- package/src/domains/onboarding/predicates/permission-supports.ts +56 -0
- package/src/domains/onboarding/predicates/predicates.test.ts +188 -0
- package/src/domains/onboarding/predicates/screen-emits-data.ts +13 -0
- package/src/domains/onboarding/predicates/storage-available.ts +14 -0
- package/src/domains/onboarding/templates/TemplateDemoFallback.tsx +92 -0
- package/src/domains/onboarding/templates/art-onboarding.ts +71 -0
- package/src/domains/onboarding/templates/health-onboarding.ts +92 -0
- package/src/domains/onboarding/templates/index.ts +6 -0
- package/src/domains/onboarding/templates/platform-onboarding.ts +201 -0
- package/src/domains/onboarding/templates/video-onboarding.ts +95 -0
- package/src/domains/onboarding/test/setup.ts +23 -0
- package/src/domains/onboarding/types/config.ts +57 -0
- package/src/domains/onboarding/types/index.ts +10 -0
- package/src/domains/onboarding/types/labels.ts +49 -0
- package/src/domains/onboarding/types/screens.ts +185 -0
- package/src/domains/onboarding/utils/analytics-dispatch.test.ts +93 -0
- package/src/domains/onboarding/utils/analytics-dispatch.ts +73 -0
- package/src/domains/onboarding/utils/default-storage-key.ts +1 -0
- package/src/domains/onboarding/utils/index.ts +12 -0
- package/src/domains/onboarding/utils/local-storage-bridge.test.ts +93 -0
- package/src/domains/onboarding/utils/local-storage-bridge.ts +84 -0
- package/src/domains/onboarding/utils/permission-bridge.test.ts +64 -0
- package/src/domains/onboarding/utils/permission-bridge.ts +87 -0
- package/src/domains/onboarding/validation/flow-structure-check.ts +22 -0
- package/src/domains/onboarding/validation/index.ts +9 -0
- package/src/domains/onboarding/validation/permission-status-check.ts +18 -0
- package/src/domains/onboarding/validation/screen-config-check.ts +100 -0
- package/src/domains/onboarding/validation/stored-progress-check.ts +59 -0
- package/src/domains/onboarding/validation/validation.test.ts +170 -0
- 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.contract.test.ts +1 -0
- package/src/styles/dashboard.css +818 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface StoredProgressShape {
|
|
2
|
+
version: 1;
|
|
3
|
+
currentStep: number;
|
|
4
|
+
completedSteps: string[];
|
|
5
|
+
collected: Record<string, unknown>;
|
|
6
|
+
startedAt: string; lastUpdated: string; }
|
|
7
|
+
|
|
8
|
+
export type StoredProgressOutcome =
|
|
9
|
+
| { ok: true; data: StoredProgressShape }
|
|
10
|
+
| { ok: false; reason: string };
|
|
11
|
+
|
|
12
|
+
const STORAGE_VERSION = 1;
|
|
13
|
+
const MIN_STEP_INDEX = 0;
|
|
14
|
+
|
|
15
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
16
|
+
return typeof value === 'object' && value !== null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isStringArray(value: unknown): value is string[] {
|
|
20
|
+
return Array.isArray(value) && value.every((v) => typeof v === 'string');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isIsoDateString(value: unknown): value is string {
|
|
24
|
+
if (typeof value !== 'string') return false;
|
|
25
|
+
const ms = Date.parse(value);
|
|
26
|
+
return Number.isFinite(ms);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function checkStoredProgress(raw: string | null): StoredProgressOutcome {
|
|
30
|
+
if (raw === null) return { ok: false, reason: 'no stored progress' };
|
|
31
|
+
let parsed: unknown;
|
|
32
|
+
try {
|
|
33
|
+
parsed = JSON.parse(raw);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
reason: e instanceof Error ? e.message : 'invalid JSON',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (!isObject(parsed)) {
|
|
41
|
+
return { ok: false, reason: 'stored payload must be an object' };
|
|
42
|
+
}
|
|
43
|
+
if (parsed.version !== STORAGE_VERSION) {
|
|
44
|
+
return { ok: false, reason: `unsupported storage version: ${String(parsed.version)}` };
|
|
45
|
+
}
|
|
46
|
+
if (typeof parsed.currentStep !== 'number' || parsed.currentStep < MIN_STEP_INDEX) {
|
|
47
|
+
return { ok: false, reason: 'currentStep must be a non-negative number' };
|
|
48
|
+
}
|
|
49
|
+
if (!isStringArray(parsed.completedSteps)) {
|
|
50
|
+
return { ok: false, reason: 'completedSteps must be a string array' };
|
|
51
|
+
}
|
|
52
|
+
if (!isObject(parsed.collected)) {
|
|
53
|
+
return { ok: false, reason: 'collected must be an object' };
|
|
54
|
+
}
|
|
55
|
+
if (!isIsoDateString(parsed.startedAt) || !isIsoDateString(parsed.lastUpdated)) {
|
|
56
|
+
return { ok: false, reason: 'startedAt and lastUpdated must be ISO date strings' };
|
|
57
|
+
}
|
|
58
|
+
return { ok: true, data: parsed as unknown as StoredProgressShape };
|
|
59
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { checkFlowStructure } from './flow-structure-check';
|
|
3
|
+
import { checkScreenConfig, getRecognizedScreenTypes } from './screen-config-check';
|
|
4
|
+
import { checkStoredProgress } from './stored-progress-check';
|
|
5
|
+
import {
|
|
6
|
+
isRecognizedPermissionStatus,
|
|
7
|
+
normalizePermissionStatus,
|
|
8
|
+
} from './permission-status-check';
|
|
9
|
+
import type { OnboardingScreen } from '../types/screens';
|
|
10
|
+
|
|
11
|
+
const welcome = (id = 'a'): OnboardingScreen => ({
|
|
12
|
+
id,
|
|
13
|
+
type: 'welcome',
|
|
14
|
+
title: 'T',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('flow-structure-check (§23.1)', () => {
|
|
18
|
+
it('rejects empty flows', () => {
|
|
19
|
+
expect(checkFlowStructure([])).toEqual({
|
|
20
|
+
ok: false,
|
|
21
|
+
reason: 'flow must contain at least one screen',
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('rejects duplicate step ids', () => {
|
|
26
|
+
const outcome = checkFlowStructure([welcome('dup'), welcome('dup')]);
|
|
27
|
+
expect(outcome.ok).toBe(false);
|
|
28
|
+
if (!outcome.ok) expect(outcome.reason).toMatch(/duplicate step id/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('accepts a valid flow and propagates per-screen failures', () => {
|
|
32
|
+
expect(checkFlowStructure([welcome('a'), welcome('b')])).toEqual({ ok: true });
|
|
33
|
+
const bad = checkFlowStructure([
|
|
34
|
+
welcome('a'),
|
|
35
|
+
{ id: 'g', type: 'goal-question', question: 'Q', options: [], onChange: () => undefined },
|
|
36
|
+
]);
|
|
37
|
+
expect(bad.ok).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('screen-config-check (§23.2)', () => {
|
|
42
|
+
it('rejects malformed screens', () => {
|
|
43
|
+
// @ts-expect-error -- deliberately malformed input (null is not a screen)
|
|
44
|
+
expect(checkScreenConfig(null).ok).toBe(false);
|
|
45
|
+
// @ts-expect-error -- deliberately malformed input (missing id)
|
|
46
|
+
expect(checkScreenConfig({ type: 'welcome' }).ok).toBe(false);
|
|
47
|
+
expect(checkScreenConfig({ id: '', type: 'welcome' } as OnboardingScreen).ok).toBe(false);
|
|
48
|
+
expect(
|
|
49
|
+
checkScreenConfig({ id: 'x', type: 'unknown' } as unknown as OnboardingScreen).ok,
|
|
50
|
+
).toBe(false);
|
|
51
|
+
expect(
|
|
52
|
+
checkScreenConfig({ id: 'x', type: 'goal-question' } as OnboardingScreen).ok,
|
|
53
|
+
).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('requires content for content-driven screens', () => {
|
|
57
|
+
expect(
|
|
58
|
+
checkScreenConfig({
|
|
59
|
+
id: 'p',
|
|
60
|
+
type: 'pain-points',
|
|
61
|
+
painPoints: [],
|
|
62
|
+
} as unknown as OnboardingScreen).ok,
|
|
63
|
+
).toBe(false);
|
|
64
|
+
expect(
|
|
65
|
+
checkScreenConfig({
|
|
66
|
+
id: 't',
|
|
67
|
+
type: 'tinder-cards',
|
|
68
|
+
statements: [],
|
|
69
|
+
} as unknown as OnboardingScreen).ok,
|
|
70
|
+
).toBe(false);
|
|
71
|
+
expect(
|
|
72
|
+
checkScreenConfig({ id: 'pp', type: 'permission-priming' } as unknown as OnboardingScreen)
|
|
73
|
+
.ok,
|
|
74
|
+
).toBe(false);
|
|
75
|
+
expect(
|
|
76
|
+
checkScreenConfig({ id: 'w', type: 'paywall', price: 9 } as unknown as OnboardingScreen).ok,
|
|
77
|
+
).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('accepts a well-formed screen', () => {
|
|
81
|
+
expect(checkScreenConfig(welcome('ok'))).toEqual({ ok: true });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('knows every screen type the orchestrator can render', () => {
|
|
85
|
+
expect(getRecognizedScreenTypes().size).toBe(14);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('stored-progress-check (§23.3)', () => {
|
|
90
|
+
const validPayload = JSON.stringify({
|
|
91
|
+
version: 1,
|
|
92
|
+
currentStep: 2,
|
|
93
|
+
completedSteps: ['a', 'b'],
|
|
94
|
+
collected: { a: { selected: ['x'] } },
|
|
95
|
+
startedAt: '2026-01-01T10:00:00.000Z',
|
|
96
|
+
lastUpdated: '2026-01-01T10:01:00.000Z',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('returns not-ok for missing, corrupt, or future-version payloads', () => {
|
|
100
|
+
expect(checkStoredProgress(null)).toEqual({ ok: false, reason: 'no stored progress' });
|
|
101
|
+
expect(checkStoredProgress('{ malformed').ok).toBe(false);
|
|
102
|
+
const v2 = checkStoredProgress(JSON.stringify({ version: 2 }));
|
|
103
|
+
expect(v2.ok).toBe(false);
|
|
104
|
+
if (!v2.ok) expect(v2.reason).toMatch(/unsupported storage version/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('rejects shape violations inside an otherwise-valid envelope', () => {
|
|
108
|
+
expect(
|
|
109
|
+
checkStoredProgress(
|
|
110
|
+
JSON.stringify({
|
|
111
|
+
version: 1,
|
|
112
|
+
currentStep: -1,
|
|
113
|
+
completedSteps: [],
|
|
114
|
+
collected: {},
|
|
115
|
+
startedAt: '2026-01-01T10:00:00.000Z',
|
|
116
|
+
lastUpdated: '2026-01-01T10:00:00.000Z',
|
|
117
|
+
}),
|
|
118
|
+
).ok,
|
|
119
|
+
).toBe(false);
|
|
120
|
+
expect(
|
|
121
|
+
checkStoredProgress(
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
version: 1,
|
|
124
|
+
currentStep: 0,
|
|
125
|
+
completedSteps: 'nope',
|
|
126
|
+
collected: {},
|
|
127
|
+
startedAt: '2026-01-01T10:00:00.000Z',
|
|
128
|
+
lastUpdated: '2026-01-01T10:00:00.000Z',
|
|
129
|
+
}),
|
|
130
|
+
).ok,
|
|
131
|
+
).toBe(false);
|
|
132
|
+
expect(
|
|
133
|
+
checkStoredProgress(
|
|
134
|
+
JSON.stringify({
|
|
135
|
+
version: 1,
|
|
136
|
+
currentStep: 0,
|
|
137
|
+
completedSteps: [],
|
|
138
|
+
collected: {},
|
|
139
|
+
startedAt: 'not a date',
|
|
140
|
+
lastUpdated: '2026-01-01T10:00:00.000Z',
|
|
141
|
+
}),
|
|
142
|
+
).ok,
|
|
143
|
+
).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('accepts a valid v1 payload and returns it typed', () => {
|
|
147
|
+
const outcome = checkStoredProgress(validPayload);
|
|
148
|
+
expect(outcome.ok).toBe(true);
|
|
149
|
+
if (outcome.ok) {
|
|
150
|
+
expect(outcome.data.currentStep).toBe(2);
|
|
151
|
+
expect(outcome.data.completedSteps).toEqual(['a', 'b']);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('permission-status-check (§23.4)', () => {
|
|
157
|
+
it('normalizes browser-level permission strings', () => {
|
|
158
|
+
expect(normalizePermissionStatus('granted')).toBe('granted');
|
|
159
|
+
expect(normalizePermissionStatus('denied')).toBe('denied');
|
|
160
|
+
expect(normalizePermissionStatus('default')).toBe('prompt');
|
|
161
|
+
expect(normalizePermissionStatus(null)).toBe('unsupported');
|
|
162
|
+
expect(normalizePermissionStatus('whatever')).toBe('unsupported');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('recognizes exactly the four internal statuses', () => {
|
|
166
|
+
expect(isRecognizedPermissionStatus('prompt')).toBe(true);
|
|
167
|
+
expect(isRecognizedPermissionStatus('revoked')).toBe(false);
|
|
168
|
+
expect(isRecognizedPermissionStatus(42)).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -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
|
}
|