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.
Files changed (53) hide show
  1. package/README.md +20 -4
  2. package/package.json +11 -1
  3. package/src/components/button/button.tsx +4 -4
  4. package/src/components/button/style.ts +3 -3
  5. package/src/components/button/type.ts +3 -3
  6. package/src/components/button_micro/button_micro.tsx +37 -0
  7. package/src/components/button_micro/style.ts +6 -0
  8. package/src/components/button_micro/type.ts +10 -0
  9. package/src/components/dialog/dialog.tsx +25 -0
  10. package/src/components/dialog/style.ts +6 -0
  11. package/src/components/dialog/type.ts +8 -0
  12. package/src/components/header/header.tsx +36 -0
  13. package/src/components/header/style.ts +8 -0
  14. package/src/components/header/type.ts +20 -0
  15. package/src/components/item/item.tsx +57 -0
  16. package/src/components/item/style.ts +13 -0
  17. package/src/components/item/type.ts +20 -0
  18. package/src/components/layer/layer.css +51 -0
  19. package/src/components/layer/layer.tsx +44 -0
  20. package/src/components/layer/style.ts +2 -0
  21. package/src/components/layer/type.ts +8 -0
  22. package/src/components/navigation_bar/navigation_bar.css +21 -0
  23. package/src/components/navigation_bar/navigation_bar.tsx +70 -0
  24. package/src/components/navigation_bar/style.ts +6 -0
  25. package/src/components/navigation_bar/type.ts +9 -0
  26. package/src/components/progress_bar/progress_bar.css +4 -7
  27. package/src/components/progress_bar/progress_bar.tsx +7 -4
  28. package/src/components/progress_bar/style.ts +2 -0
  29. package/src/components/progress_bar/type.ts +1 -0
  30. package/src/components/sheet/sheet.css +41 -0
  31. package/src/components/sheet/sheet.tsx +32 -0
  32. package/src/components/sheet/style.ts +14 -0
  33. package/src/components/sheet/type.ts +14 -0
  34. package/src/components/snippet/highlight.ts +83 -0
  35. package/src/components/snippet/snippet.tsx +63 -0
  36. package/src/components/snippet/style.ts +25 -0
  37. package/src/components/snippet/type.ts +26 -0
  38. package/src/components/table/style.ts +13 -0
  39. package/src/components/table/table.tsx +60 -0
  40. package/src/components/table/type.ts +14 -0
  41. package/src/components/tooltip/tooltip.tsx +7 -7
  42. package/src/components/tooltip/type.ts +2 -2
  43. package/src/css/grid.css +20 -0
  44. package/src/css/theme.css +8 -0
  45. package/src/css/variables.css +41 -3
  46. package/src/hoc/background_glow/background_glow.css +19 -9
  47. package/src/hoc/background_glow/background_glow.tsx +11 -4
  48. package/src/hoc/background_glow/style.ts +0 -8
  49. package/src/hoc/background_glow/type.ts +5 -4
  50. package/src/hoc/mask_gradient_x/mask_gradient_x.css +17 -0
  51. package/src/hoc/mask_gradient_x/mask_gradient_x.tsx +22 -0
  52. package/src/hoc/mask_gradient_x/type.ts +15 -0
  53. 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
- The package is published as TypeScript source: your bundler compiles it, exactly as it already has to scan it for Tailwind classes.
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.2.3",
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 = null }: ButtonIconProps) => (
55
+ Button.Icon = ({ icon, className }: ButtonIconProps) => (
56
56
  <span className={cn(buttonIconClassName, className)}>{icon}</span>
57
57
  );
58
58
 
59
- Button.Label = ({ label, className = null }: ButtonLabelProps) => (
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 = null,
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 | null;
18
+ className?: string;
19
19
  };
20
20
 
21
21
  export type ButtonLabelProps = {
22
22
  label: string;
23
- className?: string | null;
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 | null;
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,2 @@
1
+ export const layerBaseClassName = `margo-layer m-0 grid h-dvh max-h-none w-dvw max-w-none place-items-center
2
+ overflow-clip bg-transparent p-0 text-on-main outline-none backdrop:bg-black/50 backdrop:backdrop-blur-xs`;
@@ -0,0 +1,8 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+
3
+ export type LayerProps = Omit<ComponentPropsWithoutRef<"dialog">, "onClose" | "open"> & {
4
+ open?: boolean;
5
+ onClose?: () => void;
6
+ dismissible?: boolean;
7
+ children?: ReactNode;
8
+ };
@@ -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;