@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,243 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import type { TemplateId } from '../lib/types';
5
+ import { useTemplateRuntime } from '../lib/runtime-context';
6
+ import { generateSpecFromTemplate } from '../utils/generateSpecFromTemplate';
7
+
8
+ /**
9
+ * Storage key prefix for spec content persistence
10
+ */
11
+ const SPEC_STORAGE_KEY = 'contractspec-spec-content';
12
+
13
+ /**
14
+ * Validation result for spec content
15
+ */
16
+ export interface SpecValidationResult {
17
+ valid: boolean;
18
+ errors: {
19
+ line: number;
20
+ message: string;
21
+ severity: 'error' | 'warning';
22
+ }[];
23
+ }
24
+
25
+ /**
26
+ * Hook return type
27
+ */
28
+ export interface UseSpecContentReturn {
29
+ /** Current spec content */
30
+ content: string;
31
+ /** Whether the spec is loading */
32
+ loading: boolean;
33
+ /** Whether the spec has unsaved changes */
34
+ isDirty: boolean;
35
+ /** Last validation result */
36
+ validation: SpecValidationResult | null;
37
+ /** Update spec content */
38
+ setContent: (content: string) => void;
39
+ /** Save spec content to storage */
40
+ save: () => void;
41
+ /** Validate spec content */
42
+ validate: () => SpecValidationResult;
43
+ /** Reset to template default */
44
+ reset: () => void;
45
+ /** Last saved timestamp */
46
+ lastSaved: string | null;
47
+ }
48
+
49
+ /**
50
+ * Hook for managing spec content with persistence for a template.
51
+ * Uses localStorage for persistence in the sandbox environment.
52
+ */
53
+ export function useSpecContent(templateId: TemplateId): UseSpecContentReturn {
54
+ const { template } = useTemplateRuntime();
55
+ const [content, setContentState] = useState<string>('');
56
+ const [savedContent, setSavedContent] = useState<string>('');
57
+ const [loading, setLoading] = useState(true);
58
+ const [validation, setValidation] = useState<SpecValidationResult | null>(
59
+ null
60
+ );
61
+ const [lastSaved, setLastSaved] = useState<string | null>(null);
62
+
63
+ // Load spec content from storage on mount
64
+ useEffect(() => {
65
+ setLoading(true);
66
+ try {
67
+ const stored = localStorage.getItem(`${SPEC_STORAGE_KEY}-${templateId}`);
68
+ if (stored) {
69
+ const parsed = JSON.parse(stored) as {
70
+ content: string;
71
+ savedAt: string;
72
+ };
73
+ if (parsed.content) {
74
+ setContentState(parsed.content);
75
+ setSavedContent(parsed.content);
76
+ setLastSaved(parsed.savedAt);
77
+ } else {
78
+ // Invalid stored state, generate from template
79
+ const generated = generateSpecFromTemplate(template);
80
+ setContentState(generated);
81
+ setSavedContent(generated);
82
+ }
83
+ } else {
84
+ // No stored state, generate from template
85
+ const generated = generateSpecFromTemplate(template);
86
+ setContentState(generated);
87
+ setSavedContent(generated);
88
+ }
89
+ } catch {
90
+ // On error, generate from template
91
+ const generated = generateSpecFromTemplate(template);
92
+ setContentState(generated);
93
+ setSavedContent(generated);
94
+ }
95
+ setLoading(false);
96
+ }, [templateId]);
97
+
98
+ /**
99
+ * Update spec content (in-memory only until save)
100
+ */
101
+ const setContent = useCallback((newContent: string): void => {
102
+ setContentState(newContent);
103
+ // Clear validation when content changes
104
+ setValidation(null);
105
+ }, []);
106
+
107
+ /**
108
+ * Save spec content to storage
109
+ */
110
+ const save = useCallback((): void => {
111
+ try {
112
+ const savedAt = new Date().toISOString();
113
+ localStorage.setItem(
114
+ `${SPEC_STORAGE_KEY}-${templateId}`,
115
+ JSON.stringify({
116
+ content,
117
+ savedAt,
118
+ })
119
+ );
120
+ setSavedContent(content);
121
+ setLastSaved(savedAt);
122
+ } catch {
123
+ // Ignore storage errors
124
+ }
125
+ }, [content, templateId]);
126
+
127
+ /**
128
+ * Validate spec content
129
+ * Performs basic syntax validation
130
+ */
131
+ const validate = useCallback((): SpecValidationResult => {
132
+ const errors: SpecValidationResult['errors'] = [];
133
+
134
+ // Basic validation rules
135
+ const lines = content.split('\n');
136
+
137
+ // Check for contractSpec function call
138
+ if (!content.includes('contractSpec(')) {
139
+ errors.push({
140
+ line: 1,
141
+ message: 'Spec must contain a contractSpec() definition',
142
+ severity: 'error',
143
+ });
144
+ }
145
+
146
+ // Check for required fields
147
+ if (!content.includes('goal:')) {
148
+ errors.push({
149
+ line: 1,
150
+ message: 'Spec should have a goal field',
151
+ severity: 'warning',
152
+ });
153
+ }
154
+
155
+ if (!content.includes('io:')) {
156
+ errors.push({
157
+ line: 1,
158
+ message: 'Spec should define io (input/output)',
159
+ severity: 'warning',
160
+ });
161
+ }
162
+
163
+ // Check for balanced braces
164
+ const openBraces = (content.match(/{/g) ?? []).length;
165
+ const closeBraces = (content.match(/}/g) ?? []).length;
166
+ if (openBraces !== closeBraces) {
167
+ errors.push({
168
+ line: lines.length,
169
+ message: `Unbalanced braces: ${openBraces} opening, ${closeBraces} closing`,
170
+ severity: 'error',
171
+ });
172
+ }
173
+
174
+ // Check for balanced parentheses
175
+ const openParens = (content.match(/\(/g) ?? []).length;
176
+ const closeParens = (content.match(/\)/g) ?? []).length;
177
+ if (openParens !== closeParens) {
178
+ errors.push({
179
+ line: lines.length,
180
+ message: `Unbalanced parentheses: ${openParens} opening, ${closeParens} closing`,
181
+ severity: 'error',
182
+ });
183
+ }
184
+
185
+ // Check for unclosed strings (basic check)
186
+ lines.forEach((line, index) => {
187
+ const singleQuotes = (line.match(/'/g) ?? []).length;
188
+ const doubleQuotes = (line.match(/"/g) ?? []).length;
189
+ if (singleQuotes % 2 !== 0) {
190
+ errors.push({
191
+ line: index + 1,
192
+ message: 'Unclosed single quote',
193
+ severity: 'error',
194
+ });
195
+ }
196
+ if (doubleQuotes % 2 !== 0) {
197
+ errors.push({
198
+ line: index + 1,
199
+ message: 'Unclosed double quote',
200
+ severity: 'error',
201
+ });
202
+ }
203
+ });
204
+
205
+ const result: SpecValidationResult = {
206
+ valid: errors.filter((e) => e.severity === 'error').length === 0,
207
+ errors,
208
+ };
209
+
210
+ setValidation(result);
211
+ return result;
212
+ }, [content]);
213
+
214
+ /**
215
+ * Reset to template default
216
+ */
217
+ const reset = useCallback((): void => {
218
+ const generated = generateSpecFromTemplate(template);
219
+ setContentState(generated);
220
+ setSavedContent(generated);
221
+ setValidation(null);
222
+ setLastSaved(null);
223
+
224
+ // Clear from storage
225
+ try {
226
+ localStorage.removeItem(`${SPEC_STORAGE_KEY}-${templateId}`);
227
+ } catch {
228
+ // Ignore storage errors
229
+ }
230
+ }, [templateId]);
231
+
232
+ return {
233
+ content,
234
+ loading,
235
+ isDirty: content !== savedContent,
236
+ validation,
237
+ setContent,
238
+ save,
239
+ validate,
240
+ reset,
241
+ lastSaved,
242
+ };
243
+ }