@elevasis/sdk 1.10.0 → 1.11.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.
@@ -1,20 +1,12 @@
1
1
  ---
2
2
  title: UI Recipes
3
- description: Copy-paste recipes for common UI tasks -- add a page, add a nav item, add a feature-scoped component, change theme tokens.
3
+ description: Current recipes for adding pages, feature-gated routes, feature components, theme tokens, Organization Model sidebar entries, and executable resources.
4
4
  ---
5
5
 
6
6
  # UI Recipes
7
7
 
8
- **Status:** 🟢 Stable
9
-
10
- Copy-paste starting points for the five most common UI tasks. Each recipe shows the complete file contents or a tight diff first, then explains what each part does. For deeper background on auth, feature flags, or customization, follow the links at the bottom of each section.
11
-
12
- ---
13
-
14
8
  ## 1. Add a Top-Level Page
15
9
 
16
- ### Code
17
-
18
10
  Create `ui/src/routes/my-feature.index.tsx`:
19
11
 
20
12
  ```tsx
@@ -42,36 +34,26 @@ function MyFeatureRouteComponent() {
42
34
  }
43
35
  ```
44
36
 
45
- Then add a nav entry in `ui/src/config/nav-items.ts`:
37
+ Add the sidebar entry as a feature node in `core/config/organization-model.ts`:
46
38
 
47
39
  ```ts
48
- import { IconStar } from '@tabler/icons-react'
49
-
50
- export const navItems: ExtendedLinksGroupProps[] = [
51
- { label: organizationModel.navigation.homeLabel, icon: IconDashboard, link: '/' },
52
- { label: 'My Feature', icon: IconStar, link: '/my-feature' }, // Add this
40
+ features: [
41
+ {
42
+ id: 'my-feature',
43
+ label: 'My Feature',
44
+ enabled: true,
45
+ path: '/my-feature',
46
+ icon: 'star',
47
+ uiPosition: 'sidebar-primary'
48
+ }
53
49
  ]
54
50
  ```
55
51
 
56
- ### Explanation
57
-
58
- - **Route path:** TanStack Router derives the path from the filename. `my-feature.index.tsx` maps to `/my-feature/`. The `.index` suffix means this is the default child (index route), not a layout.
59
- - **`ProtectedRoute`:** redirects unauthenticated users to `/login?returnTo=<current-path>` and shows a full-screen loader while auth is initializing. Always wrap top-level pages with it.
60
- - **Layout components:** `AppTopbarAdjusterWrapper` adds the correct top padding to clear the topbar. `AppShellContentContainer` constrains the content to the main column. `PageContainer` adds consistent horizontal padding.
61
- - **`routeTree.gen.ts`:** TanStack Router regenerates this file on `pnpm dev`. Never edit it by hand.
62
- - **Nav item:** Icons come from `@tabler/icons-react`. No other icon library.
52
+ TanStack Router derives the path from the filename. The sidebar is derived from `OrganizationModel.features`.
63
53
 
64
- If the page should only appear when a feature flag is enabled, add `featureKey` to the nav item and wrap the route content with `FeatureGuard` (see recipe 1a below). If the page is admin-only, add `requiresAdmin: true` to the nav item and nest `AdminGuard` inside `ProtectedRoute`.
54
+ ## 2. Add a Feature-Gated Page
65
55
 
66
- ---
67
-
68
- ## 1a. Add a Feature-Gated Top-Level Page
69
-
70
- If the page should only be visible and accessible when a feature is enabled, add `featureKey` to the nav item and wrap the route component with `FeatureGuard`.
71
-
72
- ### Code
73
-
74
- `ui/src/routes/my-feature.index.tsx` (change from recipe 1):
56
+ Wrap the route with `FeatureGuard` and use the same feature ID as the org model node.
75
57
 
76
58
  ```tsx
77
59
  import { FeatureGuard } from '@/features/auth/guards/FeatureGuard'
@@ -93,29 +75,11 @@ function MyFeatureRouteComponent() {
93
75
  }
94
76
  ```
95
77
 
96
- `ui/src/config/nav-items.ts` (add `featureKey`):
97
-
98
- ```ts
99
- { label: 'My Feature', icon: IconStar, link: '/my-feature', featureKey: 'my-feature' }
100
- ```
101
-
102
- ### Explanation
78
+ Set `enabled: false` in the feature node to hide it by default. Use `requiresAdmin: true` on the feature node plus `AdminGuard` in the route for admin-only pages.
103
79
 
104
- - **`featureKey` on nav item:** hides the nav entry when the feature is disabled for the organization or the current membership.
105
- - **`FeatureGuard` on the route:** hard-stops at the route boundary when someone navigates directly to the URL. Shows a notification and redirects to `/` when access is denied.
106
- - **Feature keys** are declared in `foundations/config/organization-model.ts` under `features.enabled`. Add a new key there before using it in a guard.
80
+ ## 3. Add a Nested Page
107
81
 
108
- For the full three-concept model (`featureKey` / `FeatureGuard` / `AdminGuard`), see `./feature-flags-and-gating.md`.
109
-
110
- ---
111
-
112
- ## 2. Add a Nested Page
113
-
114
- Nested pages live under a layout file. The layout file holds shared guards and the `<Outlet />`. Child routes hold the actual page.
115
-
116
- ### Code
117
-
118
- First, ensure the layout file exists. For a new section called `reporting`, create `ui/src/routes/reporting.tsx`:
82
+ Create a layout file for the section and child route files under the same directory.
119
83
 
120
84
  ```tsx
121
85
  import { createFileRoute, Outlet } from '@tanstack/react-router'
@@ -134,400 +98,101 @@ function ReportingLayoutComponent() {
134
98
  }
135
99
  ```
136
100
 
137
- Then create the child page at `ui/src/routes/reporting/overview.index.tsx`:
138
-
139
- ```tsx
140
- import { createFileRoute } from '@tanstack/react-router'
141
- import { AppShellContentContainer, AppTopbarAdjusterWrapper, PageContainer } from '@elevasis/ui/layout'
142
- import { ReportingOverviewPage } from '@/features/reporting/components/ReportingOverviewPage'
143
-
144
- export const Route = createFileRoute('/reporting/overview/')({
145
- component: ReportingOverviewRouteComponent
146
- })
147
-
148
- function ReportingOverviewRouteComponent() {
149
- return (
150
- <AppTopbarAdjusterWrapper>
151
- <AppShellContentContainer>
152
- <PageContainer>
153
- <ReportingOverviewPage />
154
- </PageContainer>
155
- </AppShellContentContainer>
156
- </AppTopbarAdjusterWrapper>
157
- )
158
- }
159
- ```
160
-
161
- For a page with a dynamic param (e.g., `/reporting/reports/$reportId`), name the file `reporting/reports.$reportId.index.tsx`:
101
+ Add dotted feature nodes for the sidebar hierarchy:
162
102
 
163
- ```tsx
164
- import { createFileRoute } from '@tanstack/react-router'
165
- import { SubshellContentContainer, PageContainer } from '@elevasis/ui/layout'
166
- import { ReportDetailPage } from '@/features/reporting/components/ReportDetailPage'
167
-
168
- export const Route = createFileRoute('/reporting/reports/$reportId/')({
169
- component: ReportDetailRouteComponent
170
- })
171
-
172
- function ReportDetailRouteComponent() {
173
- const { reportId } = Route.useParams()
174
-
175
- return (
176
- <SubshellContentContainer>
177
- <PageContainer>
178
- <ReportDetailPage reportId={reportId} />
179
- </PageContainer>
180
- </SubshellContentContainer>
181
- )
182
- }
103
+ ```ts
104
+ { id: 'reporting', label: 'Reporting', enabled: true, uiPosition: 'sidebar-primary' },
105
+ { id: 'reporting.overview', label: 'Overview', enabled: true, path: '/reporting/overview' },
106
+ { id: 'reporting.reports', label: 'Reports', enabled: true, path: '/reporting/reports' }
183
107
  ```
184
108
 
185
- ### Explanation
186
-
187
- - **Layout file:** `reporting.tsx` matches the `/reporting` prefix. TanStack Router renders child routes into its `<Outlet />`. Guards placed here apply to every child automatically -- no need to repeat `ProtectedRoute` in every child.
188
- - **File naming convention:** `reporting/overview.index.tsx` maps to `/reporting/overview/`. The directory matches the layout prefix; the file maps to the path segment.
189
- - **Dynamic params:** `$reportId` in the filename becomes a typed param. `Route.useParams()` returns `{ reportId: string }` with full type safety -- no casting needed.
190
- - **`SubshellContentContainer` vs `AppShellContentContainer`:** use `SubshellContentContainer` inside sections that already have a shared subshell (like operations). Use `AppShellContentContainer` for top-level full-width pages.
191
- - **Guards in children:** if the layout already wraps `ProtectedRoute`, children do not need to repeat it. Add `FeatureGuard` in the layout if the entire section is gated.
192
-
193
- ---
194
-
195
- ## 3. Add a Feature-Scoped Component
196
-
197
- ### When to use `ui/src/features/<name>/`
198
-
199
- Create a feature directory when:
200
-
201
- - Logic is reused across two or more route files, OR
202
- - The feature owns its own context, custom hooks, or local state that does not belong to a single route.
203
-
204
- Keep it inline (in the route file itself, or in a one-off component file next to the route) when:
109
+ Containers omit `path`; leaves provide `path`.
205
110
 
206
- - The component is used in exactly one place and has no shared state.
111
+ ## 4. Add a Feature-Scoped Component
207
112
 
208
- ### Code
113
+ Use `ui/src/features/<name>/` when logic is shared across routes or owns local state.
209
114
 
210
- Standard feature directory layout:
211
-
212
- ```
115
+ ```text
213
116
  ui/src/features/reporting/
214
117
  components/
215
- ReportingOverviewPage.tsx # The top-level page component
216
- ReportCard.tsx # Smaller, reusable UI piece
118
+ ReportingOverviewPage.tsx
119
+ ReportCard.tsx
217
120
  hooks/
218
- useReports.ts # TanStack Query data hook
121
+ useReports.ts
219
122
  utils/
220
- formatReportDate.ts # Pure helper
221
- types.ts # Feature-specific TypeScript types
222
- ```
223
-
224
- `ui/src/features/reporting/hooks/useReports.ts`:
225
-
226
- ```ts
227
- import { useQuery } from '@tanstack/react-query'
228
- import { useApiClient } from '@/lib/hooks/useApiClient'
229
-
230
- export function useReports() {
231
- const { apiRequest, isOrganizationReady } = useApiClient()
232
-
233
- return useQuery({
234
- queryKey: ['reports'],
235
- queryFn: () => apiRequest('/reports', { method: 'GET' }),
236
- enabled: isOrganizationReady
237
- })
238
- }
123
+ formatReportDate.ts
124
+ types.ts
239
125
  ```
240
126
 
241
- `ui/src/features/reporting/components/ReportingOverviewPage.tsx`:
127
+ Use `@/*` imports for app-local cross-feature imports and keep route layout concerns in route files.
242
128
 
243
- ```tsx
244
- import { Stack, Text } from '@mantine/core'
245
- import { useReports } from '../hooks/useReports'
246
- import { ReportCard } from './ReportCard'
247
-
248
- export function ReportingOverviewPage() {
249
- const { data: reports, isLoading } = useReports()
250
-
251
- if (isLoading) return <Text>Loading...</Text>
252
-
253
- return (
254
- <Stack>
255
- {reports?.map((report) => (
256
- <ReportCard key={report.id} report={report} />
257
- ))}
258
- </Stack>
259
- )
260
- }
261
- ```
129
+ ## 5. Change Theme Tokens
262
130
 
263
- ### Explanation
264
-
265
- - **`@/*` alias:** resolves to `ui/src/*`. Use it for all cross-feature imports (e.g., `@/lib/hooks/useApiClient`, `@/features/auth`). Never use relative paths that cross feature boundaries.
266
- - **Server state in Query, client state in Zustand:** if data comes from an API, it belongs in a `useQuery` hook, not a Zustand slice. Zustand is for client-only UI state (open/close, selection, optimistic updates).
267
- - **`useApiClient`:** from `@/lib/hooks/useApiClient`. Returns `apiRequest` (an authenticated fetch wrapper) and `isOrganizationReady` (use this as `enabled` to avoid firing requests before the org is loaded).
268
- - **`SubshellContentContainer` / `PageContainer`:** added in the route file, not in the feature component. Feature components should be layout-agnostic -- the route decides the shell treatment.
269
- - **No React, no Node imports in `foundations/`:** types shared between frontend and `operations/` live in `foundations/`. Feature components live only in `ui/src/features/`.
270
-
271
- ---
272
-
273
- ## 4. Change Theme Tokens
274
-
275
- ### Code
276
-
277
- Edit `ui/src/config/theme.ts`. The most common task is changing the primary color and brand feel for the `default` preset:
131
+ Edit `ui/src/config/theme.ts`.
278
132
 
279
133
  ```ts
280
- // ui/src/config/theme.ts (diff -- change the default preset's dark token block)
281
-
282
134
  default: {
283
135
  label: 'Default',
284
- description: 'My project brand theme',
285
- colors: ['#7c3aed', '#080808', '#f2f2f5'], // [primary, background, surface-light]
136
+ description: 'Project brand theme',
137
+ colors: ['#2563eb', '#080808', '#f2f2f5'],
286
138
  dark: {
287
- primary: '#7c3aed', // Brand color (buttons, links, accents)
288
- primaryContrast: '#ffffff', // Text on primary-colored elements
289
- background: '#050507', // Page background
290
- surface: '#0f0f14', // Cards, modals, elevated panels
291
- surfaceHover: '#1a1a22', // Hover state for surfaces
139
+ primary: '#2563eb',
140
+ primaryContrast: '#ffffff',
141
+ background: '#050507',
142
+ surface: '#0f0f14',
143
+ surfaceHover: '#1a1a22',
292
144
  text: '#ffffff',
293
145
  textDimmed: '#b0b0c0',
294
146
  textSubtle: '#888898',
295
- border: 'rgba(255, 255, 255, 0.07)',
296
- // ... keep remaining tokens
297
- },
298
- // light: { ... },
299
- framework: {
300
- defaultRadius: 'md', // Global border radius ('xs' | 'sm' | 'md' | 'lg' | 'xl')
301
- fontFamily: '"Inter", sans-serif',
302
- headings: { fontFamily: '"Inter", sans-serif', fontWeight: '600' }
303
- }
304
- }
305
- ```
306
-
307
- To set the preset that loads by default for new users, change `defaultPreset` at the bottom of the file:
308
-
309
- ```ts
310
- export const defaultPreset = 'default' // was 'tactical'
311
- export const defaultColorScheme = 'dark' as const
312
- ```
313
-
314
- To add a completely new named preset (selectable from appearance settings), append it to `themePresets`:
315
-
316
- ```ts
317
- export const themePresets: Record<string, ThemePreset> = {
318
- ...presets,
319
- default: { /* ... */ },
320
- 'acme-brand': {
321
- label: 'Acme Brand',
322
- description: 'Acme Corp official brand',
323
- colors: ['#2563eb', '#0a0a0a', '#f5f5f5'],
324
- dark: {
325
- primary: '#2563eb',
326
- primaryContrast: '#fff',
327
- background: '#0a0a0a',
328
- surface: '#111827',
329
- surfaceHover: '#1f2937',
330
- text: '#f9fafb',
331
- textDimmed: '#9ca3af',
332
- textSubtle: '#6b7280',
333
- border: 'rgba(255,255,255,0.08)',
334
- error: '#ef4444',
335
- warning: '#f59e0b',
336
- success: '#10b981',
337
- glassBackground: 'rgba(10, 10, 10, 0.5)',
338
- glassBlur: 'blur(20px) saturate(160%)',
339
- shadow: '0 1px 3px rgba(0,0,0,0.4), 0 8px 24px -8px rgba(0,0,0,0.5)',
340
- cardShadow: 'inset 0 1px 0 rgba(255,255,255,0.06), 0 2px 8px rgba(0,0,0,0.3)',
341
- durationFast: '150ms',
342
- durationNormal: '250ms',
343
- easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
344
- destructiveFg: '#ffffff',
345
- fontHeading: '"Inter", sans-serif',
346
- fontSans: '"Inter", sans-serif'
347
- },
348
- light: { /* ... mirror pattern with light-mode values */ },
349
- framework: { defaultRadius: 'md', fontFamily: '"Inter", sans-serif' },
350
- fontImports: ['https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap']
147
+ border: 'rgba(255, 255, 255, 0.07)'
351
148
  }
352
149
  }
353
150
  ```
354
151
 
355
- ### Explanation
356
-
357
- - **All tokens flow into CSS variables automatically.** `primary` becomes `--color-primary`, `background` becomes `--color-background`, `glassBackground` becomes `--glass-background`, and so on. Every component that references those CSS variables (the sidebar, topbar, cards, modals) picks up the change immediately without touching component code.
358
- - **`framework` field:** maps directly to Mantine theme overrides. `defaultRadius` and `fontFamily` are the most commonly changed values. Component-level overrides can go here too (see the JSDoc in `theme.ts` for the pattern).
359
- - **`colors` array:** the three-element shorthand is `[primary, darkBackground, lightSurface]`. It is used by the appearance settings preview swatch only -- the actual runtime values come from `dark.*` and `light.*`.
360
- - **Appearance settings:** users can switch presets from the settings UI. The `defaultPreset` and `defaultColorScheme` exports set what loads for a user who has never changed their preference.
361
- - **`ui/src/config/README.md`:** contains the deeper guide for `background.tsx` (full-page background treatment) and `loader.tsx` (global spinner customization), which are separate from the theme preset system.
362
-
363
- ---
364
-
365
- ## 5. Add a Nav Item
366
-
367
- Nav items live in `ui/src/config/nav-items.ts`. The current (pre-foundations-refactor) pattern is to add entries directly to the `navItems` array.
368
-
369
- ### Code
370
-
371
- `ui/src/config/nav-items.ts`:
372
-
373
- ```ts
374
- import { IconDashboard, IconChartBar, IconUsers, IconSettings } from '@tabler/icons-react'
375
- import { LinksGroupProps } from '@elevasis/ui/layout'
376
- import { organizationModel } from '@foundation/config/organization-model'
377
-
378
- export const navItems: ExtendedLinksGroupProps[] = [
379
- // Top-level link (no submenu)
380
- { label: organizationModel.navigation.homeLabel, icon: IconDashboard, link: '/' },
381
-
382
- // Top-level link gated by feature flag
383
- { label: 'Reporting', icon: IconChartBar, link: '/reporting', featureKey: 'reporting' },
384
-
385
- // Group with sub-links
386
- {
387
- label: 'Team',
388
- icon: IconUsers,
389
- featureKey: 'operations', // hides entire group when feature is off
390
- links: [
391
- { label: 'Members', link: '/team/members' },
392
- { label: 'Roles', link: '/team/roles', featureKey: 'settings' }, // sub-link can have its own key
393
- ]
394
- },
395
-
396
- // Admin-only top-level link
397
- { label: 'Settings', icon: IconSettings, link: '/settings/account', requiresAdmin: true }
398
- ]
399
- ```
400
-
401
- ### Explanation
402
-
403
- - **`featureKey`:** when set on a group, the entire group (including its sub-links) is hidden when the feature is disabled. When set on a sub-link, only that link is hidden.
404
- - **`requiresAdmin`:** hides the entry for non-admin users. Combine with `ProtectedRoute` + `AdminGuard` on the actual route to enforce the restriction at the route level too.
405
- - **Icon library:** `@tabler/icons-react` only. Never Lucide, Heroicons, or others.
406
- - **Dashboard entry:** `organizationModel.navigation.homeLabel` resolves to `'Dashboard'` from `foundations/config/organization-model.ts`. Use it instead of a hard-coded string so the label stays consistent with the rest of the shell.
407
-
408
- **Upcoming change (foundations-refactor Step 8):** the nav customization pattern will become cleaner. Published feature manifests will export a `CRM_ITEMS` array (and equivalents for other features), and the sidebar will accept an `items` prop to swap or extend those arrays without editing `nav-items.ts`. When that ships, `./customization.md` will show the updated pattern. For now, `nav-items.ts` is the canonical place for host-local nav additions.
409
-
410
- ---
411
-
412
- ## Cross-References
413
-
414
- - `./index.md` -- shell architecture, auth flow, route structure overview
415
- - `./feature-flags-and-gating.md` -- full three-concept gating model (featureKey / FeatureGuard / AdminGuard)
416
- - `./customization.md` -- customizing feature sidebars and pages via manifest composition
417
- - `ui/src/config/README.md` -- deeper guide for theme, background, and loader config files
418
- - `external/_template/.claude/rules/frontend.md` -- concise agent-oriented frontend conventions
419
-
420
- ---
152
+ All tokens flow into CSS variables. Change `defaultPreset` to alter the initial preset for new users.
421
153
 
422
154
  ## 6. Execute a Resource from a Surface
423
155
 
424
- Resources (workflows, agents, triggers) deploy to the Elevasis platform and can be triggered from any feature surface. This recipe shows how to declare the resource on a surface via the org model, share its input schema via `foundations/types/`, and render a button that executes it.
425
-
426
- ### The Three Foundations Layers
427
-
428
- - **Org model** (`foundations/config/organization-model.ts`): semantic metadata -- which surfaces exist, what resources map to them, labels and colors.
429
- - **Entity contracts** (`foundations/types/entities.ts`): typed shapes for Project, Deal, etc. -- extend `BaseProject`, `BaseDeal` from `@elevasis/core/entities`.
430
- - **Resource I/O** (`foundations/types/index.ts`): Zod schemas for workflow inputs/outputs.
431
-
432
- ### Code
433
-
434
- #### 1. Declare the resource on a surface
435
-
436
- `foundations/config/organization-model.ts`:
437
-
438
- ```ts
439
- import { defineOrganizationModel, CRM_PIPELINE_SURFACE_ID } from '@elevasis/core/organization-model'
440
-
441
- export const organizationOverride = defineOrganizationModel({
442
- branding: { /* ... */ },
443
- resourceMappings: [
444
- {
445
- id: 'crm.pipeline.write-note',
446
- surfaceId: CRM_PIPELINE_SURFACE_ID,
447
- resourceId: 'my-project-write-note-workflow',
448
- resourceType: 'workflow',
449
- label: 'Write Note',
450
- color: 'blue',
451
- featureIds: ['crm'],
452
- entityIds: ['crm.deal'],
453
- capabilityIds: []
454
- }
455
- ]
456
- })
457
- ```
458
-
459
- #### 2. Share the input schema
460
-
461
- `foundations/types/index.ts`:
156
+ Declare the resource relationship in resource metadata:
462
157
 
463
158
  ```ts
464
- import { z } from 'zod'
465
-
466
- export const writeNoteInputSchema = z.object({
467
- dealId: z.string(),
468
- note: z.string().min(1).max(2000)
469
- })
470
-
471
- export type WriteNoteInput = z.infer<typeof writeNoteInputSchema>
159
+ config: {
160
+ resourceId: 'write-note',
161
+ name: 'Write Note',
162
+ type: 'workflow',
163
+ version: '1.0.0',
164
+ status: 'prod',
165
+ links: [{ nodeId: 'feature:sales.crm', kind: 'operates-on' }],
166
+ category: 'production'
167
+ }
472
168
  ```
473
169
 
474
- #### 3. Render the button
475
-
476
- `ui/src/features/crm/components/DealPanel.tsx`:
170
+ Render it with `RunResourceButton`:
477
171
 
478
172
  ```tsx
479
173
  import { RunResourceButton } from '@elevasis/ui/components'
480
- import { organizationModel } from '@foundation/config/organization-model'
481
174
  import { writeNoteInputSchema } from '@foundation/types'
482
175
 
483
176
  export function DealPanel({ dealId }: { dealId: string }) {
484
177
  return (
485
178
  <RunResourceButton
486
- resourceId="my-project-write-note-workflow"
179
+ resourceId="write-note"
487
180
  resourceType="workflow"
488
- organizationModel={organizationModel}
181
+ label="Write Note"
489
182
  getInput={() => ({
490
183
  schema: writeNoteInputSchema,
491
184
  defaults: { dealId }
492
185
  })}
493
- onSuccess={(result) => console.log('Started:', result.executionId)}
494
186
  />
495
187
  )
496
188
  }
497
189
  ```
498
190
 
499
- The three `getInput` shapes:
191
+ `links[].nodeId` binds the resource into the Organization Model graph. `category` drives operational filtering for production, diagnostic, internal, and testing resources.
500
192
 
501
- ```tsx
502
- // Plain object (no form) -- executes immediately on click
503
- getInput={() => ({ dealId, note: 'Auto-generated' })}
504
-
505
- // Mixed (form for unfixed fields) -- modal opens with dealId pre-filled
506
- getInput={() => ({ schema: writeNoteInputSchema, defaults: { dealId } })}
507
-
508
- // Full form -- modal opens with all fields blank
509
- getInput={() => ({ schema: writeNoteInputSchema })}
510
- ```
511
-
512
- #### 4. Optionally listen for live execution logs
513
-
514
- ```tsx
515
- import { useExecutionLogSSE, useMergedExecution } from '@elevasis/ui/hooks'
516
- // Pass the SSE manager from your app's sse.ts wrapper and apiUrl from config.
517
- // For the full live-logs pattern see:
518
- // apps/command-center/src/features/operations/executions/
519
- ```
520
-
521
- ### How It Works
522
-
523
- - `RunResourceButton` inspects what `getInput()` returns: a plain object triggers execution immediately; `{ schema, defaults? }` opens a `ZodFormRenderer` modal so the user can fill in missing fields before submitting.
524
- - Label and color resolve in order: explicit props > `resourceMappings` entry matching `resourceId` > default `'Run'`. The `icon` prop is prop-only and is never inferred from the mapping.
525
- - `ZodFormRenderer` introspects the Zod schema at runtime and renders Mantine fields for each top-level key. Unsupported Zod types (nested objects, arrays, unions) fall back to a JSON textarea so the form always renders regardless of schema complexity.
526
- - Typed feature and surface constants (e.g. `CRM_PIPELINE_SURFACE_ID`) come from `@elevasis/core/organization-model`. See Issue 4 constants for the full list.
527
- - Entity contracts (Deal, Project) live in `foundations/types/entities.ts` and extend base types from `@elevasis/core/entities` -- see the entity recipe for the full shape.
528
-
529
- ### Cross-References
193
+ ## Cross-References
530
194
 
531
- - `./feature-flags-and-gating.md` -- gating a surface or button by feature key or admin role
532
- - `@foundation/types/entities.ts` -- Deal, Project, and other entity contracts for this project
533
- - Recipe 5 (Add a Nav Item) -- how surfaces and nav entries are wired together
195
+ - `./feature-flags-and-gating.md` -- feature and admin access model
196
+ - `./feature-shell.mdx` -- provider and sidebar derivation
197
+ - `./customization.md` -- feature sidebar customization
198
+ - `ui/src/config/README.md` -- theme, background, and loader config