actual-css 0.8.0 → 0.9.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/CHANGELOG.md +51 -0
- package/dist/actual.full.css +125 -41
- package/dist/actual.full.min.css +1 -1
- package/docs/pages/components/alert.md +6 -1
- package/docs/pages/components/dialog.md +5 -0
- package/docs/pages/components/drawer.md +13 -0
- package/docs/pages/components/skeleton.md +3 -1
- package/docs/pages/enhancements/flyout.md +16 -2
- package/docs/pages/forms/choice-cards.md +5 -4
- package/package.json +159 -158
- package/src/css/components/drawer.css +22 -7
- package/src/css/components/modal.css +1 -1
- package/src/css/components/skeleton.css +15 -4
- package/src/css/components/table.css +8 -0
- package/src/css/forms/choice.css +70 -26
- package/src/css/forms/switch.css +9 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
|
|
6
|
+
## [0.9.0] - 2026-09-13
|
|
7
|
+
|
|
8
|
+
### Breaking changes
|
|
9
|
+
|
|
10
|
+
- `--choice-control-offset` is renamed `--choice-control-nudge` and adds to the
|
|
11
|
+
derived first-line offset instead of replacing it — an old value is a whole
|
|
12
|
+
offset and will sit that far too low.
|
|
13
|
+
- `--skeleton-width` is a maximum inline size, not a width: a placeholder fills
|
|
14
|
+
its container and stops there, so it no longer gives every shrink-to-fit
|
|
15
|
+
ancestor a min-content floor. A placeholder in a container sized by its own
|
|
16
|
+
content now collapses instead of forcing that container open.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- `.choice` aligns its control on the first line's cap height with the `cap`
|
|
21
|
+
unit, so a checkbox, radio or switch is level with the label text in any
|
|
22
|
+
font — the previous constant was tuned for Segoe UI and sat ~2px low on
|
|
23
|
+
Roboto.
|
|
24
|
+
- `.switch` lays its knob out of flow, so the track's baseline is its own
|
|
25
|
+
border box.
|
|
26
|
+
- `dialog.drawer` is `--viewport-block` tall again, so a mobile browser's
|
|
27
|
+
collapsing URL bar no longer pushes its footer below the fold.
|
|
28
|
+
- `dialog.drawer` scrolls its body region, so the footer stays reachable
|
|
29
|
+
whatever the content length; the rule replaces the `nav`-only one.
|
|
30
|
+
- `.table-wrap` is `position: relative`, so an `.sr-only` header inside a wide
|
|
31
|
+
table no longer escapes the scroller and gives the page a phantom horizontal
|
|
32
|
+
scrollbar on small viewports.
|
|
33
|
+
|
|
34
|
+
### Documentation and tooling
|
|
35
|
+
|
|
36
|
+
- `tests/browser/mobile-overflow.test.js` asserts that no component scrolls the
|
|
37
|
+
page sideways at 320px and 360px, and that wide content scrolls inside its
|
|
38
|
+
own container.
|
|
39
|
+
- `bun run report:overflow` surveys demo and site pages at phone width and
|
|
40
|
+
bisects each failure down to the element that owns it.
|
|
41
|
+
- `tests/browser/mega-menu.test.js` exercises three wide sibling nav panels —
|
|
42
|
+
takeover, toggle, focus handover, keyboard open, Escape, clamping at both
|
|
43
|
+
viewport edges, and no drawer mutation on a phone.
|
|
44
|
+
- The flyout hooks are documented as a preferred width and a width cap, with
|
|
45
|
+
the cap's `20rem` default stated: a wide panel sets both, like `width` and
|
|
46
|
+
`max-width`.
|
|
47
|
+
- The drawer and scrollable-modal pages state that a scrolling body region is
|
|
48
|
+
keyboard-focusable, takes a focus ring, and can claim a dialog's initial
|
|
49
|
+
focus — put the close control in the `header`, or use `autofocus`.
|
|
50
|
+
- The kitchen-sink, blocks, blog, combobox and select-intents templates no
|
|
51
|
+
longer scroll sideways at phone widths.
|
|
52
|
+
- The alert trailing-action recipe and the dashboard template add `.items-center`:
|
|
53
|
+
a trailing control makes the row taller than a line of text, which left the
|
|
54
|
+
start-aligned `.alert-icon` above the message.
|
|
55
|
+
|
|
5
56
|
## [0.8.0] - 2026-09-10
|
|
6
57
|
|
|
7
58
|
Actual CSS 0.8 focuses on a cleaner component contract, more predictable sizing
|
package/dist/actual.full.css
CHANGED
|
@@ -2164,7 +2164,7 @@ small {
|
|
|
2164
2164
|
* geometry and states. switch.css is imported after this file.
|
|
2165
2165
|
*
|
|
2166
2166
|
* Public hooks:
|
|
2167
|
-
* --choice-control-
|
|
2167
|
+
* --choice-control-nudge
|
|
2168
2168
|
* --choice-border
|
|
2169
2169
|
* --choice-bg
|
|
2170
2170
|
* --choice-mark
|
|
@@ -2196,45 +2196,89 @@ small {
|
|
|
2196
2196
|
cursor: pointer;
|
|
2197
2197
|
}
|
|
2198
2198
|
|
|
2199
|
-
/* Optical alignment:
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2199
|
+
/* Optical alignment: the control reads level with the first line's cap height,
|
|
2200
|
+
not with the top of its line box. Every inline choice runs the same formula
|
|
2201
|
+
over its own block size, so a checkbox and a taller switch stay level with
|
|
2202
|
+
each other under any line-height or --choice-control-size — something a
|
|
2203
|
+
per-control constant cannot do.
|
|
2204
|
+
|
|
2205
|
+
Three tiers, each the best the engine can express:
|
|
2206
|
+
|
|
2207
|
+
- `cap` unit: exact, and the only font-independent answer. Where the cap
|
|
2208
|
+
center sits inside a line box depends on the font's ascent/descent/cap
|
|
2209
|
+
ratios, so no constant can serve both Segoe UI and Roboto — the same rule
|
|
2210
|
+
was a pixel high on Windows and two pixels low on Android. Baseline
|
|
2211
|
+
alignment puts the control's border-box bottom on the text baseline, and
|
|
2212
|
+
one inset drops it by half the difference between its own height and the
|
|
2213
|
+
cap height. Both classed controls synthesize that baseline from their
|
|
2214
|
+
border box because their mark and knob are out of flow.
|
|
2215
|
+
- `lh` only: center the control on the line box, which is within a pixel of
|
|
2216
|
+
the cap center for the usual UI faces.
|
|
2217
|
+
- neither: the constants that formula yields at the default 1.5
|
|
2218
|
+
line-height, 0.1875em for a checkbox and 0.125em for the switch track.
|
|
2219
|
+
|
|
2220
|
+
--choice-control-nudge adds to whichever tier applies, rather than replacing
|
|
2221
|
+
it: the derived value is exact now, so a design has a correction to make,
|
|
2222
|
+
not a formula to restate — and one number cannot stand in for three
|
|
2223
|
+
different references anyway. It stays fallback-only rather than declared
|
|
2224
|
+
here, so an override placed anywhere up the tree — :root included — reaches
|
|
2225
|
+
the control instead of losing to a component-level declaration. */
|
|
2216
2226
|
.choice > :is(input[type="checkbox"]:not([role="switch"]), input[type="radio"]) {
|
|
2217
2227
|
flex: 0 0 auto;
|
|
2218
|
-
margin-block-start: var(--choice-control-
|
|
2228
|
+
margin-block-start: calc(0.1875em + var(--choice-control-nudge, 0px));
|
|
2219
2229
|
}
|
|
2220
2230
|
|
|
2221
2231
|
.choice > .switch {
|
|
2222
2232
|
flex: 0 0 auto;
|
|
2223
|
-
margin-block-start: var(--choice-control-
|
|
2233
|
+
margin-block-start: calc(0.125em + var(--choice-control-nudge, 0px));
|
|
2224
2234
|
}
|
|
2225
2235
|
|
|
2226
2236
|
@supports (margin-block-start: 1lh) {
|
|
2227
2237
|
.choice > :is(input[type="checkbox"]:not([role="switch"]), input[type="radio"]) {
|
|
2228
|
-
margin-block-start:
|
|
2229
|
-
--choice-control-
|
|
2230
|
-
|
|
2238
|
+
margin-block-start: calc(
|
|
2239
|
+
(1lh - var(--choice-control-size)) /
|
|
2240
|
+
2 +
|
|
2241
|
+
var(--choice-control-nudge, 0px)
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
.choice > .switch {
|
|
2246
|
+
margin-block-start: calc(
|
|
2247
|
+
(1lh - var(--switch-block-size)) /
|
|
2248
|
+
2 +
|
|
2249
|
+
var(--choice-control-nudge, 0px)
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
@supports (margin-block-start: 1cap) {
|
|
2255
|
+
.choice {
|
|
2256
|
+
align-items: baseline;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
/* Baseline alignment owns the block position now; the tier above would only
|
|
2260
|
+
grow the row. An unclassed native input keeps the UA's own baseline, which
|
|
2261
|
+
is already tuned for text — the formula below describes the framework's
|
|
2262
|
+
geometry and would not fit it. */
|
|
2263
|
+
.choice > :is(input[type="checkbox"], input[type="radio"]) {
|
|
2264
|
+
margin-block-start: 0;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
/* inset-block-start rides the `position: relative` these controls already
|
|
2268
|
+
carry for their mark and knob. */
|
|
2269
|
+
.choice > :is(.check, .radio) {
|
|
2270
|
+
inset-block-start: calc(
|
|
2271
|
+
(var(--choice-control-size) - 1cap) /
|
|
2272
|
+
2 +
|
|
2273
|
+
var(--choice-control-nudge, 0px)
|
|
2231
2274
|
);
|
|
2232
2275
|
}
|
|
2233
2276
|
|
|
2234
2277
|
.choice > .switch {
|
|
2235
|
-
|
|
2236
|
-
--
|
|
2237
|
-
|
|
2278
|
+
inset-block-start: calc(
|
|
2279
|
+
(var(--switch-block-size) - 1cap) /
|
|
2280
|
+
2 +
|
|
2281
|
+
var(--choice-control-nudge, 0px)
|
|
2238
2282
|
);
|
|
2239
2283
|
}
|
|
2240
2284
|
}
|
|
@@ -2782,8 +2826,6 @@ small {
|
|
|
2782
2826
|
|
|
2783
2827
|
position: relative;
|
|
2784
2828
|
display: inline-grid;
|
|
2785
|
-
align-items: center;
|
|
2786
|
-
justify-content: start;
|
|
2787
2829
|
inline-size: var(--switch-width);
|
|
2788
2830
|
block-size: var(--switch-block-size);
|
|
2789
2831
|
margin: 0;
|
|
@@ -2802,9 +2844,17 @@ small {
|
|
|
2802
2844
|
against the track is what makes the knob legible. */
|
|
2803
2845
|
.switch::before {
|
|
2804
2846
|
content: "";
|
|
2847
|
+
/* Out of flow like the checkbox mark, so the track carries no in-flow item
|
|
2848
|
+
and the switch synthesizes its baseline from its own border box. A knob
|
|
2849
|
+
laid out as a grid item makes the baseline the knob's bottom edge instead,
|
|
2850
|
+
which is what .choice measures its cap-height offset from. The inset is
|
|
2851
|
+
the same --switch-knob-margin on both axes: it resolves against the
|
|
2852
|
+
padding box, so the border is already out of the way. */
|
|
2853
|
+
position: absolute;
|
|
2854
|
+
inset-block-start: var(--switch-knob-margin);
|
|
2855
|
+
inset-inline-start: var(--switch-knob-margin);
|
|
2805
2856
|
inline-size: var(--switch-knob-size);
|
|
2806
2857
|
block-size: var(--switch-knob-size);
|
|
2807
|
-
margin-inline: var(--switch-knob-margin);
|
|
2808
2858
|
border-radius: var(--radius-full);
|
|
2809
2859
|
background: var(--switch-knob);
|
|
2810
2860
|
transition:
|
|
@@ -4491,7 +4541,7 @@ dialog.modal {
|
|
|
4491
4541
|
/* --modal-size stays fallback-only: declaring it on dialog.modal gives it
|
|
4492
4542
|
type+class specificity, which a single author class cannot outrank, so the
|
|
4493
4543
|
public hook would silently keep this default. Same reason choice.css never
|
|
4494
|
-
declares --choice-control-
|
|
4544
|
+
declares --choice-control-nudge. */
|
|
4495
4545
|
max-inline-size: min(calc(100% - var(--dialog-viewport-gap-2x)), var(--modal-size, 32rem));
|
|
4496
4546
|
|
|
4497
4547
|
/* Stable viewport cap for the panel: dvh tracks the dynamic browser toolbar
|
|
@@ -4817,15 +4867,24 @@ dialog.drawer {
|
|
|
4817
4867
|
|
|
4818
4868
|
/* Keep the surface anchored to the viewport in the top layer. This also
|
|
4819
4869
|
provides the containing block for the absolutely positioned close button.
|
|
4820
|
-
|
|
4821
|
-
|
|
4870
|
+
|
|
4871
|
+
The panel reaches both viewport edges, so it has to follow the space that
|
|
4872
|
+
is actually on screen: --viewport-block resolves to 100dvh and tracks a
|
|
4873
|
+
mobile browser's collapsing URL bar. A percentage resolves against the
|
|
4874
|
+
layout viewport instead, which those browsers size as if the bar were
|
|
4875
|
+
hidden — that put the footer below the fold for as long as it showed. The
|
|
4876
|
+
modal pins the small viewport for the opposite reason: it floats, so a
|
|
4877
|
+
stable height beats a panel that breathes while the bar moves.
|
|
4878
|
+
|
|
4879
|
+
An embedded preview that fakes a viewport overrides --viewport-block on
|
|
4880
|
+
its own shell; the component does not degrade to a percentage for it. */
|
|
4822
4881
|
position: fixed;
|
|
4823
4882
|
inset-block-start: 0;
|
|
4824
4883
|
|
|
4825
4884
|
inline-size: min(calc(var(--viewport-inline) * 0.9), var(--drawer-size));
|
|
4826
|
-
block-size:
|
|
4885
|
+
block-size: var(--viewport-block);
|
|
4827
4886
|
max-inline-size: none;
|
|
4828
|
-
max-block-size:
|
|
4887
|
+
max-block-size: var(--viewport-block);
|
|
4829
4888
|
margin: 0;
|
|
4830
4889
|
padding: var(--drawer-pad);
|
|
4831
4890
|
border: 0;
|
|
@@ -4897,10 +4956,16 @@ dialog.drawer[open]::backdrop {
|
|
|
4897
4956
|
}
|
|
4898
4957
|
}
|
|
4899
4958
|
|
|
4900
|
-
/*
|
|
4901
|
-
|
|
4959
|
+
/* The body region absorbs the panel's height and scrolls. A drawer is a
|
|
4960
|
+
full-height shell by definition, so it always scrolls its body — unlike the
|
|
4961
|
+
modal, which sizes to its content and opts in with .scrollable. Header and
|
|
4962
|
+
footer keep their own tracks; .drawer-close is named because a generic
|
|
4963
|
+
exclusion list is easier to trust than its `position: absolute`. */
|
|
4964
|
+
dialog.drawer > :not(:is(header, footer, form, .drawer-close)),
|
|
4965
|
+
dialog.drawer > form > :not(:is(header, footer, .drawer-close)) {
|
|
4902
4966
|
min-block-size: 0;
|
|
4903
|
-
overflow
|
|
4967
|
+
overflow: auto;
|
|
4968
|
+
overscroll-behavior: contain;
|
|
4904
4969
|
}
|
|
4905
4970
|
|
|
4906
4971
|
@supports (transition-behavior: allow-discrete) {
|
|
@@ -5672,16 +5737,24 @@ dialog.drawer > form > header {
|
|
|
5672
5737
|
* --skeleton-track
|
|
5673
5738
|
* --skeleton-highlight
|
|
5674
5739
|
*/
|
|
5740
|
+
/* A placeholder fills its container up to --skeleton-width; the preset widths
|
|
5741
|
+
are a cap, never an intrinsic size. Stated the other way round — a rem
|
|
5742
|
+
`inline-size` capped by a percentage `max-inline-size` — the cap is ignored
|
|
5743
|
+
during intrinsic sizing, so every shrink-to-fit ancestor inherits the
|
|
5744
|
+
preset as a min-content floor: a card holding a title placeholder could not
|
|
5745
|
+
go below 18rem and pushed a phone's layout sideways. A percentage
|
|
5746
|
+
`inline-size` contributes nothing intrinsically and a rem cap only lowers
|
|
5747
|
+
the contribution, so the same geometry costs no floor. */
|
|
5675
5748
|
.skeleton {
|
|
5676
5749
|
--skeleton-size: 1rem;
|
|
5677
|
-
--skeleton-width:
|
|
5750
|
+
--skeleton-width: none;
|
|
5678
5751
|
--skeleton-radius: var(--radius);
|
|
5679
5752
|
--skeleton-track: var(--surface-subtle);
|
|
5680
5753
|
--skeleton-highlight: var(--surface-raised);
|
|
5681
5754
|
|
|
5682
5755
|
display: block;
|
|
5683
|
-
inline-size:
|
|
5684
|
-
max-inline-size:
|
|
5756
|
+
inline-size: 100%;
|
|
5757
|
+
max-inline-size: var(--skeleton-width);
|
|
5685
5758
|
block-size: var(--skeleton-size);
|
|
5686
5759
|
border-radius: var(--skeleton-radius);
|
|
5687
5760
|
background:
|
|
@@ -5705,11 +5778,14 @@ dialog.drawer > form > header {
|
|
|
5705
5778
|
--skeleton-width: 18rem;
|
|
5706
5779
|
}
|
|
5707
5780
|
|
|
5781
|
+
/* The one preset with a real intrinsic width: a disc is square, so it sizes
|
|
5782
|
+
rather than caps. The floor it does carry is the avatar size, which is small
|
|
5783
|
+
enough never to widen a layout. */
|
|
5708
5784
|
.skeleton[data-shape="avatar"] {
|
|
5709
5785
|
--skeleton-size: var(--avatar-size, 2.25rem);
|
|
5710
|
-
--skeleton-width: var(--skeleton-size);
|
|
5711
5786
|
--skeleton-radius: var(--radius-full);
|
|
5712
5787
|
|
|
5788
|
+
inline-size: var(--skeleton-size);
|
|
5713
5789
|
flex: 0 0 auto;
|
|
5714
5790
|
}
|
|
5715
5791
|
|
|
@@ -7623,6 +7699,14 @@ a.list-item[aria-current]::before {
|
|
|
7623
7699
|
* --table-cell-pad
|
|
7624
7700
|
*/
|
|
7625
7701
|
.table-wrap {
|
|
7702
|
+
/* Containing block for anything absolutely positioned inside the table —
|
|
7703
|
+
`.sr-only` on a column header above all. Without it that box resolves
|
|
7704
|
+
against the initial containing block, so it keeps the static position of a
|
|
7705
|
+
cell that may be a thousand pixels along the scrolled row, escapes this
|
|
7706
|
+
scroller, and extends the *document's* scrollable width. The page then
|
|
7707
|
+
scrolls sideways on a phone with nothing painted out there. */
|
|
7708
|
+
position: relative;
|
|
7709
|
+
|
|
7626
7710
|
inline-size: 100%;
|
|
7627
7711
|
max-inline-size: 100%;
|
|
7628
7712
|
min-inline-size: 0;
|