margo-ui 1.2.3 → 1.3.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/README.md +20 -4
- package/package.json +11 -1
- package/src/components/button/button.tsx +4 -4
- package/src/components/button/style.ts +3 -3
- package/src/components/button/type.ts +3 -3
- package/src/components/button_micro/button_micro.tsx +37 -0
- package/src/components/button_micro/style.ts +6 -0
- package/src/components/button_micro/type.ts +10 -0
- package/src/components/dialog/dialog.tsx +25 -0
- package/src/components/dialog/style.ts +6 -0
- package/src/components/dialog/type.ts +8 -0
- package/src/components/header/header.tsx +36 -0
- package/src/components/header/style.ts +8 -0
- package/src/components/header/type.ts +20 -0
- package/src/components/item/item.tsx +57 -0
- package/src/components/item/style.ts +13 -0
- package/src/components/item/type.ts +20 -0
- package/src/components/layer/layer.css +51 -0
- package/src/components/layer/layer.tsx +44 -0
- package/src/components/layer/style.ts +2 -0
- package/src/components/layer/type.ts +8 -0
- package/src/components/navigation_bar/navigation_bar.css +21 -0
- package/src/components/navigation_bar/navigation_bar.tsx +70 -0
- package/src/components/navigation_bar/style.ts +6 -0
- package/src/components/navigation_bar/type.ts +9 -0
- package/src/components/progress_bar/progress_bar.css +4 -7
- package/src/components/progress_bar/progress_bar.tsx +7 -4
- package/src/components/progress_bar/style.ts +2 -0
- package/src/components/progress_bar/type.ts +1 -0
- package/src/components/sheet/sheet.css +41 -0
- package/src/components/sheet/sheet.tsx +32 -0
- package/src/components/sheet/style.ts +14 -0
- package/src/components/sheet/type.ts +14 -0
- package/src/components/snippet/highlight.ts +83 -0
- package/src/components/snippet/snippet.tsx +63 -0
- package/src/components/snippet/style.ts +25 -0
- package/src/components/snippet/type.ts +26 -0
- package/src/components/table/style.ts +13 -0
- package/src/components/table/table.tsx +60 -0
- package/src/components/table/type.ts +14 -0
- package/src/components/tooltip/tooltip.tsx +7 -7
- package/src/components/tooltip/type.ts +2 -2
- package/src/css/grid.css +20 -0
- package/src/css/theme.css +8 -0
- package/src/css/variables.css +41 -3
- package/src/hoc/background_glow/background_glow.css +19 -9
- package/src/hoc/background_glow/background_glow.tsx +11 -4
- package/src/hoc/background_glow/style.ts +0 -8
- package/src/hoc/background_glow/type.ts +5 -4
- package/src/hoc/mask_gradient_x/mask_gradient_x.css +17 -0
- package/src/hoc/mask_gradient_x/mask_gradient_x.tsx +22 -0
- package/src/hoc/mask_gradient_x/type.ts +15 -0
- package/src/index.ts +33 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# margo-ui
|
|
2
2
|
|
|
3
|
-
A small React UI kit built on Tailwind CSS v4 design tokens
|
|
3
|
+
A small React UI kit built on Tailwind CSS v4 design tokens, themeable through plain CSS custom properties.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Documentation: [dev.robertoattanasio.com/margo-ui](https://dev.robertoattanasio.com/margo-ui)
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -10,8 +10,6 @@ The package is published as TypeScript source: your bundler compiles it, exactly
|
|
|
10
10
|
npm install margo-ui
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
## Getting started
|
|
14
|
-
|
|
15
13
|
```css
|
|
16
14
|
/* your entry stylesheet */
|
|
17
15
|
@import "tailwindcss";
|
|
@@ -20,6 +18,24 @@ npm install margo-ui
|
|
|
20
18
|
@import "margo-ui/css";
|
|
21
19
|
```
|
|
22
20
|
|
|
21
|
+
Order matters: `tailwindcss` first, the kit after, your own overrides last. The font imports are optional.
|
|
22
|
+
|
|
23
|
+
## Built with
|
|
24
|
+
|
|
25
|
+
React 19, Tailwind CSS v4, TypeScript. [react-renderable](https://www.npmjs.com/package/react-renderable) for polymorphic rendering, clsx and tailwind-merge for class resolution, react-icons for the few built-in glyphs. Nunito and Bebas Neue are bundled as self-hosted woff2.
|
|
26
|
+
|
|
27
|
+
The package is published as TypeScript source: your bundler compiles it, exactly as it already has to scan it for Tailwind classes.
|
|
28
|
+
|
|
29
|
+
## Approach
|
|
30
|
+
|
|
31
|
+
- **Tokens first.** Colours, type and elevation are `--margo-` custom properties, mirrored into Tailwind's theme: redeclaring them themes the kit. Per-instance overrides go through `className`, merged over the defaults.
|
|
32
|
+
- **CSS first.** Effects live on pseudo-elements; JavaScript only writes coordinates. Most components hold no state.
|
|
33
|
+
- **Polymorphic.** `as` chooses the rendered element, and the remaining props are typed against it.
|
|
34
|
+
- **Composed, not configured.** Components expose parts to mount rather than booleans to set.
|
|
35
|
+
- **The consumer owns the layout.** Components style themselves and never their position.
|
|
36
|
+
|
|
37
|
+
Conventions for contributing: [RULE.md](./RULE.md).
|
|
38
|
+
|
|
23
39
|
## License
|
|
24
40
|
|
|
25
41
|
Code: [MIT](./LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "margo-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|
|
@@ -12,6 +12,15 @@
|
|
|
12
12
|
],
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"author": "Roberto Attanasio",
|
|
15
|
+
"homepage": "https://dev.robertoattanasio.com/margo-ui",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/robertoattanasio/dev.robertoattanasio.com.git",
|
|
19
|
+
"directory": "lib/margo-ui"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/robertoattanasio/dev.robertoattanasio.com/issues"
|
|
23
|
+
},
|
|
15
24
|
"type": "module",
|
|
16
25
|
"main": "./src/index.ts",
|
|
17
26
|
"types": "./src/index.ts",
|
|
@@ -42,6 +51,7 @@
|
|
|
42
51
|
},
|
|
43
52
|
"dependencies": {
|
|
44
53
|
"clsx": "^2.1.1",
|
|
54
|
+
"react-icons": "^5.7.0",
|
|
45
55
|
"react-renderable": "^1.0.4",
|
|
46
56
|
"tailwind-merge": "^3.6.0"
|
|
47
57
|
},
|
|
@@ -39,7 +39,7 @@ export const Button = <T extends ElementType = "button">({
|
|
|
39
39
|
<Ripple>
|
|
40
40
|
<Tag
|
|
41
41
|
{...Tag.forward<T>(rest, "button")}
|
|
42
|
-
data-open={open}
|
|
42
|
+
data-margo-open={open}
|
|
43
43
|
onClick={handleClick}
|
|
44
44
|
className={cn(
|
|
45
45
|
buttonBaseClassName,
|
|
@@ -52,11 +52,11 @@ export const Button = <T extends ElementType = "button">({
|
|
|
52
52
|
);
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
Button.Icon = ({ icon, className
|
|
55
|
+
Button.Icon = ({ icon, className }: ButtonIconProps) => (
|
|
56
56
|
<span className={cn(buttonIconClassName, className)}>{icon}</span>
|
|
57
57
|
);
|
|
58
58
|
|
|
59
|
-
Button.Label = ({ label, className
|
|
59
|
+
Button.Label = ({ label, className }: ButtonLabelProps) => (
|
|
60
60
|
<span className={cn(buttonLabelClassName, className)}>{label}</span>
|
|
61
61
|
);
|
|
62
62
|
|
|
@@ -66,7 +66,7 @@ Button.IconLabel = ({
|
|
|
66
66
|
gap = "0.5rem",
|
|
67
67
|
reverse = false,
|
|
68
68
|
side = "end",
|
|
69
|
-
className
|
|
69
|
+
className,
|
|
70
70
|
}: ButtonIconLabelProps) => (
|
|
71
71
|
<span
|
|
72
72
|
style={{ "--button-gap": gap } as CSSProperties}
|
|
@@ -17,11 +17,11 @@ export const buttonGridClassName =
|
|
|
17
17
|
|
|
18
18
|
export const buttonGridForwardClassName = `grid-cols-[1rem_0fr] group-hover/button:grid-cols-[1rem_1fr] group-hover/button:gap-(--button-gap)
|
|
19
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)`;
|
|
20
|
+
group-data-[margo-open=true]/button:grid-cols-[1rem_1fr] group-data-[margo-open=true]/button:gap-(--button-gap)`;
|
|
21
21
|
|
|
22
22
|
export const buttonGridReverseClassName = `grid-cols-[auto_0rem] group-hover/button:grid-cols-[auto_1rem] group-hover/button:gap-(--button-gap)
|
|
23
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)`;
|
|
24
|
+
group-data-[margo-open=true]/button:grid-cols-[auto_1rem] group-data-[margo-open=true]/button:gap-(--button-gap)`;
|
|
25
25
|
|
|
26
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`;
|
|
27
|
+
group-hover/button:opacity-100 group-focus-visible/button:opacity-100 group-data-[margo-open=true]/button:opacity-100`;
|
|
@@ -15,12 +15,12 @@ export type ButtonProps<T extends ElementType = "button"> = TagProps<T, ButtonOw
|
|
|
15
15
|
|
|
16
16
|
export type ButtonIconProps = {
|
|
17
17
|
icon: ReactNode;
|
|
18
|
-
className?: string
|
|
18
|
+
className?: string;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
export type ButtonLabelProps = {
|
|
22
22
|
label: string;
|
|
23
|
-
className?: string
|
|
23
|
+
className?: string;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
export type ButtonIconLabelProps = {
|
|
@@ -29,5 +29,5 @@ export type ButtonIconLabelProps = {
|
|
|
29
29
|
gap?: string;
|
|
30
30
|
reverse?: boolean;
|
|
31
31
|
side?: ButtonIconLabelSide;
|
|
32
|
-
className?: string
|
|
32
|
+
className?: string;
|
|
33
33
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Tag } from "react-renderable";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../utils/cn/cn.js";
|
|
4
|
+
import { buttonMicroActiveClassName, buttonMicroBaseClassName, buttonMicroSurfaceClassName } from "./style.js";
|
|
5
|
+
|
|
6
|
+
import type { ElementType, MouseEvent } from "react";
|
|
7
|
+
import type { ButtonMicroProps } from "./type.js";
|
|
8
|
+
|
|
9
|
+
export const ButtonMicro = <T extends ElementType = "button">({
|
|
10
|
+
active = false,
|
|
11
|
+
onClickBlur,
|
|
12
|
+
onClick,
|
|
13
|
+
className,
|
|
14
|
+
...rest
|
|
15
|
+
}: ButtonMicroProps<T>) => {
|
|
16
|
+
const handleClick = (event: MouseEvent<HTMLElement>) => {
|
|
17
|
+
onClick?.(event);
|
|
18
|
+
if (typeof onClickBlur === "function") {
|
|
19
|
+
const target = event.currentTarget;
|
|
20
|
+
onClickBlur(event);
|
|
21
|
+
setTimeout(() => target.blur(), 150);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Tag
|
|
27
|
+
{...Tag.forward<T>(rest, "button")}
|
|
28
|
+
data-margo-active={active}
|
|
29
|
+
onClick={handleClick}
|
|
30
|
+
className={cn(
|
|
31
|
+
buttonMicroBaseClassName,
|
|
32
|
+
active ? buttonMicroActiveClassName : buttonMicroSurfaceClassName,
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const buttonMicroBaseClassName = `w-fit text-xs underline underline-offset-2 outline-none
|
|
2
|
+
focus-visible:text-on-main`;
|
|
3
|
+
|
|
4
|
+
export const buttonMicroSurfaceClassName = "text-medium hover:text-on-main";
|
|
5
|
+
|
|
6
|
+
export const buttonMicroActiveClassName = "text-primary-darken";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TagProps } from "react-renderable";
|
|
2
|
+
import type { ElementType, MouseEventHandler } from "react";
|
|
3
|
+
|
|
4
|
+
export type ButtonMicroOwnProps = {
|
|
5
|
+
active?: boolean;
|
|
6
|
+
onClick?: MouseEventHandler<HTMLElement>;
|
|
7
|
+
onClickBlur?: MouseEventHandler<HTMLElement>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ButtonMicroProps<T extends ElementType = "button"> = TagProps<T, ButtonMicroOwnProps>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Tag } from "react-renderable";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../utils/cn/cn.js";
|
|
4
|
+
import { dialogBaseClassName, dialogBodyClassName, dialogFooterClassName } from "./style.js";
|
|
5
|
+
|
|
6
|
+
import type { ElementType } from "react";
|
|
7
|
+
import type { DialogBodyProps, DialogFooterProps, DialogProps } from "./type.js";
|
|
8
|
+
|
|
9
|
+
export const Dialog = <T extends ElementType = "div">({ className, children, ...rest }: DialogProps<T>) => (
|
|
10
|
+
<Tag {...Tag.forward<T>(rest)} className={cn(dialogBaseClassName, className)}>
|
|
11
|
+
{children}
|
|
12
|
+
</Tag>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
Dialog.Body = <T extends ElementType = "div">({ className, children, ...rest }: DialogBodyProps<T>) => (
|
|
16
|
+
<Tag {...Tag.forward<T>(rest)} className={cn(dialogBodyClassName, className)}>
|
|
17
|
+
{children}
|
|
18
|
+
</Tag>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
Dialog.Footer = <T extends ElementType = "div">({ className, children, ...rest }: DialogFooterProps<T>) => (
|
|
22
|
+
<Tag {...Tag.forward<T>(rest)} className={cn(dialogFooterClassName, className)}>
|
|
23
|
+
{children}
|
|
24
|
+
</Tag>
|
|
25
|
+
);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const dialogBaseClassName = `flex m-4 max-h-[calc(100%-2rem)] w-[calc(100%-2rem)] flex-col overflow-hidden
|
|
2
|
+
rounded-lg border-2 border-border bg-main text-on-main shadow-sm`;
|
|
3
|
+
|
|
4
|
+
export const dialogBodyClassName = "min-h-0 flex-1 overflow-y-auto px-5 py-4 text-sm";
|
|
5
|
+
|
|
6
|
+
export const dialogFooterClassName = "flex items-center justify-end gap-2 border-t-2 border-border px-5 py-4";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TagProps } from "react-renderable";
|
|
2
|
+
import type { ElementType } from "react";
|
|
3
|
+
|
|
4
|
+
export type DialogProps<T extends ElementType = "div"> = TagProps<T>;
|
|
5
|
+
|
|
6
|
+
export type DialogBodyProps<T extends ElementType = "div"> = TagProps<T>;
|
|
7
|
+
|
|
8
|
+
export type DialogFooterProps<T extends ElementType = "div"> = TagProps<T>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Tag } from "react-renderable";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../utils/cn/cn.js";
|
|
4
|
+
import {
|
|
5
|
+
headerBaseClassName,
|
|
6
|
+
headerLeadingClassName,
|
|
7
|
+
headerTitleClassName,
|
|
8
|
+
headerTrailingClassName,
|
|
9
|
+
} from "./style.js";
|
|
10
|
+
|
|
11
|
+
import type { ElementType } from "react";
|
|
12
|
+
import type { HeaderLeadingProps, HeaderProps, HeaderTitleProps, HeaderTrailingProps } from "./type.js";
|
|
13
|
+
|
|
14
|
+
export const Header = <T extends ElementType = "header">({ className, children, ...rest }: HeaderProps<T>) => (
|
|
15
|
+
<Tag {...Tag.forward<T>(rest, "header")} className={cn(headerBaseClassName, className)}>
|
|
16
|
+
{children}
|
|
17
|
+
</Tag>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
Header.Leading = ({ children, className }: HeaderLeadingProps) => (
|
|
21
|
+
<span data-margo-header-slot={true} className={cn(headerLeadingClassName, className)}>
|
|
22
|
+
{children}
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
Header.Title = ({ title, id, className }: HeaderTitleProps) => (
|
|
27
|
+
<span data-margo-header-slot={true} id={id} className={cn(headerTitleClassName, className)}>
|
|
28
|
+
{title}
|
|
29
|
+
</span>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
Header.Trailing = ({ children, className }: HeaderTrailingProps) => (
|
|
33
|
+
<span data-margo-header-slot={true} className={cn(headerTrailingClassName, className)}>
|
|
34
|
+
{children}
|
|
35
|
+
</span>
|
|
36
|
+
);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const headerBaseClassName = "flex w-full items-center gap-3 px-5 py-4 shadow-sm";
|
|
2
|
+
|
|
3
|
+
export const headerLeadingClassName = "flex shrink-0 items-center gap-2";
|
|
4
|
+
|
|
5
|
+
export const headerTitleClassName = "min-w-0 flex-1 line-clamp-2 text-md font-semibold text-on-main";
|
|
6
|
+
|
|
7
|
+
export const headerTrailingClassName = `ml-auto flex shrink-0 items-center gap-2
|
|
8
|
+
[[data-margo-header-slot]+&]:ml-0`;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TagProps } from "react-renderable";
|
|
2
|
+
import type { ElementType, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export type HeaderProps<T extends ElementType = "header"> = TagProps<T>;
|
|
5
|
+
|
|
6
|
+
export type HeaderLeadingProps = {
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type HeaderTitleProps = {
|
|
12
|
+
title: ReactNode;
|
|
13
|
+
id?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type HeaderTrailingProps = {
|
|
18
|
+
children?: ReactNode;
|
|
19
|
+
className?: string;
|
|
20
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { MdChevronRight } from "react-icons/md";
|
|
2
|
+
|
|
3
|
+
import { Tag } from "react-renderable";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../utils/cn/cn.js";
|
|
6
|
+
import {
|
|
7
|
+
itemActiveClassName,
|
|
8
|
+
itemBaseClassName,
|
|
9
|
+
itemIconClassName,
|
|
10
|
+
itemLabelClassName,
|
|
11
|
+
itemSurfaceClassName,
|
|
12
|
+
} from "./style.js";
|
|
13
|
+
|
|
14
|
+
import type { ElementType, MouseEvent } from "react";
|
|
15
|
+
import { Ripple } from "../../hoc/ripple/ripple.js";
|
|
16
|
+
import type { ItemIconProps, ItemLabelProps, ItemProps } from "./type.js";
|
|
17
|
+
|
|
18
|
+
export const Item = <T extends ElementType = "div">({
|
|
19
|
+
active = false,
|
|
20
|
+
onClickBlur,
|
|
21
|
+
onClick,
|
|
22
|
+
className,
|
|
23
|
+
children,
|
|
24
|
+
...rest
|
|
25
|
+
}: ItemProps<T>) => {
|
|
26
|
+
const handleClick = (event: MouseEvent<HTMLElement>) => {
|
|
27
|
+
onClick?.(event);
|
|
28
|
+
if (typeof onClickBlur === "function") {
|
|
29
|
+
const target = event.currentTarget;
|
|
30
|
+
onClickBlur(event);
|
|
31
|
+
setTimeout(() => target.blur(), 150);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Ripple>
|
|
37
|
+
<Tag
|
|
38
|
+
{...Tag.forward<T>(rest)}
|
|
39
|
+
data-margo-active={active}
|
|
40
|
+
onClick={handleClick}
|
|
41
|
+
className={cn(itemBaseClassName, active ? itemActiveClassName : itemSurfaceClassName, className)}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
</Tag>
|
|
45
|
+
</Ripple>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
Item.Icon = ({ icon = <MdChevronRight className="text-md translate-x-1" />, className }: ItemIconProps) => (
|
|
50
|
+
<span data-margo-item-slot={true} className={cn(itemIconClassName, className)}>
|
|
51
|
+
{icon}
|
|
52
|
+
</span>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
Item.Label = ({ label, className }: ItemLabelProps) => (
|
|
56
|
+
<span className={cn(itemLabelClassName, className)}>{label}</span>
|
|
57
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
|
3
|
+
focus-visible:outline-offset-1 focus-visible:outline-on-main`;
|
|
4
|
+
|
|
5
|
+
export const itemSurfaceClassName = `border-transparent text-medium hover:border-border hover:text-on-main
|
|
6
|
+
hover:shadow-item focus-visible:shadow-item`;
|
|
7
|
+
|
|
8
|
+
export const itemActiveClassName = "margo-border-gradient-primary text-on-main shadow-item";
|
|
9
|
+
|
|
10
|
+
export const itemIconClassName = `ml-auto flex size-4 shrink-0 items-center justify-center first:ml-0
|
|
11
|
+
[[data-margo-item-slot]+&]:ml-0`;
|
|
12
|
+
|
|
13
|
+
export const itemLabelClassName = "min-w-0 truncate text-left";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TagProps } from "react-renderable";
|
|
2
|
+
import type { ElementType, MouseEventHandler, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export type ItemOwnProps = {
|
|
5
|
+
active?: boolean;
|
|
6
|
+
onClick?: MouseEventHandler<HTMLElement>;
|
|
7
|
+
onClickBlur?: MouseEventHandler<HTMLElement>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ItemProps<T extends ElementType = "div"> = TagProps<T, ItemOwnProps>;
|
|
11
|
+
|
|
12
|
+
export type ItemIconProps = {
|
|
13
|
+
icon?: ReactNode;
|
|
14
|
+
className?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ItemLabelProps = {
|
|
18
|
+
label: ReactNode;
|
|
19
|
+
className?: string;
|
|
20
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
.margo-layer {
|
|
2
|
+
position: fixed;
|
|
3
|
+
inset: 0;
|
|
4
|
+
z-index: var(--margo-layer-z-index);
|
|
5
|
+
transition:
|
|
6
|
+
visibility var(--margo-layer-duration) var(--margo-layer-easing),
|
|
7
|
+
overlay var(--margo-layer-duration) var(--margo-layer-easing) allow-discrete,
|
|
8
|
+
display var(--margo-layer-duration) var(--margo-layer-easing) allow-discrete;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.margo-layer:not([open]) {
|
|
12
|
+
visibility: hidden;
|
|
13
|
+
pointer-events: none;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.margo-layer > *:not([data-margo-travel]) {
|
|
17
|
+
opacity: 0;
|
|
18
|
+
transition: opacity var(--margo-layer-duration) var(--margo-layer-easing);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.margo-layer[open] > *:not([data-margo-travel]) {
|
|
22
|
+
opacity: 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@starting-style {
|
|
26
|
+
.margo-layer[open] > *:not([data-margo-travel]) {
|
|
27
|
+
opacity: 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.margo-layer::backdrop {
|
|
32
|
+
opacity: 0;
|
|
33
|
+
transition:
|
|
34
|
+
opacity var(--margo-layer-duration) var(--margo-layer-easing),
|
|
35
|
+
overlay var(--margo-layer-duration) var(--margo-layer-easing) allow-discrete,
|
|
36
|
+
display var(--margo-layer-duration) var(--margo-layer-easing) allow-discrete;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.margo-layer[open]::backdrop {
|
|
40
|
+
opacity: 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@starting-style {
|
|
44
|
+
.margo-layer[open]::backdrop {
|
|
45
|
+
opacity: 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
html:has(.margo-layer[open]) {
|
|
50
|
+
overflow: clip;
|
|
51
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../utils/cn/cn.js";
|
|
4
|
+
import { layerBaseClassName } from "./style.js";
|
|
5
|
+
|
|
6
|
+
import type { MouseEvent, SyntheticEvent } from "react";
|
|
7
|
+
import type { LayerProps } from "./type.js";
|
|
8
|
+
|
|
9
|
+
import "./layer.css";
|
|
10
|
+
|
|
11
|
+
export const Layer = ({ open = false, onClose, dismissible = true, className, children, ...rest }: LayerProps) => {
|
|
12
|
+
const layerRef = useRef<HTMLDialogElement>(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const layer = layerRef.current;
|
|
16
|
+
|
|
17
|
+
if (!layer) return;
|
|
18
|
+
if (open && !layer.open) layer.showModal();
|
|
19
|
+
if (!open && layer.open) layer.close();
|
|
20
|
+
}, [open]);
|
|
21
|
+
|
|
22
|
+
const handleClick = (event: MouseEvent<HTMLDialogElement>) => {
|
|
23
|
+
if (!dismissible || event.target !== event.currentTarget) return;
|
|
24
|
+
|
|
25
|
+
onClose?.();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleCancel = (event: SyntheticEvent<HTMLDialogElement>) => {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
onClose?.();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<dialog
|
|
35
|
+
{...rest}
|
|
36
|
+
ref={layerRef}
|
|
37
|
+
onCancel={handleCancel}
|
|
38
|
+
onClick={handleClick}
|
|
39
|
+
className={cn(layerBaseClassName, className)}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
</dialog>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
.margo-navigation-bar {
|
|
2
|
+
position: relative;
|
|
3
|
+
height: 100%;
|
|
4
|
+
width: var(--margo-navigation-bar-progress, 0%);
|
|
5
|
+
background: var(--margo-navigation-bar-fill);
|
|
6
|
+
transition: width 200ms ease-out;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.margo-navigation-bar::after {
|
|
10
|
+
content: "";
|
|
11
|
+
position: absolute;
|
|
12
|
+
top: 0;
|
|
13
|
+
right: 0;
|
|
14
|
+
height: 100%;
|
|
15
|
+
width: 6.25rem;
|
|
16
|
+
transform: rotate(3deg) translateY(-0.25rem);
|
|
17
|
+
opacity: 0.9;
|
|
18
|
+
box-shadow:
|
|
19
|
+
0 0 10px var(--margo-color-primary),
|
|
20
|
+
0 0 5px var(--margo-color-primary);
|
|
21
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { Tag } from "react-renderable";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../utils/cn/cn.js";
|
|
6
|
+
import { navigationBarBaseClassName, navigationBarDefaultFill, navigationBarTrackClassName } from "./style.js";
|
|
7
|
+
|
|
8
|
+
import type { CSSProperties, ElementType } from "react";
|
|
9
|
+
import type { NavigationBarProps } from "./type.js";
|
|
10
|
+
|
|
11
|
+
import "./navigation_bar.css";
|
|
12
|
+
|
|
13
|
+
const TRICKLE_MS = 200;
|
|
14
|
+
const TRICKLE_CEILING = 90;
|
|
15
|
+
const TRICKLE_RATIO = 0.12;
|
|
16
|
+
const START_PROGRESS = 8;
|
|
17
|
+
const HOLD_MS = 300;
|
|
18
|
+
const FADE_MS = 400;
|
|
19
|
+
|
|
20
|
+
export const NavigationBar = <T extends ElementType = "div">({
|
|
21
|
+
loading = false,
|
|
22
|
+
fill = navigationBarDefaultFill,
|
|
23
|
+
className,
|
|
24
|
+
...rest
|
|
25
|
+
}: NavigationBarProps<T>) => {
|
|
26
|
+
const [progress, setProgress] = useState(0);
|
|
27
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
28
|
+
const timeoutRef = useRef<number | undefined>(undefined);
|
|
29
|
+
const resetRef = useRef<number | undefined>(undefined);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (loading) {
|
|
33
|
+
window.clearTimeout(timeoutRef.current);
|
|
34
|
+
window.clearTimeout(resetRef.current);
|
|
35
|
+
setIsVisible(true);
|
|
36
|
+
setProgress(START_PROGRESS);
|
|
37
|
+
|
|
38
|
+
const interval = window.setInterval(
|
|
39
|
+
() => setProgress((current) => current + (TRICKLE_CEILING - current) * TRICKLE_RATIO),
|
|
40
|
+
TRICKLE_MS,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return () => window.clearInterval(interval);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setProgress((current) => (current > 0 ? 100 : 0));
|
|
47
|
+
|
|
48
|
+
timeoutRef.current = window.setTimeout(() => setIsVisible(false), HOLD_MS);
|
|
49
|
+
resetRef.current = window.setTimeout(() => setProgress(0), HOLD_MS + FADE_MS);
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
window.clearTimeout(timeoutRef.current);
|
|
53
|
+
window.clearTimeout(resetRef.current);
|
|
54
|
+
};
|
|
55
|
+
}, [loading]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Tag
|
|
59
|
+
{...Tag.forward<T>(rest)}
|
|
60
|
+
role="progressbar"
|
|
61
|
+
aria-hidden={!isVisible}
|
|
62
|
+
style={
|
|
63
|
+
{ "--margo-navigation-bar-fill": fill, "--margo-navigation-bar-progress": `${progress}%` } as CSSProperties
|
|
64
|
+
}
|
|
65
|
+
className={cn(navigationBarBaseClassName, isVisible ? "opacity-100" : "opacity-0", className)}
|
|
66
|
+
>
|
|
67
|
+
<div className={navigationBarTrackClassName} />
|
|
68
|
+
</Tag>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const navigationBarDefaultFill = "linear-gradient(to right, var(--margo-color-primary-darken), var(--margo-color-primary))";
|
|
2
|
+
|
|
3
|
+
export const navigationBarBaseClassName =
|
|
4
|
+
"fixed inset-x-0 top-0 z-max h-0.5 pointer-events-none transition-opacity duration-200 ease-out";
|
|
5
|
+
|
|
6
|
+
export const navigationBarTrackClassName = "margo-navigation-bar";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TagProps } from "react-renderable";
|
|
2
|
+
import type { ElementType } from "react";
|
|
3
|
+
|
|
4
|
+
export type NavigationBarOwnProps = {
|
|
5
|
+
loading?: boolean;
|
|
6
|
+
fill?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type NavigationBarProps<T extends ElementType = "div"> = TagProps<T, NavigationBarOwnProps>;
|
|
@@ -2,21 +2,18 @@
|
|
|
2
2
|
content: "";
|
|
3
3
|
position: absolute;
|
|
4
4
|
inset: 0;
|
|
5
|
-
background: var(
|
|
6
|
-
--margo-progress-bar-fill,
|
|
7
|
-
linear-gradient(to right, var(--color-primary-darken), var(--color-primary))
|
|
8
|
-
);
|
|
5
|
+
background: var(--margo-progress-bar-fill);
|
|
9
6
|
}
|
|
10
7
|
|
|
11
|
-
.margo-progress-bar[data-mode="determinate"]::before {
|
|
8
|
+
.margo-progress-bar[data-margo-mode="determinate"]::before {
|
|
12
9
|
clip-path: inset(0 calc(100% - var(--margo-progress-bar-value, 0%)) 0 0);
|
|
13
10
|
}
|
|
14
11
|
|
|
15
|
-
.margo-progress-bar[data-mode="determinate"][data-animate="true"]::before {
|
|
12
|
+
.margo-progress-bar[data-margo-mode="determinate"][data-margo-animate="true"]::before {
|
|
16
13
|
transition: clip-path 300ms ease-out;
|
|
17
14
|
}
|
|
18
15
|
|
|
19
|
-
.margo-progress-bar[data-mode="indeterminate"]::before {
|
|
16
|
+
.margo-progress-bar[data-margo-mode="indeterminate"]::before {
|
|
20
17
|
inset: 0 auto 0 0;
|
|
21
18
|
width: 50%;
|
|
22
19
|
animation: margo-progress-bar-indeterminate 800ms ease-in-out infinite;
|