@smwb/ui-styles 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +210 -12
  2. package/less/components/appBar/appBar.less +108 -0
  3. package/less/components/avatar/avatar.less +22 -0
  4. package/less/components/button/button.less +7 -1
  5. package/less/components/card/card.less +15 -4
  6. package/less/components/chart/chart.less +16 -15
  7. package/less/components/checkbox/checkbox.less +55 -4
  8. package/less/components/chip/chip.less +48 -3
  9. package/less/components/commandPalette/commandPalette.entry.less +2 -0
  10. package/less/components/commandPalette/commandPalette.less +233 -0
  11. package/less/components/composer/composer.entry.less +2 -0
  12. package/less/components/composer/composer.less +294 -0
  13. package/less/components/composer/composer.variables.less +10 -0
  14. package/less/components/dagView/dagView.entry.less +2 -0
  15. package/less/components/dagView/dagView.less +280 -0
  16. package/less/components/descriptionList/descriptionList.less +7 -1
  17. package/less/components/diffStat/diffStat.entry.less +2 -0
  18. package/less/components/diffStat/diffStat.less +76 -0
  19. package/less/components/kbd/kbd.entry.less +2 -0
  20. package/less/components/kbd/kbd.less +55 -0
  21. package/less/components/list/list.less +39 -0
  22. package/less/components/menu/menu.less +25 -1
  23. package/less/components/personChip/personChip.less +4 -2
  24. package/less/components/qrCode/qrCode.entry.less +2 -0
  25. package/less/components/qrCode/qrCode.less +35 -0
  26. package/less/components/radioButton/radioButton.less +46 -1
  27. package/less/components/rating/rating.less +15 -2
  28. package/less/components/selectField/selectField.less +87 -5
  29. package/less/components/sidebar/sidebar.less +26 -0
  30. package/less/components/statusDot/statusDot.entry.less +2 -0
  31. package/less/components/statusDot/statusDot.less +118 -0
  32. package/less/components/swimlanes/swimlanes.entry.less +2 -0
  33. package/less/components/swimlanes/swimlanes.less +126 -0
  34. package/less/components/tabGroups/tabGroups.entry.less +2 -0
  35. package/less/components/tabGroups/tabGroups.less +219 -0
  36. package/less/components/table/table.less +12 -0
  37. package/less/components/tabs/tab/tab.less +128 -0
  38. package/less/components/tabs/tabPanel/tabPanel.entry.less +2 -0
  39. package/less/components/tabs/tabPanel/tabPanel.less +8 -0
  40. package/less/components/tabs/tabs.less +26 -0
  41. package/less/components/textField/textField.less +34 -9
  42. package/less/components/thumbCard/thumbCard.less +12 -2
  43. package/less/components/toggle/toggle.less +42 -2
  44. package/less/components/treeView/treeView.less +40 -2
  45. package/less/components/typography/typography.less +13 -6
  46. package/less/components.less +10 -0
  47. package/less/connects/rt/table.less +7 -0
  48. package/less/default/globals/default.variables.less +152 -1
  49. package/less/foundation/sizing.less +41 -5
  50. package/less/foundation/typography.less +21 -0
  51. package/less/themes/material/variables.less +1 -0
  52. package/less/themes/summer/variables.less +1 -0
  53. package/less/variables.less +5 -0
  54. package/package.json +5 -2
package/README.md CHANGED
@@ -19,18 +19,18 @@ npm install @smwb/ui-styles
19
19
 
20
20
  ## What's inside
21
21
 
22
- | Path | Purpose |
23
- | --- | --- |
24
- | `less/components.less` | Aggregate entry — `@import`s foundation + every component |
25
- | `less/foundation/*` | Tokens: palette, spacing, radius, elevation, breakpoints, sizing |
26
- | `less/default/globals/default.variables.less` | Semantic colors + per-component LESS variables |
27
- | `less/variables.less` | Declares the `--smui-*` tokens and emits them on `:root` (light) + `[data-theme="dark"]` |
28
- | `less/components/<name>/<name>.entry.less` | Per-component entry (component rules only, no tokens) |
29
- | `less/tokens.entry.less` | The `--smui-*` tokens at zero specificity (`:where(:root)`) for per-component CSS |
30
- | `less/headless-base.less` | Foundation + theme variables only (no component rules) |
31
- | `less/connects/rt/table.entry.less` | The React TanStack Table adapter's own rules (column resizer) |
32
- | `less/themes/*` | Built-in theme packs (`summer`, `material`) |
33
- | `normalize.css` | Optional CSS reset |
22
+ | Path | Purpose |
23
+ | --------------------------------------------- | ---------------------------------------------------------------------------------------- |
24
+ | `less/components.less` | Aggregate entry — `@import`s foundation + every component |
25
+ | `less/foundation/*` | Tokens: palette, spacing, radius, elevation, breakpoints, sizing |
26
+ | `less/default/globals/default.variables.less` | Semantic colors + per-component LESS variables |
27
+ | `less/variables.less` | Declares the `--smui-*` tokens and emits them on `:root` (light) + `[data-theme="dark"]` |
28
+ | `less/components/<name>/<name>.entry.less` | Per-component entry (component rules only, no tokens) |
29
+ | `less/tokens.entry.less` | The `--smui-*` tokens at zero specificity (`:where(:root)`) for per-component CSS |
30
+ | `less/headless-base.less` | Foundation + theme variables only (no component rules) |
31
+ | `less/connects/rt/table.entry.less` | The React TanStack Table adapter's own rules (column resizer) |
32
+ | `less/themes/*` | Built-in theme packs (`summer`, `material`) |
33
+ | `normalize.css` | Optional CSS reset |
34
34
 
35
35
  ## Usage
36
36
 
@@ -57,6 +57,204 @@ 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
+ #### StatusDot colours
97
+
98
+ `StatusDot` reads only runtime tokens, so the dark theme follows without extra settings: `active` takes
99
+ `--smui-infoColor`, `attention` `--smui-warningColor`, `idle` `--smui-onSurfaceVariant`, `done`
100
+ `--smui-successColor`, `error` `--smui-errorColor` and `unknown` `--smui-outlineColor` (as a ring); `color` takes
101
+ the matching `--smui-*Color` token (`default` is `--smui-onSurfaceVariant`). Its sizes are `@statusDotSizeSm`
102
+ (8px), `@statusDotSizeMd` (12px) and `@statusDotRing` (2px, the ring of `unknown`) in `theme.config`.
103
+
104
+ Against `--smui-surfaceBackground` and `--smui-pageBackground`, the default theme's status colours measure
105
+ at least 3:1 in both modes (the lowest is `attention` on the light surface, 3.04:1). The semantic colours are one
106
+ value for both modes, so a theme that makes `@warningColor` lighter, or any of them darker for the dark theme,
107
+ changes the numbers: re-check them there. `color="secondary"` is the one name that falls short in the default dark
108
+ theme (2.65:1): use a status for state.
109
+
110
+ #### QrCode colours
111
+
112
+ `QrCode` never reads theme tokens for the code itself: the modules are always `@qrCodeDarkColor`
113
+ (`rgb(0 0 0)`) on `@qrCodeLightColor` (`rgb(255 255 255)`), including the quiet zone, so a phone camera
114
+ reads the code in the dark theme, under forced colours and in print (`forced-color-adjust: none` and
115
+ `print-color-adjust: exact` are set on the svg, and the same colours are written as SVG `fill` attributes,
116
+ so a copied or printed svg scans without the library CSS). Only the caption is themed
117
+ (`--smui-onSurfaceVariant`). The corner rounding is `@qrCodeRadius` (`@smui-radius-sm`); the svg clips to
118
+ it, and the quiet zone keeps the modules clear of the rounding.
119
+
120
+ #### DagView colours
121
+
122
+ The dependency graph (`DagView` in `@smwb/ui-solid` and `@smwb/ui-react`) takes its colours from theme variables that default to the
123
+ runtime tokens, so the dark theme follows without extra settings:
124
+
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
+
136
+ The statuses are `idle`, `pending`, `running`, `success`, `warning`, `error` and `skipped`; a node with any other
137
+ status is drawn as `idle`. Each colour variable has a `…Dark` twin (e.g. `@dagViewStatusSuccessDark`), `false` by
138
+ default: set it to give the dark theme its own value. At runtime, set the properties on the graph itself, since it
139
+ declares them on its own root: in its `style`, or with a class in a selector that outranks `.smwb-dag-view`, such as
140
+ `.smwb-dag-view.my-graph` (a plain `.my-graph` ties with it, and the graph's stylesheet may load after yours). A
141
+ 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
142
+ `accent` prop in front, as the built-in statuses do:
143
+ `.smwb-dag-view__node[data-status="queued"] { --smwb-dag-view-accent: var(--smwb-dag-view-node-accent, teal) }`.
144
+ The graph's default height, `@dagViewHeight`, is set by the stylesheet, so its `height` prop or a height in its
145
+ `style` wins over it.
146
+
147
+ #### Swimlanes
148
+
149
+ `Swimlanes` (in `@smwb/ui-react` and `@smwb/ui-solid`) reads only runtime tokens, so the dark theme follows without
150
+ extra settings: the grid's surface and the sticky header's opaque background come from `--smui-surfaceBackground`,
151
+ the row divider from `--smui-dividerColor`, the hover tint from `--smui-onSurfaceHover`, the selected tint, the
152
+ selection stripe and the focus ring from `--smui-primaryColor`, and a disabled lane's text from
153
+ `--smui-disabledTextColor`.
154
+
155
+ | Variable (`theme.config`) | Runtime property on `.smwb-swimlanes` | Default |
156
+ | ------------------------- | ----------------------------------------------- | ------- |
157
+ | `@swimlanesHeaderWidth` | the fallback of `--smwb-swimlanes-header-width` | `240px` |
158
+ | `@swimlanesHeight` | — (the grid's `height`) | `480px` |
159
+
160
+ Three custom properties drive the layout and the component writes them on its own root:
161
+ `--smwb-swimlanes-row-height` (always, from `rowHeight`), `--smwb-swimlanes-header-width` (only with `headerWidth`)
162
+ and `--smwb-swimlanes-content-min-width` (only with `contentMinWidth`). The last two are yours to set, through the
163
+ props or at runtime on the grid itself (its `style`, which is merged last and wins). The row height is not: the
164
+ virtual window's maths needs it in JS, so it goes through `rowHeight` alone, and a different value in `style` would
165
+ resize the rows in CSS while the window still counts with `rowHeight`, leaving gaps or overlaps. A row's state is one
166
+ internal property, `--smwb-swimlanes-tint`, set on each row and read by both cells so the lane reads as one band:
167
+ `transparent`, `--smui-onSurfaceHover` on hover, and 12% of `--smui-primaryColor` when selected. It is declared on
168
+ the row, so a value set on the grid never reaches the cells.
169
+
170
+ `@swimlanesHeight` is set by the stylesheet, so the `height` prop or a height in `style` wins over it. A selected
171
+ lane also gets a 3px primary stripe on its header, so the selection is not shown by tint alone; forced colours draw
172
+ it as a `Highlight` outline on both cells instead. Cells clip, so content that needs more room needs a wider
173
+ `contentMinWidth`.
174
+
175
+ #### Composer
176
+
177
+ `Composer` is the task input: a textarea that grows from `minRows` to `maxRows`, an optional attach button, a
178
+ `toolbar` slot, a character counter and a send button that turns into Stop while the agent is busy. Its stylesheet is
179
+ `components/composer/composer.less` (per-component entry: `composer.entry.less`). The root's children are flat, and CSS
180
+ grid areas place them for each layout; the spacing is margins, so an absent child leaves no gap.
181
+
182
+ | Class | Element |
183
+ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
184
+ | `.smwb-composer` | the root; `_stacked` (field on top, a bar under it: attach, toolbar, counter, action) or `_inline` (one row: attach, field, counter, action; the toolbar, if any, under it) |
185
+ | `_focused`, `_disabled`, `_drag-active`, `_over-limit`, `_busy` | root states; `_focused` follows the textarea's focus, not `:focus-within` |
186
+ | `.smwb-composer__attachments`, `__toolbar` | slots above the field and in the bar |
187
+ | `.smwb-composer__field` | the textarea's wrapper; its `data-value` mirrors the text, and `--smwb-composer-min-rows` / `--smwb-composer-max-rows` set the height range |
188
+ | `.smwb-composer__input` | the textarea |
189
+ | `.smwb-composer__icon-button` | a round native button; with `__attach` and `__action` (`__action_stop` while busy) |
190
+ | `.smwb-composer__counter` (`_error`) | the `n / max` text |
191
+ | `.smwb-composer__status` | the visually hidden live region for the counter |
192
+
193
+ The textarea grows in CSS only: `.smwb-composer__field::after` repeats the text (`content: attr(data-value)`) in a
194
+ hidden box that shares the textarea's font, padding and line height, and the textarea fills the grid cell that box
195
+ sizes, up to `max-height`. Nothing is measured, so the height is right in server-rendered markup and follows a width
196
+ change. The send button is not natively disabled while it is unavailable: `aria-disabled="true"` mutes it, so it keeps
197
+ focus.
198
+
199
+ The colours are theme variables that default to the runtime tokens, so the dark theme follows without extra settings:
200
+
201
+ | Variable (`theme.config`) | Default |
202
+ | -------------------------------------------------------- | --------------------------------------------------------------------------------------- |
203
+ | `@composerBg`, `@composerBorder`, `@composerBorderFocus` | `var(--smui-surfaceBackground)`, `var(--smui-outlineColor)`, `var(--smui-primaryColor)` |
204
+ | `@composerText`, `@composerPlaceholder` | `var(--smui-onSurfaceColor)`, `var(--smui-onSurfaceVariant)` |
205
+ | `@composerCounter`, `@composerCounterError` | `var(--smui-onSurfaceVariant)`, `var(--smui-errorColor)` |
206
+ | `@composerActionBg`, `@composerActionColor` | `var(--smui-primaryColor)`, `var(--smui-onPrimaryColor)` |
207
+ | `@composerStopBg`, `@composerStopColor` | `var(--smui-onSurfaceColor)`, `var(--smui-surfaceBackground)` |
208
+ | `@composerDragBg` | `var(--smui-primaryContainer)` |
209
+
210
+ Sizes live in `composer.variables.less`: `@composerPadding`, `@composerGap`, `@composerRadius`, `@composerLineHeight`
211
+ (one text line, `1.5rem`), `@composerFieldPaddingY` / `@composerFieldPaddingX` and `@composerIconButtonSize`.
212
+
213
+ #### DiffStat colours
214
+
215
+ `DiffStat` draws its figures and cells in the theme's `--smui-successColor` and `--smui-errorColor`, each mixed toward
216
+ `--smui-onSurfaceColor` so that the text keeps 4.5:1 contrast (the raw tokens measure about 3.9–4.3:1) and a cell
217
+ keeps 3:1 against a neutral one, in the light and the dark theme alike. A cell nobody owns is an outline of the surface ink
218
+ (60% of it, 3.69:1 or better against the page and the surface in every bundled theme, light and dark), not a fill:
219
+ a fill light enough to sit quietly measured 1.08–1.29:1 against a light page and made "4 of 5" look like "5 of 5":
220
+
221
+ | Variable (`theme.config`) | Runtime property on `.smwb-diff-stat` | Default |
222
+ | ------------------------- | ------------------------------------- | --------------------------------------------------------------- |
223
+ | `@diffStatColorMix` | — (compiled into the two below) | `55%` of the success or error token |
224
+ | — | `--smwb-diff-stat-added` | success token mixed toward the surface ink |
225
+ | — | `--smwb-diff-stat-removed` | error token mixed toward the surface ink |
226
+ | `@diffStatNeutralMix` | `--smwb-diff-stat-neutral` | the surface ink at `60%`, the outline colour of an unowned cell |
227
+
228
+ Sizes are `@diffStatFontSize`, `@diffStatCompactFontSize`, `@diffStatCellWidth`, `@diffStatCellHeight`,
229
+ `@diffStatCompactCellHeight`, `@diffStatCellGap` and `@diffStatCellRadius`. At runtime, set the three properties on the
230
+ `DiffStat` itself (its `style`, or a class that outranks `.smwb-diff-stat`).
231
+
232
+ #### AppBar window controls
233
+
234
+ `AppBar variant="window"` is the title bar of a desktop app window (Electron, Tauri, an installed PWA with Window
235
+ Controls Overlay): the bar drags the window (`-webkit-app-region: drag`) and every control inside it is taken out of
236
+ the drag region. The width reserved for the OS window buttons is `@appBarWindowControlsLeft` (64px, the macOS traffic
237
+ lights) and `@appBarWindowControlsRight` (138px, three 46px Windows/Linux caption buttons). Both are read through one
238
+ runtime property, so an app retunes them without recompiling:
239
+
240
+ ```css
241
+ .smwb-appbar.smwb-appbar_variant-window {
242
+ --smui-appBarWindowControlsWidth: 84px;
243
+ }
244
+ ```
245
+
246
+ `--smui-appBarWindowControlsWidth` is deliberately **not** declared in `variables.less`: it is read with a fallback,
247
+ so unset means the Less defaults above. With `windowControls="auto"` the insets come from the shell instead —
248
+ `env(titlebar-area-x)` / `env(titlebar-area-width)` — and are 0 in a plain browser tab; `left`, `right` and `none`
249
+ pin them with a class. The insets are `padding-left` / `padding-right` on the bar, added to `@appBarPaddingX`, and
250
+ they survive `dense`. Overlays are `no-drag` globally — `[data-smwb-overlay-layer]`, `.smwb-menu`, `.smwb-tooltip`,
251
+ `.smwb-snackbars` and the library's own full-screen portals (`.smwb-chat-attachment-image__overlay`,
252
+ `.smwb-image-modal__overlay`, `.smwb-thumb-card__preview`, `.smwb-chat-shell`): Electron hit-tests drag regions by
253
+ geometry, and those popups portal to `<body>`. Inside the bar, the roles that keep `tabindex="-1"` while the group
254
+ holds the tab stop (`menuitemcheckbox`, `menuitemradio`, `treeitem`, `spinbutton`, `gridcell`, `listbox`,
255
+ `scrollbar`) are listed next to the focusable elements. A control the generic list cannot see opts out with
256
+ `.smwb-appbar__no-drag` or `data-no-drag`.
257
+
60
258
  ## License
61
259
 
62
260
  [MIT](./LICENSE)
@@ -39,6 +39,90 @@
39
39
  padding: @appBarActionPadding;
40
40
  height: 48px;
41
41
  }
42
+ // `variant="window"`: the bar is the title bar of a desktop app window (Electron, Tauri, an installed PWA
43
+ // with Window Controls Overlay). Compound and ordered after `&.smwb-dense` above, so its side insets
44
+ // out-rank the dense `padding` shorthand instead of being wiped by it.
45
+ &.smwb-appbar_variant-window {
46
+ -webkit-app-region: drag;
47
+ app-region: drag;
48
+ // Dragging the window out of the title must not select the title text.
49
+ user-select: none;
50
+ // Width to keep clear of the OS window buttons on each side. With no class at all (`windowControls="auto"`)
51
+ // the area comes from the shell: `env(titlebar-area-*)` is exact under Window Controls Overlay, and in a
52
+ // plain browser tab the fallbacks make both insets 0, so the bar looks like `variant="default"`.
53
+ --smwb-appbar-wc-left: env(titlebar-area-x, 0px);
54
+ --smwb-appbar-wc-right: calc(100vw - env(titlebar-area-x, 0px) - env(titlebar-area-width, 100vw));
55
+ padding-left: calc(@appBarPaddingX + var(--smwb-appbar-wc-left));
56
+ padding-right: calc(@appBarPaddingX + var(--smwb-appbar-wc-right));
57
+ // `left`: the macOS traffic lights (`titleBarStyle: "hiddenInset"`), which Electron does not report
58
+ // through env() unless `titleBarOverlay` is on, hence the fixed width.
59
+ &.smwb-appbar_window-controls-left {
60
+ --smwb-appbar-wc-left: env(titlebar-area-x, var(--smui-appBarWindowControlsWidth, @appBarWindowControlsLeft));
61
+ }
62
+ // `right`: the caption buttons Windows/Linux draw at the right edge of the window. The fallback keeps
63
+ // their width clear by shortening the area the shell would have reported.
64
+ &.smwb-appbar_window-controls-right {
65
+ --smwb-appbar-wc-right: calc(
66
+ 100vw - env(titlebar-area-x, 0px) -
67
+ env(titlebar-area-width, calc(100vw - var(--smui-appBarWindowControlsWidth, @appBarWindowControlsRight)))
68
+ );
69
+ }
70
+ &.smwb-appbar_window-controls-none {
71
+ --smwb-appbar-wc-left: 0px;
72
+ --smwb-appbar-wc-right: 0px;
73
+ }
74
+ // Electron delivers no mouse events at all inside a drag region, so every control in the bar has to opt
75
+ // out. Focusable and role-tagged elements are matched generically (the list is `:where()`, so an app's own
76
+ // selector still wins); a custom control that is neither takes `.smwb-appbar__no-drag` or `data-no-drag`.
77
+ :where(
78
+ a[href],
79
+ button,
80
+ input,
81
+ select,
82
+ textarea,
83
+ label,
84
+ summary,
85
+ iframe,
86
+ video[controls],
87
+ [contenteditable]:not([contenteditable="false"]),
88
+ [tabindex]:not([tabindex="-1"]),
89
+ [role="button"],
90
+ [role="link"],
91
+ [role="menuitem"],
92
+ [role="tab"],
93
+ [role="checkbox"],
94
+ [role="switch"],
95
+ [role="radio"],
96
+ [role="combobox"],
97
+ [role="textbox"],
98
+ [role="searchbox"],
99
+ [role="slider"],
100
+ [role="option"],
101
+ // Roving-tabindex widgets and the cells of composite widgets: while the group holds
102
+ // the tab stop their items carry `tabindex="-1"`, so `[tabindex]:not([tabindex="-1"])`
103
+ // above never matches them and only the role gets them out of the drag region.
104
+ [role="menuitemcheckbox"],
105
+ [role="menuitemradio"],
106
+ [role="treeitem"],
107
+ [role="spinbutton"],
108
+ [role="gridcell"],
109
+ [role="listbox"],
110
+ [role="scrollbar"],
111
+ .smwb-tooltip__wrapper,
112
+ .smwb-appbar__no-drag,
113
+ [data-no-drag]
114
+ ) {
115
+ -webkit-app-region: no-drag;
116
+ app-region: no-drag;
117
+ }
118
+ // Dense keeps the side insets and drops the vertical padding, which the 48px height has no room for.
119
+ // The insets are repeated here on purpose: `responsive.less` is imported after every component and resets
120
+ // `.smwb-appbar.smwb-dense` at phone widths, which would otherwise beat the two longhands above.
121
+ &.smwb-dense {
122
+ padding: 0 calc(@appBarPaddingX + var(--smwb-appbar-wc-right)) 0
123
+ calc(@appBarPaddingX + var(--smwb-appbar-wc-left));
124
+ }
125
+ }
42
126
  &__inner {
43
127
  display: flex;
44
128
  align-items: center;
@@ -52,6 +136,12 @@
52
136
  gap: @appBarActionsGap;
53
137
  white-space: nowrap;
54
138
  }
139
+ // The title may be a heading (`titleAs`): drop the UA margins and font so it
140
+ // looks the same as the div.
141
+ &__left__title {
142
+ margin: 0;
143
+ font: inherit;
144
+ }
55
145
  &__right {
56
146
  white-space: nowrap;
57
147
  }
@@ -74,6 +164,24 @@
74
164
  }
75
165
  }
76
166
 
167
+ // Overlays opened from a `variant="window"` bar are portaled to `<body>`, outside the bar's own subtree, but
168
+ // Electron hit-tests drag regions by geometry: a dropdown drawn over the bar would be unclickable. `no-drag`
169
+ // is ignored outside app windows, so this is harmless for every other page. The library's own full-screen
170
+ // portals are listed by class because they carry no `data-smwb-overlay-layer`: a lightbox opened over the top
171
+ // of the bar would otherwise stay a drag region, so a backdrop click there would drag the window instead of
172
+ // closing it.
173
+ [data-smwb-overlay-layer],
174
+ .smwb-menu,
175
+ .smwb-tooltip,
176
+ .smwb-snackbars,
177
+ .smwb-chat-attachment-image__overlay,
178
+ .smwb-image-modal__overlay,
179
+ .smwb-thumb-card__preview,
180
+ .smwb-chat-shell {
181
+ -webkit-app-region: no-drag;
182
+ app-region: no-drag;
183
+ }
184
+
77
185
  .smwb-appbar_color-primary {
78
186
  background-color: var(--smui-primaryColor);
79
187
  color: var(--smui-onPrimaryColor);
@@ -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
+ }
@@ -33,14 +33,17 @@
33
33
  &.smwb-small {
34
34
  font-size: @smallButtonFontSize;
35
35
  padding: @buttonPaddingSmall;
36
+ min-height: var(--smui-control-h-sm, @smui-control-h-sm);
36
37
  }
37
38
  &.smwb-medium {
38
39
  font-size: @mediumButtonFontSize;
39
40
  padding: @buttonPaddingMedium;
41
+ min-height: var(--smui-control-h-md, @smui-control-h-md);
40
42
  }
41
43
  &.smwb-large {
42
44
  font-size: @largeButtonFontSize;
43
45
  padding: @buttonPaddingLarge;
46
+ min-height: var(--smui-control-h-lg, @smui-control-h-lg);
44
47
  }
45
48
 
46
49
  &.smwb-contained {
@@ -52,7 +55,10 @@
52
55
  }
53
56
  &:hover {
54
57
  background-color: var(--smui-buttonContainedHoverBg);
55
- box-shadow: 0 4px 22px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
58
+ box-shadow:
59
+ 0 4px 22px -2px rgba(0, 0, 0, 0.2),
60
+ 0 2px 2px 0 rgba(0, 0, 0, 0.14),
61
+ 0 1px 5px 0 rgba(0, 0, 0, 0.12);
56
62
  }
57
63
  &:disabled {
58
64
  background-color: var(--smui-buttonDisabledBg);
@@ -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
  }
@@ -1,8 +1,12 @@
1
1
  .smwb-text-field-checkbox {
2
- min-width: 0 !important;
2
+ min-width: 0 !important;
3
3
  }
4
4
 
5
5
  .smwb-checkbox {
6
+ // The box's size, which the native input takes too. The medium default is the
7
+ // selection token (18px), so the component's unstyled rendering is unchanged.
8
+ @box-size: @smui-selection-md;
9
+
6
10
  &__icon {
7
11
  opacity: 0;
8
12
  transition: all 0.1s ease-in-out;
@@ -11,8 +15,8 @@
11
15
  &__btn {
12
16
  user-select: none;
13
17
  flex-shrink: 0;
14
- width: 18px;
15
- height: 18px;
18
+ width: @box-size;
19
+ height: @box-size;
16
20
  border-radius: @checkboxBorderRadius;
17
21
  border: 2px solid var(--smui-checkboxBorderColor);
18
22
  background-color: transparent;
@@ -23,6 +27,10 @@
23
27
  position: relative;
24
28
  transition: all 0.2s ease-in-out;
25
29
  &::before {
30
+ // The hover/active halo stays 40px at every size. It is already the box's
31
+ // own minimum touch target (40px >= the md row height) and scales from the
32
+ // box's center, so shrinking it for sm would buy nothing and drop the
33
+ // largest size's halo below the 22px box it is meant to surround.
26
34
  width: 40px;
27
35
  height: 40px;
28
36
  border-radius: @circleBorderRadius;
@@ -36,6 +44,8 @@
36
44
  }
37
45
 
38
46
  &__wrapper {
47
+ // Anchors the absolutely positioned input (below) to the control itself.
48
+ position: relative;
39
49
  color: var(--smui-primaryTextColor);
40
50
  display: flex;
41
51
  align-items: center;
@@ -58,7 +68,19 @@
58
68
  }
59
69
 
60
70
  &__input {
61
- display: none;
71
+ // Visually hidden but rendered: with display: none the input would be out of
72
+ // the tab order and the accessibility tree. It lies under the box, which
73
+ // takes the pointer and passes clicks on through the label. With no insets
74
+ // of its own it sits where the box does: at the start, centered.
75
+ position: absolute;
76
+ width: @box-size;
77
+ height: @box-size;
78
+ margin: 0;
79
+ opacity: 0;
80
+ &:focus-visible ~ .smwb-checkbox__btn {
81
+ outline: 2px solid var(--smui-primaryColor);
82
+ outline-offset: 2px;
83
+ }
62
84
  &:hover {
63
85
  & ~ .smwb-checkbox__btn::before {
64
86
  background-color: var(--smui-onSurfaceHover);
@@ -106,4 +128,33 @@
106
128
  }
107
129
  }
108
130
  }
131
+
132
+ // The size prop maps the row onto the control-height scale and the box onto the
133
+ // selection scale. The size class rides the wrapper (the row), so min-height
134
+ // goes there; the box keeps a hard height, since a checkbox box must not stretch
135
+ // to fill the row. The hidden native input is resized with the box so its hit
136
+ // area stays coincident. medium is the component default and repeats today's
137
+ // 18px box exactly, so unstyled rendering does not change.
138
+ &_size-small {
139
+ min-height: var(--smui-control-h-sm, @smui-control-h-sm);
140
+ .smwb-checkbox__btn,
141
+ .smwb-checkbox__input {
142
+ width: @smui-selection-sm;
143
+ height: @smui-selection-sm;
144
+ }
145
+ }
146
+ // The medium box stays on the base rule's @box-size: @smui-selection-md
147
+ // read — repeating it here would only raise specificity and break consumer
148
+ // overrides of the default checkbox.
149
+ &_size-medium {
150
+ min-height: var(--smui-control-h-md, @smui-control-h-md);
151
+ }
152
+ &_size-large {
153
+ min-height: var(--smui-control-h-lg, @smui-control-h-lg);
154
+ .smwb-checkbox__btn,
155
+ .smwb-checkbox__input {
156
+ width: @smui-selection-lg;
157
+ height: @smui-selection-lg;
158
+ }
159
+ }
109
160
  }