@ikas/code-components-mcp 0.28.0 → 0.30.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.
@@ -14,7 +14,7 @@
14
14
  "title": "Header Section",
15
15
  "description": "Site header with logo, navigation links, cart/account icons, and mobile menu",
16
16
  "files": {
17
- "index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n cartStore,\n customerStore,\n hasCustomer,\n Router,\n IkasNavigationLink,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function HeaderSection({\n logo,\n navigationLinks,\n announcementText,\n}: Props) {\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const itemCount = cartStore.cart?.orderLineItems.length ?? 0;\n const isLoggedIn = hasCustomer(customerStore) as unknown as boolean;\n\n return (\n <section className=\"header-section\">\n {announcementText && (\n <div className=\"header-announcement\">\n <span>{announcementText}</span>\n </div>\n )}\n <div className=\"header-main\">\n <div className=\"header-inner\">\n <button\n className=\"header-hamburger\"\n onClick={() => setMobileMenuOpen(true)}\n aria-label=\"Open menu\"\n >\n <span /><span /><span />\n </button>\n <a className=\"header-logo\" href=\"/\">\n {logo ? (\n <img src={getDefaultSrc(logo)} alt={logo.altText || \"Logo\"} className=\"header-logo-img\" />\n ) : (\n <span className=\"header-logo-text\">Store</span>\n )}\n </a>\n <nav className=\"header-nav\">\n {navigationLinks?.map((link: IkasNavigationLink, i: number) => (\n <a key={i} href={link.href} className=\"header-nav-link\">\n {link.label}\n </a>\n ))}\n </nav>\n <div className=\"header-icons\">\n <button\n className=\"header-icon-btn\"\n onClick={() => Router.navigateToPage(isLoggedIn ? \"ACCOUNT\" : \"LOGIN\")}\n >\n Account\n </button>\n <button className=\"header-icon-btn\" onClick={() => Router.navigateToPage(\"CART\")}>\n Cart{itemCount > 0 && <span className=\"header-cart-badge\">{itemCount}</span>}\n </button>\n </div>\n </div>\n </div>\n {mobileMenuOpen && (\n <div className=\"header-mobile-overlay\">\n <div className=\"header-mobile-backdrop\" onClick={() => setMobileMenuOpen(false)} />\n <div className=\"header-mobile-menu\">\n <button className=\"header-mobile-close\" onClick={() => setMobileMenuOpen(false)}>&times;</button>\n <nav className=\"header-mobile-nav\">\n {navigationLinks?.map((link: IkasNavigationLink, i: number) => (\n <a key={i} href={link.href} className=\"header-mobile-link\" onClick={() => setMobileMenuOpen(false)}>\n {link.label}\n </a>\n ))}\n </nav>\n </div>\n </div>\n )}\n </section>\n );\n}\n",
17
+ "index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n cartStore,\n customerStore,\n hasCustomer,\n Router,\n IkasNavigationLink,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function HeaderSection({\n logo,\n navigationLinks,\n announcementText,\n}: Props) {\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const itemCount = cartStore.cart?.orderLineItems.length ?? 0;\n const isLoggedIn = hasCustomer(customerStore) as unknown as boolean;\n\n return (\n <section className=\"header-section\">\n {announcementText && (\n <div className=\"header-announcement\">\n <span>{announcementText}</span>\n </div>\n )}\n <div className=\"header-main\">\n <div className=\"header-inner\">\n <button\n className=\"header-hamburger\"\n onClick={() => setMobileMenuOpen(true)}\n aria-label=\"Open menu\"\n >\n <span /><span /><span />\n </button>\n <a className=\"header-logo\" href=\"/\">\n {logo ? (\n <img src={getDefaultSrc(logo)} alt={logo.altText || \"Logo\"} className=\"header-logo-img\" />\n ) : (\n <span className=\"header-logo-text\">Store</span>\n )}\n </a>\n <nav className=\"header-nav\">\n {navigationLinks?.map((link: IkasNavigationLink, i: number) => (\n <a key={i} href={link.href} className=\"header-nav-link\">\n {link.label}\n </a>\n ))}\n </nav>\n <div className=\"header-icons\">\n <button\n className=\"header-icon-btn\"\n onClick={() => Router.navigateToPage(isLoggedIn ? \"ACCOUNT\" : \"LOGIN\")}\n >\n Account\n </button>\n <button className=\"header-icon-btn\" onClick={() => Router.navigateToPage(\"CART\")}>\n Cart{itemCount > 0 && <span className=\"header-cart-badge\">{itemCount}</span>}\n </button>\n </div>\n </div>\n </div>\n {mobileMenuOpen && (\n <div className=\"header-mobile-overlay\">\n <div className=\"header-mobile-backdrop\" onClick={() => setMobileMenuOpen(false)} />\n <div className=\"header-mobile-menu\">\n <button className=\"header-mobile-close\" onClick={() => setMobileMenuOpen(false)}>&times;</button>\n <nav className=\"header-mobile-nav\">\n {navigationLinks?.map((link: IkasNavigationLink, i: number) => (\n <a key={i} href={link.href} className=\"header-mobile-link\" onClick={() => setMobileMenuOpen(false)}>\n {link.label}\n </a>\n ))}\n </nav>\n </div>\n </div>\n )}\n </section>\n );\n}\n",
18
18
  "types.ts": "import { IkasNavigationLink, IkasImage } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n logo?: IkasImage | null;\n navigationLinks?: IkasNavigationLink[];\n announcementText?: string;\n}\n",
19
19
  "styles.css": ".header-section {\n width: 100%;\n position: sticky;\n top: 0;\n z-index: 100;\n background: #fff;\n}\n\n.header-announcement {\n text-align: center;\n padding: 8px 16px;\n font-size: 13px;\n background: #111;\n color: #fff;\n}\n\n.header-inner {\n max-width: 1200px;\n margin: 0 auto;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 24px;\n gap: 24px;\n}\n\n.header-logo { text-decoration: none; flex-shrink: 0; }\n.header-logo-img { height: 40px; width: auto; }\n.header-logo-text { font-size: 22px; font-weight: 700; color: #111; }\n\n.header-nav { display: flex; gap: 24px; flex: 1; justify-content: center; }\n.header-nav-link { font-size: 14px; font-weight: 500; color: #333; text-decoration: none; }\n\n.header-icons { display: flex; gap: 12px; align-items: center; }\n.header-icon-btn { background: none; border: none; cursor: pointer; color: #333; padding: 4px; position: relative; }\n.header-cart-badge { position: absolute; top: -4px; right: -6px; background: #111; color: #fff; font-size: 10px; width: 18px; height: 18px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }\n\n.header-hamburger { display: none; flex-direction: column; gap: 4px; background: none; border: none; cursor: pointer; }\n.header-hamburger span { display: block; width: 20px; height: 2px; background: #333; }\n\n.header-mobile-overlay { position: fixed; inset: 0; z-index: 200; }\n.header-mobile-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.4); }\n.header-mobile-menu { position: absolute; top: 0; left: 0; bottom: 0; width: 280px; background: #fff; padding: 24px; overflow-y: auto; }\n.header-mobile-close { font-size: 28px; background: none; border: none; cursor: pointer; margin-bottom: 16px; }\n.header-mobile-nav { display: flex; flex-direction: column; gap: 16px; }\n.header-mobile-link { font-size: 16px; font-weight: 500; color: #333; text-decoration: none; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }\n\n@media (max-width: 768px) {\n .header-hamburger { display: flex; }\n .header-nav { display: none; }\n}\n",
20
20
  "ikas-config-snippet.json": "{\n \"id\": \"header\",\n \"name\": \"Header\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"logo\", \"displayName\": \"Logo\", \"type\": \"IMAGE\" },\n { \"name\": \"navigationLinks\", \"displayName\": \"Navigation Links\", \"type\": \"LIST_OF_LINK\" },\n { \"name\": \"announcementText\", \"displayName\": \"Announcement Text\", \"type\": \"TEXT\" }\n ]\n}\n"
@@ -34,7 +34,7 @@
34
34
  "title": "Product Detail Section",
35
35
  "description": "Product page with image gallery, variant selection, pricing, add-to-cart, and favorites",
36
36
  "files": {
37
- "index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n getSelectedProductVariant,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n hasProductVariantStock,\n hasProductStock,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n isAddToCartEnabled,\n addItemToCart,\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n getProductVariantMainImage,\n getDefaultSrc,\n createMediaSrcset,\n IkasImage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductDetail({\n product,\n addToCartButtonText = \"Add to Cart\",\n}: Props) {\n const [isAddingToCart, setIsAddingToCart] = useState(false);\n\n if (!product) return null;\n\n const variant = getSelectedProductVariant(product) as any;\n const variantTypes = getDisplayedProductVariantTypes(product);\n const inStock = hasProductStock(product) as unknown as boolean;\n const variantInStock = hasProductVariantStock(variant) as unknown as boolean;\n const canAddToCart = isAddToCartEnabled(product) as unknown as boolean;\n const hasDiscount = hasProductVariantDiscount(variant) as unknown as boolean;\n const finalPrice = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const originalPrice = hasDiscount ? (getProductVariantFormattedSellPrice(variant) as unknown as string) : null;\n const mainProductImage = getProductVariantMainImage(variant);\n const mainImage = mainProductImage?.image;\n const images: IkasImage[] = variant?.images?.length\n ? variant.images.map((pi: any) => pi.image).filter((img: any): img is IkasImage => img != null)\n : mainImage ? [mainImage] : [];\n const isFav = isFavoriteIkasProduct(product);\n\n const handleAddToCart = async () => {\n if (!canAddToCart || isAddingToCart) return;\n setIsAddingToCart(true);\n try {\n await addItemToCart(variant, product, 1);\n } finally {\n setIsAddingToCart(false);\n }\n };\n\n const toggleFav = async () => {\n if (isFav) await removeIkasProductFromFavorites(product);\n else await addIkasProductToFavorites(product);\n };\n\n return (\n <section className=\"product-detail\">\n <div className=\"product-detail-inner\">\n <div className=\"product-gallery\">\n {images[0] && <img className=\"product-main-image\" src={getDefaultSrc(images[0])} srcSet={createMediaSrcset(images[0])} sizes=\"(max-width: 768px) 100vw, 50vw\" alt={product.name} />}\n </div>\n <div className=\"product-info\">\n <h1 className=\"product-name\">{product.name}</h1>\n <div className=\"product-pricing\">\n <span className=\"product-final-price\">{finalPrice}</span>\n {originalPrice && <span className=\"product-original-price\">{originalPrice}</span>}\n </div>\n {variantTypes.length > 0 && (\n <div className=\"product-variants\">\n {variantTypes.map((vt) => (\n <div key={vt.variantType.id} className=\"variant-group\">\n <span className=\"variant-group-label\">{vt.variantType.name}</span>\n <div className=\"variant-options\">\n {vt.displayedVariantValues.map((dvv) => (\n <button\n key={dvv.variantValue.id}\n className={`variant-option-btn ${dvv.isSelected ? \"selected\" : \"\"}`}\n disabled={!dvv.hasStock}\n onClick={() => selectVariantValue(product, dvv.variantValue)}\n >\n {dvv.variantValue.name}\n </button>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n {!inStock && <span className=\"out-of-stock-notice\">Out of Stock</span>}\n <div className=\"product-actions\">\n <button className=\"add-to-cart-btn\" disabled={!canAddToCart || isAddingToCart} onClick={handleAddToCart}>\n {isAddingToCart ? \"Adding...\" : !variantInStock ? \"Out of Stock\" : addToCartButtonText}\n </button>\n <button className={`favorite-btn ${isFav ? \"is-favorite\" : \"\"}`} onClick={toggleFav}>\n {isFav ? \"\\u2665\" : \"\\u2661\"}\n </button>\n </div>\n {product.description && (\n <div className=\"product-description\" dangerouslySetInnerHTML={{ __html: product.description }} />\n )}\n </div>\n </div>\n </section>\n );\n}\n",
37
+ "index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n getSelectedProductVariant,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n hasProductVariantStock,\n hasProductStock,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n isAddToCartEnabled,\n addItemToCart,\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n getProductVariantMainImage,\n getDefaultSrc,\n createMediaSrcset,\n IkasImage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function ProductDetail({\n product,\n addToCartButtonText = \"Add to Cart\",\n}: Props) {\n const [isAddingToCart, setIsAddingToCart] = useState(false);\n\n if (!product) return null;\n\n const variant = getSelectedProductVariant(product) as any;\n const variantTypes = getDisplayedProductVariantTypes(product);\n const inStock = hasProductStock(product) as unknown as boolean;\n const variantInStock = hasProductVariantStock(variant) as unknown as boolean;\n const canAddToCart = isAddToCartEnabled(product) as unknown as boolean;\n const hasDiscount = hasProductVariantDiscount(variant) as unknown as boolean;\n const finalPrice = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const originalPrice = hasDiscount ? (getProductVariantFormattedSellPrice(variant) as unknown as string) : null;\n const mainProductImage = getProductVariantMainImage(variant);\n const mainImage = mainProductImage?.image;\n const images: IkasImage[] = variant?.images?.length\n ? variant.images.map((pi: any) => pi.image).filter((img: any): img is IkasImage => img != null)\n : mainImage ? [mainImage] : [];\n const isFav = isFavoriteIkasProduct(product);\n\n const handleAddToCart = async () => {\n if (!canAddToCart || isAddingToCart) return;\n setIsAddingToCart(true);\n try {\n await addItemToCart(variant, product, 1);\n } finally {\n setIsAddingToCart(false);\n }\n };\n\n const toggleFav = async () => {\n if (isFav) await removeIkasProductFromFavorites(product);\n else await addIkasProductToFavorites(product);\n };\n\n return (\n <section className=\"product-detail\">\n <div className=\"product-detail-inner\">\n <div className=\"product-gallery\">\n {images[0] && <img className=\"product-main-image\" src={getDefaultSrc(images[0])} srcSet={createMediaSrcset(images[0])} sizes=\"(max-width: 768px) 100vw, 50vw\" alt={product.name} />}\n </div>\n <div className=\"product-info\">\n <h1 className=\"product-name\">{product.name}</h1>\n <div className=\"product-pricing\">\n <span className=\"product-final-price\">{finalPrice}</span>\n {originalPrice && <span className=\"product-original-price\">{originalPrice}</span>}\n </div>\n {variantTypes.length > 0 && (\n <div className=\"product-variants\">\n {variantTypes.map((vt) => (\n <div key={vt.variantType.id} className=\"variant-group\">\n <span className=\"variant-group-label\">{vt.variantType.name}</span>\n <div className=\"variant-options\">\n {vt.displayedVariantValues.map((dvv) => (\n <button\n key={dvv.variantValue.id}\n className={`variant-option-btn ${dvv.isSelected ? \"selected\" : \"\"}`}\n disabled={!dvv.hasStock}\n onClick={() => selectVariantValue(product, dvv.variantValue)}\n >\n {dvv.variantValue.name}\n </button>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n {!inStock && <span className=\"out-of-stock-notice\">Out of Stock</span>}\n <div className=\"product-actions\">\n <button className=\"add-to-cart-btn\" disabled={!canAddToCart || isAddingToCart} onClick={handleAddToCart}>\n {isAddingToCart ? \"Adding...\" : !variantInStock ? \"Out of Stock\" : addToCartButtonText}\n </button>\n <button className={`favorite-btn ${isFav ? \"is-favorite\" : \"\"}`} onClick={toggleFav}>\n {isFav ? \"\\u2665\" : \"\\u2661\"}\n </button>\n </div>\n {product.description && (\n <div className=\"product-description\" dangerouslySetInnerHTML={{ __html: product.description }} />\n )}\n </div>\n </div>\n </section>\n );\n}\n",
38
38
  "types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n addToCartButtonText?: string;\n}\n",
39
39
  "styles.css": ".product-detail {\n width: 100%;\n padding: 40px 24px;\n}\n\n.product-detail-inner {\n max-width: 1200px;\n margin: 0 auto;\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 48px;\n}\n\n.product-main-image {\n width: 100%;\n aspect-ratio: 1;\n object-fit: cover;\n border-radius: 8px;\n background: #f5f5f5;\n}\n\n.product-name { font-size: 28px; font-weight: 700; color: #111; margin: 0 0 16px 0; }\n\n.product-pricing { display: flex; align-items: center; gap: 12px; margin-bottom: 24px; }\n.product-final-price { font-size: 22px; font-weight: 700; color: #111; }\n.product-original-price { font-size: 16px; color: #999; text-decoration: line-through; }\n\n.variant-group { margin-bottom: 16px; }\n.variant-group-label { font-size: 14px; font-weight: 600; color: #333; display: block; margin-bottom: 8px; }\n.variant-options { display: flex; gap: 8px; flex-wrap: wrap; }\n.variant-option-btn { padding: 8px 16px; border: 1.5px solid #ddd; border-radius: 6px; background: #fff; cursor: pointer; font-size: 14px; }\n.variant-option-btn.selected { border-color: #111; font-weight: 600; }\n.variant-option-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n\n.out-of-stock-notice { color: #e53935; font-size: 14px; font-weight: 600; }\n\n.product-actions { display: flex; gap: 12px; margin: 24px 0; }\n.add-to-cart-btn { flex: 1; padding: 14px 24px; font-size: 16px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; }\n.add-to-cart-btn:disabled { background: #ccc; cursor: not-allowed; }\n.favorite-btn { width: 48px; height: 48px; border: 1.5px solid #ddd; border-radius: 8px; background: #fff; cursor: pointer; font-size: 20px; }\n.favorite-btn.is-favorite { color: #e53935; border-color: #e53935; }\n\n.product-description { font-size: 15px; line-height: 1.7; color: #555; margin-top: 24px; }\n\n@media (max-width: 768px) {\n .product-detail-inner { grid-template-columns: 1fr; gap: 24px; }\n}\n",
40
40
  "ikas-config-snippet.json": "{\n \"id\": \"product-detail\",\n \"name\": \"Product Detail\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"product\", \"displayName\": \"Product\", \"type\": \"PRODUCT\", \"required\": true },\n { \"name\": \"addToCartButtonText\", \"displayName\": \"Add to Cart Button Text\", \"type\": \"TEXT\", \"defaultValue\": \"Add to Cart\" }\n ]\n}\n"
@@ -44,7 +44,7 @@
44
44
  "title": "Product List Section",
45
45
  "description": "Product grid with filters, sorting, and pagination for category/search pages",
46
46
  "files": {
47
- "index.tsx": "import {\n IkasImage,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getFilterDisplayedValues,\n handleFilterValueClick,\n getProductListFilterCategories,\n getProductListSortOptions,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductListSection({\n productList,\n title = \"Products\",\n showFilters = true,\n}: Props) {\n if (!productList) return null;\n\n const products = productList.products ?? [];\n const filterCategories = getProductListFilterCategories(productList);\n const sortOptions = getProductListSortOptions(productList);\n const hasNext = hasProductListNextPage(productList);\n const hasPrev = hasProductListPrevPage(productList);\n\n return (\n <section className=\"product-list-section\">\n <div className=\"product-list-inner\">\n <div className=\"product-list-header\">\n <h1 className=\"product-list-title\">{title}</h1>\n {sortOptions.length > 0 && (\n <select className=\"product-list-sort\" value={productList.sort} onChange={(e) => { productList.sort = (e.target as HTMLSelectElement).value; }}>\n {sortOptions.map((opt) => <option key={opt.value} value={opt.value}>{opt.label}</option>)}\n </select>\n )}\n </div>\n <div className=\"product-list-layout\">\n {showFilters && filterCategories.length > 0 && (\n <aside className=\"product-list-filters\">\n {filterCategories.map((cat) => {\n const values = getFilterDisplayedValues(cat);\n return (\n <div key={cat.name} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{cat.name}</h3>\n {values.map((fv) => (\n <label key={fv.name} className=\"filter-value\">\n <input type=\"checkbox\" checked={fv.isSelected} onChange={() => handleFilterValueClick(productList, cat, fv)} />\n <span>{fv.name}</span>\n </label>\n ))}\n </div>\n );\n })}\n </aside>\n )}\n <div className=\"product-grid\">\n {products.length === 0 && <p className=\"product-grid-empty\">No products found.</p>}\n {products.map((product) => {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n return (\n <a key={product.id} href={getSelectedProductVariantHref(product)} className=\"product-card\">\n {image && <img src={getDefaultSrc(image)} alt={product.name} className=\"product-card-image\" />}\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </a>\n );\n })}\n </div>\n </div>\n {(hasPrev || hasNext) && (\n <div className=\"product-list-pagination\">\n <button disabled={!hasPrev} onClick={() => getProductListPrevPage(productList)}>Previous</button>\n <button disabled={!hasNext} onClick={() => getProductListNextPage(productList)}>Next</button>\n </div>\n )}\n </div>\n </section>\n );\n}\n",
47
+ "index.tsx": "import {\n IkasImage,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getFilterDisplayedValues,\n handleFilterValueClick,\n getProductListFilterCategories,\n getProductListSortOptions,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function ProductListSection({\n productList,\n title = \"Products\",\n showFilters = true,\n}: Props) {\n if (!productList) return null;\n\n const products = productList.products ?? [];\n const filterCategories = getProductListFilterCategories(productList);\n const sortOptions = getProductListSortOptions(productList);\n const hasNext = hasProductListNextPage(productList);\n const hasPrev = hasProductListPrevPage(productList);\n\n return (\n <section className=\"product-list-section\">\n <div className=\"product-list-inner\">\n <div className=\"product-list-header\">\n <h1 className=\"product-list-title\">{title}</h1>\n {sortOptions.length > 0 && (\n <select className=\"product-list-sort\" value={productList.sort} onChange={(e) => { productList.sort = (e.target as HTMLSelectElement).value; }}>\n {sortOptions.map((opt) => <option key={opt.value} value={opt.value}>{opt.label}</option>)}\n </select>\n )}\n </div>\n <div className=\"product-list-layout\">\n {showFilters && filterCategories.length > 0 && (\n <aside className=\"product-list-filters\">\n {filterCategories.map((cat) => {\n const values = getFilterDisplayedValues(cat);\n return (\n <div key={cat.name} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{cat.name}</h3>\n {values.map((fv) => (\n <label key={fv.name} className=\"filter-value\">\n <input type=\"checkbox\" checked={fv.isSelected} onChange={() => handleFilterValueClick(productList, cat, fv)} />\n <span>{fv.name}</span>\n </label>\n ))}\n </div>\n );\n })}\n </aside>\n )}\n <div className=\"product-grid\">\n {products.length === 0 && <p className=\"product-grid-empty\">No products found.</p>}\n {products.map((product) => {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n return (\n <a key={product.id} href={getSelectedProductVariantHref(product)} className=\"product-card\">\n {image && <img src={getDefaultSrc(image)} alt={product.name} className=\"product-card-image\" />}\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </a>\n );\n })}\n </div>\n </div>\n {(hasPrev || hasNext) && (\n <div className=\"product-list-pagination\">\n <button disabled={!hasPrev} onClick={() => getProductListPrevPage(productList)}>Previous</button>\n <button disabled={!hasNext} onClick={() => getProductListNextPage(productList)}>Next</button>\n </div>\n )}\n </div>\n </section>\n );\n}\n",
48
48
  "types.ts": "import { IkasProductList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n productList: IkasProductList;\n title?: string;\n showFilters?: boolean;\n}\n",
49
49
  "styles.css": ".product-list-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.product-list-inner {\n max-width: 1200px;\n margin: 0 auto;\n}\n\n.product-list-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 24px;\n}\n\n.product-list-title { font-size: 24px; font-weight: 700; color: #111; margin: 0; }\n.product-list-sort { padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }\n\n.product-list-layout {\n display: flex;\n gap: 32px;\n}\n\n.product-list-filters { width: 220px; flex-shrink: 0; }\n.filter-group { margin-bottom: 24px; }\n.filter-group-title { font-size: 14px; font-weight: 600; margin: 0 0 12px 0; }\n.filter-value { display: flex; align-items: center; gap: 8px; font-size: 14px; color: #555; cursor: pointer; padding: 4px 0; }\n\n.product-grid {\n flex: 1;\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 24px;\n}\n\n.product-card { text-decoration: none; color: inherit; }\n.product-card-image { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 8px; background: #f5f5f5; }\n.product-card-name { font-size: 14px; font-weight: 500; color: #111; margin: 10px 0 4px; }\n.product-card-price { font-size: 14px; font-weight: 600; color: #111; }\n\n.product-grid-empty { font-size: 16px; color: #666; grid-column: 1 / -1; text-align: center; padding: 48px 0; }\n\n.product-list-pagination { display: flex; justify-content: center; gap: 12px; margin-top: 32px; }\n.product-list-pagination button { padding: 10px 20px; border: 1px solid #ddd; border-radius: 6px; background: #fff; cursor: pointer; font-size: 14px; }\n.product-list-pagination button:disabled { opacity: 0.4; cursor: not-allowed; }\n\n@media (max-width: 768px) {\n .product-list-filters { display: none; }\n .product-grid { grid-template-columns: repeat(2, 1fr); gap: 16px; }\n}\n",
50
50
  "ikas-config-snippet.json": "{\n \"id\": \"product-list\",\n \"name\": \"Product List\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"productList\", \"displayName\": \"Product List\", \"type\": \"PRODUCT_LIST\", \"required\": true },\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"Products\" },\n { \"name\": \"showFilters\", \"displayName\": \"Show Filters\", \"type\": \"BOOLEAN\", \"defaultValue\": true }\n ]\n}\n"
@@ -54,7 +54,7 @@
54
54
  "title": "Cart Section",
55
55
  "description": "Shopping cart with line items, quantity controls, totals, and checkout button",
56
56
  "files": {
57
- "index.tsx": "import {\n cartStore,\n changeItemQuantity,\n removeItem,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getOrderLineItemFormattedFinalPrice,\n getOrderLineItemFormattedUnitPrice,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n Router,\n IkasOrderLineItem,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function CartSection({\n emptyCartMessage = \"Your cart is empty\",\n}: Props) {\n const cart = cartStore.cart;\n const lineItems = cart?.orderLineItems ?? [];\n\n if (lineItems.length === 0) {\n return (\n <section className=\"cart-section\">\n <div className=\"cart-inner\">\n <p className=\"cart-empty\">{emptyCartMessage}</p>\n <button className=\"cart-continue-btn\" onClick={() => Router.navigate(\"/\")}>Continue Shopping</button>\n </div>\n </section>\n );\n }\n\n const handleQty = async (item: IkasOrderLineItem, delta: number) => {\n const newQty = item.quantity + delta;\n if (newQty < 1) return;\n await changeItemQuantity(item, newQty);\n };\n\n return (\n <section className=\"cart-section\">\n <div className=\"cart-inner\">\n <h1 className=\"cart-title\">Shopping Cart ({lineItems.length})</h1>\n <div className=\"cart-items\">\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n return (\n <div key={item.id} className=\"cart-item\">\n {image && <img className=\"cart-item-image\" src={getDefaultSrc(image)} alt={item.variant?.name || \"Product\"} />}\n <div className=\"cart-item-info\">\n <span className=\"cart-item-name\">{item.variant?.name}</span>\n <span className=\"cart-item-unit-price\">{getOrderLineItemFormattedUnitPrice(item)}</span>\n </div>\n <div className=\"cart-item-quantity\">\n <button onClick={() => handleQty(item, -1)}>-</button>\n <span>{item.quantity}</span>\n <button onClick={() => handleQty(item, 1)}>+</button>\n </div>\n <span className=\"cart-item-total\">{getOrderLineItemFormattedFinalPrice(item)}</span>\n <button className=\"cart-item-remove\" onClick={() => removeItem(item)}>Remove</button>\n </div>\n );\n })}\n </div>\n <div className=\"cart-summary\">\n <div className=\"cart-summary-row\">\n <span>Subtotal</span>\n <span>{getIkasOrderFormattedTotalPrice(cart!)}</span>\n </div>\n <div className=\"cart-summary-row cart-summary-total\">\n <span>Total</span>\n <span>{getIkasOrderFormattedTotalFinalPrice(cart!)}</span>\n </div>\n <button className=\"cart-checkout-btn\" onClick={() => Router.navigateToPage(\"CHECKOUT\")}>Proceed to Checkout</button>\n </div>\n </div>\n </section>\n );\n}\n",
57
+ "index.tsx": "import {\n cartStore,\n changeItemQuantity,\n removeItem,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getOrderLineItemFormattedFinalPrice,\n getOrderLineItemFormattedUnitPrice,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n Router,\n IkasOrderLineItem,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function CartSection({\n emptyCartMessage = \"Your cart is empty\",\n}: Props) {\n const cart = cartStore.cart;\n const lineItems = cart?.orderLineItems ?? [];\n\n if (lineItems.length === 0) {\n return (\n <section className=\"cart-section\">\n <div className=\"cart-inner\">\n <p className=\"cart-empty\">{emptyCartMessage}</p>\n <button className=\"cart-continue-btn\" onClick={() => Router.navigate(\"/\")}>Continue Shopping</button>\n </div>\n </section>\n );\n }\n\n const handleQty = async (item: IkasOrderLineItem, delta: number) => {\n const newQty = item.quantity + delta;\n if (newQty < 1) return;\n await changeItemQuantity(item, newQty);\n };\n\n return (\n <section className=\"cart-section\">\n <div className=\"cart-inner\">\n <h1 className=\"cart-title\">Shopping Cart ({lineItems.length})</h1>\n <div className=\"cart-items\">\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n return (\n <div key={item.id} className=\"cart-item\">\n {image && <img className=\"cart-item-image\" src={getDefaultSrc(image)} alt={item.variant?.name || \"Product\"} />}\n <div className=\"cart-item-info\">\n <span className=\"cart-item-name\">{item.variant?.name}</span>\n <span className=\"cart-item-unit-price\">{getOrderLineItemFormattedUnitPrice(item)}</span>\n </div>\n <div className=\"cart-item-quantity\">\n <button onClick={() => handleQty(item, -1)}>-</button>\n <span>{item.quantity}</span>\n <button onClick={() => handleQty(item, 1)}>+</button>\n </div>\n <span className=\"cart-item-total\">{getOrderLineItemFormattedFinalPrice(item)}</span>\n <button className=\"cart-item-remove\" onClick={() => removeItem(item)}>Remove</button>\n </div>\n );\n })}\n </div>\n <div className=\"cart-summary\">\n <div className=\"cart-summary-row\">\n <span>Subtotal</span>\n <span>{getIkasOrderFormattedTotalPrice(cart!)}</span>\n </div>\n <div className=\"cart-summary-row cart-summary-total\">\n <span>Total</span>\n <span>{getIkasOrderFormattedTotalFinalPrice(cart!)}</span>\n </div>\n <button className=\"cart-checkout-btn\" onClick={() => Router.navigateToPage(\"CHECKOUT\")}>Proceed to Checkout</button>\n </div>\n </div>\n </section>\n );\n}\n",
58
58
  "types.ts": "export interface Props {\n emptyCartMessage?: string;\n}\n",
59
59
  "styles.css": ".cart-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.cart-inner {\n max-width: 960px;\n margin: 0 auto;\n}\n\n.cart-title { font-size: 24px; font-weight: 700; color: #111; margin: 0 0 24px 0; }\n.cart-empty { font-size: 16px; color: #666; text-align: center; padding: 48px 0; }\n.cart-continue-btn { display: block; margin: 0 auto; padding: 12px 24px; font-size: 14px; font-weight: 600; color: #111; background: #fff; border: 1.5px solid #111; border-radius: 8px; cursor: pointer; }\n\n.cart-items { display: flex; flex-direction: column; gap: 16px; margin-bottom: 32px; }\n.cart-item { display: flex; align-items: center; gap: 16px; padding: 16px; border: 1px solid #eee; border-radius: 8px; }\n.cart-item-image { width: 80px; height: 80px; object-fit: cover; border-radius: 6px; background: #f5f5f5; }\n.cart-item-info { flex: 1; display: flex; flex-direction: column; gap: 4px; }\n.cart-item-name { font-size: 14px; font-weight: 600; color: #111; }\n.cart-item-unit-price { font-size: 13px; color: #666; }\n.cart-item-quantity { display: flex; align-items: center; gap: 8px; }\n.cart-item-quantity button { width: 32px; height: 32px; border: 1px solid #ddd; border-radius: 4px; background: #fff; cursor: pointer; }\n.cart-item-total { font-size: 15px; font-weight: 600; color: #111; min-width: 80px; text-align: right; }\n.cart-item-remove { padding: 4px 8px; font-size: 12px; color: #e53935; background: none; border: none; cursor: pointer; }\n\n.cart-summary { border-top: 1px solid #eee; padding-top: 24px; display: flex; flex-direction: column; gap: 12px; align-items: flex-end; }\n.cart-summary-row { display: flex; justify-content: space-between; width: 280px; font-size: 14px; color: #555; }\n.cart-summary-total { font-size: 18px; font-weight: 700; color: #111; }\n.cart-checkout-btn { width: 280px; padding: 14px 24px; font-size: 16px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; margin-top: 8px; }\n\n@media (max-width: 768px) {\n .cart-item { flex-wrap: wrap; }\n .cart-summary { align-items: stretch; }\n .cart-summary-row, .cart-checkout-btn { width: 100%; }\n}\n",
60
60
  "ikas-config-snippet.json": "{\n \"id\": \"cart\",\n \"name\": \"Cart\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"emptyCartMessage\", \"displayName\": \"Empty Cart Message\", \"type\": \"TEXT\", \"defaultValue\": \"Your cart is empty\" }\n ]\n}\n"
@@ -64,7 +64,7 @@
64
64
  "title": "Login Section",
65
65
  "description": "Customer login form with email/password fields, forgot password link, and register link",
66
66
  "files": {
67
- "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getLoginForm,\n initLoginForm,\n setLoginFormEmail,\n setLoginFormPassword,\n submitLoginForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function LoginSection({\n redirectAfterLogin = \"/account\",\n}: Props) {\n const loginForm = getLoginForm(customerStore);\n\n useEffect(() => {\n initLoginForm(loginForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitLoginForm(loginForm);\n if (success) Router.navigate(redirectAfterLogin);\n };\n\n return (\n <section className=\"login-section\">\n <div className=\"login-inner\">\n <h1 className=\"login-title\">Sign In</h1>\n {loginForm.isFailure && loginForm.responseMessage && (\n <div className=\"login-error-banner\">{loginForm.responseMessage}</div>\n )}\n <form className=\"login-form\" onSubmit={handleSubmit}>\n <div className=\"login-field\">\n <label className=\"login-label\">{loginForm.email.label}</label>\n <input\n className={`login-input ${loginForm.email.hasError ? \"has-error\" : \"\"}`}\n type=\"email\"\n placeholder={loginForm.email.placeholder}\n value={loginForm.email.value}\n onInput={(e) => setLoginFormEmail(loginForm, (e.target as HTMLInputElement).value)}\n />\n {loginForm.email.hasError && loginForm.email.message && (\n <span className=\"login-field-error\">{loginForm.email.message}</span>\n )}\n </div>\n <div className=\"login-field\">\n <label className=\"login-label\">{loginForm.password.label}</label>\n <input\n className={`login-input ${loginForm.password.hasError ? \"has-error\" : \"\"}`}\n type=\"password\"\n placeholder={loginForm.password.placeholder}\n value={loginForm.password.value}\n onInput={(e) => setLoginFormPassword(loginForm, (e.target as HTMLInputElement).value)}\n />\n {loginForm.password.hasError && loginForm.password.message && (\n <span className=\"login-field-error\">{loginForm.password.message}</span>\n )}\n </div>\n <a className=\"login-forgot-link\" href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"FORGOT_PASSWORD\"); }}>Forgot password?</a>\n <button className=\"login-submit-btn\" type=\"submit\" disabled={loginForm.isSubmitting}>\n {loginForm.isSubmitting ? \"Signing in...\" : \"Sign In\"}\n </button>\n </form>\n <p className=\"login-register-link\">\n Don't have an account?{\" \"}\n <a href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"REGISTER\"); }}>Create one</a>\n </p>\n </div>\n </section>\n );\n}\n",
67
+ "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getLoginForm,\n initLoginForm,\n setLoginFormEmail,\n setLoginFormPassword,\n submitLoginForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function LoginSection({\n redirectAfterLogin = \"/account\",\n}: Props) {\n const loginForm = getLoginForm(customerStore);\n\n useEffect(() => {\n initLoginForm(loginForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitLoginForm(loginForm);\n if (success) Router.navigate(redirectAfterLogin);\n };\n\n return (\n <section className=\"login-section\">\n <div className=\"login-inner\">\n <h1 className=\"login-title\">Sign In</h1>\n {loginForm.isFailure && loginForm.responseMessage && (\n <div className=\"login-error-banner\">{loginForm.responseMessage}</div>\n )}\n <form className=\"login-form\" onSubmit={handleSubmit}>\n <div className=\"login-field\">\n <label className=\"login-label\">{loginForm.email.label}</label>\n <input\n className={`login-input ${loginForm.email.hasError ? \"has-error\" : \"\"}`}\n type=\"email\"\n placeholder={loginForm.email.placeholder}\n value={loginForm.email.value}\n onInput={(e) => setLoginFormEmail(loginForm, (e.target as HTMLInputElement).value)}\n />\n {loginForm.email.hasError && loginForm.email.message && (\n <span className=\"login-field-error\">{loginForm.email.message}</span>\n )}\n </div>\n <div className=\"login-field\">\n <label className=\"login-label\">{loginForm.password.label}</label>\n <input\n className={`login-input ${loginForm.password.hasError ? \"has-error\" : \"\"}`}\n type=\"password\"\n placeholder={loginForm.password.placeholder}\n value={loginForm.password.value}\n onInput={(e) => setLoginFormPassword(loginForm, (e.target as HTMLInputElement).value)}\n />\n {loginForm.password.hasError && loginForm.password.message && (\n <span className=\"login-field-error\">{loginForm.password.message}</span>\n )}\n </div>\n <a className=\"login-forgot-link\" href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"FORGOT_PASSWORD\"); }}>Forgot password?</a>\n <button className=\"login-submit-btn\" type=\"submit\" disabled={loginForm.isSubmitting}>\n {loginForm.isSubmitting ? \"Signing in...\" : \"Sign In\"}\n </button>\n </form>\n <p className=\"login-register-link\">\n Don't have an account?{\" \"}\n <a href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"REGISTER\"); }}>Create one</a>\n </p>\n </div>\n </section>\n );\n}\n",
68
68
  "types.ts": "export interface Props {\n redirectAfterLogin?: string;\n}\n",
69
69
  "styles.css": ".login-section {\n width: 100%;\n padding: 64px 24px;\n}\n\n.login-inner {\n max-width: 400px;\n margin: 0 auto;\n}\n\n.login-title { font-size: 28px; font-weight: 700; color: #111; margin: 0 0 24px 0; text-align: center; }\n.login-error-banner { padding: 12px 16px; font-size: 14px; color: #b71c1c; background: #ffebee; border-radius: 8px; margin-bottom: 20px; }\n.login-form { display: flex; flex-direction: column; gap: 16px; }\n.login-field { display: flex; flex-direction: column; gap: 6px; }\n.login-label { font-size: 14px; font-weight: 600; color: #333; }\n.login-input { padding: 12px 14px; font-size: 15px; border: 1.5px solid #ddd; border-radius: 8px; outline: none; }\n.login-input:focus { border-color: #111; }\n.login-input.has-error { border-color: #e53935; }\n.login-field-error { font-size: 12px; color: #e53935; }\n.login-forgot-link { font-size: 13px; color: #666; text-decoration: none; align-self: flex-end; }\n.login-submit-btn { padding: 14px 24px; font-size: 16px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; margin-top: 8px; }\n.login-submit-btn:disabled { background: #ccc; cursor: not-allowed; }\n.login-register-link { font-size: 14px; color: #666; text-align: center; margin-top: 24px; }\n.login-register-link a { color: #111; font-weight: 600; text-decoration: none; }\n",
70
70
  "ikas-config-snippet.json": "{\n \"id\": \"login\",\n \"name\": \"Login\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"redirectAfterLogin\", \"displayName\": \"Redirect After Login\", \"type\": \"TEXT\", \"defaultValue\": \"/account\" }\n ]\n}\n"
@@ -74,7 +74,7 @@
74
74
  "title": "Register Section",
75
75
  "description": "Customer registration form with name, email, and password fields",
76
76
  "files": {
77
- "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getRegisterForm,\n initRegisterForm,\n setRegisterFormEmail,\n setRegisterFormFirstName,\n setRegisterFormLastName,\n setRegisterFormPassword,\n submitRegisterForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function RegisterSection({\n redirectAfterRegister = \"/account\",\n}: Props) {\n const registerForm = getRegisterForm(customerStore);\n\n useEffect(() => {\n initRegisterForm(registerForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitRegisterForm(registerForm);\n if (success) Router.navigate(redirectAfterRegister);\n };\n\n return (\n <section className=\"register-section\">\n <div className=\"register-inner\">\n <h1 className=\"register-title\">Create Account</h1>\n {registerForm.isFailure && registerForm.responseMessage && (\n <div className=\"register-error-banner\">{registerForm.responseMessage}</div>\n )}\n <form className=\"register-form\" onSubmit={handleSubmit}>\n <div className=\"register-row\">\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.firstName.label}</label>\n <input\n className={`register-input ${registerForm.firstName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={registerForm.firstName.placeholder}\n value={registerForm.firstName.value}\n onInput={(e) => setRegisterFormFirstName(registerForm, (e.target as HTMLInputElement).value)}\n />\n {registerForm.firstName.hasError && registerForm.firstName.message && (\n <span className=\"register-field-error\">{registerForm.firstName.message}</span>\n )}\n </div>\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.lastName.label}</label>\n <input\n className={`register-input ${registerForm.lastName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={registerForm.lastName.placeholder}\n value={registerForm.lastName.value}\n onInput={(e) => setRegisterFormLastName(registerForm, (e.target as HTMLInputElement).value)}\n />\n {registerForm.lastName.hasError && registerForm.lastName.message && (\n <span className=\"register-field-error\">{registerForm.lastName.message}</span>\n )}\n </div>\n </div>\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.email.label}</label>\n <input\n className={`register-input ${registerForm.email.hasError ? \"has-error\" : \"\"}`}\n type=\"email\"\n placeholder={registerForm.email.placeholder}\n value={registerForm.email.value}\n onInput={(e) => setRegisterFormEmail(registerForm, (e.target as HTMLInputElement).value)}\n />\n {registerForm.email.hasError && registerForm.email.message && (\n <span className=\"register-field-error\">{registerForm.email.message}</span>\n )}\n </div>\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.password.label}</label>\n <input\n className={`register-input ${registerForm.password.hasError ? \"has-error\" : \"\"}`}\n type=\"password\"\n placeholder={registerForm.password.placeholder}\n value={registerForm.password.value}\n onInput={(e) => setRegisterFormPassword(registerForm, (e.target as HTMLInputElement).value)}\n />\n {registerForm.password.hasError && registerForm.password.message && (\n <span className=\"register-field-error\">{registerForm.password.message}</span>\n )}\n </div>\n <button className=\"register-submit-btn\" type=\"submit\" disabled={registerForm.isSubmitting}>\n {registerForm.isSubmitting ? \"Creating account...\" : \"Create Account\"}\n </button>\n </form>\n <p className=\"register-login-link\">\n Already have an account?{\" \"}\n <a href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"LOGIN\"); }}>Sign in</a>\n </p>\n </div>\n </section>\n );\n}\n",
77
+ "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getRegisterForm,\n initRegisterForm,\n setRegisterFormEmail,\n setRegisterFormFirstName,\n setRegisterFormLastName,\n setRegisterFormPassword,\n submitRegisterForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function RegisterSection({\n redirectAfterRegister = \"/account\",\n}: Props) {\n const registerForm = getRegisterForm(customerStore);\n\n useEffect(() => {\n initRegisterForm(registerForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitRegisterForm(registerForm);\n if (success) Router.navigate(redirectAfterRegister);\n };\n\n return (\n <section className=\"register-section\">\n <div className=\"register-inner\">\n <h1 className=\"register-title\">Create Account</h1>\n {registerForm.isFailure && registerForm.responseMessage && (\n <div className=\"register-error-banner\">{registerForm.responseMessage}</div>\n )}\n <form className=\"register-form\" onSubmit={handleSubmit}>\n <div className=\"register-row\">\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.firstName.label}</label>\n <input\n className={`register-input ${registerForm.firstName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={registerForm.firstName.placeholder}\n value={registerForm.firstName.value}\n onInput={(e) => setRegisterFormFirstName(registerForm, (e.target as HTMLInputElement).value)}\n />\n {registerForm.firstName.hasError && registerForm.firstName.message && (\n <span className=\"register-field-error\">{registerForm.firstName.message}</span>\n )}\n </div>\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.lastName.label}</label>\n <input\n className={`register-input ${registerForm.lastName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={registerForm.lastName.placeholder}\n value={registerForm.lastName.value}\n onInput={(e) => setRegisterFormLastName(registerForm, (e.target as HTMLInputElement).value)}\n />\n {registerForm.lastName.hasError && registerForm.lastName.message && (\n <span className=\"register-field-error\">{registerForm.lastName.message}</span>\n )}\n </div>\n </div>\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.email.label}</label>\n <input\n className={`register-input ${registerForm.email.hasError ? \"has-error\" : \"\"}`}\n type=\"email\"\n placeholder={registerForm.email.placeholder}\n value={registerForm.email.value}\n onInput={(e) => setRegisterFormEmail(registerForm, (e.target as HTMLInputElement).value)}\n />\n {registerForm.email.hasError && registerForm.email.message && (\n <span className=\"register-field-error\">{registerForm.email.message}</span>\n )}\n </div>\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.password.label}</label>\n <input\n className={`register-input ${registerForm.password.hasError ? \"has-error\" : \"\"}`}\n type=\"password\"\n placeholder={registerForm.password.placeholder}\n value={registerForm.password.value}\n onInput={(e) => setRegisterFormPassword(registerForm, (e.target as HTMLInputElement).value)}\n />\n {registerForm.password.hasError && registerForm.password.message && (\n <span className=\"register-field-error\">{registerForm.password.message}</span>\n )}\n </div>\n <button className=\"register-submit-btn\" type=\"submit\" disabled={registerForm.isSubmitting}>\n {registerForm.isSubmitting ? \"Creating account...\" : \"Create Account\"}\n </button>\n </form>\n <p className=\"register-login-link\">\n Already have an account?{\" \"}\n <a href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"LOGIN\"); }}>Sign in</a>\n </p>\n </div>\n </section>\n );\n}\n",
78
78
  "types.ts": "export interface Props {\n redirectAfterRegister?: string;\n}\n",
79
79
  "styles.css": ".register-section {\n width: 100%;\n padding: 64px 24px;\n}\n\n.register-inner {\n max-width: 480px;\n margin: 0 auto;\n}\n\n.register-title { font-size: 28px; font-weight: 700; color: #111; margin: 0 0 24px 0; text-align: center; }\n.register-error-banner { padding: 12px 16px; font-size: 14px; color: #b71c1c; background: #ffebee; border-radius: 8px; margin-bottom: 20px; }\n.register-form { display: flex; flex-direction: column; gap: 16px; }\n.register-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }\n.register-field { display: flex; flex-direction: column; gap: 6px; }\n.register-label { font-size: 14px; font-weight: 600; color: #333; }\n.register-input { padding: 12px 14px; font-size: 15px; border: 1.5px solid #ddd; border-radius: 8px; outline: none; }\n.register-input:focus { border-color: #111; }\n.register-input.has-error { border-color: #e53935; }\n.register-field-error { font-size: 12px; color: #e53935; }\n.register-submit-btn { padding: 14px 24px; font-size: 16px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; margin-top: 8px; }\n.register-submit-btn:disabled { background: #ccc; cursor: not-allowed; }\n.register-login-link { font-size: 14px; color: #666; text-align: center; margin-top: 24px; }\n.register-login-link a { color: #111; font-weight: 600; text-decoration: none; }\n\n@media (max-width: 480px) {\n .register-row { grid-template-columns: 1fr; }\n}\n",
80
80
  "ikas-config-snippet.json": "{\n \"id\": \"register\",\n \"name\": \"Register\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"redirectAfterRegister\", \"displayName\": \"Redirect After Register\", \"type\": \"TEXT\", \"defaultValue\": \"/account\" }\n ]\n}\n"
@@ -84,7 +84,7 @@
84
84
  "title": "Forgot Password Section",
85
85
  "description": "Password reset form with email input and success/error states",
86
86
  "files": {
87
- "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getForgotPasswordForm,\n initForgotPasswordForm,\n setForgotPasswordFormEmail,\n submitForgotPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ForgotPasswordSection({\n successMessage = \"Password reset link has been sent to your email.\",\n}: Props) {\n const forgotForm = getForgotPasswordForm(customerStore);\n\n useEffect(() => {\n initForgotPasswordForm(forgotForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitForgotPasswordForm(forgotForm);\n };\n\n return (\n <section className=\"forgot-section\">\n <div className=\"forgot-inner\">\n <h1 className=\"forgot-title\">Forgot Password</h1>\n <p className=\"forgot-subtitle\">Enter your email and we'll send you a reset link.</p>\n {forgotForm.isSuccess && <div className=\"forgot-success-banner\">{successMessage}</div>}\n {forgotForm.isFailure && forgotForm.responseMessage && (\n <div className=\"forgot-error-banner\">{forgotForm.responseMessage}</div>\n )}\n {!forgotForm.isSuccess && (\n <form className=\"forgot-form\" onSubmit={handleSubmit}>\n <div className=\"forgot-field\">\n <label className=\"forgot-label\">{forgotForm.email.label}</label>\n <input\n className={`forgot-input ${forgotForm.email.hasError ? \"has-error\" : \"\"}`}\n type=\"email\"\n placeholder={forgotForm.email.placeholder}\n value={forgotForm.email.value}\n onInput={(e) => setForgotPasswordFormEmail(forgotForm, (e.target as HTMLInputElement).value)}\n />\n {forgotForm.email.hasError && forgotForm.email.message && (\n <span className=\"forgot-field-error\">{forgotForm.email.message}</span>\n )}\n </div>\n <button className=\"forgot-submit-btn\" type=\"submit\" disabled={forgotForm.isSubmitting}>\n {forgotForm.isSubmitting ? \"Sending...\" : \"Send Reset Link\"}\n </button>\n </form>\n )}\n <p className=\"forgot-back-link\">\n <a href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"LOGIN\"); }}>Back to Sign In</a>\n </p>\n </div>\n </section>\n );\n}\n",
87
+ "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getForgotPasswordForm,\n initForgotPasswordForm,\n setForgotPasswordFormEmail,\n submitForgotPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function ForgotPasswordSection({\n successMessage = \"Password reset link has been sent to your email.\",\n}: Props) {\n const forgotForm = getForgotPasswordForm(customerStore);\n\n useEffect(() => {\n initForgotPasswordForm(forgotForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitForgotPasswordForm(forgotForm);\n };\n\n return (\n <section className=\"forgot-section\">\n <div className=\"forgot-inner\">\n <h1 className=\"forgot-title\">Forgot Password</h1>\n <p className=\"forgot-subtitle\">Enter your email and we'll send you a reset link.</p>\n {forgotForm.isSuccess && <div className=\"forgot-success-banner\">{successMessage}</div>}\n {forgotForm.isFailure && forgotForm.responseMessage && (\n <div className=\"forgot-error-banner\">{forgotForm.responseMessage}</div>\n )}\n {!forgotForm.isSuccess && (\n <form className=\"forgot-form\" onSubmit={handleSubmit}>\n <div className=\"forgot-field\">\n <label className=\"forgot-label\">{forgotForm.email.label}</label>\n <input\n className={`forgot-input ${forgotForm.email.hasError ? \"has-error\" : \"\"}`}\n type=\"email\"\n placeholder={forgotForm.email.placeholder}\n value={forgotForm.email.value}\n onInput={(e) => setForgotPasswordFormEmail(forgotForm, (e.target as HTMLInputElement).value)}\n />\n {forgotForm.email.hasError && forgotForm.email.message && (\n <span className=\"forgot-field-error\">{forgotForm.email.message}</span>\n )}\n </div>\n <button className=\"forgot-submit-btn\" type=\"submit\" disabled={forgotForm.isSubmitting}>\n {forgotForm.isSubmitting ? \"Sending...\" : \"Send Reset Link\"}\n </button>\n </form>\n )}\n <p className=\"forgot-back-link\">\n <a href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"LOGIN\"); }}>Back to Sign In</a>\n </p>\n </div>\n </section>\n );\n}\n",
88
88
  "types.ts": "export interface Props {\n successMessage?: string;\n}\n",
89
89
  "styles.css": ".forgot-section {\n width: 100%;\n padding: 64px 24px;\n}\n\n.forgot-inner {\n max-width: 400px;\n margin: 0 auto;\n}\n\n.forgot-title { font-size: 28px; font-weight: 700; color: #111; margin: 0 0 8px 0; text-align: center; }\n.forgot-subtitle { font-size: 15px; color: #666; text-align: center; margin: 0 0 24px 0; }\n.forgot-success-banner { padding: 12px 16px; font-size: 14px; color: #1b5e20; background: #e8f5e9; border-radius: 8px; margin-bottom: 20px; }\n.forgot-error-banner { padding: 12px 16px; font-size: 14px; color: #b71c1c; background: #ffebee; border-radius: 8px; margin-bottom: 20px; }\n.forgot-form { display: flex; flex-direction: column; gap: 16px; }\n.forgot-field { display: flex; flex-direction: column; gap: 6px; }\n.forgot-label { font-size: 14px; font-weight: 600; color: #333; }\n.forgot-input { padding: 12px 14px; font-size: 15px; border: 1.5px solid #ddd; border-radius: 8px; outline: none; }\n.forgot-input:focus { border-color: #111; }\n.forgot-input.has-error { border-color: #e53935; }\n.forgot-field-error { font-size: 12px; color: #e53935; }\n.forgot-submit-btn { padding: 14px 24px; font-size: 16px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; margin-top: 8px; }\n.forgot-submit-btn:disabled { background: #ccc; cursor: not-allowed; }\n.forgot-back-link { font-size: 14px; color: #666; text-align: center; margin-top: 24px; }\n.forgot-back-link a { color: #111; font-weight: 600; text-decoration: none; }\n",
90
90
  "ikas-config-snippet.json": "{\n \"id\": \"forgot-password\",\n \"name\": \"Forgot Password\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"successMessage\", \"displayName\": \"Success Message\", \"type\": \"TEXT\", \"defaultValue\": \"Password reset link has been sent to your email.\" }\n ]\n}\n"
@@ -94,7 +94,7 @@
94
94
  "title": "Account Orders Section",
95
95
  "description": "Customer order history list with order details and status",
96
96
  "files": {
97
- "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getOrders,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderTotalItemCount,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderHref,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountOrdersSection({\n title = \"My Orders\",\n emptyMessage = \"You have no orders yet.\",\n}: Props) {\n useEffect(() => {\n getOrders(customerStore);\n }, []);\n\n const orders = customerStore.orders ?? [];\n\n return (\n <section className=\"orders-section\">\n <div className=\"orders-inner\">\n <h1 className=\"orders-title\">{title}</h1>\n {orders.length === 0 && (\n <div className=\"orders-empty\">\n <p>{emptyMessage}</p>\n <button className=\"orders-shop-btn\" onClick={() => Router.navigate(\"/\")}>Start Shopping</button>\n </div>\n )}\n <div className=\"orders-list\">\n {orders.map((order) => (\n <a key={order.id} href={getIkasOrderHref(order)} className=\"order-card\">\n <div className=\"order-card-header\">\n <span className=\"order-number\">Order #{order.orderNumber}</span>\n <span className=\"order-status\">{getIkasOrderPackageStatusTranslation(order)}</span>\n </div>\n <div className=\"order-card-details\">\n <span className=\"order-date\">{getIkasOrderFormattedOrderedAt(order)}</span>\n <span className=\"order-items\">{getIkasOrderTotalItemCount(order)} items</span>\n <span className=\"order-total\">{getIkasOrderFormattedTotalFinalPrice(order)}</span>\n </div>\n </a>\n ))}\n </div>\n </div>\n </section>\n );\n}\n",
97
+ "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getOrders,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderTotalItemCount,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderHref,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function AccountOrdersSection({\n title = \"My Orders\",\n emptyMessage = \"You have no orders yet.\",\n}: Props) {\n useEffect(() => {\n getOrders(customerStore);\n }, []);\n\n const orders = customerStore.orders ?? [];\n\n return (\n <section className=\"orders-section\">\n <div className=\"orders-inner\">\n <h1 className=\"orders-title\">{title}</h1>\n {orders.length === 0 && (\n <div className=\"orders-empty\">\n <p>{emptyMessage}</p>\n <button className=\"orders-shop-btn\" onClick={() => Router.navigate(\"/\")}>Start Shopping</button>\n </div>\n )}\n <div className=\"orders-list\">\n {orders.map((order) => (\n <a key={order.id} href={getIkasOrderHref(order)} className=\"order-card\">\n <div className=\"order-card-header\">\n <span className=\"order-number\">Order #{order.orderNumber}</span>\n <span className=\"order-status\">{getIkasOrderPackageStatusTranslation(order)}</span>\n </div>\n <div className=\"order-card-details\">\n <span className=\"order-date\">{getIkasOrderFormattedOrderedAt(order)}</span>\n <span className=\"order-items\">{getIkasOrderTotalItemCount(order)} items</span>\n <span className=\"order-total\">{getIkasOrderFormattedTotalFinalPrice(order)}</span>\n </div>\n </a>\n ))}\n </div>\n </div>\n </section>\n );\n}\n",
98
98
  "types.ts": "export interface Props {\n title?: string;\n emptyMessage?: string;\n}\n",
99
99
  "styles.css": ".orders-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.orders-inner {\n max-width: 800px;\n margin: 0 auto;\n}\n\n.orders-title { font-size: 24px; font-weight: 700; color: #111; margin: 0 0 24px 0; }\n\n.orders-empty { text-align: center; padding: 48px 0; }\n.orders-empty p { font-size: 16px; color: #666; margin: 0 0 16px 0; }\n.orders-shop-btn { padding: 12px 24px; font-size: 14px; font-weight: 600; color: #111; background: #fff; border: 1.5px solid #111; border-radius: 8px; cursor: pointer; }\n\n.orders-list { display: flex; flex-direction: column; gap: 12px; }\n.order-card { display: block; text-decoration: none; color: inherit; padding: 20px; border: 1px solid #eee; border-radius: 8px; transition: border-color 0.15s; }\n.order-card:hover { border-color: #111; }\n.order-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }\n.order-number { font-size: 15px; font-weight: 600; color: #111; }\n.order-status { font-size: 13px; font-weight: 500; color: #1976d2; background: #e3f2fd; padding: 4px 10px; border-radius: 12px; }\n.order-card-details { display: flex; gap: 24px; font-size: 14px; color: #666; }\n",
100
100
  "ikas-config-snippet.json": "{\n \"id\": \"account-orders\",\n \"name\": \"Account Orders\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"My Orders\" },\n { \"name\": \"emptyMessage\", \"displayName\": \"Empty Message\", \"type\": \"TEXT\", \"defaultValue\": \"You have no orders yet.\" }\n ]\n}\n"
@@ -104,7 +104,7 @@
104
104
  "title": "Account Addresses Section",
105
105
  "description": "Customer address list with add/delete functionality",
106
106
  "files": {
107
- "index.tsx": "import {\n customerStore,\n deleteAddress,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountAddressesSection({\n title = \"My Addresses\",\n}: Props) {\n const addresses = customerStore.customer?.addresses ?? [];\n\n const handleDelete = async (addressId: string) => {\n await deleteAddress(customerStore, addressId);\n };\n\n return (\n <section className=\"addresses-section\">\n <div className=\"addresses-inner\">\n <h1 className=\"addresses-title\">{title}</h1>\n {addresses.length === 0 && (\n <p className=\"addresses-empty\">No addresses saved yet.</p>\n )}\n <div className=\"addresses-grid\">\n {addresses.map((addr) => (\n <div key={addr.id} className=\"address-card\">\n <p className=\"address-name\">{addr.firstName} {addr.lastName}</p>\n <p className=\"address-line\">{addr.addressLine1}</p>\n {addr.addressLine2 && <p className=\"address-line\">{addr.addressLine2}</p>}\n <p className=\"address-line\">{addr.city}, {addr.state?.name} {addr.postalCode}</p>\n <p className=\"address-phone\">{addr.phone}</p>\n <button className=\"address-delete-btn\" onClick={() => handleDelete(addr.id)}>Delete</button>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n",
107
+ "index.tsx": "import {\n customerStore,\n deleteAddress,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function AccountAddressesSection({\n title = \"My Addresses\",\n}: Props) {\n const addresses = customerStore.customer?.addresses ?? [];\n\n const handleDelete = async (addressId: string) => {\n await deleteAddress(customerStore, addressId);\n };\n\n return (\n <section className=\"addresses-section\">\n <div className=\"addresses-inner\">\n <h1 className=\"addresses-title\">{title}</h1>\n {addresses.length === 0 && (\n <p className=\"addresses-empty\">No addresses saved yet.</p>\n )}\n <div className=\"addresses-grid\">\n {addresses.map((addr) => (\n <div key={addr.id} className=\"address-card\">\n <p className=\"address-name\">{addr.firstName} {addr.lastName}</p>\n <p className=\"address-line\">{addr.addressLine1}</p>\n {addr.addressLine2 && <p className=\"address-line\">{addr.addressLine2}</p>}\n <p className=\"address-line\">{addr.city}, {addr.state?.name} {addr.postalCode}</p>\n <p className=\"address-phone\">{addr.phone}</p>\n <button className=\"address-delete-btn\" onClick={() => handleDelete(addr.id)}>Delete</button>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n",
108
108
  "types.ts": "export interface Props {\n title?: string;\n}\n",
109
109
  "styles.css": ".addresses-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.addresses-inner {\n max-width: 800px;\n margin: 0 auto;\n}\n\n.addresses-title { font-size: 24px; font-weight: 700; color: #111; margin: 0 0 24px 0; }\n.addresses-empty { font-size: 16px; color: #666; text-align: center; padding: 48px 0; }\n\n.addresses-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 16px;\n}\n\n.address-card { padding: 20px; border: 1px solid #eee; border-radius: 8px; }\n.address-name { font-size: 15px; font-weight: 600; color: #111; margin: 0 0 8px 0; }\n.address-line { font-size: 14px; color: #555; margin: 0 0 4px 0; }\n.address-phone { font-size: 14px; color: #555; margin: 8px 0; }\n.address-delete-btn { font-size: 13px; color: #e53935; background: none; border: none; cursor: pointer; padding: 0; margin-top: 8px; }\n\n@media (max-width: 768px) {\n .addresses-grid { grid-template-columns: 1fr; }\n}\n",
110
110
  "ikas-config-snippet.json": "{\n \"id\": \"account-addresses\",\n \"name\": \"Account Addresses\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"My Addresses\" }\n ]\n}\n"
@@ -114,7 +114,7 @@
114
114
  "title": "Favorites Section",
115
115
  "description": "Customer favorites/wishlist with product cards and remove functionality",
116
116
  "files": {
117
- "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getFavoriteProducts,\n removeIkasProductFromFavorites,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getDefaultSrc,\n IkasImage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function FavoritesSection({\n title = \"My Favorites\",\n}: Props) {\n useEffect(() => {\n getFavoriteProducts(customerStore);\n }, []);\n\n const favorites = customerStore.favoriteProducts ?? [];\n\n return (\n <section className=\"favorites-section\">\n <div className=\"favorites-inner\">\n <h1 className=\"favorites-title\">{title}</h1>\n {favorites.length === 0 && (\n <p className=\"favorites-empty\">You haven't added any favorites yet.</p>\n )}\n <div className=\"favorites-grid\">\n {favorites.map((product) => {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n return (\n <div key={product.id} className=\"favorites-card\">\n <a href={getSelectedProductVariantHref(product)} className=\"favorites-card-link\">\n {image && <img src={getDefaultSrc(image)} alt={product.name} className=\"favorites-card-image\" />}\n <h3 className=\"favorites-card-name\">{product.name}</h3>\n <span className=\"favorites-card-price\">{price}</span>\n </a>\n <button className=\"favorites-remove-btn\" onClick={() => removeIkasProductFromFavorites(product)}>Remove</button>\n </div>\n );\n })}\n </div>\n </div>\n </section>\n );\n}\n",
117
+ "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getFavoriteProducts,\n removeIkasProductFromFavorites,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getDefaultSrc,\n IkasImage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function FavoritesSection({\n title = \"My Favorites\",\n}: Props) {\n useEffect(() => {\n getFavoriteProducts(customerStore);\n }, []);\n\n const favorites = customerStore.favoriteProducts ?? [];\n\n return (\n <section className=\"favorites-section\">\n <div className=\"favorites-inner\">\n <h1 className=\"favorites-title\">{title}</h1>\n {favorites.length === 0 && (\n <p className=\"favorites-empty\">You haven't added any favorites yet.</p>\n )}\n <div className=\"favorites-grid\">\n {favorites.map((product) => {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n return (\n <div key={product.id} className=\"favorites-card\">\n <a href={getSelectedProductVariantHref(product)} className=\"favorites-card-link\">\n {image && <img src={getDefaultSrc(image)} alt={product.name} className=\"favorites-card-image\" />}\n <h3 className=\"favorites-card-name\">{product.name}</h3>\n <span className=\"favorites-card-price\">{price}</span>\n </a>\n <button className=\"favorites-remove-btn\" onClick={() => removeIkasProductFromFavorites(product)}>Remove</button>\n </div>\n );\n })}\n </div>\n </div>\n </section>\n );\n}\n",
118
118
  "types.ts": "export interface Props {\n title?: string;\n}\n",
119
119
  "styles.css": ".favorites-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.favorites-inner {\n max-width: 1200px;\n margin: 0 auto;\n}\n\n.favorites-title { font-size: 24px; font-weight: 700; color: #111; margin: 0 0 24px 0; }\n.favorites-empty { font-size: 16px; color: #666; text-align: center; padding: 48px 0; }\n\n.favorites-grid {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 24px;\n}\n\n.favorites-card { position: relative; }\n.favorites-card-link { text-decoration: none; color: inherit; }\n.favorites-card-image { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 8px; background: #f5f5f5; }\n.favorites-card-name { font-size: 14px; font-weight: 500; color: #111; margin: 10px 0 4px; }\n.favorites-card-price { font-size: 14px; font-weight: 600; color: #111; }\n.favorites-remove-btn { font-size: 13px; color: #e53935; background: none; border: none; cursor: pointer; padding: 0; margin-top: 8px; }\n\n@media (max-width: 768px) {\n .favorites-grid { grid-template-columns: repeat(2, 1fr); gap: 16px; }\n}\n",
120
120
  "ikas-config-snippet.json": "{\n \"id\": \"favorites\",\n \"name\": \"Favorites\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"My Favorites\" }\n ]\n}\n"
@@ -124,7 +124,7 @@
124
124
  "title": "Contact Form Section",
125
125
  "description": "Contact form with name, email, phone, and message fields",
126
126
  "files": {
127
- "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getContactForm,\n initContactForm,\n setContactFormEmail,\n setContactFormFirstName,\n setContactFormLastName,\n setContactFormPhone,\n setContactFormMessage,\n submitContactForm,\n clearContactForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ContactFormSection({\n title = \"Contact Us\",\n successMessage = \"Thank you! Your message has been sent.\",\n}: Props) {\n const contactForm = getContactForm(customerStore);\n\n useEffect(() => {\n initContactForm(contactForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitContactForm(contactForm);\n if (success) clearContactForm(contactForm);\n };\n\n return (\n <section className=\"contact-section\">\n <div className=\"contact-inner\">\n <h1 className=\"contact-title\">{title}</h1>\n {contactForm.isSuccess && <div className=\"contact-success-banner\">{successMessage}</div>}\n {contactForm.isFailure && contactForm.responseMessage && (\n <div className=\"contact-error-banner\">{contactForm.responseMessage}</div>\n )}\n <form className=\"contact-form\" onSubmit={handleSubmit}>\n <div className=\"contact-row\">\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.firstName.label}</label>\n <input className={`contact-input ${contactForm.firstName.hasError ? \"has-error\" : \"\"}`} type=\"text\" placeholder={contactForm.firstName.placeholder} value={contactForm.firstName.value} onInput={(e) => setContactFormFirstName(contactForm, (e.target as HTMLInputElement).value)} />\n {contactForm.firstName.hasError && contactForm.firstName.message && <span className=\"contact-field-error\">{contactForm.firstName.message}</span>}\n </div>\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.lastName.label}</label>\n <input className={`contact-input ${contactForm.lastName.hasError ? \"has-error\" : \"\"}`} type=\"text\" placeholder={contactForm.lastName.placeholder} value={contactForm.lastName.value} onInput={(e) => setContactFormLastName(contactForm, (e.target as HTMLInputElement).value)} />\n {contactForm.lastName.hasError && contactForm.lastName.message && <span className=\"contact-field-error\">{contactForm.lastName.message}</span>}\n </div>\n </div>\n <div className=\"contact-row\">\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.email.label}</label>\n <input className={`contact-input ${contactForm.email.hasError ? \"has-error\" : \"\"}`} type=\"email\" placeholder={contactForm.email.placeholder} value={contactForm.email.value} onInput={(e) => setContactFormEmail(contactForm, (e.target as HTMLInputElement).value)} />\n {contactForm.email.hasError && contactForm.email.message && <span className=\"contact-field-error\">{contactForm.email.message}</span>}\n </div>\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.phone.label}</label>\n <input className={`contact-input ${contactForm.phone.hasError ? \"has-error\" : \"\"}`} type=\"tel\" placeholder={contactForm.phone.placeholder} value={contactForm.phone.value} onInput={(e) => setContactFormPhone(contactForm, (e.target as HTMLInputElement).value)} />\n {contactForm.phone.hasError && contactForm.phone.message && <span className=\"contact-field-error\">{contactForm.phone.message}</span>}\n </div>\n </div>\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.message.label}</label>\n <textarea className={`contact-textarea ${contactForm.message.hasError ? \"has-error\" : \"\"}`} placeholder={contactForm.message.placeholder} value={contactForm.message.value} rows={5} onInput={(e) => setContactFormMessage(contactForm, (e.target as HTMLTextAreaElement).value)} />\n {contactForm.message.hasError && contactForm.message.message && <span className=\"contact-field-error\">{contactForm.message.message}</span>}\n </div>\n <button className=\"contact-submit-btn\" type=\"submit\" disabled={contactForm.isSubmitting}>\n {contactForm.isSubmitting ? \"Sending...\" : \"Send Message\"}\n </button>\n </form>\n </div>\n </section>\n );\n}\n",
127
+ "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getContactForm,\n initContactForm,\n setContactFormEmail,\n setContactFormFirstName,\n setContactFormLastName,\n setContactFormPhone,\n setContactFormMessage,\n submitContactForm,\n clearContactForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function ContactFormSection({\n title = \"Contact Us\",\n successMessage = \"Thank you! Your message has been sent.\",\n}: Props) {\n const contactForm = getContactForm(customerStore);\n\n useEffect(() => {\n initContactForm(contactForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitContactForm(contactForm);\n if (success) clearContactForm(contactForm);\n };\n\n return (\n <section className=\"contact-section\">\n <div className=\"contact-inner\">\n <h1 className=\"contact-title\">{title}</h1>\n {contactForm.isSuccess && <div className=\"contact-success-banner\">{successMessage}</div>}\n {contactForm.isFailure && contactForm.responseMessage && (\n <div className=\"contact-error-banner\">{contactForm.responseMessage}</div>\n )}\n <form className=\"contact-form\" onSubmit={handleSubmit}>\n <div className=\"contact-row\">\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.firstName.label}</label>\n <input className={`contact-input ${contactForm.firstName.hasError ? \"has-error\" : \"\"}`} type=\"text\" placeholder={contactForm.firstName.placeholder} value={contactForm.firstName.value} onInput={(e) => setContactFormFirstName(contactForm, (e.target as HTMLInputElement).value)} />\n {contactForm.firstName.hasError && contactForm.firstName.message && <span className=\"contact-field-error\">{contactForm.firstName.message}</span>}\n </div>\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.lastName.label}</label>\n <input className={`contact-input ${contactForm.lastName.hasError ? \"has-error\" : \"\"}`} type=\"text\" placeholder={contactForm.lastName.placeholder} value={contactForm.lastName.value} onInput={(e) => setContactFormLastName(contactForm, (e.target as HTMLInputElement).value)} />\n {contactForm.lastName.hasError && contactForm.lastName.message && <span className=\"contact-field-error\">{contactForm.lastName.message}</span>}\n </div>\n </div>\n <div className=\"contact-row\">\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.email.label}</label>\n <input className={`contact-input ${contactForm.email.hasError ? \"has-error\" : \"\"}`} type=\"email\" placeholder={contactForm.email.placeholder} value={contactForm.email.value} onInput={(e) => setContactFormEmail(contactForm, (e.target as HTMLInputElement).value)} />\n {contactForm.email.hasError && contactForm.email.message && <span className=\"contact-field-error\">{contactForm.email.message}</span>}\n </div>\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.phone.label}</label>\n <input className={`contact-input ${contactForm.phone.hasError ? \"has-error\" : \"\"}`} type=\"tel\" placeholder={contactForm.phone.placeholder} value={contactForm.phone.value} onInput={(e) => setContactFormPhone(contactForm, (e.target as HTMLInputElement).value)} />\n {contactForm.phone.hasError && contactForm.phone.message && <span className=\"contact-field-error\">{contactForm.phone.message}</span>}\n </div>\n </div>\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.message.label}</label>\n <textarea className={`contact-textarea ${contactForm.message.hasError ? \"has-error\" : \"\"}`} placeholder={contactForm.message.placeholder} value={contactForm.message.value} rows={5} onInput={(e) => setContactFormMessage(contactForm, (e.target as HTMLTextAreaElement).value)} />\n {contactForm.message.hasError && contactForm.message.message && <span className=\"contact-field-error\">{contactForm.message.message}</span>}\n </div>\n <button className=\"contact-submit-btn\" type=\"submit\" disabled={contactForm.isSubmitting}>\n {contactForm.isSubmitting ? \"Sending...\" : \"Send Message\"}\n </button>\n </form>\n </div>\n </section>\n );\n}\n",
128
128
  "types.ts": "export interface Props {\n title?: string;\n successMessage?: string;\n}\n",
129
129
  "styles.css": ".contact-section {\n width: 100%;\n padding: 64px 24px;\n}\n\n.contact-inner {\n max-width: 600px;\n margin: 0 auto;\n}\n\n.contact-title { font-size: 28px; font-weight: 700; color: #111; margin: 0 0 24px 0; text-align: center; }\n.contact-success-banner { padding: 12px 16px; font-size: 14px; color: #1b5e20; background: #e8f5e9; border-radius: 8px; margin-bottom: 20px; }\n.contact-error-banner { padding: 12px 16px; font-size: 14px; color: #b71c1c; background: #ffebee; border-radius: 8px; margin-bottom: 20px; }\n.contact-form { display: flex; flex-direction: column; gap: 16px; }\n.contact-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }\n.contact-field { display: flex; flex-direction: column; gap: 6px; }\n.contact-label { font-size: 14px; font-weight: 600; color: #333; }\n.contact-input { padding: 12px 14px; font-size: 15px; border: 1.5px solid #ddd; border-radius: 8px; outline: none; }\n.contact-textarea { padding: 12px 14px; font-size: 15px; border: 1.5px solid #ddd; border-radius: 8px; outline: none; resize: vertical; }\n.contact-input:focus, .contact-textarea:focus { border-color: #111; }\n.contact-input.has-error, .contact-textarea.has-error { border-color: #e53935; }\n.contact-field-error { font-size: 12px; color: #e53935; }\n.contact-submit-btn { padding: 14px 24px; font-size: 16px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; margin-top: 8px; }\n.contact-submit-btn:disabled { background: #ccc; cursor: not-allowed; }\n\n@media (max-width: 480px) {\n .contact-row { grid-template-columns: 1fr; }\n}\n",
130
130
  "ikas-config-snippet.json": "{\n \"id\": \"contact-form\",\n \"name\": \"Contact Form\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"Contact Us\" },\n { \"name\": \"successMessage\", \"displayName\": \"Success Message\", \"type\": \"TEXT\", \"defaultValue\": \"Thank you! Your message has been sent.\" }\n ]\n}\n"
@@ -144,7 +144,7 @@
144
144
  "title": "Blog List Section",
145
145
  "description": "Blog post grid with images, dates, summaries, and pagination",
146
146
  "files": {
147
- "index.tsx": "import {\n hasBlogListNextPage,\n getBlogListNextPage,\n hasBlogListPrevPage,\n getBlogListPrevPage,\n getIkasBlogFormattedDate,\n getIkasBlogHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function BlogListSection({\n blogList,\n title = \"Blog\",\n}: Props) {\n if (!blogList) return null;\n\n const blogs = blogList.blogs ?? [];\n const hasNext = hasBlogListNextPage(blogList);\n const hasPrev = hasBlogListPrevPage(blogList);\n\n return (\n <section className=\"blog-list-section\">\n <div className=\"blog-list-inner\">\n <h1 className=\"blog-list-title\">{title}</h1>\n {blogs.length === 0 && <p className=\"blog-list-empty\">No blog posts found.</p>}\n <div className=\"blog-grid\">\n {blogs.map((blog) => (\n <a key={blog.id} href={getIkasBlogHref(blog)} className=\"blog-card\">\n {blog.image && (\n <div className=\"blog-card-image-wrap\">\n <img src={getDefaultSrc(blog.image)} alt={blog.title} className=\"blog-card-image\" />\n </div>\n )}\n <div className=\"blog-card-content\">\n <span className=\"blog-card-date\">{getIkasBlogFormattedDate(blog)}</span>\n <h3 className=\"blog-card-title\">{blog.title}</h3>\n {blog.summary && <p className=\"blog-card-summary\">{blog.summary}</p>}\n <span className=\"blog-card-read-more\">Read more</span>\n </div>\n </a>\n ))}\n </div>\n {(hasPrev || hasNext) && (\n <div className=\"blog-pagination\">\n <button className=\"blog-pagination-btn\" disabled={!hasPrev} onClick={() => getBlogListPrevPage(blogList)}>Previous</button>\n <button className=\"blog-pagination-btn\" disabled={!hasNext} onClick={() => getBlogListNextPage(blogList)}>Next</button>\n </div>\n )}\n </div>\n </section>\n );\n}\n",
147
+ "index.tsx": "import {\n hasBlogListNextPage,\n getBlogListNextPage,\n hasBlogListPrevPage,\n getBlogListPrevPage,\n getIkasBlogFormattedDate,\n getIkasBlogHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function BlogListSection({\n blogList,\n title = \"Blog\",\n}: Props) {\n if (!blogList) return null;\n\n const blogs = blogList.blogs ?? [];\n const hasNext = hasBlogListNextPage(blogList);\n const hasPrev = hasBlogListPrevPage(blogList);\n\n return (\n <section className=\"blog-list-section\">\n <div className=\"blog-list-inner\">\n <h1 className=\"blog-list-title\">{title}</h1>\n {blogs.length === 0 && <p className=\"blog-list-empty\">No blog posts found.</p>}\n <div className=\"blog-grid\">\n {blogs.map((blog) => (\n <a key={blog.id} href={getIkasBlogHref(blog)} className=\"blog-card\">\n {blog.image && (\n <div className=\"blog-card-image-wrap\">\n <img src={getDefaultSrc(blog.image)} alt={blog.title} className=\"blog-card-image\" />\n </div>\n )}\n <div className=\"blog-card-content\">\n <span className=\"blog-card-date\">{getIkasBlogFormattedDate(blog)}</span>\n <h3 className=\"blog-card-title\">{blog.title}</h3>\n {blog.summary && <p className=\"blog-card-summary\">{blog.summary}</p>}\n <span className=\"blog-card-read-more\">Read more</span>\n </div>\n </a>\n ))}\n </div>\n {(hasPrev || hasNext) && (\n <div className=\"blog-pagination\">\n <button className=\"blog-pagination-btn\" disabled={!hasPrev} onClick={() => getBlogListPrevPage(blogList)}>Previous</button>\n <button className=\"blog-pagination-btn\" disabled={!hasNext} onClick={() => getBlogListNextPage(blogList)}>Next</button>\n </div>\n )}\n </div>\n </section>\n );\n}\n",
148
148
  "types.ts": "import { IkasBlogList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogList: IkasBlogList;\n title?: string;\n}\n",
149
149
  "styles.css": ".blog-list-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.blog-list-inner {\n max-width: 1200px;\n margin: 0 auto;\n}\n\n.blog-list-title { font-size: 24px; font-weight: 700; color: #111; margin: 0 0 24px 0; }\n.blog-list-empty { font-size: 16px; color: #666; text-align: center; padding: 48px 0; }\n\n.blog-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 32px;\n}\n\n.blog-card { text-decoration: none; color: inherit; }\n.blog-card-image-wrap { border-radius: 8px; overflow: hidden; margin-bottom: 16px; }\n.blog-card-image { width: 100%; aspect-ratio: 16/9; object-fit: cover; display: block; }\n.blog-card-date { font-size: 13px; color: #999; }\n.blog-card-title { font-size: 18px; font-weight: 600; color: #111; margin: 6px 0 8px; }\n.blog-card-summary { font-size: 14px; color: #666; line-height: 1.5; margin: 0 0 8px; }\n.blog-card-read-more { font-size: 14px; font-weight: 600; color: #111; }\n\n.blog-pagination { display: flex; justify-content: center; gap: 12px; margin-top: 32px; }\n.blog-pagination-btn { padding: 10px 20px; border: 1px solid #ddd; border-radius: 6px; background: #fff; cursor: pointer; font-size: 14px; }\n.blog-pagination-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n\n@media (max-width: 768px) {\n .blog-grid { grid-template-columns: 1fr; gap: 24px; }\n}\n",
150
150
  "ikas-config-snippet.json": "{\n \"id\": \"blog-list\",\n \"name\": \"Blog List\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"blogList\", \"displayName\": \"Blog List\", \"type\": \"BLOG_POST_LIST\", \"required\": true },\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"Blog\" }\n ]\n}\n"
@@ -164,7 +164,7 @@
164
164
  "title": "Product Reviews Section",
165
165
  "description": "Product reviews display with star ratings and review submission form",
166
166
  "files": {
167
- "index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n getProductCustomerReviews,\n getIkasProductCustomerReviewForm,\n setCustomerReviewFormTitle,\n setCustomerReviewFormStar,\n setCustomerReviewFormComment,\n submitCustomerReviewForm,\n isCustomerReviewLoginRequired,\n getIkasCustomerReviewFormattedDate,\n customerStore,\n hasCustomer,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductReviewsSection({\n product,\n title = \"Customer Reviews\",\n}: Props) {\n const [showForm, setShowForm] = useState(false);\n\n if (!product) return null;\n\n const reviews = getProductCustomerReviews(product) ?? [];\n const reviewForm = getIkasProductCustomerReviewForm(product);\n const loginRequired = isCustomerReviewLoginRequired() as unknown as boolean;\n const isLoggedIn = hasCustomer(customerStore) as unknown as boolean;\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitCustomerReviewForm(reviewForm);\n if (success) setShowForm(false);\n };\n\n return (\n <section className=\"reviews-section\">\n <div className=\"reviews-inner\">\n <div className=\"reviews-header\">\n <h2 className=\"reviews-title\">{title} ({reviews.length})</h2>\n {!showForm && (\n <button className=\"reviews-write-btn\" onClick={() => {\n if (loginRequired && !isLoggedIn) Router.navigateToPage(\"LOGIN\");\n else setShowForm(true);\n }}>Write a Review</button>\n )}\n </div>\n {showForm && (\n <form className=\"review-form\" onSubmit={handleSubmit}>\n <div className=\"review-form-stars\">\n <span className=\"review-form-label\">Rating</span>\n <div className=\"star-input\">\n {[1, 2, 3, 4, 5].map((star) => (\n <button key={star} type=\"button\" className={star <= reviewForm.star.value ? \"star-filled\" : \"star-empty\"} onClick={() => setCustomerReviewFormStar(reviewForm, star)}>\\u2605</button>\n ))}\n </div>\n </div>\n <div className=\"review-form-field\">\n <label className=\"review-form-label\">{reviewForm.title.label}</label>\n <input className=\"review-form-input\" type=\"text\" placeholder={reviewForm.title.placeholder} value={reviewForm.title.value} onInput={(e) => setCustomerReviewFormTitle(reviewForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"review-form-field\">\n <label className=\"review-form-label\">{reviewForm.comment.label}</label>\n <textarea className=\"review-form-textarea\" placeholder={reviewForm.comment.placeholder} value={reviewForm.comment.value} rows={4} onInput={(e) => setCustomerReviewFormComment(reviewForm, (e.target as HTMLTextAreaElement).value)} />\n </div>\n <div className=\"review-form-actions\">\n <button type=\"submit\" className=\"review-form-submit\" disabled={reviewForm.isSubmitting}>{reviewForm.isSubmitting ? \"Submitting...\" : \"Submit Review\"}</button>\n <button type=\"button\" className=\"review-form-cancel\" onClick={() => setShowForm(false)}>Cancel</button>\n </div>\n </form>\n )}\n {reviews.length === 0 && !showForm && <p className=\"reviews-empty\">No reviews yet. Be the first to review!</p>}\n <div className=\"review-list\">\n {reviews.map((review) => (\n <div key={review.id} className=\"review-card\">\n <div className=\"review-card-header\">\n <div className=\"reviews-stars\">\n {[1, 2, 3, 4, 5].map((s) => <span key={s} className={s <= review.star ? \"star-filled\" : \"star-empty\"}>\\u2605</span>)}\n </div>\n <span className=\"review-card-date\">{getIkasCustomerReviewFormattedDate(review)}</span>\n </div>\n <h4 className=\"review-card-title\">{review.title}</h4>\n <p className=\"review-card-comment\">{review.comment}</p>\n <span className=\"review-card-author\">{review.customerName}</span>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n",
167
+ "index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n getProductCustomerReviews,\n getIkasProductCustomerReviewForm,\n setCustomerReviewFormTitle,\n setCustomerReviewFormStar,\n setCustomerReviewFormComment,\n submitCustomerReviewForm,\n isCustomerReviewLoginRequired,\n getIkasCustomerReviewFormattedDate,\n customerStore,\n hasCustomer,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function ProductReviewsSection({\n product,\n title = \"Customer Reviews\",\n}: Props) {\n const [showForm, setShowForm] = useState(false);\n\n if (!product) return null;\n\n const reviews = getProductCustomerReviews(product) ?? [];\n const reviewForm = getIkasProductCustomerReviewForm(product);\n const loginRequired = isCustomerReviewLoginRequired() as unknown as boolean;\n const isLoggedIn = hasCustomer(customerStore) as unknown as boolean;\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitCustomerReviewForm(reviewForm);\n if (success) setShowForm(false);\n };\n\n return (\n <section className=\"reviews-section\">\n <div className=\"reviews-inner\">\n <div className=\"reviews-header\">\n <h2 className=\"reviews-title\">{title} ({reviews.length})</h2>\n {!showForm && (\n <button className=\"reviews-write-btn\" onClick={() => {\n if (loginRequired && !isLoggedIn) Router.navigateToPage(\"LOGIN\");\n else setShowForm(true);\n }}>Write a Review</button>\n )}\n </div>\n {showForm && (\n <form className=\"review-form\" onSubmit={handleSubmit}>\n <div className=\"review-form-stars\">\n <span className=\"review-form-label\">Rating</span>\n <div className=\"star-input\">\n {[1, 2, 3, 4, 5].map((star) => (\n <button key={star} type=\"button\" className={star <= reviewForm.star.value ? \"star-filled\" : \"star-empty\"} onClick={() => setCustomerReviewFormStar(reviewForm, star)}>\\u2605</button>\n ))}\n </div>\n </div>\n <div className=\"review-form-field\">\n <label className=\"review-form-label\">{reviewForm.title.label}</label>\n <input className=\"review-form-input\" type=\"text\" placeholder={reviewForm.title.placeholder} value={reviewForm.title.value} onInput={(e) => setCustomerReviewFormTitle(reviewForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"review-form-field\">\n <label className=\"review-form-label\">{reviewForm.comment.label}</label>\n <textarea className=\"review-form-textarea\" placeholder={reviewForm.comment.placeholder} value={reviewForm.comment.value} rows={4} onInput={(e) => setCustomerReviewFormComment(reviewForm, (e.target as HTMLTextAreaElement).value)} />\n </div>\n <div className=\"review-form-actions\">\n <button type=\"submit\" className=\"review-form-submit\" disabled={reviewForm.isSubmitting}>{reviewForm.isSubmitting ? \"Submitting...\" : \"Submit Review\"}</button>\n <button type=\"button\" className=\"review-form-cancel\" onClick={() => setShowForm(false)}>Cancel</button>\n </div>\n </form>\n )}\n {reviews.length === 0 && !showForm && <p className=\"reviews-empty\">No reviews yet. Be the first to review!</p>}\n <div className=\"review-list\">\n {reviews.map((review) => (\n <div key={review.id} className=\"review-card\">\n <div className=\"review-card-header\">\n <div className=\"reviews-stars\">\n {[1, 2, 3, 4, 5].map((s) => <span key={s} className={s <= review.star ? \"star-filled\" : \"star-empty\"}>\\u2605</span>)}\n </div>\n <span className=\"review-card-date\">{getIkasCustomerReviewFormattedDate(review)}</span>\n </div>\n <h4 className=\"review-card-title\">{review.title}</h4>\n <p className=\"review-card-comment\">{review.comment}</p>\n <span className=\"review-card-author\">{review.customerName}</span>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n",
168
168
  "types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n title?: string;\n}\n",
169
169
  "styles.css": ".reviews-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.reviews-inner {\n max-width: 800px;\n margin: 0 auto;\n}\n\n.reviews-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }\n.reviews-title { font-size: 22px; font-weight: 700; color: #111; margin: 0; }\n.reviews-write-btn { padding: 10px 20px; font-size: 14px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 6px; cursor: pointer; }\n.reviews-empty { font-size: 16px; color: #666; text-align: center; padding: 32px 0; }\n\n.review-form { padding: 24px; border: 1px solid #eee; border-radius: 8px; margin-bottom: 24px; display: flex; flex-direction: column; gap: 16px; }\n.review-form-label { font-size: 14px; font-weight: 600; color: #333; }\n.review-form-input { padding: 10px 14px; border: 1.5px solid #ddd; border-radius: 6px; font-size: 14px; outline: none; }\n.review-form-textarea { padding: 10px 14px; border: 1.5px solid #ddd; border-radius: 6px; font-size: 14px; outline: none; resize: vertical; }\n.review-form-field { display: flex; flex-direction: column; gap: 6px; }\n.review-form-stars { display: flex; align-items: center; gap: 12px; }\n.star-input { display: flex; gap: 4px; }\n.star-input button { background: none; border: none; font-size: 24px; cursor: pointer; padding: 0; }\n.star-filled { color: #f59e0b; }\n.star-empty { color: #ddd; }\n.review-form-actions { display: flex; gap: 12px; }\n.review-form-submit { padding: 10px 20px; font-size: 14px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 6px; cursor: pointer; }\n.review-form-submit:disabled { background: #ccc; }\n.review-form-cancel { padding: 10px 20px; font-size: 14px; color: #666; background: none; border: 1px solid #ddd; border-radius: 6px; cursor: pointer; }\n\n.review-list { display: flex; flex-direction: column; gap: 16px; }\n.review-card { padding: 20px; border-bottom: 1px solid #eee; }\n.review-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }\n.reviews-stars { display: flex; gap: 2px; font-size: 16px; }\n.review-card-date { font-size: 13px; color: #999; }\n.review-card-title { font-size: 16px; font-weight: 600; color: #111; margin: 0 0 6px 0; }\n.review-card-comment { font-size: 14px; color: #555; line-height: 1.6; margin: 0 0 8px 0; }\n.review-card-author { font-size: 13px; color: #999; }\n",
170
170
  "ikas-config-snippet.json": "{\n \"id\": \"product-reviews\",\n \"name\": \"Product Reviews\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"product\", \"displayName\": \"Product\", \"type\": \"PRODUCT\", \"required\": true },\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"Customer Reviews\" }\n ]\n}\n"
@@ -184,7 +184,7 @@
184
184
  "title": "Reset Password Section",
185
185
  "description": "Password reset form with new password and confirm fields, success/error states",
186
186
  "files": {
187
- "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getRecoverPasswordForm,\n initRecoverPasswordForm,\n setRecoverPasswordFormPassword,\n setRecoverPasswordFormPasswordAgain,\n submitRecoverPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ResetPasswordSection({\n successMessage = \"Password has been reset successfully.\",\n}: Props) {\n const recoverForm = getRecoverPasswordForm(customerStore);\n\n useEffect(() => {\n initRecoverPasswordForm(recoverForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitRecoverPasswordForm(recoverForm);\n if (success) Router.navigateToPage(\"LOGIN\");\n };\n\n return (\n <section className=\"reset-section\">\n <div className=\"reset-inner\">\n <h1 className=\"reset-title\">Set New Password</h1>\n {recoverForm.isSuccess && <div className=\"reset-success-banner\">{successMessage}</div>}\n {recoverForm.isFailure && recoverForm.responseMessage && (\n <div className=\"reset-error-banner\">{recoverForm.responseMessage}</div>\n )}\n {!recoverForm.isSuccess && (\n <form className=\"reset-form\" onSubmit={handleSubmit}>\n <div className=\"reset-field\">\n <label className=\"reset-label\">{recoverForm.password.label}</label>\n <input\n className={`reset-input ${recoverForm.password.hasError ? \"has-error\" : \"\"}`}\n type=\"password\"\n placeholder={recoverForm.password.placeholder}\n value={recoverForm.password.value}\n onInput={(e) => setRecoverPasswordFormPassword(recoverForm, (e.target as HTMLInputElement).value)}\n />\n {recoverForm.password.hasError && recoverForm.password.message && (\n <span className=\"reset-field-error\">{recoverForm.password.message}</span>\n )}\n </div>\n <div className=\"reset-field\">\n <label className=\"reset-label\">{recoverForm.passwordAgain.label}</label>\n <input\n className={`reset-input ${recoverForm.passwordAgain.hasError ? \"has-error\" : \"\"}`}\n type=\"password\"\n placeholder={recoverForm.passwordAgain.placeholder}\n value={recoverForm.passwordAgain.value}\n onInput={(e) => setRecoverPasswordFormPasswordAgain(recoverForm, (e.target as HTMLInputElement).value)}\n />\n {recoverForm.passwordAgain.hasError && recoverForm.passwordAgain.message && (\n <span className=\"reset-field-error\">{recoverForm.passwordAgain.message}</span>\n )}\n </div>\n <button className=\"reset-submit-btn\" type=\"submit\" disabled={recoverForm.isSubmitting}>\n {recoverForm.isSubmitting ? \"Resetting...\" : \"Reset Password\"}\n </button>\n </form>\n )}\n <p className=\"reset-back-link\">\n <a href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"LOGIN\"); }}>Back to Sign In</a>\n </p>\n </div>\n </section>\n );\n}\n",
187
+ "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getRecoverPasswordForm,\n initRecoverPasswordForm,\n setRecoverPasswordFormPassword,\n setRecoverPasswordFormPasswordAgain,\n submitRecoverPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function ResetPasswordSection({\n successMessage = \"Password has been reset successfully.\",\n}: Props) {\n const recoverForm = getRecoverPasswordForm(customerStore);\n\n useEffect(() => {\n initRecoverPasswordForm(recoverForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitRecoverPasswordForm(recoverForm);\n if (success) Router.navigateToPage(\"LOGIN\");\n };\n\n return (\n <section className=\"reset-section\">\n <div className=\"reset-inner\">\n <h1 className=\"reset-title\">Set New Password</h1>\n {recoverForm.isSuccess && <div className=\"reset-success-banner\">{successMessage}</div>}\n {recoverForm.isFailure && recoverForm.responseMessage && (\n <div className=\"reset-error-banner\">{recoverForm.responseMessage}</div>\n )}\n {!recoverForm.isSuccess && (\n <form className=\"reset-form\" onSubmit={handleSubmit}>\n <div className=\"reset-field\">\n <label className=\"reset-label\">{recoverForm.password.label}</label>\n <input\n className={`reset-input ${recoverForm.password.hasError ? \"has-error\" : \"\"}`}\n type=\"password\"\n placeholder={recoverForm.password.placeholder}\n value={recoverForm.password.value}\n onInput={(e) => setRecoverPasswordFormPassword(recoverForm, (e.target as HTMLInputElement).value)}\n />\n {recoverForm.password.hasError && recoverForm.password.message && (\n <span className=\"reset-field-error\">{recoverForm.password.message}</span>\n )}\n </div>\n <div className=\"reset-field\">\n <label className=\"reset-label\">{recoverForm.passwordAgain.label}</label>\n <input\n className={`reset-input ${recoverForm.passwordAgain.hasError ? \"has-error\" : \"\"}`}\n type=\"password\"\n placeholder={recoverForm.passwordAgain.placeholder}\n value={recoverForm.passwordAgain.value}\n onInput={(e) => setRecoverPasswordFormPasswordAgain(recoverForm, (e.target as HTMLInputElement).value)}\n />\n {recoverForm.passwordAgain.hasError && recoverForm.passwordAgain.message && (\n <span className=\"reset-field-error\">{recoverForm.passwordAgain.message}</span>\n )}\n </div>\n <button className=\"reset-submit-btn\" type=\"submit\" disabled={recoverForm.isSubmitting}>\n {recoverForm.isSubmitting ? \"Resetting...\" : \"Reset Password\"}\n </button>\n </form>\n )}\n <p className=\"reset-back-link\">\n <a href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"LOGIN\"); }}>Back to Sign In</a>\n </p>\n </div>\n </section>\n );\n}\n",
188
188
  "types.ts": "export interface Props {\n successMessage?: string;\n}\n",
189
189
  "styles.css": ".reset-section {\n width: 100%;\n padding: 64px 24px;\n}\n\n.reset-inner {\n max-width: 400px;\n margin: 0 auto;\n}\n\n.reset-title { font-size: 28px; font-weight: 700; color: #111; margin: 0 0 24px 0; text-align: center; }\n.reset-success-banner { padding: 12px 16px; font-size: 14px; color: #1b5e20; background: #e8f5e9; border-radius: 8px; margin-bottom: 20px; }\n.reset-error-banner { padding: 12px 16px; font-size: 14px; color: #b71c1c; background: #ffebee; border-radius: 8px; margin-bottom: 20px; }\n.reset-form { display: flex; flex-direction: column; gap: 16px; }\n.reset-field { display: flex; flex-direction: column; gap: 6px; }\n.reset-label { font-size: 14px; font-weight: 600; color: #333; }\n.reset-input { padding: 12px 14px; font-size: 15px; border: 1.5px solid #ddd; border-radius: 8px; outline: none; }\n.reset-input:focus { border-color: #111; }\n.reset-input.has-error { border-color: #e53935; }\n.reset-field-error { font-size: 12px; color: #e53935; }\n.reset-submit-btn { padding: 14px 24px; font-size: 16px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; margin-top: 8px; }\n.reset-submit-btn:disabled { background: #ccc; cursor: not-allowed; }\n.reset-back-link { font-size: 14px; color: #666; text-align: center; margin-top: 24px; }\n.reset-back-link a { color: #111; font-weight: 600; text-decoration: none; }\n",
190
190
  "ikas-config-snippet.json": "{\n \"id\": \"reset-password\",\n \"name\": \"Reset Password\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"successMessage\", \"displayName\": \"Success Message\", \"type\": \"TEXT\", \"defaultValue\": \"Password has been reset successfully.\" }\n ]\n}\n"
@@ -194,7 +194,7 @@
194
194
  "title": "Account Info Section",
195
195
  "description": "Account information edit form with first name, last name, and phone",
196
196
  "files": {
197
- "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getAccountInfoForm,\n initAccountInfoForm,\n setAccountInfoFormFirstName,\n setAccountInfoFormLastName,\n setAccountInfoFormPhone,\n submitAccountInfoForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountInfoSection({\n title = \"Account Information\",\n}: Props) {\n const accountForm = getAccountInfoForm(customerStore);\n\n useEffect(() => {\n initAccountInfoForm(accountForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitAccountInfoForm(accountForm);\n };\n\n return (\n <section className=\"account-info-section\">\n <div className=\"account-info-inner\">\n <h1 className=\"account-info-title\">{title}</h1>\n {accountForm.isSuccess && <div className=\"account-info-success\">Your information has been updated.</div>}\n {accountForm.isFailure && accountForm.responseMessage && (\n <div className=\"account-info-error\">{accountForm.responseMessage}</div>\n )}\n <form className=\"account-info-form\" onSubmit={handleSubmit}>\n <div className=\"account-info-row\">\n <div className=\"account-info-field\">\n <label className=\"account-info-label\">{accountForm.firstName.label}</label>\n <input\n className={`account-info-input ${accountForm.firstName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n value={accountForm.firstName.value}\n onInput={(e) => setAccountInfoFormFirstName(accountForm, (e.target as HTMLInputElement).value)}\n />\n {accountForm.firstName.hasError && accountForm.firstName.message && (\n <span className=\"account-info-field-error\">{accountForm.firstName.message}</span>\n )}\n </div>\n <div className=\"account-info-field\">\n <label className=\"account-info-label\">{accountForm.lastName.label}</label>\n <input\n className={`account-info-input ${accountForm.lastName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n value={accountForm.lastName.value}\n onInput={(e) => setAccountInfoFormLastName(accountForm, (e.target as HTMLInputElement).value)}\n />\n {accountForm.lastName.hasError && accountForm.lastName.message && (\n <span className=\"account-info-field-error\">{accountForm.lastName.message}</span>\n )}\n </div>\n </div>\n <div className=\"account-info-field\">\n <label className=\"account-info-label\">{accountForm.phone.label}</label>\n <input\n className={`account-info-input ${accountForm.phone.hasError ? \"has-error\" : \"\"}`}\n type=\"tel\"\n value={accountForm.phone.value}\n onInput={(e) => setAccountInfoFormPhone(accountForm, (e.target as HTMLInputElement).value)}\n />\n {accountForm.phone.hasError && accountForm.phone.message && (\n <span className=\"account-info-field-error\">{accountForm.phone.message}</span>\n )}\n </div>\n <button className=\"account-info-submit\" type=\"submit\" disabled={accountForm.isSubmitting}>\n {accountForm.isSubmitting ? \"Saving...\" : \"Save Changes\"}\n </button>\n </form>\n </div>\n </section>\n );\n}\n",
197
+ "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getAccountInfoForm,\n initAccountInfoForm,\n setAccountInfoFormFirstName,\n setAccountInfoFormLastName,\n setAccountInfoFormPhone,\n submitAccountInfoForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function AccountInfoSection({\n title = \"Account Information\",\n}: Props) {\n const accountForm = getAccountInfoForm(customerStore);\n\n useEffect(() => {\n initAccountInfoForm(accountForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitAccountInfoForm(accountForm);\n };\n\n return (\n <section className=\"account-info-section\">\n <div className=\"account-info-inner\">\n <h1 className=\"account-info-title\">{title}</h1>\n {accountForm.isSuccess && <div className=\"account-info-success\">Your information has been updated.</div>}\n {accountForm.isFailure && accountForm.responseMessage && (\n <div className=\"account-info-error\">{accountForm.responseMessage}</div>\n )}\n <form className=\"account-info-form\" onSubmit={handleSubmit}>\n <div className=\"account-info-row\">\n <div className=\"account-info-field\">\n <label className=\"account-info-label\">{accountForm.firstName.label}</label>\n <input\n className={`account-info-input ${accountForm.firstName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n value={accountForm.firstName.value}\n onInput={(e) => setAccountInfoFormFirstName(accountForm, (e.target as HTMLInputElement).value)}\n />\n {accountForm.firstName.hasError && accountForm.firstName.message && (\n <span className=\"account-info-field-error\">{accountForm.firstName.message}</span>\n )}\n </div>\n <div className=\"account-info-field\">\n <label className=\"account-info-label\">{accountForm.lastName.label}</label>\n <input\n className={`account-info-input ${accountForm.lastName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n value={accountForm.lastName.value}\n onInput={(e) => setAccountInfoFormLastName(accountForm, (e.target as HTMLInputElement).value)}\n />\n {accountForm.lastName.hasError && accountForm.lastName.message && (\n <span className=\"account-info-field-error\">{accountForm.lastName.message}</span>\n )}\n </div>\n </div>\n <div className=\"account-info-field\">\n <label className=\"account-info-label\">{accountForm.phone.label}</label>\n <input\n className={`account-info-input ${accountForm.phone.hasError ? \"has-error\" : \"\"}`}\n type=\"tel\"\n value={accountForm.phone.value}\n onInput={(e) => setAccountInfoFormPhone(accountForm, (e.target as HTMLInputElement).value)}\n />\n {accountForm.phone.hasError && accountForm.phone.message && (\n <span className=\"account-info-field-error\">{accountForm.phone.message}</span>\n )}\n </div>\n <button className=\"account-info-submit\" type=\"submit\" disabled={accountForm.isSubmitting}>\n {accountForm.isSubmitting ? \"Saving...\" : \"Save Changes\"}\n </button>\n </form>\n </div>\n </section>\n );\n}\n",
198
198
  "types.ts": "export interface Props {\n title?: string;\n}\n",
199
199
  "styles.css": ".account-info-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.account-info-inner {\n max-width: 480px;\n margin: 0 auto;\n}\n\n.account-info-title { font-size: 24px; font-weight: 700; color: #111; margin: 0 0 24px 0; }\n.account-info-success { padding: 12px 16px; font-size: 14px; color: #1b5e20; background: #e8f5e9; border-radius: 8px; margin-bottom: 20px; }\n.account-info-error { padding: 12px 16px; font-size: 14px; color: #b71c1c; background: #ffebee; border-radius: 8px; margin-bottom: 20px; }\n.account-info-form { display: flex; flex-direction: column; gap: 16px; }\n.account-info-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }\n.account-info-field { display: flex; flex-direction: column; gap: 6px; }\n.account-info-label { font-size: 14px; font-weight: 600; color: #333; }\n.account-info-input { padding: 12px 14px; font-size: 15px; border: 1.5px solid #ddd; border-radius: 8px; outline: none; }\n.account-info-input:focus { border-color: #111; }\n.account-info-input.has-error { border-color: #e53935; }\n.account-info-field-error { font-size: 12px; color: #e53935; }\n.account-info-submit { padding: 14px 24px; font-size: 16px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; margin-top: 8px; }\n.account-info-submit:disabled { background: #ccc; cursor: not-allowed; }\n\n@media (max-width: 480px) {\n .account-info-row { grid-template-columns: 1fr; }\n}\n",
200
200
  "ikas-config-snippet.json": "{\n \"id\": \"account-info\",\n \"name\": \"Account Info\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"Account Information\" }\n ]\n}\n"
@@ -204,7 +204,7 @@
204
204
  "title": "Order Detail Section",
205
205
  "description": "Single order detail page with line items, adjustments, transactions, and totals",
206
206
  "files": {
207
- "index.tsx": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderDisplayedPackages,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderLineVariantMainImage,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getDefaultSrc,\n Router,\n IkasOrder,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function OrderDetailSection(_props: Props) {\n const [order, setOrder] = useState<IkasOrder | null>(null);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n const params = Router.getPageParams();\n const orderId = params.id;\n if (orderId) {\n getOrder(customerStore, orderId).then((o) => {\n setOrder(o);\n setLoading(false);\n });\n }\n }, []);\n\n if (loading) return <section className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>Loading...</p></div></section>;\n if (!order) return <section className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>Order not found.</p></div></section>;\n\n const packages = getIkasOrderDisplayedPackages(order);\n const lineItems = order.orderLineItems ?? [];\n const adjustments = order.orderAdjustments ?? [];\n\n return (\n <section className=\"order-detail-section\">\n <div className=\"order-detail-inner\">\n <h1 className=\"order-detail-title\">Order #{order.orderNumber}</h1>\n <p className=\"order-detail-date\">{getIkasOrderFormattedOrderedAt(order)}</p>\n {packages.map((pkg, i) => (\n <span key={i} className=\"order-status-badge\">{getIkasOrderPackageStatusTranslation(order)}</span>\n ))}\n <div className=\"order-items\">\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n return (\n <div key={item.id} className=\"order-item\">\n {image && <img className=\"order-item-image\" src={getDefaultSrc(image)} alt=\"\" />}\n <div className=\"order-item-info\">\n <span className=\"order-item-name\">{item.variant?.name}</span>\n <span className=\"order-item-qty\">x{item.quantity}</span>\n <span className=\"order-item-price\">{getOrderLineItemFormattedFinalPriceWithQuantity(item)}</span>\n </div>\n </div>\n );\n })}\n </div>\n {adjustments.length > 0 && (\n <div className=\"order-adjustments\">\n {adjustments.map((adj: any, i: number) => (\n <div key={i} className=\"order-adjustment-row\">\n <span>{getOrderAdjustmentDisplayName(adj)}</span>\n <span>{getOrderAdjustmentFormattedAmount(adj)}</span>\n </div>\n ))}\n </div>\n )}\n <div className=\"order-total-row\">\n <span>Total</span>\n <span className=\"order-total-value\">{getIkasOrderFormattedTotalFinalPrice(order)}</span>\n </div>\n </div>\n </section>\n );\n}\n",
207
+ "index.tsx": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderDisplayedPackages,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderLineVariantMainImage,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getDefaultSrc,\n Router,\n IkasOrder,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function OrderDetailSection(_props: Props) {\n const [order, setOrder] = useState<IkasOrder | null>(null);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n const params = Router.getPageParams();\n const orderId = params.id;\n if (orderId) {\n getOrder(customerStore, orderId).then((o) => {\n setOrder(o);\n setLoading(false);\n });\n }\n }, []);\n\n if (loading) return <section className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>Loading...</p></div></section>;\n if (!order) return <section className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>Order not found.</p></div></section>;\n\n const packages = getIkasOrderDisplayedPackages(order);\n const lineItems = order.orderLineItems ?? [];\n const adjustments = order.orderAdjustments ?? [];\n\n return (\n <section className=\"order-detail-section\">\n <div className=\"order-detail-inner\">\n <h1 className=\"order-detail-title\">Order #{order.orderNumber}</h1>\n <p className=\"order-detail-date\">{getIkasOrderFormattedOrderedAt(order)}</p>\n {packages.map((pkg, i) => (\n <span key={i} className=\"order-status-badge\">{getIkasOrderPackageStatusTranslation(order)}</span>\n ))}\n <div className=\"order-items\">\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n return (\n <div key={item.id} className=\"order-item\">\n {image && <img className=\"order-item-image\" src={getDefaultSrc(image)} alt=\"\" />}\n <div className=\"order-item-info\">\n <span className=\"order-item-name\">{item.variant?.name}</span>\n <span className=\"order-item-qty\">x{item.quantity}</span>\n <span className=\"order-item-price\">{getOrderLineItemFormattedFinalPriceWithQuantity(item)}</span>\n </div>\n </div>\n );\n })}\n </div>\n {adjustments.length > 0 && (\n <div className=\"order-adjustments\">\n {adjustments.map((adj: any, i: number) => (\n <div key={i} className=\"order-adjustment-row\">\n <span>{getOrderAdjustmentDisplayName(adj)}</span>\n <span>{getOrderAdjustmentFormattedAmount(adj)}</span>\n </div>\n ))}\n </div>\n )}\n <div className=\"order-total-row\">\n <span>Total</span>\n <span className=\"order-total-value\">{getIkasOrderFormattedTotalFinalPrice(order)}</span>\n </div>\n </div>\n </section>\n );\n}\n",
208
208
  "types.ts": "export interface Props {}\n",
209
209
  "styles.css": ".order-detail-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.order-detail-inner {\n max-width: 800px;\n margin: 0 auto;\n}\n\n.order-detail-title { font-size: 24px; font-weight: 700; color: #111; margin: 0 0 4px 0; }\n.order-detail-date { font-size: 14px; color: #999; margin: 0 0 16px 0; }\n.order-status-badge { display: inline-block; font-size: 13px; font-weight: 500; color: #1976d2; background: #e3f2fd; padding: 4px 10px; border-radius: 12px; margin-bottom: 24px; }\n\n.order-items { display: flex; flex-direction: column; gap: 12px; margin-bottom: 24px; }\n.order-item { display: flex; align-items: center; gap: 12px; padding: 12px; border: 1px solid #eee; border-radius: 8px; }\n.order-item-image { width: 64px; height: 64px; object-fit: cover; border-radius: 4px; background: #f5f5f5; }\n.order-item-info { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }\n.order-item-name { font-size: 14px; font-weight: 600; color: #111; }\n.order-item-qty { font-size: 13px; color: #666; }\n.order-item-price { font-size: 14px; font-weight: 600; color: #111; }\n\n.order-adjustments { margin-bottom: 16px; }\n.order-adjustment-row { display: flex; justify-content: space-between; font-size: 14px; color: #555; padding: 4px 0; }\n\n.order-total-row { display: flex; justify-content: space-between; border-top: 1px solid #eee; padding-top: 16px; font-size: 18px; font-weight: 700; color: #111; }\n",
210
210
  "ikas-config-snippet.json": "{\n \"id\": \"order-detail\",\n \"name\": \"Order Detail\",\n \"type\": \"section\",\n \"props\": []\n}\n"