NextGraph integration (WIP), broker banner, and feature-based architecture

- Add NextGraph data layer with @ng-org/orm, SHEX shapes (Event, UserProfile,
  Participation), session management, and FestipodDataContext with dual-mode
  operation (connected via NextGraph or local seed data)
- Add BrokerBanner and NgStatus components showing connection status
- Refactor to feature-based architecture: organize code by business domain
  (event, user, home, auth, workshop, meeting, notification) instead of
  technical layer. Modules only import from shared/, never from each other
- Collocate BDD features and step definitions with their modules: event-specific
  steps in event/steps/, user steps in user/steps/, shared generic steps remain
  in shared/steps/
- Set up multi-layer BDD structure (frontend/backend/e2e steps per module)
- Add project documentation (AGENTS.md, .project/knowledge/)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-03-11 12:19:45 +01:00
parent c9bc957d2a
commit 901fd659df
128 changed files with 5738 additions and 2885 deletions
+30
View File
@@ -0,0 +1,30 @@
import React from 'react';
interface AvatarProps {
initials?: string;
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const sizeMap = {
sm: 32,
md: 40,
lg: 56,
};
export function Avatar({ initials = '?', size = 'md', className = '' }: AvatarProps) {
const pixelSize = sizeMap[size];
return (
<div
className={`sketchy-avatar ${className}`}
style={{
width: pixelSize,
height: pixelSize,
fontSize: pixelSize * 0.45,
}}
>
{initials}
</div>
);
}
+15
View File
@@ -0,0 +1,15 @@
import React from 'react';
interface BadgeProps {
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
}
export function Badge({ children, className = '', style }: BadgeProps) {
return (
<span className={`sketchy-badge ${className}`} style={style}>
{children}
</span>
);
}
@@ -0,0 +1,57 @@
import React from 'react';
import { useNextGraph } from '../../context/NextGraphContext';
export function BrokerBanner() {
const { status, connect } = useNextGraph();
const isConnected = status === 'connected';
const isConnecting = status === 'connecting';
const bgColor = isConnected ? '#4CAF50' : isConnecting ? '#FFB74D' : '#A5D6A7';
const textColor = isConnected ? 'white' : isConnecting ? 'white' : '#2E7D32';
return (
<div
style={{
background: bgColor,
color: textColor,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '6px 12px',
fontFamily: 'var(--font-sketch)',
fontSize: 13,
fontWeight: 'bold',
flexShrink: 0,
cursor: !isConnected && !isConnecting ? 'pointer' : 'default',
}}
onClick={!isConnected && !isConnecting ? connect : undefined}
title={isConnected ? 'Connecté à NextGraph' : isConnecting ? 'Connexion en cours...' : 'Cliquer pour se connecter à NextGraph'}
>
<span>
{isConnected ? 'NextGraph' : isConnecting ? 'Connexion...' : 'Se connecter'}
</span>
{isConnected && (
<button
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '2px 4px',
lineHeight: 1,
display: 'flex',
alignItems: 'center',
}}
title="Recharger"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.85)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 2v6h-6" />
<path d="M3 12a9 9 0 0 1 15-6.7L21 8" />
<path d="M3 22v-6h6" />
<path d="M21 12a9 9 0 0 1-15 6.7L3 16" />
</svg>
</button>
)}
</div>
);
}
+19
View File
@@ -0,0 +1,19 @@
import React from 'react';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'primary';
children: React.ReactNode;
}
export function Button({ variant = 'default', children, className = '', ...props }: ButtonProps) {
const variantClass = variant === 'primary' ? 'sketchy-btn-primary' : '';
return (
<button
className={`sketchy-btn ${variantClass} ${className}`}
{...props}
>
{children}
</button>
);
}
+20
View File
@@ -0,0 +1,20 @@
import React from 'react';
interface CardProps {
children: React.ReactNode;
className?: string;
onClick?: () => void;
style?: React.CSSProperties;
}
export function Card({ children, className = '', onClick, style }: CardProps) {
return (
<div
className={`sketchy-card ${className}`}
onClick={onClick}
style={{ ...(onClick ? { cursor: 'pointer' } : {}), ...style }}
>
{children}
</div>
);
}
@@ -0,0 +1,16 @@
import React from 'react';
interface CheckboxProps {
checked?: boolean;
onChange?: (checked: boolean) => void;
className?: string;
}
export function Checkbox({ checked = false, onChange, className = '' }: CheckboxProps) {
return (
<div
className={`sketchy-checkbox ${checked ? 'checked' : ''} ${className}`}
onClick={() => onChange?.(!checked)}
/>
);
}
@@ -0,0 +1,9 @@
import React from 'react';
interface DividerProps {
className?: string;
}
export function Divider({ className = '' }: DividerProps) {
return <div className={`sketchy-divider ${className}`} />;
}
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
interface HeaderProps {
title?: string;
left?: React.ReactNode;
right?: React.ReactNode;
className?: string;
}
export function Header({ title, left, right, className = '' }: HeaderProps) {
return (
<div className={`sketchy-header ${className}`}>
<div style={{ width: 40 }}>{left}</div>
<div className="sketchy-subtitle" style={{ margin: 0 }}>{title}</div>
<div style={{ width: 40, textAlign: 'right' }}>{right}</div>
</div>
);
}
+12
View File
@@ -0,0 +1,12 @@
import React from 'react';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
export function Input({ className = '', ...props }: InputProps) {
return (
<input
className={`sketchy-input ${className}`}
{...props}
/>
);
}
@@ -0,0 +1,18 @@
import React from 'react';
interface ListItemProps {
children: React.ReactNode;
onClick?: () => void;
className?: string;
}
export function ListItem({ children, onClick, className = '' }: ListItemProps) {
return (
<div
className={`sketchy-list-item ${className}`}
onClick={onClick}
>
{children}
</div>
);
}
+38
View File
@@ -0,0 +1,38 @@
import React from 'react';
interface NavItem {
icon: string;
label: string;
active?: boolean;
onClick?: () => void;
}
interface NavBarProps {
items: NavItem[];
className?: string;
}
export function NavBar({ items, className = '' }: NavBarProps) {
return (
<div className={`sketchy-navbar ${className}`}>
{items.map((item, index) => (
<div
key={index}
className={`nav-item ${item.active ? 'active' : ''}`}
onClick={item.onClick}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 4,
cursor: 'pointer',
opacity: item.active ? 1 : 0.6,
}}
>
<span style={{ fontSize: 20 }}>{item.icon}</span>
<span style={{ fontSize: 12 }}>{item.label}</span>
</div>
))}
</div>
);
}
@@ -0,0 +1,78 @@
import React from 'react';
import { useNextGraph } from '../../context/NextGraphContext';
export function NgStatus() {
const { status } = useNextGraph();
if (status === 'disconnected' || status === 'error') {
return (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 4,
fontSize: 11,
color: 'var(--sketch-gray)',
fontFamily: 'var(--font-sketch)',
}}
title="Mode démonstration — NextGraph non connecté"
>
<span style={{
width: 6,
height: 6,
borderRadius: '50%',
background: '#ccc',
display: 'inline-block',
}} />
démo
</div>
);
}
if (status === 'connecting') {
return (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 4,
fontSize: 11,
color: 'var(--sketch-gray)',
fontFamily: 'var(--font-sketch)',
}}
>
<span style={{
width: 6,
height: 6,
borderRadius: '50%',
background: '#ff9800',
display: 'inline-block',
}} />
connexion...
</div>
);
}
return (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 4,
fontSize: 11,
color: '#4caf50',
fontFamily: 'var(--font-sketch)',
}}
title="Connecté à NextGraph"
>
<span style={{
width: 6,
height: 6,
borderRadius: '50%',
background: '#4caf50',
display: 'inline-block',
}} />
NextGraph
</div>
);
}
@@ -0,0 +1,116 @@
import React from 'react';
interface PhoneFrameProps {
children: React.ReactNode;
scale?: number;
className?: string;
}
export function PhoneFrame({ children, scale = 1, className = '' }: PhoneFrameProps) {
// iPhone-like dimensions (375 x 812 logical pixels)
const width = 375;
const height = 812;
return (
<div
className={`phone-frame-wrapper ${className}`}
style={{
width: width * scale,
height: height * scale,
position: 'relative',
background: 'var(--sketch-white)',
borderRadius: 40 * scale,
border: `${3 * scale}px solid var(--sketch-black)`,
boxShadow: `${4 * scale}px ${4 * scale}px 0 var(--sketch-black)`,
overflow: 'hidden',
// Sketchy irregular border effect
borderTopLeftRadius: `${42 * scale}px`,
borderTopRightRadius: `${38 * scale}px`,
borderBottomLeftRadius: `${39 * scale}px`,
borderBottomRightRadius: `${41 * scale}px`,
}}
>
{/* Notch */}
<div
style={{
position: 'absolute',
top: 0,
left: '50%',
transform: 'translateX(-50%)',
width: 150 * scale,
height: 28 * scale,
background: 'var(--sketch-black)',
borderBottomLeftRadius: 14 * scale,
borderBottomRightRadius: 16 * scale,
zIndex: 10,
}}
/>
{/* Screen content */}
<div
style={{
width: '100%',
height: '100%',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
}}
>
{/* Status bar area */}
<div
style={{
height: 44 * scale,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: `0 ${20 * scale}px`,
fontSize: 12 * scale,
fontFamily: 'var(--font-sketch)',
flexShrink: 0,
color: 'var(--sketch-black)',
}}
>
<span>9:41</span>
<span style={{ display: 'flex', gap: 4 * scale }}>
<span>~</span>
<span>|</span>
<span>|</span>
</span>
</div>
{/* Main content area */}
<div
className="phone-screen"
style={{
flex: 1,
overflow: 'auto',
display: 'flex',
flexDirection: 'column',
}}
>
{children}
</div>
{/* Home indicator */}
<div
style={{
height: 34 * scale,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}
>
<div
style={{
width: 134 * scale,
height: 5 * scale,
background: 'var(--sketch-black)',
borderRadius: 3 * scale,
}}
/>
</div>
</div>
</div>
);
}
@@ -0,0 +1,26 @@
import React from 'react';
interface PlaceholderProps {
width?: string | number;
height?: string | number;
label?: string;
className?: string;
style?: React.CSSProperties;
}
export function Placeholder({
width = '100%',
height = 100,
label = 'Image',
className = '',
style
}: PlaceholderProps) {
return (
<div
className={`sketchy-placeholder ${className}`}
style={{ width, height, ...style }}
>
{label}
</div>
);
}
+20
View File
@@ -0,0 +1,20 @@
import React from 'react';
interface TextProps {
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
onClick?: () => void;
}
export function Title({ children, className = '', style }: TextProps) {
return <h1 className={`sketchy-title ${className}`} style={style}>{children}</h1>;
}
export function Subtitle({ children, className = '', style }: TextProps) {
return <h2 className={`sketchy-subtitle ${className}`} style={style}>{children}</h2>;
}
export function Text({ children, className = '', style, onClick }: TextProps) {
return <p className={`sketchy-text ${className}`} style={style} onClick={onClick}>{children}</p>;
}
+16
View File
@@ -0,0 +1,16 @@
import React from 'react';
interface ToggleProps {
checked?: boolean;
onChange?: (checked: boolean) => void;
className?: string;
}
export function Toggle({ checked = false, onChange, className = '' }: ToggleProps) {
return (
<div
className={`sketchy-toggle ${checked ? 'on' : ''} ${className}`}
onClick={() => onChange?.(!checked)}
/>
);
}
+15
View File
@@ -0,0 +1,15 @@
export { Button } from './Button';
export { Input } from './Input';
export { Card } from './Card';
export { Title, Subtitle, Text } from './Text';
export { Placeholder } from './Placeholder';
export { Avatar } from './Avatar';
export { Badge } from './Badge';
export { Toggle } from './Toggle';
export { Checkbox } from './Checkbox';
export { ListItem } from './ListItem';
export { Header } from './Header';
export { NavBar } from './NavBar';
export { Divider } from './Divider';
export { PhoneFrame } from './PhoneFrame';
export { BrokerBanner } from './BrokerBanner';
+52
View File
@@ -0,0 +1,52 @@
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/shared/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />;
}
export { Button, buttonVariants };
+56
View File
@@ -0,0 +1,56 @@
import * as React from "react";
import { cn } from "@/shared/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", className)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-title" className={cn("leading-none font-semibold", className)} {...props} />;
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-description" className={cn("text-muted-foreground text-sm", className)} {...props} />;
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-content" className={cn("px-6", className)} {...props} />;
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="card-footer" className={cn("flex items-center px-6 [.border-t]:pt-6", className)} {...props} />
);
}
export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
+21
View File
@@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "@/shared/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
/>
);
}
export { Input };
+21
View File
@@ -0,0 +1,21 @@
"use client";
import * as LabelPrimitive from "@radix-ui/react-label";
import * as React from "react";
import { cn } from "@/shared/lib/utils";
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className,
)}
{...props}
/>
);
}
export { Label };
+162
View File
@@ -0,0 +1,162 @@
"use client";
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import * as React from "react";
import { cn } from "@/shared/lib/utils";
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = "popper",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
);
}
function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};
+18
View File
@@ -0,0 +1,18 @@
import * as React from "react";
import { cn } from "@/shared/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
{...props}
/>
);
}
export { Textarea };
+44
View File
@@ -0,0 +1,44 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/shared/lib/utils";
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return <TooltipPrimitive.Provider delayDuration={delayDuration} {...props} />;
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return <TooltipPrimitive.Root {...props} />;
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger {...props} />;
}
function TooltipContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };