@signal9/era-ui 2.14.0 → 2.16.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.
@@ -27,7 +27,7 @@ export { default as Checkpoint } from './checkpoint.svelte';
27
27
  export { default as Loader } from './loader.svelte';
28
28
  export { default as Shimmer } from './shimmer.svelte';
29
29
  export { default as AIImage } from './image.svelte';
30
- export { default as CopyButton } from './copy-button.svelte';
30
+ export { CopyButton } from '../ui/copy-button/index.js';
31
31
  export { StickToBottom } from './stick-to-bottom.svelte.js';
32
32
  export type { PromptAttachment, PromptMessage, ChatStatus } from './prompt-input/index.js';
33
33
  export type { MessageRole, MessageVersion } from './message/index.js';
package/dist/ai/index.js CHANGED
@@ -32,7 +32,8 @@ export { default as Checkpoint } from './checkpoint.svelte';
32
32
  export { default as Loader } from './loader.svelte';
33
33
  export { default as Shimmer } from './shimmer.svelte';
34
34
  export { default as AIImage } from './image.svelte';
35
- export { default as CopyButton } from './copy-button.svelte';
35
+ // Promoted to a ui primitive; re-exported here so `$lib/ai`'s surface is stable.
36
+ export { CopyButton } from '../ui/copy-button/index.js';
36
37
  export { StickToBottom } from './stick-to-bottom.svelte.js';
37
38
  export { categorizeError } from './error-panel/index.js';
38
39
  export { projectMessages, toolState } from './project.js';
@@ -1,5 +1,14 @@
1
1
  /** Sub-pixel tolerance. Below this, treat values as equal. */
2
2
  export declare const TOL = 0.5;
3
+ /**
4
+ * The children that actually participate in the container's flex layout.
5
+ *
6
+ * Out-of-flow children (a slider thumb, a file-upload drop overlay, any
7
+ * absolutely-positioned decoration) are laid out against the container's padding
8
+ * box, not against their siblings — measuring them as flex items produces
9
+ * nonsense: overlapping boxes report NEGATIVE sibling gaps, which is how the
10
+ * slider reported a −87px gap and file-upload a −237px one. Skip them.
11
+ */
3
12
  export declare function flexChildren(el: HTMLElement): HTMLElement[];
4
13
  export declare function isFlex(style: CSSStyleDeclaration): boolean;
5
14
  export declare function isHorizontalFlex(style: CSSStyleDeclaration): boolean;
@@ -1,7 +1,21 @@
1
1
  /** Sub-pixel tolerance. Below this, treat values as equal. */
2
2
  export const TOL = 0.5;
3
+ /**
4
+ * The children that actually participate in the container's flex layout.
5
+ *
6
+ * Out-of-flow children (a slider thumb, a file-upload drop overlay, any
7
+ * absolutely-positioned decoration) are laid out against the container's padding
8
+ * box, not against their siblings — measuring them as flex items produces
9
+ * nonsense: overlapping boxes report NEGATIVE sibling gaps, which is how the
10
+ * slider reported a −87px gap and file-upload a −237px one. Skip them.
11
+ */
3
12
  export function flexChildren(el) {
4
- return [...el.children].filter((c) => c instanceof HTMLElement);
13
+ return [...el.children].filter((c) => {
14
+ if (!(c instanceof HTMLElement))
15
+ return false;
16
+ const pos = getComputedStyle(c).position;
17
+ return pos !== 'absolute' && pos !== 'fixed';
18
+ });
5
19
  }
6
20
  export function isFlex(style) {
7
21
  return style.display === 'flex' || style.display === 'inline-flex';
@@ -1,4 +1,5 @@
1
1
  import { concentricInset } from './concentric-inset.js';
2
+ import { radiusConcentricity } from './radius-concentricity.js';
2
3
  import { uniformSiblingGaps } from './uniform-sibling-gaps.js';
3
4
  import { wallMatchesGap } from './wall-matches-gap.js';
4
- export { concentricInset, uniformSiblingGaps, wallMatchesGap };
5
+ export { concentricInset, radiusConcentricity, uniformSiblingGaps, wallMatchesGap };
@@ -1,9 +1,11 @@
1
1
  import { registry } from '../registry.js';
2
2
  import { concentricInset } from './concentric-inset.js';
3
+ import { radiusConcentricity } from './radius-concentricity.js';
3
4
  import { uniformSiblingGaps } from './uniform-sibling-gaps.js';
4
5
  import { wallMatchesGap } from './wall-matches-gap.js';
5
- export { concentricInset, uniformSiblingGaps, wallMatchesGap };
6
+ export { concentricInset, radiusConcentricity, uniformSiblingGaps, wallMatchesGap };
6
7
  // Auto-register built-ins. Consumers can still unregister or add their own.
7
8
  registry.register(concentricInset);
9
+ registry.register(radiusConcentricity);
8
10
  registry.register(uniformSiblingGaps);
9
11
  registry.register(wallMatchesGap);
@@ -0,0 +1,2 @@
1
+ import type { Audit } from '../types.js';
2
+ export declare const radiusConcentricity: Audit;
@@ -0,0 +1,52 @@
1
+ import { TOL, flexChildren, isFlex } from './_helpers.js';
2
+ import { getConcentricity } from '../../measure.js';
3
+ /*
4
+ * The concentricity law: nested corners share a centre of curvature only when
5
+ * the radius step equals the gap. Stated as one identity —
6
+ *
7
+ * rd_outer − rd_inner == (h_outer − h_inner) / 2 == gap
8
+ *
9
+ * Checking any TWO of those three terms passes boxes that are visibly wrong, so
10
+ * this asserts all three together. Rounded containers only: on data-corners
11
+ * ="square" every radius collapses to 0 and the law is vacuously true.
12
+ */
13
+ /**
14
+ * Tallest a container can be and still be a bounded TIER control. The tallest
15
+ * tier is --era-h-lg, which peaks at 64px on touch; anything above that is an
16
+ * unbounded content container.
17
+ */
18
+ const BOUNDED_MAX = 64;
19
+ export const radiusConcentricity = {
20
+ id: 'layout/radius-concentricity',
21
+ name: 'Concentric radius',
22
+ description: 'A rounded child in a rounded container must satisfy rd_outer − rd_inner = (h_outer − h_inner)/2 = gap, so the corners share a centre of curvature.',
23
+ category: 'layout',
24
+ severity: 'warn',
25
+ selector: '*',
26
+ check(el) {
27
+ const s = getComputedStyle(el);
28
+ if (!isFlex(s) || parseFloat(s.borderTopLeftRadius) <= 0)
29
+ return null;
30
+ // Bounded tier controls only. An unbounded container (card, pane, dialog
31
+ // body) is padded OPTICALLY (--era-pad-*), not by the even-gap law, so the
32
+ // radius step has nothing to be equal to — holding it to the identity is a
33
+ // false positive by the design system's own rules.
34
+ if (el.getBoundingClientRect().height > BOUNDED_MAX)
35
+ return null;
36
+ const issues = [];
37
+ for (const kid of flexChildren(el)) {
38
+ if (parseFloat(getComputedStyle(kid).borderTopLeftRadius) <= 0)
39
+ continue;
40
+ const c = getConcentricity(kid, el, TOL);
41
+ if (c.holds)
42
+ continue;
43
+ issues.push({
44
+ auditId: 'layout/radius-concentricity',
45
+ element: kid,
46
+ message: `radius step ${c.radiusDelta.toFixed(1)} ≠ implied gap ${c.impliedGap.toFixed(1)} ≠ measured gap ${c.gap.toFixed(1)}`,
47
+ details: { ...c }
48
+ });
49
+ }
50
+ return issues.length > 0 ? issues : null;
51
+ }
52
+ };
@@ -1,2 +1,3 @@
1
1
  export * from './audit/index.js';
2
+ export * from './measure.js';
2
3
  export { default as AuditOverlay } from './audit-overlay.svelte';
package/dist/dev/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './audit/index.js';
2
+ export * from './measure.js';
2
3
  export { default as AuditOverlay } from './audit-overlay.svelte';
@@ -0,0 +1,75 @@
1
+ /** Sub-pixel residual to forgive — fractional line boxes and dpr rounding. */
2
+ export declare const TOL = 0.5;
3
+ export interface Box {
4
+ readonly x: number;
5
+ readonly y: number;
6
+ readonly width: number;
7
+ readonly height: number;
8
+ /** Computed top-left border-radius, in px (all four corners are equal in era). */
9
+ readonly radius: number;
10
+ }
11
+ export interface Gaps {
12
+ readonly top: number;
13
+ readonly right: number;
14
+ readonly bottom: number;
15
+ readonly left: number;
16
+ }
17
+ /** Rect + resolved radius, relative to the viewport. */
18
+ export declare function getRect(el: Element): Box;
19
+ /** Rect relative to an offset root, so overlays can draw in the root's space. */
20
+ export declare function getRelativeRect(el: Element, root: Element): Box;
21
+ /**
22
+ * The four gaps between a child's border box and its container's border box —
23
+ * what the eye actually reads as the inset, padding and border included.
24
+ */
25
+ export declare function getGaps(child: Element, container: Element): Gaps;
26
+ /** Shortest edge-to-edge distance between two boxes; 0 when they intersect. */
27
+ export declare function getDistance(a: Element, b: Element): number;
28
+ export declare function isEven(g: Gaps, tol?: number): boolean;
29
+ export interface Concentricity {
30
+ readonly outerRadius: number;
31
+ readonly innerRadius: number;
32
+ /** rd_outer − rd_inner */
33
+ readonly radiusDelta: number;
34
+ /** (h_outer − h_inner) / 2 — the vertical gap the tiers imply. */
35
+ readonly impliedGap: number;
36
+ /** The gap actually measured on top. */
37
+ readonly gap: number;
38
+ /** The identity: rd_outer − rd_inner == (h_outer − h_inner)/2 == gap. */
39
+ readonly holds: boolean;
40
+ }
41
+ /**
42
+ * The concentricity law. Nested corners share a centre of curvature only when
43
+ * the radius step equals the gap — checking any two of the three terms passes
44
+ * boxes that are visibly wrong, so assert all three at once.
45
+ */
46
+ export declare function getConcentricity(child: Element, container: Element, tol?: number): Concentricity;
47
+ /**
48
+ * One control nested in one container — the unit the overlay puts numbers on.
49
+ *
50
+ * A readout is NOT a verdict. Whether a box is WRONG is the audits' call
51
+ * (`runAudits` in ./audit), because the laws are subtler than any single pair:
52
+ * in a row of three buttons the middle one's distance to the container's left
53
+ * wall is meaningless, so "is this child's left gap equal to its top gap?" is
54
+ * the wrong question to ask of it. The audits ask the right ones — vertical
55
+ * symmetry per child, wall-vs-gap on the row, uniform sibling gaps, concentric
56
+ * radius — and this type just carries the geometry to draw.
57
+ */
58
+ export interface Readout {
59
+ readonly container: HTMLElement;
60
+ readonly child: HTMLElement;
61
+ readonly childBox: Box;
62
+ readonly gaps: Gaps;
63
+ /** Only meaningful when both boxes are rounded; null otherwise. */
64
+ readonly concentric: Concentricity | null;
65
+ }
66
+ /**
67
+ * Every container/child pair in a subtree worth putting a number on.
68
+ *
69
+ * The filter is what keeps this from drowning in noise: a pair qualifies only
70
+ * when the container is a CONTROL-shaped box — bounded height, and rounded or
71
+ * flex/grid — because the even-gap law is about the geometric padding inside a
72
+ * bounded tier, not the optical padding of an unbounded card. Layout wrappers (a
73
+ * section, a demo column) are skipped, and so is anything inside an <svg>.
74
+ */
75
+ export declare function measureReadouts(root: HTMLElement, maxContainerHeight?: number): Readout[];
@@ -0,0 +1,142 @@
1
+ /*
2
+ * Measurement primitives — the numbers era's layout rules are stated in.
3
+ *
4
+ * A class string is a claim; the rendered box is the fact. Every metric rule in
5
+ * the design system (the even-gap law, the concentricity law) is an assertion
6
+ * about geometry that only a real layout pass can settle, so this module reads
7
+ * it back off the DOM: rects, computed radii, and the gaps between a control and
8
+ * its container.
9
+ *
10
+ * The API is deliberately shaped like mezr's (getRect / getGaps / getDistance)
11
+ * so it can be swapped for that library, but it stays in-repo for two reasons a
12
+ * generic measurement library can't cover:
13
+ *
14
+ * - era's rules turn on border-RADIUS as much as on distance (the
15
+ * concentricity identity), which no DOM-distance library reports;
16
+ * - the readouts have to render as token-driven DOM so they inherit
17
+ * data-surface / data-mode / data-font like everything else. A canvas ruler
18
+ * (@scena/ruler) is blind to those axes.
19
+ *
20
+ * DOM-only: every function here reads layout, so it must run in the browser.
21
+ */
22
+ /** Sub-pixel residual to forgive — fractional line boxes and dpr rounding. */
23
+ export const TOL = 0.5;
24
+ /** Rect + resolved radius, relative to the viewport. */
25
+ export function getRect(el) {
26
+ const r = el.getBoundingClientRect();
27
+ return {
28
+ x: r.x,
29
+ y: r.y,
30
+ width: r.width,
31
+ height: r.height,
32
+ radius: parseFloat(getComputedStyle(el).borderTopLeftRadius) || 0
33
+ };
34
+ }
35
+ /** Rect relative to an offset root, so overlays can draw in the root's space. */
36
+ export function getRelativeRect(el, root) {
37
+ const b = getRect(el);
38
+ const r = root.getBoundingClientRect();
39
+ return { ...b, x: b.x - r.x, y: b.y - r.y };
40
+ }
41
+ /**
42
+ * The four gaps between a child's border box and its container's border box —
43
+ * what the eye actually reads as the inset, padding and border included.
44
+ */
45
+ export function getGaps(child, container) {
46
+ const c = child.getBoundingClientRect();
47
+ const p = container.getBoundingClientRect();
48
+ return {
49
+ top: c.top - p.top,
50
+ right: p.right - c.right,
51
+ bottom: p.bottom - c.bottom,
52
+ left: c.left - p.left
53
+ };
54
+ }
55
+ /** Shortest edge-to-edge distance between two boxes; 0 when they intersect. */
56
+ export function getDistance(a, b) {
57
+ const r1 = a.getBoundingClientRect();
58
+ const r2 = b.getBoundingClientRect();
59
+ const dx = Math.max(r2.left - r1.right, r1.left - r2.right, 0);
60
+ const dy = Math.max(r2.top - r1.bottom, r1.top - r2.bottom, 0);
61
+ return Math.hypot(dx, dy);
62
+ }
63
+ export function isEven(g, tol = TOL) {
64
+ const { top, right, bottom, left } = g;
65
+ return (Math.abs(top - bottom) <= tol && Math.abs(left - right) <= tol && Math.abs(top - left) <= tol);
66
+ }
67
+ /**
68
+ * The concentricity law. Nested corners share a centre of curvature only when
69
+ * the radius step equals the gap — checking any two of the three terms passes
70
+ * boxes that are visibly wrong, so assert all three at once.
71
+ */
72
+ export function getConcentricity(child, container, tol = TOL) {
73
+ const inner = getRect(child);
74
+ const outer = getRect(container);
75
+ const gaps = getGaps(child, container);
76
+ const radiusDelta = outer.radius - inner.radius;
77
+ const impliedGap = (outer.height - inner.height) / 2;
78
+ return {
79
+ outerRadius: outer.radius,
80
+ innerRadius: inner.radius,
81
+ radiusDelta,
82
+ impliedGap,
83
+ gap: gaps.top,
84
+ holds: Math.abs(radiusDelta - impliedGap) <= tol &&
85
+ Math.abs(impliedGap - gaps.top) <= tol &&
86
+ Math.abs(radiusDelta - gaps.top) <= tol
87
+ };
88
+ }
89
+ /**
90
+ * Every container/child pair in a subtree worth putting a number on.
91
+ *
92
+ * The filter is what keeps this from drowning in noise: a pair qualifies only
93
+ * when the container is a CONTROL-shaped box — bounded height, and rounded or
94
+ * flex/grid — because the even-gap law is about the geometric padding inside a
95
+ * bounded tier, not the optical padding of an unbounded card. Layout wrappers (a
96
+ * section, a demo column) are skipped, and so is anything inside an <svg>.
97
+ */
98
+ export function measureReadouts(root, maxContainerHeight = 96) {
99
+ const out = [];
100
+ const isCandidate = (el) => {
101
+ if (el === root)
102
+ return false;
103
+ const r = el.getBoundingClientRect();
104
+ if (r.height === 0 || r.height > maxContainerHeight)
105
+ return false;
106
+ const s = getComputedStyle(el);
107
+ return parseFloat(s.borderTopLeftRadius) > 0 || /flex|grid/.test(s.display);
108
+ };
109
+ const push = (container, child) => {
110
+ const childBox = getRect(child);
111
+ if (childBox.width === 0 || childBox.height === 0)
112
+ return;
113
+ // An absolutely-positioned child is out of flow — its gaps say nothing
114
+ // about the container's padding.
115
+ if (getComputedStyle(child).position === 'absolute')
116
+ return;
117
+ const containerBox = getRect(container);
118
+ out.push({
119
+ container,
120
+ child: child,
121
+ childBox,
122
+ gaps: getGaps(child, container),
123
+ concentric: childBox.radius > 0 && containerBox.radius > 0 ? getConcentricity(child, container) : null
124
+ });
125
+ };
126
+ const walk = (el) => {
127
+ for (const child of el.children) {
128
+ if (child instanceof SVGElement) {
129
+ if (isCandidate(el))
130
+ push(el, child); // never descend into svg internals
131
+ continue;
132
+ }
133
+ if (!(child instanceof HTMLElement))
134
+ continue;
135
+ if (isCandidate(el))
136
+ push(el, child);
137
+ walk(child);
138
+ }
139
+ };
140
+ walk(root);
141
+ return out;
142
+ }