@jirawatpyk/aura-react 5.49.0 → 5.51.0

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,32 @@ 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.51 — Chamber-OS addenda 58–64 (#157–163)
190
+
191
+ - **DataTable column `sortDirection`** (#157): `'ascending' | 'descending'` — for a list the page orders (from the URL, a line under the table), puts `aria-sort` on that column's header with no sort button, icon or click, in the server's HTML too. Give it to one column (two warn in development); move it when the order changes. Once the table has its own sort (a sortable header was used, or `sort` is set) that wins and `sortDirection` is ignored, so one header at most claims the order.
192
+ - **DataTable `selectAllRef`** (#157): a ref to the select-all checkbox, e.g. to return focus there after a bulk bar's Clear.
193
+ - **The row minimum counts each flexible column's `minWidth`** (#159), the value its cells already used, instead of 160px for every one — eight columns with minimums of 80–132px now fit a 959px box without a sideways scroll. Columns without `minWidth` still count 160.
194
+ - **`StatusPill wrap`** (#158): in a box narrower than the words they wrap inside the pill (the icon beside the first line, the pill's own line height, Thai's too), and its corners are `aura-radius-lg` (16px) instead of full (addendum 61): on one line that still gives fully round ends, so it looks exactly as before; wrapped it is a rounded rectangle rather than a tall lozenge instead of running out of it — for a narrow DataTable column or a stacked card. In a wide box, or without `wrap`, a pill is one line as before.
195
+ - **Toggle Tags among FilterBar's controls show their whole label** (#160): from 640px a selectable Tag inside FilterBar isn't cut at 24ch, only when wider than the bar, and in the Filters sheet it never is; below 640px in the bar itself, and for a Tag outside FilterBar, the cap and its tip stay. Applied-filter chips were already uncapped (5.28).
196
+ - **The wheel over a DataTable scrolls the page again** (#163): `.aura-table__scroll` contains only the sideways axis (`overscroll-behavior-x: contain`), so a vertical wheel over a grid that scrolls sideways scrolls the page, a sideways swipe stays in the grid (no back navigation), and a fixed-`height` grid scrolls inside first, then the page at its end. Remove any `overscroll-behavior-y` override you added for it.
197
+ - **DataTable column `grow`** (#162): a flexible column's share of the spare width (its cells' flex-grow; default 1), e.g. `{ key: 'actions', minWidth: 128, grow: 3 }` puts Approve and Reject side by side once the table is wide. At the row minimum nothing changes, the row minimum ignores it, and fixed-`width` columns and stacked cards ignore it. `grow: 0` keeps a column at its minimum; leave at least one flexible column above 0 or the rows won't fill a wide table.
198
+ - **DataTable `keyboard="tab"`** (#161): every checkbox, sort and column button, row link and row action is an ordinary Tab stop in reading order (Shift+Tab walks back), each with its focus ring, in the grid and in stacked cards — for lists whose rows carry actions people expect to Tab to. The cells stop being Tab stops; the arrows still move between them once focus is in one (click a cell, or Escape from a control), and the grid's row and column semantics stay. Without it (`'grid'`, the default) the table is one Tab stop as before. Keyboard resizing and reordering of columns (Alt/Ctrl+Shift with the arrows on a header cell) need focus on that header cell, so they stay with the grid keyboard.
199
+ - **5.50 follow-up:** a disabled box whose control has `aria-invalid="true"` keeps its disabled edge inside an enabled custom `Field` too.
200
+
201
+ ```jsx
202
+ <DataTable columns={[{ key: 'timeInStage', label: 'Time in stage', minWidth: 132, sortDirection: 'descending' }, …]} selectAllRef={selectAll} … />
203
+ <StatusPill tone="progress" wrap>Awaiting marketing review</StatusPill>
204
+ ```
205
+
206
+ ## 5.50 — Chamber-OS addendum 57 (#156)
207
+
208
+ - **`invalid` on TextField and Textarea** (and PasswordField, which is a TextField; `Field` for a custom control): marks the value invalid without AURA's message — the danger edge (`--aura-border-danger`, light and dark, also on hover and focus) and `aria-invalid="true"` on the control, no error line, and `aria-describedby` as it would be without it (the addon, the hint, then your own ids). For a page that explains the problem itself — a live contrast readout, a reason line a disabled Save points at. Point `aria-describedby` at that explanation, or a screen reader hears "invalid" with no reason. `error` still wins: its message replaces the hint and its id the hint's. A disabled field keeps its disabled edge.
209
+ - **Your own `aria-invalid="true"` is enough too**: a field whose input or textarea carries it gets the same edge, so a custom control inside `Field` (with `invalid` on the Field for the class) or a form library that sets it needs nothing else.
210
+
211
+ ```jsx
212
+ <TextField label="Primary colour" hint="A hex value." value={colour} onChange={…} invalid={!meetsAa} aria-describedby="contrast-readout" />
213
+ ```
214
+
189
215
  ## 5.49 — Chamber-OS addendum 56 (#155)
190
216
 
191
217
  - **The short-screen threshold follows the browser's text size**: the height at which bottom bars stop pinning (5.48, #154) is `30rem`, not `480px`. In a media query `rem` is the browser's default font size (Chrome, Edge, Firefox; not iOS Dynamic Type or Safari's text size), not the page's `html` font size: 480px at the default 16px (nothing changes), 720px at 24px (an iPhone SE counts as short, an 844px phone keeps the bar), 960px at 32px (an 844px phone counts as short — it holds what a 422px screen holds at 16px). There the `actionsBarBelow` actions stay in the header, an ActionBar sits at the end of its container and AppShell's top bar scrolls away, as at 400% zoom. Changing the text size on an open page re-checks it (the bar-height tokens follow). The width breakpoints stay in px.
@@ -684,8 +684,17 @@ window.Aura = (() => {
684
684
  };
685
685
  function statusPillElement(props, ref) {
686
686
  const tone2 = props.tone || toneFor(props.children);
687
- const rest = omit(props, ["tone", "className", "children"]);
688
- return /* @__PURE__ */ React4.createElement("span", { ...rest, ref, className: cx("aura-pill", "aura-pill--" + tone2, props.className) }, /* @__PURE__ */ React4.createElement(Icon, { name: h2(PILL_ICON[tone2] || IconCircle), size: 12 }), props.children);
687
+ const rest = omit(props, ["tone", "className", "children", "wrap"]);
688
+ return /* @__PURE__ */ React4.createElement(
689
+ "span",
690
+ {
691
+ ...rest,
692
+ ref,
693
+ className: cx("aura-pill", "aura-pill--" + tone2, props.wrap && "is-wrap", props.className)
694
+ },
695
+ /* @__PURE__ */ React4.createElement(Icon, { name: h2(PILL_ICON[tone2] || IconCircle), size: 12 }),
696
+ props.wrap ? /* @__PURE__ */ React4.createElement("span", { className: "aura-pill__text" }, props.children) : props.children
697
+ );
689
698
  }
690
699
  function badgeElement(props, ref) {
691
700
  const rest = omit(props, ["tone", "variant", "icon", "className", "children"]);
@@ -1992,7 +2001,12 @@ window.Aura = (() => {
1992
2001
  "div",
1993
2002
  {
1994
2003
  ref,
1995
- className: cx("aura-field", props.error && "is-invalid", props.disabled && "is-disabled", props.className)
2004
+ className: cx(
2005
+ "aura-field",
2006
+ (props.error || props.invalid) && "is-invalid",
2007
+ props.disabled && "is-disabled",
2008
+ props.className
2009
+ )
1996
2010
  },
1997
2011
  props.label ? h3(
1998
2012
  props.labelAs || "label",
@@ -2037,7 +2051,8 @@ window.Aura = (() => {
2037
2051
  "placeholder",
2038
2052
  "touchHeight",
2039
2053
  "labelAddon",
2040
- "labelAddonDescribes"
2054
+ "labelAddonDescribes",
2055
+ "invalid"
2041
2056
  ];
2042
2057
 
2043
2058
  // src/TextField.tsx
@@ -2052,6 +2067,7 @@ window.Aura = (() => {
2052
2067
  label: props.label,
2053
2068
  hint: props.hint,
2054
2069
  error: props.error,
2070
+ invalid: props.invalid,
2055
2071
  required: props.required,
2056
2072
  optional: props.optional,
2057
2073
  labelAddon: props.labelAddon,
@@ -2068,7 +2084,7 @@ window.Aura = (() => {
2068
2084
  className: "aura-input__control",
2069
2085
  placeholder: props.placeholder,
2070
2086
  required: props.required,
2071
- "aria-invalid": props.error ? true : rest["aria-invalid"],
2087
+ "aria-invalid": props.error || props.invalid ? true : rest["aria-invalid"],
2072
2088
  "aria-describedby": joinDescribedBy(id, props, rest["aria-describedby"])
2073
2089
  }
2074
2090
  ), props.suffix ? /* @__PURE__ */ React14.createElement("span", { className: "aura-input__suffix" }, props.suffix) : null)
@@ -2087,6 +2103,7 @@ window.Aura = (() => {
2087
2103
  label: props.label,
2088
2104
  hint: props.hint,
2089
2105
  error: props.error,
2106
+ invalid: props.invalid,
2090
2107
  required: props.required,
2091
2108
  optional: props.optional,
2092
2109
  labelAddon: props.labelAddon,
@@ -2103,7 +2120,7 @@ window.Aura = (() => {
2103
2120
  className: "aura-input aura-textarea",
2104
2121
  placeholder: props.placeholder,
2105
2122
  required: props.required,
2106
- "aria-invalid": props.error ? true : rest["aria-invalid"],
2123
+ "aria-invalid": props.error || props.invalid ? true : rest["aria-invalid"],
2107
2124
  "aria-describedby": joinDescribedBy(id, props, rest["aria-describedby"])
2108
2125
  }
2109
2126
  )
@@ -5751,6 +5768,8 @@ window.Aura = (() => {
5751
5768
  const refreshing = busy && manual && rows.length > 0;
5752
5769
  const loading = busy && !refreshing;
5753
5770
  const selectable = !!props.selectable;
5771
+ const tabMode = props.keyboard === "tab";
5772
+ const tabIdx = tabMode ? void 0 : -1;
5754
5773
  const reorderable = props.reorderable !== false && !!props.columnControls;
5755
5774
  const controls = !!props.columnControls;
5756
5775
  const oneCallback = !!props.onStateChange;
@@ -5943,7 +5962,7 @@ window.Aura = (() => {
5943
5962
  const pending = React33.useRef(null);
5944
5963
  const resizing = React33.useRef(false);
5945
5964
  useIsoLayoutEffect(function() {
5946
- if (!gridRef.current) return;
5965
+ if (!gridRef.current || tabMode) return;
5947
5966
  const els = gridRef.current.querySelectorAll(
5948
5967
  '.aura-table__td button, .aura-table__td a[href], .aura-table__td input, .aura-table__td select, .aura-table__td textarea, .aura-table__td [tabindex="0"]'
5949
5968
  );
@@ -6027,7 +6046,7 @@ window.Aura = (() => {
6027
6046
  });
6028
6047
  let fixedSum = 0, hideTerms = "";
6029
6048
  vis.forEach(function(c) {
6030
- const w = widthOf(c) != null ? widthOf(c) : FLEX_MIN, lv = hideLevel(c);
6049
+ const w = widthOf(c) != null ? widthOf(c) : c.minWidth || FLEX_MIN, lv = hideLevel(c);
6031
6050
  if (lv && boxWidth[0] == null) hideTerms += " + " + w + "px * var(--aura-h" + lv + "-on, 1)";
6032
6051
  else fixedSum += w;
6033
6052
  });
@@ -6039,13 +6058,21 @@ window.Aura = (() => {
6039
6058
  acc += " + " + widthOf(c) + "px" + (lv && boxWidth[0] == null ? " * var(--aura-h" + lv + "-on, 1)" : "");
6040
6059
  }
6041
6060
  });
6061
+ if (vis.filter(function(c) {
6062
+ return !!c.sortDirection;
6063
+ }).length > 1)
6064
+ devWarnOnce(
6065
+ "datatable-sortdirection-twice",
6066
+ "DataTable: more than one column has sortDirection. The order follows one column; give it to that one."
6067
+ );
6042
6068
  const selW = selectable ? " + var(--aura-table-select-width)" : "";
6043
6069
  const gutterR = controls ? "var(--aura-space-12)" : "var(--aura-space-6)";
6044
6070
  const rowMinWidth = "calc(" + fixedSum + "px" + hideTerms + " + var(--aura-space-6) + " + gutterR + selW + ")";
6045
6071
  function cellStyle(c, i) {
6046
6072
  const w = widthOf(c), s = {};
6047
6073
  if (w == null) {
6048
- s["--aura-cell-flex"] = "1 1 0";
6074
+ const g = typeof c.grow === "number" && c.grow >= 0 && isFinite(c.grow) ? c.grow : 1;
6075
+ s["--aura-cell-flex"] = g + " 1 0";
6049
6076
  s["--aura-cell-min"] = (c.minWidth || FLEX_MIN) + "px";
6050
6077
  } else {
6051
6078
  s["--aura-cell-flex"] = !hasFlex && i === vis.length - 1 ? "1 0 auto" : "none";
@@ -6222,6 +6249,7 @@ window.Aura = (() => {
6222
6249
  [stacked]
6223
6250
  );
6224
6251
  function tabFor(r2, c) {
6252
+ if (tabMode) return -1;
6225
6253
  if (!activeRendered) return r2 === 0 && c === ac ? 0 : -1;
6226
6254
  return r2 === ar && c === acShown ? 0 : -1;
6227
6255
  }
@@ -6244,7 +6272,7 @@ window.Aura = (() => {
6244
6272
  const el = gridRef.current.querySelector('[data-rc="' + p.r + ":" + c + '"]');
6245
6273
  if (el) {
6246
6274
  pending.current = null;
6247
- el.focus();
6275
+ (p.menuBtn && el.querySelector(".aura-table__menu-btn") || el).focus();
6248
6276
  }
6249
6277
  });
6250
6278
  function focusCell(r2, c, dir) {
@@ -6520,7 +6548,7 @@ window.Aura = (() => {
6520
6548
  const m = menu;
6521
6549
  setMenu(null);
6522
6550
  if (restore && m) {
6523
- if (m.kind === "col") pending.current = { r: 0, key: m.key, c: m.rc ? m.rc.c : 0 };
6551
+ if (m.kind === "col") pending.current = { r: 0, key: m.key, c: m.rc ? m.rc.c : 0, menuBtn: tabMode };
6524
6552
  else if (m.anchor) m.anchor.focus();
6525
6553
  }
6526
6554
  }
@@ -6608,10 +6636,11 @@ window.Aura = (() => {
6608
6636
  pageKeys.length ? /* @__PURE__ */ React33.createElement(
6609
6637
  Checkbox,
6610
6638
  {
6639
+ ref: props.selectAllRef,
6611
6640
  hideLabel: true,
6612
6641
  checked: all,
6613
6642
  indeterminate: nSel > 0 && !all,
6614
- tabIndex: -1,
6643
+ tabIndex: tabIdx,
6615
6644
  label: all ? t.deselectAllRows : t.selectAllRows,
6616
6645
  onChange: toggleAll
6617
6646
  }
@@ -6623,7 +6652,8 @@ window.Aura = (() => {
6623
6652
  const ci = i + (selectable ? 1 : 0);
6624
6653
  const isSorted = sort && sort.key === c.key;
6625
6654
  const canSort = c.sortable && canSortAny;
6626
- const ariaSort = isSorted && canSort ? sort.dir === "desc" ? "descending" : "ascending" : canSort ? "none" : void 0;
6655
+ const own = c.sortDirection === "ascending" || c.sortDirection === "descending" ? c.sortDirection : void 0;
6656
+ const ariaSort = sort && canSortAny ? isSorted && canSort ? sort.dir === "desc" ? "descending" : "ascending" : canSort ? "none" : void 0 : own || (canSort ? "none" : void 0);
6627
6657
  const pin = isPinned(c), edge = pin && i === nPinned - 1;
6628
6658
  head.push(
6629
6659
  /* @__PURE__ */ React33.createElement(
@@ -6681,7 +6711,7 @@ window.Aura = (() => {
6681
6711
  "button",
6682
6712
  {
6683
6713
  type: "button",
6684
- tabIndex: -1,
6714
+ tabIndex: tabIdx,
6685
6715
  className: cx("aura-table__sort", isSorted && "is-active"),
6686
6716
  onClick: function() {
6687
6717
  sortBy(c.key);
@@ -6700,7 +6730,7 @@ window.Aura = (() => {
6700
6730
  "button",
6701
6731
  {
6702
6732
  type: "button",
6703
- tabIndex: -1,
6733
+ tabIndex: tabIdx,
6704
6734
  className: "aura-table__menu-btn",
6705
6735
  "aria-label": t.columnOptions(c.label),
6706
6736
  "aria-haspopup": "menu",
@@ -6775,7 +6805,7 @@ window.Aura = (() => {
6775
6805
  {
6776
6806
  hideLabel: true,
6777
6807
  checked: isSel,
6778
- tabIndex: -1,
6808
+ tabIndex: tabIdx,
6779
6809
  label: selLabel,
6780
6810
  onChange: function(on) {
6781
6811
  const shift = shiftClick.current;
@@ -666,8 +666,17 @@ var PILL_ICON = {
666
666
  };
667
667
  function statusPillElement(props, ref) {
668
668
  const tone2 = props.tone || toneFor(props.children);
669
- const rest = omit(props, ["tone", "className", "children"]);
670
- return /* @__PURE__ */ React4.createElement("span", { ...rest, ref, className: cx("aura-pill", "aura-pill--" + tone2, props.className) }, /* @__PURE__ */ React4.createElement(Icon, { name: h2(PILL_ICON[tone2] || IconCircle), size: 12 }), props.children);
669
+ const rest = omit(props, ["tone", "className", "children", "wrap"]);
670
+ return /* @__PURE__ */ React4.createElement(
671
+ "span",
672
+ {
673
+ ...rest,
674
+ ref,
675
+ className: cx("aura-pill", "aura-pill--" + tone2, props.wrap && "is-wrap", props.className)
676
+ },
677
+ /* @__PURE__ */ React4.createElement(Icon, { name: h2(PILL_ICON[tone2] || IconCircle), size: 12 }),
678
+ props.wrap ? /* @__PURE__ */ React4.createElement("span", { className: "aura-pill__text" }, props.children) : props.children
679
+ );
671
680
  }
672
681
  function badgeElement(props, ref) {
673
682
  const rest = omit(props, ["tone", "variant", "icon", "className", "children"]);
@@ -1974,7 +1983,12 @@ var Field = React13.forwardRef(function Field2(props, ref) {
1974
1983
  "div",
1975
1984
  {
1976
1985
  ref,
1977
- className: cx("aura-field", props.error && "is-invalid", props.disabled && "is-disabled", props.className)
1986
+ className: cx(
1987
+ "aura-field",
1988
+ (props.error || props.invalid) && "is-invalid",
1989
+ props.disabled && "is-disabled",
1990
+ props.className
1991
+ )
1978
1992
  },
1979
1993
  props.label ? h3(
1980
1994
  props.labelAs || "label",
@@ -2019,7 +2033,8 @@ var FIELD_KEYS = [
2019
2033
  "placeholder",
2020
2034
  "touchHeight",
2021
2035
  "labelAddon",
2022
- "labelAddonDescribes"
2036
+ "labelAddonDescribes",
2037
+ "invalid"
2023
2038
  ];
2024
2039
 
2025
2040
  // src/TextField.tsx
@@ -2034,6 +2049,7 @@ var TextField = React14.forwardRef(function TextField2(props, ref) {
2034
2049
  label: props.label,
2035
2050
  hint: props.hint,
2036
2051
  error: props.error,
2052
+ invalid: props.invalid,
2037
2053
  required: props.required,
2038
2054
  optional: props.optional,
2039
2055
  labelAddon: props.labelAddon,
@@ -2050,7 +2066,7 @@ var TextField = React14.forwardRef(function TextField2(props, ref) {
2050
2066
  className: "aura-input__control",
2051
2067
  placeholder: props.placeholder,
2052
2068
  required: props.required,
2053
- "aria-invalid": props.error ? true : rest["aria-invalid"],
2069
+ "aria-invalid": props.error || props.invalid ? true : rest["aria-invalid"],
2054
2070
  "aria-describedby": joinDescribedBy(id, props, rest["aria-describedby"])
2055
2071
  }
2056
2072
  ), props.suffix ? /* @__PURE__ */ React14.createElement("span", { className: "aura-input__suffix" }, props.suffix) : null)
@@ -2069,6 +2085,7 @@ var Textarea = React15.forwardRef(function Textarea2(props, ref) {
2069
2085
  label: props.label,
2070
2086
  hint: props.hint,
2071
2087
  error: props.error,
2088
+ invalid: props.invalid,
2072
2089
  required: props.required,
2073
2090
  optional: props.optional,
2074
2091
  labelAddon: props.labelAddon,
@@ -2085,7 +2102,7 @@ var Textarea = React15.forwardRef(function Textarea2(props, ref) {
2085
2102
  className: "aura-input aura-textarea",
2086
2103
  placeholder: props.placeholder,
2087
2104
  required: props.required,
2088
- "aria-invalid": props.error ? true : rest["aria-invalid"],
2105
+ "aria-invalid": props.error || props.invalid ? true : rest["aria-invalid"],
2089
2106
  "aria-describedby": joinDescribedBy(id, props, rest["aria-describedby"])
2090
2107
  }
2091
2108
  )
@@ -5733,6 +5750,8 @@ var DataTableImpl = React33.forwardRef(function DataTable(props, ref) {
5733
5750
  const refreshing = busy && manual && rows.length > 0;
5734
5751
  const loading = busy && !refreshing;
5735
5752
  const selectable = !!props.selectable;
5753
+ const tabMode = props.keyboard === "tab";
5754
+ const tabIdx = tabMode ? void 0 : -1;
5736
5755
  const reorderable = props.reorderable !== false && !!props.columnControls;
5737
5756
  const controls = !!props.columnControls;
5738
5757
  const oneCallback = !!props.onStateChange;
@@ -5925,7 +5944,7 @@ var DataTableImpl = React33.forwardRef(function DataTable(props, ref) {
5925
5944
  const pending = React33.useRef(null);
5926
5945
  const resizing = React33.useRef(false);
5927
5946
  useIsoLayoutEffect(function() {
5928
- if (!gridRef.current) return;
5947
+ if (!gridRef.current || tabMode) return;
5929
5948
  const els = gridRef.current.querySelectorAll(
5930
5949
  '.aura-table__td button, .aura-table__td a[href], .aura-table__td input, .aura-table__td select, .aura-table__td textarea, .aura-table__td [tabindex="0"]'
5931
5950
  );
@@ -6009,7 +6028,7 @@ var DataTableImpl = React33.forwardRef(function DataTable(props, ref) {
6009
6028
  });
6010
6029
  let fixedSum = 0, hideTerms = "";
6011
6030
  vis.forEach(function(c) {
6012
- const w = widthOf(c) != null ? widthOf(c) : FLEX_MIN, lv = hideLevel(c);
6031
+ const w = widthOf(c) != null ? widthOf(c) : c.minWidth || FLEX_MIN, lv = hideLevel(c);
6013
6032
  if (lv && boxWidth[0] == null) hideTerms += " + " + w + "px * var(--aura-h" + lv + "-on, 1)";
6014
6033
  else fixedSum += w;
6015
6034
  });
@@ -6021,13 +6040,21 @@ var DataTableImpl = React33.forwardRef(function DataTable(props, ref) {
6021
6040
  acc += " + " + widthOf(c) + "px" + (lv && boxWidth[0] == null ? " * var(--aura-h" + lv + "-on, 1)" : "");
6022
6041
  }
6023
6042
  });
6043
+ if (vis.filter(function(c) {
6044
+ return !!c.sortDirection;
6045
+ }).length > 1)
6046
+ devWarnOnce(
6047
+ "datatable-sortdirection-twice",
6048
+ "DataTable: more than one column has sortDirection. The order follows one column; give it to that one."
6049
+ );
6024
6050
  const selW = selectable ? " + var(--aura-table-select-width)" : "";
6025
6051
  const gutterR = controls ? "var(--aura-space-12)" : "var(--aura-space-6)";
6026
6052
  const rowMinWidth = "calc(" + fixedSum + "px" + hideTerms + " + var(--aura-space-6) + " + gutterR + selW + ")";
6027
6053
  function cellStyle(c, i) {
6028
6054
  const w = widthOf(c), s = {};
6029
6055
  if (w == null) {
6030
- s["--aura-cell-flex"] = "1 1 0";
6056
+ const g = typeof c.grow === "number" && c.grow >= 0 && isFinite(c.grow) ? c.grow : 1;
6057
+ s["--aura-cell-flex"] = g + " 1 0";
6031
6058
  s["--aura-cell-min"] = (c.minWidth || FLEX_MIN) + "px";
6032
6059
  } else {
6033
6060
  s["--aura-cell-flex"] = !hasFlex && i === vis.length - 1 ? "1 0 auto" : "none";
@@ -6204,6 +6231,7 @@ var DataTableImpl = React33.forwardRef(function DataTable(props, ref) {
6204
6231
  [stacked]
6205
6232
  );
6206
6233
  function tabFor(r2, c) {
6234
+ if (tabMode) return -1;
6207
6235
  if (!activeRendered) return r2 === 0 && c === ac ? 0 : -1;
6208
6236
  return r2 === ar && c === acShown ? 0 : -1;
6209
6237
  }
@@ -6226,7 +6254,7 @@ var DataTableImpl = React33.forwardRef(function DataTable(props, ref) {
6226
6254
  const el = gridRef.current.querySelector('[data-rc="' + p.r + ":" + c + '"]');
6227
6255
  if (el) {
6228
6256
  pending.current = null;
6229
- el.focus();
6257
+ (p.menuBtn && el.querySelector(".aura-table__menu-btn") || el).focus();
6230
6258
  }
6231
6259
  });
6232
6260
  function focusCell(r2, c, dir) {
@@ -6502,7 +6530,7 @@ var DataTableImpl = React33.forwardRef(function DataTable(props, ref) {
6502
6530
  const m = menu;
6503
6531
  setMenu(null);
6504
6532
  if (restore && m) {
6505
- if (m.kind === "col") pending.current = { r: 0, key: m.key, c: m.rc ? m.rc.c : 0 };
6533
+ if (m.kind === "col") pending.current = { r: 0, key: m.key, c: m.rc ? m.rc.c : 0, menuBtn: tabMode };
6506
6534
  else if (m.anchor) m.anchor.focus();
6507
6535
  }
6508
6536
  }
@@ -6590,10 +6618,11 @@ var DataTableImpl = React33.forwardRef(function DataTable(props, ref) {
6590
6618
  pageKeys.length ? /* @__PURE__ */ React33.createElement(
6591
6619
  Checkbox,
6592
6620
  {
6621
+ ref: props.selectAllRef,
6593
6622
  hideLabel: true,
6594
6623
  checked: all,
6595
6624
  indeterminate: nSel > 0 && !all,
6596
- tabIndex: -1,
6625
+ tabIndex: tabIdx,
6597
6626
  label: all ? t.deselectAllRows : t.selectAllRows,
6598
6627
  onChange: toggleAll
6599
6628
  }
@@ -6605,7 +6634,8 @@ var DataTableImpl = React33.forwardRef(function DataTable(props, ref) {
6605
6634
  const ci = i + (selectable ? 1 : 0);
6606
6635
  const isSorted = sort && sort.key === c.key;
6607
6636
  const canSort = c.sortable && canSortAny;
6608
- const ariaSort = isSorted && canSort ? sort.dir === "desc" ? "descending" : "ascending" : canSort ? "none" : void 0;
6637
+ const own = c.sortDirection === "ascending" || c.sortDirection === "descending" ? c.sortDirection : void 0;
6638
+ const ariaSort = sort && canSortAny ? isSorted && canSort ? sort.dir === "desc" ? "descending" : "ascending" : canSort ? "none" : void 0 : own || (canSort ? "none" : void 0);
6609
6639
  const pin = isPinned(c), edge = pin && i === nPinned - 1;
6610
6640
  head.push(
6611
6641
  /* @__PURE__ */ React33.createElement(
@@ -6663,7 +6693,7 @@ var DataTableImpl = React33.forwardRef(function DataTable(props, ref) {
6663
6693
  "button",
6664
6694
  {
6665
6695
  type: "button",
6666
- tabIndex: -1,
6696
+ tabIndex: tabIdx,
6667
6697
  className: cx("aura-table__sort", isSorted && "is-active"),
6668
6698
  onClick: function() {
6669
6699
  sortBy(c.key);
@@ -6682,7 +6712,7 @@ var DataTableImpl = React33.forwardRef(function DataTable(props, ref) {
6682
6712
  "button",
6683
6713
  {
6684
6714
  type: "button",
6685
- tabIndex: -1,
6715
+ tabIndex: tabIdx,
6686
6716
  className: "aura-table__menu-btn",
6687
6717
  "aria-label": t.columnOptions(c.label),
6688
6718
  "aria-haspopup": "menu",
@@ -6757,7 +6787,7 @@ var DataTableImpl = React33.forwardRef(function DataTable(props, ref) {
6757
6787
  {
6758
6788
  hideLabel: true,
6759
6789
  checked: isSel,
6760
- tabIndex: -1,
6790
+ tabIndex: tabIdx,
6761
6791
  label: selLabel,
6762
6792
  onChange: function(on) {
6763
6793
  const shift = shiftClick.current;
@@ -71,6 +71,8 @@ const DataTableImpl = React.forwardRef(function DataTable(props, ref) {
71
71
  const refreshing = busy && manual && rows.length > 0;
72
72
  const loading = busy && !refreshing;
73
73
  const selectable = !!props.selectable;
74
+ const tabMode = props.keyboard === "tab";
75
+ const tabIdx = tabMode ? void 0 : -1;
74
76
  const reorderable = props.reorderable !== false && !!props.columnControls;
75
77
  const controls = !!props.columnControls;
76
78
  const oneCallback = !!props.onStateChange;
@@ -263,7 +265,7 @@ const DataTableImpl = React.forwardRef(function DataTable(props, ref) {
263
265
  const pending = React.useRef(null);
264
266
  const resizing = React.useRef(false);
265
267
  useIsoLayoutEffect(function() {
266
- if (!gridRef.current) return;
268
+ if (!gridRef.current || tabMode) return;
267
269
  const els = gridRef.current.querySelectorAll(
268
270
  '.aura-table__td button, .aura-table__td a[href], .aura-table__td input, .aura-table__td select, .aura-table__td textarea, .aura-table__td [tabindex="0"]'
269
271
  );
@@ -347,7 +349,7 @@ const DataTableImpl = React.forwardRef(function DataTable(props, ref) {
347
349
  });
348
350
  let fixedSum = 0, hideTerms = "";
349
351
  vis.forEach(function(c) {
350
- const w = widthOf(c) != null ? widthOf(c) : FLEX_MIN, lv = hideLevel(c);
352
+ const w = widthOf(c) != null ? widthOf(c) : c.minWidth || FLEX_MIN, lv = hideLevel(c);
351
353
  if (lv && boxWidth[0] == null) hideTerms += " + " + w + "px * var(--aura-h" + lv + "-on, 1)";
352
354
  else fixedSum += w;
353
355
  });
@@ -359,13 +361,21 @@ const DataTableImpl = React.forwardRef(function DataTable(props, ref) {
359
361
  acc += " + " + widthOf(c) + "px" + (lv && boxWidth[0] == null ? " * var(--aura-h" + lv + "-on, 1)" : "");
360
362
  }
361
363
  });
364
+ if (vis.filter(function(c) {
365
+ return !!c.sortDirection;
366
+ }).length > 1)
367
+ devWarnOnce(
368
+ "datatable-sortdirection-twice",
369
+ "DataTable: more than one column has sortDirection. The order follows one column; give it to that one."
370
+ );
362
371
  const selW = selectable ? " + var(--aura-table-select-width)" : "";
363
372
  const gutterR = controls ? "var(--aura-space-12)" : "var(--aura-space-6)";
364
373
  const rowMinWidth = "calc(" + fixedSum + "px" + hideTerms + " + var(--aura-space-6) + " + gutterR + selW + ")";
365
374
  function cellStyle(c, i) {
366
375
  const w = widthOf(c), s = {};
367
376
  if (w == null) {
368
- s["--aura-cell-flex"] = "1 1 0";
377
+ const g = typeof c.grow === "number" && c.grow >= 0 && isFinite(c.grow) ? c.grow : 1;
378
+ s["--aura-cell-flex"] = g + " 1 0";
369
379
  s["--aura-cell-min"] = (c.minWidth || FLEX_MIN) + "px";
370
380
  } else {
371
381
  s["--aura-cell-flex"] = !hasFlex && i === vis.length - 1 ? "1 0 auto" : "none";
@@ -542,6 +552,7 @@ const DataTableImpl = React.forwardRef(function DataTable(props, ref) {
542
552
  [stacked]
543
553
  );
544
554
  function tabFor(r, c) {
555
+ if (tabMode) return -1;
545
556
  if (!activeRendered) return r === 0 && c === ac ? 0 : -1;
546
557
  return r === ar && c === acShown ? 0 : -1;
547
558
  }
@@ -564,7 +575,7 @@ const DataTableImpl = React.forwardRef(function DataTable(props, ref) {
564
575
  const el = gridRef.current.querySelector('[data-rc="' + p.r + ":" + c + '"]');
565
576
  if (el) {
566
577
  pending.current = null;
567
- el.focus();
578
+ (p.menuBtn && el.querySelector(".aura-table__menu-btn") || el).focus();
568
579
  }
569
580
  });
570
581
  function focusCell(r, c, dir) {
@@ -840,7 +851,7 @@ const DataTableImpl = React.forwardRef(function DataTable(props, ref) {
840
851
  const m = menu;
841
852
  setMenu(null);
842
853
  if (restore && m) {
843
- if (m.kind === "col") pending.current = { r: 0, key: m.key, c: m.rc ? m.rc.c : 0 };
854
+ if (m.kind === "col") pending.current = { r: 0, key: m.key, c: m.rc ? m.rc.c : 0, menuBtn: tabMode };
844
855
  else if (m.anchor) m.anchor.focus();
845
856
  }
846
857
  }
@@ -928,10 +939,11 @@ const DataTableImpl = React.forwardRef(function DataTable(props, ref) {
928
939
  pageKeys.length ? /* @__PURE__ */ React.createElement(
929
940
  Checkbox,
930
941
  {
942
+ ref: props.selectAllRef,
931
943
  hideLabel: true,
932
944
  checked: all,
933
945
  indeterminate: nSel > 0 && !all,
934
- tabIndex: -1,
946
+ tabIndex: tabIdx,
935
947
  label: all ? t.deselectAllRows : t.selectAllRows,
936
948
  onChange: toggleAll
937
949
  }
@@ -943,7 +955,8 @@ const DataTableImpl = React.forwardRef(function DataTable(props, ref) {
943
955
  const ci = i + (selectable ? 1 : 0);
944
956
  const isSorted = sort && sort.key === c.key;
945
957
  const canSort = c.sortable && canSortAny;
946
- const ariaSort = isSorted && canSort ? sort.dir === "desc" ? "descending" : "ascending" : canSort ? "none" : void 0;
958
+ const own = c.sortDirection === "ascending" || c.sortDirection === "descending" ? c.sortDirection : void 0;
959
+ const ariaSort = sort && canSortAny ? isSorted && canSort ? sort.dir === "desc" ? "descending" : "ascending" : canSort ? "none" : void 0 : own || (canSort ? "none" : void 0);
947
960
  const pin = isPinned(c), edge = pin && i === nPinned - 1;
948
961
  head.push(
949
962
  /* @__PURE__ */ React.createElement(
@@ -1001,7 +1014,7 @@ const DataTableImpl = React.forwardRef(function DataTable(props, ref) {
1001
1014
  "button",
1002
1015
  {
1003
1016
  type: "button",
1004
- tabIndex: -1,
1017
+ tabIndex: tabIdx,
1005
1018
  className: cx("aura-table__sort", isSorted && "is-active"),
1006
1019
  onClick: function() {
1007
1020
  sortBy(c.key);
@@ -1020,7 +1033,7 @@ const DataTableImpl = React.forwardRef(function DataTable(props, ref) {
1020
1033
  "button",
1021
1034
  {
1022
1035
  type: "button",
1023
- tabIndex: -1,
1036
+ tabIndex: tabIdx,
1024
1037
  className: "aura-table__menu-btn",
1025
1038
  "aria-label": t.columnOptions(c.label),
1026
1039
  "aria-haspopup": "menu",
@@ -1095,7 +1108,7 @@ const DataTableImpl = React.forwardRef(function DataTable(props, ref) {
1095
1108
  {
1096
1109
  hideLabel: true,
1097
1110
  checked: isSel,
1098
- tabIndex: -1,
1111
+ tabIndex: tabIdx,
1099
1112
  label: selLabel,
1100
1113
  onChange: function(on) {
1101
1114
  const shift = shiftClick.current;
package/dist/esm/Field.js CHANGED
@@ -11,7 +11,12 @@ const Field = React.forwardRef(function Field2(props, ref) {
11
11
  "div",
12
12
  {
13
13
  ref,
14
- className: cx("aura-field", props.error && "is-invalid", props.disabled && "is-disabled", props.className)
14
+ className: cx(
15
+ "aura-field",
16
+ (props.error || props.invalid) && "is-invalid",
17
+ props.disabled && "is-disabled",
18
+ props.className
19
+ )
15
20
  },
16
21
  props.label ? h(
17
22
  props.labelAs || "label",
@@ -56,7 +61,8 @@ const FIELD_KEYS = [
56
61
  "placeholder",
57
62
  "touchHeight",
58
63
  "labelAddon",
59
- "labelAddonDescribes"
64
+ "labelAddonDescribes",
65
+ "invalid"
60
66
  ];
61
67
  export {
62
68
  FIELD_KEYS,
@@ -13,6 +13,7 @@ const TextField = React.forwardRef(function TextField2(props, ref) {
13
13
  label: props.label,
14
14
  hint: props.hint,
15
15
  error: props.error,
16
+ invalid: props.invalid,
16
17
  required: props.required,
17
18
  optional: props.optional,
18
19
  labelAddon: props.labelAddon,
@@ -29,7 +30,7 @@ const TextField = React.forwardRef(function TextField2(props, ref) {
29
30
  className: "aura-input__control",
30
31
  placeholder: props.placeholder,
31
32
  required: props.required,
32
- "aria-invalid": props.error ? true : rest["aria-invalid"],
33
+ "aria-invalid": props.error || props.invalid ? true : rest["aria-invalid"],
33
34
  "aria-describedby": joinDescribedBy(id, props, rest["aria-describedby"])
34
35
  }
35
36
  ), props.suffix ? /* @__PURE__ */ React.createElement("span", { className: "aura-input__suffix" }, props.suffix) : null)
@@ -12,6 +12,7 @@ const Textarea = React.forwardRef(function Textarea2(props, ref) {
12
12
  label: props.label,
13
13
  hint: props.hint,
14
14
  error: props.error,
15
+ invalid: props.invalid,
15
16
  required: props.required,
16
17
  optional: props.optional,
17
18
  labelAddon: props.labelAddon,
@@ -28,7 +29,7 @@ const Textarea = React.forwardRef(function Textarea2(props, ref) {
28
29
  className: "aura-input aura-textarea",
29
30
  placeholder: props.placeholder,
30
31
  required: props.required,
31
- "aria-invalid": props.error ? true : rest["aria-invalid"],
32
+ "aria-invalid": props.error || props.invalid ? true : rest["aria-invalid"],
32
33
  "aria-describedby": joinDescribedBy(id, props, rest["aria-describedby"])
33
34
  }
34
35
  )
@@ -85,8 +85,17 @@ const PILL_ICON = {
85
85
  };
86
86
  function statusPillElement(props, ref) {
87
87
  const tone = props.tone || toneFor(props.children);
88
- const rest = omit(props, ["tone", "className", "children"]);
89
- return /* @__PURE__ */ React.createElement("span", { ...rest, ref, className: cx("aura-pill", "aura-pill--" + tone, props.className) }, /* @__PURE__ */ React.createElement(Icon, { name: h(PILL_ICON[tone] || IconCircle), size: 12 }), props.children);
88
+ const rest = omit(props, ["tone", "className", "children", "wrap"]);
89
+ return /* @__PURE__ */ React.createElement(
90
+ "span",
91
+ {
92
+ ...rest,
93
+ ref,
94
+ className: cx("aura-pill", "aura-pill--" + tone, props.wrap && "is-wrap", props.className)
95
+ },
96
+ /* @__PURE__ */ React.createElement(Icon, { name: h(PILL_ICON[tone] || IconCircle), size: 12 }),
97
+ props.wrap ? /* @__PURE__ */ React.createElement("span", { className: "aura-pill__text" }, props.children) : props.children
98
+ );
90
99
  }
91
100
  function badgeElement(props, ref) {
92
101
  const rest = omit(props, ["tone", "variant", "icon", "className", "children"]);
package/dist/index.d.cts CHANGED
@@ -89,12 +89,22 @@ export interface DataTableColumn<Row extends Record<string, any> = Record<string
89
89
  tones?: Record<string, StatusTone> | undefined;
90
90
  /** Header becomes a button cycling asc → desc → unsorted. */
91
91
  sortable?: boolean | undefined;
92
+ /** 5.51 (Chamber-OS 157): the order this column is in when the page sets it (from the URL, say), not a header
93
+ * button — puts `aria-sort` on the header, with no button, icon or click. Give it to one column. Once the table has
94
+ * its own sort (someone used a sortable header, or `sort` is set) that wins and this is ignored, so only one header
95
+ * claims the order. */
96
+ sortDirection?: "ascending" | "descending" | undefined;
92
97
  /** Value to sort by, when not the cell value (dates, amounts). */
93
98
  sortValue?: RowCallback<string | number | null, Row> | undefined;
94
99
  /** Custom cell content. */
95
100
  render?: RowCallback<React$1.ReactNode, Row> | undefined;
96
101
  /** Set false to keep a sized column fixed when the table is `resizable`. */
97
102
  resizable?: boolean | undefined;
103
+ /** 5.51 (Chamber-OS 162): a flexible column's share of the spare width (its cells' flex-grow; default 1) — e.g.
104
+ * `grow: 3` lets row actions sit side by side once the table is wide. At the minimum width nothing changes; the row
105
+ * minimum ignores it; a fixed `width` column and stacked cards ignore it. `0` keeps the column at its minimum; leave
106
+ * at least one flexible column above 0, or the rows won't fill a wide table. */
107
+ grow?: number | undefined;
98
108
  /** Resize limits in px. Defaults 64 / 480. For the flexible column, minWidth defaults to 160. */
99
109
  minWidth?: number | undefined;
100
110
  maxWidth?: number | undefined;
@@ -160,6 +170,15 @@ export interface DataTableProps<Row extends Record<string, any> = Record<string,
160
170
  onSortChange?: ((sort: DataTableSort | null) => void) | undefined;
161
171
  /** Adds a checkbox column. */
162
172
  selectable?: boolean | undefined;
173
+ /** 5.51 (Chamber-OS 161): how the keyboard moves through the table. `'grid'` (default): the table is one Tab stop,
174
+ * the arrows move between cells and Enter or F2 enters a cell's controls (APG grid). `'tab'`: every checkbox, sort
175
+ * and column button, row link and row action is an ordinary Tab stop in reading order (Shift+Tab walks back), for a
176
+ * list whose rows carry actions people expect to Tab to; the cells are not Tab stops, the arrows still move between
177
+ * them once focus is in one (click a cell, or Escape from a control), and the grid's row and column semantics stay. */
178
+ keyboard?: "grid" | "tab" | undefined;
179
+ /** 5.51 (Chamber-OS 157): a ref to the select-all checkbox (an `<input>`), e.g. to return focus to it after a bulk
180
+ * bar's Clear. Null while the page has no selectable rows. */
181
+ selectAllRef?: React$1.Ref<HTMLInputElement> | undefined;
163
182
  /** Which rows can be selected (5.6). A row it rejects shows no checkbox (the cell stays, so columns line up), is
164
183
  * skipped by select-all, Space and the count, and never appears in `onSelectionChange` — even if passed in. */
165
184
  isRowSelectable?: RowCallback<boolean, Row> | undefined;
@@ -307,6 +326,9 @@ export type StatusTone = "neutral" | "progress" | "ready" | "warning" | "blocked
307
326
  export interface StatusPillProps extends Omit<React$1.HTMLAttributes<HTMLSpanElement>, "children"> {
308
327
  /** The status word shown, e.g. "In Progress". Other attributes (`id`, `data-*`, `aria-*`) go on the pill (5.8). */
309
328
  children: React$1.ReactNode;
329
+ /** 5.51 (Chamber-OS 158): in a box narrower than the words, the text wraps inside the pill (still rounded, the icon
330
+ * on the first line) instead of running out of it. Without it a pill stays on one line. */
331
+ wrap?: boolean | undefined;
310
332
  /** Force a tone. Default: matched from the word (Ready/Done → ready, In Progress/In Review → progress, Warning/Problem/Degraded/At risk → warning (5.1), Blocked/Failed → blocked, anything else → neutral). */
311
333
  tone?: StatusTone | undefined;
312
334
  className?: string | undefined;
@@ -419,6 +441,10 @@ export interface FieldProps {
419
441
  optional?: boolean | undefined;
420
442
  }
421
443
  export interface TextFieldProps extends FieldProps, Omit<React$1.InputHTMLAttributes<HTMLInputElement>, "required"> {
444
+ /** 5.50 (Chamber-OS 156): marks the value invalid — the danger border and `aria-invalid="true"` — without AURA's
445
+ * message: no error line, and `aria-describedby` stays as it is (the hint, the addon, your own ids). For a page that
446
+ * explains the problem itself (a live readout, a reason a button points at). `error` still wins when set. */
447
+ invalid?: boolean | undefined;
422
448
  /** Leading icon inside the field. */
423
449
  icon?: IconInput | undefined;
424
450
  /** Trailing unit, e.g. "THB". */
@@ -570,6 +596,10 @@ export interface CommandProps {
570
596
  className?: string | undefined;
571
597
  }
572
598
  export interface TextareaProps extends FieldProps, Omit<React$1.TextareaHTMLAttributes<HTMLTextAreaElement>, "required"> {
599
+ /** 5.50 (Chamber-OS 156): marks the value invalid — the danger border and `aria-invalid="true"` — without AURA's
600
+ * message: no error line, and `aria-describedby` stays as it is (the hint, the addon, your own ids). For a page that
601
+ * explains the problem itself (a live readout, a reason a button points at). `error` still wins when set. */
602
+ invalid?: boolean | undefined;
573
603
  /** 5.31 (Chamber-OS addendum 41): a node between the label and the box — e.g. the phrase to type, with a copy
574
604
  * button, on a typed confirmation. The hint and error stay below. Its box's id (`{id}-addon`) comes first in the
575
605
  * input's `aria-describedby`, so its text is read after the label; keep any button names in it short. */
@@ -1798,6 +1828,9 @@ export interface FieldComponentProps extends Omit<FieldPropsPublic, "label"> {
1798
1828
  labelAs?: string | undefined;
1799
1829
  labelId?: string | undefined;
1800
1830
  disabled?: boolean | undefined;
1831
+ /** 5.50 (Chamber-OS 156): the danger look without a message (`is-invalid`, no error line). Set `aria-invalid` on
1832
+ * your control yourself; `error` still renders its message. */
1833
+ invalid?: boolean | undefined;
1801
1834
  className?: string | undefined;
1802
1835
  }
1803
1836
  export declare const Field: React$1.ForwardRefExoticComponent<FieldComponentProps & React$1.RefAttributes<HTMLDivElement>>;
package/dist/index.d.ts CHANGED
@@ -89,12 +89,22 @@ export interface DataTableColumn<Row extends Record<string, any> = Record<string
89
89
  tones?: Record<string, StatusTone> | undefined;
90
90
  /** Header becomes a button cycling asc → desc → unsorted. */
91
91
  sortable?: boolean | undefined;
92
+ /** 5.51 (Chamber-OS 157): the order this column is in when the page sets it (from the URL, say), not a header
93
+ * button — puts `aria-sort` on the header, with no button, icon or click. Give it to one column. Once the table has
94
+ * its own sort (someone used a sortable header, or `sort` is set) that wins and this is ignored, so only one header
95
+ * claims the order. */
96
+ sortDirection?: "ascending" | "descending" | undefined;
92
97
  /** Value to sort by, when not the cell value (dates, amounts). */
93
98
  sortValue?: RowCallback<string | number | null, Row> | undefined;
94
99
  /** Custom cell content. */
95
100
  render?: RowCallback<React$1.ReactNode, Row> | undefined;
96
101
  /** Set false to keep a sized column fixed when the table is `resizable`. */
97
102
  resizable?: boolean | undefined;
103
+ /** 5.51 (Chamber-OS 162): a flexible column's share of the spare width (its cells' flex-grow; default 1) — e.g.
104
+ * `grow: 3` lets row actions sit side by side once the table is wide. At the minimum width nothing changes; the row
105
+ * minimum ignores it; a fixed `width` column and stacked cards ignore it. `0` keeps the column at its minimum; leave
106
+ * at least one flexible column above 0, or the rows won't fill a wide table. */
107
+ grow?: number | undefined;
98
108
  /** Resize limits in px. Defaults 64 / 480. For the flexible column, minWidth defaults to 160. */
99
109
  minWidth?: number | undefined;
100
110
  maxWidth?: number | undefined;
@@ -160,6 +170,15 @@ export interface DataTableProps<Row extends Record<string, any> = Record<string,
160
170
  onSortChange?: ((sort: DataTableSort | null) => void) | undefined;
161
171
  /** Adds a checkbox column. */
162
172
  selectable?: boolean | undefined;
173
+ /** 5.51 (Chamber-OS 161): how the keyboard moves through the table. `'grid'` (default): the table is one Tab stop,
174
+ * the arrows move between cells and Enter or F2 enters a cell's controls (APG grid). `'tab'`: every checkbox, sort
175
+ * and column button, row link and row action is an ordinary Tab stop in reading order (Shift+Tab walks back), for a
176
+ * list whose rows carry actions people expect to Tab to; the cells are not Tab stops, the arrows still move between
177
+ * them once focus is in one (click a cell, or Escape from a control), and the grid's row and column semantics stay. */
178
+ keyboard?: "grid" | "tab" | undefined;
179
+ /** 5.51 (Chamber-OS 157): a ref to the select-all checkbox (an `<input>`), e.g. to return focus to it after a bulk
180
+ * bar's Clear. Null while the page has no selectable rows. */
181
+ selectAllRef?: React$1.Ref<HTMLInputElement> | undefined;
163
182
  /** Which rows can be selected (5.6). A row it rejects shows no checkbox (the cell stays, so columns line up), is
164
183
  * skipped by select-all, Space and the count, and never appears in `onSelectionChange` — even if passed in. */
165
184
  isRowSelectable?: RowCallback<boolean, Row> | undefined;
@@ -307,6 +326,9 @@ export type StatusTone = "neutral" | "progress" | "ready" | "warning" | "blocked
307
326
  export interface StatusPillProps extends Omit<React$1.HTMLAttributes<HTMLSpanElement>, "children"> {
308
327
  /** The status word shown, e.g. "In Progress". Other attributes (`id`, `data-*`, `aria-*`) go on the pill (5.8). */
309
328
  children: React$1.ReactNode;
329
+ /** 5.51 (Chamber-OS 158): in a box narrower than the words, the text wraps inside the pill (still rounded, the icon
330
+ * on the first line) instead of running out of it. Without it a pill stays on one line. */
331
+ wrap?: boolean | undefined;
310
332
  /** Force a tone. Default: matched from the word (Ready/Done → ready, In Progress/In Review → progress, Warning/Problem/Degraded/At risk → warning (5.1), Blocked/Failed → blocked, anything else → neutral). */
311
333
  tone?: StatusTone | undefined;
312
334
  className?: string | undefined;
@@ -419,6 +441,10 @@ export interface FieldProps {
419
441
  optional?: boolean | undefined;
420
442
  }
421
443
  export interface TextFieldProps extends FieldProps, Omit<React$1.InputHTMLAttributes<HTMLInputElement>, "required"> {
444
+ /** 5.50 (Chamber-OS 156): marks the value invalid — the danger border and `aria-invalid="true"` — without AURA's
445
+ * message: no error line, and `aria-describedby` stays as it is (the hint, the addon, your own ids). For a page that
446
+ * explains the problem itself (a live readout, a reason a button points at). `error` still wins when set. */
447
+ invalid?: boolean | undefined;
422
448
  /** Leading icon inside the field. */
423
449
  icon?: IconInput | undefined;
424
450
  /** Trailing unit, e.g. "THB". */
@@ -570,6 +596,10 @@ export interface CommandProps {
570
596
  className?: string | undefined;
571
597
  }
572
598
  export interface TextareaProps extends FieldProps, Omit<React$1.TextareaHTMLAttributes<HTMLTextAreaElement>, "required"> {
599
+ /** 5.50 (Chamber-OS 156): marks the value invalid — the danger border and `aria-invalid="true"` — without AURA's
600
+ * message: no error line, and `aria-describedby` stays as it is (the hint, the addon, your own ids). For a page that
601
+ * explains the problem itself (a live readout, a reason a button points at). `error` still wins when set. */
602
+ invalid?: boolean | undefined;
573
603
  /** 5.31 (Chamber-OS addendum 41): a node between the label and the box — e.g. the phrase to type, with a copy
574
604
  * button, on a typed confirmation. The hint and error stay below. Its box's id (`{id}-addon`) comes first in the
575
605
  * input's `aria-describedby`, so its text is read after the label; keep any button names in it short. */
@@ -1798,6 +1828,9 @@ export interface FieldComponentProps extends Omit<FieldPropsPublic, "label"> {
1798
1828
  labelAs?: string | undefined;
1799
1829
  labelId?: string | undefined;
1800
1830
  disabled?: boolean | undefined;
1831
+ /** 5.50 (Chamber-OS 156): the danger look without a message (`is-invalid`, no error line). Set `aria-invalid` on
1832
+ * your control yourself; `error` still renders its message. */
1833
+ invalid?: boolean | undefined;
1801
1834
  className?: string | undefined;
1802
1835
  }
1803
1836
  export declare const Field: React$1.ForwardRefExoticComponent<FieldComponentProps & React$1.RefAttributes<HTMLDivElement>>;
@@ -1240,8 +1240,17 @@ var PILL_ICON = {
1240
1240
  };
1241
1241
  function statusPillElement(props, ref) {
1242
1242
  const tone2 = props.tone || toneFor(props.children);
1243
- const rest = omit(props, ["tone", "className", "children"]);
1244
- return /* @__PURE__ */ React3.createElement("span", { ...rest, ref, className: cx("aura-pill", "aura-pill--" + tone2, props.className) }, /* @__PURE__ */ React3.createElement(Icon, { name: h2(PILL_ICON[tone2] || IconCircle), size: 12 }), props.children);
1243
+ const rest = omit(props, ["tone", "className", "children", "wrap"]);
1244
+ return /* @__PURE__ */ React3.createElement(
1245
+ "span",
1246
+ {
1247
+ ...rest,
1248
+ ref,
1249
+ className: cx("aura-pill", "aura-pill--" + tone2, props.wrap && "is-wrap", props.className)
1250
+ },
1251
+ /* @__PURE__ */ React3.createElement(Icon, { name: h2(PILL_ICON[tone2] || IconCircle), size: 12 }),
1252
+ props.wrap ? /* @__PURE__ */ React3.createElement("span", { className: "aura-pill__text" }, props.children) : props.children
1253
+ );
1245
1254
  }
1246
1255
  function badgeElement(props, ref) {
1247
1256
  const rest = omit(props, ["tone", "variant", "icon", "className", "children"]);
@@ -46,6 +46,9 @@ export type StatusTone = "neutral" | "progress" | "ready" | "warning" | "blocked
46
46
  export interface StatusPillProps extends Omit<React$1.HTMLAttributes<HTMLSpanElement>, "children"> {
47
47
  /** The status word shown, e.g. "In Progress". Other attributes (`id`, `data-*`, `aria-*`) go on the pill (5.8). */
48
48
  children: React$1.ReactNode;
49
+ /** 5.51 (Chamber-OS 158): in a box narrower than the words, the text wraps inside the pill (still rounded, the icon
50
+ * on the first line) instead of running out of it. Without it a pill stays on one line. */
51
+ wrap?: boolean | undefined;
49
52
  /** Force a tone. Default: matched from the word (Ready/Done → ready, In Progress/In Review → progress, Warning/Problem/Degraded/At risk → warning (5.1), Blocked/Failed → blocked, anything else → neutral). */
50
53
  tone?: StatusTone | undefined;
51
54
  className?: string | undefined;
@@ -46,6 +46,9 @@ export type StatusTone = "neutral" | "progress" | "ready" | "warning" | "blocked
46
46
  export interface StatusPillProps extends Omit<React$1.HTMLAttributes<HTMLSpanElement>, "children"> {
47
47
  /** The status word shown, e.g. "In Progress". Other attributes (`id`, `data-*`, `aria-*`) go on the pill (5.8). */
48
48
  children: React$1.ReactNode;
49
+ /** 5.51 (Chamber-OS 158): in a box narrower than the words, the text wraps inside the pill (still rounded, the icon
50
+ * on the first line) instead of running out of it. Without it a pill stays on one line. */
51
+ wrap?: boolean | undefined;
49
52
  /** Force a tone. Default: matched from the word (Ready/Done → ready, In Progress/In Review → progress, Warning/Problem/Degraded/At risk → warning (5.1), Blocked/Failed → blocked, anything else → neutral). */
50
53
  tone?: StatusTone | undefined;
51
54
  className?: string | undefined;
@@ -1176,8 +1176,17 @@ var PILL_ICON = {
1176
1176
  };
1177
1177
  function statusPillElement(props, ref) {
1178
1178
  const tone2 = props.tone || toneFor(props.children);
1179
- const rest = omit(props, ["tone", "className", "children"]);
1180
- return /* @__PURE__ */ React3.createElement("span", { ...rest, ref, className: cx("aura-pill", "aura-pill--" + tone2, props.className) }, /* @__PURE__ */ React3.createElement(Icon, { name: h2(PILL_ICON[tone2] || IconCircle), size: 12 }), props.children);
1179
+ const rest = omit(props, ["tone", "className", "children", "wrap"]);
1180
+ return /* @__PURE__ */ React3.createElement(
1181
+ "span",
1182
+ {
1183
+ ...rest,
1184
+ ref,
1185
+ className: cx("aura-pill", "aura-pill--" + tone2, props.wrap && "is-wrap", props.className)
1186
+ },
1187
+ /* @__PURE__ */ React3.createElement(Icon, { name: h2(PILL_ICON[tone2] || IconCircle), size: 12 }),
1188
+ props.wrap ? /* @__PURE__ */ React3.createElement("span", { className: "aura-pill__text" }, props.children) : props.children
1189
+ );
1181
1190
  }
1182
1191
  function badgeElement(props, ref) {
1183
1192
  const rest = omit(props, ["tone", "variant", "icon", "className", "children"]);
package/dist/styles.css CHANGED
@@ -105,7 +105,10 @@ button.aura-btn--creative[aria-disabled='true']:hover { box-shadow: var(--aura-s
105
105
  background: var(--aura-bg-surface);
106
106
  color: var(--aura-fg-primary);
107
107
  }
108
- .aura-table__scroll { position: relative; overflow: auto; overscroll-behavior: contain; height: var(--aura-table-h, auto); }
108
+ /* 5.51 (Chamber-OS 163): only the sideways axis is contained (a trackpad swipe in a wide grid never goes back a page);
109
+ * a vertical wheel over a grid that scrolls sideways still scrolls the page, and a fixed-height grid scrolls inside
110
+ * first, then chains to the page at its end. */
111
+ .aura-table__scroll { position: relative; overflow: auto; overscroll-behavior-x: contain; overscroll-behavior-y: auto; height: var(--aura-table-h, auto); }
109
112
  /* 5.30: more columns past the end edge — a soft shadow over the grid's end edge (not over its scrollbars, its footer
110
113
  * or the column button), so it reads as scrolling sideways. JavaScript places it (top, height, scrollbar inset). */
111
114
  .aura-table__edge {
@@ -440,6 +443,13 @@ a.aura-icon-btn { text-decoration: none; }
440
443
  background: var(--aura-status-neutral-bg);
441
444
  color: var(--aura-status-neutral-fg);
442
445
  }
446
+ /* 5.51 (Chamber-OS 158, addendum 61): wrap — the words break inside the pill when its box is narrower; the icon stays
447
+ * beside the first line. Corners are aura-radius-lg (16px), not full: on one line (20px tall) that still clamps to fully
448
+ * round ends, and a wrapped pill is a rounded rectangle, not a tall lozenge. */
449
+ .aura-pill.is-wrap { align-items: flex-start; white-space: normal; border-radius: var(--aura-radius-lg); }
450
+ .aura-pill.is-wrap > .aura-icon { margin-top: calc((max(16px, 1rem) - 12px) / 2); }
451
+ .aura-pill.is-wrap:lang(th) > .aura-icon { margin-top: calc((max(18px, 1.125rem) - 12px) / 2); }
452
+ .aura-pill__text { min-width: 0; overflow-wrap: anywhere; }
443
453
  .aura-pill--progress { background: var(--aura-status-progress-bg); color: var(--aura-status-progress-fg); }
444
454
  .aura-pill--ready { background: var(--aura-status-ready-bg); color: var(--aura-status-ready-fg); }
445
455
  .aura-pill--warning { background: var(--aura-status-warning-bg); color: var(--aura-status-warning-fg); }
@@ -528,6 +538,11 @@ a.aura-icon-btn { text-decoration: none; }
528
538
  .aura-input__icon { position: absolute; left: var(--aura-space-3); color: var(--aura-fg-secondary); pointer-events: none; }
529
539
  .aura-input__suffix { padding: 0 var(--aura-space-3) 0 0; color: var(--aura-fg-secondary); font-size: max(13px, .8125rem); white-space: nowrap; }
530
540
  .aura-field.is-invalid .aura-input, .aura-field.is-invalid .aura-textarea { border-color: var(--aura-border-danger); }
541
+ /* 5.50 (Chamber-OS 156): a control the page marks aria-invalid="true" itself gets the same edge, message or not (a
542
+ * disabled field keeps its disabled edge, as with is-invalid; 5.51: so does a disabled control inside an enabled custom
543
+ * Field). */
544
+ .aura-field:not(.is-disabled) .aura-input:has(> .aura-input__control[aria-invalid="true"]:not(:disabled)),
545
+ .aura-field:not(.is-disabled) .aura-textarea[aria-invalid="true"]:not(:disabled) { border-color: var(--aura-border-danger); }
531
546
  .aura-field.is-disabled .aura-input { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-border-default); }
532
547
  .aura-field.is-disabled .aura-input__control { cursor: not-allowed; }
533
548
  /* A read-only text field. Only an input: `:read-only` also matches any element that isn't editable — a
@@ -1860,6 +1875,16 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
1860
1875
  * (So a FilterBar with only chips needs a width from its parent — it is full width in a block.) */
1861
1876
  .aura-filterbar__chips { contain: inline-size; }
1862
1877
  .aura-filterbar__chips > .aura-tag .aura-tag__text { max-width: none; min-width: 0; }
1878
+ /* 5.51 (Chamber-OS 160): a toggle Tag among FilterBar's controls (or in its sheet) is a filter, not a label to cut:
1879
+ * from 640px it shows its whole text, cut only when wider than the bar. Below 640px (outside the sheet) the 24ch cap
1880
+ * and its tip stay. */
1881
+ @media (min-width: 640px) {
1882
+ .aura-filterbar .aura-tag.is-selectable { max-width: 100%; min-width: 0; }
1883
+ .aura-filterbar .aura-tag.is-selectable .aura-tag__text { max-width: none; min-width: 0; }
1884
+ }
1885
+ /* …and in the Filters sheet at any width: it is full width, with every control labelled. */
1886
+ .aura-filterbar--sheet .aura-tag.is-selectable { max-width: 100%; min-width: 0; }
1887
+ .aura-filterbar--sheet .aura-tag.is-selectable .aura-tag__text { max-width: none; min-width: 0; }
1863
1888
  .aura-filterbar__clear {
1864
1889
  border: 0; background: transparent; padding: 4px var(--aura-space-2); border-radius: var(--aura-radius-sm);
1865
1890
  font: inherit; font-size: max(13px, .8125rem); font-weight: 500; color: var(--aura-fg-accent); cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
@@ -107,7 +107,10 @@ button.aura-btn--creative[aria-disabled='true']:hover { box-shadow: var(--aura-s
107
107
  background: var(--aura-bg-surface);
108
108
  color: var(--aura-fg-primary);
109
109
  }
110
- .aura-table__scroll { position: relative; overflow: auto; overscroll-behavior: contain; height: var(--aura-table-h, auto); }
110
+ /* 5.51 (Chamber-OS 163): only the sideways axis is contained (a trackpad swipe in a wide grid never goes back a page);
111
+ * a vertical wheel over a grid that scrolls sideways still scrolls the page, and a fixed-height grid scrolls inside
112
+ * first, then chains to the page at its end. */
113
+ .aura-table__scroll { position: relative; overflow: auto; overscroll-behavior-x: contain; overscroll-behavior-y: auto; height: var(--aura-table-h, auto); }
111
114
  /* 5.30: more columns past the end edge — a soft shadow over the grid's end edge (not over its scrollbars, its footer
112
115
  * or the column button), so it reads as scrolling sideways. JavaScript places it (top, height, scrollbar inset). */
113
116
  .aura-table__edge {
@@ -442,6 +445,13 @@ a.aura-icon-btn { text-decoration: none; }
442
445
  background: var(--aura-status-neutral-bg);
443
446
  color: var(--aura-status-neutral-fg);
444
447
  }
448
+ /* 5.51 (Chamber-OS 158, addendum 61): wrap — the words break inside the pill when its box is narrower; the icon stays
449
+ * beside the first line. Corners are aura-radius-lg (16px), not full: on one line (20px tall) that still clamps to fully
450
+ * round ends, and a wrapped pill is a rounded rectangle, not a tall lozenge. */
451
+ .aura-pill.is-wrap { align-items: flex-start; white-space: normal; border-radius: var(--aura-radius-lg); }
452
+ .aura-pill.is-wrap > .aura-icon { margin-top: calc((max(16px, 1rem) - 12px) / 2); }
453
+ .aura-pill.is-wrap:lang(th) > .aura-icon { margin-top: calc((max(18px, 1.125rem) - 12px) / 2); }
454
+ .aura-pill__text { min-width: 0; overflow-wrap: anywhere; }
445
455
  .aura-pill--progress { background: var(--aura-status-progress-bg); color: var(--aura-status-progress-fg); }
446
456
  .aura-pill--ready { background: var(--aura-status-ready-bg); color: var(--aura-status-ready-fg); }
447
457
  .aura-pill--warning { background: var(--aura-status-warning-bg); color: var(--aura-status-warning-fg); }
@@ -530,6 +540,11 @@ a.aura-icon-btn { text-decoration: none; }
530
540
  .aura-input__icon { position: absolute; left: var(--aura-space-3); color: var(--aura-fg-secondary); pointer-events: none; }
531
541
  .aura-input__suffix { padding: 0 var(--aura-space-3) 0 0; color: var(--aura-fg-secondary); font-size: max(13px, .8125rem); white-space: nowrap; }
532
542
  .aura-field.is-invalid .aura-input, .aura-field.is-invalid .aura-textarea { border-color: var(--aura-border-danger); }
543
+ /* 5.50 (Chamber-OS 156): a control the page marks aria-invalid="true" itself gets the same edge, message or not (a
544
+ * disabled field keeps its disabled edge, as with is-invalid; 5.51: so does a disabled control inside an enabled custom
545
+ * Field). */
546
+ .aura-field:not(.is-disabled) .aura-input:has(> .aura-input__control[aria-invalid="true"]:not(:disabled)),
547
+ .aura-field:not(.is-disabled) .aura-textarea[aria-invalid="true"]:not(:disabled) { border-color: var(--aura-border-danger); }
533
548
  .aura-field.is-disabled .aura-input { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-border-default); }
534
549
  .aura-field.is-disabled .aura-input__control { cursor: not-allowed; }
535
550
  /* A read-only text field. Only an input: `:read-only` also matches any element that isn't editable — a
@@ -1862,6 +1877,16 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
1862
1877
  * (So a FilterBar with only chips needs a width from its parent — it is full width in a block.) */
1863
1878
  .aura-filterbar__chips { contain: inline-size; }
1864
1879
  .aura-filterbar__chips > .aura-tag .aura-tag__text { max-width: none; min-width: 0; }
1880
+ /* 5.51 (Chamber-OS 160): a toggle Tag among FilterBar's controls (or in its sheet) is a filter, not a label to cut:
1881
+ * from 640px it shows its whole text, cut only when wider than the bar. Below 640px (outside the sheet) the 24ch cap
1882
+ * and its tip stay. */
1883
+ @media (min-width: 640px) {
1884
+ .aura-filterbar .aura-tag.is-selectable { max-width: 100%; min-width: 0; }
1885
+ .aura-filterbar .aura-tag.is-selectable .aura-tag__text { max-width: none; min-width: 0; }
1886
+ }
1887
+ /* …and in the Filters sheet at any width: it is full width, with every control labelled. */
1888
+ .aura-filterbar--sheet .aura-tag.is-selectable { max-width: 100%; min-width: 0; }
1889
+ .aura-filterbar--sheet .aura-tag.is-selectable .aura-tag__text { max-width: none; min-width: 0; }
1865
1890
  .aura-filterbar__clear {
1866
1891
  border: 0; background: transparent; padding: 4px var(--aura-space-2); border-radius: var(--aura-radius-sm);
1867
1892
  font: inherit; font-size: max(13px, .8125rem); font-weight: 500; color: var(--aura-fg-accent); cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jirawatpyk/aura-react",
3
- "version": "5.49.0",
3
+ "version": "5.51.0",
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",