@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
|
@@ -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:
|
|
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
|
}
|
package/src/layout/Accordion.ts
CHANGED
|
@@ -7,8 +7,8 @@ export class Accordion extends LitElement {
|
|
|
7
7
|
return css`
|
|
8
8
|
:host {
|
|
9
9
|
display: block;
|
|
10
|
-
border: 1px solid
|
|
11
|
-
border-radius:
|
|
10
|
+
border: 1px solid var(--color-widget-border);
|
|
11
|
+
border-radius: var(--curvature-widget);
|
|
12
12
|
overflow: hidden;
|
|
13
13
|
}
|
|
14
14
|
`;
|
package/src/layout/Modax.ts
CHANGED
package/src/list/SortableList.ts
CHANGED
|
@@ -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
|
}
|
package/src/live/ContactChat.ts
CHANGED
|
@@ -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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
<
|
|
1424
|
-
<
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
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
|
`
|
|
@@ -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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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.
|
|
196
|
-
padding-right: 1em;
|
|
207
|
+
padding: 0 0.6em 0 0;
|
|
197
208
|
}
|
|
198
209
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
padding-left:
|
|
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
|
-
|
|
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 {
|
package/src/live/FieldManager.ts
CHANGED
|
@@ -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
|
-
<
|
|
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
|
-
|
|
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
|
+
`;
|