Basic Architecture of Sport Web

This commit is contained in:
Nông Đức Huy
2026-08-11 13:37:25 +07:00
commit 8032fff6ac
262 changed files with 20348 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
import { reactConfig } from '@sport/eslint-config/react';
export default reactConfig;
+31
View File
@@ -0,0 +1,31 @@
{
"name": "@sport/ui",
"version": "0.0.0",
"private": true,
"description": "Shared, presentational React design-system primitives. Consumed as source via Next.js transpilePackages.",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"lint": "eslint src",
"typecheck": "tsc -p tsconfig.json --noEmit",
"clean": "rm -rf .turbo *.tsbuildinfo"
},
"dependencies": {
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@sport/config": "workspace:*",
"@sport/eslint-config": "workspace:*",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"eslint": "catalog:",
"react": "catalog:",
"typescript": "catalog:"
},
"peerDependencies": {
"react": "^19.0.0"
}
}
+27
View File
@@ -0,0 +1,27 @@
/**
* @sport/ui — the design system.
*
* WHAT BELONGS HERE
* Presentational primitives that are identical in the storefront and the
* admin: buttons, inputs, badges, skeletons, typography, layout helpers.
*
* WHAT MUST NEVER BE HERE
* - Data fetching or any @sport/api-client import.
* - App state (cart, auth session, filters).
* - Domain components. A `<ProductCard>` knows about pricing, sale badges and
* variant swatches — that is storefront business UI and lives in
* `apps/storefront/src/features/product/`. Sharing it would couple two apps
* that must be free to diverge.
*
* The test: if the component would be meaningless in the admin dashboard, it
* does not go in this package.
*/
export { cn } from './lib/cn';
export { Button, buttonVariants } from './primitives/button';
export type { ButtonProps } from './primitives/button';
export { Badge, badgeVariants } from './primitives/badge';
export type { BadgeProps } from './primitives/badge';
export { Input } from './primitives/input';
export type { InputProps } from './primitives/input';
export { Skeleton } from './primitives/skeleton';
+7
View File
@@ -0,0 +1,7 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
/** Conditional classes + last-wins conflict resolution for Tailwind utilities. */
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}
+29
View File
@@ -0,0 +1,29 @@
import { cva, type VariantProps } from 'class-variance-authority';
import type { HTMLAttributes } from 'react';
import { cn } from '../lib/cn';
export const badgeVariants = cva(
'inline-flex items-center gap-1 px-2 py-1 text-[0.625rem] font-semibold uppercase tracking-widest',
{
variants: {
variant: {
neutral: 'bg-ink-100 text-ink-700',
solid: 'bg-ink-950 text-white',
sale: 'bg-sale text-white',
new: 'bg-volt-500 text-ink-950',
success: 'bg-success text-white',
warning: 'bg-warning text-ink-950',
outline: 'border border-ink-300 text-ink-700',
},
},
defaultVariants: { variant: 'neutral' },
},
);
export interface BadgeProps
extends HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {}
export function Badge({ className, variant, ...props }: BadgeProps) {
return <span className={cn(badgeVariants({ variant }), className)} {...props} />;
}
+56
View File
@@ -0,0 +1,56 @@
import { cva, type VariantProps } from 'class-variance-authority';
import type { ButtonHTMLAttributes, Ref } from 'react';
import { cn } from '../lib/cn';
export const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors ' +
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ink-900 focus-visible:ring-offset-2 ' +
'disabled:pointer-events-none disabled:opacity-40',
{
variants: {
variant: {
primary: 'bg-ink-950 text-white hover:bg-ink-800',
secondary: 'bg-ink-100 text-ink-950 hover:bg-ink-200',
outline:
'border border-ink-950 bg-transparent text-ink-950 hover:bg-ink-950 hover:text-white',
ghost: 'bg-transparent text-ink-950 hover:bg-ink-100',
accent: 'bg-volt-500 text-ink-950 hover:bg-volt-400',
danger: 'bg-danger text-white hover:opacity-90',
},
size: {
sm: 'h-9 px-4 text-xs uppercase tracking-wide',
md: 'h-11 px-6 text-sm uppercase tracking-wide',
lg: 'h-14 px-8 text-sm uppercase tracking-widest',
icon: 'size-10',
},
shape: {
square: 'rounded-none',
rounded: 'rounded-card',
pill: 'rounded-pill',
},
fullWidth: {
true: 'w-full',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
shape: 'square',
},
},
);
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
ref?: Ref<HTMLButtonElement>;
}
export function Button({ className, variant, size, shape, fullWidth, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size, shape, fullWidth }), className)}
{...props}
/>
);
}
+24
View File
@@ -0,0 +1,24 @@
import type { InputHTMLAttributes, Ref } from 'react';
import { cn } from '../lib/cn';
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
invalid?: boolean;
ref?: Ref<HTMLInputElement>;
}
export function Input({ className, invalid, ...props }: InputProps) {
return (
<input
aria-invalid={invalid || undefined}
className={cn(
'border-ink-300 text-ink-950 h-11 w-full border bg-white px-4 text-sm transition-colors',
'placeholder:text-ink-400 focus:border-ink-950 focus:outline-none',
'disabled:bg-ink-50 disabled:cursor-not-allowed',
invalid && 'border-danger focus:border-danger',
className,
)}
{...props}
/>
);
}
+7
View File
@@ -0,0 +1,7 @@
import type { HTMLAttributes } from 'react';
import { cn } from '../lib/cn';
export function Skeleton({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
return <div className={cn('bg-ink-100 animate-pulse', className)} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "@sport/config/typescript/react-library.json",
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}