@codapet/design-system 0.7.4 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,132 @@
1
+ # AGENTS.md — `@codapet/design-system`
2
+
3
+ Guidance for AI coding agents working in **consumer codebases** that import `@codapet/design-system`. Read this before reaching for shadcn docs or muscle memory — most things are the same, but a handful of defaults and APIs are not, and the install/publish model is different.
4
+
5
+ > This file is published with the npm package. Consumer repos pull it into agent context via `@./node_modules/@codapet/design-system/AGENTS.md` in their own `CLAUDE.md` / `AGENTS.md`. Don't edit it in a consumer repo — fixes belong upstream in the design-system repo.
6
+
7
+ ## Mental model
8
+
9
+ This is a **shadcn/ui-style** library (Radix primitives + cva variants + Tailwind), but it ships as a **single published npm package** — not copy-paste components. So:
10
+
11
+ - ✅ `import { Button } from '@codapet/design-system'`
12
+ - ❌ Don't `npx shadcn add ...`. Don't copy components into the consumer repo. Add new variants by extending via `className` or, if missing, propose them upstream.
13
+ - ✅ Most shadcn examples translate 1:1 — same `data-slot` attrs, same compound-component patterns (`Card` / `CardHeader` / `CardContent`, `Dialog.Trigger` / `Dialog.Content`, etc.).
14
+ - 'use client' is **already baked in** to every export by the build — never wrap design-system components in your own `'use client'` boundary just for that reason. RSCs that pass props down still work.
15
+ - ESM-only. If a consumer uses Jest, add the package to `transformIgnorePatterns` (or use Vitest, which handles it).
16
+ - Single entry: `from '@codapet/design-system'`. There are **no subpath component imports** — only `'@codapet/design-system'` and `'@codapet/design-system/styles'` exist.
17
+
18
+ ## Required setup in a consumer (Tailwind v4)
19
+
20
+ In the app's global CSS:
21
+
22
+ ```css
23
+ @import 'tailwindcss';
24
+ @source "../node_modules/@codapet/design-system/dist/**/*.{js,mjs,ts,tsx}";
25
+ @import '@codapet/design-system/styles';
26
+ ```
27
+
28
+ The `@source` line is **mandatory** — without it Tailwind won't see the class names used inside the package and components render unstyled. Tailwind v3 consumers add the same path under `content` in `tailwind.config.js`.
29
+
30
+ Wrap the app in `<ThemeProvider>` (re-export of `next-themes` with `attribute="class"`, `defaultTheme="light"`, `enableSystem`, `disableTransitionOnChange` pre-set), and mount `<Toaster />` once at the root. Both come from the package.
31
+
32
+ Required font CSS variables (set on `<body>` or `<html>`): `--font-plus-jakarta-sans` (sans), `--font-noto-serif` (serif, used by display headings — italic), `--font-geist-mono`. Without these, `font-sans`/`font-serif`/`font-mono` fall back to the browser default.
33
+
34
+ ## Differences from shadcn defaults
35
+
36
+ These are the foot-guns. Knowing them prevents most "why does my Button look wrong" loops.
37
+
38
+ ### Button
39
+
40
+ - **Default `variant` is `primary`**, not `default`. There is no `default` variant. Available: `primary | secondary | tertiary | outline | ghost | ghost-secondary | ghost-destructive | link | destructive | destructive-secondary | destructive-tertiary`.
41
+ - **Default `size` is `lg` (h-12)**, not `default` (h-9). Sizes: `sm` (h-9) · `md` (h-10) · `lg` (h-12) · `icon` (size-8). For a typical inline action, you usually want `size="md"` — passing nothing gives you a chunky button.
42
+ - Has `cursor-pointer` baked in (shadcn doesn't).
43
+
44
+ ### Input
45
+
46
+ - Custom `size` prop: `sm` (h-10) · `md` (h-12, default) · `lg` (h-14). All bigger than shadcn's h-9.
47
+ - Built-in `leftIcon`, `rightIcon`, `rightIconOnClick`, `error` props — don't wrap Input in your own icon container, use these. `rightIcon` renders inside a `Button` (clickable); `leftIcon` is decorative.
48
+ - Pass `error={true}` to switch to the error color scheme; it also sets `aria-invalid`.
49
+
50
+ ### Textarea
51
+
52
+ - Same `error` prop pattern as Input.
53
+ - For auto-grow, use `AutoResizeTextarea` (custom, takes `minHeight` / `maxHeight` in px) — don't reach for `field-sizing-content` manually.
54
+
55
+ ### Toast
56
+
57
+ - Import `toast` and `Toaster` from `@codapet/design-system`, **not** from `'sonner'`. The exported `Toaster` is pre-styled to match alert tokens; using sonner directly will produce off-brand toasts.
58
+ - Mount `<Toaster />` once in the root layout.
59
+
60
+ ### Form
61
+
62
+ - Standard shadcn pattern: `react-hook-form` + `zod` + `Form / FormField / FormItem / FormLabel / FormControl / FormMessage`. Same API as shadcn — no surprises here.
63
+
64
+ ### Accordion
65
+
66
+ - Adds a `variant` prop on `<Accordion>`: `default` (shadcn-equivalent, bottom-stripe items) or `outlined` (each item is its own bordered card with taller trigger and roomier padding). Reach for `variant="outlined"` on FAQ sections, settings panels, or any "stacked cards" pattern.
67
+ - Variant propagates to children via context — set it once on the root, not on every `AccordionItem`/`Trigger`/`Content`.
68
+ - Plus/Minus (or any custom) icons are opt-in via `collapsedIcon`/`expandedIcon` on `AccordionTrigger`. The default remains a rotating `ChevronDown`.
69
+
70
+ ### Dialog vs SmartDialog
71
+
72
+ - `Dialog`/`Drawer` are the standard Radix/Vaul primitives.
73
+ - **`SmartDialog*`** is a CodaPet addition: same API surface, but renders `Drawer` on `≤600px` and `Dialog` above. Prefer it for any modal that should bottom-sheet on mobile. Replace every `Dialog` token with `SmartDialog` (`SmartDialogTrigger`, `SmartDialogContent`, etc.).
74
+
75
+ ## Components added on top of shadcn
76
+
77
+ These don't exist in shadcn — reach for them instead of building your own:
78
+
79
+ | Component | Use when |
80
+ |---|---|
81
+ | `AlertBanner` | Inline page-level alerts with `type` = `informative` / `error` / `success`, optional `heading`, `icon`, `dismissible`. Distinct from `Alert` (shadcn-equivalent). |
82
+ | `BadgeActionable` | Clickable chip/filter badge with `selected` state and `onBackground` modifier. |
83
+ | `BadgeInformative` (+ `Group` / `Item`) | Read-only info badge with `colorScheme` = `gray` / `blue` / `yellow`. Use `Group` + `Item` for multi-content badges in one container. |
84
+ | `BadgeNumber` | Numeric pill (counts, step indicators). `state` = `active` / `disabled` / `resting`. |
85
+ | `OptionCard` | Selectable card with built-in radio/checkbox indicator. `selectionType` = `single` / `multiple`, `selectorPosition` = `left` / `right`. Visual-only — wire `selected` and `onClick` yourself. |
86
+ | `DropdownSelect` | Compound `DropdownSelect` / `Trigger` / `Content` / `Option` / `Label`. Lighter alternative to `Select` for simple lists. |
87
+ | `SearchableSelect` | Combobox-style select with search; supports `mode="single"` or `"multiple"` + `maxCount` for tag overflow. |
88
+ | `MultiSelectFreeText` | Tag input where users can type free text **and** pick from suggestions. |
89
+ | `SearchInput` | Search field with `variant="icon"` or `"button"`, suggestions dropdown, and clear button. Don't compose this from `Input` + a Search icon. |
90
+ | `DateInput` / `DateRangeInput` | Text input + Calendar popover. Controlled via `date`/`setDate` (or `dateRange`/`setDateRange`). Configurable `dateFormat` (15 options including `'MMM D, YYYY'`, `'DD/MM/YYYY'`, etc.). Prefer over a bare `Calendar`. |
91
+ | `TimeInput` | Time picker with `timeFormat` = `'12h' \| '24h' \| 'h:mm a' \| 'h:mm A'`. Value is `{ hours, minutes }`, not a `Date`. |
92
+ | `AutoResizeTextarea` | Textarea that grows with content; `maxHeight` enables scroll. Handles RHF `setValue`/`reset` correctly. |
93
+ | `ProgressBar` | Step-based bar; pass `currentStep` + `totalSteps`, or `value` (0–100) directly. |
94
+ | `SmartDialog*` | Responsive Dialog↔Drawer (see above). |
95
+ | `Typography`: `DisplayHeading`, `HeadingXL` … `HeadingXXS` (+ `*Medium` variants), `Body` | Use these instead of raw `<h1>`/`<p>` to inherit the right tokens (`font-serif italic` for display, `text-vibrant-text-heading` for headings, `text-vibrant-text-body` for body). Sizes are responsive (md: breakpoint baked in). |
96
+ | `ThemeToggle` | Drop-in light/dark toggle. |
97
+
98
+ ## Color tokens (don't reach for raw Tailwind colors)
99
+
100
+ The brand palette lives in CSS variables exposed as Tailwind colors. Use these, not `bg-blue-600`, `text-gray-500`, `border-red-300`, etc. — raw colors won't dark-mode correctly.
101
+
102
+ - **Brand**: `brand-{subtle,light,normal,vibrant,dark}`, `brand-text-vibrant`. `primary` aliases `brand-normal`.
103
+ - **Surfaces** (backgrounds): `gray-surface-{light,default,dark}`, `primary-surface-{subtle,light,default}`, `secondary-surface-default`, `sand-{subtle,light,normal,dark}`, `sage-{light,normal,dark}`, `rose-{light,normal,dark}`, `error-surface-{subtle,light,default,dark}`, `success-surface-{subtle,default}`, `warning-surface-{subtle,light}`.
104
+ - **Strokes** (borders): `gray-stroke-{light,default}`, `primary-stroke-default`, `secondary-stroke-{light,default}`, `error-stroke-{light,default}`, `success-stroke-light`, `warning-stroke-{default,dark}`, `sand-stroke-disabled`.
105
+ - **Text**: `vibrant-text-{display,heading,body,details,white-darker}`, `secondary-text-dark`, `gray-subtle`, `foreground-secondary`, `destructive-text`.
106
+ - **Icons**: `gray-icon-{subtle,light,default,dark}`, `icon-disabled`.
107
+ - **Semantic** (inherited from shadcn): `background`, `foreground`, `border`, `input`, `ring`, `card`, `popover`, `primary`, `secondary`, `muted`, `accent`, `destructive`, `sidebar*`. These are wired to the brand palette in light **and** dark mode.
108
+
109
+ Source of truth: `src/styles.css` in this package. If a token is missing, propose it there rather than hardcoding a hex.
110
+
111
+ ## Spacing & sizing conventions
112
+
113
+ - Components are built with **fixed pixel heights**, not `py-*` shorthands. Buttons: 36/40/48 (sm/md/lg). Inputs: 40/48/56. Badges (informative/actionable): 24/32/40. Match these when building adjacent custom UI.
114
+ - Border radius is **per-component**, not global — Buttons `rounded-md`, Cards `rounded-xl`, Badges `rounded-md` (default) or `rounded-[8px]` (informative/actionable), Alert banners `rounded-[12px]`. Don't override unless you have a reason.
115
+ - Mobile breakpoint is **768px** (`useIsMobile`), but `SmartDialog*` switches at **600px** via its own `useMediaQuery`. They are intentionally different — use the right one.
116
+
117
+ ## Utilities & hooks
118
+
119
+ - `cn(...inputs)` — `clsx` + `tailwind-merge`. Use it when composing `className` props passed to design-system components, so caller classes win conflicts cleanly.
120
+ - `useIsMobile()` — boolean, 768px breakpoint, SSR-safe (returns `false` on first render).
121
+ - `useTheme()` — re-exported from `next-themes`.
122
+ - `buttonVariants`, `badgeVariants`, etc. — exported `cva` instances. Use them when you need the same look on a non-button element (e.g. an `<a>` styled like a button) instead of reimplementing the styles.
123
+
124
+ ## Common gotchas
125
+
126
+ - **Unstyled components** → missing `@source` glob for `node_modules/@codapet/design-system/dist/**` in the consumer's CSS.
127
+ - **Wrong default Button look** → you forgot `variant="primary"`/`size="md"` are not the same as shadcn's defaults; passing nothing gives you `primary` + `lg`.
128
+ - **Toast looks generic** → you imported `toast` from `'sonner'` instead of from `@codapet/design-system`.
129
+ - **Modal doesn't bottom-sheet on mobile** → you used `Dialog` instead of `SmartDialog`.
130
+ - **Headings look wrong** → you used a raw `<h1>` instead of `HeadingXL` / `DisplayHeading`. The serif-italic display style only comes from `DisplayHeading`.
131
+ - **Dark mode broken** → you used raw Tailwind colors (`bg-gray-100`, `text-zinc-700`) instead of brand tokens; or you forgot to wrap in `ThemeProvider`.
132
+ - **"Module not found" in tests** → Jest can't parse ESM; add `'@codapet/design-system'` to `transformIgnorePatterns` or switch the test file to Vitest.
package/README.md CHANGED
@@ -349,6 +349,20 @@ For detailed information about how dependencies are organized and managed, see [
349
349
  - **Dependencies**: UI libraries and utilities (bundled with library)
350
350
  - **Dev Dependencies**: Build tools and development utilities (not included in package)
351
351
 
352
+ ## Using with AI coding agents
353
+
354
+ This package ships an [`AGENTS.md`](./AGENTS.md) at its root — a guide for AI agents (Claude Code, Cursor, Codex, etc.) explaining how the library differs from stock shadcn/ui (button defaults, custom components, brand tokens, common gotchas). It's published with the npm tarball, so once you've installed the package, the file is at `node_modules/@codapet/design-system/AGENTS.md`.
355
+
356
+ To make Claude Code automatically pull it into every session in your consumer repo, add one line to your repo's `CLAUDE.md` (or `AGENTS.md`):
357
+
358
+ ```
359
+ @./node_modules/@codapet/design-system/AGENTS.md
360
+ ```
361
+
362
+ The `@path` syntax inlines the file's content into the agent's context. Because it resolves at session start, the guide always matches the version of `@codapet/design-system` you have installed — bump the dep, get the updated guide, no copy-paste.
363
+
364
+ For other agent tools (Cursor, Codex, etc.) the file is still readable at the same path; refer to your tool's docs for how to point it at additional context files.
365
+
352
366
  ## Contributing
353
367
 
354
368
  1. Fork the repository
package/dist/index.d.mts CHANGED
@@ -44,7 +44,10 @@ import * as TogglePrimitive from '@radix-ui/react-toggle';
44
44
  import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
45
45
  import { ClassValue } from 'clsx';
46
46
 
47
- declare function Accordion({ ...props }: React$1.ComponentProps<typeof AccordionPrimitive.Root>): react_jsx_runtime.JSX.Element;
47
+ type AccordionVariant = 'default' | 'outlined';
48
+ declare function Accordion({ variant, className, ...props }: React$1.ComponentProps<typeof AccordionPrimitive.Root> & {
49
+ variant?: AccordionVariant;
50
+ }): react_jsx_runtime.JSX.Element;
48
51
  declare function AccordionItem({ className, ...props }: React$1.ComponentProps<typeof AccordionPrimitive.Item>): react_jsx_runtime.JSX.Element;
49
52
  declare function AccordionTrigger({ className, children, expandedIcon, collapsedIcon, ...props }: React$1.ComponentProps<typeof AccordionPrimitive.Trigger> & {
50
53
  expandedIcon?: React$1.ReactNode;
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  // src/components/ui/accordion.tsx
4
- import "react";
4
+ import * as React from "react";
5
5
  import * as AccordionPrimitive from "@radix-ui/react-accordion";
6
6
  import { ChevronDownIcon } from "lucide-react";
7
7
 
@@ -14,20 +14,40 @@ function cn(...inputs) {
14
14
 
15
15
  // src/components/ui/accordion.tsx
16
16
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
17
+ var AccordionContext = React.createContext({
18
+ variant: "default"
19
+ });
17
20
  function Accordion({
21
+ variant = "default",
22
+ className,
18
23
  ...props
19
24
  }) {
20
- return /* @__PURE__ */ jsx(AccordionPrimitive.Root, { "data-slot": "accordion", ...props });
25
+ return /* @__PURE__ */ jsx(AccordionContext.Provider, { value: { variant }, children: /* @__PURE__ */ jsx(
26
+ AccordionPrimitive.Root,
27
+ {
28
+ "data-slot": "accordion",
29
+ "data-variant": variant,
30
+ className: cn(
31
+ variant === "outlined" && "flex flex-col gap-3",
32
+ className
33
+ ),
34
+ ...props
35
+ }
36
+ ) });
21
37
  }
22
38
  function AccordionItem({
23
39
  className,
24
40
  ...props
25
41
  }) {
42
+ const { variant } = React.useContext(AccordionContext);
26
43
  return /* @__PURE__ */ jsx(
27
44
  AccordionPrimitive.Item,
28
45
  {
29
46
  "data-slot": "accordion-item",
30
- className: cn("border-b last:border-b-0", className),
47
+ className: cn(
48
+ variant === "outlined" ? "rounded-lg border" : "border-b last:border-b-0",
49
+ className
50
+ ),
31
51
  ...props
32
52
  }
33
53
  );
@@ -39,20 +59,26 @@ function AccordionTrigger({
39
59
  collapsedIcon,
40
60
  ...props
41
61
  }) {
62
+ const { variant } = React.useContext(AccordionContext);
42
63
  const hasCustomIcon = expandedIcon !== void 0 || collapsedIcon !== void 0;
64
+ const animatedIconSwap = variant === "outlined" && collapsedIcon !== void 0 && expandedIcon !== void 0;
43
65
  return /* @__PURE__ */ jsx(AccordionPrimitive.Header, { className: "flex", children: /* @__PURE__ */ jsxs(
44
66
  AccordionPrimitive.Trigger,
45
67
  {
46
68
  "data-slot": "accordion-trigger",
47
69
  className: cn(
48
70
  "group focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50",
71
+ variant === "outlined" && "min-h-16 items-center gap-8 px-5 py-4 text-base lg:px-6 lg:text-lg lg:leading-7 hover:no-underline",
49
72
  !hasCustomIcon && "[&[data-state=open]>svg]:rotate-180",
50
73
  className
51
74
  ),
52
75
  ...props,
53
76
  children: [
54
77
  children,
55
- hasCustomIcon ? /* @__PURE__ */ jsxs(Fragment, { children: [
78
+ hasCustomIcon ? animatedIconSwap ? /* @__PURE__ */ jsxs("span", { className: "pointer-events-none grid shrink-0 text-muted-foreground *:[grid-area:1/1] *:transition-[rotate,opacity] *:duration-300 *:ease-[cubic-bezier(0.33,1,0.68,1)]", children: [
79
+ /* @__PURE__ */ jsx("span", { className: "group-data-[state=open]:rotate-90 group-data-[state=open]:opacity-0", children: collapsedIcon }),
80
+ /* @__PURE__ */ jsx("span", { className: "-rotate-90 opacity-0 group-data-[state=open]:rotate-0 group-data-[state=open]:opacity-100", children: expandedIcon })
81
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
56
82
  collapsedIcon !== void 0 && /* @__PURE__ */ jsx("span", { className: "group-data-[state=open]:hidden pointer-events-none shrink-0 text-muted-foreground", children: collapsedIcon }),
57
83
  expandedIcon !== void 0 && /* @__PURE__ */ jsx("span", { className: "group-data-[state=closed]:hidden pointer-events-none shrink-0 text-muted-foreground", children: expandedIcon })
58
84
  ] }) : /* @__PURE__ */ jsx(ChevronDownIcon, { className: "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-400" })
@@ -65,13 +91,23 @@ function AccordionContent({
65
91
  children,
66
92
  ...props
67
93
  }) {
94
+ const { variant } = React.useContext(AccordionContext);
68
95
  return /* @__PURE__ */ jsx(
69
96
  AccordionPrimitive.Content,
70
97
  {
71
98
  "data-slot": "accordion-content",
72
99
  className: "data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm",
73
100
  ...props,
74
- children: /* @__PURE__ */ jsx("div", { className: cn("pt-0 pb-4", className), children })
101
+ children: /* @__PURE__ */ jsx(
102
+ "div",
103
+ {
104
+ className: cn(
105
+ variant === "outlined" ? "px-5 pb-5 lg:px-6 lg:pb-6" : "pt-0 pb-4",
106
+ className
107
+ ),
108
+ children
109
+ }
110
+ )
75
111
  }
76
112
  );
77
113
  }