webcake-storefront-mcp 1.22.0 → 1.24.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.
@@ -22,6 +22,8 @@ export const SHARED_SPECIALS = {
22
22
  custom_css: "Raw CSS scoped to this element.",
23
23
  element_async: "Mark the element async/lazy (advanced — usually leave unset).",
24
24
  event_name_custom: "Custom analytics event name fired on interaction.",
25
+ tabIndex: "Bind this element to a tab item (TAB-ITEM-…) so a tabs element shows/hides it.",
26
+ text_animation_type: "Entrance animation for the element's text (none|…).",
25
27
  };
26
28
  // Generic layout keys every node's runtime.config may carry (documented in the build
27
29
  // guide); listed here so describeAttributes can flag them as 'generic, not element-specific'.
@@ -98,6 +100,30 @@ const FIELD = {
98
100
  required: "Field is required (boolean).",
99
101
  validate: "Enable validation (boolean).",
100
102
  };
103
+ // Common runtime.config keys shared by EVERY input-like field (mined from 34 templates).
104
+ // Spread into each field's `config` so they're documented everywhere.
105
+ const FIELD_CONFIG = {
106
+ backgroundInput: "Input background colour.",
107
+ placeholderColor: "Placeholder text colour.",
108
+ labelColor: "Label colour.",
109
+ labelMarginBottom: "Gap below the label (px).",
110
+ textPadding: "Inner padding of the input (px).",
111
+ optionBorderColor: "Input border colour.",
112
+ optionBorderWidth: "Input border width (e.g. '1px').",
113
+ optionBorderStyle: "Input border style (solid|dashed|none).",
114
+ optionBorderRadius: "Input corner radius.",
115
+ };
116
+ // Filter-widget specials shared by category-page filters that drive a product grid
117
+ // (checkbox-group, color-group, radio-group, two-point-range, tags, dropdown). These wire a
118
+ // sidebar control to a grid-product so picking an option re-queries the products.
119
+ const FILTER_SPECIALS = {
120
+ filter_elements: "Ids of the grid-product(s) this filter controls, e.g. ['GRID-PRODUCT-xxx'].",
121
+ sync_tab: "What to filter by: attribute | brand | category | price | tag.",
122
+ count: "How many options to show.",
123
+ options: "Filter options [{id,name,...}] (auto-built when syncing).",
124
+ is_auto_child_categories: "Auto-include child categories (boolean).",
125
+ use_button_filter: "Apply only when an Apply button is clicked (boolean).",
126
+ };
101
127
  export const ELEMENT_ATTRS = {
102
128
  // ── content ──
103
129
  text: {
@@ -107,7 +133,8 @@ export const ELEMENT_ATTRS = {
107
133
  notes: "Set specials.text (HTML) + specials.tag. Style via runtime.style (color, fontSize, fontWeight, textAlign, lineHeight, letterSpacing).",
108
134
  },
109
135
  "text-dataset": {
110
- config: {}, events: "none",
136
+ specials: { tag: "Semantic tag (h1..h6/p) — set 'h1' for the product/post title." },
137
+ config: { hover_color: "Text colour on hover." }, events: "none",
111
138
  bindings: ["product::product_name", "product::product_price", "cart_item::cart_item_name", "cart_item::cart_item_price", "order_item::product_name", "customer_address::full_name"],
112
139
  notes: "Text bound to live data — set bindings[].target to a source::field; no static specials.text.",
113
140
  },
@@ -123,35 +150,37 @@ export const ELEMENT_ATTRS = {
123
150
  notes: "Image bound to live data — set bindings[].target; no static src.",
124
151
  },
125
152
  video: {
126
- specials: { src: "Video URL.", thumbnail: "Poster image URL.", time: "Duration label (e.g. '10.0s').", type: "Source type (e.g. store)." },
153
+ specials: { src: "Video URL.", thumbnail: "Poster image URL.", time: "Duration label (e.g. '10.0s').", type: "Source type (store|youtube).", id: "YouTube/provider video id.", autoplay: "Auto-play (boolean).", loop: "Loop (boolean).", showControl: "Show player controls (boolean)." },
154
+ config: { ratio: "Aspect ratio (e.g. 0.5625 = 16/9).", src: "Video URL (mirror)." },
127
155
  events: "none",
128
156
  },
129
- "video-dataset": { events: "none", bindings: ["lesson::lesson_video"], notes: "Video bound to a dataset field." },
157
+ "video-dataset": { specials: { autoRotateInMobile: "Rotate to landscape on mobile (boolean).", show_list_video_lesson: "Show the lesson playlist (boolean).", show_next_video_lesson: "Show the next-lesson button (boolean)." }, events: "none", bindings: ["lesson::lesson_video"], notes: "Video bound to a dataset field (course lesson video)." },
130
158
  rectangle: {
131
159
  specials: { shapeType: "Shape kind.", custom_css: "Raw CSS for the shape." },
132
- config: { mask: "Mask id/shape.", maskId: "Mask reference." },
160
+ config: { mask: "Mask SVG/shape (clips the box into an icon).", maskId: "Mask reference.", custom_mask: "Custom mask SVG markup.", fixedPosition: "Pin/float the shape (bottom_right|…) — e.g. a floating badge.", animation: "Entrance animation." },
133
161
  events: "click, hover — often used as a clickable card/background.",
134
162
  notes: "Coloured box / divider / background. Style via runtime.style (background, border*, boxShadow).",
135
163
  },
136
- line: { events: "none", notes: "Separator line — style via runtime.style background/border." },
137
- gallery: { specials: { media: "Array of media (image) objects." }, config: { showThumbnail: "Show thumbnails.", showNavigation: "Show nav arrows.", thumbnailPosition: "none|top|bottom|left|right." }, events: "none" },
138
- carousel: { config: { slideWidth: "Slide width (px).", slideItems: "Visible slides count." }, events: "none", notes: "Holds slide children." },
139
- swiper: { config: {}, events: "none", notes: "Swiper carousel — holds slide children." },
164
+ line: { specials: { type: "horizontal | vertical." }, events: "none", notes: "Separator line — style via runtime.style background/border." },
165
+ gallery: { specials: { media: "Array of media (image) objects.", showNavigation: "Show nav arrows (boolean).", arrowColor: "Arrow colour.", autoplay_time: "Autoplay interval (s)." }, config: { showThumbnail: "Show thumbnails.", showNavigation: "Show nav arrows.", thumbnailPosition: "none|top|bottom|left|right.", image_ratio: "Image ratio (auto|…).", img_object_fit: "cover|contain.", showPaginate: "Pagination style (bar|none)." }, events: "none" },
166
+ carousel: { specials: { type: "slide_only | …", autoplay: "on|off.", autoplay_time: "Seconds per slide.", arrowColor: "Arrow colour.", slides: "Slide list [{id,name}]." }, config: { slideWidth: "Slide width (px).", slideItems: "Visible slides count.", slideWidthUnit: "px|%.", showPaginate: "Pagination style (bar-dot|…).", paginateMode: "auto|manual.", arrowWidth: "Arrow width.", navColor: "Nav colour." }, events: "none", notes: "Holds slide children." },
167
+ swiper: { specials: { autoplay: "on|off.", autoplayDelay: "ms between slides.", paginate: "Show pagination (boolean).", paginateBar: "bullet|bar|fraction.", direction: "row|column.", infinity: "Loop (boolean).", transitionMode: "slide|fade.", transitionTime: "ms.", auto_hide_navigation: "Hide arrows until hover (boolean).", arrowIcon: "Arrow SVG." }, config: { navColor: "Arrow colour.", navSize: "Arrow size (px).", navBackground: "Arrow background.", bulletSizeRow: "Bullet size.", barWidth: "Progress bar width.", fractionFontSize: "Fraction text size." }, events: "tab — fires on slide change.", notes: "Full-width hero/banner slider — holds slide children. The most common homepage hero." },
140
168
  slide: { events: "none", notes: "A single slide inside a carousel/swiper." },
141
- embed: { specials: { iframe: "Raw HTML / iframe markup." }, events: "none" },
142
- googlemap: { specials: { iframe: "Google Maps embed iframe string." }, events: "none" },
143
- countdown: { specials: { time: "Target date/time.", type: "Countdown mode." }, events: "none", notes: "Timer; check the live skeleton for exact specials." },
144
- breadcrumb: { specials: { options: "Per-context paths (search/product/category/blog/post/default).", icon: "Separator string e.g. '>'." }, events: "none" },
169
+ embed: { specials: { iframe: "Raw HTML / iframe markup.", htmlCode: "Raw HTML/JS snippet (type='code').", type: "code | iframe." }, events: "none", notes: "Raw HTML/JS embed — set htmlCode (type:'code') or iframe." },
170
+ googlemap: { specials: { iframe: "Google Maps embed iframe string.", type: "code | place." }, config: { maxHeight: "Map max height (px)." }, events: "none" },
171
+ countdown: { specials: { time: "Target date/time.", type: "Countdown mode.", duration: "Countdown length (s).", repeat: "Restart when finished (boolean).", typeShow: "Largest unit shown (day|minute|…).", showDay: "Show days (boolean).", showHours: "Show hours (boolean).", showSecond: "Show seconds (boolean).", isPromotion: "Tie to a promotion (boolean).", durationStart: "Start datetime.", durationEnd: "End datetime.", dailyStart: "Daily reset start (HH:MM:SS).", dailyEnd: "Daily reset end.", positionTitle: "Title position (top|bottom).", showText: "Show the title text (boolean)." }, config: { bgTop: "Top-digit background.", bgBottom: "Bottom-digit background.", sizeDot: "Separator dot size.", spacingTitle: "Gap to title." }, events: "none", notes: "Countdown timer (flash sale / launch). Set duration or durationStart/End." },
172
+ breadcrumb: { specials: { options: "Per-context paths (search/product/category/blog/post/default).", icon: "Separator string e.g. '>'." }, config: { hoverColor: "Link hover colour." }, events: "none", notes: "Auto-builds the trail (Home › Category › Product) from the current page — common on category/product pages." },
145
173
  "list-ordered": { specials: { tableItems: "Column definitions [{name,key,i18nKey}]." }, events: "none" },
146
174
  // ── layout ──
147
175
  section: {
148
176
  specials: { global: "Mark as a global/shared section (reused across pages).", custom_class: "CSS class.", layout: "Nested layout payload (advanced)." },
177
+ config: { stickyPosition: "Make the section sticky: default | autobottom | none.", stickyTop: "Sticky offset from top (px).", showShadowWhenSticky: "Add a shadow when stuck (0/1).", video_background: "Section video background {background_display,…}.", pattern_overlay: "Overlay pattern image url.", apply_pattern_overlay: "Apply the overlay pattern (boolean).", nameLineClamp: "Clamp the section title to N lines." },
149
178
  events: "none",
150
- notes: "Top-level band; centred 3-column grid. Children go in the centre column (new_section handles this).",
179
+ notes: "Top-level band; centred 3-column grid. Children go in the centre column (new_section handles this). Make a header section sticky with config.stickyPosition; add a video/pattern background via config.",
151
180
  },
152
- 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." },
181
+ container: { specials: { tabIndex: "Tab order.", custom_class: "CSS class.", custom_css: "Raw CSS." }, config: { maxWidth: "Max width.", maxWidthUnit: "Max-width unit.", fixedPosition: "Float/pin the box (e.g. bottom_right) — for back-to-top / chat bubbles.", fixedRight: "Pinned offset from right (px).", fixedBottom: "Pinned offset from bottom (px).", fixedLeft: "Pinned offset from left (px).", stickyPosition: "Sticky mode.", stickyTop: "Sticky offset from top (px).", stickyBottom: "Sticky offset from bottom (px).", pinnedPoint: "Section id where pinning starts.", unpinnedPoint: "Section id where pinning ends.", useAutoScroll: "Auto-scroll the box (boolean).", initMaxHeight: "Collapsed max-height (px).", isUseInitMaxHeight: "Animate from initMaxHeight (boolean).", maxHeight: "Expanded max-height (px).", animation: "Entrance animation." }, events: "click, hover — clickable groups are common.", notes: "Generic grid box; nest children with their own grid. Pin a floating button/sidebar via config.fixedPosition; sticky filters via stickyTop + pinnedPoint/unpinnedPoint." },
153
182
  row: { events: "none", notes: "Simple horizontal wrapper." },
154
- tabs: { specials: { activeTab: "Default active tab index." }, events: "none", notes: "Tabbed container." },
183
+ 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
184
  collapse: { events: "none", notes: "Accordion container (collapse-item children)." },
156
185
  "collapse-item": { events: "none", notes: "One accordion panel." },
157
186
  "custom-layout": { events: "none", notes: "Free-form layout container." },
@@ -166,63 +195,73 @@ export const ELEMENT_ATTRS = {
166
195
  // ── form ──
167
196
  form: {
168
197
  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.",
198
+ 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
199
  submit_success: "Action after a successful submit (e.g. show a message / redirect).",
171
200
  message: "Success message text.",
172
201
  show_message: "Show a success message after submit (boolean).",
173
202
  show_message_time: "How long the success message stays (seconds).",
203
+ multiForms: "Ids of inner sub-forms this form owns (order forms split fields across an inner form).",
204
+ formParent: "On an inner sub-form: the id of the owning outer form.",
205
+ tabIndex: "Bind the form to a tab item (TAB-ITEM-…) in a tabbed account panel.",
206
+ autoGetInfoCustomer: "Pre-fill from the logged-in customer (boolean).",
207
+ partner_id: "Partner/agency id (booking/search-agency forms).",
208
+ time_otp: "OTP resend countdown (seconds).",
174
209
  },
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)." },
210
+ 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
211
  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
212
  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
213
  },
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" },
214
+ input: { specials: { ...FIELD, type: "Input type (default text).", is_manually_added: "Field was added by hand vs auto (boolean)." }, config: { ...FIELD_CONFIG }, events: "none" },
215
+ email: { specials: { ...FIELD, validate_email: "Email regex." }, config: { ...FIELD_CONFIG }, events: "none", notes: "field_name usually 'email'." },
216
+ "phone-number": { specials: { ...FIELD }, config: { ...FIELD_CONFIG }, events: "none", notes: "field_name usually 'phone_number'." },
217
+ "retype-phone-number": { specials: { ...FIELD }, config: { ...FIELD_CONFIG }, events: "none" },
218
+ 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'." },
219
+ "retype-password": { specials: { ...FIELD, field_type: "retype-password.", showHidePassword: "Show the eye toggle (boolean)." }, config: { ...FIELD_CONFIG }, events: "none" },
220
+ "current-password": { specials: { ...FIELD, field_type: "current-password.", showHidePassword: "Show the eye toggle (boolean)." }, config: { ...FIELD_CONFIG }, events: "none" },
221
+ "text-area": { specials: { ...FIELD }, config: { ...FIELD_CONFIG }, events: "none", notes: "Multi-line; field_name often 'note'." },
222
+ 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" },
223
+ "group-select": { specials: { ...FIELD, options: "Choices [{id,name}]." }, config: { ...FIELD_CONFIG }, events: "none" },
224
+ 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" },
225
+ "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
226
  "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." },
227
+ 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" },
228
+ "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)." },
229
+ 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." },
230
+ "detect-address": { specials: { ...FIELD, field_type: "detect_address." }, config: { ...FIELD_CONFIG }, events: "none", notes: "Free-text address that auto-detects province/district." },
231
+ "postal-code": { specials: { ...FIELD, postal_code: "Regex (e.g. [0-9]{6}).", numberCharacter: "Expected length (e.g. '4_character')." }, config: { ...FIELD_CONFIG }, events: "none" },
232
+ country: { specials: { ...FIELD, countries: "Country list [{country_code,country_name}].", defaultValue: "Default country code (e.g. 'VN')." }, config: { ...FIELD_CONFIG }, events: "none" },
233
+ "input-file": { specials: { ...FIELD, isTransferImage: "Transfer as image (boolean)." }, config: { ...FIELD_CONFIG }, events: "none", notes: "Upload box — usually a dashed optionBorderStyle." },
234
+ "input-number": { specials: { ...FIELD }, config: { ...FIELD_CONFIG }, events: "none" },
235
+ "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" },
236
+ "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." },
237
+ "otp-input": { specials: { ...FIELD, isOpenModalOtp: "Open OTP modal (boolean).", time_otp: "Resend countdown (seconds)." }, config: { ...FIELD_CONFIG }, events: "none" },
238
+ "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)." },
239
+ identity: { specials: { ...FIELD }, config: { ...FIELD_CONFIG }, events: "none", notes: "Email-or-phone login field; field_name 'identity'." },
240
+ "search-form": { specials: { ...FIELD, field_type: "search." }, config: { ...FIELD_CONFIG }, events: "none", notes: "A search field inside a form." },
241
+ "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." },
242
+ "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." },
243
+ "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." },
244
+ 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
245
  // ── commerce ──
207
246
  "grid-product": {
208
- 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)." },
209
- config: { columns: "Products per row (real range 1–6).", responsive: "autofit | fixed | custom (fixed keeps the column count; custom collapses on mobile).", item_show: "How many products to show.", image_ratio: "INDUSTRY-DEPENDENT: 4/5 fashion · 2/3 kids · 3/4 food · 1/1 square (cosmetics/electronics). Also 16/9|3/2|4/3|9/16.", img_object_fit: "cover|contain|fill|stretch.", gap_column: "Column gap (real range 8–40).", gap_row: "Row gap (real range 0–40).", product_info_padding_y: "Vertical padding of the name/price block (px).", price_padding_y: "Vertical padding around the price (px).", product_info_alignment: "left|center|right.", productNameFont: "Product-name font family.", productNameFontSize: "Product-name font size (px, real 12–18).", productNameColor: "Product-name colour.", productNameHover: "Product-name hover colour.", productNamePaddingY: "Vertical padding of the name (px).", productPriceFont: "Price font family.", productPriceFontSize: "Price font size (px).", productPriceFontWeight: "Price font weight — bold across ALL surveyed templates.", productPriceColor: "Price colour (often a theme accent / var).", productOriginalPriceFontSize: "Compare-at price font size (px).", productOriginalPriceColor: "Compare-at price colour (muted grey).", sizeThumbnail: "{ width, height } of the card image." },
247
+ specials: { displayStyle: "default | only_image | border.", original_price_position: "left | right | top | bottom.", 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).", rating_number_type: "Rating count style (review_count|…).", 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).", max_show_categories: "Max category labels per card.", show_total_sold: "Show units sold (boolean).", show_wholesale_price: "Show wholesale price (boolean).", show_divider: "Show a divider in the card (boolean).", show_button: "Show an add-to-cart/quick button (boolean).", show_button_on_hover: "Reveal the button only on hover (boolean).", textQuickBtn: "Quick add-to-cart button label.", action_add_to_cart: "Add-to-cart behaviour (numeric mode).", action_attachment_add_to_cart: "After add-to-cart: open_cart | …", scroll_type: "Pagination scroll behaviour (scroll_to_top|…).", type_view_more: "Load-more style.", barValue: "Stock bar source (remain_quantity|…).", saleInfoDisplayText: "In-stock label.", saleInfoSoldoutText: "Sold-out label.", disable_filter: "Ignore active filters (boolean).", disable_search: "Ignore the search query (boolean).", show_detail_price: "Show a detailed price breakdown (boolean).", show_quantity: "Show a quantity stepper on the card (boolean).", show_wholesale_quantity: "Show wholesale tiers (boolean).", auto_hide_without_star: "Hide rating when none (boolean).", promotion_id: "Restrict to a promotion's products.", action_product_card: "Click behaviour for the whole card (numeric mode).", store_show_attribute_template_1: "Attribute-template plugin config {show_cart,…}.", filledStarIcon: "Filled-star SVG.", unfilledStarIcon: "Empty-star SVG.", 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)." },
248
+ config: { columns: "Products per row (real range 1–6).", responsive: "autofit | fixed | custom (fixed keeps the column count; custom collapses on mobile).", item_show: "How many products to show.", image_ratio: "INDUSTRY-DEPENDENT: 4/5 fashion · 2/3 kids · 3/4 food · 1/1 square (cosmetics/electronics). Also 16/9|3/2|4/3|9/16.", img_object_fit: "cover|contain|fill|stretch.", image_border_radius: "Card image corner radius (px).", image_placement: "Image vs text layout (column|row).", gap_column: "Column gap (real range 8–40).", gap_row: "Row gap (real range 0–40).", product_info_padding_y: "Vertical padding of the name/price block (px).", price_padding_y: "Vertical padding around the price (px).", price_padding_x: "Horizontal padding around the price (px).", product_info_alignment: "left|center|right.", productNameFont: "Product-name font family.", productNameFontSize: "Product-name font size (px, real 12–18).", productNameFontWeight: "Product-name weight.", productNameColor: "Product-name colour.", productNameHover: "Product-name hover colour.", productNamePaddingY: "Vertical padding of the name (px).", productCategoryFontSize: "Category-label size (px).", productPriceFont: "Price font family.", productPriceFontSize: "Price font size (px).", productPriceFontWeight: "Price font weight — bold across ALL surveyed templates.", productPriceColor: "Price colour (often a theme accent / var).", productOriginalPriceFontSize: "Compare-at price font size (px).", productOriginalPriceColor: "Compare-at price colour (muted grey).", cardBackground: "Card background colour.", ribbonBackground: "Ribbon background.", ribbonFont: "Ribbon font.", ribbonPadding: "Ribbon padding.", ribbonGap: "Ribbon gap.", customRibbonStyle: "Per-ribbon style overrides {ribbonId:{…}}.", paginationColor: "Pagination dot colour.", activePaginationColor: "Active pagination colour.", sizeThumbnail: "{ width, height } of the card image." },
210
249
  events: "none",
211
250
  bindings: ["product::product_image", "product::product_name", "product::product_price"],
212
251
  notes: "Lists store products (self-renders image+name+price card). Requires a 'store' page / use_store. Config is INDUSTRY-DEPENDENT (surveyed fashion/kids/cosmetics/food/electronics): only bold price + responsive collapse + show original/discount are near-universal; pick image_ratio/columns/gaps/fonts/colours to match the brand.",
213
252
  },
214
- "slider-product": { specials: { displayStyle: "default|only_image|border.", original_price_position: "left|right." }, config: { slideItems: "Visible products.", slideWidth: "Card width.", image_ratio: "Aspect ratio." }, events: "none", bindings: ["product::product_image", "product::product_name", "product::product_price"] },
215
- "product-gallery": { specials: { showNavigation: "Show the thumbnail strip / nav (boolean — real templates often false).", on_hover: "Hover effect on the main image (e.g. zoom)." }, config: { sizeThumbnail: "{ width, height } of the main image (real 500–1044).", image_ratio: "Main image ratio (often 'auto')." }, events: "none", notes: "Product detail image gallery (product page). Mostly just sizeThumbnail + showNavigation." },
253
+ "slider-product": { specials: { displayStyle: "default|only_image|border.", original_price_position: "left|right|top.", category_id: "Source category id.", products_per_load: "Products to fetch.", autoplay: "on|off.", autoplay_time: "ms per slide.", autoplay_step: "Slides advanced per step.", on_hover: "zoom|swap.", show_navigation: "Show arrows (boolean).", show_nav_on_hover: "Reveal arrows on hover (boolean).", arrow_icon: "Arrow SVG.", show_rating: "Show rating (boolean).", show_ribbon: "Show ribbon (boolean).", show_product_name: "Show name (boolean).", show_product_price: "Show price (boolean).", show_button: "Show add-to-cart button (boolean).", action_add_to_cart: "Add-to-cart mode.", typeLinkCategory: "Related source (similar_product|…).", randomDisplay: "Randomise order (boolean)." }, config: { slideItems: "Visible products.", item_show: "Visible products (real 1–6, fractional ok e.g. 3.6).", slideWidth: "Card width.", image_ratio: "Aspect ratio (industry-dependent, see grid-product).", productNameFontSize: "Name size.", productPriceFontWeight: "Price weight (bold).", productPriceColor: "Price colour.", image_border_radius: "Card image radius.", sizeThumbnail: "{width,height} of card image." }, events: "none", bindings: ["product::product_image", "product::product_name", "product::product_price"], notes: "Horizontal product carousel (homepage 'featured/related'). Same card config family as grid-product + slider keys (item_show, autoplay, arrows)." },
254
+ "product-gallery": { specials: { showNavigation: "Show the thumbnail strip / nav (boolean — real templates often false).", show_ribbon: "Show a ribbon on the image (boolean).", zoom_type: "Hover zoom: none | lens | …", on_hover: "Hover effect on the main image (e.g. zoom).", arrowColor: "Nav arrow colour.", autoplay: "Auto-advance images (on|off).", autoplay_video: "Auto-play a product video (boolean).", type_split_video: "How a video splits from images (none|…).", infinity: "Loop images (boolean).", auto_resize_height: "Fit height to image (boolean)." }, config: { sizeThumbnail: "{ width, height } of the main image (real 500–1044).", image_ratio: "Main image ratio (often 'auto').", img_object_fit: "cover|contain.", showThumbnail: "Show the thumbnail strip (boolean).", thumbnailPosition: "left|bottom|…", thumbnailWidth: "Thumb width (px).", thumbnailHeight: "Thumb height (px).", scrollDirection: "Thumb scroll axis (horizontal|vertical).", arrowWidth: "Arrow width.", arrowHeight: "Arrow height.", max_show_items: "Max thumbnails shown." }, events: "none", notes: "Product detail image gallery (product page): main image + thumbnail strip (thumbnailPosition) + optional hover zoom (zoom_type)." },
216
255
  "product-image-carousel": { events: "none", notes: "Product image carousel (product page)." },
217
256
  "cart-items": { events: "none", bindings: ["cart_item::cart_item_image", "cart_item::cart_item_name", "cart_item::cart_item_price", "cart_item::cart_item_total_price", "cart_item::cart_item_prod_attr"], notes: "Cart line items (cart page). A repeater whose config.columns defines the per-row column template — real templates use [70px image, 1fr name, ~240px price, ~116px qty, ~240px total, 34px remove]." },
218
- "cart-icon": { specials: { svg: "Icon SVG (defaults to a bag icon; fill='currentColor' so it inherits style.color).", itemCount: "Badge count (0 placeholder)." }, config: { itemCountFontFamily: "Badge font family.", itemCountFontSize: "Badge font size (e.g. 11px).", itemCountColor: "Badge text colour.", itemCountBorderRadius: "Badge corner radius (50% = circle)." }, events: "click — opens cart. Real templates wrap it in a container with open_popup(mouseenter)/close_popup(mouseleave) for a mini-cart, plus a container with open_page→cart page." },
219
- "quantity-input": { specials: { spinner: "Native spinner style: hide-spin (product/cart, the default) | circle (checkout).", showRightBtn: "Show the +/- buttons (boolean)." }, config: { textColor: "Quantity text colour." }, events: "none", notes: "Quantity stepper (product/cart). Defaults to spinner:'hide-spin'." },
220
- attr: { specials: { attrName: "Attribute to show, or 'auto'.", autoAttrName: "Resolved attribute name when attrName='auto' (e.g. Màu sắc, Kích thước).", autoNamePriority: "Order when auto (1,2,3…).", attrPrimary: "This is the primary variation (boolean).", layout: "Swatch layout variant (1,4…).", defaultSelected: "Pre-select the first option (boolean)." }, config: { borderColorHover: "Swatch hover border colour." }, events: "none", notes: "Product attribute/variation selector (product page). Add one attr per variation axis (colour, size); attrName:'auto' lets it resolve from the product." },
221
- payment: { events: "none", notes: "Payment methods block (checkout)." },
257
+ "cart-icon": { specials: { svg: "Icon SVG (defaults to a bag icon; fill='currentColor' so it inherits style.color).", itemCount: "Badge count (0 placeholder)." }, config: { itemCountFontFamily: "Badge font family.", itemCountFont: "Badge font (theme var).", itemCountFontSize: "Badge font size (e.g. 11px).", itemCountFontWeight: "Badge font weight.", itemCountColor: "Badge text colour.", itemBackgroundColor: "Badge background colour.", itemCountBorderRadius: "Badge corner radius (50% = circle)." }, events: "click — opens cart. Real templates wrap it in a container with open_popup(mouseenter)/close_popup(mouseleave) for a mini-cart, plus a container with open_page→cart page." },
258
+ "quantity-input": { specials: { spinner: "Native spinner style: hide-spin (product/cart, the default) | circle (checkout).", showLeftBtn: "Show the minus button (boolean).", showRightBtn: "Show the plus button (boolean).", min: "Minimum quantity.", max: "Maximum quantity." }, config: { textColor: "Quantity text colour.", btnIncrease: "Plus-button style {borderColor,background,…}.", btnDecrease: "Minus-button style." }, events: "none", notes: "Quantity stepper (product/cart). Defaults to spinner:'hide-spin'." },
259
+ attr: { specials: { attrName: "Attribute to show, or 'auto'.", autoAttrName: "Resolved attribute name when attrName='auto' (e.g. Màu sắc, Kích thước).", autoNamePriority: "Order when auto (1,2,3…).", attrPrimary: "This is the primary variation (boolean).", layout: "Swatch layout variant (1,4…).", defaultSelected: "Pre-select the first option (boolean).", tooltip: "Show a tooltip on hover (boolean)." }, config: { borderColorHover: "Swatch hover border colour.", type: "Display: hover | regular.", selectedType: "Selected style (outer|…).", sizeImage: "Image swatch size (px).", tooltipBackground: "Tooltip background.", tooltipColor: "Tooltip text colour.", tooltipPosition: "top|bottom|…" }, events: "none", notes: "Product attribute/variation selector (product page). Add one attr per variation axis (colour, size); attrName:'auto' lets it resolve from the product." },
260
+ payment: { specials: { options: "Payment methods [{type,name,image,active}].", icon_color: "Method icon colour.", runNow: "Auto-run the selected method (boolean)." }, config: { activeColor: "Selected-method colour." }, events: "none", notes: "Payment methods block (checkout)." },
222
261
  "delivery-method": { specials: { options: "[{type: delivery|store_pickup, name, active}]." }, events: "none" },
223
262
  "order-items": { events: "none", bindings: ["order_item::product_image", "order_item::product_name", "order_item::product_quantity", "order_item::product_attrs", "order_item::items_sum_up_price"], notes: "Order summary lines (checkout/thank-you). Repeater whose config.columns is the per-row template — real templates use [75px image, 1fr name, 95px price]." },
224
- "order-history": { events: "none", notes: "Member order history (member page)." },
225
- coupon: { events: "none", notes: "Coupon / discount code input." },
263
+ "order-history": { specials: { items_per_load: "Orders per page.", type_view_more: "Load-more style.", editView: "Layout mode (data|…).", default_filter: "Default status filter ids.", countdown_time: "Auto-cancel countdown (hours).", cancel_text: "Cancel-order label.", order_id_text: "Order-code label.", payment_text: "Payment label.", total_price_text: "Total label.", item_attr_text: "Variation label.", is_scroll_to_top: "Scroll up on page change (boolean)." }, config: { show_variation: "Show item variations (boolean).", productNameClamp: "Clamp product name lines.", imageSize: "Thumbnail size (px).", priceColor: "Price colour." }, events: "none", notes: "Member order history (member page)." },
264
+ coupon: { specials: { field_name: "coupon.", label: FIELD.label, placeholder: FIELD.placeholder, show_label: FIELD.show_label, required: FIELD.required, title: "Heading text.", auto_fill_coupon: "Pre-fill an available coupon (boolean)." }, config: { ...FIELD_CONFIG }, events: "none", notes: "Coupon / discount-code input (cart/checkout). Place inside a form_discount." },
226
265
  "product-overlay": { specials: { type: "discount | custom." }, events: "none", notes: "Badge overlaid on a product card." },
227
266
  "product-review": {
228
267
  specials: {
@@ -244,32 +283,44 @@ export const ELEMENT_ATTRS = {
244
283
  },
245
284
  "masonry-review": { specials: { view_product_mode: "How linked products display in the review wall." }, events: "none", notes: "Masonry wall of reviews." },
246
285
  "flash-sale": { specials: { promotion_advance_id: "Promotion id (or 'none')." }, events: "none" },
247
- promotions: { events: "none", bindings: ["promotion_item::image", "promotion_item::name", "promotion_item::code", "promotion_item::description", "promotion_item::end_date"] },
248
- "promotions-short": { events: "none", bindings: ["promotion_item::name", "promotion_item::code"] },
249
- "grid-category": { config: { columns: "Categories per row." }, events: "none", notes: "Lists product categories. Create categories first (create_product_category)." },
250
- "slider-category": { config: { columns: "Visible categories." }, events: "none" },
286
+ promotions: { specials: { displayPromotions: "Which promotion kinds to list (e.g. ['coupon_id_multiple_times','discount_by_coupon'])." }, config: { direction: "horizontal | vertical.", promotionBorderColor: "Card border colour.", promotionDotColor: "Perforation dot colour." }, events: "none", bindings: ["promotion_item::image", "promotion_item::name", "promotion_item::code", "promotion_item::description", "promotion_item::end_date"], notes: "Lists active promotions/coupons (often with a copy-code button)." },
287
+ "promotions-short": { specials: { displayPromotions: "Promotion kinds to list." }, config: { direction: "horizontal | vertical.", promotionBorderColor: "Border colour.", promotionDotColor: "Dot colour." }, events: "none", bindings: ["promotion_item::name", "promotion_item::code"] },
288
+ "grid-category": { specials: { showCategories: "Categories to show [{id,...}] (else all).", isAutoCatchMenuLevelFirst: "Auto-pick top-level categories (boolean)." }, config: { columns: "Categories per row.", minimum_card_width: "Min card width (autofit, px).", sizeThumbnail: "{width,height} of the category image.", responsive: "fixed | autofit.", imageBorderRadius: "Image radius (px).", cardBackground: "Card background.", cardBorderRadius: "Card radius.", name_padding_y: "Padding around the name." }, events: "none", notes: "Lists product categories. Create categories first (create_product_category)." },
289
+ "slider-category": { specials: { showCategories: "Categories to show [{id,...}].", on_hover: "Card hover effect (zoom-in|…)." }, config: { columns: "Visible categories.", minimum_card_width: "Min card width (px).", sizeThumbnail: "{width,height} of the image.", imageBorderRadius: "Image radius.", navSize: "Arrow size (px).", arrowColor: "Arrow colour." }, events: "none" },
251
290
  "customer-address": { events: "none", bindings: ["customer_address::full_name", "customer_address::address", "customer_address::phone_number", "customer_address::pdc", "customer_address::is_default"], notes: "Member saved addresses." },
252
- wishlist: { specials: { svg: "Icon SVG.", itemCount: "Badge count." }, events: "click." },
291
+ wishlist: { specials: { svg: "Icon SVG.", itemCount: "Badge count." }, config: { itemCountColor: "Badge text colour.", itemBackgroundColor: "Badge background.", itemCountFontSize: "Badge size.", itemCountFontWeight: "Badge weight.", itemCountBorderRadius: "Badge radius.", itemCountFontFamily: "Badge font." }, events: "click — opens the wishlist." },
253
292
  "favorite-icon": { events: "click — toggle favourite." },
254
293
  "bonus-items": { specials: { typeBonus: "combo_product | other." }, events: "none" },
255
294
  // ── navigation ──
256
- 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)." }, 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 headers often have TWO menus (a horizontal desktop menu + a hamburger/collapse mobile menu). 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." },
295
+ 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))." },
296
+ "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}].)" },
297
+ "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." },
298
+ 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." },
299
+ "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)." },
300
+ "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." },
301
+ "member-dropdown": { specials: { dropdown_position: "left | right." }, events: "none", notes: "The account dropdown under a member-bar." },
302
+ 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." },
303
+ "dropdown-content": { events: "none", notes: "The panel a dropdown toggles." },
304
+ "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
305
  // ── blog ──
267
- "post-list": { events: "none", notes: "Lists blog articles. Create blog category + articles first." },
268
- "slider-post": { config: { slideItems: "Visible posts.", slideWidth: "Card width." }, events: "none" },
306
+ "post-list": { specials: { numbers_posts_to_show: "Total posts to show.", numbers_posts_to_show_per_page: "Posts per page.", posts_display_by: "Source mode (category/recent/…).", category_id: "Blog category id (or 'similar_posts').", readButtonText: "Read-more label.", on_hover: "Card hover effect (zoom).", tagTitle: "Title HTML tag (h3…).", tagDescription: "Description tag.", tagDatePublished: "Date tag.", tagCategory: "Category tag." }, config: { posts_per_row: "Posts per row.", image_ratio: "Card image ratio (3/2|1/1|…).", image_align: "Image position (top|left).", imageBorderRadius: "Image radius (px).", responsive: "fixed | custom.", spacing_between_posts: "Gap between cards (px).", showCategory: "Show category (0/1).", showPublishDate: "Show date (0/1).", showDescription: "Show excerpt (0/1).", showReadButton: "Show read-more (0/1).", descriptionFontSize: "Excerpt size.", descriptionColor: "Excerpt colour.", dateColor: "Date colour." }, events: "none", notes: "Lists blog articles (children are post-overlay cards). Create blog category + articles first." },
307
+ "slider-post": { specials: { numbers_posts_to_show: "Posts to show.", category_id: "Blog category id (or 'all_posts').", readButtonText: "Read-more label.", on_hover: "Hover effect (zoom)." }, config: { slideItems: "Visible posts.", slideWidth: "Card width.", posts_per_row: "Posts per view.", image_ratio: "Card image ratio.", responsive: "fixed | custom.", spacing_between_posts: "Gap (px).", layout: "Card layout variant." }, events: "none" },
269
308
  "grid-blog": { specials: { showBlogs: "Categories to show [{...}]." }, config: { columns: "Posts per row." }, events: "none" },
270
309
  "slider-blog": { specials: { showBlogs: "Categories to show." }, config: { columns: "Visible posts." }, events: "none" },
271
310
  "post-overlay": { specials: { type: "regular | custom." }, events: "none", notes: "Post card overlay (post page)." },
272
311
  "blog-overlay": { specials: { type: "regular | custom." }, events: "none" },
312
+ // ── filters / widgets / states (mined from 34 templates) ──
313
+ tags: { specials: { ...FILTER_SPECIALS, activeMode: "How active tags render.", selectionMode: "single | multiple.", auto_category_type: "Auto-source: product_category | …", auto_option: "Behaviour on click: filter | redirect.", max_child_categories: "Max child categories listed.", use_filter_or: "OR vs AND across selected tags (boolean).", keyword_suggestions: "Suggested search keywords [{key,value}]." }, config: { tags_layout: "Tag layout style." }, events: "click — filters the linked grid-product (or redirects).", notes: "Tag/attribute/keyword chips that FILTER a product grid (category page) — set filter_elements + sync_tab:'tag'; or auto_option:'redirect' to navigate." },
314
+ "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')." },
315
+ 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::*)." },
316
+ "user-point-log": { specials: { itemGap: "Row gap.", noteColor: "Note colour.", datetimeColor: "Date colour." }, events: "none", notes: "Loyalty reward-point history (member page)." },
317
+ "empty-product-layout": { events: "none", notes: "Empty-state shown inside a grid-product when no products match." },
318
+ "cart-items-empty": { events: "none", notes: "Empty-state shown inside cart-items when the cart is empty." },
319
+ "cart-droppable": { events: "none", notes: "Mini-cart drawer/panel (header). Open via open_cart / open_popup." },
320
+ "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)." },
321
+ "lesson-items": { events: "none", notes: "Lessons list inside a module (course app)." },
322
+ "next-lesson-droppable": { events: "none", notes: "Next-lesson panel (course app)." },
323
+ "list-lesson-droppable": { events: "none", notes: "Lessons droppable list (course app)." },
273
324
  };
274
325
  const describe = (type, key, where) => {
275
326
  const curated = ELEMENT_ATTRS[type] || {};
@@ -152,7 +152,12 @@ Repeaters fall into two kinds (surveyed across 34 templates):
152
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
153
  - \`promotions\` = \`text-dataset[promotion_item::code|description|end_date]\` + a copy button (\`copy_promotion\`).
154
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\`, 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).
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].
156
161
 
157
162
  ## Events (clicks, navigation, cart, popups)
158
163
  Interactive nodes (button, text, image, container, rectangle, icons) carry an \`events\`
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "v": "1.24.0",
4
+ "d": "26/06/2026",
5
+ "type": "Changed",
6
+ "en": "list_elements now documents tabIndex and text_animation_type as shared specials present on every element, covering tab-panel visibility binding and…",
7
+ "vi": "list_elements nay tài liệu hóa tabIndex và text_animation_type là các specials dùng chung có trên mọi phần tử, bao gồm gắn kết hiển thị/ẩn cho…"
8
+ },
9
+ {
10
+ "v": "1.23.0",
11
+ "d": "25/06/2026",
12
+ "type": "Added",
13
+ "en": "get_build_guide now includes a category-page filter recipe explaining how sidebar filter widgets (checkbox-group, color-group, tags,…",
14
+ "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,…"
15
+ },
2
16
  {
3
17
  "v": "1.22.0",
4
18
  "d": "25/06/2026",
@@ -26,19 +40,5 @@
26
40
  "type": "Fixed",
27
41
  "en": "create_site_from_template now auto-generates a URL-safe slug from the site name when none is supplied, fixing the 400 error returned by the backend…",
28
42
  "vi": "create_site_from_template nay tự động tạo slug an toàn cho URL từ tên site khi không có slug nào được cung cấp, khắc phục lỗi 400 mà backend trả về…"
29
- },
30
- {
31
- "v": "1.19.0",
32
- "d": "25/06/2026",
33
- "type": "Added",
34
- "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…",
35
- "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…"
36
- },
37
- {
38
- "v": "1.18.1",
39
- "d": "25/06/2026",
40
- "type": "Changed",
41
- "en": "get_build_guide now includes three new sections: \"Build the WHOLE storefront\" with per-page recipes for enriching category, product detail, cart,…",
42
- "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,…"
43
43
  }
44
44
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-storefront-mcp",
3
- "version": "1.22.0",
3
+ "version": "1.24.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",