@treenity/mods 3.0.1 → 3.0.2
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/board/view.tsx +1 -1
- package/brahman/helpers.ts +7 -7
- package/brahman/service.ts +24 -24
- package/brahman/types.ts +21 -21
- package/brahman/views/action-cards.tsx +33 -23
- package/brahman/views/bot-view.tsx +3 -2
- package/brahman/views/chat-editor.tsx +119 -124
- package/brahman/views/menu-editor.tsx +75 -89
- package/brahman/views/page-layout.tsx +10 -8
- package/brahman/views/tstring-input.tsx +25 -15
- package/canary/service.ts +18 -18
- package/dist/board/view.js +1 -1
- package/dist/board/view.js.map +1 -1
- package/dist/brahman/helpers.d.ts +1 -1
- package/dist/brahman/helpers.d.ts.map +1 -1
- package/dist/brahman/helpers.js +6 -6
- package/dist/brahman/helpers.js.map +1 -1
- package/dist/brahman/service.js +24 -24
- package/dist/brahman/service.js.map +1 -1
- package/dist/brahman/types.d.ts +1 -1
- package/dist/brahman/types.d.ts.map +1 -1
- package/dist/brahman/types.js +21 -21
- package/dist/brahman/types.js.map +1 -1
- package/dist/brahman/views/action-cards.d.ts.map +1 -1
- package/dist/brahman/views/action-cards.js +7 -4
- package/dist/brahman/views/action-cards.js.map +1 -1
- package/dist/brahman/views/bot-view.d.ts.map +1 -1
- package/dist/brahman/views/bot-view.js +2 -1
- package/dist/brahman/views/bot-view.js.map +1 -1
- package/dist/brahman/views/chat-editor.d.ts.map +1 -1
- package/dist/brahman/views/chat-editor.js +27 -18
- package/dist/brahman/views/chat-editor.js.map +1 -1
- package/dist/brahman/views/menu-editor.d.ts.map +1 -1
- package/dist/brahman/views/menu-editor.js +12 -16
- package/dist/brahman/views/menu-editor.js.map +1 -1
- package/dist/brahman/views/page-layout.d.ts.map +1 -1
- package/dist/brahman/views/page-layout.js +1 -1
- package/dist/brahman/views/page-layout.js.map +1 -1
- package/dist/brahman/views/tstring-input.d.ts.map +1 -1
- package/dist/brahman/views/tstring-input.js +7 -3
- package/dist/brahman/views/tstring-input.js.map +1 -1
- package/dist/canary/service.js +18 -18
- package/dist/canary/service.js.map +1 -1
- package/dist/doc/fs-codec.js +1 -1
- package/dist/doc/fs-codec.js.map +1 -1
- package/dist/doc/renderers.d.ts.map +1 -1
- package/dist/doc/renderers.js +2 -1
- package/dist/doc/renderers.js.map +1 -1
- package/dist/doc/toolbar.d.ts.map +1 -1
- package/dist/doc/toolbar.js +5 -5
- package/dist/doc/toolbar.js.map +1 -1
- package/dist/launcher/types.js +2 -2
- package/dist/launcher/types.js.map +1 -1
- package/dist/launcher/view.js +2 -2
- package/dist/launcher/view.js.map +1 -1
- package/dist/mindmap/branch.d.ts +10 -0
- package/dist/mindmap/branch.d.ts.map +1 -1
- package/dist/mindmap/branch.js +42 -9
- package/dist/mindmap/branch.js.map +1 -1
- package/dist/mindmap/sidebar.d.ts.map +1 -1
- package/dist/mindmap/sidebar.js +4 -3
- package/dist/mindmap/sidebar.js.map +1 -1
- package/dist/mindmap/view.d.ts.map +1 -1
- package/dist/mindmap/view.js +35 -4
- package/dist/mindmap/view.js.map +1 -1
- package/dist/sensor-demo/service.js +6 -5
- package/dist/sensor-demo/service.js.map +1 -1
- package/dist/sensor-generator/action.js +1 -1
- package/dist/sensor-generator/action.js.map +1 -1
- package/dist/sim/service.js +41 -41
- package/dist/sim/service.js.map +1 -1
- package/dist/table/view.js.map +1 -1
- package/dist/todo/types.js +2 -2
- package/dist/todo/types.js.map +1 -1
- package/dist/todo/view.js +6 -4
- package/dist/todo/view.js.map +1 -1
- package/dist/whisper/inbox.js +3 -3
- package/dist/whisper/inbox.js.map +1 -1
- package/dist/whisper/route.d.ts +1 -1
- package/dist/whisper/route.d.ts.map +1 -1
- package/dist/whisper/route.js +13 -13
- package/dist/whisper/route.js.map +1 -1
- package/doc/CLAUDE.md +1 -1
- package/doc/fs-codec.ts +1 -1
- package/doc/renderers.tsx +4 -3
- package/doc/toolbar.tsx +12 -9
- package/launcher/types.ts +2 -2
- package/launcher/view.tsx +12 -8
- package/mindmap/branch.tsx +121 -22
- package/mindmap/mindmap.css +52 -0
- package/mindmap/sidebar.tsx +9 -6
- package/mindmap/view.tsx +40 -4
- package/package.json +27 -3
- package/sensor-demo/service.ts +6 -5
- package/sensor-generator/action.ts +1 -1
- package/sim/service.ts +41 -41
- package/table/view.tsx +7 -2
- package/todo/types.ts +2 -2
- package/todo/view.tsx +9 -10
- package/whisper/inbox.ts +3 -3
- package/whisper/route.ts +13 -13
- package/board/board.test.ts +0 -212
- package/brahman/brahman.test.ts +0 -855
- package/doc/fs-codec.test.ts +0 -119
- package/doc/markdown.test.ts +0 -152
- package/sim/sim.test.ts +0 -282
|
@@ -21,11 +21,16 @@ import {
|
|
|
21
21
|
import { CSS } from '@dnd-kit/utilities';
|
|
22
22
|
import type { NodeData } from '@treenity/core';
|
|
23
23
|
import { getDefaults } from '@treenity/core/comp';
|
|
24
|
+
import { Button } from '@treenity/react/components/ui/button';
|
|
25
|
+
import { Checkbox } from '@treenity/react/components/ui/checkbox';
|
|
26
|
+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@treenity/react/components/ui/dialog';
|
|
27
|
+
import { Input } from '@treenity/react/components/ui/input';
|
|
28
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@treenity/react/components/ui/select';
|
|
24
29
|
import { set, useChildren, usePath } from '@treenity/react/hooks';
|
|
30
|
+
import { sanitizeHref } from '@treenity/react/lib/sanitize-href';
|
|
25
31
|
import { trpc } from '@treenity/react/trpc';
|
|
26
|
-
import { Camera, File, GripVertical, Mic, MoreHorizontal, Plus, Trash2, Video
|
|
32
|
+
import { Camera, File, GripVertical, Mic, MoreHorizontal, Plus, Trash2, Video } from 'lucide-react';
|
|
27
33
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
28
|
-
import { createPortal } from 'react-dom';
|
|
29
34
|
import { ACTION_TYPES, MENU_TYPES, type MenuButton, type MenuRow, type MenuType, type TString } from '../types';
|
|
30
35
|
import { actionIcon, actionSummary } from './action-cards';
|
|
31
36
|
import { TStringLineInput, tstringPreview } from './tstring-input';
|
|
@@ -97,7 +102,8 @@ function sanitizeHtml(html: string): string {
|
|
|
97
102
|
if (!ALLOWED_TAGS.has(tag)) return inner;
|
|
98
103
|
if (tag === 'a') {
|
|
99
104
|
const href = el.getAttribute('href');
|
|
100
|
-
|
|
105
|
+
const safe = href ? sanitizeHref(href) : null;
|
|
106
|
+
return safe ? `<a href="${safe}">${inner}</a>` : inner;
|
|
101
107
|
}
|
|
102
108
|
return `<${tag}>${inner}</${tag}>`;
|
|
103
109
|
}
|
|
@@ -144,6 +150,7 @@ function EditableText({
|
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
// ── Bubble shells ──
|
|
153
|
+
// NOTE: These use hardcoded Telegram dark-theme colors intentionally to emulate the TG chat UI
|
|
147
154
|
|
|
148
155
|
function BotBubble({ children, first, tools }: { children: React.ReactNode; first?: boolean; tools?: React.ReactNode }) {
|
|
149
156
|
return (
|
|
@@ -187,9 +194,9 @@ function SystemPill({ children, onClick }: { children: React.ReactNode; onClick?
|
|
|
187
194
|
);
|
|
188
195
|
}
|
|
189
196
|
|
|
190
|
-
// ── Settings
|
|
197
|
+
// ── Settings dialog ──
|
|
191
198
|
|
|
192
|
-
function
|
|
199
|
+
function SettingsDialog({
|
|
193
200
|
node,
|
|
194
201
|
onUpdate,
|
|
195
202
|
onDelete,
|
|
@@ -202,65 +209,60 @@ function SettingsPopover({
|
|
|
202
209
|
}) {
|
|
203
210
|
const type = node.$type;
|
|
204
211
|
|
|
205
|
-
return
|
|
206
|
-
<
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
onClick={e => e.stopPropagation()}
|
|
211
|
-
>
|
|
212
|
-
<div className="flex items-center justify-between mb-1">
|
|
213
|
-
<span className="text-xs font-medium text-[#e1e3e6]">
|
|
212
|
+
return (
|
|
213
|
+
<Dialog open onOpenChange={open => { if (!open) onClose(); }}>
|
|
214
|
+
<DialogContent className="sm:max-w-xs">
|
|
215
|
+
<DialogHeader>
|
|
216
|
+
<DialogTitle className="text-sm">
|
|
214
217
|
{ACTION_TYPES.find(a => a.type === type)?.label ?? type.split('.').at(-1)}
|
|
215
|
-
</
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
{
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
</
|
|
218
|
+
</DialogTitle>
|
|
219
|
+
</DialogHeader>
|
|
220
|
+
|
|
221
|
+
<div className="space-y-2">
|
|
222
|
+
{/* Type-specific settings */}
|
|
223
|
+
{type === 'brahman.action.message' && (
|
|
224
|
+
<>
|
|
225
|
+
<SettingSelect label="Menu" value={(node.menuType as string) ?? 'none'}
|
|
226
|
+
options={MENU_TYPES.map(m => ({ value: m.value, label: m.label }))}
|
|
227
|
+
onChange={v => onUpdate({ menuType: v })} />
|
|
228
|
+
<SettingCheckbox label="Disable links" checked={!!node.disableLinks}
|
|
229
|
+
onChange={v => onUpdate({ disableLinks: v })} />
|
|
230
|
+
</>
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
{type === 'brahman.action.question' && (
|
|
234
|
+
<>
|
|
235
|
+
<SettingSelect label="Input type" value={(node.inputType as string) ?? 'text'}
|
|
236
|
+
options={[{ value: 'text', label: 'Text' }, { value: 'photo', label: 'Photo' }]}
|
|
237
|
+
onChange={v => onUpdate({ inputType: v })} />
|
|
238
|
+
<SettingInput label="Save to" value={(node.saveTo as string) ?? ''}
|
|
239
|
+
placeholder="session.field" onChange={v => onUpdate({ saveTo: v })} />
|
|
240
|
+
</>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
{type === 'brahman.action.file' && (
|
|
244
|
+
<SettingSelect label="Send as" value={(node.asType as string) ?? ''}
|
|
245
|
+
options={[
|
|
246
|
+
{ value: '', label: 'Auto' }, { value: 'photo', label: 'Photo' },
|
|
247
|
+
{ value: 'document', label: 'Document' }, { value: 'video', label: 'Video' },
|
|
248
|
+
{ value: 'audio', label: 'Audio' }, { value: 'voice', label: 'Voice' },
|
|
249
|
+
]}
|
|
250
|
+
onChange={v => onUpdate({ asType: v })} />
|
|
251
|
+
)}
|
|
252
|
+
|
|
253
|
+
<div className="pt-1 border-t border-border">
|
|
254
|
+
<Button
|
|
255
|
+
variant="ghost"
|
|
256
|
+
size="sm"
|
|
257
|
+
onClick={onDelete}
|
|
258
|
+
className="text-destructive hover:text-destructive p-0 h-auto text-xs"
|
|
259
|
+
>
|
|
260
|
+
<Trash2 className="h-3 w-3 mr-1" /> Delete action
|
|
261
|
+
</Button>
|
|
262
|
+
</div>
|
|
260
263
|
</div>
|
|
261
|
-
</
|
|
262
|
-
</
|
|
263
|
-
document.body,
|
|
264
|
+
</DialogContent>
|
|
265
|
+
</Dialog>
|
|
264
266
|
);
|
|
265
267
|
}
|
|
266
268
|
|
|
@@ -269,13 +271,15 @@ function SettingSelect({ label, value, options, onChange }: {
|
|
|
269
271
|
}) {
|
|
270
272
|
return (
|
|
271
273
|
<div className="flex items-center justify-between gap-2">
|
|
272
|
-
<span className="text-[11px] text-
|
|
273
|
-
<
|
|
274
|
-
className="
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
274
|
+
<span className="text-[11px] text-muted-foreground">{label}</span>
|
|
275
|
+
<Select value={value} onValueChange={onChange}>
|
|
276
|
+
<SelectTrigger size="sm" className="w-auto">
|
|
277
|
+
<SelectValue />
|
|
278
|
+
</SelectTrigger>
|
|
279
|
+
<SelectContent>
|
|
280
|
+
{options.map(o => <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>)}
|
|
281
|
+
</SelectContent>
|
|
282
|
+
</Select>
|
|
279
283
|
</div>
|
|
280
284
|
);
|
|
281
285
|
}
|
|
@@ -285,9 +289,8 @@ function SettingCheckbox({ label, checked, onChange }: {
|
|
|
285
289
|
}) {
|
|
286
290
|
return (
|
|
287
291
|
<label className="flex items-center justify-between gap-2 cursor-pointer">
|
|
288
|
-
<span className="text-[11px] text-
|
|
289
|
-
<
|
|
290
|
-
className="rounded border-[#2b3945]" />
|
|
292
|
+
<span className="text-[11px] text-muted-foreground">{label}</span>
|
|
293
|
+
<Checkbox checked={checked} onChange={e => onChange(e.target.checked)} />
|
|
291
294
|
</label>
|
|
292
295
|
);
|
|
293
296
|
}
|
|
@@ -297,9 +300,9 @@ function SettingInput({ label, value, placeholder, onChange }: {
|
|
|
297
300
|
}) {
|
|
298
301
|
return (
|
|
299
302
|
<div className="flex items-center justify-between gap-2">
|
|
300
|
-
<span className="text-[11px] text-
|
|
301
|
-
<
|
|
302
|
-
className="
|
|
303
|
+
<span className="text-[11px] text-muted-foreground shrink-0">{label}</span>
|
|
304
|
+
<Input
|
|
305
|
+
className="w-32 h-7 text-xs font-mono"
|
|
303
306
|
value={value} placeholder={placeholder} onChange={e => onChange(e.target.value)}
|
|
304
307
|
/>
|
|
305
308
|
</div>
|
|
@@ -387,9 +390,9 @@ function EditableButtons({
|
|
|
387
390
|
+ row
|
|
388
391
|
</button>
|
|
389
392
|
|
|
390
|
-
{/* Button edit
|
|
393
|
+
{/* Button edit dialog */}
|
|
391
394
|
{editBtn && rows[editBtn.ri]?.buttons[editBtn.bi] && (
|
|
392
|
-
<
|
|
395
|
+
<ButtonEditDialog
|
|
393
396
|
button={rows[editBtn.ri].buttons[editBtn.bi]}
|
|
394
397
|
onSave={btn => updateButton(editBtn.ri, editBtn.bi, btn)}
|
|
395
398
|
onDelete={() => deleteButton(editBtn.ri, editBtn.bi)}
|
|
@@ -400,7 +403,7 @@ function EditableButtons({
|
|
|
400
403
|
);
|
|
401
404
|
}
|
|
402
405
|
|
|
403
|
-
function
|
|
406
|
+
function ButtonEditDialog({
|
|
404
407
|
button,
|
|
405
408
|
onSave,
|
|
406
409
|
onDelete,
|
|
@@ -413,54 +416,46 @@ function ButtonEditPopover({
|
|
|
413
416
|
}) {
|
|
414
417
|
const [draft, setDraft] = useState(() => ({ ...button }));
|
|
415
418
|
|
|
416
|
-
return
|
|
417
|
-
<
|
|
418
|
-
<
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
<div className="
|
|
424
|
-
<
|
|
425
|
-
|
|
426
|
-
<
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
<TStringLineInput
|
|
433
|
-
value={draft.title}
|
|
434
|
-
onChange={title => setDraft(d => ({ ...d, title }))}
|
|
435
|
-
langs={['ru', 'en']}
|
|
436
|
-
/>
|
|
437
|
-
</div>
|
|
419
|
+
return (
|
|
420
|
+
<Dialog open onOpenChange={open => { if (!open) onClose(); }}>
|
|
421
|
+
<DialogContent className="sm:max-w-xs">
|
|
422
|
+
<DialogHeader>
|
|
423
|
+
<DialogTitle className="text-sm">Edit button</DialogTitle>
|
|
424
|
+
</DialogHeader>
|
|
425
|
+
|
|
426
|
+
<div className="space-y-2">
|
|
427
|
+
<div>
|
|
428
|
+
<span className="text-[11px] text-muted-foreground">Title</span>
|
|
429
|
+
<TStringLineInput
|
|
430
|
+
value={draft.title}
|
|
431
|
+
onChange={title => setDraft(d => ({ ...d, title }))}
|
|
432
|
+
langs={['ru', 'en']}
|
|
433
|
+
/>
|
|
434
|
+
</div>
|
|
438
435
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
436
|
+
<div className="flex items-center gap-2">
|
|
437
|
+
<span className="text-[11px] text-muted-foreground shrink-0">URL</span>
|
|
438
|
+
<Input
|
|
439
|
+
className="flex-1 h-7 text-xs"
|
|
440
|
+
placeholder="https://..."
|
|
441
|
+
value={draft.url ?? ''}
|
|
442
|
+
onChange={e => setDraft(d => ({ ...d, url: e.target.value || undefined }))}
|
|
443
|
+
/>
|
|
444
|
+
</div>
|
|
447
445
|
</div>
|
|
448
446
|
|
|
449
|
-
<
|
|
450
|
-
<
|
|
451
|
-
className="text-
|
|
452
|
-
<Trash2 className="h-3 w-3" /> Delete
|
|
453
|
-
</
|
|
447
|
+
<DialogFooter>
|
|
448
|
+
<Button variant="ghost" size="sm" onClick={onDelete}
|
|
449
|
+
className="text-destructive hover:text-destructive text-xs">
|
|
450
|
+
<Trash2 className="h-3 w-3 mr-1" /> Delete
|
|
451
|
+
</Button>
|
|
454
452
|
<div className="flex gap-1">
|
|
455
|
-
<
|
|
456
|
-
|
|
457
|
-
<button type="button" onClick={() => { onSave(draft); onClose(); }}
|
|
458
|
-
className="text-xs bg-[#2b5278] text-[#e1e3e6] px-3 py-1 rounded hover:bg-[#3d6a99]">Save</button>
|
|
453
|
+
<Button variant="outline" size="sm" onClick={onClose}>Cancel</Button>
|
|
454
|
+
<Button size="sm" onClick={() => { onSave(draft); onClose(); }}>Save</Button>
|
|
459
455
|
</div>
|
|
460
|
-
</
|
|
461
|
-
</
|
|
462
|
-
</
|
|
463
|
-
document.body,
|
|
456
|
+
</DialogFooter>
|
|
457
|
+
</DialogContent>
|
|
458
|
+
</Dialog>
|
|
464
459
|
);
|
|
465
460
|
}
|
|
466
461
|
|
|
@@ -614,7 +609,7 @@ function SortableAction({
|
|
|
614
609
|
{dragHandle}
|
|
615
610
|
{content}
|
|
616
611
|
{showSettings && (
|
|
617
|
-
<
|
|
612
|
+
<SettingsDialog
|
|
618
613
|
node={n}
|
|
619
614
|
onUpdate={update}
|
|
620
615
|
onDelete={onDelete}
|
|
@@ -644,12 +639,12 @@ function ChatActionPalette({ onSelect }: { onSelect: (type: string) => void }) {
|
|
|
644
639
|
|
|
645
640
|
{open && (
|
|
646
641
|
<div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-1 z-50 w-52
|
|
647
|
-
bg-
|
|
642
|
+
bg-popover border border-border rounded-lg shadow-xl py-1 max-h-64 overflow-y-auto">
|
|
648
643
|
{ACTION_TYPES.map(at => (
|
|
649
644
|
<button
|
|
650
645
|
key={at.type}
|
|
651
646
|
type="button"
|
|
652
|
-
className="w-full text-left px-3 py-1.5 text-xs
|
|
647
|
+
className="w-full text-left px-3 py-1.5 text-xs hover:bg-accent flex items-center gap-2"
|
|
653
648
|
onClick={() => { onSelect(at.type); setOpen(false); }}
|
|
654
649
|
>
|
|
655
650
|
{actionIcon(at.type)}
|
|
@@ -15,41 +15,15 @@ import { arrayMove, horizontalListSortingStrategy, SortableContext, useSortable
|
|
|
15
15
|
import { CSS } from '@dnd-kit/utilities';
|
|
16
16
|
import { Badge } from '@treenity/react/components/ui/badge';
|
|
17
17
|
import { Button } from '@treenity/react/components/ui/button';
|
|
18
|
+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@treenity/react/components/ui/dialog';
|
|
18
19
|
import { Input } from '@treenity/react/components/ui/input';
|
|
20
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@treenity/react/components/ui/select';
|
|
19
21
|
import { trpc } from '@treenity/react/trpc';
|
|
20
22
|
import { ArrowRight, Plus, X } from 'lucide-react';
|
|
21
23
|
import { useRef, useState } from 'react';
|
|
22
|
-
import { createPortal } from 'react-dom';
|
|
23
24
|
import { ACTION_TYPES, MENU_TYPES, type MenuButton, type MenuRow, type MenuType, type TString } from '../types';
|
|
24
25
|
import { TStringLineInput, tstringPreview } from './tstring-input';
|
|
25
26
|
|
|
26
|
-
// ── Modal (lightweight, no radix dependency) ──
|
|
27
|
-
|
|
28
|
-
function Modal({ open, onClose, title, children }: {
|
|
29
|
-
open: boolean;
|
|
30
|
-
onClose: () => void;
|
|
31
|
-
title: string;
|
|
32
|
-
children: React.ReactNode;
|
|
33
|
-
}) {
|
|
34
|
-
if (!open) return null;
|
|
35
|
-
|
|
36
|
-
return createPortal(
|
|
37
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
38
|
-
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
|
|
39
|
-
<div className="relative bg-background border border-border rounded-lg shadow-lg w-full max-w-md mx-4">
|
|
40
|
-
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
|
41
|
-
<h3 className="text-sm font-semibold">{title}</h3>
|
|
42
|
-
<button type="button" onClick={onClose} className="text-muted-foreground hover:text-foreground">
|
|
43
|
-
<X className="h-4 w-4" />
|
|
44
|
-
</button>
|
|
45
|
-
</div>
|
|
46
|
-
<div className="p-4 space-y-4">{children}</div>
|
|
47
|
-
</div>
|
|
48
|
-
</div>,
|
|
49
|
-
document.body,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
27
|
// ── Tag editor (for button tags) ──
|
|
54
28
|
|
|
55
29
|
function TagEditor({ tags, onChange }: { tags: string[]; onChange: (tags: string[]) => void }) {
|
|
@@ -107,56 +81,66 @@ function ButtonEditModal({
|
|
|
107
81
|
}
|
|
108
82
|
|
|
109
83
|
return (
|
|
110
|
-
<
|
|
111
|
-
<
|
|
112
|
-
<
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
84
|
+
<Dialog open onOpenChange={open => { if (!open) onClose(); }}>
|
|
85
|
+
<DialogContent className="sm:max-w-md">
|
|
86
|
+
<DialogHeader>
|
|
87
|
+
<DialogTitle>Edit Button</DialogTitle>
|
|
88
|
+
</DialogHeader>
|
|
89
|
+
|
|
90
|
+
<div className="space-y-3">
|
|
91
|
+
<div>
|
|
92
|
+
<label className="text-xs font-medium text-muted-foreground">Title</label>
|
|
93
|
+
<TStringLineInput value={draft.title} onChange={t => update('title', t)} langs={langs} />
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div>
|
|
97
|
+
<label className="text-xs font-medium text-muted-foreground">URL (optional)</label>
|
|
98
|
+
<Input className="h-8 text-sm" placeholder="https://..."
|
|
99
|
+
value={draft.url ?? ''} onChange={e => update('url', e.target.value || undefined)} />
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div>
|
|
103
|
+
<label className="text-xs font-medium text-muted-foreground">Tags</label>
|
|
104
|
+
<TagEditor tags={draft.tags ?? []} onChange={t => update('tags', t)} />
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div>
|
|
108
|
+
<label className="text-xs font-medium text-muted-foreground">Button action</label>
|
|
109
|
+
<Select
|
|
110
|
+
value={draft.action?.type ?? ''}
|
|
111
|
+
onValueChange={v => {
|
|
112
|
+
if (!v) { update('action', undefined); return; }
|
|
113
|
+
update('action', { type: v });
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
<SelectTrigger size="sm" className="w-full">
|
|
117
|
+
<SelectValue placeholder="No action" />
|
|
118
|
+
</SelectTrigger>
|
|
119
|
+
<SelectContent>
|
|
120
|
+
<SelectItem value="">No action</SelectItem>
|
|
121
|
+
{ACTION_TYPES.map(at => (
|
|
122
|
+
<SelectItem key={at.type} value={at.type}>{at.label}</SelectItem>
|
|
123
|
+
))}
|
|
124
|
+
</SelectContent>
|
|
125
|
+
</Select>
|
|
126
|
+
|
|
127
|
+
{draft.action?.type === 'brahman.action.page' && (
|
|
128
|
+
<Input className="h-8 text-sm mt-1" placeholder="Target page path"
|
|
129
|
+
value={(draft.action.target as string) ?? ''}
|
|
130
|
+
onChange={e => update('action', { ...draft.action!, target: e.target.value })} />
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
126
133
|
</div>
|
|
127
134
|
|
|
128
|
-
<
|
|
129
|
-
<
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
>
|
|
138
|
-
<option value="">No action</option>
|
|
139
|
-
{ACTION_TYPES.map(at => (
|
|
140
|
-
<option key={at.type} value={at.type}>{at.label}</option>
|
|
141
|
-
))}
|
|
142
|
-
</select>
|
|
143
|
-
|
|
144
|
-
{draft.action?.type === 'brahman.action.page' && (
|
|
145
|
-
<Input className="h-8 text-sm mt-1" placeholder="Target page path"
|
|
146
|
-
value={(draft.action.target as string) ?? ''}
|
|
147
|
-
onChange={e => update('action', { ...draft.action!, target: e.target.value })} />
|
|
148
|
-
)}
|
|
149
|
-
</div>
|
|
150
|
-
</div>
|
|
151
|
-
|
|
152
|
-
<div className="flex justify-between pt-2">
|
|
153
|
-
<Button variant="destructive" size="sm" onClick={onDelete}>Delete</Button>
|
|
154
|
-
<div className="flex gap-2">
|
|
155
|
-
<Button variant="outline" size="sm" onClick={onClose}>Cancel</Button>
|
|
156
|
-
<Button size="sm" onClick={() => { onSave(draft); onClose(); }}>Save</Button>
|
|
157
|
-
</div>
|
|
158
|
-
</div>
|
|
159
|
-
</Modal>
|
|
135
|
+
<DialogFooter>
|
|
136
|
+
<Button variant="destructive" size="sm" onClick={onDelete}>Delete</Button>
|
|
137
|
+
<div className="flex gap-2">
|
|
138
|
+
<Button variant="outline" size="sm" onClick={onClose}>Cancel</Button>
|
|
139
|
+
<Button size="sm" onClick={() => { onSave(draft); onClose(); }}>Save</Button>
|
|
140
|
+
</div>
|
|
141
|
+
</DialogFooter>
|
|
142
|
+
</DialogContent>
|
|
143
|
+
</Dialog>
|
|
160
144
|
);
|
|
161
145
|
}
|
|
162
146
|
|
|
@@ -354,15 +338,16 @@ export function MenuEditor({ menuType = 'none', rows = [], langs, onChangeType,
|
|
|
354
338
|
{/* Menu type selector */}
|
|
355
339
|
<div className="flex items-center gap-3">
|
|
356
340
|
<label className="text-xs font-medium text-muted-foreground w-20">Menu type</label>
|
|
357
|
-
<
|
|
358
|
-
className="flex-1
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
341
|
+
<Select value={menuType} onValueChange={v => onChangeType(v as MenuType)}>
|
|
342
|
+
<SelectTrigger size="sm" className="flex-1">
|
|
343
|
+
<SelectValue />
|
|
344
|
+
</SelectTrigger>
|
|
345
|
+
<SelectContent>
|
|
346
|
+
{MENU_TYPES.map(mt => (
|
|
347
|
+
<SelectItem key={mt.value} value={mt.value}>{mt.label}</SelectItem>
|
|
348
|
+
))}
|
|
349
|
+
</SelectContent>
|
|
350
|
+
</Select>
|
|
366
351
|
</div>
|
|
367
352
|
|
|
368
353
|
{/* Rows & buttons grid */}
|
|
@@ -409,14 +394,15 @@ export function MenuEditor({ menuType = 'none', rows = [], langs, onChangeType,
|
|
|
409
394
|
</div>
|
|
410
395
|
</SortableContext>
|
|
411
396
|
|
|
412
|
-
<
|
|
413
|
-
|
|
397
|
+
<Button
|
|
398
|
+
variant="ghost"
|
|
399
|
+
size="sm"
|
|
400
|
+
className="p-1 h-auto"
|
|
414
401
|
onClick={() => addButton(ri)}
|
|
415
|
-
className="p-1 text-muted-foreground hover:text-foreground"
|
|
416
402
|
title="Add button to row"
|
|
417
403
|
>
|
|
418
404
|
<Plus className="h-3.5 w-3.5" />
|
|
419
|
-
</
|
|
405
|
+
</Button>
|
|
420
406
|
</div>
|
|
421
407
|
</div>
|
|
422
408
|
))}
|
|
@@ -112,22 +112,24 @@ function SortableAction({
|
|
|
112
112
|
)}
|
|
113
113
|
</div>
|
|
114
114
|
|
|
115
|
-
<
|
|
116
|
-
|
|
115
|
+
<Button
|
|
116
|
+
variant="ghost"
|
|
117
|
+
size="sm"
|
|
118
|
+
className="p-0 h-auto text-[10px] text-muted-foreground opacity-0 group-hover:opacity-100"
|
|
117
119
|
onClick={e => { e.stopPropagation(); onSelect(); }}
|
|
118
|
-
className="text-muted-foreground hover:text-foreground text-[10px] opacity-0 group-hover:opacity-100"
|
|
119
120
|
title="Select in tree"
|
|
120
121
|
>
|
|
121
122
|
↗
|
|
122
|
-
</
|
|
123
|
+
</Button>
|
|
123
124
|
|
|
124
|
-
<
|
|
125
|
-
|
|
125
|
+
<Button
|
|
126
|
+
variant="ghost"
|
|
127
|
+
size="sm"
|
|
128
|
+
className="p-0 h-auto opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-destructive"
|
|
126
129
|
onClick={e => { e.stopPropagation(); onRemove(); }}
|
|
127
|
-
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-destructive transition-opacity"
|
|
128
130
|
>
|
|
129
131
|
<Trash2 className="h-3.5 w-3.5" />
|
|
130
|
-
</
|
|
132
|
+
</Button>
|
|
131
133
|
</div>
|
|
132
134
|
|
|
133
135
|
{/* Inline editor */}
|