@jirawatpyk/aura-react 5.9.0 → 5.10.1

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/README.md CHANGED
@@ -186,6 +186,21 @@ DataTable with sort and page in the URL (4.17): `onStateChange={({ sort, page })
186
186
  - **Time zone**: `timeZone` on AuraProvider, DatePicker, DateRangePicker and Calendar decides "today" (the marker, `min`/`max="today"`, the first month shown); or pass `today` as an ISO date. `todayIn('Asia/Bangkok')` is exported from the root and `/server`.
187
187
  - **Toasts** queue past three instead of dropping: six in a row all show, in order, three at a time.
188
188
 
189
+ ## 5.10.1 — fixes from a new project
190
+
191
+ - **Switch**: a disabled switch that is on ("always on", enforced by policy) no longer fades into the same grey as an off one. Disabled rows, on or off, now look alike and stay readable: the label turns `aura-fg-secondary`, the description (often the reason it's locked) keeps its colour, the row shows a not-allowed cursor, and only the switch fades — off to the disabled opacity, on to 0.75, at least 3:1 apart in both themes. In forced colours a locked switch is `GrayText`. Use it instead of a Badge for settings people can see but not change.
192
+ - **Tag**: the remove button is named from the words inside any children — `<Tag onRemove={…}><strong>Acme</strong> AB</Tag>` is "Remove Acme AB" (was "Remove "). With no text at all (an icon only), a dev notice asks for `removeLabel`.
193
+ - **SideNav** (docs): an item's `badge` takes any element, so a fault or warning is a toned Badge — `badge: <Badge tone="danger">Fault</Badge>`; `count` stays a neutral number.
194
+
195
+ ## 5.10 — Chamber-OS addendum 10 (adopting 5.9)
196
+
197
+ - **Tabs `variant="segmented"`** (item 72): the tab list as a pill track with the selected tab raised — SegmentedControl's look — keeping every Tabs behaviour (tab / tabpanel roles, `keepMounted`, `activation`, `tabProps`). `fullWidth` stretches the track and shares it equally. Tabs are 44px on touch screens, the list doesn't scroll so the focus ring is never clipped, and in forced colours the selected tab gets a `Highlight` outline. Default (`underline`) output is unchanged.
198
+ ```tsx
199
+ <Tabs label="Payment method" variant="segmented" fullWidth keepMounted activation="manual" … />
200
+ ```
201
+ - **Disabled menu items stay reachable** (item 73): a disabled `MenuItem` is `aria-disabled` rather than a disabled button, so the arrow keys reach it and screen readers announce it as dimmed (WAI-ARIA APG); choosing it does nothing and keeps the menu open. `disabledReason` shows in place of the hint and is read with the item ("Again in 5 min"). A menu with nothing to focus takes focus itself, so Escape and Tab still close it and focus returns to the trigger. Enabled items are unchanged; tests that expected the arrows to skip a disabled item now land on it.
202
+ - **SegmentedControl boundary** (item 74): measured again — the selected pill's inset edge (`aura-border-control`, zinc-500) is 4.6:1 against the light track (zinc-50) and 5.8:1 in dark; CI now measures the rendered edge in both themes (≥3:1). A value near 2.5:1 is zinc-400, dark mode's `aura-border-control`: check that the light page isn't picking up the dark tokens (a `.dark` / `data-theme="dark"` ancestor, or a theme override).
203
+
189
204
  ## 5.9 — Chamber-OS addendum 9 (pay sheet)
190
205
 
191
206
  - **Drawer** (item 70): other attributes (`id`, `data-*`, `aria-*`) go on the `.aura-drawer` panel; `closeLabel` names the close button and its tooltip after what it closes ("Close payment drawer" — pass it in the page's language); `closeProps` puts attributes on that button (`{ 'data-testid': 'pay-sheet-close' }`; its `onClick` runs first and can `preventDefault()` to keep the drawer open). Without them the output is as before; focus trap, Escape, scrim and focus return are unchanged.
@@ -1233,13 +1233,14 @@ window.Aura = (() => {
1233
1233
  // src/Menu.tsx
1234
1234
  var React8 = __toESM(require_react(), 1);
1235
1235
  var import_react_dom = __toESM(require_react_dom(), 1);
1236
+ var ITEMS = '[role^="menuitem"]';
1236
1237
  var Menu = React8.forwardRef(function Menu2(props, ref) {
1237
1238
  const own = React8.useRef(null), merged = useMergedRef(ref, own);
1238
1239
  const posState = React8.useState(null);
1239
1240
  const pos = posState[0], setPos = posState[1];
1240
1241
  const items2 = props.items || [];
1241
1242
  const mounted = useMounted();
1242
- const headerId = uid();
1243
+ const headerId = uid(), itemId = uid();
1243
1244
  const hasHeader = props.header != null && props.header !== false && props.header !== "";
1244
1245
  const Link = useLinkComponent(props.linkComponent);
1245
1246
  useIsoLayoutEffect(
@@ -1267,8 +1268,8 @@ window.Aura = (() => {
1267
1268
  hadHeader.current = hasHeader;
1268
1269
  const a = document.activeElement;
1269
1270
  if (own.current && (!a || a === document.body)) {
1270
- const first = own.current.querySelector('[role^="menuitem"]:not([disabled])');
1271
- if (first) first.focus();
1271
+ const first = own.current.querySelector(ITEMS);
1272
+ (first || own.current.querySelector('[role="menu"]') || own.current).focus();
1272
1273
  }
1273
1274
  },
1274
1275
  [hasHeader]
@@ -1276,8 +1277,11 @@ window.Aura = (() => {
1276
1277
  React8.useEffect(
1277
1278
  function() {
1278
1279
  if (!mounted) return;
1279
- const first = own.current && own.current.querySelector('[role^="menuitem"]:not([disabled])');
1280
- if (first && props.autoFocus !== false) first.focus();
1280
+ const first = own.current && own.current.querySelector(ITEMS);
1281
+ if (props.autoFocus !== false) {
1282
+ if (first) first.focus();
1283
+ else if (own.current) (own.current.querySelector('[role="menu"]') || own.current).focus();
1284
+ }
1281
1285
  function outside(e) {
1282
1286
  if (own.current && !own.current.contains(e.target) && !(props.anchor && props.anchor.contains(e.target)))
1283
1287
  props.onClose(false);
@@ -1297,16 +1301,18 @@ window.Aura = (() => {
1297
1301
  [mounted]
1298
1302
  );
1299
1303
  function onKeyDown(e) {
1300
- const list2 = Array.prototype.slice.call(
1301
- own.current.querySelectorAll('[role^="menuitem"]:not([disabled])')
1302
- );
1304
+ const list2 = Array.prototype.slice.call(own.current.querySelectorAll(ITEMS));
1303
1305
  const i = list2.indexOf(document.activeElement);
1306
+ if (!list2.length && e.key !== "Escape" && e.key !== "Tab") {
1307
+ e.stopPropagation();
1308
+ return;
1309
+ }
1304
1310
  if (e.key === "ArrowDown") {
1305
1311
  e.preventDefault();
1306
1312
  list2[(i + 1) % list2.length].focus();
1307
1313
  } else if (e.key === "ArrowUp") {
1308
1314
  e.preventDefault();
1309
- list2[(i - 1 + list2.length) % list2.length].focus();
1315
+ list2[i < 0 ? list2.length - 1 : (i - 1 + list2.length) % list2.length].focus();
1310
1316
  } else if (e.key === "Home") {
1311
1317
  e.preventDefault();
1312
1318
  list2[0].focus();
@@ -1345,6 +1351,10 @@ window.Aura = (() => {
1345
1351
  it.hint ? /* @__PURE__ */ React8.createElement("span", { key: "h", className: "aura-menu__hint" }, it.hint) : null
1346
1352
  ];
1347
1353
  const cls = cx("aura-menu__item", it.tone === "danger" && "aura-menu__item--danger");
1354
+ const reasonId = it.disabled && it.disabledReason ? itemId + "-r" + i : void 0;
1355
+ if (reasonId)
1356
+ body[2] = /* Hidden from the name (it would be read twice); aria-describedby still reads it. */
1357
+ /* @__PURE__ */ React8.createElement("span", { key: "h", id: reasonId, className: "aura-menu__hint aura-menu__reason", "aria-hidden": true }, it.disabledReason);
1348
1358
  if (it.href && !it.disabled)
1349
1359
  return /* @__PURE__ */ React8.createElement(
1350
1360
  Link,
@@ -1369,11 +1379,13 @@ window.Aura = (() => {
1369
1379
  key: i,
1370
1380
  type: "button",
1371
1381
  tabIndex: -1,
1372
- disabled: it.disabled,
1382
+ "aria-disabled": it.disabled ? true : void 0,
1383
+ "aria-describedby": reasonId,
1373
1384
  role: isRadio ? "menuitemradio" : isCheck ? "menuitemcheckbox" : "menuitem",
1374
1385
  "aria-checked": isRadio || isCheck ? !!it.checked : void 0,
1375
1386
  className: cls,
1376
1387
  onClick: function() {
1388
+ if (it.disabled) return;
1377
1389
  if (it.onSelect) it.onSelect();
1378
1390
  if (!it.keepOpen) props.onClose(true);
1379
1391
  }
@@ -1402,7 +1414,19 @@ window.Aura = (() => {
1402
1414
  }
1403
1415
  },
1404
1416
  props.header
1405
- ), /* @__PURE__ */ React8.createElement("div", { role: "menu", "aria-label": props.label, "aria-describedby": headerId, className: "aura-menu__list" }, list)) : /* @__PURE__ */ React8.createElement("div", { ref: merged, role: "menu", "aria-label": props.label, className: "aura-menu", onKeyDown, style }, list);
1417
+ ), /* @__PURE__ */ React8.createElement("div", { role: "menu", tabIndex: -1, "aria-label": props.label, "aria-describedby": headerId, className: "aura-menu__list" }, list)) : /* @__PURE__ */ React8.createElement(
1418
+ "div",
1419
+ {
1420
+ ref: merged,
1421
+ role: "menu",
1422
+ tabIndex: -1,
1423
+ "aria-label": props.label,
1424
+ className: "aura-menu",
1425
+ onKeyDown,
1426
+ style
1427
+ },
1428
+ list
1429
+ );
1406
1430
  return mounted ? (0, import_react_dom.createPortal)(el, document.body) : null;
1407
1431
  });
1408
1432
 
@@ -1417,7 +1441,7 @@ window.Aura = (() => {
1417
1441
  function() {
1418
1442
  if (!anchor || !toLast.current || !menuRef.current) return;
1419
1443
  toLast.current = false;
1420
- const list = menuRef.current.querySelectorAll('[role^="menuitem"]:not([disabled])');
1444
+ const list = menuRef.current.querySelectorAll('[role^="menuitem"]');
1421
1445
  if (list.length) list[list.length - 1].focus();
1422
1446
  },
1423
1447
  [anchor]
@@ -3859,6 +3883,16 @@ window.Aura = (() => {
3859
3883
 
3860
3884
  // src/Tag.tsx
3861
3885
  var React24 = __toESM(require_react(), 1);
3886
+ function textOf(node) {
3887
+ if (node == null || typeof node === "boolean") return "";
3888
+ if (typeof node === "string" || typeof node === "number") return String(node);
3889
+ if (Array.isArray(node)) return node.map(textOf).join("");
3890
+ if (React24.isValidElement(node)) {
3891
+ const p = node.props;
3892
+ return p["aria-hidden"] === true || p["aria-hidden"] === "true" ? "" : textOf(p.children);
3893
+ }
3894
+ return "";
3895
+ }
3862
3896
  var Tag = React24.forwardRef(function Tag2(props, ref) {
3863
3897
  const t = useStrings();
3864
3898
  const selectable = props.onClick != null || props.selected != null;
@@ -3882,12 +3916,18 @@ window.Aura = (() => {
3882
3916
  inner[1]
3883
3917
  );
3884
3918
  }
3919
+ const name = props.onRemove && !props.removeLabel ? textOf(props.children).replace(/\s+/g, " ").trim() : "";
3920
+ if (props.onRemove && !props.disabled && !props.removeLabel && !name)
3921
+ devWarnOnce(
3922
+ "tag-remove-label",
3923
+ 'Tag: the remove button has no name \u2014 its children have no text. Pass removeLabel (e.g. "Remove Acme AB").'
3924
+ );
3885
3925
  return /* @__PURE__ */ React24.createElement("span", { ...rest, ref, className: cx("aura-tag", props.disabled && "is-disabled", props.className) }, inner, props.onRemove && !props.disabled ? /* @__PURE__ */ React24.createElement(
3886
3926
  "button",
3887
3927
  {
3888
3928
  type: "button",
3889
3929
  className: "aura-tag__remove",
3890
- "aria-label": props.removeLabel || t.remove(typeof props.children === "string" ? props.children : ""),
3930
+ "aria-label": props.removeLabel || t.remove(name),
3891
3931
  onClick: props.onRemove
3892
3932
  },
3893
3933
  /* @__PURE__ */ React24.createElement(Icon, { name: /* @__PURE__ */ React24.createElement(IconX, null), size: 12 })
@@ -5876,6 +5916,12 @@ window.Aura = (() => {
5876
5916
  })[0] || firstOn || items2[0];
5877
5917
  const Link = useLinkComponent(props.linkComponent);
5878
5918
  const manual = props.activation === "manual";
5919
+ const seg = props.variant === "segmented";
5920
+ const listCls = cx("aura-tabs__list", seg && "aura-segmented", seg && props.fullWidth && "is-full");
5921
+ const tabCls = function(on, extra) {
5922
+ return cx("aura-tab", on && "is-active", seg && "aura-segmented__option", seg && on && "is-selected", extra);
5923
+ };
5924
+ const rootCls = cx("aura-tabs", seg && "aura-tabs--segmented");
5879
5925
  const [focusId, setFocusId] = React32.useState(null);
5880
5926
  items2.forEach(function(t) {
5881
5927
  const al = t.tabProps && t.tabProps["aria-label"];
@@ -5891,9 +5937,9 @@ window.Aura = (() => {
5891
5937
  {
5892
5938
  ref,
5893
5939
  "aria-label": props.label,
5894
- className: cx("aura-tabs aura-tabs--links", props.className)
5940
+ className: cx(rootCls, "aura-tabs--links", props.className)
5895
5941
  },
5896
- /* @__PURE__ */ React32.createElement("div", { className: "aura-tabs__list" }, items2.map(function(t) {
5942
+ /* @__PURE__ */ React32.createElement("div", { className: listCls }, items2.map(function(t) {
5897
5943
  const on = t.id === cur;
5898
5944
  const inner = [
5899
5945
  t.icon ? /* @__PURE__ */ React32.createElement(Icon, { key: "i", name: t.icon }) : null,
@@ -5917,7 +5963,7 @@ window.Aura = (() => {
5917
5963
  {
5918
5964
  ...omit(own, ["onClick"]),
5919
5965
  key: t.id,
5920
- className: cx("aura-tab is-disabled", own.className),
5966
+ className: cx(tabCls(false), "is-disabled", own.className),
5921
5967
  "aria-disabled": true
5922
5968
  },
5923
5969
  inner
@@ -5927,7 +5973,7 @@ window.Aura = (() => {
5927
5973
  ...own,
5928
5974
  key: t.id,
5929
5975
  href: t.href,
5930
- className: cx("aura-tab", on && "is-active", own.className),
5976
+ className: tabCls(on, own.className),
5931
5977
  "aria-current": on ? "page" : void 0,
5932
5978
  onClick: function(e) {
5933
5979
  if (own.onClick) own.onClick(e);
@@ -5951,12 +5997,12 @@ window.Aura = (() => {
5951
5997
  const stop = manual && focusId && items2.filter(function(t) {
5952
5998
  return t.id === focusId && !t.disabled;
5953
5999
  })[0] || current2;
5954
- return /* @__PURE__ */ React32.createElement("div", { ref, className: cx("aura-tabs", props.className) }, /* @__PURE__ */ React32.createElement(
6000
+ return /* @__PURE__ */ React32.createElement("div", { ref, className: cx(rootCls, props.className) }, /* @__PURE__ */ React32.createElement(
5955
6001
  "div",
5956
6002
  {
5957
6003
  role: "tablist",
5958
6004
  "aria-label": props.label,
5959
- className: "aura-tabs__list",
6005
+ className: listCls,
5960
6006
  onBlur: manual ? function(e) {
5961
6007
  if (!e.currentTarget.contains(e.relatedTarget)) setFocusId(null);
5962
6008
  } : void 0,
@@ -5999,7 +6045,7 @@ window.Aura = (() => {
5999
6045
  ref: function(el) {
6000
6046
  refs.current[t.id] = el;
6001
6047
  },
6002
- className: cx("aura-tab", on && "is-active", own.className),
6048
+ className: tabCls(on, own.className),
6003
6049
  onFocus: manual ? function(e) {
6004
6050
  if (own.onFocus) own.onFocus(e);
6005
6051
  setFocusId(t.id);
@@ -1215,13 +1215,14 @@ var IconButton = React7.forwardRef(function IconButton2(props, ref) {
1215
1215
  // src/Menu.tsx
1216
1216
  var React8 = __toESM(require("react"), 1);
1217
1217
  var import_react_dom = require("react-dom");
1218
+ var ITEMS = '[role^="menuitem"]';
1218
1219
  var Menu = React8.forwardRef(function Menu2(props, ref) {
1219
1220
  const own = React8.useRef(null), merged = useMergedRef(ref, own);
1220
1221
  const posState = React8.useState(null);
1221
1222
  const pos = posState[0], setPos = posState[1];
1222
1223
  const items2 = props.items || [];
1223
1224
  const mounted = useMounted();
1224
- const headerId = uid();
1225
+ const headerId = uid(), itemId = uid();
1225
1226
  const hasHeader = props.header != null && props.header !== false && props.header !== "";
1226
1227
  const Link = useLinkComponent(props.linkComponent);
1227
1228
  useIsoLayoutEffect(
@@ -1249,8 +1250,8 @@ var Menu = React8.forwardRef(function Menu2(props, ref) {
1249
1250
  hadHeader.current = hasHeader;
1250
1251
  const a = document.activeElement;
1251
1252
  if (own.current && (!a || a === document.body)) {
1252
- const first = own.current.querySelector('[role^="menuitem"]:not([disabled])');
1253
- if (first) first.focus();
1253
+ const first = own.current.querySelector(ITEMS);
1254
+ (first || own.current.querySelector('[role="menu"]') || own.current).focus();
1254
1255
  }
1255
1256
  },
1256
1257
  [hasHeader]
@@ -1258,8 +1259,11 @@ var Menu = React8.forwardRef(function Menu2(props, ref) {
1258
1259
  React8.useEffect(
1259
1260
  function() {
1260
1261
  if (!mounted) return;
1261
- const first = own.current && own.current.querySelector('[role^="menuitem"]:not([disabled])');
1262
- if (first && props.autoFocus !== false) first.focus();
1262
+ const first = own.current && own.current.querySelector(ITEMS);
1263
+ if (props.autoFocus !== false) {
1264
+ if (first) first.focus();
1265
+ else if (own.current) (own.current.querySelector('[role="menu"]') || own.current).focus();
1266
+ }
1263
1267
  function outside(e) {
1264
1268
  if (own.current && !own.current.contains(e.target) && !(props.anchor && props.anchor.contains(e.target)))
1265
1269
  props.onClose(false);
@@ -1279,16 +1283,18 @@ var Menu = React8.forwardRef(function Menu2(props, ref) {
1279
1283
  [mounted]
1280
1284
  );
1281
1285
  function onKeyDown(e) {
1282
- const list2 = Array.prototype.slice.call(
1283
- own.current.querySelectorAll('[role^="menuitem"]:not([disabled])')
1284
- );
1286
+ const list2 = Array.prototype.slice.call(own.current.querySelectorAll(ITEMS));
1285
1287
  const i = list2.indexOf(document.activeElement);
1288
+ if (!list2.length && e.key !== "Escape" && e.key !== "Tab") {
1289
+ e.stopPropagation();
1290
+ return;
1291
+ }
1286
1292
  if (e.key === "ArrowDown") {
1287
1293
  e.preventDefault();
1288
1294
  list2[(i + 1) % list2.length].focus();
1289
1295
  } else if (e.key === "ArrowUp") {
1290
1296
  e.preventDefault();
1291
- list2[(i - 1 + list2.length) % list2.length].focus();
1297
+ list2[i < 0 ? list2.length - 1 : (i - 1 + list2.length) % list2.length].focus();
1292
1298
  } else if (e.key === "Home") {
1293
1299
  e.preventDefault();
1294
1300
  list2[0].focus();
@@ -1327,6 +1333,10 @@ var Menu = React8.forwardRef(function Menu2(props, ref) {
1327
1333
  it.hint ? /* @__PURE__ */ React8.createElement("span", { key: "h", className: "aura-menu__hint" }, it.hint) : null
1328
1334
  ];
1329
1335
  const cls = cx("aura-menu__item", it.tone === "danger" && "aura-menu__item--danger");
1336
+ const reasonId = it.disabled && it.disabledReason ? itemId + "-r" + i : void 0;
1337
+ if (reasonId)
1338
+ body[2] = /* Hidden from the name (it would be read twice); aria-describedby still reads it. */
1339
+ /* @__PURE__ */ React8.createElement("span", { key: "h", id: reasonId, className: "aura-menu__hint aura-menu__reason", "aria-hidden": true }, it.disabledReason);
1330
1340
  if (it.href && !it.disabled)
1331
1341
  return /* @__PURE__ */ React8.createElement(
1332
1342
  Link,
@@ -1351,11 +1361,13 @@ var Menu = React8.forwardRef(function Menu2(props, ref) {
1351
1361
  key: i,
1352
1362
  type: "button",
1353
1363
  tabIndex: -1,
1354
- disabled: it.disabled,
1364
+ "aria-disabled": it.disabled ? true : void 0,
1365
+ "aria-describedby": reasonId,
1355
1366
  role: isRadio ? "menuitemradio" : isCheck ? "menuitemcheckbox" : "menuitem",
1356
1367
  "aria-checked": isRadio || isCheck ? !!it.checked : void 0,
1357
1368
  className: cls,
1358
1369
  onClick: function() {
1370
+ if (it.disabled) return;
1359
1371
  if (it.onSelect) it.onSelect();
1360
1372
  if (!it.keepOpen) props.onClose(true);
1361
1373
  }
@@ -1384,7 +1396,19 @@ var Menu = React8.forwardRef(function Menu2(props, ref) {
1384
1396
  }
1385
1397
  },
1386
1398
  props.header
1387
- ), /* @__PURE__ */ React8.createElement("div", { role: "menu", "aria-label": props.label, "aria-describedby": headerId, className: "aura-menu__list" }, list)) : /* @__PURE__ */ React8.createElement("div", { ref: merged, role: "menu", "aria-label": props.label, className: "aura-menu", onKeyDown, style }, list);
1399
+ ), /* @__PURE__ */ React8.createElement("div", { role: "menu", tabIndex: -1, "aria-label": props.label, "aria-describedby": headerId, className: "aura-menu__list" }, list)) : /* @__PURE__ */ React8.createElement(
1400
+ "div",
1401
+ {
1402
+ ref: merged,
1403
+ role: "menu",
1404
+ tabIndex: -1,
1405
+ "aria-label": props.label,
1406
+ className: "aura-menu",
1407
+ onKeyDown,
1408
+ style
1409
+ },
1410
+ list
1411
+ );
1388
1412
  return mounted ? (0, import_react_dom.createPortal)(el, document.body) : null;
1389
1413
  });
1390
1414
 
@@ -1399,7 +1423,7 @@ var DropdownMenu = React9.forwardRef(function DropdownMenu2(props, ref) {
1399
1423
  function() {
1400
1424
  if (!anchor || !toLast.current || !menuRef.current) return;
1401
1425
  toLast.current = false;
1402
- const list = menuRef.current.querySelectorAll('[role^="menuitem"]:not([disabled])');
1426
+ const list = menuRef.current.querySelectorAll('[role^="menuitem"]');
1403
1427
  if (list.length) list[list.length - 1].focus();
1404
1428
  },
1405
1429
  [anchor]
@@ -3841,6 +3865,16 @@ var React25 = __toESM(require("react"), 1);
3841
3865
 
3842
3866
  // src/Tag.tsx
3843
3867
  var React24 = __toESM(require("react"), 1);
3868
+ function textOf(node) {
3869
+ if (node == null || typeof node === "boolean") return "";
3870
+ if (typeof node === "string" || typeof node === "number") return String(node);
3871
+ if (Array.isArray(node)) return node.map(textOf).join("");
3872
+ if (React24.isValidElement(node)) {
3873
+ const p = node.props;
3874
+ return p["aria-hidden"] === true || p["aria-hidden"] === "true" ? "" : textOf(p.children);
3875
+ }
3876
+ return "";
3877
+ }
3844
3878
  var Tag = React24.forwardRef(function Tag2(props, ref) {
3845
3879
  const t = useStrings();
3846
3880
  const selectable = props.onClick != null || props.selected != null;
@@ -3864,12 +3898,18 @@ var Tag = React24.forwardRef(function Tag2(props, ref) {
3864
3898
  inner[1]
3865
3899
  );
3866
3900
  }
3901
+ const name = props.onRemove && !props.removeLabel ? textOf(props.children).replace(/\s+/g, " ").trim() : "";
3902
+ if (props.onRemove && !props.disabled && !props.removeLabel && !name)
3903
+ devWarnOnce(
3904
+ "tag-remove-label",
3905
+ 'Tag: the remove button has no name \u2014 its children have no text. Pass removeLabel (e.g. "Remove Acme AB").'
3906
+ );
3867
3907
  return /* @__PURE__ */ React24.createElement("span", { ...rest, ref, className: cx("aura-tag", props.disabled && "is-disabled", props.className) }, inner, props.onRemove && !props.disabled ? /* @__PURE__ */ React24.createElement(
3868
3908
  "button",
3869
3909
  {
3870
3910
  type: "button",
3871
3911
  className: "aura-tag__remove",
3872
- "aria-label": props.removeLabel || t.remove(typeof props.children === "string" ? props.children : ""),
3912
+ "aria-label": props.removeLabel || t.remove(name),
3873
3913
  onClick: props.onRemove
3874
3914
  },
3875
3915
  /* @__PURE__ */ React24.createElement(Icon, { name: /* @__PURE__ */ React24.createElement(IconX, null), size: 12 })
@@ -5858,6 +5898,12 @@ var Tabs = React32.forwardRef(function Tabs2(props, ref) {
5858
5898
  })[0] || firstOn || items2[0];
5859
5899
  const Link = useLinkComponent(props.linkComponent);
5860
5900
  const manual = props.activation === "manual";
5901
+ const seg = props.variant === "segmented";
5902
+ const listCls = cx("aura-tabs__list", seg && "aura-segmented", seg && props.fullWidth && "is-full");
5903
+ const tabCls = function(on, extra) {
5904
+ return cx("aura-tab", on && "is-active", seg && "aura-segmented__option", seg && on && "is-selected", extra);
5905
+ };
5906
+ const rootCls = cx("aura-tabs", seg && "aura-tabs--segmented");
5861
5907
  const [focusId, setFocusId] = React32.useState(null);
5862
5908
  items2.forEach(function(t) {
5863
5909
  const al = t.tabProps && t.tabProps["aria-label"];
@@ -5873,9 +5919,9 @@ var Tabs = React32.forwardRef(function Tabs2(props, ref) {
5873
5919
  {
5874
5920
  ref,
5875
5921
  "aria-label": props.label,
5876
- className: cx("aura-tabs aura-tabs--links", props.className)
5922
+ className: cx(rootCls, "aura-tabs--links", props.className)
5877
5923
  },
5878
- /* @__PURE__ */ React32.createElement("div", { className: "aura-tabs__list" }, items2.map(function(t) {
5924
+ /* @__PURE__ */ React32.createElement("div", { className: listCls }, items2.map(function(t) {
5879
5925
  const on = t.id === cur;
5880
5926
  const inner = [
5881
5927
  t.icon ? /* @__PURE__ */ React32.createElement(Icon, { key: "i", name: t.icon }) : null,
@@ -5899,7 +5945,7 @@ var Tabs = React32.forwardRef(function Tabs2(props, ref) {
5899
5945
  {
5900
5946
  ...omit(own, ["onClick"]),
5901
5947
  key: t.id,
5902
- className: cx("aura-tab is-disabled", own.className),
5948
+ className: cx(tabCls(false), "is-disabled", own.className),
5903
5949
  "aria-disabled": true
5904
5950
  },
5905
5951
  inner
@@ -5909,7 +5955,7 @@ var Tabs = React32.forwardRef(function Tabs2(props, ref) {
5909
5955
  ...own,
5910
5956
  key: t.id,
5911
5957
  href: t.href,
5912
- className: cx("aura-tab", on && "is-active", own.className),
5958
+ className: tabCls(on, own.className),
5913
5959
  "aria-current": on ? "page" : void 0,
5914
5960
  onClick: function(e) {
5915
5961
  if (own.onClick) own.onClick(e);
@@ -5933,12 +5979,12 @@ var Tabs = React32.forwardRef(function Tabs2(props, ref) {
5933
5979
  const stop = manual && focusId && items2.filter(function(t) {
5934
5980
  return t.id === focusId && !t.disabled;
5935
5981
  })[0] || current2;
5936
- return /* @__PURE__ */ React32.createElement("div", { ref, className: cx("aura-tabs", props.className) }, /* @__PURE__ */ React32.createElement(
5982
+ return /* @__PURE__ */ React32.createElement("div", { ref, className: cx(rootCls, props.className) }, /* @__PURE__ */ React32.createElement(
5937
5983
  "div",
5938
5984
  {
5939
5985
  role: "tablist",
5940
5986
  "aria-label": props.label,
5941
- className: "aura-tabs__list",
5987
+ className: listCls,
5942
5988
  onBlur: manual ? function(e) {
5943
5989
  if (!e.currentTarget.contains(e.relatedTarget)) setFocusId(null);
5944
5990
  } : void 0,
@@ -5981,7 +6027,7 @@ var Tabs = React32.forwardRef(function Tabs2(props, ref) {
5981
6027
  ref: function(el) {
5982
6028
  refs.current[t.id] = el;
5983
6029
  },
5984
- className: cx("aura-tab", on && "is-active", own.className),
6030
+ className: tabCls(on, own.className),
5985
6031
  onFocus: manual ? function(e) {
5986
6032
  if (own.onFocus) own.onFocus(e);
5987
6033
  setFocusId(t.id);
@@ -11,7 +11,7 @@ const DropdownMenu = React.forwardRef(function DropdownMenu2(props, ref) {
11
11
  function() {
12
12
  if (!anchor || !toLast.current || !menuRef.current) return;
13
13
  toLast.current = false;
14
- const list = menuRef.current.querySelectorAll('[role^="menuitem"]:not([disabled])');
14
+ const list = menuRef.current.querySelectorAll('[role^="menuitem"]');
15
15
  if (list.length) list[list.length - 1].focus();
16
16
  },
17
17
  [anchor]
package/dist/esm/Menu.js CHANGED
@@ -5,13 +5,14 @@ import { cx, uid, useIsoLayoutEffect, useMounted, useMergedRef } from "./interna
5
5
  import { Icon } from "./Icon.js";
6
6
  import { useLinkComponent } from "./locale.js";
7
7
  import { IconCheck } from "./icons.js";
8
+ const ITEMS = '[role^="menuitem"]';
8
9
  const Menu = React.forwardRef(function Menu2(props, ref) {
9
10
  const own = React.useRef(null), merged = useMergedRef(ref, own);
10
11
  const posState = React.useState(null);
11
12
  const pos = posState[0], setPos = posState[1];
12
13
  const items = props.items || [];
13
14
  const mounted = useMounted();
14
- const headerId = uid();
15
+ const headerId = uid(), itemId = uid();
15
16
  const hasHeader = props.header != null && props.header !== false && props.header !== "";
16
17
  const Link = useLinkComponent(props.linkComponent);
17
18
  useIsoLayoutEffect(
@@ -39,8 +40,8 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
39
40
  hadHeader.current = hasHeader;
40
41
  const a = document.activeElement;
41
42
  if (own.current && (!a || a === document.body)) {
42
- const first = own.current.querySelector('[role^="menuitem"]:not([disabled])');
43
- if (first) first.focus();
43
+ const first = own.current.querySelector(ITEMS);
44
+ (first || own.current.querySelector('[role="menu"]') || own.current).focus();
44
45
  }
45
46
  },
46
47
  [hasHeader]
@@ -48,8 +49,11 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
48
49
  React.useEffect(
49
50
  function() {
50
51
  if (!mounted) return;
51
- const first = own.current && own.current.querySelector('[role^="menuitem"]:not([disabled])');
52
- if (first && props.autoFocus !== false) first.focus();
52
+ const first = own.current && own.current.querySelector(ITEMS);
53
+ if (props.autoFocus !== false) {
54
+ if (first) first.focus();
55
+ else if (own.current) (own.current.querySelector('[role="menu"]') || own.current).focus();
56
+ }
53
57
  function outside(e) {
54
58
  if (own.current && !own.current.contains(e.target) && !(props.anchor && props.anchor.contains(e.target)))
55
59
  props.onClose(false);
@@ -69,16 +73,18 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
69
73
  [mounted]
70
74
  );
71
75
  function onKeyDown(e) {
72
- const list2 = Array.prototype.slice.call(
73
- own.current.querySelectorAll('[role^="menuitem"]:not([disabled])')
74
- );
76
+ const list2 = Array.prototype.slice.call(own.current.querySelectorAll(ITEMS));
75
77
  const i = list2.indexOf(document.activeElement);
78
+ if (!list2.length && e.key !== "Escape" && e.key !== "Tab") {
79
+ e.stopPropagation();
80
+ return;
81
+ }
76
82
  if (e.key === "ArrowDown") {
77
83
  e.preventDefault();
78
84
  list2[(i + 1) % list2.length].focus();
79
85
  } else if (e.key === "ArrowUp") {
80
86
  e.preventDefault();
81
- list2[(i - 1 + list2.length) % list2.length].focus();
87
+ list2[i < 0 ? list2.length - 1 : (i - 1 + list2.length) % list2.length].focus();
82
88
  } else if (e.key === "Home") {
83
89
  e.preventDefault();
84
90
  list2[0].focus();
@@ -117,6 +123,10 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
117
123
  it.hint ? /* @__PURE__ */ React.createElement("span", { key: "h", className: "aura-menu__hint" }, it.hint) : null
118
124
  ];
119
125
  const cls = cx("aura-menu__item", it.tone === "danger" && "aura-menu__item--danger");
126
+ const reasonId = it.disabled && it.disabledReason ? itemId + "-r" + i : void 0;
127
+ if (reasonId)
128
+ body[2] = /* Hidden from the name (it would be read twice); aria-describedby still reads it. */
129
+ /* @__PURE__ */ React.createElement("span", { key: "h", id: reasonId, className: "aura-menu__hint aura-menu__reason", "aria-hidden": true }, it.disabledReason);
120
130
  if (it.href && !it.disabled)
121
131
  return /* @__PURE__ */ React.createElement(
122
132
  Link,
@@ -141,11 +151,13 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
141
151
  key: i,
142
152
  type: "button",
143
153
  tabIndex: -1,
144
- disabled: it.disabled,
154
+ "aria-disabled": it.disabled ? true : void 0,
155
+ "aria-describedby": reasonId,
145
156
  role: isRadio ? "menuitemradio" : isCheck ? "menuitemcheckbox" : "menuitem",
146
157
  "aria-checked": isRadio || isCheck ? !!it.checked : void 0,
147
158
  className: cls,
148
159
  onClick: function() {
160
+ if (it.disabled) return;
149
161
  if (it.onSelect) it.onSelect();
150
162
  if (!it.keepOpen) props.onClose(true);
151
163
  }
@@ -174,7 +186,19 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
174
186
  }
175
187
  },
176
188
  props.header
177
- ), /* @__PURE__ */ React.createElement("div", { role: "menu", "aria-label": props.label, "aria-describedby": headerId, className: "aura-menu__list" }, list)) : /* @__PURE__ */ React.createElement("div", { ref: merged, role: "menu", "aria-label": props.label, className: "aura-menu", onKeyDown, style }, list);
189
+ ), /* @__PURE__ */ React.createElement("div", { role: "menu", tabIndex: -1, "aria-label": props.label, "aria-describedby": headerId, className: "aura-menu__list" }, list)) : /* @__PURE__ */ React.createElement(
190
+ "div",
191
+ {
192
+ ref: merged,
193
+ role: "menu",
194
+ tabIndex: -1,
195
+ "aria-label": props.label,
196
+ className: "aura-menu",
197
+ onKeyDown,
198
+ style
199
+ },
200
+ list
201
+ );
178
202
  return mounted ? createPortal(el, document.body) : null;
179
203
  });
180
204
  export {
package/dist/esm/Tabs.js CHANGED
@@ -23,6 +23,12 @@ const Tabs = React.forwardRef(function Tabs2(props, ref) {
23
23
  })[0] || firstOn || items[0];
24
24
  const Link = useLinkComponent(props.linkComponent);
25
25
  const manual = props.activation === "manual";
26
+ const seg = props.variant === "segmented";
27
+ const listCls = cx("aura-tabs__list", seg && "aura-segmented", seg && props.fullWidth && "is-full");
28
+ const tabCls = function(on, extra) {
29
+ return cx("aura-tab", on && "is-active", seg && "aura-segmented__option", seg && on && "is-selected", extra);
30
+ };
31
+ const rootCls = cx("aura-tabs", seg && "aura-tabs--segmented");
26
32
  const [focusId, setFocusId] = React.useState(null);
27
33
  items.forEach(function(t) {
28
34
  const al = t.tabProps && t.tabProps["aria-label"];
@@ -38,9 +44,9 @@ const Tabs = React.forwardRef(function Tabs2(props, ref) {
38
44
  {
39
45
  ref,
40
46
  "aria-label": props.label,
41
- className: cx("aura-tabs aura-tabs--links", props.className)
47
+ className: cx(rootCls, "aura-tabs--links", props.className)
42
48
  },
43
- /* @__PURE__ */ React.createElement("div", { className: "aura-tabs__list" }, items.map(function(t) {
49
+ /* @__PURE__ */ React.createElement("div", { className: listCls }, items.map(function(t) {
44
50
  const on = t.id === cur;
45
51
  const inner = [
46
52
  t.icon ? /* @__PURE__ */ React.createElement(Icon, { key: "i", name: t.icon }) : null,
@@ -64,7 +70,7 @@ const Tabs = React.forwardRef(function Tabs2(props, ref) {
64
70
  {
65
71
  ...omit(own, ["onClick"]),
66
72
  key: t.id,
67
- className: cx("aura-tab is-disabled", own.className),
73
+ className: cx(tabCls(false), "is-disabled", own.className),
68
74
  "aria-disabled": true
69
75
  },
70
76
  inner
@@ -74,7 +80,7 @@ const Tabs = React.forwardRef(function Tabs2(props, ref) {
74
80
  ...own,
75
81
  key: t.id,
76
82
  href: t.href,
77
- className: cx("aura-tab", on && "is-active", own.className),
83
+ className: tabCls(on, own.className),
78
84
  "aria-current": on ? "page" : void 0,
79
85
  onClick: function(e) {
80
86
  if (own.onClick) own.onClick(e);
@@ -98,12 +104,12 @@ const Tabs = React.forwardRef(function Tabs2(props, ref) {
98
104
  const stop = manual && focusId && items.filter(function(t) {
99
105
  return t.id === focusId && !t.disabled;
100
106
  })[0] || current;
101
- return /* @__PURE__ */ React.createElement("div", { ref, className: cx("aura-tabs", props.className) }, /* @__PURE__ */ React.createElement(
107
+ return /* @__PURE__ */ React.createElement("div", { ref, className: cx(rootCls, props.className) }, /* @__PURE__ */ React.createElement(
102
108
  "div",
103
109
  {
104
110
  role: "tablist",
105
111
  "aria-label": props.label,
106
- className: "aura-tabs__list",
112
+ className: listCls,
107
113
  onBlur: manual ? function(e) {
108
114
  if (!e.currentTarget.contains(e.relatedTarget)) setFocusId(null);
109
115
  } : void 0,
@@ -146,7 +152,7 @@ const Tabs = React.forwardRef(function Tabs2(props, ref) {
146
152
  ref: function(el) {
147
153
  refs.current[t.id] = el;
148
154
  },
149
- className: cx("aura-tab", on && "is-active", own.className),
155
+ className: tabCls(on, own.className),
150
156
  onFocus: manual ? function(e) {
151
157
  if (own.onFocus) own.onFocus(e);
152
158
  setFocusId(t.id);
package/dist/esm/Tag.js CHANGED
@@ -1,9 +1,19 @@
1
1
  'use client';
2
2
  import * as React from "react";
3
3
  import { Icon } from "./Icon.js";
4
- import { cx, omit } from "./internal.js";
4
+ import { cx, devWarnOnce, omit } from "./internal.js";
5
5
  import { useStrings } from "./locale.js";
6
6
  import { IconCheck, IconX } from "./icons.js";
7
+ function textOf(node) {
8
+ if (node == null || typeof node === "boolean") return "";
9
+ if (typeof node === "string" || typeof node === "number") return String(node);
10
+ if (Array.isArray(node)) return node.map(textOf).join("");
11
+ if (React.isValidElement(node)) {
12
+ const p = node.props;
13
+ return p["aria-hidden"] === true || p["aria-hidden"] === "true" ? "" : textOf(p.children);
14
+ }
15
+ return "";
16
+ }
7
17
  const Tag = React.forwardRef(function Tag2(props, ref) {
8
18
  const t = useStrings();
9
19
  const selectable = props.onClick != null || props.selected != null;
@@ -27,12 +37,18 @@ const Tag = React.forwardRef(function Tag2(props, ref) {
27
37
  inner[1]
28
38
  );
29
39
  }
40
+ const name = props.onRemove && !props.removeLabel ? textOf(props.children).replace(/\s+/g, " ").trim() : "";
41
+ if (props.onRemove && !props.disabled && !props.removeLabel && !name)
42
+ devWarnOnce(
43
+ "tag-remove-label",
44
+ 'Tag: the remove button has no name \u2014 its children have no text. Pass removeLabel (e.g. "Remove Acme AB").'
45
+ );
30
46
  return /* @__PURE__ */ React.createElement("span", { ...rest, ref, className: cx("aura-tag", props.disabled && "is-disabled", props.className) }, inner, props.onRemove && !props.disabled ? /* @__PURE__ */ React.createElement(
31
47
  "button",
32
48
  {
33
49
  type: "button",
34
50
  className: "aura-tag__remove",
35
- "aria-label": props.removeLabel || t.remove(typeof props.children === "string" ? props.children : ""),
51
+ "aria-label": props.removeLabel || t.remove(name),
36
52
  onClick: props.onRemove
37
53
  },
38
54
  /* @__PURE__ */ React.createElement(Icon, { name: /* @__PURE__ */ React.createElement(IconX, null), size: 12 })
package/dist/index.d.cts CHANGED
@@ -278,7 +278,10 @@ export interface MenuItem {
278
278
  icon?: IconInput | undefined;
279
279
  /** Present = a checkable item (menuitemcheckbox). */
280
280
  checked?: boolean | undefined;
281
+ /** Dimmed and does nothing when chosen, but still reachable with the arrow keys and announced (5.10). */
281
282
  disabled?: boolean | undefined;
283
+ /** Why it's disabled ("You can resend in 5 minutes"): shown in place of the hint and read with the item. (5.10) */
284
+ disabledReason?: string | undefined;
282
285
  /** Keep the menu open after choosing (checkbox lists). */
283
286
  keepOpen?: boolean | undefined;
284
287
  /** Short right-aligned hint, e.g. a shortcut. */
@@ -617,6 +620,11 @@ export interface TabsProps {
617
620
  /** `auto` (default): arrow keys select as they move. `manual`: arrows, Home and End only move focus; Enter, Space
618
621
  * or a click selects — for tabs whose selection does work (loads data, starts a payment). (5.9) */
619
622
  activation?: "auto" | "manual" | undefined;
623
+ /** `underline` (default) or `segmented`: the tabs as a pill track with the selected one raised, like
624
+ * SegmentedControl, keeping every Tabs behaviour (panels, keepMounted, activation, tabProps). (5.10, Chamber-OS 72) */
625
+ variant?: "underline" | "segmented" | undefined;
626
+ /** With `variant="segmented"`: stretch the track to the full width, tabs sharing it equally. (5.10) */
627
+ fullWidth?: boolean | undefined;
620
628
  }
621
629
  export interface NavItem {
622
630
  id: string;
package/dist/index.d.ts CHANGED
@@ -278,7 +278,10 @@ export interface MenuItem {
278
278
  icon?: IconInput | undefined;
279
279
  /** Present = a checkable item (menuitemcheckbox). */
280
280
  checked?: boolean | undefined;
281
+ /** Dimmed and does nothing when chosen, but still reachable with the arrow keys and announced (5.10). */
281
282
  disabled?: boolean | undefined;
283
+ /** Why it's disabled ("You can resend in 5 minutes"): shown in place of the hint and read with the item. (5.10) */
284
+ disabledReason?: string | undefined;
282
285
  /** Keep the menu open after choosing (checkbox lists). */
283
286
  keepOpen?: boolean | undefined;
284
287
  /** Short right-aligned hint, e.g. a shortcut. */
@@ -617,6 +620,11 @@ export interface TabsProps {
617
620
  /** `auto` (default): arrow keys select as they move. `manual`: arrows, Home and End only move focus; Enter, Space
618
621
  * or a click selects — for tabs whose selection does work (loads data, starts a payment). (5.9) */
619
622
  activation?: "auto" | "manual" | undefined;
623
+ /** `underline` (default) or `segmented`: the tabs as a pill track with the selected one raised, like
624
+ * SegmentedControl, keeping every Tabs behaviour (panels, keepMounted, activation, tabProps). (5.10, Chamber-OS 72) */
625
+ variant?: "underline" | "segmented" | undefined;
626
+ /** With `variant="segmented"`: stretch the track to the full width, tabs sharing it equally. (5.10) */
627
+ fullWidth?: boolean | undefined;
620
628
  }
621
629
  export interface NavItem {
622
630
  id: string;
package/dist/styles.css CHANGED
@@ -226,7 +226,13 @@ a.aura-btn { text-decoration: none; }
226
226
  }
227
227
  .aura-menu__item:hover, .aura-menu__item:focus { background: var(--aura-bg-surface-hover); outline: none; }
228
228
  .aura-menu__item:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: calc(-1 * var(--aura-focus-ring-width)); }
229
- .aura-menu__item:disabled { opacity: var(--aura-disabled-opacity); cursor: default; background: transparent; }
229
+ .aura-menu__item:disabled { opacity: var(--aura-disabled-opacity); cursor: default; }
230
+ /* 5.10: a disabled item is dimmed but focusable; its reason stays at full strength (it is the information). */
231
+ .aura-menu__item[aria-disabled="true"] { cursor: default; }
232
+ .aura-menu__item[aria-disabled="true"] > :not(.aura-menu__reason) { opacity: var(--aura-disabled-opacity); }
233
+ .aura-menu__reason { padding-left: var(--aura-space-4); }
234
+ .aura-menu__item:disabled, .aura-menu__item[aria-disabled="true"]:hover:not(:focus) { background: transparent; }
235
+ .aura-menu:focus, .aura-menu__list:focus { outline: none; }
230
236
  .aura-menu__item .aura-icon { color: var(--aura-fg-secondary); }
231
237
  .aura-menu__label { flex: 1; }
232
238
  .aura-menu__blank { width: var(--aura-icon-sm); flex: none; }
@@ -460,7 +466,13 @@ a.aura-icon-btn { text-decoration: none; }
460
466
  .aura-radio-group.is-invalid .aura-radio:not(:checked) { border-color: var(--aura-border-danger); }
461
467
 
462
468
  .aura-switch-row { display: flex; align-items: flex-start; gap: var(--aura-space-3); font-family: var(--font-sans); }
463
- .aura-switch-row.is-disabled { opacity: var(--aura-disabled-opacity); }
469
+ /* 5.10.1: disabled rows, on or off, are treated alike and stay readable — the label turns secondary, the description
470
+ * keeps its colour (often the reason it's locked) — and only the switch fades: off to the disabled opacity, on to 0.75,
471
+ * ≥3:1 apart in both themes, so "locked on" never looks like "off". */
472
+ .aura-switch-row.is-disabled { cursor: not-allowed; }
473
+ .aura-switch-row.is-disabled .aura-choice__label { color: var(--aura-fg-secondary); cursor: not-allowed; }
474
+ .aura-switch:disabled { opacity: var(--aura-disabled-opacity); }
475
+ .aura-switch.is-on:disabled { opacity: 0.75; }
464
476
  .aura-switch {
465
477
  position: relative; flex: none; box-sizing: border-box; margin: 0; padding: 0;
466
478
  width: var(--aura-switch-width); height: var(--aura-switch-height); border-radius: var(--aura-radius-full);
@@ -1203,6 +1215,16 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
1203
1215
  .aura-segmented--sm .aura-segmented__option { height: 24px; padding: 0 var(--aura-space-3); font-size: 12px; }
1204
1216
  .aura-segmented--sm .aura-segmented__option.is-icon { width: 28px; padding: 0; }
1205
1217
  .aura-segmented.is-disabled { opacity: var(--aura-disabled-opacity); }
1218
+ /* 5.10 (Chamber-OS 72): Tabs variant="segmented" — the tablist wears SegmentedControl's track and pills. The focus
1219
+ * ring is drawn inside the pill, so neither the scrolling list (many tabs on a phone) nor the next pill can cover it;
1220
+ * full-width tabs share the track but never shrink below their label (the track scrolls instead). The phone rule's
1221
+ * wider tab gap doesn't apply. */
1222
+ .aura-tabs--segmented .aura-tabs__list.aura-segmented { gap: 2px; }
1223
+ .aura-tabs--segmented .aura-tabs__list.aura-segmented.is-full .aura-tab { min-width: max-content; }
1224
+ .aura-tabs--segmented .aura-tab.aura-segmented__option { margin-bottom: 0; }
1225
+ .aura-tabs--segmented .aura-tab.aura-segmented__option:focus-visible { border-radius: var(--aura-radius-full); outline-offset: calc(-1 * var(--aura-focus-ring-width)); }
1226
+ .aura-tabs--segmented .aura-tab.aura-segmented__option:disabled,
1227
+ .aura-tabs--segmented .aura-tab.aura-segmented__option.is-disabled { opacity: var(--aura-disabled-opacity); cursor: not-allowed; }
1206
1228
 
1207
1229
  /* A field inside a row Stack takes the remaining width (a search box beside its button). */
1208
1230
  .aura-stack > .aura-field { flex: 1 1 auto; min-width: 0; }
@@ -1585,9 +1607,18 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
1585
1607
  .aura-switch__thumb { background: CanvasText; forced-color-adjust: none; }
1586
1608
  .aura-switch.is-on { background: Highlight; forced-color-adjust: none; }
1587
1609
  .aura-switch.is-on .aura-switch__thumb { background: HighlightText; }
1610
+ .aura-switch:disabled, .aura-switch.is-on:disabled { border-color: GrayText; opacity: 1; }
1611
+ .aura-switch:disabled .aura-switch__thumb { background: GrayText; }
1612
+ .aura-switch.is-on:disabled { background: GrayText; }
1613
+ .aura-switch.is-on:disabled .aura-switch__thumb { background: Canvas; }
1614
+ .aura-switch-row.is-disabled .aura-choice__label { color: GrayText; }
1588
1615
  .aura-progress__track, .aura-upload__bar { border: 1px solid CanvasText; }
1589
1616
  .aura-progress__bar, .aura-upload__bar > span { background: Highlight; forced-color-adjust: none; }
1590
1617
  .aura-segmented__option.is-selected { outline: 2px solid Highlight; outline-offset: -2px; }
1591
1618
  .aura-tab { border-bottom-color: Canvas; }
1592
1619
  .aura-tab.is-active { border-bottom-color: Highlight; }
1620
+ /* 5.10: segmented tabs mark the selection with a border and focus with a dashed ring inside it, so a focused tab
1621
+ * and the selected one never look alike (manual activation moves focus without selecting). */
1622
+ .aura-tabs--segmented .aura-tab.aura-segmented__option.is-selected { outline: none; border: 2px solid Highlight; }
1623
+ .aura-tabs--segmented .aura-tab.aura-segmented__option:focus-visible { outline: 2px dashed CanvasText; outline-offset: -5px; }
1593
1624
  }
@@ -228,7 +228,13 @@ a.aura-btn { text-decoration: none; }
228
228
  }
229
229
  .aura-menu__item:hover, .aura-menu__item:focus { background: var(--aura-bg-surface-hover); outline: none; }
230
230
  .aura-menu__item:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: calc(-1 * var(--aura-focus-ring-width)); }
231
- .aura-menu__item:disabled { opacity: var(--aura-disabled-opacity); cursor: default; background: transparent; }
231
+ .aura-menu__item:disabled { opacity: var(--aura-disabled-opacity); cursor: default; }
232
+ /* 5.10: a disabled item is dimmed but focusable; its reason stays at full strength (it is the information). */
233
+ .aura-menu__item[aria-disabled="true"] { cursor: default; }
234
+ .aura-menu__item[aria-disabled="true"] > :not(.aura-menu__reason) { opacity: var(--aura-disabled-opacity); }
235
+ .aura-menu__reason { padding-left: var(--aura-space-4); }
236
+ .aura-menu__item:disabled, .aura-menu__item[aria-disabled="true"]:hover:not(:focus) { background: transparent; }
237
+ .aura-menu:focus, .aura-menu__list:focus { outline: none; }
232
238
  .aura-menu__item .aura-icon { color: var(--aura-fg-secondary); }
233
239
  .aura-menu__label { flex: 1; }
234
240
  .aura-menu__blank { width: var(--aura-icon-sm); flex: none; }
@@ -462,7 +468,13 @@ a.aura-icon-btn { text-decoration: none; }
462
468
  .aura-radio-group.is-invalid .aura-radio:not(:checked) { border-color: var(--aura-border-danger); }
463
469
 
464
470
  .aura-switch-row { display: flex; align-items: flex-start; gap: var(--aura-space-3); font-family: var(--font-sans); }
465
- .aura-switch-row.is-disabled { opacity: var(--aura-disabled-opacity); }
471
+ /* 5.10.1: disabled rows, on or off, are treated alike and stay readable — the label turns secondary, the description
472
+ * keeps its colour (often the reason it's locked) — and only the switch fades: off to the disabled opacity, on to 0.75,
473
+ * ≥3:1 apart in both themes, so "locked on" never looks like "off". */
474
+ .aura-switch-row.is-disabled { cursor: not-allowed; }
475
+ .aura-switch-row.is-disabled .aura-choice__label { color: var(--aura-fg-secondary); cursor: not-allowed; }
476
+ .aura-switch:disabled { opacity: var(--aura-disabled-opacity); }
477
+ .aura-switch.is-on:disabled { opacity: 0.75; }
466
478
  .aura-switch {
467
479
  position: relative; flex: none; box-sizing: border-box; margin: 0; padding: 0;
468
480
  width: var(--aura-switch-width); height: var(--aura-switch-height); border-radius: var(--aura-radius-full);
@@ -1205,6 +1217,16 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
1205
1217
  .aura-segmented--sm .aura-segmented__option { height: 24px; padding: 0 var(--aura-space-3); font-size: 12px; }
1206
1218
  .aura-segmented--sm .aura-segmented__option.is-icon { width: 28px; padding: 0; }
1207
1219
  .aura-segmented.is-disabled { opacity: var(--aura-disabled-opacity); }
1220
+ /* 5.10 (Chamber-OS 72): Tabs variant="segmented" — the tablist wears SegmentedControl's track and pills. The focus
1221
+ * ring is drawn inside the pill, so neither the scrolling list (many tabs on a phone) nor the next pill can cover it;
1222
+ * full-width tabs share the track but never shrink below their label (the track scrolls instead). The phone rule's
1223
+ * wider tab gap doesn't apply. */
1224
+ .aura-tabs--segmented .aura-tabs__list.aura-segmented { gap: 2px; }
1225
+ .aura-tabs--segmented .aura-tabs__list.aura-segmented.is-full .aura-tab { min-width: max-content; }
1226
+ .aura-tabs--segmented .aura-tab.aura-segmented__option { margin-bottom: 0; }
1227
+ .aura-tabs--segmented .aura-tab.aura-segmented__option:focus-visible { border-radius: var(--aura-radius-full); outline-offset: calc(-1 * var(--aura-focus-ring-width)); }
1228
+ .aura-tabs--segmented .aura-tab.aura-segmented__option:disabled,
1229
+ .aura-tabs--segmented .aura-tab.aura-segmented__option.is-disabled { opacity: var(--aura-disabled-opacity); cursor: not-allowed; }
1208
1230
 
1209
1231
  /* A field inside a row Stack takes the remaining width (a search box beside its button). */
1210
1232
  .aura-stack > .aura-field { flex: 1 1 auto; min-width: 0; }
@@ -1587,11 +1609,20 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
1587
1609
  .aura-switch__thumb { background: CanvasText; forced-color-adjust: none; }
1588
1610
  .aura-switch.is-on { background: Highlight; forced-color-adjust: none; }
1589
1611
  .aura-switch.is-on .aura-switch__thumb { background: HighlightText; }
1612
+ .aura-switch:disabled, .aura-switch.is-on:disabled { border-color: GrayText; opacity: 1; }
1613
+ .aura-switch:disabled .aura-switch__thumb { background: GrayText; }
1614
+ .aura-switch.is-on:disabled { background: GrayText; }
1615
+ .aura-switch.is-on:disabled .aura-switch__thumb { background: Canvas; }
1616
+ .aura-switch-row.is-disabled .aura-choice__label { color: GrayText; }
1590
1617
  .aura-progress__track, .aura-upload__bar { border: 1px solid CanvasText; }
1591
1618
  .aura-progress__bar, .aura-upload__bar > span { background: Highlight; forced-color-adjust: none; }
1592
1619
  .aura-segmented__option.is-selected { outline: 2px solid Highlight; outline-offset: -2px; }
1593
1620
  .aura-tab { border-bottom-color: Canvas; }
1594
1621
  .aura-tab.is-active { border-bottom-color: Highlight; }
1622
+ /* 5.10: segmented tabs mark the selection with a border and focus with a dashed ring inside it, so a focused tab
1623
+ * and the selected one never look alike (manual activation moves focus without selecting). */
1624
+ .aura-tabs--segmented .aura-tab.aura-segmented__option.is-selected { outline: none; border: 2px solid Highlight; }
1625
+ .aura-tabs--segmented .aura-tab.aura-segmented__option:focus-visible { outline: 2px dashed CanvasText; outline-offset: -5px; }
1595
1626
  }
1596
1627
 
1597
1628
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jirawatpyk/aura-react",
3
- "version": "5.9.0",
3
+ "version": "5.10.1",
4
4
  "description": "AURA Design System — React components (Enterprise Standard, Human Creative). Thai-first: Buddhist-era dates, Thai collation.",
5
5
  "license": "MIT",
6
6
  "type": "module",