tempest-react-sdk 0.42.1 → 0.44.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 (125) hide show
  1. package/README.md +13 -7
  2. package/bin/create-tempest-app.mjs +10 -1
  3. package/dist/app/AppProviders.cjs +1 -1
  4. package/dist/app/AppProviders.js +6 -6
  5. package/dist/audio/sfx-pool.cjs +2 -0
  6. package/dist/audio/sfx-pool.cjs.map +1 -0
  7. package/dist/audio/sfx-pool.js +62 -0
  8. package/dist/audio/sfx-pool.js.map +1 -0
  9. package/dist/audio/use-sfx-pool.cjs +2 -0
  10. package/dist/audio/use-sfx-pool.cjs.map +1 -0
  11. package/dist/audio/use-sfx-pool.js +18 -0
  12. package/dist/audio/use-sfx-pool.js.map +1 -0
  13. package/dist/auth/lazy-with-retry.cjs +1 -1
  14. package/dist/auth/lazy-with-retry.cjs.map +1 -1
  15. package/dist/auth/lazy-with-retry.js +8 -1
  16. package/dist/auth/lazy-with-retry.js.map +1 -1
  17. package/dist/components/AppBar/AppBar.cjs +1 -1
  18. package/dist/components/AppBar/AppBar.cjs.map +1 -1
  19. package/dist/components/AppBar/AppBar.js +27 -25
  20. package/dist/components/AppBar/AppBar.js.map +1 -1
  21. package/dist/components/AppBar/use-sticky-body-warning.cjs +2 -0
  22. package/dist/components/AppBar/use-sticky-body-warning.cjs.map +1 -0
  23. package/dist/components/AppBar/use-sticky-body-warning.js +16 -0
  24. package/dist/components/AppBar/use-sticky-body-warning.js.map +1 -0
  25. package/dist/components/BarList/BarList.cjs +2 -0
  26. package/dist/components/BarList/BarList.cjs.map +1 -0
  27. package/dist/components/BarList/BarList.js +42 -0
  28. package/dist/components/BarList/BarList.js.map +1 -0
  29. package/dist/components/BarList/BarList.module.cjs +2 -0
  30. package/dist/components/BarList/BarList.module.cjs.map +1 -0
  31. package/dist/components/BarList/BarList.module.js +15 -0
  32. package/dist/components/BarList/BarList.module.js.map +1 -0
  33. package/dist/components/BarList/bar-list-model.cjs +2 -0
  34. package/dist/components/BarList/bar-list-model.cjs.map +1 -0
  35. package/dist/components/BarList/bar-list-model.js +18 -0
  36. package/dist/components/BarList/bar-list-model.js.map +1 -0
  37. package/dist/components/DataTable/DataTable.cjs +1 -1
  38. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  39. package/dist/components/DataTable/DataTable.js +141 -108
  40. package/dist/components/DataTable/DataTable.js.map +1 -1
  41. package/dist/components/DataTable/DataTable.module.cjs +1 -1
  42. package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
  43. package/dist/components/DataTable/DataTable.module.js +13 -10
  44. package/dist/components/DataTable/DataTable.module.js.map +1 -1
  45. package/dist/components/DataTable/LoadingRows.cjs +2 -0
  46. package/dist/components/DataTable/LoadingRows.cjs.map +1 -0
  47. package/dist/components/DataTable/LoadingRows.js +18 -0
  48. package/dist/components/DataTable/LoadingRows.js.map +1 -0
  49. package/dist/components/DataTable/use-dev-warnings.cjs +2 -0
  50. package/dist/components/DataTable/use-dev-warnings.cjs.map +1 -0
  51. package/dist/components/DataTable/use-dev-warnings.js +17 -0
  52. package/dist/components/DataTable/use-dev-warnings.js.map +1 -0
  53. package/dist/components/FilterBar/filter-apply.cjs +2 -0
  54. package/dist/components/FilterBar/filter-apply.cjs.map +1 -0
  55. package/dist/components/FilterBar/filter-apply.js +76 -0
  56. package/dist/components/FilterBar/filter-apply.js.map +1 -0
  57. package/dist/components/FilterBar/filter-query.cjs +2 -0
  58. package/dist/components/FilterBar/filter-query.cjs.map +1 -0
  59. package/dist/components/FilterBar/filter-query.js +38 -0
  60. package/dist/components/FilterBar/filter-query.js.map +1 -0
  61. package/dist/hooks/use-countdown.cjs +2 -0
  62. package/dist/hooks/use-countdown.cjs.map +1 -0
  63. package/dist/hooks/use-countdown.js +18 -0
  64. package/dist/hooks/use-countdown.js.map +1 -0
  65. package/dist/hooks/use-latest-ref.cjs +2 -0
  66. package/dist/hooks/use-latest-ref.cjs.map +1 -0
  67. package/dist/hooks/use-latest-ref.js +10 -0
  68. package/dist/hooks/use-latest-ref.js.map +1 -0
  69. package/dist/hooks/use-typewriter.cjs +2 -0
  70. package/dist/hooks/use-typewriter.cjs.map +1 -0
  71. package/dist/hooks/use-typewriter.js +28 -0
  72. package/dist/hooks/use-typewriter.js.map +1 -0
  73. package/dist/http/describe-api-error.cjs +2 -0
  74. package/dist/http/describe-api-error.cjs.map +1 -0
  75. package/dist/http/describe-api-error.js +22 -0
  76. package/dist/http/describe-api-error.js.map +1 -0
  77. package/dist/http/use-describe-api-error.cjs +2 -0
  78. package/dist/http/use-describe-api-error.cjs.map +1 -0
  79. package/dist/http/use-describe-api-error.js +15 -0
  80. package/dist/http/use-describe-api-error.js.map +1 -0
  81. package/dist/i18n/I18nProvider.cjs +1 -1
  82. package/dist/i18n/I18nProvider.cjs.map +1 -1
  83. package/dist/i18n/I18nProvider.js +4 -1
  84. package/dist/i18n/I18nProvider.js.map +1 -1
  85. package/dist/query/QueryProvider.cjs +1 -1
  86. package/dist/query/QueryProvider.cjs.map +1 -1
  87. package/dist/query/QueryProvider.js +13 -12
  88. package/dist/query/QueryProvider.js.map +1 -1
  89. package/dist/query/retry-policy.cjs +2 -0
  90. package/dist/query/retry-policy.cjs.map +1 -0
  91. package/dist/query/retry-policy.js +10 -0
  92. package/dist/query/retry-policy.js.map +1 -0
  93. package/dist/router/AppRouter.cjs.map +1 -1
  94. package/dist/router/AppRouter.js.map +1 -1
  95. package/dist/styles.css +1 -1
  96. package/dist/tempest-react-sdk.cjs +1 -1
  97. package/dist/tempest-react-sdk.d.ts +790 -10
  98. package/dist/tempest-react-sdk.js +275 -261
  99. package/dist/utils/compressed-storage.cjs +2 -0
  100. package/dist/utils/compressed-storage.cjs.map +1 -0
  101. package/dist/utils/compressed-storage.js +52 -0
  102. package/dist/utils/compressed-storage.js.map +1 -0
  103. package/dist/utils/csv.cjs +4 -0
  104. package/dist/utils/csv.cjs.map +1 -0
  105. package/dist/utils/csv.js +25 -0
  106. package/dist/utils/csv.js.map +1 -0
  107. package/dist/utils/format.cjs +1 -1
  108. package/dist/utils/format.cjs.map +1 -1
  109. package/dist/utils/format.js +11 -4
  110. package/dist/utils/format.js.map +1 -1
  111. package/dist/utils/numbers.cjs +1 -1
  112. package/dist/utils/numbers.cjs.map +1 -1
  113. package/dist/utils/numbers.js +5 -2
  114. package/dist/utils/numbers.js.map +1 -1
  115. package/dist/vite/tempest-pwa-dev-sw.cjs +1 -1
  116. package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
  117. package/dist/vite/tempest-pwa-dev-sw.js +10 -5
  118. package/dist/vite/tempest-pwa-dev-sw.js.map +1 -1
  119. package/dist/vite/tempest-pwa-manifest.cjs +2 -2
  120. package/dist/vite/tempest-pwa-manifest.cjs.map +1 -1
  121. package/dist/vite/tempest-pwa-manifest.js +17 -13
  122. package/dist/vite/tempest-pwa-manifest.js.map +1 -1
  123. package/dist/vite.d.ts +19 -2
  124. package/package.json +3 -2
  125. package/template/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BarList.cjs","names":[],"sources":["../../../src/components/BarList/BarList.tsx"],"sourcesContent":["import type { HTMLAttributes } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { buildBarListRows, type BarListItem, type BarListSort } from \"./bar-list-model\";\nimport styles from \"./BarList.module.css\";\n\nexport type { BarListItem, BarListSort } from \"./bar-list-model\";\n\nexport interface BarListProps extends Omit<HTMLAttributes<HTMLUListElement>, \"children\"> {\n /** The rows. Entries whose value is not finite are dropped. */\n items: readonly BarListItem[];\n /** Render the number. Defaults to the value as-is. */\n valueFormatter?: (value: number) => string;\n /** Show each row's share of the total next to its value. Default `false`. */\n showPercentage?: boolean;\n /** Ordering. Default `\"desc\"`, which is what a ranking means. */\n sort?: BarListSort;\n /** Keep at most this many rows. */\n max?: number;\n /**\n * Label for one aggregated row holding everything `max` cut off.\n *\n * Without it, `max` simply truncates. The aggregate row only appears when more\n * than one row was cut — collapsing a single row into \"others\" hides its name\n * for nothing.\n */\n otherLabel?: string;\n}\n\n/**\n * A ranked distribution: label, proportional bar, value, optional share.\n *\n * The most common chart on an admin panel, and the one the SDK kept sending\n * people to write by hand — `Progress` is a single bar and `Sparkline` is a time\n * series, so a \"users per plan\" block ended up reimplemented per screen, each\n * with its own CSS and its own `.sort()`.\n *\n * It is a **list**, not a picture: `<ul>` / `<li>` with the value written as\n * text, the bar `aria-hidden` behind it. A screen reader reads \"Free, 128, 62%\"\n * because that text is there, not because an `aria-label` restates a drawing.\n * Which is also why the label never sits on top of the bar — text over a tinted\n * fill has to be re-verified against that fill, and the SDK has been caught by\n * that twice.\n *\n * Bar width is relative to the **largest** row, so the biggest bar fills the\n * track; the percentage is the row's share of the **total**. They are different\n * numbers on purpose: a width scaled by the total leaves every bar short in a\n * long list, which is when the chart is needed most.\n *\n * @example\n * <BarList\n * items={[{ label: \"Free\", value: 128 }, { label: \"Pro\", value: 32 }]}\n * valueFormatter={(n) => `${n} ativos`}\n * showPercentage\n * max={5}\n * otherLabel=\"Outros\"\n * />\n *\n * @param props - Items plus presentation.\n * @returns The list.\n */\nexport function BarList({\n items,\n valueFormatter,\n showPercentage = false,\n sort = \"desc\",\n max,\n otherLabel,\n className,\n ...rest\n}: BarListProps) {\n const rows = buildBarListRows(items, sort, max, otherLabel);\n\n return (\n <ul className={cn(styles.list, className)} {...rest}>\n {rows.map((row) => (\n <li key={`${row.label}-${row.index}`} className={styles.item}>\n <div className={styles.line}>\n <span className={styles.label}>{row.label}</span>\n <span className={styles.value}>\n {valueFormatter ? valueFormatter(row.value) : row.value}\n {showPercentage && (\n <span className={styles.percentage}>\n {Math.round(row.percentage)}%\n </span>\n )}\n </span>\n </div>\n <div className={styles.track} aria-hidden=\"true\">\n <div\n className={styles.fill}\n style={{\n width: `${row.width}%`,\n backgroundColor:\n row.color ?? `var(--tempest-chart-${(row.index % 8) + 1})`,\n }}\n />\n </div>\n </li>\n ))}\n </ul>\n );\n}\n"],"mappings":"6IA8DA,SAAgB,EAAQ,CACpB,QACA,iBACA,iBAAiB,GACjB,OAAO,OACP,MACA,aACA,YACA,GAAG,GACU,CACb,IAAM,EAAO,EAAA,iBAAiB,EAAO,EAAM,EAAK,CAAU,EAE1D,OACI,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,GAAG,EAAA,QAAO,KAAM,CAAS,EAAG,GAAI,EAC1C,SAAA,EAAK,IAAK,IACP,EAAA,EAAA,KAAA,CAAC,KAAD,CAAsC,UAAW,EAAA,QAAO,KAAxD,SAAA,EACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAvB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAQ,SAAA,EAAI,KAAY,CAAA,GAChD,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAxB,SAAA,CACK,EAAiB,EAAe,EAAI,KAAK,EAAI,EAAI,MACjD,IACG,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,WAAxB,SAAA,CACK,KAAK,MAAM,EAAI,UAAU,EAAE,GAC1B,CAER,CAAA,CAAA,CACL,CAAA,CAAA,CACL,CAAA,GAAA,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,MAAO,cAAY,OACtC,UAAA,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,UAAW,EAAA,QAAO,KAClB,MAAO,CACH,MAAO,GAAG,EAAI,MAAM,GACpB,gBACI,EAAI,OAAS,uBAAwB,EAAI,MAAQ,EAAK,EAAE,EAChE,CACH,CAAA,CACA,CAAA,CACL,CAtBK,EAAA,GAAG,EAAI,MAAM,GAAG,EAAI,OAsBzB,CACP,CACD,CAAA,CAEZ"}
@@ -0,0 +1,42 @@
1
+ import { cn as e } from "../../utils/cn.js";
2
+ import { buildBarListRows as t } from "./bar-list-model.js";
3
+ import n from "./BarList.module.js";
4
+ import { jsx as r, jsxs as i } from "react/jsx-runtime";
5
+ //#region src/components/BarList/BarList.tsx
6
+ function a({ items: a, valueFormatter: o, showPercentage: s = !1, sort: c = "desc", max: l, otherLabel: u, className: d, ...f }) {
7
+ let p = t(a, c, l, u);
8
+ return /* @__PURE__ */ r("ul", {
9
+ className: e(n.list, d),
10
+ ...f,
11
+ children: p.map((e) => /* @__PURE__ */ i("li", {
12
+ className: n.item,
13
+ children: [/* @__PURE__ */ i("div", {
14
+ className: n.line,
15
+ children: [/* @__PURE__ */ r("span", {
16
+ className: n.label,
17
+ children: e.label
18
+ }), /* @__PURE__ */ i("span", {
19
+ className: n.value,
20
+ children: [o ? o(e.value) : e.value, s && /* @__PURE__ */ i("span", {
21
+ className: n.percentage,
22
+ children: [Math.round(e.percentage), "%"]
23
+ })]
24
+ })]
25
+ }), /* @__PURE__ */ r("div", {
26
+ className: n.track,
27
+ "aria-hidden": "true",
28
+ children: /* @__PURE__ */ r("div", {
29
+ className: n.fill,
30
+ style: {
31
+ width: `${e.width}%`,
32
+ backgroundColor: e.color ?? `var(--tempest-chart-${e.index % 8 + 1})`
33
+ }
34
+ })
35
+ })]
36
+ }, `${e.label}-${e.index}`))
37
+ });
38
+ }
39
+ //#endregion
40
+ export { a as BarList };
41
+
42
+ //# sourceMappingURL=BarList.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BarList.js","names":[],"sources":["../../../src/components/BarList/BarList.tsx"],"sourcesContent":["import type { HTMLAttributes } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { buildBarListRows, type BarListItem, type BarListSort } from \"./bar-list-model\";\nimport styles from \"./BarList.module.css\";\n\nexport type { BarListItem, BarListSort } from \"./bar-list-model\";\n\nexport interface BarListProps extends Omit<HTMLAttributes<HTMLUListElement>, \"children\"> {\n /** The rows. Entries whose value is not finite are dropped. */\n items: readonly BarListItem[];\n /** Render the number. Defaults to the value as-is. */\n valueFormatter?: (value: number) => string;\n /** Show each row's share of the total next to its value. Default `false`. */\n showPercentage?: boolean;\n /** Ordering. Default `\"desc\"`, which is what a ranking means. */\n sort?: BarListSort;\n /** Keep at most this many rows. */\n max?: number;\n /**\n * Label for one aggregated row holding everything `max` cut off.\n *\n * Without it, `max` simply truncates. The aggregate row only appears when more\n * than one row was cut — collapsing a single row into \"others\" hides its name\n * for nothing.\n */\n otherLabel?: string;\n}\n\n/**\n * A ranked distribution: label, proportional bar, value, optional share.\n *\n * The most common chart on an admin panel, and the one the SDK kept sending\n * people to write by hand — `Progress` is a single bar and `Sparkline` is a time\n * series, so a \"users per plan\" block ended up reimplemented per screen, each\n * with its own CSS and its own `.sort()`.\n *\n * It is a **list**, not a picture: `<ul>` / `<li>` with the value written as\n * text, the bar `aria-hidden` behind it. A screen reader reads \"Free, 128, 62%\"\n * because that text is there, not because an `aria-label` restates a drawing.\n * Which is also why the label never sits on top of the bar — text over a tinted\n * fill has to be re-verified against that fill, and the SDK has been caught by\n * that twice.\n *\n * Bar width is relative to the **largest** row, so the biggest bar fills the\n * track; the percentage is the row's share of the **total**. They are different\n * numbers on purpose: a width scaled by the total leaves every bar short in a\n * long list, which is when the chart is needed most.\n *\n * @example\n * <BarList\n * items={[{ label: \"Free\", value: 128 }, { label: \"Pro\", value: 32 }]}\n * valueFormatter={(n) => `${n} ativos`}\n * showPercentage\n * max={5}\n * otherLabel=\"Outros\"\n * />\n *\n * @param props - Items plus presentation.\n * @returns The list.\n */\nexport function BarList({\n items,\n valueFormatter,\n showPercentage = false,\n sort = \"desc\",\n max,\n otherLabel,\n className,\n ...rest\n}: BarListProps) {\n const rows = buildBarListRows(items, sort, max, otherLabel);\n\n return (\n <ul className={cn(styles.list, className)} {...rest}>\n {rows.map((row) => (\n <li key={`${row.label}-${row.index}`} className={styles.item}>\n <div className={styles.line}>\n <span className={styles.label}>{row.label}</span>\n <span className={styles.value}>\n {valueFormatter ? valueFormatter(row.value) : row.value}\n {showPercentage && (\n <span className={styles.percentage}>\n {Math.round(row.percentage)}%\n </span>\n )}\n </span>\n </div>\n <div className={styles.track} aria-hidden=\"true\">\n <div\n className={styles.fill}\n style={{\n width: `${row.width}%`,\n backgroundColor:\n row.color ?? `var(--tempest-chart-${(row.index % 8) + 1})`,\n }}\n />\n </div>\n </li>\n ))}\n </ul>\n );\n}\n"],"mappings":";;;;;AA8DA,SAAgB,EAAQ,EACpB,UACA,mBACA,oBAAiB,IACjB,UAAO,QACP,QACA,eACA,cACA,GAAG,KACU;CACb,IAAM,IAAO,EAAiB,GAAO,GAAM,GAAK,CAAU;CAE1D,OACI,kBAAC,MAAD;EAAI,WAAW,EAAG,EAAO,MAAM,CAAS;EAAG,GAAI;EAC1C,UAAA,EAAK,KAAK,MACP,kBAAC,MAAD;GAAsC,WAAW,EAAO;GAAxD,UAAA,CACI,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA,CACI,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAQ,UAAA,EAAI;IAAY,CAAA,GAChD,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAxB,UAAA,CACK,IAAiB,EAAe,EAAI,KAAK,IAAI,EAAI,OACjD,KACG,kBAAC,QAAD;MAAM,WAAW,EAAO;MAAxB,UAAA,CACK,KAAK,MAAM,EAAI,UAAU,GAAE,GAC1B;KAER,CAAA,CAAA;IACL,CAAA,CAAA;GACL,CAAA,GAAA,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAO,eAAY;IACtC,UAAA,kBAAC,OAAD;KACI,WAAW,EAAO;KAClB,OAAO;MACH,OAAO,GAAG,EAAI,MAAM;MACpB,iBACI,EAAI,SAAS,uBAAwB,EAAI,QAAQ,IAAK,EAAE;KAChE;IACH,CAAA;GACA,CAAA,CACL;EAtBK,GAAA,GAAG,EAAI,MAAM,GAAG,EAAI,OAsBzB,CACP;CACD,CAAA;AAEZ"}
@@ -0,0 +1,2 @@
1
+ var e=`tempest_list_XtltA`,t=`tempest_item_y-W9f`,n=`tempest_line_WKQ57`,r=`tempest_label_AywVf`,i=`tempest_value_Y0DD0`,a=`tempest_percentage_YTyMa`,o=`tempest_track_OVSC3`,s=`tempest_fill_kg-Vl`,c={list:e,item:t,line:n,label:r,value:i,percentage:a,track:o,fill:s};exports.default=c,exports.fill=s,exports.item=t,exports.label=r,exports.line=n,exports.list=e,exports.percentage=a,exports.track=o,exports.value=i;
2
+ //# sourceMappingURL=BarList.module.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BarList.module.cjs","names":[],"sources":["../../../src/components/BarList/BarList.module.css"],"sourcesContent":[".list {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-3, 0.75rem);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.item {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1, 0.25rem);\n}\n\n.line {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--tempest-space-3, 0.75rem);\n font-size: var(--tempest-font-size-sm, 0.8125rem);\n}\n\n/*\n * The label truncates instead of wrapping: a plan name long enough to wrap would\n * push its own bar down and break the alignment the list exists to show.\n */\n.label {\n min-width: 0;\n overflow: hidden;\n color: var(--tempest-text);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.value {\n display: inline-flex;\n align-items: baseline;\n gap: var(--tempest-space-2, 0.5rem);\n color: var(--tempest-text);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n/*\n * The share is de-emphasised by size, and only then by colour — and the colour it\n * uses is resolved against the page background, which is where this text actually\n * sits. Putting the label on top of the bar would need the fill's own foreground\n * re-verified per series, which is the trap this layout avoids entirely.\n */\n.percentage {\n color: var(--tempest-text-subtle);\n font-size: var(--tempest-font-size-xs, 0.75rem);\n}\n\n.track {\n overflow: hidden;\n width: 100%;\n height: 0.5rem;\n border-radius: var(--tempest-radius-sm, 0.25rem);\n background-color: var(--tempest-surface-2);\n}\n\n.fill {\n height: 100%;\n border-radius: inherit;\n transition: width var(--tempest-duration-normal, 200ms) ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fill {\n transition: none;\n }\n}\n"],"mappings":""}
@@ -0,0 +1,15 @@
1
+ //#region src/components/BarList/BarList.module.css
2
+ var e = "tempest_list_XtltA", t = "tempest_item_y-W9f", n = "tempest_line_WKQ57", r = "tempest_label_AywVf", i = "tempest_value_Y0DD0", a = "tempest_percentage_YTyMa", o = "tempest_track_OVSC3", s = "tempest_fill_kg-Vl", c = {
3
+ list: e,
4
+ item: t,
5
+ line: n,
6
+ label: r,
7
+ value: i,
8
+ percentage: a,
9
+ track: o,
10
+ fill: s
11
+ };
12
+ //#endregion
13
+ export { c as default, s as fill, t as item, r as label, n as line, e as list, a as percentage, o as track, i as value };
14
+
15
+ //# sourceMappingURL=BarList.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BarList.module.js","names":[],"sources":["../../../src/components/BarList/BarList.module.css"],"sourcesContent":[".list {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-3, 0.75rem);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.item {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1, 0.25rem);\n}\n\n.line {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--tempest-space-3, 0.75rem);\n font-size: var(--tempest-font-size-sm, 0.8125rem);\n}\n\n/*\n * The label truncates instead of wrapping: a plan name long enough to wrap would\n * push its own bar down and break the alignment the list exists to show.\n */\n.label {\n min-width: 0;\n overflow: hidden;\n color: var(--tempest-text);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.value {\n display: inline-flex;\n align-items: baseline;\n gap: var(--tempest-space-2, 0.5rem);\n color: var(--tempest-text);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n/*\n * The share is de-emphasised by size, and only then by colour — and the colour it\n * uses is resolved against the page background, which is where this text actually\n * sits. Putting the label on top of the bar would need the fill's own foreground\n * re-verified per series, which is the trap this layout avoids entirely.\n */\n.percentage {\n color: var(--tempest-text-subtle);\n font-size: var(--tempest-font-size-xs, 0.75rem);\n}\n\n.track {\n overflow: hidden;\n width: 100%;\n height: 0.5rem;\n border-radius: var(--tempest-radius-sm, 0.25rem);\n background-color: var(--tempest-surface-2);\n}\n\n.fill {\n height: 100%;\n border-radius: inherit;\n transition: width var(--tempest-duration-normal, 200ms) ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fill {\n transition: none;\n }\n}\n"],"mappings":""}
@@ -0,0 +1,2 @@
1
+ const e=require("../../utils/numbers.cjs");function t(t,n,r,i){let a=t.filter(e=>Number.isFinite(e.value)),o=a.reduce((e,t)=>e+Math.max(t.value,0),0),s=n===`none`?[...a]:[...a].sort((e,t)=>n===`asc`?e.value-t.value:t.value-e.value),c=r!==void 0&&r>0?s.slice(0,r):s,l=r!==void 0&&r>0?s.slice(r):[],u=i!==void 0&&l.length>1?[...c,{label:i,value:l.reduce((e,t)=>e+t.value,0)}]:i!==void 0&&l.length===1?[...c,...l]:c,d=u.reduce((e,t)=>Math.max(e,t.value),0);return u.map((t,n)=>({...t,index:n,percentage:e.percentOf(Math.max(t.value,0),o),width:e.percentOf(Math.max(t.value,0),d)}))}exports.buildBarListRows=t;
2
+ //# sourceMappingURL=bar-list-model.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bar-list-model.cjs","names":[],"sources":["../../../src/components/BarList/bar-list-model.ts"],"sourcesContent":["// The arithmetic behind BarList, kept out of the component so the decisions that\n// are easy to get wrong — an empty total, a negative value, what \"100% wide\"\n// means — are testable without rendering anything.\n\nimport { percentOf } from \"@/utils/numbers\";\n\n/** One entry of a {@link BarList}. */\nexport interface BarListItem {\n /** What the row is called. */\n label: string;\n /** The measured amount. */\n value: number;\n /** Override the bar colour. Defaults to the chart series token for its position. */\n color?: string;\n}\n\n/** How the list orders itself before drawing. */\nexport type BarListSort = \"desc\" | \"asc\" | \"none\";\n\n/** One row, with everything the component needs to draw it. */\nexport interface BarListRow extends BarListItem {\n /** Share of the total, 0–100. */\n percentage: number;\n /** Bar width relative to the largest row, 0–100. */\n width: number;\n /** Position in the original palette cycle. */\n index: number;\n}\n\n/**\n * Order, truncate and measure the rows.\n *\n * Two different numbers come out of this, and conflating them is the classic bug\n * in a hand-written bar list:\n *\n * - `percentage` is the row's share of the **total**, which is what the label\n * claims when it reads \"32%\".\n * - `width` is relative to the **largest** row, so the biggest bar fills the\n * track. Scaling width by the total instead leaves every bar short in a list of\n * many small values, and the chart stops being readable exactly when it has the\n * most rows.\n *\n * The total counts positive values only. A negative amount draws no bar (a bar of\n * negative width does not exist) and reports 0%, but its number is still shown —\n * hiding the row would be worse than showing an odd one.\n *\n * @param items - The rows as given.\n * @param sort - Ordering to apply. `\"none\"` keeps the caller's order.\n * @param max - Keep at most this many rows.\n * @param otherLabel - Aggregate what `max` cut into one row with this label.\n * @returns The rows to draw, in order.\n */\nexport function buildBarListRows(\n items: readonly BarListItem[],\n sort: BarListSort,\n max: number | undefined,\n otherLabel: string | undefined,\n): BarListRow[] {\n const usable = items.filter((item) => Number.isFinite(item.value));\n const total = usable.reduce((sum, item) => sum + Math.max(item.value, 0), 0);\n\n const ordered =\n sort === \"none\"\n ? [...usable]\n : [...usable].sort((a, b) => (sort === \"asc\" ? a.value - b.value : b.value - a.value));\n\n const kept = max !== undefined && max > 0 ? ordered.slice(0, max) : ordered;\n const cut = max !== undefined && max > 0 ? ordered.slice(max) : [];\n\n const shown: BarListItem[] =\n otherLabel !== undefined && cut.length > 1\n ? [\n ...kept,\n {\n label: otherLabel,\n value: cut.reduce((sum, item) => sum + item.value, 0),\n },\n ]\n : otherLabel !== undefined && cut.length === 1\n ? [...kept, ...cut]\n : kept;\n\n const largest = shown.reduce((high, item) => Math.max(high, item.value), 0);\n\n return shown.map((item, index) => ({\n ...item,\n index,\n percentage: percentOf(Math.max(item.value, 0), total),\n width: percentOf(Math.max(item.value, 0), largest),\n }));\n}\n"],"mappings":"2CAoDA,SAAgB,EACZ,EACA,EACA,EACA,EACY,CACZ,IAAM,EAAS,EAAM,OAAQ,GAAS,OAAO,SAAS,EAAK,KAAK,CAAC,EAC3D,EAAQ,EAAO,QAAQ,EAAK,IAAS,EAAM,KAAK,IAAI,EAAK,MAAO,CAAC,EAAG,CAAC,EAErE,EACF,IAAS,OACH,CAAC,GAAG,CAAM,EACV,CAAC,GAAG,CAAM,CAAC,CAAC,MAAM,EAAG,IAAO,IAAS,MAAQ,EAAE,MAAQ,EAAE,MAAQ,EAAE,MAAQ,EAAE,KAAM,EAEvF,EAAO,IAAQ,IAAA,IAAa,EAAM,EAAI,EAAQ,MAAM,EAAG,CAAG,EAAI,EAC9D,EAAM,IAAQ,IAAA,IAAa,EAAM,EAAI,EAAQ,MAAM,CAAG,EAAI,CAAC,EAE3D,EACF,IAAe,IAAA,IAAa,EAAI,OAAS,EACnC,CACI,GAAG,EACH,CACI,MAAO,EACP,MAAO,EAAI,QAAQ,EAAK,IAAS,EAAM,EAAK,MAAO,CAAC,CACxD,CACJ,EACA,IAAe,IAAA,IAAa,EAAI,SAAW,EACzC,CAAC,GAAG,EAAM,GAAG,CAAG,EAChB,EAEN,EAAU,EAAM,QAAQ,EAAM,IAAS,KAAK,IAAI,EAAM,EAAK,KAAK,EAAG,CAAC,EAE1E,OAAO,EAAM,KAAK,EAAM,KAAW,CAC/B,GAAG,EACH,QACA,WAAY,EAAA,UAAU,KAAK,IAAI,EAAK,MAAO,CAAC,EAAG,CAAK,EACpD,MAAO,EAAA,UAAU,KAAK,IAAI,EAAK,MAAO,CAAC,EAAG,CAAO,CACrD,EAAE,CACN"}
@@ -0,0 +1,18 @@
1
+ import { percentOf as e } from "../../utils/numbers.js";
2
+ //#region src/components/BarList/bar-list-model.ts
3
+ function t(t, n, r, i) {
4
+ let a = t.filter((e) => Number.isFinite(e.value)), o = a.reduce((e, t) => e + Math.max(t.value, 0), 0), s = n === "none" ? [...a] : [...a].sort((e, t) => n === "asc" ? e.value - t.value : t.value - e.value), c = r !== void 0 && r > 0 ? s.slice(0, r) : s, l = r !== void 0 && r > 0 ? s.slice(r) : [], u = i !== void 0 && l.length > 1 ? [...c, {
5
+ label: i,
6
+ value: l.reduce((e, t) => e + t.value, 0)
7
+ }] : i !== void 0 && l.length === 1 ? [...c, ...l] : c, d = u.reduce((e, t) => Math.max(e, t.value), 0);
8
+ return u.map((t, n) => ({
9
+ ...t,
10
+ index: n,
11
+ percentage: e(Math.max(t.value, 0), o),
12
+ width: e(Math.max(t.value, 0), d)
13
+ }));
14
+ }
15
+ //#endregion
16
+ export { t as buildBarListRows };
17
+
18
+ //# sourceMappingURL=bar-list-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bar-list-model.js","names":[],"sources":["../../../src/components/BarList/bar-list-model.ts"],"sourcesContent":["// The arithmetic behind BarList, kept out of the component so the decisions that\n// are easy to get wrong — an empty total, a negative value, what \"100% wide\"\n// means — are testable without rendering anything.\n\nimport { percentOf } from \"@/utils/numbers\";\n\n/** One entry of a {@link BarList}. */\nexport interface BarListItem {\n /** What the row is called. */\n label: string;\n /** The measured amount. */\n value: number;\n /** Override the bar colour. Defaults to the chart series token for its position. */\n color?: string;\n}\n\n/** How the list orders itself before drawing. */\nexport type BarListSort = \"desc\" | \"asc\" | \"none\";\n\n/** One row, with everything the component needs to draw it. */\nexport interface BarListRow extends BarListItem {\n /** Share of the total, 0–100. */\n percentage: number;\n /** Bar width relative to the largest row, 0–100. */\n width: number;\n /** Position in the original palette cycle. */\n index: number;\n}\n\n/**\n * Order, truncate and measure the rows.\n *\n * Two different numbers come out of this, and conflating them is the classic bug\n * in a hand-written bar list:\n *\n * - `percentage` is the row's share of the **total**, which is what the label\n * claims when it reads \"32%\".\n * - `width` is relative to the **largest** row, so the biggest bar fills the\n * track. Scaling width by the total instead leaves every bar short in a list of\n * many small values, and the chart stops being readable exactly when it has the\n * most rows.\n *\n * The total counts positive values only. A negative amount draws no bar (a bar of\n * negative width does not exist) and reports 0%, but its number is still shown —\n * hiding the row would be worse than showing an odd one.\n *\n * @param items - The rows as given.\n * @param sort - Ordering to apply. `\"none\"` keeps the caller's order.\n * @param max - Keep at most this many rows.\n * @param otherLabel - Aggregate what `max` cut into one row with this label.\n * @returns The rows to draw, in order.\n */\nexport function buildBarListRows(\n items: readonly BarListItem[],\n sort: BarListSort,\n max: number | undefined,\n otherLabel: string | undefined,\n): BarListRow[] {\n const usable = items.filter((item) => Number.isFinite(item.value));\n const total = usable.reduce((sum, item) => sum + Math.max(item.value, 0), 0);\n\n const ordered =\n sort === \"none\"\n ? [...usable]\n : [...usable].sort((a, b) => (sort === \"asc\" ? a.value - b.value : b.value - a.value));\n\n const kept = max !== undefined && max > 0 ? ordered.slice(0, max) : ordered;\n const cut = max !== undefined && max > 0 ? ordered.slice(max) : [];\n\n const shown: BarListItem[] =\n otherLabel !== undefined && cut.length > 1\n ? [\n ...kept,\n {\n label: otherLabel,\n value: cut.reduce((sum, item) => sum + item.value, 0),\n },\n ]\n : otherLabel !== undefined && cut.length === 1\n ? [...kept, ...cut]\n : kept;\n\n const largest = shown.reduce((high, item) => Math.max(high, item.value), 0);\n\n return shown.map((item, index) => ({\n ...item,\n index,\n percentage: percentOf(Math.max(item.value, 0), total),\n width: percentOf(Math.max(item.value, 0), largest),\n }));\n}\n"],"mappings":";;AAoDA,SAAgB,EACZ,GACA,GACA,GACA,GACY;CACZ,IAAM,IAAS,EAAM,QAAQ,MAAS,OAAO,SAAS,EAAK,KAAK,CAAC,GAC3D,IAAQ,EAAO,QAAQ,GAAK,MAAS,IAAM,KAAK,IAAI,EAAK,OAAO,CAAC,GAAG,CAAC,GAErE,IACF,MAAS,SACH,CAAC,GAAG,CAAM,IACV,CAAC,GAAG,CAAM,CAAC,CAAC,MAAM,GAAG,MAAO,MAAS,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAM,GAEvF,IAAO,MAAQ,KAAA,KAAa,IAAM,IAAI,EAAQ,MAAM,GAAG,CAAG,IAAI,GAC9D,IAAM,MAAQ,KAAA,KAAa,IAAM,IAAI,EAAQ,MAAM,CAAG,IAAI,CAAC,GAE3D,IACF,MAAe,KAAA,KAAa,EAAI,SAAS,IACnC,CACI,GAAG,GACH;EACI,OAAO;EACP,OAAO,EAAI,QAAQ,GAAK,MAAS,IAAM,EAAK,OAAO,CAAC;CACxD,CACJ,IACA,MAAe,KAAA,KAAa,EAAI,WAAW,IACzC,CAAC,GAAG,GAAM,GAAG,CAAG,IAChB,GAEN,IAAU,EAAM,QAAQ,GAAM,MAAS,KAAK,IAAI,GAAM,EAAK,KAAK,GAAG,CAAC;CAE1E,OAAO,EAAM,KAAK,GAAM,OAAW;EAC/B,GAAG;EACH;EACA,YAAY,EAAU,KAAK,IAAI,EAAK,OAAO,CAAC,GAAG,CAAK;EACpD,OAAO,EAAU,KAAK,IAAI,EAAK,OAAO,CAAC,GAAG,CAAO;CACrD,EAAE;AACN"}
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/cn.cjs"),t=require("../../hooks/use-pagination.cjs"),n=require("../../hooks/use-announce.cjs"),r=require("../Pagination/Pagination.cjs"),i=require("../SearchBar/SearchBar.cjs"),a=require("../Table/Table.cjs"),o=require("../../utils/compare-values.cjs"),s=require("./DataTable.module.cjs"),c=require("./EditableCell.cjs"),l=require("./edit-labels.cjs");let u=require("react"),d=require("react/jsx-runtime");function f(e,t){return`${String(e)}::${String(t)}`}function p(e){return typeof e.header==`string`?e.header:String(e.key)}function m({data:m,columns:h,pageSize:g=10,searchable:_=!1,searchKeys:v,initialSort:ee,rowKey:y=(e,t)=>t,emptyMessage:b,onCellChange:x,editLabels:S,className:C,...te}){let[w,ne]=(0,u.useState)(``),[T,E]=(0,u.useState)(ee??null),{page:D,setPage:O}=t.usePagination(1,g),k=n.useAnnounce(),[A,j]=(0,u.useState)(null),[M,N]=(0,u.useState)(null),[P,F]=(0,u.useState)({}),[I,L]=(0,u.useState)({}),[R,z]=(0,u.useState)({}),B=(0,u.useMemo)(()=>({...l.DEFAULT_EDIT_LABELS,...S}),[S]),V=x!==void 0&&h.some(e=>e.editable),H=(0,u.useMemo)(()=>v&&v.length>0?v:h.filter(e=>{let t=m.find(t=>t[e.key]!=null),n=t?t[e.key]:void 0;return typeof n==`string`||typeof n==`number`}).map(e=>e.key),[v,h,m]),U=(0,u.useMemo)(()=>{let e=w.trim().toLowerCase();return!e||!_?m:m.filter(t=>H.some(n=>{let r=t[n];return r!=null&&String(r).toLowerCase().includes(e)}))},[m,w,_,H]),W=(0,u.useMemo)(()=>{if(!T)return U;let e=T.direction===`asc`?1:-1;return[...U].sort((t,n)=>o.compareValues(t[T.key],n[T.key])*e)},[U,T]),G=Math.max(1,Math.ceil(W.length/g));(0,u.useEffect)(()=>{D>G&&O(G)},[D,G,O]);let K=Math.min(D,G),q=(0,u.useMemo)(()=>{let e=(K-1)*g;return W.slice(e,e+g)},[W,K,g]);function re(e){E(t=>!t||t.key!==e?{key:e,direction:`asc`}:t.direction===`asc`?{key:e,direction:`desc`}:null)}let J=(0,u.useCallback)(e=>(K-1)*g+e,[K,g]),Y=(0,u.useMemo)(()=>{if(!V)return[];let e=[];return q.forEach((t,n)=>{let r=y(t,J(n));for(let t of h)t.editable&&e.push(f(r,t.key))}),e},[V,q,h,J]),X=(0,u.useCallback)((e,t,n)=>n in P?P[n]:e[t.key],[P]),Z=(0,u.useCallback)(async(e,t,n)=>{z(t=>({...t,[e]:!0}));try{await x?.(n),k(B.saved(p(t)))}catch(n){F(t=>{let n={...t};return delete n[e],n});let r=n instanceof Error&&n.message?n.message:B.saveFailed(p(t));L(t=>({...t,[e]:r}))}finally{z(t=>{let n={...t};return delete n[e],n})}},[x,k,B]),Q=(0,u.useCallback)((e,t)=>{if(t===`none`){j(null),N(e);return}let n=Y.indexOf(e),r=Y[n+(t===`next`?1:-1)];if(r===void 0){j(null),N(e);return}j(r),N(null)},[Y]),$=(0,u.useCallback)((e,t,n,r,i,a)=>{let o=X(e,t,n);if(i===(t.formatEdit?t.formatEdit(e):String(o??``))){Q(n,a);return}let s=t.parse?t.parse(i,e):t.editorType===`number`?Number(i):i.trim(),c=t.validate?.(s,e)??null;if(c){L(e=>({...e,[n]:c}));return}L(e=>{let t={...e};return delete t[n],t}),F(e=>({...e,[n]:s})),Q(n,a),Z(n,t,{row:e,key:t.key,value:s,previous:o,rowIndex:J(r)})},[X,Q,Z,J]),ie=(0,u.useMemo)(()=>h.map(e=>{let t=T?.key===e.key?T?.direction===`asc`?` ▲`:` ▼`:``,n=e.sortable?(0,d.jsxs)(`button`,{type:`button`,className:s.default.sortButton,onClick:()=>re(e.key),"aria-label":`Ordenar por ${p(e)}`,children:[e.header,(0,d.jsx)(`span`,{className:s.default.sortIndicator,"aria-hidden":!0,children:t})]}):e.header,r=t=>e.render?e.render(t):t[e.key]??null,i=(t,n)=>{let r={...t,[e.key]:n};return e.render?e.render(r):r[e.key]??null},a=V&&e.editable===!0;return{key:String(e.key),header:n,align:e.align,priority:e.priority,width:e.width,render:a?(t,n)=>{let r=f(y(t,J(n)),e.key),a=X(t,e,r),o=e.formatEdit?e.formatEdit(t):String(a??``);return(0,d.jsx)(c.EditableCell,{text:o,columnLabel:p(e),rowNumber:n+1,inputType:e.editorType??`text`,editing:A===r,refocus:M===r,saving:R[r]===!0,error:I[r]??null,errorId:`tempest-cell-error-${r.replace(/[^\w-]/g,`_`)}`,labels:B,onOpen:()=>{j(r),N(null)},onCommit:(i,a)=>$(t,e,r,n,i,a),onCancel:()=>{j(null),N(r),L(e=>{let t={...e};return delete t[r],t})},children:i(t,a)})}:r}}),[h,T,V,A,M,R,I,B,P,J,$]);return(0,d.jsxs)(`div`,{className:e.cn(s.default.wrapper,C),...te,children:[_&&(0,d.jsx)(i.SearchBar,{value:w,onChange:e=>{ne(e),O(1)},wrapperClassName:s.default.search}),(0,d.jsx)(a.Table,{columns:ie,data:q,rowKey:(e,t)=>y(e,J(t)),emptyMessage:b}),G>1&&(0,d.jsx)(r.Pagination,{page:K,totalPages:G,onPageChange:O,totalItems:W.length})]})}exports.DataTable=m;
1
+ const e=require("../../utils/cn.cjs"),t=require("../../hooks/use-pagination.cjs"),n=require("../../hooks/use-announce.cjs"),r=require("../Pagination/Pagination.cjs"),i=require("../SearchBar/SearchBar.cjs"),a=require("../Table/Table.cjs"),o=require("../../utils/compare-values.cjs"),s=require("./DataTable.module.cjs"),c=require("./EditableCell.cjs"),ee=require("./LoadingRows.cjs"),te=require("./use-dev-warnings.cjs"),ne=require("./edit-labels.cjs");let l=require("react"),u=require("react/jsx-runtime");function d(e,t){return`${String(e)}::${String(t)}`}function f(e){return typeof e.header==`string`?e.header:String(e.key)}function p({data:p,columns:m,pageSize:h=10,searchable:g=!1,searchKeys:_,initialSort:re,rowKey:v=(e,t)=>t,emptyMessage:ie,onCellChange:y,editLabels:b,totalItems:ae,page:x,onPageChange:S,manualSort:oe,onSortChange:se,manualSearch:ce,onSearchChange:le,loading:C=!1,className:ue,...de}){let[w,fe]=(0,l.useState)(``),[T,pe]=(0,l.useState)(re??null),{page:me,setPage:E}=t.usePagination(1,h),D=n.useAnnounce(),O=ae!==void 0,k=oe??O,A=ce??O,j=x??me,M=(0,l.useCallback)(e=>{x===void 0&&E(e),S?.(e)},[x,E,S]);te.useDevWarnings({serverMode:O,controlledPage:x,onPageChange:S,sortIsManual:k,onSortChange:se});let[N,P]=(0,l.useState)(null),[F,I]=(0,l.useState)(null),[L,R]=(0,l.useState)({}),[z,B]=(0,l.useState)({}),[V,H]=(0,l.useState)({}),U=(0,l.useMemo)(()=>({...ne.DEFAULT_EDIT_LABELS,...b}),[b]),W=y!==void 0&&m.some(e=>e.editable),he=(0,l.useMemo)(()=>_&&_.length>0?_:m.filter(e=>{let t=p.find(t=>t[e.key]!=null),n=t?t[e.key]:void 0;return typeof n==`string`||typeof n==`number`}).map(e=>e.key),[_,m,p]),G=(0,l.useMemo)(()=>{let e=w.trim().toLowerCase();return!e||!g||A?p:p.filter(t=>he.some(n=>{let r=t[n];return r!=null&&String(r).toLowerCase().includes(e)}))},[p,w,g,A,he]),K=(0,l.useMemo)(()=>{if(!T||k)return G;let e=T.direction===`asc`?1:-1;return[...G].sort((t,n)=>o.compareValues(t[T.key],n[T.key])*e)},[G,T,k]),ge=ae??K.length,q=Math.max(1,Math.ceil(ge/h));(0,l.useEffect)(()=>{!O&&j>q&&M(q)},[O,j,q,M]);let J=O?j:Math.min(j,q),Y=(0,l.useMemo)(()=>{if(O)return K;let e=(J-1)*h;return K.slice(e,e+h)},[O,K,J,h]);function _e(e){let t=T,n=!t||t.key!==e?{key:e,direction:`asc`}:t.direction===`asc`?{key:e,direction:`desc`}:null;pe(n),se?.(n)}let X=(0,l.useCallback)(e=>(J-1)*h+e,[J,h]),Z=(0,l.useMemo)(()=>{if(!W)return[];let e=[];return Y.forEach((t,n)=>{let r=v(t,X(n));for(let t of m)t.editable&&e.push(d(r,t.key))}),e},[W,Y,m,X]),Q=(0,l.useCallback)((e,t,n)=>n in L?L[n]:e[t.key],[L]),ve=(0,l.useCallback)(async(e,t,n)=>{H(t=>({...t,[e]:!0}));try{await y?.(n),D(U.saved(f(t)))}catch(n){R(t=>{let n={...t};return delete n[e],n});let r=n instanceof Error&&n.message?n.message:U.saveFailed(f(t));B(t=>({...t,[e]:r}))}finally{H(t=>{let n={...t};return delete n[e],n})}},[y,D,U]),$=(0,l.useCallback)((e,t)=>{if(t===`none`){P(null),I(e);return}let n=Z.indexOf(e),r=Z[n+(t===`next`?1:-1)];if(r===void 0){P(null),I(e);return}P(r),I(null)},[Z]),ye=(0,l.useCallback)((e,t,n,r,i,a)=>{let o=Q(e,t,n);if(i===(t.formatEdit?t.formatEdit(e):String(o??``))){$(n,a);return}let s=t.parse?t.parse(i,e):t.editorType===`number`?Number(i):i.trim(),c=t.validate?.(s,e)??null;if(c){B(e=>({...e,[n]:c}));return}B(e=>{let t={...e};return delete t[n],t}),R(e=>({...e,[n]:s})),$(n,a),ve(n,t,{row:e,key:t.key,value:s,previous:o,rowIndex:X(r)})},[Q,$,ve,X]),be=(0,l.useMemo)(()=>m.map(e=>{let t=T?.key===e.key?T?.direction===`asc`?` ▲`:` ▼`:``,n=e.sortable?(0,u.jsxs)(`button`,{type:`button`,className:s.default.sortButton,onClick:()=>_e(e.key),"aria-label":`Ordenar por ${f(e)}`,children:[e.header,(0,u.jsx)(`span`,{className:s.default.sortIndicator,"aria-hidden":!0,children:t})]}):e.header,r=t=>e.render?e.render(t):t[e.key]??null,i=(t,n)=>{let r={...t,[e.key]:n};return e.render?e.render(r):r[e.key]??null},a=W&&e.editable===!0;return{key:String(e.key),header:n,align:e.align,priority:e.priority,width:e.width,render:a?(t,n)=>{let r=d(v(t,X(n)),e.key),a=Q(t,e,r),o=e.formatEdit?e.formatEdit(t):String(a??``);return(0,u.jsx)(c.EditableCell,{text:o,columnLabel:f(e),rowNumber:n+1,inputType:e.editorType??`text`,editing:N===r,refocus:F===r,saving:V[r]===!0,error:z[r]??null,errorId:`tempest-cell-error-${r.replace(/[^\w-]/g,`_`)}`,labels:U,onOpen:()=>{P(r),I(null)},onCommit:(i,a)=>ye(t,e,r,n,i,a),onCancel:()=>{P(null),I(r),B(e=>{let t={...e};return delete t[r],t})},children:i(t,a)})}:r}}),[m,T,W,N,F,V,z,U,L,X,ye]),xe=C&&Y.length===0;return(0,u.jsxs)(`div`,{className:e.cn(s.default.wrapper,ue),...de,children:[g&&(0,u.jsx)(i.SearchBar,{value:w,onChange:e=>{fe(e),M(1),le?.(e)},wrapperClassName:s.default.search}),(0,u.jsx)(`div`,{className:e.cn(C&&!xe&&s.default.pending),"aria-busy":C||void 0,"data-testid":`tempest-datatable-body`,children:xe?(0,u.jsx)(ee.LoadingRows,{columns:be.length,rows:Math.min(h,8)}):(0,u.jsx)(a.Table,{columns:be,data:Y,rowKey:(e,t)=>v(e,X(t)),emptyMessage:ie})}),q>1&&(0,u.jsx)(r.Pagination,{page:J,totalPages:q,onPageChange:M,totalItems:ge})]})}exports.DataTable=p;
2
2
  //# sourceMappingURL=DataTable.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"DataTable.cjs","names":[],"sources":["../../../src/components/DataTable/DataTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the table does four jobs\n * a caller turns on independently — paging (pageSize), search (searchable,\n * searchKeys), sort (initialSort) and inline edit (onCellChange, editLabels) — over\n * one row model (data, columns, rowKey, emptyMessage). The body is long because\n * those four share the derived-rows pipeline: filter, then sort, then page, then map\n * to cells, in that order and off the same memo.\n */\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\nimport { usePagination } from \"@/hooks\";\nimport { useAnnounce } from \"@/hooks/use-announce\";\nimport { Table, type TableAlign, type TableColumn, type TablePriority } from \"../Table\";\nimport { Pagination } from \"../Pagination\";\nimport { SearchBar } from \"../SearchBar\";\nimport { EditableCell } from \"./EditableCell\";\nimport { DEFAULT_EDIT_LABELS, type CellCommitMove, type DataTableEditLabels } from \"./edit-labels\";\nimport styles from \"./DataTable.module.css\";\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface DataTableSort<T> {\n key: keyof T;\n direction: SortDirection;\n}\n\n/** Input types an editable column can use. */\nexport type DataTableEditorType = \"text\" | \"number\" | \"date\" | \"email\" | \"tel\" | \"url\";\n\n/** One accepted cell edit, handed to `onCellChange`. */\nexport interface DataTableCellChange<T> {\n /** The row as it was before the edit. */\n row: T;\n /** Which column changed. */\n key: keyof T;\n /** The parsed new value. */\n value: unknown;\n /** The value that was displayed before the edit. */\n previous: unknown;\n /** Index of the row in the full `data` array. */\n rowIndex: number;\n}\n\n/**\n * Column definition for {@link DataTable}. Extends the headless {@link Table}\n * column shape with a typed `key`, opt-in sorting, opt-in inline editing, and the\n * visual options that are forwarded to the underlying Table cell.\n */\nexport interface DataTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `String(row[key])`. */\n render?: (row: T) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment forwarded to the Table cell. */\n align?: TableAlign;\n /** Responsive visibility priority forwarded to the Table cell. */\n priority?: TablePriority;\n /** Fixed column width forwarded to the Table cell. */\n width?: string | number;\n /**\n * Let cells in this column be edited in place. Requires `onCellChange` on the\n * table; without it the column stays read-only.\n */\n editable?: boolean;\n /** Editor input type. Default `\"text\"`. */\n editorType?: DataTableEditorType;\n /** Text the editor opens with. Defaults to `String(value ?? \"\")`. */\n formatEdit?: (row: T) => string;\n /**\n * Turn the typed string into the stored value. Defaults to the trimmed string,\n * or `Number(raw)` when `editorType` is `\"number\"`.\n */\n parse?: (raw: string, row: T) => unknown;\n /** Return a message to reject the edit, or `null` to accept it. */\n validate?: (value: unknown, row: T) => string | null;\n}\n\nexport interface DataTableProps<T> extends HTMLAttributes<HTMLDivElement> {\n /** Full, unfiltered dataset. Sorting/filtering/pagination happen client-side. */\n data: T[];\n /** Column definitions. */\n columns: DataTableColumn<T>[];\n /** Rows per page. Default 10. */\n pageSize?: number;\n /** Render a search input above the table. Default false. */\n searchable?: boolean;\n /**\n * Keys to match the search term against. When omitted, every column whose\n * value is a string or number is searched.\n */\n searchKeys?: (keyof T)[];\n /** Initial sort applied before any header interaction. */\n initialSort?: DataTableSort<T>;\n /** Stable key extractor for rows. Defaults to the row index. */\n rowKey?: (row: T, index: number) => string | number;\n /** Content shown when no rows match. */\n emptyMessage?: ReactNode;\n /**\n * Persist an accepted cell edit. Return a promise: while it is pending the cell\n * already shows the new value, and a rejection rolls that back and surfaces the\n * error in the cell. Without this prop no column is editable.\n */\n onCellChange?: (change: DataTableCellChange<T>) => void | Promise<void>;\n /** Override the PT-BR copy of the editing affordances. */\n editLabels?: Partial<DataTableEditLabels>;\n}\n\n/** Identity of one cell, stable across re-renders and pagination. */\nfunction cellId(rowKeyValue: string | number, columnKey: PropertyKey): string {\n return `${String(rowKeyValue)}::${String(columnKey)}`;\n}\n\nfunction headerText<T>(column: DataTableColumn<T>): string {\n return typeof column.header === \"string\" ? column.header : String(column.key);\n}\n\n/**\n * Stateful, headless data table built on top of {@link Table}. Adds\n * client-side searching, click-to-sort columns, pagination and opt-in inline\n * editing while delegating all table markup to the underlying Table component.\n *\n * - Clicking a sortable header cycles asc → desc → unsorted.\n * - Search matches a case-insensitive substring across `searchKeys`\n * (or every string/number column when not provided).\n * - Pagination is hidden when the result fits on a single page.\n * - A column with `editable` renders a button that opens an inline editor;\n * `Enter` commits, `Escape` discards, `Tab` walks to the next editable cell.\n *\n * Editing is strictly opt-in: with no `editable` column (or no `onCellChange`) the\n * rendered markup is byte-for-byte what it was before the feature existed, which\n * matters because the component is published.\n *\n * ## Optimistic, with a visible rollback\n *\n * An accepted edit is shown immediately and `onCellChange` runs in the background.\n * If it rejects, the cell returns to the old value **and** shows the reason as a\n * `role=\"alert\"` tied to the cell. A silent revert is worse than no optimistic\n * update at all: the user watched their edit appear and has no reason to doubt it.\n *\n * The header memo depends on `columns`, `sort` and the editing state only:\n * `toggleSort` and the commit callbacks are recreated each render but always close\n * over the same setters, so including them would rebuild every header on every\n * render without changing behaviour. That is why `exhaustive-deps` is silenced on\n * that dependency array.\n */\nexport function DataTable<T>({\n data,\n columns,\n pageSize = 10,\n searchable = false,\n searchKeys,\n initialSort,\n rowKey = (_row, index) => index,\n emptyMessage,\n onCellChange,\n editLabels,\n className,\n ...rest\n}: DataTableProps<T>) {\n const [search, setSearch] = useState<string>(\"\");\n const [sort, setSort] = useState<DataTableSort<T> | null>(initialSort ?? null);\n const { page, setPage } = usePagination(1, pageSize);\n const announce = useAnnounce();\n\n const [editing, setEditing] = useState<string | null>(null);\n const [refocus, setRefocus] = useState<string | null>(null);\n const [overrides, setOverrides] = useState<Record<string, unknown>>({});\n const [errors, setErrors] = useState<Record<string, string>>({});\n const [saving, setSaving] = useState<Record<string, boolean>>({});\n\n const labels = useMemo<DataTableEditLabels>(\n () => ({ ...DEFAULT_EDIT_LABELS, ...editLabels }),\n [editLabels],\n );\n const editingEnabled = onCellChange !== undefined && columns.some((column) => column.editable);\n\n const effectiveSearchKeys = useMemo<(keyof T)[]>(() => {\n if (searchKeys && searchKeys.length > 0) return searchKeys;\n return columns\n .filter((column) => {\n const sample = data.find((row) => row[column.key] != null);\n const value = sample ? sample[column.key] : undefined;\n return typeof value === \"string\" || typeof value === \"number\";\n })\n .map((column) => column.key);\n }, [searchKeys, columns, data]);\n\n const filtered = useMemo<T[]>(() => {\n const term = search.trim().toLowerCase();\n if (!term || !searchable) return data;\n return data.filter((row) =>\n effectiveSearchKeys.some((key) => {\n const value = row[key];\n return value != null && String(value).toLowerCase().includes(term);\n }),\n );\n }, [data, search, searchable, effectiveSearchKeys]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort) return filtered;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...filtered].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [filtered, sort]);\n\n const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));\n\n // Clamp the current page when the dataset shrinks (e.g. after filtering).\n useEffect(() => {\n if (page > totalPages) setPage(totalPages);\n }, [page, totalPages, setPage]);\n\n const safePage = Math.min(page, totalPages);\n const pageRows = useMemo<T[]>(() => {\n const start = (safePage - 1) * pageSize;\n return sorted.slice(start, start + pageSize);\n }, [sorted, safePage, pageSize]);\n\n function toggleSort(key: keyof T): void {\n setSort((current) => {\n if (!current || current.key !== key) return { key, direction: \"asc\" };\n if (current.direction === \"asc\") return { key, direction: \"desc\" };\n return null;\n });\n }\n\n const absoluteIndex = useCallback(\n (pageIndex: number) => (safePage - 1) * pageSize + pageIndex,\n [safePage, pageSize],\n );\n\n /**\n * Every editable cell on the page, row-major — the order `Tab` walks.\n *\n * Row-major and not column-major because a row is the record a user is\n * correcting; walking down a column would make them re-find their place on\n * every keystroke.\n */\n const editableCellIds = useMemo<string[]>(() => {\n if (!editingEnabled) return [];\n const ids: string[] = [];\n pageRows.forEach((row, index) => {\n const key = rowKey(row, absoluteIndex(index));\n for (const column of columns) {\n if (column.editable) ids.push(cellId(key, column.key));\n }\n });\n return ids;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editingEnabled, pageRows, columns, absoluteIndex]);\n\n const displayed = useCallback(\n (row: T, column: DataTableColumn<T>, id: string): unknown =>\n id in overrides ? overrides[id] : row[column.key],\n [overrides],\n );\n\n /**\n * Run the caller's `onCellChange` behind the optimistic update.\n *\n * Only the *success* is announced. The failure already renders as a\n * `role=\"alert\"` inside the cell, which screen readers read on insertion —\n * announcing it again from the shared region would read it twice and put the\n * same text in the document twice.\n */\n const persist = useCallback(\n async (id: string, column: DataTableColumn<T>, change: DataTableCellChange<T>) => {\n setSaving((current) => ({ ...current, [id]: true }));\n try {\n await onCellChange?.(change);\n announce(labels.saved(headerText(column)));\n } catch (error) {\n setOverrides((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n const message =\n error instanceof Error && error.message\n ? error.message\n : labels.saveFailed(headerText(column));\n setErrors((current) => ({ ...current, [id]: message }));\n } finally {\n setSaving((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n }\n },\n [onCellChange, announce, labels],\n );\n\n const moveFrom = useCallback(\n (id: string, move: CellCommitMove): void => {\n if (move === \"none\") {\n setEditing(null);\n setRefocus(id);\n return;\n }\n const index = editableCellIds.indexOf(id);\n const target = editableCellIds[index + (move === \"next\" ? 1 : -1)];\n if (target === undefined) {\n setEditing(null);\n setRefocus(id);\n return;\n }\n setEditing(target);\n setRefocus(null);\n },\n [editableCellIds],\n );\n\n /**\n * Parse, validate, stage optimistically, then save in the background.\n *\n * Validation runs before anything is staged, and a rejection leaves the editor\n * open with the message attached to the input — the user has to be able to fix\n * what they typed without retyping it.\n */\n const commit = useCallback(\n (\n row: T,\n column: DataTableColumn<T>,\n id: string,\n pageIndex: number,\n raw: string,\n move: CellCommitMove,\n ): void => {\n const previous = displayed(row, column, id);\n const currentText = column.formatEdit ? column.formatEdit(row) : String(previous ?? \"\");\n if (raw === currentText) {\n moveFrom(id, move);\n return;\n }\n\n const value = column.parse\n ? column.parse(raw, row)\n : column.editorType === \"number\"\n ? Number(raw)\n : raw.trim();\n\n const invalid = column.validate?.(value, row) ?? null;\n if (invalid) {\n setErrors((current) => ({ ...current, [id]: invalid }));\n return;\n }\n\n setErrors((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n setOverrides((current) => ({ ...current, [id]: value }));\n moveFrom(id, move);\n void persist(id, column, {\n row,\n key: column.key,\n value,\n previous,\n rowIndex: absoluteIndex(pageIndex),\n });\n },\n [displayed, moveFrom, persist, absoluteIndex],\n );\n\n const tableColumns = useMemo<TableColumn<T>[]>(\n () =>\n columns.map((column) => {\n const isSorted = sort?.key === column.key;\n const indicator = isSorted ? (sort?.direction === \"asc\" ? \" ▲\" : \" ▼\") : \"\";\n const header = column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n aria-label={`Ordenar por ${headerText(column)}`}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {indicator}\n </span>\n </button>\n ) : (\n column.header\n );\n\n const plainCell = (row: T): ReactNode => {\n if (column.render) return column.render(row);\n return (row[column.key] as ReactNode) ?? null;\n };\n\n /**\n * Render an editable cell's content against the optimistic value.\n *\n * The row is shallow-patched rather than the value passed alongside it,\n * because a column with a custom `render` (a `<Money>`, a badge) reads\n * the row — handing it the stale row would show the old number under a\n * cell the user just changed.\n */\n const patchedCell = (row: T, value: unknown): ReactNode => {\n const patched = { ...row, [column.key]: value } as T;\n if (column.render) return column.render(patched);\n return (patched[column.key] as ReactNode) ?? null;\n };\n\n const editable = editingEnabled && column.editable === true;\n\n return {\n key: String(column.key),\n header,\n align: column.align,\n priority: column.priority,\n width: column.width,\n render: editable\n ? (row: T, index: number) => {\n const id = cellId(rowKey(row, absoluteIndex(index)), column.key);\n const value = displayed(row, column, id);\n const text = column.formatEdit\n ? column.formatEdit(row)\n : String(value ?? \"\");\n return (\n <EditableCell\n text={text}\n columnLabel={headerText(column)}\n rowNumber={index + 1}\n inputType={column.editorType ?? \"text\"}\n editing={editing === id}\n refocus={refocus === id}\n saving={saving[id] === true}\n error={errors[id] ?? null}\n errorId={`tempest-cell-error-${id.replace(/[^\\w-]/g, \"_\")}`}\n labels={labels}\n onOpen={() => {\n setEditing(id);\n setRefocus(null);\n }}\n onCommit={(raw, move) =>\n commit(row, column, id, index, raw, move)\n }\n onCancel={() => {\n setEditing(null);\n setRefocus(id);\n setErrors((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n }}\n >\n {patchedCell(row, value)}\n </EditableCell>\n );\n }\n : plainCell,\n };\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n columns,\n sort,\n editingEnabled,\n editing,\n refocus,\n saving,\n errors,\n labels,\n overrides,\n absoluteIndex,\n commit,\n ],\n );\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n {searchable && (\n <SearchBar\n value={search}\n onChange={(value) => {\n setSearch(value);\n setPage(1);\n }}\n wrapperClassName={styles.search}\n />\n )}\n <Table\n columns={tableColumns}\n data={pageRows}\n rowKey={(row, index) => rowKey(row, absoluteIndex(index))}\n emptyMessage={emptyMessage}\n />\n {totalPages > 1 && (\n <Pagination\n page={safePage}\n totalPages={totalPages}\n onPageChange={setPage}\n totalItems={sorted.length}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DataTable.cjs","names":[],"sources":["../../../src/components/DataTable/DataTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the table does four jobs\n * a caller turns on independently — paging (pageSize), search (searchable,\n * searchKeys), sort (initialSort) and inline edit (onCellChange, editLabels) — over\n * one row model (data, columns, rowKey, emptyMessage). The body is long because\n * those four share the derived-rows pipeline: filter, then sort, then page, then map\n * to cells, in that order and off the same memo.\n */\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\nimport { usePagination } from \"@/hooks\";\nimport { useAnnounce } from \"@/hooks/use-announce\";\nimport { Table, type TableAlign, type TableColumn, type TablePriority } from \"../Table\";\nimport { Pagination } from \"../Pagination\";\nimport { SearchBar } from \"../SearchBar\";\nimport { EditableCell } from \"./EditableCell\";\nimport { LoadingRows } from \"./LoadingRows\";\nimport { useDevWarnings } from \"./use-dev-warnings\";\nimport { DEFAULT_EDIT_LABELS, type CellCommitMove, type DataTableEditLabels } from \"./edit-labels\";\nimport styles from \"./DataTable.module.css\";\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface DataTableSort<T> {\n key: keyof T;\n direction: SortDirection;\n}\n\n/** Input types an editable column can use. */\nexport type DataTableEditorType = \"text\" | \"number\" | \"date\" | \"email\" | \"tel\" | \"url\";\n\n/** One accepted cell edit, handed to `onCellChange`. */\nexport interface DataTableCellChange<T> {\n /** The row as it was before the edit. */\n row: T;\n /** Which column changed. */\n key: keyof T;\n /** The parsed new value. */\n value: unknown;\n /** The value that was displayed before the edit. */\n previous: unknown;\n /** Index of the row in the full `data` array. */\n rowIndex: number;\n}\n\n/**\n * Column definition for {@link DataTable}. Extends the headless {@link Table}\n * column shape with a typed `key`, opt-in sorting, opt-in inline editing, and the\n * visual options that are forwarded to the underlying Table cell.\n */\nexport interface DataTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `String(row[key])`. */\n render?: (row: T) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment forwarded to the Table cell. */\n align?: TableAlign;\n /** Responsive visibility priority forwarded to the Table cell. */\n priority?: TablePriority;\n /** Fixed column width forwarded to the Table cell. */\n width?: string | number;\n /**\n * Let cells in this column be edited in place. Requires `onCellChange` on the\n * table; without it the column stays read-only.\n */\n editable?: boolean;\n /** Editor input type. Default `\"text\"`. */\n editorType?: DataTableEditorType;\n /** Text the editor opens with. Defaults to `String(value ?? \"\")`. */\n formatEdit?: (row: T) => string;\n /**\n * Turn the typed string into the stored value. Defaults to the trimmed string,\n * or `Number(raw)` when `editorType` is `\"number\"`.\n */\n parse?: (raw: string, row: T) => unknown;\n /** Return a message to reject the edit, or `null` to accept it. */\n validate?: (value: unknown, row: T) => string | null;\n}\n\nexport interface DataTableProps<T> extends HTMLAttributes<HTMLDivElement> {\n /**\n * The rows to work with.\n *\n * By default this is the **full** dataset and sorting, searching and paging\n * all happen in memory. Pass `totalItems` and it becomes the current page as\n * the server returned it, with those three delegated to the caller.\n */\n data: T[];\n /** Column definitions. */\n columns: DataTableColumn<T>[];\n /** Rows per page. Default 10. */\n pageSize?: number;\n /** Render a search input above the table. Default false. */\n searchable?: boolean;\n /**\n * Keys to match the search term against. When omitted, every column whose\n * value is a string or number is searched.\n */\n searchKeys?: (keyof T)[];\n /** Initial sort applied before any header interaction. */\n initialSort?: DataTableSort<T>;\n /** Stable key extractor for rows. Defaults to the row index. */\n rowKey?: (row: T, index: number) => string | number;\n /** Content shown when no rows match. */\n emptyMessage?: ReactNode;\n /**\n * Persist an accepted cell edit. Return a promise: while it is pending the cell\n * already shows the new value, and a rejection rolls that back and surfaces the\n * error in the cell. Without this prop no column is editable.\n */\n onCellChange?: (change: DataTableCellChange<T>) => void | Promise<void>;\n /** Override the PT-BR copy of the editing affordances. */\n editLabels?: Partial<DataTableEditLabels>;\n /**\n * Total row count across every page — the `total` of a paginated envelope.\n *\n * Passing it switches the table to **server mode**: `data` is read as the\n * current page, the page count comes from this number instead of\n * `data.length`, and sorting and searching are delegated to the caller\n * (see `manualSort` / `manualSearch`, which are implied here). Pair it with\n * `page` and `onPageChange`.\n */\n totalItems?: number;\n /** Current page, 1-based. Controlled — required in server mode. */\n page?: number;\n /** Called with the next page. Required whenever `page` is controlled. */\n onPageChange?: (page: number) => void;\n /**\n * Sorting is the caller's job: clicking a header reports through\n * `onSortChange` and the rows are left in the order they arrived.\n *\n * Implied by `totalItems`, because sorting the page in memory would sort\n * *that page only* while the header claims the whole table is ordered.\n */\n manualSort?: boolean;\n /** Called with the next sort state — `null` when the header cycles back to unsorted. */\n onSortChange?: (sort: DataTableSort<T> | null) => void;\n /**\n * Searching is the caller's job: typing reports through `onSearchChange` and\n * the rows are left as they arrived.\n *\n * Implied by `totalItems`. Filtering the current page would hide the rows\n * that do not match *on this page* and show nothing for a term that only\n * matches on page three — an empty table that looks like \"no results\".\n */\n manualSearch?: boolean;\n /** Called with the current search term (debouncing, if any, is the caller's). */\n onSearchChange?: (term: string) => void;\n /**\n * A fetch is in flight.\n *\n * With rows already on screen they stay put, dimmed and `aria-busy`, so the\n * page does not jump under the cursor between pages. With no rows yet it\n * renders placeholder lines at full height, which is a different statement\n * from `emptyMessage`: \"loading\" and \"there is nothing\" are not the same\n * screen.\n */\n loading?: boolean;\n}\n\n/** Identity of one cell, stable across re-renders and pagination. */\nfunction cellId(rowKeyValue: string | number, columnKey: PropertyKey): string {\n return `${String(rowKeyValue)}::${String(columnKey)}`;\n}\n\nfunction headerText<T>(column: DataTableColumn<T>): string {\n return typeof column.header === \"string\" ? column.header : String(column.key);\n}\n\n/**\n * Stateful, headless data table built on top of {@link Table}. Adds\n * client-side searching, click-to-sort columns, pagination and opt-in inline\n * editing while delegating all table markup to the underlying Table component.\n *\n * - Clicking a sortable header cycles asc → desc → unsorted.\n * - Search matches a case-insensitive substring across `searchKeys`\n * (or every string/number column when not provided).\n * - Pagination is hidden when the result fits on a single page.\n * - A column with `editable` renders a button that opens an inline editor;\n * `Enter` commits, `Escape` discards, `Tab` walks to the next editable cell.\n *\n * Editing is strictly opt-in: with no `editable` column (or no `onCellChange`) the\n * rendered markup is byte-for-byte what it was before the feature existed, which\n * matters because the component is published.\n *\n * ## Optimistic, with a visible rollback\n *\n * An accepted edit is shown immediately and `onCellChange` runs in the background.\n * If it rejects, the cell returns to the old value **and** shows the reason as a\n * `role=\"alert\"` tied to the cell. A silent revert is worse than no optimistic\n * update at all: the user watched their edit appear and has no reason to doubt it.\n *\n * The header memo depends on `columns`, `sort` and the editing state only:\n * `toggleSort` and the commit callbacks are recreated each render but always close\n * over the same setters, so including them would rebuild every header on every\n * render without changing behaviour. That is why `exhaustive-deps` is silenced on\n * that dependency array.\n */\nexport function DataTable<T>({\n data,\n columns,\n pageSize = 10,\n searchable = false,\n searchKeys,\n initialSort,\n rowKey = (_row, index) => index,\n emptyMessage,\n onCellChange,\n editLabels,\n totalItems,\n page: controlledPage,\n onPageChange,\n manualSort,\n onSortChange,\n manualSearch,\n onSearchChange,\n loading = false,\n className,\n ...rest\n}: DataTableProps<T>) {\n const [search, setSearch] = useState<string>(\"\");\n const [sort, setSort] = useState<DataTableSort<T> | null>(initialSort ?? null);\n const { page: internalPage, setPage: setInternalPage } = usePagination(1, pageSize);\n const announce = useAnnounce();\n\n const serverMode = totalItems !== undefined;\n const sortIsManual = manualSort ?? serverMode;\n const searchIsManual = manualSearch ?? serverMode;\n const page = controlledPage ?? internalPage;\n\n const setPage = useCallback(\n (next: number) => {\n if (controlledPage === undefined) setInternalPage(next);\n onPageChange?.(next);\n },\n [controlledPage, setInternalPage, onPageChange],\n );\n\n useDevWarnings({ serverMode, controlledPage, onPageChange, sortIsManual, onSortChange });\n\n const [editing, setEditing] = useState<string | null>(null);\n const [refocus, setRefocus] = useState<string | null>(null);\n const [overrides, setOverrides] = useState<Record<string, unknown>>({});\n const [errors, setErrors] = useState<Record<string, string>>({});\n const [saving, setSaving] = useState<Record<string, boolean>>({});\n\n const labels = useMemo<DataTableEditLabels>(\n () => ({ ...DEFAULT_EDIT_LABELS, ...editLabels }),\n [editLabels],\n );\n const editingEnabled = onCellChange !== undefined && columns.some((column) => column.editable);\n\n const effectiveSearchKeys = useMemo<(keyof T)[]>(() => {\n if (searchKeys && searchKeys.length > 0) return searchKeys;\n return columns\n .filter((column) => {\n const sample = data.find((row) => row[column.key] != null);\n const value = sample ? sample[column.key] : undefined;\n return typeof value === \"string\" || typeof value === \"number\";\n })\n .map((column) => column.key);\n }, [searchKeys, columns, data]);\n\n const filtered = useMemo<T[]>(() => {\n const term = search.trim().toLowerCase();\n if (!term || !searchable || searchIsManual) return data;\n return data.filter((row) =>\n effectiveSearchKeys.some((key) => {\n const value = row[key];\n return value != null && String(value).toLowerCase().includes(term);\n }),\n );\n }, [data, search, searchable, searchIsManual, effectiveSearchKeys]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort || sortIsManual) return filtered;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...filtered].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [filtered, sort, sortIsManual]);\n\n const rowCount = totalItems ?? sorted.length;\n const totalPages = Math.max(1, Math.ceil(rowCount / pageSize));\n\n /**\n * Clamp the current page when the dataset shrinks (e.g. after filtering).\n *\n * Skipped in server mode: `page` belongs to the caller there, and a clamp\n * fired against a `totalItems` that has not caught up with the new filter\n * yet would send them a page they did not ask for, mid-fetch.\n */\n useEffect(() => {\n if (!serverMode && page > totalPages) setPage(totalPages);\n }, [serverMode, page, totalPages, setPage]);\n\n const safePage = serverMode ? page : Math.min(page, totalPages);\n const pageRows = useMemo<T[]>(() => {\n if (serverMode) return sorted;\n const start = (safePage - 1) * pageSize;\n return sorted.slice(start, start + pageSize);\n }, [serverMode, sorted, safePage, pageSize]);\n\n function toggleSort(key: keyof T): void {\n const current = sort;\n const next: DataTableSort<T> | null =\n !current || current.key !== key\n ? { key, direction: \"asc\" }\n : current.direction === \"asc\"\n ? { key, direction: \"desc\" }\n : null;\n setSort(next);\n onSortChange?.(next);\n }\n\n const absoluteIndex = useCallback(\n (pageIndex: number) => (safePage - 1) * pageSize + pageIndex,\n [safePage, pageSize],\n );\n\n /**\n * Every editable cell on the page, row-major — the order `Tab` walks.\n *\n * Row-major and not column-major because a row is the record a user is\n * correcting; walking down a column would make them re-find their place on\n * every keystroke.\n */\n const editableCellIds = useMemo<string[]>(() => {\n if (!editingEnabled) return [];\n const ids: string[] = [];\n pageRows.forEach((row, index) => {\n const key = rowKey(row, absoluteIndex(index));\n for (const column of columns) {\n if (column.editable) ids.push(cellId(key, column.key));\n }\n });\n return ids;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editingEnabled, pageRows, columns, absoluteIndex]);\n\n const displayed = useCallback(\n (row: T, column: DataTableColumn<T>, id: string): unknown =>\n id in overrides ? overrides[id] : row[column.key],\n [overrides],\n );\n\n /**\n * Run the caller's `onCellChange` behind the optimistic update.\n *\n * Only the *success* is announced. The failure already renders as a\n * `role=\"alert\"` inside the cell, which screen readers read on insertion —\n * announcing it again from the shared region would read it twice and put the\n * same text in the document twice.\n */\n const persist = useCallback(\n async (id: string, column: DataTableColumn<T>, change: DataTableCellChange<T>) => {\n setSaving((current) => ({ ...current, [id]: true }));\n try {\n await onCellChange?.(change);\n announce(labels.saved(headerText(column)));\n } catch (error) {\n setOverrides((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n const message =\n error instanceof Error && error.message\n ? error.message\n : labels.saveFailed(headerText(column));\n setErrors((current) => ({ ...current, [id]: message }));\n } finally {\n setSaving((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n }\n },\n [onCellChange, announce, labels],\n );\n\n const moveFrom = useCallback(\n (id: string, move: CellCommitMove): void => {\n if (move === \"none\") {\n setEditing(null);\n setRefocus(id);\n return;\n }\n const index = editableCellIds.indexOf(id);\n const target = editableCellIds[index + (move === \"next\" ? 1 : -1)];\n if (target === undefined) {\n setEditing(null);\n setRefocus(id);\n return;\n }\n setEditing(target);\n setRefocus(null);\n },\n [editableCellIds],\n );\n\n /**\n * Parse, validate, stage optimistically, then save in the background.\n *\n * Validation runs before anything is staged, and a rejection leaves the editor\n * open with the message attached to the input — the user has to be able to fix\n * what they typed without retyping it.\n */\n const commit = useCallback(\n (\n row: T,\n column: DataTableColumn<T>,\n id: string,\n pageIndex: number,\n raw: string,\n move: CellCommitMove,\n ): void => {\n const previous = displayed(row, column, id);\n const currentText = column.formatEdit ? column.formatEdit(row) : String(previous ?? \"\");\n if (raw === currentText) {\n moveFrom(id, move);\n return;\n }\n\n const value = column.parse\n ? column.parse(raw, row)\n : column.editorType === \"number\"\n ? Number(raw)\n : raw.trim();\n\n const invalid = column.validate?.(value, row) ?? null;\n if (invalid) {\n setErrors((current) => ({ ...current, [id]: invalid }));\n return;\n }\n\n setErrors((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n setOverrides((current) => ({ ...current, [id]: value }));\n moveFrom(id, move);\n void persist(id, column, {\n row,\n key: column.key,\n value,\n previous,\n rowIndex: absoluteIndex(pageIndex),\n });\n },\n [displayed, moveFrom, persist, absoluteIndex],\n );\n\n const tableColumns = useMemo<TableColumn<T>[]>(\n () =>\n columns.map((column) => {\n const isSorted = sort?.key === column.key;\n const indicator = isSorted ? (sort?.direction === \"asc\" ? \" ▲\" : \" ▼\") : \"\";\n const header = column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n aria-label={`Ordenar por ${headerText(column)}`}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {indicator}\n </span>\n </button>\n ) : (\n column.header\n );\n\n const plainCell = (row: T): ReactNode => {\n if (column.render) return column.render(row);\n return (row[column.key] as ReactNode) ?? null;\n };\n\n /**\n * Render an editable cell's content against the optimistic value.\n *\n * The row is shallow-patched rather than the value passed alongside it,\n * because a column with a custom `render` (a `<Money>`, a badge) reads\n * the row — handing it the stale row would show the old number under a\n * cell the user just changed.\n */\n const patchedCell = (row: T, value: unknown): ReactNode => {\n const patched = { ...row, [column.key]: value } as T;\n if (column.render) return column.render(patched);\n return (patched[column.key] as ReactNode) ?? null;\n };\n\n const editable = editingEnabled && column.editable === true;\n\n return {\n key: String(column.key),\n header,\n align: column.align,\n priority: column.priority,\n width: column.width,\n render: editable\n ? (row: T, index: number) => {\n const id = cellId(rowKey(row, absoluteIndex(index)), column.key);\n const value = displayed(row, column, id);\n const text = column.formatEdit\n ? column.formatEdit(row)\n : String(value ?? \"\");\n return (\n <EditableCell\n text={text}\n columnLabel={headerText(column)}\n rowNumber={index + 1}\n inputType={column.editorType ?? \"text\"}\n editing={editing === id}\n refocus={refocus === id}\n saving={saving[id] === true}\n error={errors[id] ?? null}\n errorId={`tempest-cell-error-${id.replace(/[^\\w-]/g, \"_\")}`}\n labels={labels}\n onOpen={() => {\n setEditing(id);\n setRefocus(null);\n }}\n onCommit={(raw, move) =>\n commit(row, column, id, index, raw, move)\n }\n onCancel={() => {\n setEditing(null);\n setRefocus(id);\n setErrors((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n }}\n >\n {patchedCell(row, value)}\n </EditableCell>\n );\n }\n : plainCell,\n };\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n columns,\n sort,\n editingEnabled,\n editing,\n refocus,\n saving,\n errors,\n labels,\n overrides,\n absoluteIndex,\n commit,\n ],\n );\n\n const showSkeleton = loading && pageRows.length === 0;\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n {searchable && (\n <SearchBar\n value={search}\n onChange={(value) => {\n setSearch(value);\n setPage(1);\n onSearchChange?.(value);\n }}\n wrapperClassName={styles.search}\n />\n )}\n <div\n className={cn(loading && !showSkeleton && styles.pending)}\n aria-busy={loading || undefined}\n data-testid=\"tempest-datatable-body\"\n >\n {showSkeleton ? (\n <LoadingRows columns={tableColumns.length} rows={Math.min(pageSize, 8)} />\n ) : (\n <Table\n columns={tableColumns}\n data={pageRows}\n rowKey={(row, index) => rowKey(row, absoluteIndex(index))}\n emptyMessage={emptyMessage}\n />\n )}\n </div>\n {totalPages > 1 && (\n <Pagination\n page={safePage}\n totalPages={totalPages}\n onPageChange={setPage}\n totalItems={rowCount}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}