margo-ui 1.0.1 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "margo-ui",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "A small React UI kit built on Tailwind CSS v4 design tokens, themeable through plain CSS custom properties.",
5
5
  "keywords": [
6
6
  "react",
@@ -0,0 +1,32 @@
1
+ .margo-background-glow::before {
2
+ content: "";
3
+ position: absolute;
4
+ inset: 0;
5
+ z-index: -1;
6
+ pointer-events: none;
7
+ opacity: 0;
8
+ border-radius: inherit;
9
+ background: radial-gradient(
10
+ var(--margo-background-glow-size) circle at var(--margo-background-glow-x, 50%) var(--margo-background-glow-y, 50%),
11
+ var(--color-primary),
12
+ transparent 70%
13
+ );
14
+ transition: opacity 300ms ease-out;
15
+ }
16
+
17
+ .margo-background-glow:hover::before,
18
+ .margo-background-glow:focus-within::before {
19
+ opacity: var(--margo-background-glow-opacity, 0.1);
20
+ }
21
+
22
+ .margo-background-glow--size-sm {
23
+ --margo-background-glow-size: 200px;
24
+ }
25
+
26
+ .margo-background-glow--size-md {
27
+ --margo-background-glow-size: 320px;
28
+ }
29
+
30
+ .margo-background-glow--size-lg {
31
+ --margo-background-glow-size: 480px;
32
+ }
@@ -0,0 +1,45 @@
1
+ import { cloneElement } from "react";
2
+
3
+ import { cn } from "../helpers/cn";
4
+
5
+ import type { PointerEvent, PointerEventHandler, ReactElement } from "react";
6
+
7
+ import "./background_glow.css";
8
+
9
+ export type BackgroundGlowSize = "sm" | "md" | "lg";
10
+
11
+ export type BackgroundGlowChildProps = {
12
+ className?: string;
13
+ onPointerMove?: PointerEventHandler<HTMLElement>;
14
+ };
15
+
16
+ export type BackgroundGlowProps = {
17
+ children: ReactElement<BackgroundGlowChildProps>;
18
+ size?: BackgroundGlowSize;
19
+ };
20
+
21
+ const BACKGROUND_GLOW_SIZE_CLASS: Record<BackgroundGlowSize, string> = {
22
+ sm: "margo-background-glow--size-sm",
23
+ md: "margo-background-glow--size-md",
24
+ lg: "margo-background-glow--size-lg",
25
+ };
26
+
27
+ export const BackgroundGlow = ({ children, size = "md" }: BackgroundGlowProps) => {
28
+ const handlePointerMove = (event: PointerEvent<HTMLElement>) => {
29
+ const bounds = event.currentTarget.getBoundingClientRect();
30
+
31
+ event.currentTarget.style.setProperty("--margo-background-glow-x", `${event.clientX - bounds.left}px`);
32
+ event.currentTarget.style.setProperty("--margo-background-glow-y", `${event.clientY - bounds.top}px`);
33
+
34
+ children.props.onPointerMove?.(event);
35
+ };
36
+
37
+ return cloneElement(children, {
38
+ className: cn(
39
+ "relative isolate margo-background-glow",
40
+ BACKGROUND_GLOW_SIZE_CLASS[size],
41
+ children.props.className,
42
+ ),
43
+ onPointerMove: handlePointerMove,
44
+ });
45
+ };
@@ -6,7 +6,7 @@
6
6
  opacity: 0;
7
7
  background: radial-gradient(
8
8
  480px circle at var(--margo-border-glow-x, 70%) var(--margo-border-glow-y, 70%),
9
- var(--color-primary),
9
+ var(--color-primary-darken),
10
10
  transparent 70%
11
11
  );
12
12
  transition: opacity 300ms ease-out;
@@ -2,7 +2,7 @@ import { Tag } from "react-renderable";
2
2
  import { cn } from "../helpers/cn";
3
3
  import { Ripple } from "./ripple";
4
4
 
5
- import type { CSSProperties, ElementType, ReactNode } from "react";
5
+ import type { CSSProperties, ElementType, MouseEvent, MouseEventHandler, ReactNode } from "react";
6
6
  import type { TagProps } from "react-renderable";
7
7
 
8
8
  const CLASSNAME_BASE = `group/button flex h-8 w-fit shrink-0 items-center px-2 rounded-lg border-2 text-on-main
@@ -15,21 +15,31 @@ const CLASSNAME_GRID = "grid items-center gap-0 transition-[grid-template-column
15
15
  const CLASSNAME_SLOT = `min-w-0 overflow-hidden opacity-0 transition-opacity duration-[180ms] ease-in
16
16
  group-hover/button:opacity-100 group-focus-visible/button:opacity-100 group-data-[open=true]/button:opacity-100`;
17
17
 
18
+ export type ButtonIconLabelSide = "start" | "end";
19
+
18
20
  export type ButtonProps<T extends ElementType = "button"> = TagProps<T> & {
19
21
  clickable?: boolean;
20
22
  active?: boolean;
21
23
  open?: boolean;
24
+ blurOnClick?: boolean;
22
25
  };
23
26
 
24
27
  export const Button = <T extends ElementType = "button">(props: ButtonProps<T>) => {
25
- const { as, clickable = true, active = false, open = false, ...restProps } = props;
28
+ const { as, clickable = true, active = false, open = false, blurOnClick = true, ...restProps } = props;
26
29
  const nativeProps = { as: as ?? "button", ...restProps } as TagProps<T>;
30
+ const onClick = props.onClick as MouseEventHandler<HTMLElement> | undefined;
31
+
32
+ const handleClick = (event: MouseEvent<HTMLElement>) => {
33
+ onClick?.(event);
34
+ if (blurOnClick && event.detail > 0) setTimeout(() => event.currentTarget.blur(), 150);
35
+ };
27
36
 
28
37
  return (
29
38
  <Ripple>
30
39
  <Tag
31
40
  {...nativeProps}
32
41
  data-open={open}
42
+ onClick={handleClick}
33
43
  className={cn(
34
44
  CLASSNAME_BASE,
35
45
  active ? "margo-border-gradient-primary" : CLASSNAME_SURFACE,
@@ -61,7 +71,7 @@ Button.IconLabel = ({
61
71
  label: ReactNode;
62
72
  gap?: string;
63
73
  reverse?: boolean;
64
- side?: "start" | "end";
74
+ side?: ButtonIconLabelSide;
65
75
  className?: string | null;
66
76
  }) => {
67
77
  return (
@@ -1,6 +1,5 @@
1
1
  import { Tag } from "react-renderable";
2
2
  import { cn } from "../helpers/cn";
3
- import { BorderGlow } from "./border_glow";
4
3
 
5
4
  import type { ElementType } from "react";
6
5
  import type { TagProps } from "react-renderable";
@@ -12,18 +11,16 @@ export const Card = <T extends ElementType = "div">(props: CardProps<T>) => {
12
11
  const nativeProps = { as: as ?? "div", ...restProps } as TagProps<T>;
13
12
 
14
13
  return (
15
- <BorderGlow>
16
- <Tag
17
- {...nativeProps}
18
- className={cn(
19
- "rounded-lg border-2 border-border bg-main p-5 text-on-main",
20
- `focus:margo-border-gradient-primary focus-visible:outline focus-visible:outline-offset-1
21
- focus-visible:outline-on-main`,
22
- className,
23
- )}
24
- >
25
- {children}
26
- </Tag>
27
- </BorderGlow>
14
+ <Tag
15
+ {...nativeProps}
16
+ className={cn(
17
+ "rounded-lg border-2 border-border bg-main p-5 text-on-main",
18
+ `focus:margo-border-gradient-primary focus-visible:outline focus-visible:outline-offset-1
19
+ focus-visible:outline-on-main`,
20
+ className,
21
+ )}
22
+ >
23
+ {children}
24
+ </Tag>
28
25
  );
29
26
  };
@@ -0,0 +1,27 @@
1
+ import { Tag } from "react-renderable";
2
+ import { cn } from "../helpers/cn";
3
+
4
+ import type { ElementType } from "react";
5
+ import type { TagProps } from "react-renderable";
6
+
7
+ export type ChipProps<T extends ElementType = "div"> = TagProps<T> & {
8
+ active?: boolean;
9
+ };
10
+
11
+ export const Chip = <T extends ElementType = "div">(props: ChipProps<T>) => {
12
+ const { as, active = false, className, children, ...restProps } = props;
13
+ const nativeProps = { as: as ?? "div", ...restProps } as TagProps<T>;
14
+
15
+ return (
16
+ <Tag
17
+ {...nativeProps}
18
+ className={cn(
19
+ "bg-secondary px-3 py-0.5 whitespace-nowrap lowercase flex-none leading-none rounded-md text-mc",
20
+ className,
21
+ active && "bg-on-main text-main",
22
+ )}
23
+ >
24
+ {children}
25
+ </Tag>
26
+ );
27
+ };
@@ -19,7 +19,7 @@ export type RippleProps = {
19
19
  rippleClassName?: string;
20
20
  };
21
21
 
22
- export const Ripple = ({ children, rippleClassName = "bg-primary" }: RippleProps) => {
22
+ export const Ripple = ({ children, rippleClassName = "bg-primary-darken" }: RippleProps) => {
23
23
  const { endRipple, ripple, startRipple } = useRipple();
24
24
 
25
25
  const handlePointerDown = (event: PointerEvent<HTMLElement>) => {
@@ -0,0 +1,55 @@
1
+ import { Tag } from "react-renderable";
2
+ import { cn } from "../helpers/cn";
3
+
4
+ import { useRef } from "react";
5
+
6
+ import type { ElementType, ReactNode } from "react";
7
+ import type { TagProps } from "react-renderable";
8
+
9
+ export type TooltipPosition = "up" | "down" | "left" | "right";
10
+
11
+ export type TooltipProps<T extends ElementType = "div"> = Omit<TagProps<T>, "aria-label" | "children" | "id"> & {
12
+ ariaLabel?: string;
13
+ id?: string;
14
+ placeholder?: ReactNode;
15
+ position?: TooltipPosition;
16
+ children?: ReactNode;
17
+ };
18
+
19
+ const TOOLTIP_POSITION_CLASS: Record<TooltipPosition, string> = {
20
+ up: "bottom-[120%] left-1/2 -translate-x-1/2",
21
+ down: "top-[120%] left-1/2 -translate-x-1/2",
22
+ left: "top-1/2 right-[calc(100%+0.5rem)] -translate-y-1/2",
23
+ right: "top-1/2 left-[calc(100%+0.5rem)] -translate-y-1/2",
24
+ };
25
+
26
+ export const Tooltip = <T extends ElementType = "div">(props: TooltipProps<T>) => {
27
+ const { as, ariaLabel, children, className, id, placeholder, position = "down", ...restProps } = props;
28
+ const nativeProps = { as: as ?? "div", ...restProps } as TagProps<T>;
29
+
30
+ const placeholderRef = useRef<ReactNode>(null);
31
+
32
+ const isFilled = placeholder !== null && placeholder !== undefined;
33
+
34
+ if (isFilled) placeholderRef.current = placeholder;
35
+
36
+ return (
37
+ <Tag {...nativeProps} className={cn("group relative inline-block size-fit", className)}>
38
+ {children}
39
+ <span
40
+ id={id}
41
+ role="tooltip"
42
+ aria-label={isFilled ? undefined : ariaLabel}
43
+ className={cn(
44
+ `pointer-events-none absolute z-10 rounded-sm bg-on-main px-2 py-0.5 text-xs text-main lowercase
45
+ whitespace-nowrap opacity-0`,
46
+ "transition-opacity duration-[250ms] ease-in-out",
47
+ isFilled && "group-hover:opacity-100 group-focus-within:opacity-100",
48
+ TOOLTIP_POSITION_CLASS[position],
49
+ )}
50
+ >
51
+ {placeholderRef.current}
52
+ </span>
53
+ </Tag>
54
+ );
55
+ };
package/src/index.ts CHANGED
@@ -1,16 +1,22 @@
1
+ export { BackgroundGlow } from "./components/background_glow";
1
2
  export { BorderGlow } from "./components/border_glow";
2
3
  export { Button } from "./components/button";
3
4
  export { Card } from "./components/card";
5
+ export { Chip } from "./components/chip";
4
6
  export { MetaColorScheme } from "./components/meta_color_scheme";
5
7
  export { Ripple } from "./components/ripple";
8
+ export { Tooltip } from "./components/tooltip";
6
9
 
7
10
  export { useTheme } from "./hooks/use_theme";
8
11
 
9
12
  export { cn } from "./helpers/cn";
10
13
  export { THEME, themeClient } from "./helpers/theme";
11
14
 
15
+ export type { BackgroundGlowChildProps, BackgroundGlowProps, BackgroundGlowSize } from "./components/background_glow";
12
16
  export type { BorderGlowChildProps, BorderGlowPosition, BorderGlowProps } from "./components/border_glow";
13
- export type { ButtonProps } from "./components/button";
17
+ export type { ButtonIconLabelSide, ButtonProps } from "./components/button";
14
18
  export type { CardProps } from "./components/card";
19
+ export type { ChipProps } from "./components/chip";
15
20
  export type { RippleChildProps, RippleProps } from "./components/ripple";
21
+ export type { TooltipPosition, TooltipProps } from "./components/tooltip";
16
22
  export type { Theme } from "./helpers/theme";