@astryxdesign/core 0.5.0-canary.cac77bc → 0.5.0-canary.cb20a01

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,6 @@
1
1
  /**
2
2
  * @file AlertDialog.tsx
3
- * @input Uses React, Dialog, Layout, Heading, Text, Button
3
+ * @input Uses React, StyleX, Dialog, Layout, Heading, Text, Button
4
4
  * @output Exports AlertDialog component, AlertDialogProps type
5
5
  * @position Core implementation; consumed by index.ts, tested by AlertDialog.test.tsx
6
6
  *
@@ -1 +1 @@
1
- {"version":3,"file":"AlertDialog.d.ts","sourceRoot":"","sources":["../../src/AlertDialog/AlertDialog.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAA2B,MAAM,OAAO,CAAC;AAQhD,OAAO,EAAS,KAAK,aAAa,EAAC,MAAM,WAAW,CAAC;AACrD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAK5C,MAAM,WAAW,gBAAiB,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IACpE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IAE3C;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAE9B;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;OAGG;IACH,QAAQ,EAAE,MAAM,OAAO,CAAC;IAExB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,GAAG,EACH,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,KAAK,EACL,WAAW,EACX,WAAW,EAAE,oBAAoB,EACjC,WAAW,EACX,aAA6B,EAC7B,eAAe,EACf,QAAQ,EACR,KAAW,EACX,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,IAAI,EACR,EAAE,gBAAgB,qBAkElB;yBApFe,WAAW"}
1
+ {"version":3,"file":"AlertDialog.d.ts","sourceRoot":"","sources":["../../src/AlertDialog/AlertDialog.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAA2B,MAAM,OAAO,CAAC;AAShD,OAAO,EAAS,KAAK,aAAa,EAAC,MAAM,WAAW,CAAC;AACrD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAkB5C,MAAM,WAAW,gBAAiB,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IACpE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IAE3C;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAE9B;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;OAGG;IACH,QAAQ,EAAE,MAAM,OAAO,CAAC;IAExB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,GAAG,EACH,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,KAAK,EACL,WAAW,EACX,WAAW,EAAE,oBAAoB,EACjC,WAAW,EACX,aAA6B,EAC7B,eAAe,EACf,QAAQ,EACR,KAAW,EACX,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,IAAI,EACR,EAAE,gBAAgB,qBA8FlB;yBAhHe,WAAW"}
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file AlertDialog.tsx
7
- * @input Uses React, Dialog, Layout, Heading, Text, Button
7
+ * @input Uses React, StyleX, Dialog, Layout, Heading, Text, Button
8
8
  * @output Exports AlertDialog component, AlertDialogProps type
9
9
  * @position Core implementation; consumed by index.ts, tested by AlertDialog.test.tsx
10
10
  *
@@ -16,18 +16,32 @@
16
16
  * - /packages/cli/assets/templates/blocks/components/AlertDialog/ (showcase blocks)
17
17
  */
18
18
  import React, { useId, useCallback } from 'react';
19
+ import * as stylex from '@stylexjs/stylex';
19
20
  import { Dialog } from "../Dialog/index.js";
20
21
  import { Layout } from "../Layout/Layout.js";
21
22
  import { LayoutContent } from "../Layout/LayoutContent.js";
22
23
  import { LayoutFooter } from "../Layout/LayoutFooter.js";
23
- import { HStack } from "../Stack/index.js";
24
+ import { Stack } from "../Stack/index.js";
24
25
  import { Heading } from "../Heading/Heading.js";
25
26
  import { Text } from "../Text/Text.js";
26
27
  import { Button } from "../Button/index.js";
27
28
  import { mergeProps } from "../utils/index.js";
28
29
  import { themeProps } from "../utils/themeProps.js";
29
30
  import { useTranslator } from "../i18n/index.js";
31
+ import { useMediaQuery } from "../hooks/index.js";
30
32
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
33
+ const SMALL_SCREEN_QUERY = '(max-width: 640px)';
34
+ const styles = {
35
+ action: {
36
+ ks0D6T: "x193iq5w",
37
+ k7Eaqz: "xeuugli",
38
+ $$css: true
39
+ },
40
+ stackedAction: {
41
+ kzqmXN: "xh8yej3",
42
+ $$css: true
43
+ }
44
+ };
31
45
  /**
32
46
  * A confirmation dialog for destructive or irreversible actions.
33
47
  *
@@ -77,9 +91,33 @@ export function AlertDialog({
77
91
  const cancelLabel = cancelLabelFromProps ?? t('@astryx.alertDialog.cancel');
78
92
  const titleId = useId();
79
93
  const descriptionId = useId();
94
+ // Width stays delegated to Dialog: this query exists only so AlertDialog can
95
+ // keep narrow visual, DOM, and tab order aligned for its destructive/cancel
96
+ // semantics without coupling action order to pointer or hover capability.
97
+ const isSmallScreen = useMediaQuery(SMALL_SCREEN_QUERY);
80
98
  const handleCancel = useCallback(() => {
81
99
  onOpenChange(false);
82
100
  }, [onOpenChange]);
101
+ const buttonActionStyle = [styles.action, isSmallScreen && styles.stackedAction];
102
+ const cancelButton = /*#__PURE__*/_jsx(Button, {
103
+ variant: "ghost",
104
+ label: cancelLabel,
105
+ onClick: handleCancel,
106
+ xstyle: buttonActionStyle
107
+ // Dialog focuses [data-autofocus] itself after showModal(), because
108
+ // React's autoFocus runs during commit while the dialog is invisible.
109
+ // Cancel is least destructive, so it remains the autofocus target even
110
+ // when small-screen visual order places the destructive action above it.
111
+ ,
112
+ "data-autofocus": true
113
+ }, "cancel");
114
+ const actionButton = /*#__PURE__*/_jsx(Button, {
115
+ variant: actionVariant,
116
+ label: actionLabel,
117
+ onClick: onAction,
118
+ isLoading: isActionLoading,
119
+ xstyle: buttonActionStyle
120
+ }, "action");
83
121
  return /*#__PURE__*/_jsx(Dialog, {
84
122
  ...rest,
85
123
  ref: ref,
@@ -117,25 +155,12 @@ export function AlertDialog({
117
155
  })]
118
156
  }),
119
157
  footer: /*#__PURE__*/_jsx(LayoutFooter, {
120
- children: /*#__PURE__*/_jsxs(HStack, {
158
+ children: /*#__PURE__*/_jsx(Stack, {
159
+ direction: isSmallScreen ? 'vertical' : 'horizontal',
121
160
  gap: 2,
122
- hAlign: "end",
123
- children: [/*#__PURE__*/_jsx(Button, {
124
- variant: "ghost",
125
- label: cancelLabel,
126
- onClick: handleCancel
127
- // Dialog focuses [data-autofocus] itself after showModal(),
128
- // because React's autoFocus runs during commit while the
129
- // dialog is still invisible. Cancel is the least destructive
130
- // choice, so it is the one that should be preselected.
131
- ,
132
- "data-autofocus": true
133
- }), /*#__PURE__*/_jsx(Button, {
134
- variant: actionVariant,
135
- label: actionLabel,
136
- onClick: onAction,
137
- isLoading: isActionLoading
138
- })]
161
+ hAlign: isSmallScreen ? 'stretch' : 'end',
162
+ wrap: isSmallScreen ? 'nowrap' : 'wrap',
163
+ children: isSmallScreen ? [actionButton, cancelButton] : [cancelButton, actionButton]
139
164
  })
140
165
  })
141
166
  })
@@ -1 +1 @@
1
- {"version":3,"file":"TouchDateField.d.ts","sourceRoot":"","sources":["../../src/DateInput/TouchDateField.tsx"],"names":[],"mappings":"AA4DA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,aAAa,CAAC;AAgchD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAkB,EAClB,UAAkB,EAClB,UAAkB,EAClB,eAAe,EACf,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,SAAiB,EACjB,GAAG,EACH,GAAG,EACH,eAAe,EACf,WAAW,EAAE,oBAAoB,EACjC,IAAI,EAAE,QAAQ,EACd,MAAM,EACN,aAA0B,EAC1B,YAAY,EACZ,QAAgB,EAIhB,cAAc,EAAE,eAAe,EAC/B,YAAY,EAAE,gBAAoB,EAClC,MAAoB,EACpB,KAAK,EACL,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,IAAI,EACR,EAAE,cAAc,+BAqsBhB;yBAtuBe,cAAc"}
1
+ {"version":3,"file":"TouchDateField.d.ts","sourceRoot":"","sources":["../../src/DateInput/TouchDateField.tsx"],"names":[],"mappings":"AAgEA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,aAAa,CAAC;AAoehD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAkB,EAClB,UAAkB,EAClB,UAAkB,EAClB,eAAe,EACf,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,SAAiB,EACjB,GAAG,EACH,GAAG,EACH,eAAe,EACf,WAAW,EAAE,oBAAoB,EACjC,IAAI,EAAE,QAAQ,EACd,MAAM,EACN,aAA0B,EAC1B,YAAY,EACZ,QAAgB,EAIhB,cAAc,EAAE,eAAe,EAC/B,YAAY,EAAE,gBAAoB,EAClC,MAAoB,EACpB,KAAK,EACL,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,IAAI,EACR,EAAE,cAAc,+BAmtBhB;yBApvBe,cAAc"}
@@ -38,6 +38,10 @@
38
38
  * 3. The title is the escape hatch. Tap it and the same box becomes a month
39
39
  * wheel and a year wheel — a flick each to reach 2019 instead of forty.
40
40
  *
41
+ * Reset is chrome, so it sits in the header beside the arrows rather than in
42
+ * the footer: the footer is where the task ends, and an undo of equal weight
43
+ * beside Save is a mis-tap that throws away the date just chosen.
44
+ *
41
45
  * SYNC: When modified, update these files to stay in sync:
42
46
  * - /packages/core/src/DateInput/DateInput.tsx
43
47
  * - /packages/core/src/DateInput/DateInput.doc.mjs
@@ -152,6 +156,10 @@ const styles = {
152
156
  k1xSpc: "x3nfvp2",
153
157
  $$css: true
154
158
  },
159
+ resetButton: {
160
+ kAzted: "xygd9yz",
161
+ $$css: true
162
+ },
155
163
  title: {
156
164
  k1xSpc: "x78zum5",
157
165
  kGNEyG: "x6s0dn4",
@@ -168,10 +176,13 @@ const styles = {
168
176
  k63SB2: "x2mo6ok",
169
177
  kkrTdU: "x1ypdohk x16khyan",
170
178
  khDVqt: "xuxw1ft",
179
+ k7Eaqz: "xeuugli",
180
+ kVQacm: "xb3r6kr",
171
181
  $$css: true
172
182
  },
173
183
  titleChevron: {
174
184
  k1xSpc: "x3nfvp2",
185
+ kmuXW: "x2lah0s",
175
186
  k1ekBW: "x11xpdln",
176
187
  kIyJzY: "xuedmi6",
177
188
  kAMwcw: "xlr8y92",
@@ -547,6 +558,9 @@ export function TouchDateField({
547
558
  "data-title": "month-year",
548
559
  ...stylex.props(styles.title, focusOutlineStyles.focusVisible),
549
560
  children: [/*#__PURE__*/_jsx("span", {
561
+ ...{
562
+ className: "xeuugli xb3r6kr xlyipyv"
563
+ },
550
564
  children: monthYearLabel
551
565
  }), /*#__PURE__*/_jsx(Icon, {
552
566
  icon: "chevronDown",
@@ -603,6 +617,27 @@ export function TouchDateField({
603
617
  })
604
618
  })
605
619
  })]
620
+ }), /*#__PURE__*/_jsx("span", {
621
+ "data-action": "reset",
622
+ inert: isWheelOpen ? true : undefined,
623
+ ...{
624
+ 0: {
625
+ className: "x78zum5 x6s0dn4 x1jl3cmp xuedmi6 xcj1dhv xzg1mie"
626
+ },
627
+ 1: {
628
+ className: "x78zum5 x6s0dn4 x1jl3cmp xuedmi6 xcj1dhv xzg1mie xlshs6z xg01cxk x47corl"
629
+ }
630
+ }[!!isWheelOpen << 0],
631
+ children: /*#__PURE__*/_jsx(Button
632
+ // ghost: a filled button up here would outrank the Save that
633
+ // finishes the task.
634
+ , {
635
+ variant: "ghost",
636
+ size: "sm",
637
+ xstyle: styles.resetButton,
638
+ label: t('@astryx.dateInput.resetPicking'),
639
+ onClick: handleResetInSheet
640
+ })
606
641
  })]
607
642
  }), /*#__PURE__*/_jsx("div", {
608
643
  "aria-hidden": "true",
@@ -677,23 +712,17 @@ export function TouchDateField({
677
712
  ...{
678
713
  className: "x1xye8es xrvj5dj x1y6fwsi"
679
714
  },
680
- children: [/*#__PURE__*/_jsxs("div", {
715
+ children: [/*#__PURE__*/_jsx("div", {
681
716
  inert: isWheelOpen ? true : undefined,
682
717
  ...{
683
718
  0: {
684
- className: "x15r89dc x78zum5 x1txdalj x74b7sa xuedmi6 xzg1mie"
719
+ className: "x15r89dc x78zum5 x74b7sa xuedmi6 xzg1mie"
685
720
  },
686
721
  1: {
687
- className: "x15r89dc x78zum5 x1txdalj x74b7sa xuedmi6 xzg1mie xlshs6z x47corl"
722
+ className: "x15r89dc x78zum5 x74b7sa xuedmi6 xzg1mie xlshs6z x47corl"
688
723
  }
689
724
  }[!!isWheelOpen << 0],
690
- children: [/*#__PURE__*/_jsx(Button, {
691
- variant: "secondary",
692
- size: "md",
693
- width: "100%",
694
- label: t('@astryx.dateInput.resetPicking'),
695
- onClick: handleResetInSheet
696
- }), /*#__PURE__*/_jsx(Button, {
725
+ children: /*#__PURE__*/_jsx(Button, {
697
726
  variant: "primary"
698
727
  // md, not sm: it is the action a thumb reaches for, so it gets
699
728
  // the comfortable size rather than the compact one the header's
@@ -703,15 +732,15 @@ export function TouchDateField({
703
732
  width: "100%",
704
733
  label: t('@astryx.dateInput.savePicking'),
705
734
  onClick: () => setIsSheetOpen(false)
706
- })]
735
+ })
707
736
  }), /*#__PURE__*/_jsx("div", {
708
737
  inert: isWheelOpen ? undefined : true,
709
738
  ...{
710
739
  0: {
711
- className: "x15r89dc x78zum5 x1txdalj x10xzikg xc8icb0 x1jl3cmp xuedmi6 xcj1dhv xzg1mie"
740
+ className: "x15r89dc x78zum5 x10xzikg xc8icb0 x1jl3cmp xuedmi6 xcj1dhv xzg1mie"
712
741
  },
713
742
  1: {
714
- className: "x15r89dc x78zum5 x1txdalj x10xzikg xc8icb0 x1jl3cmp xuedmi6 xcj1dhv xzg1mie xlshs6z xg01cxk x47corl"
743
+ className: "x15r89dc x78zum5 x10xzikg xc8icb0 x1jl3cmp xuedmi6 xcj1dhv xzg1mie xlshs6z xg01cxk x47corl"
715
744
  }
716
745
  }[!!!isWheelOpen << 0],
717
746
  children: /*#__PURE__*/_jsx(Button
@@ -1 +1 @@
1
- {"version":3,"file":"HoverCard.d.ts","sourceRoot":"","sources":["../../src/HoverCard/HoverCard.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAsB,KAAK,YAAY,EAAE,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAG7E,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC3B,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAC,cAAc,EAAE,cAAc,EAAC,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAG5C,YAAY,EACV,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AAiBxB,MAAM,WAAW,cAAe,SAAQ,IAAI,CAC1C,SAAS,EACT,QAAQ,GAAG,WAAW,GAAG,OAAO,CACjC;IACC;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;OAEG;IACH,OAAO,EAAE,SAAS,CAAC;IAEnB;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAEzC;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAEtC;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAiBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,EACxB,QAAQ,EACR,OAAO,EACP,SAAmB,EACnB,SAAoB,EACpB,KAAW,EACX,SAAe,EACf,YAAqB,EACrB,YAAqB,EACrB,SAAgB,EAChB,KAAK,EACL,YAAY,EACZ,kBAA2B,EAC3B,MAAM,EACN,aAAa,EACb,MAAM,EACN,SAAS,EACT,KAAK,GACN,EAAE,cAAc,GAAG,YAAY,CAwK/B;yBA1Le,SAAS"}
1
+ {"version":3,"file":"HoverCard.d.ts","sourceRoot":"","sources":["../../src/HoverCard/HoverCard.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAsB,KAAK,YAAY,EAAE,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAG7E,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC3B,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAC,cAAc,EAAE,cAAc,EAAC,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAG5C,YAAY,EACV,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AA6ExB,MAAM,WAAW,cAAe,SAAQ,IAAI,CAC1C,SAAS,EACT,QAAQ,GAAG,WAAW,GAAG,OAAO,CACjC;IACC;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;OAEG;IACH,OAAO,EAAE,SAAS,CAAC;IAEnB;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAEzC;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAEtC;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAiBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,EACxB,QAAQ,EACR,OAAO,EACP,SAAmB,EACnB,SAAoB,EACpB,KAAW,EACX,SAAe,EACf,YAAqB,EACrB,YAAqB,EACrB,SAAgB,EAChB,KAAK,EACL,YAAY,EACZ,kBAA2B,EAC3B,MAAM,EACN,aAAa,EACb,MAAM,EACN,SAAS,EACT,KAAK,GACN,EAAE,cAAc,GAAG,YAAY,CAoL/B;yBAtMe,SAAS"}
@@ -21,6 +21,45 @@ import { useHoverCard } from "./useHoverCard.js";
21
21
  import "../theme/tokens.stylex.js";
22
22
  import { colorVars, spacingVars } from "../theme/tokens.stylex.js";
23
23
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
24
+ // `aria-haspopup` and `aria-controls` are global, so any trigger may carry
25
+ // them. `aria-expanded` is not. This is the ARIA 1.2 "Supported States and
26
+ // Properties" list for it — deliberately the narrow reading: axe accepts the
27
+ // attribute on rather more roles, since it also allows every subclass of these,
28
+ // so a role outside this set is not necessarily a violation. Widening it is
29
+ // safe; the set is a floor, not the spec's ceiling. The same rule is applied in
30
+ // Chat/useTriggerMenu.tsx, which only emits the combobox attributes once the
31
+ // element is actually a combobox.
32
+ const EXPANDABLE_ROLES = new Set(['application', 'button', 'checkbox', 'columnheader', 'combobox', 'gridcell', 'link', 'listbox', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'row', 'rowheader', 'switch', 'tab', 'treeitem']);
33
+ const BUTTON_INPUT_TYPES = new Set(['button', 'submit', 'reset', 'image']);
34
+
35
+ // Deliberately partial, and unlisted elements are read as role-less: dropping
36
+ // aria-expanded where it might have been legal costs an AT user a state they
37
+ // can still infer, whereas emitting it where it is illegal is a critical
38
+ // aria-allowed-attr defect.
39
+ function supportsAriaExpanded(el) {
40
+ const explicit = el.getAttribute('role')?.trim().split(/\s+/)[0];
41
+ if (explicit) {
42
+ return EXPANDABLE_ROLES.has(explicit);
43
+ }
44
+ switch (el.tagName) {
45
+ case 'BUTTON':
46
+ case 'SUMMARY':
47
+ return true;
48
+ case 'A':
49
+ case 'AREA':
50
+ return el.hasAttribute('href');
51
+ case 'INPUT':
52
+ // The button-flavoured input types map to role="button"; every other
53
+ // type maps to a textbox-family role, which does not take it.
54
+ return BUTTON_INPUT_TYPES.has(el.type?.toLowerCase());
55
+ case 'SELECT':
56
+ // combobox when it is a single-line picker, listbox otherwise; both
57
+ // support aria-expanded.
58
+ return true;
59
+ default:
60
+ return false;
61
+ }
62
+ }
24
63
  /**
25
64
  * Check if children are text-only (no React elements)
26
65
  */
@@ -121,12 +160,17 @@ export function HoverCard({
121
160
  // trigger with the dialog's content (aria-describedby is for plain-text
122
161
  // descriptions, not navigable regions). See #5049.
123
162
  //
163
+ // aria-expanded only goes on a trigger whose role permits it. A role-less
164
+ // trigger (Timestamp's <time>/<span>) gets haspopup + controls, which are
165
+ // global, and no expanded state.
166
+ //
124
167
  // Merge rather than overwrite: the trigger may already carry its own
125
168
  // popup semantics (e.g. a menu button wrapped in a labelled HoverCard),
126
169
  // so preserve the existing values and restore them on cleanup.
127
170
  const existingHaspopup = firstChild.getAttribute('aria-haspopup');
128
171
  const existingControls = firstChild.getAttribute('aria-controls');
129
172
  const existingExpanded = firstChild.getAttribute('aria-expanded');
173
+ const canExpand = supportsAriaExpanded(firstChild);
130
174
  firstChild.setAttribute('aria-haspopup', 'dialog');
131
175
  // Only point aria-controls at the layer while it is open and in the DOM.
132
176
  // While closed, useLayer leaves only an inert <template> marker, so the
@@ -139,7 +183,13 @@ export function HoverCard({
139
183
  } else {
140
184
  firstChild.removeAttribute('aria-controls');
141
185
  }
142
- firstChild.setAttribute('aria-expanded', String(hoverCard.isOpen));
186
+ if (canExpand) {
187
+ firstChild.setAttribute('aria-expanded', String(hoverCard.isOpen));
188
+ } else if (existingExpanded) {
189
+ firstChild.setAttribute('aria-expanded', existingExpanded);
190
+ } else {
191
+ firstChild.removeAttribute('aria-expanded');
192
+ }
143
193
  return () => {
144
194
  hoverCard.ref(null);
145
195
  if (existingHaspopup) {
@@ -192,8 +242,10 @@ export function HoverCard({
192
242
  ref: hoverCard.ref,
193
243
  tabIndex: 0,
194
244
  "aria-haspopup": label ? 'dialog' : undefined,
195
- "aria-controls": label && hoverCard.isOpen ? hoverCard.id : undefined,
196
- "aria-expanded": label ? hoverCard.isOpen : undefined,
245
+ "aria-controls": label && hoverCard.isOpen ? hoverCard.id : undefined
246
+ // No aria-expanded: this wrapper is a role-less <span>, and
247
+ // aria-expanded is invalid on it (see EXPANDABLE_ROLES above).
248
+ ,
197
249
  "aria-describedby": label ? undefined : hoverCard.describedBy,
198
250
  ...{
199
251
  0: {
@@ -134,7 +134,10 @@ export interface HoverCardReturn {
134
134
  describedBy: string;
135
135
  /**
136
136
  * Whether the hover card is currently open.
137
- * Useful for driving `aria-expanded` on the trigger.
137
+ * Useful for driving `aria-expanded` on the trigger — but only when the
138
+ * trigger's role permits it (`button`, `link`, `combobox`, …). On a role-less
139
+ * trigger `aria-expanded` is invalid; use `aria-haspopup`/`aria-controls`
140
+ * alone there.
138
141
  */
139
142
  isOpen: boolean;
140
143
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"useHoverCard.d.ts","sourceRoot":"","sources":["../../src/HoverCard/useHoverCard.tsx"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,OAAO,CAAC;AAEf,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AA6BlC;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEhE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,iBAAiB,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,GAAG,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAE9B;;;OAGG;IACH,WAAW,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEtC;;;;OAIG;IACH,cAAc,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEzC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;;;OAOG;IACH,eAAe,EAAE,CACf,QAAQ,EAAE,SAAS,EACnB,KAAK,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAC,KAC5C,SAAS,CAAC;IAEf;;OAEG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;IAEjB;;OAEG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAyBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,gBAAqB,GAAG,eAAe,CAib5E"}
1
+ {"version":3,"file":"useHoverCard.d.ts","sourceRoot":"","sources":["../../src/HoverCard/useHoverCard.tsx"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,OAAO,CAAC;AAEf,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AA6BlC;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEhE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,iBAAiB,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,GAAG,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAE9B;;;OAGG;IACH,WAAW,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEtC;;;;OAIG;IACH,cAAc,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEzC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;;OAMG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;;;OAOG;IACH,eAAe,EAAE,CACf,QAAQ,EAAE,SAAS,EACnB,KAAK,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAC,KAC5C,SAAS,CAAC;IAEf;;OAEG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;IAEjB;;OAEG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAyBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,gBAAqB,GAAG,eAAe,CAib5E"}
package/locales/en.json CHANGED
@@ -725,7 +725,7 @@
725
725
  },
726
726
  "@astryx.dateInput.resetPicking": {
727
727
  "defaultMessage": "Reset",
728
- "description": "Secondary button in the mobile date picker's footer, beside Save. Removes the chosen date AND returns the calendar to the current month — it undoes the picking done so far and puts the picker back to how it opens, so prefer a word covering both, not one that only means emptying a field. Not cancel: the picker stays open afterwards and nothing is dismissed."
728
+ "description": "Ghost button in the mobile date picker's header, at the trailing corner beside the month arrows. Removes the chosen date AND returns the calendar to the current month — it undoes the picking done so far and puts the picker back to how it opens, so prefer a word covering both, not one that only means emptying a field. Not cancel: the picker stays open afterwards and nothing is dismissed. Keep it short; it shares one line with the month title and two arrows."
729
729
  },
730
730
  "@astryx.dateInput.savePicking": {
731
731
  "defaultMessage": "Save",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.5.0-canary.cac77bc",
3
+ "version": "0.5.0-canary.cb20a01",
4
4
  "displayName": "Astryx Core",
5
5
  "description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
6
6
  "author": "Meta Open Source",
@@ -661,7 +661,7 @@
661
661
  "react-dom": ">=19.0.0"
662
662
  },
663
663
  "devDependencies": {
664
- "@astryxdesign/cli": "0.5.0-canary.cac77bc",
664
+ "@astryxdesign/cli": "0.5.0-canary.cb20a01",
665
665
  "@babel/cli": "^8.0.4",
666
666
  "@babel/core": "^7.29.7",
667
667
  "@babel/preset-react": "^8.0.1",
@@ -21,21 +21,75 @@ export const docs = {
21
21
  ],
22
22
  usage: {
23
23
  description:
24
- 'AlertDialog asks the user to confirm a destructive or irreversible action before it happens. Use it for things like deleting content, revoking access, or discarding unsaved changes.\n\nIt implements the WAI-ARIA APG [Alert Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/): `role="alertdialog"`, a title linked by `aria-labelledby`, a consequence description linked by `aria-describedby`, focus moved into the dialog on open and returned to the trigger on close, and no dismissal by clicking outside. Escape cancels.\n\nFor cases where you want to show an alert without managing open state, use the `useImperativeAlertDialog` hook: call `alert.show(options)` and render `alert.element` in your tree.',
24
+ 'AlertDialog asks the user to confirm a destructive or irreversible action before it happens. Use it for things like deleting content, revoking access, or discarding unsaved changes.\n\nIt implements the WAI-ARIA APG [Alert Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/): `role="alertdialog"`, a title linked by `aria-labelledby`, a consequence description linked by `aria-describedby`, focus moved into the dialog on open and returned to the trigger on close, and no dismissal by clicking outside. Escape cancels.\n\nAlertDialog passes its requested width through to Dialog, which clamps the surface to the container and dynamic viewport with token gutters. Generic Dialog footers should wrap, but Dialog does not own action semantics or order; consumer composition controls that. AlertDialog owns its confirmation semantics: above 640px, actions render horizontally and may move onto another row; at 640px and below, the destructive action appears above Cancel and both buttons fill the footer width. Button labels retain their standard single-line behavior. The breakpoint follows available width, not pointer or hover capability. The body scrolls when block space is constrained.\n\nFor cases where you want to show an alert without managing open state, use the `useImperativeAlertDialog` hook: call `alert.show(options)` and render `alert.element` in your tree.',
25
25
  bestPractices: [
26
- {guidance: true, description: 'Make the action button label specific: "Delete project" is better than "OK" or "Confirm".'},
27
- {guidance: true, description: 'Describe what will happen in the description so the user knows the consequences before confirming.'},
28
- {guidance: true, description: 'Keep the cancel button first: it takes initial focus, so the least destructive choice is the one already selected when the dialog opens.'},
29
- {guidance: false, description: 'Use AlertDialog for non-destructive actions; use a standard Dialog instead.'},
30
- {guidance: false, description: 'Rely on color alone to signal danger; the action label itself should say what will happen.'},
31
- {guidance: false, description: 'Close the dialog from onAction before the work finishes; hold it open with isActionLoading and call onOpenChange(false) when the action settles.'},
26
+ {
27
+ guidance: true,
28
+ description:
29
+ 'Make the action button label specific: "Delete project" is better than "OK" or "Confirm".',
30
+ },
31
+ {
32
+ guidance: true,
33
+ description:
34
+ 'Describe what will happen in the description so the user knows the consequences before confirming.',
35
+ },
36
+ {
37
+ guidance: true,
38
+ description:
39
+ 'Keep the cancel button as the least-destructive focus target. On narrow screens the destructive action is visually and structurally above Cancel, but Cancel still receives initial focus.',
40
+ },
41
+ {
42
+ guidance: true,
43
+ description:
44
+ 'Use concise, specific action labels. Above 640px, complete buttons may move onto another row; at 640px and below, the destructive action appears above Cancel and both buttons fill the footer width.',
45
+ },
46
+ {
47
+ guidance: false,
48
+ description:
49
+ 'Use AlertDialog for non-destructive actions; use a standard Dialog instead.',
50
+ },
51
+ {
52
+ guidance: false,
53
+ description:
54
+ 'Rely on color alone to signal danger; the action label itself should say what will happen.',
55
+ },
56
+ {
57
+ guidance: false,
58
+ description:
59
+ 'Close the dialog from onAction before the work finishes; hold it open with isActionLoading and call onOpenChange(false) when the action settles.',
60
+ },
32
61
  ],
33
62
  anatomy: [
34
- {name: 'Title', required: true, description: 'The question being asked. Renders as a level-2 heading and labels the dialog via aria-labelledby.'},
35
- {name: 'Description', required: true, description: 'What will happen if the user confirms. Linked to the dialog via aria-describedby.'},
36
- {name: 'Cancel button', required: true, description: 'Ghost button that dismisses without acting. Takes initial focus, and Escape does the same thing.'},
37
- {name: 'Action button', required: true, description: 'The confirming action. Destructive by default; shows a spinner while isActionLoading is set.'},
38
- {name: 'Backdrop', required: true, description: 'Overlay behind the dialog that blocks page interaction. Clicking it does not dismiss.'},
63
+ {
64
+ name: 'Title',
65
+ required: true,
66
+ description:
67
+ 'The question being asked. Renders as a level-2 heading and labels the dialog via aria-labelledby.',
68
+ },
69
+ {
70
+ name: 'Description',
71
+ required: true,
72
+ description:
73
+ 'What will happen if the user confirms. Linked to the dialog via aria-describedby.',
74
+ },
75
+ {
76
+ name: 'Cancel button',
77
+ required: true,
78
+ description:
79
+ 'Ghost button that dismisses without acting. Takes initial focus, and Escape does the same thing.',
80
+ },
81
+ {
82
+ name: 'Action button',
83
+ required: true,
84
+ description:
85
+ 'The confirming action. Destructive by default; shows a spinner while isActionLoading is set.',
86
+ },
87
+ {
88
+ name: 'Backdrop',
89
+ required: true,
90
+ description:
91
+ 'Overlay behind the dialog that blocks page interaction. Clicking it does not dismiss.',
92
+ },
39
93
  ],
40
94
  },
41
95
  // Intentionally a contained isInline preview, not playground.overlay: the
@@ -48,11 +102,13 @@ export const docs = {
48
102
  isInline: true,
49
103
  onOpenChange: undefined,
50
104
  title: 'Delete item?',
51
- description: 'This action cannot be undone. The item and all its data will be permanently removed.',
105
+ description:
106
+ 'This action cannot be undone. The item and all its data will be permanently removed.',
52
107
  actionLabel: 'Delete',
53
108
  },
54
109
  },
55
- description: 'A modal dialog that asks the user to confirm a destructive action.',
110
+ description:
111
+ 'A modal dialog that asks the user to confirm a destructive action.',
56
112
  props: [
57
113
  {
58
114
  name: 'isOpen',
@@ -111,38 +167,66 @@ export const docs = {
111
167
  name: 'width',
112
168
  type: 'number | string',
113
169
  default: '400',
114
- description: 'Dialog width.',
170
+ description:
171
+ 'Requested dialog width. Dialog preserves this preferred width and clamps it to the container and dynamic viewport with token gutters.',
115
172
  },
116
173
  {
117
174
  name: 'isInline',
118
175
  type: 'boolean',
119
176
  default: 'false',
120
- description: 'Renders alert dialog content inline without modal behavior. For documentation previews and showcases only. Not being a modal, the inline path renders role="group" instead of role="alertdialog".',
177
+ description:
178
+ 'Renders alert dialog content inline without modal behavior. For documentation previews and showcases only. Not being a modal, the inline path renders role="group" instead of role="alertdialog".',
121
179
  },
122
180
  ],
123
- components: [
124
- {name: 'useImperativeAlertDialog'},
125
- ],
181
+ components: [{name: 'useImperativeAlertDialog'}],
126
182
  theming: {
127
- targets: [
128
- {className: 'astryx-alert-dialog'},
129
- ],
183
+ targets: [{className: 'astryx-alert-dialog'}],
130
184
  },
131
185
  };
132
186
 
133
187
  /** @type {import('@astryxdesign/cli/authoring').ComponentTranslationDoc} */
134
188
  export const docsDense = {
135
- description: 'Confirms destructive/irreversible action before it happens (delete, revoke access, discard unsaved changes).',
189
+ description:
190
+ 'Confirms destructive/irreversible action before it happens (delete, revoke access, discard unsaved changes).',
136
191
  usage: {
137
192
  description:
138
- 'AlertDialog confirms destructive/irreversible action (delete, revoke access, discard changes). Implements WAI-ARIA APG Alert Dialog pattern (https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/): role="alertdialog", aria-labelledby title, aria-describedby description, focus into dialog on open + back to trigger on close, no outside-click dismissal, Escape cancels. To show w/o managing open state, use useImperativeAlertDialog hook: call alert.show(options) + render alert.element in tree.',
193
+ 'AlertDialog confirms destructive/irreversible action (delete, revoke access, discard changes). Implements WAI-ARIA APG Alert Dialog pattern (https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/): role="alertdialog", aria-labelledby title, aria-describedby description, focus into dialog on open + back to trigger on close, no outside-click dismissal, Escape cancels. Dialog preserves requested width + clamps it to container/dynamic viewport gutters. Generic Dialog footers wrap while consumers own action order. AlertDialog owns confirmation action layout: >640px horizontal buttons may move to another row; <=640px puts the destructive action above Cancel and makes both buttons full width regardless of pointer/hover capability. Button labels remain single-line. Body scrolls when height constrained. To show w/o managing open state, use useImperativeAlertDialog hook: call alert.show(options) + render alert.element in tree.',
139
194
  bestPractices: [
140
- {guidance: true, description: 'Make action button label specific: "Delete project" > "OK"/"Confirm".'},
141
- {guidance: true, description: 'Describe consequences in description so user knows outcome before confirming.'},
142
- {guidance: true, description: 'Keep cancel first: it takes initial focus, so least destructive choice is preselected.'},
143
- {guidance: false, description: 'Use AlertDialog for non-destructive actions; use standard Dialog instead.'},
144
- {guidance: false, description: 'Rely on color alone for danger; the action label should say what happens.'},
145
- {guidance: false, description: 'Close from onAction before work finishes; hold open w/ isActionLoading, call onOpenChange(false) when it settles.'},
195
+ {
196
+ guidance: true,
197
+ description:
198
+ 'Make action button label specific: "Delete project" > "OK"/"Confirm".',
199
+ },
200
+ {
201
+ guidance: true,
202
+ description:
203
+ 'Describe consequences in description so user knows outcome before confirming.',
204
+ },
205
+ {
206
+ guidance: true,
207
+ description:
208
+ 'Keep cancel as initial focus: least destructive choice is preselected even when narrow visual/DOM order places it after the destructive action.',
209
+ },
210
+ {
211
+ guidance: true,
212
+ description:
213
+ 'Use concise action labels; >640px complete buttons may move to another row, and <=640px puts the destructive action above Cancel with both buttons full width regardless of pointer type.',
214
+ },
215
+ {
216
+ guidance: false,
217
+ description:
218
+ 'Use AlertDialog for non-destructive actions; use standard Dialog instead.',
219
+ },
220
+ {
221
+ guidance: false,
222
+ description:
223
+ 'Rely on color alone for danger; the action label should say what happens.',
224
+ },
225
+ {
226
+ guidance: false,
227
+ description:
228
+ 'Close from onAction before work finishes; hold open w/ isActionLoading, call onOpenChange(false) when it settles.',
229
+ },
146
230
  ],
147
231
  },
148
232
  };