@nexub/react 7.5.0 → 7.6.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,5 +1,5 @@
1
1
  {
2
- "version": "7.5.0",
2
+ "version": "7.6.0",
3
3
  "standard": "WCAG 2.2 Level AA",
4
4
  "method": {
5
5
  "automated": "axe-core rules tagged wcag2a, wcag2aa, wcag21a, wcag21aa and wcag22aa, run on every Storybook story in light and dark, and on open popups (menus, listboxes, dialogs), on every pull request; a new violation fails the build.",
@@ -8,5 +8,14 @@ export declare const badgeVariants: (props?: ({
8
8
  export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {
9
9
  tone?: ToneValue;
10
10
  leadingIcon?: React.ReactNode;
11
+ /**
12
+ * A status dot. Before the label when there is one; on its own (no children
13
+ * or count) it is just the dot: give it an `aria-label`, or it's decorative.
14
+ */
15
+ dot?: boolean;
16
+ /** A number to show. Above `max` it reads "99+"; screen readers get the full number. */
17
+ count?: number;
18
+ /** @default 99 */
19
+ max?: number;
11
20
  }
12
21
  export declare const Badge: React.ForwardRefExoticComponent<BadgeProps & React.RefAttributes<HTMLSpanElement>>;
@@ -72,10 +72,37 @@ const toneClasses = {
72
72
  outline: "border-[color:light-dark(var(--color-palette-founderub-80),var(--color-palette-founderub-40))] text-[var(--color-founderub)]"
73
73
  }
74
74
  };
75
+ const dotClasses = {
76
+ neutral: "bg-[var(--color-outline)]",
77
+ info: "bg-[var(--color-info)]",
78
+ success: "bg-[var(--color-success)]",
79
+ warning: "bg-[var(--color-warning)]",
80
+ danger: "bg-[var(--color-error)]",
81
+ leap: "bg-[var(--color-leap)]",
82
+ bookub: "bg-[var(--color-bookub)]",
83
+ payub: "bg-[var(--color-payub)]",
84
+ founderub: "bg-[var(--color-founderub)]"
85
+ };
86
+ const dotSizes = { sm: "size-1.5", md: "size-2", lg: "size-2.5" };
75
87
  const Badge = React.forwardRef(
76
- ({ className, size, variant = "solid", tone = "neutral", leadingIcon, children, ...props }, ref) => {
88
+ ({ className, size, variant = "solid", tone = "neutral", leadingIcon, dot, count, max = 99, children, ...props }, ref) => {
77
89
  const resolvedVariant = variant ?? "solid";
78
90
  const toneStyle = toneClasses[tone][resolvedVariant];
91
+ const dotSize = dotSizes[size ?? "md"];
92
+ if (dot && children == null && count == null) {
93
+ const labelled = props["aria-label"] != null || props["aria-labelledby"] != null;
94
+ return /* @__PURE__ */ jsx(
95
+ "span",
96
+ {
97
+ ref,
98
+ role: labelled ? "img" : void 0,
99
+ "aria-hidden": labelled ? void 0 : true,
100
+ className: cn("inline-block shrink-0 rounded-full", dotSize, dotClasses[tone], className),
101
+ ...props
102
+ }
103
+ );
104
+ }
105
+ const capped = count != null && count > max;
79
106
  return /* @__PURE__ */ jsxs(
80
107
  "span",
81
108
  {
@@ -83,8 +110,19 @@ const Badge = React.forwardRef(
83
110
  className: cn(badgeVariants({ size, variant: resolvedVariant }), toneStyle, className),
84
111
  ...props,
85
112
  children: [
113
+ dot && /* @__PURE__ */ jsx(
114
+ "span",
115
+ {
116
+ "aria-hidden": "true",
117
+ className: cn("shrink-0 rounded-full", dotSize, resolvedVariant === "solid" ? "bg-current" : dotClasses[tone])
118
+ }
119
+ ),
86
120
  leadingIcon && /* @__PURE__ */ jsx("span", { className: "shrink-0 inline-flex items-center", children: leadingIcon }),
87
- children
121
+ children,
122
+ count != null && /* @__PURE__ */ jsxs("span", { className: "tabular-nums", children: [
123
+ /* @__PURE__ */ jsx("span", { "aria-hidden": capped || void 0, className: "nx-tick", children: capped ? `${max}+` : count }, count),
124
+ capped && /* @__PURE__ */ jsx("span", { className: "sr-only", children: count })
125
+ ] })
88
126
  ]
89
127
  }
90
128
  );
@@ -4,7 +4,7 @@ import type { ToneValue } from '@nexub/core';
4
4
  export type { ToneValue };
5
5
  export { toneTextClasses } from '../../utils/control.js';
6
6
  export declare const buttonVariants: (props?: ({
7
- variant?: "accent" | "text" | "link" | "outline" | "primary" | "glass" | "ghost" | "subtle" | "filled" | "tonal" | "outlined" | "hero" | "secondary" | "destructive" | null | undefined;
7
+ variant?: "accent" | "text" | "link" | "outline" | "primary" | "glass" | "filled" | "tonal" | "outlined" | "hero" | "secondary" | "subtle" | "ghost" | "destructive" | null | undefined;
8
8
  size?: "xs" | "sm" | "md" | "lg" | "xl" | null | undefined;
9
9
  fullWidth?: boolean | null | undefined;
10
10
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import * as RadixDialog from '@radix-ui/react-dialog';
3
+ import { Command } from 'cmdk';
3
4
  export interface CommandPaletteRootProps extends RadixDialog.DialogProps {
4
5
  className?: string;
5
6
  /** Accessible name of the dialog. @default 'Command palette' */
@@ -17,6 +18,36 @@ export interface CommandPaletteRootProps extends RadixDialog.DialogProps {
17
18
  * the page, e.g. `'k'` for Cmd+K / Ctrl+K. Off by default.
18
19
  */
19
20
  shortcut?: string;
21
+ /** The open page stack (ids of `CommandPalette.Page`s), deepest last. Empty = the root page. */
22
+ pages?: string[];
23
+ defaultPages?: string[];
24
+ onPagesChange?: (pages: string[]) => void;
25
+ /** Shows a loading row (e.g. while async results arrive) and hides `Empty`. */
26
+ loading?: boolean;
27
+ /**
28
+ * Opt-in recents: the last `recentLimit` items picked on the root page are
29
+ * kept in localStorage under this key and shown as a "Recent" group while
30
+ * the query is empty.
31
+ */
32
+ recentKey?: string;
33
+ /** @default 5 */
34
+ recentLimit?: number;
35
+ }
36
+ export interface CommandPaletteItemProps extends React.ComponentProps<typeof Command.Item> {
37
+ /** Opens this `CommandPalette.Page` (by id) when selected. */
38
+ page?: string;
39
+ }
40
+ export interface CommandPalettePageProps {
41
+ /** Pushed by `<CommandPalette.Item page="id">`. Leave out for the root page. */
42
+ id?: string;
43
+ /** Shown in the input's breadcrumb. Defaults to the id. */
44
+ title?: string;
45
+ children?: React.ReactNode;
46
+ }
47
+ /** Content shown only while this page is on top of the stack (no id: the root page). */
48
+ declare function Page({ id, title, children }: CommandPalettePageProps): React.JSX.Element | null;
49
+ declare namespace Page {
50
+ var displayName: string;
20
51
  }
21
52
  export declare const CommandPalette: {
22
53
  Root: React.ForwardRefExoticComponent<CommandPaletteRootProps & React.RefAttributes<HTMLDivElement>>;
@@ -55,19 +86,7 @@ export declare const CommandPalette: {
55
86
  value?: string;
56
87
  forceMount?: boolean;
57
88
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
58
- Item: React.ForwardRefExoticComponent<Omit<{
59
- children?: React.ReactNode;
60
- } & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
61
- ref?: React.Ref<HTMLDivElement>;
62
- } & {
63
- asChild?: boolean;
64
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>>, "value" | "disabled" | "onSelect"> & {
65
- disabled?: boolean;
66
- onSelect?: (value: string) => void;
67
- value?: string;
68
- keywords?: string[];
69
- forceMount?: boolean;
70
- } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
89
+ Item: React.ForwardRefExoticComponent<Omit<CommandPaletteItemProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
71
90
  Separator: React.ForwardRefExoticComponent<Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
72
91
  ref?: React.Ref<HTMLDivElement>;
73
92
  } & {
@@ -75,4 +94,6 @@ export declare const CommandPalette: {
75
94
  }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
76
95
  alwaysRender?: boolean;
77
96
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
97
+ Page: typeof Page;
78
98
  };
99
+ export {};
@@ -1,8 +1,8 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as RadixDialog from "@radix-ui/react-dialog";
5
- import { Command } from "cmdk";
5
+ import { Command, useCommandState } from "cmdk";
6
6
  import { Search } from "@nexub/icons";
7
7
  import { cn } from "../../utils/cn.js";
8
8
  import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
@@ -10,15 +10,72 @@ import { overlaySurface, scrim } from "../../utils/surface.js";
10
10
  import { useClassScrollLock } from "../../hooks/useClassScrollLock.js";
11
11
  import { useControllableState } from "../../hooks/useControllableState.js";
12
12
  import { useMergedRefs } from "../../hooks/useMergedRefs.js";
13
+ import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect.js";
14
+ import { Badge } from "../Badge/Badge.js";
15
+ import { Spinner } from "../Spinner/Spinner.js";
16
+ const PaletteContext = React.createContext(null);
17
+ const usePalette = () => React.useContext(PaletteContext);
18
+ function readRecents(key) {
19
+ try {
20
+ const parsed = JSON.parse(localStorage.getItem(key) ?? "[]");
21
+ return Array.isArray(parsed) ? parsed.filter((r) => typeof r?.value === "string" && typeof r?.label === "string") : [];
22
+ } catch {
23
+ return [];
24
+ }
25
+ }
13
26
  const overlayClasses = cn("fixed inset-0 z-[var(--z-modal)] nx-motion", scrim);
14
27
  function ClassLockOverlay() {
15
28
  useClassScrollLock();
16
29
  return /* @__PURE__ */ jsx("div", { "data-state": "open", className: overlayClasses });
17
30
  }
18
31
  const Root = React.forwardRef(
19
- ({ children, label: labelProp, scrollLock = "style", shouldFilter, shortcut, open: openProp, defaultOpen, onOpenChange, ...props }, ref) => {
32
+ ({
33
+ children,
34
+ label: labelProp,
35
+ scrollLock = "style",
36
+ shouldFilter,
37
+ shortcut,
38
+ open: openProp,
39
+ defaultOpen,
40
+ onOpenChange,
41
+ pages: pagesProp,
42
+ defaultPages,
43
+ onPagesChange,
44
+ loading,
45
+ recentKey,
46
+ recentLimit = 5,
47
+ ...props
48
+ }, ref) => {
20
49
  const messages = useNexubMessages();
21
- const [open = false, setOpen] = useControllableState({ value: openProp, defaultValue: defaultOpen, onChange: onOpenChange });
50
+ const [pages = [], setPages] = useControllableState({ value: pagesProp, defaultValue: defaultPages, onChange: onPagesChange });
51
+ const [open = false, setOpenState] = useControllableState({ value: openProp, defaultValue: defaultOpen, onChange: onOpenChange });
52
+ const setOpen = (next) => {
53
+ setOpenState(next);
54
+ if (!next && pages.length) setPages([]);
55
+ };
56
+ const [titles, setTitles] = React.useState({});
57
+ const [recents, setRecents] = React.useState([]);
58
+ const handlers = React.useRef(/* @__PURE__ */ new Map()).current;
59
+ React.useEffect(() => setRecents(recentKey ? readRecents(recentKey) : []), [recentKey]);
60
+ const ctx = {
61
+ pages,
62
+ push: (page) => setPages([...pages, page]),
63
+ pop: () => setPages(pages.slice(0, -1)),
64
+ titles,
65
+ setTitle: (page, title) => setTitles((t) => t[page] === title ? t : { ...t, [page]: title }),
66
+ loading,
67
+ recents,
68
+ record: (recent) => {
69
+ if (!recentKey || pages.length) return;
70
+ const next = [recent, ...recents.filter((r) => r.value !== recent.value)].slice(0, recentLimit);
71
+ setRecents(next);
72
+ try {
73
+ localStorage.setItem(recentKey, JSON.stringify(next));
74
+ } catch {
75
+ }
76
+ },
77
+ handlers
78
+ };
22
79
  React.useEffect(() => {
23
80
  if (!shortcut) return;
24
81
  const onKeyDown = (e) => {
@@ -60,7 +117,7 @@ const Root = React.forwardRef(
60
117
  shouldFilter,
61
118
  loop: true,
62
119
  className: "flex h-full w-full flex-col overflow-hidden text-[var(--color-on-surface)]",
63
- children
120
+ children: /* @__PURE__ */ jsx(PaletteContext.Provider, { value: ctx, children })
64
121
  }
65
122
  )
66
123
  ]
@@ -70,31 +127,75 @@ const Root = React.forwardRef(
70
127
  }
71
128
  );
72
129
  Root.displayName = "CommandPalette.Root";
73
- const Input = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsxs("div", { className: "flex items-center border-b border-[var(--color-outline-variant)] px-3", children: [
74
- /* @__PURE__ */ jsx(Search, { size: "sm", className: "me-2 shrink-0 opacity-50", "aria-hidden": "true" }),
75
- /* @__PURE__ */ jsx(
76
- Command.Input,
130
+ const Input = React.forwardRef(({ className, value: valueProp, onValueChange, onKeyDown, ...props }, ref) => {
131
+ const palette = usePalette();
132
+ const pages = palette?.pages ?? [];
133
+ const [value = "", setValue] = useControllableState({ value: valueProp, defaultValue: "", onChange: onValueChange });
134
+ const depth = pages.length;
135
+ const first = React.useRef(true);
136
+ React.useEffect(() => {
137
+ if (first.current) first.current = false;
138
+ else setValue("");
139
+ }, [depth]);
140
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 border-b border-[var(--color-outline-variant)] px-3", children: [
141
+ /* @__PURE__ */ jsx(Search, { size: "sm", className: "me-1 shrink-0 opacity-50", "aria-hidden": "true" }),
142
+ pages.map((page) => /* @__PURE__ */ jsx(Badge, { variant: "soft", size: "sm", className: "shrink-0", children: palette?.titles[page] ?? page }, page)),
143
+ /* @__PURE__ */ jsx(
144
+ Command.Input,
145
+ {
146
+ ref,
147
+ value,
148
+ onValueChange: setValue,
149
+ onKeyDown: (e) => {
150
+ onKeyDown?.(e);
151
+ if (!e.defaultPrevented && e.key === "Backspace" && !e.currentTarget.value && depth) {
152
+ e.preventDefault();
153
+ palette?.pop();
154
+ }
155
+ },
156
+ className: cn(
157
+ "ms-1 flex h-control-xl w-full bg-transparent text-body-lg outline-none placeholder:text-[var(--color-on-surface-muted)] disabled:cursor-not-allowed disabled:text-[var(--color-text-disabled)]",
158
+ className
159
+ ),
160
+ ...props
161
+ }
162
+ )
163
+ ] });
164
+ });
165
+ Input.displayName = "CommandPalette.Input";
166
+ const List = React.forwardRef(({ className, children, ...props }, ref) => {
167
+ const palette = usePalette();
168
+ const messages = useNexubMessages();
169
+ const search = useCommandState((state) => state.search);
170
+ const showRecents = palette && !palette.loading && !palette.pages.length && !search && palette.recents.length > 0;
171
+ return /* @__PURE__ */ jsxs(
172
+ Command.List,
77
173
  {
78
174
  ref,
79
- className: cn(
80
- "flex h-control-xl w-full bg-transparent text-body-lg outline-none placeholder:text-[var(--color-on-surface-muted)] disabled:cursor-not-allowed disabled:text-[var(--color-text-disabled)]",
81
- className
82
- ),
83
- ...props
175
+ className: cn("max-h-[400px] overflow-y-auto overflow-x-hidden p-2", className),
176
+ ...props,
177
+ children: [
178
+ palette?.loading && /* @__PURE__ */ jsxs(Command.Loading, { label: messages.loading, className: cn(itemClasses, "text-[var(--color-on-surface-muted)]"), children: [
179
+ /* @__PURE__ */ jsx(Spinner, { size: "sm", "aria-hidden": "true" }),
180
+ messages.loading
181
+ ] }),
182
+ showRecents && /* @__PURE__ */ jsx(Group, { heading: messages.recent, children: palette.recents.map((r) => /* @__PURE__ */ jsx(
183
+ Command.Item,
184
+ {
185
+ value: `recent:${r.value}`,
186
+ onSelect: () => palette.handlers.get(r.value)?.(r.value),
187
+ className: itemClasses,
188
+ children: r.label
189
+ },
190
+ r.value
191
+ )) }),
192
+ children
193
+ ]
84
194
  }
85
- )
86
- ] }));
87
- Input.displayName = "CommandPalette.Input";
88
- const List = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
89
- Command.List,
90
- {
91
- ref,
92
- className: cn("max-h-[400px] overflow-y-auto overflow-x-hidden p-2", className),
93
- ...props
94
- }
95
- ));
195
+ );
196
+ });
96
197
  List.displayName = "CommandPalette.List";
97
- const Empty = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
198
+ const Empty = React.forwardRef(({ className, ...props }, ref) => usePalette()?.loading ? null : /* @__PURE__ */ jsx(
98
199
  Command.Empty,
99
200
  {
100
201
  ref,
@@ -106,6 +207,11 @@ const Empty = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */
106
207
  }
107
208
  ));
108
209
  Empty.displayName = "CommandPalette.Empty";
210
+ const itemClasses = cn(
211
+ "relative flex cursor-default select-none items-center gap-2 rounded-shape-container px-3 py-2.5 text-body-sm outline-none",
212
+ "aria-selected:bg-[color-mix(in_srgb,var(--color-on-surface)_8%,transparent)]",
213
+ "data-[disabled=true]:pointer-events-none data-[disabled=true]:text-[var(--color-text-disabled)]"
214
+ );
109
215
  const Group = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
110
216
  Command.Group,
111
217
  {
@@ -119,20 +225,37 @@ const Group = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */
119
225
  }
120
226
  ));
121
227
  Group.displayName = "CommandPalette.Group";
122
- const Item = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
123
- Command.Item,
124
- {
125
- ref,
126
- className: cn(
127
- // Concentric: overlay 24px − 8px padding = 16px.
128
- "relative flex cursor-default select-none items-center gap-2 rounded-shape-container px-3 py-2.5 text-body-sm outline-none",
129
- "aria-selected:bg-[color-mix(in_srgb,var(--color-on-surface)_8%,transparent)]",
130
- "data-[disabled=true]:pointer-events-none data-[disabled=true]:text-[var(--color-text-disabled)]",
131
- className
132
- ),
133
- ...props
228
+ const Item = React.forwardRef(
229
+ ({ className, page, onSelect, ...props }, ref) => {
230
+ const palette = usePalette();
231
+ const local = React.useRef(null);
232
+ const select = (value) => {
233
+ onSelect?.(value);
234
+ if (page) palette?.push(page);
235
+ palette?.record({ value, label: local.current?.textContent?.trim() ?? value });
236
+ };
237
+ const selectRef = React.useRef(select);
238
+ selectRef.current = select;
239
+ React.useEffect(() => {
240
+ const value = local.current?.getAttribute("data-value");
241
+ if (!palette || !value) return;
242
+ const run = (v) => selectRef.current(v);
243
+ palette.handlers.set(value, run);
244
+ return () => {
245
+ if (palette.handlers.get(value) === run) palette.handlers.delete(value);
246
+ };
247
+ });
248
+ return /* @__PURE__ */ jsx(
249
+ Command.Item,
250
+ {
251
+ ref: useMergedRefs(ref, local),
252
+ onSelect: select,
253
+ className: cn(itemClasses, className),
254
+ ...props
255
+ }
256
+ );
134
257
  }
135
- ));
258
+ );
136
259
  Item.displayName = "CommandPalette.Item";
137
260
  const Separator = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
138
261
  Command.Separator,
@@ -143,7 +266,18 @@ const Separator = React.forwardRef(({ className, ...props }, ref) => /* @__PURE_
143
266
  }
144
267
  ));
145
268
  Separator.displayName = "CommandPalette.Separator";
146
- const CommandPalette = { Root, Input, List, Empty, Group, Item, Separator };
269
+ function Page({ id, title, children }) {
270
+ const palette = usePalette();
271
+ const setTitle = palette?.setTitle;
272
+ useIsomorphicLayoutEffect(() => {
273
+ if (id && title) setTitle?.(id, title);
274
+ }, [id, title]);
275
+ const pages = palette?.pages ?? [];
276
+ const active = id ? pages[pages.length - 1] === id : pages.length === 0;
277
+ return active ? /* @__PURE__ */ jsx(Fragment, { children }) : null;
278
+ }
279
+ Page.displayName = "CommandPalette.Page";
280
+ const CommandPalette = { Root, Input, List, Empty, Group, Item, Separator, Page };
147
281
  export {
148
282
  CommandPalette
149
283
  };
@@ -1,2 +1,2 @@
1
1
  export { CommandPalette } from './CommandPalette.js';
2
- export type { CommandPaletteRootProps } from './CommandPalette.js';
2
+ export type { CommandPaletteRootProps, CommandPaletteItemProps, CommandPalettePageProps } from './CommandPalette.js';
@@ -15,6 +15,12 @@ declare function Root({ scrollLock, modal, ...props }: DialogRootProps): React.J
15
15
  interface ContentProps extends RadixDialog.DialogContentProps {
16
16
  size?: Size | 'xl' | 'full';
17
17
  surface?: SurfaceValue;
18
+ /** Adds a close button in the top-end corner. @default false */
19
+ showClose?: boolean;
20
+ /** Accessible name of the close button. @default messages.close */
21
+ closeLabel?: string;
22
+ /** `true`: bottom sheet. `'auto'`: bottom sheet below the `md` breakpoint, centred above. @default false */
23
+ sheet?: boolean | 'auto';
18
24
  }
19
25
  export declare const Dialog: {
20
26
  Root: typeof Root;
@@ -26,6 +32,7 @@ export declare const Dialog: {
26
32
  Title: React.ForwardRefExoticComponent<RadixDialog.DialogTitleProps & React.RefAttributes<HTMLHeadingElement>>;
27
33
  Description: React.ForwardRefExoticComponent<RadixDialog.DialogDescriptionProps & React.RefAttributes<HTMLParagraphElement>>;
28
34
  Header: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
35
+ Body: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
29
36
  Footer: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
30
37
  };
31
38
  export {};
@@ -1,11 +1,15 @@
1
1
  "use client";
2
- import { jsx } from "react/jsx-runtime";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as RadixDialog from "@radix-ui/react-dialog";
5
5
  import { cn } from "../../utils/cn.js";
6
6
  import { overlaySurface, scrim } from "../../utils/surface.js";
7
7
  import { glassSurfaceClass } from "../../utils/glass.js";
8
8
  import { ClassLockOverlay, ScrollLockContext } from "../../hooks/useClassScrollLock.js";
9
+ import { useMediaQuery } from "../../hooks/useMediaQuery.js";
10
+ import { X } from "@nexub/icons";
11
+ import { IconButton } from "../IconButton/IconButton.js";
12
+ import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
9
13
  function Root({ scrollLock = "style", modal = true, ...props }) {
10
14
  const lock = React.useMemo(() => ({ scrollLock, modal }), [scrollLock, modal]);
11
15
  return /* @__PURE__ */ jsx(ScrollLockContext.Provider, { value: lock, children: /* @__PURE__ */ jsx(RadixDialog.Root, { modal, ...props }) });
@@ -33,8 +37,19 @@ const Overlay = React.forwardRef(
33
37
  }
34
38
  );
35
39
  Overlay.displayName = "Dialog.Overlay";
40
+ const StackContext = React.createContext(null);
36
41
  const Content = React.forwardRef(
37
- function DialogContent({ className, size = "md", surface = "solid", children, ...props }, ref) {
42
+ function DialogContent({ className, size = "md", surface = "solid", showClose = false, closeLabel, sheet = false, children, ...props }, ref) {
43
+ const messages = useNexubMessages();
44
+ const narrow = useMediaQuery("(max-width: 47.99rem)");
45
+ const asSheet = sheet === true || sheet === "auto" && narrow;
46
+ const parent = React.useContext(StackContext);
47
+ const [openChildren, setOpenChildren] = React.useState(0);
48
+ React.useEffect(() => {
49
+ if (!parent) return;
50
+ parent((n) => n + 1);
51
+ return () => parent((n) => n - 1);
52
+ }, [parent]);
38
53
  const sizeMap = {
39
54
  sm: "max-w-sm",
40
55
  md: "max-w-lg",
@@ -42,23 +57,28 @@ const Content = React.forwardRef(
42
57
  xl: "max-w-4xl",
43
58
  full: "max-w-[calc(100vw-2rem)]"
44
59
  };
45
- return /* @__PURE__ */ jsx(
60
+ return /* @__PURE__ */ jsxs(
46
61
  RadixDialog.Content,
47
62
  {
48
63
  ref,
49
64
  className: cn(
50
- "fixed left-1/2 top-1/2 z-[var(--z-modal)] w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2",
65
+ asSheet ? "fixed inset-x-0 bottom-0 z-[var(--z-modal)] w-full" : "fixed left-1/2 top-1/2 z-[var(--z-modal)] w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2",
51
66
  "nx-motion nx-dialog",
52
67
  overlaySurface,
53
68
  "max-h-[90vh] overflow-y-auto p-6 focus:outline-none",
54
- sizeMap[size],
69
+ asSheet ? "rounded-b-none" : sizeMap[size],
55
70
  surface === "glass" && glassSurfaceClass,
56
71
  className
57
72
  ),
58
73
  "data-material": surface === "glass" ? "glass" : void 0,
59
74
  "data-glass-thickness": surface === "glass" ? "thick" : void 0,
75
+ "data-sheet": asSheet ? "" : void 0,
76
+ "data-nested": openChildren > 0 ? "" : void 0,
60
77
  ...props,
61
- children
78
+ children: [
79
+ /* @__PURE__ */ jsx(StackContext.Provider, { value: setOpenChildren, children }),
80
+ showClose && /* @__PURE__ */ jsx(RadixDialog.Close, { asChild: true, children: /* @__PURE__ */ jsx(IconButton, { label: closeLabel ?? messages.close, variant: "ghost", size: "sm", className: "absolute end-3 top-3", children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" }) }) })
81
+ ]
62
82
  }
63
83
  );
64
84
  }
@@ -92,16 +112,22 @@ const Description = React.forwardRef(
92
112
  Description.displayName = "Dialog.Description";
93
113
  const Header = React.forwardRef(
94
114
  function DialogHeader({ className, ...props }, ref) {
95
- return /* @__PURE__ */ jsx("div", { ref, className: cn("mb-4 flex flex-col gap-1", className), ...props });
115
+ return /* @__PURE__ */ jsx("div", { ref, className: cn("nx-dialog-header mb-4 flex flex-col gap-1", className), ...props });
96
116
  }
97
117
  );
98
118
  Header.displayName = "Dialog.Header";
99
119
  const Footer = React.forwardRef(
100
120
  function DialogFooter({ className, ...props }, ref) {
101
- return /* @__PURE__ */ jsx("div", { ref, className: cn("mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className), ...props });
121
+ return /* @__PURE__ */ jsx("div", { ref, className: cn("nx-dialog-footer mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className), ...props });
102
122
  }
103
123
  );
104
124
  Footer.displayName = "Dialog.Footer";
125
+ const Body = React.forwardRef(
126
+ function DialogBody({ className, ...props }, ref) {
127
+ return /* @__PURE__ */ jsx("div", { ref, className: cn("nx-dialog-body", className), ...props });
128
+ }
129
+ );
130
+ Body.displayName = "Dialog.Body";
105
131
  const Dialog = {
106
132
  Root,
107
133
  Trigger,
@@ -112,6 +138,7 @@ const Dialog = {
112
138
  Title,
113
139
  Description,
114
140
  Header,
141
+ Body,
115
142
  Footer
116
143
  };
117
144
  export {
@@ -1,6 +1,12 @@
1
1
  import * as React from 'react';
2
+ export interface EmptyStateRootProps extends React.HTMLAttributes<HTMLDivElement> {
3
+ /** Scales spacing, icon and text. @default 'md' */
4
+ size?: 'sm' | 'md' | 'lg';
5
+ /** Fades in on mount (nx-reveal; reduced motion keeps the crossfade). */
6
+ animate?: boolean;
7
+ }
2
8
  export declare const EmptyState: {
3
- Root: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
9
+ Root: React.ForwardRefExoticComponent<EmptyStateRootProps & React.RefAttributes<HTMLDivElement>>;
4
10
  Illustration: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
5
11
  Title: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLHeadingElement> & React.RefAttributes<HTMLHeadingElement>>;
6
12
  Description: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLParagraphElement> & React.RefAttributes<HTMLParagraphElement>>;
@@ -1,13 +1,17 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { cn } from "../../utils/cn.js";
4
+ const rootSizes = { sm: "gap-3 py-8", md: "gap-4 py-16", lg: "gap-6 py-24" };
4
5
  const Root = React.forwardRef(
5
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx(
6
+ ({ className, size = "md", animate, ...props }, ref) => /* @__PURE__ */ jsx(
6
7
  "div",
7
8
  {
8
9
  ref,
10
+ "data-size": size,
9
11
  className: cn(
10
- "flex flex-col items-center justify-center gap-4 py-16 text-center",
12
+ "group/empty-state flex flex-col items-center justify-center text-center",
13
+ rootSizes[size],
14
+ animate && "nx-reveal",
11
15
  className
12
16
  ),
13
17
  ...props
@@ -20,7 +24,11 @@ const Illustration = React.forwardRef(
20
24
  "div",
21
25
  {
22
26
  ref,
23
- className: cn("text-[var(--color-on-surface-muted)]", className),
27
+ className: cn(
28
+ "text-[var(--color-on-surface-muted)]",
29
+ "group-data-[size=sm]/empty-state:[&_svg]:size-8 group-data-[size=lg]/empty-state:[&_svg]:size-16",
30
+ className
31
+ ),
24
32
  ...props
25
33
  }
26
34
  )
@@ -31,7 +39,11 @@ const Title = React.forwardRef(
31
39
  "h3",
32
40
  {
33
41
  ref,
34
- className: cn("text-lg font-semibold text-[var(--color-on-surface)]", className),
42
+ className: cn(
43
+ "text-lg font-semibold text-[var(--color-on-surface)]",
44
+ "group-data-[size=sm]/empty-state:text-base group-data-[size=lg]/empty-state:text-xl",
45
+ className
46
+ ),
35
47
  ...props
36
48
  }
37
49
  )
@@ -41,7 +53,11 @@ const Description = React.forwardRef(({ className, ...props }, ref) => /* @__PUR
41
53
  "p",
42
54
  {
43
55
  ref,
44
- className: cn("max-w-sm text-sm text-[var(--color-on-surface-muted)]", className),
56
+ className: cn(
57
+ "max-w-sm text-sm text-[var(--color-on-surface-muted)]",
58
+ "group-data-[size=sm]/empty-state:text-xs group-data-[size=lg]/empty-state:text-base",
59
+ className
60
+ ),
45
61
  ...props
46
62
  }
47
63
  ));
@@ -1,7 +1,8 @@
1
1
  import * as React from 'react';
2
2
  import type { NexubMessages } from '@nexub/core';
3
+ import { type EmptyStateRootProps } from './EmptyState.js';
3
4
  export type EmptyStateKind = keyof NexubMessages['emptyStates'];
4
- export interface EmptyStatePresetProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
5
+ export interface EmptyStatePresetProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'>, Pick<EmptyStateRootProps, 'size' | 'animate'> {
5
6
  /** noData, noResults, noAccess (403), serverError (500) or offline. */
6
7
  kind: EmptyStateKind;
7
8
  /** Overrides the preset's title; the description too. Both default to localised text. */