@jirawatpyk/aura-react 5.16.0 → 5.17.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,16 @@ 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.17 — Chamber-OS addendum 18
190
+
191
+ - **Stat attributes, status line and a link on the label** (item 110): `id`, `data-*`, `aria-*`, `style`, `lang` and `dir` go on the `.aura-stat` root, from the root package and `/server` — a `data-testid`, or `aria-hidden` on a loading placeholder. `status` is a line under the value ("Active · renews 1 Jan" with its tone icon), hidden while loading. With `href`, `linkArea="label"` puts the link on the label (inside the heading when there is one) and stretches its hit area over the tile: a click anywhere follows it, the tile shows the focus ring and hover edge, and other controls in the tile (links, buttons, fields, anything focusable) stay clickable above it. `aria-label`, `aria-labelledby`, `aria-describedby` and `aria-current` then go on the link, and an `onClick` too. Text in such a tile can't be selected by dragging (the click belongs to the link). The default (`tile`) is unchanged. Only the documented attributes reach the root; anything else passed is dropped, as before.
192
+ - **Progress `valueText`** (item 111): what screen readers hear (`aria-valuetext`), apart from the shown `valueLabel` — show "2 of 6 used", read "2 used, 1 reserved, 3 remaining of 6".
193
+ - The CSS budget is 26 kB (was 25).
194
+
195
+ ## 5.16.1 — Select ground (Chamber-OS 109)
196
+
197
+ - **A custom Select keeps the input ground.** The read-only rule for text fields matched the Select's trigger button too (`:read-only` matches anything not editable), so every custom Select showed the disabled ground and looked disabled. It now applies to a read-only `input` only; a read-only TextField still takes the disabled ground, and a read-only Textarea now does too.
198
+
189
199
  ## 5.16 — Chamber-OS addenda 15–16, the larger items
190
200
 
191
201
  - **SideNav action rows** (item 95): an item with `selectable: false` is an action row — Sign out, Help — with the rows' look, 44px on touch screens and the arrow keys, that runs `onSelect` and never becomes the current item (no `aria-current`, no `onChange`). In the rail it is its icon, the label its name. Any item can take `onSelect`. `collapseToggle="row"` shows `collapsible`'s toggle as a labelled row ("Collapse sidebar" / "Expand sidebar") instead of an icon button. In AppShell's phone drawer an action row closes the drawer; `onAction(id)` on SideNav reports action rows.
@@ -717,8 +717,17 @@ window.Aura = (() => {
717
717
  const tone2 = ch && (ch.tone || (dir === "up" ? "positive" : dir === "down" ? "negative" : "neutral"));
718
718
  const v = props.value;
719
719
  const numeric = typeof v === "number" || typeof v === "string" && /\d/.test(v) && /^[\s\d.,:+\-\u2212%()\u0E3F$\u20AC\u00A3\u00A5kKmMbB]+$/.test(v.replace(/\b[A-Z]{3}\b/g, ""));
720
- const Tag3 = props.href ? Link : props.onClick ? "button" : "div";
720
+ const labelLink = !!props.href && props.linkArea === "label";
721
+ const Tag3 = labelLink ? "div" : props.href ? Link : props.onClick ? "button" : "div";
721
722
  const interactive = !!(props.href || props.onClick);
723
+ const LINK_ARIA = ["aria-label", "aria-labelledby", "aria-describedby", "aria-current"];
724
+ const root = {}, toLink = {};
725
+ Object.keys(props).forEach(function(k) {
726
+ const v2 = props[k];
727
+ if (labelLink && LINK_ARIA.indexOf(k) >= 0) toLink[k] = v2;
728
+ else if (k === "id" || k === "style" || k === "lang" || k === "dir" || /^(aria|data)-/.test(k)) root[k] = v2;
729
+ });
730
+ const busy = props.loading || props["aria-busy"] || void 0;
722
731
  if (props.headingLevel && Tag3 === "button")
723
732
  devWarnOnce(
724
733
  "stat-heading-button",
@@ -729,15 +738,23 @@ window.Aura = (() => {
729
738
  return /* @__PURE__ */ React4.createElement(
730
739
  Tag3,
731
740
  {
741
+ ...root,
732
742
  ref,
733
- className: cx("aura-stat", interactive && "is-interactive", props.loading && "is-loading", props.className),
734
- href: props.href,
735
- onClick: props.onClick,
743
+ className: cx(
744
+ "aura-stat",
745
+ interactive && "is-interactive",
746
+ labelLink && "aura-stat--label-link",
747
+ props.loading && "is-loading",
748
+ props.className
749
+ ),
750
+ href: labelLink ? void 0 : props.href,
751
+ onClick: labelLink ? void 0 : props.onClick,
736
752
  type: Tag3 === "button" ? "button" : void 0,
737
- "aria-busy": props.loading || void 0
753
+ "aria-busy": busy
738
754
  },
739
- /* @__PURE__ */ React4.createElement(HeadTag, { className: "aura-stat__head" }, /* @__PURE__ */ React4.createElement(LabelTag, { className: "aura-stat__label" }, props.label), props.icon ? /* @__PURE__ */ React4.createElement("span", { className: "aura-stat__icon" }, /* @__PURE__ */ React4.createElement(Icon, { name: props.icon })) : null),
755
+ /* @__PURE__ */ React4.createElement(HeadTag, { className: "aura-stat__head" }, /* @__PURE__ */ React4.createElement(LabelTag, { className: "aura-stat__label" }, labelLink ? /* @__PURE__ */ React4.createElement(Link, { ...toLink, href: props.href, onClick: props.onClick, className: "aura-stat__link" }, props.label) : props.label), props.icon ? /* @__PURE__ */ React4.createElement("span", { className: "aura-stat__icon" }, /* @__PURE__ */ React4.createElement(Icon, { name: props.icon })) : null),
740
756
  props.loading ? /* @__PURE__ */ React4.createElement("span", { className: "aura-stat__value" }, /* @__PURE__ */ React4.createElement("span", { className: "aura-skel aura-stat__skel" })) : /* @__PURE__ */ React4.createElement("span", { className: cx("aura-stat__value", numeric && "is-numeric") }, props.value, props.unit ? /* @__PURE__ */ React4.createElement("span", { className: "aura-stat__unit" }, props.unit) : null),
757
+ props.status != null && props.status !== false && props.status !== "" && !props.loading ? /* @__PURE__ */ React4.createElement("span", { className: "aura-stat__status" }, props.status) : null,
741
758
  ch && !props.loading || props.caption ? /* @__PURE__ */ React4.createElement("span", { className: "aura-stat__foot" }, ch && !props.loading ? /* @__PURE__ */ React4.createElement("span", { className: cx("aura-stat__change", "is-" + tone2) }, /* @__PURE__ */ React4.createElement(
742
759
  Icon,
743
760
  {
@@ -8344,7 +8361,7 @@ window.Aura = (() => {
8344
8361
  const pct = det ? Math.max(0, Math.min(100, props.value / max * 100)) : 0;
8345
8362
  const sec = det && props.secondaryValue != null && props.secondaryValue > 0 ? props.secondaryValue : 0;
8346
8363
  const spct = sec ? Math.max(0, Math.min(100 - pct, sec / max * 100)) : 0;
8347
- const valueText = props.valueLabel != null ? String(props.valueLabel) : sec ? t.progressReserved(props.value, sec, max) : void 0;
8364
+ const valueText = props.valueText != null ? props.valueText : props.valueLabel != null ? String(props.valueLabel) : sec ? t.progressReserved(props.value, sec, max) : void 0;
8348
8365
  const shown = props.valueLabel != null ? props.valueLabel : det ? Math.round(pct) + "%" : null;
8349
8366
  return /* @__PURE__ */ React53.createElement(
8350
8367
  "div",
@@ -699,8 +699,17 @@ function statElement(props, ref, Link) {
699
699
  const tone2 = ch && (ch.tone || (dir === "up" ? "positive" : dir === "down" ? "negative" : "neutral"));
700
700
  const v = props.value;
701
701
  const numeric = typeof v === "number" || typeof v === "string" && /\d/.test(v) && /^[\s\d.,:+\-\u2212%()\u0E3F$\u20AC\u00A3\u00A5kKmMbB]+$/.test(v.replace(/\b[A-Z]{3}\b/g, ""));
702
- const Tag3 = props.href ? Link : props.onClick ? "button" : "div";
702
+ const labelLink = !!props.href && props.linkArea === "label";
703
+ const Tag3 = labelLink ? "div" : props.href ? Link : props.onClick ? "button" : "div";
703
704
  const interactive = !!(props.href || props.onClick);
705
+ const LINK_ARIA = ["aria-label", "aria-labelledby", "aria-describedby", "aria-current"];
706
+ const root = {}, toLink = {};
707
+ Object.keys(props).forEach(function(k) {
708
+ const v2 = props[k];
709
+ if (labelLink && LINK_ARIA.indexOf(k) >= 0) toLink[k] = v2;
710
+ else if (k === "id" || k === "style" || k === "lang" || k === "dir" || /^(aria|data)-/.test(k)) root[k] = v2;
711
+ });
712
+ const busy = props.loading || props["aria-busy"] || void 0;
704
713
  if (props.headingLevel && Tag3 === "button")
705
714
  devWarnOnce(
706
715
  "stat-heading-button",
@@ -711,15 +720,23 @@ function statElement(props, ref, Link) {
711
720
  return /* @__PURE__ */ React4.createElement(
712
721
  Tag3,
713
722
  {
723
+ ...root,
714
724
  ref,
715
- className: cx("aura-stat", interactive && "is-interactive", props.loading && "is-loading", props.className),
716
- href: props.href,
717
- onClick: props.onClick,
725
+ className: cx(
726
+ "aura-stat",
727
+ interactive && "is-interactive",
728
+ labelLink && "aura-stat--label-link",
729
+ props.loading && "is-loading",
730
+ props.className
731
+ ),
732
+ href: labelLink ? void 0 : props.href,
733
+ onClick: labelLink ? void 0 : props.onClick,
718
734
  type: Tag3 === "button" ? "button" : void 0,
719
- "aria-busy": props.loading || void 0
735
+ "aria-busy": busy
720
736
  },
721
- /* @__PURE__ */ React4.createElement(HeadTag, { className: "aura-stat__head" }, /* @__PURE__ */ React4.createElement(LabelTag, { className: "aura-stat__label" }, props.label), props.icon ? /* @__PURE__ */ React4.createElement("span", { className: "aura-stat__icon" }, /* @__PURE__ */ React4.createElement(Icon, { name: props.icon })) : null),
737
+ /* @__PURE__ */ React4.createElement(HeadTag, { className: "aura-stat__head" }, /* @__PURE__ */ React4.createElement(LabelTag, { className: "aura-stat__label" }, labelLink ? /* @__PURE__ */ React4.createElement(Link, { ...toLink, href: props.href, onClick: props.onClick, className: "aura-stat__link" }, props.label) : props.label), props.icon ? /* @__PURE__ */ React4.createElement("span", { className: "aura-stat__icon" }, /* @__PURE__ */ React4.createElement(Icon, { name: props.icon })) : null),
722
738
  props.loading ? /* @__PURE__ */ React4.createElement("span", { className: "aura-stat__value" }, /* @__PURE__ */ React4.createElement("span", { className: "aura-skel aura-stat__skel" })) : /* @__PURE__ */ React4.createElement("span", { className: cx("aura-stat__value", numeric && "is-numeric") }, props.value, props.unit ? /* @__PURE__ */ React4.createElement("span", { className: "aura-stat__unit" }, props.unit) : null),
739
+ props.status != null && props.status !== false && props.status !== "" && !props.loading ? /* @__PURE__ */ React4.createElement("span", { className: "aura-stat__status" }, props.status) : null,
723
740
  ch && !props.loading || props.caption ? /* @__PURE__ */ React4.createElement("span", { className: "aura-stat__foot" }, ch && !props.loading ? /* @__PURE__ */ React4.createElement("span", { className: cx("aura-stat__change", "is-" + tone2) }, /* @__PURE__ */ React4.createElement(
724
741
  Icon,
725
742
  {
@@ -8326,7 +8343,7 @@ var Progress = React53.forwardRef(function Progress2(props, ref) {
8326
8343
  const pct = det ? Math.max(0, Math.min(100, props.value / max * 100)) : 0;
8327
8344
  const sec = det && props.secondaryValue != null && props.secondaryValue > 0 ? props.secondaryValue : 0;
8328
8345
  const spct = sec ? Math.max(0, Math.min(100 - pct, sec / max * 100)) : 0;
8329
- const valueText = props.valueLabel != null ? String(props.valueLabel) : sec ? t.progressReserved(props.value, sec, max) : void 0;
8346
+ const valueText = props.valueText != null ? props.valueText : props.valueLabel != null ? String(props.valueLabel) : sec ? t.progressReserved(props.value, sec, max) : void 0;
8330
8347
  const shown = props.valueLabel != null ? props.valueLabel : det ? Math.round(pct) + "%" : null;
8331
8348
  return /* @__PURE__ */ React53.createElement(
8332
8349
  "div",
@@ -9,7 +9,7 @@ const Progress = React.forwardRef(function Progress2(props, ref) {
9
9
  const pct = det ? Math.max(0, Math.min(100, props.value / max * 100)) : 0;
10
10
  const sec = det && props.secondaryValue != null && props.secondaryValue > 0 ? props.secondaryValue : 0;
11
11
  const spct = sec ? Math.max(0, Math.min(100 - pct, sec / max * 100)) : 0;
12
- const valueText = props.valueLabel != null ? String(props.valueLabel) : sec ? t.progressReserved(props.value, sec, max) : void 0;
12
+ const valueText = props.valueText != null ? props.valueText : props.valueLabel != null ? String(props.valueLabel) : sec ? t.progressReserved(props.value, sec, max) : void 0;
13
13
  const shown = props.valueLabel != null ? props.valueLabel : det ? Math.round(pct) + "%" : null;
14
14
  return /* @__PURE__ */ React.createElement(
15
15
  "div",
@@ -146,8 +146,17 @@ function statElement(props, ref, Link) {
146
146
  const tone = ch && (ch.tone || (dir === "up" ? "positive" : dir === "down" ? "negative" : "neutral"));
147
147
  const v = props.value;
148
148
  const numeric = typeof v === "number" || typeof v === "string" && /\d/.test(v) && /^[\s\d.,:+\-\u2212%()\u0E3F$\u20AC\u00A3\u00A5kKmMbB]+$/.test(v.replace(/\b[A-Z]{3}\b/g, ""));
149
- const Tag = props.href ? Link : props.onClick ? "button" : "div";
149
+ const labelLink = !!props.href && props.linkArea === "label";
150
+ const Tag = labelLink ? "div" : props.href ? Link : props.onClick ? "button" : "div";
150
151
  const interactive = !!(props.href || props.onClick);
152
+ const LINK_ARIA = ["aria-label", "aria-labelledby", "aria-describedby", "aria-current"];
153
+ const root = {}, toLink = {};
154
+ Object.keys(props).forEach(function(k) {
155
+ const v2 = props[k];
156
+ if (labelLink && LINK_ARIA.indexOf(k) >= 0) toLink[k] = v2;
157
+ else if (k === "id" || k === "style" || k === "lang" || k === "dir" || /^(aria|data)-/.test(k)) root[k] = v2;
158
+ });
159
+ const busy = props.loading || props["aria-busy"] || void 0;
151
160
  if (props.headingLevel && Tag === "button")
152
161
  devWarnOnce(
153
162
  "stat-heading-button",
@@ -158,15 +167,23 @@ function statElement(props, ref, Link) {
158
167
  return /* @__PURE__ */ React.createElement(
159
168
  Tag,
160
169
  {
170
+ ...root,
161
171
  ref,
162
- className: cx("aura-stat", interactive && "is-interactive", props.loading && "is-loading", props.className),
163
- href: props.href,
164
- onClick: props.onClick,
172
+ className: cx(
173
+ "aura-stat",
174
+ interactive && "is-interactive",
175
+ labelLink && "aura-stat--label-link",
176
+ props.loading && "is-loading",
177
+ props.className
178
+ ),
179
+ href: labelLink ? void 0 : props.href,
180
+ onClick: labelLink ? void 0 : props.onClick,
165
181
  type: Tag === "button" ? "button" : void 0,
166
- "aria-busy": props.loading || void 0
182
+ "aria-busy": busy
167
183
  },
168
- /* @__PURE__ */ React.createElement(HeadTag, { className: "aura-stat__head" }, /* @__PURE__ */ React.createElement(LabelTag, { className: "aura-stat__label" }, props.label), props.icon ? /* @__PURE__ */ React.createElement("span", { className: "aura-stat__icon" }, /* @__PURE__ */ React.createElement(Icon, { name: props.icon })) : null),
184
+ /* @__PURE__ */ React.createElement(HeadTag, { className: "aura-stat__head" }, /* @__PURE__ */ React.createElement(LabelTag, { className: "aura-stat__label" }, labelLink ? /* @__PURE__ */ React.createElement(Link, { ...toLink, href: props.href, onClick: props.onClick, className: "aura-stat__link" }, props.label) : props.label), props.icon ? /* @__PURE__ */ React.createElement("span", { className: "aura-stat__icon" }, /* @__PURE__ */ React.createElement(Icon, { name: props.icon })) : null),
169
185
  props.loading ? /* @__PURE__ */ React.createElement("span", { className: "aura-stat__value" }, /* @__PURE__ */ React.createElement("span", { className: "aura-skel aura-stat__skel" })) : /* @__PURE__ */ React.createElement("span", { className: cx("aura-stat__value", numeric && "is-numeric") }, props.value, props.unit ? /* @__PURE__ */ React.createElement("span", { className: "aura-stat__unit" }, props.unit) : null),
186
+ props.status != null && props.status !== false && props.status !== "" && !props.loading ? /* @__PURE__ */ React.createElement("span", { className: "aura-stat__status" }, props.status) : null,
170
187
  ch && !props.loading || props.caption ? /* @__PURE__ */ React.createElement("span", { className: "aura-stat__foot" }, ch && !props.loading ? /* @__PURE__ */ React.createElement("span", { className: cx("aura-stat__change", "is-" + tone) }, /* @__PURE__ */ React.createElement(
171
188
  Icon,
172
189
  {
package/dist/index.d.cts CHANGED
@@ -1215,7 +1215,8 @@ export interface StatChange {
1215
1215
  /** e.g. "vs last month". */
1216
1216
  label?: React$1.ReactNode | undefined;
1217
1217
  }
1218
- export interface StatProps {
1218
+ /** `id`, `data-*`, `aria-*`, `style`, `lang` and `dir` go on the `.aura-stat` root (5.17, Chamber-OS 110). */
1219
+ export interface StatProps extends Pick<React$1.HTMLAttributes<HTMLElement>, "id" | "style" | "lang" | "dir">, React$1.AriaAttributes, DataAttributes {
1219
1220
  /** Your router's link (e.g. `Link` from `next/link`) for this component; defaults to AuraProvider's `linkComponent`, then `<a>`. */
1220
1221
  linkComponent?: React$1.ElementType | undefined;
1221
1222
  label: React$1.ReactNode;
@@ -1233,6 +1234,13 @@ export interface StatProps {
1233
1234
  /** The label as a heading of this level (5.14), when the tile titles its section. Not with `onClick` (a heading
1234
1235
  * can't sit in a button); a link is fine. Default: a plain label. */
1235
1236
  headingLevel?: 2 | 3 | 4 | 5 | 6 | undefined;
1237
+ /** A status line under the value — "Active · renews 1 Jan" with its tone icon (5.17, Chamber-OS 110). Hidden while
1238
+ * `loading`. */
1239
+ status?: React$1.ReactNode | undefined;
1240
+ /** With `href`: `tile` (default) makes the whole tile the link; `label` puts the link on the label (inside its
1241
+ * heading) and stretches its hit area over the tile, so a click anywhere follows it and the tile shows the focus
1242
+ * ring (5.17, Chamber-OS 110). Other links or buttons in the tile stay clickable above it. */
1243
+ linkArea?: "tile" | "label" | undefined;
1236
1244
  className?: string | undefined;
1237
1245
  }
1238
1246
  export interface TimePickerProps extends FieldProps {
@@ -1316,6 +1324,9 @@ export interface ProgressProps {
1316
1324
  showValue?: boolean | undefined;
1317
1325
  /** Replaces the % text, e.g. "3 of 5 files". Also read out as aria-valuetext. */
1318
1326
  valueLabel?: React$1.ReactNode | undefined;
1327
+ /** What screen readers hear (`aria-valuetext`), apart from the shown `valueLabel` (5.17, Chamber-OS 111): show
1328
+ * "2 of 6 used", read "2 used, 1 reserved, 3 remaining of 6". */
1329
+ valueText?: string | undefined;
1319
1330
  /** A reserved amount drawn after `value` as a striped segment of the same tone — queued sends on a quota
1320
1331
  * (5.15, Chamber-OS 89). Both counts are read out ("2 of 6 used, 1 reserved") unless `valueLabel` is given. */
1321
1332
  secondaryValue?: number | undefined;
package/dist/index.d.ts CHANGED
@@ -1215,7 +1215,8 @@ export interface StatChange {
1215
1215
  /** e.g. "vs last month". */
1216
1216
  label?: React$1.ReactNode | undefined;
1217
1217
  }
1218
- export interface StatProps {
1218
+ /** `id`, `data-*`, `aria-*`, `style`, `lang` and `dir` go on the `.aura-stat` root (5.17, Chamber-OS 110). */
1219
+ export interface StatProps extends Pick<React$1.HTMLAttributes<HTMLElement>, "id" | "style" | "lang" | "dir">, React$1.AriaAttributes, DataAttributes {
1219
1220
  /** Your router's link (e.g. `Link` from `next/link`) for this component; defaults to AuraProvider's `linkComponent`, then `<a>`. */
1220
1221
  linkComponent?: React$1.ElementType | undefined;
1221
1222
  label: React$1.ReactNode;
@@ -1233,6 +1234,13 @@ export interface StatProps {
1233
1234
  /** The label as a heading of this level (5.14), when the tile titles its section. Not with `onClick` (a heading
1234
1235
  * can't sit in a button); a link is fine. Default: a plain label. */
1235
1236
  headingLevel?: 2 | 3 | 4 | 5 | 6 | undefined;
1237
+ /** A status line under the value — "Active · renews 1 Jan" with its tone icon (5.17, Chamber-OS 110). Hidden while
1238
+ * `loading`. */
1239
+ status?: React$1.ReactNode | undefined;
1240
+ /** With `href`: `tile` (default) makes the whole tile the link; `label` puts the link on the label (inside its
1241
+ * heading) and stretches its hit area over the tile, so a click anywhere follows it and the tile shows the focus
1242
+ * ring (5.17, Chamber-OS 110). Other links or buttons in the tile stay clickable above it. */
1243
+ linkArea?: "tile" | "label" | undefined;
1236
1244
  className?: string | undefined;
1237
1245
  }
1238
1246
  export interface TimePickerProps extends FieldProps {
@@ -1316,6 +1324,9 @@ export interface ProgressProps {
1316
1324
  showValue?: boolean | undefined;
1317
1325
  /** Replaces the % text, e.g. "3 of 5 files". Also read out as aria-valuetext. */
1318
1326
  valueLabel?: React$1.ReactNode | undefined;
1327
+ /** What screen readers hear (`aria-valuetext`), apart from the shown `valueLabel` (5.17, Chamber-OS 111): show
1328
+ * "2 of 6 used", read "2 used, 1 reserved, 3 remaining of 6". */
1329
+ valueText?: string | undefined;
1319
1330
  /** A reserved amount drawn after `value` as a striped segment of the same tone — queued sends on a quota
1320
1331
  * (5.15, Chamber-OS 89). Both counts are read out ("2 of 6 used, 1 reserved") unless `valueLabel` is given. */
1321
1332
  secondaryValue?: number | undefined;
@@ -1212,8 +1212,17 @@ function statElement(props, ref, Link) {
1212
1212
  const tone2 = ch && (ch.tone || (dir === "up" ? "positive" : dir === "down" ? "negative" : "neutral"));
1213
1213
  const v = props.value;
1214
1214
  const numeric = typeof v === "number" || typeof v === "string" && /\d/.test(v) && /^[\s\d.,:+\-\u2212%()\u0E3F$\u20AC\u00A3\u00A5kKmMbB]+$/.test(v.replace(/\b[A-Z]{3}\b/g, ""));
1215
- const Tag = props.href ? Link : props.onClick ? "button" : "div";
1215
+ const labelLink = !!props.href && props.linkArea === "label";
1216
+ const Tag = labelLink ? "div" : props.href ? Link : props.onClick ? "button" : "div";
1216
1217
  const interactive = !!(props.href || props.onClick);
1218
+ const LINK_ARIA = ["aria-label", "aria-labelledby", "aria-describedby", "aria-current"];
1219
+ const root = {}, toLink = {};
1220
+ Object.keys(props).forEach(function(k) {
1221
+ const v2 = props[k];
1222
+ if (labelLink && LINK_ARIA.indexOf(k) >= 0) toLink[k] = v2;
1223
+ else if (k === "id" || k === "style" || k === "lang" || k === "dir" || /^(aria|data)-/.test(k)) root[k] = v2;
1224
+ });
1225
+ const busy = props.loading || props["aria-busy"] || void 0;
1217
1226
  if (props.headingLevel && Tag === "button")
1218
1227
  devWarnOnce(
1219
1228
  "stat-heading-button",
@@ -1224,15 +1233,23 @@ function statElement(props, ref, Link) {
1224
1233
  return /* @__PURE__ */ React3.createElement(
1225
1234
  Tag,
1226
1235
  {
1236
+ ...root,
1227
1237
  ref,
1228
- className: cx("aura-stat", interactive && "is-interactive", props.loading && "is-loading", props.className),
1229
- href: props.href,
1230
- onClick: props.onClick,
1238
+ className: cx(
1239
+ "aura-stat",
1240
+ interactive && "is-interactive",
1241
+ labelLink && "aura-stat--label-link",
1242
+ props.loading && "is-loading",
1243
+ props.className
1244
+ ),
1245
+ href: labelLink ? void 0 : props.href,
1246
+ onClick: labelLink ? void 0 : props.onClick,
1231
1247
  type: Tag === "button" ? "button" : void 0,
1232
- "aria-busy": props.loading || void 0
1248
+ "aria-busy": busy
1233
1249
  },
1234
- /* @__PURE__ */ React3.createElement(HeadTag, { className: "aura-stat__head" }, /* @__PURE__ */ React3.createElement(LabelTag, { className: "aura-stat__label" }, props.label), props.icon ? /* @__PURE__ */ React3.createElement("span", { className: "aura-stat__icon" }, /* @__PURE__ */ React3.createElement(Icon, { name: props.icon })) : null),
1250
+ /* @__PURE__ */ React3.createElement(HeadTag, { className: "aura-stat__head" }, /* @__PURE__ */ React3.createElement(LabelTag, { className: "aura-stat__label" }, labelLink ? /* @__PURE__ */ React3.createElement(Link, { ...toLink, href: props.href, onClick: props.onClick, className: "aura-stat__link" }, props.label) : props.label), props.icon ? /* @__PURE__ */ React3.createElement("span", { className: "aura-stat__icon" }, /* @__PURE__ */ React3.createElement(Icon, { name: props.icon })) : null),
1235
1251
  props.loading ? /* @__PURE__ */ React3.createElement("span", { className: "aura-stat__value" }, /* @__PURE__ */ React3.createElement("span", { className: "aura-skel aura-stat__skel" })) : /* @__PURE__ */ React3.createElement("span", { className: cx("aura-stat__value", numeric && "is-numeric") }, props.value, props.unit ? /* @__PURE__ */ React3.createElement("span", { className: "aura-stat__unit" }, props.unit) : null),
1252
+ props.status != null && props.status !== false && props.status !== "" && !props.loading ? /* @__PURE__ */ React3.createElement("span", { className: "aura-stat__status" }, props.status) : null,
1236
1253
  ch && !props.loading || props.caption ? /* @__PURE__ */ React3.createElement("span", { className: "aura-stat__foot" }, ch && !props.loading ? /* @__PURE__ */ React3.createElement("span", { className: cx("aura-stat__change", "is-" + tone2) }, /* @__PURE__ */ React3.createElement(
1237
1254
  Icon,
1238
1255
  {
@@ -107,6 +107,10 @@ export interface DateDisplayOptions {
107
107
  /** `buddhist` (พ.ศ.; default for th) or `gregory` (ค.ศ.; default for en and sv). */
108
108
  calendar?: "buddhist" | "gregory" | undefined;
109
109
  }
110
+ /** `data-*` attributes in an object prop (TypeScript checks object literals, where JSX would allow them anyway). */
111
+ export type DataAttributes = {
112
+ [key: `data-${string}`]: string | number | boolean | undefined;
113
+ };
110
114
  export interface StatChange {
111
115
  /** Shown as written, e.g. "+12%" or "−3". */
112
116
  value: React$1.ReactNode;
@@ -116,7 +120,8 @@ export interface StatChange {
116
120
  /** e.g. "vs last month". */
117
121
  label?: React$1.ReactNode | undefined;
118
122
  }
119
- export interface StatProps {
123
+ /** `id`, `data-*`, `aria-*`, `style`, `lang` and `dir` go on the `.aura-stat` root (5.17, Chamber-OS 110). */
124
+ export interface StatProps extends Pick<React$1.HTMLAttributes<HTMLElement>, "id" | "style" | "lang" | "dir">, React$1.AriaAttributes, DataAttributes {
120
125
  /** Your router's link (e.g. `Link` from `next/link`) for this component; defaults to AuraProvider's `linkComponent`, then `<a>`. */
121
126
  linkComponent?: React$1.ElementType | undefined;
122
127
  label: React$1.ReactNode;
@@ -134,6 +139,13 @@ export interface StatProps {
134
139
  /** The label as a heading of this level (5.14), when the tile titles its section. Not with `onClick` (a heading
135
140
  * can't sit in a button); a link is fine. Default: a plain label. */
136
141
  headingLevel?: 2 | 3 | 4 | 5 | 6 | undefined;
142
+ /** A status line under the value — "Active · renews 1 Jan" with its tone icon (5.17, Chamber-OS 110). Hidden while
143
+ * `loading`. */
144
+ status?: React$1.ReactNode | undefined;
145
+ /** With `href`: `tile` (default) makes the whole tile the link; `label` puts the link on the label (inside its
146
+ * heading) and stretches its hit area over the tile, so a click anywhere follows it and the tile shows the focus
147
+ * ring (5.17, Chamber-OS 110). Other links or buttons in the tile stay clickable above it. */
148
+ linkArea?: "tile" | "label" | undefined;
137
149
  className?: string | undefined;
138
150
  }
139
151
  export type Tone = "neutral" | "accent" | "success" | "warning" | "danger";
@@ -107,6 +107,10 @@ export interface DateDisplayOptions {
107
107
  /** `buddhist` (พ.ศ.; default for th) or `gregory` (ค.ศ.; default for en and sv). */
108
108
  calendar?: "buddhist" | "gregory" | undefined;
109
109
  }
110
+ /** `data-*` attributes in an object prop (TypeScript checks object literals, where JSX would allow them anyway). */
111
+ export type DataAttributes = {
112
+ [key: `data-${string}`]: string | number | boolean | undefined;
113
+ };
110
114
  export interface StatChange {
111
115
  /** Shown as written, e.g. "+12%" or "−3". */
112
116
  value: React$1.ReactNode;
@@ -116,7 +120,8 @@ export interface StatChange {
116
120
  /** e.g. "vs last month". */
117
121
  label?: React$1.ReactNode | undefined;
118
122
  }
119
- export interface StatProps {
123
+ /** `id`, `data-*`, `aria-*`, `style`, `lang` and `dir` go on the `.aura-stat` root (5.17, Chamber-OS 110). */
124
+ export interface StatProps extends Pick<React$1.HTMLAttributes<HTMLElement>, "id" | "style" | "lang" | "dir">, React$1.AriaAttributes, DataAttributes {
120
125
  /** Your router's link (e.g. `Link` from `next/link`) for this component; defaults to AuraProvider's `linkComponent`, then `<a>`. */
121
126
  linkComponent?: React$1.ElementType | undefined;
122
127
  label: React$1.ReactNode;
@@ -134,6 +139,13 @@ export interface StatProps {
134
139
  /** The label as a heading of this level (5.14), when the tile titles its section. Not with `onClick` (a heading
135
140
  * can't sit in a button); a link is fine. Default: a plain label. */
136
141
  headingLevel?: 2 | 3 | 4 | 5 | 6 | undefined;
142
+ /** A status line under the value — "Active · renews 1 Jan" with its tone icon (5.17, Chamber-OS 110). Hidden while
143
+ * `loading`. */
144
+ status?: React$1.ReactNode | undefined;
145
+ /** With `href`: `tile` (default) makes the whole tile the link; `label` puts the link on the label (inside its
146
+ * heading) and stretches its hit area over the tile, so a click anywhere follows it and the tile shows the focus
147
+ * ring (5.17, Chamber-OS 110). Other links or buttons in the tile stay clickable above it. */
148
+ linkArea?: "tile" | "label" | undefined;
137
149
  className?: string | undefined;
138
150
  }
139
151
  export type Tone = "neutral" | "accent" | "success" | "warning" | "danger";
@@ -1153,8 +1153,17 @@ function statElement(props, ref, Link) {
1153
1153
  const tone2 = ch && (ch.tone || (dir === "up" ? "positive" : dir === "down" ? "negative" : "neutral"));
1154
1154
  const v = props.value;
1155
1155
  const numeric = typeof v === "number" || typeof v === "string" && /\d/.test(v) && /^[\s\d.,:+\-\u2212%()\u0E3F$\u20AC\u00A3\u00A5kKmMbB]+$/.test(v.replace(/\b[A-Z]{3}\b/g, ""));
1156
- const Tag = props.href ? Link : props.onClick ? "button" : "div";
1156
+ const labelLink = !!props.href && props.linkArea === "label";
1157
+ const Tag = labelLink ? "div" : props.href ? Link : props.onClick ? "button" : "div";
1157
1158
  const interactive = !!(props.href || props.onClick);
1159
+ const LINK_ARIA = ["aria-label", "aria-labelledby", "aria-describedby", "aria-current"];
1160
+ const root = {}, toLink = {};
1161
+ Object.keys(props).forEach(function(k) {
1162
+ const v2 = props[k];
1163
+ if (labelLink && LINK_ARIA.indexOf(k) >= 0) toLink[k] = v2;
1164
+ else if (k === "id" || k === "style" || k === "lang" || k === "dir" || /^(aria|data)-/.test(k)) root[k] = v2;
1165
+ });
1166
+ const busy = props.loading || props["aria-busy"] || void 0;
1158
1167
  if (props.headingLevel && Tag === "button")
1159
1168
  devWarnOnce(
1160
1169
  "stat-heading-button",
@@ -1165,15 +1174,23 @@ function statElement(props, ref, Link) {
1165
1174
  return /* @__PURE__ */ React3.createElement(
1166
1175
  Tag,
1167
1176
  {
1177
+ ...root,
1168
1178
  ref,
1169
- className: cx("aura-stat", interactive && "is-interactive", props.loading && "is-loading", props.className),
1170
- href: props.href,
1171
- onClick: props.onClick,
1179
+ className: cx(
1180
+ "aura-stat",
1181
+ interactive && "is-interactive",
1182
+ labelLink && "aura-stat--label-link",
1183
+ props.loading && "is-loading",
1184
+ props.className
1185
+ ),
1186
+ href: labelLink ? void 0 : props.href,
1187
+ onClick: labelLink ? void 0 : props.onClick,
1172
1188
  type: Tag === "button" ? "button" : void 0,
1173
- "aria-busy": props.loading || void 0
1189
+ "aria-busy": busy
1174
1190
  },
1175
- /* @__PURE__ */ React3.createElement(HeadTag, { className: "aura-stat__head" }, /* @__PURE__ */ React3.createElement(LabelTag, { className: "aura-stat__label" }, props.label), props.icon ? /* @__PURE__ */ React3.createElement("span", { className: "aura-stat__icon" }, /* @__PURE__ */ React3.createElement(Icon, { name: props.icon })) : null),
1191
+ /* @__PURE__ */ React3.createElement(HeadTag, { className: "aura-stat__head" }, /* @__PURE__ */ React3.createElement(LabelTag, { className: "aura-stat__label" }, labelLink ? /* @__PURE__ */ React3.createElement(Link, { ...toLink, href: props.href, onClick: props.onClick, className: "aura-stat__link" }, props.label) : props.label), props.icon ? /* @__PURE__ */ React3.createElement("span", { className: "aura-stat__icon" }, /* @__PURE__ */ React3.createElement(Icon, { name: props.icon })) : null),
1176
1192
  props.loading ? /* @__PURE__ */ React3.createElement("span", { className: "aura-stat__value" }, /* @__PURE__ */ React3.createElement("span", { className: "aura-skel aura-stat__skel" })) : /* @__PURE__ */ React3.createElement("span", { className: cx("aura-stat__value", numeric && "is-numeric") }, props.value, props.unit ? /* @__PURE__ */ React3.createElement("span", { className: "aura-stat__unit" }, props.unit) : null),
1193
+ props.status != null && props.status !== false && props.status !== "" && !props.loading ? /* @__PURE__ */ React3.createElement("span", { className: "aura-stat__status" }, props.status) : null,
1177
1194
  ch && !props.loading || props.caption ? /* @__PURE__ */ React3.createElement("span", { className: "aura-stat__foot" }, ch && !props.loading ? /* @__PURE__ */ React3.createElement("span", { className: cx("aura-stat__change", "is-" + tone2) }, /* @__PURE__ */ React3.createElement(
1178
1195
  Icon,
1179
1196
  {
package/dist/styles.css CHANGED
@@ -464,7 +464,11 @@ a.aura-icon-btn { text-decoration: none; }
464
464
  .aura-field.is-invalid .aura-input, .aura-field.is-invalid .aura-textarea { border-color: var(--aura-border-danger); }
465
465
  .aura-field.is-disabled .aura-input { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-border-default); }
466
466
  .aura-field.is-disabled .aura-input__control { cursor: not-allowed; }
467
- .aura-input:has(.aura-input__control:read-only:not(select)) { background: var(--aura-bg-input-disabled); }
467
+ /* A read-only text field. Only an input: `:read-only` also matches any element that isn't editable — a
468
+ * Select's trigger <button> — which painted every custom Select with the disabled ground (5.16.1, Chamber-OS 109). */
469
+ .aura-input:has(input.aura-input__control:read-only) { background: var(--aura-bg-input-disabled); }
470
+ /* A read-only Textarea looks like a read-only TextField (it is its own .aura-input, so the rule above can't reach it). */
471
+ .aura-textarea:read-only:not(:disabled) { background: var(--aura-bg-input-disabled); }
468
472
 
469
473
  .aura-textarea { display: block; padding: 10px var(--aura-space-3); min-height: 96px; line-height: 1.6; resize: vertical; outline: none; }
470
474
  .aura-textarea:focus { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: 1px; border-color: var(--aura-border-strong); }
@@ -1028,6 +1032,20 @@ button.aura-stat { font: inherit; width: 100%; cursor: pointer; }
1028
1032
  .aura-stat__change.is-negative { color: var(--aura-fg-danger); }
1029
1033
  .aura-stat__change.is-neutral { color: var(--aura-fg-secondary); }
1030
1034
  .aura-stat__caption { color: var(--aura-fg-secondary); }
1035
+ /* 5.17 (Chamber-OS 110): a status line under the value, and a link on the label that covers the tile. */
1036
+ .aura-stat__status { display: flex; flex-wrap: wrap; align-items: center; gap: var(--aura-space-1) var(--aura-space-2); font-size: 13px; color: var(--aura-fg-secondary); }
1037
+ .aura-stat--label-link { position: relative; }
1038
+ .aura-stat__link { color: inherit; text-decoration: none; }
1039
+ .aura-stat__link::after { content: ''; position: absolute; inset: 0; border-radius: var(--aura-card-radius); }
1040
+ /* The ring is drawn on the tile where :has() works; elsewhere the link keeps its own. */
1041
+ .aura-stat__link:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: 2px; }
1042
+ @supports selector(:has(*)) {
1043
+ .aura-stat__link:focus-visible { outline: none; }
1044
+ .aura-stat--label-link:has(.aura-stat__link:focus-visible) { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: var(--aura-focus-ring-offset); }
1045
+ }
1046
+ .aura-stat--label-link:hover .aura-stat__link { text-decoration: underline; text-underline-offset: 3px; }
1047
+ /* Anything else interactive in the tile sits above the stretched link. No specificity, so a control's own position wins. */
1048
+ :where(.aura-stat--label-link) :where(a, button, input, select, textarea, label, summary, [tabindex]):not(:where(.aura-stat__link)) { position: relative; z-index: 1; }
1031
1049
 
1032
1050
  /* ---------- TimePicker ---------- */
1033
1051
  .aura-time__popover { min-width: 160px; }
@@ -466,7 +466,11 @@ a.aura-icon-btn { text-decoration: none; }
466
466
  .aura-field.is-invalid .aura-input, .aura-field.is-invalid .aura-textarea { border-color: var(--aura-border-danger); }
467
467
  .aura-field.is-disabled .aura-input { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-border-default); }
468
468
  .aura-field.is-disabled .aura-input__control { cursor: not-allowed; }
469
- .aura-input:has(.aura-input__control:read-only:not(select)) { background: var(--aura-bg-input-disabled); }
469
+ /* A read-only text field. Only an input: `:read-only` also matches any element that isn't editable — a
470
+ * Select's trigger <button> — which painted every custom Select with the disabled ground (5.16.1, Chamber-OS 109). */
471
+ .aura-input:has(input.aura-input__control:read-only) { background: var(--aura-bg-input-disabled); }
472
+ /* A read-only Textarea looks like a read-only TextField (it is its own .aura-input, so the rule above can't reach it). */
473
+ .aura-textarea:read-only:not(:disabled) { background: var(--aura-bg-input-disabled); }
470
474
 
471
475
  .aura-textarea { display: block; padding: 10px var(--aura-space-3); min-height: 96px; line-height: 1.6; resize: vertical; outline: none; }
472
476
  .aura-textarea:focus { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: 1px; border-color: var(--aura-border-strong); }
@@ -1030,6 +1034,20 @@ button.aura-stat { font: inherit; width: 100%; cursor: pointer; }
1030
1034
  .aura-stat__change.is-negative { color: var(--aura-fg-danger); }
1031
1035
  .aura-stat__change.is-neutral { color: var(--aura-fg-secondary); }
1032
1036
  .aura-stat__caption { color: var(--aura-fg-secondary); }
1037
+ /* 5.17 (Chamber-OS 110): a status line under the value, and a link on the label that covers the tile. */
1038
+ .aura-stat__status { display: flex; flex-wrap: wrap; align-items: center; gap: var(--aura-space-1) var(--aura-space-2); font-size: 13px; color: var(--aura-fg-secondary); }
1039
+ .aura-stat--label-link { position: relative; }
1040
+ .aura-stat__link { color: inherit; text-decoration: none; }
1041
+ .aura-stat__link::after { content: ''; position: absolute; inset: 0; border-radius: var(--aura-card-radius); }
1042
+ /* The ring is drawn on the tile where :has() works; elsewhere the link keeps its own. */
1043
+ .aura-stat__link:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: 2px; }
1044
+ @supports selector(:has(*)) {
1045
+ .aura-stat__link:focus-visible { outline: none; }
1046
+ .aura-stat--label-link:has(.aura-stat__link:focus-visible) { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: var(--aura-focus-ring-offset); }
1047
+ }
1048
+ .aura-stat--label-link:hover .aura-stat__link { text-decoration: underline; text-underline-offset: 3px; }
1049
+ /* Anything else interactive in the tile sits above the stretched link. No specificity, so a control's own position wins. */
1050
+ :where(.aura-stat--label-link) :where(a, button, input, select, textarea, label, summary, [tabindex]):not(:where(.aura-stat__link)) { position: relative; z-index: 1; }
1033
1051
 
1034
1052
  /* ---------- TimePicker ---------- */
1035
1053
  .aura-time__popover { min-width: 160px; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jirawatpyk/aura-react",
3
- "version": "5.16.0",
3
+ "version": "5.17.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",