@ecomiq/storefront 1.1.9 → 2.0.1

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 (134) hide show
  1. package/README.md +30 -415
  2. package/bin/ecomiq.mjs +165 -0
  3. package/package.json +72 -94
  4. package/src/api-proxy.ts +194 -0
  5. package/src/auth/create-storefront-auth.ts +35 -0
  6. package/src/auth/ecomiq-oauth-password-client.ts +26 -0
  7. package/src/auth/ecomiq-oauth-password-constants.ts +26 -0
  8. package/src/auth/ecomiq-oauth-password-plugin.ts +299 -0
  9. package/src/auth/ecomiq-oauth-password-types.ts +25 -0
  10. package/src/auth/ecomiq-storefront-sign-in.ts +47 -0
  11. package/src/auth/env.ts +7 -0
  12. package/src/cart-session.ts +241 -0
  13. package/src/client.ts +48 -0
  14. package/src/codegen.mjs +704 -0
  15. package/src/components/checkout/cart-sync.ts +143 -0
  16. package/src/components/checkout/checkout-external-update-banner.tsx +15 -0
  17. package/src/components/checkout/checkout-form-controls.tsx +139 -0
  18. package/src/components/checkout/checkout-line-items.tsx +43 -0
  19. package/src/components/checkout/checkout-order-summary.tsx +156 -0
  20. package/src/components/checkout/checkout-payment-section.tsx +215 -0
  21. package/src/components/checkout/checkout-session-view.tsx +240 -0
  22. package/src/components/checkout/checkout-shipping-address-section.tsx +266 -0
  23. package/src/components/checkout/checkout-shipping-section.tsx +90 -0
  24. package/src/components/checkout/checkout-states.tsx +73 -0
  25. package/src/components/checkout/form-schema.ts +229 -0
  26. package/src/components/checkout/format.ts +27 -0
  27. package/src/components/checkout/index.ts +51 -0
  28. package/src/components/checkout/types.ts +43 -0
  29. package/src/components/checkout/use-checkout-geo.ts +50 -0
  30. package/src/components/checkout/use-checkout-session.ts +583 -0
  31. package/src/components/hero.tsx +291 -0
  32. package/src/config.d.ts +69 -0
  33. package/src/config.mjs +165 -0
  34. package/src/index.ts +274 -0
  35. package/src/link.tsx +2 -0
  36. package/src/pages/checkout.$checkoutId.tsx +60 -0
  37. package/src/pages/checkout.tsx +31 -0
  38. package/src/pages/thank-you.tsx +53 -0
  39. package/src/providers/auth.tsx +69 -0
  40. package/src/providers/cart.tsx +275 -0
  41. package/src/providers/ecomiq.ts +137 -0
  42. package/src/providers/index.ts +26 -0
  43. package/src/public-config.ts +44 -0
  44. package/src/review-submission.ts +89 -0
  45. package/src/runtime/context.ts +1 -0
  46. package/src/runtime/default-layout.tsx +80 -0
  47. package/src/runtime/head.ts +126 -0
  48. package/src/runtime/page.tsx +82 -0
  49. package/src/runtime/postcss-browser-stub.mjs +5 -0
  50. package/src/server-config-store.ts +26 -0
  51. package/src/server.ts +279 -0
  52. package/src/styles.css +76 -0
  53. package/src/ui/badge.tsx +32 -0
  54. package/src/ui/button.tsx +31 -0
  55. package/src/ui/card.tsx +31 -0
  56. package/src/ui/carousel.tsx +218 -0
  57. package/src/ui/cn.ts +6 -0
  58. package/src/ui/index.ts +8 -0
  59. package/src/ui/separator.tsx +6 -0
  60. package/src/ui/skeleton.tsx +6 -0
  61. package/src/ui/storefront-error.tsx +49 -0
  62. package/src/utils.ts +36 -0
  63. package/src/vite-inline.mjs +135 -0
  64. package/templates/__root.tsx +77 -0
  65. package/templates/router.tsx +19 -0
  66. package/dist/chunk-64GLOWMJ.js +0 -216
  67. package/dist/chunk-64GLOWMJ.js.map +0 -1
  68. package/dist/chunk-AKD5IDEB.js +0 -177
  69. package/dist/chunk-AKD5IDEB.js.map +0 -1
  70. package/dist/chunk-EFEVJJWF.js +0 -1345
  71. package/dist/chunk-EFEVJJWF.js.map +0 -1
  72. package/dist/chunk-NAJQB5DN.js +0 -155
  73. package/dist/chunk-NAJQB5DN.js.map +0 -1
  74. package/dist/chunk-OABDGTBI.js +0 -253
  75. package/dist/chunk-OABDGTBI.js.map +0 -1
  76. package/dist/chunk-Q4U5OEUT.js +0 -21
  77. package/dist/chunk-Q4U5OEUT.js.map +0 -1
  78. package/dist/chunk-S7CPZ4UE.js +0 -6
  79. package/dist/chunk-S7CPZ4UE.js.map +0 -1
  80. package/dist/chunks/client.css +0 -984
  81. package/dist/chunks/client.css.map +0 -1
  82. package/dist/chunks/client.d.ts +0 -10759
  83. package/dist/chunks/client.js +0 -5398
  84. package/dist/chunks/client.js.map +0 -1
  85. package/dist/chunks/config.d.ts +0 -31
  86. package/dist/chunks/config.js +0 -5
  87. package/dist/chunks/config.js.map +0 -1
  88. package/dist/chunks/core-public.d.ts +0 -130
  89. package/dist/chunks/core-public.js +0 -92
  90. package/dist/chunks/core-public.js.map +0 -1
  91. package/dist/chunks/server-nextjs.d.ts +0 -33
  92. package/dist/chunks/server-nextjs.js +0 -59
  93. package/dist/chunks/server-nextjs.js.map +0 -1
  94. package/dist/chunks/server-tanstack.d.ts +0 -22
  95. package/dist/chunks/server-tanstack.js +0 -40
  96. package/dist/chunks/server-tanstack.js.map +0 -1
  97. package/dist/collection-DU7gREXS.d.ts +0 -4062
  98. package/dist/ecomiq-oauth-password-plugin-B_gMdGuc.d.ts +0 -3631
  99. package/dist/ecomiq-shell-ui-SWIFLQVM.js +0 -8
  100. package/dist/ecomiq-shell-ui-SWIFLQVM.js.map +0 -1
  101. package/dist/index.d.ts +0 -9
  102. package/dist/index.js +0 -1
  103. package/dist/index.js.map +0 -1
  104. package/dist/index.server.d.ts +0 -9
  105. package/dist/index.server.js +0 -8
  106. package/dist/index.server.js.map +0 -1
  107. package/dist/nextjs/api.d.ts +0 -45
  108. package/dist/nextjs/api.js +0 -32
  109. package/dist/nextjs/api.js.map +0 -1
  110. package/dist/nextjs/globals.css +0 -13
  111. package/dist/nextjs.d.ts +0 -2
  112. package/dist/nextjs.js +0 -2
  113. package/dist/nextjs.js.map +0 -1
  114. package/dist/nextjs.server.d.ts +0 -18
  115. package/dist/nextjs.server.js +0 -1
  116. package/dist/nextjs.server.js.map +0 -1
  117. package/dist/postcss.config.mjs +0 -6
  118. package/dist/react/checkout-page.css +0 -855
  119. package/dist/react/client.css +0 -984
  120. package/dist/react/payment-provider.css +0 -91
  121. package/dist/react/thank-you-page.css +0 -105
  122. package/dist/react.d.ts +0 -1
  123. package/dist/react.js +0 -2
  124. package/dist/react.js.map +0 -1
  125. package/dist/store-proxy-handler-CNzKlTFA.d.ts +0 -10
  126. package/dist/tanstack/api.d.ts +0 -41
  127. package/dist/tanstack/api.js +0 -30
  128. package/dist/tanstack/api.js.map +0 -1
  129. package/dist/tanstack.d.ts +0 -1
  130. package/dist/tanstack.js +0 -2
  131. package/dist/tanstack.js.map +0 -1
  132. package/dist/tanstack.server.d.ts +0 -18
  133. package/dist/tanstack.server.js +0 -1
  134. package/dist/tanstack.server.js.map +0 -1
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Runtime interno: construcción del <head>.
3
+ * Mezcla el SEO global de ecomiq.config.js con el `meta` de cada página
4
+ * (la página siempre gana) y genera los scripts de analytics declarados.
5
+ */
6
+ import type { PageMeta, PublicEcomiqConfig } from '../index'
7
+
8
+ type MetaTag = Record<string, string>
9
+ type LinkTag = Record<string, string>
10
+ type ScriptTag = Record<string, string | boolean> & { children?: string }
11
+
12
+ interface HeadResult {
13
+ meta?: MetaTag[]
14
+ links?: LinkTag[]
15
+ scripts?: ScriptTag[]
16
+ }
17
+
18
+ function serializeJsonLd(value: Record<string, unknown>) {
19
+ return JSON.stringify(value)
20
+ .replace(/</g, '\\u003c')
21
+ .replace(/\u2028/g, '\\u2028')
22
+ .replace(/\u2029/g, '\\u2029')
23
+ }
24
+
25
+ /** Aplica titleTemplate ('%s · Tienda') o cae al patrón `Título · Tienda`. */
26
+ export function resolveTitle(pageTitle: string | undefined, config: PublicEcomiqConfig): string {
27
+ const seo = config.seo ?? {}
28
+ if (!pageTitle) return config.name
29
+ const template = seo.titleTemplate
30
+ return template ? template.replace('%s', pageTitle) : `${pageTitle} · ${config.name}`
31
+ }
32
+
33
+ /** Head global: charset, viewport, favicon, metas/links extra y analytics. Va en el root. */
34
+ export function buildRootHead(config: PublicEcomiqConfig): HeadResult {
35
+ const seo = config.seo ?? {}
36
+
37
+ const meta: MetaTag[] = [
38
+ { charSet: 'utf-8' },
39
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
40
+ ]
41
+ if (seo.keywords?.length) meta.push({ name: 'keywords', content: seo.keywords.join(', ') })
42
+ for (const extra of seo.meta ?? []) meta.push(extra)
43
+
44
+ const links: LinkTag[] = [{ rel: 'icon', href: seo.favicon ?? '/favicon.ico' }]
45
+ for (const extra of seo.links ?? []) links.push(extra)
46
+
47
+ return { meta, links, scripts: buildAnalyticsScripts(config) }
48
+ }
49
+
50
+ /** Head por página: title (con plantilla), description, OG/Twitter, robots, metas extra y JSON-LD. */
51
+ export function buildPageHead(
52
+ pageMeta: PageMeta | undefined,
53
+ config: PublicEcomiqConfig,
54
+ ): HeadResult {
55
+ const seo = config.seo ?? {}
56
+ const title = resolveTitle(pageMeta?.title, config)
57
+ const description = pageMeta?.description ?? config.description
58
+ const ogImage = pageMeta?.ogImage ?? seo.ogImage
59
+
60
+ const meta: MetaTag[] = [{ title }]
61
+ if (description) meta.push({ name: 'description', content: description })
62
+ if (pageMeta?.noIndex) meta.push({ name: 'robots', content: 'noindex, nofollow' })
63
+
64
+ // Open Graph + Twitter con fallbacks razonables
65
+ meta.push({ property: 'og:title', content: title })
66
+ meta.push({ property: 'og:type', content: 'website' })
67
+ meta.push({ property: 'og:site_name', content: config.name })
68
+ if (description) meta.push({ property: 'og:description', content: description })
69
+ if (ogImage) {
70
+ meta.push({ property: 'og:image', content: ogImage })
71
+ meta.push({ name: 'twitter:card', content: seo.twitterCard ?? 'summary_large_image' })
72
+ meta.push({ name: 'twitter:image', content: ogImage })
73
+ }
74
+
75
+ for (const extra of pageMeta?.meta ?? []) meta.push(extra as MetaTag)
76
+
77
+ const scripts: ScriptTag[] = []
78
+ if (pageMeta?.jsonLd) {
79
+ scripts.push({ type: 'application/ld+json', children: serializeJsonLd(pageMeta.jsonLd) })
80
+ }
81
+
82
+ return { meta, ...(scripts.length ? { scripts } : {}) }
83
+ }
84
+
85
+ /** Traduce el bloque `analytics` del config a etiquetas <script>. */
86
+ export function buildAnalyticsScripts(config: PublicEcomiqConfig): ScriptTag[] {
87
+ const analytics = config.analytics ?? {}
88
+ const scripts: ScriptTag[] = []
89
+
90
+ if (analytics.googleAnalytics) {
91
+ const id = analytics.googleAnalytics
92
+ scripts.push({ src: `https://www.googletagmanager.com/gtag/js?id=${id}`, async: true })
93
+ scripts.push({
94
+ children: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)};gtag('js',new Date());gtag('config','${id}');`,
95
+ })
96
+ }
97
+
98
+ if (analytics.plausible) {
99
+ scripts.push({
100
+ src: 'https://plausible.io/js/script.js',
101
+ defer: true,
102
+ 'data-domain': analytics.plausible,
103
+ })
104
+ }
105
+
106
+ if (analytics.metaPixel) {
107
+ const id = analytics.metaPixel
108
+ scripts.push({
109
+ children: `!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init','${id}');fbq('track','PageView');`,
110
+ })
111
+ }
112
+
113
+ for (const custom of analytics.scripts ?? []) {
114
+ if (custom.src) {
115
+ scripts.push({
116
+ src: custom.src,
117
+ ...(custom.defer ? { defer: true } : {}),
118
+ ...(custom.async ? { async: true } : {}),
119
+ })
120
+ } else if (custom.inline) {
121
+ scripts.push({ children: custom.inline })
122
+ }
123
+ }
124
+
125
+ return scripts
126
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Runtime interno de Ecomiq (solo lo importa el código generado en .ecomiq).
3
+ * Traduce el contrato público `definePage` a las opciones del router interno.
4
+ */
5
+ import type { EcomiqPage, PublicEcomiqConfig } from '../index'
6
+ import { StorefrontError } from '../ui/storefront-error'
7
+ import { buildPageHead } from './head'
8
+
9
+ export function pageToRouteOptions<
10
+ TData,
11
+ TParams extends Record<string, string>,
12
+ TSearch extends Record<string, unknown>,
13
+ >(
14
+ page: EcomiqPage<TData, TParams, TSearch>,
15
+ config: PublicEcomiqConfig,
16
+ ) {
17
+ const readSearch = page.validateSearch ?? ((raw: Record<string, unknown>) => raw as TSearch)
18
+ const reloadOnSearch = page.reloadOnSearch !== false
19
+ const stableLoaderDeps: Record<string, never> = {}
20
+
21
+ return {
22
+ preload: page.preload,
23
+ shouldReload: reloadOnSearch ? undefined : false,
24
+ validateSearch: readSearch,
25
+ loaderDeps: ({ search }: { search: TSearch }) => reloadOnSearch ? search : stableLoaderDeps,
26
+ loader: page.loader
27
+ ? ({ params, deps, location }: {
28
+ params: TParams
29
+ deps: TSearch | Record<string, never>
30
+ location?: { search?: Record<string, unknown> }
31
+ }) => {
32
+ let search = readSearch(location?.search ?? {})
33
+ if (reloadOnSearch) {
34
+ search = deps as TSearch
35
+ }
36
+ return page.loader!({
37
+ params,
38
+ search,
39
+ config,
40
+ })
41
+ }
42
+ : undefined,
43
+ head: ({ loaderData, params, match }: {
44
+ loaderData: TData
45
+ params: TParams
46
+ match: { search: TSearch }
47
+ }) => buildPageHead(
48
+ loaderData === undefined
49
+ ? undefined
50
+ : page.meta?.({
51
+ data: loaderData,
52
+ params,
53
+ search: match.search,
54
+ config,
55
+ }),
56
+ config,
57
+ ),
58
+ pendingComponent: page.loading,
59
+ errorComponent: page.error
60
+ ? ({ error, reset }: { error: Error; reset: () => void }) => page.error!({ error, reset })
61
+ : ({ reset }: { error: Error; reset: () => void }) => <StorefrontError onRetry={reset} />,
62
+ }
63
+ }
64
+
65
+ export function PageBoundary<
66
+ TData,
67
+ TParams extends Record<string, string>,
68
+ TSearch extends Record<string, unknown>,
69
+ >({
70
+ page,
71
+ data,
72
+ params,
73
+ search,
74
+ }: {
75
+ page: EcomiqPage<TData, TParams, TSearch>
76
+ data: TData
77
+ params: TParams
78
+ search: TSearch
79
+ }) {
80
+ const Component = page.component
81
+ return <Component data={data} params={params} search={search} />
82
+ }
@@ -0,0 +1,5 @@
1
+ export function parse() {
2
+ throw new Error('El storefront no admite estilos inline en HTML enriquecido.')
3
+ }
4
+
5
+ export default { parse }
@@ -0,0 +1,26 @@
1
+ import type { CreateApiOptions } from '@ecomiq/core'
2
+ import { createServerOnlyFn } from '@tanstack/react-start'
3
+
4
+ export interface ServerEcomiqConfig {
5
+ slug: string
6
+ api?: CreateApiOptions
7
+ }
8
+
9
+ const serverConfigKey = Symbol.for('@ecomiq/storefront/server-config')
10
+ const serverGlobal = globalThis as typeof globalThis & {
11
+ [key: symbol]: ServerEcomiqConfig | undefined
12
+ }
13
+
14
+ export const registerServerConfig = createServerOnlyFn((config: ServerEcomiqConfig): void => {
15
+ serverGlobal[serverConfigKey] = config
16
+ })
17
+
18
+ export const getServerConfig = createServerOnlyFn((): ServerEcomiqConfig => {
19
+ const registered = serverGlobal[serverConfigKey]
20
+ if (!registered) {
21
+ throw new Error(
22
+ 'Config server de Ecomiq no disponible. Ejecuta la app mediante `ecomiq dev|build`.',
23
+ )
24
+ }
25
+ return registered
26
+ })
package/src/server.ts ADDED
@@ -0,0 +1,279 @@
1
+ /**
2
+ * API storefront que solo corre en el servidor (TanStack Start RPC).
3
+ * En SSR ejecuta el handler localmente; en el cliente llama al Worker.
4
+ *
5
+ * ```ts
6
+ * import { definePage, serverApi } from '@ecomiq/storefront'
7
+ *
8
+ * export default definePage({
9
+ * loader: () => serverApi.getProducts({ page: 1, pageSize: 12 }),
10
+ * component: ({ data }) => …,
11
+ * })
12
+ * ```
13
+ *
14
+ * Nota: cada `createServerFn` debe vivir en el top-level del módulo
15
+ * (el compilador de Start asigna IDs estables por call-site).
16
+ * El cliente storefront es singleton → el cache de settings se reutiliza.
17
+ */
18
+ import { createServerFn } from '@tanstack/react-start'
19
+ import {
20
+ AddCartItemParamsSchema,
21
+ CartAddressesParamsSchema,
22
+ CartBuyerParamsSchema,
23
+ CartCheckoutParamsSchema,
24
+ CartCheckoutReadParamsSchema,
25
+ CartPromotionCodeParamsSchema,
26
+ CreateProductReviewParamsSchema,
27
+ GetCartParamsSchema,
28
+ GetDepartmentsParamsSchema,
29
+ GetDistrictsParamsSchema,
30
+ GetProvincesParamsSchema,
31
+ GetShippingOptionsParamsSchema,
32
+ ListProductReviewsParamsSchema,
33
+ ProductAutocompleteParamsSchema,
34
+ ProductDetailParamsSchema,
35
+ ProductsParamsSchema,
36
+ PutShippingMethodParamsSchema,
37
+ RelatedProductsParamsSchema,
38
+ RemoveCartItemParamsSchema,
39
+ UpdateCartItemQuantityParamsSchema,
40
+ type AddCartItemParams,
41
+ type CartAddressesParams,
42
+ type CartBuyerParams,
43
+ type CartCheckoutParams,
44
+ type CartCheckoutReadParams,
45
+ type CartPromotionCodeParams,
46
+ type CreateProductReviewParams,
47
+ type GetCartParams,
48
+ type GetDepartmentsParams,
49
+ type GetDistrictsParams,
50
+ type GetProvincesParams,
51
+ type GetShippingOptionsParams,
52
+ type ListProductReviewsParams,
53
+ type ProductAutocompleteParams,
54
+ type ProductDetailParams,
55
+ type ProductsParamsInput,
56
+ type PutShippingMethodParams,
57
+ type RelatedProductsParams,
58
+ type RemoveCartItemParams,
59
+ type UpdateCartItemQuantityParams,
60
+ type UploadPaymentReceiptParams,
61
+ } from '@ecomiq/core'
62
+ import { z } from 'zod'
63
+ import { getStorefrontClient } from './client'
64
+ import {
65
+ loadActiveCart,
66
+ submitCartItemQuantity,
67
+ submitCartItemRemoval,
68
+ submitProductToCart,
69
+ type AddProductToCartInput,
70
+ } from './cart-session'
71
+ import { submitProductReview } from './review-submission'
72
+
73
+ const getSettingsFn = createServerFn({ method: 'GET' }).handler(() =>
74
+ getStorefrontClient().getSettings(),
75
+ )
76
+
77
+ const getStoreIdFn = createServerFn({ method: 'GET' }).handler(() =>
78
+ getStorefrontClient().getStoreId(),
79
+ )
80
+
81
+ const getCollectionsFn = createServerFn({ method: 'GET' }).handler(() =>
82
+ getStorefrontClient().getCollections(),
83
+ )
84
+
85
+ const getCollectionBySlugFn = createServerFn({ method: 'GET' })
86
+ .validator(z.object({ slug: z.string().min(1) }))
87
+ .handler(({ data }) => getStorefrontClient().getCollectionBySlug(data.slug))
88
+
89
+ const getProductsFn = createServerFn({ method: 'GET' })
90
+ .validator(ProductsParamsSchema)
91
+ .handler(({ data }) => getStorefrontClient().getProducts(data))
92
+
93
+ const ProductBySlugServerParamsSchema = ProductDetailParamsSchema.extend({
94
+ slug: z.string().min(1),
95
+ })
96
+
97
+ const getProductBySlugFn = createServerFn({ method: 'GET' })
98
+ .validator(ProductBySlugServerParamsSchema)
99
+ .handler(({ data }) =>
100
+ getStorefrontClient().getProductBySlug(data.slug, { sellerId: data.sellerId }),
101
+ )
102
+
103
+ const getRelatedProductsFn = createServerFn({ method: 'GET' })
104
+ .validator(RelatedProductsParamsSchema)
105
+ .handler(({ data }) => getStorefrontClient().getRelatedProducts(data))
106
+
107
+ const autocompleteProductsFn = createServerFn({ method: 'GET' })
108
+ .validator(ProductAutocompleteParamsSchema)
109
+ .handler(({ data }) => getStorefrontClient().autocompleteProducts(data))
110
+
111
+ const listProductReviewsFn = createServerFn({ method: 'GET' })
112
+ .validator(ListProductReviewsParamsSchema)
113
+ .handler(({ data }) => getStorefrontClient().listProductReviews(data))
114
+
115
+ const submitProductReviewFn = createServerFn({ method: 'POST' })
116
+ .validator(CreateProductReviewParamsSchema)
117
+ .handler(({ data }) => submitProductReview(getStorefrontClient(), data))
118
+
119
+ const loadCartFn = createServerFn({ method: 'POST' })
120
+ .validator(GetCartParamsSchema)
121
+ .handler(({ data }) => loadActiveCart(getStorefrontClient(), data))
122
+
123
+ const setCartItemQuantityFn = createServerFn({ method: 'POST' })
124
+ .validator(UpdateCartItemQuantityParamsSchema)
125
+ .handler(({ data }) => submitCartItemQuantity(getStorefrontClient(), data))
126
+
127
+ const removeCartItemFn = createServerFn({ method: 'POST' })
128
+ .validator(RemoveCartItemParamsSchema)
129
+ .handler(({ data }) => submitCartItemRemoval(getStorefrontClient(), data))
130
+
131
+ const AddProductToCartServerSchema = z.object({
132
+ cartId: z.string().uuid().optional(),
133
+ currencyCode: z.string().length(3),
134
+ variantId: z.string().regex(/^[1-9]\d*$/),
135
+ quantity: z.number().int().positive(),
136
+ })
137
+
138
+ const addProductToCartFn = createServerFn({ method: 'POST' })
139
+ .validator(AddProductToCartServerSchema)
140
+ .handler(({ data }) => submitProductToCart(getStorefrontClient(), data))
141
+
142
+ const addCartItemFn = createServerFn({ method: 'POST' })
143
+ .validator(AddCartItemParamsSchema)
144
+ .handler(({ data }) => getStorefrontClient().addCartItem(data))
145
+
146
+ const getCartCheckoutFn = createServerFn({ method: 'GET' })
147
+ .validator(CartCheckoutReadParamsSchema)
148
+ .handler(({ data }) => getStorefrontClient().getCartCheckout(data))
149
+
150
+ const getShippingOptionsFn = createServerFn({ method: 'GET' })
151
+ .validator(GetShippingOptionsParamsSchema)
152
+ .handler(({ data }) => getStorefrontClient().getShippingOptions(data))
153
+
154
+ const putCartShippingMethodFn = createServerFn({ method: 'POST' })
155
+ .validator(PutShippingMethodParamsSchema)
156
+ .handler(({ data }) => getStorefrontClient().putCartShippingMethod(data))
157
+
158
+ const patchCartBuyerFn = createServerFn({ method: 'POST' })
159
+ .validator(CartBuyerParamsSchema)
160
+ .handler(({ data }) => getStorefrontClient().patchCartBuyer(data))
161
+
162
+ const patchCartAddressesFn = createServerFn({ method: 'POST' })
163
+ .validator(CartAddressesParamsSchema)
164
+ .handler(({ data }) => getStorefrontClient().patchCartAddresses(data))
165
+
166
+ const checkoutCartFn = createServerFn({ method: 'POST' })
167
+ .validator(CartCheckoutParamsSchema)
168
+ .handler(({ data }) => getStorefrontClient().checkoutCart(data))
169
+
170
+ const CartPaymentReceiptServerSchema = z.object({
171
+ cartId: z.string().uuid(),
172
+ fileBase64: z.string().min(1),
173
+ fileName: z.string().min(1),
174
+ mimeType: z.string().min(1).optional(),
175
+ })
176
+
177
+ const uploadPaymentReceiptFn = createServerFn({ method: 'POST' })
178
+ .validator(CartPaymentReceiptServerSchema)
179
+ .handler(({ data }) => {
180
+ const binary = Uint8Array.from(atob(data.fileBase64), (c) => c.charCodeAt(0))
181
+ const file = new Blob([binary], { type: data.mimeType ?? 'application/octet-stream' })
182
+ return getStorefrontClient().uploadPaymentReceipt({
183
+ cartId: data.cartId,
184
+ file,
185
+ fileName: data.fileName,
186
+ })
187
+ })
188
+
189
+ const addCartPromotionCodeFn = createServerFn({ method: 'POST' })
190
+ .validator(CartPromotionCodeParamsSchema)
191
+ .handler(({ data }) => getStorefrontClient().addCartPromotionCode(data))
192
+
193
+ const removeCartPromotionCodeFn = createServerFn({ method: 'POST' })
194
+ .validator(CartPromotionCodeParamsSchema)
195
+ .handler(({ data }) => getStorefrontClient().removeCartPromotionCode(data))
196
+
197
+ const getCountriesFn = createServerFn({ method: 'GET' }).handler(() =>
198
+ getStorefrontClient().getCountries(),
199
+ )
200
+
201
+ const getDepartmentsFn = createServerFn({ method: 'GET' })
202
+ .validator(GetDepartmentsParamsSchema)
203
+ .handler(({ data }) => getStorefrontClient().getDepartments(data))
204
+
205
+ const getProvincesFn = createServerFn({ method: 'GET' })
206
+ .validator(GetProvincesParamsSchema)
207
+ .handler(({ data }) => getStorefrontClient().getProvinces(data))
208
+
209
+ const getDistrictsFn = createServerFn({ method: 'GET' })
210
+ .validator(GetDistrictsParamsSchema)
211
+ .handler(({ data }) => getStorefrontClient().getDistricts(data))
212
+
213
+ /**
214
+ * Cliente storefront server-only. Misma superficie útil que `createClient()`,
215
+ * pero cada método es una server function reutilizable desde cualquier página.
216
+ */
217
+ export const serverApi = {
218
+ getSettings: () => getSettingsFn(),
219
+ getStoreId: () => getStoreIdFn(),
220
+ getCollections: () => getCollectionsFn(),
221
+ getCollectionBySlug: (slug: string) => getCollectionBySlugFn({ data: { slug } }),
222
+ getProducts: (
223
+ params: ProductsParamsInput = {},
224
+ options: { signal?: AbortSignal } = {},
225
+ ) => getProductsFn({ data: params, signal: options.signal }),
226
+ getProductBySlug: (slug: string, params: ProductDetailParams = {}) =>
227
+ getProductBySlugFn({ data: { slug, ...params } }),
228
+ getRelatedProducts: (params: RelatedProductsParams) =>
229
+ getRelatedProductsFn({ data: params }),
230
+ autocompleteProducts: (params: ProductAutocompleteParams) =>
231
+ autocompleteProductsFn({ data: params }),
232
+ listProductReviews: (params: ListProductReviewsParams) =>
233
+ listProductReviewsFn({ data: params }),
234
+ submitProductReview: (params: CreateProductReviewParams) =>
235
+ submitProductReviewFn({ data: params }),
236
+ loadCart: (params: GetCartParams) => loadCartFn({ data: params }),
237
+ setCartItemQuantity: (params: UpdateCartItemQuantityParams) =>
238
+ setCartItemQuantityFn({ data: params }),
239
+ removeCartItem: (params: RemoveCartItemParams) =>
240
+ removeCartItemFn({ data: params }),
241
+ addProductToCart: (params: AddProductToCartInput) =>
242
+ addProductToCartFn({ data: params }),
243
+ addCartItem: (params: AddCartItemParams) => addCartItemFn({ data: params }),
244
+ getCartCheckout: (params: CartCheckoutReadParams) =>
245
+ getCartCheckoutFn({ data: params }),
246
+ getShippingOptions: (params: GetShippingOptionsParams) =>
247
+ getShippingOptionsFn({ data: params }),
248
+ putCartShippingMethod: (params: PutShippingMethodParams) =>
249
+ putCartShippingMethodFn({ data: params }),
250
+ patchCartBuyer: (params: CartBuyerParams) => patchCartBuyerFn({ data: params }),
251
+ patchCartAddresses: (params: CartAddressesParams) =>
252
+ patchCartAddressesFn({ data: params }),
253
+ checkoutCart: (params: CartCheckoutParams) => checkoutCartFn({ data: params }),
254
+ uploadPaymentReceipt: async (params: UploadPaymentReceiptParams) => {
255
+ const buffer = await params.file.arrayBuffer()
256
+ const bytes = new Uint8Array(buffer)
257
+ let binary = ''
258
+ for (const byte of bytes) binary += String.fromCharCode(byte)
259
+ return uploadPaymentReceiptFn({
260
+ data: {
261
+ cartId: params.cartId,
262
+ fileBase64: btoa(binary),
263
+ fileName:
264
+ params.fileName ??
265
+ (params.file instanceof File ? params.file.name : 'receipt'),
266
+ mimeType: params.file.type || undefined,
267
+ },
268
+ })
269
+ },
270
+ addCartPromotionCode: (params: CartPromotionCodeParams) =>
271
+ addCartPromotionCodeFn({ data: params }),
272
+ removeCartPromotionCode: (params: CartPromotionCodeParams) =>
273
+ removeCartPromotionCodeFn({ data: params }),
274
+ getCountries: () => getCountriesFn(),
275
+ getDepartments: (params: GetDepartmentsParams) =>
276
+ getDepartmentsFn({ data: params }),
277
+ getProvinces: (params: GetProvincesParams) => getProvincesFn({ data: params }),
278
+ getDistricts: (params: GetDistrictsParams) => getDistrictsFn({ data: params }),
279
+ }
package/src/styles.css ADDED
@@ -0,0 +1,76 @@
1
+ /* Design system de Ecomiq — Tailwind v4 + tokens estilo shadcn.
2
+ * `source(...)` fija el scan para que client y SSR generen el mismo CSS/hash. */
3
+ @import 'tailwindcss' source('./');
4
+
5
+ /* Escanear los componentes UI del propio framework */
6
+ @source './ui';
7
+ @source './components';
8
+ @source './runtime';
9
+ @source './pages';
10
+ @source '../templates';
11
+
12
+ @theme inline {
13
+ --color-background: var(--background);
14
+ --color-foreground: var(--foreground);
15
+ --color-card: var(--card);
16
+ --color-card-foreground: var(--card-foreground);
17
+ --color-primary: var(--primary);
18
+ --color-primary-foreground: var(--primary-foreground);
19
+ --color-secondary: var(--secondary);
20
+ --color-secondary-foreground: var(--secondary-foreground);
21
+ --color-muted: var(--muted);
22
+ --color-muted-foreground: var(--muted-foreground);
23
+ --color-accent: var(--accent);
24
+ --color-accent-foreground: var(--accent-foreground);
25
+ --color-destructive: var(--destructive);
26
+ --color-destructive-foreground: var(--destructive-foreground);
27
+ --color-border: var(--border);
28
+ --color-input: var(--input);
29
+ --color-ring: var(--ring);
30
+ --radius-lg: var(--radius);
31
+ --radius-md: calc(var(--radius) - 2px);
32
+ --radius-sm: calc(var(--radius) - 4px);
33
+ }
34
+
35
+ :root {
36
+ --background: oklch(0.99 0.003 90);
37
+ --foreground: oklch(0.2 0.02 260);
38
+ --card: oklch(1 0 0);
39
+ --card-foreground: oklch(0.2 0.02 260);
40
+ --primary: oklch(0.45 0.15 165);
41
+ --primary-foreground: oklch(0.98 0.01 165);
42
+ --secondary: oklch(0.96 0.01 165);
43
+ --secondary-foreground: oklch(0.3 0.05 165);
44
+ --muted: oklch(0.96 0.005 90);
45
+ --muted-foreground: oklch(0.5 0.02 260);
46
+ --accent: oklch(0.94 0.03 165);
47
+ --accent-foreground: oklch(0.3 0.05 165);
48
+ --destructive: oklch(0.55 0.2 25);
49
+ --destructive-foreground: oklch(0.98 0.01 25);
50
+ --border: oklch(0.91 0.005 90);
51
+ --input: oklch(0.91 0.005 90);
52
+ --ring: oklch(0.45 0.15 165);
53
+ --radius: 0.625rem;
54
+ }
55
+
56
+ * {
57
+ border-color: var(--border);
58
+ }
59
+
60
+ ::selection {
61
+ background: var(--accent);
62
+ }
63
+
64
+ :focus-visible {
65
+ outline: 2px solid var(--ring);
66
+ outline-offset: 2px;
67
+ }
68
+
69
+ @media (prefers-reduced-motion: reduce) {
70
+ *,
71
+ *::before,
72
+ *::after {
73
+ animation-duration: 0.01ms !important;
74
+ transition-duration: 0.01ms !important;
75
+ }
76
+ }
@@ -0,0 +1,32 @@
1
+ import type { ComponentProps, ReactNode } from 'react'
2
+ import { cva, type VariantProps } from 'class-variance-authority'
3
+ import { cn } from './cn'
4
+
5
+ const badgeVariants = cva(
6
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors',
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: 'border-transparent bg-primary text-primary-foreground',
11
+ secondary: 'border-transparent bg-secondary text-secondary-foreground',
12
+ outline: 'text-foreground',
13
+ destructive: 'border-transparent bg-destructive text-destructive-foreground',
14
+ },
15
+ },
16
+ defaultVariants: { variant: 'default' },
17
+ },
18
+ )
19
+
20
+ export interface BadgeProps
21
+ extends Omit<ComponentProps<'span'>, 'children'>,
22
+ VariantProps<typeof badgeVariants> {
23
+ children?: ReactNode
24
+ }
25
+
26
+ export function Badge({ className, variant, children, ...props }: BadgeProps) {
27
+ return (
28
+ <span className={cn(badgeVariants({ variant }), className)} {...props}>
29
+ {children}
30
+ </span>
31
+ )
32
+ }
@@ -0,0 +1,31 @@
1
+ import type { ComponentProps } from 'react'
2
+ import { cva, type VariantProps } from 'class-variance-authority'
3
+ import { cn } from './cn'
4
+
5
+ export const buttonVariants = cva(
6
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0',
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
11
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
12
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
13
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
14
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15
+ },
16
+ size: {
17
+ default: 'h-9 px-4 py-2',
18
+ sm: 'h-8 rounded-md px-3 text-xs',
19
+ lg: 'h-10 rounded-md px-8',
20
+ icon: 'h-9 w-9',
21
+ },
22
+ },
23
+ defaultVariants: { variant: 'default', size: 'default' },
24
+ },
25
+ )
26
+
27
+ export interface ButtonProps extends ComponentProps<'button'>, VariantProps<typeof buttonVariants> {}
28
+
29
+ export function Button({ className, variant, size, type = 'button', ...props }: ButtonProps) {
30
+ return <button type={type} className={cn(buttonVariants({ variant, size }), className)} {...props} />
31
+ }