@jirawatpyk/aura-react 5.78.0 → 5.80.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
@@ -100,7 +100,69 @@ export function Providers({ children, locale }: { children: React.ReactNode; loc
100
100
  // app/layout.tsx (a Server Component): <body><Providers locale="th">{children}</Providers></body>
101
101
  ```
102
102
 
103
- `linkComponent` is used by Button `href`, Breadcrumb, Tabs and Menu items with `href`, BottomNav, Pagination `getHref` (page numbers and, since 4.17, the previous / next arrows), Stat `href`, SideNav and DataTable row/pager links. Each of them also takes its own `linkComponent`, which wins over the provider's. Pass `getHref` from a client component (it's a function too). The Next.js starter does all of this, and CI clicks every AURA link in it to check none triggers a full page load. Only `th` shows Buddhist-era years; `en` and `sv` are Gregorian (`sv` weeks start Monday). **Without a provider, components are English with Gregorian dates** — wrap Thai apps in `<AuraProvider locale="th">`. For dates in your own components use `useFormatDate()` — it follows the provider (`const fmt = useFormatDate(); fmt(iso, { format: 'long' })`). Plain `formatDate()` has no provider to read: since 5.0 it is English and Gregorian unless you pass `locale`, the same function from the package root and from `@jirawatpyk/aura-react/server`. Values are always Gregorian ISO dates.
103
+ `linkComponent` is used by Button `href`, Breadcrumb, Tabs and Menu items with `href`, BottomNav, Pagination `getHref` (page numbers and, since 4.17, the previous / next arrows), Stat and StatusTile `href`, Th `sortHref`, toast action `href`, SideNav and DataTable row/pager links. Each of them also takes its own `linkComponent`, which wins over the provider's. Pass `getHref` from a client component (it's a function too). The Next.js starter does all of this, and CI clicks every AURA link in it to check none triggers a full page load. Only `th` shows Buddhist-era years; `en` and `sv` are Gregorian (`sv` weeks start Monday). **Without a provider, components are English with Gregorian dates** — wrap Thai apps in `<AuraProvider locale="th">`. For dates in your own components use `useFormatDate()` — it follows the provider (`const fmt = useFormatDate(); fmt(iso, { format: 'long' })`). Plain `formatDate()` has no provider to read: since 5.0 it is English and Gregorian unless you pass `locale`, the same function from the package root and from `@jirawatpyk/aura-react/server`. Values are always Gregorian ISO dates.
104
+
105
+ TanStack Start / TanStack Router (5.80): AURA gives `linkComponent` a plain `href`, so pass a small `<a>` that hands same-origin clicks to the router — not TanStack's `<Link>`, which adds `aria-current="page"` to every link its own matcher calls active (on `/members?page=3`, the page 1 link and the "All" tab as well), over AURA's own:
106
+
107
+ ```tsx
108
+ // src/components/aura-link.tsx
109
+ import * as React from 'react';
110
+ import { useRouter } from '@tanstack/react-router';
111
+
112
+ function routerHref(href?: string): string | null {
113
+ if (href == null || typeof window === 'undefined') return null;
114
+ let url: URL;
115
+ try {
116
+ url = new URL(href, window.location.href); // also '?page=2' and 'members'
117
+ } catch {
118
+ return null;
119
+ }
120
+ const here = window.location;
121
+ if (url.origin !== here.origin) return null; // other sites, mailto:, tel:
122
+ if (url.hash && url.pathname === here.pathname && url.search === here.search) return null; // #fragment here
123
+ return url.pathname + url.search + url.hash;
124
+ }
125
+
126
+ export const AuraLink = React.forwardRef<HTMLAnchorElement, React.AnchorHTMLAttributes<HTMLAnchorElement>>(
127
+ function AuraLink({ href, onClick, onMouseEnter, onFocus, ...rest }, ref) {
128
+ const router = useRouter();
129
+ const preload = () => {
130
+ const to = routerHref(href);
131
+ if (to) router.preloadRoute({ href: to } as Parameters<typeof router.preloadRoute>[0]).catch(() => undefined);
132
+ };
133
+ return (
134
+ <a ref={ref} href={href} {...rest}
135
+ onClick={(e) => {
136
+ onClick?.(e); // AURA's own handler first (a DataTable pager with onPageChange, a toast action)
137
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
138
+ const target = e.currentTarget.getAttribute('target');
139
+ if ((target && target !== '_self') || e.currentTarget.hasAttribute('download')) return;
140
+ const to = routerHref(href);
141
+ if (!to) return;
142
+ e.preventDefault();
143
+ void router.navigate({ href: to });
144
+ }}
145
+ onMouseEnter={(e) => { onMouseEnter?.(e); preload(); }}
146
+ onFocus={(e) => { onFocus?.(e); preload(); }}
147
+ />
148
+ );
149
+ },
150
+ );
151
+
152
+ // src/routes/__root.tsx — the provider inside the router (the root route's component), the document in shellComponent
153
+ export const Route = createRootRoute({
154
+ head: () => ({ links: [{ rel: 'stylesheet', href: tokensCss }, { rel: 'stylesheet', href: auraCss }] }), // '…css?url' imports
155
+ shellComponent: RootDocument, // <html><head><HeadContent /><ColorSchemeScript /></head><body>{children}<Scripts /></body></html>
156
+ component: () => (
157
+ <AuraProvider locale="th" strings={th} linkComponent={AuraLink}>
158
+ <Outlet />
159
+ <Toaster />
160
+ </AuraProvider>
161
+ ),
162
+ });
163
+ ```
164
+
165
+ Write hrefs as URLs (`getHref={(p) => '/members?page=' + p}`) and read them back with the route's `validateSearch`. A client navigation passes the query through the router's search parser and serializer, so keep params single-valued (`?a=1&a=2` comes back as JSON, `?q=1e3` as `1000`) or set the router's `parseSearch` / `stringifySearch`. With a `basepath`, write hrefs with it; a same-origin href outside it goes to the router's not-found route. DataTable keyboard paging (PageDown / PageUp, arrows past the page) clicks the pager links, and since 5.80 the focus moves to the new page's row. The TanStack Start starter (`templates/tanstack-start-starter`) has all of this; CI builds it against the packed packages and clicks every AURA link in it — Button, Breadcrumb, link Tabs, DropdownMenu items, BottomNav, Pagination, Th `sortHref`, Stat, StatusTile, SideNav, DataTable rows and pager, toast actions — checking that none loads a full page, that `aria-current` is AURA's and that Ctrl/⌘-click still opens a tab.
104
166
 
105
167
  ## Compact density
106
168
 
@@ -186,6 +248,19 @@ DataTable with sort and page in the URL (4.17): `onStateChange={({ sort, page })
186
248
  - **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
249
  - **Toasts** queue past three instead of dropping: six in a row all show, in order, three at a time.
188
250
 
251
+ ## 5.79 — visual audit, set 3 (component by component)
252
+
253
+ - **One disabled look for every Button variant:** a faint fill (`--aura-button-disabled-bg`: zinc-100 light, none in dark), a faint edge (`--aura-button-disabled-border` — the variant's own border, or 1px inside for primary and danger) and the tertiary label, at full opacity — primary, secondary, danger, creative and `aria-disabled` alike; a ghost button has neither fill nor edge. (5.30 gave filled buttons a grey fill that read as a To Do pill and faded outline ones.)
254
+ - **ActionBar `look="edge"` in an AppShell on a desktop** starts after the sidebar (the rail width when collapsed; the right edge in RTL) and lines its contents up with the content's 32px padding (not with `contentPadding={false}`), instead of running under the sidebar. It reads the sidebar's width from `--aura-sidenav-width` / `--aura-sidenav-rail-width`: to change it, set the token on `.aura-shell` or `:root`, not on the nav — and a custom `nav` set them to its width. Outside an AppShell it keeps the screen's width; `edgeBelow` is unchanged (no sidebar there).
255
+ - **A list box** (`<Select multiple>` or `size` > 1) shows its rows, as tall as they need, without the chevron.
256
+ - **Combobox `multiple`:** a chip no longer shrinks to "V.."; it keeps its label and moves to the next line (a label wider than the field still ends in "…").
257
+ - **Switch with an `icon`** (a locked, read-only switch): the lock sits 8px after the label, not at the row's far end. Its colours stay as 5.19 set them.
258
+ - **Menu / DropdownMenu:** a menu whose items have no icon, check box or radio has no empty icon column; it opens on its trigger's left edge when ending on its right would cross the screen (LTR; positioning stays physical, as before); and it is lifted with `--aura-shadow-overlay`, as Popover and the list boxes.
259
+ - **Small fixes:** FileUpload's hint names formats (PNG, JPEG, SVG, DOCX, XLSX), not "IMAGE/PNG"; an outline Badge has a quieter edge (its colour at 45%), so it doesn't read as a secondary Button; a date range is a violet-100 band in light (`--aura-calendar-range-bg`, the brand's 100 step in `createTheme`), visible on white; a disabled field's edge shows in dark (`--aura-input-disabled-border`, zinc-700).
260
+ - **Thai:** a Calendar with a Thai locale carries `lang="th"` and its weekday row is 13px; two-line Thai SideNav labels get room between lines; Combobox options have fixed line heights, so Thai and English rows are spaced alike.
261
+ - **PageHeader:** where `actionsFullWidthBelow` reaches wider than `actionsBarBelow`, every button in the row is `radius-md` between the two breakpoints (5.78 left the bar's buttons as pills there).
262
+ - **Stories:** the ⋯ beside outline buttons in action rows is `variant="outline"` (keep it so in an action row); the launch page shows with its own styles.
263
+
189
264
  ## 5.78 — visual audit, set 2 (phones)
190
265
 
191
266
  - **Bottom strips on a phone follow one rule:** ActionBar's edge strip (`look="edge"`, `edgeBelow`) now lays out as PageHeader's bar (5.48) — the status on its own row at the top, the actions sharing the row under it (an IconButton keeps its square; labels too long to share take a row each, as PageHeader's) — and both strips are 16px from either side, the page's phone gutter (they were 16 and 12). `edgeBelow="md"` follows it up to 768px. A single label wider than the screen still overflows, as in PageHeader's strip — keep bar labels short.
@@ -1929,7 +1929,8 @@ window.Aura = (() => {
1929
1929
  top = r2.top - above - 4;
1930
1930
  } else maxHeight = below2;
1931
1931
  }
1932
- const left = Math.max(8, Math.min(r2.right - mw, window.innerWidth - mw - 8));
1932
+ const endAligned = r2.right - mw;
1933
+ const left = Math.max(8, Math.min(endAligned >= 8 ? endAligned : r2.left, window.innerWidth - mw - 8));
1933
1934
  setPos(function(p) {
1934
1935
  return p && p.top === top && p.left === left && p.maxHeight === maxHeight ? p : { top, left, maxHeight };
1935
1936
  });
@@ -2028,12 +2029,15 @@ window.Aura = (() => {
2028
2029
  });
2029
2030
  return out;
2030
2031
  }
2032
+ const anyLead = items2.some(function(it) {
2033
+ return !it.separator && (!!it.icon || it.type === "radio" || it.checked !== void 0);
2034
+ });
2031
2035
  function renderItem(it, i) {
2032
2036
  if (it.separator) return /* @__PURE__ */ React9.createElement("div", { key: "s" + i, role: "separator", className: "aura-menu__sep" });
2033
2037
  const isRadio = it.type === "radio";
2034
2038
  const isCheck = !isRadio && it.checked !== void 0;
2035
2039
  const checkAtEnd = isCheck && !!it.icon && it.checkPlacement !== "start";
2036
- const lead = isRadio ? /* @__PURE__ */ React9.createElement("span", { className: cx("aura-menu__radio", it.checked && "is-on") }) : isCheck && !checkAtEnd ? /* @__PURE__ */ React9.createElement("span", { className: cx("aura-menu__check", it.checked && "is-on") }, it.checked ? /* @__PURE__ */ React9.createElement(Icon, { name: /* @__PURE__ */ React9.createElement(IconCheck, null), size: 12, strokeWidth: 3 }) : null) : it.icon ? /* @__PURE__ */ React9.createElement(Icon, { name: it.icon }) : /* @__PURE__ */ React9.createElement("span", { className: "aura-menu__blank" });
2040
+ const lead = isRadio ? /* @__PURE__ */ React9.createElement("span", { className: cx("aura-menu__radio", it.checked && "is-on") }) : isCheck && !checkAtEnd ? /* @__PURE__ */ React9.createElement("span", { className: cx("aura-menu__check", it.checked && "is-on") }, it.checked ? /* @__PURE__ */ React9.createElement(Icon, { name: /* @__PURE__ */ React9.createElement(IconCheck, null), size: 12, strokeWidth: 3 }) : null) : it.icon ? /* @__PURE__ */ React9.createElement(Icon, { name: it.icon }) : anyLead ? /* @__PURE__ */ React9.createElement("span", { className: "aura-menu__blank" }) : null;
2037
2041
  const body = [
2038
2042
  /* @__PURE__ */ React9.createElement(React9.Fragment, { key: "l" }, lead),
2039
2043
  /* @__PURE__ */ React9.createElement("span", { key: "t", className: "aura-menu__label" }, it.label),
@@ -3444,7 +3448,7 @@ window.Aura = (() => {
3444
3448
  setFocus(addMonths(focusDate, 1));
3445
3449
  }
3446
3450
  }
3447
- )), /* @__PURE__ */ React18.createElement("table", { role: "grid", className: "aura-cal__grid", "aria-label": monthTitle }, /* @__PURE__ */ React18.createElement("thead", null, /* @__PURE__ */ React18.createElement("tr", null, weekdayNames.map(function(n3, i) {
3451
+ )), /* @__PURE__ */ React18.createElement("table", { role: "grid", className: "aura-cal__grid", "aria-label": monthTitle, lang: th2 ? "th" : void 0 }, /* @__PURE__ */ React18.createElement("thead", null, /* @__PURE__ */ React18.createElement("tr", null, weekdayNames.map(function(n3, i) {
3448
3452
  return /* @__PURE__ */ React18.createElement("th", { key: i, scope: "col", abbr: n3.long }, /* @__PURE__ */ React18.createElement("span", { "aria-hidden": true }, n3.short));
3449
3453
  }))), /* @__PURE__ */ React18.createElement(
3450
3454
  "tbody",
@@ -6544,6 +6548,7 @@ window.Aura = (() => {
6544
6548
  if (next === page) return false;
6545
6549
  if (!quiet) emit(sort, next);
6546
6550
  if (linkPaging) {
6551
+ if (focus) pending.current = { ...focus, page: next, at: Date.now() };
6547
6552
  const a = next === page - 1 ? prevLink.current : next === page + 1 ? nextLink.current : null;
6548
6553
  if (a) a.click();
6549
6554
  else jumpState[1](next);
@@ -6642,6 +6647,11 @@ window.Aura = (() => {
6642
6647
  React34.useEffect(function() {
6643
6648
  const p = pending.current;
6644
6649
  if (!p || !gridRef.current) return;
6650
+ if (p.at != null && Date.now() - p.at > 1e4) {
6651
+ pending.current = null;
6652
+ return;
6653
+ }
6654
+ if (p.page != null && p.page !== page) return;
6645
6655
  let c = p.c;
6646
6656
  if (p.key) {
6647
6657
  let at = -1;
@@ -10074,6 +10084,19 @@ window.Aura = (() => {
10074
10084
  a = a.trim();
10075
10085
  if (a === "image/*") return t.images;
10076
10086
  if (a === "application/pdf") return "PDF";
10087
+ const known = {
10088
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "DOCX",
10089
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "XLSX",
10090
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "PPTX",
10091
+ "application/msword": "DOC",
10092
+ "application/vnd.ms-excel": "XLS",
10093
+ "application/vnd.ms-powerpoint": "PPT",
10094
+ "text/plain": "TXT"
10095
+ };
10096
+ if (known[a]) return known[a];
10097
+ const slash = a.indexOf("/");
10098
+ if (slash > 0 && a.slice(slash + 1) !== "*")
10099
+ a = a.slice(slash + 1).replace(/^(?:vnd\.|x-)/, "").replace(/\+xml$/, "");
10077
10100
  return a.replace(/^\./, "").toUpperCase();
10078
10101
  }).filter(function(x, i, arr) {
10079
10102
  return arr.indexOf(x) === i;
@@ -10482,6 +10505,7 @@ window.Aura = (() => {
10482
10505
  L["accent-violet"] = L["fg-accent"];
10483
10506
  L["focus-ring"] = fit(b[700], lightGrounds.concat([b[50]]), 3, -1);
10484
10507
  L["bg-selected"] = b[50];
10508
+ L["calendar-range-bg"] = b[100];
10485
10509
  L["status-progress-bg"] = b[100];
10486
10510
  L["status-progress-fg"] = fit(b[800], [b[100]], 4.5, -1);
10487
10511
  L["alert-info-bg"] = b[50];
@@ -10521,6 +10545,7 @@ window.Aura = (() => {
10521
10545
  D["control-checked-bg"] = fit(b[300], darkGrounds, 3, 1);
10522
10546
  D["control-checked-fg"] = onFill(D["control-checked-bg"]);
10523
10547
  D["nav-active-edge"] = D["control-checked-bg"];
10548
+ D["calendar-range-bg"] = D["bg-selected"];
10524
10549
  const shadowLight = INK, shadowDark = D["accent-violet"];
10525
10550
  if (o.primary === "brand") {
10526
10551
  L["button-primary-bg"] = fit(b[600], [ZINC[0]], 4.5, -1);
@@ -10578,6 +10603,7 @@ window.Aura = (() => {
10578
10603
  check("dark", "chart-1", "chart-2 (lightness step)", 1.8, D["chart-1"], CHART_2);
10579
10604
  check("light", "fg-primary", "bg-selected", 4.5, INK, L["bg-selected"]);
10580
10605
  check("dark", "fg-primary", "bg-selected", 4.5, "#ffffff", D["bg-selected"]);
10606
+ check("light", "fg-primary", "calendar-range-bg", 4.5, INK, L["calendar-range-bg"]);
10581
10607
  check("light", "status-progress-fg", "status-progress-bg", 4.5, L["status-progress-fg"], L["status-progress-bg"]);
10582
10608
  check("dark", "status-progress-fg", "status-progress-bg", 4.5, D["status-progress-fg"], D["status-progress-bg"]);
10583
10609
  check("light", "alert-info-fg", "alert-info-bg", 4.5, L["alert-info-fg"], L["alert-info-bg"]);
@@ -1911,7 +1911,8 @@ var Menu = React9.forwardRef(function Menu2(props, ref) {
1911
1911
  top = r2.top - above - 4;
1912
1912
  } else maxHeight = below2;
1913
1913
  }
1914
- const left = Math.max(8, Math.min(r2.right - mw, window.innerWidth - mw - 8));
1914
+ const endAligned = r2.right - mw;
1915
+ const left = Math.max(8, Math.min(endAligned >= 8 ? endAligned : r2.left, window.innerWidth - mw - 8));
1915
1916
  setPos(function(p) {
1916
1917
  return p && p.top === top && p.left === left && p.maxHeight === maxHeight ? p : { top, left, maxHeight };
1917
1918
  });
@@ -2010,12 +2011,15 @@ var Menu = React9.forwardRef(function Menu2(props, ref) {
2010
2011
  });
2011
2012
  return out;
2012
2013
  }
2014
+ const anyLead = items2.some(function(it) {
2015
+ return !it.separator && (!!it.icon || it.type === "radio" || it.checked !== void 0);
2016
+ });
2013
2017
  function renderItem(it, i) {
2014
2018
  if (it.separator) return /* @__PURE__ */ React9.createElement("div", { key: "s" + i, role: "separator", className: "aura-menu__sep" });
2015
2019
  const isRadio = it.type === "radio";
2016
2020
  const isCheck = !isRadio && it.checked !== void 0;
2017
2021
  const checkAtEnd = isCheck && !!it.icon && it.checkPlacement !== "start";
2018
- const lead = isRadio ? /* @__PURE__ */ React9.createElement("span", { className: cx("aura-menu__radio", it.checked && "is-on") }) : isCheck && !checkAtEnd ? /* @__PURE__ */ React9.createElement("span", { className: cx("aura-menu__check", it.checked && "is-on") }, it.checked ? /* @__PURE__ */ React9.createElement(Icon, { name: /* @__PURE__ */ React9.createElement(IconCheck, null), size: 12, strokeWidth: 3 }) : null) : it.icon ? /* @__PURE__ */ React9.createElement(Icon, { name: it.icon }) : /* @__PURE__ */ React9.createElement("span", { className: "aura-menu__blank" });
2022
+ const lead = isRadio ? /* @__PURE__ */ React9.createElement("span", { className: cx("aura-menu__radio", it.checked && "is-on") }) : isCheck && !checkAtEnd ? /* @__PURE__ */ React9.createElement("span", { className: cx("aura-menu__check", it.checked && "is-on") }, it.checked ? /* @__PURE__ */ React9.createElement(Icon, { name: /* @__PURE__ */ React9.createElement(IconCheck, null), size: 12, strokeWidth: 3 }) : null) : it.icon ? /* @__PURE__ */ React9.createElement(Icon, { name: it.icon }) : anyLead ? /* @__PURE__ */ React9.createElement("span", { className: "aura-menu__blank" }) : null;
2019
2023
  const body = [
2020
2024
  /* @__PURE__ */ React9.createElement(React9.Fragment, { key: "l" }, lead),
2021
2025
  /* @__PURE__ */ React9.createElement("span", { key: "t", className: "aura-menu__label" }, it.label),
@@ -3426,7 +3430,7 @@ var Calendar = React18.forwardRef(function Calendar2(props, ref) {
3426
3430
  setFocus(addMonths(focusDate, 1));
3427
3431
  }
3428
3432
  }
3429
- )), /* @__PURE__ */ React18.createElement("table", { role: "grid", className: "aura-cal__grid", "aria-label": monthTitle }, /* @__PURE__ */ React18.createElement("thead", null, /* @__PURE__ */ React18.createElement("tr", null, weekdayNames.map(function(n3, i) {
3433
+ )), /* @__PURE__ */ React18.createElement("table", { role: "grid", className: "aura-cal__grid", "aria-label": monthTitle, lang: th2 ? "th" : void 0 }, /* @__PURE__ */ React18.createElement("thead", null, /* @__PURE__ */ React18.createElement("tr", null, weekdayNames.map(function(n3, i) {
3430
3434
  return /* @__PURE__ */ React18.createElement("th", { key: i, scope: "col", abbr: n3.long }, /* @__PURE__ */ React18.createElement("span", { "aria-hidden": true }, n3.short));
3431
3435
  }))), /* @__PURE__ */ React18.createElement(
3432
3436
  "tbody",
@@ -6526,6 +6530,7 @@ var DataTableImpl = React34.forwardRef(function DataTable(props, ref) {
6526
6530
  if (next === page) return false;
6527
6531
  if (!quiet) emit(sort, next);
6528
6532
  if (linkPaging) {
6533
+ if (focus) pending.current = { ...focus, page: next, at: Date.now() };
6529
6534
  const a = next === page - 1 ? prevLink.current : next === page + 1 ? nextLink.current : null;
6530
6535
  if (a) a.click();
6531
6536
  else jumpState[1](next);
@@ -6624,6 +6629,11 @@ var DataTableImpl = React34.forwardRef(function DataTable(props, ref) {
6624
6629
  React34.useEffect(function() {
6625
6630
  const p = pending.current;
6626
6631
  if (!p || !gridRef.current) return;
6632
+ if (p.at != null && Date.now() - p.at > 1e4) {
6633
+ pending.current = null;
6634
+ return;
6635
+ }
6636
+ if (p.page != null && p.page !== page) return;
6627
6637
  let c = p.c;
6628
6638
  if (p.key) {
6629
6639
  let at = -1;
@@ -10056,6 +10066,19 @@ function describeAccept(accept, t) {
10056
10066
  a = a.trim();
10057
10067
  if (a === "image/*") return t.images;
10058
10068
  if (a === "application/pdf") return "PDF";
10069
+ const known = {
10070
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "DOCX",
10071
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "XLSX",
10072
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "PPTX",
10073
+ "application/msword": "DOC",
10074
+ "application/vnd.ms-excel": "XLS",
10075
+ "application/vnd.ms-powerpoint": "PPT",
10076
+ "text/plain": "TXT"
10077
+ };
10078
+ if (known[a]) return known[a];
10079
+ const slash = a.indexOf("/");
10080
+ if (slash > 0 && a.slice(slash + 1) !== "*")
10081
+ a = a.slice(slash + 1).replace(/^(?:vnd\.|x-)/, "").replace(/\+xml$/, "");
10059
10082
  return a.replace(/^\./, "").toUpperCase();
10060
10083
  }).filter(function(x, i, arr) {
10061
10084
  return arr.indexOf(x) === i;
@@ -10464,6 +10487,7 @@ function createTheme(opts) {
10464
10487
  L["accent-violet"] = L["fg-accent"];
10465
10488
  L["focus-ring"] = fit(b[700], lightGrounds.concat([b[50]]), 3, -1);
10466
10489
  L["bg-selected"] = b[50];
10490
+ L["calendar-range-bg"] = b[100];
10467
10491
  L["status-progress-bg"] = b[100];
10468
10492
  L["status-progress-fg"] = fit(b[800], [b[100]], 4.5, -1);
10469
10493
  L["alert-info-bg"] = b[50];
@@ -10503,6 +10527,7 @@ function createTheme(opts) {
10503
10527
  D["control-checked-bg"] = fit(b[300], darkGrounds, 3, 1);
10504
10528
  D["control-checked-fg"] = onFill(D["control-checked-bg"]);
10505
10529
  D["nav-active-edge"] = D["control-checked-bg"];
10530
+ D["calendar-range-bg"] = D["bg-selected"];
10506
10531
  const shadowLight = INK, shadowDark = D["accent-violet"];
10507
10532
  if (o.primary === "brand") {
10508
10533
  L["button-primary-bg"] = fit(b[600], [ZINC[0]], 4.5, -1);
@@ -10560,6 +10585,7 @@ function createTheme(opts) {
10560
10585
  check("dark", "chart-1", "chart-2 (lightness step)", 1.8, D["chart-1"], CHART_2);
10561
10586
  check("light", "fg-primary", "bg-selected", 4.5, INK, L["bg-selected"]);
10562
10587
  check("dark", "fg-primary", "bg-selected", 4.5, "#ffffff", D["bg-selected"]);
10588
+ check("light", "fg-primary", "calendar-range-bg", 4.5, INK, L["calendar-range-bg"]);
10563
10589
  check("light", "status-progress-fg", "status-progress-bg", 4.5, L["status-progress-fg"], L["status-progress-bg"]);
10564
10590
  check("dark", "status-progress-fg", "status-progress-bg", 4.5, D["status-progress-fg"], D["status-progress-bg"]);
10565
10591
  check("light", "alert-info-fg", "alert-info-bg", 4.5, L["alert-info-fg"], L["alert-info-bg"]);
@@ -487,6 +487,7 @@ const DataTableImpl = React.forwardRef(function DataTable(props, ref) {
487
487
  if (next === page) return false;
488
488
  if (!quiet) emit(sort, next);
489
489
  if (linkPaging) {
490
+ if (focus) pending.current = { ...focus, page: next, at: Date.now() };
490
491
  const a = next === page - 1 ? prevLink.current : next === page + 1 ? nextLink.current : null;
491
492
  if (a) a.click();
492
493
  else jumpState[1](next);
@@ -585,6 +586,11 @@ const DataTableImpl = React.forwardRef(function DataTable(props, ref) {
585
586
  React.useEffect(function() {
586
587
  const p = pending.current;
587
588
  if (!p || !gridRef.current) return;
589
+ if (p.at != null && Date.now() - p.at > 1e4) {
590
+ pending.current = null;
591
+ return;
592
+ }
593
+ if (p.page != null && p.page !== page) return;
588
594
  let c = p.c;
589
595
  if (p.key) {
590
596
  let at = -1;
@@ -226,7 +226,7 @@ const Calendar = React.forwardRef(function Calendar2(props, ref) {
226
226
  setFocus(addMonths(focusDate, 1));
227
227
  }
228
228
  }
229
- )), /* @__PURE__ */ React.createElement("table", { role: "grid", className: "aura-cal__grid", "aria-label": monthTitle }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", null, weekdayNames.map(function(n, i) {
229
+ )), /* @__PURE__ */ React.createElement("table", { role: "grid", className: "aura-cal__grid", "aria-label": monthTitle, lang: th ? "th" : void 0 }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", null, weekdayNames.map(function(n, i) {
230
230
  return /* @__PURE__ */ React.createElement("th", { key: i, scope: "col", abbr: n.long }, /* @__PURE__ */ React.createElement("span", { "aria-hidden": true }, n.short));
231
231
  }))), /* @__PURE__ */ React.createElement(
232
232
  "tbody",
@@ -26,6 +26,19 @@ function describeAccept(accept, t) {
26
26
  a = a.trim();
27
27
  if (a === "image/*") return t.images;
28
28
  if (a === "application/pdf") return "PDF";
29
+ const known = {
30
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "DOCX",
31
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "XLSX",
32
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "PPTX",
33
+ "application/msword": "DOC",
34
+ "application/vnd.ms-excel": "XLS",
35
+ "application/vnd.ms-powerpoint": "PPT",
36
+ "text/plain": "TXT"
37
+ };
38
+ if (known[a]) return known[a];
39
+ const slash = a.indexOf("/");
40
+ if (slash > 0 && a.slice(slash + 1) !== "*")
41
+ a = a.slice(slash + 1).replace(/^(?:vnd\.|x-)/, "").replace(/\+xml$/, "");
29
42
  return a.replace(/^\./, "").toUpperCase();
30
43
  }).filter(function(x, i, arr) {
31
44
  return arr.indexOf(x) === i;
package/dist/esm/Menu.js CHANGED
@@ -61,7 +61,8 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
61
61
  top = r.top - above - 4;
62
62
  } else maxHeight = below;
63
63
  }
64
- const left = Math.max(8, Math.min(r.right - mw, window.innerWidth - mw - 8));
64
+ const endAligned = r.right - mw;
65
+ const left = Math.max(8, Math.min(endAligned >= 8 ? endAligned : r.left, window.innerWidth - mw - 8));
65
66
  setPos(function(p) {
66
67
  return p && p.top === top && p.left === left && p.maxHeight === maxHeight ? p : { top, left, maxHeight };
67
68
  });
@@ -160,12 +161,15 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
160
161
  });
161
162
  return out;
162
163
  }
164
+ const anyLead = items.some(function(it) {
165
+ return !it.separator && (!!it.icon || it.type === "radio" || it.checked !== void 0);
166
+ });
163
167
  function renderItem(it, i) {
164
168
  if (it.separator) return /* @__PURE__ */ React.createElement("div", { key: "s" + i, role: "separator", className: "aura-menu__sep" });
165
169
  const isRadio = it.type === "radio";
166
170
  const isCheck = !isRadio && it.checked !== void 0;
167
171
  const checkAtEnd = isCheck && !!it.icon && it.checkPlacement !== "start";
168
- const lead = isRadio ? /* @__PURE__ */ React.createElement("span", { className: cx("aura-menu__radio", it.checked && "is-on") }) : isCheck && !checkAtEnd ? /* @__PURE__ */ React.createElement("span", { className: cx("aura-menu__check", it.checked && "is-on") }, it.checked ? /* @__PURE__ */ React.createElement(Icon, { name: /* @__PURE__ */ React.createElement(IconCheck, null), size: 12, strokeWidth: 3 }) : null) : it.icon ? /* @__PURE__ */ React.createElement(Icon, { name: it.icon }) : /* @__PURE__ */ React.createElement("span", { className: "aura-menu__blank" });
172
+ const lead = isRadio ? /* @__PURE__ */ React.createElement("span", { className: cx("aura-menu__radio", it.checked && "is-on") }) : isCheck && !checkAtEnd ? /* @__PURE__ */ React.createElement("span", { className: cx("aura-menu__check", it.checked && "is-on") }, it.checked ? /* @__PURE__ */ React.createElement(Icon, { name: /* @__PURE__ */ React.createElement(IconCheck, null), size: 12, strokeWidth: 3 }) : null) : it.icon ? /* @__PURE__ */ React.createElement(Icon, { name: it.icon }) : anyLead ? /* @__PURE__ */ React.createElement("span", { className: "aura-menu__blank" }) : null;
169
173
  const body = [
170
174
  /* @__PURE__ */ React.createElement(React.Fragment, { key: "l" }, lead),
171
175
  /* @__PURE__ */ React.createElement("span", { key: "t", className: "aura-menu__label" }, it.label),
package/dist/esm/theme.js CHANGED
@@ -131,6 +131,7 @@ function createTheme(opts) {
131
131
  L["accent-violet"] = L["fg-accent"];
132
132
  L["focus-ring"] = fit(b[700], lightGrounds.concat([b[50]]), 3, -1);
133
133
  L["bg-selected"] = b[50];
134
+ L["calendar-range-bg"] = b[100];
134
135
  L["status-progress-bg"] = b[100];
135
136
  L["status-progress-fg"] = fit(b[800], [b[100]], 4.5, -1);
136
137
  L["alert-info-bg"] = b[50];
@@ -170,6 +171,7 @@ function createTheme(opts) {
170
171
  D["control-checked-bg"] = fit(b[300], darkGrounds, 3, 1);
171
172
  D["control-checked-fg"] = onFill(D["control-checked-bg"]);
172
173
  D["nav-active-edge"] = D["control-checked-bg"];
174
+ D["calendar-range-bg"] = D["bg-selected"];
173
175
  const shadowLight = INK, shadowDark = D["accent-violet"];
174
176
  if (o.primary === "brand") {
175
177
  L["button-primary-bg"] = fit(b[600], [ZINC[0]], 4.5, -1);
@@ -227,6 +229,7 @@ function createTheme(opts) {
227
229
  check("dark", "chart-1", "chart-2 (lightness step)", 1.8, D["chart-1"], CHART_2);
228
230
  check("light", "fg-primary", "bg-selected", 4.5, INK, L["bg-selected"]);
229
231
  check("dark", "fg-primary", "bg-selected", 4.5, "#ffffff", D["bg-selected"]);
232
+ check("light", "fg-primary", "calendar-range-bg", 4.5, INK, L["calendar-range-bg"]);
230
233
  check("light", "status-progress-fg", "status-progress-bg", 4.5, L["status-progress-fg"], L["status-progress-bg"]);
231
234
  check("dark", "status-progress-fg", "status-progress-bg", 4.5, D["status-progress-fg"], D["status-progress-bg"]);
232
235
  check("light", "alert-info-fg", "alert-info-bg", 4.5, L["alert-info-fg"], L["alert-info-bg"]);
@@ -780,6 +780,7 @@ function createTheme(opts) {
780
780
  L["accent-violet"] = L["fg-accent"];
781
781
  L["focus-ring"] = fit(b[700], lightGrounds.concat([b[50]]), 3, -1);
782
782
  L["bg-selected"] = b[50];
783
+ L["calendar-range-bg"] = b[100];
783
784
  L["status-progress-bg"] = b[100];
784
785
  L["status-progress-fg"] = fit(b[800], [b[100]], 4.5, -1);
785
786
  L["alert-info-bg"] = b[50];
@@ -819,6 +820,7 @@ function createTheme(opts) {
819
820
  D["control-checked-bg"] = fit(b[300], darkGrounds, 3, 1);
820
821
  D["control-checked-fg"] = onFill(D["control-checked-bg"]);
821
822
  D["nav-active-edge"] = D["control-checked-bg"];
823
+ D["calendar-range-bg"] = D["bg-selected"];
822
824
  const shadowLight = INK, shadowDark = D["accent-violet"];
823
825
  if (o.primary === "brand") {
824
826
  L["button-primary-bg"] = fit(b[600], [ZINC[0]], 4.5, -1);
@@ -876,6 +878,7 @@ function createTheme(opts) {
876
878
  check("dark", "chart-1", "chart-2 (lightness step)", 1.8, D["chart-1"], CHART_2);
877
879
  check("light", "fg-primary", "bg-selected", 4.5, INK, L["bg-selected"]);
878
880
  check("dark", "fg-primary", "bg-selected", 4.5, "#ffffff", D["bg-selected"]);
881
+ check("light", "fg-primary", "calendar-range-bg", 4.5, INK, L["calendar-range-bg"]);
879
882
  check("light", "status-progress-fg", "status-progress-bg", 4.5, L["status-progress-fg"], L["status-progress-bg"]);
880
883
  check("dark", "status-progress-fg", "status-progress-bg", 4.5, D["status-progress-fg"], D["status-progress-bg"]);
881
884
  check("light", "alert-info-fg", "alert-info-bg", 4.5, L["alert-info-fg"], L["alert-info-bg"]);
@@ -713,6 +713,7 @@ function createTheme(opts) {
713
713
  L["accent-violet"] = L["fg-accent"];
714
714
  L["focus-ring"] = fit(b[700], lightGrounds.concat([b[50]]), 3, -1);
715
715
  L["bg-selected"] = b[50];
716
+ L["calendar-range-bg"] = b[100];
716
717
  L["status-progress-bg"] = b[100];
717
718
  L["status-progress-fg"] = fit(b[800], [b[100]], 4.5, -1);
718
719
  L["alert-info-bg"] = b[50];
@@ -752,6 +753,7 @@ function createTheme(opts) {
752
753
  D["control-checked-bg"] = fit(b[300], darkGrounds, 3, 1);
753
754
  D["control-checked-fg"] = onFill(D["control-checked-bg"]);
754
755
  D["nav-active-edge"] = D["control-checked-bg"];
756
+ D["calendar-range-bg"] = D["bg-selected"];
755
757
  const shadowLight = INK, shadowDark = D["accent-violet"];
756
758
  if (o.primary === "brand") {
757
759
  L["button-primary-bg"] = fit(b[600], [ZINC[0]], 4.5, -1);
@@ -809,6 +811,7 @@ function createTheme(opts) {
809
811
  check("dark", "chart-1", "chart-2 (lightness step)", 1.8, D["chart-1"], CHART_2);
810
812
  check("light", "fg-primary", "bg-selected", 4.5, INK, L["bg-selected"]);
811
813
  check("dark", "fg-primary", "bg-selected", 4.5, "#ffffff", D["bg-selected"]);
814
+ check("light", "fg-primary", "calendar-range-bg", 4.5, INK, L["calendar-range-bg"]);
812
815
  check("light", "status-progress-fg", "status-progress-bg", 4.5, L["status-progress-fg"], L["status-progress-bg"]);
813
816
  check("dark", "status-progress-fg", "status-progress-bg", 4.5, D["status-progress-fg"], D["status-progress-bg"]);
814
817
  check("light", "alert-info-fg", "alert-info-bg", 4.5, L["alert-info-fg"], L["alert-info-bg"]);
package/dist/styles.css CHANGED
@@ -50,18 +50,28 @@ button.aura-btn--secondary[aria-disabled='true']:hover, button.aura-btn--ghost[a
50
50
  button.aura-btn--danger-secondary[aria-disabled='true']:hover { background: transparent; }
51
51
  button.aura-btn--danger[aria-disabled='true']:hover { background: var(--aura-button-danger-bg); }
52
52
  button.aura-btn--creative[aria-disabled='true']:hover { box-shadow: var(--aura-shadow-creative); transform: none; }
53
- /* 5.30: a disabled filled button (primary, danger, creative) is a quiet grey fill with muted text, not a faded copy of
54
- * itself — faded ink read as a separate grey button, faded red as pink. Outline and ghost buttons keep the fade. */
55
- .aura-btn:is(.aura-btn--primary, .aura-btn--danger, .aura-btn--creative):is(:disabled, [aria-disabled='true']:not(.is-loading)),
56
- .aura-btn:is(.aura-btn--primary, .aura-btn--danger, .aura-btn--creative):is(:disabled, [aria-disabled='true']:not(.is-loading)):hover {
57
- opacity: 1; background: var(--aura-button-disabled-bg); color: var(--aura-button-disabled-fg); border-color: transparent; box-shadow: none;
58
- }
53
+ /* 5.30: a disabled button is not a faded copy of itself (faded ink read as a separate grey button, faded red as pink).
54
+ * 5.79: one look for every variant — see below. */
55
+ .aura-btn.aura-btn:is(:disabled, [aria-disabled='true']:not(.is-loading)),
56
+ .aura-btn.aura-btn:is(:disabled, [aria-disabled='true']:not(.is-loading)):hover {
57
+ opacity: 1; background: var(--aura-button-disabled-bg); color: var(--aura-button-disabled-fg); border-color: var(--aura-button-disabled-border); box-shadow: none;
58
+ }
59
+ /* …a variant without a border of its own (primary, danger) draws the same edge inside, so none is edgeless */
60
+ .aura-btn.aura-btn:is(.aura-btn--primary, .aura-btn--danger):is(:disabled, [aria-disabled='true']:not(.is-loading)),
61
+ .aura-btn.aura-btn:is(.aura-btn--primary, .aura-btn--danger):is(:disabled, [aria-disabled='true']:not(.is-loading)):hover {
62
+ box-shadow: inset 0 0 0 1px var(--aura-button-disabled-border);
63
+ }
64
+ /* 5.79 (visual audit 6): one disabled look for every variant — a faint fill, a faint edge, the tertiary label — so a
65
+ * disabled primary doesn't read as a To Do pill and a disabled outline button isn't a crisp white one in dark. A ghost
66
+ * button stays without fill or edge. */
67
+ .aura-btn.aura-btn.aura-btn--ghost:is(:disabled, [aria-disabled='true']:not(.is-loading)),
68
+ .aura-btn.aura-btn.aura-btn--ghost:is(:disabled, [aria-disabled='true']:not(.is-loading)):hover { background: transparent; border-color: transparent; box-shadow: none; }
59
69
  /* Forced colours drop the fills: filled buttons get a 1px edge (as outline buttons have), and a disabled or gated one
60
70
  * (still focusable) shows GrayText and keeps the fade, without changing size. */
61
71
  @media (forced-colors: active) {
62
72
  .aura-btn:is(.aura-btn--primary, .aura-btn--danger) { border: 1px solid ButtonText; }
63
- .aura-btn.aura-btn:is(:disabled, [aria-disabled='true']:not(.is-loading)),
64
- .aura-btn.aura-btn.aura-btn:is(:disabled, [aria-disabled='true']:not(.is-loading)):hover { color: GrayText; border-color: GrayText; opacity: var(--aura-disabled-opacity); }
73
+ .aura-btn.aura-btn.aura-btn:is(:disabled, [aria-disabled='true']:not(.is-loading)),
74
+ .aura-btn.aura-btn.aura-btn:is(:disabled, [aria-disabled='true']:not(.is-loading)):hover { color: GrayText; border-color: GrayText; opacity: var(--aura-disabled-opacity); } /* 5.79: one class more, over the ghost rule */
65
75
  }
66
76
  .aura-btn.is-loading:active { transform: none; }
67
77
 
@@ -295,7 +305,7 @@ a.aura-table__sort { text-decoration: none; }
295
305
  position: fixed; z-index: var(--aura-z-menu); min-width: 200px; padding: var(--aura-space-1); overflow-y: auto; overscroll-behavior: contain;
296
306
  background: var(--aura-bg-surface); color: var(--aura-fg-primary);
297
307
  border: 1px solid var(--aura-border-default); border-radius: var(--aura-radius-md);
298
- box-shadow: var(--aura-shadow-enterprise);
308
+ box-shadow: var(--aura-shadow-overlay); /* 5.79 (visual audit 18): lifted as Popover and the list boxes are; its light edge alone was faint */
299
309
  font-family: var(--font-sans); font-size: max(13px, .8125rem);
300
310
  }
301
311
  .aura-menu__item {
@@ -583,7 +593,7 @@ a.aura-icon-btn { text-decoration: none; }
583
593
  * Field). */
584
594
  .aura-field:not(.is-disabled) .aura-input:has(> .aura-input__control[aria-invalid="true"]:not(:disabled)),
585
595
  .aura-field:not(.is-disabled) .aura-textarea[aria-invalid="true"]:not(:disabled) { border-color: var(--aura-border-danger); }
586
- .aura-field.is-disabled .aura-input { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-border-default); }
596
+ .aura-field.is-disabled .aura-input { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-input-disabled-border); }
587
597
  .aura-field.is-disabled .aura-input__control { cursor: not-allowed; }
588
598
  /* A read-only text field. Only an input: `:read-only` also matches any element that isn't editable — a
589
599
  * Select's trigger <button> — which painted every custom Select with the disabled ground (5.16.1, Chamber-OS 109). */
@@ -593,13 +603,18 @@ a.aura-icon-btn { text-decoration: none; }
593
603
 
594
604
  .aura-textarea { display: block; padding: 10px var(--aura-space-3); min-height: 96px; line-height: 1.6; resize: vertical; outline: none; }
595
605
  .aura-textarea:focus { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: 1px; border-color: var(--aura-border-strong); }
596
- .aura-field.is-disabled .aura-textarea { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-border-default); }
606
+ .aura-field.is-disabled .aura-textarea { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-input-disabled-border); }
597
607
 
598
608
  .aura-select .aura-input__control { padding-right: calc(var(--aura-space-3) + var(--aura-icon-sm) + var(--aura-space-2)); cursor: pointer; }
599
609
  .aura-select__chevron { position: absolute; right: var(--aura-space-3); color: var(--aura-fg-secondary); pointer-events: none; }
600
610
  .aura-select select:invalid, .aura-select select:has(option[value=""]:checked) { color: var(--aura-fg-tertiary); }
601
611
  /* 5.3: the field is a button over an invisible <select> (forms, refs, events); the list is AURA's, like Combobox. */
602
612
  .aura-select option { color: var(--aura-fg-primary); background: var(--aura-bg-input); }
613
+ /* 5.79 (visual audit 15): a list box (multiple, or size > 1) shows its rows — as tall as they need, no chevron — instead
614
+ * of being squeezed into a one-line field with its third option cut in half. */
615
+ .aura-select .aura-input__control:is([multiple], [size]:not([size='0'], [size='1'])) { height: auto; padding: var(--aura-space-1) var(--aura-space-2); overflow-y: auto; cursor: default; }
616
+ .aura-select:has(> .aura-input__control:is([multiple], [size]:not([size='0'], [size='1']))) .aura-select__chevron { display: none; }
617
+ .aura-select .aura-input__control:is([multiple], [size]:not([size='0'], [size='1'])) option { padding: var(--aura-space-1) var(--aura-space-2); border-radius: var(--aura-radius-xs); }
603
618
  .aura-select--custom { position: relative; }
604
619
  .aura-select__native { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; opacity: 0; pointer-events: none; appearance: none; }
605
620
  .aura-select__trigger { display: flex; align-items: center; text-align: start; background: none; border: 0; color: inherit; font: inherit; width: 100%; min-width: 0; }
@@ -659,7 +674,7 @@ a.aura-icon-btn { text-decoration: none; }
659
674
  /* 5.19 (Chamber-OS 113): a read-only switch keeps its colours (a locked "on" reads as on at full contrast) and stays
660
675
  * focusable; only the pointer says it won't change. The row's icon (a lock) sits at its end, like a field's icon. */
661
676
  .aura-switch-row.is-readonly, .aura-switch-row.is-readonly .aura-switch, .aura-switch-row.is-readonly .aura-choice__label { cursor: default; }
662
- .aura-switch-row__icon { flex: none; margin-inline-start: auto; margin-top: 2px; width: var(--aura-icon-sm); height: var(--aura-icon-sm); color: var(--aura-fg-secondary); }
677
+ .aura-switch-row__icon { flex: none; margin-inline-start: calc(-1 * var(--aura-space-1)); margin-top: 2px; /* 5.79 (visual audit 17): beside the label it belongs to, 8px after it, not at the row's far end */ width: var(--aura-icon-sm); height: var(--aura-icon-sm); color: var(--aura-fg-secondary); }
663
678
 
664
679
  /* ---------- Alerts & toasts ---------- */
665
680
  .aura-alert {
@@ -1051,8 +1066,8 @@ a.aura-tab { text-decoration: none; }
1051
1066
  .aura-combo__option.is-selected { font-weight: 500; }
1052
1067
  .aura-combo__option.is-disabled { opacity: var(--aura-disabled-opacity); cursor: default; }
1053
1068
  .aura-combo__text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
1054
- .aura-combo__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1055
- .aura-combo__desc { font-size: max(12px, .75rem); line-height: 1.5; color: var(--aura-fg-secondary); }
1069
+ .aura-combo__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: max(20px, 1.25rem); }
1070
+ .aura-combo__desc { font-size: max(12px, .75rem); line-height: max(18px, 1.125rem); color: var(--aura-fg-secondary); } /* 5.79: fixed line heights, so a Thai row and an English row are spaced alike (normal follows each font) */
1056
1071
  /* Same specificity as `.aura-combo__option > .aura-icon` above and later in the file, so it wins by order (4.18). */
1057
1072
  .aura-combo__option > .aura-combo__check { color: var(--aura-fg-accent); }
1058
1073
  /* 5.16 (Chamber-OS 105): option groups under a heading. */
@@ -1095,9 +1110,9 @@ a.aura-tab { text-decoration: none; }
1095
1110
  .aura-cal__day.is-selected { background: var(--aura-control-checked-bg); color: var(--aura-control-checked-fg); box-shadow: none; }
1096
1111
  .aura-cal__day:disabled, .aura-cal__day[aria-disabled='true'] { opacity: var(--aura-disabled-opacity); cursor: not-allowed; background: transparent; text-decoration: line-through; }
1097
1112
  .aura-cal__day:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: 1px; }
1098
- .aura-cal__grid td.is-in-range { background: var(--aura-bg-selected); }
1099
- .aura-cal__grid td.is-range-start { background: linear-gradient(to right, transparent 50%, var(--aura-bg-selected) 50%); }
1100
- .aura-cal__grid td.is-range-end { background: linear-gradient(to left, transparent 50%, var(--aura-bg-selected) 50%); }
1113
+ .aura-cal__grid td.is-in-range { background: var(--aura-calendar-range-bg); } /* 5.79 (visual audit 19) */
1114
+ .aura-cal__grid td.is-range-start { background: linear-gradient(to right, transparent 50%, var(--aura-calendar-range-bg) 50%); }
1115
+ .aura-cal__grid td.is-range-end { background: linear-gradient(to left, transparent 50%, var(--aura-calendar-range-bg) 50%); }
1101
1116
  .aura-cal__grid td.is-range-start.is-range-end { background: none; }
1102
1117
  .aura-cal__years { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--aura-space-1); }
1103
1118
  .aura-cal__year {
@@ -1652,7 +1667,7 @@ a.aura-upload__name:focus-visible { outline: var(--aura-focus-ring-width) solid
1652
1667
  .aura-badge--success { background: var(--aura-alert-success-bg); color: var(--aura-alert-success-fg); }
1653
1668
  .aura-badge--warning { background: var(--aura-alert-warning-bg); color: var(--aura-alert-warning-fg); }
1654
1669
  .aura-badge--danger { background: var(--aura-alert-danger-bg); color: var(--aura-alert-danger-fg); }
1655
- .aura-badge.is-outline { background: transparent; border-color: currentColor; }
1670
+ .aura-badge.is-outline { background: transparent; border-color: currentColor; border-color: color-mix(in srgb, currentColor 45%, transparent); } /* 5.79 (visual audit 19): a quieter edge, so an outline badge doesn't read as a secondary button */
1656
1671
  .aura-badge--neutral.is-outline { border-color: var(--aura-border-control); }
1657
1672
  .aura-badge.is-solid { background: var(--aura-button-primary-bg); color: var(--aura-button-primary-fg); }
1658
1673
  .aura-badge--danger.is-solid { background: var(--aura-status-blocked-solid-bg); color: var(--aura-status-blocked-solid-fg); }
@@ -1791,6 +1806,7 @@ a.aura-upload__name:focus-visible { outline: var(--aura-focus-ring-width) solid
1791
1806
  .aura-combo__chips { display: contents; }
1792
1807
  .aura-combo__chip {
1793
1808
  display: inline-flex; align-items: center; gap: 2px; max-width: 100%; height: 28px; box-sizing: border-box;
1809
+ flex: none; /* 5.79 (visual audit 16): a chip keeps its label and goes to the next line, not shrunk to "V.." */
1794
1810
  padding: 0 2px 0 var(--aura-space-2); border-radius: var(--aura-radius-full);
1795
1811
  background: var(--aura-bg-selected); color: var(--aura-fg-primary); font-size: max(13px, .8125rem); font-weight: 500;
1796
1812
  }
@@ -2137,6 +2153,11 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
2137
2153
  .aura-filterbar--fill .aura-filterbar__controls > .aura-filterselect .aura-filterselect__chevron { margin-inline-start: auto; }
2138
2154
  /* Compact filters sit closer together than full fields. */
2139
2155
  .aura-filterbar__controls:has(> .aura-filterselect) { gap: var(--aura-space-2); }
2156
+ /* 5.80: on a phone 6px, so three filters and a Tag keep one 375px row with either font source — the self-hosted
2157
+ * Inter (aura-fonts.local.css) measured 2.3px over at 8px. */
2158
+ @media (max-width: 639.98px) {
2159
+ .aura-filterbar__controls:has(> .aura-filterselect) { gap: 6px; }
2160
+ }
2140
2161
  /* 5.42 (Chamber-OS 146): collapseBelow — below the breakpoint the controls fold into a "Filters" sheet; the button
2141
2162
  * shares a row with the search, at its end, as tall as the search (the count and actions wrap below). From the
2142
2163
  * breakpoint up the search and the hidden button sit in a display: contents box, so the bar lays out as before. */
@@ -2193,7 +2214,7 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
2193
2214
  .aura-filterselect__face:focus-visible { outline-style: solid; }
2194
2215
  /* Before hydration: the real <select>, invisible, over the face (16px so iOS doesn't zoom when it opens). */
2195
2216
  .aura-filterselect__native { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; opacity: 0; font-size: max(16px, 1rem); cursor: pointer; appearance: none; }
2196
- .aura-filterselect.is-disabled .aura-filterselect__face { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-border-default); cursor: not-allowed; }
2217
+ .aura-filterselect.is-disabled .aura-filterselect__face { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-input-disabled-border); cursor: not-allowed; }
2197
2218
  .aura-filterselect.is-disabled .aura-filterselect__native { cursor: not-allowed; }
2198
2219
  /* 5.38 (Chamber-OS 140): touchHeight on FilterBar (its search and field-height controls) or on one FilterSelect —
2199
2220
  * 44px below 640px or on a coarse pointer (is-touch), or always. */
@@ -2393,6 +2414,25 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
2393
2414
  :root:has(.aura-actionbar--edge:not(.is-idle)) .aura-shell__content { padding-bottom: calc(var(--aura-space-6) + var(--aura-actionbar-space)); }
2394
2415
  :root:has(.aura-actionbar--edge:not(.is-idle)):not(:has(.aura-shell__content)) body { padding-bottom: var(--aura-actionbar-space); }
2395
2416
  }
2417
+ /* 5.79 (5.78 carry-over): where actionsFullWidthBelow reaches wider than actionsBarBelow, the bar isn't pinned between
2418
+ * the two breakpoints and its buttons sit in the same full-width row — every button there is radius-md, not some. */
2419
+ @media (min-width: 640px) and (max-width: 767.98px) {
2420
+ .aura-page-header--actions-full-below-md.aura-page-header--actions-bar-below-sm > .aura-page-header__actions .aura-btn:not(.aura-icon-btn) { border-radius: var(--aura-radius-md); }
2421
+ }
2422
+ @media (min-width: 640px) and (max-width: 1023.98px) {
2423
+ .aura-page-header--actions-full-below-lg.aura-page-header--actions-bar-below-sm > .aura-page-header__actions .aura-btn:not(.aura-icon-btn) { border-radius: var(--aura-radius-md); }
2424
+ }
2425
+ @media (min-width: 768px) and (max-width: 1023.98px) {
2426
+ .aura-page-header--actions-full-below-lg.aura-page-header--actions-bar-below-md > .aura-page-header__actions .aura-btn:not(.aura-icon-btn) { border-radius: var(--aura-radius-md); }
2427
+ }
2428
+ /* 5.79 (visual audit 10): in an AppShell on a desktop the edge strip starts where the content does — after the
2429
+ * sidebar (its rail width when collapsed; the right edge in RTL) — and its contents line up with the content's 32px
2430
+ * padding, instead of running under the sidebar. Outside an AppShell it keeps the screen's full width. */
2431
+ @media (min-width: 1024px) and (min-height: 30.00125rem) {
2432
+ .aura-shell:has(> .aura-shell__nav) .aura-actionbar--edge:not(.is-idle) { inset-inline-start: var(--aura-sidenav-width); }
2433
+ .aura-shell:has(> .aura-shell__nav .aura-nav--collapsed) .aura-actionbar--edge:not(.is-idle) { inset-inline-start: var(--aura-sidenav-rail-width); }
2434
+ .aura-shell:not(:has(.aura-shell__content.is-flush)) .aura-actionbar--edge:not(.is-idle) > .aura-actionbar__inner { padding-inline: var(--aura-space-8); } /* not with contentPadding={false} */
2435
+ }
2396
2436
  /* 5.78 (visual audit 9): one rule for the bottom strips on a phone, as PageHeader's (5.48) — the status on its own row
2397
2437
  * at the top, the actions sharing the row under it (an IconButton keeps its square; labels too long to share take a row
2398
2438
  * each), 16px both sides as the page. edgeBelow="md" follows it up to 768px, as PageHeader's md strip. */
@@ -2753,6 +2793,10 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
2753
2793
  * 1.6 line, so a loading card (its bars follow 1lh) is as tall as one with Thai text. */
2754
2794
  .aura-field__label:lang(th), .aura-field__hint:lang(th), .aura-field__error:lang(th), .aura-field__addon:lang(th) { line-height: 1.6; }
2755
2795
  .aura-choice__desc:lang(th), .aura-check__desc:lang(th) { font-size: max(13px, .8125rem); line-height: 1.6; }
2796
+ /* 5.79 (visual audit, Thai): the calendar's weekday row in Thai (อา จ อ …) at 13px — 12px was the hardest text on the
2797
+ * page to read; the grid carries lang="th" with a Thai locale. Two-line Thai nav labels get room between lines. */
2798
+ .aura-cal__grid:lang(th) th { font-size: max(13px, .8125rem); }
2799
+ .aura-nav__label:lang(th) { line-height: 1.5; }
2756
2800
  .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: max(12px, .75rem); line-height: max(18px, 1.125rem); }
2757
2801
  .aura-table--stacked .aura-table__td[data-label]:lang(th) { line-height: 1.6; }
2758
2802
 
@@ -52,18 +52,28 @@ button.aura-btn--secondary[aria-disabled='true']:hover, button.aura-btn--ghost[a
52
52
  button.aura-btn--danger-secondary[aria-disabled='true']:hover { background: transparent; }
53
53
  button.aura-btn--danger[aria-disabled='true']:hover { background: var(--aura-button-danger-bg); }
54
54
  button.aura-btn--creative[aria-disabled='true']:hover { box-shadow: var(--aura-shadow-creative); transform: none; }
55
- /* 5.30: a disabled filled button (primary, danger, creative) is a quiet grey fill with muted text, not a faded copy of
56
- * itself — faded ink read as a separate grey button, faded red as pink. Outline and ghost buttons keep the fade. */
57
- .aura-btn:is(.aura-btn--primary, .aura-btn--danger, .aura-btn--creative):is(:disabled, [aria-disabled='true']:not(.is-loading)),
58
- .aura-btn:is(.aura-btn--primary, .aura-btn--danger, .aura-btn--creative):is(:disabled, [aria-disabled='true']:not(.is-loading)):hover {
59
- opacity: 1; background: var(--aura-button-disabled-bg); color: var(--aura-button-disabled-fg); border-color: transparent; box-shadow: none;
60
- }
55
+ /* 5.30: a disabled button is not a faded copy of itself (faded ink read as a separate grey button, faded red as pink).
56
+ * 5.79: one look for every variant — see below. */
57
+ .aura-btn.aura-btn:is(:disabled, [aria-disabled='true']:not(.is-loading)),
58
+ .aura-btn.aura-btn:is(:disabled, [aria-disabled='true']:not(.is-loading)):hover {
59
+ opacity: 1; background: var(--aura-button-disabled-bg); color: var(--aura-button-disabled-fg); border-color: var(--aura-button-disabled-border); box-shadow: none;
60
+ }
61
+ /* …a variant without a border of its own (primary, danger) draws the same edge inside, so none is edgeless */
62
+ .aura-btn.aura-btn:is(.aura-btn--primary, .aura-btn--danger):is(:disabled, [aria-disabled='true']:not(.is-loading)),
63
+ .aura-btn.aura-btn:is(.aura-btn--primary, .aura-btn--danger):is(:disabled, [aria-disabled='true']:not(.is-loading)):hover {
64
+ box-shadow: inset 0 0 0 1px var(--aura-button-disabled-border);
65
+ }
66
+ /* 5.79 (visual audit 6): one disabled look for every variant — a faint fill, a faint edge, the tertiary label — so a
67
+ * disabled primary doesn't read as a To Do pill and a disabled outline button isn't a crisp white one in dark. A ghost
68
+ * button stays without fill or edge. */
69
+ .aura-btn.aura-btn.aura-btn--ghost:is(:disabled, [aria-disabled='true']:not(.is-loading)),
70
+ .aura-btn.aura-btn.aura-btn--ghost:is(:disabled, [aria-disabled='true']:not(.is-loading)):hover { background: transparent; border-color: transparent; box-shadow: none; }
61
71
  /* Forced colours drop the fills: filled buttons get a 1px edge (as outline buttons have), and a disabled or gated one
62
72
  * (still focusable) shows GrayText and keeps the fade, without changing size. */
63
73
  @media (forced-colors: active) {
64
74
  .aura-btn:is(.aura-btn--primary, .aura-btn--danger) { border: 1px solid ButtonText; }
65
- .aura-btn.aura-btn:is(:disabled, [aria-disabled='true']:not(.is-loading)),
66
- .aura-btn.aura-btn.aura-btn:is(:disabled, [aria-disabled='true']:not(.is-loading)):hover { color: GrayText; border-color: GrayText; opacity: var(--aura-disabled-opacity); }
75
+ .aura-btn.aura-btn.aura-btn:is(:disabled, [aria-disabled='true']:not(.is-loading)),
76
+ .aura-btn.aura-btn.aura-btn:is(:disabled, [aria-disabled='true']:not(.is-loading)):hover { color: GrayText; border-color: GrayText; opacity: var(--aura-disabled-opacity); } /* 5.79: one class more, over the ghost rule */
67
77
  }
68
78
  .aura-btn.is-loading:active { transform: none; }
69
79
 
@@ -297,7 +307,7 @@ a.aura-table__sort { text-decoration: none; }
297
307
  position: fixed; z-index: var(--aura-z-menu); min-width: 200px; padding: var(--aura-space-1); overflow-y: auto; overscroll-behavior: contain;
298
308
  background: var(--aura-bg-surface); color: var(--aura-fg-primary);
299
309
  border: 1px solid var(--aura-border-default); border-radius: var(--aura-radius-md);
300
- box-shadow: var(--aura-shadow-enterprise);
310
+ box-shadow: var(--aura-shadow-overlay); /* 5.79 (visual audit 18): lifted as Popover and the list boxes are; its light edge alone was faint */
301
311
  font-family: var(--font-sans); font-size: max(13px, .8125rem);
302
312
  }
303
313
  .aura-menu__item {
@@ -585,7 +595,7 @@ a.aura-icon-btn { text-decoration: none; }
585
595
  * Field). */
586
596
  .aura-field:not(.is-disabled) .aura-input:has(> .aura-input__control[aria-invalid="true"]:not(:disabled)),
587
597
  .aura-field:not(.is-disabled) .aura-textarea[aria-invalid="true"]:not(:disabled) { border-color: var(--aura-border-danger); }
588
- .aura-field.is-disabled .aura-input { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-border-default); }
598
+ .aura-field.is-disabled .aura-input { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-input-disabled-border); }
589
599
  .aura-field.is-disabled .aura-input__control { cursor: not-allowed; }
590
600
  /* A read-only text field. Only an input: `:read-only` also matches any element that isn't editable — a
591
601
  * Select's trigger <button> — which painted every custom Select with the disabled ground (5.16.1, Chamber-OS 109). */
@@ -595,13 +605,18 @@ a.aura-icon-btn { text-decoration: none; }
595
605
 
596
606
  .aura-textarea { display: block; padding: 10px var(--aura-space-3); min-height: 96px; line-height: 1.6; resize: vertical; outline: none; }
597
607
  .aura-textarea:focus { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: 1px; border-color: var(--aura-border-strong); }
598
- .aura-field.is-disabled .aura-textarea { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-border-default); }
608
+ .aura-field.is-disabled .aura-textarea { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-input-disabled-border); }
599
609
 
600
610
  .aura-select .aura-input__control { padding-right: calc(var(--aura-space-3) + var(--aura-icon-sm) + var(--aura-space-2)); cursor: pointer; }
601
611
  .aura-select__chevron { position: absolute; right: var(--aura-space-3); color: var(--aura-fg-secondary); pointer-events: none; }
602
612
  .aura-select select:invalid, .aura-select select:has(option[value=""]:checked) { color: var(--aura-fg-tertiary); }
603
613
  /* 5.3: the field is a button over an invisible <select> (forms, refs, events); the list is AURA's, like Combobox. */
604
614
  .aura-select option { color: var(--aura-fg-primary); background: var(--aura-bg-input); }
615
+ /* 5.79 (visual audit 15): a list box (multiple, or size > 1) shows its rows — as tall as they need, no chevron — instead
616
+ * of being squeezed into a one-line field with its third option cut in half. */
617
+ .aura-select .aura-input__control:is([multiple], [size]:not([size='0'], [size='1'])) { height: auto; padding: var(--aura-space-1) var(--aura-space-2); overflow-y: auto; cursor: default; }
618
+ .aura-select:has(> .aura-input__control:is([multiple], [size]:not([size='0'], [size='1']))) .aura-select__chevron { display: none; }
619
+ .aura-select .aura-input__control:is([multiple], [size]:not([size='0'], [size='1'])) option { padding: var(--aura-space-1) var(--aura-space-2); border-radius: var(--aura-radius-xs); }
605
620
  .aura-select--custom { position: relative; }
606
621
  .aura-select__native { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; opacity: 0; pointer-events: none; appearance: none; }
607
622
  .aura-select__trigger { display: flex; align-items: center; text-align: start; background: none; border: 0; color: inherit; font: inherit; width: 100%; min-width: 0; }
@@ -661,7 +676,7 @@ a.aura-icon-btn { text-decoration: none; }
661
676
  /* 5.19 (Chamber-OS 113): a read-only switch keeps its colours (a locked "on" reads as on at full contrast) and stays
662
677
  * focusable; only the pointer says it won't change. The row's icon (a lock) sits at its end, like a field's icon. */
663
678
  .aura-switch-row.is-readonly, .aura-switch-row.is-readonly .aura-switch, .aura-switch-row.is-readonly .aura-choice__label { cursor: default; }
664
- .aura-switch-row__icon { flex: none; margin-inline-start: auto; margin-top: 2px; width: var(--aura-icon-sm); height: var(--aura-icon-sm); color: var(--aura-fg-secondary); }
679
+ .aura-switch-row__icon { flex: none; margin-inline-start: calc(-1 * var(--aura-space-1)); margin-top: 2px; /* 5.79 (visual audit 17): beside the label it belongs to, 8px after it, not at the row's far end */ width: var(--aura-icon-sm); height: var(--aura-icon-sm); color: var(--aura-fg-secondary); }
665
680
 
666
681
  /* ---------- Alerts & toasts ---------- */
667
682
  .aura-alert {
@@ -1053,8 +1068,8 @@ a.aura-tab { text-decoration: none; }
1053
1068
  .aura-combo__option.is-selected { font-weight: 500; }
1054
1069
  .aura-combo__option.is-disabled { opacity: var(--aura-disabled-opacity); cursor: default; }
1055
1070
  .aura-combo__text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
1056
- .aura-combo__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1057
- .aura-combo__desc { font-size: max(12px, .75rem); line-height: 1.5; color: var(--aura-fg-secondary); }
1071
+ .aura-combo__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: max(20px, 1.25rem); }
1072
+ .aura-combo__desc { font-size: max(12px, .75rem); line-height: max(18px, 1.125rem); color: var(--aura-fg-secondary); } /* 5.79: fixed line heights, so a Thai row and an English row are spaced alike (normal follows each font) */
1058
1073
  /* Same specificity as `.aura-combo__option > .aura-icon` above and later in the file, so it wins by order (4.18). */
1059
1074
  .aura-combo__option > .aura-combo__check { color: var(--aura-fg-accent); }
1060
1075
  /* 5.16 (Chamber-OS 105): option groups under a heading. */
@@ -1097,9 +1112,9 @@ a.aura-tab { text-decoration: none; }
1097
1112
  .aura-cal__day.is-selected { background: var(--aura-control-checked-bg); color: var(--aura-control-checked-fg); box-shadow: none; }
1098
1113
  .aura-cal__day:disabled, .aura-cal__day[aria-disabled='true'] { opacity: var(--aura-disabled-opacity); cursor: not-allowed; background: transparent; text-decoration: line-through; }
1099
1114
  .aura-cal__day:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: 1px; }
1100
- .aura-cal__grid td.is-in-range { background: var(--aura-bg-selected); }
1101
- .aura-cal__grid td.is-range-start { background: linear-gradient(to right, transparent 50%, var(--aura-bg-selected) 50%); }
1102
- .aura-cal__grid td.is-range-end { background: linear-gradient(to left, transparent 50%, var(--aura-bg-selected) 50%); }
1115
+ .aura-cal__grid td.is-in-range { background: var(--aura-calendar-range-bg); } /* 5.79 (visual audit 19) */
1116
+ .aura-cal__grid td.is-range-start { background: linear-gradient(to right, transparent 50%, var(--aura-calendar-range-bg) 50%); }
1117
+ .aura-cal__grid td.is-range-end { background: linear-gradient(to left, transparent 50%, var(--aura-calendar-range-bg) 50%); }
1103
1118
  .aura-cal__grid td.is-range-start.is-range-end { background: none; }
1104
1119
  .aura-cal__years { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--aura-space-1); }
1105
1120
  .aura-cal__year {
@@ -1654,7 +1669,7 @@ a.aura-upload__name:focus-visible { outline: var(--aura-focus-ring-width) solid
1654
1669
  .aura-badge--success { background: var(--aura-alert-success-bg); color: var(--aura-alert-success-fg); }
1655
1670
  .aura-badge--warning { background: var(--aura-alert-warning-bg); color: var(--aura-alert-warning-fg); }
1656
1671
  .aura-badge--danger { background: var(--aura-alert-danger-bg); color: var(--aura-alert-danger-fg); }
1657
- .aura-badge.is-outline { background: transparent; border-color: currentColor; }
1672
+ .aura-badge.is-outline { background: transparent; border-color: currentColor; border-color: color-mix(in srgb, currentColor 45%, transparent); } /* 5.79 (visual audit 19): a quieter edge, so an outline badge doesn't read as a secondary button */
1658
1673
  .aura-badge--neutral.is-outline { border-color: var(--aura-border-control); }
1659
1674
  .aura-badge.is-solid { background: var(--aura-button-primary-bg); color: var(--aura-button-primary-fg); }
1660
1675
  .aura-badge--danger.is-solid { background: var(--aura-status-blocked-solid-bg); color: var(--aura-status-blocked-solid-fg); }
@@ -1793,6 +1808,7 @@ a.aura-upload__name:focus-visible { outline: var(--aura-focus-ring-width) solid
1793
1808
  .aura-combo__chips { display: contents; }
1794
1809
  .aura-combo__chip {
1795
1810
  display: inline-flex; align-items: center; gap: 2px; max-width: 100%; height: 28px; box-sizing: border-box;
1811
+ flex: none; /* 5.79 (visual audit 16): a chip keeps its label and goes to the next line, not shrunk to "V.." */
1796
1812
  padding: 0 2px 0 var(--aura-space-2); border-radius: var(--aura-radius-full);
1797
1813
  background: var(--aura-bg-selected); color: var(--aura-fg-primary); font-size: max(13px, .8125rem); font-weight: 500;
1798
1814
  }
@@ -2139,6 +2155,11 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
2139
2155
  .aura-filterbar--fill .aura-filterbar__controls > .aura-filterselect .aura-filterselect__chevron { margin-inline-start: auto; }
2140
2156
  /* Compact filters sit closer together than full fields. */
2141
2157
  .aura-filterbar__controls:has(> .aura-filterselect) { gap: var(--aura-space-2); }
2158
+ /* 5.80: on a phone 6px, so three filters and a Tag keep one 375px row with either font source — the self-hosted
2159
+ * Inter (aura-fonts.local.css) measured 2.3px over at 8px. */
2160
+ @media (max-width: 639.98px) {
2161
+ .aura-filterbar__controls:has(> .aura-filterselect) { gap: 6px; }
2162
+ }
2142
2163
  /* 5.42 (Chamber-OS 146): collapseBelow — below the breakpoint the controls fold into a "Filters" sheet; the button
2143
2164
  * shares a row with the search, at its end, as tall as the search (the count and actions wrap below). From the
2144
2165
  * breakpoint up the search and the hidden button sit in a display: contents box, so the bar lays out as before. */
@@ -2195,7 +2216,7 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
2195
2216
  .aura-filterselect__face:focus-visible { outline-style: solid; }
2196
2217
  /* Before hydration: the real <select>, invisible, over the face (16px so iOS doesn't zoom when it opens). */
2197
2218
  .aura-filterselect__native { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; opacity: 0; font-size: max(16px, 1rem); cursor: pointer; appearance: none; }
2198
- .aura-filterselect.is-disabled .aura-filterselect__face { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-border-default); cursor: not-allowed; }
2219
+ .aura-filterselect.is-disabled .aura-filterselect__face { background: var(--aura-bg-input-disabled); color: var(--aura-fg-secondary); border-color: var(--aura-input-disabled-border); cursor: not-allowed; }
2199
2220
  .aura-filterselect.is-disabled .aura-filterselect__native { cursor: not-allowed; }
2200
2221
  /* 5.38 (Chamber-OS 140): touchHeight on FilterBar (its search and field-height controls) or on one FilterSelect —
2201
2222
  * 44px below 640px or on a coarse pointer (is-touch), or always. */
@@ -2395,6 +2416,25 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
2395
2416
  :root:has(.aura-actionbar--edge:not(.is-idle)) .aura-shell__content { padding-bottom: calc(var(--aura-space-6) + var(--aura-actionbar-space)); }
2396
2417
  :root:has(.aura-actionbar--edge:not(.is-idle)):not(:has(.aura-shell__content)) body { padding-bottom: var(--aura-actionbar-space); }
2397
2418
  }
2419
+ /* 5.79 (5.78 carry-over): where actionsFullWidthBelow reaches wider than actionsBarBelow, the bar isn't pinned between
2420
+ * the two breakpoints and its buttons sit in the same full-width row — every button there is radius-md, not some. */
2421
+ @media (min-width: 640px) and (max-width: 767.98px) {
2422
+ .aura-page-header--actions-full-below-md.aura-page-header--actions-bar-below-sm > .aura-page-header__actions .aura-btn:not(.aura-icon-btn) { border-radius: var(--aura-radius-md); }
2423
+ }
2424
+ @media (min-width: 640px) and (max-width: 1023.98px) {
2425
+ .aura-page-header--actions-full-below-lg.aura-page-header--actions-bar-below-sm > .aura-page-header__actions .aura-btn:not(.aura-icon-btn) { border-radius: var(--aura-radius-md); }
2426
+ }
2427
+ @media (min-width: 768px) and (max-width: 1023.98px) {
2428
+ .aura-page-header--actions-full-below-lg.aura-page-header--actions-bar-below-md > .aura-page-header__actions .aura-btn:not(.aura-icon-btn) { border-radius: var(--aura-radius-md); }
2429
+ }
2430
+ /* 5.79 (visual audit 10): in an AppShell on a desktop the edge strip starts where the content does — after the
2431
+ * sidebar (its rail width when collapsed; the right edge in RTL) — and its contents line up with the content's 32px
2432
+ * padding, instead of running under the sidebar. Outside an AppShell it keeps the screen's full width. */
2433
+ @media (min-width: 1024px) and (min-height: 30.00125rem) {
2434
+ .aura-shell:has(> .aura-shell__nav) .aura-actionbar--edge:not(.is-idle) { inset-inline-start: var(--aura-sidenav-width); }
2435
+ .aura-shell:has(> .aura-shell__nav .aura-nav--collapsed) .aura-actionbar--edge:not(.is-idle) { inset-inline-start: var(--aura-sidenav-rail-width); }
2436
+ .aura-shell:not(:has(.aura-shell__content.is-flush)) .aura-actionbar--edge:not(.is-idle) > .aura-actionbar__inner { padding-inline: var(--aura-space-8); } /* not with contentPadding={false} */
2437
+ }
2398
2438
  /* 5.78 (visual audit 9): one rule for the bottom strips on a phone, as PageHeader's (5.48) — the status on its own row
2399
2439
  * at the top, the actions sharing the row under it (an IconButton keeps its square; labels too long to share take a row
2400
2440
  * each), 16px both sides as the page. edgeBelow="md" follows it up to 768px, as PageHeader's md strip. */
@@ -2755,6 +2795,10 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
2755
2795
  * 1.6 line, so a loading card (its bars follow 1lh) is as tall as one with Thai text. */
2756
2796
  .aura-field__label:lang(th), .aura-field__hint:lang(th), .aura-field__error:lang(th), .aura-field__addon:lang(th) { line-height: 1.6; }
2757
2797
  .aura-choice__desc:lang(th), .aura-check__desc:lang(th) { font-size: max(13px, .8125rem); line-height: 1.6; }
2798
+ /* 5.79 (visual audit, Thai): the calendar's weekday row in Thai (อา จ อ …) at 13px — 12px was the hardest text on the
2799
+ * page to read; the grid carries lang="th" with a Thai locale. Two-line Thai nav labels get room between lines. */
2800
+ .aura-cal__grid:lang(th) th { font-size: max(13px, .8125rem); }
2801
+ .aura-nav__label:lang(th) { line-height: 1.5; }
2758
2802
  .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: max(12px, .75rem); line-height: max(18px, 1.125rem); }
2759
2803
  .aura-table--stacked .aura-table__td[data-label]:lang(th) { line-height: 1.6; }
2760
2804
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jirawatpyk/aura-react",
3
- "version": "5.78.0",
3
+ "version": "5.80.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",