@seedgrid/fe-components 2026.8.8 → 2026.9.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.
@@ -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 } from "lucide-react";
4
+ import { ChevronDown, ChevronUp, Search, Sparkles } from "lucide-react";
5
5
  import { SgInputText } from "../inputs/SgInputText";
6
6
  import { SgButton } from "../buttons/SgButton";
7
7
  import { t, useComponentsI18n } from "../i18n";
@@ -141,6 +141,31 @@ function setBranchChecked(id, value, childrenById, checked) {
141
141
  checked.delete(x);
142
142
  }
143
143
  }
144
+ function computeCascadeValue(id, childrenById, valueById) {
145
+ const kids = childrenById.get(id) ?? [];
146
+ if (!kids.length)
147
+ return { mixed: false, value: valueById.get(id) };
148
+ let acc = null;
149
+ for (const k of kids) {
150
+ const r = computeCascadeValue(k, childrenById, valueById);
151
+ if (r.mixed)
152
+ return { mixed: true };
153
+ if (acc === null)
154
+ acc = r;
155
+ else if (acc.mixed || acc.value !== r.value)
156
+ return { mixed: true };
157
+ }
158
+ return acc ?? { mixed: false, value: valueById.get(id) };
159
+ }
160
+ function setBranchValue(id, value, childrenById, valueById) {
161
+ const all = [id, ...collectDescendants(childrenById, id)];
162
+ for (const x of all) {
163
+ if (value === undefined)
164
+ valueById.delete(x);
165
+ else
166
+ valueById.set(x, value);
167
+ }
168
+ }
144
169
  function useControllable(opts, fallback) {
145
170
  const isControlled = "value" in opts && opts.value !== undefined;
146
171
  const [inner, setInner] = React.useState("defaultValue" in opts && opts.defaultValue !== undefined ? opts.defaultValue : fallback);
@@ -176,6 +201,10 @@ const densityMap = {
176
201
  normal: { rowPy: "py-1", rowPx: "px-2", gap: "gap-2", caret: "h-6 w-6", indent: 16 },
177
202
  comfortable: { rowPy: "py-1.5", rowPx: "px-2.5", gap: "gap-2.5", caret: "h-7 w-7", indent: 18 }
178
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";
179
208
  function toneClasses(tone) {
180
209
  if (tone === "muted") {
181
210
  return {
@@ -224,6 +253,13 @@ export const SgTreeView = React.forwardRef(function SgTreeView(props, ref) {
224
253
  const searchId = React.useId();
225
254
  const expandedSet = React.useMemo(() => new Set(expandedIds), [expandedIds]);
226
255
  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.
258
+ const tracks = props.tracks;
259
+ 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]);
227
263
  const { filtered, matchIds } = React.useMemo(() => filterTreeByQuery(nodes, search), [nodes, search]);
228
264
  React.useEffect(() => {
229
265
  if (!expandMatchesOnSearch)
@@ -321,10 +357,40 @@ export const SgTreeView = React.forwardRef(function SgTreeView(props, ref) {
321
357
  const isOpen = expandedSet.has(node.id);
322
358
  const isDisabled = !!node.disabled;
323
359
  const checkedState = checkable ? computeCheckedState(node.id, maps.childrenById, checkedSet) : "unchecked";
324
- return (_jsxs("div", { children: [_jsxs("div", { className: cn("flex items-center rounded-md", DY.rowPx, DY.rowPy, DY.gap, T.rowHover, isDisabled && "opacity-60"), style: { paddingLeft: 8 + depth * DY.indent }, children: [_jsx("button", { type: "button", className: cn(DY.caret, "shrink-0 rounded-md flex items-center justify-center", "hover:bg-sg-hover2 hover:bg-foreground/10", "focus:outline-none focus:ring-2 focus:ring-sg-focus/30", !hasChildren && "opacity-0 pointer-events-none"), onClick: () => hasChildren && !isDisabled && toggleExpand(node), "aria-label": isOpen ? t(i18n, "components.actions.collapse") : t(i18n, "components.actions.expand"), disabled: isDisabled, children: _jsx("span", { className: cn("transition-transform", isOpen && "rotate-90"), children: ">" }) }), checkable && (_jsx("input", { type: "checkbox", className: cn(SZ.checkbox, "rounded border border-sg-border bg-sg-surface", "focus:outline-none focus:ring-2 focus:ring-sg-focus/30"), disabled: isDisabled, checked: checkedState === "checked", ref: (el) => {
325
- if (el)
326
- el.indeterminate = checkedState === "indeterminate";
327
- }, onChange: () => toggleCheck(node) })), 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: () => {
360
+ return (_jsxs("div", { children: [_jsxs("div", { className: cn("flex items-center rounded-md", DY.rowPx, DY.rowPy, DY.gap, T.rowHover, isDisabled && "opacity-60"), style: { paddingLeft: 8 + depth * DY.indent }, children: [_jsx("button", { type: "button", className: cn(DY.caret, "shrink-0 rounded-md flex items-center justify-center", "hover:bg-sg-hover2 hover:bg-foreground/10", "focus:outline-none focus:ring-2 focus:ring-sg-focus/30", !hasChildren && "opacity-0 pointer-events-none"), onClick: () => hasChildren && !isDisabled && toggleExpand(node), "aria-label": isOpen ? t(i18n, "components.actions.collapse") : t(i18n, "components.actions.expand"), disabled: isDisabled, children: _jsx("span", { className: cn("transition-transform", isOpen && "rotate-90"), children: ">" }) }), checkable &&
361
+ (() => {
362
+ const checkboxEl = (_jsx("input", { type: "checkbox", className: cn(SZ.checkbox, "rounded border border-sg-border bg-sg-surface", "focus:outline-none focus:ring-2 focus:ring-sg-focus/30"), disabled: isDisabled, checked: checkedState === "checked", ref: (el) => {
363
+ if (el)
364
+ el.indeterminate = checkedState === "indeterminate";
365
+ }, onChange: () => toggleCheck(node) }));
366
+ // So' empacota (e reserva a largura da coluna) quando ha' cabecalho pra alinhar --
367
+ // 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);
369
+ })(), trackData.map(({ track, ...data }) => {
370
+ 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;
373
+ if (track.kind === "boolean") {
374
+ const checkedSetTrack = data.checkedSet;
375
+ 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) => {
377
+ if (el)
378
+ el.indeterminate = state === "indeterminate";
379
+ }, onChange: () => {
380
+ const next = new Set(checkedSetTrack);
381
+ setBranchChecked(node.id, state !== "checked", maps.childrenById, next);
382
+ track.onCheckedChange(Array.from(next));
383
+ } }), suggestedIndicator] }, track.id));
384
+ }
385
+ const valueById = data.valueById;
386
+ 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) => {
388
+ const v = e.target.value === "" ? undefined : e.target.value;
389
+ const nextValueById = new Map(valueById);
390
+ setBranchValue(node.id, v, maps.childrenById, nextValueById);
391
+ 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));
393
+ }), 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: () => {
328
394
  if (isDisabled)
329
395
  return;
330
396
  if (!hasChildren) {
@@ -337,6 +403,6 @@ export const SgTreeView = React.forwardRef(function SgTreeView(props, ref) {
337
403
  }, disabled: isDisabled, children: node.label })] }), hasChildren && isOpen && _jsx("div", { children: node.children.map((c) => renderNode(c, depth + 1)) })] }, node.id));
338
404
  };
339
405
  const hasAny = filtered.length > 0;
340
- 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: [_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") })] })] }) }))] })] }));
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") })] })] }) }))] })] }));
341
407
  });
342
408
  SgTreeView.displayName = "SgTreeView";