@nyaruka/temba-components 0.163.0 → 0.164.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/CHANGELOG.md +14 -0
- package/dist/temba-components.js +1842 -678
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/display/Chat.ts +3 -2
- package/src/display/Lightbox.ts +57 -109
- package/src/display/Thumbnail.ts +34 -7
- package/src/interfaces.ts +79 -2
- package/src/layout/Card.ts +311 -0
- package/src/layout/CardLayout.ts +425 -0
- package/src/layout/CardStack.ts +131 -0
- package/src/layout/Dialog.ts +33 -24
- package/src/layout/HeaderBar.ts +38 -0
- package/src/layout/PageHeader.ts +5 -6
- package/src/layout/Resizer.ts +20 -0
- package/src/list/CampaignList.ts +144 -0
- package/src/list/ContactList.ts +3 -1
- package/src/list/FlowList.ts +31 -7
- package/src/list/MsgList.ts +18 -12
- package/src/list/SortableList.ts +45 -6
- package/src/list/TriggerList.ts +538 -0
- package/src/live/CampaignEvents.ts +1094 -0
- package/src/live/ContactDetails.ts +3 -4
- package/src/live/ContactFields.ts +1 -1
- package/src/live/ContactNameFetch.ts +5 -2
- package/src/live/ContactNotepad.ts +88 -2
- package/src/live/ContactStoreElement.ts +12 -2
- package/src/utils.ts +19 -0
- package/temba-modules.ts +14 -0
- package/web-dev-server.config.mjs +54 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { css, html, TemplateResult } from 'lit';
|
|
2
|
+
import { property } from 'lit/decorators.js';
|
|
3
|
+
import { RapidElement } from '../RapidElement';
|
|
4
|
+
import { designTokens } from '../styles/designTokens';
|
|
5
|
+
import { getClasses } from '../utils';
|
|
6
|
+
import { Icon } from '../Icons';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A collapsible card with a header showing an icon, label and optional
|
|
10
|
+
* count badge. Designed to live inside a temba-card-stack where the header
|
|
11
|
+
* doubles as the drag handle, but works standalone too. The body never
|
|
12
|
+
* scrolls internally — cards grow to their content.
|
|
13
|
+
*/
|
|
14
|
+
export class Card extends RapidElement {
|
|
15
|
+
static get styles() {
|
|
16
|
+
return css`
|
|
17
|
+
${designTokens}
|
|
18
|
+
|
|
19
|
+
:host {
|
|
20
|
+
display: block;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* The chrome lives on an inner frame rather than the host so
|
|
24
|
+
document-level universal rules (e.g. tailwind's preflight
|
|
25
|
+
border-color) can't override it. */
|
|
26
|
+
.frame {
|
|
27
|
+
background: var(--card-bg, var(--surface));
|
|
28
|
+
border: 1px solid var(--card-border, var(--border-strong));
|
|
29
|
+
border-radius: var(--r-sm);
|
|
30
|
+
box-shadow: var(--shadow-2);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* note variant — the sticky-note surface, header included */
|
|
34
|
+
:host([variant='note']) .frame {
|
|
35
|
+
background: var(--surface-note);
|
|
36
|
+
border-color: var(--border-note);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.card-header {
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
padding: 8px 10px;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
user-select: none;
|
|
45
|
+
border-radius: var(--r-sm);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.card-header temba-icon {
|
|
49
|
+
--icon-color: var(--text-3);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.grip {
|
|
53
|
+
margin-right: 0.5em;
|
|
54
|
+
cursor: grab;
|
|
55
|
+
--icon-color: var(--text-4);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.card-header:hover .grip {
|
|
59
|
+
--icon-color: var(--text-3);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.label {
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
flex-grow: 1;
|
|
66
|
+
font-size: 13px;
|
|
67
|
+
font-weight: var(--w-medium);
|
|
68
|
+
color: var(--text-2);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.label temba-icon {
|
|
72
|
+
margin-right: 0.5em;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.count {
|
|
76
|
+
display: inline-flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
justify-content: center;
|
|
79
|
+
min-width: 16px;
|
|
80
|
+
height: 16px;
|
|
81
|
+
padding: 0 4px;
|
|
82
|
+
margin-right: 0.5em;
|
|
83
|
+
border-radius: 999px;
|
|
84
|
+
background: var(--accent-100);
|
|
85
|
+
color: var(--accent-700);
|
|
86
|
+
font-size: 11px;
|
|
87
|
+
font-weight: var(--w-semibold);
|
|
88
|
+
font-variant-numeric: tabular-nums;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.dot {
|
|
92
|
+
height: 0.5em;
|
|
93
|
+
width: 0.5em;
|
|
94
|
+
margin-right: 0.5em;
|
|
95
|
+
background: var(--accent-600);
|
|
96
|
+
border-radius: 99px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.toggle {
|
|
100
|
+
transition: transform 200ms ease;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.toggle.collapsed {
|
|
104
|
+
transform: rotate(-90deg);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Grid trick so collapse animates to natural content height without
|
|
108
|
+
capping how tall an expanded card can grow. */
|
|
109
|
+
.body {
|
|
110
|
+
display: grid;
|
|
111
|
+
grid-template-rows: 1fr;
|
|
112
|
+
transition: grid-template-rows 200ms ease;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.body.collapsed {
|
|
116
|
+
grid-template-rows: 0fr;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.inner {
|
|
120
|
+
min-height: 0;
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* once fully expanded, let popovers (date pickers etc.) escape */
|
|
125
|
+
.body:not(.collapsed):not(.animating) .inner {
|
|
126
|
+
overflow: visible;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.content {
|
|
130
|
+
padding: 0 10px 10px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* bleed mode: the body content runs edge-to-edge so a panel with its
|
|
134
|
+
own surface (e.g. the notepad) fills the card, clipped to the card
|
|
135
|
+
radius. The footer of such content sits on the card's bottom edge. */
|
|
136
|
+
:host([bleed]) .content {
|
|
137
|
+
padding: 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
:host([bleed]) .body:not(.collapsed):not(.animating) .inner {
|
|
141
|
+
overflow: hidden;
|
|
142
|
+
border-radius: 0 0 var(--r-sm) var(--r-sm);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* plain mode: headerless and non-collapsible — the wrapper (e.g. a
|
|
146
|
+
tab pane) supplies the label — but still a proper card surface
|
|
147
|
+
with padding. Fills its pane and scrolls its content internally,
|
|
148
|
+
since a tab pane is height-bounded. */
|
|
149
|
+
:host([plain]) {
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
flex-grow: 1;
|
|
153
|
+
min-height: 0;
|
|
154
|
+
margin-top: var(--layout-spacing, 8px);
|
|
155
|
+
margin-bottom: var(--layout-spacing, 8px);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
:host([plain]) .frame,
|
|
159
|
+
:host([plain]) .body,
|
|
160
|
+
:host([plain]) .inner {
|
|
161
|
+
display: flex;
|
|
162
|
+
flex-direction: column;
|
|
163
|
+
flex-grow: 1;
|
|
164
|
+
min-height: 0;
|
|
165
|
+
overflow: hidden;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
:host([plain]) .inner {
|
|
169
|
+
border-radius: var(--r-sm);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
:host([plain]) .content {
|
|
173
|
+
display: flex;
|
|
174
|
+
flex-direction: column;
|
|
175
|
+
flex-grow: 1;
|
|
176
|
+
min-height: 0;
|
|
177
|
+
padding: 10px;
|
|
178
|
+
overflow-y: auto;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* no header row in plain mode, so bleeding content gets a top inset
|
|
182
|
+
directly against the card surface */
|
|
183
|
+
:host([plain][bleed]) .content {
|
|
184
|
+
padding: 10px 0 0 0;
|
|
185
|
+
}
|
|
186
|
+
`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@property({ type: String })
|
|
190
|
+
label = '';
|
|
191
|
+
|
|
192
|
+
@property({ type: String })
|
|
193
|
+
icon = '';
|
|
194
|
+
|
|
195
|
+
@property({ type: Number })
|
|
196
|
+
count = 0;
|
|
197
|
+
|
|
198
|
+
// show a dot instead of the count
|
|
199
|
+
@property({ type: Boolean })
|
|
200
|
+
activity = false;
|
|
201
|
+
|
|
202
|
+
@property({ type: Boolean, reflect: true })
|
|
203
|
+
collapsed = false;
|
|
204
|
+
|
|
205
|
+
// render without chrome (no header, border or collapse) — content only
|
|
206
|
+
@property({ type: Boolean, reflect: true })
|
|
207
|
+
plain = false;
|
|
208
|
+
|
|
209
|
+
// body content runs edge-to-edge instead of getting the inset padding
|
|
210
|
+
@property({ type: Boolean, reflect: true })
|
|
211
|
+
bleed = false;
|
|
212
|
+
|
|
213
|
+
// named surface treatments, e.g. "note" for the sticky-note look
|
|
214
|
+
@property({ type: String, reflect: true })
|
|
215
|
+
variant = '';
|
|
216
|
+
|
|
217
|
+
@property({ type: Boolean })
|
|
218
|
+
dirty = false;
|
|
219
|
+
|
|
220
|
+
private animating = false;
|
|
221
|
+
|
|
222
|
+
private handleHeaderClick() {
|
|
223
|
+
this.collapsed = !this.collapsed;
|
|
224
|
+
this.animating = true;
|
|
225
|
+
this.requestUpdate();
|
|
226
|
+
this.dispatchEvent(
|
|
227
|
+
new CustomEvent('toggle', {
|
|
228
|
+
bubbles: true,
|
|
229
|
+
composed: true,
|
|
230
|
+
detail: { collapsed: this.collapsed, label: this.label }
|
|
231
|
+
})
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private handleTransitionEnd(event: TransitionEvent) {
|
|
236
|
+
// transitionend bubbles composed out of slotted content — only our own
|
|
237
|
+
// grid collapse animation should clear the clipping state
|
|
238
|
+
if (
|
|
239
|
+
event.target !== event.currentTarget ||
|
|
240
|
+
event.propertyName !== 'grid-template-rows'
|
|
241
|
+
) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
this.animating = false;
|
|
245
|
+
this.requestUpdate();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private handleDetailsChanged(event: CustomEvent) {
|
|
249
|
+
if ('dirty' in event.detail) {
|
|
250
|
+
this.dirty = event.detail.dirty;
|
|
251
|
+
}
|
|
252
|
+
if ('count' in event.detail) {
|
|
253
|
+
this.count = event.detail.count;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
public render(): TemplateResult {
|
|
258
|
+
if (this.plain) {
|
|
259
|
+
return html`
|
|
260
|
+
<div class="frame">
|
|
261
|
+
<div class="body">
|
|
262
|
+
<div class="inner">
|
|
263
|
+
<div class="content">
|
|
264
|
+
<slot
|
|
265
|
+
@temba-details-changed=${this.handleDetailsChanged}
|
|
266
|
+
></slot>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return html`
|
|
275
|
+
<div class="frame">
|
|
276
|
+
<div class="card-header" @click=${this.handleHeaderClick}>
|
|
277
|
+
<temba-icon name=${Icon.drag} class="grip"></temba-icon>
|
|
278
|
+
<div class="label">
|
|
279
|
+
${this.icon
|
|
280
|
+
? html`<temba-icon name=${this.icon}></temba-icon>`
|
|
281
|
+
: null}
|
|
282
|
+
${this.label}${this.dirty ? ' *' : ''}
|
|
283
|
+
</div>
|
|
284
|
+
<slot name="header-actions"></slot>
|
|
285
|
+
${this.count > 0
|
|
286
|
+
? this.activity
|
|
287
|
+
? html`<div class="dot"></div>`
|
|
288
|
+
: html`<div class="count">${this.count.toLocaleString()}</div>`
|
|
289
|
+
: null}
|
|
290
|
+
<temba-icon
|
|
291
|
+
name=${Icon.arrow_down}
|
|
292
|
+
class="toggle ${this.collapsed ? 'collapsed' : ''}"
|
|
293
|
+
></temba-icon>
|
|
294
|
+
</div>
|
|
295
|
+
<div
|
|
296
|
+
class="body ${getClasses({
|
|
297
|
+
collapsed: this.collapsed,
|
|
298
|
+
animating: this.animating
|
|
299
|
+
})}"
|
|
300
|
+
@transitionend=${this.handleTransitionEnd}
|
|
301
|
+
>
|
|
302
|
+
<div class="inner">
|
|
303
|
+
<div class="content">
|
|
304
|
+
<slot @temba-details-changed=${this.handleDetailsChanged}></slot>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
`;
|
|
310
|
+
}
|
|
311
|
+
}
|