@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.
- package/CHANGELOG.md +17 -0
- package/dist/temba-components.js +1189 -767
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/display/Chat.ts +14 -0
- package/src/display/Label.ts +156 -2
- package/src/display/Options.ts +71 -16
- package/src/display/TembaUser.ts +23 -5
- package/src/events/eventRenderers.ts +104 -41
- package/src/excellent/caret-utils.ts +0 -1
- package/src/flow/RevisionsWindow.ts +53 -9
- package/src/flow/nodes/shared.ts +14 -0
- package/src/flow/nodes/split_by_llm_categorize.ts +33 -8
- package/src/flow/revision-summary.ts +25 -0
- package/src/flow/utils.ts +38 -40
- package/src/form/ArrayEditor.ts +9 -11
- package/src/form/Checkbox.ts +2 -2
- package/src/form/Compose.ts +1 -1
- package/src/form/FieldElement.ts +8 -8
- package/src/form/KeyValueEditor.ts +4 -4
- package/src/form/MessageEditor.ts +2 -3
- package/src/form/RangePicker.ts +17 -17
- package/src/form/TembaSlider.ts +10 -10
- package/src/form/TemplateEditor.ts +4 -4
- package/src/form/TextInput.ts +19 -1
- package/src/form/select/Omnibox.ts +22 -19
- package/src/form/select/Select.ts +379 -171
- package/src/form/select/WorkspaceSelect.ts +7 -1
- package/src/layout/Accordion.ts +2 -2
- package/src/layout/Modax.ts +1 -1
- package/src/list/SortableList.ts +159 -0
- package/src/live/ContactChat.ts +46 -44
- package/src/live/ContactDetails.ts +1 -0
- package/src/live/ContactFieldEditor.ts +38 -31
- package/src/live/FieldManager.ts +4 -4
- package/src/styles/designTokens.ts +145 -0
- package/src/styles/pillVariants.ts +136 -0
- package/static/css/temba-components.css +106 -28
- 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
|
-
· ${
|
|
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 = [...
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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();
|
package/src/flow/nodes/shared.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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`<
|
|
323
|
-
|
|
324
|
-
|
|
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`<
|
|
372
|
-
|
|
373
|
-
|
|
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
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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`<
|
|
440
|
-
|
|
441
|
-
|
|
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;
|
package/src/form/ArrayEditor.ts
CHANGED
|
@@ -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
|
|
608
|
-
border-radius:
|
|
609
|
-
background:
|
|
607
|
+
border: 1px solid var(--color-widget-border);
|
|
608
|
+
border-radius: var(--curvature-widget);
|
|
609
|
+
background: var(--surface);
|
|
610
610
|
cursor: pointer;
|
|
611
|
-
|
|
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
|
|
655
|
-
border-radius:
|
|
656
|
-
background:
|
|
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:
|
|
662
|
+
background: var(--sunken);
|
|
664
663
|
}
|
|
665
664
|
|
|
666
665
|
.remove-btn {
|
|
667
|
-
|
|
668
|
-
color: #999;
|
|
666
|
+
color: var(--text-3);
|
|
669
667
|
}
|
|
670
668
|
|
|
671
669
|
.removable .remove-btn {
|
package/src/form/Checkbox.ts
CHANGED
|
@@ -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:
|
|
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,
|
|
37
|
+
background: var(--checkbox-hover-bg, var(--sunken));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
.checkbox-container {
|
package/src/form/Compose.ts
CHANGED
|
@@ -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:
|
|
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;
|
package/src/form/FieldElement.ts
CHANGED
|
@@ -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:
|
|
71
|
-
margin-left: 4px;
|
|
73
|
+
margin-bottom: 6px;
|
|
72
74
|
display: block;
|
|
73
|
-
font-weight:
|
|
74
|
-
font-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
|
|
78
|
+
color: var(--color-label);
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
.help-text {
|
|
81
|
-
font-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
|
|
286
|
-
border-radius:
|
|
287
|
-
background:
|
|
288
|
-
color:
|
|
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:
|
|
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:
|
|
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
|
}
|
package/src/form/RangePicker.ts
CHANGED
|
@@ -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:
|
|
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
|
|
26
|
-
background: var(--
|
|
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:
|
|
33
|
-
border: 1px solid
|
|
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:
|
|
44
|
-
border: 1px solid
|
|
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:
|
|
63
|
-
border-color:
|
|
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:
|
|
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:
|
|
82
|
-
border: 1px solid
|
|
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:
|
|
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
|
|
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:
|
|
104
|
-
border-color:
|
|
103
|
+
background: var(--accent-50);
|
|
104
|
+
border-color: var(--accent);
|
|
105
105
|
}
|
|
106
106
|
`;
|
|
107
107
|
|