@payglocal_ui/flux-ui 0.3.4 → 0.3.5

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": "@payglocal_ui/flux-ui",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Flux UI primitives — inputs, fields, dialog, data table, charts, calendar, and more (Tailwind v4 + Radix).",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/alert.tsx CHANGED
@@ -3,6 +3,7 @@
3
3
  import * as React from "react";
4
4
  import { X, Info, CheckCircle2, AlertTriangle, XCircle } from "lucide-react";
5
5
  import { cn } from "./utils";
6
+ import { elevation } from "./elevation";
6
7
 
7
8
  export type AlertVariant = "info" | "success" | "warning" | "error" | "neutral";
8
9
 
@@ -35,7 +36,7 @@ const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
35
36
  <div
36
37
  ref={ref}
37
38
  role="alert"
38
- className={cn("relative flex gap-3 rounded-xl border p-4 shadow-sm", variantStyles[variant], className)}
39
+ className={cn("relative flex gap-3 rounded-xl border p-4", elevation.surface, variantStyles[variant], className)}
39
40
  {...props}
40
41
  >
41
42
  <Icon className="mt-0.5 size-4 shrink-0" aria-hidden />
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { cn } from "./utils";
4
+ import { elevation } from "./elevation";
4
5
  import { ChevronDown } from "lucide-react";
5
6
  import {
6
7
  forwardRef,
@@ -144,15 +145,15 @@ export const SplitButton = forwardRef<HTMLDivElement, SplitButtonProps>(
144
145
  "disabled:cursor-not-allowed disabled:opacity-50",
145
146
  // Match variant styling
146
147
  variant === "primary" &&
147
- "bg-primary text-primary-foreground border border-primary shadow-sm hover:bg-[var(--primary-hover)] border-l border-l-primary-foreground/20",
148
+ `bg-primary text-primary-foreground border border-primary ${elevation.control} hover:bg-[var(--primary-hover)] border-l border-l-primary-foreground/20`,
148
149
  variant === "secondary" &&
149
- "bg-muted text-foreground border border-border shadow-sm hover:bg-muted/85 dark:bg-muted/35 dark:hover:bg-muted/55 border-l-0",
150
+ `bg-muted text-foreground border border-border ${elevation.control} hover:bg-muted/85 dark:bg-muted/35 dark:hover:bg-muted/55 border-l-0`,
150
151
  variant === "outline" &&
151
- "bg-card text-foreground border border-border shadow-sm hover:bg-muted border-l-0",
152
+ `bg-card text-foreground border border-border ${elevation.control} hover:bg-muted border-l-0`,
152
153
  variant === "ghost" &&
153
154
  "bg-transparent text-foreground border border-transparent hover:bg-muted border-l-0",
154
155
  variant === "danger" &&
155
- "bg-red-600 text-white border border-red-600 shadow-sm hover:bg-red-700 border-l border-l-white/20",
156
+ `bg-red-600 text-white border border-red-600 ${elevation.control} hover:bg-red-700 border-l border-l-white/20`,
156
157
  // Size heights to match Button
157
158
  size === "sm" && "h-9 min-h-9 rounded-lg",
158
159
  size === "md" && "h-10 min-h-10 rounded-lg",
@@ -174,7 +175,8 @@ export const SplitButton = forwardRef<HTMLDivElement, SplitButtonProps>(
174
175
  role="menu"
175
176
  className={cn(
176
177
  "absolute right-0 top-full mt-1 z-50",
177
- "bg-card border border-border rounded-lg shadow-lg py-1 min-w-32"
178
+ "bg-card border border-border rounded-lg py-1 min-w-32",
179
+ elevation.popover
178
180
  )}
179
181
  >
180
182
  {children}
package/src/button.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { cn } from "./utils";
4
+ import { elevation } from "./elevation";
4
5
  import { Loader2 } from "lucide-react";
5
6
  import { type ButtonHTMLAttributes, forwardRef } from "react";
6
7
 
@@ -14,15 +15,15 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
14
15
 
15
16
  const variantClasses: Record<NonNullable<ButtonProps["variant"]>, string> = {
16
17
  primary:
17
- "bg-primary text-primary-foreground border border-primary shadow-sm hover:bg-[var(--primary-hover)]",
18
+ `bg-primary text-primary-foreground border border-primary ${elevation.control} hover:bg-[var(--primary-hover)]`,
18
19
  secondary:
19
- "bg-muted text-foreground border border-border shadow-sm hover:bg-muted/85 dark:bg-muted/35 dark:text-foreground dark:border-border dark:hover:bg-muted/55",
20
+ `bg-muted text-foreground border border-border ${elevation.control} hover:bg-muted/85 dark:bg-muted/35 dark:text-foreground dark:border-border dark:hover:bg-muted/55`,
20
21
  ghost:
21
22
  "bg-transparent text-foreground border border-transparent hover:bg-muted focus-visible:bg-muted/80 active:bg-muted/90",
22
23
  danger:
23
- "bg-red-600 text-white border border-red-600 shadow-sm hover:bg-red-700",
24
+ `bg-red-600 text-white border border-red-600 ${elevation.control} hover:bg-red-700`,
24
25
  outline:
25
- "bg-card text-foreground border border-border shadow-sm hover:bg-muted",
26
+ `bg-card text-foreground border border-border ${elevation.control} hover:bg-muted`,
26
27
  link:
27
28
  "h-auto min-h-0 rounded-md border border-transparent bg-transparent px-2 py-2 text-[15px] font-medium text-primary shadow-none underline-offset-4 hover:bg-primary/5 hover:underline focus-visible:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35 disabled:pointer-events-none disabled:opacity-50 disabled:hover:bg-transparent disabled:hover:no-underline",
28
29
  };
package/src/calendar.tsx CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  import { Button } from "./button";
13
13
  import type { ButtonProps } from "./button";
14
14
  import { cn } from "./utils";
15
+ import { elevation } from "./elevation";
15
16
 
16
17
  const cellSize = "[--cell-size:2rem]";
17
18
 
@@ -22,7 +23,8 @@ function navButtonClasses(variant: NonNullable<ButtonProps["variant"]>): string
22
23
  case "outline":
23
24
  return cn(
24
25
  base,
25
- "border-border bg-card text-foreground shadow-sm hover:bg-muted"
26
+ elevation.control,
27
+ "border-border bg-card text-foreground hover:bg-muted"
26
28
  );
27
29
  case "ghost":
28
30
  default:
@@ -100,7 +102,8 @@ function Calendar({
100
102
  defaultClassNames.dropdowns
101
103
  ),
102
104
  dropdown_root: cn(
103
- "relative rounded-md border border-border bg-card shadow-sm focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/35",
105
+ "relative rounded-md border border-border bg-card focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/35",
106
+ elevation.field,
104
107
  defaultClassNames.dropdown_root
105
108
  ),
106
109
  dropdown: cn("absolute inset-0 cursor-pointer opacity-0", defaultClassNames.dropdown),
package/src/card.tsx CHANGED
@@ -2,8 +2,11 @@ import * as React from "react";
2
2
  import { cva, type VariantProps } from "class-variance-authority";
3
3
 
4
4
  import { cn } from "./utils";
5
+ import { elevation } from "./elevation";
5
6
 
6
- const cardVariants = cva("flex flex-col rounded-xl border border-border bg-card text-card-foreground shadow-sm", {
7
+ const cardVariants = cva(
8
+ cn("flex flex-col rounded-xl border border-border bg-card text-card-foreground", elevation.surface),
9
+ {
7
10
  variants: {
8
11
  size: {
9
12
  default: "gap-10 px-10 py-10",
@@ -21,6 +21,7 @@ import {
21
21
  Minus,
22
22
  } from "lucide-react";
23
23
  import { cn } from "./utils";
24
+ import { elevation } from "./elevation";
24
25
  import { Button } from "./button";
25
26
  import { Separator } from "./separator";
26
27
 
@@ -70,7 +71,8 @@ export function MetricSparklineCard({
70
71
  return (
71
72
  <div
72
73
  className={cn(
73
- "relative overflow-hidden rounded-xl border border-border bg-card p-5 text-card-foreground shadow-sm",
74
+ "relative overflow-hidden rounded-xl border border-border bg-card p-5 text-card-foreground",
75
+ elevation.surface,
74
76
  className
75
77
  )}
76
78
  >
@@ -167,7 +169,7 @@ export function DashboardAreaChartTemplate({
167
169
  const areaGid = React.useId().replace(/:/g, "");
168
170
 
169
171
  return (
170
- <div className={cn("rounded-xl border border-border bg-card text-card-foreground shadow-sm", className)}>
172
+ <div className={cn("rounded-xl border border-border bg-card text-card-foreground", elevation.surface, className)}>
171
173
  <div className="flex flex-col gap-4 border-b border-border px-5 pt-4 pb-3 sm:flex-row sm:items-start sm:justify-between">
172
174
  <h3 className="text-sm font-semibold text-foreground">{title}</h3>
173
175
  <div className="flex flex-wrap gap-1 rounded-lg border border-border bg-muted/30 p-0.5">
@@ -285,7 +287,7 @@ export function GroupedBarChartTemplate({
285
287
  className,
286
288
  }: GroupedBarChartTemplateProps) {
287
289
  return (
288
- <div className={cn("rounded-xl border border-border bg-card px-5 pt-4 pb-3 text-card-foreground shadow-sm", className)}>
290
+ <div className={cn("rounded-xl border border-border bg-card px-5 pt-4 pb-3 text-card-foreground", elevation.surface, className)}>
289
291
  <div className="mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
290
292
  <div>
291
293
  <h3 className="text-sm font-semibold text-foreground">{title}</h3>
@@ -362,7 +364,7 @@ export function RankedBarListTemplate({
362
364
  className,
363
365
  }: RankedBarListTemplateProps) {
364
366
  return (
365
- <div className={cn("rounded-xl border border-border bg-card p-5 text-card-foreground shadow-sm", className)}>
367
+ <div className={cn("rounded-xl border border-border bg-card p-5 text-card-foreground", elevation.surface, className)}>
366
368
  <div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
367
369
  <div>
368
370
  <h3 className="text-sm font-semibold text-foreground">{title}</h3>
@@ -422,7 +424,7 @@ export function CategoryBarChartTemplate({
422
424
  const chartData = data.map((d) => ({ name: d.category, v: d.value }));
423
425
 
424
426
  return (
425
- <div className={cn("rounded-xl border border-border bg-card px-5 pt-4 pb-3 text-card-foreground shadow-sm", className)}>
427
+ <div className={cn("rounded-xl border border-border bg-card px-5 pt-4 pb-3 text-card-foreground", elevation.surface, className)}>
426
428
  <div className="mb-3">
427
429
  <h3 className="text-sm font-semibold text-foreground">{title}</h3>
428
430
  {subtitle ? <p className="mt-0.5 text-xs text-muted-foreground">{subtitle}</p> : null}
@@ -486,7 +488,7 @@ export function MiniSparklineChartCard({
486
488
  const hasCompare = data.some((d) => d.compare != null);
487
489
 
488
490
  return (
489
- <div className={cn("rounded-xl border border-border bg-card p-5 text-card-foreground shadow-sm", className)}>
491
+ <div className={cn("rounded-xl border border-border bg-card p-5 text-card-foreground", elevation.surface, className)}>
490
492
  <h3 className="text-sm font-semibold text-foreground">{title}</h3>
491
493
  <div className="mt-2 text-2xl font-semibold tabular-nums">{value}</div>
492
494
  <div className="mt-2" style={{ height }}>
@@ -554,7 +556,7 @@ const toneCls = {
554
556
 
555
557
  export function AttentionListTemplate({ title, items, className }: AttentionListTemplateProps) {
556
558
  return (
557
- <div className={cn("rounded-xl border border-border bg-card p-5 text-card-foreground shadow-sm", className)}>
559
+ <div className={cn("rounded-xl border border-border bg-card p-5 text-card-foreground", elevation.surface, className)}>
558
560
  <h3 className="text-sm font-semibold text-foreground">{title}</h3>
559
561
  <ul className="mt-4 space-y-4">
560
562
  {items.map((item) => (
@@ -5,6 +5,7 @@ import * as PopoverPrimitive from "@radix-ui/react-popover";
5
5
  import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
6
6
  import { Check, ChevronDown, Search } from "lucide-react";
7
7
  import { cn } from "./utils";
8
+ import { elevation } from "./elevation";
8
9
  import { filterOptions, type OptionFilter } from "./option-filter";
9
10
  import { ScrollLockTakeover } from "./scroll-lock";
10
11
 
@@ -114,7 +115,8 @@ const CheckboxSelect = React.forwardRef<HTMLButtonElement, CheckboxSelectProps>(
114
115
  aria-expanded={open}
115
116
  aria-haspopup="listbox"
116
117
  className={cn(
117
- "flex h-11 w-full items-center justify-between gap-2.5 rounded-lg border border-border bg-card px-4 py-2 text-[15px] shadow-sm outline-none",
118
+ "flex h-11 w-full items-center justify-between gap-2.5 rounded-lg border border-border bg-card px-4 py-2 text-[15px] outline-none",
119
+ elevation.field,
118
120
  "transition-colors duration-pg-fast ease-pg-standard",
119
121
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
120
122
  "disabled:cursor-not-allowed disabled:opacity-50",
@@ -141,7 +143,8 @@ const CheckboxSelect = React.forwardRef<HTMLButtonElement, CheckboxSelectProps>(
141
143
  sideOffset={6}
142
144
  collisionPadding={8}
143
145
  className={cn(
144
- "z-[120] min-w-[var(--radix-popover-trigger-width)] w-full rounded-xl border border-border bg-popover text-popover-foreground shadow-lg outline-none p-1",
146
+ "z-[120] min-w-[var(--radix-popover-trigger-width)] w-full rounded-xl border border-border bg-popover text-popover-foreground outline-none p-1",
147
+ elevation.popover,
145
148
  "data-[state=open]:opacity-100 data-[state=closed]:opacity-0 transition-opacity duration-150"
146
149
  )}
147
150
  >
@@ -155,7 +158,8 @@ const CheckboxSelect = React.forwardRef<HTMLButtonElement, CheckboxSelectProps>(
155
158
  placeholder={searchPlaceholder}
156
159
  aria-label={searchPlaceholder}
157
160
  className={cn(
158
- "flex h-9 w-full rounded-md border border-border bg-card pl-8 pr-3 text-sm shadow-sm placeholder:text-muted-foreground",
161
+ "flex h-9 w-full rounded-md border border-border bg-card pl-8 pr-3 text-sm placeholder:text-muted-foreground",
162
+ elevation.field,
159
163
  "transition-colors duration-pg-fast ease-pg-standard",
160
164
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35"
161
165
  )}
@@ -222,7 +226,8 @@ const CheckboxSelect = React.forwardRef<HTMLButtonElement, CheckboxSelectProps>(
222
226
  onCheckedChange={() => !option.disabled && handleToggle(option.value)}
223
227
  onClick={(e) => e.stopPropagation()}
224
228
  className={cn(
225
- "peer h-4 w-4 shrink-0 rounded-md border border-border bg-card shadow-sm",
229
+ "peer h-4 w-4 shrink-0 rounded-md border border-border bg-card",
230
+ elevation.field,
226
231
  "transition-colors duration-pg-fast ease-pg-standard",
227
232
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
228
233
  "disabled:cursor-not-allowed disabled:opacity-50",
package/src/checkbox.tsx CHANGED
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
5
5
  import { Check, Minus } from "lucide-react";
6
6
  import { cn } from "./utils";
7
+ import { elevation } from "./elevation";
7
8
 
8
9
  export type CheckboxSize = "sm" | "md" | "lg";
9
10
 
@@ -30,7 +31,8 @@ const Checkbox = React.forwardRef<
30
31
  <CheckboxPrimitive.Root
31
32
  ref={ref}
32
33
  className={cn(
33
- "peer shrink-0 border border-border bg-card shadow-sm transition-colors duration-pg-fast ease-pg-standard",
34
+ "peer shrink-0 border border-border bg-card transition-colors duration-pg-fast ease-pg-standard",
35
+ elevation.field,
34
36
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
35
37
  "disabled:cursor-not-allowed disabled:opacity-50",
36
38
  "data-[state=checked]:bg-primary data-[state=checked]:border-primary",
package/src/command.tsx CHANGED
@@ -3,6 +3,7 @@
3
3
  import * as React from "react";
4
4
  import { Search } from "lucide-react";
5
5
  import { cn } from "./utils";
6
+ import { elevation } from "./elevation";
6
7
 
7
8
  export interface CommandProps extends React.HTMLAttributes<HTMLDivElement> {}
8
9
  export interface CommandInputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
@@ -14,7 +15,7 @@ export interface CommandSeparatorProps extends React.HTMLAttributes<HTMLDivEleme
14
15
  export interface CommandShortcutProps extends React.HTMLAttributes<HTMLSpanElement> {}
15
16
 
16
17
  const Command = React.forwardRef<HTMLDivElement, CommandProps>(({ className, ...props }, ref) => (
17
- <div ref={ref} className={cn("bg-card rounded-xl border border-border shadow-lg overflow-hidden", className)} {...props} />
18
+ <div ref={ref} className={cn("bg-card rounded-xl border border-border overflow-hidden", elevation.popover, className)} {...props} />
18
19
  ));
19
20
  Command.displayName = "Command";
20
21
 
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import * as PopoverPrimitive from "@radix-ui/react-popover";
5
5
  import { Check, ChevronDown, Search } from "lucide-react";
6
6
  import { cn } from "./utils";
7
+ import { elevation } from "./elevation";
7
8
  import { ScrollLockTakeover } from "./scroll-lock";
8
9
 
9
10
  export interface Country {
@@ -107,7 +108,8 @@ const CountrySelect = React.forwardRef<HTMLButtonElement, CountrySelectProps>(
107
108
  aria-expanded={open}
108
109
  aria-haspopup="listbox"
109
110
  className={cn(
110
- "flex h-11 min-h-11 w-full items-center justify-between gap-2.5 rounded-lg border border-border bg-card px-4 text-[15px] text-foreground shadow-sm",
111
+ "flex h-11 min-h-11 w-full items-center justify-between gap-2.5 rounded-lg border border-border bg-card px-4 text-[15px] text-foreground",
112
+ elevation.field,
111
113
  "transition-colors duration-pg-fast ease-pg-standard",
112
114
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
113
115
  "disabled:cursor-not-allowed disabled:opacity-50",
@@ -150,7 +152,8 @@ const CountrySelect = React.forwardRef<HTMLButtonElement, CountrySelectProps>(
150
152
  align="start"
151
153
  sideOffset={6}
152
154
  className={cn(
153
- "z-[120] w-[var(--radix-popover-trigger-width)] min-w-[260px] rounded-xl border border-border bg-popover text-popover-foreground shadow-lg outline-none",
155
+ "z-[120] w-[var(--radix-popover-trigger-width)] min-w-[260px] rounded-xl border border-border bg-popover text-popover-foreground outline-none",
156
+ elevation.popover,
154
157
  "data-[state=open]:opacity-100 data-[state=closed]:opacity-0 transition-opacity duration-150"
155
158
  )}
156
159
  onOpenAutoFocus={(e) => e.preventDefault()}
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { CSSProperties, ReactNode } from "react";
4
4
  import { cn } from "./utils";
5
+ import { elevation } from "./elevation";
5
6
  import { Shimmer } from "./skeleton";
6
7
  import { EmptyState } from "./empty-state";
7
8
  import {
@@ -1066,7 +1067,8 @@ export function DataTable<T>({
1066
1067
  type="button"
1067
1068
  onClick={() => rowCta?.onClick?.(row)}
1068
1069
  className={cn(
1069
- "inline-flex items-center font-medium text-foreground bg-card rounded-lg border border-border hover:border-muted-foreground/50 whitespace-nowrap shadow-sm",
1070
+ "inline-flex items-center font-medium text-foreground bg-card rounded-lg border border-border hover:border-muted-foreground/50 whitespace-nowrap",
1071
+ elevation.control,
1070
1072
  compact
1071
1073
  ? "px-2.5 py-1 text-[11px]"
1072
1074
  : "px-3 py-1.5 text-[12px]"
@@ -1146,7 +1148,7 @@ const PAGE_BUTTON =
1146
1148
  "w-7 h-7 rounded-md flex items-center justify-center text-[12px] font-medium tabular-nums transition-colors";
1147
1149
  const PAGE_BUTTON_IDLE =
1148
1150
  "text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed";
1149
- const PAGE_BUTTON_ACTIVE = "bg-primary text-primary-foreground shadow-sm";
1151
+ const PAGE_BUTTON_ACTIVE = `bg-primary text-primary-foreground ${elevation.control}`;
1150
1152
 
1151
1153
  /**
1152
1154
  * One footer for every pagination mode, so a cursor-paged grid and a
@@ -6,6 +6,7 @@ import { RemoveScroll } from "react-remove-scroll";
6
6
  import { ChevronLeft, ChevronRight, ChevronDown, CalendarDays } from "lucide-react";
7
7
  import { AnimatePresence, motion } from "framer-motion";
8
8
  import { cn } from "./utils";
9
+ import { elevation } from "./elevation";
9
10
  import { ScrollLockTakeover } from "./scroll-lock";
10
11
 
11
12
  /* ─── Constants ─────────────────────────────────────────────────────────── */
@@ -453,7 +454,7 @@ export function DatePicker({ value, onChange, placeholder = "Select date", class
453
454
  <motion.div
454
455
  initial={{ opacity: 0, y: -4 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -4 }}
455
456
  transition={{ duration: 0.12 }}
456
- className="absolute top-full left-0 z-[10000] mt-1 max-h-[220px] min-w-[130px] overflow-y-auto rounded-xl border border-border bg-popover py-1 shadow-lg"
457
+ className={`absolute top-full left-0 z-[10000] mt-1 max-h-[220px] min-w-[130px] overflow-y-auto rounded-xl border border-border bg-popover py-1 ${elevation.popover}`}
457
458
  >
458
459
  {MONTHS.map((mn, mi) => (
459
460
  <button type="button" key={mn} onClick={() => { setViewMonth(mi); setMonthMenu(false); }}
@@ -480,7 +481,7 @@ export function DatePicker({ value, onChange, placeholder = "Select date", class
480
481
  <motion.div
481
482
  initial={{ opacity: 0, y: -4 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -4 }}
482
483
  transition={{ duration: 0.12 }}
483
- className="absolute top-full left-0 z-[10000] mt-1 max-h-[200px] min-w-[90px] overflow-y-auto rounded-xl border border-border bg-popover py-1 shadow-lg"
484
+ className={`absolute top-full left-0 z-[10000] mt-1 max-h-[200px] min-w-[90px] overflow-y-auto rounded-xl border border-border bg-popover py-1 ${elevation.popover}`}
484
485
  >
485
486
  {years.map(yr => (
486
487
  <button type="button" key={yr} onClick={() => { setViewYear(yr); setYearMenu(false); }}
@@ -601,7 +602,10 @@ export function DatePicker({ value, onChange, placeholder = "Select date", class
601
602
  animate={{ opacity: 1, scale: 1, y: 0 }}
602
603
  exit={{ opacity: 0, scale: 0.97, y: -6 }}
603
604
  transition={{ duration: 0.16, ease: [0.16, 1, 0.3, 1] }}
604
- className="isolate rounded-2xl border border-border bg-popover text-popover-foreground shadow-lg select-none dark:shadow-black/40"
605
+ className={cn(
606
+ "isolate rounded-2xl border border-border bg-popover text-popover-foreground select-none dark:shadow-black/40",
607
+ elevation.popover
608
+ )}
605
609
  style={{
606
610
  position: "fixed",
607
611
  top: panelPos.top,
@@ -645,7 +649,8 @@ export function DatePicker({ value, onChange, placeholder = "Select date", class
645
649
  type="button"
646
650
  onClick={() => open ? closePanel() : openPanel()}
647
651
  className={cn(
648
- "flex h-12 min-h-12 w-full items-center gap-3 rounded-xl border border-border bg-card px-5 text-left text-[15px] shadow-sm transition-colors",
652
+ "flex h-12 min-h-12 w-full items-center gap-3 rounded-xl border border-border bg-card px-5 text-left text-[15px] transition-colors",
653
+ elevation.field,
649
654
  open ? "border-ring ring-2 ring-ring/20" : "hover:border-muted-foreground/45",
650
655
  )}
651
656
  >
package/src/dialog.tsx CHANGED
@@ -4,6 +4,7 @@ import type { ComponentProps } from "react";
4
4
  import * as DialogPrimitive from "@radix-ui/react-dialog";
5
5
  import { X } from "lucide-react";
6
6
  import { cn } from "./utils";
7
+ import { elevation } from "./elevation";
7
8
 
8
9
  const Dialog = DialogPrimitive.Root;
9
10
  const DialogTrigger = DialogPrimitive.Trigger;
@@ -41,7 +42,8 @@ function DialogContent({
41
42
  "fixed left-1/2 top-1/2 z-[101] w-[calc(100%-1.5rem)] max-w-[min(100%,26rem)] -translate-x-1/2 -translate-y-1/2",
42
43
  // Default padding reserves space for the close button.
43
44
  // Pass p-0 in className to opt out (manage padding per section).
44
- "rounded-2xl border border-border bg-card p-6 pt-10 text-card-foreground shadow-2xl outline-none",
45
+ "rounded-2xl border border-border bg-card p-6 pt-10 text-card-foreground outline-none",
46
+ elevation.modal,
45
47
  "max-h-[min(90vh,720px)] overflow-y-auto",
46
48
  "data-[state=open]:opacity-100 data-[state=closed]:opacity-0 data-[state=open]:scale-100 data-[state=closed]:scale-[0.98]",
47
49
  "transition-[opacity,transform] duration-200 ease-out",
package/src/drawer.tsx CHANGED
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import * as DialogPrimitive from "@radix-ui/react-dialog";
5
5
  import { X } from "lucide-react";
6
6
  import { cn } from "./utils";
7
+ import { elevation } from "./elevation";
7
8
 
8
9
  type DrawerSide = "right" | "left" | "top" | "bottom";
9
10
  const DrawerContext = React.createContext<{ side: DrawerSide }>({ side: "right" });
@@ -49,7 +50,7 @@ const DrawerContent = React.forwardRef<
49
50
  <DrawerOverlay />
50
51
  <DialogPrimitive.Content
51
52
  ref={ref}
52
- className={cn("fixed z-50 flex flex-col bg-card border-border shadow-xl transition-all duration-pg-normal ease-pg-standard data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-200", sideClasses[side], className)}
53
+ className={cn(elevation.drawer, "fixed z-50 flex flex-col bg-card border-border transition-all duration-pg-normal ease-pg-standard data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-200", sideClasses[side], className)}
53
54
  {...props}
54
55
  >
55
56
  {children}
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5
5
  import { Check, ChevronRight, Circle } from "lucide-react";
6
6
  import { cn } from "./utils";
7
+ import { elevation } from "./elevation";
7
8
 
8
9
  const DropdownMenu = DropdownMenuPrimitive.Root;
9
10
  const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
@@ -41,7 +42,8 @@ const DropdownMenuSubContent = React.forwardRef<
41
42
  <DropdownMenuPrimitive.SubContent
42
43
  ref={ref}
43
44
  className={cn(
44
- "z-[130] min-w-[8rem] overflow-hidden rounded-xl border border-border bg-popover p-1.5 text-popover-foreground shadow-lg",
45
+ "z-[130] min-w-[8rem] overflow-hidden rounded-xl border border-border bg-popover p-1.5 text-popover-foreground",
46
+ elevation.popover,
45
47
  "data-[state=open]:opacity-100 data-[state=closed]:opacity-0 transition-opacity duration-150",
46
48
  className
47
49
  )}
@@ -59,7 +61,8 @@ const DropdownMenuContent = React.forwardRef<
59
61
  ref={ref}
60
62
  sideOffset={sideOffset}
61
63
  className={cn(
62
- "z-[130] min-w-[11rem] overflow-hidden rounded-xl border border-border bg-popover p-1.5 text-popover-foreground shadow-lg",
64
+ "z-[130] min-w-[11rem] overflow-hidden rounded-xl border border-border bg-popover p-1.5 text-popover-foreground",
65
+ elevation.popover,
63
66
  "data-[state=open]:opacity-100 data-[state=closed]:opacity-0 transition-opacity duration-150",
64
67
  className
65
68
  )}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Elevation — the one place flux decides what casts a shadow.
3
+ *
4
+ * Shadows used to be typed inline at ~60 call sites, which is how the library
5
+ * drifted: a Card, an Alert and a Skeleton card are the same kind of surface,
6
+ * but nothing held them to the same value, and "the default card shadow" was
7
+ * whatever the last person wrote. These constants are that default, named.
8
+ *
9
+ * They are plain Tailwind utility strings rather than CSS custom properties on
10
+ * purpose. flux ships no stylesheet — consuming apps compile its classes via
11
+ * `@source ".../flux-ui/src"` — so a `--shadow-*` token would have to be
12
+ * redeclared in every app's globals.css and would silently resolve to nothing
13
+ * in any app that forgot. A literal class here is scanned like any other and
14
+ * needs no app-side setup.
15
+ *
16
+ * Changing a value here restyles every component that uses it. That is the point.
17
+ */
18
+ export const elevation = {
19
+ /**
20
+ * Flat surfaces that sit ON the page and are delineated by their border:
21
+ * Card, chart surfaces, DataTableCard, Alert, SectionMessage, Skeleton.
22
+ * This is the shadow on the mca-home revenue card, and the reference the
23
+ * rest of the library was pulled onto.
24
+ */
25
+ surface: "shadow-sm",
26
+
27
+ /**
28
+ * Button-shaped things: Button, IconButton, ButtonGroup segments, the
29
+ * pagination page buttons, a table's row CTA. Just enough lift to read as
30
+ * pressable. Ghost and link variants stay flat and do not use this.
31
+ */
32
+ control: "shadow-sm",
33
+
34
+ /**
35
+ * Anything you type into or pick from: Input, Textarea, every select
36
+ * trigger, the date and time pickers, OTP boxes, Checkbox, Radio.
37
+ *
38
+ * Deliberately flat. A field is a well, not a raised object, and a form of
39
+ * ten shadowed controls reads as clutter. Emitted as an explicit
40
+ * `shadow-none` rather than by omitting the class so that a caller passing
41
+ * `shadow-sm` through `className` still wins under tailwind-merge.
42
+ */
43
+ field: "shadow-none",
44
+
45
+ /**
46
+ * Things that genuinely float above the page, where depth is the signal that
47
+ * says "this is detached and dismissible". Kept heavier than `surface` on
48
+ * purpose — flattening these is what makes a dropdown look painted on.
49
+ */
50
+ tooltip: "shadow-md",
51
+ popover: "shadow-lg",
52
+ drawer: "shadow-xl",
53
+ modal: "shadow-2xl",
54
+ } as const;
package/src/flag.tsx CHANGED
@@ -3,6 +3,7 @@
3
3
  import * as React from "react";
4
4
  import { Info, CheckCircle2, AlertTriangle, XCircle, X } from "lucide-react";
5
5
  import { cn } from "./utils";
6
+ import { elevation } from "./elevation";
6
7
 
7
8
  // ─── Types ────────────────────────────────────────────────────────────────────
8
9
 
@@ -150,7 +151,8 @@ const Flag = React.forwardRef<HTMLDivElement, FlagProps>(
150
151
  onMouseEnter={handleMouseEnter}
151
152
  onMouseLeave={handleMouseLeave}
152
153
  className={cn(
153
- "relative w-80 overflow-hidden rounded-xl border border-border bg-card shadow-lg",
154
+ "relative w-80 overflow-hidden rounded-xl border border-border bg-card",
155
+ elevation.popover,
154
156
  "border-l-4",
155
157
  variantBorder[variant],
156
158
  className
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { cn } from "./utils";
4
+ import { elevation } from "./elevation";
4
5
  import { Loader2 } from "lucide-react";
5
6
  import { type ButtonHTMLAttributes, forwardRef } from "react";
6
7
 
@@ -14,15 +15,15 @@ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
14
15
 
15
16
  const variantClasses: Record<NonNullable<IconButtonProps["variant"]>, string> = {
16
17
  primary:
17
- "bg-primary text-primary-foreground border border-primary shadow-sm hover:bg-[var(--primary-hover)]",
18
+ `bg-primary text-primary-foreground border border-primary ${elevation.control} hover:bg-[var(--primary-hover)]`,
18
19
  secondary:
19
- "bg-muted text-foreground border border-border shadow-sm hover:bg-muted/85 dark:bg-muted/35 dark:text-foreground dark:border-border dark:hover:bg-muted/55",
20
+ `bg-muted text-foreground border border-border ${elevation.control} hover:bg-muted/85 dark:bg-muted/35 dark:text-foreground dark:border-border dark:hover:bg-muted/55`,
20
21
  ghost:
21
22
  "bg-transparent text-foreground border border-transparent hover:bg-muted focus-visible:bg-muted/80 active:bg-muted/90",
22
23
  outline:
23
- "bg-card text-foreground border border-border shadow-sm hover:bg-muted",
24
+ `bg-card text-foreground border border-border ${elevation.control} hover:bg-muted`,
24
25
  danger:
25
- "bg-red-600 text-white border border-red-600 shadow-sm hover:bg-red-700",
26
+ `bg-red-600 text-white border border-red-600 ${elevation.control} hover:bg-red-700`,
26
27
  };
27
28
 
28
29
  const sizeClasses: Record<NonNullable<IconButtonProps["size"]>, string> = {
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { cn } from "./utils";
2
+ export { elevation } from "./elevation";
2
3
 
3
4
  // Layout
4
5
  export { Box, Stack, Inline } from "./layout";
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import * as PopoverPrimitive from "@radix-ui/react-popover";
5
5
  import { X } from "lucide-react";
6
6
  import { cn } from "./utils";
7
+ import { elevation } from "./elevation";
7
8
  import { ScrollLockTakeover } from "./scroll-lock";
8
9
 
9
10
  // ---------------------------------------------------------------------------
@@ -114,7 +115,8 @@ const InlineDialogContent = React.forwardRef<
114
115
  className={cn(
115
116
  // Base card
116
117
  "relative z-[120] max-w-xs overflow-visible",
117
- "rounded-xl border border-border bg-card p-4 text-foreground shadow-lg",
118
+ "rounded-xl border border-border bg-card p-4 text-foreground",
119
+ elevation.popover,
118
120
  // Animation
119
121
  "data-[state=open]:opacity-100 data-[state=closed]:opacity-0",
120
122
  "transition-opacity duration-pg-fast ease-pg-standard",
@@ -3,6 +3,7 @@
3
3
  import * as React from "react";
4
4
  import { Check, Pencil, X } from "lucide-react";
5
5
  import { cn } from "./utils";
6
+ import { elevation } from "./elevation";
6
7
 
7
8
  export interface InlineEditProps {
8
9
  value: string;
@@ -93,7 +94,8 @@ const InlineEdit = React.forwardRef<HTMLDivElement, InlineEditProps>(
93
94
  };
94
95
 
95
96
  const sharedInputClass = cn(
96
- "w-full rounded-md border border-border bg-card px-2 py-1 text-[15px] leading-tight text-foreground shadow-sm",
97
+ "w-full rounded-md border border-border bg-card px-2 py-1 text-[15px] leading-tight text-foreground",
98
+ elevation.field,
97
99
  "placeholder:text-muted-foreground",
98
100
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
99
101
  "transition-colors duration-pg-fast ease-pg-standard",
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import { cva, type VariantProps } from "class-variance-authority";
5
5
 
6
6
  import { cn } from "./utils";
7
+ import { elevation } from "./elevation";
7
8
  import { Input } from "./input";
8
9
  import { Textarea } from "./textarea";
9
10
 
@@ -13,7 +14,8 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
13
14
  data-slot="input-group"
14
15
  role="group"
15
16
  className={cn(
16
- "group/input-group relative flex w-full items-stretch rounded-xl border border-border bg-card shadow-sm outline-none transition-[color,box-shadow] dark:bg-card/50",
17
+ "group/input-group relative flex w-full items-stretch rounded-xl border border-border bg-card outline-none transition-[color,box-shadow] dark:bg-card/50",
18
+ elevation.field,
17
19
  "min-h-11 h-11 has-[>textarea]:h-auto",
18
20
  "has-[>[data-align=inline-start]]:[&>input]:pl-2",
19
21
  "has-[>[data-align=inline-end]]:[&>input]:pr-2",