@hexclave/dashboard-ui-components 1.0.3 → 1.0.6

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.
Files changed (98) hide show
  1. package/dist/components/alert.js +4 -4
  2. package/dist/components/alert.js.map +1 -1
  3. package/dist/components/badge.js +1 -1
  4. package/dist/components/badge.js.map +1 -1
  5. package/dist/components/button.d.ts +4 -4
  6. package/dist/components/button.js +3 -3
  7. package/dist/components/button.js.map +1 -1
  8. package/dist/components/data-grid/data-grid-toolbar.js +5 -5
  9. package/dist/components/data-grid/data-grid-toolbar.js.map +1 -1
  10. package/dist/components/data-grid/data-grid.d.ts.map +1 -1
  11. package/dist/components/data-grid/data-grid.js +11 -5
  12. package/dist/components/data-grid/data-grid.js.map +1 -1
  13. package/dist/components/data-grid/data-grid.test.js +34 -1
  14. package/dist/components/data-grid/data-grid.test.js.map +1 -1
  15. package/dist/components/dialog.d.ts +6 -2
  16. package/dist/components/dialog.d.ts.map +1 -1
  17. package/dist/components/dialog.js +14 -10
  18. package/dist/components/dialog.js.map +1 -1
  19. package/dist/components/input.js +12 -9
  20. package/dist/components/input.js.map +1 -1
  21. package/dist/components/pill-toggle.js +3 -3
  22. package/dist/components/pill-toggle.js.map +1 -1
  23. package/dist/components/tabs.js +2 -2
  24. package/dist/components/tabs.js.map +1 -1
  25. package/dist/dashboard-ui-components.global.js +15639 -17400
  26. package/dist/dashboard-ui-components.global.js.map +4 -4
  27. package/dist/esm/components/alert.js +4 -4
  28. package/dist/esm/components/alert.js.map +1 -1
  29. package/dist/esm/components/badge.js +1 -1
  30. package/dist/esm/components/badge.js.map +1 -1
  31. package/dist/esm/components/button.d.ts +4 -4
  32. package/dist/esm/components/button.js +3 -3
  33. package/dist/esm/components/button.js.map +1 -1
  34. package/dist/esm/components/data-grid/data-grid-toolbar.js +5 -5
  35. package/dist/esm/components/data-grid/data-grid-toolbar.js.map +1 -1
  36. package/dist/esm/components/data-grid/data-grid.d.ts.map +1 -1
  37. package/dist/esm/components/data-grid/data-grid.js +11 -5
  38. package/dist/esm/components/data-grid/data-grid.js.map +1 -1
  39. package/dist/esm/components/data-grid/data-grid.test.js +34 -1
  40. package/dist/esm/components/data-grid/data-grid.test.js.map +1 -1
  41. package/dist/esm/components/dialog.d.ts +6 -2
  42. package/dist/esm/components/dialog.d.ts.map +1 -1
  43. package/dist/esm/components/dialog.js +13 -11
  44. package/dist/esm/components/dialog.js.map +1 -1
  45. package/dist/esm/components/input.js +12 -9
  46. package/dist/esm/components/input.js.map +1 -1
  47. package/dist/esm/components/pill-toggle.js +4 -4
  48. package/dist/esm/components/pill-toggle.js.map +1 -1
  49. package/dist/esm/components/tabs.js +2 -2
  50. package/dist/esm/components/tabs.js.map +1 -1
  51. package/dist/esm/index.d.ts +2 -2
  52. package/dist/esm/index.js +2 -2
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.js +12 -0
  55. package/package.json +4 -3
  56. package/src/components/alert.tsx +120 -0
  57. package/src/components/analytics-chart/analytics-chart-pie.tsx +369 -0
  58. package/src/components/analytics-chart/analytics-chart.tsx +1585 -0
  59. package/src/components/analytics-chart/default-analytics-chart-tooltip.tsx +265 -0
  60. package/src/components/analytics-chart/format.ts +101 -0
  61. package/src/components/analytics-chart/index.ts +75 -0
  62. package/src/components/analytics-chart/palette.ts +68 -0
  63. package/src/components/analytics-chart/render-data-series.tsx +169 -0
  64. package/src/components/analytics-chart/state.ts +165 -0
  65. package/src/components/analytics-chart/strings.ts +72 -0
  66. package/src/components/analytics-chart/types.ts +220 -0
  67. package/src/components/badge.tsx +108 -0
  68. package/src/components/button.tsx +104 -0
  69. package/src/components/card.tsx +263 -0
  70. package/src/components/chart-card.tsx +101 -0
  71. package/src/components/chart-container.tsx +155 -0
  72. package/src/components/chart-legend.tsx +65 -0
  73. package/src/components/chart-theme.tsx +52 -0
  74. package/src/components/chart-tooltip.tsx +165 -0
  75. package/src/components/cursor-blast-effect.tsx +334 -0
  76. package/src/components/data-grid/data-grid-sizing.ts +51 -0
  77. package/src/components/data-grid/data-grid-toolbar.tsx +373 -0
  78. package/src/components/data-grid/data-grid.test.tsx +407 -0
  79. package/src/components/data-grid/data-grid.tsx +1231 -0
  80. package/src/components/data-grid/index.ts +64 -0
  81. package/src/components/data-grid/state.ts +235 -0
  82. package/src/components/data-grid/strings.ts +45 -0
  83. package/src/components/data-grid/types.ts +401 -0
  84. package/src/components/data-grid/use-data-source.ts +413 -0
  85. package/src/components/data-grid/use-url-state.test.tsx +88 -0
  86. package/src/components/data-grid/use-url-state.ts +298 -0
  87. package/src/components/dialog.tsx +218 -0
  88. package/src/components/edit-mode.tsx +17 -0
  89. package/src/components/empty-state.tsx +64 -0
  90. package/src/components/input.tsx +97 -0
  91. package/src/components/metric-card.tsx +142 -0
  92. package/src/components/pill-toggle.tsx +159 -0
  93. package/src/components/progress-bar.tsx +82 -0
  94. package/src/components/separator.tsx +36 -0
  95. package/src/components/skeleton.tsx +30 -0
  96. package/src/components/table.tsx +113 -0
  97. package/src/components/tabs.tsx +217 -0
  98. package/src/index.ts +71 -0
@@ -0,0 +1,298 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { DEFAULT_COL_WIDTH, clampColumnWidth } from "./data-grid-sizing";
5
+ import { createDefaultDataGridState } from "./state";
6
+ import type {
7
+ DataGridColumnDef,
8
+ DataGridSortItem,
9
+ DataGridSortModel,
10
+ DataGridState,
11
+ } from "./types";
12
+
13
+ // ─── URL <-> state encoding ──────────────────────────────────────────
14
+ // Compact, human-readable formats so URLs stay short. Each piece of
15
+ // state gets its own param so unrelated changes don't churn shared keys:
16
+ //
17
+ // ?{prefix}_w=name:200,email:300 column widths (only non-defaults)
18
+ // ?{prefix}_h=createdAt,role hidden column ids
19
+ // ?{prefix}_s=signedUpAt:desc,name:asc multi-column sort
20
+ // ?{prefix}_q=alice quick-search text
21
+ //
22
+ // Column ids are URL-encoded so ids containing `,` `:` or other reserved
23
+ // characters round-trip safely. Without encoding, an id like "user:name"
24
+ // would silently break the parser.
25
+
26
+ // ── widths ─────────────────────────────────────────────────────────
27
+ function serializeWidths(
28
+ widths: Record<string, number>,
29
+ columns: readonly DataGridColumnDef<any>[],
30
+ ): string {
31
+ const parts: string[] = [];
32
+ for (const col of columns) {
33
+ const w = widths[col.id];
34
+ if (typeof w !== "number" || !Number.isFinite(w)) continue;
35
+ const defaultW = clampColumnWidth(col, col.width ?? DEFAULT_COL_WIDTH);
36
+ if (Math.round(w) === Math.round(defaultW)) continue;
37
+ parts.push(`${encodeURIComponent(col.id)}:${Math.round(w)}`);
38
+ }
39
+ return parts.join(",");
40
+ }
41
+
42
+ function parseWidths(
43
+ raw: string | null,
44
+ fallback: Record<string, number>,
45
+ columns: readonly DataGridColumnDef<any>[],
46
+ ): Record<string, number> {
47
+ if (!raw) return fallback;
48
+ const colMap = new Map(columns.map((c) => [c.id, c]));
49
+ const out: Record<string, number> = { ...fallback };
50
+ for (const part of raw.split(",")) {
51
+ // Only split on the FIRST colon — id-side is always pre-encoded so it
52
+ // can't contain a literal `:`, but width-side is always numeric.
53
+ const colonIdx = part.indexOf(":");
54
+ if (colonIdx <= 0) continue;
55
+ const encodedId = part.slice(0, colonIdx);
56
+ const num = part.slice(colonIdx + 1);
57
+ let id: string;
58
+ try {
59
+ id = decodeURIComponent(encodedId);
60
+ } catch {
61
+ continue;
62
+ }
63
+ if (!id || !num) continue;
64
+ const col = colMap.get(id);
65
+ if (!col) continue;
66
+ const n = Number(num);
67
+ if (!Number.isFinite(n)) continue;
68
+ out[id] = clampColumnWidth(col, n);
69
+ }
70
+ return out;
71
+ }
72
+
73
+ // ── hidden columns ─────────────────────────────────────────────────
74
+ function serializeHidden(visibility: Record<string, boolean>): string {
75
+ return Object.entries(visibility)
76
+ .filter(([, v]) => v === false)
77
+ .map(([id]) => encodeURIComponent(id))
78
+ .join(",");
79
+ }
80
+
81
+ function parseHidden(
82
+ raw: string | null,
83
+ columns: readonly DataGridColumnDef<any>[],
84
+ ): Record<string, boolean> {
85
+ if (!raw) return {};
86
+ const known = new Set(columns.map((c) => c.id));
87
+ const out: Record<string, boolean> = {};
88
+ for (const encodedId of raw.split(",")) {
89
+ let id: string;
90
+ try {
91
+ id = decodeURIComponent(encodedId);
92
+ } catch {
93
+ continue;
94
+ }
95
+ if (id && known.has(id)) out[id] = false;
96
+ }
97
+ return out;
98
+ }
99
+
100
+ // ── sort model ─────────────────────────────────────────────────────
101
+ function serializeSort(sort: DataGridSortModel): string {
102
+ return sort
103
+ .map((s) => `${encodeURIComponent(s.columnId)}:${s.direction === "desc" ? "desc" : "asc"}`)
104
+ .join(",");
105
+ }
106
+
107
+ function sortEqual(a: DataGridSortModel, b: DataGridSortModel): boolean {
108
+ if (a.length !== b.length) return false;
109
+ for (let i = 0; i < a.length; i++) {
110
+ if (a[i].columnId !== b[i].columnId || a[i].direction !== b[i].direction) return false;
111
+ }
112
+ return true;
113
+ }
114
+
115
+ function parseSort(
116
+ raw: string | null,
117
+ fallback: DataGridSortModel,
118
+ columns: readonly DataGridColumnDef<any>[],
119
+ ): DataGridSortModel {
120
+ if (raw == null) return fallback;
121
+ if (raw === "") return [];
122
+ const known = new Set(columns.map((c) => c.id));
123
+ const out: DataGridSortItem[] = [];
124
+ for (const part of raw.split(",")) {
125
+ const colonIdx = part.indexOf(":");
126
+ if (colonIdx <= 0) continue;
127
+ const encodedId = part.slice(0, colonIdx);
128
+ const dir = part.slice(colonIdx + 1);
129
+ if (dir !== "asc" && dir !== "desc") continue;
130
+ let id: string;
131
+ try {
132
+ id = decodeURIComponent(encodedId);
133
+ } catch {
134
+ continue;
135
+ }
136
+ if (!id || !known.has(id)) continue;
137
+ out.push({ columnId: id, direction: dir });
138
+ }
139
+ return out;
140
+ }
141
+
142
+ // ── quick search ───────────────────────────────────────────────────
143
+ function parseQuickSearch(raw: string | null, fallback: string): string {
144
+ if (raw == null) return fallback;
145
+ return raw;
146
+ }
147
+
148
+ // ─── Hook ────────────────────────────────────────────────────────────
149
+
150
+ type UrlStateOptions = {
151
+ /** Disambiguates URL params when multiple grids share a page. Defaults
152
+ * to `"grid"`. Use unique prefixes per-grid (e.g. `"users"`, `"teams"`). */
153
+ paramPrefix?: string,
154
+ /** Overrides for default state used when the URL has no value for a
155
+ * given key. Useful for things like a sensible initial sort (e.g.
156
+ * "newest signups first") that should appear on first load but be
157
+ * overridden when the user navigates to a bookmarked URL. */
158
+ initial?: Partial<Pick<DataGridState, "sorting" | "quickSearch" | "columnVisibility">>,
159
+ };
160
+
161
+ /**
162
+ * Drop-in replacement for `useState(() => createDefaultDataGridState(columns))`
163
+ * that persists user view preferences to URL search params, so a view
164
+ * can be bookmarked / shared / restored on reload.
165
+ *
166
+ * **Persisted:** column widths, hidden columns, sort model, quick-search.
167
+ * **Not persisted** (deliberately): pagination scroll position, selection,
168
+ * column pinning/order, date display mode — these are session-scoped.
169
+ *
170
+ * ```tsx
171
+ * const [gridState, setGridState] = useDataGridUrlState(columns, {
172
+ * paramPrefix: "users",
173
+ * initial: { sorting: [{ columnId: "signedUpAt", direction: "desc" }] },
174
+ * });
175
+ * ```
176
+ *
177
+ * URL encoding: `?{prefix}_w=...&{prefix}_h=...&{prefix}_s=...&{prefix}_q=...`.
178
+ * Default values are omitted so URLs stay clean. Updates use
179
+ * `history.replaceState` (not pushState) so back/forward isn't polluted,
180
+ * and `popstate` is observed so external URL changes flow back into state.
181
+ */
182
+ export function useDataGridUrlState<TRow>(
183
+ columns: readonly DataGridColumnDef<TRow>[],
184
+ opts?: UrlStateOptions,
185
+ ): [DataGridState, React.Dispatch<React.SetStateAction<DataGridState>>] {
186
+ const prefix = opts?.paramPrefix ?? "grid";
187
+ const widthsKey = `${prefix}_w`;
188
+ const hiddenKey = `${prefix}_h`;
189
+ const sortKey = `${prefix}_s`;
190
+ const searchKey = `${prefix}_q`;
191
+
192
+ const columnsRef = useRef(columns);
193
+ columnsRef.current = columns;
194
+
195
+ // `initial` snapshots are captured once on first render. Re-running them
196
+ // each render would clobber the user's interactions.
197
+ const initialRef = useRef(opts?.initial);
198
+
199
+ const [state, setState] = useState<DataGridState>(() => {
200
+ const base = createDefaultDataGridState(columns);
201
+ const initial = initialRef.current ?? {};
202
+ const baseWithInitial: DataGridState = {
203
+ ...base,
204
+ sorting: initial.sorting ?? base.sorting,
205
+ quickSearch: initial.quickSearch ?? base.quickSearch,
206
+ columnVisibility: initial.columnVisibility ?? base.columnVisibility,
207
+ };
208
+ if (typeof window === "undefined") return baseWithInitial;
209
+ const params = new URLSearchParams(window.location.search);
210
+ return {
211
+ ...baseWithInitial,
212
+ columnWidths: parseWidths(params.get(widthsKey), base.columnWidths, columns),
213
+ columnVisibility: params.get(hiddenKey) != null
214
+ ? parseHidden(params.get(hiddenKey), columns)
215
+ : baseWithInitial.columnVisibility,
216
+ sorting: parseSort(params.get(sortKey), baseWithInitial.sorting, columns),
217
+ quickSearch: parseQuickSearch(params.get(searchKey), baseWithInitial.quickSearch),
218
+ };
219
+ });
220
+
221
+ // Sync state -> URL. Debounced so that high-frequency state changes
222
+ // (e.g. dragging a column resize handle, typing in the search box)
223
+ // don't fire a URL write per pixel / per keystroke.
224
+ useEffect(() => {
225
+ if (typeof window === "undefined") return;
226
+ const timer = setTimeout(() => {
227
+ const params = new URLSearchParams(window.location.search);
228
+ const before = params.toString();
229
+ const cols = columnsRef.current;
230
+ const initial = initialRef.current ?? {};
231
+
232
+ const widthsStr = serializeWidths(state.columnWidths, cols);
233
+ if (widthsStr) params.set(widthsKey, widthsStr);
234
+ else params.delete(widthsKey);
235
+
236
+ const hiddenStr = serializeHidden(state.columnVisibility);
237
+ // If the consumer supplied initial visibility, "no hidden cols" must
238
+ // be encoded explicitly (empty string) so a bookmark with no `_h=`
239
+ // doesn't silently re-hide a column the user just un-hid.
240
+ const initialHidden = initial.columnVisibility
241
+ ? serializeHidden(initial.columnVisibility)
242
+ : "";
243
+ if (hiddenStr) params.set(hiddenKey, hiddenStr);
244
+ else if (initialHidden) params.set(hiddenKey, "");
245
+ else params.delete(hiddenKey);
246
+
247
+ const initialSort = initial.sorting ?? [];
248
+ if (sortEqual(state.sorting, initialSort)) params.delete(sortKey);
249
+ else params.set(sortKey, serializeSort(state.sorting));
250
+
251
+ const initialSearch = initial.quickSearch ?? "";
252
+ if (state.quickSearch === initialSearch) params.delete(searchKey);
253
+ else params.set(searchKey, state.quickSearch);
254
+
255
+ const after = params.toString();
256
+ if (before === after) return;
257
+ const url = `${window.location.pathname}${after ? `?${after}` : ""}${window.location.hash}`;
258
+ window.history.replaceState(window.history.state, "", url);
259
+ }, 100);
260
+ return () => clearTimeout(timer);
261
+ }, [
262
+ state.columnWidths,
263
+ state.columnVisibility,
264
+ state.sorting,
265
+ state.quickSearch,
266
+ widthsKey,
267
+ hiddenKey,
268
+ sortKey,
269
+ searchKey,
270
+ ]);
271
+
272
+ // React to back/forward navigation.
273
+ useEffect(() => {
274
+ if (typeof window === "undefined") return;
275
+ const onPop = () => {
276
+ const params = new URLSearchParams(window.location.search);
277
+ const cols = columnsRef.current;
278
+ const initial = initialRef.current ?? {};
279
+ // When the URL no longer has a value for a key, reset to defaults
280
+ // rather than preserving the previous in-memory state — otherwise
281
+ // navigating back to a clean URL leaves stale state.
282
+ const defaults = createDefaultDataGridState(cols);
283
+ setState((prev) => ({
284
+ ...prev,
285
+ columnWidths: parseWidths(params.get(widthsKey), defaults.columnWidths, cols),
286
+ columnVisibility: params.get(hiddenKey) != null
287
+ ? parseHidden(params.get(hiddenKey), cols)
288
+ : (initial.columnVisibility ?? defaults.columnVisibility),
289
+ sorting: parseSort(params.get(sortKey), initial.sorting ?? defaults.sorting, cols),
290
+ quickSearch: parseQuickSearch(params.get(searchKey), initial.quickSearch ?? defaults.quickSearch),
291
+ }));
292
+ };
293
+ window.addEventListener("popstate", onPop);
294
+ return () => window.removeEventListener("popstate", onPop);
295
+ }, [widthsKey, hiddenKey, sortKey, searchKey]);
296
+
297
+ return [state, setState];
298
+ }
@@ -0,0 +1,218 @@
1
+ "use client";
2
+
3
+ import {
4
+ Dialog,
5
+ DialogBody,
6
+ DialogClose,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ DialogTrigger,
13
+ cn,
14
+ } from "@hexclave/ui";
15
+ import React from "react";
16
+
17
+ export type DesignDialogSize =
18
+ | "sm"
19
+ | "md"
20
+ | "lg"
21
+ | "xl"
22
+ | "2xl"
23
+ | "3xl"
24
+ | "4xl"
25
+ | "5xl"
26
+ | "6xl"
27
+ | "7xl"
28
+ | "full";
29
+
30
+ export type DesignDialogVariant = "glassmorphic" | "plain";
31
+
32
+ const dialogSizeClasses = new Map<DesignDialogSize, string>([
33
+ ["sm", "max-w-sm"],
34
+ ["md", "max-w-md"],
35
+ ["lg", "max-w-lg"],
36
+ ["xl", "max-w-xl"],
37
+ ["2xl", "max-w-2xl"],
38
+ ["3xl", "max-w-3xl"],
39
+ ["4xl", "max-w-4xl"],
40
+ ["5xl", "max-w-5xl"],
41
+ ["6xl", "max-w-6xl"],
42
+ ["7xl", "max-w-7xl"],
43
+ ["full", "max-w-[calc(100vw-2rem)]"],
44
+ ]);
45
+
46
+ const dialogSurfaceClasses = new Map<DesignDialogVariant, string>([
47
+ ["glassmorphic", "border-0 sm:rounded-2xl bg-white/95 backdrop-blur-2xl ring-1 ring-black/[0.08] shadow-[0_24px_48px_-12px_rgba(0,0,0,0.25),0_4px_24px_-8px_rgba(0,0,0,0.12)] dark:bg-background/80 dark:ring-white/[0.06]"],
48
+ ["plain", "border border-black/[0.08] bg-white shadow-lg sm:rounded-lg dark:border-white/[0.06] dark:bg-background"],
49
+ ]);
50
+
51
+ export const designDialogGlassmorphicSurfaceClass = dialogSurfaceClasses.get("glassmorphic") ?? "";
52
+ export const designDialogGlassmorphicOverlayClass = "bg-black/50 backdrop-blur-sm";
53
+
54
+ type DesignDialogIcon = React.ElementType<{ className?: string }>;
55
+
56
+ type DesignDialogRootProps = Omit<React.ComponentProps<typeof Dialog>, "children">;
57
+
58
+ export type DesignDialogProps = {
59
+ trigger?: React.ReactElement,
60
+ size?: DesignDialogSize,
61
+ variant?: DesignDialogVariant,
62
+ icon?: DesignDialogIcon | null,
63
+ title?: React.ReactNode,
64
+ description?: React.ReactNode,
65
+ headerContent?: React.ReactNode,
66
+ customHeader?: React.ReactNode,
67
+ footer?: React.ReactNode,
68
+ noBodyPadding?: boolean,
69
+ hideTopCloseButton?: boolean,
70
+ className?: string,
71
+ overlayClassName?: string,
72
+ headerClassName?: string,
73
+ bodyClassName?: string,
74
+ footerClassName?: string,
75
+ children?: React.ReactNode,
76
+ contentProps?: Omit<
77
+ React.ComponentPropsWithoutRef<typeof DialogContent>,
78
+ "className" | "children" | "overlayProps" | "noCloseButton"
79
+ >,
80
+ } & DesignDialogRootProps;
81
+
82
+ /**
83
+ * Canonical dashboard modal surface. This wraps the base dialog primitives with
84
+ * a reusable glassmorphic shell and consistent header/body/footer regions.
85
+ */
86
+ export function DesignDialog({
87
+ trigger,
88
+ size = "lg",
89
+ variant = "glassmorphic",
90
+ icon: Icon = null,
91
+ title,
92
+ description,
93
+ headerContent,
94
+ customHeader,
95
+ footer,
96
+ noBodyPadding = false,
97
+ hideTopCloseButton = false,
98
+ className,
99
+ overlayClassName,
100
+ headerClassName,
101
+ bodyClassName,
102
+ footerClassName,
103
+ contentProps,
104
+ children,
105
+ ...dialogRootProps
106
+ }: DesignDialogProps) {
107
+ const resolvedSizeClass = dialogSizeClasses.get(size) ?? "max-w-lg";
108
+ const resolvedSurfaceClass = dialogSurfaceClasses.get(variant) ?? dialogSurfaceClasses.get("glassmorphic");
109
+ const resolvedOverlayClass = cn(
110
+ variant === "glassmorphic" ? designDialogGlassmorphicOverlayClass : undefined,
111
+ overlayClassName,
112
+ );
113
+ const shouldRenderTopHeaderRow = Icon != null || title != null || description != null;
114
+ const shouldRenderHeader = customHeader != null || shouldRenderTopHeaderRow || headerContent != null;
115
+ // Use toArray + filter(Boolean) instead of Children.count so that
116
+ // expressions like `{condition && <Foo/>}` resolving to `false` don't
117
+ // produce an empty DialogBody (which would still render padding/borders).
118
+ const shouldRenderBody = React.Children.toArray(children).filter(Boolean).length > 0;
119
+ const hasStandardTitle = title != null;
120
+ const needsAccessibleTitleFallback = !hasStandardTitle && customHeader == null;
121
+
122
+ if (process.env.NODE_ENV !== "production" && needsAccessibleTitleFallback) {
123
+ console.warn(
124
+ "[DesignDialog] Rendered without a `title` or `customHeader`. Every dialog needs an accessible name — pass `title`, or render a `DialogTitle` inside `customHeader`.",
125
+ );
126
+ }
127
+
128
+ return (
129
+ <Dialog {...dialogRootProps}>
130
+ {trigger != null && (
131
+ <DialogTrigger asChild>
132
+ {trigger}
133
+ </DialogTrigger>
134
+ )}
135
+
136
+ <DialogContent
137
+ className={cn(
138
+ "gap-0 h-auto max-h-[min(100dvh-2rem,36rem)] overflow-hidden p-0",
139
+ resolvedSizeClass,
140
+ resolvedSurfaceClass,
141
+ className
142
+ )}
143
+ overlayProps={resolvedOverlayClass ? { className: resolvedOverlayClass } : undefined}
144
+ noCloseButton={hideTopCloseButton}
145
+ {...contentProps}
146
+ >
147
+ {needsAccessibleTitleFallback && (
148
+ <DialogTitle className="sr-only">Dialog</DialogTitle>
149
+ )}
150
+ {shouldRenderHeader && (
151
+ <DialogHeader className={cn("shrink-0 px-6 pt-6 pb-4 border-b border-foreground/[0.06]", headerClassName)}>
152
+ {customHeader ?? (
153
+ <>
154
+ {shouldRenderTopHeaderRow && (
155
+ <div className={cn(
156
+ "flex gap-3",
157
+ description != null ? "items-start" : "items-center",
158
+ Icon == null && "gap-0",
159
+ )}>
160
+ {Icon != null && (
161
+ <div className="h-9 w-9 rounded-xl bg-primary/10 ring-1 ring-primary/15 flex items-center justify-center shrink-0">
162
+ <Icon className="h-4 w-4 text-primary" />
163
+ </div>
164
+ )}
165
+ {(title != null || description != null) && (
166
+ <div className="flex-1 min-w-0 space-y-1">
167
+ {title != null ? (
168
+ <DialogTitle className="text-base font-semibold leading-snug">
169
+ {title}
170
+ </DialogTitle>
171
+ ) : null}
172
+ {description != null ? (
173
+ <DialogDescription className="text-xs">
174
+ {description}
175
+ </DialogDescription>
176
+ ) : null}
177
+ </div>
178
+ )}
179
+ </div>
180
+ )}
181
+
182
+ {headerContent != null ? (
183
+ <div className={cn(shouldRenderTopHeaderRow && "mt-4")}>
184
+ {headerContent}
185
+ </div>
186
+ ) : null}
187
+ </>
188
+ )}
189
+ </DialogHeader>
190
+ )}
191
+
192
+ {shouldRenderBody && (
193
+ <DialogBody
194
+ className={cn(
195
+ "mx-0 my-0 w-auto min-h-0 overflow-y-auto",
196
+ noBodyPadding ? "px-0 py-0" : "px-6 py-4",
197
+ bodyClassName
198
+ )}
199
+ >
200
+ {children}
201
+ </DialogBody>
202
+ )}
203
+
204
+ {footer != null ? (
205
+ <DialogFooter className={cn("shrink-0 px-6 py-3 border-t border-foreground/[0.06] bg-foreground/[0.02]", footerClassName)}>
206
+ {footer}
207
+ </DialogFooter>
208
+ ) : null}
209
+ </DialogContent>
210
+ </Dialog>
211
+ );
212
+ }
213
+
214
+ export const DesignDialogRoot = Dialog;
215
+ export const DesignDialogTrigger = DialogTrigger;
216
+ export const DesignDialogClose = DialogClose;
217
+ export const DesignDialogTitle = DialogTitle;
218
+ export const DesignDialogDescription = DialogDescription;
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+
5
+ const DesignEditModeContext = createContext(false);
6
+
7
+ export function DesignEditMode({ children }: { children: React.ReactNode }) {
8
+ return (
9
+ <DesignEditModeContext.Provider value={true}>
10
+ {children}
11
+ </DesignEditModeContext.Provider>
12
+ );
13
+ }
14
+
15
+ export function useDesignEditMode(): boolean {
16
+ return useContext(DesignEditModeContext);
17
+ }
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ import { cn } from "@hexclave/ui";
4
+ import React from "react";
5
+
6
+ export type DesignEmptyStateProps = {
7
+ icon?: React.ElementType,
8
+ title?: string,
9
+ description?: string,
10
+ children?: React.ReactNode,
11
+ className?: string,
12
+ };
13
+
14
+ /**
15
+ * Centered "no data" placeholder. Show this inside a `DataGrid` via the
16
+ * `emptyState` prop, inside a chart when a query returns zero rows, or
17
+ * inside a card when a section has nothing to display.
18
+ *
19
+ * ```tsx
20
+ * <DesignEmptyState
21
+ * icon={SearchIcon}
22
+ * title="No results"
23
+ * description="Try adjusting your filters."
24
+ * />
25
+ * ```
26
+ *
27
+ * Prefer this over a raw "No data" div — it handles spacing, typography,
28
+ * and the optional icon for you. `icon` is a component type, not a rendered node.
29
+ */
30
+ export function DesignEmptyState({
31
+ icon: Icon,
32
+ title = "No data available",
33
+ description,
34
+ children,
35
+ className,
36
+ }: DesignEmptyStateProps) {
37
+ return (
38
+ <div
39
+ className={cn(
40
+ "flex flex-col items-center justify-center py-12 px-6 text-center",
41
+ className
42
+ )}
43
+ >
44
+ {Icon && (
45
+ <div className="mb-4">
46
+ <Icon className="h-10 w-10 text-muted-foreground/30" />
47
+ </div>
48
+ )}
49
+ <h3 className="text-sm font-medium text-foreground">
50
+ {title}
51
+ </h3>
52
+ {description && (
53
+ <p className="mt-1 text-xs text-muted-foreground max-w-[280px]">
54
+ {description}
55
+ </p>
56
+ )}
57
+ {children && (
58
+ <div className="mt-4">
59
+ {children}
60
+ </div>
61
+ )}
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import { forwardRefIfNeeded } from "@hexclave/shared/dist/utils/react";
4
+ import React from "react";
5
+
6
+ import { cn } from "@hexclave/ui";
7
+
8
+ export type DesignInputProps = {
9
+ prefixItem?: React.ReactNode,
10
+ leadingIcon?: React.ReactNode,
11
+ size?: "sm" | "md" | "lg",
12
+ } & Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">;
13
+
14
+ export const DesignInput = forwardRefIfNeeded<HTMLInputElement, DesignInputProps>(
15
+ ({ className, type, prefixItem, leadingIcon, size = "md", ...props }, ref) => {
16
+ const heightTextClasses = size === "sm"
17
+ ? "h-7 text-xs"
18
+ : size === "lg"
19
+ ? "h-10 text-sm"
20
+ : "h-9 text-sm";
21
+ const horizontalPaddingClasses = size === "sm" ? "px-2" : size === "lg" ? "px-4" : "px-3";
22
+ const baseClasses = cn(
23
+ "stack-scope flex w-full rounded-xl border border-black/[0.08] dark:border-white/[0.06] bg-white/80 dark:bg-foreground/[0.03] shadow-sm ring-1 ring-black/[0.08] dark:ring-white/[0.06]",
24
+ "file:border-0 file:bg-transparent file:text-sm file:font-medium",
25
+ "placeholder:text-muted-foreground/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/[0.1]",
26
+ "disabled:cursor-not-allowed disabled:opacity-50",
27
+ "transition-all duration-150 hover:transition-none hover:bg-white dark:hover:bg-foreground/[0.06]",
28
+ heightTextClasses
29
+ );
30
+
31
+ const leadingIconClasses = size === "sm"
32
+ ? "left-2.5 [&_svg]:size-3"
33
+ : size === "lg"
34
+ ? "left-3.5 [&_svg]:size-4"
35
+ : "left-3 [&_svg]:size-3.5";
36
+ const leadingIconPadding = size === "sm" ? "pl-8" : size === "lg" ? "pl-10" : "pl-9";
37
+
38
+ if (prefixItem) {
39
+ return (
40
+ <div className="flex w-full flex-row items-center overflow-hidden rounded-xl border border-black/[0.08] bg-white/80 shadow-sm ring-1 ring-black/[0.08] transition-all duration-150 hover:bg-white hover:transition-none dark:border-white/[0.06] dark:bg-foreground/[0.03] dark:ring-white/[0.06] focus-within:ring-1 focus-within:ring-foreground/[0.1] dark:hover:bg-foreground/[0.06]">
41
+ <div className={cn(
42
+ "flex self-stretch items-center justify-center select-none text-muted-foreground/70 border-r border-black/[0.06] dark:border-white/[0.06] bg-black/[0.03] dark:bg-white/[0.02]",
43
+ size === "sm" ? "px-2.5 text-xs" : size === "lg" ? "px-3.5 text-sm" : "px-3 text-sm"
44
+ )}>
45
+ {prefixItem}
46
+ </div>
47
+ <input
48
+ type={type}
49
+ className={cn(
50
+ "stack-scope flex w-full bg-transparent",
51
+ "file:border-0 file:bg-transparent file:text-sm file:font-medium",
52
+ "placeholder:text-muted-foreground/50 focus-visible:outline-none",
53
+ "disabled:cursor-not-allowed disabled:opacity-50",
54
+ heightTextClasses,
55
+ horizontalPaddingClasses,
56
+ "rounded-none border-0 shadow-none ring-0 focus-visible:ring-0",
57
+ className
58
+ )}
59
+ ref={ref}
60
+ {...props}
61
+ />
62
+ </div>
63
+ );
64
+ }
65
+
66
+ if (leadingIcon) {
67
+ return (
68
+ <div className={cn("relative w-full", className)}>
69
+ <div className={cn(
70
+ "pointer-events-none absolute top-1/2 -translate-y-1/2 text-muted-foreground [&_svg]:block",
71
+ leadingIconClasses,
72
+ )}>
73
+ {leadingIcon}
74
+ </div>
75
+ <input
76
+ type={type}
77
+ className={cn(baseClasses, leadingIconPadding)}
78
+ ref={ref}
79
+ {...props}
80
+ />
81
+ </div>
82
+ );
83
+ }
84
+
85
+ return (
86
+ <div className="w-full">
87
+ <input
88
+ type={type}
89
+ className={cn(baseClasses, horizontalPaddingClasses, className)}
90
+ ref={ref}
91
+ {...props}
92
+ />
93
+ </div>
94
+ );
95
+ }
96
+ );
97
+ DesignInput.displayName = "DesignInput";