@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.
Files changed (35) hide show
  1. package/dist/index.js +2417 -0
  2. package/dist/styles.css +4309 -0
  3. package/guidelines/components/accordion.md +99 -0
  4. package/guidelines/components/breadcrumb.md +74 -0
  5. package/guidelines/components/buttons.md +90 -0
  6. package/guidelines/components/checkbox.md +86 -0
  7. package/guidelines/components/chip.md +77 -0
  8. package/guidelines/components/divider.md +52 -0
  9. package/guidelines/components/dropdown.md +125 -0
  10. package/guidelines/components/filter-sort.md +108 -0
  11. package/guidelines/components/fulfillment.md +100 -0
  12. package/guidelines/components/global-footer.md +71 -0
  13. package/guidelines/components/global-header.md +67 -0
  14. package/guidelines/components/hamburger-nav.md +79 -0
  15. package/guidelines/components/handle.md +49 -0
  16. package/guidelines/components/icons.md +147 -0
  17. package/guidelines/components/link.md +70 -0
  18. package/guidelines/components/logo.md +63 -0
  19. package/guidelines/components/mega-nav.md +103 -0
  20. package/guidelines/components/notification.md +97 -0
  21. package/guidelines/components/pagination.md +88 -0
  22. package/guidelines/components/popover.md +77 -0
  23. package/guidelines/components/price.md +86 -0
  24. package/guidelines/components/product-card.md +82 -0
  25. package/guidelines/components/radio.md +92 -0
  26. package/guidelines/components/search-input.md +63 -0
  27. package/guidelines/components/selector-swatch.md +78 -0
  28. package/guidelines/components/selector.md +95 -0
  29. package/guidelines/components/slider.md +116 -0
  30. package/guidelines/components/switch.md +108 -0
  31. package/guidelines/components/tabs.md +83 -0
  32. package/guidelines/components/text-input.md +80 -0
  33. package/guidelines/composition/buy-box.md +132 -0
  34. package/guidelines/composition/recommendations.md +100 -0
  35. 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`