margo-ui 1.1.0 → 1.2.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 (50) hide show
  1. package/package.json +5 -7
  2. package/src/components/blockquote/blockquote.tsx +11 -0
  3. package/src/components/blockquote/style.ts +1 -0
  4. package/src/components/blockquote/type.ts +4 -0
  5. package/src/components/button/button.tsx +83 -0
  6. package/src/components/button/style.ts +27 -0
  7. package/src/components/button/type.ts +33 -0
  8. package/src/components/card/card.tsx +13 -0
  9. package/src/components/card/style.ts +4 -0
  10. package/src/components/card/type.ts +4 -0
  11. package/src/components/chip/chip.tsx +13 -0
  12. package/src/components/chip/style.ts +4 -0
  13. package/src/components/chip/type.ts +8 -0
  14. package/src/components/progress_bar/progress_bar.css +33 -0
  15. package/src/components/progress_bar/progress_bar.tsx +35 -0
  16. package/src/components/progress_bar/style.ts +1 -0
  17. package/src/components/progress_bar/type.ts +12 -0
  18. package/src/components/spinner/spinner.tsx +21 -0
  19. package/src/components/spinner/style.ts +3 -0
  20. package/src/components/spinner/type.ts +8 -0
  21. package/src/components/tooltip/style.ts +17 -0
  22. package/src/components/tooltip/tooltip.tsx +50 -0
  23. package/src/components/tooltip/type.ts +17 -0
  24. package/src/css/theme.css +5 -0
  25. package/src/css/variables.css +21 -11
  26. package/src/hoc/background_glow/background_glow.tsx +25 -0
  27. package/src/hoc/background_glow/style.ts +9 -0
  28. package/src/hoc/background_glow/type.ts +13 -0
  29. package/src/hoc/border_glow/border_glow.tsx +27 -0
  30. package/src/hoc/border_glow/style.ts +11 -0
  31. package/src/hoc/border_glow/type.ts +13 -0
  32. package/src/{components → hoc/ripple}/ripple.tsx +5 -14
  33. package/src/hoc/ripple/type.ts +25 -0
  34. package/src/{hooks → hoc/ripple}/use_ripple.ts +2 -14
  35. package/src/hooks/use_theme.ts +8 -8
  36. package/src/index.ts +39 -22
  37. package/src/utils/theme/theme.ts +26 -0
  38. package/src/utils/theme/type.ts +12 -0
  39. package/src/components/background_glow.tsx +0 -45
  40. package/src/components/border_glow.tsx +0 -45
  41. package/src/components/button.tsx +0 -98
  42. package/src/components/card.tsx +0 -26
  43. package/src/components/chip.tsx +0 -27
  44. package/src/components/tooltip.tsx +0 -55
  45. package/src/helpers/theme.ts +0 -31
  46. /package/src/{components → hoc/background_glow}/background_glow.css +0 -0
  47. /package/src/{components → hoc/border_glow}/border_glow.css +0 -0
  48. /package/src/{components → hoc/ripple}/ripple.css +0 -0
  49. /package/src/{components → meta/meta_color_scheme}/meta_color_scheme.tsx +0 -0
  50. /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.1.0",
3
+ "version": "1.2.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",
@@ -36,15 +36,13 @@
36
36
  },
37
37
  "scripts": {
38
38
  "ts:check": "tsc --noEmit",
39
- "build": "rm -rf dist && tsc",
40
- "change": "changeset",
41
- "change:status": "changeset status --verbose",
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.1",
45
+ "react-renderable": "^1.0.4",
48
46
  "tailwind-merge": "^3.6.0"
49
47
  },
50
48
  "peerDependencies": {
@@ -0,0 +1,11 @@
1
+ import { Tag } from "react-renderable";
2
+
3
+ import { cn } from "../../utils/cn/cn.js";
4
+ import { blockquoteBaseClassName } from "./style.js";
5
+
6
+ import type { ElementType } from "react";
7
+ import type { BlockquoteProps } from "./type.js";
8
+
9
+ export const Blockquote = <T extends ElementType = "blockquote">({ className, ...rest }: BlockquoteProps<T>) => (
10
+ <Tag {...Tag.forward<T>(rest, "blockquote")} className={cn(blockquoteBaseClassName, className)} />
11
+ );
@@ -0,0 +1 @@
1
+ export const blockquoteBaseClassName = "border-l-2 border-primary-darken py-0.5 pl-4";
@@ -0,0 +1,4 @@
1
+ import type { TagProps } from "react-renderable";
2
+ import type { ElementType } from "react";
3
+
4
+ export type BlockquoteProps<T extends ElementType = "blockquote"> = TagProps<T>;
@@ -0,0 +1,83 @@
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
+ onClickBlur,
25
+ className,
26
+ onClick,
27
+ ...rest
28
+ }: ButtonProps<T>) => {
29
+ const handleClick = (event: MouseEvent<HTMLElement>) => {
30
+ onClick?.(event);
31
+ if (typeof onClickBlur === "function") {
32
+ const target = event.currentTarget;
33
+ onClickBlur(event);
34
+ setTimeout(() => target.blur(), 150);
35
+ }
36
+ };
37
+
38
+ return (
39
+ <Ripple>
40
+ <Tag
41
+ {...Tag.forward<T>(rest, "button")}
42
+ data-open={open}
43
+ onClick={handleClick}
44
+ className={cn(
45
+ buttonBaseClassName,
46
+ active ? buttonActiveClassName : buttonSurfaceClassName,
47
+ !clickable && "pointer-events-none",
48
+ className,
49
+ )}
50
+ />
51
+ </Ripple>
52
+ );
53
+ };
54
+
55
+ Button.Icon = ({ icon, className = null }: ButtonIconProps) => (
56
+ <span className={cn(buttonIconClassName, className)}>{icon}</span>
57
+ );
58
+
59
+ Button.Label = ({ label, className = null }: ButtonLabelProps) => (
60
+ <span className={cn(buttonLabelClassName, className)}>{label}</span>
61
+ );
62
+
63
+ Button.IconLabel = ({
64
+ icon,
65
+ label,
66
+ gap = "0.5rem",
67
+ reverse = false,
68
+ side = "end",
69
+ className = null,
70
+ }: ButtonIconLabelProps) => (
71
+ <span
72
+ style={{ "--button-gap": gap } as CSSProperties}
73
+ className={cn(
74
+ buttonGridClassName,
75
+ side === "start" && "[direction:rtl]",
76
+ reverse ? buttonGridReverseClassName : buttonGridForwardClassName,
77
+ className,
78
+ )}
79
+ >
80
+ {reverse ? label : icon}
81
+ <span className={buttonSlotClassName}>{reverse ? icon : label}</span>
82
+ </span>
83
+ );
@@ -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 shadow-button`;
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
+ onClick?: MouseEventHandler<HTMLElement>;
11
+ onClickBlur?: 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,4 @@
1
+ export const cardBaseClassName = "rounded-lg border-2 border-border bg-main p-5 text-on-main shadow-card";
2
+
3
+ export const cardFocusClassName = `focus:margo-border-gradient-primary focus-visible:outline focus-visible:outline-offset-1
4
+ focus-visible:outline-on-main`;
@@ -0,0 +1,4 @@
1
+ import type { TagProps } from "react-renderable";
2
+ import type { ElementType } from "react";
3
+
4
+ export type CardProps<T extends ElementType = "div"> = TagProps<T>;
@@ -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,4 @@
1
+ export const chipBaseClassName =
2
+ "bg-secondary px-3 py-0.5 whitespace-nowrap lowercase flex-none leading-none rounded-md text-mc shadow-chip";
3
+
4
+ export const chipActiveClassName = "bg-on-main text-main";
@@ -0,0 +1,8 @@
1
+ import type { TagProps } from "react-renderable";
2
+ import type { ElementType } from "react";
3
+
4
+ export type ChipOwnProps = {
5
+ active?: boolean;
6
+ };
7
+
8
+ export type ChipProps<T extends ElementType = "div"> = TagProps<T, ChipOwnProps>;
@@ -0,0 +1,33 @@
1
+ .margo-progress-bar::before {
2
+ content: "";
3
+ position: absolute;
4
+ inset: 0;
5
+ background: var(
6
+ --margo-progress-bar-fill,
7
+ linear-gradient(to right, var(--color-primary-darken), var(--color-primary))
8
+ );
9
+ }
10
+
11
+ .margo-progress-bar[data-mode="determinate"]::before {
12
+ clip-path: inset(0 calc(100% - var(--margo-progress-bar-value, 0%)) 0 0);
13
+ }
14
+
15
+ .margo-progress-bar[data-mode="determinate"][data-animate="true"]::before {
16
+ transition: clip-path 300ms ease-out;
17
+ }
18
+
19
+ .margo-progress-bar[data-mode="indeterminate"]::before {
20
+ inset: 0 auto 0 0;
21
+ width: 40%;
22
+ animation: margo-progress-bar-indeterminate 800ms ease-in-out infinite;
23
+ }
24
+
25
+ @keyframes margo-progress-bar-indeterminate {
26
+ from {
27
+ transform: translateX(-100%);
28
+ }
29
+
30
+ to {
31
+ transform: translateX(250%);
32
+ }
33
+ }
@@ -0,0 +1,35 @@
1
+ import { Tag } from "react-renderable";
2
+
3
+ import { cn } from "../../utils/cn/cn.js";
4
+ import { progressBarBaseClassName } from "./style.js";
5
+
6
+ import type { CSSProperties, ElementType } from "react";
7
+ import type { ProgressBarProps } from "./type.js";
8
+
9
+ import "./progress_bar.css";
10
+
11
+ export const ProgressBar = <T extends ElementType = "div">({
12
+ animate = true,
13
+ className,
14
+ mode = "indeterminate",
15
+ style,
16
+ value = 0,
17
+ ...rest
18
+ }: ProgressBarProps<T>) => {
19
+ const isDeterminate = mode === "determinate";
20
+ const progress = Math.min(100, Math.max(0, value));
21
+
22
+ return (
23
+ <Tag
24
+ {...Tag.forward<T>(rest)}
25
+ role="progressbar"
26
+ aria-valuemin={isDeterminate ? 0 : undefined}
27
+ aria-valuemax={isDeterminate ? 100 : undefined}
28
+ aria-valuenow={isDeterminate ? progress : undefined}
29
+ data-mode={mode}
30
+ data-animate={animate}
31
+ style={{ ...style, "--margo-progress-bar-value": `${progress}%` } as CSSProperties}
32
+ className={cn(progressBarBaseClassName, className)}
33
+ />
34
+ );
35
+ };
@@ -0,0 +1 @@
1
+ export const progressBarBaseClassName = "margo-progress-bar relative h-0.5 overflow-hidden";
@@ -0,0 +1,12 @@
1
+ import type { TagProps } from "react-renderable";
2
+ import type { ElementType } from "react";
3
+
4
+ export type ProgressBarMode = "determinate" | "indeterminate";
5
+
6
+ export type ProgressBarOwnProps = {
7
+ animate?: boolean;
8
+ mode?: ProgressBarMode;
9
+ value?: number;
10
+ };
11
+
12
+ export type ProgressBarProps<T extends ElementType = "div"> = TagProps<T, ProgressBarOwnProps>;
@@ -0,0 +1,21 @@
1
+ import { Tag } from "react-renderable";
2
+
3
+ import { cn } from "../../utils/cn/cn.js";
4
+ import { spinnerBaseClassName, spinnerIconClassName } from "./style.js";
5
+
6
+ import type { ElementType } from "react";
7
+ import type { SpinnerProps } from "./type.js";
8
+
9
+ export const Spinner = <T extends ElementType = "span">({ className, label = "Loading", ...rest }: SpinnerProps<T>) => (
10
+ <Tag
11
+ {...Tag.forward<T>(rest, "span")}
12
+ role="status"
13
+ aria-label={label}
14
+ className={cn(spinnerBaseClassName, className)}
15
+ >
16
+ <svg viewBox="0 0 24 24" aria-hidden className={spinnerIconClassName}>
17
+ <circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" strokeOpacity="0.25" strokeWidth="3" />
18
+ <path d="M12 3a9 9 0 0 1 9 9" fill="none" stroke="currentColor" strokeLinecap="round" strokeWidth="3" />
19
+ </svg>
20
+ </Tag>
21
+ );
@@ -0,0 +1,3 @@
1
+ export const spinnerBaseClassName = "inline-flex size-4 animate-spin";
2
+
3
+ export const spinnerIconClassName = "size-full";
@@ -0,0 +1,8 @@
1
+ import type { TagProps } from "react-renderable";
2
+ import type { ElementType } from "react";
3
+
4
+ export type SpinnerOwnProps = {
5
+ label?: string;
6
+ };
7
+
8
+ export type SpinnerProps<T extends ElementType = "span"> = Omit<TagProps<T, SpinnerOwnProps>, "aria-label">;
@@ -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
+ >;
package/src/css/theme.css CHANGED
@@ -23,6 +23,11 @@
23
23
  --color-medium: var(--margo-color-medium);
24
24
  --color-border: var(--margo-color-border);
25
25
 
26
+ /* Shadows */
27
+ --shadow-card: var(--margo-shadow-card);
28
+ --shadow-button: var(--margo-shadow-button);
29
+ --shadow-chip: var(--margo-shadow-chip);
30
+
26
31
  /* Border widths */
27
32
  --border-width-2: var(--margo-border-width-2);
28
33
 
@@ -30,34 +30,44 @@
30
30
  --margo-font-weight-black: 900;
31
31
 
32
32
  --margo-border-width-2: 1.5px;
33
+
34
+ --margo-shadow-card-color: 0 0 0;
35
+ --margo-shadow-button-color: 0 0 0;
36
+ --margo-shadow-chip-color: 0 0 0;
33
37
  }
34
38
 
35
- /* Colors Theme Light */
36
39
  :root {
37
40
  color-scheme: light;
38
41
 
39
42
  --margo-color-neutral: #ffffff;
40
43
  --margo-color-on-neutral: #000000;
41
- --margo-color-main: #e9e9e9;
42
- --margo-color-on-main: #222222;
43
- --margo-color-primary: #2c4ee7;
44
- --margo-color-primary-darken: #1b24a5;
44
+ --margo-color-main: #f6f6f6;
45
+ --margo-color-on-main: #151515;
46
+ --margo-color-primary: #57e550;
47
+ --margo-color-primary-darken: #45b93f;
45
48
  --margo-color-secondary: #d3d3d3;
46
49
  --margo-color-medium: #7e7e7e;
47
- --margo-color-border: #d3d3d3;
50
+ --margo-color-border: #dcdcdc;
51
+
52
+ --margo-shadow-card: 0 4px 12px -4px rgb(var(--margo-shadow-card-color) / 0.1);
53
+ --margo-shadow-button: 0 2px 6px -2px rgb(var(--margo-shadow-button-color) / 0.1);
54
+ --margo-shadow-chip: 0 1px 3px -1px rgb(var(--margo-shadow-chip-color) / 0.5);
48
55
  }
49
56
 
50
- /* Colors Theme Dark */
51
57
  .dark {
52
58
  color-scheme: dark;
53
59
 
54
60
  --margo-color-neutral: #000000;
55
61
  --margo-color-on-neutral: #ffffff;
56
- --margo-color-main: #141414;
57
- --margo-color-on-main: #f9f9f9;
58
- --margo-color-primary: #f5cb77;
59
- --margo-color-primary-darken: #ad9055;
62
+ --margo-color-main: #151515;
63
+ --margo-color-on-main: #eeeeee;
64
+ --margo-color-primary: #57e550;
65
+ --margo-color-primary-darken: #45b93f;
60
66
  --margo-color-secondary: #2c2c2c;
61
67
  --margo-color-medium: #999999;
62
68
  --margo-color-border: #2c2c2c;
69
+
70
+ --margo-shadow-card: 0 4px 12px -4px rgb(var(--margo-shadow-card-color) / 0.5);
71
+ --margo-shadow-button: 0 2px 6px -2px rgb(var(--margo-shadow-button-color) / 0.5);
72
+ --margo-shadow-chip: 0 1px 3px -1px rgb(var(--margo-shadow-chip-color) / 0.5);
63
73
  }
@@ -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,23 +1,14 @@
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 type { PointerEvent, PointerEventHandler, ReactElement, ReactNode } from "react";
5
+ import { cn } from "../../utils/cn/cn.js";
6
+ import { useRipple } from "./use_ripple.js";
8
7
 
9
- import "./ripple.css";
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
- export type RippleProps = {
18
- children: ReactElement<RippleChildProps>;
19
- rippleClassName?: string;
20
- };
11
+ import "./ripple.css";
21
12
 
22
13
  export const Ripple = ({ children, rippleClassName = "bg-primary-darken" }: RippleProps) => {
23
14
  const { endRipple, ripple, startRipple } = useRipple();
@@ -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<Ripple | null>(null);
7
+ const [ripple, setRipple] = useState<RippleState | null>(null);
20
8
 
21
9
  const startRipple = (event: PointerEvent<HTMLElement>) => {
22
10
  const bounds = event.currentTarget.getBoundingClientRect();
@@ -1,20 +1,20 @@
1
1
  import { useCallback, useEffect, useState } from "react";
2
2
 
3
- import { themeClient } from "../helpers/theme";
3
+ import { margoThemeClient } from "../utils/theme/theme.js";
4
4
 
5
- import type { Theme } from "../helpers/theme";
5
+ import type { MargoTheme } from "../utils/theme/type.js";
6
6
 
7
- export const useTheme = (): [Theme, (next: Theme | ((current: Theme) => Theme)) => void] => {
8
- const [theme, setThemeState] = useState<Theme>(() => themeClient.get());
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(themeClient.get());
14
+ setThemeState(margoThemeClient.get());
15
15
 
16
16
  const observer = new MutationObserver(() => {
17
- setThemeState(themeClient.get());
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: Theme | ((current: Theme) => Theme)) => {
26
- themeClient.set(typeof next === "function" ? next(themeClient.get()) : next);
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,22 +1,39 @@
1
- export { BackgroundGlow } from "./components/background_glow";
2
- export { BorderGlow } from "./components/border_glow";
3
- export { Button } from "./components/button";
4
- export { Card } from "./components/card";
5
- export { Chip } from "./components/chip";
6
- export { MetaColorScheme } from "./components/meta_color_scheme";
7
- export { Ripple } from "./components/ripple";
8
- export { Tooltip } from "./components/tooltip";
9
-
10
- export { useTheme } from "./hooks/use_theme";
11
-
12
- export { cn } from "./helpers/cn";
13
- export { THEME, themeClient } from "./helpers/theme";
14
-
15
- export type { BackgroundGlowChildProps, BackgroundGlowProps, BackgroundGlowSize } from "./components/background_glow";
16
- export type { BorderGlowChildProps, BorderGlowPosition, BorderGlowProps } from "./components/border_glow";
17
- export type { ButtonIconLabelSide, ButtonProps } from "./components/button";
18
- export type { CardProps } from "./components/card";
19
- export type { ChipProps } from "./components/chip";
20
- export type { RippleChildProps, RippleProps } from "./components/ripple";
21
- export type { TooltipPosition, TooltipProps } from "./components/tooltip";
22
- export type { Theme } from "./helpers/theme";
1
+ export { Blockquote } from "./components/blockquote/blockquote.js";
2
+ export { Button } from "./components/button/button.js";
3
+ export { Card } from "./components/card/card.js";
4
+ export { Chip } from "./components/chip/chip.js";
5
+ export { ProgressBar } from "./components/progress_bar/progress_bar.js";
6
+ export { Spinner } from "./components/spinner/spinner.js";
7
+ export { Tooltip } from "./components/tooltip/tooltip.js";
8
+
9
+ export { BackgroundGlow } from "./hoc/background_glow/background_glow.js";
10
+ export { BorderGlow } from "./hoc/border_glow/border_glow.js";
11
+ export { Ripple } from "./hoc/ripple/ripple.js";
12
+
13
+ export { MetaColorScheme } from "./meta/meta_color_scheme/meta_color_scheme.js";
14
+
15
+ export { useMargoTheme } from "./hooks/use_theme.js";
16
+
17
+ export { cn } from "./utils/cn/cn.js";
18
+ export { margoTheme, margoThemeClient } from "./utils/theme/theme.js";
19
+
20
+ export type { BlockquoteProps } from "./components/blockquote/type.js";
21
+
22
+ export type {
23
+ ButtonIconLabelProps,
24
+ ButtonIconLabelSide,
25
+ ButtonIconProps,
26
+ ButtonLabelProps,
27
+ ButtonProps,
28
+ } from "./components/button/type.js";
29
+ export type { CardProps } from "./components/card/type.js";
30
+ export type { ChipProps } from "./components/chip/type.js";
31
+ export type { ProgressBarMode, ProgressBarProps } from "./components/progress_bar/type.js";
32
+ export type { SpinnerProps } from "./components/spinner/type.js";
33
+ export type { TooltipPosition, TooltipProps } from "./components/tooltip/type.js";
34
+
35
+ export type { BackgroundGlowProps, BackgroundGlowSize } from "./hoc/background_glow/type.js";
36
+ export type { BorderGlowPosition, BorderGlowProps } from "./hoc/border_glow/type.js";
37
+ export type { RippleProps } from "./hoc/ripple/type.js";
38
+
39
+ 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 "./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
- };
@@ -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,98 +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, MouseEvent, MouseEventHandler, 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 ButtonIconLabelSide = "start" | "end";
19
-
20
- export type ButtonProps<T extends ElementType = "button"> = TagProps<T> & {
21
- clickable?: boolean;
22
- active?: boolean;
23
- open?: boolean;
24
- blurOnClick?: boolean;
25
- };
26
-
27
- export const Button = <T extends ElementType = "button">(props: ButtonProps<T>) => {
28
- const { as, clickable = true, active = false, open = false, blurOnClick = true, ...restProps } = props;
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
- };
36
-
37
- return (
38
- <Ripple>
39
- <Tag
40
- {...nativeProps}
41
- data-open={open}
42
- onClick={handleClick}
43
- className={cn(
44
- CLASSNAME_BASE,
45
- active ? "margo-border-gradient-primary" : CLASSNAME_SURFACE,
46
- !clickable && "pointer-events-none",
47
- props.className,
48
- )}
49
- />
50
- </Ripple>
51
- );
52
- };
53
-
54
- Button.Icon = ({ icon, className = null }: { icon: ReactNode; className?: string | null }) => (
55
- <span className={cn(CLASSNAME_ICON, className)}>{icon}</span>
56
- );
57
-
58
- Button.Label = ({ label, className = null }: { label: string; className?: string | null }) => (
59
- <span className={cn(CLASSNAME_LABEL, className)}>{label}</span>
60
- );
61
-
62
- Button.IconLabel = ({
63
- icon,
64
- label,
65
- gap = "0.5rem",
66
- reverse = false,
67
- side = "end",
68
- className = null,
69
- }: {
70
- icon: ReactNode;
71
- label: ReactNode;
72
- gap?: string;
73
- reverse?: boolean;
74
- side?: ButtonIconLabelSide;
75
- className?: string | null;
76
- }) => {
77
- return (
78
- <span
79
- style={{ "--button-gap": gap } as CSSProperties}
80
- className={cn(
81
- CLASSNAME_GRID,
82
- side === "start" && "[direction:rtl]",
83
- !reverse &&
84
- `grid-cols-[1rem_0fr] group-hover/button:grid-cols-[1rem_1fr] group-hover/button:gap-(--button-gap)
85
- group-focus-visible/button:grid-cols-[1rem_1fr] group-focus-visible/button:gap-(--button-gap)
86
- group-data-[open=true]/button:grid-cols-[1rem_1fr] group-data-[open=true]/button:gap-(--button-gap)`,
87
- reverse &&
88
- `grid-cols-[auto_0rem] group-hover/button:grid-cols-[auto_1rem] group-hover/button:gap-(--button-gap)
89
- group-focus-visible/button:grid-cols-[auto_1rem] group-focus-visible/button:gap-(--button-gap)
90
- group-data-[open=true]/button:grid-cols-[auto_1rem] group-data-[open=true]/button:gap-(--button-gap)`,
91
- className,
92
- )}
93
- >
94
- {reverse ? label : icon}
95
- <span className={CLASSNAME_SLOT}>{reverse ? icon : label}</span>
96
- </span>
97
- );
98
- };
@@ -1,26 +0,0 @@
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 CardProps<T extends ElementType = "div"> = TagProps<T>;
8
-
9
- export const Card = <T extends ElementType = "div">(props: CardProps<T>) => {
10
- const { as, className, children, ...restProps } = props;
11
- const nativeProps = { as: as ?? "div", ...restProps } as TagProps<T>;
12
-
13
- return (
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>
25
- );
26
- };
@@ -1,27 +0,0 @@
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
- };
@@ -1,55 +0,0 @@
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
- };
@@ -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