@astryxdesign/lab 0.7.5-canary.fd0aba8 → 0.7.6-canary.0ef60dd

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.
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @file CircularProgress.tsx
3
- * @input Uses React, useId, stylex, SVG, color/spacing/duration/ease tokens
3
+ * @input Uses React (useId, refs, layout effects), stylex, SVG, Web
4
+ * Animations, color/spacing/duration/ease tokens
4
5
  * @output Exports CircularProgress component, CircularProgressProps, CircularProgressSize, CircularProgressVariant types
5
6
  * @position Core implementation; consumed by index.ts
6
7
  *
@@ -1 +1 @@
1
- {"version":3,"file":"CircularProgress.d.ts","sourceRoot":"","sources":["../../src/CircularProgress/CircularProgress.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAQ,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAY5C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAGlD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,IAAI,CAAC;IACb,OAAO,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,IAAI,CAAC;IACZ,OAAO,EAAE,IAAI,CAAC;CACf;AAED,MAAM,MAAM,uBAAuB,GAAG,MAAM,0BAA0B,CAAC;AAEvE,MAAM,MAAM,oBAAoB,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAQtD,MAAM,WAAW,qBAAsB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACtE,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1D;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,oBAAoB,CAAC;IAC5B;;;OAGG;IACH,OAAO,CAAC,EAAE,uBAAuB,CAAC;IAClC;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAuKD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,KAAS,EACT,GAAS,EACT,KAAK,EACL,aAAoB,EACpB,aAAqB,EACrB,gBAA0C,EAC1C,QAAQ,EACR,IAAW,EACX,OAAkB,EAClB,eAAuB,EACvB,UAAkB,EAClB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,UAAU,EACzB,GAAG,EACH,GAAG,IAAI,EACR,EAAE,qBAAqB,+BAuHvB;yBAzIe,gBAAgB"}
1
+ {"version":3,"file":"CircularProgress.d.ts","sourceRoot":"","sources":["../../src/CircularProgress/CircularProgress.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAiC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAYrE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAGlD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,IAAI,CAAC;IACb,OAAO,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,IAAI,CAAC;IACZ,OAAO,EAAE,IAAI,CAAC;CACf;AAED,MAAM,MAAM,uBAAuB,GAAG,MAAM,0BAA0B,CAAC;AAEvE,MAAM,MAAM,oBAAoB,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAWtD,MAAM,WAAW,qBAAsB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACtE,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1D;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,oBAAoB,CAAC;IAC5B;;;OAGG;IACH,OAAO,CAAC,EAAE,uBAAuB,CAAC;IAClC;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAmWD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,KAAS,EACT,GAAS,EACT,KAAK,EACL,aAAoB,EACpB,aAAqB,EACrB,gBAA0C,EAC1C,QAAQ,EACR,IAAW,EACX,OAAkB,EAClB,eAAuB,EACvB,UAAkB,EAClB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,UAAU,EACzB,GAAG,EACH,GAAG,IAAI,EACR,EAAE,qBAAqB,+BA+QvB;yBAjSe,gBAAgB"}
@@ -4,7 +4,8 @@
4
4
 
5
5
  /**
6
6
  * @file CircularProgress.tsx
7
- * @input Uses React, useId, stylex, SVG, color/spacing/duration/ease tokens
7
+ * @input Uses React (useId, refs, layout effects), stylex, SVG, Web
8
+ * Animations, color/spacing/duration/ease tokens
8
9
  * @output Exports CircularProgress component, CircularProgressProps, CircularProgressSize, CircularProgressVariant types
9
10
  * @position Core implementation; consumed by index.ts
10
11
  *
@@ -14,7 +15,7 @@
14
15
  * - /packages/lab/src/CircularProgress/index.ts (exports if types change)
15
16
  * - /apps/storybook/stories/CircularProgress.stories.tsx (storybook stories)
16
17
  */
17
- import { useId } from 'react';
18
+ import { useId, useLayoutEffect, useRef } from 'react';
18
19
  import * as stylex from '@stylexjs/stylex';
19
20
  import '@astryxdesign/core/theme/tokens.stylex';
20
21
  import { colorVars, durationVars, easeVars, fontWeightVars, spacingVars, typeScaleVars } from '@astryxdesign/core/theme/tokens.stylex';
@@ -35,6 +36,8 @@ import { themeProps } from '@astryxdesign/core/utils';
35
36
  * ```
36
37
  */
37
38
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
39
+ /** How far a turning layer's drawing reaches past the ring's box, in px. */
40
+ const FILL_BLEED = 2;
38
41
  const SIZE_CONFIG = {
39
42
  sm: {
40
43
  diameter: 32,
@@ -83,9 +86,11 @@ const styles = {
83
86
  $$css: true
84
87
  },
85
88
  fill: {
86
- kDwRjp: "xbh8q5q",
87
- kU5bRw: "x1owpc8m",
88
- k1ekBW: "xxnu56j",
89
+ kVAEAm: "x10l6tqk",
90
+ kpwlN0: "x10a8y8t",
91
+ kVQacm: "xb3r6kr",
92
+ kfzvcC: "x47corl",
93
+ k1ekBW: "x13b0p5u",
89
94
  kIyJzY: "x80gvsz",
90
95
  kAMwcw: "xlr8y92",
91
96
  $$css: true
@@ -153,6 +158,129 @@ function defaultFormatValueLabel(value, max) {
153
158
  return `${pct}%`;
154
159
  }
155
160
 
161
+ /**
162
+ * Each half-ring's turn and the leading cap's for a filled share `p` (0–1),
163
+ * and which parts show. A half-ring parked at the edge of its half (the right
164
+ * one at 0, the left one until the value passes half) is hidden, so its
165
+ * antialiased end never leaves a hairline at 6 o'clock.
166
+ */
167
+ function fillTurns(p) {
168
+ const turn = 360 * p;
169
+ return {
170
+ right: Math.min(Math.max(turn, 0), 180),
171
+ left: Math.min(Math.max(turn, 180), 360),
172
+ cap: turn,
173
+ rightShown: p > 0 ? 1 : 0,
174
+ leftShown: p > 0.5 ? 1 : 0,
175
+ caps: p > 0 ? 1 : 0
176
+ };
177
+ }
178
+
179
+ /** A CSS time (`410ms`, `0.41s`, or a list's first entry) in milliseconds. */
180
+ function parseTime(value) {
181
+ const first = value.split(',')[0]?.trim() ?? '';
182
+ const amount = Number.parseFloat(first);
183
+ if (!Number.isFinite(amount)) {
184
+ return 0;
185
+ }
186
+ return first.endsWith('ms') ? amount : amount * 1000;
187
+ }
188
+
189
+ /**
190
+ * The theme's easing as a function of time (0–1), evaluated by the browser
191
+ * itself through a detached animation, so any CSS easing works. It runs only
192
+ * where Web Animations exists (its caller returns first otherwise), on the
193
+ * fill's computed `transition-timing-function`: one easing, which a browser
194
+ * always reports as valid CSS.
195
+ */
196
+ function easingOf(easing) {
197
+ const effect = new KeyframeEffect(null, null, {
198
+ duration: 1000,
199
+ easing,
200
+ fill: 'both'
201
+ });
202
+ const probe = new Animation(effect);
203
+ return t => {
204
+ probe.currentTime = t * 1000;
205
+ return effect.getComputedTiming().progress ?? t;
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Keyframes for a change from `from` to `to` (filled shares), sampled once a
211
+ * frame on the theme's easing, with an extra sample exactly where the arc
212
+ * crosses 12 or 6 o'clock so each half stops and starts on time.
213
+ */
214
+ function fillKeyframes(from, to, duration, ease) {
215
+ const count = Math.max(8, Math.ceil(duration / (1000 / 60)));
216
+ const grid = Array.from({
217
+ length: count + 1
218
+ }, (_, i) => i / count);
219
+ const at = t => from + (to - from) * ease(t);
220
+ // Where the eased path passes half (p = 0.5): a sample there, and one just
221
+ // after it, so the halves hand over on time and the left half's
222
+ // visibility steps rather than fades.
223
+ const crossings = [];
224
+ for (let i = 0; i < count; i++) {
225
+ const a = grid[i];
226
+ const b = grid[i + 1];
227
+ const fa = at(a) - 0.5;
228
+ const fb = at(b) - 0.5;
229
+ if (fa === 0) {
230
+ crossings.push(a);
231
+ } else if (fa * fb < 0) {
232
+ let low = a;
233
+ let high = b;
234
+ for (let k = 0; k < 24; k++) {
235
+ const mid = (low + high) / 2;
236
+ if ((at(low) - 0.5) * (at(mid) - 0.5) <= 0) {
237
+ high = mid;
238
+ } else {
239
+ low = mid;
240
+ }
241
+ }
242
+ crossings.push((low + high) / 2);
243
+ }
244
+ }
245
+ const times = [...new Set([...grid, ...crossings, ...crossings.map(c => c + 1e-4).filter(t => t < 1)])].sort((x, y) => x - y);
246
+ const frames = times.map(t => ({
247
+ t,
248
+ ...fillTurns(t === 1 ? to : at(t))
249
+ }));
250
+ // The sample at a crossing sits exactly on half, where the left half reads
251
+ // as hidden. Give it the side the arc is leaving instead, so the left half
252
+ // steps at once both ways: shrinking through half, it used to fade out
253
+ // from the sample before, while it still drew part of the arc (a lighter
254
+ // band between the end cap and 6 o'clock for a frame).
255
+ const onHalf = new Set(crossings);
256
+ for (let i = 1; i < frames.length; i += 1) {
257
+ if (onHalf.has(frames[i].t) || frames[i].t === 1 && to === 0.5) {
258
+ frames[i].leftShown = frames[i - 1].leftShown;
259
+ }
260
+ }
261
+ return {
262
+ right: frames.map(f => ({
263
+ offset: f.t,
264
+ transform: `rotate(${f.right}deg)`,
265
+ opacity: f.rightShown
266
+ })),
267
+ left: frames.map(f => ({
268
+ offset: f.t,
269
+ transform: `rotate(${f.left}deg)`,
270
+ opacity: f.leftShown
271
+ })),
272
+ cap: frames.map(f => ({
273
+ offset: f.t,
274
+ transform: `rotate(${f.cap}deg)`,
275
+ opacity: f.caps
276
+ })),
277
+ start: frames.map(f => ({
278
+ offset: f.t,
279
+ opacity: f.caps
280
+ }))
281
+ };
282
+ }
283
+
156
284
  /**
157
285
  * A circular/radial progress indicator that shows completion as a ring.
158
286
  *
@@ -205,7 +333,6 @@ export function CircularProgress({
205
333
  const safeMax = Number.isFinite(max) ? max : 0;
206
334
  const clampedValue = Math.min(Math.max(0, safeValue), safeMax);
207
335
  const percentage = safeMax > 0 ? clampedValue / safeMax : 0;
208
- const dashoffset = circumference * (1 - percentage);
209
336
  const valueText = formatValueLabel(clampedValue, safeMax);
210
337
  const center = diameter / 2;
211
338
  const showLabel = !isLabelHidden;
@@ -213,6 +340,89 @@ export function CircularProgress({
213
340
  const centerContent = children != null ? children : showValueLabel ? valueText : null;
214
341
  const fillVariant = isDisabled ? 'disabled' : variant;
215
342
  const trackVariant = isDisabled ? 'neutral' : variant;
343
+
344
+ // A value change turns the fill's layers with Web Animations on the
345
+ // compositor, from wherever the arc is drawn now (mid-change, the running
346
+ // change's own curve says where) to the new value, on the theme's
347
+ // duration and easing. What renders is always the resting state, so a
348
+ // first render, a render without script, or a skipped animation shows the
349
+ // value.
350
+ const fillRef = useRef(null);
351
+ const rightRef = useRef(null);
352
+ const leftRef = useRef(null);
353
+ const capRef = useRef(null);
354
+ const startRef = useRef(null);
355
+ // Where the arc is drawn: the value at rest, or, when a change is
356
+ // interrupted, the point its own curve has reached. A change's cleanup
357
+ // records it and releases the change's animations (react-component-runtime
358
+ // INV3), so the next change starts from there and an unmount or a switch to
359
+ // indeterminate leaves nothing running. While indeterminate no value is
360
+ // drawn (null): the ring draws its own arc, not the value it holds, so the
361
+ // next value draws at once instead of sweeping from that held value.
362
+ const drawn = useRef(percentage);
363
+ useLayoutEffect(() => {
364
+ const from = drawn.current;
365
+ if (isIndeterminate) {
366
+ drawn.current = null;
367
+ return;
368
+ }
369
+ if (from === null || from === percentage) {
370
+ drawn.current = percentage;
371
+ return;
372
+ }
373
+ const fill = fillRef.current;
374
+ const layers = [rightRef.current, leftRef.current, capRef.current, startRef.current];
375
+ drawn.current = percentage;
376
+ if (fill == null || layers.some(layer => typeof layer?.animate !== 'function')) {
377
+ return;
378
+ }
379
+ const style = getComputedStyle(fill);
380
+ const duration = parseTime(style.transitionDuration);
381
+ if (duration <= 0) {
382
+ return;
383
+ }
384
+ const ease = easingOf(style.transitionTimingFunction);
385
+ const frames = fillKeyframes(from, percentage, duration, ease);
386
+ const timing = {
387
+ duration,
388
+ easing: 'linear',
389
+ fill: 'backwards'
390
+ };
391
+ const [right, left, cap, start] = layers;
392
+ const animations = [right.animate(frames.right, timing), left.animate(frames.left, timing), cap.animate(frames.cap, timing), start.animate(frames.start, timing)];
393
+ return () => {
394
+ const [first] = animations;
395
+ const progress = first.playState !== 'finished' ? Number(first.effect?.getComputedTiming().progress ?? 1) : 1;
396
+ // `progress` is the change's linear time fraction; the arc is drawn on
397
+ // its easing, so the eased position is where it is.
398
+ drawn.current = from + (percentage - from) * ease(progress);
399
+ for (const animation of animations) {
400
+ animation.cancel();
401
+ }
402
+ };
403
+ }, [percentage, isIndeterminate]);
404
+ const turns = fillTurns(percentage);
405
+ // One half of the ring's stroke, from 6 o'clock clockwise to 12 (the left
406
+ // half) before its layer turns; butt ends, the caps are drawn apart.
407
+ const bleedBox = `${-FILL_BLEED} ${-FILL_BLEED} ${diameter + 2 * FILL_BLEED} ${diameter + 2 * FILL_BLEED}`;
408
+ const halfRing = /*#__PURE__*/_jsx("svg", {
409
+ width: diameter + 2 * FILL_BLEED,
410
+ height: diameter + 2 * FILL_BLEED,
411
+ viewBox: bleedBox,
412
+ ...{
413
+ className: "x1lliihq xbh8q5q xy6n8ys"
414
+ },
415
+ children: /*#__PURE__*/_jsx("circle", {
416
+ cx: center,
417
+ cy: center,
418
+ r: radius,
419
+ strokeWidth: strokeWidth,
420
+ strokeDasharray: `${circumference / 2} ${circumference}`,
421
+ transform: `rotate(90 ${center} ${center})`
422
+ })
423
+ });
424
+ // A round cap at 12 o'clock on the ring's centerline (a zero-length stroke).
425
+ const capPath = `M ${center} ${strokeWidth / 2} h 0`;
216
426
  return /*#__PURE__*/_jsxs("div", {
217
427
  ref: ref,
218
428
  ...mergeProps(themeProps('circular-progress', {
@@ -274,16 +484,83 @@ export function CircularProgress({
274
484
  cy: center,
275
485
  r: radius,
276
486
  strokeWidth: strokeWidth
277
- }) : /*#__PURE__*/_jsx("circle", {
278
- ...mergeProps(themeProps('circular-progress-fill', {
279
- variant: fillVariant
280
- }), stylex.props(styles.fill, variantStyles[fillVariant])),
281
- cx: center,
282
- cy: center,
283
- r: radius,
284
- strokeWidth: strokeWidth,
285
- strokeDasharray: circumference,
286
- strokeDashoffset: dashoffset
487
+ }) : null]
488
+ }), isIndeterminate ? null :
489
+ /*#__PURE__*/
490
+ // The determinate fill (see `styles.fill`). Its stroke color is
491
+ // set on the theming target and inherited by every part.
492
+ _jsxs("span", {
493
+ ref: fillRef,
494
+ "aria-hidden": "true",
495
+ ...mergeProps(themeProps('circular-progress-fill', {
496
+ variant: fillVariant
497
+ }), stylex.props(styles.fill, variantStyles[fillVariant])),
498
+ children: [/*#__PURE__*/_jsx("span", {
499
+ ...{
500
+ className: "x10l6tqk x13vifvy x3hqpx7 x5yr21d xb3r6kr x1nrll8i"
501
+ },
502
+ children: /*#__PURE__*/_jsx("span", {
503
+ ref: rightRef,
504
+ ...{
505
+ className: "x10l6tqk x13vifvy x75ztot x5yr21d x1q1rmc8 xb5nubl"
506
+ },
507
+ style: {
508
+ transform: `rotate(${turns.right}deg)`,
509
+ opacity: turns.rightShown
510
+ },
511
+ children: halfRing
512
+ })
513
+ }), /*#__PURE__*/_jsx("span", {
514
+ ...{
515
+ className: "x10l6tqk x13vifvy x3hqpx7 x5yr21d xb3r6kr xu96u03"
516
+ },
517
+ children: /*#__PURE__*/_jsx("span", {
518
+ ref: leftRef,
519
+ ...{
520
+ className: "x10l6tqk x13vifvy x75ztot x5yr21d x1q1rmc8 xu96u03"
521
+ },
522
+ style: {
523
+ transform: `rotate(${turns.left}deg)`,
524
+ opacity: turns.leftShown
525
+ },
526
+ children: halfRing
527
+ })
528
+ }), /*#__PURE__*/_jsx("svg", {
529
+ width: diameter,
530
+ height: diameter,
531
+ viewBox: `0 0 ${diameter} ${diameter}`,
532
+ ...{
533
+ className: "x10l6tqk x10a8y8t x1q1rmc8 x1lliihq xbh8q5q x1owpc8m"
534
+ },
535
+ children: /*#__PURE__*/_jsx("path", {
536
+ ref: startRef,
537
+ d: capPath,
538
+ strokeWidth: strokeWidth,
539
+ style: {
540
+ opacity: turns.caps
541
+ }
542
+ })
543
+ }), /*#__PURE__*/_jsx("span", {
544
+ ref: capRef,
545
+ ...{
546
+ className: "x10l6tqk x10a8y8t x1q1rmc8"
547
+ },
548
+ style: {
549
+ transform: `rotate(${turns.cap}deg)`,
550
+ opacity: turns.caps
551
+ },
552
+ children: /*#__PURE__*/_jsx("svg", {
553
+ width: diameter + 2 * FILL_BLEED,
554
+ height: diameter + 2 * FILL_BLEED,
555
+ viewBox: bleedBox,
556
+ ...{
557
+ className: "x1owpc8m x1lliihq xbh8q5q xy6n8ys"
558
+ },
559
+ children: /*#__PURE__*/_jsx("path", {
560
+ d: capPath,
561
+ strokeWidth: strokeWidth
562
+ })
563
+ })
287
564
  })]
288
565
  }), centerContent != null && /*#__PURE__*/_jsx("div", {
289
566
  ...{
@@ -1 +1 @@
1
- {"version":3,"file":"CommentPin.d.ts","sourceRoot":"","sources":["../../src/CommentPin/CommentPin.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AA4BlD,qEAAqE;AACrE,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEhD,sEAAsE;AACtE,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,EAAE,EAAE,MAAM,CAAC;IACX,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,SAAS,CAAC,iBAAiB,CAAC,EAC5B,UAAU,CACX;IACC,0CAA0C;IAC1C,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;OAOG;IACH,YAAY,EAAE,SAAS,CAAC,qBAAqB,EAAE,GAAG,qBAAqB,EAAE,CAAC,CAAC;IAC3E;;;;OAIG;IACH,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,yCAAyC;IACzC,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AA8ND;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,YAAY,EACZ,IAAW,EACX,WAAe,EACf,WAAe,EACf,UAAkB,EAClB,QAAgB,EAChB,UAAkB,EAClB,UAAkB,EAClB,GAAG,EACH,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,IAAI,EACR,EAAE,eAAe,GAAG,KAAK,CAAC,SAAS,CA4JnC;yBA3Ke,UAAU"}
1
+ {"version":3,"file":"CommentPin.d.ts","sourceRoot":"","sources":["../../src/CommentPin/CommentPin.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AA4BlD,qEAAqE;AACrE,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEhD,sEAAsE;AACtE,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,EAAE,EAAE,MAAM,CAAC;IACX,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,SAAS,CAAC,iBAAiB,CAAC,EAC5B,UAAU,CACX;IACC,0CAA0C;IAC1C,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;OAOG;IACH,YAAY,EAAE,SAAS,CAAC,qBAAqB,EAAE,GAAG,qBAAqB,EAAE,CAAC,CAAC;IAC3E;;;;OAIG;IACH,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,yCAAyC;IACzC,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AA6OD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,YAAY,EACZ,IAAW,EACX,WAAe,EACf,WAAe,EACf,UAAkB,EAClB,QAAgB,EAChB,UAAkB,EAClB,UAAkB,EAClB,GAAG,EACH,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,IAAI,EACR,EAAE,eAAe,GAAG,KAAK,CAAC,SAAS,CAwLnC;yBAvMe,UAAU"}
@@ -88,6 +88,11 @@ const styles = {
88
88
  kSiTet: "xyd83as x1tbh5bv",
89
89
  $$css: true
90
90
  },
91
+ disabledUnselected: {
92
+ kVAM5u: "x9r1u3d xmnqt3w",
93
+ kMwMTN: "xv1l7n4 x1bhcspo",
94
+ $$css: true
95
+ },
91
96
  overflowCircle: {
92
97
  kg3NbH: "xnjsko4",
93
98
  $$css: true
@@ -269,8 +274,37 @@ export function CommentPin({
269
274
  ref: mergedRef,
270
275
  type: "button",
271
276
  "aria-label": label,
272
- "aria-pressed": isSelected,
273
- disabled: isDisabled,
277
+ "aria-pressed": isSelected
278
+ // A disabled pin keeps its tab stop and is announced as disabled through
279
+ // aria-disabled instead of the native attribute (spec:AST-085 FR1). The
280
+ // activation guards below keep it inert (FR2); focus is kept when the
281
+ // pin becomes disabled while focused (FR4), and the shared focus outline
282
+ // still paints, dimmed with the rest of the pin (FR6).
283
+ ,
284
+ "aria-disabled": isDisabled ? 'true' : undefined,
285
+ onClick: e => {
286
+ if (isDisabled) {
287
+ // A natively disabled element dispatched no click at all, so an
288
+ // aria-disabled pin must not reach the caller's handler, and the
289
+ // click must not leak to a clickable ancestor (annotation surface)
290
+ // the native attribute never let it reach either (FR2).
291
+ e.preventDefault();
292
+ e.stopPropagation();
293
+ return;
294
+ }
295
+ rest.onClick?.(e);
296
+ }
297
+ // The keyboard paths to activation are Enter and Space; other keys still
298
+ // reach caller handlers, as on an enabled pin (FR2).
299
+ ,
300
+ onKeyDown: e => {
301
+ if (isDisabled && (e.key === 'Enter' || e.key === ' ')) {
302
+ e.preventDefault();
303
+ e.stopPropagation();
304
+ return;
305
+ }
306
+ rest.onKeyDown?.(e);
307
+ },
274
308
  ...mergeProps(themeProps('comment-pin', {
275
309
  size: resolvedSize,
276
310
  grouped: isGrouped ? 'grouped' : null,
@@ -279,7 +313,7 @@ export function CommentPin({
279
313
  dimmed: isDimmed ? 'dimmed' : null,
280
314
  resolved: isResolved ? 'resolved' : null,
281
315
  disabled: isDisabled ? 'disabled' : null
282
- }), focusOutlineProps.focusVisible(styles.root, sizeStyles[resolvedSize], interactionOverlayStyles.backgroundImage, (isGrouped || hasUnread) && styles.withCount, isDimmed && !isSelected && styles.dimmed, isSelected && styles.selected, isDisabled && styles.disabled, xstyle), className, style),
316
+ }), focusOutlineProps.focusVisible(styles.root, sizeStyles[resolvedSize], interactionOverlayStyles.backgroundImage, (isGrouped || hasUnread) && styles.withCount, isDimmed && !isSelected && styles.dimmed, isSelected && styles.selected, isDisabled && styles.disabled, isDisabled && !isSelected && styles.disabledUnselected, xstyle), className, style),
283
317
  children: [/*#__PURE__*/_jsx("span", {
284
318
  "aria-hidden": "true",
285
319
  ...{
package/dist/lab.css CHANGED
@@ -63,6 +63,7 @@
63
63
  .xln7xf2:not(#\#){font:inherit}
64
64
  .x10a8y8t:not(#\#){inset:0}
65
65
  .xkdpibf:not(#\#){margin:-1px}
66
+ .xy6n8ys:not(#\#){margin:-2px}
66
67
  .x1ghz6dp:not(#\#){margin:0}
67
68
  .x10h29id:not(#\#){margin:var(--spacing-0-5)}
68
69
  .x1717udv:not(#\#){padding:0}
@@ -562,8 +563,8 @@
562
563
  .x15406qy:not(#\#):not(#\#):not(#\#){transition-property:background-color}
563
564
  .xq90yva:not(#\#):not(#\#):not(#\#){transition-property:border-color,background-color}
564
565
  .xt3l3uh:not(#\#):not(#\#):not(#\#){transition-property:color}
566
+ .x13b0p5u:not(#\#):not(#\#):not(#\#){transition-property:none}
565
567
  .x19991ni:not(#\#):not(#\#):not(#\#){transition-property:opacity}
566
- .xxnu56j:not(#\#):not(#\#):not(#\#){transition-property:stroke-dashoffset}
567
568
  .x1gfj591:not(#\#):not(#\#):not(#\#){transition-property:top,left,width,height}
568
569
  .x11xpdln:not(#\#):not(#\#):not(#\#){transition-property:transform}
569
570
  .x9lcvmn:not(#\#):not(#\#):not(#\#){transition-timing-function:ease-out}
@@ -578,6 +579,7 @@
578
579
  .xuxw1ft:not(#\#):not(#\#):not(#\#){white-space:nowrap}
579
580
  .x126k92a:not(#\#):not(#\#):not(#\#){white-space:pre-wrap}
580
581
  .x1sdyfia:not(#\#):not(#\#):not(#\#){white-space:pre}
582
+ .x1q1rmc8:not(#\#):not(#\#):not(#\#){will-change:transform,opacity}
581
583
  .x1so62im:not(#\#):not(#\#):not(#\#){will-change:transform}
582
584
  .x1lldw8n:not(#\#):not(#\#):not(#\#){word-break:normal}
583
585
  .x8knxv4:not(#\#):not(#\#):not(#\#){z-index:-1}
@@ -676,6 +678,9 @@
676
678
  .x1ueg155:not(#\#):not(#\#):not(#\#):not(#\#){height:var(--size-element-md)}
677
679
  .x6k0iem:not(#\#):not(#\#):not(#\#):not(#\#){height:var(--size-element-sm)}
678
680
  .x16ye13r:not(#\#):not(#\#):not(#\#):not(#\#){height:var(--x-height)}
681
+ .xb5nubl:not(#\#):not(#\#):not(#\#):not(#\#){left:-100%}
682
+ .xu96u03:not(#\#):not(#\#):not(#\#):not(#\#){left:0}
683
+ .x1nrll8i:not(#\#):not(#\#):not(#\#):not(#\#){left:50%}
679
684
  .x1unci72:not(#\#):not(#\#):not(#\#):not(#\#){left:var(--spacing-4)}
680
685
  .xlstkdb:not(#\#):not(#\#):not(#\#):not(#\#){margin-bottom:var(--spacing-0-5)}
681
686
  .x1p37lm5:not(#\#):not(#\#):not(#\#):not(#\#){margin-bottom:var(--spacing-2)}
@@ -746,10 +751,12 @@
746
751
  .x6jxa94:not(#\#):not(#\#):not(#\#):not(#\#){width:14px}
747
752
  .x1kky2od:not(#\#):not(#\#):not(#\#):not(#\#){width:16px}
748
753
  .x1i1rx1s:not(#\#):not(#\#):not(#\#):not(#\#){width:1px}
754
+ .x75ztot:not(#\#):not(#\#):not(#\#):not(#\#){width:200%}
749
755
  .xvy4d1p:not(#\#):not(#\#):not(#\#):not(#\#){width:24px}
750
756
  .x1dz1jew:not(#\#):not(#\#):not(#\#):not(#\#){width:280px}
751
757
  .x1849jeq:not(#\#):not(#\#):not(#\#):not(#\#){width:30px}
752
758
  .x1td3qas:not(#\#):not(#\#):not(#\#):not(#\#){width:32px}
759
+ .x3hqpx7:not(#\#):not(#\#):not(#\#):not(#\#){width:50%}
753
760
  .x1ftt334:not(#\#):not(#\#):not(#\#):not(#\#){width:5px}
754
761
  .x1smflpy:not(#\#):not(#\#):not(#\#):not(#\#){width:calc(100% - 2 * var(--spacing-0-5) - var(--border-width))}
755
762
  .xurgr9q:not(#\#):not(#\#):not(#\#):not(#\#){width:calc(100% + (var(--spacing-3) * 2))}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/lab",
3
- "version": "0.7.5-canary.fd0aba8",
3
+ "version": "0.7.6-canary.0ef60dd",
4
4
  "description": "Experimental Astryx components — published to npm only under the @canary dist-tag for early testing; never released as a stable (latest) version.",
5
5
  "author": "Meta Open Source",
6
6
  "license": "MIT",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "peerDependencies": {
51
51
  "@stylexjs/stylex": ">=0.10.0",
52
- "@astryxdesign/core": "0.7.5-canary.fd0aba8",
52
+ "@astryxdesign/core": "0.7.6-canary.0ef60dd",
53
53
  "react": ">=19.0.0",
54
54
  "react-dom": ">=19.0.0"
55
55
  },
@@ -59,8 +59,8 @@
59
59
  "d3-shape": "^3.2.0"
60
60
  },
61
61
  "devDependencies": {
62
- "@astryxdesign/a11y-spec": "0.7.5-canary.fd0aba8",
63
- "@astryxdesign/cli": "0.7.5-canary.fd0aba8",
62
+ "@astryxdesign/a11y-spec": "0.7.6-canary.0ef60dd",
63
+ "@astryxdesign/cli": "0.7.6-canary.0ef60dd",
64
64
  "@babel/cli": "^7.29.7",
65
65
  "@babel/core": "^7.29.7",
66
66
  "@babel/preset-react": "^7.29.7",
@@ -140,17 +140,29 @@ async function runState(
140
140
  });
141
141
  }
142
142
 
143
- test('the exact known failure matches one executed ChatTypingIndicator result', async ({
144
- page,
145
- }) => {
146
- const cdp = await page.context().newCDPSession(page);
143
+ // Checking coverage runs every state again, which on a loaded runner nears
144
+ // the per-test timeout as one test. It runs as one serial part per state,
145
+ // sharing their results, then one check over all of them.
146
+ test.describe('the exact known failure matches one executed ChatTypingIndicator result', () => {
147
+ test.describe.configure({mode: 'serial'});
147
148
  const results: BindingResult[] = [];
149
+
148
150
  for (const state of CHAT_TYPING_STATUS_BINDING_STATES) {
149
- results.push(await runState(page, cdp, state));
151
+ test(`runs ${state.id}`, async ({page}) => {
152
+ const cdp = await page.context().newCDPSession(page);
153
+ results.push(await runState(page, cdp, state));
154
+ });
150
155
  }
151
- expect(
152
- unmatchedKnownFailures(CHAT_TYPING_STATUS_KNOWN_FAILURES, results),
153
- ).toEqual([]);
156
+
157
+ test('the exact known failure matches one executed result', () => {
158
+ // Every part ran in this worker; this test alone proves nothing.
159
+ expect(results.map(result => result.state).sort()).toEqual(
160
+ CHAT_TYPING_STATUS_BINDING_STATES.map(state => state.id).sort(),
161
+ );
162
+ expect(
163
+ unmatchedKnownFailures(CHAT_TYPING_STATUS_KNOWN_FAILURES, results),
164
+ ).toEqual([]);
165
+ });
154
166
  });
155
167
 
156
168
  for (const state of CHAT_TYPING_STATUS_BINDING_STATES) {