refactor: remove useless folders

This commit is contained in:
master 2025-03-04 01:03:39 +08:00
parent 2844e1fc32
commit 408d211f27
405 changed files with 9988 additions and 28082 deletions

View File

@ -1,29 +0,0 @@
# 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"

View File

@ -1,29 +0,0 @@
# 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
View File

@ -1,45 +0,0 @@
# 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

View File

@ -1,13 +0,0 @@
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();
});

View File

@ -1,13 +0,0 @@
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();
});

View File

@ -1,59 +0,0 @@
'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>
);
};

View File

@ -1,48 +0,0 @@
'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>
);
};

View File

@ -1,106 +0,0 @@
'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}
/>
);
});
};

View File

@ -1,43 +0,0 @@
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>
);

View File

@ -1,44 +0,0 @@
'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;
};

View File

@ -1,342 +0,0 @@
'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>
</>
);
};

View File

@ -1,42 +0,0 @@
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;

View File

@ -1,57 +0,0 @@
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;

View File

@ -1,29 +0,0 @@
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;

View File

@ -1,58 +0,0 @@
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">
&ldquo;This library has saved me countless hours of work and helped
me deliver stunning designs to my clients faster than ever
before.&rdquo;
</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;

View File

@ -1,23 +0,0 @@
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;

View File

@ -1,23 +0,0 @@
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;

View File

@ -1,3 +0,0 @@
import { getFlags } from '@konobangu/feature-flags/access';
export const GET = getFlags;

View File

@ -1,63 +0,0 @@
'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 };
}
};

View File

@ -1,50 +0,0 @@
'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 };
}
};

View File

@ -1,42 +0,0 @@
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)],
},
});
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 B

View File

@ -1,17 +0,0 @@
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 });
};

View File

@ -1,29 +0,0 @@
'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;

View File

@ -1,3 +0,0 @@
export const runtime = 'edge';
export const GET = (): Response => new Response('OK', { status: 200 });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 B

View File

@ -1,18 +0,0 @@
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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

View File

@ -1,3 +0,0 @@
import { initializeSentry } from '@konobangu/next-config/instrumentation';
export const register = initializeSentry();

View File

@ -1 +0,0 @@
export * from '@konobangu/collaboration/config';

View File

@ -1,22 +0,0 @@
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)(.*)',
],
};

View File

@ -1,15 +0,0 @@
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;

View File

@ -1,51 +0,0 @@
{
"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"
}
}

View File

@ -1 +0,0 @@
export { default } from '@konobangu/design-system/postcss.config.mjs';

View File

@ -1,34 +0,0 @@
/*
* 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,
}),
],
});

View File

@ -1 +0,0 @@
export { config as default } from '@konobangu/tailwind-config/config';

View File

@ -1,17 +0,0 @@
{
"extends": "@konobangu/typescript-config/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@konobangu/*": ["../../packages/*"]
}
},
"include": [
"next-env.d.ts",
"next.config.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
]
}

View File

@ -1,8 +0,0 @@
{
"crons": [
{
"path": "/cron/keep-alive",
"schedule": "0 1 * * *"
}
]
}

View File

@ -1 +0,0 @@
export { default } from '@konobangu/testing';

View File

@ -4,8 +4,5 @@
"scripts": {
"dev": "npx --yes mintlify dev --port 5004",
"lint": "npx --yes mintlify broken-links"
},
"devDependencies": {
"typescript": "^5.7.3"
}
}

View File

@ -10,15 +10,11 @@
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"dependencies": {
"@konobangu/email": "workspace:*",
"@react-email/components": "0.0.31",
"react": "^19.0.0",
"react-email": "3.0.4"
},
"devDependencies": {
"@konobangu/typescript-config": "workspace:*",
"@types/node": "22.10.1",
"@types/react": "19.0.1",
"typescript": "^5.7.3"
"@types/react": "19.0.1"
}
}

View File

@ -1,5 +1,9 @@
{
"extends": "@konobangu/typescript-config/nextjs.json",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"jsx": "react-jsx"
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@ -36,7 +36,6 @@ sea-orm = { version = "1.1", features = [
"debug-print",
] }
figment = { version = "0.10", features = ["toml", "json", "env", "yaml"] }
axum = "0.8"
uuid = { version = "1.6.0", features = ["v4"] }
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }

View File

@ -8,30 +8,17 @@
"preview": "rsbuild preview"
},
"dependencies": {
"@abraham/reflection": "^0.12.0",
"@graphiql/react": "^0.28.2",
"@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",
"graphql-ws": "^5.16.2",
"graphql-ws": "^6.0.4",
"observable-hooks": "^4.2.4",
"oidc-client-rx": "0.1.0-alpha.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"rxjs": "^7.8.1"
"react-dom": "^19.0.0"
},
"devDependencies": {
"@konobangu/typescript-config": "workspace:*",
"@rsbuild/core": "1.1.3",
"@rsbuild/plugin-react": "^1.1.1",
"@tanstack/router-plugin": "^1.95.6",
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3"
"@types/react-dom": "^19.0.3"
}
}

View File

@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};

View File

@ -0,0 +1,103 @@
# 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

View File

@ -1,101 +0,0 @@
# 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

View File

@ -1,7 +1,75 @@
use serde::{Deserialize, Serialize};
use core::f64;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GraphQLConfig {
pub depth_limit: Option<usize>,
pub complexity_limit: Option<usize>,
use serde::{
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 depth_limit: Option<GraphQLLimitNum>,
pub complexity_limit: Option<GraphQLLimitNum>,
}

View File

@ -14,7 +14,11 @@ impl GraphQLService {
config: GraphQLConfig,
db: DatabaseConnection,
) -> RResult<Self> {
let schema = schema_root::schema(db, config.depth_limit, config.complexity_limit)?;
let schema = schema_root::schema(
db,
config.depth_limit.and_then(|l| l.into()),
config.complexity_limit.and_then(|l| l.into()),
)?;
Ok(Self { schema })
}
}

View File

@ -24,5 +24,5 @@ pub mod sync;
pub mod tasks;
#[cfg(test)]
pub mod test_utils;
pub mod views;
pub mod utils;
pub mod web;

View File

@ -1,3 +1 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";

View File

@ -0,0 +1 @@

View File

@ -1 +0,0 @@
pub mod subscribers;

View File

@ -1,13 +0,0 @@
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 {}
}
}

View File

@ -2,10 +2,10 @@ import { type Fetcher, createGraphiQLFetcher } from '@graphiql/toolkit';
import { createFileRoute } from '@tanstack/react-router';
import GraphiQL from 'graphiql';
import { useMemo } from 'react';
import { beforeLoadGuard } from '../../../auth/guard';
import 'graphiql/graphiql.css';
import { firstValueFrom } from 'rxjs';
import { beforeLoadGuard } from '../../../auth/guard';
import { useAuth } from '../../../auth/hooks';
import 'graphiql/graphiql.css';
export const Route = createFileRoute('/graphql/')({
component: RouteComponent,
@ -32,5 +32,5 @@ function RouteComponent() {
[oidcSecurityService]
);
return <GraphiQL fetcher={fetcher} className="h-svh" />;
return <GraphiQL fetcher={fetcher} className="!h-svh" />;
}

View File

@ -1 +0,0 @@
export { config as default } from '@konobangu/tailwind-config/config';

View File

@ -1,12 +1,11 @@
{
"extends": "@konobangu/typescript-config/base.json",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": ["DOM", "ES2024", "DOM.AsyncIterable", "DOM.Iterable"],
"rootDir": ".",
"composite": true,
"jsx": "react-jsx",
"noEmit": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": true,

View File

@ -1,45 +0,0 @@
# 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/

View File

@ -1,30 +0,0 @@
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;

View File

@ -1,17 +0,0 @@
<!-- 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>

View File

@ -1,53 +0,0 @@
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;

View File

@ -1,40 +0,0 @@
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.

View File

@ -1,7 +0,0 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactStrictMode: true,
};
export default nextConfig;

View File

@ -1,39 +0,0 @@
{
"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"
}
}

View File

@ -1,8 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,60 +0,0 @@
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 = {};

View File

@ -1,54 +0,0 @@
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 = {};

View File

@ -1,60 +0,0 @@
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',
},
};

View File

@ -1,71 +0,0 @@
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,
},
};

View File

@ -1,35 +0,0 @@
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 = {};

View File

@ -1,62 +0,0 @@
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',
},
};

View File

@ -1,78 +0,0 @@
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>
),
};

View File

@ -1,157 +0,0 @@
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,
},
};

View File

@ -1,81 +0,0 @@
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,
},
};

View File

@ -1,75 +0,0 @@
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 = {};

View File

@ -1,73 +0,0 @@
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',
},
};

View File

@ -1,271 +0,0 @@
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>
);
},
};

View File

@ -1,50 +0,0 @@
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,
},
};

View File

@ -1,55 +0,0 @@
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,
},
};

View File

@ -1,55 +0,0 @@
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 = {};

View File

@ -1,153 +0,0 @@
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>
),
};

View File

@ -1,62 +0,0 @@
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 = {};

View File

@ -1,58 +0,0 @@
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 = {};

View File

@ -1,159 +0,0 @@
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>
),
};

View File

@ -1,85 +0,0 @@
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 = {};

View File

@ -1,49 +0,0 @@
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,
},
};

View File

@ -1,70 +0,0 @@
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>
),
};

View File

@ -1,84 +0,0 @@
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>
),
};

View File

@ -1,30 +0,0 @@
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 = {};

View File

@ -1,126 +0,0 @@
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>
),
};

View File

@ -1,79 +0,0 @@
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 = {};

View File

@ -1,57 +0,0 @@
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 = {};

View File

@ -1,36 +0,0 @@
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 = {};

View File

@ -1,45 +0,0 @@
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,
},
};

View File

@ -1,40 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
RadioGroup,
RadioGroupItem,
} from '@konobangu/design-system/components/ui/radio-group';
/**
* A set of checkable buttonsknown as radio buttonswhere 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 = {};

View File

@ -1,59 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@konobangu/design-system/components/ui/resizable';
/**
* Accessible resizable panel groups and layouts with keyboard support.
*/
const meta: Meta<typeof ResizablePanelGroup> = {
title: 'ui/ResizablePanelGroup',
component: ResizablePanelGroup,
tags: ['autodocs'],
argTypes: {
onLayout: {
control: false,
},
},
args: {
className: 'max-w-96 rounded-lg border',
direction: 'horizontal',
},
render: (args) => (
<ResizablePanelGroup {...args}>
<ResizablePanel defaultSize={50}>
<div className="flex h-[200px] items-center justify-center p-6">
<span className="font-semibold">One</span>
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={50}>
<ResizablePanelGroup direction="vertical">
<ResizablePanel defaultSize={25}>
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Two</span>
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={75}>
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Three</span>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
),
} satisfies Meta<typeof ResizablePanelGroup>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the resizable panel group.
*/
export const Default: Story = {};

Some files were not shown because too many files have changed in this diff Show More