@clypra/ui 1.0.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,428 @@
1
+ /**
2
+ * ValidationPanel Component
3
+ *
4
+ * Shows errors and warnings from effect validation.
5
+ * Categorized by type with actionable suggestions.
6
+ */
7
+
8
+ import React, { useState } from "react";
9
+
10
+ export type ValidationSeverity = "error" | "warning" | "info";
11
+
12
+ export interface ValidationIssue {
13
+ id: string;
14
+ severity: ValidationSeverity;
15
+ category: string;
16
+ message: string;
17
+ details?: string;
18
+ suggestion?: string;
19
+ location?: {
20
+ node?: string;
21
+ pass?: number;
22
+ line?: number;
23
+ };
24
+ }
25
+
26
+ export interface ValidationPanelProps {
27
+ /** Validation issues to display */
28
+ issues: ValidationIssue[];
29
+ /** Callback when an issue is clicked */
30
+ onIssueClick?: (issue: ValidationIssue) => void;
31
+ /** Show only specific severity levels */
32
+ severityFilter?: ValidationSeverity[];
33
+ /** Show only specific categories */
34
+ categoryFilter?: string[];
35
+ }
36
+
37
+ export function ValidationPanel({ issues, onIssueClick, severityFilter, categoryFilter }: ValidationPanelProps) {
38
+ const [expandedIssues, setExpandedIssues] = useState<Set<string>>(new Set());
39
+ const [activeSeverities, setActiveSeverities] = useState<Set<ValidationSeverity>>(new Set(["error", "warning", "info"]));
40
+ const [activeCategories, setActiveCategories] = useState<Set<string>>(new Set());
41
+
42
+ // Get all unique categories
43
+ const allCategories = Array.from(new Set(issues.map((i) => i.category))).sort();
44
+
45
+ // Initialize active categories
46
+ React.useEffect(() => {
47
+ if (activeCategories.size === 0 && allCategories.length > 0) {
48
+ setActiveCategories(new Set(allCategories));
49
+ }
50
+ }, [allCategories.length]);
51
+
52
+ // Apply filters
53
+ const filteredIssues = issues.filter((issue) => {
54
+ const severityMatch = !severityFilter || severityFilter.length === 0 || severityFilter.includes(issue.severity) || activeSeverities.has(issue.severity);
55
+
56
+ const categoryMatch = !categoryFilter || categoryFilter.length === 0 || categoryFilter.includes(issue.category) || activeCategories.has(issue.category);
57
+
58
+ return severityMatch && categoryMatch;
59
+ });
60
+
61
+ // Count by severity
62
+ const counts = {
63
+ error: issues.filter((i) => i.severity === "error").length,
64
+ warning: issues.filter((i) => i.severity === "warning").length,
65
+ info: issues.filter((i) => i.severity === "info").length,
66
+ };
67
+
68
+ const toggleExpanded = (issueId: string) => {
69
+ const newExpanded = new Set(expandedIssues);
70
+ if (newExpanded.has(issueId)) {
71
+ newExpanded.delete(issueId);
72
+ } else {
73
+ newExpanded.add(issueId);
74
+ }
75
+ setExpandedIssues(newExpanded);
76
+ };
77
+
78
+ const toggleSeverity = (severity: ValidationSeverity) => {
79
+ const newSeverities = new Set(activeSeverities);
80
+ if (newSeverities.has(severity)) {
81
+ newSeverities.delete(severity);
82
+ } else {
83
+ newSeverities.add(severity);
84
+ }
85
+ setActiveSeverities(newSeverities);
86
+ };
87
+
88
+ const toggleCategory = (category: string) => {
89
+ const newCategories = new Set(activeCategories);
90
+ if (newCategories.has(category)) {
91
+ newCategories.delete(category);
92
+ } else {
93
+ newCategories.add(category);
94
+ }
95
+ setActiveCategories(newCategories);
96
+ };
97
+
98
+ const getSeverityColor = (severity: ValidationSeverity) => {
99
+ switch (severity) {
100
+ case "error":
101
+ return { bg: "#7f1d1d", border: "#ef4444", text: "#fca5a5" };
102
+ case "warning":
103
+ return { bg: "#713f12", border: "#f59e0b", text: "#fcd34d" };
104
+ case "info":
105
+ return { bg: "#1e3a8a", border: "#3b82f6", text: "#93c5fd" };
106
+ }
107
+ };
108
+
109
+ const getSeverityIcon = (severity: ValidationSeverity) => {
110
+ switch (severity) {
111
+ case "error":
112
+ return "✖";
113
+ case "warning":
114
+ return "âš ";
115
+ case "info":
116
+ return "ℹ";
117
+ }
118
+ };
119
+
120
+ return (
121
+ <div
122
+ style={{
123
+ padding: "16px",
124
+ background: "#0f172a",
125
+ borderRadius: "8px",
126
+ border: "1px solid #334155",
127
+ }}
128
+ >
129
+ {/* Header */}
130
+ <div
131
+ style={{
132
+ display: "flex",
133
+ justifyContent: "space-between",
134
+ alignItems: "center",
135
+ marginBottom: "16px",
136
+ }}
137
+ >
138
+ <h3 style={{ margin: 0, color: "#f1f5f9", fontSize: "16px" }}>Validation Results</h3>
139
+ <div
140
+ style={{
141
+ display: "flex",
142
+ gap: "12px",
143
+ fontSize: "14px",
144
+ fontWeight: 600,
145
+ }}
146
+ >
147
+ <button
148
+ onClick={() => toggleSeverity("error")}
149
+ style={{
150
+ padding: "4px 10px",
151
+ background: activeSeverities.has("error") ? "#7f1d1d" : "#334155",
152
+ color: activeSeverities.has("error") ? "#fca5a5" : "#94a3b8",
153
+ border: "none",
154
+ borderRadius: "4px",
155
+ cursor: "pointer",
156
+ }}
157
+ >
158
+ ✖ {counts.error}
159
+ </button>
160
+ <button
161
+ onClick={() => toggleSeverity("warning")}
162
+ style={{
163
+ padding: "4px 10px",
164
+ background: activeSeverities.has("warning") ? "#713f12" : "#334155",
165
+ color: activeSeverities.has("warning") ? "#fcd34d" : "#94a3b8",
166
+ border: "none",
167
+ borderRadius: "4px",
168
+ cursor: "pointer",
169
+ }}
170
+ >
171
+ âš  {counts.warning}
172
+ </button>
173
+ <button
174
+ onClick={() => toggleSeverity("info")}
175
+ style={{
176
+ padding: "4px 10px",
177
+ background: activeSeverities.has("info") ? "#1e3a8a" : "#334155",
178
+ color: activeSeverities.has("info") ? "#93c5fd" : "#94a3b8",
179
+ border: "none",
180
+ borderRadius: "4px",
181
+ cursor: "pointer",
182
+ }}
183
+ >
184
+ ℹ {counts.info}
185
+ </button>
186
+ </div>
187
+ </div>
188
+
189
+ {/* Category filters */}
190
+ {allCategories.length > 0 && (
191
+ <div style={{ marginBottom: "16px" }}>
192
+ <div
193
+ style={{
194
+ fontSize: "12px",
195
+ color: "#94a3b8",
196
+ marginBottom: "8px",
197
+ fontWeight: 600,
198
+ }}
199
+ >
200
+ Categories:
201
+ </div>
202
+ <div style={{ display: "flex", gap: "6px", flexWrap: "wrap" }}>
203
+ {allCategories.map((category) => (
204
+ <button
205
+ key={category}
206
+ onClick={() => toggleCategory(category)}
207
+ style={{
208
+ padding: "4px 10px",
209
+ background: activeCategories.has(category) ? "#3b82f6" : "#334155",
210
+ color: "white",
211
+ border: "none",
212
+ borderRadius: "4px",
213
+ cursor: "pointer",
214
+ fontSize: "12px",
215
+ }}
216
+ >
217
+ {category}
218
+ </button>
219
+ ))}
220
+ </div>
221
+ </div>
222
+ )}
223
+
224
+ {/* Issues list */}
225
+ <div style={{ maxHeight: "500px", overflowY: "auto" }}>
226
+ {filteredIssues.length === 0 ? (
227
+ <div
228
+ style={{
229
+ padding: "48px 32px",
230
+ textAlign: "center",
231
+ color: "#64748b",
232
+ fontSize: "14px",
233
+ }}
234
+ >
235
+ {issues.length === 0 ? (
236
+ <div>
237
+ <div style={{ fontSize: "32px", marginBottom: "12px" }}>✓</div>
238
+ <div style={{ color: "#10b981", fontWeight: 600, fontSize: "16px" }}>No validation issues</div>
239
+ <div style={{ marginTop: "8px" }}>Everything looks good!</div>
240
+ </div>
241
+ ) : (
242
+ <div>No issues match your filters</div>
243
+ )}
244
+ </div>
245
+ ) : (
246
+ <div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
247
+ {filteredIssues.map((issue) => {
248
+ const colors = getSeverityColor(issue.severity);
249
+ const icon = getSeverityIcon(issue.severity);
250
+ const isExpanded = expandedIssues.has(issue.id);
251
+
252
+ return (
253
+ <div
254
+ key={issue.id}
255
+ style={{
256
+ padding: "12px",
257
+ background: "#1e293b",
258
+ border: `1px solid ${colors.border}`,
259
+ borderLeft: `4px solid ${colors.border}`,
260
+ borderRadius: "6px",
261
+ cursor: "pointer",
262
+ }}
263
+ onClick={() => {
264
+ toggleExpanded(issue.id);
265
+ onIssueClick?.(issue);
266
+ }}
267
+ >
268
+ {/* Issue header */}
269
+ <div
270
+ style={{
271
+ display: "flex",
272
+ alignItems: "start",
273
+ gap: "10px",
274
+ }}
275
+ >
276
+ <span
277
+ style={{
278
+ fontSize: "16px",
279
+ color: colors.text,
280
+ fontWeight: 600,
281
+ marginTop: "2px",
282
+ }}
283
+ >
284
+ {icon}
285
+ </span>
286
+ <div style={{ flex: 1 }}>
287
+ <div
288
+ style={{
289
+ display: "flex",
290
+ alignItems: "center",
291
+ gap: "8px",
292
+ marginBottom: "6px",
293
+ }}
294
+ >
295
+ <span
296
+ style={{
297
+ fontSize: "12px",
298
+ fontWeight: 600,
299
+ padding: "2px 6px",
300
+ background: colors.bg,
301
+ color: colors.text,
302
+ borderRadius: "4px",
303
+ }}
304
+ >
305
+ {issue.category}
306
+ </span>
307
+ {issue.location && (
308
+ <span style={{ fontSize: "12px", color: "#64748b" }}>
309
+ {issue.location.node && `Node: ${issue.location.node}`}
310
+ {issue.location.pass !== undefined && ` • Pass ${issue.location.pass}`}
311
+ {issue.location.line !== undefined && ` • Line ${issue.location.line}`}
312
+ </span>
313
+ )}
314
+ </div>
315
+ <div
316
+ style={{
317
+ color: "#f1f5f9",
318
+ fontSize: "14px",
319
+ fontWeight: 500,
320
+ marginBottom: isExpanded ? "8px" : 0,
321
+ }}
322
+ >
323
+ {issue.message}
324
+ </div>
325
+
326
+ {/* Expanded details */}
327
+ {isExpanded && (
328
+ <div style={{ marginTop: "12px" }}>
329
+ {issue.details && (
330
+ <div
331
+ style={{
332
+ padding: "10px",
333
+ background: "#0f172a",
334
+ borderRadius: "4px",
335
+ marginBottom: "10px",
336
+ }}
337
+ >
338
+ <div
339
+ style={{
340
+ fontSize: "12px",
341
+ fontWeight: 600,
342
+ color: "#94a3b8",
343
+ marginBottom: "6px",
344
+ }}
345
+ >
346
+ Details:
347
+ </div>
348
+ <div
349
+ style={{
350
+ fontSize: "13px",
351
+ color: "#cbd5e1",
352
+ fontFamily: "monospace",
353
+ whiteSpace: "pre-wrap",
354
+ }}
355
+ >
356
+ {issue.details}
357
+ </div>
358
+ </div>
359
+ )}
360
+ {issue.suggestion && (
361
+ <div
362
+ style={{
363
+ padding: "10px",
364
+ background: "#064e3b",
365
+ border: "1px solid #10b981",
366
+ borderRadius: "4px",
367
+ }}
368
+ >
369
+ <div
370
+ style={{
371
+ fontSize: "12px",
372
+ fontWeight: 600,
373
+ color: "#6ee7b7",
374
+ marginBottom: "6px",
375
+ }}
376
+ >
377
+ 💡 Suggestion:
378
+ </div>
379
+ <div
380
+ style={{
381
+ fontSize: "13px",
382
+ color: "#d1fae5",
383
+ }}
384
+ >
385
+ {issue.suggestion}
386
+ </div>
387
+ </div>
388
+ )}
389
+ </div>
390
+ )}
391
+ </div>
392
+ <span
393
+ style={{
394
+ fontSize: "12px",
395
+ color: "#64748b",
396
+ transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)",
397
+ transition: "transform 0.2s",
398
+ }}
399
+ >
400
+ â–¼
401
+ </span>
402
+ </div>
403
+ </div>
404
+ );
405
+ })}
406
+ </div>
407
+ )}
408
+ </div>
409
+
410
+ {/* Summary footer */}
411
+ {filteredIssues.length > 0 && (
412
+ <div
413
+ style={{
414
+ marginTop: "16px",
415
+ paddingTop: "16px",
416
+ borderTop: "1px solid #334155",
417
+ fontSize: "13px",
418
+ color: "#94a3b8",
419
+ textAlign: "center",
420
+ }}
421
+ >
422
+ Showing {filteredIssues.length} of {issues.length} issue
423
+ {issues.length !== 1 ? "s" : ""}
424
+ </div>
425
+ )}
426
+ </div>
427
+ );
428
+ }
@@ -0,0 +1,2 @@
1
+ export { ValidationPanel } from "./ValidationPanel";
2
+ export type { ValidationPanelProps, ValidationIssue, ValidationSeverity } from "./ValidationPanel";
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @clypra/ui
3
+ *
4
+ * Shared UI components for all Clypra Studio Labs.
5
+ * These components provide common developer tools: graph inspection,
6
+ * performance monitoring, resource visualization, and parameter editing.
7
+ */
8
+
9
+ // Week 3 Components - Developer Panels
10
+ export { GraphInspector } from "./components/GraphInspector";
11
+ export type { GraphInspectorProps } from "./components/GraphInspector";
12
+
13
+ export { PassInspector } from "./components/PassInspector";
14
+ export type { PassInspectorProps } from "./components/PassInspector";
15
+
16
+ export { ResourceInspector } from "./components/ResourceInspector";
17
+ export type { ResourceInspectorProps } from "./components/ResourceInspector";
18
+
19
+ export { PerformanceMonitor } from "./components/PerformanceMonitor";
20
+ export type { PerformanceMonitorProps, PerformanceMetrics } from "./components/PerformanceMonitor";
21
+
22
+ // Week 4 Components - Preview & Timeline
23
+ export { PreviewCanvas } from "./components/PreviewCanvas";
24
+ export type { PreviewCanvasProps } from "./components/PreviewCanvas";
25
+
26
+ export { Timeline } from "./components/Timeline";
27
+ export type { TimelineProps } from "./components/Timeline";
28
+
29
+ export { PresetManager } from "./components/PresetManager";
30
+ export type { PresetManagerProps, Preset } from "./components/PresetManager";
31
+
32
+ export { ValidationPanel } from "./components/ValidationPanel";
33
+ export type { ValidationPanelProps, ValidationIssue, ValidationSeverity } from "./components/ValidationPanel";
34
+
35
+ export const UI_VERSION = "1.0.0";
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "lib": ["ES2022", "DOM"],
6
+ "moduleResolution": "bundler",
7
+ "resolveJsonModule": true,
8
+ "allowJs": true,
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "outDir": "./dist",
17
+ "rootDir": "./src",
18
+ "jsx": "react-jsx"
19
+ },
20
+ "include": ["src/**/*"],
21
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"]
22
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: {
5
+ index: "src/index.ts",
6
+ },
7
+ format: ["esm"],
8
+ dts: true,
9
+ sourcemap: true,
10
+ clean: true,
11
+ external: ["react", "react-dom"],
12
+ splitting: false,
13
+ treeshake: true,
14
+ });