@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteos/ui",
3
- "version": "0.52.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.52.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
- 'group inline-flex items-center gap-2.5 cursor-pointer',
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 strings', () => {
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: ['string', 'number', 'integer', 'currency', 'datetime', 'enum', 'boolean', 'relation', 'user', 'principal'],
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: ['string', 'number', 'integer', 'currency', 'datetime', 'enum', 'boolean', 'relation', 'user', 'principal'],
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: ['string', 'number', 'integer', 'currency', 'enum', 'relation', 'user', 'principal'],
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: ['string', 'number', 'integer', 'currency', 'enum', 'relation', 'user', 'principal'],
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
- { id: 'lt', label: 'is less than', arity: 1, types: ['number', 'integer', 'currency', 'datetime'] },
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: ['string', 'number', 'integer', 'currency', 'datetime', 'enum', 'boolean', 'array', 'relation', 'user', 'principal', 'file'],
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: ['string', 'number', 'integer', 'currency', 'datetime', 'enum', 'boolean', 'array', 'relation', 'user', 'principal', 'file'],
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
- 'group inline-flex items-center gap-3 cursor-pointer select-none',
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',