@seedgrid/fe-components 2026.9.0 → 2026.9.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.
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { ChevronDown, ChevronUp, Search, Sparkles } from "lucide-react";
4
+ import { ChevronDown, ChevronUp, Search } from "lucide-react";
5
5
  import { SgInputText } from "../inputs/SgInputText";
6
6
  import { SgButton } from "../buttons/SgButton";
7
7
  import { t, useComponentsI18n } from "../i18n";
@@ -201,10 +201,14 @@ const densityMap = {
201
201
  normal: { rowPy: "py-1", rowPx: "px-2", gap: "gap-2", caret: "h-6 w-6", indent: 16 },
202
202
  comfortable: { rowPy: "py-1.5", rowPx: "px-2.5", gap: "gap-2.5", caret: "h-7 w-7", indent: 18 }
203
203
  };
204
- // Largura fixa de cada coluna de trilha (checkbox ou select), usada tanto na linha quanto no
205
- // cabecalho -- e' o que deixa o texto do cabecalho ("Prioridade", "Tipo de conta") alinhado com
206
- // o controle por baixo dele, em vez de so' o controle estreito sem legenda nenhuma.
207
- const TRACK_CELL_CLASS = "w-[110px] shrink-0";
204
+ // Cada coluna de trilha tem largura propria, aplicada igual na linha e no cabecalho -- e' o que
205
+ // deixa o texto do cabecalho alinhado com o controle por baixo dele, em vez de so' o controle
206
+ // estreito sem legenda nenhuma.
207
+ const TRACK_CELL_CLASS = "shrink-0";
208
+ const TRACK_CELL_WIDTH_DEFAULT = "110px";
209
+ function trackCellWidth(track) {
210
+ return { width: track.width ?? TRACK_CELL_WIDTH_DEFAULT };
211
+ }
208
212
  function toneClasses(tone) {
209
213
  if (tone === "muted") {
210
214
  return {
@@ -253,13 +257,19 @@ export const SgTreeView = React.forwardRef(function SgTreeView(props, ref) {
253
257
  const searchId = React.useId();
254
258
  const expandedSet = React.useMemo(() => new Set(expandedIds), [expandedIds]);
255
259
  const checkedSet = React.useMemo(() => new Set(effectiveCheckedIds), [effectiveCheckedIds]);
256
- // Um Set/Map por trilha, montado uma vez por render (nao por no) -- mesma logica de
257
- // `checkedSet` acima, só que uma cópia por track em vez de uma única.
260
+ // Um Set/Map por trilha de selecao, montado uma vez por render (nao por no) -- mesma logica de
261
+ // `checkedSet` acima, só que uma cópia por track em vez de uma única. Trilha `custom` nao tem
262
+ // estado: passa direto.
258
263
  const tracks = props.tracks;
259
264
  const hasTracks = !!tracks && tracks.length > 0;
260
- const trackData = React.useMemo(() => (tracks ?? []).map((track) => track.kind === "boolean"
261
- ? { track, checkedSet: new Set(track.checkedIds) }
262
- : { track, valueById: new Map(Object.entries(track.valueByNodeId)) }), [tracks]);
265
+ const trackData = React.useMemo(() => (tracks ?? []).map((track) => {
266
+ if (track.kind === "boolean")
267
+ return { track, checkedSet: new Set(track.checkedIds) };
268
+ if (track.kind === "enum") {
269
+ return { track, valueById: new Map(Object.entries(track.valueByNodeId)) };
270
+ }
271
+ return { track };
272
+ }), [tracks]);
263
273
  const { filtered, matchIds } = React.useMemo(() => filterTreeByQuery(nodes, search), [nodes, search]);
264
274
  React.useEffect(() => {
265
275
  if (!expandMatchesOnSearch)
@@ -365,31 +375,33 @@ export const SgTreeView = React.forwardRef(function SgTreeView(props, ref) {
365
375
  }, onChange: () => toggleCheck(node) }));
366
376
  // So' empacota (e reserva a largura da coluna) quando ha' cabecalho pra alinhar --
367
377
  // sem `tracks`, o checkbox continua exatamente como sempre foi.
368
- return hasTracks ? (_jsx("div", { className: cn(TRACK_CELL_CLASS, "flex items-center"), children: checkboxEl })) : (checkboxEl);
378
+ return hasTracks ? (_jsx("div", { className: cn(TRACK_CELL_CLASS, "flex items-center"), style: trackCellWidth({}), children: checkboxEl })) : (checkboxEl);
369
379
  })(), trackData.map(({ track, ...data }) => {
380
+ if (track.kind === "custom") {
381
+ return (_jsx("div", { className: cn(TRACK_CELL_CLASS, "flex items-center gap-1"), style: trackCellWidth(track), children: track.render(node) }, track.id));
382
+ }
370
383
  const trackDisabled = isDisabled || !!track.disabled?.(node);
371
- const suggested = !!track.suggestedIds?.includes(node.id);
372
- const suggestedIndicator = suggested ? (_jsx("span", { title: t(i18n, "components.tree.suggested"), className: "shrink-0", children: track.suggestedIcon ?? (_jsx(Sparkles, { className: cn(SZ.icon, "text-[rgb(var(--sg-primary-600))]") })) })) : null;
384
+ const adornment = track.adornment?.(node) ?? null;
373
385
  if (track.kind === "boolean") {
374
386
  const checkedSetTrack = data.checkedSet;
375
387
  const state = computeCheckedState(node.id, maps.childrenById, checkedSetTrack);
376
- return (_jsxs("div", { className: cn(TRACK_CELL_CLASS, "flex items-center gap-1"), children: [_jsx("input", { type: "checkbox", "aria-label": track.ariaLabel, className: cn(SZ.checkbox, "shrink-0 rounded border border-sg-border bg-sg-surface", "focus:outline-none focus:ring-2 focus:ring-sg-focus/30"), disabled: trackDisabled, checked: state === "checked", ref: (el) => {
388
+ return (_jsxs("div", { className: cn(TRACK_CELL_CLASS, "flex items-center gap-1"), style: trackCellWidth(track), children: [_jsx("input", { type: "checkbox", "aria-label": track.ariaLabel, className: cn(SZ.checkbox, "shrink-0 rounded border border-sg-border bg-sg-surface", "focus:outline-none focus:ring-2 focus:ring-sg-focus/30"), disabled: trackDisabled, checked: state === "checked", ref: (el) => {
377
389
  if (el)
378
390
  el.indeterminate = state === "indeterminate";
379
391
  }, onChange: () => {
380
392
  const next = new Set(checkedSetTrack);
381
393
  setBranchChecked(node.id, state !== "checked", maps.childrenById, next);
382
394
  track.onCheckedChange(Array.from(next));
383
- } }), suggestedIndicator] }, track.id));
395
+ } }), adornment] }, track.id));
384
396
  }
385
397
  const valueById = data.valueById;
386
398
  const result = computeCascadeValue(node.id, maps.childrenById, valueById);
387
- return (_jsxs("div", { className: cn(TRACK_CELL_CLASS, "flex items-center gap-1"), children: [_jsxs("select", { "aria-label": track.id, disabled: trackDisabled, value: result.mixed ? "" : (result.value ?? ""), className: cn(SZ.text, "w-full rounded-md border border-sg-border bg-sg-surface px-1 py-0.5", "focus:outline-none focus:ring-2 focus:ring-sg-focus/30"), onChange: (e) => {
399
+ return (_jsxs("div", { className: cn(TRACK_CELL_CLASS, "flex items-center gap-1"), style: trackCellWidth(track), children: [_jsxs("select", { "aria-label": track.ariaLabel ?? track.label ?? track.id, disabled: trackDisabled, value: result.mixed ? "" : (result.value ?? ""), className: cn(SZ.text, "w-full rounded-md border border-sg-border bg-sg-surface px-1 py-0.5", "focus:outline-none focus:ring-2 focus:ring-sg-focus/30"), onChange: (e) => {
388
400
  const v = e.target.value === "" ? undefined : e.target.value;
389
401
  const nextValueById = new Map(valueById);
390
402
  setBranchValue(node.id, v, maps.childrenById, nextValueById);
391
403
  track.onChange(Object.fromEntries(nextValueById));
392
- }, children: [_jsx("option", { value: "", children: result.mixed ? (track.mixedLabel ?? "— misto —") : (track.placeholder ?? "—") }), track.options.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value)))] }), suggestedIndicator] }, track.id));
404
+ }, children: [_jsx("option", { value: "", children: result.mixed ? (track.mixedLabel ?? "— misto —") : (track.placeholder ?? "—") }), track.options.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value)))] }), adornment] }, track.id));
393
405
  }), node.icon && (_jsx("div", { className: cn("shrink-0", SZ.icon, iconTone === "primary" ? "text-[rgb(var(--sg-primary-600))]" : ""), children: node.icon })), _jsx("button", { type: "button", className: cn("flex-1 text-left rounded-md", SZ.text, isDisabled ? "cursor-not-allowed" : "cursor-pointer", "focus:outline-none focus:ring-2 focus:ring-sg-focus/30"), onClick: () => {
394
406
  if (isDisabled)
395
407
  return;
@@ -403,6 +415,6 @@ export const SgTreeView = React.forwardRef(function SgTreeView(props, ref) {
403
415
  }, disabled: isDisabled, children: node.label })] }), hasChildren && isOpen && _jsx("div", { children: node.children.map((c) => renderNode(c, depth + 1)) })] }, node.id));
404
416
  };
405
417
  const hasAny = filtered.length > 0;
406
- return (_jsxs("div", { className: cn("w-full", className), style: style, children: [_jsxs("div", { className: "mb-2 flex items-center gap-2", children: [searchable && (_jsx(SgInputText, { id: searchId, labelText: searchPlaceholder, placeholder: searchPlaceholder, prefixIcon: _jsx(Search, { size: 16 }), clearButton: true, filled: true, inputProps: { value: search }, onChange: (value) => setSearch(value) })), _jsx(SgButton, { severity: "secondary", appearance: "ghost", size: "sm", shape: "rounded", onClick: expandAll, leftIcon: _jsx(ChevronDown, { className: "size-4" }), children: t(i18n, "components.actions.expandAll") }), _jsx(SgButton, { severity: "secondary", appearance: "ghost", size: "sm", shape: "rounded", onClick: collapseAll, leftIcon: _jsx(ChevronUp, { className: "size-4" }), children: t(i18n, "components.actions.collapseAll") }), checkable && (_jsx(SgButton, { severity: "danger", appearance: "outline", size: "sm", shape: "rounded", onClick: clearChecked, title: t(i18n, "components.actions.clearSelection"), children: t(i18n, "components.actions.clear") }))] }), _jsxs("div", { className: cn("rounded-sg border border-sg-border", T.panelBg, "text-sg-text"), children: [hasTracks && (_jsxs("div", { className: cn("flex items-center border-b border-sg-border", DY.rowPx, DY.rowPy, DY.gap, SZ.text, "font-semibold text-sg-muted"), children: [_jsx("span", { className: cn(DY.caret, "shrink-0") }), checkable && (_jsx("div", { className: cn(TRACK_CELL_CLASS, "truncate"), title: props.checkableLabel, children: props.checkableLabel })), (tracks ?? []).map((track) => (_jsx("div", { className: cn(TRACK_CELL_CLASS, "truncate"), title: track.label, children: track.label }, track.id)))] })), _jsx("div", { className: cn("overflow-auto", maxHeightClassName), children: _jsx("div", { className: "p-2", children: !hasAny ? (_jsx("div", { className: cn("p-6 text-center", SZ.text, T.subtle), children: emptyText })) : (filtered.map((n) => renderNode(n, 0))) }) }), confirmEnabled && confirmCfg && (_jsx("div", { className: cn("border-t border-sg-border px-3 py-2", (confirmCfg.sticky ?? true) && "sticky bottom-0 bg-sg-surface/95 bg-background/95 backdrop-blur"), children: _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("div", { className: cn("text-xs", T.subtle), children: confirmCfg.hint ?? (_jsxs(_Fragment, { children: [t(i18n, "components.tree.selected", { count: confirmSelectedIds.length }), confirmSelection === "leafOnly" ? t(i18n, "components.tree.selectedLeafs") : ""] })) }), _jsxs("div", { className: "flex items-center gap-2", children: [(confirmCfg.showCancel ?? false) && (_jsx("button", { type: "button", onClick: onCancel, className: cn("rounded-md border border-sg-border", T.panelBg, "text-sg-text hover:bg-sg-hover", "focus:outline-none focus:ring-2 focus:ring-sg-focus/30", SZ.btn), children: confirmCfg.cancelLabel ?? t(i18n, "components.actions.cancel") })), _jsx(SgButton, { severity: "secondary", appearance: "outline", size: "md", shape: "rounded", onClick: clearChecked, disabled: !!confirmDisabled, children: t(i18n, "components.actions.clear") }), _jsx(SgButton, { severity: "primary", appearance: "solid", elevation: "sm", size: "md", shape: "rounded", disabled: !!confirmDisabled, onClick: onConfirm, children: confirmCfg.label ?? t(i18n, "components.actions.confirm") })] })] }) }))] })] }));
418
+ return (_jsxs("div", { className: cn("w-full", className), style: style, children: [_jsxs("div", { className: "mb-2 flex items-center gap-2", children: [searchable && (_jsx(SgInputText, { id: searchId, labelText: searchPlaceholder, placeholder: searchPlaceholder, prefixIcon: _jsx(Search, { size: 16 }), clearButton: true, filled: true, inputProps: { value: search }, onChange: (value) => setSearch(value) })), _jsx(SgButton, { severity: "secondary", appearance: "ghost", size: "sm", shape: "rounded", onClick: expandAll, leftIcon: _jsx(ChevronDown, { className: "size-4" }), children: t(i18n, "components.actions.expandAll") }), _jsx(SgButton, { severity: "secondary", appearance: "ghost", size: "sm", shape: "rounded", onClick: collapseAll, leftIcon: _jsx(ChevronUp, { className: "size-4" }), children: t(i18n, "components.actions.collapseAll") }), checkable && (_jsx(SgButton, { severity: "danger", appearance: "outline", size: "sm", shape: "rounded", onClick: clearChecked, title: t(i18n, "components.actions.clearSelection"), children: t(i18n, "components.actions.clear") }))] }), _jsxs("div", { className: cn("rounded-sg border border-sg-border", T.panelBg, "text-sg-text"), children: [hasTracks && (_jsxs("div", { className: cn("flex items-center border-b border-sg-border", DY.rowPx, DY.rowPy, DY.gap, SZ.text, "font-semibold text-sg-muted"), children: [_jsx("span", { className: cn(DY.caret, "shrink-0") }), checkable && (_jsx("div", { className: cn(TRACK_CELL_CLASS, "truncate"), style: trackCellWidth({}), title: props.checkableLabel, children: props.checkableLabel })), (tracks ?? []).map((track) => (_jsx("div", { className: cn(TRACK_CELL_CLASS, "truncate"), style: trackCellWidth(track), title: track.label, children: track.label }, track.id)))] })), _jsx("div", { className: cn("overflow-auto", maxHeightClassName), children: _jsx("div", { className: "p-2", children: !hasAny ? (_jsx("div", { className: cn("p-6 text-center", SZ.text, T.subtle), children: emptyText })) : (filtered.map((n) => renderNode(n, 0))) }) }), confirmEnabled && confirmCfg && (_jsx("div", { className: cn("border-t border-sg-border px-3 py-2", (confirmCfg.sticky ?? true) && "sticky bottom-0 bg-sg-surface/95 bg-background/95 backdrop-blur"), children: _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("div", { className: cn("text-xs", T.subtle), children: confirmCfg.hint ?? (_jsxs(_Fragment, { children: [t(i18n, "components.tree.selected", { count: confirmSelectedIds.length }), confirmSelection === "leafOnly" ? t(i18n, "components.tree.selectedLeafs") : ""] })) }), _jsxs("div", { className: "flex items-center gap-2", children: [(confirmCfg.showCancel ?? false) && (_jsx("button", { type: "button", onClick: onCancel, className: cn("rounded-md border border-sg-border", T.panelBg, "text-sg-text hover:bg-sg-hover", "focus:outline-none focus:ring-2 focus:ring-sg-focus/30", SZ.btn), children: confirmCfg.cancelLabel ?? t(i18n, "components.actions.cancel") })), _jsx(SgButton, { severity: "secondary", appearance: "outline", size: "md", shape: "rounded", onClick: clearChecked, disabled: !!confirmDisabled, children: t(i18n, "components.actions.clear") }), _jsx(SgButton, { severity: "primary", appearance: "solid", elevation: "sm", size: "md", shape: "rounded", disabled: !!confirmDisabled, onClick: onConfirm, children: confirmCfg.label ?? t(i18n, "components.actions.confirm") })] })] }) }))] })] }));
407
419
  });
408
420
  SgTreeView.displayName = "SgTreeView";