React App
// src/api/rulesApi.ts
import axios from ‘axios’;
export type Operator =
| ‘equals’ | ‘notEqual’ | ‘contains’ | ‘notContains’
| ‘startsWith’ | ‘endsWith’ | ‘lessThan’ | ‘lessThanOrEqual’
| ‘greaterThan’ | ‘greaterThanOrEqual’ | ‘inRange’ | ‘blank’ | ‘notBlank’;
export type LogicalOperator = ‘AND’ | ‘OR’;
export interface Condition {
id?: string;
field: string;
operator: Operator;
value: any;
valueTo?: any;
logicalOperator?: LogicalOperator; // Operator connecting to next condition
}
export interface Rule {
id?: string;
name: string;
conditions: Condition[];
}
export interface RuleDomain {
id?: string;
name: string;
description: string;
rules: Rule[];
}
export interface RuleExecutionResult {
matched: boolean;
message?: string;
details?: any;
}
const api = axios.create({ baseURL: ‘/api’ });
// Domain API
export const getDomains = () => api.get<RuleDomain[]>(‘/domains’);
export const createDomain = (domain: RuleDomain) => api.post<RuleDomain>(‘/domains’, domain);
export const updateDomain = (id: string, domain: RuleDomain) => api.put<RuleDomain>(`/domains/${id}`, domain);
export const deleteDomain = (id: string) => api.delete(`/domains/${id}`);
// Rule API (within a domain)
export const getRules = (domainId: string) => api.get<Rule[]>(`/domains/${domainId}/rules`);
export const createRule = (domainId: string, rule: Rule) => api.post<Rule>(`/domains/${domainId}/rules`, rule);
export const updateRule = (domainId: string, ruleId: string, rule: Rule) => api.put<Rule>(`/domains/${domainId}/rules/${ruleId}`, rule);
export const deleteRule = (domainId: string, ruleId: string) => api.delete(`/domains/${domainId}/rules/${ruleId}`);
export const runRule = (domainId: string, ruleId: string, inputData: any) =>
api.post<RuleExecutionResult>(`/domains/${domainId}/rules/${ruleId}/execute`, inputData);
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — -
// src/components/DomainManager/DomainManager.tsx
import React, { useEffect, useState } from ‘react’;
import { RuleDomain, getDomains, createDomain, deleteDomain } from ‘../../api/rulesApi’;
import Button from ‘../Shared/Button’;
interface DomainManagerProps {
onSelectDomain: (domain: RuleDomain) => void;
}
export const DomainManager: React.FC<DomainManagerProps> = ({ onSelectDomain }) => {
const [domains, setDomains] = useState<RuleDomain[]>([]);
const [newDomainName, setNewDomainName] = useState(‘’);
const [newDomainDescription, setNewDomainDescription] = useState(‘’);
const [error, setError] = useState(‘’);
useEffect(() => {
loadDomains();
}, []);
const loadDomains = async () => {
const res = await getDomains();
setDomains(res.data);
};
const handleCreateDomain = async () => {
if (!newDomainName.trim()) {
setError(‘Domain name is required’);
return;
}
const newDomain: RuleDomain = {
name: newDomainName,
description: newDomainDescription,
rules: []
};
await createDomain(newDomain);
setNewDomainName(‘’);
setNewDomainDescription(‘’);
setError(‘’);
loadDomains();
};
const handleDeleteDomain = async (id: string) => {
if (confirm(‘Are you sure you want to delete this domain and all its rules?’)) {
await deleteDomain(id);
loadDomains();
}
};
return (
<div className=”domain-manager”>
<h2>Rule Domains</h2>
<div className=”create-domain-form”>
<h3>Create New Domain</h3>
{error && <div className=”error-message”>{error}</div>}
<div className=”form-group”>
<label>Domain Name *</label>
<input
value={newDomainName}
onChange={(e) => setNewDomainName(e.target.value)}
placeholder=”Enter domain name”
/>
</div>
<div className=”form-group”>
<label>Description</label>
<textarea
value={newDomainDescription}
onChange={(e) => setNewDomainDescription(e.target.value)}
placeholder=”Enter domain description”
rows={3}
/>
</div>
<Button
variant=”contained”
onClick={handleCreateDomain}
style={{ marginTop: ‘10px’ }}
>
Create Domain
</Button>
</div>
<div className=”domains-list”>
<h3>Existing Domains</h3>
{domains.length === 0 ? (
<div className=”no-domains”>No domains created yet</div>
) : (
<div className=”domain-cards”>
{domains.map(domain => (
<div key={domain.id} className=”domain-card”>
<div className=”domain-header”>
<h4>{domain.name}</h4>
<div className=”domain-actions”>
<Button
variant=”outlined”
onClick={() => onSelectDomain(domain)}
>
Manage Rules
</Button>
<Button
variant=”outlined”
color=”error”
onClick={() => domain.id && handleDeleteDomain(domain.id)}
>
Delete
</Button>
</div>
</div>
<p className=”domain-description”>{domain.description || ‘No description’}</p>
<div className=”domain-stats”>
<span>{domain.rules?.length || 0} rules</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
};
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// src/components/RuleManager/RuleManager.tsx
import React, { useState, useEffect } from ‘react’;
import { RuleDomain, Rule, getRules } from ‘../../api/rulesApi’;
import { RuleGrid } from ‘../RuleGrid/RuleGrid’;
import { RuleForm } from ‘../RuleForm/RuleForm’;
import Button from ‘../Shared/Button’;
interface RuleManagerProps {
domain: RuleDomain;
onBack: () => void;
}
export const RuleManager: React.FC<RuleManagerProps> = ({ domain, onBack }) => {
const [rules, setRules] = useState<Rule[]>([]);
const [editing, setEditing] = useState<Rule | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadRules();
}, [domain]);
const loadRules = async () => {
if (!domain.id) return;
setLoading(true);
const res = await getRules(domain.id);
setRules(res.data);
setLoading(false);
};
const handleSaved = () => {
setEditing(null);
loadRules();
};
if (loading) {
return <div className=”loading”>Loading rules…</div>;
}
return (
<div className=”rule-manager”>
<div className=”rule-manager-header”>
<Button variant=”outlined” onClick={onBack}>
← Back to Domains
</Button>
<h2>Rule Management: {domain.name}</h2>
</div>
{editing ? (
<RuleForm
domainId={domain.id!}
rule={editing}
onSaved={handleSaved}
onCancel={() => setEditing(null)}
/>
) : (
<>
<div className=”rule-actions”>
<Button
variant=”contained”
onClick={() => setEditing({ name: ‘’, conditions: [] })}
>
+ Create New Rule
</Button>
</div>
<RuleGrid
domainId={domain.id!}
rules={rules}
onEdit={setEditing}
onRefresh={loadRules}
/>
</>
)}
</div>
);
};
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// src/components/RuleGrid/RuleGrid.tsx
import React from ‘react’;
import { Rule, Condition, Operator, LogicalOperator } from ‘../../api/rulesApi’;
import { RunRuleDialog } from ‘../RunRuleDialog/RunRuleDialog’;
import Button from ‘../Shared/Button’;
import { AgGridReact } from ‘ag-grid-react’;
import ‘ag-grid-community/styles/ag-grid.css’;
import ‘ag-grid-community/styles/ag-theme-alpine.css’;
import ‘ag-grid-enterprise’;
interface RuleGridProps {
domainId: string;
rules: Rule[];
onEdit: (rule: Rule) => void;
onRefresh: () => void;
}
export const RuleGrid: React.FC<RuleGridProps> = ({
domainId, rules, onEdit, onRefresh
}) => {
const [runDialogOpen, setRunDialogOpen] = useState(false);
const [selectedRule, setSelectedRule] = useState<{id: string, name: string} | null>(null);
const handleRunClick = (id: string, name: string) => {
setSelectedRule({id, name});
setRunDialogOpen(true);
};
const formatCondition = (condition: Condition, index: number) => {
const operatorMap: Record<Operator, string> = {
equals: ‘=’,
notEqual: ‘≠’,
contains: ‘contains’,
notContains: ‘not contains’,
startsWith: ‘starts with’,
endsWith: ‘ends with’,
lessThan: ‘<’,
lessThanOrEqual: ‘≤’,
greaterThan: ‘>’,
greaterThanOrEqual: ‘≥’,
inRange: ‘between’,
blank: ‘is blank’,
notBlank: ‘is not blank’
};
let conditionStr = ‘’;
// Add logical operator if not the first condition
if (index > 0 && condition.logicalOperator) {
conditionStr += `${condition.logicalOperator} `;
}
if (condition.operator === ‘inRange’) {
conditionStr += `${condition.field} ${operatorMap[condition.operator]} ${condition.value} and ${condition.valueTo}`;
} else if ([‘blank’, ‘notBlank’].includes(condition.operator)) {
conditionStr += `${condition.field} ${operatorMap[condition.operator]}`;
} else {
conditionStr += `${condition.field} ${operatorMap[condition.operator]} ${condition.value}`;
}
return conditionStr;
};
const columns = [
{
headerName: ‘Name’,
field: ‘name’,
filter: ‘agTextColumnFilter’,
flex: 1,
cellStyle: { fontWeight: ‘bold’ }
},
{
headerName: ‘Conditions’,
autoHeight: true,
wrapText: true,
cellRenderer: (params: any) => {
const rule = params.data as Rule;
return (
<div style={{ padding: ‘8px 0’ }}>
{rule.conditions.map((cond, idx) => (
<div key={idx} style={{
marginBottom: ‘4px’,
padding: ‘4px 8px’,
backgroundColor: idx % 2 === 0 ? ‘#f8f9fa’ : ‘transparent’,
borderRadius: ‘4px’
}}>
{formatCondition(cond, idx)}
</div>
))}
</div>
);
},
flex: 3
},
{
headerName: ‘Actions’,
cellRenderer: (params: any) => {
const rule = params.data as Rule;
return (
<div className=”rule-actions”>
<Button
variant=”outlined”
onClick={() => onEdit(rule)}
>
Edit
</Button>
<Button
variant=”outlined”
color=”success”
onClick={() => handleRunClick(rule.id!, rule.name)}
>
Run
</Button>
</div>
);
},
sortable: false,
filter: false,
width: 200,
},
];
return (
<div className=”rule-grid-container”>
<div className=”grid-header”>
<h3>Rules in Domain</h3>
<Button variant=”outlined” onClick={onRefresh}>
Refresh Rules
</Button>
</div>
<div className=”ag-theme-alpine” style={{ height: 400, width: ‘100%’ }}>
<AgGridReact
rowData={rules}
columnDefs={columns}
rowSelection=”single”
animateRows={true}
pagination={true}
paginationPageSize={10}
domLayout=’autoHeight’
defaultColDef={{
resizable: true,
sortable: true,
filter: true,
}}
/>
</div>
{selectedRule && (
<RunRuleDialog
domainId={domainId}
open={runDialogOpen}
ruleId={selectedRule.id}
ruleName={selectedRule.name}
onClose={() => setRunDialogOpen(false)}
/>
)}
</div>
);
};
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// src/components/RuleForm/RuleForm.tsx
import React, { useState } from ‘react’;
import { Rule, Condition, Operator, LogicalOperator } from ‘../../api/rulesApi’;
import Button from ‘../Shared/Button’;
import { v4 as uuidv4 } from ‘uuid’;
interface RuleFormProps {
domainId: string;
rule?: Rule;
onSaved: () => void;
onCancel: () => void;
}
export const RuleForm: React.FC<RuleFormProps> = ({
domainId, rule, onSaved, onCancel
}) => {
const [name, setName] = useState(rule?.name || ‘’);
const [conditions, setConditions] = useState<Condition[]>(
rule?.conditions?.length ? […rule.conditions] : [{
id: uuidv4(),
field: ‘’,
operator: ‘equals’,
value: ‘’,
logicalOperator: ‘AND’
}]
);
const availableOperators: Operator[] = [
‘equals’, ‘notEqual’, ‘contains’, ‘notContains’,
‘startsWith’, ‘endsWith’, ‘lessThan’, ‘lessThanOrEqual’,
‘greaterThan’, ‘greaterThanOrEqual’, ‘inRange’, ‘blank’, ‘notBlank’
];
const updateCondition = (id: string, key: keyof Condition, value: any) => {
setConditions(conditions.map(cond =>
cond.id === id ? { …cond, [key]: value } : cond
));
};
const addCondition = () => {
setConditions([
…conditions,
{
id: uuidv4(),
field: ‘’,
operator: ‘equals’,
value: ‘’,
logicalOperator: ‘AND’
}
]);
};
const removeCondition = (id: string) => {
if (conditions.length > 1) {
setConditions(conditions.filter(cond => cond.id !== id));
}
};
const handleSubmit = async () => {
// Clean up condition objects before saving
const cleanConditions = conditions.map(({ id, …rest }) => rest);
const newRule: Rule = {
name,
conditions: cleanConditions
};
if (rule?.id) {
await updateRule(domainId, rule.id, newRule);
} else {
await createRule(domainId, newRule);
}
onSaved();
};
return (
<div className=”rule-form”>
<h2>{rule?.id ? ‘Edit Rule’ : ‘Create New Rule’}</h2>
<div className=”form-section”>
<label>Rule Name</label>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder=”Enter rule name”
/>
</div>
<div className=”form-section”>
<div className=”section-header”>
<h3>Conditions</h3>
<Button variant=”outlined” onClick={addCondition}>
+ Add Condition
</Button>
</div>
<div className=”conditions-list”>
{conditions.map((condition, index) => (
<div key={condition.id} className=”condition-card”>
<div className=”condition-header”>
<h4>Condition {index + 1}</h4>
{conditions.length > 1 && (
<Button
variant=”outlined”
color=”error”
onClick={() => removeCondition(condition.id!)}
>
Remove
</Button>
)}
</div>
{index > 0 && (
<div className=”form-group”>
<label>Logical Operator</label>
<select
value={condition.logicalOperator || ‘AND’}
onChange={e => updateCondition(condition.id!, ‘logicalOperator’, e.target.value as LogicalOperator)}
>
<option value=”AND”>AND</option>
<option value=”OR”>OR</option>
</select>
</div>
)}
<div className=”form-group”>
<label>Field</label>
<input
value={condition.field}
onChange={e => updateCondition(condition.id!, ‘field’, e.target.value)}
placeholder=”Enter field name”
/>
</div>
<div className=”form-group”>
<label>Operator</label>
<select
value={condition.operator}
onChange={e => updateCondition(condition.id!, ‘operator’, e.target.value as Operator)}
>
{availableOperators.map(op => (
<option key={op} value={op}>
{op.replace(/([A-Z])/g, ‘ $1’).trim()}
</option>
))}
</select>
</div>
{![‘blank’, ‘notBlank’].includes(condition.operator) && (
<div className=”form-group”>
<label>
{condition.operator === ‘inRange’ ? ‘Value Range’ : ‘Value’}
</label>
{condition.operator === ‘inRange’ ? (
<div className=”range-inputs”>
<input
type=”text”
value={condition.value || ‘’}
onChange={e => updateCondition(condition.id!, ‘value’, e.target.value)}
placeholder=”From”
/>
<span>to</span>
<input
type=”text”
value={condition.valueTo || ‘’}
onChange={e => updateCondition(condition.id!, ‘valueTo’, e.target.value)}
placeholder=”To”
/>
</div>
) : (
<input
type=”text”
value={condition.value || ‘’}
onChange={e => updateCondition(condition.id!, ‘value’, e.target.value)}
placeholder=”Enter value”
/>
)}
</div>
)}
</div>
))}
</div>
</div>
<div className=”form-actions”>
<Button variant=”outlined” onClick={onCancel}>
Cancel
</Button>
<Button variant=”contained” onClick={handleSubmit}>
Save Rule
</Button>
</div>
</div>
);
};
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// src/components/RunRuleDialog/RunRuleDialog.tsx
import React, { useState } from ‘react’;
import {
Dialog, DialogTitle, DialogContent, DialogActions,
TextField, Button, Alert, Collapse, IconButton, CircularProgress
} from ‘@mui/material’;
import CloseIcon from ‘@mui/icons-material/Close’;
import { RuleExecutionResult, runRule } from ‘../../api/rulesApi’;
interface RunRuleDialogProps {
domainId: string;
open: boolean;
ruleId: string;
ruleName: string;
onClose: () => void;
}
export const RunRuleDialog: React.FC<RunRuleDialogProps> = ({
domainId, open, ruleId, ruleName, onClose
}) => {
const [inputData, setInputData] = useState<string>(‘{\n \n}’);
const [error, setError] = useState<string>(‘’);
const [result, setResult] = useState<RuleExecutionResult | null>(null);
const [loading, setLoading] = useState(false);
Get Aamirmtasgaonkar’s stories in your inbox
Join Medium for free to get updates from this writer.
const handleRun = async () => {
try {
const parsedData = JSON.parse(inputData);
setLoading(true);
const response = await runRule(domainId, ruleId, parsedData);
setResult(response.data);
setError(‘’);
} catch (e) {
setError(e instanceof Error ? e.message : ‘Invalid JSON format’);
setResult(null);
} finally {
setLoading(false);
}
};
const handleClose = () => {
onClose();
setResult(null);
setInputData(‘{\n \n}’);
setError(‘’);
};
return (
<Dialog open={open} onClose={handleClose} maxWidth=”md” fullWidth>
<DialogTitle>Execute Rule: {ruleName}</DialogTitle>
<DialogContent>
<TextField
label=”Input Data (JSON)”
multiline
minRows={10}
maxRows={15}
value={inputData}
onChange={(e) => setInputData(e.target.value)}
fullWidth
variant=”outlined”
error={!!error}
helperText={error || “Enter valid JSON input for the rule”}
disabled={loading}
sx={{ mt: 2, fontFamily: ‘monospace’ }}
/>
<Collapse in={!!result}>
{result && (
<Alert
severity={result.matched ? “success” : “warning”}
action={
<IconButton
size=”small”
onClick={() => setResult(null)}
>
<CloseIcon fontSize=”inherit” />
</IconButton>
}
sx={{ mt: 2 }}
>
<strong>{result.matched ? “✅ Rule matched!” : “❌ Rule did not match”}</strong>
{result.message && <div style={{ marginTop: 8 }}>{result.message}</div>}
{result.details && (
<div style={{ marginTop: 8 }}>
<pre style={{ margin: 0, whiteSpace: ‘pre-wrap’ }}>
{JSON.stringify(result.details, null, 2)}
</pre>
</div>
)}
</Alert>
)}
</Collapse>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} disabled={loading}>Cancel</Button>
<Button
onClick={handleRun}
color=”primary”
variant=”contained”
disabled={loading}
endIcon={loading ? <CircularProgress size={20} /> : null}
>
{loading ? “Executing…” : “Execute”}
</Button>
</DialogActions>
</Dialog>
);
};
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// src/components/Shared/Button.tsx
import React from ‘react’;
import Button from ‘@mui/material/Button’;
interface ButtonProps {
variant?: ‘text’ | ‘outlined’ | ‘contained’;
color?: ‘inherit’ | ‘primary’ | ‘secondary’ | ‘success’ | ‘error’ | ‘info’ | ‘warning’;
onClick?: () => void;
children: React.ReactNode;
disabled?: boolean;
style?: React.CSSProperties;
}
export default function CustomButton({
variant = ‘contained’,
color = ‘primary’,
onClick,
children,
disabled = false,
style
}: ButtonProps) {
return (
<Button
variant={variant}
color={color}
onClick={onClick}
disabled={disabled}
style={style}
>
{children}
</Button>
);
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// src/App.tsx
import React, { useState } from ‘react’;
import { DomainManager } from ‘./components/DomainManager/DomainManager’;
import { RuleManager } from ‘./components/RuleManager/RuleManager’;
import { RuleDomain } from ‘./api/rulesApi’;
import ‘./App.css’;
const App: React.FC = () => {
const [selectedDomain, setSelectedDomain] = useState<RuleDomain | null>(null);
return (
<div className=”app-container”>
<h1 className=”app-title”>
Business Rules Management System
</h1>
{selectedDomain ? (
<RuleManager
domain={selectedDomain}
onBack={() => setSelectedDomain(null)}
/>
) : (
<DomainManager onSelectDomain={setSelectedDomain} />
)}
</div>
);
};
export default App;
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
/* src/App.css */
:root {
— primary: #1976d2;
— primary-dark: #1565c0;
— secondary: #dc004e;
— success: #4caf50;
— warning: #ff9800;
— error: #f44336;
— text: #333;
— text-light: #666;
— border: #ddd;
— background: #f5f7fa;
— card-bg: #fff;
— card-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: ‘Roboto’, -apple-system, BlinkMacSystemFont, ‘Segoe UI’, Arial, sans-serif;
background-color: var( — background);
color: var( — text);
line-height: 1.6;
}
.app-container {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
min-height: 100vh;
}
.app-title {
margin-bottom: 24px;
color: #1a3d7c;
border-bottom: 2px solid var( — primary);
padding-bottom: 12px;
text-align: center;
}
/* Domain Manager */
.domain-manager {
background: var( — card-bg);
border-radius: 8px;
padding: 24px;
box-shadow: var( — card-shadow);
}
.create-domain-form {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 10px;
border: 1px solid var( — border);
border-radius: 4px;
font-size: 16px;
}
.error-message {
color: var( — error);
margin-bottom: 15px;
padding: 10px;
background: #ffebee;
border-radius: 4px;
}
.domains-list h3 {
margin-bottom: 15px;
}
.no-domains {
text-align: center;
padding: 20px;
color: var( — text-light);
font-style: italic;
}
.domain-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.domain-card {
background: var( — card-bg);
border: 1px solid var( — border);
border-radius: 8px;
padding: 20px;
box-shadow: var( — card-shadow);
transition: transform 0.2s;
}
.domain-card:hover {
transform: translateY(-5px);
}
.domain-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.domain-header h4 {
font-size: 18px;
color: var( — primary);
}
.domain-actions {
display: flex;
gap: 8px;
}
.domain-description {
color: var( — text-light);
margin-bottom: 15px;
min-height: 60px;
}
.domain-stats {
display: flex;
justify-content: space-between;
color: var( — text-light);
font-size: 14px;
}
/* Rule Manager */
.rule-manager {
background: var( — card-bg);
border-radius: 8px;
padding: 24px;
box-shadow: var( — card-shadow);
}
.rule-manager-header {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}
.rule-manager-header h2 {
flex-grow: 1;
}
.rule-actions {
margin-bottom: 20px;
display: flex;
justify-content: flex-end;
}
/* Rule Form */
.rule-form {
background: var( — card-bg);
border-radius: 8px;
padding: 24px;
box-shadow: var( — card-shadow);
}
.form-section {
margin-bottom: 24px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.conditions-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.condition-card {
background: #f8f9fa;
border-radius: 8px;
padding: 15px;
border: 1px solid var( — border);
}
.condition-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.range-inputs {
display: flex;
align-items: center;
gap: 10px;
}
.range-inputs span {
color: var( — text-light);
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
border-top: 1px solid var( — border);
padding-top: 24px;
}
/* Rule Grid */
.rule-grid-container {
margin-top: 20px;
}
.grid-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.ag-theme-alpine {
border-radius: 8px;
overflow: hidden;
box-shadow: var( — card-shadow);
}
/* Utilities */
.loading {
text-align: center;
padding: 40px;
font-size: 18px;
color: var( — text-light);
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — -





