Documentation Index
Fetch the complete documentation index at: https://grantmaster.dev/llms.txt
Use this file to discover all available pages before exploring further.
Widget System
Overview
The widget system is the foundation of GrantMaster’s customisable dashboard experience. Each widget is a self-contained React component that displays a specific piece of information or functionality. Widgets are registered in Firestore, assigned to roles by a Superadmin, and rendered in a 12-column responsive grid. Users and organisations can further customise which widgets they see and in what order.
The system has four layers:
- Registry — Firestore-backed catalogue of widget definitions and role assignments
- Templates — default dashboard layouts per role, applied on user onboarding
- Overrides — per-organisation visibility and ordering changes
- Preferences — per-user hidden-widget list and custom ordering
The grid uses 12 columns with 8 pt spacing. Each WidgetSize value maps to a column span and a pixel height:
| Enum value | Columns | Height | Typical use |
|---|
COMPACT | 4 | 160 px | Small counters, quick-action buttons |
MEDIUM | 6 | 160 px | Summary cards with a small chart |
WIDE | 8 | 160 px | Horizontal lists or progress bars |
TALL | 4 | 320 px | Narrow vertical lists |
CHART | 8 | 320 px | Line/bar charts |
LARGE | 12 | 320 px | Full-width tables or complex views |
Deprecated aliases (kept for backward compatibility, removed in v2.0):
| Alias | Resolves to |
|---|
SMALL | COMPACT |
FULL | LARGE |
Categories group widgets in the Add Widget modal and the assignment matrix:
| Value | Description |
|---|
PERSONAL | User-specific metrics (leave balance, journals) |
PROJECTS | Project status, milestones, budget burn |
FINANCE | Expense summaries, financial KPIs |
GRANTS | Grant pipeline, submission deadlines |
USERS | Team headcount, capacity |
MISSION | Impact indicators, M&E summaries |
COMPLIANCE | Compliance scores, overdue tasks |
PLATFORM | System health, subscription usage |
WidgetHeight is a string-union used when a widget needs to declare an explicit pixel height outside the standard WidgetSize row heights. It is less commonly used than WidgetSize.
enum WidgetLoadingState {
IDLE = 'IDLE',
LOADING = 'LOADING',
SUCCESS = 'SUCCESS',
ERROR = 'ERROR',
}
Used by useWidgets to expose fine-grained loading state beyond a simple boolean.
Core Type Definitions
All widget types are defined in src/shared/platform/widget.contracts.ts.
The canonical record stored in Firestore for each widget:
interface WidgetDefinition {
id: string; // unique slug, e.g. 'grant-pipeline-summary'
name: string;
description: string;
category: WidgetCategory;
defaultSize: WidgetSize;
supportedSizes: WidgetSize[]; // sizes the component can render at
componentPath: string; // path for dynamic import, e.g. 'widgets/grants/PipelineSummary'
icon?: string; // lucide-react icon name
isActive: boolean;
noFrame?: boolean; // widget provides its own container; skip default Card wrapper
configSchema?: WidgetConfigSchema; // Zod schema for per-instance settings
metadata?: WidgetMetadata;
createdAt: Timestamp;
updatedAt: Timestamp;
}
Links a widget to zero or more roles:
interface WidgetAssignment {
id: string;
widgetId: string;
assignedRoles: SystemRole[];
priority: number; // default sort order within the role's dashboard
isActive: boolean;
updatedAt: Timestamp;
updatedBy: string;
}
Per-organisation customisation stored under organizations/{orgId}/widgetOverrides/{widgetId}:
interface WidgetOverride {
widgetId: string;
organizationId: string;
isHidden?: boolean;
displayName?: string; // rename for this org
priority?: number; // reorder within this org
updatedAt: Timestamp;
updatedBy: string;
}
What is actually rendered — definition merged with any org override:
interface WidgetInstance {
definition: WidgetDefinition;
override?: WidgetOverride;
currentSize: WidgetSize;
}
Per-user preferences stored in the user’s Firestore document (employees/{uid}.widgetPreferences):
interface WidgetPreferences {
hiddenWidgets: string[]; // widgetIds to hide
widgetOrder: Array<{
widgetId: string;
priority: number;
}>;
widgetConfig?: Record<string, WidgetConfig>; // per-widget user settings
}
Standard props injected into every widget component:
interface WidgetProps {
widgetId: string;
size: WidgetSize;
config?: WidgetConfig; // resolved user/org config
onConfigChange?: (config: WidgetConfig) => void;
}
Firestore Storage Paths
| Data | Firestore path |
|---|
| Widget definitions | platform/widgets/definitions/{widgetId} |
| Role assignments | platform/widgets/assignments/{assignmentId} |
| Org overrides | organizations/{orgId}/widgetOverrides/{widgetId} |
src/shared/platform/widgetRegistry.ts is the single access point for all Firestore widget operations. It is a static service class with a 5-minute in-memory cache that is shared across the session.
Key methods:
| Method | Description |
|---|
getAllWidgets() | All active WidgetDefinition records |
getWidget(id) | Single definition by id |
saveWidget(def) | Create or update a definition |
deleteWidget(id) | Soft-delete (sets isActive: false) |
getAllAssignments() | All WidgetAssignment records |
getWidgetsByRole(role) | Definitions assigned to a specific role |
getWidgetsForUser(role, orgId) | Full resolution pipeline (role → org overrides → instances) |
bulkUpdateAssignments(changes, updatedBy, priorities?) | Batch-write assignment changes |
filterWidgets(filter) | Filter definitions by category, size, or active flag |
clearCache() | Invalidate the in-memory cache |
Superadmin shortcut: getWidgetsForUser returns ALL active definitions when the role is SystemRole.SUPER_ADMIN, skipping the assignment lookup.
- Create the component under
src/widgets/[category]/MyWidget.tsx. It must accept WidgetProps.
- Register it in Firestore (via the Superadmin Widget Library UI or a seed script) with
componentPath matching the file path.
- Create a
WidgetAssignment record linking it to the appropriate roles.
- (Optional) Add a
WidgetConfigSchema if the widget has user-configurable settings.
- Add a
WidgetTemplate entry to the relevant ROLE_TEMPLATES in src/config/dashboardTemplates.ts.
Marketplace modules (the Extensions feature) can contribute widgets by registering additional WidgetDefinition records in Firestore with the same schema. No code change is required in the host app — the registry service picks them up automatically via getAllWidgets(). The module’s component bundle must be deployed separately and referenced via componentPath.
Role-Based Dashboard Templates
Purpose
Templates define the default widget layout applied to a new user of a given role during onboarding. They are also used when a Superadmin clicks “Apply to existing users”.
Defined in src/config/dashboardTemplates.ts:
interface WidgetTemplate {
widgetId: string;
priority: number; // lower = higher on dashboard
size: WidgetSize;
category: WidgetCategory;
rationale?: string; // dev-facing explanation of why this widget is included
}
interface RoleTemplate {
role: SystemRole;
name: string;
description: string;
widgets: WidgetTemplate[];
}
Pre-built Templates
ROLE_TEMPLATES contains one entry for each of the five roles:
| Role | Focus |
|---|
SUPER_ADMIN | Platform health, subscription metrics, cross-org overview |
ADMIN | Organisation KPIs, compliance, team capacity |
MANAGER | Project status, budget burn, journals requiring approval |
MEMBER | Personal tasks, leave balance, assigned project widgets |
AUDITOR | Compliance scores, audit trail, financial summaries |
Helper Functions
// Retrieve the default template for a role
getDefaultDashboardTemplate(role: SystemRole): RoleTemplate | undefined
// Convert a RoleTemplate into WidgetPreferences (for writing to user profile)
templateToWidgetPreferences(template: RoleTemplate): WidgetPreferences
src/hooks/useWidgets.ts — primary hook for loading the current user’s widget instances:
const { widgets, loading, error, refresh, loadingState } = useWidgets(includeHidden?);
Resolution order:
- Fetch
WidgetAssignment records for the user’s role via WidgetRegistryService.getWidgetsForUser.
- Apply org-level overrides (hidden, renamed, reordered).
- Filter out widgets in
currentUserProfile.widgetPreferences.hiddenWidgets (unless includeHidden is true).
- Sort by
widgetPreferences.widgetOrder priorities if defined.
loading is a simple boolean derived from loadingState === WidgetLoadingState.LOADING. Use loadingState when you need to differentiate IDLE, SUCCESS, or ERROR states.
Other Hooks
| Hook | Purpose |
|---|
useAllWidgets(filter?) | Superadmin only — all definitions, optional WidgetFilter |
useWidgetAssignments() | Manages the role ↔ widget assignment matrix; exposes toggleAssignment, saveChanges, reset |
useWidgetStats() | Aggregate counts: total active widgets and per-category breakdown |
DashboardTemplateEditor (Superadmin UI)
src/features/dashboard/components/DashboardTemplateEditor/ implements the Superadmin template editing screen as a compound component:
<DashboardTemplateEditor>
<DashboardTemplateEditor.Header />
<DashboardTemplateEditor.InfoCard />
<DashboardTemplateEditor.RoleTabs />
<DashboardTemplateEditor.TemplateCanvas />
<DashboardTemplateEditor.Actions />
</DashboardTemplateEditor>
State is managed via DashboardTemplateEditorContext which tracks:
templates: Map<SystemRole, RoleTemplate> — one entry per role
activeRole — the role tab currently selected
isDirty — whether unsaved changes exist
allWidgets — full widget definition catalogue from useAllWidgets
saving / loading — async operation states
handleSave(applyToExisting: boolean) writes templates to Firestore and, if applyToExisting is true, calls applyTemplateToRole to update all existing users of that role.
When WidgetDefinition.noFrame is true, the dashboard renderer skips the default Card wrapper and renders the component directly into the grid cell. Use this for widgets that need a custom background, full-bleed images, or a map canvas.
Grid Rendering
The dashboard grid reads the currentSize from each WidgetInstance and applies Tailwind col-span-* classes accordingly:
| WidgetSize | Tailwind class |
|---|
COMPACT | col-span-4 |
MEDIUM | col-span-6 |
WIDE | col-span-8 |
TALL | col-span-4 |
CHART | col-span-8 |
LARGE | col-span-12 |
Row heights are applied via inline style using the pixel values from the WidgetHeight enum.
Widgets that have user-configurable settings declare a WidgetConfigSchema:
interface WidgetConfigSchema {
fields: Array<{
key: string;
label: string;
type: 'text' | 'number' | 'boolean' | 'select';
defaultValue?: unknown;
options?: Array<{ label: string; value: string }>; // for 'select'
}>;
}
Resolved config is passed into the widget as the config prop. The widget calls onConfigChange to persist changes back to widgetPreferences.widgetConfig[widgetId] on the user’s Firestore document.
Cache Management
WidgetRegistryService maintains a module-level in-memory cache with a 5-minute TTL. The cache covers getAllWidgets() and getAllAssignments() responses.
- Call
WidgetRegistryService.clearCache() or refresh() from useWidgets / useAllWidgets after any mutation that should propagate immediately.
- The cache is process-scoped (not shared across browser tabs or server instances).