Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lumyx/ui",
"version": "0.2.0",
"version": "0.3.0",
"description": "The Lumyx design system: React components and Tailwind CSS v4 tokens.",
"license": "MIT",
"author": "Mathieu Chambaud",
Expand Down
33 changes: 33 additions & 0 deletions scripts/verify-ds.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,39 @@ for (const size of uniqueScale) {
}
}

// 7-9. Les namespaces par defaut de Tailwind restent actifs (styles.css ne les remet pas a
// zero) : rien n'empeche a la compilation un `text-sm`, un `bg-white` ou un `duration-300`.
// Ces regles ferment la porte cote source.
const CODE_FILES = files.filter((f) => ['.ts', '.tsx', '.js'].includes(extname(f)));
function scanClasses(rule, pattern, accept = () => false) {
for (const f of CODE_FILES) {
readFileSync(f, 'utf8')
.split('\n')
.forEach((line, i) => {
for (const m of line.matchAll(pattern)) {
if (!accept(m)) fail(rule, `${rel(f)}:${i + 1} — ${m[0]}`);
}
});
}
}

// 7. Taille de texte sur l'echelle numerique (text-11 … text-44), jamais l'echelle Tailwind.
scanClasses('type-scale', /(?<![\w-])text-(?:xs|sm|base|lg|xl|[2-9]xl)(?![\w-])/g);

// 8. Couleur par token semantique : ni black/white, ni la palette par defaut de Tailwind.
const PALETTE =
'slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose';
const COLOR_UTILS =
'bg|text|border(?:-[trblxyse])?|ring|ring-offset|outline|fill|stroke|from|via|to|divide|placeholder|caret|accent|decoration|shadow';
scanClasses(
'no-raw-palette',
new RegExp(`(?<![\\w-])(?:${COLOR_UTILS})-(?:black|white|(?:${PALETTE})-\\d{2,3})(?![\\w-])`, 'g'),
);

// 9. Durees de motion : 120ms teintes · 180ms par defaut · 260ms taille & position (styles.css).
const DURATIONS = new Set(['0', '120', '180', '260']);
scanClasses('motion-duration', /(?<![\w-])duration-(\d+|\[[^\]]*\])/g, (m) => DURATIONS.has(m[1]));

if (failures.length) {
console.error(`\n✗ ${failures.length} violation(s) des contraintes du design system\n`);
failures.forEach((f) => console.error(' ' + f));
Expand Down
44 changes: 44 additions & 0 deletions scripts/verify-ds.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,47 @@ describe('verify-ds regle 2 — monospace par token', () => {
expect(proc.exitCode).toBe(0);
});
});

describe('verify-ds regle 7 — echelle typographique', () => {
test('accepte l echelle numerique', () => {
expect(runWith('export const A = () => <p className="text-13 text-muted" />;\n').code).toBe(0);
});

test('refuse l echelle Tailwind par defaut', () => {
const { code, output } = runWith('export const A = () => <p className="text-sm" />;\n');
expect(code).toBe(1);
expect(output).toContain('type-scale');
});
});

describe('verify-ds regle 8 — couleurs par token', () => {
test('accepte les utilitaires semantiques', () => {
expect(runWith('export const A = () => <p className="bg-danger text-on-accent" />;\n').code).toBe(0);
});

test('refuse black / white', () => {
const { code, output } = runWith('export const A = () => <p className="bg-black/50 text-white" />;\n');
expect(code).toBe(1);
expect(output).toContain('no-raw-palette');
});

test('refuse la palette Tailwind par defaut', () => {
expect(runWith('export const A = () => <p className="border-zinc-200" />;\n').code).toBe(1);
});
});

describe('verify-ds regle 9 — durees de motion', () => {
test('accepte 120 / 180 / 260', () => {
expect(runWith('export const A = () => <p className="duration-120 hover:duration-260" />;\n').code).toBe(0);
});

test('refuse une duree hors systeme', () => {
const { code, output } = runWith('export const A = () => <p className="duration-300" />;\n');
expect(code).toBe(1);
expect(output).toContain('motion-duration');
});

test('refuse une valeur arbitraire', () => {
expect(runWith('export const A = () => <p className="duration-[120ms]" />;\n').code).toBe(1);
});
});
2 changes: 1 addition & 1 deletion src/components/lumyx/peer-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type Peer = {
/** Peers are indigo. Rooms and sessions are coral. They are never blended. */
export function PeerCard({ peer, className }: { peer: Peer; className?: string }) {
return (
<div className={cn("rounded-lg border border-hairline bg-card p-4 shadow-[var(--shadow-sm)] transition-shadow duration-[180ms] hover:shadow-[var(--shadow-md)]", className)}>
<div className={cn("rounded-lg border border-hairline bg-card p-4 shadow-[var(--shadow-sm)] transition-shadow duration-180 hover:shadow-[var(--shadow-md)]", className)}>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<StatusDot status={peer.status} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/lumyx/room-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type Room = {
export function RoomCard({ room, href, className }: { room: Room; href?: string; className?: string }) {
const { Link } = useLumyxLink();
const body = (
<div className={cn("rounded-lg border border-hairline bg-card p-5 shadow-[var(--shadow-sm)] transition-shadow duration-[180ms] hover:shadow-[var(--shadow-md)]", className)}>
<div className={cn("rounded-lg border border-hairline bg-card p-5 shadow-[var(--shadow-sm)] transition-shadow duration-180 hover:shadow-[var(--shadow-md)]", className)}>
<div className="flex items-start justify-between gap-3">
<div className="flex flex-col gap-1">
<span className="flex items-center gap-2">
Expand Down
2 changes: 1 addition & 1 deletion src/components/lumyx/sidebar-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function SidebarNav({
asChild
isActive={active}
className={cn(
"h-auto gap-2.5 rounded-sm px-2 py-1.5 text-13 transition-colors duration-[120ms]",
"h-auto gap-2.5 rounded-sm px-2 py-1.5 text-13 transition-colors duration-120",
active
? "bg-accent-tint font-medium text-accent-text shadow-[inset_2px_0_0_var(--accent)] hover:bg-accent-tint hover:text-accent-text"
: "text-body hover:bg-hover hover:text-strong",
Expand Down
4 changes: 2 additions & 2 deletions src/components/lumyx/time-series-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function TimeSeriesChart({
{ticks.map((t) => (
<g key={t}>
<line x1={padL} x2={W - padR} y1={y(t)} y2={y(t)} stroke="var(--border-subtle)" strokeWidth="1" />
<text x={0} y={y(t) + 3} fontSize="10" fill="var(--text-faint)" style={{ fontVariantNumeric: "tabular-nums" }}>
<text x={0} y={y(t) + 3} fontSize="11" fill="var(--text-faint)" style={{ fontVariantNumeric: "tabular-nums" }}>
{Math.round(t)}{yUnit}
</text>
</g>
Expand All @@ -38,7 +38,7 @@ export function TimeSeriesChart({
return <polyline key={s.name} points={pts} fill="none" stroke={s.color} strokeWidth="1.75" strokeLinejoin="round" strokeLinecap="round" />;
})}
{xLabels.map((l, i) => (
<text key={l + i} x={x((i / Math.max(xLabels.length - 1, 1)) * (len - 1))} y={H - 4} fontSize="10" fill="var(--text-faint)" textAnchor="middle">
<text key={l + i} x={x((i / Math.max(xLabels.length - 1, 1)) * (len - 1))} y={H - 4} fontSize="11" fill="var(--text-faint)" textAnchor="middle">
{l}
</text>
))}
Expand Down
2 changes: 1 addition & 1 deletion src/components/lumyx/video-tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function VideoTile({
<span className="text-12">No stream</span>
</div>
)}
<span className="absolute bottom-3 left-3 flex items-center gap-2 rounded-pill bg-[var(--surface-overlay)] px-2.5 py-1 text-11 text-white backdrop-blur-[6px]">
<span className="absolute bottom-3 left-3 flex items-center gap-2 rounded-pill bg-[var(--surface-overlay)] px-2.5 py-1 text-11 text-on-overlay backdrop-blur-[6px]">
<StatusDot status={status} />
<span className="sl-num">{peerId}</span>
{latencyMs != null ? <span className="sl-num opacity-70">{latencyMs}ms</span> : null}
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import { cn } from '../../lib/utils';
* Focus is the 3px indigo ring, never a browser outline.
*/
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors duration-[120ms] ease-[var(--ease-out)] outline-none focus-visible:border-accent focus-visible:shadow-[var(--ring-accent)] disabled:opacity-45 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:size-4 [&_svg]:stroke-[1.75]",
"inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors duration-120 ease-[var(--ease-out)] outline-none focus-visible:border-accent focus-visible:shadow-[var(--ring-accent)] disabled:opacity-45 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:size-4 [&_svg]:stroke-[1.75]",
{
variants: {
variant: {
primary: "bg-accent text-on-accent border border-transparent hover:bg-accent-hover shadow-[var(--shadow-xs)]",
secondary: "bg-card text-strong border border-stroke hover:bg-hover shadow-[var(--shadow-xs)]",
ghost: "bg-transparent text-body border border-transparent hover:bg-hover",
danger: "bg-danger text-white border border-transparent hover:brightness-95 focus-visible:shadow-[var(--ring-danger)]",
danger: "bg-danger text-on-accent border border-transparent hover:brightness-95 focus-visible:shadow-[var(--ring-danger)]",
link: "bg-transparent text-accent-text underline-offset-2 hover:underline p-0 h-auto",
},
size: {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function DialogContent({ className, children, ...props }: React.ComponentProps<t
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-xs p-1 text-muted transition-colors duration-[120ms] hover:bg-hover hover:text-strong outline-none focus-visible:shadow-[var(--ring-accent)]">
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-xs p-1 text-muted transition-colors duration-120 hover:bg-hover hover:text-strong outline-none focus-visible:shadow-[var(--ring-accent)]">
<X className="size-4 stroke-[1.75]" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
data-slot="input"
className={cn(
"h-9 w-full rounded-sm border border-stroke bg-card px-3 text-13 text-strong shadow-[var(--shadow-xs)]",
"placeholder:text-faint transition-colors duration-[120ms]",
"placeholder:text-faint transition-colors duration-120",
"outline-none focus-visible:border-accent focus-visible:shadow-[var(--ring-accent)]",
"disabled:opacity-45 disabled:cursor-not-allowed",
"aria-invalid:border-danger aria-invalid:focus-visible:shadow-[var(--ring-danger)]",
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/progress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function Progress({
{...props}
>
<ProgressPrimitive.Indicator
className={cn("h-full rounded-pill bg-accent transition-[width] duration-[260ms] ease-[var(--ease-out)]", indicatorClassName)}
className={cn("h-full rounded-pill bg-accent transition-[width] duration-260 ease-[var(--ease-out)]", indicatorClassName)}
style={{ width: `${value ?? 0}%` }}
/>
</ProgressPrimitive.Root>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function SelectTrigger({ className, children, ...props }: React.ComponentProps<t
data-slot="select-trigger"
className={cn(
"flex h-9 w-full items-center justify-between gap-2 rounded-sm border border-stroke bg-card px-3 text-13 text-strong",
"shadow-[var(--shadow-xs)] outline-none transition-colors duration-[120ms]",
"shadow-[var(--shadow-xs)] outline-none transition-colors duration-120",
"focus-visible:border-accent focus-visible:shadow-[var(--ring-accent)] disabled:opacity-45 disabled:cursor-not-allowed",
className
)}
Expand Down
6 changes: 3 additions & 3 deletions src/components/ui/sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function SheetOverlay({
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
"fixed inset-0 z-50 bg-overlay backdrop-blur-[6px] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
className
)}
{...props}
Expand All @@ -60,7 +60,7 @@ function SheetContent({
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
"fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-260 data-[state=open]:animate-in data-[state=open]:duration-260",
side === "right" &&
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
side === "left" &&
Expand Down Expand Up @@ -125,7 +125,7 @@ function SheetDescription({
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-sm text-muted-foreground", className)}
className={cn("text-13 text-muted-foreground", className)}
{...props}
/>
)
Expand Down
22 changes: 11 additions & 11 deletions src/components/ui/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ function Sidebar({
<div
data-slot="sidebar-gap"
className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-260 ease-out",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
Expand All @@ -229,7 +229,7 @@ function Sidebar({
<div
data-slot="sidebar-container"
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear lg:flex",
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-260 ease-out lg:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
Expand Down Expand Up @@ -405,7 +405,7 @@ function SidebarGroupLabel({
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"flex h-8 shrink-0 items-center rounded-md px-2 text-12 font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-260 ease-out focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
Expand Down Expand Up @@ -445,7 +445,7 @@ function SidebarGroupContent({
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
className={cn("w-full text-13", className)}
{...props}
/>
)
Expand Down Expand Up @@ -474,7 +474,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
}

const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-13 ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
Expand All @@ -483,9 +483,9 @@ const sidebarMenuButtonVariants = cva(
"bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
default: "h-8 text-13",
sm: "h-7 text-12",
lg: "h-12 text-13 group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
Expand Down Expand Up @@ -586,7 +586,7 @@ function SidebarMenuBadge({
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none",
"pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-12 font-medium text-sidebar-foreground tabular-nums select-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
Expand Down Expand Up @@ -688,8 +688,8 @@ function SidebarMenuSubButton({
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
size === "sm" && "text-12",
size === "md" && "text-13",
"group-data-[collapsible=icon]:hidden",
className
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
className={cn("relative overflow-hidden rounded-sm bg-inset", className)}
{...props}
>
<div className="absolute inset-0 animate-[var(--animate-shimmer)] bg-gradient-to-r from-transparent via-black/[.04] to-transparent" />
<div className="absolute inset-0 animate-[var(--animate-shimmer)] bg-gradient-to-r from-transparent via-active to-transparent" />
</div>
);
}
Expand Down
Loading
Loading