@flood-md/react 0.2.11 → 0.2.12

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.ts CHANGED
@@ -82,6 +82,8 @@ export interface CollectionToolbarProps extends Omit<FieldsetProps, "children" |
82
82
  summaryPrefix?: ReactNode;
83
83
  /** At most two task-important quick filters; move additional properties into filterContent. */
84
84
  quickFilters?: ReactNode;
85
+ /** Additional commands beside Filters in the same tool row. */
86
+ actions?: ReactNode;
85
87
  filterContent?: ReactNode;
86
88
  filtersLabel?: string;
87
89
  filterTitle?: string;
@@ -277,10 +279,21 @@ export declare const ImagePreview: import("react").ForwardRefExoticComponent<Ima
277
279
  export interface DataTableColumn {
278
280
  id: string;
279
281
  label: string;
282
+ /** Short unit shown alongside the label, such as USD or kg. */
283
+ unit?: string;
280
284
  sortable?: boolean;
281
285
  numeric?: boolean;
282
286
  align?: "start" | "end";
283
287
  width?: CSSProperties["width"];
288
+ /** Keep this column visible when the host or Columns menu hides other columns. */
289
+ hideable?: boolean;
290
+ /** Opt into pointer and keyboard width adjustments for this column. */
291
+ resizable?: boolean;
292
+ /** Interactive width bounds in CSS pixels. */
293
+ minWidth?: number;
294
+ maxWidth?: number;
295
+ /** Keep the column at a logical edge when the table scrolls horizontally. */
296
+ pinned?: "start" | "end";
284
297
  }
285
298
  export interface DataTableRow {
286
299
  id: string;
@@ -288,6 +301,8 @@ export interface DataTableRow {
288
301
  cells: Readonly<Record<string, ReactNode>>;
289
302
  sortValues?: Readonly<Record<string, string | number | null>>;
290
303
  selectionDisabled?: boolean;
304
+ /** Related detail rendered in a separate full-width row on disclosure. */
305
+ expandedContent?: ReactNode;
291
306
  /** One named record-opening action. Other controls in the row remain independent. */
292
307
  primaryAction?: {
293
308
  column: string;
@@ -312,6 +327,13 @@ export interface DataTableLabels {
312
327
  results?: (from: number, to: number, total: number) => string;
313
328
  selection?: (selected: number, total: number) => string;
314
329
  sort?: (label: string) => string;
330
+ columns?: string;
331
+ resetColumns?: string;
332
+ sortedColumn?: (label: string) => string;
333
+ resizeColumn?: (label: string) => string;
334
+ expandRow?: (label: string) => string;
335
+ collapseRow?: (label: string) => string;
336
+ rowDetails?: string;
315
337
  pagination?: string;
316
338
  previousPage?: string;
317
339
  nextPage?: string;
@@ -319,17 +341,33 @@ export interface DataTableLabels {
319
341
  }
320
342
  export interface DataTableProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
321
343
  /** Host-owned heading, search and filters aligned with the outer table cells. */
322
- toolbar?: ReactNode;
344
+ toolbar?: ReactNode | ((controls: {
345
+ columns: ReactNode;
346
+ }) => ReactNode);
323
347
  inset?: "standard" | "roomy";
324
348
  /** Ready with no rows only. Compose EmptyState/actions here; loading and error keep their owners. */
325
349
  emptyContent?: ReactNode;
326
350
  caption: string;
327
351
  hideCaption?: boolean;
328
352
  columns: readonly DataTableColumn[];
353
+ showColumnVisibility?: boolean;
354
+ hiddenColumnIds?: readonly string[];
355
+ /** Initial hidden columns and the layout restored by Reset columns. */
356
+ defaultHiddenColumnIds?: readonly string[];
357
+ onHiddenColumnIdsChange?: (ids: string[]) => void;
358
+ columnWidths?: Readonly<Record<string, number>>;
359
+ defaultColumnWidths?: Readonly<Record<string, number>>;
360
+ onColumnWidthsChange?: (widths: Record<string, number>) => void;
329
361
  rows: readonly DataTableRow[];
362
+ expandedIds?: readonly string[];
363
+ defaultExpandedIds?: readonly string[];
364
+ onExpandedChange?: (ids: string[]) => void;
330
365
  density?: "compact" | "comfortable";
331
366
  striped?: boolean;
332
367
  minWidth?: CSSProperties["minWidth"];
368
+ /** Maximum height of the scroll viewport, including the header. */
369
+ maxViewportHeight?: CSSProperties["maxBlockSize"];
370
+ stickyHeader?: boolean;
333
371
  sort?: DataTableSort | null;
334
372
  defaultSort?: DataTableSort | null;
335
373
  onSortChange?: (sort: DataTableSort | null) => void;
@@ -344,6 +382,8 @@ export interface DataTableProps extends Omit<HTMLAttributes<HTMLDivElement>, "ch
344
382
  onPageChange?: (page: number) => void;
345
383
  /** Hide the result range while preserving selection and pagination controls. */
346
384
  showResults?: boolean;
385
+ /** Hide the visible selection count while retaining its accessible status. */
386
+ showSelection?: boolean;
347
387
  /** Server mode renders rows as the supplied page, without local sorting/slicing. */
348
388
  paginationMode?: "client" | "server";
349
389
  totalRows?: number;
@@ -355,6 +395,20 @@ export interface DataTableProps extends Omit<HTMLAttributes<HTMLDivElement>, "ch
355
395
  }
356
396
  /** A client-side table composed from native table, checkbox and pagination. @since 0.2.8 */
357
397
  export declare const DataTable: import("react").ForwardRefExoticComponent<DataTableProps & import("react").RefAttributes<HTMLDivElement>>;
398
+ export interface SparklineProps extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "role" | "aria-label" | "aria-labelledby"> {
399
+ /** Equally spaced observations in order. Missing or nonfinite values break the line. */
400
+ values: readonly (number | null)[];
401
+ /** Localized text conveying the trend or the absence of data. */
402
+ label: string;
403
+ showLabel?: boolean;
404
+ /** Shared lower and upper bounds for comparisons. Values outside the bounds are clipped. */
405
+ domain?: readonly [number, number];
406
+ size?: "sm" | "md";
407
+ tone?: "neutral" | "accent" | "success" | "warning" | "error";
408
+ variant?: "line" | "area";
409
+ }
410
+ /** @since 0.2.11 */
411
+ export declare const Sparkline: import("react").ForwardRefExoticComponent<SparklineProps & import("react").RefAttributes<HTMLDivElement>>;
358
412
  export interface DataListProps extends Omit<HTMLAttributes<HTMLUListElement>, "role"> {
359
413
  density?: "compact" | "comfortable";
360
414
  dividers?: boolean;
@@ -898,6 +952,12 @@ export interface EditableProps extends Omit<HTMLAttributes<HTMLDivElement>, "def
898
952
  onEditingChange?: (editing: boolean) => void;
899
953
  onCommit?: (value: string) => void;
900
954
  onCancel?: () => void;
955
+ /** Return a message to keep the draft open and prevent a commit. */
956
+ validate?: (value: string) => string | undefined;
957
+ inputMode?: InputHTMLAttributes<HTMLInputElement>["inputMode"];
958
+ previewVariant?: "subtle" | "ghost";
959
+ /** Render the editor over its trigger at this preferred width. */
960
+ editorOverlayWidth?: CSSProperties["inlineSize"];
901
961
  blurAction?: "commit" | "cancel" | "none";
902
962
  readOnly?: boolean;
903
963
  disabled?: boolean;
@@ -970,6 +1030,9 @@ export interface TableProps extends Omit<TableHTMLAttributes<HTMLTableElement>,
970
1030
  density?: DataDensity;
971
1031
  striped?: boolean;
972
1032
  scrollLabel?: string;
1033
+ /** Maximum height of the scroll viewport, including the header and caption. */
1034
+ maxViewportHeight?: CSSProperties["maxBlockSize"];
1035
+ stickyHeader?: boolean;
973
1036
  /** Shared cell alignment. Middle keeps mixed record/action rows coherent; top is an explicit prose-table choice. */
974
1037
  cellVerticalAlign?: "top" | "middle" | "bottom";
975
1038
  }
@@ -1206,6 +1269,9 @@ export interface DrawerProps extends DialogProps {
1206
1269
  footer?: ReactNode;
1207
1270
  }
1208
1271
  export declare const Drawer: import("react").ForwardRefExoticComponent<DrawerProps & import("react").RefAttributes<HTMLDialogElement>>;
1272
+ export type SheetProps = Omit<DrawerProps, "side">;
1273
+ /** A modal bottom sheet for a short task or a list of choices. @since 0.2.12 */
1274
+ export declare const Sheet: import("react").ForwardRefExoticComponent<SheetProps & import("react").RefAttributes<HTMLDialogElement>>;
1209
1275
  export interface AlertDialogProps extends Omit<DialogProps, "children" | "role" | "description" | "closeLabel" | "closeOnOutsideClick" | "initialFocusRef"> {
1210
1276
  description: string;
1211
1277
  confirmLabel?: string;
@@ -1504,7 +1570,7 @@ export interface TextProps extends HTMLAttributes<HTMLElement> {
1504
1570
  export declare const Text: import("react").ForwardRefExoticComponent<TextProps & import("react").RefAttributes<HTMLElement>>;
1505
1571
  export interface HeadingProps extends HTMLAttributes<HTMLHeadingElement> {
1506
1572
  level?: 1 | 2 | 3 | 4 | 5 | 6;
1507
- size?: "display" | "large" | "page" | "section";
1573
+ size?: "display" | "large" | "page" | "section" | "group";
1508
1574
  }
1509
1575
  export declare const Heading: import("react").ForwardRefExoticComponent<HeadingProps & import("react").RefAttributes<HTMLHeadingElement>>;
1510
1576
  export type Space = "0" | "1" | "2" | "3" | "4" | "6" | "8" | "12";
@@ -1748,7 +1814,7 @@ export interface SectionProps extends Omit<HTMLAttributes<HTMLElement>, "title">
1748
1814
  description?: ReactNode;
1749
1815
  actions?: ReactNode;
1750
1816
  headingLevel?: 2 | 3 | 4 | 5 | 6;
1751
- headingSize?: "display" | "large" | "page" | "section";
1817
+ headingSize?: HeadingProps["size"];
1752
1818
  }
1753
1819
  export declare const Section: import("react").ForwardRefExoticComponent<SectionProps & import("react").RefAttributes<HTMLElement>>;
1754
1820
  export interface PageHeaderProps extends Omit<HTMLAttributes<HTMLElement>, "title" | "children"> {
@@ -1792,6 +1858,8 @@ export interface AppShellProps extends HTMLAttributes<HTMLDivElement> {
1792
1858
  mainRef?: Ref<HTMLElement>;
1793
1859
  scrollLabel?: string;
1794
1860
  scrollOrientation?: ScrollAreaProps["orientation"];
1861
+ /** Keep the content width stable with "none" when the layout already clears the overlay rails. */
1862
+ scrollContentInset?: ScrollAreaProps["contentInset"];
1795
1863
  skipLabel?: string;
1796
1864
  }
1797
1865
  /** Composes application navigation and a main landmark with optional bounded scrolling. @since 0.2.11 */
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ import { ClockTimePicker } from "./time-picker.js";
51
51
  import { CivilDateTimeRangePicker } from "./date-time-range-picker.js";
52
52
  import { FileRecord, UploadRecord, ModalImagePreview, FileMetadata } from "./file-records.js";
53
53
  import { ManagedDataTable } from "./data-table.js";
54
+ import { TrendPlot } from "./sparkline.js";
54
55
  import { CheckableCard, ExclusiveCard } from "./collection-cards.js";
55
56
  import { ColorPreview, ColorEditor } from "./color-controls.js";
56
57
  import { HoverPreview } from "./hover-card.js";
@@ -96,6 +97,10 @@ export const UploadList = /* @__PURE__ */ forwardRef(function UploadList(props,
96
97
  export const ImagePreview = /* @__PURE__ */ forwardRef(function ImagePreview(props, ref) { return _jsx(ModalImagePreview, { ...props, ref: ref }); });
97
98
  /** A client-side table composed from native table, checkbox and pagination. @since 0.2.8 */
98
99
  export const DataTable = /* @__PURE__ */ forwardRef(function DataTable(props, ref) { return _jsx(ManagedDataTable, { ...props, ref: ref }); });
100
+ /** @since 0.2.11 */
101
+ export const Sparkline = /* @__PURE__ */ forwardRef(function Sparkline({ showLabel = false, size = "md", tone = "accent", variant = "area", ...props }, ref) {
102
+ return _jsx(TrendPlot, { ...props, showLabel: showLabel, size: size, tone: tone, variant: variant, ref: ref });
103
+ });
99
104
  /** A native list; links and actions stay independent. @since 0.2.8 */
100
105
  export const DataList = /* @__PURE__ */ forwardRef(function DataList({ density = "comfortable", dividers = true, className, ...props }, ref) {
101
106
  return _jsx("ul", { ...props, ref: ref, role: "list", "data-density": density, "data-dividers": dividers, className: classes("flood-data-list", className) });
@@ -184,8 +189,8 @@ export const DescriptionList = /* @__PURE__ */ forwardRef(function DescriptionLi
184
189
  return _jsx(DescribedData, { ...props, ref: ref, items: items, layout: layout, density: density, dividers: dividers });
185
190
  });
186
191
  /** @since 0.2.4 */
187
- export const Table = /* @__PURE__ */ forwardRef(function Table({ caption, children, hideCaption = false, density = "comfortable", striped = false, scrollLabel, cellVerticalAlign = "middle", ...props }, ref) {
188
- return _jsx(TabularData, { ...props, ref: ref, caption: caption, hideCaption: hideCaption, density: density, striped: striped, scrollLabel: scrollLabel, cellVerticalAlign: cellVerticalAlign, children: children });
192
+ export const Table = /* @__PURE__ */ forwardRef(function Table({ caption, children, hideCaption = false, density = "comfortable", striped = false, scrollLabel, cellVerticalAlign = "middle", stickyHeader = false, ...props }, ref) {
193
+ return _jsx(TabularData, { ...props, ref: ref, caption: caption, hideCaption: hideCaption, density: density, striped: striped, scrollLabel: scrollLabel, cellVerticalAlign: cellVerticalAlign, stickyHeader: stickyHeader, children: children });
189
194
  });
190
195
  /** @since 0.2.4 */
191
196
  export const Badge = /* @__PURE__ */ forwardRef(function Badge({ size = "md", tone = "neutral", variant = "subtle", className, ...props }, ref) {
@@ -242,6 +247,10 @@ export const CommandPalette = /* @__PURE__ */ forwardRef(function CommandPalette
242
247
  export const Drawer = /* @__PURE__ */ forwardRef(function Drawer({ side = "end", size = "md", defaultOpen = false, closeLabel = "Close", closeOnOutsideClick = true, className, footer, ...props }, ref) {
243
248
  return _jsx(ModalOverlay, { ...props, ref: ref, defaultOpen: defaultOpen, closeLabel: closeLabel, closeOnOutsideClick: closeOnOutsideClick, className: classes("flood-drawer", className), "data-side": side, "data-size": size, scrollBody: true, footer: footer });
244
249
  });
250
+ /** A modal bottom sheet for a short task or a list of choices. @since 0.2.12 */
251
+ export const Sheet = /* @__PURE__ */ forwardRef(function Sheet({ className, size = "md", ...props }, ref) {
252
+ return _jsx(ModalOverlay, { ...props, ref: ref, "data-side": "bottom", "data-size": size, className: classes("flood-drawer", "flood-sheet", className), scrollBody: true, bodyContentInset: "none" });
253
+ });
245
254
  export const AlertDialog = /* @__PURE__ */ forwardRef(function AlertDialog({ defaultOpen = false, confirmLabel = "Continue", cancelLabel = "Cancel", confirmDisabled = false, ...props }, ref) {
246
255
  return _jsx(ConfirmationDialog, { ...props, ref: ref, defaultOpen: defaultOpen, confirmLabel: confirmLabel, cancelLabel: cancelLabel, confirmDisabled: confirmDisabled });
247
256
  });
@@ -18,7 +18,7 @@ export declare function placeOverlay(anchor: Rect, size: {
18
18
  top: number;
19
19
  width: number;
20
20
  height: number;
21
- }, side: Side, align: Align, gap: number, rtl?: boolean): {
21
+ }, side: Side, align: Align, gap: number, rtl?: boolean, overlap?: boolean): {
22
22
  left: number;
23
23
  top: number;
24
24
  side: Side;
@@ -29,13 +29,14 @@ type Layer = {
29
29
  anchor: () => HTMLElement | null;
30
30
  close: () => void;
31
31
  outside?: () => void;
32
+ closeOnFocusOutside?: boolean;
32
33
  modal?: boolean;
33
34
  tooltip?: boolean;
34
35
  };
35
36
  export declare function registerLayer(document: Document, layer: Layer): () => void;
36
37
  export declare function lockScroll(document: Document): () => void;
37
- export declare function positionLayer(anchor: HTMLElement, panel: HTMLElement, side: Side, align: Align): void;
38
+ export declare function positionLayer(anchor: HTMLElement, panel: HTMLElement, side: Side, align: Align, overlap?: boolean): void;
38
39
  export declare function returnFocus(panel: HTMLElement, anchor: HTMLElement | null): void;
39
40
  /** Retain pixels for a CSS exit after the native layer has released focus. */
40
- export declare function useLayerExitContent(panel: RefObject<HTMLElement | null>, open: boolean, enabled?: boolean): boolean;
41
+ export declare function useLayerExitContent(panel: RefObject<HTMLElement | null>, open: boolean, enabled?: boolean, transitionProperty?: string): boolean;
41
42
  export {};
@@ -1,16 +1,18 @@
1
1
  import { useEffect, useLayoutEffect, useState } from "react";
2
2
  /** Pure collision handling in layout-viewport coordinates. */
3
- export function placeOverlay(anchor, size, viewport, side, align, gap, rtl = false) {
3
+ export function placeOverlay(anchor, size, viewport, side, align, gap, rtl = false, overlap = false) {
4
4
  const edge = { left: viewport.left + gap, right: viewport.left + viewport.width - gap, top: viewport.top + gap, bottom: viewport.top + viewport.height - gap };
5
5
  const space = { top: anchor.top - edge.top, bottom: edge.bottom - anchor.bottom, left: anchor.left - edge.left, right: edge.right - anchor.right };
6
6
  const opposite = { top: "bottom", bottom: "top", left: "right", right: "left" };
7
7
  const needed = (side === "top" || side === "bottom" ? size.height : size.width) + gap;
8
- if (space[side] < needed && space[opposite[side]] > space[side])
8
+ if (!overlap && space[side] < needed && space[opposite[side]] > space[side])
9
9
  side = opposite[side];
10
10
  const horizontal = side === "top" || side === "bottom";
11
11
  const resolved = horizontal && rtl ? (align === "start" ? "end" : align === "end" ? "start" : align) : align;
12
12
  let left = horizontal ? (resolved === "start" ? anchor.left : resolved === "end" ? anchor.right - size.width : anchor.left + (anchor.width - size.width) / 2) : side === "left" ? anchor.left - size.width - gap : anchor.right + gap;
13
13
  let top = horizontal ? (side === "top" ? anchor.top - size.height - gap : anchor.bottom + gap) : (resolved === "start" ? anchor.top : resolved === "end" ? anchor.bottom - size.height : anchor.top + (anchor.height - size.height) / 2);
14
+ if (overlap)
15
+ top = anchor.top;
14
16
  left = Math.max(edge.left, Math.min(left, edge.right - size.width));
15
17
  top = Math.max(edge.top, Math.min(top, edge.bottom - size.height));
16
18
  return { left, top, side };
@@ -58,7 +60,7 @@ export function registerLayer(document, layer) {
58
60
  (layer.outside ?? layer.close)();
59
61
  };
60
62
  const focus = (event) => {
61
- if (top() !== layer || layer.modal)
63
+ if (top() !== layer || layer.modal || layer.closeOnFocusOutside === false)
62
64
  return;
63
65
  const target = event.target;
64
66
  if (!layer.panel.contains(target) && !layer.anchor()?.contains(target))
@@ -109,7 +111,7 @@ export function lockScroll(document) {
109
111
  locks.delete(document);
110
112
  };
111
113
  }
112
- export function positionLayer(anchor, panel, side, align) {
114
+ export function positionLayer(anchor, panel, side, align, overlap = false) {
113
115
  const view = panel.ownerDocument.defaultView;
114
116
  const visual = view.visualViewport;
115
117
  const viewport = { left: visual?.offsetLeft ?? 0, top: visual?.offsetTop ?? 0, width: visual?.width ?? view.innerWidth, height: visual?.height ?? view.innerHeight };
@@ -121,7 +123,7 @@ export function positionLayer(anchor, panel, side, align) {
121
123
  panel.style.setProperty("--flood-overlay-anchor-width", anchorWidth);
122
124
  if (panel.style.getPropertyValue("--flood-overlay-anchor-height") !== anchorHeight)
123
125
  panel.style.setProperty("--flood-overlay-anchor-height", anchorHeight);
124
- const position = placeOverlay(bounds, panel.getBoundingClientRect(), viewport, side, align, gap, getComputedStyle(anchor).direction === "rtl");
126
+ const position = placeOverlay(bounds, panel.getBoundingClientRect(), viewport, side, align, gap, getComputedStyle(anchor).direction === "rtl", overlap);
125
127
  const left = `${position.left}px`, top = `${position.top}px`;
126
128
  if (panel.style.left !== left)
127
129
  panel.style.left = left;
@@ -136,7 +138,7 @@ export function returnFocus(panel, anchor) {
136
138
  anchor.focus({ preventScroll: true });
137
139
  }
138
140
  /** Retain pixels for a CSS exit after the native layer has released focus. */
139
- export function useLayerExitContent(panel, open, enabled = true) {
141
+ export function useLayerExitContent(panel, open, enabled = true, transitionProperty = "opacity") {
140
142
  const [retained, setRetained] = useState(false);
141
143
  useLayerLayoutEffect(() => {
142
144
  const node = panel.current;
@@ -149,7 +151,7 @@ export function useLayerExitContent(panel, open, enabled = true) {
149
151
  return;
150
152
  }
151
153
  node.inert = true;
152
- const transitions = node.getAnimations().filter(animation => "transitionProperty" in animation && animation.transitionProperty === "opacity");
154
+ const transitions = node.getAnimations().filter(animation => "transitionProperty" in animation && animation.transitionProperty === transitionProperty);
153
155
  let active = true;
154
156
  if (!transitions.length)
155
157
  setRetained(false);
@@ -159,7 +161,7 @@ export function useLayerExitContent(panel, open, enabled = true) {
159
161
  setRetained(false);
160
162
  });
161
163
  return () => { active = false; node.inert = false; };
162
- }, [panel, open, enabled]);
164
+ }, [panel, open, enabled, transitionProperty]);
163
165
  return open || enabled && retained;
164
166
  }
165
167
  const useLayerLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
@@ -7,6 +7,7 @@ interface ModalOverlayProps extends DialogProps {
7
7
  showCloseButton?: boolean;
8
8
  role?: "dialog" | "alertdialog";
9
9
  scrollBody?: boolean;
10
+ bodyContentInset?: "none" | "scrollbar";
10
11
  footer?: import("react").ReactNode;
11
12
  }
12
13
  export declare const ModalOverlay: import("react").ForwardRefExoticComponent<ModalOverlayProps & import("react").RefAttributes<HTMLDialogElement>>;
package/dist/overlays.js CHANGED
@@ -126,7 +126,7 @@ export const AnchoredOverlay = /* @__PURE__ */ forwardRef(function AnchoredOverl
126
126
  leave();
127
127
  } }, children: kind === "tooltip" ? tip.content : _jsxs(_Fragment, { children: [pop.chrome === "minimal" ? _jsx(VisuallyHidden, { children: _jsx("h2", { id: `${id}-title`, children: pop.title }) }) : _jsxs("div", { className: "flood-overlay-header", children: [_jsx("h2", { id: `${id}-title`, className: "flood-overlay-title", children: pop.title }), !titleOnly && _jsx(OverlayClose, { label: pop.closeLabel ?? "Close", onClick: close })] }), showContent && _jsx("div", { className: "flood-overlay-body", children: pop.children })] }) })] });
128
128
  });
129
- export const ModalOverlay = /* @__PURE__ */ forwardRef(function ModalOverlay({ showCloseButton = true, scrollBody = false, footer, title, description, trigger, children, open: controlled, defaultOpen = false, onOpenChange, disabled = false, closeLabel = "Close", closeOnOutsideClick = true, initialFocusRef, id: explicitId, className, onKeyDown, "aria-describedby": describedBy, ...props }, forwardedRef) {
129
+ export const ModalOverlay = /* @__PURE__ */ forwardRef(function ModalOverlay({ showCloseButton = true, scrollBody = false, bodyContentInset, footer, title, description, trigger, children, open: controlled, defaultOpen = false, onOpenChange, disabled = false, closeLabel = "Close", closeOnOutsideClick = true, initialFocusRef, id: explicitId, className, onKeyDown, "aria-describedby": describedBy, ...props }, forwardedRef) {
130
130
  const [local, setLocal] = useState(defaultOpen), open = controlled ?? local;
131
131
  const dialog = useRef(null), anchor = useRef(null), generated = useId(), id = explicitId ?? `flood-dialog-${generated}`;
132
132
  const request = (next) => { if (controlled === undefined)
@@ -194,5 +194,5 @@ export const ModalOverlay = /* @__PURE__ */ forwardRef(function ModalOverlay({ s
194
194
  event.preventDefault();
195
195
  first.focus();
196
196
  }
197
- }, children: [_jsxs("div", { className: "flood-overlay-header", children: [_jsx("h2", { id: `${id}-title`, className: "flood-overlay-title", children: title }), showCloseButton && _jsx(OverlayClose, { label: closeLabel, onClick: () => request(false) })] }), description && _jsx("p", { id: `${id}-description`, className: "flood-overlay-description", children: description }), open && (scrollBody ? _jsx("div", { className: "flood-drawer__body", children: _jsx(ScrollArea, { className: "flood-overlay-body", style: { height: "100%" }, children: children }) }) : _jsx("div", { className: "flood-overlay-body", children: children })), open && footer && _jsx("div", { className: "flood-drawer__footer", children: footer })] })] });
197
+ }, children: [_jsxs("div", { className: "flood-overlay-header", children: [_jsx("h2", { id: `${id}-title`, className: "flood-overlay-title", children: title }), showCloseButton && _jsx(OverlayClose, { label: closeLabel, onClick: () => request(false) })] }), description && _jsx("p", { id: `${id}-description`, className: "flood-overlay-description", children: description }), open && (scrollBody ? _jsx("div", { className: "flood-drawer__body", children: _jsx(ScrollArea, { className: "flood-overlay-body", contentInset: bodyContentInset, style: { height: "100%" }, children: children }) }) : _jsx("div", { className: "flood-overlay-body", children: children })), open && footer && _jsx("div", { className: "flood-drawer__footer", children: footer })] })] });
198
198
  });
@@ -0,0 +1,2 @@
1
+ import { type SparklineProps } from "./index.js";
2
+ export declare const TrendPlot: import("react").ForwardRefExoticComponent<SparklineProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,65 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useId } from "react";
3
+ import { Text } from "./index.js";
4
+ const plotWidth = 96;
5
+ const plotHeight = 32;
6
+ const plotInset = 3;
7
+ function isObservation(value) {
8
+ return typeof value === "number" && Number.isFinite(value);
9
+ }
10
+ function getPlot(values, domain, area) {
11
+ let observedMin = Infinity;
12
+ let observedMax = -Infinity;
13
+ for (const value of values) {
14
+ if (isObservation(value)) {
15
+ observedMin = Math.min(observedMin, value);
16
+ observedMax = Math.max(observedMax, value);
17
+ }
18
+ }
19
+ if (observedMin === Infinity)
20
+ return null;
21
+ const [min, max] = domain && Number.isFinite(domain[0]) && Number.isFinite(domain[1]) && domain[0] < domain[1]
22
+ ? domain
23
+ : [observedMin, observedMax];
24
+ const span = max - min;
25
+ const commands = [];
26
+ const areas = [];
27
+ const markers = [];
28
+ let lastPoint;
29
+ let segmentStart = 0;
30
+ let segmentStartX = "";
31
+ for (const [index, value] of values.entries()) {
32
+ if (!isObservation(value))
33
+ continue;
34
+ const clamped = Math.min(max, Math.max(min, value));
35
+ // Halve the operands before subtracting if the finite bounds span more than Number.MAX_VALUE.
36
+ const fraction = span === 0 ? 0.5 : Number.isFinite(span)
37
+ ? (clamped - min) / span
38
+ : (clamped / 2 - min / 2) / (max / 2 - min / 2);
39
+ const x = values.length === 1 ? plotWidth / 2 : plotInset + index * (plotWidth - 2 * plotInset) / (values.length - 1);
40
+ const y = plotHeight - plotInset - fraction * (plotHeight - 2 * plotInset);
41
+ const continuesLine = isObservation(values[index - 1]);
42
+ const plotX = x.toFixed(2);
43
+ if (!continuesLine) {
44
+ segmentStart = commands.length;
45
+ segmentStartX = plotX;
46
+ }
47
+ commands.push(`${continuesLine ? "L" : "M"}${plotX} ${y.toFixed(2)}`);
48
+ lastPoint = { x, y, index };
49
+ if (!isObservation(values[index + 1])) {
50
+ if (!continuesLine)
51
+ markers.push(lastPoint);
52
+ else if (area)
53
+ areas.push(`${commands.slice(segmentStart).join(" ")} L${plotX} ${plotHeight} H${segmentStartX} Z`);
54
+ }
55
+ }
56
+ if (lastPoint && markers.at(-1) !== lastPoint)
57
+ markers.push(lastPoint);
58
+ return { path: commands.join(" "), area: areas.join(" "), markers, lastPoint };
59
+ }
60
+ const markerPath = ({ x, y }) => `M${x.toFixed(2)} ${y.toFixed(2)}h0`;
61
+ export const TrendPlot = /* @__PURE__ */ forwardRef(function TrendPlot({ values, domain, label, showLabel, size, tone, variant, className, ...props }, ref) {
62
+ const gradientId = useId();
63
+ const plot = getPlot(values, domain, variant === "area");
64
+ return (_jsxs("div", { ...props, ref: ref, className: ["flood-sparkline", className].filter(Boolean).join(" "), "data-size": size, "data-tone": tone, role: "img", "aria-label": label, children: [plot ? (_jsxs("svg", { className: "flood-sparkline__plot", viewBox: `0 0 ${plotWidth} ${plotHeight}`, preserveAspectRatio: "none", "aria-hidden": "true", focusable: "false", children: [plot.area && _jsxs(_Fragment, { children: [_jsx("defs", { children: _jsxs("linearGradient", { id: gradientId, x1: "0", x2: "0", y1: "0", y2: "1", children: [_jsx("stop", { stopColor: "currentColor", stopOpacity: ".42" }), _jsx("stop", { offset: "100%", stopColor: "currentColor", stopOpacity: "0" })] }) }), _jsx("path", { className: "flood-sparkline__area", d: plot.area, fill: `url(#${gradientId})` })] }), _jsx("path", { className: "flood-sparkline__line", d: plot.path }), plot.lastPoint && _jsx("path", { className: "flood-sparkline__halo", d: markerPath(plot.lastPoint) }), plot.markers.map(point => (_jsx("path", { className: "flood-sparkline__point", d: markerPath(point), "data-latest": point === plot.lastPoint || undefined }, point.index)))] })) : (_jsx(Text, { className: "flood-sparkline__empty", size: "helper", tone: "subtle", "aria-hidden": "true", children: "\u2014" })), showLabel && _jsx(Text, { className: "flood-sparkline__label", size: "helper", tone: "subtle", "aria-hidden": "true", children: label })] }));
65
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flood-md/react",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "type": "module",
5
5
  "description": "Original Flood components for React and Next.js.",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  "react-dom": "^18.3.0 || ^19.0.0"
31
31
  },
32
32
  "dependencies": {
33
- "@flood-md/tokens": "0.2.11"
33
+ "@flood-md/tokens": "0.2.12"
34
34
  },
35
35
  "publishConfig": {
36
36
  "access": "public"