webcake-storefront-mcp 1.21.0 → 1.23.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.
@@ -98,6 +98,30 @@ const FIELD = {
98
98
  required: "Field is required (boolean).",
99
99
  validate: "Enable validation (boolean).",
100
100
  };
101
+ // Common runtime.config keys shared by EVERY input-like field (mined from 34 templates).
102
+ // Spread into each field's `config` so they're documented everywhere.
103
+ const FIELD_CONFIG = {
104
+ backgroundInput: "Input background colour.",
105
+ placeholderColor: "Placeholder text colour.",
106
+ labelColor: "Label colour.",
107
+ labelMarginBottom: "Gap below the label (px).",
108
+ textPadding: "Inner padding of the input (px).",
109
+ optionBorderColor: "Input border colour.",
110
+ optionBorderWidth: "Input border width (e.g. '1px').",
111
+ optionBorderStyle: "Input border style (solid|dashed|none).",
112
+ optionBorderRadius: "Input corner radius.",
113
+ };
114
+ // Filter-widget specials shared by category-page filters that drive a product grid
115
+ // (checkbox-group, color-group, radio-group, two-point-range, tags, dropdown). These wire a
116
+ // sidebar control to a grid-product so picking an option re-queries the products.
117
+ const FILTER_SPECIALS = {
118
+ filter_elements: "Ids of the grid-product(s) this filter controls, e.g. ['GRID-PRODUCT-xxx'].",
119
+ sync_tab: "What to filter by: attribute | brand | category | price | tag.",
120
+ count: "How many options to show.",
121
+ options: "Filter options [{id,name,...}] (auto-built when syncing).",
122
+ is_auto_child_categories: "Auto-include child categories (boolean).",
123
+ use_button_filter: "Apply only when an Apply button is clicked (boolean).",
124
+ };
101
125
  export const ELEMENT_ATTRS = {
102
126
  // ── content ──
103
127
  text: {
@@ -151,7 +175,7 @@ export const ELEMENT_ATTRS = {
151
175
  },
152
176
  container: { specials: { tabIndex: "Tab order.", custom_class: "CSS class.", custom_css: "Raw CSS." }, config: { maxWidth: "Max width.", maxWidthUnit: "Max-width unit.", fixedPosition: "Sticky/fixed positioning.", animation: "Entrance animation." }, events: "click, hover — clickable groups are common.", notes: "Generic grid box; nest children with their own grid." },
153
177
  row: { events: "none", notes: "Simple horizontal wrapper." },
154
- tabs: { specials: { activeTab: "Default active tab index." }, events: "none", notes: "Tabbed container." },
178
+ tabs: { specials: { activeTab: "Default active tab index.", tabs: "Tab definitions [{id,name}].", navMode: "How tabs switch: toggle | …", showNav: "Show the tab nav (boolean).", showBullet: "Show bullets (boolean).", showDivider: "Show a divider (boolean)." }, config: { navPosition: "top|left|right|bottom.", alignNav: "Nav alignment.", nav_display_style: "Nav style." }, events: "tab — fires when the active tab changes.", notes: "Tabbed container; each tab maps to a tab item the content reacts to (e.g. account panels, product detail/description/reviews)." },
155
179
  collapse: { events: "none", notes: "Accordion container (collapse-item children)." },
156
180
  "collapse-item": { events: "none", notes: "One accordion panel." },
157
181
  "custom-layout": { events: "none", notes: "Free-form layout container." },
@@ -166,43 +190,53 @@ export const ELEMENT_ATTRS = {
166
190
  // ── form ──
167
191
  form: {
168
192
  specials: {
169
- type: "Form behaviour — one of: form_login | form_signup | form_order | form_discount | subscribe | order_tracking | form_booking | form_search_agency | question_form.",
193
+ type: "Form behaviour — one of: form_login | form_signup | form_order | form_discount | subscribe | order_tracking | form_booking | form_search_agency | question_form | customer_data | reset_password | forgot_password.",
170
194
  submit_success: "Action after a successful submit (e.g. show a message / redirect).",
171
195
  message: "Success message text.",
172
196
  show_message: "Show a success message after submit (boolean).",
173
197
  show_message_time: "How long the success message stays (seconds).",
198
+ multiForms: "Ids of inner sub-forms this form owns (order forms split fields across an inner form).",
199
+ formParent: "On an inner sub-form: the id of the owning outer form.",
200
+ tabIndex: "Bind the form to a tab item (TAB-ITEM-…) in a tabbed account panel.",
201
+ autoGetInfoCustomer: "Pre-fill from the logged-in customer (boolean).",
202
+ partner_id: "Partner/agency id (booking/search-agency forms).",
203
+ time_otp: "OTP resend countdown (seconds).",
174
204
  },
175
- config: { backgroundInput: "Input background.", labelColor: "Label colour.", placeholderColor: "Placeholder colour.", textPadding: "Input padding (px).", labelMarginBottom: "Gap below label (px).", borderColor: "Input border colour (e.g. #dbe0e6).", borderRadius: "Input corner radius (e.g. 8px)." },
205
+ config: { ...FIELD_CONFIG, borderColor: "Input border colour (e.g. #dbe0e6).", borderRadius: "Input corner radius (e.g. 8px).", labelPadding: "Label padding.", strokeColor: "Text stroke colour.", textColor: "Form text colour." },
176
206
  events: "success — fire follow-up actions after a successful submit (a form_order commonly does open_page→Thank-you page). eventName defaults to 'success' on a form.",
177
207
  notes: "Wrap input fields as children; put a submit-button inside. specials.type selects what the form does on submit. Real order forms split fields across an inner form via specials.multiForms:[innerFormId] (inner form sets specials.formParent). A form_order uses dedicated field types: input(full_name), phone-number, email, detect-address, address (×3 province/district/ward), text-area(note).",
178
208
  },
179
- input: { specials: { ...FIELD, type: "Input type (default text)." }, config: { textPadding: "Padding.", backgroundInput: "Background." }, events: "none" },
180
- email: { specials: { ...FIELD, validate_email: "Email regex." }, events: "none", notes: "field_name usually 'email'." },
181
- "phone-number": { specials: { ...FIELD }, events: "none", notes: "field_name usually 'phone_number'." },
182
- "retype-phone-number": { specials: { ...FIELD }, events: "none" },
183
- password: { specials: { ...FIELD, field_type: "password.", validate_password: "Password regex." }, events: "none", notes: "field_name 'password'." },
184
- "retype-password": { specials: { ...FIELD, validate_password: "Password regex." }, events: "none" },
185
- "current-password": { specials: { ...FIELD, validate_password: "Password regex." }, events: "none" },
186
- "text-area": { specials: { ...FIELD }, events: "none", notes: "Multi-line; field_name often 'note'." },
187
- select: { specials: { ...FIELD, options: "Choices [{id,name}] (or {type,name,image,active}).", defaultValue: "Selected option id." }, config: { arrowColor: "Dropdown arrow colour.", optionBorderColor: "Option border." }, events: "none" },
188
- "group-select": { specials: { ...FIELD, options: "Choices [{id,name}]." }, events: "none" },
189
- checkbox: { specials: { field_name: FIELD.field_name, label: FIELD.label, show_label: FIELD.show_label, required: FIELD.required }, events: "none" },
190
- "checkbox-group": { specials: { ...FIELD, options: "Choices [{id,name}]." }, config: { gap: "Item spacing.", column: "Columns." }, events: "none" },
209
+ input: { specials: { ...FIELD, type: "Input type (default text).", is_manually_added: "Field was added by hand vs auto (boolean)." }, config: { ...FIELD_CONFIG }, events: "none" },
210
+ email: { specials: { ...FIELD, validate_email: "Email regex." }, config: { ...FIELD_CONFIG }, events: "none", notes: "field_name usually 'email'." },
211
+ "phone-number": { specials: { ...FIELD }, config: { ...FIELD_CONFIG }, events: "none", notes: "field_name usually 'phone_number'." },
212
+ "retype-phone-number": { specials: { ...FIELD }, config: { ...FIELD_CONFIG }, events: "none" },
213
+ password: { specials: { ...FIELD, field_type: "password.", validate_p: "Password regex (e.g. ^[a-zA-Z0-9]{6,}$).", validate_password: "Password regex (alt key).", showHidePassword: "Show the eye toggle (boolean)." }, config: { ...FIELD_CONFIG }, events: "none", notes: "field_name 'password'." },
214
+ "retype-password": { specials: { ...FIELD, field_type: "retype-password.", showHidePassword: "Show the eye toggle (boolean)." }, config: { ...FIELD_CONFIG }, events: "none" },
215
+ "current-password": { specials: { ...FIELD, field_type: "current-password.", showHidePassword: "Show the eye toggle (boolean)." }, config: { ...FIELD_CONFIG }, events: "none" },
216
+ "text-area": { specials: { ...FIELD }, config: { ...FIELD_CONFIG }, events: "none", notes: "Multi-line; field_name often 'note'." },
217
+ select: { specials: { ...FIELD, options: "Choices [{id,name}] (or {type,name,image,active}).", defaultValue: "Selected option id.", isRetrieve: "Pre-fill from saved data (boolean).", isHideImage: "Hide option images (boolean)." }, config: { ...FIELD_CONFIG, arrowColor: "Dropdown arrow colour.", strokeColor: "Text stroke.", textColor: "Option text colour." }, events: "none" },
218
+ "group-select": { specials: { ...FIELD, options: "Choices [{id,name}]." }, config: { ...FIELD_CONFIG }, events: "none" },
219
+ checkbox: { specials: { field_name: FIELD.field_name, label: FIELD.label, show_label: FIELD.show_label, required: FIELD.required, options: "Choices [{id,name,connects?}].", defaultValue: "Default checked id ('none').", icon_color: "Check colour.", isRetrieve: "Pre-fill from saved data (boolean)." }, config: { ...FIELD_CONFIG, strokeColor: "Text stroke." }, events: "none" },
220
+ "checkbox-group": { specials: { ...FIELD, ...FILTER_SPECIALS, icon_color: "Check colour.", is_attribute_by_category: "Build options from the category's attributes (boolean)." }, config: { ...FIELD_CONFIG, borderRadiusInput: "Checkbox corner radius.", radioButtonSize: "Checkbox size (px).", spaceButtonText: "Gap between box and label.", strokeColor: "Text stroke.", textColor: "Label colour." }, events: "none", notes: "Doubles as a category-page FILTER: set filter_elements + sync_tab ('brand'/'attribute') to drive a grid-product." },
191
221
  "checkbox-item": { specials: { label: "Option text." }, events: "none" },
192
- radio: { specials: { field_name: FIELD.field_name, label: FIELD.label, show_label: FIELD.show_label, required: FIELD.required }, events: "none" },
193
- "radio-group": { specials: { ...FIELD, options: "Choices [{id,name,events?,connects?}]." }, config: { gap: "Item spacing.", column: "Columns." }, events: "none" },
194
- address: { specials: { field_name: "province_id/district_id/commune_id.", label: FIELD.label, placeholder: FIELD.placeholder, show_label: FIELD.show_label, country: "Country code (e.g. 84).", required_province: "Require province.", required_districts: "Require district.", required_commune: "Require commune." }, events: "none" },
195
- "detect-address": { specials: { ...FIELD, field_type: "detect_address." }, events: "none" },
196
- "postal-code": { specials: { ...FIELD }, events: "none" },
197
- country: { specials: { ...FIELD }, events: "none" },
198
- "input-file": { specials: { ...FIELD, isTransferImage: "Transfer as image (boolean)." }, events: "none" },
199
- "input-number": { specials: { ...FIELD }, events: "none" },
200
- "input-date": { specials: { ...FIELD, type: "date | time." }, events: "none" },
201
- "input-search": { specials: { field_name: "search.", label: FIELD.label, placeholder: FIELD.placeholder }, events: "none" },
202
- "otp-input": { specials: { ...FIELD, isOpenModalOtp: "Open OTP modal (boolean)." }, events: "none" },
203
- "rating-input": { specials: { field_name: FIELD.field_name, label: FIELD.label, show_label: FIELD.show_label }, events: "none" },
204
- identity: { specials: { ...FIELD }, events: "none", notes: "Email-or-phone login field; field_name 'identity'." },
205
- switch: { events: "none", notes: "Toggle; style width/height." },
222
+ radio: { specials: { field_name: FIELD.field_name, label: FIELD.label, show_label: FIELD.show_label, required: FIELD.required, options: "Choices [{id,name}].", defaultValue: "Default selected id.", icon_color: "Dot colour." }, config: { ...FIELD_CONFIG, sizeInput: "Radio size (px).", strokeColor: "Text stroke.", textColor: "Label colour." }, events: "none" },
223
+ "radio-group": { specials: { ...FIELD, ...FILTER_SPECIALS }, config: { ...FIELD_CONFIG, radioButtonSize: "Radio size (px).", spaceButtonText: "Gap between dot and label.", strokeColor: "Text stroke." }, events: "none", notes: "Can act as a FILTER (filter_elements + sync_tab)." },
224
+ address: { specials: { field_name: "province_id/district_id/commune_id.", label: FIELD.label, show_label: FIELD.show_label, useNewAddress: "Use the newer province/district/commune picker (boolean).", isColumn: "Stack the three selects in a column (boolean).", provinceName: "Province placeholder.", districtName: "District placeholder.", communeName: "Commune placeholder.", default_province: "Default province id ('none').", country: "Country code (e.g. 84).", required_province: "Require province.", required_districts: "Require district.", required_commune: "Require commune." }, config: { ...FIELD_CONFIG }, events: "none", notes: "Renders 3 linked selects (province→district→commune). A form_order usually has 3 address elements." },
225
+ "detect-address": { specials: { ...FIELD, field_type: "detect_address." }, config: { ...FIELD_CONFIG }, events: "none", notes: "Free-text address that auto-detects province/district." },
226
+ "postal-code": { specials: { ...FIELD, postal_code: "Regex (e.g. [0-9]{6}).", numberCharacter: "Expected length (e.g. '4_character')." }, config: { ...FIELD_CONFIG }, events: "none" },
227
+ country: { specials: { ...FIELD, countries: "Country list [{country_code,country_name}].", defaultValue: "Default country code (e.g. 'VN')." }, config: { ...FIELD_CONFIG }, events: "none" },
228
+ "input-file": { specials: { ...FIELD, isTransferImage: "Transfer as image (boolean)." }, config: { ...FIELD_CONFIG }, events: "none", notes: "Upload box — usually a dashed optionBorderStyle." },
229
+ "input-number": { specials: { ...FIELD }, config: { ...FIELD_CONFIG }, events: "none" },
230
+ "input-date": { specials: { ...FIELD, type: "date | time.", field_connect_pos: "Map to a POS customer field (e.g. customer_birthday)." }, config: { ...FIELD_CONFIG }, events: "none" },
231
+ "input-search": { specials: { field_name: "search.", label: FIELD.label, placeholder: FIELD.placeholder, showIcon: "Show the search icon (boolean).", searchIcon: "Search icon SVG.", showModalSearch: "Open results in a modal (boolean).", default_search_page: "Where Enter searches ('category_page').", isSearchInCategoryPage: "Search within the current category (boolean).", isSearchInBlogPage: "Search blog too (boolean).", view_keep_keywords: "Keep recent keywords ('category')." }, config: { inputBackground: "Input bg.", inputBorderColor: "Border colour.", inputBorderRadius: "Corner radius.", inputBorderWidth: "Border width.", placeholderColor: "Placeholder colour.", iconColor: "Icon colour.", iconAlign: "left|right.", iconSize: "Icon size (px).", textAlign: "Text alignment." }, events: "onenter — fires a search on Enter.", notes: "Storefront search box; pair with search-droppable for live results." },
232
+ "otp-input": { specials: { ...FIELD, isOpenModalOtp: "Open OTP modal (boolean).", time_otp: "Resend countdown (seconds)." }, config: { ...FIELD_CONFIG }, events: "none" },
233
+ "rating-input": { specials: { field_name: FIELD.field_name, label: FIELD.label, show_label: FIELD.show_label, auto_hide_without_star: "Hide when no rating (boolean).", activeStarIcon: "Filled-star SVG.", inactiveStarIcon: "Empty-star SVG." }, config: { activeStarColor: "Filled star colour.", inactiveStarColor: "Empty star colour." }, events: "none", notes: "Star rating input (review form)." },
234
+ identity: { specials: { ...FIELD }, config: { ...FIELD_CONFIG }, events: "none", notes: "Email-or-phone login field; field_name 'identity'." },
235
+ "search-form": { specials: { ...FIELD, field_type: "search." }, config: { ...FIELD_CONFIG }, events: "none", notes: "A search field inside a form." },
236
+ "search-droppable": { config: { use_overlay: "Dim the page behind the results (boolean).", droppable_position: "Where results open (custom|...)." }, events: "none", notes: "Live search-results dropdown paired with an input-search." },
237
+ "two-point-range": { specials: { filter: "What it filters (e.g. 'filter_price').", min: "Range minimum.", max: "Range maximum.", display_type: "Handle style (round|...).", use_button_filter: "Apply only on a button (boolean)." }, config: { dotColor: "Handle colour.", dotSize: "Handle size (px).", progressColor: "Active track colour.", progressBgColor: "Inactive track colour.", progressSize: "Track thickness (px).", textColor: "Min/max label colour." }, events: "none", notes: "A price (or numeric) RANGE filter for the category page — set filter + filter_elements to drive a grid-product." },
238
+ "color-group": { specials: { ...FILTER_SPECIALS, options: "Swatches [{color, filter:{attrName,...}}].", sync_tab: "attribute (colour variations)." }, config: { colorWidth: "Swatch width.", colorHeight: "Swatch height.", colorGap: "Gap between swatches.", colorBorderRadius: "Swatch radius.", colorBorderColor: "Swatch border.", colorBorderColorActive: "Selected swatch border.", textPosition: "Label position.", hiddenText: "Hide the label (boolean)." }, events: "none", notes: "Colour-swatch FILTER (category page) — filter_elements + sync_tab:'attribute' drive a grid-product." },
239
+ switch: { specials: { defaultValue: "Default on/off (boolean)." }, config: { backgroundSlider: "Track colour.", colorRound: "Knob colour.", colorSlider: "Active track colour." }, events: "none", notes: "Toggle; style width/height." },
206
240
  // ── commerce ──
207
241
  "grid-product": {
208
242
  specials: { displayStyle: "default | only_image | border.", original_price_position: "left | right.", products_per_load: "Products fetched per page/scroll (real range 8–36).", category_id: "Source products from a specific category id (else current category).", show_original_price: "Show the compare-at price (boolean).", show_discount_on_price: "Show a discount badge on the price (boolean).", show_quick_view: "Show a quick-view button on hover (boolean).", show_product_name: "Show the product name (boolean).", show_product_price: "Show the price (boolean).", show_rating: "Show the star rating (boolean).", load_rating: "Fetch ratings for the cards (boolean).", show_ribbon: "Show a ribbon/badge (boolean).", load_brand: "Fetch + show the brand (boolean).", load_category: "Fetch + show the category (boolean).", show_categories: "Show category labels (boolean).", on_hover: "Card hover effect: zoom | swap (swap to 2nd image).", randomDisplay: "Randomise product order (boolean).", typeLinkCategory: "Related-products source: similar_product | products_viewed.", plugins: "Extra card plugins [{name,type,icon}] (e.g. attribute templates, extra info)." },
@@ -253,16 +287,16 @@ export const ELEMENT_ATTRS = {
253
287
  "favorite-icon": { events: "click — toggle favourite." },
254
288
  "bonus-items": { specials: { typeBonus: "combo_product | other." }, events: "none" },
255
289
  // ── navigation ──
256
- menu: { specials: { type: "horizontal | vertical | hamburger | anchor.", sync: "Auto-sync items with site pages (boolean).", fullField: "Stretch items to fill the menu width (boolean)." }, config: { showArrow: "Show the dropdown caret on items with a submenu (boolean).", textColor: "Item text colour (often a theme var like var(--color_20)).", colorHover: "Item hover colour.", "--padX": "Horizontal padding inside each item (CSS var, e.g. 15px)." }, events: "none", notes: "Holds menu-item children. Real templates colour items via theme vars (var(--color_20))." },
257
- "menu-item": { specials: { name: "Link label.", isCustom: "Custom link (boolean).", linkType: "page | category | blog_category | brand | product_tag | custom.", linkPage: "Target page id (linkType=page).", pageId: "Target page id (mirror of linkPage, set both).", linkCategory: "Linked category id.", categoryId: "Category id.", syncSubmenu: "Auto-build submenu (boolean)." }, events: "navigation lives in SPECIALS here, NOT events — set linkType + the matching id.", notes: "For a page link set linkType='page' + linkPage + pageId + name. (Generic text/container/button elements navigate via events:[{action:'open_page',open_page_id}] instead.)" },
258
- "menu-anchor-item": { specials: { name: "Label.", isCustom: "Custom.", linkType: "Link kind.", categoryId: "Category id." }, events: "click — scrolls to an anchor." },
259
- submenu: { events: "none", notes: "Dropdown under a menu-item." },
260
- "menu-droppable": { events: "none" },
261
- "member-bar": { events: "none", notes: "Logged-in member bar (account links)." },
262
- "member-dropdown": { events: "none" },
263
- dropdown: { events: "click/hover — toggles dropdown-content." },
264
- "dropdown-content": { events: "none" },
265
- "language-menu": { events: "none", notes: "Language switcher." },
290
+ menu: { specials: { type: "horizontal | vertical | hamburger | collapse | grid | anchor.", sync: "Auto-sync items with site pages (boolean).", fullField: "Stretch items to fill the menu width (boolean).", shapeType: "'Icon' to render a hamburger/icon trigger instead of text.", url: "Icon image url (icon-style menu).", isPostList: "Build items from blog posts (boolean).", tabIndex: "Bind to a tab item." }, config: { showArrow: "Show the dropdown caret on items with a submenu (boolean).", textColor: "Item text colour (often a theme var like var(--color_20)).", colorHover: "Item hover colour.", activeColor: "Active item colour.", "--padX": "Horizontal padding inside each item (CSS var, e.g. 15px).", mask: "Icon SVG mask (icon-style menu).", arrowColor: "Caret colour.", arrowSize: "Caret size (px).", backgroundHover: "Item hover background.", borderColorHover: "Item hover border colour.", borderRadiusHover: "Item hover radius.", fontWeightHover: "Item hover font weight.", activeLink: "Active-item style object (e.g. {fontWeight:'bold'}).", maxHeight: "Max dropdown height (px)." }, events: "none", notes: "Holds menu-item children. Real headers carry TWO menus (a horizontal desktop menu + a hamburger/collapse mobile menu). Colour items via theme vars (var(--color_20))." },
291
+ "menu-item": { specials: { name: "Link label.", isCustom: "Custom link (boolean).", isSync: "Auto-synced from site pages (boolean).", linkType: "page | category | blog_category | brand | product_tag | custom.", linkPage: "Target page id (linkType=page).", pageId: "Target page id (mirror of linkPage, set both).", linkCategory: "Linked product category id.", linkCategoryTarget: "Open target for the category (_self/_blank).", linkBlogCategory: "Linked blog category id.", link: "Custom href (linkType=custom).", categoryId: "Category id.", syncSubmenu: "Auto-build submenu (boolean).", type: "vertical for a stacked submenu item." }, config: { strokeColor: "Text stroke colour." }, events: "navigation lives in SPECIALS here, NOT events — set linkType + the matching id.", notes: "For a page link set linkType='page' + linkPage + pageId + name. Category → linkType='category' + linkCategory. Custom url → linkType='custom' + link + isCustom. (Generic text/container/button navigate via events:[{action:'open_page',open_page_id}].)" },
292
+ "menu-anchor-item": { specials: { name: "Label.", isCustom: "Custom.", linkType: "Link kind.", anchor: "Target anchor id on the page.", categoryId: "Category id." }, events: "click — scrolls to an anchor." },
293
+ submenu: { specials: { syncSubmenu: "Auto-build from child pages/categories (boolean).", type: "vertical | horizontal.", effect: "Open animation (fade-in|…).", timeAnim: "Animation duration (s)." }, events: "none", notes: "Dropdown panel under a menu-item." },
294
+ "menu-droppable": { specials: { effect: "Open animation (light-speed|fade-in|…).", timeAnim: "Animation duration (s)." }, config: { isUseInitMaxHeight: "Animate from a max-height (boolean)." }, events: "none", notes: "Mega-menu panel under a menu-item (holds a nested menu + content)." },
295
+ "member-bar": { specials: { showView: "Which state to show: not_logged | logged.", layoutShow: "avatar | text | …", textLogin: "Login label.", textSignup: "Signup label.", textLogout: "Logout label.", textBeforeLogin: "Text shown before login.", login_icon: "Login icon SVG.", signup_icon: "Signup icon SVG.", login_popup: "Popup id opened for login.", register_popup: "Popup id opened for signup.", show_signup: "Show the signup option (boolean).", event_type: "What clicking does (open_page|open_popup).", open_when_logged: "Logged-in behaviour (collapse|…).", arrowShape: "Dropdown arrow shape." }, config: { avatarSize: "Avatar size (px).", colorIcon: "Icon colour.", memberColorHover: "Hover colour.", bgDropdown: "Dropdown background.", justifyBar: "Bar alignment." }, events: "click — login/logout/open account.", notes: "Account bar: shows login/signup when logged out, avatar+menu when logged in. member-dropdown is its dropdown panel." },
296
+ "member-dropdown": { specials: { dropdown_position: "left | right." }, events: "none", notes: "The account dropdown under a member-bar." },
297
+ dropdown: { specials: { placeholder: "Closed-state label (e.g. 'Thứ tự mặc định').", options: "Choices [{id,name}].", count: "How many options.", is_default_select: "Pre-select the first (boolean).", filter_elements: "Grid-product id(s) this sort/filter controls.", sync_tab: "What it syncs from (category|…).", redirect_type: "Navigate on select (none|page).", redirect_id: "Target id when redirecting.", arrowIcon: "Arrow SVG.", use_button_filter: "Apply only on a button (boolean)." }, config: { arrowColor: "Arrow colour.", arrowSize: "Arrow size (px).", strokeColor: "Text stroke.", textColor: "Text colour." }, events: "click/hover — toggles dropdown-content. Commonly a product SORT control on the category page (set filter_elements).", notes: "Product sort/filter dropdown, or a generic toggle for dropdown-content." },
298
+ "dropdown-content": { events: "none", notes: "The panel a dropdown toggles." },
299
+ "language-menu": { specials: { show_flag: "Show country flags (boolean).", type: "dropdown | inline." }, config: { display: "full | abbreviated.", flagSize: "Flag size (px).", color: "Text colour.", colorHover: "Hover colour.", backgroundDropdown: "Dropdown background.", colorSelected: "Selected language colour.", dropdown_icon_size: "Caret size (px)." }, events: "none", notes: "Language switcher (multilingual sites)." },
266
300
  // ── blog ──
267
301
  "post-list": { events: "none", notes: "Lists blog articles. Create blog category + articles first." },
268
302
  "slider-post": { config: { slideItems: "Visible posts.", slideWidth: "Card width." }, events: "none" },
@@ -270,6 +304,18 @@ export const ELEMENT_ATTRS = {
270
304
  "slider-blog": { specials: { showBlogs: "Categories to show." }, config: { columns: "Visible posts." }, events: "none" },
271
305
  "post-overlay": { specials: { type: "regular | custom." }, events: "none", notes: "Post card overlay (post page)." },
272
306
  "blog-overlay": { specials: { type: "regular | custom." }, events: "none" },
307
+ // ── filters / widgets / states (mined from 34 templates) ──
308
+ tags: { specials: { ...FILTER_SPECIALS, activeMode: "How active tags render.", selectionMode: "single | multiple.", tabIndex: "Bind to a tab." }, config: { tags_layout: "Tag layout style." }, events: "click — filters the linked grid-product.", notes: "Tag/attribute chips that FILTER a product grid (category page) — set filter_elements + sync_tab:'tag'." },
309
+ "auto-number": { specials: { startNumber: "Start value.", endNumber: "End value.", jumpNumber: "Increment step.", duration: "Count-up duration (s)." }, config: { strokeColor: "Text stroke.", textColor: "Number colour." }, events: "none", notes: "Animated count-up stat (e.g. '10,000+ customers')." },
310
+ agency: { specials: { datagrid_id: "Linked data grid id.", googleTarget: "Google-map target." }, config: { hoverColor: "Hover colour.", activeColor: "Active colour." }, events: "none", bindings: ["agency::name", "agency::phone_number", "agency::address", "agency::time"], notes: "Store-locator / branch list (repeater over agency::*)." },
311
+ "user-point-log": { specials: { itemGap: "Row gap.", noteColor: "Note colour.", datetimeColor: "Date colour." }, events: "none", notes: "Loyalty reward-point history (member page)." },
312
+ "empty-product-layout": { events: "none", notes: "Empty-state shown inside a grid-product when no products match." },
313
+ "cart-items-empty": { events: "none", notes: "Empty-state shown inside cart-items when the cart is empty." },
314
+ "cart-droppable": { events: "none", notes: "Mini-cart drawer/panel (header). Open via open_cart / open_popup." },
315
+ "lesson-sidebar": { specials: { submit_status: "Lesson completion state." }, config: { backgroundModule: "Module background.", iconSize: "Icon size (px)." }, events: "none", notes: "Course lesson navigation sidebar (course app)." },
316
+ "lesson-items": { events: "none", notes: "Lessons list inside a module (course app)." },
317
+ "next-lesson-droppable": { events: "none", notes: "Next-lesson panel (course app)." },
318
+ "list-lesson-droppable": { events: "none", notes: "Lessons droppable list (course app)." },
273
319
  };
274
320
  const describe = (type, key, where) => {
275
321
  const curated = ELEMENT_ATTRS[type] || {};
@@ -26,7 +26,12 @@ export const BINDING_DATASETS = {
26
26
  "product::product_categories", "product::brand_name", "product::product_tag",
27
27
  "product::product_remain_quantity", "product::total_sold_web", "product::rating_point",
28
28
  "product::rating_count", "product::variation_sku", "product::product_wholesale_price",
29
- "product::discount_price_from_original_price", "product::preorder_text", "product::product_note",
29
+ "product::discount_price_from_original_price", "product::discount_price_to_original_price",
30
+ "product::preorder_text", "product::product_note",
31
+ // course / LMS products (use_store + course app):
32
+ "product::total_course", "product::total_course_learning", "product::course_member_quantity",
33
+ "product::course_total_time", "product::course_video_quantity", "product::lesson_name",
34
+ "product::lesson_description", "product::video_lesson",
30
35
  ],
31
36
  },
32
37
  "product-overlay": {
@@ -46,7 +51,9 @@ export const BINDING_DATASETS = {
46
51
  "cart_item::cart_item_original_price", "cart_item::cart_item_subtotal", "cart_item::cart_item_total_price",
47
52
  "cart_item::cart_item_total_quantity", "cart_item::cart_item_prod_attr", "cart_item::sku",
48
53
  "cart_item::cart_total_price", "cart_item::cart_total_tax", "cart_item::cart_shipping_fee",
49
- "cart_item::cart_promotion_discount", "cart_item::subtotal_before_discount", "cart_item::product_note",
54
+ "cart_item::cart_surcharge_fee", "cart_item::cart_promotion_discount", "cart_item::discount_percent",
55
+ "cart_item::discount_reward_point", "cart_item::total_price_all_promotions",
56
+ "cart_item::subtotal_before_discount", "cart_item::product_note",
50
57
  ],
51
58
  },
52
59
  order: {
@@ -58,6 +65,7 @@ export const BINDING_DATASETS = {
58
65
  "order::order_payment_method", "order::shipping_fee", "order::subtotal", "order::total_price",
59
66
  "order::total_tax", "order::code_order", "order::order_status", "order::payment_status",
60
67
  "order::order_quantity", "order::order_date", "order::order_coupon", "order::order_promotion_discount",
68
+ "order::order_promotion_coupon",
61
69
  ],
62
70
  },
63
71
  order_item: {
@@ -97,6 +105,7 @@ export const BINDING_DATASETS = {
97
105
  "customer::profile_avatar", "customer::profile_name", "customer::phone_number", "customer::email",
98
106
  "customer::gender", "customer::birthday", "customer::order_count", "customer::purchased_amount",
99
107
  "customer::pos_reward_point", "customer::pos_reward_point_level", "customer::pos_level_discount",
108
+ "customer::succeed_order_count",
100
109
  ],
101
110
  },
102
111
  customer_address: {
@@ -146,6 +155,46 @@ export const BINDING_DATASETS = {
146
155
  summary: "Page / site info.",
147
156
  fields: ["general_info::page_name", "general_info::site_name", "general_info::menu_title"],
148
157
  },
158
+ agency: {
159
+ name: "agency",
160
+ page_type: "any",
161
+ repeater_parents: ["agency"],
162
+ summary: "A store / branch in a store-locator (agency) list.",
163
+ fields: ["agency::name", "agency::phone_number", "agency::address", "agency::time"],
164
+ },
165
+ blog: {
166
+ name: "blog",
167
+ page_type: "blog",
168
+ summary: "The current blog CATEGORY (heading on a blog list page) — distinct from `post`.",
169
+ fields: ["blog::category_name", "blog::category_description"],
170
+ },
171
+ search_result: {
172
+ name: "search_result",
173
+ page_type: "store",
174
+ summary: "Search-results context (the query + matched products) on a search page.",
175
+ fields: ["search_result::search_name", "search_result::products"],
176
+ },
177
+ };
178
+ // Binding META keys beyond {id,name,target} — these are HOW a source field combines into the
179
+ // element (mined from 34 real templates). makeBinding preserves any of them verbatim, e.g.
180
+ // new_element('text-dataset',{ bindings:[{ target:'product::product_name', name_style:'sku',
181
+ // number_of_line_title:1 }] }). Listed so the AI knows the real combine-keys (no invented keys).
182
+ export const BINDING_META_KEYS = {
183
+ name_style: "Render style of a name/value: 'sku' | 'variation-sku' (show the SKU/variation form).",
184
+ attr_id: "Bind a SPECIFIC product attribute by id (ATTR-xxxx) — used with attr::attr_name / attr::attr_value.",
185
+ target_attr: "For cart_item::cart_item_prod_attr — pick one attribute by name (e.g. 'Màu sắc').",
186
+ value_attr: "Companion to target_attr — which part to show ('value').",
187
+ prefix_content: "Text prepended before the value (e.g. '-' or a label).",
188
+ separator: "Separator string when the field is a list.",
189
+ value: "A SECOND source for a COMPUTED display, e.g. cart_item::cart_total_price with value:'form::coupon_price' subtracts the coupon. Combine two datasets in one node.",
190
+ order: "Index into a multi-value field (pick the Nth).",
191
+ order_value: "Which sub-value of an ordered field to show (e.g. 'description').",
192
+ number_of_line_title: "Clamp the text to N lines.",
193
+ showText: "Show the field label/text alongside the value (boolean).",
194
+ max_item_show: "Max items to render for a list-valued field.",
195
+ bind_image_by_category_id: "For images: resolve from a category instead of the product.",
196
+ auto_hide_without_star: "Hide the node when there's no rating (boolean).",
197
+ show_video_demo: "Show the product video demo (boolean).",
149
198
  };
150
199
  // Build fast lookups.
151
200
  const ALL_TARGETS = new Set();
@@ -241,8 +290,9 @@ export function validateBindings(node) {
241
290
  /** Catalog for the list_bindings discovery tool. */
242
291
  export function describeBindingsCatalog() {
243
292
  return {
244
- note: "Dataset elements (text-dataset/image-dataset/…) and children of a repeater (grid-product, cart-items, post-list…) carry bindings:[{ id, name, target }]. Build via new_element opts.bindings (ids auto-minted) — e.g. new_element('text-dataset',{ bindings:[{ target:'product::product_price', show_tax:true }] }). A target only resolves on a page whose type enables its dataset (store→use_store, member→use_member, blog→use_blog) — build_page handles the flag.",
245
- page_type_required: { store: ["product", "product-overlay", "cart_item", "order", "order_item", "category", "bonus_item", "promotion_item", "attr"], member: ["customer", "customer_address"], blog: ["post"], any: ["form", "general_info"] },
293
+ note: "Dataset elements (text-dataset/image-dataset/…) and children of a repeater (grid-product, cart-items, post-list…) carry bindings:[{ id, name, target, ...meta }]. Build via new_element opts.bindings (ids auto-minted) — e.g. new_element('text-dataset',{ bindings:[{ target:'product::product_price', show_tax:true }] }). A target only resolves on a page whose type enables its dataset (store→use_store, member→use_member, blog→use_blog) — build_page handles the flag. The optional meta keys (name_style, prefix_content, value, attr_id, …) control HOW the value renders/combines — see meta_keys.",
294
+ meta_keys: BINDING_META_KEYS,
295
+ page_type_required: { store: ["product", "product-overlay", "cart_item", "order", "order_item", "category", "bonus_item", "promotion_item", "attr", "search_result"], member: ["customer", "customer_address"], blog: ["post", "blog"], any: ["form", "general_info", "agency"] },
246
296
  repeater_context: REPEATER_CONTEXT,
247
297
  datasets: Object.values(BINDING_DATASETS).map((ds) => ({
248
298
  name: ds.name,
@@ -26,22 +26,22 @@ export const EVENT_TRIGGERS = [
26
26
  const TRIGGER_NAMES = new Set(EVENT_TRIGGERS.map((t) => t.name));
27
27
  export const EVENT_ACTIONS = [
28
28
  // navigation
29
- { action: "open_page", trigger: "click", category: "navigation", summary: "Go to a page in this site.", required: ["open_page_id"], optional: ["scrollTarget", "scroll_to_id", "scroll_to_element_id", "scrollMore", "no_follow", "active_color"] },
29
+ { action: "open_page", trigger: "click", category: "navigation", summary: "Go to a page in this site.", required: ["open_page_id"], optional: ["scrollTarget", "scroll_to_id", "scroll_to_element_id", "scrollMore", "no_follow", "active_color", "popup_id", "popup_overlay", "close_all_other_popup", "open_category_id", "open_blog_category_id", "open_menu_id", "activeNotify"] },
30
30
  { action: "open_link", trigger: "click", category: "navigation", summary: "Open an external URL.", required: ["link_target"], optional: ["link_target_url", "no_follow"] },
31
31
  { action: "open_category", trigger: "click", category: "navigation", summary: "Open a product category page.", required: ["open_category_id"], optional: ["no_follow"] },
32
32
  { action: "open_blog_category", trigger: "click", category: "navigation", summary: "Open a blog category page.", required: ["open_blog_category_id"], optional: ["no_follow"] },
33
33
  { action: "scroll_to", trigger: "click", category: "navigation", summary: "Smooth-scroll to a section/element on this page.", required: ["scroll_to_id"], optional: ["scroll_to_element_id", "scrollMore", "scrollTarget"], target_in_page: ["scroll_to_id", "scroll_to_element_id"] },
34
34
  { action: "direction_login", trigger: "click", category: "navigation", summary: "Redirect by login state: open_page_id if logged in, target_direction_id if not.", required: ["open_page_id"], optional: ["target_direction_id"] },
35
35
  // ui control
36
- { action: "toggle", trigger: "click", category: "ui", summary: "Show/hide another element on this page.", required: ["toggle_id"], optional: ["toggle_status", "only_mode"], target_in_page: ["toggle_id"] },
37
- { action: "open_popup", trigger: "click", category: "ui", summary: "Open a popup.", required: ["popup_id"], optional: ["popup_overlay", "close_all_other_popup"] },
36
+ { action: "toggle", trigger: "click", category: "ui", summary: "Show/hide another element on this page (the most common interaction — accordions, drawers, filters).", required: ["toggle_id"], optional: ["toggle_status", "only_mode"], target_in_page: ["toggle_id"] },
37
+ { action: "open_popup", trigger: "click", category: "ui", summary: "Open a popup (use mouseenter for a hover mini-cart/submenu).", required: ["popup_id"], optional: ["popup_overlay", "close_all_other_popup", "open_page_id", "only_mode", "toggle_id", "open_category_id", "scroll_to_id"] },
38
38
  { action: "close_popup", trigger: "click", category: "ui", summary: "Close a popup.", required: ["popup_id"], optional: ["popup_overlay", "close_all_other_popup"] },
39
39
  { action: "open_menu", trigger: "click", category: "ui", summary: "Open a dropdown/submenu.", required: ["open_menu_id"], target_in_page: ["open_menu_id"] },
40
40
  { action: "close_menu", trigger: "click", category: "ui", summary: "Close a menu.", optional: ["close_menu_id"] },
41
41
  { action: "change_tab", trigger: "click", category: "ui", summary: "Switch a swiper/tabs to a given tab.", required: ["change_tab_id"], optional: ["move_to", "tab_index", "tab_color"], target_in_page: ["change_tab_id"] },
42
42
  { action: "load_more", trigger: "click", category: "ui", summary: "Load more items in a list.", optional: ["load_more_id"], target_in_page: ["load_more_id"] },
43
43
  // cart / commerce
44
- { action: "add_to_cart", trigger: "click", category: "cart", summary: "Add the current product to the cart.", optional: ["open_page", "auto_add_bonus", "activeNotify", "productNoteElId"] },
44
+ { action: "add_to_cart", trigger: "click", category: "cart", summary: "Add the current product to the cart.", optional: ["open_page", "open_page_id", "open_popup_id", "open_target", "open_category_id", "auto_add_bonus", "activeNotify", "productNoteElId"] },
45
45
  { action: "buy_now", trigger: "click", category: "cart", summary: "Add to cart and go straight to checkout.", optional: ["open_page", "auto_add_bonus", "activeNotify"] },
46
46
  { action: "add_to_cart_form", trigger: "success", category: "cart", summary: "Add to cart from a form submit (product form).", optional: ["open_page"] },
47
47
  { action: "open_cart", trigger: "click", category: "cart", summary: "Open the cart sidebar/drawer." },
@@ -66,10 +66,32 @@ export const EVENT_ACTIONS = [
66
66
  { action: "send_whatsapp_mess", trigger: "click", category: "contact", summary: "Open WhatsApp chat.", optional: ["whatsapp_phone_number"] },
67
67
  { action: "sharing", trigger: "click", category: "contact", summary: "Share to a social network.", optional: ["shareTarget", "shareLink", "shareLinkCustom"] },
68
68
  // hover styles (eventName must be "hover")
69
- { action: "scale", trigger: "hover", category: "hover", summary: "Scale the element on hover.", optional: ["el_target_id"] },
70
- { action: "change_background", trigger: "hover", category: "hover", summary: "Change background colour on hover." },
71
- { action: "change_text_color", trigger: "hover", category: "hover", summary: "Change text colour on hover." },
72
- { action: "change_border_color", trigger: "hover", category: "hover", summary: "Change border colour on hover." },
69
+ { action: "scale", trigger: "hover", category: "hover", summary: "Scale the element on hover.", optional: ["el_target_id", "scale_value"] },
70
+ { action: "change_background", trigger: "hover", category: "hover", summary: "Change background colour on hover.", optional: ["change_background", "apply_element", "cursor", "el_target_id"] },
71
+ { action: "change_text_color", trigger: "hover", category: "hover", summary: "Change text colour on hover.", optional: ["change_text_color", "apply_element", "cursor", "el_target_id"] },
72
+ { action: "change_border_color", trigger: "hover", category: "hover", summary: "Change border colour on hover.", optional: ["change_border_color", "apply_element", "cursor", "el_target_id"] },
73
+ { action: "transform_y", trigger: "hover", category: "hover", summary: "Shift the element vertically on hover (lift effect).", optional: ["transform_y", "apply_element"] },
74
+ { action: "transform_x", trigger: "hover", category: "hover", summary: "Shift the element horizontally on hover.", optional: ["transform_x", "apply_element"] },
75
+ { action: "cursor", trigger: "hover", category: "hover", summary: "Change the cursor on hover.", optional: ["cursor"] },
76
+ // commerce / cart interactions (mined from 34 real templates)
77
+ { action: "open_quick_view", trigger: "click", category: "cart", summary: "Open a product quick-view popup from a product card.", optional: ["popup_id", "activeNotify", "active_color"] },
78
+ { action: "open_product_page", trigger: "click", category: "navigation", summary: "Open the product detail page for this card's product." },
79
+ { action: "open_preview_product_gallery", trigger: "click", category: "media", summary: "Open the product image in a gallery preview.", optional: ["open_preview_product_gallery_id", "target"] },
80
+ { action: "close_cart_item", trigger: "click", category: "cart", summary: "Remove this line item from the cart." },
81
+ { action: "edit_cart_item", trigger: "click", category: "cart", summary: "Edit this cart line item (qty / variation)." },
82
+ { action: "change_column_grid_prd", trigger: "click", category: "ui", summary: "Switch the product grid's column count.", optional: ["columnNumber", "target_id"], target_in_page: ["target_id"] },
83
+ { action: "add_wishlist_product", trigger: "click", category: "cart", summary: "Add the product to the wishlist.", optional: ["activeNotify"] },
84
+ { action: "wishlist_remove_item", trigger: "click", category: "cart", summary: "Remove an item from the wishlist.", optional: ["target_custom_links"] },
85
+ { action: "use_reward_point", trigger: "click", category: "cart", summary: "Apply loyalty reward points to the order.", optional: ["reward_point", "target"] },
86
+ { action: "copy_promotion", trigger: "click", category: "cart", summary: "Copy a promotion/coupon code to the clipboard.", optional: ["has_text", "is_hidden", "text_change"] },
87
+ // search + menu + account interactions
88
+ { action: "search", trigger: "click", category: "navigation", summary: "Run a storefront product search.", optional: ["open_page_id", "open_category_id"] },
89
+ { action: "close_search", trigger: "click", category: "ui", summary: "Close the search box/droppable.", optional: ["close_search_id"], target_in_page: ["close_search_id"] },
90
+ { action: "toggle_group", trigger: "click", category: "ui", summary: "Toggle a group of elements together (e.g. a mobile menu / filter group).", optional: ["actions", "data", "data_more", "show"] },
91
+ { action: "back_to_menu_parent", trigger: "click", category: "ui", summary: "Go back up one level in a multi-level mobile menu." },
92
+ { action: "lightbox", trigger: "click", category: "media", summary: "Open an image in a lightbox.", optional: ["lightboxLink"] },
93
+ { action: "set_address_default", trigger: "click", category: "account", summary: "Mark a saved address as the default." },
94
+ { action: "change_avatar", trigger: "click", category: "account", summary: "Change the member avatar." },
73
95
  ];
74
96
  const ACTION_BY_NAME = {};
75
97
  for (const a of EVENT_ACTIONS)
@@ -131,6 +131,33 @@ key — only bp1..bp4.)
131
131
  \`text-dataset\` with \`product::product_name\` resolves to that cell's product (no extra
132
132
  wiring). Same for cart-items→cart_item, order-items→order_item, post-list→post,
133
133
  grid-category→category. A target only resolves on a page of the matching \`type\` (below).
134
+ - BINDING COMBINE-KEYS (how a field renders/combines — mined from real templates; pass them
135
+ alongside \`target\`): \`name_style:"sku"|"variation-sku"\`, \`prefix_content:"-"\` (prepend),
136
+ \`number_of_line_title:1\` (clamp lines), \`attr_id:"ATTR-…"\` (bind ONE attribute), and
137
+ \`value:"form::coupon_price"\` — a SECOND source so one node shows a COMPUTED value (e.g.
138
+ \`cart_item::cart_total_price\` with \`value:"form::coupon_price"\` = total minus coupon). Call
139
+ \`list_bindings\` → meta_keys for the full list.
140
+
141
+ ## Composition recipes (how real templates assemble components)
142
+ Repeaters fall into two kinds (surveyed across 34 templates):
143
+ - SELF-RENDERING (just add badges): \`grid-product\` / \`slider-product\` / \`post-list\` render the
144
+ card themselves — their children are only \`product-overlay\` (sale/discount badge) or, for a
145
+ fully custom card, a \`custom-layout\` wrapping dataset cells. \`post-list\`→\`post-overlay\` children.
146
+ - HAND-COMPOSED from per-item dataset cells (build the row yourself):
147
+ - \`cart-items\` row = \`image-dataset[cart_item::cart_item_image]\`, \`text-dataset[cart_item::cart_item_name]\`,
148
+ \`text-dataset[cart_item::cart_item_price]\`, \`quantity-input\`, \`text-dataset[cart_item::cart_item_total_price]\`,
149
+ a \`container\` for name+\`cart_item::cart_item_prod_attr\`, \`line\` (config.columns sets the row template).
150
+ - \`order-items\` row = \`image-dataset[order_item::product_image]\` + \`text-dataset\` for
151
+ \`order_item::product_name\` / \`product_attrs\` / \`product_quantity\` / \`items_sum_up_price\`.
152
+ - \`layout-dataset\` / \`customer-address\` = a generic repeater: \`text-dataset[customer_address::full_name|phone_number|address|pdc|is_default]\` + a "Set default" button.
153
+ - \`promotions\` = \`text-dataset[promotion_item::code|description|end_date]\` + a copy button (\`copy_promotion\`).
154
+ The bare \`cart-items\`/\`order-items\` (no children) self-render a DEFAULT row — fine for a quick build; compose cells only when you want a custom layout.
155
+ HEADER patterns: real headers carry TWO menus (a horizontal desktop \`menu\` + a hamburger/collapse mobile \`menu\`), the logo (image/text), \`cart-icon\`, often \`input-search\`/\`search-droppable\`, \`member-bar\` (login/signup ↔ avatar+account), and \`language-menu\`. BREADCRUMB is common — keep it on category/product pages (often its own slim section, or next to a \`text-dataset\` page title).
156
+ CATEGORY-PAGE FILTERS: a category page sidebar FILTERS the product grid. The filter widgets —
157
+ \`checkbox-group\` / \`color-group\` / \`tags\` / \`two-point-range\` (price) / \`radio-group\` / a sort
158
+ \`dropdown\` — each carry \`specials.filter_elements:["GRID-PRODUCT-id"]\` (the grid they drive) +
159
+ \`specials.sync_tab\` ("attribute" | "brand" | "category" | "price" | "tag") so picking an option
160
+ re-queries that grid-product. So a real category page = breadcrumb + [filter sidebar | grid-product].
134
161
 
135
162
  ## Events (clicks, navigation, cart, popups)
136
163
  Interactive nodes (button, text, image, container, rectangle, icons) carry an \`events\`
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "v": "1.23.0",
4
+ "d": "25/06/2026",
5
+ "type": "Added",
6
+ "en": "get_build_guide now includes a category-page filter recipe explaining how sidebar filter widgets (checkbox-group, color-group, tags,…",
7
+ "vi": "get_build_guide nay bổ sung công thức filter trang danh mục, giải thích cách các widget lọc trong sidebar (checkbox-group, color-group, tags,…"
8
+ },
9
+ {
10
+ "v": "1.22.0",
11
+ "d": "25/06/2026",
12
+ "type": "Added",
13
+ "en": "list_bindings now returns a meta_keys map documenting binding combine-keys mined from 34 production templates — name_style, attr_id, prefix_content,…",
14
+ "vi": "list_bindings nay trả về map meta_keys tài liệu hóa các combine-key liên kết dữ liệu được khai thác từ 34 template production — name_style, attr_id,…"
15
+ },
2
16
  {
3
17
  "v": "1.21.0",
4
18
  "d": "25/06/2026",
@@ -26,19 +40,5 @@
26
40
  "type": "Added",
27
41
  "en": "New scaffold_global_sections tool generates a fully-designed global Header and Footer in one call — logo, nav links, cart icon, CTA button, and a…",
28
42
  "vi": "Tool mới scaffold_global_sections tạo Header và Footer toàn cục đã thiết kế sẵn trong một lần gọi — logo, liên kết điều hướng, icon giỏ hàng, nút…"
29
- },
30
- {
31
- "v": "1.18.1",
32
- "d": "25/06/2026",
33
- "type": "Changed",
34
- "en": "get_build_guide now includes three new sections: \"Build the WHOLE storefront\" with per-page recipes for enriching category, product detail, cart,…",
35
- "vi": "get_build_guide nay bổ sung ba mục mới: \"Build the WHOLE storefront\" — công thức từng trang để nâng cấp danh mục, trang chi tiết sản phẩm, giỏ hàng,…"
36
- },
37
- {
38
- "v": "1.18.0",
39
- "d": "25/06/2026",
40
- "type": "Added",
41
- "en": "New get_intake_guide tool returns a step-by-step questionnaire and recommended build flow (create_site → products → build_page → global sections →…",
42
- "vi": "Tool mới get_intake_guide trả về bảng câu hỏi từng bước và quy trình build được khuyến nghị (create_site → sản phẩm → build_page → global sections →…"
43
43
  }
44
44
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-storefront-mcp",
3
- "version": "1.21.0",
3
+ "version": "1.23.0",
4
4
  "description": "MCP server for the WebCake/StoreCake storefront builder — page CRUD, page authoring, products, orders, and more",
5
5
  "mcpName": "io.github.vuluu2k/webcake-storefront-mcp",
6
6
  "license": "MIT",