@handled-ai/design-system 0.18.10 → 0.18.12
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/dist/components/account-contacts-popover.d.ts +5 -1
- package/dist/components/account-contacts-popover.js +25 -4
- package/dist/components/account-contacts-popover.js.map +1 -1
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/data-table-condition-filter.d.ts +15 -3
- package/dist/components/data-table-condition-filter.js +199 -52
- package/dist/components/data-table-condition-filter.js.map +1 -1
- package/dist/components/data-table-filter.js +7 -8
- package/dist/components/data-table-filter.js.map +1 -1
- package/dist/components/entity-panel.d.ts +2 -1
- package/dist/components/entity-panel.js +52 -45
- package/dist/components/entity-panel.js.map +1 -1
- package/dist/components/pill.d.ts +1 -1
- package/dist/components/score-why-chips.d.ts +1 -1
- package/dist/components/signal-priority-popover.d.ts +1 -1
- package/dist/components/signal-priority-popover.js +4 -4
- package/dist/components/signal-priority-popover.js.map +1 -1
- package/dist/components/tabs.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/prototype/index.d.ts +1 -1
- package/dist/prototype/prototype-accounts-view.d.ts +1 -1
- package/dist/prototype/prototype-admin-view.d.ts +1 -1
- package/dist/prototype/prototype-config.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.d.ts +5 -3
- package/dist/prototype/prototype-inbox-view.js +11 -5
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/dist/prototype/prototype-insights-view.d.ts +1 -1
- package/dist/prototype/prototype-shell.d.ts +1 -1
- package/dist/{signal-priority-popover-BT6CPYNs.d.ts → signal-priority-popover-BEDoPsNE.d.ts} +6 -0
- package/package.json +1 -2
- package/src/components/__tests__/account-contacts-popover.test.tsx +79 -0
- package/src/components/__tests__/data-table-condition-filter.test.tsx +239 -7
- package/src/components/__tests__/entity-panel-header.test.tsx +44 -0
- package/src/components/__tests__/signal-priority-popover.test.tsx +30 -0
- package/src/components/account-contacts-popover.tsx +29 -1
- package/src/components/data-table-condition-filter.tsx +278 -68
- package/src/components/data-table-filter.tsx +6 -9
- package/src/components/entity-panel.tsx +56 -40
- package/src/components/signal-priority-popover.tsx +15 -4
- package/src/prototype/__tests__/detail-view-title-slots.test.tsx +15 -0
- package/src/prototype/prototype-config.ts +2 -0
- package/src/prototype/prototype-inbox-view.tsx +17 -5
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
DollarSign,
|
|
5
|
+
Check,
|
|
7
6
|
Eye,
|
|
8
|
-
Hash,
|
|
9
7
|
MoreHorizontal,
|
|
10
8
|
Plus,
|
|
11
9
|
Trash2,
|
|
12
|
-
Type,
|
|
13
10
|
} from "lucide-react"
|
|
14
11
|
|
|
15
12
|
import { cn } from "../lib/utils"
|
|
@@ -36,15 +33,26 @@ export type ConditionOperator =
|
|
|
36
33
|
| "is_null"
|
|
37
34
|
| "is_not_null"
|
|
38
35
|
|
|
36
|
+
export interface ConditionOptionObject {
|
|
37
|
+
label: string
|
|
38
|
+
value: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ConditionFieldOption = string | ConditionOptionObject
|
|
42
|
+
|
|
39
43
|
export interface ConditionFieldDef {
|
|
40
44
|
/** Unique field key (e.g., "Account_Balance__c") */
|
|
41
45
|
id: string
|
|
42
46
|
/** Display label (e.g., "Account Balance") */
|
|
43
47
|
label: string
|
|
44
48
|
/** Field data type — determines which operators are available and how the value input renders */
|
|
45
|
-
type: "text" | "number" | "currency" | "date"
|
|
49
|
+
type: "text" | "number" | "currency" | "date" | "select" | "multi_select"
|
|
46
50
|
/** Allowed operators for this field. Defaults based on type if not provided. */
|
|
47
51
|
operators?: ConditionOperator[]
|
|
52
|
+
/** Options used by select and multi-select fields. Strings use the same label and value. */
|
|
53
|
+
options?: ConditionFieldOption[]
|
|
54
|
+
/** Show a search box for option-backed value inputs. */
|
|
55
|
+
searchable?: boolean | { threshold?: number }
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
export interface ConditionFilterValue {
|
|
@@ -52,7 +60,7 @@ export interface ConditionFilterValue {
|
|
|
52
60
|
id: string
|
|
53
61
|
field: string
|
|
54
62
|
operator: ConditionOperator
|
|
55
|
-
value: string | number | null
|
|
63
|
+
value: string | number | string[] | null
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
interface DataTableConditionFilterProps {
|
|
@@ -74,7 +82,7 @@ const OPERATOR_LABELS: Record<ConditionOperator, string> = {
|
|
|
74
82
|
gte: "≥",
|
|
75
83
|
lt: "<",
|
|
76
84
|
lte: "≤",
|
|
77
|
-
in: "
|
|
85
|
+
in: "is any of",
|
|
78
86
|
is_null: "is empty",
|
|
79
87
|
is_not_null: "is not empty",
|
|
80
88
|
}
|
|
@@ -90,11 +98,29 @@ const NUMERIC_OPERATORS: ConditionOperator[] = [
|
|
|
90
98
|
"is_not_null",
|
|
91
99
|
]
|
|
92
100
|
|
|
101
|
+
const NAV_KEYS = new Set(["ArrowDown", "ArrowUp", "Enter", "Escape", "Tab"])
|
|
102
|
+
|
|
103
|
+
export function shouldShowOptionSearch(
|
|
104
|
+
searchable: ConditionFieldDef["searchable"],
|
|
105
|
+
optionCount: number,
|
|
106
|
+
defaultThreshold = 8,
|
|
107
|
+
): boolean {
|
|
108
|
+
if (searchable === true) return true
|
|
109
|
+
if (searchable === false) return false
|
|
110
|
+
const threshold =
|
|
111
|
+
typeof searchable === "object"
|
|
112
|
+
? (searchable.threshold ?? defaultThreshold)
|
|
113
|
+
: defaultThreshold
|
|
114
|
+
return optionCount > threshold
|
|
115
|
+
}
|
|
116
|
+
|
|
93
117
|
const DEFAULT_OPERATORS: Record<ConditionFieldDef["type"], ConditionOperator[]> = {
|
|
94
118
|
text: ["eq", "neq", "is_null", "is_not_null"],
|
|
95
119
|
number: NUMERIC_OPERATORS,
|
|
96
120
|
currency: NUMERIC_OPERATORS,
|
|
97
121
|
date: ["gt", "gte", "lt", "lte", "eq", "neq", "is_null", "is_not_null"],
|
|
122
|
+
select: ["eq", "neq", "is_null", "is_not_null"],
|
|
123
|
+
multi_select: ["in", "is_null", "is_not_null"],
|
|
98
124
|
}
|
|
99
125
|
|
|
100
126
|
/** Generate a stable unique ID for a new condition row. */
|
|
@@ -108,7 +134,7 @@ function generateConditionId(): string {
|
|
|
108
134
|
// ── Helpers ────────────────────────────────────────────────────
|
|
109
135
|
|
|
110
136
|
function getOperators(field: ConditionFieldDef): ConditionOperator[] {
|
|
111
|
-
return
|
|
137
|
+
return field.operators ?? DEFAULT_OPERATORS[field.type]
|
|
112
138
|
}
|
|
113
139
|
|
|
114
140
|
function isUnaryOperator(op: ConditionOperator): boolean {
|
|
@@ -128,6 +154,18 @@ function createDraftCondition(field: ConditionFieldDef): ConditionFilterValue {
|
|
|
128
154
|
}
|
|
129
155
|
}
|
|
130
156
|
|
|
157
|
+
function normalizeConditionValue(
|
|
158
|
+
value: ConditionFilterValue["value"],
|
|
159
|
+
field: ConditionFieldDef,
|
|
160
|
+
operator: ConditionOperator,
|
|
161
|
+
): ConditionFilterValue["value"] {
|
|
162
|
+
if (isUnaryOperator(operator)) return null
|
|
163
|
+
if (field.type === "multi_select") {
|
|
164
|
+
return Array.isArray(value) ? value : null
|
|
165
|
+
}
|
|
166
|
+
return Array.isArray(value) ? null : value
|
|
167
|
+
}
|
|
168
|
+
|
|
131
169
|
function normalizeCondition(
|
|
132
170
|
condition: ConditionFilterValue,
|
|
133
171
|
fields: ConditionFieldDef[],
|
|
@@ -144,7 +182,7 @@ function normalizeCondition(
|
|
|
144
182
|
...condition,
|
|
145
183
|
field: field.id,
|
|
146
184
|
operator,
|
|
147
|
-
value:
|
|
185
|
+
value: normalizeConditionValue(condition.value, field, operator),
|
|
148
186
|
}
|
|
149
187
|
}
|
|
150
188
|
|
|
@@ -168,18 +206,40 @@ function isCompleteCondition(
|
|
|
168
206
|
if (!field) return false
|
|
169
207
|
if (!getOperators(field).includes(condition.operator)) return false
|
|
170
208
|
if (isUnaryOperator(condition.operator)) return true
|
|
171
|
-
|
|
209
|
+
if (field.type === "multi_select") {
|
|
210
|
+
return Array.isArray(condition.value) && condition.value.length > 0
|
|
211
|
+
}
|
|
212
|
+
return condition.value !== null && condition.value !== "" && !Array.isArray(condition.value)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function getConditionValueSignature(value: ConditionFilterValue["value"]): string {
|
|
216
|
+
return Array.isArray(value) ? JSON.stringify(value) : String(value)
|
|
172
217
|
}
|
|
173
218
|
|
|
174
219
|
function getConditionsSignature(conditions: ConditionFilterValue[]): string {
|
|
175
220
|
return conditions
|
|
176
|
-
.map((condition) => `${condition.id}:${condition.field}:${condition.operator}:${
|
|
221
|
+
.map((condition) => `${condition.id}:${condition.field}:${condition.operator}:${getConditionValueSignature(condition.value)}`)
|
|
177
222
|
.join(";")
|
|
178
223
|
}
|
|
179
224
|
|
|
225
|
+
function normalizeFieldOptions(field: ConditionFieldDef): ConditionOptionObject[] {
|
|
226
|
+
return (field.options ?? []).map((option) =>
|
|
227
|
+
typeof option === "string" ? { label: option, value: option } : option,
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
180
231
|
function getFieldsSignature(fields: ConditionFieldDef[]): string {
|
|
181
232
|
return fields
|
|
182
|
-
.map((field) =>
|
|
233
|
+
.map((field) => {
|
|
234
|
+
const optionsSignature = normalizeFieldOptions(field)
|
|
235
|
+
.map((option) => `${option.label}:${option.value}`)
|
|
236
|
+
.join("|")
|
|
237
|
+
const searchSignature =
|
|
238
|
+
typeof field.searchable === "object"
|
|
239
|
+
? `threshold:${field.searchable.threshold ?? ""}`
|
|
240
|
+
: String(field.searchable ?? "")
|
|
241
|
+
return `${field.id}:${field.type}:${field.label}:${(field.operators ?? []).join("|")}:${optionsSignature}:${searchSignature}`
|
|
242
|
+
})
|
|
183
243
|
.join(";")
|
|
184
244
|
}
|
|
185
245
|
|
|
@@ -192,14 +252,180 @@ function getCommittedConditions(
|
|
|
192
252
|
.filter((condition) => isCompleteCondition(condition, fields))
|
|
193
253
|
}
|
|
194
254
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (
|
|
199
|
-
return
|
|
255
|
+
// ── Condition Row ──────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
function getInputType(fieldType: ConditionFieldDef["type"]): "text" | "number" | "date" {
|
|
258
|
+
if (fieldType === "number" || fieldType === "currency") return "number"
|
|
259
|
+
if (fieldType === "date") return "date"
|
|
260
|
+
return "text"
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getInputPlaceholder(fieldType: ConditionFieldDef["type"]): string {
|
|
264
|
+
if (fieldType === "currency") return "Amount"
|
|
265
|
+
if (fieldType === "number") return "Enter number..."
|
|
266
|
+
if (fieldType === "date") return ""
|
|
267
|
+
return "Enter value..."
|
|
200
268
|
}
|
|
201
269
|
|
|
202
|
-
|
|
270
|
+
interface ConditionValueInputProps {
|
|
271
|
+
condition: ConditionFilterValue
|
|
272
|
+
fieldDef: ConditionFieldDef
|
|
273
|
+
onValueChange: (event: React.ChangeEvent<HTMLInputElement>) => void
|
|
274
|
+
onSelectValueChange: (value: string) => void
|
|
275
|
+
onMultiSelectValueToggle: (value: string) => void
|
|
276
|
+
onCommit: () => void
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function SelectConditionValueInput({
|
|
280
|
+
condition,
|
|
281
|
+
fieldDef,
|
|
282
|
+
onSelectValueChange,
|
|
283
|
+
}: Pick<ConditionValueInputProps, "condition" | "fieldDef" | "onSelectValueChange">) {
|
|
284
|
+
const [query, setQuery] = React.useState("")
|
|
285
|
+
const options = normalizeFieldOptions(fieldDef)
|
|
286
|
+
const showSearch = shouldShowOptionSearch(fieldDef.searchable, options.length)
|
|
287
|
+
const normalizedQuery = showSearch ? query.trim().toLowerCase() : ""
|
|
288
|
+
const filteredOptions = normalizedQuery
|
|
289
|
+
? options.filter((option) => option.label.toLowerCase().includes(normalizedQuery))
|
|
290
|
+
: options
|
|
291
|
+
|
|
292
|
+
React.useEffect(() => {
|
|
293
|
+
setQuery("")
|
|
294
|
+
}, [fieldDef.id])
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<Select
|
|
298
|
+
value={typeof condition.value === "string" ? condition.value : ""}
|
|
299
|
+
onValueChange={onSelectValueChange}
|
|
300
|
+
>
|
|
301
|
+
<SelectTrigger className="h-8 w-full" size="sm">
|
|
302
|
+
<SelectValue placeholder="Select value..." />
|
|
303
|
+
</SelectTrigger>
|
|
304
|
+
<SelectContent>
|
|
305
|
+
{showSearch ? (
|
|
306
|
+
<div className="sticky top-0 z-10 border-b border-border bg-popover p-1.5">
|
|
307
|
+
<Input
|
|
308
|
+
value={query}
|
|
309
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
310
|
+
onClick={(event) => event.stopPropagation()}
|
|
311
|
+
onKeyDown={(event) => {
|
|
312
|
+
if (!NAV_KEYS.has(event.key)) {
|
|
313
|
+
event.stopPropagation()
|
|
314
|
+
}
|
|
315
|
+
}}
|
|
316
|
+
placeholder="Search options..."
|
|
317
|
+
className="h-7 text-xs"
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
) : null}
|
|
321
|
+
{filteredOptions.length > 0 ? (
|
|
322
|
+
filteredOptions.map((option) => (
|
|
323
|
+
<SelectItem key={option.value} value={option.value}>
|
|
324
|
+
{option.label}
|
|
325
|
+
</SelectItem>
|
|
326
|
+
))
|
|
327
|
+
) : (
|
|
328
|
+
<div className="px-2 py-1.5 text-xs text-muted-foreground">No options</div>
|
|
329
|
+
)}
|
|
330
|
+
</SelectContent>
|
|
331
|
+
</Select>
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function MultiSelectConditionValueInput({
|
|
336
|
+
condition,
|
|
337
|
+
fieldDef,
|
|
338
|
+
onMultiSelectValueToggle,
|
|
339
|
+
}: Pick<ConditionValueInputProps, "condition" | "fieldDef" | "onMultiSelectValueToggle">) {
|
|
340
|
+
const selectedValues = Array.isArray(condition.value) ? condition.value : []
|
|
341
|
+
const options = normalizeFieldOptions(fieldDef)
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<div className="max-h-28 overflow-y-auto rounded-md border border-border bg-background p-1">
|
|
345
|
+
{options.length > 0 ? (
|
|
346
|
+
options.map((option) => {
|
|
347
|
+
const checked = selectedValues.includes(option.value)
|
|
348
|
+
return (
|
|
349
|
+
<button
|
|
350
|
+
key={option.value}
|
|
351
|
+
type="button"
|
|
352
|
+
role="checkbox"
|
|
353
|
+
aria-checked={checked}
|
|
354
|
+
className={cn(
|
|
355
|
+
"flex w-full items-center gap-2 rounded-sm px-2 py-1 text-left text-xs hover:bg-muted",
|
|
356
|
+
checked && "text-brand-purple",
|
|
357
|
+
)}
|
|
358
|
+
onClick={(event) => {
|
|
359
|
+
event.stopPropagation()
|
|
360
|
+
onMultiSelectValueToggle(option.value)
|
|
361
|
+
}}
|
|
362
|
+
>
|
|
363
|
+
<span
|
|
364
|
+
className={cn(
|
|
365
|
+
"flex h-3.5 w-3.5 items-center justify-center rounded-sm border border-border",
|
|
366
|
+
checked && "border-brand-purple bg-brand-purple text-white",
|
|
367
|
+
)}
|
|
368
|
+
aria-hidden="true"
|
|
369
|
+
>
|
|
370
|
+
{checked ? <Check className="h-3 w-3" /> : null}
|
|
371
|
+
</span>
|
|
372
|
+
<span className="min-w-0 flex-1 truncate">{option.label}</span>
|
|
373
|
+
</button>
|
|
374
|
+
)
|
|
375
|
+
})
|
|
376
|
+
) : (
|
|
377
|
+
<div className="px-2 py-1 text-xs text-muted-foreground">No options</div>
|
|
378
|
+
)}
|
|
379
|
+
</div>
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function ScalarConditionValueInput({
|
|
384
|
+
condition,
|
|
385
|
+
fieldDef,
|
|
386
|
+
onValueChange,
|
|
387
|
+
onCommit,
|
|
388
|
+
}: Pick<ConditionValueInputProps, "condition" | "fieldDef" | "onValueChange" | "onCommit">) {
|
|
389
|
+
return (
|
|
390
|
+
<div className="relative flex items-center">
|
|
391
|
+
{fieldDef.type === "currency" ? (
|
|
392
|
+
<span className="pointer-events-none absolute left-2 text-sm text-muted-foreground">
|
|
393
|
+
$
|
|
394
|
+
</span>
|
|
395
|
+
) : null}
|
|
396
|
+
<Input
|
|
397
|
+
type={getInputType(fieldDef.type)}
|
|
398
|
+
value={
|
|
399
|
+
condition.value != null && !Array.isArray(condition.value)
|
|
400
|
+
? String(condition.value)
|
|
401
|
+
: ""
|
|
402
|
+
}
|
|
403
|
+
onChange={onValueChange}
|
|
404
|
+
onClick={(event) => event.stopPropagation()}
|
|
405
|
+
onKeyDown={(event) => {
|
|
406
|
+
event.stopPropagation()
|
|
407
|
+
if (event.key === "Enter") {
|
|
408
|
+
onCommit()
|
|
409
|
+
}
|
|
410
|
+
}}
|
|
411
|
+
placeholder={getInputPlaceholder(fieldDef.type)}
|
|
412
|
+
className={cn("h-8", fieldDef.type === "currency" && "pl-6")}
|
|
413
|
+
/>
|
|
414
|
+
</div>
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function ConditionValueInput(props: ConditionValueInputProps) {
|
|
419
|
+
if (props.fieldDef.type === "select") {
|
|
420
|
+
return <SelectConditionValueInput {...props} />
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (props.fieldDef.type === "multi_select") {
|
|
424
|
+
return <MultiSelectConditionValueInput {...props} />
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return <ScalarConditionValueInput {...props} />
|
|
428
|
+
}
|
|
203
429
|
|
|
204
430
|
interface ConditionRowProps {
|
|
205
431
|
condition: ConditionFilterValue
|
|
@@ -221,8 +447,6 @@ function ConditionRow({
|
|
|
221
447
|
const fieldDef = fields.find((f) => f.id === condition.field) ?? fields[0]
|
|
222
448
|
const operators = getOperators(fieldDef)
|
|
223
449
|
const isUnary = isUnaryOperator(condition.operator)
|
|
224
|
-
const FieldIcon = getFieldIcon(fieldDef.type)
|
|
225
|
-
|
|
226
450
|
const handleFieldChange = (newFieldId: string) => {
|
|
227
451
|
const newFieldDef = fields.find((f) => f.id === newFieldId) ?? fields[0]
|
|
228
452
|
if (!newFieldDef) return
|
|
@@ -238,7 +462,7 @@ function ConditionRow({
|
|
|
238
462
|
onChange({
|
|
239
463
|
...condition,
|
|
240
464
|
operator: newOp,
|
|
241
|
-
value:
|
|
465
|
+
value: normalizeConditionValue(condition.value, fieldDef, newOp),
|
|
242
466
|
})
|
|
243
467
|
}
|
|
244
468
|
|
|
@@ -249,6 +473,26 @@ function ConditionRow({
|
|
|
249
473
|
})
|
|
250
474
|
}
|
|
251
475
|
|
|
476
|
+
const handleSelectValueChange = (value: string) => {
|
|
477
|
+
onChange({
|
|
478
|
+
...condition,
|
|
479
|
+
value,
|
|
480
|
+
})
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const handleMultiSelectValueToggle = (value: string) => {
|
|
484
|
+
const currentValues = Array.isArray(condition.value) ? condition.value : []
|
|
485
|
+
const nextValues = currentValues.includes(value)
|
|
486
|
+
? currentValues.filter((currentValue) => currentValue !== value)
|
|
487
|
+
: [...currentValues, value]
|
|
488
|
+
|
|
489
|
+
onChange({
|
|
490
|
+
...condition,
|
|
491
|
+
value: nextValues,
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
|
|
252
496
|
return (
|
|
253
497
|
<div
|
|
254
498
|
className="grid grid-cols-[52px_minmax(150px,1fr)_120px_minmax(140px,1fr)_auto] items-center gap-2 rounded-lg border border-border/70 bg-background p-2 shadow-sm"
|
|
@@ -260,21 +504,14 @@ function ConditionRow({
|
|
|
260
504
|
|
|
261
505
|
<Select value={condition.field} onValueChange={handleFieldChange}>
|
|
262
506
|
<SelectTrigger className="h-8 w-full justify-start gap-2" size="sm">
|
|
263
|
-
<FieldIcon className="h-3.5 w-3.5 text-muted-foreground" />
|
|
264
507
|
<SelectValue placeholder={fieldDef.label} />
|
|
265
508
|
</SelectTrigger>
|
|
266
509
|
<SelectContent>
|
|
267
|
-
{fields.map((field) =>
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
<Icon className="h-3.5 w-3.5 text-muted-foreground" />
|
|
273
|
-
{field.label}
|
|
274
|
-
</span>
|
|
275
|
-
</SelectItem>
|
|
276
|
-
)
|
|
277
|
-
})}
|
|
510
|
+
{fields.map((field) => (
|
|
511
|
+
<SelectItem key={field.id} value={field.id}>
|
|
512
|
+
{field.label}
|
|
513
|
+
</SelectItem>
|
|
514
|
+
))}
|
|
278
515
|
</SelectContent>
|
|
279
516
|
</Select>
|
|
280
517
|
|
|
@@ -299,41 +536,14 @@ function ConditionRow({
|
|
|
299
536
|
No value needed
|
|
300
537
|
</div>
|
|
301
538
|
) : (
|
|
302
|
-
<
|
|
303
|
-
{
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
fieldDef.type === "number" || fieldDef.type === "currency"
|
|
311
|
-
? "number"
|
|
312
|
-
: fieldDef.type === "date"
|
|
313
|
-
? "date"
|
|
314
|
-
: "text"
|
|
315
|
-
}
|
|
316
|
-
value={condition.value != null ? String(condition.value) : ""}
|
|
317
|
-
onChange={handleValueChange}
|
|
318
|
-
onClick={(event) => event.stopPropagation()}
|
|
319
|
-
onKeyDown={(event) => {
|
|
320
|
-
event.stopPropagation()
|
|
321
|
-
if (event.key === "Enter") {
|
|
322
|
-
onCommit()
|
|
323
|
-
}
|
|
324
|
-
}}
|
|
325
|
-
placeholder={
|
|
326
|
-
fieldDef.type === "currency"
|
|
327
|
-
? "Amount"
|
|
328
|
-
: fieldDef.type === "number"
|
|
329
|
-
? "Enter number..."
|
|
330
|
-
: fieldDef.type === "date"
|
|
331
|
-
? ""
|
|
332
|
-
: "Enter value..."
|
|
333
|
-
}
|
|
334
|
-
className={cn("h-8", fieldDef.type === "currency" && "pl-6")}
|
|
335
|
-
/>
|
|
336
|
-
</div>
|
|
539
|
+
<ConditionValueInput
|
|
540
|
+
condition={condition}
|
|
541
|
+
fieldDef={fieldDef}
|
|
542
|
+
onValueChange={handleValueChange}
|
|
543
|
+
onSelectValueChange={handleSelectValueChange}
|
|
544
|
+
onMultiSelectValueToggle={handleMultiSelectValueToggle}
|
|
545
|
+
onCommit={onCommit}
|
|
546
|
+
/>
|
|
337
547
|
)}
|
|
338
548
|
|
|
339
549
|
<div className="flex items-center gap-1">
|
|
@@ -9,6 +9,7 @@ import { cn } from "../lib/utils"
|
|
|
9
9
|
import { Button } from "./button"
|
|
10
10
|
import {
|
|
11
11
|
DataTableConditionFilter,
|
|
12
|
+
shouldShowOptionSearch,
|
|
12
13
|
type ConditionFieldDef,
|
|
13
14
|
type ConditionFilterValue,
|
|
14
15
|
} from "./data-table-condition-filter"
|
|
@@ -214,15 +215,11 @@ export function DataTableFilter({
|
|
|
214
215
|
getOptionLabel(opt).toLowerCase().includes(subQuery)
|
|
215
216
|
)
|
|
216
217
|
: category.options
|
|
217
|
-
const shouldShowSubmenuSearch = (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
? (category.searchable.threshold ?? optionSearchThreshold)
|
|
223
|
-
: optionSearchThreshold
|
|
224
|
-
return category.options.length > threshold
|
|
225
|
-
})()
|
|
218
|
+
const shouldShowSubmenuSearch = shouldShowOptionSearch(
|
|
219
|
+
category.searchable,
|
|
220
|
+
category.options.length,
|
|
221
|
+
optionSearchThreshold,
|
|
222
|
+
)
|
|
226
223
|
|
|
227
224
|
return (
|
|
228
225
|
<DropdownMenuSub
|
|
@@ -135,14 +135,16 @@ export function EntityPanelHeader({
|
|
|
135
135
|
icon,
|
|
136
136
|
title,
|
|
137
137
|
badgeLabel,
|
|
138
|
-
subtitle
|
|
138
|
+
subtitle,
|
|
139
139
|
headerAction,
|
|
140
|
+
headerSecondaryAction,
|
|
140
141
|
}: {
|
|
141
142
|
icon?: React.ReactNode
|
|
142
143
|
title: string
|
|
143
144
|
badgeLabel?: string
|
|
144
145
|
subtitle?: string
|
|
145
146
|
headerAction?: React.ReactNode
|
|
147
|
+
headerSecondaryAction?: React.ReactNode
|
|
146
148
|
}) {
|
|
147
149
|
const { panelMode, cyclePanelMode, onClose } = useEntityPanel()
|
|
148
150
|
|
|
@@ -150,49 +152,63 @@ export function EntityPanelHeader({
|
|
|
150
152
|
panelMode === 'default' ? 'Wide' : panelMode === 'wide' ? 'Fullscreen' : 'Exit fullscreen'
|
|
151
153
|
|
|
152
154
|
return (
|
|
153
|
-
<div className="
|
|
154
|
-
<div className="flex items-center
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
155
|
+
<div className="mb-3 space-y-2">
|
|
156
|
+
<div className="flex items-center justify-between">
|
|
157
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
158
|
+
{icon ?? <CalendarDays className="w-5 h-5 text-muted-foreground shrink-0" />}
|
|
159
|
+
<h2 className="text-[16px] font-semibold text-foreground truncate">{title}</h2>
|
|
160
|
+
{badgeLabel && (
|
|
161
|
+
<Badge
|
|
162
|
+
variant="outline"
|
|
163
|
+
className="text-blue-600 border-blue-300 dark:border-blue-700 dark:text-blue-400 shadow-none px-2 py-0.5 text-[11px] font-medium shrink-0"
|
|
164
|
+
>
|
|
165
|
+
{badgeLabel}
|
|
166
|
+
</Badge>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
<div className="flex items-center gap-1 shrink-0 ml-4 text-muted-foreground">
|
|
170
|
+
{headerAction}
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
className="p-1.5 rounded-md hover:bg-secondary transition-colors"
|
|
174
|
+
title="Copy Link"
|
|
175
|
+
>
|
|
176
|
+
<LinkIcon className="w-4 h-4" />
|
|
177
|
+
</button>
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
onClick={cyclePanelMode}
|
|
181
|
+
className="p-1.5 rounded-md hover:bg-secondary transition-colors"
|
|
182
|
+
title={sizeButtonTitle}
|
|
161
183
|
>
|
|
162
|
-
{
|
|
163
|
-
|
|
164
|
-
|
|
184
|
+
{panelMode === 'fullscreen' ? (
|
|
185
|
+
<Minimize2 className="w-4 h-4" />
|
|
186
|
+
) : (
|
|
187
|
+
<Maximize2 className="w-4 h-4" />
|
|
188
|
+
)}
|
|
189
|
+
</button>
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
onClick={onClose}
|
|
193
|
+
className="p-1.5 rounded-md hover:bg-secondary transition-colors"
|
|
194
|
+
title="Close"
|
|
195
|
+
>
|
|
196
|
+
<X className="w-4 h-4" />
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
165
199
|
</div>
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
className="p-1.5 rounded-md hover:bg-secondary transition-colors"
|
|
171
|
-
title="Copy Link"
|
|
172
|
-
>
|
|
173
|
-
<LinkIcon className="w-4 h-4" />
|
|
174
|
-
</button>
|
|
175
|
-
<button
|
|
176
|
-
type="button"
|
|
177
|
-
onClick={cyclePanelMode}
|
|
178
|
-
className="p-1.5 rounded-md hover:bg-secondary transition-colors"
|
|
179
|
-
title={sizeButtonTitle}
|
|
180
|
-
>
|
|
181
|
-
{panelMode === 'fullscreen' ? (
|
|
182
|
-
<Minimize2 className="w-4 h-4" />
|
|
200
|
+
{(subtitle || headerSecondaryAction) && (
|
|
201
|
+
<div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-2">
|
|
202
|
+
{subtitle ? (
|
|
203
|
+
<p className="min-w-0 flex-1 text-xs text-muted-foreground">{subtitle}</p>
|
|
183
204
|
) : (
|
|
184
|
-
<
|
|
205
|
+
<div className="min-w-0 flex-1" />
|
|
185
206
|
)}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
title="Close"
|
|
192
|
-
>
|
|
193
|
-
<X className="w-4 h-4" />
|
|
194
|
-
</button>
|
|
195
|
-
</div>
|
|
207
|
+
{headerSecondaryAction ? (
|
|
208
|
+
<div className="flex shrink-0 items-center gap-2">{headerSecondaryAction}</div>
|
|
209
|
+
) : null}
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
196
212
|
</div>
|
|
197
213
|
)
|
|
198
214
|
}
|
|
@@ -40,8 +40,12 @@ export interface PriorityFactor {
|
|
|
40
40
|
tone: "alert" | "warn" | "info"
|
|
41
41
|
/** Explicit semantic label - NOT inferred from score+weight. */
|
|
42
42
|
direction: "raises" | "lowers" | "neutral"
|
|
43
|
+
/** Optional display label for the direction text. Keeps semantic direction icon/color unchanged. */
|
|
44
|
+
directionLabel?: string
|
|
43
45
|
/** 0-100 */
|
|
44
46
|
score: number
|
|
47
|
+
/** Optional display label rendered instead of the numeric score cell. */
|
|
48
|
+
displayValueLabel?: string
|
|
45
49
|
/** Evidence text (e.g. "$3.4M moved in 8h - current treasury balance $0.00"). */
|
|
46
50
|
rationale: string
|
|
47
51
|
}
|
|
@@ -168,12 +172,13 @@ function PriorityFactorRow({ factor, initialFeedback, onFactorFeedback }: Priori
|
|
|
168
172
|
const IconComponent = FACTOR_ICONS[factor.icon] ?? Activity
|
|
169
173
|
const toneClasses = TONE_ICON_CLASSES[factor.tone]
|
|
170
174
|
const directionClasses = DIRECTION_CLASSES[factor.direction]
|
|
171
|
-
const directionLabel =
|
|
175
|
+
const directionLabel = factor.directionLabel ?? (
|
|
172
176
|
factor.direction === "raises"
|
|
173
177
|
? "Raises"
|
|
174
178
|
: factor.direction === "lowers"
|
|
175
179
|
? "Lowers"
|
|
176
180
|
: "Neutral"
|
|
181
|
+
)
|
|
177
182
|
|
|
178
183
|
return (
|
|
179
184
|
<div
|
|
@@ -206,10 +211,16 @@ function PriorityFactorRow({ factor, initialFeedback, onFactorFeedback }: Priori
|
|
|
206
211
|
</span>
|
|
207
212
|
</div>
|
|
208
213
|
|
|
209
|
-
{/* Score number */}
|
|
214
|
+
{/* Score number / display label */}
|
|
210
215
|
<div className="flex items-center text-right">
|
|
211
|
-
|
|
212
|
-
|
|
216
|
+
{factor.displayValueLabel ? (
|
|
217
|
+
<span className="text-xs font-semibold text-foreground">{factor.displayValueLabel}</span>
|
|
218
|
+
) : (
|
|
219
|
+
<>
|
|
220
|
+
<span className="text-sm font-bold tabular-nums">{factor.score}</span>
|
|
221
|
+
<span className="text-xs font-normal text-muted-foreground">/100</span>
|
|
222
|
+
</>
|
|
223
|
+
)}
|
|
213
224
|
</div>
|
|
214
225
|
|
|
215
226
|
{/* empty grid cell under icon column */}
|