Chapter 21 · Operator · Brand the widget

Designer toolkit. The brand controls.

The operator-side surface for branding the chat widget before deploying it. Five components — color field, swatch picker, style section, style editor, embed snippet — compose into the kit’s “brand my widget” workflow. The widget itself lives in chapter 17; this chapter is how operators design it.

21.1 ColorField

The kit’s standard “pick a hex colour” field. Pairs a native <input type="color"> swatch with a hex text field so users can type, paste, or pick. Optional presets row beneath gives quick access to the brand palette. Used dozens of times across the <WidgetStyleEditor> form pane and any other “design your X” surface.

ColorField

.color-field

Three side-by-side fields demonstrating the common shapes — default empty (black swatch, placeholder hex), filled with a brand colour and a presets row, and a captioned variant labelled for an icon foreground. The hex input is mono, tabular-numeric, and uppercased; the leading swatch is a 32×32 cell that opens the native picker.

default · black
Used for the launcher disc.
filled · with presets
Launcher + chat header.
paired · icon foreground
Foreground glyph on the launcher.
<!-- .color-field wraps the label row, the swatch + hex input row,    -->
<!-- and the optional .color-field-presets quick-pick group. The      -->
<!-- swatch <label> carries a real <input type="color"> underneath.   -->
<div class="color-field">
  <div class="color-field-head">
    <div class="color-field-head-text">
      <label class="color-field-label">Widget background</label>
      <span class="color-field-caption">Launcher + chat header.</span>
    </div>
  </div>
  <div class="color-field-row">
    <label class="color-field-swatch" aria-label="Pick a colour">
      <input type="color" class="color-field-swatch-input" value="#C69C6D" aria-hidden="true" tabindex="-1">
      <span class="color-field-swatch-preview" style="background: #C69C6D;"></span>
    </label>
    <input type="text" class="color-field-input" value="#C69C6D" spellcheck="false">
  </div>
  <div class="color-field-presets" role="group" aria-label="Preset colours">
    <button class="color-field-preset is-on" style="background: #C69C6D;"></button>
    <!-- …more presets… -->
  </div>
</div>
.color-field { display: flex; flex-direction: column; gap: 6px; }

.color-field.is-disabled { opacity: 0.55; pointer-events: none; }

.color-field-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
}

.color-field-head-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.color-field-label { font: 500 13px/1.3 var(--f-body); color: var(--fg); }

.color-field-caption { font: 400 12px/1.4 var(--f-body); color: var(--fg-soft); }

.color-field-meta { display: inline-flex; align-items: center; gap: 4px; }

.color-field-row {
  display: grid;
  grid-template-columns: 32px 1fr;
  gap: 6px;
  align-items: center;
}

.color-field-swatch {
  position: relative;
  width: 32px;
  height: 32px;
  border-radius: var(--r-sm);
  border: 1px solid var(--hair);
  overflow: hidden;
  cursor: pointer;
  display: block;
}

.color-field-swatch-input {
  position: absolute;
  inset: 0;
  opacity: 0;
  width: 100%;
  height: 100%;
  cursor: pointer;
}

.color-field-swatch-preview {
  position: absolute;
  inset: 0;
}

.color-field-input {
  height: 32px;
  padding: 0 var(--s-2);
  border: 1px solid var(--hair);
  border-radius: var(--r-sm);
  background: var(--bg-paper);
  color: var(--fg);
  font: 400 12.5px/1 var(--f-mono);
  font-variant-numeric: tabular-nums;
  text-transform: uppercase;
}

.color-field-input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.color-field-input[aria-invalid="true"] {
  border-color: var(--error-text, #C0392B);
}

.color-field-presets {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.color-field-preset {
  appearance: none;
  width: 18px;
  height: 18px;
  border-radius: 999px;
  border: 1.5px solid var(--hair);
  cursor: pointer;
  transition: transform var(--dur-2) var(--ease);
}

.color-field-preset:hover { transform: scale(1.1); }

.color-field-preset.is-on {
  border-color: var(--ink);
  box-shadow: 0 0 0 2px var(--bg-paper) inset;
}

@media (prefers-reduced-motion: reduce) {
  .color-field-preset { transition: none; }
  .color-field-preset:hover { transform: none; }
}
import { useState } from 'react';
import { ColorField } from '@magicblocksai/ui';

function Example() {
  const [color, setColor] = useState('#C69C6D');
  return (
    <ColorField
      label="Widget background"
      caption="Launcher + chat header."
      value={color}
      onValueChange={setColor}
      presets={['#C69C6D', '#18181B', '#FFFFFF', '#EC4899', '#2563EB']}
    />
  );
}

// Uncontrolled: defaultValue + onValueChange, kit handles state.
<ColorField
  label="Icon colour"
  defaultValue="#FFFFFF"
  onValueChange={(hex) => console.log(hex)}
/>

21.2 ColorSwatchPicker

The named-preset row beneath the ColorField. A horizontal grid of labelled discs — one per scheme — with the selected swatch carrying a ring + scale. The kit ships WIDGET_SCHEMES (Green / Yellow / Pink / Blue / Navy / Orange / Black / White) as a sensible default starting palette. Operators pick a scheme to flood the rest of the form with sensible colours, then fine-tune individual fields through the <ColorField> rows below.

ColorSwatchPicker

.color-swatch-picker

The full eight-disc WIDGET_SCHEMES grid with the “Orange” swatch selected. Hover lifts the disc on the background; the active state rings the disc with the ink colour and scales it up slightly. Two-tone diagonal-split discs are also supported (see the second variant) for custom paired colour schemes.

WIDGET_SCHEMES · orange selected
two-tone · diagonal split
<!-- .color-swatch-picker is a flex-wrap row of .color-swatch buttons. -->
<!-- Each swatch is a column of .color-swatch-disc + .color-swatch-    -->
<!-- label. The is-on modifier rings + scales the active disc. Two-    -->
<!-- tone discs use a 135deg linear-gradient between two colours.      -->
<div class="color-swatch-picker" role="radiogroup">
  <button class="color-swatch is-on" role="radio" aria-checked="true" title="Orange">
    <span class="color-swatch-disc" style="background: #EA580C;"></span>
    <span class="color-swatch-label">Orange</span>
  </button>
  <!-- …more swatches… -->
</div>

<!-- Two-tone swatch (accent = second colour):                         -->
<button class="color-swatch" role="radio">
  <span class="color-swatch-disc"
        style="background: linear-gradient(135deg, #C69C6D 50%, #FFE4C4 50%);"></span>
  <span class="color-swatch-label">Warm Glow</span>
</button>
.color-swatch-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.color-swatch-picker.is-disabled { opacity: 0.55; pointer-events: none; }

.color-swatch {
  appearance: none;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  background: transparent;
  border: 0;
  padding: 4px;
  cursor: pointer;
  border-radius: var(--r-sm);
  transition: background var(--dur-2) var(--ease);
}

.color-swatch:hover { background: var(--bg-warm); }

.color-swatch:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.color-swatch.is-disabled { opacity: 0.5; cursor: not-allowed; }

.color-swatch-disc {
  width: 32px;
  height: 32px;
  border-radius: 999px;
  border: 1.5px solid var(--hair);
  display: block;
  position: relative;
  transition: transform var(--dur-2) var(--ease);
}

.color-swatch.is-on .color-swatch-disc {
  border-color: var(--ink);
  transform: scale(1.05);
  box-shadow: 0 0 0 2px var(--bg-paper) inset;
}

@media (prefers-reduced-motion: reduce) {
  .color-swatch-disc { transition: none; }
  .color-swatch.is-on .color-swatch-disc { transform: none; }
}

.color-swatch-label {
  font: 500 11px/1 var(--f-body);
  color: var(--fg);
}
import { useState } from 'react';
import { ColorSwatchPicker, WIDGET_SCHEMES } from '@magicblocksai/ui';
import type { ColorSwatch } from '@magicblocksai/ui';

function Example() {
  const [scheme, setScheme] = useState<string | null>('orange');
  return (
    <ColorSwatchPicker
      swatches={WIDGET_SCHEMES}
      value={scheme}
      onValueChange={setScheme}
    />
  );
}

// Custom two-tone schemes: pass accent for the diagonal-split disc.
const twoTone: ColorSwatch[] = [
  { id: 'warm-glow',  label: 'Warm Glow',  color: '#C69C6D', accent: '#FFE4C4' },
  { id: 'deep-ocean', label: 'Deep Ocean', color: '#0F172A', accent: '#2563EB' },
];

<ColorSwatchPicker swatches={twoTone} defaultValue="warm-glow" />

21.3 WidgetStyleSection

A single section of the editor’s form pane. Carries a title, an optional caption, and a vertical stack of field children — typically <ColorField> rows. Provides consistent heading typography and divider spacing without forcing a layout; consumers slot in any field combination. Sections are stacked top-to-bottom inside the <WidgetStyleEditor>’s form pane, one per logical parameter family (Widget styling, Chat messages, Send style, Fonts, Buttons).

WidgetStyleSection

.widget-style-section

Two side-by-side variants — a collapsed section with just the header and a meta chip (the rest of the body hidden), and an expanded section showing two <ColorField> rows. The hairline divider runs along the bottom edge so sections stack cleanly in the form pane above and below.

collapsed · header only

Chat messages

Bubble colour, text colour, and spacing.

expanded · two fields

Header

The bar across the top of the chat shell.

<!-- .widget-style-section wraps a .widget-style-section-head (title + -->
<!-- caption + optional meta slot) and a .widget-style-section-body    -->
<!-- (the field stack). The hairline divider runs along the bottom     -->
<!-- edge; the last section in a stack drops its divider.              -->
<section class="widget-style-section">
  <header class="widget-style-section-head">
    <div class="widget-style-section-head-text">
      <h3 class="widget-style-section-title">Header</h3>
      <p class="widget-style-section-caption">
        The bar across the top of the chat shell.
      </p>
    </div>
  </header>
  <div class="widget-style-section-body">
    <!-- field children: typically <div class="color-field">… rows -->
  </div>
</section>
.btn-ghost {
  background: transparent;
  color: var(--fg);
  border-color: transparent;
}

.btn-ghost:hover {
  background: var(--bg-sunk);
  border-color: var(--hair);
}

.unsaved-changes-bar .btn-ghost { color: var(--paper); }

.unsaved-changes-bar .btn-ghost:hover { background: color-mix(in oklab, var(--paper) 12%, transparent); }

.color-field { display: flex; flex-direction: column; gap: 6px; }

.color-field.is-disabled { opacity: 0.55; pointer-events: none; }

.color-field-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
}

.color-field-head-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.color-field-label { font: 500 13px/1.3 var(--f-body); color: var(--fg); }

.color-field-caption { font: 400 12px/1.4 var(--f-body); color: var(--fg-soft); }

.color-field-meta { display: inline-flex; align-items: center; gap: 4px; }

.color-field-row {
  display: grid;
  grid-template-columns: 32px 1fr;
  gap: 6px;
  align-items: center;
}

.color-field-swatch {
  position: relative;
  width: 32px;
  height: 32px;
  border-radius: var(--r-sm);
  border: 1px solid var(--hair);
  overflow: hidden;
  cursor: pointer;
  display: block;
}

.color-field-swatch-input {
  position: absolute;
  inset: 0;
  opacity: 0;
  width: 100%;
  height: 100%;
  cursor: pointer;
}

.color-field-swatch-preview {
  position: absolute;
  inset: 0;
}

.color-field-input {
  height: 32px;
  padding: 0 var(--s-2);
  border: 1px solid var(--hair);
  border-radius: var(--r-sm);
  background: var(--bg-paper);
  color: var(--fg);
  font: 400 12.5px/1 var(--f-mono);
  font-variant-numeric: tabular-nums;
  text-transform: uppercase;
}

.color-field-input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.color-field-input[aria-invalid="true"] {
  border-color: var(--error-text, #C0392B);
}

.color-field-presets {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.color-field-preset {
  appearance: none;
  width: 18px;
  height: 18px;
  border-radius: 999px;
  border: 1.5px solid var(--hair);
  cursor: pointer;
  transition: transform var(--dur-2) var(--ease);
}

.color-field-preset:hover { transform: scale(1.1); }

.color-field-preset.is-on {
  border-color: var(--ink);
  box-shadow: 0 0 0 2px var(--bg-paper) inset;
}

/* …additional rules trimmed for brevity — see _shared.css */
import {
  WidgetStyleSection,
  ColorField,
} from '@magicblocksai/ui';

<WidgetStyleSection
  title="Header"
  caption="The bar across the top of the chat shell."
>
  <ColorField label="Header background" defaultValue="#C69C6D" />
  <ColorField label="Header text"       defaultValue="#FCFCFC" />
</WidgetStyleSection>

// With meta slot — any ReactNode (count badge, status pill, action button).
<WidgetStyleSection
  title="Chat messages"
  caption="Bubble colour, text colour, and spacing."
  meta={<button aria-label="Expand">+</button>}
>
  {/* …fields */}
</WidgetStyleSection>

21.4 WidgetStyleEditor

The flagship designer surface. A split-pane shell composing the form pane on the left and a live-preview <WidgetShell> on the right. The form pane stacks <WidgetStyleSection> blocks; the preview pane reflects every keystroke through <WidgetThemeProvider>. The header slot carries the title and save controls; the optional sidebar slot adds a section TOC. Consumer owns the form fields and the preview content — the editor is chrome only, so reorganisation, validation, autosave, and version history live outside the kit.

WidgetStyleEditor

.widget-style-editor

The full split-pane shell — header strip with the appearance title and a Save control, form pane on the left stacking three <WidgetStyleSection> blocks (Header, Launcher, Composer) with their <ColorField> children, and a preview pane on the right rendering a themed <WidgetShell> in flow. Every form change drives the preview without prop-drilling, via the parent <WidgetThemeProvider>’s scoped CSS custom properties.

Charlie’s Wines · Chat Appearance

Header

The bar across the top of the chat shell.

Launcher

The floating bubble visitors see first.

Composer

The send box at the bottom.

Live preview
CW
Charlie’s Wines Online
You’re chatting with an AI assistant.
CW
Hi! Looking for a wine recommendation?
<!-- .widget-style-editor is a column of header + body. The body is a   -->
<!-- two-column grid (form-pane + preview-pane) when no sidebar is set, -->
<!-- three columns when .has-sidebar is added. The form pane stacks     -->
<!-- .widget-style-section blocks; the preview pane is a stage.         -->
<div class="widget-style-editor">
  <div class="widget-style-editor-header">
    <h2>Charlie’s Wines · Chat Appearance</h2>
    <button class="btn btn-primary btn-sm">Save</button>
  </div>
  <div class="widget-style-editor-body">
    <div class="widget-style-editor-form" role="form">
      <section class="widget-style-section">…</section>
      <!-- …more sections… -->
    </div>
    <div class="widget-style-editor-preview">
      <div class="widget-style-editor-preview-head">
        <span class="widget-style-editor-preview-label">Live preview</span>
      </div>
      <div class="widget-style-editor-preview-stage">
        <!-- <WidgetThemeProvider> + <WidgetShell> (or other widget runtime) -->
      </div>
    </div>
  </div>
</div>
.btn-primary {
  /* `color: var(--on-accent)` (added in v1.19.0) is the canonical
     "text on --accent" token. Falls back to `var(--paper)` for any
     consumer on an older @magicblocksai/css that pre-dates the
     `--on-accent` token. */
  background: var(--accent); color: var(--on-accent, var(--paper));
  box-shadow: var(--sh-pink);
}

.btn-primary:hover   { transform: translateY(-1px); filter: brightness(1.04); }

.btn-primary:active  { transform: translateY(0); filter: brightness(0.96); }

.btn-sm { padding: 7px var(--s-4); font-size: 13px; border-radius: var(--r-sm); }

.section-card-action .btn-primary { box-shadow: none; }

@media (pointer: coarse) {
  /* Compact buttons */
  .btn-sm,
  .btn-icon-sm { min-height: 44px; }

  /* Modal / drawer close (×) */
  .modal-close,
  .drawer-close {
    min-width: 44px;
    min-height: 44px;
  }

  /* Section card action — usually a text link "All todos →".
     Pad the click target out without growing the visual element by
     pulling negative margin on the same axes. */
  .section-card-action {
    padding: 10px;
    margin: -10px -10px -10px 10px;       /* top/right/bottom -10, leave left auto-margin */
  }

  /* Section card count chip — 18px tall on desktop, expand the
     click target via padding+margin trick. */
  .section-card-count {
    min-height: 28px;
    padding-top: 6px;
    padding-bottom: 6px;
    margin-top: -6px;
    margin-bottom: -6px;
  }

  /* Dropdown menu items — already close to 44px in `comfortable`
     density; nudge dense menus to the floor. */
  .dropdown-menu-item,
  .menu-item {
    min-height: 44px;
  }

  /* Inbox row action buttons — visible, but small (28×28). Touch
     users get a 36×36 chevron with the same icon + the same row
     hover behaviour. */
  .inbox-row .ix-action {
    min-width: 36px;
    min-height: 36px;
  }

  /* Tab buttons in chapter-style demos — already largeish but
     ensure none drop below 44px. */
  .demo-tabs button { min-height: 44px; }
}

.widget-theme-scope {
  --w-font-family: Inter, system-ui, sans-serif;
  --w-font-weight: 400;
  --w-font-size: 14px;
  --w-line-height: 22px;
  font-family: var(--w-font-family);
  font-weight: var(--w-font-weight);
  font-size: var(--w-font-size);
  line-height: var(--w-line-height);
  color-scheme: light;
}

.color-field { display: flex; flex-direction: column; gap: 6px; }

/* …additional rules trimmed for brevity — see _shared.css */
import {
  WidgetStyleEditor,
  WidgetStyleSection,
  ColorField,
  WidgetThemeProvider,
  WidgetShell,
  WidgetLauncher,
} from '@magicblocksai/ui';
import type { WidgetTheme } from '@magicblocksai/ui';
import { useState } from 'react';

function Example() {
  const [theme, setTheme] = useState<WidgetTheme>({
    launcher: { bg: '#C69C6D', icon: '#FFFFFF' },
    shell:    { headerBg: '#C69C6D', headerText: '#FCFCFC', chatBg: '#FCFCFC' },
    composer: { sendBg: '#C69C6D', sendText: '#FFFFFF' },
  });
  const set = (path: string, hex: string) =>
    setTheme((t) => /* immutably merge by path */ ({ ...t }));
  return (
    <WidgetStyleEditor
      header={
        <>
          <h2>Charlie’s Wines · Chat Appearance</h2>
          <button className="btn btn-primary btn-sm">Save</button>
        </>
      }
      form={
        <>
          <WidgetStyleSection title="Header" caption="The bar across the top.">
            <ColorField label="Background" value={theme.shell?.headerBg}
              onValueChange={(hex) => set('shell.headerBg', hex)} />
            <ColorField label="Text"       value={theme.shell?.headerText}
              onValueChange={(hex) => set('shell.headerText', hex)} />
          </WidgetStyleSection>
          <WidgetStyleSection title="Launcher" caption="The floating bubble.">
            <ColorField label="Launcher background" value={theme.launcher?.bg}
              onValueChange={(hex) => set('launcher.bg', hex)} />
            <ColorField label="Launcher icon"       value={theme.launcher?.icon}
              onValueChange={(hex) => set('launcher.icon', hex)} />
          </WidgetStyleSection>
          <WidgetStyleSection title="Composer" caption="The send box.">
            <ColorField label="Send button" value={theme.composer?.sendBg}
              onValueChange={(hex) => set('composer.sendBg', hex)} />
          </WidgetStyleSection>
        </>
      }
      preview={
        <WidgetThemeProvider theme={theme}>
          <WidgetShell floating={false} agentName="Charlie’s Wines" />
        </WidgetThemeProvider>
      }
    />
  );
}

21.5 WidgetEmbedSnippet

The output of the designer surface — a copy-ready <script> snippet operators paste into their site to install the widget. Tab strip switches between five framework targets (HTML, React, Next.js App Router, Vue, WordPress) — same widget id, different wrapper. Built-in Copy button writes the active snippet to the clipboard. Pin to a specific appearance with appearanceId so the widget reads from that published theme.

WidgetEmbedSnippet

.widget-embed

Two side-by-side variants — the default HTML target with just a widget id, and the same widget id pinned to a published appearanceId. Both show the optional title + caption header, the framework target tabs, and the snippet block with the copy affordance pinned to the top-right corner.

default · widget id only
Install on your site
Paste this into your site’s <head> or footer.
<!-- MagicBlocks chat widget -->
<script async src="https://widget.magicblocks.ai/embed.js?id=wid_01H9XY"></script>
with appearance · pinned theme
Charlie’s Wines widget
Pinned to appearance ap_42 (Warm Glow).
<!-- MagicBlocks chat widget -->
<script async src="https://widget.magicblocks.ai/embed.js?id=wid_01H9XY&appearance=ap_42"></script>
<!-- .widget-embed wraps a head (title + caption), a tabs strip       -->
<!-- (one .widget-embed-tab per target), and the body (the snippet    -->
<!-- <pre> + the absolutely-positioned copy affordance).               -->
<div class="widget-embed">
  <div class="widget-embed-head">
    <div class="widget-embed-title">Install on your site</div>
    <div class="widget-embed-caption">Paste into <head> or footer.</div>
  </div>
  <div class="widget-embed-tabs" role="tablist" aria-label="Embed target">
    <button class="widget-embed-tab is-on" role="tab" aria-selected="true">HTML</button>
    <button class="widget-embed-tab" role="tab" aria-selected="false">React</button>
    <!-- …more targets… -->
  </div>
  <div class="widget-embed-body">
    <pre class="widget-embed-pre"><code><!-- snippet --></code></pre>
    <button class="widget-embed-copy" aria-label="Copy embed snippet">Copy</button>
  </div>
</div>
.widget-embed {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  background: var(--bg-paper);
  overflow: hidden;
}

.widget-embed-head {
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--hair-soft);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.widget-embed-title { font: 500 14px/1.3 var(--f-body); color: var(--fg); }

.widget-embed-caption { font: 400 12.5px/1.4 var(--f-body); color: var(--fg-soft); }

.widget-embed-tabs {
  display: flex;
  gap: 0;
  padding: 0 var(--s-4);
  border-bottom: 1px solid var(--hair-soft);
}

.widget-embed-tab {
  appearance: none;
  background: transparent;
  border: 0;
  padding: 8px 12px;
  font: 500 12.5px/1 var(--f-body);
  color: var(--fg-soft);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color var(--dur-2) var(--ease),
              border-color var(--dur-2) var(--ease);
}

.widget-embed-tab:hover { color: var(--fg); }

.widget-embed-tab.is-on {
  color: var(--fg);
  border-bottom-color: var(--accent);
}

.widget-embed-body {
  position: relative;
}

.widget-embed-pre {
  margin: 0;
  padding: var(--s-3) var(--s-4);
  font: 400 12px/1.55 var(--f-mono);
  color: var(--fg);
  white-space: pre;
  overflow-x: auto;
  background: color-mix(in oklab, var(--bg-warm) 30%, var(--bg-paper));
}

.widget-embed-copy {
  position: absolute;
  top: 6px;
  right: 6px;
  appearance: none;
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-xs);
  padding: 3px 8px;
  font: 500 11.5px/1 var(--f-body);
  color: var(--fg);
  cursor: pointer;
  transition: background var(--dur-2) var(--ease);
}

.widget-embed-copy:hover { background: var(--bg-warm); }
import { WidgetEmbedSnippet } from '@magicblocksai/ui';

// Default — all five framework targets visible, HTML active.
<WidgetEmbedSnippet
  widgetId="wid_01H9XY"
  title="Install on your site"
  caption="Paste this into your site's <head> or footer."
/>

// Pinned to a specific published appearance.
<WidgetEmbedSnippet
  widgetId="wid_01H9XY"
  appearanceId="ap_42"
  title="Charlie's Wines widget"
  caption="Pinned to appearance ap_42 (Warm Glow)."
/>

// Restrict the visible targets — e.g. only HTML + React.
<WidgetEmbedSnippet
  widgetId="wid_01H9XY"
  targets={['html', 'react']}
  defaultTarget="react"
/>