@fracazo/design-system 0.6.0 → 0.7.0

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/DESIGN.md CHANGED
@@ -220,7 +220,7 @@ Source order is reading order; landmarks and heading levels are real;
220
220
 
221
221
  ## Components
222
222
 
223
- The package ships seventeen shadcn-based components under
223
+ The package ships eighteen components, seventeen shadcn-based and one house primitive, under
224
224
  `@fracazo/design-system/ui/*`. Each carries an intent block at the top of
225
225
  its source (one line, then Use for, Avoid when, Variants); that block is
226
226
  the contract and is updated whenever variants change. In brief:
@@ -250,6 +250,11 @@ the contract and is updated whenever variants change. In brief:
250
250
  bespoke. **Calendar** exists for range selection; a single date uses a
251
251
  native input. **SortableList** ranks a small capped set with drag,
252
252
  touch and keyboard.
253
+ - **OfferCard** is the one house primitive: a linked offer surface with
254
+ a media slot, tag, title, description and a source footer, for a grid
255
+ of peer offers. The whole card is one link; `highlighted` is its single
256
+ emphasis signal; hover motion is CSS transitions under motion-safe.
257
+ It lays out in a grid, never a carousel.
253
258
 
254
259
  A component that must import app code (stores, data clients, routes)
255
260
  does not belong in the package; it stays in the product, like
package/README.md CHANGED
@@ -10,7 +10,7 @@ It lives in three places.
10
10
 
11
11
  **Lint.** Eight ESLint rules catch what a reviewer would: colour literals, radius literals, stock palette, dark pairs, arbitrary sizes, focus rings, text on dark surfaces. The build fails before anyone posts a screenshot.
12
12
 
13
- **Components.** Each of the seventeen components carries its own guidance in JSDoc: use for, avoid when, variants. The decision sits where it gets made, not in a doc nobody opens.
13
+ **Components.** Each of the eighteen components carries its own guidance in JSDoc: use for, avoid when, variants. The decision sits where it gets made, not in a doc nobody opens.
14
14
 
15
15
  **The agent skill.** AI tools load the design rules before they build or review any UI, route to the reference that applies, and cite rules by stable ID. The system proposes, the human commits.
16
16
 
@@ -26,7 +26,7 @@ Built for BirthGuide and birthplans.app, and designed to start the next product
26
26
  | `ds-build-brand-css` | Composes the plain-CSS token file a product serves publicly (e.g. `/brand.css`) |
27
27
  | `ds-init` and `template/` | Writes a new product: Next 16, Tailwind v4, this package, a blank brand file and the guardrails on, pinned to the package version that wrote it |
28
28
  | `css/motion.css` | The animation vocabulary the components use (enter, exit, accordion, the fade, zoom, blur and slide utilities); `roles.css` imports it |
29
- | `@fracazo/design-system` and `./ui/*` | `cn` and seventeen shadcn-based components (button, card, dialog, form, select, sortable-list and the rest), each with intent JSDoc: use for, avoid when, variants |
29
+ | `@fracazo/design-system` and `./ui/*` | `cn` and eighteen components (button, card, dialog, form, select, sortable-list, offer-card and the rest), each with intent JSDoc: use for, avoid when, variants |
30
30
  | `@fracazo/design-system/eslint` | Eight guardrails as an ESLint plugin, one per rule ID: colour literals, arbitrary clamp sizes, dark pairs, radius literals, stock palette, `focus:` rings, text on always-dark surfaces, em dashes |
31
31
  | `demo/index.html` | A showcase page that renders the roles in both modes off a served `/brand.css` |
32
32
  | `skills/product-design/` | The agent skill: request modes, routed references, rules with stable IDs, exemplars, coverage gaps. Point your CLAUDE.md or AGENTS.md at its `SKILL.md` |
@@ -16,6 +16,7 @@ export * from "./ui/dialog.js";
16
16
  export * from "./ui/form.js";
17
17
  export * from "./ui/input.js";
18
18
  export * from "./ui/label.js";
19
+ export * from "./ui/offer-card.js";
19
20
  export * from "./ui/popover.js";
20
21
  export * from "./ui/progress.js";
21
22
  export * from "./ui/radio-group.js";
package/dist/src/index.js CHANGED
@@ -16,6 +16,7 @@ export * from "./ui/dialog.js";
16
16
  export * from "./ui/form.js";
17
17
  export * from "./ui/input.js";
18
18
  export * from "./ui/label.js";
19
+ export * from "./ui/offer-card.js";
19
20
  export * from "./ui/popover.js";
20
21
  export * from "./ui/progress.js";
21
22
  export * from "./ui/radio-group.js";
@@ -0,0 +1,52 @@
1
+ import * as React from "react";
2
+ import { type LucideIcon } from "lucide-react";
3
+ /**
4
+ * A linked offer surface: media on top, a tag, a title, a line of
5
+ * description and a footer that names the source, with an arrow that
6
+ * answers hover.
7
+ *
8
+ * Use for: a grid of peer offers the reader picks from (deals, price
9
+ * matches, plans), where the whole card is one link and the footer
10
+ * carries the source (a retailer, a brand, a promo code, a was-price).
11
+ * Avoid when: the item has more than one action (use a house card with
12
+ * Buttons), or when the cards would sit in a carousel; the system rejects
13
+ * carousels, so lay OfferCards in a grid and let it wrap.
14
+ * Slots: `media` is a node, not a URL, so a product can pass its own tile
15
+ * (an initial-letter fallback, a next/image) and it fills a 16:9 area;
16
+ * `footer.avatar` fills a 40px circle; `footer.meta` is a node so it can
17
+ * carry a struck-through was-price, not only a code. `tagIcon` defaults to
18
+ * Lucide's Tag.
19
+ * States: `highlighted` tints the body with the primary at low opacity, the
20
+ * one emphasis signal for "cheapest" or "best"; nothing else changes, so a
21
+ * grid keeps one signal per card. `external` opens in a new tab and says so
22
+ * in the accessible name.
23
+ * Motion: hover lifts the card 8px, scales the media and rotates the arrow,
24
+ * all as CSS transitions on the anchor's hover, and only under
25
+ * motion-safe; with reduced motion the card sits still and the arrow only
26
+ * fills. The card fills its column at any width; height follows a grid
27
+ * row's tallest card because the anchor is h-full and the body flexes.
28
+ */
29
+ type OfferCardProps = Omit<React.ComponentProps<"a">, "href" | "title" | "media"> & {
30
+ href: string;
31
+ /** Opens in a new tab, with rel and an sr-only note on the accessible name. */
32
+ external?: boolean;
33
+ /** Fills a 16:9 area; give an image `fill` (next/image) or `size-full object-cover`. */
34
+ media?: React.ReactNode;
35
+ tag?: string;
36
+ tagIcon?: LucideIcon;
37
+ title: string;
38
+ description?: string;
39
+ footer?: {
40
+ /** Fills a 40px circle. */
41
+ avatar?: React.ReactNode;
42
+ name: string;
43
+ /** Second line under the name: a code, a was-price, a note. */
44
+ meta?: React.ReactNode;
45
+ };
46
+ /** The one emphasis signal: a subtle primary tint on the body. */
47
+ highlighted?: boolean;
48
+ /** Heading level for the title; h3 suits a card under a section heading. */
49
+ titleAs?: "h2" | "h3" | "h4" | "p";
50
+ };
51
+ declare function OfferCard({ href, external, media, tag, tagIcon: TagIcon, title, description, footer, highlighted, titleAs: TitleTag, className, ...props }: OfferCardProps): React.JSX.Element;
52
+ export { OfferCard, type OfferCardProps };
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ArrowRight, Tag } from "lucide-react";
3
+ import { cn } from "../cn.js";
4
+ function OfferCard({ href, external = false, media, tag, tagIcon: TagIcon = Tag, title, description, footer, highlighted = false, titleAs: TitleTag = "h3", className, ...props }) {
5
+ return (_jsxs("a", { "data-slot": "offer-card", "data-highlighted": highlighted || undefined, href: href, target: external ? "_blank" : undefined, rel: external ? "noopener noreferrer" : undefined, className: cn("group flex h-full flex-col overflow-hidden rounded-2xl bg-card text-card-foreground shadow-card", "transition-[translate,box-shadow] duration-200 ease-out hover:shadow-card-hover motion-safe:hover:-translate-y-2", "outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50", className), ...props, children: [media !== undefined && media !== null ? (_jsx("div", { className: "aspect-video overflow-hidden bg-muted", children: _jsx("div", { className: "relative size-full transition-transform duration-300 ease-out *:size-full *:object-cover motion-safe:group-hover:scale-105", children: media }) })) : null, _jsxs("div", { className: cn("flex flex-1 flex-col", highlighted && "bg-primary/5"), children: [_jsxs("div", { className: "flex flex-1 flex-col gap-2 px-5 pt-5 pb-4", children: [tag ? (_jsxs("span", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(TagIcon, { "aria-hidden": "true", className: "size-4 shrink-0" }), tag] })) : null, _jsx(TitleTag, { className: "text-xl font-semibold tracking-tight text-balance", children: title }), description ? (_jsx("p", { className: "text-sm text-muted-foreground text-pretty", children: description })) : null] }), footer ? (_jsxs("div", { className: "mx-5 mb-4 flex items-center gap-3 border-t border-border pt-4", children: [footer.avatar !== undefined && footer.avatar !== null ? (_jsx("span", { className: "size-10 shrink-0 overflow-hidden rounded-full *:size-full *:object-cover", children: footer.avatar })) : null, _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-sm font-semibold", children: footer.name }), footer.meta !== undefined && footer.meta !== null ? (_jsx("span", { className: "block truncate text-sm text-muted-foreground", children: footer.meta })) : null] }), _jsx("span", { "aria-hidden": "true", className: "flex size-11 shrink-0 items-center justify-center rounded-full bg-secondary text-secondary-foreground transition-[background-color,color,rotate] duration-200 ease-out group-hover:bg-primary group-hover:text-primary-foreground motion-safe:group-hover:-rotate-45", children: _jsx(ArrowRight, { className: "size-4" }) })] })) : null] }), external ? _jsx("span", { className: "sr-only", children: " (opens in a new tab)" }) : null] }));
6
+ }
7
+ export { OfferCard };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fracazo/design-system",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "An agent-native design system. Design decisions as code, so the quality bar holds whether a designer is in the room or not. Lint guardrails, components with intent docs, and an agent skill. One brand file per product, the system stays the same.",
5
5
  "license": "MIT",
6
6
  "author": "Alex Fracazo",
@@ -26,6 +26,7 @@ Components, has the one-paragraph digest.
26
26
  | Secondary detail under a heading | `Accordion` | Tabs |
27
27
  | Ranking a small capped set | `SortableList` | drag on a plain list |
28
28
  | Long free text | `Textarea` (auto-grows) | Input |
29
+ | A grid of peer offers, each one link | `OfferCard` in a grid | a carousel, a house card with a Button per offer |
29
30
 
30
31
  ## Composing
31
32