@fabio.caffarello/react-design-system 3.5.0 → 3.7.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.
@@ -0,0 +1,105 @@
1
+ import { type HTMLAttributes, type ReactNode } from "react";
2
+ export type StatTone = "neutral" | "success" | "warning" | "error";
3
+ export type StatAlign = "start" | "center";
4
+ export interface StatProps extends HTMLAttributes<HTMLDivElement> {
5
+ /**
6
+ * The metric value to display. Strings are rendered verbatim — formatting
7
+ * (number locale, currency, units, relative time, etc.) is the consumer's
8
+ * responsibility, not the design system's. Pass `null` or `undefined` to
9
+ * render the empty-state placeholder (see "Empty state" below).
10
+ */
11
+ value: ReactNode;
12
+ /**
13
+ * Short metric label (e.g. "Votos", "Alinhamento"). Required for screen
14
+ * reader context — the label describes what the value means.
15
+ */
16
+ label: string;
17
+ /**
18
+ * Optional third line of context below the value (e.g. "no banco",
19
+ * "últimos 12 m", "+3% no mês"). The `tone` prop styles THIS line — see
20
+ * `tone` for the contract.
21
+ */
22
+ hint?: ReactNode;
23
+ /**
24
+ * Optional icon rendered above the value (home-style stats use icons;
25
+ * detail-page stats typically don't).
26
+ */
27
+ icon?: ReactNode;
28
+ /**
29
+ * Block alignment. `start` left-aligns label/value/hint (detail-page
30
+ * style); `center` centers them (home-hero style).
31
+ * @default 'start'
32
+ */
33
+ align?: StatAlign;
34
+ /**
35
+ * Semantic tone for the metric — `neutral` for plain stats, the others
36
+ * for classified states (good/warning/bad).
37
+ *
38
+ * **Scope (contract).** Tone affects ONLY the `hint`, not the `value`,
39
+ * `label`, or `icon`. The `value` always renders in `text-fg-primary`
40
+ * regardless of tone; the `label` in `text-fg-secondary`; the `icon` in
41
+ * `text-icon-default`. This is deliberate — a colored value would
42
+ * compete with the label for attention and bias the reader's
43
+ * interpretation of the metric. If a future requirement needs the
44
+ * `value` (or icon) to inherit tone, that becomes a new prop or a
45
+ * semver-bound default change, not a surprise expansion of `tone`.
46
+ *
47
+ * Tone maps directly to the semantic foreground tokens (no new
48
+ * vocabulary): `neutral` → `text-fg-tertiary`, `success` →
49
+ * `text-fg-success`, `warning` → `text-fg-warning`, `error` →
50
+ * `text-fg-error`. See `.claude/rules/colors.md`.
51
+ *
52
+ * @default 'neutral'
53
+ */
54
+ tone?: StatTone;
55
+ }
56
+ /**
57
+ * `Stat` — a single statistic block (icon? + value + label + hint?).
58
+ *
59
+ * Composes with `StatGroup` (1-px-divider strip or grid) but is also
60
+ * valid standalone — a single `Stat` outside a group is a legitimate use
61
+ * case for a hero metric.
62
+ *
63
+ * ### Empty state contract
64
+ *
65
+ * When `value` is `null` OR `undefined`, the component renders the
66
+ * em-dash placeholder `—` (U+2014) with `aria-label="No data"` on its
67
+ * wrapper. The label is intentionally hard-coded in English in this
68
+ * version; a screen reader announcing "No data" in an otherwise
69
+ * Portuguese app is a known inconsistency accepted for now — if i18n
70
+ * becomes a requirement, an `emptyLabel?: string` prop will be added in
71
+ * a follow-up PR (semver-safe addition).
72
+ *
73
+ * Other falsy values — `0`, `""`, `false`, an empty fragment — are
74
+ * **legitimate values** and render as-is. The empty trigger is only
75
+ * `null`/`undefined`, because `0` (count = zero) is meaningful data that
76
+ * the consumer would not want masked.
77
+ *
78
+ * ### Server-safe
79
+ *
80
+ * Pure presentation — no hooks, no event handlers on the DOM. Ships in
81
+ * the `./server` entry alongside `StatGroup`. Consumer-supplied `icon`
82
+ * may itself be a client component; React's RSC boundary handles that
83
+ * normally.
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * // Home-style (centered, with icon)
88
+ * <Stat
89
+ * icon={<Users size={20} aria-hidden="true" />}
90
+ * value="9,4 mil"
91
+ * label="Parlamentares"
92
+ * align="center"
93
+ * />
94
+ *
95
+ * // Detail-page-style (start-aligned, with hint)
96
+ * <Stat
97
+ * value="87%"
98
+ * label="Alinhamento"
99
+ * hint="últimos 12 meses"
100
+ * tone="success"
101
+ * />
102
+ * ```
103
+ */
104
+ export declare function Stat({ value, label, hint, icon, align, tone, className, ...props }: StatProps): import("react").JSX.Element;
105
+ export default Stat;
@@ -0,0 +1,69 @@
1
+ import { type HTMLAttributes, type ReactNode } from "react";
2
+ export type StatGroupLayout = "strip" | "grid";
3
+ export type StatGroupCols = 2 | 3 | 4;
4
+ export interface StatGroupProps extends HTMLAttributes<HTMLDivElement> {
5
+ /**
6
+ * `strip` — single horizontal row, no wrap. Each `Stat` shares the row
7
+ * width via `flex-1`. Use when you guarantee the horizontal space
8
+ * (hero areas, wide dashboards). On narrow viewports the row does NOT
9
+ * reflow — choose `grid` if you need responsive collapse.
10
+ *
11
+ * `grid` — multi-column grid that reflows. Always 2-up on mobile,
12
+ * expands to `cols` columns at the `md` breakpoint (768 px) and up.
13
+ * Five or more children spill to a second row with the divider lines
14
+ * preserved.
15
+ *
16
+ * @default 'grid'
17
+ */
18
+ layout?: StatGroupLayout;
19
+ /**
20
+ * Desktop column count (≥ 768 px). Only effective in `layout="grid"`;
21
+ * ignored in `layout="strip"`. Mobile is always 2 columns regardless.
22
+ *
23
+ * @default 4
24
+ */
25
+ cols?: StatGroupCols;
26
+ children: ReactNode;
27
+ }
28
+ /**
29
+ * `StatGroup` — container for one or more `Stat` blocks with 1-px
30
+ * dividers between them.
31
+ *
32
+ * ### Divider technique
33
+ *
34
+ * The container has `bg-line-default` and `gap-px` (1 px); each child
35
+ * `Stat` carries its own `bg-surface-base`. The 1 px of gap exposes the
36
+ * container's background as the divider line, while each cell masks its
37
+ * own area with `bg-surface-base`. Works identically for the strip
38
+ * layout (vertical dividers) and the grid layout (vertical AND
39
+ * horizontal dividers, automatically, as the grid reflows). No separate
40
+ * `Divider` component, no per-cell border logic.
41
+ *
42
+ * The outer ring is `border border-line-default` matching the gap color,
43
+ * with `rounded-lg` and `overflow-hidden` clipping the inner cells to
44
+ * the radius.
45
+ *
46
+ * ### Server-safe
47
+ *
48
+ * Pure presentation — no hooks, no event handlers on the DOM. Ships in
49
+ * `./server` alongside `Stat`.
50
+ *
51
+ * @example
52
+ * ```tsx
53
+ * <StatGroup layout="strip">
54
+ * <Stat icon={<Users />} value="9,4 mil" label="Parlamentares" align="center" />
55
+ * <Stat icon={<FileText />} value="3,2 mil" label="Proposições" align="center" />
56
+ * <Stat icon={<Vote />} value="1,1 mil" label="Votações" align="center" />
57
+ * <Stat icon={<Clock />} value="há 18 dias" label="Última atualização" align="center" />
58
+ * </StatGroup>
59
+ *
60
+ * <StatGroup layout="grid" cols={4}>
61
+ * <Stat value="87%" label="Alinhamento" hint="últimos 12 m" tone="success" />
62
+ * <Stat value={null} label="Sem orientação" hint="no período" />
63
+ * <Stat value="R$ 187.472,95" label="Gastos" hint="no mandato" />
64
+ * <Stat value="42" label="Votações" hint="no banco" />
65
+ * </StatGroup>
66
+ * ```
67
+ */
68
+ export declare function StatGroup({ layout, cols, className, children, ...props }: StatGroupProps): import("react").JSX.Element;
69
+ export default StatGroup;
@@ -0,0 +1,4 @@
1
+ export { Stat, default } from "./Stat";
2
+ export type { StatProps, StatTone, StatAlign } from "./Stat";
3
+ export { StatGroup } from "./StatGroup";
4
+ export type { StatGroupProps, StatGroupLayout, StatGroupCols, } from "./StatGroup";
@@ -1,4 +1,6 @@
1
1
  export { default as Card } from "./Card";
2
+ export { Stat, StatGroup } from "./Stat";
3
+ export type { StatProps, StatTone, StatAlign, StatGroupProps, StatGroupLayout, StatGroupCols, } from "./Stat";
2
4
  export * from "./Form";
3
5
  export { default as Breadcrumb } from "./Breadcrumb";
4
6
  export type { BreadcrumbItem } from "./Breadcrumb";
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Options for `useScrollSpy`.
3
+ */
4
+ export interface UseScrollSpyOptions {
5
+ /**
6
+ * `IntersectionObserver` `rootMargin`. Shrinks the effective viewport
7
+ * the observer reports on. The default `"0px 0px -50% 0px"` shrinks
8
+ * the bottom edge by half — a section is considered "in view" only
9
+ * when part of it sits in the upper half of the viewport, which is
10
+ * the canonical "table-of-contents follows the scroll" behaviour. To
11
+ * compensate for a sticky header, prefix the top with a negative
12
+ * pixel value, e.g. `"-56px 0px -50% 0px"`.
13
+ *
14
+ * @default "0px 0px -50% 0px"
15
+ */
16
+ rootMargin?: string;
17
+ /**
18
+ * `IntersectionObserver` `threshold`. With the default `0`, the
19
+ * observer fires when any part of the target enters the (margin-
20
+ * shrunken) viewport.
21
+ *
22
+ * @default 0
23
+ */
24
+ threshold?: number | number[];
25
+ }
26
+ /**
27
+ * Track which section of a long scroll surface is currently in view,
28
+ * suitable for a table-of-contents nav that highlights the active section
29
+ * (the classic "scroll spy" pattern).
30
+ *
31
+ * The hook resolves each `id` to a DOM element via
32
+ * `document.getElementById`, observes those elements with a single
33
+ * `IntersectionObserver`, and returns the **id of the topmost visible
34
+ * section**. Returns `null` when nothing has reported as visible yet —
35
+ * including on the server, during the first render before the effect
36
+ * runs, and any frame where no observed section intersects the
37
+ * (margin-shrunken) viewport.
38
+ *
39
+ * ### Behavioural contract
40
+ *
41
+ * - **Return value.** `string | null`. `null` until at least one section
42
+ * has been reported intersecting; never falls back to a "first id"
43
+ * heuristic. Consumers that want a default highlight should fall back
44
+ * themselves: `active ?? ids[0]`.
45
+ * - **Tie-breaking.** When multiple sections intersect simultaneously,
46
+ * the hook picks the one **closest to the top of the viewport**
47
+ * (smallest `boundingClientRect.top`). This matches the user's
48
+ * expectation that scrolling DOWN advances the highlight forward, not
49
+ * backward.
50
+ * - **Missing ids.** An id that resolves to no element is skipped
51
+ * silently. The observer is created only when at least one element
52
+ * resolves; an empty `ids` array (or one with all-missing ids) leaves
53
+ * `activeId` as `null` and creates no observer.
54
+ * - **Cleanup.** The observer is disconnected on unmount and when the
55
+ * `ids` set changes, before a new observer is created. No leaks.
56
+ * - **Re-observation on `ids` change.** The hook detects changes via a
57
+ * string sentinel `ids.join("|")`. Pass `ids` as a stable reference
58
+ * (constant module-scope array, or `useMemo`) to avoid recreating the
59
+ * observer on every render. The hook does not memoise `ids` for you
60
+ * because the consumer typically already knows whether the array is
61
+ * stable.
62
+ * - **SSR safety.** `IntersectionObserver` and `document` are accessed
63
+ * only inside `useEffect`, which never runs on the server. The hook
64
+ * returns `null` during server rendering and the first client render
65
+ * pre-commit. A `typeof window` guard inside the effect protects
66
+ * older runtimes that evaluate `useEffect` outside a browser.
67
+ * - **`useState` initial value.** Always `null`. Returning the first id
68
+ * would highlight a section that the user has not yet seen and
69
+ * contradict the SSR/hydration contract.
70
+ *
71
+ * ### Why this lives in the design system as a hook, not as a component
72
+ *
73
+ * The visual surface (a sticky nav with highlighted active item) is
74
+ * already covered by `Navigation` + `NavLink` with the `active` prop. A
75
+ * `<ScrollSpy>` component would fuse behaviour and visual, restrict
76
+ * layout choice, and couple to a sibling component (`SectionCard`) via
77
+ * an opaque id-string convention. As a hook the consumer keeps the
78
+ * `ids` constant in one place and composes the nav however they want:
79
+ * vertical, horizontal, sticky, in a drawer, etc.
80
+ *
81
+ * @example
82
+ * ```tsx
83
+ * "use client";
84
+ * import { useScrollSpy, Navigation, NavLink } from "@fabio.caffarello/react-design-system";
85
+ *
86
+ * const SECTIONS = ["intro", "votos", "gastos"];
87
+ *
88
+ * function ProfileToc() {
89
+ * const active = useScrollSpy(SECTIONS, { rootMargin: "-56px 0px -50% 0px" });
90
+ * return (
91
+ * <nav className="sticky top-14">
92
+ * <Navigation orientation="vertical">
93
+ * {SECTIONS.map((id) => (
94
+ * <NavLink
95
+ * key={id}
96
+ * href={`#${id}`}
97
+ * active={id === active}
98
+ * aria-current={id === active ? "location" : undefined}
99
+ * >
100
+ * {id}
101
+ * </NavLink>
102
+ * ))}
103
+ * </Navigation>
104
+ * </nav>
105
+ * );
106
+ * }
107
+ * ```
108
+ *
109
+ * @param ids - Element ids to observe, in document order. Stable
110
+ * reference recommended (constant or `useMemo`).
111
+ * @param options - Optional `IntersectionObserver` overrides — see
112
+ * {@link UseScrollSpyOptions}.
113
+ * @returns The id of the topmost visible section, or `null` when
114
+ * nothing is reported visible yet.
115
+ */
116
+ export declare function useScrollSpy(ids: string[], options?: UseScrollSpyOptions): string | null;
@@ -15,3 +15,5 @@ export { ThemeProvider, ConfigProvider, ToastProvider, DialogProvider, useTheme,
15
15
  export { AppProvider, useApp, type AppProviderProps, type AppProviderConfig, } from "./providers/AppProvider";
16
16
  export * from "./primitives";
17
17
  export * from "./components";
18
+ export { useScrollSpy } from "./hooks/useScrollSpy";
19
+ export type { UseScrollSpyOptions } from "./hooks/useScrollSpy";
@@ -53,6 +53,10 @@ export { default as NavbarSeparator } from "./components/SideNavbar/components/N
53
53
  export type { NavbarSeparatorProps } from "./components/SideNavbar/types";
54
54
  export { default as PageHeader } from "./components/PageHeader/PageHeader";
55
55
  export type { PageHeaderProps, PageHeaderVariant, } from "./components/PageHeader/types";
56
+ export { default as Stat } from "./components/Stat/Stat";
57
+ export type { StatProps, StatTone, StatAlign } from "./components/Stat/Stat";
58
+ export { StatGroup } from "./components/Stat/StatGroup";
59
+ export type { StatGroupProps, StatGroupLayout, StatGroupCols, } from "./components/Stat/StatGroup";
56
60
  export { default as TableCell } from "./components/Table/TableCell";
57
61
  export type { TableCellProps } from "./components/Table/TableCell";
58
62
  export { default as Timeline } from "./components/Timeline/Timeline";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fabio.caffarello/react-design-system",
3
3
  "private": false,
4
- "version": "3.5.0",
4
+ "version": "3.7.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",