@growthub/cli 0.3.58 → 0.3.60

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.
Files changed (41) hide show
  1. package/assets/worker-kits/growthub-zernio-social-v1/.env.example +5 -0
  2. package/assets/worker-kits/growthub-zernio-social-v1/QUICKSTART.md +36 -4
  3. package/assets/worker-kits/growthub-zernio-social-v1/bundles/growthub-zernio-social-v1.json +30 -1
  4. package/assets/worker-kits/growthub-zernio-social-v1/docs/growthub-agentic-social-platform-ui-shell.md +134 -0
  5. package/assets/worker-kits/growthub-zernio-social-v1/docs/local-adapters.md +2 -2
  6. package/assets/worker-kits/growthub-zernio-social-v1/growthub-meta/README.md +5 -8
  7. package/assets/worker-kits/growthub-zernio-social-v1/growthub-meta/kit-standard.md +1 -1
  8. package/assets/worker-kits/growthub-zernio-social-v1/kit.json +33 -1
  9. package/assets/worker-kits/growthub-zernio-social-v1/skills.md +1 -1
  10. package/assets/worker-kits/growthub-zernio-social-v1/studio/.env.example +3 -0
  11. package/assets/worker-kits/growthub-zernio-social-v1/studio/dist/assets/index-DTmBMuXr.js +78 -0
  12. package/assets/worker-kits/growthub-zernio-social-v1/studio/dist/assets/index-gHr-nTMF.css +1 -0
  13. package/assets/worker-kits/growthub-zernio-social-v1/studio/dist/index.html +14 -0
  14. package/assets/worker-kits/growthub-zernio-social-v1/studio/index.html +13 -0
  15. package/assets/worker-kits/growthub-zernio-social-v1/studio/package-lock.json +1677 -0
  16. package/assets/worker-kits/growthub-zernio-social-v1/studio/package.json +20 -0
  17. package/assets/worker-kits/growthub-zernio-social-v1/studio/serve.mjs +60 -0
  18. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/App.jsx +130 -0
  19. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/api.js +146 -0
  20. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/app.css +558 -0
  21. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/lib/rules.js +64 -0
  22. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/lib/templates.js +207 -0
  23. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/main.jsx +10 -0
  24. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Accounts.jsx +57 -0
  25. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Agent.jsx +167 -0
  26. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Analytics.jsx +164 -0
  27. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/ApiKeys.jsx +143 -0
  28. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Automations.jsx +122 -0
  29. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/CommentRules.jsx +592 -0
  30. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Compose.jsx +185 -0
  31. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Dashboard.jsx +87 -0
  32. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Inbox.jsx +144 -0
  33. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Queues.jsx +167 -0
  34. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Scheduled.jsx +85 -0
  35. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Sequences.jsx +160 -0
  36. package/assets/worker-kits/growthub-zernio-social-v1/studio/src/views/Templates.jsx +275 -0
  37. package/assets/worker-kits/growthub-zernio-social-v1/studio/vite.config.js +7 -0
  38. package/assets/worker-kits/growthub-zernio-social-v1/workers/zernio-social-operator/CLAUDE.md +3 -3
  39. package/dist/index.js +8541 -850
  40. package/package.json +1 -1
  41. package/assets/worker-kits/growthub-zernio-social-v1/docs/postiz-ui-shell-integration.md +0 -166
@@ -0,0 +1,160 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { api } from '../api.js';
3
+ import { useApp } from '../App.jsx';
4
+
5
+ export default function Sequences({ onNavigate }) {
6
+ const { showToast } = useApp();
7
+ const [sequences, setSequences] = useState([]);
8
+ const [loading, setLoading] = useState(true);
9
+ const [toggling, setToggling] = useState(null);
10
+ const [expanded, setExpanded] = useState(null);
11
+
12
+ const load = useCallback(() => {
13
+ api.getSequences()
14
+ .then(d => setSequences(d.sequences || d.data || []))
15
+ .catch(e => showToast(e.message, false))
16
+ .finally(() => setLoading(false));
17
+ }, []);
18
+
19
+ useEffect(load, [load]);
20
+
21
+ const toggle = async (seq) => {
22
+ const id = seq._id || seq.id;
23
+ const isActive = seq.status === 'active';
24
+ setToggling(id);
25
+ try {
26
+ if (isActive) {
27
+ await api.pauseSequence(id);
28
+ showToast('Sequence paused');
29
+ } else {
30
+ await api.activateSequence(id);
31
+ showToast('Sequence activated ✓');
32
+ }
33
+ load();
34
+ } catch (e) {
35
+ showToast(e.message, false);
36
+ } finally {
37
+ setToggling(null);
38
+ }
39
+ };
40
+
41
+ if (loading) return <div className="loading-row"><span className="spinner" />Loading sequences…</div>;
42
+
43
+ if (!sequences.length) return (
44
+ <div>
45
+ <div className="empty">
46
+ <div className="empty-icon">🔀</div>
47
+ <div className="empty-msg">No sequences found.<br />Sequences auto-enroll contacts into multi-step follow-up flows after they trigger a comment rule.</div>
48
+ </div>
49
+
50
+ <div className="card mt12" style={{ maxWidth: 600 }}>
51
+ <div className="section-title mb12">How Sequences Work with Comment Rules</div>
52
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
53
+ {[
54
+ { step: '1', icon: '💬', title: 'Comment trigger fires', desc: 'Someone comments "X" on your post' },
55
+ { step: '2', icon: '📩', title: 'Instant reply + DM', desc: 'Comment Rule sends the lead magnet immediately' },
56
+ { step: '3', icon: '🔀', title: 'Contact enrolled in sequence', desc: 'Commenter auto-enrolled into a follow-up nurture sequence' },
57
+ { step: '4', icon: '📬', title: 'Automated follow-ups', desc: 'Day 1: "Did you get a chance to check it out?" · Day 3: Case study · Day 7: Offer' },
58
+ ].map(s => (
59
+ <div key={s.step} className="row" style={{ gap: 14, alignItems: 'flex-start' }}>
60
+ <div style={{ width: 28, height: 28, borderRadius: '50%', background: 'var(--accentb)', border: '1px solid var(--accent)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, color: 'var(--accentl)', flexShrink: 0 }}>
61
+ {s.step}
62
+ </div>
63
+ <div>
64
+ <div style={{ fontWeight: 500, fontSize: 13 }}>{s.icon} {s.title}</div>
65
+ <div style={{ fontSize: 12, color: 'var(--muted)' }}>{s.desc}</div>
66
+ </div>
67
+ </div>
68
+ ))}
69
+ </div>
70
+ <div style={{ marginTop: 16, fontSize: 12, color: 'var(--muted)' }}>
71
+ Create sequences in the Zernio dashboard, then they appear here for activation management.
72
+ </div>
73
+ </div>
74
+ </div>
75
+ );
76
+
77
+ return (
78
+ <div>
79
+ <div className="row mb16" style={{ justifyContent: 'space-between' }}>
80
+ <div>
81
+ <div className="section-title" style={{ marginBottom: 2 }}>{sequences.length} Sequence{sequences.length !== 1 ? 's' : ''}</div>
82
+ <div style={{ fontSize: 12, color: 'var(--muted)' }}>Multi-step follow-up flows enrolled from comment automations</div>
83
+ </div>
84
+ <button className="btn btn-ghost btn-sm" onClick={load}>↻ Refresh</button>
85
+ </div>
86
+
87
+ {sequences.map(seq => {
88
+ const id = seq._id || seq.id;
89
+ const isActive = seq.status === 'active';
90
+ const steps = seq.steps || seq.messages || [];
91
+ const isExpanded = expanded === id;
92
+
93
+ return (
94
+ <div key={id} className="seq-card">
95
+ <div className="seq-header">
96
+ <div className="row" style={{ gap: 10 }}>
97
+ <span style={{ fontWeight: 600 }}>{seq.name || seq.title || id}</span>
98
+ <span className={`badge ${isActive ? 'badge-green' : 'badge-neutral'}`}>{seq.status || 'unknown'}</span>
99
+ </div>
100
+ <div className="row" style={{ gap: 8 }}>
101
+ <button className="btn btn-ghost btn-sm" onClick={() => setExpanded(e => e === id ? null : id)}>
102
+ {isExpanded ? 'Hide Steps' : `${steps.length} Steps`}
103
+ </button>
104
+ <div className="toggle-wrap" onClick={() => !toggling && toggle(seq)}>
105
+ <div className={`toggle ${isActive ? 'on' : ''}`} />
106
+ <span className="toggle-label">{toggling === id ? '…' : (isActive ? 'On' : 'Off')}</span>
107
+ </div>
108
+ </div>
109
+ </div>
110
+
111
+ <div className="seq-meta">
112
+ {seq.enrolledCount !== undefined && <span>👥 {seq.enrolledCount} enrolled</span>}
113
+ {seq.completedCount !== undefined && <span>✅ {seq.completedCount} completed</span>}
114
+ {seq.trigger && <span>🎯 Trigger: <span style={{ fontFamily: 'monospace', color: 'var(--accentl)' }}>{seq.trigger}</span></span>}
115
+ {seq.createdAt && <span>Created {new Date(seq.createdAt).toLocaleDateString()}</span>}
116
+ </div>
117
+
118
+ {seq.description && (
119
+ <div style={{ fontSize: 12, color: 'var(--muted)', marginBottom: 10 }}>{seq.description}</div>
120
+ )}
121
+
122
+ {isExpanded && steps.length > 0 && (
123
+ <div className="seq-steps">
124
+ {steps.map((step, i) => (
125
+ <div key={i} className="seq-step">
126
+ <span className="seq-step-num">{i + 1}</span>
127
+ <div style={{ flex: 1 }}>
128
+ <div style={{ fontWeight: 500, marginBottom: 2 }}>
129
+ {step.type === 'send_dm' ? '📩' : step.type === 'reply_comment' ? '💬' : '📬'}{' '}
130
+ {step.name || step.type || 'Step'}
131
+ {step.delayDays ? <span style={{ color: 'var(--muted)', fontWeight: 400 }}> · Day {step.delayDays}</span> : null}
132
+ {step.delayHours ? <span style={{ color: 'var(--muted)', fontWeight: 400 }}> · +{step.delayHours}h</span> : null}
133
+ </div>
134
+ {step.content && (
135
+ <div style={{ fontSize: 11, color: 'var(--muted)', fontStyle: 'italic' }}>
136
+ "{step.content.slice(0, 100)}{step.content.length > 100 ? '…' : ''}"
137
+ </div>
138
+ )}
139
+ </div>
140
+ <span className={`badge ${step.status === 'active' ? 'badge-green' : 'badge-neutral'}`} style={{ fontSize: 10 }}>
141
+ {step.status || 'active'}
142
+ </span>
143
+ </div>
144
+ ))}
145
+ </div>
146
+ )}
147
+
148
+ {isExpanded && !steps.length && (
149
+ <div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 6 }}>No steps defined for this sequence.</div>
150
+ )}
151
+ </div>
152
+ );
153
+ })}
154
+
155
+ <div style={{ marginTop: 12, fontSize: 12, color: 'var(--muted)' }}>
156
+ Sequences use <span style={{ fontFamily: 'monospace', color: 'var(--accentl)' }}>POST /api/v1/sequences/:id/activate</span> and <span style={{ fontFamily: 'monospace', color: 'var(--accentl)' }}>/pause</span>. Link sequences to comment rules in <span style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={() => onNavigate?.('commentrules')}>Comment Rules →</span>
157
+ </div>
158
+ </div>
159
+ );
160
+ }
@@ -0,0 +1,275 @@
1
+ import { useState, useEffect } from 'react';
2
+ import {
3
+ getTemplates, saveTemplate, deleteTemplate,
4
+ exportTemplates, importTemplates, previewTemplate,
5
+ TEMPLATE_TYPES, VARIABLES, seedIfEmpty, forceSeed,
6
+ } from '../lib/templates.js';
7
+ import { useApp } from '../App.jsx';
8
+
9
+ const BADGE_CLASS = { blue: 'badge-blue', purple: 'badge-purple', green: 'badge-green' };
10
+
11
+ const EMPTY_FORM = { name: '', type: 'both', body: '', replyBody: '', dmBody: '' };
12
+
13
+ export default function Templates({ onNavigate }) {
14
+ const { showToast } = useApp();
15
+ const [templates, setTemplates] = useState([]);
16
+ const [filter, setFilter] = useState('all');
17
+ const [editing, setEditing] = useState(null);
18
+ const [form, setForm] = useState(EMPTY_FORM);
19
+ const [previewing, setPreviewing] = useState(null);
20
+
21
+ useEffect(() => {
22
+ seedIfEmpty();
23
+ setTemplates(getTemplates());
24
+ }, []);
25
+
26
+ const refresh = () => setTemplates(getTemplates());
27
+
28
+ const openNew = () => { setForm(EMPTY_FORM); setEditing('new'); setPreviewing(null); };
29
+ const openEdit = (t) => {
30
+ setForm({ name: t.name, type: t.type, body: t.body || '', replyBody: t.replyBody || '', dmBody: t.dmBody || '', id: t.id });
31
+ setEditing(t.id);
32
+ setPreviewing(null);
33
+ };
34
+ const cancel = () => { setEditing(null); setForm(EMPTY_FORM); };
35
+
36
+ const save = () => {
37
+ if (!form.name.trim()) { showToast('Template name required', false); return; }
38
+ if (form.type === 'both' && (!form.replyBody.trim() || !form.dmBody.trim())) {
39
+ showToast('Both reply and DM body required for "Reply + DM" type', false); return;
40
+ }
41
+ if (form.type !== 'both' && !form.body.trim()) {
42
+ showToast('Message body required', false); return;
43
+ }
44
+ saveTemplate({ ...form });
45
+ refresh();
46
+ cancel();
47
+ showToast('Template saved ✓');
48
+ };
49
+
50
+ const del = (id) => {
51
+ if (!confirm('Delete this template? Any rules using it will need to be updated.')) return;
52
+ deleteTemplate(id);
53
+ refresh();
54
+ showToast('Template deleted');
55
+ };
56
+
57
+ const insertVar = (token, field) => {
58
+ setForm(f => ({ ...f, [field]: (f[field] || '') + token }));
59
+ };
60
+
61
+ const handleImport = (e) => {
62
+ const file = e.target.files[0];
63
+ if (!file) return;
64
+ const reader = new FileReader();
65
+ reader.onload = (ev) => {
66
+ try {
67
+ importTemplates(ev.target.result);
68
+ refresh();
69
+ showToast('Templates imported ✓');
70
+ } catch (err) {
71
+ showToast(err.message, false);
72
+ }
73
+ };
74
+ reader.readAsText(file);
75
+ e.target.value = '';
76
+ };
77
+
78
+ const visible = filter === 'all' ? templates : templates.filter(t => t.type === filter);
79
+
80
+ return (
81
+ <div>
82
+ <div className="row mb16" style={{ justifyContent: 'space-between', flexWrap: 'wrap', gap: 10 }}>
83
+ <div>
84
+ <div style={{ fontWeight: 600, marginBottom: 4 }}>Template Library</div>
85
+ <div style={{ fontSize: 12, color: 'var(--muted)' }}>Saved messages for auto-reply comments and DM lead magnets</div>
86
+ </div>
87
+ <div className="row" style={{ gap: 8, flexWrap: 'wrap' }}>
88
+ <label className="btn btn-ghost btn-sm" style={{ cursor: 'pointer' }}>
89
+ Import
90
+ <input type="file" accept=".json" style={{ display: 'none' }} onChange={handleImport} />
91
+ </label>
92
+ <button
93
+ className="btn btn-ghost btn-sm"
94
+ title="Reset to all 9 Growthub lead magnet templates"
95
+ onClick={() => { if (confirm('Load all 9 Growthub lead magnet templates? This will overwrite current templates.')) { forceSeed(); refresh(); showToast('Growthub templates loaded ✓'); } }}
96
+ >
97
+ ⚡ Load Growthub Templates
98
+ </button>
99
+ <button className="btn btn-ghost btn-sm" onClick={exportTemplates} disabled={!templates.length}>Export</button>
100
+ <button className="btn btn-primary btn-sm" onClick={openNew}>+ New Template</button>
101
+ </div>
102
+ </div>
103
+
104
+ <div className="filter-bar">
105
+ {[['all','All'], ['both','Reply + DM'], ['reply_comment','Reply Only'], ['send_dm','DM Only']].map(([v, l]) => (
106
+ <button key={v} className={`filter-btn ${filter === v ? 'active' : ''}`} onClick={() => setFilter(v)}>{l}</button>
107
+ ))}
108
+ </div>
109
+
110
+ {editing && (
111
+ <div className="rule-form mb24">
112
+ <div style={{ fontWeight: 600, marginBottom: 16 }}>
113
+ {editing === 'new' ? '+ New Template' : 'Edit Template'}
114
+ </div>
115
+
116
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 180px', gap: 12, marginBottom: 12 }}>
117
+ <div className="field" style={{ marginBottom: 0 }}>
118
+ <label>Template Name</label>
119
+ <input className="input" placeholder="e.g. Lead Magnet — Free Guide" value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} />
120
+ </div>
121
+ <div className="field" style={{ marginBottom: 0 }}>
122
+ <label>Type</label>
123
+ <select className="select" value={form.type} onChange={e => setForm(f => ({ ...f, type: e.target.value }))}>
124
+ <option value="both">Reply + DM</option>
125
+ <option value="reply_comment">Reply Only</option>
126
+ <option value="send_dm">DM Only</option>
127
+ </select>
128
+ </div>
129
+ </div>
130
+
131
+ <div style={{ marginBottom: 10 }}>
132
+ <div style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 6 }}>Insert variable</div>
133
+ <div className="row" style={{ flexWrap: 'wrap', gap: 6 }}>
134
+ {VARIABLES.map(v => (
135
+ <span
136
+ key={v.token}
137
+ className="tpl-var"
138
+ title={v.desc}
139
+ onClick={() => insertVar(v.token, form.type === 'both' ? 'dmBody' : 'body')}
140
+ >
141
+ {v.token}
142
+ </span>
143
+ ))}
144
+ </div>
145
+ </div>
146
+
147
+ {form.type === 'both' ? (
148
+ <div className="both-fields">
149
+ <div className="field" style={{ marginBottom: 0 }}>
150
+ <label>💬 Public Reply (comment)</label>
151
+ <textarea
152
+ className="textarea"
153
+ style={{ minHeight: 90 }}
154
+ placeholder="e.g. Check your DMs, {{firstName}}! 📩"
155
+ value={form.replyBody}
156
+ onChange={e => setForm(f => ({ ...f, replyBody: e.target.value }))}
157
+ />
158
+ <div className="char-count">{form.replyBody.length} chars</div>
159
+ </div>
160
+ <div className="field" style={{ marginBottom: 0 }}>
161
+ <label>📩 DM Body (private)</label>
162
+ <textarea
163
+ className="textarea"
164
+ style={{ minHeight: 90 }}
165
+ placeholder="Hey {{firstName}}! Here's your free guide..."
166
+ value={form.dmBody}
167
+ onChange={e => setForm(f => ({ ...f, dmBody: e.target.value }))}
168
+ />
169
+ <div className="char-count">{form.dmBody.length} chars</div>
170
+ </div>
171
+ </div>
172
+ ) : (
173
+ <div className="field" style={{ marginBottom: 0 }}>
174
+ <label>{form.type === 'send_dm' ? '📩 DM Body' : '💬 Reply Body'}</label>
175
+ <textarea
176
+ className="textarea"
177
+ style={{ minHeight: 120 }}
178
+ placeholder={form.type === 'send_dm' ? 'Hey {{firstName}}! Here\'s your free guide...' : 'Thanks for your interest! Check your DMs 📩'}
179
+ value={form.body}
180
+ onChange={e => setForm(f => ({ ...f, body: e.target.value }))}
181
+ />
182
+ <div className="char-count">{form.body.length} chars</div>
183
+ </div>
184
+ )}
185
+
186
+ {(form.body || form.dmBody || form.replyBody) && (
187
+ <div style={{ marginTop: 12 }}>
188
+ <div style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 6 }}>Preview (sample values)</div>
189
+ <div className="preview-box">
190
+ {form.type === 'both'
191
+ ? `💬 ${previewTemplate(form.replyBody)}\n\n📩 ${previewTemplate(form.dmBody)}`
192
+ : previewTemplate(form.body)}
193
+ </div>
194
+ </div>
195
+ )}
196
+
197
+ <div className="row-end mt12">
198
+ <button className="btn btn-ghost btn-sm" onClick={cancel}>Cancel</button>
199
+ <button className="btn btn-primary btn-sm" onClick={save}>Save Template</button>
200
+ </div>
201
+ </div>
202
+ )}
203
+
204
+ {!visible.length && (
205
+ <div className="empty">
206
+ <div className="empty-icon">📝</div>
207
+ <div className="empty-msg">
208
+ {filter === 'all' ? 'No templates yet. Create your first reply or DM template.' : `No ${filter} templates.`}
209
+ </div>
210
+ </div>
211
+ )}
212
+
213
+ <div className="tpl-grid">
214
+ {visible.map(t => {
215
+ const meta = TEMPLATE_TYPES[t.type] || TEMPLATE_TYPES.both;
216
+ const body = t.type === 'both'
217
+ ? `💬 ${t.replyBody || ''}\n\n📩 ${t.dmBody || ''}`
218
+ : (t.body || '');
219
+ return (
220
+ <div key={t.id} className={`tpl-card ${previewing === t.id ? 'selected' : ''}`}>
221
+ <div className="tpl-type-row">
222
+ <span className="tpl-type-icon">{meta.icon}</span>
223
+ <span className="tpl-name">{t.name}</span>
224
+ <span className={`badge badge-${BADGE_CLASS[meta.color]}`}>{meta.label}</span>
225
+ </div>
226
+
227
+ {t.keyword_hint && (
228
+ <div style={{ fontSize: 11, color: 'var(--muted)' }}>
229
+ 🔑 Trigger: <span style={{ color: 'var(--accentl)', fontFamily: 'monospace' }}>{t.keyword_hint}</span>
230
+ </div>
231
+ )}
232
+ <div className="tpl-body">{body}</div>
233
+
234
+ {previewing === t.id && (
235
+ <div>
236
+ <div style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 5 }}>Preview</div>
237
+ <div className="preview-box" style={{ fontSize: 12, maxHeight: 'none' }}>{previewTemplate(body)}</div>
238
+ </div>
239
+ )}
240
+
241
+ <div className="tpl-footer">
242
+ <button className="btn btn-ghost btn-xs" onClick={() => setPreviewing(p => p === t.id ? null : t.id)}>
243
+ {previewing === t.id ? 'Hide' : 'Preview'}
244
+ </button>
245
+ <button className="btn btn-secondary btn-xs" onClick={() => openEdit(t)}>Edit</button>
246
+ <button
247
+ className="btn btn-ghost btn-xs"
248
+ onClick={() => {
249
+ navigator.clipboard.writeText(body);
250
+ showToast('Copied ✓');
251
+ }}
252
+ >Copy</button>
253
+ <button className="btn btn-danger btn-xs" style={{ marginLeft: 'auto' }} onClick={() => del(t.id)}>✕</button>
254
+ </div>
255
+
256
+ {t.createdAt && (
257
+ <div style={{ fontSize: 10, color: 'var(--muted)' }}>
258
+ {new Date(t.createdAt).toLocaleDateString()}
259
+ </div>
260
+ )}
261
+ </div>
262
+ );
263
+ })}
264
+ </div>
265
+
266
+ {templates.length > 0 && !editing && (
267
+ <div className="row mt12" style={{ justifyContent: 'flex-end' }}>
268
+ <button className="btn btn-primary btn-sm" onClick={() => onNavigate && onNavigate('commentrules')}>
269
+ Use in Comment Rules →
270
+ </button>
271
+ </div>
272
+ )}
273
+ </div>
274
+ );
275
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: { port: 5173, open: true },
7
+ });
@@ -140,13 +140,13 @@ validation-checklist.md
140
140
 
141
141
  These files define the execution environment, platform constraints, and output contract. Do not improvise around them.
142
142
 
143
- If the user is pairing this kit with the Postiz UI shell (a.k.a. `growthub-postiz-social-v1` running as the presentation layer while Zernio is the engine), also read:
143
+ If the user is working on the exported Growthub UI shell that ships with this worker kit, also read:
144
144
 
145
145
  ```text
146
- docs/postiz-ui-shell-integration.md
146
+ docs/growthub-agentic-social-platform-ui-shell.md
147
147
  ```
148
148
 
149
- That document defines the 7-module bridge (provider override, post submission, queue sync, caption surface, platform coverage config, env/secret surface, CLI entry point). Treat it as authoritative whenever a request references Postiz.
149
+ That document defines the exported-workspace UI-shell truth: launch flow, API wiring, comment automation scope, and validation sequence. Treat it as authoritative whenever a request references the Growthub social UI shell.
150
150
 
151
151
  ---
152
152