@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.
Files changed (106) hide show
  1. package/board/view.tsx +1 -1
  2. package/brahman/helpers.ts +7 -7
  3. package/brahman/service.ts +24 -24
  4. package/brahman/types.ts +21 -21
  5. package/brahman/views/action-cards.tsx +33 -23
  6. package/brahman/views/bot-view.tsx +3 -2
  7. package/brahman/views/chat-editor.tsx +119 -124
  8. package/brahman/views/menu-editor.tsx +75 -89
  9. package/brahman/views/page-layout.tsx +10 -8
  10. package/brahman/views/tstring-input.tsx +25 -15
  11. package/canary/service.ts +18 -18
  12. package/dist/board/view.js +1 -1
  13. package/dist/board/view.js.map +1 -1
  14. package/dist/brahman/helpers.d.ts +1 -1
  15. package/dist/brahman/helpers.d.ts.map +1 -1
  16. package/dist/brahman/helpers.js +6 -6
  17. package/dist/brahman/helpers.js.map +1 -1
  18. package/dist/brahman/service.js +24 -24
  19. package/dist/brahman/service.js.map +1 -1
  20. package/dist/brahman/types.d.ts +1 -1
  21. package/dist/brahman/types.d.ts.map +1 -1
  22. package/dist/brahman/types.js +21 -21
  23. package/dist/brahman/types.js.map +1 -1
  24. package/dist/brahman/views/action-cards.d.ts.map +1 -1
  25. package/dist/brahman/views/action-cards.js +7 -4
  26. package/dist/brahman/views/action-cards.js.map +1 -1
  27. package/dist/brahman/views/bot-view.d.ts.map +1 -1
  28. package/dist/brahman/views/bot-view.js +2 -1
  29. package/dist/brahman/views/bot-view.js.map +1 -1
  30. package/dist/brahman/views/chat-editor.d.ts.map +1 -1
  31. package/dist/brahman/views/chat-editor.js +27 -18
  32. package/dist/brahman/views/chat-editor.js.map +1 -1
  33. package/dist/brahman/views/menu-editor.d.ts.map +1 -1
  34. package/dist/brahman/views/menu-editor.js +12 -16
  35. package/dist/brahman/views/menu-editor.js.map +1 -1
  36. package/dist/brahman/views/page-layout.d.ts.map +1 -1
  37. package/dist/brahman/views/page-layout.js +1 -1
  38. package/dist/brahman/views/page-layout.js.map +1 -1
  39. package/dist/brahman/views/tstring-input.d.ts.map +1 -1
  40. package/dist/brahman/views/tstring-input.js +7 -3
  41. package/dist/brahman/views/tstring-input.js.map +1 -1
  42. package/dist/canary/service.js +18 -18
  43. package/dist/canary/service.js.map +1 -1
  44. package/dist/doc/fs-codec.js +1 -1
  45. package/dist/doc/fs-codec.js.map +1 -1
  46. package/dist/doc/renderers.d.ts.map +1 -1
  47. package/dist/doc/renderers.js +2 -1
  48. package/dist/doc/renderers.js.map +1 -1
  49. package/dist/doc/toolbar.d.ts.map +1 -1
  50. package/dist/doc/toolbar.js +5 -5
  51. package/dist/doc/toolbar.js.map +1 -1
  52. package/dist/launcher/types.js +2 -2
  53. package/dist/launcher/types.js.map +1 -1
  54. package/dist/launcher/view.js +2 -2
  55. package/dist/launcher/view.js.map +1 -1
  56. package/dist/mindmap/branch.d.ts +10 -0
  57. package/dist/mindmap/branch.d.ts.map +1 -1
  58. package/dist/mindmap/branch.js +42 -9
  59. package/dist/mindmap/branch.js.map +1 -1
  60. package/dist/mindmap/sidebar.d.ts.map +1 -1
  61. package/dist/mindmap/sidebar.js +4 -3
  62. package/dist/mindmap/sidebar.js.map +1 -1
  63. package/dist/mindmap/view.d.ts.map +1 -1
  64. package/dist/mindmap/view.js +35 -4
  65. package/dist/mindmap/view.js.map +1 -1
  66. package/dist/sensor-demo/service.js +6 -5
  67. package/dist/sensor-demo/service.js.map +1 -1
  68. package/dist/sensor-generator/action.js +1 -1
  69. package/dist/sensor-generator/action.js.map +1 -1
  70. package/dist/sim/service.js +41 -41
  71. package/dist/sim/service.js.map +1 -1
  72. package/dist/table/view.js.map +1 -1
  73. package/dist/todo/types.js +2 -2
  74. package/dist/todo/types.js.map +1 -1
  75. package/dist/todo/view.js +6 -4
  76. package/dist/todo/view.js.map +1 -1
  77. package/dist/whisper/inbox.js +3 -3
  78. package/dist/whisper/inbox.js.map +1 -1
  79. package/dist/whisper/route.d.ts +1 -1
  80. package/dist/whisper/route.d.ts.map +1 -1
  81. package/dist/whisper/route.js +13 -13
  82. package/dist/whisper/route.js.map +1 -1
  83. package/doc/CLAUDE.md +1 -1
  84. package/doc/fs-codec.ts +1 -1
  85. package/doc/renderers.tsx +4 -3
  86. package/doc/toolbar.tsx +12 -9
  87. package/launcher/types.ts +2 -2
  88. package/launcher/view.tsx +12 -8
  89. package/mindmap/branch.tsx +121 -22
  90. package/mindmap/mindmap.css +52 -0
  91. package/mindmap/sidebar.tsx +9 -6
  92. package/mindmap/view.tsx +40 -4
  93. package/package.json +27 -3
  94. package/sensor-demo/service.ts +6 -5
  95. package/sensor-generator/action.ts +1 -1
  96. package/sim/service.ts +41 -41
  97. package/table/view.tsx +7 -2
  98. package/todo/types.ts +2 -2
  99. package/todo/view.tsx +9 -10
  100. package/whisper/inbox.ts +3 -3
  101. package/whisper/route.ts +13 -13
  102. package/board/board.test.ts +0 -212
  103. package/brahman/brahman.test.ts +0 -855
  104. package/doc/fs-codec.test.ts +0 -119
  105. package/doc/markdown.test.ts +0 -152
  106. 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, X } from 'lucide-react';
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
- return href ? `<a href="${href}">${inner}</a>` : inner;
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 popover ──
197
+ // ── Settings dialog ──
191
198
 
192
- function SettingsPopover({
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 createPortal(
206
- <div className="fixed inset-0 z-50" onClick={onClose}>
207
- <div
208
- className="absolute bg-[#1c2733] border border-[#2b3945] rounded-lg shadow-xl p-3 space-y-2 w-64"
209
- style={{ top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}
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
- </span>
216
- <button type="button" onClick={onClose} className="text-[#6c7883] hover:text-[#e1e3e6]">
217
- <X className="h-3.5 w-3.5" />
218
- </button>
219
- </div>
220
-
221
- {/* Type-specific settings */}
222
- {type === 'brahman.action.message' && (
223
- <>
224
- <SettingSelect label="Menu" value={(node.menuType as string) ?? 'none'}
225
- options={MENU_TYPES.map(m => ({ value: m.value, label: m.label }))}
226
- onChange={v => onUpdate({ menuType: v })} />
227
- <SettingCheckbox label="Disable links" checked={!!node.disableLinks}
228
- onChange={v => onUpdate({ disableLinks: v })} />
229
- </>
230
- )}
231
-
232
- {type === 'brahman.action.question' && (
233
- <>
234
- <SettingSelect label="Input type" value={(node.inputType as string) ?? 'text'}
235
- options={[{ value: 'text', label: 'Text' }, { value: 'photo', label: 'Photo' }]}
236
- onChange={v => onUpdate({ inputType: v })} />
237
- <SettingInput label="Save to" value={(node.saveTo as string) ?? ''}
238
- placeholder="session.field" onChange={v => onUpdate({ saveTo: v })} />
239
- </>
240
- )}
241
-
242
- {type === 'brahman.action.file' && (
243
- <SettingSelect label="Send as" value={(node.asType as string) ?? ''}
244
- options={[
245
- { value: '', label: 'Auto' }, { value: 'photo', label: 'Photo' },
246
- { value: 'document', label: 'Document' }, { value: 'video', label: 'Video' },
247
- { value: 'audio', label: 'Audio' }, { value: 'voice', label: 'Voice' },
248
- ]}
249
- onChange={v => onUpdate({ asType: v })} />
250
- )}
251
-
252
- <div className="pt-1 border-t border-[#2b3945]">
253
- <button
254
- type="button"
255
- onClick={onDelete}
256
- className="flex items-center gap-1.5 text-xs text-red-400 hover:text-red-300 py-1"
257
- >
258
- <Trash2 className="h-3 w-3" /> Delete action
259
- </button>
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
- </div>
262
- </div>,
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-[#6c7883]">{label}</span>
273
- <select
274
- className="bg-[#0e1621] border border-[#2b3945] rounded text-xs text-[#e1e3e6] px-2 py-1"
275
- value={value} onChange={e => onChange(e.target.value)}
276
- >
277
- {options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
278
- </select>
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-[#6c7883]">{label}</span>
289
- <input type="checkbox" checked={checked} onChange={e => onChange(e.target.checked)}
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-[#6c7883] shrink-0">{label}</span>
301
- <input
302
- className="bg-[#0e1621] border border-[#2b3945] rounded text-xs text-[#e1e3e6] px-2 py-1 w-32 font-mono"
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 modal */}
393
+ {/* Button edit dialog */}
391
394
  {editBtn && rows[editBtn.ri]?.buttons[editBtn.bi] && (
392
- <ButtonEditPopover
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 ButtonEditPopover({
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 createPortal(
417
- <div className="fixed inset-0 z-50" onClick={onClose}>
418
- <div
419
- className="absolute bg-[#1c2733] border border-[#2b3945] rounded-lg shadow-xl p-3 space-y-2 w-72"
420
- style={{ top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}
421
- onClick={e => e.stopPropagation()}
422
- >
423
- <div className="flex items-center justify-between mb-1">
424
- <span className="text-xs font-medium text-[#e1e3e6]">Edit button</span>
425
- <button type="button" onClick={onClose} className="text-[#6c7883] hover:text-[#e1e3e6]">
426
- <X className="h-3.5 w-3.5" />
427
- </button>
428
- </div>
429
-
430
- <div>
431
- <span className="text-[11px] text-[#6c7883]">Title</span>
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
- <div className="flex items-center gap-2">
440
- <span className="text-[11px] text-[#6c7883] shrink-0">URL</span>
441
- <input
442
- className="flex-1 bg-[#0e1621] border border-[#2b3945] rounded text-xs text-[#e1e3e6] px-2 py-1"
443
- placeholder="https://..."
444
- value={draft.url ?? ''}
445
- onChange={e => setDraft(d => ({ ...d, url: e.target.value || undefined }))}
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
- <div className="flex justify-between pt-1 border-t border-[#2b3945]">
450
- <button type="button" onClick={onDelete}
451
- className="text-xs text-red-400 hover:text-red-300 flex items-center gap-1">
452
- <Trash2 className="h-3 w-3" /> Delete
453
- </button>
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
- <button type="button" onClick={onClose}
456
- className="text-xs text-[#6c7883] hover:text-[#e1e3e6] px-2 py-1">Cancel</button>
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
- </div>
461
- </div>
462
- </div>,
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
- <SettingsPopover
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-[#1c2733] border border-[#2b3945] rounded-lg shadow-xl py-1 max-h-64 overflow-y-auto">
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 text-[#e1e3e6] hover:bg-[#2b3945] flex items-center gap-2"
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
- <Modal open title="Edit Button" onClose={onClose}>
111
- <div className="space-y-3">
112
- <div>
113
- <label className="text-xs font-medium text-muted-foreground">Title</label>
114
- <TStringLineInput value={draft.title} onChange={t => update('title', t)} langs={langs} />
115
- </div>
116
-
117
- <div>
118
- <label className="text-xs font-medium text-muted-foreground">URL (optional)</label>
119
- <Input className="h-8 text-sm" placeholder="https://..."
120
- value={draft.url ?? ''} onChange={e => update('url', e.target.value || undefined)} />
121
- </div>
122
-
123
- <div>
124
- <label className="text-xs font-medium text-muted-foreground">Tags</label>
125
- <TagEditor tags={draft.tags ?? []} onChange={t => update('tags', t)} />
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
- <div>
129
- <label className="text-xs font-medium text-muted-foreground">Button action</label>
130
- <select
131
- className="w-full h-8 text-sm border border-border rounded-md bg-background px-2"
132
- value={draft.action?.type ?? ''}
133
- onChange={e => {
134
- if (!e.target.value) { update('action', undefined); return; }
135
- update('action', { type: e.target.value });
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
- <select
358
- className="flex-1 h-8 text-sm border border-border rounded-md bg-background px-2"
359
- value={menuType}
360
- onChange={e => onChangeType(e.target.value as MenuType)}
361
- >
362
- {MENU_TYPES.map(mt => (
363
- <option key={mt.value} value={mt.value}>{mt.label}</option>
364
- ))}
365
- </select>
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
- <button
413
- type="button"
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
- </button>
405
+ </Button>
420
406
  </div>
421
407
  </div>
422
408
  ))}
@@ -112,22 +112,24 @@ function SortableAction({
112
112
  )}
113
113
  </div>
114
114
 
115
- <button
116
- type="button"
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
  &#8599;
122
- </button>
123
+ </Button>
123
124
 
124
- <button
125
- type="button"
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
- </button>
132
+ </Button>
131
133
  </div>
132
134
 
133
135
  {/* Inline editor */}