@signal9/era-ui 19.0.0 → 21.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.
- package/CHANGELOG.md +29 -0
- package/dist/dev/audit/audits/index.d.ts +2 -1
- package/dist/dev/audit/audits/index.js +3 -1
- package/dist/dev/audit/audits/wall-is-isotropic.d.ts +2 -0
- package/dist/dev/audit/audits/wall-is-isotropic.js +147 -0
- package/dist/era-ui.css +1 -1
- package/dist/os/taskbar.svelte +7 -1
- package/dist/styles/density.css +68 -67
- package/dist/ui/bar/bar.svelte +57 -14
- package/dist/ui/bar/bar.svelte.d.ts +78 -18
- package/package.json +6 -5
- package/skill/SKILL.md +12 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
## [21.0.0](https://github.com/sig-nine/era-ui/compare/v20.0.0...v21.0.0) (2026-08-09)
|
|
2
|
+
|
|
3
|
+
### ⚠ BREAKING CHANGES
|
|
4
|
+
|
|
5
|
+
* **bar:** a `Bar` with `size="control"` and the default `content="tier"`
|
|
6
|
+
now pads and gaps by `chip-inset-control` (1/2/3) instead of `icon-inset-chip`
|
|
7
|
+
(4/5/6). That is the concentric answer for the chip-tier content the variant
|
|
8
|
+
describes, and it is visibly tighter. A control bar that actually holds
|
|
9
|
+
icon-tier buttons was never described by `content="tier"` and was relying on the
|
|
10
|
+
old value being wrong in a helpful direction — give it `size="chip"`, or pass
|
|
11
|
+
the wall explicitly. `size="bar"` and `size="chip"` are unchanged in value;
|
|
12
|
+
`size="bar"` now names its inset `gutter`, which is what era already calls that
|
|
13
|
+
pairing, rather than borrowing icon-inset-chip's name for the same number.
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **bar:** a bar's wall is the concentric inset for the pair it actually holds ([45f76a8](https://github.com/sig-nine/era-ui/commit/45f76a8c83c6d810c5346ccff03e1750342a57b4))
|
|
18
|
+
|
|
19
|
+
## [20.0.0](https://github.com/sig-nine/era-ui/compare/v19.0.0...v20.0.0) (2026-08-09)
|
|
20
|
+
|
|
21
|
+
### ⚠ BREAKING CHANGES
|
|
22
|
+
|
|
23
|
+
* **release:** No consumer change required. This changes commit tooling and
|
|
24
|
+
the contents of future changelog entries, not the library.
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* **release:** every migration note I wrote was dropped from the changelog ([2e83596](https://github.com/sig-nine/era-ui/commit/2e8359651d70412e204b8636eaf6fcf07b64d296))
|
|
29
|
+
|
|
1
30
|
## [19.0.0](https://github.com/sig-nine/era-ui/compare/v18.0.0...v19.0.0) (2026-08-09)
|
|
2
31
|
|
|
3
32
|
### ⚠ BREAKING CHANGES
|
|
@@ -15,5 +15,6 @@ import { radiusConcentricity } from './radius-concentricity.js';
|
|
|
15
15
|
import { restingGap } from './resting-gap.js';
|
|
16
16
|
import { singleGlyphSquare } from './single-glyph-square.js';
|
|
17
17
|
import { uniformSiblingGaps } from './uniform-sibling-gaps.js';
|
|
18
|
+
import { wallIsIsotropic } from './wall-is-isotropic.js';
|
|
18
19
|
import { wallMatchesGap } from './wall-matches-gap.js';
|
|
19
|
-
export { clippedGlyphs, collapsedTextTrim, concentricInset, cornersAxis, evenContainerPadding, flatChrome, invisibleBox, iconFlush, anonymousText, offAxisMotion, offTierHeight, pillHoldsItsInk, proseLeading, radiusConcentricity, restingGap, singleGlyphSquare, uniformSiblingGaps, wallMatchesGap };
|
|
20
|
+
export { clippedGlyphs, collapsedTextTrim, concentricInset, cornersAxis, evenContainerPadding, flatChrome, invisibleBox, iconFlush, anonymousText, offAxisMotion, offTierHeight, pillHoldsItsInk, proseLeading, radiusConcentricity, restingGap, singleGlyphSquare, uniformSiblingGaps, wallIsIsotropic, wallMatchesGap };
|
|
@@ -16,8 +16,9 @@ import { radiusConcentricity } from './radius-concentricity.js';
|
|
|
16
16
|
import { restingGap } from './resting-gap.js';
|
|
17
17
|
import { singleGlyphSquare } from './single-glyph-square.js';
|
|
18
18
|
import { uniformSiblingGaps } from './uniform-sibling-gaps.js';
|
|
19
|
+
import { wallIsIsotropic } from './wall-is-isotropic.js';
|
|
19
20
|
import { wallMatchesGap } from './wall-matches-gap.js';
|
|
20
|
-
export { clippedGlyphs, collapsedTextTrim, concentricInset, cornersAxis, evenContainerPadding, flatChrome, invisibleBox, iconFlush, anonymousText, offAxisMotion, offTierHeight, pillHoldsItsInk, proseLeading, radiusConcentricity, restingGap, singleGlyphSquare, uniformSiblingGaps, wallMatchesGap };
|
|
21
|
+
export { clippedGlyphs, collapsedTextTrim, concentricInset, cornersAxis, evenContainerPadding, flatChrome, invisibleBox, iconFlush, anonymousText, offAxisMotion, offTierHeight, pillHoldsItsInk, proseLeading, radiusConcentricity, restingGap, singleGlyphSquare, uniformSiblingGaps, wallIsIsotropic, wallMatchesGap };
|
|
21
22
|
// Auto-register built-ins. Consumers can still unregister or add their own.
|
|
22
23
|
//
|
|
23
24
|
// Two of these only tell the truth on a particular axis setting, because the bug
|
|
@@ -43,4 +44,5 @@ registry.register(radiusConcentricity);
|
|
|
43
44
|
registry.register(restingGap);
|
|
44
45
|
registry.register(singleGlyphSquare);
|
|
45
46
|
registry.register(uniformSiblingGaps);
|
|
47
|
+
registry.register(wallIsIsotropic);
|
|
46
48
|
registry.register(wallMatchesGap);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { TOL, flexChildren, isHorizontalFlex, paintsChrome, tiers } from './_helpers.js';
|
|
2
|
+
/*
|
|
3
|
+
* A bar's wall is the same on every side.
|
|
4
|
+
*
|
|
5
|
+
* THE EVEN-GAP LAW IS "on ALL sides", and era had two audits enforcing it one
|
|
6
|
+
* axis at a time, with nothing looking across them:
|
|
7
|
+
*
|
|
8
|
+
* layout/wall-matches-gap horizontal wall vs horizontal gap
|
|
9
|
+
* layout/even-container-padding padding-top vs padding-bottom
|
|
10
|
+
*
|
|
11
|
+
* A bar that pads 4px horizontally, gaps 4px horizontally, and centres its
|
|
12
|
+
* children to 1px vertically passes BOTH — the first sees 4 against 4, the
|
|
13
|
+
* second sees equal top and bottom padding — and is visibly wrong. Two shipped
|
|
14
|
+
* this way. Bar's content="tier" used one token for all three sizes on the
|
|
15
|
+
* reasoning that chip⊃icon, control⊃chip and bar⊃control all equal sp/2, which
|
|
16
|
+
* is true for two of them (chip is the rung that is not a clean sp multiple, so
|
|
17
|
+
* the difference cancels for the pairs that span it and survives for the pair
|
|
18
|
+
* that does not). The Taskbar padded px-inset-chip — a TEXT inset, (h-chip −
|
|
19
|
+
* text)/2 — where the concentric inset belonged. Both read to a user as "more
|
|
20
|
+
* space on the left than on the top", which is exactly how it was reported.
|
|
21
|
+
*
|
|
22
|
+
* WHY THE VERTICAL SIDE IS MEASURED, NOT READ. On a bar the vertical wall is
|
|
23
|
+
* usually not padding at all — it is what `align-items: center` produces from
|
|
24
|
+
* the leftover height, so padding-top is 0 and the real gap is
|
|
25
|
+
* (contentHeight − childHeight)/2. Reading the padding property would report
|
|
26
|
+
* zero and see nothing. That is the whole reason this class of bug was
|
|
27
|
+
* invisible to a rule written in terms of padding.
|
|
28
|
+
*
|
|
29
|
+
* SCOPE. Only centred horizontal flex rows with a BOUNDED height — a tier. An
|
|
30
|
+
* auto-height row has no leftover to distribute, so there is no vertical wall to
|
|
31
|
+
* compare against, and an unbounded container's padding is optical rather than
|
|
32
|
+
* geometric (see the geometric/optical split in density.css).
|
|
33
|
+
*
|
|
34
|
+
* DELIBERATE ASYMMETRY. Some era chrome is legitimately not isotropic: a pane
|
|
35
|
+
* handle sits its leading side at the text inset while the trailing side keeps
|
|
36
|
+
* the concentric wall. Those opt out with data-era-anisotropic, which is a claim
|
|
37
|
+
* a reviewer can check rather than a silence — the same shape as
|
|
38
|
+
* data-era-continuous on surface/invisible-box.
|
|
39
|
+
*/
|
|
40
|
+
export const wallIsIsotropic = {
|
|
41
|
+
id: 'layout/wall-is-isotropic',
|
|
42
|
+
name: 'A tier row walls its content evenly on every side',
|
|
43
|
+
description: 'In a fixed-height centred flex row, the horizontal padding must equal the vertical gap that align-items:center leaves above and below the tallest child. Comparing only within an axis passes a bar that pads 4px at the sides and 1px top and bottom.',
|
|
44
|
+
category: 'layout',
|
|
45
|
+
severity: 'warn',
|
|
46
|
+
selector: '*',
|
|
47
|
+
check(el) {
|
|
48
|
+
if (el.dataset?.eraAnisotropic !== undefined)
|
|
49
|
+
return null;
|
|
50
|
+
const s = getComputedStyle(el);
|
|
51
|
+
if (!isHorizontalFlex(s))
|
|
52
|
+
return null;
|
|
53
|
+
// Centred rows only: any other alignment means the vertical position is
|
|
54
|
+
// not a wall, it is a deliberate stretch/baseline/start.
|
|
55
|
+
if (s.alignItems !== 'center')
|
|
56
|
+
return null;
|
|
57
|
+
if (s.flexWrap === 'wrap')
|
|
58
|
+
return null;
|
|
59
|
+
// A WALL ONLY EXISTS WHERE CONTENT IS PACKED AGAINST ONE. justify-center
|
|
60
|
+
// splits the leftover width evenly on both sides, so the horizontal gap is
|
|
61
|
+
// (width − content)/2 and has nothing to do with padding — an icon button
|
|
62
|
+
// is exactly this: px-0, aspect-square, glyph centred, and its real
|
|
63
|
+
// horizontal gap equals its vertical one by construction. Reading
|
|
64
|
+
// padding-left there reports 0 against a true gap of 5 and flags every
|
|
65
|
+
// centred control in the library.
|
|
66
|
+
if (!/^(flex-)?start$|^normal$/.test(s.justifyContent))
|
|
67
|
+
return null;
|
|
68
|
+
const host = el;
|
|
69
|
+
const kids = flexChildren(host);
|
|
70
|
+
if (!kids.length)
|
|
71
|
+
return null;
|
|
72
|
+
const padTop = parseFloat(s.paddingTop) || 0;
|
|
73
|
+
const padBottom = parseFloat(s.paddingBottom) || 0;
|
|
74
|
+
const padLeft = parseFloat(s.paddingLeft) || 0;
|
|
75
|
+
const padRight = parseFloat(s.paddingRight) || 0;
|
|
76
|
+
// An asymmetric row is even-container-padding's / wall-matches-gap's
|
|
77
|
+
// business; this rule is about the two axes disagreeing.
|
|
78
|
+
if (Math.abs(padLeft - padRight) > TOL)
|
|
79
|
+
return null;
|
|
80
|
+
// THE ROW MUST CLAIM A WALL. A flush transparent row — a checkbox beside
|
|
81
|
+
// its label, a radio and its text — pads nothing and paints nothing: it is
|
|
82
|
+
// content in a line, not a container with edges, and its left edge is the
|
|
83
|
+
// text's edge. Asking it to inset by the concentric gap would push the
|
|
84
|
+
// label off the alignment it shares with the prose around it. A row claims
|
|
85
|
+
// a wall by padding horizontally or by painting a box.
|
|
86
|
+
if (padLeft <= TOL && !paintsChrome(host))
|
|
87
|
+
return null;
|
|
88
|
+
const contentH = host.clientHeight - padTop - padBottom;
|
|
89
|
+
if (contentH <= 0)
|
|
90
|
+
return null;
|
|
91
|
+
// The tallest child is what the wall is measured against — a short child
|
|
92
|
+
// beside a tall one has slack of its own, which is not the container's.
|
|
93
|
+
const tallest = Math.max(...kids.map((k) => k.getBoundingClientRect().height));
|
|
94
|
+
if (tallest <= 0)
|
|
95
|
+
return null;
|
|
96
|
+
// ONLY ROWS THAT HOLD A CONTROL BOX. The rule is about a bar walling the
|
|
97
|
+
// controls inside it, and it needs the child's BOX to mean something.
|
|
98
|
+
//
|
|
99
|
+
// Two things are deliberately out of scope, and both were measured before
|
|
100
|
+
// being excluded rather than assumed:
|
|
101
|
+
//
|
|
102
|
+
// An ICON+TEXT row (file-upload's drop label, Step's summary) walls by
|
|
103
|
+
// the TEXT inset, not by the icon's concentric one — that is Button's
|
|
104
|
+
// own documented convention, `px-inset-control` with the gap widened to
|
|
105
|
+
// match when an icon is present. Measuring those against the icon
|
|
106
|
+
// reports 6-vs-5 on rows that are following the house rule.
|
|
107
|
+
//
|
|
108
|
+
// A TEXT FIELD (date/time field and their ranges) walls by
|
|
109
|
+
// --era-field-px, which density.css names and floors on purpose: it is
|
|
110
|
+
// the room where typed text starts, and it is not the segment box's
|
|
111
|
+
// concentric gap. Measuring those reports 8-vs-1 on a deliberate choice.
|
|
112
|
+
//
|
|
113
|
+
// What is left is the case the law is actually about — chrome holding
|
|
114
|
+
// controls — and it is where both shipped bugs were.
|
|
115
|
+
const CONTROL = 'button, a[href], input, select, textarea, [role="button"], [role="switch"],' +
|
|
116
|
+
' [role="checkbox"], [role="radio"], [role="combobox"], [role="tab"], [role="menuitem"]';
|
|
117
|
+
const tallestKid = kids.find((k) => Math.abs(k.getBoundingClientRect().height - tallest) <= TOL);
|
|
118
|
+
if (!tallestKid)
|
|
119
|
+
return null;
|
|
120
|
+
if (!tallestKid.matches(CONTROL) && !tallestKid.querySelector(CONTROL))
|
|
121
|
+
return null;
|
|
122
|
+
// A control that is not on a tier is off-tier-height's problem, not this
|
|
123
|
+
// rule's — measuring a wall against a box of the wrong size compounds one
|
|
124
|
+
// defect into two reports.
|
|
125
|
+
const onTier = tiers(host).some((t) => Math.abs(t.px - tallest) <= TOL);
|
|
126
|
+
if (!onTier)
|
|
127
|
+
return null;
|
|
128
|
+
const vertical = (contentH - tallest) / 2 + padTop;
|
|
129
|
+
// Flush content (a child filling its row) has no vertical wall to match,
|
|
130
|
+
// and a stretched child would report a spurious zero.
|
|
131
|
+
if (vertical < TOL)
|
|
132
|
+
return null;
|
|
133
|
+
if (Math.abs(padLeft - vertical) <= TOL)
|
|
134
|
+
return null;
|
|
135
|
+
const issue = {
|
|
136
|
+
auditId: 'layout/wall-is-isotropic',
|
|
137
|
+
element: el,
|
|
138
|
+
message: `wall is ${padLeft.toFixed(1)}px at the sides but ${vertical.toFixed(1)}px above and below — the even-gap law is on ALL sides. Pad by the concentric inset for the pairing this row actually holds ((container − child)/2), not by a text inset or a neighbouring tier's token.`,
|
|
139
|
+
details: {
|
|
140
|
+
horizontal: padLeft,
|
|
141
|
+
vertical: +vertical.toFixed(2),
|
|
142
|
+
delta: +(padLeft - vertical).toFixed(2)
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
return issue;
|
|
146
|
+
}
|
|
147
|
+
};
|