@visns-studio/visns-components 6.6.4 → 6.15.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 (97) hide show
  1. package/README.md +1623 -18
  2. package/package.json +10 -4
  3. package/src/components/DataGrid.jsx +171 -37
  4. package/src/components/Fetch.jsx +80 -3
  5. package/src/components/Form.jsx +9 -0
  6. package/src/components/Navigation.jsx +109 -50
  7. package/src/components/Notification.jsx +279 -9
  8. package/src/components/TableFilter.jsx +14 -1
  9. package/src/components/auth/AuthBrandPanel.jsx +43 -0
  10. package/src/components/auth/AuthLoading.jsx +78 -0
  11. package/src/components/auth/AuthShell.jsx +59 -0
  12. package/src/components/auth/ClientAuth.jsx +161 -0
  13. package/src/components/auth/ClientAuthFrame.jsx +59 -0
  14. package/src/components/auth/ClientLogin.jsx +266 -55
  15. package/src/components/auth/ClientOTPVerify.jsx +587 -115
  16. package/src/components/auth/ImpersonateGate.jsx +254 -0
  17. package/src/components/auth/Login.jsx +134 -41
  18. package/src/components/auth/LogoutScreen.jsx +112 -0
  19. package/src/components/auth/Reset.jsx +134 -77
  20. package/src/components/auth/TwoFactorAuth.jsx +475 -297
  21. package/src/components/auth/Verify.jsx +237 -126
  22. package/src/components/auth/authEndpoints.js +105 -0
  23. package/src/components/auth/authFont.js +23 -0
  24. package/src/components/auth/authHelpers.js +465 -0
  25. package/src/components/auth/clientAuthProtocols.js +240 -0
  26. package/src/components/auth/useOptionalRouter.js +49 -0
  27. package/src/components/callQueue/CallQueuePop.jsx +1502 -0
  28. package/src/components/callQueue/CallQueueSettings.jsx +508 -0
  29. package/src/components/callQueue/callQueueHelpers.js +280 -0
  30. package/src/components/callQueue/callQueueSettingsHelpers.js +75 -0
  31. package/src/components/columns/ColumnRenderers.jsx +53 -7
  32. package/src/components/generic/GenericAuth.jsx +163 -96
  33. package/src/components/generic/GenericDetail.jsx +215 -90
  34. package/src/components/generic/GenericIndex.jsx +20 -47
  35. package/src/components/generic/GenericMain.jsx +5 -0
  36. package/src/components/generic/GroupedReportRenderer.jsx +1 -5
  37. package/src/components/generic/StandardModal.jsx +17 -5
  38. package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +1 -6
  39. package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +4 -11
  40. package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +9 -27
  41. package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +5 -13
  42. package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +5 -13
  43. package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +14 -31
  44. package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +3 -9
  45. package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +2 -15
  46. package/src/components/navActive.js +60 -0
  47. package/src/components/notify/desktopNotifications.js +256 -0
  48. package/src/components/sketch/SketchField.jsx +12 -2
  49. package/src/components/sms/SmsComposeModal.jsx +495 -0
  50. package/src/components/sms/SmsInbox.jsx +596 -0
  51. package/src/components/sms/SmsInboxBadge.jsx +468 -0
  52. package/src/components/sms/SmsLineSettings.jsx +854 -0
  53. package/src/components/sms/SmsThreadPanel.jsx +1184 -0
  54. package/src/components/sms/smsEndpoints.js +56 -0
  55. package/src/components/sms/smsHelpers.js +1075 -0
  56. package/src/components/sms/smsLiveState.js +132 -0
  57. package/src/components/sms/useSmsLive.js +307 -0
  58. package/src/components/styles/CallQueuePop.module.scss +646 -0
  59. package/src/components/styles/CallQueueSettings.module.scss +460 -0
  60. package/src/components/styles/ClientAuth.module.scss +229 -0
  61. package/src/components/styles/GenericDetail.module.scss +153 -26
  62. package/src/components/styles/GenericIndex.module.scss +13 -0
  63. package/src/components/styles/GenericMain.module.scss +19 -2
  64. package/src/components/styles/ImpersonateGate.module.scss +85 -0
  65. package/src/components/styles/Login.module.scss +13 -122
  66. package/src/components/styles/LogoutScreen.module.scss +89 -0
  67. package/src/components/styles/Navigation.module.scss +509 -223
  68. package/src/components/styles/Notification.module.scss +103 -0
  69. package/src/components/styles/Reset.module.scss +52 -186
  70. package/src/components/styles/Sms.module.scss +2382 -0
  71. package/src/components/styles/TableFilter.module.scss +118 -98
  72. package/src/components/styles/TwoFactorAuth.module.scss +115 -176
  73. package/src/components/styles/Vault.module.scss +1983 -0
  74. package/src/components/styles/Verify.module.scss +57 -180
  75. package/src/components/styles/_authBrandPanel.scss +163 -0
  76. package/src/components/styles/_authShell.scss +369 -0
  77. package/src/components/utils/buildEnv.js +67 -0
  78. package/src/components/utils/displayValue.js +94 -0
  79. package/src/components/utils/useDensity.js +345 -9
  80. package/src/components/vault/OtpChip.jsx +208 -0
  81. package/src/components/vault/PasswordGenerator.jsx +145 -0
  82. package/src/components/vault/QrScanner.jsx +326 -0
  83. package/src/components/vault/VaultAccessLog.jsx +225 -0
  84. package/src/components/vault/VaultConfirmPanel.jsx +113 -0
  85. package/src/components/vault/VaultEntryForm.jsx +740 -0
  86. package/src/components/vault/VaultManager.jsx +1000 -0
  87. package/src/components/vault/VaultQuickSearch.jsx +681 -0
  88. package/src/components/vault/qrDecode.js +257 -0
  89. package/src/components/vault/useDebouncedValue.js +22 -0
  90. package/src/components/vault/useVaultReveal.js +160 -0
  91. package/src/components/vault/vaultClipboard.js +33 -0
  92. package/src/components/vault/vaultEndpoints.js +40 -0
  93. package/src/components/vault/vaultFit.js +116 -0
  94. package/src/components/vault/vaultHelpers.js +547 -0
  95. package/src/components/vault/vaultNavigation.jsx +58 -0
  96. package/src/components/vault/vaultOtp.js +105 -0
  97. package/src/index.js +233 -1
@@ -0,0 +1,1983 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Vault (components/vault/*)
3
+ // ---------------------------------------------------------------------------
4
+ // The staff password manager: a popover that lives in the navigation's action
5
+ // pill (VaultQuickSearch), the page behind it (VaultManager), and the small
6
+ // pieces both of them mount — the OTP chip, the confirm panel, the entry form.
7
+ //
8
+ // Every brand value is a CSS custom property with a neutral fallback, as in
9
+ // CallQueueSettings.module.scss, so the library carries no palette of its own.
10
+ //
11
+ // IMPORTANT — why every control below restates its own colour, background,
12
+ // border, font-size AND box size:
13
+ //
14
+ // The popover mounts INSIDE `.hactions > ul`, whose chip rules used to reach
15
+ // every descendant `a` and `button` and hand it white text, a fixed 36px box
16
+ // and a pill radius. On a white panel that renders as blank boxes; it is the
17
+ // bug this file's second pass fixed. Navigation.module.scss now excludes
18
+ // `[data-nav-overlay] *` from those rules, which is the real fix, but a
19
+ // component that survives only because its host is well behaved is a
20
+ // component that breaks the next time it is mounted somewhere new. So nothing
21
+ // here inherits anything it actually needs. No `!important` was required.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ $vault-mono: 'SFMono-Regular', 'Menlo', 'Consolas', monospace;
25
+
26
+ @mixin vault-tokens {
27
+ --v-primary: var(--primary-color, #00386c);
28
+ --v-secondary: var(--secondary-color, #404040);
29
+ --v-paragraph: var(--paragraph-color, #2b2b2b);
30
+ --v-background: var(--background-color, #fff);
31
+ --v-br: var(--br, 6px);
32
+ --v-btn-br: var(--btn-br, 6px);
33
+
34
+ --v-line: color-mix(in srgb, var(--v-paragraph) 18%, transparent);
35
+ // 76% of the body colour over white lands around #5b5b5b — roughly 6:1,
36
+ // comfortably past 4.5:1. The 62% this started at was 4.0:1 and genuinely
37
+ // hard to read on a tablet.
38
+ --v-muted: color-mix(in srgb, var(--v-paragraph) 76%, transparent);
39
+ --v-surface: color-mix(in srgb, var(--v-paragraph) 4%, var(--v-background));
40
+ --v-hover: color-mix(in srgb, var(--v-primary) 9%, var(--v-background));
41
+ --v-tint: color-mix(in srgb, var(--v-primary) 12%, var(--v-background));
42
+ --v-ok: rgb(31, 122, 71);
43
+ --v-bad: rgb(190, 62, 62);
44
+ }
45
+
46
+ // Undo anything a host may have imposed on a generic element. Applied to every
47
+ // interactive element in the module — see the note at the top of the file.
48
+ @mixin vault-reset {
49
+ box-sizing: border-box;
50
+ width: auto;
51
+ height: auto;
52
+ min-width: 0;
53
+ min-height: 0;
54
+ max-width: none;
55
+ margin: 0;
56
+ font-family: inherit;
57
+ font-weight: 400;
58
+ line-height: 1.3;
59
+ letter-spacing: normal;
60
+ text-transform: none;
61
+ text-decoration: none;
62
+ text-align: left;
63
+ appearance: none;
64
+ outline: none;
65
+ transform: none;
66
+ box-shadow: none;
67
+ // The host's chip rules transition `all`, which makes a control visibly
68
+ // resize on hover once its box is no longer fixed.
69
+ transition: background-color 0.15s ease, color 0.15s ease,
70
+ border-color 0.15s ease;
71
+ }
72
+
73
+ @mixin vault-focus {
74
+ &:focus-visible {
75
+ outline: 2px solid var(--v-primary);
76
+ outline-offset: 2px;
77
+ }
78
+ }
79
+
80
+ // A checkbox or a radio, and nothing else.
81
+ //
82
+ // The host CRM styles bare `input` — padding, a min-width, a text-field
83
+ // height, a pill radius — and none of that is typed, so it lands on
84
+ // `input[type=radio]` too and renders the visibility choice as a pair of wide
85
+ // ovals with a dot floating in one of them. Restating the box is not enough on
86
+ // its own: the host's width wins over `width: 16px` often enough (a later
87
+ // rule, a more specific selector) that the only reliable fix is to stop
88
+ // relying on the native painting altogether.
89
+ //
90
+ // So the control is drawn here. `appearance: none` throws away the platform
91
+ // widget, every axis of the box is pinned — width, height, min, max and the
92
+ // flex basis, because a flex parent can stretch a box that has all four — and
93
+ // the mark is painted: an inset ring for the radio's dot, a background tick
94
+ // for the checkbox. Nothing below inherits from the host.
95
+ $vault-tick: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.4l3.1 3.1 5.9-6'/%3E%3C/svg%3E";
96
+
97
+ @mixin vault-check {
98
+ @include vault-focus;
99
+
100
+ flex: 0 0 16px;
101
+ box-sizing: border-box;
102
+ width: 16px;
103
+ height: 16px;
104
+ min-width: 16px;
105
+ min-height: 16px;
106
+ max-width: 16px;
107
+ max-height: 16px;
108
+ padding: 0;
109
+ margin: 0;
110
+ border: 1.5px solid var(--v-line);
111
+ border-radius: 3px;
112
+ background: var(--v-background);
113
+ background-image: none;
114
+ box-shadow: none;
115
+ appearance: none;
116
+ -webkit-appearance: none;
117
+ cursor: pointer;
118
+ vertical-align: middle;
119
+ transition: background-color 0.12s ease, border-color 0.12s ease,
120
+ box-shadow 0.12s ease;
121
+
122
+ &:hover:not(:disabled) {
123
+ border-color: var(--v-primary);
124
+ }
125
+
126
+ &:checked {
127
+ border-color: var(--v-primary);
128
+ background-color: var(--v-primary);
129
+ }
130
+
131
+ &:disabled {
132
+ opacity: 0.55;
133
+ cursor: default;
134
+ }
135
+
136
+ &[type='radio'] {
137
+ border-radius: 50%;
138
+
139
+ // Fill, then an inset ring in the surface colour: what is left in the
140
+ // middle is the dot. One property, no pseudo-element — and pseudo
141
+ // elements on an input are exactly the sort of thing a host reset is
142
+ // liable to have opinions about.
143
+ &:checked {
144
+ box-shadow: inset 0 0 0 3px var(--v-background);
145
+ }
146
+ }
147
+
148
+ &[type='checkbox'] {
149
+ &:checked {
150
+ background-image: url($vault-tick);
151
+ background-repeat: no-repeat;
152
+ background-position: center;
153
+ background-size: 12px 12px;
154
+ }
155
+ }
156
+ }
157
+
158
+ @keyframes vaultSpin {
159
+ to {
160
+ transform: rotate(360deg);
161
+ }
162
+ }
163
+
164
+ @keyframes vaultPopIn {
165
+ from {
166
+ opacity: 0;
167
+ transform: translateY(-4px);
168
+ }
169
+ to {
170
+ opacity: 1;
171
+ transform: translateY(0);
172
+ }
173
+ }
174
+
175
+ .spin {
176
+ animation: vaultSpin 1s linear infinite;
177
+ flex-shrink: 0;
178
+ }
179
+
180
+ .muted {
181
+ color: var(--v-muted);
182
+ }
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Shared controls
186
+ // ---------------------------------------------------------------------------
187
+
188
+ .primaryButton {
189
+ @include vault-reset;
190
+ @include vault-focus;
191
+
192
+ display: inline-flex;
193
+ flex-shrink: 0;
194
+ gap: 0.4rem;
195
+ align-items: center;
196
+ justify-content: center;
197
+ padding: 0.5rem 0.85rem;
198
+ border: 1px solid var(--v-primary);
199
+ border-radius: var(--v-btn-br);
200
+ background: var(--v-primary);
201
+ color: #fff;
202
+ font-size: 0.82rem;
203
+ font-weight: 600;
204
+ white-space: nowrap;
205
+ cursor: pointer;
206
+
207
+ svg {
208
+ color: currentColor;
209
+ flex-shrink: 0;
210
+ }
211
+
212
+ &:hover:not(:disabled) {
213
+ background: color-mix(in srgb, var(--v-primary) 85%, #000);
214
+ border-color: color-mix(in srgb, var(--v-primary) 85%, #000);
215
+ color: #fff;
216
+ }
217
+
218
+ &:disabled {
219
+ opacity: 0.55;
220
+ cursor: default;
221
+ }
222
+ }
223
+
224
+ .ghostButton {
225
+ @include vault-reset;
226
+ @include vault-focus;
227
+
228
+ display: inline-flex;
229
+ flex-shrink: 0;
230
+ gap: 0.4rem;
231
+ align-items: center;
232
+ justify-content: center;
233
+ padding: 0.45rem 0.75rem;
234
+ border: 1px solid var(--v-line);
235
+ border-radius: var(--v-btn-br);
236
+ background: var(--v-background);
237
+ color: var(--v-paragraph);
238
+ font-size: 0.8rem;
239
+ font-weight: 500;
240
+ white-space: nowrap;
241
+ cursor: pointer;
242
+
243
+ svg {
244
+ color: currentColor;
245
+ flex-shrink: 0;
246
+ }
247
+
248
+ &:hover:not(:disabled) {
249
+ background: var(--v-hover);
250
+ border-color: var(--v-primary);
251
+ color: var(--v-primary);
252
+ }
253
+
254
+ &:disabled {
255
+ opacity: 0.5;
256
+ cursor: default;
257
+ }
258
+ }
259
+
260
+ // The row-level actions. Icon plus word: four unexplained glyphs in a row is a
261
+ // puzzle, and both surfaces are used in a hurry.
262
+ .actionPill {
263
+ @include vault-reset;
264
+ @include vault-focus;
265
+
266
+ display: inline-flex;
267
+ flex-shrink: 0;
268
+ gap: 0.3rem;
269
+ align-items: center;
270
+ justify-content: center;
271
+ padding: 0.28rem 0.55rem;
272
+ border: 1px solid var(--v-line);
273
+ border-radius: 999px;
274
+ background: var(--v-background);
275
+ color: var(--v-paragraph);
276
+ font-size: 0.74rem;
277
+ font-weight: 600;
278
+ white-space: nowrap;
279
+ cursor: pointer;
280
+
281
+ svg {
282
+ color: currentColor;
283
+ flex-shrink: 0;
284
+ }
285
+
286
+ &:hover:not(:disabled) {
287
+ background: var(--v-tint);
288
+ border-color: var(--v-primary);
289
+ color: var(--v-primary);
290
+ }
291
+
292
+ &:disabled {
293
+ opacity: 0.5;
294
+ cursor: default;
295
+ }
296
+ }
297
+
298
+ // The password copy is the thing people came for, so it leads.
299
+ .actionPillPrimary {
300
+ border-color: color-mix(in srgb, var(--v-primary) 45%, transparent);
301
+ background: var(--v-tint);
302
+ color: var(--v-primary);
303
+ }
304
+
305
+ .actionPillDanger {
306
+ &:hover:not(:disabled) {
307
+ border-color: var(--v-bad);
308
+ background: color-mix(in srgb, var(--v-bad) 12%, var(--v-background));
309
+ color: var(--v-bad);
310
+ }
311
+ }
312
+
313
+ // The row's actions, in one cluster: three glyphs that copy something, a
314
+ // hairline, then the pills that change the entry.
315
+ //
316
+ // The copies are icon-only and the pills are not, which is deliberate. A copy
317
+ // is repeatable and harmless, always in the same order, and the row has only
318
+ // so much width; edit and delete are neither repeatable nor harmless, and a
319
+ // bare glyph there asks the user to guess.
320
+ .actionCluster {
321
+ display: inline-flex;
322
+ flex-wrap: nowrap;
323
+ gap: 0.25rem;
324
+ align-items: center;
325
+ justify-content: flex-end;
326
+ }
327
+
328
+ // The tooltip hangs on this rather than on the button inside it: a disabled
329
+ // button fires no pointer events, and "no 2FA on this entry" is worth reading
330
+ // precisely when the button is disabled.
331
+ .actionIconWrap {
332
+ display: inline-flex;
333
+ }
334
+
335
+ .actionIcon {
336
+ @include vault-reset;
337
+ @include vault-focus;
338
+
339
+ display: inline-flex;
340
+ flex-shrink: 0;
341
+ box-sizing: border-box;
342
+ width: 28px;
343
+ height: 28px;
344
+ align-items: center;
345
+ justify-content: center;
346
+ padding: 0;
347
+ border: 1px solid var(--v-line);
348
+ border-radius: var(--v-btn-br);
349
+ background: var(--v-background);
350
+ color: var(--v-muted);
351
+ cursor: pointer;
352
+
353
+ svg {
354
+ color: currentColor;
355
+ flex-shrink: 0;
356
+ }
357
+
358
+ &:hover:not(:disabled) {
359
+ border-color: var(--v-primary);
360
+ background: var(--v-tint);
361
+ color: var(--v-primary);
362
+ }
363
+
364
+ &:disabled {
365
+ opacity: 0.45;
366
+ cursor: default;
367
+ }
368
+ }
369
+
370
+ // The password is what most people came for, so it reads as the default.
371
+ .actionIconDanger {
372
+ color: var(--v-bad, #b42318);
373
+
374
+ &:hover:not(:disabled) {
375
+ border-color: color-mix(in srgb, var(--v-bad, #b42318) 45%, transparent);
376
+ background: color-mix(in srgb, var(--v-bad, #b42318) 10%, transparent);
377
+ color: var(--v-bad, #b42318);
378
+ }
379
+ }
380
+
381
+ .actionIconPrimary {
382
+ border-color: color-mix(in srgb, var(--v-primary) 45%, transparent);
383
+ background: var(--v-tint);
384
+ color: var(--v-primary);
385
+ }
386
+
387
+ // A second and a half of "that worked", where the pointer already is.
388
+ .actionIconDone {
389
+ border-color: var(--v-ok);
390
+ background: color-mix(in srgb, var(--v-ok) 14%, var(--v-background));
391
+ color: var(--v-ok);
392
+
393
+ &:hover:not(:disabled) {
394
+ border-color: var(--v-ok);
395
+ background: color-mix(in srgb, var(--v-ok) 14%, var(--v-background));
396
+ color: var(--v-ok);
397
+ }
398
+ }
399
+
400
+ .actionDivider {
401
+ display: inline-block;
402
+ flex-shrink: 0;
403
+ width: 1px;
404
+ height: 18px;
405
+ margin: 0 0.2rem;
406
+ background: var(--v-line);
407
+ }
408
+
409
+ .iconButton {
410
+ @include vault-reset;
411
+ @include vault-focus;
412
+
413
+ display: inline-flex;
414
+ flex-shrink: 0;
415
+ align-items: center;
416
+ justify-content: center;
417
+ padding: 0.3rem;
418
+ border: 1px solid transparent;
419
+ border-radius: var(--v-btn-br);
420
+ background: transparent;
421
+ color: var(--v-paragraph);
422
+ font-size: 0.8rem;
423
+ cursor: pointer;
424
+
425
+ svg {
426
+ color: currentColor;
427
+ }
428
+
429
+ &:hover:not(:disabled) {
430
+ border-color: var(--v-line);
431
+ background: var(--v-hover);
432
+ color: var(--v-primary);
433
+ }
434
+
435
+ &:disabled {
436
+ opacity: 0.5;
437
+ cursor: default;
438
+ }
439
+ }
440
+
441
+ .linkButton {
442
+ @include vault-reset;
443
+ @include vault-focus;
444
+
445
+ display: inline-flex;
446
+ align-items: center;
447
+ padding: 0.1rem 0;
448
+ border: none;
449
+ border-radius: 2px;
450
+ background: none;
451
+ color: var(--v-primary);
452
+ font-size: 0.78rem;
453
+ font-weight: 600;
454
+ cursor: pointer;
455
+
456
+ &:hover {
457
+ color: var(--v-primary);
458
+ text-decoration: underline;
459
+ }
460
+ }
461
+
462
+ .dangerLink {
463
+ @extend .linkButton;
464
+
465
+ color: var(--v-bad);
466
+
467
+ &:hover {
468
+ color: var(--v-bad);
469
+ }
470
+ }
471
+
472
+ .select {
473
+ @include vault-reset;
474
+ @include vault-focus;
475
+
476
+ padding: 0.45rem 0.6rem;
477
+ border: 1px solid var(--v-line);
478
+ border-radius: var(--v-btn-br);
479
+ background: var(--v-background);
480
+ color: var(--v-paragraph);
481
+ font-size: 0.8rem;
482
+ cursor: pointer;
483
+ }
484
+
485
+ // Every checkbox and radio in the vault carries this, so the box survives
486
+ // being mounted anywhere — see `vault-check`. The descendant selectors below
487
+ // repeat it only as a safety net for markup that forgets the class.
488
+ // Every checkbox and radio in the vault carries this class; the mixin draws
489
+ // the whole control, both shapes included. Qualified with the element so the
490
+ // selector outweighs a host rule of the form `.some-form input`, which is how
491
+ // the CRM was stretching these in the first place.
492
+ input.checkControl {
493
+ @include vault-check;
494
+ }
495
+
496
+ .inlineCheck {
497
+ display: inline-flex;
498
+ flex-shrink: 0;
499
+ gap: 0.5rem;
500
+ align-items: center;
501
+ color: var(--v-paragraph);
502
+ font-size: 0.8rem;
503
+ line-height: 1.2;
504
+ cursor: pointer;
505
+
506
+ input[type='checkbox'],
507
+ input[type='radio'] {
508
+ @include vault-check;
509
+ }
510
+ }
511
+
512
+ .searchBar {
513
+ display: flex;
514
+ gap: 0.5rem;
515
+ align-items: center;
516
+ box-sizing: border-box;
517
+ padding: 0.5rem 0.75rem;
518
+ border: 1px solid var(--v-line);
519
+ border-radius: var(--v-btn-br);
520
+ background: var(--v-background);
521
+ color: var(--v-muted);
522
+
523
+ &:focus-within {
524
+ border-color: var(--v-primary);
525
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--v-primary) 15%, transparent);
526
+ }
527
+ }
528
+
529
+ .searchInput {
530
+ @include vault-reset;
531
+
532
+ flex: 1;
533
+ min-width: 0;
534
+ padding: 0;
535
+ border: none;
536
+ background: transparent;
537
+ color: var(--v-paragraph);
538
+ font-size: 0.88rem;
539
+
540
+ &::placeholder {
541
+ color: var(--v-muted);
542
+ opacity: 1;
543
+ }
544
+ }
545
+
546
+ // ---------------------------------------------------------------------------
547
+ // OTP chip
548
+ // ---------------------------------------------------------------------------
549
+
550
+ .otpChip {
551
+ @include vault-tokens;
552
+ @include vault-reset;
553
+ @include vault-focus;
554
+
555
+ display: inline-flex;
556
+ flex-shrink: 0;
557
+ gap: 0.3rem;
558
+ align-items: center;
559
+ justify-content: center;
560
+ padding: 0.28rem 0.55rem;
561
+ border: 1px solid var(--v-line);
562
+ border-radius: 999px;
563
+ background: var(--v-background);
564
+ color: var(--v-paragraph);
565
+ font-size: 0.74rem;
566
+ font-weight: 600;
567
+ white-space: nowrap;
568
+ cursor: pointer;
569
+
570
+ svg {
571
+ color: currentColor;
572
+ }
573
+
574
+ &:hover:not(:disabled) {
575
+ border-color: var(--v-primary);
576
+ background: var(--v-tint);
577
+ color: var(--v-primary);
578
+ }
579
+
580
+ &:disabled {
581
+ opacity: 0.6;
582
+ cursor: default;
583
+ }
584
+ }
585
+
586
+ .otpChipLg {
587
+ padding: 0.4rem 0.7rem;
588
+ font-size: 0.85rem;
589
+ }
590
+
591
+ // The quick search's variant: a 29px square that matches the icon buttons
592
+ // beside it, until a code arrives and the digits become the content.
593
+ .otpChipCompact {
594
+ height: 29px;
595
+ border-radius: var(--v-btn-br);
596
+
597
+ // Reserved so the row does not lurch sideways the moment the code lands.
598
+ &.otpChipOpen {
599
+ min-width: 96px;
600
+ padding: 0 0.45rem;
601
+ }
602
+ }
603
+
604
+ .otpChipIcon {
605
+ width: 29px;
606
+ min-width: 29px;
607
+ padding: 0;
608
+ }
609
+
610
+ .otpChipOpen {
611
+ border-color: color-mix(in srgb, var(--v-primary) 45%, transparent);
612
+ background: var(--v-tint);
613
+ color: var(--v-primary);
614
+ }
615
+
616
+ .otpChipExpiring {
617
+ border-color: var(--v-bad);
618
+ background: color-mix(in srgb, var(--v-bad) 12%, var(--v-background));
619
+ color: var(--v-bad);
620
+ }
621
+
622
+ .otpCode {
623
+ // Tabular figures, or the code jitters every second as the glyph widths
624
+ // change under it.
625
+ font-family: $vault-mono;
626
+ font-variant-numeric: tabular-nums;
627
+ font-feature-settings: 'tnum';
628
+ letter-spacing: 0.02em;
629
+ }
630
+
631
+ .otpLabel {
632
+ letter-spacing: 0.04em;
633
+ }
634
+
635
+ .otpRing {
636
+ flex-shrink: 0;
637
+ // 12 o'clock, draining clockwise.
638
+ transform: rotate(-90deg);
639
+ }
640
+
641
+ .otpRingTrack {
642
+ fill: none;
643
+ stroke: currentColor;
644
+ stroke-opacity: 0.22;
645
+ stroke-width: 2;
646
+ }
647
+
648
+ .otpRingValue {
649
+ fill: none;
650
+ stroke: currentColor;
651
+ stroke-width: 2;
652
+ stroke-linecap: round;
653
+ transition: stroke-dashoffset 0.95s linear;
654
+ }
655
+
656
+ // ---------------------------------------------------------------------------
657
+ // Confirm panel
658
+ // ---------------------------------------------------------------------------
659
+
660
+ .confirmPanel {
661
+ @include vault-tokens;
662
+
663
+ box-sizing: border-box;
664
+ padding: 0.85rem;
665
+ border: 1px solid color-mix(in srgb, var(--v-primary) 35%, transparent);
666
+ border-radius: var(--v-br);
667
+ background: var(--v-tint);
668
+ color: var(--v-paragraph);
669
+ }
670
+
671
+ .confirmCompact {
672
+ // Inside the popover the panel replaces the result list. Holding a height
673
+ // close to a couple of rows keeps the popover from collapsing and jumping
674
+ // back out again when the panel closes.
675
+ min-height: 152px;
676
+ margin: 0.6rem;
677
+ }
678
+
679
+ .confirmHead {
680
+ display: flex;
681
+ gap: 0.4rem;
682
+ align-items: center;
683
+ margin-bottom: 0.35rem;
684
+ color: var(--v-primary);
685
+ font-size: 0.85rem;
686
+ }
687
+
688
+ .confirmMessage {
689
+ margin: 0 0 0.55rem;
690
+ color: var(--v-muted);
691
+ font-size: 0.78rem;
692
+ line-height: 1.45;
693
+ }
694
+
695
+ .confirmRow {
696
+ display: flex;
697
+ flex-wrap: wrap;
698
+ gap: 0.4rem;
699
+ align-items: center;
700
+ }
701
+
702
+ .confirmInput {
703
+ @include vault-reset;
704
+ @include vault-focus;
705
+
706
+ flex: 1;
707
+ min-width: 150px;
708
+ padding: 0.45rem 0.6rem;
709
+ border: 1px solid var(--v-line);
710
+ border-radius: var(--v-btn-br);
711
+ background: var(--v-background);
712
+ color: var(--v-paragraph);
713
+ font-size: 0.85rem;
714
+
715
+ &[aria-invalid='true'] {
716
+ border-color: var(--v-bad);
717
+ }
718
+ }
719
+
720
+ .confirmButton {
721
+ @extend .primaryButton;
722
+
723
+ padding: 0.45rem 0.8rem;
724
+ }
725
+
726
+ .confirmCancel {
727
+ @extend .ghostButton;
728
+
729
+ padding: 0.45rem 0.7rem;
730
+ }
731
+
732
+ .confirmError {
733
+ // Always in the DOM, holding its own height, so an error does not shove
734
+ // the buttons down the instant it appears.
735
+ min-height: 1.15em;
736
+ margin: 0.45rem 0 0;
737
+ color: var(--v-bad);
738
+ font-size: 0.76rem;
739
+ font-weight: 500;
740
+ }
741
+
742
+ // ---------------------------------------------------------------------------
743
+ // Quick search (header popover)
744
+ // ---------------------------------------------------------------------------
745
+
746
+ .quickSearch {
747
+ @include vault-tokens;
748
+
749
+ position: relative;
750
+ align-items: center;
751
+ }
752
+
753
+ // Deliberately the same shape as Notification's trigger: these two sit side by
754
+ // side in the action pill and must read as one row of chips, not two designs.
755
+ // This one DOES inherit the header's colour, which is correct — it is a chip.
756
+ .trigger {
757
+ @include vault-focus;
758
+
759
+ position: relative;
760
+ display: flex;
761
+ align-items: center;
762
+ justify-content: center;
763
+ width: 40px;
764
+ height: 40px;
765
+ padding: 8px;
766
+ border: none;
767
+ border-radius: 50%;
768
+ background: transparent;
769
+ color: var(--paragraph-color, #333);
770
+ cursor: pointer;
771
+ transition: all 0.2s ease;
772
+
773
+ &:hover {
774
+ background-color: rgba(var(--primary-color-rgb, 0, 86, 179), 0.1);
775
+ color: var(--primary-color, #0056b3);
776
+ transform: scale(1.05);
777
+ }
778
+
779
+ &:active {
780
+ transform: scale(0.95);
781
+ }
782
+ }
783
+
784
+ .popoverWrapper {
785
+ position: absolute;
786
+ top: 100%;
787
+ right: 0;
788
+ z-index: 1100;
789
+ margin-top: 10px;
790
+ // The host's chip `li` is a centring flex container. The popover is
791
+ // absolutely positioned so it escapes the flexbox, but `text-align` is
792
+ // inherited and would centre every line of prose inside it.
793
+ text-align: left;
794
+ }
795
+
796
+ .popover {
797
+ @include vault-tokens;
798
+
799
+ display: flex;
800
+ flex-direction: column;
801
+ box-sizing: border-box;
802
+ // 560 rather than 480: a real title, a real email username and five
803
+ // actions have to share one line, and 480 was where the actions started
804
+ // wrapping onto a second and third row.
805
+ width: 560px;
806
+ max-width: calc(100vw - 2rem);
807
+ border: 1px solid var(--v-line);
808
+ border-radius: 10px;
809
+ // Opaque, and stated: the header behind this is a dark brand bar, and a
810
+ // panel that inherits any of it is unreadable.
811
+ background: var(--v-background);
812
+ color: var(--v-paragraph);
813
+ font-size: 0.85rem;
814
+ text-align: left;
815
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22), 0 2px 6px rgba(0, 0, 0, 0.12);
816
+ overflow: hidden;
817
+ animation: vaultPopIn 0.18s ease-out;
818
+
819
+ > .searchBar {
820
+ border: none;
821
+ border-bottom: 1px solid var(--v-line);
822
+ border-radius: 0;
823
+
824
+ &:focus-within {
825
+ border-bottom-color: var(--v-primary);
826
+ box-shadow: none;
827
+ }
828
+ }
829
+ }
830
+
831
+ .popoverHead {
832
+ display: flex;
833
+ gap: 0.75rem;
834
+ align-items: center;
835
+ justify-content: space-between;
836
+ padding: 0.7rem 0.85rem;
837
+ border-bottom: 1px solid var(--v-line);
838
+ background: var(--v-surface);
839
+ }
840
+
841
+ .popoverTitle {
842
+ display: inline-flex;
843
+ gap: 0.4rem;
844
+ align-items: center;
845
+ color: var(--v-paragraph);
846
+ font-size: 0.95rem;
847
+ font-weight: 700;
848
+
849
+ svg {
850
+ color: var(--v-primary);
851
+ }
852
+ }
853
+
854
+ .rows {
855
+ max-height: 348px;
856
+ margin: 0;
857
+ padding: 0;
858
+ list-style: none;
859
+ overflow-y: auto;
860
+
861
+ // The same reason every control in this file restates its own box: two
862
+ // hosts reach in here with bare element selectors. Navigation's
863
+ // `.hactions > ul li` handed every row `display: flex`, a 32px minimum
864
+ // box, `justify-content: center` and 2px side margins — at 0,1,2, which
865
+ // the `.row` grid below (0,1,0) cannot outrank — and the CRM's own
866
+ // `ul { padding-left: … }` indented the list. Navigation is scoped to
867
+ // `> li` now, but a component that survives only because its host is well
868
+ // behaved breaks the next time it is mounted somewhere new.
869
+ > li {
870
+ min-width: 0;
871
+ min-height: 0;
872
+ margin: 0;
873
+ padding: 0;
874
+ position: static;
875
+ list-style: none;
876
+ }
877
+ }
878
+
879
+ // Geometry, restated at 0,2,0 so it wins outright wherever this is mounted.
880
+ // Paint stays in `.row`; only the box is repeated.
881
+ .rows > .row {
882
+ display: grid;
883
+ grid-template-columns: minmax(0, 1fr) auto;
884
+ align-items: center;
885
+ justify-content: normal;
886
+ box-sizing: border-box;
887
+ min-width: 0;
888
+ min-height: 0;
889
+ margin: 0;
890
+ padding: 0.5rem 0.85rem;
891
+ position: static;
892
+ }
893
+
894
+ .rows > .row > .rowMain {
895
+ display: block;
896
+ }
897
+
898
+ // One row, one line. The identity truncates; the actions never do.
899
+ //
900
+ // It was three stacked blocks — title, meta, then a wrapping strip of labelled
901
+ // pills — which cost three lines per entry and, with a long title beside a
902
+ // long email username, spilled the pills onto two or three rows of their own.
903
+ // A grid with a `minmax(0, 1fr)` first column is what lets the text give way
904
+ // to the buttons instead of pushing them.
905
+ .row {
906
+ display: grid;
907
+ grid-template-columns: minmax(0, 1fr) auto;
908
+ gap: 0.75rem;
909
+ align-items: center;
910
+ padding: 0.5rem 0.85rem;
911
+ border-bottom: 1px solid color-mix(in srgb, var(--v-line) 55%, transparent);
912
+ border-left: 3px solid transparent;
913
+ cursor: default;
914
+
915
+ &:last-child {
916
+ border-bottom: none;
917
+ }
918
+ }
919
+
920
+ .rowActive {
921
+ border-left-color: var(--v-primary);
922
+ background: var(--v-hover);
923
+ }
924
+
925
+ .rowMain {
926
+ // `min-width: 0` is the load-bearing declaration: without it a grid item
927
+ // refuses to shrink below its content and the ellipsis never engages.
928
+ min-width: 0;
929
+ // Hard left, and stated rather than inherited: every title in the list
930
+ // starts on the same edge whatever its length, which is what makes the
931
+ // rows scan as a ledger.
932
+ //
933
+ // `stretch` — the default — and NOT `justify-self: start`, which sizes a
934
+ // grid item to fit-content. The content here is `white-space: nowrap`, so
935
+ // its min-content width IS its full width: under `start` the cell would
936
+ // refuse to shrink, overflow its track and slide under the action icons
937
+ // instead of ellipsing. Filling the track is what makes the ellipsis work,
938
+ // and `text-align: left` is what puts the text on its left edge.
939
+ justify-self: stretch;
940
+ overflow: hidden;
941
+ text-align: left;
942
+ color: var(--v-paragraph);
943
+ font-size: 0.88rem;
944
+ line-height: 1.35;
945
+ text-overflow: ellipsis;
946
+ white-space: nowrap;
947
+ }
948
+
949
+ // Kept for anything still rendering the old two-line shape.
950
+ .rowTitle {
951
+ display: flex;
952
+ gap: 0.35rem;
953
+ align-items: center;
954
+ min-width: 0;
955
+ }
956
+
957
+ .rowName {
958
+ color: var(--v-paragraph);
959
+ font-size: 0.9rem;
960
+ font-weight: 700;
961
+ }
962
+
963
+ .privateMark {
964
+ flex-shrink: 0;
965
+ margin-right: 0.25rem;
966
+ color: var(--v-muted);
967
+ vertical-align: -1px;
968
+ }
969
+
970
+ .rowMeta {
971
+ color: var(--v-muted);
972
+ font-size: 0.82rem;
973
+ }
974
+
975
+ .rowActions {
976
+ display: flex;
977
+ flex: 0 0 auto;
978
+ flex-wrap: nowrap;
979
+ gap: 0.25rem;
980
+ // Flush right on every row, so the clusters line up with each other
981
+ // rather than trailing whatever length the title happened to be.
982
+ justify-self: end;
983
+ margin-left: auto;
984
+ align-items: center;
985
+ }
986
+
987
+ // A square icon button. The label lives in the tooltip and the aria-label,
988
+ // which is what keeps five of them on one line.
989
+ .rowAction {
990
+ @include vault-reset;
991
+ @include vault-focus;
992
+
993
+ display: inline-flex;
994
+ flex: 0 0 auto;
995
+ align-items: center;
996
+ justify-content: center;
997
+ width: 29px;
998
+ height: 29px;
999
+ min-width: 29px;
1000
+ padding: 0;
1001
+ border: 1px solid var(--v-line);
1002
+ border-radius: var(--v-btn-br);
1003
+ background: var(--v-background);
1004
+ color: var(--v-secondary);
1005
+ cursor: pointer;
1006
+
1007
+ svg {
1008
+ flex-shrink: 0;
1009
+ color: currentColor;
1010
+ }
1011
+
1012
+ &:hover:not(:disabled) {
1013
+ border-color: var(--v-primary);
1014
+ background: var(--v-tint);
1015
+ color: var(--v-primary);
1016
+ }
1017
+
1018
+ &:disabled {
1019
+ opacity: 0.55;
1020
+ cursor: default;
1021
+ }
1022
+ }
1023
+
1024
+ .rowActionPrimary {
1025
+ border-color: var(--v-primary);
1026
+ background: var(--v-primary);
1027
+ color: #fff;
1028
+
1029
+ &:hover:not(:disabled) {
1030
+ border-color: color-mix(in srgb, var(--v-primary) 85%, #000);
1031
+ background: color-mix(in srgb, var(--v-primary) 85%, #000);
1032
+ color: #fff;
1033
+ }
1034
+ }
1035
+
1036
+ .empty {
1037
+ padding: 1.75rem 1rem;
1038
+ text-align: center;
1039
+
1040
+ p {
1041
+ margin: 0 0 0.85rem;
1042
+ color: var(--v-muted);
1043
+ font-size: 0.85rem;
1044
+ }
1045
+ }
1046
+
1047
+ .fallback {
1048
+ display: flex;
1049
+ flex-wrap: wrap;
1050
+ gap: 0.4rem;
1051
+ align-items: center;
1052
+ padding: 0.65rem 0.85rem;
1053
+ border-bottom: 1px solid var(--v-line);
1054
+ background: var(--v-tint);
1055
+
1056
+ label {
1057
+ width: 100%;
1058
+ color: var(--v-paragraph);
1059
+ font-size: 0.75rem;
1060
+ font-weight: 600;
1061
+ }
1062
+ }
1063
+
1064
+ .fallbackInput {
1065
+ @include vault-reset;
1066
+ @include vault-focus;
1067
+
1068
+ flex: 1;
1069
+ min-width: 160px;
1070
+ padding: 0.4rem 0.5rem;
1071
+ border: 1px solid var(--v-line);
1072
+ border-radius: var(--v-btn-br);
1073
+ background: var(--v-background);
1074
+ color: var(--v-paragraph);
1075
+ font-family: $vault-mono;
1076
+ font-size: 0.82rem;
1077
+ }
1078
+
1079
+ .footer {
1080
+ display: flex;
1081
+ gap: 0.5rem;
1082
+ align-items: center;
1083
+ justify-content: center;
1084
+ padding: 0.55rem 0.85rem;
1085
+ border-top: 1px solid var(--v-line);
1086
+ background: var(--v-surface);
1087
+ }
1088
+
1089
+ .footerHint {
1090
+ color: var(--v-muted);
1091
+ font-size: 0.75rem; // 12px at a 16px root
1092
+ }
1093
+
1094
+ // ---------------------------------------------------------------------------
1095
+ // Generator
1096
+ // ---------------------------------------------------------------------------
1097
+
1098
+ .generator {
1099
+ @include vault-tokens;
1100
+
1101
+ box-sizing: border-box;
1102
+ margin-top: 0.5rem;
1103
+ padding: 0.75rem;
1104
+ border: 1px solid var(--v-line);
1105
+ border-radius: var(--v-br);
1106
+ background: var(--v-surface);
1107
+ }
1108
+
1109
+ .genCandidate {
1110
+ display: flex;
1111
+ gap: 0.5rem;
1112
+ align-items: center;
1113
+ justify-content: space-between;
1114
+ padding: 0.45rem 0.6rem;
1115
+ border: 1px solid var(--v-line);
1116
+ border-radius: var(--v-btn-br);
1117
+ background: var(--v-background);
1118
+
1119
+ code {
1120
+ overflow-wrap: anywhere;
1121
+ color: var(--v-paragraph);
1122
+ font-family: $vault-mono;
1123
+ font-size: 0.88rem;
1124
+ user-select: all;
1125
+ }
1126
+ }
1127
+
1128
+ .strength {
1129
+ display: flex;
1130
+ gap: 0.5rem;
1131
+ align-items: center;
1132
+ margin-top: 0.5rem;
1133
+
1134
+ &[data-score='0'] .strengthFill {
1135
+ background: var(--v-bad);
1136
+ }
1137
+ &[data-score='1'] .strengthFill {
1138
+ background: rgb(200, 130, 50);
1139
+ }
1140
+ &[data-score='2'] .strengthFill {
1141
+ background: rgb(175, 155, 40);
1142
+ }
1143
+ &[data-score='3'] .strengthFill,
1144
+ &[data-score='4'] .strengthFill {
1145
+ background: var(--v-ok);
1146
+ }
1147
+ }
1148
+
1149
+ .strengthTrack {
1150
+ flex: 1;
1151
+ height: 5px;
1152
+ border-radius: 999px;
1153
+ background: var(--v-line);
1154
+ overflow: hidden;
1155
+ }
1156
+
1157
+ .strengthFill {
1158
+ height: 100%;
1159
+ border-radius: 999px;
1160
+ transition: width 0.2s ease, background-color 0.2s ease;
1161
+ }
1162
+
1163
+ .strengthLabel {
1164
+ min-width: 62px;
1165
+ color: var(--v-muted);
1166
+ font-size: 0.75rem;
1167
+ font-weight: 600;
1168
+ text-align: right;
1169
+ }
1170
+
1171
+ .genLength {
1172
+ display: block;
1173
+ margin-top: 0.75rem;
1174
+ color: var(--v-muted);
1175
+ font-size: 0.78rem;
1176
+
1177
+ strong {
1178
+ color: var(--v-paragraph);
1179
+ }
1180
+
1181
+ input {
1182
+ width: 100%;
1183
+ margin-top: 0.3rem;
1184
+ accent-color: var(--v-primary);
1185
+ }
1186
+ }
1187
+
1188
+ .genOptions {
1189
+ display: flex;
1190
+ flex-wrap: wrap;
1191
+ gap: 0.75rem;
1192
+ margin-top: 0.6rem;
1193
+ }
1194
+
1195
+ .genOption {
1196
+ @extend .inlineCheck;
1197
+
1198
+ font-size: 0.78rem;
1199
+ }
1200
+
1201
+ .genActions {
1202
+ display: flex;
1203
+ gap: 0.75rem;
1204
+ align-items: center;
1205
+ margin-top: 0.85rem;
1206
+ }
1207
+
1208
+ // ---------------------------------------------------------------------------
1209
+ // Entry form
1210
+ // ---------------------------------------------------------------------------
1211
+
1212
+ .form {
1213
+ @include vault-tokens;
1214
+
1215
+ display: flex;
1216
+ flex-direction: column;
1217
+ gap: 0.9rem;
1218
+ box-sizing: border-box;
1219
+ padding: 1rem;
1220
+ color: var(--v-paragraph);
1221
+ font-size: 0.85rem;
1222
+ text-align: left;
1223
+ }
1224
+
1225
+ .field {
1226
+ display: flex;
1227
+ flex-direction: column;
1228
+ gap: 0.3rem;
1229
+ }
1230
+
1231
+ .label {
1232
+ color: var(--v-muted);
1233
+ font-size: 0.74rem;
1234
+ font-weight: 700;
1235
+ letter-spacing: 0.04em;
1236
+ text-transform: uppercase;
1237
+ }
1238
+
1239
+ .input,
1240
+ .textarea {
1241
+ @include vault-reset;
1242
+ @include vault-focus;
1243
+
1244
+ width: 100%;
1245
+ padding: 0.5rem 0.6rem;
1246
+ border: 1px solid var(--v-line);
1247
+ border-radius: var(--v-btn-br);
1248
+ background: var(--v-background);
1249
+ color: var(--v-paragraph);
1250
+ font-size: 0.87rem;
1251
+
1252
+ &::placeholder {
1253
+ color: var(--v-muted);
1254
+ opacity: 1;
1255
+ }
1256
+
1257
+ &:focus {
1258
+ border-color: var(--v-primary);
1259
+ }
1260
+
1261
+ &:disabled {
1262
+ background: var(--v-surface);
1263
+ opacity: 0.7;
1264
+ }
1265
+ }
1266
+
1267
+ .textarea {
1268
+ line-height: 1.5;
1269
+ resize: vertical;
1270
+ }
1271
+
1272
+ .help {
1273
+ margin: 0;
1274
+ color: var(--v-muted);
1275
+ font-size: 0.75rem;
1276
+ }
1277
+
1278
+ .fieldError {
1279
+ display: flex;
1280
+ gap: 0.3rem;
1281
+ align-items: center;
1282
+ margin: 0;
1283
+ color: var(--v-bad);
1284
+ font-size: 0.76rem;
1285
+ font-weight: 500;
1286
+ }
1287
+
1288
+ .totpOk {
1289
+ display: flex;
1290
+ gap: 0.3rem;
1291
+ align-items: center;
1292
+ margin: 0;
1293
+ color: var(--v-ok);
1294
+ font-size: 0.76rem;
1295
+ font-weight: 600;
1296
+ font-variant-numeric: tabular-nums;
1297
+ }
1298
+
1299
+ .passwordField {
1300
+ display: flex;
1301
+ flex-direction: column;
1302
+ gap: 0.45rem;
1303
+ }
1304
+
1305
+ .inputRow {
1306
+ display: flex;
1307
+ gap: 0.4rem;
1308
+ align-items: center;
1309
+ }
1310
+
1311
+ .totpConfigured {
1312
+ display: flex;
1313
+ flex-wrap: wrap;
1314
+ gap: 0.7rem;
1315
+ align-items: center;
1316
+ padding: 0.55rem 0.6rem;
1317
+ border: 1px solid var(--v-line);
1318
+ border-radius: var(--v-btn-br);
1319
+ background: var(--v-surface);
1320
+ color: var(--v-ok);
1321
+ font-size: 0.83rem;
1322
+ font-weight: 600;
1323
+ }
1324
+
1325
+ // ---------------------------------------------------------------------------
1326
+ // 2FA: the field, and the QR paths into it
1327
+ // ---------------------------------------------------------------------------
1328
+
1329
+ .totpField {
1330
+ display: flex;
1331
+ flex-direction: column;
1332
+ gap: 0.45rem;
1333
+ box-sizing: border-box;
1334
+ padding: 2px;
1335
+ border: 1px dashed transparent;
1336
+ border-radius: var(--v-br);
1337
+ // Only the border colour moves on a drag, so the field does not shift the
1338
+ // fields below it by a pixel when an image is dragged over the modal.
1339
+ transition: border-color 0.15s ease, background-color 0.15s ease;
1340
+ }
1341
+
1342
+ .totpFieldDropping {
1343
+ border-color: var(--v-primary);
1344
+ background: var(--v-tint);
1345
+ }
1346
+
1347
+ .qrActions {
1348
+ display: flex;
1349
+ flex-wrap: wrap;
1350
+ gap: 0.5rem;
1351
+ align-items: center;
1352
+ }
1353
+
1354
+ // The file input behind "Upload QR image". Not `display: none`, which would
1355
+ // take it out of the accessibility tree along with the label the button
1356
+ // already carries.
1357
+ .visuallyHidden {
1358
+ position: absolute;
1359
+ width: 1px;
1360
+ height: 1px;
1361
+ padding: 0;
1362
+ margin: -1px;
1363
+ overflow: hidden;
1364
+ clip: rect(0, 0, 0, 0);
1365
+ white-space: nowrap;
1366
+ border: 0;
1367
+ }
1368
+
1369
+ .qrScanner {
1370
+ display: flex;
1371
+ flex-direction: column;
1372
+ gap: 0.5rem;
1373
+ box-sizing: border-box;
1374
+ padding: 0.6rem;
1375
+ border: 1px solid var(--v-line);
1376
+ border-radius: var(--v-br);
1377
+ background: var(--v-surface);
1378
+ animation: vaultPopIn 0.12s ease-out;
1379
+ }
1380
+
1381
+ .qrScannerHead {
1382
+ display: flex;
1383
+ gap: 0.5rem;
1384
+ align-items: center;
1385
+ color: var(--v-paragraph);
1386
+ font-size: 0.8rem;
1387
+ font-weight: 600;
1388
+
1389
+ svg {
1390
+ flex-shrink: 0;
1391
+ color: var(--v-primary);
1392
+ }
1393
+
1394
+ // The title takes the slack, so the controls sit hard right.
1395
+ > span {
1396
+ margin-right: auto;
1397
+ }
1398
+ }
1399
+
1400
+ .qrStage {
1401
+ position: relative;
1402
+ overflow: hidden;
1403
+ box-sizing: border-box;
1404
+ width: 100%;
1405
+ max-height: 300px;
1406
+ aspect-ratio: 4 / 3;
1407
+ border-radius: var(--v-br);
1408
+ background: #000;
1409
+ }
1410
+
1411
+ .qrVideo {
1412
+ display: block;
1413
+ width: 100%;
1414
+ height: 100%;
1415
+ object-fit: cover;
1416
+ // The preview is a viewfinder, not a mirror: a rear camera pointed at a
1417
+ // printed code must not show the code reversed.
1418
+ transform: none;
1419
+ }
1420
+
1421
+ .qrViewfinder {
1422
+ position: absolute;
1423
+ top: 50%;
1424
+ left: 50%;
1425
+ width: min(62%, 200px);
1426
+ aspect-ratio: 1;
1427
+ transform: translate(-50%, -50%);
1428
+ border: 2px solid rgba(255, 255, 255, 0.85);
1429
+ border-radius: 10px;
1430
+ // Everything outside the frame dims, which is what makes the frame read
1431
+ // as a target rather than as a border.
1432
+ box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.35);
1433
+ pointer-events: none;
1434
+ }
1435
+
1436
+ .qrSweep {
1437
+ position: absolute;
1438
+ top: 0;
1439
+ right: 6px;
1440
+ left: 6px;
1441
+ height: 2px;
1442
+ border-radius: 2px;
1443
+ background: rgba(255, 255, 255, 0.9);
1444
+ animation: vaultQrSweep 2.4s ease-in-out infinite;
1445
+ }
1446
+
1447
+ // `top` rather than a transform, because a percentage translate on a 2px-tall
1448
+ // element is 2px — the travel has to be measured against the frame.
1449
+ @keyframes vaultQrSweep {
1450
+ 0%,
1451
+ 100% {
1452
+ top: 8%;
1453
+ opacity: 0.35;
1454
+ }
1455
+ 50% {
1456
+ top: 90%;
1457
+ opacity: 0.9;
1458
+ }
1459
+ }
1460
+
1461
+ @media (prefers-reduced-motion: reduce) {
1462
+ .qrSweep {
1463
+ top: 50%;
1464
+ animation: none;
1465
+ opacity: 0.5;
1466
+ }
1467
+ }
1468
+
1469
+ .qrStarting {
1470
+ position: absolute;
1471
+ inset: 0;
1472
+ display: flex;
1473
+ gap: 0.5rem;
1474
+ align-items: center;
1475
+ justify-content: center;
1476
+ background: rgba(0, 0, 0, 0.55);
1477
+ color: #fff;
1478
+ font-size: 0.8rem;
1479
+ font-weight: 600;
1480
+ }
1481
+
1482
+ .tagField {
1483
+ display: flex;
1484
+ flex-wrap: wrap;
1485
+ gap: 0.3rem;
1486
+ align-items: center;
1487
+ box-sizing: border-box;
1488
+ padding: 0.35rem 0.45rem;
1489
+ border: 1px solid var(--v-line);
1490
+ border-radius: var(--v-btn-br);
1491
+ background: var(--v-background);
1492
+
1493
+ &:focus-within {
1494
+ border-color: var(--v-primary);
1495
+ }
1496
+ }
1497
+
1498
+ .tagInput {
1499
+ @include vault-reset;
1500
+
1501
+ flex: 1;
1502
+ min-width: 110px;
1503
+ padding: 0.2rem 0;
1504
+ border: none;
1505
+ background: transparent;
1506
+ color: var(--v-paragraph);
1507
+ font-size: 0.84rem;
1508
+
1509
+ &::placeholder {
1510
+ color: var(--v-muted);
1511
+ opacity: 1;
1512
+ }
1513
+ }
1514
+
1515
+ .tagPill {
1516
+ display: inline-flex;
1517
+ gap: 0.25rem;
1518
+ align-items: center;
1519
+ padding: 0.12rem 0.5rem;
1520
+ border-radius: 999px;
1521
+ background: var(--v-tint);
1522
+ color: var(--v-primary);
1523
+ font-size: 0.72rem;
1524
+ font-weight: 700;
1525
+
1526
+ button {
1527
+ @include vault-reset;
1528
+
1529
+ display: inline-flex;
1530
+ align-items: center;
1531
+ padding: 0;
1532
+ border: none;
1533
+ background: none;
1534
+ color: inherit;
1535
+ font-size: 0.72rem;
1536
+ cursor: pointer;
1537
+ opacity: 0.7;
1538
+
1539
+ &:hover {
1540
+ color: inherit;
1541
+ opacity: 1;
1542
+ }
1543
+ }
1544
+ }
1545
+
1546
+ .radioRow {
1547
+ display: flex;
1548
+ flex-wrap: wrap;
1549
+ gap: 1rem;
1550
+
1551
+ label {
1552
+ display: inline-flex;
1553
+ gap: 0.5rem;
1554
+ align-items: center;
1555
+ color: var(--v-paragraph);
1556
+ font-size: 0.84rem;
1557
+ line-height: 1.2;
1558
+ cursor: pointer;
1559
+ }
1560
+
1561
+ input[type='radio'],
1562
+ input[type='checkbox'] {
1563
+ @include vault-check;
1564
+ }
1565
+ }
1566
+
1567
+ .radioDisabled {
1568
+ cursor: default;
1569
+ opacity: 0.5;
1570
+ }
1571
+
1572
+ .formActions {
1573
+ display: flex;
1574
+ gap: 0.6rem;
1575
+ align-items: center;
1576
+ padding-top: 0.5rem;
1577
+ border-top: 1px solid var(--v-line);
1578
+ }
1579
+
1580
+ // ---------------------------------------------------------------------------
1581
+ // Manager page
1582
+ // ---------------------------------------------------------------------------
1583
+
1584
+ .manager {
1585
+ @include vault-tokens;
1586
+
1587
+ box-sizing: border-box;
1588
+ // A tight bottom: the pager is measured to end just above the fold, and a
1589
+ // 2rem skirt under it was enough to put a scrollbar on the page.
1590
+ padding: 1.25rem 1.5rem 1rem;
1591
+ color: var(--v-paragraph);
1592
+ font-size: 0.85rem;
1593
+ }
1594
+
1595
+ .managerHead {
1596
+ display: flex;
1597
+ flex-wrap: wrap;
1598
+ gap: 1rem;
1599
+ align-items: flex-start;
1600
+ justify-content: space-between;
1601
+ margin-bottom: 1rem;
1602
+ }
1603
+
1604
+ .pageTitle {
1605
+ margin: 0;
1606
+ color: var(--v-paragraph);
1607
+ font-size: 1.5rem;
1608
+ font-weight: 700;
1609
+ line-height: 1.2;
1610
+ }
1611
+
1612
+ .pageIntro {
1613
+ max-width: 62ch;
1614
+ margin: 0.35rem 0 0;
1615
+ color: var(--v-muted);
1616
+ font-size: 0.83rem;
1617
+ line-height: 1.5;
1618
+ }
1619
+
1620
+ .toolbar {
1621
+ display: flex;
1622
+ flex-wrap: wrap;
1623
+ gap: 0.6rem;
1624
+ align-items: center;
1625
+ margin-bottom: 1rem;
1626
+ padding: 0.65rem;
1627
+ border: 1px solid var(--v-line);
1628
+ border-radius: var(--v-br);
1629
+ background: var(--v-surface);
1630
+
1631
+ .searchBar {
1632
+ flex: 1;
1633
+ min-width: 240px;
1634
+ }
1635
+ }
1636
+
1637
+ .tableWrap {
1638
+ // A last resort only: the table is sized to fit a 1024px tablet, and the
1639
+ // URL column folds away below 1200px so it does not have to scroll there.
1640
+ overflow-x: auto;
1641
+ border: 1px solid var(--v-line);
1642
+ border-radius: var(--v-br);
1643
+ background: var(--v-background);
1644
+ }
1645
+
1646
+ // The list is sized to the window rather than to its contents — the height
1647
+ // comes from JS, which is the only place the distance to the bottom of the
1648
+ // viewport is knowable. What this adds is the clipping: a row that does not
1649
+ // fit is cut off here instead of growing the page and pushing the pager out
1650
+ // of reach. In a window too short even for the five-row floor, the component
1651
+ // switches this to `overflow-y: auto` and the header stays put.
1652
+ .tableWrapFitted {
1653
+ overflow-x: hidden;
1654
+
1655
+ thead th {
1656
+ position: sticky;
1657
+ top: 0;
1658
+ z-index: 1;
1659
+ }
1660
+
1661
+ // Fitted mode: every row is exactly --vault-row-height tall so the page size
1662
+ // is arithmetic. One line per row; anything longer is clipped with an
1663
+ // ellipsis, and the tag chips stop wrapping.
1664
+ .tableWrapFitted {
1665
+ --vault-row-height: 58px;
1666
+
1667
+ tbody tr {
1668
+ height: var(--vault-row-height);
1669
+ max-height: var(--vault-row-height);
1670
+ }
1671
+
1672
+ tbody td {
1673
+ height: var(--vault-row-height);
1674
+ padding-top: 0;
1675
+ padding-bottom: 0;
1676
+ white-space: nowrap;
1677
+ overflow: hidden;
1678
+ text-overflow: ellipsis;
1679
+ }
1680
+
1681
+ tbody td > * {
1682
+ max-width: 100%;
1683
+ }
1684
+
1685
+ // Title + tag line share the fixed row; the tags clip, never wrap.
1686
+ tbody td .tagRow {
1687
+ flex-wrap: nowrap;
1688
+ overflow: hidden;
1689
+ }
1690
+
1691
+ tbody td {
1692
+ line-height: 1.25;
1693
+ }
1694
+ }
1695
+ }
1696
+
1697
+ .table {
1698
+ width: 100%;
1699
+ border-collapse: collapse;
1700
+ color: var(--v-paragraph);
1701
+ font-size: 0.84rem;
1702
+ text-align: left;
1703
+
1704
+ th,
1705
+ td {
1706
+ padding: 0.6rem 0.7rem;
1707
+ text-align: left;
1708
+ vertical-align: middle;
1709
+ }
1710
+
1711
+ thead th {
1712
+ border-bottom: 1px solid var(--v-line);
1713
+ background: var(--v-surface);
1714
+ color: var(--v-muted);
1715
+ font-size: 0.72rem;
1716
+ font-weight: 700;
1717
+ letter-spacing: 0.05em;
1718
+ text-transform: uppercase;
1719
+ white-space: nowrap;
1720
+ }
1721
+
1722
+ tbody tr {
1723
+ border-bottom: 1px solid color-mix(in srgb, var(--v-line) 55%, transparent);
1724
+
1725
+ &:last-child {
1726
+ border-bottom: none;
1727
+ }
1728
+
1729
+ &:hover {
1730
+ background: var(--v-hover);
1731
+ }
1732
+ }
1733
+
1734
+ tbody td strong {
1735
+ color: var(--v-paragraph);
1736
+ font-size: 0.88rem;
1737
+ font-weight: 700;
1738
+ }
1739
+ }
1740
+
1741
+ .sortButton {
1742
+ @include vault-reset;
1743
+ @include vault-focus;
1744
+
1745
+ display: inline-flex;
1746
+ gap: 0.25rem;
1747
+ align-items: center;
1748
+ padding: 0;
1749
+ border: none;
1750
+ background: none;
1751
+ color: inherit;
1752
+ font-size: inherit;
1753
+ font-weight: inherit;
1754
+ letter-spacing: inherit;
1755
+ text-transform: inherit;
1756
+ cursor: pointer;
1757
+
1758
+ svg {
1759
+ color: var(--v-primary);
1760
+ }
1761
+
1762
+ &:hover {
1763
+ color: var(--v-primary);
1764
+ }
1765
+ }
1766
+
1767
+ .actionsHead,
1768
+ .actionsCell {
1769
+ text-align: right;
1770
+ white-space: nowrap;
1771
+ }
1772
+
1773
+ .actionsCell {
1774
+ display: table-cell;
1775
+
1776
+ // Wraps rather than forcing a horizontal scrollbar on a narrow tablet.
1777
+ > * {
1778
+ margin-left: 0.25rem;
1779
+ }
1780
+ }
1781
+
1782
+ .hostLink {
1783
+ @include vault-reset;
1784
+ @include vault-focus;
1785
+
1786
+ display: inline-flex;
1787
+ gap: 0.25rem;
1788
+ align-items: center;
1789
+ color: var(--v-primary);
1790
+ font-size: 0.83rem;
1791
+ font-weight: 500;
1792
+
1793
+ &:hover {
1794
+ color: var(--v-primary);
1795
+ text-decoration: underline;
1796
+ }
1797
+ }
1798
+
1799
+ // Only visible once the URL column folds away, so the site is never a mystery
1800
+ // on a tablet.
1801
+ .hostUnderTitle {
1802
+ display: none;
1803
+ color: var(--v-muted);
1804
+ font-size: 0.76rem;
1805
+ }
1806
+
1807
+ .pill {
1808
+ display: inline-flex;
1809
+ gap: 0.25rem;
1810
+ align-items: center;
1811
+ padding: 0.12rem 0.5rem;
1812
+ border-radius: 999px;
1813
+ font-size: 0.72rem;
1814
+ font-weight: 700;
1815
+ white-space: nowrap;
1816
+ }
1817
+
1818
+ .pillShared {
1819
+ background: color-mix(in srgb, var(--v-ok) 15%, var(--v-background));
1820
+ color: var(--v-ok);
1821
+ }
1822
+
1823
+ .pillPrivate {
1824
+ background: color-mix(in srgb, var(--v-paragraph) 12%, var(--v-background));
1825
+ color: var(--v-muted);
1826
+ }
1827
+
1828
+ .deletedRow {
1829
+ opacity: 0.65;
1830
+
1831
+ strong {
1832
+ text-decoration: line-through;
1833
+ }
1834
+ }
1835
+
1836
+ .deletedTag {
1837
+ margin-left: 0.4rem;
1838
+ padding: 0.05rem 0.4rem;
1839
+ border-radius: 999px;
1840
+ background: color-mix(in srgb, var(--v-bad) 15%, var(--v-background));
1841
+ color: var(--v-bad);
1842
+ font-size: 0.66rem;
1843
+ font-weight: 700;
1844
+ text-decoration: none;
1845
+ }
1846
+
1847
+ // Under the title, not beside it: the title keeps its own line and the
1848
+ // chips take a compact second one, which is what lets a row stay short.
1849
+ .tagRow {
1850
+ display: flex;
1851
+ flex-wrap: nowrap;
1852
+ gap: 0.25rem;
1853
+ margin: 0.2rem 0 0;
1854
+ max-width: 100%;
1855
+ overflow: hidden;
1856
+
1857
+ .tagPill {
1858
+ flex: 0 0 auto;
1859
+ padding: 0.05rem 0.45rem;
1860
+ font-size: 0.68rem;
1861
+ }
1862
+ }
1863
+
1864
+ .tableEmpty {
1865
+ padding: 2.5rem 1rem !important;
1866
+ color: var(--v-muted);
1867
+ text-align: center !important;
1868
+
1869
+ p {
1870
+ margin: 0 0 0.9rem;
1871
+ color: var(--v-muted);
1872
+ font-size: 0.88rem;
1873
+ }
1874
+ }
1875
+
1876
+ .pager {
1877
+ display: flex;
1878
+ flex-wrap: wrap;
1879
+ gap: 0.75rem;
1880
+ align-items: center;
1881
+ justify-content: space-between;
1882
+ // PAGER_GAP in VaultManager.jsx budgets for this margin; changing one
1883
+ // without the other loses the bottom of the pager or leaves a gap.
1884
+ margin-top: 0.85rem;
1885
+ color: var(--v-muted);
1886
+ font-size: 0.8rem;
1887
+ }
1888
+
1889
+ .pagerButtons {
1890
+ display: flex;
1891
+ gap: 0.5rem;
1892
+ align-items: center;
1893
+ }
1894
+
1895
+ // An iPad in landscape is 1024px, and the CRM's own chrome takes some of that.
1896
+ // Below 1200 the URL column folds and the host moves under the title, which
1897
+ // keeps the whole table inside the viewport with no horizontal scroll.
1898
+ @media (max-width: 1200px) {
1899
+ .urlColumn {
1900
+ display: none;
1901
+ }
1902
+
1903
+ .hostUnderTitle {
1904
+ display: block;
1905
+ }
1906
+ }
1907
+
1908
+ @media (max-width: 900px) {
1909
+ .manager {
1910
+ padding: 1rem 0.85rem 1rem;
1911
+ }
1912
+
1913
+ .actionsCell {
1914
+ text-align: left;
1915
+
1916
+ > * {
1917
+ margin: 0.15rem 0.25rem 0.15rem 0;
1918
+ }
1919
+ }
1920
+
1921
+ .actionCluster {
1922
+ justify-content: flex-start;
1923
+ }
1924
+ }
1925
+
1926
+ // ---------------------------------------------------------------------------
1927
+ // Access log
1928
+ // ---------------------------------------------------------------------------
1929
+
1930
+ .log {
1931
+ @include vault-tokens;
1932
+
1933
+ box-sizing: border-box;
1934
+ padding: 1rem;
1935
+ color: var(--v-paragraph);
1936
+ font-size: 0.85rem;
1937
+ text-align: left;
1938
+ }
1939
+
1940
+ .logFilters {
1941
+ display: flex;
1942
+ flex-wrap: wrap;
1943
+ gap: 0.5rem;
1944
+ margin-bottom: 0.85rem;
1945
+ }
1946
+
1947
+ .logRows {
1948
+ max-height: 50vh;
1949
+ margin: 0;
1950
+ padding: 0;
1951
+ list-style: none;
1952
+ overflow-y: auto;
1953
+ }
1954
+
1955
+ .logRow {
1956
+ display: flex;
1957
+ flex-wrap: wrap;
1958
+ gap: 0.5rem;
1959
+ align-items: baseline;
1960
+ justify-content: space-between;
1961
+ padding: 0.55rem 0;
1962
+ border-bottom: 1px solid color-mix(in srgb, var(--v-line) 55%, transparent);
1963
+ color: var(--v-paragraph);
1964
+ font-size: 0.83rem;
1965
+
1966
+ &:last-child {
1967
+ border-bottom: none;
1968
+ }
1969
+
1970
+ em {
1971
+ color: var(--v-primary);
1972
+ font-style: normal;
1973
+ font-weight: 600;
1974
+ }
1975
+ }
1976
+
1977
+ .logMeta {
1978
+ display: flex;
1979
+ gap: 0.75rem;
1980
+ color: var(--v-muted);
1981
+ font-size: 0.75rem;
1982
+ white-space: nowrap;
1983
+ }