@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.
Files changed (112) hide show
  1. package/README.md +63 -0
  2. package/components/content/info-panel-primitives.tsx +297 -0
  3. package/components/diagrams/diagram-utils.tsx +908 -0
  4. package/components/hooks/use-click-outside.ts +27 -0
  5. package/components/hooks/use-dropdown-max-height.ts +20 -0
  6. package/components/hooks/use-navigation-history.ts +94 -0
  7. package/components/lib/ai-tools.tsx +44 -0
  8. package/components/lib/cn.ts +6 -0
  9. package/components/lib/form-colors.ts +32 -0
  10. package/components/lib/theme-engine.ts +97 -0
  11. package/components/lib/toolr-brand.tsx +31 -0
  12. package/components/sections/ai-tools-paths/index.ts +37 -0
  13. package/components/sections/ai-tools-paths/tools-paths-panel.tsx +212 -0
  14. package/components/sections/ai-tools-paths/types.ts +111 -0
  15. package/components/sections/ai-tools-paths/use-tools-paths.ts +159 -0
  16. package/components/sections/captured-issues/captured-issues-panel.tsx +214 -0
  17. package/components/sections/captured-issues/index.ts +38 -0
  18. package/components/sections/captured-issues/types.ts +113 -0
  19. package/components/sections/captured-issues/use-captured-issues.ts +111 -0
  20. package/components/sections/golden-snapshots/file-diff-viewer.tsx +420 -0
  21. package/components/sections/golden-snapshots/golden-sync-panel.tsx +223 -0
  22. package/components/sections/golden-snapshots/index.ts +145 -0
  23. package/components/sections/golden-snapshots/snapshot-manager.tsx +200 -0
  24. package/components/sections/golden-snapshots/status-overview.tsx +305 -0
  25. package/components/sections/golden-snapshots/types.ts +288 -0
  26. package/components/sections/golden-snapshots/use-golden-sync.ts +477 -0
  27. package/components/sections/golden-snapshots/version-manager.tsx +186 -0
  28. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +210 -0
  29. package/components/sections/prompt-editor/index.ts +121 -0
  30. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +276 -0
  31. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +514 -0
  32. package/components/sections/prompt-editor/types.ts +101 -0
  33. package/components/sections/prompt-editor/use-prompt-editor.ts +131 -0
  34. package/components/sections/report-bug/error-logger.ts +392 -0
  35. package/components/sections/report-bug/index.ts +59 -0
  36. package/components/sections/report-bug/issue-reporter-api.ts +83 -0
  37. package/components/sections/report-bug/report-bug-form.tsx +282 -0
  38. package/components/sections/report-bug/screenshot-uploader.tsx +228 -0
  39. package/components/sections/report-bug/use-report-bug.ts +170 -0
  40. package/components/sections/snapshot-browser/index.ts +53 -0
  41. package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +147 -0
  42. package/components/sections/snapshot-browser/snapshot-tree.tsx +451 -0
  43. package/components/sections/snapshot-browser/types.ts +106 -0
  44. package/components/sections/snapshot-browser/use-snapshot-browser.ts +125 -0
  45. package/components/sections/snippets-editor/index.ts +31 -0
  46. package/components/sections/snippets-editor/snippets-editor.tsx +381 -0
  47. package/components/sections/snippets-editor/types.ts +48 -0
  48. package/components/sections/snippets-editor/use-snippets-editor.ts +217 -0
  49. package/components/ui/action-dialog.tsx +309 -0
  50. package/components/ui/ai-action-button.tsx +137 -0
  51. package/components/ui/ai-execution-action-buttons.tsx +106 -0
  52. package/components/ui/badge.tsx +67 -0
  53. package/components/ui/bottom-panel-header.tsx +240 -0
  54. package/components/ui/breadcrumb.tsx +168 -0
  55. package/components/ui/checkbox.tsx +102 -0
  56. package/components/ui/collapsible-section.tsx +100 -0
  57. package/components/ui/confirm-badge.tsx +71 -0
  58. package/components/ui/detail-section.tsx +67 -0
  59. package/components/ui/detail-view-wrapper.tsx +55 -0
  60. package/components/ui/editor-placeholder-card.tsx +197 -0
  61. package/components/ui/editor-toolbar.tsx +123 -0
  62. package/components/ui/execution-details-panel.tsx +93 -0
  63. package/components/ui/extension-list-card.tsx +105 -0
  64. package/components/ui/file-structure-section.tsx +373 -0
  65. package/components/ui/file-tree.tsx +171 -0
  66. package/components/ui/files-panel.tsx +251 -0
  67. package/components/ui/filter-dropdown.tsx +173 -0
  68. package/components/ui/form-actions.tsx +127 -0
  69. package/components/ui/frontmatter-form-header.tsx +80 -0
  70. package/components/ui/icon-button.tsx +388 -0
  71. package/components/ui/input.tsx +211 -0
  72. package/components/ui/label.tsx +159 -0
  73. package/components/ui/layout-tab-bar.tsx +289 -0
  74. package/components/ui/modal.tsx +194 -0
  75. package/components/ui/nav-card.tsx +81 -0
  76. package/components/ui/navigation-bar.tsx +285 -0
  77. package/components/ui/number-input.tsx +165 -0
  78. package/components/ui/registry-browser.tsx +261 -0
  79. package/components/ui/registry-card.tsx +710 -0
  80. package/components/ui/registry-detail.tsx +224 -0
  81. package/components/ui/resizable-textarea.tsx +290 -0
  82. package/components/ui/scope-badge.tsx +67 -0
  83. package/components/ui/segmented-toggle.tsx +133 -0
  84. package/components/ui/select.tsx +172 -0
  85. package/components/ui/selection-grid.tsx +313 -0
  86. package/components/ui/setting-row.tsx +97 -0
  87. package/components/ui/snapshot-card.tsx +107 -0
  88. package/components/ui/snippets-panel.tsx +161 -0
  89. package/components/ui/sort-dropdown.tsx +109 -0
  90. package/components/ui/status-card.tsx +96 -0
  91. package/components/ui/tab-bar.tsx +340 -0
  92. package/components/ui/toggle.tsx +142 -0
  93. package/components/ui/tooltip.tsx +326 -0
  94. package/dist/content.d.ts +110 -0
  95. package/dist/content.js +195 -0
  96. package/dist/diagrams.d.ts +371 -0
  97. package/dist/diagrams.js +702 -0
  98. package/dist/index.d.ts +2714 -0
  99. package/dist/index.js +11220 -0
  100. package/dist/preset.d.ts +24 -0
  101. package/dist/preset.js +17 -0
  102. package/dist/tokens/tokens/primitives.css +45 -0
  103. package/dist/tokens/tokens/semantic.css +46 -0
  104. package/dist/tokens/tokens/theme.css +11 -0
  105. package/dist/tokens/tokens/tokens.json +65 -0
  106. package/index.ts +123 -0
  107. package/package.json +63 -0
  108. package/tailwind-preset.ts +22 -0
  109. package/tokens/primitives.css +45 -0
  110. package/tokens/semantic.css +46 -0
  111. package/tokens/theme.css +11 -0
  112. 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
+ }