@visns-studio/visns-components 6.32.2 → 6.34.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 (41) hide show
  1. package/README.md +504 -0
  2. package/package.json +1 -1
  3. package/src/components/DataGrid.jsx +44 -2
  4. package/src/components/Field.jsx +32 -0
  5. package/src/components/Form.jsx +39 -2
  6. package/src/components/Navigation.jsx +353 -43
  7. package/src/components/Notification.jsx +13 -2
  8. package/src/components/TableFilter.jsx +14 -2
  9. package/src/components/auth/Login.jsx +9 -2
  10. package/src/components/auth/PasskeyEnrolPrompt.jsx +269 -0
  11. package/src/components/auth/Profile.jsx +422 -26
  12. package/src/components/auth/authEndpoints.js +43 -0
  13. package/src/components/auth/passkeyClient.js +102 -0
  14. package/src/components/auth/passkeyPrompt.js +408 -0
  15. package/src/components/auth/profileLayout.js +45 -0
  16. package/src/components/controls/DataGridSearch.jsx +16 -2
  17. package/src/components/controls/DataGridSortSheet.jsx +2 -0
  18. package/src/components/generic/ActionButtons.jsx +1 -1
  19. package/src/components/generic/GenericAuth.jsx +197 -21
  20. package/src/components/generic/GenericDetail.jsx +95 -3
  21. package/src/components/generic/GenericMain.jsx +7 -0
  22. package/src/components/generic/StandardModal.jsx +71 -158
  23. package/src/components/sms/SmsCampaigns.jsx +2487 -0
  24. package/src/components/sms/SmsInbox.jsx +18 -0
  25. package/src/components/sms/SmsThreadPanel.jsx +20 -0
  26. package/src/components/sms/smsEndpoints.js +23 -0
  27. package/src/components/styles/DataGrid.module.scss +22 -0
  28. package/src/components/styles/Form.module.scss +100 -2
  29. package/src/components/styles/Navigation.module.scss +354 -15
  30. package/src/components/styles/Notification.module.scss +140 -8
  31. package/src/components/styles/PasskeyEnrolPrompt.module.scss +105 -0
  32. package/src/components/styles/Profile.module.scss +157 -0
  33. package/src/components/styles/Sms.module.scss +451 -0
  34. package/src/components/styles/StandardModal.module.scss +273 -0
  35. package/src/components/styles/global.css +24 -5
  36. package/src/components/utils/navCollapsed.js +201 -0
  37. package/src/components/utils/rowActionSettings.js +76 -0
  38. package/src/components/utils/usePasskeysEnabled.js +84 -0
  39. package/src/components/utils/useShellLayout.js +131 -0
  40. package/src/index.js +103 -1
  41. package/src/utils/rememberTab.js +70 -0
@@ -1,4 +1,10 @@
1
- import React, { useState, useEffect, useInsertionEffect } from 'react';
1
+ import React, {
2
+ useCallback,
3
+ useState,
4
+ useEffect,
5
+ useInsertionEffect,
6
+ useRef,
7
+ } from 'react';
2
8
  import {
3
9
  useNavigate,
4
10
  useLocation,
@@ -33,6 +39,24 @@ import {
33
39
  // the same one for the transitions they own; identical object, so a screen's
34
40
  // curtain and this one are indistinguishable when both are up.
35
41
  import AuthLoading from '../auth/AuthLoading';
42
+ // The shell layout mirrors itself onto <html> the way `density` does, so the
43
+ // screens below the route table can read it without being handed the prop.
44
+ import {
45
+ SHELL_LAYOUT_ATTRIBUTE,
46
+ normaliseShellLayout,
47
+ } from '../utils/useShellLayout';
48
+ // The offer to enrol a passkey, made once after a sign-in. Everything about
49
+ // WHEN it appears is in auth/passkeyPrompt.js; this file supplies the two
50
+ // facts only it knows — whether the host turned the feature on, and whether
51
+ // this page load contains an actual sign-in.
52
+ import PasskeyEnrolPrompt from '../auth/PasskeyEnrolPrompt';
53
+ import { resolvePasskeyOptions } from '../auth/passkeyPrompt';
54
+ // `data-passkeys` on <html>, for the screens this file does not mount — the
55
+ // profile page is routed by the consuming app, not from here. See the module.
56
+ import {
57
+ PASSKEYS_ATTRIBUTE,
58
+ PASSKEYS_ENABLED_VALUE,
59
+ } from '../utils/usePasskeysEnabled';
36
60
 
37
61
  /**
38
62
  * Path segments that mean "an auth screen that hands off to the app".
@@ -94,8 +118,30 @@ const GenericAuth = ({
94
118
  logo,
95
119
  headerLogo,
96
120
  providers,
97
- // Whether this host offers passkeys. Threaded to the login screen the
98
- // same way `providers` is; false leaves the button undrawn.
121
+ /*
122
+ * Whether this host offers passkeys (WebAuthn), and what to do about it.
123
+ * Config-driven exactly like `providers`: the server says whether the
124
+ * feature exists, the browser is asked separately whether it can do it.
125
+ *
126
+ * false / absent nothing. No button on the login screen, no tab on
127
+ * the profile, no prompt. Byte-identical to 6.32.
128
+ * true the login button (what it has always meant), plus
129
+ * the profile's passkeys tab and the enrol-once prompt
130
+ * after signing in.
131
+ * {…} the same, with the details named:
132
+ * enabled default true — an object is the
133
+ * opt-in, so this is only written to
134
+ * switch the feature off from a
135
+ * config object the host already has.
136
+ * promptOnLogin default true. False offers passkeys
137
+ * without ever interrupting anybody.
138
+ * snoozeDays default 30 — how long "Not now"
139
+ * holds for.
140
+ * copy the dialog's words, merged over the
141
+ * defaults, like Login's `copy`.
142
+ *
143
+ * See auth/passkeyPrompt.js, which owns the shape and every gate.
144
+ */
99
145
  passkeys = false,
100
146
  navigation,
101
147
  routeConfig,
@@ -132,6 +178,14 @@ const GenericAuth = ({
132
178
  // the websocket lives) simply passes its own `navBadges` from there
133
179
  // instead, which lands after this one and wins.
134
180
  navBadges = {},
181
+ /*
182
+ * The square brand mark for the collapsed sidebar rail, if the project has
183
+ * one. `headerLogo` is a wide lock-up and there is no room for a wordmark
184
+ * in a 64px strip; passing nothing leaves the collapsed plate holding the
185
+ * collapse toggle alone. Sidebar layouts only — the header bar never
186
+ * collapses. Threaded straight through the shell to Navigation.
187
+ */
188
+ collapsedLogo,
135
189
  }) => {
136
190
  const navigate = useNavigate();
137
191
  const location = useLocation();
@@ -146,6 +200,65 @@ const GenericAuth = ({
146
200
  const [isAuthenticated, setIsAuthenticated] = useState(false);
147
201
  const [isClientAuthenticated, setIsClientAuthenticated] = useState(false);
148
202
 
203
+ // The `passkeys` prop in one shape, whichever of the three the host used.
204
+ const passkeyOptions = resolvePasskeyOptions(passkeys);
205
+
206
+ /*
207
+ * THE SIGN-IN EVENT — "somebody signed in during this page's life".
208
+ *
209
+ * The passkey prompt must not appear on a refresh, and from below this
210
+ * component the two are indistinguishable: either way the shell mounts
211
+ * with a live session and a profile. The difference is visible in exactly
212
+ * one place, here, because there are two ways `isAuthenticated` becomes
213
+ * true and only one of them is a sign-in:
214
+ *
215
+ * - `updateAuthStatus` sets it after `/ajax/user/profile` answers. That
216
+ * is a session that ALREADY existed — a refresh, a second tab, a
217
+ * bookmark opened this morning.
218
+ * - an auth screen calls the setter this component handed it. That is
219
+ * somebody typing a password, entering a 2FA code, or using a passkey,
220
+ * right now.
221
+ *
222
+ * So the screens get `markAuthenticated` instead of the raw setter, and
223
+ * `updateAuthStatus` keeps the raw one. A ref rather than a boolean state
224
+ * would do as well, except the prompt has to re-render when it flips, and
225
+ * a ref does not cause one.
226
+ *
227
+ * It is deliberately NOT sessionStorage. A flag in storage outlives the
228
+ * page that wrote it — a duplicated tab, a restored session, a back button
229
+ * — and would have to be cleared by whoever read it first, which is a
230
+ * second piece of state to get wrong. This value cannot survive a reload,
231
+ * which is exactly the property being tested for.
232
+ */
233
+ const [signInEvent, setSignInEvent] = useState(null);
234
+
235
+ // Read inside the stable callback below so its identity never changes:
236
+ // the auth screens are memoised on their props, and a setter with a new
237
+ // identity every render would re-render each of them on every render.
238
+ const promptOnLoginRef = useRef(passkeyOptions.promptOnLogin);
239
+ promptOnLoginRef.current = passkeyOptions.promptOnLogin;
240
+
241
+ /**
242
+ * What the auth screens are given in place of `setIsAuthenticated`.
243
+ *
244
+ * `meta.method` says how the sign-in happened — Login passes 'passkey' for
245
+ * the passkey button and 'password' for the form; a screen that passes
246
+ * nothing counts as 'password'. Nothing else changes: the boolean still
247
+ * goes straight to the same state setter, and a host that passes this
248
+ * component's setter somewhere expecting a plain `useState` setter is
249
+ * unaffected, because the extra argument is optional.
250
+ */
251
+ const markAuthenticated = useCallback((value, meta) => {
252
+ if (value && promptOnLoginRef.current) {
253
+ setSignInEvent({
254
+ method: meta?.method || 'password',
255
+ at: Date.now(),
256
+ });
257
+ }
258
+
259
+ setIsAuthenticated(value);
260
+ }, []);
261
+
149
262
  // Mirror the `density` prop onto <html> as data-density, which is what
150
263
  // styles/density.css keys off. 'default' writes nothing, so an app that
151
264
  // never passes the prop leaves no attribute and matches no rule.
@@ -166,6 +279,30 @@ const GenericAuth = ({
166
279
  };
167
280
  }, [density]);
168
281
 
282
+ // Same mechanism, same node: the shell layout mirrors itself onto <html>
283
+ // as `data-layout`, so a screen several components below the route table
284
+ // can find out whether the app has a rail down its left edge without the
285
+ // prop being threaded to it. See utils/useShellLayout.js for the reading
286
+ // half and for why 'full' — GenericAuth's own default — is what an absent
287
+ // attribute means. Written for EVERY layout, 'full' included: the value is
288
+ // the contract, and nothing in this library styles off `data-layout`, so
289
+ // an app that never passed the prop renders exactly what it rendered
290
+ // before with one more attribute on <html>.
291
+ useInsertionEffect(() => {
292
+ if (typeof document === 'undefined') {
293
+ return undefined;
294
+ }
295
+
296
+ document.documentElement.setAttribute(
297
+ SHELL_LAYOUT_ATTRIBUTE,
298
+ normaliseShellLayout(layout)
299
+ );
300
+
301
+ return () => {
302
+ document.documentElement.removeAttribute(SHELL_LAYOUT_ATTRIBUTE);
303
+ };
304
+ }, [layout]);
305
+
169
306
  // Same mechanism, same node, same reasoning: `useCardRowsEnabled` in
170
307
  // utils/useDensity.js reads this attribute off <html>, so an app opts its
171
308
  // grids in from one place and every grid — including one mounted inside a
@@ -182,6 +319,26 @@ const GenericAuth = ({
182
319
  };
183
320
  }, [cardRows]);
184
321
 
322
+ // And once more for passkeys. The profile screen is mounted by the
323
+ // consuming app's own `routeConfig` — this file never sees it — so the one
324
+ // way to tell it the feature is on, without every app editing its route
325
+ // config, is the document. Absent means off, which is what an app that
326
+ // never passed the prop writes.
327
+ useInsertionEffect(() => {
328
+ if (typeof document === 'undefined' || !passkeyOptions.enabled) {
329
+ return undefined;
330
+ }
331
+
332
+ document.documentElement.setAttribute(
333
+ PASSKEYS_ATTRIBUTE,
334
+ PASSKEYS_ENABLED_VALUE
335
+ );
336
+
337
+ return () => {
338
+ document.documentElement.removeAttribute(PASSKEYS_ATTRIBUTE);
339
+ };
340
+ }, [passkeyOptions.enabled]);
341
+
185
342
  const updateAuthStatus = async () => {
186
343
  // The loading curtain covers two moments and stays down for neither
187
344
  // more nor less: the first profile check of a page load (no usable
@@ -298,8 +455,8 @@ const GenericAuth = ({
298
455
  loginBg={loginBg}
299
456
  logo={logo}
300
457
  providers={providers}
301
- passkeys={passkeys}
302
- setIsAuthenticated={setIsAuthenticated}
458
+ passkeys={passkeyOptions.enabled}
459
+ setIsAuthenticated={markAuthenticated}
303
460
  setUserProfile={setUserProfile}
304
461
  config={config}
305
462
  endpoints={endpoints}
@@ -337,7 +494,7 @@ const GenericAuth = ({
337
494
  <TwoFactorAuthComponent
338
495
  loginBg={loginBg}
339
496
  logo={logo}
340
- setIsAuthenticated={setIsAuthenticated}
497
+ setIsAuthenticated={markAuthenticated}
341
498
  setUserProfile={setUserProfile}
342
499
  config={config}
343
500
  endpoints={endpoints}
@@ -428,21 +585,40 @@ const GenericAuth = ({
428
585
  <Route
429
586
  path="/*"
430
587
  element={
431
- <MainShell
432
- component={mainComponent}
433
- layout={layout}
434
- logo={headerLogo}
435
- navigation={navigation}
436
- routeConfig={routeConfig}
437
- setUserProfile={setUserProfile}
438
- setSystemAuth={setIsAuthenticated}
439
- themeType={themeType}
440
- userProfile={userProfile}
441
- enforce2FA={enforce2FA}
442
- density={density}
443
- navBadges={navBadges}
444
- {...mainComponentProps}
445
- />
588
+ <>
589
+ <MainShell
590
+ component={mainComponent}
591
+ layout={layout}
592
+ logo={headerLogo}
593
+ collapsedLogo={collapsedLogo}
594
+ navigation={navigation}
595
+ routeConfig={routeConfig}
596
+ setUserProfile={setUserProfile}
597
+ setSystemAuth={setIsAuthenticated}
598
+ themeType={themeType}
599
+ userProfile={userProfile}
600
+ enforce2FA={enforce2FA}
601
+ density={density}
602
+ navBadges={navBadges}
603
+ {...mainComponentProps}
604
+ />
605
+ {/* Inside the protected route on purpose: it
606
+ can then only ever mount for a signed-in
607
+ user, in the authenticated shell, and it
608
+ renders nothing at all until every gate in
609
+ passkeyPrompt.js has passed. An app that
610
+ left `passkeys` alone never even mounts
611
+ it. */}
612
+ {passkeyOptions.promptOnLogin ? (
613
+ <PasskeyEnrolPrompt
614
+ passkeys={passkeys}
615
+ userProfile={userProfile}
616
+ endpoints={endpoints}
617
+ justSignedIn={!!signInEvent}
618
+ signInMethod={signInEvent?.method}
619
+ />
620
+ ) : null}
621
+ </>
446
622
  }
447
623
  />
448
624
  </Route>
@@ -102,6 +102,7 @@ import ProposalTemplatePreview from '../proposal/ProposalTemplatePreview';
102
102
  import GalleryModal from '../modals/GalleryModal';
103
103
 
104
104
  import styles from '../styles/GenericDetail.module.scss';
105
+ import { openTabFromUrl, writeTabToUrl } from '../../utils/rememberTab.js';
105
106
 
106
107
  const localizer = momentLocalizer(moment);
107
108
  const DnDCalendar = withDragAndDrop(Calendar);
@@ -257,6 +258,28 @@ function GenericDetail({
257
258
  * wrapped this component for one page knows something the config does not.
258
259
  */
259
260
  editPlacement = null,
261
+ /**
262
+ * REMEMBER THE OPEN TAB IN THE ADDRESS BAR — `?tab=<filter id>` — so a
263
+ * refresh, a copied link or the back button lands on the tab the reader
264
+ * was on rather than on the first one (product owner, 2026-09-14: "when
265
+ * refreshing on project page, can it remember which tab you are on").
266
+ *
267
+ * READ once, when the tab list is built from the config: a `?tab` naming
268
+ * a top-level filter id makes that tab the shown one, in the same
269
+ * `show`/`active`/`class` vocabulary the config itself uses (the class is
270
+ * borrowed from whichever tab the config marks shown, so a page that
271
+ * says `subactive` keeps saying it). An id that names nothing is ignored
272
+ * and the config's own default stands — a stale link is a first-tab
273
+ * landing, never a blank page.
274
+ *
275
+ * WRITTEN with `history.replaceState`, never a router navigation: the
276
+ * host may have rewritten the path to a slug the router does not know
277
+ * about, and a navigation would put the numeric id back and remount the
278
+ * page around the reader. Only the `tab` parameter is touched; every
279
+ * other query parameter and the hash are kept. `false` turns both halves
280
+ * off for a host that owns its query string.
281
+ */
282
+ rememberTab = true,
260
283
  /**
261
284
  * Host-supplied components for `type: "custom"` tabs, keyed by the tab's
262
285
  * `renderer`.
@@ -851,6 +874,19 @@ function GenericDetail({
851
874
  return action.label;
852
875
  };
853
876
 
877
+ /**
878
+ * The native tooltip for a host-configured action button.
879
+ *
880
+ * A tooltip is meant to say what pressing the button DOES rather than
881
+ * repeat the word written on it — but a custom action's behaviour lives
882
+ * in the consuming application's config and not here, so the only
883
+ * sentence this library can write is the one the host hands it.
884
+ * `tooltip` is that opening; everything else falls back to the label,
885
+ * which at least rescues a button whose text the layout has truncated.
886
+ */
887
+ const getActionTooltip = (action) =>
888
+ action?.tooltip || getActionLabel(action) || undefined;
889
+
854
890
  const getNestedValue = (obj, path) => {
855
891
  return path.split('.').reduce((current, key) => current?.[key], obj);
856
892
  };
@@ -1373,6 +1409,16 @@ function GenericDetail({
1373
1409
  {passwordVisible ? stringValue : '******'}
1374
1410
  <button
1375
1411
  type="button"
1412
+ title={
1413
+ passwordVisible
1414
+ ? 'Hide this value again'
1415
+ : 'Show this value in plain text'
1416
+ }
1417
+ aria-label={
1418
+ passwordVisible
1419
+ ? 'Hide this value again'
1420
+ : 'Show this value in plain text'
1421
+ }
1376
1422
  onClick={togglePasswordVisibility}
1377
1423
  style={{
1378
1424
  marginLeft: '10px',
@@ -3821,7 +3867,7 @@ function GenericDetail({
3821
3867
 
3822
3868
  useEffect(() => {
3823
3869
  setSubnav(() => {
3824
- return filters.map((f) => {
3870
+ const built = filters.map((f) => {
3825
3871
  if (
3826
3872
  f.hasOwnProperty('url') &&
3827
3873
  f.baseUrl &&
@@ -3836,9 +3882,24 @@ function GenericDetail({
3836
3882
  return f;
3837
3883
  }
3838
3884
  });
3885
+
3886
+ return rememberTab ? openTabFromUrl(built) : built;
3839
3887
  });
3840
3888
  }, [filters]);
3841
3889
 
3890
+ /**
3891
+ * The tab strip's setter, with the address bar kept in step. TableFilter
3892
+ * hands back the whole updated array (never a function), and the shown
3893
+ * top-level entry is what goes into `?tab=`.
3894
+ */
3895
+ const setSubnavRemembered = (next) => {
3896
+ setSubnav(next);
3897
+
3898
+ if (rememberTab && Array.isArray(next)) {
3899
+ writeTabToUrl(next);
3900
+ }
3901
+ };
3902
+
3842
3903
  /** Modal States */
3843
3904
  const [formData, setFormData] = useState({});
3844
3905
  const [formType, setFormType] = useState('');
@@ -3928,7 +3989,9 @@ function GenericDetail({
3928
3989
  className={styles.btn}
3929
3990
  title={
3930
3991
  activeTabConfig.form
3931
- .editLabel || 'Edit'
3992
+ .inPlaceEdit
3993
+ ? 'Open this record’s fields for editing, here on the page'
3994
+ : 'Open this record’s fields for editing'
3932
3995
  }
3933
3996
  aria-label={
3934
3997
  activeTabConfig.form
@@ -3974,6 +4037,7 @@ function GenericDetail({
3974
4037
  className={
3975
4038
  styles.btnSecondary
3976
4039
  }
4040
+ title="Stop editing and leave the record as it was — nothing you have typed is kept"
3977
4041
  onClick={
3978
4042
  cancelInPlaceEditing
3979
4043
  }
@@ -3984,6 +4048,7 @@ function GenericDetail({
3984
4048
  className={
3985
4049
  styles.btn
3986
4050
  }
4051
+ title="Save what you have typed and close the fields"
3987
4052
  onClick={() => {
3988
4053
  // Find the form's save button and click it
3989
4054
  const saveButton =
@@ -4042,6 +4107,7 @@ function GenericDetail({
4042
4107
  className={
4043
4108
  styles.btn
4044
4109
  }
4110
+ title="Save the template as it is laid out below"
4045
4111
  onClick={() => {
4046
4112
  // Handle the specific "ocrTemplate" item
4047
4113
  const ocrItem =
@@ -4066,6 +4132,11 @@ function GenericDetail({
4066
4132
  {activeTabConfig.export && (
4067
4133
  <button
4068
4134
  className={styles.btn}
4135
+ title={
4136
+ activeTabConfig.export
4137
+ .tooltip ||
4138
+ 'Download what this tab is showing as a file'
4139
+ }
4069
4140
  onClick={() =>
4070
4141
  handleExport(
4071
4142
  activeTabConfig.export
@@ -4080,6 +4151,9 @@ function GenericDetail({
4080
4151
  {activeTabConfig.customAction && (
4081
4152
  <button
4082
4153
  className={styles.btn}
4154
+ title={getActionTooltip(
4155
+ activeTabConfig.customAction
4156
+ )}
4083
4157
  onClick={() =>
4084
4158
  handleCustomAction(
4085
4159
  activeTabConfig.customAction
@@ -4097,6 +4171,7 @@ function GenericDetail({
4097
4171
  <button
4098
4172
  key={index}
4099
4173
  className={styles.btn}
4174
+ title={getActionTooltip(customAction)}
4100
4175
  onClick={() =>
4101
4176
  handleCustomAction(customAction)
4102
4177
  }
@@ -4118,6 +4193,12 @@ function GenericDetail({
4118
4193
  <>
4119
4194
  <button
4120
4195
  className={styles.btn}
4196
+ title={
4197
+ activeTabConfig.functions
4198
+ .checkboxUpdate
4199
+ .tooltip ||
4200
+ 'Apply this change to every row you have ticked'
4201
+ }
4121
4202
  onClick={
4122
4203
  handleCheckboxUpdate
4123
4204
  }
@@ -4149,6 +4230,13 @@ function GenericDetail({
4149
4230
  ?.checkboxDelete && (
4150
4231
  <button
4151
4232
  className={styles.btn}
4233
+ title={
4234
+ activeTabConfig
4235
+ .functions
4236
+ .checkboxDelete
4237
+ .tooltip ||
4238
+ 'Delete every row you have ticked'
4239
+ }
4152
4240
  onClick={
4153
4241
  handleCheckboxDelete
4154
4242
  }
@@ -4163,6 +4251,9 @@ function GenericDetail({
4163
4251
  {activeTabConfig.customAction && (
4164
4252
  <button
4165
4253
  className={styles.btn}
4254
+ title={getActionTooltip(
4255
+ activeTabConfig.customAction
4256
+ )}
4166
4257
  onClick={() =>
4167
4258
  handleCustomAction(
4168
4259
  activeTabConfig.customAction
@@ -4181,6 +4272,7 @@ function GenericDetail({
4181
4272
  <button
4182
4273
  key={index}
4183
4274
  className={styles.btn}
4275
+ title={getActionTooltip(customAction)}
4184
4276
  onClick={() =>
4185
4277
  handleCustomAction(customAction)
4186
4278
  }
@@ -4489,7 +4581,7 @@ function GenericDetail({
4489
4581
  >
4490
4582
  <TableFilter
4491
4583
  filters={subnav}
4492
- setFilters={setSubnav}
4584
+ setFilters={setSubnavRemembered}
4493
4585
  variant={tabsUnderlined ? 'underline' : undefined}
4494
4586
  />
4495
4587
  </div>
@@ -43,6 +43,12 @@ const GenericMain = ({
43
43
  // `navRenderers` is — the shell does not know what a project counts, who
44
44
  // is allowed to see it, or how often it changes. See navBadges.js.
45
45
  navBadges = {},
46
+ /*
47
+ * The square brand mark the sidebar rail wears when it is collapsed.
48
+ * Accepted here so it is part of the main-layout interface; the header
49
+ * layout has no rail and passes it on to a Navigation that ignores it.
50
+ */
51
+ collapsedLogo,
46
52
  }) => {
47
53
  const navigate = useNavigate();
48
54
  const location = useLocation();
@@ -183,6 +189,7 @@ const GenericMain = ({
183
189
  <Navigation
184
190
  layout={layout}
185
191
  logo={logo}
192
+ collapsedLogo={collapsedLogo}
186
193
  navConfig={navigation}
187
194
  setSystemAuth={setSystemAuth}
188
195
  themeType={themeType}