@visns-studio/visns-components 6.17.2 → 6.20.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 (83) hide show
  1. package/package.json +10 -7
  2. package/src/components/AssociationManager.jsx +344 -177
  3. package/src/components/AsyncSelect.jsx +2 -2
  4. package/src/components/Autocomplete.jsx +57 -12
  5. package/src/components/Breadcrumb.jsx +23 -4
  6. package/src/components/ClientAssociationManager.jsx +321 -145
  7. package/src/components/DataGrid.jsx +625 -20
  8. package/src/components/DropZone.jsx +4 -1
  9. package/src/components/Form.jsx +233 -6
  10. package/src/components/MultiSelect.jsx +2 -2
  11. package/src/components/Navigation.jsx +39 -23
  12. package/src/components/TableFilter.jsx +172 -12
  13. package/src/components/UniversalSearch.jsx +390 -0
  14. package/src/components/auth/Login.jsx +164 -8
  15. package/src/components/auth/Profile.jsx +209 -22
  16. package/src/components/auth/authEndpoints.js +4 -0
  17. package/src/components/auth/webauthn.js +194 -0
  18. package/src/components/columns/ColumnRenderers.jsx +6 -1
  19. package/src/components/controls/DataGridSearch.jsx +11 -5
  20. package/src/components/generic/GenericAuth.jsx +6 -0
  21. package/src/components/generic/GenericDashboard.jsx +93 -36
  22. package/src/components/generic/GenericDetail.jsx +146 -61
  23. package/src/components/generic/GenericIndex.jsx +134 -38
  24. package/src/components/generic/GenericSettings.jsx +220 -0
  25. package/src/components/generic/StandardModal.jsx +165 -8
  26. package/src/components/generic/dashboardChartDefaults.js +230 -0
  27. package/src/components/generic/shared/indexLayout.js +38 -0
  28. package/src/components/settings/IntegrationsManager.jsx +474 -0
  29. package/src/components/sms/SmsLineSettings.jsx +16 -12
  30. package/src/components/styles/AssociationManager.module.scss +4 -363
  31. package/src/components/styles/AuditLog.module.scss +6 -6
  32. package/src/components/styles/AuditLogs.module.scss +3 -3
  33. package/src/components/styles/Autocomplete.module.scss +7 -7
  34. package/src/components/styles/BusinessCardOcr.module.scss +6 -1
  35. package/src/components/styles/CallQueueSettings.module.scss +4 -1
  36. package/src/components/styles/CameraPlacement.module.scss +19 -19
  37. package/src/components/styles/CategorizedDropZone.module.scss +6 -1
  38. package/src/components/styles/ClientAssociationManager.module.scss +5 -289
  39. package/src/components/styles/ClientAuth.module.scss +1 -1
  40. package/src/components/styles/DataGrid.module.scss +455 -8
  41. package/src/components/styles/Field.module.scss +32 -24
  42. package/src/components/styles/Form.module.scss +123 -3
  43. package/src/components/styles/GenericDashboard.module.scss +612 -825
  44. package/src/components/styles/GenericDetail.module.scss +406 -61
  45. package/src/components/styles/GenericDynamic.module.scss +25 -25
  46. package/src/components/styles/GenericEditableTable.module.scss +33 -33
  47. package/src/components/styles/GenericFormBuilder.module.scss +119 -119
  48. package/src/components/styles/GenericIndex.module.scss +483 -38
  49. package/src/components/styles/GenericMain.module.scss +6 -3
  50. package/src/components/styles/GenericQuote.module.scss +399 -81
  51. package/src/components/styles/GenericReport.module.scss +8 -8
  52. package/src/components/styles/GenericSettings.module.scss +209 -0
  53. package/src/components/styles/GenericSort.module.scss +9 -9
  54. package/src/components/styles/Integrations.module.scss +321 -0
  55. package/src/components/styles/List.module.scss +4 -4
  56. package/src/components/styles/Login.module.scss +57 -0
  57. package/src/components/styles/MultiSelect.module.scss +4 -4
  58. package/src/components/styles/Navigation.module.scss +12 -12
  59. package/src/components/styles/Notification.module.scss +12 -8
  60. package/src/components/styles/NotificationList.module.scss +5 -5
  61. package/src/components/styles/Profile.module.scss +5 -5
  62. package/src/components/styles/QrCode.module.scss +4 -4
  63. package/src/components/styles/QuickAction.module.scss +20 -20
  64. package/src/components/styles/SortableQuoteItems.module.scss +4 -4
  65. package/src/components/styles/TableFilter.module.scss +128 -17
  66. package/src/components/styles/TicketConversation.module.scss +497 -0
  67. package/src/components/styles/UniversalSearch.module.scss +229 -0
  68. package/src/components/styles/Vault.module.scss +413 -0
  69. package/src/components/styles/_associationManager.scss +467 -0
  70. package/src/components/styles/_authBrandPanel.scss +15 -9
  71. package/src/components/styles/_controls.scss +43 -39
  72. package/src/components/styles/global-datagrid.css +196 -0
  73. package/src/components/styles/global.css +168 -17
  74. package/src/components/tickets/TicketConversation.jsx +455 -0
  75. package/src/components/utils/rowActionSettings.js +117 -0
  76. package/src/components/vault/VaultClientPicker.jsx +183 -0
  77. package/src/components/vault/VaultEntryForm.jsx +23 -0
  78. package/src/components/vault/VaultManager.jsx +382 -34
  79. package/src/components/vault/VaultShareModal.jsx +454 -0
  80. package/src/components/vault/vaultEndpoints.js +16 -2
  81. package/src/components/vault/vaultShares.js +247 -0
  82. package/src/index.js +35 -0
  83. package/src/utils/fetchUtil.js +38 -14
@@ -20,9 +20,10 @@
20
20
  * edge under the selected one instead of the vertical rail's brand fill. It is
21
21
  * the treatment narrow viewports get for any rail without expandable parents
22
22
  * (`.flatFilter`, inside the phone media query at the bottom of this file),
23
- * and the treatment a page opts into at every width with
24
- * `<TableFilter variant="underline">` — a vertical rail spends a fifth of the
25
- * page on navigation, which pays for itself at seven tabs and does not at two.
23
+ * and the treatment `<TableFilter variant="underline">` gets at every width —
24
+ * which is every detail page, since GenericDetail now asks for it
25
+ * unconditionally. A vertical rail spends a fifth of the page on navigation,
26
+ * permanently, and it is the content panel beside it that pays.
26
27
  *
27
28
  * The selectors inside are deliberately longer than the ones they override:
28
29
  * `.subactive` and `.activetab button` carry `!important`, so only a more
@@ -62,6 +63,54 @@
62
63
  display: none;
63
64
  }
64
65
 
66
+ /* The hidden scrollbar leaves a strip that overflows looking identical to
67
+ one that fits, so the overflowing edge is faded instead. TableFilter
68
+ measures the scroller and writes `data-overflow`; masking only the edge
69
+ that actually has something past it means a strip that fits is never
70
+ faded, and the fade disappears as you reach each end.
71
+
72
+ `mask-image` over a gradient rather than an overlay element: the strip is
73
+ a scroller, so a positioned overlay inside it would scroll away with the
74
+ tabs. */
75
+ .tableFilter[data-overflow='end'] {
76
+ -webkit-mask-image: linear-gradient(
77
+ to right,
78
+ #000 calc(100% - 2.5rem),
79
+ transparent
80
+ );
81
+ mask-image: linear-gradient(
82
+ to right,
83
+ #000 calc(100% - 2.5rem),
84
+ transparent
85
+ );
86
+ }
87
+
88
+ .tableFilter[data-overflow='start'] {
89
+ -webkit-mask-image: linear-gradient(
90
+ to right,
91
+ transparent,
92
+ #000 2.5rem
93
+ );
94
+ mask-image: linear-gradient(to right, transparent, #000 2.5rem);
95
+ }
96
+
97
+ .tableFilter[data-overflow='both'] {
98
+ -webkit-mask-image: linear-gradient(
99
+ to right,
100
+ transparent,
101
+ #000 2.5rem,
102
+ #000 calc(100% - 2.5rem),
103
+ transparent
104
+ );
105
+ mask-image: linear-gradient(
106
+ to right,
107
+ transparent,
108
+ #000 2.5rem,
109
+ #000 calc(100% - 2.5rem),
110
+ transparent
111
+ );
112
+ }
113
+
65
114
  .tableFilter > li {
66
115
  flex: 0 0 auto;
67
116
  margin: 0;
@@ -70,13 +119,20 @@
70
119
 
71
120
  /* Inactive tabs are muted labels, not tinted blocks. `white-space: nowrap`
72
121
  is what keeps the row one row: without it a two-word tab wraps and the
73
- strip grows a second line instead of overflowing. */
122
+ strip grows a second line instead of overflowing.
123
+
124
+ Flex rather than block, with a floor on the height: the padding alone
125
+ gave a ~42px target for a one-line label and a taller one for anything
126
+ that wrapped, and a control people click all day should have one height
127
+ whatever is written in it. */
74
128
  .tableFilter > li > .link,
75
129
  .tableFilter > li > .parentLink,
76
130
  .tableFilter > li > button {
77
- display: block;
131
+ display: flex;
132
+ align-items: center;
133
+ min-height: var(--tab-strip-height, 40px);
78
134
  white-space: nowrap;
79
- padding: 0.75rem var(--spacing-md, 1rem);
135
+ padding: 0.5rem var(--spacing-md, 1rem);
80
136
  border: none;
81
137
  border-bottom: 3px solid transparent;
82
138
  border-radius: 0;
@@ -85,32 +141,68 @@
85
141
  font-size: var(--font-size-sm, 0.8125rem);
86
142
  font-weight: var(--font-weight-medium, 500);
87
143
  cursor: pointer;
144
+ transition:
145
+ color var(--speed, 0.18s) var(--ease, ease),
146
+ border-color var(--speed, 0.18s) var(--ease, ease);
88
147
  }
89
148
 
90
- /* The selected tab. `--primary-color` first because it is what the rail
91
- already fills the active item with everywhere else; `--accent-color`
92
- covers apps that only declare that one, and the literal keeps the edge
93
- visible in an app that declares neither. */
149
+ /* The selected tab. `--accent-color` first, because that is the variable
150
+ the vertical rail's own selected edge reads (`.subactive` below) and the
151
+ strip is the same marker turned on its side — an app that retunes the
152
+ accent should not have to discover it applies to one of the two.
153
+ `--primary-color` covers apps that only declare that one, and the
154
+ literal keeps the edge visible in an app that declares neither. */
94
155
  .tableFilter > li > .subactive,
95
156
  .tableFilter > li.activetab > button {
96
157
  background: none !important;
97
158
  color: var(--heading-color, #111) !important;
98
159
  font-weight: var(--font-weight-semibold, 600);
99
160
  border-bottom-color: var(
100
- --primary-color,
101
- var(--accent-color, #0076c0)
161
+ --accent-color,
162
+ var(--primary-color, #0076c0)
102
163
  );
164
+ /* The strip marks its selection with the bottom edge. The vertical
165
+ rail's left edge would read as a stray tick on a horizontal row, and
166
+ its compensating padding would knock the tab off the strip's
167
+ rhythm. */
168
+ border-left: none;
169
+ padding-left: var(--spacing-md, 1rem);
103
170
  }
104
171
 
105
172
  /* Touch devices keep :hover stuck on the last tap, which on the old rule
106
173
  tinted whichever tab was tapped last and made it compete with the
107
- selected one. */
174
+ selected one. An unselected tab under the cursor darkens its ink and
175
+ shows a faint edge — enough to read as reachable, not enough to be
176
+ mistaken for the selection. */
108
177
  .tableFilter > li > .link:hover,
109
178
  .tableFilter > li > .parentLink:hover,
110
179
  .tableFilter > li > button:hover {
111
180
  background: none !important;
112
181
  color: var(--heading-color, #111) !important;
113
182
  }
183
+
184
+ .tableFilter > li > .link:not(.subactive):hover,
185
+ .tableFilter > li:not(.activetab) > button:hover {
186
+ border-bottom-color: color-mix(
187
+ in srgb,
188
+ var(--accent-color, var(--primary-color, #0076c0)) 35%,
189
+ transparent
190
+ );
191
+ }
192
+
193
+ /* The ring has to sit INSIDE the tab: the strip is `overflow-y: hidden`,
194
+ so the outset `--focus-ring` the base `.link` rule uses is clipped off
195
+ at the top and bottom here and the keyboard user sees two stray
196
+ vertical ticks. Written out rather than reusing the token, because the
197
+ token is an outset shadow and `inset var(--focus-ring)` would break for
198
+ any app that declares it as anything other than one plain shadow. */
199
+ .tableFilter > li > .link:focus-visible,
200
+ .tableFilter > li > .parentLink:focus-visible,
201
+ .tableFilter > li > button:focus-visible {
202
+ outline: none;
203
+ box-shadow: inset 0 0 0 2px
204
+ var(--accent-color, var(--primary-color, #0076c0));
205
+ }
114
206
  }
115
207
 
116
208
  /* The opt-in variant. Same strip, at every width; TableFilter sets this class
@@ -245,10 +337,29 @@
245
337
  rather than as decoration. */
246
338
  .subactive {
247
339
  font-weight: var(--font-weight-semibold, 600);
248
- background: var(--primary-color, #000) !important;
249
- /* Was --tertiary-color, which several projects set to their page
250
- off-white; white is what sits on this blue elsewhere. */
251
- color: #fff !important;
340
+ /* 2026: was a fully saturated fill of the brand colour with white type —
341
+ a solid block shouting at the same volume as the data it filters, and
342
+ the single loudest thing on an index page. A tint plus the brand ink
343
+ marks the selection just as unambiguously (still exactly one item) at a
344
+ fraction of the weight.
345
+
346
+ The tint alone, though, was measured at 10% opacity against a white
347
+ panel, which is a ~1.02:1 difference from its neighbours — legible if
348
+ you are looking for it and invisible if you are not. So identity is
349
+ carried three ways now, none of them colour alone: the accent edge, a
350
+ firmer tint, and the weight. */
351
+ position: relative;
352
+ background: color-mix(
353
+ in srgb,
354
+ var(--primary-color, #000) 12%,
355
+ transparent
356
+ ) !important;
357
+ color: var(--primary-color, #000) !important;
358
+ /* The edge is a border rather than a pseudo-element so it cannot be
359
+ clipped by the rail's own `overflow-y: auto`. The matching negative
360
+ padding keeps the label from shifting 3px when it becomes selected. */
361
+ border-left: 3px solid var(--accent-color, var(--primary-color, #000));
362
+ padding-left: calc(var(--spacing-md, 1rem) - 3px);
252
363
  }
253
364
 
254
365
  .subactivechildren {
@@ -0,0 +1,497 @@
1
+ /**
2
+ * The support conversation.
3
+ *
4
+ * The three entry kinds are distinguished by an accent edge, a tint AND a
5
+ * word — never by colour alone, because the one mistake this view must make
6
+ * impossible is an agent reading an internal note as something the customer
7
+ * has already seen.
8
+ *
9
+ * Literal fallbacks on every token: a bare var() for a token the host has not
10
+ * declared is invalid at computed-value time and discards its declaration.
11
+ */
12
+
13
+ .wrap {
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: 1.25rem;
17
+ }
18
+
19
+ /* -- header ---------------------------------------------------------------- */
20
+
21
+ .head {
22
+ display: grid;
23
+ grid-template-columns: minmax(0, 1fr) auto;
24
+ gap: 0.75rem 1.5rem;
25
+ padding-bottom: 1rem;
26
+ border-bottom: 1px solid var(--border-color, rgba(43, 43, 43, 0.12));
27
+ }
28
+
29
+ .headMain {
30
+ grid-column: 1 / -1;
31
+
32
+ h3 {
33
+ margin: 0.2rem 0 0;
34
+ color: var(--heading-color, #111827);
35
+ font-size: 1.05rem;
36
+ font-weight: var(--font-weight-semibold, 600);
37
+ line-height: 1.35;
38
+ }
39
+ }
40
+
41
+ .reference {
42
+ display: flex;
43
+ gap: 0.5rem;
44
+ align-items: center;
45
+ margin: 0;
46
+ color: var(--muted-color, #6b7280);
47
+ font-size: 0.78rem;
48
+ font-weight: var(--font-weight-semibold, 600);
49
+ font-variant-numeric: tabular-nums;
50
+ }
51
+
52
+ .status {
53
+ padding: 0.1rem 0.5rem;
54
+ border-radius: 999px;
55
+ font-size: 0.7rem;
56
+ letter-spacing: 0.02em;
57
+ }
58
+
59
+ .open {
60
+ background: color-mix(in srgb, var(--highlight-color, #3cbf7d) 20%, transparent);
61
+ color: #0f6b41;
62
+ }
63
+
64
+ .closed {
65
+ background: rgba(43, 43, 43, 0.09);
66
+ color: var(--muted-color, #6b7280);
67
+ }
68
+
69
+ .meta {
70
+ display: flex;
71
+ flex-wrap: wrap;
72
+ gap: 0.35rem 1.75rem;
73
+ margin: 0;
74
+
75
+ dt {
76
+ color: var(--muted-color, #6b7280);
77
+ font-size: 0.68rem;
78
+ font-weight: var(--font-weight-semibold, 600);
79
+ letter-spacing: 0.06em;
80
+ text-transform: uppercase;
81
+ }
82
+
83
+ dd {
84
+ margin: 0.1rem 0 0;
85
+ color: var(--paragraph-color, #2b2b2b);
86
+ font-size: 0.83rem;
87
+ }
88
+ }
89
+
90
+ .channel {
91
+ display: inline-flex;
92
+ gap: 0.3rem;
93
+ align-items: center;
94
+ }
95
+
96
+ .source {
97
+ display: inline-flex;
98
+ gap: 0.3rem;
99
+ align-items: center;
100
+ align-self: start;
101
+ color: var(--muted-color, #6b7280);
102
+ font-size: 0.76rem;
103
+ text-decoration: none;
104
+ white-space: nowrap;
105
+
106
+ &:hover {
107
+ color: var(--primary-color, #1b3933);
108
+ }
109
+ }
110
+
111
+ /* -- thread ---------------------------------------------------------------- */
112
+
113
+ .thread {
114
+ display: flex;
115
+ flex-direction: column;
116
+ gap: 0.85rem;
117
+ margin: 0;
118
+ padding: 0;
119
+ list-style: none;
120
+ }
121
+
122
+ .entry {
123
+ padding: 0.85rem 1rem;
124
+ border: 1px solid var(--border-color, rgba(43, 43, 43, 0.12));
125
+ /* The accent edge carries the direction; the tag beside it says the same
126
+ thing in words. */
127
+ border-left-width: 3px;
128
+ border-radius: var(--btn-br, 5px);
129
+ background: var(--surface-color, #fff);
130
+ }
131
+
132
+ /* From the customer. The plainest surface — most of a thread is these, and
133
+ they should read as the baseline. */
134
+ .entryIn {
135
+ border-left-color: var(--border-color, rgba(43, 43, 43, 0.25));
136
+ }
137
+
138
+ /* Our reply. Tinted so a scan down a long thread shows who answered when. */
139
+ .entryOut {
140
+ border-left-color: var(--primary-color, #1b3933);
141
+ background: color-mix(in srgb, var(--primary-color, #1b3933) 4%, var(--surface-color, #fff));
142
+ }
143
+
144
+ /* An internal note. Deliberately the loudest of the three. */
145
+ .entryNote {
146
+ border-left-color: #b7791f;
147
+ background: rgba(183, 121, 31, 0.07);
148
+ border-style: dashed;
149
+ border-left-style: solid;
150
+ }
151
+
152
+ .entryHead {
153
+ display: flex;
154
+ flex-wrap: wrap;
155
+ gap: 0.45rem;
156
+ align-items: center;
157
+ margin-bottom: 0.45rem;
158
+ }
159
+
160
+ .entryIcon {
161
+ display: inline-flex;
162
+ align-items: center;
163
+ color: var(--muted-color, #6b7280);
164
+ }
165
+
166
+ .author {
167
+ color: var(--heading-color, #111827);
168
+ font-size: 0.84rem;
169
+ font-weight: var(--font-weight-semibold, 600);
170
+ }
171
+
172
+ .tag,
173
+ .privateTag,
174
+ .firstTag,
175
+ .attachTag {
176
+ display: inline-flex;
177
+ gap: 0.25rem;
178
+ align-items: center;
179
+ padding: 0.08rem 0.45rem;
180
+ border-radius: 999px;
181
+ font-size: 0.68rem;
182
+ font-weight: var(--font-weight-semibold, 600);
183
+ }
184
+
185
+ .tag {
186
+ background: rgba(43, 43, 43, 0.07);
187
+ color: var(--muted-color, #6b7280);
188
+ }
189
+
190
+ .privateTag {
191
+ background: rgba(183, 121, 31, 0.18);
192
+ color: #8a5a00;
193
+ }
194
+
195
+ .firstTag {
196
+ background: color-mix(in srgb, var(--primary-color, #1b3933) 12%, transparent);
197
+ color: var(--primary-color, #1b3933);
198
+ }
199
+
200
+ .attachTag {
201
+ background: rgba(43, 43, 43, 0.07);
202
+ color: var(--paragraph-color, #374151);
203
+ }
204
+
205
+ .when {
206
+ margin-left: auto;
207
+ color: var(--muted-color, #6b7280);
208
+ font-size: 0.72rem;
209
+ font-variant-numeric: tabular-nums;
210
+ white-space: nowrap;
211
+ }
212
+
213
+ .body {
214
+ margin: 0;
215
+ color: var(--paragraph-color, #2b2b2b);
216
+ font-size: 0.85rem;
217
+ line-height: 1.55;
218
+ /* Email bodies arrive with their own newlines and no markup; preserving
219
+ them is the difference between a readable message and a wall. */
220
+ white-space: pre-wrap;
221
+ overflow-wrap: anywhere;
222
+
223
+ /* A long email (forwarded chains, ticket-system footers) must not turn
224
+ the conversation into a mile of scrolling — the message scrolls inside
225
+ its own card instead. */
226
+ max-height: 24rem;
227
+ overflow-y: auto;
228
+ }
229
+
230
+ /* -- rendered email -------------------------------------------------------- */
231
+
232
+ /**
233
+ * The sanitised HTML of a message.
234
+ *
235
+ * The markup is already safe by the time it reaches here — an allowlist ran on
236
+ * the server, and script, style, iframe, form and every event handler are gone.
237
+ * What is left is a STYLING problem: an email carries its own inline CSS,
238
+ * written for a full-width mail client, and dropped into a card it will happily
239
+ * set a 900px table, a 24pt font and a white background on a dark surface.
240
+ *
241
+ * So this block is containment, not sanitising. The message may lay itself out
242
+ * however it likes inside these bounds and may not affect anything outside
243
+ * them.
244
+ */
245
+ .bodyHtml {
246
+ /* Same cap as the plain-text body: one long forwarded chain must not turn
247
+ the thread into a mile of scrolling. */
248
+ max-height: 24rem;
249
+ overflow: auto;
250
+
251
+ /* Layout, paint and style containment. The message cannot draw outside
252
+ this box, cannot reflow the card around it, and counters/quotes it
253
+ declares do not leak into the rest of the conversation. */
254
+ contain: content;
255
+
256
+ color: var(--paragraph-color, #2b2b2b);
257
+ font-size: 0.85rem;
258
+ line-height: 1.55;
259
+ overflow-wrap: anywhere;
260
+
261
+ /* Nothing in a message is wider than the card it sits in. This is the one
262
+ rule that keeps a 900px signature table from stretching the thread. */
263
+ * {
264
+ max-width: 100%;
265
+ }
266
+
267
+ p,
268
+ div,
269
+ ul,
270
+ ol,
271
+ blockquote,
272
+ table,
273
+ pre,
274
+ h1, h2, h3, h4, h5, h6 {
275
+ margin: 0 0 0.55rem;
276
+ }
277
+
278
+ > *:last-child {
279
+ margin-bottom: 0;
280
+ }
281
+
282
+ /* Email headings are written for a full-width client; at card width they
283
+ arrive as shouting. Capped, but still ranked. */
284
+ h1, h2 { font-size: 1rem; font-weight: 600; }
285
+ h3, h4 { font-size: 0.92rem; font-weight: 600; }
286
+ h5, h6 { font-size: 0.85rem; font-weight: 600; }
287
+
288
+ ul,
289
+ ol {
290
+ padding-left: 1.3rem;
291
+ }
292
+
293
+ a {
294
+ color: var(--primary-color, #1b3933);
295
+ text-decoration: underline;
296
+ }
297
+
298
+ img {
299
+ height: auto;
300
+ /* An inline `height="600"` on an image we have capped to the card
301
+ width would otherwise stretch it. */
302
+ max-width: 100%;
303
+ }
304
+
305
+ /* The quoted history under a reply. Marked rather than hidden — an agent
306
+ needs to see what was quoted, and needs to see that it IS a quote. */
307
+ blockquote {
308
+ margin-left: 0;
309
+ padding-left: 0.75rem;
310
+ border-left: 2px solid var(--border-color, rgba(43, 43, 43, 0.18));
311
+ color: var(--muted-color, #6b7280);
312
+ }
313
+
314
+ table {
315
+ border-collapse: collapse;
316
+ }
317
+
318
+ td,
319
+ th {
320
+ padding: 0.15rem 0.4rem;
321
+ vertical-align: top;
322
+ }
323
+
324
+ pre {
325
+ white-space: pre-wrap;
326
+ overflow-x: auto;
327
+ }
328
+
329
+ hr {
330
+ margin: 0.7rem 0;
331
+ border: 0;
332
+ border-top: 1px solid var(--border-color, rgba(43, 43, 43, 0.14));
333
+ }
334
+ }
335
+
336
+ /* A remote image that has not been unblocked. Deliberately visible: an image
337
+ that silently disappears reads as a broken message, and the reader would
338
+ never know there is a button that brings it back. */
339
+ .blockedImage {
340
+ display: inline-flex;
341
+ align-items: center;
342
+ max-width: 100%;
343
+ padding: 0.05rem 0.4rem;
344
+ border: 1px dashed var(--border-color, rgba(43, 43, 43, 0.3));
345
+ border-radius: 3px;
346
+ color: var(--muted-color, #6b7280);
347
+ font-size: 0.72rem;
348
+ vertical-align: middle;
349
+ }
350
+
351
+ .bodyBar {
352
+ display: flex;
353
+ flex-wrap: wrap;
354
+ gap: 0.35rem 0.6rem;
355
+ align-items: center;
356
+ margin-bottom: 0.4rem;
357
+ }
358
+
359
+ .bodyAction {
360
+ display: inline-flex;
361
+ gap: 0.25rem;
362
+ align-items: center;
363
+ padding: 0.1rem 0.45rem;
364
+ border: 1px solid var(--border-color, rgba(43, 43, 43, 0.18));
365
+ border-radius: 999px;
366
+ background: transparent;
367
+ color: var(--muted-color, #6b7280);
368
+ font-family: inherit;
369
+ font-size: 0.68rem;
370
+ font-weight: var(--font-weight-semibold, 600);
371
+ cursor: pointer;
372
+
373
+ &:hover {
374
+ border-color: var(--primary-color, #1b3933);
375
+ color: var(--primary-color, #1b3933);
376
+ }
377
+ }
378
+
379
+ /* The reason, in words, next to the button. "Show images" alone does not tell
380
+ an agent why they were hidden, and the why is the part that matters. */
381
+ .bodyHint {
382
+ color: var(--muted-color, #6b7280);
383
+ font-size: 0.68rem;
384
+ }
385
+
386
+ .state {
387
+ display: flex;
388
+ gap: 0.5rem;
389
+ align-items: center;
390
+ padding: 1.5rem 0;
391
+ color: var(--muted-color, #6b7280);
392
+ font-size: 0.85rem;
393
+ }
394
+
395
+ .spin { animation: ticketSpin 0.9s linear infinite; }
396
+
397
+ @keyframes ticketSpin {
398
+ to { transform: rotate(360deg); }
399
+ }
400
+
401
+ @media (prefers-reduced-motion: reduce) {
402
+ .spin { animation: none; }
403
+ }
404
+
405
+ @media (max-width: 759px) {
406
+ .head { grid-template-columns: minmax(0, 1fr); }
407
+ .when { margin-left: 0; width: 100%; }
408
+ }
409
+
410
+ /* -- composer -------------------------------------------------------------- */
411
+
412
+ .composer {
413
+ padding: 0.85rem 1rem;
414
+ border: 1px solid var(--border-color, rgba(43, 43, 43, 0.14));
415
+ border-radius: var(--btn-br, 5px);
416
+ background: var(--surface-color, #fff);
417
+ }
418
+
419
+ /* An internal note looks like an internal note BEFORE it is sent, not after.
420
+ The composer takes on the note colour the moment the switch is flipped, so
421
+ the state is visible in peripheral vision while typing. */
422
+ .composerNote {
423
+ border-color: rgba(183, 121, 31, 0.5);
424
+ background: rgba(183, 121, 31, 0.06);
425
+ }
426
+
427
+ .composerBar {
428
+ display: flex;
429
+ flex-wrap: wrap;
430
+ gap: 0.5rem 0.9rem;
431
+ align-items: center;
432
+ margin-bottom: 0.6rem;
433
+ }
434
+
435
+ .switch {
436
+ display: inline-flex;
437
+ gap: 0.4rem;
438
+ align-items: center;
439
+ color: var(--paragraph-color, #374151);
440
+ font-size: 0.8rem;
441
+ font-weight: var(--font-weight-semibold, 600);
442
+ cursor: pointer;
443
+ }
444
+
445
+ /* The consequence in words, always present. */
446
+ .consequence {
447
+ color: var(--muted-color, #6b7280);
448
+ font-size: 0.76rem;
449
+ }
450
+
451
+ .composerInput {
452
+ width: 100%;
453
+ padding: 0.6rem 0.7rem;
454
+ border: 1px solid var(--border-color, rgba(43, 43, 43, 0.15));
455
+ border-radius: var(--btn-br, 5px);
456
+ background: var(--background-color, #fff);
457
+ color: var(--paragraph-color, #2b2b2b);
458
+ font: inherit;
459
+ font-size: 0.85rem;
460
+ line-height: 1.5;
461
+ resize: vertical;
462
+
463
+ &:focus {
464
+ outline: none;
465
+ border-color: color-mix(
466
+ in srgb,
467
+ var(--primary-color, #1b3933) 45%,
468
+ var(--border-color, rgba(43, 43, 43, 0.15))
469
+ );
470
+ box-shadow: 0 0 0 3px
471
+ color-mix(in srgb, var(--highlight-color, #3cbf7d) 22%, transparent);
472
+ }
473
+ }
474
+
475
+ .composerActions {
476
+ display: flex;
477
+ justify-content: flex-end;
478
+ margin-top: 0.6rem;
479
+ }
480
+
481
+ .send {
482
+ height: 34px;
483
+ padding: 0 1.1rem;
484
+ border: 1px solid var(--primary-color, #1b3933);
485
+ border-radius: var(--btn-br, 5px);
486
+ background: var(--primary-color, #1b3933);
487
+ color: #fff;
488
+ font-family: inherit;
489
+ font-size: 0.8rem;
490
+ font-weight: var(--font-weight-semibold, 600);
491
+ cursor: pointer;
492
+
493
+ &:disabled {
494
+ opacity: 0.5;
495
+ cursor: not-allowed;
496
+ }
497
+ }