@nyaruka/temba-components 0.171.0 → 0.172.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 (37) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/dist/temba-components.js +1015 -747
  3. package/dist/temba-components.js.map +1 -1
  4. package/package.json +1 -1
  5. package/src/RapidElement.ts +83 -41
  6. package/src/display/Button.ts +14 -2
  7. package/src/display/Chat.ts +41 -22
  8. package/src/display/Label.ts +18 -3
  9. package/src/events/eventRenderers.ts +4 -0
  10. package/src/flow/FlowSearch.ts +12 -392
  11. package/src/flow/actions/send_msg.ts +2 -1
  12. package/src/flow/actions/set_contact_field.ts +1 -0
  13. package/src/flow/actions/set_contact_name.ts +1 -0
  14. package/src/flow/nodes/split_by_ticket.ts +1 -0
  15. package/src/flow/nodes/wait_for_dial.ts +1 -0
  16. package/src/form/RangePicker.ts +10 -2
  17. package/src/form/select/Select.ts +15 -2
  18. package/src/layout/SearchModal.ts +564 -0
  19. package/src/list/BroadcastList.ts +17 -6
  20. package/src/list/ContentList.ts +29 -13
  21. package/src/list/FieldList.ts +8 -1
  22. package/src/list/TembaMenu.ts +71 -13
  23. package/src/live/CampaignEvents.ts +31 -11
  24. package/src/live/ContactChat.ts +276 -76
  25. package/src/live/ContactTimeline.ts +46 -24
  26. package/src/live/ContactWatch.ts +166 -63
  27. package/src/live/Realtime.ts +137 -8
  28. package/src/live/SocketService.ts +115 -10
  29. package/src/live/TicketSearch.ts +290 -0
  30. package/src/live/Watchers.ts +93 -0
  31. package/src/simulator/Simulator.ts +59 -36
  32. package/src/store/Store.ts +17 -34
  33. package/src/styles/designTokens.ts +57 -17
  34. package/src/styles/pillVariants.ts +42 -10
  35. package/static/css/design-system.css +47 -12
  36. package/static/css/temba-components.css +31 -9
  37. package/temba-modules.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyaruka/temba-components",
3
- "version": "0.171.0",
3
+ "version": "0.172.0",
4
4
  "description": "Web components to support rapidpro and related projects",
5
5
  "author": "Nyaruka <code@nyaruka.coim>",
6
6
  "main": "dist/index.js",
@@ -40,6 +40,34 @@ export interface EventHandler {
40
40
  isWindow?: boolean;
41
41
  }
42
42
 
43
+ /**
44
+ * Inline handlers compiled from an element's -<event-type> attribute, keyed
45
+ * by their source. Page authors write a finite set of these, so compiling
46
+ * each one once keeps dispatch off the Function constructor entirely.
47
+ */
48
+ const inlineHandlers = new Map<string, (event: Event) => any>();
49
+
50
+ const compileInlineHandler = (source: string): ((event: Event) => any) => {
51
+ let compiled = inlineHandlers.get(source);
52
+ if (!compiled) {
53
+ compiled = new Function(
54
+ 'event',
55
+ `
56
+ with(document) {
57
+ with(this) {
58
+ let handler = ${source};
59
+ if(typeof handler === 'function') {
60
+ handler(event);
61
+ }
62
+ }
63
+ }
64
+ `
65
+ ) as (event: Event) => any;
66
+ inlineHandlers.set(source, compiled);
67
+ }
68
+ return compiled;
69
+ };
70
+
43
71
  export class RapidElement extends LitElement {
44
72
  DEBUG = false;
45
73
  DEBUG_UPDATES = false;
@@ -49,34 +77,50 @@ export class RapidElement extends LitElement {
49
77
  service: string;
50
78
 
51
79
  private eles: { [selector: string]: HTMLDivElement } = {};
80
+
81
+ // teardowns for the listeners we installed, so disconnecting removes the
82
+ // exact functions we added
83
+ private listenerTeardowns: (() => void)[] = [];
84
+
52
85
  public getEventHandlers(): EventHandler[] {
53
86
  return [];
54
87
  }
55
88
 
89
+ /**
90
+ * Adds a listener this element owns the teardown for - it is bound to this
91
+ * element and removed automatically when we disconnect. Use this instead of
92
+ * addEventListener for anything on document or window, where a listener
93
+ * that outlives its element keeps the whole element alive.
94
+ */
95
+ public listenTo(
96
+ target: EventTarget,
97
+ event: string,
98
+ method: EventListener,
99
+ options?: AddEventListenerOptions
100
+ ): void {
101
+ const bound = method.bind(this);
102
+ target.addEventListener(event, bound, options);
103
+ this.listenerTeardowns.push(() =>
104
+ target.removeEventListener(event, bound, options)
105
+ );
106
+ }
107
+
56
108
  connectedCallback() {
57
109
  super.connectedCallback();
58
110
 
59
111
  for (const handler of this.getEventHandlers()) {
60
- if (handler.isDocument) {
61
- document.addEventListener(handler.event, handler.method.bind(this));
62
- } else if (handler.isWindow) {
63
- window.addEventListener(handler.event, handler.method.bind(this));
64
- } else {
65
- this.addEventListener(handler.event, handler.method.bind(this));
66
- }
112
+ const target = handler.isDocument
113
+ ? document
114
+ : handler.isWindow
115
+ ? window
116
+ : this;
117
+ this.listenTo(target, handler.event, handler.method);
67
118
  }
68
119
  }
69
120
 
70
121
  disconnectedCallback() {
71
- for (const handler of this.getEventHandlers()) {
72
- if (handler.isDocument) {
73
- document.removeEventListener(handler.event, handler.method);
74
- } else if (handler.isWindow) {
75
- window.removeEventListener(handler.event, handler.method);
76
- } else {
77
- this.removeEventListener(handler.event, handler.method);
78
- }
79
- }
122
+ this.listenerTeardowns.forEach((teardown) => teardown());
123
+ this.listenerTeardowns = [];
80
124
  super.disconnectedCallback();
81
125
  }
82
126
 
@@ -121,9 +165,7 @@ export class RapidElement extends LitElement {
121
165
  }
122
166
 
123
167
  public fireCustomEvent(type: CustomEventType, detail: any = {}): any {
124
- if (this['DEBUG_EVENTS']) {
125
- showEvent(this, type, detail);
126
- }
168
+ showEvent(this, type, detail);
127
169
 
128
170
  const event = new CustomEvent(type, {
129
171
  detail,
@@ -135,30 +177,30 @@ export class RapidElement extends LitElement {
135
177
  }
136
178
 
137
179
  public dispatchEvent(event: any): any {
138
- super.dispatchEvent(event);
180
+ const dispatched = super.dispatchEvent(event);
181
+
182
+ // the page can hang a handler off the target for any of our events, as a
183
+ // -<event-type> property or an attribute of the same name
139
184
  const ele = event.target;
140
- if (ele) {
141
- // lookup events with - prefix and try to invoke them
142
- const eventFire = (ele as any)['-' + event.type];
143
- if (eventFire) {
144
- return eventFire(event);
145
- } else {
146
- const func = new Function(
147
- 'event',
148
- `
149
- with(document) {
150
- with(this) {
151
- let handler = ${ele.getAttribute('-' + event.type)};
152
- if(typeof handler === 'function') {
153
- handler(event);
154
- }
155
- }
156
- }
157
- `
158
- );
159
- return func.call(ele, event);
160
- }
185
+ if (!ele) {
186
+ return dispatched;
187
+ }
188
+
189
+ const eventFire = (ele as any)['-' + event.type];
190
+ if (eventFire) {
191
+ return eventFire(event);
161
192
  }
193
+
194
+ const inline = ele.getAttribute ? ele.getAttribute('-' + event.type) : null;
195
+ if (!inline) {
196
+ return dispatched;
197
+ }
198
+
199
+ // the compiled handler has no return value of its own, so handing its
200
+ // undefined back would read as a cancelled event - what the caller wants
201
+ // to know is whether anything called preventDefault
202
+ compileInlineHandler(inline).call(ele, event);
203
+ return dispatched;
162
204
  }
163
205
 
164
206
  public closestElement(selector: string, base: Element = this) {
@@ -101,9 +101,11 @@ export class Button extends LitElement {
101
101
  background: var(--success, #16a34a);
102
102
  color: #fff;
103
103
  }
104
+ /* Hover darkening: static pre-mixed values first for browsers
105
+ without color-mix(), re-derived from the token below. */
104
106
  .attention-button:hover,
105
107
  .affirmative:hover {
106
- background: color-mix(in srgb, var(--success, #16a34a) 88%, black);
108
+ background: #138f41;
107
109
  }
108
110
 
109
111
  /* DS .btn-danger — flat danger fill (no lift). */
@@ -112,7 +114,17 @@ export class Button extends LitElement {
112
114
  color: #fff;
113
115
  }
114
116
  .destructive-button:hover {
115
- background: color-mix(in srgb, var(--danger, #d03f3f) 88%, black);
117
+ background: #b73737;
118
+ }
119
+
120
+ @supports (color: color-mix(in srgb, red, red)) {
121
+ .attention-button:hover,
122
+ .affirmative:hover {
123
+ background: color-mix(in srgb, var(--success, #16a34a) 88%, black);
124
+ }
125
+ .destructive-button:hover {
126
+ background: color-mix(in srgb, var(--danger, #d03f3f) 88%, black);
127
+ }
116
128
  }
117
129
 
118
130
  /* DS .btn-ghost — text-only, hover fills with sunken */
@@ -518,19 +518,15 @@ export class Chat extends RapidElement {
518
518
  same theme vars the flat fills used — color-mix keeps host
519
519
  re-theming working — with a whisper of shadow for depth and
520
520
  a hairline border mixed just darker than the fill so bubbles
521
- hold their edge against the background. */
521
+ hold their edge against the background. Base rules carry flat
522
+ fills and pre-mixed borders for browsers without color-mix()
523
+ (pre-Chrome 111); the @supports block below layers the
524
+ gradients on top. */
522
525
  .bubble {
523
526
  padding: 10px 14px;
524
- background: linear-gradient(
525
- 180deg,
526
- color-mix(in srgb, var(--color-chat-in, #e5e5ea) 62%, white) 0%,
527
- var(--color-chat-in, #e5e5ea) 100%
528
- );
527
+ background: var(--color-chat-in, #e5e5ea);
529
528
  border-radius: 18px;
530
- border: var(
531
- --chat-border-in,
532
- 1px solid color-mix(in srgb, var(--color-chat-in, #e5e5ea) 93%, black)
533
- );
529
+ border: var(--chat-border-in, 1px solid #d5d5da);
534
530
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.06);
535
531
  }
536
532
 
@@ -550,20 +546,40 @@ export class Chat extends RapidElement {
550
546
  }
551
547
 
552
548
  .incoming .bubble {
553
- background: linear-gradient(
554
- 180deg,
555
- color-mix(in srgb, var(--color-chat-out, #007aff) 90%, white) 0%,
556
- var(--color-chat-out, #007aff) 58%,
557
- color-mix(in srgb, var(--color-chat-out, #007aff) 95%, black) 100%
558
- );
559
- border: var(
560
- --chat-border-out,
561
- 1px solid
562
- color-mix(in srgb, var(--color-chat-out, #007aff) 90%, black)
563
- );
549
+ background: var(--color-chat-out, #007aff);
550
+ border: var(--chat-border-out, 1px solid #006ee6);
564
551
  color: white;
565
552
  }
566
553
 
554
+ @supports (color: color-mix(in srgb, red, red)) {
555
+ .bubble {
556
+ background: linear-gradient(
557
+ 180deg,
558
+ color-mix(in srgb, var(--color-chat-in, #e5e5ea) 62%, white) 0%,
559
+ var(--color-chat-in, #e5e5ea) 100%
560
+ );
561
+ border: var(
562
+ --chat-border-in,
563
+ 1px solid
564
+ color-mix(in srgb, var(--color-chat-in, #e5e5ea) 93%, black)
565
+ );
566
+ }
567
+
568
+ .incoming .bubble {
569
+ background: linear-gradient(
570
+ 180deg,
571
+ color-mix(in srgb, var(--color-chat-out, #007aff) 90%, white) 0%,
572
+ var(--color-chat-out, #007aff) 58%,
573
+ color-mix(in srgb, var(--color-chat-out, #007aff) 95%, black) 100%
574
+ );
575
+ border: var(
576
+ --chat-border-out,
577
+ 1px solid
578
+ color-mix(in srgb, var(--color-chat-out, #007aff) 90%, black)
579
+ );
580
+ }
581
+ }
582
+
567
583
  .incoming .latest .bubble {
568
584
  border-bottom-right-radius: 4px;
569
585
  }
@@ -573,8 +589,11 @@ export class Chat extends RapidElement {
573
589
  }
574
590
 
575
591
  /* Notes get the same top-lit gradient treatment as message
576
- bubbles, derived from their sticky-note yellow. */
592
+ bubbles, derived from their sticky-note yellow. The flat fill
593
+ first is the fallback for browsers without color-mix() — all
594
+ literals here, so the invalid gradient drops at parse time. */
577
595
  .note .bubble {
596
+ background: #fffac3;
578
597
  background: linear-gradient(
579
598
  180deg,
580
599
  color-mix(in srgb, #fffac3 62%, white) 0%,
@@ -100,7 +100,10 @@ export default class Label extends LitElement {
100
100
  dark variant shade), so flow stays bluish, group stays purplish,
101
101
  etc. — no grey wash. */
102
102
  .label[class*='pill-'].clickable .mask:hover {
103
- background: color-mix(in oklab, currentColor 10%, transparent);
103
+ /* Neutral wash first for browsers without color-mix() the
104
+ currentColor mix can't fall back per-declaration because
105
+ currentColor keeps it valid until computed-value time. */
106
+ background: rgba(0, 0, 0, 0.06);
104
107
  }
105
108
  .label[class*='pill-'] temba-icon {
106
109
  margin-right: 0;
@@ -121,14 +124,26 @@ export default class Label extends LitElement {
121
124
  margin: 0;
122
125
  border: 0;
123
126
  border-radius: 999px;
124
- background: color-mix(in oklab, currentColor 25%, transparent);
127
+ background: rgba(0, 0, 0, 0.12);
125
128
  color: inherit;
126
129
  opacity: 0.8;
127
130
  --icon-color: currentColor;
128
131
  }
129
132
  .label[class*='pill-'] .remove:hover {
130
133
  opacity: 1;
131
- background: color-mix(in oklab, currentColor 45%, transparent);
134
+ background: rgba(0, 0, 0, 0.22);
135
+ }
136
+
137
+ @supports (color: color-mix(in srgb, red, red)) {
138
+ .label[class*='pill-'].clickable .mask:hover {
139
+ background: color-mix(in oklab, currentColor 10%, transparent);
140
+ }
141
+ .label[class*='pill-'] .remove {
142
+ background: color-mix(in oklab, currentColor 25%, transparent);
143
+ }
144
+ .label[class*='pill-'] .remove:hover {
145
+ background: color-mix(in oklab, currentColor 45%, transparent);
146
+ }
132
147
  }
133
148
 
134
149
  /* When a removable X is present, tighten the mask's left padding
@@ -59,6 +59,10 @@ export enum Events {
59
59
  TICKET_OPENED = 'ticket_opened',
60
60
  TICKET_REOPENED = 'ticket_reopened',
61
61
  TICKET_TOPIC_CHANGED = 'ticket_topic_changed',
62
+ // ephemeral, drives the chat's typing indicator instead of recording
63
+ // history - published by agents as they compose and never persisted
64
+ TYPING_STARTED = 'typing_started',
65
+ TYPING_STOPPED = 'typing_stopped',
62
66
  WARNING = 'warning',
63
67
  WEBHOOK_CALLED = 'webhook_called'
64
68
  }