@thorprovider/blocks 0.1.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 (184) hide show
  1. package/.turbo/turbo-build.log +24 -0
  2. package/.turbo/turbo-test.log +10 -0
  3. package/.turbo/turbo-type-check.log +0 -0
  4. package/AGENTS.md +1 -0
  5. package/README.md +161 -0
  6. package/components.json +33 -0
  7. package/dist/.tsbuildinfo +1 -0
  8. package/dist/contracts.d.mts +563 -0
  9. package/dist/contracts.d.ts +563 -0
  10. package/dist/contracts.js +207 -0
  11. package/dist/contracts.js.map +1 -0
  12. package/dist/contracts.mjs +168 -0
  13. package/dist/contracts.mjs.map +1 -0
  14. package/dist/index.d.mts +2408 -0
  15. package/dist/index.d.ts +2408 -0
  16. package/dist/index.js +16422 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/index.mjs +16352 -0
  19. package/dist/index.mjs.map +1 -0
  20. package/docs/standards/data-contracts.md +70 -0
  21. package/docs/standards/theming.md +85 -0
  22. package/package.json +62 -0
  23. package/src/blocks/application/app-shells/app-shell-1-sidebar.tsx +198 -0
  24. package/src/blocks/application/app-shells/app-shell-1.tsx +196 -0
  25. package/src/blocks/application/login-forms/login-page-1.tsx +246 -0
  26. package/src/blocks/application/login-forms/login-page-2.tsx +207 -0
  27. package/src/blocks/application/password-recovery/forgot-password-page-1-brand-mark.tsx +38 -0
  28. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +320 -0
  29. package/src/blocks/application/signup-forms/signup-page-1.tsx +231 -0
  30. package/src/blocks/application/verification/verify-1.tsx +307 -0
  31. package/src/blocks/application/verification/verify-confetti.tsx +192 -0
  32. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +260 -0
  33. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +179 -0
  34. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +341 -0
  35. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +57 -0
  36. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +124 -0
  37. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +170 -0
  38. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +52 -0
  39. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +57 -0
  40. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +68 -0
  41. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +298 -0
  42. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +47 -0
  43. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +9 -0
  44. package/src/blocks/e-commerce/checkout-forms/checkbox-check.tsx +74 -0
  45. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +189 -0
  46. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +283 -0
  47. package/src/blocks/e-commerce/checkout-forms/checkout-field-feedback.tsx +29 -0
  48. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +96 -0
  49. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-country-field.tsx +51 -0
  50. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +139 -0
  51. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +184 -0
  52. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +36 -0
  53. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +107 -0
  54. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +235 -0
  55. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +130 -0
  56. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +175 -0
  57. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +190 -0
  58. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +312 -0
  59. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +91 -0
  60. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +110 -0
  61. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +137 -0
  62. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +68 -0
  63. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +79 -0
  64. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +172 -0
  65. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +392 -0
  66. package/src/blocks/e-commerce/checkout-forms/checkout-form-types.ts +7 -0
  67. package/src/blocks/e-commerce/navigation/category-icon.test.tsx +54 -0
  68. package/src/blocks/e-commerce/navigation/category-icon.tsx +94 -0
  69. package/src/blocks/e-commerce/navigation/mega-menu.test.tsx +139 -0
  70. package/src/blocks/e-commerce/navigation/mega-menu.tsx +271 -0
  71. package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +152 -0
  72. package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +276 -0
  73. package/src/blocks/e-commerce/order-history/order-history-1.tsx +159 -0
  74. package/src/blocks/e-commerce/order-history/order-history-2.tsx +222 -0
  75. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +86 -0
  76. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +126 -0
  77. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +100 -0
  78. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +129 -0
  79. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +218 -0
  80. package/src/blocks/e-commerce/product-list/product-card-1.tsx +131 -0
  81. package/src/blocks/e-commerce/product-list/product-card-2.tsx +71 -0
  82. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +166 -0
  83. package/src/blocks/e-commerce/product-list/product-card-3.tsx +215 -0
  84. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +98 -0
  85. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +171 -0
  86. package/src/blocks/e-commerce/product-list/product-card-5.tsx +367 -0
  87. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +201 -0
  88. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +272 -0
  89. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +59 -0
  90. package/src/blocks/e-commerce/product-overview/product-overview-5-lightbox-tab.tsx +104 -0
  91. package/src/blocks/e-commerce/product-overview/product-overview-5-reviews-tab.tsx +82 -0
  92. package/src/blocks/e-commerce/product-overview/product-overview-5-tabs.tsx +69 -0
  93. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +274 -0
  94. package/src/blocks/e-commerce/product-overview/product-quick-view-card.tsx +135 -0
  95. package/src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx +393 -0
  96. package/src/blocks/e-commerce/product-overview/product-quick-view.tsx +136 -0
  97. package/src/blocks/e-commerce/reviews/review-rating-1-review-form.tsx +133 -0
  98. package/src/blocks/e-commerce/reviews/review-rating-1.tsx +178 -0
  99. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +246 -0
  100. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +142 -0
  101. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +291 -0
  102. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +110 -0
  103. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +51 -0
  104. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +146 -0
  105. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +295 -0
  106. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +142 -0
  107. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +342 -0
  108. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +146 -0
  109. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +391 -0
  110. package/src/blocks/e-commerce/store-containers/store-container-1.tsx +29 -0
  111. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +244 -0
  112. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +283 -0
  113. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +318 -0
  114. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +205 -0
  115. package/src/blocks/marketing/banners/avatar-group.tsx +69 -0
  116. package/src/blocks/marketing/banners/banner-1.tsx +188 -0
  117. package/src/blocks/marketing/cta/cta-section-1.tsx +112 -0
  118. package/src/blocks/marketing/faq/faq-block-1.tsx +45 -0
  119. package/src/blocks/marketing/features/feature-1.tsx +97 -0
  120. package/src/blocks/marketing/features/feature-2.tsx +82 -0
  121. package/src/blocks/marketing/footers/footer-section-1.tsx +141 -0
  122. package/src/blocks/marketing/footers/footer-section-3.tsx +192 -0
  123. package/src/blocks/marketing/footers/footer-section-4.tsx +355 -0
  124. package/src/blocks/marketing/hero-sections/avatar-group.tsx +86 -0
  125. package/src/blocks/marketing/hero-sections/hero-section-1.tsx +135 -0
  126. package/src/blocks/marketing/logo-clouds/logo-cloud-1-logo-icon.tsx +52 -0
  127. package/src/blocks/marketing/logo-clouds/logo-cloud-1.tsx +56 -0
  128. package/src/blocks/marketing/navbars/navbar-1.tsx +267 -0
  129. package/src/blocks/marketing/navbars/navbar-2.tsx +351 -0
  130. package/src/blocks/marketing/navbars/navbar-3.tsx +303 -0
  131. package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +118 -0
  132. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +248 -0
  133. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +177 -0
  134. package/src/blocks/marketing/navbars/navbar-4-mobile-nav.tsx +104 -0
  135. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +219 -0
  136. package/src/blocks/marketing/navbars/navbar-4-search-bar.tsx +52 -0
  137. package/src/blocks/marketing/navbars/navbar-4.tsx +411 -0
  138. package/src/blocks/marketing/pricing/pricing-section-1.tsx +94 -0
  139. package/src/blocks/marketing/statistics/statistic-1.tsx +93 -0
  140. package/src/blocks/marketing/testimonials/testimonial-1.tsx +141 -0
  141. package/src/blocks/marketing/testimonials/testimonial-2.tsx +108 -0
  142. package/src/blocks/shared/reaction-toggle.tsx +145 -0
  143. package/src/hooks/use-mobile.ts +19 -0
  144. package/src/index.ts +200 -0
  145. package/src/lib/contracts.test.ts +32 -0
  146. package/src/lib/contracts.ts +235 -0
  147. package/src/lib/types/menu.ts +43 -0
  148. package/src/lib/types.ts +4 -0
  149. package/src/lib/utils.ts +10 -0
  150. package/src/ui/accordion.tsx +65 -0
  151. package/src/ui/avatar.tsx +108 -0
  152. package/src/ui/badge.tsx +47 -0
  153. package/src/ui/breadcrumb.tsx +108 -0
  154. package/src/ui/button-group.tsx +83 -0
  155. package/src/ui/button.tsx +63 -0
  156. package/src/ui/card.tsx +91 -0
  157. package/src/ui/carousel.tsx +241 -0
  158. package/src/ui/dialog.tsx +158 -0
  159. package/src/ui/drawer.tsx +134 -0
  160. package/src/ui/dropdown-menu.tsx +256 -0
  161. package/src/ui/empty.tsx +103 -0
  162. package/src/ui/field.tsx +248 -0
  163. package/src/ui/input-group.tsx +170 -0
  164. package/src/ui/input-otp.tsx +76 -0
  165. package/src/ui/input.tsx +20 -0
  166. package/src/ui/label.tsx +23 -0
  167. package/src/ui/navigation-menu.tsx +167 -0
  168. package/src/ui/pagination.tsx +127 -0
  169. package/src/ui/progress.tsx +30 -0
  170. package/src/ui/radio-group.tsx +44 -0
  171. package/src/ui/select.tsx +189 -0
  172. package/src/ui/separator.tsx +27 -0
  173. package/src/ui/sheet.tsx +142 -0
  174. package/src/ui/sidebar.tsx +726 -0
  175. package/src/ui/skeleton.tsx +13 -0
  176. package/src/ui/table.tsx +115 -0
  177. package/src/ui/tabs.tsx +90 -0
  178. package/src/ui/textarea.tsx +17 -0
  179. package/src/ui/toggle-group.tsx +83 -0
  180. package/src/ui/toggle.tsx +46 -0
  181. package/src/ui/tooltip.tsx +56 -0
  182. package/tsconfig.json +20 -0
  183. package/tsup.config.ts +16 -0
  184. package/vitest.config.ts +9 -0
@@ -0,0 +1,70 @@
1
+ ---
2
+ title: "Block data contracts"
3
+ purpose: "Warn block authors and hosts about the two boundary data-hygiene rules that prevent empty-src and duplicate-key console errors"
4
+ lastReviewed: "2026-09-27"
5
+ type: standards
6
+ tags: [blocks, contracts, data-boundary, images, react-keys]
7
+ ---
8
+
9
+ # Block data contracts
10
+
11
+ Blocks receive data via props and render it blindly. Two recurring defects at
12
+ the data boundary — `src=""` and duplicate React keys — are both caused by the
13
+ **host/boundary** emitting invalid values, not by the block. This document
14
+ states the two hygiene rules every boundary (mapper/transform/store spec) must
15
+ follow, and the defensive pattern every block should apply as a second layer.
16
+
17
+ ## Rule 1: Optional fields are never `""` — use `undefined` or a fallback
18
+
19
+ A field that "may be absent" is a field that is **optional**. Serializing it as
20
+ an empty string is a lie: it looks like data but renders as a broken element
21
+ (`<img src="">` → browser warning, broken layout).
22
+
23
+ - **Genuinely optional fields** → emit `undefined`. React omits the attribute
24
+ entirely, so `<img src={undefined}>` never warns.
25
+ - **Fields whose contract requires a string** (e.g. `image: z.string()`) →
26
+ emit a non-empty **fallback**, never `""`. An `undefined` here would fail Zod
27
+ validation at the renderer boundary.
28
+ - **Never** reach for `?? ''` in a mapper, transform, or store spec.
29
+
30
+ Reference pattern — category images in electrostore
31
+ (`projects/electrostore/lib/electrostore/category-mappers.ts`):
32
+
33
+ ```ts
34
+ export const CATEGORY_IMAGE_FALLBACK = 'data:image/svg+xml;utf8,...'
35
+ // image: category.image ?? category.metadata?.image ?? CATEGORY_IMAGE_FALLBACK
36
+ ```
37
+
38
+ The same pattern ships in the blocks library as
39
+ `PRODUCT_IMAGE_FALLBACK` (SVG data-URI) exported from
40
+ `@thorprovider/blocks/contracts`. Hosts may override with a store-specific
41
+ fallback; blocks render it as the last-resort value.
42
+
43
+ ## Rule 2: React keys are unique by contract, not by convention
44
+
45
+ Never use a data field (href, id, image URL) as a React key unless uniqueness
46
+ is **guaranteed by contract**. Placeholder or repeated hrefs (e.g. two utility
47
+ links pointing at the same route) silently produce duplicate keys.
48
+
49
+ - **Dedupe at the boundary** when concatenating data from multiple sources
50
+ (categories + utility links + account link).
51
+ - **Blocks** that receive arrays use composite stable keys
52
+ (`key={`${item.id}:${item.label}`}`) so a duplicate id from any host cannot
53
+ collide.
54
+ - Duplicate `href` values in navigation are often a sign of placeholder data —
55
+ fix the data, don't just silence the warning.
56
+
57
+ ## Second layer: blocks render defensively
58
+
59
+ Even with correct boundaries, blocks should render
60
+ `src={value || undefined}` for every image prop. This is a cheap invariant
61
+ that makes the whole catalog immune to one misbehaving host.
62
+
63
+ ## Review checklist (boundary code)
64
+
65
+ - [ ] No `?? ''` or `|| ''` on visual fields (image, avatar, icon) in mappers/transforms.
66
+ - [ ] Required `z.string()` fields always receive a non-empty fallback.
67
+ - [ ] Optional fields emit `undefined`, never `""`.
68
+ - [ ] Arrays passed as React children have unique keys guaranteed by contract or deduped at the boundary.
69
+ - [ ] Concatenated arrays (categories + links) are deduped before render.
70
+ - [ ] Every `<img>`/`<AvatarImage>` in the block uses `src={expr || undefined}`.
@@ -0,0 +1,85 @@
1
+ ---
2
+ title: "Theme token contract"
3
+ purpose: "Define the theme tokens library blocks may reference and the rules that keep block styling theme-agnostic and AA-conformant"
4
+ lastReviewed: "2026-09-28"
5
+ type: standards
6
+ tags: [blocks, theming, tokens, contrast, a11y]
7
+ ---
8
+
9
+ # Theme token contract
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.
16
+
17
+ ## Contract tokens
18
+
19
+ A conformant consumer theme defines **each** token below with its foreground
20
+ pair, in every mode the theme declares (light and dark where dark mode
21
+ exists). Foreground pairs SHALL meet WCAG AA contrast (≥ 4.5:1 normal text,
22
+ ≥ 3:1 UI components / large text).
23
+
24
+ | Token | Foreground | Role |
25
+ | --- | --- | --- |
26
+ | `--background` | `--foreground` | Page surface / body text |
27
+ | `--card` | `--card-foreground` | Card surface |
28
+ | `--popover` | `--popover-foreground` | Popover / dialog surface |
29
+ | `--primary` | `--primary-foreground` | Brand / main CTAs |
30
+ | `--secondary` | `--secondary-foreground` | Secondary surfaces |
31
+ | `--muted` | `--muted-foreground` | De-emphasized surfaces / hints |
32
+ | `--accent` | `--accent-foreground` | Hover / selected surface (neutral) |
33
+ | `--destructive` | `--destructive-foreground` | Errors, removals, danger |
34
+ | `--success` | `--success-foreground` | Positive status (savings, in stock) |
35
+ | `--warning` | `--warning-foreground` | Caution status (low stock, notices) |
36
+ | `--border` | — | Hairlines, dividers |
37
+ | `--input` | — | Input borders / fills |
38
+ | `--ring` | — | Focus rings |
39
+
40
+ `accent` is a **surface** token, not a brand token. Brand color belongs in
41
+ `--primary`. `accent` SHALL be a neutral, AA-safe surface so hover/selected
42
+ states across nav, links, and menus stay readable.
43
+
44
+ ## Rules
45
+
46
+ ### Rule 1: Blocks reference only contract tokens
47
+
48
+ A block SHALL NOT hardcode raw Tailwind palette utilities (`bg-orange-500`,
49
+ `text-green-600`, `fill-amber-500`, `bg-green-50`), arbitrary CSS values
50
+ (`bg-[#0070ba]`), or app-only tokens (`bg-promo`). Every color class SHALL
51
+ resolve against a contract token.
52
+
53
+ ### Rule 2: Status has one of two shapes
54
+
55
+ - **Status chip / badge** → use a `Badge` variant (`secondary`, `outline`,
56
+ `destructive`) and let the label carry the meaning.
57
+ - **Inline status text, icon, or dot** → use the semantic tokens
58
+ (`text-success`, `bg-success`, `text-warning`, `bg-warning`).
59
+
60
+ Rating stars use `fill-foreground` / `fill-current` — rating color carries no
61
+ status semantics.
62
+
63
+ ### Rule 3: Primitives use contract foreground pairs
64
+
65
+ UI primitives (button, badge, ...) SHALL use `text-destructive-foreground`
66
+ (never `text-white`) on destructive surfaces so the pair resolves against the
67
+ consumer's tokens in light and dark mode.
68
+
69
+ ### Rule 4: Theming.md is the review-time gate
70
+
71
+ Adding a color class not in this contract is a review-time violation. Theme
72
+ authors verify pairs with `scripts/contrast-check.mjs` in the Thor Commerce
73
+ repo, which asserts every pair in both themes meets AA.
74
+
75
+ ## Documented exceptions
76
+
77
+ Two cases are legitimate and MUST NOT be converted to theme tokens:
78
+
79
+ - **Brand colors supplied as data.** Payment-brand chips (e.g. PayPal blue),
80
+ product swatches, and category icon colors arrive as data props or app-local
81
+ tokens (`--nav`, `--stock`, `--urgent`, `--promo` in electrostore) and are
82
+ styled by the app, never referenced by library blocks.
83
+ - **Overlays over imagery.** `bg-black/50 text-white`, `bg-white/90`,
84
+ photo-count pills, lightbox controls. Contrast is image-dependent and cannot
85
+ be guaranteed by theme tokens; these stay as raw overlay utilities.
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@thorprovider/blocks",
3
+ "version": "0.1.0",
4
+ "description": "Reusable, framework-agnostic React component blocks for Thor Commerce storefronts.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "development": "./dist/index.mjs",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js",
13
+ "default": "./dist/index.mjs"
14
+ },
15
+ "./contracts": {
16
+ "types": "./dist/contracts.d.ts",
17
+ "development": "./dist/contracts.mjs",
18
+ "import": "./dist/contracts.mjs",
19
+ "require": "./dist/contracts.js",
20
+ "default": "./dist/contracts.mjs"
21
+ }
22
+ },
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "dev": "tsup --watch",
26
+ "type-check": "tsc --noEmit",
27
+ "test": "vitest run --config vitest.config.ts",
28
+ "generate:agent-index": "node ../../scripts/generate-package-agents-index.cjs packages/blocks @thorprovider/blocks"
29
+ },
30
+ "peerDependencies": {
31
+ "react": "^19.0.0",
32
+ "react-dom": "^19.0.0"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/thorprovider/thor-commerce",
37
+ "directory": "packages/blocks"
38
+ },
39
+ "license": "MIT",
40
+ "dependencies": {
41
+ "@base-ui/react": "^1.8.0",
42
+ "@thorprovider/types": "^5.7.7",
43
+ "class-variance-authority": "^0.7.1",
44
+ "clsx": "^2.1.1",
45
+ "cn": "^0.4.0",
46
+ "embla-carousel-react": "^8.6.0",
47
+ "input-otp": "^1.5.0",
48
+ "lucide-react": "^1.47.0",
49
+ "motion": "^13.4.3",
50
+ "radix-ui": "^1.6.7",
51
+ "tailwind-merge": "^3.3.1",
52
+ "vaul": "^1.1.2",
53
+ "zod": "^3.25.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/react": "^19",
57
+ "@types/react-dom": "^19",
58
+ "tsup": "^8.0.0",
59
+ "typescript": "^5.0.0",
60
+ "vitest": "^3.2.4"
61
+ }
62
+ }
@@ -0,0 +1,198 @@
1
+ 'use client'
2
+
3
+ import {
4
+ BarChart3,
5
+ ChevronsUpDown,
6
+ FolderKanban,
7
+ Home,
8
+ LayoutDashboard,
9
+ Package,
10
+ Settings,
11
+ ShoppingCart,
12
+ User,
13
+ Users,
14
+ } from 'lucide-react'
15
+ import type { ComponentType } from 'react'
16
+
17
+ import { Avatar, AvatarFallback } from '../../../ui/avatar'
18
+ import {
19
+ DropdownMenu,
20
+ DropdownMenuContent,
21
+ DropdownMenuGroup,
22
+ DropdownMenuItem,
23
+ DropdownMenuLabel,
24
+ DropdownMenuTrigger,
25
+ } from '../../../ui/dropdown-menu'
26
+ import { Input } from '../../../ui/input'
27
+ import {
28
+ Sidebar,
29
+ SidebarContent,
30
+ SidebarFooter,
31
+ SidebarGroup,
32
+ SidebarGroupContent,
33
+ SidebarGroupLabel,
34
+ SidebarHeader,
35
+ SidebarMenu,
36
+ SidebarMenuButton,
37
+ SidebarMenuItem,
38
+ useSidebar,
39
+ } from '../../../ui/sidebar'
40
+
41
+ export type AppShell1NavIcon =
42
+ | 'layout-dashboard'
43
+ | 'package'
44
+ | 'user'
45
+ | 'settings'
46
+ | 'home'
47
+ | 'shopping-cart'
48
+ | 'bar-chart-3'
49
+ | 'users'
50
+ | 'folder-kanban'
51
+
52
+ export interface AppShell1NavItem {
53
+ label: string
54
+ href: string
55
+ icon: AppShell1NavIcon
56
+ active?: boolean
57
+ }
58
+
59
+ export interface AppShell1User {
60
+ name: string
61
+ email: string
62
+ initials: string
63
+ }
64
+
65
+ const appShell1NavIcons: Record<AppShell1NavIcon, ComponentType> = {
66
+ 'layout-dashboard': LayoutDashboard,
67
+ package: Package,
68
+ user: User,
69
+ settings: Settings,
70
+ home: Home,
71
+ 'shopping-cart': ShoppingCart,
72
+ 'bar-chart-3': BarChart3,
73
+ users: Users,
74
+ 'folder-kanban': FolderKanban,
75
+ }
76
+
77
+ // SidebarInset must remain a direct sibling of Sidebar. The sidebar primitive persists open state in the `sidebar_state` cookie.
78
+ function BrandMark() {
79
+ return (
80
+ <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
81
+ <path
82
+ d="M26 24.75C26.4142 24.75 26.75 24.4142 26.75 24C26.75 23.5858 26.4142 23.25 26 23.25V24.75ZM26 23.25H11V24.75H26V23.25ZM8.75 21V15H7.25V21H8.75ZM11 23.25C9.75736 23.25 8.75 22.2426 8.75 21H7.25C7.25 23.0711 8.92893 24.75 11 24.75V23.25Z"
83
+ fill="currentColor"
84
+ />
85
+ <path
86
+ d="M1.5 3.25C1.08579 3.25 0.75 3.58579 0.75 4C0.75 4.41421 1.08579 4.75 1.5 4.75V3.25ZM1.5 4.75H6V3.25H1.5V4.75ZM7.25 6V21H8.75V6H7.25ZM6 4.75C6.69036 4.75 7.25 5.30964 7.25 6H8.75C8.75 4.48122 7.51878 3.25 6 3.25V4.75Z"
87
+ fill="currentColor"
88
+ />
89
+ <path
90
+ d="M22 21.75C22.4142 21.75 22.75 21.4142 22.75 21C22.75 20.5858 22.4142 20.25 22 20.25V21.75ZM22 20.25H11V21.75H22V20.25ZM8.75 18V12H7.25V18H8.75ZM11 20.25C9.75736 20.25 8.75 19.2426 8.75 18H7.25C7.25 20.0711 8.92893 21.75 11 21.75V20.25Z"
91
+ fill="currentColor"
92
+ />
93
+ <path
94
+ d="M27.2057 19.754C27.0654 20.1438 26.6357 20.346 26.246 20.2057C25.8562 20.0654 25.654 19.6357 25.7943 19.246L27.2057 19.754ZM30.0361 9.67744L29.3305 9.4234L29.3305 9.4234L30.0361 9.67744ZM25.7943 19.246L29.3305 9.4234L30.7418 9.93148L27.2057 19.754L25.7943 19.246ZM28.1543 7.75L8 7.75V6.25L28.1543 6.25V7.75ZM29.3305 9.4234C29.6237 8.60882 29.0201 7.75 28.1543 7.75V6.25C30.059 6.25 31.3869 8.13941 30.7418 9.93148L29.3305 9.4234Z"
95
+ fill="currentColor"
96
+ />
97
+ <path
98
+ d="M13.5 21.75C13.0858 21.75 12.75 21.4142 12.75 21C12.75 20.5858 13.0858 20.25 13.5 20.25V21.75ZM26.7111 19.009L27.4174 19.2613L27.4174 19.2613L26.7111 19.009ZM13.5 20.25H23.8858V21.75H13.5V20.25ZM26.0048 18.7568L27.7937 13.7477L29.2063 14.2523L27.4174 19.2613L26.0048 18.7568ZM23.8858 20.25C24.8367 20.25 25.6849 19.6522 26.0048 18.7568L27.4174 19.2613C26.8843 20.7537 25.4706 21.75 23.8858 21.75V20.25Z"
99
+ fill="currentColor"
100
+ />
101
+ <path d="M21.1694 10.5806L14.5651 17.1849" stroke="currentColor" />
102
+ <path d="M22.1694 14.5806L18.5632 18.1868" stroke="currentColor" />
103
+ <circle cx="13.1" cy="26.1" r="1.7" stroke="currentColor" />
104
+ <circle cx="22.1" cy="26.1" r="1.7" stroke="currentColor" />
105
+ </svg>
106
+ )
107
+ }
108
+
109
+ export interface AppShell1SidebarProps {
110
+ brandName?: string
111
+ navGroupLabel?: string
112
+ navItems?: AppShell1NavItem[]
113
+ user?: AppShell1User
114
+ }
115
+
116
+ export function AppShell1Sidebar({
117
+ brandName,
118
+ navGroupLabel = 'Workspace',
119
+ navItems = [],
120
+ user,
121
+ }: AppShell1SidebarProps) {
122
+ const { setOpenMobile } = useSidebar()
123
+
124
+ return (
125
+ <Sidebar>
126
+ <SidebarHeader>
127
+ <a
128
+ href="#resumen"
129
+ className="flex min-h-11 items-center gap-2 rounded-lg px-2 font-semibold"
130
+ onClick={() => setOpenMobile(false)}
131
+ >
132
+ <BrandMark />
133
+ {brandName}
134
+ </a>
135
+ </SidebarHeader>
136
+ <SidebarContent>
137
+ <SidebarGroup>
138
+ <SidebarGroupLabel>{navGroupLabel}</SidebarGroupLabel>
139
+ <SidebarGroupContent>
140
+ <nav aria-label={navGroupLabel}>
141
+ <SidebarMenu>
142
+ {navItems.map(item => {
143
+ const Icon = appShell1NavIcons[item.icon]
144
+ return (
145
+ <SidebarMenuItem key={item.label}>
146
+ <SidebarMenuButton isActive={item.active} asChild>
147
+ <a
148
+ href={item.href}
149
+ aria-current={item.active ? 'page' : undefined}
150
+ aria-label={item.label}
151
+ onClick={() => setOpenMobile(false)}
152
+ >
153
+ <Icon />
154
+ <span>{item.label}</span>
155
+ </a>
156
+ </SidebarMenuButton>
157
+ </SidebarMenuItem>
158
+ )
159
+ })}
160
+ </SidebarMenu>
161
+ </nav>
162
+ </SidebarGroupContent>
163
+ </SidebarGroup>
164
+ <div className="px-2 md:hidden">
165
+ <Input aria-label="Buscar en el espacio de trabajo" placeholder="Buscar…" />
166
+ </div>
167
+ </SidebarContent>
168
+ {user ? (
169
+ <SidebarFooter>
170
+ <SidebarMenu>
171
+ <SidebarMenuItem>
172
+ <DropdownMenu>
173
+ <DropdownMenuTrigger asChild>
174
+ <SidebarMenuButton className="min-h-11">
175
+ <Avatar className="size-7">
176
+ <AvatarFallback>{user.initials}</AvatarFallback>
177
+ </Avatar>
178
+ <span className="truncate">{user.name}</span>
179
+ <ChevronsUpDown className="ml-auto" />
180
+ </SidebarMenuButton>
181
+ </DropdownMenuTrigger>
182
+ <DropdownMenuContent side="top">
183
+ <DropdownMenuGroup>
184
+ <DropdownMenuLabel>{user.email}</DropdownMenuLabel>
185
+ <DropdownMenuItem>Configuración de la cuenta</DropdownMenuItem>
186
+ <DropdownMenuItem>Cerrar sesión</DropdownMenuItem>
187
+ </DropdownMenuGroup>
188
+ </DropdownMenuContent>
189
+ </DropdownMenu>
190
+ </SidebarMenuItem>
191
+ </SidebarMenu>
192
+ </SidebarFooter>
193
+ ) : null}
194
+ </Sidebar>
195
+ )
196
+ }
197
+
198
+ export default AppShell1Sidebar
@@ -0,0 +1,196 @@
1
+ 'use client'
2
+
3
+ import { Bell, Plus, Search } from 'lucide-react'
4
+ import type { ReactNode } from 'react'
5
+
6
+ import type { CTA } from '../../../lib/types'
7
+ import {
8
+ Breadcrumb,
9
+ BreadcrumbItem,
10
+ BreadcrumbLink,
11
+ BreadcrumbList,
12
+ BreadcrumbPage,
13
+ BreadcrumbSeparator,
14
+ } from '../../../ui/breadcrumb'
15
+ import { Button } from '../../../ui/button'
16
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../../ui/card'
17
+ import { Input } from '../../../ui/input'
18
+ import { Separator } from '../../../ui/separator'
19
+ import { SidebarInset, SidebarProvider, SidebarTrigger } from '../../../ui/sidebar'
20
+ import AppShell1Sidebar from './app-shell-1-sidebar'
21
+ import type { AppShell1NavItem, AppShell1User } from './app-shell-1-sidebar'
22
+
23
+ export interface AppShell1BreadcrumbItem {
24
+ label: string
25
+ href?: string
26
+ }
27
+
28
+ export interface AppShell1Stat {
29
+ label: string
30
+ value: string
31
+ }
32
+
33
+ export interface AppShell1Props {
34
+ brandName?: string
35
+ navGroupLabel?: string
36
+ navItems?: AppShell1NavItem[]
37
+ user?: AppShell1User
38
+ breadcrumb?: AppShell1BreadcrumbItem[]
39
+ pageTitle?: string
40
+ pageDescription?: string
41
+ action?: CTA
42
+ stats?: AppShell1Stat[]
43
+ children?: ReactNode
44
+ className?: string
45
+ }
46
+
47
+ function AppHeader({ breadcrumb }: { breadcrumb?: AppShell1BreadcrumbItem[] }) {
48
+ return (
49
+ <header className="flex h-14 shrink-0 items-center gap-3 border-b px-4 sm:px-6">
50
+ <SidebarTrigger aria-label="Toggle navigation" className="min-h-11 min-w-11" />
51
+ <Separator orientation="vertical" className="data-vertical:h-5 data-vertical:self-center" />
52
+ {breadcrumb && breadcrumb.length > 0 ? (
53
+ <Breadcrumb>
54
+ <BreadcrumbList>
55
+ {breadcrumb.map((item, index) => {
56
+ const isLast = index === breadcrumb.length - 1
57
+ return (
58
+ <div key={item.label} className="contents">
59
+ <BreadcrumbItem className={isLast ? undefined : 'hidden sm:flex'}>
60
+ {isLast ? (
61
+ <BreadcrumbPage>{item.label}</BreadcrumbPage>
62
+ ) : item.href ? (
63
+ <BreadcrumbLink href={item.href}>{item.label}</BreadcrumbLink>
64
+ ) : (
65
+ <span>{item.label}</span>
66
+ )}
67
+ </BreadcrumbItem>
68
+ {!isLast ? (
69
+ <BreadcrumbSeparator className="hidden sm:flex" />
70
+ ) : null}
71
+ </div>
72
+ )
73
+ })}
74
+ </BreadcrumbList>
75
+ </Breadcrumb>
76
+ ) : null}
77
+ <div className="ml-auto hidden w-56 md:block">
78
+ <div className="relative">
79
+ <Search className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
80
+ <Input
81
+ aria-label="Buscar en el espacio de trabajo"
82
+ placeholder="Buscar…"
83
+ className="h-9 pl-9"
84
+ />
85
+ </div>
86
+ </div>
87
+ <Button variant="ghost" size="icon" aria-label="Notifications, 3 unread">
88
+ <Bell />
89
+ <span className="sr-only">3 unread</span>
90
+ </Button>
91
+ </header>
92
+ )
93
+ }
94
+
95
+ function AppContent({
96
+ pageTitle,
97
+ pageDescription,
98
+ action,
99
+ stats,
100
+ children,
101
+ }: {
102
+ pageTitle?: string
103
+ pageDescription?: string
104
+ action?: CTA
105
+ stats?: AppShell1Stat[]
106
+ children?: ReactNode
107
+ }) {
108
+ return (
109
+ <main className="flex flex-1 flex-col gap-6 p-4 sm:p-6">
110
+ {pageTitle || pageDescription || action ? (
111
+ <div className="flex items-center justify-between gap-4">
112
+ <div>
113
+ {pageTitle ? (
114
+ <h1 className="text-2xl font-semibold tracking-tight">{pageTitle}</h1>
115
+ ) : null}
116
+ {pageDescription ? (
117
+ <p className="text-sm text-muted-foreground">{pageDescription}</p>
118
+ ) : null}
119
+ </div>
120
+ {action ? (
121
+ <Button asChild>
122
+ <a href={action.href}>
123
+ <Plus data-icon="inline-start" />
124
+ {action.label}
125
+ </a>
126
+ </Button>
127
+ ) : null}
128
+ </div>
129
+ ) : null}
130
+ {stats && stats.length > 0 ? (
131
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
132
+ {stats.map(stat => (
133
+ <Card key={stat.label}>
134
+ <CardHeader>
135
+ <CardDescription>{stat.label}</CardDescription>
136
+ <CardTitle className="text-3xl">{stat.value}</CardTitle>
137
+ </CardHeader>
138
+ </Card>
139
+ ))}
140
+ </div>
141
+ ) : null}
142
+ <Card className="min-h-72">
143
+ <CardHeader>
144
+ <CardTitle>Project activity</CardTitle>
145
+ <CardDescription>Your product content goes inside this shell.</CardDescription>
146
+ </CardHeader>
147
+ <CardContent className="flex flex-1">
148
+ {children ?? (
149
+ <div className="flex flex-1 items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground">
150
+ <Search className="mr-2 size-4" />
151
+ Replace with your screen
152
+ </div>
153
+ )}
154
+ </CardContent>
155
+ </Card>
156
+ </main>
157
+ )
158
+ }
159
+
160
+ export function AppShell1({
161
+ brandName,
162
+ navGroupLabel,
163
+ navItems,
164
+ user,
165
+ breadcrumb,
166
+ pageTitle,
167
+ pageDescription,
168
+ action,
169
+ stats,
170
+ children,
171
+ className,
172
+ }: AppShell1Props) {
173
+ return (
174
+ <SidebarProvider className={className}>
175
+ <AppShell1Sidebar
176
+ brandName={brandName}
177
+ navGroupLabel={navGroupLabel}
178
+ navItems={navItems}
179
+ user={user}
180
+ />
181
+ <SidebarInset>
182
+ <AppHeader breadcrumb={breadcrumb} />
183
+ <AppContent
184
+ pageTitle={pageTitle}
185
+ pageDescription={pageDescription}
186
+ action={action}
187
+ stats={stats}
188
+ >
189
+ {children}
190
+ </AppContent>
191
+ </SidebarInset>
192
+ </SidebarProvider>
193
+ )
194
+ }
195
+
196
+ export default AppShell1