Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/dev-os/llms.txt

Use this file to discover all available pages before exploring further.

The Contact app makes reaching out feel native to the DevOS environment by wrapping a contact form in the chrome of a classic desktop email client. The layout copies the familiar three-part anatomy of Microsoft Outlook’s compose window: a toolbar of action buttons across the top, a form with labelled fields in the centre, and an address book sidebar on the right. Under the hood it is a standard React controlled form with a Framer Motion animation that plays on submit.

Layout

The app is divided into three horizontal layers:

Toolbar

bg-gray-200 border-b border-gray-300 — contains three buttons:
ButtonIconNotes
SendSend (lucide-react)Submits the form; disabled while isSending or isSent is true
AttachPaperclip (lucide-react)Rendered but not wired to file input
CCUsers (lucide-react)Rendered but not wired to any action
A vertical w-px h-4 bg-gray-400 divider separates the Send button from Attach and CC.

Form + Sidebar

The main body is a flex row containing the compose form on the left and the address book on the right. Compose form (bg-white flex-1) — three fields stacked vertically, each separated by a border-b:
FieldTypeDefault / Placeholder
Totext inputhello@devos.local (pre-filled, readOnly)
Subjecttext inputPlaceholder: "Let's work together!" (required)
Body<textarea>Placeholder: "Write your message here..." (required, font-serif)
The To: field uses readOnly so visitors cannot change the recipient address. The label spans are w-16 text-sm text-gray-500. Address Book sidebar (w-48 bg-gray-50 border-l border-gray-200) — visible only on sm: screens and above (hidden sm:block). Contains three anchor links styled in text-os-teal font-medium:
  • GitHubhref="#" (replace with your profile URL)
  • LinkedInhref="#" (replace with your profile URL)
  • Twitterhref="#" (replace with your profile URL)

Submit Flow

Submitting the form (either via the toolbar Send button or the native form submit) triggers a three-stage animation sequence managed by two boolean state variables: isSending and isSent.
User submits


isSending = true  ──►  Framer Motion Send icon animates
                        (flies to top-right corner)

     │ after 1 000 ms

isSending = false
isSent    = true  ──►  "Message Sent!" overlay appears

     │ after 3 000 ms

isSent = false    ──►  Form resets to initial state
The Framer Motion animation uses AnimatePresence wrapping a motion.div:
<motion.div
  initial={{ opacity: 0, scale: 0.5, x: 0, y: 0 }}
  animate={{ opacity: 1, scale: 1, x: 500, y: -500 }}
  exit={{ opacity: 0 }}
  transition={{ duration: 1, ease: 'easeInOut' }}
  className="absolute inset-0 pointer-events-none flex items-center justify-center z-50"
>
  <Send size={64} className="text-os-teal" />
</motion.div>
The success overlay is a bg-white/80 backdrop-blur-sm full-cover div (z-40) containing a card with "Message Sent!" in text-os-teal font-bold and a sub-line "I'll get back to you soon.".
The current handleSubmit function is a simulation — no data is sent anywhere. The form will appear to succeed for every submission. To send real messages, wire handleSubmit to a third-party service such as Formspree, EmailJS, or your own API endpoint before deploying to production.

Customising the Contact App

Change the recipient address — find the To: input’s value prop and replace "hello@devos.local" with your real email address:
<input type="text" value="you@yourdomain.com" readOnly ... />
Wire up social links — replace the href="#" values on the three address book anchors:
<a href="https://github.com/yourusername" ...>GitHub</a>
<a href="https://linkedin.com/in/yourprofile" ...>LinkedIn</a>
<a href="https://twitter.com/yourhandle" ...>Twitter</a>
Connect to a real email service — replace the body of handleSubmit with a fetch call to your chosen provider. Because the Subject and Body fields are uncontrolled inputs, read their values from the form element via e.target. Example using Formspree:
const handleSubmit = async (e) => {
  e.preventDefault();
  setIsSending(true);
  const data = new FormData(e.target);
  await fetch('https://formspree.io/f/YOUR_FORM_ID', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      subject: data.get('subject'),
      message: data.get('message'),
    }),
  });
  setIsSending(false);
  setIsSent(true);
  setTimeout(() => setIsSent(false), 3000);
};
Add matching name attributes to the Subject input (name="subject") and the Body textarea (name="message") so FormData can read them. Show the address book on mobile — remove hidden sm:block from the sidebar className if you want it visible at all screen sizes.

Build docs developers (and LLMs) love