@signal9/era-ui 4.12.0 → 4.12.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.
@@ -5,9 +5,10 @@ import { flatChrome } from './flat-chrome.js';
5
5
  import { iconFlush } from './icon-flush.js';
6
6
  import { offAxisMotion } from './off-axis-motion.js';
7
7
  import { offTierHeight } from './off-tier-height.js';
8
+ import { pillHoldsItsInk } from './pill-holds-its-ink.js';
8
9
  import { radiusConcentricity } from './radius-concentricity.js';
9
10
  import { restingGap } from './resting-gap.js';
10
11
  import { singleGlyphSquare } from './single-glyph-square.js';
11
12
  import { uniformSiblingGaps } from './uniform-sibling-gaps.js';
12
13
  import { wallMatchesGap } from './wall-matches-gap.js';
13
- export { clippedGlyphs, collapsedTextTrim, concentricInset, flatChrome, iconFlush, offAxisMotion, offTierHeight, radiusConcentricity, restingGap, singleGlyphSquare, uniformSiblingGaps, wallMatchesGap };
14
+ export { clippedGlyphs, collapsedTextTrim, concentricInset, flatChrome, iconFlush, offAxisMotion, offTierHeight, pillHoldsItsInk, radiusConcentricity, restingGap, singleGlyphSquare, uniformSiblingGaps, wallMatchesGap };
@@ -6,12 +6,13 @@ import { flatChrome } from './flat-chrome.js';
6
6
  import { iconFlush } from './icon-flush.js';
7
7
  import { offAxisMotion } from './off-axis-motion.js';
8
8
  import { offTierHeight } from './off-tier-height.js';
9
+ import { pillHoldsItsInk } from './pill-holds-its-ink.js';
9
10
  import { radiusConcentricity } from './radius-concentricity.js';
10
11
  import { restingGap } from './resting-gap.js';
11
12
  import { singleGlyphSquare } from './single-glyph-square.js';
12
13
  import { uniformSiblingGaps } from './uniform-sibling-gaps.js';
13
14
  import { wallMatchesGap } from './wall-matches-gap.js';
14
- export { clippedGlyphs, collapsedTextTrim, concentricInset, flatChrome, iconFlush, offAxisMotion, offTierHeight, radiusConcentricity, restingGap, singleGlyphSquare, uniformSiblingGaps, wallMatchesGap };
15
+ export { clippedGlyphs, collapsedTextTrim, concentricInset, flatChrome, iconFlush, offAxisMotion, offTierHeight, pillHoldsItsInk, radiusConcentricity, restingGap, singleGlyphSquare, uniformSiblingGaps, wallMatchesGap };
15
16
  // Auto-register built-ins. Consumers can still unregister or add their own.
16
17
  //
17
18
  // Two of these only tell the truth on a particular axis setting, because the bug
@@ -27,6 +28,7 @@ registry.register(flatChrome);
27
28
  registry.register(iconFlush);
28
29
  registry.register(offAxisMotion);
29
30
  registry.register(offTierHeight);
31
+ registry.register(pillHoldsItsInk);
30
32
  registry.register(radiusConcentricity);
31
33
  registry.register(restingGap);
32
34
  registry.register(singleGlyphSquare);
@@ -0,0 +1,2 @@
1
+ import type { Audit } from '../types.js';
2
+ export declare const pillHoldsItsInk: Audit;
@@ -0,0 +1,53 @@
1
+ import { BOUNDED_TIER_MAX } from '../../measure.js';
2
+ import { paintsChrome } from './_helpers.js';
3
+ /*
4
+ * A pill's background must be at least as wide as its ink.
5
+ *
6
+ * A badge or chip is sized BY its text — it has no slack to surrender — so when
7
+ * a flex ancestor squeezes it, the only thing that can give is the fill: the
8
+ * background narrows and the label bleeds past one or both rounded ends. The
9
+ * class string is identical squeezed and unsqueezed, which is why this is a
10
+ * runtime law and not a lint rule: only the rendered box can tell you.
11
+ *
12
+ * The tell is scrollWidth: content wider than the padding box, on an element
13
+ * that paints chrome and does NOT clip. An element that clips (Tool.Chip's
14
+ * truncating label, KV's squared key cell) has chosen the sanctioned answer to
15
+ * tightness — box holds, text truncates — and is exempt here (its failure mode
16
+ * is vertical, and typography/clipped-glyphs owns that).
17
+ *
18
+ * Bounded heights only: this is a law about tier-sized pills and controls. An
19
+ * unbounded container that "overflows" horizontally is a scroll region.
20
+ */
21
+ export const pillHoldsItsInk = {
22
+ id: 'layout/pill-holds-its-ink',
23
+ name: 'Pill background holds its ink',
24
+ description: 'A chrome-painting, non-clipping element must be at least as wide as its content — squeezed pills slide the fill out from under the label.',
25
+ category: 'layout',
26
+ severity: 'error',
27
+ selector: '*',
28
+ check(el) {
29
+ const s = getComputedStyle(el);
30
+ if (s.overflowX !== 'visible')
31
+ return null; // clipping = the sanctioned fix
32
+ // An absolutely-positioned element is outside flex/grid layout, so nothing
33
+ // can squeeze it — its scrollWidth quirks (a slider thumb reports one) are
34
+ // not this bug.
35
+ if (s.position === 'absolute' || s.position === 'fixed')
36
+ return null;
37
+ if (!paintsChrome(el))
38
+ return null;
39
+ const h = el.getBoundingClientRect().height;
40
+ if (h === 0 || h > BOUNDED_TIER_MAX)
41
+ return null;
42
+ const bleed = el.scrollWidth - el.clientWidth;
43
+ if (bleed <= 1)
44
+ return null; // sub-pixel rounding
45
+ const issue = {
46
+ auditId: 'layout/pill-holds-its-ink',
47
+ element: el,
48
+ message: `ink is ${bleed}px wider than the pill that carries it (${el.scrollWidth} in ${el.clientWidth}) — a flex ancestor squeezed it; add shrink-0, or truncate an inner span if it must fit`,
49
+ details: { scrollWidth: el.scrollWidth, clientWidth: el.clientWidth, bleed }
50
+ };
51
+ return issue;
52
+ }
53
+ };
@@ -5,7 +5,13 @@
5
5
  // min-w matches height (design rule #5): a single glyph — a count, a
6
6
  // keycap letter — renders as a perfect square/circle instead of a
7
7
  // skinny pill; wider content is unaffected.
8
- base: 'inline-flex h-(--era-h-xxs) min-w-(--era-h-xxs) items-center justify-center gap-(--era-gap) rounded-(--era-rd-xxs) px-(--era-pill-xxs) font-mono text-body leading-none has-[svg]:gap-(--era-inset-xxs) era-text-trim',
8
+ //
9
+ // shrink-0 + whitespace-nowrap: a pill IS its text's size — it has no slack
10
+ // to give up, so letting a flex ancestor squeeze it just slid the background
11
+ // out from under the ink and the label bled past the fill. A pill that must
12
+ // fit a tight row does it the Tool.Chip way: the BOX holds and an inner
13
+ // min-w-0 span truncates. Enforced at runtime by layout/pill-holds-its-ink.
14
+ base: 'inline-flex h-(--era-h-xxs) min-w-(--era-h-xxs) shrink-0 items-center justify-center gap-(--era-gap) rounded-(--era-rd-xxs) px-(--era-pill-xxs) font-mono text-body leading-none whitespace-nowrap has-[svg]:gap-(--era-inset-xxs) era-text-trim',
9
15
  variants: {
10
16
  tone: {
11
17
  default: 'bg-hover text-fg',
@@ -6,7 +6,7 @@ export declare const badgeVariants: import("tailwind-variants").TVReturnType<{
6
6
  success: "bg-success text-success-fg";
7
7
  warning: "bg-warning text-warning-fg";
8
8
  };
9
- }, undefined, "inline-flex h-(--era-h-xxs) min-w-(--era-h-xxs) items-center justify-center gap-(--era-gap) rounded-(--era-rd-xxs) px-(--era-pill-xxs) font-mono text-body leading-none has-[svg]:gap-(--era-inset-xxs) era-text-trim", {
9
+ }, undefined, "inline-flex h-(--era-h-xxs) min-w-(--era-h-xxs) shrink-0 items-center justify-center gap-(--era-gap) rounded-(--era-rd-xxs) px-(--era-pill-xxs) font-mono text-body leading-none whitespace-nowrap has-[svg]:gap-(--era-inset-xxs) era-text-trim", {
10
10
  tone: {
11
11
  default: "bg-hover text-fg";
12
12
  accent: "bg-primary text-primary-fg";
@@ -19,7 +19,10 @@
19
19
  <div
20
20
  bind:this={ref}
21
21
  class={cn(
22
- 'flex gap-(--era-xs-inset-sm)',
22
+ // shrink-0: a segmented control is its buttons' width — Badge/Chip's law
23
+ // one tier up. Squeezed, its rounded fill slid out from under the end
24
+ // buttons (measured 4px at touch on the board).
25
+ 'flex shrink-0 gap-(--era-xs-inset-sm)',
23
26
  orientation === 'vertical'
24
27
  ? 'flex-col items-stretch p-(--era-xs-inset-sm) [&>*]:justify-start'
25
28
  : 'items-center',
@@ -11,7 +11,9 @@
11
11
  // select trigger): h-xxs = sp + 10, leaving the 3/5/7
12
12
  // concentric gap so it sits evenly inset, not cramped.
13
13
  export const chipVariants = tv({
14
- base: 'inline-flex items-center bg-hover font-mono text-body leading-none text-fg shadow-(--era-shadow) era-text-trim',
14
+ // shrink-0 + nowrap for the same reason Badge carries them: the pill is
15
+ // already exactly its text's size, so any squeeze puts ink outside the fill.
16
+ base: 'inline-flex shrink-0 items-center bg-hover font-mono text-body leading-none whitespace-nowrap text-fg shadow-(--era-shadow) era-text-trim',
15
17
  variants: {
16
18
  size: {
17
19
  sm: 'h-(--era-h-sm) min-w-(--era-h-sm) rounded-(--era-rd-sm) pl-(--era-pill-sm)',
@@ -7,7 +7,7 @@ export declare const chipVariants: import("tailwind-variants").TVReturnType<{
7
7
  true: "";
8
8
  false: "";
9
9
  };
10
- }, undefined, "inline-flex items-center bg-hover font-mono text-body leading-none text-fg shadow-(--era-shadow) era-text-trim", {
10
+ }, undefined, "inline-flex shrink-0 items-center bg-hover font-mono text-body leading-none whitespace-nowrap text-fg shadow-(--era-shadow) era-text-trim", {
11
11
  size: {
12
12
  sm: "h-(--era-h-sm) min-w-(--era-h-sm) rounded-(--era-rd-sm) pl-(--era-pill-sm)";
13
13
  xxs: "h-(--era-h-xxs) min-w-(--era-h-xxs) rounded-(--era-rd-xxs) pl-(--era-pill-xxs)";
@@ -207,7 +207,14 @@
207
207
  itself by the font's own metrics. The input and the ghost mirror BOTH inherit
208
208
  it, so they centre by the identical mechanism — no native-input-centering vs
209
209
  flex-centering divergence that reads as the ghost sitting a hair high. -->
210
- <div class="relative h-full min-w-0 flex-1 text-body leading-(--era-h-md) text-bright">
210
+ <!-- overflow-hidden: the transparent <input> clips its text natively, but the
211
+ MIRROR that actually paints it did not — so in a host narrower than the
212
+ prompt line (placeholder included), the text bled past the bar's rounded
213
+ end where any real input would clip. Safe with the line-height centring:
214
+ the box is the bar's full h-md, well above the ink (clipped-glyphs' law). -->
215
+ <div
216
+ class="relative h-full min-w-0 flex-1 overflow-hidden text-body leading-(--era-h-md) text-bright"
217
+ >
211
218
  <!-- Transparent input: it owns editing and the caret only, so the visible text
212
219
  can come entirely from the mirror below (see it for why). Everything but
213
220
  the see-through fill is inherited: font + line-height from the container,
@@ -21,7 +21,10 @@
21
21
  <span
22
22
  bind:this={ref}
23
23
  class={cn(
24
- 'inline-flex h-(--era-h-xxs) items-center overflow-hidden rounded-(--era-rd-xxs) bg-hover font-mono text-body era-text-trim leading-none shadow-(--era-shadow)',
24
+ // shrink-0 + nowrap like Badge/Chip: the overflow-hidden here is for the
25
+ // key cell's squared corners, not a licence to crush — squeezed, this pill
26
+ // clipped its value instead of bleeding it, which is quieter but no truer.
27
+ 'inline-flex h-(--era-h-xxs) shrink-0 items-center overflow-hidden rounded-(--era-rd-xxs) bg-hover font-mono text-body era-text-trim leading-none whitespace-nowrap shadow-(--era-shadow)',
25
28
  className
26
29
  )}
27
30
  {...restProps}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signal9/era-ui",
3
- "version": "4.12.0",
3
+ "version": "4.12.1",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host",
6
6
  "build": "vite build && npm run prepack",