@flytedan/flytebot-design-system 0.10.0 → 0.11.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.
package/dist/index.d.cts CHANGED
@@ -29,24 +29,41 @@ interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>
29
29
  /** Accessible name, also the title tooltip. Required. */
30
30
  label: string;
31
31
  size?: "sm" | "md" | "lg";
32
- variant?: "ghost" | "outline";
32
+ /** "ghost" and "outline" are quiet: no fill, the glyph carries the tone. "solid" is a
33
+ * filled button in the tone's colour — for the one action on a surface that has to be
34
+ * found at a glance, like the add/remove button on every row of a transfer list. */
35
+ variant?: "ghost" | "outline" | "solid";
36
+ /** What the action does. On "solid" it picks the fill; on "ghost"/"outline" it tints the
37
+ * glyph. "neutral" is the default look of each variant, which for "solid" is the brand
38
+ * fill. Every tone has hover, pressed and focus states and is legible in both themes —
39
+ * the fills are the --btn-solid-* tokens, measured against light and dark grounds. */
40
+ tone?: "neutral" | "ok" | "danger";
33
41
  round?: boolean;
34
42
  disabled?: boolean;
35
43
  className?: string;
36
44
  }
37
- declare function IconButton({ icon, label, size, variant, round, disabled, className, ...rest }: IconButtonProps): React.JSX.Element;
45
+ declare function IconButton({ icon, label, size, variant, tone, round, disabled, className, ...rest }: IconButtonProps): React.JSX.Element;
38
46
 
39
47
  type Placement = "bottom-start" | "bottom-end" | "bottom-center" | "top-start" | "top-end" | "top-center";
40
48
  interface PopoverProps {
41
49
  open?: boolean;
42
50
  anchorRef: React.RefObject<HTMLElement>;
51
+ /** The element whose own clicks open and close this popover, when that is more than
52
+ * the anchor — a form field's label toggles it too, but the surface is positioned
53
+ * against the input box alone. A pointerdown inside it is not an "outside" press, so
54
+ * the toggle it is about to run is not undone by a dismissal a moment earlier.
55
+ * Defaults to `anchorRef`. */
56
+ triggerRef?: React.RefObject<HTMLElement>;
43
57
  onClose?: () => void;
44
58
  placement?: Placement;
45
59
  offset?: number;
46
- matchWidth?: boolean;
60
+ /** `true`: exactly the anchor's width. `"min"`: at least the anchor's width, growing
61
+ * with content past it (a select whose options are wider than its box). */
62
+ matchWidth?: boolean | "min";
47
63
  width?: number;
48
64
  minWidth?: number;
49
65
  maxHeight?: number;
66
+ /** Standard content padding on the body. The header and footer own their own. */
50
67
  padded?: boolean;
51
68
  role?: string;
52
69
  label?: string;
@@ -55,6 +72,13 @@ interface PopoverProps {
55
72
  returnFocus?: boolean;
56
73
  className?: string;
57
74
  style?: React.CSSProperties;
75
+ /** Pinned above the body and never scrolled away: a back link, a title, a segmented
76
+ * toggle, a search field. */
77
+ header?: React.ReactNode;
78
+ /** Pinned below the body and never scrolled away: the Add / Apply / Confirm action and
79
+ * any summary beside it. */
80
+ footer?: React.ReactNode;
81
+ /** The body — the one region of the surface that scrolls. */
58
82
  children?: React.ReactNode;
59
83
  }
60
84
  interface PopoverPosition {
@@ -82,6 +106,14 @@ interface PopoverPosition {
82
106
  * only set when the geometry actually changed (see samePosition), so a stationary anchor
83
107
  * causes no re-renders at all.
84
108
  *
109
+ * The side is chosen from the layer's real size once `layerRef` has rendered (see
110
+ * measureLayer): it flips when what it needs does not fit on the preferred side and the
111
+ * other side has more room. Until then — the first frame, before there is anything to
112
+ * measure — `estHeight` stands in, and only a nearly-exhausted side (under 180px) flips,
113
+ * so a small layer of unknown size is not thrown to the far side on a guess. The real
114
+ * width replaces `estWidth` the same way, which is what keeps an end- or center-aligned
115
+ * layer of content width lined up with its anchor.
116
+ *
85
117
  * The same loop is what notices the anchor being REMOVED — a virtualized row scrolled
86
118
  * out of the mounted band, a menu item deleted — and reports it through `onDetach`, so
87
119
  * an anchored layer can never end up pinned to an element that no longer exists.
@@ -90,12 +122,29 @@ declare function usePopoverPosition(open: boolean, anchorRef: React.RefObject<HT
90
122
  estHeight?: number;
91
123
  estWidth?: number;
92
124
  onDetach?: () => void;
125
+ /** The rendered layer, so its real size replaces the estimates once it exists. */
126
+ layerRef?: React.RefObject<HTMLElement>;
93
127
  }): PopoverPosition | null;
94
128
  /**
95
129
  * An anchored floating surface: menus, pickers, disclosure panels, meters.
96
- * Owns nothing but placement, dismissal and focus return — the content is yours.
97
- */
98
- declare function Popover({ open, anchorRef, onClose, placement, offset, matchWidth, width, minWidth, maxHeight, padded, role, label, closeOnOutside, closeOnEscape, returnFocus, className, style, children, }: PopoverProps): React.ReactElement<any, string | React.JSXElementConstructor<any>> | null;
130
+ * Owns placement, dismissal, focus return — and its own layout, which is the one rule
131
+ * every popover shares: THE SURFACE NEVER SCROLLS.
132
+ *
133
+ * It is always three stacked regions: an optional `header`, the body (`children`), and
134
+ * an optional `footer`. Header and footer are pinned; the body is the only thing that
135
+ * scrolls, and it shrinks to whatever height is left once they are laid out. The surface
136
+ * is capped by the real room between the anchor and the viewport edge (or `maxHeight`, if
137
+ * that is smaller) and flips to the other side when it doesn't fit, so the footer is
138
+ * never the thing that gets cut off.
139
+ *
140
+ * This is structural rather than advice because the failure it prevents is structural:
141
+ * a surface that scrolls as a whole takes its Apply button below the fold with it, and a
142
+ * scrolling list inside a scrolling surface is two nested scrollbars. Anything inside
143
+ * the body that owns a scroll region of its own (a Menu, a Select's options) is laid out
144
+ * to shrink into the body rather than overflow it — see `.fd-pop-body` in
145
+ * components.css — so there is only ever one scrollbar.
146
+ */
147
+ declare function Popover({ open, anchorRef, triggerRef, onClose, placement, offset, matchWidth, width, minWidth, maxHeight, padded, role, label, closeOnOutside, closeOnEscape, returnFocus, className, style, header, footer, children, }: PopoverProps): React.ReactElement<any, string | React.JSXElementConstructor<any>> | null;
99
148
  interface MenuItem {
100
149
  id?: string;
101
150
  label?: React.ReactNode;
@@ -134,6 +183,10 @@ interface MenuProps {
134
183
  * {id,label,icon,description,meta,shortcut,checked,disabled,submenu,onSelect}
135
184
  * {kind:"separator"} · {kind:"section",label} · {kind:"custom",render}
136
185
  * `shortcut` is a HINT — it renders a cap and binds nothing.
186
+ *
187
+ * `header` and `footer` are pinned and only the items scroll: the whole menu is one
188
+ * shrinkable frame, so inside a Popover body a search header stays put while a long list
189
+ * scrolls beneath it, instead of the header scrolling away with the items.
137
190
  */
138
191
  declare function Menu({ items, onSelect, onClose, autoFocus, className, footer, header }: MenuProps): React.JSX.Element;
139
192
  interface MenuButtonProps extends Omit<MenuProps, "onClose" | "autoFocus"> {
@@ -495,13 +548,33 @@ interface ToastProps {
495
548
  }
496
549
  declare function Toast({ title, children, tone, onUndo, onDismiss, className, ...rest }: ToastProps): React.JSX.Element;
497
550
 
498
- /** A short clarification on hover or focus. Never the only place information lives. */
551
+ /** A short clarification on hover, focus or tap. Never the only place information lives. */
499
552
  interface TooltipProps {
500
553
  label: React.ReactNode;
501
554
  placement?: "top" | "bottom";
502
555
  children?: React.ReactNode;
503
556
  className?: string;
504
557
  }
558
+ /**
559
+ * Tooltip — the kit's one tooltip, for any trigger.
560
+ *
561
+ * Three ways in, because a tooltip reachable only by a mouse hides its text from
562
+ * everyone else: hover (mouse only), keyboard focus (`:focus-visible`, so a mouse click
563
+ * that happens to focus a button does not pin a tip open) and tap (touch or pen — there is
564
+ * no hover on a touchscreen, so a tap toggles it). Escape and a press anywhere else
565
+ * dismiss it; the pointer can move from the trigger onto the tip without it closing.
566
+ *
567
+ * The text is the trigger's accessible DESCRIPTION, not just a visual: a visually hidden
568
+ * copy is always in the document and the trigger points at it with aria-describedby, so a
569
+ * screen reader announces it on focus whether or not the bubble is showing at that
570
+ * instant. The trigger is the single child element when there is one (it receives the
571
+ * attribute); otherwise the wrapper does.
572
+ *
573
+ * The bubble is portaled to document.body and positioned with the same anchored-layer
574
+ * geometry as Popover (flip, clamp, follow a moving anchor), because a tooltip drawn
575
+ * inside its trigger's box is clipped by the first `overflow: hidden` ancestor — which in
576
+ * a fixed-width table cell or a list row is all of them.
577
+ */
505
578
  declare function Tooltip({ label, placement, children, className }: TooltipProps): React.JSX.Element;
506
579
 
507
580
  interface ClampProps {
@@ -1230,8 +1303,11 @@ interface EntityRowAction {
1230
1303
  /** Accessible name for the action button. */
1231
1304
  label: string;
1232
1305
  onClick: () => void;
1233
- /** Tints the icon — "add" reads as constructive (ok-text), "remove" as destructive
1234
- * (danger-text). No effect on layout, only color, so a caller can still pass any icon. */
1306
+ /** Picks the fill — "add" is the constructive (ok) fill, "remove" the destructive
1307
+ * (danger) fill, and no tone the brand fill. The button is always a filled
1308
+ * IconButton (variant "solid"): it is the one action on the row, repeated down a long
1309
+ * list, and has to be findable at a glance on a default card and on a danger-toned one
1310
+ * alike. No effect on layout, so a caller can still pass any icon. */
1235
1311
  tone?: "add" | "remove";
1236
1312
  }
1237
1313
  /** Width in px a metric cell takes when it doesn't ask for its own. Wide enough for a
@@ -1243,9 +1319,17 @@ interface EntityRowMetric {
1243
1319
  * columns legitimately share a label. */
1244
1320
  id?: string;
1245
1321
  /** What the number means — "students", "ad units". Never rendered as running text:
1246
- * it is the metric's accessible name (and its tooltip), so the row stays scannable
1247
- * as numbers while a screen reader still hears "1,240 students". */
1322
+ * it is the metric's accessible name, so the row stays scannable as numbers while a
1323
+ * screen reader still hears "1,240 students". Without a `tooltip` it is also the
1324
+ * cell's native hover title. */
1248
1325
  label: string;
1326
+ /** The long-form explanation of this figure — what it counts, why it is zero, what an
1327
+ * "unknown" means. Shown in the kit's Tooltip on hover, keyboard focus and tap, and
1328
+ * exposed as the cell's accessible description (`label` stays its short name). A cell
1329
+ * with a tooltip becomes a focusable button so the explanation is reachable without a
1330
+ * mouse, and drops the native `title` so two tooltips never show at once. A button is
1331
+ * also what keeps a tap on it from arming TransferList's row drag. */
1332
+ tooltip?: React.ReactNode;
1249
1333
  value: React.ReactNode;
1250
1334
  /** Phosphor icon name shown before the value, e.g. "student". */
1251
1335
  icon?: string;
@@ -1516,6 +1600,11 @@ interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "
1516
1600
  loading?: boolean;
1517
1601
  /** Applied to the outer .fd-field element — this is where width belongs. */
1518
1602
  style?: React.CSSProperties;
1603
+ /** Applied to the outer .fd-field element, like `style` and for the same reason: that
1604
+ * element is the field's box in whatever layout it sits in (a flex toolbar, a grid
1605
+ * cell). The input box inside it is a child of a vertical flex column, so a sizing
1606
+ * class landing THERE turns a row's `flex-basis` into a height. */
1607
+ className?: string;
1519
1608
  /** Applied to the inner input element. Rarely needed. */
1520
1609
  inputStyle?: React.CSSProperties;
1521
1610
  }
@@ -1532,6 +1621,7 @@ interface SearchFieldProps {
1532
1621
  onClear?: () => void;
1533
1622
  /** Applied to the outer .fd-field element — this is where width belongs. */
1534
1623
  style?: React.CSSProperties;
1624
+ /** Applied to the outer .fd-field element, like `style`. */
1535
1625
  className?: string;
1536
1626
  /** Visible field label, same as Input. Most search fields skip this and rely on placeholder. */
1537
1627
  label?: string;
@@ -1563,8 +1653,10 @@ interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement
1563
1653
  error?: string;
1564
1654
  required?: boolean;
1565
1655
  rows?: number;
1566
- /** Applied to the field wrapper. */
1656
+ /** Applied to the outer .fd-field element — this is where width belongs. */
1567
1657
  style?: React.CSSProperties;
1658
+ /** Applied to the outer .fd-field element, like `style` — the same rule as Input. */
1659
+ className?: string;
1568
1660
  }
1569
1661
  declare function Textarea({ label, help, error, required, rows, disabled, id, className, style, ...rest }: TextareaProps): React.JSX.Element;
1570
1662
 
package/dist/index.d.ts CHANGED
@@ -29,24 +29,41 @@ interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>
29
29
  /** Accessible name, also the title tooltip. Required. */
30
30
  label: string;
31
31
  size?: "sm" | "md" | "lg";
32
- variant?: "ghost" | "outline";
32
+ /** "ghost" and "outline" are quiet: no fill, the glyph carries the tone. "solid" is a
33
+ * filled button in the tone's colour — for the one action on a surface that has to be
34
+ * found at a glance, like the add/remove button on every row of a transfer list. */
35
+ variant?: "ghost" | "outline" | "solid";
36
+ /** What the action does. On "solid" it picks the fill; on "ghost"/"outline" it tints the
37
+ * glyph. "neutral" is the default look of each variant, which for "solid" is the brand
38
+ * fill. Every tone has hover, pressed and focus states and is legible in both themes —
39
+ * the fills are the --btn-solid-* tokens, measured against light and dark grounds. */
40
+ tone?: "neutral" | "ok" | "danger";
33
41
  round?: boolean;
34
42
  disabled?: boolean;
35
43
  className?: string;
36
44
  }
37
- declare function IconButton({ icon, label, size, variant, round, disabled, className, ...rest }: IconButtonProps): React.JSX.Element;
45
+ declare function IconButton({ icon, label, size, variant, tone, round, disabled, className, ...rest }: IconButtonProps): React.JSX.Element;
38
46
 
39
47
  type Placement = "bottom-start" | "bottom-end" | "bottom-center" | "top-start" | "top-end" | "top-center";
40
48
  interface PopoverProps {
41
49
  open?: boolean;
42
50
  anchorRef: React.RefObject<HTMLElement>;
51
+ /** The element whose own clicks open and close this popover, when that is more than
52
+ * the anchor — a form field's label toggles it too, but the surface is positioned
53
+ * against the input box alone. A pointerdown inside it is not an "outside" press, so
54
+ * the toggle it is about to run is not undone by a dismissal a moment earlier.
55
+ * Defaults to `anchorRef`. */
56
+ triggerRef?: React.RefObject<HTMLElement>;
43
57
  onClose?: () => void;
44
58
  placement?: Placement;
45
59
  offset?: number;
46
- matchWidth?: boolean;
60
+ /** `true`: exactly the anchor's width. `"min"`: at least the anchor's width, growing
61
+ * with content past it (a select whose options are wider than its box). */
62
+ matchWidth?: boolean | "min";
47
63
  width?: number;
48
64
  minWidth?: number;
49
65
  maxHeight?: number;
66
+ /** Standard content padding on the body. The header and footer own their own. */
50
67
  padded?: boolean;
51
68
  role?: string;
52
69
  label?: string;
@@ -55,6 +72,13 @@ interface PopoverProps {
55
72
  returnFocus?: boolean;
56
73
  className?: string;
57
74
  style?: React.CSSProperties;
75
+ /** Pinned above the body and never scrolled away: a back link, a title, a segmented
76
+ * toggle, a search field. */
77
+ header?: React.ReactNode;
78
+ /** Pinned below the body and never scrolled away: the Add / Apply / Confirm action and
79
+ * any summary beside it. */
80
+ footer?: React.ReactNode;
81
+ /** The body — the one region of the surface that scrolls. */
58
82
  children?: React.ReactNode;
59
83
  }
60
84
  interface PopoverPosition {
@@ -82,6 +106,14 @@ interface PopoverPosition {
82
106
  * only set when the geometry actually changed (see samePosition), so a stationary anchor
83
107
  * causes no re-renders at all.
84
108
  *
109
+ * The side is chosen from the layer's real size once `layerRef` has rendered (see
110
+ * measureLayer): it flips when what it needs does not fit on the preferred side and the
111
+ * other side has more room. Until then — the first frame, before there is anything to
112
+ * measure — `estHeight` stands in, and only a nearly-exhausted side (under 180px) flips,
113
+ * so a small layer of unknown size is not thrown to the far side on a guess. The real
114
+ * width replaces `estWidth` the same way, which is what keeps an end- or center-aligned
115
+ * layer of content width lined up with its anchor.
116
+ *
85
117
  * The same loop is what notices the anchor being REMOVED — a virtualized row scrolled
86
118
  * out of the mounted band, a menu item deleted — and reports it through `onDetach`, so
87
119
  * an anchored layer can never end up pinned to an element that no longer exists.
@@ -90,12 +122,29 @@ declare function usePopoverPosition(open: boolean, anchorRef: React.RefObject<HT
90
122
  estHeight?: number;
91
123
  estWidth?: number;
92
124
  onDetach?: () => void;
125
+ /** The rendered layer, so its real size replaces the estimates once it exists. */
126
+ layerRef?: React.RefObject<HTMLElement>;
93
127
  }): PopoverPosition | null;
94
128
  /**
95
129
  * An anchored floating surface: menus, pickers, disclosure panels, meters.
96
- * Owns nothing but placement, dismissal and focus return — the content is yours.
97
- */
98
- declare function Popover({ open, anchorRef, onClose, placement, offset, matchWidth, width, minWidth, maxHeight, padded, role, label, closeOnOutside, closeOnEscape, returnFocus, className, style, children, }: PopoverProps): React.ReactElement<any, string | React.JSXElementConstructor<any>> | null;
130
+ * Owns placement, dismissal, focus return — and its own layout, which is the one rule
131
+ * every popover shares: THE SURFACE NEVER SCROLLS.
132
+ *
133
+ * It is always three stacked regions: an optional `header`, the body (`children`), and
134
+ * an optional `footer`. Header and footer are pinned; the body is the only thing that
135
+ * scrolls, and it shrinks to whatever height is left once they are laid out. The surface
136
+ * is capped by the real room between the anchor and the viewport edge (or `maxHeight`, if
137
+ * that is smaller) and flips to the other side when it doesn't fit, so the footer is
138
+ * never the thing that gets cut off.
139
+ *
140
+ * This is structural rather than advice because the failure it prevents is structural:
141
+ * a surface that scrolls as a whole takes its Apply button below the fold with it, and a
142
+ * scrolling list inside a scrolling surface is two nested scrollbars. Anything inside
143
+ * the body that owns a scroll region of its own (a Menu, a Select's options) is laid out
144
+ * to shrink into the body rather than overflow it — see `.fd-pop-body` in
145
+ * components.css — so there is only ever one scrollbar.
146
+ */
147
+ declare function Popover({ open, anchorRef, triggerRef, onClose, placement, offset, matchWidth, width, minWidth, maxHeight, padded, role, label, closeOnOutside, closeOnEscape, returnFocus, className, style, header, footer, children, }: PopoverProps): React.ReactElement<any, string | React.JSXElementConstructor<any>> | null;
99
148
  interface MenuItem {
100
149
  id?: string;
101
150
  label?: React.ReactNode;
@@ -134,6 +183,10 @@ interface MenuProps {
134
183
  * {id,label,icon,description,meta,shortcut,checked,disabled,submenu,onSelect}
135
184
  * {kind:"separator"} · {kind:"section",label} · {kind:"custom",render}
136
185
  * `shortcut` is a HINT — it renders a cap and binds nothing.
186
+ *
187
+ * `header` and `footer` are pinned and only the items scroll: the whole menu is one
188
+ * shrinkable frame, so inside a Popover body a search header stays put while a long list
189
+ * scrolls beneath it, instead of the header scrolling away with the items.
137
190
  */
138
191
  declare function Menu({ items, onSelect, onClose, autoFocus, className, footer, header }: MenuProps): React.JSX.Element;
139
192
  interface MenuButtonProps extends Omit<MenuProps, "onClose" | "autoFocus"> {
@@ -495,13 +548,33 @@ interface ToastProps {
495
548
  }
496
549
  declare function Toast({ title, children, tone, onUndo, onDismiss, className, ...rest }: ToastProps): React.JSX.Element;
497
550
 
498
- /** A short clarification on hover or focus. Never the only place information lives. */
551
+ /** A short clarification on hover, focus or tap. Never the only place information lives. */
499
552
  interface TooltipProps {
500
553
  label: React.ReactNode;
501
554
  placement?: "top" | "bottom";
502
555
  children?: React.ReactNode;
503
556
  className?: string;
504
557
  }
558
+ /**
559
+ * Tooltip — the kit's one tooltip, for any trigger.
560
+ *
561
+ * Three ways in, because a tooltip reachable only by a mouse hides its text from
562
+ * everyone else: hover (mouse only), keyboard focus (`:focus-visible`, so a mouse click
563
+ * that happens to focus a button does not pin a tip open) and tap (touch or pen — there is
564
+ * no hover on a touchscreen, so a tap toggles it). Escape and a press anywhere else
565
+ * dismiss it; the pointer can move from the trigger onto the tip without it closing.
566
+ *
567
+ * The text is the trigger's accessible DESCRIPTION, not just a visual: a visually hidden
568
+ * copy is always in the document and the trigger points at it with aria-describedby, so a
569
+ * screen reader announces it on focus whether or not the bubble is showing at that
570
+ * instant. The trigger is the single child element when there is one (it receives the
571
+ * attribute); otherwise the wrapper does.
572
+ *
573
+ * The bubble is portaled to document.body and positioned with the same anchored-layer
574
+ * geometry as Popover (flip, clamp, follow a moving anchor), because a tooltip drawn
575
+ * inside its trigger's box is clipped by the first `overflow: hidden` ancestor — which in
576
+ * a fixed-width table cell or a list row is all of them.
577
+ */
505
578
  declare function Tooltip({ label, placement, children, className }: TooltipProps): React.JSX.Element;
506
579
 
507
580
  interface ClampProps {
@@ -1230,8 +1303,11 @@ interface EntityRowAction {
1230
1303
  /** Accessible name for the action button. */
1231
1304
  label: string;
1232
1305
  onClick: () => void;
1233
- /** Tints the icon — "add" reads as constructive (ok-text), "remove" as destructive
1234
- * (danger-text). No effect on layout, only color, so a caller can still pass any icon. */
1306
+ /** Picks the fill — "add" is the constructive (ok) fill, "remove" the destructive
1307
+ * (danger) fill, and no tone the brand fill. The button is always a filled
1308
+ * IconButton (variant "solid"): it is the one action on the row, repeated down a long
1309
+ * list, and has to be findable at a glance on a default card and on a danger-toned one
1310
+ * alike. No effect on layout, so a caller can still pass any icon. */
1235
1311
  tone?: "add" | "remove";
1236
1312
  }
1237
1313
  /** Width in px a metric cell takes when it doesn't ask for its own. Wide enough for a
@@ -1243,9 +1319,17 @@ interface EntityRowMetric {
1243
1319
  * columns legitimately share a label. */
1244
1320
  id?: string;
1245
1321
  /** What the number means — "students", "ad units". Never rendered as running text:
1246
- * it is the metric's accessible name (and its tooltip), so the row stays scannable
1247
- * as numbers while a screen reader still hears "1,240 students". */
1322
+ * it is the metric's accessible name, so the row stays scannable as numbers while a
1323
+ * screen reader still hears "1,240 students". Without a `tooltip` it is also the
1324
+ * cell's native hover title. */
1248
1325
  label: string;
1326
+ /** The long-form explanation of this figure — what it counts, why it is zero, what an
1327
+ * "unknown" means. Shown in the kit's Tooltip on hover, keyboard focus and tap, and
1328
+ * exposed as the cell's accessible description (`label` stays its short name). A cell
1329
+ * with a tooltip becomes a focusable button so the explanation is reachable without a
1330
+ * mouse, and drops the native `title` so two tooltips never show at once. A button is
1331
+ * also what keeps a tap on it from arming TransferList's row drag. */
1332
+ tooltip?: React.ReactNode;
1249
1333
  value: React.ReactNode;
1250
1334
  /** Phosphor icon name shown before the value, e.g. "student". */
1251
1335
  icon?: string;
@@ -1516,6 +1600,11 @@ interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "
1516
1600
  loading?: boolean;
1517
1601
  /** Applied to the outer .fd-field element — this is where width belongs. */
1518
1602
  style?: React.CSSProperties;
1603
+ /** Applied to the outer .fd-field element, like `style` and for the same reason: that
1604
+ * element is the field's box in whatever layout it sits in (a flex toolbar, a grid
1605
+ * cell). The input box inside it is a child of a vertical flex column, so a sizing
1606
+ * class landing THERE turns a row's `flex-basis` into a height. */
1607
+ className?: string;
1519
1608
  /** Applied to the inner input element. Rarely needed. */
1520
1609
  inputStyle?: React.CSSProperties;
1521
1610
  }
@@ -1532,6 +1621,7 @@ interface SearchFieldProps {
1532
1621
  onClear?: () => void;
1533
1622
  /** Applied to the outer .fd-field element — this is where width belongs. */
1534
1623
  style?: React.CSSProperties;
1624
+ /** Applied to the outer .fd-field element, like `style`. */
1535
1625
  className?: string;
1536
1626
  /** Visible field label, same as Input. Most search fields skip this and rely on placeholder. */
1537
1627
  label?: string;
@@ -1563,8 +1653,10 @@ interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement
1563
1653
  error?: string;
1564
1654
  required?: boolean;
1565
1655
  rows?: number;
1566
- /** Applied to the field wrapper. */
1656
+ /** Applied to the outer .fd-field element — this is where width belongs. */
1567
1657
  style?: React.CSSProperties;
1658
+ /** Applied to the outer .fd-field element, like `style` — the same rule as Input. */
1659
+ className?: string;
1568
1660
  }
1569
1661
  declare function Textarea({ label, help, error, required, rows, disabled, id, className, style, ...rest }: TextareaProps): React.JSX.Element;
1570
1662