Command center components for react and Mantine
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Mantine Spotlight provides utility functions for type checking and custom filter implementations to support advanced use cases.
Type guard functions for distinguishing between different action data structures.
/**
* Checks if an item is an actions group rather than a single action
* @param item - The item to check (action or group)
* @returns Type predicate indicating if item is SpotlightActionGroupData
*/
function isActionsGroup(
item: SpotlightActionData | SpotlightActionGroupData
): item is SpotlightActionGroupData;Usage Example:
import { isActionsGroup } from "@mantine/spotlight";
function processActions(actions: SpotlightActions[]) {
actions.forEach(item => {
if (isActionsGroup(item)) {
console.log(`Group: ${item.group} has ${item.actions.length} actions`);
item.actions.forEach(action => {
console.log(` - ${action.label}`);
});
} else {
console.log(`Action: ${item.label}`);
}
});
}The Spotlight component uses an internal default filter function that provides smart searching across action labels, descriptions, and keywords. This function is not exported from the package but you can create custom filter functions with similar behavior.
type SpotlightFilterFunction = (
query: string,
actions: SpotlightActions[]
) => SpotlightActions[];Custom Filter Examples:
import { SpotlightFilterFunction, isActionsGroup } from "@mantine/spotlight";
// Simple filter example
const simpleFilter: SpotlightFilterFunction = (query, actions) => {
if (!query.trim()) return actions;
return actions.filter(action => {
if (isActionsGroup(action)) {
// Filter group actions
const filteredActions = action.actions.filter(subAction =>
subAction.label?.toLowerCase().includes(query.toLowerCase())
);
return filteredActions.length > 0;
} else {
// Filter individual action
return action.label?.toLowerCase().includes(query.toLowerCase());
}
});
};
// Priority-based filter (similar to internal default)
const priorityFilter: SpotlightFilterFunction = (query, actions) => {
if (!query.trim()) return actions;
const queryLower = query.toLowerCase();
const labelMatches: SpotlightActions[] = [];
const descriptionMatches: SpotlightActions[] = [];
actions.forEach(action => {
if (isActionsGroup(action)) {
const labelMatched = action.actions.filter(subAction =>
subAction.label?.toLowerCase().includes(queryLower)
);
const descMatched = action.actions.filter(subAction =>
subAction.description?.toLowerCase().includes(queryLower) ||
(typeof subAction.keywords === 'string' &&
subAction.keywords.toLowerCase().includes(queryLower)) ||
(Array.isArray(subAction.keywords) &&
subAction.keywords.some(k => k.toLowerCase().includes(queryLower)))
);
if (labelMatched.length > 0) {
labelMatches.push({ ...action, actions: labelMatched });
} else if (descMatched.length > 0) {
descriptionMatches.push({ ...action, actions: descMatched });
}
} else {
if (action.label?.toLowerCase().includes(queryLower)) {
labelMatches.push(action);
} else if (
action.description?.toLowerCase().includes(queryLower) ||
(typeof action.keywords === 'string' &&
action.keywords.toLowerCase().includes(queryLower)) ||
(Array.isArray(action.keywords) &&
action.keywords.some(k => k.toLowerCase().includes(queryLower)))
) {
descriptionMatches.push(action);
}
}
});
return [...labelMatches, ...descriptionMatches];
};Type definitions for working with actions and action groups.
interface SpotlightActionData extends SpotlightActionProps {
/** Unique identifier for the action */
id: string;
/** Optional group name for organizing actions */
group?: string;
}
interface SpotlightActionGroupData {
/** Group label displayed in the interface */
group: string;
/** Array of actions belonging to this group */
actions: SpotlightActionData[];
}
type SpotlightActions = SpotlightActionData | SpotlightActionGroupData;You can create custom filter functions for specialized search behavior:
import { SpotlightFilterFunction, isActionsGroup } from "@mantine/spotlight";
// Fuzzy search filter
const fuzzyFilter: SpotlightFilterFunction = (query, actions) => {
if (!query.trim()) return actions;
const fuzzyMatch = (text: string, query: string): boolean => {
const queryLower = query.toLowerCase();
const textLower = text.toLowerCase();
let queryIndex = 0;
for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {
if (textLower[i] === queryLower[queryIndex]) {
queryIndex++;
}
}
return queryIndex === queryLower.length;
};
return actions.filter(action => {
if (isActionsGroup(action)) {
const filteredActions = action.actions.filter(subAction =>
fuzzyMatch(subAction.label || "", query) ||
fuzzyMatch(subAction.description || "", query)
);
if (filteredActions.length > 0) {
return { ...action, actions: filteredActions };
}
return false;
} else {
return fuzzyMatch(action.label || "", query) ||
fuzzyMatch(action.description || "", query);
}
});
};
// Weighted search filter
const weightedFilter: SpotlightFilterFunction = (query, actions) => {
if (!query.trim()) return actions;
const queryLower = query.toLowerCase();
const scoredActions: Array<{ action: SpotlightActions; score: number }> = [];
actions.forEach(action => {
if (isActionsGroup(action)) {
let groupScore = 0;
const filteredActions = action.actions.filter(subAction => {
let score = 0;
if (subAction.label?.toLowerCase().includes(queryLower)) score += 10;
if (subAction.description?.toLowerCase().includes(queryLower)) score += 5;
if (typeof subAction.keywords === 'string' &&
subAction.keywords.toLowerCase().includes(queryLower)) score += 3;
if (Array.isArray(subAction.keywords) &&
subAction.keywords.some(k => k.toLowerCase().includes(queryLower))) score += 3;
if (score > 0) groupScore += score;
return score > 0;
});
if (filteredActions.length > 0) {
scoredActions.push({
action: { ...action, actions: filteredActions },
score: groupScore
});
}
} else {
let score = 0;
if (action.label?.toLowerCase().includes(queryLower)) score += 10;
if (action.description?.toLowerCase().includes(queryLower)) score += 5;
if (typeof action.keywords === 'string' &&
action.keywords.toLowerCase().includes(queryLower)) score += 3;
if (Array.isArray(action.keywords) &&
action.keywords.some(k => k.toLowerCase().includes(queryLower))) score += 3;
if (score > 0) {
scoredActions.push({ action, score });
}
}
});
// Sort by score (highest first) and return actions
return scoredActions
.sort((a, b) => b.score - a.score)
.map(item => item.action);
};Helper functions for working with action data structures:
import { SpotlightActions, isActionsGroup } from "@mantine/spotlight";
// Flatten grouped actions into a single array
function flattenActions(actions: SpotlightActions[]): SpotlightActionData[] {
const flattened: SpotlightActionData[] = [];
actions.forEach(action => {
if (isActionsGroup(action)) {
flattened.push(...action.actions);
} else {
flattened.push(action);
}
});
return flattened;
}
// Group individual actions by their group property
function groupActions(actions: SpotlightActionData[]): SpotlightActions[] {
const groups: Record<string, SpotlightActionData[]> = {};
const ungrouped: SpotlightActionData[] = [];
actions.forEach(action => {
if (action.group) {
if (!groups[action.group]) {
groups[action.group] = [];
}
groups[action.group].push(action);
} else {
ungrouped.push(action);
}
});
const result: SpotlightActions[] = [];
// Add grouped actions
Object.entries(groups).forEach(([group, groupActions]) => {
result.push({ group, actions: groupActions });
});
// Add ungrouped actions
result.push(...ungrouped);
return result;
}
// Count total actions including those in groups
function countActions(actions: SpotlightActions[]): number {
return actions.reduce((count, action) => {
if (isActionsGroup(action)) {
return count + action.actions.length;
} else {
return count + 1;
}
}, 0);
}For large action datasets, consider implementing optimized filtering:
import { SpotlightFilterFunction } from "@mantine/spotlight";
// Memoized filter for better performance
function createMemoizedFilter(baseFilter: SpotlightFilterFunction): SpotlightFilterFunction {
const cache = new Map<string, SpotlightActions[]>();
return (query: string, actions: SpotlightActions[]) => {
const cacheKey = `${query}_${actions.length}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey)!;
}
const result = baseFilter(query, actions);
cache.set(cacheKey, result);
// Limit cache size
if (cache.size > 100) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
return result;
};
}
const memoizedFilter = createMemoizedFilter(simpleFilter);