@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
|
+
# Search Input
|
|
2
|
+
|
|
3
|
+
A minimal search bar designed for the global header. Features a bottom-border style, inline clear action, a vertical divider, and a search magnifying-glass icon.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
import { SearchInput } from "@stoovles/gap-kit";
|
|
9
|
+
|
|
10
|
+
<SearchInput
|
|
11
|
+
placeholder="Search"
|
|
12
|
+
onSubmit={(term) => console.log("Search:", term)}
|
|
13
|
+
/>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Description |
|
|
19
|
+
|------|------|---------|-------------|
|
|
20
|
+
| `value` | `string` | — | Controlled value |
|
|
21
|
+
| `defaultValue` | `string` | `""` | Initial uncontrolled value |
|
|
22
|
+
| `placeholder` | `string` | `"Search"` | Placeholder text |
|
|
23
|
+
| `onChange` | `(value: string) => void` | — | Fires on every keystroke |
|
|
24
|
+
| `onSubmit` | `(value: string) => void` | — | Fires on Enter or search icon click |
|
|
25
|
+
| `onClear` | `() => void` | — | Fires when the Clear button is clicked |
|
|
26
|
+
|
|
27
|
+
## Visual reference
|
|
28
|
+
|
|
29
|
+
- **Background:** Semi-transparent light gray (`--search-input-background`)
|
|
30
|
+
- **Bottom border:** 1px, shifts to accent blue when focused
|
|
31
|
+
- **Caret:** Accent blue (`--search-cursor-color`)
|
|
32
|
+
- **Clear action:** 10px helper text + 1px vertical divider + 24×24 search icon
|
|
33
|
+
- **Size:** 270×32px default, minimum 228px
|
|
34
|
+
|
|
35
|
+
## States
|
|
36
|
+
|
|
37
|
+
| State | Appearance |
|
|
38
|
+
|-------|-----------|
|
|
39
|
+
| Default (empty) | "Search" placeholder, search icon right |
|
|
40
|
+
| Active (empty) | Accent bottom border, dimmed placeholder, blinking caret |
|
|
41
|
+
| Active (filled) | Search term + caret, "Clear | 🔍" on right |
|
|
42
|
+
| Default (filled) | Search term, "Clear | 🔍" on right |
|
|
43
|
+
|
|
44
|
+
## Examples
|
|
45
|
+
|
|
46
|
+
Controlled search:
|
|
47
|
+
|
|
48
|
+
```jsx
|
|
49
|
+
const [term, setTerm] = useState("");
|
|
50
|
+
|
|
51
|
+
<SearchInput
|
|
52
|
+
value={term}
|
|
53
|
+
onChange={setTerm}
|
|
54
|
+
onSubmit={(t) => navigate(`/search?q=${t}`)}
|
|
55
|
+
onClear={() => setTerm("")}
|
|
56
|
+
/>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Rules
|
|
60
|
+
|
|
61
|
+
- Place the search input inside the Global Header actions area.
|
|
62
|
+
- Always provide `onSubmit`; users expect Enter to trigger search.
|
|
63
|
+
- The "Clear" button only appears when the field has content.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Selector Swatch
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Use `SelectorSwatch` to let customers choose a product color. Each swatch is a circular button filled with the color value. Wrap multiple swatches in a `SwatchGrid` for proper layout.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { SelectorSwatch, SwatchGrid } from '@stoovles/gap-kit'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Props
|
|
12
|
+
|
|
13
|
+
### SelectorSwatch
|
|
14
|
+
|
|
15
|
+
| Prop | Type | Default | Description |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| `color` | `string` | — | CSS color value for the circle fill |
|
|
18
|
+
| `active` | `boolean` | `false` | Whether the swatch is selected |
|
|
19
|
+
| `focused` | `boolean` | `false` | Whether the swatch has keyboard focus (thicker ring) |
|
|
20
|
+
| `unavailable` | `boolean` | `false` | Shows a diagonal strikethrough over the color |
|
|
21
|
+
| `label` | `string` | — | Optional text label below the swatch |
|
|
22
|
+
| `size` | `"xs" \| "sm" \| "md" \| "lg" \| "xl"` | `"lg"` | Swatch diameter |
|
|
23
|
+
| `aria-label` | `string` | — | Accessible name for the color |
|
|
24
|
+
| `onClick` | `() => void` | — | Click handler |
|
|
25
|
+
| `className` | `string` | — | Additional CSS class |
|
|
26
|
+
|
|
27
|
+
### SwatchGrid
|
|
28
|
+
|
|
29
|
+
| Prop | Type | Default | Description |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| `children` | `ReactNode` | — | `SelectorSwatch` elements |
|
|
32
|
+
| `className` | `string` | — | Additional CSS class |
|
|
33
|
+
|
|
34
|
+
## Visual reference
|
|
35
|
+
|
|
36
|
+
| State | Ring | Circle border |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| Default (inactive) | Transparent | 1px subtle gray |
|
|
39
|
+
| Active (selected) | 1px accent blue | 1px subtle gray |
|
|
40
|
+
| Active + Focused | 2px accent blue | 1px subtle gray |
|
|
41
|
+
| Unavailable | — | 1px subtle gray + diagonal slash |
|
|
42
|
+
|
|
43
|
+
### Sizes
|
|
44
|
+
|
|
45
|
+
| Size | Circle diameter | Total with ring |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `xl` | 36px | 44px |
|
|
48
|
+
| `lg` | 32px | 40px |
|
|
49
|
+
| `md` | 28px | 36px |
|
|
50
|
+
| `sm` | 24px | 32px |
|
|
51
|
+
| `xs` | 20px | 28px |
|
|
52
|
+
|
|
53
|
+
## Examples
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
{/* Color swatch grid */}
|
|
57
|
+
<SwatchGrid>
|
|
58
|
+
<SelectorSwatch color="#000" active label="Black" onClick={() => setColor("black")} />
|
|
59
|
+
<SelectorSwatch color="#fff" label="White" onClick={() => setColor("white")} />
|
|
60
|
+
<SelectorSwatch color="#031ba1" label="Navy" onClick={() => setColor("navy")} />
|
|
61
|
+
<SelectorSwatch color="#d00000" unavailable label="Red" />
|
|
62
|
+
</SwatchGrid>
|
|
63
|
+
|
|
64
|
+
{/* Small swatches without labels */}
|
|
65
|
+
<SwatchGrid>
|
|
66
|
+
<SelectorSwatch color="#000" size="sm" active />
|
|
67
|
+
<SelectorSwatch color="#595959" size="sm" />
|
|
68
|
+
<SelectorSwatch color="#fff" size="sm" />
|
|
69
|
+
</SwatchGrid>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Rules
|
|
73
|
+
|
|
74
|
+
- Always pass a meaningful `aria-label` or `label` for accessibility
|
|
75
|
+
- Use `unavailable` for out-of-stock colors — the slash clearly communicates unavailability
|
|
76
|
+
- The `active` ring uses `--color-border-accent` (brand blue in Gap 1.0.0)
|
|
77
|
+
- The circle border uses `--color-border-subtle` for a clean, light outline
|
|
78
|
+
- Use `SwatchGrid` to wrap swatches — it provides flex-wrap layout with proper spacing
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Selector
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Use `Selector` for picking a single value from a set of discrete options — most commonly product sizes. Use `SelectorGroup` to add a header label above a set of selectors.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Selector, SelectorGroup } from '@stoovles/gap-kit'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Props
|
|
12
|
+
|
|
13
|
+
### Selector
|
|
14
|
+
|
|
15
|
+
| Prop | Type | Default | Description |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| `options` | `SelectorOption[]` | — | Array of selectable options |
|
|
18
|
+
| `value` | `string` | — | Controlled selected value |
|
|
19
|
+
| `defaultValue` | `string` | — | Uncontrolled initial value |
|
|
20
|
+
| `onChange` | `(value: string) => void` | — | Selection change callback |
|
|
21
|
+
| `className` | `string` | — | Additional CSS class |
|
|
22
|
+
|
|
23
|
+
### SelectorOption
|
|
24
|
+
|
|
25
|
+
| Field | Type | Description |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `label` | `string` | Display text inside the pill |
|
|
28
|
+
| `value` | `string` | Unique value |
|
|
29
|
+
| `unavailable` | `boolean` | Shows diagonal slash — still clickable |
|
|
30
|
+
| `disabled` | `boolean` | Grayed out — not clickable |
|
|
31
|
+
|
|
32
|
+
### SelectorGroup
|
|
33
|
+
|
|
34
|
+
| Prop | Type | Default | Description |
|
|
35
|
+
|---|---|---|---|
|
|
36
|
+
| `label` | `string` | — | Group header text (e.g. "Size") |
|
|
37
|
+
| `selectedLabel` | `string` | — | Currently selected value shown after header |
|
|
38
|
+
| `children` | `ReactNode` | — | A `Selector` or other content |
|
|
39
|
+
| `className` | `string` | — | Additional CSS class |
|
|
40
|
+
|
|
41
|
+
## Visual reference
|
|
42
|
+
|
|
43
|
+
| State | Background | Border | Text |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| Default | White | 2px gray (`--selector-size-border-default`) | Black |
|
|
46
|
+
| Selected | White | 2px black (`--selector-size-border-selected`) | Black |
|
|
47
|
+
| Hover | White | 2px black (`--selector-size-border-hover`) | Black |
|
|
48
|
+
| Unavailable | White | 2px gray | Black + diagonal slash |
|
|
49
|
+
| Disabled | Gray (`--selector-size-unavailable-disabled`) | 2px light gray | Gray (`--selector-size-font-disabled`) |
|
|
50
|
+
|
|
51
|
+
## Sizing
|
|
52
|
+
|
|
53
|
+
- Pill: min-width 44px, min-height 44px
|
|
54
|
+
- Padding: 8px vertical, 12px horizontal
|
|
55
|
+
- Border radius: 0px (Gap square-corner identity)
|
|
56
|
+
- Font: Gap Sans, 16px (`--selector-size-font-size`), weight 400
|
|
57
|
+
- Gap between pills: 8px
|
|
58
|
+
- Group header: 16px, weight 400 (`--font-weight-base-heavier`)
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
{/* Basic size selector */}
|
|
64
|
+
<SelectorGroup label="Size" selectedLabel="M">
|
|
65
|
+
<Selector
|
|
66
|
+
value="m"
|
|
67
|
+
onChange={setSize}
|
|
68
|
+
options={[
|
|
69
|
+
{ label: "XS", value: "xs" },
|
|
70
|
+
{ label: "S", value: "s" },
|
|
71
|
+
{ label: "M", value: "m" },
|
|
72
|
+
{ label: "L", value: "l" },
|
|
73
|
+
{ label: "XL", value: "xl" },
|
|
74
|
+
{ label: "XXL", value: "xxl", unavailable: true },
|
|
75
|
+
]}
|
|
76
|
+
/>
|
|
77
|
+
</SelectorGroup>
|
|
78
|
+
|
|
79
|
+
{/* Multi-size selector (e.g. waist + length) */}
|
|
80
|
+
<SelectorGroup label="Waist" selectedLabel="32">
|
|
81
|
+
<Selector value="32" onChange={setWaist} options={waistOptions} />
|
|
82
|
+
</SelectorGroup>
|
|
83
|
+
<SelectorGroup label="Length" selectedLabel="30">
|
|
84
|
+
<Selector value="30" onChange={setLength} options={lengthOptions} />
|
|
85
|
+
</SelectorGroup>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Rules
|
|
89
|
+
|
|
90
|
+
- Use `unavailable` for sizes that are out of stock but still viewable (e.g. for waitlist)
|
|
91
|
+
- Use `disabled` for sizes that cannot be interacted with at all
|
|
92
|
+
- The selected pill shows a heavier border but keeps a white background in Gap 1.0.0
|
|
93
|
+
- `SelectorGroup` uses a `<fieldset>` + `<legend>` for accessibility
|
|
94
|
+
- Each pill has `role="radio"` and `aria-checked` for screen readers
|
|
95
|
+
- Support both controlled (`value` + `onChange`) and uncontrolled (`defaultValue`) patterns
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Slider / Price Filter
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Use `RangeSlider` for any dual-handle range selection. Use `PriceFilter` as a ready-made composition that pairs the slider with Min/Max text inputs and a Reset link — ideal for filtering product listings by price.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { RangeSlider, PriceFilter } from '@stoovles/gap-kit'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Props
|
|
12
|
+
|
|
13
|
+
### RangeSlider
|
|
14
|
+
|
|
15
|
+
| Prop | Type | Default | Description |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| `min` | `number` | — | Minimum possible value |
|
|
18
|
+
| `max` | `number` | — | Maximum possible value |
|
|
19
|
+
| `low` | `number` | — | Controlled lower bound |
|
|
20
|
+
| `high` | `number` | — | Controlled upper bound |
|
|
21
|
+
| `defaultLow` | `number` | `min` | Uncontrolled initial lower bound |
|
|
22
|
+
| `defaultHigh` | `number` | `max` | Uncontrolled initial upper bound |
|
|
23
|
+
| `step` | `number` | `1` | Step increment |
|
|
24
|
+
| `onChange` | `(low, high) => void` | — | Callback when either handle moves |
|
|
25
|
+
| `aria-label` | `string` | `"Range"` | Accessible label for the slider group |
|
|
26
|
+
| `className` | `string` | — | Additional CSS class |
|
|
27
|
+
|
|
28
|
+
### PriceFilter
|
|
29
|
+
|
|
30
|
+
| Prop | Type | Default | Description |
|
|
31
|
+
|---|---|---|---|
|
|
32
|
+
| `min` | `number` | — | Minimum possible price |
|
|
33
|
+
| `max` | `number` | — | Maximum possible price |
|
|
34
|
+
| `low` | `number` | — | Controlled lower price |
|
|
35
|
+
| `high` | `number` | — | Controlled upper price |
|
|
36
|
+
| `defaultLow` | `number` | `min` | Uncontrolled initial lower price |
|
|
37
|
+
| `defaultHigh` | `number` | `max` | Uncontrolled initial upper price |
|
|
38
|
+
| `step` | `number` | `1` | Price step |
|
|
39
|
+
| `onChange` | `(low, high) => void` | — | Callback when range changes |
|
|
40
|
+
| `onReset` | `() => void` | — | If provided, shows a "Reset" link |
|
|
41
|
+
| `className` | `string` | — | Additional CSS class |
|
|
42
|
+
|
|
43
|
+
## Visual reference
|
|
44
|
+
|
|
45
|
+
### RangeSlider
|
|
46
|
+
|
|
47
|
+
| Element | Style |
|
|
48
|
+
|---|---|
|
|
49
|
+
| Track | 2px height, gray (`--color-gray-3`) |
|
|
50
|
+
| Active fill | 2px height, dark gray (`--slider-default-color`) |
|
|
51
|
+
| Handles | 24px white circles with drop shadow |
|
|
52
|
+
| Focus ring | Expanded shadow on keyboard focus |
|
|
53
|
+
|
|
54
|
+
### PriceFilter layout
|
|
55
|
+
|
|
56
|
+
| Element | Description |
|
|
57
|
+
|---|---|
|
|
58
|
+
| Slider | Full-width `RangeSlider` |
|
|
59
|
+
| Text inputs | Two 96px-wide number fields with "Min"/"Max" floating labels |
|
|
60
|
+
| Reset link | Centered below inputs, secondary color |
|
|
61
|
+
|
|
62
|
+
## Sizing
|
|
63
|
+
|
|
64
|
+
- Slider height: 48px (touch target)
|
|
65
|
+
- Handle: 24px diameter, white, drop shadow
|
|
66
|
+
- Focus ring: additional 4px shadow spread
|
|
67
|
+
- Text inputs: 44px height, 96px width, 2px focused border
|
|
68
|
+
- Floating labels: 10px, positioned above border
|
|
69
|
+
- Gap between slider and inputs: 16px
|
|
70
|
+
- Gap between inputs row and reset: 24px
|
|
71
|
+
- Max width (PriceFilter): 342px
|
|
72
|
+
|
|
73
|
+
## Examples
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
{/* Standalone range slider */}
|
|
77
|
+
<RangeSlider
|
|
78
|
+
min={0}
|
|
79
|
+
max={200}
|
|
80
|
+
defaultLow={25}
|
|
81
|
+
defaultHigh={150}
|
|
82
|
+
onChange={(low, high) => console.log(low, high)}
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
{/* Full price filter */}
|
|
86
|
+
<PriceFilter
|
|
87
|
+
min={0}
|
|
88
|
+
max={500}
|
|
89
|
+
step={5}
|
|
90
|
+
defaultLow={0}
|
|
91
|
+
defaultHigh={500}
|
|
92
|
+
onChange={(low, high) => applyPriceFilter(low, high)}
|
|
93
|
+
onReset={() => clearPriceFilter()}
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
{/* Controlled price filter */}
|
|
97
|
+
const [range, setRange] = useState({ low: 10, high: 100 });
|
|
98
|
+
<PriceFilter
|
|
99
|
+
min={0}
|
|
100
|
+
max={200}
|
|
101
|
+
low={range.low}
|
|
102
|
+
high={range.high}
|
|
103
|
+
onChange={(low, high) => setRange({ low, high })}
|
|
104
|
+
onReset={() => setRange({ low: 0, high: 200 })}
|
|
105
|
+
/>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Rules
|
|
109
|
+
|
|
110
|
+
- The low handle cannot exceed the high handle (enforced with a minimum `step` gap)
|
|
111
|
+
- The text inputs in `PriceFilter` are synced with the slider — changing one updates the other
|
|
112
|
+
- The "Reset" link only renders when `onReset` is provided
|
|
113
|
+
- Uses native `<input type="range">` elements for accessibility (keyboard arrows work)
|
|
114
|
+
- Each handle has its own `aria-label` ("Minimum value" / "Maximum value")
|
|
115
|
+
- Supports both controlled and uncontrolled patterns
|
|
116
|
+
- The track and handles use CSS-only styling — no images
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Switch
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Use the `Switch` component for binary on/off toggles that take immediate effect — no form submission needed. For boolean choices within a form, prefer `Checkbox`.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Switch } from '@stoovles/gap-kit'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Props
|
|
12
|
+
|
|
13
|
+
| Prop | Type | Default | Description |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| `checked` | `boolean` | — | Controlled on/off state |
|
|
16
|
+
| `defaultChecked` | `boolean` | `false` | Uncontrolled initial state |
|
|
17
|
+
| `onChange` | `(checked: boolean) => void` | — | Called with the new state when toggled |
|
|
18
|
+
| `showLabel` | `boolean` | `false` | Shows "ON" / "OFF" text inside the track |
|
|
19
|
+
| `disabled` | `boolean` | `false` | Prevents interaction |
|
|
20
|
+
| `aria-label` | `string` | — | Accessible label (required when no visible label is adjacent) |
|
|
21
|
+
| `className` | `string` | — | Additional CSS class |
|
|
22
|
+
|
|
23
|
+
## Visual reference
|
|
24
|
+
|
|
25
|
+
| State | Track | Handle |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| Off | Gray (#ededed) | Gray circle (#757575), left-aligned |
|
|
28
|
+
| On | Gray (#ededed) | Blue circle (#031ba1), right-aligned |
|
|
29
|
+
|
|
30
|
+
## Sizing
|
|
31
|
+
|
|
32
|
+
- Track: 54px wide, pill-shaped (999px border-radius)
|
|
33
|
+
- Handle: 24px circle with drop shadow
|
|
34
|
+
- Padding: 2px
|
|
35
|
+
- Label (optional): 10px, 0.2px letter-spacing
|
|
36
|
+
|
|
37
|
+
## Examples
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
{/* Controlled switch */}
|
|
41
|
+
<Switch
|
|
42
|
+
checked={darkMode}
|
|
43
|
+
onChange={setDarkMode}
|
|
44
|
+
aria-label="Dark mode"
|
|
45
|
+
/>
|
|
46
|
+
|
|
47
|
+
{/* With ON/OFF label */}
|
|
48
|
+
<Switch
|
|
49
|
+
checked={notifications}
|
|
50
|
+
onChange={setNotifications}
|
|
51
|
+
showLabel
|
|
52
|
+
aria-label="Notifications"
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
{/* Uncontrolled */}
|
|
56
|
+
<Switch defaultChecked aria-label="Auto-save" />
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Rules
|
|
60
|
+
|
|
61
|
+
- Always provide an `aria-label` or an adjacent visible label for accessibility
|
|
62
|
+
- Use `role="switch"` and `aria-checked` (built in) — do not override these
|
|
63
|
+
- The switch takes immediate effect — do not use inside forms that require submit
|
|
64
|
+
- Pair with a text label to the left when the context is not obvious
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
# BopisSwitch
|
|
69
|
+
|
|
70
|
+
## When to use
|
|
71
|
+
|
|
72
|
+
Use the `BopisSwitch` component for the "Buy Online, Pick Up In Store" toggle on product pages. It combines a `Switch` with a header, description, and optional store link.
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import { BopisSwitch } from '@stoovles/gap-kit'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Props
|
|
79
|
+
|
|
80
|
+
| Prop | Type | Default | Description |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| `checked` | `boolean` | `false` | Whether store pickup is enabled |
|
|
83
|
+
| `onChange` | `(checked: boolean) => void` | — | Called when toggled |
|
|
84
|
+
| `storeName` | `string` | — | Name of the selected store (shown when on) |
|
|
85
|
+
| `storeHref` | `string` | — | Link to store details page |
|
|
86
|
+
| `description` | `string` | `"Find items available for pickup"` | Description text shown when off |
|
|
87
|
+
| `className` | `string` | — | Additional CSS class |
|
|
88
|
+
|
|
89
|
+
## Examples
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
{/* Off state — shows description */}
|
|
93
|
+
<BopisSwitch onChange={handleToggle} />
|
|
94
|
+
|
|
95
|
+
{/* On state — shows store link */}
|
|
96
|
+
<BopisSwitch
|
|
97
|
+
checked
|
|
98
|
+
onChange={handleToggle}
|
|
99
|
+
storeName="Broadway Plaza, Walnut Creek"
|
|
100
|
+
storeHref="/stores/broadway-plaza"
|
|
101
|
+
/>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Rules
|
|
105
|
+
|
|
106
|
+
- Always provide `storeName` and `storeHref` when `checked` is true
|
|
107
|
+
- The component includes a subtle divider at the top — do not add an extra one above it
|
|
108
|
+
- Place below the product price and above the size selector on PDP
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Tabs
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Use the `Tabs` component to switch between related views or filter categories within the same context — for example, product fit options (Regular, Tall, Petite, Plus) or content sections.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Tabs } from '@stoovles/gap-kit'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Props
|
|
12
|
+
|
|
13
|
+
### Tabs
|
|
14
|
+
|
|
15
|
+
| Prop | Type | Default | Description |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| `title` | `string` | — | Optional header text above the tab bar |
|
|
18
|
+
| `tabs` | `Tab[]` | — | Array of tab items |
|
|
19
|
+
| `value` | `string` | — | Controlled selected tab value |
|
|
20
|
+
| `defaultValue` | `string` | First tab | Uncontrolled initial selection |
|
|
21
|
+
| `onChange` | `(value: string) => void` | — | Callback when a tab is selected |
|
|
22
|
+
| `className` | `string` | — | Additional CSS class |
|
|
23
|
+
|
|
24
|
+
### Tab
|
|
25
|
+
|
|
26
|
+
| Field | Type | Description |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `label` | `string` | Display text |
|
|
29
|
+
| `value` | `string` | Unique identifier |
|
|
30
|
+
|
|
31
|
+
## Visual reference
|
|
32
|
+
|
|
33
|
+
| State | Bottom border | Text color |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| Unselected | 1px light gray (`--tabs-border-color`) | Gray (`--tabs-font-color`) |
|
|
36
|
+
| Selected | 2px accent blue (`--color-border-accent`) | Accent/primary (`--color-type-accent`) |
|
|
37
|
+
|
|
38
|
+
## Sizing
|
|
39
|
+
|
|
40
|
+
- Tab padding: 12px (`--spacing-utk-m`)
|
|
41
|
+
- Tabs are equal-width (flex: 1)
|
|
42
|
+
- Title-to-tabs gap: 8px (`--spacing-s`)
|
|
43
|
+
- Font: Gap Sans, 16px, weight 400, 0.32px letter-spacing
|
|
44
|
+
- Title font: 16px, weight 400, primary color
|
|
45
|
+
- Background: white for all tabs
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
{/* Basic fit tabs */}
|
|
51
|
+
<Tabs
|
|
52
|
+
title="Fit"
|
|
53
|
+
tabs={[
|
|
54
|
+
{ label: "Regular", value: "regular" },
|
|
55
|
+
{ label: "Tall", value: "tall" },
|
|
56
|
+
{ label: "Petite", value: "petite" },
|
|
57
|
+
{ label: "Plus", value: "plus" },
|
|
58
|
+
]}
|
|
59
|
+
defaultValue="regular"
|
|
60
|
+
onChange={(fit) => setFit(fit)}
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
{/* Controlled tabs without title */}
|
|
64
|
+
<Tabs
|
|
65
|
+
tabs={[
|
|
66
|
+
{ label: "Description", value: "desc" },
|
|
67
|
+
{ label: "Reviews", value: "reviews" },
|
|
68
|
+
{ label: "Size & Fit", value: "size" },
|
|
69
|
+
]}
|
|
70
|
+
value={activeTab}
|
|
71
|
+
onChange={setActiveTab}
|
|
72
|
+
/>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Rules
|
|
76
|
+
|
|
77
|
+
- Always provide at least two tabs
|
|
78
|
+
- Each tab spans equal width across the container
|
|
79
|
+
- The selected tab has a prominent bottom border in the accent color; unselected tabs have a subtle gray underline
|
|
80
|
+
- Use `title` for a section label above the tabs when context is needed (e.g. "Fit")
|
|
81
|
+
- Each tab has `role="tab"` and `aria-selected` for screen readers
|
|
82
|
+
- Only the selected tab is in the tab order (`tabIndex={0}`); others are `tabIndex={-1}`
|
|
83
|
+
- Supports both controlled (`value` + `onChange`) and uncontrolled (`defaultValue`) patterns
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# TextInput
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Use the `TextInput` component for single-line text entry — names, emails, search queries, etc. It features a floating label that rises above the field when focused or filled.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { TextInput } from '@stoovles/gap-kit'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Props
|
|
12
|
+
|
|
13
|
+
| Prop | Type | Default | Description |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| `label` | `string` | — | Field label (shown as placeholder when empty, floats when focused/filled) |
|
|
16
|
+
| `error` | `boolean` | `false` | Renders the field in error state with red border and error text |
|
|
17
|
+
| `helperText` | `string` | — | Assistive text below the field (default state) |
|
|
18
|
+
| `errorText` | `string` | — | Error message shown below the field when `error` is true |
|
|
19
|
+
| `maxLength` | `number` | — | Maximum character count, displays a counter in the helper row |
|
|
20
|
+
| `disabled` | `boolean` | `false` | Grays out the field and prevents input |
|
|
21
|
+
| `value` | `string` | — | Controlled input value |
|
|
22
|
+
| `defaultValue` | `string` | — | Uncontrolled initial value |
|
|
23
|
+
| `onChange` | `(e) => void` | — | Change handler |
|
|
24
|
+
| `className` | `string` | — | Additional CSS class |
|
|
25
|
+
|
|
26
|
+
All standard `<input>` HTML attributes are also supported (e.g. `type`, `name`, `placeholder`, `autoComplete`).
|
|
27
|
+
|
|
28
|
+
## Visual reference
|
|
29
|
+
|
|
30
|
+
| State | Border | Label | Background |
|
|
31
|
+
|---|---|---|---|
|
|
32
|
+
| Default (empty) | 1px subtle gray | Inline as placeholder | Light transparent gray |
|
|
33
|
+
| Focused (empty) | 2px black | Floating above field | Light transparent gray |
|
|
34
|
+
| Filled | 1px subtle gray | Floating above field | Light transparent gray |
|
|
35
|
+
| Error | 1px red (#d00000) | Floating, red text | Light transparent gray |
|
|
36
|
+
| Error + focus | 2px red (#d00000) | Floating, red text | Light transparent gray |
|
|
37
|
+
| Disabled | 1px gray (#ededed) | Grayed out | Gray (#ededed) |
|
|
38
|
+
|
|
39
|
+
## Sizing
|
|
40
|
+
|
|
41
|
+
- Height: 44px
|
|
42
|
+
- Horizontal padding: 16px
|
|
43
|
+
- Border radius: 0px (Gap square-corner identity)
|
|
44
|
+
- Input font: Gap Sans, 16px, weight 400, 0.32px letter-spacing
|
|
45
|
+
- Floating label: 10px, positioned above the border
|
|
46
|
+
- Helper text: 10px, below the field
|
|
47
|
+
|
|
48
|
+
## Examples
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
{/* Basic text input */}
|
|
52
|
+
<TextInput label="First Name" />
|
|
53
|
+
|
|
54
|
+
{/* With helper text */}
|
|
55
|
+
<TextInput label="Email" type="email" helperText="We'll never share your email" />
|
|
56
|
+
|
|
57
|
+
{/* With character limit */}
|
|
58
|
+
<TextInput label="Bio" helperText="Brief description" maxLength={150} />
|
|
59
|
+
|
|
60
|
+
{/* Controlled with error */}
|
|
61
|
+
<TextInput
|
|
62
|
+
label="Zip Code"
|
|
63
|
+
value={zip}
|
|
64
|
+
onChange={(e) => setZip(e.target.value)}
|
|
65
|
+
error={!isValidZip}
|
|
66
|
+
errorText="Please enter a valid zip code"
|
|
67
|
+
/>
|
|
68
|
+
|
|
69
|
+
{/* Disabled */}
|
|
70
|
+
<TextInput label="Country" value="United States" disabled />
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Rules
|
|
74
|
+
|
|
75
|
+
- Always provide a `label` — it acts as both placeholder and floating label
|
|
76
|
+
- Use `helperText` for guidance and `errorText` for validation messages
|
|
77
|
+
- Set `maxLength` when a character limit exists — the counter updates automatically
|
|
78
|
+
- Never show both `helperText` and `errorText` — error takes precedence
|
|
79
|
+
- Use `type="password"` for password fields, `type="email"` for email, etc.
|
|
80
|
+
- The floating label requires a background color to cover the border — this uses `--text-input-label-fill`
|