@toolr/ui-design 0.1.0
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/README.md +63 -0
- package/components/content/info-panel-primitives.tsx +297 -0
- package/components/diagrams/diagram-utils.tsx +908 -0
- package/components/hooks/use-click-outside.ts +27 -0
- package/components/hooks/use-dropdown-max-height.ts +20 -0
- package/components/hooks/use-navigation-history.ts +94 -0
- package/components/lib/ai-tools.tsx +44 -0
- package/components/lib/cn.ts +6 -0
- package/components/lib/form-colors.ts +32 -0
- package/components/lib/theme-engine.ts +97 -0
- package/components/lib/toolr-brand.tsx +31 -0
- package/components/sections/ai-tools-paths/index.ts +37 -0
- package/components/sections/ai-tools-paths/tools-paths-panel.tsx +212 -0
- package/components/sections/ai-tools-paths/types.ts +111 -0
- package/components/sections/ai-tools-paths/use-tools-paths.ts +159 -0
- package/components/sections/captured-issues/captured-issues-panel.tsx +214 -0
- package/components/sections/captured-issues/index.ts +38 -0
- package/components/sections/captured-issues/types.ts +113 -0
- package/components/sections/captured-issues/use-captured-issues.ts +111 -0
- package/components/sections/golden-snapshots/file-diff-viewer.tsx +420 -0
- package/components/sections/golden-snapshots/golden-sync-panel.tsx +223 -0
- package/components/sections/golden-snapshots/index.ts +145 -0
- package/components/sections/golden-snapshots/snapshot-manager.tsx +200 -0
- package/components/sections/golden-snapshots/status-overview.tsx +305 -0
- package/components/sections/golden-snapshots/types.ts +288 -0
- package/components/sections/golden-snapshots/use-golden-sync.ts +477 -0
- package/components/sections/golden-snapshots/version-manager.tsx +186 -0
- package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +210 -0
- package/components/sections/prompt-editor/index.ts +121 -0
- package/components/sections/prompt-editor/simulator-prompt-editor.tsx +276 -0
- package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +514 -0
- package/components/sections/prompt-editor/types.ts +101 -0
- package/components/sections/prompt-editor/use-prompt-editor.ts +131 -0
- package/components/sections/report-bug/error-logger.ts +392 -0
- package/components/sections/report-bug/index.ts +59 -0
- package/components/sections/report-bug/issue-reporter-api.ts +83 -0
- package/components/sections/report-bug/report-bug-form.tsx +282 -0
- package/components/sections/report-bug/screenshot-uploader.tsx +228 -0
- package/components/sections/report-bug/use-report-bug.ts +170 -0
- package/components/sections/snapshot-browser/index.ts +53 -0
- package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +147 -0
- package/components/sections/snapshot-browser/snapshot-tree.tsx +451 -0
- package/components/sections/snapshot-browser/types.ts +106 -0
- package/components/sections/snapshot-browser/use-snapshot-browser.ts +125 -0
- package/components/sections/snippets-editor/index.ts +31 -0
- package/components/sections/snippets-editor/snippets-editor.tsx +381 -0
- package/components/sections/snippets-editor/types.ts +48 -0
- package/components/sections/snippets-editor/use-snippets-editor.ts +217 -0
- package/components/ui/action-dialog.tsx +309 -0
- package/components/ui/ai-action-button.tsx +137 -0
- package/components/ui/ai-execution-action-buttons.tsx +106 -0
- package/components/ui/badge.tsx +67 -0
- package/components/ui/bottom-panel-header.tsx +240 -0
- package/components/ui/breadcrumb.tsx +168 -0
- package/components/ui/checkbox.tsx +102 -0
- package/components/ui/collapsible-section.tsx +100 -0
- package/components/ui/confirm-badge.tsx +71 -0
- package/components/ui/detail-section.tsx +67 -0
- package/components/ui/detail-view-wrapper.tsx +55 -0
- package/components/ui/editor-placeholder-card.tsx +197 -0
- package/components/ui/editor-toolbar.tsx +123 -0
- package/components/ui/execution-details-panel.tsx +93 -0
- package/components/ui/extension-list-card.tsx +105 -0
- package/components/ui/file-structure-section.tsx +373 -0
- package/components/ui/file-tree.tsx +171 -0
- package/components/ui/files-panel.tsx +251 -0
- package/components/ui/filter-dropdown.tsx +173 -0
- package/components/ui/form-actions.tsx +127 -0
- package/components/ui/frontmatter-form-header.tsx +80 -0
- package/components/ui/icon-button.tsx +388 -0
- package/components/ui/input.tsx +211 -0
- package/components/ui/label.tsx +159 -0
- package/components/ui/layout-tab-bar.tsx +289 -0
- package/components/ui/modal.tsx +194 -0
- package/components/ui/nav-card.tsx +81 -0
- package/components/ui/navigation-bar.tsx +285 -0
- package/components/ui/number-input.tsx +165 -0
- package/components/ui/registry-browser.tsx +261 -0
- package/components/ui/registry-card.tsx +710 -0
- package/components/ui/registry-detail.tsx +224 -0
- package/components/ui/resizable-textarea.tsx +290 -0
- package/components/ui/scope-badge.tsx +67 -0
- package/components/ui/segmented-toggle.tsx +133 -0
- package/components/ui/select.tsx +172 -0
- package/components/ui/selection-grid.tsx +313 -0
- package/components/ui/setting-row.tsx +97 -0
- package/components/ui/snapshot-card.tsx +107 -0
- package/components/ui/snippets-panel.tsx +161 -0
- package/components/ui/sort-dropdown.tsx +109 -0
- package/components/ui/status-card.tsx +96 -0
- package/components/ui/tab-bar.tsx +340 -0
- package/components/ui/toggle.tsx +142 -0
- package/components/ui/tooltip.tsx +326 -0
- package/dist/content.d.ts +110 -0
- package/dist/content.js +195 -0
- package/dist/diagrams.d.ts +371 -0
- package/dist/diagrams.js +702 -0
- package/dist/index.d.ts +2714 -0
- package/dist/index.js +11220 -0
- package/dist/preset.d.ts +24 -0
- package/dist/preset.js +17 -0
- package/dist/tokens/tokens/primitives.css +45 -0
- package/dist/tokens/tokens/semantic.css +46 -0
- package/dist/tokens/tokens/theme.css +11 -0
- package/dist/tokens/tokens/tokens.json +65 -0
- package/index.ts +123 -0
- package/package.json +63 -0
- package/tailwind-preset.ts +22 -0
- package/tokens/primitives.css +45 -0
- package/tokens/semantic.css +46 -0
- package/tokens/theme.css +11 -0
- package/tokens/tokens.json +65 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
ChevronRight, Settings, Folder, Code, Terminal, Database,
|
|
4
|
+
Star, Heart, Bell, Bookmark, Tag, Pin, Mail, Globe, Cloud,
|
|
5
|
+
Shield, Zap, Sparkles, Search, Filter, Eye, Lock, User, Users,
|
|
6
|
+
File, Image, Download, Upload, Play, Pause,
|
|
7
|
+
} from 'lucide-react'
|
|
8
|
+
import type { LucideIcon } from 'lucide-react'
|
|
9
|
+
import type { IconName } from './icon-button.tsx'
|
|
10
|
+
import { Badge, type BadgeColor } from './badge.tsx'
|
|
11
|
+
import { cn } from '../lib/cn.ts'
|
|
12
|
+
|
|
13
|
+
const iconSubset: Partial<Record<IconName, LucideIcon>> = {
|
|
14
|
+
settings: Settings,
|
|
15
|
+
folder: Folder,
|
|
16
|
+
code: Code,
|
|
17
|
+
terminal: Terminal,
|
|
18
|
+
database: Database,
|
|
19
|
+
star: Star,
|
|
20
|
+
heart: Heart,
|
|
21
|
+
bell: Bell,
|
|
22
|
+
bookmark: Bookmark,
|
|
23
|
+
tag: Tag,
|
|
24
|
+
pin: Pin,
|
|
25
|
+
mail: Mail,
|
|
26
|
+
globe: Globe,
|
|
27
|
+
cloud: Cloud,
|
|
28
|
+
shield: Shield,
|
|
29
|
+
zap: Zap,
|
|
30
|
+
sparkles: Sparkles,
|
|
31
|
+
search: Search,
|
|
32
|
+
filter: Filter,
|
|
33
|
+
eye: Eye,
|
|
34
|
+
lock: Lock,
|
|
35
|
+
user: User,
|
|
36
|
+
users: Users,
|
|
37
|
+
file: File,
|
|
38
|
+
image: Image,
|
|
39
|
+
download: Download,
|
|
40
|
+
upload: Upload,
|
|
41
|
+
play: Play,
|
|
42
|
+
pause: Pause,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CollapsibleSectionProps {
|
|
46
|
+
title: string
|
|
47
|
+
icon?: IconName
|
|
48
|
+
iconColor?: string
|
|
49
|
+
defaultOpen?: boolean
|
|
50
|
+
badge?: string | number
|
|
51
|
+
badgeColor?: BadgeColor
|
|
52
|
+
children: React.ReactNode
|
|
53
|
+
className?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function CollapsibleSection({
|
|
57
|
+
title,
|
|
58
|
+
icon,
|
|
59
|
+
iconColor,
|
|
60
|
+
defaultOpen = false,
|
|
61
|
+
badge,
|
|
62
|
+
badgeColor,
|
|
63
|
+
children,
|
|
64
|
+
className,
|
|
65
|
+
}: CollapsibleSectionProps) {
|
|
66
|
+
const [open, setOpen] = useState(defaultOpen)
|
|
67
|
+
const Icon = icon ? iconSubset[icon] : undefined
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className={cn('border-b border-neutral-700', className)}>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
onClick={() => setOpen(!open)}
|
|
74
|
+
className="flex w-full items-center gap-2 py-2.5 px-1 text-left hover:bg-neutral-700/30 transition-colors cursor-pointer"
|
|
75
|
+
>
|
|
76
|
+
<ChevronRight
|
|
77
|
+
className={cn(
|
|
78
|
+
'w-3.5 h-3.5 text-neutral-500 transition-transform duration-150',
|
|
79
|
+
open && 'rotate-90',
|
|
80
|
+
)}
|
|
81
|
+
/>
|
|
82
|
+
{Icon && (
|
|
83
|
+
<span
|
|
84
|
+
className="flex items-center justify-center w-5 h-5 rounded bg-neutral-700/60"
|
|
85
|
+
style={iconColor ? { color: iconColor } : undefined}
|
|
86
|
+
>
|
|
87
|
+
<Icon className="w-3.5 h-3.5" />
|
|
88
|
+
</span>
|
|
89
|
+
)}
|
|
90
|
+
<span className="text-sm font-medium text-neutral-200">{title}</span>
|
|
91
|
+
{badge !== undefined && (
|
|
92
|
+
<span className="ml-auto">
|
|
93
|
+
<Badge value={badge} color={badgeColor} size="xss" />
|
|
94
|
+
</span>
|
|
95
|
+
)}
|
|
96
|
+
</button>
|
|
97
|
+
{open && <div className="pb-3 pl-7 pr-2">{children}</div>}
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfirmBadge - Outline-styled check badge for indicating selection/confirmation
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - SelectionGrid - selected state indicator on cards
|
|
6
|
+
* - Any UI that needs a small check/confirmed indicator
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Outline variant matching IconButton outline style (border + text, no fill)
|
|
10
|
+
* - 13 color variants
|
|
11
|
+
* - 5 size variants (xss, xs, sm, md, lg)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Check } from 'lucide-react'
|
|
15
|
+
|
|
16
|
+
export type ConfirmBadgeColor = 'blue' | 'green' | 'red' | 'orange' | 'cyan' | 'yellow' | 'purple' | 'indigo' | 'emerald' | 'amber' | 'violet' | 'neutral' | 'sky'
|
|
17
|
+
|
|
18
|
+
export interface ConfirmBadgeProps {
|
|
19
|
+
color?: ConfirmBadgeColor
|
|
20
|
+
size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
|
|
21
|
+
className?: string
|
|
22
|
+
testId?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const colorClasses: Record<ConfirmBadgeColor, string> = {
|
|
26
|
+
green: 'border-green-500/30 text-green-400',
|
|
27
|
+
red: 'border-red-500/30 text-red-400',
|
|
28
|
+
blue: 'border-blue-500/30 text-blue-400',
|
|
29
|
+
orange: 'border-orange-500/30 text-orange-400',
|
|
30
|
+
cyan: 'border-cyan-500/30 text-cyan-400',
|
|
31
|
+
yellow: 'border-yellow-500/30 text-yellow-400',
|
|
32
|
+
purple: 'border-purple-500/30 text-purple-400',
|
|
33
|
+
indigo: 'border-indigo-500/30 text-indigo-400',
|
|
34
|
+
emerald: 'border-emerald-500/30 text-emerald-400',
|
|
35
|
+
amber: 'border-amber-500/30 text-amber-400',
|
|
36
|
+
violet: 'border-violet-500/30 text-violet-400',
|
|
37
|
+
neutral: 'border-neutral-500/30 text-neutral-400',
|
|
38
|
+
sky: 'border-sky-500/30 text-sky-400',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sizeClasses = {
|
|
42
|
+
xss: 'w-[14px] h-[14px] rounded-full',
|
|
43
|
+
xs: 'w-[16px] h-[16px] rounded-full',
|
|
44
|
+
sm: 'w-[18px] h-[18px] rounded-full',
|
|
45
|
+
md: 'w-[20px] h-[20px] rounded-full',
|
|
46
|
+
lg: 'w-[22px] h-[22px] rounded-full',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const iconSizeClasses = {
|
|
50
|
+
xss: 'w-2 h-2',
|
|
51
|
+
xs: 'w-2.5 h-2.5',
|
|
52
|
+
sm: 'w-3 h-3',
|
|
53
|
+
md: 'w-3.5 h-3.5',
|
|
54
|
+
lg: 'w-4 h-4',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function ConfirmBadge({
|
|
58
|
+
color = 'neutral',
|
|
59
|
+
size = 'sm',
|
|
60
|
+
className = '',
|
|
61
|
+
testId,
|
|
62
|
+
}: ConfirmBadgeProps) {
|
|
63
|
+
return (
|
|
64
|
+
<span
|
|
65
|
+
data-testid={testId}
|
|
66
|
+
className={`inline-flex items-center justify-center border ${colorClasses[color]} ${sizeClasses[size]} ${className}`}
|
|
67
|
+
>
|
|
68
|
+
<Check className={iconSizeClasses[size]} />
|
|
69
|
+
</span>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DetailSection - Labeled key-value detail rows with optional section header.
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - Execution detail panels (tool info, permissions, output format)
|
|
6
|
+
* - Server/service info displays
|
|
7
|
+
* - Any key-value metadata layout
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
Info, Settings, Code, Shield, Terminal, Database, Globe, Zap,
|
|
12
|
+
Star, Cloud, Bell, Heart, Sparkles, Bot, Plug, Lock, Eye,
|
|
13
|
+
File, Folder, User, Users, Tag, Bookmark, Mail, Send, Search,
|
|
14
|
+
Play, ShieldCheck, Wand2, Copy,
|
|
15
|
+
} from 'lucide-react'
|
|
16
|
+
import type { LucideIcon } from 'lucide-react'
|
|
17
|
+
import type { IconName } from './icon-button.tsx'
|
|
18
|
+
import { cn } from '../lib/cn.ts'
|
|
19
|
+
|
|
20
|
+
const iconSubset: Partial<Record<IconName, LucideIcon>> = {
|
|
21
|
+
info: Info, settings: Settings, code: Code, shield: Shield,
|
|
22
|
+
terminal: Terminal, database: Database, globe: Globe, zap: Zap,
|
|
23
|
+
star: Star, cloud: Cloud, bell: Bell, heart: Heart,
|
|
24
|
+
sparkles: Sparkles, bot: Bot, plug: Plug, lock: Lock, eye: Eye,
|
|
25
|
+
file: File, folder: Folder, user: User, users: Users, tag: Tag,
|
|
26
|
+
bookmark: Bookmark, mail: Mail, send: Send, search: Search,
|
|
27
|
+
play: Play, 'shield-check': ShieldCheck, wand: Wand2, copy: Copy,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DetailRow {
|
|
31
|
+
label: string
|
|
32
|
+
value: string
|
|
33
|
+
mono?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DetailSectionProps {
|
|
37
|
+
/** Section title (e.g., "Execution Details") */
|
|
38
|
+
title: string
|
|
39
|
+
/** Icon before the title */
|
|
40
|
+
icon?: IconName
|
|
41
|
+
/** Detail rows to display */
|
|
42
|
+
rows: DetailRow[]
|
|
43
|
+
className?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function DetailSection({ title, icon, rows, className }: DetailSectionProps) {
|
|
47
|
+
const Icon = icon ? iconSubset[icon] : undefined
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className={className}>
|
|
51
|
+
<div className="flex items-center gap-2 mb-3">
|
|
52
|
+
{Icon && <Icon className="w-4 h-4 text-neutral-500" />}
|
|
53
|
+
<span className="text-sm font-medium text-neutral-400">{title}</span>
|
|
54
|
+
</div>
|
|
55
|
+
<div className="space-y-2">
|
|
56
|
+
{rows.map((row) => (
|
|
57
|
+
<div key={row.label} className="flex items-start gap-3">
|
|
58
|
+
<span className="text-xs text-neutral-500 w-24 shrink-0">{row.label}:</span>
|
|
59
|
+
<span className={cn('text-xs text-neutral-400', row.mono && 'font-mono')}>
|
|
60
|
+
{row.value}
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface DetailViewWrapperProps {
|
|
4
|
+
/** Main editor content */
|
|
5
|
+
editorContent?: ReactNode
|
|
6
|
+
|
|
7
|
+
/** Action buttons rendered in navigation bar */
|
|
8
|
+
actions?: ReactNode
|
|
9
|
+
|
|
10
|
+
/** Optional bottom panel */
|
|
11
|
+
bottomPanel?: ReactNode
|
|
12
|
+
/** Optional right sidebar */
|
|
13
|
+
rightSidebar?: ReactNode
|
|
14
|
+
showRightSidebar?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function DetailViewWrapper({
|
|
18
|
+
editorContent,
|
|
19
|
+
actions,
|
|
20
|
+
bottomPanel,
|
|
21
|
+
rightSidebar,
|
|
22
|
+
showRightSidebar = false,
|
|
23
|
+
}: DetailViewWrapperProps) {
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="flex-1 flex flex-col overflow-hidden min-h-0">
|
|
27
|
+
{actions && (
|
|
28
|
+
<div className="flex items-center justify-end border-b border-neutral-800 bg-neutral-950/50 px-2 h-[36px]">
|
|
29
|
+
<div className="flex items-center gap-1">{actions}</div>
|
|
30
|
+
</div>
|
|
31
|
+
)}
|
|
32
|
+
|
|
33
|
+
{/* Main content area */}
|
|
34
|
+
<div className="flex-1 flex overflow-hidden min-h-0">
|
|
35
|
+
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
|
36
|
+
{editorContent && (
|
|
37
|
+
<div className="flex-1 flex flex-col overflow-hidden">{editorContent}</div>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
{/* Optional right sidebar */}
|
|
42
|
+
{showRightSidebar && rightSidebar && (
|
|
43
|
+
<div className="w-[300px] border-l border-neutral-800 overflow-hidden flex-shrink-0">
|
|
44
|
+
{rightSidebar}
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{/* Optional bottom panel */}
|
|
50
|
+
{bottomPanel && (
|
|
51
|
+
<div className="border-t border-neutral-800">{bottomPanel}</div>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { useState, useRef, useLayoutEffect } from 'react'
|
|
2
|
+
import { IconButton, type ActionItem } from './icon-button.tsx'
|
|
3
|
+
import { Input } from './input.tsx'
|
|
4
|
+
|
|
5
|
+
export interface EditorPlaceholderCardProps {
|
|
6
|
+
/** Placeholder name (without braces) */
|
|
7
|
+
name: string
|
|
8
|
+
/** Description text */
|
|
9
|
+
description: string
|
|
10
|
+
/** Value/example content */
|
|
11
|
+
value?: string
|
|
12
|
+
/** Whether this placeholder is required */
|
|
13
|
+
required?: boolean
|
|
14
|
+
/** Label for the value section (default: "Value:") */
|
|
15
|
+
valueLabel?: string
|
|
16
|
+
/** Color scheme */
|
|
17
|
+
accentColor?: 'purple' | 'blue' | 'neutral' | 'sky'
|
|
18
|
+
/** Action buttons to show on the right (e.g., edit/delete for settings) */
|
|
19
|
+
actions?: ActionItem[]
|
|
20
|
+
/** Show copy button that copies the {{PLACEHOLDER}} syntax */
|
|
21
|
+
showCopyPlaceholder?: boolean
|
|
22
|
+
/** Show copy button that copies the actual value */
|
|
23
|
+
showCopyValue?: boolean
|
|
24
|
+
/** Hide the value behind ****** with a toggle to reveal */
|
|
25
|
+
hideValue?: boolean
|
|
26
|
+
/** Additional class names */
|
|
27
|
+
className?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const COLORS = {
|
|
31
|
+
purple: {
|
|
32
|
+
name: 'text-purple-400',
|
|
33
|
+
nameBg: 'bg-purple-500/10',
|
|
34
|
+
},
|
|
35
|
+
blue: {
|
|
36
|
+
name: 'text-blue-400',
|
|
37
|
+
nameBg: 'bg-blue-500/10',
|
|
38
|
+
},
|
|
39
|
+
neutral: {
|
|
40
|
+
name: 'text-neutral-400',
|
|
41
|
+
nameBg: 'bg-neutral-500/10',
|
|
42
|
+
},
|
|
43
|
+
sky: {
|
|
44
|
+
name: 'text-sky-400',
|
|
45
|
+
nameBg: 'bg-sky-500/10',
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function EditorPlaceholderCard({
|
|
50
|
+
name,
|
|
51
|
+
description,
|
|
52
|
+
value,
|
|
53
|
+
required = false,
|
|
54
|
+
valueLabel = 'Value:',
|
|
55
|
+
accentColor = 'purple',
|
|
56
|
+
actions,
|
|
57
|
+
showCopyPlaceholder = false,
|
|
58
|
+
showCopyValue = false,
|
|
59
|
+
hideValue = false,
|
|
60
|
+
className = '',
|
|
61
|
+
}: EditorPlaceholderCardProps) {
|
|
62
|
+
const [isExpanded, setIsExpanded] = useState(false)
|
|
63
|
+
const [isCopied, setIsCopied] = useState(false)
|
|
64
|
+
const [isOverflowing, setIsOverflowing] = useState(false)
|
|
65
|
+
const valueRef = useRef<HTMLDivElement>(null)
|
|
66
|
+
|
|
67
|
+
const colors = COLORS[accentColor]
|
|
68
|
+
const hasValue = !!value
|
|
69
|
+
|
|
70
|
+
// Check if content overflows (truncated) - sync state with DOM measurement
|
|
71
|
+
useLayoutEffect(() => {
|
|
72
|
+
if (valueRef.current && !isExpanded) {
|
|
73
|
+
const { scrollWidth, clientWidth, scrollHeight, clientHeight } = valueRef.current
|
|
74
|
+
setIsOverflowing(scrollWidth > clientWidth || scrollHeight > clientHeight)
|
|
75
|
+
}
|
|
76
|
+
}, [value, isExpanded])
|
|
77
|
+
|
|
78
|
+
const handleCopyPlaceholder = async () => {
|
|
79
|
+
try {
|
|
80
|
+
await navigator.clipboard.writeText(`{{${name}}}`)
|
|
81
|
+
setIsCopied(true)
|
|
82
|
+
setTimeout(() => setIsCopied(false), 1500)
|
|
83
|
+
} catch {
|
|
84
|
+
// Clipboard API not available
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handleCopyValue = async () => {
|
|
89
|
+
if (!value) return
|
|
90
|
+
try {
|
|
91
|
+
await navigator.clipboard.writeText(value)
|
|
92
|
+
setIsCopied(true)
|
|
93
|
+
setTimeout(() => setIsCopied(false), 1500)
|
|
94
|
+
} catch {
|
|
95
|
+
// Clipboard API not available
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className={`group px-3 py-2.5 ${!hasValue ? 'opacity-60' : ''} ${className}`}>
|
|
101
|
+
{/* Header: Name + Actions */}
|
|
102
|
+
<div className="flex items-start justify-between gap-2">
|
|
103
|
+
<div className="flex-1 min-w-0">
|
|
104
|
+
{/* Placeholder name with {{ }} */}
|
|
105
|
+
<code className={`text-xs font-mono px-1.5 py-0.5 rounded ${colors.name} ${colors.nameBg}`}>
|
|
106
|
+
{'{{' + name + '}}'}
|
|
107
|
+
</code>
|
|
108
|
+
{/* Required badge */}
|
|
109
|
+
{required && (
|
|
110
|
+
<span className="ml-2 inline-block px-1.5 py-0.5 text-xs font-semibold uppercase bg-red-500/15 text-red-400 border border-red-500/30 rounded">
|
|
111
|
+
Required
|
|
112
|
+
</span>
|
|
113
|
+
)}
|
|
114
|
+
{/* Description */}
|
|
115
|
+
<p className="text-xs text-neutral-500 mt-1.5 line-clamp-2">{description}</p>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Actions (copy for templates, edit/delete for settings) */}
|
|
119
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
120
|
+
{showCopyPlaceholder && (
|
|
121
|
+
<IconButton
|
|
122
|
+
icon={isCopied ? 'check' : 'copy'}
|
|
123
|
+
onClick={handleCopyPlaceholder}
|
|
124
|
+
size="sm"
|
|
125
|
+
color={isCopied ? 'green' : 'neutral'}
|
|
126
|
+
tooltip={{ description: `Copy {{${name}}}` }}
|
|
127
|
+
tooltipPosition="left"
|
|
128
|
+
/>
|
|
129
|
+
)}
|
|
130
|
+
{showCopyValue && hasValue && (
|
|
131
|
+
<IconButton
|
|
132
|
+
icon={isCopied ? 'check' : 'copy'}
|
|
133
|
+
onClick={handleCopyValue}
|
|
134
|
+
size="sm"
|
|
135
|
+
color={isCopied ? 'green' : 'neutral'}
|
|
136
|
+
tooltip={{ description: 'Copy value to clipboard' }}
|
|
137
|
+
tooltipPosition="left"
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
{actions?.map((a, i) => <IconButton key={i} {...a} />)}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Value section */}
|
|
145
|
+
{hasValue && (
|
|
146
|
+
<div className="mt-2">
|
|
147
|
+
{hideValue ? (
|
|
148
|
+
<>
|
|
149
|
+
<span className="text-xs text-neutral-500 font-medium">{valueLabel}</span>
|
|
150
|
+
<div className="mt-1.5">
|
|
151
|
+
<Input
|
|
152
|
+
type="password"
|
|
153
|
+
value={value}
|
|
154
|
+
onChange={() => {}}
|
|
155
|
+
readOnly
|
|
156
|
+
size="xs"
|
|
157
|
+
variant="filled"
|
|
158
|
+
color="neutral"
|
|
159
|
+
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
</>
|
|
163
|
+
) : (
|
|
164
|
+
<>
|
|
165
|
+
<div className="flex items-center justify-between">
|
|
166
|
+
<span className="text-xs text-neutral-500 font-medium">{valueLabel}</span>
|
|
167
|
+
{(isOverflowing || isExpanded) && (
|
|
168
|
+
<IconButton
|
|
169
|
+
icon={isExpanded ? 'chevron-up' : 'chevron-down'}
|
|
170
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
171
|
+
size="xss"
|
|
172
|
+
tooltip={{ description: isExpanded ? 'Show less' : 'Show more' }}
|
|
173
|
+
/>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
<div
|
|
177
|
+
ref={valueRef}
|
|
178
|
+
className={`mt-1.5 px-2 py-1.5 bg-neutral-800/50 rounded text-xs text-neutral-400 font-mono ${
|
|
179
|
+
isExpanded
|
|
180
|
+
? 'whitespace-pre-wrap break-all max-h-[190px] overflow-y-auto'
|
|
181
|
+
: 'truncate'
|
|
182
|
+
}`}
|
|
183
|
+
>
|
|
184
|
+
{value}
|
|
185
|
+
</div>
|
|
186
|
+
</>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{/* No value hint */}
|
|
192
|
+
{!hasValue && (
|
|
193
|
+
<p className="mt-1.5 text-xs text-neutral-600 italic">No value set - add one in Settings</p>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { IconButton, type ActionItem } from './icon-button.tsx'
|
|
3
|
+
import { Label } from './label.tsx'
|
|
4
|
+
import { ConfirmModal } from './modal.tsx'
|
|
5
|
+
|
|
6
|
+
export interface EditorToolbarProps {
|
|
7
|
+
/** Whether content has unsaved changes */
|
|
8
|
+
isDirty: boolean
|
|
9
|
+
/** Whether save operation is in progress */
|
|
10
|
+
isSaving?: boolean
|
|
11
|
+
/** Called when save button is clicked */
|
|
12
|
+
onSave: () => void
|
|
13
|
+
/** Optional: Whether reset is available */
|
|
14
|
+
canReset?: boolean
|
|
15
|
+
/** Optional: Called when reset button is clicked */
|
|
16
|
+
onReset?: () => void
|
|
17
|
+
/** Optional: Tooltip for reset button */
|
|
18
|
+
resetTooltip?: {
|
|
19
|
+
title: string
|
|
20
|
+
description: string
|
|
21
|
+
}
|
|
22
|
+
/** Optional: Whether editor is read-only */
|
|
23
|
+
isReadOnly?: boolean
|
|
24
|
+
/** Optional: Validation error preventing save */
|
|
25
|
+
hasError?: boolean
|
|
26
|
+
/** Optional: Additional action buttons on the left side */
|
|
27
|
+
leftActions?: ActionItem[]
|
|
28
|
+
/** Optional: Additional action buttons on the right side (before save) */
|
|
29
|
+
rightActions?: ActionItem[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function EditorToolbar({
|
|
33
|
+
isDirty,
|
|
34
|
+
isSaving = false,
|
|
35
|
+
onSave,
|
|
36
|
+
canReset = false,
|
|
37
|
+
onReset,
|
|
38
|
+
resetTooltip = {
|
|
39
|
+
title: 'Reset to Default',
|
|
40
|
+
description: 'Restore the default content',
|
|
41
|
+
},
|
|
42
|
+
isReadOnly = false,
|
|
43
|
+
hasError = false,
|
|
44
|
+
leftActions,
|
|
45
|
+
rightActions,
|
|
46
|
+
}: EditorToolbarProps) {
|
|
47
|
+
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
|
|
48
|
+
|
|
49
|
+
const showSaveButton = !isReadOnly
|
|
50
|
+
|
|
51
|
+
const handleResetClick = () => {
|
|
52
|
+
setShowConfirmDialog(true)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const handleConfirm = () => {
|
|
56
|
+
setShowConfirmDialog(false)
|
|
57
|
+
onReset?.()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!showSaveButton && !canReset && !leftActions?.length && !rightActions?.length) {
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
<div className="flex items-center justify-between px-4 py-1.5 bg-neutral-900 border-b border-neutral-800">
|
|
67
|
+
{/* Left side */}
|
|
68
|
+
<div className="flex items-center gap-2">
|
|
69
|
+
{leftActions?.map((a, i) => <IconButton key={i} {...a} />)}
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{/* Center - unsaved indicator */}
|
|
73
|
+
{isDirty && (
|
|
74
|
+
<Label
|
|
75
|
+
text="modified"
|
|
76
|
+
color="yellow"
|
|
77
|
+
icon="pencil"
|
|
78
|
+
size="xs"
|
|
79
|
+
tooltip={{ description: 'File has unsaved changes' }}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{/* Right side */}
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
{rightActions?.map((a, i) => <IconButton key={i} {...a} />)}
|
|
86
|
+
|
|
87
|
+
{canReset && onReset && (
|
|
88
|
+
<IconButton
|
|
89
|
+
icon="rotate"
|
|
90
|
+
onClick={handleResetClick}
|
|
91
|
+
size="sm"
|
|
92
|
+
color="orange"
|
|
93
|
+
tooltip={resetTooltip}
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{showSaveButton && (
|
|
98
|
+
<IconButton
|
|
99
|
+
icon="save"
|
|
100
|
+
onClick={onSave}
|
|
101
|
+
disabled={!isDirty || isSaving || hasError}
|
|
102
|
+
size="sm"
|
|
103
|
+
color={isDirty && !hasError ? 'amber' : 'neutral'}
|
|
104
|
+
status={isSaving ? 'loading' : undefined}
|
|
105
|
+
tooltip={{ description: 'Save changes to file' }}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Confirmation Dialog */}
|
|
112
|
+
<ConfirmModal
|
|
113
|
+
isOpen={showConfirmDialog}
|
|
114
|
+
onClose={() => setShowConfirmDialog(false)}
|
|
115
|
+
onConfirm={handleConfirm}
|
|
116
|
+
title={resetTooltip.title}
|
|
117
|
+
message={`${resetTooltip.description} Your custom configuration will be lost.`}
|
|
118
|
+
kind="orange"
|
|
119
|
+
confirmColor="orange"
|
|
120
|
+
/>
|
|
121
|
+
</>
|
|
122
|
+
)
|
|
123
|
+
}
|