diff --git a/.cargo/config.toml b/.cargo/config.toml index bc10c1a..01ffcf7 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,6 @@ [alias] -recorder = "run -p recorder --bin recorder_cli -- --environment recorder.development" -recorder-playground = "run -p recorder --example playground -- --environment recorder.development" +recorder = "run -p recorder --bin recorder_cli -- --environment recorder/development" +recorder-playground = "run -p recorder --example playground -- --environment recorder/development" [build] rustflags = ["-Zthreads=8"] diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d8e085a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +end_of_line = lf +max_line_length = null diff --git a/.gitignore b/.gitignore index cc5bee7..9bcd4eb 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,8 @@ build/Release # Dependency directories node_modules/ jspm_packages/ +.pnp +.pnp.js # Snowpack dependency directory (https://snowpack.dev/) web_modules/ @@ -155,9 +157,12 @@ web_modules/ # Yarn Integrity file .yarn-integrity -# dotenv environment variables file +# Local env files .env -.env.test +.env.local +.env.development.local +.env.test.local +.env.production.local # parcel-bundler cache (https://parceljs.org/) .cache @@ -222,4 +227,32 @@ index.d.ts.map /temp /rustc-ice-* + +# Misc +.DS_Store +*.pem + +# Sentry Config File +.env.sentry-build-plugin + +# BaseHub +.basehub + +# Build Outputs +build +dist + +# Turbo +.turbo + +# Vercel +.vercel + +# Payload default media upload directory +public/media/ + +public/robots.txt +public/sitemap*.xml + +# Custom /data \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..996b10e --- /dev/null +++ b/.prettierignore @@ -0,0 +1,14 @@ +**/payload-types.ts +.tmp +**/.git +**/.hg +**/.pnp.* +**/.svn +**/.yarn/** +**/build +**/dist/** +**/node_modules +**/temp +**/docs/** +tsconfig.json + diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f1f907f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "biomejs.biome", + "bradlc.vscode-tailwindcss", + "unifiedjs.vscode-mdx", + "mikestead.dotenv", + "christian-kohler.npm-intellisense", + "skellock.just" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 427cd52..057362d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -40,7 +40,7 @@ }, "args": [ "--environment", - "recorder.development" + "recorder/development" ], "cwd": "${workspaceFolder}" }, @@ -61,7 +61,7 @@ }, "args": [ "--environment", - "recorder.development" + "recorder/development" ], "cwd": "${workspaceFolder}" }, @@ -83,6 +83,57 @@ }, "args": [], "cwd": "${workspaceFolder}" + }, + { + "name": "Next.js: debug server-side", + "type": "node-terminal", + "request": "launch", + "command": "pnpm dev" + }, + { + "name": "Next.js: debug client-side (app)", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000" + }, + { + "name": "Next.js: debug client-side (web)", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3001" + }, + { + "name": "Next.js: debug client-side (api)", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3002" + }, + { + "name": "Next.js: debug client-side (email)", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3003" + }, + { + "name": "Next.js: debug client-side (app)", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3004" + }, + { + "name": "Next.js: debug full stack", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/.bin/next", + "runtimeArgs": ["--inspect"], + "skipFiles": ["/**"], + "serverReadyAction": { + "action": "debugWithEdge", + "killOnServerStop": true, + "pattern": "- Local:.+(https?://.+)", + "uriFormat": "%s", + "webRoot": "${workspaceFolder}" + } } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 875c867..05e54d8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,32 @@ { - "rust-analyzer.showUnlinkedFileNotification": false -} \ No newline at end of file + "npm.packageManager": "pnpm", + "rust-analyzer.showUnlinkedFileNotification": false, + "[javascript]": { + "editor.defaultFormatter": "vscode.typescript-language-features", + "editor.formatOnSave": true + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true + }, + "editor.codeActionsOnSave": { + "quickfix.biome": "explicit", + "source.organizeImports.biome": "explicit" + }, + "emmet.showExpandedAbbreviation": "never", + "prettier.enable": false, + "tailwindCSS.experimental.configFile": "./packages/tailwind-config/config.ts", + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/Cargo.toml b/Cargo.toml index 23eaeac..429c0db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["crates/quirks-path", "crates/recorder", "crates/torrent"] +members = ["apps/recorder", "packages/quirks-path", "packages/torrent"] resolver = "2" diff --git a/apps/api/.env.development b/apps/api/.env.development new file mode 100644 index 0000000..52a6723 --- /dev/null +++ b/apps/api/.env.development @@ -0,0 +1,15 @@ +# Server +BETTER_AUTH_SECRET="konobangu" +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:3000" +NEXT_PUBLIC_WEB_URL="http://localhost:3001" +NEXT_PUBLIC_DOCS_URL="http://localhost:3004" +NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="https://webui.konobangu.com" \ No newline at end of file diff --git a/apps/api/.env.example b/apps/api/.env.example new file mode 100644 index 0000000..ca7d1ee --- /dev/null +++ b/apps/api/.env.example @@ -0,0 +1,15 @@ +# Server +BETTER_AUTH_SECRET="" +DATABASE_URL="" +BETTERSTACK_API_KEY="" +BETTERSTACK_URL="" +FLAGS_SECRET="" +ARCJET_KEY="" +SVIX_TOKEN="" +LIVEBLOCKS_SECRET="" + +# Client +NEXT_PUBLIC_APP_URL="http://localhost:3000" +NEXT_PUBLIC_WEB_URL="http://localhost:3001" +NEXT_PUBLIC_DOCS_URL="http://localhost:3004" +NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3000" \ No newline at end of file diff --git a/apps/webui/.gitignore b/apps/api/.gitignore similarity index 81% rename from apps/webui/.gitignore rename to apps/api/.gitignore index fd3dbb5..663a9cc 100644 --- a/apps/webui/.gitignore +++ b/apps/api/.gitignore @@ -4,7 +4,6 @@ /node_modules /.pnp .pnp.js -.yarn/install-state.gz # testing /coverage @@ -24,6 +23,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +.pnpm-debug.log* # local env files .env*.local @@ -34,3 +34,12 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# prisma +.env + +# react.email +.react-email + +# Sentry +.sentryclirc \ No newline at end of file diff --git a/apps/api/app/apple-icon.png b/apps/api/app/apple-icon.png new file mode 100644 index 0000000..d185929 Binary files /dev/null and b/apps/api/app/apple-icon.png differ diff --git a/apps/api/app/cron/keep-alive/route.ts b/apps/api/app/cron/keep-alive/route.ts new file mode 100644 index 0000000..b34a27c --- /dev/null +++ b/apps/api/app/cron/keep-alive/route.ts @@ -0,0 +1,17 @@ +import { database } from '@konobangu/database'; + +export const POST = async () => { + const newPage = await database + .insertInto('page') + .values([ + { + name: 'cron-temp', + }, + ]) + .returning('id') + .executeTakeFirstOrThrow(); + + await database.deleteFrom('page').where('id', '=', newPage.id); + + return new Response('OK', { status: 200 }); +}; diff --git a/apps/api/app/global-error.tsx b/apps/api/app/global-error.tsx new file mode 100644 index 0000000..6e0511e --- /dev/null +++ b/apps/api/app/global-error.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { Button } from '@konobangu/design-system/components/ui/button'; +import { fonts } from '@konobangu/design-system/lib/fonts'; +import { captureException } from '@sentry/nextjs'; +import type NextError from 'next/error'; +import { useEffect } from 'react'; + +type GlobalErrorProperties = { + readonly error: NextError & { digest?: string }; + readonly reset: () => void; +}; + +const GlobalError = ({ error, reset }: GlobalErrorProperties) => { + useEffect(() => { + captureException(error); + }, [error]); + + return ( + + +

Oops, something went wrong

+ + + + ); +}; + +export default GlobalError; diff --git a/apps/api/app/health/route.ts b/apps/api/app/health/route.ts new file mode 100644 index 0000000..4ad1dd2 --- /dev/null +++ b/apps/api/app/health/route.ts @@ -0,0 +1,3 @@ +export const runtime = 'edge'; + +export const GET = (): Response => new Response('OK', { status: 200 }); diff --git a/apps/api/app/icon.png b/apps/api/app/icon.png new file mode 100644 index 0000000..d798285 Binary files /dev/null and b/apps/api/app/icon.png differ diff --git a/apps/api/app/layout.tsx b/apps/api/app/layout.tsx new file mode 100644 index 0000000..23be9db --- /dev/null +++ b/apps/api/app/layout.tsx @@ -0,0 +1,13 @@ +import type { ReactNode } from 'react'; + +type RootLayoutProperties = { + readonly children: ReactNode; +}; + +const RootLayout = ({ children }: RootLayoutProperties) => ( + + {children} + +); + +export default RootLayout; diff --git a/apps/api/app/opengraph-image.png b/apps/api/app/opengraph-image.png new file mode 100644 index 0000000..c79169f Binary files /dev/null and b/apps/api/app/opengraph-image.png differ diff --git a/apps/api/instrumentation.ts b/apps/api/instrumentation.ts new file mode 100644 index 0000000..99d8478 --- /dev/null +++ b/apps/api/instrumentation.ts @@ -0,0 +1,3 @@ +import { initializeSentry } from '@konobangu/next-config/instrumentation'; + +export const register = initializeSentry(); diff --git a/apps/api/next.config.ts b/apps/api/next.config.ts new file mode 100644 index 0000000..5ca2ed6 --- /dev/null +++ b/apps/api/next.config.ts @@ -0,0 +1,15 @@ +import { env } from '@konobangu/env'; +import { config, withAnalyzer, withSentry } from '@konobangu/next-config'; +import type { NextConfig } from 'next'; + +let nextConfig: NextConfig = { ...config }; + +if (env.VERCEL) { + nextConfig = withSentry(nextConfig); +} + +if (env.ANALYZE === 'true') { + nextConfig = withAnalyzer(nextConfig); +} + +export default nextConfig; diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..b2dbcb7 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,37 @@ +{ + "name": "api", + "private": true, + "scripts": { + "dev": "concurrently \"pnpm:next\"", + "next": "next dev -p 3002 --turbopack", + "build": "next build", + "start": "next start", + "analyze": "ANALYZE=true pnpm build", + "clean": "git clean -xdf .cache .turbo dist node_modules", + "typecheck": "tsc --noEmit --emitDeclarationOnly false" + }, + "dependencies": { + "@konobangu/analytics": "workspace:*", + "@konobangu/auth": "workspace:*", + "@konobangu/database": "workspace:*", + "@konobangu/design-system": "workspace:*", + "@konobangu/env": "workspace:*", + "@konobangu/next-config": "workspace:*", + "@konobangu/observability": "workspace:*", + "@sentry/nextjs": "^8.43.0", + "import-in-the-middle": "^1.11.3", + "next": "^15.1.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "require-in-the-middle": "^7.4.0", + "svix": "^1.43.0" + }, + "devDependencies": { + "@konobangu/typescript-config": "workspace:*", + "@types/node": "22.10.1", + "@types/react": "19.0.1", + "@types/react-dom": "19.0.2", + "concurrently": "^9.1.0", + "typescript": "^5.7.2" + } +} diff --git a/apps/api/sentry.client.config.ts b/apps/api/sentry.client.config.ts new file mode 100644 index 0000000..9f17f95 --- /dev/null +++ b/apps/api/sentry.client.config.ts @@ -0,0 +1,34 @@ +/* + * This file configures the initialization of Sentry on the client. + * The config you add here will be used whenever a users loads a page in their browser. + * https://docs.sentry.io/platforms/javascript/guides/nextjs/ + */ + +import { init, replayIntegration } from '@sentry/nextjs'; + +init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 1, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + + replaysOnErrorSampleRate: 1, + + /* + * This sets the sample rate to be 10%. You may want this to be 100% while + * in development and sample at a lower rate in production + */ + replaysSessionSampleRate: 0.1, + + // You can remove this option if you're not planning to use the Sentry Session Replay feature: + integrations: [ + replayIntegration({ + // Additional Replay configuration goes in here, for example: + maskAllText: true, + blockAllMedia: true, + }), + ], +}); diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..2f5c616 --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "@konobangu/typescript-config/nextjs.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./*"], + "@konobangu/*": ["../../packages/*"] + } + }, + "include": [ + "next-env.d.ts", + "next.config.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ] +} diff --git a/apps/api/vercel.json b/apps/api/vercel.json new file mode 100644 index 0000000..0632aa0 --- /dev/null +++ b/apps/api/vercel.json @@ -0,0 +1,8 @@ +{ + "crons": [ + { + "path": "/cron/keep-alive", + "schedule": "0 1 * * *" + } + ] +} diff --git a/apps/app/.env.development b/apps/app/.env.development new file mode 100644 index 0000000..52a6723 --- /dev/null +++ b/apps/app/.env.development @@ -0,0 +1,15 @@ +# Server +BETTER_AUTH_SECRET="konobangu" +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:3000" +NEXT_PUBLIC_WEB_URL="http://localhost:3001" +NEXT_PUBLIC_DOCS_URL="http://localhost:3004" +NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="https://webui.konobangu.com" \ No newline at end of file diff --git a/apps/app/.env.example b/apps/app/.env.example new file mode 100644 index 0000000..ca7d1ee --- /dev/null +++ b/apps/app/.env.example @@ -0,0 +1,15 @@ +# Server +BETTER_AUTH_SECRET="" +DATABASE_URL="" +BETTERSTACK_API_KEY="" +BETTERSTACK_URL="" +FLAGS_SECRET="" +ARCJET_KEY="" +SVIX_TOKEN="" +LIVEBLOCKS_SECRET="" + +# Client +NEXT_PUBLIC_APP_URL="http://localhost:3000" +NEXT_PUBLIC_WEB_URL="http://localhost:3001" +NEXT_PUBLIC_DOCS_URL="http://localhost:3004" +NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3000" \ No newline at end of file diff --git a/apps/app/.gitignore b/apps/app/.gitignore new file mode 100644 index 0000000..9045095 --- /dev/null +++ b/apps/app/.gitignore @@ -0,0 +1,45 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# prisma +.env + +# react.email +.react-email + +# Sentry +.sentryclirc diff --git a/apps/app/__tests__/sign-in.test.tsx b/apps/app/__tests__/sign-in.test.tsx new file mode 100644 index 0000000..5bd4c28 --- /dev/null +++ b/apps/app/__tests__/sign-in.test.tsx @@ -0,0 +1,13 @@ +import { render, screen } from '@testing-library/react'; +import { expect, test } from 'vitest'; +import Page from '../app/(unauthenticated)/sign-in/[[...sign-in]]/page'; + +test('Sign In Page', () => { + render(); + expect( + screen.getByRole('heading', { + level: 1, + name: 'Welcome back', + }) + ).toBeDefined(); +}); diff --git a/apps/app/__tests__/sign-up.test.tsx b/apps/app/__tests__/sign-up.test.tsx new file mode 100644 index 0000000..9a4dcbe --- /dev/null +++ b/apps/app/__tests__/sign-up.test.tsx @@ -0,0 +1,13 @@ +import { render, screen } from '@testing-library/react'; +import { expect, test } from 'vitest'; +import Page from '../app/(unauthenticated)/sign-up/[[...sign-up]]/page'; + +test('Sign Up Page', () => { + render(); + expect( + screen.getByRole('heading', { + level: 1, + name: 'Create an account', + }) + ).toBeDefined(); +}); diff --git a/apps/app/app/(authenticated)/components/avatar-stack.tsx b/apps/app/app/(authenticated)/components/avatar-stack.tsx new file mode 100644 index 0000000..1e926d5 --- /dev/null +++ b/apps/app/app/(authenticated)/components/avatar-stack.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { useOthers, useSelf } from '@konobangu/collaboration/hooks'; +import { + Avatar, + AvatarFallback, + AvatarImage, +} from '@konobangu/design-system/components/ui/avatar'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@konobangu/design-system/components/ui/tooltip'; +import { tailwind } from '@konobangu/tailwind-config'; + +type PresenceAvatarProps = { + info?: Liveblocks['UserMeta']['info']; +}; + +const PresenceAvatar = ({ info }: PresenceAvatarProps) => ( + + + + + + {info?.name?.slice(0, 2)} + + + + +

{info?.name ?? 'Unknown'}

+
+
+); + +export const AvatarStack = () => { + const others = useOthers(); + const self = useSelf(); + const hasMoreUsers = others.length > 3; + + return ( +
+ {others.slice(0, 3).map(({ connectionId, info }) => ( + + ))} + + {hasMoreUsers && ( + + )} + + {self && } +
+ ); +}; diff --git a/apps/app/app/(authenticated)/components/collaboration-provider.tsx b/apps/app/app/(authenticated)/components/collaboration-provider.tsx new file mode 100644 index 0000000..94bb373 --- /dev/null +++ b/apps/app/app/(authenticated)/components/collaboration-provider.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { getUsers } from '@/app/actions/users/get'; +import { searchUsers } from '@/app/actions/users/search'; +import { Room } from '@konobangu/collaboration/room'; +import type { ReactNode } from 'react'; + +export const CollaborationProvider = ({ + orgId, + children, +}: { + orgId: string; + children: ReactNode; +}) => { + const resolveUsers = async ({ userIds }: { userIds: string[] }) => { + const response = await getUsers(userIds); + + if ('error' in response) { + throw new Error('Problem resolving users'); + } + + return response.data; + }; + + const resolveMentionSuggestions = async ({ text }: { text: string }) => { + const response = await searchUsers(text); + + if ('error' in response) { + throw new Error('Problem resolving mention suggestions'); + } + + return response.data; + }; + + return ( + Loading... + } + resolveUsers={resolveUsers} + resolveMentionSuggestions={resolveMentionSuggestions} + > + {children} + + ); +}; diff --git a/apps/app/app/(authenticated)/components/cursors.tsx b/apps/app/app/(authenticated)/components/cursors.tsx new file mode 100644 index 0000000..d630598 --- /dev/null +++ b/apps/app/app/(authenticated)/components/cursors.tsx @@ -0,0 +1,106 @@ +'use client'; + +import { useMyPresence, useOthers } from '@konobangu/collaboration/hooks'; +import { useEffect } from 'react'; + +const Cursor = ({ + name, + color, + x, + y, +}: { + name: string | undefined; + color: string; + x: number; + y: number; +}) => ( +
+ + Cursor + + +
+ {name} +
+
+); + +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 ( + + ); + }); +}; diff --git a/apps/app/app/(authenticated)/components/header.tsx b/apps/app/app/(authenticated)/components/header.tsx new file mode 100644 index 0000000..138eb88 --- /dev/null +++ b/apps/app/app/(authenticated)/components/header.tsx @@ -0,0 +1,43 @@ +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from '@konobangu/design-system/components/ui/breadcrumb'; +import { Separator } from '@konobangu/design-system/components/ui/separator'; +import { SidebarTrigger } from '@konobangu/design-system/components/ui/sidebar'; +import { Fragment, type ReactNode } from 'react'; + +type HeaderProps = { + pages: string[]; + page: string; + children?: ReactNode; +}; + +export const Header = ({ pages, page, children }: HeaderProps) => ( +
+
+ + + + + {pages.map((page, index) => ( + + {index > 0 && } + + {page} + + + ))} + + + {page} + + + +
+ {children} +
+); diff --git a/apps/app/app/(authenticated)/components/posthog-identifier.tsx b/apps/app/app/(authenticated)/components/posthog-identifier.tsx new file mode 100644 index 0000000..bda1472 --- /dev/null +++ b/apps/app/app/(authenticated)/components/posthog-identifier.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { analytics } from '@konobangu/analytics/client'; +import { useSession } from '@konobangu/auth/client'; +import { usePathname, useSearchParams } from 'next/navigation'; +import { useEffect, useRef } from 'react'; + +export const PostHogIdentifier = () => { + const session = useSession(); + const user = session?.data?.user; + const identified = useRef(false); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + useEffect(() => { + // Track pageviews + if (pathname && analytics) { + let url = window.origin + pathname; + if (searchParams.toString()) { + url = `${url}?${searchParams.toString()}`; + } + analytics.capture('$pageview', { + $current_url: url, + }); + } + }, [pathname, searchParams]); + + useEffect(() => { + if (!user || identified.current) { + return; + } + + analytics.identify(user.id, { + email: user.email, + name: user.name, + createdAt: user.createdAt, + avatar: user.image, + }); + + identified.current = true; + }, [user]); + + return null; +}; diff --git a/apps/app/app/(authenticated)/components/sidebar.tsx b/apps/app/app/(authenticated)/components/sidebar.tsx new file mode 100644 index 0000000..5a5c103 --- /dev/null +++ b/apps/app/app/(authenticated)/components/sidebar.tsx @@ -0,0 +1,342 @@ +'use client'; + +// import { OrganizationSwitcher, UserButton } from '@konobangu/auth/client'; +import { ModeToggle } from '@konobangu/design-system/components/mode-toggle'; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@konobangu/design-system/components/ui/collapsible'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@konobangu/design-system/components/ui/dropdown-menu'; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarInset, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + useSidebar, +} from '@konobangu/design-system/components/ui/sidebar'; +import { cn } from '@konobangu/design-system/lib/utils'; +import { + AnchorIcon, + BookOpenIcon, + BotIcon, + ChevronRightIcon, + FolderIcon, + FrameIcon, + LifeBuoyIcon, + MapIcon, + MoreHorizontalIcon, + PieChartIcon, + SendIcon, + Settings2Icon, + ShareIcon, + SquareTerminalIcon, + Trash2Icon, +} from 'lucide-react'; +import type { ReactNode } from 'react'; + +type GlobalSidebarProperties = { + readonly children: ReactNode; +}; + +const data = { + user: { + name: 'shadcn', + email: 'm@example.com', + avatar: '/avatars/shadcn.jpg', + }, + navMain: [ + { + title: 'Playground', + url: '#', + icon: SquareTerminalIcon, + isActive: true, + items: [ + { + title: 'History', + url: '#', + }, + { + title: 'Starred', + url: '#', + }, + { + title: 'Settings', + url: '#', + }, + ], + }, + { + title: 'Models', + url: '#', + icon: BotIcon, + items: [ + { + title: 'Genesis', + url: '#', + }, + { + title: 'Explorer', + url: '#', + }, + { + title: 'Quantum', + url: '#', + }, + ], + }, + { + title: 'Documentation', + url: '#', + icon: BookOpenIcon, + items: [ + { + title: 'Introduction', + url: '#', + }, + { + title: 'Get Started', + url: '#', + }, + { + title: 'Tutorials', + url: '#', + }, + { + title: 'Changelog', + url: '#', + }, + ], + }, + { + title: 'Settings', + url: '#', + icon: Settings2Icon, + items: [ + { + title: 'General', + url: '#', + }, + { + title: 'Team', + url: '#', + }, + { + title: 'Billing', + url: '#', + }, + { + title: 'Limits', + url: '#', + }, + ], + }, + ], + navSecondary: [ + { + title: 'Webhooks', + url: '/webhooks', + icon: AnchorIcon, + }, + { + title: 'Support', + url: '#', + icon: LifeBuoyIcon, + }, + { + title: 'Feedback', + url: '#', + icon: SendIcon, + }, + ], + projects: [ + { + name: 'Design Engineering', + url: '#', + icon: FrameIcon, + }, + { + name: 'Sales & Marketing', + url: '#', + icon: PieChartIcon, + }, + { + name: 'Travel', + url: '#', + icon: MapIcon, + }, + ], +}; + +export const GlobalSidebar = ({ children }: GlobalSidebarProperties) => { + const sidebar = useSidebar(); + + return ( + <> + + + + +
div]:w-full', + sidebar.open ? '' : '-mx-1' + )} + > + {/* */} +
+
+
+
+ + + Platform + + {data.navMain.map((item) => ( + + + + + + {item.title} + + + {item.items?.length ? ( + <> + + + + Toggle + + + + + {item.items?.map((subItem) => ( + + + + {subItem.title} + + + + ))} + + + + ) : null} + + + ))} + + + + Projects + + {data.projects.map((item) => ( + + + + + {item.name} + + + + + + + More + + + + + + View Project + + + + Share Project + + + + + Delete Project + + + + + ))} + + + + More + + + + + + + + {data.navSecondary.map((item) => ( + + + + + {item.title} + + + + ))} + + + + + + + + {/* */} + + + + +
+ {children} + + ); +}; diff --git a/apps/app/app/(authenticated)/layout.tsx b/apps/app/app/(authenticated)/layout.tsx new file mode 100644 index 0000000..ccf64bd --- /dev/null +++ b/apps/app/app/(authenticated)/layout.tsx @@ -0,0 +1,42 @@ +import { getSessionFromHeaders } from '@konobangu/auth/server'; +import { SidebarProvider } from '@konobangu/design-system/components/ui/sidebar'; +import { env } from '@konobangu/env'; +import { showBetaFeature } from '@konobangu/feature-flags'; +import { secure } from '@konobangu/security'; +import { redirect } from 'next/navigation'; +import type { ReactNode } from 'react'; +import { PostHogIdentifier } from './components/posthog-identifier'; +import { GlobalSidebar } from './components/sidebar'; + +type AppLayoutProperties = { + readonly children: ReactNode; +}; + +const AppLayout = async ({ children }: AppLayoutProperties) => { + if (env.ARCJET_KEY) { + await secure(['CATEGORY:PREVIEW']); + } + + const { user } = await getSessionFromHeaders(); + + if (!user) { + return redirect('/sign-in'); // from next/navigation + } + const betaFeature = await showBetaFeature(); + + return ( + + + {betaFeature && ( +
+ Beta feature now available +
+ )} + {children} +
+ +
+ ); +}; + +export default AppLayout; diff --git a/apps/app/app/(authenticated)/page.tsx b/apps/app/app/(authenticated)/page.tsx new file mode 100644 index 0000000..ddaafd1 --- /dev/null +++ b/apps/app/app/(authenticated)/page.tsx @@ -0,0 +1,57 @@ +import { getSessionFromHeaders } from '@konobangu/auth/server'; +import { database } from '@konobangu/database'; +import { env } from '@konobangu/env'; +import type { Metadata } from 'next'; +import dynamic from 'next/dynamic'; +import { notFound } from 'next/navigation'; +import { AvatarStack } from './components/avatar-stack'; +import { Cursors } from './components/cursors'; +import { Header } from './components/header'; + +const title = 'Acme Inc'; +const description = 'My application.'; + +const CollaborationProvider = dynamic(() => + import('./components/collaboration-provider').then( + (mod) => mod.CollaborationProvider + ) +); + +export const metadata: Metadata = { + title, + description, +}; + +const App = async () => { + const pages = await database.selectFrom('page').selectAll().execute(); + const { orgId } = await getSessionFromHeaders(); + + if (!orgId) { + notFound(); + } + + return ( + <> +
+ {env.LIVEBLOCKS_SECRET && ( + + + + + )} +
+
+
+ {pages.map((page) => ( +
+ {page.name} +
+ ))} +
+
+
+ + ); +}; + +export default App; diff --git a/apps/app/app/(authenticated)/webhooks/page.tsx b/apps/app/app/(authenticated)/webhooks/page.tsx new file mode 100644 index 0000000..b0bae17 --- /dev/null +++ b/apps/app/app/(authenticated)/webhooks/page.tsx @@ -0,0 +1,29 @@ +import { webhooks } from '@konobangu/webhooks'; +import { notFound } from 'next/navigation'; + +export const metadata = { + title: 'Webhooks', + description: 'Send webhooks to your users.', +}; + +const WebhooksPage = async () => { + const response = await webhooks.getAppPortal(); + + if (!response?.url) { + notFound(); + } + + return ( +
+ + +
+ + + +Mintlify supports [HTML tags in Markdown](https://www.markdownguide.org/basic-syntax/#html). This is helpful if you prefer HTML tags to Markdown syntax, and lets you create documentation with infinite flexibility. + + + +### iFrames + +Loads another HTML page within the document. Most commonly used for embedding videos. + +```html + +``` diff --git a/apps/docs/essentials/markdown.mdx b/apps/docs/essentials/markdown.mdx new file mode 100644 index 0000000..c8ad9c1 --- /dev/null +++ b/apps/docs/essentials/markdown.mdx @@ -0,0 +1,88 @@ +--- +title: 'Markdown Syntax' +description: 'Text, title, and styling in standard markdown' +icon: 'text-size' +--- + +## Titles + +Best used for section headers. + +```md +## Titles +``` + +### Subtitles + +Best use to subsection headers. + +```md +### Subtitles +``` + + + +Each **title** and **subtitle** creates an anchor and also shows up on the table of contents on the right. + + + +## Text Formatting + +We support most markdown formatting. Simply add `**`, `_`, or `~` around text to format it. + +| Style | How to write it | Result | +| ------------- | ----------------- | --------------- | +| Bold | `**bold**` | **bold** | +| Italic | `_italic_` | _italic_ | +| Strikethrough | `~strikethrough~` | ~strikethrough~ | + +You can combine these. For example, write `**_bold and italic_**` to get **_bold and italic_** text. + +You need to use HTML to write superscript and subscript text. That is, add `` or `` around your text. + +| Text Size | How to write it | Result | +| ----------- | ------------------------ | ---------------------- | +| Superscript | `superscript` | superscript | +| Subscript | `subscript` | subscript | + +## Linking to Pages + +You can add a link by wrapping text in `[]()`. You would write `[link to google](https://google.com)` to [link to google](https://google.com). + +Links to pages in your docs need to be root-relative. Basically, you should include the entire folder path. For example, `[link to text](/writing-content/text)` links to the page "Text" in our components section. + +Relative links like `[link to text](../text)` will open slower because we cannot optimize them as easily. + +## Blockquotes + +### Singleline + +To create a blockquote, add a `>` in front of a paragraph. + +> Dorothy followed her through many of the beautiful rooms in her castle. + +```md +> Dorothy followed her through many of the beautiful rooms in her castle. +``` + +### Multiline + +> Dorothy followed her through many of the beautiful rooms in her castle. +> +> The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood. + +```md +> Dorothy followed her through many of the beautiful rooms in her castle. +> +> The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood. +``` + +### LaTeX + +Mintlify supports [LaTeX](https://www.latex-project.org) through the Latex component. + +8 x (vk x H1 - H2) = (0,1) + +```md +8 x (vk x H1 - H2) = (0,1) +``` diff --git a/apps/docs/essentials/navigation.mdx b/apps/docs/essentials/navigation.mdx new file mode 100644 index 0000000..ca44bb6 --- /dev/null +++ b/apps/docs/essentials/navigation.mdx @@ -0,0 +1,66 @@ +--- +title: 'Navigation' +description: 'The navigation field in mint.json defines the pages that go in the navigation menu' +icon: 'map' +--- + +The navigation menu is the list of links on every website. + +You will likely update `mint.json` every time you add a new page. Pages do not show up automatically. + +## Navigation syntax + +Our navigation syntax is recursive which means you can make nested navigation groups. You don't need to include `.mdx` in page names. + + + +```json Regular Navigation +"navigation": [ + { + "group": "Getting Started", + "pages": ["quickstart"] + } +] +``` + +```json Nested Navigation +"navigation": [ + { + "group": "Getting Started", + "pages": [ + "quickstart", + { + "group": "Nested Reference Pages", + "pages": ["nested-reference-page"] + } + ] + } +] +``` + + + +## Folders + +Simply put your MDX files in folders and update the paths in `mint.json`. + +For example, to have a page at `https://yoursite.com/your-folder/your-page` you would make a folder called `your-folder` containing an MDX file called `your-page.mdx`. + + + +You cannot use `api` for the name of a folder unless you nest it inside another folder. Mintlify uses Next.js which reserves the top-level `api` folder for internal server calls. A folder name such as `api-reference` would be accepted. + + + +```json Navigation With Folder +"navigation": [ + { + "group": "Group Name", + "pages": ["your-folder/your-page"] + } +] +``` + +## Hidden Pages + +MDX files not included in `mint.json` will not show up in the sidebar but are accessible through the search bar and by linking directly to them. diff --git a/apps/docs/essentials/reusable-snippets.mdx b/apps/docs/essentials/reusable-snippets.mdx new file mode 100644 index 0000000..a0a5529 --- /dev/null +++ b/apps/docs/essentials/reusable-snippets.mdx @@ -0,0 +1,110 @@ +--- +title: Reusable Snippets +description: Reusable, custom snippets to keep content in sync +icon: 'recycle' +--- + +import SnippetIntro from '/snippets/snippet-intro.mdx'; + + + +## Creating a custom snippet + +**Pre-condition**: You must create your snippet file in the `snippets` directory. + + + Any page in the `snippets` directory will be treated as a snippet and will not + be rendered into a standalone page. If you want to create a standalone page + from the snippet, import the snippet into another file and call it as a + component. + + +### Default export + +1. Add content to your snippet file that you want to re-use across multiple + locations. Optionally, you can add variables that can be filled in via props + when you import the snippet. + +```mdx snippets/my-snippet.mdx +Hello world! This is my content I want to reuse across pages. My keyword of the +day is {word}. +``` + + + The content that you want to reuse must be inside the `snippets` directory in + order for the import to work. + + +2. Import the snippet into your destination file. + +```mdx destination-file.mdx +--- +title: My title +description: My Description +--- + +import MySnippet from '/snippets/path/to/my-snippet.mdx'; + +## Header + +Lorem impsum dolor sit amet. + + +``` + +### Reusable variables + +1. Export a variable from your snippet file: + +```mdx snippets/path/to/custom-variables.mdx +export const myName = 'my name'; + +export const myObject = { fruit: 'strawberries' }; +``` + +2. Import the snippet from your destination file and use the variable: + +```mdx destination-file.mdx +--- +title: My title +description: My Description +--- + +import { myName, myObject } from '/snippets/path/to/custom-variables.mdx'; + +Hello, my name is {myName} and I like {myObject.fruit}. +``` + +### Reusable components + +1. Inside your snippet file, create a component that takes in props by exporting + your component in the form of an arrow function. + +```mdx snippets/custom-component.mdx +export const MyComponent = ({ title }) => ( +
+

{title}

+

... snippet content ...

+
+); +``` + + + MDX does not compile inside the body of an arrow function. Stick to HTML + syntax when you can or use a default export if you need to use MDX. + + +2. Import the snippet into your destination file and pass in the props + +```mdx destination-file.mdx +--- +title: My title +description: My Description +--- + +import { MyComponent } from '/snippets/custom-component.mdx'; + +Lorem ipsum dolor sit amet. + + +``` diff --git a/apps/docs/essentials/settings.mdx b/apps/docs/essentials/settings.mdx new file mode 100644 index 0000000..d9dd2d7 --- /dev/null +++ b/apps/docs/essentials/settings.mdx @@ -0,0 +1,318 @@ +--- +title: 'Global Settings' +description: 'Mintlify gives you complete control over the look and feel of your documentation using the mint.json file' +icon: 'gear' +--- + +Every Mintlify site needs a `mint.json` file with the core configuration settings. Learn more about the [properties](#properties) below. + +## Properties + + +Name of your project. Used for the global title. + +Example: `mintlify` + + + + + An array of groups with all the pages within that group + + + The name of the group. + + Example: `Settings` + + + + The relative paths to the markdown files that will serve as pages. + + Example: `["customization", "page"]` + + + + + + + + Path to logo image or object with path to "light" and "dark" mode logo images + + + Path to the logo in light mode + + + Path to the logo in dark mode + + + Where clicking on the logo links you to + + + + + + Path to the favicon image + + + + Hex color codes for your global theme + + + The primary color. Used for most often for highlighted content, section + headers, accents, in light mode + + + The primary color for dark mode. Used for most often for highlighted + content, section headers, accents, in dark mode + + + The primary color for important buttons + + + The color of the background in both light and dark mode + + + The hex color code of the background in light mode + + + The hex color code of the background in dark mode + + + + + + + + Array of `name`s and `url`s of links you want to include in the topbar + + + The name of the button. + + Example: `Contact us` + + + The url once you click on the button. Example: `https://mintlify.com/contact` + + + + + + + + + Link shows a button. GitHub shows the repo information at the url provided including the number of GitHub stars. + + + If `link`: What the button links to. + + If `github`: Link to the repository to load GitHub information from. + + + Text inside the button. Only required if `type` is a `link`. + + + + + + + Array of version names. Only use this if you want to show different versions + of docs with a dropdown in the navigation bar. + + + + An array of the anchors, includes the `icon`, `color`, and `url`. + + + The [Font Awesome](https://fontawesome.com/search?s=brands%2Cduotone) icon used to feature the anchor. + + Example: `comments` + + + The name of the anchor label. + + Example: `Community` + + + The start of the URL that marks what pages go in the anchor. Generally, this is the name of the folder you put your pages in. + + + The hex color of the anchor icon background. Can also be a gradient if you pass an object with the properties `from` and `to` that are each a hex color. + + + Used if you want to hide an anchor until the correct docs version is selected. + + + Pass `true` if you want to hide the anchor until you directly link someone to docs inside it. + + + One of: "brands", "duotone", "light", "sharp-solid", "solid", or "thin" + + + + + + + Override the default configurations for the top-most anchor. + + + The name of the top-most anchor + + + Font Awesome icon. + + + One of: "brands", "duotone", "light", "sharp-solid", "solid", or "thin" + + + + + + An array of navigational tabs. + + + The name of the tab label. + + + The start of the URL that marks what pages go in the tab. Generally, this + is the name of the folder you put your pages in. + + + + + + Configuration for API settings. Learn more about API pages at [API Components](/api-playground/demo). + + + The base url for all API endpoints. If `baseUrl` is an array, it will enable for multiple base url + options that the user can toggle. + + + + + + The authentication strategy used for all API endpoints. + + + The name of the authentication parameter used in the API playground. + + If method is `basic`, the format should be `[usernameName]:[passwordName]` + + + The default value that's designed to be a prefix for the authentication input field. + + E.g. If an `inputPrefix` of `AuthKey` would inherit the default input result of the authentication field as `AuthKey`. + + + + + + Configurations for the API playground + + + + Whether the playground is showing, hidden, or only displaying the endpoint with no added user interactivity `simple` + + Learn more at the [playground guides](/api-playground/demo) + + + + + + Enabling this flag ensures that key ordering in OpenAPI pages matches the key ordering defined in the OpenAPI file. + + This behavior will soon be enabled by default, at which point this field will be deprecated. + + + + + + + A string or an array of strings of URL(s) or relative path(s) pointing to your + OpenAPI file. + + Examples: + + ```json Absolute + "openapi": "https://example.com/openapi.json" + ``` + ```json Relative + "openapi": "/openapi.json" + ``` + ```json Multiple + "openapi": ["https://example.com/openapi1.json", "/openapi2.json", "/openapi3.json"] + ``` + + + + + + An object of social media accounts where the key:property pair represents the social media platform and the account url. + + Example: + ```json + { + "x": "https://x.com/mintlify", + "website": "https://mintlify.com" + } + ``` + + + One of the following values `website`, `facebook`, `x`, `discord`, `slack`, `github`, `linkedin`, `instagram`, `hacker-news` + + Example: `x` + + + The URL to the social platform. + + Example: `https://x.com/mintlify` + + + + + + Configurations to enable feedback buttons + + + + Enables a button to allow users to suggest edits via pull requests + + + Enables a button to allow users to raise an issue about the documentation + + + + + + Customize the dark mode toggle. + + + Set if you always want to show light or dark mode for new users. When not + set, we default to the same mode as the user's operating system. + + + Set to true to hide the dark/light mode toggle. You can combine `isHidden` with `default` to force your docs to only use light or dark mode. For example: + + + ```json Only Dark Mode + "modeToggle": { + "default": "dark", + "isHidden": true + } + ``` + + ```json Only Light Mode + "modeToggle": { + "default": "light", + "isHidden": true + } + ``` + + + + + + + + + A background image to be displayed behind every page. See example with + [Infisical](https://infisical.com/docs) and [FRPC](https://frpc.io). + diff --git a/apps/docs/favicon.svg b/apps/docs/favicon.svg new file mode 100644 index 0000000..6a32332 --- /dev/null +++ b/apps/docs/favicon.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/docs/images/checks-passed.png b/apps/docs/images/checks-passed.png new file mode 100644 index 0000000..3303c77 Binary files /dev/null and b/apps/docs/images/checks-passed.png differ diff --git a/apps/docs/images/hero-dark.svg b/apps/docs/images/hero-dark.svg new file mode 100644 index 0000000..c6a30e8 --- /dev/null +++ b/apps/docs/images/hero-dark.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/docs/images/hero-light.svg b/apps/docs/images/hero-light.svg new file mode 100644 index 0000000..297d68f --- /dev/null +++ b/apps/docs/images/hero-light.svg @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/docs/introduction.mdx b/apps/docs/introduction.mdx new file mode 100644 index 0000000..2589c0b --- /dev/null +++ b/apps/docs/introduction.mdx @@ -0,0 +1,71 @@ +--- +title: Introduction +description: 'Welcome to the home of your new documentation' +--- + +Hero Light +Hero Dark + +## Setting up + +The first step to world-class documentation is setting up your editing environments. + + + + Get your docs set up locally for easy development + + + Preview your changes before you push to make sure they're perfect + + + +## Make it yours + +Update your docs to your brand and add valuable content for the best user conversion. + + + + Customize your docs to your company's colors and brands + + + Automatically generate endpoints from an OpenAPI spec + + + Build interactive features and designs to guide your users + + + Check out our showcase of our favorite documentation + + diff --git a/apps/docs/logo/dark.svg b/apps/docs/logo/dark.svg new file mode 100644 index 0000000..a628378 --- /dev/null +++ b/apps/docs/logo/dark.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/docs/logo/light.svg b/apps/docs/logo/light.svg new file mode 100644 index 0000000..582b3b9 --- /dev/null +++ b/apps/docs/logo/light.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/docs/mint.json b/apps/docs/mint.json new file mode 100644 index 0000000..f32f763 --- /dev/null +++ b/apps/docs/mint.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://mintlify.com/schema.json", + "name": "Starter Kit", + "logo": { + "dark": "/logo/dark.svg", + "light": "/logo/light.svg" + }, + "favicon": "/favicon.svg", + "colors": { + "primary": "#0D9373", + "light": "#07C983", + "dark": "#0D9373", + "anchors": { + "from": "#0D9373", + "to": "#07C983" + } + }, + "topbarLinks": [ + { + "name": "Support", + "url": "mailto:support@mintlify.com" + } + ], + "topbarCtaButton": { + "name": "Dashboard", + "url": "https://dashboard.mintlify.com" + }, + "tabs": [ + { + "name": "API Reference", + "url": "api-reference" + } + ], + "anchors": [ + { + "name": "Documentation", + "icon": "book-open-cover", + "url": "https://mintlify.com/docs" + }, + { + "name": "Community", + "icon": "slack", + "url": "https://mintlify.com/community" + }, + { + "name": "Blog", + "icon": "newspaper", + "url": "https://mintlify.com/blog" + } + ], + "navigation": [ + { + "group": "Get Started", + "pages": ["introduction", "quickstart", "development"] + }, + { + "group": "Essentials", + "pages": [ + "essentials/markdown", + "essentials/code", + "essentials/images", + "essentials/settings", + "essentials/navigation", + "essentials/reusable-snippets" + ] + }, + { + "group": "API Documentation", + "pages": ["api-reference/introduction"] + }, + { + "group": "Endpoint Examples", + "pages": [ + "api-reference/endpoint/get", + "api-reference/endpoint/create", + "api-reference/endpoint/delete" + ] + } + ], + "footerSocials": { + "x": "https://x.com/mintlify", + "github": "https://github.com/mintlify", + "linkedin": "https://www.linkedin.com/company/mintlify" + } +} diff --git a/apps/docs/package.json b/apps/docs/package.json new file mode 100644 index 0000000..d8b11e6 --- /dev/null +++ b/apps/docs/package.json @@ -0,0 +1,11 @@ +{ + "name": "docs", + "private": true, + "scripts": { + "dev": "npx --yes mintlify dev --port 3004", + "lint": "npx --yes mintlify broken-links" + }, + "devDependencies": { + "typescript": "^5.7.2" + } +} diff --git a/apps/docs/quickstart.mdx b/apps/docs/quickstart.mdx new file mode 100644 index 0000000..d7f3486 --- /dev/null +++ b/apps/docs/quickstart.mdx @@ -0,0 +1,86 @@ +--- +title: 'Quickstart' +description: 'Start building awesome documentation in under 5 minutes' +--- + +## Setup your development + +Learn how to update your docs locally and and deploy them to the public. + +### Edit and preview + + + + During the onboarding process, we created a repository on your Github with + your docs content. You can find this repository on our + [dashboard](https://dashboard.mintlify.com). To clone the repository + locally, follow these + [instructions](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) + in your terminal. + + + Previewing helps you make sure your changes look as intended. We built a + command line interface to render these changes locally. 1. Install the + [Mintlify CLI](https://www.npmjs.com/package/mintlify) to preview the + documentation changes locally with this command: ``` npm i -g mintlify ``` + 2. Run the following command at the root of your documentation (where + `mint.json` is): ``` mintlify dev ``` + + + +### Deploy your changes + + + + + Our Github app automatically deploys your changes to your docs site, so you + don't need to manage deployments yourself. You can find the link to install on + your [dashboard](https://dashboard.mintlify.com). Once the bot has been + successfully installed, there should be a check mark next to the commit hash + of the repo. + + + [Commit and push your changes to + Git](https://docs.github.com/en/get-started/using-git/pushing-commits-to-a-remote-repository#about-git-push) + for your changes to update in your docs site. If you push and don't see that + the Github app successfully deployed your changes, you can also manually + update your docs through our [dashboard](https://dashboard.mintlify.com). + + + + +## Update your docs + +Add content directly in your files with MDX syntax and React components. You can use any of our components, or even build your own. + + + + + Add flair to your docs with personalized branding. + + + + Implement your OpenAPI spec and enable API user interaction. + + + + Draw insights from user interactions with your documentation. + + + + Keep your docs on your own website's subdomain. + + + diff --git a/apps/docs/snippets/snippet-intro.mdx b/apps/docs/snippets/snippet-intro.mdx new file mode 100644 index 0000000..c57e7c7 --- /dev/null +++ b/apps/docs/snippets/snippet-intro.mdx @@ -0,0 +1,4 @@ +One of the core principles of software development is DRY (Don't Repeat +Yourself). This is a principle that apply to documentation as +well. If you find yourself repeating the same content in multiple places, you +should consider creating a custom snippet to keep your content in sync. diff --git a/apps/email-playground/.gitignore b/apps/email-playground/.gitignore new file mode 100644 index 0000000..9e9e879 --- /dev/null +++ b/apps/email-playground/.gitignore @@ -0,0 +1 @@ +.react-email \ No newline at end of file diff --git a/apps/email-playground/emails/contact.tsx b/apps/email-playground/emails/contact.tsx new file mode 100644 index 0000000..2bc22d1 --- /dev/null +++ b/apps/email-playground/emails/contact.tsx @@ -0,0 +1,11 @@ +import { ContactTemplate } from '@konobangu/email/templates/contact'; + +const ExampleContactEmail = () => ( + +); + +export default ExampleContactEmail; diff --git a/apps/email-playground/package.json b/apps/email-playground/package.json new file mode 100644 index 0000000..7209526 --- /dev/null +++ b/apps/email-playground/package.json @@ -0,0 +1,24 @@ +{ + "name": "email-playground", + "version": "0.0.0", + "private": true, + "scripts": { + "build": "email build", + "dev": "email dev --port 3003", + "export": "email export", + "clean": "git clean -xdf .cache .turbo dist node_modules", + "typecheck": "tsc --noEmit --emitDeclarationOnly false" + }, + "dependencies": { + "@react-email/components": "0.0.31", + "@konobangu/email": "workspace:*", + "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.2" + } +} diff --git a/apps/email-playground/tsconfig.json b/apps/email-playground/tsconfig.json new file mode 100644 index 0000000..9d2f043 --- /dev/null +++ b/apps/email-playground/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@konobangu/typescript-config/nextjs.json", + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/apps/proxy/package.json b/apps/proxy/package.json index 7eba008..18bc1c5 100644 --- a/apps/proxy/package.json +++ b/apps/proxy/package.json @@ -3,11 +3,13 @@ "version": "0.1.0", "private": true, "scripts": { - "start": "whistle run -p 8899 -t 30000 -M \"keepXFF|prod|capture\" -D . --no-global-plugins" + "start": "cross-env WHISTLE_MODE=\"prod|capture|keepXFF\" whistle run -p 8899 -t 30000 -D . --no-global-plugins", + "dev": "pnpm run start" }, "keywords": [], "license": "MIT", "devDependencies": { - "whistle": "^2.9.61" + "whistle": "^2.9.61", + "cross-env": "^7.0.3" } } \ No newline at end of file diff --git a/crates/recorder/.devcontainer/Dockerfile b/apps/recorder/.devcontainer/Dockerfile similarity index 100% rename from crates/recorder/.devcontainer/Dockerfile rename to apps/recorder/.devcontainer/Dockerfile diff --git a/crates/recorder/.devcontainer/devcontainer.json b/apps/recorder/.devcontainer/devcontainer.json similarity index 100% rename from crates/recorder/.devcontainer/devcontainer.json rename to apps/recorder/.devcontainer/devcontainer.json diff --git a/crates/recorder/.devcontainer/docker-compose.yml b/apps/recorder/.devcontainer/docker-compose.yml similarity index 100% rename from crates/recorder/.devcontainer/docker-compose.yml rename to apps/recorder/.devcontainer/docker-compose.yml diff --git a/crates/recorder/.github/workflows/ci.yaml b/apps/recorder/.github/workflows/ci.yaml similarity index 100% rename from crates/recorder/.github/workflows/ci.yaml rename to apps/recorder/.github/workflows/ci.yaml diff --git a/crates/recorder/.gitignore b/apps/recorder/.gitignore similarity index 100% rename from crates/recorder/.gitignore rename to apps/recorder/.gitignore diff --git a/crates/recorder/Cargo.toml b/apps/recorder/Cargo.toml similarity index 93% rename from crates/recorder/Cargo.toml rename to apps/recorder/Cargo.toml index a6e70cf..0ce4b1d 100644 --- a/crates/recorder/Cargo.toml +++ b/apps/recorder/Cargo.toml @@ -14,6 +14,8 @@ path = "src/bin/main.rs" required-features = [] [dependencies] +quirks_path = { path = "../../packages/quirks-path" } +torrent = { path = "../../packages/torrent" } loco-rs = { version = "0.13" } serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -47,8 +49,6 @@ maplit = "1.0.2" lightningcss = "1.0.0-alpha.61" html-escape = "0.2.13" opendal = { version = "0.51.0", features = ["default", "services-fs"] } -quirks_path = { path = "../quirks-path" } -torrent = { path = "../torrent" } zune-image = "0.4.15" once_cell = "1.20.2" reqwest-middleware = "0.4.0" diff --git a/crates/recorder/examples/playground.rs b/apps/recorder/examples/playground.rs similarity index 100% rename from crates/recorder/examples/playground.rs rename to apps/recorder/examples/playground.rs diff --git a/crates/recorder/src/app.rs b/apps/recorder/src/app.rs similarity index 100% rename from crates/recorder/src/app.rs rename to apps/recorder/src/app.rs diff --git a/crates/recorder/src/bin/main.rs b/apps/recorder/src/bin/main.rs similarity index 100% rename from crates/recorder/src/bin/main.rs rename to apps/recorder/src/bin/main.rs diff --git a/crates/recorder/src/config/mod.rs b/apps/recorder/src/config/mod.rs similarity index 100% rename from crates/recorder/src/config/mod.rs rename to apps/recorder/src/config/mod.rs diff --git a/crates/recorder/src/controllers/mod.rs b/apps/recorder/src/controllers/mod.rs similarity index 100% rename from crates/recorder/src/controllers/mod.rs rename to apps/recorder/src/controllers/mod.rs diff --git a/crates/recorder/src/controllers/subscribers.rs b/apps/recorder/src/controllers/subscribers.rs similarity index 100% rename from crates/recorder/src/controllers/subscribers.rs rename to apps/recorder/src/controllers/subscribers.rs diff --git a/crates/recorder/src/dal/client.rs b/apps/recorder/src/dal/client.rs similarity index 100% rename from crates/recorder/src/dal/client.rs rename to apps/recorder/src/dal/client.rs diff --git a/crates/recorder/src/dal/config.rs b/apps/recorder/src/dal/config.rs similarity index 100% rename from crates/recorder/src/dal/config.rs rename to apps/recorder/src/dal/config.rs diff --git a/crates/recorder/src/dal/mod.rs b/apps/recorder/src/dal/mod.rs similarity index 100% rename from crates/recorder/src/dal/mod.rs rename to apps/recorder/src/dal/mod.rs diff --git a/crates/recorder/src/extract/defs.rs b/apps/recorder/src/extract/defs.rs similarity index 100% rename from crates/recorder/src/extract/defs.rs rename to apps/recorder/src/extract/defs.rs diff --git a/crates/recorder/src/extract/errors.rs b/apps/recorder/src/extract/errors.rs similarity index 100% rename from crates/recorder/src/extract/errors.rs rename to apps/recorder/src/extract/errors.rs diff --git a/crates/recorder/src/extract/html/mod.rs b/apps/recorder/src/extract/html/mod.rs similarity index 100% rename from crates/recorder/src/extract/html/mod.rs rename to apps/recorder/src/extract/html/mod.rs diff --git a/crates/recorder/src/extract/html/styles.rs b/apps/recorder/src/extract/html/styles.rs similarity index 100% rename from crates/recorder/src/extract/html/styles.rs rename to apps/recorder/src/extract/html/styles.rs diff --git a/crates/recorder/src/extract/mikan/client.rs b/apps/recorder/src/extract/mikan/client.rs similarity index 100% rename from crates/recorder/src/extract/mikan/client.rs rename to apps/recorder/src/extract/mikan/client.rs diff --git a/crates/recorder/src/extract/mikan/config.rs b/apps/recorder/src/extract/mikan/config.rs similarity index 100% rename from crates/recorder/src/extract/mikan/config.rs rename to apps/recorder/src/extract/mikan/config.rs diff --git a/crates/recorder/src/extract/mikan/constants.rs b/apps/recorder/src/extract/mikan/constants.rs similarity index 100% rename from crates/recorder/src/extract/mikan/constants.rs rename to apps/recorder/src/extract/mikan/constants.rs diff --git a/crates/recorder/src/extract/mikan/mod.rs b/apps/recorder/src/extract/mikan/mod.rs similarity index 100% rename from crates/recorder/src/extract/mikan/mod.rs rename to apps/recorder/src/extract/mikan/mod.rs diff --git a/crates/recorder/src/extract/mikan/rss_parser.rs b/apps/recorder/src/extract/mikan/rss_parser.rs similarity index 100% rename from crates/recorder/src/extract/mikan/rss_parser.rs rename to apps/recorder/src/extract/mikan/rss_parser.rs diff --git a/crates/recorder/src/extract/mikan/web_parser.rs b/apps/recorder/src/extract/mikan/web_parser.rs similarity index 100% rename from crates/recorder/src/extract/mikan/web_parser.rs rename to apps/recorder/src/extract/mikan/web_parser.rs diff --git a/crates/recorder/src/extract/mod.rs b/apps/recorder/src/extract/mod.rs similarity index 100% rename from crates/recorder/src/extract/mod.rs rename to apps/recorder/src/extract/mod.rs diff --git a/crates/recorder/src/extract/rawname/mod.rs b/apps/recorder/src/extract/rawname/mod.rs similarity index 100% rename from crates/recorder/src/extract/rawname/mod.rs rename to apps/recorder/src/extract/rawname/mod.rs diff --git a/crates/recorder/src/extract/rawname/parser.rs b/apps/recorder/src/extract/rawname/parser.rs similarity index 99% rename from crates/recorder/src/extract/rawname/parser.rs rename to apps/recorder/src/extract/rawname/parser.rs index 1fa8d26..ef597d0 100644 --- a/crates/recorder/src/extract/rawname/parser.rs +++ b/apps/recorder/src/extract/rawname/parser.rs @@ -282,7 +282,7 @@ pub fn parse_episode_meta_from_raw_name(s: &str) -> eyre::Result if movie_capture { title_body += title_episode; title_episode = ""; - } else if EP_COLLECTION_RE.is_match(&title_episode) { + } else if EP_COLLECTION_RE.is_match(title_episode) { title_episode = ""; } diff --git a/crates/recorder/src/extract/torrent/mod.rs b/apps/recorder/src/extract/torrent/mod.rs similarity index 100% rename from crates/recorder/src/extract/torrent/mod.rs rename to apps/recorder/src/extract/torrent/mod.rs diff --git a/crates/recorder/src/extract/torrent/parser.rs b/apps/recorder/src/extract/torrent/parser.rs similarity index 100% rename from crates/recorder/src/extract/torrent/parser.rs rename to apps/recorder/src/extract/torrent/parser.rs diff --git a/crates/recorder/src/fetch/bytes.rs b/apps/recorder/src/fetch/bytes.rs similarity index 100% rename from crates/recorder/src/fetch/bytes.rs rename to apps/recorder/src/fetch/bytes.rs diff --git a/crates/recorder/src/fetch/client.rs b/apps/recorder/src/fetch/client.rs similarity index 100% rename from crates/recorder/src/fetch/client.rs rename to apps/recorder/src/fetch/client.rs diff --git a/crates/recorder/src/fetch/core.rs b/apps/recorder/src/fetch/core.rs similarity index 100% rename from crates/recorder/src/fetch/core.rs rename to apps/recorder/src/fetch/core.rs diff --git a/crates/recorder/src/fetch/html.rs b/apps/recorder/src/fetch/html.rs similarity index 100% rename from crates/recorder/src/fetch/html.rs rename to apps/recorder/src/fetch/html.rs diff --git a/crates/recorder/src/fetch/image.rs b/apps/recorder/src/fetch/image.rs similarity index 100% rename from crates/recorder/src/fetch/image.rs rename to apps/recorder/src/fetch/image.rs diff --git a/crates/recorder/src/fetch/mod.rs b/apps/recorder/src/fetch/mod.rs similarity index 100% rename from crates/recorder/src/fetch/mod.rs rename to apps/recorder/src/fetch/mod.rs diff --git a/crates/recorder/src/lib.rs b/apps/recorder/src/lib.rs similarity index 100% rename from crates/recorder/src/lib.rs rename to apps/recorder/src/lib.rs diff --git a/crates/recorder/src/migrations/defs.rs b/apps/recorder/src/migrations/defs.rs similarity index 100% rename from crates/recorder/src/migrations/defs.rs rename to apps/recorder/src/migrations/defs.rs diff --git a/crates/recorder/src/migrations/m20220101_000001_init.rs b/apps/recorder/src/migrations/m20220101_000001_init.rs similarity index 100% rename from crates/recorder/src/migrations/m20220101_000001_init.rs rename to apps/recorder/src/migrations/m20220101_000001_init.rs diff --git a/crates/recorder/src/migrations/m20240224_082543_add_downloads.rs b/apps/recorder/src/migrations/m20240224_082543_add_downloads.rs similarity index 100% rename from crates/recorder/src/migrations/m20240224_082543_add_downloads.rs rename to apps/recorder/src/migrations/m20240224_082543_add_downloads.rs diff --git a/crates/recorder/src/migrations/m20240225_060853_subscriber_add_downloader.rs b/apps/recorder/src/migrations/m20240225_060853_subscriber_add_downloader.rs similarity index 100% rename from crates/recorder/src/migrations/m20240225_060853_subscriber_add_downloader.rs rename to apps/recorder/src/migrations/m20240225_060853_subscriber_add_downloader.rs diff --git a/crates/recorder/src/migrations/mod.rs b/apps/recorder/src/migrations/mod.rs similarity index 100% rename from crates/recorder/src/migrations/mod.rs rename to apps/recorder/src/migrations/mod.rs diff --git a/crates/recorder/src/models/bangumi.rs b/apps/recorder/src/models/bangumi.rs similarity index 100% rename from crates/recorder/src/models/bangumi.rs rename to apps/recorder/src/models/bangumi.rs diff --git a/crates/recorder/src/models/downloaders.rs b/apps/recorder/src/models/downloaders.rs similarity index 100% rename from crates/recorder/src/models/downloaders.rs rename to apps/recorder/src/models/downloaders.rs diff --git a/crates/recorder/src/models/downloads.rs b/apps/recorder/src/models/downloads.rs similarity index 100% rename from crates/recorder/src/models/downloads.rs rename to apps/recorder/src/models/downloads.rs diff --git a/crates/recorder/src/models/entities/bangumi.rs b/apps/recorder/src/models/entities/bangumi.rs similarity index 100% rename from crates/recorder/src/models/entities/bangumi.rs rename to apps/recorder/src/models/entities/bangumi.rs diff --git a/crates/recorder/src/models/entities/downloaders.rs b/apps/recorder/src/models/entities/downloaders.rs similarity index 100% rename from crates/recorder/src/models/entities/downloaders.rs rename to apps/recorder/src/models/entities/downloaders.rs diff --git a/crates/recorder/src/models/entities/downloads.rs b/apps/recorder/src/models/entities/downloads.rs similarity index 100% rename from crates/recorder/src/models/entities/downloads.rs rename to apps/recorder/src/models/entities/downloads.rs diff --git a/crates/recorder/src/models/entities/episodes.rs b/apps/recorder/src/models/entities/episodes.rs similarity index 100% rename from crates/recorder/src/models/entities/episodes.rs rename to apps/recorder/src/models/entities/episodes.rs diff --git a/crates/recorder/src/models/entities/mod.rs b/apps/recorder/src/models/entities/mod.rs similarity index 100% rename from crates/recorder/src/models/entities/mod.rs rename to apps/recorder/src/models/entities/mod.rs diff --git a/crates/recorder/src/models/entities/subscribers.rs b/apps/recorder/src/models/entities/subscribers.rs similarity index 100% rename from crates/recorder/src/models/entities/subscribers.rs rename to apps/recorder/src/models/entities/subscribers.rs diff --git a/crates/recorder/src/models/entities/subscriptions.rs b/apps/recorder/src/models/entities/subscriptions.rs similarity index 100% rename from crates/recorder/src/models/entities/subscriptions.rs rename to apps/recorder/src/models/entities/subscriptions.rs diff --git a/crates/recorder/src/models/episodes.rs b/apps/recorder/src/models/episodes.rs similarity index 100% rename from crates/recorder/src/models/episodes.rs rename to apps/recorder/src/models/episodes.rs diff --git a/crates/recorder/src/models/mod.rs b/apps/recorder/src/models/mod.rs similarity index 100% rename from crates/recorder/src/models/mod.rs rename to apps/recorder/src/models/mod.rs diff --git a/crates/recorder/src/models/notifications.rs b/apps/recorder/src/models/notifications.rs similarity index 100% rename from crates/recorder/src/models/notifications.rs rename to apps/recorder/src/models/notifications.rs diff --git a/crates/recorder/src/models/prelude.rs b/apps/recorder/src/models/prelude.rs similarity index 100% rename from crates/recorder/src/models/prelude.rs rename to apps/recorder/src/models/prelude.rs diff --git a/crates/recorder/src/models/query/mod.rs b/apps/recorder/src/models/query/mod.rs similarity index 100% rename from crates/recorder/src/models/query/mod.rs rename to apps/recorder/src/models/query/mod.rs diff --git a/crates/recorder/src/models/subscribers.rs b/apps/recorder/src/models/subscribers.rs similarity index 100% rename from crates/recorder/src/models/subscribers.rs rename to apps/recorder/src/models/subscribers.rs diff --git a/crates/recorder/src/models/subscriptions.rs b/apps/recorder/src/models/subscriptions.rs similarity index 93% rename from crates/recorder/src/models/subscriptions.rs rename to apps/recorder/src/models/subscriptions.rs index f05672d..490d6ad 100644 --- a/crates/recorder/src/models/subscriptions.rs +++ b/apps/recorder/src/models/subscriptions.rs @@ -196,17 +196,17 @@ impl Model { if let MikanBangumiPosterMeta { poster_src: Some(poster_src), .. - } = parse_mikan_bangumi_poster_from_origin_poster_src_with_cache( - ctx, - origin_poster_src, - self.subscriber_id, - ) - .await? - { - am.poster_link = ActiveValue::Set(Some(poster_src)) + } = parse_mikan_bangumi_poster_from_origin_poster_src_with_cache( + ctx, + origin_poster_src, + self.subscriber_id, + ) + .await? + { + am.poster_link = ActiveValue::Set(Some(poster_src)) + } } - } - Ok(()) + Ok(()) }, ) .await?, diff --git a/crates/recorder/src/tasks/mod.rs b/apps/recorder/src/tasks/mod.rs similarity index 100% rename from crates/recorder/src/tasks/mod.rs rename to apps/recorder/src/tasks/mod.rs diff --git a/crates/recorder/src/views/mod.rs b/apps/recorder/src/views/mod.rs similarity index 100% rename from crates/recorder/src/views/mod.rs rename to apps/recorder/src/views/mod.rs diff --git a/crates/recorder/src/views/subscribers.rs b/apps/recorder/src/views/subscribers.rs similarity index 100% rename from crates/recorder/src/views/subscribers.rs rename to apps/recorder/src/views/subscribers.rs diff --git a/crates/recorder/src/workers/mod.rs b/apps/recorder/src/workers/mod.rs similarity index 100% rename from crates/recorder/src/workers/mod.rs rename to apps/recorder/src/workers/mod.rs diff --git a/crates/recorder/src/workers/subscription_worker.rs b/apps/recorder/src/workers/subscription_worker.rs similarity index 100% rename from crates/recorder/src/workers/subscription_worker.rs rename to apps/recorder/src/workers/subscription_worker.rs diff --git a/crates/recorder/tests/mod.rs b/apps/recorder/tests/mod.rs similarity index 100% rename from crates/recorder/tests/mod.rs rename to apps/recorder/tests/mod.rs diff --git a/crates/recorder/tests/models/mod.rs b/apps/recorder/tests/models/mod.rs similarity index 100% rename from crates/recorder/tests/models/mod.rs rename to apps/recorder/tests/models/mod.rs diff --git a/crates/recorder/tests/models/snapshots/can_find_by_pid@subscribers-2.snap b/apps/recorder/tests/models/snapshots/can_find_by_pid@subscribers-2.snap similarity index 100% rename from crates/recorder/tests/models/snapshots/can_find_by_pid@subscribers-2.snap rename to apps/recorder/tests/models/snapshots/can_find_by_pid@subscribers-2.snap diff --git a/crates/recorder/tests/models/snapshots/can_find_by_pid@subscribers.snap b/apps/recorder/tests/models/snapshots/can_find_by_pid@subscribers.snap similarity index 100% rename from crates/recorder/tests/models/snapshots/can_find_by_pid@subscribers.snap rename to apps/recorder/tests/models/snapshots/can_find_by_pid@subscribers.snap diff --git a/crates/recorder/tests/models/subscribers.rs b/apps/recorder/tests/models/subscribers.rs similarity index 100% rename from crates/recorder/tests/models/subscribers.rs rename to apps/recorder/tests/models/subscribers.rs diff --git a/crates/recorder/tests/requests/mod.rs b/apps/recorder/tests/requests/mod.rs similarity index 100% rename from crates/recorder/tests/requests/mod.rs rename to apps/recorder/tests/requests/mod.rs diff --git a/crates/recorder/tests/requests/subscribers.rs b/apps/recorder/tests/requests/subscribers.rs similarity index 58% rename from crates/recorder/tests/requests/subscribers.rs rename to apps/recorder/tests/requests/subscribers.rs index 5d06180..2ad05b2 100644 --- a/crates/recorder/tests/requests/subscribers.rs +++ b/apps/recorder/tests/requests/subscribers.rs @@ -1,3 +1,4 @@ +#![allow(unused_imports)] use insta::{assert_debug_snapshot, with_settings}; use loco_rs::testing; use recorder::app::App; @@ -5,14 +6,14 @@ use serial_test::serial; // TODO: see how to dedup / extract this to app-local test utils // not to framework, because that would require a runtime dep on insta -macro_rules! configure_insta { - ($($expr:expr),*) => { - let mut settings = insta::Settings::clone_current(); - settings.set_prepend_module_to_snapshot(false); - settings.set_snapshot_suffix("user_request"); - let _guard = settings.bind_to_scope(); - }; -} +// macro_rules! configure_insta { +// ($($expr:expr),*) => { +// let mut settings = insta::Settings::clone_current(); +// settings.set_prepend_module_to_snapshot(false); +// settings.set_snapshot_suffix("user_request"); +// let _guard = settings.bind_to_scope(); +// }; +// } #[tokio::test] #[serial] @@ -25,8 +26,8 @@ async fn can_get_current_user() { // with_settings!({ // filters => testing::cleanup_user_model() // }, { - // assert_debug_snapshot!((response.status_code(), response.text())); - // }); + // assert_debug_snapshot!((response.status_code(), + // response.text())); }); // }) // .await; } diff --git a/crates/recorder/tests/tasks/mod.rs b/apps/recorder/tests/tasks/mod.rs similarity index 100% rename from crates/recorder/tests/tasks/mod.rs rename to apps/recorder/tests/tasks/mod.rs diff --git a/crates/recorder/tests/tasks/seed.rs b/apps/recorder/tests/tasks/seed.rs similarity index 87% rename from crates/recorder/tests/tasks/seed.rs rename to apps/recorder/tests/tasks/seed.rs index c7d95ea..01f7f52 100644 --- a/crates/recorder/tests/tasks/seed.rs +++ b/apps/recorder/tests/tasks/seed.rs @@ -13,6 +13,7 @@ //! ```sh //! cargo run task seed_data refresh:true //! ``` +#![allow(unused_imports)] use loco_rs::{db, prelude::*}; use recorder::{app::App, migrations::Migrator}; @@ -27,8 +28,9 @@ impl Task for SeedData { } } - async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> { - // let refresh = vars.cli.get("refresh").is_some_and(|refresh| refresh == "true"); + async fn run(&self, _app_context: &AppContext, _vars: &task::Vars) -> Result<()> { + // let refresh = vars.cli.get("refresh").is_some_and(|refresh| refresh == + // "true"); // // if refresh { // db::reset::(&app_context.db).await?; diff --git a/apps/storybook/.gitignore b/apps/storybook/.gitignore new file mode 100644 index 0000000..d85f7c1 --- /dev/null +++ b/apps/storybook/.gitignore @@ -0,0 +1,45 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# env files (can opt-in for commiting if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +*storybook.log + +# storybook +storybook-static/ \ No newline at end of file diff --git a/apps/storybook/.storybook/main.ts b/apps/storybook/.storybook/main.ts new file mode 100644 index 0000000..0592005 --- /dev/null +++ b/apps/storybook/.storybook/main.ts @@ -0,0 +1,30 @@ +import { dirname, join } from 'node:path'; +import type { StorybookConfig } from '@storybook/nextjs'; + +/** + * This function is used to resolve the absolute path of a package. + * It is needed in projects that use Yarn PnP or are set up within a monorepo. + */ +const getAbsolutePath = (value: string) => + dirname(require.resolve(join(value, 'package.json'))); + +const config: StorybookConfig = { + stories: [ + '../stories/**/*.mdx', + '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)', + ], + addons: [ + getAbsolutePath('@storybook/addon-onboarding'), + getAbsolutePath('@storybook/addon-essentials'), + getAbsolutePath('@chromatic-com/storybook'), + getAbsolutePath('@storybook/addon-interactions'), + getAbsolutePath('@storybook/addon-themes'), + ], + framework: { + name: getAbsolutePath('@storybook/nextjs'), + options: {}, + }, + staticDirs: ['../public'], +}; + +export default config; diff --git a/apps/storybook/.storybook/preview-head.html b/apps/storybook/.storybook/preview-head.html new file mode 100644 index 0000000..5873c0b --- /dev/null +++ b/apps/storybook/.storybook/preview-head.html @@ -0,0 +1,17 @@ + + + + + + + diff --git a/apps/storybook/.storybook/preview.tsx b/apps/storybook/.storybook/preview.tsx new file mode 100644 index 0000000..7c2c1fd --- /dev/null +++ b/apps/storybook/.storybook/preview.tsx @@ -0,0 +1,53 @@ +import { Toaster } from '@konobangu/design-system/components/ui/sonner'; +import { TooltipProvider } from '@konobangu/design-system/components/ui/tooltip'; +import { ThemeProvider } from '@konobangu/design-system/providers/theme'; +import { withThemeByClassName } from '@storybook/addon-themes'; +import type { Preview } from '@storybook/react'; + +import '@konobangu/design-system/styles/globals.css'; + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + chromatic: { + modes: { + light: { + theme: 'light', + className: 'light', + }, + dark: { + theme: 'dark', + className: 'dark', + }, + }, + }, + }, + decorators: [ + withThemeByClassName({ + themes: { + light: 'light', + dark: 'dark', + }, + defaultTheme: 'light', + }), + (Story) => { + return ( +
+ + + + + + +
+ ); + }, + ], +}; + +export default preview; diff --git a/apps/storybook/README.md b/apps/storybook/README.md new file mode 100644 index 0000000..ef0e47e --- /dev/null +++ b/apps/storybook/README.md @@ -0,0 +1,40 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) 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:3000/api/hello](http://localhost:3000/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. diff --git a/apps/storybook/next.config.ts b/apps/storybook/next.config.ts new file mode 100644 index 0000000..b08f02b --- /dev/null +++ b/apps/storybook/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + reactStrictMode: true, +}; + +export default nextConfig; diff --git a/apps/storybook/package.json b/apps/storybook/package.json new file mode 100644 index 0000000..1cb1e32 --- /dev/null +++ b/apps/storybook/package.json @@ -0,0 +1,39 @@ +{ + "name": "storybook", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "storybook dev -p 6006", + "build-storybook": "storybook build", + "chromatic": "chromatic --exit-zero-on-changes", + "clean": "git clean -xdf .cache .turbo dist node_modules", + "typecheck": "tsc --noEmit --emitDeclarationOnly false" + }, + "dependencies": { + "@konobangu/design-system": "workspace:*", + "lucide-react": "^0.468.0", + "next": "^15.1.3", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@chromatic-com/storybook": "^3.2.2", + "@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", + "@types/react": "^19", + "@types/react-dom": "^19", + "chromatic": "^11.20.1", + "postcss": "^8", + "storybook": "^8.4.7", + "tailwindcss": "^3.4.16", + "typescript": "^5" + } +} diff --git a/apps/storybook/postcss.config.mjs b/apps/storybook/postcss.config.mjs new file mode 100644 index 0000000..1a69fd2 --- /dev/null +++ b/apps/storybook/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/apps/webui/app/favicon.ico b/apps/storybook/public/favicon.ico similarity index 100% rename from apps/webui/app/favicon.ico rename to apps/storybook/public/favicon.ico diff --git a/apps/storybook/stories/accordion.stories.tsx b/apps/storybook/stories/accordion.stories.tsx new file mode 100644 index 0000000..c08c137 --- /dev/null +++ b/apps/storybook/stories/accordion.stories.tsx @@ -0,0 +1,60 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@konobangu/design-system/components/ui/accordion'; + +/** + * A vertically stacked set of interactive headings that each reveal a section + * of content. + */ +const meta = { + title: 'ui/Accordion', + component: Accordion, + tags: ['autodocs'], + argTypes: { + type: { + options: ['single', 'multiple'], + control: { type: 'radio' }, + }, + }, + args: { + type: 'single', + collapsible: true, + }, + render: (args) => ( + + + Is it accessible? + + Yes. It adheres to the WAI-ARIA design pattern. + + + + Is it styled? + + Yes. It comes with default styles that matches the other components' + aesthetic. + + + + Is it animated? + + Yes. It's animated by default, but you can disable it if you prefer. + + + + ), +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default behavior of the accordion allows only one item to be open. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/alert-dialog.stories.tsx b/apps/storybook/stories/alert-dialog.stories.tsx new file mode 100644 index 0000000..1df1f2d --- /dev/null +++ b/apps/storybook/stories/alert-dialog.stories.tsx @@ -0,0 +1,54 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@konobangu/design-system/components/ui/alert-dialog'; + +/** + * A modal dialog that interrupts the user with important content and expects + * a response. + */ +const meta = { + title: 'ui/AlertDialog', + component: AlertDialog, + tags: ['autodocs'], + argTypes: {}, + render: (args) => ( + + Open + + + Are you sure absolutely sure? + + This action cannot be undone. This will permanently delete your + account and remove your data from our servers. + + + + Cancel + Continue + + + + ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the alert dialog. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/alert.stories.tsx b/apps/storybook/stories/alert.stories.tsx new file mode 100644 index 0000000..6cede53 --- /dev/null +++ b/apps/storybook/stories/alert.stories.tsx @@ -0,0 +1,60 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { AlertCircle } from 'lucide-react'; + +import { + Alert, + AlertDescription, + AlertTitle, +} from '@konobangu/design-system/components/ui/alert'; + +/** + * Displays a callout for user attention. + */ +const meta = { + title: 'ui/Alert', + component: Alert, + tags: ['autodocs'], + argTypes: { + variant: { + options: ['default', 'destructive'], + control: { type: 'radio' }, + }, + }, + args: { + variant: 'default', + }, + render: (args) => ( + + Heads up! + + You can add components to your app using the cli. + + + ), +} satisfies Meta; + +export default meta; + +type Story = StoryObj; +/** + * 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) => ( + + + Error + + Your session has expired. Please log in again. + + + ), + args: { + variant: 'destructive', + }, +}; diff --git a/apps/storybook/stories/aspect-ratio.stories.tsx b/apps/storybook/stories/aspect-ratio.stories.tsx new file mode 100644 index 0000000..b99d442 --- /dev/null +++ b/apps/storybook/stories/aspect-ratio.stories.tsx @@ -0,0 +1,71 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Image from 'next/image'; + +import { AspectRatio } from '@konobangu/design-system/components/ui/aspect-ratio'; + +/** + * Displays content within a desired ratio. + */ +const meta: Meta = { + title: 'ui/AspectRatio', + component: AspectRatio, + tags: ['autodocs'], + argTypes: {}, + render: (args) => ( + + Photo by Alvaro Pinot + + ), + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * 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, + }, +}; diff --git a/apps/storybook/stories/avatar.stories.tsx b/apps/storybook/stories/avatar.stories.tsx new file mode 100644 index 0000000..45100c5 --- /dev/null +++ b/apps/storybook/stories/avatar.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + Avatar, + AvatarFallback, + AvatarImage, +} from '@konobangu/design-system/components/ui/avatar'; + +/** + * An image element with a fallback for representing the user. + */ +const meta = { + title: 'ui/Avatar', + component: Avatar, + tags: ['autodocs'], + argTypes: {}, + render: (args) => ( + + + CN + + ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the avatar. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/badge.stories.tsx b/apps/storybook/stories/badge.stories.tsx new file mode 100644 index 0000000..4a72991 --- /dev/null +++ b/apps/storybook/stories/badge.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Badge } from '@konobangu/design-system/components/ui/badge'; + +/** + * Displays a badge or a component that looks like a badge. + */ +const meta = { + title: 'ui/Badge', + component: Badge, + tags: ['autodocs'], + argTypes: { + children: { + control: 'text', + }, + }, + args: { + children: 'Badge', + }, + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * 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', + }, +}; diff --git a/apps/storybook/stories/breadcrumb.stories.tsx b/apps/storybook/stories/breadcrumb.stories.tsx new file mode 100644 index 0000000..bb4ca82 --- /dev/null +++ b/apps/storybook/stories/breadcrumb.stories.tsx @@ -0,0 +1,78 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { ArrowRightSquare } from 'lucide-react'; + +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from '@konobangu/design-system/components/ui/breadcrumb'; + +/** + * Displays the path to the current resource using a hierarchy of links. + */ +const meta = { + title: 'ui/Breadcrumb', + component: Breadcrumb, + tags: ['autodocs'], + argTypes: {}, + args: {}, + render: (args) => ( + + + + Home + + + + Components + + + + Breadcrumb + + + + ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * 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) => ( + + + + Home + + + + + + Components + + + + + + Breadcrumb + + + + ), +}; diff --git a/apps/storybook/stories/button.stories.tsx b/apps/storybook/stories/button.stories.tsx new file mode 100644 index 0000000..5cb5839 --- /dev/null +++ b/apps/storybook/stories/button.stories.tsx @@ -0,0 +1,157 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Loader2, Mail } from 'lucide-react'; + +import { Button } from '@konobangu/design-system/components/ui/button'; + +/** + * Displays a button or a component that looks like a button. + */ +const meta = { + title: 'ui/Button', + component: Button, + tags: ['autodocs'], + argTypes: { + children: { + control: 'text', + }, + }, + parameters: { + layout: 'centered', + }, + args: { + variant: 'default', + size: 'default', + children: 'Button', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * 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) => ( + + ), + 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) => ( + + ), + 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: , + }, +}; + +/** + * Add the `disabled` prop to prevent interactions with the button. + */ +export const Disabled: Story = { + args: { + disabled: true, + }, +}; diff --git a/apps/storybook/stories/calendar.stories.tsx b/apps/storybook/stories/calendar.stories.tsx new file mode 100644 index 0000000..ddd5b3f --- /dev/null +++ b/apps/storybook/stories/calendar.stories.tsx @@ -0,0 +1,81 @@ +import { action } from '@storybook/addon-actions'; +import type { Meta, StoryObj } from '@storybook/react'; +import { addDays } from 'date-fns'; + +import { Calendar } from '@konobangu/design-system/components/ui/calendar'; + +/** + * A date field component that allows users to enter and edit date. + */ +const meta = { + title: 'ui/Calendar', + component: Calendar, + tags: ['autodocs'], + argTypes: {}, + args: { + mode: 'single', + selected: new Date(), + onSelect: action('onDayClick'), + className: 'rounded-md border w-fit', + }, + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * 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, + }, +}; diff --git a/apps/storybook/stories/card.stories.tsx b/apps/storybook/stories/card.stories.tsx new file mode 100644 index 0000000..7e0c04c --- /dev/null +++ b/apps/storybook/stories/card.stories.tsx @@ -0,0 +1,75 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { BellRing } from 'lucide-react'; + +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@konobangu/design-system/components/ui/card'; + +const notifications = [ + { + title: 'Your call has been confirmed.', + description: '1 hour ago', + }, + { + title: 'You have a new message!', + description: '1 hour ago', + }, + { + title: 'Your subscription is expiring soon!', + description: '2 hours ago', + }, +]; + +/** + * Displays a card with header, content, and footer. + */ +const meta = { + title: 'ui/Card', + component: Card, + tags: ['autodocs'], + argTypes: {}, + args: { + className: 'w-96', + }, + render: (args) => ( + + + Notifications + You have 3 unread messages. + + + {notifications.map((notification, index) => ( +
+ +
+

{notification.title}

+

{notification.description}

+
+
+ ))} +
+ + + +
+ ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the card. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/carousel.stories.tsx b/apps/storybook/stories/carousel.stories.tsx new file mode 100644 index 0000000..0e0ec68 --- /dev/null +++ b/apps/storybook/stories/carousel.stories.tsx @@ -0,0 +1,73 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, +} from '@konobangu/design-system/components/ui/carousel'; + +/** + * A carousel with motion and swipe built using Embla. + */ +const meta: Meta = { + title: 'ui/Carousel', + component: Carousel, + tags: ['autodocs'], + argTypes: {}, + args: { + className: 'w-full max-w-xs', + }, + render: (args) => ( + + + {Array.from({ length: 5 }).map((_, index) => ( + +
+ {index + 1} +
+
+ ))} +
+ + +
+ ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * 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) => ( + + + {Array.from({ length: 5 }).map((_, index) => ( + +
+ {index + 1} +
+
+ ))} +
+ + +
+ ), + args: { + className: 'mx-12 w-full max-w-xs', + }, +}; diff --git a/apps/storybook/stories/chart.stories.tsx b/apps/storybook/stories/chart.stories.tsx new file mode 100644 index 0000000..d90937f --- /dev/null +++ b/apps/storybook/stories/chart.stories.tsx @@ -0,0 +1,271 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { useMemo } from 'react'; +import { + Area, + AreaChart, + Bar, + BarChart, + CartesianGrid, + Label, + Line, + LineChart, + Pie, + PieChart, + XAxis, +} from 'recharts'; + +import { + type ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from '@konobangu/design-system/components/ui/chart'; + +const multiSeriesData = [ + { month: 'January', desktop: 186, mobile: 80 }, + { month: 'February', desktop: 305, mobile: 200 }, + { month: 'March', desktop: 237, mobile: 120 }, + { month: 'April', desktop: 73, mobile: 190 }, + { month: 'May', desktop: 209, mobile: 130 }, + { month: 'June', desktop: 214, mobile: 140 }, +]; + +const multiSeriesConfig = { + desktop: { + label: 'Desktop', + color: 'hsl(var(--chart-1))', + }, + mobile: { + label: 'Mobile', + color: 'hsl(var(--chart-2))', + }, +} satisfies ChartConfig; + +const singleSeriesData = [ + { browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' }, + { browser: 'safari', visitors: 200, fill: 'var(--color-safari)' }, + { browser: 'other', visitors: 190, fill: 'var(--color-other)' }, +]; + +const singleSeriesConfig = { + visitors: { + label: 'Visitors', + }, + chrome: { + label: 'Chrome', + color: 'hsl(var(--chart-1))', + }, + safari: { + label: 'Safari', + color: 'hsl(var(--chart-2))', + }, + other: { + label: 'Other', + color: 'hsl(var(--chart-5))', + }, +} satisfies ChartConfig; + +/** + * Beautiful charts. Built using Recharts. Copy and paste into your apps. + */ +const meta = { + title: 'ui/Chart', + component: ChartContainer, + tags: ['autodocs'], + argTypes: {}, + args: { + children:
, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * Combine multiple Area components to create a stacked area chart. + */ +export const StackedAreaChart: Story = { + args: { + config: multiSeriesConfig, + }, + render: (args) => ( + + + + value.slice(0, 3)} + /> + } + /> + + + + + ), +}; + +/** + * Combine multiple Bar components to create a stacked bar chart. + */ +export const StackedBarChart: Story = { + args: { + config: multiSeriesConfig, + }, + render: (args) => ( + + + + value.slice(0, 3)} + /> + } + /> + + + + + ), +}; + +/** + * Combine multiple Line components to create a single line chart. + */ +export const MultiLineChart: Story = { + args: { + config: multiSeriesConfig, + }, + render: (args) => ( + + + + value.slice(0, 3)} + /> + } + /> + + + + + ), +}; + +/** + * 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 ( + + + } + /> + + + + + ); + }, +}; diff --git a/apps/storybook/stories/checkbox.stories.tsx b/apps/storybook/stories/checkbox.stories.tsx new file mode 100644 index 0000000..0415d10 --- /dev/null +++ b/apps/storybook/stories/checkbox.stories.tsx @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Checkbox } from '@konobangu/design-system/components/ui/checkbox'; + +/** + * A control that allows the user to toggle between checked and not checked. + */ +const meta: Meta = { + title: 'ui/Checkbox', + component: Checkbox, + tags: ['autodocs'], + argTypes: {}, + args: { + id: 'terms', + disabled: false, + }, + render: (args) => ( +
+ + +
+ ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * 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, + }, +}; diff --git a/apps/storybook/stories/collapsible.stories.tsx b/apps/storybook/stories/collapsible.stories.tsx new file mode 100644 index 0000000..e4d6bd9 --- /dev/null +++ b/apps/storybook/stories/collapsible.stories.tsx @@ -0,0 +1,55 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Info } from 'lucide-react'; + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@konobangu/design-system/components/ui/collapsible'; + +/** + * An interactive component which expands/collapses a panel. + */ +const meta = { + title: 'ui/Collapsible', + component: Collapsible, + tags: ['autodocs'], + argTypes: {}, + args: { + className: 'w-96', + disabled: false, + }, + render: (args) => ( + + +

Can I use this in my project?

+ +
+ + Yes. Free to use for personal and commercial projects. No attribution + required. + +
+ ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * 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, + }, +}; diff --git a/apps/storybook/stories/command.stories.tsx b/apps/storybook/stories/command.stories.tsx new file mode 100644 index 0000000..ca2a6ad --- /dev/null +++ b/apps/storybook/stories/command.stories.tsx @@ -0,0 +1,55 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { CommandSeparator } from 'cmdk'; + +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@konobangu/design-system/components/ui/command'; + +/** + * Fast, composable, unstyled command menu for React. + */ +const meta = { + title: 'ui/Command', + component: Command, + tags: ['autodocs'], + argTypes: {}, + args: { + className: 'rounded-lg w-96 border shadow-md', + }, + render: (args) => ( + + + + No results found. + + Calendar + Search Emoji + Calculator + + + + Profile + Billing + Settings + + + + ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the command. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/context-menu.stories.tsx b/apps/storybook/stories/context-menu.stories.tsx new file mode 100644 index 0000000..0133300 --- /dev/null +++ b/apps/storybook/stories/context-menu.stories.tsx @@ -0,0 +1,153 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + ContextMenu, + ContextMenuCheckboxItem, + ContextMenuContent, + ContextMenuItem, + ContextMenuLabel, + ContextMenuRadioGroup, + ContextMenuRadioItem, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuTrigger, +} from '@konobangu/design-system/components/ui/context-menu'; + +/** + * Displays a menu to the user — such as a set of actions or functions — + * triggered by a button. + */ +const meta = { + title: 'ui/ContextMenu', + component: ContextMenu, + tags: ['autodocs'], + argTypes: {}, + args: {}, + render: (args) => ( + + + Right click here + + + Profile + Billing + Team + Subscription + + + ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the context menu. + */ +export const Default: Story = {}; + +/** + * A context menu with shortcuts. + */ +export const WithShortcuts: Story = { + render: (args) => ( + + + Right click here + + + + Back + ⌘[ + + + Forward + ⌘] + + + Reload + ⌘R + + + + ), +}; + +/** + * A context menu with a submenu. + */ +export const WithSubmenu: Story = { + render: (args) => ( + + + Right click here + + + + New Tab + ⌘N + + + More Tools + + + Save Page As... + ⇧⌘S + + Create Shortcut... + Name Window... + + Developer Tools + + + + + ), +}; + +/** + * A context menu with checkboxes. + */ +export const WithCheckboxes: Story = { + render: (args) => ( + + + Right click here + + + + Show Comments + ⌘⇧C + + Show Preview + + + ), +}; + +/** + * A context menu with a radio group. + */ +export const WithRadioGroup: Story = { + render: (args) => ( + + + Right click here + + + + Theme + Light + Dark + + + + ), +}; diff --git a/apps/storybook/stories/dialog.stories.tsx b/apps/storybook/stories/dialog.stories.tsx new file mode 100644 index 0000000..9ffd6f1 --- /dev/null +++ b/apps/storybook/stories/dialog.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@konobangu/design-system/components/ui/dialog'; + +/** + * A window overlaid on either the primary window or another dialog window, + * rendering the content underneath inert. + */ +const meta = { + title: 'ui/Dialog', + component: Dialog, + tags: ['autodocs'], + argTypes: {}, + render: (args) => ( + + Open + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your + account and remove your data from our servers. + + + + + + + + + + + ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the dialog. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/drawer.stories.tsx b/apps/storybook/stories/drawer.stories.tsx new file mode 100644 index 0000000..45c0d00 --- /dev/null +++ b/apps/storybook/stories/drawer.stories.tsx @@ -0,0 +1,58 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from '@konobangu/design-system/components/ui/drawer'; + +/** + * A drawer component for React. + */ +const meta: Meta = { + title: 'ui/Drawer', + component: Drawer, + tags: ['autodocs'], + argTypes: {}, + render: (args) => ( + + Open + + + Are you sure absolutely sure? + This action cannot be undone. + + + + + + + + + + ), + parameters: { + layout: 'centered', + }, +}; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the drawer. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/dropdown-menu.stories.tsx b/apps/storybook/stories/dropdown-menu.stories.tsx new file mode 100644 index 0000000..701ee9d --- /dev/null +++ b/apps/storybook/stories/dropdown-menu.stories.tsx @@ -0,0 +1,159 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Mail, Plus, PlusCircle, Search, UserPlus } from 'lucide-react'; + +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from '@konobangu/design-system/components/ui/dropdown-menu'; + +/** + * Displays a menu to the user — such as a set of actions or functions — + * triggered by a button. + */ +const meta = { + title: 'ui/DropdownMenu', + component: DropdownMenu, + tags: ['autodocs'], + argTypes: {}, + render: (args) => ( + + Open + + My Account + + Profile + Billing + Team + Subscription + + + ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the dropdown menu. + */ +export const Default: Story = {}; + +/** + * A dropdown menu with shortcuts. + */ +export const WithShortcuts: Story = { + render: (args) => ( + + Open + + Controls + + Back + ⌘[ + + + Forward + ⌘] + + + + ), +}; + +/** + * A dropdown menu with submenus. + */ +export const WithSubmenus: Story = { + render: (args) => ( + + Open + + + + Search + + + + + + New Team + ⌘+T + + + + + Invite users + + + + + + Email + + + + + More... + + + + + + + + ), +}; + +/** + * A dropdown menu with radio items. + */ +export const WithRadioItems: Story = { + render: (args) => ( + + Open + + Status + + Info + Warning + Error + + + + ), +}; + +/** + * A dropdown menu with checkboxes. + */ +export const WithCheckboxes: Story = { + render: (args) => ( + + Open + + + Autosave + ⌘S + + Show Comments + + + ), +}; diff --git a/apps/storybook/stories/form.stories.tsx b/apps/storybook/stories/form.stories.tsx new file mode 100644 index 0000000..d7bc5e8 --- /dev/null +++ b/apps/storybook/stories/form.stories.tsx @@ -0,0 +1,85 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { action } from '@storybook/addon-actions'; +import type { Meta, StoryObj } from '@storybook/react'; +import { useForm } from 'react-hook-form'; +import * as z from 'zod'; + +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@konobangu/design-system/components/ui/form'; + +/** + * Building forms with React Hook Form and Zod. + */ +const meta: Meta = { + title: 'ui/Form', + component: Form, + tags: ['autodocs'], + argTypes: {}, + render: (args) => , +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +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>({ + resolver: zodResolver(formSchema), + defaultValues: { + username: '', + }, + }); + function onSubmit(values: z.infer) { + action('onSubmit')(values); + } + return ( +
+ + ( + + Username + + + + + This is your public display name. + + + + )} + /> + + + + ); +}; + +/** + * The default form of the form. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/hover-card.stories.tsx b/apps/storybook/stories/hover-card.stories.tsx new file mode 100644 index 0000000..4b8b1d0 --- /dev/null +++ b/apps/storybook/stories/hover-card.stories.tsx @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from '@konobangu/design-system/components/ui/hover-card'; + +/** + * For sighted users to preview content available behind a link. + */ +const meta = { + title: 'ui/HoverCard', + component: HoverCard, + tags: ['autodocs'], + argTypes: {}, + args: {}, + render: (args) => ( + + Hover + + The React Framework - created and maintained by @vercel. + + + ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * 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, + }, +}; diff --git a/apps/storybook/stories/input-otp.stories.tsx b/apps/storybook/stories/input-otp.stories.tsx new file mode 100644 index 0000000..c2102ab --- /dev/null +++ b/apps/storybook/stories/input-otp.stories.tsx @@ -0,0 +1,70 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp'; + +import { + InputOTP, + InputOTPGroup, + InputOTPSeparator, + InputOTPSlot, +} from '@konobangu/design-system/components/ui/input-otp'; + +/** + * Accessible one-time password component with copy paste functionality. + */ +const meta = { + title: 'ui/InputOTP', + component: InputOTP, + tags: ['autodocs'], + argTypes: {}, + args: { + maxLength: 6, + pattern: REGEXP_ONLY_DIGITS_AND_CHARS, + children: null, + }, + + render: (args) => ( + + + + + + + + + + + ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * 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) => ( + + + + + + + + + + + + + + ), +}; diff --git a/apps/storybook/stories/input.stories.tsx b/apps/storybook/stories/input.stories.tsx new file mode 100644 index 0000000..42abeb8 --- /dev/null +++ b/apps/storybook/stories/input.stories.tsx @@ -0,0 +1,84 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Input } from '@konobangu/design-system/components/ui/input'; + +/** + * Displays a form input field or a component that looks like an input field. + */ +const meta = { + title: 'ui/Input', + component: Input, + tags: ['autodocs'], + argTypes: {}, + args: { + className: 'w-96', + type: 'email', + placeholder: 'Email', + disabled: false, + }, + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * 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) => ( +
+ + +
+ ), +}; + +/** + * Use a text element below the input field to provide additional instructions + * or information to users. + */ +export const WithHelperText: Story = { + render: (args) => ( +
+ + +

Enter your email address.

+
+ ), +}; + +/** + * 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) => ( +
+ + +
+ ), +}; diff --git a/apps/storybook/stories/label.stories.tsx b/apps/storybook/stories/label.stories.tsx new file mode 100644 index 0000000..8e63fea --- /dev/null +++ b/apps/storybook/stories/label.stories.tsx @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Label } from '@konobangu/design-system/components/ui/label'; + +/** + * Renders an accessible label associated with controls. + */ +const meta = { + title: 'ui/Label', + component: Label, + tags: ['autodocs'], + argTypes: { + children: { + control: { type: 'text' }, + }, + }, + args: { + children: 'Your email address', + htmlFor: 'email', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the label. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/menubar.stories.tsx b/apps/storybook/stories/menubar.stories.tsx new file mode 100644 index 0000000..59d6103 --- /dev/null +++ b/apps/storybook/stories/menubar.stories.tsx @@ -0,0 +1,126 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + Menubar, + MenubarCheckboxItem, + MenubarContent, + MenubarGroup, + MenubarItem, + MenubarLabel, + MenubarMenu, + MenubarRadioGroup, + MenubarRadioItem, + MenubarSeparator, + MenubarShortcut, + MenubarSub, + MenubarSubContent, + MenubarSubTrigger, + MenubarTrigger, +} from '@konobangu/design-system/components/ui/menubar'; + +/** + * A visually persistent menu common in desktop applications that provides + * quick access to a consistent set of commands. + */ +const meta = { + title: 'ui/Menubar', + component: Menubar, + tags: ['autodocs'], + argTypes: {}, + + render: (args) => ( + + + File + + + New Tab ⌘T + + New Window + + Share + + Print + + + + ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the menubar. + */ +export const Default: Story = {}; + +/** + * A menubar with a submenu. + */ +export const WithSubmenu: Story = { + render: (args) => ( + + + Actions + + Download + + Share + + Email link + Messages + Notes + + + + + + ), +}; + +/** + * A menubar with radio items. + */ +export const WithRadioItems: Story = { + render: (args) => ( + + + View + + Device Size + + Small + Medium + Large + + + + + ), +}; + +/** + * A menubar with checkbox items. + */ +export const WithCheckboxItems: Story = { + render: (args) => ( + + + Filters + + Show All + + Unread + Important + Flagged + + + + + ), +}; diff --git a/apps/storybook/stories/navigation-menu.stories.tsx b/apps/storybook/stories/navigation-menu.stories.tsx new file mode 100644 index 0000000..e2f158c --- /dev/null +++ b/apps/storybook/stories/navigation-menu.stories.tsx @@ -0,0 +1,79 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + NavigationMenu, + NavigationMenuContent, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, + navigationMenuTriggerStyle, +} from '@konobangu/design-system/components/ui/navigation-menu'; + +/** + * A collection of links for navigating websites. + */ +const meta = { + title: 'ui/NavigationMenu', + component: NavigationMenu, + tags: ['autodocs'], + argTypes: {}, + render: (args) => ( + + + + + Overview + + + + + + Documentation + + +
    +
  • + + API Reference + +
  • +
  • + + Getting Started + +
  • +
  • + + Guides + +
  • +
+
+
+
+ + + External + + +
+
+ ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the navigation menu. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/pagination.stories.tsx b/apps/storybook/stories/pagination.stories.tsx new file mode 100644 index 0000000..4ecb778 --- /dev/null +++ b/apps/storybook/stories/pagination.stories.tsx @@ -0,0 +1,57 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from '@konobangu/design-system/components/ui/pagination'; + +/** + * Pagination with page navigation, next and previous links. + */ +const meta = { + title: 'ui/Pagination', + component: Pagination, + tags: ['autodocs'], + argTypes: {}, + render: (args) => ( + + + + + + + 1 + + + 2 + + + 3 + + + + + + + + + + ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the pagination. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/popover.stories.tsx b/apps/storybook/stories/popover.stories.tsx new file mode 100644 index 0000000..16212ec --- /dev/null +++ b/apps/storybook/stories/popover.stories.tsx @@ -0,0 +1,36 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@konobangu/design-system/components/ui/popover'; + +/** + * Displays rich content in a portal, triggered by a button. + */ +const meta = { + title: 'ui/Popover', + component: Popover, + tags: ['autodocs'], + argTypes: {}, + + render: (args) => ( + + Open + Place content for the popover here. + + ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the popover. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/progress.stories.tsx b/apps/storybook/stories/progress.stories.tsx new file mode 100644 index 0000000..6969c73 --- /dev/null +++ b/apps/storybook/stories/progress.stories.tsx @@ -0,0 +1,45 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Progress } from '@konobangu/design-system/components/ui/progress'; + +/** + * Displays an indicator showing the completion progress of a task, typically + * displayed as a progress bar. + */ +const meta = { + title: 'ui/Progress', + component: Progress, + tags: ['autodocs'], + argTypes: {}, + args: { + value: 30, + max: 100, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * 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, + }, +}; diff --git a/apps/storybook/stories/radio-group.stories.tsx b/apps/storybook/stories/radio-group.stories.tsx new file mode 100644 index 0000000..66df39f --- /dev/null +++ b/apps/storybook/stories/radio-group.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + RadioGroup, + RadioGroupItem, +} from '@konobangu/design-system/components/ui/radio-group'; + +/** + * A set of checkable buttons—known as radio buttons—where no more than one of + * the buttons can be checked at a time. + */ +const meta = { + title: 'ui/RadioGroup', + component: RadioGroup, + tags: ['autodocs'], + argTypes: {}, + args: { + defaultValue: 'comfortable', + className: 'grid gap-2 grid-cols-[1rem_1fr] items-center', + }, + render: (args) => ( + + + + + + + + + ), +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the radio group. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/resizable.stories.tsx b/apps/storybook/stories/resizable.stories.tsx new file mode 100644 index 0000000..b60bcad --- /dev/null +++ b/apps/storybook/stories/resizable.stories.tsx @@ -0,0 +1,59 @@ +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 = { + title: 'ui/ResizablePanelGroup', + component: ResizablePanelGroup, + tags: ['autodocs'], + argTypes: { + onLayout: { + control: false, + }, + }, + args: { + className: 'max-w-96 rounded-lg border', + direction: 'horizontal', + }, + render: (args) => ( + + +
+ One +
+
+ + + + +
+ Two +
+
+ + +
+ Three +
+
+
+
+
+ ), +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the resizable panel group. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/scroll-area.stories.tsx b/apps/storybook/stories/scroll-area.stories.tsx new file mode 100644 index 0000000..d9d8a9e --- /dev/null +++ b/apps/storybook/stories/scroll-area.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { ScrollArea } from '@konobangu/design-system/components/ui/scroll-area'; + +/** + * Augments native scroll functionality for custom, cross-browser styling. + */ +const meta = { + title: 'ui/ScrollArea', + component: ScrollArea, + tags: ['autodocs'], + argTypes: { + children: { + control: 'text', + }, + }, + args: { + className: 'h-32 w-80 rounded-md border p-4', + type: 'auto', + children: + "Jokester began sneaking into the castle in the middle of the night and leaving jokes all over the place: under the king's pillow, in his soup, even in the royal toilet. The king was furious, but he couldn't seem to stop Jokester. And then, one day, the people of the kingdom discovered that the jokes left by Jokester were so funny that they couldn't help but laugh. And once they started laughing, they couldn't stop. The king was so angry that he banished Jokester from the kingdom, but the people still laughed, and they laughed, and they laughed. And they all lived happily ever after.", + }, + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the scroll area. + */ +export const Default: Story = {}; + +/** + * Use the `type` prop with `always` to always show the scroll area. + */ +export const Always: Story = { + args: { + type: 'always', + }, +}; + +/** + * Use the `type` prop with `hover` to show the scroll area on hover. + */ +export const Hover: Story = { + args: { + type: 'hover', + }, +}; + +/** + * Use the `type` prop with `scroll` to show the scroll area when scrolling. + */ +export const Scroll: Story = { + args: { + type: 'scroll', + }, +}; diff --git a/apps/storybook/stories/select.stories.tsx b/apps/storybook/stories/select.stories.tsx new file mode 100644 index 0000000..e92b779 --- /dev/null +++ b/apps/storybook/stories/select.stories.tsx @@ -0,0 +1,70 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectSeparator, + SelectTrigger, + SelectValue, +} from '@konobangu/design-system/components/ui/select'; + +/** + * Displays a list of options for the user to pick from—triggered by a button. + */ +const meta: Meta = { + title: 'ui/Select', + component: Select, + tags: ['autodocs'], + argTypes: {}, + render: (args) => ( + + ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the select. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/separator.stories.tsx b/apps/storybook/stories/separator.stories.tsx new file mode 100644 index 0000000..f088a94 --- /dev/null +++ b/apps/storybook/stories/separator.stories.tsx @@ -0,0 +1,43 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Separator } from '@konobangu/design-system/components/ui/separator'; + +/** + * Visually or semantically separates content. + */ +const meta = { + title: 'ui/Separator', + component: Separator, + tags: ['autodocs'], + argTypes: {}, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the separator. + */ +export const Horizontal: Story = { + render: () => ( +
+
Left
+ +
Right
+
+ ), +}; + +/** + * A vertical separator. + */ +export const Vertical: Story = { + render: () => ( +
+
Top
+ +
Bottom
+
+ ), +}; diff --git a/apps/storybook/stories/sheet.stories.tsx b/apps/storybook/stories/sheet.stories.tsx new file mode 100644 index 0000000..934d99f --- /dev/null +++ b/apps/storybook/stories/sheet.stories.tsx @@ -0,0 +1,72 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger, +} from '@konobangu/design-system/components/ui/sheet'; + +/** + * Extends the Dialog component to display content that complements the main + * content of the screen. + */ +const meta: Meta = { + title: 'ui/Sheet', + component: Sheet, + tags: ['autodocs'], + argTypes: { + side: { + options: ['top', 'bottom', 'left', 'right'], + control: { + type: 'radio', + }, + }, + }, + args: { + side: 'right', + }, + render: (args) => ( + + Open + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your + account and remove your data from our servers. + + + + + + + + + + + ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the sheet. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/sidebar.stories.tsx b/apps/storybook/stories/sidebar.stories.tsx new file mode 100644 index 0000000..debdc54 --- /dev/null +++ b/apps/storybook/stories/sidebar.stories.tsx @@ -0,0 +1,494 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { + AudioWaveform, + BadgeCheck, + Bell, + BookOpen, + Bot, + ChevronRight, + ChevronsUpDown, + Command, + CreditCard, + Folder, + Forward, + Frame, + GalleryVerticalEnd, + LogOut, + // biome-ignore lint/suspicious/noShadowRestrictedNames: "icon name" + Map, + MoreHorizontal, + PieChart, + Plus, + Settings2, + Sparkles, + SquareTerminal, + Trash2, +} from 'lucide-react'; + +import { + Avatar, + AvatarFallback, + AvatarImage, +} from '@konobangu/design-system/components/ui/avatar'; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from '@konobangu/design-system/components/ui/breadcrumb'; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@konobangu/design-system/components/ui/collapsible'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from '@konobangu/design-system/components/ui/dropdown-menu'; +import { Separator } from '@konobangu/design-system/components/ui/separator'; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupLabel, + SidebarHeader, + SidebarInset, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + SidebarProvider, + SidebarRail, + SidebarTrigger, +} from '@konobangu/design-system/components/ui/sidebar'; +import { useState } from 'react'; + +const meta: Meta = { + title: 'ui/Sidebar', + component: Sidebar, + tags: ['autodocs'], + argTypes: {}, +}; +export default meta; + +type Story = StoryObj; + +const data = { + user: { + name: 'shadcn', + email: 'm@example.com', + avatar: '/avatars/shadcn.jpg', + }, + teams: [ + { + name: 'Acme Inc', + logo: GalleryVerticalEnd, + plan: 'Enterprise', + }, + { + name: 'Acme Corp.', + logo: AudioWaveform, + plan: 'Startup', + }, + { + name: 'Evil Corp.', + logo: Command, + plan: 'Free', + }, + ], + navMain: [ + { + title: 'Playground', + url: '#', + icon: SquareTerminal, + isActive: true, + items: [ + { + title: 'History', + url: '#', + }, + { + title: 'Starred', + url: '#', + }, + { + title: 'Settings', + url: '#', + }, + ], + }, + { + title: 'Models', + url: '#', + icon: Bot, + items: [ + { + title: 'Genesis', + url: '#', + }, + { + title: 'Explorer', + url: '#', + }, + { + title: 'Quantum', + url: '#', + }, + ], + }, + { + title: 'Documentation', + url: '#', + icon: BookOpen, + items: [ + { + title: 'Introduction', + url: '#', + }, + { + title: 'Get Started', + url: '#', + }, + { + title: 'Tutorials', + url: '#', + }, + { + title: 'Changelog', + url: '#', + }, + ], + }, + { + title: 'Settings', + url: '#', + icon: Settings2, + items: [ + { + title: 'General', + url: '#', + }, + { + title: 'Team', + url: '#', + }, + { + title: 'Billing', + url: '#', + }, + { + title: 'Limits', + url: '#', + }, + ], + }, + ], + projects: [ + { + name: 'Design Engineering', + url: '#', + icon: Frame, + }, + { + name: 'Sales & Marketing', + url: '#', + icon: PieChart, + }, + { + name: 'Travel', + url: '#', + icon: Map, + }, + ], +}; + +export const Base: Story = { + render: () => { + const [activeTeam, setActiveTeam] = useState(data.teams[0]); + + return ( + + + + + + + + +
+ +
+
+ + {activeTeam.name} + + + {activeTeam.plan} + +
+ +
+
+ + + Teams + + {data.teams.map((team, index) => ( + setActiveTeam(team)} + className="gap-2 p-2" + > +
+ +
+ {team.name} + + ⌘{index + 1} + +
+ ))} + + +
+ +
+
+ Add team +
+
+
+
+
+
+
+ + + Platform + + {data.navMain.map((item) => ( + + + + + {item.icon && } + {item.title} + + + + + + {item.items?.map((subItem) => ( + + + + {subItem.title} + + + + ))} + + + + + ))} + + + + Projects + + {data.projects.map((item) => ( + + + + + {item.name} + + + + + + + More + + + + + + View Project + + + + Share Project + + + + + Delete Project + + + + + ))} + + + + More + + + + + + + + + + + + + + + CN + + +
+ + {data.user.name} + + + {data.user.email} + +
+ +
+
+ + +
+ + + + CN + + +
+ + {data.user.name} + + + {data.user.email} + +
+
+
+ + + + + Upgrade to Pro + + + + + + + Account + + + + Billing + + + + Notifications + + + + + + Log out + +
+
+
+
+
+ +
+ +
+
+ + + + + + + Building Your Application + + + + + Data Fetching + + + +
+
+
+
+
+
+
+
+
+
+ + + ); + }, + args: {}, +}; diff --git a/apps/storybook/stories/skeleton.stories.tsx b/apps/storybook/stories/skeleton.stories.tsx new file mode 100644 index 0000000..a3cbd6d --- /dev/null +++ b/apps/storybook/stories/skeleton.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Skeleton } from '@konobangu/design-system/components/ui/skeleton'; + +/** + * Use to show a placeholder while content is loading. + */ +const meta = { + title: 'ui/Skeleton', + component: Skeleton, + tags: ['autodocs'], + argTypes: {}, + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the skeleton. + */ +export const Default: Story = { + render: (args) => ( +
+ +
+ + +
+
+ ), +}; diff --git a/apps/storybook/stories/slider.stories.tsx b/apps/storybook/stories/slider.stories.tsx new file mode 100644 index 0000000..e6aac4d --- /dev/null +++ b/apps/storybook/stories/slider.stories.tsx @@ -0,0 +1,45 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Slider } from '@konobangu/design-system/components/ui/slider'; + +/** + * An input where the user selects a value from within a given range. + */ +const meta = { + title: 'ui/Slider', + component: Slider, + tags: ['autodocs'], + argTypes: {}, + args: { + defaultValue: [33], + max: 100, + step: 1, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the slider. + */ +export const Default: Story = {}; + +/** + * Use the `inverted` prop to have the slider fill from right to left. + */ +export const Inverted: Story = { + args: { + inverted: true, + }, +}; + +/** + * Use the `disabled` prop to disable the slider. + */ +export const Disabled: Story = { + args: { + disabled: true, + }, +}; diff --git a/apps/storybook/stories/sonner.stories.tsx b/apps/storybook/stories/sonner.stories.tsx new file mode 100644 index 0000000..befc40d --- /dev/null +++ b/apps/storybook/stories/sonner.stories.tsx @@ -0,0 +1,50 @@ +import { action } from '@storybook/addon-actions'; +import type { Meta, StoryObj } from '@storybook/react'; +import { toast } from 'sonner'; + +import { Toaster } from '@konobangu/design-system/components/ui/sonner'; + +/** + * An opinionated toast component for React. + */ +const meta: Meta = { + title: 'ui/Sonner', + component: Toaster, + tags: ['autodocs'], + argTypes: {}, + args: { + position: 'bottom-right', + }, + parameters: { + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the toaster. + */ +export const Default: Story = { + render: (args) => ( +
+ + +
+ ), +}; diff --git a/apps/storybook/stories/switch.stories.tsx b/apps/storybook/stories/switch.stories.tsx new file mode 100644 index 0000000..3a25b89 --- /dev/null +++ b/apps/storybook/stories/switch.stories.tsx @@ -0,0 +1,47 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Switch } from '@konobangu/design-system/components/ui/switch'; + +/** + * A control that allows the user to toggle between checked and not checked. + */ +const meta = { + title: 'ui/Switch', + component: Switch, + tags: ['autodocs'], + argTypes: {}, + parameters: { + layout: 'centered', + }, + render: (args) => ( +
+ + +
+ ), +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the switch. + */ +export const Default: Story = { + args: { + id: 'default-switch', + }, +}; + +/** + * Use the `disabled` prop to disable the switch. + */ +export const Disabled: Story = { + args: { + id: 'disabled-switch', + disabled: true, + }, +}; diff --git a/apps/storybook/stories/table.stories.tsx b/apps/storybook/stories/table.stories.tsx new file mode 100644 index 0000000..1bf2cb7 --- /dev/null +++ b/apps/storybook/stories/table.stories.tsx @@ -0,0 +1,80 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@konobangu/design-system/components/ui/table'; + +const invoices = [ + { + invoice: 'INV001', + paymentStatus: 'Paid', + totalAmount: '$250.00', + paymentMethod: 'Credit Card', + }, + { + invoice: 'INV002', + paymentStatus: 'Pending', + totalAmount: '$150.00', + paymentMethod: 'PayPal', + }, + { + invoice: 'INV003', + paymentStatus: 'Unpaid', + totalAmount: '$350.00', + paymentMethod: 'Bank Transfer', + }, + { + invoice: 'INV004', + paymentStatus: 'Paid', + totalAmount: '$450.00', + paymentMethod: 'Credit Card', + }, +]; + +/** + * Powerful table and datagrids built using TanStack Table. + */ +const meta = { + title: 'ui/Table', + component: Table, + tags: ['autodocs'], + argTypes: {}, + render: (args) => ( + + A list of your recent invoices. + + + Invoice + Status + Method + Amount + + + + {invoices.map((invoice) => ( + + {invoice.invoice} + {invoice.paymentStatus} + {invoice.paymentMethod} + {invoice.totalAmount} + + ))} + +
+ ), +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the table. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/tabs.stories.tsx b/apps/storybook/stories/tabs.stories.tsx new file mode 100644 index 0000000..2359528 --- /dev/null +++ b/apps/storybook/stories/tabs.stories.tsx @@ -0,0 +1,47 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '@konobangu/design-system/components/ui/tabs'; + +/** + * A set of layered sections of content—known as tab panels—that are displayed + * one at a time. + */ +const meta = { + title: 'ui/Tabs', + component: Tabs, + tags: ['autodocs'], + argTypes: {}, + args: { + defaultValue: 'account', + className: 'w-96', + }, + render: (args) => ( + + + Account + Password + + + Make changes to your account here. + + Change your password here. + + ), + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the tabs. + */ +export const Default: Story = {}; diff --git a/apps/storybook/stories/textarea.stories.tsx b/apps/storybook/stories/textarea.stories.tsx new file mode 100644 index 0000000..49f5387 --- /dev/null +++ b/apps/storybook/stories/textarea.stories.tsx @@ -0,0 +1,82 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Textarea } from '@konobangu/design-system/components/ui/textarea'; + +/** + * Displays a form textarea or a component that looks like a textarea. + */ +const meta = { + title: 'ui/Textarea', + component: Textarea, + tags: ['autodocs'], + argTypes: {}, + args: { + placeholder: 'Type your message here.', + disabled: false, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * The default form of the textarea. + */ +export const Default: Story = {}; + +/** + * Use the `disabled` prop to disable the textarea. + */ +export const Disabled: Story = { + args: { + disabled: true, + }, +}; + +/** + * Use the `Label` component to includes a clear, descriptive label above or + * alongside the text area to guide users. + */ +export const WithLabel: Story = { + render: (args) => ( +
+ +