@ponchia/ui 0.5.0 → 0.6.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 +322 -0
- package/MIGRATIONS.json +14 -0
- package/README.md +28 -5
- package/annotations/index.d.ts +398 -276
- package/annotations/index.d.ts.map +1 -0
- package/annotations/index.js +315 -45
- package/behaviors/carousel.js +17 -16
- package/behaviors/combobox.js +47 -16
- package/behaviors/command.js +18 -15
- package/behaviors/connectors.js +4 -5
- package/behaviors/crosshair.js +4 -5
- package/behaviors/dialog.js +3 -2
- package/behaviors/disclosure.js +3 -2
- package/behaviors/dismissible.js +3 -2
- package/behaviors/forms.js +41 -13
- package/behaviors/glyph.js +4 -5
- package/behaviors/internal.js +47 -0
- package/behaviors/legend.js +23 -2
- package/behaviors/menu.js +3 -2
- package/behaviors/popover.js +78 -7
- package/behaviors/spotlight.js +4 -5
- package/behaviors/table.js +39 -12
- package/behaviors/tabs.js +14 -14
- package/behaviors/theme.js +5 -3
- package/behaviors/toast.js +13 -1
- package/classes/classes.json +1857 -0
- package/classes/index.d.ts +28 -13
- package/classes/index.js +34 -18
- package/classes/vscode.css-custom-data.json +12 -0
- package/connectors/index.d.ts +189 -69
- package/connectors/index.d.ts.map +1 -0
- package/connectors/index.js +120 -24
- package/css/app.css +43 -13
- package/css/base.css +15 -10
- package/css/connectors.css +17 -0
- package/css/content.css +7 -1
- package/css/dataviz.css +5 -1
- package/css/disclosure.css +38 -6
- package/css/dots.css +57 -0
- package/css/feedback.css +60 -2
- package/css/forms.css +42 -1
- package/css/legend.css +11 -7
- package/css/marks.css +38 -8
- package/css/motion.css +24 -44
- package/css/navigation.css +7 -0
- package/css/overlay.css +31 -1
- package/css/primitives.css +91 -5
- package/css/report.css +40 -63
- package/css/site.css +16 -2
- package/css/sources.css +43 -1
- package/css/spotlight.css +1 -1
- package/css/tokens.css +36 -1
- package/css/workbench.css +1 -1
- package/dist/bronto.css +1 -1
- package/dist/css/analytical.css +1 -1
- package/dist/css/app.css +1 -1
- package/dist/css/base.css +1 -1
- package/dist/css/connectors.css +1 -1
- package/dist/css/content.css +1 -1
- package/dist/css/disclosure.css +1 -1
- package/dist/css/dots.css +1 -1
- package/dist/css/feedback.css +1 -1
- package/dist/css/forms.css +1 -1
- package/dist/css/legend.css +1 -1
- package/dist/css/marks.css +1 -1
- package/dist/css/motion.css +1 -1
- package/dist/css/navigation.css +1 -1
- package/dist/css/overlay.css +1 -1
- package/dist/css/primitives.css +1 -1
- package/dist/css/report.css +1 -1
- package/dist/css/site.css +1 -1
- package/dist/css/sources.css +1 -1
- package/dist/css/spotlight.css +1 -1
- package/dist/css/tokens.css +1 -1
- package/dist/css/workbench.css +1 -1
- package/docs/adr/0003-theme-model.md +1 -1
- package/docs/annotations.md +94 -14
- package/docs/architecture.md +50 -6
- package/docs/contrast.md +116 -92
- package/docs/d2.md +195 -0
- package/docs/legends.md +18 -2
- package/docs/marks.md +9 -2
- package/docs/mermaid.md +152 -0
- package/docs/reference.md +78 -22
- package/docs/reporting.md +395 -57
- package/docs/sources.md +27 -0
- package/docs/stability.md +9 -2
- package/docs/usage.md +101 -4
- package/docs/vega.md +225 -0
- package/docs/workbench.md +7 -1
- package/glyphs/glyphs.js +6 -4
- package/llms.txt +139 -14
- package/package.json +50 -12
- package/qwik/index.d.ts +42 -59
- package/qwik/index.d.ts.map +1 -0
- package/qwik/index.js +55 -3
- package/react/index.d.ts +39 -61
- package/react/index.d.ts.map +1 -0
- package/react/index.js +57 -3
- package/solid/index.d.ts +64 -61
- package/solid/index.d.ts.map +1 -0
- package/solid/index.js +60 -3
- package/tokens/d2.d.ts +38 -0
- package/tokens/d2.js +71 -0
- package/tokens/d2.json +43 -0
- package/tokens/index.d.ts +5 -5
- package/tokens/index.js +15 -1
- package/tokens/index.json +9 -0
- package/tokens/mermaid.d.ts +23 -0
- package/tokens/mermaid.js +181 -0
- package/tokens/mermaid.json +163 -0
- package/tokens/resolved.json +45 -1
- package/tokens/skins.js +3 -2
- package/tokens/tokens.dtcg.json +26 -0
- package/tokens/vega.d.ts +34 -0
- package/tokens/vega.js +155 -0
- package/tokens/vega.json +179 -0
package/annotations/index.js
CHANGED
|
@@ -1,47 +1,229 @@
|
|
|
1
1
|
// Shared SVG geometry primitives live in the connectors kernel; annotations
|
|
2
2
|
// (figure callouts) build on them so a line/curve/arrow/dot is drawn one way.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @ponchia/ui — SVG annotation geometry helpers.
|
|
6
|
+
*
|
|
7
|
+
* The public types below are JSDoc `@typedef`s; the shipped `index.d.ts` is
|
|
8
|
+
* generated from them (and these signatures) by `tsc --emitDeclarationOnly`.
|
|
9
|
+
*
|
|
10
|
+
* @typedef {{ x: number, y: number }} AnnotationPoint
|
|
11
|
+
* @typedef {{ dx: number, dy: number }} AnnotationOffset
|
|
12
|
+
* @typedef {'callout' | 'elbow' | 'curve'} AnnotationConnectorType
|
|
13
|
+
* @typedef {'start' | 'middle' | 'end'} AnnotationAlign
|
|
14
|
+
* @typedef {'top' | 'middle' | 'bottom'} AnnotationValign
|
|
15
|
+
* @typedef {'horizontal' | 'vertical'} AxisOrientation
|
|
16
|
+
* @typedef {'up' | 'down' | 'left' | 'right'} TimelineDirection
|
|
17
|
+
*
|
|
18
|
+
* @typedef {object} CircleSubject
|
|
19
|
+
* @property {'circle'} type
|
|
20
|
+
* @property {number} radius
|
|
21
|
+
* @property {number} [radiusPadding]
|
|
22
|
+
*
|
|
23
|
+
* @typedef {object} RectSubject
|
|
24
|
+
* @property {'rect'} type
|
|
25
|
+
* @property {number} width
|
|
26
|
+
* @property {number} height
|
|
27
|
+
* @property {number} [x]
|
|
28
|
+
* @property {number} [y]
|
|
29
|
+
* @property {number} [padding]
|
|
30
|
+
*
|
|
31
|
+
* @typedef {CircleSubject | RectSubject} ConnectorSubject
|
|
32
|
+
*
|
|
33
|
+
* @typedef {AnnotationOffset & { subject?: ConnectorSubject, mid?: number }} ConnectorOptions
|
|
34
|
+
*
|
|
35
|
+
* @typedef {object} CircleSubjectOptions
|
|
36
|
+
* @property {number} radius
|
|
37
|
+
*
|
|
38
|
+
* @typedef {object} RectSubjectOptions
|
|
39
|
+
* @property {number} width
|
|
40
|
+
* @property {number} height
|
|
41
|
+
* @property {number} [x]
|
|
42
|
+
* @property {number} [y]
|
|
43
|
+
* @property {number} [padding]
|
|
44
|
+
*
|
|
45
|
+
* @typedef {object} ThresholdOptions
|
|
46
|
+
* @property {number} [x1]
|
|
47
|
+
* @property {number} [y1]
|
|
48
|
+
* @property {number} x2
|
|
49
|
+
* @property {number} y2
|
|
50
|
+
*
|
|
51
|
+
* @typedef {object} AxisThresholdOptions
|
|
52
|
+
* @property {AxisOrientation} [orientation]
|
|
53
|
+
* @property {number} [value]
|
|
54
|
+
* @property {number} [start]
|
|
55
|
+
* @property {number} end
|
|
56
|
+
*
|
|
57
|
+
* @typedef {object} BracketSubjectOptions
|
|
58
|
+
* @property {number} x1
|
|
59
|
+
* @property {number} y1
|
|
60
|
+
* @property {number} x2
|
|
61
|
+
* @property {number} y2
|
|
62
|
+
* @property {number} [depth]
|
|
63
|
+
*
|
|
64
|
+
* @typedef {object} BandSubjectOptions
|
|
65
|
+
* @property {number} [x]
|
|
66
|
+
* @property {number} [y]
|
|
67
|
+
* @property {number} width
|
|
68
|
+
* @property {number} height
|
|
69
|
+
* @property {number} [padding]
|
|
70
|
+
*
|
|
71
|
+
* @typedef {object} SlopeSubjectOptions
|
|
72
|
+
* @property {number} x1
|
|
73
|
+
* @property {number} y1
|
|
74
|
+
* @property {number} x2
|
|
75
|
+
* @property {number} y2
|
|
76
|
+
*
|
|
77
|
+
* @typedef {object} ComparisonBraceOptions
|
|
78
|
+
* @property {number} x1
|
|
79
|
+
* @property {number} y1
|
|
80
|
+
* @property {number} x2
|
|
81
|
+
* @property {number} y2
|
|
82
|
+
* @property {number} [depth]
|
|
83
|
+
*
|
|
84
|
+
* @typedef {object} OutlierClusterOptions
|
|
85
|
+
* @property {AnnotationPoint[]} points
|
|
86
|
+
* @property {number} [radius]
|
|
87
|
+
*
|
|
88
|
+
* @typedef {object} TimelineEventOptions
|
|
89
|
+
* @property {number} [size]
|
|
90
|
+
* @property {TimelineDirection} [direction]
|
|
91
|
+
*
|
|
92
|
+
* @typedef {object} EvidenceMarkerOptions
|
|
93
|
+
* @property {number} [x]
|
|
94
|
+
* @property {number} [y]
|
|
95
|
+
* @property {number} [width]
|
|
96
|
+
* @property {number} [height]
|
|
97
|
+
* @property {number} [padding]
|
|
98
|
+
*
|
|
99
|
+
* @typedef {AnnotationPoint & { radius?: number }} ConnectorEndDotOptions
|
|
100
|
+
*
|
|
101
|
+
* @typedef {object} ConnectorEndArrowOptions
|
|
102
|
+
* @property {number} [x1]
|
|
103
|
+
* @property {number} [y1]
|
|
104
|
+
* @property {number} x2
|
|
105
|
+
* @property {number} y2
|
|
106
|
+
* @property {number} [size]
|
|
107
|
+
* @property {number} [spread] Half-angle of the arrowhead in radians (default
|
|
108
|
+
* 0.32 ≈ a crisp 37° included angle). Larger = blunter.
|
|
109
|
+
*
|
|
110
|
+
* @typedef {object} NoteTransformOptions
|
|
111
|
+
* @property {number} [dx]
|
|
112
|
+
* @property {number} [dy]
|
|
113
|
+
* @property {number} [x]
|
|
114
|
+
* @property {number} [y]
|
|
115
|
+
* @property {AnnotationAlign} [align]
|
|
116
|
+
* @property {AnnotationValign} [valign]
|
|
117
|
+
* @property {number} [width]
|
|
118
|
+
* @property {number} [height]
|
|
119
|
+
*
|
|
120
|
+
* @typedef {object} AnnotationBounds
|
|
121
|
+
* @property {number} [x]
|
|
122
|
+
* @property {number} [y]
|
|
123
|
+
* @property {number} width
|
|
124
|
+
* @property {number} height
|
|
125
|
+
*
|
|
126
|
+
* @typedef {object} NotePlacementOptions
|
|
127
|
+
* @property {number} [x]
|
|
128
|
+
* @property {number} [y]
|
|
129
|
+
* @property {number} width
|
|
130
|
+
* @property {number} height
|
|
131
|
+
* @property {AnnotationBounds} bounds
|
|
132
|
+
* @property {number} [padding]
|
|
133
|
+
* @property {number} [gap]
|
|
134
|
+
* @property {'right' | 'left' | 'top' | 'bottom'} [preferred]
|
|
135
|
+
* @property {number} [inset] Extra margin (user units) the note must keep from
|
|
136
|
+
* the bounds edge, on top of `padding`. Reserve the note's title stroke-halo
|
|
137
|
+
* (~3) or a leader stub so a placement that "fits" doesn't clip. Default 0.
|
|
138
|
+
*
|
|
139
|
+
* @typedef {object} NotePlacement
|
|
140
|
+
* @property {number} dx
|
|
141
|
+
* @property {number} dy
|
|
142
|
+
* @property {AnnotationAlign} align
|
|
143
|
+
* @property {AnnotationValign} valign
|
|
144
|
+
* @property {string} transform
|
|
145
|
+
*
|
|
146
|
+
* @typedef {(
|
|
147
|
+
* | CircleSubject
|
|
148
|
+
* | RectSubject
|
|
149
|
+
* | ({ type: 'threshold' } & ThresholdOptions)
|
|
150
|
+
* | ({ type: 'bracket' } & BracketSubjectOptions)
|
|
151
|
+
* | ({ type: 'band' } & BandSubjectOptions)
|
|
152
|
+
* | ({ type: 'slope' } & SlopeSubjectOptions)
|
|
153
|
+
* | ({ type: 'compare' } & ComparisonBraceOptions)
|
|
154
|
+
* | ({ type: 'cluster' } & OutlierClusterOptions)
|
|
155
|
+
* | ({ type: 'axis' } & AxisThresholdOptions)
|
|
156
|
+
* | ({ type: 'timeline' } & TimelineEventOptions)
|
|
157
|
+
* | ({ type: 'evidence' } & EvidenceMarkerOptions)
|
|
158
|
+
* )} AnnotationPartsSubject
|
|
159
|
+
*
|
|
160
|
+
* @typedef {object} AnnotationPartsOptions
|
|
161
|
+
* @property {AnnotationConnectorType} [type]
|
|
162
|
+
* @property {number} [x]
|
|
163
|
+
* @property {number} [y]
|
|
164
|
+
* @property {number} [dx]
|
|
165
|
+
* @property {number} [dy]
|
|
166
|
+
* @property {AnnotationPartsSubject} [subject]
|
|
167
|
+
*
|
|
168
|
+
* @typedef {object} AnnotationParts
|
|
169
|
+
* @property {string} transform
|
|
170
|
+
* @property {string} subject
|
|
171
|
+
* @property {string} connector
|
|
172
|
+
* @property {string} note
|
|
173
|
+
*
|
|
174
|
+
* @typedef {object} DeclutterLabelItem
|
|
175
|
+
* @property {number} pos Desired centre coordinate along the axis.
|
|
176
|
+
* @property {number} size The label's extent along the axis.
|
|
177
|
+
*
|
|
178
|
+
* @typedef {object} DeclutterLabelsOptions
|
|
179
|
+
* @property {number} [gap] Minimum gap kept between adjacent labels. Default 0.
|
|
180
|
+
* @property {number} [min] Lower bound of the axis. Default -Infinity.
|
|
181
|
+
* @property {number} [max] Upper bound of the axis. Default Infinity.
|
|
182
|
+
*
|
|
183
|
+
* @typedef {object} DirectLabelItem
|
|
184
|
+
* @property {AnnotationPoint} anchor The true data point the label refers to (figure coordinates).
|
|
185
|
+
* @property {number} size The label's extent along the layout axis.
|
|
186
|
+
* @property {string | number} [key] Optional identifier, echoed back on the matching output (input order).
|
|
187
|
+
*
|
|
188
|
+
* @typedef {object} DirectLabelsOptions
|
|
189
|
+
* @property {'x' | 'y'} [axis] Axis the labels declutter along. 'y' = a vertical column. Default 'y'.
|
|
190
|
+
* @property {number} [cross] Fixed coordinate on the other axis where the label column/row sits. Default 0.
|
|
191
|
+
* @property {number} [gap] Minimum gap kept between adjacent labels. Default 0.
|
|
192
|
+
* @property {number} [min] Lower bound of the layout axis. Default -Infinity.
|
|
193
|
+
* @property {number} [max] Upper bound of the layout axis. Default Infinity.
|
|
194
|
+
* @property {'straight' | 'elbow' | 'curve'} [shape] Leader-line shape. Default 'straight'.
|
|
195
|
+
*
|
|
196
|
+
* @typedef {object} DirectLabel
|
|
197
|
+
* @property {number} x Placed label point — the leader's label-side end.
|
|
198
|
+
* @property {number} y
|
|
199
|
+
* @property {AnnotationPoint} anchor The echoed input anchor.
|
|
200
|
+
* @property {string | number} [key] The echoed input key, if any.
|
|
201
|
+
* @property {string} d SVG path for the leader (anchor → label point); '' if they coincide.
|
|
202
|
+
*/
|
|
203
|
+
|
|
3
204
|
import {
|
|
4
205
|
straightPath,
|
|
5
206
|
curvePath,
|
|
6
207
|
connectorPath,
|
|
208
|
+
elbowPath,
|
|
7
209
|
arrowHead,
|
|
8
210
|
dotMark,
|
|
9
211
|
angleBetween,
|
|
212
|
+
// Shared scalar/geometry kernel — single source of truth (was copy-pasted,
|
|
213
|
+
// and the local clamp had silently diverged from the connectors one).
|
|
214
|
+
PRECISION,
|
|
215
|
+
finite,
|
|
216
|
+
dimension,
|
|
217
|
+
fmt,
|
|
218
|
+
point,
|
|
219
|
+
clamp,
|
|
10
220
|
} from '../connectors/index.js';
|
|
11
221
|
|
|
12
|
-
const PRECISION = 1000;
|
|
13
|
-
|
|
14
|
-
function finite(name, value, fallback) {
|
|
15
|
-
const v = value ?? fallback;
|
|
16
|
-
if (!Number.isFinite(v)) throw new TypeError(`${name} must be a finite number`);
|
|
17
|
-
return v;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function dimension(name, value, fallback) {
|
|
21
|
-
const v = finite(name, value, fallback);
|
|
22
|
-
if (v < 0) throw new RangeError(`${name} must be greater than or equal to 0`);
|
|
23
|
-
return v;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function fmt(value) {
|
|
27
|
-
const rounded = Math.round((Object.is(value, -0) ? 0 : value) * PRECISION) / PRECISION;
|
|
28
|
-
return String(Object.is(rounded, -0) ? 0 : rounded);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
222
|
function roundedNumber(value) {
|
|
32
223
|
const rounded = Math.round((Object.is(value, -0) ? 0 : value) * PRECISION) / PRECISION;
|
|
33
224
|
return Object.is(rounded, -0) ? 0 : rounded;
|
|
34
225
|
}
|
|
35
226
|
|
|
36
|
-
function point(x, y) {
|
|
37
|
-
return `${fmt(x)},${fmt(y)}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function clamp(value, min, max) {
|
|
41
|
-
if (max < min) return min;
|
|
42
|
-
return Math.min(max, Math.max(min, value));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
227
|
function circlePathAt(x, y, radius) {
|
|
46
228
|
const r = dimension('radius', radius);
|
|
47
229
|
if (r === 0) return '';
|
|
@@ -106,10 +288,18 @@ function linePath(start, end) {
|
|
|
106
288
|
return `M${point(start.x, start.y)}L${point(end.x, end.y)}`;
|
|
107
289
|
}
|
|
108
290
|
|
|
291
|
+
/**
|
|
292
|
+
* @param {Partial<AnnotationPoint>} [point]
|
|
293
|
+
* @returns {string}
|
|
294
|
+
*/
|
|
109
295
|
export function annotationTransform({ x = 0, y = 0 } = {}) {
|
|
110
296
|
return `translate(${fmt(finite('x', x))}, ${fmt(finite('y', y))})`;
|
|
111
297
|
}
|
|
112
298
|
|
|
299
|
+
/**
|
|
300
|
+
* @param {NoteTransformOptions} [options]
|
|
301
|
+
* @returns {string}
|
|
302
|
+
*/
|
|
113
303
|
export function noteTransform({
|
|
114
304
|
dx,
|
|
115
305
|
dy,
|
|
@@ -172,6 +362,10 @@ function noteRect(x, y, width, height, placement) {
|
|
|
172
362
|
};
|
|
173
363
|
}
|
|
174
364
|
|
|
365
|
+
/**
|
|
366
|
+
* @param {NotePlacementOptions} options
|
|
367
|
+
* @returns {NotePlacement}
|
|
368
|
+
*/
|
|
175
369
|
export function notePlacement({
|
|
176
370
|
x = 0,
|
|
177
371
|
y = 0,
|
|
@@ -181,6 +375,7 @@ export function notePlacement({
|
|
|
181
375
|
padding = 8,
|
|
182
376
|
gap = 32,
|
|
183
377
|
preferred = 'right',
|
|
378
|
+
inset = 0,
|
|
184
379
|
} = {}) {
|
|
185
380
|
const anchorX = finite('x', x);
|
|
186
381
|
const anchorY = finite('y', y);
|
|
@@ -188,14 +383,17 @@ export function notePlacement({
|
|
|
188
383
|
const h = dimension('height', height);
|
|
189
384
|
const p = dimension('padding', padding);
|
|
190
385
|
const g = dimension('gap', gap);
|
|
386
|
+
const ins = dimension('inset', inset, 0);
|
|
191
387
|
const bx = finite('bounds.x', bounds?.x, 0);
|
|
192
388
|
const by = finite('bounds.y', bounds?.y, 0);
|
|
193
389
|
const bw = dimension('bounds.width', bounds?.width);
|
|
194
390
|
const bh = dimension('bounds.height', bounds?.height);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
const
|
|
391
|
+
// `inset` reserves an extra margin (e.g. the title stroke-halo) inside the
|
|
392
|
+
// padded bounds, so a placement that "just fits" doesn't clip the halo/leader.
|
|
393
|
+
const minX = bx + p + ins;
|
|
394
|
+
const minY = by + p + ins;
|
|
395
|
+
const maxX = bx + bw - p - ins;
|
|
396
|
+
const maxY = by + bh - p - ins;
|
|
199
397
|
|
|
200
398
|
for (const side of placementOrder(preferred)) {
|
|
201
399
|
const placement = candidatePlacement(side, g);
|
|
@@ -226,10 +424,18 @@ export function notePlacement({
|
|
|
226
424
|
};
|
|
227
425
|
}
|
|
228
426
|
|
|
427
|
+
/**
|
|
428
|
+
* @param {CircleSubjectOptions} options
|
|
429
|
+
* @returns {string}
|
|
430
|
+
*/
|
|
229
431
|
export function circleSubjectPath({ radius } = {}) {
|
|
230
432
|
return circlePathAt(0, 0, radius);
|
|
231
433
|
}
|
|
232
434
|
|
|
435
|
+
/**
|
|
436
|
+
* @param {RectSubjectOptions} options
|
|
437
|
+
* @returns {string}
|
|
438
|
+
*/
|
|
233
439
|
export function rectSubjectPath({ width, height, x, y, padding = 0 } = {}) {
|
|
234
440
|
const w = dimension('width', width);
|
|
235
441
|
const h = dimension('height', height);
|
|
@@ -242,12 +448,20 @@ export function rectSubjectPath({ width, height, x, y, padding = 0 } = {}) {
|
|
|
242
448
|
return `M${point(left, top)}H${fmt(right)}V${fmt(bottom)}H${fmt(left)}Z`;
|
|
243
449
|
}
|
|
244
450
|
|
|
451
|
+
/**
|
|
452
|
+
* @param {ThresholdOptions} options
|
|
453
|
+
* @returns {string}
|
|
454
|
+
*/
|
|
245
455
|
export function thresholdPath({ x1 = 0, y1 = 0, x2, y2 } = {}) {
|
|
246
456
|
const start = { x: finite('x1', x1), y: finite('y1', y1) };
|
|
247
457
|
const end = { x: finite('x2', x2), y: finite('y2', y2) };
|
|
248
458
|
return linePath(start, end);
|
|
249
459
|
}
|
|
250
460
|
|
|
461
|
+
/**
|
|
462
|
+
* @param {AxisThresholdOptions} options
|
|
463
|
+
* @returns {string}
|
|
464
|
+
*/
|
|
251
465
|
export function axisThresholdPath({ orientation = 'horizontal', value = 0, start = 0, end } = {}) {
|
|
252
466
|
const v = finite('value', value);
|
|
253
467
|
const s = finite('start', start);
|
|
@@ -257,6 +471,10 @@ export function axisThresholdPath({ orientation = 'horizontal', value = 0, start
|
|
|
257
471
|
throw new TypeError('orientation must be "horizontal" or "vertical"');
|
|
258
472
|
}
|
|
259
473
|
|
|
474
|
+
/**
|
|
475
|
+
* @param {BracketSubjectOptions} options
|
|
476
|
+
* @returns {string}
|
|
477
|
+
*/
|
|
260
478
|
export function bracketSubjectPath({ x1, y1, x2, y2, depth = 12 } = {}) {
|
|
261
479
|
const start = { x: finite('x1', x1), y: finite('y1', y1) };
|
|
262
480
|
const end = { x: finite('x2', x2), y: finite('y2', y2) };
|
|
@@ -268,14 +486,26 @@ export function bracketSubjectPath({ x1, y1, x2, y2, depth = 12 } = {}) {
|
|
|
268
486
|
return `M${point(start.x, start.y)}H${fmt(start.x + d)}V${fmt(end.y)}H${fmt(end.x)}`;
|
|
269
487
|
}
|
|
270
488
|
|
|
489
|
+
/**
|
|
490
|
+
* @param {BandSubjectOptions} options
|
|
491
|
+
* @returns {string}
|
|
492
|
+
*/
|
|
271
493
|
export function bandSubjectPath({ x = 0, y = 0, width, height, padding = 0 } = {}) {
|
|
272
494
|
return rectSubjectPath({ x, y, width, height, padding });
|
|
273
495
|
}
|
|
274
496
|
|
|
497
|
+
/**
|
|
498
|
+
* @param {SlopeSubjectOptions} options
|
|
499
|
+
* @returns {string}
|
|
500
|
+
*/
|
|
275
501
|
export function slopeSubjectPath({ x1, y1, x2, y2 } = {}) {
|
|
276
502
|
return thresholdPath({ x1, y1, x2, y2 });
|
|
277
503
|
}
|
|
278
504
|
|
|
505
|
+
/**
|
|
506
|
+
* @param {ComparisonBraceOptions} options
|
|
507
|
+
* @returns {string}
|
|
508
|
+
*/
|
|
279
509
|
export function comparisonBracePath({ x1, y1, x2, y2, depth = 14 } = {}) {
|
|
280
510
|
const start = { x: finite('x1', x1), y: finite('y1', y1) };
|
|
281
511
|
const end = { x: finite('x2', x2), y: finite('y2', y2) };
|
|
@@ -313,6 +543,10 @@ export function comparisonBracePath({ x1, y1, x2, y2, depth = 14 } = {}) {
|
|
|
313
543
|
)} ${point(x, end.y)}`;
|
|
314
544
|
}
|
|
315
545
|
|
|
546
|
+
/**
|
|
547
|
+
* @param {OutlierClusterOptions} options
|
|
548
|
+
* @returns {string}
|
|
549
|
+
*/
|
|
316
550
|
export function outlierClusterPath({ points, radius = 6 } = {}) {
|
|
317
551
|
if (!Array.isArray(points)) throw new TypeError('points must be an array');
|
|
318
552
|
return points
|
|
@@ -323,6 +557,10 @@ export function outlierClusterPath({ points, radius = 6 } = {}) {
|
|
|
323
557
|
.join('');
|
|
324
558
|
}
|
|
325
559
|
|
|
560
|
+
/**
|
|
561
|
+
* @param {TimelineEventOptions} [options]
|
|
562
|
+
* @returns {string}
|
|
563
|
+
*/
|
|
326
564
|
export function timelineEventPath({ size = 10, direction = 'down' } = {}) {
|
|
327
565
|
const s = dimension('size', size);
|
|
328
566
|
if (s === 0) return '';
|
|
@@ -333,6 +571,10 @@ export function timelineEventPath({ size = 10, direction = 'down' } = {}) {
|
|
|
333
571
|
throw new TypeError('direction must be "up", "down", "left" or "right"');
|
|
334
572
|
}
|
|
335
573
|
|
|
574
|
+
/**
|
|
575
|
+
* @param {EvidenceMarkerOptions} [options]
|
|
576
|
+
* @returns {string}
|
|
577
|
+
*/
|
|
336
578
|
export function evidenceMarkerPath({ x = 0, y = 0, width = 36, height = 36, padding = 0 } = {}) {
|
|
337
579
|
const w = dimension('width', width);
|
|
338
580
|
const h = dimension('height', height);
|
|
@@ -347,18 +589,30 @@ export function evidenceMarkerPath({ x = 0, y = 0, width = 36, height = 36, padd
|
|
|
347
589
|
return `M${point(left, top)}H${fmt(right)}V${fmt(bottom)}H${fmt(left)}Z`;
|
|
348
590
|
}
|
|
349
591
|
|
|
592
|
+
/**
|
|
593
|
+
* @param {ConnectorEndDotOptions} options
|
|
594
|
+
* @returns {string}
|
|
595
|
+
*/
|
|
350
596
|
export function connectorEndDot({ x, y, radius = 3 } = {}) {
|
|
351
597
|
return dotMark({ x: finite('x', x), y: finite('y', y) }, radius);
|
|
352
598
|
}
|
|
353
599
|
|
|
354
|
-
|
|
600
|
+
/**
|
|
601
|
+
* @param {ConnectorEndArrowOptions} options
|
|
602
|
+
* @returns {string}
|
|
603
|
+
*/
|
|
604
|
+
export function connectorEndArrow({ x1 = 0, y1 = 0, x2, y2, size = 8, spread = 0.32 } = {}) {
|
|
355
605
|
const start = { x: finite('x1', x1), y: finite('y1', y1) };
|
|
356
606
|
const end = { x: finite('x2', x2), y: finite('y2', y2) };
|
|
357
607
|
const s = dimension('size', size);
|
|
358
608
|
if (s === 0 || (end.x === start.x && end.y === start.y)) return '';
|
|
359
|
-
return arrowHead(end, angleBetween(start, end), s);
|
|
609
|
+
return arrowHead(end, angleBetween(start, end), s, spread);
|
|
360
610
|
}
|
|
361
611
|
|
|
612
|
+
/**
|
|
613
|
+
* @param {ConnectorOptions} opts
|
|
614
|
+
* @returns {string}
|
|
615
|
+
*/
|
|
362
616
|
export function connectorLine(opts = {}) {
|
|
363
617
|
const { dx, dy } = validateOffset(opts);
|
|
364
618
|
if (dx === 0 && dy === 0) return '';
|
|
@@ -370,25 +624,29 @@ export function connectorLine(opts = {}) {
|
|
|
370
624
|
return straightPath(start, end);
|
|
371
625
|
}
|
|
372
626
|
|
|
627
|
+
/**
|
|
628
|
+
* @param {ConnectorOptions} opts
|
|
629
|
+
* @returns {string}
|
|
630
|
+
*/
|
|
373
631
|
export function connectorElbow(opts = {}) {
|
|
374
632
|
const { dx, dy } = validateOffset(opts);
|
|
375
633
|
if (dx === 0 && dy === 0) return '';
|
|
376
634
|
const start = connectorStart(dx, dy, opts.subject);
|
|
377
635
|
if (!start) return '';
|
|
378
636
|
const end = { x: dx, y: dy };
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
: { x: end.x, y: start.y + Math.sign(vy) * Math.abs(vx) };
|
|
387
|
-
|
|
388
|
-
if (samePoint(start, elbow) || samePoint(elbow, end)) return linePath(start, end);
|
|
389
|
-
return `M${point(start.x, start.y)}L${point(elbow.x, elbow.y)}L${point(end.x, end.y)}`;
|
|
637
|
+
if (samePoint(start, end)) return '';
|
|
638
|
+
// A true right-angle dogleg (H/V/H), turning on the dominant axis at `mid`
|
|
639
|
+
// (0..1, default 0.5). Delegated to the connectors geometry kernel so an
|
|
640
|
+
// annotation leader and a node connector draw the same elbow. (The former
|
|
641
|
+
// inline form turned by min(|dx|,|dy|), i.e. a 45° chamfer that read as a
|
|
642
|
+
// diagonal stub, not an elbow — which the `stroke-linejoin` bevel assumes.)
|
|
643
|
+
return elbowPath(start, end, { mid: opts.mid });
|
|
390
644
|
}
|
|
391
645
|
|
|
646
|
+
/**
|
|
647
|
+
* @param {ConnectorOptions} opts
|
|
648
|
+
* @returns {string}
|
|
649
|
+
*/
|
|
392
650
|
export function connectorCurve(opts = {}) {
|
|
393
651
|
const { dx, dy } = validateOffset(opts);
|
|
394
652
|
if (dx === 0 && dy === 0) return '';
|
|
@@ -400,6 +658,10 @@ export function connectorCurve(opts = {}) {
|
|
|
400
658
|
return curvePath(start, end, { curvature: 0.35 });
|
|
401
659
|
}
|
|
402
660
|
|
|
661
|
+
/**
|
|
662
|
+
* @param {AnnotationPartsOptions} [opts]
|
|
663
|
+
* @returns {AnnotationParts}
|
|
664
|
+
*/
|
|
403
665
|
export function annotationParts(opts = {}) {
|
|
404
666
|
const type = opts.type ?? 'callout';
|
|
405
667
|
const transform = annotationTransform({ x: opts.x ?? 0, y: opts.y ?? 0 });
|
|
@@ -442,6 +704,10 @@ export function annotationParts(opts = {}) {
|
|
|
442
704
|
* `items`: `[{ pos, size }]` — `pos` is the desired centre coordinate along the
|
|
443
705
|
* axis, `size` the label's extent along it. Returns the adjusted centre per
|
|
444
706
|
* input item, in the original order.
|
|
707
|
+
*
|
|
708
|
+
* @param {DeclutterLabelItem[]} items
|
|
709
|
+
* @param {DeclutterLabelsOptions} [opts]
|
|
710
|
+
* @returns {number[]}
|
|
445
711
|
*/
|
|
446
712
|
export function declutterLabels(items, opts = {}) {
|
|
447
713
|
if (!Array.isArray(items)) throw new TypeError('items must be an array');
|
|
@@ -488,6 +754,10 @@ export function declutterLabels(items, opts = {}) {
|
|
|
488
754
|
* in input order, the placed label point `{x, y}`, the echoed `anchor` and
|
|
489
755
|
* `key`, and the leader path `d` (anchor → label; `''` if they coincide) ready
|
|
490
756
|
* for a `<path class="ui-annotation__connector">`.
|
|
757
|
+
*
|
|
758
|
+
* @param {DirectLabelItem[]} items
|
|
759
|
+
* @param {DirectLabelsOptions} [opts]
|
|
760
|
+
* @returns {DirectLabel[]}
|
|
491
761
|
*/
|
|
492
762
|
export function directLabels(items, opts = {}) {
|
|
493
763
|
if (!Array.isArray(items)) throw new TypeError('items must be an array');
|
package/behaviors/carousel.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
hasDom,
|
|
3
|
+
resolveHost,
|
|
4
|
+
noop,
|
|
5
|
+
bindOnce,
|
|
6
|
+
scrollIntoViewSafe,
|
|
7
|
+
collectHosts,
|
|
8
|
+
} from './internal.js';
|
|
2
9
|
|
|
3
10
|
/**
|
|
4
11
|
* Image carousel / gallery, built on CSS scroll-snap so touch + trackpad
|
|
@@ -25,10 +32,9 @@ import { hasDom, noop, bindOnce } from './internal.js';
|
|
|
25
32
|
*/
|
|
26
33
|
export function initCarousel({ root } = {}) {
|
|
27
34
|
if (!hasDom()) return noop;
|
|
28
|
-
const host = root
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
boxes.push(...(host.querySelectorAll?.('[data-bronto-carousel]') ?? []));
|
|
35
|
+
const host = resolveHost(root);
|
|
36
|
+
if (!host) return noop;
|
|
37
|
+
const boxes = collectHosts(host, '[data-bronto-carousel]');
|
|
32
38
|
const cleanups = [];
|
|
33
39
|
|
|
34
40
|
for (const box of boxes) {
|
|
@@ -103,16 +109,7 @@ export function initCarousel({ root } = {}) {
|
|
|
103
109
|
const emit = () =>
|
|
104
110
|
box.dispatchEvent(new CustomEvent('bronto:change', { detail: { index }, bubbles: true }));
|
|
105
111
|
|
|
106
|
-
|
|
107
|
-
// affordance, so never let it break index/aria sync — same guard as
|
|
108
|
-
// initCombobox.
|
|
109
|
-
const reveal = (el) => {
|
|
110
|
-
try {
|
|
111
|
-
el?.scrollIntoView({ block: 'nearest', inline: 'center' });
|
|
112
|
-
} catch {
|
|
113
|
-
/* no layout — ignore */
|
|
114
|
-
}
|
|
115
|
-
};
|
|
112
|
+
const reveal = (el) => scrollIntoViewSafe(el, { block: 'nearest', inline: 'center' });
|
|
116
113
|
|
|
117
114
|
const goTo = (i, { emitChange = true } = {}) => {
|
|
118
115
|
const next = loop ? (i + n) % n : Math.max(0, Math.min(n - 1, i));
|
|
@@ -176,13 +173,17 @@ export function initCarousel({ root } = {}) {
|
|
|
176
173
|
},
|
|
177
174
|
{ root: viewport, threshold: 0.6 },
|
|
178
175
|
);
|
|
179
|
-
slides.forEach((s) => io.observe(s));
|
|
180
176
|
}
|
|
181
177
|
|
|
182
178
|
render();
|
|
183
179
|
const bound = bindOnce(box, 'carousel', () => {
|
|
184
180
|
viewport.addEventListener('keydown', onKey);
|
|
185
181
|
box.addEventListener('click', onClick);
|
|
182
|
+
// Observe inside the add callback so observe/disconnect pair with the
|
|
183
|
+
// binding lifecycle: a re-init tears down the prior binding (which
|
|
184
|
+
// disconnects the old observer) before this starts, so two observers
|
|
185
|
+
// never watch the same slides — even for one tick.
|
|
186
|
+
slides.forEach((s) => io?.observe(s));
|
|
186
187
|
return () => {
|
|
187
188
|
viewport.removeEventListener('keydown', onKey);
|
|
188
189
|
box.removeEventListener('click', onClick);
|
package/behaviors/combobox.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
hasDom,
|
|
3
|
+
resolveHost,
|
|
4
|
+
noop,
|
|
5
|
+
bindOnce,
|
|
6
|
+
nextFieldUid,
|
|
7
|
+
scrollIntoViewSafe,
|
|
8
|
+
wrapIndex,
|
|
9
|
+
collectHosts,
|
|
10
|
+
} from './internal.js';
|
|
2
11
|
|
|
3
12
|
/**
|
|
4
13
|
* Editable combobox with a filtered listbox popup, implementing the
|
|
@@ -6,6 +15,11 @@ import { hasDom, noop, bindOnce, nextFieldUid } from './internal.js';
|
|
|
6
15
|
* and consumers most often build badly). Dependency-free, no
|
|
7
16
|
* positioning library — the list is CSS-anchored under the input.
|
|
8
17
|
*
|
|
18
|
+
* The input MUST have an accessible name — a `<label>`, `aria-label`, or
|
|
19
|
+
* `aria-labelledby` (a placeholder does not count). A nameless `role="combobox"`
|
|
20
|
+
* is a silent screen-reader failure, so the behavior warns at dev time when it
|
|
21
|
+
* finds one, and mirrors the input's name onto the listbox.
|
|
22
|
+
*
|
|
9
23
|
* Markup: `[data-bronto-combobox]` wrapping an `<input role="combobox">`
|
|
10
24
|
* (`.ui-combobox__input`) and a `<ul role="listbox">`
|
|
11
25
|
* (`.ui-combobox__list`) of `<li role="option">` (`.ui-combobox__option`,
|
|
@@ -16,13 +30,16 @@ import { hasDom, noop, bindOnce, nextFieldUid } from './internal.js';
|
|
|
16
30
|
* pointer select, and outside-click close; it emits a `bronto:change`
|
|
17
31
|
* CustomEvent ({ detail: { value } }) on selection. SSR-safe,
|
|
18
32
|
* idempotent per instance; returns a cleanup function.
|
|
33
|
+
*
|
|
34
|
+
* Options are read from the DOM at init; if you replace the listbox contents
|
|
35
|
+
* (e.g. async/remote results) without re-initialising, filtering and keyboard
|
|
36
|
+
* nav act on the stale nodes — re-run initCombobox after mutating the options.
|
|
19
37
|
*/
|
|
20
38
|
export function initCombobox({ root } = {}) {
|
|
21
39
|
if (!hasDom()) return noop;
|
|
22
|
-
const host = root
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
boxes.push(...(host.querySelectorAll?.('[data-bronto-combobox]') ?? []));
|
|
40
|
+
const host = resolveHost(root);
|
|
41
|
+
if (!host) return noop;
|
|
42
|
+
const boxes = collectHosts(host, '[data-bronto-combobox]');
|
|
26
43
|
const cleanups = [];
|
|
27
44
|
|
|
28
45
|
for (const box of boxes) {
|
|
@@ -38,6 +55,29 @@ export function initCombobox({ root } = {}) {
|
|
|
38
55
|
o.setAttribute('role', 'option');
|
|
39
56
|
});
|
|
40
57
|
list.setAttribute('role', 'listbox');
|
|
58
|
+
// Give the listbox its own accessible name (a bare role=listbox is unnamed
|
|
59
|
+
// to a screen reader) by mirroring the input's name. (a11y review C30.)
|
|
60
|
+
if (!list.hasAttribute('aria-label') && !list.hasAttribute('aria-labelledby')) {
|
|
61
|
+
const name =
|
|
62
|
+
input.getAttribute('aria-label') ||
|
|
63
|
+
input.labels?.[0]?.textContent?.trim() ||
|
|
64
|
+
input.getAttribute('placeholder');
|
|
65
|
+
if (name) list.setAttribute('aria-label', name);
|
|
66
|
+
}
|
|
67
|
+
// A `role="combobox"` with no accessible name is a silent AT failure. A
|
|
68
|
+
// placeholder is not a robust name (it can vanish and is ignored by some
|
|
69
|
+
// AT), so warn unless there is a real label/aria-label/aria-labelledby/title
|
|
70
|
+
// (C7). We can't invent a good name, hence a dev-time warning, not a guess.
|
|
71
|
+
const inputNamed =
|
|
72
|
+
input.hasAttribute('aria-label') ||
|
|
73
|
+
input.hasAttribute('aria-labelledby') ||
|
|
74
|
+
!!input.labels?.length ||
|
|
75
|
+
input.hasAttribute('title');
|
|
76
|
+
if (!inputNamed && typeof console !== 'undefined') {
|
|
77
|
+
console.warn(
|
|
78
|
+
'[bronto] initCombobox(): the combobox input has no accessible name — add a <label>, aria-label, or aria-labelledby (a placeholder is not enough).',
|
|
79
|
+
);
|
|
80
|
+
}
|
|
41
81
|
input.setAttribute('role', 'combobox');
|
|
42
82
|
input.setAttribute('aria-controls', listId);
|
|
43
83
|
input.setAttribute('aria-autocomplete', 'list');
|
|
@@ -53,13 +93,7 @@ export function initCombobox({ root } = {}) {
|
|
|
53
93
|
if (opt) {
|
|
54
94
|
opt.classList.add('is-active');
|
|
55
95
|
input.setAttribute('aria-activedescendant', opt.id);
|
|
56
|
-
|
|
57
|
-
// pure affordance, so never let it break keyboard nav.
|
|
58
|
-
try {
|
|
59
|
-
opt.scrollIntoView({ block: 'nearest' });
|
|
60
|
-
} catch {
|
|
61
|
-
/* non-DOM/headless environment — ignore */
|
|
62
|
-
}
|
|
96
|
+
scrollIntoViewSafe(opt);
|
|
63
97
|
} else {
|
|
64
98
|
input.removeAttribute('aria-activedescendant');
|
|
65
99
|
}
|
|
@@ -112,10 +146,7 @@ export function initCombobox({ root } = {}) {
|
|
|
112
146
|
const vis = visible();
|
|
113
147
|
if (!vis.length) return;
|
|
114
148
|
open();
|
|
115
|
-
const
|
|
116
|
-
let next = curIdx + delta;
|
|
117
|
-
if (next < 0) next = vis.length - 1;
|
|
118
|
-
if (next >= vis.length) next = 0;
|
|
149
|
+
const next = wrapIndex(vis.indexOf(options[active]), delta, vis.length);
|
|
119
150
|
active = options.indexOf(vis[next]);
|
|
120
151
|
setActive(options[active]);
|
|
121
152
|
};
|