@contractspec/lib.example-shared-ui 6.0.6 → 6.0.10
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/.turbo/turbo-build.log +90 -84
- package/AGENTS.md +43 -25
- package/CHANGELOG.md +24 -0
- package/README.md +63 -35
- package/dist/EvolutionDashboard.js +9 -9
- package/dist/EvolutionSidebar.js +15 -15
- package/dist/LocalDataIndicator.js +3 -3
- package/dist/MarkdownView.d.ts +0 -7
- package/dist/MarkdownView.js +76 -172
- package/dist/PersonalizationInsights.js +12 -12
- package/dist/SaveToStudioButton.js +2 -2
- package/dist/SpecDrivenTemplateShell.d.ts +1 -1
- package/dist/SpecDrivenTemplateShell.js +10 -10
- package/dist/SpecEditorPanel.js +3 -3
- package/dist/TemplateShell.js +10 -10
- package/dist/browser/EvolutionDashboard.js +9 -9
- package/dist/browser/EvolutionSidebar.js +15 -15
- package/dist/browser/LocalDataIndicator.js +3 -3
- package/dist/browser/MarkdownView.js +76 -172
- package/dist/browser/PersonalizationInsights.js +12 -12
- package/dist/browser/SaveToStudioButton.js +2 -2
- package/dist/browser/SpecDrivenTemplateShell.js +10 -10
- package/dist/browser/SpecEditorPanel.js +3 -3
- package/dist/browser/TemplateShell.js +10 -10
- package/dist/browser/hooks/index.js +29 -29
- package/dist/browser/index.js +193 -286
- package/dist/browser/lib/component-registry.js +1 -1
- package/dist/browser/markdown/formatPresentationName.js +9 -0
- package/dist/browser/markdown/useMarkdownPresentation.js +65 -0
- package/dist/hooks/index.d.ts +3 -3
- package/dist/hooks/index.js +29 -29
- package/dist/index.d.ts +12 -11
- package/dist/index.js +193 -286
- package/dist/lib/component-registry.js +1 -1
- package/dist/markdown/formatPresentationName.d.ts +1 -0
- package/dist/markdown/formatPresentationName.js +10 -0
- package/dist/markdown/useMarkdownPresentation.d.ts +21 -0
- package/dist/markdown/useMarkdownPresentation.js +66 -0
- package/dist/node/EvolutionDashboard.js +9 -9
- package/dist/node/EvolutionSidebar.js +15 -15
- package/dist/node/LocalDataIndicator.js +3 -3
- package/dist/node/MarkdownView.js +76 -172
- package/dist/node/PersonalizationInsights.js +12 -12
- package/dist/node/SaveToStudioButton.js +2 -2
- package/dist/node/SpecDrivenTemplateShell.js +10 -10
- package/dist/node/SpecEditorPanel.js +3 -3
- package/dist/node/TemplateShell.js +10 -10
- package/dist/node/hooks/index.js +29 -29
- package/dist/node/index.js +193 -286
- package/dist/node/lib/component-registry.js +1 -1
- package/dist/node/markdown/formatPresentationName.js +9 -0
- package/dist/node/markdown/useMarkdownPresentation.js +65 -0
- package/dist/utils/index.d.ts +1 -1
- package/package.json +40 -13
- package/src/EvolutionDashboard.tsx +415 -415
- package/src/EvolutionSidebar.tsx +245 -245
- package/src/LocalDataIndicator.tsx +28 -28
- package/src/MarkdownView.tsx +119 -372
- package/src/OverlayContextProvider.tsx +272 -272
- package/src/PersonalizationInsights.tsx +232 -232
- package/src/SaveToStudioButton.tsx +51 -51
- package/src/SpecDrivenTemplateShell.tsx +59 -59
- package/src/SpecEditorPanel.tsx +138 -138
- package/src/TemplateShell.tsx +50 -50
- package/src/bundles/ExampleTemplateBundle.ts +78 -78
- package/src/hooks/index.ts +3 -3
- package/src/hooks/useBehaviorTracking.ts +252 -252
- package/src/hooks/useEvolution.ts +437 -437
- package/src/hooks/useRegistryTemplates.ts +42 -42
- package/src/hooks/useSpecContent.ts +214 -214
- package/src/hooks/useWorkflowComposer.ts +567 -567
- package/src/index.ts +12 -11
- package/src/lib/component-registry.tsx +40 -40
- package/src/lib/runtime-context.tsx +31 -31
- package/src/lib/types.ts +57 -57
- package/src/markdown/formatPresentationName.ts +9 -0
- package/src/markdown/useMarkdownPresentation.ts +107 -0
- package/src/overlay-types.ts +15 -15
- package/src/utils/fetchPresentationData.ts +13 -13
- package/src/utils/generateSpecFromTemplate.ts +29 -29
- package/src/utils/index.ts +1 -1
- package/tsconfig.json +8 -8
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useCallback, useMemo, useState } from 'react';
|
|
4
3
|
import { Button } from '@contractspec/lib.design-system';
|
|
5
|
-
import { Card } from '@contractspec/lib.ui-kit-web/ui/card';
|
|
6
4
|
import { Badge } from '@contractspec/lib.ui-kit-web/ui/badge';
|
|
7
|
-
import
|
|
5
|
+
import { Card } from '@contractspec/lib.ui-kit-web/ui/card';
|
|
6
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
8
7
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
type BehaviorSummary,
|
|
9
|
+
useBehaviorTracking,
|
|
11
10
|
} from './hooks/useBehaviorTracking';
|
|
11
|
+
import type { TemplateId } from './lib/types';
|
|
12
12
|
|
|
13
13
|
export interface PersonalizationInsightsProps {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
templateId: TemplateId;
|
|
15
|
+
/** Whether to show in collapsed mode */
|
|
16
|
+
collapsed?: boolean;
|
|
17
|
+
/** Toggle collapsed state */
|
|
18
|
+
onToggle?: () => void;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -23,271 +23,271 @@ export interface PersonalizationInsightsProps {
|
|
|
23
23
|
* Displays usage patterns, recommendations, and feature adoption.
|
|
24
24
|
*/
|
|
25
25
|
export function PersonalizationInsights({
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
templateId,
|
|
27
|
+
collapsed = false,
|
|
28
|
+
onToggle,
|
|
29
29
|
}: PersonalizationInsightsProps) {
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const { getSummary, eventCount, clear, sessionStart } =
|
|
31
|
+
useBehaviorTracking(templateId);
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
const [showDetails, setShowDetails] = useState(false);
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
const summary = useMemo(() => getSummary(), [getSummary]);
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const formatDuration = useCallback((ms: number) => {
|
|
38
|
+
const seconds = Math.floor(ms / 1000);
|
|
39
|
+
const minutes = Math.floor(seconds / 60);
|
|
40
|
+
const hours = Math.floor(minutes / 60);
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
if (hours > 0) {
|
|
43
|
+
return `${hours}h ${minutes % 60}m`;
|
|
44
|
+
}
|
|
45
|
+
if (minutes > 0) {
|
|
46
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
47
|
+
}
|
|
48
|
+
return `${seconds}s`;
|
|
49
|
+
}, []);
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
const handleClear = useCallback(() => {
|
|
52
|
+
clear();
|
|
53
|
+
}, [clear]);
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
55
|
+
// Collapsed view
|
|
56
|
+
if (collapsed) {
|
|
57
|
+
return (
|
|
58
|
+
<button
|
|
59
|
+
onClick={onToggle}
|
|
60
|
+
className="flex items-center gap-2 rounded-lg border border-blue-500/30 bg-blue-500/10 px-3 py-2 text-sm transition hover:bg-blue-500/20"
|
|
61
|
+
type="button"
|
|
62
|
+
>
|
|
63
|
+
<span>📊</span>
|
|
64
|
+
<span>Insights</span>
|
|
65
|
+
<Badge variant="secondary">{eventCount}</Badge>
|
|
66
|
+
</button>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
70
|
+
return (
|
|
71
|
+
<Card className="overflow-hidden">
|
|
72
|
+
{/* Header */}
|
|
73
|
+
<div className="flex items-center justify-between border-blue-500/20 border-b bg-blue-500/5 px-4 py-3">
|
|
74
|
+
<div className="flex items-center gap-2">
|
|
75
|
+
<span>📊</span>
|
|
76
|
+
<span className="font-semibold">Personalization Insights</span>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="flex items-center gap-2">
|
|
79
|
+
<Button
|
|
80
|
+
variant="ghost"
|
|
81
|
+
size="sm"
|
|
82
|
+
onPress={() => setShowDetails(!showDetails)}
|
|
83
|
+
>
|
|
84
|
+
{showDetails ? 'Hide Details' : 'Show Details'}
|
|
85
|
+
</Button>
|
|
86
|
+
{onToggle && (
|
|
87
|
+
<button
|
|
88
|
+
onClick={onToggle}
|
|
89
|
+
className="p-1 text-muted-foreground hover:text-foreground"
|
|
90
|
+
type="button"
|
|
91
|
+
title="Collapse"
|
|
92
|
+
>
|
|
93
|
+
✕
|
|
94
|
+
</button>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
99
|
+
<div className="p-4">
|
|
100
|
+
{/* Quick Stats */}
|
|
101
|
+
<div className="mb-4 grid grid-cols-2 gap-3 md:grid-cols-4">
|
|
102
|
+
<StatCard
|
|
103
|
+
label="Session Time"
|
|
104
|
+
value={formatDuration(summary.sessionDuration)}
|
|
105
|
+
icon="⏱️"
|
|
106
|
+
/>
|
|
107
|
+
<StatCard
|
|
108
|
+
label="Events Tracked"
|
|
109
|
+
value={summary.totalEvents.toString()}
|
|
110
|
+
icon="📈"
|
|
111
|
+
/>
|
|
112
|
+
<StatCard
|
|
113
|
+
label="Features Used"
|
|
114
|
+
value={`${summary.featuresUsed.length}/${summary.featuresUsed.length + summary.unusedFeatures.length}`}
|
|
115
|
+
icon="✨"
|
|
116
|
+
/>
|
|
117
|
+
<StatCard
|
|
118
|
+
label="Errors"
|
|
119
|
+
value={summary.errorCount.toString()}
|
|
120
|
+
icon="⚠️"
|
|
121
|
+
variant={summary.errorCount > 0 ? 'warning' : 'success'}
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
125
|
+
{/* Recommendations */}
|
|
126
|
+
{summary.recommendations.length > 0 && (
|
|
127
|
+
<div className="mb-4">
|
|
128
|
+
<h4 className="mb-2 font-semibold text-blue-400 text-xs uppercase">
|
|
129
|
+
Recommendations
|
|
130
|
+
</h4>
|
|
131
|
+
<ul className="space-y-1">
|
|
132
|
+
{summary.recommendations.map((rec, index) => (
|
|
133
|
+
<li key={index} className="flex items-start gap-2 text-sm">
|
|
134
|
+
<span className="text-blue-400">💡</span>
|
|
135
|
+
<span>{rec}</span>
|
|
136
|
+
</li>
|
|
137
|
+
))}
|
|
138
|
+
</ul>
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
142
|
+
{/* Unused Features */}
|
|
143
|
+
{summary.unusedFeatures.length > 0 && (
|
|
144
|
+
<div className="mb-4">
|
|
145
|
+
<h4 className="mb-2 font-semibold text-blue-400 text-xs uppercase">
|
|
146
|
+
Try These Features
|
|
147
|
+
</h4>
|
|
148
|
+
<div className="flex flex-wrap gap-2">
|
|
149
|
+
{summary.unusedFeatures.slice(0, 5).map((feature) => (
|
|
150
|
+
<Badge key={feature} variant="secondary">
|
|
151
|
+
{formatFeatureName(feature)}
|
|
152
|
+
</Badge>
|
|
153
|
+
))}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
{/* Detailed View */}
|
|
159
|
+
{showDetails && (
|
|
160
|
+
<DetailedInsights summary={summary} sessionStart={sessionStart} />
|
|
161
|
+
)}
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
163
|
+
{/* Footer Actions */}
|
|
164
|
+
<div className="mt-4 flex justify-end border-blue-500/10 border-t pt-4">
|
|
165
|
+
<Button variant="ghost" size="sm" onPress={handleClear}>
|
|
166
|
+
Clear Data
|
|
167
|
+
</Button>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</Card>
|
|
171
|
+
);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
175
|
* Stat card component
|
|
176
176
|
*/
|
|
177
177
|
function StatCard({
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
178
|
+
label,
|
|
179
|
+
value,
|
|
180
|
+
icon,
|
|
181
|
+
variant = 'default',
|
|
182
182
|
}: {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
183
|
+
label: string;
|
|
184
|
+
value: string;
|
|
185
|
+
icon: string;
|
|
186
|
+
variant?: 'default' | 'warning' | 'success';
|
|
187
187
|
}) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
188
|
+
const bgColors = {
|
|
189
|
+
default: 'bg-blue-500/5 border-blue-500/20',
|
|
190
|
+
warning: 'bg-amber-500/5 border-amber-500/20',
|
|
191
|
+
success: 'bg-green-500/5 border-green-500/20',
|
|
192
|
+
};
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
194
|
+
return (
|
|
195
|
+
<div className={`rounded-lg border p-3 text-center ${bgColors[variant]}`}>
|
|
196
|
+
<div className="mb-1 text-lg">{icon}</div>
|
|
197
|
+
<div className="font-bold text-lg">{value}</div>
|
|
198
|
+
<div className="text-muted-foreground text-xs">{label}</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
/**
|
|
204
204
|
* Detailed insights section
|
|
205
205
|
*/
|
|
206
206
|
function DetailedInsights({
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
summary,
|
|
208
|
+
sessionStart,
|
|
209
209
|
}: {
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
summary: BehaviorSummary;
|
|
211
|
+
sessionStart: Date;
|
|
212
212
|
}) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
213
|
+
return (
|
|
214
|
+
<div className="mt-4 space-y-4 border-blue-500/10 border-t pt-4">
|
|
215
|
+
{/* Most Used Templates */}
|
|
216
|
+
{summary.mostUsedTemplates.length > 0 && (
|
|
217
|
+
<div>
|
|
218
|
+
<h4 className="mb-2 font-semibold text-blue-400 text-xs uppercase">
|
|
219
|
+
Most Used Templates
|
|
220
|
+
</h4>
|
|
221
|
+
<div className="space-y-1">
|
|
222
|
+
{summary.mostUsedTemplates.map(({ templateId, count }) => (
|
|
223
|
+
<div
|
|
224
|
+
key={templateId}
|
|
225
|
+
className="flex items-center justify-between text-sm"
|
|
226
|
+
>
|
|
227
|
+
<span>{formatTemplateId(templateId)}</span>
|
|
228
|
+
<span className="text-muted-foreground">{count} events</span>
|
|
229
|
+
</div>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
234
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
235
|
+
{/* Most Used Modes */}
|
|
236
|
+
{summary.mostUsedModes.length > 0 && (
|
|
237
|
+
<div>
|
|
238
|
+
<h4 className="mb-2 font-semibold text-blue-400 text-xs uppercase">
|
|
239
|
+
Mode Usage
|
|
240
|
+
</h4>
|
|
241
|
+
<div className="space-y-1">
|
|
242
|
+
{summary.mostUsedModes.map(({ mode, count }) => (
|
|
243
|
+
<div
|
|
244
|
+
key={mode}
|
|
245
|
+
className="flex items-center justify-between text-sm"
|
|
246
|
+
>
|
|
247
|
+
<span>{formatFeatureName(mode)}</span>
|
|
248
|
+
<span className="text-muted-foreground">{count} switches</span>
|
|
249
|
+
</div>
|
|
250
|
+
))}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
)}
|
|
254
254
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
255
|
+
{/* Features Used */}
|
|
256
|
+
<div>
|
|
257
|
+
<h4 className="mb-2 font-semibold text-blue-400 text-xs uppercase">
|
|
258
|
+
Features Used
|
|
259
|
+
</h4>
|
|
260
|
+
<div className="flex flex-wrap gap-2">
|
|
261
|
+
{summary.featuresUsed.map((feature) => (
|
|
262
|
+
<Badge
|
|
263
|
+
key={feature}
|
|
264
|
+
variant="default"
|
|
265
|
+
className="border-green-500/30 bg-green-500/20 text-green-400"
|
|
266
|
+
>
|
|
267
|
+
✓ {formatFeatureName(feature)}
|
|
268
|
+
</Badge>
|
|
269
|
+
))}
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
272
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
273
|
+
{/* Session Info */}
|
|
274
|
+
<div className="text-muted-foreground text-xs">
|
|
275
|
+
Session started: {sessionStart.toLocaleString()}
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
/**
|
|
282
282
|
* Format feature name for display
|
|
283
283
|
*/
|
|
284
284
|
function formatFeatureName(feature: string): string {
|
|
285
|
-
|
|
285
|
+
return feature.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
/**
|
|
289
289
|
* Format template ID for display
|
|
290
290
|
*/
|
|
291
291
|
function formatTemplateId(id: string): string {
|
|
292
|
-
|
|
292
|
+
return id.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
293
293
|
}
|
|
@@ -1,64 +1,64 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react';
|
|
4
3
|
import { Sparkles } from 'lucide-react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
5
|
import { useTemplateRuntime } from './lib/runtime-context';
|
|
6
6
|
|
|
7
7
|
export interface SaveToStudioButtonProps {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
organizationId?: string;
|
|
9
|
+
projectName?: string;
|
|
10
|
+
endpoint?: string;
|
|
11
|
+
token?: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function SaveToStudioButton({
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
organizationId = 'demo-org',
|
|
16
|
+
projectName,
|
|
17
|
+
endpoint,
|
|
18
|
+
token,
|
|
19
19
|
}: SaveToStudioButtonProps) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
const { installer, templateId, template } = useTemplateRuntime();
|
|
21
|
+
const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>(
|
|
22
|
+
'idle'
|
|
23
|
+
);
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
26
|
+
const handleSave = async () => {
|
|
27
|
+
setStatus('saving');
|
|
28
|
+
setError(null);
|
|
29
|
+
try {
|
|
30
|
+
await installer.saveToStudio({
|
|
31
|
+
templateId,
|
|
32
|
+
projectName: projectName ?? `${template.name} demo`,
|
|
33
|
+
organizationId,
|
|
34
|
+
endpoint,
|
|
35
|
+
token,
|
|
36
|
+
});
|
|
37
|
+
setStatus('saved');
|
|
38
|
+
setTimeout(() => setStatus('idle'), 3000);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
setStatus('error');
|
|
41
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex flex-col items-end gap-1">
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
className="btn-primary inline-flex items-center gap-2 text-sm"
|
|
50
|
+
onClick={handleSave}
|
|
51
|
+
disabled={status === 'saving'}
|
|
52
|
+
>
|
|
53
|
+
<Sparkles className="h-4 w-4" />
|
|
54
|
+
{status === 'saving' ? 'Publishing…' : 'Save to Studio'}
|
|
55
|
+
</button>
|
|
56
|
+
{status === 'error' && error ? (
|
|
57
|
+
<p className="text-destructive text-xs">{error}</p>
|
|
58
|
+
) : null}
|
|
59
|
+
{status === 'saved' ? (
|
|
60
|
+
<p className="text-emerald-400 text-xs">Template sent to Studio.</p>
|
|
61
|
+
) : null}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
64
|
}
|