@visns-studio/visns-components 6.24.4 → 6.25.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 (43) hide show
  1. package/package.json +4 -2
  2. package/src/components/Autocomplete.jsx +189 -119
  3. package/src/components/DataGrid.jsx +472 -31
  4. package/src/components/Navigation.jsx +475 -51
  5. package/src/components/auth/ClientAuthFrame.jsx +5 -0
  6. package/src/components/auth/ClientAuthScreen.jsx +29 -0
  7. package/src/components/columns/ColumnRenderers.jsx +3 -46
  8. package/src/components/columns/StackedRow.jsx +186 -0
  9. package/src/components/controls/DataGridSearch.jsx +110 -2
  10. package/src/components/controls/DataGridSortSheet.jsx +155 -0
  11. package/src/components/generic/GenericAuth.jsx +50 -18
  12. package/src/components/generic/GenericDashboard.jsx +20 -1
  13. package/src/components/generic/GenericDetail.jsx +446 -259
  14. package/src/components/mapboxSearchBox.js +640 -0
  15. package/src/components/navBadges.js +63 -1
  16. package/src/components/navDrawer.js +147 -0
  17. package/src/components/sms/SmsThreadPanel.jsx +34 -6
  18. package/src/components/sms/smsHelpers.js +15 -0
  19. package/src/components/styles/ClientAuth.module.scss +39 -0
  20. package/src/components/styles/DataGrid.module.scss +158 -5
  21. package/src/components/styles/Field.module.scss +52 -1
  22. package/src/components/styles/Form.module.scss +82 -0
  23. package/src/components/styles/GenericClientPortal.module.scss +72 -20
  24. package/src/components/styles/GenericDashboard.module.scss +50 -0
  25. package/src/components/styles/GenericDetail.module.scss +63 -1
  26. package/src/components/styles/GenericDynamic.module.scss +23 -0
  27. package/src/components/styles/GenericFormBuilder.module.scss +11 -0
  28. package/src/components/styles/GenericIndex.module.scss +6 -1
  29. package/src/components/styles/Navigation.module.scss +460 -7
  30. package/src/components/styles/Sms.module.scss +92 -0
  31. package/src/components/styles/StackedRow.module.scss +182 -0
  32. package/src/components/styles/TicketConversation.module.scss +76 -0
  33. package/src/components/styles/Vault.module.scss +192 -0
  34. package/src/components/styles/density.css +10 -0
  35. package/src/components/styles/global-datagrid.css +163 -0
  36. package/src/components/styles/global.css +20 -0
  37. package/src/components/tickets/TicketConversation.jsx +13 -8
  38. package/src/components/utils/ConfirmDialog.js +22 -3
  39. package/src/components/utils/cardLayout.js +666 -0
  40. package/src/components/utils/contactChannels.js +130 -0
  41. package/src/components/utils/editPlacement.js +95 -0
  42. package/src/components/utils/useDensity.js +303 -7
  43. package/src/index.js +32 -0
@@ -54,6 +54,32 @@
54
54
  tallest thing in it happens to measure. Total
55
55
  bar height is this plus --nav-bar-padding: 52px
56
56
  and 4px gives the 60px bar. default: 50px
57
+ --nav-header-height the FULL height of the header bar, border
58
+ included — what the ≤1024px drawer and its
59
+ backdrop hang off, so that they start exactly
60
+ where the bar stops.
61
+ The default composes the bar's own box:
62
+ --nav-logo-height + twice --spacing-sm + the
63
+ 1px bottom border, which is exact for every
64
+ project that leaves --nav-bar-padding alone.
65
+ A PROJECT THAT OVERRIDES --nav-bar-padding
66
+ MUST SET THIS TOO — nothing can read a
67
+ shorthand's vertical half back out of it, and
68
+ the failure is a drawer overlapping the bar
69
+ or floating below it.
70
+ default: calc(var(--nav-logo-height, 50px) +
71
+ 2 * var(--spacing-sm, 0.5rem) + 1px)
72
+ --nav-logo-width-sm the cap on the logo's width below 640px. The
73
+ logo keeps --logo-width wherever that is
74
+ already narrower — this only ever shrinks.
75
+ default: 92px
76
+ --nav-badge-warn the ground of a `tone: 'warn'` count pill. The
77
+ only two hooks here that are colour rather than
78
+ --nav-badge-danger density, and they are hooks at all because the
79
+ severity grounds are stated as literals — see
80
+ the contrast note at .nav-badge-warn for why
81
+ they cannot simply read the app's tokens.
82
+ defaults: #d9902a (dark ink) and #c1272d (white ink)
57
83
 
58
84
  Two deliberate omissions: the icon-to-label gap inside a nav item and the
59
85
  padding on the chips themselves are internal alignment, not density — the
@@ -269,6 +295,15 @@
269
295
  transform: rotate(180deg);
270
296
  }
271
297
 
298
+ /* The same caret, turned by the state a TOUCH reader's tap put on the
299
+ button rather than by a hover the device cannot produce. The
300
+ attribute is only ever rendered on a coarse pointer — see the group
301
+ button in Navigation.jsx — so this cannot fire on a desktop. */
302
+ &:has(> button[aria-expanded='true']) > button::after {
303
+ opacity: 1;
304
+ transform: rotate(180deg);
305
+ }
306
+
272
307
  &.nav-item {
273
308
  a,
274
309
  span,
@@ -434,9 +469,30 @@
434
469
  the group heading, the rows appear, tab again and focus moves
435
470
  into them — and the menu stays open because focus is still
436
471
  inside the item. Without this the dropdown was hover-only and
437
- every page inside a group was unreachable without a mouse. */
438
- &:hover > ul,
439
- &:focus-within > ul {
472
+ every page inside a group was unreachable without a mouse.
473
+
474
+ FENCED BEHIND `hover: hover`, which is every mouse and every
475
+ trackpad and therefore leaves the desktop bar byte-identical.
476
+ What it excludes is the touch screen, where a "hover" is a
477
+ sticky artefact of the last tap that then refuses to end, and
478
+ `:focus-within` fights the button's own blur-on-click. Those
479
+ devices get the rule below instead — one mechanism at a time,
480
+ so the menu's open state is never two disagreeing answers. */
481
+ @media (hover: hover) {
482
+ &:hover > ul,
483
+ &:focus-within > ul {
484
+ display: block;
485
+ opacity: 1;
486
+ }
487
+ }
488
+
489
+ /* Touch: the open state is React's, published on the group button
490
+ as `aria-expanded` (Navigation.jsx sets the attribute ONLY on a
491
+ coarse pointer, which is why this needs no media query of its
492
+ own and cannot affect a mouse). A keyboard on a tablet lands
493
+ here too — Enter fires the same click handler — so nothing is
494
+ left unreachable by fencing the hover path above. */
495
+ > button[aria-expanded='true'] + ul {
440
496
  display: block;
441
497
  opacity: 1;
442
498
  }
@@ -560,9 +616,17 @@
560
616
  }
561
617
  }
562
618
 
563
- /* Same keyboard path as `.nav-item` above. */
564
- &:hover > ul,
565
- &:focus-within > ul {
619
+ /* Same keyboard path, and the same touch/hover split, as
620
+ `.nav-item` above — see the long note there. */
621
+ @media (hover: hover) {
622
+ &:hover > ul,
623
+ &:focus-within > ul {
624
+ display: block;
625
+ opacity: 1;
626
+ }
627
+ }
628
+
629
+ > button[aria-expanded='true'] + ul {
566
630
  display: block;
567
631
  opacity: 1;
568
632
  }
@@ -616,6 +680,48 @@
616
680
  font-variant-numeric: tabular-nums;
617
681
  }
618
682
 
683
+ /* ---------------------------------------------------------------------------
684
+ Tones — how BAD the number is, not what it counts.
685
+ ---------------------------------------------------------------------------
686
+ `ok` is the block above and has no class of its own; a consumer that
687
+ publishes a plain number keeps exactly the green pill it has always had.
688
+ These two are modifiers written after it, so they win on source order at the
689
+ same specificity — deliberately, because anything heavier here would have to
690
+ be restated in the `.hactions` block below, which already positions this
691
+ same element.
692
+
693
+ BOTH grounds are hard-coded rather than reaching for --secondary-color the
694
+ way the ok pill and the bell do. In this house that token IS the green: a
695
+ "danger" pill written as `var(--secondary-color, #c1272d)` would resolve to
696
+ the same green as the default one in every project that declares the token,
697
+ and the fallback crimson would only ever appear in projects that do not —
698
+ which is precisely backwards. The public `--vs-warn` / `--vs-danger` hooks
699
+ from _surface.scss are honoured first so a project that has already stated
700
+ its palette is not overridden here.
701
+
702
+ Contrast, because the green pill shipped illegible once and a 10px digit has
703
+ no margin for a guess (all figures against the house ink #1b3933 and #fff):
704
+
705
+ amber #c8811b vs ink 3.94:1 vs white 3.18:1 — neither reaches 4.5
706
+ amber #d9902a vs ink 4.75:1 vs white 2.63:1 — ink clears it
707
+ crimson #c1272d vs ink 2.14:1 vs white 5.84:1 — white clears it
708
+
709
+ So warn is a step lighter than the library's #c8811b — the same hue, lifted
710
+ until the ink the ok pill already uses is legible on it, which also keeps
711
+ one ink across ok and warn so only the GROUND moves between them. Danger is
712
+ dark enough that the same ink is hopeless and takes white, which is what
713
+ every other crimson badge in the house does anyway.
714
+ ------------------------------------------------------------------------- */
715
+ .nav-badge-warn {
716
+ background-color: var(--nav-badge-warn, var(--vs-warn, #d9902a));
717
+ color: var(--primary-color, #1b3933);
718
+ }
719
+
720
+ .nav-badge-danger {
721
+ background-color: var(--nav-badge-danger, var(--vs-danger, #c1272d));
722
+ color: #fff;
723
+ }
724
+
619
725
  /* In a dropdown row the label is a block-level link, so the pill is pushed to
620
726
  the row's trailing edge rather than butted against the text. Both themed
621
727
  selectors are spelled out to match the specificity of the blocks above —
@@ -631,6 +737,15 @@
631
737
 
632
738
  .hactions {
633
739
  width: max-content;
740
+ /* A grid item's automatic minimum size is its CONTENT's min-content width,
741
+ not zero — so a cluster of nine 34px chips at `width: max-content` set a
742
+ floor of roughly 360px on this column, the bar's min-content width came
743
+ out around 530px, and on a 390px phone the browser resolved that by
744
+ collapsing the navigation column to nothing and letting the whole page
745
+ pan sideways. `min-width: 0` lets the column give way instead; the
746
+ cluster's own `overflow-x` below is what it gives way INTO.
747
+ Costs nothing above the breakpoint, where the bar has room to spare. */
748
+ min-width: 0;
634
749
  justify-self: end;
635
750
  position: relative;
636
751
  z-index: 1000; // Higher z-index to stay above sticky header
@@ -908,6 +1023,8 @@
908
1023
 
909
1024
  .hactions-alternate {
910
1025
  width: max-content;
1026
+ /* Same reason as `.hactions` — see the note there. */
1027
+ min-width: 0;
911
1028
  justify-self: end;
912
1029
  position: relative;
913
1030
  z-index: 1000; // Higher z-index to stay above sticky header
@@ -1355,6 +1472,20 @@
1355
1472
  margin: 0;
1356
1473
  line-height: 1.25;
1357
1474
  text-align: left;
1475
+ /* STATED, not left at `normal`. A mounted component's trigger was
1476
+ a round 36px chip before it was a row, and a chip centres its
1477
+ lone icon — `justify-content: center` is in Sms.module.scss,
1478
+ Vault.module.scss and Notification.module.scss alike. Left
1479
+ unset here, that centring survives the row shell and the
1480
+ action reads as an icon floating in the middle of a full-width
1481
+ row with the labelled rows above and below it left-aligned.
1482
+ The row grammar belongs to the row, so the row states it. */
1483
+ justify-content: flex-start;
1484
+ /* Same story, same chips: a 36/40px SQUARE is the other half of
1485
+ being a chip, and a fixed height in a list of rows sized by
1486
+ their own padding is a row that is visibly shorter than its
1487
+ neighbours. A row is as tall as the padding the rail gives it. */
1488
+ height: auto;
1358
1489
  text-decoration: none;
1359
1490
  color: var(--tertiary-color);
1360
1491
  background: var(--nav-bg);
@@ -1399,7 +1530,15 @@
1399
1530
  // trigger wears it. Empty by default, so a component that already
1400
1531
  // labels itself is untouched. (The notification bell sets its own
1401
1532
  // fallback text — see Notification.module.scss.)
1402
- > li > div > a::after {
1533
+ //
1534
+ // BOTH TRIGGER ELEMENTS, and that is the fix rather than the tidy-up:
1535
+ // this read `> li > div > a` alone, so the account switcher — a link —
1536
+ // wore its label and every component whose trigger is a <button>
1537
+ // (SmsInboxBadge, VaultQuickSearch) rendered in the drawer as a bare
1538
+ // icon with no text beside it at all. The row shell above has always
1539
+ // matched buttons; only the label did not.
1540
+ > li > div > a::after,
1541
+ > li > div > button::after {
1403
1542
  content: var(--nav-action-label, '');
1404
1543
  flex: 1 1 auto;
1405
1544
  min-width: 0;
@@ -1574,6 +1713,14 @@
1574
1713
  .rail-actions {
1575
1714
  padding: 0.35rem 0 calc(0.35rem + env(safe-area-inset-bottom, 0px));
1576
1715
 
1716
+ /* The footer casts a shadow UP over the list it footers, so the band
1717
+ above it reads as passing underneath rather than stopping at it.
1718
+ The hairline border alone drew a floor: a list clipped at a row
1719
+ boundary and a list that genuinely ends look identical against it,
1720
+ which is most of why a menu with six more entries below the fold
1721
+ read as a menu with six entries. */
1722
+ box-shadow: 0 -10px 12px -10px rgba(0, 0, 0, 0.55);
1723
+
1577
1724
  > ul > li {
1578
1725
  padding: 0.2rem 0.65rem;
1579
1726
  }
@@ -1994,3 +2141,309 @@
1994
2141
  }
1995
2142
  }
1996
2143
  }
2144
+
2145
+ /* ============================================================================
2146
+ The header layout on a phone — the drawer (≤1024px)
2147
+ ----------------------------------------------------------------------------
2148
+ The sliding panel, the backdrop, the hamburger and the body scroll lock were
2149
+ all written for the SIDEBAR layouts. The one-line header — the layout most of
2150
+ these applications actually run — had none of it: below 1024px its answer to
2151
+ a narrow screen was a horizontally-scrolling nav strip with the scrollbar
2152
+ hidden, which is a menu you have to know is there to find, next to an action
2153
+ cluster whose min-content width alone exceeded a 390px phone.
2154
+
2155
+ Nothing here is a second implementation of any of that. The panel IS
2156
+ `.aside--cms`, the open state IS `.aside--cms--open`, the backdrop IS
2157
+ `.drawer-backdrop`, the action footer IS `.rail-actions`, and the scroll lock
2158
+ is the effect that was already keyed on the same `isOpen`. What this block
2159
+ adds is the two things the header layout genuinely does not share: the anchor
2160
+ (this drawer hangs off the bottom of the BAR, not off a 50px content header)
2161
+ and the flat row list that replaces dropdowns nobody can open with a finger.
2162
+ ========================================================================= */
2163
+
2164
+ /* The bar's full outside height, composed rather than guessed — see the
2165
+ `--nav-header-height` entry in this file's density-hook header, including the
2166
+ one case a project has to state it by hand. Written as repeated additions
2167
+ because a multiplication of a custom property is one more thing that can go
2168
+ invalid at computed-value time for no gain. */
2169
+ $drawer-top: var(
2170
+ --nav-header-height,
2171
+ calc(
2172
+ var(--nav-logo-height, 50px) + var(--spacing-sm, 0.5rem) +
2173
+ var(--spacing-sm, 0.5rem) + 1px
2174
+ )
2175
+ );
2176
+
2177
+ @media (max-width: 1024px) {
2178
+ /* The strip goes; the drawer replaces it. `> .navwrap` and not a bare
2179
+ `.navwrap`: the sidebar layouts' own nav list wears the same class and
2180
+ is the CONTENTS of a drawer rather than an alternative to one. */
2181
+ .hwrap > .navwrap {
2182
+ display: none;
2183
+ }
2184
+
2185
+ /* The hamburger. Every span, every offset and every transform that folds
2186
+ the six bars into an X is `.mobmenu`'s, unchanged — what differs is
2187
+ placement: the content header pins its copy absolutely into a corner,
2188
+ and this one is an item in the bar's grid.
2189
+
2190
+ The scale is the other change. `.mobmenu` draws inside a 60x45 box and
2191
+ then scales the whole button to 0.4, which is a 24x18 target — under any
2192
+ touch guideline — while still reserving all 60px of bar width, because a
2193
+ transform does not change a layout box. 0.55 puts the DRAWN mark at
2194
+ 33x25: the box is the same order as the 34px chips at the far end, and —
2195
+ the part 0.75 got wrong — the bars themselves thin from ~4px to ~3px and
2196
+ stop out-weighing every glyph in the bar. (The chips draw ~18px icons
2197
+ inside their boxes; a hamburger whose bars span the full box will always
2198
+ read a step heavier than its box-for-box equal, so it sits a step BELOW
2199
+ the chips' box size, not at it.) Hit-testing follows the transform, so
2200
+ this is also the tap target — 33x25 is the same order as the chips, and
2201
+ the bar around it is dead space, so a near miss lands on nothing. The
2202
+ negative margins hand back the slack the untransformed box still holds
2203
+ in layout: half of (60 - 33) across and half of (45 - 24.75) down. */
2204
+ .hwrap .hmenu {
2205
+ position: relative;
2206
+ top: auto;
2207
+ left: auto;
2208
+ flex: none;
2209
+ justify-self: start;
2210
+ transform: scale(0.55);
2211
+ margin: -10px -13.5px;
2212
+ z-index: auto;
2213
+ }
2214
+
2215
+ /* THE DRAWER. The transform, the transition curve, the width, the z-index
2216
+ and the shadow all come from `.aside--cms` in the media block above; this
2217
+ re-anchors it and gives it the column the header layout has no `.awrap`
2218
+ to provide. */
2219
+ .hwrap .aside--drawer {
2220
+ top: #{$drawer-top};
2221
+ left: 0;
2222
+ height: calc(100% - #{$drawer-top});
2223
+ display: flex;
2224
+ flex-direction: column;
2225
+ /* `.aside--cms` reads `--nav-bg` with no fallback, which is fine for
2226
+ the layouts that ship with a rail and declare it. A header-layout
2227
+ project may never have needed one, and a `var()` that resolves to
2228
+ nothing takes the whole declaration with it — a transparent drawer
2229
+ over the page. */
2230
+ background: var(--nav-bg, var(--primary-color, #1b3933));
2231
+ /* The panels the action rows open — the notification list, the account
2232
+ switcher — are absolutely positioned inside the drawer and would be
2233
+ cut off by `.aside--cms`'s own clip. The nav list below keeps its own
2234
+ scroll container, so nothing here depended on that clip. */
2235
+ overflow: visible;
2236
+
2237
+ /* OFF-SCREEN IS NOT ENOUGH. A translated panel is still in the tab
2238
+ order and still in the accessibility tree, so a closed drawer parks
2239
+ the entire menu between the hamburger and the page for anyone using a
2240
+ keyboard or a screen reader. `visibility` removes it from both, and
2241
+ the delay is what keeps the CLOSING slide visible: the panel stays
2242
+ painted for exactly the length of the transform and goes at the end
2243
+ of it. */
2244
+ visibility: hidden;
2245
+ transition:
2246
+ transform 0.55s cubic-bezier(0.25, 0.8, 0.25, 1),
2247
+ visibility 0s linear 0.55s;
2248
+ }
2249
+
2250
+ .hwrap .aside--drawer.aside--cms--open {
2251
+ visibility: visible;
2252
+ transition:
2253
+ transform 0.55s cubic-bezier(0.25, 0.8, 0.25, 1),
2254
+ visibility 0s;
2255
+ }
2256
+
2257
+ /* The backdrop starts where the drawer does, so the bar above it stays
2258
+ tappable — which is what lets the hamburger close what it opened. */
2259
+ .hwrap .drawer-backdrop {
2260
+ top: #{$drawer-top};
2261
+ }
2262
+
2263
+ /* The hairline before the LAST chip says sign-out is a different kind of
2264
+ action from the eight before it. Below this width sign-out is a drawer
2265
+ row and the last chip is the ticket count, so the divider would be
2266
+ separating nothing from nothing. */
2267
+ .hwrap .hactions > ul > li:last-child,
2268
+ .hwrap .hactions-alternate > ul > li:last-child {
2269
+ margin-left: var(--nav-action-gap, 2px);
2270
+
2271
+ &::before {
2272
+ content: none;
2273
+ }
2274
+ }
2275
+
2276
+ /* ---------------------------------------------------------------------- *
2277
+ * the drawer's rows
2278
+ * ---------------------------------------------------------------------- *
2279
+ * The band between the top of the drawer and the actions pinned under it,
2280
+ * and the only part of the drawer that scrolls.
2281
+ */
2282
+ .drawer-nav {
2283
+ flex: 1 1 auto;
2284
+ /* Or the list refuses to shrink below its own content and pushes the
2285
+ action footer off the bottom of the panel. */
2286
+ min-height: 0;
2287
+ overflow-x: hidden;
2288
+ overflow-y: auto;
2289
+ padding: var(--padding, 1rem) 0;
2290
+
2291
+ /* THE SCROLLBAR IS THE AFFORDANCE, and it was hidden.
2292
+ `scrollbar-width: none` + `::-webkit-scrollbar { display: none }`
2293
+ is the right call for the horizontal strip this file hides it on —
2294
+ a 3px bar under a row of chips is noise. Here it is the only thing
2295
+ on screen that says the menu continues below the fold: the band
2296
+ stops at a full row and the pinned actions start immediately under
2297
+ it, so a truncated list and a complete one look identical. Safari
2298
+ on iOS honours `scrollbar-width`, so hiding it took the flashing
2299
+ overlay indicator away too — the list scrolled and said nothing
2300
+ about it, on the one device this drawer exists for.
2301
+ Thin and low-contrast rather than back to the platform default. */
2302
+ scrollbar-width: thin;
2303
+ scrollbar-color: rgba(var(--tertiary-color-rgb, 255, 255, 255), 0.28)
2304
+ transparent;
2305
+
2306
+ &::-webkit-scrollbar {
2307
+ width: 3px;
2308
+ }
2309
+
2310
+ &::-webkit-scrollbar-thumb {
2311
+ background: rgba(var(--tertiary-color-rgb, 255, 255, 255), 0.28);
2312
+ border-radius: 3px;
2313
+ }
2314
+
2315
+ /* The page behind the drawer is frozen by a scroll lock on <body>,
2316
+ which iOS Safari does not honour for the viewport scroller. Without
2317
+ this, a swipe that reaches either end of the list — and on first
2318
+ open the list IS at its top — chains into the page underneath and
2319
+ the drawer reads as a menu that will not move. */
2320
+ overscroll-behavior: contain;
2321
+ -webkit-overflow-scrolling: touch;
2322
+
2323
+ > ul {
2324
+ margin: 0;
2325
+ padding: 0;
2326
+ list-style: none;
2327
+ }
2328
+
2329
+ /* The same gutter `.app-nav .nav-item` and `.rail-actions > ul > li`
2330
+ use at this width, so the three bands of the drawer line up. */
2331
+ > ul > li {
2332
+ width: 100%;
2333
+ padding: 0.2rem 0.65rem;
2334
+ box-sizing: border-box;
2335
+ }
2336
+
2337
+ /* A row: full width, one line, a finger-sized target. The pill is
2338
+ pushed to the trailing edge rather than butted against the label —
2339
+ the labels here are of wildly different lengths and an inline badge
2340
+ would land in a different place on every line.
2341
+
2342
+ THERE IS NO HOVER RULE ANYWHERE IN THIS DRAWER, deliberately. On the
2343
+ devices it exists for, `:hover` is whatever was tapped last and stays
2344
+ lit until something else is tapped — a row that looks selected
2345
+ because you scrolled past it. The current page is marked by `.active`
2346
+ below, which is a fact rather than an artefact.
2347
+
2348
+ `font-size` is stated rather than inherited for the reason every list
2349
+ in this house states it: global.css carries
2350
+ `ul { font-size: clamp(1rem, 2vw, 1.25rem) }` and an element selector
2351
+ beats a class. */
2352
+ a {
2353
+ display: flex;
2354
+ flex-wrap: nowrap;
2355
+ align-items: center;
2356
+ justify-content: space-between;
2357
+ gap: 0.5rem;
2358
+ width: 100%;
2359
+ box-sizing: border-box;
2360
+ padding: 0.85rem 1rem;
2361
+ line-height: 1.3;
2362
+ color: var(--tertiary-color, #fff);
2363
+ font-size: 1rem;
2364
+ font-weight: var(--font-weight-medium, 500);
2365
+ text-decoration: none;
2366
+ border-radius: var(--br, 5px);
2367
+
2368
+ &:focus-visible {
2369
+ outline: 2px solid
2370
+ var(--nav-active-bar-color, var(--highlight-color, #fff));
2371
+ outline-offset: -2px;
2372
+ }
2373
+ }
2374
+
2375
+ a.active {
2376
+ background: rgba(var(--tertiary-color-rgb, 255, 255, 255), 0.12);
2377
+ font-weight: var(--font-weight-semibold, 600);
2378
+ }
2379
+ }
2380
+
2381
+ /* A group's NAME, and not a control: it has no page of its own, and it has
2382
+ nothing left to disclose because its rows are already open beneath it. So
2383
+ it takes the section-label voice — small, quiet, uppercase — and no tap
2384
+ target at all. A <button> that opens a dropdown is precisely what does
2385
+ not work on a touch screen, and is the reason this drawer exists. */
2386
+ .drawer-heading {
2387
+ display: block;
2388
+ padding: 1rem 1rem 0.35rem;
2389
+ color: rgba(var(--tertiary-color-rgb, 255, 255, 255), 0.5);
2390
+ font-size: 0.72rem;
2391
+ font-weight: 600;
2392
+ letter-spacing: 0.06em;
2393
+ text-transform: uppercase;
2394
+ }
2395
+
2396
+ /* Indented under the heading that names it, with a rule down the left so
2397
+ the grouping survives being scrolled past its own heading. */
2398
+ .drawer-child > a {
2399
+ padding-left: 1.75rem;
2400
+ border-left: 2px solid
2401
+ rgba(var(--tertiary-color-rgb, 255, 255, 255), 0.12);
2402
+ border-radius: 0 var(--br, 5px) var(--br, 5px) 0;
2403
+ }
2404
+ }
2405
+
2406
+ /* ----------------------------------------------------------------------------
2407
+ A drawer shorter than its own footer
2408
+ ----------------------------------------------------------------------------
2409
+ The pinned footer is `flex: 0 0 auto` — it takes its content height and the
2410
+ list takes what is left, which is the rail's arrangement and the right one
2411
+ while there IS anything left. A phone in LANDSCAPE is 393px tall: the panel
2412
+ is that minus the bar, and six labelled action rows are taller than all of
2413
+ it. The list is then squeezed to zero and the navigation — the entire reason
2414
+ the drawer exists — is not reachable by any gesture.
2415
+
2416
+ So below this height the drawer stops being two bands and becomes one
2417
+ scroller: the list and the actions scroll together, everything is reachable,
2418
+ and the footer is simply the end of the list. The cost is that the panels
2419
+ those action rows open are clipped by the new scroll container, and it is
2420
+ the right cost — a 350px-tall popover has nowhere to open on a 393px screen
2421
+ whether or not anything clips it.
2422
+ ------------------------------------------------------------------------- */
2423
+ @media (max-width: 1024px) and (max-height: 600px) {
2424
+ .hwrap .aside--drawer {
2425
+ overflow-y: auto;
2426
+ overscroll-behavior: contain;
2427
+ -webkit-overflow-scrolling: touch;
2428
+ }
2429
+
2430
+ .drawer-nav {
2431
+ flex: 0 0 auto;
2432
+ overflow: visible;
2433
+ }
2434
+ }
2435
+
2436
+ /* The mark, on a phone. 130px of logo beside a hamburger and three chips is the
2437
+ difference between the bar fitting a 390px screen and not, and a mark is the
2438
+ one thing in the bar that is still recognisable at two thirds the size.
2439
+ `min()` so this can only ever SHRINK: a project whose --logo-width is already
2440
+ narrower than the cap keeps its own figure. The logo's own gutter tightens
2441
+ with it — a hairline with 16px either side reads as a gap, not a divider,
2442
+ once everything around it has closed up. */
2443
+ @media (max-width: 640px) {
2444
+ .hwrap .logo {
2445
+ width: min(var(--logo-width, 130px), var(--nav-logo-width-sm, 92px));
2446
+ margin-right: var(--spacing-sm, 0.5rem);
2447
+ padding-right: var(--spacing-sm, 0.5rem);
2448
+ }
2449
+ }
@@ -967,6 +967,77 @@ $sms-mono: 'SFMono-Regular', 'Menlo', 'Consolas', monospace;
967
967
  font-size: 0.75rem; // 12px at a 16px root
968
968
  }
969
969
 
970
+ // ---------------------------------------------------------------------------
971
+ // Mounted in the sidebar rail, and in the mobile drawer
972
+ // ---------------------------------------------------------------------------
973
+ // `actionsPlacement: 'sidebar'` — and, below 1024px, the header layout's
974
+ // drawer — draws the account actions as full-width rows instead of chips. The
975
+ // block that holds them advertises itself with `data-nav-actions="sidebar"`
976
+ // (Navigation.jsx / Navigation.module.scss `.rail-actions`), which is all this
977
+ // file needs to know. Notification.module.scss carries the same block for the
978
+ // bell; this is the copy for the inbox badge, and it exists because a chip and
979
+ // a row are two different things in three places.
980
+ //
981
+ // The row's box — full width, the rail's padding, left-aligned, and the LABEL
982
+ // beside the icon — all come from `.rail-actions`. The trigger publishes no
983
+ // label of its own, so the row publishes one as `--nav-action-label` and the
984
+ // rail hangs it off the trigger's `::after`. Only what this file would
985
+ // otherwise force back is restated here.
986
+ [data-nav-actions='sidebar'] {
987
+ // The chip's lift and scale read as a stray button in a list of rows.
988
+ // The focus ring is left alone — it is the only keyboard affordance the
989
+ // row has.
990
+ .trigger {
991
+ &:hover,
992
+ &:focus,
993
+ &:active {
994
+ transform: none;
995
+ }
996
+
997
+ &:hover {
998
+ background-color: rgba(var(--tertiary-color-rgb, 255, 255, 255), 0.05);
999
+ color: var(--secondary-color);
1000
+ }
1001
+ }
1002
+
1003
+ // Was pinned to the corner of a round 40px chip, which on a full-width
1004
+ // row is the row's top-right corner, floating clear of the count it
1005
+ // belongs to. At the end of the line is where the eye lands after the
1006
+ // label.
1007
+ .badge {
1008
+ top: 50%;
1009
+ right: 14px;
1010
+ transform: translateY(-50%);
1011
+ }
1012
+
1013
+ // `top: 100%` drops the popover below the row. These rows are pinned to
1014
+ // the FOOT of the rail, so that is below the foot of the viewport.
1015
+ // Beside the rail, bottom-aligned with the row that opened it.
1016
+ .popoverWrapper {
1017
+ top: auto;
1018
+ right: auto;
1019
+ bottom: 0;
1020
+ left: calc(100% + 10px);
1021
+ margin-top: 0;
1022
+ }
1023
+
1024
+ @media (max-width: 1024px) {
1025
+ // Inside the drawer there is nothing to the right, so the panel
1026
+ // opens upward from the row.
1027
+ .popoverWrapper {
1028
+ left: 0;
1029
+ bottom: calc(100% + 8px);
1030
+ }
1031
+
1032
+ // Keeps the panel inside a phone's viewport when the list is long —
1033
+ // it grows from the foot of the drawer towards the header, and 360px
1034
+ // of rows plus a head and a footer is taller than the room above.
1035
+ .rows {
1036
+ max-height: 40vh;
1037
+ }
1038
+ }
1039
+ }
1040
+
970
1041
  // ---------------------------------------------------------------------------
971
1042
  // SmsInbox — the page
972
1043
  // ---------------------------------------------------------------------------
@@ -1703,6 +1774,27 @@ $sms-mono: 'SFMono-Regular', 'Menlo', 'Consolas', monospace;
1703
1774
  background: var(--s-surface);
1704
1775
  }
1705
1776
 
1777
+ // Stands exactly where the composer does on a receive-only thread - one from an
1778
+ // alphanumeric sender ID, which has no handset behind it to answer. The pane
1779
+ // keeps its footer and its height; what changes is that the footer explains
1780
+ // itself instead of offering a control that could not work.
1781
+ .receiveOnly {
1782
+ box-sizing: border-box;
1783
+ padding: 0.6rem 0.7rem 0.7rem;
1784
+ border-top: 1px solid var(--s-line);
1785
+ background: var(--s-surface);
1786
+
1787
+ .banner {
1788
+ // The sentence wraps on a narrow pane, so the icon sits with the first
1789
+ // line rather than floating in the middle of a two-line block.
1790
+ align-items: flex-start;
1791
+
1792
+ svg {
1793
+ margin-top: 0.15rem;
1794
+ }
1795
+ }
1796
+ }
1797
+
1706
1798
  // Stands exactly where the composer does while the timeline is being picked
1707
1799
  // over, so the pane keeps its height and its one footer.
1708
1800
  .selectBar {