@elevasis/sdk 1.3.0 → 1.5.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/dist/cli.cjs +75 -8
- package/dist/index.d.ts +1464 -749
- package/dist/index.js +74 -7
- package/dist/types/worker/adapters/crm.d.ts +20 -0
- package/dist/types/worker/adapters/index.d.ts +2 -0
- package/dist/types/worker/adapters/projects.d.ts +20 -0
- package/dist/worker/index.js +41 -1
- package/package.json +2 -2
- package/reference/_navigation.md +103 -5
- package/reference/_reference-manifest.json +72 -0
- package/reference/deployment/provided-features.mdx +64 -25
- package/reference/framework/index.mdx +2 -2
- package/reference/framework/project-structure.mdx +10 -8
- package/reference/index.mdx +3 -3
- package/reference/packages/core/src/README.md +34 -0
- package/reference/packages/core/src/organization-model/README.md +94 -0
- package/reference/packages/ui/src/api/README.md +18 -0
- package/reference/packages/ui/src/auth/README.md +18 -0
- package/reference/packages/ui/src/components/README.md +24 -0
- package/reference/packages/ui/src/execution/README.md +16 -0
- package/reference/packages/ui/src/features/README.md +28 -0
- package/reference/packages/ui/src/graph/README.md +16 -0
- package/reference/packages/ui/src/hooks/README.md +24 -0
- package/reference/packages/ui/src/initialization/README.md +19 -0
- package/reference/packages/ui/src/organization/README.md +18 -0
- package/reference/packages/ui/src/profile/README.md +19 -0
- package/reference/packages/ui/src/provider/README.md +31 -0
- package/reference/packages/ui/src/router/README.md +18 -0
- package/reference/packages/ui/src/sse/README.md +13 -0
- package/reference/packages/ui/src/theme/README.md +23 -0
- package/reference/packages/ui/src/types/README.md +16 -0
- package/reference/packages/ui/src/utils/README.md +18 -0
- package/reference/packages/ui/src/zustand/README.md +18 -0
- package/reference/resources/patterns.mdx +54 -8
- package/reference/scaffold/core/organization-graph.mdx +262 -0
- package/reference/scaffold/core/organization-model.mdx +257 -0
- package/reference/scaffold/index.mdx +59 -0
- package/reference/scaffold/operations/workflow-recipes.md +419 -0
- package/reference/scaffold/recipes/add-a-feature.md +142 -0
- package/reference/scaffold/recipes/add-a-resource.md +163 -0
- package/reference/scaffold/recipes/gate-by-feature-or-admin.md +152 -0
- package/reference/scaffold/recipes/index.md +32 -0
- package/reference/scaffold/reference/contracts.md +1044 -0
- package/reference/scaffold/reference/feature-registry.md +30 -0
- package/reference/scaffold/reference/glossary.md +88 -0
- package/reference/scaffold/ui/composition-extensibility.mdx +216 -0
- package/reference/scaffold/ui/customization.md +239 -0
- package/reference/scaffold/ui/feature-flags-and-gating.md +265 -0
- package/reference/scaffold/ui/feature-shell.mdx +241 -0
- package/reference/scaffold/ui/recipes.md +418 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Feature Flags & Gating
|
|
3
|
+
description: End-to-end recipe for the three gating concepts -- featureKey nav visibility, FeatureGuard route gate, AdminGuard / requiresAdmin. One doc resolves confusion that scored 2/5 in scaffold eval.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Feature Flags & Gating
|
|
7
|
+
|
|
8
|
+
**Status: 🟢 Stable**
|
|
9
|
+
|
|
10
|
+
This doc resolves the three-concept confusion that trips up most agents building new features. Read
|
|
11
|
+
the overview table first, then follow the end-to-end chain for the template's actual grouped-key
|
|
12
|
+
plus alias flow.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
Three distinct mechanisms control access in the template. They operate at different layers and are not interchangeable.
|
|
19
|
+
|
|
20
|
+
| Concept | What it gates | Where it applies | How it works |
|
|
21
|
+
| ------------------------------ | -------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
22
|
+
| `featureKey` on a nav entry | Whether a nav item is visible in the sidebar | `navEntry` in a feature manifest or `nav-items.ts` | The shell reads the organization model and hides items whose `featureKey` is disabled |
|
|
23
|
+
| `FeatureGuard` component | Whether a route subtree is accessible | Route layout files (`ui/src/routes/*.tsx`) | Reads org + membership access and redirects with a notification if the feature is off |
|
|
24
|
+
| `AdminGuard` / `requiresAdmin` | Whether an admin-only surface is accessible | Route layout files and manifest nav entries | Reads `profile.is_platform_admin`; redirects non-admins to `/` |
|
|
25
|
+
|
|
26
|
+
The three concepts work together but are independent. Hiding a nav item does not protect a route -- you must also wrap the route in `FeatureGuard`. Showing a nav item does not grant access.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## End-to-End Chain
|
|
31
|
+
|
|
32
|
+
This section walks through the template's existing acquisition flow. In this scaffold, `crm` and
|
|
33
|
+
`lead-gen` are route-level aliases that map to the grouped org-model key `acquisition`.
|
|
34
|
+
|
|
35
|
+
If you need a brand-new shell feature key such as `analytics`, that is not a template-only change.
|
|
36
|
+
It requires updating the published `@elevasis/core` organization-model contract and any shared
|
|
37
|
+
`@elevasis/ui` feature manifests that should consume it.
|
|
38
|
+
|
|
39
|
+
### Step 1: Declare the grouped org-model gate in `organization-model.ts`
|
|
40
|
+
|
|
41
|
+
File: `foundations/config/organization-model.ts`
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
const foundationOrganizationModelOverride = defineOrganizationModel({
|
|
45
|
+
features: {
|
|
46
|
+
enabled: {
|
|
47
|
+
acquisition: true
|
|
48
|
+
},
|
|
49
|
+
labels: {
|
|
50
|
+
acquisition: 'Acquisition'
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
// ... rest of model
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The `features.enabled` record is the source of truth for the published org-model gates. In the
|
|
58
|
+
current template, those keys are the grouped platform keys:
|
|
59
|
+
`acquisition`, `delivery`, `operations`, `monitoring`, `settings`, `seo`, and `calibration`.
|
|
60
|
+
|
|
61
|
+
Further down in the same file, the template adapts that canonical model into shell-friendly aliases:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const featuresEnabled: Record<FoundationFeatureKey, boolean> = {
|
|
65
|
+
acquisition: resolvedOrganizationModel.features.enabled.acquisition,
|
|
66
|
+
crm: resolvedOrganizationModel.features.enabled.acquisition,
|
|
67
|
+
'lead-gen': resolvedOrganizationModel.features.enabled.acquisition
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
That alias layer is why route guards can ask for `crm` or `lead-gen` even though the published
|
|
72
|
+
organization-model contract only knows about `acquisition`.
|
|
73
|
+
|
|
74
|
+
### Step 2: Wire `featureKey` on a nav item
|
|
75
|
+
|
|
76
|
+
Nav items come from one of two places: the manifest `navEntry` (for features registered through `ElevasisFeaturesProvider`) or the local `nav-items.ts` config (for app-local nav).
|
|
77
|
+
|
|
78
|
+
**Manifest nav entry** (preferred for platform features):
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// Published shared manifest
|
|
82
|
+
import type { FeatureModule } from '@elevasis/ui/provider'
|
|
83
|
+
import { crmManifest } from '@elevasis/ui/features/crm'
|
|
84
|
+
|
|
85
|
+
const manifest: FeatureModule = crmManifest
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`accessFeatureKey` on the `FeatureModule` is what drives whether the entire shared feature
|
|
89
|
+
(including its nav entry) is visible. In the published manifests, `crm` and `lead-gen` both use
|
|
90
|
+
`accessFeatureKey: 'acquisition'`; `delivery` uses `accessFeatureKey: 'delivery'`. The shell hides
|
|
91
|
+
the nav entry when org or membership access disables that grouped key.
|
|
92
|
+
|
|
93
|
+
**Local nav-items.ts** (for one-off items not part of a registered feature):
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// ui/src/config/nav-items.ts
|
|
97
|
+
export const navItems: ExtendedLinksGroupProps[] = [
|
|
98
|
+
{ label: 'Dashboard', icon: IconDashboard, link: '/' },
|
|
99
|
+
{ label: 'CRM', icon: IconBriefcase, link: '/crm', featureKey: 'crm' }
|
|
100
|
+
]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
When `featureKey` is set on a nav item in `nav-items.ts`, the shell hides that item if
|
|
104
|
+
`isFeatureEnabled(featureKey)` returns false. Template-local aliases like `crm`, `lead-gen`, and
|
|
105
|
+
`projects` work here because the adapted `organizationModel` exposed by foundations includes them.
|
|
106
|
+
|
|
107
|
+
### Step 3: Wrap the route in `FeatureGuard`
|
|
108
|
+
|
|
109
|
+
File: `ui/src/routes/crm.tsx` (TanStack Router layout file for the `/crm` subtree)
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
import { ProtectedRoute } from '@/features/auth'
|
|
113
|
+
import { FeatureGuard } from '@/features/auth/guards/FeatureGuard'
|
|
114
|
+
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
|
115
|
+
|
|
116
|
+
export const Route = createFileRoute('/crm')({
|
|
117
|
+
component: RouteComponent
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
function RouteComponent() {
|
|
121
|
+
return (
|
|
122
|
+
<ProtectedRoute>
|
|
123
|
+
<FeatureGuard featureKey="crm">
|
|
124
|
+
<Outlet />
|
|
125
|
+
</FeatureGuard>
|
|
126
|
+
</ProtectedRoute>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`FeatureGuard` must always be nested inside `ProtectedRoute`. It reads from `useFeatureAccess()`
|
|
132
|
+
(the template's wrapper around `createFeatureAccessHook`), checks the effective org-plus-membership
|
|
133
|
+
access, and redirects to `/` with a Mantine notification if the feature is off. The layout pattern
|
|
134
|
+
means every child route under `/crm` is automatically protected without repeating the guard.
|
|
135
|
+
|
|
136
|
+
All existing feature routes in the template follow this exact pattern: `operations.tsx`,
|
|
137
|
+
`projects.tsx`, `lead-gen.tsx`, `crm.tsx`, and `monitoring.tsx`.
|
|
138
|
+
|
|
139
|
+
### Step 4: Check in a component via `useFeatureAccess`
|
|
140
|
+
|
|
141
|
+
For conditional UI within a component (showing or hiding a button, panel, or section based on a feature flag), use the `useFeatureAccess` hook directly.
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
import { useFeatureAccess } from '@/shared/hooks/useFeatureAccess'
|
|
145
|
+
|
|
146
|
+
function Dashboard() {
|
|
147
|
+
const { hasFeature } = useFeatureAccess()
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div>
|
|
151
|
+
<CoreMetrics />
|
|
152
|
+
{hasFeature('crm') && <CrmSummaryPanel />}
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`hasFeature` returns a boolean. Use it for render-conditional logic only. Do not use it as a substitute for `FeatureGuard` on a route -- hiding a component does not prevent direct URL access.
|
|
159
|
+
|
|
160
|
+
The hook is defined in `ui/src/shared/hooks/useFeatureAccess.ts`. It wraps
|
|
161
|
+
`createFeatureAccessHook` from `@elevasis/ui/hooks` and extends it with template-specific key
|
|
162
|
+
aliases (`crm` and `lead-gen` alias to `acquisition`; `projects` aliases to `delivery`).
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Admin Gating
|
|
167
|
+
|
|
168
|
+
Admin gating is separate from feature gating. It restricts surfaces to users with `profile.is_platform_admin === true`.
|
|
169
|
+
|
|
170
|
+
### `requiresAdmin` on a manifest nav entry
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
// In a FeatureNavEntry
|
|
174
|
+
navEntry: {
|
|
175
|
+
label: 'Admin',
|
|
176
|
+
icon: IconShield,
|
|
177
|
+
link: '/admin',
|
|
178
|
+
requiresAdmin: true
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Setting `requiresAdmin: true` on a `FeatureNavEntry` causes the shell to hide that nav item for non-admin users. This is purely a visibility concern -- the route is still accessible by URL. Always combine with a route-level `AdminGuard`.
|
|
183
|
+
|
|
184
|
+
### `AdminGuard` in a route
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
// ui/src/routes/admin.tsx
|
|
188
|
+
import { ProtectedRoute } from '@/features/auth'
|
|
189
|
+
import { AdminGuard } from '@elevasis/ui/auth'
|
|
190
|
+
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
|
191
|
+
|
|
192
|
+
export const Route = createFileRoute('/admin')({
|
|
193
|
+
component: RouteComponent
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
function RouteComponent() {
|
|
197
|
+
return (
|
|
198
|
+
<ProtectedRoute>
|
|
199
|
+
<AdminGuard fallback={<AppShellLoader />}>
|
|
200
|
+
<Outlet />
|
|
201
|
+
</AdminGuard>
|
|
202
|
+
</ProtectedRoute>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`AdminGuard` reads `profile.is_platform_admin` from the initialization context. Non-admins are redirected to `redirectTo` (default `/`). Always nest inside `ProtectedRoute` so that the user profile is loaded before the guard runs.
|
|
208
|
+
|
|
209
|
+
### `AdminGuard` in a component
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
import { AdminGuard } from '@elevasis/ui/auth'
|
|
213
|
+
|
|
214
|
+
function SettingsPage() {
|
|
215
|
+
return (
|
|
216
|
+
<>
|
|
217
|
+
<GeneralSettings />
|
|
218
|
+
<AdminGuard>
|
|
219
|
+
<DangerZone />
|
|
220
|
+
</AdminGuard>
|
|
221
|
+
</>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Use this pattern when only a section of a page is admin-only, not the entire route. The rest of the page renders normally for all authenticated users.
|
|
227
|
+
|
|
228
|
+
### When to use each
|
|
229
|
+
|
|
230
|
+
- `requiresAdmin` on `navEntry`: hide the nav item for non-admins (cosmetic only, always pair with route guard)
|
|
231
|
+
- `AdminGuard` wrapping a route's `Outlet`: protect the entire route subtree
|
|
232
|
+
- `AdminGuard` wrapping a component section: protect part of a page
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Import Paths
|
|
237
|
+
|
|
238
|
+
| Symbol | Import path | Notes |
|
|
239
|
+
| ------------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
240
|
+
| `FeatureGuard` | `@/features/auth/guards/FeatureGuard` | Template-local wrapper; uses `useFeatureAccess` from `@/shared/hooks/useFeatureAccess` |
|
|
241
|
+
| `AdminGuard` | `@elevasis/ui/auth` | Published from `@elevasis/ui` |
|
|
242
|
+
| `useFeatureAccess` | `@/shared/hooks/useFeatureAccess` | Template-local wrapper around `createFeatureAccessHook` from `@elevasis/ui/hooks` |
|
|
243
|
+
| `createFeatureAccessHook` | `@elevasis/ui/hooks` | Factory for building a feature-access hook; already consumed by the template wrapper |
|
|
244
|
+
| `ProtectedRoute` | `@elevasis/ui/auth` or `@/features/auth` | Ensures user is authenticated before any guard runs |
|
|
245
|
+
|
|
246
|
+
The template's `FeatureGuard` (`@/features/auth/guards/FeatureGuard`) is not the same as the published `FeatureGuard` from `@elevasis/ui/features`. The template version closes over the local `useFeatureAccess` hook and the local organization model labels, so it knows your feature key aliases. Use the template-local version.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Common Mistakes
|
|
251
|
+
|
|
252
|
+
- **Inventing a new org-model key in the template only.** The published
|
|
253
|
+
`OrganizationModelFeatureKey` union is fixed in `@elevasis/core`. Template-local aliases like
|
|
254
|
+
`crm`, `lead-gen`, and `projects` work because foundations maps them onto grouped keys; a brand
|
|
255
|
+
new shell key such as `analytics` requires a core/package contract change, not just a doc tweak.
|
|
256
|
+
|
|
257
|
+
- **Using `FeatureGuard` on a nav item instead of a route.** `FeatureGuard` is a React component that redirects on mount. Placing it inside a nav item or link will fire a redirect whenever the sidebar renders. Gate nav visibility with `featureKey` on the nav entry or `accessFeatureKey` on the manifest; gate route access with `FeatureGuard` in the route layout.
|
|
258
|
+
|
|
259
|
+
- **Confusing `requiresAdmin` with `AdminGuard`.** `requiresAdmin` on a `FeatureNavEntry` is a display hint read by the shell nav renderer -- it hides the nav item but does not block the route. `AdminGuard` is a React component that actively redirects. You need both if you want the nav hidden and the route blocked.
|
|
260
|
+
|
|
261
|
+
- **Placing `FeatureGuard` or `AdminGuard` outside `ProtectedRoute`.** Both guards depend on the user profile being loaded. Unauthenticated users will see a redirect loop or a blank screen if the profile is not yet available. Always nest guards inside `ProtectedRoute`.
|
|
262
|
+
|
|
263
|
+
- **Using `hasFeature` from `useFeatureAccess` as a route guard.** `hasFeature` is for conditional
|
|
264
|
+
rendering inside components. A user can still navigate directly to a URL and reach the route
|
|
265
|
+
component. Use `FeatureGuard` in the route file for actual access control.
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Feature Shell & Provider Runtime
|
|
3
|
+
description: Organization OS UI Shell Runtime and Features layer architecture covering FeatureModule manifests, ElevasisFeaturesProvider, route-to-sidebar dispatch, and organization-model-aware runtime resolution.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
Within Organization OS, the feature shell spans two layers: the **UI Shell Runtime** that resolves shared nav/sidebar behavior, and the **Features** layer that declares what each feature contributes. It lets command-center and external template consumers compose the same nav, sidebars, and subshell routes from a small set of manifests. It is intentionally a thin layer: manifests describe what a feature contributes, the provider resolves that into a runtime shell model, and consumers still own their own routes, branding, and app-local nav.
|
|
9
|
+
|
|
10
|
+
Three layers participate, and the word "feature" means something different in each:
|
|
11
|
+
|
|
12
|
+
- **Platform capabilities** -- product-facing areas documented under `technical/features/` (Execution Engine, Workflows, Agents, Operations, etc.). These are not one-to-one with shell features.
|
|
13
|
+
- **Shell features** -- features in `packages/ui/src/features/*`. Seven are manifest-backed (`lead-gen`, `crm`, `delivery`, `operations`, `monitoring`, `settings`, `seo`); two are utility features without manifests (`auth`, `dashboard`).
|
|
14
|
+
- **Organization-model features** -- grouped semantic/access keys in `@repo/core/organization-model` (`acquisition`, `delivery`, `operations`, `monitoring`, `settings`, `seo`, `calibration`). See [Organization Model](../core/organization-model.mdx).
|
|
15
|
+
|
|
16
|
+
One shell feature can contain several platform capabilities; shell and organization-model keys are different concepts.
|
|
17
|
+
|
|
18
|
+
## Source of Truth
|
|
19
|
+
|
|
20
|
+
- `packages/ui/src/features/registry/types.ts` -- `FeatureModule` contract
|
|
21
|
+
- `packages/ui/src/features/registry/manifests.ts` -- published `FEATURE_MANIFESTS` convenience map
|
|
22
|
+
- `packages/ui/src/provider/ElevasisFeaturesProvider.tsx` -- runtime composition
|
|
23
|
+
- `packages/ui/src/provider/FeatureShell.tsx` -- route-to-sidebar dispatch
|
|
24
|
+
- `packages/ui/src/provider/resolvers/{RouteResolver,NavResolver}.ts` -- pure path/nav helpers
|
|
25
|
+
- `packages/ui/src/provider/validateManifests.ts` -- startup-time manifest validation
|
|
26
|
+
- `packages/ui/src/provider/published.ts` -- headless published barrel
|
|
27
|
+
- `apps/command-center/src/routes/__root.tsx` -- reference composition
|
|
28
|
+
|
|
29
|
+
## The FeatureModule Contract
|
|
30
|
+
|
|
31
|
+
A `FeatureModule` describes one shell feature's contribution. Fields:
|
|
32
|
+
|
|
33
|
+
- `key` -- unique stable identifier (e.g., `'crm'`, `'delivery'`)
|
|
34
|
+
- `accessFeatureKey` -- **required**; the key `useFeatureAccess()` checks for gating
|
|
35
|
+
- `domainIds`, `capabilityIds` -- semantic references into the organization model
|
|
36
|
+
- `navEntry` -- top-level nav contribution (label, icon, optional `link`, optional nested `links`, optional `requiresAdmin`, optional onboarding-tour ID, optional `featureKey` override when nav identity must diverge from access identity)
|
|
37
|
+
- `sidebar` -- optional `ComponentType` for the feature's subshell sidebar
|
|
38
|
+
- `subshellRoutes` -- routes the feature owns under its subshell
|
|
39
|
+
- `organizationGraph` -- **Operations-only**; bridges a manifest to an organization-model surface (ignored by other features)
|
|
40
|
+
|
|
41
|
+
`FeatureModule.label` has been removed; nav labels resolve from `navEntry.label` or from organization-model feature labels.
|
|
42
|
+
|
|
43
|
+
Manifests are validated at provider registration via `validateManifests()`. Unknown `accessFeatureKey`, `domainIds`, or `capabilityIds` (measured against the resolved organization model) throw with all violations collected into one error.
|
|
44
|
+
|
|
45
|
+
### ResolvedFeatureModule
|
|
46
|
+
|
|
47
|
+
`ResolvedFeatureModule` extends `FeatureModule` with two additional fields added during provider resolution:
|
|
48
|
+
|
|
49
|
+
- `access: ResolvedFeatureAccess` -- `{ featureKey: string, enabled: boolean }` -- the resolved access state for this feature
|
|
50
|
+
- `semantics: ResolvedFeatureSemantics` -- `{ domainIds, capabilityIds, surfaceIds, surfaces }` -- merged semantic identifiers derived from both manifest declarations and organization-model surface data
|
|
51
|
+
|
|
52
|
+
`ResolvedFeatureSemantics.surfaces` carries the full `OrganizationModelSurface[]` objects (not just IDs), so consumers can inspect surface metadata without a separate lookup.
|
|
53
|
+
|
|
54
|
+
## Provider Architecture
|
|
55
|
+
|
|
56
|
+
The provider layer is composed of several focused context providers. `ElevasisCoreProvider` and `ElevasisFeaturesProvider` are the two consumer-facing entry points; the others are internal building blocks also published for advanced use.
|
|
57
|
+
|
|
58
|
+
### ElevasisCoreProvider
|
|
59
|
+
|
|
60
|
+
`ElevasisCoreProvider` is the headless root provider for Elevasis-powered applications. It is pure auth + API with no style side-effects -- no CSS variables, no `data-elevasis-scheme`, no font loading. When `apiUrl` is provided it composes the full provider stack:
|
|
61
|
+
|
|
62
|
+
`QueryClientProvider` -> `AuthProvider` -> `ApiClientProvider` -> `ProfileProvider` -> `OrganizationProvider` -> `ElevasisServiceProvider` -> `NotificationProvider` -> `InitializationProvider`
|
|
63
|
+
|
|
64
|
+
Consumers that need Mantine theming use `ElevasisUIProvider` (internal) or `ElevasisProvider` instead. `ElevasisCoreProvider` is exported from `packages/ui/src/provider/published.ts` for headless SDK consumers.
|
|
65
|
+
|
|
66
|
+
### ElevasisServiceProvider
|
|
67
|
+
|
|
68
|
+
`ElevasisServiceProvider` (from `ElevasisServiceContext.tsx`) is the standalone service context. It accepts three props directly: `apiRequest`, `organizationId`, and `isReady`. `ElevasisCoreProvider` composes this internally after org resolution; advanced consumers can mount it standalone for testing or embedding.
|
|
69
|
+
|
|
70
|
+
`useElevasisServices()` reads this context and throws if used outside a provider. Consumed throughout the shared component library wherever API requests are needed.
|
|
71
|
+
|
|
72
|
+
### AppearanceProvider
|
|
73
|
+
|
|
74
|
+
`AppearanceProvider` (from `AppearanceContext.tsx`) supplies an `AppearanceConfig` to the tree: `{ background?: ReactNode, loader?: ReactNode }`. The `background` field controls layers rendered behind app content; `loader` controls loading-state elements. Defaults are set in the provider rather than at context creation to avoid importing heavy visual components at module scope.
|
|
75
|
+
|
|
76
|
+
`useAppearance()` reads this context and throws if used outside a provider.
|
|
77
|
+
|
|
78
|
+
### NotificationProvider
|
|
79
|
+
|
|
80
|
+
`NotificationProvider` (from `NotificationContext.tsx`) accepts a pluggable `NotificationAdapter` for routing notifications to any library. The adapter interface is `{ success, error, info, warning, apiError }`. `ElevasisUIProvider` wires the Mantine adapter automatically.
|
|
81
|
+
|
|
82
|
+
When no provider is present in the tree, `useNotificationAdapter()` falls back to a console-based adapter (not a no-op), so template consumers work without Mantine. The hook never throws.
|
|
83
|
+
|
|
84
|
+
### Memoization Strategy
|
|
85
|
+
|
|
86
|
+
All computed values inside `ElevasisFeaturesProvider` are wrapped in `useMemo()` with precise dependency tracking. Resolved features, nav items, shell model, organization graph, and the context value object are each memoized separately so downstream consumers only re-render when their specific inputs change.
|
|
87
|
+
|
|
88
|
+
## Provider Responsibilities
|
|
89
|
+
|
|
90
|
+
`ElevasisFeaturesProvider` owns:
|
|
91
|
+
|
|
92
|
+
- registration of shared feature manifests
|
|
93
|
+
- feature-flag-aware nav contribution
|
|
94
|
+
- route-to-sidebar subshell dispatch (via `shellRuntime.resolveRoute`)
|
|
95
|
+
- provider-scoped shared runtime context
|
|
96
|
+
- resolved shell-model composition and shell-native route matching
|
|
97
|
+
- organization-model-aware nav label and path resolution
|
|
98
|
+
- resolved feature output (no legacy `shellModule` wrapper)
|
|
99
|
+
|
|
100
|
+
It does **not** own:
|
|
101
|
+
|
|
102
|
+
- TanStack file-based route registration
|
|
103
|
+
- app branding, topbar behavior, admin entries, assistant/context glue
|
|
104
|
+
|
|
105
|
+
Consumers keep thin local route wrappers and app-local nav where needed.
|
|
106
|
+
|
|
107
|
+
## Runtime Flow
|
|
108
|
+
|
|
109
|
+
1. The app defines a manifest list (often by spreading `FEATURE_MANIFESTS` and overriding entries).
|
|
110
|
+
2. The app optionally resolves an organization model and passes it to the provider.
|
|
111
|
+
3. The provider combines `useFeatureAccess()` with organization-model feature state to compute enabled features.
|
|
112
|
+
4. Nav labels and nav paths resolve from `organizationModel.features.labels` and `organizationModel.navigation.surfaces` when present.
|
|
113
|
+
5. The provider exposes `shellModel`, `shellRuntime`, resolved feature access, `organizationGraph`, and shared runtime inputs via `useElevasisFeatures()`.
|
|
114
|
+
6. `FeatureShell` calls `shellRuntime.resolveRoute(currentPath)`:
|
|
115
|
+
- `matched` -- render the feature's sidebar subshell
|
|
116
|
+
- `hidden` -- render `FeatureUnavailableState`
|
|
117
|
+
- `unmatched` -- render plain children (consumer owns the route)
|
|
118
|
+
|
|
119
|
+
A route resolves as `hidden` when the matched nav link meets either of these conditions: its `featureKey` fails the `isFeatureEnabled()` check, or its path matches an entry in `disabledSubsectionPaths`. Both checks are applied recursively through nested nav links.
|
|
120
|
+
|
|
121
|
+
Consumer nav derivation runs locally from `shellModel.navItems`; the provider no longer exposes `visibleNavItems`.
|
|
122
|
+
|
|
123
|
+
## Provider-Scoped Runtime Context
|
|
124
|
+
|
|
125
|
+
`useElevasisFeatures()` exposes:
|
|
126
|
+
|
|
127
|
+
- `shellModel` -- `{ navItems: ResolvedShellNavItem[] }` -- the full resolved nav list
|
|
128
|
+
- `shellRuntime` -- `{ resolveRoute: (path) => ResolvedShellRouteMatch }` -- route dispatch
|
|
129
|
+
- `resolvedFeatures` -- all `ResolvedFeatureModule[]` regardless of enabled state
|
|
130
|
+
- `enabledResolvedFeatures` -- filtered to `access.enabled === true`
|
|
131
|
+
- `timeRange`
|
|
132
|
+
- `operationsApiUrl`, `operationsSSEManager`
|
|
133
|
+
- `organizationModel`
|
|
134
|
+
- `organizationGraph` (resolved from `operations.organization-graph` surface; see [Organization Graph](../core/organization-graph.mdx))
|
|
135
|
+
- `disabledSubsectionPaths` -- used by consumers like `_template` to hide specific subsections (e.g., `/settings/appearance`)
|
|
136
|
+
- `isFeatureEnabled(key: string): boolean` -- checks both membership flag and organization-model feature state
|
|
137
|
+
- `getResolvedFeature(key: string): ResolvedFeatureModule | undefined` -- looks up a resolved feature by its `key` field (module key, not access key)
|
|
138
|
+
|
|
139
|
+
## Nav Resolution
|
|
140
|
+
|
|
141
|
+
### ResolvedShellNavItem
|
|
142
|
+
|
|
143
|
+
`ResolvedShellNavItem` extends `FeatureNavEntry` with two additional fields:
|
|
144
|
+
|
|
145
|
+
- `placement: 'primary' | 'bottom'` -- where the item appears in the shell nav; feature manifests always produce `'primary'`; `appShellOverrides.bottomNavItems` produce `'bottom'`
|
|
146
|
+
- `source: 'app' | 'feature'` -- whether the item came from a manifest (`'feature'`) or from `appShellOverrides` (`'app'`)
|
|
147
|
+
- `accessFeatureKey?: string` -- the feature key associated with this nav item for gating checks
|
|
148
|
+
|
|
149
|
+
`shellModel.navItems` contains the merged and filtered list of all `ResolvedShellNavItem`s from both manifest features and `appShellOverrides`.
|
|
150
|
+
|
|
151
|
+
### filterNavLinks
|
|
152
|
+
|
|
153
|
+
`filterNavLinks()` in `ElevasisFeaturesProvider.tsx` recursively removes gated and disabled-subsection links from a `FeatureNavLink[]` array. A link is removed if its `featureKey` fails `isFeatureEnabled()` or if its path matches any entry in `disabledSubsectionPaths`. The function recurses into nested `links` arrays before deciding whether a parent with now-empty children should be retained or dropped.
|
|
154
|
+
|
|
155
|
+
## Access Resolution & Key Aliasing
|
|
156
|
+
|
|
157
|
+
Shell feature-module keys (`crm`, `lead-gen`, `delivery`) differ from the organization-model grouped keys used by published membership config (`acquisition`, `delivery`). The published `@elevasis/ui` line preserves grouped-key compatibility:
|
|
158
|
+
|
|
159
|
+
- `createFeatureAccessHook` is the factory that produces `useFeatureAccess`. (Old name `createUseFeatureAccess` is re-exported as `@deprecated`.)
|
|
160
|
+
- `FEATURE_KEY_ALIASES` in `packages/ui/src/hooks/feature-access/aliases.ts` is the single authoritative alias map. `crm -> acquisition`, `lead-gen -> acquisition`, `projects -> delivery`.
|
|
161
|
+
- Shared manifests declare grouped `accessFeatureKey` (e.g., `crmManifest.accessFeatureKey = 'acquisition'`) so the published contract gates against `MembershipFeatureConfig` as written.
|
|
162
|
+
- The provider fallback for missing feature access identity is retired -- explicit `accessFeatureKey` is required.
|
|
163
|
+
|
|
164
|
+
## Published Surface
|
|
165
|
+
|
|
166
|
+
`packages/ui/src/provider/published.ts` is intentionally headless: it exports the provider, types, and hooks, but no Mantine-dependent visual provider pieces. External consumers can adopt the feature/provider contract without pulling the full internal visual surface.
|
|
167
|
+
|
|
168
|
+
All nine features are published as individual subpath exports from `@elevasis/ui`:
|
|
169
|
+
|
|
170
|
+
- `@elevasis/ui/features/auth` -- `ProtectedRoute`, `AdminGuard`, `FeatureGuard`, `useUserProfile` (utility feature; no manifest)
|
|
171
|
+
- `@elevasis/ui/features/dashboard` -- `Dashboard`, `ResourceOverview`, `RecentExecutionsByResource`, `UnresolvedErrorsTeaser` (utility feature; no manifest)
|
|
172
|
+
- `@elevasis/ui/features/crm`
|
|
173
|
+
- `@elevasis/ui/features/delivery`
|
|
174
|
+
- `@elevasis/ui/features/lead-gen`
|
|
175
|
+
- `@elevasis/ui/features/monitoring`
|
|
176
|
+
- `@elevasis/ui/features/operations`
|
|
177
|
+
- `@elevasis/ui/features/seo`
|
|
178
|
+
- `@elevasis/ui/features/settings`
|
|
179
|
+
|
|
180
|
+
The `auth` and `dashboard` features are not in `FEATURE_MANIFESTS` and are not registered with `ElevasisFeaturesProvider`. They are standalone published feature modules consumed directly by host apps.
|
|
181
|
+
|
|
182
|
+
## Composition Patterns
|
|
183
|
+
|
|
184
|
+
### Command-center composition
|
|
185
|
+
|
|
186
|
+
`apps/command-center/src/routes/__root.tsx`:
|
|
187
|
+
|
|
188
|
+
1. imports the mounted manifest list
|
|
189
|
+
2. resolves `COMMAND_CENTER_ORGANIZATION_MODEL`
|
|
190
|
+
3. passes manifests, organization model, time range, operations API URL, and SSE manager to `ElevasisFeaturesProvider`
|
|
191
|
+
4. keeps app-local nav entries for dashboard, admin, archive (passed into `appShellOverrides`)
|
|
192
|
+
5. derives filtered nav locally from `shellModel.navItems`
|
|
193
|
+
6. lets `FeatureShell` dispatch subshell sidebars for matched routes
|
|
194
|
+
|
|
195
|
+
### appShellOverrides
|
|
196
|
+
|
|
197
|
+
`appShellOverrides` is an `AppShellOverrides` prop on `ElevasisFeaturesProvider` that lets the host app inject nav items outside the manifest registry:
|
|
198
|
+
|
|
199
|
+
- `primaryNavItems?: FeatureNavEntry[]` -- prepended before feature nav items in the primary nav position; organization-model label and path resolution is applied to these entries
|
|
200
|
+
- `bottomNavItems?: FeatureNavEntry[]` -- rendered at the bottom of the shell nav; also subject to organization-model resolution and `filterNavLinks` filtering
|
|
201
|
+
|
|
202
|
+
Both arrays go through the same `isFeatureEnabled()` and `disabledSubsectionPaths` filtering as manifest-derived items. Items whose `featureKey` is disabled, or that have only disabled nested links and no direct `link`, are dropped entirely.
|
|
203
|
+
|
|
204
|
+
Nine feature modules are published (see **Published Surface** above). Six are currently mounted in command-center: `leadGenManifest`, `crmManifest`, `deliveryManifest`, `operationsManifest`, `monitoringManifest`, `settingsManifest`. The remaining three (`seoManifest`, `auth`, `dashboard`) are published but not mounted through `ElevasisFeaturesProvider` in command-center -- `auth` and `dashboard` are utility features consumed directly, while `seoManifest` is available for composition but not wired into the current command-center shell.
|
|
205
|
+
|
|
206
|
+
### External-consumer composition
|
|
207
|
+
|
|
208
|
+
External shells consume the published `@elevasis/ui` provider surface. `_template/ui` imports individual manifests, composes a local `FEATURE_MANIFESTS: FeatureModule[]` array, and passes `canonicalOrganizationModel` (from `@foundation/config/organization-model`) into the provider. Host-local nav (home/dashboard) remains app-owned -- the provider covers shared shell features, not total shell ownership.
|
|
209
|
+
|
|
210
|
+
See [Composition & Extensibility](./composition-extensibility.mdx) for the sidebar/page override patterns consumers use to customize shared features without forking.
|
|
211
|
+
|
|
212
|
+
## Feature-Specific Sidebar Behavior
|
|
213
|
+
|
|
214
|
+
### CRM Sidebar
|
|
215
|
+
|
|
216
|
+
The CRM sidebar (`packages/ui/src/features/crm/sidebar/`) exports `CrmSidebar`, `CrmSidebarTop`, and `CrmSidebarMiddle`. `SavedViewsPanel` lives in `packages/ui/src/features/crm/workbench/SavedViewsPanel.tsx` and is exported from the CRM workbench barrel; it provides the saved contact views panel rendered within the CRM workbench surface.
|
|
217
|
+
|
|
218
|
+
### Operations Sidebar
|
|
219
|
+
|
|
220
|
+
`OperationsSidebarTop` in `packages/ui/src/features/operations/sidebar/OperationsSidebarTop.tsx` is context-aware by route:
|
|
221
|
+
|
|
222
|
+
- Returns `null` for sessions (`/operations/sessions`) and command-view (`/operations/command-view`) routes -- these sections own their own top-area UI
|
|
223
|
+
- Shows a "Resource Overview" navigation button for the resources section (`/operations/resources`)
|
|
224
|
+
- Shows a "Calibration Projects" navigation button for the calibration section (`/operations/calibration`)
|
|
225
|
+
- Returns `null` for all other paths (e.g., the operations index)
|
|
226
|
+
|
|
227
|
+
This pattern avoids hardcoding sidebar top content in the parent and lets each operations sub-section declare its own top panel entry point.
|
|
228
|
+
|
|
229
|
+
## Testing
|
|
230
|
+
|
|
231
|
+
- `createTestFeaturesProvider` fixture at `packages/ui/src/provider/createTestFeaturesProvider.tsx` (internal; exported from `provider/index.ts`) gives tests a pre-wired provider with configurable organization model and feature access.
|
|
232
|
+
- Resolver modules have focused unit tests (`RouteResolver.test.ts`, `NavResolver.test.ts`).
|
|
233
|
+
- `validateManifests.test.ts` covers manifest validation against the organization model.
|
|
234
|
+
- `ElevasisFeaturesProvider.test.tsx`, `FeatureShell.test.tsx`, and `feature-contract.test.ts` cover runtime composition and the published surface.
|
|
235
|
+
|
|
236
|
+
## Key Conceptual Distinctions
|
|
237
|
+
|
|
238
|
+
- **Platform capability vs. shell feature** -- capabilities are the product map; shell features are manifest-backed UI surfaces.
|
|
239
|
+
- **Shell feature key vs. organization-model feature key** -- `crm` (shell) vs. `acquisition` (published access identity).
|
|
240
|
+
- **Semantic domain vs. feature key** -- `domain = crm` describes the business area; `feature key = acquisition` gates access.
|
|
241
|
+
- **Provider-owned vs. consumer-owned** -- provider owns shared manifests, nav contribution, sidebar dispatch, shared runtime context; consumers own route files, branding, topbar behavior, admin entries.
|