@usetheo/ui 0.34.0 → 0.35.1

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.
@@ -50,6 +50,17 @@ export declare function niceMax(values: number[]): number;
50
50
  * marker is what makes it visible.
51
51
  */
52
52
  export declare function seriesPath(points: TrendPoint[], xScale: (x: number) => number, yScale: (y: number) => number): string;
53
+ /**
54
+ * Quais posicoes do eixo recebem rotulo.
55
+ *
56
+ * Rotular 31 pontos em ~450px e ilegivel; rotular so as pontas nao responde "quando foi o pico".
57
+ * A densidade sai da largura REAL (`viewW`, que o ResizeObserver ja rastreia), com o primeiro e o
58
+ * ultimo SEMPRE presentes: sao as bordas da janela, e sem eles o leitor nao sabe de quando ate
59
+ * quando o grafico fala.
60
+ *
61
+ * Fora do JSX porque e aritmetica, e aritmetica isolada e aritmetica que a mutacao alcanca.
62
+ */
63
+ export declare function xTicks(axisXs: number[], viewW: number): number[];
53
64
  /**
54
65
  * Quais pontos recebem marcador.
55
66
  *
@@ -86,6 +97,20 @@ export interface TrendChartProps extends HTMLAttributes<HTMLElement> {
86
97
  * Ignored when ≤ 0. Default: auto (`niceMax`).
87
98
  */
88
99
  yMax?: number;
100
+ /**
101
+ * Formata o `x` para o eixo e para a tabela acessivel. **Opcional de proposito.**
102
+ *
103
+ * O contrato do `x` e ambiguo — "bucket index or epoch-ms" — e na pratica quase unanime: das seis
104
+ * fabricas de serie do consumidor, cinco passam INDICE e uma passa epoch-ms. Inferir por magnitude
105
+ * funcionaria e seria a forma exata do defeito que este componente ja pagou (a regra de marcador
106
+ * do M76 chaveava em `points.length` em vez de perguntar o que queria saber, e a densificacao do
107
+ * M144 a desligou em silencio). Quem sabe o que o `x` significa e o chamador.
108
+ *
109
+ * Sem esta prop, nada muda: nenhum rotulo de eixo X e desenhado e a tabela segue numerando.
110
+ */
111
+ xFormatter?: (x: number) => string;
112
+ /** Cabecalho da primeira coluna da tabela acessivel. "Point" sobre datas seria desonesto. */
113
+ xLabel?: string;
89
114
  }
90
115
  declare const TrendChart: import("react").ForwardRefExoticComponent<TrendChartProps & import("react").RefAttributes<HTMLElement>>;
91
116
  export { TrendChart };
package/dist/index.js CHANGED
@@ -4935,53 +4935,66 @@ function DataTableDefaultBody(props) {
4935
4935
  return /* @__PURE__ */ jsx("div", { className: cn("w-full", className), children: emptyState ?? /* @__PURE__ */ jsx(EmptyState, { title: "No data", description: "There's nothing here yet." }) });
4936
4936
  }
4937
4937
  const totalPages = pagination ? Math.ceil(sortedData.length / effectivePageSize) : 1;
4938
- return /* @__PURE__ */ jsxs("div", { className: cn("w-full", className), children: [
4939
- /* @__PURE__ */ jsxs(Table, { children: [
4940
- /* @__PURE__ */ jsx(Table.Header, { className: stickyHeader ? "sticky top-0 z-10 bg-card" : void 0, children: /* @__PURE__ */ jsx(
4941
- DataTableHeaderRow,
4938
+ return (
4939
+ // `overflow-x-auto`: uma tabela mais larga que o contêiner ROLA dentro dele, em vez de vazar por
4940
+ // cima do resto da página. Medido no consumidor: 11 colunas produziam 1838 px dentro de 1486 px,
4941
+ // e a raiz era só `w-full` — então o excesso escapava e desconfigurava a tela inteira. Não era
4942
+ // defeito de uma tela: era desta raiz, e alcançava qualquer consumidor com colunas demais.
4943
+ //
4944
+ // A variante virtualizada já fazia isto; a não-virtualizada é que tinha ficado para trás.
4945
+ //
4946
+ // Efeito colateral declarado: com `overflow-x` diferente de `visible`, o CSS computa `overflow-y`
4947
+ // como `auto` — logo este `div` vira contexto de rolagem, e um `stickyHeader` passa a grudar nele
4948
+ // e não na viewport. Para o cabeçalho grudar de fato, o consumidor precisa dar altura máxima ao
4949
+ // contêiner (`className="max-h-[...]"`), que é o que torna a rolagem vertical real.
4950
+ /* @__PURE__ */ jsxs("div", { className: cn("w-full overflow-x-auto", className), children: [
4951
+ /* @__PURE__ */ jsxs(Table, { children: [
4952
+ /* @__PURE__ */ jsx(Table.Header, { className: stickyHeader ? "sticky top-0 z-10 bg-card" : void 0, children: /* @__PURE__ */ jsx(
4953
+ DataTableHeaderRow,
4954
+ {
4955
+ columns,
4956
+ sort,
4957
+ onSortClick: handleSort,
4958
+ hasExpandColumn: expandable !== void 0,
4959
+ hasActionsColumn: rowActions !== void 0
4960
+ }
4961
+ ) }),
4962
+ /* @__PURE__ */ jsx(Table.Body, { children: visibleData.map((row) => {
4963
+ const key = rowKey(row);
4964
+ const expandedContent = expandable ? expandable(row) : null;
4965
+ const isExpandable = expandedContent !== null && expandedContent !== void 0;
4966
+ const isExpanded = expanded.has(key);
4967
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
4968
+ /* @__PURE__ */ jsxs(Table.Row, { children: [
4969
+ expandable ? /* @__PURE__ */ jsx(Table.Cell, { children: isExpandable ? /* @__PURE__ */ jsx(
4970
+ "button",
4971
+ {
4972
+ type: "button",
4973
+ onClick: () => toggleExpand(key),
4974
+ "aria-expanded": isExpanded,
4975
+ "aria-controls": `expanded-${key}`,
4976
+ "aria-label": isExpanded ? "Collapse row" : "Expand row",
4977
+ className: "inline-flex items-center justify-center rounded-md p-0.5 hover:bg-muted",
4978
+ children: isExpanded ? /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": "true", className: "size-4" }) : /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true", className: "size-4" })
4979
+ }
4980
+ ) : null }) : null,
4981
+ columns.map((col) => /* @__PURE__ */ jsx(Table.Cell, { align: col.align, className: col.className, children: renderCellContent(row, col) }, col.key)),
4982
+ rowActions ? /* @__PURE__ */ jsx(DataTableRowActionsCell, { children: rowActions(row) }) : null
4983
+ ] }),
4984
+ isExpanded && isExpandable ? /* @__PURE__ */ jsx("tr", { id: `expanded-${key}`, children: /* @__PURE__ */ jsx("td", { colSpan: expandedColSpan, className: "bg-muted/30 p-4", children: expandedContent }) }) : null
4985
+ ] }, key);
4986
+ }) })
4987
+ ] }),
4988
+ pagination && totalPages > 1 ? /* @__PURE__ */ jsx("div", { className: "mt-4 flex items-center justify-end", children: /* @__PURE__ */ jsx(
4989
+ Pagination,
4942
4990
  {
4943
- columns,
4944
- sort,
4945
- onSortClick: handleSort,
4946
- hasExpandColumn: expandable !== void 0,
4947
- hasActionsColumn: rowActions !== void 0
4991
+ currentPage: currentPage + 1,
4992
+ totalPages,
4993
+ onPageChange: handlePageChange
4948
4994
  }
4949
- ) }),
4950
- /* @__PURE__ */ jsx(Table.Body, { children: visibleData.map((row) => {
4951
- const key = rowKey(row);
4952
- const expandedContent = expandable ? expandable(row) : null;
4953
- const isExpandable = expandedContent !== null && expandedContent !== void 0;
4954
- const isExpanded = expanded.has(key);
4955
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [
4956
- /* @__PURE__ */ jsxs(Table.Row, { children: [
4957
- expandable ? /* @__PURE__ */ jsx(Table.Cell, { children: isExpandable ? /* @__PURE__ */ jsx(
4958
- "button",
4959
- {
4960
- type: "button",
4961
- onClick: () => toggleExpand(key),
4962
- "aria-expanded": isExpanded,
4963
- "aria-controls": `expanded-${key}`,
4964
- "aria-label": isExpanded ? "Collapse row" : "Expand row",
4965
- className: "inline-flex items-center justify-center rounded-md p-0.5 hover:bg-muted",
4966
- children: isExpanded ? /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": "true", className: "size-4" }) : /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true", className: "size-4" })
4967
- }
4968
- ) : null }) : null,
4969
- columns.map((col) => /* @__PURE__ */ jsx(Table.Cell, { align: col.align, className: col.className, children: renderCellContent(row, col) }, col.key)),
4970
- rowActions ? /* @__PURE__ */ jsx(DataTableRowActionsCell, { children: rowActions(row) }) : null
4971
- ] }),
4972
- isExpanded && isExpandable ? /* @__PURE__ */ jsx("tr", { id: `expanded-${key}`, children: /* @__PURE__ */ jsx("td", { colSpan: expandedColSpan, className: "bg-muted/30 p-4", children: expandedContent }) }) : null
4973
- ] }, key);
4974
- }) })
4975
- ] }),
4976
- pagination && totalPages > 1 ? /* @__PURE__ */ jsx("div", { className: "mt-4 flex items-center justify-end", children: /* @__PURE__ */ jsx(
4977
- Pagination,
4978
- {
4979
- currentPage: currentPage + 1,
4980
- totalPages,
4981
- onPageChange: handlePageChange
4982
- }
4983
- ) }) : null
4984
- ] });
4995
+ ) }) : null
4996
+ ] })
4997
+ );
4985
4998
  }
4986
4999
  var PageShell = forwardRef(
4987
5000
  ({
@@ -5109,6 +5122,17 @@ function seriesPath(points, xScale, yScale) {
5109
5122
  }
5110
5123
  var VIEW_W = 600;
5111
5124
  var PAD = { top: 8, right: 8, bottom: 4, left: 40 };
5125
+ var X_AXIS_PX = 14;
5126
+ var MIN_TICK_PX2 = 70;
5127
+ function xTicks(axisXs, viewW) {
5128
+ if (axisXs.length <= 2) return [...axisXs];
5129
+ const maxTicks = Math.max(2, Math.floor((viewW - PAD.left - PAD.right) / MIN_TICK_PX2));
5130
+ if (axisXs.length <= maxTicks) return [...axisXs];
5131
+ const passo = (axisXs.length - 1) / (maxTicks - 1);
5132
+ const escolhidos = /* @__PURE__ */ new Set();
5133
+ for (let i = 0; i < maxTicks; i++) escolhidos.add(Math.round(i * passo));
5134
+ return [...escolhidos].sort((a, b) => a - b).map((i) => axisXs[i]);
5135
+ }
5112
5136
  var SPARSE_MARKER_MAX = 5;
5113
5137
  function markedPoints(points) {
5114
5138
  const finito = (p) => Number.isFinite(p?.y);
@@ -5123,10 +5147,13 @@ var TrendChart = forwardRef(
5123
5147
  height = 180,
5124
5148
  valueFormatter = (v) => String(v),
5125
5149
  yMax: yMaxProp,
5150
+ xFormatter,
5151
+ xLabel,
5126
5152
  className,
5127
5153
  ...props
5128
5154
  }, ref) => {
5129
5155
  const [viewW, setViewW] = useState(VIEW_W);
5156
+ const [hoverIdx, setHoverIdx] = useState(null);
5130
5157
  const roRef = useRef(null);
5131
5158
  const setSvgNode = useCallback((node) => {
5132
5159
  roRef.current?.disconnect();
@@ -5172,72 +5199,149 @@ var TrendChart = forwardRef(
5172
5199
  const yMax = yMaxProp != null && yMaxProp > 0 ? yMaxProp : niceMax(allPoints.map((p) => p.y)) || 1;
5173
5200
  const axisXs = Array.from(new Set(allPoints.map((p) => p.x))).sort((a, b) => a - b);
5174
5201
  const yByX = series.map((s) => new Map(s.points.map((p) => [p.x, p.y])));
5202
+ const padBottom = PAD.bottom + (xFormatter ? X_AXIS_PX : 0);
5175
5203
  const xScale = linScale([xMin, xMax], [PAD.left, viewW - PAD.right]);
5176
- const yScale = linScale([0, yMax], [height - PAD.bottom, PAD.top]);
5204
+ const yScale = linScale([0, yMax], [height - padBottom, PAD.top]);
5177
5205
  return /* @__PURE__ */ jsxs("figure", { "data-slot": "trend-chart", ref, className: cn("space-y-1", className), ...props, children: [
5178
5206
  /* @__PURE__ */ jsx("figcaption", { className: "font-semibold text-label-caps text-muted-foreground uppercase", children: title }),
5179
5207
  /* @__PURE__ */ jsxs(
5180
- "svg",
5208
+ "div",
5181
5209
  {
5182
- ref: setSvgNode,
5183
- viewBox: `0 0 ${viewW} ${height}`,
5184
- preserveAspectRatio: "none",
5185
- role: "img",
5186
- "aria-label": `${title} trend`,
5187
- className: "w-full",
5188
- style: { height },
5210
+ "data-slot": "trend-chart-surface",
5211
+ className: "relative",
5212
+ onMouseMove: (e) => {
5213
+ const caixa = e.currentTarget.getBoundingClientRect();
5214
+ if (caixa.width === 0 || axisXs.length === 0) return;
5215
+ const frac = (e.clientX - caixa.left) / caixa.width;
5216
+ const alvo = xMin + frac * (xMax - xMin);
5217
+ let melhor = 0;
5218
+ for (let i = 1; i < axisXs.length; i++) {
5219
+ if (Math.abs(axisXs[i] - alvo) < Math.abs(axisXs[melhor] - alvo))
5220
+ melhor = i;
5221
+ }
5222
+ setHoverIdx(melhor);
5223
+ },
5224
+ onMouseLeave: () => setHoverIdx(null),
5189
5225
  children: [
5190
- /* @__PURE__ */ jsx(
5191
- "line",
5226
+ /* @__PURE__ */ jsxs(
5227
+ "svg",
5192
5228
  {
5193
- x1: PAD.left,
5194
- y1: height - PAD.bottom,
5195
- x2: viewW - PAD.right,
5196
- y2: height - PAD.bottom,
5197
- className: "stroke-border",
5198
- strokeWidth: 1
5229
+ ref: setSvgNode,
5230
+ viewBox: `0 0 ${viewW} ${height}`,
5231
+ preserveAspectRatio: "none",
5232
+ role: "img",
5233
+ "aria-label": `${title} trend`,
5234
+ className: "w-full",
5235
+ style: { height },
5236
+ children: [
5237
+ /* @__PURE__ */ jsx(
5238
+ "line",
5239
+ {
5240
+ x1: PAD.left,
5241
+ y1: height - padBottom,
5242
+ x2: viewW - PAD.right,
5243
+ y2: height - padBottom,
5244
+ className: "stroke-border",
5245
+ strokeWidth: 1
5246
+ }
5247
+ ),
5248
+ /* @__PURE__ */ jsx(
5249
+ "line",
5250
+ {
5251
+ x1: PAD.left,
5252
+ y1: PAD.top,
5253
+ x2: viewW - PAD.right,
5254
+ y2: PAD.top,
5255
+ className: "stroke-border/50",
5256
+ strokeWidth: 1,
5257
+ strokeDasharray: "2 3"
5258
+ }
5259
+ ),
5260
+ /* @__PURE__ */ jsx("text", { x: 4, y: PAD.top + 8, className: "fill-muted-foreground text-[10px]", children: valueFormatter(yMax) }),
5261
+ /* @__PURE__ */ jsx("text", { x: 4, y: height - padBottom, className: "fill-muted-foreground text-[10px]", children: "0" }),
5262
+ xFormatter ? xTicks(axisXs, viewW).map((x, i, todos) => /* @__PURE__ */ jsx(
5263
+ "text",
5264
+ {
5265
+ "data-slot": "trend-chart-xtick",
5266
+ x: xScale(x),
5267
+ y: height - 2,
5268
+ textAnchor: i === 0 ? "start" : i === todos.length - 1 ? "end" : "middle",
5269
+ className: "fill-muted-foreground text-[10px]",
5270
+ children: xFormatter(x)
5271
+ },
5272
+ x
5273
+ )) : null,
5274
+ series.map((s) => /* @__PURE__ */ jsxs("g", { children: [
5275
+ /* @__PURE__ */ jsx(
5276
+ "path",
5277
+ {
5278
+ "data-slot": "trend-chart-line",
5279
+ "data-series": s.name,
5280
+ d: seriesPath(s.points, xScale, yScale),
5281
+ fill: "none",
5282
+ stroke: s.color,
5283
+ strokeWidth: 1.5,
5284
+ children: /* @__PURE__ */ jsx("title", { children: s.name })
5285
+ }
5286
+ ),
5287
+ markedPoints(s.points).map((p, i) => /* @__PURE__ */ jsx(
5288
+ "circle",
5289
+ {
5290
+ "data-slot": "trend-chart-dot",
5291
+ cx: xScale(p.x),
5292
+ cy: yScale(p.y),
5293
+ r: 4,
5294
+ fill: s.color,
5295
+ children: /* @__PURE__ */ jsx("title", { children: s.name })
5296
+ },
5297
+ `${s.name}-${p.x}-${i}`
5298
+ ))
5299
+ ] }, s.name)),
5300
+ hoverIdx !== null && axisXs[hoverIdx] !== void 0 ? /* @__PURE__ */ jsx(
5301
+ "line",
5302
+ {
5303
+ "data-slot": "trend-chart-crosshair",
5304
+ x1: xScale(axisXs[hoverIdx]),
5305
+ y1: PAD.top,
5306
+ x2: xScale(axisXs[hoverIdx]),
5307
+ y2: height - padBottom,
5308
+ className: "stroke-muted-foreground/40",
5309
+ strokeWidth: 1
5310
+ }
5311
+ ) : null
5312
+ ]
5199
5313
  }
5200
5314
  ),
5201
- /* @__PURE__ */ jsx(
5202
- "line",
5315
+ hoverIdx !== null && axisXs[hoverIdx] !== void 0 ? /* @__PURE__ */ jsxs(
5316
+ "div",
5203
5317
  {
5204
- x1: PAD.left,
5205
- y1: PAD.top,
5206
- x2: viewW - PAD.right,
5207
- y2: PAD.top,
5208
- className: "stroke-border/50",
5209
- strokeWidth: 1,
5210
- strokeDasharray: "2 3"
5211
- }
5212
- ),
5213
- /* @__PURE__ */ jsx("text", { x: 4, y: PAD.top + 8, className: "fill-muted-foreground text-[10px]", children: valueFormatter(yMax) }),
5214
- /* @__PURE__ */ jsx("text", { x: 4, y: height - PAD.bottom, className: "fill-muted-foreground text-[10px]", children: "0" }),
5215
- series.map((s) => /* @__PURE__ */ jsxs("g", { children: [
5216
- /* @__PURE__ */ jsx(
5217
- "path",
5218
- {
5219
- "data-slot": "trend-chart-line",
5220
- "data-series": s.name,
5221
- d: seriesPath(s.points, xScale, yScale),
5222
- fill: "none",
5223
- stroke: s.color,
5224
- strokeWidth: 1.5,
5225
- children: /* @__PURE__ */ jsx("title", { children: s.name })
5226
- }
5227
- ),
5228
- markedPoints(s.points).map((p, i) => /* @__PURE__ */ jsx(
5229
- "circle",
5230
- {
5231
- "data-slot": "trend-chart-dot",
5232
- cx: xScale(p.x),
5233
- cy: yScale(p.y),
5234
- r: 4,
5235
- fill: s.color,
5236
- children: /* @__PURE__ */ jsx("title", { children: s.name })
5318
+ "data-slot": "trend-chart-tooltip",
5319
+ "aria-hidden": "true",
5320
+ className: "pointer-events-none absolute top-0 z-10 rounded border bg-popover px-2 py-1 text-[10px] text-popover-foreground shadow",
5321
+ style: {
5322
+ left: `${xScale(axisXs[hoverIdx]) / viewW * 100}%`,
5323
+ transform: "translateX(-50%)"
5237
5324
  },
5238
- `${s.name}-${p.x}-${i}`
5239
- ))
5240
- ] }, s.name))
5325
+ children: [
5326
+ /* @__PURE__ */ jsx("div", { className: "font-medium", children: xFormatter ? xFormatter(axisXs[hoverIdx]) : `#${hoverIdx + 1}` }),
5327
+ series.map((s, si) => {
5328
+ const y = yByX[si]?.get(axisXs[hoverIdx]);
5329
+ return /* @__PURE__ */ jsxs("div", { className: "whitespace-nowrap", children: [
5330
+ /* @__PURE__ */ jsx(
5331
+ "span",
5332
+ {
5333
+ className: "mr-1 inline-block size-2 rounded-full align-middle",
5334
+ style: { background: s.color }
5335
+ }
5336
+ ),
5337
+ s.name,
5338
+ ": ",
5339
+ y !== void 0 && Number.isFinite(y) ? valueFormatter(y) : "\u2014"
5340
+ ] }, s.name);
5341
+ })
5342
+ ]
5343
+ }
5344
+ ) : null
5241
5345
  ]
5242
5346
  }
5243
5347
  ),
@@ -5265,11 +5369,11 @@ var TrendChart = forwardRef(
5265
5369
  " \u2014 data table"
5266
5370
  ] }),
5267
5371
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
5268
- /* @__PURE__ */ jsx("th", { children: "Point" }),
5372
+ /* @__PURE__ */ jsx("th", { children: xLabel ?? "Point" }),
5269
5373
  series.map((s) => /* @__PURE__ */ jsx("th", { children: s.name }, s.name))
5270
5374
  ] }) }),
5271
5375
  /* @__PURE__ */ jsx("tbody", { children: axisXs.map((x, i) => /* @__PURE__ */ jsxs("tr", { children: [
5272
- /* @__PURE__ */ jsx("td", { children: i + 1 }),
5376
+ /* @__PURE__ */ jsx("td", { children: xFormatter ? xFormatter(x) : i + 1 }),
5273
5377
  series.map((s, si) => {
5274
5378
  const y = yByX[si]?.get(x);
5275
5379
  return /* @__PURE__ */ jsx("td", { children: y !== void 0 && Number.isFinite(y) ? valueFormatter(y) : "\u2014" }, s.name);
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@usetheo/ui",
3
- "version": "0.34.0",
3
+ "version": "0.35.1",
4
4
  "packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
- "description": "Violet Forge design system — generic React components + primitives (the non-AI foundation of @theokit/ui).",
7
+ "description": "Violet Forge design system \u2014 generic React components + primitives (the non-AI foundation of @theokit/ui).",
8
8
  "sideEffects": false,
9
9
  "files": [
10
10
  "dist"