@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,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-02-20T06:42:35.816Z",
2
+ "generatedAt": "2026-02-23T07:10:37.688Z",
3
3
  "functions": [
4
4
  {
5
5
  "name": "apiListBlog",
@@ -15810,17 +15810,6 @@
15810
15810
  "isClass": true,
15811
15811
  "className": "I18n"
15812
15812
  },
15813
- {
15814
- "name": "testSetup",
15815
- "signature": "function testSetup(): void",
15816
- "description": "",
15817
- "params": [],
15818
- "returnType": "void",
15819
- "categories": [],
15820
- "related": [],
15821
- "isAsync": false,
15822
- "isClass": false
15823
- },
15824
15813
  {
15825
15814
  "name": "Router.navigate",
15826
15815
  "signature": "static Router.navigate(path: string, shallow: boolean = false, newTab: boolean = false): void",
@@ -16161,11 +16150,41 @@
16161
16150
  }
16162
16151
  ],
16163
16152
  "codeExamples": [
16153
+ {
16154
+ "id": "404-section",
16155
+ "title": "404 Page Section",
16156
+ "description": "Page not found section with message and navigation back to home.",
16157
+ "code": "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",
16158
+ "relatedFunctions": [
16159
+ "Router.navigate"
16160
+ ],
16161
+ "categories": [
16162
+ "Navigation"
16163
+ ],
16164
+ "files": [
16165
+ {
16166
+ "filename": "index.tsx",
16167
+ "content": "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"
16168
+ },
16169
+ {
16170
+ "filename": "types.ts",
16171
+ "content": "export interface Props {\n heading?: string;\n message?: string;\n buttonText?: string;\n backgroundColor?: string;\n}\n"
16172
+ },
16173
+ {
16174
+ "filename": "styles.css",
16175
+ "content": ".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"
16176
+ },
16177
+ {
16178
+ "filename": "ikas-config-snippet.json",
16179
+ "content": "{\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"
16180
+ }
16181
+ ]
16182
+ },
16164
16183
  {
16165
16184
  "id": "account-addresses-section",
16166
16185
  "title": "Account Addresses Section (Complete)",
16167
16186
  "description": "Complete address management with list display, add/edit form (getIkasCustomerAddressForm/initAddressForm/setAddressForm*/submitAddressForm), and delete (deleteCustomerAddress). Uses isEmpty/isNotEmpty.",
16168
- "code": "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\";\n\nexport default function AccountAddressesSection() {\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\">\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",
16187
+ "code": "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",
16169
16188
  "relatedFunctions": [
16170
16189
  "getIkasCustomerAddressForm",
16171
16190
  "initAddressForm",
@@ -16193,11 +16212,11 @@
16193
16212
  "files": [
16194
16213
  {
16195
16214
  "filename": "index.tsx",
16196
- "content": "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\";\n\nexport default function AccountAddressesSection() {\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\">\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"
16215
+ "content": "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"
16197
16216
  },
16198
16217
  {
16199
16218
  "filename": "types.ts",
16200
- "content": "export interface Props {\n title?: string;\n}\n"
16219
+ "content": "export interface Props {\n title?: string;\n backgroundColor?: string;\n}\n"
16201
16220
  },
16202
16221
  {
16203
16222
  "filename": "styles.css",
@@ -16205,7 +16224,7 @@
16205
16224
  },
16206
16225
  {
16207
16226
  "filename": "ikas-config-snippet.json",
16208
- "content": "{ \"id\": \"account-addresses\", \"name\": \"Account Addresses\", \"type\": \"section\", \"props\": [{ \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"My Addresses\" }] }\n"
16227
+ "content": "{\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"
16209
16228
  }
16210
16229
  ]
16211
16230
  },
@@ -16213,7 +16232,7 @@
16213
16232
  "id": "account-info-section",
16214
16233
  "title": "Account Info Section (Complete)",
16215
16234
  "description": "Complete account info section with first name, last name, and phone fields. Uses getAccountInfoForm/initAccountInfoForm/setAccountInfoForm*/submitAccountInfoForm pattern.",
16216
- "code": "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\";\n\nexport default function AccountInfoSection() {\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\">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",
16235
+ "code": "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",
16217
16236
  "relatedFunctions": [
16218
16237
  "getAccountInfoForm",
16219
16238
  "initAccountInfoForm",
@@ -16231,11 +16250,11 @@
16231
16250
  "files": [
16232
16251
  {
16233
16252
  "filename": "index.tsx",
16234
- "content": "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\";\n\nexport default function AccountInfoSection() {\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\">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"
16253
+ "content": "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"
16235
16254
  },
16236
16255
  {
16237
16256
  "filename": "types.ts",
16238
- "content": "export interface Props {}\n"
16257
+ "content": "export interface Props {\n backgroundColor?: string;\n}\n"
16239
16258
  },
16240
16259
  {
16241
16260
  "filename": "styles.css",
@@ -16243,7 +16262,7 @@
16243
16262
  },
16244
16263
  {
16245
16264
  "filename": "ikas-config-snippet.json",
16246
- "content": "{ \"id\": \"account-info\", \"name\": \"Account Info\", \"type\": \"section\", \"props\": [] }\n"
16265
+ "content": "{\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"
16247
16266
  }
16248
16267
  ]
16249
16268
  },
@@ -16251,7 +16270,7 @@
16251
16270
  "id": "account-orders-section",
16252
16271
  "title": "Account Orders Section (Complete)",
16253
16272
  "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.",
16254
- "code": "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}: 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\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",
16273
+ "code": "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",
16255
16274
  "relatedFunctions": [
16256
16275
  "customerStore",
16257
16276
  "getOrders",
@@ -16273,11 +16292,11 @@
16273
16292
  "files": [
16274
16293
  {
16275
16294
  "filename": "index.tsx",
16276
- "content": "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}: 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\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"
16295
+ "content": "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"
16277
16296
  },
16278
16297
  {
16279
16298
  "filename": "types.ts",
16280
- "content": "export interface Props {\n title?: string;\n emptyMessage?: string;\n}\n"
16299
+ "content": "export interface Props {\n title?: string;\n emptyMessage?: string;\n backgroundColor?: string;\n}\n"
16281
16300
  },
16282
16301
  {
16283
16302
  "filename": "styles.css",
@@ -16285,7 +16304,7 @@
16285
16304
  },
16286
16305
  {
16287
16306
  "filename": "ikas-config-snippet.json",
16288
- "content": "{\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}\n"
16307
+ "content": "{\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"
16289
16308
  }
16290
16309
  ]
16291
16310
  },
@@ -16330,7 +16349,7 @@
16330
16349
  "id": "blog-list-section",
16331
16350
  "title": "Blog List Section (Complete)",
16332
16351
  "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.",
16333
- "code": "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}: 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\">\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",
16352
+ "code": "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",
16334
16353
  "relatedFunctions": [
16335
16354
  "hasBlogListNextPage",
16336
16355
  "getBlogListNextPage",
@@ -16345,11 +16364,11 @@
16345
16364
  "files": [
16346
16365
  {
16347
16366
  "filename": "index.tsx",
16348
- "content": "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}: 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\">\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"
16367
+ "content": "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"
16349
16368
  },
16350
16369
  {
16351
16370
  "filename": "types.ts",
16352
- "content": "import { IkasBlogList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogList: IkasBlogList;\n title?: string;\n}\n"
16371
+ "content": "import { IkasBlogList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogList: IkasBlogList;\n title?: string;\n backgroundColor?: string;\n}\n"
16353
16372
  },
16354
16373
  {
16355
16374
  "filename": "styles.css",
@@ -16357,7 +16376,38 @@
16357
16376
  },
16358
16377
  {
16359
16378
  "filename": "ikas-config-snippet.json",
16360
- "content": "{\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}\n"
16379
+ "content": "{\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"
16380
+ }
16381
+ ]
16382
+ },
16383
+ {
16384
+ "id": "blog-post-section",
16385
+ "title": "Blog Post Section",
16386
+ "description": "Single blog post detail page with image, title, date, and HTML content.",
16387
+ "code": "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",
16388
+ "relatedFunctions": [
16389
+ "getIkasBlogFormattedDate",
16390
+ "getDefaultSrc"
16391
+ ],
16392
+ "categories": [
16393
+ "Blog"
16394
+ ],
16395
+ "files": [
16396
+ {
16397
+ "filename": "index.tsx",
16398
+ "content": "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"
16399
+ },
16400
+ {
16401
+ "filename": "types.ts",
16402
+ "content": "import { IkasBlog } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogPost: IkasBlog;\n backgroundColor?: string;\n}\n"
16403
+ },
16404
+ {
16405
+ "filename": "styles.css",
16406
+ "content": ".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"
16407
+ },
16408
+ {
16409
+ "filename": "ikas-config-snippet.json",
16410
+ "content": "{\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"
16361
16411
  }
16362
16412
  ]
16363
16413
  },
@@ -16411,7 +16461,7 @@
16411
16461
  "id": "cart-section",
16412
16462
  "title": "Cart Section (Complete)",
16413
16463
  "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().",
16414
- "code": "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\" }: 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\">\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",
16464
+ "code": "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",
16415
16465
  "relatedFunctions": [
16416
16466
  "cartStore",
16417
16467
  "hasCart",
@@ -16436,11 +16486,11 @@
16436
16486
  "files": [
16437
16487
  {
16438
16488
  "filename": "index.tsx",
16439
- "content": "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\" }: 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\">\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"
16489
+ "content": "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"
16440
16490
  },
16441
16491
  {
16442
16492
  "filename": "types.ts",
16443
- "content": "export interface Props {\n emptyCartMessage?: string;\n}\n"
16493
+ "content": "export interface Props {\n emptyCartMessage?: string;\n backgroundColor?: string;\n}\n"
16444
16494
  },
16445
16495
  {
16446
16496
  "filename": "styles.css",
@@ -16448,7 +16498,7 @@
16448
16498
  },
16449
16499
  {
16450
16500
  "filename": "ikas-config-snippet.json",
16451
- "content": "{\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}\n"
16501
+ "content": "{\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"
16452
16502
  }
16453
16503
  ]
16454
16504
  },
@@ -16456,7 +16506,7 @@
16456
16506
  "id": "contact-form-section",
16457
16507
  "title": "Contact Form Section (Complete)",
16458
16508
  "description": "Complete contact form section with name, email, phone, and message fields. Uses the initContactForm/setContactForm*/submitContactForm pattern with validation and success state.",
16459
- "code": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getContactForm,\n initContactForm,\n setContactFormEmail,\n setContactFormFirstName,\n setContactFormLastName,\n setContactFormPhone,\n setContactFormMessage,\n submitContactForm,\n clearContactForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ContactFormSection({\n title = \"Contact Us\",\n successMessage = \"Thank you! Your message has been sent.\",\n}: Props) {\n const contactForm = getContactForm(customerStore);\n\n useEffect(() => {\n initContactForm(contactForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitContactForm(contactForm);\n if (success) {\n clearContactForm(contactForm);\n }\n };\n\n return (\n <section className=\"contact-section\">\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",
16509
+ "code": "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",
16460
16510
  "relatedFunctions": [
16461
16511
  "initContactForm",
16462
16512
  "setContactFormEmail",
@@ -16474,11 +16524,11 @@
16474
16524
  "files": [
16475
16525
  {
16476
16526
  "filename": "index.tsx",
16477
- "content": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getContactForm,\n initContactForm,\n setContactFormEmail,\n setContactFormFirstName,\n setContactFormLastName,\n setContactFormPhone,\n setContactFormMessage,\n submitContactForm,\n clearContactForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ContactFormSection({\n title = \"Contact Us\",\n successMessage = \"Thank you! Your message has been sent.\",\n}: Props) {\n const contactForm = getContactForm(customerStore);\n\n useEffect(() => {\n initContactForm(contactForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitContactForm(contactForm);\n if (success) {\n clearContactForm(contactForm);\n }\n };\n\n return (\n <section className=\"contact-section\">\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"
16527
+ "content": "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"
16478
16528
  },
16479
16529
  {
16480
16530
  "filename": "types.ts",
16481
- "content": "export interface Props {\n title?: string;\n successMessage?: string;\n}\n"
16531
+ "content": "export interface Props {\n title?: string;\n successMessage?: string;\n backgroundColor?: string;\n}\n"
16482
16532
  },
16483
16533
  {
16484
16534
  "filename": "styles.css",
@@ -16486,7 +16536,7 @@
16486
16536
  },
16487
16537
  {
16488
16538
  "filename": "ikas-config-snippet.json",
16489
- "content": "{\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}\n"
16539
+ "content": "{\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"
16490
16540
  }
16491
16541
  ]
16492
16542
  },
@@ -16534,7 +16584,7 @@
16534
16584
  "id": "faq-section",
16535
16585
  "title": "FAQ Accordion Section (Complete)",
16536
16586
  "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).",
16537
- "code": "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\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",
16587
+ "code": "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",
16538
16588
  "relatedFunctions": [],
16539
16589
  "categories": [
16540
16590
  "Content",
@@ -16543,11 +16593,11 @@
16543
16593
  "files": [
16544
16594
  {
16545
16595
  "filename": "index.tsx",
16546
- "content": "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\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"
16596
+ "content": "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"
16547
16597
  },
16548
16598
  {
16549
16599
  "filename": "types.ts",
16550
- "content": "export interface FaqItem {\n question: string;\n answer: string;\n}\n\nexport interface Props {\n title?: string;\n items?: FaqItem[];\n}\n"
16600
+ "content": "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"
16551
16601
  },
16552
16602
  {
16553
16603
  "filename": "styles.css",
@@ -16555,7 +16605,7 @@
16555
16605
  },
16556
16606
  {
16557
16607
  "filename": "ikas-config-snippet.json",
16558
- "content": "{\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}\n"
16608
+ "content": "{\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"
16559
16609
  }
16560
16610
  ]
16561
16611
  },
@@ -16587,7 +16637,7 @@
16587
16637
  "id": "favorites-page-section",
16588
16638
  "title": "Favorites Page Section (Complete)",
16589
16639
  "description": "Complete favorites page with getFavoriteProducts loading, product cards with pricing/images/links, and remove functionality.",
16590
- "code": "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\";\n\nexport default function FavoritesPageSection() {\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\">\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",
16640
+ "code": "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",
16591
16641
  "relatedFunctions": [
16592
16642
  "getFavoriteProducts",
16593
16643
  "removeIkasProductFromFavorites",
@@ -16607,11 +16657,11 @@
16607
16657
  "files": [
16608
16658
  {
16609
16659
  "filename": "index.tsx",
16610
- "content": "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\";\n\nexport default function FavoritesPageSection() {\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\">\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"
16660
+ "content": "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"
16611
16661
  },
16612
16662
  {
16613
16663
  "filename": "types.ts",
16614
- "content": "export interface Props {}\n"
16664
+ "content": "export interface Props {\n backgroundColor?: string;\n}\n"
16615
16665
  },
16616
16666
  {
16617
16667
  "filename": "styles.css",
@@ -16619,7 +16669,7 @@
16619
16669
  },
16620
16670
  {
16621
16671
  "filename": "ikas-config-snippet.json",
16622
- "content": "{ \"id\": \"favorites-page\", \"name\": \"Favorites Page\", \"type\": \"section\", \"props\": [] }\n"
16672
+ "content": "{\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"
16623
16673
  }
16624
16674
  ]
16625
16675
  },
@@ -16627,7 +16677,7 @@
16627
16677
  "id": "footer-section",
16628
16678
  "title": "Footer Section (Complete)",
16629
16679
  "description": "Complete footer section with logo, navigation link columns, contact info, social media links, and copyright. Uses IkasNavigationLink for editable links.",
16630
- "code": "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 {/* 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?.map((link: IkasNavigationLink, i: number) => (\n <a\n key={i}\n href={link.href}\n className=\"footer-link\"\n target={link.isTargetBlank ? \"_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?.map((link: IkasNavigationLink, i: number) => (\n <a\n key={i}\n href={link.href}\n className=\"footer-link\"\n target={link.isTargetBlank ? \"_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",
16680
+ "code": "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",
16631
16681
  "relatedFunctions": [],
16632
16682
  "categories": [
16633
16683
  "Navigation",
@@ -16636,19 +16686,19 @@
16636
16686
  "files": [
16637
16687
  {
16638
16688
  "filename": "index.tsx",
16639
- "content": "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 {/* 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?.map((link: IkasNavigationLink, i: number) => (\n <a\n key={i}\n href={link.href}\n className=\"footer-link\"\n target={link.isTargetBlank ? \"_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?.map((link: IkasNavigationLink, i: number) => (\n <a\n key={i}\n href={link.href}\n className=\"footer-link\"\n target={link.isTargetBlank ? \"_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"
16689
+ "content": "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"
16640
16690
  },
16641
16691
  {
16642
16692
  "filename": "types.ts",
16643
- "content": "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"
16693
+ "content": "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"
16644
16694
  },
16645
16695
  {
16646
16696
  "filename": "styles.css",
16647
- "content": ".footer-section {\n width: 100%;\n background-color: #f9fafb;\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"
16697
+ "content": ".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"
16648
16698
  },
16649
16699
  {
16650
16700
  "filename": "ikas-config-snippet.json",
16651
- "content": "{\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}\n"
16701
+ "content": "{\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"
16652
16702
  }
16653
16703
  ]
16654
16704
  },
@@ -16656,7 +16706,7 @@
16656
16706
  "id": "forgot-password-section",
16657
16707
  "title": "Forgot Password Section (Complete)",
16658
16708
  "description": "Complete forgot password section with email input for password recovery. Uses the initForgotPasswordForm/setForgotPasswordFormEmail/submitForgotPasswordForm pattern with success and error states.",
16659
- "code": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getForgotPasswordForm,\n initForgotPasswordForm,\n setForgotPasswordFormEmail,\n submitForgotPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ForgotPasswordSection({\n successMessage = \"Password reset link has been sent to your email.\",\n}: Props) {\n const forgotForm = getForgotPasswordForm(customerStore);\n\n useEffect(() => {\n initForgotPasswordForm(forgotForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitForgotPasswordForm(forgotForm);\n };\n\n return (\n <section className=\"forgot-section\">\n <div className=\"forgot-inner\">\n <h1 className=\"forgot-title\">Forgot Password</h1>\n <p className=\"forgot-subtitle\">\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",
16709
+ "code": "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",
16660
16710
  "relatedFunctions": [
16661
16711
  "initForgotPasswordForm",
16662
16712
  "setForgotPasswordFormEmail",
@@ -16672,11 +16722,11 @@
16672
16722
  "files": [
16673
16723
  {
16674
16724
  "filename": "index.tsx",
16675
- "content": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getForgotPasswordForm,\n initForgotPasswordForm,\n setForgotPasswordFormEmail,\n submitForgotPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ForgotPasswordSection({\n successMessage = \"Password reset link has been sent to your email.\",\n}: Props) {\n const forgotForm = getForgotPasswordForm(customerStore);\n\n useEffect(() => {\n initForgotPasswordForm(forgotForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitForgotPasswordForm(forgotForm);\n };\n\n return (\n <section className=\"forgot-section\">\n <div className=\"forgot-inner\">\n <h1 className=\"forgot-title\">Forgot Password</h1>\n <p className=\"forgot-subtitle\">\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"
16725
+ "content": "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"
16676
16726
  },
16677
16727
  {
16678
16728
  "filename": "types.ts",
16679
- "content": "export interface Props {\n successMessage?: string;\n}\n"
16729
+ "content": "export interface Props {\n successMessage?: string;\n backgroundColor?: string;\n}\n"
16680
16730
  },
16681
16731
  {
16682
16732
  "filename": "styles.css",
@@ -16684,7 +16734,7 @@
16684
16734
  },
16685
16735
  {
16686
16736
  "filename": "ikas-config-snippet.json",
16687
- "content": "{\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}\n"
16737
+ "content": "{\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"
16688
16738
  }
16689
16739
  ]
16690
16740
  },
@@ -16692,7 +16742,7 @@
16692
16742
  "id": "header-section",
16693
16743
  "title": "Header Section (Complete)",
16694
16744
  "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().",
16695
- "code": "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}: 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\">\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?.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?.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",
16745
+ "code": "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",
16696
16746
  "relatedFunctions": [
16697
16747
  "cartStore",
16698
16748
  "customerStore",
@@ -16721,11 +16771,11 @@
16721
16771
  "files": [
16722
16772
  {
16723
16773
  "filename": "index.tsx",
16724
- "content": "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}: 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\">\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?.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?.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"
16774
+ "content": "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"
16725
16775
  },
16726
16776
  {
16727
16777
  "filename": "types.ts",
16728
- "content": "import { IkasNavigationLink, IkasImage } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n logo?: IkasImage | null;\n navigationLinks?: IkasNavigationLink[];\n announcementText?: string;\n announcementBgColor?: string;\n announcementTextColor?: string;\n}\n"
16778
+ "content": "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"
16729
16779
  },
16730
16780
  {
16731
16781
  "filename": "styles.css",
@@ -16733,7 +16783,38 @@
16733
16783
  },
16734
16784
  {
16735
16785
  "filename": "ikas-config-snippet.json",
16736
- "content": "{\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}\n"
16786
+ "content": "{\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"
16787
+ }
16788
+ ]
16789
+ },
16790
+ {
16791
+ "id": "hero-banner-section",
16792
+ "title": "Hero Banner Section",
16793
+ "description": "Full-width hero banner with heading, subtitle, CTA button, and background image.",
16794
+ "code": "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",
16795
+ "relatedFunctions": [
16796
+ "getDefaultSrc",
16797
+ "Router.navigate"
16798
+ ],
16799
+ "categories": [
16800
+ "Navigation"
16801
+ ],
16802
+ "files": [
16803
+ {
16804
+ "filename": "index.tsx",
16805
+ "content": "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"
16806
+ },
16807
+ {
16808
+ "filename": "types.ts",
16809
+ "content": "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"
16810
+ },
16811
+ {
16812
+ "filename": "styles.css",
16813
+ "content": ".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"
16814
+ },
16815
+ {
16816
+ "filename": "ikas-config-snippet.json",
16817
+ "content": "{\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"
16737
16818
  }
16738
16819
  ]
16739
16820
  },
@@ -16758,7 +16839,7 @@
16758
16839
  "id": "login-section",
16759
16840
  "title": "Login Section (Complete)",
16760
16841
  "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.",
16761
- "code": "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}: 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\">\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",
16842
+ "code": "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",
16762
16843
  "relatedFunctions": [
16763
16844
  "initLoginForm",
16764
16845
  "setLoginFormEmail",
@@ -16778,11 +16859,11 @@
16778
16859
  "files": [
16779
16860
  {
16780
16861
  "filename": "index.tsx",
16781
- "content": "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}: 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\">\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"
16862
+ "content": "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"
16782
16863
  },
16783
16864
  {
16784
16865
  "filename": "types.ts",
16785
- "content": "export interface Props {\n redirectAfterLogin?: string;\n showForgotPassword?: boolean;\n}\n"
16866
+ "content": "export interface Props {\n redirectAfterLogin?: string;\n showForgotPassword?: boolean;\n backgroundColor?: string;\n}\n"
16786
16867
  },
16787
16868
  {
16788
16869
  "filename": "styles.css",
@@ -16790,7 +16871,7 @@
16790
16871
  },
16791
16872
  {
16792
16873
  "filename": "ikas-config-snippet.json",
16793
- "content": "{\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}\n"
16874
+ "content": "{\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"
16794
16875
  }
16795
16876
  ]
16796
16877
  },
@@ -16819,7 +16900,7 @@
16819
16900
  "id": "order-detail-section",
16820
16901
  "title": "Order Detail Section (Complete)",
16821
16902
  "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.",
16822
- "code": "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\";\n\nexport default function OrderDetailSection() {\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\">\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",
16903
+ "code": "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",
16823
16904
  "relatedFunctions": [
16824
16905
  "getOrder",
16825
16906
  "getIkasOrderFormattedTotalFinalPrice",
@@ -16858,11 +16939,11 @@
16858
16939
  "files": [
16859
16940
  {
16860
16941
  "filename": "index.tsx",
16861
- "content": "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\";\n\nexport default function OrderDetailSection() {\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\">\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"
16942
+ "content": "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"
16862
16943
  },
16863
16944
  {
16864
16945
  "filename": "types.ts",
16865
- "content": "export interface Props {}\n"
16946
+ "content": "export interface Props {\n backgroundColor?: string;\n}\n"
16866
16947
  },
16867
16948
  {
16868
16949
  "filename": "styles.css",
@@ -16870,7 +16951,7 @@
16870
16951
  },
16871
16952
  {
16872
16953
  "filename": "ikas-config-snippet.json",
16873
- "content": "{ \"id\": \"order-detail\", \"name\": \"Order Detail\", \"type\": \"section\", \"props\": [] }\n"
16954
+ "content": "{\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"
16874
16955
  }
16875
16956
  ]
16876
16957
  },
@@ -16963,7 +17044,7 @@
16963
17044
  "id": "product-detail-section",
16964
17045
  "title": "Product Detail Section (Complete)",
16965
17046
  "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.",
16966
- "code": "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}: 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\">\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",
17047
+ "code": "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",
16967
17048
  "relatedFunctions": [
16968
17049
  "getSelectedProductVariant",
16969
17050
  "getDisplayedProductVariantTypes",
@@ -17003,11 +17084,11 @@
17003
17084
  "files": [
17004
17085
  {
17005
17086
  "filename": "index.tsx",
17006
- "content": "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}: 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\">\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"
17087
+ "content": "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"
17007
17088
  },
17008
17089
  {
17009
17090
  "filename": "types.ts",
17010
- "content": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n showFavoriteButton?: boolean;\n addToCartButtonText?: string;\n}\n"
17091
+ "content": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n showFavoriteButton?: boolean;\n addToCartButtonText?: string;\n backgroundColor?: string;\n}\n"
17011
17092
  },
17012
17093
  {
17013
17094
  "filename": "styles.css",
@@ -17015,7 +17096,7 @@
17015
17096
  },
17016
17097
  {
17017
17098
  "filename": "ikas-config-snippet.json",
17018
- "content": "{\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}\n"
17099
+ "content": "{\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"
17019
17100
  }
17020
17101
  ]
17021
17102
  },
@@ -17047,7 +17128,7 @@
17047
17128
  "id": "product-list-section",
17048
17129
  "title": "Product List Section (Complete)",
17049
17130
  "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().",
17050
- "code": "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}: 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\">\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",
17131
+ "code": "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",
17051
17132
  "relatedFunctions": [
17052
17133
  "getSelectedProductVariant",
17053
17134
  "getProductVariantFormattedFinalPrice",
@@ -17085,11 +17166,11 @@
17085
17166
  "files": [
17086
17167
  {
17087
17168
  "filename": "index.tsx",
17088
- "content": "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}: 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\">\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"
17169
+ "content": "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"
17089
17170
  },
17090
17171
  {
17091
17172
  "filename": "types.ts",
17092
- "content": "import { IkasProductList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n productList: IkasProductList;\n title?: string;\n showFilters?: boolean;\n}\n"
17173
+ "content": "import { IkasProductList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n productList: IkasProductList;\n title?: string;\n showFilters?: boolean;\n backgroundColor?: string;\n}\n"
17093
17174
  },
17094
17175
  {
17095
17176
  "filename": "styles.css",
@@ -17097,7 +17178,7 @@
17097
17178
  },
17098
17179
  {
17099
17180
  "filename": "ikas-config-snippet.json",
17100
- "content": "{\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}\n"
17181
+ "content": "{\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"
17101
17182
  }
17102
17183
  ]
17103
17184
  },
@@ -17105,7 +17186,7 @@
17105
17186
  "id": "product-reviews-section",
17106
17187
  "title": "Product Reviews Section (Complete)",
17107
17188
  "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().",
17108
- "code": "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}: 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\">\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",
17189
+ "code": "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",
17109
17190
  "relatedFunctions": [
17110
17191
  "getProductCustomerReviews",
17111
17192
  "getIkasProductCustomerReviewForm",
@@ -17127,11 +17208,11 @@
17127
17208
  "files": [
17128
17209
  {
17129
17210
  "filename": "index.tsx",
17130
- "content": "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}: 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\">\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"
17211
+ "content": "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"
17131
17212
  },
17132
17213
  {
17133
17214
  "filename": "types.ts",
17134
- "content": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n title?: string;\n}\n"
17215
+ "content": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n title?: string;\n backgroundColor?: string;\n}\n"
17135
17216
  },
17136
17217
  {
17137
17218
  "filename": "styles.css",
@@ -17139,7 +17220,7 @@
17139
17220
  },
17140
17221
  {
17141
17222
  "filename": "ikas-config-snippet.json",
17142
- "content": "{\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}\n"
17223
+ "content": "{\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"
17143
17224
  }
17144
17225
  ]
17145
17226
  },
@@ -17147,7 +17228,7 @@
17147
17228
  "id": "register-section",
17148
17229
  "title": "Register Section (Complete)",
17149
17230
  "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.",
17150
- "code": "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}: 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\">\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",
17231
+ "code": "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",
17151
17232
  "relatedFunctions": [
17152
17233
  "initRegisterForm",
17153
17234
  "setRegisterFormEmail",
@@ -17169,11 +17250,11 @@
17169
17250
  "files": [
17170
17251
  {
17171
17252
  "filename": "index.tsx",
17172
- "content": "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}: 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\">\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"
17253
+ "content": "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"
17173
17254
  },
17174
17255
  {
17175
17256
  "filename": "types.ts",
17176
- "content": "export interface Props {\n redirectAfterRegister?: string;\n}\n"
17257
+ "content": "export interface Props {\n redirectAfterRegister?: string;\n backgroundColor?: string;\n}\n"
17177
17258
  },
17178
17259
  {
17179
17260
  "filename": "styles.css",
@@ -17181,7 +17262,7 @@
17181
17262
  },
17182
17263
  {
17183
17264
  "filename": "ikas-config-snippet.json",
17184
- "content": "{\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}\n"
17265
+ "content": "{\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"
17185
17266
  }
17186
17267
  ]
17187
17268
  },
@@ -17189,7 +17270,7 @@
17189
17270
  "id": "reset-password-section",
17190
17271
  "title": "Reset Password Section (Complete)",
17191
17272
  "description": "Complete reset password section with new password and confirm password fields. Uses getRecoverPasswordForm/initRecoverPasswordForm/setRecoverPasswordFormPassword/setRecoverPasswordFormPasswordAgain/submitRecoverPasswordForm pattern.",
17192
- "code": "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\";\n\nexport default function ResetPasswordSection() {\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\">\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",
17273
+ "code": "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",
17193
17274
  "relatedFunctions": [
17194
17275
  "getRecoverPasswordForm",
17195
17276
  "initRecoverPasswordForm",
@@ -17206,11 +17287,11 @@
17206
17287
  "files": [
17207
17288
  {
17208
17289
  "filename": "index.tsx",
17209
- "content": "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\";\n\nexport default function ResetPasswordSection() {\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\">\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"
17290
+ "content": "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"
17210
17291
  },
17211
17292
  {
17212
17293
  "filename": "types.ts",
17213
- "content": "export interface Props {}\n"
17294
+ "content": "export interface Props {\n backgroundColor?: string;\n}\n"
17214
17295
  },
17215
17296
  {
17216
17297
  "filename": "styles.css",
@@ -17218,7 +17299,7 @@
17218
17299
  },
17219
17300
  {
17220
17301
  "filename": "ikas-config-snippet.json",
17221
- "content": "{\n \"id\": \"reset-password\",\n \"name\": \"Reset Password\",\n \"type\": \"section\",\n \"props\": []\n}\n"
17302
+ "content": "{\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"
17222
17303
  }
17223
17304
  ]
17224
17305
  },