@visns-studio/visns-components 6.21.0 → 6.23.1

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.
@@ -0,0 +1,550 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Zoom Phone presence (components/phone/*)
3
+ // ---------------------------------------------------------------------------
4
+ // The header chip and its popover: every Zoom Phone extension, who is free, who
5
+ // is on a call, and which client that call is with.
6
+ //
7
+ // Every brand value is a CSS custom property with a neutral fallback, exactly
8
+ // as in Sms.module.scss and Vault.module.scss, so the library carries no
9
+ // palette of its own.
10
+ //
11
+ // IMPORTANT — why every control below restates its colour, background, border,
12
+ // font-size AND box size:
13
+ //
14
+ // The popover mounts INSIDE `.hactions > ul`, whose chip rules reach every
15
+ // descendant `a` and `button` and hand it white text, a fixed 36px box and a
16
+ // pill radius. On a white panel that renders as blank boxes.
17
+ // Navigation.module.scss excludes `[data-nav-overlay] *` from those rules,
18
+ // which is the real fix — but a component that survives only because its host
19
+ // is well behaved is a component that breaks the next time it is mounted
20
+ // somewhere new. So nothing here inherits anything it actually needs, and no
21
+ // `!important` was required to do it.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ @mixin zp-tokens {
25
+ --zp-primary: var(--primary-color, #00386c);
26
+ --zp-paragraph: var(--paragraph-color, #2b2b2b);
27
+ --zp-background: var(--background-color, #fff);
28
+
29
+ --zp-line: color-mix(in srgb, var(--zp-paragraph) 18%, transparent);
30
+ // 76% of the body colour over white lands around #5b5b5b — roughly 6:1,
31
+ // comfortably past 4.5:1 on the small type this file is full of.
32
+ --zp-muted: color-mix(in srgb, var(--zp-paragraph) 76%, transparent);
33
+ --zp-surface: color-mix(
34
+ in srgb,
35
+ var(--zp-paragraph) 4%,
36
+ var(--zp-background)
37
+ );
38
+ --zp-hover: color-mix(in srgb, var(--zp-primary) 9%, var(--zp-background));
39
+
40
+ // The three states, and only three: free, ringing, talking. Chosen to be
41
+ // distinguishable without relying on hue alone — every dot is paired with
42
+ // its word, because a roster read by somebody with deuteranopia has to
43
+ // work too.
44
+ --zp-free: rgb(31, 122, 71);
45
+ --zp-ringing: rgb(163, 106, 20);
46
+ --zp-busy: rgb(190, 62, 62);
47
+ --zp-bad: rgb(190, 62, 62);
48
+ --zp-warn: rgb(163, 106, 20);
49
+ }
50
+
51
+ @mixin zp-reset {
52
+ box-sizing: border-box;
53
+ width: auto;
54
+ height: auto;
55
+ min-width: 0;
56
+ min-height: 0;
57
+ max-width: none;
58
+ margin: 0;
59
+ font-family: inherit;
60
+ font-weight: 400;
61
+ line-height: 1.3;
62
+ letter-spacing: normal;
63
+ text-transform: none;
64
+ text-decoration: none;
65
+ text-align: left;
66
+ appearance: none;
67
+ outline: none;
68
+ transform: none;
69
+ box-shadow: none;
70
+ // The host's chip rules transition `all`, which makes a control visibly
71
+ // resize on hover once its box is no longer fixed.
72
+ transition:
73
+ background-color 0.15s ease,
74
+ color 0.15s ease,
75
+ border-color 0.15s ease;
76
+ }
77
+
78
+ @mixin zp-focus {
79
+ &:focus-visible {
80
+ outline: 2px solid var(--zp-primary);
81
+ outline-offset: 2px;
82
+ }
83
+ }
84
+
85
+ @keyframes zpSpin {
86
+ to {
87
+ transform: rotate(360deg);
88
+ }
89
+ }
90
+
91
+ @keyframes zpPopIn {
92
+ from {
93
+ opacity: 0;
94
+ transform: translateY(-4px);
95
+ }
96
+ to {
97
+ opacity: 1;
98
+ transform: translateY(0);
99
+ }
100
+ }
101
+
102
+ .spin {
103
+ animation: zpSpin 0.9s linear infinite;
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // The header chip
108
+ // ---------------------------------------------------------------------------
109
+
110
+ .zoomPhone {
111
+ @include zp-tokens;
112
+
113
+ position: relative;
114
+ align-items: center;
115
+ }
116
+
117
+ // Deliberately the same shape as Notification's, the vault's and the SMS
118
+ // badge's triggers: these sit side by side in the action pill and must read as
119
+ // one row of chips, not four designs. This one DOES inherit the header's
120
+ // colour, which is correct — it is a chip.
121
+ .trigger {
122
+ @include zp-focus;
123
+
124
+ position: relative;
125
+ display: flex;
126
+ align-items: center;
127
+ justify-content: center;
128
+ width: 40px;
129
+ height: 40px;
130
+ padding: 8px;
131
+ border: none;
132
+ border-radius: 50%;
133
+ background: transparent;
134
+ color: var(--paragraph-color, #333);
135
+ cursor: pointer;
136
+ transition: all 0.2s ease;
137
+
138
+ &:hover {
139
+ background-color: rgba(var(--primary-color-rgb, 0, 86, 179), 0.1);
140
+ color: var(--primary-color, #0056b3);
141
+ transform: scale(1.05);
142
+ }
143
+
144
+ &:active {
145
+ transform: scale(0.95);
146
+ }
147
+ }
148
+
149
+ // Matches Notification's and the SMS badge's, down to the offsets: three counts
150
+ // side by side in the header have to sit on the same line as each other.
151
+ .badge {
152
+ position: absolute;
153
+ top: -2px;
154
+ right: -2px;
155
+ display: flex;
156
+ align-items: center;
157
+ justify-content: center;
158
+ box-sizing: border-box;
159
+ min-width: 16px;
160
+ height: 16px;
161
+ padding: 0 4px;
162
+ border-radius: 8px;
163
+ background-color: var(--secondary-color, #c1272d);
164
+ color: #fff;
165
+ font-size: 10px;
166
+ font-weight: 600;
167
+ font-variant-numeric: tabular-nums;
168
+ line-height: 1;
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // The popover
173
+ // ---------------------------------------------------------------------------
174
+
175
+ .popoverWrapper {
176
+ position: absolute;
177
+ top: 100%;
178
+ right: 0;
179
+ z-index: 1100;
180
+ margin-top: 10px;
181
+ // The host's chip `li` is a centring flex container. The popover is
182
+ // absolutely positioned so it escapes the flexbox, but `text-align` is
183
+ // inherited and would centre every line of prose inside it.
184
+ text-align: left;
185
+ }
186
+
187
+ .popover {
188
+ @include zp-tokens;
189
+
190
+ display: flex;
191
+ flex-direction: column;
192
+ box-sizing: border-box;
193
+ width: 380px;
194
+ max-width: calc(100vw - 2rem);
195
+ border: 1px solid var(--zp-line);
196
+ border-radius: 10px;
197
+ // Opaque, and stated: the header behind this is a dark brand bar, and a
198
+ // panel that inherits any of it is unreadable.
199
+ background: var(--zp-background);
200
+ color: var(--zp-paragraph);
201
+ font-size: 0.85rem;
202
+ text-align: left;
203
+ box-shadow:
204
+ 0 12px 32px rgba(0, 0, 0, 0.22),
205
+ 0 2px 6px rgba(0, 0, 0, 0.12);
206
+ overflow: hidden;
207
+ animation: zpPopIn 0.18s ease-out;
208
+ }
209
+
210
+ .popoverHead {
211
+ display: flex;
212
+ gap: 0.75rem;
213
+ align-items: center;
214
+ justify-content: space-between;
215
+ padding: 0.7rem 0.85rem;
216
+ border-bottom: 1px solid var(--zp-line);
217
+ background: var(--zp-surface);
218
+ }
219
+
220
+ .popoverTitle {
221
+ display: inline-flex;
222
+ gap: 0.4rem;
223
+ align-items: center;
224
+ color: var(--zp-paragraph);
225
+ font-size: 0.95rem;
226
+ font-weight: 700;
227
+
228
+ svg {
229
+ color: var(--zp-primary);
230
+ }
231
+ }
232
+
233
+ .iconButton {
234
+ @include zp-reset;
235
+ @include zp-focus;
236
+
237
+ display: inline-flex;
238
+ align-items: center;
239
+ justify-content: center;
240
+ width: 28px;
241
+ height: 28px;
242
+ padding: 0;
243
+ border: 1px solid var(--zp-line);
244
+ border-radius: 6px;
245
+ background: var(--zp-background);
246
+ color: var(--zp-muted);
247
+ cursor: pointer;
248
+
249
+ &:hover {
250
+ background: var(--zp-hover);
251
+ color: var(--zp-primary);
252
+ }
253
+ }
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // The roster
257
+ // ---------------------------------------------------------------------------
258
+
259
+ .rows {
260
+ max-height: 380px;
261
+ margin: 0;
262
+ padding: 0;
263
+ list-style: none;
264
+ overflow-y: auto;
265
+
266
+ // Two hosts reach in here with bare element selectors: Navigation's
267
+ // `.hactions > ul li` and the CRM's own `ul { padding-left: … }` in
268
+ // main.scss. Restated so the list is a list wherever it is mounted.
269
+ > li {
270
+ min-width: 0;
271
+ min-height: 0;
272
+ margin: 0;
273
+ padding: 0;
274
+ position: static;
275
+ list-style: none;
276
+ }
277
+ }
278
+
279
+ // Specificity, deliberately: `.hactions > ul li` is 0,1,2 and no single class
280
+ // can outrank it. This restates the row's BOX at 0,2,0, where it wins. Paint
281
+ // stays in `.row` below; only geometry is repeated.
282
+ .rows > .row {
283
+ display: grid;
284
+ grid-template-columns: 30px minmax(0, 1fr) auto;
285
+ column-gap: 0.6rem;
286
+ align-items: center;
287
+ justify-content: normal;
288
+ box-sizing: border-box;
289
+ min-width: 0;
290
+ min-height: 0;
291
+ margin: 0;
292
+ padding: 0.55rem 0.85rem;
293
+ position: static;
294
+ }
295
+
296
+ .rows > .row > .rowMain {
297
+ display: block;
298
+ }
299
+
300
+ // One extension, one row: avatar, name + what they are doing, status.
301
+ //
302
+ // Three columns, and only the middle one gives way — it is the only part of the
303
+ // row that can be any length. Fixed tracks either side keep a list of mixed
304
+ // rows measured identically, so nothing zig-zags down the edge.
305
+ .row {
306
+ display: grid;
307
+ grid-template-columns: 30px minmax(0, 1fr) auto;
308
+ column-gap: 0.6rem;
309
+ align-items: center;
310
+ padding: 0.55rem 0.85rem;
311
+ border: none;
312
+ border-bottom: 1px solid color-mix(in srgb, var(--zp-line) 55%, transparent);
313
+ background: var(--zp-background);
314
+
315
+ &:last-child {
316
+ border-bottom: none;
317
+ }
318
+
319
+ &:hover {
320
+ background: var(--zp-hover);
321
+ }
322
+ }
323
+
324
+ .avatar {
325
+ display: flex;
326
+ align-items: center;
327
+ justify-content: center;
328
+ box-sizing: border-box;
329
+ width: 30px;
330
+ height: 30px;
331
+ border-radius: 50%;
332
+ background: color-mix(in srgb, var(--zp-primary) 14%, var(--zp-background));
333
+ color: var(--zp-primary);
334
+ font-size: 0.7rem;
335
+ font-weight: 700;
336
+ letter-spacing: 0.02em;
337
+ // Fixed, so a two-letter avatar and a one-letter one are the same size and
338
+ // the column of them does not ripple.
339
+ flex: 0 0 30px;
340
+ }
341
+
342
+ .rowMain {
343
+ // `min-width: 0` is the load-bearing declaration: without it a grid item
344
+ // refuses to shrink below its content and the ellipsis never engages.
345
+ min-width: 0;
346
+ justify-self: stretch;
347
+ overflow: hidden;
348
+ color: var(--zp-paragraph);
349
+ line-height: 1.35;
350
+ text-align: left;
351
+ }
352
+
353
+ .rowTop {
354
+ display: flex;
355
+ gap: 0.4rem;
356
+ align-items: baseline;
357
+ min-width: 0;
358
+ }
359
+
360
+ .rowName {
361
+ overflow: hidden;
362
+ font-size: 0.85rem;
363
+ font-weight: 600;
364
+ text-overflow: ellipsis;
365
+ white-space: nowrap;
366
+ }
367
+
368
+ .extension {
369
+ flex: 0 0 auto;
370
+ color: var(--zp-muted);
371
+ font-size: 0.72rem;
372
+ font-variant-numeric: tabular-nums;
373
+ }
374
+
375
+ .idleLine {
376
+ display: block;
377
+ color: var(--zp-muted);
378
+ font-size: 0.76rem;
379
+ }
380
+
381
+ // The busy line: what they are doing, the number, the queue, the timer. Wraps
382
+ // rather than ellipsing, because every part of it is load bearing and the row
383
+ // is allowed to be two lines tall.
384
+ .callLine {
385
+ display: flex;
386
+ flex-wrap: wrap;
387
+ gap: 0.35rem;
388
+ align-items: baseline;
389
+ min-width: 0;
390
+ font-size: 0.76rem;
391
+ }
392
+
393
+ .callSummary {
394
+ overflow: hidden;
395
+ color: var(--zp-paragraph);
396
+ text-overflow: ellipsis;
397
+ white-space: nowrap;
398
+ }
399
+
400
+ .callNumber {
401
+ color: var(--zp-muted);
402
+ font-variant-numeric: tabular-nums;
403
+ }
404
+
405
+ .queueTag {
406
+ padding: 0 0.3rem;
407
+ border-radius: 4px;
408
+ background: color-mix(in srgb, var(--zp-primary) 10%, var(--zp-background));
409
+ color: var(--zp-primary);
410
+ font-size: 0.68rem;
411
+ font-weight: 600;
412
+ }
413
+
414
+ // Tabular figures, so a timer counting up does not shuffle the row sideways
415
+ // every time a digit changes width.
416
+ .duration {
417
+ margin-left: auto;
418
+ color: var(--zp-muted);
419
+ font-variant-numeric: tabular-nums;
420
+ }
421
+
422
+ .clientLine {
423
+ display: flex;
424
+ gap: 0.4rem;
425
+ align-items: baseline;
426
+ min-width: 0;
427
+ font-size: 0.76rem;
428
+ }
429
+
430
+ .clientLink {
431
+ @include zp-reset;
432
+ @include zp-focus;
433
+
434
+ overflow: hidden;
435
+ padding: 0;
436
+ border: none;
437
+ background: none;
438
+ color: var(--zp-primary);
439
+ font-size: 0.76rem;
440
+ font-weight: 600;
441
+ text-overflow: ellipsis;
442
+ white-space: nowrap;
443
+ cursor: pointer;
444
+
445
+ &:hover {
446
+ text-decoration: underline;
447
+ }
448
+ }
449
+
450
+ .clientFlat {
451
+ overflow: hidden;
452
+ color: var(--zp-primary);
453
+ font-weight: 600;
454
+ text-overflow: ellipsis;
455
+ white-space: nowrap;
456
+ }
457
+
458
+ .clientMeta {
459
+ flex: 0 0 auto;
460
+ color: var(--zp-muted);
461
+ }
462
+
463
+ // The status cell. The dot is never alone — the word beside it is what makes
464
+ // the roster readable without relying on hue.
465
+ .status {
466
+ display: inline-flex;
467
+ gap: 0.35rem;
468
+ align-items: center;
469
+ flex: 0 0 auto;
470
+ color: var(--zp-muted);
471
+ font-size: 0.72rem;
472
+ font-weight: 600;
473
+ white-space: nowrap;
474
+ }
475
+
476
+ .dot {
477
+ display: inline-block;
478
+ width: 8px;
479
+ height: 8px;
480
+ border-radius: 50%;
481
+ background: currentColor;
482
+ }
483
+
484
+ .status_free {
485
+ color: var(--zp-free);
486
+ }
487
+
488
+ .status_ringing {
489
+ color: var(--zp-ringing);
490
+ }
491
+
492
+ .status_busy {
493
+ color: var(--zp-busy);
494
+ }
495
+
496
+ .status_muted {
497
+ color: var(--zp-muted);
498
+ }
499
+
500
+ // ---------------------------------------------------------------------------
501
+ // Empty, notes, footer
502
+ // ---------------------------------------------------------------------------
503
+
504
+ .empty {
505
+ padding: 1.4rem 0.85rem;
506
+ color: var(--zp-muted);
507
+ text-align: center;
508
+
509
+ p {
510
+ margin: 0;
511
+ }
512
+ }
513
+
514
+ .statusNote {
515
+ display: flex;
516
+ gap: 0.4rem;
517
+ align-items: center;
518
+ margin: 0;
519
+ padding: 0.5rem 0.85rem;
520
+ border-top: 1px solid var(--zp-line);
521
+ background: color-mix(in srgb, var(--zp-warn) 8%, var(--zp-background));
522
+ color: var(--zp-warn);
523
+ font-size: 0.75rem;
524
+
525
+ svg {
526
+ flex: 0 0 auto;
527
+ }
528
+ }
529
+
530
+ .statusNoteBad {
531
+ background: color-mix(in srgb, var(--zp-bad) 8%, var(--zp-background));
532
+ color: var(--zp-bad);
533
+ }
534
+
535
+ .footer {
536
+ display: flex;
537
+ gap: 0.75rem;
538
+ align-items: center;
539
+ justify-content: space-between;
540
+ padding: 0.45rem 0.85rem;
541
+ border-top: 1px solid var(--zp-line);
542
+ background: var(--zp-surface);
543
+ color: var(--zp-muted);
544
+ font-size: 0.72rem;
545
+ }
546
+
547
+ .footerHint,
548
+ .footerStamp {
549
+ white-space: nowrap;
550
+ }
@@ -761,6 +761,29 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
761
761
  background: none !important;
762
762
  }
763
763
 
764
+ /* The keyboard highlight in a dropdown's option list — declared by the
765
+ vendored theme, then painted with nothing.
766
+
767
+ Arrow keys have always worked in these filters: the combo tracks an active
768
+ item, scrolls it into view, Enter selects it and Escape closes. But the
769
+ theme's ONLY rule for that active item is `border: 1px solid transparent`,
770
+ while hover and selected both get a background — so navigating with the
771
+ arrows changed nothing on screen and the keys read as dead.
772
+
773
+ Colouring the border the theme already reserves is why this cannot shift the
774
+ list: every item carries that 1px border, active or not. Scoped to the same
775
+ theme selector the invisible rule uses, so it lands exactly where that rule
776
+ lands and nowhere else.
777
+
778
+ `--primary-color` rather than one of the `--dgx-*` tokens above: those are
779
+ defined on `.InovuaReactDataGrid`, and the option list is rendered outside
780
+ the grid element, where they would not resolve. */
781
+ .inovua-react-toolkit-combo-box__list--theme-default-light
782
+ .inovua-react-toolkit-combo-box__list__item--active {
783
+ border-color: color-mix(in srgb, var(--primary-color, #1b3933) 55%, transparent) !important;
784
+ background: color-mix(in srgb, var(--primary-color, #1b3933) 12%, transparent) !important;
785
+ }
786
+
764
787
  /* The native date filter is the odd one out in the row.
765
788
 
766
789
  Measured on /tickets: every other filter editor is 28px tall with a 6px
@@ -24,11 +24,34 @@ export const VAULT_ACTIONS = {
24
24
  update: 'edited the entry',
25
25
  delete: 'deleted the entry',
26
26
  restore: 'restored the entry',
27
+ share_create: 'created a share link',
28
+ // The user on a `share_view` row is the person who CREATED the link, not
29
+ // the reader — the reader has no account — so the line is phrased from
30
+ // their side. The IP beside it is the reader's.
31
+ share_view: 'had a share link opened',
32
+ share_revoke: 'revoked a share link',
27
33
  };
28
34
 
29
35
  const describeAction = (action) =>
30
36
  VAULT_ACTIONS[action] || String(action || '').replace(/_/g, ' ');
31
37
 
38
+ /**
39
+ * The extra clause a row's `meta` earns, or null.
40
+ *
41
+ * Today the only one: where a share link was emailed. Written as "sent to" or
42
+ * "could not be emailed to" because a link that failed to send still went
43
+ * somewhere in intent, and an incident asks about the address either way.
44
+ */
45
+ const describeMeta = (row) => {
46
+ const address = row?.meta?.recipient_email;
47
+
48
+ if (!address) return null;
49
+
50
+ return row.meta.emailed === false
51
+ ? `could not be emailed to ${address}`
52
+ : `sent to ${address}`;
53
+ };
54
+
32
55
  /**
33
56
  * Who took what out of the vault, and when.
34
57
  *
@@ -180,6 +203,7 @@ const VaultAccessLog = ({ entry = null, endpoints, isOpen = true, onClose }) =>
180
203
  )}
181
204
  </div>
182
205
  <div className={styles.logMeta}>
206
+ {describeMeta(row) && <span>{describeMeta(row)}</span>}
183
207
  {row.ip && <span>{row.ip}</span>}
184
208
  <span
185
209
  data-tooltip-id="system-tooltip"