@jirawatpyk/aura-react 4.13.0 → 4.14.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
@@ -9,8 +9,8 @@ AURA Design System components as a real React package — ES modules, TypeScript
9
9
  One HTML file, no install — the packages are on npmjs, so jsDelivr serves them:
10
10
 
11
11
  ```html
12
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jirawatpyk/aura-tokens@4/aura.css">
13
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jirawatpyk/aura-react@4/dist/styles.css">
12
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jirawatpyk/aura-tokens@4/aura.css" />
13
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jirawatpyk/aura-react@4/dist/styles.css" />
14
14
  <script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
15
15
  <script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>
16
16
  <script src="https://cdn.jsdelivr.net/npm/@jirawatpyk/aura-react@4/dist/aura.bundle.js"></script>
@@ -19,10 +19,17 @@ One HTML file, no install — the packages are on npmjs, so jsDelivr serves them
19
19
  <script>
20
20
  const h = React.createElement;
21
21
  ReactDOM.createRoot(document.getElementById('root')).render(
22
- h(Aura.AuraProvider, { locale: 'th' },
23
- h(Aura.Stack, { gap: 4 },
22
+ h(
23
+ Aura.AuraProvider,
24
+ { locale: 'th' },
25
+ h(
26
+ Aura.Stack,
27
+ { gap: 4 },
24
28
  h(Aura.Button, { icon: 'plus' }, 'New order'),
25
- h(Aura.DatePicker, { label: 'Delivery date' }))));
29
+ h(Aura.DatePicker, { label: 'Delivery date' }),
30
+ ),
31
+ ),
32
+ );
26
33
  </script>
27
34
  ```
28
35
 
@@ -38,15 +45,17 @@ Public on npmjs; internal projects can use GitHub Packages instead ([repository
38
45
 
39
46
  ```tsx
40
47
  // app root (once)
41
- import '@aura/tokens/aura.css'; // tokens (light + dark)
42
- import '@aura/react/styles.css'; // component styles
48
+ import '@aura/tokens/aura.css'; // tokens (light + dark)
49
+ import '@aura/react/styles.css'; // component styles
43
50
  // fonts: Google Fonts <link> tags in <head>, or '@aura/tokens/aura-fonts.css'
44
51
 
45
52
  import { AuraProvider, AppShell, SideNav, DataTable, DatePicker } from '@aura/react';
46
53
 
47
54
  export default function Root() {
48
55
  return (
49
- <AuraProvider locale="th"> {/* Thai built-in labels; dates are พ.ศ. by default */}
56
+ <AuraProvider locale="th">
57
+ {' '}
58
+ {/* Thai built-in labels; dates are พ.ศ. by default */}
50
59
  <App />
51
60
  </AuraProvider>
52
61
  );
@@ -55,17 +64,17 @@ export default function Root() {
55
64
 
56
65
  ## What's inside
57
66
 
58
- | Output | For |
59
- |---|---|
60
- | `dist/esm/*.js` | Bundlers (Vite, Next.js, webpack). One file per module with `'use client'`, so Next.js App Router can import it from Server Components. `sideEffects` is limited to CSS, so unused components are dropped. |
61
- | `dist/cjs/index.cjs` | `require()` / Jest. |
62
- | `dist/aura.bundle.js` | A classic `<script>` that sets `window.Aura` (needs `window.React` / `window.ReactDOM`). The design-system artifact uses this file. |
63
- | `dist/styles.css` | Component CSS (no font import). |
64
- | `dist/index.d.ts` | Types for every component and helper, generated from the TypeScript sources (one file). |
67
+ | Output | For |
68
+ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
69
+ | `dist/esm/*.js` | Bundlers (Vite, Next.js, webpack). One file per module with `'use client'`, so Next.js App Router can import it from Server Components. `sideEffects` is limited to CSS, so unused components are dropped. |
70
+ | `dist/cjs/index.cjs` | `require()` / Jest. |
71
+ | `dist/aura.bundle.js` | A classic `<script>` that sets `window.Aura` (needs `window.React` / `window.ReactDOM`). The design-system artifact uses this file. |
72
+ | `dist/styles.css` | Component CSS (no font import). |
73
+ | `dist/index.d.ts` | Types for every component and helper, generated from the TypeScript sources (one file). |
65
74
 
66
75
  ## Components (49)
67
76
 
68
- Actions: Button, IconButton, Menu, DropdownMenu, Tag · Forms: TextField, PasswordField, Textarea, NumberField, Select, RadioGroup, Checkbox, Switch, SegmentedControl, Combobox (one value, or `multiple`), FileUpload (+ `formatBytes`) · Dates & times: DatePicker, DateRangePicker, Calendar, TimePicker (+ `useFormatDate`, `formatDate`, `parseDate`, `parseTime`) · Feedback: Alert, FormErrorSummary, Toaster/`toast()` (+ `.success/.error/.warning/.info/.loading`), Tooltip, StatusPill, Badge, Progress, Skeleton, EmptyState · Overlays: Dialog, Drawer, Popover · Data: DataTable, FilterBar, Stat · Navigation: Command (⌘K palette) · Layout: AppShell, Container, Stack, Grid, Accordion, Pagination, Card, Tabs, Stepper, SideNav, Breadcrumb, Avatar, Surface, Icon · Theme: ColorSchemeToggle, ColorSchemeScript, `useColorScheme`, ThemeStyle/`createTheme` · Hooks: `useBreakpoint`, `useResponsive`, `breakpoints`, `useAuraLocale`.
77
+ Actions: Button, IconButton, Menu, DropdownMenu, Tag · Forms: TextField, PasswordField, Textarea, NumberField, Select, RadioGroup, Checkbox, Switch, SegmentedControl, Combobox (one value, or `multiple`), FileUpload (+ `formatBytes`) · Dates & times: DatePicker, DateRangePicker, Calendar, TimePicker (+ `useFormatDate`, `formatDate`, `parseDate`, `parseTime`) · Feedback: Alert, FormErrorSummary, Toaster/`toast()` (+ `.success/.error/.warning/.info/.loading`), Tooltip, StatusPill, Badge, Progress, Skeleton, EmptyState · Overlays: Dialog, Drawer, Popover · Data: DataTable, FilterBar, Stat · Navigation: Command (⌘K palette) · Layout: AppShell, Container, Stack, Grid, Accordion, Pagination, Card, Tabs, Stepper, SideNav, Breadcrumb, Avatar, Surface, Icon · Theme: ColorSchemeToggle, ColorSchemeScript, `useColorScheme`, ThemeStyle/`createTheme` · Hooks: `useBreakpoint`, `useResponsive`, `breakpoints`, `useAuraLocale`, `useDensity`.
69
78
 
70
79
  ## Router links, Swedish, motion
71
80
 
@@ -76,6 +85,15 @@ import Link from 'next/link';
76
85
 
77
86
  `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.
78
87
 
88
+ ## Compact density
89
+
90
+ ```tsx
91
+ <AuraProvider density="compact">…</AuraProvider> {/* whole app or one section */}
92
+ <DataTable density="compact" … /> {/* just one table */}
93
+ ```
94
+
95
+ Compact makes fields and buttons 36px (from 44px), table rows 40px (from 48px) and tightens button padding. Dialogs, drawers, popovers and the ⌘K palette opened inside the provider follow it. On touch screens (`pointer: coarse`) compact keeps 44px targets and 48px rows. Read it with `useDensity()`. Without CSS-in-JS you can set `data-density="compact"` on any element — the CSS variables `--aura-input-height`, `--aura-button-height`, `--aura-table-row-height` and `--aura-button-padding-x` do the rest; virtual DataTables read the row height from CSS.
96
+
79
97
  Motion: skeleton pulses and indeterminate bars honour `prefers-reduced-motion: reduce` — skeletons stop (a static bar), spinners and progress bars slow to a third.
80
98
 
81
99
  ## Toasts, forms, filters, rich text, ⌘K
@@ -123,6 +123,7 @@ window.Aura = (() => {
123
123
  useAuraLocale: () => useAuraLocale,
124
124
  useBreakpoint: () => useBreakpoint,
125
125
  useColorScheme: () => useColorScheme,
126
+ useDensity: () => useDensity,
126
127
  useFormatDate: () => useFormatDate,
127
128
  useResponsive: () => useResponsive
128
129
  });
@@ -609,6 +610,8 @@ window.Aura = (() => {
609
610
  };
610
611
  var LocaleContext = React3.createContext(null);
611
612
  function AuraProvider(props) {
613
+ const outer = React3.useContext(LocaleContext);
614
+ const density = props.density || outer && outer.density || null;
612
615
  const value = React3.useMemo(
613
616
  function() {
614
617
  const base = props.locale && STRINGS[props.locale] || STRINGS.en;
@@ -616,12 +619,13 @@ window.Aura = (() => {
616
619
  locale: props.locale || "en",
617
620
  calendar: props.calendar || null,
618
621
  strings: props.strings ? Object.assign({}, base, props.strings) : base,
619
- linkComponent: props.linkComponent || null
622
+ linkComponent: props.linkComponent || null,
623
+ density
620
624
  };
621
625
  },
622
- [props.locale, props.calendar, props.strings, props.linkComponent]
626
+ [props.locale, props.calendar, props.strings, props.linkComponent, density]
623
627
  );
624
- return /* @__PURE__ */ React3.createElement(LocaleContext.Provider, { value }, props.children);
628
+ return /* @__PURE__ */ React3.createElement(LocaleContext.Provider, { value }, props.density ? /* @__PURE__ */ React3.createElement("div", { className: "aura-density", "data-density": props.density }, props.children) : props.children);
625
629
  }
626
630
  function useAuraLocale() {
627
631
  return React3.useContext(LocaleContext) || { locale: null, calendar: null, strings: STRINGS.en };
@@ -630,6 +634,10 @@ window.Aura = (() => {
630
634
  const ctx = React3.useContext(LocaleContext);
631
635
  return own || ctx && ctx.linkComponent || "a";
632
636
  }
637
+ function useDensity() {
638
+ const ctx = React3.useContext(LocaleContext);
639
+ return ctx && ctx.density || void 0;
640
+ }
633
641
  function useStrings() {
634
642
  return useAuraLocale().strings;
635
643
  }
@@ -2722,6 +2730,7 @@ window.Aura = (() => {
2722
2730
  }
2723
2731
  function Command(props) {
2724
2732
  const t = useStrings();
2733
+ const density = useDensity();
2725
2734
  const id = uid(), listId = id + "-list";
2726
2735
  const box = React25.useRef(null);
2727
2736
  const input = React25.useRef(null);
@@ -2833,6 +2842,7 @@ window.Aura = (() => {
2833
2842
  "div",
2834
2843
  {
2835
2844
  className: "aura-dialog-layer aura-command-layer",
2845
+ "data-density": density,
2836
2846
  onKeyDown: function(e) {
2837
2847
  modal.onKeyDown(e);
2838
2848
  e.stopPropagation();
@@ -3002,6 +3012,7 @@ window.Aura = (() => {
3002
3012
  var import_react_dom7 = __toESM(require_react_dom(), 1);
3003
3013
  var Dialog = React27.forwardRef(function Dialog2(props, ref) {
3004
3014
  const t = useStrings();
3015
+ const density = useDensity();
3005
3016
  const own = React27.useRef(null), merged = useMergedRef(ref, own), titleId = uid(), descId = uid();
3006
3017
  function close() {
3007
3018
  if (props.dismissible !== false && props.onClose) props.onClose();
@@ -3014,7 +3025,7 @@ window.Aura = (() => {
3014
3025
  });
3015
3026
  if (!modal.ready) return null;
3016
3027
  return (0, import_react_dom7.createPortal)(
3017
- /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer", onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React27.createElement(
3028
+ /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React27.createElement(
3018
3029
  "div",
3019
3030
  {
3020
3031
  ref: merged,
@@ -3035,6 +3046,7 @@ window.Aura = (() => {
3035
3046
  });
3036
3047
  var Drawer = React27.forwardRef(function Drawer2(props, ref) {
3037
3048
  const t = useStrings();
3049
+ const density = useDensity();
3038
3050
  const own = React27.useRef(null), merged = useMergedRef(ref, own), titleId = uid(), descId = uid();
3039
3051
  function close() {
3040
3052
  if (props.dismissible !== false && props.onClose) props.onClose();
@@ -3048,7 +3060,7 @@ window.Aura = (() => {
3048
3060
  if (!modal.ready) return null;
3049
3061
  const side = props.side === "left" ? "left" : "right";
3050
3062
  return (0, import_react_dom7.createPortal)(
3051
- /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer aura-drawer-layer", onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React27.createElement(
3063
+ /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer aura-drawer-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React27.createElement(
3052
3064
  "div",
3053
3065
  {
3054
3066
  ref: merged,
@@ -3079,7 +3091,7 @@ window.Aura = (() => {
3079
3091
  { key: "owner", label: "OWNER" }
3080
3092
  ];
3081
3093
  var SKELETON_WIDTHS = ["72%", "56%", "84%", "44%", "64%"];
3082
- var ROW_H = 48;
3094
+ var ROW_H_DEFAULT = 48;
3083
3095
  var OVERSCAN = 8;
3084
3096
  var FLEX_MIN = 160;
3085
3097
  var DataTable = React28.forwardRef(function DataTable2(props, ref) {
@@ -3165,6 +3177,14 @@ window.Aura = (() => {
3165
3177
  return px != null && boxWidth[0] < px;
3166
3178
  }
3167
3179
  const scrollRef = React28.useRef(null);
3180
+ const rowH = React28.useState(ROW_H_DEFAULT);
3181
+ const ROW_H = rowH[0];
3182
+ useIsoLayoutEffect(function() {
3183
+ const el = wrapRef.current;
3184
+ if (!el || typeof getComputedStyle === "undefined") return;
3185
+ const v = parseFloat(getComputedStyle(el).getPropertyValue("--aura-table-row-height"));
3186
+ if (v > 0 && v !== rowH[0]) rowH[1](v);
3187
+ });
3168
3188
  const pending = React28.useRef(null);
3169
3189
  const resizing = React28.useRef(false);
3170
3190
  useIsoLayoutEffect(function() {
@@ -4027,6 +4047,7 @@ window.Aura = (() => {
4027
4047
  "div",
4028
4048
  {
4029
4049
  ref: wrapMerged,
4050
+ "data-density": props.density,
4030
4051
  className: cx("aura-table aura-table--stacked", refreshing && "is-refreshing", props.className),
4031
4052
  role: "region",
4032
4053
  "aria-label": props.label,
@@ -4055,6 +4076,7 @@ window.Aura = (() => {
4055
4076
  "div",
4056
4077
  {
4057
4078
  ref: wrapMerged,
4079
+ "data-density": props.density,
4058
4080
  className: cx("aura-table", scrolledX[0] && "is-scrolled-x", refreshing && "is-refreshing", props.className)
4059
4081
  },
4060
4082
  refreshing ? /* @__PURE__ */ React28.createElement("span", { className: "aura-table__busy-bar", "aria-hidden": true }) : null,
@@ -5759,6 +5781,7 @@ window.Aura = (() => {
5759
5781
  }
5760
5782
  var Popover = React51.forwardRef(function Popover2(props, ref) {
5761
5783
  const t = useStrings();
5784
+ const density = useDensity();
5762
5785
  const auto = uid(), id = props.id || auto;
5763
5786
  const st = useMaybeControlled(props.open, !!props.defaultOpen, props.onOpenChange);
5764
5787
  const open = !!st[0];
@@ -5816,6 +5839,7 @@ window.Aura = (() => {
5816
5839
  {
5817
5840
  ref: popMerged,
5818
5841
  id,
5842
+ "data-density": density,
5819
5843
  role: "dialog",
5820
5844
  "aria-modal": false,
5821
5845
  "aria-label": props.title ? void 0 : props.label,
@@ -104,6 +104,7 @@ __export(index_exports, {
104
104
  useAuraLocale: () => useAuraLocale,
105
105
  useBreakpoint: () => useBreakpoint,
106
106
  useColorScheme: () => useColorScheme,
107
+ useDensity: () => useDensity,
107
108
  useFormatDate: () => useFormatDate,
108
109
  useResponsive: () => useResponsive
109
110
  });
@@ -591,6 +592,8 @@ var STRINGS = {
591
592
  };
592
593
  var LocaleContext = React3.createContext(null);
593
594
  function AuraProvider(props) {
595
+ const outer = React3.useContext(LocaleContext);
596
+ const density = props.density || outer && outer.density || null;
594
597
  const value = React3.useMemo(
595
598
  function() {
596
599
  const base = props.locale && STRINGS[props.locale] || STRINGS.en;
@@ -598,12 +601,13 @@ function AuraProvider(props) {
598
601
  locale: props.locale || "en",
599
602
  calendar: props.calendar || null,
600
603
  strings: props.strings ? Object.assign({}, base, props.strings) : base,
601
- linkComponent: props.linkComponent || null
604
+ linkComponent: props.linkComponent || null,
605
+ density
602
606
  };
603
607
  },
604
- [props.locale, props.calendar, props.strings, props.linkComponent]
608
+ [props.locale, props.calendar, props.strings, props.linkComponent, density]
605
609
  );
606
- return /* @__PURE__ */ React3.createElement(LocaleContext.Provider, { value }, props.children);
610
+ return /* @__PURE__ */ React3.createElement(LocaleContext.Provider, { value }, props.density ? /* @__PURE__ */ React3.createElement("div", { className: "aura-density", "data-density": props.density }, props.children) : props.children);
607
611
  }
608
612
  function useAuraLocale() {
609
613
  return React3.useContext(LocaleContext) || { locale: null, calendar: null, strings: STRINGS.en };
@@ -612,6 +616,10 @@ function useLinkComponent(own) {
612
616
  const ctx = React3.useContext(LocaleContext);
613
617
  return own || ctx && ctx.linkComponent || "a";
614
618
  }
619
+ function useDensity() {
620
+ const ctx = React3.useContext(LocaleContext);
621
+ return ctx && ctx.density || void 0;
622
+ }
615
623
  function useStrings() {
616
624
  return useAuraLocale().strings;
617
625
  }
@@ -2704,6 +2712,7 @@ function isMac() {
2704
2712
  }
2705
2713
  function Command(props) {
2706
2714
  const t = useStrings();
2715
+ const density = useDensity();
2707
2716
  const id = uid(), listId = id + "-list";
2708
2717
  const box = React25.useRef(null);
2709
2718
  const input = React25.useRef(null);
@@ -2815,6 +2824,7 @@ function Command(props) {
2815
2824
  "div",
2816
2825
  {
2817
2826
  className: "aura-dialog-layer aura-command-layer",
2827
+ "data-density": density,
2818
2828
  onKeyDown: function(e) {
2819
2829
  modal.onKeyDown(e);
2820
2830
  e.stopPropagation();
@@ -2984,6 +2994,7 @@ var React27 = __toESM(require("react"), 1);
2984
2994
  var import_react_dom7 = require("react-dom");
2985
2995
  var Dialog = React27.forwardRef(function Dialog2(props, ref) {
2986
2996
  const t = useStrings();
2997
+ const density = useDensity();
2987
2998
  const own = React27.useRef(null), merged = useMergedRef(ref, own), titleId = uid(), descId = uid();
2988
2999
  function close() {
2989
3000
  if (props.dismissible !== false && props.onClose) props.onClose();
@@ -2996,7 +3007,7 @@ var Dialog = React27.forwardRef(function Dialog2(props, ref) {
2996
3007
  });
2997
3008
  if (!modal.ready) return null;
2998
3009
  return (0, import_react_dom7.createPortal)(
2999
- /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer", onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React27.createElement(
3010
+ /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React27.createElement(
3000
3011
  "div",
3001
3012
  {
3002
3013
  ref: merged,
@@ -3017,6 +3028,7 @@ var Dialog = React27.forwardRef(function Dialog2(props, ref) {
3017
3028
  });
3018
3029
  var Drawer = React27.forwardRef(function Drawer2(props, ref) {
3019
3030
  const t = useStrings();
3031
+ const density = useDensity();
3020
3032
  const own = React27.useRef(null), merged = useMergedRef(ref, own), titleId = uid(), descId = uid();
3021
3033
  function close() {
3022
3034
  if (props.dismissible !== false && props.onClose) props.onClose();
@@ -3030,7 +3042,7 @@ var Drawer = React27.forwardRef(function Drawer2(props, ref) {
3030
3042
  if (!modal.ready) return null;
3031
3043
  const side = props.side === "left" ? "left" : "right";
3032
3044
  return (0, import_react_dom7.createPortal)(
3033
- /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer aura-drawer-layer", onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React27.createElement(
3045
+ /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer aura-drawer-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React27.createElement(
3034
3046
  "div",
3035
3047
  {
3036
3048
  ref: merged,
@@ -3061,7 +3073,7 @@ var DEFAULT_COLUMNS = [
3061
3073
  { key: "owner", label: "OWNER" }
3062
3074
  ];
3063
3075
  var SKELETON_WIDTHS = ["72%", "56%", "84%", "44%", "64%"];
3064
- var ROW_H = 48;
3076
+ var ROW_H_DEFAULT = 48;
3065
3077
  var OVERSCAN = 8;
3066
3078
  var FLEX_MIN = 160;
3067
3079
  var DataTable = React28.forwardRef(function DataTable2(props, ref) {
@@ -3147,6 +3159,14 @@ var DataTable = React28.forwardRef(function DataTable2(props, ref) {
3147
3159
  return px != null && boxWidth[0] < px;
3148
3160
  }
3149
3161
  const scrollRef = React28.useRef(null);
3162
+ const rowH = React28.useState(ROW_H_DEFAULT);
3163
+ const ROW_H = rowH[0];
3164
+ useIsoLayoutEffect(function() {
3165
+ const el = wrapRef.current;
3166
+ if (!el || typeof getComputedStyle === "undefined") return;
3167
+ const v = parseFloat(getComputedStyle(el).getPropertyValue("--aura-table-row-height"));
3168
+ if (v > 0 && v !== rowH[0]) rowH[1](v);
3169
+ });
3150
3170
  const pending = React28.useRef(null);
3151
3171
  const resizing = React28.useRef(false);
3152
3172
  useIsoLayoutEffect(function() {
@@ -4009,6 +4029,7 @@ var DataTable = React28.forwardRef(function DataTable2(props, ref) {
4009
4029
  "div",
4010
4030
  {
4011
4031
  ref: wrapMerged,
4032
+ "data-density": props.density,
4012
4033
  className: cx("aura-table aura-table--stacked", refreshing && "is-refreshing", props.className),
4013
4034
  role: "region",
4014
4035
  "aria-label": props.label,
@@ -4037,6 +4058,7 @@ var DataTable = React28.forwardRef(function DataTable2(props, ref) {
4037
4058
  "div",
4038
4059
  {
4039
4060
  ref: wrapMerged,
4061
+ "data-density": props.density,
4040
4062
  className: cx("aura-table", scrolledX[0] && "is-scrolled-x", refreshing && "is-refreshing", props.className)
4041
4063
  },
4042
4064
  refreshing ? /* @__PURE__ */ React28.createElement("span", { className: "aura-table__busy-bar", "aria-hidden": true }) : null,
@@ -5741,6 +5763,7 @@ function position(anchor, pop, placement) {
5741
5763
  }
5742
5764
  var Popover = React51.forwardRef(function Popover2(props, ref) {
5743
5765
  const t = useStrings();
5766
+ const density = useDensity();
5744
5767
  const auto = uid(), id = props.id || auto;
5745
5768
  const st = useMaybeControlled(props.open, !!props.defaultOpen, props.onOpenChange);
5746
5769
  const open = !!st[0];
@@ -5798,6 +5821,7 @@ var Popover = React51.forwardRef(function Popover2(props, ref) {
5798
5821
  {
5799
5822
  ref: popMerged,
5800
5823
  id,
5824
+ "data-density": density,
5801
5825
  role: "dialog",
5802
5826
  "aria-modal": false,
5803
5827
  "aria-label": props.title ? void 0 : props.label,
@@ -4,13 +4,14 @@ import { createPortal } from "react-dom";
4
4
  import { Icon } from "./Icon.js";
5
5
  import { defaultFilter } from "./Combobox.js";
6
6
  import { cx, uid } from "./internal.js";
7
- import { useStrings } from "./locale.js";
7
+ import { useDensity, useStrings } from "./locale.js";
8
8
  import { useModal } from "./useModal.js";
9
9
  function isMac() {
10
10
  return typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
11
11
  }
12
12
  function Command(props) {
13
13
  const t = useStrings();
14
+ const density = useDensity();
14
15
  const id = uid(), listId = id + "-list";
15
16
  const box = React.useRef(null);
16
17
  const input = React.useRef(null);
@@ -122,6 +123,7 @@ function Command(props) {
122
123
  "div",
123
124
  {
124
125
  className: "aura-dialog-layer aura-command-layer",
126
+ "data-density": density,
125
127
  onKeyDown: function(e) {
126
128
  modal.onKeyDown(e);
127
129
  e.stopPropagation();
@@ -15,7 +15,7 @@ const DEFAULT_COLUMNS = [
15
15
  { key: "owner", label: "OWNER" }
16
16
  ];
17
17
  const SKELETON_WIDTHS = ["72%", "56%", "84%", "44%", "64%"];
18
- const ROW_H = 48, OVERSCAN = 8, FLEX_MIN = 160;
18
+ const ROW_H_DEFAULT = 48, OVERSCAN = 8, FLEX_MIN = 160;
19
19
  const DataTable = React.forwardRef(function DataTable2(props, ref) {
20
20
  const t = useStrings();
21
21
  const columns = props.columns || DEFAULT_COLUMNS;
@@ -99,6 +99,14 @@ const DataTable = React.forwardRef(function DataTable2(props, ref) {
99
99
  return px != null && boxWidth[0] < px;
100
100
  }
101
101
  const scrollRef = React.useRef(null);
102
+ const rowH = React.useState(ROW_H_DEFAULT);
103
+ const ROW_H = rowH[0];
104
+ useIsoLayoutEffect(function() {
105
+ const el = wrapRef.current;
106
+ if (!el || typeof getComputedStyle === "undefined") return;
107
+ const v = parseFloat(getComputedStyle(el).getPropertyValue("--aura-table-row-height"));
108
+ if (v > 0 && v !== rowH[0]) rowH[1](v);
109
+ });
102
110
  const pending = React.useRef(null);
103
111
  const resizing = React.useRef(false);
104
112
  useIsoLayoutEffect(function() {
@@ -961,6 +969,7 @@ const DataTable = React.forwardRef(function DataTable2(props, ref) {
961
969
  "div",
962
970
  {
963
971
  ref: wrapMerged,
972
+ "data-density": props.density,
964
973
  className: cx("aura-table aura-table--stacked", refreshing && "is-refreshing", props.className),
965
974
  role: "region",
966
975
  "aria-label": props.label,
@@ -989,6 +998,7 @@ const DataTable = React.forwardRef(function DataTable2(props, ref) {
989
998
  "div",
990
999
  {
991
1000
  ref: wrapMerged,
1001
+ "data-density": props.density,
992
1002
  className: cx("aura-table", scrolledX[0] && "is-scrolled-x", refreshing && "is-refreshing", props.className)
993
1003
  },
994
1004
  refreshing ? /* @__PURE__ */ React.createElement("span", { className: "aura-table__busy-bar", "aria-hidden": true }) : null,
@@ -1,12 +1,13 @@
1
1
  'use client';
2
2
  import * as React from "react";
3
- import { useStrings } from "./locale.js";
3
+ import { useStrings, useDensity } from "./locale.js";
4
4
  import { createPortal } from "react-dom";
5
5
  import { cx, uid, useMergedRef } from "./internal.js";
6
6
  import { IconButton } from "./IconButton.js";
7
7
  import { useModal } from "./useModal.js";
8
8
  const Dialog = React.forwardRef(function Dialog2(props, ref) {
9
9
  const t = useStrings();
10
+ const density = useDensity();
10
11
  const own = React.useRef(null), merged = useMergedRef(ref, own), titleId = uid(), descId = uid();
11
12
  function close() {
12
13
  if (props.dismissible !== false && props.onClose) props.onClose();
@@ -19,7 +20,7 @@ const Dialog = React.forwardRef(function Dialog2(props, ref) {
19
20
  });
20
21
  if (!modal.ready) return null;
21
22
  return createPortal(
22
- /* @__PURE__ */ React.createElement("div", { className: "aura-dialog-layer", onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React.createElement(
23
+ /* @__PURE__ */ React.createElement("div", { className: "aura-dialog-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React.createElement(
23
24
  "div",
24
25
  {
25
26
  ref: merged,
@@ -40,6 +41,7 @@ const Dialog = React.forwardRef(function Dialog2(props, ref) {
40
41
  });
41
42
  const Drawer = React.forwardRef(function Drawer2(props, ref) {
42
43
  const t = useStrings();
44
+ const density = useDensity();
43
45
  const own = React.useRef(null), merged = useMergedRef(ref, own), titleId = uid(), descId = uid();
44
46
  function close() {
45
47
  if (props.dismissible !== false && props.onClose) props.onClose();
@@ -53,7 +55,7 @@ const Drawer = React.forwardRef(function Drawer2(props, ref) {
53
55
  if (!modal.ready) return null;
54
56
  const side = props.side === "left" ? "left" : "right";
55
57
  return createPortal(
56
- /* @__PURE__ */ React.createElement("div", { className: "aura-dialog-layer aura-drawer-layer", onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React.createElement(
58
+ /* @__PURE__ */ React.createElement("div", { className: "aura-dialog-layer aura-drawer-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React.createElement(
57
59
  "div",
58
60
  {
59
61
  ref: merged,
@@ -11,7 +11,7 @@ import {
11
11
  useMergedRef,
12
12
  useMounted
13
13
  } from "./internal.js";
14
- import { useStrings } from "./locale.js";
14
+ import { useDensity, useStrings } from "./locale.js";
15
15
  import { createPortal } from "react-dom";
16
16
  function position(anchor, pop, placement) {
17
17
  const r = anchor.getBoundingClientRect(), pw = pop.offsetWidth, ph = pop.offsetHeight, vw = window.innerWidth, vh = window.innerHeight, gap = 6;
@@ -24,6 +24,7 @@ function position(anchor, pop, placement) {
24
24
  }
25
25
  const Popover = React.forwardRef(function Popover2(props, ref) {
26
26
  const t = useStrings();
27
+ const density = useDensity();
27
28
  const auto = uid(), id = props.id || auto;
28
29
  const st = useMaybeControlled(props.open, !!props.defaultOpen, props.onOpenChange);
29
30
  const open = !!st[0];
@@ -81,6 +82,7 @@ const Popover = React.forwardRef(function Popover2(props, ref) {
81
82
  {
82
83
  ref: popMerged,
83
84
  id,
85
+ "data-density": density,
84
86
  role: "dialog",
85
87
  "aria-modal": false,
86
88
  "aria-label": props.title ? void 0 : props.label,
package/dist/esm/index.js CHANGED
@@ -34,7 +34,7 @@ import { Container } from "./Container.js";
34
34
  import { AppShell } from "./AppShell.js";
35
35
  import { useBreakpoint, useResponsive, breakpoints } from "./responsive.js";
36
36
  import { Surface } from "./Surface.js";
37
- import { AuraProvider, useAuraLocale, STRINGS } from "./locale.js";
37
+ import { AuraProvider, useAuraLocale, useDensity, STRINGS } from "./locale.js";
38
38
  import { Stat } from "./Stat.js";
39
39
  import { TimePicker, parseTime } from "./TimePicker.js";
40
40
  import { FileUpload, formatBytes } from "./FileUpload.js";
@@ -126,6 +126,7 @@ export {
126
126
  useAuraLocale,
127
127
  useBreakpoint,
128
128
  useColorScheme,
129
+ useDensity,
129
130
  useFormatDate,
130
131
  useResponsive
131
132
  };
@@ -343,6 +343,8 @@ const STRINGS = {
343
343
  };
344
344
  const LocaleContext = React.createContext(null);
345
345
  function AuraProvider(props) {
346
+ const outer = React.useContext(LocaleContext);
347
+ const density = props.density || outer && outer.density || null;
346
348
  const value = React.useMemo(
347
349
  function() {
348
350
  const base = props.locale && STRINGS[props.locale] || STRINGS.en;
@@ -350,12 +352,13 @@ function AuraProvider(props) {
350
352
  locale: props.locale || "en",
351
353
  calendar: props.calendar || null,
352
354
  strings: props.strings ? Object.assign({}, base, props.strings) : base,
353
- linkComponent: props.linkComponent || null
355
+ linkComponent: props.linkComponent || null,
356
+ density
354
357
  };
355
358
  },
356
- [props.locale, props.calendar, props.strings, props.linkComponent]
359
+ [props.locale, props.calendar, props.strings, props.linkComponent, density]
357
360
  );
358
- return /* @__PURE__ */ React.createElement(LocaleContext.Provider, { value }, props.children);
361
+ return /* @__PURE__ */ React.createElement(LocaleContext.Provider, { value }, props.density ? /* @__PURE__ */ React.createElement("div", { className: "aura-density", "data-density": props.density }, props.children) : props.children);
359
362
  }
360
363
  function useAuraLocale() {
361
364
  return React.useContext(LocaleContext) || { locale: null, calendar: null, strings: STRINGS.en };
@@ -364,6 +367,10 @@ function useLinkComponent(own) {
364
367
  const ctx = React.useContext(LocaleContext);
365
368
  return own || ctx && ctx.linkComponent || "a";
366
369
  }
370
+ function useDensity() {
371
+ const ctx = React.useContext(LocaleContext);
372
+ return ctx && ctx.density || void 0;
373
+ }
367
374
  function useStrings() {
368
375
  return useAuraLocale().strings;
369
376
  }
@@ -371,6 +378,7 @@ export {
371
378
  AuraProvider,
372
379
  STRINGS,
373
380
  useAuraLocale,
381
+ useDensity,
374
382
  useLinkComponent,
375
383
  useStrings
376
384
  };
package/dist/index.d.ts CHANGED
@@ -149,6 +149,8 @@ export interface DataTableProps {
149
149
  /** Controlled pinned columns (keys); pair with onPinnedColumnsChange. */
150
150
  pinnedColumns?: string[];
151
151
  onPinnedColumnsChange?: (keys: string[]) => void;
152
+ /** `compact` = 40px rows (48px on touch screens). Default: the surrounding density. */
153
+ density?: "comfortable" | "compact";
152
154
  /** Container width in px below which rows render as stacked cards (phones). Try 640. Measured with ResizeObserver, so it follows the container, not the window. */
153
155
  stackBelow?: number;
154
156
  className?: string;
@@ -184,6 +186,8 @@ export interface AuraProviderProps {
184
186
  calendar?: "buddhist" | "gregory";
185
187
  /** Override individual strings. */
186
188
  strings?: Partial<Record<string, string | ((...args: any[]) => string)>>;
189
+ /** `compact`: 36px fields and buttons, 40px table rows — dense admin screens. Touch screens keep 44px. Default: comfortable (or an ancestor's `data-density`). */
190
+ density?: "comfortable" | "compact";
187
191
  /** Your router's link (e.g. `Link` from `next/link`), used by every AURA component that renders a link: Button with `href`, SideNav, Breadcrumb, Stat, Pagination, DataTable row links. It gets `href`, `className`, `aria-current`, the children and the ref. A component's own `linkComponent` wins. */
188
192
  linkComponent?: React$1.ElementType;
189
193
  children?: React$1.ReactNode;
@@ -1216,11 +1220,15 @@ export interface AuraLocaleValue {
1216
1220
  strings: AuraStrings;
1217
1221
  /** The router link set on AuraProvider, if any. */
1218
1222
  linkComponent?: React$1.ElementType | null;
1223
+ /** The density set on the nearest AuraProvider that sets one (null: none, i.e. comfortable unless an ancestor's data-density says otherwise). */
1224
+ density?: "comfortable" | "compact" | null;
1219
1225
  }
1220
1226
  /** Sets the language of built-in labels (and the default date locale) for everything inside. */
1221
1227
  export declare function AuraProvider(props: AuraProviderProps): React$1.ReactElement;
1222
1228
  /** { locale, calendar, strings } from the nearest AuraProvider (English strings when there is none). */
1223
1229
  export declare function useAuraLocale(): AuraLocaleValue;
1230
+ /** The provider's density, for portals that render outside its wrapper. */
1231
+ export declare function useDensity(): "comfortable" | "compact" | undefined;
1224
1232
  export declare const Stat: React$1.ForwardRefExoticComponent<StatProps & React$1.RefAttributes<HTMLElement>>;
1225
1233
  /** Parse typed time: 9 · 09 · 930 · 0930 · 9:30 · 9.30 · 09.30 น. · 9:30 pm → "HH:mm" (24-hour) or null. */
1226
1234
  export declare function parseTime(text: string | null | undefined): string | null;
package/dist/styles.css CHANGED
@@ -7,7 +7,7 @@
7
7
  box-sizing: border-box;
8
8
  height: var(--aura-button-height);
9
9
  white-space: nowrap; flex-shrink: 0; /* a pill never wraps its label */
10
- padding: 0 var(--aura-space-6);
10
+ padding: 0 var(--aura-button-padding-x, var(--aura-space-6));
11
11
  border-radius: var(--aura-button-radius);
12
12
  border: 0;
13
13
  background: transparent;
@@ -74,14 +74,14 @@ a.aura-btn { text-decoration: none; }
74
74
  }
75
75
  .aura-table__scroll { position: relative; overflow: auto; overscroll-behavior: contain; }
76
76
  .aura-table__head {
77
- position: sticky; top: 0; z-index: 3;
78
- display: flex; height: var(--aura-space-12);
77
+ position: sticky; top: 0; z-index: 3; box-sizing: border-box; /* the hairline is inside the row height */
78
+ display: flex; height: var(--aura-table-row-height);
79
79
  background: var(--aura-bg-canvas);
80
80
  border-bottom: 1px solid var(--aura-border-default);
81
81
  font-family: var(--font-mono); font-size: 12px; color: var(--aura-fg-secondary);
82
82
  }
83
83
  .aura-table__row {
84
- display: flex; height: var(--aura-space-12);
84
+ box-sizing: border-box; display: flex; height: var(--aura-table-row-height); /* exactly the virtual row height */
85
85
  background: var(--aura-bg-surface);
86
86
  border-bottom: 1px solid var(--aura-border-default);
87
87
  font-size: 13px;
@@ -105,7 +105,7 @@ a.aura-btn { text-decoration: none; }
105
105
  /* Server mode refresh: the previous page stays, inactive, under a progress bar until the next one arrives */
106
106
  .aura-table.is-refreshing .aura-table__row, .aura-table.is-refreshing .aura-table__card { opacity: 0.6; pointer-events: none; }
107
107
  .aura-table__busy-bar {
108
- position: absolute; z-index: 6; top: calc(var(--aura-space-12) - 2px); left: 0; right: 0; height: 2px; overflow: hidden;
108
+ position: absolute; z-index: 6; top: calc(var(--aura-table-row-height) - 2px); left: 0; right: 0; height: 2px; overflow: hidden;
109
109
  border-radius: var(--aura-radius-full); pointer-events: none;
110
110
  }
111
111
  .aura-table--stacked .aura-table__busy-bar { top: 0; }
@@ -117,7 +117,7 @@ a.aura-btn { text-decoration: none; }
117
117
 
118
118
  /* Cells: text cells stay block so long values end in an ellipsis */
119
119
  .aura-table__td {
120
- box-sizing: border-box; height: 100%; line-height: calc(var(--aura-space-12) - 1px);
120
+ box-sizing: border-box; height: 100%; line-height: calc(var(--aura-table-row-height) - 1px);
121
121
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-right: var(--aura-space-4);
122
122
  }
123
123
  .aura-table__td .aura-pill, .aura-table__td .aura-dropdown, .aura-table__td > .aura-icon-btn, .aura-table__td > .aura-btn { vertical-align: middle; }
@@ -130,7 +130,7 @@ a.aura-btn { text-decoration: none; }
130
130
  .aura-table__gutter { flex: none; width: var(--aura-space-6); position: sticky; left: 0; z-index: 2; background: inherit; }
131
131
  .aura-table__gutter--end { position: static; }
132
132
  .aura-table__picker {
133
- position: absolute; top: 0; right: 0; z-index: 5; width: var(--aura-space-12); height: calc(var(--aura-space-12) - 1px);
133
+ position: absolute; top: 0; right: 0; z-index: 5; width: var(--aura-space-12); height: calc(var(--aura-table-row-height) - 1px);
134
134
  display: flex; align-items: center; justify-content: center; background: var(--aura-bg-canvas); border-top-right-radius: calc(var(--aura-card-radius) - 1px);
135
135
  }
136
136
  .aura-table__gutter--end.has-picker {
@@ -228,7 +228,7 @@ a.aura-btn { text-decoration: none; }
228
228
 
229
229
  /* Footer + pager */
230
230
  .aura-table__foot {
231
- height: var(--aura-space-12); display: flex; align-items: center; justify-content: space-between;
231
+ height: var(--aura-table-row-height); display: flex; align-items: center; justify-content: space-between;
232
232
  padding: 0 var(--aura-space-3) 0 var(--aura-space-6);
233
233
  border-top: 1px solid var(--aura-border-default); background: var(--aura-bg-surface);
234
234
  font-size: 13px; color: var(--aura-fg-secondary); font-variant-numeric: tabular-nums;
@@ -1241,3 +1241,19 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
1241
1241
  }
1242
1242
  @media (prefers-reduced-motion: reduce) { .aura-command { animation: none; } }
1243
1243
 
1244
+ /* Density. data-density="compact" on <html> or any container (AuraProvider density="compact" and DataTable
1245
+ * density do it for you): 36px fields and buttons, 40px table rows, less button padding — for dense admin screens.
1246
+ * Touch screens keep 44px targets whatever the density. "comfortable" inside compact switches back. */
1247
+ [data-density='compact'] {
1248
+ --aura-input-height: 36px; --aura-button-height: 36px; --aura-table-row-height: 40px;
1249
+ --aura-button-padding-x: var(--aura-space-4);
1250
+ }
1251
+ [data-density='comfortable'] {
1252
+ --aura-input-height: 44px; --aura-button-height: 44px; --aura-table-row-height: 48px;
1253
+ --aura-button-padding-x: var(--aura-space-6);
1254
+ }
1255
+ @media (pointer: coarse) {
1256
+ [data-density='compact'] { --aura-input-height: 44px; --aura-button-height: 44px; --aura-table-row-height: 48px; --aura-button-padding-x: var(--aura-space-6); }
1257
+ }
1258
+ .aura-density { display: contents; }
1259
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jirawatpyk/aura-react",
3
- "version": "4.13.0",
3
+ "version": "4.14.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",