@hilum/ui 3.8.6 → 3.10.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.
Files changed (90) hide show
  1. package/README.md +32 -1
  2. package/dist/{chunk-RI6QJR2L.js → chunk-R576DV2A.js} +120 -14
  3. package/dist/chunk-R576DV2A.js.map +1 -0
  4. package/dist/create-theme.js +2 -1
  5. package/dist/create-theme.js.map +1 -1
  6. package/dist/index.d.ts +733 -68
  7. package/dist/index.js +3097 -1619
  8. package/dist/index.js.map +1 -1
  9. package/dist/tokens.css +163 -19
  10. package/dist/tokens.d.ts +69 -5
  11. package/dist/tokens.js +1 -1
  12. package/llms.txt +11 -4
  13. package/package.json +11 -10
  14. package/src/components/accordion.tsx +2 -2
  15. package/src/components/account-menu.tsx +1 -1
  16. package/src/components/alert-dialog.tsx +107 -0
  17. package/src/components/ask-user-questions.tsx +24 -18
  18. package/src/components/badge.tsx +36 -3
  19. package/src/components/breadcrumb.tsx +4 -2
  20. package/src/components/button-group.tsx +1 -1
  21. package/src/components/button.tsx +31 -23
  22. package/src/components/calendar.tsx +1 -1
  23. package/src/components/card.tsx +4 -2
  24. package/src/components/checkbox-card.tsx +1 -1
  25. package/src/components/checkbox-group.tsx +2 -1
  26. package/src/components/code-block.tsx +99 -0
  27. package/src/components/color-input.tsx +9 -6
  28. package/src/components/color-picker.tsx +38 -26
  29. package/src/components/combobox.tsx +59 -12
  30. package/src/components/command-palette.tsx +150 -42
  31. package/src/components/command.tsx +5 -3
  32. package/src/components/context-menu.tsx +18 -15
  33. package/src/components/contextual-save-bar.tsx +156 -0
  34. package/src/components/data-table.tsx +34 -5
  35. package/src/components/date-picker.tsx +275 -28
  36. package/src/components/date-text.tsx +90 -0
  37. package/src/components/dropdown-menu.tsx +11 -8
  38. package/src/components/dropdown.tsx +6 -3
  39. package/src/components/empty-state.tsx +85 -20
  40. package/src/components/file-dropzone.tsx +2 -1
  41. package/src/components/file-thumbnail.tsx +84 -20
  42. package/src/components/filter-bar.tsx +91 -0
  43. package/src/components/grid-list.tsx +2 -2
  44. package/src/components/help-tooltip.tsx +1 -1
  45. package/src/components/input-copy.tsx +1 -1
  46. package/src/components/input-group.tsx +1 -1
  47. package/src/components/input-message.tsx +5 -4
  48. package/src/components/input-number.tsx +103 -11
  49. package/src/components/input.tsx +5 -3
  50. package/src/components/label.tsx +1 -0
  51. package/src/components/media-asset-card.tsx +1 -1
  52. package/src/components/menu-item.tsx +37 -3
  53. package/src/components/menubar.tsx +21 -18
  54. package/src/components/native-select.tsx +1 -1
  55. package/src/components/navigation-menu.tsx +2 -2
  56. package/src/components/notification.tsx +1 -1
  57. package/src/components/pagination.tsx +97 -3
  58. package/src/components/popover.tsx +2 -0
  59. package/src/components/property-row.tsx +7 -4
  60. package/src/components/radio-card.tsx +1 -1
  61. package/src/components/radio-group.tsx +2 -1
  62. package/src/components/rating.tsx +136 -0
  63. package/src/components/resizable.tsx +1 -1
  64. package/src/components/resource-item.tsx +133 -0
  65. package/src/components/rich-text-editor.tsx +1 -0
  66. package/src/components/search-input.tsx +98 -0
  67. package/src/components/searchable-table.tsx +10 -11
  68. package/src/components/select.tsx +28 -9
  69. package/src/components/sidebar.tsx +4 -4
  70. package/src/components/slider.tsx +78 -36
  71. package/src/components/sonner.tsx +18 -4
  72. package/src/components/stat-card.tsx +121 -37
  73. package/src/components/status-badge.tsx +142 -47
  74. package/src/components/status-tile.tsx +9 -0
  75. package/src/components/switch.tsx +6 -5
  76. package/src/components/tabs-subtle.tsx +10 -2
  77. package/src/components/tabs.tsx +23 -3
  78. package/src/components/textarea.tsx +2 -3
  79. package/src/components/thinking-steps.tsx +3 -3
  80. package/src/components/toggle.tsx +14 -13
  81. package/src/components/tooltip.tsx +11 -8
  82. package/src/components/url-redirect-prompt.tsx +0 -1
  83. package/src/index.ts +15 -0
  84. package/src/lib/density-context.tsx +65 -0
  85. package/src/lib/format.tsx +301 -0
  86. package/src/lib/interaction.ts +12 -4
  87. package/src/lib/scroll-fade.tsx +39 -0
  88. package/src/tokens/tokens.test.ts +77 -0
  89. package/src/tokens/tokens.ts +114 -5
  90. package/dist/chunk-RI6QJR2L.js.map +0 -1
@@ -125,6 +125,7 @@ const CommandList = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDi
125
125
  };
126
126
 
127
127
  return (
128
+ // eslint-disable-next-line jsx-a11y/interactive-supports-focus -- focus lives on the options (roving tabindex); the listbox only delegates Arrow keys
128
129
  <div
129
130
  ref={combinedRef}
130
131
  role="listbox"
@@ -197,6 +198,7 @@ const CommandItem = React.forwardRef<HTMLDivElement, CommandItemProps>(
197
198
  };
198
199
 
199
200
  return (
201
+ // eslint-disable-next-line jsx-a11y/interactive-supports-focus -- enabled options get tabIndex=0; disabled options are intentionally unfocusable
200
202
  <div
201
203
  ref={ref}
202
204
  role="option"
@@ -205,10 +207,10 @@ const CommandItem = React.forwardRef<HTMLDivElement, CommandItemProps>(
205
207
  data-cmd-item
206
208
  tabIndex={disabled ? undefined : 0}
207
209
  className={cn(
208
- "relative mx-1 flex min-h-10 cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2",
210
+ "relative mx-1 flex min-h-10 cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2 compact:min-h-7 compact:px-2 compact:py-1 compact:text-[13px] compact:rounded-[4px]",
209
211
  "body text-muted-foreground outline-none transition-colors",
210
212
  "hover:bg-muted hover:text-foreground",
211
- "focus:bg-muted focus:text-foreground",
213
+ "focus:bg-active focus:text-foreground",
212
214
  disabled && "pointer-events-none opacity-40",
213
215
  className,
214
216
  )}
@@ -230,7 +232,7 @@ CommandItem.displayName = "CommandItem";
230
232
 
231
233
  const CommandSeparator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
232
234
  ({ className, ...props }, ref) => (
233
- <div ref={ref} className={cn("my-1 h-px bg-muted", className)} {...props} />
235
+ <div ref={ref} className={cn("my-1 h-px bg-border", className)} {...props} />
234
236
  ),
235
237
  );
236
238
  CommandSeparator.displayName = "CommandSeparator";
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import { ContextMenu } from "radix-ui";
5
5
  import { Check, ChevronRight, Circle } from "lucide-react";
6
6
  import { cn } from "../lib/utils";
7
+ import { useDensityAttributes } from "../lib/density-context";
7
8
  import {
8
9
  mobilePopperSheetMotionClassName,
9
10
  mobilePopperSheetPositionClassName,
@@ -26,6 +27,7 @@ const ContextMenuContent = React.forwardRef<
26
27
  <style>{mobilePopperSheetStyle}</style>
27
28
  <ContextMenu.Portal>
28
29
  <ContextMenu.Content
30
+ {...useDensityAttributes()}
29
31
  ref={ref}
30
32
  data-hilum-mobile-sheet="true"
31
33
  className={cn(
@@ -58,13 +60,13 @@ const ContextMenuItem = React.forwardRef<
58
60
  <ContextMenu.Item
59
61
  ref={ref}
60
62
  className={cn(
61
- "relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2",
63
+ "relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2 compact:min-h-7 compact:px-2 compact:py-1 compact:text-[13px] compact:rounded-[4px]",
62
64
  "body outline-none transition-colors",
63
65
  destructive
64
66
  ? "text-destructive focus:bg-destructive/10 focus:text-destructive"
65
- : "text-muted-foreground focus:bg-muted focus:text-foreground",
67
+ : "text-foreground focus:bg-active",
66
68
  "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
67
- inset && "pl-8",
69
+ inset && "pl-8 compact:pl-7",
68
70
  className,
69
71
  )}
70
72
  {...props}
@@ -79,9 +81,9 @@ const ContextMenuCheckboxItem = React.forwardRef<
79
81
  <ContextMenu.CheckboxItem
80
82
  ref={ref}
81
83
  className={cn(
82
- "relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md py-2 pl-8 pr-2.5",
83
- "body text-muted-foreground outline-none transition-colors",
84
- "focus:bg-muted focus:text-foreground",
84
+ "relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md py-2 pl-8 pr-2.5 compact:min-h-7 compact:py-1 compact:pl-7 compact:pr-2 compact:text-[13px] compact:rounded-[4px]",
85
+ "body text-foreground outline-none transition-colors",
86
+ "focus:bg-active focus:text-foreground",
85
87
  "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
86
88
  className,
87
89
  )}
@@ -104,9 +106,9 @@ const ContextMenuRadioItem = React.forwardRef<
104
106
  <ContextMenu.RadioItem
105
107
  ref={ref}
106
108
  className={cn(
107
- "relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md py-2 pl-8 pr-2.5",
108
- "body text-muted-foreground outline-none transition-colors",
109
- "focus:bg-muted focus:text-foreground",
109
+ "relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md py-2 pl-8 pr-2.5 compact:min-h-7 compact:py-1 compact:pl-7 compact:pr-2 compact:text-[13px] compact:rounded-[4px]",
110
+ "body text-foreground outline-none transition-colors",
111
+ "focus:bg-active focus:text-foreground",
110
112
  "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
111
113
  className,
112
114
  )}
@@ -130,7 +132,7 @@ const ContextMenuLabel = React.forwardRef<
130
132
  >(({ className, inset, ...props }, ref) => (
131
133
  <ContextMenu.Label
132
134
  ref={ref}
133
- className={cn("px-2.5 py-1 label text-muted-foreground", inset && "pl-8", className)}
135
+ className={cn("px-2.5 py-1 label text-muted-foreground compact:px-2 compact:text-[11px]", inset && "pl-8 compact:pl-7", className)}
134
136
  {...props}
135
137
  />
136
138
  ));
@@ -142,7 +144,7 @@ const ContextMenuSeparator = React.forwardRef<
142
144
  >(({ className, ...props }, ref) => (
143
145
  <ContextMenu.Separator
144
146
  ref={ref}
145
- className={cn("mx-1 my-1 h-px bg-muted", className)}
147
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
146
148
  {...props}
147
149
  />
148
150
  ));
@@ -157,10 +159,10 @@ const ContextMenuSubTrigger = React.forwardRef<
157
159
  <ContextMenu.SubTrigger
158
160
  ref={ref}
159
161
  className={cn(
160
- "relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2",
161
- "body text-muted-foreground outline-none transition-colors",
162
- "focus:bg-muted focus:text-foreground data-[state=open]:bg-muted",
163
- inset && "pl-8",
162
+ "relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2 compact:min-h-7 compact:px-2 compact:py-1 compact:text-[13px] compact:rounded-[4px]",
163
+ "body text-foreground outline-none transition-colors",
164
+ "focus:bg-active focus:text-foreground data-[state=open]:bg-active",
165
+ inset && "pl-8 compact:pl-7",
164
166
  className,
165
167
  )}
166
168
  {...props}
@@ -178,6 +180,7 @@ const ContextMenuSubContent = React.forwardRef<
178
180
  <>
179
181
  <style>{mobilePopperSheetStyle}</style>
180
182
  <ContextMenu.SubContent
183
+ {...useDensityAttributes()}
181
184
  ref={ref}
182
185
  data-hilum-mobile-sheet="true"
183
186
  className={cn(
@@ -0,0 +1,156 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
5
+ import { CircleAlert } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+ import { spring } from "../lib/springs";
8
+ import { Button } from "./button";
9
+
10
+ interface ContextualSaveBarProps {
11
+ /** Show the bar — usually `form.isDirty`. */
12
+ open: boolean;
13
+ /** Message on the leading edge. Default: "Unsaved changes". */
14
+ message?: React.ReactNode;
15
+ /** Primary action. */
16
+ onSave?: () => void;
17
+ /** Secondary action — resets the form to its saved state. */
18
+ onDiscard?: () => void;
19
+ saveLabel?: React.ReactNode;
20
+ discardLabel?: React.ReactNode;
21
+ /** Shows a spinner on Save and disables both actions. */
22
+ saving?: boolean;
23
+ /** Disable Save (e.g. while the form is invalid). */
24
+ saveDisabled?: boolean;
25
+ /**
26
+ * Wire the Save button to a `<form id>` instead of `onSave` — the button
27
+ * becomes `type="submit" form={formId}` so native validation still runs.
28
+ */
29
+ formId?: string;
30
+ /**
31
+ * `fixed` pins the bar to the viewport (default: top on desktop,
32
+ * bottom on mobile for thumb reach). `sticky` keeps it inside its scroll
33
+ * container — useful inside a settings pane or a dialog.
34
+ */
35
+ position?: "fixed" | "sticky";
36
+ /** Extra content between the message and the actions (e.g. a "Preview" link). */
37
+ children?: React.ReactNode;
38
+ className?: string;
39
+ }
40
+
41
+ /**
42
+ * Contextual save bar — the "you have unsaved changes" strip that replaces
43
+ * per-card Save buttons on edit screens (Shopify's contextual save bar).
44
+ * Announces itself politely to assistive tech and binds ⌘S / Ctrl+S to Save
45
+ * while open.
46
+ */
47
+ function ContextualSaveBar({
48
+ open,
49
+ message = "Unsaved changes",
50
+ onSave,
51
+ onDiscard,
52
+ saveLabel = "Save",
53
+ discardLabel = "Discard",
54
+ saving = false,
55
+ saveDisabled = false,
56
+ formId,
57
+ position = "fixed",
58
+ children,
59
+ className,
60
+ }: ContextualSaveBarProps) {
61
+ const reduceMotion = useReducedMotion();
62
+ const saveRef = React.useRef<HTMLButtonElement>(null);
63
+
64
+ React.useEffect(() => {
65
+ if (!open) return;
66
+ const onKeyDown = (event: KeyboardEvent) => {
67
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") {
68
+ event.preventDefault();
69
+ if (!saving && !saveDisabled) saveRef.current?.click();
70
+ }
71
+ };
72
+ window.addEventListener("keydown", onKeyDown);
73
+ return () => window.removeEventListener("keydown", onKeyDown);
74
+ }, [open, saving, saveDisabled]);
75
+
76
+ return (
77
+ <AnimatePresence initial={false}>
78
+ {open && (
79
+ <motion.div
80
+ key="contextual-save-bar"
81
+ role="region"
82
+ aria-label="Unsaved changes"
83
+ data-slot="contextual-save-bar"
84
+ data-position={position}
85
+ initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: -8 }}
86
+ animate={{ opacity: 1, y: 0 }}
87
+ exit={reduceMotion ? { opacity: 0 } : { opacity: 0, y: -8 }}
88
+ transition={spring.fast}
89
+ className={cn(
90
+ "z-(--z-sticky) flex min-w-0 items-center gap-3 border-border bg-foreground px-4 py-2.5 text-background",
91
+ position === "fixed"
92
+ ? "fixed inset-x-0 bottom-0 border-t pb-[max(0.625rem,env(safe-area-inset-bottom))] sm:top-0 sm:bottom-auto sm:border-t-0 sm:border-b sm:pb-2.5"
93
+ : "sticky top-0 rounded-xl shadow-elevated",
94
+ className,
95
+ )}
96
+ >
97
+ <p
98
+ className="body-sm flex min-w-0 flex-1 items-center gap-2 font-medium"
99
+ aria-live="polite"
100
+ >
101
+ <CircleAlert className="size-4 shrink-0 opacity-70" aria-hidden="true" />
102
+ <span className="truncate">{message}</span>
103
+ </p>
104
+ {children}
105
+ <div className="flex shrink-0 items-center gap-2">
106
+ {onDiscard && (
107
+ <Button
108
+ type="button"
109
+ size="sm"
110
+ variant="ghost"
111
+ className="text-background/80 hover:bg-background/10 hover:text-background"
112
+ onClick={onDiscard}
113
+ disabled={saving}
114
+ >
115
+ {discardLabel}
116
+ </Button>
117
+ )}
118
+ <Button
119
+ ref={saveRef}
120
+ size="sm"
121
+ variant="brand"
122
+ {...(formId ? { type: "submit" as const, form: formId } : { type: "button" as const })}
123
+ {...(onSave ? { onClick: onSave } : {})}
124
+ loading={saving}
125
+ disabled={saveDisabled}
126
+ >
127
+ {saveLabel}
128
+ </Button>
129
+ </div>
130
+ </motion.div>
131
+ )}
132
+ </AnimatePresence>
133
+ );
134
+ }
135
+
136
+ ContextualSaveBar.displayName = "ContextualSaveBar";
137
+
138
+ /**
139
+ * Warn before the tab closes / reloads while `dirty` is true. Pair with
140
+ * ContextualSaveBar; route-level blocking belongs to your router.
141
+ */
142
+ function useUnsavedChangesWarning(dirty: boolean) {
143
+ React.useEffect(() => {
144
+ if (!dirty) return;
145
+ const onBeforeUnload = (event: BeforeUnloadEvent) => {
146
+ event.preventDefault();
147
+ // Legacy browsers require returnValue to be set.
148
+ event.returnValue = "";
149
+ };
150
+ window.addEventListener("beforeunload", onBeforeUnload);
151
+ return () => window.removeEventListener("beforeunload", onBeforeUnload);
152
+ }, [dirty]);
153
+ }
154
+
155
+ export { ContextualSaveBar, useUnsavedChangesWarning };
156
+ export type { ContextualSaveBarProps };
@@ -17,6 +17,7 @@ import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from ".
17
17
  import { Button } from "./button";
18
18
  import { Input } from "./input";
19
19
  import { cn } from "../lib/utils";
20
+ import { pluralize, useFormatter } from "../lib/format";
20
21
 
21
22
  interface DataTableProps<TData> {
22
23
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -26,6 +27,12 @@ interface DataTableProps<TData> {
26
27
  searchPlaceholder?: string;
27
28
  pageSize?: number;
28
29
  showPagination?: boolean;
30
+ /** Rendered in the table body when there are no rows (e.g. an `<EmptyState>` with an action). */
31
+ emptyState?: React.ReactNode;
32
+ /** Make rows clickable (e.g. open the detail page). Enter/Space activate focused rows. */
33
+ onRowClick?: (row: TData) => void;
34
+ /** Noun for the result count — "12 orders". Default: "result". */
35
+ itemLabel?: string;
29
36
  }
30
37
 
31
38
  function DataTable<TData>({
@@ -35,7 +42,11 @@ function DataTable<TData>({
35
42
  searchPlaceholder = "Search...",
36
43
  pageSize = 10,
37
44
  showPagination = true,
45
+ emptyState,
46
+ onRowClick,
47
+ itemLabel = "result",
38
48
  }: DataTableProps<TData>) {
49
+ const fmt = useFormatter();
39
50
  const [sorting, setSorting] = React.useState<SortingState>([]);
40
51
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
41
52
 
@@ -107,7 +118,23 @@ function DataTable<TData>({
107
118
  <TableBody>
108
119
  {table.getRowModel().rows?.length ? (
109
120
  table.getRowModel().rows.map((row) => (
110
- <TableRow key={row.id}>
121
+ <TableRow
122
+ key={row.id}
123
+ {...(onRowClick
124
+ ? {
125
+ tabIndex: 0,
126
+ className:
127
+ "cursor-pointer focus-visible:outline-none focus-visible:bg-muted hover:bg-muted",
128
+ onClick: () => onRowClick(row.original),
129
+ onKeyDown: (event: React.KeyboardEvent<HTMLTableRowElement>) => {
130
+ if (event.key === "Enter" || event.key === " ") {
131
+ event.preventDefault();
132
+ onRowClick(row.original);
133
+ }
134
+ },
135
+ }
136
+ : {})}
137
+ >
111
138
  {row.getVisibleCells().map((cell) => (
112
139
  <TableCell key={cell.id}>
113
140
  {flexRender(cell.column.columnDef.cell, cell.getContext())}
@@ -119,9 +146,9 @@ function DataTable<TData>({
119
146
  <TableRow>
120
147
  <TableCell
121
148
  colSpan={columns.length}
122
- className="h-24 text-center text-muted-foreground"
149
+ className={cn(!emptyState && "h-24 text-center text-muted-foreground")}
123
150
  >
124
- No results.
151
+ {emptyState ?? "No results."}
125
152
  </TableCell>
126
153
  </TableRow>
127
154
  )}
@@ -130,8 +157,9 @@ function DataTable<TData>({
130
157
  {showPagination && (
131
158
  <div className="flex items-center justify-between">
132
159
  <p className="caption tabular-nums text-muted-foreground">
133
- {table.getFilteredRowModel().rows.length} result
134
- {table.getFilteredRowModel().rows.length !== 1 ? "s" : ""}
160
+ {pluralize(table.getFilteredRowModel().rows.length, itemLabel, {
161
+ ...(fmt.locale ? { locale: fmt.locale } : {}),
162
+ })}
135
163
  </p>
136
164
  <div className="flex items-center gap-2">
137
165
  <span className="caption tabular-nums text-muted-foreground">
@@ -161,5 +189,6 @@ function DataTable<TData>({
161
189
  }
162
190
 
163
191
  export { DataTable };
192
+ export type { DataTableProps };
164
193
  export { createColumnHelper } from "@tanstack/react-table";
165
194
  export type { ColumnDef };