@ikas/code-components-mcp 0.33.0 → 0.35.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.
@@ -1,217 +1,213 @@
1
1
  {
2
2
  "templates": {
3
- "404": {
3
+ "not-found": {
4
4
  "title": "404 Page Section",
5
- "description": "Page not found section with message and navigation back to home",
5
+ "description": "Page not found section with message and navigation back to home.",
6
6
  "files": {
7
- "index.tsx": "import { Router } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function NotFoundSection({\n heading = \"Page Not Found\",\n message = \"The page you're looking for doesn't exist or has been moved.\",\n buttonText = \"Back to Home\",\n}: Props) {\n return (\n <section className=\"not-found-section\">\n <div className=\"not-found-inner\">\n <span className=\"not-found-code\">404</span>\n <h1 className=\"not-found-heading\">{heading}</h1>\n <p className=\"not-found-message\">{message}</p>\n <button\n className=\"not-found-btn\"\n onClick={() => Router.navigate(\"/\")}\n >\n {buttonText}\n </button>\n </div>\n </section>\n );\n}\n",
8
- "types.ts": "export interface Props {\n heading?: string;\n message?: string;\n buttonText?: string;\n}\n",
7
+ "index.tsx": "import { Router } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function NotFoundSection({\n heading = \"Page Not Found\",\n message = \"The page you're looking for doesn't exist or has been moved.\",\n buttonText = \"Back to Home\",\n backgroundColor = \"#ffffff\",\n}: Props) {\n return (\n <section className=\"not-found-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"not-found-inner\">\n <span className=\"not-found-code\">404</span>\n <h1 className=\"not-found-heading\">{heading}</h1>\n <p className=\"not-found-message\">{message}</p>\n <button\n className=\"not-found-btn\"\n onClick={() => Router.navigate(\"/\")}\n >\n {buttonText}\n </button>\n </div>\n </section>\n );\n}\n\nexport default NotFoundSection;\n",
8
+ "types.ts": "export interface Props {\n heading?: string;\n message?: string;\n buttonText?: string;\n backgroundColor?: string;\n}\n",
9
9
  "styles.css": ".not-found-section {\n width: 100%;\n padding: 80px 24px;\n text-align: center;\n}\n\n.not-found-inner {\n max-width: 480px;\n margin: 0 auto;\n}\n\n.not-found-code {\n font-size: 96px;\n font-weight: 800;\n color: #eee;\n line-height: 1;\n display: block;\n margin-bottom: 16px;\n}\n\n.not-found-heading {\n font-size: 28px;\n font-weight: 700;\n color: #111;\n margin: 0 0 12px 0;\n}\n\n.not-found-message {\n font-size: 16px;\n color: #666;\n margin: 0 0 32px 0;\n line-height: 1.5;\n}\n\n.not-found-btn {\n display: inline-block;\n padding: 14px 32px;\n font-size: 16px;\n font-weight: 600;\n color: #fff;\n background: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n}\n\n.not-found-btn:hover {\n background: #333;\n}\n",
10
- "ikas-config-snippet.json": "{\n \"id\": \"not-found\",\n \"name\": \"404 Page\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"heading\", \"displayName\": \"Heading\", \"type\": \"TEXT\", \"defaultValue\": \"Page Not Found\" },\n { \"name\": \"message\", \"displayName\": \"Message\", \"type\": \"TEXT\", \"defaultValue\": \"The page you're looking for doesn't exist or has been moved.\" },\n { \"name\": \"buttonText\", \"displayName\": \"Button Text\", \"type\": \"TEXT\", \"defaultValue\": \"Back to Home\" }\n ]\n}\n"
10
+ "ikas-config-snippet.json": "{\n \"id\": \"not-found\",\n \"name\": \"404 Page\",\n \"type\": \"section\",\n \"props\": [\n {\n \"name\": \"heading\",\n \"displayName\": \"Heading\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Page Not Found\"\n },\n {\n \"name\": \"message\",\n \"displayName\": \"Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"The page you're looking for doesn't exist or has been moved.\"\n },\n {\n \"name\": \"buttonText\",\n \"displayName\": \"Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Back to Home\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
11
11
  }
12
12
  },
13
- "header": {
14
- "title": "Header Section",
15
- "description": "Site header with logo, navigation links, cart/account icons, and mobile menu",
13
+ "account-addresses": {
14
+ "title": "Account Addresses Section",
15
+ "description": "Complete address management with list display, add/edit form (getIkasCustomerAddressForm/initAddressForm/setAddressForm*/submitAddressForm), and delete (deleteCustomerAddress). Uses isEmpty/isNotEmpty.",
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\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
- "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
- "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
- "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"
17
+ "index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n customerStore,\n getIkasCustomerAddressForm,\n initAddressForm,\n submitAddressForm,\n deleteCustomerAddress,\n setAddressFormFirstName,\n setAddressFormLastName,\n setAddressFormPhone,\n setAddressFormAddressLine1,\n setAddressFormAddressLine2,\n setAddressFormCity,\n setAddressFormDistrict,\n setAddressFormCountry,\n setAddressFormPostalCode,\n setAddressFormTitle,\n isEmpty,\n isNotEmpty,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountAddressesSection({ backgroundColor = \"#ffffff\" }: Props) {\n const [showForm, setShowForm] = useState(false);\n const [editingAddress, setEditingAddress] = useState<any>(null);\n const addresses = customerStore.customer?.addresses ?? [];\n\n const handleAddNew = () => {\n setEditingAddress(null);\n setShowForm(true);\n };\n\n const handleEdit = (address: any) => {\n setEditingAddress(address);\n setShowForm(true);\n };\n\n const handleDelete = async (address: any) => {\n await deleteCustomerAddress(customerStore, address);\n };\n\n return (\n <section className=\"addresses-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"addresses-inner\">\n <div className=\"addresses-header\">\n <h1 className=\"addresses-title\">My Addresses</h1>\n <button className=\"addresses-add-btn\" onClick={handleAddNew}>Add Address</button>\n </div>\n\n {showForm && <AddressFormComponent address={editingAddress} onDone={() => setShowForm(false)} />}\n\n {isEmpty(addresses) && !showForm && (\n <p className=\"addresses-empty\">No addresses saved yet.</p>\n )}\n\n {isNotEmpty(addresses) && (\n <div className=\"addresses-grid\">\n {addresses.map((addr: any) => (\n <div key={addr.id} className=\"address-card\">\n {addr.title && <h3 className=\"address-card-title\">{addr.title}</h3>}\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.postalCode}</p>\n <p className=\"address-phone\">{addr.phone}</p>\n <div className=\"address-actions\">\n <button onClick={() => handleEdit(addr)}>Edit</button>\n <button className=\"address-delete-btn\" onClick={() => handleDelete(addr)}>Delete</button>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </section>\n );\n}\n\nfunction AddressFormComponent({ address, onDone }: { address?: any; onDone: () => void }) {\n const addressForm = address\n ? getIkasCustomerAddressForm(address)\n : getIkasCustomerAddressForm({} as any);\n\n const handleInit = async () => {\n await initAddressForm(addressForm, address);\n };\n\n useState(() => { handleInit(); });\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitAddressForm(addressForm);\n if (success) onDone();\n };\n\n return (\n <form className=\"address-form\" onSubmit={handleSubmit}>\n <div className=\"address-form-field\">\n <label>Title</label>\n <input value={addressForm.title?.value ?? \"\"} onInput={(e) => setAddressFormTitle(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"address-form-row\">\n <div className=\"address-form-field\">\n <label>First Name</label>\n <input value={addressForm.firstName?.value ?? \"\"} onInput={(e) => setAddressFormFirstName(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"address-form-field\">\n <label>Last Name</label>\n <input value={addressForm.lastName?.value ?? \"\"} onInput={(e) => setAddressFormLastName(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n </div>\n <div className=\"address-form-field\">\n <label>Phone</label>\n <input value={addressForm.phone?.value ?? \"\"} onInput={(e) => setAddressFormPhone(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"address-form-field\">\n <label>Address Line 1</label>\n <input value={addressForm.addressLine1?.value ?? \"\"} onInput={(e) => setAddressFormAddressLine1(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"address-form-field\">\n <label>Address Line 2</label>\n <input value={addressForm.addressLine2?.value ?? \"\"} onInput={(e) => setAddressFormAddressLine2(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"address-form-row\">\n <div className=\"address-form-field\">\n <label>City</label>\n <input value={addressForm.city?.value ?? \"\"} onInput={(e) => setAddressFormCity(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"address-form-field\">\n <label>District</label>\n <input value={addressForm.district?.value ?? \"\"} onInput={(e) => setAddressFormDistrict(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n </div>\n <div className=\"address-form-row\">\n <div className=\"address-form-field\">\n <label>Postal Code</label>\n <input value={addressForm.postalCode?.value ?? \"\"} onInput={(e) => setAddressFormPostalCode(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"address-form-field\">\n <label>Country</label>\n <input value={addressForm.country?.value ?? \"\"} onInput={(e) => setAddressFormCountry(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n </div>\n <div className=\"address-form-actions\">\n <button type=\"submit\" disabled={addressForm.isSubmitting}>\n {addressForm.isSubmitting ? \"Saving...\" : \"Save Address\"}\n </button>\n <button type=\"button\" onClick={onDone}>Cancel</button>\n </div>\n </form>\n );\n}\n\n",
18
+ "types.ts": "export interface Props {\n title?: string;\n backgroundColor?: string;\n}\n",
19
+ "styles.css": ".addresses-section { width: 100%; padding: 40px 24px; }\n.addresses-inner { max-width: 800px; margin: 0 auto; }\n.addresses-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }\n.addresses-title { font-size: 24px; font-weight: 700; color: #111; margin: 0; }\n.addresses-add-btn { padding: 10px 20px; font-size: 14px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; }\n.addresses-empty { font-size: 16px; color: #666; text-align: center; padding: 48px 0; }\n.addresses-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }\n.address-card { padding: 20px; border: 1px solid #eee; border-radius: 8px; }\n.address-card-title { font-size: 14px; font-weight: 700; color: #111; margin: 0 0 8px 0; text-transform: uppercase; letter-spacing: 0.5px; }\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-actions { display: flex; gap: 12px; margin-top: 12px; }\n.address-actions button { font-size: 13px; background: none; border: none; cursor: pointer; padding: 0; color: #111; font-weight: 600; }\n.address-delete-btn { color: #e53935 !important; }\n.address-form { display: flex; flex-direction: column; gap: 12px; padding: 20px; border: 1px solid #eee; border-radius: 8px; margin-bottom: 24px; }\n.address-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }\n.address-form-field { display: flex; flex-direction: column; gap: 4px; }\n.address-form-field label { font-size: 13px; font-weight: 600; color: #333; }\n.address-form-field input { padding: 10px 12px; font-size: 14px; border: 1.5px solid #ddd; border-radius: 6px; outline: none; }\n.address-form-field input:focus { border-color: #111; }\n.address-form-actions { display: flex; gap: 12px; margin-top: 8px; }\n.address-form-actions button { padding: 10px 20px; font-size: 14px; font-weight: 600; border-radius: 6px; cursor: pointer; }\n.address-form-actions button[type=\"submit\"] { color: #fff; background: #111; border: none; }\n.address-form-actions button[type=\"button\"] { color: #333; background: #fff; border: 1.5px solid #ddd; }\n@media (max-width: 768px) { .addresses-grid { grid-template-columns: 1fr; } .address-form-row { grid-template-columns: 1fr; } }\n",
20
+ "ikas-config-snippet.json": "{\n \"id\": \"account-addresses\",\n \"name\": \"Account Addresses\",\n \"type\": \"section\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"My Addresses\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
21
21
  }
22
22
  },
23
- "footer": {
24
- "title": "Footer Section",
25
- "description": "Site footer with logo, link columns, and copyright text",
23
+ "account-info": {
24
+ "title": "Account Info Section",
25
+ "description": "Complete account info section with first name, last name, and phone fields. Uses getAccountInfoForm/initAccountInfoForm/setAccountInfoForm*/submitAccountInfoForm pattern.",
26
26
  "files": {
27
- "index.tsx": "import { IkasNavigationLink, getDefaultSrc } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function FooterSection({\n logo,\n description,\n linkColumn1Title = \"Shop\",\n linkColumn1,\n linkColumn2Title = \"Company\",\n linkColumn2,\n copyright = \"All rights reserved.\",\n}: Props) {\n return (\n <footer className=\"footer-section\">\n <div className=\"footer-inner\">\n <div className=\"footer-grid\">\n <div className=\"footer-brand\">\n {logo ? (\n <img src={getDefaultSrc(logo)} alt={logo.altText || \"Logo\"} className=\"footer-logo\" />\n ) : (\n <span className=\"footer-logo-text\">Store</span>\n )}\n {description && <p className=\"footer-description\">{description}</p>}\n </div>\n <div className=\"footer-link-column\">\n <h4 className=\"footer-column-title\">{linkColumn1Title}</h4>\n <nav className=\"footer-links\">\n {linkColumn1?.map((link: IkasNavigationLink, i: number) => (\n <a key={i} href={link.href} className=\"footer-link\" target={link.isTargetBlank ? \"_blank\" : undefined}>\n {link.label}\n </a>\n ))}\n </nav>\n </div>\n <div className=\"footer-link-column\">\n <h4 className=\"footer-column-title\">{linkColumn2Title}</h4>\n <nav className=\"footer-links\">\n {linkColumn2?.map((link: IkasNavigationLink, i: number) => (\n <a key={i} href={link.href} className=\"footer-link\" target={link.isTargetBlank ? \"_blank\" : undefined}>\n {link.label}\n </a>\n ))}\n </nav>\n </div>\n </div>\n <div className=\"footer-bottom\">\n <p className=\"footer-copyright\">{copyright}</p>\n </div>\n </div>\n </footer>\n );\n}\n",
28
- "types.ts": "import { IkasNavigationLink, IkasImage } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n logo?: IkasImage | null;\n description?: string;\n linkColumn1Title?: string;\n linkColumn1?: IkasNavigationLink[];\n linkColumn2Title?: string;\n linkColumn2?: IkasNavigationLink[];\n copyright?: string;\n}\n",
29
- "styles.css": ".footer-section {\n width: 100%;\n background: #111;\n color: #fff;\n padding: 48px 24px 24px;\n}\n\n.footer-inner {\n max-width: 1200px;\n margin: 0 auto;\n}\n\n.footer-grid {\n display: grid;\n grid-template-columns: 2fr 1fr 1fr;\n gap: 48px;\n margin-bottom: 32px;\n}\n\n.footer-logo { height: 36px; width: auto; }\n.footer-logo-text { font-size: 20px; font-weight: 700; }\n.footer-description { font-size: 14px; color: #aaa; margin-top: 12px; line-height: 1.6; }\n\n.footer-column-title { font-size: 14px; font-weight: 600; margin: 0 0 16px 0; text-transform: uppercase; letter-spacing: 0.05em; }\n.footer-links { display: flex; flex-direction: column; gap: 10px; }\n.footer-link { font-size: 14px; color: #aaa; text-decoration: none; }\n.footer-link:hover { color: #fff; }\n\n.footer-bottom { border-top: 1px solid #333; padding-top: 20px; }\n.footer-copyright { font-size: 13px; color: #666; margin: 0; }\n\n@media (max-width: 768px) {\n .footer-grid { grid-template-columns: 1fr; gap: 32px; }\n}\n",
30
- "ikas-config-snippet.json": "{\n \"id\": \"footer\",\n \"name\": \"Footer\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"logo\", \"displayName\": \"Logo\", \"type\": \"IMAGE\" },\n { \"name\": \"description\", \"displayName\": \"Description\", \"type\": \"TEXT\" },\n { \"name\": \"linkColumn1Title\", \"displayName\": \"Column 1 Title\", \"type\": \"TEXT\", \"defaultValue\": \"Shop\" },\n { \"name\": \"linkColumn1\", \"displayName\": \"Column 1 Links\", \"type\": \"LIST_OF_LINK\" },\n { \"name\": \"linkColumn2Title\", \"displayName\": \"Column 2 Title\", \"type\": \"TEXT\", \"defaultValue\": \"Company\" },\n { \"name\": \"linkColumn2\", \"displayName\": \"Column 2 Links\", \"type\": \"LIST_OF_LINK\" },\n { \"name\": \"copyright\", \"displayName\": \"Copyright Text\", \"type\": \"TEXT\", \"defaultValue\": \"All rights reserved.\" }\n ]\n}\n"
27
+ "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({ backgroundColor = \"#ffffff\" }: 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\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"account-info-inner\">\n <h1 className=\"account-info-title\">Account Information</h1>\n\n {accountForm.isSuccess && (\n <div className=\"account-info-success\">Your information has been updated.</div>\n )}\n {accountForm.isFailure && accountForm.responseMessage && (\n <div className=\"account-info-error\">{accountForm.responseMessage}</div>\n )}\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\n",
28
+ "types.ts": "export interface Props {\n backgroundColor?: string;\n}\n",
29
+ "styles.css": ".account-info-section { width: 100%; padding: 40px 24px; }\n.account-info-inner { max-width: 480px; margin: 0 auto; }\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@media (max-width: 480px) { .account-info-row { grid-template-columns: 1fr; } }\n",
30
+ "ikas-config-snippet.json": "{\n \"id\": \"account-info\",\n \"name\": \"Account Info\",\n \"type\": \"section\",\n \"props\": [\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
31
31
  }
32
32
  },
33
- "product-detail": {
34
- "title": "Product Detail Section",
35
- "description": "Product page with image gallery, variant selection, pricing, add-to-cart, and favorites",
33
+ "account-orders": {
34
+ "title": "Account Orders Section",
35
+ "description": "Complete account orders section showing order history with order number, date, distinct item count (getIkasOrderDistinctItemCount), total price, package status, and order line item thumbnails (getIkasOrderLineVariantMainImage). Uses isEmpty/isNotEmpty utilities.",
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\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
- "types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n addToCartButtonText?: string;\n}\n",
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
- "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"
37
+ "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getOrders,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderDistinctItemCount,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderHref,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n isEmpty,\n isNotEmpty,\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 backgroundColor = \"#ffffff\",\n}: Props) {\n useEffect(() => {\n getOrders(customerStore);\n }, []);\n\n const orders = customerStore.orders ?? [];\n\n return (\n <section className=\"orders-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"orders-inner\">\n <h1 className=\"orders-title\">{title}</h1>\n\n {isEmpty(orders) && (\n <div className=\"orders-empty\">\n <p>{emptyMessage}</p>\n <button\n className=\"orders-shop-btn\"\n onClick={() => Router.navigate(\"/\")}\n >\n Start Shopping\n </button>\n </div>\n )}\n\n {isNotEmpty(orders) && (\n <div className=\"orders-list\">\n {orders.map((order) => {\n // Show first line item thumbnail\n const firstItem = order.orderLineItems?.[0];\n const thumbnail = firstItem?.variant\n ? getIkasOrderLineVariantMainImage(firstItem.variant)\n : null;\n\n return (\n <a\n key={order.id}\n href={getIkasOrderHref(order)}\n className=\"order-card\"\n >\n <div className=\"order-card-header\">\n <span className=\"order-number\">Order #{order.orderNumber}</span>\n <span className=\"order-status\">\n {getIkasOrderPackageStatusTranslation(order)}\n </span>\n </div>\n <div className=\"order-card-body\">\n {thumbnail && (\n <img\n className=\"order-thumbnail\"\n src={getDefaultSrc(thumbnail)}\n alt=\"\"\n />\n )}\n <div className=\"order-card-details\">\n <span className=\"order-date\">\n {getIkasOrderFormattedOrderedAt(order)}\n </span>\n <span className=\"order-items\">\n {getIkasOrderDistinctItemCount(order)} items\n </span>\n <span className=\"order-total\">\n {getIkasOrderFormattedTotalFinalPrice(order)}\n </span>\n </div>\n </div>\n </a>\n );\n })}\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
38
+ "types.ts": "export interface Props {\n title?: string;\n emptyMessage?: string;\n backgroundColor?: string;\n}\n",
39
+ "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 {\n font-size: 24px;\n font-weight: 700;\n color: #111;\n margin: 0 0 24px 0;\n}\n\n.orders-empty {\n text-align: center;\n padding: 48px 0;\n}\n\n.orders-empty p {\n font-size: 16px;\n color: #666;\n margin: 0 0 16px 0;\n}\n\n.orders-shop-btn {\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n color: #111;\n background: #fff;\n border: 1.5px solid #111;\n border-radius: 8px;\n cursor: pointer;\n}\n\n.orders-list {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.order-card {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 20px;\n border: 1px solid #eee;\n border-radius: 10px;\n text-decoration: none;\n color: inherit;\n transition: border-color 0.15s ease;\n}\n\n.order-card:hover {\n border-color: #ccc;\n}\n\n.order-card-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.order-number {\n font-size: 15px;\n font-weight: 600;\n color: #111;\n}\n\n.order-status {\n font-size: 12px;\n font-weight: 600;\n color: #fff;\n background: #111;\n padding: 4px 10px;\n border-radius: 12px;\n}\n\n.order-card-details {\n display: flex;\n gap: 24px;\n font-size: 14px;\n color: #666;\n}\n\n.order-total {\n font-weight: 600;\n color: #111;\n margin-left: auto;\n}\n\n@media (max-width: 480px) {\n .order-card-details {\n flex-direction: column;\n gap: 4px;\n }\n\n .order-total {\n margin-left: 0;\n }\n}\n",
40
+ "ikas-config-snippet.json": "{\n \"id\": \"account-orders\",\n \"name\": \"Account Orders\",\n \"type\": \"section\",\n \"entry\": \"./src/components/AccountOrders/index.tsx\",\n \"styles\": \"./src/components/AccountOrders/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"My Orders\"\n },\n {\n \"name\": \"emptyMessage\",\n \"displayName\": \"Empty State Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"You have no orders yet.\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
41
41
  }
42
42
  },
43
- "product-list": {
44
- "title": "Product List Section",
45
- "description": "Product grid with filters, sorting, and pagination. Demonstrates sub-component pattern: ProductCard and FilterSidebar are extracted into src/sub-components/.",
43
+ "blog-list": {
44
+ "title": "Blog List Section",
45
+ "description": "Complete blog list section with blog card grid, featured images, formatted dates, and forward-only pagination (infinite scroll pattern with IkasThemeInfiniteScroller). Uses hasBlogListNextPage/getBlogListNextPage only no prev page in production.",
46
46
  "files": {
47
- "index.tsx": "import {\n IkasProductListSortType,\n getProductListSortOptions,\n setSortType,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n} from \"@ikas/bp-storefront\";\nimport ProductCard from \"../../sub-components/ProductCard\";\nimport FilterSidebar from \"../../sub-components/FilterSidebar\";\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.data ?? [];\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) => { setSortType(productList, (e.target as HTMLSelectElement).value as IkasProductListSortType); }}>\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 && <FilterSidebar productList={productList} />}\n <div className=\"product-grid\">\n {products.length === 0 && <p className=\"product-grid-empty\">No products found.</p>}\n {products.map((product) => (\n <ProductCard key={product.id} product={product} />\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
- "types.ts": "import { IkasProductList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n productList: IkasProductList;\n title?: string;\n showFilters?: boolean;\n}\n",
49
- "styles.css": "@import \"../../sub-components/ProductCard/styles.css\";\n@import \"../../sub-components/FilterSidebar/styles.css\";\n\n.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-grid {\n flex: 1;\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 24px;\n}\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
- "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",
51
- "src/sub-components/ProductCard/index.tsx": "import {\n IkasProduct,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\n// Sub-component: Props defined inline (no types.ts needed)\ninterface Props {\n product: IkasProduct;\n}\n\n// No observer() needed — this component only uses props from parent\nexport default function ProductCard({ product }: Props) {\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\n return (\n <a 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",
52
- "src/sub-components/ProductCard/styles.css": ".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",
53
- "src/sub-components/FilterSidebar/index.tsx": "import {\n IkasProductList,\n getFilterDisplayedValues,\n handleFilterValueClick,\n handleNumberRangeOptionClick,\n isSwatchFilter,\n getIkasFilterThumbnailImage,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\n// Sub-component: Props defined inline (no types.ts needed)\ninterface Props {\n productList: IkasProductList;\n}\n\n// No observer() needed — this component only uses props from parent\nexport default function FilterSidebar({ productList }: Props) {\n const filters = productList.filters ?? [];\n if (filters.length === 0) return null;\n\n return (\n <aside className=\"product-list-filters\">\n {filters.map((filter) => {\n const values = getFilterDisplayedValues(filter);\n if (values.length === 0 && !filter.numberRangeListOptions?.length) return null;\n\n return (\n <div key={filter.id} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{filter.name}</h3>\n {isSwatchFilter(filter) ? (\n <div className=\"filter-swatches\">\n {values.map((fv) => {\n const thumbnail = getIkasFilterThumbnailImage(fv);\n return (\n <button key={fv.name} className={`filter-swatch${fv.isSelected === true ? \" selected\" : \"\"}`} onClick={() => handleFilterValueClick(productList, filter, fv)} title={fv.name}>\n {thumbnail ? <img src={getDefaultSrc(thumbnail)} alt={fv.name} /> : <span className=\"filter-swatch-color\" style={{ backgroundColor: fv.colorCode ?? \"#ccc\" }} />}\n </button>\n );\n })}\n </div>\n ) : (\n values.map((fv) => (\n <label key={fv.name} className=\"filter-value\">\n <input type=\"checkbox\" checked={fv.isSelected === true} onChange={() => handleFilterValueClick(productList, filter, fv)} />\n <span>{fv.name}</span>\n {fv.resultCount != null && <span className=\"filter-count\">({fv.resultCount})</span>}\n </label>\n ))\n )}\n {filter.numberRangeListOptions?.map((option) => (\n <button key={`${option.from}-${option.to}`} className={`filter-range-btn${option.isSelected ? \" selected\" : \"\"}`} onClick={() => handleNumberRangeOptionClick(productList, filter, option)}>\n {option.from} - {option.to ?? \"+\"}\n </button>\n ))}\n </div>\n );\n })}\n </aside>\n );\n}\n",
54
- "src/sub-components/FilterSidebar/styles.css": ".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.filter-count { color: #999; font-size: 12px; }\n.filter-swatches { display: flex; flex-wrap: wrap; gap: 8px; }\n.filter-swatch { width: 32px; height: 32px; border-radius: 50%; border: 2px solid #ddd; padding: 0; cursor: pointer; overflow: hidden; background: none; }\n.filter-swatch.selected { border-color: #000; }\n.filter-swatch img { width: 100%; height: 100%; object-fit: cover; }\n.filter-swatch-color { display: block; width: 100%; height: 100%; border-radius: 50%; }\n.filter-range-btn { padding: 6px 12px; border: 1px solid #ddd; border-radius: 4px; background: #fff; cursor: pointer; font-size: 13px; margin: 4px 4px 4px 0; }\n.filter-range-btn.selected { border-color: #000; font-weight: 600; }\n"
47
+ "index.tsx": "import {\n IkasBlogList,\n hasBlogListNextPage,\n getBlogListNextPage,\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 backgroundColor = \"#ffffff\",\n}: Props) {\n if (!blogList) return null;\n\n const blogs = blogList.data ?? [];\n const hasNext = hasBlogListNextPage(blogList);\n // Note: dynavit uses only forward pagination (infinite scroll with IkasThemeInfiniteScroller)\n // No hasBlogListPrevPage/getBlogListPrevPage — scroll-only pattern\n\n return (\n <section className=\"blog-list-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"blog-list-inner\">\n <h1 className=\"blog-list-title\">{title}</h1>\n\n {blogs.length === 0 && (\n <p className=\"blog-list-empty\">No blog posts found.</p>\n )}\n\n {/* Blog Grid production uses IkasThemeInfiniteScroller for infinite scroll */}\n <div className=\"blog-grid\">\n {blogs.map((blog) => (\n <a\n key={blog.id}\n href={getIkasBlogHref(blog)}\n className=\"blog-card\"\n >\n {blog.image && (\n <div className=\"blog-card-image-wrap\">\n <img\n src={getDefaultSrc(blog.image)}\n alt={blog.title}\n className=\"blog-card-image\"\n />\n </div>\n )}\n <div className=\"blog-card-content\">\n <span className=\"blog-card-date\">\n {getIkasBlogFormattedDate(blog)}\n </span>\n <h3 className=\"blog-card-title\">{blog.title}</h3>\n {blog.summary && (\n <p className=\"blog-card-summary\">{blog.summary}</p>\n )}\n <span className=\"blog-card-read-more\">Read more</span>\n </div>\n </a>\n ))}\n </div>\n\n {/* Load More — infinite scroll pattern */}\n {hasNext && (\n <div className=\"blog-load-more\">\n <button\n className=\"blog-load-more-btn\"\n onClick={() => getBlogListNextPage(blogList)}\n >\n Load More\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
48
+ "types.ts": "import { IkasBlogList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogList: IkasBlogList;\n title?: string;\n backgroundColor?: string;\n}\n",
49
+ "styles.css": ".blog-list-section {\n width: 100%;\n padding: 48px 24px;\n}\n\n.blog-list-inner {\n max-width: 1200px;\n margin: 0 auto;\n}\n\n.blog-list-title {\n font-size: 28px;\n font-weight: 700;\n color: #111;\n margin: 0 0 32px 0;\n}\n\n.blog-list-empty {\n font-size: 16px;\n color: #666;\n text-align: center;\n padding: 48px 0;\n}\n\n.blog-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 32px;\n}\n\n.blog-card {\n text-decoration: none;\n color: inherit;\n display: flex;\n flex-direction: column;\n border-radius: 12px;\n overflow: hidden;\n border: 1px solid #eee;\n transition: box-shadow 0.2s ease;\n}\n\n.blog-card:hover {\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);\n}\n\n.blog-card-image-wrap {\n aspect-ratio: 16 / 10;\n overflow: hidden;\n background: #f5f5f5;\n}\n\n.blog-card-image {\n width: 100%;\n height: 100%;\n object-fit: cover;\n transition: transform 0.2s ease;\n}\n\n.blog-card:hover .blog-card-image {\n transform: scale(1.03);\n}\n\n.blog-card-content {\n padding: 20px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n flex: 1;\n}\n\n.blog-card-date {\n font-size: 12px;\n color: #999;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.blog-card-title {\n font-size: 18px;\n font-weight: 600;\n color: #111;\n margin: 0;\n line-height: 1.4;\n}\n\n.blog-card-summary {\n font-size: 14px;\n color: #666;\n line-height: 1.6;\n margin: 0;\n display: -webkit-box;\n -webkit-line-clamp: 3;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.blog-card-read-more {\n font-size: 14px;\n font-weight: 600;\n color: #111;\n margin-top: auto;\n padding-top: 8px;\n}\n\n/* Pagination */\n.blog-pagination {\n display: flex;\n justify-content: center;\n gap: 12px;\n margin-top: 40px;\n}\n\n.blog-pagination-btn {\n padding: 10px 24px;\n font-size: 14px;\n font-weight: 500;\n background: #fff;\n border: 1px solid #ddd;\n border-radius: 6px;\n cursor: pointer;\n}\n\n.blog-pagination-btn:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.blog-pagination-btn:hover:not(:disabled) {\n border-color: #111;\n}\n\n/* Responsive */\n@media (max-width: 768px) {\n .blog-grid {\n grid-template-columns: 1fr;\n gap: 24px;\n }\n}\n",
50
+ "ikas-config-snippet.json": "{\n \"id\": \"blog-list\",\n \"name\": \"Blog List\",\n \"type\": \"section\",\n \"entry\": \"./src/components/BlogList/index.tsx\",\n \"styles\": \"./src/components/BlogList/styles.css\",\n \"props\": [\n {\n \"name\": \"blogList\",\n \"displayName\": \"Blog List\",\n \"type\": \"BLOG_POST_LIST\",\n \"required\": true\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Blog\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
55
51
  }
56
52
  },
57
- "cart": {
58
- "title": "Cart Section",
59
- "description": "Shopping cart with line items, quantity controls, totals, and checkout button",
53
+ "blog-post": {
54
+ "title": "Blog Post Section",
55
+ "description": "Single blog post detail page with image, title, date, and HTML content.",
60
56
  "files": {
61
- "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",
62
- "types.ts": "export interface Props {\n emptyCartMessage?: string;\n}\n",
63
- "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",
64
- "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"
57
+ "index.tsx": "import {\n getIkasBlogFormattedDate,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function BlogPostSection({\n blogPost,\n backgroundColor = \"#ffffff\",\n}: Props) {\n if (!blogPost) return null;\n\n return (\n <section className=\"blog-post-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"blog-post-inner\">\n {blogPost.image && (\n <img src={getDefaultSrc(blogPost.image)} alt={blogPost.title} className=\"blog-post-hero\" />\n )}\n <h1 className=\"blog-post-title\">{blogPost.title}</h1>\n <span className=\"blog-post-date\">{getIkasBlogFormattedDate(blogPost)}</span>\n <div className=\"blog-post-content\" dangerouslySetInnerHTML={{ __html: blogPost.blogContent.content }} />\n </div>\n </section>\n );\n}\n\nexport default BlogPostSection;\n",
58
+ "types.ts": "import { IkasBlog } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogPost: IkasBlog;\n backgroundColor?: string;\n}\n",
59
+ "styles.css": ".blog-post-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.blog-post-inner {\n max-width: 720px;\n margin: 0 auto;\n}\n\n.blog-post-hero { width: 100%; aspect-ratio: 16/9; object-fit: cover; border-radius: 8px; margin-bottom: 32px; }\n.blog-post-title { font-size: 32px; font-weight: 700; color: #111; margin: 0 0 8px 0; line-height: 1.3; }\n.blog-post-date { font-size: 14px; color: #999; display: block; margin-bottom: 32px; }\n.blog-post-content { font-size: 16px; line-height: 1.8; color: #333; }\n.blog-post-content img { max-width: 100%; height: auto; border-radius: 8px; margin: 24px 0; }\n.blog-post-content h2 { font-size: 24px; font-weight: 700; margin: 32px 0 16px; }\n.blog-post-content h3 { font-size: 20px; font-weight: 600; margin: 24px 0 12px; }\n.blog-post-content p { margin: 0 0 16px 0; }\n",
60
+ "ikas-config-snippet.json": "{\n \"id\": \"blog-post\",\n \"name\": \"Blog Post\",\n \"type\": \"section\",\n \"props\": [\n {\n \"name\": \"blogPost\",\n \"displayName\": \"Blog Post\",\n \"type\": \"BLOG\",\n \"required\": true\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
65
61
  }
66
62
  },
67
- "login": {
68
- "title": "Login Section",
69
- "description": "Customer login form with email/password fields, forgot password link, and register link",
63
+ "cart-page": {
64
+ "title": "Cart Section",
65
+ "description": "Complete cart section with loading state, empty cart handling, line items with variant images/links/discount detection, quantity controls, order adjustments (coupons/discounts), subtotal/total, and checkout via getCheckoutUrlFromCartStore. Cart data is automatically reactive in root components via autorun().",
70
66
  "files": {
71
- "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",
72
- "types.ts": "export interface Props {\n redirectAfterLogin?: string;\n}\n",
73
- "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",
74
- "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"
67
+ "index.tsx": "import {\n cartStore,\n hasCart,\n changeItemQuantity,\n removeItem,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderTotalItemCount,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n getOrderLineItemFormattedPriceWithQuantity,\n hasOrderLineItemDiscount,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getCheckoutUrlFromCartStore,\n getDefaultSrc,\n Router,\n IkasOrderLineItem,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function CartSection({ emptyCartMessage = \"Your cart is empty\" ,\nbackgroundColor = \"#ffffff\",\n}: Props) {\n const cart = cartStore.cart;\n const isLoading = cartStore.isCartLoading;\n const cartHasItems = hasCart(cartStore) as unknown as boolean;\n const lineItems = cart?.orderLineItems ?? [];\n const adjustments = cart?.orderAdjustments ?? [];\n const totalItemCount = cart ? (getIkasOrderTotalItemCount(cart) as unknown as number) : 0;\n\n if (isLoading) {\n return (\n <section className=\"cart-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"cart-inner\">\n <p className=\"cart-loading\">Loading cart...</p>\n </div>\n </section>\n );\n }\n\n if (!cartHasItems) {\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(\"/\")}>\n Continue Shopping\n </button>\n </div>\n </section>\n );\n }\n\n const handleQuantityChange = async (item: IkasOrderLineItem, delta: number) => {\n const newQty = item.quantity + delta;\n if (newQty < 1) return;\n await changeItemQuantity(item, newQty);\n };\n\n const handleRemove = async (item: IkasOrderLineItem) => {\n await removeItem(item);\n };\n\n return (\n <section className=\"cart-section\">\n <div className=\"cart-inner\">\n <h1 className=\"cart-title\">Shopping Cart ({totalItemCount} items)</h1>\n\n <div className=\"cart-items\">\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n const href = item.variant ? getIkasOrderLineVariantHref(item.variant) : undefined;\n const hasDiscount = hasOrderLineItemDiscount(item) as unknown as boolean;\n return (\n <div key={item.id} className=\"cart-item\">\n {image && (\n <a href={href}>\n <img\n className=\"cart-item-image\"\n src={getDefaultSrc(image)}\n alt={item.variant?.name || \"Product\"}\n />\n </a>\n )}\n <div className=\"cart-item-info\">\n <a href={href} className=\"cart-item-name\">{item.variant?.name}</a>\n {hasDiscount && (\n <span className=\"cart-item-original-price\">\n {getOrderLineItemFormattedPriceWithQuantity(item)}\n </span>\n )}\n <span className={`cart-item-total ${hasDiscount ? \"has-discount\" : \"\"}`}>\n {getOrderLineItemFormattedFinalPriceWithQuantity(item)}\n </span>\n </div>\n <div className=\"cart-item-quantity\">\n <button onClick={() => handleQuantityChange(item, -1)}>-</button>\n <span>{item.quantity}</span>\n <button onClick={() => handleQuantityChange(item, 1)}>+</button>\n </div>\n <button className=\"cart-item-remove\" onClick={() => handleRemove(item)}>\n Remove\n </button>\n </div>\n );\n })}\n </div>\n\n {/* Order Adjustments (discounts, coupons, shipping fees) */}\n {adjustments.length > 0 && (\n <div className=\"cart-adjustments\">\n {adjustments.map((adj: any, i: number) => (\n <div key={i} className=\"cart-adjustment-row\">\n <span>{getOrderAdjustmentDisplayName(adj)}</span>\n <span>{getOrderAdjustmentFormattedAmount(adj)}</span>\n </div>\n ))}\n </div>\n )}\n\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 <a\n className=\"cart-checkout-btn\"\n href={getCheckoutUrlFromCartStore(cartStore)}\n >\n Proceed to Checkout\n </a>\n </div>\n </div>\n </section>\n );\n}\n\n",
68
+ "types.ts": "export interface Props {\n emptyCartMessage?: string;\n backgroundColor?: string;\n}\n",
69
+ "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 {\n font-size: 24px;\n font-weight: 700;\n color: #111;\n margin: 0 0 24px 0;\n}\n\n.cart-empty {\n font-size: 16px;\n color: #666;\n text-align: center;\n padding: 48px 0;\n}\n\n.cart-loading {\n font-size: 16px;\n color: #666;\n text-align: center;\n padding: 48px 0;\n}\n\n.cart-continue-btn {\n display: block;\n margin: 0 auto;\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n color: #111;\n background: #fff;\n border: 1.5px solid #111;\n border-radius: 8px;\n cursor: pointer;\n}\n\n/* Cart Items */\n.cart-items {\n display: flex;\n flex-direction: column;\n gap: 16px;\n margin-bottom: 32px;\n}\n\n.cart-item {\n display: flex;\n align-items: center;\n gap: 16px;\n padding: 16px;\n border: 1px solid #eee;\n border-radius: 8px;\n}\n\n.cart-item-image {\n width: 80px;\n height: 80px;\n object-fit: cover;\n border-radius: 6px;\n background-color: #f5f5f5;\n}\n\n.cart-item-info {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.cart-item-name {\n font-size: 14px;\n font-weight: 600;\n color: #111;\n text-decoration: none;\n}\n\n.cart-item-name:hover {\n text-decoration: underline;\n}\n\n.cart-item-original-price {\n font-size: 13px;\n color: #999;\n text-decoration: line-through;\n}\n\n.cart-item-total {\n font-size: 15px;\n font-weight: 600;\n color: #111;\n min-width: 80px;\n}\n\n.cart-item-total.has-discount {\n color: #e53935;\n}\n\n.cart-item-quantity {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.cart-item-quantity button {\n width: 32px;\n height: 32px;\n border: 1px solid #ddd;\n border-radius: 4px;\n background: #fff;\n cursor: pointer;\n font-size: 16px;\n}\n\n.cart-item-remove {\n padding: 4px 8px;\n font-size: 12px;\n color: #e53935;\n background: none;\n border: none;\n cursor: pointer;\n}\n\n/* Order Adjustments */\n.cart-adjustments {\n border-top: 1px solid #eee;\n padding: 16px 0;\n margin-bottom: 8px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.cart-adjustment-row {\n display: flex;\n justify-content: space-between;\n font-size: 14px;\n color: #555;\n}\n\n/* Cart Summary */\n.cart-summary {\n border-top: 1px solid #eee;\n padding-top: 24px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n align-items: flex-end;\n}\n\n.cart-summary-row {\n display: flex;\n justify-content: space-between;\n width: 280px;\n font-size: 14px;\n color: #555;\n}\n\n.cart-summary-total {\n font-size: 18px;\n font-weight: 700;\n color: #111;\n}\n\n.cart-checkout-btn {\n display: block;\n width: 280px;\n padding: 14px 24px;\n font-size: 16px;\n font-weight: 600;\n color: #fff;\n background-color: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n margin-top: 8px;\n text-align: center;\n text-decoration: none;\n}\n\n.cart-checkout-btn:hover {\n background-color: #333;\n}\n\n/* Responsive */\n@media (max-width: 768px) {\n .cart-item {\n flex-wrap: wrap;\n }\n\n .cart-summary {\n align-items: stretch;\n }\n\n .cart-summary-row,\n .cart-checkout-btn {\n width: 100%;\n }\n}\n",
70
+ "ikas-config-snippet.json": "{\n \"id\": \"cart-page\",\n \"name\": \"Cart Page\",\n \"type\": \"section\",\n \"entry\": \"./src/components/CartSection/index.tsx\",\n \"styles\": \"./src/components/CartSection/styles.css\",\n \"props\": [\n {\n \"name\": \"emptyCartMessage\",\n \"displayName\": \"Empty Cart Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Your cart is empty\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
75
71
  }
76
72
  },
77
- "register": {
78
- "title": "Register Section",
79
- "description": "Customer registration form with name, email, and password fields",
73
+ "contact-form": {
74
+ "title": "Contact Form Section",
75
+ "description": "Complete contact form section with name, email, phone, and message fields. Uses the initContactForm/setContactForm*/submitContactForm pattern with validation and success state.",
80
76
  "files": {
81
- "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",
82
- "types.ts": "export interface Props {\n redirectAfterRegister?: string;\n}\n",
83
- "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",
84
- "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"
77
+ "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 backgroundColor = \"#ffffff\",\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) {\n clearContactForm(contactForm);\n }\n };\n\n return (\n <section className=\"contact-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"contact-inner\">\n <h1 className=\"contact-title\">{title}</h1>\n\n {contactForm.isSuccess && (\n <div className=\"contact-success-banner\">{successMessage}</div>\n )}\n\n {contactForm.isFailure && contactForm.responseMessage && (\n <div className=\"contact-error-banner\">{contactForm.responseMessage}</div>\n )}\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\n className={`contact-input ${contactForm.firstName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={contactForm.firstName.placeholder}\n value={contactForm.firstName.value}\n onInput={(e) =>\n setContactFormFirstName(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.firstName.hasError && contactForm.firstName.message && (\n <span className=\"contact-field-error\">{contactForm.firstName.message}</span>\n )}\n </div>\n\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.lastName.label}</label>\n <input\n className={`contact-input ${contactForm.lastName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={contactForm.lastName.placeholder}\n value={contactForm.lastName.value}\n onInput={(e) =>\n setContactFormLastName(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.lastName.hasError && contactForm.lastName.message && (\n <span className=\"contact-field-error\">{contactForm.lastName.message}</span>\n )}\n </div>\n </div>\n\n <div className=\"contact-row\">\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.email.label}</label>\n <input\n className={`contact-input ${contactForm.email.hasError ? \"has-error\" : \"\"}`}\n type=\"email\"\n placeholder={contactForm.email.placeholder}\n value={contactForm.email.value}\n onInput={(e) =>\n setContactFormEmail(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.email.hasError && contactForm.email.message && (\n <span className=\"contact-field-error\">{contactForm.email.message}</span>\n )}\n </div>\n\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.phone.label}</label>\n <input\n className={`contact-input ${contactForm.phone.hasError ? \"has-error\" : \"\"}`}\n type=\"tel\"\n placeholder={contactForm.phone.placeholder}\n value={contactForm.phone.value}\n onInput={(e) =>\n setContactFormPhone(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.phone.hasError && contactForm.phone.message && (\n <span className=\"contact-field-error\">{contactForm.phone.message}</span>\n )}\n </div>\n </div>\n\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.message.label}</label>\n <textarea\n className={`contact-textarea ${contactForm.message.hasError ? \"has-error\" : \"\"}`}\n placeholder={contactForm.message.placeholder}\n value={contactForm.message.value}\n rows={5}\n onInput={(e) =>\n setContactFormMessage(contactForm, (e.target as HTMLTextAreaElement).value)\n }\n />\n {contactForm.message.hasError && contactForm.message.message && (\n <span className=\"contact-field-error\">{contactForm.message.message}</span>\n )}\n </div>\n\n <button\n className=\"contact-submit-btn\"\n type=\"submit\"\n disabled={contactForm.isSubmitting}\n >\n {contactForm.isSubmitting ? \"Sending...\" : \"Send Message\"}\n </button>\n </form>\n </div>\n </section>\n );\n}\n\n",
78
+ "types.ts": "export interface Props {\n title?: string;\n successMessage?: string;\n backgroundColor?: string;\n}\n",
79
+ "styles.css": ".contact-section {\n width: 100%;\n padding: 64px 24px;\n}\n\n.contact-inner {\n max-width: 640px;\n margin: 0 auto;\n}\n\n.contact-title {\n font-size: 28px;\n font-weight: 700;\n color: #111;\n margin: 0 0 24px 0;\n text-align: center;\n}\n\n.contact-success-banner {\n padding: 12px 16px;\n font-size: 14px;\n color: #1b5e20;\n background-color: #e8f5e9;\n border-radius: 8px;\n margin-bottom: 20px;\n text-align: center;\n}\n\n.contact-error-banner {\n padding: 12px 16px;\n font-size: 14px;\n color: #b71c1c;\n background-color: #ffebee;\n border-radius: 8px;\n margin-bottom: 20px;\n}\n\n.contact-form {\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.contact-row {\n display: flex;\n gap: 12px;\n}\n\n.contact-row .contact-field {\n flex: 1;\n}\n\n.contact-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.contact-label {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n}\n\n.contact-input,\n.contact-textarea {\n padding: 12px 14px;\n font-size: 15px;\n border: 1.5px solid #ddd;\n border-radius: 8px;\n outline: none;\n transition: border-color 0.15s ease;\n font-family: inherit;\n}\n\n.contact-input:focus,\n.contact-textarea:focus {\n border-color: #111;\n}\n\n.contact-input.has-error,\n.contact-textarea.has-error {\n border-color: #e53935;\n}\n\n.contact-textarea {\n resize: vertical;\n min-height: 120px;\n}\n\n.contact-field-error {\n font-size: 12px;\n color: #e53935;\n}\n\n.contact-submit-btn {\n padding: 14px 24px;\n font-size: 16px;\n font-weight: 600;\n color: #fff;\n background-color: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s ease;\n margin-top: 8px;\n}\n\n.contact-submit-btn:hover:not(:disabled) {\n background-color: #333;\n}\n\n.contact-submit-btn:disabled {\n background-color: #ccc;\n cursor: not-allowed;\n}\n\n@media (max-width: 480px) {\n .contact-row {\n flex-direction: column;\n }\n}\n",
80
+ "ikas-config-snippet.json": "{\n \"id\": \"contact-form\",\n \"name\": \"Contact Form\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ContactForm/index.tsx\",\n \"styles\": \"./src/components/ContactForm/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Contact Us\"\n },\n {\n \"name\": \"successMessage\",\n \"displayName\": \"Success Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Thank you! Your message has been sent.\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
85
81
  }
86
82
  },
87
- "forgot-password": {
88
- "title": "Forgot Password Section",
89
- "description": "Password reset form with email input and success/error states",
83
+ "faq": {
84
+ "title": "FAQ Accordion Section",
85
+ "description": "Complete FAQ section with accordion-style question/answer pairs. Uses static TEXT props for questions and answers with toggle state management. Commonly reused across multiple pages (about, contact, product detail).",
90
86
  "files": {
91
- "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",
92
- "types.ts": "export interface Props {\n successMessage?: string;\n}\n",
93
- "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",
94
- "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"
87
+ "index.tsx": "import { useState } from \"preact/hooks\";\nimport { Props, FaqItem } from \"./types\";\n\nexport default function FaqSection({\n title = \"Frequently Asked Questions\",\n items = [],\n backgroundColor = \"#ffffff\",\n}: Props) {\n const [openIndex, setOpenIndex] = useState<number | null>(null);\n\n const handleToggle = (index: number) => {\n setOpenIndex(openIndex === index ? null : index);\n };\n\n return (\n <section className=\"faq-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"faq-inner\">\n <h2 className=\"faq-title\">{title}</h2>\n\n {items.length === 0 && (\n <p className=\"faq-empty\">No questions added yet.</p>\n )}\n\n <div className=\"faq-list\">\n {items.map((item: FaqItem, index: number) => {\n const isOpen = openIndex === index;\n return (\n <div\n key={index}\n className={`faq-item ${isOpen ? \"faq-item-open\" : \"\"}`}\n >\n <button\n className=\"faq-question\"\n onClick={() => handleToggle(index)}\n aria-expanded={isOpen}\n >\n <span>{item.question}</span>\n <span className=\"faq-icon\">{isOpen ? \"−\" : \"+\"}</span>\n </button>\n {isOpen && (\n <div className=\"faq-answer\">\n <p>{item.answer}</p>\n </div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n </section>\n );\n}\n",
88
+ "types.ts": "export interface FaqItem {\n question: string;\n answer: string;\n}\n\nexport interface Props {\n title?: string;\n items?: FaqItem[];\n backgroundColor?: string;\n}\n",
89
+ "styles.css": ".faq-section {\n width: 100%;\n padding: 64px 24px;\n}\n\n.faq-inner {\n max-width: 720px;\n margin: 0 auto;\n}\n\n.faq-title {\n font-size: 28px;\n font-weight: 700;\n color: #111;\n margin: 0 0 32px 0;\n text-align: center;\n}\n\n.faq-empty {\n font-size: 16px;\n color: #666;\n text-align: center;\n}\n\n.faq-list {\n display: flex;\n flex-direction: column;\n}\n\n.faq-item {\n border-bottom: 1px solid #eee;\n}\n\n.faq-question {\n width: 100%;\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 20px 0;\n font-size: 16px;\n font-weight: 500;\n color: #111;\n background: none;\n border: none;\n cursor: pointer;\n text-align: left;\n gap: 16px;\n}\n\n.faq-question:hover {\n color: #333;\n}\n\n.faq-icon {\n font-size: 20px;\n font-weight: 300;\n flex-shrink: 0;\n color: #666;\n}\n\n.faq-answer {\n padding: 0 0 20px 0;\n}\n\n.faq-answer p {\n font-size: 15px;\n line-height: 1.7;\n color: #555;\n margin: 0;\n}\n\n.faq-item-open .faq-question {\n font-weight: 600;\n}\n",
90
+ "ikas-config-snippet.json": "{\n \"id\": \"faq\",\n \"name\": \"FAQ\",\n \"type\": \"section\",\n \"entry\": \"./src/components/FAQ/index.tsx\",\n \"styles\": \"./src/components/FAQ/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Frequently Asked Questions\"\n },\n {\n \"name\": \"items\",\n \"displayName\": \"FAQ Items\",\n \"type\": \"COMPONENT_LIST\",\n \"componentProps\": [\n {\n \"name\": \"question\",\n \"displayName\": \"Question\",\n \"type\": \"TEXT\",\n \"required\": true\n },\n {\n \"name\": \"answer\",\n \"displayName\": \"Answer\",\n \"type\": \"TEXT\",\n \"required\": true\n }\n ]\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
95
91
  }
96
92
  },
97
- "account-orders": {
98
- "title": "Account Orders Section",
99
- "description": "Customer order history list with order details and status",
93
+ "favorites-page": {
94
+ "title": "Favorites Page Section",
95
+ "description": "Complete favorites page with getFavoriteProducts loading, product cards with pricing/images/links, and remove functionality.",
100
96
  "files": {
101
- "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",
102
- "types.ts": "export interface Props {\n title?: string;\n emptyMessage?: string;\n}\n",
103
- "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",
104
- "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"
97
+ "index.tsx": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getFavoriteProducts,\n removeIkasProductFromFavorites,\n getSelectedProductVariant,\n getSelectedProductVariantHref,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantMainImage,\n getDefaultSrc,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function FavoritesPageSection({ backgroundColor = \"#ffffff\" }: Props) {\n const [favorites, setFavorites] = useState<IkasProduct[]>([]);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n getFavoriteProducts(customerStore).then((products) => {\n setFavorites(products ?? []);\n setLoading(false);\n });\n }, []);\n\n const handleRemove = async (product: IkasProduct) => {\n await removeIkasProductFromFavorites(product);\n setFavorites(favorites.filter((f) => f.id !== product.id));\n };\n\n if (loading) {\n return <section className=\"favorites-page\"><div className=\"favorites-inner\"><p>Loading...</p></div></section>;\n }\n\n return (\n <section className=\"favorites-page\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"favorites-inner\">\n <h1 className=\"favorites-title\">My Favorites</h1>\n\n {favorites.length === 0 && <p className=\"favorites-empty\">No favorites yet.</p>}\n\n <div className=\"favorites-grid\">\n {favorites.map((product) => {\n const variant = getSelectedProductVariant(product);\n const href = getSelectedProductVariantHref(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const hasDiscount = hasProductVariantDiscount(variant) as unknown as boolean;\n const finalPrice = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const sellPrice = hasDiscount ? (getProductVariantFormattedSellPrice(variant) as unknown as string) : null;\n\n return (\n <div key={product.id} className=\"favorites-card\">\n {image && (\n <a href={href}><img className=\"favorites-card-img\" src={getDefaultSrc(image)} alt={product.name} /></a>\n )}\n <div className=\"favorites-card-info\">\n <a href={href} className=\"favorites-card-name\">{product.name}</a>\n <div className=\"favorites-card-price\">\n {sellPrice && <span className=\"favorites-card-old-price\">{sellPrice}</span>}\n <span>{finalPrice}</span>\n </div>\n <button className=\"favorites-card-remove\" onClick={() => handleRemove(product)}>Remove</button>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n </section>\n );\n}\n\n",
98
+ "types.ts": "export interface Props {\n backgroundColor?: string;\n}\n",
99
+ "styles.css": ".favorites-page { width: 100%; padding: 40px 24px; }\n.favorites-inner { max-width: 1000px; margin: 0 auto; }\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.favorites-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }\n.favorites-card { border: 1px solid #eee; border-radius: 8px; overflow: hidden; }\n.favorites-card-img { width: 100%; aspect-ratio: 1; object-fit: cover; background: #f5f5f5; display: block; }\n.favorites-card-info { padding: 12px; }\n.favorites-card-name { font-size: 14px; font-weight: 600; color: #111; text-decoration: none; display: block; margin-bottom: 4px; }\n.favorites-card-price { font-size: 15px; font-weight: 700; }\n.favorites-card-old-price { text-decoration: line-through; color: #999; margin-right: 8px; font-weight: 400; font-size: 13px; }\n.favorites-card-remove { font-size: 13px; color: #e53935; background: none; border: none; cursor: pointer; padding: 0; margin-top: 8px; }\n@media (max-width: 768px) { .favorites-grid { grid-template-columns: repeat(2, 1fr); } }\n@media (max-width: 480px) { .favorites-grid { grid-template-columns: 1fr; } }\n",
100
+ "ikas-config-snippet.json": "{\n \"id\": \"favorites-page\",\n \"name\": \"Favorites Page\",\n \"type\": \"section\",\n \"props\": [\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
105
101
  }
106
102
  },
107
- "account-addresses": {
108
- "title": "Account Addresses Section",
109
- "description": "Customer address list with add/delete functionality",
103
+ "footer": {
104
+ "title": "Footer Section",
105
+ "description": "Complete footer section with logo, navigation link columns, contact info, social media links, and copyright. Uses IkasNavigationLink for editable links.",
110
106
  "files": {
111
- "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",
112
- "types.ts": "export interface Props {\n title?: string;\n}\n",
113
- "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",
114
- "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"
107
+ "index.tsx": "import { IkasNavigationLink, getDefaultSrc } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function FooterSection({\n logo,\n description,\n linkColumn1Title = \"Shop\",\n linkColumn1,\n linkColumn2Title = \"Company\",\n linkColumn2,\n copyright = \"All rights reserved.\",\n backgroundColor = \"#f9fafb\",\n}: Props) {\n return (\n <footer className=\"footer-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"footer-inner\">\n <div className=\"footer-grid\">\n {/* Brand Column */}\n <div className=\"footer-brand\">\n {logo ? (\n <img src={getDefaultSrc(logo)} alt={logo.altText || \"Logo\"} className=\"footer-logo\" />\n ) : (\n <span className=\"footer-logo-text\">Store</span>\n )}\n {description && <p className=\"footer-description\">{description}</p>}\n </div>\n\n {/* Link Column 1 */}\n <div className=\"footer-link-column\">\n <h4 className=\"footer-column-title\">{linkColumn1Title}</h4>\n <nav className=\"footer-links\">\n {(linkColumn1?.links ?? []).map((link: IkasNavigationLink, i: number) => (\n <a\n key={i}\n href={link.href}\n className=\"footer-link\"\n target={link.openInNewTab ? \"_blank\" : undefined}\n >\n {link.label}\n </a>\n ))}\n </nav>\n </div>\n\n {/* Link Column 2 */}\n <div className=\"footer-link-column\">\n <h4 className=\"footer-column-title\">{linkColumn2Title}</h4>\n <nav className=\"footer-links\">\n {(linkColumn2?.links ?? []).map((link: IkasNavigationLink, i: number) => (\n <a\n key={i}\n href={link.href}\n className=\"footer-link\"\n target={link.openInNewTab ? \"_blank\" : undefined}\n >\n {link.label}\n </a>\n ))}\n </nav>\n </div>\n </div>\n\n {/* Copyright */}\n <div className=\"footer-bottom\">\n <p className=\"footer-copyright\">{copyright}</p>\n </div>\n </div>\n </footer>\n );\n}\n",
108
+ "types.ts": "import { IkasNavigationLinkList, IkasImage } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n logo?: IkasImage | null;\n description?: string;\n linkColumn1Title?: string;\n linkColumn1?: IkasNavigationLinkList;\n linkColumn2Title?: string;\n linkColumn2?: IkasNavigationLinkList;\n copyright?: string;\n backgroundColor?: string;\n}\n",
109
+ "styles.css": ".footer-section {\n width: 100%;\n border-top: 1px solid #eee;\n padding: 48px 24px 24px;\n}\n\n.footer-inner {\n max-width: 1200px;\n margin: 0 auto;\n}\n\n.footer-grid {\n display: grid;\n grid-template-columns: 2fr 1fr 1fr;\n gap: 48px;\n margin-bottom: 48px;\n}\n\n/* Brand */\n.footer-brand {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.footer-logo {\n height: 36px;\n width: auto;\n}\n\n.footer-logo-text {\n font-size: 20px;\n font-weight: 700;\n color: #111;\n}\n\n.footer-description {\n font-size: 14px;\n color: #666;\n line-height: 1.6;\n max-width: 300px;\n margin: 0;\n}\n\n/* Link Columns */\n.footer-link-column {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.footer-column-title {\n font-size: 14px;\n font-weight: 700;\n color: #111;\n margin: 0;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.footer-links {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.footer-link {\n font-size: 14px;\n color: #666;\n text-decoration: none;\n transition: color 0.15s ease;\n}\n\n.footer-link:hover {\n color: #111;\n}\n\n/* Bottom */\n.footer-bottom {\n border-top: 1px solid #eee;\n padding-top: 24px;\n}\n\n.footer-copyright {\n font-size: 13px;\n color: #999;\n margin: 0;\n text-align: center;\n}\n\n/* Responsive */\n@media (max-width: 768px) {\n .footer-grid {\n grid-template-columns: 1fr;\n gap: 32px;\n }\n}\n",
110
+ "ikas-config-snippet.json": "{\n \"id\": \"footer\",\n \"name\": \"Footer\",\n \"type\": \"section\",\n \"entry\": \"./src/components/Footer/index.tsx\",\n \"styles\": \"./src/components/Footer/styles.css\",\n \"props\": [\n {\n \"name\": \"logo\",\n \"displayName\": \"Logo\",\n \"type\": \"IMAGE\"\n },\n {\n \"name\": \"description\",\n \"displayName\": \"Description\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Your one-stop shop for quality products.\"\n },\n {\n \"name\": \"linkColumn1Title\",\n \"displayName\": \"Column 1 Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Shop\"\n },\n {\n \"name\": \"linkColumn1\",\n \"displayName\": \"Column 1 Links\",\n \"type\": \"LIST_OF_LINK\"\n },\n {\n \"name\": \"linkColumn2Title\",\n \"displayName\": \"Column 2 Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Company\"\n },\n {\n \"name\": \"linkColumn2\",\n \"displayName\": \"Column 2 Links\",\n \"type\": \"LIST_OF_LINK\"\n },\n {\n \"name\": \"copyright\",\n \"displayName\": \"Copyright Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"All rights reserved.\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#f9fafb\"\n }\n ]\n}\n"
115
111
  }
116
112
  },
117
- "favorites": {
118
- "title": "Favorites Section",
119
- "description": "Customer favorites/wishlist with product cards and remove functionality",
113
+ "forgot-password": {
114
+ "title": "Forgot Password Section",
115
+ "description": "Complete forgot password section with email input for password recovery. Uses the initForgotPasswordForm/setForgotPasswordFormEmail/submitForgotPasswordForm pattern with success and error states.",
120
116
  "files": {
121
- "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",
122
- "types.ts": "export interface Props {\n title?: string;\n}\n",
123
- "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",
124
- "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"
117
+ "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 backgroundColor = \"#ffffff\",\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\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"forgot-inner\">\n <h1 className=\"forgot-title\">Forgot Password</h1>\n <p className=\"forgot-subtitle\">\n Enter your email address and we'll send you a link to reset your password.\n </p>\n\n {forgotForm.isSuccess && (\n <div className=\"forgot-success-banner\">{successMessage}</div>\n )}\n\n {forgotForm.isFailure && forgotForm.responseMessage && (\n <div className=\"forgot-error-banner\">{forgotForm.responseMessage}</div>\n )}\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) =>\n setForgotPasswordFormEmail(forgotForm, (e.target as HTMLInputElement).value)\n }\n />\n {forgotForm.email.hasError && forgotForm.email.message && (\n <span className=\"forgot-field-error\">{forgotForm.email.message}</span>\n )}\n </div>\n\n <button\n className=\"forgot-submit-btn\"\n type=\"submit\"\n disabled={forgotForm.isSubmitting}\n >\n {forgotForm.isSubmitting ? \"Sending...\" : \"Send Reset Link\"}\n </button>\n </form>\n )}\n\n <p className=\"forgot-back-link\">\n <a\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n Router.navigateToPage(\"LOGIN\");\n }}\n >\n Back to Sign In\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
118
+ "types.ts": "export interface Props {\n successMessage?: string;\n backgroundColor?: string;\n}\n",
119
+ "styles.css": ".forgot-section {\n width: 100%;\n padding: 64px 24px;\n min-height: 60vh;\n display: flex;\n align-items: center;\n}\n\n.forgot-inner {\n max-width: 400px;\n margin: 0 auto;\n width: 100%;\n}\n\n.forgot-title {\n font-size: 28px;\n font-weight: 700;\n color: #111;\n margin: 0 0 8px 0;\n text-align: center;\n}\n\n.forgot-subtitle {\n font-size: 14px;\n color: #666;\n text-align: center;\n margin: 0 0 24px 0;\n line-height: 1.5;\n}\n\n.forgot-success-banner {\n padding: 12px 16px;\n font-size: 14px;\n color: #1b5e20;\n background-color: #e8f5e9;\n border-radius: 8px;\n margin-bottom: 20px;\n text-align: center;\n}\n\n.forgot-error-banner {\n padding: 12px 16px;\n font-size: 14px;\n color: #b71c1c;\n background-color: #ffebee;\n border-radius: 8px;\n margin-bottom: 20px;\n}\n\n.forgot-form {\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.forgot-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.forgot-label {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n}\n\n.forgot-input {\n padding: 12px 14px;\n font-size: 15px;\n border: 1.5px solid #ddd;\n border-radius: 8px;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.forgot-input:focus {\n border-color: #111;\n}\n\n.forgot-input.has-error {\n border-color: #e53935;\n}\n\n.forgot-field-error {\n font-size: 12px;\n color: #e53935;\n}\n\n.forgot-submit-btn {\n padding: 14px 24px;\n font-size: 16px;\n font-weight: 600;\n color: #fff;\n background-color: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s ease;\n margin-top: 8px;\n}\n\n.forgot-submit-btn:hover:not(:disabled) {\n background-color: #333;\n}\n\n.forgot-submit-btn:disabled {\n background-color: #ccc;\n cursor: not-allowed;\n}\n\n.forgot-back-link {\n font-size: 14px;\n text-align: center;\n margin-top: 24px;\n}\n\n.forgot-back-link a {\n color: #111;\n font-weight: 600;\n text-decoration: none;\n}\n\n.forgot-back-link a:hover {\n text-decoration: underline;\n}\n",
120
+ "ikas-config-snippet.json": "{\n \"id\": \"forgot-password\",\n \"name\": \"Forgot Password\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ForgotPassword/index.tsx\",\n \"styles\": \"./src/components/ForgotPassword/styles.css\",\n \"props\": [\n {\n \"name\": \"successMessage\",\n \"displayName\": \"Success Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Password reset link has been sent to your email.\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
125
121
  }
126
122
  },
127
- "contact-form": {
128
- "title": "Contact Form Section",
129
- "description": "Contact form with name, email, phone, and message fields",
123
+ "header": {
124
+ "title": "Header Section",
125
+ "description": "Complete header section with announcement bar, navigation with subLinks (mega-menu), logo, customer icon, mini-cart sidebar with item images/prices/quantity controls/adjustments/checkout, and mobile menu overlay. Cart badge and customer state are automatically reactive in root components via autorun().",
130
126
  "files": {
131
- "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",
132
- "types.ts": "export interface Props {\n title?: string;\n successMessage?: string;\n}\n",
133
- "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",
134
- "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"
127
+ "index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n cartStore,\n customerStore,\n hasCustomer,\n hasCart,\n getIkasOrderTotalItemCount,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderLineItemFormattedFinalPrice,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n changeItemQuantity,\n removeItem,\n getCheckoutUrlFromCartStore,\n getDefaultSrc,\n Router,\n IkasNavigationLink,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function HeaderSection({\n logo,\n navigationLinks,\n announcementText,\n announcementBgColor = \"#111\",\n announcementTextColor = \"#fff\",\n backgroundColor = \"#ffffff\",\n}: Props) {\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const [cartSidebarOpen, setCartSidebarOpen] = useState(false);\n\n const cart = cartStore.cart;\n const isCartLoading = cartStore.isCartLoading;\n const cartHasItems = hasCart(cartStore) as unknown as boolean;\n const itemCount = cart ? (getIkasOrderTotalItemCount(cart) as unknown as number) : 0;\n const isLoggedIn = hasCustomer(customerStore) as unknown as boolean;\n const lineItems = cart?.orderLineItems ?? [];\n const adjustments = cart?.orderAdjustments ?? [];\n\n return (\n <section className=\"header-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n {/* Announcement Bar */}\n {announcementText && (\n <div\n className=\"header-announcement\"\n style={{ backgroundColor: announcementBgColor, color: announcementTextColor }}\n >\n <span>{announcementText}</span>\n </div>\n )}\n\n {/* Main Header */}\n <div className=\"header-main\">\n <div className=\"header-inner\">\n {/* Mobile Hamburger */}\n <button\n className=\"header-hamburger\"\n onClick={() => setMobileMenuOpen(true)}\n aria-label=\"Open menu\"\n >\n <span /><span /><span />\n </button>\n\n {/* Logo */}\n <a className=\"header-logo\" href=\"/\" onClick={(e) => { e.preventDefault(); Router.navigate(\"/\"); }}>\n {logo ? (\n <img src={getDefaultSrc(logo)} alt={logo.altText || \"Store Logo\"} className=\"header-logo-img\" />\n ) : (\n <span className=\"header-logo-text\">Store</span>\n )}\n </a>\n\n {/* Desktop Navigation with subLinks */}\n <nav className=\"header-nav\">\n {(navigationLinks?.links ?? []).map((link: IkasNavigationLink, i: number) => (\n <div key={i} className=\"header-nav-item\">\n <a\n href={link.href}\n className=\"header-nav-link\"\n target={link.openInNewTab ? \"_blank\" : undefined}\n >\n {link.label}\n </a>\n {/* Mega-menu for subLinks */}\n {link.subLinks && link.subLinks.length > 0 && (\n <div className=\"header-submenu\">\n {link.subLinks.map((sub: IkasNavigationLink, j: number) => (\n <a\n key={j}\n href={sub.href}\n className=\"header-submenu-link\"\n target={sub.openInNewTab ? \"_blank\" : undefined}\n >\n {sub.label}\n </a>\n ))}\n </div>\n )}\n </div>\n ))}\n </nav>\n\n {/* Utility Icons */}\n <div className=\"header-icons\">\n <button\n className=\"header-icon-btn\"\n onClick={() => Router.navigateToPage(isLoggedIn ? \"ACCOUNT\" : \"LOGIN\")}\n aria-label=\"Account\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\" />\n <circle cx=\"12\" cy=\"7\" r=\"4\" />\n </svg>\n </button>\n <button\n className=\"header-icon-btn header-cart-btn\"\n onClick={() => setCartSidebarOpen(true)}\n aria-label=\"Cart\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z\" />\n <line x1=\"3\" y1=\"6\" x2=\"21\" y2=\"6\" />\n <path d=\"M16 10a4 4 0 01-8 0\" />\n </svg>\n {itemCount > 0 && <span className=\"header-cart-badge\">{itemCount}</span>}\n </button>\n </div>\n </div>\n </div>\n\n {/* Mini-Cart Sidebar — production uses IkasThemeOverlay for visibility */}\n {cartSidebarOpen && (\n <div className=\"cart-sidebar-overlay\">\n <div className=\"cart-sidebar-backdrop\" onClick={() => setCartSidebarOpen(false)} />\n <div className=\"cart-sidebar\">\n <div className=\"cart-sidebar-header\">\n <h3>Cart ({itemCount})</h3>\n <button className=\"cart-sidebar-close\" onClick={() => setCartSidebarOpen(false)}>&times;</button>\n </div>\n\n {isCartLoading && <div className=\"cart-sidebar-loading\">Loading...</div>}\n\n {!cartHasItems && !isCartLoading && (\n <p className=\"cart-sidebar-empty\">Your cart is empty</p>\n )}\n\n <div className=\"cart-sidebar-items\">\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n const href = item.variant ? getIkasOrderLineVariantHref(item.variant) : undefined;\n return (\n <div key={item.id} className=\"cart-sidebar-item\">\n {image && (\n <a href={href}>\n <img className=\"cart-sidebar-item-img\" src={getDefaultSrc(image)} alt={item.variant?.name || \"\"} />\n </a>\n )}\n <div className=\"cart-sidebar-item-info\">\n <a href={href} className=\"cart-sidebar-item-name\">{item.variant?.name}</a>\n <span className=\"cart-sidebar-item-price\">{getOrderLineItemFormattedFinalPrice(item)}</span>\n <div className=\"cart-sidebar-item-qty\">\n <button onClick={() => changeItemQuantity(item, Math.max(1, item.quantity - 1))}>-</button>\n <span>{item.quantity}</span>\n <button onClick={() => changeItemQuantity(item, item.quantity + 1)}>+</button>\n </div>\n </div>\n <button className=\"cart-sidebar-item-remove\" onClick={() => removeItem(item)}>&times;</button>\n </div>\n );\n })}\n </div>\n\n {/* Order Adjustments (discounts, shipping, etc.) */}\n {adjustments.length > 0 && (\n <div className=\"cart-sidebar-adjustments\">\n {adjustments.map((adj: any, i: number) => (\n <div key={i} className=\"cart-sidebar-adj-row\">\n <span>{getOrderAdjustmentDisplayName(adj)}</span>\n <span>{getOrderAdjustmentFormattedAmount(adj)}</span>\n </div>\n ))}\n </div>\n )}\n\n {cartHasItems && cart && (\n <div className=\"cart-sidebar-footer\">\n <div className=\"cart-sidebar-total\">\n <span>Total</span>\n <span>{getIkasOrderFormattedTotalPrice(cart)}</span>\n </div>\n <a\n href={getCheckoutUrlFromCartStore(cartStore)}\n className=\"cart-sidebar-checkout-btn\"\n >\n Checkout\n </a>\n </div>\n )}\n </div>\n </div>\n )}\n\n {/* Mobile Menu — production uses IkasThemeOverlay visible property */}\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)}>\n &times;\n </button>\n <nav className=\"header-mobile-nav\">\n {(navigationLinks?.links ?? []).map((link: IkasNavigationLink, i: number) => (\n <div key={i}>\n <a\n href={link.href}\n className=\"header-mobile-link\"\n onClick={() => setMobileMenuOpen(false)}\n >\n {link.label}\n </a>\n {link.subLinks?.map((sub: IkasNavigationLink, j: number) => (\n <a\n key={j}\n href={sub.href}\n className=\"header-mobile-sublink\"\n onClick={() => setMobileMenuOpen(false)}\n >\n {sub.label}\n </a>\n ))}\n </div>\n ))}\n </nav>\n </div>\n </div>\n )}\n </section>\n );\n}\n\n",
128
+ "types.ts": "import { IkasImage, IkasNavigationLinkList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n logo?: IkasImage | null;\n navigationLinks?: IkasNavigationLinkList;\n announcementText?: string;\n announcementBgColor?: string;\n announcementTextColor?: string;\n backgroundColor?: string;\n}\n",
129
+ "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 font-weight: 500;\n}\n\n.header-main {\n border-bottom: 1px solid #eee;\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/* Logo */\n.header-logo {\n text-decoration: none;\n flex-shrink: 0;\n}\n\n.header-logo-img {\n height: 40px;\n width: auto;\n display: block;\n}\n\n.header-logo-text {\n font-size: 22px;\n font-weight: 700;\n color: #111;\n}\n\n/* Navigation */\n.header-nav {\n display: flex;\n gap: 24px;\n flex: 1;\n justify-content: center;\n}\n\n.header-nav-link {\n font-size: 14px;\n font-weight: 500;\n color: #333;\n text-decoration: none;\n white-space: nowrap;\n transition: color 0.15s ease;\n}\n\n.header-nav-link:hover {\n color: #111;\n}\n\n/* Icons */\n.header-icons {\n display: flex;\n gap: 12px;\n align-items: center;\n flex-shrink: 0;\n}\n\n.header-icon-btn {\n background: none;\n border: none;\n cursor: pointer;\n color: #333;\n padding: 4px;\n position: relative;\n}\n\n.header-cart-badge {\n position: absolute;\n top: -4px;\n right: -6px;\n background: #111;\n color: #fff;\n font-size: 10px;\n font-weight: 700;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n/* Hamburger */\n.header-hamburger {\n display: none;\n flex-direction: column;\n gap: 4px;\n background: none;\n border: none;\n cursor: pointer;\n padding: 4px;\n}\n\n.header-hamburger span {\n display: block;\n width: 20px;\n height: 2px;\n background: #333;\n}\n\n/* Mobile Menu */\n.header-mobile-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 200;\n}\n\n.header-mobile-backdrop {\n position: absolute;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n}\n\n.header-mobile-menu {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n width: 280px;\n background: #fff;\n padding: 24px;\n overflow-y: auto;\n}\n\n.header-mobile-close {\n font-size: 28px;\n background: none;\n border: none;\n cursor: pointer;\n color: #333;\n margin-bottom: 16px;\n}\n\n.header-mobile-nav {\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.header-mobile-link {\n font-size: 16px;\n font-weight: 500;\n color: #333;\n text-decoration: none;\n padding: 8px 0;\n border-bottom: 1px solid #f0f0f0;\n}\n\n/* SubMenu */\n.header-nav-item {\n position: relative;\n}\n\n.header-submenu {\n display: none;\n position: absolute;\n top: 100%;\n left: 0;\n background: #fff;\n box-shadow: 0 4px 12px rgba(0,0,0,0.1);\n border-radius: 8px;\n padding: 12px 0;\n min-width: 180px;\n z-index: 50;\n}\n\n.header-nav-item:hover .header-submenu {\n display: flex;\n flex-direction: column;\n}\n\n.header-submenu-link {\n padding: 8px 20px;\n font-size: 14px;\n color: #333;\n text-decoration: none;\n white-space: nowrap;\n}\n\n.header-submenu-link:hover {\n background: #f5f5f5;\n color: #111;\n}\n\n/* Cart Sidebar */\n.cart-sidebar-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 300;\n}\n\n.cart-sidebar-backdrop {\n position: absolute;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n}\n\n.cart-sidebar {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 380px;\n max-width: 100%;\n background: #fff;\n display: flex;\n flex-direction: column;\n box-shadow: -4px 0 12px rgba(0,0,0,0.1);\n}\n\n.cart-sidebar-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 16px 20px;\n border-bottom: 1px solid #eee;\n}\n\n.cart-sidebar-header h3 {\n font-size: 18px;\n font-weight: 700;\n margin: 0;\n}\n\n.cart-sidebar-close {\n font-size: 28px;\n background: none;\n border: none;\n cursor: pointer;\n color: #333;\n}\n\n.cart-sidebar-loading {\n padding: 24px 20px;\n text-align: center;\n color: #666;\n font-size: 14px;\n}\n\n.cart-sidebar-empty {\n padding: 48px 20px;\n text-align: center;\n color: #666;\n font-size: 14px;\n}\n\n.cart-sidebar-items {\n flex: 1;\n overflow-y: auto;\n padding: 12px 20px;\n}\n\n.cart-sidebar-item {\n display: flex;\n gap: 12px;\n padding: 12px 0;\n border-bottom: 1px solid #f5f5f5;\n position: relative;\n}\n\n.cart-sidebar-item-img {\n width: 64px;\n height: 64px;\n object-fit: cover;\n border-radius: 6px;\n background: #f5f5f5;\n}\n\n.cart-sidebar-item-info {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.cart-sidebar-item-name {\n font-size: 13px;\n font-weight: 600;\n color: #111;\n text-decoration: none;\n}\n\n.cart-sidebar-item-price {\n font-size: 13px;\n color: #666;\n}\n\n.cart-sidebar-item-qty {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-top: 4px;\n}\n\n.cart-sidebar-item-qty button {\n width: 24px;\n height: 24px;\n border: 1px solid #ddd;\n border-radius: 4px;\n background: #fff;\n cursor: pointer;\n font-size: 14px;\n}\n\n.cart-sidebar-item-qty span {\n font-size: 13px;\n min-width: 20px;\n text-align: center;\n}\n\n.cart-sidebar-item-remove {\n position: absolute;\n top: 12px;\n right: 0;\n background: none;\n border: none;\n font-size: 18px;\n color: #999;\n cursor: pointer;\n}\n\n.cart-sidebar-adjustments {\n padding: 8px 20px;\n border-top: 1px solid #eee;\n}\n\n.cart-sidebar-adj-row {\n display: flex;\n justify-content: space-between;\n font-size: 13px;\n color: #666;\n padding: 4px 0;\n}\n\n.cart-sidebar-footer {\n padding: 16px 20px;\n border-top: 1px solid #eee;\n}\n\n.cart-sidebar-total {\n display: flex;\n justify-content: space-between;\n font-size: 16px;\n font-weight: 700;\n color: #111;\n margin-bottom: 12px;\n}\n\n.cart-sidebar-checkout-btn {\n display: block;\n width: 100%;\n padding: 14px;\n font-size: 15px;\n font-weight: 600;\n color: #fff;\n background: #111;\n border: none;\n border-radius: 8px;\n text-align: center;\n text-decoration: none;\n cursor: pointer;\n}\n\n/* Mobile SubLinks */\n.header-mobile-sublink {\n font-size: 14px;\n color: #666;\n text-decoration: none;\n padding: 6px 0 6px 16px;\n display: block;\n}\n\n/* Responsive */\n@media (max-width: 768px) {\n .header-hamburger {\n display: flex;\n }\n\n .header-nav {\n display: none;\n }\n}\n",
130
+ "ikas-config-snippet.json": "{\n \"id\": \"header\",\n \"name\": \"Header\",\n \"type\": \"section\",\n \"entry\": \"./src/components/Header/index.tsx\",\n \"styles\": \"./src/components/Header/styles.css\",\n \"props\": [\n {\n \"name\": \"logo\",\n \"displayName\": \"Logo\",\n \"type\": \"IMAGE\"\n },\n {\n \"name\": \"navigationLinks\",\n \"displayName\": \"Navigation Links\",\n \"type\": \"LIST_OF_LINK\"\n },\n {\n \"name\": \"announcementText\",\n \"displayName\": \"Announcement Text\",\n \"type\": \"TEXT\"\n },\n {\n \"name\": \"announcementBgColor\",\n \"displayName\": \"Announcement Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#111111\"\n },\n {\n \"name\": \"announcementTextColor\",\n \"displayName\": \"Announcement Text Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
135
131
  }
136
132
  },
137
- "faq": {
138
- "title": "FAQ Section",
139
- "description": "Accordion-style frequently asked questions section",
133
+ "hero-banner": {
134
+ "title": "Hero Banner Section",
135
+ "description": "Full-width hero banner with heading, subtitle, CTA button, and background image.",
140
136
  "files": {
141
- "index.tsx": "import { useState } from \"preact/hooks\";\nimport { Props, FaqItem } from \"./types\";\n\nexport default function FaqSection({\n title = \"Frequently Asked Questions\",\n items = [],\n}: Props) {\n const [openIndex, setOpenIndex] = useState<number | null>(null);\n\n const handleToggle = (index: number) => {\n setOpenIndex(openIndex === index ? null : index);\n };\n\n return (\n <section className=\"faq-section\">\n <div className=\"faq-inner\">\n <h2 className=\"faq-title\">{title}</h2>\n {items.length === 0 && <p className=\"faq-empty\">No questions added yet.</p>}\n <div className=\"faq-list\">\n {items.map((item: FaqItem, index: number) => {\n const isOpen = openIndex === index;\n return (\n <div key={index} className={`faq-item ${isOpen ? \"faq-item-open\" : \"\"}`}>\n <button className=\"faq-question\" onClick={() => handleToggle(index)} aria-expanded={isOpen}>\n <span>{item.question}</span>\n <span className=\"faq-icon\">{isOpen ? \"\\u2212\" : \"+\"}</span>\n </button>\n {isOpen && (\n <div className=\"faq-answer\"><p>{item.answer}</p></div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n </section>\n );\n}\n",
142
- "types.ts": "export interface FaqItem {\n question: string;\n answer: string;\n}\n\nexport interface Props {\n title?: string;\n items?: FaqItem[];\n}\n",
143
- "styles.css": ".faq-section {\n width: 100%;\n padding: 64px 24px;\n}\n\n.faq-inner {\n max-width: 720px;\n margin: 0 auto;\n}\n\n.faq-title { font-size: 28px; font-weight: 700; color: #111; margin: 0 0 32px 0; text-align: center; }\n.faq-empty { font-size: 16px; color: #666; text-align: center; }\n.faq-list { display: flex; flex-direction: column; }\n.faq-item { border-bottom: 1px solid #eee; }\n.faq-question { width: 100%; display: flex; justify-content: space-between; align-items: center; padding: 20px 0; font-size: 16px; font-weight: 500; color: #111; background: none; border: none; cursor: pointer; text-align: left; gap: 16px; }\n.faq-question:hover { color: #333; }\n.faq-icon { font-size: 20px; font-weight: 300; flex-shrink: 0; color: #666; }\n.faq-answer { padding: 0 0 20px 0; }\n.faq-answer p { font-size: 15px; line-height: 1.7; color: #555; margin: 0; }\n.faq-item-open .faq-question { font-weight: 600; }\n",
144
- "ikas-config-snippet.json": "{\n \"id\": \"faq\",\n \"name\": \"FAQ\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"Frequently Asked Questions\" }\n ]\n}\n"
137
+ "index.tsx": "import { getDefaultSrc, Router } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function HeroBannerSection({\n heading = \"Welcome to Our Store\",\n subtitle,\n buttonText = \"Shop Now\",\n buttonLink = \"/\",\n backgroundImage,\n backgroundColor = \"#111\",\n textColor = \"#fff\",\n}: Props) {\n const bgStyle: Record<string, string> = { backgroundColor, color: textColor };\n if (backgroundImage) {\n bgStyle.backgroundImage = `url(${getDefaultSrc(backgroundImage)})`;\n bgStyle.backgroundSize = \"cover\";\n bgStyle.backgroundPosition = \"center\";\n }\n\n return (\n <section className=\"hero-banner\" style={bgStyle}>\n <div className=\"hero-inner\">\n <h1 className=\"hero-heading\">{heading}</h1>\n {subtitle && <p className=\"hero-subtitle\">{subtitle}</p>}\n {buttonText && (\n <button\n className=\"hero-cta\"\n onClick={() => Router.navigate(buttonLink)}\n >\n {buttonText}\n </button>\n )}\n </div>\n </section>\n );\n}\n\nexport default HeroBannerSection;\n",
138
+ "types.ts": "import { IkasImage } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n heading?: string;\n subtitle?: string;\n buttonText?: string;\n buttonLink?: string;\n backgroundImage?: IkasImage | null;\n backgroundColor?: string;\n textColor?: string;\n}\n",
139
+ "styles.css": ".hero-banner {\n width: 100%;\n min-height: 480px;\n display: flex;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: 64px 24px;\n}\n\n.hero-inner {\n max-width: 720px;\n}\n\n.hero-heading {\n font-size: 48px;\n font-weight: 800;\n margin: 0 0 16px 0;\n line-height: 1.1;\n}\n\n.hero-subtitle {\n font-size: 18px;\n opacity: 0.85;\n margin: 0 0 32px 0;\n line-height: 1.5;\n}\n\n.hero-cta {\n display: inline-block;\n padding: 14px 32px;\n font-size: 16px;\n font-weight: 600;\n color: #111;\n background: #fff;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: opacity 0.15s;\n}\n\n.hero-cta:hover {\n opacity: 0.9;\n}\n\n@media (max-width: 768px) {\n .hero-banner { min-height: 360px; padding: 48px 24px; }\n .hero-heading { font-size: 32px; }\n .hero-subtitle { font-size: 16px; }\n}\n",
140
+ "ikas-config-snippet.json": "{\n \"id\": \"hero-banner\",\n \"name\": \"Hero Banner\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"heading\", \"displayName\": \"Heading\", \"type\": \"TEXT\", \"defaultValue\": \"Welcome to Our Store\" },\n { \"name\": \"subtitle\", \"displayName\": \"Subtitle\", \"type\": \"TEXT\" },\n { \"name\": \"buttonText\", \"displayName\": \"Button Text\", \"type\": \"TEXT\", \"defaultValue\": \"Shop Now\" },\n { \"name\": \"buttonLink\", \"displayName\": \"Button Link\", \"type\": \"TEXT\", \"defaultValue\": \"/\" },\n { \"name\": \"backgroundImage\", \"displayName\": \"Background Image\", \"type\": \"IMAGE\" },\n { \"name\": \"backgroundColor\", \"displayName\": \"Background Color\", \"type\": \"COLOR\", \"defaultValue\": \"#111\" },\n { \"name\": \"textColor\", \"displayName\": \"Text Color\", \"type\": \"COLOR\", \"defaultValue\": \"#fff\" }\n ]\n}\n"
145
141
  }
146
142
  },
147
- "blog-list": {
148
- "title": "Blog List Section",
149
- "description": "Blog post grid with images, dates, summaries, and pagination",
143
+ "login-page": {
144
+ "title": "Login Section",
145
+ "description": "Complete login section with form validation, error display, loading state, forgot password link, register redirect, social login (socialLogin + SocialLoginProvider), and handleSocialLogin callback with HandleSocialLoginReturnType status handling.",
150
146
  "files": {
151
- "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.data ?? [];\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",
152
- "types.ts": "import { IkasBlogList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogList: IkasBlogList;\n title?: string;\n}\n",
153
- "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",
154
- "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"
147
+ "index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getLoginForm,\n initLoginForm,\n setLoginFormEmail,\n setLoginFormPassword,\n submitLoginForm,\n handleSocialLogin,\n socialLogin,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function LoginSection({\n redirectAfterLogin = \"/account\",\n showForgotPassword = true,\n backgroundColor = \"#ffffff\",\n}: Props) {\n const loginForm = getLoginForm(customerStore);\n\n useEffect(() => {\n initLoginForm(loginForm);\n // Handle social login callback if returning from OAuth redirect\n handleSocialLogin(customerStore).then((result) => {\n // HandleSocialLoginReturnType: { status: \"success\" | \"fail\", message?: string }\n if (result.status === \"success\") {\n Router.navigate(redirectAfterLogin);\n }\n });\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitLoginForm(loginForm);\n if (success) {\n Router.navigate(redirectAfterLogin);\n }\n };\n\n const handleSocialLoginClick = async (provider: \"GOOGLE\" | \"FACEBOOK\" | \"APPLE\") => {\n // SocialLoginProvider enum — redirects user to provider's OAuth page\n await socialLogin(customerStore, provider as any);\n };\n\n return (\n <section className=\"login-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"login-inner\">\n <h1 className=\"login-title\">Sign In</h1>\n\n {loginForm.isFailure && loginForm.responseMessage && (\n <div className=\"login-error-banner\">{loginForm.responseMessage}</div>\n )}\n\n {/* Social Login Buttons */}\n <div className=\"login-social\">\n <button className=\"login-social-btn\" onClick={() => handleSocialLoginClick(\"GOOGLE\")}>\n Continue with Google\n </button>\n <button className=\"login-social-btn\" onClick={() => handleSocialLoginClick(\"FACEBOOK\")}>\n Continue with Facebook\n </button>\n </div>\n\n <div className=\"login-divider\"><span>or</span></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\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\n {showForgotPassword && (\n <a\n className=\"login-forgot-link\"\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n Router.navigateToPage(\"FORGOT_PASSWORD\");\n }}\n >\n Forgot password?\n </a>\n )}\n\n <button\n className=\"login-submit-btn\"\n type=\"submit\"\n disabled={loginForm.isSubmitting}\n >\n {loginForm.isSubmitting ? \"Signing in...\" : \"Sign In\"}\n </button>\n </form>\n\n <p className=\"login-register-link\">\n Don't have an account?{\" \"}\n <a\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n Router.navigateToPage(\"REGISTER\");\n }}\n >\n Create one\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
148
+ "types.ts": "export interface Props {\n redirectAfterLogin?: string;\n showForgotPassword?: boolean;\n backgroundColor?: string;\n}\n",
149
+ "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 {\n font-size: 28px;\n font-weight: 700;\n color: #111;\n margin: 0 0 24px 0;\n text-align: center;\n}\n\n.login-error-banner {\n padding: 12px 16px;\n font-size: 14px;\n color: #b71c1c;\n background-color: #ffebee;\n border-radius: 8px;\n margin-bottom: 20px;\n}\n\n.login-form {\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.login-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.login-label {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n}\n\n.login-input {\n padding: 12px 14px;\n font-size: 15px;\n border: 1.5px solid #ddd;\n border-radius: 8px;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.login-input:focus {\n border-color: #111;\n}\n\n.login-input.has-error {\n border-color: #e53935;\n}\n\n.login-field-error {\n font-size: 12px;\n color: #e53935;\n}\n\n.login-forgot-link {\n font-size: 13px;\n color: #666;\n text-decoration: none;\n align-self: flex-end;\n}\n\n.login-forgot-link:hover {\n color: #111;\n}\n\n.login-submit-btn {\n padding: 14px 24px;\n font-size: 16px;\n font-weight: 600;\n color: #fff;\n background-color: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s ease;\n margin-top: 8px;\n}\n\n.login-submit-btn:hover:not(:disabled) {\n background-color: #333;\n}\n\n.login-submit-btn:disabled {\n background-color: #ccc;\n cursor: not-allowed;\n}\n\n.login-register-link {\n font-size: 14px;\n color: #666;\n text-align: center;\n margin-top: 24px;\n}\n\n.login-register-link a {\n color: #111;\n font-weight: 600;\n text-decoration: none;\n}\n\n.login-register-link a:hover {\n text-decoration: underline;\n}\n",
150
+ "ikas-config-snippet.json": "{\n \"id\": \"login-page\",\n \"name\": \"Login Page\",\n \"type\": \"section\",\n \"entry\": \"./src/components/LoginSection/index.tsx\",\n \"styles\": \"./src/components/LoginSection/styles.css\",\n \"props\": [\n {\n \"name\": \"redirectAfterLogin\",\n \"displayName\": \"Redirect After Login\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"/account\"\n },\n {\n \"name\": \"showForgotPassword\",\n \"displayName\": \"Show Forgot Password Link\",\n \"type\": \"BOOLEAN\",\n \"defaultValue\": true\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
155
151
  }
156
152
  },
157
- "blog-post": {
158
- "title": "Blog Post Section",
159
- "description": "Single blog post detail page with image, title, date, and HTML content",
153
+ "order-detail": {
154
+ "title": "Order Detail Section",
155
+ "description": "Complete order detail section with getOrder fetch, line items with variant images/links/discount, order adjustments, payment transactions, totals, package status, and refund functionality. Uses getPageParams for order ID from URL.",
160
156
  "files": {
161
- "index.tsx": "import {\n getIkasBlogFormattedDate,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function BlogPostSection({\n blogPost,\n}: Props) {\n if (!blogPost) return null;\n\n return (\n <section className=\"blog-post-section\">\n <div className=\"blog-post-inner\">\n {blogPost.image && (\n <img src={getDefaultSrc(blogPost.image)} alt={blogPost.title} className=\"blog-post-hero\" />\n )}\n <h1 className=\"blog-post-title\">{blogPost.title}</h1>\n <span className=\"blog-post-date\">{getIkasBlogFormattedDate(blogPost)}</span>\n <div className=\"blog-post-content\" dangerouslySetInnerHTML={{ __html: blogPost.content }} />\n </div>\n </section>\n );\n}\n",
162
- "types.ts": "import { IkasBlogPost } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogPost: IkasBlogPost;\n}\n",
163
- "styles.css": ".blog-post-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.blog-post-inner {\n max-width: 720px;\n margin: 0 auto;\n}\n\n.blog-post-hero { width: 100%; aspect-ratio: 16/9; object-fit: cover; border-radius: 8px; margin-bottom: 32px; }\n.blog-post-title { font-size: 32px; font-weight: 700; color: #111; margin: 0 0 8px 0; line-height: 1.3; }\n.blog-post-date { font-size: 14px; color: #999; display: block; margin-bottom: 32px; }\n.blog-post-content { font-size: 16px; line-height: 1.8; color: #333; }\n.blog-post-content img { max-width: 100%; height: auto; border-radius: 8px; margin: 24px 0; }\n.blog-post-content h2 { font-size: 24px; font-weight: 700; margin: 32px 0 16px; }\n.blog-post-content h3 { font-size: 20px; font-weight: 600; margin: 24px 0 12px; }\n.blog-post-content p { margin: 0 0 16px 0; }\n",
164
- "ikas-config-snippet.json": "{\n \"id\": \"blog-post\",\n \"name\": \"Blog Post\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"blogPost\", \"displayName\": \"Blog Post\", \"type\": \"BLOG_POST\", \"required\": true }\n ]\n}\n"
157
+ "index.tsx": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderFormattedShippingTotal,\n getIkasOrderFormattedTotalTax,\n getIkasOrderDisplayedPackages,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n hasOrderLineItemDiscount,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getOrderAdjustmentIsDecrement,\n getOrderTransactionFormattedAmount,\n getOrderTransactionPaymentMethodTranslation,\n getIkasOrderRefundableItems,\n isIkasOrderRefundable,\n getOrderLineItemRefundQuantity,\n setOrderLineItemRefundQuantity,\n refundOrder,\n getDefaultSrc,\n Router,\n IkasOrder,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function OrderDetailSection({ backgroundColor = \"#ffffff\" }: 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 <div className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>Loading...</p></div></div>;\n if (!order) return <div className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>Order not found.</p></div></div>;\n\n const packages = getIkasOrderDisplayedPackages(order);\n const lineItems = order.orderLineItems ?? [];\n const adjustments = order.orderAdjustments ?? [];\n const transactions = order.orderTransactions ?? [];\n const canRefund = isIkasOrderRefundable(order) as unknown as boolean;\n const refundableItems = canRefund ? getIkasOrderRefundableItems(order) : [];\n\n return (\n <section className=\"order-detail-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"order-detail-inner\">\n <h1>Order #{order.orderNumber}</h1>\n <p>{getIkasOrderFormattedOrderedAt(order)}</p>\n\n {packages.map((pkg, i) => (\n <span key={i} className=\"order-status-badge\">{getIkasOrderPackageStatusTranslation(order)}</span>\n ))}\n\n <div className=\"order-items\">\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n const href = item.variant ? getIkasOrderLineVariantHref(item.variant) : undefined;\n const hasDiscount = hasOrderLineItemDiscount(item) as unknown as boolean;\n return (\n <div key={item.id} className=\"order-item\">\n {image && <a href={href}><img src={getDefaultSrc(image)} width={64} height={64} style={{ objectFit: \"cover\", borderRadius: 4 }} alt=\"\" /></a>}\n <div>\n <a href={href}>{item.variant?.name}</a>\n <span> x{item.quantity}</span>\n <span> {getOrderLineItemFormattedFinalPriceWithQuantity(item)}</span>\n </div>\n </div>\n );\n })}\n </div>\n\n {adjustments.length > 0 && adjustments.map((adj: any, i: number) => (\n <div key={i}>\n <span>{getOrderAdjustmentDisplayName(adj)}: </span>\n <span style={{ color: getOrderAdjustmentIsDecrement(adj) ? \"green\" : \"inherit\" }}>{getOrderAdjustmentFormattedAmount(adj)}</span>\n </div>\n ))}\n\n {transactions.map((tx: any, i: number) => (\n <div key={i}>{getOrderTransactionPaymentMethodTranslation(tx)}: {getOrderTransactionFormattedAmount(tx)}</div>\n ))}\n\n <div style={{ marginTop: 16, borderTop: \"1px solid #eee\", paddingTop: 16 }}>\n <div>Shipping: {getIkasOrderFormattedShippingTotal(order)}</div>\n <div>Tax: {getIkasOrderFormattedTotalTax(order)}</div>\n <div>Subtotal: {getIkasOrderFormattedTotalPrice(order)}</div>\n <div style={{ fontWeight: 700, fontSize: 18 }}>Total: {getIkasOrderFormattedTotalFinalPrice(order)}</div>\n </div>\n\n {canRefund && (\n <div style={{ marginTop: 24 }}>\n <h3>Refund</h3>\n {refundableItems.map((item: any) => (\n <div key={item.id}>\n {item.variant?.name}: <input type=\"number\" min={0} max={item.quantity} value={getOrderLineItemRefundQuantity(item) ?? 0}\n onChange={(e) => setOrderLineItemRefundQuantity(Number((e.target as HTMLInputElement).value), item)} style={{ width: 60 }} />\n </div>\n ))}\n <button onClick={() => refundOrder(customerStore, order)} style={{ marginTop: 8 }}>Submit Refund</button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
158
+ "types.ts": "export interface Props {\n backgroundColor?: string;\n}\n",
159
+ "styles.css": ".order-detail-section { width: 100%; padding: 40px 24px; }\n.order-detail-inner { max-width: 800px; margin: 0 auto; }\n",
160
+ "ikas-config-snippet.json": "{\n \"id\": \"order-detail\",\n \"name\": \"Order Detail\",\n \"type\": \"section\",\n \"props\": [\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
165
161
  }
166
162
  },
167
- "product-reviews": {
168
- "title": "Product Reviews Section",
169
- "description": "Product reviews display with star ratings and review submission form",
163
+ "product-detail": {
164
+ "title": "Product Detail Section",
165
+ "description": "Complete product detail section with breadcrumb, image gallery (IkasThemeSlider pattern), variant selection, pricing with discount amount, add-to-cart with result handling, favorites, brand link, product attributes, and bundle products. Based on production dynavit.json patterns.",
170
166
  "files": {
171
- "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",
172
- "types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n title?: string;\n}\n",
173
- "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",
174
- "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"
167
+ "index.tsx": "import { useState, useEffect } from \"preact/hooks\";\nimport {\n getSelectedProductVariant,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n hasProductVariantStock,\n hasProductStock,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantDiscountPercentage,\n getProductVariantFormattedDiscountAmount,\n isAddToCartEnabled,\n addItemToCart,\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n getProductVariantMainImage,\n getDefaultSrc,\n getThumbnailSrc,\n getSrc,\n createMediaSrcset,\n getProductCategoryPath,\n getIkasCategoryPathItemHref,\n getIkasBrandHref,\n getAttributeListValues,\n hasBundleSettings,\n initBundleProducts,\n getDisplayedProductGroups,\n isNotEmpty,\n IkasImage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductDetail({\n product,\n showFavoriteButton = true,\n addToCartButtonText = \"Add to Cart\",\n backgroundColor = \"#ffffff\",\n}: Props) {\n const [selectedImageIndex, setSelectedImageIndex] = useState(0);\n const [isAddingToCart, setIsAddingToCart] = useState(false);\n const [quantity, setQuantity] = useState(1);\n\n useEffect(() => {\n if (product) {\n const variant = getSelectedProductVariant(product);\n if (variant && hasBundleSettings(variant)) {\n initBundleProducts(product);\n }\n }\n }, [product]);\n\n if (!product) {\n return null;\n }\n\n const selectedVariant = getSelectedProductVariant(product) as any;\n const variantTypes = getDisplayedProductVariantTypes(product);\n const inStock = hasProductStock(product) as unknown as boolean;\n const variantInStock = hasProductVariantStock(selectedVariant) as unknown as boolean;\n const canAddToCart = isAddToCartEnabled(product) as unknown as boolean;\n\n // Pricing\n const hasDiscount = hasProductVariantDiscount(selectedVariant) as unknown as boolean;\n const finalPrice = getProductVariantFormattedFinalPrice(selectedVariant) as unknown as string;\n const originalPrice = hasDiscount\n ? (getProductVariantFormattedSellPrice(selectedVariant) as unknown as string)\n : null;\n const discountPercentage = hasDiscount\n ? (getProductVariantDiscountPercentage(selectedVariant) as unknown as number)\n : null;\n const discountAmount = hasDiscount\n ? (getProductVariantFormattedDiscountAmount(selectedVariant) as unknown as string)\n : null;\n\n // Breadcrumb\n const categoryPath = getProductCategoryPath(product);\n\n // Images — in production, dynavit uses IkasThemeSlider for image carousel\n const mainProductImage = getProductVariantMainImage(selectedVariant);\n const mainImage = mainProductImage?.image;\n const variantImages = selectedVariant?.images;\n const images: IkasImage[] = variantImages?.length\n ? variantImages\n .map((pi: any) => pi.image)\n .filter((img: any): img is IkasImage => img != null)\n : mainImage\n ? [mainImage]\n : [];\n const currentImage = images[selectedImageIndex] ?? images[0];\n\n // Favorites\n const isFavorite = isFavoriteIkasProduct(product);\n\n // Attributes\n const attributes = product.attributeList\n ? getAttributeListValues(product.attributeList)\n : [];\n\n // Bundle / Offer products\n const hasBundle = selectedVariant ? (hasBundleSettings(selectedVariant) as unknown as boolean) : false;\n const productGroups = hasBundle ? getDisplayedProductGroups(product) : [];\n\n const handleAddToCart = async () => {\n if (!canAddToCart || isAddingToCart) return;\n setIsAddingToCart(true);\n try {\n const result = await addItemToCart(selectedVariant, product, quantity);\n if (result.success) {\n setQuantity(1);\n }\n } finally {\n setIsAddingToCart(false);\n }\n };\n\n const handleToggleFavorite = async () => {\n if (isFavorite) {\n await removeIkasProductFromFavorites(product);\n } else {\n await addIkasProductToFavorites(product);\n }\n };\n\n return (\n <section className=\"product-detail\" style={backgroundColor ? { backgroundColor } : undefined}>\n {/* Breadcrumb */}\n {isNotEmpty(categoryPath) && (\n <nav className=\"product-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((pathItem: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\">/</span>\n <a href={getIkasCategoryPathItemHref(pathItem)}>{pathItem.name}</a>\n </span>\n ))}\n <span className=\"breadcrumb-sep\">/</span>\n <span className=\"breadcrumb-current\">{product.name}</span>\n </nav>\n )}\n\n <div className=\"product-detail-inner\">\n {/* Image Gallery — production uses IkasThemeSlider for carousel */}\n <div className=\"product-gallery\">\n {currentImage && (\n <img\n className=\"product-main-image\"\n src={getDefaultSrc(currentImage)}\n srcSet={createMediaSrcset(currentImage)}\n sizes=\"(max-width: 768px) 100vw, 50vw\"\n alt={currentImage.altText || product.name}\n />\n )}\n {images.length > 1 && (\n <div className=\"product-thumbnails\">\n {images.map((img, i) => (\n <button\n key={img.id || i}\n className={`product-thumbnail-btn ${i === selectedImageIndex ? \"active\" : \"\"}`}\n onClick={() => setSelectedImageIndex(i)}\n >\n <img src={getThumbnailSrc(img)} alt={`${product.name} ${i + 1}`} />\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Product Info */}\n <div className=\"product-info\">\n {/* Brand with link */}\n {product.brand && (\n <a className=\"product-brand\" href={getIkasBrandHref(product.brand)}>\n {product.brand.name}\n </a>\n )}\n <h1 className=\"product-name\">{product.name}</h1>\n\n {/* Average Rating */}\n {product.averageRating > 0 && (\n <div className=\"product-rating\">\n {\"★\".repeat(Math.round(product.averageRating))}\n {\"☆\".repeat(5 - Math.round(product.averageRating))}\n <span className=\"product-rating-value\">({product.averageRating.toFixed(1)})</span>\n </div>\n )}\n\n {/* Campaign badges */}\n {isNotEmpty(product.campaigns) && (\n <div className=\"product-campaigns\">\n {product.campaigns.map((campaign: any) => (\n <span key={campaign.id} className=\"product-campaign-badge\">\n {campaign.name}\n </span>\n ))}\n </div>\n )}\n\n {/* Pricing */}\n <div className=\"product-pricing\">\n <span className=\"product-final-price\">{finalPrice}</span>\n {originalPrice && <span className=\"product-original-price\">{originalPrice}</span>}\n {discountPercentage != null && (\n <span className=\"product-discount-badge\">-{discountPercentage}%</span>\n )}\n {discountAmount && (\n <span className=\"product-savings\">Save {discountAmount}</span>\n )}\n </div>\n\n {/* Variant Selection */}\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\n {/* Stock notice */}\n {!inStock && <span className=\"out-of-stock-notice\">Out of Stock</span>}\n\n {/* Quantity + Add to Cart */}\n <div className=\"product-actions\">\n <div className=\"product-quantity\">\n <button onClick={() => setQuantity(Math.max(1, quantity - 1))}>-</button>\n <span>{quantity}</span>\n <button onClick={() => setQuantity(quantity + 1)}>+</button>\n </div>\n <button\n className=\"add-to-cart-btn\"\n disabled={!canAddToCart || isAddingToCart}\n onClick={handleAddToCart}\n >\n {isAddingToCart ? \"Adding...\" : !variantInStock ? \"Out of Stock\" : addToCartButtonText}\n </button>\n {showFavoriteButton && (\n <button\n className={`favorite-btn ${isFavorite ? \"is-favorite\" : \"\"}`}\n onClick={handleToggleFavorite}\n aria-label={isFavorite ? \"Remove from favorites\" : \"Add to favorites\"}\n >\n {isFavorite ? \"\\u2665\" : \"\\u2661\"}\n </button>\n )}\n </div>\n\n {/* Description */}\n {product.description && (\n <div className=\"product-description\">\n <h3>Description</h3>\n <div dangerouslySetInnerHTML={{ __html: product.description }} />\n </div>\n )}\n\n {/* Product Attributes (e.g. ingredients, specs) */}\n {isNotEmpty(attributes) && (\n <div className=\"product-attributes\">\n <h3>Details</h3>\n {attributes.map((attr: any, i: number) => (\n <div key={i} className=\"attribute-row\">\n <span className=\"attribute-name\">{attr.name}</span>\n <span className=\"attribute-value\">{attr.value}</span>\n </div>\n ))}\n </div>\n )}\n\n {/* Bundle / Offer Products */}\n {isNotEmpty(productGroups) && (\n <div className=\"product-bundles\">\n <h3>Frequently Bought Together</h3>\n {productGroups.map((group: any, i: number) => (\n <div key={i} className=\"bundle-group\">\n {group.title && <h4>{group.title}</h4>}\n <div className=\"bundle-items\">\n {group.products?.map((bp: any) => (\n <div key={bp.product?.id} className=\"bundle-item\">\n <span>{bp.product?.name}</span>\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n </section>\n );\n}\n\n",
168
+ "types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n showFavoriteButton?: boolean;\n addToCartButtonText?: string;\n backgroundColor?: string;\n}\n",
169
+ "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 align-items: start;\n}\n\n/* Image Gallery */\n.product-gallery {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.product-main-image {\n width: 100%;\n aspect-ratio: 1;\n object-fit: cover;\n border-radius: 8px;\n background-color: #f5f5f5;\n}\n\n.product-thumbnails {\n display: flex;\n gap: 8px;\n overflow-x: auto;\n}\n\n.product-thumbnail-btn {\n flex-shrink: 0;\n width: 72px;\n height: 72px;\n padding: 0;\n border: 2px solid transparent;\n border-radius: 6px;\n cursor: pointer;\n overflow: hidden;\n background: none;\n}\n\n.product-thumbnail-btn.active {\n border-color: #111;\n}\n\n.product-thumbnail-btn img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n/* Product Info */\n.product-info {\n display: flex;\n flex-direction: column;\n gap: 20px;\n}\n\n.product-brand {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: #666;\n text-decoration: none;\n}\n\n.product-brand:hover {\n color: #111;\n}\n\n.product-name {\n font-size: 28px;\n font-weight: 700;\n line-height: 1.2;\n color: #111;\n margin: 0;\n}\n\n/* Pricing */\n.product-pricing {\n display: flex;\n align-items: baseline;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.product-final-price {\n font-size: 26px;\n font-weight: 700;\n color: #111;\n}\n\n.product-original-price {\n font-size: 18px;\n color: #999;\n text-decoration: line-through;\n}\n\n.product-discount-badge {\n font-size: 13px;\n font-weight: 600;\n color: #fff;\n background-color: #e53935;\n padding: 2px 8px;\n border-radius: 4px;\n}\n\n/* Variants */\n.product-variants {\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.variant-group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.variant-group-label {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n}\n\n.variant-options {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.variant-option-btn {\n padding: 8px 16px;\n border: 1.5px solid #ddd;\n border-radius: 6px;\n background: #fff;\n font-size: 14px;\n cursor: pointer;\n transition: all 0.15s ease;\n color: #333;\n}\n\n.variant-option-btn:hover:not(:disabled) {\n border-color: #111;\n}\n\n.variant-option-btn.selected {\n border-color: #111;\n background-color: #111;\n color: #fff;\n}\n\n.variant-option-btn:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n text-decoration: line-through;\n}\n\n/* Actions */\n.product-actions {\n display: flex;\n gap: 12px;\n align-items: stretch;\n}\n\n.add-to-cart-btn {\n flex: 1;\n padding: 14px 24px;\n font-size: 16px;\n font-weight: 600;\n color: #fff;\n background-color: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s ease;\n}\n\n.add-to-cart-btn:hover:not(:disabled) {\n background-color: #333;\n}\n\n.add-to-cart-btn:disabled {\n background-color: #ccc;\n cursor: not-allowed;\n}\n\n.favorite-btn {\n width: 48px;\n height: 48px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 1.5px solid #ddd;\n border-radius: 8px;\n background: #fff;\n cursor: pointer;\n font-size: 20px;\n transition: all 0.15s ease;\n flex-shrink: 0;\n}\n\n.favorite-btn:hover {\n border-color: #e53935;\n}\n\n.favorite-btn.is-favorite {\n color: #e53935;\n border-color: #e53935;\n}\n\n/* Description */\n.product-description {\n padding-top: 20px;\n border-top: 1px solid #eee;\n}\n\n.out-of-stock-notice {\n font-size: 14px;\n font-weight: 600;\n color: #e53935;\n}\n\n/* Breadcrumb */\n.product-breadcrumb {\n max-width: 1200px;\n margin: 0 auto 16px;\n padding: 0 24px;\n font-size: 13px;\n color: #666;\n}\n\n.product-breadcrumb a {\n color: #666;\n text-decoration: none;\n}\n\n.product-breadcrumb a:hover {\n color: #111;\n}\n\n.breadcrumb-sep {\n margin: 0 8px;\n color: #ccc;\n}\n\n.breadcrumb-current {\n color: #111;\n}\n\n/* Rating */\n.product-rating {\n color: #f5a623;\n font-size: 16px;\n}\n\n.product-rating-value {\n font-size: 14px;\n color: #666;\n margin-left: 8px;\n}\n\n/* Campaigns */\n.product-campaigns {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.product-campaign-badge {\n font-size: 12px;\n font-weight: 600;\n color: #fff;\n background: #1976d2;\n padding: 4px 10px;\n border-radius: 4px;\n}\n\n/* Savings */\n.product-savings {\n font-size: 14px;\n font-weight: 600;\n color: #2e7d32;\n}\n\n/* Quantity */\n.product-quantity {\n display: flex;\n align-items: center;\n gap: 8px;\n border: 1.5px solid #ddd;\n border-radius: 8px;\n overflow: hidden;\n}\n\n.product-quantity button {\n width: 40px;\n height: 48px;\n border: none;\n background: #fff;\n font-size: 18px;\n cursor: pointer;\n}\n\n.product-quantity span {\n width: 32px;\n text-align: center;\n font-size: 15px;\n font-weight: 600;\n}\n\n/* Attributes */\n.product-attributes {\n padding-top: 20px;\n border-top: 1px solid #eee;\n}\n\n.attribute-row {\n display: flex;\n justify-content: space-between;\n padding: 8px 0;\n font-size: 14px;\n border-bottom: 1px solid #f5f5f5;\n}\n\n.attribute-name {\n color: #666;\n font-weight: 500;\n}\n\n.attribute-value {\n color: #111;\n}\n\n/* Bundle Products */\n.product-bundles {\n padding-top: 20px;\n border-top: 1px solid #eee;\n}\n\n.bundle-group {\n margin-top: 12px;\n}\n\n.bundle-items {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.bundle-item {\n padding: 12px;\n border: 1px solid #eee;\n border-radius: 6px;\n font-size: 14px;\n}\n\n/* Responsive */\n@media (max-width: 768px) {\n .product-detail-inner {\n grid-template-columns: 1fr;\n gap: 32px;\n }\n\n .product-name {\n font-size: 22px;\n }\n\n .product-final-price {\n font-size: 22px;\n }\n\n .product-actions {\n flex-wrap: wrap;\n }\n}\n",
170
+ "ikas-config-snippet.json": "{\n \"id\": \"product-detail\",\n \"name\": \"Product Detail\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ProductDetail/index.tsx\",\n \"styles\": \"./src/components/ProductDetail/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": true\n },\n {\n \"name\": \"showFavoriteButton\",\n \"displayName\": \"Show Favorite Button\",\n \"type\": \"BOOLEAN\",\n \"defaultValue\": true\n },\n {\n \"name\": \"addToCartButtonText\",\n \"displayName\": \"Add to Cart Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Add to Cart\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
175
171
  }
176
172
  },
177
- "hero-banner": {
178
- "title": "Hero Banner Section",
179
- "description": "Full-width hero banner with heading, subtitle, CTA button, and background image",
173
+ "product-list": {
174
+ "title": "Product List Section",
175
+ "description": "Complete product list section with category breadcrumb (getCategoryPath + getIkasCategoryHref), filter sidebar with handleFilterValueClick and onFilterCategoryClick, sort via setSortType (not direct mutation), search via searchProductList, product grid, and pagination with setProductListVisiblePage. Data is automatically reactive in root components via autorun().",
180
176
  "files": {
181
- "index.tsx": "import { getDefaultSrc, Router } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function HeroBannerSection({\n heading = \"Welcome to Our Store\",\n subtitle,\n buttonText = \"Shop Now\",\n buttonLink = \"/\",\n backgroundImage,\n backgroundColor = \"#111\",\n textColor = \"#fff\",\n}: Props) {\n const bgStyle: Record<string, string> = { backgroundColor, color: textColor };\n if (backgroundImage) {\n bgStyle.backgroundImage = `url(${getDefaultSrc(backgroundImage)})`;\n bgStyle.backgroundSize = \"cover\";\n bgStyle.backgroundPosition = \"center\";\n }\n\n return (\n <section className=\"hero-banner\" style={bgStyle}>\n <div className=\"hero-inner\">\n <h1 className=\"hero-heading\">{heading}</h1>\n {subtitle && <p className=\"hero-subtitle\">{subtitle}</p>}\n {buttonText && (\n <button\n className=\"hero-cta\"\n onClick={() => Router.navigate(buttonLink)}\n >\n {buttonText}\n </button>\n )}\n </div>\n </section>\n );\n}\n",
182
- "types.ts": "import { IkasImage } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n heading?: string;\n subtitle?: string;\n buttonText?: string;\n buttonLink?: string;\n backgroundImage?: IkasImage | null;\n backgroundColor?: string;\n textColor?: string;\n}\n",
183
- "styles.css": ".hero-banner {\n width: 100%;\n min-height: 480px;\n display: flex;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: 64px 24px;\n}\n\n.hero-inner {\n max-width: 720px;\n}\n\n.hero-heading {\n font-size: 48px;\n font-weight: 800;\n margin: 0 0 16px 0;\n line-height: 1.1;\n}\n\n.hero-subtitle {\n font-size: 18px;\n opacity: 0.85;\n margin: 0 0 32px 0;\n line-height: 1.5;\n}\n\n.hero-cta {\n display: inline-block;\n padding: 14px 32px;\n font-size: 16px;\n font-weight: 600;\n color: #111;\n background: #fff;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: opacity 0.15s;\n}\n\n.hero-cta:hover {\n opacity: 0.9;\n}\n\n@media (max-width: 768px) {\n .hero-banner { min-height: 360px; padding: 48px 24px; }\n .hero-heading { font-size: 32px; }\n .hero-subtitle { font-size: 16px; }\n}\n",
184
- "ikas-config-snippet.json": "{\n \"id\": \"hero-banner\",\n \"name\": \"Hero Banner\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"heading\", \"displayName\": \"Heading\", \"type\": \"TEXT\", \"defaultValue\": \"Welcome to Our Store\" },\n { \"name\": \"subtitle\", \"displayName\": \"Subtitle\", \"type\": \"TEXT\" },\n { \"name\": \"buttonText\", \"displayName\": \"Button Text\", \"type\": \"TEXT\", \"defaultValue\": \"Shop Now\" },\n { \"name\": \"buttonLink\", \"displayName\": \"Button Link\", \"type\": \"TEXT\", \"defaultValue\": \"/\" },\n { \"name\": \"backgroundImage\", \"displayName\": \"Background Image\", \"type\": \"IMAGE\" },\n { \"name\": \"backgroundColor\", \"displayName\": \"Background Color\", \"type\": \"COLOR\", \"defaultValue\": \"#111\" },\n { \"name\": \"textColor\", \"displayName\": \"Text Color\", \"type\": \"COLOR\", \"defaultValue\": \"#fff\" }\n ]\n}\n"
177
+ "index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n IkasProductList,\n IkasProductListSortType,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getFilterDisplayedValues,\n handleFilterValueClick,\n handleNumberRangeOptionClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n isSwatchFilter,\n getIkasFilterThumbnailImage,\n getProductListSortOptions,\n setSortType,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n setProductListVisiblePage,\n searchProductList,\n getCategoryPath,\n getIkasCategoryHref,\n isEmpty,\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 backgroundColor = \"#ffffff\",\n}: Props) {\n const [searchKeyword, setSearchKeyword] = useState(\"\");\n\n if (!productList) return null;\n\n const products = productList.data ?? [];\n const filterCategories = getProductListFilterCategories(productList);\n const sortOptions = getProductListSortOptions(productList);\n const hasNext = hasProductListNextPage(productList);\n const hasPrev = hasProductListPrevPage(productList);\n\n // Category breadcrumb path\n const category = productList.category;\n const categoryPath = category ? getCategoryPath(category) : [];\n\n const handleSort = (value: string) => {\n const selected = sortOptions.find((opt) => opt.value === value);\n if (selected) {\n setSortType(productList, selected.value as IkasProductListSortType);\n }\n };\n\n const handleSearch = (keyword: string) => {\n setSearchKeyword(keyword);\n searchProductList(productList, keyword);\n };\n\n return (\n <section className=\"product-list-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"product-list-inner\">\n {/* Category Breadcrumb */}\n {categoryPath.length > 0 && (\n <nav className=\"product-list-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((cat: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\"> / </span>\n <a href={getIkasCategoryHref(cat)}>{cat.name}</a>\n </span>\n ))}\n </nav>\n )}\n\n <div className=\"product-list-header\">\n <h1 className=\"product-list-title\">\n {category?.name || title}\n {category?.description && (\n <p className=\"product-list-description\">{category.description}</p>\n )}\n </h1>\n\n <div className=\"product-list-controls\">\n {/* Search */}\n <input\n className=\"product-list-search\"\n type=\"text\"\n placeholder=\"Search products...\"\n value={searchKeyword}\n onInput={(e) => handleSearch((e.target as HTMLInputElement).value)}\n />\n\n {/* Sort — use setSortType instead of direct mutation */}\n {sortOptions.length > 0 && (\n <select\n className=\"product-list-sort\"\n value={sortOptions.find((o) => o.isSelected)?.value ?? \"\"}\n onChange={(e) => handleSort((e.target as HTMLSelectElement).value)}\n >\n {sortOptions.map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\n </select>\n )}\n </div>\n </div>\n\n <div className=\"product-list-layout\">\n {/* Filter Sidebar — display-type-aware rendering */}\n {showFilters && (productList.filters ?? []).length > 0 && (\n <aside className=\"product-list-filters\">\n {(productList.filters ?? []).map((filter) => {\n const values = getFilterDisplayedValues(filter);\n if (values.length === 0 && !filter.numberRangeListOptions?.length) return null;\n\n return (\n <div key={filter.id} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{filter.name}</h3>\n\n {/* SWATCH: color circles / thumbnail images */}\n {isSwatchFilter(filter) ? (\n <div className=\"filter-swatches\">\n {values.map((fv) => {\n const thumbnail = getIkasFilterThumbnailImage(fv);\n return (\n <button\n key={fv.name}\n className={`filter-swatch${fv.isSelected === true ? \" selected\" : \"\"}`}\n onClick={() => handleFilterValueClick(productList, filter, fv)}\n title={fv.name}\n >\n {thumbnail ? (\n <img src={getDefaultSrc(thumbnail)} alt={fv.name} />\n ) : (\n <span\n className=\"filter-swatch-color\"\n style={{ backgroundColor: fv.colorCode ?? \"#ccc\" }}\n />\n )}\n </button>\n );\n })}\n </div>\n ) : (\n /* BOX / LIST: checkboxes (default) */\n <div className=\"filter-values\">\n {values.map((fv) => (\n <label key={fv.name} className=\"filter-value\">\n <input\n type=\"checkbox\"\n checked={fv.isSelected === true}\n onChange={() =>\n handleFilterValueClick(productList, filter, fv)\n }\n />\n <span>{fv.name}</span>\n {fv.count != null && (\n <span className=\"filter-count\">({fv.count})</span>\n )}\n </label>\n ))}\n </div>\n )}\n\n {/* NUMBER_RANGE_LIST: predefined range buttons */}\n {filter.numberRangeListOptions?.map((option) => (\n <button\n key={`${option.from}-${option.to}`}\n className={`filter-range-btn${option.isSelected ? \" selected\" : \"\"}`}\n onClick={() => handleNumberRangeOptionClick(productList, filter, option)}\n >\n {option.from} - {option.to ?? \"+\"}\n </button>\n ))}\n </div>\n );\n })}\n\n {/* Category-level filtering */}\n {filterCategories.length > 0 && (\n <div className=\"filter-group\">\n <h3 className=\"filter-group-title\">Categories</h3>\n {filterCategories.map((cat) => (\n <button\n key={cat.name}\n className=\"filter-category-btn\"\n onClick={() => onFilterCategoryClick(productList, cat, true)}\n >\n {cat.name}\n </button>\n ))}\n </div>\n )}\n </aside>\n )}\n\n {/* Product Grid — IkasThemeInfiniteScroller pattern for infinite scroll */}\n <div className=\"product-grid\">\n {isEmpty(products) && (\n <p className=\"product-grid-empty\">No products found.</p>\n )}\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 const href = getSelectedProductVariantHref(product);\n\n return (\n <a key={product.id} href={href} className=\"product-card\">\n <div className=\"product-card-image-wrap\">\n {image && (\n <img\n src={getDefaultSrc(image)}\n alt={product.name}\n className=\"product-card-image\"\n />\n )}\n </div>\n <div className=\"product-card-info\">\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </div>\n </a>\n );\n })}\n </div>\n </div>\n\n {/* Pagination — includes setProductListVisiblePage for page jumping */}\n {(hasPrev || hasNext) && (\n <div className=\"product-list-pagination\">\n <button\n className=\"pagination-btn\"\n disabled={!hasPrev}\n onClick={() => getProductListPrevPage(productList)}\n >\n Previous\n </button>\n <span className=\"pagination-info\">\n Page {productList.page}\n </span>\n <button\n className=\"pagination-btn\"\n disabled={!hasNext}\n onClick={() => getProductListNextPage(productList)}\n >\n Next\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
178
+ "types.ts": "import { IkasProductList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n productList: IkasProductList;\n title?: string;\n showFilters?: boolean;\n backgroundColor?: string;\n}\n",
179
+ "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 {\n font-size: 24px;\n font-weight: 700;\n color: #111;\n margin: 0;\n}\n\n.product-list-sort {\n padding: 8px 12px;\n font-size: 14px;\n border: 1px solid #ddd;\n border-radius: 6px;\n background: #fff;\n cursor: pointer;\n}\n\n.product-list-layout {\n display: flex;\n gap: 32px;\n}\n\n/* Filters */\n.product-list-filters {\n width: 240px;\n flex-shrink: 0;\n}\n\n.filter-group {\n margin-bottom: 24px;\n}\n\n.filter-group-title {\n font-size: 14px;\n font-weight: 600;\n color: #111;\n margin: 0 0 12px 0;\n}\n\n.filter-values {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.filter-value {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 14px;\n color: #555;\n cursor: pointer;\n}\n\n.filter-count {\n color: #999;\n font-size: 12px;\n}\n\n/* Product Grid */\n.product-grid {\n flex: 1;\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 24px;\n}\n\n.product-grid-empty {\n grid-column: 1 / -1;\n text-align: center;\n color: #666;\n padding: 48px 0;\n}\n\n.product-card {\n text-decoration: none;\n color: inherit;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.product-card-image-wrap {\n aspect-ratio: 1;\n overflow: hidden;\n border-radius: 8px;\n background: #f5f5f5;\n}\n\n.product-card-image {\n width: 100%;\n height: 100%;\n object-fit: cover;\n transition: transform 0.2s ease;\n}\n\n.product-card:hover .product-card-image {\n transform: scale(1.03);\n}\n\n.product-card-info {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.product-card-name {\n font-size: 14px;\n font-weight: 500;\n color: #111;\n margin: 0;\n}\n\n.product-card-price {\n font-size: 15px;\n font-weight: 600;\n color: #111;\n}\n\n/* Pagination */\n.product-list-pagination {\n display: flex;\n justify-content: center;\n gap: 12px;\n margin-top: 40px;\n}\n\n.pagination-btn {\n padding: 10px 24px;\n font-size: 14px;\n font-weight: 500;\n background: #fff;\n border: 1px solid #ddd;\n border-radius: 6px;\n cursor: pointer;\n}\n\n.pagination-btn:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.pagination-btn:hover:not(:disabled) {\n border-color: #111;\n}\n\n/* Responsive */\n@media (max-width: 768px) {\n .product-list-layout {\n flex-direction: column;\n }\n\n .product-list-filters {\n width: 100%;\n }\n\n .product-grid {\n grid-template-columns: repeat(2, 1fr);\n gap: 16px;\n }\n}\n",
180
+ "ikas-config-snippet.json": "{\n \"id\": \"product-list\",\n \"name\": \"Product List\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ProductList/index.tsx\",\n \"styles\": \"./src/components/ProductList/styles.css\",\n \"props\": [\n {\n \"name\": \"productList\",\n \"displayName\": \"Product List\",\n \"type\": \"PRODUCT_LIST\",\n \"required\": true\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Products\"\n },\n {\n \"name\": \"showFilters\",\n \"displayName\": \"Show Filters\",\n \"type\": \"BOOLEAN\",\n \"defaultValue\": true\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
185
181
  }
186
182
  },
187
- "reset-password": {
188
- "title": "Reset Password Section",
189
- "description": "Password reset form with new password and confirm fields, success/error states",
183
+ "product-reviews": {
184
+ "title": "Product Reviews Section",
185
+ "description": "Complete product reviews section with review list display, star ratings, review submission form with login-required check. getProductCustomerReviews supports limit, page, and hasImage params. Review data is automatically reactive in root components via autorun().",
190
186
  "files": {
191
- "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",
192
- "types.ts": "export interface Props {\n successMessage?: string;\n}\n",
193
- "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",
194
- "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"
187
+ "index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n IkasProduct,\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 backgroundColor = \"#ffffff\",\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) {\n setShowForm(false);\n }\n };\n\n const StarDisplay = ({ rating }: { rating: number }) => (\n <div className=\"reviews-stars\">\n {[1, 2, 3, 4, 5].map((star) => (\n <span key={star} className={star <= rating ? \"star-filled\" : \"star-empty\"}>\n ★\n </span>\n ))}\n </div>\n );\n\n return (\n <section className=\"reviews-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"reviews-inner\">\n <div className=\"reviews-header\">\n <h2 className=\"reviews-title\">\n {title} ({reviews.length})\n </h2>\n {!showForm && (\n <button\n className=\"reviews-write-btn\"\n onClick={() => {\n if (loginRequired && !isLoggedIn) {\n Router.navigateToPage(\"LOGIN\");\n } else {\n setShowForm(true);\n }\n }}\n >\n Write a Review\n </button>\n )}\n </div>\n\n {/* Review Form */}\n {showForm && (\n <form className=\"review-form\" onSubmit={handleSubmit}>\n {reviewForm.isFailure && reviewForm.responseMessage && (\n <div className=\"review-form-error\">{reviewForm.responseMessage}</div>\n )}\n\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\n key={star}\n type=\"button\"\n className={star <= reviewForm.star.value ? \"star-filled\" : \"star-empty\"}\n onClick={() => setCustomerReviewFormStar(reviewForm, star)}\n >\n ★\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"review-form-field\">\n <label className=\"review-form-label\">{reviewForm.title.label}</label>\n <input\n className=\"review-form-input\"\n type=\"text\"\n placeholder={reviewForm.title.placeholder}\n value={reviewForm.title.value}\n onInput={(e) =>\n setCustomerReviewFormTitle(reviewForm, (e.target as HTMLInputElement).value)\n }\n />\n {reviewForm.title.hasError && reviewForm.title.message && (\n <span className=\"review-form-error-text\">{reviewForm.title.message}</span>\n )}\n </div>\n\n <div className=\"review-form-field\">\n <label className=\"review-form-label\">{reviewForm.comment.label}</label>\n <textarea\n className=\"review-form-textarea\"\n placeholder={reviewForm.comment.placeholder}\n value={reviewForm.comment.value}\n rows={4}\n onInput={(e) =>\n setCustomerReviewFormComment(\n reviewForm,\n (e.target as HTMLTextAreaElement).value\n )\n }\n />\n {reviewForm.comment.hasError && reviewForm.comment.message && (\n <span className=\"review-form-error-text\">{reviewForm.comment.message}</span>\n )}\n </div>\n\n <div className=\"review-form-actions\">\n <button\n type=\"submit\"\n className=\"review-form-submit\"\n disabled={reviewForm.isSubmitting}\n >\n {reviewForm.isSubmitting ? \"Submitting...\" : \"Submit Review\"}\n </button>\n <button\n type=\"button\"\n className=\"review-form-cancel\"\n onClick={() => setShowForm(false)}\n >\n Cancel\n </button>\n </div>\n </form>\n )}\n\n {/* Review List */}\n {reviews.length === 0 && !showForm && (\n <p className=\"reviews-empty\">No reviews yet. Be the first to review!</p>\n )}\n\n <div className=\"review-list\">\n {reviews.map((review) => (\n <div key={review.id} className=\"review-card\">\n <div className=\"review-card-header\">\n <StarDisplay rating={review.star} />\n <span className=\"review-card-date\">\n {getIkasCustomerReviewFormattedDate(review)}\n </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\n",
188
+ "types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n title?: string;\n backgroundColor?: string;\n}\n",
189
+ "styles.css": ".reviews-section {\n width: 100%;\n padding: 48px 24px;\n border-top: 1px solid #eee;\n}\n\n.reviews-inner {\n max-width: 800px;\n margin: 0 auto;\n}\n\n.reviews-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 24px;\n}\n\n.reviews-title {\n font-size: 22px;\n font-weight: 700;\n color: #111;\n margin: 0;\n}\n\n.reviews-write-btn {\n padding: 10px 20px;\n font-size: 14px;\n font-weight: 600;\n color: #fff;\n background: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n}\n\n.reviews-write-btn:hover {\n background: #333;\n}\n\n.reviews-empty {\n text-align: center;\n color: #666;\n padding: 32px 0;\n font-size: 15px;\n}\n\n/* Stars */\n.reviews-stars,\n.star-input {\n display: flex;\n gap: 2px;\n}\n\n.star-filled {\n color: #f59e0b;\n font-size: 16px;\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n}\n\n.star-empty {\n color: #ddd;\n font-size: 16px;\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n}\n\n.star-input .star-filled,\n.star-input .star-empty {\n font-size: 24px;\n}\n\n/* Review Form */\n.review-form {\n background: #f9fafb;\n border-radius: 12px;\n padding: 24px;\n margin-bottom: 32px;\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.review-form-error {\n padding: 10px 14px;\n font-size: 13px;\n color: #b71c1c;\n background: #ffebee;\n border-radius: 6px;\n}\n\n.review-form-stars {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.review-form-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.review-form-label {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n}\n\n.review-form-input,\n.review-form-textarea {\n padding: 10px 12px;\n font-size: 14px;\n border: 1.5px solid #ddd;\n border-radius: 6px;\n outline: none;\n font-family: inherit;\n}\n\n.review-form-input:focus,\n.review-form-textarea:focus {\n border-color: #111;\n}\n\n.review-form-textarea {\n resize: vertical;\n min-height: 100px;\n}\n\n.review-form-error-text {\n font-size: 12px;\n color: #e53935;\n}\n\n.review-form-actions {\n display: flex;\n gap: 12px;\n}\n\n.review-form-submit {\n padding: 10px 20px;\n font-size: 14px;\n font-weight: 600;\n color: #fff;\n background: #111;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n}\n\n.review-form-submit:disabled {\n background: #ccc;\n cursor: not-allowed;\n}\n\n.review-form-cancel {\n padding: 10px 20px;\n font-size: 14px;\n color: #666;\n background: none;\n border: 1px solid #ddd;\n border-radius: 6px;\n cursor: pointer;\n}\n\n/* Review List */\n.review-list {\n display: flex;\n flex-direction: column;\n gap: 20px;\n}\n\n.review-card {\n padding: 20px 0;\n border-bottom: 1px solid #f0f0f0;\n}\n\n.review-card:last-child {\n border-bottom: none;\n}\n\n.review-card-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n\n.review-card-date {\n font-size: 12px;\n color: #999;\n}\n\n.review-card-title {\n font-size: 15px;\n font-weight: 600;\n color: #111;\n margin: 0 0 6px 0;\n}\n\n.review-card-comment {\n font-size: 14px;\n color: #555;\n line-height: 1.6;\n margin: 0 0 8px 0;\n}\n\n.review-card-author {\n font-size: 13px;\n color: #888;\n font-weight: 500;\n}\n",
190
+ "ikas-config-snippet.json": "{\n \"id\": \"product-reviews\",\n \"name\": \"Product Reviews\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ProductReviews/index.tsx\",\n \"styles\": \"./src/components/ProductReviews/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": true\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Customer Reviews\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
195
191
  }
196
192
  },
197
- "account-info": {
198
- "title": "Account Info Section",
199
- "description": "Account information edit form with first name, last name, and phone",
193
+ "register-page": {
194
+ "title": "Register Section",
195
+ "description": "Complete registration section with first name, last name, email, password, marketing consent (setRegisterFormIsMarketingAccepted), membership agreement (setRegisterFormIsMembershipAgreementAccepted), and social login (socialLogin + SocialLoginProvider). Uses navigateToPage for navigation.",
200
196
  "files": {
201
- "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",
202
- "types.ts": "export interface Props {\n title?: string;\n}\n",
203
- "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",
204
- "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"
197
+ "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 setRegisterFormIsMarketingAccepted,\n setRegisterFormIsMembershipAgreementAccepted,\n socialLogin,\n navigateToPage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function RegisterSection({\n redirectAfterRegister = \"/account\",\n backgroundColor = \"#ffffff\",\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) {\n navigateToPage(\"ACCOUNT\");\n }\n };\n\n const handleSocialRegister = async (provider: \"GOOGLE\" | \"FACEBOOK\" | \"APPLE\") => {\n await socialLogin(customerStore, provider as any);\n };\n\n return (\n <section className=\"register-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"register-inner\">\n <h1 className=\"register-title\">Create Account</h1>\n\n {registerForm.isFailure && registerForm.responseMessage && (\n <div className=\"register-error-banner\">{registerForm.responseMessage}</div>\n )}\n\n {/* Social Login Buttons */}\n <div className=\"register-social\">\n <button className=\"register-social-btn\" onClick={() => handleSocialRegister(\"GOOGLE\")}>\n Continue with Google\n </button>\n <button className=\"register-social-btn\" onClick={() => handleSocialRegister(\"FACEBOOK\")}>\n Continue with Facebook\n </button>\n </div>\n\n <div className=\"register-divider\"><span>or</span></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) =>\n setRegisterFormFirstName(registerForm, (e.target as HTMLInputElement).value)\n }\n />\n {registerForm.firstName.hasError && registerForm.firstName.message && (\n <span className=\"register-field-error\">{registerForm.firstName.message}</span>\n )}\n </div>\n\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) =>\n setRegisterFormLastName(registerForm, (e.target as HTMLInputElement).value)\n }\n />\n {registerForm.lastName.hasError && registerForm.lastName.message && (\n <span className=\"register-field-error\">{registerForm.lastName.message}</span>\n )}\n </div>\n </div>\n\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) =>\n setRegisterFormEmail(registerForm, (e.target as HTMLInputElement).value)\n }\n />\n {registerForm.email.hasError && registerForm.email.message && (\n <span className=\"register-field-error\">{registerForm.email.message}</span>\n )}\n </div>\n\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) =>\n setRegisterFormPassword(registerForm, (e.target as HTMLInputElement).value)\n }\n />\n {registerForm.password.hasError && registerForm.password.message && (\n <span className=\"register-field-error\">{registerForm.password.message}</span>\n )}\n </div>\n\n {/* Marketing Consent — IkasFormItemBoolean */}\n <label className=\"register-checkbox\">\n <input\n type=\"checkbox\"\n checked={registerForm.isMarketingAccepted?.value ?? false}\n onChange={(e) =>\n setRegisterFormIsMarketingAccepted(registerForm, (e.target as HTMLInputElement).checked)\n }\n />\n <span>I want to receive marketing emails and promotions</span>\n </label>\n\n {/* Membership Agreement — IkasFormItemBoolean */}\n <label className=\"register-checkbox\">\n <input\n type=\"checkbox\"\n checked={registerForm.isMembershipAgreementAccepted?.value ?? false}\n onChange={(e) =>\n setRegisterFormIsMembershipAgreementAccepted(registerForm, (e.target as HTMLInputElement).checked)\n }\n />\n <span>I accept the membership agreement</span>\n </label>\n\n <button\n className=\"register-submit-btn\"\n type=\"submit\"\n disabled={registerForm.isSubmitting}\n >\n {registerForm.isSubmitting ? \"Creating account...\" : \"Create Account\"}\n </button>\n </form>\n\n <p className=\"register-login-link\">\n Already have an account?{\" \"}\n <a\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n navigateToPage(\"LOGIN\");\n }}\n >\n Sign in\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
198
+ "types.ts": "export interface Props {\n redirectAfterRegister?: string;\n backgroundColor?: string;\n}\n",
199
+ "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 {\n font-size: 28px;\n font-weight: 700;\n color: #111;\n margin: 0 0 24px 0;\n text-align: center;\n}\n\n.register-error-banner {\n padding: 12px 16px;\n font-size: 14px;\n color: #b71c1c;\n background-color: #ffebee;\n border-radius: 8px;\n margin-bottom: 20px;\n}\n\n.register-form {\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.register-row {\n display: flex;\n gap: 12px;\n}\n\n.register-row .register-field {\n flex: 1;\n}\n\n.register-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.register-label {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n}\n\n.register-input {\n padding: 12px 14px;\n font-size: 15px;\n border: 1.5px solid #ddd;\n border-radius: 8px;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.register-input:focus {\n border-color: #111;\n}\n\n.register-input.has-error {\n border-color: #e53935;\n}\n\n.register-field-error {\n font-size: 12px;\n color: #e53935;\n}\n\n.register-submit-btn {\n padding: 14px 24px;\n font-size: 16px;\n font-weight: 600;\n color: #fff;\n background-color: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s ease;\n margin-top: 8px;\n}\n\n.register-submit-btn:hover:not(:disabled) {\n background-color: #333;\n}\n\n.register-submit-btn:disabled {\n background-color: #ccc;\n cursor: not-allowed;\n}\n\n.register-login-link {\n font-size: 14px;\n color: #666;\n text-align: center;\n margin-top: 24px;\n}\n\n.register-login-link a {\n color: #111;\n font-weight: 600;\n text-decoration: none;\n}\n\n.register-login-link a:hover {\n text-decoration: underline;\n}\n\n@media (max-width: 480px) {\n .register-row {\n flex-direction: column;\n }\n}\n",
200
+ "ikas-config-snippet.json": "{\n \"id\": \"register-page\",\n \"name\": \"Register Page\",\n \"type\": \"section\",\n \"entry\": \"./src/components/RegisterSection/index.tsx\",\n \"styles\": \"./src/components/RegisterSection/styles.css\",\n \"props\": [\n {\n \"name\": \"redirectAfterRegister\",\n \"displayName\": \"Redirect After Register\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"/account\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
205
201
  }
206
202
  },
207
- "order-detail": {
208
- "title": "Order Detail Section",
209
- "description": "Single order detail page with line items, adjustments, transactions, and totals",
203
+ "reset-password": {
204
+ "title": "Reset Password Section",
205
+ "description": "Complete reset password section with new password and confirm password fields. Uses getRecoverPasswordForm/initRecoverPasswordForm/setRecoverPasswordFormPassword/setRecoverPasswordFormPasswordAgain/submitRecoverPasswordForm pattern.",
210
206
  "files": {
211
- "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",
212
- "types.ts": "export interface Props {}\n",
213
- "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",
214
- "ikas-config-snippet.json": "{\n \"id\": \"order-detail\",\n \"name\": \"Order Detail\",\n \"type\": \"section\",\n \"props\": []\n}\n"
207
+ "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({ backgroundColor = \"#ffffff\" }: 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) {\n Router.navigateToPage(\"LOGIN\");\n }\n };\n\n return (\n <section className=\"reset-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"reset-inner\">\n <h1 className=\"reset-title\">Set New Password</h1>\n <p className=\"reset-subtitle\">Enter your new password below.</p>\n\n {recoverForm.isSuccess && (\n <div className=\"reset-success-banner\">Password has been reset successfully!</div>\n )}\n {recoverForm.isFailure && recoverForm.responseMessage && (\n <div className=\"reset-error-banner\">{recoverForm.responseMessage}</div>\n )}\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) =>\n setRecoverPasswordFormPassword(recoverForm, (e.target as HTMLInputElement).value)\n }\n />\n {recoverForm.password.hasError && recoverForm.password.message && (\n <span className=\"reset-field-error\">{recoverForm.password.message}</span>\n )}\n </div>\n\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) =>\n setRecoverPasswordFormPasswordAgain(recoverForm, (e.target as HTMLInputElement).value)\n }\n />\n {recoverForm.passwordAgain.hasError && recoverForm.passwordAgain.message && (\n <span className=\"reset-field-error\">{recoverForm.passwordAgain.message}</span>\n )}\n </div>\n\n <button className=\"reset-submit-btn\" type=\"submit\" disabled={recoverForm.isSubmitting}>\n {recoverForm.isSubmitting ? \"Resetting...\" : \"Reset Password\"}\n </button>\n </form>\n )}\n\n <p className=\"reset-back-link\">\n <a href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"LOGIN\"); }}>\n Back to Sign In\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
208
+ "types.ts": "export interface Props {\n backgroundColor?: string;\n}\n",
209
+ "styles.css": ".reset-section { width: 100%; padding: 64px 24px; }\n.reset-inner { max-width: 400px; margin: 0 auto; }\n.reset-title { font-size: 28px; font-weight: 700; color: #111; margin: 0 0 8px 0; text-align: center; }\n.reset-subtitle { font-size: 15px; color: #666; text-align: center; margin: 0 0 24px 0; }\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",
210
+ "ikas-config-snippet.json": "{\n \"id\": \"reset-password\",\n \"name\": \"Reset Password\",\n \"type\": \"section\",\n \"props\": [\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
215
211
  }
216
212
  }
217
213
  }