@proteos/ui 0.52.0 → 0.54.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.
- package/dist/index.cjs +107 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +108 -16
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/attribute-type-icon/attribute-type-icon.test.tsx +12 -0
- package/src/components/attribute-type-icon/attribute-type-icon.tsx +6 -0
- package/src/components/radio-group/radio-group.tsx +5 -1
- package/src/components/record-filter/operators.test.ts +20 -2
- package/src/components/record-filter/operators.ts +93 -11
- package/src/components/switch/switch.tsx +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteos/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
4
4
|
"license": "FSL-1.1-Apache-2.0",
|
|
5
5
|
"description": "Proteos React component library (React Aria Components + Tailwind v4).",
|
|
6
6
|
"repository": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"tailwind-variants": "^3.2.2",
|
|
57
57
|
"tailwindcss": "^4.2.4",
|
|
58
58
|
"thinking-orbs": "^0.3.1",
|
|
59
|
-
"@proteos/sdk": "0.
|
|
59
|
+
"@proteos/sdk": "0.54.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@biomejs/biome": "^2.4.13",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { ATTRIBUTE_TYPE_ICONS, ATTRIBUTE_TYPES } from './attribute-type-icon'
|
|
4
|
+
|
|
5
|
+
describe('ATTRIBUTE_TYPE_ICONS', () => {
|
|
6
|
+
it('maps every attribute type to an icon (a missing entry crashes the type picker at runtime)', () => {
|
|
7
|
+
for (const type of ATTRIBUTE_TYPES) {
|
|
8
|
+
expect(ATTRIBUTE_TYPE_ICONS[type], `no icon for ${type}`).toBeTypeOf('object')
|
|
9
|
+
}
|
|
10
|
+
expect(ATTRIBUTE_TYPES).toContain('contact-address')
|
|
11
|
+
})
|
|
12
|
+
})
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Brackets,
|
|
6
6
|
Calendar,
|
|
7
7
|
CircleDot,
|
|
8
|
+
Contact,
|
|
8
9
|
Hash,
|
|
9
10
|
Link2,
|
|
10
11
|
type LucideIcon,
|
|
@@ -55,6 +56,9 @@ import { cn } from '../../lib/cn'
|
|
|
55
56
|
currency → Banknote (money — amount + currency_code)
|
|
56
57
|
knowledge-text → BookText (long text stored as a knowledge node)
|
|
57
58
|
file → Paperclip (attachment — stored-file reference)
|
|
59
|
+
contact-address → Contact (one canonical reachable address — email /
|
|
60
|
+
phone / LinkedIn — binding the record to a
|
|
61
|
+
conversation contact)
|
|
58
62
|
========================================================================= */
|
|
59
63
|
|
|
60
64
|
/**
|
|
@@ -81,6 +85,7 @@ export const AttributeType = {
|
|
|
81
85
|
Currency: 'currency',
|
|
82
86
|
KnowledgeText: 'knowledge-text',
|
|
83
87
|
File: 'file',
|
|
88
|
+
ContactAddress: 'contact-address',
|
|
84
89
|
} as const
|
|
85
90
|
export type AttributeType = (typeof AttributeType)[keyof typeof AttributeType]
|
|
86
91
|
|
|
@@ -102,6 +107,7 @@ export const ATTRIBUTE_TYPE_ICONS: Record<AttributeType, LucideIcon> = {
|
|
|
102
107
|
[AttributeType.Currency]: Banknote,
|
|
103
108
|
[AttributeType.KnowledgeText]: BookText,
|
|
104
109
|
[AttributeType.File]: Paperclip,
|
|
110
|
+
[AttributeType.ContactAddress]: Contact,
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
export function useAttributeTypeIcon(type: AttributeType): LucideIcon {
|
|
@@ -72,7 +72,11 @@ export function Radio({ children, className, ...props }: RadioProps) {
|
|
|
72
72
|
{...props}
|
|
73
73
|
className={(renderProps) =>
|
|
74
74
|
cn(
|
|
75
|
-
|
|
75
|
+
// `relative` for the same reason as Checkbox / Switch: RAC's
|
|
76
|
+
// visually-hidden <input> is absolutely positioned and must resolve
|
|
77
|
+
// against this radio, not a far-away shell, or focusing it scrolls
|
|
78
|
+
// the clipped page.
|
|
79
|
+
'group relative inline-flex items-center gap-2.5 cursor-pointer',
|
|
76
80
|
'font-sans text-[14px] text-ink-2 select-none',
|
|
77
81
|
'data-[disabled]:opacity-45 data-[disabled]:cursor-not-allowed',
|
|
78
82
|
typeof className === 'function' ? className(renderProps) : className,
|
|
@@ -11,13 +11,31 @@ describe('operatorsForType', () => {
|
|
|
11
11
|
}
|
|
12
12
|
})
|
|
13
13
|
|
|
14
|
-
it('only allows starts_with / ends_with on
|
|
14
|
+
it('only allows starts_with / ends_with on string-like types', () => {
|
|
15
15
|
for (const op of ['starts_with', 'ends_with'] as const) {
|
|
16
16
|
const types = OPERATORS.find((o) => o.id === op)?.types ?? []
|
|
17
|
-
expect(types).toEqual(['string'])
|
|
17
|
+
expect(types).toEqual(['string', 'contact-address'])
|
|
18
18
|
}
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
+
it('treats contact-address like a string (one canonical value)', () => {
|
|
22
|
+
const ops = operatorsForType('contact-address').map((o) => o.id)
|
|
23
|
+
for (const expected of [
|
|
24
|
+
'eq',
|
|
25
|
+
'ne',
|
|
26
|
+
'in',
|
|
27
|
+
'not_in',
|
|
28
|
+
'contains',
|
|
29
|
+
'starts_with',
|
|
30
|
+
'ends_with',
|
|
31
|
+
'empty',
|
|
32
|
+
'not_empty',
|
|
33
|
+
]) {
|
|
34
|
+
expect(ops).toContain(expected)
|
|
35
|
+
}
|
|
36
|
+
expect(ops).not.toContain('gt')
|
|
37
|
+
})
|
|
38
|
+
|
|
21
39
|
it('allows in / not_in on strings, numbers, and enums (not datetime/boolean/array)', () => {
|
|
22
40
|
for (const op of ['in', 'not_in'] as const) {
|
|
23
41
|
const types = new Set(OPERATORS.find((o) => o.id === op)?.types ?? [])
|
|
@@ -18,44 +18,126 @@ export const OPERATORS: ReadonlyArray<OperatorDescriptor> = [
|
|
|
18
18
|
id: 'eq',
|
|
19
19
|
label: 'is',
|
|
20
20
|
arity: 1,
|
|
21
|
-
types: [
|
|
21
|
+
types: [
|
|
22
|
+
'string',
|
|
23
|
+
'number',
|
|
24
|
+
'integer',
|
|
25
|
+
'currency',
|
|
26
|
+
'datetime',
|
|
27
|
+
'enum',
|
|
28
|
+
'boolean',
|
|
29
|
+
'relation',
|
|
30
|
+
'user',
|
|
31
|
+
'principal',
|
|
32
|
+
'contact-address',
|
|
33
|
+
],
|
|
22
34
|
},
|
|
23
35
|
{
|
|
24
36
|
id: 'ne',
|
|
25
37
|
label: 'is not',
|
|
26
38
|
arity: 1,
|
|
27
|
-
types: [
|
|
39
|
+
types: [
|
|
40
|
+
'string',
|
|
41
|
+
'number',
|
|
42
|
+
'integer',
|
|
43
|
+
'currency',
|
|
44
|
+
'datetime',
|
|
45
|
+
'enum',
|
|
46
|
+
'boolean',
|
|
47
|
+
'relation',
|
|
48
|
+
'user',
|
|
49
|
+
'principal',
|
|
50
|
+
'contact-address',
|
|
51
|
+
],
|
|
28
52
|
},
|
|
29
53
|
{
|
|
30
54
|
id: 'in',
|
|
31
55
|
label: 'is any of',
|
|
32
56
|
arity: 'n',
|
|
33
|
-
types: [
|
|
57
|
+
types: [
|
|
58
|
+
'string',
|
|
59
|
+
'number',
|
|
60
|
+
'integer',
|
|
61
|
+
'currency',
|
|
62
|
+
'enum',
|
|
63
|
+
'relation',
|
|
64
|
+
'user',
|
|
65
|
+
'principal',
|
|
66
|
+
'contact-address',
|
|
67
|
+
],
|
|
34
68
|
},
|
|
35
69
|
{
|
|
36
70
|
id: 'not_in',
|
|
37
71
|
label: 'is none of',
|
|
38
72
|
arity: 'n',
|
|
39
|
-
types: [
|
|
73
|
+
types: [
|
|
74
|
+
'string',
|
|
75
|
+
'number',
|
|
76
|
+
'integer',
|
|
77
|
+
'currency',
|
|
78
|
+
'enum',
|
|
79
|
+
'relation',
|
|
80
|
+
'user',
|
|
81
|
+
'principal',
|
|
82
|
+
'contact-address',
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{ id: 'contains', label: 'contains', arity: 1, types: ['string', 'array', 'contact-address'] },
|
|
86
|
+
{ id: 'starts_with', label: 'starts with', arity: 1, types: ['string', 'contact-address'] },
|
|
87
|
+
{ id: 'ends_with', label: 'ends with', arity: 1, types: ['string', 'contact-address'] },
|
|
88
|
+
{
|
|
89
|
+
id: 'gt',
|
|
90
|
+
label: 'is greater than',
|
|
91
|
+
arity: 1,
|
|
92
|
+
types: ['number', 'integer', 'currency', 'datetime'],
|
|
40
93
|
},
|
|
41
|
-
{ id: 'contains', label: 'contains', arity: 1, types: ['string', 'array'] },
|
|
42
|
-
{ id: 'starts_with', label: 'starts with', arity: 1, types: ['string'] },
|
|
43
|
-
{ id: 'ends_with', label: 'ends with', arity: 1, types: ['string'] },
|
|
44
|
-
{ id: 'gt', label: 'is greater than', arity: 1, types: ['number', 'integer', 'currency', 'datetime'] },
|
|
45
94
|
{ id: 'gte', label: 'is on or after', arity: 1, types: ['datetime'] },
|
|
46
|
-
{
|
|
95
|
+
{
|
|
96
|
+
id: 'lt',
|
|
97
|
+
label: 'is less than',
|
|
98
|
+
arity: 1,
|
|
99
|
+
types: ['number', 'integer', 'currency', 'datetime'],
|
|
100
|
+
},
|
|
47
101
|
{ id: 'lte', label: 'is on or before', arity: 1, types: ['datetime'] },
|
|
48
102
|
{
|
|
49
103
|
id: 'empty',
|
|
50
104
|
label: 'is empty',
|
|
51
105
|
arity: 0,
|
|
52
|
-
types: [
|
|
106
|
+
types: [
|
|
107
|
+
'string',
|
|
108
|
+
'number',
|
|
109
|
+
'integer',
|
|
110
|
+
'currency',
|
|
111
|
+
'datetime',
|
|
112
|
+
'enum',
|
|
113
|
+
'boolean',
|
|
114
|
+
'array',
|
|
115
|
+
'relation',
|
|
116
|
+
'user',
|
|
117
|
+
'principal',
|
|
118
|
+
'file',
|
|
119
|
+
'contact-address',
|
|
120
|
+
],
|
|
53
121
|
},
|
|
54
122
|
{
|
|
55
123
|
id: 'not_empty',
|
|
56
124
|
label: 'is not empty',
|
|
57
125
|
arity: 0,
|
|
58
|
-
types: [
|
|
126
|
+
types: [
|
|
127
|
+
'string',
|
|
128
|
+
'number',
|
|
129
|
+
'integer',
|
|
130
|
+
'currency',
|
|
131
|
+
'datetime',
|
|
132
|
+
'enum',
|
|
133
|
+
'boolean',
|
|
134
|
+
'array',
|
|
135
|
+
'relation',
|
|
136
|
+
'user',
|
|
137
|
+
'principal',
|
|
138
|
+
'file',
|
|
139
|
+
'contact-address',
|
|
140
|
+
],
|
|
59
141
|
},
|
|
60
142
|
]
|
|
61
143
|
|
|
@@ -15,7 +15,11 @@ export function Switch({ children, labelPlacement = 'end', className, ...props }
|
|
|
15
15
|
{...props}
|
|
16
16
|
className={(renderProps) =>
|
|
17
17
|
cn(
|
|
18
|
-
'
|
|
18
|
+
// `relative`: RAC's visually-hidden <input> is absolutely positioned;
|
|
19
|
+
// without a positioned label it escapes an overflow-auto ancestor and
|
|
20
|
+
// its containing block becomes the page shell, so focusing it (any
|
|
21
|
+
// press) scrolls the clipped shell and the whole tab goes blank.
|
|
22
|
+
'group relative inline-flex items-center gap-3 cursor-pointer select-none',
|
|
19
23
|
'font-sans text-[14px] text-ink-2',
|
|
20
24
|
labelPlacement === 'start' && 'flex-row-reverse',
|
|
21
25
|
'data-[disabled]:opacity-45 data-[disabled]:cursor-not-allowed',
|