aibos-design-system 1.0.0 → 1.0.1
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.
- package/LICENSE +21 -0
- package/README.md +335 -333
- package/dist/headless-map.json +44 -41
- package/dist/tokens/index.d.ts +66 -66
- package/dist/tokens.json +34 -34
- package/{API_REFERENCE.md → docs/API_REFERENCE.md} +379 -379
- package/{EXTERNAL_USAGE.md → docs/EXTERNAL_USAGE.md} +372 -370
- package/docs/INTEGRATION_GUIDE.md +433 -0
- package/docs/QUICK_REFERENCE.md +303 -0
- package/input.css +4056 -4050
- package/lib/cli-autocomplete.ts +231 -0
- package/lib/cli-commands.ts +364 -0
- package/lib/cli-filter-engine.ts +271 -0
- package/lib/cli-parser.ts +197 -0
- package/lib/utils.ts +18 -0
- package/package.json +11 -4
- package/style.css +683 -237
|
@@ -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
|
+
'&': '&',
|
|
126
|
+
'<': '<',
|
|
127
|
+
'>': '>',
|
|
128
|
+
'"': '"',
|
|
129
|
+
"'": ''',
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aibos-design-system",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Enterprise-grade design system with 254 tokens, 171 semantic classes, and Beast Mode patterns. Zero framework overhead, 100% Figma compliant.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
"format:check": "prettier --check \"**/*.{css,js,ts,json,md}\"",
|
|
38
38
|
"validate": "node scripts/validate-design-tokens.js",
|
|
39
39
|
"validate:all": "pnpm lint && pnpm validate && pnpm enforce:semantics",
|
|
40
|
-
"quality": "pnpm validate:all"
|
|
40
|
+
"quality": "pnpm validate:all",
|
|
41
|
+
"prepublishOnly": "pnpm build",
|
|
42
|
+
"prepare": "pnpm build"
|
|
41
43
|
},
|
|
42
44
|
"main": "./style.css",
|
|
43
45
|
"exports": {
|
|
@@ -50,9 +52,14 @@
|
|
|
50
52
|
"style.css",
|
|
51
53
|
"input.css",
|
|
52
54
|
"dist/**/*",
|
|
55
|
+
"lib/**/*.ts",
|
|
56
|
+
"lib/**/*.js",
|
|
53
57
|
"README.md",
|
|
54
|
-
"API_REFERENCE.md",
|
|
55
|
-
"EXTERNAL_USAGE.md"
|
|
58
|
+
"docs/API_REFERENCE.md",
|
|
59
|
+
"docs/EXTERNAL_USAGE.md",
|
|
60
|
+
"docs/QUICK_REFERENCE.md",
|
|
61
|
+
"docs/INTEGRATION_GUIDE.md",
|
|
62
|
+
"LICENSE"
|
|
56
63
|
],
|
|
57
64
|
"devDependencies": {
|
|
58
65
|
"@tailwindcss/postcss": "^4.1.18",
|