@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.
- package/package.json +10 -7
- package/src/components/AssociationManager.jsx +344 -177
- package/src/components/AsyncSelect.jsx +2 -2
- package/src/components/Autocomplete.jsx +57 -12
- package/src/components/Breadcrumb.jsx +23 -4
- package/src/components/ClientAssociationManager.jsx +321 -145
- package/src/components/DataGrid.jsx +625 -20
- package/src/components/DropZone.jsx +4 -1
- package/src/components/Form.jsx +233 -6
- package/src/components/MultiSelect.jsx +2 -2
- package/src/components/Navigation.jsx +39 -23
- package/src/components/TableFilter.jsx +172 -12
- package/src/components/UniversalSearch.jsx +390 -0
- package/src/components/auth/Login.jsx +164 -8
- package/src/components/auth/Profile.jsx +209 -22
- package/src/components/auth/authEndpoints.js +4 -0
- package/src/components/auth/webauthn.js +194 -0
- package/src/components/columns/ColumnRenderers.jsx +6 -1
- package/src/components/controls/DataGridSearch.jsx +11 -5
- package/src/components/generic/GenericAuth.jsx +6 -0
- package/src/components/generic/GenericDashboard.jsx +93 -36
- package/src/components/generic/GenericDetail.jsx +146 -61
- package/src/components/generic/GenericIndex.jsx +134 -38
- package/src/components/generic/GenericSettings.jsx +220 -0
- package/src/components/generic/StandardModal.jsx +165 -8
- package/src/components/generic/dashboardChartDefaults.js +230 -0
- package/src/components/generic/shared/indexLayout.js +38 -0
- package/src/components/settings/IntegrationsManager.jsx +474 -0
- package/src/components/sms/SmsLineSettings.jsx +16 -12
- package/src/components/styles/AssociationManager.module.scss +4 -363
- package/src/components/styles/AuditLog.module.scss +6 -6
- package/src/components/styles/AuditLogs.module.scss +3 -3
- package/src/components/styles/Autocomplete.module.scss +7 -7
- package/src/components/styles/BusinessCardOcr.module.scss +6 -1
- package/src/components/styles/CallQueueSettings.module.scss +4 -1
- package/src/components/styles/CameraPlacement.module.scss +19 -19
- package/src/components/styles/CategorizedDropZone.module.scss +6 -1
- package/src/components/styles/ClientAssociationManager.module.scss +5 -289
- package/src/components/styles/ClientAuth.module.scss +1 -1
- package/src/components/styles/DataGrid.module.scss +455 -8
- package/src/components/styles/Field.module.scss +32 -24
- package/src/components/styles/Form.module.scss +123 -3
- package/src/components/styles/GenericDashboard.module.scss +612 -825
- package/src/components/styles/GenericDetail.module.scss +406 -61
- package/src/components/styles/GenericDynamic.module.scss +25 -25
- package/src/components/styles/GenericEditableTable.module.scss +33 -33
- package/src/components/styles/GenericFormBuilder.module.scss +119 -119
- package/src/components/styles/GenericIndex.module.scss +483 -38
- package/src/components/styles/GenericMain.module.scss +6 -3
- package/src/components/styles/GenericQuote.module.scss +399 -81
- package/src/components/styles/GenericReport.module.scss +8 -8
- package/src/components/styles/GenericSettings.module.scss +209 -0
- package/src/components/styles/GenericSort.module.scss +9 -9
- package/src/components/styles/Integrations.module.scss +321 -0
- package/src/components/styles/List.module.scss +4 -4
- package/src/components/styles/Login.module.scss +57 -0
- package/src/components/styles/MultiSelect.module.scss +4 -4
- package/src/components/styles/Navigation.module.scss +12 -12
- package/src/components/styles/Notification.module.scss +12 -8
- package/src/components/styles/NotificationList.module.scss +5 -5
- package/src/components/styles/Profile.module.scss +5 -5
- package/src/components/styles/QrCode.module.scss +4 -4
- package/src/components/styles/QuickAction.module.scss +20 -20
- package/src/components/styles/SortableQuoteItems.module.scss +4 -4
- package/src/components/styles/TableFilter.module.scss +128 -17
- package/src/components/styles/TicketConversation.module.scss +497 -0
- package/src/components/styles/UniversalSearch.module.scss +229 -0
- package/src/components/styles/Vault.module.scss +413 -0
- package/src/components/styles/_associationManager.scss +467 -0
- package/src/components/styles/_authBrandPanel.scss +15 -9
- package/src/components/styles/_controls.scss +43 -39
- package/src/components/styles/global-datagrid.css +196 -0
- package/src/components/styles/global.css +168 -17
- package/src/components/tickets/TicketConversation.jsx +455 -0
- package/src/components/utils/rowActionSettings.js +117 -0
- package/src/components/vault/VaultClientPicker.jsx +183 -0
- package/src/components/vault/VaultEntryForm.jsx +23 -0
- package/src/components/vault/VaultManager.jsx +382 -34
- package/src/components/vault/VaultShareModal.jsx +454 -0
- package/src/components/vault/vaultEndpoints.js +16 -2
- package/src/components/vault/vaultShares.js +247 -0
- package/src/index.js +35 -0
- 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
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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:
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
min-height: var(--tab-strip-height, 40px);
|
|
78
134
|
white-space: nowrap;
|
|
79
|
-
padding: 0.
|
|
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. `--
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
--
|
|
101
|
-
var(--
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
}
|