margo-ui 1.2.3 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) 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/item/item.tsx +57 -0
  7. package/src/components/item/style.ts +13 -0
  8. package/src/components/item/type.ts +20 -0
  9. package/src/components/navigation_bar/navigation_bar.css +21 -0
  10. package/src/components/navigation_bar/navigation_bar.tsx +63 -0
  11. package/src/components/navigation_bar/style.ts +6 -0
  12. package/src/components/navigation_bar/type.ts +9 -0
  13. package/src/components/progress_bar/progress_bar.css +4 -7
  14. package/src/components/progress_bar/progress_bar.tsx +7 -4
  15. package/src/components/progress_bar/style.ts +2 -0
  16. package/src/components/progress_bar/type.ts +1 -0
  17. package/src/components/snippet/highlight.ts +83 -0
  18. package/src/components/snippet/snippet.tsx +63 -0
  19. package/src/components/snippet/style.ts +25 -0
  20. package/src/components/snippet/type.ts +26 -0
  21. package/src/components/table/style.ts +13 -0
  22. package/src/components/table/table.tsx +60 -0
  23. package/src/components/table/type.ts +14 -0
  24. package/src/components/tooltip/tooltip.tsx +7 -7
  25. package/src/components/tooltip/type.ts +2 -2
  26. package/src/css/grid.css +20 -0
  27. package/src/css/theme.css +1 -0
  28. package/src/css/variables.css +31 -3
  29. package/src/hoc/background_glow/background_glow.css +19 -9
  30. package/src/hoc/background_glow/background_glow.tsx +11 -4
  31. package/src/hoc/background_glow/style.ts +0 -8
  32. package/src/hoc/background_glow/type.ts +5 -4
  33. package/src/index.ts +16 -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, not props.** Every visual decision is a `--margo-` custom property, mirrored into Tailwind's theme. Theming means redeclaring variables, never overriding classes.
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.2.4",
4
4
  "description": "A small React UI kit built on Tailwind CSS v4 design tokens, themeable through plain CSS custom properties.",
5
5
  "keywords": [
6
6
  "react",
@@ -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,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
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,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,63 @@
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 { navigationBarDefaultFill, navigationBarBaseClassName, 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 FADE_MS = 250;
18
+
19
+ export const NavigationBar = <T extends ElementType = "div">({
20
+ loading = false,
21
+ fill = navigationBarDefaultFill,
22
+ className,
23
+ ...rest
24
+ }: NavigationBarProps<T>) => {
25
+ const [progress, setProgress] = useState(0);
26
+ const [isVisible, setIsVisible] = useState(false);
27
+ const timeoutRef = useRef<number | undefined>(undefined);
28
+
29
+ useEffect(() => {
30
+ if (loading) {
31
+ window.clearTimeout(timeoutRef.current);
32
+ setIsVisible(true);
33
+ setProgress(START_PROGRESS);
34
+
35
+ const interval = window.setInterval(
36
+ () => setProgress((current) => current + (TRICKLE_CEILING - current) * TRICKLE_RATIO),
37
+ TRICKLE_MS,
38
+ );
39
+
40
+ return () => window.clearInterval(interval);
41
+ }
42
+
43
+ setProgress((current) => (current > 0 ? 100 : 0));
44
+ timeoutRef.current = window.setTimeout(() => {
45
+ setIsVisible(false);
46
+ setProgress(0);
47
+ }, FADE_MS);
48
+
49
+ return () => window.clearTimeout(timeoutRef.current);
50
+ }, [loading]);
51
+
52
+ return (
53
+ <Tag
54
+ {...Tag.forward<T>(rest)}
55
+ role="progressbar"
56
+ aria-hidden={!isVisible}
57
+ style={{ "--margo-navigation-bar-fill": fill, "--margo-navigation-bar-progress": `${progress}%` } as CSSProperties}
58
+ className={cn(navigationBarBaseClassName, isVisible ? "opacity-100" : "opacity-0", className)}
59
+ >
60
+ <div className={navigationBarTrackClassName} />
61
+ </Tag>
62
+ );
63
+ };
@@ -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;
@@ -1,7 +1,7 @@
1
1
  import { Tag } from "react-renderable";
2
2
 
3
3
  import { cn } from "../../utils/cn/cn.js";
4
- import { progressBarBaseClassName } from "./style.js";
4
+ import { progressBarDefaultFill, progressBarBaseClassName } from "./style.js";
5
5
 
6
6
  import type { CSSProperties, ElementType } from "react";
7
7
  import type { ProgressBarProps } from "./type.js";
@@ -10,6 +10,7 @@ import "./progress_bar.css";
10
10
 
11
11
  export const ProgressBar = <T extends ElementType = "div">({
12
12
  animate = true,
13
+ fill = progressBarDefaultFill,
13
14
  mode = "indeterminate",
14
15
  style,
15
16
  value = 0,
@@ -26,9 +27,11 @@ export const ProgressBar = <T extends ElementType = "div">({
26
27
  aria-valuemin={isDeterminate ? 0 : undefined}
27
28
  aria-valuemax={isDeterminate ? 100 : undefined}
28
29
  aria-valuenow={isDeterminate ? progress : undefined}
29
- data-mode={mode}
30
- data-animate={animate}
31
- style={{ ...style, "--margo-progress-bar-value": `${progress}%` } as CSSProperties}
30
+ data-margo-mode={mode}
31
+ data-margo-animate={animate}
32
+ style={
33
+ { ...style, "--margo-progress-bar-fill": fill, "--margo-progress-bar-value": `${progress}%` } as CSSProperties
34
+ }
32
35
  className={cn(progressBarBaseClassName, className)}
33
36
  />
34
37
  );
@@ -1 +1,3 @@
1
+ export const progressBarDefaultFill = "linear-gradient(to right, var(--margo-color-primary-darken), var(--margo-color-primary))";
2
+
1
3
  export const progressBarBaseClassName = "margo-progress-bar relative h-0.5 overflow-hidden";
@@ -5,6 +5,7 @@ export type ProgressBarMode = "determinate" | "indeterminate";
5
5
 
6
6
  export type ProgressBarOwnProps = {
7
7
  animate?: boolean;
8
+ fill?: string;
8
9
  mode?: ProgressBarMode;
9
10
  value?: number;
10
11
  };
@@ -0,0 +1,83 @@
1
+ import type { SnippetToken, SnippetTokenType } from "./type.js";
2
+
3
+ const KEYWORDS = [
4
+ "as",
5
+ "async",
6
+ "await",
7
+ "break",
8
+ "case",
9
+ "catch",
10
+ "class",
11
+ "const",
12
+ "continue",
13
+ "default",
14
+ "delete",
15
+ "do",
16
+ "else",
17
+ "export",
18
+ "extends",
19
+ "false",
20
+ "for",
21
+ "from",
22
+ "function",
23
+ "if",
24
+ "implements",
25
+ "import",
26
+ "in",
27
+ "instanceof",
28
+ "interface",
29
+ "let",
30
+ "new",
31
+ "null",
32
+ "of",
33
+ "readonly",
34
+ "return",
35
+ "satisfies",
36
+ "switch",
37
+ "this",
38
+ "throw",
39
+ "true",
40
+ "try",
41
+ "type",
42
+ "typeof",
43
+ "undefined",
44
+ "var",
45
+ "void",
46
+ "while",
47
+ "yield",
48
+ ];
49
+
50
+ const PATTERNS: readonly { type: SnippetTokenType; source: string }[] = [
51
+ { type: "comment", source: "\\/\\/[^\\n]*|\\/\\*[\\s\\S]*?\\*\\/" },
52
+ { type: "string", source: "\"(?:[^\"\\\\]|\\\\.)*\"|'(?:[^'\\\\]|\\\\.)*'|`(?:[^`\\\\]|\\\\.)*`" },
53
+ { type: "tag", source: "<\\/?[A-Za-z][\\w.$]*" },
54
+ { type: "keyword", source: `\\b(?:${KEYWORDS.join("|")})\\b` },
55
+ { type: "number", source: "\\b\\d+(?:\\.\\d+)?\\b" },
56
+ { type: "attr", source: "\\b[A-Za-z_$][\\w$]*(?=\\s*[=:](?!=))" },
57
+ { type: "fn", source: "\\b[A-Za-z_$][\\w$]*(?=\\s*\\()" },
58
+ { type: "type", source: "\\b[A-Z][\\w$]*\\b" },
59
+ { type: "punct", source: "[{}()\\[\\]<>=;,.:!?|&+\\-*/]" },
60
+ ];
61
+
62
+ const TOKENIZER = new RegExp(PATTERNS.map((pattern) => `(${pattern.source})`).join("|"), "g");
63
+
64
+ export const snippetHighlight = (code: string): SnippetToken[] => {
65
+ const tokens: SnippetToken[] = [];
66
+ let cursor = 0;
67
+ let match: RegExpExecArray | null;
68
+
69
+ TOKENIZER.lastIndex = 0;
70
+
71
+ while ((match = TOKENIZER.exec(code)) !== null) {
72
+ if (match.index > cursor) tokens.push({ type: "plain", value: code.slice(cursor, match.index) });
73
+
74
+ const groupIndex = PATTERNS.findIndex((_, index) => match![index + 1] !== undefined);
75
+
76
+ tokens.push({ type: PATTERNS[groupIndex].type, value: match[0] });
77
+ cursor = match.index + match[0].length;
78
+ }
79
+
80
+ if (cursor < code.length) tokens.push({ type: "plain", value: code.slice(cursor) });
81
+
82
+ return tokens;
83
+ };
@@ -0,0 +1,63 @@
1
+ import { useState } from "react";
2
+
3
+ import { MdCheck, MdContentCopy } from "react-icons/md";
4
+
5
+ import { Guard, List, Swap, Tag } from "react-renderable";
6
+
7
+ import { cn } from "../../utils/cn/cn.js";
8
+ import { Button } from "../button/button.js";
9
+ import { snippetHighlight } from "./highlight.js";
10
+ import {
11
+ snippetBarClassName,
12
+ snippetBaseClassName,
13
+ snippetCodeClassName,
14
+ snippetCopyClassName,
15
+ snippetTitleClassName,
16
+ snippetTokenClassName,
17
+ } from "./style.js";
18
+
19
+ import type { ElementType } from "react";
20
+ import type { SnippetProps } from "./type.js";
21
+
22
+ export const Snippet = <T extends ElementType = "div">({ snippet, title, className, ...rest }: SnippetProps<T>) => {
23
+ const [isCopied, setIsCopied] = useState(false);
24
+ const source = snippet.trim();
25
+
26
+ const handleCopy = async () => {
27
+ await navigator.clipboard.writeText(source);
28
+ setIsCopied(true);
29
+ window.setTimeout(() => setIsCopied(false), 1500);
30
+ };
31
+
32
+ return (
33
+ <Tag {...Tag.forward<T>(rest)} className={cn(snippetBaseClassName, className)}>
34
+ <div className={snippetBarClassName}>
35
+ <Guard guardIf={!title} shouldHide>
36
+ <span className={snippetTitleClassName}>{title}</span>
37
+ </Guard>
38
+ <Button onClickBlur={handleCopy} aria-label="copy" aria-pressed={isCopied} className={snippetCopyClassName}>
39
+ <Button.Icon
40
+ icon={
41
+ <Swap.Boolean
42
+ swapOn={isCopied}
43
+ components={[<MdContentCopy className="text-sm" />, <MdCheck className="text-sm" />]}
44
+ />
45
+ }
46
+ />
47
+ </Button>
48
+ </div>
49
+ <pre className={snippetCodeClassName}>
50
+ <code>
51
+ <List
52
+ array={snippetHighlight(source)}
53
+ itemExtractor={({ row, index }) => (
54
+ <span key={index} className={snippetTokenClassName[row.type]}>
55
+ {row.value}
56
+ </span>
57
+ )}
58
+ />
59
+ </code>
60
+ </pre>
61
+ </Tag>
62
+ );
63
+ };
@@ -0,0 +1,25 @@
1
+ import type { SnippetTokenType } from "./type.js";
2
+
3
+ export const snippetBaseClassName = "overflow-hidden rounded-lg border-2 border-border bg-(--margo-code-surface)";
4
+
5
+ export const snippetBarClassName =
6
+ "flex items-center justify-between gap-4 border-b-2 border-border bg-(--margo-code-bar) px-3 py-2";
7
+
8
+ export const snippetTitleClassName = "text-mc uppercase tracking-widest text-medium";
9
+
10
+ export const snippetCopyClassName = "ml-auto bg-neutral/60";
11
+
12
+ export const snippetCodeClassName = "overflow-x-auto p-4 text-xs leading-relaxed";
13
+
14
+ export const snippetTokenClassName: Record<SnippetTokenType, string> = {
15
+ plain: "text-(--margo-code-plain)",
16
+ comment: "text-(--margo-code-comment) italic",
17
+ string: "text-(--margo-code-string)",
18
+ keyword: "text-(--margo-code-keyword)",
19
+ number: "text-(--margo-code-number)",
20
+ tag: "text-(--margo-code-tag)",
21
+ attr: "text-(--margo-code-attr)",
22
+ fn: "text-(--margo-code-fn)",
23
+ type: "text-(--margo-code-type)",
24
+ punct: "text-(--margo-code-punct)",
25
+ };
@@ -0,0 +1,26 @@
1
+ import type { TagProps } from "react-renderable";
2
+ import type { ElementType } from "react";
3
+
4
+ export type SnippetTokenType =
5
+ | "plain"
6
+ | "comment"
7
+ | "string"
8
+ | "keyword"
9
+ | "number"
10
+ | "tag"
11
+ | "attr"
12
+ | "fn"
13
+ | "type"
14
+ | "punct";
15
+
16
+ export type SnippetToken = {
17
+ type: SnippetTokenType;
18
+ value: string;
19
+ };
20
+
21
+ export type SnippetOwnProps = {
22
+ snippet: string;
23
+ title?: string;
24
+ };
25
+
26
+ export type SnippetProps<T extends ElementType = "div"> = TagProps<T, SnippetOwnProps>;
@@ -0,0 +1,13 @@
1
+ export const tableWrapperClassName = "w-full overflow-x-auto rounded-lg border-2 border-border";
2
+
3
+ export const tableBaseClassName = "w-full border-collapse text-xs text-on-main";
4
+
5
+ export const tableHeadClassName = "border-b-2 border-border bg-neutral/40 text-left";
6
+
7
+ export const tableBodyClassName = "align-top";
8
+
9
+ export const tableRowClassName = "border-b-2 border-border last:border-b-0";
10
+
11
+ export const tableHeadCellClassName = "px-3 py-2 font-medium whitespace-nowrap";
12
+
13
+ export const tableCellClassName = "px-3 py-2 align-top whitespace-nowrap";
@@ -0,0 +1,60 @@
1
+ import { Tag } from "react-renderable";
2
+
3
+ import { cn } from "../../utils/cn/cn.js";
4
+ import {
5
+ tableBaseClassName,
6
+ tableBodyClassName,
7
+ tableCellClassName,
8
+ tableHeadCellClassName,
9
+ tableHeadClassName,
10
+ tableRowClassName,
11
+ tableWrapperClassName,
12
+ } from "./style.js";
13
+
14
+ import type { ElementType } from "react";
15
+ import type {
16
+ TableBodyProps,
17
+ TableCellProps,
18
+ TableHeadCellProps,
19
+ TableHeadProps,
20
+ TableProps,
21
+ TableRowProps,
22
+ } from "./type.js";
23
+
24
+ export const Table = <T extends ElementType = "table">({ className, children, ...rest }: TableProps<T>) => (
25
+ <div className={tableWrapperClassName}>
26
+ <Tag {...Tag.forward<T>(rest, "table")} className={cn(tableBaseClassName, className)}>
27
+ {children}
28
+ </Tag>
29
+ </div>
30
+ );
31
+
32
+ Table.Head = <T extends ElementType = "thead">({ className, children, ...rest }: TableHeadProps<T>) => (
33
+ <Tag {...Tag.forward<T>(rest, "thead")} className={cn(tableHeadClassName, className)}>
34
+ {children}
35
+ </Tag>
36
+ );
37
+
38
+ Table.Body = <T extends ElementType = "tbody">({ className, children, ...rest }: TableBodyProps<T>) => (
39
+ <Tag {...Tag.forward<T>(rest, "tbody")} className={cn(tableBodyClassName, className)}>
40
+ {children}
41
+ </Tag>
42
+ );
43
+
44
+ Table.Row = <T extends ElementType = "tr">({ className, children, ...rest }: TableRowProps<T>) => (
45
+ <Tag {...Tag.forward<T>(rest, "tr")} className={cn(tableRowClassName, className)}>
46
+ {children}
47
+ </Tag>
48
+ );
49
+
50
+ Table.HeadCell = <T extends ElementType = "th">({ className, children, ...rest }: TableHeadCellProps<T>) => (
51
+ <Tag {...Tag.forward<T>(rest, "th")} className={cn(tableHeadCellClassName, className)}>
52
+ {children}
53
+ </Tag>
54
+ );
55
+
56
+ Table.Cell = <T extends ElementType = "td">({ className, children, ...rest }: TableCellProps<T>) => (
57
+ <Tag {...Tag.forward<T>(rest, "td")} className={cn(tableCellClassName, className)}>
58
+ {children}
59
+ </Tag>
60
+ );
@@ -0,0 +1,14 @@
1
+ import type { TagProps } from "react-renderable";
2
+ import type { ElementType } from "react";
3
+
4
+ export type TableProps<T extends ElementType = "table"> = TagProps<T>;
5
+
6
+ export type TableHeadProps<T extends ElementType = "thead"> = TagProps<T>;
7
+
8
+ export type TableBodyProps<T extends ElementType = "tbody"> = TagProps<T>;
9
+
10
+ export type TableRowProps<T extends ElementType = "tr"> = TagProps<T>;
11
+
12
+ export type TableHeadCellProps<T extends ElementType = "th"> = TagProps<T>;
13
+
14
+ export type TableCellProps<T extends ElementType = "td"> = TagProps<T>;
@@ -15,19 +15,19 @@ import type { ElementType, ReactNode } from "react";
15
15
  import type { TooltipProps } from "./type.js";
16
16
 
17
17
  export const Tooltip = <T extends ElementType = "div">({
18
- ariaLabel,
18
+ label,
19
19
  children,
20
20
  className,
21
21
  id,
22
- placeholder,
22
+ content,
23
23
  position = "down",
24
24
  ...rest
25
25
  }: TooltipProps<T>) => {
26
- const placeholderRef = useRef<ReactNode>(null);
26
+ const contentRef = useRef<ReactNode>(null);
27
27
 
28
- const isFilled = placeholder !== null && placeholder !== undefined;
28
+ const isFilled = content !== null && content !== undefined;
29
29
 
30
- if (isFilled) placeholderRef.current = placeholder;
30
+ if (isFilled) contentRef.current = content;
31
31
 
32
32
  return (
33
33
  <Tag {...Tag.forward<T>(rest)} className={cn(tooltipAnchorClassName, className)}>
@@ -35,7 +35,7 @@ export const Tooltip = <T extends ElementType = "div">({
35
35
  <span
36
36
  id={id}
37
37
  role="tooltip"
38
- aria-label={isFilled ? undefined : ariaLabel}
38
+ aria-label={isFilled ? undefined : label}
39
39
  className={cn(
40
40
  tooltipBubbleClassName,
41
41
  tooltipTransitionClassName,
@@ -43,7 +43,7 @@ export const Tooltip = <T extends ElementType = "div">({
43
43
  tooltipPositionClassName[position],
44
44
  )}
45
45
  >
46
- {placeholderRef.current}
46
+ {contentRef.current}
47
47
  </span>
48
48
  </Tag>
49
49
  );
@@ -4,9 +4,9 @@ import type { ElementType, ReactNode } from "react";
4
4
  export type TooltipPosition = "up" | "down" | "left" | "right";
5
5
 
6
6
  export type TooltipOwnProps = {
7
- ariaLabel?: string;
7
+ label?: string;
8
8
  id?: string;
9
- placeholder?: ReactNode;
9
+ content?: ReactNode;
10
10
  position?: TooltipPosition;
11
11
  children?: ReactNode;
12
12
  };
package/src/css/grid.css CHANGED
@@ -88,6 +88,26 @@
88
88
  margin-inline: calc(var(--margo-grid-bleed) * -1);
89
89
  }
90
90
 
91
+ /*
92
+ * Bleed padding: the exact counterpart of the bleed margins above. An element
93
+ * that bled off an edge gets its content back on the grid by padding it by the
94
+ * same distance, so the background reaches the viewport while the text stays
95
+ * aligned with the columns.
96
+ *
97
+ * Use on: a bled sidebar, band or bar whose children must keep the alignment.
98
+ */
99
+ @utility margo-pl-bleed {
100
+ padding-inline-start: var(--margo-grid-bleed);
101
+ }
102
+
103
+ @utility margo-pr-bleed {
104
+ padding-inline-end: var(--margo-grid-bleed);
105
+ }
106
+
107
+ @utility margo-px-bleed {
108
+ padding-inline: var(--margo-grid-bleed);
109
+ }
110
+
91
111
  /*
92
112
  * Back to the centered content area (all 12 columns), dropping any bleed.
93
113
  * Use to reset an element that a variant put on `full` at another breakpoint.
package/src/css/theme.css CHANGED
@@ -26,6 +26,7 @@
26
26
  /* Shadows */
27
27
  --shadow-card: var(--margo-shadow-card);
28
28
  --shadow-button: var(--margo-shadow-button);
29
+ --shadow-item: var(--margo-shadow-item);
29
30
  --shadow-chip: var(--margo-shadow-chip);
30
31
 
31
32
  /* Border widths */
@@ -43,17 +43,31 @@
43
43
 
44
44
  --margo-color-neutral: #ffffff;
45
45
  --margo-color-on-neutral: #000000;
46
- --margo-color-main: #f6f6f6;
46
+ --margo-color-main: #e7e7e7;
47
47
  --margo-color-on-main: #151515;
48
48
  --margo-color-primary: #57e550;
49
49
  --margo-color-primary-darken: #45b93f;
50
50
  --margo-color-secondary: #d3d3d3;
51
- --margo-color-medium: #7e7e7e;
52
- --margo-color-border: #cfcfcf;
51
+ --margo-color-medium: #858585;
52
+ --margo-color-border: #cecece;
53
53
 
54
54
  --margo-shadow-card: 0 8px 6px -4px rgb(220, 220, 220, 0.5);
55
55
  --margo-shadow-button: 0px 2px 3px -1px rgb(220, 220, 220, 0.6);
56
+ --margo-shadow-item: 0px 2px 3px -1px rgb(220, 220, 220, 0.6);
56
57
  --margo-shadow-chip: 0 1px 3px -1px rgb(0, 0, 0, 0.6);
58
+
59
+ --margo-code-surface: #fbfbfb;
60
+ --margo-code-bar: #f1f1f1;
61
+ --margo-code-plain: #24292f;
62
+ --margo-code-comment: #8b949e;
63
+ --margo-code-string: #0a7d3f;
64
+ --margo-code-keyword: #a5309a;
65
+ --margo-code-number: #b3510f;
66
+ --margo-code-tag: #1f6fd6;
67
+ --margo-code-attr: #7a5cd6;
68
+ --margo-code-fn: #0f7c93;
69
+ --margo-code-type: #1f6fd6;
70
+ --margo-code-punct: #6e7781;
57
71
  }
58
72
 
59
73
  .dark {
@@ -71,5 +85,19 @@
71
85
 
72
86
  --margo-shadow-card: 0 4px 12px -4px rgb(0, 0, 0, 0.5);
73
87
  --margo-shadow-button: 0 2px 6px -2px rgb(0, 0, 0, 0.5);
88
+ --margo-shadow-item: 0 2px 6px -2px rgb(0, 0, 0, 0.5);
74
89
  --margo-shadow-chip: 0 1px 3px -1px rgb(0, 0, 0, 0.5);
90
+
91
+ --margo-code-surface: #0d0d0d;
92
+ --margo-code-bar: #161616;
93
+ --margo-code-plain: #e6e6e6;
94
+ --margo-code-comment: #6b6b6b;
95
+ --margo-code-string: #7ee787;
96
+ --margo-code-keyword: #ff7bd5;
97
+ --margo-code-number: #ffab70;
98
+ --margo-code-tag: #79c0ff;
99
+ --margo-code-attr: #c3a6ff;
100
+ --margo-code-fn: #56d4dd;
101
+ --margo-code-type: #79c0ff;
102
+ --margo-code-punct: #8b949e;
75
103
  }
@@ -7,26 +7,36 @@
7
7
  opacity: 0;
8
8
  border-radius: inherit;
9
9
  background: radial-gradient(
10
- var(--margo-background-glow-size) circle at var(--margo-background-glow-x, 50%) var(--margo-background-glow-y, 50%),
10
+ var(--margo-background-glow-size, 480px) circle at var(--margo-background-glow-x, 50%) var(--margo-background-glow-y, 50%),
11
11
  var(--color-primary),
12
12
  transparent 70%
13
13
  );
14
14
  transition: opacity 300ms ease-out;
15
15
  }
16
16
 
17
+ .margo-background-glow::after {
18
+ content: "";
19
+ position: absolute;
20
+ inset: 0;
21
+ z-index: -1;
22
+ pointer-events: none;
23
+ opacity: 0;
24
+ border-radius: inherit;
25
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
26
+ background-size: var(--margo-background-glow-noise-tile, 120px);
27
+ mix-blend-mode: overlay;
28
+ transition: opacity 300ms ease-out;
29
+ }
30
+
17
31
  .margo-background-glow:hover::before,
18
32
  .margo-background-glow:focus-within::before {
19
33
  opacity: var(--margo-background-glow-opacity, 0.1);
20
34
  }
21
35
 
22
- .margo-background-glow--size-sm {
23
- --margo-background-glow-size: 200px;
36
+ .margo-background-glow:hover::after,
37
+ .margo-background-glow:focus-within::after {
38
+ opacity: var(--margo-background-glow-noise-opacity, 0.04);
24
39
  }
25
40
 
26
- .margo-background-glow--size-md {
27
- --margo-background-glow-size: 320px;
28
- }
29
41
 
30
- .margo-background-glow--size-lg {
31
- --margo-background-glow-size: 480px;
32
- }
42
+
@@ -1,17 +1,19 @@
1
1
  import { cloneElement } from "react";
2
2
 
3
3
  import { cn } from "../../utils/cn/cn.js";
4
- import { backgroundGlowBaseClassName, backgroundGlowSizeClassName } from "./style.js";
4
+ import { backgroundGlowBaseClassName } from "./style.js";
5
5
 
6
- import type { PointerEvent } from "react";
6
+ import type { CSSProperties, PointerEvent } from "react";
7
7
  import type { BackgroundGlowProps } from "./type.js";
8
8
 
9
9
  import "./background_glow.css";
10
10
 
11
- export const BackgroundGlow = ({ children, size = "md" }: BackgroundGlowProps) => {
11
+ export const BackgroundGlow = ({ children, tolerance = 1, opacity = 0.2, noise = 0.04 }: BackgroundGlowProps) => {
12
12
  const handlePointerMove = (event: PointerEvent<HTMLElement>) => {
13
13
  const bounds = event.currentTarget.getBoundingClientRect();
14
+ const spread = Math.max(bounds.width, bounds.height) * Math.min(Math.max(tolerance, 0), 1);
14
15
 
16
+ event.currentTarget.style.setProperty("--margo-background-glow-size", `${spread}px`);
15
17
  event.currentTarget.style.setProperty("--margo-background-glow-x", `${event.clientX - bounds.left}px`);
16
18
  event.currentTarget.style.setProperty("--margo-background-glow-y", `${event.clientY - bounds.top}px`);
17
19
 
@@ -19,7 +21,12 @@ export const BackgroundGlow = ({ children, size = "md" }: BackgroundGlowProps) =
19
21
  };
20
22
 
21
23
  return cloneElement(children, {
22
- className: cn(backgroundGlowBaseClassName, backgroundGlowSizeClassName[size], children.props.className),
24
+ className: cn(backgroundGlowBaseClassName, children.props.className),
25
+ style: {
26
+ ...children.props.style,
27
+ "--margo-background-glow-opacity": opacity,
28
+ "--margo-background-glow-noise-opacity": noise,
29
+ } as CSSProperties,
23
30
  onPointerMove: handlePointerMove,
24
31
  });
25
32
  };
@@ -1,9 +1 @@
1
- import type { BackgroundGlowSize } from "./type.js";
2
-
3
1
  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
- };
@@ -1,13 +1,14 @@
1
- import type { PointerEventHandler, ReactElement } from "react";
2
-
3
- export type BackgroundGlowSize = "sm" | "md" | "lg";
1
+ import type { CSSProperties, PointerEventHandler, ReactElement } from "react";
4
2
 
5
3
  export type BackgroundGlowChildProps = {
6
4
  className?: string;
5
+ style?: CSSProperties;
7
6
  onPointerMove?: PointerEventHandler<HTMLElement>;
8
7
  };
9
8
 
10
9
  export type BackgroundGlowProps = {
11
10
  children: ReactElement<BackgroundGlowChildProps>;
12
- size?: BackgroundGlowSize;
11
+ tolerance?: number;
12
+ opacity?: number;
13
+ noise?: number;
13
14
  };
package/src/index.ts CHANGED
@@ -2,8 +2,12 @@ export { Blockquote } from "./components/blockquote/blockquote.js";
2
2
  export { Button } from "./components/button/button.js";
3
3
  export { Card } from "./components/card/card.js";
4
4
  export { Chip } from "./components/chip/chip.js";
5
+ export { Item } from "./components/item/item.js";
6
+ export { NavigationBar } from "./components/navigation_bar/navigation_bar.js";
5
7
  export { ProgressBar } from "./components/progress_bar/progress_bar.js";
8
+ export { Table } from "./components/table/table.js";
6
9
  export { Splash } from "./components/splash/splash.js";
10
+ export { Snippet } from "./components/snippet/snippet.js";
7
11
  export { Spinner } from "./components/spinner/spinner.js";
8
12
  export { Tooltip } from "./components/tooltip/tooltip.js";
9
13
 
@@ -30,12 +34,23 @@ export type {
30
34
  } from "./components/button/type.js";
31
35
  export type { CardProps } from "./components/card/type.js";
32
36
  export type { ChipProps } from "./components/chip/type.js";
37
+ export type { ItemIconProps, ItemLabelProps, ItemProps } from "./components/item/type.js";
38
+ export type { NavigationBarProps } from "./components/navigation_bar/type.js";
33
39
  export type { ProgressBarMode, ProgressBarProps } from "./components/progress_bar/type.js";
40
+ export type {
41
+ TableBodyProps,
42
+ TableCellProps,
43
+ TableHeadCellProps,
44
+ TableHeadProps,
45
+ TableProps,
46
+ TableRowProps,
47
+ } from "./components/table/type.js";
34
48
  export type { SplashProps, SplashServeProps } from "./components/splash/type.js";
49
+ export type { SnippetProps, SnippetToken, SnippetTokenType } from "./components/snippet/type.js";
35
50
  export type { SpinnerProps } from "./components/spinner/type.js";
36
51
  export type { TooltipPosition, TooltipProps } from "./components/tooltip/type.js";
37
52
 
38
- export type { BackgroundGlowProps, BackgroundGlowSize } from "./hoc/background_glow/type.js";
53
+ export type { BackgroundGlowProps } from "./hoc/background_glow/type.js";
39
54
  export type { BorderGlowPosition, BorderGlowProps } from "./hoc/border_glow/type.js";
40
55
  export type { MaskGradientYProps } from "./hoc/mask_gradient_y/type.js";
41
56
  export type { RippleProps } from "./hoc/ripple/type.js";