@majapisoftwares/ui 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/AGENTS.md +11 -1
  2. package/README.md +19 -11
  3. package/THIRD_PARTY_NOTICES.md +25 -1
  4. package/components.json +1 -1
  5. package/docs/MIGRATION-6.0.md +33 -0
  6. package/docs/STANDARDS.md +11 -6
  7. package/docs/VERSIONING.md +28 -0
  8. package/package.json +19 -10
  9. package/scripts/check-standards.mjs +2 -0
  10. package/src/components/ui/accordion.tsx +82 -0
  11. package/src/components/ui/alert-dialog.tsx +187 -0
  12. package/src/components/ui/alert.tsx +2 -2
  13. package/src/components/ui/avatar.tsx +108 -0
  14. package/src/components/ui/badge.tsx +1 -1
  15. package/src/components/ui/breadcrumb.tsx +125 -0
  16. package/src/components/ui/button-group.tsx +89 -0
  17. package/src/components/ui/button.tsx +12 -16
  18. package/src/components/ui/card.tsx +101 -0
  19. package/src/components/ui/checkbox.tsx +27 -0
  20. package/src/components/ui/collapsible.tsx +21 -0
  21. package/src/components/ui/context-menu.tsx +271 -0
  22. package/src/components/ui/dialog.tsx +161 -0
  23. package/src/components/ui/dropdown-menu.tsx +271 -0
  24. package/src/components/ui/empty.tsx +4 -4
  25. package/src/components/ui/field.tsx +6 -6
  26. package/src/components/ui/hover-card.tsx +50 -0
  27. package/src/components/ui/input-group.tsx +155 -0
  28. package/src/components/ui/input.tsx +1 -1
  29. package/src/components/ui/kbd.tsx +28 -0
  30. package/src/components/ui/orb.css +1002 -0
  31. package/src/components/ui/orb.tsx +752 -0
  32. package/src/components/ui/popover.tsx +89 -0
  33. package/src/components/ui/progress.tsx +82 -0
  34. package/src/components/ui/radio-group.tsx +37 -0
  35. package/src/components/ui/scroll-area.tsx +54 -0
  36. package/src/components/ui/select.tsx +204 -0
  37. package/src/components/ui/sheet.tsx +8 -6
  38. package/src/components/ui/sidebar.tsx +1035 -0
  39. package/src/components/ui/skeleton.tsx +1 -1
  40. package/src/components/ui/slider.tsx +53 -0
  41. package/src/components/ui/sonner.tsx +44 -0
  42. package/src/components/ui/spinner.tsx +32 -0
  43. package/src/components/ui/switch.tsx +31 -0
  44. package/src/components/ui/table.tsx +10 -4
  45. package/src/components/ui/tabs.tsx +2 -2
  46. package/src/components/ui/textarea.tsx +19 -0
  47. package/src/components/ui/toggle-group.tsx +89 -0
  48. package/src/components/ui/toggle.tsx +44 -0
  49. package/src/components/ui/tooltip.tsx +65 -0
  50. package/src/hooks/use-mobile.ts +20 -0
  51. package/src/lib/i18n.tsx +109 -0
  52. package/src/patterns/app-shell.tsx +158 -53
  53. package/src/patterns/data-table.tsx +2441 -180
  54. package/src/patterns/details-sheet.tsx +8 -6
  55. package/src/patterns/filter-tabs.tsx +53 -0
  56. package/src/patterns/majapi-logo.tsx +32 -0
  57. package/src/patterns/page-header.tsx +44 -22
  58. package/src/patterns/preferences.tsx +172 -0
  59. package/src/patterns/status-badge.tsx +4 -3
  60. package/src/styles.css +41 -341
  61. package/src/templates/admin-dashboard.tsx +14 -33
  62. package/src/templates/auth-layout.tsx +18 -24
  63. package/src/templates/login-form.tsx +97 -65
  64. package/src/templates/signup-form.tsx +139 -99
  65. package/src/components/ui/native-select.tsx +0 -66
  66. package/src/vite-env.d.ts +0 -1
@@ -6,7 +6,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
6
6
  return (
7
7
  <div
8
8
  data-slot="skeleton"
9
- className={cn("animate-pulse rounded-md bg-muted", className)}
9
+ className={cn("animate-pulse rounded-2xl bg-muted", className)}
10
10
  {...props}
11
11
  />
12
12
  );
@@ -0,0 +1,53 @@
1
+ "use client";
2
+
3
+ import { Slider as SliderPrimitive } from "@base-ui/react/slider";
4
+ import { cn } from "cn";
5
+
6
+ function Slider({
7
+ className,
8
+ defaultValue,
9
+ value,
10
+ min = 0,
11
+ max = 100,
12
+ ...props
13
+ }: SliderPrimitive.Root.Props) {
14
+ const _values = Array.isArray(value)
15
+ ? value
16
+ : Array.isArray(defaultValue)
17
+ ? defaultValue
18
+ : [min, max];
19
+
20
+ return (
21
+ <SliderPrimitive.Root
22
+ className={cn("data-horizontal:w-full data-vertical:h-full", className)}
23
+ data-slot="slider"
24
+ defaultValue={defaultValue}
25
+ value={value}
26
+ min={min}
27
+ max={max}
28
+ thumbAlignment="edge"
29
+ {...props}
30
+ >
31
+ <SliderPrimitive.Control className="relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:min-h-40 data-vertical:w-auto data-vertical:flex-col">
32
+ <SliderPrimitive.Track
33
+ data-slot="slider-track"
34
+ className="relative grow overflow-hidden rounded-full bg-input/90 select-none data-horizontal:h-2 data-horizontal:w-full data-vertical:h-full data-vertical:w-2"
35
+ >
36
+ <SliderPrimitive.Indicator
37
+ data-slot="slider-range"
38
+ className="bg-primary select-none data-horizontal:h-full data-vertical:w-full"
39
+ />
40
+ </SliderPrimitive.Track>
41
+ {Array.from({ length: _values.length }, (_, index) => (
42
+ <SliderPrimitive.Thumb
43
+ data-slot="slider-thumb"
44
+ key={index}
45
+ className="block h-4 w-6 shrink-0 rounded-full bg-white shadow-md ring-1 ring-black/10 transition-[color,box-shadow,background-color] select-none not-dark:bg-clip-padding hover:ring-4 hover:ring-ring/30 focus-visible:ring-4 focus-visible:ring-ring/30 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-vertical:h-6 data-vertical:w-4"
46
+ />
47
+ ))}
48
+ </SliderPrimitive.Control>
49
+ </SliderPrimitive.Root>
50
+ );
51
+ }
52
+
53
+ export { Slider };
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ import { Orb } from "./orb";
4
+ import { Toaster as Sonner, type ToasterProps } from "sonner";
5
+ import {
6
+ CircleCheckIcon,
7
+ InfoIcon,
8
+ TriangleAlertIcon,
9
+ OctagonXIcon,
10
+ } from "lucide-react";
11
+
12
+ // Theme comes from the caller (Preferences passes the active theme).
13
+ const Toaster = ({ theme = "system", ...props }: ToasterProps) => {
14
+ return (
15
+ <Sonner
16
+ theme={theme}
17
+ className="toaster group"
18
+ icons={{
19
+ success: <CircleCheckIcon className="size-4" />,
20
+ info: <InfoIcon className="size-4" />,
21
+ warning: <TriangleAlertIcon className="size-4" />,
22
+ error: <OctagonXIcon className="size-4" />,
23
+ loading: <Orb size={16} />,
24
+ }}
25
+ style={
26
+ {
27
+ "--normal-bg": "var(--popover)",
28
+ "--normal-text": "var(--popover-foreground)",
29
+ "--normal-border": "var(--border)",
30
+ "--border-radius": "var(--radius)",
31
+ } as React.CSSProperties
32
+ }
33
+ toastOptions={{
34
+ classNames: {
35
+ toast: "cn-toast",
36
+ },
37
+ }}
38
+ {...props}
39
+ />
40
+ );
41
+ };
42
+
43
+ export { Toaster };
44
+ export { toast } from "sonner";
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import type * as React from "react";
4
+ import { cn } from "cn";
5
+ import { Orb, type OrbVariant } from "./orb";
6
+ import { useUiMessages } from "../../lib/i18n";
7
+
8
+ // Majapi UI: loading uses the animated Orb instead of a rotating icon.
9
+ function Spinner({
10
+ className,
11
+ size = 16,
12
+ variant = "S1",
13
+ label,
14
+ ...props
15
+ }: React.ComponentProps<"span"> & {
16
+ size?: number;
17
+ variant?: OrbVariant;
18
+ label?: string;
19
+ }) {
20
+ const messages = useUiMessages();
21
+ return (
22
+ <span
23
+ data-slot="spinner"
24
+ className={cn("inline-flex shrink-0", className)}
25
+ {...props}
26
+ >
27
+ <Orb variant={variant} size={size} label={label ?? messages.loading} />
28
+ </span>
29
+ );
30
+ }
31
+
32
+ export { Spinner };
@@ -0,0 +1,31 @@
1
+ "use client";
2
+
3
+ import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
4
+ import { cn } from "cn";
5
+
6
+ function Switch({
7
+ className,
8
+ size = "default",
9
+ ...props
10
+ }: SwitchPrimitive.Root.Props & {
11
+ size?: "sm" | "default";
12
+ }) {
13
+ return (
14
+ <SwitchPrimitive.Root
15
+ data-slot="switch"
16
+ data-size={size}
17
+ className={cn(
18
+ "peer group/switch relative inline-flex shrink-0 items-center rounded-full border-2 transition-all outline-none group-has-[:focus-visible]/field-label:ring-0 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-5 data-[size=default]:w-11 data-[size=sm]:h-4 data-[size=sm]:w-7 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary group-has-[:focus-visible]/field-label:data-checked:border-primary data-unchecked:border-transparent data-unchecked:bg-input/90 group-has-[:focus-visible]/field-label:data-unchecked:border-transparent data-disabled:cursor-not-allowed data-disabled:opacity-50",
19
+ className,
20
+ )}
21
+ {...props}
22
+ >
23
+ <SwitchPrimitive.Thumb
24
+ data-slot="switch-thumb"
25
+ className="pointer-events-none block rounded-full bg-background shadow-sm ring-0 transition-transform not-dark:bg-clip-padding group-data-[size=default]/switch:h-4 group-data-[size=default]/switch:w-6 group-data-[size=sm]/switch:h-3 group-data-[size=sm]/switch:w-4 data-checked:translate-x-[calc(100%-8px)] dark:data-checked:bg-primary-foreground data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
26
+ />
27
+ </SwitchPrimitive.Root>
28
+ );
29
+ }
30
+
31
+ export { Switch };
@@ -3,11 +3,17 @@
3
3
  import * as React from "react";
4
4
  import { cn } from "cn";
5
5
 
6
- function Table({ className, ...props }: React.ComponentProps<"table">) {
6
+ // Majapi UI: `containerClassName` styles the scroll container (e.g. to fill
7
+ // the available height and scroll vertically).
8
+ function Table({
9
+ className,
10
+ containerClassName,
11
+ ...props
12
+ }: React.ComponentProps<"table"> & { containerClassName?: string }) {
7
13
  return (
8
14
  <div
9
15
  data-slot="table-container"
10
- className="relative w-full overflow-x-auto"
16
+ className={cn("relative w-full overflow-x-auto", containerClassName)}
11
17
  >
12
18
  <table
13
19
  data-slot="table"
@@ -69,7 +75,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
69
75
  <th
70
76
  data-slot="table-head"
71
77
  className={cn(
72
- "h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
78
+ "h-12 px-3 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
73
79
  className,
74
80
  )}
75
81
  {...props}
@@ -82,7 +88,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
82
88
  <td
83
89
  data-slot="table-cell"
84
90
  className={cn(
85
- "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
91
+ "p-3 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
86
92
  className,
87
93
  )}
88
94
  {...props}
@@ -23,7 +23,7 @@ function Tabs({
23
23
  }
24
24
 
25
25
  const tabsListVariants = cva(
26
- "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
26
+ "group/tabs-list inline-flex w-fit items-center justify-center rounded-full p-1 text-muted-foreground group-data-horizontal/tabs:h-9 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col group-data-vertical/tabs:rounded-2xl data-[variant=line]:rounded-none",
27
27
  {
28
28
  variants: {
29
29
  variant: {
@@ -57,7 +57,7 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
57
57
  <TabsPrimitive.Tab
58
58
  data-slot="tabs-trigger"
59
59
  className={cn(
60
- "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
60
+ "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-2 rounded-full border border-transparent! px-3 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start group-data-vertical/tabs:rounded-2xl group-data-vertical/tabs:px-3 group-data-vertical/tabs:py-1.5 hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
61
61
  "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
62
62
  "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
63
63
  "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "cn";
5
+
6
+ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
7
+ return (
8
+ <textarea
9
+ data-slot="textarea"
10
+ className={cn(
11
+ "flex field-sizing-content min-h-16 w-full resize-none rounded-2xl border border-transparent bg-input/50 px-3 py-3 text-base transition-[color,box-shadow,background-color] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
12
+ className,
13
+ )}
14
+ {...props}
15
+ />
16
+ );
17
+ }
18
+
19
+ export { Textarea };
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
5
+ import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group";
6
+ import { type VariantProps } from "class-variance-authority";
7
+ import { cn } from "cn";
8
+
9
+ import { toggleVariants } from "./toggle";
10
+
11
+ const ToggleGroupContext = React.createContext<
12
+ VariantProps<typeof toggleVariants> & {
13
+ spacing?: number;
14
+ orientation?: "horizontal" | "vertical";
15
+ }
16
+ >({
17
+ size: "default",
18
+ variant: "default",
19
+ spacing: 2,
20
+ orientation: "horizontal",
21
+ });
22
+
23
+ function ToggleGroup({
24
+ className,
25
+ variant,
26
+ size,
27
+ spacing = 2,
28
+ orientation = "horizontal",
29
+ children,
30
+ ...props
31
+ }: ToggleGroupPrimitive.Props &
32
+ VariantProps<typeof toggleVariants> & {
33
+ spacing?: number;
34
+ orientation?: "horizontal" | "vertical";
35
+ }) {
36
+ return (
37
+ <ToggleGroupPrimitive
38
+ data-slot="toggle-group"
39
+ data-variant={variant}
40
+ data-size={size}
41
+ data-spacing={spacing}
42
+ data-orientation={orientation}
43
+ style={{ "--gap": spacing } as React.CSSProperties}
44
+ className={cn(
45
+ "group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[spacing=0]:data-[variant=outline]:rounded-3xl data-vertical:flex-col data-vertical:items-stretch",
46
+ className,
47
+ )}
48
+ {...props}
49
+ >
50
+ <ToggleGroupContext.Provider
51
+ value={{ variant, size, spacing, orientation }}
52
+ >
53
+ {children}
54
+ </ToggleGroupContext.Provider>
55
+ </ToggleGroupPrimitive>
56
+ );
57
+ }
58
+
59
+ function ToggleGroupItem({
60
+ className,
61
+ children,
62
+ variant = "default",
63
+ size = "default",
64
+ ...props
65
+ }: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
66
+ const context = React.useContext(ToggleGroupContext);
67
+
68
+ return (
69
+ <TogglePrimitive
70
+ data-slot="toggle-group-item"
71
+ data-variant={context.variant || variant}
72
+ data-size={context.size || size}
73
+ data-spacing={context.spacing}
74
+ className={cn(
75
+ "shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-3 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-2.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-2.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-3xl group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-3xl group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-3xl group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-3xl data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t",
76
+ toggleVariants({
77
+ variant: context.variant || variant,
78
+ size: context.size || size,
79
+ }),
80
+ className,
81
+ )}
82
+ {...props}
83
+ >
84
+ {children}
85
+ </TogglePrimitive>
86
+ );
87
+ }
88
+
89
+ export { ToggleGroup, ToggleGroupItem };
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { cn } from "cn";
6
+
7
+ const toggleVariants = cva(
8
+ "group/toggle inline-flex items-center justify-center gap-1 rounded-3xl text-sm font-medium whitespace-nowrap transition-colors outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-transparent",
13
+ outline: "border border-input bg-transparent hover:bg-muted",
14
+ },
15
+ size: {
16
+ default:
17
+ "h-9 min-w-9 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5",
18
+ sm: "h-8 min-w-8 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
19
+ lg: "h-10 min-w-10 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ size: "default",
25
+ },
26
+ },
27
+ );
28
+
29
+ function Toggle({
30
+ className,
31
+ variant = "default",
32
+ size = "default",
33
+ ...props
34
+ }: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
35
+ return (
36
+ <TogglePrimitive
37
+ data-slot="toggle"
38
+ className={cn(toggleVariants({ variant, size, className }))}
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+
44
+ export { Toggle, toggleVariants };
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
4
+ import { cn } from "cn";
5
+
6
+ function TooltipProvider({
7
+ delay = 0,
8
+ ...props
9
+ }: TooltipPrimitive.Provider.Props) {
10
+ return (
11
+ <TooltipPrimitive.Provider
12
+ data-slot="tooltip-provider"
13
+ delay={delay}
14
+ {...props}
15
+ />
16
+ );
17
+ }
18
+
19
+ function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
20
+ return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
21
+ }
22
+
23
+ function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
24
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
25
+ }
26
+
27
+ function TooltipContent({
28
+ className,
29
+ side = "top",
30
+ sideOffset = 4,
31
+ align = "center",
32
+ alignOffset = 0,
33
+ children,
34
+ ...props
35
+ }: TooltipPrimitive.Popup.Props &
36
+ Pick<
37
+ TooltipPrimitive.Positioner.Props,
38
+ "align" | "alignOffset" | "side" | "sideOffset"
39
+ >) {
40
+ return (
41
+ <TooltipPrimitive.Portal>
42
+ <TooltipPrimitive.Positioner
43
+ align={align}
44
+ alignOffset={alignOffset}
45
+ side={side}
46
+ sideOffset={sideOffset}
47
+ className="isolate z-50"
48
+ >
49
+ <TooltipPrimitive.Popup
50
+ data-slot="tooltip-content"
51
+ className={cn(
52
+ "z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-xl bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-lg data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
53
+ className,
54
+ )}
55
+ {...props}
56
+ >
57
+ {children}
58
+ <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:translate-x-[1.5px] data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:translate-x-[-1.5px] data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:translate-x-[-1.5px] data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:translate-x-[1.5px] data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
59
+ </TooltipPrimitive.Popup>
60
+ </TooltipPrimitive.Positioner>
61
+ </TooltipPrimitive.Portal>
62
+ );
63
+ }
64
+
65
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ const MOBILE_BREAKPOINT = 768;
6
+ const query = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`;
7
+
8
+ function subscribe(onChange: () => void) {
9
+ const mql = window.matchMedia(query);
10
+ mql.addEventListener("change", onChange);
11
+ return () => mql.removeEventListener("change", onChange);
12
+ }
13
+
14
+ export function useIsMobile() {
15
+ return React.useSyncExternalStore(
16
+ subscribe,
17
+ () => window.matchMedia(query).matches,
18
+ () => false,
19
+ );
20
+ }
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, type ReactNode } from "react";
4
+
5
+ export const locales = ["en", "pt-BR"] as const;
6
+ export type Locale = (typeof locales)[number];
7
+ export type Messages<T> = Record<Locale, T>;
8
+
9
+ /** Declares a message catalog; every locale must provide every key. */
10
+ export function defineMessages<T>(messages: Messages<T>) {
11
+ return messages;
12
+ }
13
+
14
+ const LocaleContext = createContext<Locale>("en");
15
+
16
+ export function LocaleProvider({
17
+ locale,
18
+ children,
19
+ }: {
20
+ locale: Locale;
21
+ children: ReactNode;
22
+ }) {
23
+ return (
24
+ <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
25
+ );
26
+ }
27
+
28
+ export function useLocale() {
29
+ return useContext(LocaleContext);
30
+ }
31
+
32
+ export function useMessages<T>(messages: Messages<T>): T {
33
+ return messages[useLocale()];
34
+ }
35
+
36
+ /** Picks the supported locale that best matches a list such as navigator.languages. */
37
+ export function matchLocale(preferred: readonly string[]): Locale {
38
+ for (const tag of preferred) {
39
+ const lower = tag.toLowerCase();
40
+ if (lower.startsWith("pt")) return "pt-BR";
41
+ if (lower.startsWith("en")) return "en";
42
+ }
43
+ return "en";
44
+ }
45
+
46
+ export const uiMessages = defineMessages({
47
+ en: {
48
+ sidebar: "Sidebar",
49
+ toggleSidebar: "Toggle sidebar",
50
+ collapseSidebar: "Collapse sidebar",
51
+ expandSidebar: "Expand sidebar",
52
+ resizeSidebar: "Resize sidebar",
53
+ modeOpen: "Expanded",
54
+ modeCompact: "Compact",
55
+ modeClosed: "Hidden",
56
+ sidebarHint: (shortcut: string) =>
57
+ `Right-click for options · ${shortcut} hides`,
58
+ search: "Search",
59
+ noResults: "No results",
60
+ noData: "Nothing here yet",
61
+ clearSearch: "Clear search",
62
+ retry: "Retry",
63
+ loadFailed: "Couldn't load data",
64
+ staleData: "Showing data from the last successful load",
65
+ previous: "Previous",
66
+ next: "Next",
67
+ open: "Open",
68
+ page: (page: number, total: number) => `Page ${page} of ${total}`,
69
+ rows: (count: number) => `${count} ${count === 1 ? "row" : "rows"}`,
70
+ close: "Close",
71
+ language: "Language",
72
+ toggleTheme: "Toggle theme",
73
+ more: "More",
74
+ loading: "Loading",
75
+ },
76
+ "pt-BR": {
77
+ sidebar: "Barra lateral",
78
+ toggleSidebar: "Alternar barra lateral",
79
+ collapseSidebar: "Recolher barra lateral",
80
+ expandSidebar: "Expandir barra lateral",
81
+ resizeSidebar: "Redimensionar barra lateral",
82
+ modeOpen: "Expandida",
83
+ modeCompact: "Compacta",
84
+ modeClosed: "Escondida",
85
+ sidebarHint: (shortcut: string) =>
86
+ `Clique direito para opções · ${shortcut} esconde`,
87
+ search: "Buscar",
88
+ noResults: "Nenhum resultado",
89
+ noData: "Nada por aqui ainda",
90
+ clearSearch: "Limpar busca",
91
+ retry: "Tentar novamente",
92
+ loadFailed: "Não foi possível carregar os dados",
93
+ staleData: "Exibindo dados do último carregamento",
94
+ previous: "Anterior",
95
+ next: "Próxima",
96
+ open: "Abrir",
97
+ page: (page: number, total: number) => `Página ${page} de ${total}`,
98
+ rows: (count: number) => `${count} ${count === 1 ? "linha" : "linhas"}`,
99
+ close: "Fechar",
100
+ language: "Idioma",
101
+ toggleTheme: "Alternar tema",
102
+ more: "Mais",
103
+ loading: "Carregando",
104
+ },
105
+ });
106
+
107
+ export function useUiMessages() {
108
+ return useMessages(uiMessages);
109
+ }