@jirawatpyk/aura-react 5.30.0 → 5.31.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,11 @@ 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.31 — Chamber-OS addenda 41–42
190
+
191
+ - **`labelAddon` on TextField, Textarea and PasswordField** (item 138): a node between the label and the box, in the field's 6px gap — e.g. the phrase to type on a typed confirmation, as a chip with a copy button, so it is read before the box and can be copied. The hint and error stay below the box. The addon's box is `.aura-field__addon` with the id `{id}-addon`, and that id comes first in the input's `aria-describedby` (then the error or hint, then your own `aria-describedby` ids), so a screen reader reads it after the label; keep any button names in it short ("Copy"). `labelAddonDescribes={false}` keeps it out — for an addon that isn't a description (a "Forgot password?" link), or to name only part of it (give that part an id and pass it in `aria-describedby`); ids are never repeated. A focusable control in it comes before the input in tab order; clicking the label still focuses the input. `Field` takes `labelAddon` too, for a custom control (add `{id}-addon` to that control's `aria-describedby` yourself). The other fields (Select, Combobox, NumberField, the pickers, FileUpload) don't take it yet. Unset, nothing changes.
192
+ - **"Chosen" follows the brand** (item 139): `createTheme`, `<ThemeStyle>` and `npx aura-theme --brand` now emit `--aura-control-checked-bg` / `-fg` for light and dark, so a themed product's checked box, radio dot, switch, active tab underline, current page, selected day and year, completed step and checked menu item match its links and focus instead of staying violet. Light is the theme's accent itself; dark is the brand's 300 step, lightened to at least 3:1 on the dark surface and canvas (as AURA's own violet-300). The mark on it is white or ink, whichever reads better; both are in the theme's contrast checks (fill 3:1 on surface and canvas, mark 4.5:1 on the fill). In aura.css the light `--aura-control-checked-bg` is now `var(--aura-accent-violet)` (the same violet-700 by default), so a hand-written `:root` override of the accent carries it too; a scoped override needs the generator, or both tokens. Unthemed, nothing changes. Remove any hand-written `--aura-control-checked-*` stand-in after regenerating the theme.
193
+
189
194
  ## 5.30 — Visual polish (audit, Oct 2026) and Chamber-OS addenda 38–40
190
195
 
191
196
  One release: everything below ships together in 5.30.0.
@@ -1755,6 +1755,7 @@ window.Aura = (() => {
1755
1755
  props.required ? /* @__PURE__ */ React12.createElement("span", { className: "aura-field__req", "aria-hidden": true }, " *") : null,
1756
1756
  props.optional ? /* @__PURE__ */ React12.createElement("span", { className: "aura-field__opt" }, " (" + t.optional + ")") : null
1757
1757
  ) : null,
1758
+ hasAddon(props.labelAddon) ? /* @__PURE__ */ React12.createElement("div", { className: "aura-field__addon", id: props.id + "-addon" }, props.labelAddon) : null,
1758
1759
  props.children,
1759
1760
  props.error ? /* @__PURE__ */ React12.createElement("p", { className: "aura-field__error", id: props.id + "-error" }, /* @__PURE__ */ React12.createElement(Icon, { name: /* @__PURE__ */ React12.createElement(IconCircleAlert, null), size: 14 }), props.error) : props.hint ? /* @__PURE__ */ React12.createElement("p", { className: "aura-field__hint", id: props.id + "-hint" }, props.hint) : null
1760
1761
  );
@@ -1762,6 +1763,20 @@ window.Aura = (() => {
1762
1763
  function describedBy(id, p) {
1763
1764
  return p.error ? id + "-error" : p.hint ? id + "-hint" : void 0;
1764
1765
  }
1766
+ function hasAddon(n3) {
1767
+ return n3 != null && typeof n3 !== "boolean" && n3 !== "";
1768
+ }
1769
+ function joinDescribedBy(id, p, own) {
1770
+ const ids = [
1771
+ hasAddon(p.labelAddon) && p.labelAddonDescribes !== false ? id + "-addon" : "",
1772
+ describedBy(id, p) || ""
1773
+ ].concat(typeof own === "string" ? own.split(/\s+/) : []);
1774
+ const out = [];
1775
+ ids.forEach(function(x) {
1776
+ if (x && out.indexOf(x) < 0) out.push(x);
1777
+ });
1778
+ return out.join(" ") || void 0;
1779
+ }
1765
1780
  var FIELD_KEYS = [
1766
1781
  "label",
1767
1782
  "hint",
@@ -1774,7 +1789,9 @@ window.Aura = (() => {
1774
1789
  "suffix",
1775
1790
  "options",
1776
1791
  "placeholder",
1777
- "touchHeight"
1792
+ "touchHeight",
1793
+ "labelAddon",
1794
+ "labelAddonDescribes"
1778
1795
  ];
1779
1796
 
1780
1797
  // src/TextField.tsx
@@ -1791,6 +1808,7 @@ window.Aura = (() => {
1791
1808
  error: props.error,
1792
1809
  required: props.required,
1793
1810
  optional: props.optional,
1811
+ labelAddon: props.labelAddon,
1794
1812
  disabled: props.disabled,
1795
1813
  className: cx(touchClass(props.touchHeight), props.className)
1796
1814
  },
@@ -1805,7 +1823,7 @@ window.Aura = (() => {
1805
1823
  placeholder: props.placeholder,
1806
1824
  required: props.required,
1807
1825
  "aria-invalid": props.error ? true : rest["aria-invalid"],
1808
- "aria-describedby": [describedBy(id, props), rest["aria-describedby"]].filter(Boolean).join(" ") || void 0
1826
+ "aria-describedby": joinDescribedBy(id, props, rest["aria-describedby"])
1809
1827
  }
1810
1828
  ), props.suffix ? /* @__PURE__ */ React13.createElement("span", { className: "aura-input__suffix" }, props.suffix) : null)
1811
1829
  );
@@ -1825,6 +1843,7 @@ window.Aura = (() => {
1825
1843
  error: props.error,
1826
1844
  required: props.required,
1827
1845
  optional: props.optional,
1846
+ labelAddon: props.labelAddon,
1828
1847
  disabled: props.disabled,
1829
1848
  className: cx(touchClass(props.touchHeight), props.className)
1830
1849
  },
@@ -1839,7 +1858,7 @@ window.Aura = (() => {
1839
1858
  placeholder: props.placeholder,
1840
1859
  required: props.required,
1841
1860
  "aria-invalid": props.error ? true : rest["aria-invalid"],
1842
- "aria-describedby": [describedBy(id, props), rest["aria-describedby"]].filter(Boolean).join(" ") || void 0
1861
+ "aria-describedby": joinDescribedBy(id, props, rest["aria-describedby"])
1843
1862
  }
1844
1863
  )
1845
1864
  );
@@ -8582,6 +8601,13 @@ window.Aura = (() => {
8582
8601
  D["accent-lime"] = s[200];
8583
8602
  D["mesh-to"] = s[200];
8584
8603
  }
8604
+ function onFill(bg) {
8605
+ return contrast(ZINC[0], bg) >= contrast(INK, bg) ? ZINC[0] : INK;
8606
+ }
8607
+ L["control-checked-bg"] = L["accent-violet"];
8608
+ L["control-checked-fg"] = onFill(L["control-checked-bg"]);
8609
+ D["control-checked-bg"] = fit(b[300], darkGrounds, 3, 1);
8610
+ D["control-checked-fg"] = onFill(D["control-checked-bg"]);
8585
8611
  const shadowLight = INK, shadowDark = D["accent-violet"];
8586
8612
  if (o.primary === "brand") {
8587
8613
  L["button-primary-bg"] = fit(b[600], [ZINC[0]], 4.5, -1);
@@ -8664,6 +8690,30 @@ window.Aura = (() => {
8664
8690
  );
8665
8691
  }
8666
8692
  if (s) check("light", "ink", "accent-lime (signal)", 4.5, INK, s[200]);
8693
+ [
8694
+ [
8695
+ "light",
8696
+ L,
8697
+ [
8698
+ ["bg-surface", ZINC[0]],
8699
+ ["bg-canvas", ZINC[50]]
8700
+ ]
8701
+ ],
8702
+ [
8703
+ "dark",
8704
+ D,
8705
+ [
8706
+ ["bg-surface", ZINC[900]],
8707
+ ["bg-canvas", ZINC[950]]
8708
+ ]
8709
+ ]
8710
+ ].forEach(function(g) {
8711
+ const t = g[0], m = g[1];
8712
+ g[2].forEach(function(gr) {
8713
+ check(t, "control-checked-bg", gr[0], 3, m["control-checked-bg"], gr[1]);
8714
+ });
8715
+ check(t, "control-checked-fg", "control-checked-bg", 4.5, m["control-checked-fg"], m["control-checked-bg"]);
8716
+ });
8667
8717
  function css(selector) {
8668
8718
  if (selector !== void 0 && /[<{};@\r\n]|\/\*|\*\//.test(selector))
8669
8719
  throw new Error('createTheme: "' + selector + '" is not a plain CSS selector');
@@ -1737,6 +1737,7 @@ var Field = React12.forwardRef(function Field2(props, ref) {
1737
1737
  props.required ? /* @__PURE__ */ React12.createElement("span", { className: "aura-field__req", "aria-hidden": true }, " *") : null,
1738
1738
  props.optional ? /* @__PURE__ */ React12.createElement("span", { className: "aura-field__opt" }, " (" + t.optional + ")") : null
1739
1739
  ) : null,
1740
+ hasAddon(props.labelAddon) ? /* @__PURE__ */ React12.createElement("div", { className: "aura-field__addon", id: props.id + "-addon" }, props.labelAddon) : null,
1740
1741
  props.children,
1741
1742
  props.error ? /* @__PURE__ */ React12.createElement("p", { className: "aura-field__error", id: props.id + "-error" }, /* @__PURE__ */ React12.createElement(Icon, { name: /* @__PURE__ */ React12.createElement(IconCircleAlert, null), size: 14 }), props.error) : props.hint ? /* @__PURE__ */ React12.createElement("p", { className: "aura-field__hint", id: props.id + "-hint" }, props.hint) : null
1742
1743
  );
@@ -1744,6 +1745,20 @@ var Field = React12.forwardRef(function Field2(props, ref) {
1744
1745
  function describedBy(id, p) {
1745
1746
  return p.error ? id + "-error" : p.hint ? id + "-hint" : void 0;
1746
1747
  }
1748
+ function hasAddon(n3) {
1749
+ return n3 != null && typeof n3 !== "boolean" && n3 !== "";
1750
+ }
1751
+ function joinDescribedBy(id, p, own) {
1752
+ const ids = [
1753
+ hasAddon(p.labelAddon) && p.labelAddonDescribes !== false ? id + "-addon" : "",
1754
+ describedBy(id, p) || ""
1755
+ ].concat(typeof own === "string" ? own.split(/\s+/) : []);
1756
+ const out = [];
1757
+ ids.forEach(function(x) {
1758
+ if (x && out.indexOf(x) < 0) out.push(x);
1759
+ });
1760
+ return out.join(" ") || void 0;
1761
+ }
1747
1762
  var FIELD_KEYS = [
1748
1763
  "label",
1749
1764
  "hint",
@@ -1756,7 +1771,9 @@ var FIELD_KEYS = [
1756
1771
  "suffix",
1757
1772
  "options",
1758
1773
  "placeholder",
1759
- "touchHeight"
1774
+ "touchHeight",
1775
+ "labelAddon",
1776
+ "labelAddonDescribes"
1760
1777
  ];
1761
1778
 
1762
1779
  // src/TextField.tsx
@@ -1773,6 +1790,7 @@ var TextField = React13.forwardRef(function TextField2(props, ref) {
1773
1790
  error: props.error,
1774
1791
  required: props.required,
1775
1792
  optional: props.optional,
1793
+ labelAddon: props.labelAddon,
1776
1794
  disabled: props.disabled,
1777
1795
  className: cx(touchClass(props.touchHeight), props.className)
1778
1796
  },
@@ -1787,7 +1805,7 @@ var TextField = React13.forwardRef(function TextField2(props, ref) {
1787
1805
  placeholder: props.placeholder,
1788
1806
  required: props.required,
1789
1807
  "aria-invalid": props.error ? true : rest["aria-invalid"],
1790
- "aria-describedby": [describedBy(id, props), rest["aria-describedby"]].filter(Boolean).join(" ") || void 0
1808
+ "aria-describedby": joinDescribedBy(id, props, rest["aria-describedby"])
1791
1809
  }
1792
1810
  ), props.suffix ? /* @__PURE__ */ React13.createElement("span", { className: "aura-input__suffix" }, props.suffix) : null)
1793
1811
  );
@@ -1807,6 +1825,7 @@ var Textarea = React14.forwardRef(function Textarea2(props, ref) {
1807
1825
  error: props.error,
1808
1826
  required: props.required,
1809
1827
  optional: props.optional,
1828
+ labelAddon: props.labelAddon,
1810
1829
  disabled: props.disabled,
1811
1830
  className: cx(touchClass(props.touchHeight), props.className)
1812
1831
  },
@@ -1821,7 +1840,7 @@ var Textarea = React14.forwardRef(function Textarea2(props, ref) {
1821
1840
  placeholder: props.placeholder,
1822
1841
  required: props.required,
1823
1842
  "aria-invalid": props.error ? true : rest["aria-invalid"],
1824
- "aria-describedby": [describedBy(id, props), rest["aria-describedby"]].filter(Boolean).join(" ") || void 0
1843
+ "aria-describedby": joinDescribedBy(id, props, rest["aria-describedby"])
1825
1844
  }
1826
1845
  )
1827
1846
  );
@@ -8564,6 +8583,13 @@ function createTheme(opts) {
8564
8583
  D["accent-lime"] = s[200];
8565
8584
  D["mesh-to"] = s[200];
8566
8585
  }
8586
+ function onFill(bg) {
8587
+ return contrast(ZINC[0], bg) >= contrast(INK, bg) ? ZINC[0] : INK;
8588
+ }
8589
+ L["control-checked-bg"] = L["accent-violet"];
8590
+ L["control-checked-fg"] = onFill(L["control-checked-bg"]);
8591
+ D["control-checked-bg"] = fit(b[300], darkGrounds, 3, 1);
8592
+ D["control-checked-fg"] = onFill(D["control-checked-bg"]);
8567
8593
  const shadowLight = INK, shadowDark = D["accent-violet"];
8568
8594
  if (o.primary === "brand") {
8569
8595
  L["button-primary-bg"] = fit(b[600], [ZINC[0]], 4.5, -1);
@@ -8646,6 +8672,30 @@ function createTheme(opts) {
8646
8672
  );
8647
8673
  }
8648
8674
  if (s) check("light", "ink", "accent-lime (signal)", 4.5, INK, s[200]);
8675
+ [
8676
+ [
8677
+ "light",
8678
+ L,
8679
+ [
8680
+ ["bg-surface", ZINC[0]],
8681
+ ["bg-canvas", ZINC[50]]
8682
+ ]
8683
+ ],
8684
+ [
8685
+ "dark",
8686
+ D,
8687
+ [
8688
+ ["bg-surface", ZINC[900]],
8689
+ ["bg-canvas", ZINC[950]]
8690
+ ]
8691
+ ]
8692
+ ].forEach(function(g) {
8693
+ const t = g[0], m = g[1];
8694
+ g[2].forEach(function(gr) {
8695
+ check(t, "control-checked-bg", gr[0], 3, m["control-checked-bg"], gr[1]);
8696
+ });
8697
+ check(t, "control-checked-fg", "control-checked-bg", 4.5, m["control-checked-fg"], m["control-checked-bg"]);
8698
+ });
8649
8699
  function css(selector) {
8650
8700
  if (selector !== void 0 && /[<{};@\r\n]|\/\*|\*\//.test(selector))
8651
8701
  throw new Error('createTheme: "' + selector + '" is not a plain CSS selector');
package/dist/esm/Field.js CHANGED
@@ -20,6 +20,7 @@ const Field = React.forwardRef(function Field2(props, ref) {
20
20
  props.required ? /* @__PURE__ */ React.createElement("span", { className: "aura-field__req", "aria-hidden": true }, " *") : null,
21
21
  props.optional ? /* @__PURE__ */ React.createElement("span", { className: "aura-field__opt" }, " (" + t.optional + ")") : null
22
22
  ) : null,
23
+ hasAddon(props.labelAddon) ? /* @__PURE__ */ React.createElement("div", { className: "aura-field__addon", id: props.id + "-addon" }, props.labelAddon) : null,
23
24
  props.children,
24
25
  props.error ? /* @__PURE__ */ React.createElement("p", { className: "aura-field__error", id: props.id + "-error" }, /* @__PURE__ */ React.createElement(Icon, { name: /* @__PURE__ */ React.createElement(IconCircleAlert, null), size: 14 }), props.error) : props.hint ? /* @__PURE__ */ React.createElement("p", { className: "aura-field__hint", id: props.id + "-hint" }, props.hint) : null
25
26
  );
@@ -27,6 +28,20 @@ const Field = React.forwardRef(function Field2(props, ref) {
27
28
  function describedBy(id, p) {
28
29
  return p.error ? id + "-error" : p.hint ? id + "-hint" : void 0;
29
30
  }
31
+ function hasAddon(n) {
32
+ return n != null && typeof n !== "boolean" && n !== "";
33
+ }
34
+ function joinDescribedBy(id, p, own) {
35
+ const ids = [
36
+ hasAddon(p.labelAddon) && p.labelAddonDescribes !== false ? id + "-addon" : "",
37
+ describedBy(id, p) || ""
38
+ ].concat(typeof own === "string" ? own.split(/\s+/) : []);
39
+ const out = [];
40
+ ids.forEach(function(x) {
41
+ if (x && out.indexOf(x) < 0) out.push(x);
42
+ });
43
+ return out.join(" ") || void 0;
44
+ }
30
45
  const FIELD_KEYS = [
31
46
  "label",
32
47
  "hint",
@@ -39,10 +54,14 @@ const FIELD_KEYS = [
39
54
  "suffix",
40
55
  "options",
41
56
  "placeholder",
42
- "touchHeight"
57
+ "touchHeight",
58
+ "labelAddon",
59
+ "labelAddonDescribes"
43
60
  ];
44
61
  export {
45
62
  FIELD_KEYS,
46
63
  Field,
47
- describedBy
64
+ describedBy,
65
+ hasAddon,
66
+ joinDescribedBy
48
67
  };
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import * as React from "react";
3
- import { FIELD_KEYS, Field, describedBy } from "./Field.js";
3
+ import { FIELD_KEYS, Field, joinDescribedBy } from "./Field.js";
4
4
  import { Icon } from "./Icon.js";
5
5
  import { cx, omit, touchClass, uid } from "./internal.js";
6
6
  const TextField = React.forwardRef(function TextField2(props, ref) {
@@ -15,6 +15,7 @@ const TextField = React.forwardRef(function TextField2(props, ref) {
15
15
  error: props.error,
16
16
  required: props.required,
17
17
  optional: props.optional,
18
+ labelAddon: props.labelAddon,
18
19
  disabled: props.disabled,
19
20
  className: cx(touchClass(props.touchHeight), props.className)
20
21
  },
@@ -29,7 +30,7 @@ const TextField = React.forwardRef(function TextField2(props, ref) {
29
30
  placeholder: props.placeholder,
30
31
  required: props.required,
31
32
  "aria-invalid": props.error ? true : rest["aria-invalid"],
32
- "aria-describedby": [describedBy(id, props), rest["aria-describedby"]].filter(Boolean).join(" ") || void 0
33
+ "aria-describedby": joinDescribedBy(id, props, rest["aria-describedby"])
33
34
  }
34
35
  ), props.suffix ? /* @__PURE__ */ React.createElement("span", { className: "aura-input__suffix" }, props.suffix) : null)
35
36
  );
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import * as React from "react";
3
- import { FIELD_KEYS, Field, describedBy } from "./Field.js";
3
+ import { FIELD_KEYS, Field, joinDescribedBy } from "./Field.js";
4
4
  import { cx, omit, touchClass, uid } from "./internal.js";
5
5
  const Textarea = React.forwardRef(function Textarea2(props, ref) {
6
6
  const auto = uid(), id = props.id || auto;
@@ -14,6 +14,7 @@ const Textarea = React.forwardRef(function Textarea2(props, ref) {
14
14
  error: props.error,
15
15
  required: props.required,
16
16
  optional: props.optional,
17
+ labelAddon: props.labelAddon,
17
18
  disabled: props.disabled,
18
19
  className: cx(touchClass(props.touchHeight), props.className)
19
20
  },
@@ -28,7 +29,7 @@ const Textarea = React.forwardRef(function Textarea2(props, ref) {
28
29
  placeholder: props.placeholder,
29
30
  required: props.required,
30
31
  "aria-invalid": props.error ? true : rest["aria-invalid"],
31
- "aria-describedby": [describedBy(id, props), rest["aria-describedby"]].filter(Boolean).join(" ") || void 0
32
+ "aria-describedby": joinDescribedBy(id, props, rest["aria-describedby"])
32
33
  }
33
34
  )
34
35
  );
package/dist/esm/theme.js CHANGED
@@ -162,6 +162,13 @@ function createTheme(opts) {
162
162
  D["accent-lime"] = s[200];
163
163
  D["mesh-to"] = s[200];
164
164
  }
165
+ function onFill(bg) {
166
+ return contrast(ZINC[0], bg) >= contrast(INK, bg) ? ZINC[0] : INK;
167
+ }
168
+ L["control-checked-bg"] = L["accent-violet"];
169
+ L["control-checked-fg"] = onFill(L["control-checked-bg"]);
170
+ D["control-checked-bg"] = fit(b[300], darkGrounds, 3, 1);
171
+ D["control-checked-fg"] = onFill(D["control-checked-bg"]);
165
172
  const shadowLight = INK, shadowDark = D["accent-violet"];
166
173
  if (o.primary === "brand") {
167
174
  L["button-primary-bg"] = fit(b[600], [ZINC[0]], 4.5, -1);
@@ -244,6 +251,30 @@ function createTheme(opts) {
244
251
  );
245
252
  }
246
253
  if (s) check("light", "ink", "accent-lime (signal)", 4.5, INK, s[200]);
254
+ [
255
+ [
256
+ "light",
257
+ L,
258
+ [
259
+ ["bg-surface", ZINC[0]],
260
+ ["bg-canvas", ZINC[50]]
261
+ ]
262
+ ],
263
+ [
264
+ "dark",
265
+ D,
266
+ [
267
+ ["bg-surface", ZINC[900]],
268
+ ["bg-canvas", ZINC[950]]
269
+ ]
270
+ ]
271
+ ].forEach(function(g) {
272
+ const t = g[0], m = g[1];
273
+ g[2].forEach(function(gr) {
274
+ check(t, "control-checked-bg", gr[0], 3, m["control-checked-bg"], gr[1]);
275
+ });
276
+ check(t, "control-checked-fg", "control-checked-bg", 4.5, m["control-checked-fg"], m["control-checked-bg"]);
277
+ });
247
278
  function css(selector) {
248
279
  if (selector !== void 0 && /[<{};@\r\n]|\/\*|\*\//.test(selector))
249
280
  throw new Error('createTheme: "' + selector + '" is not a plain CSS selector');
package/dist/index.d.cts CHANGED
@@ -415,6 +415,13 @@ export interface TextFieldProps extends FieldProps, Omit<React$1.InputHTMLAttrib
415
415
  icon?: IconInput | undefined;
416
416
  /** Trailing unit, e.g. "THB". */
417
417
  suffix?: React$1.ReactNode | undefined;
418
+ /** 5.31 (Chamber-OS addendum 41): a node between the label and the box — e.g. the phrase to type, with a copy
419
+ * button, on a typed confirmation. The hint and error stay below. Its box's id (`{id}-addon`) comes first in the
420
+ * input's `aria-describedby`, so its text is read after the label; keep any button names in it short. */
421
+ labelAddon?: React$1.ReactNode | undefined;
422
+ /** `false` keeps the addon out of `aria-describedby` — for one that isn't a description (a "Forgot password?"
423
+ * link), or to name only part of it: give that part an id and pass it in `aria-describedby`. Default `true`. */
424
+ labelAddonDescribes?: boolean | undefined;
418
425
  /** 44px box (5.30, Chamber-OS 135): `true` below 640px or on a coarse pointer, as Button's `touchHeight`; `'always'`
419
426
  * at every width, also with a mouse in compact density — for a form whose spec asks for 44px targets everywhere.
420
427
  * Default: the density's input height. */
@@ -540,6 +547,13 @@ export interface CommandProps {
540
547
  className?: string | undefined;
541
548
  }
542
549
  export interface TextareaProps extends FieldProps, Omit<React$1.TextareaHTMLAttributes<HTMLTextAreaElement>, "required"> {
550
+ /** 5.31 (Chamber-OS addendum 41): a node between the label and the box — e.g. the phrase to type, with a copy
551
+ * button, on a typed confirmation. The hint and error stay below. Its box's id (`{id}-addon`) comes first in the
552
+ * input's `aria-describedby`, so its text is read after the label; keep any button names in it short. */
553
+ labelAddon?: React$1.ReactNode | undefined;
554
+ /** `false` keeps the addon out of `aria-describedby` — for one that isn't a description (a "Forgot password?"
555
+ * link), or to name only part of it: give that part an id and pass it in `aria-describedby`. Default `true`. */
556
+ labelAddonDescribes?: boolean | undefined;
543
557
  /** Accepted like the other fields' (5.30, Chamber-OS 135), so a form can pass it to every field. A Textarea is
544
558
  * always at least 96px tall, well over the 44px target, so it changes nothing you can see. */
545
559
  touchHeight?: TouchHeight | undefined;
@@ -1634,6 +1648,9 @@ export interface FieldComponentProps extends Omit<FieldPropsPublic, "label"> {
1634
1648
  /** The control's id: the label points at it, and the hint/error ids derive from it. */
1635
1649
  id?: string | undefined;
1636
1650
  children?: React$1.ReactNode | undefined;
1651
+ /** 5.31 (Chamber-OS addendum 41): a node between the label and the control, in a `.aura-field__addon` box with the
1652
+ * id `{id}-addon`. TextField and Textarea add that id to the input's `aria-describedby`; a custom control should too. */
1653
+ labelAddon?: React$1.ReactNode | undefined;
1637
1654
  /** Render the label as another element (e.g. `span` for a group of controls) with this id. */
1638
1655
  labelAs?: string | undefined;
1639
1656
  labelId?: string | undefined;
package/dist/index.d.ts CHANGED
@@ -415,6 +415,13 @@ export interface TextFieldProps extends FieldProps, Omit<React$1.InputHTMLAttrib
415
415
  icon?: IconInput | undefined;
416
416
  /** Trailing unit, e.g. "THB". */
417
417
  suffix?: React$1.ReactNode | undefined;
418
+ /** 5.31 (Chamber-OS addendum 41): a node between the label and the box — e.g. the phrase to type, with a copy
419
+ * button, on a typed confirmation. The hint and error stay below. Its box's id (`{id}-addon`) comes first in the
420
+ * input's `aria-describedby`, so its text is read after the label; keep any button names in it short. */
421
+ labelAddon?: React$1.ReactNode | undefined;
422
+ /** `false` keeps the addon out of `aria-describedby` — for one that isn't a description (a "Forgot password?"
423
+ * link), or to name only part of it: give that part an id and pass it in `aria-describedby`. Default `true`. */
424
+ labelAddonDescribes?: boolean | undefined;
418
425
  /** 44px box (5.30, Chamber-OS 135): `true` below 640px or on a coarse pointer, as Button's `touchHeight`; `'always'`
419
426
  * at every width, also with a mouse in compact density — for a form whose spec asks for 44px targets everywhere.
420
427
  * Default: the density's input height. */
@@ -540,6 +547,13 @@ export interface CommandProps {
540
547
  className?: string | undefined;
541
548
  }
542
549
  export interface TextareaProps extends FieldProps, Omit<React$1.TextareaHTMLAttributes<HTMLTextAreaElement>, "required"> {
550
+ /** 5.31 (Chamber-OS addendum 41): a node between the label and the box — e.g. the phrase to type, with a copy
551
+ * button, on a typed confirmation. The hint and error stay below. Its box's id (`{id}-addon`) comes first in the
552
+ * input's `aria-describedby`, so its text is read after the label; keep any button names in it short. */
553
+ labelAddon?: React$1.ReactNode | undefined;
554
+ /** `false` keeps the addon out of `aria-describedby` — for one that isn't a description (a "Forgot password?"
555
+ * link), or to name only part of it: give that part an id and pass it in `aria-describedby`. Default `true`. */
556
+ labelAddonDescribes?: boolean | undefined;
543
557
  /** Accepted like the other fields' (5.30, Chamber-OS 135), so a form can pass it to every field. A Textarea is
544
558
  * always at least 96px tall, well over the 44px target, so it changes nothing you can see. */
545
559
  touchHeight?: TouchHeight | undefined;
@@ -1634,6 +1648,9 @@ export interface FieldComponentProps extends Omit<FieldPropsPublic, "label"> {
1634
1648
  /** The control's id: the label points at it, and the hint/error ids derive from it. */
1635
1649
  id?: string | undefined;
1636
1650
  children?: React$1.ReactNode | undefined;
1651
+ /** 5.31 (Chamber-OS addendum 41): a node between the label and the control, in a `.aura-field__addon` box with the
1652
+ * id `{id}-addon`. TextField and Textarea add that id to the input's `aria-describedby`; a custom control should too. */
1653
+ labelAddon?: React$1.ReactNode | undefined;
1637
1654
  /** Render the label as another element (e.g. `span` for a group of controls) with this id. */
1638
1655
  labelAs?: string | undefined;
1639
1656
  labelId?: string | undefined;
@@ -768,6 +768,13 @@ function createTheme(opts) {
768
768
  D["accent-lime"] = s[200];
769
769
  D["mesh-to"] = s[200];
770
770
  }
771
+ function onFill(bg) {
772
+ return contrast(ZINC[0], bg) >= contrast(INK, bg) ? ZINC[0] : INK;
773
+ }
774
+ L["control-checked-bg"] = L["accent-violet"];
775
+ L["control-checked-fg"] = onFill(L["control-checked-bg"]);
776
+ D["control-checked-bg"] = fit(b[300], darkGrounds, 3, 1);
777
+ D["control-checked-fg"] = onFill(D["control-checked-bg"]);
771
778
  const shadowLight = INK, shadowDark = D["accent-violet"];
772
779
  if (o.primary === "brand") {
773
780
  L["button-primary-bg"] = fit(b[600], [ZINC[0]], 4.5, -1);
@@ -850,6 +857,30 @@ function createTheme(opts) {
850
857
  );
851
858
  }
852
859
  if (s) check("light", "ink", "accent-lime (signal)", 4.5, INK, s[200]);
860
+ [
861
+ [
862
+ "light",
863
+ L,
864
+ [
865
+ ["bg-surface", ZINC[0]],
866
+ ["bg-canvas", ZINC[50]]
867
+ ]
868
+ ],
869
+ [
870
+ "dark",
871
+ D,
872
+ [
873
+ ["bg-surface", ZINC[900]],
874
+ ["bg-canvas", ZINC[950]]
875
+ ]
876
+ ]
877
+ ].forEach(function(g) {
878
+ const t = g[0], m = g[1];
879
+ g[2].forEach(function(gr) {
880
+ check(t, "control-checked-bg", gr[0], 3, m["control-checked-bg"], gr[1]);
881
+ });
882
+ check(t, "control-checked-fg", "control-checked-bg", 4.5, m["control-checked-fg"], m["control-checked-bg"]);
883
+ });
853
884
  function css(selector) {
854
885
  if (selector !== void 0 && /[<{};@\r\n]|\/\*|\*\//.test(selector))
855
886
  throw new Error('createTheme: "' + selector + '" is not a plain CSS selector');
@@ -708,6 +708,13 @@ function createTheme(opts) {
708
708
  D["accent-lime"] = s[200];
709
709
  D["mesh-to"] = s[200];
710
710
  }
711
+ function onFill(bg) {
712
+ return contrast(ZINC[0], bg) >= contrast(INK, bg) ? ZINC[0] : INK;
713
+ }
714
+ L["control-checked-bg"] = L["accent-violet"];
715
+ L["control-checked-fg"] = onFill(L["control-checked-bg"]);
716
+ D["control-checked-bg"] = fit(b[300], darkGrounds, 3, 1);
717
+ D["control-checked-fg"] = onFill(D["control-checked-bg"]);
711
718
  const shadowLight = INK, shadowDark = D["accent-violet"];
712
719
  if (o.primary === "brand") {
713
720
  L["button-primary-bg"] = fit(b[600], [ZINC[0]], 4.5, -1);
@@ -790,6 +797,30 @@ function createTheme(opts) {
790
797
  );
791
798
  }
792
799
  if (s) check("light", "ink", "accent-lime (signal)", 4.5, INK, s[200]);
800
+ [
801
+ [
802
+ "light",
803
+ L,
804
+ [
805
+ ["bg-surface", ZINC[0]],
806
+ ["bg-canvas", ZINC[50]]
807
+ ]
808
+ ],
809
+ [
810
+ "dark",
811
+ D,
812
+ [
813
+ ["bg-surface", ZINC[900]],
814
+ ["bg-canvas", ZINC[950]]
815
+ ]
816
+ ]
817
+ ].forEach(function(g) {
818
+ const t = g[0], m = g[1];
819
+ g[2].forEach(function(gr) {
820
+ check(t, "control-checked-bg", gr[0], 3, m["control-checked-bg"], gr[1]);
821
+ });
822
+ check(t, "control-checked-fg", "control-checked-bg", 4.5, m["control-checked-fg"], m["control-checked-bg"]);
823
+ });
793
824
  function css(selector) {
794
825
  if (selector !== void 0 && /[<{};@\r\n]|\/\*|\*\//.test(selector))
795
826
  throw new Error('createTheme: "' + selector + '" is not a plain CSS selector');
package/dist/styles.css CHANGED
@@ -474,6 +474,8 @@ a.aura-icon-btn { text-decoration: none; }
474
474
  .aura-field__hint, .aura-field__error { display: flex; align-items: flex-start; gap: var(--aura-space-1); margin: 0; font-size: 12px; line-height: 1.5; color: var(--aura-fg-secondary); }
475
475
  .aura-field__error { color: var(--aura-fg-danger); }
476
476
  .aura-field__error .aura-icon { margin-top: 2px; }
477
+ /* 5.31 (Chamber-OS addendum 41): TextField/Textarea `labelAddon`, between the label and the box, in the field's gap. */
478
+ .aura-field__addon { min-width: 0; font-size: 13px; line-height: 1.5; color: var(--aura-fg-primary); }
477
479
  .aura-field.is-disabled .aura-field__label { color: var(--aura-fg-secondary); }
478
480
 
479
481
  .aura-input {
@@ -2077,7 +2079,7 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
2077
2079
  /* 5.30 (audit D): field labels, hints and errors and choice descriptions get Thai line room; a stacked card's field
2078
2080
  * labels are 12px in the sans face (the mono face has no Thai), and a stacked DataTable card's values sit on a set
2079
2081
  * 1.6 line, so a loading card (its bars follow 1lh) is as tall as one with Thai text. */
2080
- .aura-field__label:lang(th), .aura-field__hint:lang(th), .aura-field__error:lang(th) { line-height: 1.6; }
2082
+ .aura-field__label:lang(th), .aura-field__hint:lang(th), .aura-field__error:lang(th), .aura-field__addon:lang(th) { line-height: 1.6; }
2081
2083
  .aura-choice__desc:lang(th), .aura-check__desc:lang(th) { font-size: 13px; line-height: 1.6; }
2082
2084
  .aura-tbl__row > .aura-tbl__td[data-label]:lang(th)::before, .aura-table__td[data-label]:lang(th)::before { font-family: var(--font-sans); font-size: 12px; line-height: 18px; }
2083
2085
  .aura-table--stacked .aura-table__td[data-label]:lang(th) { line-height: 1.6; }
@@ -476,6 +476,8 @@ a.aura-icon-btn { text-decoration: none; }
476
476
  .aura-field__hint, .aura-field__error { display: flex; align-items: flex-start; gap: var(--aura-space-1); margin: 0; font-size: 12px; line-height: 1.5; color: var(--aura-fg-secondary); }
477
477
  .aura-field__error { color: var(--aura-fg-danger); }
478
478
  .aura-field__error .aura-icon { margin-top: 2px; }
479
+ /* 5.31 (Chamber-OS addendum 41): TextField/Textarea `labelAddon`, between the label and the box, in the field's gap. */
480
+ .aura-field__addon { min-width: 0; font-size: 13px; line-height: 1.5; color: var(--aura-fg-primary); }
479
481
  .aura-field.is-disabled .aura-field__label { color: var(--aura-fg-secondary); }
480
482
 
481
483
  .aura-input {
@@ -2079,7 +2081,7 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
2079
2081
  /* 5.30 (audit D): field labels, hints and errors and choice descriptions get Thai line room; a stacked card's field
2080
2082
  * labels are 12px in the sans face (the mono face has no Thai), and a stacked DataTable card's values sit on a set
2081
2083
  * 1.6 line, so a loading card (its bars follow 1lh) is as tall as one with Thai text. */
2082
- .aura-field__label:lang(th), .aura-field__hint:lang(th), .aura-field__error:lang(th) { line-height: 1.6; }
2084
+ .aura-field__label:lang(th), .aura-field__hint:lang(th), .aura-field__error:lang(th), .aura-field__addon:lang(th) { line-height: 1.6; }
2083
2085
  .aura-choice__desc:lang(th), .aura-check__desc:lang(th) { font-size: 13px; line-height: 1.6; }
2084
2086
  .aura-tbl__row > .aura-tbl__td[data-label]:lang(th)::before, .aura-table__td[data-label]:lang(th)::before { font-family: var(--font-sans); font-size: 12px; line-height: 18px; }
2085
2087
  .aura-table--stacked .aura-table__td[data-label]:lang(th) { line-height: 1.6; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jirawatpyk/aura-react",
3
- "version": "5.30.0",
3
+ "version": "5.31.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",