@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,106 @@
1
+ /**
2
+ * Snapshot Browser — Shared type definitions
3
+ *
4
+ * Part of: Sections > Snapshot Browser
5
+ *
6
+ * These types define the CONTRACT between the UI layer (this package) and
7
+ * the Rust/Tauri backend that each app must implement. Every type here has
8
+ * a 1:1 mapping to a Rust struct returned by Tauri commands.
9
+ *
10
+ * IMPORTANT FOR AI AGENTS:
11
+ * When adding a new field here, the corresponding Rust struct must also be
12
+ * updated (and vice versa). The serde rename attributes in Rust use camelCase
13
+ * to match these TypeScript interfaces.
14
+ *
15
+ * Data hierarchy:
16
+ * SnapshotScope → SnapshotCategory → SnapshotItem → SnapshotEntry
17
+ *
18
+ * Example:
19
+ * Scope: "Settings (Prompts)"
20
+ * Category: "Verifier Prompts"
21
+ * Item: "system-prompt"
22
+ * Entry: { content: "You are a...", savedAt: "2026-02-25T10:00:00Z" }
23
+ */
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Snapshot data hierarchy
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /** A single snapshot — one saved version of content */
30
+ export interface SnapshotEntry {
31
+ id: string
32
+ content: string
33
+ savedAt: string
34
+ label?: string
35
+ }
36
+
37
+ /** An item containing snapshots (e.g. a specific prompt or file) */
38
+ export interface SnapshotItem {
39
+ id: string
40
+ name: string
41
+ snapshots: SnapshotEntry[]
42
+ }
43
+
44
+ /** A category containing items (e.g. "Verifier Prompts", "Skills") */
45
+ export interface SnapshotCategory {
46
+ id: string
47
+ name: string
48
+ icon?: string
49
+ items: SnapshotItem[]
50
+ }
51
+
52
+ /** A top-level scope containing categories (e.g. "Settings", "Extensions") */
53
+ export interface SnapshotScope {
54
+ id: string
55
+ name: string
56
+ categories: SnapshotCategory[]
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // API adapter interface
61
+ // ---------------------------------------------------------------------------
62
+
63
+ /**
64
+ * SnapshotBrowserApi — The callback interface that each app must implement.
65
+ *
66
+ * This is the bridge between the shared UI and the app-specific backend.
67
+ * Each function maps to a Tauri command (or any other backend).
68
+ *
69
+ * ┌─────────────────────────────────────────────────────────────────────┐
70
+ * │ RUST BACKEND IMPLEMENTATION GUIDE │
71
+ * │ │
72
+ * │ Each method below corresponds to a Tauri #[tauri::command]. │
73
+ * │ The Rust reference implementation lives in: │
74
+ * │ configr/main/src-tauri/src/commands/snapshots/ │
75
+ * │ │
76
+ * │ REQUIRED Tauri commands: │
77
+ * │ │
78
+ * │ delete_snapshot(scope, category, item, snapshot) → void │
79
+ * │ Deletes a single snapshot entry by its composite key. │
80
+ * │ The four IDs form a unique path to the snapshot. │
81
+ * │ │
82
+ * │ clear_all_snapshots() → void │
83
+ * │ Removes all snapshots across all scopes. │
84
+ * │ Should clear the backing store completely. │
85
+ * │ │
86
+ * │ STORAGE STRUCTURE (Rust backend must maintain): │
87
+ * │ Snapshots are stored as JSON in the app data directory: │
88
+ * │ {app_data_dir}/ │
89
+ * │ └── snapshots/ │
90
+ * │ └── {scope}/ │
91
+ * │ └── {category}/ │
92
+ * │ └── {item}.json — SnapshotEntry[] serialized │
93
+ * │ │
94
+ * │ KEY BEHAVIORS the Rust backend must implement: │
95
+ * │ 1. Pruning: when snapshot limit is reached, remove oldest entry │
96
+ * │ 2. Atomic writes: use temp file + rename for crash safety │
97
+ * │ 3. IDs should be stable (UUID or timestamp-based) │
98
+ * └─────────────────────────────────────────────────────────────────────┘
99
+ */
100
+ export interface SnapshotBrowserApi {
101
+ /** Delete a single snapshot by its composite key */
102
+ deleteSnapshot: (scopeId: string, categoryId: string, itemId: string, snapshotId: string) => Promise<void>
103
+
104
+ /** Clear all snapshots across all scopes */
105
+ clearAllSnapshots: () => Promise<void>
106
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * useSnapshotBrowser — State management hook for the snapshot tree browser
3
+ *
4
+ * Part of: Sections > Snapshot Browser
5
+ *
6
+ * Manages:
7
+ * - Tree expansion state (expanded scopes, categories, items)
8
+ * - Search filter with auto-expand on search
9
+ * - Expand all / collapse all
10
+ * - Total snapshot count computation
11
+ * - Delete operations with loading state
12
+ */
13
+
14
+ import { useState, useMemo, useEffect, useRef, useCallback } from 'react'
15
+ import type { SnapshotScope, SnapshotBrowserApi } from './types.ts'
16
+
17
+ export interface UseSnapshotBrowserOptions {
18
+ scopes: SnapshotScope[]
19
+ api: SnapshotBrowserApi
20
+ }
21
+
22
+ export interface UseSnapshotBrowserReturn {
23
+ searchQuery: string
24
+ setSearchQuery: (query: string) => void
25
+ expandedPaths: Set<string>
26
+ toggleExpand: (path: string) => void
27
+ expandAll: () => void
28
+ collapseAll: () => void
29
+ allExpanded: boolean
30
+ totalSnapshotCount: number
31
+ allExpandablePaths: string[]
32
+ deleteSnapshot: (scopeId: string, categoryId: string, itemId: string, snapshotId: string) => Promise<void>
33
+ deletingSnapshotId: string | null
34
+ }
35
+
36
+ function collectExpandablePaths(scopes: SnapshotScope[]): string[] {
37
+ const paths: string[] = []
38
+ for (const scope of scopes) {
39
+ paths.push(scope.id)
40
+ for (const category of scope.categories) {
41
+ paths.push(`${scope.id}/${category.id}`)
42
+ for (const item of category.items) {
43
+ if (item.snapshots.length > 0) {
44
+ paths.push(`${scope.id}/${category.id}/${item.id}`)
45
+ }
46
+ }
47
+ }
48
+ }
49
+ return paths
50
+ }
51
+
52
+ function countSnapshots(scopes: SnapshotScope[]): number {
53
+ let count = 0
54
+ for (const scope of scopes) {
55
+ for (const category of scope.categories) {
56
+ for (const item of category.items) {
57
+ count += item.snapshots.length
58
+ }
59
+ }
60
+ }
61
+ return count
62
+ }
63
+
64
+ export function useSnapshotBrowser({ scopes, api }: UseSnapshotBrowserOptions): UseSnapshotBrowserReturn {
65
+ const [searchQuery, setSearchQuery] = useState('')
66
+ const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => new Set())
67
+ const [deletingSnapshotId, setDeletingSnapshotId] = useState<string | null>(null)
68
+ const prevSearchRef = useRef('')
69
+
70
+ const allExpandablePaths = useMemo(() => collectExpandablePaths(scopes), [scopes])
71
+ const totalSnapshotCount = useMemo(() => countSnapshots(scopes), [scopes])
72
+
73
+ const allExpanded = allExpandablePaths.length > 0 && allExpandablePaths.every((p: string) => expandedPaths.has(p))
74
+
75
+ // Auto-expand all when search query is entered
76
+ useEffect(() => {
77
+ if (searchQuery.trim() && !prevSearchRef.current.trim()) {
78
+ setExpandedPaths(new Set(allExpandablePaths))
79
+ }
80
+ prevSearchRef.current = searchQuery
81
+ }, [searchQuery, allExpandablePaths])
82
+
83
+ const toggleExpand = useCallback((path: string) => {
84
+ setExpandedPaths((prev: Set<string>) => {
85
+ const next = new Set(prev)
86
+ if (next.has(path)) {
87
+ next.delete(path)
88
+ } else {
89
+ next.add(path)
90
+ }
91
+ return next
92
+ })
93
+ }, [])
94
+
95
+ const expandAll = useCallback(() => {
96
+ setExpandedPaths(new Set(allExpandablePaths))
97
+ }, [allExpandablePaths])
98
+
99
+ const collapseAll = useCallback(() => {
100
+ setExpandedPaths(new Set())
101
+ }, [])
102
+
103
+ const deleteSnapshot = useCallback(async (scopeId: string, categoryId: string, itemId: string, snapshotId: string) => {
104
+ setDeletingSnapshotId(snapshotId)
105
+ try {
106
+ await api.deleteSnapshot(scopeId, categoryId, itemId, snapshotId)
107
+ } finally {
108
+ setDeletingSnapshotId(null)
109
+ }
110
+ }, [api])
111
+
112
+ return {
113
+ searchQuery,
114
+ setSearchQuery,
115
+ expandedPaths,
116
+ toggleExpand,
117
+ expandAll,
118
+ collapseAll,
119
+ allExpanded,
120
+ totalSnapshotCount,
121
+ allExpandablePaths,
122
+ deleteSnapshot,
123
+ deletingSnapshotId,
124
+ }
125
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Snippets Editor — Section barrel export
3
+ *
4
+ * This section provides a complete, reusable snippets CRUD editor.
5
+ * Consuming apps provide data and an API adapter; the component handles
6
+ * the full UI including search, validation, and two-column layout.
7
+ *
8
+ * File structure:
9
+ * - snippets-editor.tsx — Main two-column editor component (drop-in usage)
10
+ * - use-snippets-editor.ts — Form state & CRUD hook (used by editor, also standalone)
11
+ * - types.ts — Shared types and validation regex
12
+ *
13
+ * Quick start:
14
+ * import { SnippetsEditor } from '@toolr/ui-design'
15
+ *
16
+ * <SnippetsEditor
17
+ * api={snippetsApi}
18
+ * snippets={currentSnippets}
19
+ * title="Skills Snippets"
20
+ * description="Define snippets to reuse in skills prompts with {{SNIPPET_NAME}} syntax."
21
+ * />
22
+ */
23
+
24
+ // Main editor component
25
+ export { SnippetsEditor, type SnippetsEditorProps } from './snippets-editor.tsx'
26
+
27
+ // Hook for custom UIs
28
+ export { useSnippetsEditor, type UseSnippetsEditorOptions, type UseSnippetsEditorReturn } from './use-snippets-editor.ts'
29
+
30
+ // Types
31
+ export { SNIPPET_NAME_REGEX, type SnippetData, type SnippetsEditorApi } from './types.ts'
@@ -0,0 +1,381 @@
1
+ /**
2
+ * SnippetsEditor — Two-column CRUD editor for prompt snippets
3
+ *
4
+ * Part of: Sections > Snippets Editor
5
+ *
6
+ * Replicates the configr "Settings > Extensions > [type] > Snippets" page
7
+ * as a reusable, self-contained component. Left column shows the snippet
8
+ * list with search; right column shows the editor form.
9
+ *
10
+ * Usage:
11
+ * <SnippetsEditor
12
+ * api={snippetsApi}
13
+ * snippets={currentSnippets}
14
+ * title="Skills Snippets"
15
+ * description="Define snippets to reuse in skills prompts with {{SNIPPET_NAME}} syntax."
16
+ * />
17
+ *
18
+ * AI agent notes:
19
+ * - Uses a resizable divider between left sidebar and right editor
20
+ * - The component is self-contained — no Zustand, no Tauri dependencies
21
+ * - Consuming apps provide data + API callbacks; this component handles UI
22
+ * - Dark theme styling matches configr's Catppuccin-inspired palette
23
+ */
24
+
25
+ import { useCallback, useRef, useState } from 'react'
26
+ import { Plus, X, Braces, Trash2, RotateCcw, Save } from 'lucide-react'
27
+ import { cn } from '../../lib/cn.ts'
28
+ import { Input } from '../../ui/input.tsx'
29
+ import { ResizableTextarea } from '../../ui/resizable-textarea.tsx'
30
+ import { IconButton } from '../../ui/icon-button.tsx'
31
+ import { useSnippetsEditor } from './use-snippets-editor.ts'
32
+ import type { SnippetData, SnippetsEditorApi } from './types.ts'
33
+
34
+ export interface SnippetsEditorProps {
35
+ api: SnippetsEditorApi
36
+ snippets: SnippetData[]
37
+ /** Section title, e.g. "Skills Snippets" */
38
+ title?: string
39
+ /** Section description, e.g. "Define snippets to reuse in skills prompts..." */
40
+ description?: string
41
+ className?: string
42
+ }
43
+
44
+ const MIN_SIDEBAR = 200
45
+ const MAX_SIDEBAR = 350
46
+ const DEFAULT_SIDEBAR = 260
47
+
48
+ export function SnippetsEditor({
49
+ api,
50
+ snippets,
51
+ title = 'Snippets',
52
+ description = 'Define reusable snippets for prompts using {{SNIPPET_NAME}} syntax.',
53
+ className,
54
+ }: SnippetsEditorProps) {
55
+ const {
56
+ selectedName,
57
+ selectSnippet,
58
+ searchQuery,
59
+ setSearchQuery,
60
+ filteredSnippets,
61
+ formData,
62
+ setFormField,
63
+ formError,
64
+ isEditing,
65
+ isAdding,
66
+ startAdd,
67
+ cancelForm,
68
+ save,
69
+ remove,
70
+ resetForm,
71
+ isSaving,
72
+ } = useSnippetsEditor({ api, snippets })
73
+
74
+ // Resizable sidebar
75
+ const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR)
76
+ const dragRef = useRef<{ startX: number; startW: number } | null>(null)
77
+
78
+ const onDividerMouseDown = useCallback((e: React.MouseEvent) => {
79
+ e.preventDefault()
80
+ dragRef.current = { startX: e.clientX, startW: sidebarWidth }
81
+ const onMove = (ev: MouseEvent) => {
82
+ if (!dragRef.current) return
83
+ const newW = Math.min(MAX_SIDEBAR, Math.max(MIN_SIDEBAR, dragRef.current.startW + ev.clientX - dragRef.current.startX))
84
+ setSidebarWidth(newW)
85
+ }
86
+ const onUp = () => {
87
+ dragRef.current = null
88
+ document.removeEventListener('mousemove', onMove)
89
+ document.removeEventListener('mouseup', onUp)
90
+ }
91
+ document.addEventListener('mousemove', onMove)
92
+ document.addEventListener('mouseup', onUp)
93
+ }, [sidebarWidth])
94
+
95
+ const hasSelection = isEditing || isAdding
96
+ const nameHasError = formError !== null && (
97
+ formError.includes('name') || formError.includes('Name') || formError.includes('uppercase') || formError.includes('exists')
98
+ )
99
+
100
+ return (
101
+ <div className={cn('flex flex-col bg-[#181825] border border-[#313244] rounded-lg overflow-hidden', className)}>
102
+ {/* Header */}
103
+ <div className="flex items-center justify-between px-4 py-3 border-b border-[#313244] bg-purple-500/5">
104
+ <div className="flex items-center gap-2">
105
+ <Braces className="w-4 h-4 text-purple-400" />
106
+ <h3 className="text-sm font-medium text-[#cdd6f4]">{title}</h3>
107
+ <span className="px-2 py-0.5 text-xs rounded-full bg-[#313244] text-[#a6adc8]">
108
+ {snippets.length}
109
+ </span>
110
+ </div>
111
+ <p className="text-xs text-[#6c7086] hidden sm:block">{description}</p>
112
+ </div>
113
+
114
+ {/* Body: two columns */}
115
+ <div className="flex flex-1 min-h-[400px]">
116
+ {/* Left: Snippet list */}
117
+ <div className="flex flex-col border-r border-[#313244]" style={{ width: sidebarWidth, minWidth: MIN_SIDEBAR }}>
118
+ {/* Search + Add */}
119
+ <div className="flex items-center gap-1.5 p-2 border-b border-[#313244]">
120
+ <div className="flex-1">
121
+ <Input
122
+ type="search"
123
+ value={searchQuery}
124
+ onChange={setSearchQuery}
125
+ placeholder="Search snippets..."
126
+ size="xs"
127
+ />
128
+ </div>
129
+ <IconButton
130
+ icon={<Plus className="w-3.5 h-3.5" />}
131
+ onClick={startAdd}
132
+ size="xs"
133
+ color="blue"
134
+ tooltip={{ title: 'Add Snippet', description: 'Create a new snippet' }}
135
+ disabled={isAdding}
136
+ />
137
+ </div>
138
+
139
+ {/* Snippet list */}
140
+ <div className="flex-1 overflow-y-auto">
141
+ {filteredSnippets.length === 0 && !isAdding && (
142
+ <div className="text-center py-10 px-4">
143
+ <Braces className="w-8 h-8 mx-auto text-purple-400/40 mb-3" />
144
+ <p className="text-xs text-[#6c7086] mb-1">
145
+ {searchQuery ? 'No matching snippets' : 'No snippets defined'}
146
+ </p>
147
+ <p className="text-[10px] text-[#45475a]">
148
+ {searchQuery ? 'Try a different search term' : 'Click + to add your first snippet'}
149
+ </p>
150
+ </div>
151
+ )}
152
+ {filteredSnippets.map((snippet) => (
153
+ <SnippetListItem
154
+ key={snippet.name}
155
+ snippet={snippet}
156
+ selected={selectedName === snippet.name}
157
+ onSelect={() => selectSnippet(snippet.name)}
158
+ onDelete={() => remove(snippet.name)}
159
+ />
160
+ ))}
161
+ </div>
162
+ </div>
163
+
164
+ {/* Resizable divider */}
165
+ <div
166
+ className="w-1 cursor-col-resize bg-transparent hover:bg-blue-500/30 transition-colors flex-shrink-0"
167
+ onMouseDown={onDividerMouseDown}
168
+ />
169
+
170
+ {/* Right: Editor */}
171
+ <div className="flex-1 flex flex-col min-w-0">
172
+ {hasSelection ? (
173
+ <SnippetForm
174
+ formData={formData}
175
+ setFormField={setFormField}
176
+ formError={formError}
177
+ nameHasError={nameHasError}
178
+ isEditing={isEditing}
179
+ isSaving={isSaving}
180
+ onSave={save}
181
+ onReset={resetForm}
182
+ onDelete={isEditing && selectedName ? () => remove(selectedName) : undefined}
183
+ onCancel={isAdding ? cancelForm : undefined}
184
+ />
185
+ ) : (
186
+ <div className="flex-1 flex items-center justify-center p-6">
187
+ <div className="text-center max-w-xs">
188
+ <Braces className="w-10 h-10 mx-auto text-purple-400/30 mb-4" />
189
+ <p className="text-sm text-[#6c7086] mb-2">Select a snippet to edit</p>
190
+ <p className="text-xs text-[#45475a] leading-relaxed">
191
+ Choose a snippet from the list, or click{' '}
192
+ <span className="text-blue-400">+</span> to create a new one.
193
+ Reference snippets in prompts with{' '}
194
+ <span className="font-mono text-purple-400">{'{{SNIPPET_NAME}}'}</span> syntax.
195
+ </p>
196
+ </div>
197
+ </div>
198
+ )}
199
+ </div>
200
+ </div>
201
+ </div>
202
+ )
203
+ }
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // Snippet list item
207
+ // ---------------------------------------------------------------------------
208
+
209
+ interface SnippetListItemProps {
210
+ snippet: SnippetData
211
+ selected: boolean
212
+ onSelect: () => void
213
+ onDelete: () => void
214
+ }
215
+
216
+ function SnippetListItem({ snippet, selected, onSelect, onDelete }: SnippetListItemProps) {
217
+ return (
218
+ <div
219
+ className={cn(
220
+ 'group flex items-start gap-2 px-3 py-2.5 cursor-pointer transition-colors border-l-2',
221
+ selected
222
+ ? 'bg-[#1e1e2e] border-l-[#89b4fa]'
223
+ : 'border-l-transparent hover:bg-[#1e1e2e]/50',
224
+ )}
225
+ onClick={onSelect}
226
+ >
227
+ <div className="flex-1 min-w-0">
228
+ <p className="text-xs font-mono font-medium text-[#cdd6f4] truncate">
229
+ {snippet.name}
230
+ </p>
231
+ <p className="text-[10px] text-[#6c7086] truncate mt-0.5">
232
+ {snippet.description}
233
+ </p>
234
+ {snippet.value && (
235
+ <p className="text-[10px] text-[#45475a] truncate mt-0.5 font-mono">
236
+ {snippet.value.slice(0, 80)}{snippet.value.length > 80 ? '...' : ''}
237
+ </p>
238
+ )}
239
+ </div>
240
+ <button
241
+ type="button"
242
+ onClick={(e) => { e.stopPropagation(); onDelete() }}
243
+ className="opacity-0 group-hover:opacity-100 mt-0.5 p-0.5 rounded text-[#6c7086] hover:text-red-400 hover:bg-red-500/10 transition-all"
244
+ >
245
+ <X className="w-3 h-3" />
246
+ </button>
247
+ </div>
248
+ )
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Snippet editor form
253
+ // ---------------------------------------------------------------------------
254
+
255
+ interface SnippetFormProps {
256
+ formData: { name: string; description: string; value: string }
257
+ setFormField: (field: 'name' | 'description' | 'value', value: string) => void
258
+ formError: string | null
259
+ nameHasError: boolean
260
+ isEditing: boolean
261
+ isSaving: boolean
262
+ onSave: () => void
263
+ onReset: () => void
264
+ onDelete?: () => void
265
+ onCancel?: () => void
266
+ }
267
+
268
+ function SnippetForm({
269
+ formData,
270
+ setFormField,
271
+ formError,
272
+ nameHasError,
273
+ isEditing,
274
+ isSaving,
275
+ onSave,
276
+ onReset,
277
+ onDelete,
278
+ onCancel,
279
+ }: SnippetFormProps) {
280
+ return (
281
+ <div className="flex-1 flex flex-col">
282
+ <div className="flex-1 overflow-y-auto p-4 space-y-4">
283
+ {/* Name */}
284
+ <div>
285
+ <label className="block text-xs text-[#6c7086] mb-1.5">
286
+ Snippet Name <span className="text-red-400">*</span>
287
+ </label>
288
+ <Input
289
+ value={formData.name}
290
+ onChange={(val: string) => setFormField('name', val)}
291
+ placeholder="MY_SNIPPET"
292
+ error={nameHasError}
293
+ autoFocus={!isEditing}
294
+ />
295
+ <p className="mt-1 text-[10px] text-[#45475a]">
296
+ Use in prompts as <span className="font-mono text-purple-400">{'{{' + (formData.name || 'NAME') + '}}'}</span>
297
+ </p>
298
+ </div>
299
+
300
+ {/* Description */}
301
+ <div>
302
+ <label className="block text-xs text-[#6c7086] mb-1.5">
303
+ Description <span className="text-red-400">*</span>
304
+ </label>
305
+ <Input
306
+ value={formData.description}
307
+ onChange={(val: string) => setFormField('description', val)}
308
+ placeholder="Describe what this snippet is used for"
309
+ />
310
+ </div>
311
+
312
+ {/* Value */}
313
+ <div>
314
+ <label className="block text-xs text-[#6c7086] mb-1.5">Value</label>
315
+ <ResizableTextarea
316
+ mode="code"
317
+ language="markdown"
318
+ value={formData.value}
319
+ onChange={(val) => setFormField('value', val)}
320
+ minHeight={160}
321
+ />
322
+ <p className="mt-1 text-[10px] text-[#45475a]">
323
+ Can be a single value, multi-line text, or an entire document
324
+ </p>
325
+ </div>
326
+
327
+ {/* Error */}
328
+ {formError && (
329
+ <div className="px-3 py-2 bg-red-500/10 border border-red-500/30 rounded-lg">
330
+ <p className="text-xs text-red-400">{formError}</p>
331
+ </div>
332
+ )}
333
+ </div>
334
+
335
+ {/* Footer actions */}
336
+ <div className="flex items-center justify-between gap-2 border-t border-[#313244] px-4 py-3">
337
+ <div>
338
+ {onDelete && (
339
+ <IconButton
340
+ icon={<Trash2 className="w-3.5 h-3.5" />}
341
+ onClick={onDelete}
342
+ size="sm"
343
+ color="red"
344
+ disabled={isSaving}
345
+ tooltip={{ title: 'Delete', description: 'Remove this snippet' }}
346
+ />
347
+ )}
348
+ </div>
349
+ <div className="flex items-center gap-2">
350
+ {onCancel && (
351
+ <button
352
+ type="button"
353
+ onClick={onCancel}
354
+ disabled={isSaving}
355
+ className="rounded-md border border-[#313244] bg-transparent px-3 py-1.5 text-xs text-[#a6adc8] transition-colors hover:bg-[#313244] hover:text-[#cdd6f4] disabled:opacity-50"
356
+ >
357
+ Cancel
358
+ </button>
359
+ )}
360
+ <IconButton
361
+ icon={<RotateCcw className="w-3.5 h-3.5" />}
362
+ onClick={onReset}
363
+ size="sm"
364
+ color="neutral"
365
+ disabled={isSaving}
366
+ tooltip={{ title: 'Reset', description: 'Revert to saved values' }}
367
+ />
368
+ <button
369
+ type="button"
370
+ onClick={onSave}
371
+ disabled={isSaving}
372
+ className="flex items-center gap-1.5 rounded-md bg-blue-600 px-3 py-1.5 text-xs text-white transition-colors hover:bg-blue-500 disabled:cursor-not-allowed disabled:opacity-50"
373
+ >
374
+ <Save className="w-3 h-3" />
375
+ {isEditing ? 'Save' : 'Add'}
376
+ </button>
377
+ </div>
378
+ </div>
379
+ </div>
380
+ )
381
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Snippets Editor — Shared type definitions
3
+ *
4
+ * Part of: Sections > Snippets Editor
5
+ *
6
+ * Snippets are reusable text blocks that can be referenced in prompts
7
+ * using the {{SNIPPET_NAME}} syntax. Each snippet has a name (uppercase
8
+ * with underscores), a description, and a value that gets substituted
9
+ * at render time.
10
+ *
11
+ * Example usage in a prompt:
12
+ * "Follow the coding standards defined in {{CODE_STYLE}}"
13
+ *
14
+ * The consuming app is responsible for the actual template substitution.
15
+ * This UI only manages CRUD operations via the provided API.
16
+ */
17
+
18
+ /** A single snippet definition */
19
+ export interface SnippetData {
20
+ /** Snippet name — must match SNIPPET_NAME_REGEX (e.g., PROJECT_CONTEXT) */
21
+ name: string
22
+ /** Human-readable description of what this snippet contains */
23
+ description: string
24
+ /** The actual snippet content that gets substituted for {{NAME}} */
25
+ value: string
26
+ }
27
+
28
+ /**
29
+ * Validation regex for snippet names.
30
+ * Must start with an uppercase letter, followed by uppercase letters, digits, or underscores.
31
+ * Examples: MY_SNIPPET, CODE_STYLE, ERROR_HANDLING_V2
32
+ */
33
+ export const SNIPPET_NAME_REGEX = /^[A-Z][A-Z0-9_]*$/
34
+
35
+ /**
36
+ * SnippetsEditorApi — The callback interface that consuming apps must implement.
37
+ *
38
+ * Each method maps to a backend operation (Tauri command, API call, etc.).
39
+ * The UI calls these methods and awaits their completion before updating state.
40
+ */
41
+ export interface SnippetsEditorApi {
42
+ /** Add a new snippet. Rejects if a snippet with the same name already exists. */
43
+ addSnippet: (snippet: SnippetData) => Promise<void>
44
+ /** Update an existing snippet. originalName is used to find the snippet to replace. */
45
+ updateSnippet: (originalName: string, snippet: SnippetData) => Promise<void>
46
+ /** Remove a snippet by name. */
47
+ removeSnippet: (name: string) => Promise<void>
48
+ }