margo-ui 1.3.1 → 1.3.3
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/README.md +2 -0
- package/package.json +3 -4
- package/src/components/button/button.tsx +10 -2
- package/src/components/button/style.ts +4 -0
- package/src/components/button/type.ts +1 -0
- package/src/components/header/style.ts +1 -1
- package/src/components/item/item.tsx +18 -4
- package/src/components/item/style.ts +6 -2
- package/src/components/item/type.ts +3 -1
- package/src/components/layer/layer.tsx +28 -3
- package/src/hoc/ripple/ripple.tsx +2 -2
- package/src/hoc/ripple/type.ts +1 -0
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A small React UI kit built on Tailwind CSS v4 design tokens, themeable through plain CSS custom properties.
|
|
4
4
|
|
|
5
|
+
Package: [npmjs.com/package/margo-ui](https://www.npmjs.com/package/margo-ui)
|
|
6
|
+
|
|
5
7
|
Documentation: [dev.robertoattanasio.com/margo-ui](https://dev.robertoattanasio.com/margo-ui)
|
|
6
8
|
|
|
7
9
|
## Install
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "margo-ui",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
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",
|
|
@@ -15,11 +15,10 @@
|
|
|
15
15
|
"homepage": "https://dev.robertoattanasio.com/margo-ui",
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|
|
18
|
-
"url": "git+https://github.com/robertoattanasio/
|
|
19
|
-
"directory": "lib/margo-ui"
|
|
18
|
+
"url": "git+https://github.com/robertoattanasio/margo-ui.git"
|
|
20
19
|
},
|
|
21
20
|
"bugs": {
|
|
22
|
-
"url": "https://github.com/robertoattanasio/
|
|
21
|
+
"url": "https://github.com/robertoattanasio/margo-ui/issues"
|
|
23
22
|
},
|
|
24
23
|
"type": "module",
|
|
25
24
|
"main": "./src/index.ts",
|
|
@@ -5,11 +5,13 @@ import { cn } from "../../utils/cn/cn.js";
|
|
|
5
5
|
import {
|
|
6
6
|
buttonActiveClassName,
|
|
7
7
|
buttonBaseClassName,
|
|
8
|
+
buttonDisabledClassName,
|
|
8
9
|
buttonGridClassName,
|
|
9
10
|
buttonGridForwardClassName,
|
|
10
11
|
buttonGridReverseClassName,
|
|
11
12
|
buttonIconClassName,
|
|
12
13
|
buttonLabelClassName,
|
|
14
|
+
buttonNotClickableClassName,
|
|
13
15
|
buttonSlotClassName,
|
|
14
16
|
buttonSurfaceClassName,
|
|
15
17
|
} from "./style.js";
|
|
@@ -19,6 +21,7 @@ import type { ButtonIconLabelProps, ButtonIconProps, ButtonLabelProps, ButtonPro
|
|
|
19
21
|
|
|
20
22
|
export const Button = <T extends ElementType = "button">({
|
|
21
23
|
clickable = true,
|
|
24
|
+
disabled = false,
|
|
22
25
|
active = false,
|
|
23
26
|
open = false,
|
|
24
27
|
onClickBlur,
|
|
@@ -27,6 +30,7 @@ export const Button = <T extends ElementType = "button">({
|
|
|
27
30
|
...rest
|
|
28
31
|
}: ButtonProps<T>) => {
|
|
29
32
|
const handleClick = (event: MouseEvent<HTMLElement>) => {
|
|
33
|
+
if (disabled) return;
|
|
30
34
|
onClick?.(event);
|
|
31
35
|
if (typeof onClickBlur === "function") {
|
|
32
36
|
const target = event.currentTarget;
|
|
@@ -36,15 +40,19 @@ export const Button = <T extends ElementType = "button">({
|
|
|
36
40
|
};
|
|
37
41
|
|
|
38
42
|
return (
|
|
39
|
-
<Ripple>
|
|
43
|
+
<Ripple disabled={disabled || !clickable}>
|
|
40
44
|
<Tag
|
|
41
45
|
{...Tag.forward<T>(rest, "button")}
|
|
42
46
|
data-margo-open={open}
|
|
47
|
+
data-margo-disabled={disabled}
|
|
48
|
+
aria-disabled={disabled || undefined}
|
|
49
|
+
tabIndex={disabled || !clickable ? -1 : undefined}
|
|
43
50
|
onClick={handleClick}
|
|
44
51
|
className={cn(
|
|
45
52
|
buttonBaseClassName,
|
|
46
53
|
active ? buttonActiveClassName : buttonSurfaceClassName,
|
|
47
|
-
!clickable &&
|
|
54
|
+
!clickable && buttonNotClickableClassName,
|
|
55
|
+
disabled && buttonDisabledClassName,
|
|
48
56
|
className,
|
|
49
57
|
)}
|
|
50
58
|
/>
|
|
@@ -6,6 +6,10 @@ export const buttonSurfaceClassName = "border-border bg-main";
|
|
|
6
6
|
|
|
7
7
|
export const buttonActiveClassName = "margo-border-gradient-primary";
|
|
8
8
|
|
|
9
|
+
export const buttonNotClickableClassName = "pointer-events-none cursor-auto";
|
|
10
|
+
|
|
11
|
+
export const buttonDisabledClassName = "pointer-events-none cursor-auto opacity-40";
|
|
12
|
+
|
|
9
13
|
export const buttonIconClassName =
|
|
10
14
|
"relative z-10 flex size-4 min-w-0 shrink-0 items-center justify-center [direction:ltr]";
|
|
11
15
|
|
|
@@ -6,8 +6,10 @@ import { cn } from "../../utils/cn/cn.js";
|
|
|
6
6
|
import {
|
|
7
7
|
itemActiveClassName,
|
|
8
8
|
itemBaseClassName,
|
|
9
|
+
itemDisabledClassName,
|
|
9
10
|
itemIconClassName,
|
|
10
11
|
itemLabelClassName,
|
|
12
|
+
itemNotClickableClassName,
|
|
11
13
|
itemSurfaceClassName,
|
|
12
14
|
} from "./style.js";
|
|
13
15
|
|
|
@@ -15,7 +17,9 @@ import type { ElementType, MouseEvent } from "react";
|
|
|
15
17
|
import { Ripple } from "../../hoc/ripple/ripple.js";
|
|
16
18
|
import type { ItemIconProps, ItemLabelProps, ItemProps } from "./type.js";
|
|
17
19
|
|
|
18
|
-
export const Item = <T extends ElementType = "
|
|
20
|
+
export const Item = <T extends ElementType = "button">({
|
|
21
|
+
clickable = true,
|
|
22
|
+
disabled = false,
|
|
19
23
|
active = false,
|
|
20
24
|
onClickBlur,
|
|
21
25
|
onClick,
|
|
@@ -24,6 +28,7 @@ export const Item = <T extends ElementType = "div">({
|
|
|
24
28
|
...rest
|
|
25
29
|
}: ItemProps<T>) => {
|
|
26
30
|
const handleClick = (event: MouseEvent<HTMLElement>) => {
|
|
31
|
+
if (disabled) return;
|
|
27
32
|
onClick?.(event);
|
|
28
33
|
if (typeof onClickBlur === "function") {
|
|
29
34
|
const target = event.currentTarget;
|
|
@@ -33,12 +38,21 @@ export const Item = <T extends ElementType = "div">({
|
|
|
33
38
|
};
|
|
34
39
|
|
|
35
40
|
return (
|
|
36
|
-
<Ripple>
|
|
41
|
+
<Ripple disabled={disabled || !clickable}>
|
|
37
42
|
<Tag
|
|
38
|
-
{...Tag.forward<T>(rest)}
|
|
43
|
+
{...Tag.forward<T>(rest, "button")}
|
|
39
44
|
data-margo-active={active}
|
|
45
|
+
data-margo-disabled={disabled}
|
|
46
|
+
aria-disabled={disabled || undefined}
|
|
47
|
+
tabIndex={disabled || !clickable ? -1 : undefined}
|
|
40
48
|
onClick={handleClick}
|
|
41
|
-
className={cn(
|
|
49
|
+
className={cn(
|
|
50
|
+
itemBaseClassName,
|
|
51
|
+
active ? itemActiveClassName : itemSurfaceClassName,
|
|
52
|
+
!clickable && itemNotClickableClassName,
|
|
53
|
+
disabled && itemDisabledClassName,
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
42
56
|
>
|
|
43
57
|
{children}
|
|
44
58
|
</Tag>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export const itemBaseClassName = `group/item flex w-full items-center gap-1 rounded-lg border-2 px-3 py-2 text-on-main
|
|
2
|
-
text-sm focus-visible:margo-border-gradient-primary focus-visible:outline
|
|
1
|
+
export const itemBaseClassName = `group/item flex w-full cursor-pointer items-center gap-1 rounded-lg border-2 px-3 py-2 text-on-main
|
|
2
|
+
text-sm focus-visible:margo-border-gradient-primary focus-visible:outline select-none
|
|
3
3
|
focus-visible:outline-offset-1 focus-visible:outline-on-main`;
|
|
4
4
|
|
|
5
5
|
export const itemSurfaceClassName = `border-transparent text-medium hover:border-border hover:text-on-main
|
|
@@ -7,6 +7,10 @@ hover:shadow-item focus-visible:shadow-item`;
|
|
|
7
7
|
|
|
8
8
|
export const itemActiveClassName = "margo-border-gradient-primary text-on-main shadow-item";
|
|
9
9
|
|
|
10
|
+
export const itemNotClickableClassName = "pointer-events-none cursor-auto";
|
|
11
|
+
|
|
12
|
+
export const itemDisabledClassName = "pointer-events-none cursor-auto opacity-40";
|
|
13
|
+
|
|
10
14
|
export const itemIconClassName = `ml-auto flex size-4 shrink-0 items-center justify-center first:ml-0
|
|
11
15
|
[[data-margo-item-slot]+&]:ml-0`;
|
|
12
16
|
|
|
@@ -2,12 +2,14 @@ import type { TagProps } from "react-renderable";
|
|
|
2
2
|
import type { ElementType, MouseEventHandler, ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
export type ItemOwnProps = {
|
|
5
|
+
clickable?: boolean;
|
|
6
|
+
disabled?: boolean;
|
|
5
7
|
active?: boolean;
|
|
6
8
|
onClick?: MouseEventHandler<HTMLElement>;
|
|
7
9
|
onClickBlur?: MouseEventHandler<HTMLElement>;
|
|
8
10
|
};
|
|
9
11
|
|
|
10
|
-
export type ItemProps<T extends ElementType = "
|
|
12
|
+
export type ItemProps<T extends ElementType = "button"> = TagProps<T, ItemOwnProps>;
|
|
11
13
|
|
|
12
14
|
export type ItemIconProps = {
|
|
13
15
|
icon?: ReactNode;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
import { cn } from "../../utils/cn/cn.js";
|
|
4
4
|
import { layerBaseClassName } from "./style.js";
|
|
@@ -10,13 +10,38 @@ import "./layer.css";
|
|
|
10
10
|
|
|
11
11
|
export const Layer = ({ open = false, onClose, dismissible = true, className, children, ...rest }: LayerProps) => {
|
|
12
12
|
const layerRef = useRef<HTMLDialogElement>(null);
|
|
13
|
+
const childrenRef = useRef(children);
|
|
14
|
+
|
|
15
|
+
const [isMounted, setIsMounted] = useState(open);
|
|
16
|
+
|
|
17
|
+
if (open) childrenRef.current = children;
|
|
18
|
+
if (open && !isMounted) setIsMounted(true);
|
|
13
19
|
|
|
14
20
|
useEffect(() => {
|
|
15
21
|
const layer = layerRef.current;
|
|
16
22
|
|
|
17
23
|
if (!layer) return;
|
|
18
24
|
if (open && !layer.open) layer.showModal();
|
|
19
|
-
if (
|
|
25
|
+
if (open || !layer.open) return;
|
|
26
|
+
|
|
27
|
+
layer.close();
|
|
28
|
+
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
|
|
31
|
+
const frame = requestAnimationFrame(() => {
|
|
32
|
+
const animations = layer.getAnimations({ subtree: true }).map((animation) => animation.finished);
|
|
33
|
+
|
|
34
|
+
void Promise.allSettled(animations).then(() => {
|
|
35
|
+
if (controller.signal.aborted) return;
|
|
36
|
+
|
|
37
|
+
setIsMounted(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
controller.abort();
|
|
43
|
+
cancelAnimationFrame(frame);
|
|
44
|
+
};
|
|
20
45
|
}, [open]);
|
|
21
46
|
|
|
22
47
|
const handleClick = (event: MouseEvent<HTMLDialogElement>) => {
|
|
@@ -38,7 +63,7 @@ export const Layer = ({ open = false, onClose, dismissible = true, className, ch
|
|
|
38
63
|
onClick={handleClick}
|
|
39
64
|
className={cn(layerBaseClassName, className)}
|
|
40
65
|
>
|
|
41
|
-
{
|
|
66
|
+
{isMounted ? childrenRef.current : null}
|
|
42
67
|
</dialog>
|
|
43
68
|
);
|
|
44
69
|
};
|
|
@@ -10,11 +10,11 @@ import type { RippleProps } from "./type.js";
|
|
|
10
10
|
|
|
11
11
|
import "./ripple.css";
|
|
12
12
|
|
|
13
|
-
export const Ripple = ({ children, rippleClassName = "bg-primary-darken" }: RippleProps) => {
|
|
13
|
+
export const Ripple = ({ children, disabled = false, rippleClassName = "bg-primary-darken" }: RippleProps) => {
|
|
14
14
|
const { endRipple, ripple, startRipple } = useRipple();
|
|
15
15
|
|
|
16
16
|
const handlePointerDown = (event: PointerEvent<HTMLElement>) => {
|
|
17
|
-
startRipple(event);
|
|
17
|
+
if (!disabled) startRipple(event);
|
|
18
18
|
children.props.onPointerDown?.(event);
|
|
19
19
|
};
|
|
20
20
|
|