margo-ui 1.3.2 → 1.3.4

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.3.2",
3
+ "version": "1.3.4",
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",
@@ -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 && "pointer-events-none",
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
 
@@ -5,6 +5,7 @@ export type ButtonIconLabelSide = "start" | "end";
5
5
 
6
6
  export type ButtonOwnProps = {
7
7
  clickable?: boolean;
8
+ disabled?: boolean;
8
9
  active?: boolean;
9
10
  open?: boolean;
10
11
  onClick?: MouseEventHandler<HTMLElement>;
@@ -1,4 +1,4 @@
1
- export const headerBaseClassName = "flex w-full items-center gap-3 px-5 py-4 shadow-sm";
1
+ export const headerBaseClassName = "flex w-full items-center gap-3 px-5 py-4";
2
2
 
3
3
  export const headerLeadingClassName = "flex shrink-0 items-center gap-2";
4
4
 
@@ -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 = "div">({
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(itemBaseClassName, active ? itemActiveClassName : itemSurfaceClassName, className)}
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 = "div"> = TagProps<T, ItemOwnProps>;
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 (!open && layer.open) layer.close();
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
- {children}
66
+ {isMounted ? childrenRef.current : null}
42
67
  </dialog>
43
68
  );
44
69
  };
@@ -17,6 +17,7 @@ import {
17
17
  } from "./style.js";
18
18
 
19
19
  import type { ElementType } from "react";
20
+ import { MaskGradientX } from "../../hoc/mask_gradient_x/mask_gradient_x.js";
20
21
  import type { SnippetProps } from "./type.js";
21
22
 
22
23
  export const Snippet = <T extends ElementType = "div">({ snippet, title, className, ...rest }: SnippetProps<T>) => {
@@ -46,18 +47,20 @@ export const Snippet = <T extends ElementType = "div">({ snippet, title, classNa
46
47
  />
47
48
  </Button>
48
49
  </div>
49
- <pre className={snippetCodeClassName}>
50
- <code>
51
- <List
52
- array={snippetHighlight(source)}
53
- itemExtractor={({ row, index }) => (
54
- <span key={index} className={snippetTokenClassName[row.type]}>
55
- {row.value}
56
- </span>
57
- )}
58
- />
59
- </code>
60
- </pre>
50
+ <MaskGradientX fade="1rem">
51
+ <pre className={snippetCodeClassName}>
52
+ <code>
53
+ <List
54
+ array={snippetHighlight(source)}
55
+ itemExtractor={({ row, index }) => (
56
+ <span key={index} className={snippetTokenClassName[row.type]}>
57
+ {row.value}
58
+ </span>
59
+ )}
60
+ />
61
+ </code>
62
+ </pre>
63
+ </MaskGradientX>
61
64
  </Tag>
62
65
  );
63
66
  };
@@ -0,0 +1,40 @@
1
+ import { cloneElement, useState } from "react";
2
+
3
+ import type { PointerEvent } from "react";
4
+ import type { PointerHolderProps } from "./type.js";
5
+
6
+ const readKey = (target: EventTarget | null, attribute: string) => {
7
+ if (!(target instanceof Element)) return null;
8
+ const holder = target.closest(`[${attribute}]`);
9
+ return holder?.getAttribute(attribute) ?? null;
10
+ };
11
+
12
+ export const PointerHolder = ({ children, attribute = "data-margo-pointer-holder", disabled = false }: PointerHolderProps) => {
13
+ const [keys, setKeys] = useState<string[]>([]);
14
+
15
+ const hold = (target: EventTarget | null) => {
16
+ const key = readKey(target, attribute);
17
+
18
+ if (key === null) return;
19
+ setKeys((prev) => (prev.includes(key) ? prev : [...prev, key]));
20
+ };
21
+
22
+ const release = () => setKeys((prev) => (prev.length ? [] : prev));
23
+
24
+ const child = children({ keys });
25
+
26
+ const handlePointerOver = (event: PointerEvent<HTMLElement>) => {
27
+ if (!disabled && event.pointerType !== "touch") hold(event.target);
28
+ child.props.onPointerOver?.(event);
29
+ };
30
+
31
+ const handlePointerLeave = (event: PointerEvent<HTMLElement>) => {
32
+ release();
33
+ child.props.onPointerLeave?.(event);
34
+ };
35
+
36
+ return cloneElement(child, {
37
+ onPointerLeave: handlePointerLeave,
38
+ onPointerOver: handlePointerOver,
39
+ });
40
+ };
@@ -0,0 +1,18 @@
1
+ import type { FocusEventHandler, PointerEventHandler, ReactElement } from "react";
2
+
3
+ export type PointerHolderApi = {
4
+ keys: string[];
5
+ };
6
+
7
+ export type PointerHolderChildProps = {
8
+ onBlur?: FocusEventHandler<HTMLElement>;
9
+ onFocus?: FocusEventHandler<HTMLElement>;
10
+ onPointerLeave?: PointerEventHandler<HTMLElement>;
11
+ onPointerOver?: PointerEventHandler<HTMLElement>;
12
+ };
13
+
14
+ export type PointerHolderProps = {
15
+ children: (api: PointerHolderApi) => ReactElement<PointerHolderChildProps>;
16
+ attribute?: string;
17
+ disabled?: boolean;
18
+ };
@@ -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
 
@@ -8,6 +8,7 @@ export type RippleChildProps = {
8
8
 
9
9
  export type RippleProps = {
10
10
  children: ReactElement<RippleChildProps>;
11
+ disabled?: boolean;
11
12
  rippleClassName?: string;
12
13
  };
13
14
 
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export { Tooltip } from "./components/tooltip/tooltip.js";
18
18
 
19
19
  export { BackgroundGlow } from "./hoc/background_glow/background_glow.js";
20
20
  export { BorderGlow } from "./hoc/border_glow/border_glow.js";
21
+ export { PointerHolder } from "./hoc/pointer_holder/pointer_holder.js";
21
22
  export { MaskGradientX } from "./hoc/mask_gradient_x/mask_gradient_x.js";
22
23
  export { MaskGradientY } from "./hoc/mask_gradient_y/mask_gradient_y.js";
23
24
  export { Ripple } from "./hoc/ripple/ripple.js";
@@ -68,6 +69,7 @@ export type { TooltipPosition, TooltipProps } from "./components/tooltip/type.js
68
69
 
69
70
  export type { BackgroundGlowProps } from "./hoc/background_glow/type.js";
70
71
  export type { BorderGlowPosition, BorderGlowProps } from "./hoc/border_glow/type.js";
72
+ export type { PointerHolderProps } from "./hoc/pointer_holder/type.js";
71
73
  export type { MaskGradientXProps } from "./hoc/mask_gradient_x/type.js";
72
74
  export type { MaskGradientYProps } from "./hoc/mask_gradient_y/type.js";
73
75
  export type { RippleProps } from "./hoc/ripple/type.js";