@hilum/ui 3.6.11 → 3.7.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.
package/llms.txt CHANGED
@@ -5,7 +5,7 @@
5
5
  ## Key Facts
6
6
 
7
7
  - Package name: `@hilum/ui`
8
- - Package version: 3.6.11
8
+ - Package version: 3.6.12
9
9
  - Component modules: 104
10
10
  - Atom components: 73
11
11
  - Molecule components: 31
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hilum/ui",
3
- "version": "3.6.11",
3
+ "version": "3.7.0",
4
4
  "private": false,
5
5
  "description": "Hilum UI — design system primitives (Button, Input, Dialog, Combobox, …) + tokens + fonts + icons",
6
6
  "license": "MIT",
@@ -7,10 +7,20 @@ import { motionClasses } from "../lib/interaction";
7
7
  /* Card root */
8
8
  /* ------------------------------------------------------------------ */
9
9
 
10
+ type CardMobileSurface = "default" | "flat" | "flush";
11
+
12
+ const cardMobileSurfaceClasses: Record<CardMobileSurface, string> = {
13
+ default: "",
14
+ flat: "max-sm:rounded-none max-sm:border-x-0 max-sm:border-y max-sm:bg-transparent max-sm:shadow-none",
15
+ flush: "max-sm:rounded-none max-sm:border-0 max-sm:bg-transparent max-sm:shadow-none",
16
+ };
17
+
10
18
  const cardVariants = cva(["rounded-xl", motionClasses], {
11
19
  variants: {
12
20
  variant: {
13
21
  default: "bg-card shadow-natural",
22
+ outlined: "border border-border bg-card shadow-natural",
23
+ elevated: "border border-border bg-card shadow-elevated",
14
24
  muted: "bg-muted shadow-natural",
15
25
  ghost: "bg-transparent",
16
26
  responsive:
@@ -22,14 +32,16 @@ const cardVariants = cva(["rounded-xl", motionClasses], {
22
32
  },
23
33
  });
24
34
 
25
- interface CardProps extends React.ComponentProps<"div">, VariantProps<typeof cardVariants> {}
35
+ interface CardProps extends React.ComponentProps<"div">, VariantProps<typeof cardVariants> {
36
+ mobileSurface?: CardMobileSurface;
37
+ }
26
38
 
27
39
  const Card = React.forwardRef<HTMLDivElement, CardProps>(
28
- ({ className, variant, ...props }, ref) => (
40
+ ({ className, variant, mobileSurface = "default", ...props }, ref) => (
29
41
  <div
30
42
  ref={ref}
31
43
  data-slot="card"
32
- className={cn(cardVariants({ variant }), className)}
44
+ className={cn(cardVariants({ variant }), cardMobileSurfaceClasses[mobileSurface], className)}
33
45
  {...props}
34
46
  />
35
47
  ),
@@ -134,3 +146,4 @@ export {
134
146
  CardMedia,
135
147
  cardVariants,
136
148
  };
149
+ export type { CardMobileSurface, CardProps };
@@ -100,7 +100,8 @@ function ChartContainer({
100
100
  data-slot="chart"
101
101
  data-chart={chartId}
102
102
  className={cn(
103
- "flex aspect-video justify-center text-xs",
103
+ "flex justify-center text-xs",
104
+ height === undefined ? "aspect-video" : "w-full",
104
105
  "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground",
105
106
  "[&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50",
106
107
  "[&_.recharts-curve.recharts-tooltip-cursor]:stroke-border",
@@ -4,7 +4,8 @@ import * as React from "react";
4
4
  import { cn } from "../lib/utils";
5
5
  import { motionClasses } from "../lib/interaction";
6
6
  import { useShape } from "../lib/shape-context";
7
- import { ColorPicker } from "./color-picker";
7
+ import { ColorPickerPopover } from "./color-picker";
8
+ import type { ControlMobileSurface } from "./input";
8
9
 
9
10
  interface ColorInputProps {
10
11
  value: string;
@@ -15,6 +16,7 @@ interface ColorInputProps {
15
16
  className?: string;
16
17
  disabled?: boolean;
17
18
  presets?: string[];
19
+ mobileSurface?: ControlMobileSurface;
18
20
  }
19
21
 
20
22
  /**
@@ -29,6 +31,7 @@ function ColorInput({
29
31
  className,
30
32
  disabled,
31
33
  presets,
34
+ mobileSurface = "default",
32
35
  }: ColorInputProps) {
33
36
  const shape = useShape();
34
37
  const [hex, setHex] = React.useState(value);
@@ -42,6 +45,11 @@ function ColorInput({
42
45
  else setHex(value);
43
46
  };
44
47
 
48
+ const mobileSurfaceClass =
49
+ mobileSurface === "flush"
50
+ ? "max-sm:rounded-none max-sm:border-x-0 max-sm:border-t-0 max-sm:bg-transparent max-sm:px-0 max-sm:shadow-none max-sm:focus-within:ring-0"
51
+ : "";
52
+
45
53
  return (
46
54
  <div
47
55
  className={cn(
@@ -49,16 +57,19 @@ function ColorInput({
49
57
  shape.input,
50
58
  "focus-within:border-brand-primary focus-within:ring-2 focus-within:ring-brand-primary/20",
51
59
  motionClasses,
60
+ mobileSurfaceClass,
52
61
  disabled && "opacity-50 pointer-events-none",
53
62
  className,
54
63
  )}
55
64
  >
56
- <ColorPicker
65
+ <ColorPickerPopover
57
66
  value={value}
58
67
  onChange={onChange}
59
68
  {...(presets !== undefined && { presets })}
60
69
  {...(disabled !== undefined && { disabled })}
61
- className="h-full w-8 border-0 rounded-none focus-visible:ring-0"
70
+ triggerShowValue={false}
71
+ hideEyedropper
72
+ triggerClassName="h-full w-8 border-0 rounded-none bg-transparent px-1 hover:bg-hover focus-visible:ring-0"
62
73
  />
63
74
  <input
64
75
  type="text"
@@ -5,7 +5,39 @@ import { cn } from "../lib/utils";
5
5
  import { controlSurfaceClasses, focusRingClasses, motionClasses } from "../lib/interaction";
6
6
  import { useShape } from "../lib/shape-context";
7
7
 
8
- function Input({ className, type, ...props }: React.ComponentProps<"input">) {
8
+ type ControlDensity = "default" | "compact";
9
+ type ControlMobileSurface = "default" | "flush";
10
+
11
+ interface InputProps extends React.ComponentProps<"input"> {
12
+ density?: ControlDensity;
13
+ mobileDensity?: ControlDensity;
14
+ mobileSurface?: ControlMobileSurface;
15
+ }
16
+
17
+ const inputDensityClasses: Record<ControlDensity, string> = {
18
+ default: "h-10 px-3 py-1",
19
+ compact: "h-8 px-2.5 py-1",
20
+ };
21
+
22
+ const inputMobileDensityClasses: Record<ControlDensity, string> = {
23
+ default: "",
24
+ compact: "max-sm:h-8 max-sm:px-2.5 max-sm:py-1",
25
+ };
26
+
27
+ const controlMobileSurfaceClasses: Record<ControlMobileSurface, string> = {
28
+ default: "",
29
+ flush:
30
+ "max-sm:rounded-none max-sm:border-x-0 max-sm:border-t-0 max-sm:bg-transparent max-sm:px-0 max-sm:shadow-none max-sm:focus-visible:ring-0 max-sm:focus-visible:ring-offset-0",
31
+ };
32
+
33
+ function Input({
34
+ className,
35
+ type,
36
+ density = "default",
37
+ mobileDensity = "default",
38
+ mobileSurface = "default",
39
+ ...props
40
+ }: InputProps) {
9
41
  const shape = useShape();
10
42
 
11
43
  return (
@@ -13,12 +45,15 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
13
45
  type={type}
14
46
  data-slot="input"
15
47
  className={cn(
16
- "flex h-10 w-full px-3 py-1",
48
+ "flex w-full",
49
+ inputDensityClasses[density],
50
+ inputMobileDensityClasses[mobileDensity],
17
51
  shape.input,
18
52
  "body text-foreground placeholder:text-muted-foreground",
19
53
  controlSurfaceClasses,
20
54
  motionClasses,
21
55
  focusRingClasses,
56
+ controlMobileSurfaceClasses[mobileSurface],
22
57
  "focus-visible:border-brand-primary",
23
58
  "disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-muted",
24
59
  "file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground",
@@ -31,3 +66,4 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
31
66
  Input.displayName = "Input";
32
67
 
33
68
  export { Input };
69
+ export type { ControlDensity, ControlMobileSurface, InputProps };
@@ -24,6 +24,7 @@ import { useProximityHover } from "../hooks/use-proximity-hover";
24
24
  import { useShape } from "../lib/shape-context";
25
25
  import { useScrollEdges, ScrollEdgeCue } from "../lib/scroll-fade";
26
26
  import { Elevated } from "../lib/elevated";
27
+ import type { ControlDensity, ControlMobileSurface } from "./input";
27
28
 
28
29
  // ---------------------------------------------------------------------------
29
30
  // Select context
@@ -140,15 +141,48 @@ const triggerVariants = cva(
140
141
  },
141
142
  );
142
143
 
144
+ const selectTriggerDensityClasses: Record<ControlDensity, string> = {
145
+ default: "h-9 px-3",
146
+ compact: "h-8 px-2.5",
147
+ };
148
+
149
+ const selectTriggerMobileDensityClasses: Record<ControlDensity, string> = {
150
+ default: "",
151
+ compact: "max-sm:h-8 max-sm:px-2.5",
152
+ };
153
+
154
+ const selectTriggerMobileSurfaceClasses: Record<ControlMobileSurface, string> = {
155
+ default: "",
156
+ flush:
157
+ "max-sm:rounded-none max-sm:border-x-0 max-sm:border-t-0 max-sm:bg-transparent max-sm:px-0 max-sm:shadow-none max-sm:focus-visible:ring-0 max-sm:focus-visible:ring-offset-0",
158
+ };
159
+
143
160
  interface SelectTriggerProps
144
161
  extends HTMLAttributes<HTMLButtonElement>, VariantProps<typeof triggerVariants> {
145
162
  icon?: IconComponent;
146
163
  placeholder?: string;
147
164
  error?: string;
165
+ density?: ControlDensity;
166
+ mobileDensity?: ControlDensity;
167
+ mobileSurface?: ControlMobileSurface;
148
168
  }
149
169
 
150
170
  const SelectTrigger = forwardRef<HTMLButtonElement, SelectTriggerProps>(
151
- ({ className, variant, icon: Icon, placeholder = "Select…", error, children, ...props }, ref) => {
171
+ (
172
+ {
173
+ className,
174
+ variant,
175
+ icon: Icon,
176
+ placeholder = "Select…",
177
+ error,
178
+ children,
179
+ density = "default",
180
+ mobileDensity = "default",
181
+ mobileSurface = "default",
182
+ ...props
183
+ },
184
+ ref,
185
+ ) => {
152
186
  const { value, open, setOpen, disabled, triggerRef, labelMap } = useSelectContext();
153
187
  const shape = useShape();
154
188
  const label = value ? (labelMap.current.get(value) ?? value) : undefined;
@@ -179,6 +213,9 @@ const SelectTrigger = forwardRef<HTMLButtonElement, SelectTriggerProps>(
179
213
  aria-invalid={!!error || undefined}
180
214
  className={cn(
181
215
  triggerVariants({ variant }),
216
+ selectTriggerDensityClasses[density],
217
+ selectTriggerMobileDensityClasses[mobileDensity],
218
+ selectTriggerMobileSurfaceClasses[mobileSurface],
182
219
  shape.input,
183
220
  error && "border-destructive/50 hover:border-destructive/50",
184
221
  className,
@@ -405,13 +405,16 @@ function SidebarGroupLabel({
405
405
  ...props
406
406
  }: React.ComponentProps<"div"> & { asChild?: boolean }) {
407
407
  const Comp = asChild ? Slot.Root : "div";
408
+ const state = useOptionalSidebar()?.state ?? "expanded";
408
409
 
409
410
  return (
410
411
  <Comp
411
412
  data-sidebar="group-label"
413
+ data-state={state}
412
414
  className={cn(
413
415
  "flex h-8 shrink-0 items-center rounded-md px-2 label text-muted-foreground",
414
416
  "outline-none ring-ring transition-[margin,opacity] duration-200 ease-linear",
417
+ "data-[state=collapsed]:h-0 data-[state=collapsed]:m-0 data-[state=collapsed]:overflow-hidden data-[state=collapsed]:p-0 data-[state=collapsed]:opacity-0",
415
418
  "group-data-[state=collapsed]/sidebar-wrapper:opacity-0",
416
419
  "group-data-[state=collapsed]/sidebar-wrapper:h-0",
417
420
  "group-data-[state=collapsed]/sidebar-wrapper:m-0",
@@ -492,10 +495,12 @@ function SidebarMenuButton({
492
495
  ...props
493
496
  }: SidebarMenuButtonProps) {
494
497
  const Comp = asChild ? (Slot.Root as React.ElementType) : "button";
498
+ const state = useOptionalSidebar()?.state ?? "expanded";
495
499
 
496
500
  const button = (
497
501
  <Comp
498
502
  data-sidebar="menu-button"
503
+ data-state={state}
499
504
  data-active={isActive}
500
505
  data-size={size}
501
506
  className={cn(
@@ -508,6 +513,7 @@ function SidebarMenuButton({
508
513
  "active:bg-muted",
509
514
  "disabled:pointer-events-none disabled:opacity-50",
510
515
  "[&>svg]:size-4 [&>svg]:shrink-0",
516
+ "[&>span]:min-w-0 [&>span]:truncate",
511
517
  // Size variants
512
518
  size === "sm" && "h-7 text-xs",
513
519
  size === "lg" && "h-10",
@@ -517,9 +523,12 @@ function SidebarMenuButton({
517
523
  "hover:bg-brand-primary/15 hover:text-brand-primary",
518
524
  ],
519
525
  // Collapsed state
526
+ "data-[state=collapsed]:size-9 data-[state=collapsed]:justify-center data-[state=collapsed]:p-0",
527
+ "data-[state=collapsed]:[&>span]:hidden",
520
528
  "group-data-[state=collapsed]/sidebar-wrapper:justify-center",
521
529
  "group-data-[state=collapsed]/sidebar-wrapper:size-9",
522
530
  "group-data-[state=collapsed]/sidebar-wrapper:p-0",
531
+ "group-data-[state=collapsed]/sidebar-wrapper:[&>span]:hidden",
523
532
  className,
524
533
  )}
525
534
  {...props}
@@ -590,12 +599,16 @@ function SidebarMenuAction({
590
599
  /* ------------------------------------------------------------------ */
591
600
 
592
601
  function SidebarMenuBadge({ className, ...props }: React.ComponentProps<"div">) {
602
+ const state = useOptionalSidebar()?.state ?? "expanded";
603
+
593
604
  return (
594
605
  <div
595
606
  data-sidebar="menu-badge"
607
+ data-state={state}
596
608
  className={cn(
597
609
  "absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-full",
598
610
  "caption font-medium bg-muted text-muted-foreground px-1",
611
+ "data-[state=collapsed]:hidden",
599
612
  "group-data-[state=collapsed]/sidebar-wrapper:hidden",
600
613
  className,
601
614
  )}
@@ -637,11 +650,15 @@ function SidebarMenuSkeleton({
637
650
  /* ------------------------------------------------------------------ */
638
651
 
639
652
  function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
653
+ const state = useOptionalSidebar()?.state ?? "expanded";
654
+
640
655
  return (
641
656
  <ul
642
657
  data-sidebar="menu-sub"
658
+ data-state={state}
643
659
  className={cn(
644
660
  "mx-3.5 flex min-w-0 flex-col gap-1 border-l border-border pl-3",
661
+ "data-[state=collapsed]:hidden",
645
662
  "group-data-[state=collapsed]/sidebar-wrapper:hidden",
646
663
  className,
647
664
  )}
@@ -22,19 +22,38 @@ import { useProximityHover } from "../hooks/use-proximity-hover";
22
22
  interface TableContextValue {
23
23
  registerItem: (index: number, element: HTMLElement | null) => void;
24
24
  activeIndex: number | null;
25
+ density: TableDensity;
26
+ mobileDensity: TableDensity;
25
27
  }
26
28
 
27
29
  const TableContext = createContext<TableContextValue | null>(null);
28
30
 
29
31
  // ── Table ────────────────────────────────────────────────
30
32
 
33
+ type TableDensity = "default" | "compact";
34
+ type TableMobileSurface = "default" | "flat";
35
+
31
36
  interface TableProps extends HTMLAttributes<HTMLTableElement> {
32
37
  children: ReactNode;
33
38
  containerClassName?: string;
39
+ density?: TableDensity;
40
+ mobileDensity?: TableDensity;
41
+ mobileSurface?: TableMobileSurface;
34
42
  }
35
43
 
36
44
  const Table = forwardRef<HTMLTableElement, TableProps>(
37
- ({ children, className, containerClassName, ...props }, ref) => {
45
+ (
46
+ {
47
+ children,
48
+ className,
49
+ containerClassName,
50
+ density = "default",
51
+ mobileDensity = "default",
52
+ mobileSurface = "default",
53
+ ...props
54
+ },
55
+ ref,
56
+ ) => {
38
57
  const containerRef = useRef<HTMLDivElement>(null);
39
58
 
40
59
  const { activeIndex, itemRects, sessionRef, handlers, registerItem, measureItems } =
@@ -47,10 +66,15 @@ const Table = forwardRef<HTMLTableElement, TableProps>(
47
66
  const activeRect = activeIndex !== null ? itemRects[activeIndex] : null;
48
67
 
49
68
  return (
50
- <TableContext.Provider value={{ registerItem, activeIndex }}>
69
+ <TableContext.Provider value={{ registerItem, activeIndex, density, mobileDensity }}>
51
70
  <div
52
71
  ref={containerRef}
53
- className={cn("relative", containerClassName)}
72
+ className={cn(
73
+ "relative",
74
+ mobileSurface === "flat" &&
75
+ "max-sm:-mx-4 max-sm:overflow-x-auto max-sm:rounded-none max-sm:border-x-0",
76
+ containerClassName,
77
+ )}
54
78
  onMouseEnter={handlers.onMouseEnter}
55
79
  onMouseMove={handlers.onMouseMove}
56
80
  onMouseLeave={handlers.onMouseLeave}
@@ -86,7 +110,7 @@ const Table = forwardRef<HTMLTableElement, TableProps>(
86
110
 
87
111
  <table
88
112
  ref={ref}
89
- className={cn("w-full text-[13px] border-collapse", className)}
113
+ className={cn("w-full border-collapse text-[13px]", className)}
90
114
  {...props}
91
115
  >
92
116
  {children}
@@ -168,9 +192,20 @@ TableRow.displayName = "TableRow";
168
192
  // ── TableHead ────────────────────────────────────────────
169
193
 
170
194
  const TableHead = forwardRef<HTMLTableCellElement, ThHTMLAttributes<HTMLTableCellElement>>(
171
- ({ className, ...props }, ref) => (
172
- <th ref={ref} className={cn("px-3 py-2 text-left text-foreground", className)} {...props} />
173
- ),
195
+ ({ className, ...props }, ref) => {
196
+ const ctx = useContext(TableContext);
197
+ const densityClass = ctx?.density === "compact" ? "px-2 py-1.5" : "px-3 py-2";
198
+ const mobileDensityClass =
199
+ ctx?.mobileDensity === "compact" ? "max-sm:px-2 max-sm:py-1.5" : "";
200
+
201
+ return (
202
+ <th
203
+ ref={ref}
204
+ className={cn(densityClass, mobileDensityClass, "text-left text-foreground", className)}
205
+ {...props}
206
+ />
207
+ );
208
+ },
174
209
  );
175
210
 
176
211
  TableHead.displayName = "TableHead";
@@ -178,16 +213,25 @@ TableHead.displayName = "TableHead";
178
213
  // ── TableCell ────────────────────────────────────────────
179
214
 
180
215
  const TableCell = forwardRef<HTMLTableCellElement, TdHTMLAttributes<HTMLTableCellElement>>(
181
- ({ className, ...props }, ref) => (
182
- <td
183
- ref={ref}
184
- className={cn(
185
- "px-3 py-2 text-muted-foreground transition-colors duration-80 group-[.is-active]/row:text-foreground",
186
- className,
187
- )}
188
- {...props}
189
- />
190
- ),
216
+ ({ className, ...props }, ref) => {
217
+ const ctx = useContext(TableContext);
218
+ const densityClass = ctx?.density === "compact" ? "px-2 py-1.5" : "px-3 py-2";
219
+ const mobileDensityClass =
220
+ ctx?.mobileDensity === "compact" ? "max-sm:px-2 max-sm:py-1.5" : "";
221
+
222
+ return (
223
+ <td
224
+ ref={ref}
225
+ className={cn(
226
+ densityClass,
227
+ mobileDensityClass,
228
+ "text-muted-foreground transition-colors duration-80 group-[.is-active]/row:text-foreground",
229
+ className,
230
+ )}
231
+ {...props}
232
+ />
233
+ );
234
+ },
191
235
  );
192
236
 
193
237
  TableCell.displayName = "TableCell";
@@ -213,3 +257,4 @@ TableCaption.displayName = "TableCaption";
213
257
  // ── Exports ──────────────────────────────────────────────
214
258
 
215
259
  export { Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption };
260
+ export type { TableDensity, TableMobileSurface, TableProps };
@@ -4,21 +4,53 @@ import * as React from "react";
4
4
  import { cn } from "../lib/utils";
5
5
  import { controlSurfaceClasses, focusRingClasses, motionClasses } from "../lib/interaction";
6
6
  import { useShape } from "../lib/shape-context";
7
+ import type { ControlDensity, ControlMobileSurface } from "./input";
7
8
 
8
- function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
9
+ interface TextareaProps extends React.ComponentProps<"textarea"> {
10
+ density?: ControlDensity;
11
+ mobileDensity?: ControlDensity;
12
+ mobileSurface?: ControlMobileSurface;
13
+ }
14
+
15
+ const textareaDensityClasses: Record<ControlDensity, string> = {
16
+ default: "min-h-[80px] px-3 py-2",
17
+ compact: "min-h-16 px-2.5 py-1.5",
18
+ };
19
+
20
+ const textareaMobileDensityClasses: Record<ControlDensity, string> = {
21
+ default: "",
22
+ compact: "max-sm:min-h-16 max-sm:px-2.5 max-sm:py-1.5",
23
+ };
24
+
25
+ const textareaMobileSurfaceClasses: Record<ControlMobileSurface, string> = {
26
+ default: "",
27
+ flush:
28
+ "max-sm:rounded-none max-sm:border-x-0 max-sm:border-t-0 max-sm:bg-transparent max-sm:px-0 max-sm:shadow-none max-sm:focus-visible:ring-0 max-sm:focus-visible:ring-offset-0",
29
+ };
30
+
31
+ function Textarea({
32
+ className,
33
+ density = "default",
34
+ mobileDensity = "default",
35
+ mobileSurface = "default",
36
+ ...props
37
+ }: TextareaProps) {
9
38
  const shape = useShape();
10
39
 
11
40
  return (
12
41
  <textarea
13
42
  data-slot="textarea"
14
43
  className={cn(
15
- "flex min-h-[80px] w-full px-3 py-2",
44
+ "flex w-full",
45
+ textareaDensityClasses[density],
46
+ textareaMobileDensityClasses[mobileDensity],
16
47
  shape.input,
17
48
  "body text-foreground placeholder:text-muted-foreground",
18
49
  "resize-none",
19
50
  controlSurfaceClasses,
20
51
  motionClasses,
21
52
  focusRingClasses,
53
+ textareaMobileSurfaceClasses[mobileSurface],
22
54
  "focus-visible:border-brand-primary",
23
55
  "disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-muted",
24
56
  className,
@@ -29,3 +61,4 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
29
61
  }
30
62
 
31
63
  export { Textarea };
64
+ export type { TextareaProps };
@@ -2,7 +2,14 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import { cn } from "../lib/utils";
5
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./card";
5
+ import {
6
+ Card,
7
+ CardContent,
8
+ CardDescription,
9
+ CardHeader,
10
+ CardTitle,
11
+ type CardMobileSurface,
12
+ } from "./card";
6
13
  import { CardHeading } from "./card-heading";
7
14
 
8
15
  interface TitledCardProps {
@@ -12,6 +19,7 @@ interface TitledCardProps {
12
19
  actionButtons?: React.ReactNode;
13
20
  icon?: React.ComponentType<{ className?: string }>;
14
21
  contentPadding?: "default" | "flush-mobile" | "flush";
22
+ mobileSurface?: CardMobileSurface;
15
23
  className?: string;
16
24
  contentClassName?: string;
17
25
  containerClassName?: string;
@@ -25,6 +33,7 @@ function TitledCard({
25
33
  actionButtons,
26
34
  icon: Icon,
27
35
  contentPadding = "default",
36
+ mobileSurface = "flush",
28
37
  className,
29
38
  contentClassName,
30
39
  containerClassName,
@@ -40,12 +49,13 @@ function TitledCard({
40
49
  : hasHeader
41
50
  ? "p-4 sm:p-5"
42
51
  : "p-0 sm:p-5";
52
+ const isMobileFlat = mobileSurface === "flat" || mobileSurface === "flush";
43
53
 
44
54
  return (
45
55
  <Card
56
+ mobileSurface={mobileSurface}
46
57
  className={cn(
47
58
  "min-w-0 overflow-hidden border border-border bg-card shadow-natural",
48
- "max-sm:rounded-none max-sm:border-0 max-sm:bg-transparent max-sm:shadow-none",
49
59
  containerClassName,
50
60
  className,
51
61
  )}
@@ -55,12 +65,17 @@ function TitledCard({
55
65
  <CardHeading
56
66
  title={title!}
57
67
  {...(subtitle ? { description: subtitle } : {})}
58
- className="max-sm:border-b-0 max-sm:px-0 max-sm:pb-3"
68
+ className={cn(isMobileFlat && "max-sm:border-b-0 max-sm:px-0 max-sm:pb-3")}
59
69
  >
60
70
  {Icon && <Icon className="size-5 shrink-0 text-muted-foreground" />}
61
71
  </CardHeading>
62
72
  ) : hasHeader ? (
63
- <CardHeader className="flex flex-col gap-3 border-b border-border p-4 max-sm:border-b-0 max-sm:px-0 max-sm:pb-3 sm:flex-row sm:items-start sm:justify-between sm:p-5">
73
+ <CardHeader
74
+ className={cn(
75
+ "flex flex-col gap-3 border-b border-border p-4 sm:flex-row sm:items-start sm:justify-between sm:p-5",
76
+ isMobileFlat && "max-sm:border-b-0 max-sm:px-0 max-sm:pb-3",
77
+ )}
78
+ >
64
79
  <div className="min-w-0 flex-1">
65
80
  {title && (
66
81
  <CardTitle
@@ -88,7 +103,12 @@ function TitledCard({
88
103
  ) : null}
89
104
  {children && (
90
105
  <CardContent
91
- className={cn(contentPaddingClassName, "min-w-0 max-sm:px-0", contentClassName)}
106
+ className={cn(
107
+ contentPaddingClassName,
108
+ "min-w-0",
109
+ isMobileFlat && "max-sm:px-0",
110
+ contentClassName,
111
+ )}
92
112
  >
93
113
  {children}
94
114
  </CardContent>