@hilum/ui 3.5.4 → 3.6.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hilum/ui",
3
- "version": "3.5.4",
3
+ "version": "3.6.1",
4
4
  "private": false,
5
5
  "description": "Hilum UI — design system primitives (Button, Input, Dialog, Combobox, …) + tokens + fonts + icons",
6
6
  "license": "MIT",
@@ -210,7 +210,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
210
210
  className="transition-[stroke-width] duration-80 group-hover:stroke-[2]"
211
211
  />
212
212
  )}
213
- <span>{children}</span>
213
+ {LeadingIcon || TrailingIcon ? <span>{children}</span> : children}
214
214
  {TrailingIcon && (
215
215
  <TrailingIcon
216
216
  size={iconSize}
@@ -3,6 +3,7 @@
3
3
  import * as React from "react";
4
4
  import { cn } from "../lib/utils";
5
5
  import { motionClasses } from "../lib/interaction";
6
+ import { useShape } from "../lib/shape-context";
6
7
  import { ColorPicker } from "./color-picker";
7
8
 
8
9
  interface ColorInputProps {
@@ -29,6 +30,7 @@ function ColorInput({
29
30
  disabled,
30
31
  presets,
31
32
  }: ColorInputProps) {
33
+ const shape = useShape();
32
34
  const [hex, setHex] = React.useState(value);
33
35
 
34
36
  React.useEffect(() => {
@@ -43,8 +45,9 @@ function ColorInput({
43
45
  return (
44
46
  <div
45
47
  className={cn(
46
- "inline-flex h-8 items-stretch gap-0 rounded-md border border-border bg-card overflow-hidden",
47
- "shadow-natural focus-within:ring-2 focus-within:ring-brand-primary/20 focus-within:border-brand-primary",
48
+ "inline-flex h-8 items-stretch gap-0 overflow-hidden border border-border bg-background",
49
+ shape.input,
50
+ "focus-within:border-brand-primary focus-within:ring-2 focus-within:ring-brand-primary/20",
48
51
  motionClasses,
49
52
  disabled && "opacity-50 pointer-events-none",
50
53
  className,
@@ -3,6 +3,7 @@
3
3
  import * as React from "react";
4
4
  import { Check, ChevronsUpDown } from "lucide-react";
5
5
  import { cn } from "../lib/utils";
6
+ import { useShape } from "../lib/shape-context";
6
7
 
7
8
  export interface ComboboxOption {
8
9
  value: string;
@@ -31,6 +32,7 @@ function Combobox({
31
32
  emptyText = "No results found.",
32
33
  className,
33
34
  }: ComboboxProps) {
35
+ const shape = useShape();
34
36
  const [open, setOpen] = React.useState(false);
35
37
  const [query, setQuery] = React.useState("");
36
38
  const [activeIndex, setActiveIndex] = React.useState(-1);
@@ -132,9 +134,10 @@ function Combobox({
132
134
  }
133
135
  aria-autocomplete="list"
134
136
  className={cn(
135
- "flex h-10 w-full rounded-lg border border-border bg-card pr-10 body text-foreground",
137
+ "flex h-10 w-full border border-border bg-background pr-10 body text-foreground",
138
+ shape.input,
136
139
  "placeholder:text-muted-foreground",
137
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30 focus-visible:border-border",
140
+ "focus-visible:border-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30",
138
141
  "disabled:cursor-not-allowed disabled:opacity-50",
139
142
  selectedOption?.avatar && !open
140
143
  ? "pl-8"
@@ -17,6 +17,11 @@ import { spring } from "../lib/springs";
17
17
  import { useShape } from "../lib/shape-context";
18
18
  import { SurfaceProvider, useSurface } from "../lib/surface-context";
19
19
  import { surfaceClasses } from "../lib/surface-classes";
20
+ import {
21
+ desktopDialogContentClassName,
22
+ dialogSheetMotionClassName,
23
+ mobileDialogSheetContentClassName,
24
+ } from "../lib/mobile-popper-sheet";
20
25
  import { Button } from "./button";
21
26
 
22
27
  const DIALOG_OFFSET = 4;
@@ -75,6 +80,15 @@ const DialogContent = forwardRef<HTMLDivElement, DialogContentProps>(
75
80
 
76
81
  if (!mounted) return null;
77
82
 
83
+ const sizeClassName =
84
+ size === "sm"
85
+ ? container
86
+ ? "max-w-[400px]"
87
+ : "sm:max-w-[400px]"
88
+ : container
89
+ ? "max-w-[540px]"
90
+ : "sm:max-w-[540px]";
91
+
78
92
  return (
79
93
  <DialogPrimitive.Portal forceMount container={container ?? undefined}>
80
94
  <DialogPrimitive.Overlay asChild forceMount>
@@ -91,24 +105,31 @@ const DialogContent = forwardRef<HTMLDivElement, DialogContentProps>(
91
105
  <DialogPrimitive.Content ref={ref} asChild forceMount {...props}>
92
106
  <motion.div
93
107
  className={cn(
94
- container ? "absolute" : "fixed",
95
- "left-1/2 top-1/2 z-50 w-[calc(100%-2rem)]",
108
+ container
109
+ ? "absolute left-1/2 top-1/2 z-50 w-[calc(100%-2rem)]"
110
+ : [
111
+ mobileDialogSheetContentClassName,
112
+ desktopDialogContentClassName,
113
+ dialogSheetMotionClassName,
114
+ ],
96
115
  surfaceClasses(dialogLevel),
97
116
  "p-6 focus:outline-none",
98
- "rounded-t-2xl max-sm:pb-[calc(1.5rem+env(safe-area-inset-bottom))] data-[state=open]:slide-in-from-bottom sm:data-[state=open]:zoom-in-95",
99
- size === "sm" && "max-w-[400px]",
100
- size === "lg" && "max-w-[540px]",
117
+ sizeClassName,
101
118
  shape.container,
102
119
  "rounded-t-2xl",
103
120
  className,
104
121
  )}
105
- initial={{ opacity: 0, scale: 0.97, x: "-50%", y: "-50%" }}
106
- animate={{
107
- opacity: open ? 1 : 0,
108
- scale: open ? 1 : 0.97,
109
- x: "-50%",
110
- y: "-50%",
111
- }}
122
+ initial={container ? { opacity: 0, scale: 0.97, x: "-50%", y: "-50%" } : { opacity: 0 }}
123
+ animate={
124
+ container
125
+ ? {
126
+ opacity: open ? 1 : 0,
127
+ scale: open ? 1 : 0.97,
128
+ x: "-50%",
129
+ y: "-50%",
130
+ }
131
+ : { opacity: open ? 1 : 0 }
132
+ }
112
133
  transition={open ? spring.slow : spring.slow.exit}
113
134
  onAnimationComplete={handleExitComplete}
114
135
  >
@@ -78,6 +78,7 @@ const InputGroup = forwardRef<HTMLDivElement, InputGroupProps>(
78
78
  ) => {
79
79
  const itemsRef = useRef(new Map<number, HTMLLabelElement>());
80
80
  const [activeIndex, setActiveIndex] = useState<number | null>(null);
81
+ const shape = useShape();
81
82
 
82
83
  const registerItem = useCallback((index: number, element: HTMLLabelElement | null) => {
83
84
  if (element) {
@@ -128,7 +129,8 @@ const InputGroup = forwardRef<HTMLDivElement, InputGroupProps>(
128
129
  <div
129
130
  ref={ref}
130
131
  className={cn(
131
- "relative flex items-center gap-2 rounded-[var(--radius-input)] border bg-background px-3 py-2 text-[13px] transition-colors",
132
+ "relative flex items-center gap-2 border bg-background px-3 py-2 text-[13px] transition-colors",
133
+ shape.input,
132
134
  error ? "border-destructive/50" : "border-border",
133
135
  disabled && "opacity-50",
134
136
  pill && "rounded-full",
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import { cn } from "../lib/utils";
5
+ import { useShape } from "../lib/shape-context";
5
6
 
6
7
  interface InputNumberProps extends Omit<
7
8
  React.ComponentProps<"input">,
@@ -50,6 +51,8 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>(
50
51
  },
51
52
  ref,
52
53
  ) => {
54
+ const shape = useShape();
55
+
53
56
  const formatValue = React.useCallback(
54
57
  (next: number | null) => (next === null ? mixedLabel : next.toFixed(precision)),
55
58
  [mixedLabel, precision],
@@ -63,13 +66,16 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>(
63
66
 
64
67
  const clamp = React.useCallback((n: number) => Math.min(max, Math.max(min, n)), [min, max]);
65
68
 
66
- const parseAndClamp = React.useCallback((raw: string) => {
67
- const n = parseFloat(raw);
68
- if (Number.isFinite(n)) {
69
- return clamp(n);
70
- }
71
- return null;
72
- }, [clamp]);
69
+ const parseAndClamp = React.useCallback(
70
+ (raw: string) => {
71
+ const n = parseFloat(raw);
72
+ if (Number.isFinite(n)) {
73
+ return clamp(n);
74
+ }
75
+ return null;
76
+ },
77
+ [clamp],
78
+ );
73
79
 
74
80
  const commit = (raw: string) => {
75
81
  const next = parseAndClamp(raw);
@@ -105,8 +111,9 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>(
105
111
  return (
106
112
  <div
107
113
  className={cn(
108
- "inline-flex h-8 items-stretch rounded-md border border-border bg-card overflow-hidden",
109
- "focus-within:ring-2 focus-within:ring-brand-primary/20 focus-within:border-brand-primary",
114
+ "inline-flex h-8 items-stretch overflow-hidden border border-border bg-background",
115
+ shape.input,
116
+ "focus-within:border-brand-primary focus-within:ring-2 focus-within:ring-brand-primary/20",
110
117
  disabled && "opacity-50 pointer-events-none",
111
118
  className,
112
119
  )}
@@ -1,14 +1,20 @@
1
+ "use client";
2
+
1
3
  import * as React from "react";
2
4
  import { cn } from "../lib/utils";
3
5
  import { controlSurfaceClasses, focusRingClasses, motionClasses } from "../lib/interaction";
6
+ import { useShape } from "../lib/shape-context";
4
7
 
5
8
  function Input({ className, type, ...props }: React.ComponentProps<"input">) {
9
+ const shape = useShape();
10
+
6
11
  return (
7
12
  <input
8
13
  type={type}
9
14
  data-slot="input"
10
15
  className={cn(
11
- "flex h-10 w-full rounded-md px-3 py-1",
16
+ "flex h-10 w-full px-3 py-1",
17
+ shape.input,
12
18
  "body text-foreground placeholder:text-muted-foreground",
13
19
  controlSurfaceClasses,
14
20
  motionClasses,
@@ -1,32 +1,40 @@
1
+ "use client";
2
+
1
3
  import * as React from "react";
2
4
  import { ChevronDown } from "lucide-react";
3
5
  import { cn } from "../lib/utils";
6
+ import { useShape } from "../lib/shape-context";
4
7
 
5
8
  const NativeSelect = React.forwardRef<
6
9
  HTMLSelectElement,
7
10
  React.SelectHTMLAttributes<HTMLSelectElement>
8
- >(({ className, children, ...props }, ref) => (
9
- <div className="relative">
10
- <select
11
- ref={ref}
12
- className={cn(
13
- "peer h-10 w-full appearance-none rounded-lg border border-border bg-card pl-3 pr-8",
14
- "body text-foreground",
15
- "focus:outline-none focus:ring-2 focus:ring-ring/30 focus:border-border",
16
- "disabled:cursor-not-allowed disabled:opacity-50",
17
- "aria-[invalid=true]:border-destructive aria-[invalid=true]:focus:ring-destructive/20",
18
- className,
19
- )}
20
- {...props}
21
- >
22
- {children}
23
- </select>
24
- <ChevronDown
25
- size={14}
26
- className="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground peer-disabled:opacity-50"
27
- />
28
- </div>
29
- ));
11
+ >(({ className, children, ...props }, ref) => {
12
+ const shape = useShape();
13
+
14
+ return (
15
+ <div className="relative">
16
+ <select
17
+ ref={ref}
18
+ className={cn(
19
+ "peer h-10 w-full appearance-none border border-border bg-background pl-3 pr-8",
20
+ shape.input,
21
+ "body text-foreground",
22
+ "focus:border-border focus:outline-none focus:ring-2 focus:ring-ring/30",
23
+ "disabled:cursor-not-allowed disabled:opacity-50",
24
+ "aria-[invalid=true]:border-destructive aria-[invalid=true]:focus:ring-destructive/20",
25
+ className,
26
+ )}
27
+ {...props}
28
+ >
29
+ {children}
30
+ </select>
31
+ <ChevronDown
32
+ size={14}
33
+ className="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground peer-disabled:opacity-50"
34
+ />
35
+ </div>
36
+ );
37
+ });
30
38
  NativeSelect.displayName = "NativeSelect";
31
39
 
32
40
  const NativeSelectOption = React.forwardRef<
@@ -1,13 +1,19 @@
1
+ "use client";
2
+
1
3
  import * as React from "react";
2
4
  import { cn } from "../lib/utils";
3
5
  import { controlSurfaceClasses, focusRingClasses, motionClasses } from "../lib/interaction";
6
+ import { useShape } from "../lib/shape-context";
4
7
 
5
8
  function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
9
+ const shape = useShape();
10
+
6
11
  return (
7
12
  <textarea
8
13
  data-slot="textarea"
9
14
  className={cn(
10
- "flex min-h-[80px] w-full rounded-md px-3 py-2",
15
+ "flex min-h-[80px] w-full px-3 py-2",
16
+ shape.input,
11
17
  "body text-foreground placeholder:text-muted-foreground",
12
18
  "resize-none",
13
19
  controlSurfaceClasses,
package/src/index.ts CHANGED
@@ -7,6 +7,9 @@
7
7
 
8
8
  // Utilities
9
9
  export { cn } from "./lib/utils";
10
+ export * from "./lib/icon-context";
11
+ export * from "./lib/shape-context";
12
+ export * from "./lib/surface-context";
10
13
 
11
14
  // Components (alphabetical)
12
15
  export * from "./components/account-menu";
@@ -14,8 +14,7 @@ export const focusRingClasses =
14
14
  export const iconStrokeClasses =
15
15
  "[&_svg]:transition-[stroke-width,transform,color] [&_svg]:duration-150 [&_svg]:ease-out group-hover:[&_svg]:stroke-[2]";
16
16
 
17
- export const controlSurfaceClasses =
18
- "border border-border bg-card shadow-natural hover:border-ground-200 hover:shadow-elevated";
17
+ export const controlSurfaceClasses = "border border-border bg-background hover:border-ground-200";
19
18
 
20
19
  export const surfaceElevationClasses = {
21
20
  flat: "border border-border bg-card",
@@ -59,7 +59,7 @@ const ShapeContext = createContext<ShapeContextValue | null>(null);
59
59
 
60
60
  function useShape(): ShapeClasses {
61
61
  const ctx = useContext(ShapeContext);
62
- if (!ctx) return shapeMap.pill;
62
+ if (!ctx) return shapeMap.rounded;
63
63
  return ctx.classes;
64
64
  }
65
65
 
@@ -79,7 +79,7 @@ function transitionShape(callback: () => void) {
79
79
 
80
80
  function ShapeProvider({
81
81
  children,
82
- defaultShape = "pill",
82
+ defaultShape = "rounded",
83
83
  }: {
84
84
  children: ReactNode;
85
85
  defaultShape?: ShapeVariant;