You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A designer building a search page in Plasmic Studio drops in EP Search Box, opens the style panel, and sets border, padding, font, and background. Their styles never reach the live site.
PRD #305 fixed the surface symptom — we no longer ship inline appearance defaults that would have clobbered designer styles. But the underlying Plasmic platform constraint blocks the fix from working: when a code-component instance lives in a Plasmic page, codegen filters its className-bound styles down to a small allow-list (TPL_COMPONENT_PROPS: position, top/left/etc., width/height, margin, align-self, flex-*, opacity, transform, transition). Padding, border, border-radius, background, color, font-family, font-size, line-height — all stored in the model, all stripped before reaching the rendered class. We confirmed this with the MCP returning a "stored but will not render" warning, and with browser DOM inspection of the live site after a styling attempt: the input renders with browser-default chrome (2px inset border, no radius, 13.3px font) regardless of what the designer sets.
Three official Plasmic packages handle this constraint in different ways:
@plasmicpkgs/antd5 and @plasmicpkgs/radix-ui set styleSections: ["visibility"] and ship defaultStyles — they admit "the component owns appearance, designer doesn't override from the canvas." The component looks fine because the underlying CSS framework (antd / radix) carries the styling.
@plasmicpkgs/react-aria sets defaultStyles but leaves styleSections open — the panel shows knobs that are silently inert. The package relies on users knowing react-aria's CSS-vars system from elsewhere.
@plasmicpkgs/radix-ui Dialog and @plasmicpkgs/react-aria TextField also use a third pattern — the component is split into a provider that wraps the InstantSearch/state machinery, and the visible chrome is composed from separately registered sub-components or slot children. The designer drops Plasmic-controlled tags (TplTags) inside; those tags escape the strip because they're not code-component instances.
EP catalog-search has no underlying CSS framework to inherit from. We're building a behaviour-only headless layer over react-instantsearch. Option (a) and (b) above leave us with a search input that looks like a 2003 form; option (c) is the only path that gives designers full control of the chrome from the Plasmic canvas.
Solution
Refactor EPSearchBox from a code component that renders a search input into a provider that exposes search-field state. The visible chrome — the <input>, the clear <button> — is no longer rendered by EP. The designer drops a Plasmic-controlled <input> and <button> into the EPSearchBox slot and wires them via the existing Plasmic interaction system (the same one EPRefinementList already uses for toggleRefinement).
Concretely after the refactor:
EPSearchBox renders no DOM. It calls useSearchBox() from react-instantsearch, manages debounce locally, and exposes:
searchFieldData via DataProvider, with { value, displayValue, isEmpty }
setValue(value: string) and clear() as refActions
The component's children slot ships a sensible default — a Plasmic <input> and a Plasmic <button>, pre-templated so a fresh drop-in still looks like a search box. Designers replace, restyle, or rearrange freely. Because these elements are TplTags (not code-component instances), Plasmic's TPL_COMPONENT_PROPS strip does not apply to them — every appearance style designers set in the panel reaches the rendered DOM.
The wiring instructions are documented in the README and inline in the component description: bind input value to $ctx.searchFieldData.value, wire input onChange interaction to the setValue ref-action, wire clear-button onClick to the clear ref-action, set clear-button visibility to !$ctx.searchFieldData.isEmpty.
The placeholder, autoFocus, showClear props are removed from EPSearchBox. The designer sets these on their own Plasmic input and button — that's the whole point of the slot pattern.
The debounceMs prop is preserved on EPSearchBox itself (it's behaviour, not appearance).
The registered name (plasmic-commerce-ep-search-box) is preserved. Existing instances in Plasmic projects re-render against the new shape — they will lose their input chrome and need re-authoring. Acceptable because no production project currently uses this component.
User Stories
As a designer, I want to drop EP Search Box onto a page and see a working search input that I can style end-to-end from the Plasmic style panel.
As a designer, I want to set border, border-radius, padding, font-family, font-size, background, and color on the search input and have those settings applied on the live site.
As a designer, I want to set the same appearance properties on the clear button without having to write CSS.
As a designer, I want to control placeholder text, autofocus, input type, and ARIA attributes on the input element directly via Plasmic's HTML attribute panel.
As a designer, I want to bind the input's value and onChange to the EPSearchBox's exposed state and action via Plasmic's interaction panel.
As a designer, when the input is empty I want the clear button hidden, and when there's a query I want it shown — wired to a Plasmic visibility binding.
As a designer, I want the EPSearchBox slot's default content to demonstrate the wiring so that I have a working example to copy or modify.
As a designer, I want the EPSearchBox debounce behaviour to keep working when I substitute my own input element — debounce belongs to the provider, not the input.
As a designer composing search UI for the first time, I want the EPSearchBox component description in Plasmic to tell me how to wire up the input and clear button.
As a developer maintaining this package, I want EPSearchBox's API surface to match the existing EP catalog-search composition pattern (refinement list, hierarchical menu, range filter — all exposing data + ref-actions for the designer to wire).
As a developer, I want the component's tests to verify the data exposure and ref-action behaviour rather than DOM shape — because there is no DOM shape any more.
As a downstream consumer, when I update to the new EPSearchBox I want the changelog to tell me my existing instances need re-authoring and link to the new wiring docs.
As an MCP user re-authoring an existing EPSearchBox instance, I want the existing slot's children to be ergonomic to populate via the MCP — no exotic dynamic-expression patterns that don't round-trip cleanly.
Implementation Decisions
EPSearchBox becomes a render-children-only component. It calls useSearchBox() and an internal useDebounce, builds searchFieldData, and renders <DataProvider name="searchFieldData" data={...}>{children}</DataProvider> plus nothing else.
The provider runs in editor and runtime alike; no editor mock branch. The mock data path lives entirely in searchFieldData — when in editor, searchFieldData.value is "leather" and searchFieldData.isEmpty is false. There is no separate JSX tree for editor vs runtime.
searchFieldData.value is the user's in-flight input. searchFieldData.displayValue is the query that has actually been refined. They diverge during the debounce window. Most designers will bind value (the controlled-input fed back into the input element); some will bind displayValue (e.g. for "you searched for: " labels).
searchFieldData.isEmpty is value.length === 0. Pre-computed so designers don't have to write JS expressions to hide a clear button.
The component is forwardRef'd so useImperativeHandle exposes setValue(v: string) and clear() as refActions. Argument signatures match the rest of the EP catalog-search package — setValue(value: string); clear() with no args.
The slot default content is a Plasmic vbox containing a Plasmic <input> (placeholder set to "Search products...") and a Plasmic <button> (text "×"). The default does NOT set up the value/onChange/visibility bindings — those bindings cannot be expressed in the registration's defaultValue PlasmicElement schema cleanly, and shipping a half-wired default would be more confusing than no default. The component description and README explain how to wire.
placeholder, autoFocus, showClear props are deleted. debounceMs is preserved (defaults to 300ms). The previewState prop is preserved for forced editor states (auto / withData).
The exported EPSearchBox symbol from the package keeps its name and registration. The epSearchBoxMeta updates: description is rewritten to explain the slot pattern; props shrinks to debounceMs + previewState + children slot; refActions adds setValue and clear; providesData: true is added.
Documentation lives in: (a) the package README's "Styling contract" section, augmented with a "Slot composition" subsection; (b) the component's description field, since that's what designers see in Plasmic Studio.
Testing Decisions
The behavioural tests for EPSearchBox (currently three editor-render tests) are replaced with provider-shape tests:
searchFieldData is provided with {value, displayValue, isEmpty} matching the mock shape in editor mode
In runtime mode, searchFieldData.value reflects local state and displayValue reflects the refined query
setValue(v) updates local state immediately and schedules a debounced refine(v) after debounceMs
clear() resets local state and calls clear() on the useSearchBox result
previewState: "withData" works in non-editor contexts (force mock)
The headless-styling-contract test for EPSearchBox is deleted (no leaf to assert against). The other eight contract tests are unchanged.
The existing 1522 jest tests in plasmicpkgs/commerce-providers/elastic-path continue to pass.
A new test verifies that epSearchBoxMeta.refActions.setValue and .clear are both defined, with the documented argument types.
A new test verifies that epSearchBoxMeta.providesData === true.
Browser-level verification (Playwright MCP, post-merge): drop a Plasmic input + button into the EPSearchBox slot in a test project, set border/radius/padding/font on both, publish, verify the live DOM shows the designer's styles.
Out of Scope
The other eight catalog-search components. They already follow the provider+slot pattern (EPRefinementList, EPHierarchicalMenu, EPRangeFilter, EPSearchSortBy, EPSearchPagination, EPSearchStats, EPCatalogSearchProvider) or are correctly inline-styled (EPSearchHits's grid layout). No changes needed.
Audit of cart, product detail, and other EP commerce code components for the same TPL_COMPONENT_PROPS issue. Filed separately as a follow-up tracking issue.
A backwards-compatibility "EPSearchBoxLegacy" component preserving the old DOM rendering. Justified by the user confirming no production project uses EPSearchBox today.
Custom CSS-vars or theming surface (--ep-search-input-border, etc.). The slot pattern obviates these; designers style the input directly.
Auto-wiring magic (e.g. React.cloneElement walking the slot to inject value/onChange). Brittle when Plasmic-controlled inputs render through Plasmic's own React component shells; explicit $ctx binding via interactions is the project's existing convention.
Storybook / visual regression tooling. The behavioural and DOM tests are sufficient for the contract.
Further Notes
Three decisions taken without explicit confirmation. Reviewers can override before implementation:
Drop placeholder/autoFocus/showClear from EPSearchBox entirely. Alternative: keep them as fallback hints applied to the slot's default input. Argument for dropping: once the designer replaces the slot content (the expected workflow), these props become silently inert — same lying-preview problem PRD: EP catalog-search components must honour designer styling (headless styling contract) #305 solved.
Keep the registered name plasmic-commerce-ep-search-box. Alternative: register a new component plasmic-commerce-ep-search-field and deprecate the old one. Argument for keeping: zero production usage, no migration cost; a renamed component would mean one more code component in the picker for no benefit.
No bindings in the slot's defaultValue. Alternative: encode the $ctx.searchFieldData.value binding via attrs.value: "{{$ctx.searchFieldData.value}}" if Plasmic's PlasmicElement schema supports it. We chose to leave bindings unwired in the default because we have not verified the round-trip behaviour and a half-wired default that needs the designer to clear-then-rewire is worse than an unwired one.
Open questions worth a comment before implementation:
Should setValue skip the debounce entirely (firing refine synchronously), or share the same debounce that the input's onChange does? My default: skip the debounce. Programmatic callers (e.g. an "I'm Feeling Lucky" button calling setValue('shoes')) usually want the search to happen now.
Should displayValue and value ever differ in editor mode? My default: no — both equal "leather". The debounce is invisible at design time.
Should the slot's default include working bindings if the PlasmicElement schema supports attrs expressions? Worth a 10-minute spike before settling.
packages/host/src/registerComponent.ts:226-231 — styleSections jsdoc and StyleSection enum.
packages/plasmic-mcp/src/edit-tools.ts:2370-2390 — TPL_COMPONENT_PROPS allow-list with the rationale comment.
plasmicpkgs/radix-ui/src/dialog.tsx — Plasmic's slot composition pattern (triggerSlot via wrapFragmentInDiv).
plasmicpkgs/react-aria/src/registerTextField.tsx — Plasmic's provider+sub-component composition pattern (TextField provides InputContext; Input reads it via useContextProps).
plasmicpkgs/commerce-providers/elastic-path/src/catalog-search/EPRefinementList.tsx — existing EP catalog-search component using the provider+slot pattern; the closest in-house template for this refactor.
Problem Statement
A designer building a search page in Plasmic Studio drops in
EP Search Box, opens the style panel, and sets border, padding, font, and background. Their styles never reach the live site.PRD #305 fixed the surface symptom — we no longer ship inline appearance defaults that would have clobbered designer styles. But the underlying Plasmic platform constraint blocks the fix from working: when a code-component instance lives in a Plasmic page, codegen filters its className-bound styles down to a small allow-list (
TPL_COMPONENT_PROPS:position,top/left/etc.,width/height,margin,align-self,flex-*,opacity,transform,transition). Padding, border, border-radius, background, color, font-family, font-size, line-height — all stored in the model, all stripped before reaching the rendered class. We confirmed this with the MCP returning a "stored but will not render" warning, and with browser DOM inspection of the live site after a styling attempt: the input renders with browser-default chrome (2px insetborder, no radius, 13.3px font) regardless of what the designer sets.Three official Plasmic packages handle this constraint in different ways:
@plasmicpkgs/antd5and@plasmicpkgs/radix-uisetstyleSections: ["visibility"]and shipdefaultStyles— they admit "the component owns appearance, designer doesn't override from the canvas." The component looks fine because the underlying CSS framework (antd / radix) carries the styling.@plasmicpkgs/react-ariasetsdefaultStylesbut leavesstyleSectionsopen — the panel shows knobs that are silently inert. The package relies on users knowing react-aria's CSS-vars system from elsewhere.@plasmicpkgs/radix-uiDialog and@plasmicpkgs/react-ariaTextField also use a third pattern — the component is split into a provider that wraps the InstantSearch/state machinery, and the visible chrome is composed from separately registered sub-components or slot children. The designer drops Plasmic-controlled tags (TplTags) inside; those tags escape the strip because they're not code-component instances.EP catalog-search has no underlying CSS framework to inherit from. We're building a behaviour-only headless layer over react-instantsearch. Option (a) and (b) above leave us with a search input that looks like a 2003 form; option (c) is the only path that gives designers full control of the chrome from the Plasmic canvas.
Solution
Refactor
EPSearchBoxfrom a code component that renders a search input into a provider that exposes search-field state. The visible chrome — the<input>, the clear<button>— is no longer rendered by EP. The designer drops a Plasmic-controlled<input>and<button>into the EPSearchBox slot and wires them via the existing Plasmic interaction system (the same one EPRefinementList already uses fortoggleRefinement).Concretely after the refactor:
EPSearchBoxrenders no DOM. It callsuseSearchBox()from react-instantsearch, manages debounce locally, and exposes:searchFieldDatavia DataProvider, with{ value, displayValue, isEmpty }setValue(value: string)andclear()asrefActionschildrenslot ships a sensible default — a Plasmic<input>and a Plasmic<button>, pre-templated so a fresh drop-in still looks like a search box. Designers replace, restyle, or rearrange freely. Because these elements are TplTags (not code-component instances), Plasmic's TPL_COMPONENT_PROPS strip does not apply to them — every appearance style designers set in the panel reaches the rendered DOM.valueto$ctx.searchFieldData.value, wire inputonChangeinteraction to thesetValueref-action, wire clear-buttononClickto theclearref-action, set clear-button visibility to!$ctx.searchFieldData.isEmpty.placeholder,autoFocus,showClearprops are removed from EPSearchBox. The designer sets these on their own Plasmic input and button — that's the whole point of the slot pattern.debounceMsprop is preserved on EPSearchBox itself (it's behaviour, not appearance).plasmic-commerce-ep-search-box) is preserved. Existing instances in Plasmic projects re-render against the new shape — they will lose their input chrome and need re-authoring. Acceptable because no production project currently uses this component.User Stories
Implementation Decisions
EPSearchBoxbecomes a render-children-only component. It callsuseSearchBox()and an internaluseDebounce, buildssearchFieldData, and renders<DataProvider name="searchFieldData" data={...}>{children}</DataProvider>plus nothing else.searchFieldData— when in editor,searchFieldData.valueis"leather"andsearchFieldData.isEmptyisfalse. There is no separate JSX tree for editor vs runtime.searchFieldData.valueis the user's in-flight input.searchFieldData.displayValueis the query that has actually been refined. They diverge during the debounce window. Most designers will bindvalue(the controlled-input fed back into the input element); some will binddisplayValue(e.g. for "you searched for: " labels).searchFieldData.isEmptyisvalue.length === 0. Pre-computed so designers don't have to write JS expressions to hide a clear button.forwardRef'd souseImperativeHandleexposessetValue(v: string)andclear()as refActions. Argument signatures match the rest of the EP catalog-search package —setValue(value: string);clear()with no args.vboxcontaining a Plasmic<input>(placeholder set to "Search products...") and a Plasmic<button>(text "×"). The default does NOT set up the value/onChange/visibility bindings — those bindings cannot be expressed in the registration'sdefaultValuePlasmicElement schema cleanly, and shipping a half-wired default would be more confusing than no default. The component description and README explain how to wire.placeholder,autoFocus,showClearprops are deleted.debounceMsis preserved (defaults to 300ms). ThepreviewStateprop is preserved for forced editor states (auto/withData).EPSearchBoxsymbol from the package keeps its name and registration. TheepSearchBoxMetaupdates:descriptionis rewritten to explain the slot pattern;propsshrinks todebounceMs+previewState+childrenslot;refActionsaddssetValueandclear;providesData: trueis added.headless-styling.tsmodule from PRD: EP catalog-search components must honour designer styling (headless styling contract) #305 keeps its[data-ep-search-box]selector but the rule is now structurally meaningless (the wrapper no longer exists). The:where()rule fordata-ep-search-boxis removed. Thedata-ep-catalog-search-providerrule stays.descriptionfield, since that's what designers see in Plasmic Studio.Testing Decisions
searchFieldDatais provided with{value, displayValue, isEmpty}matching the mock shape in editor modesearchFieldData.valuereflects local state anddisplayValuereflects the refined querysetValue(v)updates local state immediately and schedules a debouncedrefine(v)afterdebounceMsclear()resets local state and callsclear()on theuseSearchBoxresultpreviewState: "withData"works in non-editor contexts (force mock)plasmicpkgs/commerce-providers/elastic-pathcontinue to pass.epSearchBoxMeta.refActions.setValueand.clearare both defined, with the documented argument types.epSearchBoxMeta.providesData === true.Out of Scope
--ep-search-input-border, etc.). The slot pattern obviates these; designers style the input directly.React.cloneElementwalking the slot to inject value/onChange). Brittle when Plasmic-controlled inputs render through Plasmic's own React component shells; explicit$ctxbinding via interactions is the project's existing convention.Further Notes
Three decisions taken without explicit confirmation. Reviewers can override before implementation:
placeholder/autoFocus/showClearfrom EPSearchBox entirely. Alternative: keep them as fallback hints applied to the slot's default input. Argument for dropping: once the designer replaces the slot content (the expected workflow), these props become silently inert — same lying-preview problem PRD: EP catalog-search components must honour designer styling (headless styling contract) #305 solved.plasmic-commerce-ep-search-box. Alternative: register a new componentplasmic-commerce-ep-search-fieldand deprecate the old one. Argument for keeping: zero production usage, no migration cost; a renamed component would mean one more code component in the picker for no benefit.defaultValue. Alternative: encode the$ctx.searchFieldData.valuebinding viaattrs.value: "{{$ctx.searchFieldData.value}}"if Plasmic's PlasmicElement schema supports it. We chose to leave bindings unwired in the default because we have not verified the round-trip behaviour and a half-wired default that needs the designer to clear-then-rewire is worse than an unwired one.Open questions worth a comment before implementation:
setValueskip the debounce entirely (firingrefinesynchronously), or share the same debounce that the input's onChange does? My default: skip the debounce. Programmatic callers (e.g. an "I'm Feeling Lucky" button calling setValue('shoes')) usually want the search to happen now.displayValueandvalueever differ in editor mode? My default: no — both equal"leather". The debounce is invisible at design time.attrsexpressions? Worth a 10-minute spike before settling.References
packages/host/src/registerComponent.ts:226-231—styleSectionsjsdoc andStyleSectionenum.packages/plasmic-mcp/src/edit-tools.ts:2370-2390—TPL_COMPONENT_PROPSallow-list with the rationale comment.plasmicpkgs/radix-ui/src/dialog.tsx— Plasmic's slot composition pattern (triggerSlot viawrapFragmentInDiv).plasmicpkgs/react-aria/src/registerTextField.tsx— Plasmic's provider+sub-component composition pattern (TextField provides InputContext; Input reads it viauseContextProps).plasmicpkgs/commerce-providers/elastic-path/src/catalog-search/EPRefinementList.tsx— existing EP catalog-search component using the provider+slot pattern; the closest in-house template for this refactor.