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