@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/dist/index.d.ts +34 -10
- package/dist/index.js +192 -48
- package/dist/index.js.map +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/src/components/card.tsx +16 -3
- package/src/components/chart.tsx +2 -1
- package/src/components/color-input.tsx +14 -3
- package/src/components/input.tsx +38 -2
- package/src/components/select.tsx +38 -1
- package/src/components/sidebar.tsx +17 -0
- package/src/components/table.tsx +62 -17
- package/src/components/textarea.tsx +35 -2
- package/src/components/titled-card.tsx +25 -5
package/llms.txt
CHANGED
package/package.json
CHANGED
package/src/components/card.tsx
CHANGED
|
@@ -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 };
|
package/src/components/chart.tsx
CHANGED
|
@@ -100,7 +100,8 @@ function ChartContainer({
|
|
|
100
100
|
data-slot="chart"
|
|
101
101
|
data-chart={chartId}
|
|
102
102
|
className={cn(
|
|
103
|
-
"flex
|
|
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 {
|
|
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
|
-
<
|
|
65
|
+
<ColorPickerPopover
|
|
57
66
|
value={value}
|
|
58
67
|
onChange={onChange}
|
|
59
68
|
{...(presets !== undefined && { presets })}
|
|
60
69
|
{...(disabled !== undefined && { disabled })}
|
|
61
|
-
|
|
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"
|
package/src/components/input.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
(
|
|
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
|
)}
|
package/src/components/table.tsx
CHANGED
|
@@ -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
|
-
(
|
|
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(
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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(
|
|
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>
|