@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,165 @@
1
+ import { useState, useRef, useCallback, type KeyboardEvent } from 'react'
2
+ import { ChevronUp, ChevronDown } from 'lucide-react'
3
+ import { FORM_COLORS, type FormColor } from '../lib/form-colors.ts'
4
+
5
+ export interface NumberInputProps {
6
+ value: number
7
+ onChange: (value: number) => void
8
+ min?: number
9
+ max?: number
10
+ step?: number
11
+ variant?: 'filled' | 'outline'
12
+ color?: FormColor
13
+ size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
14
+ disabled?: boolean
15
+ className?: string
16
+ }
17
+
18
+ const SIZE_CONFIG = {
19
+ xss: { wrapper: 'h-[18px]', input: 'px-1 text-[10px]', chevron: 'w-2.5 h-2.5', stepperW: 'w-4' },
20
+ xs: { wrapper: 'h-6', input: 'px-1.5 text-xs', chevron: 'w-2.5 h-2.5', stepperW: 'w-5' },
21
+ sm: { wrapper: 'h-7', input: 'px-2 text-xs', chevron: 'w-3 h-3', stepperW: 'w-5' },
22
+ md: { wrapper: 'h-8', input: 'px-3 text-sm', chevron: 'w-3 h-3', stepperW: 'w-6' },
23
+ lg: { wrapper: 'h-9', input: 'px-3 text-sm', chevron: 'w-3.5 h-3.5', stepperW: 'w-7' },
24
+ }
25
+
26
+ const VARIANT_CLASSES = {
27
+ filled: 'bg-neutral-800',
28
+ outline: 'bg-transparent',
29
+ }
30
+
31
+ export function NumberInput({
32
+ value,
33
+ onChange,
34
+ min,
35
+ max,
36
+ step = 1,
37
+ variant = 'outline',
38
+ color = 'blue',
39
+ size = 'sm',
40
+ disabled = false,
41
+ className = '',
42
+ }: NumberInputProps) {
43
+ const [focused, setFocused] = useState(false)
44
+ const [editText, setEditText] = useState<string | null>(null)
45
+ const inputRef = useRef<HTMLInputElement>(null)
46
+
47
+ const clamp = useCallback((n: number) => {
48
+ if (min != null && n < min) return min
49
+ if (max != null && n > max) return max
50
+ return n
51
+ }, [min, max])
52
+
53
+ const nudge = useCallback((direction: 1 | -1) => {
54
+ onChange(clamp(value + step * direction))
55
+ }, [value, step, clamp, onChange])
56
+
57
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
58
+ if (e.key === 'ArrowUp') {
59
+ e.preventDefault()
60
+ nudge(1)
61
+ } else if (e.key === 'ArrowDown') {
62
+ e.preventDefault()
63
+ nudge(-1)
64
+ }
65
+ }
66
+
67
+ const commitEdit = () => {
68
+ if (editText != null) {
69
+ const parsed = Number(editText)
70
+ if (!Number.isNaN(parsed)) {
71
+ onChange(clamp(parsed))
72
+ }
73
+ setEditText(null)
74
+ }
75
+ setFocused(false)
76
+ }
77
+
78
+ const sc = SIZE_CONFIG[size]
79
+ const fc = FORM_COLORS[color]
80
+
81
+ return (
82
+ <div
83
+ className={`
84
+ inline-flex items-stretch rounded-lg border transition-colors
85
+ ${VARIANT_CLASSES[variant]}
86
+ ${focused ? fc.focus.replace('focus:', '') : fc.border}
87
+ ${disabled ? 'opacity-50 cursor-not-allowed' : ''}
88
+ ${className}
89
+ `}
90
+ >
91
+ <input
92
+ ref={inputRef}
93
+ type="text"
94
+ inputMode="numeric"
95
+ value={editText ?? value}
96
+ onChange={(e) => setEditText(e.target.value)}
97
+ onFocus={() => {
98
+ setFocused(true)
99
+ setEditText(String(value))
100
+ }}
101
+ onBlur={commitEdit}
102
+ onKeyDown={(e) => {
103
+ if (e.key === 'Enter') {
104
+ commitEdit()
105
+ inputRef.current?.blur()
106
+ } else {
107
+ handleKeyDown(e)
108
+ }
109
+ }}
110
+ disabled={disabled}
111
+ style={{ width: `${Math.max(1, String(editText ?? value).length) + 0.5}ch` }}
112
+ className={`
113
+ ${sc.wrapper} ${sc.input}
114
+ bg-transparent text-neutral-200 text-right
115
+ focus:outline-none
116
+ box-content min-w-0
117
+ disabled:cursor-not-allowed
118
+ [appearance:textfield]
119
+ [&::-webkit-outer-spin-button]:appearance-none
120
+ [&::-webkit-inner-spin-button]:appearance-none
121
+ `}
122
+ />
123
+ <div
124
+ className={`
125
+ flex flex-col border-l ${fc.border} ${sc.stepperW}
126
+ ${disabled ? 'pointer-events-none' : ''}
127
+ `}
128
+ >
129
+ <button
130
+ type="button"
131
+ tabIndex={-1}
132
+ onMouseDown={(e) => e.preventDefault()}
133
+ onClick={() => nudge(1)}
134
+ disabled={disabled || (max != null && value >= max)}
135
+ className={`
136
+ flex-1 flex items-center justify-center
137
+ text-neutral-500 hover:text-neutral-200 hover:bg-white/5
138
+ transition-colors cursor-pointer
139
+ disabled:opacity-30 disabled:cursor-not-allowed
140
+ rounded-tr-[calc(0.5rem-1px)]
141
+ `}
142
+ >
143
+ <ChevronUp className={sc.chevron} />
144
+ </button>
145
+ <div className={`border-t ${fc.border}`} />
146
+ <button
147
+ type="button"
148
+ tabIndex={-1}
149
+ onMouseDown={(e) => e.preventDefault()}
150
+ onClick={() => nudge(-1)}
151
+ disabled={disabled || (min != null && value <= min)}
152
+ className={`
153
+ flex-1 flex items-center justify-center
154
+ text-neutral-500 hover:text-neutral-200 hover:bg-white/5
155
+ transition-colors cursor-pointer
156
+ disabled:opacity-30 disabled:cursor-not-allowed
157
+ rounded-br-[calc(0.5rem-1px)]
158
+ `}
159
+ >
160
+ <ChevronDown className={sc.chevron} />
161
+ </button>
162
+ </div>
163
+ </div>
164
+ )
165
+ }
@@ -0,0 +1,261 @@
1
+ import { type ReactNode, useState, useEffect, useRef, useCallback } from 'react'
2
+ import { Search, ArrowRight, RefreshCw, Loader2, X, AlertTriangle } from 'lucide-react'
3
+ import { IconButton } from './icon-button.tsx'
4
+ import { Input } from './input.tsx'
5
+ import { RegistryCard, type RegistryCardProps } from './registry-card.tsx'
6
+
7
+ const PAGE_SIZE = 60
8
+
9
+ export interface RegistryBrowserProps {
10
+ // Content states
11
+ isLoading: boolean
12
+ loadingMessage?: string
13
+ error: string | null
14
+ isEmpty: boolean
15
+ emptyMessage?: string
16
+ onRetry?: () => void
17
+
18
+ // Search
19
+ searchQuery: string
20
+ onSearchChange: (q: string) => void
21
+ onSearchSubmit?: () => void
22
+ searchPlaceholder?: string
23
+
24
+ // Debounce indicator: increment key on each keystroke to restart the animation
25
+ debounceKey?: number
26
+ debounceDurationMs?: number
27
+
28
+ // Toolbar extras (between search and refresh)
29
+ toolbarCenter?: ReactNode
30
+ toolbarEnd?: ReactNode
31
+
32
+ // Refresh
33
+ onRefresh?: () => void
34
+ refreshTooltip?: { title?: string; description: string }
35
+
36
+ // Above grid (e.g., type tabs, suggested tags)
37
+ aboveGrid?: ReactNode
38
+
39
+ // Reset filters
40
+ hasActiveFilters?: boolean
41
+ onResetFilters?: () => void
42
+
43
+ // Scroll persistence
44
+ initialScrollTop?: number
45
+ onScrollChange?: (scrollTop: number) => void
46
+
47
+ // Max width class
48
+ maxWidth?: string
49
+
50
+ // Grid items
51
+ items: RegistryCardProps[]
52
+ }
53
+
54
+ export function RegistryBrowser({
55
+ isLoading,
56
+ loadingMessage = 'Loading...',
57
+ error,
58
+ isEmpty,
59
+ emptyMessage = 'No items match your filters.',
60
+ onRetry,
61
+ searchQuery,
62
+ onSearchChange,
63
+ onSearchSubmit,
64
+ searchPlaceholder = 'Search...',
65
+ debounceKey,
66
+ debounceDurationMs = 1000,
67
+ toolbarCenter,
68
+ toolbarEnd,
69
+ onRefresh,
70
+ refreshTooltip,
71
+ aboveGrid,
72
+ hasActiveFilters,
73
+ onResetFilters,
74
+ initialScrollTop,
75
+ onScrollChange,
76
+ maxWidth = 'max-w-[1440px]',
77
+ items,
78
+ }: RegistryBrowserProps) {
79
+ // Incremental rendering: render a page at a time, load more on scroll
80
+ const totalCount = items.length
81
+ const needsPaging = totalCount > PAGE_SIZE
82
+
83
+ const [visibleCount, setVisibleCount] = useState(PAGE_SIZE)
84
+ const scrollRef = useRef<HTMLDivElement>(null)
85
+ const sentinelRef = useRef<HTMLDivElement>(null)
86
+ const isFirstRender = useRef(true)
87
+ const prevTotalCount = useRef(totalCount)
88
+
89
+ // Reset visible count when the item list changes (filters/sort/search)
90
+ useEffect(() => {
91
+ setVisibleCount(PAGE_SIZE)
92
+ }, [totalCount])
93
+
94
+ // Scroll back to top when list changes — but skip on first render if restoring scroll
95
+ useEffect(() => {
96
+ if (isFirstRender.current) {
97
+ isFirstRender.current = false
98
+ if (initialScrollTop && initialScrollTop > 0) {
99
+ // Restore scroll position on first render
100
+ requestAnimationFrame(() => {
101
+ scrollRef.current?.scrollTo(0, initialScrollTop)
102
+ })
103
+ prevTotalCount.current = totalCount
104
+ return
105
+ }
106
+ }
107
+ // Only scroll to top when totalCount actually changes (filter/search action)
108
+ if (totalCount !== prevTotalCount.current) {
109
+ scrollRef.current?.scrollTo(0, 0)
110
+ prevTotalCount.current = totalCount
111
+ }
112
+ }, [totalCount, initialScrollTop])
113
+
114
+ // Debounced scroll position reporting
115
+ const scrollTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
116
+ const handleScroll = useCallback(() => {
117
+ if (!onScrollChange) return
118
+ clearTimeout(scrollTimerRef.current)
119
+ scrollTimerRef.current = setTimeout(() => {
120
+ if (scrollRef.current) {
121
+ onScrollChange(scrollRef.current.scrollTop)
122
+ }
123
+ }, 150)
124
+ }, [onScrollChange])
125
+
126
+ const loadMore = useCallback(() => {
127
+ setVisibleCount((prev: number) => Math.min(prev + PAGE_SIZE, totalCount))
128
+ }, [totalCount])
129
+
130
+ // IntersectionObserver using the scroll container as root
131
+ useEffect(() => {
132
+ const sentinel = sentinelRef.current
133
+ const scrollRoot = scrollRef.current
134
+ if (!sentinel || !scrollRoot || !needsPaging) return
135
+
136
+ const observer = new IntersectionObserver(
137
+ ([entry]) => {
138
+ if (entry.isIntersecting) loadMore()
139
+ },
140
+ { root: scrollRoot, rootMargin: '400px' },
141
+ )
142
+ observer.observe(sentinel)
143
+ return () => observer.disconnect()
144
+ }, [needsPaging, loadMore])
145
+
146
+ const visibleItems = needsPaging ? items.slice(0, visibleCount) : items
147
+ const hasMore = visibleCount < totalCount
148
+
149
+ return (
150
+ <div ref={scrollRef} className="h-full overflow-y-auto p-4 pb-8" onScroll={handleScroll}>
151
+ {/* Toolbar */}
152
+ <div className="sticky top-0 flex items-center gap-2 pb-4">
153
+ <div className="relative flex-1 max-w-sm">
154
+ <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-neutral-500" />
155
+ <Input
156
+ value={searchQuery}
157
+ onChange={onSearchChange}
158
+ onKeyDown={onSearchSubmit ? (e: { key: string }) => { if (e.key === 'Enter') onSearchSubmit() } : undefined}
159
+ placeholder={searchPlaceholder}
160
+ variant="outline"
161
+ className="pl-8 pr-8"
162
+ />
163
+ {searchQuery && (
164
+ <button
165
+ type="button"
166
+ onClick={() => onSearchChange('')}
167
+ className="absolute right-2 top-1/2 -translate-y-1/2 w-[18px] h-[18px] flex items-center justify-center rounded-md text-neutral-400 hover:text-neutral-300 hover:bg-neutral-500/20 transition-colors"
168
+ >
169
+ <X className="w-2.5 h-2.5" />
170
+ </button>
171
+ )}
172
+ {debounceKey != null && debounceKey > 0 && (
173
+ <svg
174
+ key={debounceKey}
175
+ className="absolute inset-0 pointer-events-none"
176
+ style={{ width: '100%', height: '100%' }}
177
+ >
178
+ <rect
179
+ x="1" y="1" rx="5" ry="5"
180
+ fill="none"
181
+ stroke="rgb(52 211 153 / 0.7)"
182
+ strokeWidth="1.5"
183
+ pathLength="100"
184
+ strokeDasharray="100"
185
+ strokeDashoffset="0"
186
+ style={{
187
+ width: 'calc(100% - 2px)',
188
+ height: 'calc(100% - 2px)',
189
+ animation: `debounce-border-drain ${debounceDurationMs}ms linear forwards`,
190
+ }}
191
+ />
192
+ </svg>
193
+ )}
194
+ </div>
195
+
196
+ {onSearchSubmit && (
197
+ <IconButton
198
+ icon={<ArrowRight className="w-3.5 h-3.5" />}
199
+ onClick={onSearchSubmit}
200
+ size="sm"
201
+ color="blue"
202
+ tooltip={{ description: 'Search' }}
203
+ />
204
+ )}
205
+
206
+ <div className="flex-1" />
207
+
208
+ {toolbarCenter}
209
+ {toolbarEnd}
210
+
211
+ {onRefresh && (
212
+ <IconButton
213
+ icon={<RefreshCw className="w-3.5 h-3.5" />}
214
+ onClick={onRefresh}
215
+ size="sm"
216
+ tooltip={refreshTooltip ?? { description: 'Refresh registry data' }}
217
+ />
218
+ )}
219
+ </div>
220
+
221
+ {/* Above grid */}
222
+ {aboveGrid}
223
+
224
+ {/* Content */}
225
+ <div className={`${maxWidth} mx-auto relative z-10`}>
226
+ {isLoading ? (
227
+ <div className="flex items-center justify-center py-12 text-neutral-500">
228
+ <Loader2 className="w-5 h-5 animate-spin mr-2" />
229
+ {loadingMessage}
230
+ </div>
231
+ ) : error ? (
232
+ <div className="flex flex-col items-center justify-center py-12 text-neutral-500">
233
+ <AlertTriangle className="w-6 h-6 text-amber-400 mb-2" />
234
+ <p className="text-sm">{error}</p>
235
+ {onRetry && (
236
+ <button onClick={onRetry} className="mt-2 text-xs text-blue-400 hover:underline cursor-pointer">
237
+ Try again
238
+ </button>
239
+ )}
240
+ </div>
241
+ ) : isEmpty ? (
242
+ <div className="flex flex-col items-center justify-center py-12 text-neutral-500">
243
+ <p className="text-sm">{emptyMessage}</p>
244
+ {hasActiveFilters && onResetFilters && (
245
+ <button onClick={onResetFilters} className="mt-2 text-xs text-blue-400 hover:underline cursor-pointer">
246
+ Reset filters
247
+ </button>
248
+ )}
249
+ </div>
250
+ ) : (
251
+ <div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
252
+ {visibleItems.map((item) => (
253
+ <RegistryCard key={item.name} {...item} />
254
+ ))}
255
+ {hasMore && <div ref={sentinelRef} className="col-span-full h-1" />}
256
+ </div>
257
+ )}
258
+ </div>
259
+ </div>
260
+ )
261
+ }