@astryxdesign/core 0.7.1-canary.b32d73f → 0.7.1-canary.c4df922

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,7 +1,7 @@
1
1
  /**
2
2
  * @file Dialog.tsx
3
3
  * @input Uses React, DialogHTMLAttributes, ReactNode, container (Layout), DialogContext, layerTextReset,
4
- * layerStructureReset, modalOutlet, interactionModality
4
+ * layerStructureReset, modalOutlet, interactionModality, chainEventHandlers
5
5
  * @output Exports Dialog component, DialogProps, DialogVariant, DialogPurpose types
6
6
  * @position Core implementation; consumed by index.ts, tested by Dialog.test.tsx
7
7
  *
@@ -1 +1 @@
1
- {"version":3,"file":"Dialog.d.ts","sourceRoot":"","sources":["../../src/Dialog/Dialog.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA2B5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAKhD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,SAAS,CAAC;AAyB9C;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,gBAAgB,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAEzD,4EAA4E;AAC5E,UAAU,mBAAmB;IAC3B,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAe,SAAQ,mBAAmB;IACzD,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAuBD,2FAA2F;AAC3F,eAAO,MAAM,uCAAuC;;;;;;;;;;;CAW1C,CAAC;AAoJX;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,cAAc;;;;;EAUpE;AAED,MAAM,WAAW,WAAY,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IAC/D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;;;;;OAOG;IACH,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IAE3C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAExB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAE5B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;IAEpC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IAExB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IAExB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,MAAM,CAAC,EACrB,MAAM,EACN,QAAgB,EAChB,YAAY,EACZ,KAAW,EACX,SAAmB,EACnB,QAAQ,EACR,OAAoB,EACpB,OAAgB,EAChB,OAAO,EACP,QAAQ,EACR,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,WAAW,sCAiUb;yBAjVe,MAAM"}
1
+ {"version":3,"file":"Dialog.d.ts","sourceRoot":"","sources":["../../src/Dialog/Dialog.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA2B5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAMhD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,SAAS,CAAC;AAyB9C;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,gBAAgB,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAEzD,4EAA4E;AAC5E,UAAU,mBAAmB;IAC3B,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAe,SAAQ,mBAAmB;IACzD,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAuBD,2FAA2F;AAC3F,eAAO,MAAM,uCAAuC;;;;;;;;;;;CAW1C,CAAC;AAoJX;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,cAAc;;;;;EAUpE;AAED,MAAM,WAAW,WAAY,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IAC/D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;;;;;OAOG;IACH,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IAE3C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAExB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAE5B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;IAEpC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IAExB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IAExB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,MAAM,CAAC,EACrB,MAAM,EACN,QAAgB,EAChB,YAAY,EACZ,KAAW,EACX,SAAmB,EACnB,QAAQ,EACR,OAAoB,EACpB,OAAgB,EAChB,OAAO,EACP,QAAQ,EACR,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,WAAW,sCA2Ub;yBA3Ve,MAAM"}
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * @file Dialog.tsx
7
7
  * @input Uses React, DialogHTMLAttributes, ReactNode, container (Layout), DialogContext, layerTextReset,
8
- * layerStructureReset, modalOutlet, interactionModality
8
+ * layerStructureReset, modalOutlet, interactionModality, chainEventHandlers
9
9
  * @output Exports Dialog component, DialogProps, DialogVariant, DialogPurpose types
10
10
  * @position Core implementation; consumed by index.ts, tested by Dialog.test.tsx
11
11
  *
@@ -41,6 +41,7 @@ import { colorVars, radiusVars, durationVars, easeVars, shadowVars, spacingVars
41
41
  import { container } from "../Layout/container.stylex.js";
42
42
  import { paddingStyles, containerPaddingInlineVarStyles, containerPaddingBlockStartVarStyles, containerPaddingBlockEndVarStyles, spacingStepToToken, overlayPaddingReset } from "../Layout/padding.stylex.js";
43
43
  import { mergeProps } from "../utils/index.js";
44
+ import { chainEventHandlers } from "../utils/chainEventHandlers.js";
44
45
  import { devWarn } from "../utils/devWarning.js";
45
46
  import { DialogContext } from "./DialogContext.js";
46
47
  import { themeProps } from "../utils/themeProps.js";
@@ -489,12 +490,22 @@ export function Dialog({
489
490
  })
490
491
  });
491
492
 
492
- // Filter out native open to prevent InvalidStateError when accidentally passed
493
+ // Filter out native open to prevent InvalidStateError when accidentally passed.
494
+ // Keep the consumer click handler separate so the modal path can compose it
495
+ // with Dialog-owned behavior without changing the inline preview path.
493
496
  const hasPosition = position != null && !isFullscreen;
497
+ const {
498
+ onClick: onClickProp,
499
+ ...propsWithoutClick
500
+ } = props;
494
501
  const {
495
502
  open: _open,
496
503
  ...safeProps
497
- } = props;
504
+ } = propsWithoutClick;
505
+ const inlineSafeProps = {
506
+ ...safeProps,
507
+ onClick: onClickProp
508
+ };
498
509
 
499
510
  // --- Inline rendering path (for documentation previews) ---
500
511
  if (isInline) {
@@ -502,7 +513,7 @@ export function Dialog({
502
513
  return null;
503
514
  }
504
515
  return /*#__PURE__*/_jsx("div", {
505
- ...safeProps,
516
+ ...inlineSafeProps,
506
517
  ...mergeProps(themeProps('dialog', {
507
518
  variant
508
519
  }), stylex.props(layerTextReset.reset, layerStructureReset.reset, styles.inlineWrapper, overlayPaddingReset.reset, standardSizing && dynamicStyles.sizing(standardSizing.width, standardSizing.maxWidth, standardSizing.maxHeight), isFullscreen && styles.fullscreen, xstyle), className, style),
@@ -526,7 +537,7 @@ export function Dialog({
526
537
  const o = resolveDialogPositionOffsets(position);
527
538
  return dynamicStyles.position(o.top, o.insetInlineStart, o.insetInlineEnd, o.bottom);
528
539
  })(), isFullscreen && styles.fullscreen, isFullscreen && isOpen && styles.fullscreenOpen, xstyle), className, style),
529
- onClick: handleClick,
540
+ onClick: chainEventHandlers(handleClick, onClickProp),
530
541
  onCancel: handleCancel,
531
542
  "aria-modal": "true"
532
543
  // The default aria-labelledby (from a DialogHeader title) is set
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file DialogHeader.tsx
3
- * @input Uses React, StyleX, LayoutHeader, Button, Icon, Heading, Text, DialogContext, mergeProps, themeProps
3
+ * @input Uses React, StyleX, LayoutHeader, Button, Icon, Heading, Text, DialogContext, isRenderable, mergeProps, themeProps
4
4
  * @output Exports DialogHeader component and DialogHeaderProps
5
5
  * @position Dialog header component; used with Dialog and Layout
6
6
  *
@@ -1 +1 @@
1
- {"version":3,"file":"DialogHeader.d.ts","sourceRoot":"","sources":["../../src/Dialog/DialogHeader.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAoB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAQxD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA2C5C,MAAM,WAAW,iBAAkB,SAAQ,SAAS,CAAC,cAAc,CAAC;IAClE,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;;;;;;;OASG;IACH,KAAK,EAAE,SAAS,CAAC;IAEjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IAE5C;;OAEG;IACH,YAAY,CAAC,EAAE,SAAS,CAAC;IAEzB;;OAEG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IAEvB;;;;;OAKG;IACH,0BAA0B,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,KAAK,CAAC;IAExD;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,0BAA0B,EAC1B,UAAU,EACV,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,IAAI,EACR,EAAE,iBAAiB,+BA2GnB;yBAxHe,YAAY"}
1
+ {"version":3,"file":"DialogHeader.d.ts","sourceRoot":"","sources":["../../src/Dialog/DialogHeader.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAoB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAQxD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA2C5C,MAAM,WAAW,iBAAkB,SAAQ,SAAS,CAAC,cAAc,CAAC;IAClE,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;;;;;;;OASG;IACH,KAAK,EAAE,SAAS,CAAC;IAEjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IAE5C;;OAEG;IACH,YAAY,CAAC,EAAE,SAAS,CAAC;IAEzB;;OAEG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IAEvB;;;;;OAKG;IACH,0BAA0B,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,KAAK,CAAC;IAExD;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,0BAA0B,EAC1B,UAAU,EACV,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,IAAI,EACR,EAAE,iBAAiB,+BA0GnB;yBAvHe,YAAY"}
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file DialogHeader.tsx
7
- * @input Uses React, StyleX, LayoutHeader, Button, Icon, Heading, Text, DialogContext, mergeProps, themeProps
7
+ * @input Uses React, StyleX, LayoutHeader, Button, Icon, Heading, Text, DialogContext, isRenderable, mergeProps, themeProps
8
8
  * @output Exports DialogHeader component and DialogHeaderProps
9
9
  * @position Dialog header component; used with Dialog and Layout
10
10
  *
@@ -24,7 +24,7 @@ import { Button } from "../Button/index.js";
24
24
  import { Icon } from "../Icon/index.js";
25
25
  import { Heading } from "../Heading/Heading.js";
26
26
  import { Text } from "../Text/Text.js";
27
- import { mergeProps } from "../utils/index.js";
27
+ import { isRenderable, mergeProps } from "../utils/index.js";
28
28
  import { themeProps } from "../utils/themeProps.js";
29
29
  import { useDialogContext } from "./DialogContext.js";
30
30
  import { useTranslator } from "../i18n/index.js";
@@ -76,9 +76,9 @@ export function DialogHeader({
76
76
  const dialogContext = useDialogContext();
77
77
  const shouldAutoFocus = dialogContext?.isInline !== true;
78
78
  const titleId = dialogContext?.titleId;
79
- // A node subtitle may be `0`: render it inside Text instead of letting a
80
- // truthiness check leak a bare text node, and skip only empty values.
81
- const hasSubtitle = subtitle != null && typeof subtitle !== 'boolean' && subtitle !== '';
79
+ const hasSubtitle = isRenderable(subtitle);
80
+ const hasStartContent = isRenderable(startContent);
81
+ const hasEndContent = isRenderable(endContent);
82
82
  const shouldCompensateEndBlock = endContentEdgeCompensation == null ? onOpenChange != null : endContentEdgeCompensation === 'block' || endContentEdgeCompensation === 'all';
83
83
  const shouldCompensateEndInline = endContentEdgeCompensation == null ? onOpenChange != null : endContentEdgeCompensation === 'inline' || endContentEdgeCompensation === 'all';
84
84
 
@@ -103,7 +103,7 @@ export function DialogHeader({
103
103
  ...mergeProps(themeProps('dialog-header'), {
104
104
  className: "x78zum5 x1cy8zhl x1qughib xjcht0a"
105
105
  }),
106
- children: [startContent && /*#__PURE__*/_jsx("div", {
106
+ children: [hasStartContent && /*#__PURE__*/_jsx("div", {
107
107
  ...mergeProps(themeProps('dialog-header-start-content'), {
108
108
  className: "x78zum5 x6s0dn4 x1txdalj x2lah0s"
109
109
  }),
@@ -125,7 +125,7 @@ export function DialogHeader({
125
125
  color: "secondary",
126
126
  children: subtitle
127
127
  })]
128
- }), (endContent || onOpenChange) && /*#__PURE__*/_jsxs("div", {
128
+ }), (hasEndContent || onOpenChange != null) && /*#__PURE__*/_jsxs("div", {
129
129
  ...mergeProps(themeProps('dialog-header-end-content'), {
130
130
  0: {
131
131
  className: "x78zum5 x6s0dn4 x1txdalj x2lah0s"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.1-canary.b32d73f",
3
+ "version": "0.7.1-canary.c4df922",
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",
@@ -787,8 +787,8 @@
787
787
  "react-dom": ">=19.0.0"
788
788
  },
789
789
  "devDependencies": {
790
- "@astryxdesign/a11y-spec": "0.7.1-canary.b32d73f",
791
- "@astryxdesign/cli": "0.7.1-canary.b32d73f",
790
+ "@astryxdesign/a11y-spec": "0.7.1-canary.c4df922",
791
+ "@astryxdesign/cli": "0.7.1-canary.c4df922",
792
792
  "@babel/cli": "^7.29.7",
793
793
  "@babel/core": "^7.29.7",
794
794
  "@babel/preset-react": "^7.29.7",
@@ -45,7 +45,7 @@ system_specs: [spec:AST-025]
45
45
  | Behavior | CodeBlock renders a read-only multi-line code surface, applies syntax highlighting when available, preserves the source string for copy, and exposes optional line, header, copy, theme, and disclosure behavior without editing the content. |
46
46
  | End-user impact | Readers can inspect and copy code, understand optional line emphasis, and operate long or collapsible examples with keyboard and assistive technology. |
47
47
  | Builder impact | Builders choose the released display axes without an additional migration or opt-in; the diff layer is one more opt-in axis, and five defaulted syntax-theme slots mean no syntax theme has to change for it. |
48
- | Compatibility/readiness | The released `CodeBlock` API and defaults remain stable; FR9 (the diff layer) is current and an implementation gap until its implementation ships; Markdown's default path for `diff` fences changes with it (`component:Markdown` FR29). |
48
+ | Compatibility/readiness | The released `CodeBlock` API and defaults remain stable; FR9 (the diff layer) is current and shipped; Markdown's default path for `diff` fences follows it (`component:Markdown` FR29). |
49
49
  | Review checks | Reject lost code text, copy of a transformed string, an unnamed copy or disclosure control, stale disclosure state/wiring, dropped root props/ref, a theme target off its painter, or viewport focusability that ignores effective overflow. |
50
50
  | Governing rules | `architecture:public-component-api/INV1,INV5–INV9`; `architecture:component-theming-surface/INV3–INV7,INV12`; `architecture:component-test-sufficiency/INV1–INV7,INV10–INV11`; `spec:AST-025/FR12–FR13`; `architecture:theme-tokens` (syntax vocabulary and the diff slot defaults). |
51
51
 
@@ -93,17 +93,17 @@ Consumer migration instructions belong in consumer docs and release notes.
93
93
 
94
94
  ## Behavioral and layout contract
95
95
 
96
- | ID | Candidate invariant | Basis | Draft review state |
97
- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
98
- | FR1 | CodeBlock MUST display the caller's code string as read-only code. Built-in or custom tokenization MAY change syntax paint but MUST NOT change displayed or copied source text. | released types, source, tests, consumer docs; established public interface | verify |
99
- | FR2 | A successful copy MUST write the exact `code` string, invoke `onCopy` once, expose localized copied feedback, and preserve the copy control's discoverable purpose. A failed write MUST NOT report success. | shipped source/tests; `architecture:component-test-sufficiency/INV2–INV6`; WCAG name/status obligations | verify |
100
- | FR3 | A title or visible non-plaintext language label MUST create the visible header. Hiding the language label MUST NOT remove copy behavior. Headerless copy remains available when enabled. | shipped source/tests/docs; established public interface | verify |
101
- | FR4 | Line numbers, highlighted lines, wrapping, width, container presentation, size, and maximum height MUST affect only their documented presentation axes. Numeric `maxHeight={0}` MUST remain a valid zero-pixel bound rather than being discarded. | released public type/docs; `architecture:public-component-api/INV3,INV9`; objective JavaScript nullish semantics | verified by focused regression |
102
- | FR5 | Collapse MUST become available only when enabled, a visible header exists, and the line threshold is met. The control MUST expose expanded state and a resolvable controlled region; collapsed content remains mounted but inert, and losing the control expands the region before hiding the control. | shipped docs/source/tests; WAI-ARIA disclosure pattern and HTML `inert` semantics | verify |
103
- | FR6 | The root and documented header/title/copy targets MUST remain on their visible painters, reflect the documented visual axes, and continue emitting the approved deprecated aliases until their authorized removal window. | `architecture:component-theming-surface/INV3–INV7,INV12`; source/docs/tests | verify |
104
- | FR7 | Supported DOM, data, ARIA, class, style, and `xstyle` inputs MUST compose on the documented root, and the public ref MUST reach that root. | `architecture:public-component-api/INV5,INV6,INV8`; shipped source | verify |
105
- | FR8 | The code viewport MUST enter sequential keyboard navigation only while at least one applicable axis effectively overflows; losing overflow MUST NOT move current focus. It retains an appropriate role and accessible name while focusable. | `spec:AST-025/FR12–FR13` | settled by current authority |
106
- | FR9 | With the diff layer on (`isDiff`), CodeBlock MUST classify each line by its first character — `+` added, `-` removed, `@@` header, anything else context — except that a `--- ` line (three minus signs and a space, the unified-diff file header form) directly followed by a `+++ ` line and then an `@@` line is a file header (both lines), wherever it occurs; every other `---`/`+++` line, `---i;` included, is removed or added. CodeBlock MUST set the glyph column aside and tokenize the REST of every line (context, added, and removed alike) with the base `language`'s syntax highlighting. Added, removed, and header lines MUST paint as whole rows — the line-number gutter cell included — from the syntax-theme slots `--color-syntax-added`, `--color-syntax-removed`, and `--color-syntax-diff-header`, whose defaults `architecture:theme-tokens` owns; header rows take the syntax theme's `punctuation` colour as text (`--color-text-secondary` in the default theme) and never change paint; context rows keep ordinary paint. On an added or removed row, the line number and the glyph take the row's marker slot — `--color-syntax-added-marker` / `--color-syntax-removed-marker` — so the margin reads the change in colour; every run of code text, painted by a token or not, keeps its syntax-theme colour. In the Astryx default syntax theme the marker slots default to the theme's tinted-text tokens and the header paint to the muted background (`architecture:theme-tokens` owns the values); a syntax theme that does not set them inherits its own colours — the marker slots and the header text its line-number colour (its `punctuation` slot), the header paint none at all (the header row sits on the theme's own syntax background) — so number, glyph, and header text stay legible on that theme's background in every page mode, and the glyph's shape carries the meaning where its colour does not. The glyph (`+`, `-`, the leading space of a context line, or nothing for a context or header line without one — the marker column is then empty and the whole line is code) MUST render in its own fixed-width marker column — secondary colour on context rows and the row's marker slot on added and removed rows, separated from the code by a token gap (`--spacing-2`, a proportion themes may vary like the rest), excluded from selection for layout yet present in the copied text — so the code column starts at the same inline offset on every line and the content's own indentation is never shifted; the gutter order is line number, marker, code, mirrored in RTL with the code's direction unchanged. The header (language label, copy, disclosure) and the viewport's name stay as for any other code: with the layer over plaintext the visible language label and the viewport name are `diff`, and over a language they name both (`tsx diff`). Markdown's fence mapping onto the layer is `component:Markdown` FR29. Row paint MUST compose with line numbers without moving text; on a highlighted added, removed, or header line the highlight REPLACES the row paint rather than stacking on it, so text keeps its single-paint contrast; and row paint MUST span the full line width — the scrolled overflow of a long line included, in wrapped and unwrapped mode — not only the visible viewport. Contrast floor: in the Astryx default syntax theme, every default token colour and the row's marker slot (line number and glyph) on a painted row MUST measure ≥ 4.5:1 against the default tints composited over the code block's own `--color-syntax-background` on the default card surface. In a syntax theme that overrides a diff slot, a token that already measures under 4.5:1 on that theme's own syntax background MUST NOT drop further on the slot, and every other token MUST clear 4.5:1 on it. Two known shortfalls, tracked separately: the shipped syntax presets do not yet meet the floor on the default tints with the layer on, and shipped page themes whose own text tokens fall under it on the tints keep that shortfall, tracked for a fix in those themes, until each sets its tokens or the diff slots. | this record DEC-1 | current; owner-approved 2026-10-12; implementation gap |
96
+ | ID | Candidate invariant | Basis | Draft review state |
97
+ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
98
+ | FR1 | CodeBlock MUST display the caller's code string as read-only code. Built-in or custom tokenization MAY change syntax paint but MUST NOT change displayed or copied source text. | released types, source, tests, consumer docs; established public interface | verify |
99
+ | FR2 | A successful copy MUST write the exact `code` string, invoke `onCopy` once, expose localized copied feedback, and preserve the copy control's discoverable purpose. A failed write MUST NOT report success. | shipped source/tests; `architecture:component-test-sufficiency/INV2–INV6`; WCAG name/status obligations | verify |
100
+ | FR3 | A title or visible non-plaintext language label MUST create the visible header. Hiding the language label MUST NOT remove copy behavior. Headerless copy remains available when enabled. | shipped source/tests/docs; established public interface | verify |
101
+ | FR4 | Line numbers, highlighted lines, wrapping, width, container presentation, size, and maximum height MUST affect only their documented presentation axes. Numeric `maxHeight={0}` MUST remain a valid zero-pixel bound rather than being discarded. | released public type/docs; `architecture:public-component-api/INV3,INV9`; objective JavaScript nullish semantics | verified by focused regression |
102
+ | FR5 | Collapse MUST become available only when enabled, a visible header exists, and the line threshold is met. The control MUST expose expanded state and a resolvable controlled region; collapsed content remains mounted but inert, and losing the control expands the region before hiding the control. | shipped docs/source/tests; WAI-ARIA disclosure pattern and HTML `inert` semantics | verify |
103
+ | FR6 | The root and documented header/title/copy targets MUST remain on their visible painters, reflect the documented visual axes, and continue emitting the approved deprecated aliases until their authorized removal window. | `architecture:component-theming-surface/INV3–INV7,INV12`; source/docs/tests | verify |
104
+ | FR7 | Supported DOM, data, ARIA, class, style, and `xstyle` inputs MUST compose on the documented root, and the public ref MUST reach that root. | `architecture:public-component-api/INV5,INV6,INV8`; shipped source | verify |
105
+ | FR8 | The code viewport MUST enter sequential keyboard navigation only while at least one applicable axis effectively overflows; losing overflow MUST NOT move current focus. It retains an appropriate role and accessible name while focusable. | `spec:AST-025/FR12–FR13` | settled by current authority |
106
+ | FR9 | With the diff layer on (`isDiff`), CodeBlock MUST classify each line by its first character — `+` added, `-` removed, `@@` header, anything else context — except that a `--- ` line (three minus signs and a space, the unified-diff file header form) directly followed by a `+++ ` line and then an `@@` line is a file header (both lines), wherever it occurs; every other `---`/`+++` line, `---i;` included, is removed or added. CodeBlock MUST set the glyph column aside and tokenize the REST of every line (context, added, and removed alike) with the base `language`'s syntax highlighting. Added, removed, and header lines MUST paint as whole rows — the line-number gutter cell included — from the syntax-theme slots `--color-syntax-added`, `--color-syntax-removed`, and `--color-syntax-diff-header`, whose defaults `architecture:theme-tokens` owns; header rows take the syntax theme's `punctuation` colour as text (`--color-text-secondary` in the default theme) and never change paint; context rows keep ordinary paint. On an added or removed row, the line number and the glyph take the row's marker slot — `--color-syntax-added-marker` / `--color-syntax-removed-marker` — so the margin reads the change in colour; every run of code text, painted by a token or not, keeps its syntax-theme colour. In the Astryx default syntax theme the marker slots default to the theme's tinted-text tokens and the header paint to the muted background (`architecture:theme-tokens` owns the values); a syntax theme that does not set them inherits its own colours — the marker slots and the header text its line-number colour (its `punctuation` slot), the header paint none at all (the header row sits on the theme's own syntax background) — so number, glyph, and header text stay legible on that theme's background in every page mode, and the glyph's shape carries the meaning where its colour does not. The glyph (`+`, `-`, the leading space of a context line, or nothing for a context or header line without one — the marker column is then empty and the whole line is code) MUST render in its own fixed-width marker column — secondary colour on context rows and the row's marker slot on added and removed rows, separated from the code by a token gap (`--spacing-2`, a proportion themes may vary like the rest), excluded from selection for layout yet present in the copied text — so the code column starts at the same inline offset on every line and the content's own indentation is never shifted; the gutter order is line number, marker, code, mirrored in RTL with the code's direction unchanged. The header (language label, copy, disclosure) and the viewport's name stay as for any other code: with the layer over plaintext the visible language label and the viewport name are `diff`, and over a language they name both (`tsx diff`). Markdown's fence mapping onto the layer is `component:Markdown` FR29. Row paint MUST compose with line numbers without moving text; on a highlighted added, removed, or header line the highlight REPLACES the row paint rather than stacking on it, so text keeps its single-paint contrast; and row paint MUST span the full line width — the scrolled overflow of a long line included, in wrapped and unwrapped mode — not only the visible viewport. Contrast floor: in the Astryx default syntax theme, every default token colour and the row's marker slot (line number and glyph) on a painted row MUST measure ≥ 4.5:1 against the default tints composited over the code block's own `--color-syntax-background` on the default card surface. In a syntax theme that overrides a diff slot, a token that already measures under 4.5:1 on that theme's own syntax background MUST NOT drop further on the slot, and every other token MUST clear 4.5:1 on it. Two known shortfalls, tracked separately: the shipped syntax presets do not yet meet the floor on the default tints with the layer on, and shipped page themes whose own text tokens fall under it on the tints keep that shortfall, tracked for a fix in those themes, until each sets its tokens or the diff slots. | this record DEC-1 | current; owner-approved 2026-10-12 |
107
107
 
108
108
  ### Allowed variation
109
109
 
@@ -9,7 +9,7 @@
9
9
  * SYNC: When Dialog.tsx changes, update tests to match new behavior
10
10
  */
11
11
 
12
- import {useState} from 'react';
12
+ import {useState, type SyntheticEvent} from 'react';
13
13
  import {readFileSync} from 'node:fs';
14
14
  import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
15
15
  import type {MockInstance} from 'vitest';
@@ -1030,6 +1030,32 @@ describe('Dialog', () => {
1030
1030
  expect(screen.getByTestId('custom-dialog')).toBeInTheDocument();
1031
1031
  });
1032
1032
 
1033
+ it('runs backdrop dismissal before a consumer onClick', () => {
1034
+ const callOrder: string[] = [];
1035
+ const onClick = vi.fn((event: SyntheticEvent) => {
1036
+ event.preventDefault();
1037
+ callOrder.push('consumer');
1038
+ });
1039
+ const onOpenChange = vi.fn(() => {
1040
+ callOrder.push('dialog');
1041
+ });
1042
+ render(
1043
+ <Dialog
1044
+ isOpen={true}
1045
+ onOpenChange={onOpenChange}
1046
+ onClick={onClick}
1047
+ data-testid="click-dialog">
1048
+ Content
1049
+ </Dialog>,
1050
+ );
1051
+
1052
+ fireEvent.click(screen.getByTestId('click-dialog'));
1053
+
1054
+ expect(onClick).toHaveBeenCalledTimes(1);
1055
+ expect(onOpenChange).toHaveBeenCalledWith(false);
1056
+ expect(callOrder).toEqual(['dialog', 'consumer']);
1057
+ });
1058
+
1033
1059
  it('does not forward native open prop to dialog element', () => {
1034
1060
  render(
1035
1061
  <Dialog
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * @file Dialog.tsx
7
7
  * @input Uses React, DialogHTMLAttributes, ReactNode, container (Layout), DialogContext, layerTextReset,
8
- * layerStructureReset, modalOutlet, interactionModality
8
+ * layerStructureReset, modalOutlet, interactionModality, chainEventHandlers
9
9
  * @output Exports Dialog component, DialogProps, DialogVariant, DialogPurpose types
10
10
  * @position Core implementation; consumed by index.ts, tested by Dialog.test.tsx
11
11
  *
@@ -65,6 +65,7 @@ import {
65
65
  } from '../Layout/padding.stylex';
66
66
  import type {SpacingStep} from '../utils/types';
67
67
  import {mergeProps} from '../utils';
68
+ import {chainEventHandlers} from '../utils/chainEventHandlers';
68
69
  import {devWarn} from '../utils/devWarning';
69
70
  import {DialogContext} from './DialogContext';
70
71
  import {themeProps} from '../utils/themeProps';
@@ -679,9 +680,19 @@ export function Dialog({
679
680
  </div>
680
681
  );
681
682
 
682
- // Filter out native open to prevent InvalidStateError when accidentally passed
683
+ // Filter out native open to prevent InvalidStateError when accidentally passed.
684
+ // Keep the consumer click handler separate so the modal path can compose it
685
+ // with Dialog-owned behavior without changing the inline preview path.
683
686
  const hasPosition = position != null && !isFullscreen;
684
- const {open: _open, ...safeProps} = props as Record<string, unknown>;
687
+ const {onClick: onClickProp, ...propsWithoutClick} = props;
688
+ const {open: _open, ...safeProps} = propsWithoutClick as Record<
689
+ string,
690
+ unknown
691
+ >;
692
+ const inlineSafeProps: Record<string, unknown> = {
693
+ ...safeProps,
694
+ onClick: onClickProp,
695
+ };
685
696
 
686
697
  // --- Inline rendering path (for documentation previews) ---
687
698
  if (isInline) {
@@ -691,7 +702,7 @@ export function Dialog({
691
702
 
692
703
  return (
693
704
  <div
694
- {...safeProps}
705
+ {...inlineSafeProps}
695
706
  {...mergeProps(
696
707
  themeProps('dialog', {variant}),
697
708
  stylex.props(
@@ -760,7 +771,7 @@ export function Dialog({
760
771
  className,
761
772
  style,
762
773
  )}
763
- onClick={handleClick}
774
+ onClick={chainEventHandlers(handleClick, onClickProp)}
764
775
  onCancel={handleCancel}
765
776
  aria-modal="true"
766
777
  // The default aria-labelledby (from a DialogHeader title) is set
@@ -260,6 +260,19 @@ describe('DialogHeader', () => {
260
260
  expect(closeIcon).toHaveClass('astryx-icon');
261
261
  });
262
262
 
263
+ it('renders numeric zero in both content slots', () => {
264
+ const {container} = render(
265
+ <DialogHeader title="Title" startContent={0} endContent={0} />,
266
+ );
267
+
268
+ expect(
269
+ container.querySelector('.astryx-dialog-header-start-content'),
270
+ ).toHaveTextContent(/^0$/);
271
+ expect(
272
+ container.querySelector('.astryx-dialog-header-end-content'),
273
+ ).toHaveTextContent(/^0$/);
274
+ });
275
+
263
276
  it('exposes the start-content target when startContent renders', () => {
264
277
  const {container} = render(
265
278
  <DialogHeader
@@ -292,8 +305,20 @@ describe('DialogHeader', () => {
292
305
  ).toHaveLength(1);
293
306
  });
294
307
 
295
- it('omits the content-slot targets when their content does not render', () => {
296
- const {container} = render(<DialogHeader title="Title" />);
308
+ it.each([
309
+ ['empty string', ''],
310
+ ['false', false],
311
+ ['true', true],
312
+ ['null', null],
313
+ ['undefined', undefined],
314
+ ])('omits both content-slot targets for %s', (_, content) => {
315
+ const {container} = render(
316
+ <DialogHeader
317
+ title="Title"
318
+ startContent={content}
319
+ endContent={content}
320
+ />,
321
+ );
297
322
 
298
323
  expect(
299
324
  container.querySelector('.astryx-dialog-header-start-content'),
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file DialogHeader.tsx
7
- * @input Uses React, StyleX, LayoutHeader, Button, Icon, Heading, Text, DialogContext, mergeProps, themeProps
7
+ * @input Uses React, StyleX, LayoutHeader, Button, Icon, Heading, Text, DialogContext, isRenderable, mergeProps, themeProps
8
8
  * @output Exports DialogHeader component and DialogHeaderProps
9
9
  * @position Dialog header component; used with Dialog and Layout
10
10
  *
@@ -25,7 +25,7 @@ import {Icon} from '../Icon';
25
25
  import {Heading} from '../Heading/Heading';
26
26
  import {Text} from '../Text/Text';
27
27
  import type {BaseProps} from '../BaseProps';
28
- import {mergeProps} from '../utils';
28
+ import {isRenderable, mergeProps} from '../utils';
29
29
  import {themeProps} from '../utils/themeProps';
30
30
  import {useDialogContext} from './DialogContext';
31
31
  import {useTranslator} from '../i18n';
@@ -164,10 +164,9 @@ export function DialogHeader({
164
164
  const dialogContext = useDialogContext();
165
165
  const shouldAutoFocus = dialogContext?.isInline !== true;
166
166
  const titleId = dialogContext?.titleId;
167
- // A node subtitle may be `0`: render it inside Text instead of letting a
168
- // truthiness check leak a bare text node, and skip only empty values.
169
- const hasSubtitle =
170
- subtitle != null && typeof subtitle !== 'boolean' && subtitle !== '';
167
+ const hasSubtitle = isRenderable(subtitle);
168
+ const hasStartContent = isRenderable(startContent);
169
+ const hasEndContent = isRenderable(endContent);
171
170
  const shouldCompensateEndBlock =
172
171
  endContentEdgeCompensation == null
173
172
  ? onOpenChange != null
@@ -203,7 +202,7 @@ export function DialogHeader({
203
202
  themeProps('dialog-header'),
204
203
  stylex.props(styles.container),
205
204
  )}>
206
- {startContent && (
205
+ {hasStartContent && (
207
206
  <div
208
207
  {...mergeProps(
209
208
  themeProps('dialog-header-start-content'),
@@ -231,7 +230,7 @@ export function DialogHeader({
231
230
  </Text>
232
231
  )}
233
232
  </div>
234
- {(endContent || onOpenChange) && (
233
+ {(hasEndContent || onOpenChange != null) && (
235
234
  <div
236
235
  {...mergeProps(
237
236
  themeProps('dialog-header-end-content'),