aibos-design-system 1.0.0 → 1.1.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.
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Filter Engine: Translator between CLI and Table
3
+ *
4
+ * This is the missing link that makes the CLI control the table.
5
+ * It parses tokens from the CLI parser and applies them to row data.
6
+ *
7
+ * See docs/CLI_FILTER_INTEGRATION.md for usage examples.
8
+ */
9
+
10
+ export interface FilterToken {
11
+ type: 'key-value' | 'operator' | 'raw';
12
+ key?: string;
13
+ operator?: string;
14
+ value?: string;
15
+ text?: string;
16
+ }
17
+
18
+ export interface FilterContext {
19
+ tokens: FilterToken[];
20
+ matchCount: number;
21
+ totalCount: number;
22
+ }
23
+
24
+ export interface AggregateMetrics {
25
+ count: number;
26
+ revenue: { total: number; average: number };
27
+ health: { total: number; average: number; distribution: Record<string, number> };
28
+ status: Record<string, number>;
29
+ riskLevel: 'low' | 'medium' | 'high' | 'critical';
30
+ description: string;
31
+ }
32
+
33
+ /**
34
+ * FilterEngine
35
+ *
36
+ * Accepts parsed CLI tokens and applies them to a dataset.
37
+ * Handles:
38
+ * - Enum values (status:healthy)
39
+ * - Numeric operators (revenue>100000, health<70)
40
+ * - String matching (owner:"A. Patel")
41
+ * - AND logic (all filters must match)
42
+ */
43
+ export class FilterEngine {
44
+ /**
45
+ * Parse CLI input into filter tokens
46
+ *
47
+ * Converts raw CLI text like "status:healthy owner:chen revenue>100000"
48
+ * into structured FilterToken objects.
49
+ */
50
+ parseFilters(input: string): FilterToken[] {
51
+ if (!input.trim()) return [];
52
+
53
+ const tokens: FilterToken[] = [];
54
+
55
+ // Match patterns like "key:value", "key>value", "key<value", etc.
56
+ const regex = /([a-z-]+)([:<>=!]*)"?([^"\s]+)"?/gi;
57
+ let match;
58
+
59
+ while ((match = regex.exec(input)) !== null) {
60
+ const key = match[1].toLowerCase();
61
+ const operator = match[2] || '=';
62
+ const value = match[3];
63
+
64
+ tokens.push({
65
+ type: 'key-value',
66
+ key,
67
+ operator,
68
+ value,
69
+ text: match[0],
70
+ });
71
+ }
72
+
73
+ return tokens;
74
+ }
75
+
76
+ /**
77
+ * Apply parsed filters to rows
78
+ *
79
+ * Returns only rows where ALL filters match (AND logic).
80
+ * Handles type coercion (numeric, date, enum).
81
+ */
82
+ applyFilters(
83
+ rows: any[],
84
+ tokens: FilterToken[],
85
+ fieldMap?: Record<string, string>
86
+ ): any[] {
87
+ if (tokens.length === 0) return rows;
88
+
89
+ return rows.filter(row => {
90
+ return tokens.every(token => {
91
+ if (token.type !== 'key-value') return true;
92
+
93
+ const { key, operator, value } = token;
94
+ if (!key || !value) return true;
95
+
96
+ // Map CLI key to actual data field
97
+ const fieldName = fieldMap?.[key] || key;
98
+ const rowValue = row[fieldName] || row.dataset?.[fieldName];
99
+
100
+ // Type checking and comparison
101
+ return this.compareValues(rowValue, operator || '=', value);
102
+ });
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Compare row value against filter criteria
108
+ *
109
+ * Handles:
110
+ * - String equality: "healthy" === "healthy"
111
+ * - String contains: "patel".includes("tel")
112
+ * - Numeric comparison: 100000 > 50000
113
+ * - Date comparison: "2024-01-01" > "2023-12-31"
114
+ */
115
+ private compareValues(
116
+ rowValue: any,
117
+ operator: string,
118
+ filterValue: string
119
+ ): boolean {
120
+ // Normalize row value
121
+ const normalizedRow = String(rowValue).toLowerCase().trim();
122
+ const normalizedFilter = filterValue.toLowerCase().trim();
123
+
124
+ // Handle numeric operators
125
+ if (['>','<','>=','<=','=','!='].includes(operator)) {
126
+ const rowNum = parseFloat(normalizedRow);
127
+ const filterNum = parseFloat(normalizedFilter);
128
+
129
+ if (!isNaN(rowNum) && !isNaN(filterNum)) {
130
+ switch (operator) {
131
+ case '>': return rowNum > filterNum;
132
+ case '<': return rowNum < filterNum;
133
+ case '>=': return rowNum >= filterNum;
134
+ case '<=': return rowNum <= filterNum;
135
+ case '=': return rowNum === filterNum;
136
+ case '!=': return rowNum !== filterNum;
137
+ }
138
+ }
139
+ }
140
+
141
+ // Handle string matching (equality by default, or contains)
142
+ if (operator === '=' || operator === ':') {
143
+ return normalizedRow === normalizedFilter || normalizedRow.includes(normalizedFilter);
144
+ }
145
+
146
+ if (operator === '!=') {
147
+ return normalizedRow !== normalizedFilter;
148
+ }
149
+
150
+ return false;
151
+ }
152
+
153
+ /**
154
+ * Get filter statistics
155
+ */
156
+ getStats(rows: any[], matchedRows: any[]): FilterContext {
157
+ return {
158
+ tokens: [],
159
+ matchCount: matchedRows.length,
160
+ totalCount: rows.length,
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Generate human-readable filter description
166
+ *
167
+ * Converts "status:healthy revenue>100000" into
168
+ * "Status is healthy AND Revenue greater than 100000"
169
+ */
170
+ describeFilters(tokens: FilterToken[]): string {
171
+ return tokens
172
+ .filter(t => t.type === 'key-value' && t.key && t.value)
173
+ .map(t => {
174
+ const opSymbol = {
175
+ ':': 'is',
176
+ '=': 'equals',
177
+ '>': 'greater than',
178
+ '<': 'less than',
179
+ '>=': 'at least',
180
+ '<=': 'at most',
181
+ '!=': 'not equals',
182
+ }[t.operator || '='] || 'is';
183
+
184
+ return `${t.key} ${opSymbol} ${t.value}`;
185
+ })
186
+ .join(' AND ');
187
+ }
188
+
189
+ /**
190
+ * Calculate aggregate metrics from filtered rows
191
+ *
192
+ * Provides high-level insights:
193
+ * - Total and average revenue
194
+ * - Average health score
195
+ * - Status distribution
196
+ * - Risk assessment
197
+ *
198
+ * This is the "HUD" data for the Decision Engine.
199
+ */
200
+ aggregateMetrics(rows: any[]): AggregateMetrics {
201
+ if (rows.length === 0) {
202
+ return {
203
+ count: 0,
204
+ revenue: { total: 0, average: 0 },
205
+ health: { total: 0, average: 0, distribution: {} },
206
+ status: {},
207
+ riskLevel: 'low',
208
+ description: 'No data to analyze',
209
+ };
210
+ }
211
+
212
+ // Revenue metrics
213
+ const revenues = rows
214
+ .map(row => parseFloat(row.dataset?.revenue || row.revenue || 0))
215
+ .filter(v => !isNaN(v));
216
+ const totalRevenue = revenues.reduce((sum, v) => sum + v, 0);
217
+ const avgRevenue = revenues.length > 0 ? totalRevenue / revenues.length : 0;
218
+
219
+ // Health metrics
220
+ const healths = rows
221
+ .map(row => parseFloat(row.dataset?.health || row.health || 0))
222
+ .filter(v => !isNaN(v));
223
+ const totalHealth = healths.reduce((sum, v) => sum + v, 0);
224
+ const avgHealth = healths.length > 0 ? totalHealth / healths.length : 0;
225
+
226
+ // Status distribution
227
+ const statusCounts: Record<string, number> = {};
228
+ rows.forEach(row => {
229
+ const status = (row.dataset?.status || row.status || 'unknown').toLowerCase();
230
+ statusCounts[status] = (statusCounts[status] || 0) + 1;
231
+ });
232
+
233
+ // Health distribution (buckets)
234
+ const healthBuckets: Record<string, number> = {
235
+ 'critical': 0, // < 40
236
+ 'poor': 0, // 40-60
237
+ 'fair': 0, // 60-80
238
+ 'good': 0, // 80-95
239
+ 'excellent': 0, // 95+
240
+ };
241
+ healths.forEach(h => {
242
+ if (h < 40) healthBuckets.critical++;
243
+ else if (h < 60) healthBuckets.poor++;
244
+ else if (h < 80) healthBuckets.fair++;
245
+ else if (h < 95) healthBuckets.good++;
246
+ else healthBuckets.excellent++;
247
+ });
248
+
249
+ // Risk assessment
250
+ const watchCount = statusCounts['watch'] || 0;
251
+ const criticalCount = healthBuckets.critical;
252
+ const riskScore = (watchCount * 0.5) + (criticalCount * 0.3);
253
+
254
+ let riskLevel: 'low' | 'medium' | 'high' | 'critical' = 'low';
255
+ if (riskScore >= 2) riskLevel = 'critical';
256
+ else if (riskScore >= 1.5) riskLevel = 'high';
257
+ else if (riskScore >= 0.75) riskLevel = 'medium';
258
+
259
+ // Trend indicator
260
+ const trend = avgRevenue > 0 ? (avgHealth / 100) > 0.8 ? '↗' : '↘' : '→';
261
+
262
+ return {
263
+ count: rows.length,
264
+ revenue: { total: totalRevenue, average: avgRevenue },
265
+ health: { total: totalHealth, average: avgHealth, distribution: healthBuckets },
266
+ status: statusCounts,
267
+ riskLevel,
268
+ description: `${rows.length} accounts analyzed. ${trend} ${avgHealth.toFixed(0)}% avg health. ${riskLevel === 'low' ? '✓ Clean' : `⚠ ${riskLevel.toUpperCase()}`}`,
269
+ };
270
+ }
271
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * NEO-ANALOG CLI PARSER
3
+ *
4
+ * Tokenizes user input for data table filters with governance rules.
5
+ * Syntax: `key:value >operator <number status:active`
6
+ *
7
+ * Example:
8
+ * - `status:active` → key="status", value="active"
9
+ * - `>100` → operator=">", value="100"
10
+ * - `error` → raw value="error"
11
+ */
12
+
13
+ export interface FilterToken {
14
+ type: 'key' | 'operator' | 'value' | 'raw';
15
+ text: string;
16
+ start: number;
17
+ end: number;
18
+ }
19
+
20
+ export interface ParsedFilter {
21
+ tokens: FilterToken[];
22
+ hasKey: boolean;
23
+ hasOperator: boolean;
24
+ raw: string;
25
+ }
26
+
27
+ /**
28
+ * Parse search input into semantic tokens
29
+ * Supports: `key:value`, `><=`, `"quoted values"`, and raw words
30
+ */
31
+ export function parseSearchInput(input: string): FilterToken[] {
32
+ const tokens: FilterToken[] = [];
33
+
34
+ // Regex pattern:
35
+ // - ([a-zA-Z0-9_-]+:) = key with colon (e.g., "status:")
36
+ // - ([><=!]+) = operators (e.g., ">", "<=", "!=")
37
+ // - (".*?"|[^"\s]+) = quoted strings or unquoted words
38
+ // - (\s+) = whitespace (ignored)
39
+ const regex = /([a-zA-Z0-9_-]+:)|([><=!]+)|(".*?"|[^"\s]+)|(\s+)/g;
40
+
41
+ let match;
42
+ while ((match = regex.exec(input)) !== null) {
43
+ const [fullMatch, key, operator, value, whitespace] = match;
44
+ const start = match.index;
45
+ const end = start + fullMatch.length;
46
+
47
+ if (whitespace) continue; // Skip pure whitespace
48
+
49
+ if (key) {
50
+ tokens.push({ type: 'key', text: key, start, end });
51
+ } else if (operator) {
52
+ tokens.push({ type: 'operator', text: operator, start, end });
53
+ } else if (value) {
54
+ // Remove quotes if present
55
+ const cleanValue = value.startsWith('"') && value.endsWith('"')
56
+ ? value.slice(1, -1)
57
+ : value;
58
+ tokens.push({ type: 'value', text: cleanValue, start, end });
59
+ } else {
60
+ tokens.push({ type: 'raw', text: fullMatch, start, end });
61
+ }
62
+ }
63
+
64
+ return tokens;
65
+ }
66
+
67
+ /**
68
+ * Advanced parser that returns structured filter object
69
+ */
70
+ export function parseFilter(input: string): ParsedFilter {
71
+ const tokens = parseSearchInput(input);
72
+ const hasKey = tokens.some(t => t.type === 'key');
73
+ const hasOperator = tokens.some(t => t.type === 'operator');
74
+
75
+ return {
76
+ tokens,
77
+ hasKey,
78
+ hasOperator,
79
+ raw: input,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Generate HTML with syntax highlighting for display
85
+ * Uses Neo-Analog color scheme:
86
+ * - Keys (blue) = Data field names
87
+ * - Operators (amber) = Comparison symbols
88
+ * - Values (emerald) = Data values
89
+ * - Raw (gray) = Unrecognized tokens
90
+ */
91
+ export function highlightCommand(input: string, colorScheme: 'neo-analog' | 'monochrome' = 'neo-analog'): string {
92
+ const tokens = parseSearchInput(input);
93
+ let html = '';
94
+
95
+ const colors = {
96
+ 'neo-analog': {
97
+ key: 'text-blue-600 font-semibold', // Keywords
98
+ operator: 'text-amber-600 font-bold', // Operators
99
+ value: 'text-emerald-700', // Values
100
+ raw: 'text-gray-600', // Unrecognized
101
+ },
102
+ 'monochrome': {
103
+ key: 'text-gray-900 font-semibold',
104
+ operator: 'text-gray-900 font-bold',
105
+ value: 'text-gray-900',
106
+ raw: 'text-gray-600 italic',
107
+ },
108
+ };
109
+
110
+ const palette = colors[colorScheme];
111
+
112
+ tokens.forEach(t => {
113
+ const colorClass = palette[t.type];
114
+ html += `<span class="${colorClass}">${escapeHtml(t.text)}</span> `;
115
+ });
116
+
117
+ return html.trim();
118
+ }
119
+
120
+ /**
121
+ * Escape HTML special characters
122
+ */
123
+ function escapeHtml(text: string): string {
124
+ const map: { [key: string]: string } = {
125
+ '&': '&amp;',
126
+ '<': '&lt;',
127
+ '>': '&gt;',
128
+ '"': '&quot;',
129
+ "'": '&#039;',
130
+ };
131
+ return text.replace(/[&<>"']/g, char => map[char]);
132
+ }
133
+
134
+ /**
135
+ * Extract semantic key-value pairs from parsed tokens
136
+ * Example: "status:active" → { status: "active" }
137
+ */
138
+ export function extractKeyValues(tokens: FilterToken[]): Record<string, string[]> {
139
+ const result: Record<string, string[]> = {};
140
+
141
+ for (let i = 0; i < tokens.length; i++) {
142
+ const token = tokens[i];
143
+ if (token.type === 'key') {
144
+ const key = token.text.slice(0, -1); // Remove trailing ':'
145
+ const nextToken = tokens[i + 1];
146
+ const value = nextToken?.type === 'value' ? nextToken.text : '';
147
+
148
+ if (!result[key]) result[key] = [];
149
+ if (value) result[key].push(value);
150
+ }
151
+ }
152
+
153
+ return result;
154
+ }
155
+
156
+ /**
157
+ * Filter array of objects based on parsed CLI input
158
+ * @param data Array of objects to filter
159
+ * @param input CLI-style filter string
160
+ * @param keyMapping Map semantic keys to object properties
161
+ */
162
+ export function filterData<T extends Record<string, any>>(
163
+ data: T[],
164
+ input: string,
165
+ keyMapping?: Record<string, string>
166
+ ): T[] {
167
+ const parsed = parseFilter(input);
168
+ const keyValues = extractKeyValues(parsed.tokens);
169
+
170
+ // If no structured keys, do simple text search
171
+ if (!parsed.hasKey) {
172
+ const searchTerms = parsed.tokens
173
+ .filter(t => t.type === 'value' || t.type === 'raw')
174
+ .map(t => t.text.toLowerCase());
175
+
176
+ return data.filter(item =>
177
+ Object.values(item).some(val =>
178
+ searchTerms.some(term =>
179
+ String(val).toLowerCase().includes(term)
180
+ )
181
+ )
182
+ );
183
+ }
184
+
185
+ // Filter by key-value pairs
186
+ return data.filter(item => {
187
+ for (const [key, values] of Object.entries(keyValues)) {
188
+ const propKey = keyMapping?.[key] ?? key;
189
+ const itemValue = String(item[propKey] ?? '').toLowerCase();
190
+
191
+ if (!values.some(v => itemValue.includes(v.toLowerCase()))) {
192
+ return false;
193
+ }
194
+ }
195
+ return true;
196
+ });
197
+ }
package/lib/utils.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ /**
5
+ * Utility function to merge Tailwind CSS classes with design system classes
6
+ * Combines clsx for conditional classes and tailwind-merge for conflict resolution
7
+ *
8
+ * @param inputs - Class values (strings, objects, arrays)
9
+ * @returns Merged class string
10
+ *
11
+ * @example
12
+ * cn("na-btn", "na-btn-primary", isActive && "na-btn-active")
13
+ * cn("px-4", "py-2", className)
14
+ */
15
+ export function cn(...inputs: ClassValue[]) {
16
+ return twMerge(clsx(inputs))
17
+ }
18
+
package/package.json CHANGED
@@ -1,18 +1,21 @@
1
1
  {
2
2
  "name": "aibos-design-system",
3
- "version": "1.0.0",
4
- "description": "Enterprise-grade design system with 254 tokens, 171 semantic classes, and Beast Mode patterns. Zero framework overhead, 100% Figma compliant.",
3
+ "version": "1.1.0",
4
+ "description": "Enterprise-grade design system with 254 tokens, 171 semantic classes, and Beast Mode patterns. Includes React components for Next.js integration. Zero framework overhead, 100% Figma compliant.",
5
5
  "type": "module",
6
- "keywords": [
6
+ "keywords": [
7
7
  "design-system",
8
8
  "css",
9
9
  "tokens",
10
10
  "ui",
11
11
  "components",
12
+ "react",
13
+ "nextjs",
12
14
  "tailwind",
13
15
  "design-tokens",
14
16
  "neural-analog",
15
- "enterprise"
17
+ "enterprise",
18
+ "typescript"
16
19
  ],
17
20
  "author": "AI-BOS",
18
21
  "license": "MIT",
@@ -37,23 +40,69 @@
37
40
  "format:check": "prettier --check \"**/*.{css,js,ts,json,md}\"",
38
41
  "validate": "node scripts/validate-design-tokens.js",
39
42
  "validate:all": "pnpm lint && pnpm validate && pnpm enforce:semantics",
40
- "quality": "pnpm validate:all"
43
+ "quality": "pnpm validate:all",
44
+ "prepublishOnly": "pnpm build",
45
+ "prepare": "pnpm build"
41
46
  },
42
47
  "main": "./style.css",
43
48
  "exports": {
44
49
  ".": "./style.css",
45
50
  "./css": "./style.css",
46
51
  "./tokens": "./dist/tokens.json",
47
- "./tokens/typescript": "./dist/tokens/index.d.ts"
52
+ "./tokens/typescript": "./dist/tokens/index.d.ts",
53
+ "./react": {
54
+ "types": "./components/react/index.ts",
55
+ "import": "./components/react/index.ts",
56
+ "require": "./components/react/index.ts"
57
+ },
58
+ "./types": {
59
+ "types": "./types/index.ts",
60
+ "import": "./types/index.ts",
61
+ "require": "./types/index.ts"
62
+ },
63
+ "./utils": {
64
+ "types": "./components/utils.ts",
65
+ "import": "./components/utils.ts",
66
+ "require": "./components/utils.ts"
67
+ },
68
+ "./design-tokens": {
69
+ "types": "./types/design-tokens.ts",
70
+ "import": "./types/design-tokens.ts",
71
+ "require": "./types/design-tokens.ts"
72
+ }
48
73
  },
49
74
  "files": [
50
75
  "style.css",
51
76
  "input.css",
52
77
  "dist/**/*",
78
+ "lib/**/*.ts",
79
+ "lib/**/*.js",
80
+ "components/**/*.ts",
81
+ "components/**/*.tsx",
82
+ "types/**/*.ts",
53
83
  "README.md",
54
- "API_REFERENCE.md",
55
- "EXTERNAL_USAGE.md"
84
+ "docs/API_REFERENCE.md",
85
+ "docs/EXTERNAL_USAGE.md",
86
+ "docs/QUICK_REFERENCE.md",
87
+ "docs/INTEGRATION_GUIDE.md",
88
+ "LICENSE"
56
89
  ],
90
+ "peerDependencies": {
91
+ "react": ">=16.8.0",
92
+ "react-dom": ">=16.8.0",
93
+ "@nextui-org/react": ">=2.0.0"
94
+ },
95
+ "peerDependenciesMeta": {
96
+ "react": {
97
+ "optional": true
98
+ },
99
+ "react-dom": {
100
+ "optional": true
101
+ },
102
+ "@nextui-org/react": {
103
+ "optional": true
104
+ }
105
+ },
57
106
  "devDependencies": {
58
107
  "@tailwindcss/postcss": "^4.1.18",
59
108
  "autoprefixer": "^10.4.23",