diff --git a/AGENTS.md b/AGENTS.md index ef479df..449877a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,6 +55,14 @@ For `skills/`: - when updating an existing skill, keep `SKILL.md`, `agents/openai.yaml`, and bundled assets consistent with each other - if you add or materially change a project skill, update [docs/implementation-roadmap.md](/home/kurihada/project/ai-workflow-skill/docs/implementation-roadmap.md) in the same change +## Frontend UI Reuse + +For frontend work in this repository: + +- default to reusing Cadence UI source-owned components before building custom UI pieces +- business pages and feature flows may compose existing components freely, but do not introduce new foundational UI primitives when the need can be met by existing Cadence UI components or by installing additional Cadence UI components +- if a task appears to require a genuinely new foundational UI primitive, explicitly tell the user before creating it instead of adding it silently + ## Sub-Agent Delegation This repository allows sub-agent delegation for parallel implementation work. diff --git a/apps/web/package.json b/apps/web/package.json index 103acf7..ad874cc 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,10 +10,18 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", "@tanstack/react-query": "^5.91.2", "@tanstack/react-router": "^1.167.5", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "motion": "^12.38.0", "react": "^19.2.4", - "react-dom": "^19.2.4" + "react-dom": "^19.2.4", + "react-hook-form": "^7.71.2", + "tailwind-merge": "^3.5.0" }, "devDependencies": { "@types/react": "^19.2.14", diff --git a/apps/web/src/cadence-ui/.install-manifest.json b/apps/web/src/cadence-ui/.install-manifest.json new file mode 100644 index 0000000..bf0073d --- /dev/null +++ b/apps/web/src/cadence-ui/.install-manifest.json @@ -0,0 +1,31 @@ +{ + "items": [ + "alert", + "badge", + "button", + "card", + "dialog", + "form", + "input", + "tabs", + "textarea", + "tokens" + ], + "packageDependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "motion": "^12.38.0", + "react": "^18.3.1 || ^19.0.0", + "react-hook-form": "^7.71.2", + "tailwind-merge": "^3.5.0" + }, + "registry": "cadence-ui", + "sourcePackages": { + "@ai-ui/tokens": "0.0.0", + "@ai-ui/ui": "0.0.0" + }, + "targetDir": "src/cadence-ui" +} diff --git a/apps/web/src/cadence-ui/components/alert.tsx b/apps/web/src/cadence-ui/components/alert.tsx new file mode 100644 index 0000000..0db021d --- /dev/null +++ b/apps/web/src/cadence-ui/components/alert.tsx @@ -0,0 +1,77 @@ +import { forwardRef } from "react"; + +import { + alertDescriptionVariants, + alertIconVariants, + alertTitleVariants, + alertVariants +} from "./alert.variants"; +import { cn } from "../lib/cn"; +import type { VariantProps } from "../lib/cva"; +import { createDataAttributes, createSlot } from "../lib/contracts"; + +export type AlertProps = React.ComponentPropsWithoutRef<"div"> & + VariantProps & { + icon?: React.ReactNode; + }; + +export const Alert = forwardRef(function Alert( + { + children, + className, + icon, + variant = "default", + ...props + }, + ref +) { + const hasIcon = Boolean(icon); + + return ( +
+ {hasIcon ? {icon} : null} + {children} +
+ ); +}); + +export type AlertTitleProps = React.ComponentPropsWithoutRef<"h4">; + +export const AlertTitle = forwardRef(function AlertTitle( + { className, ...props }, + ref +) { + return ( +

+ ); +}); + +export type AlertDescriptionProps = React.ComponentPropsWithoutRef<"div">; + +export const AlertDescription = forwardRef( + function AlertDescription({ className, ...props }, ref) { + return ( +
+ ); + } +); diff --git a/apps/web/src/cadence-ui/components/alert.variants.ts b/apps/web/src/cadence-ui/components/alert.variants.ts new file mode 100644 index 0000000..f778e79 --- /dev/null +++ b/apps/web/src/cadence-ui/components/alert.variants.ts @@ -0,0 +1,44 @@ +import { cva } from "../lib/cva"; +import { getMotionRecipeClassNames } from "../lib/motion"; + +export const alertVariants = cva( + [ + "relative grid gap-x-3 gap-y-1 rounded-[var(--radius-lg)] border p-4 shadow-[var(--shadow-xs)]", + "text-[var(--color-foreground)]", + getMotionRecipeClassNames("transition", "ring") + ], + { + variants: { + variant: { + default: + "border-[var(--color-border)] bg-[var(--color-card)]", + success: + "border-[color-mix(in_oklch,var(--color-success)_34%,var(--color-border))] bg-[color-mix(in_oklch,var(--color-success)_10%,var(--color-card))]", + warning: + "border-[color-mix(in_oklch,var(--color-warning)_34%,var(--color-border))] bg-[color-mix(in_oklch,var(--color-warning)_12%,var(--color-card))]", + destructive: + "border-[color-mix(in_oklch,var(--color-destructive)_38%,var(--color-border))] bg-[color-mix(in_oklch,var(--color-destructive)_10%,var(--color-card))]" + }, + hasIcon: { + false: "", + true: "grid-cols-[auto_1fr]" + } + }, + defaultVariants: { + variant: "default", + hasIcon: false + } + } +); + +export const alertIconVariants = cva( + "row-span-2 mt-0.5 inline-flex size-5 items-center justify-center rounded-[var(--radius-full)] text-[var(--color-muted-foreground)]" +); + +export const alertTitleVariants = cva( + "text-sm font-semibold tracking-[var(--tracking-tight)] text-[var(--color-foreground)]" +); + +export const alertDescriptionVariants = cva( + "text-sm leading-6 text-[var(--color-muted-foreground)]" +); diff --git a/apps/web/src/cadence-ui/components/badge.tsx b/apps/web/src/cadence-ui/components/badge.tsx new file mode 100644 index 0000000..5dbd871 --- /dev/null +++ b/apps/web/src/cadence-ui/components/badge.tsx @@ -0,0 +1,42 @@ +import { Slot, Slottable } from "@radix-ui/react-slot"; +import { forwardRef } from "react"; + +import { badgeVariants } from "./badge.variants"; +import { cn } from "../lib/cn"; +import type { VariantProps } from "../lib/cva"; +import { createDataAttributes, createSlot, type AsChildProp } from "../lib/contracts"; + +export type BadgeProps = React.ComponentPropsWithoutRef<"span"> & + AsChildProp & + VariantProps; + +export const Badge = forwardRef(function Badge( + { + asChild = false, + children, + className, + size, + tone, + variant, + ...props + }, + ref +) { + const Component = asChild ? Slot : "span"; + + return ( + + {asChild ? {children} : {children}} + + ); +}); diff --git a/apps/web/src/cadence-ui/components/badge.variants.ts b/apps/web/src/cadence-ui/components/badge.variants.ts new file mode 100644 index 0000000..c67c98f --- /dev/null +++ b/apps/web/src/cadence-ui/components/badge.variants.ts @@ -0,0 +1,42 @@ +import { cva } from "../lib/cva"; +import { getMotionRecipeClassNames } from "../lib/motion"; + +export const badgeVariants = cva( + [ + "inline-flex shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-[var(--radius-full)] border font-medium", + "outline-none select-none", + getMotionRecipeClassNames("transition", "ring") + ], + { + variants: { + size: { + sm: "min-h-6 px-2 py-0.5 text-[0.7rem]", + md: "min-h-7 px-2.5 py-1 text-xs" + }, + variant: { + subtle: + "border-[var(--color-border)] bg-[var(--color-card)] text-[var(--color-foreground)]", + solid: + "border-transparent bg-[var(--color-foreground)] text-[var(--color-background)]", + outline: + "border-[var(--color-border-strong)] bg-transparent text-[var(--color-foreground)]" + }, + tone: { + neutral: "", + primary: + "data-[variant=subtle]:bg-[color-mix(in_oklch,var(--color-primary)_14%,var(--color-card))] data-[variant=subtle]:text-[var(--color-primary)] data-[variant=solid]:bg-[var(--color-primary)] data-[variant=solid]:text-[var(--color-primary-foreground)] data-[variant=outline]:border-[color-mix(in_oklch,var(--color-primary)_38%,var(--color-border-strong))] data-[variant=outline]:text-[var(--color-primary)]", + success: + "data-[variant=subtle]:bg-[color-mix(in_oklch,var(--color-success)_14%,var(--color-card))] data-[variant=subtle]:text-[color-mix(in_oklch,var(--color-success)_78%,var(--color-foreground))] data-[variant=solid]:bg-[var(--color-success)] data-[variant=solid]:text-[var(--color-success-foreground)] data-[variant=outline]:border-[color-mix(in_oklch,var(--color-success)_38%,var(--color-border-strong))] data-[variant=outline]:text-[color-mix(in_oklch,var(--color-success)_72%,var(--color-foreground))]", + warning: + "data-[variant=subtle]:bg-[color-mix(in_oklch,var(--color-warning)_18%,var(--color-card))] data-[variant=subtle]:text-[color-mix(in_oklch,var(--color-warning)_70%,var(--color-foreground))] data-[variant=solid]:bg-[var(--color-warning)] data-[variant=solid]:text-[var(--color-warning-foreground)] data-[variant=outline]:border-[color-mix(in_oklch,var(--color-warning)_40%,var(--color-border-strong))] data-[variant=outline]:text-[color-mix(in_oklch,var(--color-warning)_70%,var(--color-foreground))]", + destructive: + "data-[variant=subtle]:bg-[color-mix(in_oklch,var(--color-destructive)_12%,var(--color-card))] data-[variant=subtle]:text-[var(--color-destructive)] data-[variant=solid]:bg-[var(--color-destructive)] data-[variant=solid]:text-[var(--color-destructive-foreground)] data-[variant=outline]:border-[color-mix(in_oklch,var(--color-destructive)_38%,var(--color-border-strong))] data-[variant=outline]:text-[var(--color-destructive)]" + } + }, + defaultVariants: { + size: "md", + tone: "neutral", + variant: "subtle" + } + } +); diff --git a/apps/web/src/cadence-ui/components/button.tsx b/apps/web/src/cadence-ui/components/button.tsx new file mode 100644 index 0000000..cf39cad --- /dev/null +++ b/apps/web/src/cadence-ui/components/button.tsx @@ -0,0 +1,120 @@ +import { Slot, Slottable } from "@radix-ui/react-slot"; +import { forwardRef, useState } from "react"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; + +import { buttonVariants } from "./button.variants"; +import { cn } from "../lib/cn"; +import type { VariantProps } from "../lib/cva"; +import type { ButtonLikeElementProps } from "../lib/contracts"; +import { createDataAttributes, createSlot } from "../lib/contracts"; + +export type ButtonProps = Omit< + ButtonLikeElementProps, + "onDrag" | "onDragEnd" | "onDragStart" +> & + VariantProps; + +function Spinner() { + return ( +