@astryxdesign/core 0.6.2-canary.6203871 → 0.6.2-canary.7cce14f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/Kbd/Kbd.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @file Kbd.tsx
3
- * @input Uses React, StyleX, theme tokens
4
- * @output Exports Kbd component and KbdProps
3
+ * @input Uses React, StyleX, theme tokens, shortcut key names and aliases
4
+ * @output Exports Kbd component and KbdProps with canonical shortcut labels
5
5
  * @position Core implementation; renders styled keyboard shortcut indicators
6
6
  *
7
7
  * SYNC: When modified, update:
@@ -14,7 +14,9 @@ export interface KbdProps extends BaseProps<HTMLSpanElement> {
14
14
  ref?: React.Ref<HTMLSpanElement>;
15
15
  /**
16
16
  * Keyboard shortcut string. Use "+" to separate keys.
17
- * Special keys: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape.
17
+ * Special keys: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape,
18
+ * tab, up, down, left, and right.
19
+ * Aliases: "esc" for "escape" and "return" for "enter".
18
20
  * Use "plus" to render a literal "+" key (e.g. "shift+plus").
19
21
  *
20
22
  * @example
@@ -1 +1 @@
1
- {"version":3,"file":"Kbd.d.ts","sourceRoot":"","sources":["../../src/Kbd/Kbd.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AAEH,OAAO,KAA6B,MAAM,OAAO,CAAC;AAIlD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA0G5C,MAAM,WAAW,QAAS,SAAQ,SAAS,CAAC,eAAe,CAAC;IAC1D,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACjC;;;;;;;;;;;;OAYG;IACH,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,GAAG,CAAC,EAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,IAAI,EAAC,EAAE,QAAQ,qBAgC3E;yBAhCe,GAAG"}
1
+ {"version":3,"file":"Kbd.d.ts","sourceRoot":"","sources":["../../src/Kbd/Kbd.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AAEH,OAAO,KAA6B,MAAM,OAAO,CAAC;AAIlD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA+G5C,MAAM,WAAW,QAAS,SAAQ,SAAS,CAAC,eAAe,CAAC;IAC1D,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACjC;;;;;;;;;;;;;;OAcG;IACH,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,GAAG,CAAC,EAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,IAAI,EAAC,EAAE,QAAQ,qBA6C3E;yBA7Ce,GAAG"}
package/dist/Kbd/Kbd.js CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  /**
6
6
  * @file Kbd.tsx
7
- * @input Uses React, StyleX, theme tokens
8
- * @output Exports Kbd component and KbdProps
7
+ * @input Uses React, StyleX, theme tokens, shortcut key names and aliases
8
+ * @output Exports Kbd component and KbdProps with canonical shortcut labels
9
9
  * @position Core implementation; renders styled keyboard shortcut indicators
10
10
  *
11
11
  * SYNC: When modified, update:
@@ -35,26 +35,20 @@ const styles = {
35
35
  * Note: `mod` is not in this map — it resolves dynamically via platform
36
36
  * detection inside the component.
37
37
  */
38
- const KEY_DISPLAY = {
39
- ctrl: '\u2303',
40
- // ⌃
41
- alt: '\u2325',
42
- // ⌥
43
- shift: '\u21E7',
44
- // ⇧
45
- enter: '\u21B5',
46
- // ↵
47
- backspace: '\u232B',
48
- // ⌫
49
- escape: 'Esc',
50
- tab: '\u21E5',
51
- // ⇥
52
- up: '\u2191',
53
- down: '\u2193',
54
- left: '\u2190',
55
- right: '\u2192',
56
- plus: '+'
57
- };
38
+ const KEY_DISPLAY = new Map([['ctrl', '\u2303'],
39
+ // ⌃
40
+ ['alt', '\u2325'],
41
+ // ⌥
42
+ ['shift', '\u21E7'],
43
+ // ⇧
44
+ ['enter', '\u21B5'],
45
+ // ↵
46
+ ['backspace', '\u232B'],
47
+ // ⌫
48
+ ['escape', 'Esc'], ['tab', '\u21E5'],
49
+ // ⇥
50
+ ['up', '\u2191'], ['down', '\u2193'], ['left', '\u2190'], ['right', '\u2192'], ['plus', '+']]);
51
+ const KBD_KEY_ALIASES = new Map([['esc', 'escape'], ['return', 'enter']]);
58
52
 
59
53
  /**
60
54
  * Resolves a key name to its display string. Handles the platform-aware
@@ -65,7 +59,7 @@ function getKeyDisplay(key, isMac) {
65
59
  if (key === 'mod') {
66
60
  return isMac ? '\u2318' : 'Ctrl';
67
61
  }
68
- return KEY_DISPLAY[key] ?? key.toUpperCase();
62
+ return KEY_DISPLAY.get(key) ?? key.toUpperCase();
69
63
  }
70
64
 
71
65
  /**
@@ -73,25 +67,12 @@ function getKeyDisplay(key, isMac) {
73
67
  * (⌘, ⇧, ↵, …) that assistive tech cannot announce meaningfully, so the
74
68
  * accessible name for the shortcut is built from these words instead.
75
69
  */
76
- const KEY_LABEL = {
77
- ctrl: 'Control',
78
- alt: 'Alt',
79
- shift: 'Shift',
80
- enter: 'Enter',
81
- backspace: 'Backspace',
82
- escape: 'Escape',
83
- tab: 'Tab',
84
- up: 'Up arrow',
85
- down: 'Down arrow',
86
- left: 'Left arrow',
87
- right: 'Right arrow',
88
- plus: 'Plus'
89
- };
70
+ const KEY_LABEL = new Map([['ctrl', 'Control'], ['alt', 'Alt'], ['shift', 'Shift'], ['enter', 'Enter'], ['backspace', 'Backspace'], ['escape', 'Escape'], ['tab', 'Tab'], ['up', 'Up arrow'], ['down', 'Down arrow'], ['left', 'Left arrow'], ['right', 'Right arrow'], ['plus', 'Plus']]);
90
71
  function getKeyLabel(key, isMac) {
91
72
  if (key === 'mod') {
92
73
  return isMac ? 'Command' : 'Control';
93
74
  }
94
- return KEY_LABEL[key] ?? key.toUpperCase();
75
+ return KEY_LABEL.get(key) ?? key.toUpperCase();
95
76
  }
96
77
  function subscribeToPlatformChanges() {
97
78
  return () => {};
@@ -123,24 +104,37 @@ export function Kbd({
123
104
  ...rest
124
105
  }) {
125
106
  const isMac = useSyncExternalStore(subscribeToPlatformChanges, isApplePlatform, getServerPlatformSnapshot);
126
- const parts = keys.split('+').map(key => key.trim().toLowerCase());
107
+ const keyOccurrences = new Map();
108
+ const parts = keys.split('+').map(key => key.trim().toLowerCase()).map(sourceKey => {
109
+ const occurrence = keyOccurrences.get(sourceKey) ?? 0;
110
+ keyOccurrences.set(sourceKey, occurrence + 1);
111
+ return {
112
+ key: KBD_KEY_ALIASES.get(sourceKey) ?? sourceKey,
113
+ reactKey: `${sourceKey}:${occurrence}`
114
+ };
115
+ });
127
116
 
128
117
  // Screen-reader name: the joined spoken labels (e.g. "Command + K"), since
129
118
  // the visual glyphs below are announced meaninglessly by assistive tech.
130
- const accessibleName = parts.map(key => getKeyLabel(key, isMac)).join(' + ');
119
+ const accessibleName = parts.map(({
120
+ key
121
+ }) => getKeyLabel(key, isMac)).join(' + ');
131
122
  return /*#__PURE__*/_jsx("span", {
132
123
  ...rest,
133
124
  ref: ref,
134
125
  role: "img",
135
126
  "aria-label": accessibleName,
136
127
  ...mergeProps(themeProps('kbd'), stylex.props(styles.wrapper, xstyle), className, style),
137
- children: parts.map(key => /*#__PURE__*/_jsx("kbd", {
128
+ children: parts.map(({
129
+ key,
130
+ reactKey
131
+ }) => /*#__PURE__*/_jsx("kbd", {
138
132
  "aria-hidden": "true",
139
133
  ...{
140
134
  className: "x3nfvp2 x6s0dn4 xl56j7k x16asifk x1grt7ep x7a5moj xx3sua9 x17x4s8c xlxy82 x1q0q8m5 xib2hle xv1l7n4 x9ynric x141an7d x1e4wzip x1ltkj2j x87ps6o"
141
135
  },
142
136
  children: getKeyDisplay(key, isMac)
143
- }, key))
137
+ }, reactKey))
144
138
  });
145
139
  }
146
140
  Kbd.displayName = 'Kbd';
@@ -7,7 +7,8 @@
7
7
  * Sidebar navigation container with five zones: header + topContent (sticky together),
8
8
  * children (scrollable), footer, and footerIcons (sticky bottom).
9
9
  *
10
- * Supports optional resize via drag handle at the inline-end edge.
10
+ * Supports optional resize via a drag handle at the inline-end edge. The
11
+ * resizable wrapper clips handle overflow while keeping its focus ring inside.
11
12
  *
12
13
  * SYNC: When modified, update these files to stay in sync:
13
14
  * - /packages/core/src/SideNav/SideNav.doc.mjs
@@ -1 +1 @@
1
- {"version":3,"file":"SideNav.d.ts","sourceRoot":"","sources":["../../src/SideNav/SideNav.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAA2C,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAC/E,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AAGnD,OAAO,EAGL,KAAK,wBAAwB,EAC7B,KAAK,+BAA+B,EACrC,MAAM,0BAA0B,CAAC;AAKlC,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,2BAA2B,CAAC;AA4O/D,MAAM,WAAW,YAAa,SAAQ,SAAS,CAAC,WAAW,CAAC;IAC1D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAE7B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;IAEvD;;OAEG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB;;OAEG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IACpB;;OAEG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB;;OAEG;IACH,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,eAAe,CAAC;IAEtC;;;;;;;;;;;;;OAaG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,wBAAwB,CAAC;CAClD;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CAAC,EACtB,MAAM,EACN,UAAU,EACV,QAAQ,EACR,MAAM,EACN,WAAW,EACX,WAAmB,EACnB,SAAiB,EACjB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,EACH,SAAS,EACT,GAAG,KAAK,EACT,EAAE,YAAY,+BA0Qd;yBAzRe,OAAO"}
1
+ {"version":3,"file":"SideNav.d.ts","sourceRoot":"","sources":["../../src/SideNav/SideNav.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAA2C,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAC/E,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AAGnD,OAAO,EAGL,KAAK,wBAAwB,EAC7B,KAAK,+BAA+B,EACrC,MAAM,0BAA0B,CAAC;AAKlC,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,2BAA2B,CAAC;AAsP/D,MAAM,WAAW,YAAa,SAAQ,SAAS,CAAC,WAAW,CAAC;IAC1D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAE7B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;IAEvD;;OAEG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB;;OAEG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IACpB;;OAEG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB;;OAEG;IACH,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,eAAe,CAAC;IAEtC;;;;;;;;;;;;;OAaG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,wBAAwB,CAAC;CAClD;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CAAC,EACtB,MAAM,EACN,UAAU,EACV,QAAQ,EACR,MAAM,EACN,WAAW,EACX,WAAmB,EACnB,SAAiB,EACjB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,EACH,SAAS,EACT,GAAG,KAAK,EACT,EAAE,YAAY,+BA2Qd;yBA1Re,OAAO"}
@@ -11,7 +11,8 @@
11
11
  * Sidebar navigation container with five zones: header + topContent (sticky together),
12
12
  * children (scrollable), footer, and footerIcons (sticky bottom).
13
13
  *
14
- * Supports optional resize via drag handle at the inline-end edge.
14
+ * Supports optional resize via a drag handle at the inline-end edge. The
15
+ * resizable wrapper clips handle overflow while keeping its focus ring inside.
15
16
  *
16
17
  * SYNC: When modified, update these files to stay in sync:
17
18
  * - /packages/core/src/SideNav/SideNav.doc.mjs
@@ -23,7 +24,7 @@
23
24
  import { useCallback, useImperativeHandle, useRef } from 'react';
24
25
  import * as stylex from '@stylexjs/stylex';
25
26
  import "../theme/tokens.stylex.js";
26
- import { spacingVars } from "../theme/tokens.stylex.js";
27
+ import { focusVars, spacingVars } from "../theme/tokens.stylex.js";
27
28
  import { mergeProps } from "../utils/index.js";
28
29
  import { SideNavCollapseContext } from "./SideNavCollapseContext.js";
29
30
  import { SideNavCollapseButton } from "./SideNavCollapseButton.js";
@@ -100,6 +101,10 @@ const styles = {
100
101
  kzqmXN: "xhgvbfk",
101
102
  $$css: true
102
103
  },
104
+ resizableHandle: {
105
+ kInvED: "x1wfwxd8 x1l5jwag",
106
+ $$css: true
107
+ },
103
108
  topbar: {
104
109
  k1xSpc: "x78zum5",
105
110
  kXwgrk: "x1q0g3np",
@@ -400,7 +405,7 @@ export function SideNav({
400
405
  // the panel's overflow: clip bounds.
401
406
  const content = showResizeHandle ? /*#__PURE__*/_jsxs("div", {
402
407
  ...{
403
- className: "x1n2onr6 x78zum5 x2lah0s x5yr21d"
408
+ className: "x1n2onr6 x78zum5 x2lah0s x5yr21d x7giv3"
404
409
  },
405
410
  children: [navElement, /*#__PURE__*/_jsx(ResizeHandle, {
406
411
  "data-testid": "astryx-sidenav-resize-handle",
@@ -408,6 +413,7 @@ export function SideNav({
408
413
  position: "overlay",
409
414
  pillPlacement: "end",
410
415
  isAlwaysVisible: false,
416
+ xstyle: styles.resizableHandle,
411
417
  resizable: resizableHook.props,
412
418
  label: t('@astryx.sideNav.resizeSidebar')
413
419
  })]
package/dist/astryx.css CHANGED
@@ -1365,6 +1365,7 @@
1365
1365
  .xx47ajj:focus-visible:not(#\#):not(#\#):not(#\#){outline-color:var(--focus-outline-color)}
1366
1366
  .x7s97pk:focus-visible:not(#\#):not(#\#):not(#\#){outline-offset:2px}
1367
1367
  .x1nw7mhf:focus-visible:not(#\#):not(#\#):not(#\#){outline-offset:calc(-1 * var(--focus-outline-width))}
1368
+ .x1l5jwag:focus-visible:not(#\#):not(#\#):not(#\#){outline-offset:calc(0px - var(--focus-outline-width))}
1368
1369
  .x13aywxo:focus-visible:not(#\#):not(#\#):not(#\#){outline-offset:var(--button-focus-offset)}
1369
1370
  .x1vwwbsn:focus-visible:not(#\#):not(#\#):not(#\#){outline-offset:var(--focus-outline-offset)}
1370
1371
  .xwz5zln.xwz5zln:where(.x1iwu4tg:has(:focus-visible) *):not(#\#):not(#\#):not(#\#){outline-offset:var(--focus-outline-offset)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.6.2-canary.6203871",
3
+ "version": "0.6.2-canary.7cce14f",
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",
@@ -672,8 +672,8 @@
672
672
  "react-dom": ">=19.0.0"
673
673
  },
674
674
  "devDependencies": {
675
- "@astryxdesign/a11y-spec": "0.6.2-canary.6203871",
676
- "@astryxdesign/cli": "0.6.2-canary.6203871",
675
+ "@astryxdesign/a11y-spec": "0.6.2-canary.7cce14f",
676
+ "@astryxdesign/cli": "0.6.2-canary.7cce14f",
677
677
  "@babel/cli": "^7.29.7",
678
678
  "@babel/core": "^7.29.7",
679
679
  "@babel/preset-react": "^7.29.7",
@@ -21,7 +21,13 @@
21
21
  * ./Button.a11y.known-failures.ts, both shared with the jsdom lane.
22
22
  */
23
23
 
24
- import {expect, test, type CDPSession, type Page} from '@playwright/test';
24
+ import {
25
+ expect,
26
+ test,
27
+ type CDPSession,
28
+ type Locator,
29
+ type Page,
30
+ } from '@playwright/test';
25
31
  import {
26
32
  BUTTON_PATTERN,
27
33
  blockingResults,
@@ -93,6 +99,14 @@ async function activationsOn(page: Page): Promise<number> {
93
99
  return Number(raw ?? '0');
94
100
  }
95
101
 
102
+ function pointerTargetFor(
103
+ page: Page,
104
+ state: ButtonBindingRow,
105
+ ): Locator | undefined {
106
+ const selector = (state as ButtonBindingState).pointerTargetSelector;
107
+ return selector == null ? undefined : page.locator(selector).first();
108
+ }
109
+
96
110
  async function runState(
97
111
  page: Page,
98
112
  cdp: CDPSession,
@@ -111,6 +125,7 @@ async function runState(
111
125
  return createChromiumHarness({
112
126
  page,
113
127
  subject: page.locator('#storybook-root').getByRole('button').first(),
128
+ pointerTarget: pointerTargetFor(page, state),
114
129
  cdp,
115
130
  });
116
131
  },
@@ -306,6 +321,7 @@ test('every expectation is exercised by at least one bound state', async ({
306
321
  .locator('#storybook-root')
307
322
  .getByRole('button')
308
323
  .first(),
324
+ pointerTarget: pointerTargetFor(page, state),
309
325
  cdp,
310
326
  });
311
327
  },
@@ -83,33 +83,4 @@ export const BUTTON_KNOWN_FAILURES: ReadonlyArray<KnownFailure> = [
83
83
  userImpact: 'The same, from an icon button.',
84
84
  reason: 'Inherited from Button, as above.',
85
85
  },
86
-
87
- // ---- ClickableCard's role-bearing element cannot be clicked -------------
88
- {
89
- expectation: 'button.action.survives-an-aborted-press',
90
- binding: 'ClickableCard',
91
- state: 'clickable-card',
92
- evidenceLayer: 'real-browser',
93
- failureEquals:
94
- 'a pointer press cannot land on this control: something else is on top of it at its own centre, so there is no press here to abort',
95
- standardsReference: 'WCAG 2.2 2.5.2 Pointer Cancellation (Level A)',
96
- userImpact:
97
- 'The same defect seen from the other side: pointer cancellation cannot be demonstrated on a control no pointer press can land on. Recorded rather than reported green — a serene pass here would claim an outcome nobody observed.',
98
- reason:
99
- 'Same cause as the record above. It is a second record rather than a wider one because a known failure names exactly one outcome, and these two would be fixed and verified separately.',
100
- },
101
- {
102
- expectation: 'button.action.runs-on-pointer',
103
- binding: 'ClickableCard',
104
- state: 'clickable-card',
105
- evidenceLayer: 'real-browser',
106
- failureEquals:
107
- 'a pointer could not reach this control within 2000ms: the browser never found it visible, stable, and able to receive a pointer event. Something is covering it, or it is clipped to nothing.',
108
- standardsReference:
109
- 'Current Astryx buttons family FR2 and WAI-ARIA APG Button operability requirement; supports WCAG 2.2 4.1.2 Name, Role, Value (Level A)',
110
- userImpact:
111
- "Anyone whose tooling acts on the accessible object rather than on pixels — a speech-input user saying the card's name, or assistive technology dispatching a click at the element it is told is the button — aims at a 1×1 control the card's own content paints over. A sighted mouse user is unaffected: they click the card surface, which handles it.",
112
- reason:
113
- 'ClickableCard deliberately splits the surface that takes the click from the hidden button that carries the role and name. Reconciling the two is a design-system decision, not a change this contract should make.',
114
- },
115
86
  ];
@@ -88,12 +88,19 @@ export const BUTTON_STATE_RENDERS: Record<ButtonStateId, StateRender> = {
88
88
  ),
89
89
 
90
90
  'clickable-card': activate => (
91
- <ClickableCard label="Open billing settings" onClick={activate}>
91
+ <ClickableCard
92
+ data-a11y-pointer-target
93
+ label="Open billing settings"
94
+ onClick={activate}>
92
95
  <div>Billing</div>
93
96
  </ClickableCard>
94
97
  ),
95
98
  'clickable-card-disabled': activate => (
96
- <ClickableCard label="Open billing settings" isDisabled onClick={activate}>
99
+ <ClickableCard
100
+ data-a11y-pointer-target
101
+ label="Open billing settings"
102
+ isDisabled
103
+ onClick={activate}>
97
104
  <div>Billing</div>
98
105
  </ClickableCard>
99
106
  ),
@@ -92,6 +92,13 @@ export interface ButtonBindingState {
92
92
  * typo here is reported as a stale inventory, not as a standards failure.
93
93
  */
94
94
  readonly visibleLabel: string | null;
95
+ /**
96
+ * The surface that receives pointer input when it is not the role-bearing
97
+ * element. ClickableCard keeps its button/link hidden for role, name, and
98
+ * keyboard focus, and takes the pointer on the card surface; a binding that
99
+ * aimed the pointer at the hidden control would be testing the wrong surface.
100
+ */
101
+ readonly pointerTargetSelector?: string;
95
102
  /** The checked-in Storybook story the Chromium lane drives. */
96
103
  readonly storyId: string;
97
104
  /**
@@ -220,6 +227,7 @@ export const BUTTON_BINDING_STATES = [
220
227
  "a card whose whole surface performs an action, through the hidden button that carries the card's role and name",
221
228
  facts: facts(),
222
229
  visibleLabel: null,
230
+ pointerTargetSelector: '[data-a11y-pointer-target]',
223
231
  storyId: 'a11y-button-pattern--clickable-card-default',
224
232
  },
225
233
  {
@@ -228,6 +236,7 @@ export const BUTTON_BINDING_STATES = [
228
236
  summary: 'a disabled action card',
229
237
  facts: facts({operable: false, focusable: false, unavailable: true}),
230
238
  visibleLabel: null,
239
+ pointerTargetSelector: '[data-a11y-pointer-target]',
231
240
  storyId: 'a11y-button-pattern--clickable-card-disabled',
232
241
  },
233
242
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import {describe, it, expect, vi} from 'vitest';
4
4
  import {render, screen, fireEvent} from '@testing-library/react';
5
+ import userEvent from '@testing-library/user-event';
5
6
  import {ClickableCard} from './ClickableCard';
6
7
 
7
8
  describe('ClickableCard', () => {
@@ -47,6 +48,47 @@ describe('ClickableCard', () => {
47
48
  expect(handleClick).toHaveBeenCalledTimes(1);
48
49
  });
49
50
 
51
+ it('calls onClick once when the accessible control itself is clicked', () => {
52
+ const handleClick = vi.fn();
53
+ render(
54
+ <ClickableCard label="Test card" onClick={handleClick}>
55
+ <span>Content</span>
56
+ </ClickableCard>,
57
+ );
58
+ // Pointer activation aimed at the element carrying the role — what
59
+ // speech input, assistive technology, and automation dispatch.
60
+ fireEvent.click(screen.getByRole('button', {name: 'Test card'}));
61
+ expect(handleClick).toHaveBeenCalledTimes(1);
62
+ });
63
+
64
+ it('passes the card surface as currentTarget for surface clicks', () => {
65
+ const seen: EventTarget[] = [];
66
+ render(
67
+ <ClickableCard
68
+ label="Test card"
69
+ onClick={e => seen.push(e.currentTarget)}>
70
+ <span>Content</span>
71
+ </ClickableCard>,
72
+ );
73
+ fireEvent.click(screen.getByText('Content'));
74
+ expect(seen).toHaveLength(1);
75
+ expect(seen[0]).toBe(screen.getByText('Content').parentElement);
76
+ });
77
+
78
+ it('calls onClick exactly once when the surface of an href card is clicked without preventDefault', () => {
79
+ const handleClick = vi.fn();
80
+ render(
81
+ <ClickableCard label="Nav card" href="/settings" onClick={handleClick}>
82
+ <span>Content</span>
83
+ </ClickableCard>,
84
+ );
85
+ // The container hook proxies the surface click to the link with
86
+ // `link.click()`; that synthetic click bubbles back through the card and
87
+ // must not run the consumer callback a second time.
88
+ fireEvent.click(screen.getByText('Content'));
89
+ expect(handleClick).toHaveBeenCalledTimes(1);
90
+ });
91
+
50
92
  it('does NOT call onClick when a nested button is clicked', () => {
51
93
  const handleCardClick = vi.fn();
52
94
  const handleButtonClick = vi.fn();
@@ -86,25 +128,80 @@ describe('ClickableCard', () => {
86
128
  expect(link).toHaveAttribute('target', '_blank');
87
129
  });
88
130
 
89
- it('disabled button is disabled', () => {
131
+ it('disabled button is disabled and the surface runs nothing', () => {
90
132
  const handleClick = vi.fn();
91
133
  render(
92
134
  <ClickableCard label="Disabled" onClick={handleClick} isDisabled>
93
- Content
135
+ <span>Content</span>
94
136
  </ClickableCard>,
95
137
  );
96
138
  const button = screen.getByRole('button', {name: 'Disabled'});
97
139
  expect(button).toBeDisabled();
140
+ fireEvent.click(screen.getByText('Content'));
141
+ fireEvent.click(button);
142
+ expect(handleClick).not.toHaveBeenCalled();
98
143
  });
99
144
 
100
- it('disabled link has aria-disabled', () => {
145
+ it('disabled link has aria-disabled, leaves the tab order, and the surface runs nothing', () => {
146
+ const handleClick = vi.fn();
101
147
  render(
102
- <ClickableCard label="Disabled link" href="/settings" isDisabled>
103
- Content
148
+ <ClickableCard
149
+ label="Disabled link"
150
+ href="/settings"
151
+ onClick={handleClick}
152
+ isDisabled>
153
+ <span>Content</span>
104
154
  </ClickableCard>,
105
155
  );
106
156
  const link = screen.getByRole('link', {name: 'Disabled link'});
107
157
  expect(link).toHaveAttribute('aria-disabled', 'true');
158
+ expect(link).toHaveAttribute('tabindex', '-1');
159
+ fireEvent.click(screen.getByText('Content'));
160
+ expect(handleClick).not.toHaveBeenCalled();
161
+ });
162
+
163
+ it('runs onClick once from the keyboard via the hidden button', async () => {
164
+ const user = userEvent.setup();
165
+ const handleClick = vi.fn();
166
+ render(
167
+ <ClickableCard label="Test card" onClick={handleClick}>
168
+ <span>Content</span>
169
+ </ClickableCard>,
170
+ );
171
+ await user.tab();
172
+ expect(screen.getByRole('button', {name: 'Test card'})).toHaveFocus();
173
+ await user.keyboard('{Enter}');
174
+ await user.keyboard(' ');
175
+ expect(handleClick).toHaveBeenCalledTimes(2);
176
+ });
177
+
178
+ it('does NOT call onClick when a nested input is clicked', () => {
179
+ const handleCardClick = vi.fn();
180
+ render(
181
+ <ClickableCard label="Test card" onClick={handleCardClick}>
182
+ <input aria-label="Quantity" readOnly value="1" />
183
+ </ClickableCard>,
184
+ );
185
+ fireEvent.click(screen.getByRole('textbox', {name: 'Quantity'}));
186
+ expect(handleCardClick).not.toHaveBeenCalled();
187
+ });
188
+
189
+ it('does NOT call onClick when text inside the card is selected', () => {
190
+ const handleClick = vi.fn();
191
+ render(
192
+ <ClickableCard label="Test card" onClick={handleClick}>
193
+ <span>Selectable body text</span>
194
+ </ClickableCard>,
195
+ );
196
+ const text = screen.getByText('Selectable body text');
197
+ const range = document.createRange();
198
+ range.selectNodeContents(text);
199
+ const selection = document.getSelection()!;
200
+ selection.removeAllRanges();
201
+ selection.addRange(range);
202
+ fireEvent.click(text);
203
+ expect(handleClick).not.toHaveBeenCalled();
204
+ selection.removeAllRanges();
108
205
  });
109
206
 
110
207
  describe('elevation', () => {
@@ -27,7 +27,7 @@ export const docs = {
27
27
  name: 'keys',
28
28
  type: 'string',
29
29
  description:
30
- 'Keyboard shortcut string. Use "+" to separate keys. Special keys: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape, tab, up, down, left, right.',
30
+ 'Keyboard shortcut string. Use "+" to separate keys. Special keys: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape, tab, up, down, left, right, plus. Aliases: esc for escape and return for enter.',
31
31
  required: true,
32
32
  },
33
33
  {
@@ -72,7 +72,7 @@ export const docsZh = {
72
72
  name: 'keys',
73
73
  type: 'string',
74
74
  description:
75
- '键盘快捷键字符串。使用 "+" 分隔各按键。特殊按键:mod(Mac 上为 Cmd)、ctrl、alt、shift、enter、backspace、escape、tab、up、down、left、right。',
75
+ '键盘快捷键字符串。使用 "+" 分隔各按键。特殊按键:mod(Mac 上为 Cmd)、ctrl、alt、shift、enter、backspace、escape、tab、up、down、left、right、plus。别名:esc 等同于 escape,return 等同于 enter。',
76
76
  required: true,
77
77
  },
78
78
  {
@@ -122,7 +122,7 @@ export const docsDense = {
122
122
  ],
123
123
  },
124
124
  propDescriptions: {
125
- keys: 'Shortcut string. "+" separates keys. Special: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape, tab, up, down, left, right.',
125
+ keys: 'Shortcut string. "+" separates keys. Special: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape, tab, up, down, left, right, plus. Aliases: esc for escape, return for enter.',
126
126
  xstyle: 'StyleX styles for layout customization. Must be stylex.create() value.',
127
127
  className: 'CSS class for root element. Prefer xstyle; className for non-StyleX integration.',
128
128
  style: 'Inline styles for root element. Prefer xstyle; inline styles bypass StyleX optimization.',
@@ -10,13 +10,14 @@
10
10
  */
11
11
 
12
12
  import {render, screen} from '@testing-library/react';
13
- import {describe, it, expect, afterEach} from 'vitest';
13
+ import {describe, it, expect, afterEach, vi} from 'vitest';
14
14
  import {Kbd} from './Kbd';
15
15
 
16
16
  describe('Kbd', () => {
17
17
  const originalPlatform = navigator.platform;
18
18
 
19
19
  afterEach(() => {
20
+ vi.restoreAllMocks();
20
21
  // Restore platform after any test that spoofs it \u2014 ensures no
21
22
  // test pollution even if an assertion fails mid-test.
22
23
  Object.defineProperty(navigator, 'platform', {
@@ -72,6 +73,61 @@ describe('Kbd', () => {
72
73
  expect(screen.getByText('Esc')).toBeInTheDocument();
73
74
  });
74
75
 
76
+ it.each([
77
+ {alias: 'esc', display: 'Esc', label: 'Escape'},
78
+ {alias: 'return', display: '\u21B5', label: 'Enter'},
79
+ ])('normalizes the $alias key alias', ({alias, display, label}) => {
80
+ render(<Kbd keys={alias} />);
81
+ expect(screen.getByText(display)).toBeInTheDocument();
82
+ expect(screen.getByRole('img')).toHaveAttribute('aria-label', label);
83
+ });
84
+
85
+ it.each([
86
+ {alias: 'esc', canonical: 'escape'},
87
+ {alias: 'return', canonical: 'enter'},
88
+ ])('$alias renders identically to $canonical', ({alias, canonical}) => {
89
+ const {container, rerender} = render(<Kbd keys={canonical} />);
90
+ const text = container.textContent;
91
+ const label = screen.getByRole('img').getAttribute('aria-label');
92
+ rerender(<Kbd keys={alias} />);
93
+ expect(container.textContent).toBe(text);
94
+ expect(screen.getByRole('img').getAttribute('aria-label')).toBe(label);
95
+ });
96
+
97
+ it.each([
98
+ {keys: 'ctrl + ESC', display: 'Esc', label: 'Control + Escape'},
99
+ {keys: 'shift + RETURN', display: '\u21B5', label: 'Shift + Enter'},
100
+ ])('normalizes aliases inside the $keys combo', ({keys, display, label}) => {
101
+ render(<Kbd keys={keys} />);
102
+ expect(screen.getByText(display)).toBeInTheDocument();
103
+ expect(screen.getByRole('img')).toHaveAttribute('aria-label', label);
104
+ });
105
+
106
+ it('keeps child keys unique after alias normalization', () => {
107
+ const consoleError = vi
108
+ .spyOn(console, 'error')
109
+ .mockImplementation(() => {});
110
+ const {container} = render(
111
+ <Kbd keys="escape+esc+ESC+enter+return+return" />,
112
+ );
113
+ expect(consoleError).not.toHaveBeenCalled();
114
+ expect(
115
+ Array.from(container.querySelectorAll('kbd'), key => key.textContent),
116
+ ).toEqual(['Esc', 'Esc', 'Esc', '↵', '↵', '↵']);
117
+ });
118
+
119
+ it.each(['constructor', '__proto__'])(
120
+ 'renders the unknown %s key without consulting object prototypes',
121
+ key => {
122
+ render(<Kbd keys={key} />);
123
+ expect(screen.getByText(key.toUpperCase())).toBeInTheDocument();
124
+ expect(screen.getByRole('img')).toHaveAttribute(
125
+ 'aria-label',
126
+ key.toUpperCase(),
127
+ );
128
+ },
129
+ );
130
+
75
131
  it('exposes a spoken accessible name and hides the glyphs (obs-1)', () => {
76
132
  render(<Kbd keys="mod+shift+k" />);
77
133
  // The wrapper carries a screen-reader name built from spoken key labels
package/src/Kbd/Kbd.tsx CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  /**
6
6
  * @file Kbd.tsx
7
- * @input Uses React, StyleX, theme tokens
8
- * @output Exports Kbd component and KbdProps
7
+ * @input Uses React, StyleX, theme tokens, shortcut key names and aliases
8
+ * @output Exports Kbd component and KbdProps with canonical shortcut labels
9
9
  * @position Core implementation; renders styled keyboard shortcut indicators
10
10
  *
11
11
  * SYNC: When modified, update:
@@ -61,20 +61,25 @@ const styles = stylex.create({
61
61
  * Note: `mod` is not in this map — it resolves dynamically via platform
62
62
  * detection inside the component.
63
63
  */
64
- const KEY_DISPLAY: Record<string, string> = {
65
- ctrl: '\u2303', // ⌃
66
- alt: '\u2325', // ⌥
67
- shift: '\u21E7', // ⇧
68
- enter: '\u21B5', // ↵
69
- backspace: '\u232B', // ⌫
70
- escape: 'Esc',
71
- tab: '\u21E5', // ⇥
72
- up: '\u2191',
73
- down: '\u2193',
74
- left: '\u2190',
75
- right: '\u2192',
76
- plus: '+',
77
- };
64
+ const KEY_DISPLAY = new Map<string, string>([
65
+ ['ctrl', '\u2303'], // ⌃
66
+ ['alt', '\u2325'], // ⌥
67
+ ['shift', '\u21E7'], // ⇧
68
+ ['enter', '\u21B5'], // ↵
69
+ ['backspace', '\u232B'], // ⌫
70
+ ['escape', 'Esc'],
71
+ ['tab', '\u21E5'], // ⇥
72
+ ['up', '\u2191'],
73
+ ['down', '\u2193'],
74
+ ['left', '\u2190'],
75
+ ['right', '\u2192'],
76
+ ['plus', '+'],
77
+ ]);
78
+
79
+ const KBD_KEY_ALIASES = new Map<string, string>([
80
+ ['esc', 'escape'],
81
+ ['return', 'enter'],
82
+ ]);
78
83
 
79
84
  /**
80
85
  * Resolves a key name to its display string. Handles the platform-aware
@@ -85,7 +90,7 @@ function getKeyDisplay(key: string, isMac: boolean): string {
85
90
  if (key === 'mod') {
86
91
  return isMac ? '\u2318' : 'Ctrl';
87
92
  }
88
- return KEY_DISPLAY[key] ?? key.toUpperCase();
93
+ return KEY_DISPLAY.get(key) ?? key.toUpperCase();
89
94
  }
90
95
 
91
96
  /**
@@ -93,26 +98,26 @@ function getKeyDisplay(key: string, isMac: boolean): string {
93
98
  * (⌘, ⇧, ↵, …) that assistive tech cannot announce meaningfully, so the
94
99
  * accessible name for the shortcut is built from these words instead.
95
100
  */
96
- const KEY_LABEL: Record<string, string> = {
97
- ctrl: 'Control',
98
- alt: 'Alt',
99
- shift: 'Shift',
100
- enter: 'Enter',
101
- backspace: 'Backspace',
102
- escape: 'Escape',
103
- tab: 'Tab',
104
- up: 'Up arrow',
105
- down: 'Down arrow',
106
- left: 'Left arrow',
107
- right: 'Right arrow',
108
- plus: 'Plus',
109
- };
101
+ const KEY_LABEL = new Map<string, string>([
102
+ ['ctrl', 'Control'],
103
+ ['alt', 'Alt'],
104
+ ['shift', 'Shift'],
105
+ ['enter', 'Enter'],
106
+ ['backspace', 'Backspace'],
107
+ ['escape', 'Escape'],
108
+ ['tab', 'Tab'],
109
+ ['up', 'Up arrow'],
110
+ ['down', 'Down arrow'],
111
+ ['left', 'Left arrow'],
112
+ ['right', 'Right arrow'],
113
+ ['plus', 'Plus'],
114
+ ]);
110
115
 
111
116
  function getKeyLabel(key: string, isMac: boolean): string {
112
117
  if (key === 'mod') {
113
118
  return isMac ? 'Command' : 'Control';
114
119
  }
115
- return KEY_LABEL[key] ?? key.toUpperCase();
120
+ return KEY_LABEL.get(key) ?? key.toUpperCase();
116
121
  }
117
122
 
118
123
  function subscribeToPlatformChanges(): () => void {
@@ -127,7 +132,9 @@ export interface KbdProps extends BaseProps<HTMLSpanElement> {
127
132
  ref?: React.Ref<HTMLSpanElement>;
128
133
  /**
129
134
  * Keyboard shortcut string. Use "+" to separate keys.
130
- * Special keys: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape.
135
+ * Special keys: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape,
136
+ * tab, up, down, left, and right.
137
+ * Aliases: "esc" for "escape" and "return" for "enter".
131
138
  * Use "plus" to render a literal "+" key (e.g. "shift+plus").
132
139
  *
133
140
  * @example
@@ -163,11 +170,24 @@ export function Kbd({keys, ref, xstyle, className, style, ...rest}: KbdProps) {
163
170
  getServerPlatformSnapshot,
164
171
  );
165
172
 
166
- const parts = keys.split('+').map(key => key.trim().toLowerCase());
173
+ const keyOccurrences = new Map<string, number>();
174
+ const parts = keys
175
+ .split('+')
176
+ .map(key => key.trim().toLowerCase())
177
+ .map(sourceKey => {
178
+ const occurrence = keyOccurrences.get(sourceKey) ?? 0;
179
+ keyOccurrences.set(sourceKey, occurrence + 1);
180
+ return {
181
+ key: KBD_KEY_ALIASES.get(sourceKey) ?? sourceKey,
182
+ reactKey: `${sourceKey}:${occurrence}`,
183
+ };
184
+ });
167
185
 
168
186
  // Screen-reader name: the joined spoken labels (e.g. "Command + K"), since
169
187
  // the visual glyphs below are announced meaninglessly by assistive tech.
170
- const accessibleName = parts.map(key => getKeyLabel(key, isMac)).join(' + ');
188
+ const accessibleName = parts
189
+ .map(({key}) => getKeyLabel(key, isMac))
190
+ .join(' + ');
171
191
 
172
192
  return (
173
193
  <span
@@ -181,8 +201,8 @@ export function Kbd({keys, ref, xstyle, className, style, ...rest}: KbdProps) {
181
201
  className,
182
202
  style,
183
203
  )}>
184
- {parts.map(key => (
185
- <kbd key={key} aria-hidden="true" {...stylex.props(styles.kbd)}>
204
+ {parts.map(({key, reactKey}) => (
205
+ <kbd key={reactKey} aria-hidden="true" {...stylex.props(styles.kbd)}>
186
206
  {getKeyDisplay(key, isMac)}
187
207
  </kbd>
188
208
  ))}
@@ -25,7 +25,7 @@ export const docs = {
25
25
  {className: 'astryx-side-nav-section'},
26
26
  ],
27
27
  },
28
- description: 'Container with five zones: header, topContent, children (scrollable), footer, and footerIcons. Supports collapsible mode.',
28
+ description: 'Container with five zones: header, topContent, children (scrollable), footer, and footerIcons. Supports collapsible and resizable modes.',
29
29
  props: [
30
30
  {
31
31
  name: 'header',
@@ -989,6 +989,16 @@ describe('SideNav resizable', () => {
989
989
  ).toBeInTheDocument();
990
990
  });
991
991
 
992
+ it('clips the overlay drag handle within the resizable nav bounds', () => {
993
+ render(<SideNav resizable>Content</SideNav>);
994
+ const nav = screen.getByRole('navigation');
995
+ const container = nav.parentElement;
996
+ const handle = screen.getByTestId('astryx-sidenav-resize-handle');
997
+
998
+ expect(container).toContainElement(handle);
999
+ expect(getComputedStyle(container!).overflow).toBe('clip');
1000
+ });
1001
+
992
1002
  it('does not render drag handle without resizable', () => {
993
1003
  render(<SideNav>Content</SideNav>);
994
1004
  expect(
@@ -11,7 +11,8 @@
11
11
  * Sidebar navigation container with five zones: header + topContent (sticky together),
12
12
  * children (scrollable), footer, and footerIcons (sticky bottom).
13
13
  *
14
- * Supports optional resize via drag handle at the inline-end edge.
14
+ * Supports optional resize via a drag handle at the inline-end edge. The
15
+ * resizable wrapper clips handle overflow while keeping its focus ring inside.
15
16
  *
16
17
  * SYNC: When modified, update these files to stay in sync:
17
18
  * - /packages/core/src/SideNav/SideNav.doc.mjs
@@ -25,7 +26,7 @@ import {useCallback, useImperativeHandle, useRef, type ReactNode} from 'react';
25
26
  import type {BaseProps} from '../BaseProps';
26
27
  import * as stylex from '@stylexjs/stylex';
27
28
  import type {StyleXStyles} from '@stylexjs/stylex';
28
- import {spacingVars} from '../theme/tokens.stylex';
29
+ import {focusVars, spacingVars} from '../theme/tokens.stylex';
29
30
  import {mergeProps} from '../utils';
30
31
  import {
31
32
  SideNavCollapseContext,
@@ -241,6 +242,16 @@ const styles = stylex.create({
241
242
  display: 'flex',
242
243
  flexShrink: 0,
243
244
  height: '100%',
245
+ overflow: 'clip',
246
+ },
247
+ resizableHandle: {
248
+ // Keep the shared ring inside the clipping boundary. The wrapper cannot
249
+ // use overflow-clip-margin: that paint allowance also contributes the
250
+ // overlay's half-pixel spill to AppShell's scrollable overflow.
251
+ outlineOffset: {
252
+ default: '0',
253
+ ':focus-visible': `calc(0px - ${focusVars['--focus-outline-width']})`,
254
+ },
244
255
  },
245
256
  // Topbar mode — horizontal layout for mobile top bar
246
257
  topbar: {
@@ -658,6 +669,7 @@ export function SideNav({
658
669
  position="overlay"
659
670
  pillPlacement="end"
660
671
  isAlwaysVisible={false}
672
+ xstyle={styles.resizableHandle}
661
673
  resizable={resizableHook.props}
662
674
  label={t('@astryx.sideNav.resizeSidebar')}
663
675
  />