@thorprovider/blocks 0.1.0 → 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.
Files changed (57) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/.turbo/turbo-test.log +32 -5
  3. package/AGENTS.md +1 -1
  4. package/README.md +52 -0
  5. package/dist/.tsbuildinfo +1 -1
  6. package/dist/contracts.d.mts +1111 -90
  7. package/dist/contracts.d.ts +1111 -90
  8. package/dist/contracts.js +189 -6
  9. package/dist/contracts.js.map +1 -1
  10. package/dist/contracts.mjs +173 -7
  11. package/dist/contracts.mjs.map +1 -1
  12. package/dist/index.d.mts +467 -89
  13. package/dist/index.d.ts +467 -89
  14. package/dist/index.js +2967 -2250
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +2989 -2282
  17. package/dist/index.mjs.map +1 -1
  18. package/docs/standards/theming.md +32 -5
  19. package/package.json +4 -3
  20. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +136 -93
  21. package/src/blocks/e-commerce/category-filters/category-filter-2.test.tsx +513 -0
  22. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +728 -148
  23. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +14 -8
  24. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.test.tsx +136 -0
  25. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +145 -99
  26. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +215 -102
  27. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +6 -2
  28. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +51 -2
  29. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +216 -0
  30. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +114 -11
  31. package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +42 -2
  32. package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +10 -8
  33. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +51 -0
  34. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +56 -36
  35. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +182 -0
  36. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +298 -129
  37. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +137 -0
  38. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +231 -89
  39. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +65 -0
  40. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +19 -0
  41. package/src/blocks/marketing/footers/footer-section-1.test.tsx +117 -0
  42. package/src/blocks/marketing/footers/footer-section-1.tsx +46 -9
  43. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.test.tsx +54 -0
  44. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +43 -13
  45. package/src/blocks/marketing/navbars/navbar-4.test.tsx +112 -0
  46. package/src/blocks/marketing/navbars/navbar-4.tsx +101 -28
  47. package/src/index.ts +38 -0
  48. package/src/lib/cart.test.ts +15 -0
  49. package/src/lib/cart.ts +11 -0
  50. package/src/lib/contracts-curated.test.ts +207 -0
  51. package/src/lib/contracts-server-boundary.test.ts +52 -0
  52. package/src/lib/contracts.ts +182 -4
  53. package/src/lib/money.test.ts +64 -0
  54. package/src/lib/money.ts +65 -0
  55. package/styles.css +1 -0
  56. package/tsup.config.ts +0 -1
  57. 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 **zero CSS**. Block styling resolves against the
12
- consumer's own theme tokens at build time (Tailwind `@source` points at the
13
- library source). This document is the contract between the library and every
14
- theme that renders it: the tokens blocks may reference, the status rule, and
15
- the documented exceptions.
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.0",
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",
@@ -18,7 +18,8 @@
18
18
  "import": "./dist/contracts.mjs",
19
19
  "require": "./dist/contracts.js",
20
20
  "default": "./dist/contracts.mjs"
21
- }
21
+ },
22
+ "./styles.css": "./styles.css"
22
23
  },
23
24
  "scripts": {
24
25
  "build": "tsup",
@@ -39,7 +40,7 @@
39
40
  "license": "MIT",
40
41
  "dependencies": {
41
42
  "@base-ui/react": "^1.8.0",
42
- "@thorprovider/types": "^5.7.7",
43
+ "@thorprovider/types": "^5.7.9",
43
44
  "class-variance-authority": "^0.7.1",
44
45
  "clsx": "^2.1.1",
45
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
- priceRangeConfig: CategoryFilter2PriceRange
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
- onPriceChange: (range: number[]) => void
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
- onPriceChange,
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
- <div className="mt-4 flex flex-col gap-4">
78
- <CardTitle className="font-semibold">{priceRangeTitle}</CardTitle>
79
- <div className="flex items-center gap-3">
80
- <div className="flex flex-1 flex-col gap-1">
81
- <label htmlFor={`${priceInputId}-min`}>{minLabel}</label>
82
- <Input
83
- id={`${priceInputId}-min`}
84
- type="number"
85
- value={priceRange[0]}
86
- onChange={e => {
87
- const value = parseInt(e.target.value)
88
- if (!isNaN(value) && value >= priceRangeConfig.min) {
89
- onPriceChange([value, priceRange[1]])
90
- }
91
- }}
92
- min={priceRangeConfig.min}
93
- max={priceRange[1]}
94
- step={priceRangeConfig.step}
95
- className="h-9"
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
- <div className="mt-5 flex-none">-</div>
99
- <div className="flex flex-1 flex-col gap-1">
100
- <label htmlFor={`${priceInputId}-max`}>{maxLabel}</label>
101
- <Input
102
- id={`${priceInputId}-max`}
103
- type="number"
104
- value={priceRange[1]}
105
- onChange={e => {
106
- const value = parseInt(e.target.value)
107
- if (!isNaN(value) && value <= priceRangeConfig.max) {
108
- onPriceChange([priceRange[0], value])
109
- }
110
- }}
111
- min={priceRange[0]}
112
- max={priceRangeConfig.max}
113
- step={priceRangeConfig.step}
114
- className="h-9"
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
- </div>
155
+ ) : null}
119
156
 
120
- <div className="mt-4 flex flex-col gap-4">
121
- <CardTitle className="font-semibold">{categoriesTitle}</CardTitle>
122
- <div className="flex flex-col gap-2">
123
- {categories.map(category => (
124
- <div key={category.id} className="flex items-center gap-2">
125
- <CheckboxCheck
126
- id={`${categoryInputId}-${category.id}`}
127
- checked={selectedCategories.includes(category.id)}
128
- onCheckedChange={checked => onCategoryChange(category.id, checked as boolean)}
129
- />
130
- <label htmlFor={`${categoryInputId}-${category.id}`} className="text-sm">
131
- {category.label}
132
- </label>
133
- </div>
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
- </div>
175
+ ) : null}
137
176
 
138
- <div className="mt-4 flex flex-col gap-4">
139
- <CardTitle className="font-semibold">{sizesTitle}</CardTitle>
140
- <div className="flex flex-wrap gap-2">
141
- {sizes.map(size => (
142
- <Button
143
- key={size}
144
- variant={selectedSize === size ? 'default' : 'outline'}
145
- size="sm"
146
- className="h-8 cursor-pointer px-3 text-xs"
147
- onClick={() => onSizeChange(size === selectedSize ? null : size)}
148
- aria-pressed={selectedSize === size}
149
- >
150
- {size}
151
- </Button>
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
- </div>
195
+ ) : null}
155
196
 
156
- <div className="mt-4 flex flex-col gap-4">
157
- <CardTitle className="font-semibold">{colorsTitle}</CardTitle>
158
- <div className="flex flex-wrap gap-2">
159
- {colors.map(color => (
160
- <Button
161
- key={color.id}
162
- onClick={() => onColorChange(color.id === selectedColor ? null : color.id)}
163
- className={`relative size-8 cursor-pointer rounded-sm transition-transform duration-200 ${
164
- selectedColor === color.id ? 'ring-primary scale-105 ring-2' : 'hover:ring-foreground/20 hover:ring-1'
165
- } ${color.id === 'white' ? 'border-border border' : ''}`}
166
- style={{ backgroundColor: color.color }}
167
- aria-pressed={selectedColor === color.id}
168
- aria-label={color.id}
169
- >
170
- {selectedColor === color.id && <div className="border-background absolute inset-0 rounded-sm border-2" />}
171
- </Button>
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
- </div>
217
+ ) : null}
175
218
  </>
176
219
  )
177
220
  }
178
221
 
179
- export default CategoryFilter2FilterPanel
222
+ export default CategoryFilter2FilterPanel