@smwb/ui-styles 2.1.0 → 2.3.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/README.md CHANGED
@@ -101,6 +101,16 @@ tokens on its own root, so a value set on `:root` or a wrapper does not reach it
101
101
  the matching `--smui-*Color` token (`default` is `--smui-onSurfaceVariant`). Its sizes are `@statusDotSizeSm`
102
102
  (8px), `@statusDotSizeMd` (12px) and `@statusDotRing` (2px, the ring of `unknown`) in `theme.config`.
103
103
 
104
+ `shape="diamond"` reuses every one of those tokens: the colour and the ring are unchanged, and only the box is
105
+ derived. A square turned 45deg spans side × √2, so the diamond's side is the size over `@statusDotDiamondRatio`
106
+ (√2) — 8.4853px at `md`, 5.6569px at `sm` — which lands its bounding box back on the dot's 12px and 8px. A transform leaves the layout box at the unturned
107
+ side, so the rule adds a `margin` of half of (size − side) from the same tokens, and a label beside the diamond
108
+ stays where it is beside the dot. The turn
109
+ is `@statusDotDiamondRotation` (45deg) and the corners `@statusDotDiamondRadius` (`@smui-radius-none`), because the
110
+ dot's `border-radius: 50%` would otherwise survive the turn as a circle. Turning the box rather than clipping a
111
+ full-size one is what keeps `unknown` a real `@statusDotRing` border and lets the pulse's `::after` turn with its
112
+ parent. None of the three is a `--smui-*` runtime token, matching the dot's own sizes, which are compile-time too.
113
+
104
114
  Against `--smui-surfaceBackground` and `--smui-pageBackground`, the default theme's status colours measure
105
115
  at least 3:1 in both modes (the lowest is `attention` on the light surface, 3.04:1). The semantic colours are one
106
116
  value for both modes, so a theme that makes `@warningColor` lighter, or any of them darker for the dark theme,
@@ -122,16 +132,18 @@ it, and the quiet zone keeps the modules clear of the rounding.
122
132
  The dependency graph (`DagView` in `@smwb/ui-solid` and `@smwb/ui-react`) takes its colours from theme variables that default to the
123
133
  runtime tokens, so the dark theme follows without extra settings:
124
134
 
125
- | Variable (`theme.config`) | Runtime property on `.smwb-dag-view` | Default |
126
- | ---------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------- |
127
- | `@dagViewNodeBg` | `--smwb-dag-view-node-bg` | `var(--smui-surfaceBackground)` |
128
- | `@dagViewNodeBorder` | `--smwb-dag-view-node-border` | `var(--smui-outlineColor)` |
129
- | `@dagViewEdgeColor` | `--smwb-dag-view-edge-color` | `var(--smui-outlineColor)` |
130
- | `@dagViewEdgeActiveColor` | `--smwb-dag-view-edge-active-color` | `var(--smui-primaryColor)` |
131
- | `@dagViewSelectedRing` | `--smwb-dag-view-selected-ring` | `var(--smui-primaryColor)` |
132
- | `@dagViewStatusIdle` … `@dagViewStatusSkipped` | `--smwb-dag-view-status-idle` … `-skipped` | outline, on-surface variant, info, success, warning, error, disabled text |
133
- | `@dagViewDimOpacity` | `--smwb-dag-view-dim-opacity` | `0.35` |
134
- | `@dagViewHeight` | — (the graph's `height`) | `480px` |
135
+ | Variable (`theme.config`) | Runtime property on `.smwb-dag-view` | Default |
136
+ | ---------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------- |
137
+ | `@dagViewNodeBg` | `--smwb-dag-view-node-bg` | `var(--smui-surfaceBackground)` |
138
+ | `@dagViewNodeBorder` | `--smwb-dag-view-node-border` | `var(--smui-outlineColor)` |
139
+ | `@dagViewNodeBorderStyle` | `--smwb-dag-view-node-border-style` | `solid` |
140
+ | `@dagViewStatusPendingBorderStyle` … `Skipped` | `--smwb-dag-view-status-pending-border-style` … `-skipped-border-style` | `false`: the node border style |
141
+ | `@dagViewEdgeColor` | `--smwb-dag-view-edge-color` | `var(--smui-outlineColor)` |
142
+ | `@dagViewEdgeActiveColor` | `--smwb-dag-view-edge-active-color` | `var(--smui-primaryColor)` |
143
+ | `@dagViewSelectedRing` | `--smwb-dag-view-selected-ring` | `var(--smui-primaryColor)` |
144
+ | `@dagViewStatusIdle` … `@dagViewStatusSkipped` | `--smwb-dag-view-status-idle` … `-skipped` | outline, on-surface variant, info, success, warning, error, disabled text |
145
+ | `@dagViewDimOpacity` | `--smwb-dag-view-dim-opacity` | `0.35` |
146
+ | `@dagViewHeight` | — (the graph's `height`) | `480px` |
135
147
 
136
148
  The statuses are `idle`, `pending`, `running`, `success`, `warning`, `error` and `skipped`; a node with any other
137
149
  status is drawn as `idle`. Each colour variable has a `…Dark` twin (e.g. `@dagViewStatusSuccessDark`), `false` by
@@ -144,6 +156,13 @@ node's accent comes from `--smwb-dag-view-accent`. For a status of your own, set
144
156
  The graph's default height, `@dagViewHeight`, is set by the stylesheet, so its `height` prop or a height in its
145
157
  `style` wins over it.
146
158
 
159
+ The node frame's line style is `@dagViewNodeBorderStyle` (`solid`): a theme that sets it to `dashed` dashes every
160
+ node. To dash only some nodes, set `@dagViewStatusWarningBorderStyle: dashed` (a status variable per built-in status
161
+ except `idle`; `false`, the default, follows the theme-wide style), or give a node `variant: "dashed"` (`"solid"`
162
+ keeps it solid under a dashed theme): the node's own variant outranks its status, and the status outranks the theme.
163
+ One `border-style` draws all four sides, so the status stripe on the left dashes too and the node reads as one
164
+ frame, not a frame inside a frame. The border style has no `…Dark` twin: it is not a colour.
165
+
147
166
  #### Swimlanes
148
167
 
149
168
  `Swimlanes` (in `@smwb/ui-react` and `@smwb/ui-solid`) reads only runtime tokens, so the dark theme follows without
@@ -123,35 +123,57 @@
123
123
  calc(@appBarPaddingX + var(--smwb-appbar-wc-left));
124
124
  }
125
125
  }
126
+ // The bar is a flex row itself: without `min-width: 0` the inner row would not shrink below its content
127
+ // and a long title would push the right group past the bar's edge.
126
128
  &__inner {
127
129
  display: flex;
128
130
  align-items: center;
129
131
  width: 100%;
132
+ min-width: 0;
130
133
  justify-content: space-between;
131
134
  flex-wrap: nowrap;
132
135
  }
136
+ // Room is given out in this order: the right group keeps its content size, the middle takes what is
137
+ // left over (and is the first to give it up), and only when even that is gone the title gives way.
133
138
  &__left {
134
139
  display: flex;
135
140
  align-items: center;
136
141
  gap: @appBarActionsGap;
137
142
  white-space: nowrap;
143
+ flex: 0 1 auto;
144
+ min-width: 0;
138
145
  }
139
146
  // The title may be a heading (`titleAs`): drop the UA margins and font so it
140
- // looks the same as the div.
147
+ // looks the same as the div. When the bar is too narrow for it, it is cut with an ellipsis.
148
+ // clip with a margin, not hidden: a control in the title (a link, a button) keeps its focus ring, which
149
+ // paints outside the box. `hidden` stays first for engines that know no `overflow: clip`.
141
150
  &__left__title {
142
151
  margin: 0;
143
152
  font: inherit;
153
+ min-width: 0;
154
+ overflow: hidden;
155
+ overflow: clip;
156
+ overflow-clip-margin: @appBarClipMargin;
157
+ text-overflow: ellipsis;
144
158
  }
145
159
  &__right {
146
160
  white-space: nowrap;
161
+ flex: 0 0 auto;
147
162
  }
163
+ // The basis is 0, not `auto` with `width: 100%`: the middle grows into the free space instead of
164
+ // claiming the whole bar as its basis and taking its share of a shortfall from the title and the right group.
148
165
  &__mid {
149
- width: 100%;
150
166
  display: flex;
151
- flex: 1 1 auto;
167
+ flex: 1 1 0;
168
+ min-width: 0;
152
169
  flex-direction: column;
153
170
  flex-wrap: nowrap;
154
171
 
172
+ // A field wider than the room left to the middle narrows with it instead of running under the right group.
173
+ > * {
174
+ max-width: 100%;
175
+ }
176
+
155
177
  &.smwb-middle {
156
178
  align-items: center;
157
179
  }
@@ -37,6 +37,13 @@
37
37
  border-radius: 0;
38
38
  }
39
39
 
40
+ // An image is drawn on the page, not on the fill: a transparent PNG/SVG shows
41
+ // what is behind the avatar. The fill stays under the fallback (initials, an
42
+ // icon), also after the image failed to load: the class goes with the image.
43
+ &_image {
44
+ background-color: transparent;
45
+ }
46
+
40
47
  &__img {
41
48
  width: 100%;
42
49
  height: 100%;
@@ -54,4 +54,65 @@
54
54
  background-color: var(--smui-onSurfaceActive);
55
55
  }
56
56
  }
57
+
58
+ // One line: the list never wraps, the crumbs shrink and cut their text with an
59
+ // ellipsis, the leading ones first and down to a floor, and the trailing crumb
60
+ // (the page's own title) only after them. Below the sum of the floors the trail
61
+ // keeps them and runs past its box instead of squeezing a crumb to nothing.
62
+ &--no-wrap {
63
+ min-width: 0;
64
+
65
+ .smwb-breadcrumbs__list {
66
+ flex-wrap: nowrap;
67
+ }
68
+
69
+ // `text-overflow` reaches only the inline text of the box it sits on, so a block child (a
70
+ // heading, Typography) is cut by its own rule, and the crumb's own cuts what is inline in it.
71
+ // Clip with a margin, not hidden: a link or the expand button keeps its focus ring, which
72
+ // paints outside the box. `hidden` stays first for engines that know no `overflow: clip`.
73
+ .smwb-breadcrumbs__item,
74
+ .smwb-breadcrumbs__item > * {
75
+ min-width: 0;
76
+ overflow: hidden;
77
+ overflow: clip;
78
+ overflow-clip-margin: @breadcrumbsClipMargin;
79
+ text-overflow: ellipsis;
80
+ white-space: nowrap;
81
+ }
82
+
83
+ .smwb-breadcrumbs__item {
84
+ display: block;
85
+ flex: 0 1 auto;
86
+ }
87
+
88
+ .smwb-breadcrumbs__item > * {
89
+ max-width: 100%;
90
+ }
91
+
92
+ // A link is a box of its own, so it is cut there and its focus ring wraps what is shown
93
+ // instead of the whole text running out of the crumb.
94
+ .smwb-breadcrumbs__item > a {
95
+ display: inline-block;
96
+ vertical-align: top;
97
+ }
98
+
99
+ .smwb-breadcrumbs__item:not(:last-child) {
100
+ flex-shrink: @breadcrumbsLeadingShrink;
101
+ min-width: @breadcrumbsLeadingMinWidth;
102
+ }
103
+
104
+ .smwb-breadcrumbs__item:last-child {
105
+ min-width: @breadcrumbsLastMinWidth;
106
+ }
107
+
108
+ // The expand button is not text: it keeps its size instead of taking the floor.
109
+ .smwb-breadcrumbs__item:has(> .smwb-breadcrumbs__expand) {
110
+ flex: none;
111
+ min-width: 0;
112
+ }
113
+
114
+ .smwb-breadcrumbs__separator {
115
+ flex: none;
116
+ }
117
+ }
57
118
  }
@@ -46,6 +46,36 @@
46
46
  min-height: var(--smui-control-h-lg, @smui-control-h-lg);
47
47
  }
48
48
 
49
+ // Icon-only: a square of the size's control height. The width and the height both come from the
50
+ // same token, the padding is gone and min-width with it, so the icon sits in the middle and
51
+ // the box is the same for every variant and for a link. It wins over full-width on purpose.
52
+ // border-box: a <button> has it from the user agent, an <a> only from a reset, and the outlined
53
+ // border would otherwise add 2px to the link. The corners keep the button's own radius, capped
54
+ // by @buttonIconOnlyMaxBorderRadius so a pill button does not turn into a circle unless the theme asks.
55
+ &.smwb-button_icon-only {
56
+ box-sizing: border-box;
57
+ min-width: 0;
58
+ padding: 0;
59
+ flex: none;
60
+ border-radius: min(@buttonBorderRadius, @buttonIconOnlyMaxBorderRadius);
61
+ &:focus-visible {
62
+ outline: 2px solid var(--smui-primaryColor);
63
+ outline-offset: 2px;
64
+ }
65
+ &.smwb-small {
66
+ width: var(--smui-control-h-sm, @smui-control-h-sm);
67
+ height: var(--smui-control-h-sm, @smui-control-h-sm);
68
+ }
69
+ &.smwb-medium {
70
+ width: var(--smui-control-h-md, @smui-control-h-md);
71
+ height: var(--smui-control-h-md, @smui-control-h-md);
72
+ }
73
+ &.smwb-large {
74
+ width: var(--smui-control-h-lg, @smui-control-h-lg);
75
+ height: var(--smui-control-h-lg, @smui-control-h-lg);
76
+ }
77
+ }
78
+
49
79
  &.smwb-contained {
50
80
  color: var(--smui-onPrimaryColor);
51
81
  .smui-elevation(2);
@@ -22,7 +22,8 @@
22
22
  box-shadow: none;
23
23
  }
24
24
 
25
- .smwb-horizontal & {
25
+ // Bound to the group itself: ScreenDivider also puts a bare orientation class on its wrapper and panes.
26
+ .smwb-button-group.smwb-horizontal > & {
26
27
  &:not(:first-of-type) {
27
28
  border-top-left-radius: 0;
28
29
  border-bottom-left-radius: 0;
@@ -57,7 +58,7 @@
57
58
  }
58
59
  }
59
60
 
60
- .smwb-vertical & {
61
+ .smwb-button-group.smwb-vertical > & {
61
62
  &:not(:first-of-type) {
62
63
  border-top-right-radius: 0;
63
64
  border-top-left-radius: 0;
@@ -134,6 +134,17 @@
134
134
  min-height: 24px;
135
135
  padding: 0 @smui-space-sm;
136
136
  font-size: 0.75rem;
137
+
138
+ // On a coarse pointer a small chip that is a button or a link is as tall as the sm step
139
+ // (44px there): its content is the button (the link is the box itself), and the box is
140
+ // what a finger has to hit. A small chip that only labels, or only deletes, stays 24px.
141
+ @media (pointer: coarse) {
142
+ &:is(a[href]),
143
+ &:has(> .smwb-chip__content[role="button"]) {
144
+ height: var(--smui-control-h-sm, @smui-control-h-sm-coarse);
145
+ min-height: var(--smui-control-h-sm, @smui-control-h-sm-coarse);
146
+ }
147
+ }
137
148
  }
138
149
 
139
150
  // A color repaints the neutral filled/outlined rules above. The guard mirrors
@@ -22,11 +22,24 @@
22
22
  .smwb-dag-view-token(status-error; StatusError; @suffix);
23
23
  .smwb-dag-view-token(status-skipped; StatusSkipped; @suffix);
24
24
  }
25
- // A built-in status: its accent, unless the node sets its own.
25
+ // The line style of the node frame, theme-wide and per built-in status. The status
26
+ // ones are `false` by default: they emit nothing, so the node falls back to the
27
+ // theme-wide one.
28
+ .smwb-dag-view-border-style(@status; @var) {
29
+ @full: ~"dagViewStatus@{var}BorderStyle";
30
+ .smwb-dag-view-border-style-value(@status; @@full);
31
+ }
32
+ .smwb-dag-view-border-style-value(@status; @value) when not (@value = false) {
33
+ --smwb-dag-view-status-@{status}-border-style: @value;
34
+ }
35
+ // A built-in status: its accent, unless the node sets its own, and its frame line
36
+ // style, unless the theme gives it none.
26
37
  .smwb-dag-view-status(@status) {
27
38
  @accent: ~"var(--smwb-dag-view-node-accent, var(--smwb-dag-view-status-@{status}))";
39
+ @style: ~"var(--smwb-dag-view-status-@{status}-border-style, var(--smwb-dag-view-node-border-style))";
28
40
  &[data-status="@{status}"] {
29
41
  --smwb-dag-view-accent: @accent;
42
+ --smwb-dag-view-border-style: @style;
30
43
  }
31
44
  }
32
45
 
@@ -35,6 +48,13 @@
35
48
  .smwb-dag-view {
36
49
  .smwb-dag-view-tokens("");
37
50
  --smwb-dag-view-dim-opacity: @dagViewDimOpacity;
51
+ --smwb-dag-view-node-border-style: @dagViewNodeBorderStyle;
52
+ .smwb-dag-view-border-style(pending; Pending);
53
+ .smwb-dag-view-border-style(running; Running);
54
+ .smwb-dag-view-border-style(success; Success);
55
+ .smwb-dag-view-border-style(warning; Warning);
56
+ .smwb-dag-view-border-style(error; Error);
57
+ .smwb-dag-view-border-style(skipped; Skipped);
38
58
 
39
59
  position: relative;
40
60
  width: 100%;
@@ -120,6 +140,10 @@
120
140
  .smwb-dag-view-status(warning);
121
141
  .smwb-dag-view-status(error);
122
142
  .smwb-dag-view-status(skipped);
143
+ // The frame's line style; a node's own `variant` outranks the status and the theme.
144
+ // One `border-style` draws all four sides, the stripe included, so a dashed node
145
+ // is one frame, not a frame inside a frame.
146
+ --smwb-dag-view-border-style: var(--smwb-dag-view-node-border-style);
123
147
 
124
148
  position: absolute;
125
149
  box-sizing: border-box;
@@ -130,6 +154,7 @@
130
154
  border: 1px solid var(--smwb-dag-view-node-border);
131
155
  // The status stripe.
132
156
  border-left: 4px solid var(--smwb-dag-view-accent);
157
+ border-style: var(--smwb-dag-view-border-style);
133
158
  border-radius: @smui-radius-md;
134
159
  background-color: var(--smwb-dag-view-node-bg);
135
160
  color: var(--smui-onSurfaceColor);
@@ -143,6 +168,14 @@
143
168
  border-color 0.12s,
144
169
  opacity 0.12s;
145
170
 
171
+ &[data-variant="solid"] {
172
+ --smwb-dag-view-border-style: solid;
173
+ }
174
+
175
+ &[data-variant="dashed"] {
176
+ --smwb-dag-view-border-style: dashed;
177
+ }
178
+
146
179
  &:hover {
147
180
  border-color: color-mix(in srgb, var(--smwb-dag-view-edge-active-color) 45%, var(--smwb-dag-view-node-border));
148
181
  border-left-color: var(--smwb-dag-view-accent);
@@ -62,6 +62,9 @@
62
62
  min-height: 36px;
63
63
  padding-top: @smui-space-xs;
64
64
  padding-bottom: @smui-space-xs;
65
+ @media (pointer: coarse) {
66
+ min-height: max(36px, var(--smui-control-h-sm, @smui-control-h-sm-coarse));
67
+ }
65
68
  }
66
69
 
67
70
  &_divider {
@@ -73,6 +76,10 @@
73
76
  min-height: 36px;
74
77
  padding-top: @smui-space-xs;
75
78
  padding-bottom: @smui-space-xs;
79
+ // On a coarse pointer a dense row is the sm step (44px there) at least.
80
+ @media (pointer: coarse) {
81
+ min-height: max(36px, var(--smui-control-h-sm, @smui-control-h-sm-coarse));
82
+ }
76
83
  }
77
84
 
78
85
  // A row that is a link: the anchor takes the padding, so the whole row is the link.
@@ -50,6 +50,11 @@
50
50
 
51
51
  &--dense {
52
52
  height: 32px;
53
+
54
+ // On a coarse pointer a dense item is the sm step (44px there) at least.
55
+ @media (pointer: coarse) {
56
+ height: max(32px, var(--smui-control-h-sm, @smui-control-h-sm-coarse));
57
+ }
53
58
  }
54
59
 
55
60
  &--disabled {
@@ -40,10 +40,44 @@
40
40
  color: var(--smui-onSurfaceVariant);
41
41
  }
42
42
 
43
+ // The text before the bar: it takes the first column, the bar the flexible one.
44
+ &_text-before {
45
+ grid-template-columns: auto minmax(@meterMinBarWidth, 1fr);
46
+
47
+ .smwb-meter__text {
48
+ order: -1;
49
+ }
50
+ }
51
+
52
+ // Compact: a fixed-width bar and small secondary text; the block shrinks to its
53
+ // content instead of filling the parent.
54
+ &_compact {
55
+ grid-template-columns: @meterCompactBarWidth auto;
56
+ width: fit-content;
57
+ max-width: 100%;
58
+ font-size: 0.75rem;
59
+
60
+ .smwb-meter__track {
61
+ height: @meterCompactHeight;
62
+ }
63
+
64
+ .smwb-meter__text {
65
+ color: var(--smui-onSurfaceVariant);
66
+ }
67
+ }
68
+
69
+ &_compact&_text-before {
70
+ grid-template-columns: auto @meterCompactBarWidth;
71
+ }
72
+
43
73
  &_bar-only {
44
74
  grid-template-columns: minmax(@meterMinBarWidth, 1fr);
45
75
  }
46
76
 
77
+ &_compact&_bar-only {
78
+ grid-template-columns: @meterCompactBarWidth;
79
+ }
80
+
47
81
  &_color-neutral {
48
82
  --smwb-meter-color: var(--smui-onSurfaceVariant);
49
83
  }
@@ -6,7 +6,8 @@
6
6
  overflow: hidden;
7
7
  &.smwb-vertical {
8
8
  flex-direction: column;
9
- .smwb-screen-divider__resizer {
9
+ // A child, not a descendant: a divider nested in a pane keeps its own orientation's resizer.
10
+ > .smwb-screen-divider__resizer {
10
11
  border-top: thin solid var(--smui-primaryColor);
11
12
  border-bottom: thin solid var(--smui-primaryColor);
12
13
  border-left: none;
@@ -42,15 +43,22 @@
42
43
  content: "......";
43
44
  text-align: center;
44
45
  }
46
+ // A collapsed divider has no bar to see or drag: the other pane takes the whole wrapper.
47
+ &[hidden] {
48
+ display: none;
49
+ }
45
50
  }
46
- &__first {
47
- min-height: 5px;
48
- min-width: 5px;
49
- flex-shrink: 1;
50
- }
51
+ &__first,
51
52
  &__second {
52
53
  min-height: 5px;
53
54
  min-width: 5px;
54
55
  flex-shrink: 1;
56
+ // A folded pane stays mounted but has no size, shows nothing and takes no focus.
57
+ &.smwb-collapsed {
58
+ min-height: 0;
59
+ min-width: 0;
60
+ overflow: hidden;
61
+ visibility: hidden;
62
+ }
55
63
  }
56
64
  }
@@ -1,4 +1,9 @@
1
1
  .smwb-sheet {
2
+ // Gap between the top edge of the window and the top of the sheet and of its modal scrim. `Sheet`
3
+ // sets it from its `offsetTop` prop and `AppLayout` publishes its sticky header's height as the
4
+ // page default, so a panel starts below the header; with neither it is 0 and nothing moves.
5
+ @sheetOffsetTop: var(--smui-sheet-offset-top, 0px);
6
+
2
7
  background-color: var(--smui-surfaceBackground);
3
8
  position: fixed;
4
9
  opacity: 0;
@@ -129,10 +134,12 @@
129
134
 
130
135
  &.smwb-top {
131
136
  border-radius: 0 0 @sheetBorderRadius @sheetBorderRadius;
132
- top: 0;
137
+ top: @sheetOffsetTop;
133
138
  left: 0;
134
139
  width: 100%;
135
140
  height: var(--sizeProp);
141
+ // A tall panel stops at the bottom of the window instead of running past it by the offset.
142
+ max-height: calc(100% - @sheetOffsetTop);
136
143
  transform: translateY(-100%);
137
144
  flex-direction: column;
138
145
  .smwb-sheet__header {
@@ -142,9 +149,9 @@
142
149
 
143
150
  &.smwb-left {
144
151
  border-radius: 0 @sheetBorderRadius @sheetBorderRadius 0;
145
- top: 0;
152
+ top: @sheetOffsetTop;
146
153
  left: 0;
147
- height: 100%;
154
+ height: calc(100% - @sheetOffsetTop);
148
155
  width: var(--sizeProp);
149
156
  transform: translateX(-100%);
150
157
  flex-direction: row-reverse;
@@ -157,9 +164,9 @@
157
164
 
158
165
  &.smwb-right {
159
166
  border-radius: @sheetBorderRadius 0 0 @sheetBorderRadius;
160
- top: 0;
167
+ top: @sheetOffsetTop;
161
168
  right: 0;
162
- height: 100%;
169
+ height: calc(100% - @sheetOffsetTop);
163
170
  width: var(--sizeProp);
164
171
  transform: translateX(100%);
165
172
  .smwb-sheet__header {
@@ -170,15 +177,18 @@
170
177
 
171
178
  &__overlay {
172
179
  position: fixed;
173
- top: 0;
180
+ top: @sheetOffsetTop;
174
181
  left: 0;
175
182
  width: 100%;
176
- height: 100%;
183
+ height: calc(100% - @sheetOffsetTop);
177
184
  display: flex;
178
185
  flex-direction: column;
179
186
  z-index: 100;
180
187
  opacity: 0;
181
188
  transition: all 0.25s ease-in-out;
189
+ // The panel slides in from beyond an edge of the scrim; clipped to the scrim, a panel sliding down from the top
190
+ // does not cross the header above it.
191
+ clip-path: inset(0);
182
192
  &.smwb-entered {
183
193
  opacity: 1;
184
194
  }
@@ -18,6 +18,31 @@
18
18
  height: @statusDotSizeSm;
19
19
  }
20
20
 
21
+ // A diamond is the same marker turned corner-up. Its side is the dot's size over
22
+ // sqrt(2), so the turned square spans exactly the dot's box and the two shapes sit in
23
+ // one another's place. Turning the box, rather than clipping a full-size one, is what
24
+ // keeps the statuses readable: the `unknown` ring stays a real border of
25
+ // @statusDotRing, and the pulse's ::after turns with its parent and grows as a diamond.
26
+ // A clip-path would cut that border into four corner slivers and would need a second
27
+ // element to draw the ring, which this empty <span> does not have.
28
+ // A transform does not change layout, so the smaller square alone would leave a row
29
+ // short of the dot's size: the margin gives the layout box the difference back, half on
30
+ // each side, so a label beside the diamond sits where it sits beside the dot.
31
+ &_shape-diamond {
32
+ @side: round((@statusDotSizeMd / @statusDotDiamondRatio), @statusDotDiamondPrecision);
33
+ width: @side;
34
+ height: @side;
35
+ margin: round(((@statusDotSizeMd - @side) / 2), @statusDotDiamondPrecision);
36
+ border-radius: @statusDotDiamondRadius;
37
+ transform: rotate(@statusDotDiamondRotation);
38
+ }
39
+ &_shape-diamond&_size-sm {
40
+ @side: round((@statusDotSizeSm / @statusDotDiamondRatio), @statusDotDiamondPrecision);
41
+ width: @side;
42
+ height: @side;
43
+ margin: round(((@statusDotSizeSm - @side) / 2), @statusDotDiamondPrecision);
44
+ }
45
+
21
46
  &_status-active {
22
47
  --smwb-status-dot-color: var(--smui-infoColor);
23
48
  }
@@ -29,24 +29,77 @@
29
29
  box-shadow: inset 0 @tabGroupsActiveLineWidth 0 0 var(--smui-primaryColor);
30
30
  }
31
31
 
32
+ // From a basis of 0 the tabs take only what the header has left: they give way and scroll first,
33
+ // and the strip content shrinks only once they are gone.
32
34
  &__tabs {
33
- flex: 1 1 auto;
35
+ flex: 1 1 0;
34
36
  min-width: 0;
35
37
  }
36
38
 
37
- // The group's actions button: the box of Tabs' overflow trigger, which it sits beside.
39
+ // What the app puts at the end of the tab row (`stripActions`). It keeps its size while the tabs
40
+ // can give way, then is clipped at its end, so the group's actions button never leaves the row.
41
+ &__strip-actions {
42
+ display: flex;
43
+ align-items: center;
44
+ flex: 0 1 auto;
45
+ min-width: 0;
46
+ overflow: hidden;
47
+ gap: @tabGroupsStripActionsGap;
48
+ padding: 0 @tabGroupsStripActionsPaddingX;
49
+
50
+ // A component that renders nothing leaves the wrapper empty (React cannot tell it from
51
+ // outside): no padding, no box.
52
+ &:empty {
53
+ display: none;
54
+ }
55
+ }
56
+
57
+ // Centred on the row whatever `align-self` a control carries: `Button` sets `flex-start`, so that
58
+ // it does not stretch in a column, and would sit on the row's top edge. Through the header, to
59
+ // outrank that rule whichever stylesheet comes first.
60
+ &__header &__strip-actions > * {
61
+ align-self: center;
62
+ }
63
+
64
+ // The group's actions button is an icon-only control: it carries the `smwb-button_icon-only` marker
65
+ // that `Button` writes for `iconOnly`, and its box is a square of the row's control height - the
66
+ // height the tabs beside it are. The side is a component variable, not Tabs' fixed 40px trigger
67
+ // width: that width, stretched over a dense row, drew a 40x36 rectangle where the mockups draw a
68
+ // square. A dense row is `@tabDenseHeight`, off the `--smui-control-h-*` grid, and marks itself.
69
+ // `min-width` and the horizontal padding are gone with it, so the icon sits in the middle.
38
70
  &__actions-trigger {
71
+ --smwb-tab-groups-actions-size: var(--smui-control-h-lg, @smui-control-h-lg);
72
+
39
73
  display: inline-flex;
40
74
  align-items: center;
41
75
  justify-content: center;
76
+ box-sizing: border-box;
42
77
  flex: 0 0 auto;
43
- width: @tabsOverflowTriggerWidth;
78
+ // Its own height is the square now, not the row's stretch, so a row that grows past it (a tab
79
+ // with a two-line label) still keeps the button in the middle of the row.
80
+ align-self: center;
81
+ min-width: 0;
82
+ width: var(--smwb-tab-groups-actions-size);
83
+ height: var(--smwb-tab-groups-actions-size);
44
84
  padding: 0;
45
85
  border: none;
46
86
  background: none;
47
87
  color: var(--smui-onSurfaceVariant);
48
88
  cursor: pointer;
49
89
 
90
+ // Compact tab rows (`TabGroups` `dense`): the square follows the shorter row.
91
+ &--dense {
92
+ --smwb-tab-groups-actions-size: var(--smui-tabDenseHeight, @tabDenseHeight);
93
+
94
+ // The tabs beside it are the sm step (44px) at least on a coarse pointer, so it is too.
95
+ @media (pointer: coarse) {
96
+ --smwb-tab-groups-actions-size: max(
97
+ var(--smui-tabDenseHeight, @tabDenseHeight),
98
+ var(--smui-control-h-sm, @smui-control-h-sm-coarse)
99
+ );
100
+ }
101
+ }
102
+
50
103
  &:hover:not(:disabled) {
51
104
  background-color: var(--smui-onSurfaceHover);
52
105
  }