@adatechnology/conversations-ui 0.6.0 → 0.6.1
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.js +5 -5
- package/dist/styles.css +8 -0
- package/package.json +3 -3
- package/src/cssBlocks.test-helper.ts +31 -0
- package/src/legacyStyles.golden.test.ts +2 -31
- package/src/participantLayoutShrink.test.ts +82 -0
- package/src/styles.css +3 -0
package/dist/index.js
CHANGED
|
@@ -3,11 +3,6 @@ import {
|
|
|
3
3
|
TooltipLayer,
|
|
4
4
|
WhatsAppMessagePreview
|
|
5
5
|
} from "./chunk-HOKNKXIT.js";
|
|
6
|
-
import {
|
|
7
|
-
CHAT_TEXT_PRIMARY_CLASS,
|
|
8
|
-
CHAT_TEXT_SECONDARY_CLASS,
|
|
9
|
-
MessageText
|
|
10
|
-
} from "./chunk-OVFCLRQQ.js";
|
|
11
6
|
import {
|
|
12
7
|
AudioPlayer,
|
|
13
8
|
AudioRecorderButton,
|
|
@@ -70,6 +65,11 @@ import {
|
|
|
70
65
|
useDarkMode,
|
|
71
66
|
useIsDarkTheme
|
|
72
67
|
} from "./chunk-MRVFFJEH.js";
|
|
68
|
+
import {
|
|
69
|
+
CHAT_TEXT_PRIMARY_CLASS,
|
|
70
|
+
CHAT_TEXT_SECONDARY_CLASS,
|
|
71
|
+
MessageText
|
|
72
|
+
} from "./chunk-OVFCLRQQ.js";
|
|
73
73
|
import {
|
|
74
74
|
CHANNEL_CAPABILITIES,
|
|
75
75
|
CHANNEL_FILTER_ALL,
|
package/dist/styles.css
CHANGED
|
@@ -1928,6 +1928,14 @@
|
|
|
1928
1928
|
transition: background-color 0.12s, border-color 0.12s;
|
|
1929
1929
|
}
|
|
1930
1930
|
}
|
|
1931
|
+
.cv-p-filters,
|
|
1932
|
+
.cv-p-quick,
|
|
1933
|
+
.cv-p-search,
|
|
1934
|
+
.cv-p-composer,
|
|
1935
|
+
.cv-p-thread__header,
|
|
1936
|
+
.cv-p-thread__subject-card {
|
|
1937
|
+
flex: 0 0 auto;
|
|
1938
|
+
}
|
|
1931
1939
|
.cv-p-root {
|
|
1932
1940
|
display: flex;
|
|
1933
1941
|
flex-direction: column;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adatechnology/conversations-ui",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "WhatsApp conversation UI components — parametrizável por endpoint, tema e feature flags",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"clsx": "^2.1.1",
|
|
36
36
|
"lucide-react": "^1.21.0",
|
|
37
37
|
"tailwind-merge": "^3.6.0",
|
|
38
|
-
"@adatechnology/
|
|
39
|
-
"@adatechnology/
|
|
38
|
+
"@adatechnology/meta-whatsapp-contracts": "0.6.0",
|
|
39
|
+
"@adatechnology/conversation-contracts": "0.5.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@xyflow/react": "^12",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type CssBlock = { readonly prelude: string; readonly body: string | undefined }
|
|
2
|
+
|
|
3
|
+
export function splitBlocks(css: string): CssBlock[] {
|
|
4
|
+
const blocks: CssBlock[] = []
|
|
5
|
+
let depth = 0
|
|
6
|
+
let start = 0
|
|
7
|
+
let bodyStart = 0
|
|
8
|
+
let prelude = ''
|
|
9
|
+
for (let index = 0; index < css.length; index += 1) {
|
|
10
|
+
const char = css[index]
|
|
11
|
+
if (depth === 0 && char === ';') {
|
|
12
|
+
blocks.push({ prelude: css.slice(start, index).trim(), body: undefined })
|
|
13
|
+
start = index + 1
|
|
14
|
+
}
|
|
15
|
+
if (char === '{') {
|
|
16
|
+
if (depth === 0) {
|
|
17
|
+
prelude = css.slice(start, index).trim()
|
|
18
|
+
bodyStart = index + 1
|
|
19
|
+
}
|
|
20
|
+
depth += 1
|
|
21
|
+
}
|
|
22
|
+
if (char === '}') {
|
|
23
|
+
depth -= 1
|
|
24
|
+
if (depth === 0) {
|
|
25
|
+
blocks.push({ prelude, body: css.slice(bodyStart, index) })
|
|
26
|
+
start = index + 1
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return blocks
|
|
31
|
+
}
|
|
@@ -2,6 +2,8 @@ import { readFileSync } from 'node:fs'
|
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { describe, expect, it } from 'bun:test'
|
|
4
4
|
|
|
5
|
+
import { splitBlocks } from './cssBlocks.test-helper'
|
|
6
|
+
|
|
5
7
|
// legacyStyles.fixture.css is the styles.css shipped by 0.4.2; the stylesheet is global, so anything
|
|
6
8
|
// added since must be namespaced under .cv- and must not reach the host's own elements.
|
|
7
9
|
const LEGACY_CSS = readFileSync(join(import.meta.dir, 'legacyStyles.fixture.css'), 'utf8')
|
|
@@ -10,39 +12,8 @@ const CURRENT_CSS = readFileSync(join(import.meta.dir, 'styles.css'), 'utf8')
|
|
|
10
12
|
const NESTED_AT_RULES = ['@media', '@supports']
|
|
11
13
|
const ALLOWED_NEW_AT_RULES = ['@media', '@supports', '@keyframes']
|
|
12
14
|
|
|
13
|
-
type Block = { readonly prelude: string; readonly body: string | undefined }
|
|
14
15
|
type Inventory = { selectors: string[]; atRules: string[]; keyframeNames: string[] }
|
|
15
16
|
|
|
16
|
-
function splitBlocks(css: string): Block[] {
|
|
17
|
-
const blocks: Block[] = []
|
|
18
|
-
let depth = 0
|
|
19
|
-
let start = 0
|
|
20
|
-
let bodyStart = 0
|
|
21
|
-
let prelude = ''
|
|
22
|
-
for (let index = 0; index < css.length; index += 1) {
|
|
23
|
-
const char = css[index]
|
|
24
|
-
if (depth === 0 && char === ';') {
|
|
25
|
-
blocks.push({ prelude: css.slice(start, index).trim(), body: undefined })
|
|
26
|
-
start = index + 1
|
|
27
|
-
}
|
|
28
|
-
if (char === '{') {
|
|
29
|
-
if (depth === 0) {
|
|
30
|
-
prelude = css.slice(start, index).trim()
|
|
31
|
-
bodyStart = index + 1
|
|
32
|
-
}
|
|
33
|
-
depth += 1
|
|
34
|
-
}
|
|
35
|
-
if (char === '}') {
|
|
36
|
-
depth -= 1
|
|
37
|
-
if (depth === 0) {
|
|
38
|
-
blocks.push({ prelude, body: css.slice(bodyStart, index) })
|
|
39
|
-
start = index + 1
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return blocks
|
|
44
|
-
}
|
|
45
|
-
|
|
46
17
|
function splitSelectorList(prelude: string): string[] {
|
|
47
18
|
const parts: string[] = []
|
|
48
19
|
let parenthesisDepth = 0
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { describe, expect, it } from 'bun:test'
|
|
4
|
+
|
|
5
|
+
import { splitBlocks } from './cssBlocks.test-helper'
|
|
6
|
+
|
|
7
|
+
const CSS = readFileSync(join(import.meta.dir, 'styles.css'), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
8
|
+
|
|
9
|
+
const NON_SHRINKING_ITEMS = [
|
|
10
|
+
'.cv-p-filters',
|
|
11
|
+
'.cv-p-quick',
|
|
12
|
+
'.cv-p-search',
|
|
13
|
+
'.cv-p-composer',
|
|
14
|
+
'.cv-p-thread__header',
|
|
15
|
+
'.cv-p-thread__subject-card',
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
type FlexState = { grow: number; shrink: number }
|
|
19
|
+
|
|
20
|
+
const FLEX_KEYWORDS: Record<string, FlexState> = {
|
|
21
|
+
none: { grow: 0, shrink: 0 },
|
|
22
|
+
auto: { grow: 1, shrink: 1 },
|
|
23
|
+
initial: { grow: 0, shrink: 1 },
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseFlexShorthand(value: string): FlexState {
|
|
27
|
+
const keyword = FLEX_KEYWORDS[value]
|
|
28
|
+
if (keyword) return { ...keyword }
|
|
29
|
+
const numbers = value
|
|
30
|
+
.split(/\s+/)
|
|
31
|
+
.filter((token) => /^\d*\.?\d+$/.test(token))
|
|
32
|
+
.map(Number)
|
|
33
|
+
return { grow: numbers[0] ?? 0, shrink: numbers[1] ?? 1 }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function cleanValue(raw: string): string {
|
|
37
|
+
return raw
|
|
38
|
+
.replace(/!important/g, '')
|
|
39
|
+
.trim()
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Only depth-0 style rules count (@media/@supports are conditional); later declarations win.
|
|
44
|
+
function effectiveFlex(selector: string): FlexState | undefined {
|
|
45
|
+
let state: FlexState | undefined
|
|
46
|
+
for (const { prelude, body } of splitBlocks(CSS)) {
|
|
47
|
+
if (body === undefined || prelude.startsWith('@')) continue
|
|
48
|
+
if (!prelude.split(',').some((entry) => entry.trim() === selector)) continue
|
|
49
|
+
for (const declaration of body.split(';')) {
|
|
50
|
+
const separator = declaration.indexOf(':')
|
|
51
|
+
if (separator === -1) continue
|
|
52
|
+
const property = declaration.slice(0, separator).trim().toLowerCase()
|
|
53
|
+
const value = cleanValue(declaration.slice(separator + 1))
|
|
54
|
+
if (property === 'flex') state = parseFlexShorthand(value)
|
|
55
|
+
if (property === 'flex-shrink') state = { grow: state?.grow ?? 0, shrink: Number(value) }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return state
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hasMinHeightZero(selector: string): boolean {
|
|
62
|
+
return splitBlocks(CSS).some(
|
|
63
|
+
({ prelude, body }) =>
|
|
64
|
+
body !== undefined &&
|
|
65
|
+
!prelude.startsWith('@') &&
|
|
66
|
+
prelude.split(',').some((entry) => entry.trim() === selector) &&
|
|
67
|
+
/min-height:\s*0\b/.test(body),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('participant view flex layout', () => {
|
|
72
|
+
it.each(NON_SHRINKING_ITEMS)('%s does not shrink inside a flex column', (selector) => {
|
|
73
|
+
expect(effectiveFlex(selector)?.shrink).toBe(0)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('keeps the thread scroller as the only shrinking, growing region', () => {
|
|
77
|
+
const scroller = effectiveFlex('.cv-p-thread__scroll')
|
|
78
|
+
expect(scroller?.grow).toBe(1)
|
|
79
|
+
expect(scroller?.shrink).toBe(1)
|
|
80
|
+
expect(hasMinHeightZero('.cv-p-thread__scroll')).toBe(true)
|
|
81
|
+
})
|
|
82
|
+
})
|
package/src/styles.css
CHANGED
|
@@ -1295,5 +1295,8 @@
|
|
|
1295
1295
|
.cv-p-row, .cv-p-button, .cv-p-chip { transition: background-color 0.12s, border-color 0.12s; }
|
|
1296
1296
|
}
|
|
1297
1297
|
|
|
1298
|
+
/* Item de coluna flex com overflow ≠ visible (no próprio item) perde o min-height:auto e encolhe; os quatro últimos são defensivos. */
|
|
1299
|
+
.cv-p-filters, .cv-p-quick, .cv-p-search, .cv-p-composer, .cv-p-thread__header, .cv-p-thread__subject-card { flex: 0 0 auto; }
|
|
1300
|
+
|
|
1298
1301
|
.cv-p-root { display: flex; flex-direction: column; min-height: 0; height: 100%; }
|
|
1299
1302
|
.cv-p-root__screen { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|