Appearance
Conversation Block Styling Feasibility Report
Focused analysis of hardcoded styling in the conversation rendering layer. For broader redesign context, see
todos/ConversationRedesign/feasibility.mdandconversation-v2-plan.md.
1. Hardcoded Styling Inventory
1.1 CreativeConversationBlock.vue
The outer container is mostly configurable. Its styles() computed pulls sizing, border, box-shadow, background, alignment, and rotation from conversationProperties. The hardcoded parts:
| Property | Value | Configurable? |
|---|---|---|
display: flex (wrap) | flex | No -- structural |
position: absolute (wrap + main) | absolute | No -- structural |
pointerEvents: none (wrap) / auto (main) | none/auto | No -- functional |
overflow: hidden (main block) | hidden | No -- clips content |
overflowY: auto, overflow: hidden scroll (scroll viewport) | scroll | No -- enables scrolling |
scrollbarWidth: none + ::-webkit-scrollbar: display: none | hidden scrollbar | Cosmetic but intentional |
maxHeight: 100% (scroll viewport) | 100% | No -- constrains scroll |
flexDirection: column (scroll viewport) | column | No -- stacks messages |
maskImage (scroll viewport, when scrollOut = fadeOut) | gradient | Partially -- scrollOut toggle exists but gradient values are hardcoded |
direction: inherit (scroll content) | inherit | No -- structural |
boxSizing: border-box !important (scroll content) | border-box | No -- structural |
minHeight (scroll content) | Dynamic from DimensionManager | No -- functional (prevents choices from being cut off) |
Verdict: The container layer is well-structured. Almost everything visual comes from block properties. The scroll viewport has functional CSS that must stay.
1.2 MessageHolder.vue (the big one)
This is where most hardcoded styling lives. Every value below is baked into the styles() computed with no corresponding block property.
Message wrap:
| Property | Value | Category |
|---|---|---|
marginBottom | 0px | Cosmetic |
opacity | 0 (initial) | Functional -- entrance animation |
transform | translate3d(0, 10px, 0) (initial) | Functional -- entrance animation |
fontSize | 0px | Cosmetic (prevents whitespace between inline-blocks) |
marginLeft / marginRight | 15px | Cosmetic |
&:first-child paddingTop | 15px | Cosmetic |
&.not-first-in-block marginTop | 3px | Cosmetic |
&.not-first-in-block paddingLeft (when icon) | 36px | Cosmetic (but derived from avatar 30px + 6px margin) |
&.visible opacity | 1 | Functional -- entrance animation |
&.visible transform | translate3d(0, 0, 0) | Functional -- entrance animation |
&.inputneeded marginTop | 6px | Cosmetic |
&.inputentered marginBottom | 6px | Cosmetic |
&.blocks-before-rich marginTop | 10px !important | Cosmetic |
&.blocks-after-rich marginBottom | 10px !important | Cosmetic |
Avatar:
| Property | Value | Category |
|---|---|---|
width / height | 30px | Cosmetic |
marginRight | 6px | Cosmetic |
borderRadius | 50% | Cosmetic |
display | inline-block or none | Functional (show/hide) |
background | url(...) center center / cover no-repeat | Partially configurable (URL from senderIcon) |
Message bubble:
| Property | Value | Category |
|---|---|---|
padding | 8px 12px | Cosmetic |
borderRadius | 1px [configurable] [configurable] | Mixed -- top-left 1px is hardcoded for "tail" effect |
transition opacity | 200ms ease-out (when typing animation enabled) | Functional |
opacity (bubble) | 0 initially, 1 after typing delay | Functional |
userSelect | none | Functional |
verticalAlign | top | Cosmetic |
position | relative | Structural |
&.inputentered borderRadius | [configurable] [configurable] 1px -- top-right 1px for response tail | Mixed |
&.inputentered transition | none | Functional |
Alignment classes (choice/message middle/left/right):
| Property | Value | Category |
|---|---|---|
display: flex + justifyContent variations | center, flex-end, flex-start | Partially configurable via alignment property, but flex layout is hardcoded |
Summary: ~20 hardcoded cosmetic values, ~8 functional values, ~4 mixed.
1.3 Choice.vue
| Property | Value | Category |
|---|---|---|
float | right | Cosmetic (legacy, should be flex) |
padding | 6px 12px | Cosmetic |
marginBottom / marginLeft / marginRight | 6px each | Cosmetic |
outline / background / border | none | Reset (then overridden by choiceProperties) |
fontWeight | normal | Cosmetic (overridden by choiceProperties font) |
borderRadius | from choiceProperties or 15px fallback | Configurable (with hardcoded fallback) |
borderTopRightRadius | from choiceProperties or 1px fallback | Mixed -- 1px tail effect is hardcoded |
transition | all 300ms ease 0s, width 0s | Cosmetic |
position | relative | Structural |
pointerEvents | all | Functional |
cursor | pointer | Functional |
&:hover transform | scale3d(1.05, 1.05, 1) | Cosmetic |
&:hover filter | brightness(1.1) saturate(1.05) | Cosmetic |
&:active transform | scale3d(1.03, 1.03, 1) | Cosmetic |
&:active filter | brightness(1.08) saturate(1.05) | Cosmetic |
whiteSpace | pre-line | Semi-functional (preserves line breaks) |
wordBreak | break-word | Semi-functional (prevents overflow) |
@keyframes buttonAppearing | opacity 0->1, top 20px->0 | Functional (entrance) |
animationDelay | index * 120 + 1ms | Cosmetic (stagger timing) |
animationDuration | 300ms | Cosmetic |
Container flexDirection | row or column based on fit | Configurable via fit property |
Summary: ~14 cosmetic values, ~4 functional, ~3 semi-functional.
1.4 Atoms (CloseButton, Tagline, ExpandableIcon, RemoveButton)
These are peripheral to the conversation flow itself. Their styling is mostly configurable via their respective block properties (closeProperties, taglineProperties, iconProperties, removeProperties). Hardcoded values are mainly structural positioning (position: absolute, transform: translateY(-50%)) and the X-icon diagonal rendering. Not relevant for a conversation CSS reset.
1.5 CreativeBody.vue
Container styling. Hardcoded: position: absolute, transition: all 0.3s, borderRadius: 0 0 5px 5px, backgroundSize: cover. The 5px border-radius is cosmetic but applies to the entire creative body, not conversation-specific. Not a conversation reset concern.
2. Functional vs Cosmetic Classification
Functional (must keep)
These properties are required for scroll behavior, flow navigation, entrance animations, or user interaction. Removing them breaks the conversation.
| System | Properties | Why |
|---|---|---|
| Scroll container | overflow: hidden scroll, maxHeight: 100%, flexDirection: column | Enables vertical scrolling, constrains height |
| Auto-scroll | scrollTop manipulation via requestAnimationFrame | Smooth scroll-to-bottom after new messages |
| Entrance animation | opacity: 0 -> 1, transform: translate3d(0, 10px, 0) -> (0,0,0) | New message appearance, triggers scroll timing |
| Typing delay | transition: opacity 200ms on bubble | Synchronized with conversationFlow.ts timing |
| Button stagger | @keyframes buttonAppearing, animationDelay: index * 120ms | Sequential button entrance |
| Fade-out mask | maskImage: linear-gradient(...) tied to scrollTop | Scroll position indicator |
| Scroll content minHeight | Dynamic from DimensionManager | Prevents choices from being clipped at bottom |
| Pointer events | none on wrap, auto on interactive elements | Click-through to background |
| Input switching | v-show / class toggles on inputEntered | Choice-to-response transition |
Cosmetic (safe to make configurable)
These properties control appearance only. Changing them affects how things look but not how they behave.
| Element | Properties | Current values |
|---|---|---|
| Message wrap margins | marginLeft, marginRight | 15px |
| Message wrap first-child padding | paddingTop | 15px |
| Inter-message gap | marginTop (not-first-in-block) | 3px |
| Input-needed gap | marginTop (inputneeded) | 6px |
| Input-entered gap | marginBottom (inputentered) | 6px |
| Avatar size | width, height | 30px |
| Avatar margin | marginRight | 6px |
| Avatar shape | borderRadius | 50% |
| Bubble padding | padding | 8px 12px |
| Bubble border-radius | borderRadius | 15px (with 1px tail) |
| Choice button padding | padding | 6px 12px |
| Choice button gap | margin | 6px |
| Choice hover/active effects | transform, filter | scale + brightness |
| Choice stagger timing | animationDelay, animationDuration | 120ms * index, 300ms |
| Choice entrance distance | top in keyframe | 20px |
| Hidden scrollbar | scrollbarWidth: none | none |
Gray area
| Property | Issue |
|---|---|
whiteSpace: pre-line | Preserves line breaks in message text. Removing could collapse multi-line messages. |
wordBreak: break-word | Prevents long words from overflowing bubbles. Removing could break layout. |
fontSize: 0px on message-wrap | Prevents whitespace between inline-block children. Removing adds unwanted gaps. |
Avatar paddingLeft offset (36px) | Derived from avatar size (30) + margin (6). If avatar size becomes configurable, this must recalculate. |
Bubble 1px border-radius (tail) | Design choice baked in. Some users want uniform radius, others want the tail. |
3. CSS Reset Feasibility
3.1 How existing resets work
The engine has a three-layer reset system:
- globalReset.ts -- always applied. Resets common HTML elements, sets baseline box-sizing, font smoothing.
- bannerReset.ts -- applied when
creativeType === BANNER. Heavy reset of ~65 HTML elements. - expandableReset.ts -- applied when
creativeType === EXPANDABLE. ResetsmaxWidth: noneon all elements.
Resets are injected as <style> elements via StyleComposer.applyStyleObject(), tracked by identifier (e.g., data-banner-reset). They apply BEFORE component styles, so component styles() computed values override them.
3.2 A conversationReset approach
A CSS reset for conversation elements would follow the same pattern:
ts
// src/style-engine/conversationReset.ts
export const conversationReset = {
// Reset message wrapper cosmetic styling
'.message-wrap': {
margin: '0',
padding: '0',
fontSize: 'inherit',
},
// Reset bubble cosmetic styling
'.message-bubble': {
padding: '0',
borderRadius: '0',
background: 'none',
border: 'none',
boxShadow: 'none',
},
// Reset avatar cosmetic styling
'.message-avatar': {
width: 'auto',
height: 'auto',
margin: '0',
borderRadius: '0',
},
// Reset choice cosmetic styling
'.choices a, .choices button': {
padding: '0',
margin: '0',
borderRadius: '0',
background: 'none',
float: 'none',
},
}Application: Via StyleEngine.init() gated on a resetConversationStyles flag on conversationProperties, or via a cleanSlate toggle (as described in the existing V2 plan).
3.3 Challenges
Class name obfuscation. The engine uses
obfuscateClassNames()which transforms class names in production builds. A reset targeting.message-wrapwon't work if the actual class ismw-a3f. The reset must use the same obfuscation pipeline, or target via attribute selectors ([data-step]) or parent-child combinators.Specificity wars. Component
styles()computed values are injected per-instance with unique class selectors (e.g.,.message-wrap-${uniq}). A global reset has lower specificity and would be overridden by the component styles. The reset would need to be applied AFTER component styles, or use!important, or the component must conditionally skip its cosmetic styles when reset is active.No clean separation in the current code. MessageHolder's
styles()mixes functional and cosmetic properties in the same selector. You can't simply "turn off" the cosmetic ones without modifying the component. A reset would add competing styles rather than removing them.The 1px tail. The asymmetric
borderRadius: 1px 15px 15pxon message bubbles is deeply embedded in the bubble styling logic. A reset that zeros border-radius would remove this, but there's no way to make just the tail optional without touching the component.
3.4 Verdict: CSS Reset
Feasible but fragile. A conversation reset is technically possible using the existing StyleEngine pattern, but it would fight against the component's own styles() output rather than cleanly replacing it. The result would be two competing style layers -- the component's defaults and the reset's overrides -- creating maintenance burden and specificity issues.
Better approach: Instead of a CSS reset that fights the component, modify the component to conditionally skip cosmetic defaults. This is the cleanSlate toggle from the V2 plan -- a boolean that makes the styles() computed emit only functional CSS, leaving everything else to builder configuration.
4. V2 Feasibility -- Lightweight Conversation Component
4.1 What already exists
The todos/ConversationRedesign/conversation-v2-plan.md is a complete 15-task implementation plan for a V2 rendering path. It proposes:
- MessageHolderV2.vue -- clean DOM with
data-*attributes, all hardcoded values extracted to named constants, CSS custom properties for override - ChoiceV2.vue -- flex/grid layout (replacing float), configurable stagger, variant/outcome data attributes, feedback delay
- FlowGroup.vue --
display: contentswrapper for grouping conversation steps - autoScrollV2.ts -- configurable scroll duration/easing
conversationVersion: 1 | 2flag for backward compatibility- cleanSlate toggle for minimal default styling
- CSS custom properties layer (
--conv-message-margin-x,--conv-bubble-radius, etc.)
4.2 Scope of refactoring
| What changes | Effort | Risk |
|---|---|---|
| New MessageHolderV2.vue (~350 lines) | Medium | Low -- isolated new file |
| New ChoiceV2.vue (~250 lines) | Medium | Low -- isolated new file |
| New FlowGroup.vue (~30 lines) | Low | Low -- simple wrapper |
| New autoScrollV2.ts (~60 lines) | Low | Low -- isolated utility |
| Modify CreativeConversationBlock template | Low | Low -- conditional branch, V1 untouched |
| Extract constants to constants.ts | Low | None -- additive |
| Add types to payload-v2/index.ts | Low | None -- additive |
| Frontend: types, defaults, config sections | Medium | Low -- additive |
| Total new code | ~700 lines engine + ~200 lines frontend |
4.3 What does NOT change
conversationFlow.ts(flow processor) -- reused as-isflowModifier.ts(adds steps to liveFlow) -- reused as-isFlowComponentInterface-- extended but backward-compatibleDataStore.liveFlow-- same reactive array- All existing creatives -- stay on V1 until explicitly switched
4.4 Biggest obstacles
Pixel-perfect parity. V2 must render identically to V1 with default settings. Any visual difference in existing creatives would be a regression. The 20+ hardcoded values must be carefully matched in
CONVERSATION_DEFAULTS.Custom CSS compatibility. Users with custom CSS targeting V1 class names (
.message-wrap,.message-bubble,.choices a) will break if they switch to V2. The V2 DOM uses different selectors (data attributes, unique class names). Migration needs clear documentation.Timing coordination. The conversation flow has a tight timing chain: typing delay -> bubble opacity transition -> scroll-to-bottom. MessageHolderV2 must replicate this chain exactly or messages will appear before/after scrolling, creating jarring UX.
Fill-container mode. MessageHolder uses
<Teleport>to move components out of the scroll container whensingleBlockFillsContaineris active. This is tricky to replicate because it affects both DOM structure and event handling.Per-button blockStyles. Choice buttons can have per-button style overrides from flow operators (ChangeText, ChangeImage). ChoiceV2 must apply these via the same
getFlatOpBlockStyles()mechanism.
4.5 Verdict: V2
Feasible and well-planned. The existing plan is thorough and architecturally sound. The conversationVersion flag provides clean backward compatibility. The main risk is subtle timing/layout regressions that would only surface in complex flow configurations.
Recommended approach:
- Build V2 components
- Test against a diverse set of existing creatives (banner, expandable, different speeds, with/without icons, with custom CSS)
- Ship behind the version toggle -- users opt in per creative
- Once confident, make V2 the default for new creatives
5. Summary
| Question | Answer |
|---|---|
| How much is hardcoded? | ~35 cosmetic values across MessageHolder + Choice. Container and atoms are mostly configurable. |
| What can be removed safely? | All cosmetic values (margins, padding, border-radius, colors, hover effects, stagger timing). ~20 values in MessageHolder, ~14 in Choice. |
| What must stay? | Scroll overflow, entrance opacity/transform, typing delay transition, pointer events, min-height, mask-image, flex-direction. ~12 functional values. |
| CSS reset feasibility? | Technically possible but fragile due to specificity conflicts and class obfuscation. A cleanSlate toggle in the component is better. |
| V2 feasibility? | High. Plan exists. ~900 lines of new code across 4 new files. Backward-compatible via version flag. Main risk: timing regressions. |
Key Source Files
| File | Repo | Hardcoded values |
|---|---|---|
src/components/conversationflow/MessageHolder.vue | CE | 20+ cosmetic, 8 functional |
src/components/blocks/basic/Choice.vue | CE | 14 cosmetic, 4 functional |
src/components/creative/CreativeConversationBlock/CreativeConversationBlock.vue | CE | 3 cosmetic, 9 functional |
src/components/conversationflow/CreativeBody.vue | CE | 3 cosmetic, 4 functional |
src/utils/autoScroll.ts | CE | 1 (SCROLL_DURATION) |
src/style-engine/dimensionmanager/banner.ts | CE | Dynamic minHeight calc |
Related Documents
todos/ConversationRedesign/feasibility.md-- broader problem analysis, approach comparisontodos/ConversationRedesign/conversation-v2-plan.md-- 15-task implementation plantodos/ConversationRedesign/flow-group-visual-companion.html-- interactive flow-group concept