@smwb/ui-styles 1.5.0 → 1.6.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
@@ -57,6 +57,69 @@ Override tokens at compile time via a `theme.config` (imported last, so your val
57
57
  runtime via the `--smui-*` CSS variables. Exports: `./components.less`, `./headless-base.less`,
58
58
  `./less/*`, `./normalize.css`.
59
59
 
60
+ #### Typography: letter case and spacing
61
+
62
+ Every `<Typography>` variant has a Less variable for its size, weight and line height
63
+ (`@smui-font-<variant>-size`, `-weight`, `-lh`) and, in addition, for its letter case and spacing:
64
+ `@smui-font-<variant>-transform` and `@smui-font-<variant>-letter-spacing`. The variants are `h1`–`h6`,
65
+ `subtitle1`, `subtitle2`, `body1`, `body2`, `caption`, `overline` and `button`. The defaults are `auto`, a
66
+ marker meaning the variant emits no `text-transform` or `letter-spacing` at all (as before, so consumer rules on
67
+ the element keep winning), except `overline` and `button`, which stay `uppercase` with `0.06em`. Set them in `theme.config`:
68
+
69
+ ```less
70
+ // theme.config: bold uppercase headings with wide tracking
71
+ @smui-font-h1-transform: uppercase;
72
+ @smui-font-h1-letter-spacing: 0.12em;
73
+ @smui-font-h1-weight: 700;
74
+ @smui-font-h2-transform: uppercase;
75
+ @smui-font-h2-letter-spacing: 0.06em;
76
+ @smui-font-h2-weight: 700;
77
+ ```
78
+
79
+ A value that is set compiles to `text-transform: var(--smui-font-<variant>-transform, <value>)` (and the same for
80
+ `letter-spacing`), so it can also be changed at runtime through the `--smui-font-<variant>-transform` and
81
+ `--smui-font-<variant>-letter-spacing` custom properties; the token is not defined by the library, and a
82
+ variant left at `auto` emits neither the declaration nor the token.
83
+
84
+ #### Chart series colours
85
+
86
+ Chart series colours are theme variables too: `@chartSeries1` … `@chartSeries12` (light) and
87
+ `@chartSeries1Dark` … `@chartSeries12Dark` (dark) in `theme.config`, e.g.
88
+ `@chartSeries1: #e4002b; @chartSeries1Dark: #ff6b81;`. They become `--smwb-chart-series-1` … `-12` on
89
+ `.smwb-chart`, which the chart reads and redraws from on a theme switch; a colour in the chart's own
90
+ `option.color` still wins. The defaults are the Okabe–Ito set (7 colours; slots 8–12 are `false` = not
91
+ emitted). Fill slots in order: an unset slot is skipped, so the colours after it shift up (slot 9 without slot 8
92
+ is drawn for the 8th series). Light and dark are separate sets, so set both when the brand colour must
93
+ show in the dark theme. At runtime, set `--smwb-chart-series-N` on the chart itself (its `style`): the chart declares the
94
+ tokens on its own root, so a value set on `:root` or a wrapper does not reach it.
95
+
96
+ #### DagView colours
97
+
98
+ The dependency graph (`DagView` in `@smwb/ui-solid`) takes its colours from theme variables that default to the
99
+ runtime tokens, so the dark theme follows without extra settings:
100
+
101
+ | Variable (`theme.config`) | Runtime property on `.smwb-dag-view` | Default |
102
+ | --- | --- | --- |
103
+ | `@dagViewNodeBg` | `--smwb-dag-view-node-bg` | `var(--smui-surfaceBackground)` |
104
+ | `@dagViewNodeBorder` | `--smwb-dag-view-node-border` | `var(--smui-outlineColor)` |
105
+ | `@dagViewEdgeColor` | `--smwb-dag-view-edge-color` | `var(--smui-outlineColor)` |
106
+ | `@dagViewEdgeActiveColor` | `--smwb-dag-view-edge-active-color` | `var(--smui-primaryColor)` |
107
+ | `@dagViewSelectedRing` | `--smwb-dag-view-selected-ring` | `var(--smui-primaryColor)` |
108
+ | `@dagViewStatusIdle` … `@dagViewStatusSkipped` | `--smwb-dag-view-status-idle` … `-skipped` | outline, on-surface variant, info, success, warning, error, disabled text |
109
+ | `@dagViewDimOpacity` | `--smwb-dag-view-dim-opacity` | `0.35` |
110
+ | `@dagViewHeight` | — (the graph's `height`) | `480px` |
111
+
112
+ The statuses are `idle`, `pending`, `running`, `success`, `warning`, `error` and `skipped`; a node with any other
113
+ status is drawn as `idle`. Each colour variable has a `…Dark` twin (e.g. `@dagViewStatusSuccessDark`), `false` by
114
+ default: set it to give the dark theme its own value. At runtime, set the properties on the graph itself, since it
115
+ declares them on its own root: in its `style`, or with a class in a selector that outranks `.smwb-dag-view`, such as
116
+ `.smwb-dag-view.my-graph` (a plain `.my-graph` ties with it, and the graph's stylesheet may load after yours). A
117
+ node's accent comes from `--smwb-dag-view-accent`. For a status of your own, set it on the node and keep the node's
118
+ `accent` prop in front, as the built-in statuses do:
119
+ `.smwb-dag-view__node[data-status="queued"] { --smwb-dag-view-accent: var(--smwb-dag-view-node-accent, teal) }`.
120
+ The graph's default height, `@dagViewHeight`, is set by the stylesheet, so its `height` prop or a height in its
121
+ `style` wins over it.
122
+
60
123
  ## License
61
124
 
62
125
  [MIT](./LICENSE)
@@ -58,6 +58,9 @@
58
58
  display: inline-flex;
59
59
  flex-direction: row;
60
60
  align-items: center;
61
+ // The children's z-indexes stay inside the group: they must not lift an
62
+ // avatar over the rest of the page (a sticky header, a menu).
63
+ isolation: isolate;
61
64
 
62
65
  // Direct children overlap, so an avatar wrapped in a link (a compact
63
66
  // PersonChip) tucks in like a bare one.
@@ -65,6 +68,16 @@
65
68
  margin-left: (-1 * @avatarGroupOverlap);
66
69
  }
67
70
 
71
+ // Drawn in source order, each child would lie over the previous one: a
72
+ // z-index by position puts each over the next instead.
73
+ .smwb-avatar-group-stack(1);
74
+
75
+ // A focused child comes up whole, focus ring and all.
76
+ > :focus-visible,
77
+ > :has(:focus-visible) {
78
+ z-index: (@avatarGroupMaxStacked + 1);
79
+ }
80
+
68
81
  .smwb-avatar {
69
82
  border: @avatarGroupBorder solid var(--smui-surfaceBackground);
70
83
  box-sizing: content-box;
@@ -80,3 +93,12 @@
80
93
  font-size: 0.8rem;
81
94
  }
82
95
  }
96
+
97
+ // The first child highest. Past @avatarGroupMaxStacked children the rest keep
98
+ // no z-index, so they overlap the other way.
99
+ .smwb-avatar-group-stack(@n) when (@n <= @avatarGroupMaxStacked) {
100
+ > :nth-child(@{n}) {
101
+ z-index: (@avatarGroupMaxStacked + 1 - @n);
102
+ }
103
+ .smwb-avatar-group-stack(@n + 1);
104
+ }
@@ -39,6 +39,16 @@
39
39
  pointer-events: none;
40
40
  opacity: 0.38;
41
41
  }
42
+ // The title and the children: the card's button when it has onClick. The
43
+ // footer follows it, beside the button, not inside it.
44
+ &__content {
45
+ display: flex;
46
+ flex-direction: column;
47
+ // The card draws the ring, below.
48
+ &:focus-visible {
49
+ outline: none;
50
+ }
51
+ }
42
52
  &__title {
43
53
  font-size: 1.25rem;
44
54
  font-weight: 500;
@@ -48,9 +58,10 @@
48
58
  }
49
59
  &_interactive {
50
60
  cursor: pointer;
51
- &:focus-visible {
52
- outline: 2px solid var(--smui-primaryColor);
53
- outline-offset: 2px;
54
- }
61
+ }
62
+ // Focus goes to the card's button; the ring goes around the whole card.
63
+ &:has(> &__content:focus-visible) {
64
+ outline: 2px solid var(--smui-primaryColor);
65
+ outline-offset: 2px;
55
66
  }
56
67
  }
@@ -1,15 +1,22 @@
1
+ // Emits --smwb-chart-series-N from @chartSeriesN<suffix> (suffix "" or "Dark")
2
+ // for N = 1 … 12. A slot that is `false` (the default of 8-12) emits nothing,
3
+ // so an unset slot leaves no token and the chart draws only the colours set.
4
+ .smwb-chart-series(@suffix; @i: 1) when (@i <= 12) {
5
+ @name: ~"chartSeries@{i}@{suffix}";
6
+ .smwb-chart-series-token(@i; @@name);
7
+ .smwb-chart-series(@suffix; @i + 1);
8
+ }
9
+ .smwb-chart-series-token(@i; @colour) when not (@colour = false) {
10
+ --smwb-chart-series-@{i}: @colour;
11
+ }
12
+
1
13
  // A chart: ECharts draws into __canvas; until it does, a skeleton of the same
2
14
  // size stands in, so the page does not jump.
3
15
  .smwb-chart {
4
16
  // Series colours, in order (read up to 12). The set stays apart with
5
- // colour-vision deficiencies (Okabe–Ito); a theme may set its own.
6
- --smwb-chart-series-1: #0072b2;
7
- --smwb-chart-series-2: #e69f00;
8
- --smwb-chart-series-3: #009e73;
9
- --smwb-chart-series-4: #cc79a7;
10
- --smwb-chart-series-5: #56b4e9;
11
- --smwb-chart-series-6: #d55e00;
12
- --smwb-chart-series-7: #7a7a7a;
17
+ // colour-vision deficiencies (Okabe–Ito); a theme sets its own with
18
+ // @chartSeries1 … @chartSeries12 (and …Dark for the dark theme).
19
+ .smwb-chart-series("");
13
20
 
14
21
  position: relative;
15
22
  width: 100%;
@@ -41,11 +48,5 @@
41
48
  }
42
49
 
43
50
  :root[data-theme="dark"] .smwb-chart {
44
- --smwb-chart-series-1: #5aa9e6;
45
- --smwb-chart-series-2: #f0b43a;
46
- --smwb-chart-series-3: #2fc59b;
47
- --smwb-chart-series-4: #e08cc0;
48
- --smwb-chart-series-5: #8fd0f5;
49
- --smwb-chart-series-6: #f27c3c;
50
- --smwb-chart-series-7: #a8b0bc;
51
+ .smwb-chart-series("Dark");
51
52
  }
@@ -3,6 +3,9 @@
3
3
  }
4
4
 
5
5
  .smwb-checkbox {
6
+ // The box's size, which the native input takes too.
7
+ @box-size: 18px;
8
+
6
9
  &__icon {
7
10
  opacity: 0;
8
11
  transition: all 0.1s ease-in-out;
@@ -11,8 +14,8 @@
11
14
  &__btn {
12
15
  user-select: none;
13
16
  flex-shrink: 0;
14
- width: 18px;
15
- height: 18px;
17
+ width: @box-size;
18
+ height: @box-size;
16
19
  border-radius: @checkboxBorderRadius;
17
20
  border: 2px solid var(--smui-checkboxBorderColor);
18
21
  background-color: transparent;
@@ -36,6 +39,8 @@
36
39
  }
37
40
 
38
41
  &__wrapper {
42
+ // Anchors the absolutely positioned input (below) to the control itself.
43
+ position: relative;
39
44
  color: var(--smui-primaryTextColor);
40
45
  display: flex;
41
46
  align-items: center;
@@ -58,7 +63,19 @@
58
63
  }
59
64
 
60
65
  &__input {
61
- display: none;
66
+ // Visually hidden but rendered: with display: none the input would be out of
67
+ // the tab order and the accessibility tree. It lies under the box, which
68
+ // takes the pointer and passes clicks on through the label. With no insets
69
+ // of its own it sits where the box does: at the start, centered.
70
+ position: absolute;
71
+ width: @box-size;
72
+ height: @box-size;
73
+ margin: 0;
74
+ opacity: 0;
75
+ &:focus-visible ~ .smwb-checkbox__btn {
76
+ outline: 2px solid var(--smui-primaryColor);
77
+ outline-offset: 2px;
78
+ }
62
79
  &:hover {
63
80
  & ~ .smwb-checkbox__btn::before {
64
81
  background-color: var(--smui-onSurfaceHover);
@@ -74,6 +74,39 @@
74
74
  justify-content: center;
75
75
  }
76
76
 
77
+ // The icon and the label: the chip's button when it has onClick. The delete
78
+ // button follows it, beside the button, not inside it.
79
+ &__content {
80
+ display: inline-flex;
81
+ align-items: center;
82
+ gap: @chipGap;
83
+ border-radius: inherit;
84
+
85
+ // The button's focus ring goes around the whole chip, as when the chip
86
+ // itself took focus: its own ring is pushed out of sight, and ::after, laid
87
+ // over the chip (the chip is its containing block), takes its outline.
88
+ // Whatever sets that outline still decides it: the browser's ring, an
89
+ // app's, or none under a reset that removes focus rings (normalize.css).
90
+ &:focus {
91
+ outline-offset: 9999px;
92
+ }
93
+ &:focus::after {
94
+ content: "";
95
+ position: absolute;
96
+ inset: 0;
97
+ border-radius: inherit;
98
+ outline: inherit;
99
+ pointer-events: none;
100
+ }
101
+ }
102
+ // An outlined chip's ring sits where the chip's own did: ::after takes the
103
+ // chip's box and a border as wide as its border, as the browser draws its
104
+ // ring over a border and an app's ring around it.
105
+ &.smwb-outlined > &__content:focus::after {
106
+ inset: -1px;
107
+ border: 1px solid transparent;
108
+ }
109
+
77
110
  // delete control is a <button> now — strip UA chrome (additive)
78
111
  &__delete-icon {
79
112
  background: transparent;
@@ -0,0 +1,2 @@
1
+ @import (reference) "../../_prelude.less";
2
+ @import "./dagView.less";
@@ -0,0 +1,280 @@
1
+ // Emits --smwb-dag-view-<name> from @dagView<Var><suffix> (suffix "" or "Dark").
2
+ // A variable that is `false` (the dark defaults) emits nothing, so the light
3
+ // value, a runtime token that follows the theme, stays in force.
4
+ .smwb-dag-view-token(@name; @var; @suffix) {
5
+ @full: ~"dagView@{var}@{suffix}";
6
+ .smwb-dag-view-token-value(@name; @@full);
7
+ }
8
+ .smwb-dag-view-token-value(@name; @value) when not (@value = false) {
9
+ --smwb-dag-view-@{name}: @value;
10
+ }
11
+ .smwb-dag-view-tokens(@suffix) {
12
+ .smwb-dag-view-token(node-bg; NodeBg; @suffix);
13
+ .smwb-dag-view-token(node-border; NodeBorder; @suffix);
14
+ .smwb-dag-view-token(edge-color; EdgeColor; @suffix);
15
+ .smwb-dag-view-token(edge-active-color; EdgeActiveColor; @suffix);
16
+ .smwb-dag-view-token(selected-ring; SelectedRing; @suffix);
17
+ .smwb-dag-view-token(status-idle; StatusIdle; @suffix);
18
+ .smwb-dag-view-token(status-pending; StatusPending; @suffix);
19
+ .smwb-dag-view-token(status-running; StatusRunning; @suffix);
20
+ .smwb-dag-view-token(status-success; StatusSuccess; @suffix);
21
+ .smwb-dag-view-token(status-warning; StatusWarning; @suffix);
22
+ .smwb-dag-view-token(status-error; StatusError; @suffix);
23
+ .smwb-dag-view-token(status-skipped; StatusSkipped; @suffix);
24
+ }
25
+ // A built-in status: its accent, unless the node sets its own.
26
+ .smwb-dag-view-status(@status) {
27
+ @accent: ~"var(--smwb-dag-view-node-accent, var(--smwb-dag-view-status-@{status}))";
28
+ &[data-status="@{status}"] {
29
+ --smwb-dag-view-accent: @accent;
30
+ }
31
+ }
32
+
33
+ // A dependency graph: a viewport that clips a "world" moved and scaled by one
34
+ // transform; in the world, an SVG layer of edges under absolutely placed nodes.
35
+ .smwb-dag-view {
36
+ .smwb-dag-view-tokens("");
37
+ --smwb-dag-view-dim-opacity: @dagViewDimOpacity;
38
+
39
+ position: relative;
40
+ width: 100%;
41
+ // The default height; the `height` prop or a height in `style` wins.
42
+ height: @dagViewHeight;
43
+ min-width: 0;
44
+ box-sizing: border-box;
45
+ // clip, not hidden: a focused node off screen must not scroll the viewport
46
+ // (the view moves by its transform), and a clipping box is no Tab stop.
47
+ overflow: hidden;
48
+ overflow: clip;
49
+ border: 1px solid var(--smui-dividerColor);
50
+ border-radius: @smui-radius-lg;
51
+ background-color: color-mix(in srgb, var(--smui-onSurfaceColor) 2%, var(--smui-surfaceBackground));
52
+ color: var(--smui-onSurfaceColor);
53
+ // The graph takes every touch gesture, and a wheel turn does not reach the page.
54
+ touch-action: none;
55
+ overscroll-behavior: contain;
56
+ user-select: none;
57
+ -webkit-user-select: none;
58
+ cursor: grab;
59
+
60
+ &[data-interacting] {
61
+ cursor: grabbing;
62
+ }
63
+
64
+ &__world {
65
+ position: absolute;
66
+ top: 0;
67
+ left: 0;
68
+ transform-origin: 0 0;
69
+ }
70
+
71
+ // Only during a gesture: at rest it would blur the text.
72
+ &[data-interacting] &__world {
73
+ will-change: transform;
74
+ }
75
+
76
+ &__edges {
77
+ position: absolute;
78
+ top: 0;
79
+ left: 0;
80
+ overflow: visible;
81
+ pointer-events: none;
82
+ }
83
+
84
+ &__edge {
85
+ fill: none;
86
+ stroke: var(--smwb-dag-view-edge-color);
87
+ stroke-width: 1.5px;
88
+ transition:
89
+ stroke 0.12s,
90
+ opacity 0.12s;
91
+ }
92
+
93
+ &__edge--active {
94
+ stroke: var(--smwb-dag-view-edge-active-color);
95
+ stroke-width: 2.5px;
96
+ }
97
+
98
+ // An edge that closes a cycle, drawn against the flow.
99
+ &__edge--reversed {
100
+ stroke-dasharray: 6 4;
101
+ }
102
+
103
+ &__edge--dimmed {
104
+ opacity: var(--smwb-dag-view-dim-opacity);
105
+ }
106
+
107
+ &__arrow {
108
+ fill: var(--smwb-dag-view-edge-color);
109
+ }
110
+
111
+ &__arrow--active {
112
+ fill: var(--smwb-dag-view-edge-active-color);
113
+ }
114
+
115
+ &__node {
116
+ --smwb-dag-view-accent: var(--smwb-dag-view-node-accent, var(--smwb-dag-view-status-idle));
117
+ .smwb-dag-view-status(pending);
118
+ .smwb-dag-view-status(running);
119
+ .smwb-dag-view-status(success);
120
+ .smwb-dag-view-status(warning);
121
+ .smwb-dag-view-status(error);
122
+ .smwb-dag-view-status(skipped);
123
+
124
+ position: absolute;
125
+ box-sizing: border-box;
126
+ display: flex;
127
+ align-items: center;
128
+ gap: @smui-space-sm;
129
+ padding: 0 @smui-space-md 0 10px;
130
+ border: 1px solid var(--smwb-dag-view-node-border);
131
+ // The status stripe.
132
+ border-left: 4px solid var(--smwb-dag-view-accent);
133
+ border-radius: @smui-radius-md;
134
+ background-color: var(--smwb-dag-view-node-bg);
135
+ color: var(--smui-onSurfaceColor);
136
+ font-size: 0.875rem;
137
+ line-height: 1.3;
138
+ overflow: hidden;
139
+ contain: layout paint;
140
+ cursor: pointer;
141
+ transition:
142
+ box-shadow 0.12s,
143
+ border-color 0.12s,
144
+ opacity 0.12s;
145
+
146
+ &:hover {
147
+ border-color: color-mix(in srgb, var(--smwb-dag-view-edge-active-color) 45%, var(--smwb-dag-view-node-border));
148
+ border-left-color: var(--smwb-dag-view-accent);
149
+ }
150
+
151
+ // normalize.css takes the outline off :focus; the keyboard gets a ring here.
152
+ &:focus-visible {
153
+ outline: 2px solid var(--smui-primaryColor);
154
+ outline-offset: 2px;
155
+ }
156
+ }
157
+
158
+ &__node--selected {
159
+ box-shadow: 0 0 0 2px var(--smwb-dag-view-selected-ring);
160
+ }
161
+
162
+ &__node--upstream,
163
+ &__node--downstream {
164
+ border-color: var(--smwb-dag-view-edge-active-color);
165
+ border-left-color: var(--smwb-dag-view-accent);
166
+ }
167
+
168
+ &__node--dimmed {
169
+ opacity: var(--smwb-dag-view-dim-opacity);
170
+ }
171
+
172
+ &__node--disabled {
173
+ cursor: default;
174
+ color: var(--smui-disabledTextColor);
175
+ }
176
+
177
+ &__dot {
178
+ flex-shrink: 0;
179
+ width: 8px;
180
+ height: 8px;
181
+ border-radius: 50%;
182
+ background-color: var(--smwb-dag-view-accent);
183
+ }
184
+
185
+ &__label {
186
+ flex: 1 1 auto;
187
+ min-width: 0;
188
+ overflow: hidden;
189
+ font-weight: 500;
190
+ text-overflow: ellipsis;
191
+ white-space: nowrap;
192
+ }
193
+
194
+ &__status {
195
+ flex-shrink: 0;
196
+ font-size: 0.75rem;
197
+ color: var(--smui-onSurfaceVariant);
198
+ }
199
+
200
+ // Zoomed far out the status words (and any content marked __detail) give
201
+ // way, so the boxes stay readable.
202
+ &[data-zoom="far"] &__status,
203
+ &[data-zoom="far"] &__detail {
204
+ display: none;
205
+ }
206
+
207
+ // The graph in words, for screen readers only.
208
+ &__summary {
209
+ position: absolute;
210
+ width: 1px;
211
+ height: 1px;
212
+ margin: -1px;
213
+ padding: 0;
214
+ overflow: hidden;
215
+ clip: rect(0 0 0 0);
216
+ white-space: nowrap;
217
+ border: 0;
218
+ }
219
+
220
+ &__controls {
221
+ position: absolute;
222
+ right: @smui-space-sm;
223
+ bottom: @smui-space-sm;
224
+ display: flex;
225
+ flex-direction: column;
226
+ gap: @smui-space-xs;
227
+ cursor: default;
228
+ }
229
+
230
+ &__control {
231
+ display: inline-flex;
232
+ align-items: center;
233
+ justify-content: center;
234
+ width: 32px;
235
+ height: 32px;
236
+ padding: 0;
237
+ border: 1px solid var(--smui-dividerColor);
238
+ border-radius: @smui-radius-md;
239
+ background-color: var(--smui-surfaceBackground);
240
+ color: var(--smui-onSurfaceVariant);
241
+ cursor: pointer;
242
+
243
+ svg {
244
+ width: 18px;
245
+ height: 18px;
246
+ fill: none;
247
+ stroke: currentColor;
248
+ stroke-width: 2;
249
+ stroke-linecap: round;
250
+ stroke-linejoin: round;
251
+ }
252
+
253
+ &:hover {
254
+ background-color: color-mix(in srgb, var(--smui-onSurfaceColor) 8%, var(--smui-surfaceBackground));
255
+ color: var(--smui-onSurfaceColor);
256
+ }
257
+
258
+ &:focus-visible {
259
+ outline: 2px solid var(--smui-primaryColor);
260
+ outline-offset: 2px;
261
+ }
262
+ }
263
+ }
264
+
265
+ :root[data-theme="dark"] .smwb-dag-view {
266
+ .smwb-dag-view-tokens("Dark");
267
+ }
268
+
269
+ // A running node's dot pulses, unless motion is reduced.
270
+ @media (prefers-reduced-motion: no-preference) {
271
+ .smwb-dag-view__node[data-status="running"] .smwb-dag-view__dot {
272
+ animation: smwb-dag-view-pulse 1.4s ease-in-out infinite;
273
+ }
274
+ }
275
+
276
+ @keyframes smwb-dag-view-pulse {
277
+ 50% {
278
+ opacity: 0.35;
279
+ }
280
+ }
@@ -39,7 +39,13 @@
39
39
  }
40
40
 
41
41
  // auto: the list is its own query container, so its width — not the
42
- // window's — decides when the pairs stack.
42
+ // window's — decides when the pairs stack. A container's content does not
43
+ // count toward its width, so where the parent takes its width from its
44
+ // content (a flex item that does not grow, a popover, a table cell) the list
45
+ // has none: it needs a width there, or "row" / "stacked". A fallback width
46
+ // (contain-intrinsic-inline-size) would also become the list's min-content
47
+ // width, and a card holding the list in a 1fr grid column or a flex: 1 item
48
+ // could no longer shrink below it.
43
49
  &_auto {
44
50
  container: smwb-description-list / inline-size;
45
51
  }
@@ -28,7 +28,9 @@
28
28
  white-space: nowrap;
29
29
  height: 48px;
30
30
 
31
- &:not(&--selected):not(:active) {
31
+ // A select's option the keys are on (aria-activedescendant, no DOM focus) keeps
32
+ // the focus layer under the pointer too, as a focused item does.
33
+ &:not(&--selected):not(:active):not(:focus-visible):not(.smwb-select-field__option--active) {
32
34
  &:hover {
33
35
  &:before {
34
36
  opacity: @menuOpacityHover;
@@ -61,6 +63,23 @@
61
63
  }
62
64
  }
63
65
 
66
+ // Keyboard focus: the focus state layer, over hover and the selected shade, and the
67
+ // library's ring, inside the item so that neither the menu's edge nor a scrolling menu
68
+ // clips it. normalize.css removes the browser's own ring.
69
+ &:focus-visible {
70
+ outline: 2px solid var(--smui-primaryColor);
71
+ outline-offset: -2px;
72
+
73
+ &:before {
74
+ opacity: @menuOpacityFocus;
75
+ }
76
+ }
77
+
78
+ // An item with `href` takes focus a second time, on its link, which fills the item.
79
+ &:has(> &-inner:focus-visible):before {
80
+ opacity: @menuOpacityFocus;
81
+ }
82
+
64
83
  &:active {
65
84
  &:before {
66
85
  opacity: @menuOpacityActive;
@@ -75,6 +94,11 @@
75
94
  height: 100%;
76
95
  color: currentColor;
77
96
  gap: @menuItemGap;
97
+
98
+ &:focus-visible {
99
+ outline: 2px solid var(--smui-primaryColor);
100
+ outline-offset: -2px;
101
+ }
78
102
  }
79
103
 
80
104
  &__text {
@@ -10,8 +10,10 @@
10
10
  color: inherit;
11
11
  text-decoration: none;
12
12
 
13
- // The derived colours are mid-dark in both themes: white initials read on all of them.
14
- &__avatar {
13
+ // The derived colours are mid-dark in both themes: white initials read on all
14
+ // of them. Two classes: the avatar's own colour rule (--smui-onPrimaryColor,
15
+ // dark in the dark theme) must not win on stylesheet order.
16
+ .smwb-avatar&__avatar {
15
17
  flex-shrink: 0;
16
18
  color: #fff;
17
19
  }
@@ -30,7 +30,20 @@
30
30
  }
31
31
 
32
32
  &__input {
33
- display: none;
33
+ // Visually hidden but rendered: with display: none the input would be out of
34
+ // the tab order and the accessibility tree. It fills the wrapper, under the
35
+ // button, which takes the pointer and passes clicks on through the label.
36
+ position: absolute;
37
+ top: 0;
38
+ left: 0;
39
+ width: 100%;
40
+ height: 100%;
41
+ margin: 0;
42
+ opacity: 0;
43
+ &:focus-visible ~ .smwb-radio__btn {
44
+ outline: 2px solid var(--smui-primaryColor);
45
+ outline-offset: 2px;
46
+ }
34
47
  &:active {
35
48
  & ~ .radio__btn:hover ~ .smwb-radio__bg {
36
49
  transform: translate(-50%, -50%) scale(1);
@@ -1,7 +1,8 @@
1
+ // The control (the row of icons) with its helper text below it.
1
2
  .smwb-rating {
2
3
  display: inline-flex;
3
- align-items: center;
4
- gap: @ratingGap;
4
+ flex-direction: column;
5
+ align-items: flex-start;
5
6
  color: var(--smui-primaryColor);
6
7
 
7
8
  &_disabled {
@@ -9,6 +10,18 @@
9
10
  pointer-events: none;
10
11
  }
11
12
 
13
+ &__control {
14
+ display: inline-flex;
15
+ align-items: center;
16
+ gap: @ratingGap;
17
+
18
+ // normalize.css removes the browser's focus ring.
19
+ &:focus-visible {
20
+ outline: 2px solid var(--smui-primaryColor);
21
+ outline-offset: 2px;
22
+ }
23
+ }
24
+
12
25
  &__item {
13
26
  position: relative;
14
27
  display: inline-flex;
@@ -44,6 +44,33 @@
44
44
  }
45
45
  }
46
46
 
47
+ // The control, the part of the field that is the button: the selected text
48
+ // and the search box. Chips sit before it and the clear button after it,
49
+ // outside, so it takes the room they leave.
50
+ &__control {
51
+ display: flex;
52
+ flex: 1 1 auto;
53
+ align-items: center;
54
+ align-self: stretch;
55
+ // The scrolling row the control sits in would cut its focus ring, so the
56
+ // ring goes around the whole field, as when the field was the control: the
57
+ // control's own is pushed out of sight, and ::after, laid over the field
58
+ // (the input holder is its containing block), takes its outline. Whatever
59
+ // sets that outline still decides it, e.g. a reset that removes focus rings
60
+ // (normalize.css) removes this one too.
61
+ &:focus {
62
+ outline-offset: 9999px;
63
+ }
64
+
65
+ &:focus::after {
66
+ content: "";
67
+ position: absolute;
68
+ inset: 0;
69
+ outline: inherit;
70
+ pointer-events: none;
71
+ }
72
+ }
73
+
47
74
  &__actions {
48
75
  display: flex;
49
76
  align-items: center;
@@ -51,6 +78,19 @@
51
78
  margin-right: -7px;
52
79
  }
53
80
 
81
+ // The clear button draws only its icon.
82
+ &__clear {
83
+ display: inline-flex;
84
+ flex-shrink: 0;
85
+ margin: 0;
86
+ padding: 0;
87
+ border: 0;
88
+ background: none;
89
+ color: inherit;
90
+ font: inherit;
91
+ cursor: pointer;
92
+ }
93
+
54
94
  &__values {
55
95
  white-space: nowrap;
56
96
  font: inherit;
@@ -61,7 +101,10 @@
61
101
  margin-left: @selectFieldChipMarginLeft;
62
102
  }
63
103
 
64
- & + span {
104
+ // The search box keeps its distance from the values: it follows the
105
+ // selected text in the control, or opens the control after the chips.
106
+ & + .smwb-select-field__search,
107
+ & + .smwb-select-field__control > .smwb-select-field__search {
65
108
  margin-left: @selectFieldChipMarginLeft;
66
109
  }
67
110
 
@@ -187,6 +230,28 @@
187
230
  color: var(--smui-primaryColor);
188
231
  }
189
232
 
233
+ // The options: a listbox, and the groups in it, inside the menu's list.
234
+ .smwb-select-field__listbox,
235
+ .smwb-select-field__group-options {
236
+ list-style: none;
237
+ margin: 0;
238
+ padding: 0;
239
+ }
240
+
241
+ // The option the keyboard is on. The focus stays on the field, which points at
242
+ // the option, so it is drawn as a menu item with the keyboard focus: the ring
243
+ // inside it (an optionRenderer's option too) and the focus state layer.
244
+ .smwb-select-field__option--active {
245
+ outline: 2px solid var(--smui-primaryColor);
246
+ outline-offset: -2px;
247
+ }
248
+
249
+ // Inside the menu, so it outranks the selected shade (.smwb-menu__item--selected)
250
+ // whichever stylesheet comes first; menu.less keeps hover off it.
251
+ .smwb-menu .smwb-menu__item.smwb-select-field__option--active:before {
252
+ opacity: @menuOpacity-focus;
253
+ }
254
+
190
255
  // size, as for TextField: the padding and the font follow the control scale, so
191
256
  // a small select stands as tall as a small text field or button in a toolbar.
192
257
  .smwb-text-field_size-small .smwb-select-field:not(.smwb-select-field--inline) {
@@ -64,6 +64,18 @@
64
64
  font-weight: 500;
65
65
  }
66
66
  }
67
+ // The column's controls (TableHeadCell actions) sit after the sort
68
+ // control, not in it: inside, their names and values would join its
69
+ // accessible name.
70
+ .smwb-head-cell__actions {
71
+ align-items: center;
72
+ display: inline-flex;
73
+ vertical-align: middle;
74
+ gap: @tableActionsGap;
75
+ margin-inline-start: @tableActionsGap;
76
+ // A sortable cell shows a pointer; the controls are not the sort control.
77
+ cursor: auto;
78
+ }
67
79
  &.smwb-align_left {
68
80
  text-align: left;
69
81
  }
@@ -1,10 +1,12 @@
1
+ // The root is a box around the content (the image and the caption: the card's
2
+ // button when it is selectable) and the corner slots, laid over the image
3
+ // beside that button, not inside it.
1
4
  .smwb-thumb-card {
2
5
  position: relative;
3
6
  display: flex;
4
7
  flex-direction: column;
5
8
  border-radius: 8px;
6
9
  overflow: visible;
7
- outline: none;
8
10
 
9
11
  &--interactive {
10
12
  cursor: pointer;
@@ -15,6 +17,13 @@
15
17
  pointer-events: none;
16
18
  }
17
19
 
20
+ // Takes focus; the ring is drawn on the image instead.
21
+ &__content {
22
+ display: flex;
23
+ flex-direction: column;
24
+ outline: none;
25
+ }
26
+
18
27
  &__media {
19
28
  position: relative;
20
29
  width: 100%;
@@ -29,11 +38,12 @@
29
38
  outline-offset: 1px;
30
39
  }
31
40
 
32
- &--interactive:focus-visible &__media {
41
+ &__content:focus-visible &__media {
33
42
  outline: 2px solid var(--smui-primaryColor);
34
43
  outline-offset: 1px;
35
44
  }
36
45
 
46
+ // Positioned against the root, whose top-left corner is the image's.
37
47
  &__slot {
38
48
  position: absolute;
39
49
  top: 4px;
@@ -70,7 +70,20 @@
70
70
  }
71
71
 
72
72
  &__checkbox {
73
- display: none;
73
+ // Visually hidden but rendered: with display: none the input would be out of
74
+ // the tab order and the accessibility tree. It fills the switch, under the
75
+ // track and thumb, which take the pointer and pass clicks on through the label.
76
+ position: absolute;
77
+ top: 0;
78
+ left: 0;
79
+ width: 100%;
80
+ height: 100%;
81
+ margin: 0;
82
+ opacity: 0;
83
+ &:focus-visible ~ .smwb-toggle__track {
84
+ outline: 2px solid var(--smui-primaryColor);
85
+ outline-offset: 2px;
86
+ }
74
87
  &:checked {
75
88
  & ~ .smwb-toggle__track {
76
89
  background-color: var(--smui-toggleTrackActiveColor);
@@ -7,10 +7,23 @@
7
7
  @s: ~"smui-font-@{v}-size";
8
8
  @w: ~"smui-font-@{v}-weight";
9
9
  @l: ~"smui-font-@{v}-lh";
10
+ @t: ~"smui-font-@{v}-transform";
11
+ @ls: ~"smui-font-@{v}-letter-spacing";
10
12
  &_@{v} {
11
13
  font-size: @@s;
12
14
  font-weight: @@w;
13
15
  line-height: @@l;
16
+ // `auto` (the default) declares nothing, so consumer rules on the element keep winning.
17
+ // A set value is emitted behind a runtime token: override --smui-font-<variant>-transform
18
+ // / -letter-spacing in CSS, or leave the compiled value.
19
+ & when not (@@t = auto) {
20
+ @tv: @@t;
21
+ text-transform: ~"var(--@{t}, @{tv})";
22
+ }
23
+ & when not (@@ls = auto) {
24
+ @lsv: @@ls;
25
+ letter-spacing: ~"var(--@{ls}, @{lsv})";
26
+ }
14
27
  }
15
28
  }
16
29
  .smui-type(h1);
@@ -27,12 +40,6 @@
27
40
  .smui-type(overline);
28
41
  .smui-type(button);
29
42
 
30
- &_overline,
31
- &_button {
32
- text-transform: uppercase;
33
- letter-spacing: 0.06em;
34
- }
35
-
36
43
  &_color-primary {
37
44
  color: var(--smui-primaryColor);
38
45
  }
@@ -64,6 +64,7 @@
64
64
  @import "./components/rating/rating.less";
65
65
  @import "./components/skeleton/skeleton.less";
66
66
  @import "./components/chart/chart.less";
67
+ @import "./components/dagView/dagView.less";
67
68
  @import "./components/stepper/stepper.less";
68
69
  @import "./components/typography/typography.less";
69
70
  @import "./components/stack/stack.less";
@@ -15,11 +15,18 @@
15
15
  opacity: 1;
16
16
  }
17
17
 
18
+ // The resizer is one of the head cell's actions. It sits on the cell's edge, out
19
+ // of the flow, so alone it takes no room there: no gap after the sort control.
20
+ .smwb-head-cell .smwb-head-cell__actions:has(> .smwb-rt-table--resizer:only-child) {
21
+ margin-inline-start: 0;
22
+ }
23
+
18
24
  @media (hover: hover) {
19
25
  .smwb-rt-table--resizer {
20
26
  opacity: 0.5;
21
27
  }
22
28
 
29
+ .smwb-head-cell:hover .smwb-rt-table--resizer,
23
30
  *:hover > .smwb-rt-table--resizer {
24
31
  opacity: 1;
25
32
  }
@@ -94,6 +94,7 @@
94
94
  @avatarSizeLarge: 56px;
95
95
  @avatarGroupOverlap: @smui-space-sm;
96
96
  @avatarGroupBorder: 2px;
97
+ @avatarGroupMaxStacked: 12; // children laid each over the next; any past these overlap the other way
97
98
 
98
99
  @breadcrumbsGap: @smui-space-sm;
99
100
 
@@ -373,7 +374,8 @@
373
374
  @menuDisabledTextColor: @disabledTextColor;
374
375
  @menuOpacityHover: 0.08;
375
376
  @menuOpacityActive: 0.12;
376
- @menuOpacity-focus: 0.12;
377
+ @menuOpacityFocus: 0.12;
378
+ @menuOpacity-focus: @menuOpacityFocus; // former name, kept so Less that reads it still compiles
377
379
  @menuOpacitySelected: 0.08;
378
380
  @menuDividerColor: @dividerColor;
379
381
 
@@ -456,6 +458,46 @@
456
458
  @messageBgLight: ~"color-mix(in srgb, @{primaryColor} 10%, transparent)";
457
459
  @messageBorderRadius: @smui-radius-xs;
458
460
 
461
+ // ---- Chart ----
462
+ // Series colours in order, up to 12 (--smwb-chart-series-N). `false` = not set:
463
+ // the token is not emitted, and the chart draws only the colours that are set.
464
+ // Fill slots in order: an unset slot is skipped, so later colours shift up
465
+ // (slot 9 without slot 8 is drawn for the 8th series).
466
+ // Okabe-Ito, apart under colour-vision deficiencies.
467
+ @chartSeries1: #0072b2;
468
+ @chartSeries2: #e69f00;
469
+ @chartSeries3: #009e73;
470
+ @chartSeries4: #cc79a7;
471
+ @chartSeries5: #56b4e9;
472
+ @chartSeries6: #d55e00;
473
+ @chartSeries7: #7a7a7a;
474
+ @chartSeries8: false;
475
+ @chartSeries9: false;
476
+ @chartSeries10: false;
477
+ @chartSeries11: false;
478
+ @chartSeries12: false;
479
+
480
+ // ---- DagView ----
481
+ // The graph's colours, as --smwb-dag-view-* on .smwb-dag-view. The defaults
482
+ // are the theme's runtime tokens, so the dark theme follows on its own.
483
+ @dagViewNodeBg: ~"var(--smui-surfaceBackground)";
484
+ @dagViewNodeBorder: ~"var(--smui-outlineColor)";
485
+ @dagViewEdgeColor: ~"var(--smui-outlineColor)";
486
+ @dagViewEdgeActiveColor: ~"var(--smui-primaryColor)";
487
+ @dagViewSelectedRing: ~"var(--smui-primaryColor)";
488
+ // A node's accent (its stripe and dot) by status; any other status is drawn as idle.
489
+ @dagViewStatusIdle: ~"var(--smui-outlineColor)";
490
+ @dagViewStatusPending: ~"var(--smui-onSurfaceVariant)";
491
+ @dagViewStatusRunning: ~"var(--smui-infoColor)";
492
+ @dagViewStatusSuccess: ~"var(--smui-successColor)";
493
+ @dagViewStatusWarning: ~"var(--smui-warningColor)";
494
+ @dagViewStatusError: ~"var(--smui-errorColor)";
495
+ @dagViewStatusSkipped: ~"var(--smui-disabledTextColor)";
496
+ // How far the nodes and edges away from the selection fade, with dimUnrelated.
497
+ @dagViewDimOpacity: 0.35;
498
+ // The graph's height when the app sets none (its `height` prop or `style`).
499
+ @dagViewHeight: 480px;
500
+
459
501
  /*===================================================== DARK THEME =================================================*/
460
502
  /*-------------------
461
503
  Common
@@ -614,3 +656,33 @@
614
656
  Message
615
657
  --------------------*/
616
658
  @messageBgDark: #323232;
659
+
660
+ // ---- Chart ----
661
+ // Same rule as the light set: fill in order, an unset slot is skipped.
662
+ @chartSeries1Dark: #5aa9e6;
663
+ @chartSeries2Dark: #f0b43a;
664
+ @chartSeries3Dark: #2fc59b;
665
+ @chartSeries4Dark: #e08cc0;
666
+ @chartSeries5Dark: #8fd0f5;
667
+ @chartSeries6Dark: #f27c3c;
668
+ @chartSeries7Dark: #a8b0bc;
669
+ @chartSeries8Dark: false;
670
+ @chartSeries9Dark: false;
671
+ @chartSeries10Dark: false;
672
+ @chartSeries11Dark: false;
673
+ @chartSeries12Dark: false;
674
+
675
+ // ---- DagView ----
676
+ // Dark-only values; `false` = not set: the light value (a runtime token) applies.
677
+ @dagViewNodeBgDark: false;
678
+ @dagViewNodeBorderDark: false;
679
+ @dagViewEdgeColorDark: false;
680
+ @dagViewEdgeActiveColorDark: false;
681
+ @dagViewSelectedRingDark: false;
682
+ @dagViewStatusIdleDark: false;
683
+ @dagViewStatusPendingDark: false;
684
+ @dagViewStatusRunningDark: false;
685
+ @dagViewStatusSuccessDark: false;
686
+ @dagViewStatusWarningDark: false;
687
+ @dagViewStatusErrorDark: false;
688
+ @dagViewStatusSkippedDark: false;
@@ -5,6 +5,13 @@
5
5
  // <Typography> component (smwb-typography_<variant>) and reusable by
6
6
  // any component. Override a step from theme.config, e.g.
7
7
  // @smui-font-h1-size: 40px;
8
+ // Letter case and spacing per variant are theme variables too; set them from
9
+ // theme.config to, say, put the headings in capitals:
10
+ // @smui-font-h1-transform: uppercase; @smui-font-h1-letter-spacing: 0.06em;
11
+ // The defaults are `auto`, a marker meaning the variant declares nothing of its
12
+ // own, except overline and button, which have always been uppercase with 0.06em.
13
+ // A set value is also readable at runtime as --smui-font-<variant>-transform /
14
+ // -letter-spacing (the compiled value is the var() fallback).
8
15
  //=====================================================================
9
16
 
10
17
  @smui-font-family: var(--smui-fontFamily, inherit);
@@ -28,3 +35,17 @@
28
35
  @smui-font-caption-size: 12px; @smui-font-caption-weight: 400; @smui-font-caption-lh: 1.4;
29
36
  @smui-font-overline-size: 12px; @smui-font-overline-weight: 600; @smui-font-overline-lh: 1.4;
30
37
  @smui-font-button-size: 14px; @smui-font-button-weight: 500; @smui-font-button-lh: 1.75;
38
+
39
+ @smui-font-h1-transform: auto; @smui-font-h1-letter-spacing: auto;
40
+ @smui-font-h2-transform: auto; @smui-font-h2-letter-spacing: auto;
41
+ @smui-font-h3-transform: auto; @smui-font-h3-letter-spacing: auto;
42
+ @smui-font-h4-transform: auto; @smui-font-h4-letter-spacing: auto;
43
+ @smui-font-h5-transform: auto; @smui-font-h5-letter-spacing: auto;
44
+ @smui-font-h6-transform: auto; @smui-font-h6-letter-spacing: auto;
45
+ @smui-font-subtitle1-transform: auto; @smui-font-subtitle1-letter-spacing: auto;
46
+ @smui-font-subtitle2-transform: auto; @smui-font-subtitle2-letter-spacing: auto;
47
+ @smui-font-body1-transform: auto; @smui-font-body1-letter-spacing: auto;
48
+ @smui-font-body2-transform: auto; @smui-font-body2-letter-spacing: auto;
49
+ @smui-font-caption-transform: auto; @smui-font-caption-letter-spacing: auto;
50
+ @smui-font-overline-transform: uppercase; @smui-font-overline-letter-spacing: 0.06em;
51
+ @smui-font-button-transform: uppercase; @smui-font-button-letter-spacing: 0.06em;
package/less/theme.less CHANGED
@@ -2,7 +2,7 @@
2
2
  Theme
3
3
  -------------------*/
4
4
  @theme: "";
5
- @themesFolder: "";
5
+ @themesFolder: "./themes";
6
6
 
7
7
  /*--------------------
8
8
  Site variables
@@ -170,6 +170,7 @@
170
170
  @menuDisabledTextColor: @disabledTextColor;
171
171
  @menuOpacityHover: 0.08;
172
172
  @menuOpacityActive: 0.12;
173
+ @menuOpacityFocus: 0.12;
173
174
  @menuOpacitySelected: 0.08;
174
175
  @menuDividerColor: rgba(0, 0, 0, 0.12);
175
176
 
@@ -166,6 +166,7 @@
166
166
  @menuDisabledTextColor: @disabledTextColor;
167
167
  @menuOpacityHover: 0.08;
168
168
  @menuOpacityActive: 0.12;
169
+ @menuOpacityFocus: 0.12;
169
170
  @menuOpacitySelected: 0.08;
170
171
  @menuDividerColor: rgba(0, 0, 0, 0.12);
171
172
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smwb/ui-styles",
3
- "version": "1.5.0",
3
+ "version": "1.6.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"
22
+ "lint": "node scripts/check-aggregate.mjs && node scripts/check-tokens.mjs && node scripts/check-typography-case.mjs && node scripts/check-chart-series.mjs"
23
23
  },
24
24
  "homepage": "https://sourcecraft.dev/elfenmail/ui",
25
25
  "repository": {