@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,369 @@
1
+ /**
2
+ * The sign-in design language, as a mixin.
3
+ *
4
+ * Login.module.scss is the original: an asymmetric two-pane split, the brand
5
+ * photograph on the left and the task on the right, everything driven off the
6
+ * consuming app's token layer with neutral fallbacks. Before 6.7.0 it was the
7
+ * only screen in the journey that looked like that — TwoFactorAuth, Reset and
8
+ * Verify were still the pre-6.1 centred `.lcontainer / .lwrap / .login` card,
9
+ * so signing in with 2FA meant crossing a visible design seam halfway through.
10
+ *
11
+ * The rules live here rather than in one shared module because these are CSS
12
+ * modules: each screen needs its own scoped copy of the class names. Every
13
+ * screen `@use`s this file and `@include auth-shell;` at the top of its own
14
+ * module, then adds only what is particular to it.
15
+ *
16
+ * Anything changed here must be mirrored in Login.module.scss, which keeps its
17
+ * own copy so the login screen's rendering cannot be disturbed by edits made
18
+ * for another screen.
19
+ */
20
+
21
+ @use './authBrandPanel' as brand;
22
+
23
+ @mixin auth-shell {
24
+ /* ---------------------------------------------------------------------- */
25
+ /* frame */
26
+ /* ---------------------------------------------------------------------- */
27
+
28
+ .auth {
29
+ /* Local aliases only — every brand value comes from the consuming app's
30
+ token layer so this library stays reusable. The fallbacks are neutral
31
+ on purpose: a project that defines nothing gets a plain, working
32
+ screen rather than any one project's colours. Identical to the block
33
+ at the top of Login.module.scss. */
34
+ --ink: var(--header-color, #10192a);
35
+ --navy: var(--primary-color, #26364a);
36
+ --navy-deep: var(--primary-color-darker, #16202e);
37
+ --flag: var(--accent-color, #8a8f98);
38
+ --paper: var(--surface-color, #ffffff);
39
+ --warm: var(--hover-color, #f4f4f2);
40
+ --line: var(--border-color, #e4e1dc);
41
+ --muted: var(--muted-color, #6b7688);
42
+
43
+ position: fixed;
44
+ inset: 0;
45
+ display: grid;
46
+ grid-template-columns: 52fr 48fr;
47
+ background: var(--paper);
48
+ /* Read from a token first so an app can put its own face on the whole
49
+ journey; 'Barlow' is imported by the screens themselves. */
50
+ font-family: var(--auth-font-family, 'Barlow', system-ui, sans-serif);
51
+ color: var(--ink);
52
+
53
+ @media (max-width: 900px) {
54
+ grid-template-columns: 1fr;
55
+ }
56
+ }
57
+
58
+ // ---------------------------------------------------------------------
59
+ // brand panel — shared with the staff Login and the client split layout.
60
+ // See _authBrandPanel.scss; `hide` is the staff screens' collapse.
61
+ // ---------------------------------------------------------------------
62
+
63
+ @include brand.auth-brand-panel(hide);
64
+
65
+ /* ---------------------------------------------------------------------- */
66
+ /* form panel */
67
+ /* ---------------------------------------------------------------------- */
68
+
69
+ .panel {
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ padding: 2rem clamp(1.5rem, 5vw, 4.5rem);
74
+ background: var(--paper);
75
+ overflow-y: auto;
76
+
77
+ @media (max-width: 900px) {
78
+ background: linear-gradient(var(--warm), var(--paper) 22rem);
79
+ }
80
+ }
81
+
82
+ .panelInner {
83
+ width: 100%;
84
+ max-width: 24rem;
85
+ text-align: left;
86
+ animation: rise 0.55s cubic-bezier(0.2, 0.7, 0.3, 1) both;
87
+ animation-delay: 0.08s;
88
+ }
89
+
90
+ .logo {
91
+ display: block;
92
+ width: auto;
93
+ height: 3.1rem;
94
+ margin-bottom: 2.4rem;
95
+ }
96
+
97
+ .title {
98
+ margin: 0 0 0.35rem;
99
+ font-size: 1.9rem;
100
+ font-weight: 700;
101
+ letter-spacing: -0.02em;
102
+ line-height: 1.1;
103
+ }
104
+
105
+ .subtitle {
106
+ margin: 0 0 2rem;
107
+ font-size: 0.95rem;
108
+ font-weight: 400;
109
+ color: var(--muted);
110
+ }
111
+
112
+ .form {
113
+ display: flex;
114
+ flex-direction: column;
115
+ gap: 0.9rem;
116
+ }
117
+
118
+ /* Floating label: the label sits in the field until it holds something, so
119
+ the field is self-describing without a separate line above it. */
120
+ .field {
121
+ position: relative;
122
+
123
+ input {
124
+ width: 100%;
125
+ height: 3.35rem;
126
+ padding: 1.15rem 0.9rem 0.4rem;
127
+ border: 1px solid var(--line);
128
+ border-radius: 0.6rem;
129
+ background: var(--paper);
130
+ font-family: inherit;
131
+ font-size: 1rem;
132
+ color: var(--ink);
133
+ transition:
134
+ border-color 0.15s ease,
135
+ box-shadow 0.15s ease;
136
+
137
+ &:hover {
138
+ border-color: #c9c5be;
139
+ }
140
+
141
+ &:focus {
142
+ outline: none;
143
+ border-color: var(--navy);
144
+ box-shadow: 0 0 0 3px
145
+ var(--focus-ring-soft, rgba(38, 54, 74, 0.16));
146
+ }
147
+
148
+ &:disabled {
149
+ background: var(--warm);
150
+ color: var(--muted);
151
+ cursor: default;
152
+ }
153
+ }
154
+
155
+ label {
156
+ position: absolute;
157
+ left: 0.95rem;
158
+ top: 1.05rem;
159
+ font-size: 1rem;
160
+ font-weight: 400;
161
+ color: var(--muted);
162
+ pointer-events: none;
163
+ transition:
164
+ transform 0.14s ease,
165
+ font-size 0.14s ease,
166
+ color 0.14s ease;
167
+ transform-origin: 0 0;
168
+ }
169
+
170
+ /* :not(:placeholder-shown) is why every input carries placeholder=" ". */
171
+ input:focus + label,
172
+ input:not(:placeholder-shown) + label {
173
+ transform: translateY(-0.72rem);
174
+ font-size: 0.72rem;
175
+ font-weight: 600;
176
+ letter-spacing: 0.02em;
177
+ color: var(--navy);
178
+ }
179
+
180
+ &:has(.reveal) input {
181
+ padding-right: 3rem;
182
+ }
183
+ }
184
+
185
+ /* Set imperatively by the submit handlers on a failed attempt, so it is a
186
+ plain global class rather than a module one. */
187
+ :global(.inputError) {
188
+ border-color: var(--danger-color, #b3261e) !important;
189
+ box-shadow: 0 0 0 3px var(--danger-ring, rgba(179, 38, 30, 0.14)) !important;
190
+ }
191
+
192
+ .reveal {
193
+ position: absolute;
194
+ right: 0.55rem;
195
+ top: 50%;
196
+ transform: translateY(-50%);
197
+ display: flex;
198
+ align-items: center;
199
+ justify-content: center;
200
+ width: 2.1rem;
201
+ height: 2.1rem;
202
+ border: none;
203
+ border-radius: 0.4rem;
204
+ background: none;
205
+ color: var(--muted);
206
+ cursor: pointer;
207
+
208
+ &:hover {
209
+ background: var(--warm);
210
+ color: var(--ink);
211
+ }
212
+ }
213
+
214
+ .row {
215
+ display: flex;
216
+ align-items: center;
217
+ justify-content: space-between;
218
+ gap: 1rem;
219
+ margin: 0.1rem 0 0.5rem;
220
+ }
221
+
222
+ .link {
223
+ font-size: 0.88rem;
224
+ font-weight: 600;
225
+ color: var(--navy);
226
+ text-decoration: none;
227
+ background: none;
228
+ border: none;
229
+ padding: 0;
230
+ font-family: inherit;
231
+ cursor: pointer;
232
+
233
+ &:hover {
234
+ text-decoration: underline;
235
+ }
236
+
237
+ &:disabled {
238
+ color: var(--muted);
239
+ cursor: default;
240
+ text-decoration: none;
241
+ }
242
+ }
243
+
244
+ /* Primary on these screens. Unlike Login — which keeps its email button
245
+ outlined because SSO is the filled action above it — verifying a code or
246
+ setting a password is the only thing the screen asks for, so it carries
247
+ the fill. */
248
+ .submit {
249
+ width: 100%;
250
+ height: 3.1rem;
251
+ border: 1px solid var(--navy);
252
+ border-radius: 0.6rem;
253
+ background: var(--navy);
254
+ color: #fff;
255
+ font-family: inherit;
256
+ font-size: 0.95rem;
257
+ font-weight: 600;
258
+ letter-spacing: 0.01em;
259
+ cursor: pointer;
260
+ transition:
261
+ border-color 0.15s ease,
262
+ background 0.15s ease,
263
+ transform 0.08s ease;
264
+
265
+ &:hover:not(:disabled) {
266
+ background: var(--navy-deep);
267
+ border-color: var(--navy-deep);
268
+ }
269
+
270
+ &:active:not(:disabled) {
271
+ transform: translateY(1px);
272
+ }
273
+
274
+ &:focus-visible {
275
+ outline: 3px solid var(--focus-ring-strong, rgba(38, 54, 74, 0.3));
276
+ outline-offset: 2px;
277
+ }
278
+
279
+ &:disabled {
280
+ opacity: 0.65;
281
+ cursor: default;
282
+ }
283
+ }
284
+
285
+ /* A quiet way to a secondary path. Not a link — it performs an action on
286
+ this page rather than navigating anywhere. */
287
+ .altToggle {
288
+ display: block;
289
+ width: 100%;
290
+ margin-top: 1rem;
291
+ padding: 0.6rem;
292
+ border: none;
293
+ border-radius: 0.5rem;
294
+ background: none;
295
+ font-family: inherit;
296
+ font-size: 0.9rem;
297
+ font-weight: 600;
298
+ color: var(--navy);
299
+ cursor: pointer;
300
+
301
+ &:hover {
302
+ background: var(--warm);
303
+ }
304
+
305
+ &:focus-visible {
306
+ outline: 3px solid var(--focus-ring-strong, rgba(38, 54, 74, 0.3));
307
+ outline-offset: 2px;
308
+ }
309
+
310
+ &:disabled {
311
+ color: var(--muted);
312
+ cursor: default;
313
+
314
+ &:hover {
315
+ background: none;
316
+ }
317
+ }
318
+ }
319
+
320
+ .divider {
321
+ display: flex;
322
+ align-items: center;
323
+ gap: 0.9rem;
324
+ margin: 1.4rem 0;
325
+ color: var(--muted);
326
+ font-size: 0.78rem;
327
+ letter-spacing: 0.08em;
328
+ text-transform: uppercase;
329
+
330
+ &::before,
331
+ &::after {
332
+ content: '';
333
+ flex: 1;
334
+ height: 1px;
335
+ background: var(--line);
336
+ }
337
+ }
338
+
339
+ /* A muted line under the form — expiry copy, "having trouble?", the sign
340
+ off. Never a control. */
341
+ .note {
342
+ margin: 1.4rem 0 0;
343
+ font-size: 0.82rem;
344
+ line-height: 1.5;
345
+ color: var(--muted);
346
+ text-align: left;
347
+ }
348
+
349
+ .foot {
350
+ margin: 2.5rem 0 0;
351
+ font-size: 0.78rem;
352
+ color: var(--muted);
353
+ }
354
+
355
+ @include brand.auth-rise-keyframes;
356
+
357
+ @media (prefers-reduced-motion: reduce) {
358
+ .brandInk,
359
+ .panelInner {
360
+ animation: none;
361
+ }
362
+
363
+ .submit,
364
+ .field input,
365
+ .field label {
366
+ transition: none;
367
+ }
368
+ }
369
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Build-time environment variables, whichever bundler compiled this.
3
+ *
4
+ * `import.meta.env` is Vite's interface, and two files in this library read it
5
+ * directly (Fetch.jsx for the payload ceiling, DataGrid.jsx for the asset
6
+ * path). That made the whole library Vite-only: under webpack — which is what
7
+ * the Next.js portal runs — `import.meta` survives the compile as a bare `{}`,
8
+ * so the property read threw a TypeError on the first request the app made.
9
+ * The portal needed a DefinePlugin shim to get past it.
10
+ *
11
+ * Both accesses here are guarded and both are wrapped, because the bundlers
12
+ * fail differently: one leaves `import.meta` undefined, the other leaves it an
13
+ * object with no `env`. Vite's behaviour is unchanged — its value is still
14
+ * found first and still wins.
15
+ *
16
+ * Pure module (no React, no DOM) so it can be unit-tested with `node --test`.
17
+ */
18
+
19
+ /**
20
+ * Read one variable, or `undefined`.
21
+ *
22
+ * Lookup order: Vite's `import.meta.env`, then `process.env` under the name as
23
+ * given, then `process.env` under a `NEXT_PUBLIC_` prefix — Next.js will only
24
+ * expose a variable to the browser under that prefix, so a `VITE_FOO` becomes
25
+ * `NEXT_PUBLIC_FOO` there.
26
+ */
27
+ export const readBuildEnv = (key) => {
28
+ try {
29
+ // eslint-disable-next-line no-undef
30
+ const meta = typeof import.meta === 'undefined' ? null : import.meta;
31
+ const value = meta && meta.env ? meta.env[key] : undefined;
32
+
33
+ if (value !== undefined) {
34
+ return value;
35
+ }
36
+ } catch (error) {
37
+ // No `import.meta` in this module format. Expected under a CommonJS
38
+ // output and under Jest; never worth reporting.
39
+ }
40
+
41
+ try {
42
+ if (typeof process !== 'undefined' && process.env) {
43
+ const direct = process.env[key];
44
+
45
+ if (direct !== undefined) {
46
+ return direct;
47
+ }
48
+
49
+ return process.env[
50
+ `NEXT_PUBLIC_${key.replace(/^(VITE_|REACT_APP_)/, '')}`
51
+ ];
52
+ }
53
+ } catch (error) {
54
+ // No `process` either. Fall through to the caller's own default.
55
+ }
56
+
57
+ return undefined;
58
+ };
59
+
60
+ /** `readBuildEnv` with a default for the (common) absent case. */
61
+ export const buildEnv = (key, fallback = '') => {
62
+ const value = readBuildEnv(key);
63
+
64
+ return value === undefined || value === null ? fallback : value;
65
+ };
66
+
67
+ export default readBuildEnv;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Small, pure display helpers shared by the generic detail page and the
3
+ * DataGrid column renderers.
4
+ *
5
+ * These live in their own module — with no React and no CSS import — so the
6
+ * rules they encode can be tested directly under `node --test` rather than
7
+ * inferred from a rendered tree. Every consumer of a `.jsx` in this repo has
8
+ * to bring a bundler with it; this file deliberately does not.
9
+ */
10
+
11
+ /**
12
+ * How many items are in a value that a column is counting?
13
+ *
14
+ * The grid's `arrayCount` column reads a JSON field straight off the API
15
+ * payload, and what actually arrives there varies by backend and by driver:
16
+ * a real array, a JSON *string* that was never decoded (a `json` cast the
17
+ * model forgot), an object keyed by id, or null for a record that has none.
18
+ * A bare `value.length` handles exactly the first of those and yields
19
+ * `undefined` — rendered as an empty cell — for the rest, which reads as
20
+ * "unknown" when the truthful answer is a number.
21
+ *
22
+ * @param {*} value raw field value
23
+ * @returns {number} item count; 0 when there is nothing countable
24
+ */
25
+ export function arrayCountFrom(value) {
26
+ if (Array.isArray(value)) {
27
+ return value.length;
28
+ }
29
+
30
+ if (typeof value === 'string') {
31
+ const trimmed = value.trim();
32
+
33
+ if (trimmed === '') {
34
+ return 0;
35
+ }
36
+
37
+ try {
38
+ return arrayCountFrom(JSON.parse(trimmed));
39
+ } catch {
40
+ return 0;
41
+ }
42
+ }
43
+
44
+ // A keyed object (`{"1": {...}, "2": {...}}`) is the shape PHP produces
45
+ // when a list's keys stop being consecutive — same list, different JSON.
46
+ if (value && typeof value === 'object') {
47
+ return Object.keys(value).length;
48
+ }
49
+
50
+ return 0;
51
+ }
52
+
53
+ /**
54
+ * Is a resolved field value one the page should show a placeholder for?
55
+ *
56
+ * `null` reaches the UI as the string "null" often enough (a stringified
57
+ * payload, a concatenated relation path) that treating it as content puts the
58
+ * literal word on the page. Zero and `false` are answers, not blanks.
59
+ *
60
+ * @param {*} value
61
+ * @returns {boolean}
62
+ */
63
+ export function isBlankValue(value) {
64
+ if (value === null || value === undefined) {
65
+ return true;
66
+ }
67
+
68
+ if (typeof value === 'string') {
69
+ const trimmed = value.trim();
70
+
71
+ return trimmed === '' || trimmed === 'null' || trimmed === 'undefined';
72
+ }
73
+
74
+ return false;
75
+ }
76
+
77
+ /**
78
+ * The label shown above an overview value.
79
+ *
80
+ * Overview labels were written as inline run-in labels ("Label: value"), so a
81
+ * good number of the JSON configs carry their own trailing colon. Stacked
82
+ * above the value the colon is noise, and it is not something we can go and
83
+ * strip from every project's config — so it is normalised here, once.
84
+ *
85
+ * @param {*} label
86
+ * @returns {string}
87
+ */
88
+ export function overviewFieldLabel(label) {
89
+ if (label === null || label === undefined) {
90
+ return '';
91
+ }
92
+
93
+ return String(label).trim().replace(/[::]\s*$/, '');
94
+ }