Skip to content

Create your first pop

A pop is a focused app and its document in one portable .pop package. Start with a working Checklist, then make the interface and data your own.

This guide covers authoring on a computer. Availability covers the iPhone app, Synced Library and live sharing, and make it work on iPhone shows how to design your own pop for the phone.

Use Bun 1.4.2 or newer, on macOS or Linux. Creating, previewing, checking, building and registering an app, and creating or editing its documents, need only the CLI, which also renders template artwork and exports closed documents as PNG/PDF on both platforms. To open windows and pick your registered templates in the catalog, install the matching pop Mac app in /Applications or ~/Applications. You do not need Node, Swift, Xcode, or a pop repository checkout.

The public CLI workflow is:

Terminal window
bunx @hitslop/pop init pop-counter
cd pop-counter
bun install
bun run dev

After the setup questions, an example response is:

Created /Users/you/pop-counter. Run bun install in that directory, then bun run dev.

cd prints nothing on success. bun install prints an installation summary (counts and timings vary). bun run dev starts a long-running preview and prints:

Disposable preview: http://localhost:5173/

If port 5173 is busy, choose another with bun run dev --port 5174. Stop the preview before continuing with builds.

Alternatively, install the CLI with bun install -g @hitslop/pop and run pop init pop-counter. The generated project pins its own pop version; prefer its bun run scripts after creation.

See CLI workflows for command entry points, document creation, editing, moving data between documents, export, and agent skill installation.

init asks you to describe your pop and who the author is. It starts from a working Checklist, saves your idea in BRIEF.md, and includes AGENTS.md and authoring/design skills in .agents/skills/.

The final question asks which agent CLI should build it for you. Choose an installed Codex, Claude Code, Gemini CLI, or OpenCode, or select Other CLI… to enter a tool such as Grok and its prompt option. The agent launches in your project with the brief and pop guidance. Choose Finish without launching to follow the tutorial below yourself. Add --yes for a non-interactive starter; this never launches another agent.

Read pop.ts first. It declares the app with definePop: its title, description, author, categories, initial window size, theme colors and optional artwork. Its explicit slug identifies the app. The wizard records your author name, and the agent sets the title, description, and categories to match what it builds; you can edit these and the initial window size anytime.

  • schema.ts default-exports the document fields and creation-only initial values.
  • commands.ts optionally default-exports agent commands; the starter includes addTask.
  • App.svelte renders and edits the document through typed handles.
  • Required Preview.svelte renders a compact saved-state overview for Quick Look and the catalog.
  • Required Icon.svelte renders the Finder icon. Optional Export.svelte renders complete PNG/PDF output.
  • pop.ts’s theme declares the colors people can change.
  • styles.css owns layout and component styling; import it explicitly from App.svelte.

The root role filenames are fixed and case-sensitive: pop.ts, schema.ts, App.svelte, required Preview.svelte and Icon.svelte, optional Export.svelte, Widget.svelte and commands.ts. Put helper modules anywhere. Widget.svelte supplies saved-state Mac desktop and iPhone Home Screen images; see widget authoring.

The browser preview edits a disposable copy of the document; refreshing starts a fresh one. Vite watches source changes. Component and CSS updates preserve accepted edits; a change to metadata, schema, commands, their dependencies or the role inventory resets the copy. Test native persistence in a writable document copy.

Replace the starter’s data definition:

schema.ts
import { defineDocument, s } from "@hitslop/pop";
export default defineDocument(
{ title: s.text(), count: s.counter() },
{ initial: { title: "Pop Counter" } },
);

Then describe the counter in pop.ts. Its theme declares the colors styles.css uses below:

pop.ts
import { definePop } from "@hitslop/pop";
export default definePop({
slug: "pop-counter",
title: "Pop Counter",
description: "Count the small things worth counting.",
author: { name: "Your Name" },
categories: ["utilities"],
window: { kind: "standard", width: 360, height: 360, fullscreenable: true },
theme: { surface: "#fff7e6", ink: "#382d24", accent: "#28634b" },
});

fullscreenable: true enables the native fullscreen control. This flexible window fills the screen; fixed and shaped designs fit their composition on black. See fullscreen behavior.

Replace the starter’s commands.ts with a counter action:

commands.ts
import { defineCommands } from "@hitslop/pop/commands";
import schema from "./schema";
export default defineCommands(schema, {
countOne: {
description: "Count one more.",
args: {},
run({ tx }) {
tx.fields.count.increment();
},
},
});

The CLI discovers countOne from this set. Commands are for agents; their arguments are flat scalars or optional scalars, or {} when none are needed. The page below edits with its document handle. To give the button and an agent one shared edit, see share an edit with agents.

Replace App.svelte with an editor that acquires its document during component initialization (document access):

App.svelte
<script lang="ts">
import { EditableText, getDocument } from "@hitslop/pop/svelte";
import schema from "./schema";
import "./styles.css";
const doc = getDocument(schema);
</script>
<main class="counter">
<EditableText class="counter-title" field={doc.fields.title} label="Counter title" placeholder="Name it" />
<p class="counter-value">{doc.current.count}</p>
<button data-pop-export="hide" onclick={() => doc.fields.count.increment()}>
Count one
</button>
</main>

Replace Preview.svelte with a compact saved-state view for Quick Look and the catalog. It receives the saved snapshot as props and renders independently of the editor (capture props):

Preview.svelte
<script lang="ts">
import type { CaptureMode, CaptureProps } from "@hitslop/pop/svelte";
import type schema from "./schema";
let { snapshot }: CaptureProps<typeof schema> & { mode: CaptureMode } = $props();
</script>
<main class="counter">
<h1 class="counter-title">{snapshot.title}</h1>
<p class="counter-value">{snapshot.count}</p>
</main>

This counter’s complete export fits the same layout, so replace Export.svelte with a shared view:

Export.svelte
<script lang="ts">
import type { CaptureMode, CaptureProps } from "@hitslop/pop/svelte";
import type schema from "./schema";
import Preview from "./Preview.svelte";
let props: CaptureProps<typeof schema> & { mode: CaptureMode } = $props();
</script>
<Preview {...props} />

For growing documents, keep Preview compact and let Export include all rows. Removing Export.svelte removes PNG/PDF export actions; Preview and Icon remain required.

See icons and exports for capture authoring.

Icon.svelte draws the Finder icon:

Icon.svelte
<script lang="ts">
import type { CaptureMode, CaptureProps } from "@hitslop/pop/svelte";
import type schema from "./schema";
let { snapshot }: CaptureProps<typeof schema> & { mode: CaptureMode } = $props();
</script>
<strong class="counter-icon">{snapshot.count}</strong>

Give it ordinary CSS, using the starter’s declared tokens:

styles.css
* {
box-sizing: border-box;
}
body {
background: var(--pop-surface);
color: var(--pop-ink);
font-family: system-ui;
}
.counter {
padding: 2rem;
text-align: center;
}
.counter-title {
font-size: 1.4rem;
font-weight: 600;
}
.counter-value {
font-size: 4rem;
font-variant-numeric: tabular-nums;
}
.counter button {
padding: 0.8rem 1.2rem;
background: var(--pop-accent);
color: white;
border: 0;
border-radius: 0.5rem;
}
.counter button:focus-visible {
outline: 3px solid var(--pop-ink);
outline-offset: 3px;
}
.counter-icon {
font-size: 180px;
color: var(--pop-accent);
}
Terminal window
bun run check
bun run build
bun run register

Expected responses, respectively (tool progress omitted; paths vary):

svelte-check found 0 errors and 0 warnings
/Users/you/pop-counter/dist/pop-counter.pop
/Users/you/.pop/templates/pop-counter.pop

Build also renders your Finder artwork on macOS and Linux; a failed render fails the build.

Build emits dist/pop-counter.pop. Register builds it the same way and installs an immutable template in ~/.pop/templates. In the Mac app, choose Templates, select your app, and Create a writable copy.

Edit that copy, close immediately after typing, reopen, duplicate it, and export PNG/PDF. Check keyboard focus and a narrower window. Read styling, data and schemas, and icons and exports before expanding the app.

Save the writable copy as My Counter.pop. From the directory containing it:

Terminal window
bunx @hitslop/pop describe "My Counter.pop"

Response excerpt:

Commands (pop call PATH NAME --args JSON)
Arguments accept only the listed fields.
countOne — Count one more.
No arguments. Pass --args '{}'.

describe also includes the fields, operations, version and current values.

Terminal window
bunx @hitslop/pop call "My Counter.pop" countOne
{ "result": null, "ids": [] }

This action returns no value and inserts no rows, but it increments the saved counter. If you started at zero, read it back:

Terminal window
bunx @hitslop/pop get "My Counter.pop"
{ "count": 1, "title": "Pop Counter" }

You can also edit the counter directly:

Terminal window
bunx @hitslop/pop apply "My Counter.pop" --op '{"type":"increment","path":["count"],"by":1}'
{ "ids": [] }

The window now shows 2. A named action lets an agent reuse your app’s behavior; apply and batch are available for direct field edits. Edit ▸ Undo can undo agent edits. See the Checklist walkthrough for commands with arguments and stable row IDs.