@contractspec/lib.example-shared-ui 0.0.0-canary-20260113170453

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,165 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect } from 'react';
4
+ import { Button, LoaderBlock } from '@contractspec/lib.design-system';
5
+ import { Badge } from '@contractspec/lib.ui-kit-web/ui/badge';
6
+ import type { TemplateId } from './lib/types';
7
+ import { useSpecContent } from './hooks/useSpecContent';
8
+
9
+ export interface SpecEditorProps {
10
+ projectId: string;
11
+ type?: 'CAPABILITY' | 'DATAVIEW' | 'WORKFLOW' | 'POLICY' | 'COMPONENT';
12
+ content: string;
13
+ onChange: (content: string) => void;
14
+ metadata?: Record<string, unknown>;
15
+ onSave?: () => void;
16
+ onValidate?: () => void;
17
+ }
18
+
19
+ export interface SpecEditorPanelProps {
20
+ templateId: TemplateId;
21
+ /** SpecEditor component passed as a prop (for dynamic import compatibility) */
22
+ SpecEditor: React.ComponentType<SpecEditorProps>;
23
+ /** Callback for logging actions */
24
+ onLog?: (message: string) => void;
25
+ }
26
+
27
+ /**
28
+ * Spec editor panel that wraps SpecEditor with persisted spec content.
29
+ * Uses useSpecContent hook to manage spec persistence and validation.
30
+ */
31
+ export function SpecEditorPanel({
32
+ templateId,
33
+ SpecEditor,
34
+ onLog,
35
+ }: SpecEditorPanelProps) {
36
+ const {
37
+ content,
38
+ loading,
39
+ isDirty,
40
+ validation,
41
+ setContent,
42
+ save,
43
+ validate,
44
+ reset,
45
+ lastSaved,
46
+ } = useSpecContent(templateId);
47
+
48
+ // Log when spec is loaded
49
+ useEffect(() => {
50
+ if (!loading && content) {
51
+ onLog?.(`Spec loaded for ${templateId}`);
52
+ }
53
+ }, [loading, content, templateId, onLog]);
54
+
55
+ const handleSave = useCallback(() => {
56
+ save();
57
+ onLog?.('Spec saved locally');
58
+ }, [save, onLog]);
59
+
60
+ const handleValidate = useCallback(() => {
61
+ const result = validate();
62
+ if (result.valid) {
63
+ onLog?.('Spec validation passed');
64
+ } else {
65
+ const errorCount = result.errors.filter(
66
+ (e) => e.severity === 'error'
67
+ ).length;
68
+ const warnCount = result.errors.filter(
69
+ (e) => e.severity === 'warning'
70
+ ).length;
71
+ onLog?.(`Spec validation: ${errorCount} errors, ${warnCount} warnings`);
72
+ }
73
+ }, [validate, onLog]);
74
+
75
+ const handleReset = useCallback(() => {
76
+ reset();
77
+ onLog?.('Spec reset to template defaults');
78
+ }, [reset, onLog]);
79
+
80
+ if (loading) {
81
+ return <LoaderBlock label="Loading spec..." />;
82
+ }
83
+
84
+ return (
85
+ <div className="space-y-4">
86
+ {/* Spec Toolbar */}
87
+ <div className="flex items-center justify-between">
88
+ <div className="flex items-center gap-2">
89
+ <Button variant="default" size="sm" onClick={handleSave}>
90
+ Save
91
+ </Button>
92
+ <Button variant="outline" size="sm" onClick={handleValidate}>
93
+ Validate
94
+ </Button>
95
+ {isDirty && (
96
+ <Badge
97
+ variant="secondary"
98
+ className="border-amber-500/30 bg-amber-500/20 text-amber-400"
99
+ >
100
+ Unsaved changes
101
+ </Badge>
102
+ )}
103
+ {validation && (
104
+ <Badge
105
+ variant={validation.valid ? 'default' : 'destructive'}
106
+ className={
107
+ validation.valid
108
+ ? 'border-green-500/30 bg-green-500/20 text-green-400'
109
+ : ''
110
+ }
111
+ >
112
+ {validation.valid
113
+ ? 'Valid'
114
+ : `${validation.errors.filter((e) => e.severity === 'error').length} errors`}
115
+ </Badge>
116
+ )}
117
+ </div>
118
+ <div className="flex items-center gap-2">
119
+ {lastSaved && (
120
+ <span className="text-muted-foreground text-xs">
121
+ Last saved: {new Date(lastSaved).toLocaleTimeString()}
122
+ </span>
123
+ )}
124
+ <Button variant="ghost" size="sm" onPress={handleReset}>
125
+ Reset
126
+ </Button>
127
+ </div>
128
+ </div>
129
+
130
+ {/* Validation Errors */}
131
+ {validation && validation.errors.length > 0 && (
132
+ <div className="rounded-lg border border-amber-500/50 bg-amber-500/10 p-3">
133
+ <p className="mb-2 text-xs font-semibold text-amber-400 uppercase">
134
+ Validation Issues
135
+ </p>
136
+ <ul className="space-y-1">
137
+ {validation.errors.map((error, index) => (
138
+ <li
139
+ key={`${error.line}-${error.message}-${index}`}
140
+ className={`text-xs ${
141
+ error.severity === 'error' ? 'text-red-400' : 'text-amber-400'
142
+ }`}
143
+ >
144
+ Line {error.line}: {error.message}
145
+ </li>
146
+ ))}
147
+ </ul>
148
+ </div>
149
+ )}
150
+
151
+ {/* Editor */}
152
+ <div className="border-border bg-card rounded-2xl border p-4">
153
+ <SpecEditor
154
+ projectId="sandbox"
155
+ type="CAPABILITY"
156
+ content={content}
157
+ onChange={setContent}
158
+ metadata={{ template: templateId }}
159
+ onSave={handleSave}
160
+ onValidate={handleValidate}
161
+ />
162
+ </div>
163
+ </div>
164
+ );
165
+ }
@@ -0,0 +1,63 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import { LocalDataIndicator } from './LocalDataIndicator';
4
+ import {
5
+ SaveToStudioButton,
6
+ type SaveToStudioButtonProps,
7
+ } from './SaveToStudioButton';
8
+
9
+ export interface TemplateShellProps {
10
+ title: string;
11
+ description?: string;
12
+ sidebar?: ReactNode;
13
+ actions?: ReactNode;
14
+ children: ReactNode;
15
+ showSaveAction?: boolean;
16
+ saveProps?: SaveToStudioButtonProps;
17
+ }
18
+
19
+ export const TemplateShell = ({
20
+ title,
21
+ description,
22
+ sidebar,
23
+ actions,
24
+ showSaveAction = true,
25
+ saveProps,
26
+ children,
27
+ }: TemplateShellProps) => (
28
+ <div className="space-y-6">
29
+ <header className="border-border bg-card rounded-2xl border p-6 shadow-sm">
30
+ <div className="flex flex-wrap items-center justify-between gap-4">
31
+ <div>
32
+ <p className="text-muted-foreground text-sm font-semibold tracking-wide uppercase">
33
+ ContractSpec Templates
34
+ </p>
35
+ <h1 className="text-3xl font-bold">{title}</h1>
36
+ {description ? (
37
+ <p className="text-muted-foreground mt-2 max-w-2xl text-sm">
38
+ {description}
39
+ </p>
40
+ ) : null}
41
+ </div>
42
+ <div className="flex flex-col items-end gap-2">
43
+ <LocalDataIndicator />
44
+ {showSaveAction ? <SaveToStudioButton {...saveProps} /> : null}
45
+ </div>
46
+ </div>
47
+ {actions ? <div className="mt-4">{actions}</div> : null}
48
+ </header>
49
+
50
+ <div
51
+ className={
52
+ sidebar ? 'grid gap-6 lg:grid-cols-[minmax(0,1fr)_320px]' : 'w-full'
53
+ }
54
+ >
55
+ <main className="space-y-4 p-2">{children}</main>
56
+ {sidebar ? (
57
+ <aside className="border-border bg-card rounded-2xl border p-4">
58
+ {sidebar}
59
+ </aside>
60
+ ) : null}
61
+ </div>
62
+ </div>
63
+ );
@@ -0,0 +1,5 @@
1
+ export * from './useSpecContent';
2
+ export * from './useEvolution';
3
+ export * from './useBehaviorTracking';
4
+ export * from './useWorkflowComposer';
5
+ export * from './useRegistryTemplates';
@@ -0,0 +1,327 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import type { TemplateId } from '../lib/types';
5
+
6
+ /**
7
+ * Behavior event types
8
+ */
9
+ export type BehaviorEventType =
10
+ | 'template_view'
11
+ | 'mode_change'
12
+ | 'spec_edit'
13
+ | 'canvas_interaction'
14
+ | 'presentation_view'
15
+ | 'feature_usage'
16
+ | 'error'
17
+ | 'navigation';
18
+
19
+ /**
20
+ * Behavior event data
21
+ */
22
+ export interface BehaviorEvent {
23
+ type: BehaviorEventType;
24
+ timestamp: Date;
25
+ templateId: TemplateId;
26
+ metadata?: Record<string, unknown>;
27
+ }
28
+
29
+ /**
30
+ * Behavior summary for a session
31
+ */
32
+ export interface BehaviorSummary {
33
+ totalEvents: number;
34
+ sessionDuration: number;
35
+ mostUsedTemplates: { templateId: TemplateId; count: number }[];
36
+ mostUsedModes: { mode: string; count: number }[];
37
+ featuresUsed: string[];
38
+ unusedFeatures: string[];
39
+ errorCount: number;
40
+ recommendations: string[];
41
+ }
42
+
43
+ /**
44
+ * Hook return type
45
+ */
46
+ export interface UseBehaviorTrackingReturn {
47
+ /** Track a behavior event */
48
+ trackEvent: (
49
+ type: BehaviorEventType,
50
+ metadata?: Record<string, unknown>
51
+ ) => void;
52
+ /** Get behavior summary */
53
+ getSummary: () => BehaviorSummary;
54
+ /** Get events for a specific type */
55
+ getEventsByType: (type: BehaviorEventType) => BehaviorEvent[];
56
+ /** Total event count */
57
+ eventCount: number;
58
+ /** Session start time */
59
+ sessionStart: Date;
60
+ /** Clear all tracked data */
61
+ clear: () => void;
62
+ }
63
+
64
+ /**
65
+ * Storage key for behavior data
66
+ */
67
+ const BEHAVIOR_STORAGE_KEY = 'contractspec-behavior-data';
68
+
69
+ /**
70
+ * All available features in the sandbox
71
+ */
72
+ const ALL_FEATURES = [
73
+ 'playground',
74
+ 'specs',
75
+ 'builder',
76
+ 'markdown',
77
+ 'evolution',
78
+ 'canvas_add',
79
+ 'canvas_delete',
80
+ 'spec_save',
81
+ 'spec_validate',
82
+ 'ai_suggestions',
83
+ ];
84
+
85
+ /**
86
+ * Hook for tracking user behavior in the sandbox.
87
+ * Provides insights into usage patterns and feature adoption.
88
+ */
89
+ export function useBehaviorTracking(
90
+ templateId: TemplateId
91
+ ): UseBehaviorTrackingReturn {
92
+ const [events, setEvents] = useState<BehaviorEvent[]>([]);
93
+ const sessionStartRef = useRef<Date>(new Date());
94
+ const [eventCount, setEventCount] = useState(0);
95
+
96
+ // Load persisted events on mount
97
+ useEffect(() => {
98
+ try {
99
+ const stored = localStorage.getItem(BEHAVIOR_STORAGE_KEY);
100
+ if (stored) {
101
+ const data = JSON.parse(stored) as {
102
+ events: (Omit<BehaviorEvent, 'timestamp'> & { timestamp: string })[];
103
+ sessionStart: string;
104
+ };
105
+ setEvents(
106
+ data.events.map((e) => ({
107
+ ...e,
108
+ timestamp: new Date(e.timestamp),
109
+ }))
110
+ );
111
+ sessionStartRef.current = new Date(data.sessionStart);
112
+ }
113
+ } catch {
114
+ // Ignore storage errors
115
+ }
116
+ }, []);
117
+
118
+ // Persist events when they change
119
+ useEffect(() => {
120
+ if (events.length > 0) {
121
+ try {
122
+ localStorage.setItem(
123
+ BEHAVIOR_STORAGE_KEY,
124
+ JSON.stringify({
125
+ events: events.map((e) => ({
126
+ ...e,
127
+ timestamp: e.timestamp.toISOString(),
128
+ })),
129
+ sessionStart: sessionStartRef.current.toISOString(),
130
+ })
131
+ );
132
+ } catch {
133
+ // Ignore storage errors
134
+ }
135
+ }
136
+ }, [events]);
137
+
138
+ /**
139
+ * Track a behavior event
140
+ */
141
+ const trackEvent = useCallback(
142
+ (type: BehaviorEventType, metadata?: Record<string, unknown>) => {
143
+ const event: BehaviorEvent = {
144
+ type,
145
+ timestamp: new Date(),
146
+ templateId,
147
+ metadata,
148
+ };
149
+ setEvents((prev) => [...prev, event]);
150
+ setEventCount((prev) => prev + 1);
151
+ },
152
+ [templateId]
153
+ );
154
+
155
+ /**
156
+ * Get events by type
157
+ */
158
+ const getEventsByType = useCallback(
159
+ (type: BehaviorEventType): BehaviorEvent[] => {
160
+ return events.filter((e) => e.type === type);
161
+ },
162
+ [events]
163
+ );
164
+
165
+ /**
166
+ * Get behavior summary
167
+ */
168
+ const getSummary = useCallback((): BehaviorSummary => {
169
+ const now = new Date();
170
+ const sessionDuration = now.getTime() - sessionStartRef.current.getTime();
171
+
172
+ // Count templates
173
+ const templateCounts = new Map<TemplateId, number>();
174
+ for (const event of events) {
175
+ const count = templateCounts.get(event.templateId) ?? 0;
176
+ templateCounts.set(event.templateId, count + 1);
177
+ }
178
+ const mostUsedTemplates = Array.from(templateCounts.entries())
179
+ .map(([templateId, count]) => ({ templateId, count }))
180
+ .sort((a, b) => b.count - a.count)
181
+ .slice(0, 3);
182
+
183
+ // Count modes from mode_change events
184
+ const modeCounts = new Map<string, number>();
185
+ for (const event of events) {
186
+ if (event.type === 'mode_change' && event.metadata?.mode) {
187
+ const mode = event.metadata.mode as string;
188
+ const count = modeCounts.get(mode) ?? 0;
189
+ modeCounts.set(mode, count + 1);
190
+ }
191
+ }
192
+ const mostUsedModes = Array.from(modeCounts.entries())
193
+ .map(([mode, count]) => ({ mode, count }))
194
+ .sort((a, b) => b.count - a.count);
195
+
196
+ // Track features used
197
+ const featuresUsed = new Set<string>();
198
+ for (const event of events) {
199
+ if (event.type === 'mode_change' && event.metadata?.mode) {
200
+ featuresUsed.add(event.metadata.mode as string);
201
+ }
202
+ if (event.type === 'feature_usage' && event.metadata?.feature) {
203
+ featuresUsed.add(event.metadata.feature as string);
204
+ }
205
+ if (event.type === 'canvas_interaction') {
206
+ const action = event.metadata?.action as string;
207
+ if (action === 'add') featuresUsed.add('canvas_add');
208
+ if (action === 'delete') featuresUsed.add('canvas_delete');
209
+ }
210
+ if (event.type === 'spec_edit') {
211
+ const action = event.metadata?.action as string;
212
+ if (action === 'save') featuresUsed.add('spec_save');
213
+ if (action === 'validate') featuresUsed.add('spec_validate');
214
+ }
215
+ }
216
+
217
+ // Find unused features
218
+ const unusedFeatures = ALL_FEATURES.filter((f) => !featuresUsed.has(f));
219
+
220
+ // Count errors
221
+ const errorCount = events.filter((e) => e.type === 'error').length;
222
+
223
+ // Generate recommendations
224
+ const recommendations = generateRecommendations(
225
+ Array.from(featuresUsed),
226
+ unusedFeatures,
227
+ mostUsedModes,
228
+ events.length
229
+ );
230
+
231
+ return {
232
+ totalEvents: events.length,
233
+ sessionDuration,
234
+ mostUsedTemplates,
235
+ mostUsedModes,
236
+ featuresUsed: Array.from(featuresUsed),
237
+ unusedFeatures,
238
+ errorCount,
239
+ recommendations,
240
+ };
241
+ }, [events]);
242
+
243
+ /**
244
+ * Clear all tracking data
245
+ */
246
+ const clear = useCallback(() => {
247
+ setEvents([]);
248
+ setEventCount(0);
249
+ sessionStartRef.current = new Date();
250
+ localStorage.removeItem(BEHAVIOR_STORAGE_KEY);
251
+ }, []);
252
+
253
+ return useMemo(
254
+ () => ({
255
+ trackEvent,
256
+ getSummary,
257
+ getEventsByType,
258
+ eventCount,
259
+ sessionStart: sessionStartRef.current,
260
+ clear,
261
+ }),
262
+ [trackEvent, getSummary, getEventsByType, eventCount, clear]
263
+ );
264
+ }
265
+
266
+ /**
267
+ * Generate recommendations based on behavior
268
+ */
269
+ function generateRecommendations(
270
+ featuresUsed: string[],
271
+ unusedFeatures: string[],
272
+ mostUsedModes: { mode: string; count: number }[],
273
+ totalEvents: number
274
+ ): string[] {
275
+ const recommendations: string[] = [];
276
+
277
+ // Recommend unused features
278
+ if (unusedFeatures.includes('evolution')) {
279
+ recommendations.push(
280
+ 'Try the AI Evolution mode to get automated improvement suggestions'
281
+ );
282
+ }
283
+
284
+ if (unusedFeatures.includes('markdown')) {
285
+ recommendations.push(
286
+ 'Use Markdown preview to see documentation for your specs'
287
+ );
288
+ }
289
+
290
+ if (unusedFeatures.includes('builder')) {
291
+ recommendations.push(
292
+ 'Explore the Visual Builder to design your UI components'
293
+ );
294
+ }
295
+
296
+ if (
297
+ !featuresUsed.includes('spec_validate') &&
298
+ featuresUsed.includes('specs')
299
+ ) {
300
+ recommendations.push("Don't forget to validate your specs before saving");
301
+ }
302
+
303
+ if (
304
+ featuresUsed.includes('evolution') &&
305
+ !featuresUsed.includes('ai_suggestions')
306
+ ) {
307
+ recommendations.push(
308
+ 'Generate AI suggestions to get actionable improvement recommendations'
309
+ );
310
+ }
311
+
312
+ // Time-based recommendations
313
+ if (totalEvents > 50) {
314
+ recommendations.push(
315
+ 'Great engagement! Consider saving your work regularly'
316
+ );
317
+ }
318
+
319
+ // Mode variety recommendations
320
+ if (mostUsedModes.length === 1) {
321
+ recommendations.push(
322
+ 'Try different modes to explore all sandbox capabilities'
323
+ );
324
+ }
325
+
326
+ return recommendations;
327
+ }