@smwb/ui-styles 2.0.0 → 2.2.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%;
@@ -51,6 +51,14 @@
51
51
  background-color: var(--smui-primaryColor);
52
52
  }
53
53
 
54
+ &_warning {
55
+ background-color: var(--smui-warningColor);
56
+ // The amber fill is too light for the white the other colors use; see
57
+ // @badgeWarningColor. An app that retunes --smui-warningColor at runtime
58
+ // retunes this ink to match, through --smui-badgeWarningColor.
59
+ color: var(--smui-badgeWarningColor, @badgeWarningColor);
60
+ }
61
+
54
62
  &_no-content {
55
63
  padding: 0;
56
64
  width: 8px;
@@ -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;
@@ -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);
@@ -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,11 +29,38 @@
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
 
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
+
37
64
  // The group's actions button: the box of Tabs' overflow trigger, which it sits beside.
38
65
  &__actions-trigger {
39
66
  display: inline-flex;
@@ -200,15 +200,60 @@
200
200
  }
201
201
  }
202
202
 
203
- // Compact tabs (Tabs dense): labels as written — no capitals, no letter spacing —
204
- // and less padding, so more tabs fit in a row.
203
+ // Compact tabs (Tabs dense): a shorter row, labels as written — no capitals, no letter spacing —
204
+ // and less padding, so more tabs fit in a row. The height is a token, not a literal, and min-height
205
+ // alone holds the row: the tab is a flex container and its label sets its own line box, so the plain
206
+ // tab's line-height already fits it. A dense row keeps its scroll buttons, its indicator and a
207
+ // closable tab's controls inside that height.
205
208
  .smwb-tabs--dense .smwb-tab {
209
+ min-height: var(--smui-tabDenseHeight, @tabDenseHeight);
206
210
  padding: 0 @tabDensePaddingX;
207
211
  font-weight: 500;
208
212
  letter-spacing: normal;
209
213
  text-transform: none;
210
214
  }
211
215
 
216
+ // Content-width tabs (Tabs `fit`): a tab is as wide as what it holds — marker, label, close button —
217
+ // and the row no longer shares itself out between its tabs, so what is left of the row stays empty.
218
+ // The width stays between two tokens: a short label does not shrink the tab to a sliver, a long one
219
+ // is cut with an ellipsis at the maximum (the full text is the tab's `title`). The limits belong to
220
+ // the row's flex child: the item when the tab has a close button or actions, the tab otherwise. In
221
+ // an item the tab gives way to its controls, which keep their size, and fills the minimum.
222
+ .smwb-tabs--fit {
223
+ // How far a cut label's box reaches past its line box, so that overflow: hidden, which text-overflow
224
+ // needs, does not clip the descenders and accents that line-height: 1 leaves outside it.
225
+ @tabFitLabelOverhang: 0.25em;
226
+
227
+ .smwb-tab,
228
+ .smwb-tab-item {
229
+ flex: 0 0 auto;
230
+ min-width: var(--smui-tabFitMinWidth, @tabFitMinWidth);
231
+ max-width: var(--smui-tabFitMaxWidth, @tabFitMaxWidth);
232
+ }
233
+
234
+ .smwb-tab-item > .smwb-tab {
235
+ flex: 1 1 auto;
236
+ min-width: 0;
237
+ max-width: none;
238
+ }
239
+
240
+ .smwb-tab__content {
241
+ min-width: 0;
242
+ }
243
+
244
+ .smwb-tab__icon {
245
+ flex: 0 0 auto;
246
+ }
247
+
248
+ .smwb-tab__text-label {
249
+ min-width: 0;
250
+ padding-block: @tabFitLabelOverhang;
251
+ margin-block: -@tabFitLabelOverhang;
252
+ overflow: hidden;
253
+ text-overflow: ellipsis;
254
+ }
255
+ }
256
+
212
257
  // The close button shows when it is reachable: the item is hovered (item not disabled),
213
258
  // the item is the active tab, or focus is inside the item. The disabled button stays
214
259
  // hidden; the reveal restores what the hidden defaults below took away. Hidden means
@@ -41,6 +41,30 @@
41
41
  min-height: var(--smui-control-h-lg, @smui-control-h-lg);
42
42
  justify-content: center;
43
43
  }
44
+ // The compact look, for a status row or a toolbar: the label shares the line with
45
+ // the switch (placement "top", the default, counts as "left"; "right" keeps its
46
+ // order) and the row is the small control height, capped at the compact maximum, in
47
+ // place of the size rule's. The :has() rules above weigh the same, so this one
48
+ // follows them. The switch gives up height to fit the row; its width and thumb
49
+ // stay, and the thumb centres itself on the track. Nothing here clips: the focus
50
+ // ring and the thumb halo draw outside the row.
51
+ @compact-height: min(var(--smui-control-h-sm, @smui-control-h-sm), @toggleCompactMaxHeight);
52
+ &__wrapper_compact:has(.smwb-toggle) {
53
+ flex-direction: row;
54
+ align-items: center;
55
+ justify-content: flex-start;
56
+ min-height: @compact-height;
57
+ height: @compact-height;
58
+ .smwb-toggle {
59
+ height: min(@toggleTrackHeight, 100%);
60
+ }
61
+ .smwb-toggle__label {
62
+ white-space: nowrap;
63
+ }
64
+ }
65
+ &__wrapper_compact.smwb-toggle__wrapper_label-right:has(.smwb-toggle) {
66
+ flex-direction: row-reverse;
67
+ }
44
68
  &.smwb-disabled {
45
69
  opacity: 0.35;
46
70
  pointer-events: none;
@@ -1,31 +1,54 @@
1
1
  .smwb-tree {
2
- &__wrapper {
3
- .smwb-tree__node {
4
- &:first-child {
5
- border-left: 0;
6
- &::before {
7
- opacity: 0;
8
- }
9
- }
2
+ // The row height, the node's left padding and the corner's horizontal arm are custom properties of the tree,
3
+ // so the guide corner (drawn by each node's ::before) follows the row: it is half the row height deep, which
4
+ // puts its arm at the middle of a row of any height. The dense look swaps in the small control height and a
5
+ // tighter indent; a caller can set any of the three on the tree itself (the `style` prop).
6
+ --smwb-tree-row-h: var(--smui-control-h-md, @smui-control-h-md);
7
+ --smwb-tree-indent: @treeViewIndent;
8
+ --smwb-tree-arm: @treeViewGuideArm;
9
+
10
+ &_dense {
11
+ --smwb-tree-row-h: var(--smui-control-h-sm, @smui-control-h-sm);
12
+ --smwb-tree-indent: @treeViewIndentDense;
13
+ --smwb-tree-arm: @treeViewGuideArmDense;
14
+ }
15
+
16
+ // A root node sits alone in its wrapper, so it is the wrapper's first child: that is the one to leave without
17
+ // a connector. Matched through the child combinator, the rule does not reach the first child of a nested level.
18
+ &__wrapper > .smwb-tree__node:first-child {
19
+ border-left: 0;
20
+ &::before {
21
+ opacity: 0;
10
22
  }
11
23
  }
24
+
25
+ // Without guides a nested node swaps its 1px line for the same width of padding, so the levels keep their step.
26
+ &_no-guides &__node &__node {
27
+ border-left-width: 0;
28
+ padding-left: calc(var(--smwb-tree-indent) + @treeViewGuideWidth);
29
+
30
+ &::before {
31
+ display: none;
32
+ }
33
+ }
34
+
12
35
  &__node {
13
36
  &:last-child {
14
37
  border-left-color: transparent;
15
38
  }
16
- padding-left: @treeViewIndent;
17
- margin-left: 9px;
39
+ padding-left: var(--smwb-tree-indent);
40
+ margin-left: @treeViewNodeMargin;
18
41
  position: relative;
19
- border-left: 1px solid var(--smui-primaryColor);
42
+ border-left: @treeViewGuideWidth solid var(--smui-primaryColor);
20
43
 
21
44
  &::before {
22
45
  position: absolute;
23
46
  top: 0;
24
- left: -1px;
25
- height: 20px;
26
- width: 25px;
27
- border-bottom: 1px solid var(--smui-primaryColor);
28
- border-left: 1px solid var(--smui-primaryColor);
47
+ left: -@treeViewGuideWidth;
48
+ height: calc(var(--smwb-tree-row-h) / 2);
49
+ width: var(--smwb-tree-arm);
50
+ border-bottom: @treeViewGuideWidth solid var(--smui-primaryColor);
51
+ border-left: @treeViewGuideWidth solid var(--smui-primaryColor);
29
52
  content: "";
30
53
  font-weight: 600;
31
54
  font-size: 14px;
@@ -54,7 +77,7 @@
54
77
  gap: @treeViewItemGap;
55
78
  // min-height, not height: a fixed row height let a wrapped two-line label
56
79
  // overflow the node and overlap the next one.
57
- min-height: 40px;
80
+ min-height: var(--smwb-tree-row-h);
58
81
  }
59
82
 
60
83
  &__toggle {
@@ -97,6 +97,14 @@
97
97
  @avatarGroupMaxStacked: 12; // children laid each over the next; any past these overlap the other way
98
98
 
99
99
  @breadcrumbsGap: @smui-space-sm;
100
+ // Single-line trail: the leading crumbs give way first (this many times as fast as the last one), down to
101
+ // a floor, so the last crumb is cut only once they are.
102
+ @breadcrumbsLeadingShrink: 100;
103
+ @breadcrumbsLeadingMinWidth: 3em;
104
+ // The last crumb never gets narrower than this, so a very narrow box still shows the start of the page title.
105
+ @breadcrumbsLastMinWidth: 4em;
106
+ // How far a clipped crumb still paints outside its box: a 2px focus ring at a 2px offset.
107
+ @breadcrumbsClipMargin: @smui-space-xs;
100
108
 
101
109
  @dividerSpacing: @smui-space-sm;
102
110
  @dividerTextGap: @smui-space-md;
@@ -120,6 +128,8 @@
120
128
  @appBarPaddingX: @smui-space-lg;
121
129
  @appBarActionPadding: @smui-space-md;
122
130
  @appBarActionsGap: 10px; // off-grid
131
+ // How far a clipped title still paints outside its box: a 2px focus ring at a 2px offset.
132
+ @appBarClipMargin: @smui-space-xs;
123
133
  // Width reserved for the OS window buttons in `AppBar variant="window"`: the macOS traffic lights of Electron's
124
134
  // `hiddenInset` end around 70-78px, so with @appBarPaddingX the content starts at 80px; Windows/Linux draw three
125
135
  // 46px caption buttons. Not theme tokens — an app overrides both sides with --smui-appBarWindowControlsWidth.
@@ -127,6 +137,10 @@
127
137
  @appBarWindowControlsRight: 138px;
128
138
 
129
139
  @badgePaddingX: 6px; // off-grid
140
+ // The `warning` badge fills with the theme's amber, which is light: white text on
141
+ // it measures 3.1:1, under the 4.5:1 WCAG AA needs at the badge's 12px label, so
142
+ // this one carries dark ink instead of @onWarningColor. 5.5:1 on @warningColor.
143
+ @badgeWarningColor: #1c1b1f;
130
144
 
131
145
  @buttonGap: @smui-space-sm;
132
146
  @buttonPaddingSmall: @smui-space-xs 10px; // 10 off-grid
@@ -173,9 +187,18 @@
173
187
  @progressDotMargin: @smui-space-xs;
174
188
  @meterHeight: 6px;
175
189
  @meterMinBarWidth: 3.5rem; // the bar keeps at least this much beside its text
190
+ @meterCompactHeight: 4px; // off-grid
191
+ @meterCompactBarWidth: 64px; // the compact meter's bar is this wide, not flexible
176
192
  @statusDotSizeSm: 8px; // @smui-space-2
177
193
  @statusDotSizeMd: 12px; // @smui-space-3
178
194
  @statusDotRing: 2px; // the unknown status is a ring this thick
195
+ // A square turned 45deg spans side * sqrt(2), so the diamond's side is the dot's size
196
+ // divided by this: the two shapes then share one bounding box, and a margin makes up
197
+ // the layout box, which a transform leaves at the unturned side.
198
+ @statusDotDiamondRatio: sqrt(2);
199
+ @statusDotDiamondPrecision: 4; // decimals kept in the diamond's side and margin
200
+ @statusDotDiamondRotation: 45deg; // corner-up, the diamond of the onreel thread marker
201
+ @statusDotDiamondRadius: @smui-radius-none; // sharp corners; the dot's are a circle
179
202
 
180
203
  // A QR code is always dark modules on a white square — never theme tokens, so no
181
204
  // theme (or forced-colours mode, see the component's print/forced-color-adjust) can
@@ -256,6 +279,9 @@
256
279
  @tabIconPaddingBottom: @smui-space-xs;
257
280
  @tabLabelPaddingLeft: @smui-space-sm;
258
281
  @tabDensePaddingX: @smui-space-md;
282
+ @tabDenseHeight: 36px; // off-grid; the row height of Tabs dense, the height of the compact onreel strips
283
+ @tabFitMinWidth: (@smui-space-10 * 2); // Tabs `fit`: a short label does not shrink the tab below this
284
+ @tabFitMaxWidth: (@smui-space-16 * 4); // Tabs `fit`: a longer label is cut with an ellipsis at this width
259
285
  @tabCloseSize: @smui-space-6; // the close button of a closable tab
260
286
  @tabControlsGap: @smui-space-xs; // between a tab's close button, its actions and the row's next tab
261
287
  @tabsFadeWidth: 2.5rem; // scrollable row: the fading edge beyond which tabs are hidden
@@ -268,6 +294,8 @@
268
294
  @tabGroupsSeparatorHitArea: 8px; // off-grid; the invisible grip around that track
269
295
  @tabGroupsDropTintOpacity: 0.16; // how much primary the zone a dragged tab would land in shows
270
296
  @tabGroupsActiveLineWidth: 2px; // off-grid; the line over the header of the group in use
297
+ @tabGroupsStripActionsGap: @smui-space-xs; // between the controls the app puts at the end of a group's tab row
298
+ @tabGroupsStripActionsPaddingX: @smui-space-xs; // that content's distance from the tabs and from the group's actions button
271
299
 
272
300
  @tableCellPaddingX: @smui-space-lg;
273
301
  @tableToolbarMarginBottom: 10px; // off-grid
@@ -290,11 +318,20 @@
290
318
  @textFieldAdornmentFilledMarginTop: @smui-space-md;
291
319
 
292
320
  @toggleGap: @smui-space-sm;
321
+ // The compact Toggle's row is the small control height, but never taller than this.
322
+ @toggleCompactMaxHeight: 28px;
293
323
 
294
324
  @tooltipPaddingX: @smui-space-sm;
295
325
 
296
326
  @treeViewIndent: 30px; // off-grid
297
327
  @treeViewItemGap: 10px; // off-grid
328
+ @treeViewGuideWidth: 1px;
329
+ @treeViewNodeMargin: 9px; // off-grid
330
+ @treeViewGuideArm: 25px; // off-grid
331
+ // The dense look. A node's indent is its padding; with @treeViewNodeMargin (9px) and @treeViewGuideWidth (1px) the step from a row to its
332
+ // children's is 16px (6 + 9 + 1), the arm ends at the child's row, and the row height is @smui-control-h-sm.
333
+ @treeViewIndentDense: 6px; // off-grid
334
+ @treeViewGuideArmDense: 6px; // off-grid
298
335
 
299
336
  /*-------------------
300
337
  Text Field
@@ -362,6 +399,9 @@
362
399
  Button
363
400
  --------------------*/
364
401
  @buttonBorderRadius: 24px;
402
+ // The most an icon-only button rounds its corners: the smaller of this and @buttonBorderRadius.
403
+ // A pill radius would make the square a circle; set @smui-radius-pill here to get one.
404
+ @buttonIconOnlyMaxBorderRadius: @smui-radius-md;
365
405
  @smallButtonFontSize: 13px;
366
406
  @mediumButtonFontSize: 14px;
367
407
  @largeButtonFontSize: 15px;
@@ -524,6 +564,7 @@
524
564
  // are the theme's runtime tokens, so the dark theme follows on its own.
525
565
  @dagViewNodeBg: ~"var(--smui-surfaceBackground)";
526
566
  @dagViewNodeBorder: ~"var(--smui-outlineColor)";
567
+ @dagViewNodeBorderStyle: solid;
527
568
  @dagViewEdgeColor: ~"var(--smui-outlineColor)";
528
569
  @dagViewEdgeActiveColor: ~"var(--smui-primaryColor)";
529
570
  @dagViewSelectedRing: ~"var(--smui-primaryColor)";
@@ -535,6 +576,13 @@
535
576
  @dagViewStatusWarning: ~"var(--smui-warningColor)";
536
577
  @dagViewStatusError: ~"var(--smui-errorColor)";
537
578
  @dagViewStatusSkipped: ~"var(--smui-disabledTextColor)";
579
+ // The line style of a status's node frame; `false`: the theme-wide @dagViewNodeBorderStyle.
580
+ @dagViewStatusPendingBorderStyle: false;
581
+ @dagViewStatusRunningBorderStyle: false;
582
+ @dagViewStatusSuccessBorderStyle: false;
583
+ @dagViewStatusWarningBorderStyle: false;
584
+ @dagViewStatusErrorBorderStyle: false;
585
+ @dagViewStatusSkippedBorderStyle: false;
538
586
  // How far the nodes and edges away from the selection fade, with dimUnrelated.
539
587
  @dagViewDimOpacity: 0.35;
540
588
  // The graph's height when the app sets none (its `height` prop or `style`).
@@ -57,6 +57,10 @@
57
57
 
58
58
  --smui-dividerColor: @dividerColor;
59
59
 
60
+ // Mode-independent (the amber is the same in dark), so light-only, like --smui-control-h-*:
61
+ // a dark-mixin copy would reset a consumer's :root override in dark mode.
62
+ --smui-badgeWarningColor: @badgeWarningColor;
63
+
60
64
  --smui-buttonOutlinedHoverBg: @buttonOutlinedHoverBg;
61
65
  --smui-buttonOutlinedActiveBg: @buttonOutlinedActiveBg;
62
66
  --smui-buttonOutlinedFocusBg: @buttonOutlinedFocusBg;
@@ -88,6 +92,9 @@
88
92
 
89
93
  --smui-tabIconColor: @tabIconColor;
90
94
  --smui-tabColorDefault: @tabColorDefault;
95
+ --smui-tabDenseHeight: @tabDenseHeight;
96
+ --smui-tabFitMinWidth: @tabFitMinWidth;
97
+ --smui-tabFitMaxWidth: @tabFitMaxWidth;
91
98
 
92
99
  --smui-sliderColor: @sliderColor;
93
100
  --smui-sliderDisabledColor: @sliderDisabledColor;
package/normalize.css CHANGED
@@ -25,12 +25,11 @@ main {
25
25
  }
26
26
 
27
27
  /**
28
- * Correct the font size and margin on `h1` elements within `section` and
29
- * `article` contexts in Chrome, Firefox, and Safari.
28
+ * Correct the margin on `h1` elements. The font size is set with the other
29
+ * heading sizes at the end of this file, behind `:where()`.
30
30
  */
31
31
 
32
32
  h1 {
33
- font-size: 2em;
34
33
  margin: 0.67em 0;
35
34
  }
36
35
 
@@ -74,7 +73,7 @@ a {
74
73
  * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
75
74
  */
76
75
 
77
- abbr[title] {
76
+ :where(abbr[title]) {
78
77
  border-bottom: none; /* 1 */
79
78
  text-decoration: underline; /* 2 */
80
79
  text-decoration: underline dotted; /* 2 */
@@ -186,10 +185,7 @@ select {
186
185
  * Correct the inability to style clickable types in iOS and Safari.
187
186
  */
188
187
 
189
- button,
190
- [type="button"],
191
- [type="reset"],
192
- [type="submit"] {
188
+ :where(button, [type="button"], [type="reset"], [type="submit"]) {
193
189
  -webkit-appearance: button;
194
190
  }
195
191
 
@@ -197,25 +193,11 @@ button,
197
193
  * Remove the inner border and padding in Firefox.
198
194
  */
199
195
 
200
- button::-moz-focus-inner,
201
- [type="button"]::-moz-focus-inner,
202
- [type="reset"]::-moz-focus-inner,
203
- [type="submit"]::-moz-focus-inner {
196
+ :where(button, [type="button"], [type="reset"], [type="submit"])::-moz-focus-inner {
204
197
  border-style: none;
205
198
  padding: 0;
206
199
  }
207
200
 
208
- /**
209
- * Restore the focus styles unset by the previous rule.
210
- */
211
-
212
- button:-moz-focusring,
213
- [type="button"]:-moz-focusring,
214
- [type="reset"]:-moz-focusring,
215
- [type="submit"]:-moz-focusring {
216
- outline: 1px dotted ButtonText;
217
- }
218
-
219
201
  /**
220
202
  * Correct the padding in Firefox.
221
203
  */
@@ -261,8 +243,7 @@ textarea {
261
243
  * 2. Remove the padding in IE 10.
262
244
  */
263
245
 
264
- [type="checkbox"],
265
- [type="radio"] {
246
+ :where([type="checkbox"], [type="radio"]) {
266
247
  box-sizing: border-box; /* 1 */
267
248
  padding: 0; /* 2 */
268
249
  }
@@ -271,8 +252,8 @@ textarea {
271
252
  * Correct the cursor style of increment and decrement buttons in Chrome.
272
253
  */
273
254
 
274
- [type="number"]::-webkit-inner-spin-button,
275
- [type="number"]::-webkit-outer-spin-button {
255
+ :where([type="number"])::-webkit-inner-spin-button,
256
+ :where([type="number"])::-webkit-outer-spin-button {
276
257
  height: auto;
277
258
  }
278
259
 
@@ -281,7 +262,7 @@ textarea {
281
262
  * 2. Correct the outline style in Safari.
282
263
  */
283
264
 
284
- [type="search"] {
265
+ :where([type="search"]) {
285
266
  -webkit-appearance: textfield; /* 1 */
286
267
  outline-offset: -2px; /* 2 */
287
268
  }
@@ -290,7 +271,7 @@ textarea {
290
271
  * Remove the inner padding in Chrome and Safari on macOS.
291
272
  */
292
273
 
293
- [type="search"]::-webkit-search-decoration {
274
+ :where([type="search"])::-webkit-search-decoration {
294
275
  -webkit-appearance: none;
295
276
  }
296
277
 
@@ -353,13 +334,20 @@ template {
353
334
  -webkit-box-sizing: border-box;
354
335
  box-sizing: border-box;
355
336
  }
356
- :focus,
357
- :active {
337
+ :where(:focus, :active) {
358
338
  outline: none;
359
339
  }
360
- a:focus,
361
- a:active {
362
- outline: none;
340
+ /**
341
+ * Restore the focus styles unset by the rule above. Declared after it: both have zero
342
+ * specificity, so the later one wins, and a component's own :focus-visible ring beats both.
343
+ */
344
+ :where(
345
+ button:-moz-focusring,
346
+ [type="button"]:-moz-focusring,
347
+ [type="reset"]:-moz-focusring,
348
+ [type="submit"]:-moz-focusring
349
+ ) {
350
+ outline: 1px dotted ButtonText;
363
351
  }
364
352
  nav,
365
353
  footer,
@@ -392,11 +380,7 @@ button::-moz-focus-inner {
392
380
  padding: 0;
393
381
  border: 0;
394
382
  }
395
- a,
396
- a:visited {
397
- text-decoration: none;
398
- }
399
- a:hover {
383
+ a {
400
384
  text-decoration: none;
401
385
  }
402
386
  ul li {
@@ -406,72 +390,78 @@ img {
406
390
  vertical-align: top;
407
391
  }
408
392
 
409
- /* heading elements */
410
- h1 {
393
+ /*
394
+ * heading elements
395
+ *
396
+ * All behind :where(), so these have zero specificity and any component class (the
397
+ * font size and weight of Typography, say) wins, in or out of article, aside, nav and
398
+ * section. Among themselves they cascade in source order, as before.
399
+ *
400
+ * The section rules keep `:-webkit-any()`, which Firefox does not know: :where() takes a
401
+ * forgiving list, so there the branch matches nothing and a bare h1 stays 2em, as it
402
+ * always did. Do not turn it into :is(), that would resize Firefox's headings.
403
+ */
404
+ :where(h1, h2, h3, h4, h5, h6) {
411
405
  display: block;
412
- font-size: 2em;
413
406
  font-weight: bold;
414
407
  }
415
408
 
416
- :-webkit-any(article, aside, nav, section) h1 {
409
+ :where(h1) {
410
+ font-size: 2em;
411
+ }
412
+
413
+ :where(:-webkit-any(article, aside, nav, section) h1) {
417
414
  font-size: 1.5em;
418
415
  }
419
416
 
420
- :-webkit-any(article, aside, nav, section) :-webkit-any(article, aside, nav, section) h1 {
417
+ :where(:-webkit-any(article, aside, nav, section) :-webkit-any(article, aside, nav, section) h1) {
421
418
  font-size: 1.17em;
422
419
  }
423
420
 
424
- :-webkit-any(article, aside, nav, section)
425
- :-webkit-any(article, aside, nav, section)
421
+ :where(
426
422
  :-webkit-any(article, aside, nav, section)
427
- h1 {
423
+ :-webkit-any(article, aside, nav, section)
424
+ :-webkit-any(article, aside, nav, section)
425
+ h1
426
+ ) {
428
427
  font-size: 1em;
429
428
  }
430
429
 
431
- :-webkit-any(article, aside, nav, section)
432
- :-webkit-any(article, aside, nav, section)
433
- :-webkit-any(article, aside, nav, section)
430
+ :where(
434
431
  :-webkit-any(article, aside, nav, section)
435
- h1 {
432
+ :-webkit-any(article, aside, nav, section)
433
+ :-webkit-any(article, aside, nav, section)
434
+ :-webkit-any(article, aside, nav, section)
435
+ h1
436
+ ) {
436
437
  font-size: 0.83em;
437
438
  }
438
439
 
439
- :-webkit-any(article, aside, nav, section)
440
+ :where(
440
441
  :-webkit-any(article, aside, nav, section)
441
- :-webkit-any(article, aside, nav, section)
442
- :-webkit-any(article, aside, nav, section)
443
- :-webkit-any(article, aside, nav, section)
444
- h1 {
442
+ :-webkit-any(article, aside, nav, section)
443
+ :-webkit-any(article, aside, nav, section)
444
+ :-webkit-any(article, aside, nav, section)
445
+ :-webkit-any(article, aside, nav, section)
446
+ h1
447
+ ) {
445
448
  font-size: 0.67em;
446
449
  }
447
450
 
448
- h2 {
449
- display: block;
451
+ :where(h2) {
450
452
  font-size: 1.5em;
451
- font-weight: bold;
452
453
  }
453
454
 
454
- h3 {
455
- display: block;
455
+ :where(h3) {
456
456
  font-size: 1.17em;
457
- font-weight: bold;
458
- }
459
-
460
- h4 {
461
- display: block;
462
- font-weight: bold;
463
457
  }
464
458
 
465
- h5 {
466
- display: block;
459
+ :where(h5) {
467
460
  font-size: 0.83em;
468
- font-weight: bold;
469
461
  }
470
462
 
471
- h6 {
472
- display: block;
463
+ :where(h6) {
473
464
  font-size: 0.67em;
474
- font-weight: bold;
475
465
  }
476
466
 
477
467
  body {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smwb/ui-styles",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Framework-agnostic LESS design tokens and component styles (smwb-*) shared by the @smwb UI libraries. Prebuilt aggregate CSS lives in @smwb/ui-css.",
5
5
  "license": "MIT",
6
6
  "sideEffects": [
@@ -19,7 +19,7 @@
19
19
  "./package.json": "./package.json"
20
20
  },
21
21
  "scripts": {
22
- "lint": "node scripts/check-aggregate.mjs && node scripts/check-tokens.mjs && node scripts/check-control-heights.mjs && node scripts/check-typography-case.mjs && node scripts/check-chart-series.mjs"
22
+ "lint": "node scripts/check-aggregate.mjs && node scripts/check-tokens.mjs && node scripts/check-control-heights.mjs && node scripts/check-typography-case.mjs && node scripts/check-chart-series.mjs && node scripts/check-normalize-specificity.mjs"
23
23
  },
24
24
  "homepage": "https://sourcecraft.dev/elfenmail/ui",
25
25
  "repository": {