@toolr/ui-design 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/lib/theme-engine.ts +48 -15
- package/components/sections/ai-tools-paths/tools-paths-panel.tsx +11 -11
- package/components/sections/captured-issues/captured-issues-panel.tsx +20 -20
- package/components/sections/golden-snapshots/file-diff-viewer.tsx +19 -19
- package/components/sections/golden-snapshots/golden-sync-panel.tsx +3 -3
- package/components/sections/golden-snapshots/snapshot-manager.tsx +15 -15
- package/components/sections/golden-snapshots/status-overview.tsx +40 -40
- package/components/sections/golden-snapshots/version-manager.tsx +10 -10
- package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +11 -11
- package/components/sections/prompt-editor/simulator-prompt-editor.tsx +15 -15
- package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +19 -19
- package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +10 -10
- package/components/sections/snapshot-browser/snapshot-tree.tsx +11 -11
- package/components/sections/snippets-editor/snippets-editor.tsx +24 -24
- package/components/settings/SettingsHeader.tsx +78 -0
- package/components/settings/SettingsPanel.tsx +21 -0
- package/components/settings/SettingsTreeNav.tsx +256 -0
- package/components/settings/index.ts +7 -0
- package/components/settings/settings-tree-utils.ts +120 -0
- package/components/ui/breadcrumb.tsx +16 -4
- package/components/ui/cookie-consent.tsx +82 -0
- package/components/ui/file-tree.tsx +5 -5
- package/components/ui/filter-dropdown.tsx +4 -4
- package/components/ui/form-actions.tsx +1 -1
- package/components/ui/label.tsx +31 -3
- package/components/ui/resizable-textarea.tsx +2 -2
- package/components/ui/segmented-toggle.tsx +17 -4
- package/components/ui/select.tsx +3 -3
- package/components/ui/sort-dropdown.tsx +2 -2
- package/components/ui/status-card.tsx +1 -1
- package/components/ui/tooltip.tsx +2 -2
- package/dist/index.d.ts +79 -8
- package/dist/index.js +1119 -622
- package/dist/tokens/{tokens/primitives.css → primitives.css} +10 -8
- package/dist/tokens/{tokens/semantic.css → semantic.css} +5 -0
- package/index.ts +13 -0
- package/package.json +6 -2
- package/tokens/primitives.css +10 -8
- package/tokens/semantic.css +5 -0
- /package/dist/tokens/{tokens/theme.css → theme.css} +0 -0
- /package/dist/tokens/{tokens/tokens.json → tokens.json} +0 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export type SettingsTreeNode = {
|
|
4
|
+
id: string
|
|
5
|
+
label: string
|
|
6
|
+
icon: ReactNode
|
|
7
|
+
children?: SettingsTreeNode[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function findNodeByPath(tree: SettingsTreeNode[], path: string): SettingsTreeNode | null {
|
|
11
|
+
const parts = path.split('.')
|
|
12
|
+
let current: SettingsTreeNode | undefined
|
|
13
|
+
|
|
14
|
+
for (const part of parts) {
|
|
15
|
+
const found = (current?.children ?? tree).find((n) => n.id === part)
|
|
16
|
+
if (!found) return null
|
|
17
|
+
current = found
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return current ?? null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getBreadcrumbFromPath(
|
|
24
|
+
tree: SettingsTreeNode[],
|
|
25
|
+
path: string,
|
|
26
|
+
): Array<{ label: string; icon: ReactNode; path: string }> {
|
|
27
|
+
const parts = path.split('.')
|
|
28
|
+
const breadcrumb: Array<{ label: string; icon: ReactNode; path: string }> = []
|
|
29
|
+
let currentPath = ''
|
|
30
|
+
let currentNodes: SettingsTreeNode[] = tree
|
|
31
|
+
|
|
32
|
+
for (const part of parts) {
|
|
33
|
+
const node = currentNodes.find((n) => n.id === part)
|
|
34
|
+
if (!node) break
|
|
35
|
+
|
|
36
|
+
currentPath = currentPath ? `${currentPath}.${part}` : part
|
|
37
|
+
breadcrumb.push({
|
|
38
|
+
label: node.label,
|
|
39
|
+
icon: node.icon,
|
|
40
|
+
path: currentPath,
|
|
41
|
+
})
|
|
42
|
+
currentNodes = node.children ?? []
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return breadcrumb
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isLeafNode(node: SettingsTreeNode): boolean {
|
|
49
|
+
return !node.children || node.children.length === 0
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getAllLeafPaths(tree: SettingsTreeNode[], parentPath = ''): string[] {
|
|
53
|
+
const paths: string[] = []
|
|
54
|
+
|
|
55
|
+
for (const node of tree) {
|
|
56
|
+
const nodePath = parentPath ? `${parentPath}.${node.id}` : node.id
|
|
57
|
+
|
|
58
|
+
if (isLeafNode(node)) {
|
|
59
|
+
paths.push(nodePath)
|
|
60
|
+
} else if (node.children) {
|
|
61
|
+
paths.push(...getAllLeafPaths(node.children, nodePath))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return paths
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function collectExpandablePaths(nodes: SettingsTreeNode[], parentPath = ''): string[] {
|
|
69
|
+
const paths: string[] = []
|
|
70
|
+
for (const node of nodes) {
|
|
71
|
+
const nodePath = parentPath ? `${parentPath}.${node.id}` : node.id
|
|
72
|
+
if (!isLeafNode(node) && node.children) {
|
|
73
|
+
paths.push(nodePath)
|
|
74
|
+
paths.push(...collectExpandablePaths(node.children, nodePath))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return paths
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getParentPaths(path: string): string[] {
|
|
81
|
+
const parts = path.split('.')
|
|
82
|
+
const parents: string[] = []
|
|
83
|
+
let current = ''
|
|
84
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
85
|
+
current = current ? `${current}.${parts[i]}` : parts[i]
|
|
86
|
+
parents.push(current)
|
|
87
|
+
}
|
|
88
|
+
return parents
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function filterTree(
|
|
92
|
+
nodes: SettingsTreeNode[],
|
|
93
|
+
query: string,
|
|
94
|
+
parentPath = '',
|
|
95
|
+
): SettingsTreeNode[] {
|
|
96
|
+
if (!query.trim()) return nodes
|
|
97
|
+
|
|
98
|
+
const lowerQuery = query.toLowerCase()
|
|
99
|
+
|
|
100
|
+
return nodes
|
|
101
|
+
.map((node) => {
|
|
102
|
+
const nodePath = parentPath ? `${parentPath}.${node.id}` : node.id
|
|
103
|
+
const labelMatches = node.label.toLowerCase().includes(lowerQuery)
|
|
104
|
+
|
|
105
|
+
if (isLeafNode(node)) {
|
|
106
|
+
return labelMatches ? node : null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const filteredChildren = filterTree(node.children || [], query, nodePath)
|
|
110
|
+
|
|
111
|
+
if (labelMatches) {
|
|
112
|
+
return node
|
|
113
|
+
} else if (filteredChildren.length > 0) {
|
|
114
|
+
return { ...node, children: filteredChildren }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null
|
|
118
|
+
})
|
|
119
|
+
.filter((n): n is SettingsTreeNode => n !== null)
|
|
120
|
+
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
Globe, Star, Users, User, Tag, Search, Heart,
|
|
7
7
|
Zap, Shield, ShieldCheck, Sparkles, Eye, Lock,
|
|
8
8
|
Cloud, Wand2, Bell, Bookmark, Pin, Mail, Send,
|
|
9
|
-
Image, Bot, Puzzle, Plug, Webhook,
|
|
9
|
+
Image, Bot, Puzzle, Plug, Webhook, House, Package,
|
|
10
10
|
} from 'lucide-react'
|
|
11
11
|
import type { LucideIcon } from 'lucide-react'
|
|
12
12
|
import type { IconName } from './icon-button.tsx'
|
|
@@ -44,6 +44,8 @@ const iconSubset: Partial<Record<IconName, LucideIcon>> = {
|
|
|
44
44
|
puzzle: Puzzle,
|
|
45
45
|
plug: Plug,
|
|
46
46
|
webhook: Webhook,
|
|
47
|
+
home: House,
|
|
48
|
+
'package': Package,
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export interface BreadcrumbSegment {
|
|
@@ -58,6 +60,8 @@ export interface BreadcrumbProps {
|
|
|
58
60
|
segments: BreadcrumbSegment[]
|
|
59
61
|
separator?: 'chevron' | 'slash' | 'dot'
|
|
60
62
|
size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
|
|
63
|
+
/** 'box' (default) wraps in a bordered container; 'plain' renders inline with no background */
|
|
64
|
+
variant?: 'box' | 'plain'
|
|
61
65
|
className?: string
|
|
62
66
|
}
|
|
63
67
|
|
|
@@ -116,17 +120,23 @@ export function Breadcrumb({
|
|
|
116
120
|
segments,
|
|
117
121
|
separator = 'chevron',
|
|
118
122
|
size = 'sm',
|
|
123
|
+
variant = 'box',
|
|
119
124
|
className,
|
|
120
125
|
}: BreadcrumbProps) {
|
|
121
126
|
const s = sizeConfig[size]
|
|
127
|
+
const isBox = variant === 'box'
|
|
122
128
|
|
|
123
129
|
return (
|
|
124
130
|
<nav className={cn('flex items-center', className)}>
|
|
125
|
-
<div className={cn(
|
|
131
|
+
<div className={cn(
|
|
132
|
+
'flex items-center gap-1',
|
|
133
|
+
isBox && [s.px, s.py, 'bg-neutral-800/50 border border-neutral-700/50 rounded-lg'],
|
|
134
|
+
)}>
|
|
126
135
|
{segments.map((segment, index) => {
|
|
127
136
|
const isLast = index === segments.length - 1
|
|
128
137
|
const isClickable = !isLast && !!segment.onClick
|
|
129
138
|
const colors = segment.color && colorMap[segment.color] ? colorMap[segment.color] : null
|
|
139
|
+
const isFirstPlain = !isBox && index === 0
|
|
130
140
|
|
|
131
141
|
return (
|
|
132
142
|
<div key={segment.id} className="flex items-center gap-1">
|
|
@@ -136,7 +146,8 @@ export function Breadcrumb({
|
|
|
136
146
|
type="button"
|
|
137
147
|
onClick={segment.onClick}
|
|
138
148
|
className={cn(
|
|
139
|
-
'flex items-center gap-1.5
|
|
149
|
+
'flex items-center gap-1.5 pr-2 py-0.5 rounded-md transition-colors cursor-pointer',
|
|
150
|
+
isFirstPlain ? 'pl-0' : 'pl-2',
|
|
140
151
|
s.text,
|
|
141
152
|
'font-medium hover:text-white',
|
|
142
153
|
colors ? [colors.text, `hover:${colors.bg}`] : ['text-neutral-300', 'hover:bg-neutral-700/50'],
|
|
@@ -148,7 +159,8 @@ export function Breadcrumb({
|
|
|
148
159
|
) : (
|
|
149
160
|
<div
|
|
150
161
|
className={cn(
|
|
151
|
-
'flex items-center gap-1.5
|
|
162
|
+
'flex items-center gap-1.5 pr-2 py-0.5 rounded-md',
|
|
163
|
+
isFirstPlain ? 'pl-0' : 'pl-2',
|
|
152
164
|
s.text,
|
|
153
165
|
isLast
|
|
154
166
|
? ['font-medium bg-neutral-700/50', colors ? colors.text : 'text-white']
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { IconButton } from './icon-button.tsx'
|
|
3
|
+
|
|
4
|
+
export type ConsentChoice = 'accepted' | 'declined' | 'essential'
|
|
5
|
+
|
|
6
|
+
export interface CookieConsentProps {
|
|
7
|
+
/** localStorage key used to persist the choice */
|
|
8
|
+
storageKey: string
|
|
9
|
+
/** Accent color for the "Accept All" button (default: 'cyan') */
|
|
10
|
+
accentColor?: string
|
|
11
|
+
/** Heading text (default: 'We value your privacy') */
|
|
12
|
+
heading?: string
|
|
13
|
+
/** Description text */
|
|
14
|
+
description?: string
|
|
15
|
+
/** Called after the user makes a choice */
|
|
16
|
+
onConsent?: (choice: ConsentChoice) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function CookieConsent({
|
|
20
|
+
storageKey,
|
|
21
|
+
accentColor = 'cyan',
|
|
22
|
+
heading = 'We value your privacy',
|
|
23
|
+
description = 'This site uses only essential cookies for functionality. No tracking or analytics.',
|
|
24
|
+
onConsent,
|
|
25
|
+
}: CookieConsentProps) {
|
|
26
|
+
const [isVisible, setIsVisible] = useState(false)
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const consent = localStorage.getItem(storageKey)
|
|
30
|
+
if (!consent) setIsVisible(true)
|
|
31
|
+
}, [storageKey])
|
|
32
|
+
|
|
33
|
+
const handleConsent = (choice: ConsentChoice) => {
|
|
34
|
+
localStorage.setItem(storageKey, choice)
|
|
35
|
+
setIsVisible(false)
|
|
36
|
+
onConsent?.(choice)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!isVisible) return null
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="fixed bottom-0 left-0 right-0 z-50 p-4 bg-neutral-900/95 backdrop-blur-sm border-t border-neutral-700/50">
|
|
43
|
+
<div className="max-w-6xl mx-auto flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
|
44
|
+
<div className="flex-grow">
|
|
45
|
+
<p className="text-sm text-neutral-200 mb-1">{heading}</p>
|
|
46
|
+
<p className="text-xs text-neutral-400">{description}</p>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div className="flex items-center gap-2 flex-shrink-0">
|
|
50
|
+
<button
|
|
51
|
+
onClick={() => handleConsent('declined')}
|
|
52
|
+
className="px-3 py-1.5 text-sm h-[26px] inline-flex items-center justify-center font-medium rounded-md cursor-pointer text-neutral-400 border border-transparent hover:text-neutral-200 hover:border-neutral-600 hover:bg-neutral-800 transition-colors"
|
|
53
|
+
>
|
|
54
|
+
Decline
|
|
55
|
+
</button>
|
|
56
|
+
<button
|
|
57
|
+
onClick={() => handleConsent('essential')}
|
|
58
|
+
className="px-3 py-1.5 text-sm h-[26px] inline-flex items-center justify-center font-medium rounded-md cursor-pointer text-neutral-400 border border-transparent hover:text-neutral-200 hover:border-neutral-600 hover:bg-neutral-800 transition-colors"
|
|
59
|
+
>
|
|
60
|
+
Essential Only
|
|
61
|
+
</button>
|
|
62
|
+
<button
|
|
63
|
+
onClick={() => handleConsent('accepted')}
|
|
64
|
+
className={`px-3 py-1.5 text-sm h-[26px] inline-flex items-center justify-center font-medium rounded-md cursor-pointer text-white border transition-colors bg-${accentColor}-600 border-${accentColor}-500 hover:bg-${accentColor}-500`}
|
|
65
|
+
>
|
|
66
|
+
Accept All
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<IconButton
|
|
71
|
+
icon="x"
|
|
72
|
+
color="neutral"
|
|
73
|
+
size="xs"
|
|
74
|
+
tooltip={{ description: 'Dismiss' }}
|
|
75
|
+
tooltipPosition="top"
|
|
76
|
+
onClick={() => handleConsent('declined')}
|
|
77
|
+
className="absolute top-2 right-2 sm:relative sm:top-auto sm:right-auto"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FileCode, Folder, ChevronRight, ChevronDown } from 'lucide-react'
|
|
1
|
+
import { FileCode, Folder, FolderOpen, ChevronRight, ChevronDown } from 'lucide-react'
|
|
2
2
|
|
|
3
3
|
export interface FileTreeNode {
|
|
4
4
|
name: string
|
|
@@ -126,8 +126,8 @@ function FileTreeNodeItem({ node, path, selectedPath, onSelectFile, expandedPath
|
|
|
126
126
|
const rowClass = isSelected
|
|
127
127
|
? `${base} ${selectedClass}`
|
|
128
128
|
: isDir
|
|
129
|
-
? `${base} cursor-pointer hover:text-neutral-200
|
|
130
|
-
: `${base} cursor-pointer hover:bg-neutral-700/50 hover:text-neutral-200
|
|
129
|
+
? `${base} cursor-pointer hover:text-neutral-200 ${iconColorClass}`
|
|
130
|
+
: `${base} cursor-pointer hover:bg-neutral-700/50 hover:text-neutral-200 ${iconColorClass}`
|
|
131
131
|
|
|
132
132
|
return (
|
|
133
133
|
<li>
|
|
@@ -141,9 +141,9 @@ function FileTreeNodeItem({ node, path, selectedPath, onSelectFile, expandedPath
|
|
|
141
141
|
<span className="w-3" />
|
|
142
142
|
)}
|
|
143
143
|
{isDir ? (
|
|
144
|
-
<Folder className={`w-3.5 h-3.5 shrink-0 ${iconColorClass}`} />
|
|
144
|
+
expanded ? <FolderOpen className={`w-3.5 h-3.5 shrink-0 ${iconColorClass}`} /> : <Folder className={`w-3.5 h-3.5 shrink-0 ${iconColorClass}`} />
|
|
145
145
|
) : (
|
|
146
|
-
<FileCode className={`w-3.5 h-3.5 shrink-0 ${
|
|
146
|
+
<FileCode className={`w-3.5 h-3.5 shrink-0 ${iconColorClass}`} />
|
|
147
147
|
)}
|
|
148
148
|
<span className="truncate">{node.name}</span>
|
|
149
149
|
</button>
|
|
@@ -8,7 +8,7 @@ const SEARCH_THRESHOLD = 20
|
|
|
8
8
|
|
|
9
9
|
const VARIANT_CLASSES = {
|
|
10
10
|
filled: { bg: 'bg-neutral-800', hoverBg: 'hover:bg-neutral-700' },
|
|
11
|
-
outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-
|
|
11
|
+
outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-700' },
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface FilterDropdownProps {
|
|
@@ -112,9 +112,9 @@ export function FilterDropdown({
|
|
|
112
112
|
)}
|
|
113
113
|
|
|
114
114
|
{isOpen && (
|
|
115
|
-
<div ref={menuRef} className={`absolute right-0 top-full z-50 mt-1 min-w-[140px] whitespace-nowrap
|
|
115
|
+
<div ref={menuRef} className={`absolute right-0 top-full z-50 mt-1 min-w-[140px] whitespace-nowrap bg-[var(--popover)] backdrop-blur border ${FORM_COLORS[color].border} rounded-lg shadow-xl overflow-hidden`}>
|
|
116
116
|
{showSearch && (
|
|
117
|
-
<div className={`sticky top-0 p-1.5
|
|
117
|
+
<div className={`sticky top-0 p-1.5 bg-[var(--popover)] border-b ${FORM_COLORS[color].border} z-10`}>
|
|
118
118
|
<div className="relative">
|
|
119
119
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-neutral-500" />
|
|
120
120
|
<input
|
|
@@ -124,7 +124,7 @@ export function FilterDropdown({
|
|
|
124
124
|
onChange={(e) => setSearch(e.target.value)}
|
|
125
125
|
onKeyDown={handleKeyDown}
|
|
126
126
|
placeholder="Search..."
|
|
127
|
-
className={`w-full pl-6 pr-2 py-1 text-xs bg-
|
|
127
|
+
className={`w-full pl-6 pr-2 py-1 text-xs bg-[var(--popover)] border border-neutral-600 rounded text-neutral-200 placeholder-neutral-500 outline-none ${FORM_COLORS[color].focus}`}
|
|
128
128
|
/>
|
|
129
129
|
</div>
|
|
130
130
|
</div>
|
|
@@ -67,7 +67,7 @@ export function FormActions({
|
|
|
67
67
|
const base = PADDING_CLASSES[padding]
|
|
68
68
|
const paddingClass = showBorder
|
|
69
69
|
? base
|
|
70
|
-
: base.replace(/\s*border-t\s+border
|
|
70
|
+
: base.replace(/\s*border-t\s+border-neutral-700/g, '')
|
|
71
71
|
|
|
72
72
|
const hasLeft = onBack || statusText
|
|
73
73
|
|
package/components/ui/label.tsx
CHANGED
|
@@ -41,6 +41,8 @@ export interface LabelProps {
|
|
|
41
41
|
tooltip: { title?: string; description: string }
|
|
42
42
|
size?: 'xss' | 'xs' | 'sm' | 'md' | 'lg'
|
|
43
43
|
textTransform?: 'capitalize' | 'lowercase' | 'uppercase'
|
|
44
|
+
/** Progress fill (0–100). Shows a colored background bar behind the label content. */
|
|
45
|
+
progress?: number
|
|
44
46
|
onClick?: () => void
|
|
45
47
|
className?: string
|
|
46
48
|
testId?: string
|
|
@@ -63,6 +65,23 @@ const colorClasses: Record<LabelColor, string> = {
|
|
|
63
65
|
pink: 'border-pink-500/50 text-pink-400',
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
const progressFillColors: Record<LabelColor, string> = {
|
|
69
|
+
neutral: 'bg-neutral-500/20',
|
|
70
|
+
green: 'bg-green-500/20',
|
|
71
|
+
red: 'bg-red-500/20',
|
|
72
|
+
blue: 'bg-blue-500/20',
|
|
73
|
+
yellow: 'bg-yellow-500/20',
|
|
74
|
+
orange: 'bg-orange-500/20',
|
|
75
|
+
purple: 'bg-purple-500/20',
|
|
76
|
+
amber: 'bg-amber-500/20',
|
|
77
|
+
emerald: 'bg-emerald-500/20',
|
|
78
|
+
cyan: 'bg-cyan-500/20',
|
|
79
|
+
indigo: 'bg-indigo-500/20',
|
|
80
|
+
teal: 'bg-teal-500/20',
|
|
81
|
+
violet: 'bg-violet-500/20',
|
|
82
|
+
pink: 'bg-pink-500/20',
|
|
83
|
+
}
|
|
84
|
+
|
|
66
85
|
const sizeConfig = {
|
|
67
86
|
xss: { height: 14, padding: 'px-1', text: 'text-[9px]', iconSize: 'w-2 h-2', gap: 'gap-0.5' },
|
|
68
87
|
xs: { height: 16, padding: 'px-1.5', text: 'text-[10px]', iconSize: 'w-2.5 h-2.5', gap: 'gap-1' },
|
|
@@ -89,18 +108,21 @@ export function Label({
|
|
|
89
108
|
tooltip,
|
|
90
109
|
size = 'sm',
|
|
91
110
|
textTransform,
|
|
111
|
+
progress,
|
|
92
112
|
onClick,
|
|
93
113
|
className = '',
|
|
94
114
|
testId,
|
|
95
115
|
}: LabelProps) {
|
|
96
116
|
const s = sizeConfig[size]
|
|
97
117
|
const cssTransform = textTransform === 'lowercase' || textTransform === 'uppercase' ? textTransform : undefined
|
|
118
|
+
const hasProgress = progress !== undefined && progress >= 0
|
|
98
119
|
|
|
99
120
|
const baseClasses = [
|
|
100
121
|
`inline-flex items-center ${s.gap}`,
|
|
101
122
|
s.padding,
|
|
102
123
|
s.text,
|
|
103
124
|
'border rounded font-medium leading-none',
|
|
125
|
+
hasProgress ? 'relative overflow-hidden' : '',
|
|
104
126
|
colorClasses[color],
|
|
105
127
|
onClick ? 'cursor-pointer hover:brightness-125 transition-all' : 'cursor-help',
|
|
106
128
|
className,
|
|
@@ -112,16 +134,22 @@ export function Label({
|
|
|
112
134
|
|
|
113
135
|
const content = (
|
|
114
136
|
<>
|
|
137
|
+
{hasProgress && (
|
|
138
|
+
<span
|
|
139
|
+
className={`absolute inset-y-0 left-0 ${progressFillColors[color]} rounded-[inherit]`}
|
|
140
|
+
style={{ width: `${Math.min(progress, 100)}%` }}
|
|
141
|
+
/>
|
|
142
|
+
)}
|
|
115
143
|
{CustomIcon && (
|
|
116
|
-
<span className={`${s.iconSize} flex-shrink-0`}><CustomIcon className={s.iconSize} /></span>
|
|
144
|
+
<span className={`${s.iconSize} flex-shrink-0 ${hasProgress ? 'relative' : ''}`}><CustomIcon className={s.iconSize} /></span>
|
|
117
145
|
)}
|
|
118
146
|
{icons.map((iconName, i) => {
|
|
119
147
|
const Icon = iconMap[iconName]
|
|
120
148
|
return Icon ? (
|
|
121
|
-
<span key={i} className={`${s.iconSize} flex-shrink-0`}><Icon className={s.iconSize} /></span>
|
|
149
|
+
<span key={i} className={`${s.iconSize} flex-shrink-0 ${hasProgress ? 'relative' : ''}`}><Icon className={s.iconSize} /></span>
|
|
122
150
|
) : null
|
|
123
151
|
})}
|
|
124
|
-
<span className=
|
|
152
|
+
<span className={`min-w-0 truncate ${hasProgress ? 'relative' : ''}`} style={cssTransform ? { textTransform: cssTransform } : undefined}>
|
|
125
153
|
{transformText(text, textTransform)}
|
|
126
154
|
</span>
|
|
127
155
|
</>
|
|
@@ -176,8 +176,8 @@ function ResizableCode({
|
|
|
176
176
|
inherit: true,
|
|
177
177
|
rules: [],
|
|
178
178
|
colors: {
|
|
179
|
-
'editor.background': variant === 'filled' ? '#
|
|
180
|
-
'editorGutter.background': variant === 'filled' ? '#
|
|
179
|
+
'editor.background': variant === 'filled' ? '#262626' : '#00000000',
|
|
180
|
+
'editorGutter.background': variant === 'filled' ? '#262626' : '#00000000',
|
|
181
181
|
'editor.lineHighlightBackground': '#00000000',
|
|
182
182
|
'editor.lineHighlightBorder': '#00000000',
|
|
183
183
|
},
|
|
@@ -3,7 +3,8 @@ import { Tooltip, type TooltipContent, type TooltipPosition } from './tooltip.ts
|
|
|
3
3
|
|
|
4
4
|
export interface SegmentedToggleOption<T extends string> {
|
|
5
5
|
value: T
|
|
6
|
-
icon
|
|
6
|
+
icon?: ReactNode
|
|
7
|
+
label?: string
|
|
7
8
|
tooltip: TooltipContent
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -22,8 +23,8 @@ export interface SegmentedToggleProps<T extends string> {
|
|
|
22
23
|
disabledTooltip?: TooltipContent
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
/**
|
|
26
|
-
const
|
|
26
|
+
/** Icon-only button sizes match IconButton dimensions */
|
|
27
|
+
const ICON_SIZE_CLASSES = {
|
|
27
28
|
xss: 'w-[18px] h-[18px]',
|
|
28
29
|
xs: 'w-6 h-6',
|
|
29
30
|
sm: 'w-7 h-7',
|
|
@@ -31,6 +32,15 @@ const BUTTON_SIZE_CLASSES = {
|
|
|
31
32
|
lg: 'w-9 h-9',
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/** Text label button sizes — horizontal padding instead of fixed width */
|
|
36
|
+
const TEXT_SIZE_CLASSES = {
|
|
37
|
+
xss: 'h-[18px] px-1.5 text-[9px]',
|
|
38
|
+
xs: 'h-6 px-2 text-[10px]',
|
|
39
|
+
sm: 'h-7 px-2.5 text-xs',
|
|
40
|
+
md: 'h-8 px-3 text-xs',
|
|
41
|
+
lg: 'h-9 px-3.5 text-sm',
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
const ROUNDING_CLASSES = {
|
|
35
45
|
xss: 'rounded-[3px]',
|
|
36
46
|
xs: 'rounded-[5px]',
|
|
@@ -102,18 +112,21 @@ export function SegmentedToggle<T extends string>({
|
|
|
102
112
|
const isFirst = i === 0
|
|
103
113
|
const isLast = i === options.length - 1
|
|
104
114
|
const rounding = isFirst && isLast ? ROUNDING_CLASSES[size] : isFirst ? `rounded-l-[5px]` : isLast ? `rounded-r-[5px]` : ''
|
|
115
|
+
const hasLabel = !!option.label
|
|
116
|
+
const sizeClass = hasLabel ? TEXT_SIZE_CLASSES[size] : ICON_SIZE_CLASSES[size]
|
|
105
117
|
return (
|
|
106
118
|
<Tooltip key={option.value} content={option.tooltip} position={tooltipPosition}>
|
|
107
119
|
<button
|
|
108
120
|
onClick={() => onChange(option.value)}
|
|
109
121
|
disabled={disabled}
|
|
110
|
-
className={`flex items-center justify-center ${
|
|
122
|
+
className={`flex items-center justify-center ${sizeClass} ${rounding} font-medium transition-all cursor-pointer ${
|
|
111
123
|
isActive
|
|
112
124
|
? ACTIVE_COLORS[accentColor] || ACTIVE_COLORS.blue
|
|
113
125
|
: `text-neutral-400 ${HOVER_COLORS[accentColor] || HOVER_COLORS.blue}`
|
|
114
126
|
}`}
|
|
115
127
|
>
|
|
116
128
|
{option.icon}
|
|
129
|
+
{option.label}
|
|
117
130
|
</button>
|
|
118
131
|
</Tooltip>
|
|
119
132
|
)
|
package/components/ui/select.tsx
CHANGED
|
@@ -24,8 +24,8 @@ export interface SelectProps<T extends string | number = string> {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const VARIANT_CLASSES = {
|
|
27
|
-
filled: { bg: 'bg-neutral-800', hoverBg: 'hover:bg-neutral-700', menuBg: 'bg-
|
|
28
|
-
outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-
|
|
27
|
+
filled: { bg: 'bg-neutral-800', hoverBg: 'hover:bg-neutral-700', menuBg: 'bg-[var(--popover)]' },
|
|
28
|
+
outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-700', menuBg: 'bg-[var(--popover)]' },
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const SIZE_CLASSES = {
|
|
@@ -134,7 +134,7 @@ export function Select<T extends string | number = string>({
|
|
|
134
134
|
{isOpen && menuPos && createPortal(
|
|
135
135
|
<div
|
|
136
136
|
ref={menuRef}
|
|
137
|
-
className={`fixed z-[9999] whitespace-nowrap ${v.menuBg} border ${FORM_COLORS[color].border} rounded-lg shadow-xl overflow-hidden`}
|
|
137
|
+
className={`fixed z-[9999] whitespace-nowrap ${v.menuBg} backdrop-blur border ${FORM_COLORS[color].border} rounded-lg shadow-xl overflow-hidden`}
|
|
138
138
|
style={{
|
|
139
139
|
top: menuPos.top,
|
|
140
140
|
left: align === 'right' ? undefined : menuPos.left,
|
|
@@ -6,7 +6,7 @@ import { FORM_COLORS, type FormColor } from '../lib/form-colors.ts'
|
|
|
6
6
|
|
|
7
7
|
const VARIANT_CLASSES = {
|
|
8
8
|
filled: { bg: 'bg-neutral-800', hoverBg: 'hover:bg-neutral-700' },
|
|
9
|
-
outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-
|
|
9
|
+
outline: { bg: 'bg-transparent', hoverBg: 'hover:bg-neutral-700' },
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export interface SortField {
|
|
@@ -88,7 +88,7 @@ export function SortDropdown({
|
|
|
88
88
|
</button>
|
|
89
89
|
|
|
90
90
|
{isOpen && (
|
|
91
|
-
<div ref={menuRef} className={`absolute right-0 top-full z-50 mt-1 min-w-[140px]
|
|
91
|
+
<div ref={menuRef} className={`absolute right-0 top-full z-50 mt-1 min-w-[140px] bg-[var(--popover)] backdrop-blur border ${FORM_COLORS[color].border} rounded-lg shadow-xl overflow-hidden`}>
|
|
92
92
|
{fields.map((f, idx) => (
|
|
93
93
|
<button
|
|
94
94
|
key={f.value}
|
|
@@ -66,7 +66,7 @@ export function StatusCard({
|
|
|
66
66
|
<h3 className="text-sm font-medium text-neutral-200">{title}</h3>
|
|
67
67
|
</div>
|
|
68
68
|
|
|
69
|
-
<div className="divide-y divide-
|
|
69
|
+
<div className="divide-y divide-neutral-700/60">
|
|
70
70
|
{items.map((item) => (
|
|
71
71
|
<div key={item.label} className="flex items-center justify-between px-4 py-2.5">
|
|
72
72
|
<span className="text-xs text-neutral-400">{item.label}</span>
|
|
@@ -135,7 +135,7 @@ function clampToViewport(
|
|
|
135
135
|
return { top, left }
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
const ARROW_BASE_CLASSES = 'absolute w-3 h-3 bg-
|
|
138
|
+
const ARROW_BASE_CLASSES = 'absolute w-3 h-3 bg-[var(--popover)] border-neutral-600 rotate-45'
|
|
139
139
|
|
|
140
140
|
function getAlignmentClass(align: TooltipAlign): string {
|
|
141
141
|
if (align === 'start') return 'left-2'
|
|
@@ -258,7 +258,7 @@ export function Tooltip({
|
|
|
258
258
|
const tooltipContent = (
|
|
259
259
|
<div
|
|
260
260
|
ref={tooltipRef}
|
|
261
|
-
className={`fixed px-3 py-1.5 bg-
|
|
261
|
+
className={`fixed px-3 py-1.5 bg-[var(--popover)] backdrop-blur border border-neutral-600 rounded-lg shadow-xl z-[9999] ${interactive || trigger === 'click' ? '' : 'pointer-events-none'} ${multiline ? 'whitespace-pre-line' : 'whitespace-nowrap'}`}
|
|
262
262
|
style={{
|
|
263
263
|
top: coords.top,
|
|
264
264
|
left: coords.left,
|