vibes.diy/docs Home Discord
creator docs · reference · generated

Keepsake — the thing they print, share or hand out

This is the same document the Vibes code generator reads when it builds apps — rendered from prompts/pkg/llms/keepsake.md on every deploy, so it can't drift from what the platform actually does. Read it from your terminal any time with npx vibes-diy skills.

The first frame is the keepsake and the fields that edit it, together. Fields beside it on a wide screen, below it on a phone, each keystroke redrawing it: the example's studio grid.

A keepsake is drawn with the reskin technique: one data-reskin wrapper re-scoping the look's tokens and one SKIN string rendered as its first child. The reskin doc loads with this one.

Tickets, invitations, menus, posters, certificates, place cards, a recipe card: the designed object is the point, and a page of Cards and RowCards around it makes it look like an admin screen. Build one framed artifact and let the components edit it. An app that keeps several (an invitation per party) lists them below the studio; picking one opens it there.

  • The keepsake is plain elements styled by the skin. One .keepsake element with a fixed shape (aspect-ratio: 5 / 7 for a card, 2 / 1 for a ticket, 1 / 1.414 for an A-size poster) holds the words as divs and spans with the app's own classes (.keepsake-title, .keepsake-meta), set in the display face, with the border, ornament, paper and stamp drawn in the SKIN string. It is never a Card, and its words are never Headings: the hook classes belong to the screen, the keepsake has its own.
  • The components are the editing controls around it. Field and Input for the words, Segmented for a variant (theme, size), Button for Print and Share. The keepsake opens filled with a believable example.
  • Print shows only the keepsake. A @media print block in the skin hides everything else and sets @page to the keepsake's size; the Print button calls window.print().
  • Its colours come from the wrapper's tokens, so the Look tab and a later reskin restyle the keepsake along with the controls.
jsximport React, { useState } from "react";
import { Button, Field, Input, Stack } from "@vibes.diy/ui";

const SKIN = `
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,wght@0,600;1,400&display=swap');
[data-reskin] .studio { display: grid; gap: 24px; padding: 16px; grid-template-columns: 1fr; }
@media (min-width: 48rem) { [data-reskin] .studio { grid-template-columns: 22rem 1fr; align-items: start; } [data-reskin] .studio > .keepsake { order: 2; } }
[data-reskin] .keepsake { aspect-ratio: 5 / 7; width: min(100%, 26rem); margin-inline: auto; padding: 12%; display: grid; place-content: center; gap: 14px;
  text-align: center; background: var(--surface); color: var(--ink); outline: 1px solid var(--ink); outline-offset: -14px;
  box-shadow: 0 18px 40px color-mix(in srgb, var(--ink) 18%, transparent); font-family: 'Fraunces', serif; }
[data-reskin] .keepsake-eyebrow { font-style: italic; color: var(--text-secondary); }
[data-reskin] .keepsake-title { font-size: clamp(28px, 7vw, 44px); font-weight: 600; line-height: 1.05; }
[data-reskin] .keepsake-rule { width: 40%; height: 1px; margin-inline: auto; background: var(--purple); }
@media print {
  @page { size: 5in 7in; margin: 0; }
  body:has([data-reskin]) * { visibility: hidden; }
  [data-reskin] .keepsake, [data-reskin] .keepsake * { visibility: visible; }
  [data-reskin] .keepsake { position: fixed; inset: 0; width: 100%; box-shadow: none; }
}
`;

export default function App() {
  const [title, setTitle] = useState("Maya turns thirty");
  const [when, setWhen] = useState("Saturday 14 June · 7pm · The Rooftop");

  return (
    <div data-reskin>
      <style>{SKIN}</style>
      <div className="studio">
        <div className="keepsake">
          <span className="keepsake-eyebrow">You are invited</span>
          <span className="keepsake-title">{title}</span>
          <span className="keepsake-rule" />
          <span>{when}</span>
        </div>
        <Stack gap={3}>
          <Field label="Headline">{(id, d) => <Input id={id} describedBy={d} value={title} onChange={setTitle} />}</Field>
          <Field label="When and where">{(id, d) => <Input id={id} describedBy={d} value={when} onChange={setWhen} />}</Field>
          <Button onClick={() => window.print()}>Print the invitation</Button>
        </Stack>
      </div>
    </div>
  );
}