@signal9/era-ui 30.1.0 → 30.1.1

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 CHANGED
@@ -1,3 +1,9 @@
1
+ ## [30.1.1](https://github.com/sig-nine/era-ui/compare/v30.1.0...v30.1.1) (2026-08-16)
2
+
3
+ ### Bug Fixes
4
+
5
+ * **button:** caps is a descender fix, not a case fix — and it costs a baseline ([0759f70](https://github.com/sig-nine/era-ui/commit/0759f70b0c23078e89d187c26c0b0c619d77a2ba))
6
+
1
7
  ## [30.1.0](https://github.com/sig-nine/era-ui/compare/v30.0.1...v30.1.0) (2026-08-16)
2
8
 
3
9
  ### Features
@@ -27,22 +27,35 @@ and screen-reader accessible names don't change shape mid-flight. |
27
27
  an icon in `children`: the label then gets its own box, which is the only
28
28
  place era's ink centring can reach (see era-label-ink). |
29
29
  | `trail?` | `Snippet` | — | Icon rendered AFTER the label, outside it. Same reason as `lead`. |
30
- | `caps?` | `boolean` | `false` | The label is ALL CAPS or an initialism "PDF", "CSV", "OK", "API".
31
- Trims the label to the cap band instead of the x-height band. The
32
- default (`era-text-trim`, ex/alphabetic) centres the x-height band,
33
- which is the visual mass of mixed-case text; a string with no lowercase
34
- has ink standing entirely above that band, so the box centres correctly
35
- and the ink sits high. Measured on a bar-tier button with a lead icon:
36
- "PDF" landed 2.5px high on mono and 3.5px on sans, and the cap band
37
- brought both inside half a pixel.
38
- A PROP because nothing can infer it. `text-transform: uppercase` is
39
- detectable and is not the same question — a label that is literally
40
- typed "PDF" carries no transform and a snippet's text cannot be read
41
- before it renders. The caller is the only one who knows.
42
- Named for the CONTENT rather than the band: a consumer knows their label
43
- is an initialism and should not have to learn which typographic box that
44
- implies. Same reason Badge and Chip reach for era-token-ink by default
45
- a pill holds a token, so it is caps-shaped by construction.
30
+ | `caps?` | `boolean` | `false` | Trim this label to the CAP band instead of the x-height band for a
31
+ label whose ink runs cap-to-baseline: "PDF", "CSV", "OK", "API".
32
+ THE NAME SAYS CAPS AND THE MECHANISM IS DESCENDERS. Worth knowing before
33
+ you reach for it, because it decides where it helps. A controlled A/B on
34
+ one real button same element, same classes, only the STRING changing
35
+ says case is not the variable at all:
36
+ Save asc 9 desc 0 ink 1.28px high
37
+ SAVE asc 9 desc 0 ink 1.28px high ← identical to "Save"
38
+ Savg asc 9 desc 3 ink 0.22px low
39
+ savg asc 7 desc 3 ink 1.22px low
40
+ The ex band runs x-height to baseline, so ink above the x-height pulls
41
+ the visual centre up and ink below the baseline pushes it back down;
42
+ (xh asc + desc)/2 predicts every row within 0.3px. A caps string is
43
+ simply one that always has an ascender and never a descender, which is
44
+ why the cap band boxes it exactly. "Save" has the same offset and this
45
+ prop would fix it too. Measured by signalnine.org and reproduced here.
46
+ WHAT IT COSTS, and why it is not the default. A fixed band gives every
47
+ label the SAME baseline whatever it contains — measured spread across
48
+ Save/Savg/save/savg/SAVE/PDF/Cancel/"Copy page" is 0.00px. Changing one
49
+ label's band moves that label alone: in a Bar holding bold/italic/PDF/
50
+ publish, the caps label sits 1.06px below the other three. So this trades
51
+ a uniform offset the eye cannot see for a local misalignment it can, and
52
+ it is the right trade only when the label has no neighbour to align with
53
+ — a lone action, a label paired with an icon (the reported case, where
54
+ the comparison is against an optically-centred glyph rather than against
55
+ other text). Beside other labels in a bar, prefer leaving it off.
56
+ A PROP because nothing can infer it: `text-transform: uppercase` is a
57
+ different question (a label typed "PDF" carries no transform), and a
58
+ snippet's text cannot be read before it renders.
46
59
  Only reaches the label when `lead`/`trail` give it its own box. Without
47
60
  one the label is an anonymous flex item that no selector can address,
48
61
  which is the whole reason those snippets exist. |
@@ -440,7 +440,9 @@ The value is OPTICAL, not the derivation's: pure x-band centring computes
440
440
  to ~0.23em of padding, but pixel-measured against real Title-Case text it
441
441
  over-lifts — the eye weighs the cap-to-baseline mass, which x-band
442
442
  centring parks ~2px high (caps 2.75px off the top against 5.7px of air
443
- under the baseline, measured at dense). 0.08em splits the difference
443
+ under the baseline, measured at dense; the general form of that offset,
444
+ and why era-label-ink's note about "System" is not a contradiction of it,
445
+ is derived at era-label-ink below). 0.08em splits the difference
444
446
  between the x-band ideal and the cap-band ideal, landing caps ~3.7px in an
445
447
  18px field with ~4.7px under the baseline — the reading where neither
446
448
  band's partisans win and neither complains.
@@ -532,6 +534,36 @@ class differs — a label is sentence case, so the x-height band is the visual
532
534
  mass to centre (cap-band reads low on "System"), and it is laid out against
533
535
  the row's padding rather than centred in it.
534
536
 
537
+ THAT PARENTHESIS AND era-ink-center's "x-band centring parks ~2px high" READ
538
+ AS A CONTRADICTION AND ARE NOT. Both are true, of their own example string,
539
+ and each was generalised one step too far. Under an ex band the ink offset is
540
+
541
+ (xh − ascent + descent) / 2
542
+
543
+ which predicts a controlled A/B — one real button, one class set, only the
544
+ STRING changing — within 0.3px:
545
+
546
+ Save asc 9 desc 0 1.28px high SAVE asc 9 desc 0 1.28px high
547
+ Savg asc 9 desc 3 0.22px low savg asc 7 desc 3 1.22px low
548
+
549
+ "System" has a descender and reads low under the cap band; "Save" has none
550
+ and reads high under the ex band. Case is not the variable — "Save" and
551
+ "SAVE" are identical to the hundredth of a pixel. No fixed band centres both
552
+ a descender-less and a descender-bearing label, because they differ by ~1.5px
553
+ in the same box.
554
+
555
+ WHICH IS WHY THE BAND IS FIXED ANYWAY, and this is the part that decides it.
556
+ A band gives every label the SAME baseline whatever it spells — measured
557
+ spread across Save/Savg/save/savg/SAVE/PDF/Cancel/"Copy page" is 0.00px —
558
+ while ink-extent centring would move each label independently and leave a bar
559
+ of buttons visibly ragged. A uniform offset the eye cannot see beats a local
560
+ misalignment it can. So the offset is accepted, not corrected, and the only
561
+ escape is per-label and opt-in (Button's `caps`), which buys optical centring
562
+ for a lone label at the cost of ~1.06px against its neighbours.
563
+
564
+ Measured by signalnine.org and reproduced here; see label-band.spec.ts, which
565
+ pins both the 0.00px spread and the cost of breaking it.
566
+
535
567
  WHY IT EXISTS AT ALL: era's controls are flex, and a flex container's bare
536
568
  text is an ANONYMOUS flex item — a box no selector reaches and no
537
569
  non-inherited property touches. So `era-text-trim` on a flex row is inert,
@@ -1616,22 +1648,35 @@ and screen-reader accessible names don't change shape mid-flight. |
1616
1648
  an icon in `children`: the label then gets its own box, which is the only
1617
1649
  place era's ink centring can reach (see era-label-ink). |
1618
1650
  | `trail?` | `Snippet` | — | Icon rendered AFTER the label, outside it. Same reason as `lead`. |
1619
- | `caps?` | `boolean` | `false` | The label is ALL CAPS or an initialism "PDF", "CSV", "OK", "API".
1620
- Trims the label to the cap band instead of the x-height band. The
1621
- default (`era-text-trim`, ex/alphabetic) centres the x-height band,
1622
- which is the visual mass of mixed-case text; a string with no lowercase
1623
- has ink standing entirely above that band, so the box centres correctly
1624
- and the ink sits high. Measured on a bar-tier button with a lead icon:
1625
- "PDF" landed 2.5px high on mono and 3.5px on sans, and the cap band
1626
- brought both inside half a pixel.
1627
- A PROP because nothing can infer it. `text-transform: uppercase` is
1628
- detectable and is not the same question — a label that is literally
1629
- typed "PDF" carries no transform and a snippet's text cannot be read
1630
- before it renders. The caller is the only one who knows.
1631
- Named for the CONTENT rather than the band: a consumer knows their label
1632
- is an initialism and should not have to learn which typographic box that
1633
- implies. Same reason Badge and Chip reach for era-token-ink by default
1634
- a pill holds a token, so it is caps-shaped by construction.
1651
+ | `caps?` | `boolean` | `false` | Trim this label to the CAP band instead of the x-height band for a
1652
+ label whose ink runs cap-to-baseline: "PDF", "CSV", "OK", "API".
1653
+ THE NAME SAYS CAPS AND THE MECHANISM IS DESCENDERS. Worth knowing before
1654
+ you reach for it, because it decides where it helps. A controlled A/B on
1655
+ one real button same element, same classes, only the STRING changing
1656
+ says case is not the variable at all:
1657
+ Save asc 9 desc 0 ink 1.28px high
1658
+ SAVE asc 9 desc 0 ink 1.28px high ← identical to "Save"
1659
+ Savg asc 9 desc 3 ink 0.22px low
1660
+ savg asc 7 desc 3 ink 1.22px low
1661
+ The ex band runs x-height to baseline, so ink above the x-height pulls
1662
+ the visual centre up and ink below the baseline pushes it back down;
1663
+ (xh asc + desc)/2 predicts every row within 0.3px. A caps string is
1664
+ simply one that always has an ascender and never a descender, which is
1665
+ why the cap band boxes it exactly. "Save" has the same offset and this
1666
+ prop would fix it too. Measured by signalnine.org and reproduced here.
1667
+ WHAT IT COSTS, and why it is not the default. A fixed band gives every
1668
+ label the SAME baseline whatever it contains — measured spread across
1669
+ Save/Savg/save/savg/SAVE/PDF/Cancel/"Copy page" is 0.00px. Changing one
1670
+ label's band moves that label alone: in a Bar holding bold/italic/PDF/
1671
+ publish, the caps label sits 1.06px below the other three. So this trades
1672
+ a uniform offset the eye cannot see for a local misalignment it can, and
1673
+ it is the right trade only when the label has no neighbour to align with
1674
+ — a lone action, a label paired with an icon (the reported case, where
1675
+ the comparison is against an optically-centred glyph rather than against
1676
+ other text). Beside other labels in a bar, prefer leaving it off.
1677
+ A PROP because nothing can infer it: `text-transform: uppercase` is a
1678
+ different question (a label typed "PDF" carries no transform), and a
1679
+ snippet's text cannot be read before it renders.
1635
1680
  Only reaches the label when `lead`/`trail` give it its own box. Without
1636
1681
  one the label is an anonymous flex item that no selector can address,
1637
1682
  which is the whole reason those snippets exist. |
@@ -52,7 +52,7 @@
52
52
  "slug": "utilities",
53
53
  "title": "Utilities",
54
54
  "summary": "The CSS classes era ships alongside the components — links, ink-centred text, shimmer, hidden scrollbars.",
55
- "tokenEstimate": 12608,
55
+ "tokenEstimate": 13037,
56
56
  "sections": [
57
57
  "Overview",
58
58
  "era-interactive",
@@ -207,7 +207,7 @@
207
207
  "slug": "button",
208
208
  "title": "Button",
209
209
  "summary": "A flexible button or link element.",
210
- "tokenEstimate": 860,
210
+ "tokenEstimate": 1070,
211
211
  "sections": [
212
212
  "Import",
213
213
  "Props",
@@ -1095,7 +1095,7 @@
1095
1095
  ],
1096
1096
  "file": "index.css",
1097
1097
  "useCase": "on a native single-line <input> (or the text layer that must mirror one) inside a fixed-height field — never alongside era-text-trim.",
1098
- "description": "The EDITABLE counterpart of era-text-trim — for native <input>s, which the\ntrim is forbidden on (it collapses the UA's inner editor box and slices the\nglyphs; see typography/collapsed-text-trim).\n\nWithout help, a tier-height input centres its text by FONT BOX, and the ui\nstacks' font box is deeply asymmetric: ascent ≈1.07em against descent\n≈0.29em. Centring that box parks the visible band (caps, x-height) ~0.115em\nLOW — so a field's text sat visibly below the trimmed label beside it.\n\nThe value is OPTICAL, not the derivation's: pure x-band centring computes\nto ~0.23em of padding, but pixel-measured against real Title-Case text it\nover-lifts — the eye weighs the cap-to-baseline mass, which x-band\ncentring parks ~2px high (caps 2.75px off the top against 5.7px of air\nunder the baseline, measured at dense). 0.08em splits the difference\nbetween the x-band ideal and the cap-band ideal, landing caps ~3.7px in an\n18px field with ~4.7px under the baseline — the reading where neither\nband's partisans win and neither complains.\n\nThe lift is padding, not a translateY: an icon-less Input paints the field\nchrome itself, and a transform would drag the field along with the ink.\npadding-bottom shrinks the content box from below, and a single-line input\nre-centres its line box in what remains — the ink rises by exactly half the\npadding, the chrome doesn't move, and the caret and selection follow the\ntext. In em on purpose: the correction is a fraction of the type, so it\nholds across every density and text size with one value.\n\nWHEN SOMEONE REPORTS A CORRECTLY-CENTRED FIELD AS \"TOO HIGH\", check the\nSTRING before the geometry. A placeholder like \"Filter…\" has no descender and\nno cap, so its ink occupies only the x-height band — and a box that centres\nthat band exactly still reads high, because the eye centres on the mass it\ncan see and expects the space a descender would have filled. Measured at the\npill tier on the shipped mono stack, \"Filter…\" sits within half a pixel of\ncentre while looking a little high; the pill suite documents the same effect\nfrom the other side (see OFF_BAND_LOW in ink-centering.spec.ts, where \"soon\"\ncannot be symmetric and is bounded by sign instead of magnitude).\n\nThat is not a licence to ignore the report — era-ink-center IS wrong by\n1.5–2px on the sans and serif stacks (pinned in FIELD_KNOWN_SKEW) — only a\nreason to measure the ink before touching the constant.",
1098
+ "description": "The EDITABLE counterpart of era-text-trim — for native <input>s, which the\ntrim is forbidden on (it collapses the UA's inner editor box and slices the\nglyphs; see typography/collapsed-text-trim).\n\nWithout help, a tier-height input centres its text by FONT BOX, and the ui\nstacks' font box is deeply asymmetric: ascent ≈1.07em against descent\n≈0.29em. Centring that box parks the visible band (caps, x-height) ~0.115em\nLOW — so a field's text sat visibly below the trimmed label beside it.\n\nThe value is OPTICAL, not the derivation's: pure x-band centring computes\nto ~0.23em of padding, but pixel-measured against real Title-Case text it\nover-lifts — the eye weighs the cap-to-baseline mass, which x-band\ncentring parks ~2px high (caps 2.75px off the top against 5.7px of air\nunder the baseline, measured at dense; the general form of that offset,\nand why era-label-ink's note about \"System\" is not a contradiction of it,\nis derived at era-label-ink below). 0.08em splits the difference\nbetween the x-band ideal and the cap-band ideal, landing caps ~3.7px in an\n18px field with ~4.7px under the baseline — the reading where neither\nband's partisans win and neither complains.\n\nThe lift is padding, not a translateY: an icon-less Input paints the field\nchrome itself, and a transform would drag the field along with the ink.\npadding-bottom shrinks the content box from below, and a single-line input\nre-centres its line box in what remains — the ink rises by exactly half the\npadding, the chrome doesn't move, and the caret and selection follow the\ntext. In em on purpose: the correction is a fraction of the type, so it\nholds across every density and text size with one value.\n\nWHEN SOMEONE REPORTS A CORRECTLY-CENTRED FIELD AS \"TOO HIGH\", check the\nSTRING before the geometry. A placeholder like \"Filter…\" has no descender and\nno cap, so its ink occupies only the x-height band — and a box that centres\nthat band exactly still reads high, because the eye centres on the mass it\ncan see and expects the space a descender would have filled. Measured at the\npill tier on the shipped mono stack, \"Filter…\" sits within half a pixel of\ncentre while looking a little high; the pill suite documents the same effect\nfrom the other side (see OFF_BAND_LOW in ink-centering.spec.ts, where \"soon\"\ncannot be symmetric and is bounded by sign instead of magnitude).\n\nThat is not a licence to ignore the report — era-ink-center IS wrong by\n1.5–2px on the sans and serif stacks (pinned in FIELD_KNOWN_SKEW) — only a\nreason to measure the ink before touching the constant.",
1099
1099
  "css": "@utility era-ink-center {\n\tpadding-bottom: 0.08em;\n}"
1100
1100
  },
1101
1101
  {
@@ -1115,7 +1115,7 @@
1115
1115
  ],
1116
1116
  "file": "index.css",
1117
1117
  "useCase": "the ink centring for a fixed-height control holding ONE line of sentence-case text and no icon — a menu item, a nav trigger, a button label. Never alongside `truncate` (the trimmed box becomes the clip).",
1118
- "description": "LABEL INK — era-token-ink's mixed-case sibling, and the general answer to\n\"this fixed-height control holds one line of text\".\n\nSame mechanism, same reason: `text-box-trim` needs a BLOCK box, so the row\nbecomes one and `align-content` (block-container alignment) does the\ncentring. What differs is the band and the alignment, because the content\nclass differs — a label is sentence case, so the x-height band is the visual\nmass to centre (cap-band reads low on \"System\"), and it is laid out against\nthe row's padding rather than centred in it.\n\nWHY IT EXISTS AT ALL: era's controls are flex, and a flex container's bare\ntext is an ANONYMOUS flex item — a box no selector reaches and no\nnon-inherited property touches. So `era-text-trim` on a flex row is inert,\ncontributing only its `line-height: 1`, and the ink falls back to line-box\ncentring by font metrics. era-token-ink already fixed that for centred\ntokens; this fixes it for everything else.\n\nTHE ICON CASE IS NOT SOLVED, and cannot be from CSS: an icon beside the text\nneeds the box to stay flex, which re-creates the anonymous item. A component\nwhose children are an opaque slot therefore guards it —\n`not-has-[svg]:era-label-ink` — and ink-centres the text-only case while\nleaving the icon case on line-box centring. Splitting icon and label into\nseparate slots is what would close it; typography/inert-trim reports every\nsite still on the fallback.",
1118
+ "description": "LABEL INK — era-token-ink's mixed-case sibling, and the general answer to\n\"this fixed-height control holds one line of text\".\n\nSame mechanism, same reason: `text-box-trim` needs a BLOCK box, so the row\nbecomes one and `align-content` (block-container alignment) does the\ncentring. What differs is the band and the alignment, because the content\nclass differs — a label is sentence case, so the x-height band is the visual\nmass to centre (cap-band reads low on \"System\"), and it is laid out against\nthe row's padding rather than centred in it.\n\nTHAT PARENTHESIS AND era-ink-center's \"x-band centring parks ~2px high\" READ\nAS A CONTRADICTION AND ARE NOT. Both are true, of their own example string,\nand each was generalised one step too far. Under an ex band the ink offset is\n\n (xh − ascent + descent) / 2\n\nwhich predicts a controlled A/B — one real button, one class set, only the\nSTRING changing — within 0.3px:\n\n Save asc 9 desc 0 1.28px high SAVE asc 9 desc 0 1.28px high\n Savg asc 9 desc 3 0.22px low savg asc 7 desc 3 1.22px low\n\n\"System\" has a descender and reads low under the cap band; \"Save\" has none\nand reads high under the ex band. Case is not the variable — \"Save\" and\n\"SAVE\" are identical to the hundredth of a pixel. No fixed band centres both\na descender-less and a descender-bearing label, because they differ by ~1.5px\nin the same box.\n\nWHICH IS WHY THE BAND IS FIXED ANYWAY, and this is the part that decides it.\nA band gives every label the SAME baseline whatever it spells — measured\nspread across Save/Savg/save/savg/SAVE/PDF/Cancel/\"Copy page\" is 0.00px —\nwhile ink-extent centring would move each label independently and leave a bar\nof buttons visibly ragged. A uniform offset the eye cannot see beats a local\nmisalignment it can. So the offset is accepted, not corrected, and the only\nescape is per-label and opt-in (Button's `caps`), which buys optical centring\nfor a lone label at the cost of ~1.06px against its neighbours.\n\nMeasured by signalnine.org and reproduced here; see label-band.spec.ts, which\npins both the 0.00px spread and the cost of breaking it.\n\nWHY IT EXISTS AT ALL: era's controls are flex, and a flex container's bare\ntext is an ANONYMOUS flex item — a box no selector reaches and no\nnon-inherited property touches. So `era-text-trim` on a flex row is inert,\ncontributing only its `line-height: 1`, and the ink falls back to line-box\ncentring by font metrics. era-token-ink already fixed that for centred\ntokens; this fixes it for everything else.\n\nTHE ICON CASE IS NOT SOLVED, and cannot be from CSS: an icon beside the text\nneeds the box to stay flex, which re-creates the anonymous item. A component\nwhose children are an opaque slot therefore guards it —\n`not-has-[svg]:era-label-ink` — and ink-centres the text-only case while\nleaving the icon case on line-box centring. Splitting icon and label into\nseparate slots is what would close it; typography/inert-trim reports every\nsite still on the fallback.",
1119
1119
  "css": "@utility era-label-ink {\n\tdisplay: block;\n\talign-content: center;\n\tline-height: 1;\n\ttext-box: trim-both ex alphabetic;\n}"
1120
1120
  },
1121
1121
  {
@@ -67,7 +67,7 @@
67
67
  ],
68
68
  "file": "index.css",
69
69
  "useCase": "on a native single-line <input> (or the text layer that must mirror one) inside a fixed-height field — never alongside era-text-trim.",
70
- "description": "The EDITABLE counterpart of era-text-trim — for native <input>s, which the\ntrim is forbidden on (it collapses the UA's inner editor box and slices the\nglyphs; see typography/collapsed-text-trim).\n\nWithout help, a tier-height input centres its text by FONT BOX, and the ui\nstacks' font box is deeply asymmetric: ascent ≈1.07em against descent\n≈0.29em. Centring that box parks the visible band (caps, x-height) ~0.115em\nLOW — so a field's text sat visibly below the trimmed label beside it.\n\nThe value is OPTICAL, not the derivation's: pure x-band centring computes\nto ~0.23em of padding, but pixel-measured against real Title-Case text it\nover-lifts — the eye weighs the cap-to-baseline mass, which x-band\ncentring parks ~2px high (caps 2.75px off the top against 5.7px of air\nunder the baseline, measured at dense). 0.08em splits the difference\nbetween the x-band ideal and the cap-band ideal, landing caps ~3.7px in an\n18px field with ~4.7px under the baseline — the reading where neither\nband's partisans win and neither complains.\n\nThe lift is padding, not a translateY: an icon-less Input paints the field\nchrome itself, and a transform would drag the field along with the ink.\npadding-bottom shrinks the content box from below, and a single-line input\nre-centres its line box in what remains — the ink rises by exactly half the\npadding, the chrome doesn't move, and the caret and selection follow the\ntext. In em on purpose: the correction is a fraction of the type, so it\nholds across every density and text size with one value.\n\nWHEN SOMEONE REPORTS A CORRECTLY-CENTRED FIELD AS \"TOO HIGH\", check the\nSTRING before the geometry. A placeholder like \"Filter…\" has no descender and\nno cap, so its ink occupies only the x-height band — and a box that centres\nthat band exactly still reads high, because the eye centres on the mass it\ncan see and expects the space a descender would have filled. Measured at the\npill tier on the shipped mono stack, \"Filter…\" sits within half a pixel of\ncentre while looking a little high; the pill suite documents the same effect\nfrom the other side (see OFF_BAND_LOW in ink-centering.spec.ts, where \"soon\"\ncannot be symmetric and is bounded by sign instead of magnitude).\n\nThat is not a licence to ignore the report — era-ink-center IS wrong by\n1.5–2px on the sans and serif stacks (pinned in FIELD_KNOWN_SKEW) — only a\nreason to measure the ink before touching the constant.",
70
+ "description": "The EDITABLE counterpart of era-text-trim — for native <input>s, which the\ntrim is forbidden on (it collapses the UA's inner editor box and slices the\nglyphs; see typography/collapsed-text-trim).\n\nWithout help, a tier-height input centres its text by FONT BOX, and the ui\nstacks' font box is deeply asymmetric: ascent ≈1.07em against descent\n≈0.29em. Centring that box parks the visible band (caps, x-height) ~0.115em\nLOW — so a field's text sat visibly below the trimmed label beside it.\n\nThe value is OPTICAL, not the derivation's: pure x-band centring computes\nto ~0.23em of padding, but pixel-measured against real Title-Case text it\nover-lifts — the eye weighs the cap-to-baseline mass, which x-band\ncentring parks ~2px high (caps 2.75px off the top against 5.7px of air\nunder the baseline, measured at dense; the general form of that offset,\nand why era-label-ink's note about \"System\" is not a contradiction of it,\nis derived at era-label-ink below). 0.08em splits the difference\nbetween the x-band ideal and the cap-band ideal, landing caps ~3.7px in an\n18px field with ~4.7px under the baseline — the reading where neither\nband's partisans win and neither complains.\n\nThe lift is padding, not a translateY: an icon-less Input paints the field\nchrome itself, and a transform would drag the field along with the ink.\npadding-bottom shrinks the content box from below, and a single-line input\nre-centres its line box in what remains — the ink rises by exactly half the\npadding, the chrome doesn't move, and the caret and selection follow the\ntext. In em on purpose: the correction is a fraction of the type, so it\nholds across every density and text size with one value.\n\nWHEN SOMEONE REPORTS A CORRECTLY-CENTRED FIELD AS \"TOO HIGH\", check the\nSTRING before the geometry. A placeholder like \"Filter…\" has no descender and\nno cap, so its ink occupies only the x-height band — and a box that centres\nthat band exactly still reads high, because the eye centres on the mass it\ncan see and expects the space a descender would have filled. Measured at the\npill tier on the shipped mono stack, \"Filter…\" sits within half a pixel of\ncentre while looking a little high; the pill suite documents the same effect\nfrom the other side (see OFF_BAND_LOW in ink-centering.spec.ts, where \"soon\"\ncannot be symmetric and is bounded by sign instead of magnitude).\n\nThat is not a licence to ignore the report — era-ink-center IS wrong by\n1.5–2px on the sans and serif stacks (pinned in FIELD_KNOWN_SKEW) — only a\nreason to measure the ink before touching the constant.",
71
71
  "css": "@utility era-ink-center {\n\tpadding-bottom: 0.08em;\n}"
72
72
  },
73
73
  {
@@ -87,7 +87,7 @@
87
87
  ],
88
88
  "file": "index.css",
89
89
  "useCase": "the ink centring for a fixed-height control holding ONE line of sentence-case text and no icon — a menu item, a nav trigger, a button label. Never alongside `truncate` (the trimmed box becomes the clip).",
90
- "description": "LABEL INK — era-token-ink's mixed-case sibling, and the general answer to\n\"this fixed-height control holds one line of text\".\n\nSame mechanism, same reason: `text-box-trim` needs a BLOCK box, so the row\nbecomes one and `align-content` (block-container alignment) does the\ncentring. What differs is the band and the alignment, because the content\nclass differs — a label is sentence case, so the x-height band is the visual\nmass to centre (cap-band reads low on \"System\"), and it is laid out against\nthe row's padding rather than centred in it.\n\nWHY IT EXISTS AT ALL: era's controls are flex, and a flex container's bare\ntext is an ANONYMOUS flex item — a box no selector reaches and no\nnon-inherited property touches. So `era-text-trim` on a flex row is inert,\ncontributing only its `line-height: 1`, and the ink falls back to line-box\ncentring by font metrics. era-token-ink already fixed that for centred\ntokens; this fixes it for everything else.\n\nTHE ICON CASE IS NOT SOLVED, and cannot be from CSS: an icon beside the text\nneeds the box to stay flex, which re-creates the anonymous item. A component\nwhose children are an opaque slot therefore guards it —\n`not-has-[svg]:era-label-ink` — and ink-centres the text-only case while\nleaving the icon case on line-box centring. Splitting icon and label into\nseparate slots is what would close it; typography/inert-trim reports every\nsite still on the fallback.",
90
+ "description": "LABEL INK — era-token-ink's mixed-case sibling, and the general answer to\n\"this fixed-height control holds one line of text\".\n\nSame mechanism, same reason: `text-box-trim` needs a BLOCK box, so the row\nbecomes one and `align-content` (block-container alignment) does the\ncentring. What differs is the band and the alignment, because the content\nclass differs — a label is sentence case, so the x-height band is the visual\nmass to centre (cap-band reads low on \"System\"), and it is laid out against\nthe row's padding rather than centred in it.\n\nTHAT PARENTHESIS AND era-ink-center's \"x-band centring parks ~2px high\" READ\nAS A CONTRADICTION AND ARE NOT. Both are true, of their own example string,\nand each was generalised one step too far. Under an ex band the ink offset is\n\n (xh − ascent + descent) / 2\n\nwhich predicts a controlled A/B — one real button, one class set, only the\nSTRING changing — within 0.3px:\n\n Save asc 9 desc 0 1.28px high SAVE asc 9 desc 0 1.28px high\n Savg asc 9 desc 3 0.22px low savg asc 7 desc 3 1.22px low\n\n\"System\" has a descender and reads low under the cap band; \"Save\" has none\nand reads high under the ex band. Case is not the variable — \"Save\" and\n\"SAVE\" are identical to the hundredth of a pixel. No fixed band centres both\na descender-less and a descender-bearing label, because they differ by ~1.5px\nin the same box.\n\nWHICH IS WHY THE BAND IS FIXED ANYWAY, and this is the part that decides it.\nA band gives every label the SAME baseline whatever it spells — measured\nspread across Save/Savg/save/savg/SAVE/PDF/Cancel/\"Copy page\" is 0.00px —\nwhile ink-extent centring would move each label independently and leave a bar\nof buttons visibly ragged. A uniform offset the eye cannot see beats a local\nmisalignment it can. So the offset is accepted, not corrected, and the only\nescape is per-label and opt-in (Button's `caps`), which buys optical centring\nfor a lone label at the cost of ~1.06px against its neighbours.\n\nMeasured by signalnine.org and reproduced here; see label-band.spec.ts, which\npins both the 0.00px spread and the cost of breaking it.\n\nWHY IT EXISTS AT ALL: era's controls are flex, and a flex container's bare\ntext is an ANONYMOUS flex item — a box no selector reaches and no\nnon-inherited property touches. So `era-text-trim` on a flex row is inert,\ncontributing only its `line-height: 1`, and the ink falls back to line-box\ncentring by font metrics. era-token-ink already fixed that for centred\ntokens; this fixes it for everything else.\n\nTHE ICON CASE IS NOT SOLVED, and cannot be from CSS: an icon beside the text\nneeds the box to stay flex, which re-creates the anonymous item. A component\nwhose children are an opaque slot therefore guards it —\n`not-has-[svg]:era-label-ink` — and ink-centres the text-only case while\nleaving the icon case on line-box centring. Splitting icon and label into\nseparate slots is what would close it; typography/inert-trim reports every\nsite still on the fallback.",
91
91
  "css": "@utility era-label-ink {\n\tdisplay: block;\n\talign-content: center;\n\tline-height: 1;\n\ttext-box: trim-both ex alphabetic;\n}"
92
92
  },
93
93
  {
@@ -291,7 +291,9 @@ The value is OPTICAL, not the derivation's: pure x-band centring computes
291
291
  to ~0.23em of padding, but pixel-measured against real Title-Case text it
292
292
  over-lifts — the eye weighs the cap-to-baseline mass, which x-band
293
293
  centring parks ~2px high (caps 2.75px off the top against 5.7px of air
294
- under the baseline, measured at dense). 0.08em splits the difference
294
+ under the baseline, measured at dense; the general form of that offset,
295
+ and why era-label-ink's note about "System" is not a contradiction of it,
296
+ is derived at era-label-ink below). 0.08em splits the difference
295
297
  between the x-band ideal and the cap-band ideal, landing caps ~3.7px in an
296
298
  18px field with ~4.7px under the baseline — the reading where neither
297
299
  band's partisans win and neither complains.
@@ -383,6 +385,36 @@ class differs — a label is sentence case, so the x-height band is the visual
383
385
  mass to centre (cap-band reads low on "System"), and it is laid out against
384
386
  the row's padding rather than centred in it.
385
387
 
388
+ THAT PARENTHESIS AND era-ink-center's "x-band centring parks ~2px high" READ
389
+ AS A CONTRADICTION AND ARE NOT. Both are true, of their own example string,
390
+ and each was generalised one step too far. Under an ex band the ink offset is
391
+
392
+ (xh − ascent + descent) / 2
393
+
394
+ which predicts a controlled A/B — one real button, one class set, only the
395
+ STRING changing — within 0.3px:
396
+
397
+ Save asc 9 desc 0 1.28px high SAVE asc 9 desc 0 1.28px high
398
+ Savg asc 9 desc 3 0.22px low savg asc 7 desc 3 1.22px low
399
+
400
+ "System" has a descender and reads low under the cap band; "Save" has none
401
+ and reads high under the ex band. Case is not the variable — "Save" and
402
+ "SAVE" are identical to the hundredth of a pixel. No fixed band centres both
403
+ a descender-less and a descender-bearing label, because they differ by ~1.5px
404
+ in the same box.
405
+
406
+ WHICH IS WHY THE BAND IS FIXED ANYWAY, and this is the part that decides it.
407
+ A band gives every label the SAME baseline whatever it spells — measured
408
+ spread across Save/Savg/save/savg/SAVE/PDF/Cancel/"Copy page" is 0.00px —
409
+ while ink-extent centring would move each label independently and leave a bar
410
+ of buttons visibly ragged. A uniform offset the eye cannot see beats a local
411
+ misalignment it can. So the offset is accepted, not corrected, and the only
412
+ escape is per-label and opt-in (Button's `caps`), which buys optical centring
413
+ for a lone label at the cost of ~1.06px against its neighbours.
414
+
415
+ Measured by signalnine.org and reproduced here; see label-band.spec.ts, which
416
+ pins both the 0.00px spread and the cost of breaking it.
417
+
386
418
  WHY IT EXISTS AT ALL: era's controls are flex, and a flex container's bare
387
419
  text is an ANONYMOUS flex item — a box no selector reaches and no
388
420
  non-inherited property touches. So `era-text-trim` on a flex row is inert,
@@ -295,7 +295,9 @@
295
295
  * to ~0.23em of padding, but pixel-measured against real Title-Case text it
296
296
  * over-lifts — the eye weighs the cap-to-baseline mass, which x-band
297
297
  * centring parks ~2px high (caps 2.75px off the top against 5.7px of air
298
- * under the baseline, measured at dense). 0.08em splits the difference
298
+ * under the baseline, measured at dense; the general form of that offset,
299
+ * and why era-label-ink's note about "System" is not a contradiction of it,
300
+ * is derived at era-label-ink below). 0.08em splits the difference
299
301
  * between the x-band ideal and the cap-band ideal, landing caps ~3.7px in an
300
302
  * 18px field with ~4.7px under the baseline — the reading where neither
301
303
  * band's partisans win and neither complains.
@@ -377,6 +379,36 @@
377
379
  * mass to centre (cap-band reads low on "System"), and it is laid out against
378
380
  * the row's padding rather than centred in it.
379
381
  *
382
+ * THAT PARENTHESIS AND era-ink-center's "x-band centring parks ~2px high" READ
383
+ * AS A CONTRADICTION AND ARE NOT. Both are true, of their own example string,
384
+ * and each was generalised one step too far. Under an ex band the ink offset is
385
+ *
386
+ * (xh − ascent + descent) / 2
387
+ *
388
+ * which predicts a controlled A/B — one real button, one class set, only the
389
+ * STRING changing — within 0.3px:
390
+ *
391
+ * Save asc 9 desc 0 1.28px high SAVE asc 9 desc 0 1.28px high
392
+ * Savg asc 9 desc 3 0.22px low savg asc 7 desc 3 1.22px low
393
+ *
394
+ * "System" has a descender and reads low under the cap band; "Save" has none
395
+ * and reads high under the ex band. Case is not the variable — "Save" and
396
+ * "SAVE" are identical to the hundredth of a pixel. No fixed band centres both
397
+ * a descender-less and a descender-bearing label, because they differ by ~1.5px
398
+ * in the same box.
399
+ *
400
+ * WHICH IS WHY THE BAND IS FIXED ANYWAY, and this is the part that decides it.
401
+ * A band gives every label the SAME baseline whatever it spells — measured
402
+ * spread across Save/Savg/save/savg/SAVE/PDF/Cancel/"Copy page" is 0.00px —
403
+ * while ink-extent centring would move each label independently and leave a bar
404
+ * of buttons visibly ragged. A uniform offset the eye cannot see beats a local
405
+ * misalignment it can. So the offset is accepted, not corrected, and the only
406
+ * escape is per-label and opt-in (Button's `caps`), which buys optical centring
407
+ * for a lone label at the cost of ~1.06px against its neighbours.
408
+ *
409
+ * Measured by signalnine.org and reproduced here; see label-band.spec.ts, which
410
+ * pins both the 0.00px spread and the cost of breaking it.
411
+ *
380
412
  * WHY IT EXISTS AT ALL: era's controls are flex, and a flex container's bare
381
413
  * text is an ANONYMOUS flex item — a box no selector reaches and no
382
414
  * non-inherited property touches. So `era-text-trim` on a flex row is inert,
@@ -42,25 +42,40 @@
42
42
  /** Icon rendered AFTER the label, outside it. Same reason as `lead`. */
43
43
  trail?: Snippet;
44
44
  /**
45
- * The label is ALL CAPS or an initialism "PDF", "CSV", "OK", "API".
45
+ * Trim this label to the CAP band instead of the x-height band for a
46
+ * label whose ink runs cap-to-baseline: "PDF", "CSV", "OK", "API".
46
47
  *
47
- * Trims the label to the cap band instead of the x-height band. The
48
- * default (`era-text-trim`, ex/alphabetic) centres the x-height band,
49
- * which is the visual mass of mixed-case text; a string with no lowercase
50
- * has ink standing entirely above that band, so the box centres correctly
51
- * and the ink sits high. Measured on a bar-tier button with a lead icon:
52
- * "PDF" landed 2.5px high on mono and 3.5px on sans, and the cap band
53
- * brought both inside half a pixel.
48
+ * THE NAME SAYS CAPS AND THE MECHANISM IS DESCENDERS. Worth knowing before
49
+ * you reach for it, because it decides where it helps. A controlled A/B on
50
+ * one real button same element, same classes, only the STRING changing
51
+ * says case is not the variable at all:
54
52
  *
55
- * A PROP because nothing can infer it. `text-transform: uppercase` is
56
- * detectable and is not the same question — a label that is literally
57
- * typed "PDF" carries no transform — and a snippet's text cannot be read
58
- * before it renders. The caller is the only one who knows.
53
+ * Save asc 9 desc 0 ink 1.28px high
54
+ * SAVE asc 9 desc 0 ink 1.28px high ← identical to "Save"
55
+ * Savg asc 9 desc 3 ink 0.22px low
56
+ * savg asc 7 desc 3 ink 1.22px low
59
57
  *
60
- * Named for the CONTENT rather than the band: a consumer knows their label
61
- * is an initialism and should not have to learn which typographic box that
62
- * implies. Same reason Badge and Chip reach for era-token-ink by default
63
- * a pill holds a token, so it is caps-shaped by construction.
58
+ * The ex band runs x-height to baseline, so ink above the x-height pulls
59
+ * the visual centre up and ink below the baseline pushes it back down;
60
+ * (xh asc + desc)/2 predicts every row within 0.3px. A caps string is
61
+ * simply one that always has an ascender and never a descender, which is
62
+ * why the cap band boxes it exactly. "Save" has the same offset and this
63
+ * prop would fix it too. Measured by signalnine.org and reproduced here.
64
+ *
65
+ * WHAT IT COSTS, and why it is not the default. A fixed band gives every
66
+ * label the SAME baseline whatever it contains — measured spread across
67
+ * Save/Savg/save/savg/SAVE/PDF/Cancel/"Copy page" is 0.00px. Changing one
68
+ * label's band moves that label alone: in a Bar holding bold/italic/PDF/
69
+ * publish, the caps label sits 1.06px below the other three. So this trades
70
+ * a uniform offset the eye cannot see for a local misalignment it can, and
71
+ * it is the right trade only when the label has no neighbour to align with
72
+ * — a lone action, a label paired with an icon (the reported case, where
73
+ * the comparison is against an optically-centred glyph rather than against
74
+ * other text). Beside other labels in a bar, prefer leaving it off.
75
+ *
76
+ * A PROP because nothing can infer it: `text-transform: uppercase` is a
77
+ * different question (a label typed "PDF" carries no transform), and a
78
+ * snippet's text cannot be read before it renders.
64
79
  *
65
80
  * Only reaches the label when `lead`/`trail` give it its own box. Without
66
81
  * one the label is an anonymous flex item that no selector can address,
@@ -21,25 +21,40 @@ type $$ComponentProps = Button.RootProps & {
21
21
  /** Icon rendered AFTER the label, outside it. Same reason as `lead`. */
22
22
  trail?: Snippet;
23
23
  /**
24
- * The label is ALL CAPS or an initialism "PDF", "CSV", "OK", "API".
24
+ * Trim this label to the CAP band instead of the x-height band for a
25
+ * label whose ink runs cap-to-baseline: "PDF", "CSV", "OK", "API".
25
26
  *
26
- * Trims the label to the cap band instead of the x-height band. The
27
- * default (`era-text-trim`, ex/alphabetic) centres the x-height band,
28
- * which is the visual mass of mixed-case text; a string with no lowercase
29
- * has ink standing entirely above that band, so the box centres correctly
30
- * and the ink sits high. Measured on a bar-tier button with a lead icon:
31
- * "PDF" landed 2.5px high on mono and 3.5px on sans, and the cap band
32
- * brought both inside half a pixel.
27
+ * THE NAME SAYS CAPS AND THE MECHANISM IS DESCENDERS. Worth knowing before
28
+ * you reach for it, because it decides where it helps. A controlled A/B on
29
+ * one real button same element, same classes, only the STRING changing
30
+ * says case is not the variable at all:
33
31
  *
34
- * A PROP because nothing can infer it. `text-transform: uppercase` is
35
- * detectable and is not the same question — a label that is literally
36
- * typed "PDF" carries no transform — and a snippet's text cannot be read
37
- * before it renders. The caller is the only one who knows.
32
+ * Save asc 9 desc 0 ink 1.28px high
33
+ * SAVE asc 9 desc 0 ink 1.28px high ← identical to "Save"
34
+ * Savg asc 9 desc 3 ink 0.22px low
35
+ * savg asc 7 desc 3 ink 1.22px low
38
36
  *
39
- * Named for the CONTENT rather than the band: a consumer knows their label
40
- * is an initialism and should not have to learn which typographic box that
41
- * implies. Same reason Badge and Chip reach for era-token-ink by default
42
- * a pill holds a token, so it is caps-shaped by construction.
37
+ * The ex band runs x-height to baseline, so ink above the x-height pulls
38
+ * the visual centre up and ink below the baseline pushes it back down;
39
+ * (xh asc + desc)/2 predicts every row within 0.3px. A caps string is
40
+ * simply one that always has an ascender and never a descender, which is
41
+ * why the cap band boxes it exactly. "Save" has the same offset and this
42
+ * prop would fix it too. Measured by signalnine.org and reproduced here.
43
+ *
44
+ * WHAT IT COSTS, and why it is not the default. A fixed band gives every
45
+ * label the SAME baseline whatever it contains — measured spread across
46
+ * Save/Savg/save/savg/SAVE/PDF/Cancel/"Copy page" is 0.00px. Changing one
47
+ * label's band moves that label alone: in a Bar holding bold/italic/PDF/
48
+ * publish, the caps label sits 1.06px below the other three. So this trades
49
+ * a uniform offset the eye cannot see for a local misalignment it can, and
50
+ * it is the right trade only when the label has no neighbour to align with
51
+ * — a lone action, a label paired with an icon (the reported case, where
52
+ * the comparison is against an optically-centred glyph rather than against
53
+ * other text). Beside other labels in a bar, prefer leaving it off.
54
+ *
55
+ * A PROP because nothing can infer it: `text-transform: uppercase` is a
56
+ * different question (a label typed "PDF" carries no transform), and a
57
+ * snippet's text cannot be read before it renders.
43
58
  *
44
59
  * Only reaches the label when `lead`/`trail` give it its own box. Without
45
60
  * one the label is an anonymous flex item that no selector can address,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signal9/era-ui",
3
- "version": "30.1.0",
3
+ "version": "30.1.1",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host",
6
6
  "build": "vite build && npm run prepack",