@roadlittledawn/docs-design-system-react 0.12.2 → 0.13.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/USAGE.md CHANGED
@@ -36,6 +36,7 @@ Before diving into individual components, here's guidance on common decision poi
36
36
 
37
37
  - **`Grid`/`Column`** — for asymmetric or varied multi-column layouts (tutorial + code panel, image + annotation, side-by-side comparison)
38
38
  - **`CardGrid`** — specifically for grids of uniform `Card` items; simpler API than `Grid`
39
+ - **`TileGrid`** — for dense grids of `Tile` items (integrations, frameworks, skills); supports 3–6 columns
39
40
 
40
41
  ### Headings and text
41
42
 
@@ -114,6 +115,10 @@ A contained, elevated box for grouping related content. Becomes a fully-clickabl
114
115
  - **Alerts, warnings, or notices** — use `Callout`
115
116
  - **Very long content** — cards are best for summaries; long content belongs in page sections
116
117
 
118
+ ### When to Use `Card` vs `Tile`
119
+
120
+ Use `Card` for a small number of items (2–12) with substantial content (descriptions, icons, links). Use `Tile` for large lists of compact items (integrations, frameworks, skills) where 10–100+ items fit on a page.
121
+
117
122
  ### When to Use `Card` vs `Callout`
118
123
 
119
124
  Use `Card` for content that is part of the normal page structure (navigation, feature highlights). Use `Callout` when you need to interrupt the reading flow to highlight something important, warn users, or provide tips.
@@ -136,6 +141,8 @@ import { Card } from "@roadlittledawn/docs-design-system-react";
136
141
  | `iconPlacement` | `"left" \| "top-left" \| "top-center"` | `"top-left"` | Where to place the icon: vertically centered on the left, above content flush left, or above content centered |
137
142
  | `iconSize` | `string` | — | Override the icon container size (width and height). Accepts any valid CSS length (e.g. `"2rem"`, `"48px"`). Defaults to `--dds-card-icon-size` (`1.5rem`). |
138
143
  | `showArrow` | `boolean` | `false` | Show an animated arrow in the lower-right corner to signal the card is navigable. Best used with `href`. |
144
+ | `maxWidth` | `string` | — | Constrain the card's maximum width (e.g. `"400px"`, `"32rem"`). Useful when a card fills a wide column but looks better smaller. |
145
+ | `centered` | `boolean` | `false` | Horizontally center the card within its container. Most useful combined with `maxWidth`. |
139
146
  | `children` | `ReactNode` | — | Card content |
140
147
  | `className` | `string` | `""` | Additional CSS classes |
141
148
 
@@ -171,6 +178,11 @@ import { Card } from "@roadlittledawn/docs-design-system-react";
171
178
  <Card title="New Feature" titleColor="blue" backgroundColor="blue">
172
179
  Check out our latest component additions.
173
180
  </Card>
181
+
182
+ {/* Custom width, centered in column */}
183
+ <Card title="Centered Card" maxWidth="400px" centered>
184
+ This card is constrained to 400px and centered.
185
+ </Card>
174
186
  ```
175
187
 
176
188
  ### Icon usage in MDX
@@ -258,6 +270,134 @@ import { CardGrid } from "@roadlittledawn/docs-design-system-react";
258
270
 
259
271
  ---
260
272
 
273
+ ## Tile
274
+
275
+ A compact, clickable item designed for dense listing patterns — integrations, frameworks, plugins, skills, etc. Unlike `Card`, Tile has a fixed layout (icon left, title right) and a simpler, more opinionated API.
276
+
277
+ ### When to Use
278
+
279
+ - Large lists of similar items (10–100+): integrations, frameworks, plugins, skills
280
+ - Navigation to many destinations where individual items are brief
281
+ - When `Card` would feel too spacious for the number of items
282
+
283
+ ### When Not to Use
284
+
285
+ - **Fewer than ~6 items with significant content** — use `Card` instead
286
+ - **Items that need rich content** (paragraphs, code, images) — use `Card`
287
+ - **Feature highlights** — use `Card` with colored backgrounds
288
+
289
+ ### `Tile` vs `Card`
290
+
291
+ | | Tile | Card |
292
+ |---|---|---|
293
+ | Layout | Fixed (icon-left) | Flexible (icon top/left/center) |
294
+ | Title | Required | Optional |
295
+ | Description | String prop | `children` (ReactNode) |
296
+ | Use case | Dense lists (10–100+ items) | Content groups (2–12 items) |
297
+ | Padding | Compact (0.875rem) | Spacious (1.5rem) |
298
+
299
+ ### Import
300
+
301
+ ```tsx
302
+ import { Tile } from "@roadlittledawn/docs-design-system-react";
303
+ ```
304
+
305
+ ### Props
306
+
307
+ | Prop | Type | Default | Description |
308
+ | --- | --- | --- | --- |
309
+ | `title` | `string` | — | **Required.** Tile heading text |
310
+ | `icon` | `ReactNode` | — | Optional icon displayed on the left. Pass a rendered icon component. |
311
+ | `description` | `string` | — | Optional short description below the title |
312
+ | `href` | `string` | — | Optional link URL. Makes the entire tile clickable. |
313
+ | `showArrow` | `boolean` | `false` | Show an animated arrow in the lower-right corner. Best used with `href`. |
314
+ | `className` | `string` | `""` | Additional CSS classes |
315
+
316
+ ### Examples
317
+
318
+ ```tsx
319
+ {/* Basic tile */}
320
+ <Tile title="React" icon={<ReactIcon />} href="/integrations/react" />
321
+
322
+ {/* With description */}
323
+ <Tile title="React" icon={<ReactIcon />} description="Build UIs with components" href="/integrations/react" />
324
+
325
+ {/* With arrow indicator */}
326
+ <Tile title="TypeScript" icon={<TsIcon />} description="Typed JavaScript" href="/skills/typescript" showArrow />
327
+ ```
328
+
329
+ ### Icon usage in MDX
330
+
331
+ Same pattern as `Card` — the consuming site resolves icon name strings to rendered components in the MDX component map:
332
+
333
+ ```tsx
334
+ Tile: ({ icon, ...props }) => {
335
+ const IconComp = typeof icon === 'string' ? iconMap[icon] : null;
336
+ return <DdsTile icon={IconComp ? <IconComp /> : icon} {...props} />;
337
+ }
338
+ ```
339
+
340
+ ---
341
+
342
+ ## TileGrid
343
+
344
+ A responsive CSS grid container designed for `Tile` components. Supports 3–6 columns and automatically adjusts to fewer columns on smaller screens.
345
+
346
+ ### When to Use
347
+
348
+ - Laying out 6 or more `Tile` components in a grid
349
+ - Displaying integrations, plugins, or skills lists
350
+
351
+ ### When Not to Use
352
+
353
+ - **Mixed content types** — use `Grid`
354
+ - **Fewer than 6 tiles** — `CardGrid` may be more appropriate
355
+ - **Tiles that need equal height** — TileGrid tiles size to their content; for equal-height use `CardGrid` with Card
356
+
357
+ ### Responsive behavior
358
+
359
+ | columns | Mobile (< 640px) | Tablet (≥ 640px) | Desktop (≥ 1024px) |
360
+ |---------|-----------------|-----------------|-------------------|
361
+ | 3 | 1 col | 2 col | 3 col |
362
+ | 4 | 2 col | 2 col | 4 col |
363
+ | 5 | 2 col | 3 col | 5 col |
364
+ | 6 | 2 col | 3 col | 6 col |
365
+
366
+ ### Import
367
+
368
+ ```tsx
369
+ import { TileGrid } from "@roadlittledawn/docs-design-system-react";
370
+ ```
371
+
372
+ ### Props
373
+
374
+ | Prop | Type | Default | Description |
375
+ | --- | --- | --- | --- |
376
+ | `columns` | `3 \| 4 \| 5 \| 6` | `4` | Number of columns at full width |
377
+ | `children` | `ReactNode` | — | Grid content (typically Tile components) |
378
+ | `className` | `string` | `""` | Additional CSS classes |
379
+
380
+ ### Examples
381
+
382
+ ```tsx
383
+ {/* Integration list */}
384
+ <TileGrid columns={4}>
385
+ <Tile title="React" icon={<ReactIcon />} href="/integrations/react" />
386
+ <Tile title="Vue" icon={<VueIcon />} href="/integrations/vue" />
387
+ <Tile title="Angular" icon={<AngularIcon />} href="/integrations/angular" />
388
+ {/* …more tiles */}
389
+ </TileGrid>
390
+
391
+ {/* Skills with descriptions */}
392
+ <TileGrid columns={3}>
393
+ <Tile title="TypeScript" icon={<TsIcon />} description="Typed JavaScript" href="/skills/typescript" showArrow />
394
+ <Tile title="GraphQL" icon={<GqlIcon />} description="Query language for APIs" href="/skills/graphql" showArrow />
395
+ {/* …more tiles */}
396
+ </TileGrid>
397
+ ```
398
+
399
+ ---
400
+
261
401
  ## Callout
262
402
 
263
403
  Interrupts reading flow to highlight important information. Each variant has a specific semantic meaning — choose the right one.
@@ -1035,7 +1175,15 @@ A hover/tap-activated floating panel for enriching inline documentation content.
1035
1175
 
1036
1176
  ### Mobile behavior
1037
1177
 
1038
- On screens ≤ 640px, the popover renders as a **bottom sheet** instead of a floating panel. Hover doesn't apply on touch devices; the popover toggles on tap.
1178
+ On screens ≤ 640px, the popover renders as a **bottom sheet** instead of a floating panel. Hover doesn't apply on touch devices; the popover opens on tap.
1179
+
1180
+ ### Closing the popover
1181
+
1182
+ The popover can be closed in three ways:
1183
+
1184
+ - **Close button** — the × button in the upper-right corner of the panel
1185
+ - **Click / tap outside** — native light-dismiss provided by `popover="auto"`
1186
+ - **Escape key** — handled automatically by the Popover API
1039
1187
 
1040
1188
  ### Content modes (choose one)
1041
1189
 
@@ -39,9 +39,20 @@ export interface CardProps {
39
39
  * @default false
40
40
  */
41
41
  showArrow?: boolean;
42
+ /**
43
+ * Constrain the card's maximum width. Accepts any valid CSS length value (e.g. `"400px"`, `"32rem"`).
44
+ * Useful when a card fills a wide column but its content looks better at a smaller size.
45
+ */
46
+ maxWidth?: string;
47
+ /**
48
+ * When true, horizontally centers the card within its container.
49
+ * Most useful in combination with `maxWidth`.
50
+ * @default false
51
+ */
52
+ centered?: boolean;
42
53
  /** Card content */
43
54
  children: ReactNode;
44
55
  /** Additional CSS classes */
45
56
  className?: string;
46
57
  }
47
- export declare function Card({ title, titleColor, backgroundColor, href, icon, iconPlacement, iconSize, showArrow, children, className, }: CardProps): import("react/jsx-runtime").JSX.Element;
58
+ export declare function Card({ title, titleColor, backgroundColor, href, icon, iconPlacement, iconSize, showArrow, maxWidth, centered, children, className, }: CardProps): import("react/jsx-runtime").JSX.Element;
@@ -11,7 +11,7 @@ var __assign = (this && this.__assign) || function () {
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
13
13
  export function Card(_a) {
14
- var title = _a.title, _b = _a.titleColor, titleColor = _b === void 0 ? "gray" : _b, _c = _a.backgroundColor, backgroundColor = _c === void 0 ? "white" : _c, href = _a.href, icon = _a.icon, _d = _a.iconPlacement, iconPlacement = _d === void 0 ? "top-left" : _d, iconSize = _a.iconSize, _e = _a.showArrow, showArrow = _e === void 0 ? false : _e, children = _a.children, _f = _a.className, className = _f === void 0 ? "" : _f;
14
+ var title = _a.title, _b = _a.titleColor, titleColor = _b === void 0 ? "gray" : _b, _c = _a.backgroundColor, backgroundColor = _c === void 0 ? "white" : _c, href = _a.href, icon = _a.icon, _d = _a.iconPlacement, iconPlacement = _d === void 0 ? "top-left" : _d, iconSize = _a.iconSize, _e = _a.showArrow, showArrow = _e === void 0 ? false : _e, maxWidth = _a.maxWidth, _f = _a.centered, centered = _f === void 0 ? false : _f, children = _a.children, _g = _a.className, className = _g === void 0 ? "" : _g;
15
15
  var cardClasses = [
16
16
  "dds-card",
17
17
  "dds-card-bg-".concat(backgroundColor),
@@ -33,9 +33,10 @@ export function Card(_a) {
33
33
  .join(" "), style: __assign({}, (iconSize && { width: iconSize, height: iconSize })), "aria-hidden": "true", children: icon })) : null;
34
34
  var arrowEl = showArrow ? (_jsx("span", { className: "dds-card-arrow", "aria-hidden": "true", children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M2 8H14M10 4L14 8L10 12", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) })) : null;
35
35
  var bodyContent = icon && iconPlacement === "left" ? (_jsxs("div", { className: "dds-card-icon-row", children: [iconEl, _jsxs("div", { className: "dds-card-icon-content", children: [title && _jsx("h3", { className: titleClasses, children: title }), _jsx("div", { className: textClasses, children: children })] })] })) : (_jsxs(_Fragment, { children: [iconEl, title && _jsx("h3", { className: titleClasses, children: title }), _jsx("div", { className: textClasses, children: children })] }));
36
- var content = (_jsxs("div", { className: cardClasses, children: [bodyContent, arrowEl] }));
36
+ var outerStyle = __assign(__assign({}, (maxWidth && { maxWidth: maxWidth })), (centered && { marginLeft: "auto", marginRight: "auto" }));
37
+ var content = (_jsxs("div", { className: cardClasses, style: !href ? outerStyle : undefined, children: [bodyContent, arrowEl] }));
37
38
  if (href) {
38
- return (_jsx("a", { href: href, className: "no-text-decoration", children: content }));
39
+ return (_jsx("a", { href: href, className: "no-text-decoration", style: Object.keys(outerStyle).length > 0 ? outerStyle : undefined, children: content }));
39
40
  }
40
41
  return content;
41
42
  }
@@ -55,6 +55,11 @@ export declare const WithCustomIconSize: Story;
55
55
  * All title color variants.
56
56
  */
57
57
  export declare const TitleColors: Story;
58
+ /**
59
+ * Card constrained to a custom width and centered. Useful when a card fills a wide column but
60
+ * looks better at a smaller size.
61
+ */
62
+ export declare const CustomWidth: Story;
58
63
  /**
59
64
  * All background color variants.
60
65
  */
@@ -51,6 +51,15 @@ var meta = {
51
51
  control: 'text',
52
52
  description: 'Card content.',
53
53
  },
54
+ maxWidth: {
55
+ control: 'text',
56
+ description: 'Constrain the card\'s maximum width (e.g. "400px", "32rem").',
57
+ },
58
+ centered: {
59
+ control: 'boolean',
60
+ description: 'Horizontally center the card within its container. Most useful with maxWidth.',
61
+ table: { defaultValue: { summary: 'false' } },
62
+ },
54
63
  className: {
55
64
  control: 'text',
56
65
  description: 'Additional CSS classes.',
@@ -266,6 +275,25 @@ export var TitleColors = {
266
275
  },
267
276
  render: function () { return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: '1rem' }, children: [_jsx(Card, { title: "Blue Title", titleColor: "blue", children: "Content with blue title" }), _jsx(Card, { title: "Green Title", titleColor: "green", children: "Content with green title" }), _jsx(Card, { title: "Purple Title", titleColor: "purple", children: "Content with purple title" }), _jsx(Card, { title: "Red Title", titleColor: "red", children: "Content with red title" }), _jsx(Card, { title: "Yellow Title", titleColor: "yellow", children: "Content with yellow title" }), _jsx(Card, { title: "Gray Title", titleColor: "gray", children: "Content with gray title" })] })); },
268
277
  };
278
+ /**
279
+ * Card constrained to a custom width and centered. Useful when a card fills a wide column but
280
+ * looks better at a smaller size.
281
+ */
282
+ export var CustomWidth = {
283
+ args: {
284
+ title: 'Custom Width',
285
+ maxWidth: '400px',
286
+ centered: true,
287
+ children: 'This card is constrained to 400px and centered in its container.',
288
+ },
289
+ parameters: {
290
+ docs: {
291
+ source: {
292
+ code: "<Card title=\"Custom Width\" maxWidth=\"400px\" centered>\n This card is constrained to 400px and centered in its container.\n</Card>",
293
+ },
294
+ },
295
+ },
296
+ };
269
297
  /**
270
298
  * All background color variants.
271
299
  */
@@ -80,6 +80,14 @@ export function Popover(_a) {
80
80
  var popoverRef = useRef(null);
81
81
  var showTimerRef = useRef(null);
82
82
  var hideTimerRef = useRef(null);
83
+ // Tracks when the popover was last shown automatically (hover/focus).
84
+ // Used to prevent the click handler from immediately closing a popover that
85
+ // was just opened by the hover timer (fixes the first-tap flicker on touch).
86
+ // On touch devices the synthetic `click` arrives ~300 ms after touch-start;
87
+ // the guard window must exceed showDelay (200 ms default) + that browser
88
+ // delay, so 400 ms is a safe minimum.
89
+ var FLICKER_GUARD_MS = 400;
90
+ var lastAutoShowTimeRef = useRef(0);
83
91
  var clearTimers = useCallback(function () {
84
92
  if (showTimerRef.current)
85
93
  clearTimeout(showTimerRef.current);
@@ -131,6 +139,8 @@ export function Popover(_a) {
131
139
  }
132
140
  positionPopover();
133
141
  popover.style.visibility = "";
142
+ // Record the time so the click handler knows the popover was auto-shown
143
+ lastAutoShowTimeRef.current = Date.now();
134
144
  }, showDelay);
135
145
  }, [clearTimers, showDelay, positionPopover]);
136
146
  var hidePopover = useCallback(function () {
@@ -188,6 +198,15 @@ export function Popover(_a) {
188
198
  var popover = popoverRef.current;
189
199
  if (!popover)
190
200
  return;
201
+ var isOpen = popover.matches(":popover-open") ||
202
+ popover.style.display === "block";
203
+ // Flicker guard: if the popover was just shown automatically by the
204
+ // hover/focus timer within the last 400 ms, a tap's synthetic click
205
+ // would arrive and toggle it closed. Instead, keep it open so the
206
+ // first tap reliably shows the popover.
207
+ if (isOpen && Date.now() - lastAutoShowTimeRef.current < FLICKER_GUARD_MS) {
208
+ return;
209
+ }
191
210
  try {
192
211
  popover.togglePopover();
193
212
  if (popover.matches(":popover-open"))
@@ -199,5 +218,17 @@ export function Popover(_a) {
199
218
  if (!isVisible)
200
219
  positionPopover();
201
220
  }
202
- }, "aria-describedby": popoverId, tabIndex: 0, children: children }), _jsx("div", __assign({ ref: popoverRef, id: popoverId }, { popover: "auto" }, { className: popoverClasses, onMouseEnter: clearTimers, onMouseLeave: hidePopover, children: popoverContent }))] }));
221
+ }, "aria-describedby": popoverId, tabIndex: 0, children: children }), _jsxs("div", __assign({ ref: popoverRef, id: popoverId }, { popover: "auto" }, { className: popoverClasses, onMouseEnter: clearTimers, onMouseLeave: hidePopover, children: [_jsx("button", { className: "dds-popover-close", "aria-label": "Close", onClick: function (e) {
222
+ e.stopPropagation();
223
+ clearTimers();
224
+ var popover = popoverRef.current;
225
+ if (!popover)
226
+ return;
227
+ try {
228
+ popover.hidePopover();
229
+ }
230
+ catch (_a) {
231
+ popover.style.display = "none";
232
+ }
233
+ }, children: "\u00D7" }), popoverContent] }))] }));
203
234
  }
@@ -64,7 +64,7 @@ var meta = {
64
64
  layout: "centered",
65
65
  docs: {
66
66
  description: {
67
- component: "\nA hover/tap-activated popover for enriching inline content in documentation.\nBuilt on the native [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) for reliable top-layer rendering \u2014 no z-index wars, no overflow clipping.\n\nCommon use cases include glossary term definitions and Wikipedia-style content previews.\n\n## Content modes\n\nThe `Popover` supports three mutually exclusive content modes, checked in this order:\n\n1. **`content`** \u2014 arbitrary `ReactNode`; you control everything\n2. **`glossary`** \u2014 structured `{ term, title, definition }` template\n3. **`preview`** \u2014 structured `{ title, excerpt, imageUrl, href }` template\n\n## When to Use\n\n- Inline term definitions that would interrupt reading flow if expanded in-place\n- Link previews that let users get context without navigating away\n- Any contextual content that benefits from being on-demand rather than always visible\n\n## When Not to Use\n\n- For critical information users must read \u2014 use a `Callout` instead\n- As a primary navigation mechanism\n- For content that needs persistent visibility\n\n## Mobile behavior\n\nOn screens \u2264 640 px the popover renders as a bottom sheet instead of a floating panel.\nHover events don't apply; the popover toggles on tap.\n\n## Accessibility\n\n- Trigger has `tabIndex={0}` and shows the popover on focus (keyboard accessible)\n- Popover panel has `role=\"tooltip\"` and is linked via `aria-describedby`\n- The native Popover API handles Escape-key dismissal automatically\n- Light-dismiss (click outside) is provided by `popover=\"auto\"`\n ",
67
+ component: "\nA hover/tap-activated popover for enriching inline content in documentation.\nBuilt on the native [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) for reliable top-layer rendering \u2014 no z-index wars, no overflow clipping.\n\nCommon use cases include glossary term definitions and Wikipedia-style content previews.\n\n## Content modes\n\nThe `Popover` supports three mutually exclusive content modes, checked in this order:\n\n1. **`content`** \u2014 arbitrary `ReactNode`; you control everything\n2. **`glossary`** \u2014 structured `{ term, title, definition }` template\n3. **`preview`** \u2014 structured `{ title, excerpt, imageUrl, href }` template\n\n## When to Use\n\n- Inline term definitions that would interrupt reading flow if expanded in-place\n- Link previews that let users get context without navigating away\n- Any contextual content that benefits from being on-demand rather than always visible\n\n## When Not to Use\n\n- For critical information users must read \u2014 use a `Callout` instead\n- As a primary navigation mechanism\n- For content that needs persistent visibility\n\n## Mobile behavior\n\nOn screens \u2264 640 px the popover renders as a bottom sheet instead of a floating panel.\nHover events don't apply; the popover opens on tap.\n\n## Closing the popover\n\nThe popover can be closed in three ways:\n- **Close button** \u2014 the \u00D7 button in the upper-right corner of the panel\n- **Click / tap outside** \u2014 native light-dismiss provided by `popover=\"auto\"`\n- **Escape key** \u2014 handled automatically by the Popover API\n\n## Accessibility\n\n- Trigger has `tabIndex={0}` and shows the popover on focus (keyboard accessible)\n- Popover panel has `role=\"tooltip\"` and is linked via `aria-describedby`\n- The native Popover API handles Escape-key dismissal automatically\n- Light-dismiss (click outside) is provided by `popover=\"auto\"`\n ",
68
68
  },
69
69
  },
70
70
  },
@@ -0,0 +1,24 @@
1
+ import { ReactNode } from "react";
2
+ import "./Tile.css";
3
+ export interface TileProps {
4
+ /** Tile heading text (required) */
5
+ title: string;
6
+ /**
7
+ * Optional icon to display on the left side.
8
+ * Pass a rendered icon component (e.g. `<YourIcon />`).
9
+ */
10
+ icon?: ReactNode;
11
+ /** Optional short description displayed below the title */
12
+ description?: string;
13
+ /** Optional link URL. When provided, the entire tile becomes clickable. */
14
+ href?: string;
15
+ /**
16
+ * Show an animated arrow in the lower-right corner to signal the tile is navigable.
17
+ * Best used together with `href`.
18
+ * @default false
19
+ */
20
+ showArrow?: boolean;
21
+ /** Additional CSS classes */
22
+ className?: string;
23
+ }
24
+ export declare function Tile({ title, icon, description, href, showArrow, className, }: TileProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import "./Tile.css";
3
+ export function Tile(_a) {
4
+ var title = _a.title, icon = _a.icon, description = _a.description, href = _a.href, _b = _a.showArrow, showArrow = _b === void 0 ? false : _b, _c = _a.className, className = _c === void 0 ? "" : _c;
5
+ var tileClasses = [
6
+ "dds-tile",
7
+ href ? "dds-tile-clickable" : "",
8
+ showArrow ? "dds-tile-has-arrow" : "",
9
+ className,
10
+ ]
11
+ .filter(Boolean)
12
+ .join(" ");
13
+ var arrowEl = showArrow ? (_jsx("span", { className: "dds-tile-arrow", "aria-hidden": "true", children: _jsx("svg", { width: "14", height: "14", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M2 8H14M10 4L14 8L10 12", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) })) : null;
14
+ var content = (_jsxs("div", { className: tileClasses, children: [_jsxs("div", { className: "dds-tile-body", children: [icon && (_jsx("span", { className: "dds-tile-icon", "aria-hidden": "true", children: icon })), _jsxs("div", { className: "dds-tile-text", children: [_jsx("span", { className: "dds-tile-title", children: title }), description && (_jsx("span", { className: "dds-tile-desc", children: description }))] })] }), arrowEl] }));
15
+ if (href) {
16
+ return (_jsx("a", { href: href, className: "dds-tile-link", children: content }));
17
+ }
18
+ return content;
19
+ }
@@ -0,0 +1,40 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Tile } from './Tile';
3
+ /**
4
+ * The Tile component is a compact, clickable card designed for dense lists of items
5
+ * such as integrations, frameworks, plugins, or skills.
6
+ *
7
+ * Unlike Card, Tile has a fixed layout (icon left, title right) and a simpler API
8
+ * focused on the list-item use case.
9
+ */
10
+ declare const meta: Meta<typeof Tile>;
11
+ export default meta;
12
+ type Story = StoryObj<typeof Tile>;
13
+ /**
14
+ * Basic tile with title only.
15
+ */
16
+ export declare const Basic: Story;
17
+ /**
18
+ * Tile with an icon on the left.
19
+ */
20
+ export declare const WithIcon: Story;
21
+ /**
22
+ * Tile with icon and a short description below the title.
23
+ */
24
+ export declare const WithDescription: Story;
25
+ /**
26
+ * Clickable tile with a link. Hover to see the border and shadow change.
27
+ */
28
+ export declare const Clickable: Story;
29
+ /**
30
+ * Clickable tile with the animated arrow indicator. Hover to see it animate.
31
+ */
32
+ export declare const WithArrow: Story;
33
+ /**
34
+ * A realistic grid of integration tiles. Use TileGrid to lay out multiple Tiles.
35
+ */
36
+ export declare const IntegrationGrid: Story;
37
+ /**
38
+ * A larger grid with descriptions and arrows, using 3 columns.
39
+ */
40
+ export declare const SkillsGrid: Story;
@@ -0,0 +1,180 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Tile } from './Tile';
3
+ import { TileGrid } from './TileGrid';
4
+ /**
5
+ * The Tile component is a compact, clickable card designed for dense lists of items
6
+ * such as integrations, frameworks, plugins, or skills.
7
+ *
8
+ * Unlike Card, Tile has a fixed layout (icon left, title right) and a simpler API
9
+ * focused on the list-item use case.
10
+ */
11
+ var meta = {
12
+ title: 'Components/Tile',
13
+ component: Tile,
14
+ tags: ['autodocs'],
15
+ argTypes: {
16
+ title: {
17
+ control: 'text',
18
+ description: 'Tile heading text (required).',
19
+ },
20
+ icon: {
21
+ control: false,
22
+ description: 'Optional icon to display on the left. Pass a rendered icon component.',
23
+ },
24
+ description: {
25
+ control: 'text',
26
+ description: 'Optional short description displayed below the title.',
27
+ },
28
+ href: {
29
+ control: 'text',
30
+ description: 'Optional link URL. Makes the entire tile clickable.',
31
+ },
32
+ showArrow: {
33
+ control: 'boolean',
34
+ description: 'Show an animated arrow in the lower-right corner.',
35
+ table: { defaultValue: { summary: 'false' } },
36
+ },
37
+ className: {
38
+ control: 'text',
39
+ description: 'Additional CSS classes.',
40
+ },
41
+ },
42
+ parameters: {
43
+ docs: {
44
+ description: {
45
+ component: "\nThe Tile component is a compact, clickable item designed for dense listing patterns.\n\n## When to Use\n\n- Long lists of integrations, frameworks, plugins, or skills\n- Navigation to many similar destinations (e.g., 20+ items)\n- When Card would feel too spacious for the number of items\n\n## When Not to Use\n\n- For fewer than ~6 items with significant content \u2014 use Card instead\n- When items need rich content (paragraphs, code, images) \u2014 use Card\n- For feature highlights \u2014 use Card with colored backgrounds\n\n## Vs. Card\n\n| | Tile | Card |\n|---|---|---|\n| Layout | Fixed (icon-left) | Flexible (icon top/left/center) |\n| Title | Required | Optional |\n| Description | String prop | children (ReactNode) |\n| Use case | Dense lists (10\u2013100+ items) | Content groups (2\u201312 items) |\n| Padding | Compact (0.875rem) | Spacious (1.5rem) |\n\n## Accessibility\n\n- Clickable tiles use proper link semantics (`<a>`)\n- Icon is decorative (`aria-hidden`) and does not affect screen reader output\n ",
46
+ },
47
+ },
48
+ },
49
+ };
50
+ export default meta;
51
+ var DemoIcon = function () { return (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25" }) })); };
52
+ /**
53
+ * Basic tile with title only.
54
+ */
55
+ export var Basic = {
56
+ args: {
57
+ title: 'React',
58
+ },
59
+ parameters: {
60
+ docs: {
61
+ source: { code: "<Tile title=\"React\" />" },
62
+ },
63
+ },
64
+ };
65
+ /**
66
+ * Tile with an icon on the left.
67
+ */
68
+ export var WithIcon = {
69
+ args: {
70
+ title: 'React',
71
+ icon: _jsx(DemoIcon, {}),
72
+ },
73
+ parameters: {
74
+ docs: {
75
+ source: {
76
+ code: "<Tile title=\"React\" icon={<ReactIcon />} />",
77
+ },
78
+ },
79
+ },
80
+ };
81
+ /**
82
+ * Tile with icon and a short description below the title.
83
+ */
84
+ export var WithDescription = {
85
+ args: {
86
+ title: 'React',
87
+ icon: _jsx(DemoIcon, {}),
88
+ description: 'Build UIs with components',
89
+ },
90
+ parameters: {
91
+ docs: {
92
+ source: {
93
+ code: "<Tile title=\"React\" icon={<ReactIcon />} description=\"Build UIs with components\" />",
94
+ },
95
+ },
96
+ },
97
+ };
98
+ /**
99
+ * Clickable tile with a link. Hover to see the border and shadow change.
100
+ */
101
+ export var Clickable = {
102
+ args: {
103
+ title: 'React',
104
+ icon: _jsx(DemoIcon, {}),
105
+ description: 'Build UIs with components',
106
+ href: '/integrations/react',
107
+ },
108
+ parameters: {
109
+ docs: {
110
+ source: {
111
+ code: "<Tile title=\"React\" icon={<ReactIcon />} description=\"Build UIs with components\" href=\"/integrations/react\" />",
112
+ },
113
+ },
114
+ },
115
+ };
116
+ /**
117
+ * Clickable tile with the animated arrow indicator. Hover to see it animate.
118
+ */
119
+ export var WithArrow = {
120
+ args: {
121
+ title: 'React',
122
+ icon: _jsx(DemoIcon, {}),
123
+ description: 'Build UIs with components',
124
+ href: '/integrations/react',
125
+ showArrow: true,
126
+ },
127
+ parameters: {
128
+ docs: {
129
+ source: {
130
+ code: "<Tile title=\"React\" icon={<ReactIcon />} description=\"Build UIs with components\" href=\"/integrations/react\" showArrow />",
131
+ },
132
+ },
133
+ },
134
+ };
135
+ /**
136
+ * A realistic grid of integration tiles. Use TileGrid to lay out multiple Tiles.
137
+ */
138
+ export var IntegrationGrid = {
139
+ parameters: {
140
+ docs: {
141
+ source: {
142
+ code: "<TileGrid columns={4}>\n <Tile title=\"React\" icon={<ReactIcon />} description=\"UI components\" href=\"/integrations/react\" />\n <Tile title=\"Vue\" icon={<VueIcon />} description=\"Progressive framework\" href=\"/integrations/vue\" />\n {/* \u2026more tiles */}\n</TileGrid>",
143
+ },
144
+ },
145
+ },
146
+ render: function () {
147
+ var integrations = [
148
+ 'React', 'Vue', 'Angular', 'Svelte',
149
+ 'Next.js', 'Nuxt', 'Astro', 'Remix',
150
+ 'Node.js', 'Deno', 'Bun', 'Express',
151
+ ];
152
+ return (_jsx(TileGrid, { columns: 4, children: integrations.map(function (name) { return (_jsx(Tile, { title: name, icon: _jsx(DemoIcon, {}), href: "/integrations/".concat(name.toLowerCase()) }, name)); }) }));
153
+ },
154
+ };
155
+ /**
156
+ * A larger grid with descriptions and arrows, using 3 columns.
157
+ */
158
+ export var SkillsGrid = {
159
+ parameters: {
160
+ docs: {
161
+ source: {
162
+ code: "<TileGrid columns={3}>\n <Tile title=\"TypeScript\" icon={<TsIcon />} description=\"Typed JavaScript\" href=\"/skills/typescript\" showArrow />\n {/* \u2026more tiles */}\n</TileGrid>",
163
+ },
164
+ },
165
+ },
166
+ render: function () {
167
+ var skills = [
168
+ { name: 'TypeScript', desc: 'Typed JavaScript at scale' },
169
+ { name: 'GraphQL', desc: 'Query language for APIs' },
170
+ { name: 'Docker', desc: 'Container platform' },
171
+ { name: 'Kubernetes', desc: 'Container orchestration' },
172
+ { name: 'Terraform', desc: 'Infrastructure as code' },
173
+ { name: 'Postgres', desc: 'Relational database' },
174
+ ];
175
+ return (_jsx(TileGrid, { columns: 3, children: skills.map(function (_a) {
176
+ var name = _a.name, desc = _a.desc;
177
+ return (_jsx(Tile, { title: name, icon: _jsx(DemoIcon, {}), description: desc, href: "/skills/".concat(name.toLowerCase()), showArrow: true }, name));
178
+ }) }));
179
+ },
180
+ };
@@ -0,0 +1,15 @@
1
+ import { ReactNode } from "react";
2
+ import "./TileGrid.css";
3
+ export interface TileGridProps {
4
+ /**
5
+ * Number of columns in the grid at full width.
6
+ * Responsive breakpoints apply automatically.
7
+ * @default 4
8
+ */
9
+ columns?: 3 | 4 | 5 | 6;
10
+ /** Grid content (typically Tile components) */
11
+ children: ReactNode;
12
+ /** Additional CSS classes */
13
+ className?: string;
14
+ }
15
+ export declare function TileGrid({ columns, children, className, }: TileGridProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import "./TileGrid.css";
3
+ export function TileGrid(_a) {
4
+ var _b = _a.columns, columns = _b === void 0 ? 4 : _b, children = _a.children, _c = _a.className, className = _c === void 0 ? "" : _c;
5
+ var classNames = [
6
+ "dds-tile-grid",
7
+ "dds-tile-grid-".concat(columns),
8
+ className,
9
+ ]
10
+ .filter(Boolean)
11
+ .join(" ");
12
+ return _jsx("div", { className: classNames, children: children });
13
+ }
@@ -0,0 +1,13 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { TileGrid } from './TileGrid';
3
+ /**
4
+ * TileGrid lays out Tile components in a responsive CSS grid.
5
+ * It supports 3–6 columns and automatically adjusts to fewer columns on smaller screens.
6
+ */
7
+ declare const meta: Meta<typeof TileGrid>;
8
+ export default meta;
9
+ type Story = StoryObj<typeof TileGrid>;
10
+ export declare const ThreeColumns: Story;
11
+ export declare const FourColumns: Story;
12
+ export declare const SixColumns: Story;
13
+ export declare const WithDescriptions: Story;
@@ -0,0 +1,96 @@
1
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
2
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
3
+ if (ar || !(i in from)) {
4
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
5
+ ar[i] = from[i];
6
+ }
7
+ }
8
+ return to.concat(ar || Array.prototype.slice.call(from));
9
+ };
10
+ import { jsx as _jsx } from "react/jsx-runtime";
11
+ import { TileGrid } from './TileGrid';
12
+ import { Tile } from './Tile';
13
+ /**
14
+ * TileGrid lays out Tile components in a responsive CSS grid.
15
+ * It supports 3–6 columns and automatically adjusts to fewer columns on smaller screens.
16
+ */
17
+ var meta = {
18
+ title: 'Components/TileGrid',
19
+ component: TileGrid,
20
+ tags: ['autodocs'],
21
+ argTypes: {
22
+ columns: {
23
+ control: { type: 'select' },
24
+ options: [3, 4, 5, 6],
25
+ description: 'Number of columns at full width.',
26
+ table: { defaultValue: { summary: '4' } },
27
+ },
28
+ children: {
29
+ control: false,
30
+ description: 'Grid content (typically Tile components).',
31
+ },
32
+ className: {
33
+ control: 'text',
34
+ description: 'Additional CSS classes.',
35
+ },
36
+ },
37
+ parameters: {
38
+ docs: {
39
+ description: {
40
+ component: "\nTileGrid is a responsive CSS grid container designed specifically for Tile components.\n\n## Responsive behavior\n\n| columns | Mobile (< 640px) | Tablet (\u2265 640px) | Desktop (\u2265 1024px) |\n|---------|-----------------|-----------------|-------------------|\n| 3 | 1 col | 2 col | 3 col |\n| 4 | 2 col | 2 col | 4 col |\n| 5 | 2 col | 3 col | 5 col |\n| 6 | 2 col | 3 col | 6 col |\n\n## Usage\n\n```tsx\n<TileGrid columns={4}>\n <Tile title=\"React\" icon={<ReactIcon />} href=\"/integrations/react\" />\n <Tile title=\"Vue\" icon={<VueIcon />} href=\"/integrations/vue\" />\n {/* \u2026more tiles */}\n</TileGrid>\n```\n ",
41
+ },
42
+ },
43
+ },
44
+ };
45
+ export default meta;
46
+ var DemoIcon = function () { return (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25" }) })); };
47
+ var names8 = ['React', 'Vue', 'Angular', 'Svelte', 'Next.js', 'Nuxt', 'Astro', 'Remix'];
48
+ var names12 = __spreadArray(__spreadArray([], names8, true), ['Node.js', 'Deno', 'Bun', 'Express'], false);
49
+ export var ThreeColumns = {
50
+ render: function () { return (_jsx(TileGrid, { columns: 3, children: names8.map(function (name) { return (_jsx(Tile, { title: name, icon: _jsx(DemoIcon, {}), href: "/integrations/".concat(name.toLowerCase()) }, name)); }) })); },
51
+ parameters: {
52
+ docs: {
53
+ source: {
54
+ code: "<TileGrid columns={3}>\n {integrations.map((name) => (\n <Tile key={name} title={name} icon={<Icon />} href={`/integrations/${name}`} />\n ))}\n</TileGrid>",
55
+ },
56
+ },
57
+ },
58
+ };
59
+ export var FourColumns = {
60
+ render: function () { return (_jsx(TileGrid, { columns: 4, children: names12.map(function (name) { return (_jsx(Tile, { title: name, icon: _jsx(DemoIcon, {}), href: "/integrations/".concat(name.toLowerCase()) }, name)); }) })); },
61
+ parameters: {
62
+ docs: {
63
+ source: {
64
+ code: "<TileGrid columns={4}>\n {integrations.map((name) => (\n <Tile key={name} title={name} icon={<Icon />} href={`/integrations/${name}`} />\n ))}\n</TileGrid>",
65
+ },
66
+ },
67
+ },
68
+ };
69
+ export var SixColumns = {
70
+ render: function () { return (_jsx(TileGrid, { columns: 6, children: __spreadArray(__spreadArray([], names12, true), ['Postgres', 'MySQL', 'Redis', 'MongoDB', 'Kafka', 'RabbitMQ'], false).map(function (name) { return (_jsx(Tile, { title: name, icon: _jsx(DemoIcon, {}), href: "/integrations/".concat(name.toLowerCase()) }, name)); }) })); },
71
+ parameters: {
72
+ docs: {
73
+ source: {
74
+ code: "<TileGrid columns={6}>\n {integrations.map((name) => (\n <Tile key={name} title={name} icon={<Icon />} href={`/integrations/${name}`} />\n ))}\n</TileGrid>",
75
+ },
76
+ },
77
+ },
78
+ };
79
+ export var WithDescriptions = {
80
+ render: function () {
81
+ var skills = [
82
+ { name: 'TypeScript', desc: 'Typed JavaScript' },
83
+ { name: 'GraphQL', desc: 'Query language for APIs' },
84
+ { name: 'Docker', desc: 'Container platform' },
85
+ { name: 'Kubernetes', desc: 'Orchestration' },
86
+ { name: 'Terraform', desc: 'Infrastructure as code' },
87
+ { name: 'Postgres', desc: 'Relational database' },
88
+ { name: 'Redis', desc: 'In-memory data store' },
89
+ { name: 'Kafka', desc: 'Event streaming' },
90
+ ];
91
+ return (_jsx(TileGrid, { columns: 4, children: skills.map(function (_a) {
92
+ var name = _a.name, desc = _a.desc;
93
+ return (_jsx(Tile, { title: name, icon: _jsx(DemoIcon, {}), description: desc, href: "/skills/".concat(name.toLowerCase()) }, name));
94
+ }) }));
95
+ },
96
+ };
package/dist/index.d.ts CHANGED
@@ -15,4 +15,6 @@ export * from './components/Table';
15
15
  export * from './components/Grid';
16
16
  export * from './components/Breadcrumb';
17
17
  export * from './components/Icon';
18
+ export * from './components/Tile';
19
+ export * from './components/TileGrid';
18
20
  export * from './hooks/useKeyPress';
package/dist/index.js CHANGED
@@ -16,5 +16,7 @@ export * from './components/Table';
16
16
  export * from './components/Grid';
17
17
  export * from './components/Breadcrumb';
18
18
  export * from './components/Icon';
19
+ export * from './components/Tile';
20
+ export * from './components/TileGrid';
19
21
  // Export hooks
20
22
  export * from './hooks/useKeyPress';
package/dist/styles.css CHANGED
@@ -287,6 +287,27 @@
287
287
  --dds-table-cell-padding: 0.75rem 1rem;
288
288
  --dds-table-focus-ring: #3b82f6; /* blue-500 */
289
289
 
290
+ /* Tile */
291
+ --dds-tile-padding: 0.875rem;
292
+ --dds-tile-radius: 0.375rem;
293
+ --dds-tile-border: #e5e7eb; /* gray-200 */
294
+ --dds-tile-border-hover: #d1d5db; /* gray-300 */
295
+ --dds-tile-bg: #ffffff;
296
+ --dds-tile-shadow-hover:
297
+ 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
298
+ --dds-tile-icon-size: 1.25rem; /* 20px */
299
+ --dds-tile-icon-color: #6b7280; /* gray-500 */
300
+ --dds-tile-icon-gap: 0.75rem;
301
+ --dds-tile-title-size: 0.9375rem; /* 15px */
302
+ --dds-tile-title-color: #111827; /* gray-900 */
303
+ --dds-tile-desc-size: 0.8125rem; /* 13px */
304
+ --dds-tile-desc-color: #6b7280; /* gray-500 */
305
+ --dds-tile-arrow-color: #9ca3af; /* gray-400 */
306
+ --dds-tile-arrow-inset: 0.625rem;
307
+
308
+ /* TileGrid */
309
+ --dds-tile-grid-gap: 0.75rem;
310
+
290
311
  /* Grid */
291
312
  --dds-grid-gap-sm: 1rem; /* gap-4 */
292
313
  --dds-grid-gap-md: 1.5rem; /* gap-6 */
@@ -486,6 +507,16 @@
486
507
  --dds-table-header-active-text: #93c5fd; /* blue-300 */
487
508
  --dds-table-focus-ring: #60a5fa; /* blue-400 */
488
509
 
510
+ /* Tile */
511
+ --dds-tile-border: #4b5563; /* gray-600 */
512
+ --dds-tile-border-hover: #9ca3af; /* gray-400 */
513
+ --dds-tile-bg: transparent;
514
+ --dds-tile-shadow-hover: none;
515
+ --dds-tile-icon-color: #9ca3af; /* gray-400 */
516
+ --dds-tile-title-color: #e5e7eb; /* gray-200 */
517
+ --dds-tile-desc-color: #9ca3af; /* gray-400 */
518
+ --dds-tile-arrow-color: #6b7280; /* gray-500 */
519
+
489
520
  /* Grid */
490
521
  --dds-grid-divider-color: #4b5563; /* gray-600 */
491
522
 
@@ -671,6 +702,16 @@
671
702
  --dds-table-header-active-text: #93c5fd;
672
703
  --dds-table-focus-ring: #60a5fa;
673
704
 
705
+ /* Tile */
706
+ --dds-tile-border: #4b5563; /* gray-600 */
707
+ --dds-tile-border-hover: #9ca3af; /* gray-400 */
708
+ --dds-tile-bg: transparent;
709
+ --dds-tile-shadow-hover: none;
710
+ --dds-tile-icon-color: #9ca3af; /* gray-400 */
711
+ --dds-tile-title-color: #e5e7eb; /* gray-200 */
712
+ --dds-tile-desc-color: #9ca3af; /* gray-400 */
713
+ --dds-tile-arrow-color: #6b7280; /* gray-500 */
714
+
674
715
  /* Grid */
675
716
  --dds-grid-divider-color: #4b5563; /* gray-600 */
676
717
 
@@ -838,6 +879,16 @@
838
879
  --dds-table-header-active-text: #93c5fd;
839
880
  --dds-table-focus-ring: #60a5fa;
840
881
 
882
+ /* Tile */
883
+ --dds-tile-border: #4b5563; /* gray-600 */
884
+ --dds-tile-border-hover: #9ca3af; /* gray-400 */
885
+ --dds-tile-bg: transparent;
886
+ --dds-tile-shadow-hover: none;
887
+ --dds-tile-icon-color: #9ca3af; /* gray-400 */
888
+ --dds-tile-title-color: #e5e7eb; /* gray-200 */
889
+ --dds-tile-desc-color: #9ca3af; /* gray-400 */
890
+ --dds-tile-arrow-color: #6b7280; /* gray-500 */
891
+
841
892
  /* Breadcrumb */
842
893
  --dds-breadcrumb-link-color: #9ca3af; /* gray-400 */
843
894
  --dds-breadcrumb-link-color-hover: #f9fafb; /* gray-50 */
@@ -1095,6 +1146,7 @@ pre[class*="language-"] {
1095
1146
  color: var(--dds-callout-course-title);
1096
1147
  }
1097
1148
  a.no-text-decoration {
1149
+ display: block;
1098
1150
  text-decoration: none;
1099
1151
  }
1100
1152
  .dds-card {
@@ -1844,7 +1896,7 @@ a.no-text-decoration {
1844
1896
  /* Trigger */
1845
1897
  .dds-popover-trigger {
1846
1898
  display: inline;
1847
- cursor: default;
1899
+ cursor: help;
1848
1900
  text-decoration-line: underline;
1849
1901
  text-decoration-style: dotted;
1850
1902
  text-decoration-color: currentColor;
@@ -1940,6 +1992,38 @@ a.no-text-decoration {
1940
1992
  .dds-popover-lg {
1941
1993
  width: var(--dds-popover-width-lg);
1942
1994
  }
1995
+ /* ==========================================================================
1996
+ Close button — sits in the upper-right corner of the popover panel
1997
+ ========================================================================== */
1998
+ .dds-popover-close {
1999
+ position: absolute;
2000
+ top: 0.5rem;
2001
+ right: 0.5rem;
2002
+ display: flex;
2003
+ align-items: center;
2004
+ justify-content: center;
2005
+ width: 1.5rem;
2006
+ height: 1.5rem;
2007
+ padding: 0;
2008
+ background: transparent;
2009
+ border: none;
2010
+ border-radius: 0.25rem;
2011
+ cursor: pointer;
2012
+ color: var(--dds-popover-eyebrow-color);
2013
+ font-size: 1.125rem;
2014
+ line-height: 1;
2015
+ transition:
2016
+ background 120ms,
2017
+ color 120ms;
2018
+ }
2019
+ .dds-popover-close:hover {
2020
+ background: var(--dds-popover-border);
2021
+ color: var(--dds-popover-text);
2022
+ }
2023
+ .dds-popover-close:focus-visible {
2024
+ outline: 2px solid var(--dds-link-color);
2025
+ outline-offset: 2px;
2026
+ }
1943
2027
  /* ==========================================================================
1944
2028
  Shared inner elements
1945
2029
  ========================================================================== */
@@ -1951,6 +2035,7 @@ a.no-text-decoration {
1951
2035
  text-transform: uppercase;
1952
2036
  color: var(--dds-popover-eyebrow-color);
1953
2037
  margin-bottom: 0.25rem;
2038
+ padding-right: 1.75rem; /* leave room for the close button */
1954
2039
  }
1955
2040
  .dds-popover-title {
1956
2041
  margin: 0 0 0.5rem;
@@ -1958,6 +2043,7 @@ a.no-text-decoration {
1958
2043
  font-weight: var(--dds-font-semibold);
1959
2044
  color: var(--dds-popover-title-color);
1960
2045
  line-height: var(--dds-line-height-tight);
2046
+ padding-right: 1.75rem; /* leave room for the close button */
1961
2047
  }
1962
2048
  .dds-popover-title dfn {
1963
2049
  font-style: normal;
@@ -2394,4 +2480,170 @@ a.dds-breadcrumb-link:hover {
2394
2480
  width: 100%;
2395
2481
  height: 100%;
2396
2482
  }
2483
+ a.dds-tile-link {
2484
+ display: block;
2485
+ text-decoration: none;
2486
+ }
2487
+ .dds-tile {
2488
+ padding: var(--dds-tile-padding);
2489
+ border-radius: var(--dds-tile-radius);
2490
+ border: 1px solid var(--dds-tile-border);
2491
+ background-color: var(--dds-tile-bg);
2492
+ position: relative;
2493
+ box-sizing: border-box;
2494
+ height: 100%;
2495
+ }
2496
+ .dds-tile-clickable {
2497
+ cursor: pointer;
2498
+ transition: var(--dds-transition-shadow), border-color 150ms ease-in-out;
2499
+ }
2500
+ .dds-tile-clickable:hover {
2501
+ box-shadow: var(--dds-tile-shadow-hover);
2502
+ border-color: var(--dds-tile-border-hover);
2503
+ }
2504
+ /* Layout: icon on left, text on right */
2505
+ .dds-tile-body {
2506
+ display: flex;
2507
+ align-items: center;
2508
+ gap: var(--dds-tile-icon-gap);
2509
+ }
2510
+ .dds-tile-icon {
2511
+ display: flex;
2512
+ align-items: center;
2513
+ justify-content: center;
2514
+ flex-shrink: 0;
2515
+ width: var(--dds-tile-icon-size);
2516
+ height: var(--dds-tile-icon-size);
2517
+ color: var(--dds-tile-icon-color);
2518
+ }
2519
+ .dds-tile-icon > * {
2520
+ width: 100%;
2521
+ height: 100%;
2522
+ }
2523
+ /* Text stack */
2524
+ .dds-tile-text {
2525
+ display: flex;
2526
+ flex-direction: column;
2527
+ gap: 0.125rem;
2528
+ min-width: 0;
2529
+ }
2530
+ .dds-tile-title {
2531
+ font-size: var(--dds-tile-title-size);
2532
+ font-weight: var(--dds-font-semibold);
2533
+ color: var(--dds-tile-title-color);
2534
+ line-height: var(--dds-line-height-tight);
2535
+ white-space: nowrap;
2536
+ overflow: hidden;
2537
+ text-overflow: ellipsis;
2538
+ }
2539
+ .dds-tile-desc {
2540
+ display: block;
2541
+ font-size: var(--dds-tile-desc-size);
2542
+ color: var(--dds-tile-desc-color);
2543
+ line-height: var(--dds-line-height-relaxed);
2544
+ white-space: nowrap;
2545
+ overflow: hidden;
2546
+ text-overflow: ellipsis;
2547
+ }
2548
+ /* Arrow */
2549
+ .dds-tile-has-arrow {
2550
+ padding-right: calc(var(--dds-tile-padding) + 1.125rem);
2551
+ }
2552
+ [dir="rtl"] .dds-tile-has-arrow {
2553
+ padding-right: var(--dds-tile-padding);
2554
+ padding-left: calc(var(--dds-tile-padding) + 1.125rem);
2555
+ }
2556
+ .dds-tile-arrow {
2557
+ position: absolute;
2558
+ bottom: var(--dds-tile-arrow-inset);
2559
+ right: var(--dds-tile-arrow-inset);
2560
+ color: var(--dds-tile-arrow-color);
2561
+ display: flex;
2562
+ align-items: center;
2563
+ justify-content: center;
2564
+ pointer-events: none;
2565
+ }
2566
+ [dir="rtl"] .dds-tile-arrow {
2567
+ right: auto;
2568
+ left: var(--dds-tile-arrow-inset);
2569
+ transform: scaleX(-1);
2570
+ }
2571
+ @keyframes dds-tile-arrow-nudge {
2572
+ 0% { transform: translateX(0); }
2573
+ 40% { transform: translateX(4px); }
2574
+ 70% { transform: translateX(-2px); }
2575
+ 100% { transform: translateX(0); }
2576
+ }
2577
+ @keyframes dds-tile-arrow-nudge-rtl {
2578
+ 0% { transform: scaleX(-1) translateX(0); }
2579
+ 40% { transform: scaleX(-1) translateX(4px); }
2580
+ 70% { transform: scaleX(-1) translateX(-2px); }
2581
+ 100% { transform: scaleX(-1) translateX(0); }
2582
+ }
2583
+ .dds-tile-clickable:hover .dds-tile-arrow {
2584
+ animation: dds-tile-arrow-nudge 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
2585
+ }
2586
+ [dir="rtl"] .dds-tile-clickable:hover .dds-tile-arrow {
2587
+ animation: dds-tile-arrow-nudge-rtl 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
2588
+ }
2589
+ .dds-tile-grid {
2590
+ display: grid;
2591
+ gap: var(--dds-tile-grid-gap);
2592
+ }
2593
+ /* 3 columns: 1 → 2 → 3 */
2594
+ .dds-tile-grid-3 {
2595
+ grid-template-columns: 1fr;
2596
+ }
2597
+ @media (min-width: 640px) {
2598
+ .dds-tile-grid-3 {
2599
+ grid-template-columns: repeat(2, 1fr);
2600
+ }
2601
+ }
2602
+ @media (min-width: 1024px) {
2603
+ .dds-tile-grid-3 {
2604
+ grid-template-columns: repeat(3, 1fr);
2605
+ }
2606
+ }
2607
+ /* 4 columns: 2 → 2 → 4 */
2608
+ .dds-tile-grid-4 {
2609
+ grid-template-columns: repeat(2, 1fr);
2610
+ }
2611
+ @media (min-width: 768px) {
2612
+ .dds-tile-grid-4 {
2613
+ grid-template-columns: repeat(2, 1fr);
2614
+ }
2615
+ }
2616
+ @media (min-width: 1024px) {
2617
+ .dds-tile-grid-4 {
2618
+ grid-template-columns: repeat(4, 1fr);
2619
+ }
2620
+ }
2621
+ /* 5 columns: 2 → 3 → 5 */
2622
+ .dds-tile-grid-5 {
2623
+ grid-template-columns: repeat(2, 1fr);
2624
+ }
2625
+ @media (min-width: 640px) {
2626
+ .dds-tile-grid-5 {
2627
+ grid-template-columns: repeat(3, 1fr);
2628
+ }
2629
+ }
2630
+ @media (min-width: 1024px) {
2631
+ .dds-tile-grid-5 {
2632
+ grid-template-columns: repeat(5, 1fr);
2633
+ }
2634
+ }
2635
+ /* 6 columns: 2 → 3 → 6 */
2636
+ .dds-tile-grid-6 {
2637
+ grid-template-columns: repeat(2, 1fr);
2638
+ }
2639
+ @media (min-width: 640px) {
2640
+ .dds-tile-grid-6 {
2641
+ grid-template-columns: repeat(3, 1fr);
2642
+ }
2643
+ }
2644
+ @media (min-width: 1024px) {
2645
+ .dds-tile-grid-6 {
2646
+ grid-template-columns: repeat(6, 1fr);
2647
+ }
2648
+ }
2397
2649
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roadlittledawn/docs-design-system-react",
3
- "version": "0.12.2",
3
+ "version": "0.13.0",
4
4
  "license": "MIT",
5
5
  "description": "React components for documentation design system",
6
6
  "repository": {