@btndprojects/loam-ui-native 0.0.0-stage → 0.1.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.
Files changed (269) hide show
  1. package/LICENSE +21 -0
  2. package/dist/accordion/index.js +3 -0
  3. package/dist/adaptive/index.js +2 -0
  4. package/dist/alert/index.js +3 -0
  5. package/dist/analyzer/index.js +6 -0
  6. package/dist/announcement/index.js +6 -0
  7. package/dist/audio-player/index.js +3 -0
  8. package/dist/avatar/index.js +3 -0
  9. package/dist/avatar-group/index.js +3 -0
  10. package/dist/badge/index.js +5 -0
  11. package/dist/banner/index.js +2 -0
  12. package/dist/board/index.js +7 -0
  13. package/dist/button/index.js +5 -0
  14. package/dist/camera-permission/index.js +13 -0
  15. package/dist/capture-result/index.js +18 -0
  16. package/dist/card/index.js +4 -0
  17. package/dist/card-form/index.js +10 -0
  18. package/dist/carousel/index.js +2 -0
  19. package/dist/chart/index.js +6 -0
  20. package/dist/charts/index.js +7 -0
  21. package/dist/checkbox/index.js +7 -0
  22. package/dist/checklist/index.js +10 -0
  23. package/dist/checkout/index.js +13 -0
  24. package/dist/chip/index.js +7 -0
  25. package/dist/chunk-223Q2T2Q.js +229 -0
  26. package/dist/chunk-2A5NIQAK.js +254 -0
  27. package/dist/chunk-2GK6PHGE.js +363 -0
  28. package/dist/chunk-2WUTXHMU.js +1 -0
  29. package/dist/chunk-2ZZ7TDJ4.js +324 -0
  30. package/dist/chunk-35OFI3JG.js +499 -0
  31. package/dist/chunk-3ARE4TES.js +195 -0
  32. package/dist/chunk-3ECPOSKO.js +1 -0
  33. package/dist/chunk-3ENTMPQY.js +229 -0
  34. package/dist/chunk-3HJH6FF4.js +98 -0
  35. package/dist/chunk-3NMD74ZU.js +4 -0
  36. package/dist/chunk-3WPW4KUN.js +182 -0
  37. package/dist/chunk-47FWHGUN.js +394 -0
  38. package/dist/chunk-4BXMXBJO.js +141 -0
  39. package/dist/chunk-4DS5ZWVC.js +83 -0
  40. package/dist/chunk-4GBOGOTV.js +295 -0
  41. package/dist/chunk-4SZYY6AN.js +99 -0
  42. package/dist/chunk-4YIP3CWS.js +237 -0
  43. package/dist/chunk-4YQOR7JM.js +204 -0
  44. package/dist/chunk-4ZAHCY44.js +177 -0
  45. package/dist/chunk-52X2VYTD.js +228 -0
  46. package/dist/chunk-5P26LBDL.js +1 -0
  47. package/dist/chunk-5XLRYDAX.js +1 -0
  48. package/dist/chunk-5YNTNN5A.js +177 -0
  49. package/dist/chunk-66SG2H23.js +129 -0
  50. package/dist/chunk-6HT2LP62.js +504 -0
  51. package/dist/chunk-6N33LFQA.js +35 -0
  52. package/dist/chunk-6X6PQ443.js +432 -0
  53. package/dist/chunk-7CFF4HXS.js +126 -0
  54. package/dist/chunk-7O2FMUPT.js +755 -0
  55. package/dist/chunk-AI4DWZCD.js +162 -0
  56. package/dist/chunk-AWJOCGFZ.js +213 -0
  57. package/dist/chunk-AZRX6SAC.js +285 -0
  58. package/dist/chunk-B54QE2FN.js +1 -0
  59. package/dist/chunk-BC475SJH.js +1 -0
  60. package/dist/chunk-BEDBFJND.js +202 -0
  61. package/dist/chunk-BTTFCMKG.js +166 -0
  62. package/dist/chunk-BV3B2HYZ.js +1635 -0
  63. package/dist/chunk-C2NTXVWM.js +98 -0
  64. package/dist/chunk-CDRZHT42.js +424 -0
  65. package/dist/chunk-CJ5EOYZE.js +62 -0
  66. package/dist/chunk-CUMUNPXY.js +128 -0
  67. package/dist/chunk-CUS3ZSCU.js +1 -0
  68. package/dist/chunk-CYRBHQQZ.js +411 -0
  69. package/dist/chunk-DHWADYZX.js +144 -0
  70. package/dist/chunk-DKCSPATL.js +1 -0
  71. package/dist/chunk-DM5QUKKO.js +326 -0
  72. package/dist/chunk-DSJBXLUG.js +845 -0
  73. package/dist/chunk-DYBH2I6W.js +1 -0
  74. package/dist/chunk-DZ255O2F.js +1 -0
  75. package/dist/chunk-E35BPSGW.js +1 -0
  76. package/dist/chunk-EJBDRVIH.js +166 -0
  77. package/dist/chunk-F43NP42L.js +1 -0
  78. package/dist/chunk-F5XNPWS7.js +1024 -0
  79. package/dist/chunk-FIF2M24N.js +1521 -0
  80. package/dist/chunk-FOKG57CR.js +993 -0
  81. package/dist/chunk-G3HOYSF7.js +1 -0
  82. package/dist/chunk-GDKZTW34.js +395 -0
  83. package/dist/chunk-GL7B2DOW.js +134 -0
  84. package/dist/chunk-GNNJ4GCC.js +471 -0
  85. package/dist/chunk-GNZ56EUI.js +1 -0
  86. package/dist/chunk-H6NIOQHK.js +346 -0
  87. package/dist/chunk-H6PC7RDA.js +1 -0
  88. package/dist/chunk-HPPW424V.js +334 -0
  89. package/dist/chunk-HQOQIBJI.js +1 -0
  90. package/dist/chunk-HU2EQNRZ.js +44 -0
  91. package/dist/chunk-IIQTAY3J.js +715 -0
  92. package/dist/chunk-ITTYJZWX.js +271 -0
  93. package/dist/chunk-IVNRBPGC.js +1018 -0
  94. package/dist/chunk-J4KLDGI4.js +1 -0
  95. package/dist/chunk-J7Q7VCVC.js +254 -0
  96. package/dist/chunk-JDEOITQ2.js +1 -0
  97. package/dist/chunk-JNH2TYIN.js +96 -0
  98. package/dist/chunk-JRLTGLN4.js +1 -0
  99. package/dist/chunk-JTRJJLKL.js +1 -0
  100. package/dist/chunk-JUAMLEHX.js +379 -0
  101. package/dist/chunk-JXJB2X6M.js +131 -0
  102. package/dist/chunk-KMI5ML5I.js +262 -0
  103. package/dist/chunk-KTAC4MGE.js +4 -0
  104. package/dist/chunk-LAECSR4H.js +178 -0
  105. package/dist/chunk-LK4BCSGX.js +1 -0
  106. package/dist/chunk-LOE5YNN3.js +470 -0
  107. package/dist/chunk-LRJNDEIA.js +302 -0
  108. package/dist/chunk-LSS54GLJ.js +116 -0
  109. package/dist/chunk-LZLHELAG.js +137 -0
  110. package/dist/chunk-M2LUXGKZ.js +1 -0
  111. package/dist/chunk-MOUJOP55.js +425 -0
  112. package/dist/chunk-N32RGOA5.js +159 -0
  113. package/dist/chunk-N5ZNE2YE.js +1 -0
  114. package/dist/chunk-NBNCBML6.js +1 -0
  115. package/dist/chunk-NKB7BZYK.js +167 -0
  116. package/dist/chunk-NQYBUO3K.js +1795 -0
  117. package/dist/chunk-NY2U3PBI.js +59 -0
  118. package/dist/chunk-O4AEHWII.js +464 -0
  119. package/dist/chunk-O7AE6DAF.js +146 -0
  120. package/dist/chunk-OETCUXQP.js +134 -0
  121. package/dist/chunk-OOPPQH27.js +179 -0
  122. package/dist/chunk-OPXFZ4Y5.js +204 -0
  123. package/dist/chunk-P23EQNRQ.js +338 -0
  124. package/dist/chunk-PJXUJYS6.js +248 -0
  125. package/dist/chunk-PNWUWOBF.js +1 -0
  126. package/dist/chunk-PUPM2YGF.js +376 -0
  127. package/dist/chunk-PZXZJBMP.js +620 -0
  128. package/dist/chunk-Q72LJOVE.js +169 -0
  129. package/dist/chunk-QCDXZBC5.js +247 -0
  130. package/dist/chunk-QDYDBCFP.js +197 -0
  131. package/dist/chunk-QJJ6MADV.js +274 -0
  132. package/dist/chunk-QKXCPTFS.js +349 -0
  133. package/dist/chunk-QWCMU5B2.js +860 -0
  134. package/dist/chunk-RGQWWKLA.js +634 -0
  135. package/dist/chunk-RIBDXGUG.js +108 -0
  136. package/dist/chunk-RJQOOHT2.js +214 -0
  137. package/dist/chunk-RNGLIHFA.js +191 -0
  138. package/dist/chunk-RQ3HASCC.js +6 -0
  139. package/dist/chunk-RSOJT5YW.js +124 -0
  140. package/dist/chunk-RTIKSY6A.js +1 -0
  141. package/dist/chunk-RUZHTGKB.js +237 -0
  142. package/dist/chunk-RW6ZL2FQ.js +284 -0
  143. package/dist/chunk-S7F6XLTK.js +540 -0
  144. package/dist/chunk-SBMNWWBV.js +155 -0
  145. package/dist/chunk-SDTQPRRB.js +1 -0
  146. package/dist/chunk-SYNOJOQO.js +377 -0
  147. package/dist/chunk-T753XT36.js +1 -0
  148. package/dist/chunk-TKQYWZ5V.js +196 -0
  149. package/dist/chunk-TKVFVF5D.js +95 -0
  150. package/dist/chunk-TOXAGRDQ.js +144 -0
  151. package/dist/chunk-TUTI5HOZ.js +112 -0
  152. package/dist/chunk-U7TFM2JA.js +140 -0
  153. package/dist/chunk-UC2WGUP3.js +627 -0
  154. package/dist/chunk-UWFLYMAR.js +1126 -0
  155. package/dist/chunk-UWSZB7BJ.js +1 -0
  156. package/dist/chunk-UYG5L34B.js +87 -0
  157. package/dist/chunk-VBOXAEPI.js +336 -0
  158. package/dist/chunk-VCDBXW5H.js +188 -0
  159. package/dist/chunk-VCXPHHDS.js +1 -0
  160. package/dist/chunk-VEAYTRPX.js +414 -0
  161. package/dist/chunk-VFCUIDRT.js +1 -0
  162. package/dist/chunk-VQHPQEUH.js +273 -0
  163. package/dist/chunk-VXDZFJGP.js +874 -0
  164. package/dist/chunk-WS56BL3D.js +1 -0
  165. package/dist/chunk-WSW6ZFAQ.js +98 -0
  166. package/dist/chunk-WVKKJA6U.js +870 -0
  167. package/dist/chunk-XNUVB5OL.js +375 -0
  168. package/dist/chunk-XOSCY2CW.js +242 -0
  169. package/dist/chunk-XXXEFXY5.js +99 -0
  170. package/dist/chunk-Y5CSSPV4.js +1 -0
  171. package/dist/chunk-Y6DU4J47.js +1 -0
  172. package/dist/chunk-YC6WORFL.js +535 -0
  173. package/dist/chunk-YDGVJETP.js +60 -0
  174. package/dist/chunk-YF4NSSMT.js +366 -0
  175. package/dist/chunk-YG3ZJHGU.js +223 -0
  176. package/dist/chunk-YIJ55TBC.js +1 -0
  177. package/dist/chunk-YKZ4RZZF.js +1 -0
  178. package/dist/chunk-YLLCXTMC.js +37 -0
  179. package/dist/chunk-YTA74UZR.js +1 -0
  180. package/dist/chunk-Z7H7VSFZ.js +323 -0
  181. package/dist/chunk-ZK2H4O5P.js +1 -0
  182. package/dist/chunk-ZVFE5QGU.js +524 -0
  183. package/dist/coachmark/index.js +4 -0
  184. package/dist/code-block/index.js +4 -0
  185. package/dist/color-field/index.js +7 -0
  186. package/dist/commerce/index.js +20 -0
  187. package/dist/connectivity/index.js +2 -0
  188. package/dist/context-menu/index.js +5 -0
  189. package/dist/copy-button/index.js +3 -0
  190. package/dist/data-grid/index.js +24 -0
  191. package/dist/date-range-picker/index.js +13 -0
  192. package/dist/description-list/index.js +2 -0
  193. package/dist/dialog/index.js +3 -0
  194. package/dist/empty-state/index.js +3 -0
  195. package/dist/error-summary/index.js +3 -0
  196. package/dist/fab/index.js +2 -0
  197. package/dist/file-upload/index.js +9 -0
  198. package/dist/filter-bar/index.js +8 -0
  199. package/dist/flow-run/index.js +14 -0
  200. package/dist/form/index.js +3 -0
  201. package/dist/gallery/index.js +2 -0
  202. package/dist/header/index.js +5 -0
  203. package/dist/hold-to-confirm/index.js +4 -0
  204. package/dist/index.d.ts +22883 -0
  205. package/dist/index.js +158 -0
  206. package/dist/infinite-list/index.js +2 -0
  207. package/dist/inline-edit/index.js +5 -0
  208. package/dist/input/index.js +5 -0
  209. package/dist/interactions/index.js +2 -0
  210. package/dist/invoice/index.js +10 -0
  211. package/dist/kbd/index.js +1 -0
  212. package/dist/keyboard/index.js +3 -0
  213. package/dist/list-row/index.js +7 -0
  214. package/dist/log-viewer/index.js +2 -0
  215. package/dist/map-view/index.js +3 -0
  216. package/dist/markdown/index.js +6 -0
  217. package/dist/metric/index.js +4 -0
  218. package/dist/money/index.js +3 -0
  219. package/dist/nav-drawer/index.js +2 -0
  220. package/dist/navigation/index.js +2 -0
  221. package/dist/notification-center/index.js +16 -0
  222. package/dist/number-roll/index.js +3 -0
  223. package/dist/one-handed/index.js +5 -0
  224. package/dist/os-surfaces/index.js +3 -0
  225. package/dist/otp/index.js +7 -0
  226. package/dist/page-header/index.js +1 -0
  227. package/dist/payment-method/index.js +12 -0
  228. package/dist/payment-status/index.js +16 -0
  229. package/dist/permission/index.js +2 -0
  230. package/dist/picker/index.js +7 -0
  231. package/dist/progress/index.js +5 -0
  232. package/dist/pull-to-refresh/index.js +4 -0
  233. package/dist/radio-group/index.js +6 -0
  234. package/dist/rating/index.js +2 -0
  235. package/dist/receivables/index.js +20 -0
  236. package/dist/refund/index.js +16 -0
  237. package/dist/save-status/index.js +3 -0
  238. package/dist/scroll-area/index.js +1 -0
  239. package/dist/search-bar/index.js +5 -0
  240. package/dist/segmented-control/index.js +5 -0
  241. package/dist/sheet/index.js +4 -0
  242. package/dist/skeleton/index.js +2 -0
  243. package/dist/slider/index.js +5 -0
  244. package/dist/spinner/index.js +2 -0
  245. package/dist/split-button/index.js +7 -0
  246. package/dist/spotlight/index.js +2 -0
  247. package/dist/stage-timeline/index.js +4 -0
  248. package/dist/status-dot/index.js +2 -0
  249. package/dist/stepper/index.js +6 -0
  250. package/dist/streaming-text/index.js +3 -0
  251. package/dist/survey/index.js +19 -0
  252. package/dist/switch/index.js +5 -0
  253. package/dist/system-state/index.js +4 -0
  254. package/dist/tab-bar/index.js +2 -0
  255. package/dist/tabs/index.js +2 -0
  256. package/dist/textarea/index.js +6 -0
  257. package/dist/thinking/index.js +2 -0
  258. package/dist/timeline/index.js +4 -0
  259. package/dist/toast/index.js +2 -0
  260. package/dist/toolbar/index.js +2 -0
  261. package/dist/tour/index.js +11 -0
  262. package/dist/truncation-notice/index.js +2 -0
  263. package/dist/update-notice/index.js +11 -0
  264. package/dist/video-player/index.js +2 -0
  265. package/dist/viewfinder/index.js +5 -0
  266. package/dist/widget/index.js +4 -0
  267. package/dist/wizard/index.js +6 -0
  268. package/package.json +76 -3
  269. package/README.md +0 -3
@@ -0,0 +1,1521 @@
1
+ import { Picker } from './chunk-NKB7BZYK.js';
2
+ import { EmptyState } from './chunk-LZLHELAG.js';
3
+ import { SegmentedControl } from './chunk-5YNTNN5A.js';
4
+ import { Sheet } from './chunk-ZVFE5QGU.js';
5
+ import { Spinner } from './chunk-O7AE6DAF.js';
6
+ import { Checkbox } from './chunk-JXJB2X6M.js';
7
+ import { Input } from './chunk-ITTYJZWX.js';
8
+ import { expandedProps } from './chunk-RQ3HASCC.js';
9
+ import { Button } from './chunk-3ARE4TES.js';
10
+ import { Icon } from './chunk-TKVFVF5D.js';
11
+ import * as React2 from 'react';
12
+ import { View, Text, Pressable, ScrollView, FlatList } from 'react-native';
13
+ import { createStyleSheet, useStyles, useLoamTheme } from '@btndprojects/loam-theme-native';
14
+ import { getValue, formatValue, resolveColumns, useGridState, buildRowModel, DENSITY_ROW_PX, AGGREGATION_LABELS } from '@btndprojects/loam-data-grid-core';
15
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
16
+
17
+ var OPERATOR_LABELS = {
18
+ contains: "contains",
19
+ notContains: "does not contain",
20
+ equals: "is exactly",
21
+ startsWith: "starts with",
22
+ endsWith: "ends with",
23
+ isEmpty: "is empty",
24
+ isNotEmpty: "is not empty",
25
+ is: "is",
26
+ isNot: "is not",
27
+ isAnyOf: "is any of",
28
+ eq: "is",
29
+ neq: "is not",
30
+ gt: "is more than",
31
+ gte: "is at least",
32
+ lt: "is less than",
33
+ lte: "is at most",
34
+ between: "is between",
35
+ before: "is before",
36
+ after: "is after",
37
+ hasAny: "has any of",
38
+ hasAll: "has all of"
39
+ };
40
+ var NULLARY = ["isEmpty", "isNotEmpty"];
41
+ function SortSheet({
42
+ open,
43
+ onClose,
44
+ columns,
45
+ sorting,
46
+ onToggle,
47
+ onClear
48
+ }) {
49
+ const { styles } = useStyles(sheet);
50
+ const theme = useLoamTheme();
51
+ const sortable = columns.filter((c) => c.sortable);
52
+ return /* @__PURE__ */ jsx(Sheet, { open, onClose, title: "Sort", children: /* @__PURE__ */ jsxs(View, { style: styles.body, children: [
53
+ sortable.length === 0 ? /* @__PURE__ */ jsx(Text, { style: styles.hint, children: "No column in this grid can be sorted." }) : null,
54
+ sortable.map((column) => {
55
+ const at = sorting.findIndex((s) => s.id === column.id);
56
+ const spec = at >= 0 ? sorting[at] : void 0;
57
+ return /* @__PURE__ */ jsxs(
58
+ Pressable,
59
+ {
60
+ accessibilityRole: "button",
61
+ accessibilityLabel: column.header,
62
+ accessibilityValue: {
63
+ text: spec ? spec.desc ? "descending" : "ascending" : "not sorted"
64
+ },
65
+ accessibilityHint: "Double tap to cycle ascending, descending, off",
66
+ onPress: () => onToggle(column.id),
67
+ style: ({ pressed }) => [styles.row, pressed && styles.pressed],
68
+ children: [
69
+ /* @__PURE__ */ jsx(Text, { style: styles.rowLabel, children: column.header }),
70
+ spec ? /* @__PURE__ */ jsxs(View, { style: styles.rowTrail, children: [
71
+ sorting.length > 1 ? /* @__PURE__ */ jsx(Text, { style: styles.rank, children: at + 1 }) : null,
72
+ /* @__PURE__ */ jsx(
73
+ Icon,
74
+ {
75
+ name: spec.desc ? "arrow-down" : "arrow-up",
76
+ size: theme.icon.sm,
77
+ color: theme.color.accentOnSoft
78
+ }
79
+ )
80
+ ] }) : null
81
+ ]
82
+ },
83
+ column.id
84
+ );
85
+ }),
86
+ sorting.length > 0 ? /* @__PURE__ */ jsx(Button, { variant: "ghost", onPress: onClear, leadingIcon: "x", children: "Clear sorting" }) : null
87
+ ] }) });
88
+ }
89
+ function FilterSheet({
90
+ open,
91
+ onClose,
92
+ columns,
93
+ rows,
94
+ ctx,
95
+ filters,
96
+ logic,
97
+ onChange
98
+ }) {
99
+ const { styles } = useStyles(sheet);
100
+ const filterable = columns.filter((c) => c.filterable);
101
+ const update = (index, patch) => {
102
+ const next = filters.map((f, i) => i === index ? { ...f, ...patch } : f);
103
+ onChange(next, logic);
104
+ };
105
+ const remove = (index) => {
106
+ onChange(filters.filter((_, i) => i !== index), logic);
107
+ };
108
+ const add = () => {
109
+ const column = filterable[0];
110
+ if (!column) return;
111
+ onChange([...filters, { id: column.id, op: column.operators[0], value: "" }], logic);
112
+ };
113
+ return /* @__PURE__ */ jsx(Sheet, { open, onClose, title: "Filter", detents: ["half", "full"], children: /* @__PURE__ */ jsxs(ScrollView, { contentContainerStyle: styles.body, children: [
114
+ filters.length > 1 ? /* @__PURE__ */ jsxs(View, { style: styles.logicRow, children: [
115
+ /* @__PURE__ */ jsx(Text, { style: styles.hint, children: "Match" }),
116
+ /* @__PURE__ */ jsx(
117
+ SegmentedControl,
118
+ {
119
+ size: "sm",
120
+ value: logic,
121
+ onValueChange: (next) => onChange([...filters], next),
122
+ items: [
123
+ { value: "and", label: "All" },
124
+ { value: "or", label: "Any" }
125
+ ]
126
+ }
127
+ )
128
+ ] }) : null,
129
+ filters.map((filter, index) => {
130
+ const column = columns.find((c) => c.id === filter.id);
131
+ if (!column) return null;
132
+ const operators = column.operators;
133
+ const nullary = NULLARY.includes(filter.op);
134
+ const values = column.type === "enum" ? enumValues(column, rows, ctx) : null;
135
+ return /* @__PURE__ */ jsxs(View, { style: styles.condition, children: [
136
+ /* @__PURE__ */ jsxs(View, { style: styles.conditionHead, children: [
137
+ /* @__PURE__ */ jsx(Text, { style: styles.rowLabel, children: column.header }),
138
+ /* @__PURE__ */ jsx(
139
+ Pressable,
140
+ {
141
+ accessibilityRole: "button",
142
+ accessibilityLabel: `Remove the filter on ${column.header}`,
143
+ onPress: () => remove(index),
144
+ style: ({ pressed }) => [styles.iconButton, pressed && styles.pressed],
145
+ children: /* @__PURE__ */ jsx(Icon, { name: "x", size: 16 })
146
+ }
147
+ )
148
+ ] }),
149
+ /* @__PURE__ */ jsx(
150
+ Picker,
151
+ {
152
+ title: "Column",
153
+ accessibilityLabel: `Filter column, currently ${column.header}`,
154
+ value: filter.id,
155
+ items: filterable.map((c) => ({ value: c.id, label: c.header })),
156
+ onValueChange: (id) => {
157
+ const next = columns.find((c) => c.id === id);
158
+ update(index, { id, op: next?.operators[0] ?? "contains", value: "" });
159
+ },
160
+ size: "sm"
161
+ }
162
+ ),
163
+ /* @__PURE__ */ jsx(
164
+ Picker,
165
+ {
166
+ title: "Condition",
167
+ accessibilityLabel: `Condition, currently ${OPERATOR_LABELS[filter.op]}`,
168
+ value: filter.op,
169
+ items: operators.map((op) => ({ value: op, label: OPERATOR_LABELS[op] })),
170
+ onValueChange: (op) => update(index, { op }),
171
+ size: "sm"
172
+ }
173
+ ),
174
+ nullary ? null : values ? /* @__PURE__ */ jsx(View, { style: styles.chips, children: values.map((value) => {
175
+ const active = String(filter.value ?? "") === value;
176
+ return /* @__PURE__ */ jsx(
177
+ Pressable,
178
+ {
179
+ accessibilityRole: "button",
180
+ accessibilityState: { selected: active },
181
+ accessibilityLabel: value,
182
+ onPress: () => update(index, { value: active ? "" : value }),
183
+ style: ({ pressed }) => [
184
+ styles.chip,
185
+ active && styles.chipOn,
186
+ pressed && styles.pressed
187
+ ],
188
+ children: /* @__PURE__ */ jsx(Text, { style: [styles.chipText, active && styles.chipTextOn], children: value })
189
+ },
190
+ value
191
+ );
192
+ }) }) : /* @__PURE__ */ jsxs(View, { style: styles.operands, children: [
193
+ /* @__PURE__ */ jsx(
194
+ Input,
195
+ {
196
+ size: "sm",
197
+ value: String(filter.value ?? ""),
198
+ onChangeText: (text) => update(index, { value: text }),
199
+ placeholder: "Value",
200
+ accessibilityLabel: `Value for the filter on ${column.header}`,
201
+ style: styles.operand
202
+ }
203
+ ),
204
+ filter.op === "between" ? /* @__PURE__ */ jsx(
205
+ Input,
206
+ {
207
+ size: "sm",
208
+ value: String(filter.value2 ?? ""),
209
+ onChangeText: (text) => update(index, { value2: text }),
210
+ placeholder: "and",
211
+ accessibilityLabel: `Upper bound for the filter on ${column.header}`,
212
+ style: styles.operand
213
+ }
214
+ ) : null
215
+ ] })
216
+ ] }, `${filter.id}-${index}`);
217
+ }),
218
+ /* @__PURE__ */ jsx(Button, { variant: "tonal", leadingIcon: "plus", onPress: add, fullWidth: true, children: "Add a condition" }),
219
+ filters.length > 0 ? /* @__PURE__ */ jsx(Button, { variant: "ghost", leadingIcon: "x", onPress: () => onChange([], logic), fullWidth: true, children: "Clear all filters" }) : null
220
+ ] }) });
221
+ }
222
+ function enumValues(column, rows, ctx) {
223
+ if (column.options?.length) return [...column.options];
224
+ const seen = /* @__PURE__ */ new Set();
225
+ for (const row of rows) {
226
+ const value = getValue(ctx, row, column.id);
227
+ if (value == null || value === "") continue;
228
+ seen.add(String(value));
229
+ if (seen.size >= 24) break;
230
+ }
231
+ return [...seen].sort();
232
+ }
233
+ function DisplaySheet({
234
+ open,
235
+ onClose,
236
+ columns,
237
+ state,
238
+ layout,
239
+ table,
240
+ onSetState
241
+ }) {
242
+ const { styles } = useStyles(sheet);
243
+ const groupable = columns.filter((c) => c.groupable);
244
+ const visible = state.columnOrder.filter((id) => state.columnVisibility[id] !== false);
245
+ return /* @__PURE__ */ jsx(Sheet, { open, onClose, title: "Display", detents: ["half", "full"], children: /* @__PURE__ */ jsxs(ScrollView, { contentContainerStyle: styles.body, children: [
246
+ /* @__PURE__ */ jsx(Text, { style: styles.section, children: "Row height" }),
247
+ /* @__PURE__ */ jsx(
248
+ SegmentedControl,
249
+ {
250
+ value: state.density,
251
+ onValueChange: (next) => onSetState({ density: next }, "density"),
252
+ items: [
253
+ { value: "compact", label: "Compact" },
254
+ { value: "standard", label: "Standard" },
255
+ { value: "comfortable", label: "Roomy" }
256
+ ]
257
+ }
258
+ ),
259
+ /* @__PURE__ */ jsx(Text, { style: styles.hint, children: "Every row opens a record, so the tiers are floored at the tap minimum \u2014 on a narrow screen compact and standard are the same height." }),
260
+ table ? /* @__PURE__ */ jsxs(
261
+ Pressable,
262
+ {
263
+ accessibilityRole: "checkbox",
264
+ accessibilityState: { checked: state.zebra },
265
+ accessibilityLabel: "Shade alternating rows",
266
+ onPress: () => onSetState({ zebra: !state.zebra }, "display"),
267
+ style: ({ pressed }) => [styles.row, pressed && styles.pressed],
268
+ children: [
269
+ /* @__PURE__ */ jsx(Text, { style: styles.rowLabel, children: "Shade alternating rows" }),
270
+ /* @__PURE__ */ jsx(StateBox, { on: state.zebra })
271
+ ]
272
+ }
273
+ ) : /* @__PURE__ */ jsx(Text, { style: styles.hint, children: layout === "cards" ? "This grid is set to cards." : 'This grid is showing cards because its container is narrow. Widen it, or set layout="table".' }),
274
+ groupable.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
275
+ /* @__PURE__ */ jsx(Text, { style: styles.section, children: "Group by" }),
276
+ /* @__PURE__ */ jsxs(View, { style: styles.chips, children: [
277
+ /* @__PURE__ */ jsx(
278
+ Pressable,
279
+ {
280
+ accessibilityRole: "button",
281
+ accessibilityState: { selected: state.grouping.length === 0 },
282
+ accessibilityLabel: "No grouping",
283
+ onPress: () => onSetState({ grouping: [] }, "group"),
284
+ style: ({ pressed }) => [
285
+ styles.chip,
286
+ state.grouping.length === 0 && styles.chipOn,
287
+ pressed && styles.pressed
288
+ ],
289
+ children: /* @__PURE__ */ jsx(
290
+ Text,
291
+ {
292
+ style: [styles.chipText, state.grouping.length === 0 && styles.chipTextOn],
293
+ children: "None"
294
+ }
295
+ )
296
+ }
297
+ ),
298
+ groupable.map((column) => {
299
+ const active = state.grouping[0] === column.id;
300
+ return /* @__PURE__ */ jsx(
301
+ Pressable,
302
+ {
303
+ accessibilityRole: "button",
304
+ accessibilityState: { selected: active },
305
+ accessibilityLabel: `Group by ${column.header}`,
306
+ onPress: () => onSetState({ grouping: active ? [] : [column.id] }, "group"),
307
+ style: ({ pressed }) => [
308
+ styles.chip,
309
+ active && styles.chipOn,
310
+ pressed && styles.pressed
311
+ ],
312
+ children: /* @__PURE__ */ jsx(Text, { style: [styles.chipText, active && styles.chipTextOn], children: column.header })
313
+ },
314
+ column.id
315
+ );
316
+ })
317
+ ] })
318
+ ] }) : null,
319
+ /* @__PURE__ */ jsx(Text, { style: styles.section, children: "Columns" }),
320
+ columns.map((column) => {
321
+ const on = state.columnVisibility[column.id] !== false;
322
+ const last = on && visible.length === 1;
323
+ return /* @__PURE__ */ jsxs(
324
+ Pressable,
325
+ {
326
+ accessibilityRole: "checkbox",
327
+ accessibilityState: { checked: on, disabled: !column.hideable || last },
328
+ accessibilityLabel: column.header,
329
+ accessibilityHint: last ? "The last visible column cannot be hidden" : void 0,
330
+ disabled: !column.hideable || last,
331
+ onPress: () => onSetState(
332
+ { columnVisibility: { ...state.columnVisibility, [column.id]: !on } },
333
+ "visibility"
334
+ ),
335
+ style: ({ pressed }) => [
336
+ styles.row,
337
+ (!column.hideable || last) && styles.rowDisabled,
338
+ pressed && styles.pressed
339
+ ],
340
+ children: [
341
+ /* @__PURE__ */ jsx(Text, { style: styles.rowLabel, children: column.header }),
342
+ /* @__PURE__ */ jsx(StateBox, { on, disabled: !column.hideable || last })
343
+ ]
344
+ },
345
+ column.id
346
+ );
347
+ })
348
+ ] }) });
349
+ }
350
+ function StateBox({ on, disabled }) {
351
+ return /* @__PURE__ */ jsx(View, { pointerEvents: "none", ...{ "aria-hidden": true }, children: /* @__PURE__ */ jsx(Checkbox, { checked: on, disabled, size: "sm" }) });
352
+ }
353
+ function DetailSheet({
354
+ row,
355
+ columns,
356
+ ctx,
357
+ title,
358
+ onClose
359
+ }) {
360
+ const { styles } = useStyles(sheet);
361
+ if (!row) return null;
362
+ return /* @__PURE__ */ jsx(Sheet, { open: true, onClose, title, detents: ["half", "full"], children: /* @__PURE__ */ jsx(ScrollView, { contentContainerStyle: styles.body, children: columns.map((column) => /* @__PURE__ */ jsxs(View, { style: styles.detailRow, children: [
363
+ /* @__PURE__ */ jsxs(Text, { style: styles.detailLabel, children: [
364
+ column.header,
365
+ column.headerUnit ? ` (${column.headerUnit})` : ""
366
+ ] }),
367
+ /* @__PURE__ */ jsx(Text, { style: [styles.detailValue, column.mono && styles.mono], children: formatValue(ctx, column, getValue(ctx, row, column.id), row) })
368
+ ] }, column.id)) }) });
369
+ }
370
+ var sheet = createStyleSheet((theme) => ({
371
+ body: { gap: theme.space.s3, paddingBottom: theme.space.s4 },
372
+ section: {
373
+ fontFamily: theme.font.sans,
374
+ fontSize: theme.type.caption1.fontSize,
375
+ lineHeight: theme.type.caption1.lineHeight,
376
+ fontWeight: theme.fontWeight.bold,
377
+ color: theme.color.text2,
378
+ marginTop: theme.space.s2
379
+ },
380
+ hint: {
381
+ fontFamily: theme.font.sans,
382
+ fontSize: theme.type.caption1.fontSize,
383
+ lineHeight: theme.type.caption1.lineHeight,
384
+ color: theme.color.text2
385
+ },
386
+ row: {
387
+ flexDirection: "row",
388
+ alignItems: "center",
389
+ justifyContent: "space-between",
390
+ gap: theme.space.s3,
391
+ minHeight: theme.tapMin,
392
+ paddingHorizontal: theme.space.s2,
393
+ borderRadius: theme.radius.md,
394
+ backgroundColor: theme.color.surface2
395
+ },
396
+ rowDisabled: { opacity: theme.state.disabledOpacity },
397
+ rowLabel: {
398
+ flex: 1,
399
+ fontFamily: theme.font.sans,
400
+ fontSize: theme.type.body.fontSize,
401
+ lineHeight: theme.type.body.lineHeight,
402
+ color: theme.color.text
403
+ },
404
+ rowTrail: { flexDirection: "row", alignItems: "center", gap: theme.space.s1 },
405
+ rank: {
406
+ fontFamily: theme.font.mono,
407
+ fontSize: theme.type.caption2.fontSize,
408
+ color: theme.color.accentOnSoft
409
+ },
410
+ logicRow: { flexDirection: "row", alignItems: "center", gap: theme.space.s2 },
411
+ condition: {
412
+ gap: theme.space.s2,
413
+ padding: theme.space.s3,
414
+ borderRadius: theme.radius.lg,
415
+ borderWidth: theme.borderWidth.base,
416
+ borderColor: theme.color.border,
417
+ backgroundColor: theme.color.surface2
418
+ },
419
+ conditionHead: { flexDirection: "row", alignItems: "center", gap: theme.space.s2 },
420
+ iconButton: {
421
+ minWidth: theme.tapMin,
422
+ minHeight: theme.tapMin,
423
+ alignItems: "center",
424
+ justifyContent: "center"
425
+ },
426
+ operands: { flexDirection: "row", gap: theme.space.s2 },
427
+ operand: { flex: 1 },
428
+ chips: { flexDirection: "row", flexWrap: "wrap", gap: theme.space.s1 },
429
+ chip: {
430
+ minHeight: theme.tapMin,
431
+ justifyContent: "center",
432
+ paddingHorizontal: theme.space.s3,
433
+ borderRadius: theme.radius.full,
434
+ borderWidth: theme.borderWidth.base,
435
+ borderColor: theme.color.border,
436
+ backgroundColor: theme.color.surface
437
+ },
438
+ chipOn: {
439
+ borderColor: theme.edge.accent,
440
+ backgroundColor: theme.color.accentSoft
441
+ },
442
+ chipText: {
443
+ fontFamily: theme.font.sans,
444
+ fontSize: theme.type.footnote.fontSize,
445
+ lineHeight: theme.type.footnote.lineHeight,
446
+ color: theme.color.text
447
+ },
448
+ chipTextOn: { color: theme.color.accentOnSoft, fontWeight: theme.fontWeight.bold },
449
+ detailRow: {
450
+ gap: theme.space[2],
451
+ paddingBottom: theme.space.s2,
452
+ borderBottomWidth: theme.borderWidth.hair,
453
+ borderBottomColor: theme.color.border
454
+ },
455
+ detailLabel: {
456
+ fontFamily: theme.font.sans,
457
+ fontSize: theme.type.caption1.fontSize,
458
+ lineHeight: theme.type.caption1.lineHeight,
459
+ color: theme.color.text2
460
+ },
461
+ detailValue: {
462
+ fontFamily: theme.font.sans,
463
+ fontSize: theme.type.body.fontSize,
464
+ lineHeight: theme.type.body.lineHeight,
465
+ color: theme.color.text
466
+ },
467
+ mono: { fontFamily: theme.font.mono },
468
+ pressed: { opacity: theme.state.pressedOpacity }
469
+ }));
470
+ var TABLE_MIN_WIDTH = 680;
471
+ var CARD_FIELD_LIMIT = 4;
472
+ function DataGrid({
473
+ rows,
474
+ columns,
475
+ label,
476
+ getRowId,
477
+ primaryColumn,
478
+ layout = "auto",
479
+ density,
480
+ initialState,
481
+ state: controlled,
482
+ onStateChange,
483
+ gridId,
484
+ storage,
485
+ selectionMode = "none",
486
+ bulkActions,
487
+ onRowPress,
488
+ searchable = true,
489
+ status = "ready",
490
+ onRetry,
491
+ locale,
492
+ currency = "USD",
493
+ now,
494
+ style
495
+ }) {
496
+ const { styles } = useStyles(sheet2);
497
+ const theme = useLoamTheme();
498
+ const resolved = React2.useMemo(() => resolveColumns(columns), [columns]);
499
+ const identify = React2.useMemo(
500
+ () => getRowId ?? ((row) => String(row.id)),
501
+ [getRowId]
502
+ );
503
+ const seed = React2.useMemo(
504
+ () => density ? { ...initialState, density } : initialState ?? {},
505
+ // `density` is a starting tier, not a controlled slice — a caller that wants
506
+ // to own it passes `state={{ density }}`, which is the one write path.
507
+ // eslint-disable-next-line react-hooks/exhaustive-deps
508
+ []
509
+ );
510
+ const { state, setState } = useGridState({
511
+ columns: resolved,
512
+ initialState: seed,
513
+ state: controlled,
514
+ onStateChange,
515
+ gridId,
516
+ storage
517
+ });
518
+ const visibleColumns = React2.useMemo(
519
+ () => state.columnOrder.map((id) => resolved.find((c) => c.id === id)).filter((c) => !!c && state.columnVisibility[c.id] !== false),
520
+ [resolved, state.columnOrder, state.columnVisibility]
521
+ );
522
+ const ctx = React2.useMemo(
523
+ () => ({
524
+ columnsById: new Map(resolved.map((c) => [c.id, c])),
525
+ getRowId: identify,
526
+ formulas: state.formulas,
527
+ locale,
528
+ currency,
529
+ now: now ?? Date.now()
530
+ }),
531
+ [resolved, identify, state.formulas, locale, currency, now]
532
+ );
533
+ const model = React2.useMemo(
534
+ () => buildRowModel({
535
+ rows,
536
+ visibleColumns,
537
+ state,
538
+ ctx,
539
+ // The native surface has no inline detail row — a record opens in a
540
+ // sheet, which is the platform's own answer to "show me all of it".
541
+ hasDetail: () => false
542
+ }),
543
+ [rows, visibleColumns, state, ctx]
544
+ );
545
+ const [width, setWidth] = React2.useState(0);
546
+ const onLayout = (event) => {
547
+ const next = event.nativeEvent.layout.width;
548
+ if (next > 0 && Math.abs(next - width) > 1) setWidth(next);
549
+ };
550
+ const table = layout === "table" || layout === "auto" && width >= TABLE_MIN_WIDTH;
551
+ const rowHeight = Math.max(DENSITY_ROW_PX[state.density], theme.tapMin);
552
+ const [message, setMessage] = React2.useState("");
553
+ const toggleSort = React2.useCallback(
554
+ (colId) => {
555
+ const column = resolved.find((c) => c.id === colId);
556
+ if (!column?.sortable) return;
557
+ const at = state.sorting.findIndex((s) => s.id === colId);
558
+ const next = [...state.sorting];
559
+ if (at < 0) {
560
+ next.splice(0, next.length, { id: colId, desc: false });
561
+ setMessage(`Sorted by ${column.header}, ascending.`);
562
+ } else if (!next[at].desc) {
563
+ next[at] = { id: colId, desc: true };
564
+ setMessage(`Sorted by ${column.header}, descending.`);
565
+ } else {
566
+ next.splice(at, 1);
567
+ setMessage(`Sorting cleared on ${column.header}.`);
568
+ }
569
+ setState({ sorting: next }, "sort");
570
+ },
571
+ [resolved, state.sorting, setState]
572
+ );
573
+ const selectedIds = React2.useMemo(
574
+ () => Object.keys(state.rowSelection).filter((id) => state.rowSelection[id]),
575
+ [state.rowSelection]
576
+ );
577
+ const selectedRows = React2.useMemo(
578
+ () => model.filteredRows.filter((row) => state.rowSelection[identify(row)]),
579
+ [model.filteredRows, state.rowSelection, identify]
580
+ );
581
+ const toggleRow = React2.useCallback(
582
+ (row) => {
583
+ const id = identify(row);
584
+ const next = selectionMode === "single" ? state.rowSelection[id] ? {} : { [id]: true } : { ...state.rowSelection };
585
+ if (selectionMode === "multi") {
586
+ if (next[id]) delete next[id];
587
+ else next[id] = true;
588
+ }
589
+ setState({ rowSelection: next }, "select");
590
+ },
591
+ [identify, selectionMode, state.rowSelection, setState]
592
+ );
593
+ const clearSelection = React2.useCallback(() => {
594
+ setState({ rowSelection: {} }, "select");
595
+ }, [setState]);
596
+ const toggleGroup = React2.useCallback(
597
+ (key) => {
598
+ const next = { ...state.expanded };
599
+ if (next[key]) delete next[key];
600
+ else next[key] = true;
601
+ setState({ expanded: next }, "expand");
602
+ },
603
+ [state.expanded, setState]
604
+ );
605
+ const [openSheet, setOpenSheet] = React2.useState(null);
606
+ const [detail, setDetail] = React2.useState(null);
607
+ const openRow = React2.useCallback(
608
+ (row) => {
609
+ if (onRowPress) onRowPress(row);
610
+ else setDetail(row);
611
+ },
612
+ [onRowPress]
613
+ );
614
+ const [focusIndex, setFocusIndex] = React2.useState(0);
615
+ const leaves = React2.useMemo(
616
+ () => model.nodes.filter((n) => n.kind === "leaf"),
617
+ [model.nodes]
618
+ );
619
+ const onGridKey = (event) => {
620
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
621
+ event.preventDefault?.();
622
+ setFocusIndex((i) => Math.max(0, Math.min(leaves.length - 1, i + (event.key === "ArrowDown" ? 1 : -1))));
623
+ } else if (event.key === "Enter" && leaves[focusIndex]) {
624
+ event.preventDefault?.();
625
+ openRow(leaves[focusIndex].row);
626
+ }
627
+ };
628
+ const keyProps = { onKeyDown: onGridKey };
629
+ const primary = React2.useMemo(
630
+ () => visibleColumns.find((c) => c.id === primaryColumn) ?? visibleColumns[0],
631
+ [visibleColumns, primaryColumn]
632
+ );
633
+ const filterCount = state.columnFilters.length + (state.globalFilter ? 1 : 0);
634
+ const busy = status === "loading";
635
+ const surface = status === "error" || status === "forbidden" || status === "offline" ? /* @__PURE__ */ jsx(ScrollView, { contentContainerStyle: styles.stateScroll, children: /* @__PURE__ */ jsx(
636
+ EmptyState,
637
+ {
638
+ variant: status === "forbidden" ? "no-permission" : status === "offline" ? "offline" : "error",
639
+ title: status === "forbidden" ? "You do not have access to these rows" : status === "offline" ? "You are offline" : "These rows could not be loaded",
640
+ description: status === "offline" ? "The rows below may be out of date until the connection is back." : void 0,
641
+ actionLabel: onRetry ? "Try again" : void 0,
642
+ onAction: onRetry
643
+ }
644
+ ) }) : busy && model.nodes.length === 0 ? /* @__PURE__ */ jsx(View, { style: styles.centre, accessibilityLabel: "Loading rows", role: "status", children: /* @__PURE__ */ jsx(Spinner, { size: "lg" }) }) : model.nodes.length === 0 ? /* @__PURE__ */ jsx(ScrollView, { contentContainerStyle: styles.stateScroll, children: /* @__PURE__ */ jsx(
645
+ EmptyState,
646
+ {
647
+ variant: filterCount > 0 ? "no-results" : "empty",
648
+ title: filterCount > 0 ? "No rows match" : "Nothing here yet",
649
+ description: filterCount > 0 ? "Clear a filter or widen the search to see rows again." : void 0,
650
+ actionLabel: filterCount > 0 ? "Clear filters" : void 0,
651
+ onAction: filterCount > 0 ? () => setState({ columnFilters: [], globalFilter: "" }, "filter") : void 0
652
+ }
653
+ ) }) : table ? /* @__PURE__ */ jsx(
654
+ TableSurface,
655
+ {
656
+ model,
657
+ columns: visibleColumns,
658
+ ctx,
659
+ state,
660
+ rowHeight,
661
+ selectionMode,
662
+ focusIndex,
663
+ leaves,
664
+ onToggleSort: toggleSort,
665
+ onToggleRow: toggleRow,
666
+ onToggleGroup: toggleGroup,
667
+ onOpenRow: openRow
668
+ }
669
+ ) : /* @__PURE__ */ jsx(
670
+ CardSurface,
671
+ {
672
+ model,
673
+ columns: visibleColumns,
674
+ primary,
675
+ ctx,
676
+ state,
677
+ selectionMode,
678
+ onToggleRow: toggleRow,
679
+ onToggleGroup: toggleGroup,
680
+ onOpenRow: openRow
681
+ }
682
+ );
683
+ return /* @__PURE__ */ jsxs(
684
+ View,
685
+ {
686
+ role: "group",
687
+ accessibilityLabel: label,
688
+ onLayout,
689
+ style: [styles.root, style],
690
+ ...keyProps,
691
+ children: [
692
+ /* @__PURE__ */ jsx(
693
+ GridToolbar,
694
+ {
695
+ label,
696
+ searchable,
697
+ query: state.globalFilter,
698
+ onQuery: (text) => setState({ globalFilter: text, pagination: { ...state.pagination, pageIndex: 0 } }, "quickFilter"),
699
+ sortCount: state.sorting.length,
700
+ filterCount: state.columnFilters.length,
701
+ groupCount: state.grouping.length,
702
+ onOpen: setOpenSheet
703
+ }
704
+ ),
705
+ /* @__PURE__ */ jsx(
706
+ CountLine,
707
+ {
708
+ shown: model.filteredCount,
709
+ total: model.totalCount,
710
+ selected: selectedIds.length,
711
+ busy
712
+ }
713
+ ),
714
+ /* @__PURE__ */ jsx(View, { style: styles.body, children: surface }),
715
+ model.nodes.length > 0 ? /* @__PURE__ */ jsx(FooterStrip, { model, columns: visibleColumns, ctx, state }) : null,
716
+ selectedIds.length > 0 ? /* @__PURE__ */ jsx(
717
+ BulkBar,
718
+ {
719
+ count: selectedIds.length,
720
+ actions: bulkActions,
721
+ rows: selectedRows,
722
+ onClear: clearSelection
723
+ }
724
+ ) : null,
725
+ /* @__PURE__ */ jsx(
726
+ SortSheet,
727
+ {
728
+ open: openSheet === "sort",
729
+ onClose: () => setOpenSheet(null),
730
+ columns: resolved,
731
+ sorting: state.sorting,
732
+ onToggle: toggleSort,
733
+ onClear: () => setState({ sorting: [] }, "sort")
734
+ }
735
+ ),
736
+ /* @__PURE__ */ jsx(
737
+ FilterSheet,
738
+ {
739
+ open: openSheet === "filter",
740
+ onClose: () => setOpenSheet(null),
741
+ columns: resolved,
742
+ rows,
743
+ ctx,
744
+ filters: state.columnFilters,
745
+ logic: state.filterLogic,
746
+ onChange: (filters, logic) => setState({ columnFilters: filters, filterLogic: logic }, "filter")
747
+ }
748
+ ),
749
+ /* @__PURE__ */ jsx(
750
+ DisplaySheet,
751
+ {
752
+ open: openSheet === "display",
753
+ onClose: () => setOpenSheet(null),
754
+ columns: resolved,
755
+ state,
756
+ layout,
757
+ table,
758
+ onSetState: setState
759
+ }
760
+ ),
761
+ /* @__PURE__ */ jsx(
762
+ DetailSheet,
763
+ {
764
+ row: detail,
765
+ columns: visibleColumns,
766
+ ctx,
767
+ title: detail && primary ? formatValue(ctx, primary, getValue(ctx, detail, primary.id), detail) : "",
768
+ onClose: () => setDetail(null)
769
+ }
770
+ ),
771
+ /* @__PURE__ */ jsx(
772
+ View,
773
+ {
774
+ role: "status",
775
+ style: styles.srOnly,
776
+ accessibilityLabel: message,
777
+ ...{ "aria-live": "polite" },
778
+ children: /* @__PURE__ */ jsx(Text, { style: styles.srOnly, children: message })
779
+ }
780
+ )
781
+ ]
782
+ }
783
+ );
784
+ }
785
+ function GridToolbar({
786
+ label,
787
+ searchable,
788
+ query,
789
+ onQuery,
790
+ sortCount,
791
+ filterCount,
792
+ groupCount,
793
+ onOpen
794
+ }) {
795
+ const { styles } = useStyles(sheet2);
796
+ return /* @__PURE__ */ jsxs(View, { style: styles.toolbar, children: [
797
+ searchable ? /* @__PURE__ */ jsx(
798
+ Input,
799
+ {
800
+ value: query,
801
+ onChangeText: onQuery,
802
+ placeholder: `Search ${label.toLowerCase()}`,
803
+ accessibilityLabel: `Search ${label}`,
804
+ leadingIcon: "search",
805
+ size: "sm",
806
+ style: styles.search
807
+ }
808
+ ) : null,
809
+ /* @__PURE__ */ jsxs(View, { style: styles.toolbarButtons, children: [
810
+ /* @__PURE__ */ jsx(ToolPill, { icon: "arrow-down", label: "Sort", count: sortCount, onPress: () => onOpen("sort") }),
811
+ /* @__PURE__ */ jsx(ToolPill, { icon: "filter", label: "Filter", count: filterCount, onPress: () => onOpen("filter") }),
812
+ /* @__PURE__ */ jsx(
813
+ ToolPill,
814
+ {
815
+ icon: "settings",
816
+ label: "Display",
817
+ count: groupCount,
818
+ onPress: () => onOpen("display")
819
+ }
820
+ )
821
+ ] })
822
+ ] });
823
+ }
824
+ function ToolPill({
825
+ icon,
826
+ label,
827
+ count,
828
+ onPress
829
+ }) {
830
+ const { styles } = useStyles(sheet2);
831
+ const theme = useLoamTheme();
832
+ return /* @__PURE__ */ jsxs(
833
+ Pressable,
834
+ {
835
+ accessibilityRole: "button",
836
+ accessibilityLabel: count > 0 ? `${label}, ${count} active` : label,
837
+ onPress,
838
+ style: ({ pressed }) => [styles.toolPill, pressed && styles.pressed],
839
+ children: [
840
+ /* @__PURE__ */ jsx(Icon, { name: icon, size: theme.icon.sm, color: theme.color.text2 }),
841
+ /* @__PURE__ */ jsx(Text, { style: styles.toolPillText, children: label }),
842
+ count > 0 ? /* @__PURE__ */ jsx(View, { style: styles.toolCount, children: /* @__PURE__ */ jsx(Text, { style: styles.toolCountText, children: count }) }) : null
843
+ ]
844
+ }
845
+ );
846
+ }
847
+ function CountLine({
848
+ shown,
849
+ total,
850
+ selected,
851
+ busy
852
+ }) {
853
+ const { styles } = useStyles(sheet2);
854
+ const filtered = shown !== total;
855
+ return /* @__PURE__ */ jsxs(View, { style: styles.countLine, children: [
856
+ /* @__PURE__ */ jsxs(Text, { style: styles.countText, children: [
857
+ filtered ? `${shown} of ${total} rows` : `${total} rows`,
858
+ selected > 0 ? ` \xB7 ${selected} selected` : ""
859
+ ] }),
860
+ busy ? /* @__PURE__ */ jsx(Spinner, { size: "sm" }) : null
861
+ ] });
862
+ }
863
+ function TableSurface({
864
+ model,
865
+ columns,
866
+ ctx,
867
+ state,
868
+ rowHeight,
869
+ selectionMode,
870
+ focusIndex,
871
+ leaves,
872
+ onToggleSort,
873
+ onToggleRow,
874
+ onToggleGroup,
875
+ onOpenRow
876
+ }) {
877
+ const { styles } = useStyles(sheet2);
878
+ const theme = useLoamTheme();
879
+ const selectWidth = selectionMode === "none" ? 0 : theme.tapMin;
880
+ const widths = columns.map((c) => state.columnSizing[c.id] ?? c.size);
881
+ const total = widths.reduce((sum, w) => sum + w, 0) + selectWidth;
882
+ const renderNode = ({ item, index }) => {
883
+ if (item.kind === "group") {
884
+ return /* @__PURE__ */ jsx(
885
+ GroupRow,
886
+ {
887
+ node: item,
888
+ columns,
889
+ ctx,
890
+ width: total,
891
+ height: rowHeight,
892
+ inTable: true,
893
+ onToggle: onToggleGroup
894
+ }
895
+ );
896
+ }
897
+ if (item.kind === "detail") return /* @__PURE__ */ jsx(View, { style: { width: total } });
898
+ const rowId = ctx.getRowId(item.row);
899
+ const selected = state.rowSelection[rowId] === true;
900
+ const focused = leaves[focusIndex]?.id === item.id;
901
+ const name = rowLabel(ctx, columns, item.row);
902
+ return (
903
+ // The checkbox is a **sibling** of the pressable body, never a child of it.
904
+ // A control inside a control is `nested-interactive`, and it is not a
905
+ // technicality: a screen reader user lands on the row, is told it is a
906
+ // button, and has no way to reach the checkbox inside it.
907
+ /* @__PURE__ */ jsxs(
908
+ View,
909
+ {
910
+ style: [
911
+ styles.tableRow,
912
+ { width: total, minHeight: rowHeight },
913
+ state.zebra && index % 2 === 1 && styles.zebra,
914
+ selected && styles.selectedRow,
915
+ focused && styles.focusedRow
916
+ ],
917
+ children: [
918
+ selectionMode === "none" ? null : /* @__PURE__ */ jsx(View, { style: [styles.selectCell, { width: selectWidth }], children: /* @__PURE__ */ jsx(
919
+ Checkbox,
920
+ {
921
+ checked: selected,
922
+ onCheckedChange: () => onToggleRow(item.row),
923
+ accessibilityLabel: `Select ${name}`,
924
+ size: "sm"
925
+ }
926
+ ) }),
927
+ /* @__PURE__ */ jsx(
928
+ Pressable,
929
+ {
930
+ accessibilityRole: "button",
931
+ accessibilityLabel: name,
932
+ accessibilityState: { selected },
933
+ accessibilityHint: "Double tap to see the whole record",
934
+ onPress: () => onOpenRow(item.row),
935
+ onLongPress: selectionMode === "none" ? void 0 : () => onToggleRow(item.row),
936
+ style: ({ pressed }) => [styles.tableRowBody, pressed && styles.pressed],
937
+ children: columns.map((column, i) => /* @__PURE__ */ jsx(
938
+ Cell,
939
+ {
940
+ column,
941
+ ctx,
942
+ row: item.row,
943
+ width: widths[i],
944
+ depth: i === 0 ? item.depth : 0
945
+ },
946
+ column.id
947
+ ))
948
+ }
949
+ )
950
+ ]
951
+ }
952
+ )
953
+ );
954
+ };
955
+ return /* @__PURE__ */ jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: true, contentContainerStyle: { minWidth: "100%" }, children: /* @__PURE__ */ jsxs(View, { style: { width: total, flex: 1 }, children: [
956
+ /* @__PURE__ */ jsxs(View, { style: [styles.headerRow, { width: total, minHeight: rowHeight }], children: [
957
+ selectionMode === "none" ? null : /* @__PURE__ */ jsx(View, { style: { width: selectWidth } }),
958
+ columns.map((column, i) => /* @__PURE__ */ jsx(
959
+ HeaderCell,
960
+ {
961
+ column,
962
+ width: widths[i],
963
+ sort: state.sorting.find((s) => s.id === column.id),
964
+ rank: state.sorting.findIndex((s) => s.id === column.id),
965
+ multi: state.sorting.length > 1,
966
+ onPress: () => onToggleSort(column.id)
967
+ },
968
+ column.id
969
+ ))
970
+ ] }),
971
+ /* @__PURE__ */ jsx(
972
+ FlatList,
973
+ {
974
+ data: model.nodes,
975
+ keyExtractor: (node) => node.id,
976
+ renderItem: renderNode,
977
+ style: styles.tableBody,
978
+ getItemLayout: (_, index) => ({
979
+ length: rowHeight,
980
+ offset: rowHeight * index,
981
+ index
982
+ })
983
+ }
984
+ )
985
+ ] }) });
986
+ }
987
+ function HeaderCell({
988
+ column,
989
+ width,
990
+ sort,
991
+ rank,
992
+ multi,
993
+ onPress
994
+ }) {
995
+ const { styles } = useStyles(sheet2);
996
+ const theme = useLoamTheme();
997
+ return /* @__PURE__ */ jsxs(
998
+ Pressable,
999
+ {
1000
+ accessibilityRole: column.sortable ? "button" : void 0,
1001
+ accessibilityLabel: `${column.header}${column.headerUnit ? `, ${column.headerUnit}` : ""}` + (sort ? `, sorted ${sort.desc ? "descending" : "ascending"}` : ""),
1002
+ accessibilityState: { selected: !!sort },
1003
+ accessibilityHint: column.sortable ? "Double tap to change the sort" : void 0,
1004
+ disabled: !column.sortable,
1005
+ onPress,
1006
+ style: ({ pressed }) => [
1007
+ styles.headerCell,
1008
+ { width },
1009
+ column.align === "end" && styles.alignEnd,
1010
+ column.align === "center" && styles.alignCentre,
1011
+ pressed && styles.pressed
1012
+ ],
1013
+ children: [
1014
+ /* @__PURE__ */ jsxs(Text, { numberOfLines: 1, style: styles.headerText, children: [
1015
+ column.header,
1016
+ column.headerUnit ? /* @__PURE__ */ jsxs(Text, { style: styles.headerUnit, children: [
1017
+ " ",
1018
+ column.headerUnit
1019
+ ] }) : null
1020
+ ] }),
1021
+ sort ? /* @__PURE__ */ jsxs(View, { style: styles.sortMark, children: [
1022
+ /* @__PURE__ */ jsx(
1023
+ Icon,
1024
+ {
1025
+ name: sort.desc ? "arrow-down" : "arrow-up",
1026
+ size: theme.icon.xs,
1027
+ color: theme.color.accentOnSoft
1028
+ }
1029
+ ),
1030
+ multi && rank >= 0 ? /* @__PURE__ */ jsx(Text, { style: styles.sortRank, children: rank + 1 }) : null
1031
+ ] }) : null
1032
+ ]
1033
+ }
1034
+ );
1035
+ }
1036
+ function Cell({
1037
+ column,
1038
+ ctx,
1039
+ row,
1040
+ width,
1041
+ depth
1042
+ }) {
1043
+ const { styles } = useStyles(sheet2);
1044
+ const theme = useLoamTheme();
1045
+ const value = getValue(ctx, row, column.id);
1046
+ const text = formatValue(ctx, column, value, row);
1047
+ return /* @__PURE__ */ jsx(
1048
+ View,
1049
+ {
1050
+ style: [
1051
+ styles.cell,
1052
+ { width, paddingStart: theme.space.s3 + depth * theme.space.s4 },
1053
+ column.align === "end" && styles.alignEnd,
1054
+ column.align === "center" && styles.alignCentre
1055
+ ],
1056
+ children: /* @__PURE__ */ jsx(Text, { numberOfLines: 1, style: [styles.cellText, column.mono && styles.mono], children: text })
1057
+ }
1058
+ );
1059
+ }
1060
+ function GroupRow({
1061
+ node,
1062
+ columns,
1063
+ ctx,
1064
+ width,
1065
+ height,
1066
+ inTable = false,
1067
+ onToggle
1068
+ }) {
1069
+ const { styles } = useStyles(sheet2);
1070
+ const theme = useLoamTheme();
1071
+ const field = ctx.columnsById.get(node.field);
1072
+ const summary = columns.map((c) => {
1073
+ const cell = node.aggregates[c.id];
1074
+ return cell && cell.value !== null ? `${c.header} ${AGGREGATION_LABELS[cell.fn].toLowerCase()} ${formatValue(ctx, c, cell.value, {})}` : null;
1075
+ }).filter((s) => s !== null);
1076
+ const disclosure = /* @__PURE__ */ jsxs(
1077
+ Pressable,
1078
+ {
1079
+ accessibilityRole: "button",
1080
+ accessibilityState: { expanded: node.expanded },
1081
+ ...expandedProps(node.expanded),
1082
+ accessibilityLabel: `${field ? `${field.header}: ` : ""}${node.key}, ${node.leafCount} rows`,
1083
+ accessibilityHint: summary.length ? summary.join(". ") : void 0,
1084
+ onPress: () => onToggle(node.id),
1085
+ style: ({ pressed }) => [
1086
+ styles.groupRow,
1087
+ width !== void 0 && { width },
1088
+ height !== void 0 && { minHeight: height },
1089
+ { paddingStart: theme.space.s3 + node.depth * theme.space.s4 },
1090
+ pressed && styles.pressed
1091
+ ],
1092
+ children: [
1093
+ /* @__PURE__ */ jsx(
1094
+ Icon,
1095
+ {
1096
+ name: node.expanded ? "chevron-down" : "chevron-right",
1097
+ size: theme.icon.sm,
1098
+ color: theme.color.text2
1099
+ }
1100
+ ),
1101
+ /* @__PURE__ */ jsx(Text, { numberOfLines: 1, style: styles.groupLabel, children: node.key }),
1102
+ /* @__PURE__ */ jsx(Text, { style: styles.groupCount, children: node.leafCount })
1103
+ ]
1104
+ }
1105
+ );
1106
+ return inTable ? /* @__PURE__ */ jsx(View, { style: width !== void 0 ? { width } : void 0, children: disclosure }) : disclosure;
1107
+ }
1108
+ function CardSurface({
1109
+ model,
1110
+ columns,
1111
+ primary,
1112
+ ctx,
1113
+ state,
1114
+ selectionMode,
1115
+ onToggleRow,
1116
+ onToggleGroup,
1117
+ onOpenRow
1118
+ }) {
1119
+ const { styles } = useStyles(sheet2);
1120
+ const fields = columns.filter((c) => c.id !== primary?.id);
1121
+ const shown = fields.slice(0, CARD_FIELD_LIMIT);
1122
+ const hidden = fields.length - shown.length;
1123
+ const renderNode = ({ item }) => {
1124
+ if (item.kind === "group") {
1125
+ return /* @__PURE__ */ jsx(GroupRow, { node: item, columns, ctx, onToggle: onToggleGroup });
1126
+ }
1127
+ if (item.kind === "detail") return /* @__PURE__ */ jsx(View, {});
1128
+ const rowId = ctx.getRowId(item.row);
1129
+ const selected = state.rowSelection[rowId] === true;
1130
+ const title = primary ? formatValue(ctx, primary, getValue(ctx, item.row, primary.id), item.row) : rowId;
1131
+ return (
1132
+ // The checkbox sits **beside** the pressable body, not inside it. A control
1133
+ // nested in a control is `nested-interactive`, and a screen reader user who
1134
+ // lands on the card is told it is a button with no way to reach the box.
1135
+ /* @__PURE__ */ jsxs(View, { style: [styles.card, selected && styles.cardSelected], children: [
1136
+ selectionMode === "none" ? null : /* @__PURE__ */ jsx(
1137
+ Checkbox,
1138
+ {
1139
+ checked: selected,
1140
+ onCheckedChange: () => onToggleRow(item.row),
1141
+ accessibilityLabel: `Select ${title}`,
1142
+ size: "sm"
1143
+ }
1144
+ ),
1145
+ /* @__PURE__ */ jsxs(
1146
+ Pressable,
1147
+ {
1148
+ accessibilityRole: "button",
1149
+ accessibilityLabel: rowLabel(ctx, columns, item.row),
1150
+ accessibilityState: { selected },
1151
+ accessibilityHint: "Double tap to see the whole record",
1152
+ onPress: () => onOpenRow(item.row),
1153
+ onLongPress: selectionMode === "none" ? void 0 : () => onToggleRow(item.row),
1154
+ style: ({ pressed }) => [styles.cardBody, pressed && styles.pressed],
1155
+ children: [
1156
+ /* @__PURE__ */ jsx(Text, { numberOfLines: 1, style: styles.cardTitle, children: title }),
1157
+ /* @__PURE__ */ jsx(View, { style: styles.cardFields, children: shown.map((column) => /* @__PURE__ */ jsxs(View, { style: styles.cardField, children: [
1158
+ /* @__PURE__ */ jsx(Text, { numberOfLines: 1, style: styles.cardFieldLabel, children: column.header }),
1159
+ /* @__PURE__ */ jsx(Text, { numberOfLines: 1, style: [styles.cardFieldValue, column.mono && styles.mono], children: formatValue(ctx, column, getValue(ctx, item.row, column.id), item.row) })
1160
+ ] }, column.id)) }),
1161
+ hidden > 0 ? /* @__PURE__ */ jsx(Text, { style: styles.cardMore, children: `and ${hidden} more` }) : null
1162
+ ]
1163
+ }
1164
+ )
1165
+ ] })
1166
+ );
1167
+ };
1168
+ return /* @__PURE__ */ jsx(
1169
+ FlatList,
1170
+ {
1171
+ data: model.nodes,
1172
+ keyExtractor: (node) => node.id,
1173
+ renderItem: renderNode,
1174
+ contentContainerStyle: styles.cardList
1175
+ }
1176
+ );
1177
+ }
1178
+ function FooterStrip({
1179
+ model,
1180
+ columns,
1181
+ ctx,
1182
+ state
1183
+ }) {
1184
+ const { styles } = useStyles(sheet2);
1185
+ const cells = columns.map((column) => ({ column, cell: model.footer[column.id] })).filter((entry) => !!entry.cell);
1186
+ if (cells.length === 0) return null;
1187
+ const filtered = model.filteredCount !== model.totalCount;
1188
+ return /* @__PURE__ */ jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, style: styles.footer, children: /* @__PURE__ */ jsxs(View, { style: styles.footerRow, children: [
1189
+ cells.map(({ column, cell }) => /* @__PURE__ */ jsxs(View, { style: styles.footerCell, children: [
1190
+ /* @__PURE__ */ jsx(Text, { style: styles.footerLabel, children: `${column.header} \xB7 ${AGGREGATION_LABELS[cell.fn].toLowerCase()}` }),
1191
+ /* @__PURE__ */ jsx(Text, { style: styles.footerValue, children: cell.value === null ? "\u2014" : formatValue(ctx, column, cell.value, {}) })
1192
+ ] }, column.id)),
1193
+ filtered ? /* @__PURE__ */ jsxs(View, { style: styles.footerCell, children: [
1194
+ /* @__PURE__ */ jsx(Text, { style: styles.footerLabel, children: "Scope" }),
1195
+ /* @__PURE__ */ jsx(Text, { style: styles.footerValue, children: `${model.filteredCount} filtered rows` })
1196
+ ] }) : null,
1197
+ state.grouping.length > 0 ? /* @__PURE__ */ jsxs(View, { style: styles.footerCell, children: [
1198
+ /* @__PURE__ */ jsx(Text, { style: styles.footerLabel, children: "Grouped by" }),
1199
+ /* @__PURE__ */ jsx(Text, { style: styles.footerValue, children: state.grouping.map((id) => ctx.columnsById.get(id)?.header ?? id).join(" \u203A ") })
1200
+ ] }) : null
1201
+ ] }) });
1202
+ }
1203
+ function BulkBar({
1204
+ count,
1205
+ actions,
1206
+ rows,
1207
+ onClear
1208
+ }) {
1209
+ const { styles } = useStyles(sheet2);
1210
+ const theme = useLoamTheme();
1211
+ return /* @__PURE__ */ jsxs(View, { role: "toolbar", accessibilityLabel: `${count} selected`, style: styles.bulkBar, children: [
1212
+ /* @__PURE__ */ jsx(
1213
+ Pressable,
1214
+ {
1215
+ accessibilityRole: "button",
1216
+ accessibilityLabel: "Clear selection",
1217
+ onPress: onClear,
1218
+ style: ({ pressed }) => [styles.bulkClear, pressed && styles.pressed],
1219
+ children: /* @__PURE__ */ jsx(Icon, { name: "x", size: theme.icon.sm, color: theme.color.accentInk })
1220
+ }
1221
+ ),
1222
+ /* @__PURE__ */ jsx(Text, { style: styles.bulkCount, children: `${count} selected` }),
1223
+ /* @__PURE__ */ jsx(View, { style: styles.bulkActions, children: (actions ?? []).map((action) => /* @__PURE__ */ jsxs(
1224
+ Pressable,
1225
+ {
1226
+ accessibilityRole: "button",
1227
+ accessibilityLabel: action.label,
1228
+ onPress: () => action.onRun(rows),
1229
+ style: ({ pressed }) => [
1230
+ styles.bulkAction,
1231
+ action.destructive && styles.bulkActionDanger,
1232
+ pressed && styles.pressed
1233
+ ],
1234
+ children: [
1235
+ action.icon ? /* @__PURE__ */ jsx(
1236
+ Icon,
1237
+ {
1238
+ name: action.icon,
1239
+ size: theme.icon.sm,
1240
+ color: action.destructive ? theme.color.dangerInk : theme.color.accentInk
1241
+ }
1242
+ ) : null,
1243
+ /* @__PURE__ */ jsx(
1244
+ Text,
1245
+ {
1246
+ style: [styles.bulkActionText, action.destructive && styles.bulkActionTextDanger],
1247
+ children: action.label
1248
+ }
1249
+ )
1250
+ ]
1251
+ },
1252
+ action.id
1253
+ )) })
1254
+ ] });
1255
+ }
1256
+ function rowLabel(ctx, columns, row) {
1257
+ return columns.slice(0, CARD_FIELD_LIMIT + 1).map((column) => `${column.header}: ${formatValue(ctx, column, getValue(ctx, row, column.id), row)}`).join(", ");
1258
+ }
1259
+ var sheet2 = createStyleSheet((theme) => ({
1260
+ root: { flex: 1, alignSelf: "stretch", gap: theme.space.s2 },
1261
+ body: { flex: 1, minHeight: theme.space[40] },
1262
+ centre: { flex: 1, alignItems: "center", justifyContent: "center", padding: theme.space.s6 },
1263
+ stateScroll: { flexGrow: 1, justifyContent: "center" },
1264
+ // ---- toolbar ----
1265
+ toolbar: { gap: theme.space.s2 },
1266
+ search: { alignSelf: "stretch" },
1267
+ toolbarButtons: { flexDirection: "row", gap: theme.space.s2, flexWrap: "wrap" },
1268
+ toolPill: {
1269
+ flexDirection: "row",
1270
+ alignItems: "center",
1271
+ gap: theme.space.s1,
1272
+ minHeight: theme.tapMin,
1273
+ paddingHorizontal: theme.space.s3,
1274
+ borderRadius: theme.radius.full,
1275
+ borderWidth: theme.borderWidth.base,
1276
+ borderColor: theme.color.border,
1277
+ backgroundColor: theme.color.surface
1278
+ },
1279
+ toolPillText: {
1280
+ fontFamily: theme.font.sans,
1281
+ fontSize: theme.type.footnote.fontSize,
1282
+ lineHeight: theme.type.footnote.lineHeight,
1283
+ fontWeight: theme.type.footnote.fontWeight,
1284
+ color: theme.color.text
1285
+ },
1286
+ // The tint alone carries the badge at normal contrast; under
1287
+ // `prefers-contrast: more` it collapses to the plain surface, and without an
1288
+ // edge the badge would simply stop existing. The edge is `transparent` until
1289
+ // then, so it costs nothing here.
1290
+ toolCount: {
1291
+ minWidth: theme.space.s5,
1292
+ paddingHorizontal: theme.space[2],
1293
+ borderRadius: theme.radius.full,
1294
+ borderWidth: theme.borderWidth.base,
1295
+ borderColor: theme.edge.accent,
1296
+ backgroundColor: theme.color.accentSoft,
1297
+ alignItems: "center"
1298
+ },
1299
+ toolCountText: {
1300
+ fontFamily: theme.font.mono,
1301
+ fontSize: theme.type.caption2.fontSize,
1302
+ lineHeight: theme.type.caption1.lineHeight,
1303
+ // On the accent tint, the tone itself is under AA — the measured `-on-soft`
1304
+ // ink is the only legible foreground here.
1305
+ color: theme.color.accentOnSoft
1306
+ },
1307
+ countLine: {
1308
+ flexDirection: "row",
1309
+ alignItems: "center",
1310
+ justifyContent: "space-between",
1311
+ gap: theme.space.s2,
1312
+ minHeight: theme.space.s5
1313
+ },
1314
+ countText: {
1315
+ fontFamily: theme.font.sans,
1316
+ fontSize: theme.type.caption1.fontSize,
1317
+ lineHeight: theme.type.caption1.lineHeight,
1318
+ color: theme.color.text2
1319
+ },
1320
+ // ---- table ----
1321
+ headerRow: {
1322
+ flexDirection: "row",
1323
+ alignItems: "center",
1324
+ backgroundColor: theme.color.surface2,
1325
+ borderBottomWidth: theme.borderWidth.strong,
1326
+ borderBottomColor: theme.color.border
1327
+ },
1328
+ headerCell: {
1329
+ flexDirection: "row",
1330
+ alignItems: "center",
1331
+ gap: theme.space.s1,
1332
+ height: "100%",
1333
+ paddingHorizontal: theme.space.s3
1334
+ },
1335
+ headerText: {
1336
+ flexShrink: 1,
1337
+ fontFamily: theme.font.sans,
1338
+ fontSize: theme.type.caption1.fontSize,
1339
+ lineHeight: theme.type.caption1.lineHeight,
1340
+ fontWeight: theme.fontWeight.bold,
1341
+ color: theme.color.text
1342
+ },
1343
+ headerUnit: { fontWeight: theme.fontWeight.regular, color: theme.color.text2 },
1344
+ sortMark: { flexDirection: "row", alignItems: "center", gap: theme.space[2] },
1345
+ sortRank: {
1346
+ fontFamily: theme.font.mono,
1347
+ fontSize: theme.type.caption2.fontSize,
1348
+ color: theme.color.accentOnSoft
1349
+ },
1350
+ tableBody: { flex: 1 },
1351
+ tableRow: {
1352
+ flexDirection: "row",
1353
+ alignItems: "stretch",
1354
+ borderBottomWidth: theme.borderWidth.hair,
1355
+ borderBottomColor: theme.color.border,
1356
+ backgroundColor: theme.color.surface
1357
+ },
1358
+ zebra: { backgroundColor: theme.color.surface2 },
1359
+ selectedRow: { backgroundColor: theme.state.selected },
1360
+ focusedRow: {
1361
+ borderStartWidth: theme.borderWidth.heavy,
1362
+ borderStartColor: theme.focus.ringColor
1363
+ },
1364
+ selectCell: { alignItems: "center", justifyContent: "center" },
1365
+ tableRowBody: { flex: 1, flexDirection: "row", alignItems: "center" },
1366
+ cell: { justifyContent: "center", paddingEnd: theme.space.s3, height: "100%" },
1367
+ cellText: {
1368
+ fontFamily: theme.font.sans,
1369
+ fontSize: theme.type.subheadline.fontSize,
1370
+ lineHeight: theme.type.subheadline.lineHeight,
1371
+ color: theme.color.text
1372
+ },
1373
+ mono: { fontFamily: theme.font.mono },
1374
+ alignEnd: { alignItems: "flex-end" },
1375
+ alignCentre: { alignItems: "center" },
1376
+ groupRow: {
1377
+ flexDirection: "row",
1378
+ alignItems: "center",
1379
+ gap: theme.space.s2,
1380
+ minHeight: theme.tapMin,
1381
+ paddingEnd: theme.space.s3,
1382
+ backgroundColor: theme.color.surface3,
1383
+ borderBottomWidth: theme.borderWidth.base,
1384
+ borderBottomColor: theme.color.border
1385
+ },
1386
+ groupLabel: {
1387
+ flex: 1,
1388
+ fontFamily: theme.font.sans,
1389
+ fontSize: theme.type.subheadline.fontSize,
1390
+ fontWeight: theme.fontWeight.bold,
1391
+ color: theme.color.text
1392
+ },
1393
+ groupCell: { flex: 1 },
1394
+ groupCount: {
1395
+ fontFamily: theme.font.mono,
1396
+ fontSize: theme.type.caption1.fontSize,
1397
+ color: theme.color.text2
1398
+ },
1399
+ // ---- cards ----
1400
+ cardList: { gap: theme.space.s2, paddingBottom: theme.space.s2 },
1401
+ card: {
1402
+ flexDirection: "row",
1403
+ alignItems: "flex-start",
1404
+ gap: theme.space.s2,
1405
+ padding: theme.space.s3,
1406
+ borderRadius: theme.radius.lg,
1407
+ borderWidth: theme.borderWidth.base,
1408
+ borderColor: theme.color.border,
1409
+ backgroundColor: theme.color.surface
1410
+ },
1411
+ // A selection edge is a graphical object held to 3:1, and the raw accent does
1412
+ // not clear that on every ground the card sits on. `focus.ringColor` is the
1413
+ // accent already lifted to meet it.
1414
+ cardSelected: {
1415
+ borderColor: theme.focus.ringColor,
1416
+ backgroundColor: theme.state.selected
1417
+ },
1418
+ cardBody: { flex: 1, gap: theme.space.s2 },
1419
+ cardTitle: {
1420
+ flex: 1,
1421
+ fontFamily: theme.font.sans,
1422
+ fontSize: theme.type.headline.fontSize,
1423
+ lineHeight: theme.type.headline.lineHeight,
1424
+ fontWeight: theme.type.headline.fontWeight,
1425
+ color: theme.color.text
1426
+ },
1427
+ cardFields: { gap: theme.space.s1 },
1428
+ cardField: { flexDirection: "row", alignItems: "baseline", gap: theme.space.s2 },
1429
+ // `space` is keyed by px, so `[28]` was a 28px gutter that cut every
1430
+ // header to "Tea…". 96 holds a one-word header at caption size.
1431
+ cardFieldLabel: {
1432
+ width: theme.space[96],
1433
+ fontFamily: theme.font.sans,
1434
+ fontSize: theme.type.caption1.fontSize,
1435
+ lineHeight: theme.type.caption1.lineHeight,
1436
+ color: theme.color.text2
1437
+ },
1438
+ cardFieldValue: {
1439
+ flex: 1,
1440
+ fontFamily: theme.font.sans,
1441
+ fontSize: theme.type.subheadline.fontSize,
1442
+ lineHeight: theme.type.subheadline.lineHeight,
1443
+ color: theme.color.text
1444
+ },
1445
+ cardMore: {
1446
+ fontFamily: theme.font.sans,
1447
+ fontSize: theme.type.caption1.fontSize,
1448
+ color: theme.color.text3
1449
+ },
1450
+ // ---- footer ----
1451
+ footer: {
1452
+ flexGrow: 0,
1453
+ borderTopWidth: theme.borderWidth.base,
1454
+ borderTopColor: theme.color.border,
1455
+ backgroundColor: theme.color.surface2
1456
+ },
1457
+ footerRow: { flexDirection: "row", gap: theme.space.s4, padding: theme.space.s2 },
1458
+ footerCell: { gap: theme.space[2] },
1459
+ footerLabel: {
1460
+ fontFamily: theme.font.sans,
1461
+ fontSize: theme.type.caption2.fontSize,
1462
+ lineHeight: theme.type.caption2.lineHeight,
1463
+ color: theme.color.text2
1464
+ },
1465
+ footerValue: {
1466
+ fontFamily: theme.font.mono,
1467
+ fontSize: theme.type.subheadline.fontSize,
1468
+ lineHeight: theme.type.subheadline.lineHeight,
1469
+ color: theme.color.text
1470
+ },
1471
+ // ---- selection bar ----
1472
+ bulkBar: {
1473
+ flexDirection: "row",
1474
+ alignItems: "center",
1475
+ gap: theme.space.s2,
1476
+ minHeight: theme.tapMin,
1477
+ paddingHorizontal: theme.space.s2,
1478
+ borderRadius: theme.radius.lg,
1479
+ backgroundColor: theme.color.accent,
1480
+ ...theme.shadow.md
1481
+ },
1482
+ bulkClear: {
1483
+ minWidth: theme.tapMin,
1484
+ minHeight: theme.tapMin,
1485
+ alignItems: "center",
1486
+ justifyContent: "center"
1487
+ },
1488
+ bulkCount: {
1489
+ flex: 1,
1490
+ fontFamily: theme.font.sans,
1491
+ fontSize: theme.type.subheadline.fontSize,
1492
+ fontWeight: theme.fontWeight.bold,
1493
+ color: theme.color.accentInk
1494
+ },
1495
+ bulkActions: { flexDirection: "row", gap: theme.space.s1 },
1496
+ bulkAction: {
1497
+ flexDirection: "row",
1498
+ alignItems: "center",
1499
+ gap: theme.space.s1,
1500
+ minHeight: theme.tapMin,
1501
+ paddingHorizontal: theme.space.s3,
1502
+ borderRadius: theme.radius.md
1503
+ },
1504
+ bulkActionDanger: { backgroundColor: theme.color.danger },
1505
+ bulkActionText: {
1506
+ fontFamily: theme.font.sans,
1507
+ fontSize: theme.type.footnote.fontSize,
1508
+ fontWeight: theme.fontWeight.bold,
1509
+ color: theme.color.accentInk
1510
+ },
1511
+ bulkActionTextDanger: { color: theme.color.dangerInk },
1512
+ pressed: { opacity: theme.state.pressedOpacity },
1513
+ srOnly: {
1514
+ position: "absolute",
1515
+ width: theme.space[1],
1516
+ height: theme.space[1],
1517
+ overflow: "hidden"
1518
+ }
1519
+ }));
1520
+
1521
+ export { DataGrid, DetailSheet, DisplaySheet, FilterSheet, SortSheet, TABLE_MIN_WIDTH };