Studio Kit: the SDK for Studio apps
What builders use to make and change Studio apps: one folder of web files, the Kit's helpers, live links, setups and the rules that keep end users safe.
What a Studio app is
A Studio app is one folder of plain web files: an index.html and a plugin.json that names it. The same folder runs in Studio on the phone (Android and iPhone), on the web, on a computer, and on a TV through a screen link. There is no build step and nothing to compile.
Studio Kit is the SDK those apps share: the look, the five languages, sheets and tabs, dates and money, photos read by AI, reminders, live links that customers answer from any browser, the business profile, payments, TV screens and setups. Every app in the Gallery is built on it, so a builder starts from a working app and changes what matters.
- Builders who don't code change any Gallery app by asking in plain words (Make or change apps); Studio writes Kit code for them.
- Builders who code edit the files directly, using the reference below.
- Either way, every version is kept in History and can be restored.
A first app
Two files. plugin.json:
{
"name": "Visits",
"icon": "👋",
"entry": "index.html",
"version": 1,
"kit": 1,
"permissions": []
}index.html. The first line in <head> makes sure a recent enough Kit is on the page: Studio loads the Kit in front of every app that says "kit": 1, and the line fetches the current one when the copy on the device is older.
<!doctype html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<script>(window.kit&&kit.rev>=37)||document.write('<link rel=stylesheet href=https://priorstates.com/palmtop/webapp/kit/1/kit.css><script src=https://priorstates.com/palmtop/webapp/kit/1/kit.js><\/script>')</script>
<title>Visits</title></head>
<body>
<div class="bar"><h1 id="h"></h1></div>
<button id="add"></button>
<div class="list" id="list"></div>
<script>
const S = kit.strings({
en: { h: 'Visits', add: '+ Add a visit', n: '%s visits' },
es: { h: 'Visitas', add: '+ Añadir una visita', n: '%s visitas' },
});
const $ = kit.$, el = kit.el;
let visits = kit.load('visits', []); // forge.kv as JSON
function draw() {
$('#h').textContent = S.h;
$('#list').replaceChildren(...visits.map((v) =>
el('div', 'item', [el('div', 'mid', [el('b', null, v.name), el('small', null, kit.fmtDay(v.day))])])));
}
$('#add').textContent = S.add;
$('#add').onclick = () => {
const name = el('input');
kit.sheet({ title: S.add, body: name, actions: [{ label: 'OK', onClick: () => {
if (!name.value.trim()) return false; // false keeps the sheet open
visits.unshift({ name: name.value.trim(), day: kit.today() });
kit.save('visits', visits); draw();
} }] });
};
draw();
</script></body></html>A key missing in a language falls back to English. Studio's own language choice decides kit.lang: en, zh, zh-hant, es or pt.
plugin.json
name,icon(one emoji),description,entry(usuallyindex.html),version(a whole number; raise it with every release).kit:1to have Studio load the Kit in front of the app.permissions: what the app may use, shown to anyone who installs it. Declare only what the app needs:ai,reminders,notify,net(web requests),read-apps(another app's data, with the person's yes),camera,microphone,location. An undeclared feature is blocked, even when the phone allows Studio.reads: the apps whose data this app may ask for, e.g.["receipt-keeper"](withread-apps).wide:truefor an app worth a whole window on a computer (a table, a rota, a board). One page still serves both widths.owner_only: kv keys and files that belong to the app's owner and never travel when the app is shared.
What Studio gives every app (forge)
Under the Kit is forge (also window.palmtop), injected before the app's scripts run:
forge.kv.get(k)/set(k, v)/getJSON(k, d)/setJSON(k, v)/keys(): the app's own storage.forge.files.read(path)/write(path, text)/list();forge.files.save(name, text, type)hands a file to the person (the share sheet, or a download).await forge.ai(prompt, system, {images})→{ok, text}: one turn with the AI the person uses in Studio (the free starter AI or their own key). Needsai.await forge.http(url, {method, headers, body}): an https request. Needsnet.forge.share({title, text, url}),forge.open(url): the phone's share sheet, and a link in the right app.forge.apps.open(id, data)andawait forge.apps.opened(): hand something to another app.forge.onBack(() => handled): Back closes the app's own top layer first.forge.toast(msg),forge.notify(title, body),forge.theme(),await forge.dictate({lang})(speech to text, needsmicrophone),await forge.scanQR().
Words and layout
kit.strings({en, zh, 'zh-hant', es, pt}),kit.fmt('%s of %s', a, b),kit.lang,kit.locale.kit.$,kit.$$,kit.el(tag, className, textOrChildren): build the page without HTML strings.kit.sheet({title, body, actions: [{label, kind, onClick}]})→{el, close}: a bottom sheet; Back closes the newest.kindis'ghost'or'danger'; anonClickthat returnsfalsekeeps it open.kit.tabs([{id, icon, label, panel}], {onChange})→{select, current, badge}: a bottom tab bar that remembers the open tab.kit.split(root, {list, detail, empty}): list on the left and details on the right from 900 px wide.kit.toast(msg),kit.undo(msg, onUndo),kit.confirmTap(button, fn)(a second tap confirms),kit.menu(items)(a ⋯ button),kit.chart.bars(el, rows).- CSS classes from
kit.css:.bar,.card,.list/.item,.stats/.stat,.k-chips,.empty,.muted; buttons are primary by default, with.ghost,.danger,.big,.small. Light and dark follow the phone or Studio.
Data helpers
kit.load(key, default)/kit.save(key, value): JSON inforge.kv.- Dates as
'YYYY-MM-DD':kit.today(),kit.addDays(k, n),kit.addMonths(k, n),kit.daysUntil(k),kit.fmtDay(k, opts),kit.parseDay(k). - Money and numbers:
kit.currency(),kit.money(n, cur),kit.num(n),kit.parseAmount('1.234,5'). kit.search(items, query, fields): every word must match; case and accents are ignored.kit.csv(name, header, rows): a CSV that spreadsheets open, safe from formula injection.kit.zip(files)andkit.saveBytes(name, bytes, type)for a zip.kit.streak(doneDays),kit.categories(one list of business categories in five languages).
Pictures and AI
await kit.photos({multiple})→[File]: Take a picture or Choose photos.kit.thumb(file, {side, maxBytes})→ a small JPEG;kit.thumbs.set/get/delkeep them per item.await kit.ai.json(prompt, system, {images})→ a parsed object, or an error with a readable message. Ask for JSON in the system text, and let the person check what was read before saving.kit.addFrom({title, hint, one, read, same, done}): one + Add that offers type one, choose photos or take a picture. Pictures are gathered as pages;read(file, {i, n, prev})returns the items on one page;same(a, b)drops duplicates;done(items)shows your check list.- A
kit.sheetform of two or more fields gets 📷 Fill from a photo by itself when the app hasai; passphoto: falseto leave it out.
kit.addFrom({ title: 'Add products', one: { label: '✏️ Type one', onClick: () => edit(null) },
read: (file) => kit.ai.json('Read this price list.', 'Reply with JSON only: {"items": [{"name": string, "price": number}]}', { images: [file] })
.then((r) => r.items || []),
same: (a, b) => a.name === b.name && a.price === b.price,
done: (items) => checkList(items) });Reminders
await kit.reminders.plan([{id, at, title, body}]) keeps the phone's reminders for this app in line with the list (the soonest 10). It answers {mode, covered, next}; mode is 'unsup' where reminders can't be scheduled, so show a line saying so. Needs reminders.
Live links: people answer from any browser
A live link is an item (an order form, an invite, a menu) that people open in their browser with nothing to install. Their answers are sealed so only the app's owner can read them.
- Owner:
await kit.live.create(appId, data, {private: true})→{id, link};kit.live.update(id, data, {closed});await kit.live.answers(id);kit.live.setStatus(id, rid, {state, note})(the person sees it on their page);kit.live.remove(id). kit.live.follow(onChange, {ids}): new answers within about a second while the app is open.- The answer page (
answer.htmlin the app's folder):await kit.live.load()→{data, mine, status, closed}, andawait kit.live.answer(obj). - Customer-page pieces:
kit.customerHeader,kit.timeline,kit.confirmCard; questions withkit.live.askBoxand the owner'skit.live.threads.
Business profile, payments and QR codes
await kit.business.read()→ the person's business profile (name, logo, hours, contact, currency, pay methods), set once in the Business Profile app and read after their Allow. Needsread-appsand"reads": ["business-profile"].kit.business.draw(box)shows the card.kit.pay.links(pay, amount, cur, note)→ links for PayPal, Venmo, Cash App and a payment link, with the amount filled in;kit.pay.draw(box, o)is the whole "How to pay" block with I've paid. Apps never take card numbers.kit.qr(text, {px})→ an SVG QR code, no library needed.kit.contact.links(phone, {text, email}): Call, Text, WhatsApp and Email, prefilled.
Boards and TV screens
kit.screen.open({pages, every}): full screen, turning pages every few seconds;pagesis a function returning the page elements.kit.boardStyles: styles, backgrounds and font stacks for boards, with a contrast check.kit.fonts.use(['caveat'])loads one of the Kit's own fonts.kit.styleFromPicture: a board's style, layout and decorations from a picture the person likes, as plain data, with readable contrast.
Setups: deliver an app configured for a customer
A setup is a file of data, never code, that sets up one of your apps for a particular business: items, prices, options, words. Your app names the fields a setup may carry; anything else is left out and listed.
const SETUP = {
title: { t: 'str', max: 80 },
items: { t: 'list', max: 120, of: { t: 'obj', of: { name: { t: 'str', max: 80 }, price: { t: 'num', min: 0 } } } },
};
kit.preset.pick({ app: 'my-app', schema: SETUP,
summary: (d) => [(d.items || []).length + ' items'],
onUse: (d) => openEditorFilledWith(d) }); // let the person check before anything goes live
kit.preset.save('my-app', 'Saturday market', { title, items }, 'Your company');Rules are str, text, num, int, bool, enum, date, list and obj, with max, min and of. kit.preset.check(obj, app, schema) does the same without the sheet. Put only what one business may give another in a setup: never orders, customers, links or keys.
Working with other apps
await kit.sources.read(['receipt-keeper'])→{data, state}: other apps' saved data, only for those the person tapped Allow on.kit.sources.draw(box, state, {app, onAllow})shows Allow for the rest. Needsread-appsand the ids inreads.await kit.moneyRecords.read(ids): every sale, income and expense in those apps as one list; an app that holds money of its own publishes it withkit.moneyRecords.put(rows).
Versions
- Kit 1 only grows. A function never changes meaning; new ones are added with a higher
kit.rev. - An app that needs a newer helper raises the number in its first line (
kit.rev>=37above), so an older phone loads the current Kit from the site. - Raise
versionin plugin.json with every release of your app. Copies people changed are offered the update, never overwritten.
Rules that keep end users safe
- Text is text. Put anything a person or a file typed into the page with
textContentorkit.el, neverinnerHTML. - No secrets in apps. An app that needs a key asks each person for their own and keeps it in their
forge.kv. - Declare what you use. Permissions, the apps you read and the hosts you call are shown at install; undeclared ones are blocked.
- Apps from someone else run isolated. An app installed from another person's link or file runs in its own sealed frame: it can't read Studio's storage or other apps' data. Its own storage works as usual, through Studio.
- The person checks before it counts. What AI or a setup fills in goes to a form or a check list first.
Where to start
Pick the Gallery app closest to what your customer needs (all apps), add it, and change it: by asking in plain words, or by editing its files. Share it as a link or a file (Share), set it up for each business with a setup, or publish it to a team (Teams).