Skip to content

Widget UI Catalog

A premium, standardized design system for PIE plugin widgets. Use this catalog as your single source of truth for building polished, consistent widget UIs — whether you are writing HTML by hand or using the AI builder.

This page is specifically for the authenticated ===widget=== surface inside PIE. If you are building a public website or shareable route at /apps/..., use the Public Apps and Routes guide for packaging, routing, and public action patterns.

Widgets are sandboxed iframes. They communicate with the host via PIE.onData, PIE.sendAction, PIE.resize, and PIE.onTheme. All CSS, HTML, and JS live inside the ===widget=== section of your .pie file.


Foundations

Design Tokens

Every widget should define its palette through CSS custom properties on :root. These tokens are aligned with the PIE host app's design system so widgets look native inside the shell.

html
<style>
:root {
  /* Surfaces — Light */
  --bg: #FFFFFF;
  --bg-secondary: #FAFAFA;
  --bg-tertiary: #F5F5F5;

  /* Text */
  --fg: #171717;
  --fg-secondary: #525252;
  --fg-muted: #A3A3A3;

  /* Accent — PIE Pink */
  --accent: #FF0066;
  --accent-hover: #FF3385;
  --accent-soft: rgba(255, 0, 102, 0.08);

  /* Semantic */
  --success: #22C55E;
  --success-soft: rgba(34, 197, 94, 0.10);
  --warning: #F59E0B;
  --warning-soft: rgba(245, 158, 11, 0.10);
  --danger: #EF4444;
  --danger-soft: rgba(239, 68, 68, 0.08);

  /* Chrome */
  --border: #E5E5E5;
  --border-light: rgba(0, 0, 0, 0.06);
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 10px 24px rgba(0, 0, 0, 0.1);

  /* Shape */
  --radius-sm: 6px;
  --radius: 10px;
  --radius-lg: 14px;
  --radius-xl: 16px;
  --radius-full: 9999px;

  /* Typography */
  --font: 'Inter', system-ui, -apple-system, sans-serif;
  --font-mono: 'JetBrains Mono', Menlo, Monaco, monospace;
  --text-xs: 11px;
  --text-sm: 13px;
  --text-base: 14px;
  --text-lg: 16px;
  --text-xl: 20px;

  /* Spacing scale */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;

  /* Motion */
  --duration-fast: 100ms;
  --duration-normal: 200ms;
  --duration-slow: 300ms;
  --easing: ease;
}
</style>

Dark Mode via PIE.onTheme

Use PIE.onTheme to respond to the host app's theme. Dark mode uses PIE's deep dark palette — near-black with a subtle cool tint.

html
<script>
PIE.onTheme(function(theme, colors) {
  var r = document.documentElement.style;
  if (theme === 'dark') {
    r.setProperty('--bg', '#0A0A0F');
    r.setProperty('--bg-secondary', '#111118');
    r.setProperty('--bg-tertiary', '#1A1A24');
    r.setProperty('--fg', '#F0F0F5');
    r.setProperty('--fg-secondary', '#8888A0');
    r.setProperty('--fg-muted', '#525260');
    r.setProperty('--accent-soft', 'rgba(255, 0, 102, 0.15)');
    r.setProperty('--border', '#262626');
    r.setProperty('--border-light', 'rgba(255, 255, 255, 0.06)');
    r.setProperty('--shadow-sm', '0 1px 2px rgba(0, 0, 0, 0.3)');
    r.setProperty('--shadow-md', '0 4px 6px rgba(0, 0, 0, 0.4)');
    r.setProperty('--shadow-lg', '0 10px 24px rgba(0, 0, 0, 0.5)');
  } else {
    r.setProperty('--bg', '#FFFFFF');
    r.setProperty('--bg-secondary', '#FAFAFA');
    r.setProperty('--bg-tertiary', '#F5F5F5');
    r.setProperty('--fg', '#171717');
    r.setProperty('--fg-secondary', '#525252');
    r.setProperty('--fg-muted', '#A3A3A3');
    r.setProperty('--accent-soft', 'rgba(255, 0, 102, 0.08)');
    r.setProperty('--border', '#E5E5E5');
    r.setProperty('--border-light', 'rgba(0, 0, 0, 0.06)');
    r.setProperty('--shadow-sm', '0 1px 2px rgba(0, 0, 0, 0.05)');
    r.setProperty('--shadow-md', '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)');
    r.setProperty('--shadow-lg', '0 10px 24px rgba(0, 0, 0, 0.1)');
  }
  if (colors && colors.accent) {
    r.setProperty('--accent', colors.accent);
  }
});
</script>

Base Reset and Body

html
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  font-family: var(--font);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--fg);
  background: var(--bg);
  padding: var(--space-3);
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-feature-settings: 'cv11', 'ss01';
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(120, 120, 128, 0.28); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(120, 120, 128, 0.42); }
::selection { background: rgba(255, 0, 102, 0.2); }
</style>

Premium Rules

  • Never show raw browser defaults (unstyled selects, default checkboxes, system fonts).
  • Use strong visual hierarchy: one dominant element per section, muted secondary text, restrained accents.
  • Minimum touch target: 32px height for interactive elements.
  • Use var(--fg-secondary) for labels and metadata, var(--fg-muted) for timestamps and hints.
  • Limit accent color to primary actions and active states. Everything else is neutral.
  • Animate state changes with transition: all var(--duration-normal) var(--easing).
  • Use var(--shadow-sm) for cards at rest, var(--shadow-md) on hover.
  • Use frosted glass effects (backdrop-filter: blur()) for overlays and sticky headers where appropriate.
  • Scrollbars should be slim and translucent — never default system scrollbars.
  • Enable Inter font features: font-feature-settings: 'cv11', 'ss01'.

Widget Contract

manifest.widget:
  allowedScripts: []          # HTTPS CDN URLs if needed
  allowedStyles: []           # HTTPS CDN URLs if needed
  maxHeight: 100–800          # pixels, enforced by host
  defaultHeight: 280          # initial iframe height
  launchable: true|false      # show in agent launcher
  • PIE.onData(callback) — receive data from handler via context.widget.show() / context.widget.update()
  • PIE.sendAction(actionId, payload) — trigger handler with input._widgetAction = true
  • PIE.resize(height) — request height change (clamped to maxHeight and 800px)
  • PIE.onTheme(callback) — receive (theme, colors) from host
  • Action responses arrive as PIE.onData with data.__actionMeta: { ok, error, result }

Primitive Components

Buttons

html
<style>
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  padding: 6px 16px; height: 32px; border-radius: var(--radius-sm);
  font-size: var(--text-sm); font-weight: 500; font-family: var(--font);
  cursor: pointer; border: none; outline: none;
  transition: all var(--duration-fast) var(--easing);
}
.btn:active { transform: scale(0.97); }
.btn-primary {
  background: var(--accent); color: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}
.btn-primary:hover { background: var(--accent-hover); }
.btn-secondary {
  background: var(--bg); color: var(--fg);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
}
.btn-secondary:hover { background: var(--bg-tertiary); }
.btn-ghost {
  background: transparent; color: var(--fg-secondary);
}
.btn-ghost:hover { background: var(--bg-tertiary); color: var(--fg); }
.btn-danger {
  background: var(--danger); color: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}
.btn-danger:hover { filter: brightness(1.08); }
.btn-sm { padding: 3px 10px; height: 26px; font-size: var(--text-xs); }
.btn-icon {
  padding: 0; width: 32px; height: 32px; border-radius: var(--radius-sm);
  background: transparent; border: none; color: var(--fg-secondary); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: all var(--duration-fast) var(--easing);
}
.btn-icon:hover { background: var(--bg-tertiary); color: var(--fg); }
.btn-icon:active { transform: scale(0.92); }
.btn[disabled], .btn-icon[disabled] { opacity: 0.4; pointer-events: none; }
.btn .spinner { width: 14px; height: 14px; border: 2px solid rgba(255,255,255,0.3);
  border-top-color: #fff; border-radius: 50%;
  animation: spin 0.6s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>

Inputs

html
<style>
.input, .select, .textarea {
  width: 100%; padding: 6px 12px; height: 32px;
  font-size: var(--text-sm); font-family: var(--font);
  background: var(--bg); color: var(--fg);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  transition: all var(--duration-fast) var(--easing);
  outline: none;
}
.input:focus, .select:focus, .textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.input::placeholder { color: var(--fg-muted); }
.textarea { height: auto; resize: vertical; min-height: 64px; padding: 8px 12px; line-height: 1.5; }
.select { appearance: none; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%23A3A3A3' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 12px center; padding-right: 32px;
}
.field { display: flex; flex-direction: column; gap: 3px; }
.field-label { font-size: var(--text-xs); font-weight: 500; color: var(--fg-secondary); }
.search-input {
  padding-left: 34px;
  background-image: url("data:image/svg+xml,%3Csvg width='14' height='14' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='11' cy='11' r='7' stroke='%23A3A3A3' stroke-width='2'/%3E%3Cpath d='M16 16L20 20' stroke='%23A3A3A3' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: 12px center;
}
</style>

Tabs and Navigation

html
<style>
.tabs {
  display: flex; gap: 1px; padding: 2px;
  background: var(--bg-secondary); border-radius: var(--radius);
}
.tab {
  padding: 4px 14px; height: 28px; font-size: var(--text-xs); font-weight: 500;
  border-radius: calc(var(--radius) - 2px); border: none; cursor: pointer;
  background: transparent; color: var(--fg-secondary); font-family: var(--font);
  transition: all var(--duration-fast) var(--easing);
  display: flex; align-items: center; justify-content: center;
}
.tab:hover { color: var(--fg); }
.tab.active {
  background: var(--bg); color: var(--fg); font-weight: 600;
  box-shadow: var(--shadow-sm);
}
.tab .count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 16px; height: 16px; padding: 0 4px; margin-left: 5px;
  font-size: 10px; font-weight: 600; border-radius: var(--radius-full);
  background: var(--bg-tertiary); color: var(--fg-muted);
}
.tab.active .count { background: var(--accent-soft); color: var(--accent); }
.breadcrumb {
  display: flex; align-items: center; gap: var(--space-2);
  font-size: var(--text-xs); color: var(--fg-muted);
}
.breadcrumb-link { color: var(--accent); cursor: pointer; text-decoration: none; }
.breadcrumb-link:hover { text-decoration: underline; }
.breadcrumb-sep::before { content: '\203A'; color: var(--fg-muted); }
.filter-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  padding: 3px 12px; height: 26px; font-size: var(--text-xs); font-weight: 500;
  border-radius: var(--radius-full);
  border: 1px solid var(--border);
  background: var(--bg); color: var(--fg-secondary); cursor: pointer;
  display: flex; align-items: center;
  transition: all var(--duration-fast) var(--easing);
}
.chip:hover { border-color: var(--accent); color: var(--accent); }
.chip.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); font-weight: 600; }
</style>

Cards and Display

html
<style>
.card {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--space-3);
  box-shadow: var(--shadow-sm);
  transition: all var(--duration-normal) var(--easing);
}
.card:hover { box-shadow: var(--shadow-md); }
.card-clickable { cursor: pointer; }
.card-title {
  font-size: var(--text-sm); font-weight: 600; color: var(--fg);
  margin-bottom: 2px;
}
.card-meta { font-size: var(--text-xs); color: var(--fg-secondary); }

.section-title {
  font-size: 10px; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.2em; color: var(--fg-muted); margin-bottom: var(--space-2);
}
.badge {
  display: inline-flex; align-items: center; padding: 1px 7px; height: 18px;
  font-size: 10px; font-weight: 500; border-radius: var(--radius-full);
  background: var(--bg-tertiary); color: var(--fg-secondary);
}
.badge-accent { background: var(--accent-soft); color: var(--accent); }
.badge-success { background: var(--success-soft); color: var(--success); }
.badge-warning { background: var(--warning-soft); color: var(--warning); }
.badge-danger { background: var(--danger-soft); color: var(--danger); }

.status-dot {
  width: 7px; height: 7px; border-radius: 50%; display: inline-block;
}
.status-dot-success { background: var(--success); }
.status-dot-warning { background: var(--warning); }
.status-dot-danger { background: var(--danger); }

.stat-card { text-align: center; padding: var(--space-4); }
.stat-value { font-size: var(--text-xl); font-weight: 700; color: var(--fg); }
.stat-label { font-size: var(--text-xs); color: var(--fg-secondary); margin-top: 2px; }

.kv-row { display: flex; justify-content: space-between; align-items: center;
  padding: 6px 0; border-bottom: 1px solid var(--border-light); }
.kv-label { font-size: var(--text-xs); color: var(--fg-secondary); }
.kv-value { font-size: var(--text-sm); color: var(--fg); font-weight: 500; }

.avatar {
  width: 32px; height: 32px; border-radius: var(--radius-sm);
  background: var(--accent-soft); color: var(--accent);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--text-xs); font-weight: 600; flex-shrink: 0;
}
.avatar-round { border-radius: 50%; }

.list-row {
  display: flex; align-items: center; gap: var(--space-3);
  padding: 6px var(--space-3); min-height: 36px;
  border-radius: var(--radius-sm); cursor: pointer;
  transition: background var(--duration-fast) var(--easing);
}
.list-row:hover { background: var(--bg-secondary); }
.list-row.active { background: var(--accent-soft); }
.list-row-title { font-size: var(--text-sm); font-weight: 500; color: var(--fg); }
.list-row-sub { font-size: var(--text-xs); color: var(--fg-secondary); }

.table { width: 100%; border-collapse: collapse; font-size: var(--text-xs); }
.table th {
  text-align: left; padding: 6px var(--space-3);
  font-weight: 600; color: var(--fg-muted); font-size: 10px;
  text-transform: uppercase; letter-spacing: 0.15em;
  border-bottom: 1px solid var(--border);
  background: var(--bg-secondary); position: sticky; top: 0; z-index: 1;
}
.table td {
  padding: 6px var(--space-3); border-bottom: 1px solid var(--border-light);
  color: var(--fg);
}
.table tr:hover td { background: var(--bg-secondary); }
.table tr.selected td { background: var(--accent-soft); }
</style>

Feedback and States

html
<style>
.loading { display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: var(--space-3); padding: var(--space-8); }
.loading-spinner {
  width: 22px; height: 22px; border: 2px solid var(--border);
  border-top-color: var(--accent); border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
.loading-text { font-size: var(--text-xs); color: var(--fg-muted); }

.skeleton {
  background: linear-gradient(90deg, var(--bg-secondary) 25%, var(--bg-tertiary) 50%, var(--bg-secondary) 75%);
  background-size: 200% 100%; animation: shimmer 1.5s ease-in-out infinite;
  border-radius: var(--radius-sm);
}
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

.empty-state { display: flex; flex-direction: column; align-items: center;
  justify-content: center; padding: var(--space-8); text-align: center; }
.empty-icon { width: 44px; height: 44px; margin-bottom: var(--space-3);
  color: var(--fg-muted); opacity: 0.4; }
.empty-title { font-size: var(--text-base); font-weight: 600; color: var(--fg-secondary);
  margin-bottom: 2px; }
.empty-desc { font-size: var(--text-xs); color: var(--fg-muted); max-width: 220px; line-height: 1.5; }

.toast {
  position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
  padding: 8px 16px; border-radius: var(--radius-lg); font-size: var(--text-xs);
  font-weight: 500; color: #fff; z-index: 1000;
  box-shadow: var(--shadow-lg);
  backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
  animation: toast-in var(--duration-normal) var(--easing);
}
.toast-success { background: rgba(34, 197, 94, 0.9); }
.toast-error { background: rgba(239, 68, 68, 0.9); }
@keyframes toast-in { from { opacity: 0; transform: translateX(-50%) translateY(8px); }
  to { opacity: 1; transform: translateX(-50%) translateY(0); } }

.banner {
  display: flex; align-items: center; gap: var(--space-2);
  padding: 8px var(--space-3); border-radius: var(--radius-sm);
  font-size: var(--text-xs); font-weight: 500;
}
.banner-success { background: var(--success-soft); color: var(--success); }
.banner-warning { background: var(--warning-soft); color: var(--warning); }
.banner-error { background: var(--danger-soft); color: var(--danger); }

.progress-bar { height: 4px; background: var(--bg-tertiary); border-radius: 2px;
  overflow: hidden; }
.progress-fill { height: 100%; background: var(--accent); border-radius: 2px;
  transition: width var(--duration-normal) var(--easing); }

.confirm-dialog {
  position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
  display: flex; align-items: center; justify-content: center; z-index: 1000;
  animation: fade-in var(--duration-fast) var(--easing);
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
.confirm-box {
  background: var(--bg); border-radius: var(--radius-xl);
  padding: var(--space-5);
  max-width: 300px; width: 90%; box-shadow: var(--shadow-lg);
  border: 1px solid var(--border-light);
  animation: scale-in var(--duration-normal) ease-out;
}
@keyframes scale-in { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
.confirm-title { font-size: var(--text-base); font-weight: 600; margin-bottom: var(--space-2); }
.confirm-desc { font-size: var(--text-sm); color: var(--fg-secondary);
  line-height: 1.5; margin-bottom: var(--space-4); }
.confirm-actions { display: flex; justify-content: flex-end; gap: var(--space-2); }

.timeline { display: flex; flex-direction: column; }
.timeline-item { display: flex; gap: var(--space-3); padding-bottom: var(--space-3); }
.timeline-marker {
  width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
  margin-top: 5px; flex-shrink: 0; position: relative;
}
.timeline-item:not(:last-child) .timeline-marker::after {
  content: ''; position: absolute; left: 2.5px; top: 9px;
  width: 2px; height: calc(100% + var(--space-3) - 2px);
  background: var(--border-light);
}
.timeline-content { flex: 1; }
.timeline-label { font-size: var(--text-xs); color: var(--fg-muted); }
.timeline-text { font-size: var(--text-sm); color: var(--fg); }
</style>

Workflow Patterns

Workflow patterns standardize interaction flows — not just components, but the sequence of states and events that a widget moves through. Each pattern maps to PIE.sendAction, PIE.onData, and __actionMeta.

Search → Filter → Inspect → Act

User types query → sendAction('search', { query, filters })
Widget shows loading → onData({ type: 'results', items }) → render list
User clicks item → local state update → render detail
User clicks action → sendAction('act', { id }) → __actionMeta → toast

States: idle, searching, results, no-results, detail, acting, success, error

Queue → Review → Approve/Reject → Next

onData({ type: 'queue', items, total })
User reviews top item → clicks approve/reject
sendAction('approve', { id }) → __actionMeta → advance to next
When queue empty → onData({ type: 'queue', items: [] }) → empty state

States: loading, reviewing, acting, empty, error

Generate → Preview → Accept/Regenerate

onData({ type: 'generating', progress }) → progress bar
onData({ type: 'result', output }) → render preview
User accepts → sendAction('accept', { id })
User regenerates → sendAction('regenerate', { id }) → back to generating

States: idle, generating, preview, accepting, regenerating, done, error

Connect → Authorize → Validate → Success

Widget starts → check connection state via onData
Not connected → show connect button
sendAction('connect') → host opens OAuth → onData({ type: 'connected' })
Validate → sendAction('validate') → success or retry

States: checking, disconnected, connecting, connected, validating, ready, error

Run Job → Progress → Complete/Retry

sendAction('start_job', { params }) → __actionMeta { ok }
onData({ type: 'progress', percent, message }) → progress bar
onData({ type: 'complete', result }) → show result
onData({ type: 'failed', error }) → show error + retry button
User retries → sendAction('retry', { jobId })

States: idle, running, progress, complete, failed, retrying

Browse → Select → Compare → Confirm

onData({ type: 'items', items }) → grid/list
User selects items → local state (selectedIds)
User opens comparison → render side-by-side
User confirms → sendAction('confirm', { selectedId })

States: browsing, selecting, comparing, confirming, confirmed

Create/Edit → Autosave → Publish

Widget loads document → onData({ type: 'document', content })
User edits → debounced sendAction('save', { content }) → __actionMeta
User publishes → sendAction('publish', { id }) → __actionMeta → banner

States: loading, editing, saving, saved, publishing, published, error

Install → Configure → Validate → Launch

First load → onData({ type: 'setup', step }) or onData({ type: 'ready' })
If setup needed → show config fields
sendAction('configure', { settings }) → validate → onData({ type: 'ready' })

States: checking, needs-setup, configuring, validating, ready, error

Monitor → Investigate → Resolve

onData({ type: 'status', items, alerts }) → dashboard
User clicks alert → detail view
sendAction('resolve', { alertId, action }) → __actionMeta

States: monitoring, investigating, resolving, resolved


Screen Templates

A screen template is a reusable page/workspace structure. It defines layout, hierarchy, action placement, and default states. The same template powers multiple domains by swapping the nouns and data.

Every screen template below includes cross-domain examples showing how it generalizes.

Master-Detail Screen

Layout: list on the left/top, detail pane on the right/bottom. Selecting an item updates the detail.

Cross-domain examples:

  • Plugin Marketplace: plugin list → plugin detail with install
  • HubSpot CRM: contact/deal list → record detail with inline edit
  • File Browser: document list → preview and metadata
html
<style>
.master-detail { display: flex; height: 100vh; overflow: hidden; }
.master-list {
  width: 260px; min-width: 200px; border-right: 1px solid var(--border);
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--bg-secondary);
}
.master-list-header {
  padding: var(--space-3); border-bottom: 1px solid var(--border);
  display: flex; flex-direction: column; gap: var(--space-2);
}
.master-list-body { flex: 1; overflow-y: auto; padding: var(--space-1); }
.detail-pane { flex: 1; overflow-y: auto; display: flex; flex-direction: column; background: var(--bg); }
.detail-header {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border-light);
  display: flex; align-items: center; justify-content: space-between;
}
.detail-body { flex: 1; padding: var(--space-4); overflow-y: auto; }
.detail-footer {
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--border-light);
  display: flex; justify-content: flex-end; gap: var(--space-2);
}
@media (max-width: 500px) {
  .master-detail { flex-direction: column; }
  .master-list { width: 100%; max-height: 40vh; border-right: none;
    border-bottom: 1px solid var(--border); }
}
</style>

Search + Filters + Results Screen

Layout: sticky header with search and filter controls, scrollable results area, optional footer with pagination or stats.

Cross-domain examples:

  • Travel Search: flights/hotels with price and date filters
  • Vehicle Search: cars with make/model/price filters
  • Job Board: positions with location/salary/role filters
html
<style>
.search-screen { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
.search-header {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  display: flex; flex-direction: column; gap: var(--space-2);
  background: var(--bg);
}
.search-bar { display: flex; gap: var(--space-2); }
.search-bar .input { flex: 1; }
.search-results { flex: 1; overflow-y: auto; padding: var(--space-3); }
.results-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--space-3);
}
.search-footer {
  padding: 6px var(--space-4);
  border-top: 1px solid var(--border-light);
  display: flex; align-items: center; justify-content: space-between;
  font-size: var(--text-xs); color: var(--fg-muted);
}
</style>

KPI Dashboard Screen

Layout: headline stats row, chart/content area below, optional action strip.

Cross-domain examples:

  • Finance Analyst: stock quote + price chart + fundamentals
  • Marketing Dashboard: traffic, conversions, revenue metrics
  • Operations Console: uptime, error rates, queue depths
html
<style>
.dashboard-screen { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
.dashboard-header {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  display: flex; align-items: center; justify-content: space-between;
}
.dashboard-title { font-size: var(--text-lg); font-weight: 700; }
.stats-row {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: var(--space-3); padding: var(--space-3) var(--space-4);
}
.dashboard-body { flex: 1; overflow-y: auto; padding: var(--space-3) var(--space-4); }
</style>

Table + Inspector Screen

Layout: full-width data table with optional side inspector for the selected row.

Cross-domain examples:

  • Postgres Analysis: query results with row inspector
  • SEO Audit: pages with metrics table and detail drawer
  • Inventory: stock items with quantities and supplier detail
html
<style>
.table-screen { display: flex; height: 100vh; overflow: hidden; }
.table-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.table-toolbar {
  padding: 6px var(--space-3);
  border-bottom: 1px solid var(--border);
  display: flex; align-items: center; justify-content: space-between;
  font-size: var(--text-xs); color: var(--fg-muted);
}
.table-body { flex: 1; overflow: auto; }
.inspector {
  width: 260px; border-left: 1px solid var(--border);
  overflow-y: auto; padding: var(--space-4); background: var(--bg-secondary);
  display: flex; flex-direction: column; gap: var(--space-3);
}
.inspector-title { font-size: var(--text-base); font-weight: 600; margin-bottom: var(--space-2); }
</style>

Document Editor Screen

Layout: toolbar at top, editor workspace center, optional preview or sidebar.

Cross-domain examples:

  • MarkPad: markdown editor with live preview
  • Blog Agent: post editor with metadata sidebar
  • Slide Deck: outline editor with slide preview
html
<style>
.editor-screen { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
.editor-toolbar {
  padding: 4px var(--space-3); height: 36px;
  border-bottom: 1px solid var(--border);
  display: flex; align-items: center; gap: var(--space-2);
  background: var(--bg-secondary);
}
.editor-toolbar-title {
  font-size: var(--text-sm); font-weight: 600; flex: 1;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.editor-body { display: flex; flex: 1; overflow: hidden; }
.editor-main { flex: 1; overflow-y: auto; padding: var(--space-4); }
.editor-sidebar {
  width: 220px; border-left: 1px solid var(--border);
  overflow-y: auto; padding: var(--space-3);
  background: var(--bg-secondary);
}
.editor-statusbar {
  padding: 2px var(--space-3); height: 22px;
  border-top: 1px solid var(--border-light);
  font-size: 10px; color: var(--fg-muted);
  display: flex; align-items: center; justify-content: space-between;
  background: var(--bg-secondary);
}
</style>

Queue / Review Screen

Layout: queue list with review controls. Items are processed one at a time with approve/reject/skip actions.

Cross-domain examples:

  • LinkedIn Engagement: pending actions with approve/reject
  • Content Moderation: flagged items with resolution controls
  • AI Output Review: generated results with accept/regenerate
html
<style>
.review-screen { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
.review-header {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  display: flex; align-items: center; justify-content: space-between;
}
.review-count { font-size: var(--text-xs); color: var(--fg-muted); }
.review-body { flex: 1; overflow-y: auto; padding: var(--space-3); }
.review-item {
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--space-4); margin-bottom: var(--space-3);
  box-shadow: var(--shadow-sm);
}
.review-actions {
  position: sticky; bottom: 0; padding: var(--space-3) var(--space-4);
  background: var(--bg);
  border-top: 1px solid var(--border);
  display: flex; gap: var(--space-2); justify-content: flex-end;
}
</style>

Background Job Monitor Screen

Layout: progress indicator, status message, result or error display with retry action.

Cross-domain examples:

  • Slide Deck Creator: generation progress with slide-by-slide status
  • Data Export: processing progress with download link
  • Bulk Operations: batch processing with item-level results
html
<style>
.job-screen { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
.job-header {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  display: flex; align-items: center; gap: var(--space-3);
}
.job-body { flex: 1; overflow-y: auto; padding: var(--space-4);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-4); }
.job-progress { width: 100%; max-width: 360px; }
.job-status { font-size: var(--text-sm); color: var(--fg-secondary); text-align: center; }
.job-result { width: 100%; }
</style>

Settings / Setup Screen

Layout: vertical form sections with labels, inputs, and validation states. Optional onboarding checklist.

Cross-domain examples:

  • OAuth Connection: connect account flow
  • Plugin Configuration: API key and preferences setup
  • First-Run Onboarding: step-by-step checklist
html
<style>
.setup-screen { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
.setup-header {
  padding: var(--space-4); border-bottom: 1px solid var(--border);
}
.setup-header-title { font-size: var(--text-lg); font-weight: 700; }
.setup-header-desc { font-size: var(--text-sm); color: var(--fg-secondary); margin-top: 2px; }
.setup-body { flex: 1; overflow-y: auto; padding: var(--space-4); }
.setup-section { margin-bottom: var(--space-6); }
.setup-section-title {
  font-size: var(--text-sm); font-weight: 600; margin-bottom: var(--space-3);
  padding-bottom: var(--space-2); border-bottom: 1px solid var(--border-light);
}
.setup-fields { display: flex; flex-direction: column; gap: var(--space-4); }
.setup-footer {
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--border);
  display: flex; justify-content: flex-end; gap: var(--space-2);
}
.checklist-item {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3); border-radius: var(--radius-sm);
}
.checklist-check {
  width: 20px; height: 20px; border-radius: 50%; border: 1.5px solid var(--border);
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  transition: all var(--duration-fast) var(--easing);
}
.checklist-check.done { background: var(--success); border-color: var(--success); color: #fff; }
</style>

Launchable Workspace Screen

Layout: full-panel chrome for standalone mini-apps. Header with title and navigation, main content area, optional footer actions.

Cross-domain examples:

  • MarkPad: full document browser and editor
  • Todo List: grouped task manager
  • Cursor Cloud Agents: agent list → detail → launch form
html
<style>
.workspace-screen { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
.workspace-header {
  padding: 6px var(--space-3); height: 40px;
  border-bottom: 1px solid var(--border);
  display: flex; align-items: center; gap: var(--space-3);
  background: var(--bg-secondary);
}
.workspace-header-icon {
  width: 26px; height: 26px; border-radius: var(--radius-sm);
  background: var(--accent-soft); color: var(--accent);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--text-xs); font-weight: 700;
}
.workspace-header-title { font-size: var(--text-sm); font-weight: 600; flex: 1; }
.workspace-body { flex: 1; overflow-y: auto; background: var(--bg); }
.workspace-footer {
  padding: 4px var(--space-3); height: 24px;
  border-top: 1px solid var(--border-light);
  display: flex; align-items: center; justify-content: space-between;
  font-size: 10px; color: var(--fg-muted);
  background: var(--bg-secondary);
}
</style>

Compact Inline Screen

Layout: minimal chrome for inline chat widgets. No header bar, just padded content with optional compact footer.

Cross-domain examples:

  • Weather Tool: current conditions card
  • Calculator: result display
  • Quick Status: single metric or confirmation
html
<style>
.inline-screen { padding: var(--space-3); }
.inline-card {
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--space-4);
  box-shadow: var(--shadow-sm);
}
.inline-footer {
  margin-top: var(--space-3); display: flex; justify-content: flex-end; gap: var(--space-2);
}
</style>

Data Contracts

Standard onData Payload Shape

js
PIE.onData(function(data) {
  if (data.__actionMeta) {
    if (data.__actionMeta.ok) { showToast('success', 'Done'); }
    else { showToast('error', data.__actionMeta.error); }
    return;
  }
  switch (data.type || data.event) {
    case 'init':
    case 'loaded':
      break;
    case 'item_added':
    case 'item_updated':
    case 'item_removed':
      break;
    case 'progress':
      break;
    case 'error':
      showBanner('error', data.message);
      break;
    default:
      break;
  }
  render();
});

Standard sendAction Pattern

js
function doAction(actionId, payload) {
  setLoading(actionId, true);
  PIE.sendAction(actionId, payload || {});
}

Standard Resize Pattern

js
function autoResize() {
  var body = document.getElementById('app');
  var h = Math.min(Math.max(body.scrollHeight + 24, 200), 600);
  PIE.resize(h);
}

Flash / Toast Helper

js
function showToast(type, message, duration) {
  var el = document.createElement('div');
  el.className = 'toast toast-' + type;
  el.textContent = message;
  document.body.appendChild(el);
  setTimeout(function() { el.remove(); }, duration || 3000);
}

Layout Utilities

html
<style>
.flex { display: flex; }
.flex-col { flex-direction: column; }
.flex-1 { flex: 1; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.justify-end { justify-content: flex-end; }
.gap-1 { gap: var(--space-1); }
.gap-2 { gap: var(--space-2); }
.gap-3 { gap: var(--space-3); }
.gap-4 { gap: var(--space-4); }
.p-3 { padding: var(--space-3); }
.p-4 { padding: var(--space-4); }
.px-3 { padding-left: var(--space-3); padding-right: var(--space-3); }
.py-2 { padding-top: var(--space-2); padding-bottom: var(--space-2); }
.mt-2 { margin-top: var(--space-2); }
.mb-3 { margin-bottom: var(--space-3); }
.w-full { width: 100%; }
.truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); border: 0; }
.scroll-y { overflow-y: auto; }
.hidden { display: none !important; }
</style>

Built with VitePress