@jirawatpyk/aura-react 5.48.0 → 5.50.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,10 +186,23 @@ 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.50 — Chamber-OS addendum 57 (#156)
190
+
191
+ - **`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.
192
+ - **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.
193
+
194
+ ```jsx
195
+ <TextField label="Primary colour" hint="A hex value." value={colour} onChange={…} invalid={!meetsAa} aria-describedby="contrast-readout" />
196
+ ```
197
+
198
+ ## 5.49 — Chamber-OS addendum 56 (#155)
199
+
200
+ - **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.
201
+
189
202
  ## 5.48 — Chamber-OS addendum 55 (#153–154)
190
203
 
191
204
  - **The `actionsBarBelow` bar is now ActionBar's edge strip** (#153): the full width of the screen, on its bottom edge (or on a BottomNav's top edge), a top border only — no radius, no shadow — with ActionBar's inner padding (8px block, 16px start, 12px end) and the home-indicator inset inside it, as the save and bulk bars of an app built on `ActionBar position="container"` look. The page's end padding and `scroll-padding-bottom` follow the new height. The layout inside is unchanged.
192
- - **Bottom bars on a short screen** (#154): on a screen 480px tall or less — 400% zoom on a desktop (320×256 CSS px), a phone held sideways — `actionsBarBelow` no longer fixes the actions to the bottom: they stay in the header's own row (laid out by `actionsFullWidthBelow` if set), with no status line, page-end space or scroll padding; and an `ActionBar` (either `position`) stops sticking and sits at the end of its container, in the flow. AppShell's top bar scrolls away with the page there too, so a field reached backwards (Shift+Tab) isn't left under it, and `--aura-shell-bar-height` is 0 meanwhile, so nothing sticky (a DataTable `stickyHeader`) leaves a gap for it. A focused control is never under a bar there (WCAG 2.4.11, 1.4.10); from 481px tall everything is as before. An ActionBar with `hidesBottomNav` still hides the BottomNav on a short screen while it sits at the end of its container. Wherever a BottomNav shows (any height), the page's `scroll-padding-bottom` now reserves its height, so a field tabbed into view stops above it.
205
+ - **Bottom bars on a short screen** (#154): on a screen 30rem tall or less — 480px at the browser's default 16px text, and since 5.49 (#155) it follows the default font size set in Chrome, Edge or Firefox (960px at 32px, 720px at 24px), so large-text users get the same room (iOS Dynamic Type and Safari's text-size controls don't change it); 400% zoom on a desktop (320×256 CSS px), a phone held sideways — `actionsBarBelow` no longer fixes the actions to the bottom: they stay in the header's own row (laid out by `actionsFullWidthBelow` if set), with no status line, page-end space or scroll padding; and an `ActionBar` (either `position`) stops sticking and sits at the end of its container, in the flow. AppShell's top bar scrolls away with the page there too, so a field reached backwards (Shift+Tab) isn't left under it, and `--aura-shell-bar-height` is 0 meanwhile, so nothing sticky (a DataTable `stickyHeader`) leaves a gap for it. A focused control is never under a bar there (WCAG 2.4.11, 1.4.10); taller than 30rem everything is as before. The width breakpoints stay in px. An ActionBar with `hidesBottomNav` still hides the BottomNav on a short screen while it sits at the end of its container. Wherever a BottomNav shows (any height), the page's `scroll-padding-bottom` now reserves its height, so a field tabbed into view stops above it.
193
206
  - **`actionsBarLook="floating"`** keeps 5.45's card (inset 16px, lifted 12px, bordered all round, rounded, with the overlay shadow) — the look of `ActionBar position="viewport"`. Only the bar's look changed; nothing else about `actionsBarBelow`.
194
207
 
195
208
  ## 5.47 — Chamber-OS addendum 54 (#152)
@@ -1992,7 +1992,12 @@ window.Aura = (() => {
1992
1992
  "div",
1993
1993
  {
1994
1994
  ref,
1995
- className: cx("aura-field", props.error && "is-invalid", props.disabled && "is-disabled", props.className)
1995
+ className: cx(
1996
+ "aura-field",
1997
+ (props.error || props.invalid) && "is-invalid",
1998
+ props.disabled && "is-disabled",
1999
+ props.className
2000
+ )
1996
2001
  },
1997
2002
  props.label ? h3(
1998
2003
  props.labelAs || "label",
@@ -2037,7 +2042,8 @@ window.Aura = (() => {
2037
2042
  "placeholder",
2038
2043
  "touchHeight",
2039
2044
  "labelAddon",
2040
- "labelAddonDescribes"
2045
+ "labelAddonDescribes",
2046
+ "invalid"
2041
2047
  ];
2042
2048
 
2043
2049
  // src/TextField.tsx
@@ -2052,6 +2058,7 @@ window.Aura = (() => {
2052
2058
  label: props.label,
2053
2059
  hint: props.hint,
2054
2060
  error: props.error,
2061
+ invalid: props.invalid,
2055
2062
  required: props.required,
2056
2063
  optional: props.optional,
2057
2064
  labelAddon: props.labelAddon,
@@ -2068,7 +2075,7 @@ window.Aura = (() => {
2068
2075
  className: "aura-input__control",
2069
2076
  placeholder: props.placeholder,
2070
2077
  required: props.required,
2071
- "aria-invalid": props.error ? true : rest["aria-invalid"],
2078
+ "aria-invalid": props.error || props.invalid ? true : rest["aria-invalid"],
2072
2079
  "aria-describedby": joinDescribedBy(id, props, rest["aria-describedby"])
2073
2080
  }
2074
2081
  ), props.suffix ? /* @__PURE__ */ React14.createElement("span", { className: "aura-input__suffix" }, props.suffix) : null)
@@ -2087,6 +2094,7 @@ window.Aura = (() => {
2087
2094
  label: props.label,
2088
2095
  hint: props.hint,
2089
2096
  error: props.error,
2097
+ invalid: props.invalid,
2090
2098
  required: props.required,
2091
2099
  optional: props.optional,
2092
2100
  labelAddon: props.labelAddon,
@@ -2103,7 +2111,7 @@ window.Aura = (() => {
2103
2111
  className: "aura-input aura-textarea",
2104
2112
  placeholder: props.placeholder,
2105
2113
  required: props.required,
2106
- "aria-invalid": props.error ? true : rest["aria-invalid"],
2114
+ "aria-invalid": props.error || props.invalid ? true : rest["aria-invalid"],
2107
2115
  "aria-describedby": joinDescribedBy(id, props, rest["aria-describedby"])
2108
2116
  }
2109
2117
  )
@@ -8333,6 +8341,7 @@ window.Aura = (() => {
8333
8341
  }
8334
8342
  const rootRef = React48.useRef(null);
8335
8343
  const barRef = React48.useRef(null);
8344
+ const probeRef = React48.useRef(null);
8336
8345
  const mergedRef = useMergedRef(ref, rootRef);
8337
8346
  const hasBar = !!(props.header || props.nav);
8338
8347
  const barH = React48.useState(null);
@@ -8362,6 +8371,7 @@ window.Aura = (() => {
8362
8371
  sync();
8363
8372
  const ro = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(sync);
8364
8373
  if (ro) ro.observe(bar);
8374
+ if (ro && probeRef.current) ro.observe(probeRef.current);
8365
8375
  window.addEventListener("resize", sync);
8366
8376
  return function() {
8367
8377
  if (ro) ro.disconnect();
@@ -8445,7 +8455,8 @@ window.Aura = (() => {
8445
8455
  "aria-expanded": open
8446
8456
  }
8447
8457
  ) : null,
8448
- /* @__PURE__ */ React48.createElement("div", { className: "aura-shell__bar-content" }, props.header)
8458
+ /* @__PURE__ */ React48.createElement("div", { className: "aura-shell__bar-content" }, props.header),
8459
+ /* @__PURE__ */ React48.createElement("span", { className: "aura-shell__probe", "aria-hidden": "true", ref: probeRef })
8449
8460
  ) : null, /* @__PURE__ */ React48.createElement(
8450
8461
  "main",
8451
8462
  {
@@ -1974,7 +1974,12 @@ var Field = React13.forwardRef(function Field2(props, ref) {
1974
1974
  "div",
1975
1975
  {
1976
1976
  ref,
1977
- className: cx("aura-field", props.error && "is-invalid", props.disabled && "is-disabled", props.className)
1977
+ className: cx(
1978
+ "aura-field",
1979
+ (props.error || props.invalid) && "is-invalid",
1980
+ props.disabled && "is-disabled",
1981
+ props.className
1982
+ )
1978
1983
  },
1979
1984
  props.label ? h3(
1980
1985
  props.labelAs || "label",
@@ -2019,7 +2024,8 @@ var FIELD_KEYS = [
2019
2024
  "placeholder",
2020
2025
  "touchHeight",
2021
2026
  "labelAddon",
2022
- "labelAddonDescribes"
2027
+ "labelAddonDescribes",
2028
+ "invalid"
2023
2029
  ];
2024
2030
 
2025
2031
  // src/TextField.tsx
@@ -2034,6 +2040,7 @@ var TextField = React14.forwardRef(function TextField2(props, ref) {
2034
2040
  label: props.label,
2035
2041
  hint: props.hint,
2036
2042
  error: props.error,
2043
+ invalid: props.invalid,
2037
2044
  required: props.required,
2038
2045
  optional: props.optional,
2039
2046
  labelAddon: props.labelAddon,
@@ -2050,7 +2057,7 @@ var TextField = React14.forwardRef(function TextField2(props, ref) {
2050
2057
  className: "aura-input__control",
2051
2058
  placeholder: props.placeholder,
2052
2059
  required: props.required,
2053
- "aria-invalid": props.error ? true : rest["aria-invalid"],
2060
+ "aria-invalid": props.error || props.invalid ? true : rest["aria-invalid"],
2054
2061
  "aria-describedby": joinDescribedBy(id, props, rest["aria-describedby"])
2055
2062
  }
2056
2063
  ), props.suffix ? /* @__PURE__ */ React14.createElement("span", { className: "aura-input__suffix" }, props.suffix) : null)
@@ -2069,6 +2076,7 @@ var Textarea = React15.forwardRef(function Textarea2(props, ref) {
2069
2076
  label: props.label,
2070
2077
  hint: props.hint,
2071
2078
  error: props.error,
2079
+ invalid: props.invalid,
2072
2080
  required: props.required,
2073
2081
  optional: props.optional,
2074
2082
  labelAddon: props.labelAddon,
@@ -2085,7 +2093,7 @@ var Textarea = React15.forwardRef(function Textarea2(props, ref) {
2085
2093
  className: "aura-input aura-textarea",
2086
2094
  placeholder: props.placeholder,
2087
2095
  required: props.required,
2088
- "aria-invalid": props.error ? true : rest["aria-invalid"],
2096
+ "aria-invalid": props.error || props.invalid ? true : rest["aria-invalid"],
2089
2097
  "aria-describedby": joinDescribedBy(id, props, rest["aria-describedby"])
2090
2098
  }
2091
2099
  )
@@ -8315,6 +8323,7 @@ var AppShell = React48.forwardRef(function AppShell2(props, ref) {
8315
8323
  }
8316
8324
  const rootRef = React48.useRef(null);
8317
8325
  const barRef = React48.useRef(null);
8326
+ const probeRef = React48.useRef(null);
8318
8327
  const mergedRef = useMergedRef(ref, rootRef);
8319
8328
  const hasBar = !!(props.header || props.nav);
8320
8329
  const barH = React48.useState(null);
@@ -8344,6 +8353,7 @@ var AppShell = React48.forwardRef(function AppShell2(props, ref) {
8344
8353
  sync();
8345
8354
  const ro = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(sync);
8346
8355
  if (ro) ro.observe(bar);
8356
+ if (ro && probeRef.current) ro.observe(probeRef.current);
8347
8357
  window.addEventListener("resize", sync);
8348
8358
  return function() {
8349
8359
  if (ro) ro.disconnect();
@@ -8427,7 +8437,8 @@ var AppShell = React48.forwardRef(function AppShell2(props, ref) {
8427
8437
  "aria-expanded": open
8428
8438
  }
8429
8439
  ) : null,
8430
- /* @__PURE__ */ React48.createElement("div", { className: "aura-shell__bar-content" }, props.header)
8440
+ /* @__PURE__ */ React48.createElement("div", { className: "aura-shell__bar-content" }, props.header),
8441
+ /* @__PURE__ */ React48.createElement("span", { className: "aura-shell__probe", "aria-hidden": "true", ref: probeRef })
8431
8442
  ) : null, /* @__PURE__ */ React48.createElement(
8432
8443
  "main",
8433
8444
  {
@@ -30,6 +30,7 @@ const AppShell = React.forwardRef(function AppShell2(props, ref) {
30
30
  }
31
31
  const rootRef = React.useRef(null);
32
32
  const barRef = React.useRef(null);
33
+ const probeRef = React.useRef(null);
33
34
  const mergedRef = useMergedRef(ref, rootRef);
34
35
  const hasBar = !!(props.header || props.nav);
35
36
  const barH = React.useState(null);
@@ -59,6 +60,7 @@ const AppShell = React.forwardRef(function AppShell2(props, ref) {
59
60
  sync();
60
61
  const ro = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(sync);
61
62
  if (ro) ro.observe(bar);
63
+ if (ro && probeRef.current) ro.observe(probeRef.current);
62
64
  window.addEventListener("resize", sync);
63
65
  return function() {
64
66
  if (ro) ro.disconnect();
@@ -142,7 +144,8 @@ const AppShell = React.forwardRef(function AppShell2(props, ref) {
142
144
  "aria-expanded": open
143
145
  }
144
146
  ) : null,
145
- /* @__PURE__ */ React.createElement("div", { className: "aura-shell__bar-content" }, props.header)
147
+ /* @__PURE__ */ React.createElement("div", { className: "aura-shell__bar-content" }, props.header),
148
+ /* @__PURE__ */ React.createElement("span", { className: "aura-shell__probe", "aria-hidden": "true", ref: probeRef })
146
149
  ) : null, /* @__PURE__ */ React.createElement(
147
150
  "main",
148
151
  {
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
  )
package/dist/index.d.cts CHANGED
@@ -419,6 +419,10 @@ export interface FieldProps {
419
419
  optional?: boolean | undefined;
420
420
  }
421
421
  export interface TextFieldProps extends FieldProps, Omit<React$1.InputHTMLAttributes<HTMLInputElement>, "required"> {
422
+ /** 5.50 (Chamber-OS 156): marks the value invalid — the danger border and `aria-invalid="true"` — without AURA's
423
+ * message: no error line, and `aria-describedby` stays as it is (the hint, the addon, your own ids). For a page that
424
+ * explains the problem itself (a live readout, a reason a button points at). `error` still wins when set. */
425
+ invalid?: boolean | undefined;
422
426
  /** Leading icon inside the field. */
423
427
  icon?: IconInput | undefined;
424
428
  /** Trailing unit, e.g. "THB". */
@@ -570,6 +574,10 @@ export interface CommandProps {
570
574
  className?: string | undefined;
571
575
  }
572
576
  export interface TextareaProps extends FieldProps, Omit<React$1.TextareaHTMLAttributes<HTMLTextAreaElement>, "required"> {
577
+ /** 5.50 (Chamber-OS 156): marks the value invalid — the danger border and `aria-invalid="true"` — without AURA's
578
+ * message: no error line, and `aria-describedby` stays as it is (the hint, the addon, your own ids). For a page that
579
+ * explains the problem itself (a live readout, a reason a button points at). `error` still wins when set. */
580
+ invalid?: boolean | undefined;
573
581
  /** 5.31 (Chamber-OS addendum 41): a node between the label and the box — e.g. the phrase to type, with a copy
574
582
  * button, on a typed confirmation. The hint and error stay below. Its box's id (`{id}-addon`) comes first in the
575
583
  * input's `aria-describedby`, so its text is read after the label; keep any button names in it short. */
@@ -1798,6 +1806,9 @@ export interface FieldComponentProps extends Omit<FieldPropsPublic, "label"> {
1798
1806
  labelAs?: string | undefined;
1799
1807
  labelId?: string | undefined;
1800
1808
  disabled?: boolean | undefined;
1809
+ /** 5.50 (Chamber-OS 156): the danger look without a message (`is-invalid`, no error line). Set `aria-invalid` on
1810
+ * your control yourself; `error` still renders its message. */
1811
+ invalid?: boolean | undefined;
1801
1812
  className?: string | undefined;
1802
1813
  }
1803
1814
  export declare const Field: React$1.ForwardRefExoticComponent<FieldComponentProps & React$1.RefAttributes<HTMLDivElement>>;
package/dist/index.d.ts CHANGED
@@ -419,6 +419,10 @@ export interface FieldProps {
419
419
  optional?: boolean | undefined;
420
420
  }
421
421
  export interface TextFieldProps extends FieldProps, Omit<React$1.InputHTMLAttributes<HTMLInputElement>, "required"> {
422
+ /** 5.50 (Chamber-OS 156): marks the value invalid — the danger border and `aria-invalid="true"` — without AURA's
423
+ * message: no error line, and `aria-describedby` stays as it is (the hint, the addon, your own ids). For a page that
424
+ * explains the problem itself (a live readout, a reason a button points at). `error` still wins when set. */
425
+ invalid?: boolean | undefined;
422
426
  /** Leading icon inside the field. */
423
427
  icon?: IconInput | undefined;
424
428
  /** Trailing unit, e.g. "THB". */
@@ -570,6 +574,10 @@ export interface CommandProps {
570
574
  className?: string | undefined;
571
575
  }
572
576
  export interface TextareaProps extends FieldProps, Omit<React$1.TextareaHTMLAttributes<HTMLTextAreaElement>, "required"> {
577
+ /** 5.50 (Chamber-OS 156): marks the value invalid — the danger border and `aria-invalid="true"` — without AURA's
578
+ * message: no error line, and `aria-describedby` stays as it is (the hint, the addon, your own ids). For a page that
579
+ * explains the problem itself (a live readout, a reason a button points at). `error` still wins when set. */
580
+ invalid?: boolean | undefined;
573
581
  /** 5.31 (Chamber-OS addendum 41): a node between the label and the box — e.g. the phrase to type, with a copy
574
582
  * button, on a typed confirmation. The hint and error stay below. Its box's id (`{id}-addon`) comes first in the
575
583
  * input's `aria-describedby`, so its text is read after the label; keep any button names in it short. */
@@ -1798,6 +1806,9 @@ export interface FieldComponentProps extends Omit<FieldPropsPublic, "label"> {
1798
1806
  labelAs?: string | undefined;
1799
1807
  labelId?: string | undefined;
1800
1808
  disabled?: boolean | undefined;
1809
+ /** 5.50 (Chamber-OS 156): the danger look without a message (`is-invalid`, no error line). Set `aria-invalid` on
1810
+ * your control yourself; `error` still renders its message. */
1811
+ invalid?: boolean | undefined;
1801
1812
  className?: string | undefined;
1802
1813
  }
1803
1814
  export declare const Field: React$1.ForwardRefExoticComponent<FieldComponentProps & React$1.RefAttributes<HTMLDivElement>>;
package/dist/styles.css CHANGED
@@ -528,6 +528,10 @@ a.aura-icon-btn { text-decoration: none; }
528
528
  .aura-input__icon { position: absolute; left: var(--aura-space-3); color: var(--aura-fg-secondary); pointer-events: none; }
529
529
  .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
530
  .aura-field.is-invalid .aura-input, .aura-field.is-invalid .aura-textarea { border-color: var(--aura-border-danger); }
531
+ /* 5.50 (Chamber-OS 156): a control the page marks aria-invalid="true" itself gets the same edge, message or not (a
532
+ * disabled field keeps its disabled edge, as with is-invalid). */
533
+ .aura-field:not(.is-disabled) .aura-input:has(> .aura-input__control[aria-invalid="true"]),
534
+ .aura-field:not(.is-disabled) .aura-textarea[aria-invalid="true"] { border-color: var(--aura-border-danger); }
531
535
  .aura-field.is-disabled .aura-input { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-border-default); }
532
536
  .aura-field.is-disabled .aura-input__control { cursor: not-allowed; }
533
537
  /* A read-only text field. Only an input: `:read-only` also matches any element that isn't editable — a
@@ -1109,6 +1113,8 @@ a.aura-tab { text-decoration: none; }
1109
1113
  --aura-shell-bar-height: 56px; /* 5.14 (Chamber-OS 104): the top bar's height, for sticky offsets under it */
1110
1114
  }
1111
1115
  .aura-shell--no-bar { --aura-shell-bar-height: 0px; }
1116
+ /* 5.49 (Chamber-OS 155): AppShell watches this for changes of the browser's text size (see the 30rem queries). */
1117
+ .aura-shell__probe { position: absolute; top: 0; inset-inline-start: 0; font-size: medium; width: 1em; height: 0; overflow: hidden; visibility: hidden; pointer-events: none; }
1112
1118
  /* 5.35 (DxT Monitor 13): headerHideOnScroll — below lg the bar slides up out of view while the page scrolls down (its
1113
1119
  * shadow too); AppShell sets the bar-height token to 0 meanwhile. */
1114
1120
  .aura-shell__bar--hides { transition: transform var(--aura-duration-base) var(--aura-ease); }
@@ -1208,14 +1214,14 @@ a.aura-tab { text-decoration: none; }
1208
1214
  .aura-page-header--actions-full-below-lg > .aura-page-header__actions:has(> .aura-icon-btn, > * > .aura-icon-btn) > :is(.aura-btn, * > .aura-btn):not(.aura-icon-btn) { height: auto; min-height: var(--aura-button-height); white-space: normal; padding: var(--aura-space-2) var(--aura-space-3); line-height: 1.35; text-align: center; overflow-wrap: anywhere; hyphens: auto; border-radius: min(var(--aura-button-radius), 22px); }
1209
1215
  .aura-page-header--actions-full-below-lg > .aura-page-header__actions:has(> .aura-icon-btn, > * > .aura-icon-btn) > :is(.aura-icon-btn, :has(> .aura-icon-btn)) { align-self: center; } /* 5.45 (Chamber-OS addendum 52): centred on a taller button */
1210
1216
  }
1211
- /* 5.48 (Chamber-OS 154): only on a screen taller than 480px — on a short one (400% zoom, a phone held sideways) the
1217
+ /* 5.48 (Chamber-OS 154): only on a screen taller than 30rem (5.49: 480px at the default text size) — on a short one (400% zoom, a phone held sideways) the
1212
1218
  * actions stay in the header's own row and the page keeps its full height.
1213
1219
  * 5.48 (Chamber-OS 153): the bar is ActionBar's edge strip (full width, on the bottom edge, top border, the safe area
1214
1220
  * inside); actionsBarLook="floating" keeps 5.45's inset card.
1215
1221
  * 5.45 (Chamber-OS 150): actionsBarBelow — the same actions become a fixed bar with a status line; the page gains its
1216
1222
  * height (--aura-page-actions-height, measured on the client) so its end and a focused field scroll clear. */
1217
1223
  .aura-page-header__status { display: none; }
1218
- @media (max-width: 639.98px) and (min-height: 480.02px) {
1224
+ @media (max-width: 639.98px) and (min-height: 30.00125rem) {
1219
1225
  .aura-page-header--actions-bar-below-sm > .aura-page-header__actions { position: fixed; z-index: var(--aura-z-bar); left: 0; right: 0; bottom: var(--aura-bottomnav-offset, 0px); box-sizing: border-box; min-height: 60px; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--aura-space-2); margin: 0; padding: var(--aura-space-2) var(--aura-space-3) calc(var(--aura-space-2) + max(0px, env(safe-area-inset-bottom, 0px) - var(--aura-bottomnav-offset, 0px))) var(--aura-space-4); background: var(--aura-bg-surface); color: var(--aura-fg-primary); border: 0; border-top: 1px solid var(--aura-border-default); border-radius: 0; box-shadow: none; transition: bottom var(--aura-duration-base) var(--aura-ease); }
1220
1226
  .aura-page-header--actions-bar-below-sm.aura-page-header--actions-bar-floating > .aura-page-header__actions { left: var(--aura-space-4); right: var(--aura-space-4); bottom: calc(var(--aura-bottomnav-offset, 0px) + max(var(--aura-space-3), env(safe-area-inset-bottom, 0px))); padding-bottom: var(--aura-space-2); border: 1px solid var(--aura-border-default); border-radius: var(--aura-radius-lg); box-shadow: var(--aura-shadow-overlay); }
1221
1227
  .aura-page-header--actions-bar-below-sm > .aura-page-header__actions > .aura-page-header__status { display: block; flex: 1 1 100%; min-width: 0; font-size: max(14px, .875rem); line-height: 1.5; color: var(--aura-fg-secondary); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
@@ -1233,7 +1239,7 @@ a.aura-tab { text-decoration: none; }
1233
1239
  :root:has(.aura-page-header--actions-bar-below-sm) .aura-shell__content { padding-bottom: calc(var(--aura-space-6) + var(--aura-page-actions-space)); }
1234
1240
  :root:has(.aura-page-header--actions-bar-below-sm):not(:has(.aura-shell__content)) body { padding-bottom: var(--aura-page-actions-space); }
1235
1241
  }
1236
- @media (max-width: 767.98px) and (min-height: 480.02px) {
1242
+ @media (max-width: 767.98px) and (min-height: 30.00125rem) {
1237
1243
  .aura-page-header--actions-bar-below-md > .aura-page-header__actions { position: fixed; z-index: var(--aura-z-bar); left: 0; right: 0; bottom: var(--aura-bottomnav-offset, 0px); box-sizing: border-box; min-height: 60px; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--aura-space-2); margin: 0; padding: var(--aura-space-2) var(--aura-space-3) calc(var(--aura-space-2) + max(0px, env(safe-area-inset-bottom, 0px) - var(--aura-bottomnav-offset, 0px))) var(--aura-space-4); background: var(--aura-bg-surface); color: var(--aura-fg-primary); border: 0; border-top: 1px solid var(--aura-border-default); border-radius: 0; box-shadow: none; transition: bottom var(--aura-duration-base) var(--aura-ease); }
1238
1244
  .aura-page-header--actions-bar-below-md.aura-page-header--actions-bar-floating > .aura-page-header__actions { left: var(--aura-space-4); right: var(--aura-space-4); bottom: calc(var(--aura-bottomnav-offset, 0px) + max(var(--aura-space-3), env(safe-area-inset-bottom, 0px))); padding-bottom: var(--aura-space-2); border: 1px solid var(--aura-border-default); border-radius: var(--aura-radius-lg); box-shadow: var(--aura-shadow-overlay); }
1239
1245
  .aura-page-header--actions-bar-below-md > .aura-page-header__actions > .aura-page-header__status { display: block; flex: 1 1 100%; min-width: 0; font-size: max(14px, .875rem); line-height: 1.5; color: var(--aura-fg-secondary); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
@@ -2093,11 +2099,13 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
2093
2099
  border: 1px solid var(--aura-border-default); border-radius: var(--aura-radius-lg); box-shadow: var(--aura-shadow-overlay);
2094
2100
  }
2095
2101
  .aura-actionbar--container { bottom: 0; border-top: 1px solid var(--aura-border-default); }
2096
- /* 5.48 (Chamber-OS 154): on a short screen (480px or less: 400% zoom, a phone held sideways) the bar stops sticking —
2102
+ /* 5.48 (Chamber-OS 154): on a short screen (30rem or less: 400% zoom, a phone held sideways) the bar stops sticking —
2097
2103
  * it stays at the end of its container, in the flow, so it never covers a focused field (WCAG 2.4.11, 1.4.10). */
2098
- @media (max-height: 480px) { .aura-actionbar:not(.is-idle) { position: static; } }
2104
+ /* 5.49 (Chamber-OS 155): the threshold is 30rem, which in a media query is the browser's text size, not the page's:
2105
+ * 480px at the default 16px, 960px at 32px, so large text counts lines of text, not pixels. */
2106
+ @media (max-height: 30rem) { .aura-actionbar:not(.is-idle) { position: static; } }
2099
2107
  /* …and AppShell's top bar scrolls away with the page, so Shift+Tab never lands a field under it. */
2100
- @media (max-height: 480px) { .aura-shell__bar { position: static; } }
2108
+ @media (max-height: 30rem) { .aura-shell__bar { position: static; } }
2101
2109
  /* 5.48: wherever a BottomNav shows, a field scrolled into view (Tab) stops above it, not under it. */
2102
2110
  :root { scroll-padding-bottom: var(--aura-bottomnav-offset, 0px); }
2103
2111
  .aura-actionbar__inner { display: flex; flex-wrap: wrap; align-items: center; gap: var(--aura-space-2) var(--aura-space-3); min-height: 60px; box-sizing: border-box; padding-block: var(--aura-space-2); padding-inline: var(--aura-space-4) var(--aura-space-3); }
@@ -530,6 +530,10 @@ a.aura-icon-btn { text-decoration: none; }
530
530
  .aura-input__icon { position: absolute; left: var(--aura-space-3); color: var(--aura-fg-secondary); pointer-events: none; }
531
531
  .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
532
  .aura-field.is-invalid .aura-input, .aura-field.is-invalid .aura-textarea { border-color: var(--aura-border-danger); }
533
+ /* 5.50 (Chamber-OS 156): a control the page marks aria-invalid="true" itself gets the same edge, message or not (a
534
+ * disabled field keeps its disabled edge, as with is-invalid). */
535
+ .aura-field:not(.is-disabled) .aura-input:has(> .aura-input__control[aria-invalid="true"]),
536
+ .aura-field:not(.is-disabled) .aura-textarea[aria-invalid="true"] { border-color: var(--aura-border-danger); }
533
537
  .aura-field.is-disabled .aura-input { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-border-default); }
534
538
  .aura-field.is-disabled .aura-input__control { cursor: not-allowed; }
535
539
  /* A read-only text field. Only an input: `:read-only` also matches any element that isn't editable — a
@@ -1111,6 +1115,8 @@ a.aura-tab { text-decoration: none; }
1111
1115
  --aura-shell-bar-height: 56px; /* 5.14 (Chamber-OS 104): the top bar's height, for sticky offsets under it */
1112
1116
  }
1113
1117
  .aura-shell--no-bar { --aura-shell-bar-height: 0px; }
1118
+ /* 5.49 (Chamber-OS 155): AppShell watches this for changes of the browser's text size (see the 30rem queries). */
1119
+ .aura-shell__probe { position: absolute; top: 0; inset-inline-start: 0; font-size: medium; width: 1em; height: 0; overflow: hidden; visibility: hidden; pointer-events: none; }
1114
1120
  /* 5.35 (DxT Monitor 13): headerHideOnScroll — below lg the bar slides up out of view while the page scrolls down (its
1115
1121
  * shadow too); AppShell sets the bar-height token to 0 meanwhile. */
1116
1122
  .aura-shell__bar--hides { transition: transform var(--aura-duration-base) var(--aura-ease); }
@@ -1210,14 +1216,14 @@ a.aura-tab { text-decoration: none; }
1210
1216
  .aura-page-header--actions-full-below-lg > .aura-page-header__actions:has(> .aura-icon-btn, > * > .aura-icon-btn) > :is(.aura-btn, * > .aura-btn):not(.aura-icon-btn) { height: auto; min-height: var(--aura-button-height); white-space: normal; padding: var(--aura-space-2) var(--aura-space-3); line-height: 1.35; text-align: center; overflow-wrap: anywhere; hyphens: auto; border-radius: min(var(--aura-button-radius), 22px); }
1211
1217
  .aura-page-header--actions-full-below-lg > .aura-page-header__actions:has(> .aura-icon-btn, > * > .aura-icon-btn) > :is(.aura-icon-btn, :has(> .aura-icon-btn)) { align-self: center; } /* 5.45 (Chamber-OS addendum 52): centred on a taller button */
1212
1218
  }
1213
- /* 5.48 (Chamber-OS 154): only on a screen taller than 480px — on a short one (400% zoom, a phone held sideways) the
1219
+ /* 5.48 (Chamber-OS 154): only on a screen taller than 30rem (5.49: 480px at the default text size) — on a short one (400% zoom, a phone held sideways) the
1214
1220
  * actions stay in the header's own row and the page keeps its full height.
1215
1221
  * 5.48 (Chamber-OS 153): the bar is ActionBar's edge strip (full width, on the bottom edge, top border, the safe area
1216
1222
  * inside); actionsBarLook="floating" keeps 5.45's inset card.
1217
1223
  * 5.45 (Chamber-OS 150): actionsBarBelow — the same actions become a fixed bar with a status line; the page gains its
1218
1224
  * height (--aura-page-actions-height, measured on the client) so its end and a focused field scroll clear. */
1219
1225
  .aura-page-header__status { display: none; }
1220
- @media (max-width: 639.98px) and (min-height: 480.02px) {
1226
+ @media (max-width: 639.98px) and (min-height: 30.00125rem) {
1221
1227
  .aura-page-header--actions-bar-below-sm > .aura-page-header__actions { position: fixed; z-index: var(--aura-z-bar); left: 0; right: 0; bottom: var(--aura-bottomnav-offset, 0px); box-sizing: border-box; min-height: 60px; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--aura-space-2); margin: 0; padding: var(--aura-space-2) var(--aura-space-3) calc(var(--aura-space-2) + max(0px, env(safe-area-inset-bottom, 0px) - var(--aura-bottomnav-offset, 0px))) var(--aura-space-4); background: var(--aura-bg-surface); color: var(--aura-fg-primary); border: 0; border-top: 1px solid var(--aura-border-default); border-radius: 0; box-shadow: none; transition: bottom var(--aura-duration-base) var(--aura-ease); }
1222
1228
  .aura-page-header--actions-bar-below-sm.aura-page-header--actions-bar-floating > .aura-page-header__actions { left: var(--aura-space-4); right: var(--aura-space-4); bottom: calc(var(--aura-bottomnav-offset, 0px) + max(var(--aura-space-3), env(safe-area-inset-bottom, 0px))); padding-bottom: var(--aura-space-2); border: 1px solid var(--aura-border-default); border-radius: var(--aura-radius-lg); box-shadow: var(--aura-shadow-overlay); }
1223
1229
  .aura-page-header--actions-bar-below-sm > .aura-page-header__actions > .aura-page-header__status { display: block; flex: 1 1 100%; min-width: 0; font-size: max(14px, .875rem); line-height: 1.5; color: var(--aura-fg-secondary); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
@@ -1235,7 +1241,7 @@ a.aura-tab { text-decoration: none; }
1235
1241
  :root:has(.aura-page-header--actions-bar-below-sm) .aura-shell__content { padding-bottom: calc(var(--aura-space-6) + var(--aura-page-actions-space)); }
1236
1242
  :root:has(.aura-page-header--actions-bar-below-sm):not(:has(.aura-shell__content)) body { padding-bottom: var(--aura-page-actions-space); }
1237
1243
  }
1238
- @media (max-width: 767.98px) and (min-height: 480.02px) {
1244
+ @media (max-width: 767.98px) and (min-height: 30.00125rem) {
1239
1245
  .aura-page-header--actions-bar-below-md > .aura-page-header__actions { position: fixed; z-index: var(--aura-z-bar); left: 0; right: 0; bottom: var(--aura-bottomnav-offset, 0px); box-sizing: border-box; min-height: 60px; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--aura-space-2); margin: 0; padding: var(--aura-space-2) var(--aura-space-3) calc(var(--aura-space-2) + max(0px, env(safe-area-inset-bottom, 0px) - var(--aura-bottomnav-offset, 0px))) var(--aura-space-4); background: var(--aura-bg-surface); color: var(--aura-fg-primary); border: 0; border-top: 1px solid var(--aura-border-default); border-radius: 0; box-shadow: none; transition: bottom var(--aura-duration-base) var(--aura-ease); }
1240
1246
  .aura-page-header--actions-bar-below-md.aura-page-header--actions-bar-floating > .aura-page-header__actions { left: var(--aura-space-4); right: var(--aura-space-4); bottom: calc(var(--aura-bottomnav-offset, 0px) + max(var(--aura-space-3), env(safe-area-inset-bottom, 0px))); padding-bottom: var(--aura-space-2); border: 1px solid var(--aura-border-default); border-radius: var(--aura-radius-lg); box-shadow: var(--aura-shadow-overlay); }
1241
1247
  .aura-page-header--actions-bar-below-md > .aura-page-header__actions > .aura-page-header__status { display: block; flex: 1 1 100%; min-width: 0; font-size: max(14px, .875rem); line-height: 1.5; color: var(--aura-fg-secondary); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
@@ -2095,11 +2101,13 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
2095
2101
  border: 1px solid var(--aura-border-default); border-radius: var(--aura-radius-lg); box-shadow: var(--aura-shadow-overlay);
2096
2102
  }
2097
2103
  .aura-actionbar--container { bottom: 0; border-top: 1px solid var(--aura-border-default); }
2098
- /* 5.48 (Chamber-OS 154): on a short screen (480px or less: 400% zoom, a phone held sideways) the bar stops sticking —
2104
+ /* 5.48 (Chamber-OS 154): on a short screen (30rem or less: 400% zoom, a phone held sideways) the bar stops sticking —
2099
2105
  * it stays at the end of its container, in the flow, so it never covers a focused field (WCAG 2.4.11, 1.4.10). */
2100
- @media (max-height: 480px) { .aura-actionbar:not(.is-idle) { position: static; } }
2106
+ /* 5.49 (Chamber-OS 155): the threshold is 30rem, which in a media query is the browser's text size, not the page's:
2107
+ * 480px at the default 16px, 960px at 32px, so large text counts lines of text, not pixels. */
2108
+ @media (max-height: 30rem) { .aura-actionbar:not(.is-idle) { position: static; } }
2101
2109
  /* …and AppShell's top bar scrolls away with the page, so Shift+Tab never lands a field under it. */
2102
- @media (max-height: 480px) { .aura-shell__bar { position: static; } }
2110
+ @media (max-height: 30rem) { .aura-shell__bar { position: static; } }
2103
2111
  /* 5.48: wherever a BottomNav shows, a field scrolled into view (Tab) stops above it, not under it. */
2104
2112
  :root { scroll-padding-bottom: var(--aura-bottomnav-offset, 0px); }
2105
2113
  .aura-actionbar__inner { display: flex; flex-wrap: wrap; align-items: center; gap: var(--aura-space-2) var(--aura-space-3); min-height: 60px; box-sizing: border-box; padding-block: var(--aura-space-2); padding-inline: var(--aura-space-4) var(--aura-space-3); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jirawatpyk/aura-react",
3
- "version": "5.48.0",
3
+ "version": "5.50.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",