create-nymbal-app 0.3.0 → 0.5.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 (167) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.js +14 -12
  3. package/dist/index.js.map +1 -1
  4. package/package.json +5 -4
  5. package/scaffold/project/astro/src/pages/index.astro +13 -15
  6. package/scaffold/templates/astro/_dot.node-version +1 -1
  7. package/scaffold/templates/astro/astro.config.mjs +23 -0
  8. package/scaffold/templates/astro/package.json +26 -27
  9. package/scaffold/templates/astro/src/components/SkipNav.astro +3 -1
  10. package/scaffold/templates/astro/src/elements/index.ts +11 -0
  11. package/scaffold/templates/astro/src/layouts/BaseLayout.astro +15 -4
  12. package/scaffold/templates/astro/src/lib/api.ts +126 -20
  13. package/scaffold/templates/astro/src/lib/chrome.ts +59 -0
  14. package/scaffold/templates/astro/src/lib/client.ts +4 -2
  15. package/scaffold/templates/astro/src/lib/fragments.ts +48 -0
  16. package/scaffold/templates/astro/src/lib/server-elements.ts +5 -0
  17. package/scaffold/templates/astro/src/middleware.ts +37 -0
  18. package/scaffold/templates/astro/src/pages/account/addresses.astro +5 -97
  19. package/scaffold/templates/astro/src/pages/account/login.astro +7 -34
  20. package/scaffold/templates/astro/src/pages/account/profile.astro +6 -56
  21. package/scaffold/templates/astro/src/pages/account/register.astro +7 -73
  22. package/scaffold/templates/astro/src/pages/cart.astro +14 -7
  23. package/scaffold/templates/astro/src/pages/category/[slug].astro +41 -26
  24. package/scaffold/templates/astro/src/pages/checkout.astro +34 -90
  25. package/scaffold/templates/astro/src/pages/fragments/footer.astro +5 -2
  26. package/scaffold/templates/astro/src/pages/fragments/header.astro +12 -2
  27. package/scaffold/templates/astro/src/pages/fragments/nav.astro +8 -5
  28. package/scaffold/templates/astro/src/pages/fragments/product/[slug].astro +9 -10
  29. package/scaffold/templates/astro/src/pages/index.astro +18 -15
  30. package/scaffold/templates/astro/src/pages/order/[orderNumber].astro +22 -44
  31. package/scaffold/templates/astro/src/pages/product/[slug].astro +46 -29
  32. package/scaffold/templates/astro/src/pages/products/index.astro +40 -26
  33. package/scaffold/templates/astro/src/pages/search.astro +13 -8
  34. package/scaffold/templates/astro/src/scripts/init-client.ts +14 -0
  35. package/scaffold/templates/astro/src/scripts/pdp.ts +47 -53
  36. package/scaffold/templates/astro/src/scripts/plp.ts +21 -98
  37. package/scaffold/templates/astro/src/scripts/search.ts +19 -120
  38. package/scaffold/templates/astro/src/scripts/signed-in-redirect.ts +4 -0
  39. package/scaffold/templates/astro/src/styles/global.css +49 -45
  40. package/scaffold/templates/astro/src/styles/pages/account.css +0 -90
  41. package/scaffold/templates/astro/src/styles/pages/confirmation.css +6 -0
  42. package/scaffold/templates/astro/src/styles/pages/home.css +9 -100
  43. package/scaffold/templates/astro/src/styles/pages/pdp.css +5 -167
  44. package/scaffold/templates/astro/src/styles/pages/plp.css +3 -42
  45. package/scaffold/templates/astro/src/styles/pages/search.css +13 -100
  46. package/scaffold/templates/astro/src/styles/tokens.css +56 -0
  47. package/scaffold/templates/nextjs/_dot.node-version +1 -1
  48. package/scaffold/templates/nextjs/app/(catalog)/category/[slug]/page.tsx +74 -0
  49. package/scaffold/templates/nextjs/app/(catalog)/home/[key]/page.tsx +80 -0
  50. package/scaffold/templates/nextjs/app/(catalog)/layout.tsx +13 -0
  51. package/scaffold/templates/nextjs/app/(catalog)/not-found.tsx +7 -0
  52. package/scaffold/templates/nextjs/app/(catalog)/product/[slug]/page.tsx +182 -0
  53. package/scaffold/templates/nextjs/app/(catalog)/products/page.tsx +62 -0
  54. package/scaffold/templates/nextjs/app/(catalog)/search/SearchView.tsx +90 -0
  55. package/scaffold/templates/nextjs/app/(catalog)/search/page.tsx +46 -0
  56. package/scaffold/templates/nextjs/app/(shopper)/account/addresses/AddressesView.tsx +5 -0
  57. package/scaffold/templates/nextjs/app/(shopper)/account/addresses/page.tsx +17 -0
  58. package/scaffold/templates/nextjs/app/(shopper)/account/login/LoginView.tsx +28 -0
  59. package/scaffold/templates/nextjs/app/(shopper)/account/login/page.tsx +23 -0
  60. package/scaffold/templates/nextjs/app/(shopper)/account/profile/ProfileView.tsx +5 -0
  61. package/scaffold/templates/nextjs/app/(shopper)/account/profile/page.tsx +20 -0
  62. package/scaffold/templates/nextjs/app/(shopper)/account/register/RegisterView.tsx +26 -0
  63. package/scaffold/templates/nextjs/app/(shopper)/account/register/page.tsx +11 -0
  64. package/scaffold/templates/nextjs/app/(shopper)/cart/CartView.tsx +5 -0
  65. package/scaffold/templates/nextjs/app/(shopper)/cart/page.tsx +22 -0
  66. package/scaffold/templates/nextjs/app/(shopper)/checkout/CheckoutView.tsx +27 -0
  67. package/scaffold/templates/nextjs/app/(shopper)/checkout/page.tsx +39 -0
  68. package/scaffold/templates/nextjs/app/(shopper)/layout.tsx +12 -0
  69. package/scaffold/templates/nextjs/app/{order → (shopper)/order}/[orderNumber]/page.tsx +24 -5
  70. package/scaffold/templates/nextjs/app/error.tsx +3 -7
  71. package/scaffold/templates/nextjs/app/fragments/footer/route.ts +13 -0
  72. package/scaffold/templates/nextjs/app/fragments/header/[key]/route.ts +18 -0
  73. package/scaffold/templates/nextjs/app/fragments/nav/[key]/route.ts +23 -0
  74. package/scaffold/templates/nextjs/app/fragments/product/[slug]/route.ts +22 -0
  75. package/scaffold/templates/nextjs/app/globals.css +5 -23
  76. package/scaffold/templates/nextjs/app/layout.tsx +8 -5
  77. package/scaffold/templates/nextjs/app/not-found.tsx +9 -33
  78. package/scaffold/templates/nextjs/app/tokens.css +56 -0
  79. package/scaffold/templates/nextjs/components/client/ListingControls.tsx +87 -0
  80. package/scaffold/templates/nextjs/components/client/NymbalSetup.tsx +48 -6
  81. package/scaffold/templates/nextjs/components/client/ProductInteractive.tsx +64 -28
  82. package/scaffold/templates/nextjs/components/client/elements.tsx +15 -0
  83. package/scaffold/templates/nextjs/components/server/Header.tsx +33 -97
  84. package/scaffold/templates/nextjs/components/server/NotFoundView.tsx +39 -0
  85. package/scaffold/templates/nextjs/components/server/SkipNav.tsx +7 -3
  86. package/scaffold/templates/nextjs/elements/index.ts +12 -0
  87. package/scaffold/templates/nextjs/lib/api.ts +162 -46
  88. package/scaffold/templates/nextjs/lib/chrome.ts +40 -0
  89. package/scaffold/templates/nextjs/lib/client.ts +3 -0
  90. package/scaffold/templates/nextjs/lib/fragments.ts +16 -0
  91. package/scaffold/templates/nextjs/lib/header.ts +39 -0
  92. package/scaffold/templates/nextjs/lib/render.ts +14 -0
  93. package/scaffold/templates/nextjs/next.config.mjs +32 -0
  94. package/scaffold/templates/nextjs/package.json +28 -29
  95. package/scaffold/templates/nextjs/styles/pages/account.module.css +0 -70
  96. package/scaffold/templates/nextjs/styles/pages/confirmation.module.css +6 -85
  97. package/scaffold/templates/nextjs/styles/pages/home.module.css +2 -103
  98. package/scaffold/templates/nextjs/styles/pages/pdp.module.css +3 -179
  99. package/scaffold/templates/nextjs/styles/pages/plp.module.css +8 -141
  100. package/scaffold/templates/nextjs/styles/pages/search.module.css +8 -128
  101. package/scaffold/templates/nextjs/tsconfig.dist.json +5 -0
  102. package/scaffold/templates/astro/src/components/AddressForm.astro +0 -107
  103. package/scaffold/templates/astro/src/components/Breadcrumb.astro +0 -20
  104. package/scaffold/templates/astro/src/components/CategoryNav.astro +0 -27
  105. package/scaffold/templates/astro/src/components/CheckoutSteps.astro +0 -29
  106. package/scaffold/templates/astro/src/components/Footer.astro +0 -108
  107. package/scaffold/templates/astro/src/components/Header.astro +0 -192
  108. package/scaffold/templates/astro/src/components/ImageGallery.astro +0 -42
  109. package/scaffold/templates/astro/src/components/OrderSummary.astro +0 -53
  110. package/scaffold/templates/astro/src/components/Pagination.astro +0 -13
  111. package/scaffold/templates/astro/src/components/PriceDisplay.astro +0 -25
  112. package/scaffold/templates/astro/src/components/ProductCard.astro +0 -44
  113. package/scaffold/templates/astro/src/components/RelatedProducts.astro +0 -28
  114. package/scaffold/templates/astro/src/components/ReviewForm.astro +0 -74
  115. package/scaffold/templates/astro/src/components/ReviewList.astro +0 -41
  116. package/scaffold/templates/astro/src/components/SortDropdown.astro +0 -26
  117. package/scaffold/templates/astro/src/components/StockBadge.astro +0 -32
  118. package/scaffold/templates/astro/src/scripts/auth-forms.ts +0 -82
  119. package/scaffold/templates/astro/src/scripts/cart-page.ts +0 -143
  120. package/scaffold/templates/astro/src/scripts/checkout.ts +0 -272
  121. package/scaffold/templates/astro/src/scripts/image-gallery.ts +0 -22
  122. package/scaffold/templates/astro/src/scripts/profile-form.ts +0 -57
  123. package/scaffold/templates/astro/src/scripts/review-form.ts +0 -27
  124. package/scaffold/templates/astro/src/styles/pages/cart.css +0 -243
  125. package/scaffold/templates/astro/src/styles/pages/checkout.css +0 -270
  126. package/scaffold/templates/nextjs/app/account/addresses/page.tsx +0 -10
  127. package/scaffold/templates/nextjs/app/account/login/page.tsx +0 -23
  128. package/scaffold/templates/nextjs/app/account/profile/page.tsx +0 -10
  129. package/scaffold/templates/nextjs/app/account/register/page.tsx +0 -10
  130. package/scaffold/templates/nextjs/app/cart/page.tsx +0 -10
  131. package/scaffold/templates/nextjs/app/category/[slug]/page.tsx +0 -58
  132. package/scaffold/templates/nextjs/app/checkout/page.tsx +0 -10
  133. package/scaffold/templates/nextjs/app/page.tsx +0 -63
  134. package/scaffold/templates/nextjs/app/product/[slug]/page.tsx +0 -111
  135. package/scaffold/templates/nextjs/app/products/page.tsx +0 -40
  136. package/scaffold/templates/nextjs/app/search/page.tsx +0 -24
  137. package/scaffold/templates/nextjs/components/client/AddressManager.tsx +0 -181
  138. package/scaffold/templates/nextjs/components/client/CartPage.tsx +0 -120
  139. package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +0 -523
  140. package/scaffold/templates/nextjs/components/client/ImageGallery.tsx +0 -68
  141. package/scaffold/templates/nextjs/components/client/LoginForm.tsx +0 -89
  142. package/scaffold/templates/nextjs/components/client/MiniCartWrapper.tsx +0 -12
  143. package/scaffold/templates/nextjs/components/client/ProductGrid.tsx +0 -148
  144. package/scaffold/templates/nextjs/components/client/ProfileForm.tsx +0 -112
  145. package/scaffold/templates/nextjs/components/client/RegisterForm.tsx +0 -131
  146. package/scaffold/templates/nextjs/components/client/ReviewForm.tsx +0 -116
  147. package/scaffold/templates/nextjs/components/client/SearchBarWrapper.tsx +0 -27
  148. package/scaffold/templates/nextjs/components/client/SearchResults.tsx +0 -141
  149. package/scaffold/templates/nextjs/components/client/SortDropdown.tsx +0 -35
  150. package/scaffold/templates/nextjs/components/server/Breadcrumb.tsx +0 -36
  151. package/scaffold/templates/nextjs/components/server/CategoryNav.tsx +0 -29
  152. package/scaffold/templates/nextjs/components/server/CheckoutSteps.tsx +0 -60
  153. package/scaffold/templates/nextjs/components/server/Footer.module.css +0 -64
  154. package/scaffold/templates/nextjs/components/server/Footer.tsx +0 -79
  155. package/scaffold/templates/nextjs/components/server/Header.module.css +0 -147
  156. package/scaffold/templates/nextjs/components/server/OrderSummary.tsx +0 -79
  157. package/scaffold/templates/nextjs/components/server/PriceDisplay.tsx +0 -31
  158. package/scaffold/templates/nextjs/components/server/ProductCard.tsx +0 -59
  159. package/scaffold/templates/nextjs/components/server/RelatedProducts.tsx +0 -31
  160. package/scaffold/templates/nextjs/components/server/ReviewList.tsx +0 -62
  161. package/scaffold/templates/nextjs/components/server/StockBadge.tsx +0 -19
  162. package/scaffold/templates/nextjs/styles/pages/cart.module.css +0 -240
  163. package/scaffold/templates/nextjs/styles/pages/checkout.module.css +0 -404
  164. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/layout.tsx +0 -0
  165. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/orders/[orderNumber]/page.tsx +0 -0
  166. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/orders/page.tsx +0 -0
  167. /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/page.tsx +0 -0
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import '../../styles/pages/account.css'
3
+ import NymbalElement from '@nymbalhq/web-components/astro'
3
4
  import BaseLayout from '../../layouts/BaseLayout.astro'
4
5
  ---
5
6
 
@@ -9,78 +10,7 @@ import BaseLayout from '../../layouts/BaseLayout.astro'
9
10
  <h1 class="auth-card-title">Create Account</h1>
10
11
  <p class="auth-card-subtitle">Join us and start shopping.</p>
11
12
 
12
- <div class="checkout-error" id="register-error" style="display:none; margin-bottom: var(--nymbal-spacing-md, 1rem);" role="alert"></div>
13
-
14
- <form id="register-form" data-testid="register-form">
15
- <div class="account-form-row">
16
- <div class="form-group">
17
- <label for="register-firstName">First Name</label>
18
- <input
19
- type="text"
20
- id="register-firstName"
21
- name="firstName"
22
- autocomplete="given-name"
23
- placeholder="First name"
24
- data-testid="register-firstName"
25
- />
26
- </div>
27
-
28
- <div class="form-group">
29
- <label for="register-lastName">Last Name</label>
30
- <input
31
- type="text"
32
- id="register-lastName"
33
- name="lastName"
34
- autocomplete="family-name"
35
- placeholder="Last name"
36
- data-testid="register-lastName"
37
- />
38
- </div>
39
- </div>
40
-
41
- <div class="form-group">
42
- <label for="register-email">Email</label>
43
- <input
44
- type="email"
45
- id="register-email"
46
- name="email"
47
- required
48
- autocomplete="email"
49
- placeholder="you@example.com"
50
- data-testid="register-email"
51
- />
52
- </div>
53
-
54
- <div class="form-group">
55
- <label for="register-password">Password</label>
56
- <input
57
- type="password"
58
- id="register-password"
59
- name="password"
60
- required
61
- autocomplete="new-password"
62
- placeholder="Choose a password"
63
- minlength="8"
64
- data-testid="register-password"
65
- />
66
- </div>
67
-
68
- <div class="form-group">
69
- <label for="register-phone">Phone (optional)</label>
70
- <input
71
- type="tel"
72
- id="register-phone"
73
- name="phone"
74
- autocomplete="tel"
75
- placeholder="+44 7700 900000"
76
- data-testid="register-phone"
77
- />
78
- </div>
79
-
80
- <button type="submit" class="btn btn-primary" data-testid="register-submit">
81
- Create Account
82
- </button>
83
- </form>
13
+ <NymbalElement tag="nymbal-register-form" />
84
14
 
85
15
  <p class="auth-footer">
86
16
  Already have an account? <a href="/account/login">Sign in</a>
@@ -90,5 +20,9 @@ import BaseLayout from '../../layouts/BaseLayout.astro'
90
20
  </BaseLayout>
91
21
 
92
22
  <script>
93
- import '../../scripts/auth-forms.js'
23
+ import '../../lib/client.js'
24
+ import { NymbalRegisterForm } from '@nymbalhq/web-components/elements/nymbal-register-form'
25
+ import '../../scripts/signed-in-redirect.js'
26
+
27
+ NymbalRegisterForm.define()
94
28
  </script>
@@ -1,16 +1,23 @@
1
1
  ---
2
- import '../styles/pages/cart.css'
2
+ import NymbalElement from '@nymbalhq/web-components/astro'
3
3
  import BaseLayout from '../layouts/BaseLayout.astro'
4
+ import { fetchCart } from '../lib/api'
5
+
6
+ const cart = await fetchCart(Astro.request.headers.get('cookie'))
4
7
  ---
5
8
 
6
- <BaseLayout title="Your Cart | Nymbal" description="Review the items in your cart.">
7
- <div class="container section" id="cart-content" data-testid="cart-page">
8
- <div class="cart-empty">
9
- <h1 class="cart-empty-title">Loading your cart...</h1>
10
- </div>
9
+ <BaseLayout title="Your Cart | Nymbal" description="Review the items in your cart." cart={cart}>
10
+ <div class="container section" data-testid="cart-page">
11
+ <NymbalElement
12
+ tag="nymbal-cart-page"
13
+ attributes={cart === null ? {} : { cart: JSON.stringify(cart) }}
14
+ />
11
15
  </div>
12
16
  </BaseLayout>
13
17
 
14
18
  <script>
15
- import '../scripts/cart-page.js'
19
+ import '../lib/client.js'
20
+ import { NymbalCartPage } from '@nymbalhq/web-components/elements/nymbal-cart-page'
21
+
22
+ NymbalCartPage.define()
16
23
  </script>
@@ -1,22 +1,25 @@
1
1
  ---
2
2
  import '../../styles/pages/plp.css'
3
- import '../../styles/pages/home.css'
4
3
  import BaseLayout from '../../layouts/BaseLayout.astro'
5
- import Breadcrumb from '../../components/Breadcrumb.astro'
6
- import ProductCard from '../../components/ProductCard.astro'
7
- import SortDropdown from '../../components/SortDropdown.astro'
8
- import Pagination from '../../components/Pagination.astro'
9
- import { fetchProducts, fetchCategories, sortFromUrl } from '../../lib/api'
4
+ import NymbalElement from '@nymbalhq/web-components/astro'
5
+ import { cardImageOf, cardProductOf } from '@nymbalhq/web-components/ssr'
6
+ import { PRODUCT_SORT_LABELS } from '@nymbalhq/types'
7
+ import { fetchProducts, fetchCategories, fetchCapabilities, sortFromUrl } from '../../lib/api'
10
8
 
11
9
  const { slug } = Astro.params
12
- const sort = sortFromUrl(Astro.url)
10
+ const capabilities = await fetchCapabilities()
11
+ const sortKeys = capabilities?.catalog.sortKeys ?? []
12
+ const sort = sortFromUrl(Astro.url, capabilities?.catalog)
13
13
 
14
14
  const [productResult, categories] = await Promise.all([
15
15
  fetchProducts({ limit: 24, category: slug, sort }),
16
16
  fetchCategories(),
17
17
  ])
18
18
 
19
- const products = productResult.items
19
+ const products = productResult.items.map(cardProductOf)
20
+ // The first card's image is the page's LCP, and the preload scanner never reaches it inside the grid's shadow root:
21
+ // the head starts it.
22
+ const firstImage = products[0] === undefined ? null : cardImageOf(products[0]).url
20
23
  const category = categories.find((c) => c.slug === slug)
21
24
  const categoryName = category?.name ?? slug ?? 'Category'
22
25
  const facets = productResult.facets ?? []
@@ -26,8 +29,9 @@ const facets = productResult.facets ?? []
26
29
  title={`${categoryName} | Nymbal`}
27
30
  description={category?.description ?? `Browse ${categoryName} products.`}
28
31
  >
32
+ {firstImage && <link slot="head" rel="preload" as="image" href={firstImage} fetchpriority="high" />}
29
33
  <div class="container section">
30
- <Breadcrumb items={[{ label: 'Home', href: '/' }, { label: categoryName }]} />
34
+ <NymbalElement tag="nymbal-breadcrumb" attributes={{ items: JSON.stringify([{ label: 'Home', href: '/' }, { label: categoryName }]) }} />
31
35
 
32
36
  <div class="plp-header">
33
37
  <h1 class="plp-title">{categoryName}</h1>
@@ -36,33 +40,44 @@ const facets = productResult.facets ?? []
36
40
  {category.description}
37
41
  </p>
38
42
  )}
39
- <p class="plp-count" id="product-count">{products.length} products</p>
40
43
  </div>
41
44
 
42
45
  <div class="plp-layout">
43
46
  <aside class="plp-sidebar">
44
- <nymbal-product-filter
45
- facets={JSON.stringify(facets)}
46
- ></nymbal-product-filter>
47
+ {facets.length > 0 && (
48
+ <nymbal-product-filter
49
+ facets={JSON.stringify(facets)}
50
+ ></nymbal-product-filter>
51
+ )}
47
52
  </aside>
48
53
 
49
54
  <div class="plp-grid-area">
50
- <SortDropdown sort={sort} />
51
-
52
- <div class="product-grid" id="product-grid" data-category={slug}>
53
- {products.map((product) => (
54
- <ProductCard product={product} />
55
- ))}
56
- </div>
57
-
58
- {products.length === 0 && (
59
- <div class="plp-empty">
60
- <h2 class="plp-empty-title">No products found</h2>
61
- <p class="plp-empty-text">Check back soon for new arrivals.</p>
55
+ {sortKeys.length > 0 && (
56
+ <div class="plp-sort-bar">
57
+ <label for="sort-select">Sort by</label>
58
+ <nymbal-sort-dropdown
59
+ sort-keys={JSON.stringify(sortKeys)}
60
+ value={sort}
61
+ default-sort={capabilities?.catalog.defaultSort}
62
+ >
63
+ <select id="sort-select" data-testid="sort-dropdown">
64
+ {sortKeys.map((key) => (
65
+ <option value={key} selected={key === sort}>{PRODUCT_SORT_LABELS[key]}</option>
66
+ ))}
67
+ </select>
68
+ </nymbal-sort-dropdown>
62
69
  </div>
63
70
  )}
64
71
 
65
- <Pagination />
72
+ <NymbalElement
73
+ tag="nymbal-product-grid"
74
+ attributes={{
75
+ products: JSON.stringify(products),
76
+ source: 'product-list',
77
+ ...(productResult.nextCursor !== null && { 'has-more': '' }),
78
+ 'data-category': slug!,
79
+ }}
80
+ />
66
81
  </div>
67
82
  </div>
68
83
  </div>
@@ -1,100 +1,44 @@
1
1
  ---
2
- import '../styles/pages/checkout.css'
2
+ import NymbalElement from '@nymbalhq/web-components/astro'
3
3
  import BaseLayout from '../layouts/BaseLayout.astro'
4
- import CheckoutSteps from '../components/CheckoutSteps.astro'
5
- import AddressForm from '../components/AddressForm.astro'
4
+ import { fetchCapabilities, fetchCart } from '../lib/api'
5
+
6
+ const [capabilities, cart] = await Promise.all([
7
+ fetchCapabilities(),
8
+ fetchCart(Astro.request.headers.get('cookie')),
9
+ ])
10
+ if (capabilities === null) {
11
+ throw new Error('The store did not answer GET /api/capabilities, so checkout cannot tell how to take a payment.')
12
+ }
13
+ const escapeAttribute = (value: string): string =>
14
+ value.replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;')
15
+ // The payments adapter's client half, the checkout's light-DOM child in its payment slot; a driver the project
16
+ // registers in src/lib/client.ts renders it. HTML, not markup: Astro would take `slot="payment"` on a component's
17
+ // child as a slot of NymbalElement's own.
18
+ const paymentElement = `<nymbal-payment-element slot="payment" data-testid="payment-element" client="${escapeAttribute(
19
+ JSON.stringify(capabilities.payments.client),
20
+ )}"></nymbal-payment-element>`
6
21
  ---
7
22
 
8
- <BaseLayout title="Checkout | Nymbal" description="Complete your purchase.">
23
+ <BaseLayout title="Checkout | Nymbal" description="Complete your purchase." cart={cart}>
9
24
  <div class="container section">
10
- <h1 style="font-size: 1.75rem; font-weight: var(--nymbal-font-weight-bold, 700); margin-bottom: var(--nymbal-spacing-md, 1rem);">Checkout</h1>
11
-
12
- <div id="checkout-steps-container">
13
- <CheckoutSteps currentStep="contact" />
14
- </div>
15
-
16
- <div class="checkout-error" id="checkout-error" style="display:none;" data-testid="checkout-error" role="alert"></div>
17
-
18
- <div class="checkout-layout">
19
- <div class="checkout-form">
20
- <!-- Contact Section -->
21
- <section class="checkout-section" id="checkout-contact" data-testid="checkout-contact">
22
- <h2 class="checkout-section-title">Contact Information</h2>
23
- <form id="contact-form">
24
- <div class="form-group">
25
- <label for="contact-email">Email Address</label>
26
- <input
27
- type="email"
28
- id="contact-email"
29
- name="email"
30
- required
31
- autocomplete="email"
32
- placeholder="you@example.com"
33
- data-testid="contact-email"
34
- />
35
- </div>
36
- <button type="submit" class="btn btn-primary" data-testid="contact-continue">
37
- Continue to Shipping
38
- </button>
39
- </form>
40
- </section>
41
-
42
- <!-- Shipping Section -->
43
- <section class="checkout-section" id="checkout-shipping" style="display:none;" data-testid="checkout-shipping">
44
- <h2 class="checkout-section-title">Shipping Address</h2>
45
- <form id="shipping-form">
46
- <AddressForm id="shipping" />
47
- <button type="submit" class="btn btn-primary" style="margin-top: var(--nymbal-spacing-md, 1rem);" data-testid="shipping-continue">
48
- Continue to Payment
49
- </button>
50
- </form>
51
- </section>
52
-
53
- <!-- Payment Section -->
54
- <section class="checkout-section" id="checkout-payment" style="display:none;" data-testid="checkout-payment">
55
- <h2 class="checkout-section-title">Payment</h2>
56
- <form id="payment-form" novalidate>
57
- <div class="form-group">
58
- <label>
59
- <input type="checkbox" id="billing-same" checked />
60
- Billing address same as shipping
61
- </label>
62
- </div>
63
-
64
- <div id="billing-fields" style="display:none;">
65
- <h3 class="checkout-section-title" style="font-size: 1rem; margin-top: var(--nymbal-spacing-md, 1rem);">Billing Address</h3>
66
- <div id="billing-form">
67
- <AddressForm id="billing" />
68
- </div>
69
- </div>
70
-
71
- <div class="payment-section" style="margin-top: var(--nymbal-spacing-md, 1rem);">
72
- <label style="margin-bottom: var(--nymbal-spacing-sm, 0.5rem);">Card Details</label>
73
- <div id="stripe-card-element" style="padding: var(--nymbal-spacing-sm, 0.5rem) var(--nymbal-spacing-md, 1rem); border: 1px solid var(--nymbal-color-border, #e4e4e7); border-radius: var(--nymbal-radius-md, 0.5rem); background: var(--nymbal-color-surface, #fff); min-height: 40px;" data-testid="stripe-card-element">
74
- <!-- Stripe CardElement mounts here -->
75
- </div>
76
- </div>
77
-
78
- <div class="checkout-submit">
79
- <button type="submit" class="btn btn-primary" data-testid="place-order">
80
- Place Order
81
- </button>
82
- <p class="checkout-terms">
83
- By placing your order, you agree to our terms and conditions.
84
- </p>
85
- </div>
86
- </form>
87
- </section>
88
- </div>
89
-
90
- <aside class="checkout-summary" id="checkout-summary" data-testid="checkout-summary">
91
- <h2 class="checkout-summary-title">Order Summary</h2>
92
- <p style="font-size: var(--nymbal-font-size-sm, 0.875rem); color: var(--nymbal-color-text-muted, #71717a);">Loading cart...</p>
93
- </aside>
94
- </div>
25
+ <NymbalElement tag="nymbal-checkout" attributes={cart === null ? {} : { cart: JSON.stringify(cart) }}>
26
+ <Fragment set:html={paymentElement} />
27
+ </NymbalElement>
95
28
  </div>
96
29
  </BaseLayout>
97
30
 
98
31
  <script>
99
- import '../scripts/checkout.js'
32
+ import { NymbalCheckout } from '@nymbalhq/web-components/elements/nymbal-checkout'
33
+ import { NymbalPaymentElement } from '@nymbalhq/web-components/elements/nymbal-payment-element'
34
+
35
+ // The page's own elements; the layout defines the rest.
36
+ NymbalPaymentElement.define()
37
+ NymbalCheckout.define()
38
+
39
+ // The element places the order; the page only goes to it.
40
+ document.addEventListener('nymbal:checkout:placed', (event) => {
41
+ const { orderNumber } = (event as CustomEvent<{ orderNumber: string }>).detail
42
+ window.location.href = `/order/${encodeURIComponent(orderNumber)}`
43
+ })
100
44
  </script>
@@ -1,5 +1,8 @@
1
1
  ---
2
- import Footer from '../../components/Footer.astro'
2
+ import NymbalElement from '@nymbalhq/web-components/astro'
3
+ import { FOOTER } from '../../lib/chrome'
3
4
  Astro.response.headers.set('Cache-Control', 'public, s-maxage=86400, stale-while-revalidate=300')
5
+ // A fragment has no <meta charset>, so the charset is in the header.
6
+ Astro.response.headers.set('Content-Type', 'text/html; charset=utf-8')
4
7
  ---
5
- <Footer />
8
+ <NymbalElement tag="nymbal-site-footer" attributes={FOOTER} />
@@ -1,5 +1,15 @@
1
1
  ---
2
- import Header from '../../components/Header.astro'
2
+ import NymbalElement from '@nymbalhq/web-components/astro'
3
+ import { HEADER, headerSlots } from '../../lib/chrome'
4
+ import { headerFragment } from '../../lib/fragments'
5
+
6
+ const header = await headerFragment()
7
+ if (header instanceof Response) return header
8
+
3
9
  Astro.response.headers.set('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=300')
10
+ // A fragment has no <meta charset>, so the charset is in the header.
11
+ Astro.response.headers.set('Content-Type', 'text/html; charset=utf-8')
4
12
  ---
5
- <Header />
13
+ <NymbalElement tag="nymbal-site-header" attributes={HEADER}>
14
+ <Fragment set:html={headerSlots(header.canSearch)} />
15
+ </NymbalElement>
@@ -1,9 +1,12 @@
1
1
  ---
2
- import CategoryNav from '../../components/CategoryNav.astro'
3
- import { fetchCategories } from '../../lib/api'
2
+ import NymbalElement from '@nymbalhq/web-components/astro'
3
+ import { navFragment } from '../../lib/fragments'
4
4
 
5
- Astro.response.headers.set('Cache-Control', 'public, s-maxage=21600, stale-while-revalidate=300')
5
+ const nav = await navFragment()
6
+ if (nav instanceof Response) return nav
6
7
 
7
- const categories = await fetchCategories()
8
+ Astro.response.headers.set('Cache-Control', 'public, s-maxage=21600, stale-while-revalidate=300')
9
+ // A fragment has no <meta charset>, so the charset is in the header.
10
+ Astro.response.headers.set('Content-Type', 'text/html; charset=utf-8')
8
11
  ---
9
- <CategoryNav categories={categories} />
12
+ <NymbalElement tag="nymbal-category-nav" attributes={{ categories: JSON.stringify(nav.categories) }} />
@@ -1,14 +1,13 @@
1
1
  ---
2
- import ProductCard from '../../../components/ProductCard.astro'
3
- import { fetchProduct } from '../../../lib/api'
2
+ import NymbalElement from '@nymbalhq/web-components/astro'
3
+ import { cardProductOf } from '@nymbalhq/web-components/ssr'
4
+ import { productFragment } from '../../../lib/fragments'
4
5
 
5
- Astro.response.headers.set('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=60')
6
-
7
- const { slug } = Astro.params
8
- const product = await fetchProduct(slug!)
6
+ const card = await productFragment(Astro.params.slug!)
7
+ if (card instanceof Response) return card
9
8
 
10
- if (!product) {
11
- return new Response('Not found', { status: 404 })
12
- }
9
+ Astro.response.headers.set('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=60')
10
+ // A fragment has no <meta charset>, so the charset is in the header.
11
+ Astro.response.headers.set('Content-Type', 'text/html; charset=utf-8')
13
12
  ---
14
- <ProductCard product={product} />
13
+ <NymbalElement tag="nymbal-product-card" attributes={{ product: JSON.stringify(cardProductOf(card.product)) }} />
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  import '../styles/pages/home.css'
3
+ import NymbalElement from '@nymbalhq/web-components/astro'
4
+ import { cardImageOf, cardProductOf } from '@nymbalhq/web-components/ssr'
3
5
  import BaseLayout from '../layouts/BaseLayout.astro'
4
6
  import HeroSection from '../components/HeroSection.astro'
5
- import CategoryNav from '../components/CategoryNav.astro'
6
- import ProductCard from '../components/ProductCard.astro'
7
7
  import NewsletterSignup from '../components/NewsletterSignup.astro'
8
8
  import { fetchProducts, fetchCategories } from '../lib/api'
9
9
  import { organizationJsonLd } from '../lib/seo'
@@ -12,17 +12,21 @@ const [productResult, categories] = await Promise.all([
12
12
  fetchProducts({ limit: 8 }),
13
13
  fetchCategories(),
14
14
  ])
15
+ const products = productResult.items.map(cardProductOf)
16
+ // The first card's image is the page's likely LCP, and the preload scanner never reaches it inside the grid's shadow
17
+ // root: the head starts it.
18
+ const firstImage = products[0] === undefined ? null : cardImageOf(products[0]).url
15
19
 
16
- const products = productResult.items
17
20
  const jsonLd = organizationJsonLd('Nymbal', Astro.url.origin)
18
21
  ---
19
22
 
20
23
  <BaseLayout title="Nymbal — Modern Storefront" jsonLd={jsonLd}>
24
+ {firstImage && <link slot="head" rel="preload" as="image" href={firstImage} fetchpriority="high" />}
21
25
  <HeroSection />
22
26
 
23
27
  <section class="section container">
24
28
  <h2 class="section-title">Shop by Category</h2>
25
- <CategoryNav categories={categories} />
29
+ <NymbalElement tag="nymbal-category-nav" attributes={{ categories: JSON.stringify(categories) }} />
26
30
  </section>
27
31
 
28
32
  <section class="featured-section section">
@@ -31,19 +35,18 @@ const jsonLd = organizationJsonLd('Nymbal', Astro.url.origin)
31
35
  <h2 class="section-title" style="margin-bottom: 0;">Featured Products</h2>
32
36
  <a href="/products" class="featured-link">View all</a>
33
37
  </div>
34
- {products.length > 0 ? (
35
- <div class="product-grid">
36
- {products.map((product) => (
37
- <ProductCard product={product} />
38
- ))}
39
- </div>
40
- ) : (
41
- <p style="text-align: center; color: var(--nymbal-color-text-muted, #71717a);">
42
- Products coming soon.
43
- </p>
44
- )}
38
+ <NymbalElement tag="nymbal-product-grid" attributes={{ products: JSON.stringify(products) }} />
45
39
  </div>
46
40
  </section>
47
41
 
48
42
  <NewsletterSignup />
49
43
  </BaseLayout>
44
+
45
+ <script>
46
+ import '../lib/client.js'
47
+ import { NymbalCategoryNav } from '@nymbalhq/web-components/elements/nymbal-category-nav'
48
+ import { NymbalProductGrid } from '@nymbalhq/web-components/elements/nymbal-product-grid'
49
+
50
+ NymbalCategoryNav.define()
51
+ NymbalProductGrid.define()
52
+ </script>
@@ -1,9 +1,8 @@
1
1
  ---
2
2
  import '../../styles/pages/confirmation.css'
3
3
  import BaseLayout from '../../layouts/BaseLayout.astro'
4
- import OrderSummary from '../../components/OrderSummary.astro'
4
+ import NymbalElement from '@nymbalhq/web-components/astro'
5
5
  import { fetchOrder } from '../../lib/api'
6
- import { formatPrice } from '../../lib/format'
7
6
  import { isPaid, orderHeading } from '../../lib/order-status'
8
7
 
9
8
  const { orderNumber } = Astro.params
@@ -11,11 +10,22 @@ const order = await fetchOrder(orderNumber!)
11
10
 
12
11
  if (!order) return Astro.redirect('/404')
13
12
 
14
- const lineItems = (order.lineItems ?? []).map((item) => ({
15
- name: `${item.productName}${item.variantName ? ` - ${item.variantName}` : ''}`,
16
- qty: item.qty,
17
- priceMinor: item.unitPriceMinor,
18
- }))
13
+ const summary = {
14
+ items: JSON.stringify(
15
+ (order.lineItems ?? []).map((item) => ({
16
+ name: `${item.productName}${item.variantName ? ` - ${item.variantName}` : ''}`,
17
+ qty: item.qty,
18
+ unitPriceMinor: item.unitPriceMinor,
19
+ lineTotalMinor: item.lineTotalMinor,
20
+ })),
21
+ ),
22
+ 'subtotal-minor': String(order.subtotalMinor),
23
+ 'discount-minor': String(order.discountTotalMinor ?? 0),
24
+ 'shipping-minor': String(order.shippingTotalMinor ?? 0),
25
+ 'tax-minor': String(order.taxTotalMinor ?? 0),
26
+ 'total-minor': String(order.totalMinor),
27
+ currency: order.currency,
28
+ }
19
29
 
20
30
  const shippingAddress = order.shippingAddress
21
31
  const heading = orderHeading(order.status)
@@ -63,45 +73,10 @@ const paid = isPaid(order.status)
63
73
  <dd style="text-transform: capitalize;" data-testid="order-status">{order.status}</dd>
64
74
  </div>
65
75
  </dl>
66
-
67
- <div class="confirmation-items">
68
- {lineItems.map((item) => (
69
- <div class="confirmation-item">
70
- <span>
71
- <span class="confirmation-item-name">{item.name}</span>
72
- <span class="confirmation-item-qty"> x{item.qty}</span>
73
- </span>
74
- <span class="confirmation-item-price">
75
- {formatPrice(item.priceMinor * item.qty, order.currency)}
76
- </span>
77
- </div>
78
- ))}
79
- </div>
80
-
81
- <div class="confirmation-totals">
82
- <div class="confirmation-total-row">
83
- <span>Subtotal</span>
84
- <span>{formatPrice(order.subtotalMinor, order.currency)}</span>
85
- </div>
86
- {order.shippingTotalMinor != null && (
87
- <div class="confirmation-total-row">
88
- <span>Shipping</span>
89
- <span>{order.shippingTotalMinor === 0 ? 'Free' : formatPrice(order.shippingTotalMinor, order.currency)}</span>
90
- </div>
91
- )}
92
- {order.taxTotalMinor != null && (
93
- <div class="confirmation-total-row">
94
- <span>Tax</span>
95
- <span>{formatPrice(order.taxTotalMinor, order.currency)}</span>
96
- </div>
97
- )}
98
- <div class="confirmation-total-row grand-total">
99
- <span>Total</span>
100
- <span>{formatPrice(order.totalMinor, order.currency)}</span>
101
- </div>
102
- </div>
103
76
  </div>
104
77
 
78
+ <NymbalElement tag="nymbal-order-summary" attributes={{ class: 'confirmation-summary', ...summary }} />
79
+
105
80
  {shippingAddress && (
106
81
  <div class="confirmation-card">
107
82
  <h2 class="confirmation-card-title">Shipping Address</h2>
@@ -124,4 +99,7 @@ const paid = isPaid(order.status)
124
99
 
125
100
  <script>
126
101
  import '../../scripts/order-status.js'
102
+ import { NymbalOrderSummary } from '@nymbalhq/web-components/elements/nymbal-order-summary'
103
+
104
+ NymbalOrderSummary.define()
127
105
  </script>