margo-ui 1.0.1 → 1.1.1
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 +5 -7
- package/src/components/button/button.tsx +79 -0
- package/src/components/button/style.ts +27 -0
- package/src/components/button/type.ts +33 -0
- package/src/components/card/card.tsx +13 -0
- package/src/components/card/style.ts +4 -0
- package/src/components/card/type.ts +4 -0
- package/src/components/chip/chip.tsx +13 -0
- package/src/components/chip/style.ts +4 -0
- package/src/components/chip/type.ts +8 -0
- package/src/components/tooltip/style.ts +17 -0
- package/src/components/tooltip/tooltip.tsx +50 -0
- package/src/components/tooltip/type.ts +17 -0
- package/src/hoc/background_glow/background_glow.css +32 -0
- package/src/hoc/background_glow/background_glow.tsx +25 -0
- package/src/hoc/background_glow/style.ts +9 -0
- package/src/hoc/background_glow/type.ts +13 -0
- package/src/{components → hoc/border_glow}/border_glow.css +1 -1
- package/src/hoc/border_glow/border_glow.tsx +27 -0
- package/src/hoc/border_glow/style.ts +11 -0
- package/src/hoc/border_glow/type.ts +13 -0
- package/src/{components → hoc/ripple}/ripple.tsx +6 -15
- package/src/hoc/ripple/type.ts +25 -0
- package/src/{hooks → hoc/ripple}/use_ripple.ts +2 -14
- package/src/hooks/use_theme.ts +8 -8
- package/src/index.ts +29 -13
- package/src/utils/theme/theme.ts +26 -0
- package/src/utils/theme/type.ts +12 -0
- package/src/components/border_glow.tsx +0 -45
- package/src/components/button.tsx +0 -88
- package/src/components/card.tsx +0 -29
- package/src/helpers/theme.ts +0 -31
- /package/src/{components → hoc/ripple}/ripple.css +0 -0
- /package/src/{components → meta/meta_color_scheme}/meta_color_scheme.tsx +0 -0
- /package/src/{helpers → utils/cn}/cn.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "margo-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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",
|
|
@@ -36,15 +36,13 @@
|
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"ts:check": "tsc --noEmit",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"release:version": "changeset version",
|
|
43
|
-
"release": "npm run ts:check && changeset publish"
|
|
39
|
+
"changeset:add": "changeset",
|
|
40
|
+
"changeset:bump": "changeset version",
|
|
41
|
+
"build:release": "npm run ts:check && npm pack && changeset publish && git push --follow-tags"
|
|
44
42
|
},
|
|
45
43
|
"dependencies": {
|
|
46
44
|
"clsx": "^2.1.1",
|
|
47
|
-
"react-renderable": "^1.0.
|
|
45
|
+
"react-renderable": "^1.0.4",
|
|
48
46
|
"tailwind-merge": "^3.6.0"
|
|
49
47
|
},
|
|
50
48
|
"peerDependencies": {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Tag } from "react-renderable";
|
|
2
|
+
|
|
3
|
+
import { Ripple } from "../../hoc/ripple/ripple.js";
|
|
4
|
+
import { cn } from "../../utils/cn/cn.js";
|
|
5
|
+
import {
|
|
6
|
+
buttonActiveClassName,
|
|
7
|
+
buttonBaseClassName,
|
|
8
|
+
buttonGridClassName,
|
|
9
|
+
buttonGridForwardClassName,
|
|
10
|
+
buttonGridReverseClassName,
|
|
11
|
+
buttonIconClassName,
|
|
12
|
+
buttonLabelClassName,
|
|
13
|
+
buttonSlotClassName,
|
|
14
|
+
buttonSurfaceClassName,
|
|
15
|
+
} from "./style.js";
|
|
16
|
+
|
|
17
|
+
import type { CSSProperties, ElementType, MouseEvent } from "react";
|
|
18
|
+
import type { ButtonIconLabelProps, ButtonIconProps, ButtonLabelProps, ButtonProps } from "./type.js";
|
|
19
|
+
|
|
20
|
+
export const Button = <T extends ElementType = "button">({
|
|
21
|
+
clickable = true,
|
|
22
|
+
active = false,
|
|
23
|
+
open = false,
|
|
24
|
+
blurOnClick = true,
|
|
25
|
+
className,
|
|
26
|
+
onClick,
|
|
27
|
+
...rest
|
|
28
|
+
}: ButtonProps<T>) => {
|
|
29
|
+
const handleClick = (event: MouseEvent<HTMLElement>) => {
|
|
30
|
+
onClick?.(event);
|
|
31
|
+
if (blurOnClick && event.detail > 0) setTimeout(() => event.currentTarget.blur(), 150);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Ripple>
|
|
36
|
+
<Tag
|
|
37
|
+
{...Tag.forward<T>(rest, "button")}
|
|
38
|
+
data-open={open}
|
|
39
|
+
onClick={handleClick}
|
|
40
|
+
className={cn(
|
|
41
|
+
buttonBaseClassName,
|
|
42
|
+
active ? buttonActiveClassName : buttonSurfaceClassName,
|
|
43
|
+
!clickable && "pointer-events-none",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
/>
|
|
47
|
+
</Ripple>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
Button.Icon = ({ icon, className = null }: ButtonIconProps) => (
|
|
52
|
+
<span className={cn(buttonIconClassName, className)}>{icon}</span>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
Button.Label = ({ label, className = null }: ButtonLabelProps) => (
|
|
56
|
+
<span className={cn(buttonLabelClassName, className)}>{label}</span>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
Button.IconLabel = ({
|
|
60
|
+
icon,
|
|
61
|
+
label,
|
|
62
|
+
gap = "0.5rem",
|
|
63
|
+
reverse = false,
|
|
64
|
+
side = "end",
|
|
65
|
+
className = null,
|
|
66
|
+
}: ButtonIconLabelProps) => (
|
|
67
|
+
<span
|
|
68
|
+
style={{ "--button-gap": gap } as CSSProperties}
|
|
69
|
+
className={cn(
|
|
70
|
+
buttonGridClassName,
|
|
71
|
+
side === "start" && "[direction:rtl]",
|
|
72
|
+
reverse ? buttonGridReverseClassName : buttonGridForwardClassName,
|
|
73
|
+
className,
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
{reverse ? label : icon}
|
|
77
|
+
<span className={buttonSlotClassName}>{reverse ? icon : label}</span>
|
|
78
|
+
</span>
|
|
79
|
+
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const buttonBaseClassName = `group/button flex h-8 w-fit shrink-0 items-center px-2 rounded-lg border-2 text-on-main
|
|
2
|
+
hover:border-on-main hover:text-on-main focus-visible:margo-border-gradient-primary
|
|
3
|
+
focus-visible:outline focus-visible:outline-offset-1 focus-visible:outline-on-main`;
|
|
4
|
+
|
|
5
|
+
export const buttonSurfaceClassName = "border-border bg-main";
|
|
6
|
+
|
|
7
|
+
export const buttonActiveClassName = "margo-border-gradient-primary";
|
|
8
|
+
|
|
9
|
+
export const buttonIconClassName =
|
|
10
|
+
"relative z-10 flex size-4 min-w-0 shrink-0 items-center justify-center [direction:ltr]";
|
|
11
|
+
|
|
12
|
+
export const buttonLabelClassName =
|
|
13
|
+
"relative z-10 flex items-center text-sm whitespace-nowrap lowercase [direction:ltr] px-0.5";
|
|
14
|
+
|
|
15
|
+
export const buttonGridClassName =
|
|
16
|
+
"grid items-center gap-0 transition-[grid-template-columns,gap] duration-[180ms] ease-in-out";
|
|
17
|
+
|
|
18
|
+
export const buttonGridForwardClassName = `grid-cols-[1rem_0fr] group-hover/button:grid-cols-[1rem_1fr] group-hover/button:gap-(--button-gap)
|
|
19
|
+
group-focus-visible/button:grid-cols-[1rem_1fr] group-focus-visible/button:gap-(--button-gap)
|
|
20
|
+
group-data-[open=true]/button:grid-cols-[1rem_1fr] group-data-[open=true]/button:gap-(--button-gap)`;
|
|
21
|
+
|
|
22
|
+
export const buttonGridReverseClassName = `grid-cols-[auto_0rem] group-hover/button:grid-cols-[auto_1rem] group-hover/button:gap-(--button-gap)
|
|
23
|
+
group-focus-visible/button:grid-cols-[auto_1rem] group-focus-visible/button:gap-(--button-gap)
|
|
24
|
+
group-data-[open=true]/button:grid-cols-[auto_1rem] group-data-[open=true]/button:gap-(--button-gap)`;
|
|
25
|
+
|
|
26
|
+
export const buttonSlotClassName = `min-w-0 overflow-hidden opacity-0 transition-opacity duration-[180ms] ease-in
|
|
27
|
+
group-hover/button:opacity-100 group-focus-visible/button:opacity-100 group-data-[open=true]/button:opacity-100`;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { TagProps } from "react-renderable";
|
|
2
|
+
import type { ElementType, MouseEventHandler, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export type ButtonIconLabelSide = "start" | "end";
|
|
5
|
+
|
|
6
|
+
export type ButtonOwnProps = {
|
|
7
|
+
clickable?: boolean;
|
|
8
|
+
active?: boolean;
|
|
9
|
+
open?: boolean;
|
|
10
|
+
blurOnClick?: boolean;
|
|
11
|
+
onClick?: MouseEventHandler<HTMLElement>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ButtonProps<T extends ElementType = "button"> = TagProps<T, ButtonOwnProps>;
|
|
15
|
+
|
|
16
|
+
export type ButtonIconProps = {
|
|
17
|
+
icon: ReactNode;
|
|
18
|
+
className?: string | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ButtonLabelProps = {
|
|
22
|
+
label: string;
|
|
23
|
+
className?: string | null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ButtonIconLabelProps = {
|
|
27
|
+
icon: ReactNode;
|
|
28
|
+
label: ReactNode;
|
|
29
|
+
gap?: string;
|
|
30
|
+
reverse?: boolean;
|
|
31
|
+
side?: ButtonIconLabelSide;
|
|
32
|
+
className?: string | null;
|
|
33
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Tag } from "react-renderable";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../utils/cn/cn.js";
|
|
4
|
+
import { cardBaseClassName, cardFocusClassName } from "./style.js";
|
|
5
|
+
|
|
6
|
+
import type { ElementType } from "react";
|
|
7
|
+
import type { CardProps } from "./type.js";
|
|
8
|
+
|
|
9
|
+
export const Card = <T extends ElementType = "div">({ className, children, ...rest }: CardProps<T>) => (
|
|
10
|
+
<Tag {...Tag.forward<T>(rest)} className={cn(cardBaseClassName, cardFocusClassName, className)}>
|
|
11
|
+
{children}
|
|
12
|
+
</Tag>
|
|
13
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Tag } from "react-renderable";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../utils/cn/cn.js";
|
|
4
|
+
import { chipActiveClassName, chipBaseClassName } from "./style.js";
|
|
5
|
+
|
|
6
|
+
import type { ElementType } from "react";
|
|
7
|
+
import type { ChipProps } from "./type.js";
|
|
8
|
+
|
|
9
|
+
export const Chip = <T extends ElementType = "div">({ active = false, className, children, ...rest }: ChipProps<T>) => (
|
|
10
|
+
<Tag {...Tag.forward<T>(rest)} className={cn(chipBaseClassName, className, active && chipActiveClassName)}>
|
|
11
|
+
{children}
|
|
12
|
+
</Tag>
|
|
13
|
+
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { TooltipPosition } from "./type.js";
|
|
2
|
+
|
|
3
|
+
export const tooltipAnchorClassName = "group relative inline-block size-fit";
|
|
4
|
+
|
|
5
|
+
export const tooltipBubbleClassName = `pointer-events-none absolute z-10 rounded-sm bg-on-main px-2 py-0.5 text-xs text-main lowercase
|
|
6
|
+
whitespace-nowrap opacity-0`;
|
|
7
|
+
|
|
8
|
+
export const tooltipTransitionClassName = "transition-opacity duration-[250ms] ease-in-out";
|
|
9
|
+
|
|
10
|
+
export const tooltipVisibleClassName = "group-hover:opacity-100 group-focus-within:opacity-100";
|
|
11
|
+
|
|
12
|
+
export const tooltipPositionClassName: Record<TooltipPosition, string> = {
|
|
13
|
+
up: "bottom-[120%] left-1/2 -translate-x-1/2",
|
|
14
|
+
down: "top-[120%] left-1/2 -translate-x-1/2",
|
|
15
|
+
left: "top-1/2 right-[calc(100%+0.5rem)] -translate-y-1/2",
|
|
16
|
+
right: "top-1/2 left-[calc(100%+0.5rem)] -translate-y-1/2",
|
|
17
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
|
|
3
|
+
import { Tag } from "react-renderable";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../utils/cn/cn.js";
|
|
6
|
+
import {
|
|
7
|
+
tooltipAnchorClassName,
|
|
8
|
+
tooltipBubbleClassName,
|
|
9
|
+
tooltipPositionClassName,
|
|
10
|
+
tooltipTransitionClassName,
|
|
11
|
+
tooltipVisibleClassName,
|
|
12
|
+
} from "./style.js";
|
|
13
|
+
|
|
14
|
+
import type { ElementType, ReactNode } from "react";
|
|
15
|
+
import type { TooltipProps } from "./type.js";
|
|
16
|
+
|
|
17
|
+
export const Tooltip = <T extends ElementType = "div">({
|
|
18
|
+
ariaLabel,
|
|
19
|
+
children,
|
|
20
|
+
className,
|
|
21
|
+
id,
|
|
22
|
+
placeholder,
|
|
23
|
+
position = "down",
|
|
24
|
+
...rest
|
|
25
|
+
}: TooltipProps<T>) => {
|
|
26
|
+
const placeholderRef = useRef<ReactNode>(null);
|
|
27
|
+
|
|
28
|
+
const isFilled = placeholder !== null && placeholder !== undefined;
|
|
29
|
+
|
|
30
|
+
if (isFilled) placeholderRef.current = placeholder;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Tag {...Tag.forward<T>(rest)} className={cn(tooltipAnchorClassName, className)}>
|
|
34
|
+
{children}
|
|
35
|
+
<span
|
|
36
|
+
id={id}
|
|
37
|
+
role="tooltip"
|
|
38
|
+
aria-label={isFilled ? undefined : ariaLabel}
|
|
39
|
+
className={cn(
|
|
40
|
+
tooltipBubbleClassName,
|
|
41
|
+
tooltipTransitionClassName,
|
|
42
|
+
isFilled && tooltipVisibleClassName,
|
|
43
|
+
tooltipPositionClassName[position],
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
{placeholderRef.current}
|
|
47
|
+
</span>
|
|
48
|
+
</Tag>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { TagProps } from "react-renderable";
|
|
2
|
+
import type { ElementType, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export type TooltipPosition = "up" | "down" | "left" | "right";
|
|
5
|
+
|
|
6
|
+
export type TooltipOwnProps = {
|
|
7
|
+
ariaLabel?: string;
|
|
8
|
+
id?: string;
|
|
9
|
+
placeholder?: ReactNode;
|
|
10
|
+
position?: TooltipPosition;
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type TooltipProps<T extends ElementType = "div"> = Omit<
|
|
15
|
+
TagProps<T, TooltipOwnProps>,
|
|
16
|
+
"aria-label"
|
|
17
|
+
>;
|
|
@@ -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,25 @@
|
|
|
1
|
+
import { cloneElement } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../utils/cn/cn.js";
|
|
4
|
+
import { backgroundGlowBaseClassName, backgroundGlowSizeClassName } from "./style.js";
|
|
5
|
+
|
|
6
|
+
import type { PointerEvent } from "react";
|
|
7
|
+
import type { BackgroundGlowProps } from "./type.js";
|
|
8
|
+
|
|
9
|
+
import "./background_glow.css";
|
|
10
|
+
|
|
11
|
+
export const BackgroundGlow = ({ children, size = "md" }: BackgroundGlowProps) => {
|
|
12
|
+
const handlePointerMove = (event: PointerEvent<HTMLElement>) => {
|
|
13
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
14
|
+
|
|
15
|
+
event.currentTarget.style.setProperty("--margo-background-glow-x", `${event.clientX - bounds.left}px`);
|
|
16
|
+
event.currentTarget.style.setProperty("--margo-background-glow-y", `${event.clientY - bounds.top}px`);
|
|
17
|
+
|
|
18
|
+
children.props.onPointerMove?.(event);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return cloneElement(children, {
|
|
22
|
+
className: cn(backgroundGlowBaseClassName, backgroundGlowSizeClassName[size], children.props.className),
|
|
23
|
+
onPointerMove: handlePointerMove,
|
|
24
|
+
});
|
|
25
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BackgroundGlowSize } from "./type.js";
|
|
2
|
+
|
|
3
|
+
export const backgroundGlowBaseClassName = "relative isolate margo-background-glow";
|
|
4
|
+
|
|
5
|
+
export const backgroundGlowSizeClassName: Record<BackgroundGlowSize, string> = {
|
|
6
|
+
sm: "margo-background-glow--size-sm",
|
|
7
|
+
md: "margo-background-glow--size-md",
|
|
8
|
+
lg: "margo-background-glow--size-lg",
|
|
9
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PointerEventHandler, ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
export type BackgroundGlowSize = "sm" | "md" | "lg";
|
|
4
|
+
|
|
5
|
+
export type BackgroundGlowChildProps = {
|
|
6
|
+
className?: string;
|
|
7
|
+
onPointerMove?: PointerEventHandler<HTMLElement>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type BackgroundGlowProps = {
|
|
11
|
+
children: ReactElement<BackgroundGlowChildProps>;
|
|
12
|
+
size?: BackgroundGlowSize;
|
|
13
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { cloneElement } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../utils/cn/cn.js";
|
|
4
|
+
import { borderGlowBaseClassName, borderGlowPositionClassName } from "./style.js";
|
|
5
|
+
|
|
6
|
+
import type { PointerEvent } from "react";
|
|
7
|
+
import type { BorderGlowProps } from "./type.js";
|
|
8
|
+
|
|
9
|
+
import "./border_glow.css";
|
|
10
|
+
|
|
11
|
+
export const BorderGlow = ({ children, position = "all" }: BorderGlowProps) => {
|
|
12
|
+
const handlePointerMove = (event: PointerEvent<HTMLElement>) => {
|
|
13
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
14
|
+
const x = event.clientX - bounds.left;
|
|
15
|
+
const y = event.clientY - bounds.top;
|
|
16
|
+
|
|
17
|
+
event.currentTarget.style.setProperty("--margo-border-glow-x", `${position === "right" ? bounds.width - x : x}px`);
|
|
18
|
+
event.currentTarget.style.setProperty("--margo-border-glow-y", `${position === "down" ? bounds.height - y : y}px`);
|
|
19
|
+
|
|
20
|
+
children.props.onPointerMove?.(event);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return cloneElement(children, {
|
|
24
|
+
className: cn(borderGlowBaseClassName, borderGlowPositionClassName[position], children.props.className),
|
|
25
|
+
onPointerMove: handlePointerMove,
|
|
26
|
+
});
|
|
27
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BorderGlowPosition } from "./type.js";
|
|
2
|
+
|
|
3
|
+
export const borderGlowBaseClassName = "relative isolate margo-border-glow";
|
|
4
|
+
|
|
5
|
+
export const borderGlowPositionClassName: Record<BorderGlowPosition, string> = {
|
|
6
|
+
up: "margo-border-glow--edge-top",
|
|
7
|
+
down: "margo-border-glow--edge-bottom",
|
|
8
|
+
left: "margo-border-glow--edge-left",
|
|
9
|
+
right: "margo-border-glow--edge-right",
|
|
10
|
+
all: "margo-border-glow--ring",
|
|
11
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PointerEventHandler, ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
export type BorderGlowPosition = "up" | "down" | "left" | "right" | "all";
|
|
4
|
+
|
|
5
|
+
export type BorderGlowChildProps = {
|
|
6
|
+
className?: string;
|
|
7
|
+
onPointerMove?: PointerEventHandler<HTMLElement>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type BorderGlowProps = {
|
|
11
|
+
children: ReactElement<BorderGlowChildProps>;
|
|
12
|
+
position?: BorderGlowPosition;
|
|
13
|
+
};
|
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
import { cloneElement } from "react";
|
|
2
2
|
|
|
3
3
|
import { Guard } from "react-renderable";
|
|
4
|
-
import { cn } from "../helpers/cn";
|
|
5
|
-
import { useRipple } from "../hooks/use_ripple";
|
|
6
4
|
|
|
7
|
-
import
|
|
5
|
+
import { cn } from "../../utils/cn/cn.js";
|
|
6
|
+
import { useRipple } from "./use_ripple.js";
|
|
8
7
|
|
|
9
|
-
import "
|
|
10
|
-
|
|
11
|
-
export type RippleChildProps = {
|
|
12
|
-
children?: ReactNode;
|
|
13
|
-
className?: string;
|
|
14
|
-
onPointerDown?: PointerEventHandler<HTMLElement>;
|
|
15
|
-
};
|
|
8
|
+
import type { PointerEvent } from "react";
|
|
9
|
+
import type { RippleProps } from "./type.js";
|
|
16
10
|
|
|
17
|
-
|
|
18
|
-
children: ReactElement<RippleChildProps>;
|
|
19
|
-
rippleClassName?: string;
|
|
20
|
-
};
|
|
11
|
+
import "./ripple.css";
|
|
21
12
|
|
|
22
|
-
export const Ripple = ({ children, rippleClassName = "bg-primary" }: RippleProps) => {
|
|
13
|
+
export const Ripple = ({ children, rippleClassName = "bg-primary-darken" }: RippleProps) => {
|
|
23
14
|
const { endRipple, ripple, startRipple } = useRipple();
|
|
24
15
|
|
|
25
16
|
const handlePointerDown = (event: PointerEvent<HTMLElement>) => {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PointerEvent, PointerEventHandler, ReactElement, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type RippleChildProps = {
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
onPointerDown?: PointerEventHandler<HTMLElement>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type RippleProps = {
|
|
10
|
+
children: ReactElement<RippleChildProps>;
|
|
11
|
+
rippleClassName?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type RippleState = {
|
|
15
|
+
id: number;
|
|
16
|
+
size: number;
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type UseRipple = {
|
|
22
|
+
ripple: RippleState | null;
|
|
23
|
+
endRipple: () => void;
|
|
24
|
+
startRipple: (event: PointerEvent<HTMLElement>) => void;
|
|
25
|
+
};
|
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
|
|
3
3
|
import type { PointerEvent } from "react";
|
|
4
|
-
|
|
5
|
-
type Ripple = {
|
|
6
|
-
id: number;
|
|
7
|
-
size: number;
|
|
8
|
-
x: number;
|
|
9
|
-
y: number;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
type UseRipple = {
|
|
13
|
-
ripple: Ripple | null;
|
|
14
|
-
endRipple: () => void;
|
|
15
|
-
startRipple: (event: PointerEvent<HTMLElement>) => void;
|
|
16
|
-
};
|
|
4
|
+
import type { RippleState, UseRipple } from "./type.js";
|
|
17
5
|
|
|
18
6
|
export const useRipple = (): UseRipple => {
|
|
19
|
-
const [ripple, setRipple] = useState<
|
|
7
|
+
const [ripple, setRipple] = useState<RippleState | null>(null);
|
|
20
8
|
|
|
21
9
|
const startRipple = (event: PointerEvent<HTMLElement>) => {
|
|
22
10
|
const bounds = event.currentTarget.getBoundingClientRect();
|
package/src/hooks/use_theme.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { margoThemeClient } from "../utils/theme/theme.js";
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type { MargoTheme } from "../utils/theme/type.js";
|
|
6
6
|
|
|
7
|
-
export const
|
|
8
|
-
const [theme, setThemeState] = useState<
|
|
7
|
+
export const useMargoTheme = (): [MargoTheme, (next: MargoTheme | ((current: MargoTheme) => MargoTheme)) => void] => {
|
|
8
|
+
const [theme, setThemeState] = useState<MargoTheme>(() => margoThemeClient.get());
|
|
9
9
|
|
|
10
10
|
useEffect(() => {
|
|
11
11
|
if (typeof document === "undefined") return;
|
|
12
12
|
|
|
13
13
|
const root = document.documentElement;
|
|
14
|
-
setThemeState(
|
|
14
|
+
setThemeState(margoThemeClient.get());
|
|
15
15
|
|
|
16
16
|
const observer = new MutationObserver(() => {
|
|
17
|
-
setThemeState(
|
|
17
|
+
setThemeState(margoThemeClient.get());
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
|
|
@@ -22,8 +22,8 @@ export const useTheme = (): [Theme, (next: Theme | ((current: Theme) => Theme))
|
|
|
22
22
|
return () => observer.disconnect();
|
|
23
23
|
}, []);
|
|
24
24
|
|
|
25
|
-
const setTheme = useCallback((next:
|
|
26
|
-
|
|
25
|
+
const setTheme = useCallback((next: MargoTheme | ((current: MargoTheme) => MargoTheme)) => {
|
|
26
|
+
margoThemeClient.set(typeof next === "function" ? next(margoThemeClient.get()) : next);
|
|
27
27
|
}, []);
|
|
28
28
|
|
|
29
29
|
return [theme, setTheme];
|
package/src/index.ts
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export { Ripple } from "./components/ripple";
|
|
1
|
+
export { Button } from "./components/button/button.js";
|
|
2
|
+
export { Card } from "./components/card/card.js";
|
|
3
|
+
export { Chip } from "./components/chip/chip.js";
|
|
4
|
+
export { Tooltip } from "./components/tooltip/tooltip.js";
|
|
6
5
|
|
|
7
|
-
export {
|
|
6
|
+
export { BackgroundGlow } from "./hoc/background_glow/background_glow.js";
|
|
7
|
+
export { BorderGlow } from "./hoc/border_glow/border_glow.js";
|
|
8
|
+
export { Ripple } from "./hoc/ripple/ripple.js";
|
|
8
9
|
|
|
9
|
-
export {
|
|
10
|
-
export { THEME, themeClient } from "./helpers/theme";
|
|
10
|
+
export { MetaColorScheme } from "./meta/meta_color_scheme/meta_color_scheme.js";
|
|
11
11
|
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
export
|
|
15
|
-
export
|
|
16
|
-
|
|
12
|
+
export { useMargoTheme } from "./hooks/use_theme.js";
|
|
13
|
+
|
|
14
|
+
export { cn } from "./utils/cn/cn.js";
|
|
15
|
+
export { margoTheme, margoThemeClient } from "./utils/theme/theme.js";
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
ButtonIconLabelProps,
|
|
19
|
+
ButtonIconLabelSide,
|
|
20
|
+
ButtonIconProps,
|
|
21
|
+
ButtonLabelProps,
|
|
22
|
+
ButtonProps,
|
|
23
|
+
} from "./components/button/type.js";
|
|
24
|
+
export type { CardProps } from "./components/card/type.js";
|
|
25
|
+
export type { ChipProps } from "./components/chip/type.js";
|
|
26
|
+
export type { TooltipPosition, TooltipProps } from "./components/tooltip/type.js";
|
|
27
|
+
|
|
28
|
+
export type { BackgroundGlowProps, BackgroundGlowSize } from "./hoc/background_glow/type.js";
|
|
29
|
+
export type { BorderGlowPosition, BorderGlowProps } from "./hoc/border_glow/type.js";
|
|
30
|
+
export type { RippleProps } from "./hoc/ripple/type.js";
|
|
31
|
+
|
|
32
|
+
export type { MargoTheme, MargoThemeClient } from "./utils/theme/type.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MargoTheme, MargoThemeClient, MargoThemeConstants } from "./type.js";
|
|
2
|
+
|
|
3
|
+
export const margoTheme: MargoThemeConstants = {
|
|
4
|
+
LIGHT: "light",
|
|
5
|
+
DARK: "dark",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const get = (): MargoTheme => {
|
|
9
|
+
if (typeof document === "undefined") return margoTheme.DARK;
|
|
10
|
+
|
|
11
|
+
return document.documentElement.classList.contains(margoTheme.DARK) ? margoTheme.DARK : margoTheme.LIGHT;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const set = (next: MargoTheme) => {
|
|
15
|
+
if (typeof document === "undefined") return;
|
|
16
|
+
|
|
17
|
+
document.documentElement.classList.toggle(margoTheme.DARK, next === margoTheme.DARK);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const toggle = () => set(get() === margoTheme.DARK ? margoTheme.LIGHT : margoTheme.DARK);
|
|
21
|
+
|
|
22
|
+
export const margoThemeClient: MargoThemeClient = {
|
|
23
|
+
get,
|
|
24
|
+
set,
|
|
25
|
+
toggle,
|
|
26
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type MargoTheme = "light" | "dark";
|
|
2
|
+
|
|
3
|
+
export type MargoThemeConstants = {
|
|
4
|
+
readonly LIGHT: MargoTheme;
|
|
5
|
+
readonly DARK: MargoTheme;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type MargoThemeClient = {
|
|
9
|
+
get: () => MargoTheme;
|
|
10
|
+
set: (next: MargoTheme) => void;
|
|
11
|
+
toggle: () => void;
|
|
12
|
+
};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { cloneElement } from "react";
|
|
2
|
-
|
|
3
|
-
import { cn } from "../helpers/cn";
|
|
4
|
-
|
|
5
|
-
import type { PointerEvent, PointerEventHandler, ReactElement } from "react";
|
|
6
|
-
|
|
7
|
-
import "./border_glow.css";
|
|
8
|
-
|
|
9
|
-
export type BorderGlowPosition = "up" | "down" | "left" | "right" | "all";
|
|
10
|
-
|
|
11
|
-
export type BorderGlowChildProps = {
|
|
12
|
-
className?: string;
|
|
13
|
-
onPointerMove?: PointerEventHandler<HTMLElement>;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export type BorderGlowProps = {
|
|
17
|
-
children: ReactElement<BorderGlowChildProps>;
|
|
18
|
-
position?: BorderGlowPosition;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const BORDER_GLOW_POSITION_CLASS: Record<BorderGlowPosition, string> = {
|
|
22
|
-
up: "margo-border-glow--edge-top",
|
|
23
|
-
down: "margo-border-glow--edge-bottom",
|
|
24
|
-
left: "margo-border-glow--edge-left",
|
|
25
|
-
right: "margo-border-glow--edge-right",
|
|
26
|
-
all: "margo-border-glow--ring",
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export const BorderGlow = ({ children, position = "all" }: BorderGlowProps) => {
|
|
30
|
-
const handlePointerMove = (event: PointerEvent<HTMLElement>) => {
|
|
31
|
-
const bounds = event.currentTarget.getBoundingClientRect();
|
|
32
|
-
const x = event.clientX - bounds.left;
|
|
33
|
-
const y = event.clientY - bounds.top;
|
|
34
|
-
|
|
35
|
-
event.currentTarget.style.setProperty("--margo-border-glow-x", `${position === "right" ? bounds.width - x : x}px`);
|
|
36
|
-
event.currentTarget.style.setProperty("--margo-border-glow-y", `${position === "down" ? bounds.height - y : y}px`);
|
|
37
|
-
|
|
38
|
-
children.props.onPointerMove?.(event);
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
return cloneElement(children, {
|
|
42
|
-
className: cn("relative isolate margo-border-glow", BORDER_GLOW_POSITION_CLASS[position], children.props.className),
|
|
43
|
-
onPointerMove: handlePointerMove,
|
|
44
|
-
});
|
|
45
|
-
};
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { Tag } from "react-renderable";
|
|
2
|
-
import { cn } from "../helpers/cn";
|
|
3
|
-
import { Ripple } from "./ripple";
|
|
4
|
-
|
|
5
|
-
import type { CSSProperties, ElementType, ReactNode } from "react";
|
|
6
|
-
import type { TagProps } from "react-renderable";
|
|
7
|
-
|
|
8
|
-
const CLASSNAME_BASE = `group/button flex h-8 w-fit shrink-0 items-center px-2 rounded-lg border-2 text-on-main
|
|
9
|
-
hover:border-on-main hover:text-on-main focus-visible:margo-border-gradient-primary
|
|
10
|
-
focus-visible:outline focus-visible:outline-offset-1 focus-visible:outline-on-main`;
|
|
11
|
-
const CLASSNAME_SURFACE = "border-border bg-main";
|
|
12
|
-
const CLASSNAME_ICON = "relative z-10 flex size-4 min-w-0 shrink-0 items-center justify-center [direction:ltr]";
|
|
13
|
-
const CLASSNAME_LABEL = "relative z-10 flex items-center text-sm whitespace-nowrap lowercase [direction:ltr] px-0.5";
|
|
14
|
-
const CLASSNAME_GRID = "grid items-center gap-0 transition-[grid-template-columns,gap] duration-[180ms] ease-in-out";
|
|
15
|
-
const CLASSNAME_SLOT = `min-w-0 overflow-hidden opacity-0 transition-opacity duration-[180ms] ease-in
|
|
16
|
-
group-hover/button:opacity-100 group-focus-visible/button:opacity-100 group-data-[open=true]/button:opacity-100`;
|
|
17
|
-
|
|
18
|
-
export type ButtonProps<T extends ElementType = "button"> = TagProps<T> & {
|
|
19
|
-
clickable?: boolean;
|
|
20
|
-
active?: boolean;
|
|
21
|
-
open?: boolean;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export const Button = <T extends ElementType = "button">(props: ButtonProps<T>) => {
|
|
25
|
-
const { as, clickable = true, active = false, open = false, ...restProps } = props;
|
|
26
|
-
const nativeProps = { as: as ?? "button", ...restProps } as TagProps<T>;
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<Ripple>
|
|
30
|
-
<Tag
|
|
31
|
-
{...nativeProps}
|
|
32
|
-
data-open={open}
|
|
33
|
-
className={cn(
|
|
34
|
-
CLASSNAME_BASE,
|
|
35
|
-
active ? "margo-border-gradient-primary" : CLASSNAME_SURFACE,
|
|
36
|
-
!clickable && "pointer-events-none",
|
|
37
|
-
props.className,
|
|
38
|
-
)}
|
|
39
|
-
/>
|
|
40
|
-
</Ripple>
|
|
41
|
-
);
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
Button.Icon = ({ icon, className = null }: { icon: ReactNode; className?: string | null }) => (
|
|
45
|
-
<span className={cn(CLASSNAME_ICON, className)}>{icon}</span>
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
Button.Label = ({ label, className = null }: { label: string; className?: string | null }) => (
|
|
49
|
-
<span className={cn(CLASSNAME_LABEL, className)}>{label}</span>
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
Button.IconLabel = ({
|
|
53
|
-
icon,
|
|
54
|
-
label,
|
|
55
|
-
gap = "0.5rem",
|
|
56
|
-
reverse = false,
|
|
57
|
-
side = "end",
|
|
58
|
-
className = null,
|
|
59
|
-
}: {
|
|
60
|
-
icon: ReactNode;
|
|
61
|
-
label: ReactNode;
|
|
62
|
-
gap?: string;
|
|
63
|
-
reverse?: boolean;
|
|
64
|
-
side?: "start" | "end";
|
|
65
|
-
className?: string | null;
|
|
66
|
-
}) => {
|
|
67
|
-
return (
|
|
68
|
-
<span
|
|
69
|
-
style={{ "--button-gap": gap } as CSSProperties}
|
|
70
|
-
className={cn(
|
|
71
|
-
CLASSNAME_GRID,
|
|
72
|
-
side === "start" && "[direction:rtl]",
|
|
73
|
-
!reverse &&
|
|
74
|
-
`grid-cols-[1rem_0fr] group-hover/button:grid-cols-[1rem_1fr] group-hover/button:gap-(--button-gap)
|
|
75
|
-
group-focus-visible/button:grid-cols-[1rem_1fr] group-focus-visible/button:gap-(--button-gap)
|
|
76
|
-
group-data-[open=true]/button:grid-cols-[1rem_1fr] group-data-[open=true]/button:gap-(--button-gap)`,
|
|
77
|
-
reverse &&
|
|
78
|
-
`grid-cols-[auto_0rem] group-hover/button:grid-cols-[auto_1rem] group-hover/button:gap-(--button-gap)
|
|
79
|
-
group-focus-visible/button:grid-cols-[auto_1rem] group-focus-visible/button:gap-(--button-gap)
|
|
80
|
-
group-data-[open=true]/button:grid-cols-[auto_1rem] group-data-[open=true]/button:gap-(--button-gap)`,
|
|
81
|
-
className,
|
|
82
|
-
)}
|
|
83
|
-
>
|
|
84
|
-
{reverse ? label : icon}
|
|
85
|
-
<span className={CLASSNAME_SLOT}>{reverse ? icon : label}</span>
|
|
86
|
-
</span>
|
|
87
|
-
);
|
|
88
|
-
};
|
package/src/components/card.tsx
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { Tag } from "react-renderable";
|
|
2
|
-
import { cn } from "../helpers/cn";
|
|
3
|
-
import { BorderGlow } from "./border_glow";
|
|
4
|
-
|
|
5
|
-
import type { ElementType } from "react";
|
|
6
|
-
import type { TagProps } from "react-renderable";
|
|
7
|
-
|
|
8
|
-
export type CardProps<T extends ElementType = "div"> = TagProps<T>;
|
|
9
|
-
|
|
10
|
-
export const Card = <T extends ElementType = "div">(props: CardProps<T>) => {
|
|
11
|
-
const { as, className, children, ...restProps } = props;
|
|
12
|
-
const nativeProps = { as: as ?? "div", ...restProps } as TagProps<T>;
|
|
13
|
-
|
|
14
|
-
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>
|
|
28
|
-
);
|
|
29
|
-
};
|
package/src/helpers/theme.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
export type Theme = "light" | "dark";
|
|
2
|
-
|
|
3
|
-
type ThemeConstants = {
|
|
4
|
-
readonly LIGHT: Theme;
|
|
5
|
-
readonly DARK: Theme;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export const THEME: ThemeConstants = {
|
|
9
|
-
LIGHT: "light",
|
|
10
|
-
DARK: "dark",
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const get = (): Theme => {
|
|
14
|
-
if (typeof document === "undefined") return THEME.DARK;
|
|
15
|
-
|
|
16
|
-
return document.documentElement.classList.contains(THEME.DARK) ? THEME.DARK : THEME.LIGHT;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const set = (next: Theme) => {
|
|
20
|
-
if (typeof document === "undefined") return;
|
|
21
|
-
|
|
22
|
-
document.documentElement.classList.toggle(THEME.DARK, next === THEME.DARK);
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const toggle = () => set(get() === THEME.DARK ? THEME.LIGHT : THEME.DARK);
|
|
26
|
-
|
|
27
|
-
export const themeClient = {
|
|
28
|
-
get,
|
|
29
|
-
set,
|
|
30
|
-
toggle,
|
|
31
|
-
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|