@thorprovider/blocks 0.1.1 → 0.1.2
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/.turbo/turbo-build.log +15 -15
- package/.turbo/turbo-test.log +32 -5
- package/AGENTS.md +1 -1
- package/README.md +52 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/contracts.d.mts +1111 -90
- package/dist/contracts.d.ts +1111 -90
- package/dist/contracts.js +189 -6
- package/dist/contracts.js.map +1 -1
- package/dist/contracts.mjs +173 -7
- package/dist/contracts.mjs.map +1 -1
- package/dist/index.d.mts +467 -89
- package/dist/index.d.ts +467 -89
- package/dist/index.js +2967 -2250
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2989 -2282
- package/dist/index.mjs.map +1 -1
- package/docs/standards/theming.md +32 -5
- package/package.json +2 -2
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +136 -93
- package/src/blocks/e-commerce/category-filters/category-filter-2.test.tsx +513 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +728 -148
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +14 -8
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.test.tsx +136 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +145 -99
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +215 -102
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +6 -2
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +51 -2
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +216 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +114 -11
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +42 -2
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +10 -8
- package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +51 -0
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +56 -36
- package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +182 -0
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +298 -129
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +137 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +231 -89
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +65 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +19 -0
- package/src/blocks/marketing/footers/footer-section-1.test.tsx +117 -0
- package/src/blocks/marketing/footers/footer-section-1.tsx +46 -9
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.test.tsx +54 -0
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +43 -13
- package/src/blocks/marketing/navbars/navbar-4.test.tsx +112 -0
- package/src/blocks/marketing/navbars/navbar-4.tsx +101 -28
- package/src/index.ts +38 -0
- package/src/lib/cart.test.ts +15 -0
- package/src/lib/cart.ts +11 -0
- package/src/lib/contracts-curated.test.ts +207 -0
- package/src/lib/contracts-server-boundary.test.ts +52 -0
- package/src/lib/contracts.ts +182 -4
- package/src/lib/money.test.ts +64 -0
- package/src/lib/money.ts +65 -0
- package/tsup.config.ts +0 -1
- package/vitest.config.ts +9 -1
|
@@ -8,11 +8,26 @@ tags: [blocks, theming, tokens, contrast, a11y]
|
|
|
8
8
|
|
|
9
9
|
# Theme token contract
|
|
10
10
|
|
|
11
|
-
`@thorprovider/blocks` ships **
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
the
|
|
11
|
+
`@thorprovider/blocks` ships **no component CSS** — the package declares no
|
|
12
|
+
design and defines no `:root` tokens. Block classes are utility classes that
|
|
13
|
+
resolve against the consumer's own theme tokens at build time. What the package
|
|
14
|
+
does ship is a **one-line Tailwind source-registration entry** so the consumer's
|
|
15
|
+
build scans the library's compiled class names. Register it from the consumer
|
|
16
|
+
stylesheet:
|
|
17
|
+
|
|
18
|
+
```css
|
|
19
|
+
@import "@thorprovider/blocks/styles.css";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The entry contains only `@source "./dist";` (a path relative to the entry file,
|
|
23
|
+
so it resolves in any install layout — monorepo, pnpm, npm). This is the
|
|
24
|
+
canonical Tailwind v4 pattern for a UI library: `@source` registers the files the
|
|
25
|
+
consumer's Tailwind scans, and it survives the package manager because it never
|
|
26
|
+
assumes the consumer's filesystem layout.
|
|
27
|
+
|
|
28
|
+
This document is the contract between the library and every theme that renders
|
|
29
|
+
it: the tokens blocks may reference, the status rule, and the documented
|
|
30
|
+
exceptions.
|
|
16
31
|
|
|
17
32
|
## Contract tokens
|
|
18
33
|
|
|
@@ -72,6 +87,18 @@ Adding a color class not in this contract is a review-time violation. Theme
|
|
|
72
87
|
authors verify pairs with `scripts/contrast-check.mjs` in the Thor Commerce
|
|
73
88
|
repo, which asserts every pair in both themes meets AA.
|
|
74
89
|
|
|
90
|
+
### Rule 5: The consumer registers the library source, and declares dark mode
|
|
91
|
+
|
|
92
|
+
A conformant theme:
|
|
93
|
+
|
|
94
|
+
- imports the package registration entry (`@import "@thorprovider/blocks/styles.css";`)
|
|
95
|
+
so block classes compile — never a monorepo-relative `@source "../../../packages/blocks/src"`;
|
|
96
|
+
- defines the `dark` custom variant (`@custom-variant dark (&:is(.dark *));`) so
|
|
97
|
+
the library's `dark:` utilities activate.
|
|
98
|
+
|
|
99
|
+
The scaffold template shipped by `@thorprovider/create-storefront` already does
|
|
100
|
+
both; a hand-rolled theme MUST add them.
|
|
101
|
+
|
|
75
102
|
## Documented exceptions
|
|
76
103
|
|
|
77
104
|
Two cases are legitimate and MUST NOT be converted to theme tokens:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thorprovider/blocks",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Reusable, framework-agnostic React component blocks for Thor Commerce storefronts.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"license": "MIT",
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@base-ui/react": "^1.8.0",
|
|
43
|
-
"@thorprovider/types": "^5.7.
|
|
43
|
+
"@thorprovider/types": "^5.7.9",
|
|
44
44
|
"class-variance-authority": "^0.7.1",
|
|
45
45
|
"clsx": "^2.1.1",
|
|
46
46
|
"cn": "^0.4.0",
|
|
@@ -9,6 +9,7 @@ import { CheckboxCheck } from '../checkout-forms/checkbox-check'
|
|
|
9
9
|
|
|
10
10
|
import type {
|
|
11
11
|
CategoryFilter2Category,
|
|
12
|
+
CategoryFilter2Collection,
|
|
12
13
|
CategoryFilter2Color,
|
|
13
14
|
CategoryFilter2PriceRange,
|
|
14
15
|
} from './category-filter-2'
|
|
@@ -18,14 +19,23 @@ interface CategoryFilter2FilterPanelProps {
|
|
|
18
19
|
clearAllLabel?: string
|
|
19
20
|
priceRangeTitle?: string
|
|
20
21
|
priceRange: number[]
|
|
21
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Real backend-provided range only. When absent the price section is not
|
|
24
|
+
* rendered — the block never fabricates a `0..1000` range (E2 design
|
|
25
|
+
* Decision 8 / spec "Price controls require real price metadata").
|
|
26
|
+
*/
|
|
27
|
+
priceRangeConfig?: CategoryFilter2PriceRange
|
|
22
28
|
minLabel?: string
|
|
23
29
|
maxLabel?: string
|
|
30
|
+
onPriceChange: (range: number[]) => void
|
|
24
31
|
categoriesTitle?: string
|
|
25
32
|
categories: CategoryFilter2Category[]
|
|
26
33
|
selectedCategories: string[]
|
|
27
34
|
onCategoryChange: (category: string, checked: boolean) => void
|
|
28
|
-
|
|
35
|
+
collectionsTitle?: string
|
|
36
|
+
collections: CategoryFilter2Collection[]
|
|
37
|
+
selectedCollections: string[]
|
|
38
|
+
onCollectionChange: (collection: string, checked: boolean) => void
|
|
29
39
|
sizesTitle?: string
|
|
30
40
|
sizes: string[]
|
|
31
41
|
selectedSize: string | null
|
|
@@ -45,11 +55,15 @@ export function CategoryFilter2FilterPanel({
|
|
|
45
55
|
priceRangeConfig,
|
|
46
56
|
minLabel = 'Mínimo',
|
|
47
57
|
maxLabel = 'Máximo',
|
|
58
|
+
onPriceChange,
|
|
48
59
|
categoriesTitle = 'Categoría',
|
|
49
60
|
categories,
|
|
50
61
|
selectedCategories,
|
|
51
62
|
onCategoryChange,
|
|
52
|
-
|
|
63
|
+
collectionsTitle = 'Colección',
|
|
64
|
+
collections,
|
|
65
|
+
selectedCollections,
|
|
66
|
+
onCollectionChange,
|
|
53
67
|
sizesTitle = 'Tamaño',
|
|
54
68
|
sizes,
|
|
55
69
|
selectedSize,
|
|
@@ -62,6 +76,7 @@ export function CategoryFilter2FilterPanel({
|
|
|
62
76
|
}: CategoryFilter2FilterPanelProps) {
|
|
63
77
|
const priceInputId = useId()
|
|
64
78
|
const categoryInputId = useId()
|
|
79
|
+
const collectionInputId = useId()
|
|
65
80
|
|
|
66
81
|
return (
|
|
67
82
|
<>
|
|
@@ -74,106 +89,134 @@ export function CategoryFilter2FilterPanel({
|
|
|
74
89
|
</div>
|
|
75
90
|
</div>
|
|
76
91
|
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
<div className="flex
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
{priceRangeConfig ? (
|
|
93
|
+
<div className="mt-4 flex flex-col gap-4">
|
|
94
|
+
<CardTitle className="font-semibold">{priceRangeTitle}</CardTitle>
|
|
95
|
+
<div className="flex items-center gap-3">
|
|
96
|
+
<div className="flex flex-1 flex-col gap-1">
|
|
97
|
+
<label htmlFor={`${priceInputId}-min`}>{minLabel}</label>
|
|
98
|
+
<Input
|
|
99
|
+
id={`${priceInputId}-min`}
|
|
100
|
+
type="number"
|
|
101
|
+
value={priceRange[0] ?? priceRangeConfig.min}
|
|
102
|
+
onChange={e => {
|
|
103
|
+
const value = parseInt(e.target.value)
|
|
104
|
+
if (!isNaN(value) && value >= priceRangeConfig.min) {
|
|
105
|
+
onPriceChange([value, priceRange[1] ?? priceRangeConfig.max])
|
|
106
|
+
}
|
|
107
|
+
}}
|
|
108
|
+
min={priceRangeConfig.min}
|
|
109
|
+
max={priceRange[1] ?? priceRangeConfig.max}
|
|
110
|
+
step={priceRangeConfig.step}
|
|
111
|
+
className="h-9"
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="mt-5 flex-none">-</div>
|
|
115
|
+
<div className="flex flex-1 flex-col gap-1">
|
|
116
|
+
<label htmlFor={`${priceInputId}-max`}>{maxLabel}</label>
|
|
117
|
+
<Input
|
|
118
|
+
id={`${priceInputId}-max`}
|
|
119
|
+
type="number"
|
|
120
|
+
value={priceRange[1] ?? priceRangeConfig.max}
|
|
121
|
+
onChange={e => {
|
|
122
|
+
const value = parseInt(e.target.value)
|
|
123
|
+
if (!isNaN(value) && value <= priceRangeConfig.max) {
|
|
124
|
+
onPriceChange([priceRange[0] ?? priceRangeConfig.min, value])
|
|
125
|
+
}
|
|
126
|
+
}}
|
|
127
|
+
min={priceRange[0] ?? priceRangeConfig.min}
|
|
128
|
+
max={priceRangeConfig.max}
|
|
129
|
+
step={priceRangeConfig.step}
|
|
130
|
+
className="h-9"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
97
133
|
</div>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
134
|
+
</div>
|
|
135
|
+
) : null}
|
|
136
|
+
|
|
137
|
+
{categories.length > 0 ? (
|
|
138
|
+
<div className="mt-4 flex flex-col gap-4">
|
|
139
|
+
<CardTitle className="font-semibold">{categoriesTitle}</CardTitle>
|
|
140
|
+
<div className="flex flex-col gap-2">
|
|
141
|
+
{categories.map(category => (
|
|
142
|
+
<div key={category.id} className="flex items-center gap-2">
|
|
143
|
+
<CheckboxCheck
|
|
144
|
+
id={`${categoryInputId}-${category.id}`}
|
|
145
|
+
checked={selectedCategories.includes(category.id)}
|
|
146
|
+
onCheckedChange={checked => onCategoryChange(category.id, checked as boolean)}
|
|
147
|
+
/>
|
|
148
|
+
<label htmlFor={`${categoryInputId}-${category.id}`} className="text-sm">
|
|
149
|
+
{category.label}
|
|
150
|
+
</label>
|
|
151
|
+
</div>
|
|
152
|
+
))}
|
|
116
153
|
</div>
|
|
117
154
|
</div>
|
|
118
|
-
|
|
155
|
+
) : null}
|
|
119
156
|
|
|
120
|
-
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
{
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
157
|
+
{collections.length > 0 ? (
|
|
158
|
+
<div className="mt-4 flex flex-col gap-4">
|
|
159
|
+
<CardTitle className="font-semibold">{collectionsTitle}</CardTitle>
|
|
160
|
+
<div className="flex flex-col gap-2">
|
|
161
|
+
{collections.map(collection => (
|
|
162
|
+
<div key={collection.id} className="flex items-center gap-2">
|
|
163
|
+
<CheckboxCheck
|
|
164
|
+
id={`${collectionInputId}-${collection.id}`}
|
|
165
|
+
checked={selectedCollections.includes(collection.id)}
|
|
166
|
+
onCheckedChange={checked => onCollectionChange(collection.id, checked as boolean)}
|
|
167
|
+
/>
|
|
168
|
+
<label htmlFor={`${collectionInputId}-${collection.id}`} className="text-sm">
|
|
169
|
+
{collection.label}
|
|
170
|
+
</label>
|
|
171
|
+
</div>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
135
174
|
</div>
|
|
136
|
-
|
|
175
|
+
) : null}
|
|
137
176
|
|
|
138
|
-
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
177
|
+
{sizes.length > 0 ? (
|
|
178
|
+
<div className="mt-4 flex flex-col gap-4">
|
|
179
|
+
<CardTitle className="font-semibold">{sizesTitle}</CardTitle>
|
|
180
|
+
<div className="flex flex-wrap gap-2">
|
|
181
|
+
{sizes.map(size => (
|
|
182
|
+
<Button
|
|
183
|
+
key={size}
|
|
184
|
+
variant={selectedSize === size ? 'default' : 'outline'}
|
|
185
|
+
size="sm"
|
|
186
|
+
className="h-8 cursor-pointer px-3 text-xs"
|
|
187
|
+
onClick={() => onSizeChange(size === selectedSize ? null : size)}
|
|
188
|
+
aria-pressed={selectedSize === size}
|
|
189
|
+
>
|
|
190
|
+
{size}
|
|
191
|
+
</Button>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
153
194
|
</div>
|
|
154
|
-
|
|
195
|
+
) : null}
|
|
155
196
|
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
197
|
+
{colors.length > 0 ? (
|
|
198
|
+
<div className="mt-4 flex flex-col gap-4">
|
|
199
|
+
<CardTitle className="font-semibold">{colorsTitle}</CardTitle>
|
|
200
|
+
<div className="flex flex-wrap gap-2">
|
|
201
|
+
{colors.map(color => (
|
|
202
|
+
<Button
|
|
203
|
+
key={color.id}
|
|
204
|
+
onClick={() => onColorChange(color.id === selectedColor ? null : color.id)}
|
|
205
|
+
className={`relative size-8 cursor-pointer rounded-sm transition-transform duration-200 ${
|
|
206
|
+
selectedColor === color.id ? 'ring-primary scale-105 ring-2' : 'hover:ring-foreground/20 hover:ring-1'
|
|
207
|
+
} ${color.id === 'white' ? 'border-border border' : ''}`}
|
|
208
|
+
style={{ backgroundColor: color.color }}
|
|
209
|
+
aria-pressed={selectedColor === color.id}
|
|
210
|
+
aria-label={color.id}
|
|
211
|
+
>
|
|
212
|
+
{selectedColor === color.id && <div className="border-background absolute inset-0 rounded-sm border-2" />}
|
|
213
|
+
</Button>
|
|
214
|
+
))}
|
|
215
|
+
</div>
|
|
173
216
|
</div>
|
|
174
|
-
|
|
217
|
+
) : null}
|
|
175
218
|
</>
|
|
176
219
|
)
|
|
177
220
|
}
|
|
178
221
|
|
|
179
|
-
export default CategoryFilter2FilterPanel
|
|
222
|
+
export default CategoryFilter2FilterPanel
|