@signal9/era-ui 20.0.0 → 22.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 +41 -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/off-tier-height.js +16 -2
- package/dist/dev/audit/audits/wall-is-isotropic.d.ts +2 -0
- package/dist/dev/audit/audits/wall-is-isotropic.js +152 -0
- package/dist/era-ui.css +1 -1
- package/dist/os/taskbar.svelte +7 -1
- package/dist/styles/density.css +68 -67
- package/dist/styles/typography.css +48 -9
- package/dist/ui/bar/bar.svelte +57 -14
- package/dist/ui/bar/bar.svelte.d.ts +78 -18
- package/dist/ui/card/card.svelte +3 -0
- package/dist/ui/date-field/date-field-input.svelte +9 -0
- package/dist/ui/date-range-field/date-range-field-input.svelte +9 -0
- package/dist/ui/meter/meter.svelte +50 -2
- package/dist/ui/meter/meter.svelte.d.ts +38 -1
- package/dist/ui/time-field/time-field-input.svelte +9 -0
- package/dist/ui/time-range-field/time-range-field-input.svelte +9 -0
- package/package.json +7 -4
- package/skill/SKILL.md +12 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
## [22.0.0](https://github.com/sig-nine/era-ui/compare/v21.0.0...v22.0.0) (2026-08-09)
|
|
2
|
+
|
|
3
|
+
### ⚠ BREAKING CHANGES
|
|
4
|
+
|
|
5
|
+
* **font:** a stack is read AT the element carrying `data-font`, so
|
|
6
|
+
`--era-font-mono` / `-sans` / `-serif` must be re-pointed at or above that
|
|
7
|
+
element. Setting one on a descendant of the axis host now does nothing, where
|
|
8
|
+
before it applied to that subtree. /audit was doing exactly this — it re-pointed
|
|
9
|
+
the stacks on its own <main> while the axis lived on <html>, so the harness had
|
|
10
|
+
been measuring the docs' pinned `monospace` while reporting the shipped stack —
|
|
11
|
+
and now sets them on :root. Nesting `data-font` also genuinely works, which for
|
|
12
|
+
anyone relying on the old source-order behaviour is a visible change.
|
|
13
|
+
|
|
14
|
+
Closes the test.fixme in axis-nesting.spec.ts; the suite now has no skips.
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **meter:** a meter can say which band its reading is in ([9119e38](https://github.com/sig-nine/era-ui/commit/9119e38425ca511c0ff9f31a52efdf9f80dd390f))
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **font:** the font axis resolves by depth, like every other axis ([a1041bd](https://github.com/sig-nine/era-ui/commit/a1041bdc59a41b9829e5dad42bf727f434e67f7f))
|
|
23
|
+
|
|
24
|
+
## [21.0.0](https://github.com/sig-nine/era-ui/compare/v20.0.0...v21.0.0) (2026-08-09)
|
|
25
|
+
|
|
26
|
+
### ⚠ BREAKING CHANGES
|
|
27
|
+
|
|
28
|
+
* **bar:** a `Bar` with `size="control"` and the default `content="tier"`
|
|
29
|
+
now pads and gaps by `chip-inset-control` (1/2/3) instead of `icon-inset-chip`
|
|
30
|
+
(4/5/6). That is the concentric answer for the chip-tier content the variant
|
|
31
|
+
describes, and it is visibly tighter. A control bar that actually holds
|
|
32
|
+
icon-tier buttons was never described by `content="tier"` and was relying on the
|
|
33
|
+
old value being wrong in a helpful direction — give it `size="chip"`, or pass
|
|
34
|
+
the wall explicitly. `size="bar"` and `size="chip"` are unchanged in value;
|
|
35
|
+
`size="bar"` now names its inset `gutter`, which is what era already calls that
|
|
36
|
+
pairing, rather than borrowing icon-inset-chip's name for the same number.
|
|
37
|
+
|
|
38
|
+
### Bug Fixes
|
|
39
|
+
|
|
40
|
+
* **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))
|
|
41
|
+
|
|
1
42
|
## [20.0.0](https://github.com/sig-nine/era-ui/compare/v19.0.0...v20.0.0) (2026-08-09)
|
|
2
43
|
|
|
3
44
|
### ⚠ 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);
|
|
@@ -2,12 +2,12 @@ import { TOL, tiers } from './_helpers.js';
|
|
|
2
2
|
/*
|
|
3
3
|
* Every chrome-bearing control must land on a tier.
|
|
4
4
|
*
|
|
5
|
-
* The tier ladder (
|
|
5
|
+
* The tier ladder (icon / pill / chip / control / bar) is the whole basis of the even-gap and
|
|
6
6
|
* concentricity laws: those laws are stated in terms of (h_outer − h_inner)/2, so
|
|
7
7
|
* a control that sits BETWEEN tiers can never nest concentrically in anything —
|
|
8
8
|
* it silently poisons every gap around it. That is exactly how the date segments
|
|
9
9
|
* broke: 21px tall, which is no tier at all, so their 1.5px gap could not match any
|
|
10
|
-
* radius step. Putting them on the
|
|
10
|
+
* radius step. Putting them on the chip tier (22px) is what let the identity close.
|
|
11
11
|
*
|
|
12
12
|
* A grep cannot see this. `h-[21px]` would be caught by the hardcoded-px rule, but
|
|
13
13
|
* these segments had NO height class at all — 21px was whatever the line box
|
|
@@ -21,6 +21,20 @@ export const offTierHeight = {
|
|
|
21
21
|
severity: 'warn',
|
|
22
22
|
selector: 'button, input, [role="switch"], [role="checkbox"], [role="radio"], [data-segment]',
|
|
23
23
|
check(el) {
|
|
24
|
+
// AN UNBOUNDED CONTAINER IS NOT A TIER OBJECT. era has one place where a
|
|
25
|
+
// <button> is a container rather than a control: an interactive Card, which
|
|
26
|
+
// renders as <button> or <a> and whose height is its content's. Holding it
|
|
27
|
+
// to a tier would be asking a card to be 32px tall, and the geometric/optical
|
|
28
|
+
// padding split in density.css exists precisely because unbounded containers
|
|
29
|
+
// are NOT sized from the ladder.
|
|
30
|
+
//
|
|
31
|
+
// Marked, not inferred. The tempting test — "does its padding match an
|
|
32
|
+
// optical pad token?" — collides: --era-pad-panel is 5.6px at balanced and
|
|
33
|
+
// --era-inset-chip is 6px, inside this audit's own 0.5px tolerance. That
|
|
34
|
+
// would silently exempt a real control from the audit that catches poisoned
|
|
35
|
+
// gaps, which is the worst direction for this rule to be wrong in.
|
|
36
|
+
if (el.dataset?.eraUnbounded !== undefined)
|
|
37
|
+
return null;
|
|
24
38
|
const s = getComputedStyle(el);
|
|
25
39
|
// Chromeless controls (Button's `link` variant is tinted text, no box) have
|
|
26
40
|
// no tier to be on — their height is their line box, by design.
|
|
@@ -0,0 +1,152 @@
|
|
|
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, and it
|
|
35
|
+
* opts out with data-era-anisotropic rather than by being a shape this rule
|
|
36
|
+
* happens not to match — the same claim-over-silence design as
|
|
37
|
+
* data-era-continuous on surface/invisible-box. The date/time field inputs carry
|
|
38
|
+
* it: --era-field-px is the room where typed TEXT starts, purpose-named and
|
|
39
|
+
* floored in density.css, so 8px at the sides against a 1px concentric gap is
|
|
40
|
+
* the intended reading and says so at the call site.
|
|
41
|
+
*/
|
|
42
|
+
export const wallIsIsotropic = {
|
|
43
|
+
id: 'layout/wall-is-isotropic',
|
|
44
|
+
name: 'A tier row walls its content evenly on every side',
|
|
45
|
+
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.',
|
|
46
|
+
category: 'layout',
|
|
47
|
+
severity: 'warn',
|
|
48
|
+
selector: '*',
|
|
49
|
+
check(el) {
|
|
50
|
+
if (el.dataset?.eraAnisotropic !== undefined)
|
|
51
|
+
return null;
|
|
52
|
+
const s = getComputedStyle(el);
|
|
53
|
+
if (!isHorizontalFlex(s))
|
|
54
|
+
return null;
|
|
55
|
+
// Centred rows only: any other alignment means the vertical position is
|
|
56
|
+
// not a wall, it is a deliberate stretch/baseline/start.
|
|
57
|
+
if (s.alignItems !== 'center')
|
|
58
|
+
return null;
|
|
59
|
+
if (s.flexWrap === 'wrap')
|
|
60
|
+
return null;
|
|
61
|
+
// A WALL ONLY EXISTS WHERE CONTENT IS PACKED AGAINST ONE. justify-center
|
|
62
|
+
// splits the leftover width evenly on both sides, so the horizontal gap is
|
|
63
|
+
// (width − content)/2 and has nothing to do with padding — an icon button
|
|
64
|
+
// is exactly this: px-0, aspect-square, glyph centred, and its real
|
|
65
|
+
// horizontal gap equals its vertical one by construction. Reading
|
|
66
|
+
// padding-left there reports 0 against a true gap of 5 and flags every
|
|
67
|
+
// centred control in the library.
|
|
68
|
+
if (!/^(flex-)?start$|^normal$/.test(s.justifyContent))
|
|
69
|
+
return null;
|
|
70
|
+
const host = el;
|
|
71
|
+
const kids = flexChildren(host);
|
|
72
|
+
if (!kids.length)
|
|
73
|
+
return null;
|
|
74
|
+
const padTop = parseFloat(s.paddingTop) || 0;
|
|
75
|
+
const padBottom = parseFloat(s.paddingBottom) || 0;
|
|
76
|
+
const padLeft = parseFloat(s.paddingLeft) || 0;
|
|
77
|
+
const padRight = parseFloat(s.paddingRight) || 0;
|
|
78
|
+
// An asymmetric row is even-container-padding's / wall-matches-gap's
|
|
79
|
+
// business; this rule is about the two axes disagreeing.
|
|
80
|
+
if (Math.abs(padLeft - padRight) > TOL)
|
|
81
|
+
return null;
|
|
82
|
+
// THE ROW MUST CLAIM A WALL. A flush transparent row — a checkbox beside
|
|
83
|
+
// its label, a radio and its text — pads nothing and paints nothing: it is
|
|
84
|
+
// content in a line, not a container with edges, and its left edge is the
|
|
85
|
+
// text's edge. Asking it to inset by the concentric gap would push the
|
|
86
|
+
// label off the alignment it shares with the prose around it. A row claims
|
|
87
|
+
// a wall by padding horizontally or by painting a box.
|
|
88
|
+
if (padLeft <= TOL && !paintsChrome(host))
|
|
89
|
+
return null;
|
|
90
|
+
const contentH = host.clientHeight - padTop - padBottom;
|
|
91
|
+
if (contentH <= 0)
|
|
92
|
+
return null;
|
|
93
|
+
// The tallest child is what the wall is measured against — a short child
|
|
94
|
+
// beside a tall one has slack of its own, which is not the container's.
|
|
95
|
+
const tallest = Math.max(...kids.map((k) => k.getBoundingClientRect().height));
|
|
96
|
+
if (tallest <= 0)
|
|
97
|
+
return null;
|
|
98
|
+
// ONLY ROWS THAT HOLD A CONTROL BOX. The rule is about a bar walling the
|
|
99
|
+
// controls inside it, and it needs the child's BOX to mean something.
|
|
100
|
+
//
|
|
101
|
+
// Two things are deliberately out of scope, and both were measured before
|
|
102
|
+
// being excluded rather than assumed:
|
|
103
|
+
//
|
|
104
|
+
// An ICON+TEXT row (file-upload's drop label, Step's summary) walls by
|
|
105
|
+
// the TEXT inset, not by the icon's concentric one — that is Button's
|
|
106
|
+
// own documented convention, `px-inset-control` with the gap widened to
|
|
107
|
+
// match when an icon is present. Measuring those against the icon
|
|
108
|
+
// reports 6-vs-5 on rows that are following the house rule.
|
|
109
|
+
//
|
|
110
|
+
// What is left is the case the law is actually about — chrome holding
|
|
111
|
+
// controls — and it is where both shipped bugs were.
|
|
112
|
+
//
|
|
113
|
+
// The OTHER standing exception, a text field walling by --era-field-px,
|
|
114
|
+
// is not handled here: those four components carry data-era-anisotropic
|
|
115
|
+
// and are turned away at the top of this function. Verified load-bearing
|
|
116
|
+
// by disabling this heuristic and confirming they stay silent, which is
|
|
117
|
+
// the point of declaring it — the exemption survives a change to the
|
|
118
|
+
// scope rule, and it is a claim at the call site rather than a shape
|
|
119
|
+
// this function happens not to match.
|
|
120
|
+
const CONTROL = 'button, a[href], input, select, textarea, [role="button"], [role="switch"],' +
|
|
121
|
+
' [role="checkbox"], [role="radio"], [role="combobox"], [role="tab"], [role="menuitem"]';
|
|
122
|
+
const tallestKid = kids.find((k) => Math.abs(k.getBoundingClientRect().height - tallest) <= TOL);
|
|
123
|
+
if (!tallestKid)
|
|
124
|
+
return null;
|
|
125
|
+
if (!tallestKid.matches(CONTROL) && !tallestKid.querySelector(CONTROL))
|
|
126
|
+
return null;
|
|
127
|
+
// A control that is not on a tier is off-tier-height's problem, not this
|
|
128
|
+
// rule's — measuring a wall against a box of the wrong size compounds one
|
|
129
|
+
// defect into two reports.
|
|
130
|
+
const onTier = tiers(host).some((t) => Math.abs(t.px - tallest) <= TOL);
|
|
131
|
+
if (!onTier)
|
|
132
|
+
return null;
|
|
133
|
+
const vertical = (contentH - tallest) / 2 + padTop;
|
|
134
|
+
// Flush content (a child filling its row) has no vertical wall to match,
|
|
135
|
+
// and a stretched child would report a spurious zero.
|
|
136
|
+
if (vertical < TOL)
|
|
137
|
+
return null;
|
|
138
|
+
if (Math.abs(padLeft - vertical) <= TOL)
|
|
139
|
+
return null;
|
|
140
|
+
const issue = {
|
|
141
|
+
auditId: 'layout/wall-is-isotropic',
|
|
142
|
+
element: el,
|
|
143
|
+
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.`,
|
|
144
|
+
details: {
|
|
145
|
+
horizontal: padLeft,
|
|
146
|
+
vertical: +vertical.toFixed(2),
|
|
147
|
+
delta: +(padLeft - vertical).toFixed(2)
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
return issue;
|
|
151
|
+
}
|
|
152
|
+
};
|