@koide-labs/ui 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-commit +1 -0
- package/.storybook/main.ts +25 -0
- package/.storybook/preview-head.html +6 -0
- package/.storybook/preview.tsx +48 -0
- package/.storybook/vitest.setup.ts +8 -0
- package/README.md +11 -0
- package/eslint.config.mjs +29 -0
- package/lint-staged.config.js +15 -0
- package/package.json +95 -0
- package/pnpm-workspace.yaml +2 -0
- package/postcss.config.mjs +7 -0
- package/prettier.config.mjs +24 -0
- package/scripts/build-icon-types.ts +38 -0
- package/src/-types.ts +8 -0
- package/src/-utils.tsx +64 -0
- package/src/components/accordion/accordion.module.css +44 -0
- package/src/components/accordion/accordion.stories.tsx +36 -0
- package/src/components/accordion/index.tsx +67 -0
- package/src/components/alert-dialog/alert-dialog.module.css +5 -0
- package/src/components/alert-dialog/alert-dialog.stories.tsx +53 -0
- package/src/components/alert-dialog/index.tsx +138 -0
- package/src/components/anchor/anchor.module.css +18 -0
- package/src/components/anchor/anchor.stories.tsx +28 -0
- package/src/components/anchor/index.tsx +45 -0
- package/src/components/avatar/avatar.module.css +56 -0
- package/src/components/avatar/avatar.stories.tsx +61 -0
- package/src/components/avatar/index.tsx +82 -0
- package/src/components/badge/badge.module.css +35 -0
- package/src/components/badge/badge.stories.tsx +60 -0
- package/src/components/badge/index.tsx +71 -0
- package/src/components/button/button.module.css +42 -0
- package/src/components/button/button.stories.tsx +108 -0
- package/src/components/button/index.tsx +63 -0
- package/src/components/checkbox/checkbox.module.css +36 -0
- package/src/components/checkbox/checkbox.stories.tsx +21 -0
- package/src/components/checkbox/index.tsx +41 -0
- package/src/components/code/code.module.css +20 -0
- package/src/components/code/code.stories.tsx +42 -0
- package/src/components/code/index.tsx +73 -0
- package/src/components/collapse/collapse.module.css +27 -0
- package/src/components/collapse/collapse.stories.tsx +27 -0
- package/src/components/collapse/index.tsx +59 -0
- package/src/components/command/command.module.css +95 -0
- package/src/components/command/command.stories.tsx +38 -0
- package/src/components/command/index.tsx +108 -0
- package/src/components/context-menu/context-menu.module.css +36 -0
- package/src/components/context-menu/context-menu.stories.tsx +99 -0
- package/src/components/context-menu/index.tsx +242 -0
- package/src/components/dialog/dialog.module.css +71 -0
- package/src/components/dialog/dialog.stories.tsx +29 -0
- package/src/components/dialog/index.tsx +148 -0
- package/src/components/heading/heading.module.css +3 -0
- package/src/components/heading/heading.stories.tsx +52 -0
- package/src/components/heading/index.tsx +112 -0
- package/src/components/icon/icon-names.ts +3189 -0
- package/src/components/icon/icon.module.css +36 -0
- package/src/components/icon/icon.stories.tsx +40 -0
- package/src/components/icon/index.tsx +60 -0
- package/src/components/icon-button/icon-button.module.css +33 -0
- package/src/components/icon-button/icon-button.stories.tsx +59 -0
- package/src/components/icon-button/index.tsx +48 -0
- package/src/components/inline-code/index.tsx +29 -0
- package/src/components/inline-code/inline-code.module.css +13 -0
- package/src/components/inline-code/inline-code.stories.tsx +31 -0
- package/src/components/input/index.tsx +22 -0
- package/src/components/input/input.module.css +23 -0
- package/src/components/input/input.stories.tsx +52 -0
- package/src/components/meter/index.tsx +55 -0
- package/src/components/meter/meter.module.css +23 -0
- package/src/components/meter/meter.stories.tsx +31 -0
- package/src/components/multiline-input/index.tsx +58 -0
- package/src/components/multiline-input/multiline-input.stories.tsx +26 -0
- package/src/components/number-input/index.tsx +74 -0
- package/src/components/number-input/number-input.module.css +41 -0
- package/src/components/number-input/number-input.stories.tsx +24 -0
- package/src/components/password-input/index.tsx +24 -0
- package/src/components/password-input/password-input.module.css +10 -0
- package/src/components/password-input/password-input.stories.tsx +24 -0
- package/src/components/pill/index.tsx +45 -0
- package/src/components/pill/pill.module.css +22 -0
- package/src/components/pill/pill.stories.tsx +83 -0
- package/src/components/popover/index.tsx +94 -0
- package/src/components/popover/popover.module.css +8 -0
- package/src/components/popover/popover.stories.tsx +53 -0
- package/src/components/preview-card/index.tsx +68 -0
- package/src/components/preview-card/preview-card.module.css +5 -0
- package/src/components/preview-card/preview-card.stories.tsx +58 -0
- package/src/components/radio/index.tsx +67 -0
- package/src/components/radio/radio-group.module.css +5 -0
- package/src/components/radio/radio.module.css +36 -0
- package/src/components/radio/radio.stories.tsx +27 -0
- package/src/components/search-bar/index.tsx +60 -0
- package/src/components/search-bar/search-bar.module.css +29 -0
- package/src/components/search-bar/search-bar.stories.tsx +37 -0
- package/src/components/select/index.tsx +132 -0
- package/src/components/select/select.module.css +63 -0
- package/src/components/select/select.stories.tsx +49 -0
- package/src/components/separator/index.tsx +28 -0
- package/src/components/separator/separator.module.css +24 -0
- package/src/components/separator/separator.stories.tsx +40 -0
- package/src/components/slider/index.tsx +28 -0
- package/src/components/slider/slider.module.css +52 -0
- package/src/components/slider/slider.stories.tsx +53 -0
- package/src/components/spinner/index.tsx +14 -0
- package/src/components/spinner/spinner.module.css +13 -0
- package/src/components/spinner/spinner.stories.tsx +17 -0
- package/src/components/stacked-avatars/index.tsx +88 -0
- package/src/components/stacked-avatars/stacked-avatars.module.css +79 -0
- package/src/components/stacked-avatars/stacked-avatars.stories.tsx +48 -0
- package/src/components/status-banner/index.tsx +96 -0
- package/src/components/status-banner/status-banner.module.css +52 -0
- package/src/components/status-banner/status-banner.stories.tsx +44 -0
- package/src/components/surface/index.tsx +83 -0
- package/src/components/surface/surface.module.css +35 -0
- package/src/components/surface/surface.stories.tsx +84 -0
- package/src/components/switch/index.tsx +23 -0
- package/src/components/switch/switch.module.css +45 -0
- package/src/components/switch/switch.stories.tsx +48 -0
- package/src/components/tabs/index.tsx +126 -0
- package/src/components/tabs/tabs.module.css +134 -0
- package/src/components/tabs/tabs.stories.tsx +88 -0
- package/src/components/text/index.tsx +69 -0
- package/src/components/text/text.module.css +76 -0
- package/src/components/text/text.stories.tsx +107 -0
- package/src/components/theme-provider/index.ts +2 -0
- package/src/components/theme-provider/theme-context.tsx +18 -0
- package/src/components/theme-provider/theme-provider.stories.tsx +47 -0
- package/src/components/theme-provider/theme-provider.tsx +77 -0
- package/src/components/timestamp/index.tsx +131 -0
- package/src/components/timestamp/timestamp.module.css +8 -0
- package/src/components/timestamp/timestamp.stories.tsx +37 -0
- package/src/components/toast/index.ts +2 -0
- package/src/components/toast/toast.module.css +163 -0
- package/src/components/toast/toast.stories.tsx +53 -0
- package/src/components/toast/toast.tsx +104 -0
- package/src/components/toast/use-toast-manager.ts +63 -0
- package/src/components/tooltip/index.tsx +61 -0
- package/src/components/tooltip/tooltip-arrow.tsx +17 -0
- package/src/components/tooltip/tooltip.module.css +44 -0
- package/src/components/tooltip/tooltip.stories.tsx +76 -0
- package/src/components/view/index.tsx +137 -0
- package/src/components/view/view.module.css +11 -0
- package/src/components/view/view.stories.tsx +131 -0
- package/src/components/view/view_colorway.module.css +280 -0
- package/src/components/view/view_interactive.module.css +127 -0
- package/src/components/view/view_loading.module.css +58 -0
- package/src/components/visually-hidden/index.ts +1 -0
- package/src/index.ts +49 -0
- package/src/integrations/react-markdown/index.tsx +134 -0
- package/src/integrations/react-markdown/react-markdown.module.css +62 -0
- package/src/integrations/react-markdown/react-markdown.stories.tsx +31 -0
- package/src/integrations/remix.ts +12 -0
- package/src/integrations/tailwind.css +173 -0
- package/src/integrations/twemoij/index.tsx +13 -0
- package/src/integrations/twemoij/twemoji.module.css +7 -0
- package/src/integrations/twemoij/twemoji.stories.tsx +40 -0
- package/src/stories/components/all-variants.tsx +40 -0
- package/src/stories/data.ts +72 -0
- package/src/stories/utils.ts +20 -0
- package/src/styles/core.css +153 -0
- package/src/styles/themes/dark.css +86 -0
- package/src/styles/themes/light.css +86 -0
- package/src/styles/tokens.ts +282 -0
- package/src/styles/transitions.module.css +31 -0
- package/stylelint.config.mjs +29 -0
- package/tsconfig.app.json +35 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +103 -0
- package/vitest.shims.d.ts +1 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import type { ComponentProps } from "react";
|
|
4
|
+
|
|
5
|
+
import { Button, type ButtonProps } from "../button";
|
|
6
|
+
import type { BaseDialogProps } from "../dialog";
|
|
7
|
+
import { Surface } from "../surface";
|
|
8
|
+
import { Text } from "../text";
|
|
9
|
+
import { View } from "../view";
|
|
10
|
+
|
|
11
|
+
import transitionStyles from "../../styles/transitions.module.css";
|
|
12
|
+
import dialogStyles from "../dialog/dialog.module.css";
|
|
13
|
+
import styles from "./alert-dialog.module.css";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* General guidance:
|
|
17
|
+
*
|
|
18
|
+
* Should you have nested modals? Absolutely not. It's confusing to navigate and
|
|
19
|
+
* obliterates the stacking context by putting multiple root surfaces on top of
|
|
20
|
+
* each other.
|
|
21
|
+
*
|
|
22
|
+
* Should you have payloads? Probably not... Currently unsupported but that
|
|
23
|
+
* style of generalization feels kind of anti-composition
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export type AlertDialogProps = ComponentProps<
|
|
27
|
+
typeof AlertDialogPrimitive.Root
|
|
28
|
+
> &
|
|
29
|
+
BaseDialogProps & {
|
|
30
|
+
/**
|
|
31
|
+
*
|
|
32
|
+
* The actions to display in the alert dialog. You must have at least two
|
|
33
|
+
* actions. By default, actions will be `interactive` and the last action will
|
|
34
|
+
* have `colorway` "negative_fill".
|
|
35
|
+
*
|
|
36
|
+
*/
|
|
37
|
+
actions: Array<ButtonProps>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function AlertDialog({
|
|
41
|
+
title,
|
|
42
|
+
description,
|
|
43
|
+
children,
|
|
44
|
+
actions,
|
|
45
|
+
trigger,
|
|
46
|
+
width = "md",
|
|
47
|
+
background = "root",
|
|
48
|
+
centered = false,
|
|
49
|
+
className,
|
|
50
|
+
...props
|
|
51
|
+
}: AlertDialogProps) {
|
|
52
|
+
if (actions.length < 2) {
|
|
53
|
+
console.error("You must have at least two actions.");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<AlertDialogPrimitive.Root {...props}>
|
|
58
|
+
{trigger ? <AlertDialogPrimitive.Trigger render={trigger} /> : null}
|
|
59
|
+
<AlertDialogPrimitive.Portal>
|
|
60
|
+
<AlertDialogPrimitive.Backdrop
|
|
61
|
+
className={clsx(
|
|
62
|
+
dialogStyles["dialog__backdrop"],
|
|
63
|
+
transitionStyles["transition_opacity"],
|
|
64
|
+
)}
|
|
65
|
+
/>
|
|
66
|
+
<AlertDialogPrimitive.Popup
|
|
67
|
+
className={clsx(
|
|
68
|
+
dialogStyles["dialog__popup"],
|
|
69
|
+
centered && dialogStyles["dialog__popup_centered"],
|
|
70
|
+
transitionStyles["transition_scale"],
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
<Surface
|
|
74
|
+
background={background}
|
|
75
|
+
className={clsx(
|
|
76
|
+
dialogStyles["dialog__content"],
|
|
77
|
+
dialogStyles[`dialog__content_width_${width}`],
|
|
78
|
+
className,
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
{title || description ? (
|
|
82
|
+
<View className={dialogStyles["dialog__header"]}>
|
|
83
|
+
{title ? (
|
|
84
|
+
<Text render={<AlertDialogPrimitive.Title />} size="2xl">
|
|
85
|
+
{title}
|
|
86
|
+
</Text>
|
|
87
|
+
) : null}
|
|
88
|
+
{description ? (
|
|
89
|
+
<Text
|
|
90
|
+
multiline
|
|
91
|
+
color="dimmer"
|
|
92
|
+
render={<AlertDialogPrimitive.Description />}
|
|
93
|
+
>
|
|
94
|
+
{description}
|
|
95
|
+
</Text>
|
|
96
|
+
) : null}
|
|
97
|
+
</View>
|
|
98
|
+
) : null}
|
|
99
|
+
{children}
|
|
100
|
+
<View className={styles["alert-dialog__actions"]}>
|
|
101
|
+
{actions.map(({ children, ...action }, i) => (
|
|
102
|
+
<AlertDialogClose
|
|
103
|
+
key={i}
|
|
104
|
+
{...getButtonProps(action, i + 1 === actions.length)}
|
|
105
|
+
>
|
|
106
|
+
{children}
|
|
107
|
+
</AlertDialogClose>
|
|
108
|
+
))}
|
|
109
|
+
</View>
|
|
110
|
+
</Surface>
|
|
111
|
+
</AlertDialogPrimitive.Popup>
|
|
112
|
+
</AlertDialogPrimitive.Portal>
|
|
113
|
+
</AlertDialogPrimitive.Root>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function AlertDialogClose(props: ButtonProps) {
|
|
118
|
+
return <AlertDialogPrimitive.Close render={<Button {...props} />} />;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Sensible defaults for actions (buttons)
|
|
123
|
+
* @param props Action provided by AlertDialog
|
|
124
|
+
* @param isLast Is this the last action provided in the array?
|
|
125
|
+
* @returns Props with defaults
|
|
126
|
+
*/
|
|
127
|
+
function getButtonProps(props: ButtonProps, isLast?: boolean): ButtonProps {
|
|
128
|
+
if (isLast) {
|
|
129
|
+
return {
|
|
130
|
+
interactive: "negative_fill",
|
|
131
|
+
...props,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
interactive: true,
|
|
136
|
+
...props,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.anchor {
|
|
2
|
+
cursor: pointer;
|
|
3
|
+
color: var(--primary-stronger);
|
|
4
|
+
font-weight: var(--font-weight-regular);
|
|
5
|
+
font-size: var(--font-size-default);
|
|
6
|
+
line-height: var(--line-height-default);
|
|
7
|
+
text-decoration: underline;
|
|
8
|
+
|
|
9
|
+
&:visited {
|
|
10
|
+
color: var(--blurple-stronger);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.anchor__icon {
|
|
15
|
+
/* you got me. a magic number in a sea of tokens. */
|
|
16
|
+
vertical-align: -5%;
|
|
17
|
+
margin-left: var(--space-2);
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { tokens } from "~/styles/tokens";
|
|
4
|
+
|
|
5
|
+
import { Anchor } from ".";
|
|
6
|
+
import { Surface } from "../surface";
|
|
7
|
+
import { Text } from "../text";
|
|
8
|
+
|
|
9
|
+
const meta = {
|
|
10
|
+
title: "Anchor",
|
|
11
|
+
component: Anchor,
|
|
12
|
+
parameters: { layout: "centered" },
|
|
13
|
+
} satisfies Meta<typeof Anchor>;
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
|
|
17
|
+
type Story = StoryObj<typeof meta>;
|
|
18
|
+
|
|
19
|
+
export const Default: Story = {
|
|
20
|
+
args: { href: "https://npham.dev", external: true },
|
|
21
|
+
render: (props) => (
|
|
22
|
+
<Surface elevated style={{ padding: tokens.space16 }}>
|
|
23
|
+
<Text>
|
|
24
|
+
Visit my amazing <Anchor {...props}>portfolio</Anchor> or perish.
|
|
25
|
+
</Text>
|
|
26
|
+
</Surface>
|
|
27
|
+
),
|
|
28
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mergeProps, useRender } from "@base-ui/react";
|
|
2
|
+
|
|
3
|
+
import { Icon } from "../icon";
|
|
4
|
+
|
|
5
|
+
import styles from "./anchor.module.css";
|
|
6
|
+
|
|
7
|
+
export type AnchorProps = useRender.ComponentProps<"a"> & {
|
|
8
|
+
/**
|
|
9
|
+
* Open this link in a new tab
|
|
10
|
+
*/
|
|
11
|
+
external?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function Anchor({ external, render, children, ...props }: AnchorProps) {
|
|
15
|
+
const element = useRender({
|
|
16
|
+
defaultTagName: "a",
|
|
17
|
+
render,
|
|
18
|
+
props: mergeProps(
|
|
19
|
+
{
|
|
20
|
+
className: styles["anchor"],
|
|
21
|
+
...(external
|
|
22
|
+
? {
|
|
23
|
+
target: "_blank",
|
|
24
|
+
rel: "noreferrer",
|
|
25
|
+
}
|
|
26
|
+
: {}),
|
|
27
|
+
children: (
|
|
28
|
+
<>
|
|
29
|
+
{children}
|
|
30
|
+
{external ? (
|
|
31
|
+
<Icon
|
|
32
|
+
name="external-link-line"
|
|
33
|
+
size="sm"
|
|
34
|
+
className={styles["anchor__icon"]}
|
|
35
|
+
/>
|
|
36
|
+
) : null}
|
|
37
|
+
</>
|
|
38
|
+
),
|
|
39
|
+
},
|
|
40
|
+
props,
|
|
41
|
+
),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return element;
|
|
45
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
.avatar {
|
|
2
|
+
display: flex;
|
|
3
|
+
position: relative;
|
|
4
|
+
flex-shrink: 0;
|
|
5
|
+
border-width: 1px;
|
|
6
|
+
border-style: solid;
|
|
7
|
+
border-color: var(--outline-dimmest);
|
|
8
|
+
border-radius: var(--border-radius-round);
|
|
9
|
+
background-color: var(--background-highest);
|
|
10
|
+
width: var(--avatar-size);
|
|
11
|
+
height: var(--avatar-size);
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.avatar__image {
|
|
16
|
+
width: 100%;
|
|
17
|
+
height: 100%;
|
|
18
|
+
object-fit: cover;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.avatar__fallback {
|
|
22
|
+
display: flex;
|
|
23
|
+
flex: 1 1 0%;
|
|
24
|
+
justify-content: center;
|
|
25
|
+
align-items: center;
|
|
26
|
+
border-radius: var(--border-radius-round);
|
|
27
|
+
width: 100%;
|
|
28
|
+
height: 100%;
|
|
29
|
+
color: var(--foreground-dimmest);
|
|
30
|
+
font-size: calc(var(--avatar-size) / 2.5);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Avatar size variants */
|
|
34
|
+
.avatar_size_sm {
|
|
35
|
+
--avatar-size: var(--space-24);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.avatar_size_md {
|
|
39
|
+
--avatar-size: var(--space-32);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.avatar_size_lg {
|
|
43
|
+
--avatar-size: var(--space-40);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.avatar_size_xl {
|
|
47
|
+
--avatar-size: var(--space-48);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.avatar_size_2xl {
|
|
51
|
+
--avatar-size: var(--space-56);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.avatar_size_3xl {
|
|
55
|
+
--avatar-size: var(--space-64);
|
|
56
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { AllVariants } from "~/stories/components/all-variants";
|
|
4
|
+
import { sizes } from "~/stories/data";
|
|
5
|
+
|
|
6
|
+
import { Avatar } from ".";
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: "Avatar",
|
|
10
|
+
component: Avatar,
|
|
11
|
+
parameters: { layout: "centered" },
|
|
12
|
+
argTypes: {
|
|
13
|
+
size: {
|
|
14
|
+
control: "select",
|
|
15
|
+
options: sizes,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
} satisfies Meta<typeof Avatar>;
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
image: "https://avatars.githubusercontent.com/u/146884027?v=4",
|
|
27
|
+
username: "npham-dev",
|
|
28
|
+
size: "md",
|
|
29
|
+
},
|
|
30
|
+
render: (props) => <Avatar {...props} />,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Fallback: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
username: "npham-dev",
|
|
36
|
+
size: "md",
|
|
37
|
+
},
|
|
38
|
+
render: (props) => <Avatar {...props} />,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const AllSizes: Story = {
|
|
42
|
+
args: {
|
|
43
|
+
image: "https://avatars.githubusercontent.com/u/146884027?v=4",
|
|
44
|
+
username: "npham-dev",
|
|
45
|
+
},
|
|
46
|
+
argTypes: {
|
|
47
|
+
size: {
|
|
48
|
+
control: false,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
render: (props) => (
|
|
52
|
+
<AllVariants
|
|
53
|
+
variantName="size"
|
|
54
|
+
variants={sizes}
|
|
55
|
+
element={<Avatar {...props} />}
|
|
56
|
+
style={{
|
|
57
|
+
flexDirection: "row",
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
),
|
|
61
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
|
|
4
|
+
import type { Size } from "~/styles/tokens";
|
|
5
|
+
|
|
6
|
+
import styles from "./avatar.module.css";
|
|
7
|
+
|
|
8
|
+
export interface AvatarProps extends AvatarPrimitive.Root.Props {
|
|
9
|
+
/**
|
|
10
|
+
* Avatar url; if not provided, uses fallback based on username or fullName.
|
|
11
|
+
*/
|
|
12
|
+
image?: string | null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Used for initials if avatar image is not provided.
|
|
16
|
+
*/
|
|
17
|
+
username: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* User's first & last name. Used for initials, takes precedence over username.
|
|
21
|
+
*/
|
|
22
|
+
fullName?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Size of the avatar, defaults to "md".
|
|
26
|
+
*/
|
|
27
|
+
size?: Size;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function Avatar({
|
|
31
|
+
image,
|
|
32
|
+
username,
|
|
33
|
+
fullName,
|
|
34
|
+
size = "md",
|
|
35
|
+
className,
|
|
36
|
+
...props
|
|
37
|
+
}: AvatarProps) {
|
|
38
|
+
const fallback = getAvatarFallback(username, fullName);
|
|
39
|
+
return (
|
|
40
|
+
<AvatarPrimitive.Root
|
|
41
|
+
className={clsx(
|
|
42
|
+
styles["avatar"],
|
|
43
|
+
styles[`avatar_size_${size}`],
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
48
|
+
{image ? (
|
|
49
|
+
<AvatarPrimitive.Image
|
|
50
|
+
src={image}
|
|
51
|
+
className={styles["avatar__image"]}
|
|
52
|
+
/>
|
|
53
|
+
) : null}
|
|
54
|
+
<AvatarPrimitive.Fallback className={styles["avatar__fallback"]}>
|
|
55
|
+
{fallback}
|
|
56
|
+
</AvatarPrimitive.Fallback>
|
|
57
|
+
</AvatarPrimitive.Root>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create 2 letter avatar fallback (depending on what is provided) \
|
|
63
|
+
* Prioritizes full name over usernmae
|
|
64
|
+
* @param username user selected name
|
|
65
|
+
* @param fullName real name of the user
|
|
66
|
+
* @returns 2 letter, uppercase avatar fallback
|
|
67
|
+
*/
|
|
68
|
+
function getAvatarFallback(username: string, fullName?: string) {
|
|
69
|
+
let fallback = username.substring(0, 2);
|
|
70
|
+
|
|
71
|
+
if (fullName) {
|
|
72
|
+
// if full name w/ lastName is provided, use first and last initials
|
|
73
|
+
// otherwise use first two letters of the name
|
|
74
|
+
const [firstName, lastName] = fullName.split(" ");
|
|
75
|
+
fallback =
|
|
76
|
+
firstName && lastName
|
|
77
|
+
? firstName.charAt(0) + lastName.charAt(0)
|
|
78
|
+
: fullName.substring(0, 2);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return fallback.toLocaleUpperCase();
|
|
82
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
.badge {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: var(--space-4);
|
|
5
|
+
cursor: default;
|
|
6
|
+
border-radius: var(--border-radius-16);
|
|
7
|
+
background-color: var(--view-colorway-dimmer);
|
|
8
|
+
padding: var(--space-4) var(--space-8);
|
|
9
|
+
height: var(--space-24);
|
|
10
|
+
color: var(--view-colorway-strongest);
|
|
11
|
+
font-size: var(--font-size-small);
|
|
12
|
+
line-height: 1;
|
|
13
|
+
white-space: nowrap;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.badge_shiny {
|
|
17
|
+
display: inline-flex;
|
|
18
|
+
flex-direction: row;
|
|
19
|
+
align-items: center;
|
|
20
|
+
transition: none;
|
|
21
|
+
background-image: radial-gradient(
|
|
22
|
+
circle at top left,
|
|
23
|
+
var(--view-colorway-dimmest) 34%,
|
|
24
|
+
var(--view-colorway-dimmer) 50%,
|
|
25
|
+
var(--view-colorway-dimmest) 66%
|
|
26
|
+
);
|
|
27
|
+
background-position: left;
|
|
28
|
+
background-size: 300% 100%;
|
|
29
|
+
|
|
30
|
+
&:hover {
|
|
31
|
+
transition: background-position 600ms
|
|
32
|
+
var(--transition-timing-function-chill);
|
|
33
|
+
background-position: right;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { colors } from "~/stories/data";
|
|
4
|
+
import { disable } from "~/stories/utils";
|
|
5
|
+
import { tokens } from "~/styles/tokens";
|
|
6
|
+
|
|
7
|
+
import { Badge } from ".";
|
|
8
|
+
import { View } from "../view";
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
title: "Badge",
|
|
12
|
+
component: Badge,
|
|
13
|
+
parameters: { layout: "centered" },
|
|
14
|
+
argTypes: {
|
|
15
|
+
color: {
|
|
16
|
+
control: "select",
|
|
17
|
+
options: colors,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
} satisfies Meta<typeof Badge>;
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
|
|
24
|
+
type Story = StoryObj<typeof meta>;
|
|
25
|
+
|
|
26
|
+
export const Default: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
color: "orange",
|
|
29
|
+
icon: "sparkling-line",
|
|
30
|
+
name: "Pro",
|
|
31
|
+
shiny: true,
|
|
32
|
+
},
|
|
33
|
+
render: (args) => <Badge {...args} />,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const ExampleBadges: Story = {
|
|
37
|
+
argTypes: {
|
|
38
|
+
...disable(["icon", "name", "color", "shiny", "tagline"]),
|
|
39
|
+
},
|
|
40
|
+
args: {
|
|
41
|
+
name: "",
|
|
42
|
+
color: "primary",
|
|
43
|
+
},
|
|
44
|
+
render: () => (
|
|
45
|
+
<View
|
|
46
|
+
style={{
|
|
47
|
+
flexDirection: "row",
|
|
48
|
+
flexWrap: "wrap",
|
|
49
|
+
gap: tokens.space8,
|
|
50
|
+
maxWidth: tokens.space256,
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<Badge name="Admin" color="yellow" />
|
|
54
|
+
<Badge name="Detective" color="green" />
|
|
55
|
+
<Badge icon="checkbox-circle-line" name="Verified" color="green" />
|
|
56
|
+
<Badge name="Language Jammer" color="purple" />
|
|
57
|
+
<Badge name="Community Monitor" color="teal" />
|
|
58
|
+
</View>
|
|
59
|
+
),
|
|
60
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mergeProps, useRender } from "@base-ui/react";
|
|
2
|
+
import { clsx } from "clsx";
|
|
3
|
+
|
|
4
|
+
import { Icon } from "../icon";
|
|
5
|
+
import type { IconName } from "../icon/icon-names";
|
|
6
|
+
import { Text } from "../text";
|
|
7
|
+
import { Tooltip } from "../tooltip";
|
|
8
|
+
import { View, type Color } from "../view";
|
|
9
|
+
|
|
10
|
+
import styles from "./badge.module.css";
|
|
11
|
+
|
|
12
|
+
export interface BadgeProps extends Omit<
|
|
13
|
+
useRender.ComponentProps<"span">,
|
|
14
|
+
"children"
|
|
15
|
+
> {
|
|
16
|
+
/** Optional icon, goes before name */
|
|
17
|
+
icon?: IconName;
|
|
18
|
+
|
|
19
|
+
/** Name of badge to be displayed, goes after icon */
|
|
20
|
+
name: string;
|
|
21
|
+
|
|
22
|
+
/** Communicate purpose with color. Defaults to "primary". */
|
|
23
|
+
color?: Color;
|
|
24
|
+
|
|
25
|
+
/** Add shiny hover effect. */
|
|
26
|
+
shiny?: boolean;
|
|
27
|
+
|
|
28
|
+
/** Tagline of badge. */
|
|
29
|
+
tagline?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function Badge({
|
|
33
|
+
icon,
|
|
34
|
+
name,
|
|
35
|
+
color,
|
|
36
|
+
shiny,
|
|
37
|
+
tagline,
|
|
38
|
+
render,
|
|
39
|
+
...props
|
|
40
|
+
}: BadgeProps) {
|
|
41
|
+
const element = useRender({
|
|
42
|
+
defaultTagName: "span",
|
|
43
|
+
render,
|
|
44
|
+
props: mergeProps(
|
|
45
|
+
{
|
|
46
|
+
className: clsx(styles["badge"], shiny && styles["badge_shiny"]),
|
|
47
|
+
children: (
|
|
48
|
+
<>
|
|
49
|
+
{icon ? <Icon name={icon} /> : null}
|
|
50
|
+
{name}
|
|
51
|
+
</>
|
|
52
|
+
),
|
|
53
|
+
},
|
|
54
|
+
props,
|
|
55
|
+
),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const label = <View color={color}>{element}</View>;
|
|
59
|
+
|
|
60
|
+
if (!tagline) {
|
|
61
|
+
return label;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Tooltip trigger={label}>
|
|
66
|
+
<Text size="sm" multiline>
|
|
67
|
+
{tagline}
|
|
68
|
+
</Text>
|
|
69
|
+
</Tooltip>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
.button {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: row;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
align-items: center;
|
|
6
|
+
gap: var(--space-8);
|
|
7
|
+
box-sizing: border-box;
|
|
8
|
+
border-radius: var(--border-radius-default);
|
|
9
|
+
padding-inline: var(--button-padding-inline);
|
|
10
|
+
height: var(--button-height);
|
|
11
|
+
white-space: nowrap;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.button_height_sm {
|
|
15
|
+
--button-padding-inline: var(--space-12);
|
|
16
|
+
--button-height: var(--space-28);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.button_height_md {
|
|
20
|
+
--button-padding-inline: var(--space-16);
|
|
21
|
+
--button-height: var(--space-32);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.button_height_lg {
|
|
25
|
+
--button-padding-inline: var(--space-20);
|
|
26
|
+
--button-height: var(--space-40);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.button_height_xl {
|
|
30
|
+
--button-padding-inline: var(--space-24);
|
|
31
|
+
--button-height: var(--space-48);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.button_height_2xl {
|
|
35
|
+
--button-padding-inline: var(--space-28);
|
|
36
|
+
--button-height: var(--space-56);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.button_height_3xl {
|
|
40
|
+
--button-padding-inline: var(--space-32);
|
|
41
|
+
--button-height: var(--space-64);
|
|
42
|
+
}
|