@jirawatpyk/aura-react 4.16.0 → 4.18.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
@@ -80,12 +80,27 @@ Actions: Button (`ghost`, `size="sm"`), IconButton, Menu, DropdownMenu, Tag · F
80
80
 
81
81
  ## Router links, Swedish, motion
82
82
 
83
+ Next.js App Router: `next/link` is a function, and a Server Component (your layout) can't pass functions to a client component. Put the provider in a small `'use client'` file:
84
+
83
85
  ```tsx
86
+ // app/providers.tsx
87
+ 'use client';
84
88
  import Link from 'next/link';
85
- <AuraProvider locale="sv" linkComponent={Link}> {/* th | en | sv */}
89
+ import { AuraProvider } from '@jirawatpyk/aura-react';
90
+ export function Providers({ children, locale }: { children: React.ReactNode; locale: 'th' | 'en' | 'sv' }) {
91
+ return (
92
+ <AuraProvider locale={locale} linkComponent={Link}>
93
+ {children}
94
+ </AuraProvider>
95
+ );
96
+ {
97
+ /* th | en | sv */
98
+ }
99
+ }
100
+ // app/layout.tsx (a Server Component): <body><Providers locale="th">{children}</Providers></body>
86
101
  ```
87
102
 
88
- `linkComponent` is used by Button `href`, Breadcrumb, Pagination `getHref`, Stat `href`, SideNav and DataTable row/pager links. 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 and stays Thai unless you pass `locale`. Values are always Gregorian ISO dates.
103
+ `linkComponent` is used by Button `href`, Breadcrumb, 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()` from the package root has no provider to read and stays Thai unless you pass `locale` (until 5.0); the one in `@jirawatpyk/aura-react/server` defaults to English and Gregorian. Values are always Gregorian ISO dates.
89
104
 
90
105
  ## Compact density
91
106
 
@@ -123,9 +138,30 @@ toast.error('Could not save'); // danger = role="alert"; the rest
123
138
  <Command open={open} onOpenChange={setOpen} items={commands} /> {/* ⌘K / Ctrl+K toggles it */}
124
139
  ```
125
140
 
141
+ Server search in the palette (4.17): control the query and hand over what the server found. The active item is kept by id while results change, and `loading` announces "Searching…" and then the count.
142
+
143
+ ```tsx
144
+ <Command
145
+ open={open}
146
+ onOpenChange={setOpen}
147
+ query={q}
148
+ onQueryChange={setQ} // fetch results for q (debounced)
149
+ items={results}
150
+ filter={false}
151
+ loading={isFetching}
152
+ empty={
153
+ <Button size="sm" icon="plus">
154
+ Create member
155
+ </Button>
156
+ }
157
+ />
158
+ ```
159
+
160
+ DataTable with sort and page in the URL (4.17): `onStateChange={({ sort, page }) => router.push(…)}` reports a sort click once, as `{ sort, page: 1 }`, instead of `onSortChange` and then `onPageChange(1)`. That's one history entry and one server render.
161
+
126
162
  ## Phones and touch
127
163
 
128
- Under 640px every text control uses 16px text (iOS Safari doesn't zoom). On touch screens (`pointer: coarse`) IconButton and `<Button size="sm">` keep their 32px look with a 44px hit area, and Radio, Checkbox and Switch rows are at least 44px with the whole row as the target. `<Button fullWidth>` fills its row and wraps long Thai or Swedish labels.
164
+ Under 640px every text control uses 16px text (iOS Safari doesn't zoom). On touch screens (`pointer: coarse`) IconButton and `<Button size="sm">` keep their 32px look with a 44px hit area, and Radio, Checkbox and Switch rows are at least 44px with the whole row as the target. Since 4.17 so are menu items, page numbers, segments and calendar days (44px); Combobox and DatePicker toggles and the Tag remove button get 44px hit areas. A Dialog sheet on a phone is capped at `92dvh`, so the browser toolbar never hides its footer. `<Button fullWidth>` fills its row and wraps long Thai or Swedish labels.
129
165
 
130
166
  ## Light and dark
131
167
 
@@ -163,6 +199,33 @@ Portals (Dialog, Drawer, menus, pickers, toasts) wait until after hydration; `us
163
199
 
164
200
  Layout is right before hydration (4.16). AppShell's sidebar-or-drawer switch is CSS (a 1024px media query), so a phone gets the menu button from the server's HTML. A DataTable with `stackBelow` renders cards and grid together until it has measured itself, and a container query shows the right one. It supports `stackBelow` 360, 400, 480, 520, 560, 600, 640, 720, 768, 800, 900, 960 and 1024; other widths stack once JavaScript runs. `npm run test:layout` loads both at 390 and 1280px with JavaScript off, then hydrated, and requires CLS 0. Columns with `hideBelow` still settle after hydration.
165
201
 
202
+ ## Next to another Tailwind theme (shadcn), page by page
203
+
204
+ `styles.css` is unlayered, so it beats utilities. For a migration next to an existing theme, use the layered stylesheet and the prefixed Tailwind theme (4.17):
205
+
206
+ ```css
207
+ @layer aura-tokens, theme, base, aura, components, utilities;
208
+ @import 'tailwindcss';
209
+ @import '@jirawatpyk/aura-tokens/aura.css' layer(aura-tokens); /* your theme wins for --font-sans / --font-mono */
210
+ @import '@jirawatpyk/aura-tokens/tailwind.prefixed.css'; /* bg-aura-bg-surface, font-aura-sans … no @custom-variant */
211
+ @import '@jirawatpyk/aura-react/styles.layer.css'; /* components in @layer aura: a utility className wins */
212
+ ```
213
+
214
+ Nothing an existing page uses is redefined: `npm run check:tailwind4` compiles a shadcn-style page alone and next to this setup, with AURA imported first and last. It checks that the computed styles are identical, and that `rounded-none` overrides `.aura-btn` with no `!important`. While both run, AURA components use your `--font-sans` / `--font-mono`; set them to AURA's stacks in your token bridge when a page moves over.
215
+
216
+ ## Server Components
217
+
218
+ Everything in the package root is a client module (`'use client'`), so a Server Component can render AURA components but can't call AURA functions. The pure helpers have their own entry with no `'use client'` and no React (4.17):
219
+
220
+ ```tsx
221
+ // app/invoices/[id]/page.tsx — a Server Component
222
+ import { formatDate, createTheme, statusTone, STRINGS } from '@jirawatpyk/aura-react/server';
223
+ formatDate('2026-09-24'); // "24 Sept 2026" — English and Gregorian by default
224
+ formatDate('2026-09-24', { locale: 'th' }); // "24 ก.ย. 2569"
225
+ ```
226
+
227
+ It exports `formatDate`, `parseDate`, `toISO`, `fromISO`, `parseTime`, `formatBytes`, `statusTone`, `STRINGS`, `createTheme`, `contrast`, `brandScale`, `colorSchemeScript` and `breakpoints`. `npm run test:ssr` loads it under Node's `react-server` condition, and the Next.js starter formats a date with it in a Server Component.
228
+
166
229
  ## TypeScript
167
230
 
168
231
  Every optional prop takes `undefined` (4.16), so apps on `exactOptionalPropertyTypes` can write `hint={t.hint}` or `icon={x ?? undefined}`. `types-test/exact-optional.tsx` checks every exported component against the published declarations under `strict`, `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess`.
@@ -267,6 +267,8 @@ window.Aura = (() => {
267
267
 
268
268
  // src/locale.tsx
269
269
  var React3 = __toESM(require_react(), 1);
270
+
271
+ // src/strings.ts
270
272
  var n = function(x) {
271
273
  return Number(x).toLocaleString("en");
272
274
  };
@@ -614,6 +616,8 @@ window.Aura = (() => {
614
616
  }
615
617
  }
616
618
  };
619
+
620
+ // src/locale.tsx
617
621
  var LocaleContext = React3.createContext(null);
618
622
  function AuraProvider(props) {
619
623
  const outer = React3.useContext(LocaleContext);
@@ -946,12 +950,8 @@ window.Aura = (() => {
946
950
 
947
951
  // src/StatusPill.tsx
948
952
  var React9 = __toESM(require_react(), 1);
949
- var TONE_ICON = {
950
- neutral: "circle",
951
- progress: "circle-dot-dashed",
952
- ready: "circle-check",
953
- blocked: "ban"
954
- };
953
+
954
+ // src/status.ts
955
955
  var TONE_WORDS = {
956
956
  ready: ["ready", "done", "complete", "completed", "approved", "live", "passed"],
957
957
  progress: ["in progress", "in review", "review", "syncing", "running", "pending"],
@@ -963,6 +963,14 @@ window.Aura = (() => {
963
963
  return "neutral";
964
964
  }
965
965
  var TONE_ORDER = { neutral: 0, progress: 1, ready: 2, blocked: 3 };
966
+
967
+ // src/StatusPill.tsx
968
+ var TONE_ICON = {
969
+ neutral: "circle",
970
+ progress: "circle-dot-dashed",
971
+ ready: "circle-check",
972
+ blocked: "ban"
973
+ };
966
974
  var StatusPill = React9.forwardRef(function StatusPill2(props, ref) {
967
975
  const tone2 = props.tone || toneFor(props.children);
968
976
  return /* @__PURE__ */ React9.createElement("span", { ref, className: cx("aura-pill", "aura-pill--" + tone2, props.className) }, /* @__PURE__ */ React9.createElement(Icon, { name: TONE_ICON[tone2] || "circle", size: 12 }), props.children);
@@ -1544,6 +1552,8 @@ window.Aura = (() => {
1544
1552
  // src/DatePicker.tsx
1545
1553
  var React17 = __toESM(require_react(), 1);
1546
1554
  var import_react_dom3 = __toESM(require_react_dom(), 1);
1555
+
1556
+ // src/dates.ts
1547
1557
  var ERA = /^พ\.ศ\.\s?|\s?(BE|พ\.ศ\.)$/g;
1548
1558
  var pad = function(n2) {
1549
1559
  return (n2 < 10 ? "0" : "") + n2;
@@ -1649,22 +1659,6 @@ window.Aura = (() => {
1649
1659
  const loc = o.locale || "th";
1650
1660
  return fmt(localeTag(loc, o.calendar || defaultCalendar(loc)), f, d).replace(ERA, "");
1651
1661
  }
1652
- function useFormatDate() {
1653
- const ctx = useAuraLocale(), locale = ctx.locale || "en", calendar = ctx.calendar;
1654
- return React17.useCallback(
1655
- function(iso, opts) {
1656
- const o = opts || {}, loc = o.locale || locale;
1657
- return formatDate(
1658
- iso,
1659
- Object.assign({}, o, {
1660
- locale: loc,
1661
- calendar: o.calendar || (o.locale ? void 0 : calendar) || defaultCalendar(loc)
1662
- })
1663
- );
1664
- },
1665
- [locale, calendar]
1666
- );
1667
- }
1668
1662
  var MONTHS = null;
1669
1663
  function monthIndex(word) {
1670
1664
  if (!MONTHS) {
@@ -1702,6 +1696,24 @@ window.Aura = (() => {
1702
1696
  const dt = new Date(y, mo - 1, d);
1703
1697
  return dt.getMonth() === mo - 1 && dt.getDate() === d ? toISO(dt) : null;
1704
1698
  }
1699
+
1700
+ // src/DatePicker.tsx
1701
+ function useFormatDate() {
1702
+ const ctx = useAuraLocale(), locale = ctx.locale || "en", calendar = ctx.calendar;
1703
+ return React17.useCallback(
1704
+ function(iso, opts) {
1705
+ const o = opts || {}, loc = o.locale || locale;
1706
+ return formatDate(
1707
+ iso,
1708
+ Object.assign({}, o, {
1709
+ locale: loc,
1710
+ calendar: o.calendar || (o.locale ? void 0 : calendar) || defaultCalendar(loc)
1711
+ })
1712
+ );
1713
+ },
1714
+ [locale, calendar]
1715
+ );
1716
+ }
1705
1717
  var Calendar = React17.forwardRef(function Calendar2(props, ref) {
1706
1718
  const ctx = useAuraLocale(), locale = props.locale || ctx.locale || "en", calendar = props.calendar || ctx.calendar || defaultCalendar(locale), tag = localeTag(locale, calendar);
1707
1719
  const weekStart = props.weekStartsOn == null ? locale === "sv" ? 1 : 0 : props.weekStartsOn;
@@ -2749,7 +2761,12 @@ window.Aura = (() => {
2749
2761
  const id = uid(), listId = id + "-list";
2750
2762
  const box = React25.useRef(null);
2751
2763
  const input = React25.useRef(null);
2752
- const q = React25.useState(""), act = React25.useState(0);
2764
+ const qState = React25.useState(""), act = React25.useState(null);
2765
+ const query = props.query !== void 0 ? props.query : qState[0];
2766
+ function setQuery(v) {
2767
+ if (props.query === void 0) qState[1](v);
2768
+ if (props.onQueryChange) props.onQueryChange(v);
2769
+ }
2753
2770
  const openRef = React25.useRef(props.open);
2754
2771
  openRef.current = props.open;
2755
2772
  const close = function() {
@@ -2775,15 +2792,15 @@ window.Aura = (() => {
2775
2792
  React25.useEffect(
2776
2793
  function() {
2777
2794
  if (!props.open) {
2778
- q[1]("");
2779
- act[1](0);
2795
+ if (query) setQuery("");
2796
+ act[1](null);
2780
2797
  }
2781
2798
  },
2782
2799
  [props.open]
2783
2800
  );
2784
- const filter = props.filter || defaultFilter;
2801
+ const filter = props.filter === false ? null : props.filter || defaultFilter;
2785
2802
  const shown = (props.items || []).filter(function(it) {
2786
- return filter(it, q[0]);
2803
+ return !filter || filter(it, query);
2787
2804
  });
2788
2805
  const groups = [];
2789
2806
  shown.forEach(function(it) {
@@ -2805,7 +2822,10 @@ window.Aura = (() => {
2805
2822
  }).filter(function(i2) {
2806
2823
  return i2 >= 0;
2807
2824
  });
2808
- const active = enabled.indexOf(act[0]) >= 0 ? act[0] : enabled.length ? enabled[0] : -1;
2825
+ const byId = act[0] == null ? -1 : flat.findIndex(function(it) {
2826
+ return it.id === act[0];
2827
+ });
2828
+ const active = enabled.indexOf(byId) >= 0 ? byId : enabled.length ? enabled[0] : -1;
2809
2829
  const optId = function(i2) {
2810
2830
  return id + "-o" + i2;
2811
2831
  };
@@ -2830,7 +2850,7 @@ window.Aura = (() => {
2830
2850
  if (dir === "first") n2 = 0;
2831
2851
  else if (dir === "last") n2 = enabled.length - 1;
2832
2852
  else n2 = (at + dir + enabled.length) % enabled.length;
2833
- act[1](enabled[n2]);
2853
+ act[1](flat[enabled[n2]].id);
2834
2854
  }
2835
2855
  function onKey(e) {
2836
2856
  if (e.key === "ArrowDown") {
@@ -2881,63 +2901,77 @@ window.Aura = (() => {
2881
2901
  "data-autofocus": "",
2882
2902
  type: "text",
2883
2903
  role: "combobox",
2884
- "aria-expanded": true,
2885
- "aria-controls": listId,
2904
+ "aria-expanded": flat.length > 0,
2905
+ "aria-controls": flat.length ? listId : void 0,
2886
2906
  "aria-autocomplete": "list",
2887
2907
  "aria-activedescendant": active >= 0 ? optId(active) : void 0,
2888
2908
  "aria-label": props.label || t.commandMenu,
2889
2909
  placeholder: props.placeholder || t.commandPlaceholder,
2890
2910
  className: "aura-command__input",
2891
- value: q[0],
2911
+ value: query,
2892
2912
  autoComplete: "off",
2893
2913
  spellCheck: false,
2894
2914
  onChange: function(e) {
2895
- q[1](e.target.value);
2896
- act[1](0);
2915
+ setQuery(e.target.value);
2916
+ act[1](null);
2897
2917
  },
2898
2918
  onKeyDown: onKey
2899
2919
  }
2900
2920
  ), /* @__PURE__ */ React25.createElement("kbd", { className: "aura-command__kbd" }, "Esc")),
2901
- /* @__PURE__ */ React25.createElement("div", { className: "aura-command__list", id: listId, role: "listbox", "aria-label": props.label || t.commandMenu }, !flat.length ? /* @__PURE__ */ React25.createElement("p", { className: "aura-command__empty", role: "presentation" }, props.emptyText || t.noMatches) : groups.map(function(g, gi) {
2902
- const gid = id + "-g" + gi;
2903
- return /* @__PURE__ */ React25.createElement(
2904
- "div",
2905
- {
2906
- key: g.name || gi,
2907
- role: "group",
2908
- "aria-labelledby": g.name ? gid : void 0,
2909
- className: "aura-command__group"
2910
- },
2911
- g.name ? /* @__PURE__ */ React25.createElement("div", { className: "aura-command__heading", id: gid, role: "presentation" }, g.name) : null,
2912
- g.items.map(function(it) {
2913
- i++;
2914
- const n2 = i;
2915
- return /* @__PURE__ */ React25.createElement(
2916
- "div",
2917
- {
2918
- key: it.id,
2919
- id: optId(n2),
2920
- role: "option",
2921
- "aria-selected": n2 === active,
2922
- "aria-disabled": it.disabled || void 0,
2923
- className: cx("aura-command__item", n2 === active && "is-active", it.disabled && "is-disabled"),
2924
- onPointerDown: function(e) {
2925
- e.preventDefault();
2926
- },
2927
- onPointerMove: function() {
2928
- if (!it.disabled && act[0] !== n2) act[1](n2);
2921
+ /* @__PURE__ */ React25.createElement("div", { className: "aura-command__list", "aria-busy": props.loading || void 0 }, props.loading ? /* @__PURE__ */ React25.createElement("div", { className: "aura-command__loading" }, /* @__PURE__ */ React25.createElement(Icon, { name: "loader-circle", className: "aura-spin" }), t.searching) : null, !flat.length ? props.loading ? null : /* @__PURE__ */ React25.createElement("div", { className: "aura-command__empty" }, props.empty != null ? props.empty : props.emptyText || t.noMatches) : /* @__PURE__ */ React25.createElement(
2922
+ "div",
2923
+ {
2924
+ id: listId,
2925
+ role: "listbox",
2926
+ "aria-label": props.label || t.commandMenu,
2927
+ "aria-busy": props.loading || void 0
2928
+ },
2929
+ groups.map(function(g, gi) {
2930
+ const gid = id + "-g" + gi;
2931
+ return /* @__PURE__ */ React25.createElement(
2932
+ "div",
2933
+ {
2934
+ key: g.name || gi,
2935
+ role: "group",
2936
+ "aria-labelledby": g.name ? gid : void 0,
2937
+ className: "aura-command__group"
2938
+ },
2939
+ g.name ? /* @__PURE__ */ React25.createElement("div", { className: "aura-command__heading", id: gid, role: "presentation" }, g.name) : null,
2940
+ g.items.map(function(it) {
2941
+ i++;
2942
+ const n2 = i;
2943
+ return /* @__PURE__ */ React25.createElement(
2944
+ "div",
2945
+ {
2946
+ key: it.id,
2947
+ id: optId(n2),
2948
+ role: "option",
2949
+ "aria-selected": n2 === active,
2950
+ "aria-disabled": it.disabled || void 0,
2951
+ className: cx(
2952
+ "aura-command__item",
2953
+ n2 === active && "is-active",
2954
+ it.disabled && "is-disabled"
2955
+ ),
2956
+ onPointerDown: function(e) {
2957
+ e.preventDefault();
2958
+ },
2959
+ onPointerMove: function() {
2960
+ if (!it.disabled && act[0] !== it.id) act[1](it.id);
2961
+ },
2962
+ onClick: function() {
2963
+ run(it);
2964
+ }
2929
2965
  },
2930
- onClick: function() {
2931
- run(it);
2932
- }
2933
- },
2934
- it.icon ? /* @__PURE__ */ React25.createElement(Icon, { name: it.icon }) : null,
2935
- /* @__PURE__ */ React25.createElement("span", { className: "aura-command__text" }, /* @__PURE__ */ React25.createElement("span", { className: "aura-command__label" }, it.label), it.description ? /* @__PURE__ */ React25.createElement("span", { className: "aura-command__desc" }, it.description) : null),
2936
- it.shortcut ? /* @__PURE__ */ React25.createElement("kbd", { className: "aura-command__kbd" }, it.shortcut) : null
2937
- );
2938
- })
2939
- );
2940
- })),
2966
+ it.icon ? /* @__PURE__ */ React25.createElement(Icon, { name: it.icon }) : null,
2967
+ /* @__PURE__ */ React25.createElement("span", { className: "aura-command__text" }, /* @__PURE__ */ React25.createElement("span", { className: "aura-command__label" }, it.label), it.description ? /* @__PURE__ */ React25.createElement("span", { className: "aura-command__desc" }, it.description) : null),
2968
+ it.shortcut ? /* @__PURE__ */ React25.createElement("kbd", { className: "aura-command__kbd" }, it.shortcut) : null
2969
+ );
2970
+ })
2971
+ );
2972
+ })
2973
+ )),
2974
+ /* @__PURE__ */ React25.createElement("span", { className: "aura-sr-only", role: "status" }, props.loading ? t.searching : query && !flat.length ? t.noMatches : props.loading === false && query ? t.results(flat.length) : ""),
2941
2975
  /* @__PURE__ */ React25.createElement("div", { className: "aura-command__foot", "aria-hidden": true }, t.commandHint, /* @__PURE__ */ React25.createElement("span", { className: "aura-command__mod" }, isMac() ? "\u2318K" : "Ctrl K"))
2942
2976
  )
2943
2977
  ),
@@ -3127,11 +3161,20 @@ window.Aura = (() => {
3127
3161
  const selectable = !!props.selectable;
3128
3162
  const reorderable = props.reorderable !== false && !!props.columnControls;
3129
3163
  const controls = !!props.columnControls;
3130
- const sortState = useMaybeControlled(props.sort, props.defaultSort || null, props.onSortChange);
3164
+ const oneCallback = !!props.onStateChange;
3165
+ const sortState = useMaybeControlled(
3166
+ props.sort,
3167
+ props.defaultSort || null,
3168
+ oneCallback ? null : props.onSortChange
3169
+ );
3131
3170
  const sort = sortState[0], setSort = sortState[1];
3132
3171
  const selState = useMaybeControlled(props.selected, props.defaultSelected || [], props.onSelectionChange);
3133
3172
  const selected = selState[0], setSelected = selState[1];
3134
- const pageState = useMaybeControlled(props.page, props.defaultPage || 1, props.onPageChange);
3173
+ const pageState = useMaybeControlled(
3174
+ props.page,
3175
+ props.defaultPage || 1,
3176
+ oneCallback ? null : props.onPageChange
3177
+ );
3135
3178
  const orderState = useMaybeControlled(
3136
3179
  props.columnOrder,
3137
3180
  columns.map(function(c) {
@@ -3301,11 +3344,15 @@ window.Aura = (() => {
3301
3344
  const pageRows = pageSize && !manual ? view.slice(first, first + pageSize) : view;
3302
3345
  const canSortAny = !busy && (manual ? total > 1 : rows.length > 1);
3303
3346
  const shownTotal = manual && props.totalRows == null ? first + rows.length : total;
3304
- const linkPaging = !!props.getPageHref && !props.onPageChange;
3347
+ const linkPaging = !!props.getPageHref && !props.onPageChange && !oneCallback;
3305
3348
  const prevLink = React28.useRef(null), nextLink = React28.useRef(null);
3306
- function goPage(p, focus) {
3349
+ function emit(s, p) {
3350
+ if (props.onStateChange) props.onStateChange({ sort: s, page: p });
3351
+ }
3352
+ function goPage(p, focus, quiet) {
3307
3353
  const next = Math.min(Math.max(1, p), pageCount);
3308
3354
  if (next === page) return false;
3355
+ if (!quiet) emit(sort, next);
3309
3356
  if (linkPaging) {
3310
3357
  const a = next === page - 1 ? prevLink.current : next === page + 1 ? nextLink.current : null;
3311
3358
  if (a) a.click();
@@ -3317,8 +3364,12 @@ window.Aura = (() => {
3317
3364
  return true;
3318
3365
  }
3319
3366
  function sortBy(key) {
3320
- setSort(nextSort(key));
3321
- if (!(manual && linkPaging)) goPage(1);
3367
+ applySort(nextSort(key));
3368
+ }
3369
+ function applySort(s) {
3370
+ setSort(s);
3371
+ if (!(manual && linkPaging)) goPage(1, void 0, true);
3372
+ emit(s, 1);
3322
3373
  }
3323
3374
  const rowHref = props.getRowHref;
3324
3375
  function followRow(el, e) {
@@ -3533,16 +3584,14 @@ window.Aura = (() => {
3533
3584
  label: t.sortAsc,
3534
3585
  icon: "arrow-up",
3535
3586
  onSelect: function() {
3536
- setSort({ key: c.key, dir: "asc" });
3537
- goPage(1);
3587
+ applySort({ key: c.key, dir: "asc" });
3538
3588
  }
3539
3589
  },
3540
3590
  {
3541
3591
  label: t.sortDesc,
3542
3592
  icon: "arrow-down",
3543
3593
  onSelect: function() {
3544
- setSort({ key: c.key, dir: "desc" });
3545
- goPage(1);
3594
+ applySort({ key: c.key, dir: "desc" });
3546
3595
  }
3547
3596
  },
3548
3597
  { separator: true }
@@ -3979,9 +4028,10 @@ window.Aura = (() => {
3979
4028
  className: "aura-icon-btn",
3980
4029
  "aria-label": label,
3981
4030
  onClick: function(e) {
3982
- if (props.onPageChange) {
4031
+ if (props.onPageChange || oneCallback) {
3983
4032
  e.preventDefault();
3984
4033
  pageState[1](p);
4034
+ emit(sort, p);
3985
4035
  }
3986
4036
  }
3987
4037
  },
@@ -4475,7 +4525,7 @@ window.Aura = (() => {
4475
4525
  var React32 = __toESM(require_react(), 1);
4476
4526
  var Breadcrumb = React32.forwardRef(function Breadcrumb2(props, ref) {
4477
4527
  const t = useStrings();
4478
- const Link = useLinkComponent();
4528
+ const Link = useLinkComponent(props.linkComponent);
4479
4529
  const items2 = props.items || [];
4480
4530
  return /* @__PURE__ */ React32.createElement("nav", { ref, "aria-label": props.label || t.breadcrumb, className: cx("aura-crumbs", props.className) }, /* @__PURE__ */ React32.createElement("ol", null, items2.map(function(it, i) {
4481
4531
  const last = i === items2.length - 1;
@@ -4524,7 +4574,11 @@ window.Aura = (() => {
4524
4574
 
4525
4575
  // src/responsive.tsx
4526
4576
  var React34 = __toESM(require_react(), 1);
4577
+
4578
+ // src/breakpoints.ts
4527
4579
  var breakpoints = { sm: 640, md: 768, lg: 1024, xl: 1280 };
4580
+
4581
+ // src/responsive.tsx
4528
4582
  var ORDER = ["base", "sm", "md", "lg", "xl"];
4529
4583
  function useBreakpoint() {
4530
4584
  return React34.useSyncExternalStore(subscribe, current, function() {
@@ -4693,7 +4747,7 @@ window.Aura = (() => {
4693
4747
  const ch = props.change;
4694
4748
  const dir = ch && (ch.direction || "flat");
4695
4749
  const tone2 = ch && (ch.tone || (dir === "up" ? "positive" : dir === "down" ? "negative" : "neutral"));
4696
- const Link = useLinkComponent();
4750
+ const Link = useLinkComponent(props.linkComponent);
4697
4751
  const v = props.value;
4698
4752
  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, ""));
4699
4753
  const Tag3 = props.href ? Link : props.onClick ? "button" : "div";
@@ -4717,16 +4771,21 @@ window.Aura = (() => {
4717
4771
  // src/TimePicker.tsx
4718
4772
  var React41 = __toESM(require_react(), 1);
4719
4773
  var import_react_dom9 = __toESM(require_react_dom(), 1);
4774
+
4775
+ // src/text.ts
4776
+ function formatBytes(n2) {
4777
+ if (n2 == null) return "";
4778
+ if (n2 < 1024) return n2 + " B";
4779
+ let u = ["KB", "MB", "GB"], i = -1;
4780
+ do {
4781
+ n2 /= 1024;
4782
+ i++;
4783
+ } while (n2 >= 1024 && i < u.length - 1);
4784
+ return (n2 >= 10 || Math.round(n2) === n2 ? Math.round(n2) : n2.toFixed(1)) + " " + u[i];
4785
+ }
4720
4786
  function pad2(n2) {
4721
4787
  return (n2 < 10 ? "0" : "") + n2;
4722
4788
  }
4723
- function toMin(t) {
4724
- const m = /^(\d{2}):(\d{2})$/.exec(t || "");
4725
- return m ? +m[1] * 60 + +m[2] : null;
4726
- }
4727
- function fromMin(n2) {
4728
- return pad2(Math.floor(n2 / 60)) + ":" + pad2(n2 % 60);
4729
- }
4730
4789
  function parseTime(text) {
4731
4790
  let s = String(text || "").trim().toLowerCase().replace(/\s*(น\.?|นาฬิกา)$/, "").trim();
4732
4791
  const pm = /\s*(pm|p\.m\.)$/.test(s), am = /\s*(am|a\.m\.)$/.test(s);
@@ -4739,6 +4798,18 @@ window.Aura = (() => {
4739
4798
  if (hh > 23 || mm > 59) return null;
4740
4799
  return pad2(hh) + ":" + pad2(mm);
4741
4800
  }
4801
+
4802
+ // src/TimePicker.tsx
4803
+ function pad3(n2) {
4804
+ return (n2 < 10 ? "0" : "") + n2;
4805
+ }
4806
+ function toMin(t) {
4807
+ const m = /^(\d{2}):(\d{2})$/.exec(t || "");
4808
+ return m ? +m[1] * 60 + +m[2] : null;
4809
+ }
4810
+ function fromMin(n2) {
4811
+ return pad3(Math.floor(n2 / 60)) + ":" + pad3(n2 % 60);
4812
+ }
4742
4813
  var TimePicker = React41.forwardRef(function TimePicker2(props, ref) {
4743
4814
  const t = useStrings();
4744
4815
  const auto = uid(), id = props.id || auto, listId = id + "-list";
@@ -5039,16 +5110,6 @@ window.Aura = (() => {
5039
5110
 
5040
5111
  // src/FileUpload.tsx
5041
5112
  var React42 = __toESM(require_react(), 1);
5042
- function formatBytes(n2) {
5043
- if (n2 == null) return "";
5044
- if (n2 < 1024) return n2 + " B";
5045
- let u = ["KB", "MB", "GB"], i = -1;
5046
- do {
5047
- n2 /= 1024;
5048
- i++;
5049
- } while (n2 >= 1024 && i < u.length - 1);
5050
- return (n2 >= 10 || Math.round(n2) === n2 ? Math.round(n2) : n2.toFixed(1)) + " " + u[i];
5051
- }
5052
5113
  function matches(file, accept) {
5053
5114
  if (!accept) return true;
5054
5115
  const name = (file.name || "").toLowerCase(), type = (file.type || "").toLowerCase();
@@ -5507,6 +5568,8 @@ window.Aura = (() => {
5507
5568
 
5508
5569
  // src/colorScheme.tsx
5509
5570
  var React44 = __toESM(require_react(), 1);
5571
+
5572
+ // src/colorSchemeScript.ts
5510
5573
  var DEFAULT_KEY = "aura-color-scheme";
5511
5574
  var SCHEMES = ["light", "dark", "system"];
5512
5575
  var EVENT = "aura-color-scheme";
@@ -5518,6 +5581,8 @@ window.Aura = (() => {
5518
5581
  const fallback = JSON.stringify(options && options.defaultScheme || "system");
5519
5582
  return "(function(){try{var s=localStorage.getItem(" + key + ");if(s!=='light'&&s!=='dark'&&s!=='system')s=" + fallback + `;var d=document.documentElement;d.setAttribute("data-theme",s);var dark=s==='dark'||(s==='system'&&window.matchMedia('(prefers-color-scheme: dark)').matches);d.classList.toggle("dark",dark);}catch(e){}})();`;
5520
5583
  }
5584
+
5585
+ // src/colorScheme.tsx
5521
5586
  function ColorSchemeScript(props) {
5522
5587
  return /* @__PURE__ */ React44.createElement(
5523
5588
  "script",
@@ -5745,7 +5810,7 @@ window.Aura = (() => {
5745
5810
  if (p >= 1 && p <= count && p !== page) st[1](p);
5746
5811
  }
5747
5812
  const link = props.getHref;
5748
- const Link = useLinkComponent();
5813
+ const Link = useLinkComponent(props.linkComponent);
5749
5814
  function item(p, label, extra) {
5750
5815
  const common = Object.assign(
5751
5816
  {
@@ -5780,29 +5845,40 @@ window.Aura = (() => {
5780
5845
  label
5781
5846
  );
5782
5847
  }
5783
- return /* @__PURE__ */ React49.createElement("nav", { ref, className: cx("aura-pagination", props.className), "aria-label": props.label || t.pagination }, /* @__PURE__ */ React49.createElement(
5784
- IconButton,
5785
- {
5786
- icon: "chevron-left",
5787
- label: t.prevPage,
5788
- disabled: page <= 1,
5789
- onClick: function() {
5790
- go(page - 1);
5791
- }
5792
- }
5793
- ), /* @__PURE__ */ React49.createElement("ol", { className: "aura-pagination__list" }, pageList(page, count, props.siblingCount == null ? 1 : props.siblingCount).map(function(p) {
5848
+ function arrow(p, icon, label, rel, disabled) {
5849
+ if (!link || disabled)
5850
+ return /* @__PURE__ */ React49.createElement(
5851
+ IconButton,
5852
+ {
5853
+ icon,
5854
+ label,
5855
+ disabled,
5856
+ onClick: function() {
5857
+ go(p);
5858
+ }
5859
+ }
5860
+ );
5861
+ return /* @__PURE__ */ React49.createElement(
5862
+ Link,
5863
+ {
5864
+ href: link(p),
5865
+ rel,
5866
+ className: "aura-icon-btn",
5867
+ "aria-label": label,
5868
+ title: label,
5869
+ onClick: function(e) {
5870
+ if (props.onChange) {
5871
+ e.preventDefault();
5872
+ go(p);
5873
+ }
5874
+ }
5875
+ },
5876
+ /* @__PURE__ */ React49.createElement(Icon, { name: icon, size: "sm" })
5877
+ );
5878
+ }
5879
+ return /* @__PURE__ */ React49.createElement("nav", { ref, className: cx("aura-pagination", props.className), "aria-label": props.label || t.pagination }, arrow(page - 1, "chevron-left", t.prevPage, "prev", page <= 1), /* @__PURE__ */ React49.createElement("ol", { className: "aura-pagination__list" }, pageList(page, count, props.siblingCount == null ? 1 : props.siblingCount).map(function(p) {
5794
5880
  return typeof p === "number" ? /* @__PURE__ */ React49.createElement("li", { key: p }, item(p, p)) : /* @__PURE__ */ React49.createElement("li", { key: p, className: "aura-pagination__gap", "aria-hidden": true }, "\u2026");
5795
- })), /* @__PURE__ */ React49.createElement("span", { className: "aura-pagination__compact", "aria-hidden": true }, t.page(page, count)), /* @__PURE__ */ React49.createElement(
5796
- IconButton,
5797
- {
5798
- icon: "chevron-right",
5799
- label: t.nextPage,
5800
- disabled: page >= count,
5801
- onClick: function() {
5802
- go(page + 1);
5803
- }
5804
- }
5805
- ));
5881
+ })), /* @__PURE__ */ React49.createElement("span", { className: "aura-pagination__compact", "aria-hidden": true }, t.page(page, count)), arrow(page + 1, "chevron-right", t.nextPage, "next", page >= count));
5806
5882
  });
5807
5883
 
5808
5884
  // src/Accordion.tsx