@stoovles/gap-kit 1.0.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/dist/index.js +2417 -0
- package/dist/styles.css +4309 -0
- package/guidelines/components/accordion.md +99 -0
- package/guidelines/components/breadcrumb.md +74 -0
- package/guidelines/components/buttons.md +90 -0
- package/guidelines/components/checkbox.md +86 -0
- package/guidelines/components/chip.md +77 -0
- package/guidelines/components/divider.md +52 -0
- package/guidelines/components/dropdown.md +125 -0
- package/guidelines/components/filter-sort.md +108 -0
- package/guidelines/components/fulfillment.md +100 -0
- package/guidelines/components/global-footer.md +71 -0
- package/guidelines/components/global-header.md +67 -0
- package/guidelines/components/hamburger-nav.md +79 -0
- package/guidelines/components/handle.md +49 -0
- package/guidelines/components/icons.md +147 -0
- package/guidelines/components/link.md +70 -0
- package/guidelines/components/logo.md +63 -0
- package/guidelines/components/mega-nav.md +103 -0
- package/guidelines/components/notification.md +97 -0
- package/guidelines/components/pagination.md +88 -0
- package/guidelines/components/popover.md +77 -0
- package/guidelines/components/price.md +86 -0
- package/guidelines/components/product-card.md +82 -0
- package/guidelines/components/radio.md +92 -0
- package/guidelines/components/search-input.md +63 -0
- package/guidelines/components/selector-swatch.md +78 -0
- package/guidelines/components/selector.md +95 -0
- package/guidelines/components/slider.md +116 -0
- package/guidelines/components/switch.md +108 -0
- package/guidelines/components/tabs.md +83 -0
- package/guidelines/components/text-input.md +80 -0
- package/guidelines/composition/buy-box.md +132 -0
- package/guidelines/composition/recommendations.md +100 -0
- package/package.json +38 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# GapLogo
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Use the `GapLogo` component to render the official Gap wordmark. Never recreate the logo with text, fonts, or custom SVGs. Always import from `@stoovles/gap-kit`.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { GapLogo } from '@stoovles/gap-kit'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Props
|
|
12
|
+
|
|
13
|
+
| Prop | Type | Default | Description |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| `color` | `"dark" \| "light"` | `"dark"` | Dark renders in `--color-type-primary` (#000), Light renders in `--color-type-inverse` (#fff) |
|
|
16
|
+
| `height` | `number` | `60` | Height in pixels; width scales proportionally (aspect ratio 81:60) |
|
|
17
|
+
| `className` | `string` | — | Additional CSS class |
|
|
18
|
+
| `aria-label` | `string` | `"Gap"` | Accessible label |
|
|
19
|
+
|
|
20
|
+
## Sizing
|
|
21
|
+
|
|
22
|
+
The logo has a fixed aspect ratio of **81:60** (1.35:1). Only set `height` — width is calculated automatically.
|
|
23
|
+
|
|
24
|
+
| Context | Recommended height |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Global header (desktop) | `24` – `32` |
|
|
27
|
+
| Global header (mobile) | `20` – `24` |
|
|
28
|
+
| Footer | `20` – `24` |
|
|
29
|
+
| Hero / splash | `48` – `60` |
|
|
30
|
+
|
|
31
|
+
## Color decision tree
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Which logo color should I use?
|
|
35
|
+
|
|
36
|
+
├─ On a light background (white, gray, subtle)?
|
|
37
|
+
│ └─ color="dark" (default)
|
|
38
|
+
│
|
|
39
|
+
└─ On a dark background (black, brand bar, dark overlay)?
|
|
40
|
+
└─ color="light"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Examples
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
{/* Default logo in header */}
|
|
47
|
+
<GapLogo height={24} />
|
|
48
|
+
|
|
49
|
+
{/* White logo on dark header bar */}
|
|
50
|
+
<GapLogo height={24} color="light" />
|
|
51
|
+
|
|
52
|
+
{/* Large logo for splash screen */}
|
|
53
|
+
<GapLogo height={60} />
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Rules
|
|
57
|
+
|
|
58
|
+
- Never distort the logo — only change `height`, width scales automatically
|
|
59
|
+
- Never change the logo color to anything other than "dark" (black) or "light" (white)
|
|
60
|
+
- Do not add borders, shadows, or effects to the logo
|
|
61
|
+
- Always provide sufficient contrast — dark on light backgrounds, light on dark backgrounds
|
|
62
|
+
- Do not place the logo inside colored containers that reduce contrast
|
|
63
|
+
- Minimum recommended height is 20px for legibility
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Mega Navigation
|
|
2
|
+
|
|
3
|
+
Desktop horizontal navigation bar with hover-triggered category dropdown panels. Contains utility links (top-right), a logo, department links, and an inline search input.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
import { MegaNav, SearchInput, GapLogo } from "@stoovles/gap-kit";
|
|
9
|
+
|
|
10
|
+
<MegaNav
|
|
11
|
+
logo={<GapLogo width={60} />}
|
|
12
|
+
onLogoClick={() => navigate("/")}
|
|
13
|
+
utilityLinks={[
|
|
14
|
+
{ label: "Find a Store", href: "/stores" },
|
|
15
|
+
{ label: "Rewards", href: "/rewards" },
|
|
16
|
+
{ label: "Gift Card", href: "/gift-cards" },
|
|
17
|
+
]}
|
|
18
|
+
departments={[
|
|
19
|
+
{
|
|
20
|
+
label: "Women",
|
|
21
|
+
href: "/women",
|
|
22
|
+
categories: [
|
|
23
|
+
{
|
|
24
|
+
title: "Clothing",
|
|
25
|
+
links: [
|
|
26
|
+
{ label: "Tops", href: "/women/tops" },
|
|
27
|
+
{ label: "Dresses", href: "/women/dresses" },
|
|
28
|
+
{ label: "Jeans", href: "/women/jeans" },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
title: "Accessories",
|
|
33
|
+
links: [
|
|
34
|
+
{ label: "Bags", href: "/women/bags" },
|
|
35
|
+
{ label: "Shoes", href: "/women/shoes" },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
marketingImages: [
|
|
40
|
+
{
|
|
41
|
+
imageUrl: "/promo-women.jpg",
|
|
42
|
+
label: "Spring collection",
|
|
43
|
+
href: "/women/spring",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{ label: "Men", href: "/men" },
|
|
48
|
+
{ label: "Today's Deals!", href: "/deals", sale: true },
|
|
49
|
+
]}
|
|
50
|
+
searchSlot={<SearchInput onSubmit={(q) => search(q)} />}
|
|
51
|
+
/>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Props
|
|
55
|
+
|
|
56
|
+
| Prop | Type | Default | Description |
|
|
57
|
+
|------|------|---------|-------------|
|
|
58
|
+
| `logo` | `ReactNode` | — | Logo element (e.g. `<GapLogo />`) |
|
|
59
|
+
| `onLogoClick` | `() => void` | — | Callback when logo is clicked |
|
|
60
|
+
| `departments` | `MegaNavDepartment[]` | **required** | Department links |
|
|
61
|
+
| `utilityLinks` | `{ label, href }[]` | `[]` | Top-right utility links |
|
|
62
|
+
| `searchSlot` | `ReactNode` | — | Search input slot |
|
|
63
|
+
|
|
64
|
+
### MegaNavDepartment
|
|
65
|
+
|
|
66
|
+
| Field | Type | Description |
|
|
67
|
+
|-------|------|-------------|
|
|
68
|
+
| `label` | `string` | Display text |
|
|
69
|
+
| `href` | `string` | Link URL |
|
|
70
|
+
| `sale` | `boolean` | Renders in sale red |
|
|
71
|
+
| `categories` | `MegaNavCategory[]` | Dropdown category columns |
|
|
72
|
+
| `marketingImages` | `MegaNavMarketingImage[]` | Promo images in dropdown |
|
|
73
|
+
|
|
74
|
+
### MegaNavCategory
|
|
75
|
+
|
|
76
|
+
| Field | Type | Description |
|
|
77
|
+
|-------|------|-------------|
|
|
78
|
+
| `title` | `string` | Category header |
|
|
79
|
+
| `links` | `{ label, href }[]` | Sub-category links |
|
|
80
|
+
|
|
81
|
+
### MegaNavMarketingImage
|
|
82
|
+
|
|
83
|
+
| Field | Type | Description |
|
|
84
|
+
|-------|------|-------------|
|
|
85
|
+
| `imageUrl` | `string` | Promotional image |
|
|
86
|
+
| `label` | `string` | Link text below image |
|
|
87
|
+
| `href` | `string` | Link destination |
|
|
88
|
+
|
|
89
|
+
## Visual reference
|
|
90
|
+
|
|
91
|
+
- **Utility links:** 14px accent color, slash-separated, aligned right
|
|
92
|
+
- **Nav bar:** Logo left, department links centered (32px gap), search right
|
|
93
|
+
- **Department links:** 16px, accent color; sale items in red
|
|
94
|
+
- **Dropdown:** Full-width white panel with shadow; up to 5 category columns (208px max each) + 2 marketing image columns (208px)
|
|
95
|
+
- **Category headers:** 16px primary; links: 12px secondary gray, 4px vertical padding
|
|
96
|
+
- **Marketing images:** 87:122 aspect ratio with underlined link below
|
|
97
|
+
|
|
98
|
+
## Rules
|
|
99
|
+
|
|
100
|
+
- The dropdown appears on hover over a department link and dismisses on mouse-leave.
|
|
101
|
+
- Departments without `categories` do not open a dropdown.
|
|
102
|
+
- Use the `searchSlot` to pass an existing `SearchInput` component.
|
|
103
|
+
- The `sale` flag should be reserved for one or two department links maximum.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Notification
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Use the `Notification` component to display contextual messages — informational updates, warnings, success confirmations, or error alerts. Available in three layout variants depending on prominence.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Notification } from '@stoovles/gap-kit'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Props
|
|
12
|
+
|
|
13
|
+
| Prop | Type | Default | Description |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| `type` | `"info" \| "warning" \| "success" \| "error"` | `"info"` | Controls background color, icon, and semantics |
|
|
16
|
+
| `variant` | `"banner" \| "inline" \| "bare"` | `"banner"` | Layout size — banner is full, inline is compact, bare is text-only |
|
|
17
|
+
| `children` | `ReactNode` | — | Message content |
|
|
18
|
+
| `showIcon` | `boolean` | `true` | Whether to render the type icon (not shown in `bare` variant) |
|
|
19
|
+
| `onDismiss` | `() => void` | — | If provided, shows a close (X) button — only in `banner` variant |
|
|
20
|
+
| `className` | `string` | — | Additional CSS class |
|
|
21
|
+
|
|
22
|
+
## Visual reference
|
|
23
|
+
|
|
24
|
+
### Type backgrounds
|
|
25
|
+
|
|
26
|
+
| Type | Background | Icon color | Text color |
|
|
27
|
+
|---|---|---|---|
|
|
28
|
+
| `info` | Transparent | Primary (#000) | Primary |
|
|
29
|
+
| `warning` | Light yellow (`--color-background-warning`) | Amber (`--color-type-warning`) | Primary |
|
|
30
|
+
| `success` | Light green (`--color-background-success`) | Green (`--color-type-success`) | Primary |
|
|
31
|
+
| `error` | Light red (`--color-background-error`) | Red (`--color-type-error`) | Primary |
|
|
32
|
+
|
|
33
|
+
### Variant layouts
|
|
34
|
+
|
|
35
|
+
| Variant | Padding | Gap | Close button | Icon |
|
|
36
|
+
|---|---|---|---|---|
|
|
37
|
+
| `banner` | 16px | 16px | Yes (if `onDismiss`) | Yes |
|
|
38
|
+
| `inline` | 8px | 8px | No | Yes |
|
|
39
|
+
| `bare` | 8px | 0 | No | No |
|
|
40
|
+
|
|
41
|
+
### Bare text color overrides
|
|
42
|
+
|
|
43
|
+
| Type | Text color |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `error` | Red (`--color-type-error`) |
|
|
46
|
+
| `success` | Green (`--color-type-success`) |
|
|
47
|
+
| `info` / `warning` | Primary (#000) |
|
|
48
|
+
|
|
49
|
+
## Sizing
|
|
50
|
+
|
|
51
|
+
- Icon: 16 × 16px, inline SVG
|
|
52
|
+
- Font: Gap Sans, 10px (`--text-caption-font-size`), weight 400, letter-spacing 0.2px
|
|
53
|
+
- Close button: 16 × 16px
|
|
54
|
+
|
|
55
|
+
## Examples
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
{/* Info banner with dismiss */}
|
|
59
|
+
<Notification type="info" onDismiss={() => setShow(false)}>
|
|
60
|
+
Your order has been updated. Check your email for details.
|
|
61
|
+
</Notification>
|
|
62
|
+
|
|
63
|
+
{/* Warning banner */}
|
|
64
|
+
<Notification type="warning" onDismiss={() => {}}>
|
|
65
|
+
Please re-enter your credit card security code (CVV) to continue.
|
|
66
|
+
</Notification>
|
|
67
|
+
|
|
68
|
+
{/* Success banner */}
|
|
69
|
+
<Notification type="success" onDismiss={() => {}}>
|
|
70
|
+
Your payment was processed successfully.
|
|
71
|
+
</Notification>
|
|
72
|
+
|
|
73
|
+
{/* Error banner */}
|
|
74
|
+
<Notification type="error" onDismiss={() => {}}>
|
|
75
|
+
We couldn't process your request. Please try again.
|
|
76
|
+
</Notification>
|
|
77
|
+
|
|
78
|
+
{/* Inline compact notification */}
|
|
79
|
+
<Notification type="success" variant="inline">
|
|
80
|
+
Item added to bag.
|
|
81
|
+
</Notification>
|
|
82
|
+
|
|
83
|
+
{/* Bare text-only (e.g. under a form field) */}
|
|
84
|
+
<Notification type="error" variant="bare">
|
|
85
|
+
Please enter a valid email address.
|
|
86
|
+
</Notification>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Rules
|
|
90
|
+
|
|
91
|
+
- Use `banner` for page-level or section-level messages that need attention
|
|
92
|
+
- Use `inline` for compact contextual feedback within a flow (e.g. below a form section)
|
|
93
|
+
- Use `bare` for minimal text-only messages (e.g. field-level validation summaries)
|
|
94
|
+
- The `error` type renders with `role="alert"` for screen reader announcement; all others use `role="status"`
|
|
95
|
+
- Provide `onDismiss` only when the user should be able to close the message
|
|
96
|
+
- Do not use notifications for permanent content — they are transient messages
|
|
97
|
+
- Keep message text concise and actionable
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Pagination
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Use the `Pagination` component for progressive loading on product listing pages. It shows the user how many items they've seen, offers a button to load more, and optionally provides a "Back to Top" shortcut.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Pagination } from '@stoovles/gap-kit'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Props
|
|
12
|
+
|
|
13
|
+
| Prop | Type | Default | Description |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| `shown` | `number` | — | Number of items currently displayed |
|
|
16
|
+
| `total` | `number` | — | Total number of items available |
|
|
17
|
+
| `moreCount` | `number` | — | Items remaining to load — renders the "View N more" button |
|
|
18
|
+
| `loading` | `boolean` | `false` | Shows a loading indicator inside the button |
|
|
19
|
+
| `onLoadMore` | `() => void` | — | Callback when "View more" is clicked |
|
|
20
|
+
| `showBackToTop` | `boolean` | `false` | Replaces the "View more" button with "Back to Top" |
|
|
21
|
+
| `onBackToTop` | `() => void` | — | Callback when "Back to Top" is clicked |
|
|
22
|
+
| `className` | `string` | — | Additional CSS class |
|
|
23
|
+
|
|
24
|
+
## Visual reference
|
|
25
|
+
|
|
26
|
+
| State | Count text | Button |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| View More | "48 of 120 items" | Outlined — "View 24 more" |
|
|
29
|
+
| Loading | "48 of 120 items" | Solid black with dot loader |
|
|
30
|
+
| Back to Top | "120 of 120 items" | Outlined — "Back to Top" |
|
|
31
|
+
|
|
32
|
+
### Button states
|
|
33
|
+
|
|
34
|
+
| State | Background | Border | Text |
|
|
35
|
+
|---|---|---|---|
|
|
36
|
+
| Default | White | 1px accent (#000) | Accent (#000) |
|
|
37
|
+
| Hover | Brand blue (`--button-secondary-hover-fill`) | Brand blue | White, underlined |
|
|
38
|
+
| Press | White | Accent (#000) | Accent (#000) |
|
|
39
|
+
| Loading | Black (`--button-secondary-fill`) | Black | White dots |
|
|
40
|
+
|
|
41
|
+
## Sizing
|
|
42
|
+
|
|
43
|
+
- Vertical padding: 24px (`--spacing-utk-xl`)
|
|
44
|
+
- Gap (count to button): 16px (`--spacing-utk-l`)
|
|
45
|
+
- Button: min-height 44px, min-width 175px, horizontal padding 24px
|
|
46
|
+
- Count font: Gap Sans, 16px, weight 400, 0.32px letter-spacing
|
|
47
|
+
- Button font: Gap Sans, 16px (`--text-button-font-size`), weight 400
|
|
48
|
+
- Loader dots: 3px / 4.5px / 6px white circles
|
|
49
|
+
|
|
50
|
+
## Examples
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
{/* Standard "View more" */}
|
|
54
|
+
<Pagination
|
|
55
|
+
shown={48}
|
|
56
|
+
total={120}
|
|
57
|
+
moreCount={24}
|
|
58
|
+
onLoadMore={handleLoadMore}
|
|
59
|
+
/>
|
|
60
|
+
|
|
61
|
+
{/* Loading state */}
|
|
62
|
+
<Pagination
|
|
63
|
+
shown={48}
|
|
64
|
+
total={120}
|
|
65
|
+
loading
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
{/* All items loaded — show "Back to Top" */}
|
|
69
|
+
<Pagination
|
|
70
|
+
shown={120}
|
|
71
|
+
total={120}
|
|
72
|
+
showBackToTop
|
|
73
|
+
onBackToTop={() => window.scrollTo({ top: 0, behavior: "smooth" })}
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
{/* Count only (no button) */}
|
|
77
|
+
<Pagination shown={120} total={120} />
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Rules
|
|
81
|
+
|
|
82
|
+
- Always provide `shown` and `total` — the count text is always visible
|
|
83
|
+
- Pass `moreCount` to show the "View more" button; omit it when all items are loaded
|
|
84
|
+
- Set `loading` to `true` while fetching to swap the button content to the loader
|
|
85
|
+
- Use `showBackToTop` after all items are loaded to offer scroll-to-top navigation
|
|
86
|
+
- `showBackToTop` takes priority over `moreCount` — they should not both be active
|
|
87
|
+
- Do not show both loading and "View more" buttons simultaneously
|
|
88
|
+
- The component is full-width and centers its content
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Popover
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Use the `Popover` component to overlay supplementary content near a trigger element — tooltips, contextual help, short descriptions, or inline guidance. It renders as a white card with a directional arrow pointing toward the trigger.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Popover } from '@stoovles/gap-kit'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Props
|
|
12
|
+
|
|
13
|
+
| Prop | Type | Default | Description |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| `position` | `"top" \| "bottom" \| "left" \| "right"` | `"top"` | Which side of the trigger the popover appears on |
|
|
16
|
+
| `align` | `"start" \| "center" \| "end"` | `"center"` | Where the arrow sits along the popover edge |
|
|
17
|
+
| `children` | `ReactNode` | — | Content inside the popover body |
|
|
18
|
+
| `className` | `string` | — | Additional CSS class |
|
|
19
|
+
|
|
20
|
+
## Visual reference
|
|
21
|
+
|
|
22
|
+
| Element | Style |
|
|
23
|
+
|---|---|
|
|
24
|
+
| Background | White (`--color-background-default-white`) |
|
|
25
|
+
| Shadow | 2px 4px 8px rgba(0,0,0,0.08) (`--drop-shadow-high-*`) |
|
|
26
|
+
| Arrow | 18 × 9px CSS triangle matching the white background |
|
|
27
|
+
| Text | Gap Sans, 12px, weight 400, 0.24px letter-spacing, secondary color (`--color-type-secondary`) |
|
|
28
|
+
|
|
29
|
+
### Position + Align combinations
|
|
30
|
+
|
|
31
|
+
| Position | Arrow direction | Align start | Align center | Align end |
|
|
32
|
+
|---|---|---|---|---|
|
|
33
|
+
| `top` | Points down | Left-aligned arrow | Centered arrow | Right-aligned arrow |
|
|
34
|
+
| `bottom` | Points up | Left-aligned arrow | Centered arrow | Right-aligned arrow |
|
|
35
|
+
| `left` | Points right | Arrow near top | Arrow centered | Arrow near bottom |
|
|
36
|
+
| `right` | Points left | Arrow near top | Arrow centered | Arrow near bottom |
|
|
37
|
+
|
|
38
|
+
## Sizing
|
|
39
|
+
|
|
40
|
+
- Width: 280px
|
|
41
|
+
- Padding: 12px vertical (`--spacing-utk-m`), 16px horizontal (`--spacing-utk-l`)
|
|
42
|
+
- Arrow: 18px wide × 9px tall (CSS border triangle)
|
|
43
|
+
- Font: Gap Sans, 12px (`--font-size--1`), weight 400, letter-spacing 0.24px
|
|
44
|
+
- Text color: `--color-type-secondary` (#595959)
|
|
45
|
+
|
|
46
|
+
## Examples
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
{/* Tooltip above, centered arrow */}
|
|
50
|
+
<Popover position="top" align="center">
|
|
51
|
+
Free shipping on orders over $50
|
|
52
|
+
</Popover>
|
|
53
|
+
|
|
54
|
+
{/* Below trigger, arrow aligned to start */}
|
|
55
|
+
<Popover position="bottom" align="start">
|
|
56
|
+
Select your preferred size for accurate fit recommendations.
|
|
57
|
+
</Popover>
|
|
58
|
+
|
|
59
|
+
{/* Right of trigger, arrow centered */}
|
|
60
|
+
<Popover position="right" align="center">
|
|
61
|
+
<p>This item is final sale and cannot be returned.</p>
|
|
62
|
+
</Popover>
|
|
63
|
+
|
|
64
|
+
{/* Left of trigger, arrow at end */}
|
|
65
|
+
<Popover position="left" align="end">
|
|
66
|
+
Price shown includes member discount.
|
|
67
|
+
</Popover>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Rules
|
|
71
|
+
|
|
72
|
+
- The `Popover` is a visual-only element — positioning relative to a trigger must be handled by layout or JavaScript
|
|
73
|
+
- Use `position` to match where the popover sits relative to its trigger; the arrow automatically points toward the trigger
|
|
74
|
+
- Keep content concise — the popover is 280px wide; long text will wrap
|
|
75
|
+
- Do not nest interactive elements (buttons, links) inside unless absolutely necessary
|
|
76
|
+
- For dismissible or complex overlays, consider a modal or dialog instead
|
|
77
|
+
- The arrow is a pure CSS triangle — no images required
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Price
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Use the `Price` component to display product pricing consistently across the site — in product cards, product detail pages, cart, and checkout. It handles regular prices, sale prices with strikethrough, discount percentages, and price ranges.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Price } from '@stoovles/gap-kit'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Props
|
|
12
|
+
|
|
13
|
+
| Prop | Type | Default | Description |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| `amount` | `string` | — | Current / sale price (e.g. `"$29.99"`) |
|
|
16
|
+
| `originalAmount` | `string` | — | Original price before sale, shown with strikethrough. Omit for non-sale items. |
|
|
17
|
+
| `discount` | `string` | — | Discount label (e.g. `"30% off"`). Only shown during sale. |
|
|
18
|
+
| `rangeEnd` | `string` | — | Upper bound of a price range (e.g. `"$49.99"`) |
|
|
19
|
+
| `layout` | `"inline" \| "stacked"` | `"inline"` | Inline places all elements in a row; stacked arranges vertically |
|
|
20
|
+
| `className` | `string` | — | Additional CSS class |
|
|
21
|
+
|
|
22
|
+
## Layout decision tree
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Where is this price displayed?
|
|
26
|
+
|
|
27
|
+
├─ Product card or compact listing?
|
|
28
|
+
│ └─ layout="inline"
|
|
29
|
+
│
|
|
30
|
+
└─ Product detail page or area with more vertical space?
|
|
31
|
+
└─ layout="stacked"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Visual reference
|
|
35
|
+
|
|
36
|
+
| Scenario | Display |
|
|
37
|
+
|---|---|
|
|
38
|
+
| Regular price | `$29.99` in black |
|
|
39
|
+
| Sale (inline) | ~~`$49.99`~~ gray strikethrough, `30% off` dark gray, `$29.99` red |
|
|
40
|
+
| Sale (stacked) | ~~`$49.99`~~ on line 1, `30% off` on line 2, `$29.99` red on line 3 |
|
|
41
|
+
| Sale + range | `$29.99` red ` - $39.99` black |
|
|
42
|
+
|
|
43
|
+
## Color mapping
|
|
44
|
+
|
|
45
|
+
| Element | Token | Gap value |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| Regular price | `--color-type-primary` | #000000 |
|
|
48
|
+
| Original (strikethrough) | `--product-price-strikethrough` | #757575 |
|
|
49
|
+
| Discount text | `--product-price-percent-off-font` | #595959 |
|
|
50
|
+
| Sale price | `--color-type-sale` | #e10000 |
|
|
51
|
+
|
|
52
|
+
## Examples
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
{/* Regular price */}
|
|
56
|
+
<Price amount="$39.95" />
|
|
57
|
+
|
|
58
|
+
{/* Sale price */}
|
|
59
|
+
<Price amount="$29.99" originalAmount="$49.99" discount="40% off" />
|
|
60
|
+
|
|
61
|
+
{/* Stacked sale */}
|
|
62
|
+
<Price
|
|
63
|
+
amount="$29.99"
|
|
64
|
+
originalAmount="$49.99"
|
|
65
|
+
discount="40% off"
|
|
66
|
+
layout="stacked"
|
|
67
|
+
/>
|
|
68
|
+
|
|
69
|
+
{/* Sale with range */}
|
|
70
|
+
<Price
|
|
71
|
+
amount="$19.99"
|
|
72
|
+
originalAmount="$39.99"
|
|
73
|
+
discount="50% off"
|
|
74
|
+
rangeEnd="$29.99"
|
|
75
|
+
layout="stacked"
|
|
76
|
+
/>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Rules
|
|
80
|
+
|
|
81
|
+
- Always format prices with a dollar sign and two decimal places (`$0.00`)
|
|
82
|
+
- `originalAmount` must be higher than `amount` — never show a "sale" where the price went up
|
|
83
|
+
- Only show `discount` when `originalAmount` is also provided
|
|
84
|
+
- Use `rangeEnd` only with `layout="stacked"` for clarity
|
|
85
|
+
- Do not add custom colors — sale red and strikethrough gray are controlled by tokens
|
|
86
|
+
- Keep the same `layout` for all prices within a product card for consistency
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Product Card
|
|
2
|
+
|
|
3
|
+
A product listing card used on PLP grids, search results, and recommendation carousels. Available in two variants: `default` (vertical) and `mini` (horizontal).
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
import { ProductCard } from "@stoovles/gap-kit";
|
|
9
|
+
|
|
10
|
+
<ProductCard
|
|
11
|
+
imageUrl="/images/denim-jacket.jpg"
|
|
12
|
+
name="Classic Denim Jacket"
|
|
13
|
+
originalPrice="$89.95"
|
|
14
|
+
salePrice="$59.99"
|
|
15
|
+
discount="33% off"
|
|
16
|
+
badge="New"
|
|
17
|
+
marketingFlag="Selling fast"
|
|
18
|
+
rating={4}
|
|
19
|
+
reviewCount={128}
|
|
20
|
+
swatches={[
|
|
21
|
+
{ color: "#8BA3C7", label: "Light Wash" },
|
|
22
|
+
{ color: "#2C3E50", label: "Dark Wash" },
|
|
23
|
+
]}
|
|
24
|
+
moreSwatches={6}
|
|
25
|
+
/>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Props
|
|
29
|
+
|
|
30
|
+
| Prop | Type | Default | Description |
|
|
31
|
+
|------|------|---------|-------------|
|
|
32
|
+
| `variant` | `"default" \| "mini"` | `"default"` | Card layout |
|
|
33
|
+
| `imageUrl` | `string` | — | Product image URL |
|
|
34
|
+
| `imageAlt` | `string` | `""` | Image alt text |
|
|
35
|
+
| `name` | `string` | **required** | Product name |
|
|
36
|
+
| `badge` | `string` | — | Overlay badge text (default only) |
|
|
37
|
+
| `originalPrice` | `string` | — | Strikethrough price |
|
|
38
|
+
| `salePrice` | `string` | — | Sale price in red |
|
|
39
|
+
| `discount` | `string` | — | Discount text (e.g. "30% off") |
|
|
40
|
+
| `marketingFlag` | `string` | — | Marketing callout (default only) |
|
|
41
|
+
| `rating` | `number` | — | Star rating 0–5 |
|
|
42
|
+
| `reviewCount` | `number` | — | Number of reviews |
|
|
43
|
+
| `swatches` | `ProductCardSwatch[]` | — | Color swatch array |
|
|
44
|
+
| `moreSwatches` | `number` | — | Additional swatch count |
|
|
45
|
+
| `onClick` | `() => void` | — | Card click handler |
|
|
46
|
+
|
|
47
|
+
## Visual reference
|
|
48
|
+
|
|
49
|
+
### Default variant (220px wide)
|
|
50
|
+
- Image area: full width, 136:181 aspect ratio
|
|
51
|
+
- Badge: bottom-left overlay on image
|
|
52
|
+
- Swatches: 20px circles with 4px ring padding, first has active outline
|
|
53
|
+
- Product name: 16px neutral gray
|
|
54
|
+
- Price: stacked (original strikethrough → discount → sale)
|
|
55
|
+
- Marketing flag: 16px gray
|
|
56
|
+
- Star rating: 14px filled/outline stars + review count link
|
|
57
|
+
|
|
58
|
+
### Mini variant (326px wide)
|
|
59
|
+
- 60×80px rounded image thumbnail on the left
|
|
60
|
+
- Product name + inline price on the right
|
|
61
|
+
|
|
62
|
+
## Examples
|
|
63
|
+
|
|
64
|
+
Mini card for recently viewed:
|
|
65
|
+
|
|
66
|
+
```jsx
|
|
67
|
+
<ProductCard
|
|
68
|
+
variant="mini"
|
|
69
|
+
imageUrl="/thumb.jpg"
|
|
70
|
+
name="Relaxed Fit Jeans"
|
|
71
|
+
originalPrice="$59.95"
|
|
72
|
+
salePrice="$39.99"
|
|
73
|
+
discount="33% off"
|
|
74
|
+
/>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Rules
|
|
78
|
+
|
|
79
|
+
- Always provide `name` — it's the only required field.
|
|
80
|
+
- Swatches only render in the default variant.
|
|
81
|
+
- If no `imageUrl` is given, a gray placeholder is rendered.
|
|
82
|
+
- The card width can be overridden via `className` for responsive grids.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Radio
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Use `Radio` when the user must select exactly one option from a list of mutually-exclusive choices. For multi-select, use `Checkbox` instead.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Radio, RadioGroup } from '@stoovles/gap-kit'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Radio Props
|
|
12
|
+
|
|
13
|
+
| Prop | Type | Default | Description |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| `label` | `ReactNode` | — | Text label displayed next to the radio |
|
|
16
|
+
| `error` | `boolean` | `false` | Renders the radio in the error state (red) |
|
|
17
|
+
| `disabled` | `boolean` | `false` | Grays out the radio and prevents interaction |
|
|
18
|
+
| `checked` | `boolean` | — | Controlled checked state |
|
|
19
|
+
| `value` | `string` | — | Value submitted with the form |
|
|
20
|
+
| `onChange` | `(e) => void` | — | Change handler |
|
|
21
|
+
| `className` | `string` | — | Additional CSS class |
|
|
22
|
+
|
|
23
|
+
All standard `<input>` HTML attributes are also supported.
|
|
24
|
+
|
|
25
|
+
## RadioGroup Props
|
|
26
|
+
|
|
27
|
+
| Prop | Type | Default | Description |
|
|
28
|
+
|---|---|---|---|
|
|
29
|
+
| `children` | `ReactNode` | — | `Radio` components |
|
|
30
|
+
| `name` | `string` | — | Shared `name` attribute automatically applied to all child radios |
|
|
31
|
+
| `label` | `string` | — | Group legend (optional) |
|
|
32
|
+
| `error` | `boolean` | `false` | Applies error styling to the group |
|
|
33
|
+
| `className` | `string` | — | Additional CSS class |
|
|
34
|
+
|
|
35
|
+
## Visual reference
|
|
36
|
+
|
|
37
|
+
| State | Unselected | Selected |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| Default | White circle, black 1px border | White circle, black border, dark gray inner dot (#595959) |
|
|
40
|
+
| Error | White circle, red border | White circle, red border, red inner dot (#d00000) |
|
|
41
|
+
| Disabled | White circle, gray border (#ededed) | White circle, gray border, gray inner dot (#ededed) |
|
|
42
|
+
|
|
43
|
+
## Sizing
|
|
44
|
+
|
|
45
|
+
- Outer circle: 20px inside a 24px touch target
|
|
46
|
+
- Inner dot (selected): 10px
|
|
47
|
+
- Gap between radio and label: 4px
|
|
48
|
+
- Group spacing between items: 24px
|
|
49
|
+
- Group vertical padding: 16px
|
|
50
|
+
- Font: Gap Sans, 16px, weight 400
|
|
51
|
+
|
|
52
|
+
## Examples
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
{/* RadioGroup with controlled value */}
|
|
56
|
+
<RadioGroup name="shipping" label="Shipping method">
|
|
57
|
+
<Radio
|
|
58
|
+
label="Standard (5-7 days)"
|
|
59
|
+
value="standard"
|
|
60
|
+
checked={shipping === 'standard'}
|
|
61
|
+
onChange={() => setShipping('standard')}
|
|
62
|
+
/>
|
|
63
|
+
<Radio
|
|
64
|
+
label="Express (2-3 days)"
|
|
65
|
+
value="express"
|
|
66
|
+
checked={shipping === 'express'}
|
|
67
|
+
onChange={() => setShipping('express')}
|
|
68
|
+
/>
|
|
69
|
+
<Radio
|
|
70
|
+
label="Next day"
|
|
71
|
+
value="nextday"
|
|
72
|
+
checked={shipping === 'nextday'}
|
|
73
|
+
onChange={() => setShipping('nextday')}
|
|
74
|
+
/>
|
|
75
|
+
</RadioGroup>
|
|
76
|
+
|
|
77
|
+
{/* Error state */}
|
|
78
|
+
<RadioGroup name="size" label="Select a size" error>
|
|
79
|
+
<Radio label="S" value="s" error />
|
|
80
|
+
<Radio label="M" value="m" error />
|
|
81
|
+
<Radio label="L" value="l" error />
|
|
82
|
+
</RadioGroup>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Rules
|
|
86
|
+
|
|
87
|
+
- Always wrap radios in a `RadioGroup` with a shared `name`
|
|
88
|
+
- Provide a `label` on the group via `<legend>` for accessibility
|
|
89
|
+
- Always provide a `label` on each radio — if no visible text, use `aria-label`
|
|
90
|
+
- Pre-select a default option whenever possible
|
|
91
|
+
- Never use a single radio — use a checkbox for boolean toggles
|
|
92
|
+
- Do not mix error and disabled states in the same group
|