Compare commits
No commits in common. "6e4c13661465561c947e0d2438ecab3b8dc685e4" and "2844e1fc32622654c1b86d4bf6fe3204f5acddad" have entirely different histories.
6e4c136614
...
2844e1fc32
29
apps/app/.env.development
Normal file
29
apps/app/.env.development
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Server
|
||||||
|
AUTH_TYPE="basic" #
|
||||||
|
|
||||||
|
BASIC_USER="konobangu"
|
||||||
|
BASIC_PASSWORD="konobangu"
|
||||||
|
|
||||||
|
OIDC_PROVIDER_ENDPOINT="https://some-oidc-auth.com/oidc/.well-known/openid-configuration"
|
||||||
|
OIDC_CLIENT_ID=""
|
||||||
|
OIDC_CLIENT_SECRET=""
|
||||||
|
OIDC_ISSUER="https://some-oidc-auth.com/oidc"
|
||||||
|
OIDC_AUDIENCE="https://konobangu.com/api"
|
||||||
|
OIDC_ICON_URL=""
|
||||||
|
OIDC_EXTRA_SCOPE_REGEX=""
|
||||||
|
OIDC_EXTRA_CLAIM_KEY=""
|
||||||
|
OIDC_EXTRA_CLAIM_VALUE=""
|
||||||
|
|
||||||
|
DATABASE_URL="postgres://konobangu:konobangu@127.0.0.1:5432/konobangu"
|
||||||
|
BETTERSTACK_API_KEY=""
|
||||||
|
BETTERSTACK_URL=""
|
||||||
|
FLAGS_SECRET=""
|
||||||
|
ARCJET_KEY=""
|
||||||
|
SVIX_TOKEN=""
|
||||||
|
LIVEBLOCKS_SECRET=""
|
||||||
|
|
||||||
|
# Client
|
||||||
|
NEXT_PUBLIC_APP_URL="http://localhost:5000"
|
||||||
|
NEXT_PUBLIC_WEB_URL="http://localhost:5001"
|
||||||
|
NEXT_PUBLIC_DOCS_URL="http://localhost:5004"
|
||||||
|
NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="https://konobangu.com"
|
29
apps/app/.env.example
Normal file
29
apps/app/.env.example
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# AUTH
|
||||||
|
AUTH_TYPE="basic"
|
||||||
|
|
||||||
|
NEXT_PUBLIC_OIDC_PROVIDER_ENDPOINT="https://some-oidc-auth.com/oidc/.well-known/openid-configuration"
|
||||||
|
NEXT_PUBLIC_OIDC_CLIENT_ID=""
|
||||||
|
NEXT_PUBLIC_OIDC_CLIENT_SECRET=""
|
||||||
|
NEXT_PUBLIC_OIDC_ICON_URL=""
|
||||||
|
OIDC_ISSUER="https://some-oidc-auth.com/oidc"
|
||||||
|
OIDC_AUDIENCE="https://konobangu.com/api"
|
||||||
|
OIDC_EXTRA_SCOPES="" # 如 "read:konobangu,write:konobangu"
|
||||||
|
OIDC_EXTRA_CLAIM_KEY=""
|
||||||
|
OIDC_EXTRA_CLAIM_VALUE=""
|
||||||
|
|
||||||
|
# DATABASE
|
||||||
|
DATABASE_URL="postgres://konobangu:konobangu@127.0.0.1:5432/konobangu"
|
||||||
|
|
||||||
|
# SERVER MISC
|
||||||
|
BETTERSTACK_API_KEY=""
|
||||||
|
BETTERSTACK_URL=""
|
||||||
|
FLAGS_SECRET=""
|
||||||
|
ARCJET_KEY=""
|
||||||
|
SVIX_TOKEN=""
|
||||||
|
LIVEBLOCKS_SECRET=""
|
||||||
|
|
||||||
|
# WEBUI
|
||||||
|
NEXT_PUBLIC_APP_URL="http://localhost:5000"
|
||||||
|
NEXT_PUBLIC_WEB_URL="http://localhost:5001"
|
||||||
|
NEXT_PUBLIC_DOCS_URL="http://localhost:5004"
|
||||||
|
NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="https://konobangu.com"
|
45
apps/app/.gitignore
vendored
Normal file
45
apps/app/.gitignore
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# prisma
|
||||||
|
.env
|
||||||
|
|
||||||
|
# react.email
|
||||||
|
.react-email
|
||||||
|
|
||||||
|
# Sentry
|
||||||
|
.sentryclirc
|
13
apps/app/__tests__/sign-in.test.tsx
Normal file
13
apps/app/__tests__/sign-in.test.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { expect, test } from 'vitest';
|
||||||
|
import Page from '../app/(unauthenticated)/sign-in/[[...sign-in]]/page';
|
||||||
|
|
||||||
|
test('Sign In Page', () => {
|
||||||
|
render(<Page />);
|
||||||
|
expect(
|
||||||
|
screen.getByRole('heading', {
|
||||||
|
level: 1,
|
||||||
|
name: 'Welcome back',
|
||||||
|
})
|
||||||
|
).toBeDefined();
|
||||||
|
});
|
13
apps/app/__tests__/sign-up.test.tsx
Normal file
13
apps/app/__tests__/sign-up.test.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { expect, test } from 'vitest';
|
||||||
|
import Page from '../app/(unauthenticated)/sign-up/[[...sign-up]]/page';
|
||||||
|
|
||||||
|
test('Sign Up Page', () => {
|
||||||
|
render(<Page />);
|
||||||
|
expect(
|
||||||
|
screen.getByRole('heading', {
|
||||||
|
level: 1,
|
||||||
|
name: 'Create an account',
|
||||||
|
})
|
||||||
|
).toBeDefined();
|
||||||
|
});
|
59
apps/app/app/(authenticated)/components/avatar-stack.tsx
Normal file
59
apps/app/app/(authenticated)/components/avatar-stack.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useOthers, useSelf } from '@konobangu/collaboration/hooks';
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
|
} from '@konobangu/design-system/components/ui/avatar';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@konobangu/design-system/components/ui/tooltip';
|
||||||
|
import { tailwind } from '@konobangu/tailwind-config';
|
||||||
|
|
||||||
|
type PresenceAvatarProps = {
|
||||||
|
info?: Liveblocks['UserMeta']['info'];
|
||||||
|
};
|
||||||
|
|
||||||
|
const PresenceAvatar = ({ info }: PresenceAvatarProps) => (
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Avatar className="h-7 w-7 bg-secondary ring-1 ring-background">
|
||||||
|
<AvatarImage src={info?.avatar} alt={info?.name} />
|
||||||
|
<AvatarFallback className="text-xs">
|
||||||
|
{info?.name?.slice(0, 2)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent collisionPadding={4}>
|
||||||
|
<p>{info?.name ?? 'Unknown'}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AvatarStack = () => {
|
||||||
|
const others = useOthers();
|
||||||
|
const self = useSelf();
|
||||||
|
const hasMoreUsers = others.length > 3;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="-space-x-1 flex items-center px-4">
|
||||||
|
{others.slice(0, 3).map(({ connectionId, info }) => (
|
||||||
|
<PresenceAvatar key={connectionId} info={info} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{hasMoreUsers && (
|
||||||
|
<PresenceAvatar
|
||||||
|
info={{
|
||||||
|
name: `+${others.length - 3}`,
|
||||||
|
color: tailwind.theme.colors.gray[500],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{self && <PresenceAvatar info={self.info} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,48 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { getUsers } from '@/app/actions/users/get';
|
||||||
|
import { searchUsers } from '@/app/actions/users/search';
|
||||||
|
import { Room } from '@konobangu/collaboration/room';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export const CollaborationProvider = ({
|
||||||
|
orgId,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}) => {
|
||||||
|
const resolveUsers = async ({ userIds }: { userIds: string[] }) => {
|
||||||
|
const response = await getUsers(userIds);
|
||||||
|
|
||||||
|
if ('error' in response) {
|
||||||
|
throw new Error('Problem resolving users');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveMentionSuggestions = async ({ text }: { text: string }) => {
|
||||||
|
const response = await searchUsers(text);
|
||||||
|
|
||||||
|
if ('error' in response) {
|
||||||
|
throw new Error('Problem resolving mention suggestions');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Room
|
||||||
|
id={`${orgId}:presence`}
|
||||||
|
authEndpoint="/api/collaboration/auth"
|
||||||
|
fallback={
|
||||||
|
<div className="px-3 text-muted-foreground text-xs">Loading...</div>
|
||||||
|
}
|
||||||
|
resolveUsers={resolveUsers}
|
||||||
|
resolveMentionSuggestions={resolveMentionSuggestions}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Room>
|
||||||
|
);
|
||||||
|
};
|
106
apps/app/app/(authenticated)/components/cursors.tsx
Normal file
106
apps/app/app/(authenticated)/components/cursors.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMyPresence, useOthers } from '@konobangu/collaboration/hooks';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
const Cursor = ({
|
||||||
|
name,
|
||||||
|
color,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
}: {
|
||||||
|
name: string | undefined;
|
||||||
|
color: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute top-0 left-0 z-[999] select-none transition-transform duration-100"
|
||||||
|
style={{
|
||||||
|
transform: `translateX(${x}px) translateY(${y}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="absolute top-0 left-0"
|
||||||
|
width="24"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 24 36"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<title>Cursor</title>
|
||||||
|
<path
|
||||||
|
d="M5.65376 12.3673H5.46026L5.31717 12.4976L0.500002 16.8829L0.500002 1.19841L11.7841 12.3673H5.65376Z"
|
||||||
|
fill={color}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
className="absolute top-4 left-1.5 whitespace-nowrap rounded-full px-2 py-0.5 text-white text-xs"
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Cursors = () => {
|
||||||
|
/**
|
||||||
|
* useMyPresence returns the presence of the current user and a function to update it.
|
||||||
|
* updateMyPresence is different than the setState function returned by the useState hook from React.
|
||||||
|
* You don't need to pass the full presence object to update it.
|
||||||
|
* See https://liveblocks.io/docs/api-reference/liveblocks-react#useMyPresence for more information
|
||||||
|
*/
|
||||||
|
const [_cursor, updateMyPresence] = useMyPresence();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all the other users in the room and their presence (a cursor position in this case)
|
||||||
|
*/
|
||||||
|
const others = useOthers();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onPointerMove = (event: PointerEvent) => {
|
||||||
|
// Update the user cursor position on every pointer move
|
||||||
|
updateMyPresence({
|
||||||
|
cursor: {
|
||||||
|
x: Math.round(event.clientX),
|
||||||
|
y: Math.round(event.clientY),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerLeave = () => {
|
||||||
|
// When the pointer goes out, set cursor to null
|
||||||
|
updateMyPresence({
|
||||||
|
cursor: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.addEventListener('pointermove', onPointerMove);
|
||||||
|
document.body.addEventListener('pointerleave', onPointerLeave);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.removeEventListener('pointermove', onPointerMove);
|
||||||
|
document.body.removeEventListener('pointerleave', onPointerLeave);
|
||||||
|
};
|
||||||
|
}, [updateMyPresence]);
|
||||||
|
|
||||||
|
return others.map(({ connectionId, presence, info }) => {
|
||||||
|
if (!presence.cursor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Cursor
|
||||||
|
key={`cursor-${connectionId}`}
|
||||||
|
// connectionId is an integer that is incremented at every new connections
|
||||||
|
// Assigning a color with a modulo makes sure that a specific user has the same colors on every clients
|
||||||
|
color={info.color}
|
||||||
|
x={presence.cursor.x}
|
||||||
|
y={presence.cursor.y}
|
||||||
|
name={info?.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
43
apps/app/app/(authenticated)/components/header.tsx
Normal file
43
apps/app/app/(authenticated)/components/header.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
} from '@konobangu/design-system/components/ui/breadcrumb';
|
||||||
|
import { Separator } from '@konobangu/design-system/components/ui/separator';
|
||||||
|
import { SidebarTrigger } from '@konobangu/design-system/components/ui/sidebar';
|
||||||
|
import { Fragment, type ReactNode } from 'react';
|
||||||
|
|
||||||
|
type HeaderProps = {
|
||||||
|
pages: string[];
|
||||||
|
page: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Header = ({ pages, page, children }: HeaderProps) => (
|
||||||
|
<header className="flex h-16 shrink-0 items-center justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-2 px-4">
|
||||||
|
<SidebarTrigger className="-ml-1" />
|
||||||
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||||
|
<Breadcrumb>
|
||||||
|
<BreadcrumbList>
|
||||||
|
{pages.map((page, index) => (
|
||||||
|
<Fragment key={page}>
|
||||||
|
{index > 0 && <BreadcrumbSeparator className="hidden md:block" />}
|
||||||
|
<BreadcrumbItem className="hidden md:block">
|
||||||
|
<BreadcrumbLink href="#">{page}</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
<BreadcrumbSeparator className="hidden md:block" />
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbPage>{page}</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</header>
|
||||||
|
);
|
@ -0,0 +1,44 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { analytics } from '@konobangu/analytics/client';
|
||||||
|
import { useSession } from '@konobangu/auth/client';
|
||||||
|
import { usePathname, useSearchParams } from 'next/navigation';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export const PostHogIdentifier = () => {
|
||||||
|
const session = useSession();
|
||||||
|
const user = session?.data?.user;
|
||||||
|
const identified = useRef(false);
|
||||||
|
const pathname = usePathname();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Track pageviews
|
||||||
|
if (pathname && analytics) {
|
||||||
|
let url = window.origin + pathname;
|
||||||
|
if (searchParams.toString()) {
|
||||||
|
url = `${url}?${searchParams.toString()}`;
|
||||||
|
}
|
||||||
|
analytics.capture('$pageview', {
|
||||||
|
$current_url: url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [pathname, searchParams]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user || identified.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
analytics.identify(user.id, {
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
createdAt: user.createdAt,
|
||||||
|
avatar: user.image,
|
||||||
|
});
|
||||||
|
|
||||||
|
identified.current = true;
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
342
apps/app/app/(authenticated)/components/sidebar.tsx
Normal file
342
apps/app/app/(authenticated)/components/sidebar.tsx
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
// import { OrganizationSwitcher, UserButton } from '@konobangu/auth/client';
|
||||||
|
import { ModeToggle } from '@konobangu/design-system/components/mode-toggle';
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@konobangu/design-system/components/ui/collapsible';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@konobangu/design-system/components/ui/dropdown-menu';
|
||||||
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarFooter,
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
SidebarHeader,
|
||||||
|
SidebarInset,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuAction,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
SidebarMenuSub,
|
||||||
|
SidebarMenuSubButton,
|
||||||
|
SidebarMenuSubItem,
|
||||||
|
useSidebar,
|
||||||
|
} from '@konobangu/design-system/components/ui/sidebar';
|
||||||
|
import { cn } from '@konobangu/design-system/lib/utils';
|
||||||
|
import {
|
||||||
|
AnchorIcon,
|
||||||
|
BookOpenIcon,
|
||||||
|
BotIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
FolderIcon,
|
||||||
|
FrameIcon,
|
||||||
|
LifeBuoyIcon,
|
||||||
|
MapIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
PieChartIcon,
|
||||||
|
SendIcon,
|
||||||
|
Settings2Icon,
|
||||||
|
ShareIcon,
|
||||||
|
SquareTerminalIcon,
|
||||||
|
Trash2Icon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
type GlobalSidebarProperties = {
|
||||||
|
readonly children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
user: {
|
||||||
|
name: 'shadcn',
|
||||||
|
email: 'm@example.com',
|
||||||
|
avatar: '/avatars/shadcn.jpg',
|
||||||
|
},
|
||||||
|
navMain: [
|
||||||
|
{
|
||||||
|
title: 'Playground',
|
||||||
|
url: '#',
|
||||||
|
icon: SquareTerminalIcon,
|
||||||
|
isActive: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: 'History',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Starred',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Settings',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Models',
|
||||||
|
url: '#',
|
||||||
|
icon: BotIcon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: 'Genesis',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Explorer',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Quantum',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Documentation',
|
||||||
|
url: '#',
|
||||||
|
icon: BookOpenIcon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: 'Introduction',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Get Started',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tutorials',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Changelog',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Settings',
|
||||||
|
url: '#',
|
||||||
|
icon: Settings2Icon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: 'General',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Team',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Billing',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Limits',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
navSecondary: [
|
||||||
|
{
|
||||||
|
title: 'Webhooks',
|
||||||
|
url: '/webhooks',
|
||||||
|
icon: AnchorIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Support',
|
||||||
|
url: '#',
|
||||||
|
icon: LifeBuoyIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Feedback',
|
||||||
|
url: '#',
|
||||||
|
icon: SendIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'Design Engineering',
|
||||||
|
url: '#',
|
||||||
|
icon: FrameIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sales & Marketing',
|
||||||
|
url: '#',
|
||||||
|
icon: PieChartIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Travel',
|
||||||
|
url: '#',
|
||||||
|
icon: MapIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GlobalSidebar = ({ children }: GlobalSidebarProperties) => {
|
||||||
|
const sidebar = useSidebar();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sidebar variant="inset">
|
||||||
|
<SidebarHeader>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'h-[36px] overflow-hidden transition-all [&>div]:w-full',
|
||||||
|
sidebar.open ? '' : '-mx-1'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* <OrganizationSwitcher
|
||||||
|
hidePersonal
|
||||||
|
afterSelectOrganizationUrl="/"
|
||||||
|
/> */}
|
||||||
|
</div>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarHeader>
|
||||||
|
<SidebarContent>
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
||||||
|
<SidebarMenu>
|
||||||
|
{data.navMain.map((item) => (
|
||||||
|
<Collapsible
|
||||||
|
key={item.title}
|
||||||
|
asChild
|
||||||
|
defaultOpen={item.isActive}
|
||||||
|
>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton asChild tooltip={item.title}>
|
||||||
|
<a href={item.url}>
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</a>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
{item.items?.length ? (
|
||||||
|
<>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
||||||
|
<ChevronRightIcon />
|
||||||
|
<span className="sr-only">Toggle</span>
|
||||||
|
</SidebarMenuAction>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<SidebarMenuSub>
|
||||||
|
{item.items?.map((subItem) => (
|
||||||
|
<SidebarMenuSubItem key={subItem.title}>
|
||||||
|
<SidebarMenuSubButton asChild>
|
||||||
|
<a href={subItem.url}>
|
||||||
|
<span>{subItem.title}</span>
|
||||||
|
</a>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
))}
|
||||||
|
</SidebarMenuSub>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</Collapsible>
|
||||||
|
))}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroup>
|
||||||
|
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||||
|
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
||||||
|
<SidebarMenu>
|
||||||
|
{data.projects.map((item) => (
|
||||||
|
<SidebarMenuItem key={item.name}>
|
||||||
|
<SidebarMenuButton asChild>
|
||||||
|
<a href={item.url}>
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.name}</span>
|
||||||
|
</a>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<SidebarMenuAction showOnHover>
|
||||||
|
<MoreHorizontalIcon />
|
||||||
|
<span className="sr-only">More</span>
|
||||||
|
</SidebarMenuAction>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="w-48"
|
||||||
|
side="bottom"
|
||||||
|
align="end"
|
||||||
|
>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<FolderIcon className="text-muted-foreground" />
|
||||||
|
<span>View Project</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<ShareIcon className="text-muted-foreground" />
|
||||||
|
<span>Share Project</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Trash2Icon className="text-muted-foreground" />
|
||||||
|
<span>Delete Project</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
))}
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton>
|
||||||
|
<MoreHorizontalIcon />
|
||||||
|
<span>More</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroup>
|
||||||
|
<SidebarGroup className="mt-auto">
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
{data.navSecondary.map((item) => (
|
||||||
|
<SidebarMenuItem key={item.title}>
|
||||||
|
<SidebarMenuButton asChild>
|
||||||
|
<a href={item.url}>
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</a>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
))}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
</SidebarContent>
|
||||||
|
<SidebarFooter>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem className="flex items-center gap-2">
|
||||||
|
{/* <UserButton
|
||||||
|
showName
|
||||||
|
appearance={{
|
||||||
|
elements: {
|
||||||
|
rootBox: 'flex overflow-hidden w-full',
|
||||||
|
userButtonBox: 'flex-row-reverse',
|
||||||
|
userButtonOuterIdentifier: 'truncate pl-0',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
<ModeToggle />
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarFooter>
|
||||||
|
</Sidebar>
|
||||||
|
<SidebarInset>{children}</SidebarInset>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
42
apps/app/app/(authenticated)/layout.tsx
Normal file
42
apps/app/app/(authenticated)/layout.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { getSessionFromHeaders } from '@konobangu/auth/server';
|
||||||
|
import { SidebarProvider } from '@konobangu/design-system/components/ui/sidebar';
|
||||||
|
import { env } from '@konobangu/env';
|
||||||
|
import { showBetaFeature } from '@konobangu/feature-flags';
|
||||||
|
import { secure } from '@konobangu/security';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { PostHogIdentifier } from './components/posthog-identifier';
|
||||||
|
import { GlobalSidebar } from './components/sidebar';
|
||||||
|
|
||||||
|
type AppLayoutProperties = {
|
||||||
|
readonly children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppLayout = async ({ children }: AppLayoutProperties) => {
|
||||||
|
if (env.ARCJET_KEY) {
|
||||||
|
await secure(['CATEGORY:PREVIEW']);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = await getSessionFromHeaders();
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return redirect('/sign-in'); // from next/navigation
|
||||||
|
}
|
||||||
|
const betaFeature = await showBetaFeature();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarProvider>
|
||||||
|
<GlobalSidebar>
|
||||||
|
{betaFeature && (
|
||||||
|
<div className="m-4 rounded-full bg-success p-1.5 text-center text-sm text-success-foreground">
|
||||||
|
Beta feature now available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</GlobalSidebar>
|
||||||
|
<PostHogIdentifier />
|
||||||
|
</SidebarProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
57
apps/app/app/(authenticated)/page.tsx
Normal file
57
apps/app/app/(authenticated)/page.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { getSessionFromHeaders } from '@konobangu/auth/server';
|
||||||
|
import { database } from '@konobangu/database';
|
||||||
|
import { env } from '@konobangu/env';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import { AvatarStack } from './components/avatar-stack';
|
||||||
|
import { Cursors } from './components/cursors';
|
||||||
|
import { Header } from './components/header';
|
||||||
|
|
||||||
|
const title = 'Acme Inc';
|
||||||
|
const description = 'My application.';
|
||||||
|
|
||||||
|
const CollaborationProvider = dynamic(() =>
|
||||||
|
import('./components/collaboration-provider').then(
|
||||||
|
(mod) => mod.CollaborationProvider
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
};
|
||||||
|
|
||||||
|
const App = async () => {
|
||||||
|
const pages = await database.selectFrom('page').selectAll().execute();
|
||||||
|
const { orgId } = await getSessionFromHeaders();
|
||||||
|
|
||||||
|
if (!orgId) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header pages={['Building Your Application']} page="Data Fetching">
|
||||||
|
{env.LIVEBLOCKS_SECRET && (
|
||||||
|
<CollaborationProvider orgId={orgId}>
|
||||||
|
<AvatarStack />
|
||||||
|
<Cursors />
|
||||||
|
</CollaborationProvider>
|
||||||
|
)}
|
||||||
|
</Header>
|
||||||
|
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||||
|
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||||
|
{pages.map((page) => (
|
||||||
|
<div key={page.id} className="aspect-video rounded-xl bg-muted/50">
|
||||||
|
{page.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
29
apps/app/app/(authenticated)/webhooks/page.tsx
Normal file
29
apps/app/app/(authenticated)/webhooks/page.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { webhooks } from '@konobangu/webhooks';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Webhooks',
|
||||||
|
description: 'Send webhooks to your users.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const WebhooksPage = async () => {
|
||||||
|
const response = await webhooks.getAppPortal();
|
||||||
|
|
||||||
|
if (!response?.url) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full overflow-hidden">
|
||||||
|
<iframe
|
||||||
|
title="Webhooks"
|
||||||
|
src={response.url}
|
||||||
|
className="h-full w-full border-none"
|
||||||
|
allow="clipboard-write"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WebhooksPage;
|
58
apps/app/app/(unauthenticated)/layout.tsx
Normal file
58
apps/app/app/(unauthenticated)/layout.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { ModeToggle } from '@konobangu/design-system/components/mode-toggle';
|
||||||
|
import { env } from '@konobangu/env';
|
||||||
|
import { CommandIcon } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
type AuthLayoutProps = {
|
||||||
|
readonly children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AuthLayout = ({ children }: AuthLayoutProps) => (
|
||||||
|
<div className="container relative grid h-dvh flex-col items-center justify-center lg:max-w-none lg:grid-cols-2 lg:px-0">
|
||||||
|
<div className="relative hidden h-full flex-col bg-muted p-10 text-white lg:flex dark:border-r">
|
||||||
|
<div className="absolute inset-0 bg-zinc-900" />
|
||||||
|
<div className="relative z-20 flex items-center font-medium text-lg">
|
||||||
|
<CommandIcon className="mr-2 h-6 w-6" />
|
||||||
|
Acme Inc
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-4 right-4">
|
||||||
|
<ModeToggle />
|
||||||
|
</div>
|
||||||
|
<div className="relative z-20 mt-auto">
|
||||||
|
<blockquote className="space-y-2">
|
||||||
|
<p className="text-lg">
|
||||||
|
“This library has saved me countless hours of work and helped
|
||||||
|
me deliver stunning designs to my clients faster than ever
|
||||||
|
before.”
|
||||||
|
</p>
|
||||||
|
<footer className="text-sm">Sofia Davis</footer>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="lg:p-8">
|
||||||
|
<div className="mx-auto flex w-full max-w-[400px] flex-col justify-center space-y-6">
|
||||||
|
{children}
|
||||||
|
<p className="px-8 text-center text-muted-foreground text-sm">
|
||||||
|
By clicking continue, you agree to our{' '}
|
||||||
|
<Link
|
||||||
|
href={new URL('/legal/terms', env.NEXT_PUBLIC_WEB_URL).toString()}
|
||||||
|
className="underline underline-offset-4 hover:text-primary"
|
||||||
|
>
|
||||||
|
Terms of Service
|
||||||
|
</Link>{' '}
|
||||||
|
and{' '}
|
||||||
|
<Link
|
||||||
|
href={new URL('/legal/privacy', env.NEXT_PUBLIC_WEB_URL).toString()}
|
||||||
|
className="underline underline-offset-4 hover:text-primary"
|
||||||
|
>
|
||||||
|
Privacy Policy
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AuthLayout;
|
@ -0,0 +1,23 @@
|
|||||||
|
import { createMetadata } from '@konobangu/seo/metadata';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
const title = 'Welcome back';
|
||||||
|
const description = 'Enter your details to sign in.';
|
||||||
|
const SignIn = dynamic(() =>
|
||||||
|
import('@konobangu/auth/components/sign-in').then((mod) => mod.SignIn)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const metadata: Metadata = createMetadata({ title, description });
|
||||||
|
|
||||||
|
const SignInPage = () => (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col space-y-2 text-center">
|
||||||
|
<h1 className="font-semibold text-2xl tracking-tight">{title}</h1>
|
||||||
|
<p className="text-muted-foreground text-sm">{description}</p>
|
||||||
|
</div>
|
||||||
|
<SignIn />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SignInPage;
|
@ -0,0 +1,23 @@
|
|||||||
|
import { createMetadata } from '@konobangu/seo/metadata';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
const title = 'Create an account';
|
||||||
|
const description = 'Enter your details to get started.';
|
||||||
|
const SignUp = dynamic(() =>
|
||||||
|
import('@konobangu/auth/components/sign-up').then((mod) => mod.SignUp)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const metadata: Metadata = createMetadata({ title, description });
|
||||||
|
|
||||||
|
const SignUpPage = () => (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col space-y-2 text-center">
|
||||||
|
<h1 className="font-semibold text-2xl tracking-tight">{title}</h1>
|
||||||
|
<p className="text-muted-foreground text-sm">{description}</p>
|
||||||
|
</div>
|
||||||
|
<SignUp />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SignUpPage;
|
3
apps/app/app/.well-known/vercel/flags/route.ts
Normal file
3
apps/app/app/.well-known/vercel/flags/route.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { getFlags } from '@konobangu/feature-flags/access';
|
||||||
|
|
||||||
|
export const GET = getFlags;
|
63
apps/app/app/actions/users/get.ts
Normal file
63
apps/app/app/actions/users/get.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getFullOrganizationFromSession,
|
||||||
|
getSessionFromHeaders,
|
||||||
|
} from '@konobangu/auth/server';
|
||||||
|
import { tailwind } from '@konobangu/tailwind-config';
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
tailwind.theme.colors.red[500],
|
||||||
|
tailwind.theme.colors.orange[500],
|
||||||
|
tailwind.theme.colors.amber[500],
|
||||||
|
tailwind.theme.colors.yellow[500],
|
||||||
|
tailwind.theme.colors.lime[500],
|
||||||
|
tailwind.theme.colors.green[500],
|
||||||
|
tailwind.theme.colors.emerald[500],
|
||||||
|
tailwind.theme.colors.teal[500],
|
||||||
|
tailwind.theme.colors.cyan[500],
|
||||||
|
tailwind.theme.colors.sky[500],
|
||||||
|
tailwind.theme.colors.blue[500],
|
||||||
|
tailwind.theme.colors.indigo[500],
|
||||||
|
tailwind.theme.colors.violet[500],
|
||||||
|
tailwind.theme.colors.purple[500],
|
||||||
|
tailwind.theme.colors.fuchsia[500],
|
||||||
|
tailwind.theme.colors.pink[500],
|
||||||
|
tailwind.theme.colors.rose[500],
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getUsers = async (
|
||||||
|
userIds: string[]
|
||||||
|
): Promise<
|
||||||
|
| {
|
||||||
|
data: Liveblocks['UserMeta']['info'][];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
error: unknown;
|
||||||
|
}
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const session = await getSessionFromHeaders();
|
||||||
|
const { orgId } = session;
|
||||||
|
|
||||||
|
if (!orgId) {
|
||||||
|
throw new Error('Not logged in');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fullOrganization } = await getFullOrganizationFromSession(session);
|
||||||
|
|
||||||
|
const members = fullOrganization?.members || [];
|
||||||
|
|
||||||
|
const data: Liveblocks['UserMeta']['info'][] = members
|
||||||
|
.filter((user) => user?.userId && userIds.includes(user?.userId))
|
||||||
|
.map((user) => ({
|
||||||
|
name: user.user.name ?? user.user.email ?? 'Unknown user',
|
||||||
|
picture: user.user.image,
|
||||||
|
color: colors[Math.floor(Math.random() * colors.length)],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { data };
|
||||||
|
} catch (error) {
|
||||||
|
return { error };
|
||||||
|
}
|
||||||
|
};
|
50
apps/app/app/actions/users/search.ts
Normal file
50
apps/app/app/actions/users/search.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getFullOrganizationFromSession,
|
||||||
|
getSessionFromHeaders,
|
||||||
|
} from '@konobangu/auth/server';
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
|
export const searchUsers = async (
|
||||||
|
query: string
|
||||||
|
): Promise<
|
||||||
|
| {
|
||||||
|
data: string[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
error: unknown;
|
||||||
|
}
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const session = await getSessionFromHeaders();
|
||||||
|
const { orgId } = session;
|
||||||
|
|
||||||
|
if (!orgId) {
|
||||||
|
throw new Error('Not logged in');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fullOrganization } = await getFullOrganizationFromSession(session);
|
||||||
|
|
||||||
|
const members = fullOrganization?.members || [];
|
||||||
|
|
||||||
|
const users = members.map((user) => ({
|
||||||
|
id: user.id,
|
||||||
|
name: user.user.name ?? user.user.email ?? 'Unknown user',
|
||||||
|
imageUrl: user.user.image,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const fuse = new Fuse(users, {
|
||||||
|
keys: ['name'],
|
||||||
|
minMatchCharLength: 1,
|
||||||
|
threshold: 0.3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = fuse.search(query);
|
||||||
|
const data = results.map((result) => result.item.id);
|
||||||
|
|
||||||
|
return { data };
|
||||||
|
} catch (error) {
|
||||||
|
return { error };
|
||||||
|
}
|
||||||
|
};
|
42
apps/app/app/api/collaboration/auth/route.ts
Normal file
42
apps/app/app/api/collaboration/auth/route.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { getSessionFromHeaders } from '@konobangu/auth/server';
|
||||||
|
import { authenticate } from '@konobangu/collaboration/auth';
|
||||||
|
import { tailwind } from '@konobangu/tailwind-config';
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
tailwind.theme.colors.red[500],
|
||||||
|
tailwind.theme.colors.orange[500],
|
||||||
|
tailwind.theme.colors.amber[500],
|
||||||
|
tailwind.theme.colors.yellow[500],
|
||||||
|
tailwind.theme.colors.lime[500],
|
||||||
|
tailwind.theme.colors.green[500],
|
||||||
|
tailwind.theme.colors.emerald[500],
|
||||||
|
tailwind.theme.colors.teal[500],
|
||||||
|
tailwind.theme.colors.cyan[500],
|
||||||
|
tailwind.theme.colors.sky[500],
|
||||||
|
tailwind.theme.colors.blue[500],
|
||||||
|
tailwind.theme.colors.indigo[500],
|
||||||
|
tailwind.theme.colors.violet[500],
|
||||||
|
tailwind.theme.colors.purple[500],
|
||||||
|
tailwind.theme.colors.fuchsia[500],
|
||||||
|
tailwind.theme.colors.pink[500],
|
||||||
|
tailwind.theme.colors.rose[500],
|
||||||
|
];
|
||||||
|
|
||||||
|
export const POST = async () => {
|
||||||
|
const session = await getSessionFromHeaders();
|
||||||
|
const { orgId, user } = session;
|
||||||
|
|
||||||
|
if (!user || !orgId) {
|
||||||
|
return new Response('Unauthorized', { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return authenticate({
|
||||||
|
userId: user.id,
|
||||||
|
orgId,
|
||||||
|
userInfo: {
|
||||||
|
name: user.name ?? user.email ?? undefined,
|
||||||
|
avatar: user.image ?? undefined,
|
||||||
|
color: COLORS[Math.floor(Math.random() * COLORS.length)],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
BIN
apps/app/app/apple-icon.png
Normal file
BIN
apps/app/app/apple-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 216 B |
17
apps/app/app/cron/keep-alive/route.ts
Normal file
17
apps/app/app/cron/keep-alive/route.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { database } from '@konobangu/database';
|
||||||
|
|
||||||
|
export const POST = async () => {
|
||||||
|
const newPage = await database
|
||||||
|
.insertInto('page')
|
||||||
|
.values([
|
||||||
|
{
|
||||||
|
name: 'cron-temp',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.returning('id')
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
await database.deleteFrom('page').where('id', '=', newPage.id);
|
||||||
|
|
||||||
|
return new Response('OK', { status: 200 });
|
||||||
|
};
|
29
apps/app/app/global-error.tsx
Normal file
29
apps/app/app/global-error.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@konobangu/design-system/components/ui/button';
|
||||||
|
import { fonts } from '@konobangu/design-system/lib/fonts';
|
||||||
|
import { captureException } from '@sentry/nextjs';
|
||||||
|
import type NextError from 'next/error';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
type GlobalErrorProperties = {
|
||||||
|
readonly error: NextError & { digest?: string };
|
||||||
|
readonly reset: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GlobalError = ({ error, reset }: GlobalErrorProperties) => {
|
||||||
|
useEffect(() => {
|
||||||
|
captureException(error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang="en" className={fonts}>
|
||||||
|
<body>
|
||||||
|
<h1>Oops, something went wrong</h1>
|
||||||
|
<Button onClick={() => reset()}>Try again</Button>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GlobalError;
|
3
apps/app/app/health/route.ts
Normal file
3
apps/app/app/health/route.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const runtime = 'edge';
|
||||||
|
|
||||||
|
export const GET = (): Response => new Response('OK', { status: 200 });
|
BIN
apps/app/app/icon.png
Normal file
BIN
apps/app/app/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 96 B |
18
apps/app/app/layout.tsx
Normal file
18
apps/app/app/layout.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import '@konobangu/design-system/styles/globals.css';
|
||||||
|
import { DesignSystemProvider } from '@konobangu/design-system';
|
||||||
|
import { fonts } from '@konobangu/design-system/lib/fonts';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
type RootLayoutProperties = {
|
||||||
|
readonly children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RootLayout = ({ children }: RootLayoutProperties) => (
|
||||||
|
<html lang="en" className={fonts} suppressHydrationWarning>
|
||||||
|
<body>
|
||||||
|
<DesignSystemProvider>{children}</DesignSystemProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default RootLayout;
|
BIN
apps/app/app/opengraph-image.png
Normal file
BIN
apps/app/app/opengraph-image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
3
apps/app/instrumentation.ts
Normal file
3
apps/app/instrumentation.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { initializeSentry } from '@konobangu/next-config/instrumentation';
|
||||||
|
|
||||||
|
export const register = initializeSentry();
|
1
apps/app/liveblocks.config.ts
Normal file
1
apps/app/liveblocks.config.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@konobangu/collaboration/config';
|
22
apps/app/middleware.ts
Normal file
22
apps/app/middleware.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { authMiddleware } from '@konobangu/auth/middleware';
|
||||||
|
import {
|
||||||
|
noseconeConfig,
|
||||||
|
noseconeMiddleware,
|
||||||
|
} from '@konobangu/security/middleware';
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
const securityHeaders = noseconeMiddleware(noseconeConfig);
|
||||||
|
|
||||||
|
export async function middleware(_request: NextRequest) {
|
||||||
|
const response = await securityHeaders();
|
||||||
|
return authMiddleware(response as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
// Skip Next.js internals and all static files, unless found in search params
|
||||||
|
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
||||||
|
// Always run for API routes
|
||||||
|
'/(api|trpc)(.*)',
|
||||||
|
],
|
||||||
|
};
|
15
apps/app/next.config.ts
Normal file
15
apps/app/next.config.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { env } from '@konobangu/env';
|
||||||
|
import { config, withAnalyzer, withSentry } from '@konobangu/next-config';
|
||||||
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
|
let nextConfig: NextConfig = { ...config };
|
||||||
|
|
||||||
|
if (env.VERCEL) {
|
||||||
|
nextConfig = withSentry(nextConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (env.ANALYZE === 'true') {
|
||||||
|
nextConfig = withAnalyzer(nextConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default nextConfig;
|
51
apps/app/package.json
Normal file
51
apps/app/package.json
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev -p 5000 --turbopack",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"analyze": "ANALYZE=true pnpm build",
|
||||||
|
"test": "vitest run",
|
||||||
|
"clean": "git clean -xdf .cache .turbo dist node_modules",
|
||||||
|
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@konobangu/analytics": "workspace:*",
|
||||||
|
"@konobangu/auth": "workspace:*",
|
||||||
|
"@konobangu/collaboration": "workspace:*",
|
||||||
|
"@konobangu/database": "workspace:*",
|
||||||
|
"@konobangu/design-system": "workspace:*",
|
||||||
|
"@konobangu/env": "workspace:*",
|
||||||
|
"@konobangu/feature-flags": "workspace:*",
|
||||||
|
"@konobangu/migrate": "workspace:*",
|
||||||
|
"@konobangu/next-config": "workspace:*",
|
||||||
|
"@konobangu/security": "workspace:*",
|
||||||
|
"@konobangu/seo": "workspace:*",
|
||||||
|
"@konobangu/tailwind-config": "workspace:*",
|
||||||
|
"@konobangu/webhooks": "workspace:*",
|
||||||
|
"@prisma/client": "6.0.1",
|
||||||
|
"@sentry/nextjs": "^8.48.0",
|
||||||
|
"fuse.js": "^7.0.0",
|
||||||
|
"import-in-the-middle": "^1.12.0",
|
||||||
|
"lucide-react": "^0.468.0",
|
||||||
|
"next": "^15.1.4",
|
||||||
|
"next-themes": "^0.4.4",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"require-in-the-middle": "^7.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@konobangu/testing": "workspace:*",
|
||||||
|
"@konobangu/typescript-config": "workspace:*",
|
||||||
|
"@testing-library/dom": "^10.4.0",
|
||||||
|
"@testing-library/react": "^16.1.0",
|
||||||
|
"@types/node": "22.10.1",
|
||||||
|
"@types/react": "19.0.1",
|
||||||
|
"@types/react-dom": "19.0.2",
|
||||||
|
"jsdom": "^25.0.1",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"vitest": "^2.1.8"
|
||||||
|
}
|
||||||
|
}
|
1
apps/app/postcss.config.mjs
Normal file
1
apps/app/postcss.config.mjs
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from '@konobangu/design-system/postcss.config.mjs';
|
34
apps/app/sentry.client.config.ts
Normal file
34
apps/app/sentry.client.config.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* This file configures the initialization of Sentry on the client.
|
||||||
|
* The config you add here will be used whenever a users loads a page in their browser.
|
||||||
|
* https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { init, replayIntegration } from '@sentry/nextjs';
|
||||||
|
|
||||||
|
init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
|
||||||
|
// Adjust this value in production, or use tracesSampler for greater control
|
||||||
|
tracesSampleRate: 1,
|
||||||
|
|
||||||
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
|
debug: false,
|
||||||
|
|
||||||
|
replaysOnErrorSampleRate: 1,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This sets the sample rate to be 10%. You may want this to be 100% while
|
||||||
|
* in development and sample at a lower rate in production
|
||||||
|
*/
|
||||||
|
replaysSessionSampleRate: 0.1,
|
||||||
|
|
||||||
|
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
|
||||||
|
integrations: [
|
||||||
|
replayIntegration({
|
||||||
|
// Additional Replay configuration goes in here, for example:
|
||||||
|
maskAllText: true,
|
||||||
|
blockAllMedia: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
1
apps/app/tailwind.config.ts
Normal file
1
apps/app/tailwind.config.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { config as default } from '@konobangu/tailwind-config/config';
|
17
apps/app/tsconfig.json
Normal file
17
apps/app/tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"extends": "@konobangu/typescript-config/nextjs.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"],
|
||||||
|
"@konobangu/*": ["../../packages/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"next.config.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
8
apps/app/vercel.json
Normal file
8
apps/app/vercel.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"crons": [
|
||||||
|
{
|
||||||
|
"path": "/cron/keep-alive",
|
||||||
|
"schedule": "0 1 * * *"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
1
apps/app/vitest.config.ts
Normal file
1
apps/app/vitest.config.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from '@konobangu/testing';
|
@ -4,5 +4,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npx --yes mintlify dev --port 5004",
|
"dev": "npx --yes mintlify dev --port 5004",
|
||||||
"lint": "npx --yes mintlify broken-links"
|
"lint": "npx --yes mintlify broken-links"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,11 +10,15 @@
|
|||||||
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@konobangu/email": "workspace:*",
|
||||||
"@react-email/components": "0.0.31",
|
"@react-email/components": "0.0.31",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-email": "3.0.4"
|
"react-email": "3.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "19.0.1"
|
"@konobangu/typescript-config": "workspace:*",
|
||||||
|
"@types/node": "22.10.1",
|
||||||
|
"@types/react": "19.0.1",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "@konobangu/typescript-config/nextjs.json",
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"jsx": "react-jsx"
|
|
||||||
},
|
|
||||||
"include": ["**/*.ts", "**/*.tsx"],
|
"include": ["**/*.ts", "**/*.tsx"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ sea-orm = { version = "1.1", features = [
|
|||||||
"debug-print",
|
"debug-print",
|
||||||
] }
|
] }
|
||||||
figment = { version = "0.10", features = ["toml", "json", "env", "yaml"] }
|
figment = { version = "0.10", features = ["toml", "json", "env", "yaml"] }
|
||||||
|
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
uuid = { version = "1.6.0", features = ["v4"] }
|
uuid = { version = "1.6.0", features = ["v4"] }
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||||
|
@ -8,17 +8,30 @@
|
|||||||
"preview": "rsbuild preview"
|
"preview": "rsbuild preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@abraham/reflection": "^0.12.0",
|
||||||
"@graphiql/react": "^0.28.2",
|
"@graphiql/react": "^0.28.2",
|
||||||
"@graphiql/toolkit": "^0.11.1",
|
"@graphiql/toolkit": "^0.11.1",
|
||||||
|
"@konobangu/design-system": "workspace:*",
|
||||||
|
"@konobangu/tailwind-config": "workspace:*",
|
||||||
|
"@outposts/injection-js": "^2.5.1",
|
||||||
|
"@tanstack/react-router": "^1.95.6",
|
||||||
|
"@tanstack/router-devtools": "^1.95.6",
|
||||||
"graphiql": "^3.8.3",
|
"graphiql": "^3.8.3",
|
||||||
"graphql-ws": "^6.0.4",
|
"graphql-ws": "^5.16.2",
|
||||||
"observable-hooks": "^4.2.4",
|
"observable-hooks": "^4.2.4",
|
||||||
|
"oidc-client-rx": "0.1.0-alpha.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@konobangu/typescript-config": "workspace:*",
|
||||||
|
"@rsbuild/core": "1.1.3",
|
||||||
"@rsbuild/plugin-react": "^1.1.1",
|
"@rsbuild/plugin-react": "^1.1.1",
|
||||||
|
"@tanstack/router-plugin": "^1.95.6",
|
||||||
"@types/react": "^19.0.7",
|
"@types/react": "^19.0.7",
|
||||||
"@types/react-dom": "^19.0.3"
|
"@types/react-dom": "^19.0.3",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
'tailwindcss': {},
|
tailwindcss: {},
|
||||||
},
|
},
|
||||||
};
|
}
|
@ -1,5 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
'@tailwindcss/postcss': {},
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,103 +0,0 @@
|
|||||||
# Application logging configuration
|
|
||||||
[logger]
|
|
||||||
# Enable or disable logging.
|
|
||||||
enable = true
|
|
||||||
# Enable pretty backtrace (sets RUST_BACKTRACE=1)
|
|
||||||
pretty_backtrace = true
|
|
||||||
# Log level, options: trace, debug, info, warn or error.
|
|
||||||
level = "debug"
|
|
||||||
# Define the logging format. options: compact, pretty or Json
|
|
||||||
format = "compact"
|
|
||||||
# By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries
|
|
||||||
# Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.
|
|
||||||
# override_filter: trace
|
|
||||||
|
|
||||||
# Web server configuration
|
|
||||||
[server]
|
|
||||||
# Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
|
|
||||||
port = 5001
|
|
||||||
binding = "0.0.0.0"
|
|
||||||
# The UI hostname or IP address that mailers will point to.
|
|
||||||
host = '{{ get_env(name="HOST", default="localhost") }}'
|
|
||||||
# Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
|
|
||||||
|
|
||||||
# Enable Etag cache header middleware
|
|
||||||
[server.middlewares.etag]
|
|
||||||
enable = true
|
|
||||||
|
|
||||||
# Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details.
|
|
||||||
[server.middleware.request_id]
|
|
||||||
enable = true
|
|
||||||
|
|
||||||
[server.middleware.logger]
|
|
||||||
enable = true
|
|
||||||
|
|
||||||
# when your code is panicked, the request still returns 500 status code.
|
|
||||||
[server.middleware.catch_panic]
|
|
||||||
enable = true
|
|
||||||
|
|
||||||
# Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned.
|
|
||||||
[server.middleware.timeout_request]
|
|
||||||
enable = false
|
|
||||||
# Duration time in milliseconds.
|
|
||||||
timeout = 5000
|
|
||||||
|
|
||||||
# Set the value of the [`Access-Control-Allow-Origin`][mdn] header
|
|
||||||
# allow_origins:
|
|
||||||
# - https://loco.rs
|
|
||||||
# Set the value of the [`Access-Control-Allow-Headers`][mdn] header
|
|
||||||
# allow_headers:
|
|
||||||
# - Content-Type
|
|
||||||
# Set the value of the [`Access-Control-Allow-Methods`][mdn] header
|
|
||||||
# allow_methods:
|
|
||||||
# - POST
|
|
||||||
# Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds
|
|
||||||
# max_age: 3600
|
|
||||||
[server.middleware.cors]
|
|
||||||
enable = true
|
|
||||||
|
|
||||||
# Database Configuration
|
|
||||||
[database]
|
|
||||||
# Database connection URI
|
|
||||||
uri = '{{ get_env(name="DATABASE_URL", default="postgres://konobangu:konobangu@localhost:5432/konobangu") }}'
|
|
||||||
# When enabled, the sql query will be logged.
|
|
||||||
enable_logging = true
|
|
||||||
# Set the timeout duration when acquiring a connection.
|
|
||||||
connect_timeout = 500
|
|
||||||
# Set the idle duration before closing a connection.
|
|
||||||
idle_timeout = 500
|
|
||||||
# Minimum number of connections for a pool.
|
|
||||||
min_connections = 1
|
|
||||||
# Maximum number of connections for a pool.
|
|
||||||
max_connections = 10
|
|
||||||
# Run migration up when application loaded
|
|
||||||
auto_migrate = true
|
|
||||||
|
|
||||||
[storage]
|
|
||||||
data_dir = '{{ get_env(name="STORAGE_DATA_DIR", default="./data") }}'
|
|
||||||
|
|
||||||
[mikan]
|
|
||||||
base_url = "https://mikanani.me/"
|
|
||||||
|
|
||||||
[mikan.http_client]
|
|
||||||
exponential_backoff_max_retries = 3
|
|
||||||
leaky_bucket_max_tokens = 2
|
|
||||||
leaky_bucket_initial_tokens = 1
|
|
||||||
leaky_bucket_refill_tokens = 1
|
|
||||||
leaky_bucket_refill_interval = 500
|
|
||||||
|
|
||||||
[auth]
|
|
||||||
auth_type = '{{ get_env(name="AUTH_TYPE", default = "basic") }}'
|
|
||||||
basic_user = '{{ get_env(name="BASIC_USER", default = "konobangu") }}'
|
|
||||||
basic_password = '{{ get_env(name="BASIC_PASSWORD", default = "konobangu") }}'
|
|
||||||
oidc_issuer = '{{ get_env(name="OIDC_ISSUER", default = "") }}'
|
|
||||||
oidc_audience = '{{ get_env(name="OIDC_AUDIENCE", default = "") }}'
|
|
||||||
oidc_client_id = '{{ get_env(name="OIDC_CLIENT_ID", default = "") }}'
|
|
||||||
oidc_client_secret = '{{ get_env(name="OIDC_CLIENT_SECRET", default = "") }}'
|
|
||||||
oidc_extra_scopes = '{{ get_env(name="OIDC_EXTRA_SCOPES", default = "") }}'
|
|
||||||
oidc_extra_claim_key = '{{ get_env(name="OIDC_EXTRA_CLAIM_KEY", default = "") }}'
|
|
||||||
oidc_extra_claim_value = '{{ get_env(name="OIDC_EXTRA_CLAIM_VALUE", default = "") }}'
|
|
||||||
|
|
||||||
[graphql]
|
|
||||||
# depth_limit = inf
|
|
||||||
# complexity_limit = inf
|
|
101
apps/recorder/recorder.config.yaml
Normal file
101
apps/recorder/recorder.config.yaml
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# Loco configuration file documentation
|
||||||
|
|
||||||
|
# Application logging configuration
|
||||||
|
logger:
|
||||||
|
# Enable or disable logging.
|
||||||
|
enable: true
|
||||||
|
# Enable pretty backtrace (sets RUST_BACKTRACE=1)
|
||||||
|
pretty_backtrace: true
|
||||||
|
# Log level, options: trace, debug, info, warn or error.
|
||||||
|
level: debug
|
||||||
|
# Define the logging format. options: compact, pretty or Json
|
||||||
|
format: compact
|
||||||
|
# By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries
|
||||||
|
# Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.
|
||||||
|
# override_filter: trace
|
||||||
|
|
||||||
|
# Web server configuration
|
||||||
|
server:
|
||||||
|
# Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
|
||||||
|
port: 5001
|
||||||
|
binding: "0.0.0.0"
|
||||||
|
# The UI hostname or IP address that mailers will point to.
|
||||||
|
host: '{{ get_env(name="HOST", default="localhost") }}'
|
||||||
|
# Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
|
||||||
|
middlewares:
|
||||||
|
# Enable Etag cache header middleware
|
||||||
|
etag:
|
||||||
|
enable: true
|
||||||
|
# Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details.
|
||||||
|
logger:
|
||||||
|
# Enable/Disable the middleware.
|
||||||
|
enable: true
|
||||||
|
# when your code is panicked, the request still returns 500 status code.
|
||||||
|
catch_panic:
|
||||||
|
# Enable/Disable the middleware.
|
||||||
|
enable: true
|
||||||
|
# Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned.
|
||||||
|
timeout_request:
|
||||||
|
# Enable/Disable the middleware.
|
||||||
|
enable: false
|
||||||
|
# Duration time in milliseconds.
|
||||||
|
timeout: 5000
|
||||||
|
|
||||||
|
cors:
|
||||||
|
enable: true
|
||||||
|
# Set the value of the [`Access-Control-Allow-Origin`][mdn] header
|
||||||
|
# allow_origins:
|
||||||
|
# - https://loco.rs
|
||||||
|
# Set the value of the [`Access-Control-Allow-Headers`][mdn] header
|
||||||
|
# allow_headers:
|
||||||
|
# - Content-Type
|
||||||
|
# Set the value of the [`Access-Control-Allow-Methods`][mdn] header
|
||||||
|
# allow_methods:
|
||||||
|
# - POST
|
||||||
|
# Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds
|
||||||
|
# max_age: 3600
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
database:
|
||||||
|
# Database connection URI
|
||||||
|
uri: '{{ get_env(name="DATABASE_URL", default="postgres://konobangu:konobangu@localhost:5432/konobangu") }}'
|
||||||
|
# When enabled, the sql query will be logged.
|
||||||
|
enable_logging: true
|
||||||
|
# Set the timeout duration when acquiring a connection.
|
||||||
|
connect_timeout: 500
|
||||||
|
# Set the idle duration before closing a connection.
|
||||||
|
idle_timeout: 500
|
||||||
|
# Minimum number of connections for a pool.
|
||||||
|
min_connections: 1
|
||||||
|
# Maximum number of connections for a pool.
|
||||||
|
max_connections: 1
|
||||||
|
# Run migration up when application loaded
|
||||||
|
auto_migrate: true
|
||||||
|
|
||||||
|
storage:
|
||||||
|
data_dir: '{{ get_env(name="STORAGE_DATA_DIR", default="./data") }}'
|
||||||
|
|
||||||
|
mikan:
|
||||||
|
base_url: "https://mikanani.me/"
|
||||||
|
http_client:
|
||||||
|
exponential_backoff_max_retries: 3
|
||||||
|
leaky_bucket_max_tokens: 2
|
||||||
|
leaky_bucket_initial_tokens: 0
|
||||||
|
leaky_bucket_refill_tokens: 1
|
||||||
|
leaky_bucket_refill_interval: 500
|
||||||
|
|
||||||
|
auth:
|
||||||
|
auth_type: '{{ get_env(name="AUTH_TYPE", default = "basic") }}'
|
||||||
|
basic_user: '{{ get_env(name="BASIC_USER", default = "konobangu") }}'
|
||||||
|
basic_password: '{{ get_env(name="BASIC_PASSWORD", default = "konobangu") }}'
|
||||||
|
oidc_issuer: '{{ get_env(name="OIDC_ISSUER", default = "") }}'
|
||||||
|
oidc_audience: '{{ get_env(name="OIDC_AUDIENCE", default = "") }}'
|
||||||
|
oidc_client_id: '{{ get_env(name="OIDC_CLIENT_ID", default = "") }}'
|
||||||
|
oidc_client_secret: '{{ get_env(name="OIDC_CLIENT_SECRET", default = "") }}'
|
||||||
|
oidc_extra_scopes: '{{ get_env(name="OIDC_EXTRA_SCOPES", default = "") }}'
|
||||||
|
oidc_extra_claim_key: '{{ get_env(name="OIDC_EXTRA_CLAIM_KEY", default = "") }}'
|
||||||
|
oidc_extra_claim_value: '{{ get_env(name="OIDC_EXTRA_CLAIM_VALUE", default = "") }}'
|
||||||
|
|
||||||
|
graphql:
|
||||||
|
depth_limit: null
|
||||||
|
complexity_limit: null
|
@ -1,75 +1,7 @@
|
|||||||
use core::f64;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use serde::{
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
Deserialize, Deserializer, Serialize, Serializer,
|
|
||||||
de::{self, Unexpected},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct OnlyInfOrNaN(f64);
|
|
||||||
|
|
||||||
impl OnlyInfOrNaN {
|
|
||||||
pub fn inf() -> Self {
|
|
||||||
OnlyInfOrNaN(f64::INFINITY)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn nan() -> Self {
|
|
||||||
OnlyInfOrNaN(f64::NAN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<OnlyInfOrNaN> for Option<usize> {
|
|
||||||
fn from(_: OnlyInfOrNaN) -> Self {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for OnlyInfOrNaN {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_f64(self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for OnlyInfOrNaN {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let value = f64::deserialize(deserializer)?;
|
|
||||||
if value.is_nan() {
|
|
||||||
Ok(Self::nan())
|
|
||||||
} else if value.is_infinite() {
|
|
||||||
Ok(Self::inf())
|
|
||||||
} else {
|
|
||||||
Err(de::Error::invalid_value(
|
|
||||||
Unexpected::Float(value),
|
|
||||||
&"a NaN or a Inf",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum GraphQLLimitNum {
|
|
||||||
Num(usize),
|
|
||||||
Adhoc(OnlyInfOrNaN),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<GraphQLLimitNum> for Option<usize> {
|
|
||||||
fn from(value: GraphQLLimitNum) -> Self {
|
|
||||||
match value {
|
|
||||||
GraphQLLimitNum::Adhoc(v) => v.into(),
|
|
||||||
GraphQLLimitNum::Num(v) => Some(v),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct GraphQLConfig {
|
pub struct GraphQLConfig {
|
||||||
pub depth_limit: Option<GraphQLLimitNum>,
|
pub depth_limit: Option<usize>,
|
||||||
pub complexity_limit: Option<GraphQLLimitNum>,
|
pub complexity_limit: Option<usize>,
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,7 @@ impl GraphQLService {
|
|||||||
config: GraphQLConfig,
|
config: GraphQLConfig,
|
||||||
db: DatabaseConnection,
|
db: DatabaseConnection,
|
||||||
) -> RResult<Self> {
|
) -> RResult<Self> {
|
||||||
let schema = schema_root::schema(
|
let schema = schema_root::schema(db, config.depth_limit, config.complexity_limit)?;
|
||||||
db,
|
|
||||||
config.depth_limit.and_then(|l| l.into()),
|
|
||||||
config.complexity_limit.and_then(|l| l.into()),
|
|
||||||
)?;
|
|
||||||
Ok(Self { schema })
|
Ok(Self { schema })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,5 +24,5 @@ pub mod sync;
|
|||||||
pub mod tasks;
|
pub mod tasks;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test_utils;
|
pub mod test_utils;
|
||||||
pub mod utils;
|
pub mod views;
|
||||||
pub mod web;
|
pub mod web;
|
||||||
|
@ -1 +1,3 @@
|
|||||||
@import "tailwindcss";
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
@ -1 +0,0 @@
|
|||||||
|
|
1
apps/recorder/src/views/mod.rs
Normal file
1
apps/recorder/src/views/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod subscribers;
|
13
apps/recorder/src/views/subscribers.rs
Normal file
13
apps/recorder/src/views/subscribers.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::models::subscribers;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct CurrentResponse {}
|
||||||
|
|
||||||
|
impl CurrentResponse {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(_user: &subscribers::Model) -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
@ -2,10 +2,10 @@ import { type Fetcher, createGraphiQLFetcher } from '@graphiql/toolkit';
|
|||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
import GraphiQL from 'graphiql';
|
import GraphiQL from 'graphiql';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { firstValueFrom } from 'rxjs';
|
|
||||||
import { beforeLoadGuard } from '../../../auth/guard';
|
import { beforeLoadGuard } from '../../../auth/guard';
|
||||||
import { useAuth } from '../../../auth/hooks';
|
|
||||||
import 'graphiql/graphiql.css';
|
import 'graphiql/graphiql.css';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import { useAuth } from '../../../auth/hooks';
|
||||||
|
|
||||||
export const Route = createFileRoute('/graphql/')({
|
export const Route = createFileRoute('/graphql/')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
@ -32,5 +32,5 @@ function RouteComponent() {
|
|||||||
[oidcSecurityService]
|
[oidcSecurityService]
|
||||||
);
|
);
|
||||||
|
|
||||||
return <GraphiQL fetcher={fetcher} className="!h-svh" />;
|
return <GraphiQL fetcher={fetcher} className="h-svh" />;
|
||||||
}
|
}
|
||||||
|
1
apps/recorder/tailwind.config.ts
Normal file
1
apps/recorder/tailwind.config.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { config as default } from '@konobangu/tailwind-config/config';
|
@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "@konobangu/typescript-config/base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"rootDir": ".",
|
"lib": ["DOM", "ES2024", "DOM.AsyncIterable", "DOM.Iterable"],
|
||||||
"composite": true,
|
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
"noEmit": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
45
apps/storybook/.gitignore
vendored
Normal file
45
apps/storybook/.gitignore
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for commiting if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
*storybook.log
|
||||||
|
|
||||||
|
# storybook
|
||||||
|
storybook-static/
|
30
apps/storybook/.storybook/main.ts
Normal file
30
apps/storybook/.storybook/main.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import type { StorybookConfig } from '@storybook/nextjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is used to resolve the absolute path of a package.
|
||||||
|
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
|
||||||
|
*/
|
||||||
|
const getAbsolutePath = (value: string) =>
|
||||||
|
dirname(require.resolve(join(value, 'package.json')));
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: [
|
||||||
|
'../stories/**/*.mdx',
|
||||||
|
'../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||||
|
],
|
||||||
|
addons: [
|
||||||
|
getAbsolutePath('@storybook/addon-onboarding'),
|
||||||
|
getAbsolutePath('@storybook/addon-essentials'),
|
||||||
|
getAbsolutePath('@chromatic-com/storybook'),
|
||||||
|
getAbsolutePath('@storybook/addon-interactions'),
|
||||||
|
getAbsolutePath('@storybook/addon-themes'),
|
||||||
|
],
|
||||||
|
framework: {
|
||||||
|
name: getAbsolutePath('@storybook/nextjs'),
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
staticDirs: ['../public'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
17
apps/storybook/.storybook/preview-head.html
Normal file
17
apps/storybook/.storybook/preview-head.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!-- https://github.com/vercel/geist-font/issues/72 -->
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--font-geist-sans: "Geist", sans-serif;
|
||||||
|
--font-geist-mono: "Geist Mono", monospace;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
</style>
|
53
apps/storybook/.storybook/preview.tsx
Normal file
53
apps/storybook/.storybook/preview.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Toaster } from '@konobangu/design-system/components/ui/sonner';
|
||||||
|
import { TooltipProvider } from '@konobangu/design-system/components/ui/tooltip';
|
||||||
|
import { ThemeProvider } from '@konobangu/design-system/providers/theme';
|
||||||
|
import { withThemeByClassName } from '@storybook/addon-themes';
|
||||||
|
import type { Preview } from '@storybook/react';
|
||||||
|
|
||||||
|
import '@konobangu/design-system/styles/globals.css';
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/i,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chromatic: {
|
||||||
|
modes: {
|
||||||
|
light: {
|
||||||
|
theme: 'light',
|
||||||
|
className: 'light',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
theme: 'dark',
|
||||||
|
className: 'dark',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
withThemeByClassName({
|
||||||
|
themes: {
|
||||||
|
light: 'light',
|
||||||
|
dark: 'dark',
|
||||||
|
},
|
||||||
|
defaultTheme: 'light',
|
||||||
|
}),
|
||||||
|
(Story) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-background">
|
||||||
|
<ThemeProvider>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Story />
|
||||||
|
</TooltipProvider>
|
||||||
|
<Toaster />
|
||||||
|
</ThemeProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
40
apps/storybook/README.md
Normal file
40
apps/storybook/README.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:5000](http://localhost:5000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
[API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:5000/api/hello](http://localhost:5000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
||||||
|
|
||||||
|
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn-pages-router) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details.
|
7
apps/storybook/next.config.ts
Normal file
7
apps/storybook/next.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
39
apps/storybook/package.json
Normal file
39
apps/storybook/package.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "storybook",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "storybook dev -p 6006",
|
||||||
|
"build-storybook": "storybook build",
|
||||||
|
"chromatic": "chromatic --exit-zero-on-changes",
|
||||||
|
"clean": "git clean -xdf .cache .turbo dist node_modules",
|
||||||
|
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@konobangu/design-system": "workspace:*",
|
||||||
|
"lucide-react": "^0.468.0",
|
||||||
|
"next": "^15.1.4",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@chromatic-com/storybook": "^3.2.3",
|
||||||
|
"@konobangu/typescript-config": "workspace:*",
|
||||||
|
"@storybook/addon-essentials": "^8.4.7",
|
||||||
|
"@storybook/addon-interactions": "^8.4.7",
|
||||||
|
"@storybook/addon-onboarding": "^8.4.7",
|
||||||
|
"@storybook/addon-themes": "^8.4.7",
|
||||||
|
"@storybook/blocks": "^8.4.7",
|
||||||
|
"@storybook/nextjs": "^8.4.7",
|
||||||
|
"@storybook/react": "^8.4.7",
|
||||||
|
"@storybook/test": "^8.4.7",
|
||||||
|
"@types/node": "^22.10.6",
|
||||||
|
"@types/react": "^19.0.7",
|
||||||
|
"@types/react-dom": "^19.0.3",
|
||||||
|
"chromatic": "^11.23.0",
|
||||||
|
"postcss": "^8.5.1",
|
||||||
|
"storybook": "^8.4.7",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
}
|
||||||
|
}
|
8
apps/storybook/postcss.config.mjs
Normal file
8
apps/storybook/postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
BIN
apps/storybook/public/favicon.ico
Normal file
BIN
apps/storybook/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
60
apps/storybook/stories/accordion.stories.tsx
Normal file
60
apps/storybook/stories/accordion.stories.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from '@konobangu/design-system/components/ui/accordion';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A vertically stacked set of interactive headings that each reveal a section
|
||||||
|
* of content.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Accordion',
|
||||||
|
component: Accordion,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
type: {
|
||||||
|
options: ['single', 'multiple'],
|
||||||
|
control: { type: 'radio' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
type: 'single',
|
||||||
|
collapsible: true,
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<Accordion {...args}>
|
||||||
|
<AccordionItem value="item-1">
|
||||||
|
<AccordionTrigger>Is it accessible?</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
Yes. It adheres to the WAI-ARIA design pattern.
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem value="item-2">
|
||||||
|
<AccordionTrigger>Is it styled?</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
Yes. It comes with default styles that matches the other components'
|
||||||
|
aesthetic.
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem value="item-3">
|
||||||
|
<AccordionTrigger>Is it animated?</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
Yes. It's animated by default, but you can disable it if you prefer.
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
),
|
||||||
|
} satisfies Meta<typeof Accordion>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default behavior of the accordion allows only one item to be open.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
54
apps/storybook/stories/alert-dialog.stories.tsx
Normal file
54
apps/storybook/stories/alert-dialog.stories.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from '@konobangu/design-system/components/ui/alert-dialog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A modal dialog that interrupts the user with important content and expects
|
||||||
|
* a response.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/AlertDialog',
|
||||||
|
component: AlertDialog,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
render: (args) => (
|
||||||
|
<AlertDialog {...args}>
|
||||||
|
<AlertDialogTrigger>Open</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you sure absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete your
|
||||||
|
account and remove your data from our servers.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction>Continue</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof AlertDialog>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the alert dialog.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
60
apps/storybook/stories/alert.stories.tsx
Normal file
60
apps/storybook/stories/alert.stories.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
AlertDescription,
|
||||||
|
AlertTitle,
|
||||||
|
} from '@konobangu/design-system/components/ui/alert';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a callout for user attention.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Alert',
|
||||||
|
component: Alert,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
variant: {
|
||||||
|
options: ['default', 'destructive'],
|
||||||
|
control: { type: 'radio' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
variant: 'default',
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<Alert {...args}>
|
||||||
|
<AlertTitle>Heads up!</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
You can add components to your app using the cli.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
),
|
||||||
|
} satisfies Meta<typeof Alert>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
/**
|
||||||
|
* The default form of the alert.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `destructive` alert to indicate a destructive action.
|
||||||
|
*/
|
||||||
|
export const Destructive: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<Alert {...args}>
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertTitle>Error</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Your session has expired. Please log in again.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
),
|
||||||
|
args: {
|
||||||
|
variant: 'destructive',
|
||||||
|
},
|
||||||
|
};
|
71
apps/storybook/stories/aspect-ratio.stories.tsx
Normal file
71
apps/storybook/stories/aspect-ratio.stories.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
import { AspectRatio } from '@konobangu/design-system/components/ui/aspect-ratio';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays content within a desired ratio.
|
||||||
|
*/
|
||||||
|
const meta: Meta<typeof AspectRatio> = {
|
||||||
|
title: 'ui/AspectRatio',
|
||||||
|
component: AspectRatio,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
render: (args) => (
|
||||||
|
<AspectRatio {...args} className="bg-slate-50 dark:bg-slate-800">
|
||||||
|
<Image
|
||||||
|
src="https://images.unsplash.com/photo-1576075796033-848c2a5f3696?w=800&dpr=2&q=80"
|
||||||
|
alt="Photo by Alvaro Pinot"
|
||||||
|
fill
|
||||||
|
className="rounded-md object-cover"
|
||||||
|
/>
|
||||||
|
</AspectRatio>
|
||||||
|
),
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div className="w-1/2">
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
} satisfies Meta<typeof AspectRatio>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the aspect ratio.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
ratio: 16 / 9,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `1:1` aspect ratio to display a square image.
|
||||||
|
*/
|
||||||
|
export const Square: Story = {
|
||||||
|
args: {
|
||||||
|
ratio: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `4:3` aspect ratio to display a landscape image.
|
||||||
|
*/
|
||||||
|
export const Landscape: Story = {
|
||||||
|
args: {
|
||||||
|
ratio: 4 / 3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `2.35:1` aspect ratio to display a cinemascope image.
|
||||||
|
*/
|
||||||
|
export const Cinemascope: Story = {
|
||||||
|
args: {
|
||||||
|
ratio: 2.35 / 1,
|
||||||
|
},
|
||||||
|
};
|
35
apps/storybook/stories/avatar.stories.tsx
Normal file
35
apps/storybook/stories/avatar.stories.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
|
} from '@konobangu/design-system/components/ui/avatar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An image element with a fallback for representing the user.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Avatar',
|
||||||
|
component: Avatar,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
render: (args) => (
|
||||||
|
<Avatar {...args}>
|
||||||
|
<AvatarImage src="https://github.com/shadcn.png" />
|
||||||
|
<AvatarFallback>CN</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Avatar>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the avatar.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
62
apps/storybook/stories/badge.stories.tsx
Normal file
62
apps/storybook/stories/badge.stories.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Badge } from '@konobangu/design-system/components/ui/badge';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a badge or a component that looks like a badge.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Badge',
|
||||||
|
component: Badge,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
children: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
children: 'Badge',
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Badge>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the badge.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `secondary` badge to call for less urgent information, blending
|
||||||
|
* into the interface while still signaling minor updates or statuses.
|
||||||
|
*/
|
||||||
|
export const Secondary: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'secondary',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `destructive` badge to indicate errors, alerts, or the need for
|
||||||
|
* immediate attention.
|
||||||
|
*/
|
||||||
|
export const Destructive: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'destructive',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `outline` badge for overlaying without obscuring interface details,
|
||||||
|
* emphasizing clarity and subtlety..
|
||||||
|
*/
|
||||||
|
export const Outline: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'outline',
|
||||||
|
},
|
||||||
|
};
|
78
apps/storybook/stories/breadcrumb.stories.tsx
Normal file
78
apps/storybook/stories/breadcrumb.stories.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { ArrowRightSquare } from 'lucide-react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
} from '@konobangu/design-system/components/ui/breadcrumb';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the path to the current resource using a hierarchy of links.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Breadcrumb',
|
||||||
|
component: Breadcrumb,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
args: {},
|
||||||
|
render: (args) => (
|
||||||
|
<Breadcrumb {...args}>
|
||||||
|
<BreadcrumbList>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbLink>Home</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator />
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbLink>Components</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator />
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Breadcrumb>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the path of links to the current resource.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the path with a custom icon for the separator.
|
||||||
|
*/
|
||||||
|
export const WithCustomSeparator: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<Breadcrumb {...args}>
|
||||||
|
<BreadcrumbList>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbLink>Home</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator>
|
||||||
|
<ArrowRightSquare />
|
||||||
|
</BreadcrumbSeparator>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbLink>Components</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator>
|
||||||
|
<ArrowRightSquare />
|
||||||
|
</BreadcrumbSeparator>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
),
|
||||||
|
};
|
157
apps/storybook/stories/button.stories.tsx
Normal file
157
apps/storybook/stories/button.stories.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Loader2, Mail } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Button } from '@konobangu/design-system/components/ui/button';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a button or a component that looks like a button.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Button',
|
||||||
|
component: Button,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
children: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
children: 'Button',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Button>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the button, used for primary actions and commands.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `outline` button to reduce emphasis on secondary actions, such as
|
||||||
|
* canceling or dismissing a dialog.
|
||||||
|
*/
|
||||||
|
export const Outline: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'outline',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `ghost` button is minimalistic and subtle, for less intrusive
|
||||||
|
* actions.
|
||||||
|
*/
|
||||||
|
export const Ghost: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'ghost',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `secondary` button to call for less emphasized actions, styled to
|
||||||
|
* complement the primary button while being less conspicuous.
|
||||||
|
*/
|
||||||
|
export const Secondary: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'secondary',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `destructive` button to indicate errors, alerts, or the need for
|
||||||
|
* immediate attention.
|
||||||
|
*/
|
||||||
|
export const Destructive: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'destructive',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `link` button to reduce emphasis on tertiary actions, such as
|
||||||
|
* hyperlink or navigation, providing a text-only interactive element.
|
||||||
|
*/
|
||||||
|
export const Link: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'link',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the `disabled` prop to a button to prevent interactions and add a
|
||||||
|
* loading indicator, such as a spinner, to signify an in-progress action.
|
||||||
|
*/
|
||||||
|
export const Loading: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<Button {...args}>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Button
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
args: {
|
||||||
|
...Outline.args,
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an icon element to a button to enhance visual communication and
|
||||||
|
* providing additional context for the action.
|
||||||
|
*/
|
||||||
|
export const WithIcon: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<Button {...args}>
|
||||||
|
<Mail className="mr-2 h-4 w-4" /> Login with Email Button
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
args: {
|
||||||
|
...Secondary.args,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `sm` size for a smaller button, suitable for interfaces needing
|
||||||
|
* compact elements without sacrificing usability.
|
||||||
|
*/
|
||||||
|
export const Small: Story = {
|
||||||
|
args: {
|
||||||
|
size: 'sm',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `lg` size for a larger button, offering better visibility and
|
||||||
|
* easier interaction for users.
|
||||||
|
*/
|
||||||
|
export const Large: Story = {
|
||||||
|
args: {
|
||||||
|
size: 'lg',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the "icon" size for a button with only an icon.
|
||||||
|
*/
|
||||||
|
export const Icon: Story = {
|
||||||
|
args: {
|
||||||
|
...Secondary.args,
|
||||||
|
size: 'icon',
|
||||||
|
children: <Mail />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the `disabled` prop to prevent interactions with the button.
|
||||||
|
*/
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
};
|
81
apps/storybook/stories/calendar.stories.tsx
Normal file
81
apps/storybook/stories/calendar.stories.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { addDays } from 'date-fns';
|
||||||
|
|
||||||
|
import { Calendar } from '@konobangu/design-system/components/ui/calendar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A date field component that allows users to enter and edit date.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Calendar',
|
||||||
|
component: Calendar,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
args: {
|
||||||
|
mode: 'single',
|
||||||
|
selected: new Date(),
|
||||||
|
onSelect: action('onDayClick'),
|
||||||
|
className: 'rounded-md border w-fit',
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Calendar>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the calendar.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `multiple` mode to select multiple dates.
|
||||||
|
*/
|
||||||
|
export const Multiple: Story = {
|
||||||
|
args: {
|
||||||
|
min: 1,
|
||||||
|
selected: [new Date(), addDays(new Date(), 2), addDays(new Date(), 8)],
|
||||||
|
mode: 'multiple',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `range` mode to select a range of dates.
|
||||||
|
*/
|
||||||
|
export const Range: Story = {
|
||||||
|
args: {
|
||||||
|
selected: {
|
||||||
|
from: new Date(),
|
||||||
|
to: addDays(new Date(), 7),
|
||||||
|
},
|
||||||
|
mode: 'range',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `disabled` prop to disable specific dates.
|
||||||
|
*/
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: {
|
||||||
|
disabled: [
|
||||||
|
addDays(new Date(), 1),
|
||||||
|
addDays(new Date(), 2),
|
||||||
|
addDays(new Date(), 3),
|
||||||
|
addDays(new Date(), 5),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `numberOfMonths` prop to display multiple months.
|
||||||
|
*/
|
||||||
|
export const MultipleMonths: Story = {
|
||||||
|
args: {
|
||||||
|
numberOfMonths: 2,
|
||||||
|
showOutsideDays: false,
|
||||||
|
},
|
||||||
|
};
|
75
apps/storybook/stories/card.stories.tsx
Normal file
75
apps/storybook/stories/card.stories.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { BellRing } from 'lucide-react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@konobangu/design-system/components/ui/card';
|
||||||
|
|
||||||
|
const notifications = [
|
||||||
|
{
|
||||||
|
title: 'Your call has been confirmed.',
|
||||||
|
description: '1 hour ago',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'You have a new message!',
|
||||||
|
description: '1 hour ago',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Your subscription is expiring soon!',
|
||||||
|
description: '2 hours ago',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a card with header, content, and footer.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Card',
|
||||||
|
component: Card,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
args: {
|
||||||
|
className: 'w-96',
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<Card {...args}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Notifications</CardTitle>
|
||||||
|
<CardDescription>You have 3 unread messages.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-4">
|
||||||
|
{notifications.map((notification, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-4">
|
||||||
|
<BellRing className="size-6" />
|
||||||
|
<div>
|
||||||
|
<p>{notification.title}</p>
|
||||||
|
<p className="text-foreground/50">{notification.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<button type="button" className="hover:underline">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Card>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the card.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
73
apps/storybook/stories/carousel.stories.tsx
Normal file
73
apps/storybook/stories/carousel.stories.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
CarouselNext,
|
||||||
|
CarouselPrevious,
|
||||||
|
} from '@konobangu/design-system/components/ui/carousel';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A carousel with motion and swipe built using Embla.
|
||||||
|
*/
|
||||||
|
const meta: Meta<typeof Carousel> = {
|
||||||
|
title: 'ui/Carousel',
|
||||||
|
component: Carousel,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
args: {
|
||||||
|
className: 'w-full max-w-xs',
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<Carousel {...args}>
|
||||||
|
<CarouselContent>
|
||||||
|
{Array.from({ length: 5 }).map((_, index) => (
|
||||||
|
<CarouselItem key={index}>
|
||||||
|
<div className="flex aspect-square items-center justify-center rounded border bg-card p-6">
|
||||||
|
<span className="font-semibold text-4xl">{index + 1}</span>
|
||||||
|
</div>
|
||||||
|
</CarouselItem>
|
||||||
|
))}
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious />
|
||||||
|
<CarouselNext />
|
||||||
|
</Carousel>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Carousel>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the carousel.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `basis` utility class to change the size of the carousel.
|
||||||
|
*/
|
||||||
|
export const Size: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<Carousel {...args} className="mx-12 w-full max-w-xs">
|
||||||
|
<CarouselContent>
|
||||||
|
{Array.from({ length: 5 }).map((_, index) => (
|
||||||
|
<CarouselItem key={index} className="basis-1/3">
|
||||||
|
<div className="flex aspect-square items-center justify-center rounded border bg-card p-6">
|
||||||
|
<span className="font-semibold text-4xl">{index + 1}</span>
|
||||||
|
</div>
|
||||||
|
</CarouselItem>
|
||||||
|
))}
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious />
|
||||||
|
<CarouselNext />
|
||||||
|
</Carousel>
|
||||||
|
),
|
||||||
|
args: {
|
||||||
|
className: 'mx-12 w-full max-w-xs',
|
||||||
|
},
|
||||||
|
};
|
271
apps/storybook/stories/chart.stories.tsx
Normal file
271
apps/storybook/stories/chart.stories.tsx
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
Area,
|
||||||
|
AreaChart,
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Label,
|
||||||
|
Line,
|
||||||
|
LineChart,
|
||||||
|
Pie,
|
||||||
|
PieChart,
|
||||||
|
XAxis,
|
||||||
|
} from 'recharts';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ChartConfig,
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
} from '@konobangu/design-system/components/ui/chart';
|
||||||
|
|
||||||
|
const multiSeriesData = [
|
||||||
|
{ month: 'January', desktop: 186, mobile: 80 },
|
||||||
|
{ month: 'February', desktop: 305, mobile: 200 },
|
||||||
|
{ month: 'March', desktop: 237, mobile: 120 },
|
||||||
|
{ month: 'April', desktop: 73, mobile: 190 },
|
||||||
|
{ month: 'May', desktop: 209, mobile: 130 },
|
||||||
|
{ month: 'June', desktop: 214, mobile: 140 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const multiSeriesConfig = {
|
||||||
|
desktop: {
|
||||||
|
label: 'Desktop',
|
||||||
|
color: 'hsl(var(--chart-1))',
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
label: 'Mobile',
|
||||||
|
color: 'hsl(var(--chart-2))',
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
|
const singleSeriesData = [
|
||||||
|
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
|
||||||
|
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||||
|
{ browser: 'other', visitors: 190, fill: 'var(--color-other)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const singleSeriesConfig = {
|
||||||
|
visitors: {
|
||||||
|
label: 'Visitors',
|
||||||
|
},
|
||||||
|
chrome: {
|
||||||
|
label: 'Chrome',
|
||||||
|
color: 'hsl(var(--chart-1))',
|
||||||
|
},
|
||||||
|
safari: {
|
||||||
|
label: 'Safari',
|
||||||
|
color: 'hsl(var(--chart-2))',
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
label: 'Other',
|
||||||
|
color: 'hsl(var(--chart-5))',
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beautiful charts. Built using Recharts. Copy and paste into your apps.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Chart',
|
||||||
|
component: ChartContainer,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
args: {
|
||||||
|
children: <div />,
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof ChartContainer>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine multiple Area components to create a stacked area chart.
|
||||||
|
*/
|
||||||
|
export const StackedAreaChart: Story = {
|
||||||
|
args: {
|
||||||
|
config: multiSeriesConfig,
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<ChartContainer {...args}>
|
||||||
|
<AreaChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={multiSeriesData}
|
||||||
|
margin={{
|
||||||
|
left: 12,
|
||||||
|
right: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<XAxis
|
||||||
|
dataKey="month"
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
tickMargin={8}
|
||||||
|
tickFormatter={(value) => value.slice(0, 3)}
|
||||||
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
cursor={false}
|
||||||
|
content={<ChartTooltipContent indicator="dot" />}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey="mobile"
|
||||||
|
type="natural"
|
||||||
|
fill="var(--color-mobile)"
|
||||||
|
fillOpacity={0.4}
|
||||||
|
stroke="var(--color-mobile)"
|
||||||
|
stackId="a"
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey="desktop"
|
||||||
|
type="natural"
|
||||||
|
fill="var(--color-desktop)"
|
||||||
|
fillOpacity={0.4}
|
||||||
|
stroke="var(--color-desktop)"
|
||||||
|
stackId="a"
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine multiple Bar components to create a stacked bar chart.
|
||||||
|
*/
|
||||||
|
export const StackedBarChart: Story = {
|
||||||
|
args: {
|
||||||
|
config: multiSeriesConfig,
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<ChartContainer {...args}>
|
||||||
|
<BarChart accessibilityLayer data={multiSeriesData}>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<XAxis
|
||||||
|
dataKey="month"
|
||||||
|
tickLine={false}
|
||||||
|
tickMargin={10}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={(value) => value.slice(0, 3)}
|
||||||
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
cursor={false}
|
||||||
|
content={<ChartTooltipContent indicator="dashed" />}
|
||||||
|
/>
|
||||||
|
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
|
||||||
|
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
|
||||||
|
</BarChart>
|
||||||
|
</ChartContainer>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine multiple Line components to create a single line chart.
|
||||||
|
*/
|
||||||
|
export const MultiLineChart: Story = {
|
||||||
|
args: {
|
||||||
|
config: multiSeriesConfig,
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<ChartContainer {...args}>
|
||||||
|
<LineChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={multiSeriesData}
|
||||||
|
margin={{
|
||||||
|
left: 12,
|
||||||
|
right: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<XAxis
|
||||||
|
dataKey="month"
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
tickMargin={8}
|
||||||
|
tickFormatter={(value) => value.slice(0, 3)}
|
||||||
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
cursor={false}
|
||||||
|
content={<ChartTooltipContent hideLabel />}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
dataKey="desktop"
|
||||||
|
type="natural"
|
||||||
|
stroke="var(--color-desktop)"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={false}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
dataKey="mobile"
|
||||||
|
type="natural"
|
||||||
|
stroke="var(--color-mobile)"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={false}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ChartContainer>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine Pie and Label components to create a doughnut chart.
|
||||||
|
*/
|
||||||
|
export const DoughnutChart: Story = {
|
||||||
|
args: {
|
||||||
|
config: singleSeriesConfig,
|
||||||
|
},
|
||||||
|
render: (args) => {
|
||||||
|
const totalVisitors = useMemo(() => {
|
||||||
|
return singleSeriesData.reduce((acc, curr) => acc + curr.visitors, 0);
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<ChartContainer {...args}>
|
||||||
|
<PieChart>
|
||||||
|
<ChartTooltip
|
||||||
|
cursor={false}
|
||||||
|
content={<ChartTooltipContent hideLabel />}
|
||||||
|
/>
|
||||||
|
<Pie
|
||||||
|
data={singleSeriesData}
|
||||||
|
dataKey="visitors"
|
||||||
|
nameKey="browser"
|
||||||
|
innerRadius={48}
|
||||||
|
strokeWidth={5}
|
||||||
|
>
|
||||||
|
<Label
|
||||||
|
content={({ viewBox }) => {
|
||||||
|
if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
|
||||||
|
return (
|
||||||
|
<text
|
||||||
|
x={viewBox.cx}
|
||||||
|
y={viewBox.cy}
|
||||||
|
textAnchor="middle"
|
||||||
|
dominantBaseline="middle"
|
||||||
|
>
|
||||||
|
<tspan
|
||||||
|
x={viewBox.cx}
|
||||||
|
y={viewBox.cy}
|
||||||
|
className="fill-foreground font-bold text-3xl"
|
||||||
|
>
|
||||||
|
{totalVisitors.toLocaleString()}
|
||||||
|
</tspan>
|
||||||
|
<tspan
|
||||||
|
x={viewBox.cx}
|
||||||
|
y={(viewBox.cy || 0) + 24}
|
||||||
|
className="fill-muted-foreground"
|
||||||
|
>
|
||||||
|
Visitors
|
||||||
|
</tspan>
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Pie>
|
||||||
|
</PieChart>
|
||||||
|
</ChartContainer>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
50
apps/storybook/stories/checkbox.stories.tsx
Normal file
50
apps/storybook/stories/checkbox.stories.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Checkbox } from '@konobangu/design-system/components/ui/checkbox';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A control that allows the user to toggle between checked and not checked.
|
||||||
|
*/
|
||||||
|
const meta: Meta<typeof Checkbox> = {
|
||||||
|
title: 'ui/Checkbox',
|
||||||
|
component: Checkbox,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
args: {
|
||||||
|
id: 'terms',
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Checkbox {...args} />
|
||||||
|
<label
|
||||||
|
htmlFor={args.id}
|
||||||
|
className="font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Accept terms and conditions
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Checkbox>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the checkbox.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `disabled` prop to disable the checkbox.
|
||||||
|
*/
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: {
|
||||||
|
id: 'disabled-terms',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
};
|
55
apps/storybook/stories/collapsible.stories.tsx
Normal file
55
apps/storybook/stories/collapsible.stories.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@konobangu/design-system/components/ui/collapsible';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interactive component which expands/collapses a panel.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Collapsible',
|
||||||
|
component: Collapsible,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
args: {
|
||||||
|
className: 'w-96',
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<Collapsible {...args}>
|
||||||
|
<CollapsibleTrigger className="flex gap-2">
|
||||||
|
<h3 className="font-semibold">Can I use this in my project?</h3>
|
||||||
|
<Info className="size-6" />
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
Yes. Free to use for personal and commercial projects. No attribution
|
||||||
|
required.
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Collapsible>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the collapsible.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `disabled` prop to disable the interaction.
|
||||||
|
*/
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
};
|
55
apps/storybook/stories/command.stories.tsx
Normal file
55
apps/storybook/stories/command.stories.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { CommandSeparator } from 'cmdk';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from '@konobangu/design-system/components/ui/command';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fast, composable, unstyled command menu for React.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Command',
|
||||||
|
component: Command,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
args: {
|
||||||
|
className: 'rounded-lg w-96 border shadow-md',
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<Command {...args}>
|
||||||
|
<CommandInput placeholder="Type a command or search..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
|
<CommandGroup heading="Suggestions">
|
||||||
|
<CommandItem>Calendar</CommandItem>
|
||||||
|
<CommandItem>Search Emoji</CommandItem>
|
||||||
|
<CommandItem>Calculator</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
<CommandSeparator />
|
||||||
|
<CommandGroup heading="Settings">
|
||||||
|
<CommandItem>Profile</CommandItem>
|
||||||
|
<CommandItem>Billing</CommandItem>
|
||||||
|
<CommandItem>Settings</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Command>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the command.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
153
apps/storybook/stories/context-menu.stories.tsx
Normal file
153
apps/storybook/stories/context-menu.stories.tsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuCheckboxItem,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuLabel,
|
||||||
|
ContextMenuRadioGroup,
|
||||||
|
ContextMenuRadioItem,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuShortcut,
|
||||||
|
ContextMenuSub,
|
||||||
|
ContextMenuSubContent,
|
||||||
|
ContextMenuSubTrigger,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
} from '@konobangu/design-system/components/ui/context-menu';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a menu to the user — such as a set of actions or functions —
|
||||||
|
* triggered by a button.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/ContextMenu',
|
||||||
|
component: ContextMenu,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
args: {},
|
||||||
|
render: (args) => (
|
||||||
|
<ContextMenu {...args}>
|
||||||
|
<ContextMenuTrigger className="flex h-48 w-96 items-center justify-center rounded-md border border-dashed bg-accent text-sm">
|
||||||
|
Right click here
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent className="w-32">
|
||||||
|
<ContextMenuItem>Profile</ContextMenuItem>
|
||||||
|
<ContextMenuItem>Billing</ContextMenuItem>
|
||||||
|
<ContextMenuItem>Team</ContextMenuItem>
|
||||||
|
<ContextMenuItem>Subscription</ContextMenuItem>
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof ContextMenu>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the context menu.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A context menu with shortcuts.
|
||||||
|
*/
|
||||||
|
export const WithShortcuts: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<ContextMenu {...args}>
|
||||||
|
<ContextMenuTrigger className="flex h-48 w-96 items-center justify-center rounded-md border border-dashed bg-accent text-sm">
|
||||||
|
Right click here
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent className="w-32">
|
||||||
|
<ContextMenuItem>
|
||||||
|
Back
|
||||||
|
<ContextMenuShortcut>⌘[</ContextMenuShortcut>
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem disabled>
|
||||||
|
Forward
|
||||||
|
<ContextMenuShortcut>⌘]</ContextMenuShortcut>
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem>
|
||||||
|
Reload
|
||||||
|
<ContextMenuShortcut>⌘R</ContextMenuShortcut>
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A context menu with a submenu.
|
||||||
|
*/
|
||||||
|
export const WithSubmenu: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<ContextMenu {...args}>
|
||||||
|
<ContextMenuTrigger className="flex h-48 w-96 items-center justify-center rounded-md border border-dashed bg-accent text-sm">
|
||||||
|
Right click here
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent className="w-32">
|
||||||
|
<ContextMenuItem>
|
||||||
|
New Tab
|
||||||
|
<ContextMenuShortcut>⌘N</ContextMenuShortcut>
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSub>
|
||||||
|
<ContextMenuSubTrigger>More Tools</ContextMenuSubTrigger>
|
||||||
|
<ContextMenuSubContent>
|
||||||
|
<ContextMenuItem>
|
||||||
|
Save Page As...
|
||||||
|
<ContextMenuShortcut>⇧⌘S</ContextMenuShortcut>
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem>Create Shortcut...</ContextMenuItem>
|
||||||
|
<ContextMenuItem>Name Window...</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
<ContextMenuItem>Developer Tools</ContextMenuItem>
|
||||||
|
</ContextMenuSubContent>
|
||||||
|
</ContextMenuSub>
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A context menu with checkboxes.
|
||||||
|
*/
|
||||||
|
export const WithCheckboxes: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<ContextMenu {...args}>
|
||||||
|
<ContextMenuTrigger className="flex h-48 w-96 items-center justify-center rounded-md border border-dashed bg-accent text-sm">
|
||||||
|
Right click here
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent className="w-64">
|
||||||
|
<ContextMenuCheckboxItem checked>
|
||||||
|
Show Comments
|
||||||
|
<ContextMenuShortcut>⌘⇧C</ContextMenuShortcut>
|
||||||
|
</ContextMenuCheckboxItem>
|
||||||
|
<ContextMenuCheckboxItem>Show Preview</ContextMenuCheckboxItem>
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A context menu with a radio group.
|
||||||
|
*/
|
||||||
|
export const WithRadioGroup: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<ContextMenu {...args}>
|
||||||
|
<ContextMenuTrigger className="flex h-48 w-96 items-center justify-center rounded-md border border-dashed bg-accent text-sm">
|
||||||
|
Right click here
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent className="w-64">
|
||||||
|
<ContextMenuRadioGroup value="light">
|
||||||
|
<ContextMenuLabel inset>Theme</ContextMenuLabel>
|
||||||
|
<ContextMenuRadioItem value="light">Light</ContextMenuRadioItem>
|
||||||
|
<ContextMenuRadioItem value="dark">Dark</ContextMenuRadioItem>
|
||||||
|
</ContextMenuRadioGroup>
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
|
),
|
||||||
|
};
|
62
apps/storybook/stories/dialog.stories.tsx
Normal file
62
apps/storybook/stories/dialog.stories.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@konobangu/design-system/components/ui/dialog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A window overlaid on either the primary window or another dialog window,
|
||||||
|
* rendering the content underneath inert.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Dialog',
|
||||||
|
component: Dialog,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
render: (args) => (
|
||||||
|
<Dialog {...args}>
|
||||||
|
<DialogTrigger>Open</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete your
|
||||||
|
account and remove your data from our servers.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter className="gap-4">
|
||||||
|
<button type="button" className="hover:underline">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<DialogClose>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded bg-primary px-4 py-2 text-primary-foreground"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Dialog>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the dialog.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
58
apps/storybook/stories/drawer.stories.tsx
Normal file
58
apps/storybook/stories/drawer.stories.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerDescription,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerTrigger,
|
||||||
|
} from '@konobangu/design-system/components/ui/drawer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A drawer component for React.
|
||||||
|
*/
|
||||||
|
const meta: Meta<typeof Drawer> = {
|
||||||
|
title: 'ui/Drawer',
|
||||||
|
component: Drawer,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
render: (args) => (
|
||||||
|
<Drawer {...args}>
|
||||||
|
<DrawerTrigger>Open</DrawerTrigger>
|
||||||
|
<DrawerContent>
|
||||||
|
<DrawerHeader>
|
||||||
|
<DrawerTitle>Are you sure absolutely sure?</DrawerTitle>
|
||||||
|
<DrawerDescription>This action cannot be undone.</DrawerDescription>
|
||||||
|
</DrawerHeader>
|
||||||
|
<DrawerFooter>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded bg-primary px-4 py-2 text-primary-foreground"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
<DrawerClose>
|
||||||
|
<button type="button" className="hover:underline">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</DrawerClose>
|
||||||
|
</DrawerFooter>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the drawer.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
159
apps/storybook/stories/dropdown-menu.stories.tsx
Normal file
159
apps/storybook/stories/dropdown-menu.stories.tsx
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Mail, Plus, PlusCircle, Search, UserPlus } from 'lucide-react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@konobangu/design-system/components/ui/dropdown-menu';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a menu to the user — such as a set of actions or functions —
|
||||||
|
* triggered by a button.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/DropdownMenu',
|
||||||
|
component: DropdownMenu,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
render: (args) => (
|
||||||
|
<DropdownMenu {...args}>
|
||||||
|
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-44">
|
||||||
|
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>Profile</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Billing</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Team</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Subscription</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof DropdownMenu>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the dropdown menu.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dropdown menu with shortcuts.
|
||||||
|
*/
|
||||||
|
export const WithShortcuts: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<DropdownMenu {...args}>
|
||||||
|
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-44">
|
||||||
|
<DropdownMenuLabel>Controls</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
Back
|
||||||
|
<DropdownMenuShortcut>⌘[</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem disabled>
|
||||||
|
Forward
|
||||||
|
<DropdownMenuShortcut>⌘]</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dropdown menu with submenus.
|
||||||
|
*/
|
||||||
|
export const WithSubmenus: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<DropdownMenu {...args}>
|
||||||
|
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-44">
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Search className="mr-2 size-4" />
|
||||||
|
<span>Search</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Plus className="mr-2 size-4" />
|
||||||
|
<span>New Team</span>
|
||||||
|
<DropdownMenuShortcut>⌘+T</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
|
<UserPlus className="mr-2 size-4" />
|
||||||
|
<span>Invite users</span>
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Mail className="mr-2 size-4" />
|
||||||
|
<span>Email</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<PlusCircle className="mr-2 size-4" />
|
||||||
|
<span>More...</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dropdown menu with radio items.
|
||||||
|
*/
|
||||||
|
export const WithRadioItems: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<DropdownMenu {...args}>
|
||||||
|
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-44">
|
||||||
|
<DropdownMenuLabel inset>Status</DropdownMenuLabel>
|
||||||
|
<DropdownMenuRadioGroup value="warning">
|
||||||
|
<DropdownMenuRadioItem value="info">Info</DropdownMenuRadioItem>
|
||||||
|
<DropdownMenuRadioItem value="warning">Warning</DropdownMenuRadioItem>
|
||||||
|
<DropdownMenuRadioItem value="error">Error</DropdownMenuRadioItem>
|
||||||
|
</DropdownMenuRadioGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dropdown menu with checkboxes.
|
||||||
|
*/
|
||||||
|
export const WithCheckboxes: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<DropdownMenu {...args}>
|
||||||
|
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-44">
|
||||||
|
<DropdownMenuCheckboxItem checked>
|
||||||
|
Autosave
|
||||||
|
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuCheckboxItem>Show Comments</DropdownMenuCheckboxItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
),
|
||||||
|
};
|
85
apps/storybook/stories/form.stories.tsx
Normal file
85
apps/storybook/stories/form.stories.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@konobangu/design-system/components/ui/form';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Building forms with React Hook Form and Zod.
|
||||||
|
*/
|
||||||
|
const meta: Meta<typeof Form> = {
|
||||||
|
title: 'ui/Form',
|
||||||
|
component: Form,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
render: (args) => <ProfileForm {...args} />,
|
||||||
|
} satisfies Meta<typeof Form>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
username: z.string().min(2, {
|
||||||
|
message: 'Username must be at least 2 characters.',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ProfileForm = (args: Story['args']) => {
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
username: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
action('onSubmit')(values);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Form {...args} {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<input
|
||||||
|
className="w-full rounded-md border border-input bg-background px-3 py-2"
|
||||||
|
placeholder="username"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
This is your public display name.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="rounded bg-primary px-4 py-2 text-primary-foreground"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the form.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
49
apps/storybook/stories/hover-card.stories.tsx
Normal file
49
apps/storybook/stories/hover-card.stories.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
} from '@konobangu/design-system/components/ui/hover-card';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For sighted users to preview content available behind a link.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/HoverCard',
|
||||||
|
component: HoverCard,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
args: {},
|
||||||
|
render: (args) => (
|
||||||
|
<HoverCard {...args}>
|
||||||
|
<HoverCardTrigger>Hover</HoverCardTrigger>
|
||||||
|
<HoverCardContent>
|
||||||
|
The React Framework - created and maintained by @vercel.
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof HoverCard>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the hover card.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `openDelay` and `closeDelay` props to control the delay before the
|
||||||
|
* hover card opens and closes.
|
||||||
|
*/
|
||||||
|
export const Instant: Story = {
|
||||||
|
args: {
|
||||||
|
openDelay: 0,
|
||||||
|
closeDelay: 0,
|
||||||
|
},
|
||||||
|
};
|
70
apps/storybook/stories/input-otp.stories.tsx
Normal file
70
apps/storybook/stories/input-otp.stories.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp';
|
||||||
|
|
||||||
|
import {
|
||||||
|
InputOTP,
|
||||||
|
InputOTPGroup,
|
||||||
|
InputOTPSeparator,
|
||||||
|
InputOTPSlot,
|
||||||
|
} from '@konobangu/design-system/components/ui/input-otp';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accessible one-time password component with copy paste functionality.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/InputOTP',
|
||||||
|
component: InputOTP,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
args: {
|
||||||
|
maxLength: 6,
|
||||||
|
pattern: REGEXP_ONLY_DIGITS_AND_CHARS,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
render: (args) => (
|
||||||
|
<InputOTP {...args} render={undefined}>
|
||||||
|
<InputOTPGroup>
|
||||||
|
<InputOTPSlot index={0} />
|
||||||
|
<InputOTPSlot index={1} />
|
||||||
|
<InputOTPSlot index={2} />
|
||||||
|
<InputOTPSlot index={3} />
|
||||||
|
<InputOTPSlot index={4} />
|
||||||
|
<InputOTPSlot index={5} />
|
||||||
|
</InputOTPGroup>
|
||||||
|
</InputOTP>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof InputOTP>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the InputOTP field.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use multiple groups to separate the input slots.
|
||||||
|
*/
|
||||||
|
export const SeparatedGroup: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<InputOTP {...args} render={undefined}>
|
||||||
|
<InputOTPGroup>
|
||||||
|
<InputOTPSlot index={0} />
|
||||||
|
<InputOTPSlot index={1} />
|
||||||
|
<InputOTPSlot index={2} />
|
||||||
|
</InputOTPGroup>
|
||||||
|
<InputOTPSeparator />
|
||||||
|
<InputOTPGroup>
|
||||||
|
<InputOTPSlot index={3} />
|
||||||
|
<InputOTPSlot index={4} />
|
||||||
|
<InputOTPSlot index={5} />
|
||||||
|
</InputOTPGroup>
|
||||||
|
</InputOTP>
|
||||||
|
),
|
||||||
|
};
|
84
apps/storybook/stories/input.stories.tsx
Normal file
84
apps/storybook/stories/input.stories.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Input } from '@konobangu/design-system/components/ui/input';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a form input field or a component that looks like an input field.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Input',
|
||||||
|
component: Input,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
args: {
|
||||||
|
className: 'w-96',
|
||||||
|
type: 'email',
|
||||||
|
placeholder: 'Email',
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Input>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the input field.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `disabled` prop to make the input non-interactive and appears faded,
|
||||||
|
* indicating that input is not currently accepted.
|
||||||
|
*/
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: { disabled: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `Label` component to includes a clear, descriptive label above or
|
||||||
|
* alongside the input area to guide users.
|
||||||
|
*/
|
||||||
|
export const WithLabel: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<div className="grid items-center gap-1.5">
|
||||||
|
<label htmlFor="email">{args.placeholder}</label>
|
||||||
|
<Input {...args} id="email" />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use a text element below the input field to provide additional instructions
|
||||||
|
* or information to users.
|
||||||
|
*/
|
||||||
|
export const WithHelperText: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<div className="grid items-center gap-1.5">
|
||||||
|
<label htmlFor="email-2">{args.placeholder}</label>
|
||||||
|
<Input {...args} id="email-2" />
|
||||||
|
<p className="text-foreground/50 text-sm">Enter your email address.</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the `Button` component to indicate that the input field can be submitted
|
||||||
|
* or used to trigger an action.
|
||||||
|
*/
|
||||||
|
export const WithButton: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Input {...args} />
|
||||||
|
<button
|
||||||
|
className="rounded bg-primary px-4 py-2 text-primary-foreground"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Subscribe
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
30
apps/storybook/stories/label.stories.tsx
Normal file
30
apps/storybook/stories/label.stories.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Label } from '@konobangu/design-system/components/ui/label';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders an accessible label associated with controls.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Label',
|
||||||
|
component: Label,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
children: {
|
||||||
|
control: { type: 'text' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
children: 'Your email address',
|
||||||
|
htmlFor: 'email',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Label>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof Label>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the label.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
126
apps/storybook/stories/menubar.stories.tsx
Normal file
126
apps/storybook/stories/menubar.stories.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Menubar,
|
||||||
|
MenubarCheckboxItem,
|
||||||
|
MenubarContent,
|
||||||
|
MenubarGroup,
|
||||||
|
MenubarItem,
|
||||||
|
MenubarLabel,
|
||||||
|
MenubarMenu,
|
||||||
|
MenubarRadioGroup,
|
||||||
|
MenubarRadioItem,
|
||||||
|
MenubarSeparator,
|
||||||
|
MenubarShortcut,
|
||||||
|
MenubarSub,
|
||||||
|
MenubarSubContent,
|
||||||
|
MenubarSubTrigger,
|
||||||
|
MenubarTrigger,
|
||||||
|
} from '@konobangu/design-system/components/ui/menubar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A visually persistent menu common in desktop applications that provides
|
||||||
|
* quick access to a consistent set of commands.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Menubar',
|
||||||
|
component: Menubar,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
|
||||||
|
render: (args) => (
|
||||||
|
<Menubar {...args}>
|
||||||
|
<MenubarMenu>
|
||||||
|
<MenubarTrigger>File</MenubarTrigger>
|
||||||
|
<MenubarContent>
|
||||||
|
<MenubarItem>
|
||||||
|
New Tab <MenubarShortcut>⌘T</MenubarShortcut>
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem>New Window</MenubarItem>
|
||||||
|
<MenubarSeparator />
|
||||||
|
<MenubarItem disabled>Share</MenubarItem>
|
||||||
|
<MenubarSeparator />
|
||||||
|
<MenubarItem>Print</MenubarItem>
|
||||||
|
</MenubarContent>
|
||||||
|
</MenubarMenu>
|
||||||
|
</Menubar>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Menubar>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the menubar.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A menubar with a submenu.
|
||||||
|
*/
|
||||||
|
export const WithSubmenu: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<Menubar {...args}>
|
||||||
|
<MenubarMenu>
|
||||||
|
<MenubarTrigger>Actions</MenubarTrigger>
|
||||||
|
<MenubarContent>
|
||||||
|
<MenubarItem>Download</MenubarItem>
|
||||||
|
<MenubarSub>
|
||||||
|
<MenubarSubTrigger>Share</MenubarSubTrigger>
|
||||||
|
<MenubarSubContent>
|
||||||
|
<MenubarItem>Email link</MenubarItem>
|
||||||
|
<MenubarItem>Messages</MenubarItem>
|
||||||
|
<MenubarItem>Notes</MenubarItem>
|
||||||
|
</MenubarSubContent>
|
||||||
|
</MenubarSub>
|
||||||
|
</MenubarContent>
|
||||||
|
</MenubarMenu>
|
||||||
|
</Menubar>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A menubar with radio items.
|
||||||
|
*/
|
||||||
|
export const WithRadioItems: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<Menubar {...args}>
|
||||||
|
<MenubarMenu>
|
||||||
|
<MenubarTrigger>View</MenubarTrigger>
|
||||||
|
<MenubarContent>
|
||||||
|
<MenubarLabel inset>Device Size</MenubarLabel>
|
||||||
|
<MenubarRadioGroup value="md">
|
||||||
|
<MenubarRadioItem value="sm">Small</MenubarRadioItem>
|
||||||
|
<MenubarRadioItem value="md">Medium</MenubarRadioItem>
|
||||||
|
<MenubarRadioItem value="lg">Large</MenubarRadioItem>
|
||||||
|
</MenubarRadioGroup>
|
||||||
|
</MenubarContent>
|
||||||
|
</MenubarMenu>
|
||||||
|
</Menubar>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A menubar with checkbox items.
|
||||||
|
*/
|
||||||
|
export const WithCheckboxItems: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<Menubar {...args}>
|
||||||
|
<MenubarMenu>
|
||||||
|
<MenubarTrigger>Filters</MenubarTrigger>
|
||||||
|
<MenubarContent>
|
||||||
|
<MenubarItem>Show All</MenubarItem>
|
||||||
|
<MenubarGroup>
|
||||||
|
<MenubarCheckboxItem checked>Unread</MenubarCheckboxItem>
|
||||||
|
<MenubarCheckboxItem checked>Important</MenubarCheckboxItem>
|
||||||
|
<MenubarCheckboxItem>Flagged</MenubarCheckboxItem>
|
||||||
|
</MenubarGroup>
|
||||||
|
</MenubarContent>
|
||||||
|
</MenubarMenu>
|
||||||
|
</Menubar>
|
||||||
|
),
|
||||||
|
};
|
79
apps/storybook/stories/navigation-menu.stories.tsx
Normal file
79
apps/storybook/stories/navigation-menu.stories.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuList,
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
} from '@konobangu/design-system/components/ui/navigation-menu';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of links for navigating websites.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/NavigationMenu',
|
||||||
|
component: NavigationMenu,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
render: (args) => (
|
||||||
|
<NavigationMenu {...args}>
|
||||||
|
<NavigationMenuList>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
|
||||||
|
Overview
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuList>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuTrigger className={navigationMenuTriggerStyle()}>
|
||||||
|
Documentation
|
||||||
|
</NavigationMenuTrigger>
|
||||||
|
<NavigationMenuContent>
|
||||||
|
<ul className="grid w-96 p-2">
|
||||||
|
<li>
|
||||||
|
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
|
||||||
|
API Reference
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
|
||||||
|
Getting Started
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
|
||||||
|
Guides
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</NavigationMenuContent>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
</NavigationMenuList>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuLink
|
||||||
|
className={navigationMenuTriggerStyle()}
|
||||||
|
href="https:www.google.com"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
External
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
</NavigationMenuList>
|
||||||
|
</NavigationMenu>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof NavigationMenu>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the navigation menu.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
57
apps/storybook/stories/pagination.stories.tsx
Normal file
57
apps/storybook/stories/pagination.stories.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
} from '@konobangu/design-system/components/ui/pagination';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pagination with page navigation, next and previous links.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Pagination',
|
||||||
|
component: Pagination,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
render: (args) => (
|
||||||
|
<Pagination {...args}>
|
||||||
|
<PaginationContent>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationPrevious href="#" />
|
||||||
|
</PaginationItem>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationLink href="#">1</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationLink href="#">2</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationLink href="#">3</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationEllipsis />
|
||||||
|
</PaginationItem>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext href="#" />
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Pagination>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the pagination.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
36
apps/storybook/stories/popover.stories.tsx
Normal file
36
apps/storybook/stories/popover.stories.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@konobangu/design-system/components/ui/popover';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays rich content in a portal, triggered by a button.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Popover',
|
||||||
|
component: Popover,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
|
||||||
|
render: (args) => (
|
||||||
|
<Popover {...args}>
|
||||||
|
<PopoverTrigger>Open</PopoverTrigger>
|
||||||
|
<PopoverContent>Place content for the popover here.</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Popover>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the popover.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
45
apps/storybook/stories/progress.stories.tsx
Normal file
45
apps/storybook/stories/progress.stories.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Progress } from '@konobangu/design-system/components/ui/progress';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays an indicator showing the completion progress of a task, typically
|
||||||
|
* displayed as a progress bar.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/Progress',
|
||||||
|
component: Progress,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
args: {
|
||||||
|
value: 30,
|
||||||
|
max: 100,
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Progress>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the progress.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the progress is indeterminate.
|
||||||
|
*/
|
||||||
|
export const Indeterminate: Story = {
|
||||||
|
args: {
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the progress is completed.
|
||||||
|
*/
|
||||||
|
export const Completed: Story = {
|
||||||
|
args: {
|
||||||
|
value: 100,
|
||||||
|
},
|
||||||
|
};
|
40
apps/storybook/stories/radio-group.stories.tsx
Normal file
40
apps/storybook/stories/radio-group.stories.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
RadioGroup,
|
||||||
|
RadioGroupItem,
|
||||||
|
} from '@konobangu/design-system/components/ui/radio-group';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of checkable buttons—known as radio buttons—where no more than one of
|
||||||
|
* the buttons can be checked at a time.
|
||||||
|
*/
|
||||||
|
const meta = {
|
||||||
|
title: 'ui/RadioGroup',
|
||||||
|
component: RadioGroup,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
args: {
|
||||||
|
defaultValue: 'comfortable',
|
||||||
|
className: 'grid gap-2 grid-cols-[1rem_1fr] items-center',
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<RadioGroup {...args}>
|
||||||
|
<RadioGroupItem value="default" id="r1" />
|
||||||
|
<label htmlFor="r1">Default</label>
|
||||||
|
<RadioGroupItem value="comfortable" id="r2" />
|
||||||
|
<label htmlFor="r2">Comfortable</label>
|
||||||
|
<RadioGroupItem value="compact" id="r3" />
|
||||||
|
<label htmlFor="r3">Compact</label>
|
||||||
|
</RadioGroup>
|
||||||
|
),
|
||||||
|
} satisfies Meta<typeof RadioGroup>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form of the radio group.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user