superwall-paywall-editor

Use this skill whenever the user wants to modify, update, or create paywall designs in the Superwall editor via Chrome automation. Triggers include: changing paywall text, layout, colors, fonts, positioning, product cards, buttons, badges, or any visual element. Also use when the user asks to add dynamic pricing, conditional styles, click behaviors, or save a paywall. Requires the Superwall editor to be open in Chrome and Chrome MCP tools to be available. Do NOT use for Superwall dashboard analytics, A/B test configuration, or anything unrelated to the visual paywall editor.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "superwall-paywall-editor" with this command: npx skills add r4z33n4l1/superwall-paywall-editor-skill/r4z33n4l1-superwall-paywall-editor-skill-superwall-paywall-editor

Superwall Paywall Editor via Chrome Automation

Architecture

The Superwall editor is built on tldraw with a reactive store. All UI elements are typed records in window.app.store. Modify paywalls by reading/writing records via JavaScript executed in the browser.

Key Browser Objects

ObjectPurpose
window.app.storeReactive record store — READ/WRITE all records
window.editorEditor instance — getSnapshotToSave, undo/redo
window.app.trpctRPC client — server mutations

Core Store Operations

const store = window.app.store;
store.get('node:someId')        // Read a single record
store.allRecords()              // Read all records
store.put([record1, record2])   // Create or update (batch)
store.remove(['node:someId'])   // Delete records

Workflow

Step 1 — Get Tab Context

mcp__claude-in-chrome__tabs_context_mcp

Step 2 — Screenshot

Capture the current state before making any changes:

mcp__claude-in-chrome__computer → action: screenshot

Step 3 — Explore the Store

// List all UI element nodes
const store = window.app.store;
const nodes = store.allRecords().filter(r => r.typeName === 'node');
JSON.stringify(nodes.map(n => ({
  id: n.id, name: n.name, type: n.type,
  parentId: n.parentId, index: n.index
})), null, 2)
// Find children of a specific node
store.allRecords().filter(r => r.typeName === 'node' && r.parentId === 'node:TARGET_ID');

// Inspect a node's available properties
const node = store.get('node:TARGET_ID');
JSON.stringify({ properties: Object.keys(node.properties), defaultProperties: Object.keys(node.defaultProperties) }, null, 2)

Step 4 — Make Changes via store.put()

Always spread the existing record and override only what you need:

const store = window.app.store;
const node = store.get('node:TARGET_ID');
store.put([{
  ...node,
  properties: {
    ...node.properties,
    'css:backgroundColor': { type: 'literal', value: { type: 'css-color', value: '#000000ff' } }
  }
}]);

Step 5 — Verify

mcp__claude-in-chrome__computer → action: zoom, region: [x0, y0, x1, y1]

Step 6 — Save (when requested)

Click the Save button in the editor UI, or POST to:

/api/trpc/paywalls.prepareSnapshotForPromotion?batch=1

Node Types Reference

TypePurposeKey Properties
stackLayout container (flexbox)prop:stack
textText elementprop:text
imgImage elementprop:image
iconIcon elementprop:icon

Property Value Format

Every property follows this pattern:

'css:{propertyName}': {
  type: 'literal',        // or 'conditional' or 'referential'
  value: {
    type: 'css-length',   // see CSS Value Types below
    value: '16',
    unit: 'px'
  }
}

CSS Value Types

TypeStructureExample
css-length{ value, unit: 'px'|'%'|'vh' }{ type: 'css-length', value: '16', unit: 'px' }
css-string{ value: 'string' }{ type: 'css-string', value: 'absolute' }
css-color{ value: '#RRGGBBaa' }{ type: 'css-color', value: '#000000ff' }
css-transform-translate{ x: { type, value, unit } }See centering recipe
css-font{ value, weight, style, variant, kind, url }See Font section

Compound CSS Property Keys

css:paddingTop;paddingBottom
css:paddingLeft;paddingRight
css:marginLeft;marginRight
css:borderTopLeftRadius;borderTopRightRadius;borderBottomRightRadius;borderBottomLeftRadius

Stack Property (Layout)

'prop:stack': {
  type: 'literal',
  value: {
    type: 'property-stack',
    axis: 'x' | 'y',
    reverse: false,
    crossAxisAlignment: 'center' | 'start' | 'end' | 'stretch',
    mainAxisDistribution: 'center' | 'start' | 'end' | 'space-between',
    wrap: 'nowrap' | 'wrap',
    gap: '12px',
    scroll: 'none',
    snapPosition: 'center'
  }
}

Text Property

// Static
'prop:text': {
  type: 'literal',
  value: { type: 'property-text', value: 'Hello World', rendering: { type: 'literal' } }
}

// Dynamic (Liquid)
'prop:text': {
  type: 'literal',
  value: {
    type: 'property-text',
    value: '{{ products.primary.price }}',
    rendering: { type: 'liquid', requiredStateIds: ['state:products.primary.price'] }
  }
}

Custom CSS Property

'prop:custom-css': {
  type: 'literal',
  value: {
    type: 'property-custom-css',
    properties: [
      { type: 'custom-css-property', id: 'unique-id-1', property: 'whiteSpace', value: 'nowrap' },
      { type: 'custom-css-property', id: 'unique-id-2', property: 'background', value: 'linear-gradient(...)' }
    ]
  }
}

Conditional Properties (State-Dependent)

Used for selected product highlighting, etc.:

'prop:custom-css': {
  type: 'conditional',
  options: [
    {
      query: {
        combinator: 'and', id: 'query-id',
        rules: [{
          id: 'rule-id',
          field: 'state:products.selectedIndex',
          operator: '=',
          valueSource: 'value',
          value: { type: 'variable-number', value: 1 }
        }]
      },
      value: {
        type: 'literal',
        value: {
          type: 'property-custom-css',
          properties: [
            { type: 'custom-css-property', id: 'id1', property: 'background', value: 'linear-gradient(white, white) padding-box, linear-gradient(135deg, #7B61FF, #FF6B9D) border-box' },
            { type: 'custom-css-property', id: 'id2', property: 'borderColor', value: 'transparent' }
          ]
        }
      }
    },
    {
      query: { combinator: 'and', rules: [], id: 'default-id' },
      value: { type: 'literal', value: { type: 'property-custom-css', properties: [] } }
    }
  ]
}

Font Property

The css-font type requires ALL fields — omitting any causes ValidationError:

'css:font': {
  type: 'literal',
  value: {
    type: 'css-font',
    value: 'Instrument Sans',
    weight: '600',              // REQUIRED
    style: 'normal',            // REQUIRED — 'normal' or 'italic'
    variant: 'SemiBold',
    kind: 'google',
    url: 'https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@600&display=swap'
  }
}

Check loaded fonts:

const resources = store.allRecords().filter(r => r.typeName === 'resource');
const fonts = new Set();
resources.forEach(r => { const parts = r.id.split(';'); if (parts.length > 1) fonts.add(parts[1]); });
Array.from(fonts).sort()

Common Recipes

Create a Text Node

Text nodes must have props.text:

store.put([{
  id: 'node:my-unique-id',
  typeName: 'node',
  type: 'text',
  name: 'My Text',
  parentId: 'node:PARENT_ID',
  index: 'a5',
  x: 0, y: 0, rotation: 0,
  isLocked: false, opacity: 1,
  clickBehavior: { type: 'do-nothing' },
  meta: {}, requiredRecordIds: [],
  props: { text: { type: 'literal', text: '' } },  // REQUIRED
  properties: {
    'prop:text': { type: 'literal', value: { type: 'property-text', value: 'Hello', rendering: { type: 'literal' } } },
    'css:fontSize': { type: 'literal', value: { type: 'css-length', value: '16', unit: 'px' } },
    'css:color': { type: 'literal', value: { type: 'css-color', value: '#000000ff' } },
    'css:textAlign': { type: 'literal', value: { type: 'css-string', value: 'center' } },
    'css:fontWeight': { type: 'literal', value: { type: 'css-string', value: '600' } }
  },
  defaultProperties: {
    'prop:text': { type: 'literal', value: { type: 'property-text', value: 'Hello', rendering: { type: 'literal' } } }
  }
}]);

Create a Stack (Container) Node

store.put([{
  id: 'node:my-stack-id',
  typeName: 'node',
  type: 'stack',
  name: 'My Container',
  parentId: 'node:PARENT_ID',
  index: 'a5',
  x: 0, y: 0, rotation: 0,
  isLocked: false, opacity: 1,
  clickBehavior: { type: 'do-nothing' },
  meta: {}, requiredRecordIds: [],
  props: {},
  properties: {
    'css:width': { type: 'literal', value: { type: 'css-length', value: '100', unit: '%' } },
    'css:height': { type: 'literal', value: { type: 'css-string', value: 'auto' } }
  },
  defaultProperties: {
    'prop:stack': {
      type: 'literal',
      value: {
        type: 'property-stack',
        axis: 'y', reverse: false,
        crossAxisAlignment: 'center', mainAxisDistribution: 'center',
        wrap: 'nowrap', gap: '8px', scroll: 'none', snapPosition: 'center'
      }
    }
  }
}]);

Center an Absolutely Positioned Element

const node = store.get('node:TARGET_ID');
store.put([{
  ...node,
  properties: {
    ...node.properties,
    'css:position': { type: 'literal', value: { type: 'css-string', value: 'absolute' } },
    'css:left': { type: 'literal', value: { type: 'css-length', value: '50', unit: '%' } },
    'css:transform[translate]': {
      type: 'literal',
      value: { type: 'css-transform-translate', x: { type: 'css-length', value: '-50', unit: '%' } }
    }
  }
}]);

Center a Non-Absolute Element

// Option A: margin auto
'css:marginLeft;marginRight': { type: 'literal', value: { type: 'css-string', value: 'auto' } }

// Option B: parent stack crossAxisAlignment
'prop:stack': { type: 'literal', value: { type: 'property-stack', axis: 'y', crossAxisAlignment: 'center', ... } }

Equal-Width Cards in a Row

// Container: fixed width, centered
store.put([{ ...container, properties: { ...container.properties,
  'css:width': { type: 'literal', value: { type: 'css-length', value: '320', unit: 'px' } },
  'css:marginLeft;marginRight': { type: 'literal', value: { type: 'css-string', value: 'auto' } }
}}]);

// Each card: same fixed dimensions
store.put([
  { ...card1, properties: { ...card1.properties,
    'css:width': { type: 'literal', value: { type: 'css-length', value: '154', unit: 'px' } },
    'css:height': { type: 'literal', value: { type: 'css-length', value: '110', unit: 'px' } }
  }},
  { ...card2, properties: { ...card2.properties,
    'css:width': { type: 'literal', value: { type: 'css-length', value: '154', unit: 'px' } },
    'css:height': { type: 'literal', value: { type: 'css-length', value: '110', unit: 'px' } }
  }}
]);

Fix Badge/Overlay Clipping

Set overflow: visible on all ancestor containers and a high z-index on the badge:

store.put([
  { ...badge, properties: { ...badge.properties,
    'css:zIndex': { type: 'literal', value: { type: 'css-string', value: '10' } }
  }},
  { ...parentCard, properties: { ...parentCard.properties,
    'css:overflow': { type: 'literal', value: { type: 'css-string', value: 'visible' } }
  }},
  { ...grandparentContainer, properties: { ...grandparentContainer.properties,
    'css:overflow': { type: 'literal', value: { type: 'css-string', value: 'visible' } }
  }}
]);

Mixed Fonts in One Line (Inline Text Splitting)

HTML does not render in text nodes — <span> shows as raw text. For mixed fonts, convert the text node to a wrapping stack and add inline text children:

// 1. Convert text node to horizontal wrapping stack
store.put([{
  ...existingTextNode,
  type: 'stack',
  props: {},
  properties: {
    'css:width': { type: 'literal', value: { type: 'css-length', value: '350', unit: 'px' } },
    'css:textAlign': { type: 'literal', value: { type: 'css-string', value: 'center' } }
  },
  defaultProperties: {
    'prop:stack': {
      type: 'literal',
      value: {
        type: 'property-stack',
        axis: 'x', reverse: false,
        crossAxisAlignment: 'center', mainAxisDistribution: 'center',
        wrap: 'wrap', gap: '0px', scroll: 'none', snapPosition: 'center'
      }
    }
  }
}]);

// 2. Add inline text children with different fonts
store.put([
  makeTextNode('id1', 'Save $500 on your ', sansFont, 'a1'),
  makeTextNode('id2', 'curated fits', serifItalicFont, 'a2'),
  makeTextNode('id3', ' → deal expires soon', sansFont, 'a3')
]);

Wrap an Icon in a Styled Container

Put visual styling (bg, border-radius, size) on the wrapper stack; keep the icon node minimal:

// Wrapper stack
store.put([{
  ...wrapperStack,
  properties: {
    'css:width': { type: 'literal', value: { type: 'css-length', value: '44', unit: 'px' } },
    'css:height': { type: 'literal', value: { type: 'css-length', value: '44', unit: 'px' } },
    'css:backgroundColor': { type: 'literal', value: { type: 'css-color', value: '#ffffffff' } },
    'css:borderTopLeftRadius;borderTopRightRadius;borderBottomRightRadius;borderBottomLeftRadius': {
      type: 'literal', value: { type: 'css-length', value: '12', unit: 'px' }
    },
    'css:zIndex': { type: 'literal', value: { type: 'css-string', value: '20' } }
  },
  defaultProperties: {
    'prop:stack': {
      type: 'literal',
      value: { type: 'property-stack', axis: 'y', reverse: false,
        crossAxisAlignment: 'center', mainAxisDistribution: 'center',
        wrap: 'nowrap', gap: '0px', scroll: 'none', snapPosition: 'center' }
    }
  }
}]);

// Icon: just color and size
store.put([{
  ...icon,
  properties: {
    'prop:icon': icon.properties['prop:icon'],
    'css:color': { type: 'literal', value: { type: 'css-color', value: '#000000ff' } },
    'css:width': { type: 'literal', value: { type: 'css-length', value: '20', unit: 'px' } },
    'css:height': { type: 'literal', value: { type: 'css-length', value: '20', unit: 'px' } }
  }
}]);

Liquid Math for Dynamic Calculations

// Calculate % savings from actual product prices
value: '{% assign monthly_annual = products.primary.rawPrice | times: 12 %}{% assign savings_pct = monthly_annual | minus: products.secondary.rawPrice | times: 100 | divided_by: monthly_annual | round: 0 %}{{ savings_pct }}% OFF',
rendering: {
  type: 'liquid',
  requiredStateIds: ['state:products.primary.rawPrice', 'state:products.secondary.rawPrice']
}

Key filters: times, minus, divided_by, round, plus, abs, upcase, downcase. Use rawPrice (number) for math, price (formatted string) for display.

Click Behaviors

{ type: 'purchase', productId: 'paywall_product:primary' }  // or 'secondary'
{ type: 'restore' }
{ type: 'close' }
{ type: 'do-nothing' }

Create a Dynamic State Variable

store.put([{
  id: 'state:params.my_variable',
  typeName: 'state',
  locked: false,
  derivation: null,
  nonRemovable: false,
  defaultValue: { type: 'variable-number', value: 75 }
}]);

Reference in Liquid: {{ params.my_variable }}% OFF

Delete a Node

store.remove(['node:TARGET_ID']);

Available Product State Variables

VariableState ID
products.primary.pricestate:products.primary.price
products.primary.monthlyPricestate:products.primary.monthlyPrice
products.primary.periodstate:products.primary.period
products.primary.rawPricestate:products.primary.rawPrice
products.secondary.pricestate:products.secondary.price
products.secondary.monthlyPricestate:products.secondary.monthlyPrice
products.secondary.rawPricestate:products.secondary.rawPrice
products.selectedIndexstate:products.selectedIndex
products.hasIntroductoryOfferstate:products.hasIntroductoryOffer
Custom paramsstate:params.{name} — create with store.put

Known Pitfalls

  1. Text nodes require props.text — omitting it throws ValidationError: At node.props.text: Expected an object, got undefined.
  2. css-font requires all fieldsweight and style are mandatory. Omitting either throws ValidationError.
  3. Transform type is css-transform-translate — not css-translate. Each axis needs its own { type: 'css-length', value, unit }.
  4. Custom CSS properties must be an arrayproperty-custom-css requires properties: [...].
  5. Always spread existing records{ ...existingNode, properties: { ...existingNode.properties, ... } } to avoid losing existing props.
  6. Save triggers a page reload — add a beforeunload handler first if you need to intercept.
  7. Fractional indexing — node order uses indices like a0, a1, a2, Zx. Insert between existing values.
  8. Fixed heights clip content — use overflow: hidden intentionally or avoid fixed heights on variable-content containers.
  9. Alignment with unequal card children — add a transparent spacer text node (color: #00000000, marginTop: auto) to shorter cards.
  10. HTML doesn't render in text nodes<span>, <b>, etc. display as raw text. Use inline wrapping stacks for mixed fonts.
  11. Overflow clips badges on state change — set overflow: visible on all ancestor nodes, not just the immediate parent.
  12. Converting node types — you can change type (e.g., textstack) via store.put(). Update props accordingly: stacks use props: {}, text nodes use props: { text: ... }.

Chrome MCP Output Blocking

The Chrome extension blocks large JSON outputs containing certain patterns (cookies, URLs). Workarounds:

  • Query specific fields instead of dumping entire records
  • Use string concatenation for small outputs: `${node.id} | ${node.name}`
  • Split queries into multiple smaller calls
  • Avoid JSON.stringify on full records — extract only the fields you need

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

github-tools

Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.

Archived SourceRecently Updated
Coding

openclaw-version-monitor

监控 OpenClaw GitHub 版本更新,获取最新版本发布说明,翻译成中文, 并推送到 Telegram 和 Feishu。用于:(1) 定时检查版本更新 (2) 推送版本更新通知 (3) 生成中文版发布说明

Archived SourceRecently Updated
Coding

ask-claude

Delegate a task to Claude Code CLI and immediately report the result back in chat. Supports persistent sessions with full context memory. Safe execution: no data exfiltration, no external calls, file operations confined to workspace. Use when the user asks to run Claude, delegate a coding task, continue a previous Claude session, or any task benefiting from Claude Code's tools (file editing, code analysis, bash, etc.).

Archived SourceRecently Updated
Coding

ai-dating

This skill enables dating and matchmaking workflows. Use it when a user asks to make friends, find a partner, run matchmaking, or provide dating preferences/profile updates. The skill should execute `dating-cli` commands to complete profile setup, task creation/update, match checking, contact reveal, and review.

Archived SourceRecently Updated