@nyaruka/temba-components 0.156.17 → 0.157.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 (39) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/temba-components.js +1189 -767
  3. package/dist/temba-components.js.map +1 -1
  4. package/package.json +1 -1
  5. package/src/display/Chat.ts +14 -0
  6. package/src/display/Label.ts +156 -2
  7. package/src/display/Options.ts +71 -16
  8. package/src/display/TembaUser.ts +23 -5
  9. package/src/events/eventRenderers.ts +104 -41
  10. package/src/excellent/caret-utils.ts +0 -1
  11. package/src/flow/RevisionsWindow.ts +53 -9
  12. package/src/flow/nodes/shared.ts +14 -0
  13. package/src/flow/nodes/split_by_llm_categorize.ts +33 -8
  14. package/src/flow/revision-summary.ts +25 -0
  15. package/src/flow/utils.ts +38 -40
  16. package/src/form/ArrayEditor.ts +9 -11
  17. package/src/form/Checkbox.ts +2 -2
  18. package/src/form/Compose.ts +1 -1
  19. package/src/form/FieldElement.ts +8 -8
  20. package/src/form/KeyValueEditor.ts +4 -4
  21. package/src/form/MessageEditor.ts +2 -3
  22. package/src/form/RangePicker.ts +17 -17
  23. package/src/form/TembaSlider.ts +10 -10
  24. package/src/form/TemplateEditor.ts +4 -4
  25. package/src/form/TextInput.ts +19 -1
  26. package/src/form/select/Omnibox.ts +22 -19
  27. package/src/form/select/Select.ts +379 -171
  28. package/src/form/select/WorkspaceSelect.ts +7 -1
  29. package/src/layout/Accordion.ts +2 -2
  30. package/src/layout/Modax.ts +1 -1
  31. package/src/list/SortableList.ts +159 -0
  32. package/src/live/ContactChat.ts +46 -44
  33. package/src/live/ContactDetails.ts +1 -0
  34. package/src/live/ContactFieldEditor.ts +38 -31
  35. package/src/live/FieldManager.ts +4 -4
  36. package/src/styles/designTokens.ts +145 -0
  37. package/src/styles/pillVariants.ts +136 -0
  38. package/static/css/temba-components.css +106 -28
  39. package/web-test-runner.config.mjs +98 -0
@@ -14,8 +14,14 @@ export class WorkspaceSelect extends Select<WorkspaceOption> {
14
14
  return css`
15
15
  ${super.styles}
16
16
 
17
+ /* Workspace chooser is embedded in the account menu, not a
18
+ standalone form widget — suppress the focus border + halo
19
+ on both the select itself and the dropdown popup. */
17
20
  :host {
18
- border: 0px solid blue;
21
+ --temba-select-focus-border: transparent;
22
+ --temba-select-focus-halo: none;
23
+ --temba-options-focus-border: transparent;
24
+ --temba-options-focus-halo: none;
19
25
  }
20
26
  `;
21
27
  }
@@ -7,8 +7,8 @@ export class Accordion extends LitElement {
7
7
  return css`
8
8
  :host {
9
9
  display: block;
10
- border: 1px solid #e0e0e0;
11
- border-radius: 6px;
10
+ border: 1px solid var(--color-widget-border);
11
+ border-radius: var(--curvature-widget);
12
12
  overflow: hidden;
13
13
  }
14
14
  `;
@@ -70,7 +70,7 @@ export class Modax extends RapidElement {
70
70
  color: tomato;
71
71
  padding: 10px;
72
72
  margin-bottom: 10px;
73
- border-radius: 6px;
73
+ border-radius: var(--curvature);
74
74
  }
75
75
 
76
76
  .step-ball {
@@ -252,9 +252,168 @@ export class SortableList extends RapidElement {
252
252
  // Copy form values for the root element and all descendants
253
253
  copyFormValues(element, clone);
254
254
 
255
+ // Inline computed styles onto the clone so it renders faithfully
256
+ // once detached from its original shadow-DOM scope. The ghost is
257
+ // appended to document.body (see startDrag), which means the
258
+ // original's shadow-root-scoped CSS rules no longer apply — chips
259
+ // would lose their flex layout, the X button would lose its shape,
260
+ // and the pill --icon-color would stop inheriting. Walk the cloned
261
+ // light DOM in parallel with the original and inline the layout
262
+ // properties + a curated set of DS custom properties.
263
+ this.inlineComputedStyles(element, clone);
264
+
255
265
  return clone;
256
266
  }
257
267
 
268
+ /** CSS properties copied from the original to the ghost. Covers
269
+ * layout (flex/sizing/spacing), visual (colors, borders, radii,
270
+ * shadows), and text (font, white-space, alignment) — enough to
271
+ * make a faithful free-floating clone without resolving the entire
272
+ * ~400 properties getComputedStyle returns.
273
+ *
274
+ * Important: `getComputedStyle` returns LONGHAND values only —
275
+ * asking for shorthand like `padding` / `margin` / `border` returns
276
+ * an empty string in most browsers. We list longhands explicitly so
277
+ * inlined chip padding / borders survive the move to document.body.
278
+ *
279
+ * Width/height ARE included: nested elements that depend on
280
+ * shadow-root class rules for sized boxes (e.g. select's
281
+ * `.remove-item { width: 16px; height: 16px }`) collapse to their
282
+ * content size without it, leaving a stray gap inside the chip. */
283
+ private static GHOST_COPY_PROPS = [
284
+ 'display',
285
+ 'flex-grow',
286
+ 'flex-shrink',
287
+ 'flex-basis',
288
+ 'flex-direction',
289
+ 'flex-wrap',
290
+ 'align-items',
291
+ 'align-self',
292
+ 'justify-content',
293
+ 'gap',
294
+ 'column-gap',
295
+ 'row-gap',
296
+ 'padding-top',
297
+ 'padding-right',
298
+ 'padding-bottom',
299
+ 'padding-left',
300
+ 'margin-top',
301
+ 'margin-right',
302
+ 'margin-bottom',
303
+ 'margin-left',
304
+ 'border-top-width',
305
+ 'border-right-width',
306
+ 'border-bottom-width',
307
+ 'border-left-width',
308
+ 'border-top-style',
309
+ 'border-right-style',
310
+ 'border-bottom-style',
311
+ 'border-left-style',
312
+ 'border-top-color',
313
+ 'border-right-color',
314
+ 'border-bottom-color',
315
+ 'border-left-color',
316
+ 'border-top-left-radius',
317
+ 'border-top-right-radius',
318
+ 'border-bottom-right-radius',
319
+ 'border-bottom-left-radius',
320
+ 'background-color',
321
+ 'background-image',
322
+ 'color',
323
+ 'font-family',
324
+ 'font-size',
325
+ 'font-weight',
326
+ 'font-style',
327
+ 'line-height',
328
+ 'letter-spacing',
329
+ 'text-align',
330
+ 'white-space',
331
+ 'overflow',
332
+ 'text-overflow',
333
+ 'box-shadow',
334
+ 'opacity',
335
+ 'width',
336
+ 'min-width',
337
+ 'max-width',
338
+ 'height',
339
+ 'min-height',
340
+ 'max-height',
341
+ 'box-sizing',
342
+ 'cursor',
343
+ 'user-select',
344
+ 'vertical-align'
345
+ ];
346
+
347
+ /** Design-system custom properties carried over to the ghost so
348
+ * nested custom elements (temba-icon's --icon-color, pill variants,
349
+ * etc.) read the correct values once detached from the original
350
+ * shadow-root scope. */
351
+ private static GHOST_COPY_CUSTOM_PROPS = [
352
+ '--icon-color',
353
+ '--color-widget-text',
354
+ '--color-text-help',
355
+ '--accent',
356
+ '--accent-100',
357
+ '--accent-200',
358
+ '--accent-700',
359
+ '--flow',
360
+ '--field',
361
+ '--channel',
362
+ '--text-1',
363
+ '--text-2',
364
+ '--sunken',
365
+ '--border',
366
+ '--border-strong',
367
+ '--font-family',
368
+ '--w-regular',
369
+ '--w-medium',
370
+ '--curvature',
371
+ '--curvature-widget'
372
+ ];
373
+
374
+ private inlineComputedStyles(original: Element, clone: Element): void {
375
+ if (
376
+ !(original instanceof HTMLElement) ||
377
+ !(clone instanceof HTMLElement)
378
+ ) {
379
+ return;
380
+ }
381
+
382
+ const apply = (orig: HTMLElement, cln: HTMLElement) => {
383
+ const cs = window.getComputedStyle(orig);
384
+ let inline = '';
385
+ for (const p of SortableList.GHOST_COPY_PROPS) {
386
+ const v = cs.getPropertyValue(p);
387
+ if (v) inline += `${p}:${v};`;
388
+ }
389
+ for (const p of SortableList.GHOST_COPY_CUSTOM_PROPS) {
390
+ const v = cs.getPropertyValue(p);
391
+ if (v) inline += `${p}:${v};`;
392
+ }
393
+ // existing inline style wins by sitting AFTER the inlined
394
+ // computed values — preserves anything we just authored elsewhere
395
+ // (e.g. .option-name's explicit display:flex).
396
+ cln.setAttribute('style', inline + (cln.getAttribute('style') || ''));
397
+ };
398
+
399
+ const walk = (orig: Element, cln: Element) => {
400
+ if (orig instanceof HTMLElement && cln instanceof HTMLElement) {
401
+ apply(orig, cln);
402
+ }
403
+ // Descend into both light-DOM and custom-element subtrees: the
404
+ // latter's slotted/light children still need styles inlined, and
405
+ // its own shadow DOM rebuilds itself when the clone upgrades.
406
+ const oc = Array.from(orig.children);
407
+ const cc = Array.from(cln.children);
408
+ const n = Math.min(oc.length, cc.length);
409
+ for (let i = 0; i < n; i++) {
410
+ walk(oc[i], cc[i]);
411
+ }
412
+ };
413
+
414
+ walk(original, clone);
415
+ }
416
+
258
417
  public getIds() {
259
418
  return this.getSortableElements().map((ele) => ele.id);
260
419
  }
@@ -7,6 +7,7 @@ import {
7
7
  TemplateResult
8
8
  } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
+ import { msg } from '@lit/localize';
10
11
  import {
11
12
  Contact,
12
13
  CustomEventType,
@@ -313,45 +314,51 @@ export class ContactChat extends ContactStoreElement {
313
314
  border-color: #ccc;
314
315
  }
315
316
 
316
- .action-bar {
317
- }
318
-
317
+ /* "Currently in [flow]" treatment.
318
+ Lives in the chat footer to advertise the active run with an
319
+ optional Interrupt action (the chip's X). Sized to its
320
+ contents only (inline-flex) so the chat scrollbar to the
321
+ right remains clickable, and pointer-events:none on the
322
+ wrapping footer means the rest of the row doesn't intercept
323
+ scrollbar drags either. Translucent white bg + backdrop
324
+ blur keeps the chat history legible through the chip. */
319
325
  .in-flow {
320
- border-radius: 0.8em;
321
- align-items: center;
322
- background: #666;
323
- padding: 0.5em 1em;
324
- margin: 1em;
325
- margin-right: 2em;
326
326
  display: inline-flex;
327
- opacity: 0.9;
327
+ align-items: center;
328
+ gap: 8px;
329
+ padding: 0.4em 0.75em;
330
+ margin: 0.5em;
331
+ border-radius: 999px;
332
+ background: rgba(255, 255, 255, 0.75);
333
+ backdrop-filter: blur(8px);
334
+ -webkit-backdrop-filter: blur(8px);
335
+ box-shadow: var(--shadow-1);
328
336
  }
329
337
 
330
338
  .flow-footer {
331
339
  text-align: center;
332
340
  pointer-events: none;
341
+ /* The chat history has a scrollbar on the right edge; the
342
+ footer overlay spans the full container width, so centering
343
+ inside it lands the chip slightly right-of-center relative
344
+ to the visible message area. Reserve the scrollbar width on
345
+ the right so the chip is centered to what the user sees. */
346
+ padding-right: 15px;
333
347
  }
334
348
 
335
349
  .flow-footer .in-flow {
336
350
  pointer-events: auto;
337
351
  }
338
352
 
339
- .in-flow:hover {
340
- opacity: 1;
341
- }
342
-
343
353
  .in-flow .flow-name {
344
- display: flex;
345
- color: #fff;
346
- }
347
-
348
- .in-flow a {
349
- font-weight: bold;
350
- color: #fff;
351
- }
352
-
353
- .in-flow .interrupt-button {
354
- margin-left: 1em;
354
+ display: inline-flex;
355
+ align-items: center;
356
+ gap: 6px;
357
+ /* Match the chat history event text — same hue + size — so
358
+ the "Currently in" line reads as one of the events rather
359
+ than its own UI chrome. */
360
+ font-size: 13.5px;
361
+ color: #8e8e93;
355
362
  }
356
363
 
357
364
  .in-flow .interrupt {
@@ -1420,26 +1427,21 @@ export class ContactChat extends ContactStoreElement {
1420
1427
  <div slot="footer" class="flow-footer">
1421
1428
  <div class="in-flow">
1422
1429
  <div class="flow-name">
1423
- <temba-icon name="flow" size="1.2"></temba-icon>
1424
- <div>
1425
- Currently in
1426
- <a
1427
- href="/flow/editor/${this.currentContact.flow
1428
- .uuid}/"
1429
- >${this.currentContact.flow.name}</a
1430
- >
1431
- </div>
1430
+ <span>Currently in</span>
1431
+ <a
1432
+ href="/flow/editor/${this.currentContact.flow
1433
+ .uuid}/"
1434
+ onclick="goto(event, this)"
1435
+ ><temba-label
1436
+ type="flow"
1437
+ clickable
1438
+ ?removable=${this.showInterrupt}
1439
+ removeLabel=${msg('Interrupt flow')}
1440
+ @temba-remove=${this.handleInterrupt}
1441
+ >${this.currentContact.flow.name}</temba-label
1442
+ ></a
1443
+ >
1432
1444
  </div>
1433
- ${this.showInterrupt
1434
- ? html`<temba-button
1435
- class="interrupt-button"
1436
- destructive
1437
- small
1438
- @click=${this.handleInterrupt}
1439
- name="Interrupt"
1440
- >
1441
- </temba-button>`
1442
- : null}
1443
1445
  </div>
1444
1446
  </div>
1445
1447
  `
@@ -75,6 +75,7 @@ export class ContactDetails extends ContactStoreElement {
75
75
  onclick="goto(event)"
76
76
  href="/contact/group/${group.uuid}/"
77
77
  icon=${group.is_dynamic ? Icon.group_smart : Icon.group}
78
+ type="group"
78
79
  clickable
79
80
  >
80
81
  ${group.name}
@@ -101,16 +101,20 @@ export class ContactFieldEditor extends RapidElement {
101
101
  overflow: hidden;
102
102
  text-overflow: ellipsis;
103
103
  display: flex;
104
+ /* Pin to the top-left of the host (temba-select :host is
105
+ position: relative). Using top rather than margin-top keeps
106
+ the absolute element out of the flex flow of .left-side so
107
+ it doesn't push the selected value down. */
104
108
  position: absolute;
105
- margin-top: -0.6em;
106
- margin-left: 0.5em;
109
+ top: -0.6em;
110
+ left: 0.5em;
107
111
  pointer-events: none;
108
112
  background: #fff;
109
113
  border-radius: var(--curvature);
110
114
  }
111
115
 
112
116
  temba-select .prefix {
113
- margin-top: -1em;
117
+ top: -0.7em;
114
118
  }
115
119
 
116
120
  .wrapper {
@@ -191,19 +195,22 @@ export class ContactFieldEditor extends RapidElement {
191
195
  display: none;
192
196
  }
193
197
 
198
+ /* Keep popper icons within the widget's 34px content area so
199
+ the field height doesn't change when the search/copy buttons
200
+ appear (i.e. when a value is set). Horizontal padding only —
201
+ vertical sizing comes from align-items: stretch on the
202
+ flex .input-container. Inner elements (icons, save button)
203
+ own their own horizontal spacing via margin, so the popper
204
+ itself doesn't add asymmetric padding (which would offset
205
+ its contents and prevent centering). */
194
206
  .popper temba-icon {
195
- padding: 0.5em 0em;
196
- padding-right: 1em;
207
+ padding: 0 0.6em 0 0;
197
208
  }
198
209
 
199
- .popper:first-child {
200
- padding: 0.5em 0em;
201
- padding-right: 0.5em;
202
- padding-left: 1em;
203
- }
204
-
205
- .popper:last-child {
206
- padding-right: 0em;
210
+ /* First icon gets extra left padding so it doesn't hug the
211
+ popper's left edge — visually balances the inter-icon gap. */
212
+ .popper temba-icon:first-of-type {
213
+ padding-left: 0.6em;
207
214
  }
208
215
 
209
216
  .copy.clicked temba-icon {
@@ -223,8 +230,21 @@ export class ContactFieldEditor extends RapidElement {
223
230
  align-items: center;
224
231
  }
225
232
 
226
- .save-button {
227
- padding-right: 1em;
233
+ /* .save-button is the class on the <temba-button> element
234
+ itself. Use tag+class selector and !important to outrank
235
+ :host { align-self: stretch } from inside Button.ts. min/max
236
+ height pin the button size so it can't grow with the parent
237
+ line height (relevant in DatePicker, where the container
238
+ wraps and the line is ~50px tall). */
239
+ temba-button.save-button {
240
+ align-self: center !important;
241
+ height: 22px !important;
242
+ min-height: 22px !important;
243
+ max-height: 22px !important;
244
+ margin: 5px 6px;
245
+ --button-y: 0;
246
+ --button-x: 10px;
247
+ font-size: 12px;
228
248
  }
229
249
 
230
250
  .dirty .copy,
@@ -272,22 +292,6 @@ export class ContactFieldEditor extends RapidElement {
272
292
  padding: 0;
273
293
  }
274
294
 
275
- .dirty temba-datepicker .popper:first-child {
276
- padding-left: 1em;
277
- }
278
-
279
- .success temba-datepicker .popper:first-child {
280
- padding-left: 1em;
281
- }
282
-
283
- .failure temba-datepicker .popper:first-child {
284
- padding-left: 1em;
285
- }
286
-
287
- .saving temba-datepicker .popper:first-child {
288
- padding-left: 1em;
289
- }
290
-
291
295
  temba-datepicker .postfix {
292
296
  margin-left: 0;
293
297
  }
@@ -305,6 +309,9 @@ export class ContactFieldEditor extends RapidElement {
305
309
 
306
310
  temba-select {
307
311
  --color-widget-bg: white;
312
+ /* Let the slotted prefix label escape the widget's top edge
313
+ — same notched-border look as the textinput / datepicker. */
314
+ --temba-select-container-overflow: visible;
308
315
  }
309
316
 
310
317
  temba-option {
@@ -247,14 +247,14 @@ export class FieldManager extends EndpointMonitorElement {
247
247
  <div
248
248
  style="display: flex; min-width: 200px; width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-right: 2em"
249
249
  >
250
- <span
250
+ <temba-label
251
+ type="field"
252
+ clickable
251
253
  @click=${this.handleFieldAction}
252
254
  data-key=${field.key}
253
255
  data-action="update"
254
- style="color: var(--color-link-primary); cursor:pointer;"
256
+ >${field.label}</temba-label
255
257
  >
256
- ${field.label}
257
- </span>
258
258
  ${this.hasUsages(field)
259
259
  ? html`
260
260
  <temba-icon
@@ -0,0 +1,145 @@
1
+ import { css } from 'lit';
2
+
3
+ /**
4
+ * TextIt Design System tokens — single source of truth.
5
+ *
6
+ * Embedded in component shadow DOMs (via FieldElement :host) so the
7
+ * tokens apply regardless of host-page stylesheet. Host pages may still
8
+ * override these by re-declaring the variable on the component element
9
+ * itself (e.g. `temba-select { --accent: ... }`), which beats `:host`.
10
+ *
11
+ * Mirrored in static/css/temba-components.css for hosts that want the
12
+ * tokens at :root scope; keep both in sync if the design system evolves.
13
+ */
14
+ export const designTokens = css`
15
+ :host {
16
+ /* accent ramp — derived from a single anchor via OKLab mixing */
17
+ --accent: #2a6fb5;
18
+ --accent-50: color-mix(in oklab, var(--accent) 6%, white);
19
+ --accent-100: color-mix(in oklab, var(--accent) 12%, white);
20
+ --accent-200: color-mix(in oklab, var(--accent) 25%, white);
21
+ --accent-300: color-mix(in oklab, var(--accent) 45%, white);
22
+ --accent-400: color-mix(in oklab, var(--accent) 75%, white);
23
+ --accent-500: var(--accent);
24
+ --accent-600: color-mix(in oklab, var(--accent) 88%, black);
25
+ --accent-700: color-mix(in oklab, var(--accent) 75%, black);
26
+ --accent-800: color-mix(in oklab, var(--accent) 60%, black);
27
+ --accent-900: color-mix(in oklab, var(--accent) 45%, black);
28
+
29
+ /* neutrals */
30
+ --bg: #f6f7f9;
31
+ --surface: #ffffff;
32
+ --sunken: #f1f3f5;
33
+ --border: #e6e8ec;
34
+ --border-strong: #d2d6dc;
35
+ --text-1: #1a1f26;
36
+ --text-2: #4d5664;
37
+ --text-3: #7b8593;
38
+ --text-4: #a2abb8;
39
+
40
+ /* status — full set */
41
+ --success: #16a34a;
42
+ --success-bg: #e8f6ee;
43
+ --success-border: #bfe5cd;
44
+ --info: #2563eb;
45
+ --info-bg: #e8f0fe;
46
+ --info-border: #c7d7f8;
47
+ --warning: #b45309;
48
+ --warning-bg: #fdf3e2;
49
+ --warning-border: #f2d9a9;
50
+ --danger: #d03f3f;
51
+ --danger-bg: #fcebeb;
52
+ --danger-border: #f4c8c8;
53
+ --neutral: #6b7280;
54
+ --neutral-bg: #eef0f3;
55
+ --neutral-border: #d8dce2;
56
+
57
+ /* Pill anchor hues — pillVariants derives bg/fg/border via
58
+ color-mix(in oklab, ...) so host pages can re-theme by
59
+ overriding just the anchor. (Recipient pills reuse --accent.) */
60
+ --flow: #16a34a;
61
+ --channel: #6b21a8;
62
+ /* Field stays slightly darker than the bright yellow-500 anchor
63
+ used for flow/channel — yellow-500 has too little contrast
64
+ against white to read as a foreground / icon hue on its own.
65
+ Yellow-700 doubles as the pill's icon color via .pill-field. */
66
+ --field: #a16207;
67
+
68
+ /* type */
69
+ --font: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
70
+ --font-mono:
71
+ 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
72
+ --w-regular: 400;
73
+ --w-medium: 500;
74
+ --w-semibold: 600;
75
+ --w-bold: 700;
76
+
77
+ /* shape */
78
+ --r: 8px;
79
+ --r-xs: 2px;
80
+ --r-sm: 4px;
81
+ --r-lg: 12px;
82
+
83
+ /* density */
84
+ --row-h: 36px;
85
+ --input-h: 34px;
86
+ --pad: 10px;
87
+ --gap: 14px;
88
+
89
+ /* shadows */
90
+ --shadow-1:
91
+ 0 1px 1px rgba(15, 22, 36, 0.04), 0 1px 2px rgba(15, 22, 36, 0.04);
92
+ --shadow-2:
93
+ 0 1px 1px rgba(15, 22, 36, 0.04), 0 4px 12px rgba(15, 22, 36, 0.06);
94
+ --shadow-3:
95
+ 0 6px 20px rgba(15, 22, 36, 0.1), 0 2px 6px rgba(15, 22, 36, 0.06);
96
+
97
+ /* legacy aliases — point at the DS tokens above so existing
98
+ components pick up the new design language without code changes */
99
+ --font-family: var(--font);
100
+ --curvature: var(--r-sm);
101
+ --curvature-widget: var(--r-sm);
102
+ /* Focus styling.
103
+ --focus is the single hue, kept separate from --accent so
104
+ changing the focus color doesn't shift chip / recipient hues.
105
+ --focus-muted and --focus-halo are derived once here, so
106
+ everywhere that needs to draw a focus outline or ring can
107
+ consume the same values without re-doing the formula.
108
+ --color-focus / --widget-box-shadow-focused alias the muted
109
+ versions and are what most widgets reference — these get
110
+ overridden to error-red by FieldElement's .has-error rule.
111
+ Surfaces that should stay blue even during a parent field's
112
+ error state (e.g. the dropdown popup) reference --focus-muted
113
+ / --focus-halo directly to skip that override. */
114
+ --focus: #5b9ce5;
115
+ --focus-muted: color-mix(in oklab, var(--focus) 60%, white);
116
+ --focus-halo: 0 0 0 3px color-mix(in oklab, var(--focus) 30%, transparent);
117
+ --color-focus: var(--focus-muted);
118
+ --widget-box-shadow-focused: var(--focus-halo);
119
+
120
+ --color-widget-bg: var(--surface);
121
+ --color-widget-bg-focused: var(--surface);
122
+ --color-widget-border: var(--border-strong);
123
+ --color-options-bg: var(--surface);
124
+ --color-selection: var(--accent-50);
125
+ --color-success: var(--success);
126
+ --widget-box-shadow: none;
127
+ --shadow: var(--shadow-1);
128
+ --shadow-widget: var(--shadow-1);
129
+ --color-text: var(--text-1);
130
+ --color-widget-text: var(--text-1);
131
+ --color-borders: var(--border);
132
+ --color-placeholder: var(--text-3);
133
+ --color-primary-light: var(--sunken);
134
+ --color-label: var(--text-1);
135
+ --color-text-help: var(--text-3);
136
+
137
+ --temba-textinput-padding: 7px var(--pad);
138
+ --temba-textinput-font-size: 13.5px;
139
+ --temba-textinput-min-height: var(--input-h);
140
+ --temba-select-selected-padding: 0 var(--pad);
141
+ --temba-select-selected-line-height: 1.4;
142
+ --temba-select-selected-font-size: 13.5px;
143
+ --temba-select-min-height: var(--input-h);
144
+ }
145
+ `;