@nyaruka/temba-components 0.157.1 → 0.158.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/orca/setup.sh ADDED
@@ -0,0 +1,81 @@
1
+ #!/bin/bash
2
+
3
+ # Orca setup hook for a temba-components worktree.
4
+ # Symlinks shared utility files from nyaruka/utils, ensures the devcontainer
5
+ # is built/running (recreating it if the bind mounts are stale), and installs
6
+ # pnpm dependencies inside the worktree.
7
+ #
8
+ # Usage:
9
+ # ./orca/setup.sh # run directly
10
+ # (orca invokes this via orca.yaml's scripts.setup)
11
+
12
+ set -e
13
+
14
+ CONTAINER_NAME="temba-components"
15
+
16
+ # Orca exports ORCA_WORKTREE_PATH when running this as a hook; otherwise
17
+ # derive the worktree path from the script location.
18
+ if [ -n "${ORCA_WORKTREE_PATH:-}" ]; then
19
+ SCRIPT_DIR="$ORCA_WORKTREE_PATH"
20
+ else
21
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
22
+ fi
23
+ WORKSPACE_NAME="$(basename "$SCRIPT_DIR")"
24
+
25
+ # Pin WORKTREES to the orca workspaces dir so .devcontainer/devcontainer.json
26
+ # (which reads ${localEnv:WORKTREES}) sees the orca tree, not a stale value
27
+ # inherited from the user's shell.
28
+ WORKTREES="$(dirname "$(dirname "$SCRIPT_DIR")")"
29
+ export WORKTREES
30
+
31
+ # Symlink shared files from nyaruka/utils into the worktree (gitignored).
32
+ UTILS_DIR="${UTILS_PATH:-$HOME/code/nyaruka/utils}/projects/temba-components"
33
+ if [ ! -d "$UTILS_DIR" ]; then
34
+ echo "Error: utils dir not found at $UTILS_DIR (set UTILS_PATH to override)"
35
+ exit 1
36
+ fi
37
+ for file in run.sh AGENTS.md; do
38
+ if [ -f "$UTILS_DIR/$file" ] && [ ! -e "$SCRIPT_DIR/$file" ]; then
39
+ ln -s "$UTILS_DIR/$file" "$SCRIPT_DIR/$file"
40
+ fi
41
+ done
42
+ if [ ! -e "$SCRIPT_DIR/CLAUDE.md" ]; then
43
+ ln -s "$UTILS_DIR/AGENTS.md" "$SCRIPT_DIR/CLAUDE.md"
44
+ fi
45
+
46
+ # Ensure the devcontainer is running. Bind mounts can't be edited after the
47
+ # container is created, so if an existing container's /workspaces/worktrees
48
+ # mount doesn't match $WORKTREES (e.g. a stale conductor-era container),
49
+ # recreate it to pick up the new mount.
50
+ state="$(docker inspect -f '{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null || true)"
51
+
52
+ if [ -n "$state" ]; then
53
+ current_worktrees="$(docker inspect -f '{{range .Mounts}}{{if eq .Destination "/workspaces/worktrees"}}{{.Source}}{{end}}{{end}}' "$CONTAINER_NAME" 2>/dev/null || true)"
54
+ if [ -n "$current_worktrees" ] && [ "$current_worktrees" != "$WORKTREES" ]; then
55
+ echo "Container '$CONTAINER_NAME' has a stale /workspaces/worktrees mount:"
56
+ echo " $current_worktrees (expected $WORKTREES)"
57
+ echo "Removing and recreating the container..."
58
+ docker rm -f "$CONTAINER_NAME" >/dev/null
59
+ state=""
60
+ fi
61
+ fi
62
+
63
+ case "$state" in
64
+ running) ;;
65
+ "")
66
+ echo "Container '$CONTAINER_NAME' does not exist — building devcontainer..."
67
+ devcontainer up --workspace-folder "$SCRIPT_DIR"
68
+ ;;
69
+ *)
70
+ echo "Container '$CONTAINER_NAME' is $state — starting..."
71
+ docker start "$CONTAINER_NAME"
72
+ ;;
73
+ esac
74
+
75
+ # Install dependencies inside the worktree.
76
+ docker exec "$CONTAINER_NAME" bash -c '
77
+ cd "/workspaces/worktrees/temba-components/'"$WORKSPACE_NAME"'"
78
+ pnpm install
79
+ '
80
+
81
+ echo "Worktree '$WORKSPACE_NAME' ready for development"
package/orca.yaml ADDED
@@ -0,0 +1,3 @@
1
+ scripts:
2
+ setup: |
3
+ ./orca/setup.sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyaruka/temba-components",
3
- "version": "0.157.1",
3
+ "version": "0.158.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",
@@ -13,6 +13,7 @@ import {
13
13
  UpdateFieldEvent,
14
14
  URNsChangedEvent
15
15
  } from '../events';
16
+ import { getLanguageName } from '../languages';
16
17
  import { oxfordFn } from '../utils';
17
18
 
18
19
  export enum Events {
@@ -291,9 +292,7 @@ export const renderTicketAssigneeChanged = (
291
292
  // consistently interactive across open / close / reopen / assigned
292
293
  // rows (the contact-history page can show events from any of the
293
294
  // contact's tickets, so the jump-to-ticket affordance is useful).
294
- const ticketLink = html`<a href="/ticket/all/open/${ticketUUID}/"
295
- >ticket</a
296
- >`;
295
+ const ticketLink = html`<a href="/ticket/all/open/${ticketUUID}/">ticket</a>`;
297
296
  const ticketLinkCapitalized = html`<a href="/ticket/all/open/${ticketUUID}/"
298
297
  >This ticket</a
299
298
  >`;
@@ -356,11 +355,7 @@ export const renderContactGroupsEvent = (
356
355
  if (event.groups_added) {
357
356
  return renderInfoList('Added to', 'Added to', event.groups_added);
358
357
  } else if (event.groups_removed) {
359
- return renderInfoList(
360
- 'Removed from',
361
- 'Removed from',
362
- event.groups_removed
363
- );
358
+ return renderInfoList('Removed from', 'Removed from', event.groups_removed);
364
359
  }
365
360
  };
366
361
 
@@ -382,7 +377,7 @@ export const renderContactLanguageChangedEvent = (
382
377
  return html`<div style=${eventLineStyle}>Cleared language</div>`;
383
378
  }
384
379
  return html`<div style=${eventLineStyle}>
385
- Language updated to ${valueText(event.language)}
380
+ Language updated to ${valueText(getLanguageName(event.language))}
386
381
  </div>`;
387
382
  };
388
383
 
@@ -219,13 +219,18 @@ export class CanvasNode extends RapidElement {
219
219
  background: repeating-linear-gradient(120deg, tomato, tomato 6px, #ff7056 0, #ff7056 18px) !important;
220
220
  }
221
221
 
222
- /* Disable links on actions/nodes with issues */
222
+ /* Disable links on actions/nodes with issues so clicks fall through
223
+ to open the editor instead of navigating. */
223
224
  .action-content.has-issues .linked-name div,
224
225
  .node.has-issues > .router .linked-name div {
225
226
  text-decoration: none !important;
226
227
  cursor: default !important;
227
228
  pointer-events: none;
228
229
  }
230
+ .action-content.has-issues .linked-pill,
231
+ .node.has-issues > .router .linked-pill {
232
+ pointer-events: none;
233
+ }
229
234
 
230
235
  .action.sortable {
231
236
  display: flex;
@@ -1094,13 +1099,16 @@ export class CanvasNode extends RapidElement {
1094
1099
  }
1095
1100
 
1096
1101
  /**
1097
- * Returns true if the click target is inside a `.linked-name` that is
1098
- * still active (i.e. the containing action/node has no issues).
1099
- * When an action/node has issues, links are visually disabled and clicks
1100
- * should fall through to open the editor instead.
1102
+ * Returns true if the click target is inside a `.linked-name` or
1103
+ * `.linked-pill` whose containing action/node has no issues. Active
1104
+ * links handle their own navigation, so click-vs-drag and node-edit
1105
+ * handlers bail out on them. When the action/node has issues, links are
1106
+ * visually disabled (see CSS) and clicks fall through to open the
1107
+ * editor instead.
1101
1108
  */
1102
1109
  private isActiveLink(target: HTMLElement, action?: Action): boolean {
1103
- if (!target.closest('.linked-name')) return false;
1110
+ if (!target.closest('.linked-name') && !target.closest('.linked-pill'))
1111
+ return false;
1104
1112
  if (action) return !this.issuesByAction?.has(action.uuid);
1105
1113
  return !(
1106
1114
  this.issuesByNode?.has(this.node.uuid) ||
@@ -127,7 +127,8 @@ export class DragManager {
127
127
  if (
128
128
  target.classList.contains('exit') ||
129
129
  target.closest('.exit') ||
130
- target.closest('.linked-name')
130
+ target.closest('.linked-name') ||
131
+ target.closest('.linked-pill')
131
132
  ) {
132
133
  return;
133
134
  }
@@ -186,7 +187,8 @@ export class DragManager {
186
187
  if (
187
188
  target.classList.contains('exit') ||
188
189
  target.closest('.exit') ||
189
- target.closest('.linked-name')
190
+ target.closest('.linked-name') ||
191
+ target.closest('.linked-pill')
190
192
  ) {
191
193
  return;
192
194
  }
package/src/flow/utils.ts CHANGED
@@ -386,6 +386,7 @@ const renderLinkedObject = (
386
386
 
387
387
  const pillType = iconToPillType(icon);
388
388
  return html`<temba-label
389
+ class="linked-pill"
389
390
  icon=${icon || ''}
390
391
  type=${pillType || 'neutral'}
391
392
  clickable
@@ -19,6 +19,7 @@ export class DatePicker extends FieldElement {
19
19
  .container {
20
20
  border-radius: var(--curvature);
21
21
  border: 1px solid var(--color-widget-border);
22
+ background: var(--color-widget-bg);
22
23
  display: flex;
23
24
  cursor: pointer;
24
25
  box-shadow: var(--widget-box-shadow);
@@ -59,7 +60,7 @@ export class DatePicker extends FieldElement {
59
60
  }
60
61
 
61
62
  .tz-wrapper {
62
- background: #efefef;
63
+ background: var(--sunken);
63
64
  display: flex;
64
65
  flex-direction: row;
65
66
  align-items: center;
package/src/layout/Tab.ts CHANGED
@@ -27,24 +27,9 @@ export class Tab extends RapidElement {
27
27
  @property({ type: String })
28
28
  icon: string;
29
29
 
30
- @property({ type: String })
31
- selectionColor: string;
32
-
33
- @property({ type: String })
34
- selectionBackground: string;
35
-
36
- @property({ type: String })
37
- borderColor: string = 'var(--color-widget-border)';
38
-
39
- @property({ type: String })
40
- activityColor: string = `var(--color-link-primary)`;
41
-
42
30
  @property({ type: Boolean })
43
31
  selected = false;
44
32
 
45
- @property({ type: Boolean })
46
- notify = false;
47
-
48
33
  @property({ type: Boolean })
49
34
  alert = false;
50
35
 
@@ -18,43 +18,48 @@ export class TabPane extends RapidElement {
18
18
  .options {
19
19
  display: flex;
20
20
  align-items: stretch;
21
- padding: var(--temba-tabs-options-padding, 0);
22
- border-bottom: none;
21
+ gap: 4px;
22
+ border-bottom: 1px solid var(--border);
23
23
  }
24
24
 
25
25
  .option {
26
26
  user-select: none;
27
- padding: 0.5em 0.7em;
28
- margin: 0em 0em;
29
- cursor: pointer;
30
27
  display: flex;
31
- font-size: 1.1em;
32
28
  align-items: center;
33
- border: 1px inset transparent;
34
- border-bottom: 0px;
35
- border-radius: var(--curvature);
36
- border-bottom-right-radius: 0px;
37
- border-bottom-left-radius: 0px;
38
-
39
- color: var(--color-text-dark);
40
- --icon-color: var(--color-text-dark);
29
+ cursor: pointer;
30
+ padding: 8px 14px 10px;
31
+ margin-bottom: -1px;
32
+ background: transparent;
33
+ color: var(--text-2);
34
+ --icon-color: var(--text-2);
35
+ font-size: 13px;
36
+ font-weight: var(--w-medium);
37
+ border-bottom: 2px solid transparent;
41
38
  white-space: nowrap;
42
- transition: all 100ms linear;
39
+ transition:
40
+ color 100ms linear,
41
+ border-color 100ms linear;
43
42
  }
44
43
 
45
- .focusedname .option .name {
46
- transition: all 0s linear !important;
44
+ .option:hover {
45
+ color: var(--text-1);
46
+ --icon-color: var(--text-1);
47
47
  }
48
48
 
49
- .focusedname .option.selected .name {
50
- transition: all 200ms linear !important;
49
+ .option.selected,
50
+ .option.selected:hover {
51
+ cursor: default;
52
+ color: var(--accent-700);
53
+ --icon-color: var(--accent-700);
54
+ border-bottom-color: var(--accent-600);
51
55
  }
52
56
 
53
- .option.hidden {
54
- display: none;
57
+ .unselect .option.selected {
58
+ cursor: pointer;
55
59
  }
56
60
 
57
- .option temba-icon {
61
+ .option.hidden {
62
+ display: none;
58
63
  }
59
64
 
60
65
  .option .name {
@@ -70,6 +75,9 @@ export class TabPane extends RapidElement {
70
75
 
71
76
  .option .badge {
72
77
  margin-left: 0.4em;
78
+ margin-right: -6px;
79
+ display: inline-flex;
80
+ align-items: center;
73
81
  }
74
82
 
75
83
  @media (max-width: 900px) {
@@ -85,9 +93,6 @@ export class TabPane extends RapidElement {
85
93
  }
86
94
  }
87
95
 
88
- .focusedname .option.selected {
89
- }
90
-
91
96
  .focusedname .option .name {
92
97
  max-width: 0px;
93
98
  margin: 0;
@@ -101,137 +106,81 @@ export class TabPane extends RapidElement {
101
106
  max-width: 200px;
102
107
  }
103
108
 
104
- .option {
105
- transform: scale(0.9) translateY(0em);
106
- --icon-color: rgba(0, 0, 0, 0.5);
107
- color: rgba(0, 0, 0, 0.5);
108
- }
109
-
110
- .option.selected {
111
- }
112
-
113
- .option.selected,
114
- .option.selected:hover {
115
- cursor: default;
116
- box-shadow: 0px -3px 3px 1px rgba(0, 0, 0, 0.02);
117
-
118
- background: var(--focused-tab-color, #fff);
119
- transform: scale(1) translateY(1px);
120
- --icon-color: #666;
121
- color: #666;
122
- border: 1px inset rgba(0, 0, 0, 0.15);
123
- border-bottom: 0px;
124
- }
125
-
126
- .option.selected .dot {
127
- display: none;
128
- }
129
-
130
- .unselect .option.selected {
131
- cursor: pointer;
109
+ .focusedname .option .name {
110
+ transition: all 0s linear !important;
132
111
  }
133
112
 
134
- .unselect .option.selected:hover {
135
- background: var(--unselect-tab-color, #eee);
113
+ .focusedname .option.selected .name {
114
+ transition: all 200ms linear !important;
136
115
  }
137
116
 
138
- .option:hover {
139
- --icon-color: #666;
140
- color: #666;
141
- background: rgba(0, 0, 0, 0.02);
117
+ .option.dirty {
118
+ font-weight: var(--w-semibold);
142
119
  }
143
120
 
144
- .option.dirty {
145
- font-weight: 500;
121
+ .option.alert {
122
+ color: var(--danger);
123
+ --icon-color: var(--danger);
146
124
  }
147
125
 
148
126
  .pane {
149
127
  display: flex;
150
128
  flex-direction: column;
151
129
  flex-grow: 1;
152
- background: var(--focused-tab-color, #fff);
153
- border-bottom-left-radius: var(--curvature);
154
- border-bottom-right-radius: var(--curvature);
155
- overflow: hidden;
156
-
157
- box-shadow: var(
158
- --tabs-shadow,
159
- rgba(0, 0, 0, 0.1) 0px 1px 3px 0px,
160
- rgba(0, 0, 0, 0.03) 0px 1px 2px 0px
161
- );
162
130
  min-height: 0;
163
- }
164
-
165
- .pane.first {
166
- border-top-left-radius: 0px;
167
131
  overflow: hidden;
168
132
  }
169
133
 
170
134
  .count {
171
- border-radius: 99px;
172
- background: rgba(0, 0, 0, 0.1);
173
- color: rgba(0, 0, 0, 0.5);
174
- font-size: 0.7em;
175
- font-weight: 500;
176
- min-width: 1.5em;
177
- text-align: center;
178
- }
179
-
180
- .dot {
181
- height: 0.5em;
182
- width: 0.5em;
183
- margin-left: 0.2em;
184
- background: var(--color-primary-dark);
185
- border-radius: 99px;
186
- }
187
-
188
- .notify .count {
189
- background: var(--color-alert);
190
- color: #fff;
135
+ display: inline-flex;
136
+ align-items: center;
137
+ justify-content: center;
138
+ height: 16px;
139
+ padding: 0 2px;
140
+ color: inherit;
141
+ opacity: 0.6;
142
+ font-size: 11px;
143
+ font-weight: var(--w-medium);
144
+ font-variant-numeric: tabular-nums;
191
145
  }
192
146
 
193
- .alert {
194
- color: var(--color-alert);
195
- --icon-color: var(--color-alert);
147
+ .option.selected .count,
148
+ .option.alert .count {
149
+ min-width: 16px;
150
+ padding: 0 4px;
151
+ border-radius: 999px;
152
+ opacity: 1;
196
153
  }
197
154
 
198
- .embedded.pane {
199
- box-shadow: none;
200
- margin: 0;
201
- border-left: none !important;
202
- border-right: none !important;
203
- border-bottom: none !important;
155
+ .option.selected .count {
156
+ background: var(--accent-100);
157
+ color: var(--accent-700);
158
+ font-weight: var(--w-semibold);
204
159
  }
205
160
 
206
- .embedded .option {
207
- border-bottom: none !important;
208
- border-radius: 0em;
209
- border-top: none !important;
161
+ .option.alert .count {
162
+ background: var(--danger-bg);
163
+ color: var(--danger);
210
164
  }
211
165
 
212
- .embedded .option.first {
213
- margin-left: 0em;
214
- border-top: none !important;
215
- border-left: none;
166
+ .dot {
167
+ height: 0.5em;
168
+ width: 0.5em;
169
+ margin-left: 0.2em;
170
+ background: var(--accent-600);
171
+ border-radius: 99px;
216
172
  }
217
173
 
218
- .embedded.options .option.selected {
219
- box-shadow: none !important;
174
+ .option.selected .dot {
175
+ display: none;
220
176
  }
221
177
 
222
178
  .check {
223
179
  margin-left: 0.4em;
224
180
  }
225
-
226
- .pane {
227
- display: flex;
228
- }
229
181
  `;
230
182
  }
231
183
 
232
- @property({ type: Boolean })
233
- embedded = false;
234
-
235
184
  @property({ type: Boolean })
236
185
  collapses = false;
237
186
 
@@ -367,13 +316,11 @@ export class TabPane extends RapidElement {
367
316
  }
368
317
 
369
318
  public render(): TemplateResult {
370
- const activeTab = this.options[this.index];
371
319
  return html`
372
320
  <div
373
321
  class="${getClasses({
374
322
  options: true,
375
323
  collapses: this.collapses,
376
- embedded: this.embedded,
377
324
  focusedname: this.focusedName,
378
325
  unselect: this.unselect
379
326
  })}"
@@ -388,15 +335,9 @@ export class TabPane extends RapidElement {
388
335
  first: index == 0,
389
336
  selected: index == this.index,
390
337
  hidden: tab.hidden,
391
- notify: tab.notify,
392
338
  alert: tab.alert,
393
339
  dirty: tab.dirty
394
340
  })}"
395
- style="${tab.selectionColor && index == this.index
396
- ? `color:${tab.selectionColor};--icon-color:${tab.selectionColor};`
397
- : ''} ${tab.selectionBackground && index == this.index
398
- ? `background-color:${tab.selectionBackground};`
399
- : ''}"
400
341
  >
401
342
  ${tab.icon ? html`<temba-icon name=${tab.icon} />` : null}
402
343
  <div class="name">${tab.name} ${tab.dirty ? ` *` : ``}</div>
@@ -405,14 +346,11 @@ export class TabPane extends RapidElement {
405
346
  <div class="badge">
406
347
  ${tab.count > 0 && !tab.activity
407
348
  ? html`<div class="count">
408
- ${tab.activity ? '' : tab.count.toLocaleString()}
349
+ ${tab.count.toLocaleString()}
409
350
  </div>`
410
351
  : null}
411
352
  ${tab.activity && tab.count > 0 && !tab.dirty
412
- ? html`<div
413
- class="dot"
414
- style="background:${tab.activityColor}"
415
- ></div>`
353
+ ? html`<div class="dot"></div>`
416
354
  : null}
417
355
  </div>
418
356
  `
@@ -429,35 +367,7 @@ export class TabPane extends RapidElement {
429
367
  <slot name="tab-right"></slot>
430
368
  </div>
431
369
  </div>
432
- <div
433
- @temba-details-changed=${this.handleTabDetailsChanged}
434
- style="${activeTab?.borderColor
435
- ? `
436
- border-top: var(--temba-tabs-border-top, 1px solid ${
437
- activeTab?.borderColor || 'var(--color-widget-border)'
438
- });
439
-
440
- border-left: var(--temba-tabs-border-left, 1px solid ${
441
- activeTab?.borderColor || 'var(--color-widget-border)'
442
- });
443
-
444
- border-bottom: var(--temba-tabs-border-bottom, 1px solid ${
445
- activeTab?.borderColor || 'var(--color-widget-border)'
446
- });
447
-
448
- border-right: var(--temba-tabs-border-right, 1px solid ${
449
- activeTab?.borderColor || 'var(--color-widget-border)'
450
- });
451
-
452
- `
453
- : ''} ${activeTab?.selectionBackground
454
- ? `background:${activeTab?.selectionBackground};`
455
- : ``}"
456
- class="pane ${getClasses({
457
- first: this.index == 0,
458
- embedded: this.embedded
459
- })}"
460
- >
370
+ <div @temba-details-changed=${this.handleTabDetailsChanged} class="pane">
461
371
  <slot></slot>
462
372
  <slot name="pane-bottom"></slot>
463
373
  </div>
@@ -64,11 +64,11 @@ export class ContactChat extends ContactStoreElement {
64
64
  display: flex;
65
65
  flex-direction: row;
66
66
  min-height: 0;
67
+ margin-top: var(--gap);
67
68
  --compose-shadow: none;
68
69
  --compose-border: none;
69
70
  --compose-padding: 3px;
70
71
  --compose-curvature: none;
71
- border-top: 1px inset rgba(0, 0, 0, 0.05);
72
72
  }
73
73
 
74
74
  .chat-wrapper {
@@ -76,7 +76,11 @@ export class ContactChat extends ContactStoreElement {
76
76
  flex-grow: 1;
77
77
  flex-direction: column;
78
78
  min-height: 0;
79
- background: #fff;
79
+ background: var(--surface);
80
+ border: 1px solid var(--border-strong);
81
+ border-radius: var(--r-sm);
82
+ box-shadow: var(--shadow-2);
83
+ overflow: hidden;
80
84
  }
81
85
 
82
86
  temba-contact-history {