margo-ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Roberto Attanasio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # margo-ui
2
+
3
+ A small React UI kit built on Tailwind CSS v4 design tokens..
4
+
5
+ The package is published as TypeScript source: your bundler compiles it, exactly as it already has to scan it for Tailwind classes.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install margo-ui
11
+ ```
12
+
13
+ ## Getting started
14
+
15
+ ```css
16
+ /* your entry stylesheet */
17
+ @import "tailwindcss";
18
+ @import "margo-ui/fonts/nunito.css";
19
+ @import "margo-ui/fonts/bebas.css";
20
+ @import "margo-ui/css";
21
+ ```
22
+
23
+ ## License
24
+
25
+ Code: [MIT](./LICENSE).
26
+
27
+ Fonts are redistributed under their own licenses — Nunito and Bebas Neue are both under the SIL Open Font License 1.1, bundled as `src/fonts/nunito/OFL.txt` and `src/fonts/bebas-neue/OFL.txt`.
package/css.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module "*.css";
package/margo.css ADDED
@@ -0,0 +1,7 @@
1
+ @import "./src/css/base.css";
2
+ @import "./src/css/variables.css";
3
+ @import "./src/css/theme.css";
4
+ @import "./src/css/custom-variant.css";
5
+ @import "./src/css/utility.css";
6
+
7
+ @source "../**/*.{ts,tsx,js,mjs}";
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "margo-ui",
3
+ "version": "1.0.0",
4
+ "description": "A small React UI kit built on Tailwind CSS v4 design tokens, themeable through plain CSS custom properties.",
5
+ "keywords": [
6
+ "react",
7
+ "ui",
8
+ "components",
9
+ "tailwindcss",
10
+ "design-system",
11
+ "typescript"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "Roberto Attanasio",
15
+ "type": "module",
16
+ "main": "./src/index.ts",
17
+ "types": "./src/index.ts",
18
+ "files": [
19
+ "src",
20
+ "margo.css",
21
+ "css.d.ts"
22
+ ],
23
+ "workspaces": [
24
+ "."
25
+ ],
26
+ "sideEffects": [
27
+ "**/*.css"
28
+ ],
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "exports": {
33
+ ".": "./src/index.ts",
34
+ "./css": "./margo.css",
35
+ "./fonts/*": "./src/fonts/*"
36
+ },
37
+ "scripts": {
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"
44
+ },
45
+ "dependencies": {
46
+ "clsx": "^2.1.1",
47
+ "react-renderable": "^1.0.1",
48
+ "tailwind-merge": "^3.6.0"
49
+ },
50
+ "peerDependencies": {
51
+ "react": "^19.0.0",
52
+ "react-dom": "^19.0.0",
53
+ "tailwindcss": "^4.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@changesets/cli": "^2.31.1"
57
+ }
58
+ }
@@ -0,0 +1,49 @@
1
+ .margo-border-glow::before {
2
+ content: "";
3
+ position: absolute;
4
+ z-index: 1;
5
+ pointer-events: none;
6
+ opacity: 0;
7
+ background: radial-gradient(
8
+ 480px circle at var(--margo-border-glow-x, 70%) var(--margo-border-glow-y, 70%),
9
+ var(--color-primary),
10
+ transparent 70%
11
+ );
12
+ transition: opacity 300ms ease-out;
13
+ }
14
+
15
+ .margo-border-glow:hover::before,
16
+ .margo-border-glow:focus-within::before {
17
+ opacity: 1;
18
+ }
19
+
20
+ .margo-border-glow--ring::before {
21
+ inset: calc(-1 * var(--border-width-2));
22
+ border-radius: inherit;
23
+ padding: var(--border-width-2);
24
+ mask:
25
+ linear-gradient(#000 0 0) content-box,
26
+ linear-gradient(#000 0 0);
27
+ mask-composite: exclude;
28
+ -webkit-mask-composite: xor;
29
+ }
30
+
31
+ .margo-border-glow--edge-top::before {
32
+ inset: calc(-1 * var(--border-width-2)) 0 auto 0;
33
+ height: var(--border-width-2);
34
+ }
35
+
36
+ .margo-border-glow--edge-bottom::before {
37
+ inset: auto 0 calc(-1 * var(--border-width-2)) 0;
38
+ height: var(--border-width-2);
39
+ }
40
+
41
+ .margo-border-glow--edge-left::before {
42
+ inset: 0 auto 0 calc(-1 * var(--border-width-2));
43
+ width: var(--border-width-2);
44
+ }
45
+
46
+ .margo-border-glow--edge-right::before {
47
+ inset: 0 calc(-1 * var(--border-width-2)) 0 auto;
48
+ width: var(--border-width-2);
49
+ }
@@ -0,0 +1,45 @@
1
+ import { cloneElement } from "react";
2
+
3
+ import { cn } from "../helpers/cn";
4
+
5
+ import type { PointerEvent, PointerEventHandler, ReactElement } from "react";
6
+
7
+ import "./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
+ };
@@ -0,0 +1,88 @@
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
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
+ };
@@ -0,0 +1,29 @@
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
+ };
@@ -0,0 +1 @@
1
+ export const MetaColorScheme = () => <meta name="color-scheme" content="dark light" />;
@@ -0,0 +1,15 @@
1
+ .margo-animation-ripple {
2
+ animation: margo-ripple 500ms ease-out forwards;
3
+ }
4
+
5
+ @keyframes margo-ripple {
6
+ from {
7
+ opacity: 0.4;
8
+ transform: translate(-50%, -50%) scale(0);
9
+ }
10
+
11
+ to {
12
+ opacity: 0;
13
+ transform: translate(-50%, -50%) scale(1);
14
+ }
15
+ }
@@ -0,0 +1,53 @@
1
+ import { cloneElement } from "react";
2
+
3
+ import { Guard } from "react-renderable";
4
+ import { cn } from "../helpers/cn";
5
+ import { useRipple } from "../hooks/use_ripple";
6
+
7
+ import type { PointerEvent, PointerEventHandler, ReactElement, ReactNode } from "react";
8
+
9
+ import "./ripple.css";
10
+
11
+ export type RippleChildProps = {
12
+ children?: ReactNode;
13
+ className?: string;
14
+ onPointerDown?: PointerEventHandler<HTMLElement>;
15
+ };
16
+
17
+ export type RippleProps = {
18
+ children: ReactElement<RippleChildProps>;
19
+ rippleClassName?: string;
20
+ };
21
+
22
+ export const Ripple = ({ children, rippleClassName = "bg-primary" }: RippleProps) => {
23
+ const { endRipple, ripple, startRipple } = useRipple();
24
+
25
+ const handlePointerDown = (event: PointerEvent<HTMLElement>) => {
26
+ startRipple(event);
27
+ children.props.onPointerDown?.(event);
28
+ };
29
+
30
+ return cloneElement(children, {
31
+ className: cn("relative overflow-hidden", children.props.className),
32
+ onPointerDown: handlePointerDown,
33
+ children: (
34
+ <>
35
+ {children.props.children}
36
+ <Guard guardIf={!ripple} shouldHide>
37
+ <span
38
+ key={ripple?.id}
39
+ aria-hidden
40
+ className={cn("pointer-events-none absolute z-0 rounded-full margo-animation-ripple", rippleClassName)}
41
+ style={{
42
+ height: ripple?.size,
43
+ left: ripple?.x,
44
+ top: ripple?.y,
45
+ width: ripple?.size,
46
+ }}
47
+ onAnimationEnd={endRipple}
48
+ />
49
+ </Guard>
50
+ </>
51
+ ),
52
+ });
53
+ };
@@ -0,0 +1,84 @@
1
+ @layer base {
2
+ *,
3
+ *::before,
4
+ *::after {
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ canvas {
9
+ width: 100% !important;
10
+ }
11
+
12
+ li {
13
+ list-style: none;
14
+ }
15
+
16
+ svg {
17
+ outline: none !important;
18
+ flex: none;
19
+ }
20
+
21
+ button {
22
+ text-align: left;
23
+ }
24
+
25
+ ::-webkit-scrollbar {
26
+ display: none;
27
+ -webkit-appearance: none;
28
+ }
29
+
30
+ ::-webkit-scrollbar-button {
31
+ display: none;
32
+ -webkit-appearance: none;
33
+ }
34
+
35
+ input::-webkit-outer-spin-button,
36
+ input::-webkit-inner-spin-button {
37
+ -webkit-appearance: none;
38
+ margin: 0;
39
+ }
40
+
41
+ input[type="number"] {
42
+ -moz-appearance: textfield;
43
+ appearance: textfield;
44
+ }
45
+
46
+ option,
47
+ optgroup {
48
+ -webkit-appearance: none;
49
+ appearance: none;
50
+ }
51
+
52
+ select {
53
+ -webkit-appearance: none;
54
+ -moz-appearance: none;
55
+ appearance: none;
56
+ text-indent: 1px;
57
+ text-overflow: "";
58
+ }
59
+
60
+ input[type="date"]::-webkit-inner-spin-button,
61
+ input[type="date"]::-webkit-calendar-picker-indicator {
62
+ -webkit-appearance: none;
63
+ display: none;
64
+ }
65
+
66
+ button:not(:disabled),
67
+ [role="button"]:not([aria-disabled="true"]):not([disabled]) {
68
+ cursor: pointer;
69
+ }
70
+
71
+ input:-webkit-autofill,
72
+ input:autofill {
73
+ -webkit-text-fill-color: var(--margo-color-on-main) !important;
74
+ -webkit-background-clip: text !important;
75
+ background-clip: text !important;
76
+ }
77
+
78
+ .dark input:-webkit-autofill,
79
+ .dark input:autofill {
80
+ -webkit-text-fill-color: var(--margo-color-on-main) !important;
81
+ -webkit-background-clip: text !important;
82
+ background-clip: text !important;
83
+ }
84
+ }
@@ -0,0 +1 @@
1
+ @custom-variant dark (&:where(.dark, .dark *));
@@ -0,0 +1,74 @@
1
+ @theme {
2
+ /* Breakpoints */
3
+ --breakpoint-480: 480px;
4
+ --breakpoint-640: 640px;
5
+ --breakpoint-768: 768px;
6
+ --breakpoint-900: 900px;
7
+ --breakpoint-1080: 1080px;
8
+ --breakpoint-1280: 1280px;
9
+ --breakpoint-1920: 1920px;
10
+
11
+ /* Fonts */
12
+ --font-primary: var(--margo-font-family-primary);
13
+ --font-secondary: var(--margo-font-family-secondary);
14
+
15
+ /* Colors */
16
+ --color-neutral: var(--margo-color-neutral);
17
+ --color-on-neutral: var(--margo-color-on-neutral);
18
+ --color-main: var(--margo-color-main);
19
+ --color-on-main: var(--margo-color-on-main);
20
+ --color-primary: var(--margo-color-primary);
21
+ --color-primary-darken: var(--margo-color-primary-darken);
22
+ --color-secondary: var(--margo-color-secondary);
23
+ --color-medium: var(--margo-color-medium);
24
+ --color-border: var(--margo-color-border);
25
+
26
+ /* Border widths */
27
+ --border-width-2: var(--margo-border-width-2);
28
+
29
+ /* Font weights */
30
+ --font-weight-thin: var(--margo-font-weight-thin);
31
+ --font-weight-extralight: var(--margo-font-weight-extralight);
32
+ --font-weight-light: var(--margo-font-weight-light);
33
+ --font-weight-normal: var(--margo-font-weight-normal);
34
+ --font-weight-medium: var(--margo-font-weight-medium);
35
+ --font-weight-semibold: var(--margo-font-weight-semibold);
36
+ --font-weight-bold: var(--margo-font-weight-bold);
37
+ --font-weight-extrabold: var(--margo-font-weight-extrabold);
38
+ --font-weight-black: var(--margo-font-weight-black);
39
+
40
+ /* Font sizes */
41
+ --text-mc: var(--margo-font-size-mc);
42
+ --text-mc--line-height: 1.5;
43
+ --text-xs: var(--margo-font-size-xs);
44
+ --text-xs--line-height: 1.5;
45
+ --text-sm: var(--margo-font-size-sm);
46
+ --text-sm--line-height: 1.5;
47
+ --text-base: var(--margo-font-size-base);
48
+ --text-base--line-height: 1.25;
49
+ --text-md: var(--margo-font-size-md);
50
+ --text-md--line-height: 1.25;
51
+ --text-lg: var(--margo-font-size-lg);
52
+ --text-lg--line-height: 1.25;
53
+ --text-2lg: var(--margo-font-size-2lg);
54
+ --text-2lg--line-height: 1;
55
+ --text-xl: var(--margo-font-size-xl);
56
+ --text-xl--line-height: 1;
57
+ --text-2xl: var(--margo-font-size-2xl);
58
+ --text-2xl--line-height: 0.75;
59
+ --text-3xl: var(--margo-font-size-3xl);
60
+ --text-3xl--line-height: 0.75;
61
+ --text-4xl: var(--margo-font-size-4xl);
62
+ --text-4xl--line-height: 0.75;
63
+ --text-5xl: var(--margo-font-size-5xl);
64
+ --text-5xl--line-height: 0.75;
65
+ --text-6xl: var(--margo-font-size-6xl);
66
+ --text-6xl--line-height: 0.75;
67
+ --text-7xl: var(--margo-font-size-7xl);
68
+ --text-7xl--line-height: 0.75;
69
+ --text-8xl: var(--margo-font-size-8xl);
70
+ --text-8xl--line-height: 0.75;
71
+ --text-9xl: var(--margo-font-size-9xl);
72
+ --text-9xl--line-height: 0.75;
73
+
74
+ }
@@ -0,0 +1,6 @@
1
+ @utility margo-border-gradient-primary {
2
+ border-color: transparent;
3
+ background:
4
+ linear-gradient(var(--margo-color-main), var(--margo-color-main)) padding-box,
5
+ linear-gradient(to bottom right, var(--margo-color-primary-darken), var(--margo-color-primary)) border-box;
6
+ }
@@ -0,0 +1,63 @@
1
+ :root {
2
+ --margo-font-family-primary: "Nunito", helvetica, sans-serif;
3
+ --margo-font-family-secondary: "Bebas Neue", impact, sans-serif;
4
+
5
+ --margo-font-size-mc: 0.625rem;
6
+ --margo-font-size-xs: 0.75rem;
7
+ --margo-font-size-sm: 0.875rem;
8
+ --margo-font-size-base: 1rem;
9
+ --margo-font-size-md: 1.125rem;
10
+ --margo-font-size-lg: 1.375rem;
11
+ --margo-font-size-2lg: 1.5rem;
12
+ --margo-font-size-xl: 1.75rem;
13
+ --margo-font-size-2xl: 2rem;
14
+ --margo-font-size-3xl: 2.5rem;
15
+ --margo-font-size-4xl: 3rem;
16
+ --margo-font-size-5xl: 4rem;
17
+ --margo-font-size-6xl: 5.25rem;
18
+ --margo-font-size-7xl: 6.75rem;
19
+ --margo-font-size-8xl: 8.75rem;
20
+ --margo-font-size-9xl: 11.25rem;
21
+
22
+ --margo-font-weight-thin: 100;
23
+ --margo-font-weight-extralight: 200;
24
+ --margo-font-weight-light: 300;
25
+ --margo-font-weight-normal: 400;
26
+ --margo-font-weight-medium: 500;
27
+ --margo-font-weight-semibold: 600;
28
+ --margo-font-weight-bold: 700;
29
+ --margo-font-weight-extrabold: 800;
30
+ --margo-font-weight-black: 900;
31
+
32
+ --margo-border-width-2: 1.5px;
33
+ }
34
+
35
+ /* Colors Theme Light */
36
+ :root {
37
+ color-scheme: light;
38
+
39
+ --margo-color-neutral: #ffffff;
40
+ --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;
45
+ --margo-color-secondary: #d3d3d3;
46
+ --margo-color-medium: #7e7e7e;
47
+ --margo-color-border: #d3d3d3;
48
+ }
49
+
50
+ /* Colors Theme Dark */
51
+ .dark {
52
+ color-scheme: dark;
53
+
54
+ --margo-color-neutral: #000000;
55
+ --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;
60
+ --margo-color-secondary: #2c2c2c;
61
+ --margo-color-medium: #999999;
62
+ --margo-color-border: #2c2c2c;
63
+ }
@@ -0,0 +1,22 @@
1
+ This font is licensed under SIL Open Font License (OFL), Version 1.1.
2
+
3
+ Copyright © 2026 Ryoichi TsunekawaDharma Type
4
+ https://github.com/dharmatype/Bebas-Neue
5
+
6
+ -----------------------------------------------------------SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
7
+ -----------------------------------------------------------
8
+ PREAMBLEThe goals of the Open Font License (OFL) are to stimulate worldwidedevelopment of collaborative font projects, to support the font creationefforts of academic and linguistic communities, and to provide a free andopen framework in which fonts may be shared and improved in partnershipwith others.
9
+ The OFL allows the licensed fonts to be used, studied, modified andredistributed freely as long as they are not sold by themselves. Thefonts, including any derivative works, can be bundled, embedded,redistributed and/or sold with any software provided that any reservednames are not used by derivative works. The fonts and derivatives,however, cannot be released under any other type of license. Therequirement for fonts to remain under this license does not applyto any document created using the fonts or their derivatives.
10
+ DEFINITIONS"Font Software" refers to the set of files released by the CopyrightHolder(s) under this license and clearly marked as such. This mayinclude source files, build scripts and documentation.
11
+ "Reserved Font Name" refers to any names specified as such after thecopyright statement(s).
12
+ "Original Version" refers to the collection of Font Software components asdistributed by the Copyright Holder(s).
13
+ "Modified Version" refers to any derivative made by adding to, deleting,or substituting -- in part or in whole -- any of the components of theOriginal Version, by changing formats or by porting the Font Software to anew environment.
14
+ "Author" refers to any designer, engineer, programmer, technicalwriter or other person who contributed to the Font Software.
15
+ PERMISSION &amp; CONDITIONSPermission is hereby granted, free of charge, to any person obtaininga copy of the Font Software, to use, study, copy, merge, embed, modify,redistribute, and sell modified and unmodified copies of the FontSoftware, subject to the following conditions:
16
+ 1) Neither the Font Software nor any of its individual components,in Original or Modified Versions, may be sold by itself.
17
+ 2) Original or Modified Versions of the Font Software may be bundled,redistributed and/or sold with any software, provided that each copycontains the above copyright notice and this license. These can beincluded either as stand-alone text files, human-readable headers orin the appropriate machine-readable metadata fields within text orbinary files as long as those fields can be easily viewed by the user.
18
+ 3) No Modified Version of the Font Software may use the Reserved FontName(s) unless explicit written permission is granted by the correspondingCopyright Holder. This restriction only applies to the primary font name aspresented to the users.
19
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the FontSoftware shall not be used to promote, endorse or advertise anyModified Version, except to acknowledge the contribution(s) of theCopyright Holder(s) and the Author(s) or with their explicit writtenpermission.
20
+ 5) The Font Software, modified or unmodified, in part or in whole,must be distributed entirely under this license, and must not bedistributed under any other license. The requirement for fonts toremain under this license does not apply to any document createdusing the Font Software.
21
+ TERMINATIONThis license becomes null and void if any of the above conditions arenot met.
22
+ DISCLAIMERTHE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OFMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENTOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THECOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIALDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISINGFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROMOTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,15 @@
1
+ /*!
2
+ * Font Family: Bebas Neue
3
+ * Designed by: Ryoichi Tsunekawa, Dharma Type
4
+ * Copyright (c) 2026 Ryoichi Tsunekawa, Dharma Type
5
+ * Source: https://www.fontshare.com/fonts/bebas-neue
6
+ * License: SIL Open Font License (OFL), Version 1.1 - see ./bebas-neue/OFL.txt
7
+ */
8
+
9
+ @font-face {
10
+ font-family: "Bebas Neue";
11
+ font-style: normal;
12
+ font-weight: 400;
13
+ font-display: swap;
14
+ src: url("./bebas-neue/bebas-neue-regular.woff2") format("woff2");
15
+ }
@@ -0,0 +1,22 @@
1
+ This font is licensed under SIL Open Font License (OFL), Version 1.1.
2
+
3
+ Copyright © 2026 Vernon AdamsCyrealJacques Le Bailly
4
+ https://github.com/googlefonts/nunito
5
+
6
+ -----------------------------------------------------------SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
7
+ -----------------------------------------------------------
8
+ PREAMBLEThe goals of the Open Font License (OFL) are to stimulate worldwidedevelopment of collaborative font projects, to support the font creationefforts of academic and linguistic communities, and to provide a free andopen framework in which fonts may be shared and improved in partnershipwith others.
9
+ The OFL allows the licensed fonts to be used, studied, modified andredistributed freely as long as they are not sold by themselves. Thefonts, including any derivative works, can be bundled, embedded,redistributed and/or sold with any software provided that any reservednames are not used by derivative works. The fonts and derivatives,however, cannot be released under any other type of license. Therequirement for fonts to remain under this license does not applyto any document created using the fonts or their derivatives.
10
+ DEFINITIONS"Font Software" refers to the set of files released by the CopyrightHolder(s) under this license and clearly marked as such. This mayinclude source files, build scripts and documentation.
11
+ "Reserved Font Name" refers to any names specified as such after thecopyright statement(s).
12
+ "Original Version" refers to the collection of Font Software components asdistributed by the Copyright Holder(s).
13
+ "Modified Version" refers to any derivative made by adding to, deleting,or substituting -- in part or in whole -- any of the components of theOriginal Version, by changing formats or by porting the Font Software to anew environment.
14
+ "Author" refers to any designer, engineer, programmer, technicalwriter or other person who contributed to the Font Software.
15
+ PERMISSION &amp; CONDITIONSPermission is hereby granted, free of charge, to any person obtaininga copy of the Font Software, to use, study, copy, merge, embed, modify,redistribute, and sell modified and unmodified copies of the FontSoftware, subject to the following conditions:
16
+ 1) Neither the Font Software nor any of its individual components,in Original or Modified Versions, may be sold by itself.
17
+ 2) Original or Modified Versions of the Font Software may be bundled,redistributed and/or sold with any software, provided that each copycontains the above copyright notice and this license. These can beincluded either as stand-alone text files, human-readable headers orin the appropriate machine-readable metadata fields within text orbinary files as long as those fields can be easily viewed by the user.
18
+ 3) No Modified Version of the Font Software may use the Reserved FontName(s) unless explicit written permission is granted by the correspondingCopyright Holder. This restriction only applies to the primary font name aspresented to the users.
19
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the FontSoftware shall not be used to promote, endorse or advertise anyModified Version, except to acknowledge the contribution(s) of theCopyright Holder(s) and the Author(s) or with their explicit writtenpermission.
20
+ 5) The Font Software, modified or unmodified, in part or in whole,must be distributed entirely under this license, and must not bedistributed under any other license. The requirement for fonts toremain under this license does not apply to any document createdusing the Font Software.
21
+ TERMINATIONThis license becomes null and void if any of the above conditions arenot met.
22
+ DISCLAIMERTHE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OFMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENTOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THECOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIALDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISINGFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROMOTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,71 @@
1
+ /*!
2
+ * Font Family: Nunito
3
+ * Designed by: Vernon Adams, Cyreal, Jacques Le Bailly
4
+ * Copyright (c) 2026 Vernon Adams, Cyreal, Jacques Le Bailly
5
+ * Source: https://www.fontshare.com/fonts/nunito
6
+ * License: SIL Open Font License (OFL), Version 1.1 - see ./nunito/OFL.txt
7
+ */
8
+
9
+ @font-face {
10
+ font-family: "Nunito";
11
+ font-style: normal;
12
+ font-weight: 200;
13
+ font-display: swap;
14
+ src: url("./nunito/nunito-extralight.woff2") format("woff2");
15
+ }
16
+
17
+ @font-face {
18
+ font-family: "Nunito";
19
+ font-style: normal;
20
+ font-weight: 300;
21
+ font-display: swap;
22
+ src: url("./nunito/nunito-light.woff2") format("woff2");
23
+ }
24
+
25
+ @font-face {
26
+ font-family: "Nunito";
27
+ font-style: normal;
28
+ font-weight: 400;
29
+ font-display: swap;
30
+ src: url("./nunito/nunito-regular.woff2") format("woff2");
31
+ }
32
+
33
+ @font-face {
34
+ font-family: "Nunito";
35
+ font-style: normal;
36
+ font-weight: 500;
37
+ font-display: swap;
38
+ src: url("./nunito/nunito-medium.woff2") format("woff2");
39
+ }
40
+
41
+ @font-face {
42
+ font-family: "Nunito";
43
+ font-style: normal;
44
+ font-weight: 600;
45
+ font-display: swap;
46
+ src: url("./nunito/nunito-semibold.woff2") format("woff2");
47
+ }
48
+
49
+ @font-face {
50
+ font-family: "Nunito";
51
+ font-style: normal;
52
+ font-weight: 700;
53
+ font-display: swap;
54
+ src: url("./nunito/nunito-bold.woff2") format("woff2");
55
+ }
56
+
57
+ @font-face {
58
+ font-family: "Nunito";
59
+ font-style: normal;
60
+ font-weight: 800;
61
+ font-display: swap;
62
+ src: url("./nunito/nunito-extrabold.woff2") format("woff2");
63
+ }
64
+
65
+ @font-face {
66
+ font-family: "Nunito";
67
+ font-style: normal;
68
+ font-weight: 900;
69
+ font-display: swap;
70
+ src: url("./nunito/nunito-black.woff2") format("woff2");
71
+ }
@@ -0,0 +1,14 @@
1
+ import { clsx } from "clsx";
2
+ import { extendTailwindMerge } from "tailwind-merge";
3
+
4
+ import type { ClassValue } from "clsx";
5
+
6
+ const twMerge = extendTailwindMerge({
7
+ extend: {
8
+ classGroups: {
9
+ "font-size": ["text-mc"],
10
+ },
11
+ },
12
+ });
13
+
14
+ export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
@@ -0,0 +1,31 @@
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
+ };
@@ -0,0 +1,35 @@
1
+ import { useState } from "react";
2
+
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
+ };
17
+
18
+ export const useRipple = (): UseRipple => {
19
+ const [ripple, setRipple] = useState<Ripple | null>(null);
20
+
21
+ const startRipple = (event: PointerEvent<HTMLElement>) => {
22
+ const bounds = event.currentTarget.getBoundingClientRect();
23
+
24
+ setRipple({
25
+ id: Date.now(),
26
+ size: Math.hypot(bounds.width, bounds.height) * 2,
27
+ x: event.clientX - bounds.left,
28
+ y: event.clientY - bounds.top,
29
+ });
30
+ };
31
+
32
+ const endRipple = () => setRipple(null);
33
+
34
+ return { ripple, endRipple, startRipple };
35
+ };
@@ -0,0 +1,30 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+
3
+ import { themeClient } from "../helpers/theme";
4
+
5
+ import type { Theme } from "../helpers/theme";
6
+
7
+ export const useTheme = (): [Theme, (next: Theme | ((current: Theme) => Theme)) => void] => {
8
+ const [theme, setThemeState] = useState<Theme>(() => themeClient.get());
9
+
10
+ useEffect(() => {
11
+ if (typeof document === "undefined") return;
12
+
13
+ const root = document.documentElement;
14
+ setThemeState(themeClient.get());
15
+
16
+ const observer = new MutationObserver(() => {
17
+ setThemeState(themeClient.get());
18
+ });
19
+
20
+ observer.observe(root, { attributes: true, attributeFilter: ["class"] });
21
+
22
+ return () => observer.disconnect();
23
+ }, []);
24
+
25
+ const setTheme = useCallback((next: Theme | ((current: Theme) => Theme)) => {
26
+ themeClient.set(typeof next === "function" ? next(themeClient.get()) : next);
27
+ }, []);
28
+
29
+ return [theme, setTheme];
30
+ };
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export { BorderGlow } from "./components/border_glow";
2
+ export { Button } from "./components/button";
3
+ export { Card } from "./components/card";
4
+ export { MetaColorScheme } from "./components/meta_color_scheme";
5
+ export { Ripple } from "./components/ripple";
6
+
7
+ export { useTheme } from "./hooks/use_theme";
8
+
9
+ export { cn } from "./helpers/cn";
10
+ export { THEME, themeClient } from "./helpers/theme";
11
+
12
+ export type { BorderGlowChildProps, BorderGlowPosition, BorderGlowProps } from "./components/border_glow";
13
+ export type { ButtonProps } from "./components/button";
14
+ export type { CardProps } from "./components/card";
15
+ export type { RippleChildProps, RippleProps } from "./components/ripple";
16
+ export type { Theme } from "./helpers/theme";