@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
@@ -9,6 +9,7 @@ import { fetchResults } from '../utils';
9
9
  import { FLOW_SPEC_VERSION } from '../store/AppState';
10
10
  import {
11
11
  labelsFor,
12
+ normalizeChanges,
12
13
  RevisionChanges,
13
14
  summarizeChanges
14
15
  } from './revision-summary';
@@ -151,7 +152,7 @@ export class RevisionsWindow extends RapidElement {
151
152
  value=${rev.created_on}
152
153
  display="duration"
153
154
  ></temba-date>
154
- · ${rev.user.name || rev.user.username}
155
+ · ${this.renderUser(rev.user)}
155
156
  </div>
156
157
  ${isCurrent
157
158
  ? html`<div
@@ -192,6 +193,13 @@ export class RevisionsWindow extends RapidElement {
192
193
 
193
194
  // --- Private ---
194
195
 
196
+ private renderUser(user: Revision['user']): TemplateResult | string {
197
+ if (user?.email === 'system') {
198
+ return html`<em>System update</em>`;
199
+ }
200
+ return user?.name || user?.username || '';
201
+ }
202
+
195
203
  private async fetchRevisions() {
196
204
  const requestId = ++this.fetchRequestId;
197
205
  this.isLoading = true;
@@ -217,19 +225,36 @@ export class RevisionsWindow extends RapidElement {
217
225
  // the window. The merged revision is capped at three distinct displayed
218
226
  // labels — once a fourth would be introduced we break out into a new row.
219
227
  private collapseRevisions(revisions: Revision[]): Revision[] {
228
+ // Normalize at the boundary so the rest of the logic reasons about real
229
+ // edits only. After this step, `changes === null` is the single signal
230
+ // for "no-op" — used both for the author-barrier bypass and for keeping
231
+ // housekeeping tags out of the merged tag set and label cap.
232
+ const cleaned = revisions.map((r) => ({
233
+ ...r,
234
+ changes: normalizeChanges(r.changes)
235
+ }));
220
236
  // The API returns newest-first today; sort defensively so the head/window
221
237
  // logic stays correct if that ever changes.
222
- const sorted = [...revisions].sort(
238
+ const sorted = [...cleaned].sort(
223
239
  (a, b) =>
224
240
  new Date(b.created_on).getTime() - new Date(a.created_on).getTime()
225
241
  );
226
242
  const result: Revision[] = [];
227
243
  let group: Revision[] = [];
228
244
  let groupLabels = new Set<string>();
245
+ let groupHasRealChange = false;
229
246
 
230
247
  const flush = () => {
231
248
  if (group.length === 0) return;
232
249
  const head = group[0];
250
+ // Pick the user from the most recent real-change revision in the
251
+ // group. No-op authors (typically the system, doing spec bumps)
252
+ // shouldn't appear as the editor when a real user's edit was
253
+ // absorbed into the row — that would mislabel the change as
254
+ // "System update" even though a real person did the work. Fall back
255
+ // to the head if every revision was a no-op.
256
+ const realChange = group.find((r) => r.changes);
257
+ const displayUser = realChange?.user ?? head.user;
233
258
  const tagSet = new Set<string>();
234
259
  let anyKnown = false;
235
260
  for (const r of group) {
@@ -240,41 +265,60 @@ export class RevisionsWindow extends RapidElement {
240
265
  }
241
266
  result.push({
242
267
  ...head,
268
+ user: displayUser,
243
269
  changes: anyKnown ? { tags: Array.from(tagSet) } : null
244
270
  });
245
271
  group = [];
246
272
  groupLabels = new Set();
273
+ groupHasRealChange = false;
247
274
  };
248
275
 
249
276
  for (const rev of sorted) {
250
277
  if (group.length === 0) {
251
278
  group.push(rev);
252
279
  groupLabels = labelsFor(rev.changes);
280
+ groupHasRealChange = !!rev.changes;
253
281
  continue;
254
282
  }
255
283
  const head = group[0];
256
284
  const headTime = new Date(head.created_on).getTime();
257
285
  const revTime = new Date(rev.created_on).getTime();
258
- const withinWindow = headTime - revTime < GROUP_WINDOW_MS;
259
286
  // Compare on whichever identifier the server provides — real data
260
287
  // arrives with `email`, while test fixtures use `username`. Falling
261
288
  // back through the chain keeps both shapes working.
262
289
  const headId = head.user?.email ?? head.user?.username;
263
290
  const revId = rev.user?.email ?? rev.user?.username;
264
- const sameAuthor = headId === revId;
265
- const prospective = new Set([
266
- ...groupLabels,
267
- ...labelsFor(rev.changes)
268
- ]);
269
- const fitsLabelCap = prospective.size <= MAX_GROUP_LABELS;
291
+ // Two conditions bypass the time/author barriers:
292
+ // 1. The incoming rev is itself a no-op — it carries no editorial
293
+ // intent and should disappear into whichever group it neighbors.
294
+ // 2. The group hasn't accumulated a real change yet — we never want
295
+ // to surface a row showing "nothing changed", so a no-op-only
296
+ // chain reaches forward to absorb the first real edit even if
297
+ // that edit is far away in time or by a different author.
298
+ const isNoOp = !rev.changes;
299
+ const bypassBarriers = isNoOp || !groupHasRealChange;
300
+ const withinWindow =
301
+ bypassBarriers || headTime - revTime < GROUP_WINDOW_MS;
302
+ const sameAuthor = bypassBarriers || headId === revId;
303
+ const prospective = new Set([...groupLabels, ...labelsFor(rev.changes)]);
304
+ // The label cap is meaningful only when adding a real change to a
305
+ // group that already has one. A no-op contributes zero labels by
306
+ // construction, so it never trips the cap; and a no-op-only chain
307
+ // reaching forward to absorb its first real edit must ignore the cap
308
+ // too, or a sweeping edit (4+ label areas) would still strand the
309
+ // no-op group as an empty-summary row.
310
+ const fitsLabelCap =
311
+ isNoOp || !groupHasRealChange || prospective.size <= MAX_GROUP_LABELS;
270
312
 
271
313
  if (withinWindow && sameAuthor && fitsLabelCap) {
272
314
  group.push(rev);
273
315
  groupLabels = prospective;
316
+ if (!isNoOp) groupHasRealChange = true;
274
317
  } else {
275
318
  flush();
276
319
  group.push(rev);
277
320
  groupLabels = labelsFor(rev.changes);
321
+ groupHasRealChange = !!rev.changes;
278
322
  }
279
323
  }
280
324
  flush();
@@ -69,6 +69,14 @@ const advancedSection = {
69
69
  !!(formData.localizeRules || formData.localizeCategories)
70
70
  };
71
71
 
72
+ const categoriesLocalizationSection = {
73
+ label: 'Localization',
74
+ localizable: false,
75
+ items: ['localizeCategories'],
76
+ collapsed: true,
77
+ getValueCount: (formData: FormData) => !!formData.localizeCategories
78
+ };
79
+
72
80
  export const nodeOptionsAccordion: AccordionLayoutConfig = {
73
81
  type: 'accordion',
74
82
  multi: true,
@@ -81,6 +89,12 @@ export const nodeOptionsAccordionSimple: AccordionLayoutConfig = {
81
89
  sections: [resultNameSection]
82
90
  };
83
91
 
92
+ export const nodeOptionsAccordionCategoriesOnly: AccordionLayoutConfig = {
93
+ type: 'accordion',
94
+ multi: true,
95
+ sections: [resultNameSection, categoriesLocalizationSection]
96
+ };
97
+
84
98
  /**
85
99
  * Shared category localization functions for router nodes.
86
100
  * These provide a consistent way to localize category names across all router types.
@@ -1,16 +1,25 @@
1
- import { FormData, NodeConfig, ACTION_GROUPS, Features } from '../types';
1
+ import {
2
+ FormData,
3
+ NodeConfig,
4
+ ACTION_GROUPS,
5
+ FlowTypes
6
+ } from '../types';
2
7
  import { CallLLM, Node } from '../../store/flow-definition';
3
8
  import { generateUUID, createMultiCategoryRouter } from '../../utils';
4
9
  import { html } from 'lit';
5
10
  import { validateWith } from '../utils';
6
11
  import { LLMModel, hasLLMRole } from '../flow-utils';
12
+ import {
13
+ resultNameField,
14
+ localizeCategoriesField,
15
+ nodeOptionsAccordionCategoriesOnly
16
+ } from './shared';
7
17
 
8
18
  export const split_by_llm_categorize: NodeConfig = {
9
19
  type: 'split_by_llm_categorize',
10
20
  name: 'Split by AI',
11
21
  group: ACTION_GROUPS.services,
12
- flowTypes: [],
13
- features: [Features.AI],
22
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
14
23
  form: {
15
24
  llm: {
16
25
  type: 'select',
@@ -49,9 +58,11 @@ export const split_by_llm_categorize: NodeConfig = {
49
58
  required: true
50
59
  }
51
60
  }
52
- }
61
+ },
62
+ result_name: resultNameField,
63
+ localizeCategories: localizeCategoriesField
53
64
  },
54
- layout: ['llm', 'input', 'categories'],
65
+ layout: ['llm', 'input', 'categories', nodeOptionsAccordionCategoriesOnly],
55
66
  validate: validateWith((formData, errors) => {
56
67
  if (!formData.categories || !Array.isArray(formData.categories)) return;
57
68
 
@@ -91,7 +102,7 @@ export const split_by_llm_categorize: NodeConfig = {
91
102
  <div class="body">Categorize with ${callLlmAction.llm.name}</div>
92
103
  `;
93
104
  },
94
- toFormData: (node: Node) => {
105
+ toFormData: (node: Node, nodeUI?: any) => {
95
106
  // Extract data from the existing node structure
96
107
  const callLlmAction = node.actions?.find(
97
108
  (action) => action.type === 'call_llm'
@@ -105,9 +116,18 @@ export const split_by_llm_categorize: NodeConfig = {
105
116
  uuid: node.uuid,
106
117
  llm: callLlmAction?.llm ? [callLlmAction.llm] : [],
107
118
  input: callLlmAction?.input || '@input',
108
- categories: categories
119
+ categories: categories,
120
+ result_name: node.router?.result_name || '',
121
+ localizeCategories: nodeUI?.config?.localizeCategories || false
109
122
  };
110
123
  },
124
+ toUIConfig: (formData: FormData) => {
125
+ const config: Record<string, any> = {};
126
+ config.localizeCategories = formData.result_name
127
+ ? !!formData.localizeCategories
128
+ : false;
129
+ return config;
130
+ },
111
131
  fromFormData: (formData: FormData, originalNode: Node): Node => {
112
132
  // Get LLM selection
113
133
  const llmSelection =
@@ -158,11 +178,16 @@ export const split_by_llm_categorize: NodeConfig = {
158
178
  existingCases
159
179
  );
160
180
 
181
+ const finalRouter: any = { ...router };
182
+ if (formData.result_name && formData.result_name.trim() !== '') {
183
+ finalRouter.result_name = formData.result_name.trim();
184
+ }
185
+
161
186
  // Return the complete node
162
187
  return {
163
188
  uuid: originalNode.uuid,
164
189
  actions: [callLlmAction],
165
- router: router,
190
+ router: finalRouter,
166
191
  exits: exits
167
192
  };
168
193
  },
@@ -2,6 +2,23 @@ export interface RevisionChanges {
2
2
  tags: string[];
3
3
  }
4
4
 
5
+ // "spec" is the housekeeping tag the system attaches when it bumps a flow's
6
+ // spec version. It carries no editorial intent, so we strip it at the
7
+ // boundary — every downstream consumer (summaries, label caps, no-op
8
+ // detection) then operates on a clean tag set without needing special cases.
9
+ const NOOP_TAGS = new Set(['spec']);
10
+
11
+ // Drop tags that don't represent real edits and collapse to null when nothing
12
+ // meaningful remains. Returning null lets `isNoOpChanges` and the collapse
13
+ // logic treat empty-after-filtering and originally-null the same way.
14
+ export function normalizeChanges(
15
+ changes: RevisionChanges | null | undefined
16
+ ): RevisionChanges | null {
17
+ if (!changes) return null;
18
+ const tags = (changes.tags || []).filter((t) => !NOOP_TAGS.has(t));
19
+ return tags.length === 0 ? null : { tags };
20
+ }
21
+
5
22
  const TAG_LABELS: Record<string, { label: string; order: number }> = {
6
23
  metadata: { label: 'metadata', order: 0 },
7
24
  nodes: { label: 'nodes', order: 1 },
@@ -32,6 +49,14 @@ export function labelsFor(
32
49
  return result;
33
50
  }
34
51
 
52
+ // A revision is a no-op when, after stripping housekeeping tags, nothing
53
+ // meaningful is left. These shouldn't break up adjacent edits in the browser.
54
+ export function isNoOpChanges(
55
+ changes: RevisionChanges | null | undefined
56
+ ): boolean {
57
+ return normalizeChanges(changes) === null;
58
+ }
59
+
35
60
  export function summarizeChanges(
36
61
  changes: RevisionChanges | null | undefined
37
62
  ): string {
package/src/flow/utils.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { html, TemplateResult } from 'lit-html';
2
+ import { iconToPillType } from '../styles/pillVariants';
2
3
  import { Action, NamedObject, FlowPosition } from '../store/flow-definition';
3
4
  import { FlowIssue, zustand } from '../store/AppState';
4
5
  import { CustomEventType } from '../interfaces';
@@ -256,6 +257,15 @@ export const renderClamped = (
256
257
  </div>`;
257
258
  };
258
259
 
260
+ /**
261
+ * Inline margin for stacked pills — the previous implementation used
262
+ * `class="mr-1 mb-1"` (Tailwind utility classes), but this package
263
+ * doesn't ship Tailwind, so the classes resolved to no-ops in any host
264
+ * page that didn't already include it. Inline style is predictable
265
+ * across hosts.
266
+ */
267
+ const PILL_MARGIN_STYLE = 'margin: 0 4px 4px 0;';
268
+
259
269
  /**
260
270
  * Renders a single line item with optional icon.
261
271
  * Content can be plain text or a TemplateResult (e.g. highlighted text).
@@ -265,17 +275,14 @@ export const renderLineItem = (
265
275
  icon?: string,
266
276
  content?: TemplateResult
267
277
  ) => {
268
- return html`<div style="display:flex;align-items:center;">
269
- ${icon
270
- ? html`<temba-icon name=${icon} style="margin-right:0.5em"></temba-icon>`
271
- : null}
272
- <div
273
- style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 250px;"
274
- title="${name}"
275
- >
276
- ${content || name}
277
- </div>
278
- </div>`;
278
+ const pillType = iconToPillType(icon);
279
+ return html`<temba-label
280
+ icon=${icon || ''}
281
+ type=${pillType || 'neutral'}
282
+ style=${PILL_MARGIN_STYLE}
283
+ title="${name}"
284
+ >${content || name}</temba-label
285
+ >`;
279
286
  };
280
287
 
281
288
  /**
@@ -319,12 +326,9 @@ export const renderStringList = (
319
326
  if (items.length > maxDisplay && items.length !== 4) {
320
327
  const remainingCount = items.length - maxDisplay;
321
328
  itemElements.push(
322
- html`<div style="display:flex;align-items:center;margin-top:0.2em;">
323
- ${icon
324
- ? html`<div style="margin-right:0.4em; width: 1em;"></div>` // spacing placeholder
325
- : null}
326
- <div style="font-size:0.8em">+${remainingCount} more</div>
327
- </div>`
329
+ html`<temba-label type="neutral" style=${PILL_MARGIN_STYLE}
330
+ >+${remainingCount} more</temba-label
331
+ >`
328
332
  );
329
333
  }
330
334
  return itemElements;
@@ -368,17 +372,16 @@ export const renderMixedList = (items: MixedListItem[]) => {
368
372
  if (items.length > maxDisplay && items.length !== 4) {
369
373
  const remainingCount = items.length - maxDisplay;
370
374
  itemElements.push(
371
- html`<div style="display:flex;align-items:center;margin-top:0.2em;">
372
- <div style="margin-right:0.4em; width: 1em;"></div>
373
- <div style="font-size:0.8em">+${remainingCount} more</div>
374
- </div>`
375
+ html`<temba-label type="neutral" style=${PILL_MARGIN_STYLE}
376
+ >+${remainingCount} more</temba-label
377
+ >`
375
378
  );
376
379
  }
377
380
  return itemElements;
378
381
  };
379
382
 
380
383
  /**
381
- * Renders a named object as a clickable link that fires a custom event
384
+ * Renders a named object as a clickable DS pill that fires a custom event
382
385
  */
383
386
  const renderLinkedObject = (
384
387
  obj: NamedObject,
@@ -400,18 +403,16 @@ const renderLinkedObject = (
400
403
  }
401
404
  };
402
405
 
403
- return html`<div style="display:flex;align-items:center;max-width:100%;">
404
- ${icon
405
- ? html`<temba-icon name=${icon} style="margin-right:0.5em"></temba-icon>`
406
- : null}
407
- <div
408
- class="linked-name"
409
- @click=${handleClick}
410
- style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 250px; text-decoration: underline; cursor: pointer;"
411
- >
412
- ${obj.name}
413
- </div>
414
- </div>`;
406
+ const pillType = iconToPillType(icon);
407
+ return html`<temba-label
408
+ icon=${icon || ''}
409
+ type=${pillType || 'neutral'}
410
+ clickable
411
+ style=${PILL_MARGIN_STYLE}
412
+ title="${obj.name}"
413
+ @click=${handleClick}
414
+ >${obj.name}</temba-label
415
+ >`;
415
416
  };
416
417
 
417
418
  /**
@@ -436,12 +437,9 @@ const renderLinkedObjects = (
436
437
  if (objects.length > maxDisplay && objects.length !== 4) {
437
438
  const remainingCount = objects.length - maxDisplay;
438
439
  itemElements.push(
439
- html`<div style="display:flex;align-items:center;margin-top:0.2em;">
440
- ${icon
441
- ? html`<div style="margin-right:0.4em; width: 1em;"></div>`
442
- : null}
443
- <div style="font-size:0.8em">+${remainingCount} more</div>
444
- </div>`
440
+ html`<temba-label type="neutral" style=${PILL_MARGIN_STYLE}
441
+ >+${remainingCount} more</temba-label
442
+ >`
445
443
  );
446
444
  }
447
445
  return itemElements;
@@ -604,12 +604,11 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
604
604
  class="remove-btn"
605
605
  style="
606
606
  padding: 4px;
607
- border: 1px solid #ccc;
608
- border-radius: 4px;
609
- background: white;
607
+ border: 1px solid var(--color-widget-border);
608
+ border-radius: var(--curvature-widget);
609
+ background: var(--surface);
610
610
  cursor: pointer;
611
- background: #fefefe;
612
- color: #999;
611
+ color: var(--text-3);
613
612
  font-size: 14px;
614
613
  "
615
614
  ?disabled=${!canRemove}
@@ -651,21 +650,20 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
651
650
  .add-btn,
652
651
  .remove-btn {
653
652
  padding: 4px;
654
- border: 1px solid #ccc;
655
- border-radius: 4px;
656
- background: white;
653
+ border: 1px solid var(--color-widget-border);
654
+ border-radius: var(--curvature-widget);
655
+ background: var(--surface);
657
656
  cursor: pointer;
658
657
  font-size: 14px;
659
658
  }
660
659
 
661
660
  .add-btn:hover,
662
661
  .remove-btn:hover {
663
- background: #f8f8f8;
662
+ background: var(--sunken);
664
663
  }
665
664
 
666
665
  .remove-btn {
667
- background: #fefefe;
668
- color: #999;
666
+ color: var(--text-3);
669
667
  }
670
668
 
671
669
  .removable .remove-btn {
@@ -25,7 +25,7 @@ export class Checkbox extends FieldElement {
25
25
  width: 12px;
26
26
  height: 12px;
27
27
  background: var(--checkbox-background, rgba(255, 255, 255, 0.8));
28
- border-radius: 2px;
28
+ border-radius: var(--r-xs);
29
29
  }
30
30
 
31
31
  .wrapper.label {
@@ -34,7 +34,7 @@ export class Checkbox extends FieldElement {
34
34
  }
35
35
 
36
36
  .wrapper.label:hover {
37
- background: var(--checkbox-hover-bg, #f9f9f9);
37
+ background: var(--checkbox-hover-bg, var(--sunken));
38
38
  }
39
39
 
40
40
  .checkbox-container {
@@ -104,7 +104,7 @@ export class Compose extends FieldElement {
104
104
  --curvature-widget: 0px;
105
105
  --color-options-bg: #fff;
106
106
  border: 1px solid var(--color-widget-border);
107
- border-radius: 6px;
107
+ border-radius: var(--curvature-widget);
108
108
  background: var(--color-widget-bg, #fff);
109
109
  box-shadow: var(--options-shadow);
110
110
  z-index: 1000003;
@@ -2,6 +2,7 @@ import { TemplateResult, html, css } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
3
  import { RapidElement } from '../RapidElement';
4
4
  import { renderMarkdownInline } from '../markdown';
5
+ import { designTokens } from '../styles/designTokens';
5
6
 
6
7
  /**
7
8
  * FieldElement is a base class for form components that provides built-in
@@ -62,26 +63,25 @@ export abstract class FieldElement extends RapidElement {
62
63
 
63
64
  static get styles() {
64
65
  return css`
66
+ ${designTokens}
67
+
65
68
  :host {
66
69
  font-family: var(--font-family);
67
70
  }
68
71
 
69
72
  label {
70
- margin-bottom: 5px;
71
- margin-left: 4px;
73
+ margin-bottom: 6px;
72
74
  display: block;
73
- font-weight: 400;
74
- font-size: var(--label-size);
75
- letter-spacing: 0.05em;
75
+ font-weight: var(--w-medium);
76
+ font-size: 12.5px;
76
77
  line-height: normal;
77
- color: var(--color-label, #777);
78
+ color: var(--color-label);
78
79
  }
79
80
 
80
81
  .help-text {
81
- font-size: var(--help-text-size);
82
+ font-size: 12px;
82
83
  line-height: normal;
83
84
  color: var(--color-text-help);
84
- margin-left: var(--help-text-margin-left);
85
85
  margin-top: 6px;
86
86
  opacity: 1;
87
87
  }
@@ -282,10 +282,10 @@ export class KeyValueEditor extends BaseListEditor<KeyValueItem> {
282
282
  .remove-btn {
283
283
  width: 32px;
284
284
  height: 32px;
285
- border: 1px solid #ccc;
286
- border-radius: 4px;
287
- background: #f8f8f8;
288
- color: #666;
285
+ border: 1px solid var(--color-widget-border);
286
+ border-radius: var(--curvature-widget);
287
+ background: var(--sunken);
288
+ color: var(--text-2);
289
289
  cursor: pointer;
290
290
  display: flex;
291
291
  align-items: center;
@@ -23,7 +23,7 @@ export class MessageEditor extends FieldElement {
23
23
  .message-editor-container {
24
24
  border: 1px solid var(--color-widget-border);
25
25
  border-radius: var(--curvature-widget);
26
- background: #fff;
26
+ background: var(--surface);
27
27
  position: relative;
28
28
  transition:
29
29
  border-color 0.2s ease-in-out,
@@ -75,10 +75,9 @@ export class MessageEditor extends FieldElement {
75
75
 
76
76
  .media-wrapper {
77
77
  padding: 4px 8px;
78
- background: rgba(0, 0, 0, 0.03);
78
+ background: var(--sunken);
79
79
  border-top: 1px solid var(--color-widget-border);
80
80
  border-radius: 0 0 var(--curvature-widget) var(--curvature-widget);
81
- box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.05);
82
81
  margin-top: 3px;
83
82
  display: none;
84
83
  }
@@ -16,21 +16,21 @@ export class RangePicker extends RapidElement {
16
16
  cursor: pointer;
17
17
  padding: 0.2em 0.5em;
18
18
  margin: 0.6em 0;
19
- border-radius: 4px;
19
+ border-radius: var(--curvature-widget);
20
20
  border: 1px solid transparent;
21
21
  transition: border 0.2s;
22
22
  }
23
23
 
24
24
  .date-display:hover {
25
- border: 1px solid var(--color-widget-border, #bbb);
26
- background: var(--color-widget-hover, #f5f5f5);
25
+ border: 1px solid var(--color-widget-border);
26
+ background: var(--sunken);
27
27
  }
28
28
 
29
29
  input[type='date'] {
30
30
  font-size: 1em;
31
31
  padding: 0.2em 0.5em;
32
- border-radius: 4px;
33
- border: 1px solid #bbb;
32
+ border-radius: var(--curvature-widget);
33
+ border: 1px solid var(--color-widget-border);
34
34
  }
35
35
 
36
36
  .navigation-container {
@@ -40,10 +40,10 @@ export class RangePicker extends RapidElement {
40
40
  }
41
41
 
42
42
  .nav-arrow {
43
- background: #f5f5f5;
44
- border: 1px solid #bbb;
43
+ background: var(--sunken);
44
+ border: 1px solid var(--color-widget-border);
45
45
 
46
- border-radius: var(--curvature);
46
+ border-radius: var(--curvature-widget);
47
47
  padding: 0em 0em;
48
48
  cursor: pointer;
49
49
  font-size: 0.6em;
@@ -59,14 +59,14 @@ export class RangePicker extends RapidElement {
59
59
  }
60
60
 
61
61
  .nav-arrow:hover:not(:disabled) {
62
- background: #e0eaff;
63
- border-color: #3399ff;
62
+ background: var(--accent-50);
63
+ border-color: var(--accent);
64
64
  }
65
65
 
66
66
  .nav-arrow:disabled {
67
67
  opacity: 0.5;
68
68
  cursor: not-allowed;
69
- background: #f9f9f9;
69
+ background: var(--sunken);
70
70
  }
71
71
 
72
72
  .nav-arrow.hidden {
@@ -78,8 +78,8 @@ export class RangePicker extends RapidElement {
78
78
  margin-left: 0em;
79
79
  }
80
80
  .range-btn {
81
- background: #f5f5f5;
82
- border: 1px solid #bbb;
81
+ background: var(--sunken);
82
+ border: 1px solid var(--color-widget-border);
83
83
  border-radius: 0px;
84
84
  margin-left: -1px;
85
85
  padding: 0.2em 0.8em;
@@ -91,17 +91,17 @@ export class RangePicker extends RapidElement {
91
91
  }
92
92
 
93
93
  .button-group .range-btn:first-child {
94
- border-radius: 4px 0 0 4px;
94
+ border-radius: var(--curvature-widget) 0 0 var(--curvature-widget);
95
95
  }
96
96
 
97
97
  .button-group .range-btn:last-child {
98
- border-radius: 0 4px 4px 0;
98
+ border-radius: 0 var(--curvature-widget) var(--curvature-widget) 0;
99
99
  }
100
100
 
101
101
  .range-btn.selected,
102
102
  .range-btn:active {
103
- background: #e0eaff;
104
- border-color: #3399ff;
103
+ background: var(--accent-50);
104
+ border-color: var(--accent);
105
105
  }
106
106
  `;
107
107