# Michael C. Hurley: full site as Markdown > 20 Years of SEO, Now AEO/GEO · Technical Web · AI Agent Tooling. The resume, every page, the portfolio, and every published blog post on https://www.michaelchurley.com, generated from the site's source data. Short version: https://www.michaelchurley.com/llms.txt. Agent guide: https://www.michaelchurley.com/agents.md --- # Michael C. Hurley Canton, NC · [828-593-1935](tel:+18285931935) · [michaelmonetized@gmail.com](mailto:michaelmonetized@gmail.com) · [michaelchurley.com](https://www.michaelchurley.com) · [LinkedIn](https://www.linkedin.com/in/michaelchurley) · [GitHub](https://github.com/michaelmonetized) · [X](https://x.com/michaelh_rley) ## 20 Years of SEO, Now AEO/GEO · Technical Web · AI Agent Tooling Search and growth operator with 20 years of SEO (2005–present), now focused on AEO and GEO. Founded the local SEO agency studioTWELVE, ran all operations at SEO agency White Fox Studios for nine years, did SEO for his own restaurant, owned marketing and martech as CTO of a real estate SaaS, and now leads SEO at Hustle Launch. Builds the sites being optimized: production Next.js, Convex, and Stripe sites plus the AI agent tooling around them, and writes and teaches technical SEO for answer engines and browsing agents. ## Core Skills - **AEO / GEO:** llms.txt and agent-readable (Markdown) content, AI crawler policy (training vs. retrieval bots), JSON-LD structured data and entity markup (Organization, Person, FAQPage, sameAs), citation-focused content structure, measuring AI citations and referrals - **Technical SEO:** server rendering vs. client-rendered content, crawlability and indexation, sitemaps and robots.txt, information architecture and topic clusters, page performance, local SEO and Google Business Profile, directory citations and NAP consistency - **Analytics & reporting:** PostHog, Ahrefs, SpyFu, first-party event tracking, user-intent analysis to prioritize roadmaps - **AI & agents:** MCP-based agent harnesses, multi-agent orchestration, token-efficient HTML-to-Markdown ingestion for LLMs, OpenRouter, Claude Code / Cursor workflows - **Engineering:** TypeScript, Next.js (App Router, SSR), TanStack Start, React, Convex, Vercel, Stripe API, Clerk, Bash, PHP, Swift, Rust ## Experience ### Director, [Hustle Launch](https://www.hustlelaunch.com) **Feb 2024 – Present · Performance marketing and web design agency serving businesses in FL, SC, GA, NC, and TN** - Lead SEO and local search delivery for client accounts: Google Business Profile, directory listings and citations, link building, and editorial placements. - Wrote a full technical SEO and GEO audit and phased roadmap for hustlelaunch.com covering structured data gaps, rendering, IA and topic clusters, and AI crawler policy, sequenced into a 4–6 week foundation sprint and a 6–10 week architecture and performance phase. - Built a 50+ directory NAP citation checklist used for client local-SEO audits. - Built the Assessment Toolbar Chrome extension (2024) that opens the current page in Google's Rich Results Test, the Schema.org validator, PageSpeed Insights, WAVE, SpyFu, and other audit tools in one click. - Build and maintain client and agency sites on Next.js, Convex, and Vercel, plus My.HustleLaunch.com, an AI marketing SaaS with a Buffer integration. ### Contract Product Engineer, Zaxby's Franchising LLC **Aug 2024 – Feb 2026 · Sylva, NC, then Waynesville, NC · Martech, product engineering, and DevOps for the Zaxby's "Zamily" franchise network** - Built and shipped YourZaxbys local store marketing (LSM) and operations software that ZFL franchise owners could license for their stores. - Contracted to train on site first at the Sylva, NC store (Aug 2024 – Mar 2025), then in Waynesville, NC, learning store workflows firsthand before building the software. - LSM: store-level pages (menu, catering, events, community, careers) with location-specific metadata, event promotions, and a guest satisfaction survey; local growth playbook covering Google Business Profile, events, and partnerships. - Ops: live shift dashboard (sales, labor % vs. goal, speed of service, COGS %, SMG guest satisfaction) fed by PAR, Crunchtime, Berry-AI, and SMG CSV ingestion; above-store dashboards for stores, schedules, audits, and reports; hiring, onboarding with encrypted SSNs, checklists, and Steritech tracking. - Stack and DevOps: Next.js, Convex, Clerk, Recharts, Vercel, CI gates, Sentry, PostHog, Resend. Demos: wwwyourzaxbyscom.vercel.app, waynesvilleyourzaxbyscom.vercel.app ### Founder, [HurleyUS / Michael Monetized](https://www.hurleyus.com) **Current · Independent products and open source** - Technical SEO in the Age of Agentic AI (Sep 2026): 68-page, 10-chapter field guide on pages that people and browsing agents can understand and use (rendering, structured data and entity facts, crawler access policy, measurement), with a workbook, four sector playbooks, and a Python page-inspector toolkit with tests. - url-to-md: CLI that converts HTML pages to clean Markdown for LLM ingestion, cutting token usage by about 40% compared with raw HTML. - shagent / agent-os: Bun and TypeScript agent harness using MCP and OpenRouter, and a shell-native multi-agent orchestrator with a real-time WebSocket dashboard. - stripe-convex: reusable Stripe + Convex payments package (checkout, cart, coupons, 19 webhook events). - Ship and run production sites on Next.js, Convex, Clerk, Stripe, PostHog, Sentry, and Vercel, including michaelchurley.com, bestwnc.com (Western NC local business directory), and uncap.us. ### Chief Technology Officer, Realay.com (Kaibo, LLC) **Mar 2023 – May 2024 · Real estate SaaS platform · Overlapped White Fox Studios and Hustle Launch** - Owned product engineering plus all marketing and martech development for a real estate SaaS platform, while the sales side handled outbound sales and partner onboarding. - Took the product from a WordPress MVP to a React build. ### Director of Operations, White Fox Studios **Feb 2015 – Feb 2024 · SEO agency** - The owner's only direct report; ran all agency operations: SEO strategy and delivery, sales, marketing, client delivery, and growth; built internal software and client systems. - Built and still maintain the Rotary Swing iOS app. ### Earlier SEO and Marketing Roles - **Franchise Partner (LSM), Papa John's franchise partnership** (May 2014 – Feb 2015): Franchise partnership that dissolved; the role included local store marketing (LSM). - **Co-Owner / Operator, Hurley's Creekside Dining & Rhum Bar**, Maggie Valley, NC (Jul 2010 – May 2014): Rebranded and relaunched his restaurant, ran operations, marketing, and SEO; grew annual revenue from $1.3M to $5.33M. - **Founder, studioTWELVE**, South Florida (2007 – 2010): Web design and local SEO for small businesses in South Florida; grew annual net profit from $50K to $387K. - **Production Manager, Signs R Us** (2005 – 2007): Production management with SEO as part of the role. - **Software Developer and Systems Administrator, Lowcountry Today** (2005 – 2009): COBOL and ColdFusion applications, Windows Server administration, banner ad design. ## Education - **B.S. Computer Science**, College of Charleston (2003–2007) - **A.A. Commercial Graphics**, Trident Technical College (2001–2003) --- # Book a Meeting Source: https://www.michaelchurley.com/book Schedule a 30-minute call with Michael C. Hurley to discuss business opportunities, technology projects, or collaboration ideas. - Booking form: https://www.michaelchurley.com/book - Email: [michaelmonetized@gmail.com](mailto:michaelmonetized@gmail.com) - Call or text: [828-593-1935](tel:+18285931935) --- # Portfolio Source: https://www.michaelchurley.com/portfolio Sites, interfaces, and marks by Michael C. Hurley. Machine-readable: https://www.michaelchurley.com/portfolio.json ## Marks - **Bar-B-Que Wagon** · [Media](https://www.michaelchurley.com/work/art-barbquewagon.png) · [Live](https://www.barbquewagon.com) - **de la Terre** · [Media](https://www.michaelchurley.com/work/art-delaterre.png) · [Live](https://www.delaterrestore.com) - **Best of WNC Icon** · [Media](https://www.michaelchurley.com/work/art-bestwnc-icon-alive.mp4) - **Best of WNC Logo** · [Media](https://www.michaelchurley.com/work/art-bestwnc-logo-alive.mp4) · [Live](https://www.bestwnc.com) - **Hurley US** · [Media](https://www.michaelchurley.com/work/art-hurley-shield-alive.mp4) · [Live](https://www.hurleyus.com) - **Hustle Launch** · [Media](https://www.michaelchurley.com/work/art-hustle-launch-script.webp) · [Live](https://www.hustlelaunch.com) - **Hustle Launch Star** · [Media](https://www.michaelchurley.com/work/art-hustle-launch-star-alive.mp4) · [Live](https://www.hustlelaunch.com) - **Kings Lion** · [Media](https://www.michaelchurley.com/work/art-kings-lion-alive.mp4) · [Live](https://kingsroofingnc.com) - **Monarch Logo** · [Media](https://www.michaelchurley.com/work/art-monarch-logo-alive.mp4) · [Live](https://www.monarchmountainfoundations.com) - **omadesign** · [Media](https://www.michaelchurley.com/work/art-omadesign.png) · [Live](https://www.michaelchurley.com/omadesign) - **Wicked Fresh Truck** · [Media](https://www.michaelchurley.com/work/art-wicked-fresh-truck-alive.mp4) - **Wicked Fresh Sign** · [Media](https://www.michaelchurley.com/work/art-wicked-fresh-sign-alive.mp4) - **Mountain Heritage Builders** · [Media](https://www.michaelchurley.com/work/art-mountain-heritage-alive.mp4) - **Lilly & Linen** · [Media](https://www.michaelchurley.com/work/art-lilly-linen-alive.mp4) - **M Splash** · [Media](https://www.michaelchurley.com/work/art-m-splash-alive.mp4) - **Everything Monetized** · [Media](https://www.michaelchurley.com/work/art-everything-monetized-alive.mp4) ## Sites - **Appalachian Estate Sales** · [Media](https://www.michaelchurley.com/work/web-appestatesales.mp4) · [Live](https://www.appestatesales.com) - **Best Jeep Decals** · [Media](https://www.michaelchurley.com/work/web-bestjeepdecals.mp4) · [Live](https://www.bestjeepdecals.com) - **Best of WNC** · [Media](https://www.michaelchurley.com/work/web-bestwnc.mp4) · [Live](https://www.bestwnc.com) - **de la Terre** · [Media](https://www.michaelchurley.com/work/web-delaterrestore.mp4) · [Live](https://www.delaterrestore.com) - **DJ Side Three** · [Media](https://www.michaelchurley.com/work/web-djsidethree.mp4) · [Live](https://www.djsidethree.com) - **Get At Me** · [Media](https://www.michaelchurley.com/work/web-getatme.mp4) · [Live](https://getat.me) - **Glass Design System** · [Media](https://www.michaelchurley.com/work/web-glass-design-system.mp4) · [Live](https://glass-design-system.vercel.app) - **Hurley US** · [Media](https://www.michaelchurley.com/work/web-hurleyus.mp4) · [Live](https://www.hurleyus.com) - **Hustle Launch Showreel** · [Media](https://www.michaelchurley.com/work/web-hustlelaunch-showreel.mp4) · [Live](https://www.hustlelaunch.com) - **Hustle Launch TV Ads** · [Media](https://www.michaelchurley.com/work/web-hustlelaunch-tvads.mp4) · [Live](https://www.hustlelaunch.com) - **Hustle Launch** · [Media](https://www.michaelchurley.com/work/web-hustlelaunch.mp4) · [Live](https://www.hustlelaunch.com) - **Kings Roofing** · [Media](https://www.michaelchurley.com/work/web-kingsroofing.mp4) · [Live](https://kingsroofingnc.com) - **Mack's BBQ Shack** · [Media](https://www.michaelchurley.com/work/web-macksbbqshack.mp4) · [Live](https://www.macksbbqshack.com) - **michaelchurley.com** · [Media](https://www.michaelchurley.com/work/web-michaelchurley.mp4) · [Live](https://www.michaelchurley.com) - **Modern Design Playground** · [Media](https://www.michaelchurley.com/work/web-modern-design-playground.mp4) · [Live](https://mdp-seven.vercel.app) - **Monarch Mountain Foundations** · [Media](https://www.michaelchurley.com/work/web-monarch.mp4) · [Live](https://www.monarchmountainfoundations.com) - **SantaBox** · [Media](https://www.michaelchurley.com/work/web-santabox.mp4) · [Live](https://www.santabox.org) - **The National NC** · [Media](https://www.michaelchurley.com/work/web-thenationalnc.mp4) · [Live](https://www.thenationalnc.com) - **Twelve UX** · [Media](https://www.michaelchurley.com/work/web-twelveux.mp4) · [Live](https://twelveux.vercel.app) - **Uncap** · [Media](https://www.michaelchurley.com/work/web-uncap.mp4) · [Live](https://uncap.us) - **Jennings Custom Homes** · [Media](https://www.michaelchurley.com/work/web-jennings.mp4) · [Live](https://www.jenningscustomhomes.com) - **Go Metal** · [Media](https://www.michaelchurley.com/work/webstill-Go-Metal-alive.mp4) - **Realay** · [Media](https://www.michaelchurley.com/work/webstill-Realay-alive.mp4) - **SalesPromis** · [Media](https://www.michaelchurley.com/work/webstill-Sales-Promis-alive.mp4) ## Interfaces - **Book Slots** · [Media](https://www.michaelchurley.com/work/ui-book-slots-alive.mp4) - **Naarchy Home** · [Media](https://www.michaelchurley.com/work/ui-naarchy-home-alive.mp4) - **Naarchy Clipboard** · [Media](https://www.michaelchurley.com/work/ui-naarchy-clipboard-alive.mp4) --- # A Quiet Revolution in Local Marketing Source: https://www.michaelchurley.com/vizible Official partnership with Vizible Agency: a Personal CMO, a real-time Business Dashboard, and an All-in-One CRM for local businesses. No contracts. - 30+ Years: Serving Small Biz - Zero Churn: Client Retention - No Contracts: Month to Month - Award-Winning: Platform ## Three Things That Convinced Me ### 01. Your Own Chief Marketing Officer A real strategist who learns your business, builds your plan, reviews numbers with you weekly, and executes every campaign. Not a chatbot — a person. - Custom marketing strategy - Weekly data reviews - Brand development - SEO, social, ads, email expertise > Matt my CMO takes me through the numbers and I finally get it. ### 02. The Business Dashboard #1 marketing analytics dashboard for local businesses. Rankings, reviews, social, leads, campaigns — one screen. No more logging into seven platforms. - Real-time performance metrics - Profile management - Client communication hub - AI content generation > I trust what I can see and it’s all in one place. ### 03. All-in-One CRM Built for local businesses, not enterprise. Leads, tasks, interactions, scheduling, reporting — integrates with existing tools or replaces what isn’t working. - Leads with full history - Task & team workflows - Calendar scheduling - Pipeline analytics > Finally I have found what I was looking for. A marketing relationship! ## Everything Else, Handled - **Local Search & SEO:** Listing management, keyword tracking, AI business profiles, 60+ directories. - **Reputation Mgmt:** AI review monitoring, sentiment analysis, competitive benchmarking. - **Social Media:** Scheduling, posting, engagement monitoring, AI sentiment analysis. - **AI Assistant:** Web chat lead capture, Instagram/FB integration, SMS, shared inbox. - **AI Phone Agent:** 24/7 AI receptionist, multi-language, lead capture, call summaries. - **Email & SMS:** AI email builder + SMS campaigns with 98% open rates. - **Paid Advertising:** Managed local ads, multi-platform, performance tracking. - **Website Design:** From $2,497 for 3-page sites to full e-commerce builds. ## What Business Owners Say > I love the fact that I have a personal connection that helps guide me, strategize with me and then do the work for me. This truly is the best marketing technology and I want to be on the top of Google local search. > > Chauncey Porter, Porter Insurance Group, Memphis, TN > I love the communication I have with Matt C. my Chief Marketing Officer at Vizible. He has helped me gain more reviews, improve my ranking and I actually have had new customers say they chose me because of my reviews online. > > Paula Marez, Flywheel Ventures, Santa Fe, NM > Finally I have found what I was looking for. A marketing relationship! My CMO is doing the work for me, keeps me in the loop and every month we discuss the results. Folks this is the real one! > > Frankie V., Full Screen Productions, LLC > With the Vizible Marketing AI Assistant, we transformed our lead capture process. Its ability to engage with potential clients through web chat and Facebook Messenger has not only increased our response rate but also provided a seamless experience that turns leads into loyal customers. It’s like having a 24/7 marketing team at our fingertips! > > Jim S., Dental Practice Owner, Sarasota, FL ## Plans & Pricing ### Vantage: Build Your Foundation ($497/mo) - Local SEO & Listings - Access to Business Portal - Listing Sync Pro & Distribution - Local SEO Reporting - AI Generated Business Profiles ### Visionary: Strategy & Execution ($997/mo) - Everything in Vantage - Personal CMO Guidance - AI Assistant (Chat Lead Capture) - Reputation Management - Social Media Posting & Reporting ### Velocity: Lead Generation Engine ($1,497/mo) - Everything in Visionary - SMS Marketing - Email Marketing - AI Agent (Voice Receptionist) - Advanced Engagement Tools ### Viral: Full Growth Engine (Custom) - Everything in Velocity - Paid Advertising Campaigns - Vizible Leads Analytics - Advanced CRM Implementation - Custom API Integrations ## Get started Email michael@hurleyus.com or call or text 828-593-1935. --- # omadesign: your Linux, for making things Source: https://www.michaelchurley.com/omadesign Native Linux studio for design, paint, and photograph. No Electron. This page embeds the omadesign lander. - Lander: https://michaelmonetized.github.io/omadesign/ - Source and releases: https://github.com/michaelmonetized/omadesign - Blog posts: https://www.michaelchurley.com/blog.md --- # How this site is built for AI search and agents Source: https://www.michaelchurley.com/aeo This site is built to be read correctly by search engines, AI answer engines, and browsing agents, not only by people. Every surface below is live and generated from the same source data as the human pages, so the machine versions cannot drift from what visitors see. ## Crawler access and discovery - **[robots.txt with Content-Signal](https://www.michaelchurley.com/robots.txt)**: Allows search and answer-engine crawlers (OAI-SearchBot, ChatGPT-User, GPTBot, PerplexityBot, ClaudeBot, Google-Extended, and others) and user-triggered agents; keeps private paths out; blocks CCBot. Why: Retrieval bots have to be allowed before a page can be cited. Content-Signal (search=yes, ai-input=yes, ai-train=no) separates being cited from being used for training. - **[sitemap.xml](https://www.michaelchurley.com/sitemap.xml)**: Every public page and post with a lastmod taken from the content itself. Why: Accurate lastmod tells crawlers what changed so fresh content is recrawled first. - **[IndexNow key](https://www.michaelchurley.com/indexnow-key.txt)**: Key file for IndexNow, plus a script that pings changed URLs after a deploy. Why: Bing and other IndexNow engines feed several AI answer products; pinging shortens the time to index. - **[Canonical URLs](https://www.michaelchurley.com/)**: Every HTML page declares its own canonical URL. Why: One URL per piece of content keeps ranking and citation signals from splitting across duplicates. ## Content for language models - **[llms.txt](https://www.michaelchurley.com/llms.txt)**: The resume in llms.txt format: H1, a quotable summary, the resume sections, and links to every machine endpoint. Why: A short, curated entry point a model can read in one pass to answer who Michael is and what he does. - **[llms-full.txt](https://www.michaelchurley.com/llms-full.txt)**: The whole site as one Markdown file: resume, every page, the portfolio, and every published post. Why: Lets an agent load all of the context at once instead of crawling page by page. - **[Markdown for every page](https://www.michaelchurley.com/index.md)**: Append .md to any path (/blog.md, /portfolio.md, /blog/.md), or request any page with Accept: text/markdown. Pages advertise it with a Link header and , and the footer links to it. Why: Markdown uses far fewer tokens than rendered HTML and has no layout noise, so agents quote the content, not the chrome. - **[Answer-first home page](https://www.michaelchurley.com/#who-is-michael-c-hurley)**: A "Who is Michael C. Hurley?" section near the top with a self-contained summary and stable heading ids. Why: Answer engines lift short, self-contained passages. Stable ids make the passage linkable. ## Structured data - **[JSON-LD](https://www.michaelchurley.com/)**: Person (sameAs, knowsAbout, hasOccupation, alumniOf), WebSite, and Organization site-wide; ProfilePage on the home page; BlogPosting and BreadcrumbList on posts; CollectionPage with CreativeWork and SoftwareSourceCode items on the portfolio; FAQPage on this page. Why: Entity markup states the facts (who, where, what, which profiles are the same person) so engines do not have to infer them. - **[resume.json](https://www.michaelchurley.com/resume.json)**: The resume in the JSON Resume schema. Why: A standard, typed format that resume tools and agents already parse. - **[portfolio.json and blog.json](https://www.michaelchurley.com/blog.json)**: Portfolio pieces and blog posts as JSON, each post with a link to its Markdown. Why: Lists an agent can filter without scraping. See also /portfolio.json. - **[RSS and JSON Feed](https://www.michaelchurley.com/feed.xml)**: The blog as RSS 2.0 (/feed.xml) and JSON Feed 1.1 (/feed.json). Why: Feeds are still how aggregators and many agents notice new posts. - **[OpenAPI](https://www.michaelchurley.com/openapi.json)**: OpenAPI 3.1 description of the JSON endpoints, the Markdown routes, the booking API, and the MCP endpoint. Why: Agents and tool builders can generate a client instead of guessing request shapes. ## Tools for agents - **[agents.md](https://www.michaelchurley.com/agents.md)**: A usage guide for AI agents: every endpoint, the .md convention, crawler policy, the MCP and WebMCP tools with their inputs and outputs, and the booking flow. Why: One document that tells an agent exactly how to use the site. The footer on every page points to it. - **[Remote MCP server](https://www.michaelchurley.com/mcp/server-card)**: A Model Context Protocol server at /mcp (Streamable HTTP) with tools for the resume, portfolio, blog, page Markdown, search, and booking. Discovery via /mcp/server-card, /.well-known/mcp/server-card.json, /.well-known/mcp.json, and /.well-known/ai-catalog.json. Why: Agents that speak MCP can use the site as a tool server without a browser. - **[WebMCP](https://www.michaelchurley.com/agents.md)**: The same tools registered in the browser through the WebMCP API (document.modelContext) when the browser supports it, sharing one implementation with the MCP server. Why: Browser agents can call typed tools instead of clicking through the UI. - **[Agent booking](https://www.michaelchurley.com/book)**: get_booking_options, get_availability, and book_meeting use the same rules, validation, rate limit, and confirmation emails as the /book form. Why: An agent acting for a recruiter can schedule a call end to end. ## FAQ ### How can an AI agent read this site? Start with https://www.michaelchurley.com/agents.md. For content, append .md to any page URL or send Accept: text/markdown. For everything at once, use https://www.michaelchurley.com/llms-full.txt. ### Can an agent book a call with Michael? Yes. Use the MCP server at /mcp or the WebMCP tools: get_booking_options, then get_availability, then book_meeting with a slot_start, name, and email. The visitor gets a confirmation email with a calendar invite. ### Is the content allowed in AI answers? Yes for search and answers (Content-Signal: search=yes, ai-input=yes). Training use is opted out (ai-train=no), and CCBot is blocked. ### Why does the site publish the same content in several formats? Each format fits a different reader: HTML for people, Markdown for language models, JSON and JSON-LD for software, and MCP or WebMCP tools for agents. All of them are generated from the same source data. --- # Nightly · Daily Stand Up Source: https://www.michaelchurley.com/nightly Michael C. Hurley's daily stand-up: what shipped, how it landed, inbox signal, and open loops, plus productivity and audience trends. - Reports: 25 - Latest productivity: 0 - Latest report: [2026-09-26](https://www.michaelchurley.com/nightly/report/2026-09-26.md): Ships/index 0. Omadesign master: changelog backfill for 0.6.0 QA #128–#140 + closed matching issues. X: 1 post, 5 imp. Inbox: 4 Thumbtack leads, estate-sale form (client declined off-site), Vercel domain misconfigs, Macks reviews. ## Archive - [2026-09-26](https://www.michaelchurley.com/nightly/report/2026-09-26.md) - [2026-09-25](https://www.michaelchurley.com/nightly/report/2026-09-25.md) - [2026-09-24](https://www.michaelchurley.com/nightly/report/2026-09-24.md) - [2026-09-23](https://www.michaelchurley.com/nightly/report/2026-09-23.md) - [2026-09-22](https://www.michaelchurley.com/nightly/report/2026-09-22.md) - [2026-09-21](https://www.michaelchurley.com/nightly/report/2026-09-21.md) - [2026-09-20](https://www.michaelchurley.com/nightly/report/2026-09-20.md) - [2026-09-19](https://www.michaelchurley.com/nightly/report/2026-09-19.md) - [2026-09-18](https://www.michaelchurley.com/nightly/report/2026-09-18.md) - [2026-09-17](https://www.michaelchurley.com/nightly/report/2026-09-17.md) - [2026-09-16](https://www.michaelchurley.com/nightly/report/2026-09-16.md) - [2026-09-15](https://www.michaelchurley.com/nightly/report/2026-09-15.md) - [2026-09-14](https://www.michaelchurley.com/nightly/report/2026-09-14.md) - [2026-09-13](https://www.michaelchurley.com/nightly/report/2026-09-13.md) - [2026-09-12](https://www.michaelchurley.com/nightly/report/2026-09-12.md) - [2026-09-11](https://www.michaelchurley.com/nightly/report/2026-09-11.md) - [2026-09-10](https://www.michaelchurley.com/nightly/report/2026-09-10.md) - [2026-09-09](https://www.michaelchurley.com/nightly/report/2026-09-09.md) - [2026-09-08](https://www.michaelchurley.com/nightly/report/2026-09-08.md) - [2026-09-07](https://www.michaelchurley.com/nightly/report/2026-09-07.md) - [2026-09-06](https://www.michaelchurley.com/nightly/report/2026-09-06.md) - [2026-09-05](https://www.michaelchurley.com/nightly/report/2026-09-05.md) - [2026-09-04](https://www.michaelchurley.com/nightly/report/2026-09-04.md) - [2026-09-03](https://www.michaelchurley.com/nightly/report/2026-09-03.md) - [2026-09-02](https://www.michaelchurley.com/nightly/report/2026-09-02.md) --- # Agents: how to use michaelchurley.com Source: https://www.michaelchurley.com/agents.md This is the personal site of Michael C. Hurley (20 Years of SEO, Now AEO/GEO · Technical Web · AI Agent Tooling). It holds his resume, portfolio, blog, and a public daily stand-up log. Everything below is read-only and free to use for search, answers, and user-requested tasks. Training use is opted out (see Policy). ## Machine-readable endpoints | Path | What it returns | | --- | --- | | `/llms.txt` | This file: Michael's resume in llms.txt format | | `/llms-full.txt` | The whole site as Markdown: resume, every page, portfolio, and every blog post | | `/agents.md` | Usage guide for AI agents: endpoints, .md routes, MCP and WebMCP tools, booking, contact | | `/resume.json` | Resume in JSON Resume (jsonresume.org) format | | `/portfolio.json` | Portfolio pieces with titles, categories, media, and live links | | `/blog.json` | Blog posts with title, slug, URL, date, description, tags, and Markdown link | | `/resume.md` | Resume as Markdown | | `/feed.xml` | Blog as RSS 2.0 | | `/feed.json` | Blog as JSON Feed 1.1 | | `/openapi.json` | OpenAPI 3.1 description of the JSON endpoints, Markdown routes, booking API, and MCP endpoint | | `/mcp` | Remote MCP server (Streamable HTTP, POST JSON-RPC); Server Card at /mcp/server-card | | `/aeo` | How this site is built for AI search and agents (case study with live links) | | `/robots.txt` | Crawler policy with Content-Signal | | `/sitemap.xml` | Sitemap of public pages with lastmod | | `/.well-known/ai-catalog.json` | AI Catalog pointing to the MCP Server Card | All `.md` and `.txt` endpoints return `text/markdown` or `text/plain` (UTF-8). JSON endpoints return `application/json`. ## Markdown for any page Append `.md` to any page path to get that page's content as Markdown, generated from the same source data as the HTML page: - `/` -> `/index.md` - `/resume.md` (resume only) - `/blog` -> `/blog.md`, `/blog/` -> `/blog/.md` - `/portfolio` -> `/portfolio.md` - `/book` -> `/book.md`, `/vizible` -> `/vizible.md`, `/omadesign` -> `/omadesign.md` - `/aeo` -> `/aeo.md` - `/nightly` -> `/nightly.md`, `/nightly/report/` -> `/nightly/report/.md` Or request any page URL with `Accept: text/markdown`. HTML pages advertise their Markdown twin with a `Link: <...md>; rel="alternate"; type="text/markdown"` header and a matching `` tag. Every page footer also has a "Markdown" link to its `.md` version. Unknown paths return 404. ## Remote MCP server `POST https://www.michaelchurley.com/mcp`: a Model Context Protocol server over Streamable HTTP. It is stateless and answers with `application/json` (no SSE stream; `GET /mcp` returns 405). No authentication. - Protocol versions: `2026-07-28` (per-request `_meta`, `server/discover`, `MCP-Protocol-Version` / `Mcp-Method` / `Mcp-Name` headers validated) and the legacy `initialize` handshake for `2025-11-25`, `2025-06-18`, and `2025-03-26`. - Methods: `server/discover`, `initialize`, `ping`, `tools/list`, `tools/call`. - Results: `content` (text) plus `structuredContent` for JSON results; tool errors come back with `isError: true`. - Discovery: Server Card at `/mcp/server-card` (also `/.well-known/mcp/server-card.json` and `/.well-known/mcp.json`), AI Catalog at `/.well-known/ai-catalog.json`. Example (legacy handshake, then a call): ```bash curl -s https://www.michaelchurley.com/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \ -H 'MCP-Protocol-Version: 2025-11-25' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_booking_options","arguments":{}}}' ``` ## WebMCP tools When the browser supports WebMCP, every page registers the same tools with `document.modelContext.registerTool()` (falling back to `navigator.modelContext` in older previews). Browsers without WebMCP are unaffected. WebMCP results are strings: Markdown, or JSON text. The MCP server and WebMCP share one implementation (`lib/agent-tools.ts`), so names, inputs, and outputs are identical. ### Read-only tools | Tool | Input | Returns | | --- | --- | --- | | `get_resume` | none | JSON Resume text (same as `/resume.json`) | | `list_portfolio` | `{ category?: "sites" \| "interfaces" \| "marks" }` | JSON: `{ items: [{ id, title, category, media, url? }] }` | | `list_blog_posts` | `{ tag?: string, limit?: number }` | JSON: `{ posts: [{ title, slug, url, markdown, date, description, tags }] }` | | `get_blog_post` | `{ slug: string }` | Markdown of the post | | `get_page_markdown` | `{ path: string }` (e.g. "/", "/portfolio") | Markdown of that page | | `search_site` | `{ query: string }` | JSON: `{ query, results: [{ type, title, url, markdown?, snippet }] }` | | `get_booking_options` | none | JSON: meeting types, duration, host time zone, hours, required fields, limits | | `get_availability` | `{ date_from?: "YYYY-MM-DD", date_to?: "YYYY-MM-DD", timezone?: IANA zone, meeting_type?: "intro-call" }` | JSON: `{ meeting_type, duration_minutes, timezone, days: [{ date, slots: [{ slot_start, eastern, local }] }] }` | ### Booking tool (writes) | Tool | Input | Returns | | --- | --- | --- | | `book_meeting` | `{ slot_start: ISO 8601 (from get_availability), name: string, email: string, phone?: string, notes?: string, timezone?: IANA zone, meeting_type?: "intro-call", dry_run?: boolean }` | JSON: `{ status: "confirmed", booking: { meeting_type, duration_minutes, slot_start, eastern, local, name, email }, message }`, or `{ status: "valid", dry_run: true, booking }` for a dry run | ### Booking flow 1. `get_booking_options`: one meeting type today, `intro-call` (30 minutes). Host time zone is America/New_York. Slots run Monday to Friday, 07:30 to 20:30 Eastern, every 30 minutes; same-day slots must be at least 30 minutes out. 2. `get_availability` with a date range (and the visitor's `timezone` for readable `local` labels). Up to 62 days per call. 3. `book_meeting` with a `slot_start` from step 2 plus the visitor's name and email. No extra confirmation step is required; the agent can book directly. The booking goes through the same path as the form on /book: it is saved, Michael is notified, and the visitor receives a confirmation email with a calendar invite. The same validation (name, valid email, open slot) and the same limit (5 booking requests per hour per IP) apply. Use `dry_run: true` to validate without booking. Without WebMCP, send the visitor to https://www.michaelchurley.com/book: pick a day and time (Eastern), enter name, email, and optional phone, then press Book. ## Policy - robots.txt allows search engines, answer engines, and user-triggered agents (OAI-SearchBot, ChatGPT-User, GPTBot, PerplexityBot, ClaudeBot, Google-Extended, and others) with `Content-Signal: search=yes, ai-input=yes, ai-train=no`. - CCBot is blocked. Private paths (`/api/`, `/manage/`, and `/nightly`) are disallowed for crawlers. - Please cite the canonical page URL (without `.md`) when quoting. ## Contact or book Michael - Book a 30-minute call: the booking tools above, or https://www.michaelchurley.com/book - Email: michaelmonetized@gmail.com - Call or text: 828-593-1935 - LinkedIn: https://www.linkedin.com/in/michaelchurley - GitHub: https://github.com/michaelmonetized - X: https://x.com/michaelh_rley --- # Blog Source: https://www.michaelchurley.com/blog Build logs and notes from Michael C. Hurley on web development, SEO, and AI agents. Machine-readable: https://www.michaelchurley.com/blog.json - [One-line install](https://www.michaelchurley.com/blog/omadesign-0-5-8-one-line-install.md) (2026-09-23): One command installs omadesign to ~/.local/bin and a desktop entry under your home directory. Nothing is written to /usr. - [Portable ARM64 and x86_64](https://www.michaelchurley.com/blog/omadesign-0-5-8-portable-arm64-and-x86-64.md) (2026-09-23): Omadesign 0.5.8 ships native ARM64 and x86_64 tarballs built for glibc 2.35. Same studio on Asahi, Ubuntu 22.04+, and current Arch. - [First five minutes](https://www.michaelchurley.com/blog/omadesign-0-5-8-first-five-minutes.md) (2026-09-23): Launch omadesign, pick a size or one of the 52 templates, then draw. Design starts on R, P, and T. Pixel paints with B. Photo grades a folder. - [Welcome logo 0.5.8](https://www.michaelchurley.com/blog/omadesign-0-5-8-welcome-logo-0-5-8.md) (2026-09-23): The 0.5.8 welcome screen uses the corrected transparent logo, 25% larger, on a dark ground. Panels fade from the lighter theme color into that chrome. - [Welcome segmented buttons](https://www.michaelchurley.com/blog/omadesign-0-5-8-welcome-segmented-buttons.md) (2026-09-23): 0.5.8 welcome controls use gradient segments, Phosphor utility icons, and squarer rounded project folders. The screen is the studio's front door. - [Persistent title bar](https://www.michaelchurley.com/blog/omadesign-0-5-8-persistent-title-bar.md) (2026-09-23): 0.5.8 keeps the title bar visible so open-document thumbnails stay put. The menu is the Omadesign wordmark, without a second Open or Preferences link. - [Filter icon states](https://www.michaelchurley.com/blog/omadesign-0-5-8-filter-icon-states.md) (2026-09-23): The 0.5.8 welcome filter icon has no button background. Hover turns it blue. An active filter turns it red, so you can see the funnel is on. - [Template chooser width](https://www.michaelchurley.com/blog/omadesign-0-5-8-template-chooser-width.md) (2026-09-23): The 0.5.8 template chooser opens at its final width and stays there as the pointer moves in. The 52 starters do not jump under the cursor. - [Crash recovery swap](https://www.michaelchurley.com/blog/omadesign-0-5-8-crash-recovery-swap.md) (2026-09-23): Idle a second and Omadesign writes ~/.local/share/omadesign/.oma.swp. Save deletes it. The Recovered tab lists the leftovers. Recents lists files you opened. - [Document tabs](https://www.michaelchurley.com/blog/omadesign-0-5-8-document-tabs.md) (2026-09-23): Document tabs sit above the canvas. Ctrl+N starts a tab, Ctrl+O opens another, and a close on unsaved work asks Save, Discard, or Cancel. - [Five personas one document](https://www.michaelchurley.com/blog/omadesign-0-5-8-five-personas-one-document.md) (2026-09-23): Design, Layout, Pixel, Photo, and Motion share one Linux binary, one .oma, and one layer stack. You change the tool well, not the file. - [Design persona tools](https://www.michaelchurley.com/blog/omadesign-0-5-8-design-persona-tools.md) (2026-09-23): Design is the mark, the poster, the layout. The first keys are Move V, Pen P, Rectangle R, and Type T, in the same .oma you will paint and animate. - [Layout persona tools](https://www.michaelchurley.com/blog/omadesign-0-5-8-layout-persona-tools.md) (2026-09-23): Layout builds screens in the same .oma as the drawing. Frame is F, rectangle is R, type is T. Export the selected frame as PNG, SVG, or HTML. - [Pixel persona tools](https://www.michaelchurley.com/blog/omadesign-0-5-8-pixel-persona-tools.md) (2026-09-23): Pixel is for painting and retouching. Brush B, Eraser E, Clone J, and Wand W work on a pixel layer inside the same .oma as the vectors. - [Photo persona tools](https://www.michaelchurley.com/blog/omadesign-0-5-8-photo-persona-tools.md) (2026-09-23): Photo grades the capture with Crop C and the develop sliders, then Place in Design. Settings sit in a .omaphoto file. The camera original is never rewritten. - [Motion persona tools](https://www.michaelchurley.com/blog/omadesign-0-5-8-motion-persona-tools.md) (2026-09-23): Motion animates the artboard you already drew. Space plays, K sets transform keys, and File exports Lottie or animated SVG. The rest pose stays the drawing. - [Shortcut HUD](https://www.michaelchurley.com/blog/omadesign-0-5-8-shortcut-hud.md) (2026-09-23): The Shortcut HUD sits on the bottom edge. The upper row follows the tool. The lower row lists letter keys. Hold Ctrl, Shift, or Alt to see those commands. Ctrl+/ toggles it. - [HUD modifiers while drawing](https://www.michaelchurley.com/blog/omadesign-0-5-8-hud-modifiers-while-drawing.md) (2026-09-23): The Shortcut HUD keeps a constant height when modifiers change, so a drag does not jump. Hover + more for overflow. Hints never take the keyboard. F1 is the full list. - [Omarchy theme chrome](https://www.michaelchurley.com/blog/omadesign-0-5-8-omarchy-theme-chrome.md) (2026-09-23): Studio chrome follows Omarchy theme colors and the desktop font. Icons are Phosphor Light. Point OMADESIGN_FONT at a .ttf when you want a different UI face. - [Theme fallback chain](https://www.michaelchurley.com/blog/omadesign-0-5-8-theme-fallback-chain.md) (2026-09-23): On launch, Omadesign reads the current Omarchy colors.toml, then the named theme file, then stock Catppuccin. The first file that is there wins. - [Move tool](https://www.michaelchurley.com/blog/omadesign-0-5-8-move-tool.md) (2026-09-23): Move is V. Click selects, drag moves, eight handles scale, the top handle rotates. Shift adds or constrains. Alt-drag clones. Corner dots round a rectangle. - [Layer reorder shortcuts](https://www.michaelchurley.com/blog/omadesign-0-5-8-layer-reorder-shortcuts.md) (2026-09-23): Select a layer row, then Ctrl+[ or Ctrl+] to reorder it. Add Shift to send it to the back or front of its group. Each reorder is one undo. - [Free transform](https://www.michaelchurley.com/blog/omadesign-0-5-8-free-transform.md) (2026-09-23): Ctrl+T puts the selection into Move with scale and rotation handles ready. Live text stays live. Shape parameters stay editable. The command is also under Object. - [Node tool](https://www.michaelchurley.com/blog/omadesign-0-5-8-node-tool.md) (2026-09-23): Node is A. Drag points and Bézier handles, Shift-click to add, Alt-click to convert corner and smooth, Alt-drag to break symmetry, Delete to remove points. - [Break path](https://www.michaelchurley.com/blog/omadesign-0-5-8-break-path.md) (2026-09-23): Object → Break path turns a live shape into points that sit on the rotated artwork. The first node edit converts the same way. Selecting the path writes no undo. - [Flip horizontal vertical](https://www.michaelchurley.com/blog/omadesign-0-5-8-flip-horizontal-vertical.md) (2026-09-23): Flip horizontal and Flip vertical mirror across the canvas axes you can see, rotation included. Undo restores the art. Live text needs Convert to path first. - [Pen tool](https://www.michaelchurley.com/blog/omadesign-0-5-8-pen-tool.md) (2026-09-23): Pen is P. Click for a corner, click-drag for a smooth point, and a twitch under 3px stays a corner. Shift holds 45°. Esc drops the last point, then cancels. - [Artboard tool](https://www.michaelchurley.com/blog/omadesign-0-5-8-artboard-tool.md) (2026-09-23): Artboard is Shift+O. Draw a board, drag to move it, scale from the handles, rotate from the top handle. Alt-drag clones. Rename it in Transform. - [Pencil freehand](https://www.michaelchurley.com/blog/omadesign-0-5-8-pencil-freehand.md) (2026-09-23): Pencil is N. Drag a freehand curve on the same canvas as the pen. Hold Shift and the stroke locks to horizontal, vertical, or 45 degrees. - [Shape tools](https://www.michaelchurley.com/blog/omadesign-0-5-8-shape-tools.md) (2026-09-23): Rectangle R, ellipse O, polygon Y, star S, and line L drag on like any shape tool you already know. Shift constrains. Radius, sides, and inner radius stay in Transform until you edit nodes. - [Corner radius multi-edit 0.5.8](https://www.michaelchurley.com/blog/omadesign-0-5-8-corner-radius-multi-edit-0-5-8.md) (2026-09-23): In 0.5.8, Alt-drag a corner-radius handle with the Node tool and every corner on the path changes. Shift-select a set of corners, then drag one selected handle, and that set moves together. - [Type tool](https://www.michaelchurley.com/blog/omadesign-0-5-8-type-tool.md) (2026-09-23): Type is T. Click, and the first keystroke replaces the word Type. Enter starts a new line. Esc or a click away finishes. Double-click comes back to edit. - [OpenType character studio](https://www.michaelchurley.com/blog/omadesign-0-5-8-opentype-character-studio.md) (2026-09-23): Character studio sets kerning, ligatures, tabular figures, and small caps on the text you already have. Project fonts from Brand → Typography show up in the font picker without a system install. - [Gradient eyedropper](https://www.michaelchurley.com/blog/omadesign-0-5-8-gradient-eyedropper.md) (2026-09-23): Gradient is G. Drag across a selected shape and the active fill or stroke keeps its stops. Eyedropper is I and samples the fill. X swaps fill and stroke. D restores defaults. - [Trace raster to vector](https://www.michaelchurley.com/blog/omadesign-0-5-8-trace-raster-to-vector.md) (2026-09-23): Trace is U. It turns the active pixel layer into vectors, with threshold, color count, and smoothness in the Trace studio. Object → Trace to vector runs the same command without switching tools. - [Zoom and hand](https://www.michaelchurley.com/blog/omadesign-0-5-8-zoom-and-hand.md) (2026-09-23): Zoom is Z. Drag a box, click to zoom in, Alt-click to zoom out. Ctrl-click fits the artboard. Hand is H, and Space pans. Scroll zooms the canvas, not the panels. - [Select same properties](https://www.michaelchurley.com/blog/omadesign-0-5-8-select-same-properties.md) (2026-09-23): Select gathers All, None, Invert, Same Fill, Same Stroke, Same Effects, and objects with or without those properties. A match is the whole property. Hidden and locked objects stay out. - [Compound paths 0.5.8](https://www.michaelchurley.com/blog/omadesign-0-5-8-compound-paths-0-5-8.md) (2026-09-23): In 0.5.8, repeated unions and subtractions stay editable compound paths. Holes stay holes. Double-click with Move or Node and you can move points, pull handles, insert, delete, and break a contour. - [Pathfinder ops](https://www.michaelchurley.com/blog/omadesign-0-5-8-pathfinder-ops.md) (2026-09-23): Object → Pathfinder runs Union, Subtract, Intersect, XOR, and Divide on two or more vectors on the same layer. Stacking order matters. Each operation is one undo. Holes survive Divide. - [Combine and release](https://www.michaelchurley.com/blog/omadesign-0-5-8-combine-and-release.md) (2026-09-23): Group is Ctrl+G. Ungroup is Ctrl+Shift+G, and it does not fuse paths. Combine into a compound is Ctrl+8. Release compound is Ctrl+Shift+8. Artwork and guides do not mix in one combine. - [Expand stroke](https://www.michaelchurley.com/blog/omadesign-0-5-8-expand-stroke.md) (2026-09-23): Object → Expand stroke to outline turns the visible stroke into filled geometry, including caps, joins, and dashes. The fill you already had stays beneath. Compound outlines keep their holes. - [Reshape distort skew](https://www.michaelchurley.com/blog/omadesign-0-5-8-reshape-distort-skew.md) (2026-09-23): Object → Reshape opens Distort, Skew, Perspective, or a nine-handle Warp mesh. Shift constrains. Ctrl reverses snapping. Enter finishes. The first handle you move converts live text and parameter shapes to paths. - [SVG FX stack](https://www.michaelchurley.com/blog/omadesign-0-5-8-svg-fx-stack.md) (2026-09-23): The FX studio stacks SVG filters on the selection, then on the layer underneath. Blur, shadow, offset, color, turbulence, displacement. The parameters are the SVG parameters. - [FX export to SVG](https://www.michaelchurley.com/blog/omadesign-0-5-8-fx-export-to-svg.md) (2026-09-23): FX parameters are the SVG parameters. The canvas shows them rasterized. SVG export writes a filter element and the fe primitives, so the graph you built is the graph you share. - [Layers eye lock](https://www.michaelchurley.com/blog/omadesign-0-5-8-layers-eye-lock.md) (2026-09-23): Layers expand to the objects on them. The eye and the lock work per object. Click a name to select it on the canvas. A group expands, renames, hides, locks, and reorders as one unit. - [Pass through blending](https://www.michaelchurley.com/blog/omadesign-0-5-8-pass-through-blending.md) (2026-09-23): Pass through lets child blend modes reach the backdrop outside an explicit group. Turn it off and the group isolates. Regular layers and Layout frames isolate on their own. - [Ruler guides](https://www.michaelchurley.com/blog/omadesign-0-5-8-ruler-guides.md) (2026-09-23): Drag a horizontal guide from the top ruler and a vertical guide from the left. Delete, drag off the canvas, or use the context menu to remove one. Ctrl+; shows or hides ruler and object guides. - [Convert selection to guides](https://www.michaelchurley.com/blog/omadesign-0-5-8-convert-selection-to-guides.md) (2026-09-23): Object → Guides → Convert selection to guides turns vectors into non-printing contours you can still edit. Curves, compounds, shapes, and live text keep their data. Release guides restores the artwork. - [Guides survive save](https://www.michaelchurley.com/blog/omadesign-0-5-8-guides-survive-save.md) (2026-09-23): Guides stay in the .oma when you save, and they stay out of PNG, JPEG, SVG, and Lottie. A placed photo keeps its pixels. - [Lock unlock guides 0.5.8](https://www.michaelchurley.com/blog/omadesign-0-5-8-lock-unlock-guides-0-5-8.md) (2026-09-23): 0.5.8 puts Lock all guides and Unlock all guides on View → Guides as two commands. Guides start locked so type work does not nudge a rail. - [Ruler zero and units](https://www.michaelchurley.com/blog/omadesign-0-5-8-ruler-zero-and-units.md) (2026-09-23): Drag the ruler corner to set zero. Double-click resets it. px, mm, cm, in, and pt follow document DPI and leave the artwork in pixels. - [Snapping system](https://www.michaelchurley.com/blog/omadesign-0-5-8-snapping-system.md) (2026-09-23): Snap to edges, centers, guides, the grid, and equal gaps. Ctrl+Shift+; toggles. Hold Ctrl during a drag to reverse that choice, then let go. - [Shift constrain Alt clone](https://www.michaelchurley.com/blog/omadesign-0-5-8-shift-constrain-alt-clone.md) (2026-09-23): Shift locks pen, brush, and moves to horizontal, vertical, or 45 degrees. Alt-drag clones. On a brush, Shift anchors at the last free point. - [Frames nest](https://www.michaelchurley.com/blog/omadesign-0-5-8-frames-nest.md) (2026-09-23): Press F and drag a frame. Draw another inside it and it nests. Wrap a selection, or drop an image fill, in the same .oma as the drawing. - [Auto-layout stack](https://www.michaelchurley.com/blog/omadesign-0-5-8-auto-layout-stack.md) (2026-09-23): Select a frame and turn on Stack children. The inspector checkbox reads Arrange children automatically. Direction, gap, padding, and layer order do the packing. - [Frame constraints](https://www.michaelchurley.com/blog/omadesign-0-5-8-frame-constraints.md) (2026-09-23): A child of a frame pins with Min, Max, Stretch, Center, or Scale. The pins are stored on the object in the .oma and run when you resize the parent. - [Export frame](https://www.michaelchurley.com/blog/omadesign-0-5-8-export-frame.md) (2026-09-23): File → Export frame PNG, SVG, or HTML writes the selected frame and its children. The rest of the artboard stays in the .oma. - [Layout templates](https://www.michaelchurley.com/blog/omadesign-0-5-8-layout-templates.md) (2026-09-23): Layout starters are five frame documents: Fieldwork, mobile screen, landing hero, dashboard, and card stack. They sit beside the 52 vector templates. They are not five of those 52. - [Canvas comments](https://www.michaelchurley.com/blog/omadesign-0-5-8-canvas-comments.md) (2026-09-23): Write a note, pin it on the canvas, and resolve it. The pin is stored in the .oma. The inspector shows how many are still open on the frame. - [Pixel layer](https://www.michaelchurley.com/blog/omadesign-0-5-8-pixel-layer.md) (2026-09-23): Paint lives on a pixel layer in the same stack as the vectors. If the document is vector-only, add one from the Layers menu. The paths stay paths. - [Brush eraser fill](https://www.michaelchurley.com/blog/omadesign-0-5-8-brush-eraser-fill.md) (2026-09-23): Brush is B. Size is the bracket keys. Hardness is Shift plus the brackets. Eraser E, Fill K, Clone J, Smudge M. Alt-click sets the clone source. - [Healing brush](https://www.michaelchurley.com/blog/omadesign-0-5-8-healing-brush.md) (2026-09-23): Shift+J. Alt-click clean texture, then paint the blemish. The dab blends that texture into the local color. The source stays fixed for the stroke. Undo restores the whole stroke. - [Selections pixel](https://www.michaelchurley.com/blog/omadesign-0-5-8-selections-pixel.md) (2026-09-23): Marquee is Shift+M, elliptical marquee is Shift+O, lasso is Q, wand is W. Tolerance sits in Brush. Ants stay until Esc. Paint stays inside the selection. - [Layer masks](https://www.michaelchurley.com/blog/omadesign-0-5-8-layer-masks.md) (2026-09-23): Add a layer mask from the layer menu or the Pixel inspector. Reveal all, hide all, or start from the selection. Black hides. White reveals. The pixels stay put. - [Mask invert apply](https://www.michaelchurley.com/blog/omadesign-0-5-8-mask-invert-apply.md) (2026-09-23): Invert flips coverage. Remove puts the original layer back. Apply to pixels bakes a raster and one undo restores both the pixels and the mask. Placed-image masks follow the transform. - [Open photos folder](https://www.michaelchurley.com/blog/omadesign-0-5-8-open-photos-folder.md) (2026-09-23): Open a photo, browse a folder, drop files, or load samples. The library shows camera metadata when the file has it. Imports run in the background while you grade. - [Camera RAW decoder](https://www.michaelchurley.com/blog/omadesign-0-5-8-camera-raw-decoder.md) (2026-09-23): LibRaw 0.22.2 is in the binary. DNG, CR2, CR3, NEF, ARW, RAF, ORF, RW2, and the rest of the recognized families decode to 16-bit linear sRGB. No converter download. The camera file stays put. - [Develop panel groups](https://www.michaelchurley.com/blog/omadesign-0-5-8-develop-panel-groups.md) (2026-09-23): Develop is three groups: Light, Color, and Detail. Tone curve, color mixer, and color grading stay collapsed until you open them. Before is the default development. Auto light sets exposure from the preview. - [omaphoto sidecars](https://www.michaelchurley.com/blog/omadesign-0-5-8-omaphoto-sidecars.md) (2026-09-23): Save settings writes photo.png.omaphoto beside the original. Open either file to resume. The pair is name, size, and modification time. The camera file is never rewritten. - [Photo undo history](https://www.michaelchurley.com/blog/omadesign-0-5-8-photo-undo-history.md) (2026-09-23): Photo has its own Undo and Redo, on the selected photo. Quit asks Save all, Discard, or Cancel for unsaved photo settings before palettes and artwork, and it waits for the writes. - [Photo export bit depth](https://www.michaelchurley.com/blog/omadesign-0-5-8-photo-export-bit-depth.md) (2026-09-23): Export JPEG, PNG, or TIFF in the background at full developed size, crop and rotation included. RAW PNG and TIFF keep 16-bit channels. Place in Design adds an 8-bit pixel layer and one undo. - [Tiled full-res preview](https://www.michaelchurley.com/blog/omadesign-0-5-8-tiled-full-res-preview.md) (2026-09-23): The first view is a preview whose long edge is at most 1600 pixels. Zoom in and full-resolution tiles fill in from a background develop. The preview stays up until those tiles exist. - [Copy paste adjustments](https://www.michaelchurley.com/blog/omadesign-0-5-8-copy-paste-adjustments.md) (2026-09-23): Copy a developed look with Ctrl+Shift+C and paste it onto a Library selection with Ctrl+Shift+V. Crop and rotation stay off unless you turn them on. - [Batch save settings](https://www.michaelchurley.com/blog/omadesign-0-5-8-batch-save-settings.md) (2026-09-23): After a paste, Ctrl+S writes a .omaphoto sidecar for every selected photo. The batch is one Undo and one Redo. Categories travel on their own. - [omapreset library](https://www.michaelchurley.com/blog/omadesign-0-5-8-omapreset-library.md) (2026-09-23): Save a named Photo look, filter the library, and move it as a .omapreset file. The preset keeps development values and categories, and a name clash keeps both. - [Whole folder apply](https://www.michaelchurley.com/blog/omadesign-0-5-8-whole-folder-apply.md) (2026-09-23): Browse a folder, apply a copied look or a preset to the whole folder, and write one .omaphoto per photo in the background. Pixels stay untouched. Subfolders stay out. - [Folder job limits](https://www.michaelchurley.com/blog/omadesign-0-5-8-folder-job-limits.md) (2026-09-23): A folder job stops at 10,000 photos and 32 MiB of settings history. Cancel leaves finished files undoable, and a sidecar edited outside the job is kept on Undo. - [Photo navigation](https://www.michaelchurley.com/blog/omadesign-0-5-8-photo-navigation.md) (2026-09-23): In Photo, hold Space or use Hand to drag. Middle-drag and two-finger scroll pan. Pinch, Ctrl+scroll, and Alt+scroll zoom. Ctrl+0 fits. Ctrl+1 is 100%. - [Rest pose intact](https://www.michaelchurley.com/blog/omadesign-0-5-8-rest-pose-intact.md) (2026-09-23): The artboard you drew is the rest pose. Motion keeps a clip of tracks in the .oma and does not rewrite that drawing. Still PNG, JPEG, and SVG export the rest pose. - [Motion presets](https://www.michaelchurley.com/blog/omadesign-0-5-8-motion-presets.md) (2026-09-23): Select vectors and apply Draw stroke, Pop in, Slam, Shake, Fill up, the four Slides, Fly, Zoom, Buzz, or Fade in. Locked, hidden, and guide objects are skipped. - [Timing and energy](https://www.michaelchurley.com/blog/omadesign-0-5-8-timing-and-energy.md) (2026-09-23): Duration sits above the motion presets. Timing & energy sets delay, stagger, intensity, and start-at-playhead. Each apply is its own Undo and only rewrites the channels it owns. - [Keyframe editing](https://www.michaelchurley.com/blog/omadesign-0-5-8-keyframe-editing.md) (2026-09-23): Drag a shape to write keys at the playhead. The first key after zero also plants the rest pose at zero. K keys X, Y, rotation, and scale. Delete peels a key, then the animation, then the object. - [Playback controls](https://www.michaelchurley.com/blog/omadesign-0-5-8-playback-controls.md) (2026-09-23): Space plays and pauses in Motion. Home and End jump the playhead. Loop is the repeat icon. Cycle ease on the key you selected. The preview stays in the persona. - [Export animated SVG](https://www.michaelchurley.com/blog/omadesign-0-5-8-export-animated-svg.md) (2026-09-23): File → Export animated SVG… writes animated transforms and stroke and fill reveals, and it keeps masks and effects. Text is outlined in that file. The .oma text stays editable. - [Export Lottie](https://www.michaelchurley.com/blog/omadesign-0-5-8-export-lottie.md) (2026-09-23): Export Lottie… writes a Bodymovin 5.x shape animation with trim paths and fill masks. Pixel layers, layer masks, and effects stop the export with a clear error. Use animated SVG for those. - [Import Lottie](https://www.michaelchurley.com/blog/omadesign-0-5-8-import-lottie.md) (2026-09-23): Import Lottie… places a shape-layer Lottie on the timeline. The importer covers a basic shape subset. Keep the .oma for the full editable animation. Still exports remain the rest pose. - [Fifty-two templates](https://www.michaelchurley.com/blog/omadesign-0-5-8-fifty-two-templates.md) (2026-09-23): Templates · 52 opens from the welcome screen or File → Template library. Search by name or idea, filter nine categories, and pick a built-in size or a custom width, height, and DPI. - [Templates offline editable](https://www.michaelchurley.com/blog/omadesign-0-5-8-templates-offline-editable.md) (2026-09-23): A template opens as an unsaved document with editable paper, artwork, and copy. Your other tab stays put. Fonts are local. Portrait, square, and landscape layouts are distinct, and tiny sizes drop unreadable lines. - [Three right tabs](https://www.michaelchurley.com/blog/omadesign-0-5-8-three-right-tabs.md) (2026-09-23): The right sidebar is three tabs. Inspect for the selected artwork, Palettes for reusable colors, and Brand for logos, images, fonts, and other assets. - [Project library files](https://www.michaelchurley.com/blog/omadesign-0-5-8-project-library-files.md) (2026-09-23): Project kits sit beside the work as .omacolors, .omatype, and .omabrand/. A saved document uses the nearest enclosing folder that has any of them, or starts beside the document. - [Personal vs project palettes](https://www.michaelchurley.com/blog/omadesign-0-5-8-personal-vs-project-palettes.md) (2026-09-23): Palettes are Personal across your work or Project in the current folder. Name a palette, filter it, add the current color, the selection, or a hex including #RRGGBBAA, then click a swatch onto Fill or Stroke. - [Palette save export](https://www.michaelchurley.com/blog/omadesign-0-5-8-palette-save-export.md) (2026-09-23): Palette Save is its own write, separate from the artwork. Load palettes… merges and suffixes name clashes. Export one palette or the whole collection. Duplicate and remove are there. - [Palette conflict handling](https://www.michaelchurley.com/blog/omadesign-0-5-8-palette-conflict-handling.md) (2026-09-23): Palette files refresh about every three seconds. Unsaved edits stay put and Save blocks if the file changed underneath. Export a copy or Reload saved colors. Quit asks before it leaves. - [Brand bank load](https://www.michaelchurley.com/blog/omadesign-0-5-8-brand-bank-load.md) (2026-09-23): Brand → Load bank… opens a project folder or its .omabrand directory. Create bank, name it, and Save. Add assets… copies files in. The originals stay where they were. - [Brand asset place](https://www.michaelchurley.com/blog/omadesign-0-5-8-brand-asset-place.md) (2026-09-23): Filter brand tiles by name, path, or type. Thumbnails load in the background and refresh about every three seconds. Drag onto the canvas or double-click to center. Ctrl+Z undoes the place. - [Brand accepted formats](https://www.michaelchurley.com/blog/omadesign-0-5-8-brand-accepted-formats.md) (2026-09-23): A brand bank takes PNG, JPEG, WebP, TIFF, BMP, GIF, SVG, and .oma. SVG uses the supported import subset. Save bank copy… clones the bank, its name, and its nested folders to another project. - [Brand typography kit](https://www.michaelchurley.com/blog/omadesign-0-5-8-brand-typography-kit.md) (2026-09-23): Add fonts… copies TTF and OTF files into the project. Name the kit, save roles such as Heading and Body, and Apply them to text. The faces work inside Omadesign with no system install. - [Typography load save copy](https://www.michaelchurley.com/blog/omadesign-0-5-8-typography-load-save-copy.md) (2026-09-23): Load kit… merges another .omatype and copies its fonts. Save copy… writes the kit and the faces into another project. Remove a role and the font file stays for artwork that already uses it. - [Project fonts travel](https://www.michaelchurley.com/blog/omadesign-0-5-8-project-fonts-travel.md) (2026-09-23): Move the project and native .oma text stays editable, including new characters. Saving into another folder copies the faces that artwork uses. SVG export outlines project-font text. The .oma keeps the live text. - [Share project kit](https://www.michaelchurley.com/blog/omadesign-0-5-8-share-project-kit.md) (2026-09-23): A project kit is .omacolors, .omatype, and the whole .omabrand folder beside the work. They are dotfiles. Show hidden files before you copy by hand. Fieldwork ships as a portable reference. - [Native oma format](https://www.michaelchurley.com/blog/omadesign-0-5-8-native-oma-format.md) (2026-09-23): A .oma is JSON, PNG-packed rasters, and a motion clip in one file. Version 5 carries frames, auto-layout, constraints, and opt-in cloud metadata. Save imports as .oma to keep the editable document and the conversion notes. - [Open layered foreign docs](https://www.michaelchurley.com/blog/omadesign-0-5-8-open-layered-foreign-docs.md) (2026-09-23): File → Open reads layered PSD, PSB, XCF, every PDF page, PDF-compatible AI, OpenRaster, SVG, and Affinity via the optional bridge. Each file opens in its own tab at its size. Save writes a .oma and leaves the source. - [Place command](https://www.michaelchurley.com/blog/omadesign-0-5-8-place-command.md) (2026-09-23): File → Place… is Ctrl+Shift+P. The artwork loads in the background, then you click or drag to place it. Nested layers and masks come along. Undo removes the placement in one step. Enter places at center. Esc cancels. - [Drop to open or place](https://www.michaelchurley.com/blog/omadesign-0-5-8-drop-to-open-or-place.md) (2026-09-23): Drop a layered document on the canvas or the welcome screen to open it. Ordinary images place. A .oma opens. Lottie imports. The status bar confirms copy, cut, and paste. Copy style is Ctrl+Alt+C. Paste style is Ctrl+Alt+V. - [Conversion notes](https://www.michaelchurley.com/blog/omadesign-0-5-8-conversion-notes.md) (2026-09-23): View → Document conversion notes lists what an import dropped or converted. The notes stay in the .oma. Affinity-only features, live Photoshop type, smart objects, effects, and private Illustrator data stay unrebuilt. - [Export matrix](https://www.michaelchurley.com/blog/omadesign-0-5-8-export-matrix.md) (2026-09-23): Export writes PNG at 1×, 2×, and 3×, plus JPEG, SVG, animated SVG, Lottie JSON, layered PSD and PSB, PDF, and OpenRaster. Layers a format cannot keep may become pixel layers, with notes. There is no native .af or .ai writer. - [PSD PSB bridge](https://www.michaelchurley.com/blog/omadesign-0-5-8-psd-psb-bridge.md) (2026-09-23): PSD and PSB open through a native layered reader and export as RGB 8-bit layers. Groups, names, masks, blends, and supported Normal color overlays can survive. Text and smart objects arrive as saved pixels. - [Affinity optional bridge](https://www.michaelchurley.com/blog/omadesign-0-5-8-affinity-optional-bridge.md) (2026-09-23): Affinity files open through the optional converter from setup-affinity-import.sh. Vectors, text, pixels, groups, masks, and artboards can come across. Adjustments and history often do not. The app does not download the converter. - [PDF AI import export](https://www.michaelchurley.com/blog/omadesign-0-5-8-pdf-ai-import-export.md) (2026-09-23): PDF pages open as artboards with paths, supported text, images, and optional-content layers. Export writes pages, vectors, and that layer metadata. An .ai contributes its PDF-compatible artwork. Private Illustrator data stays unrebuilt. - [OpenRaster GIMP](https://www.michaelchurley.com/blog/omadesign-0-5-8-openraster-gimp.md) (2026-09-23): OpenRaster imports and exports layers, stack.xml, a merged preview, and a thumbnail. GIMP .xcf opens in a native reader and does not write back. Text and effects arrive as pixels. Keep the .oma. Send ORA or PSD back. - [Headless inspect convert](https://www.michaelchurley.com/blog/omadesign-0-5-8-headless-inspect-convert.md) (2026-09-23): omadesign --inspect reports a PSD, Affinity file, XCF, NEF, or .omaphoto as JSON. --convert writes .oma, SVG, PNG, JPEG, PSD, PSB, PDF, or OpenRaster. The CLI uses the desktop readers. Converting a file onto itself is refused. - [RAW headless develop](https://www.michaelchurley.com/blog/omadesign-0-5-8-raw-headless-develop.md) (2026-09-23): omadesign --convert photograph.dng --output photograph.tif develops a full-resolution 16-bit file through the Photo pipeline. JPEG is 8-bit. A matching .omaphoto is applied on its own. The camera file is never overwritten. - [Cloud opt-in](https://www.michaelchurley.com/blog/omadesign-0-5-8-cloud-opt-in.md) (2026-09-23): Cloud stays off until you opt in. The .oma remains on disk. File → Sign in is the browser approval. Push project + review export is the upload. Unpublished work stays out of the gallery. - [Sign in identity](https://www.michaelchurley.com/blog/omadesign-0-5-8-sign-in-identity.md) (2026-09-23): File → Sign in… opens a browser device code. Approve only the code the desktop is showing. The site account holds the name and email. A name in a file does not, by itself, grant cloud access. - [Enable sync invite](https://www.michaelchurley.com/blog/omadesign-0-5-8-enable-sync-invite.md) (2026-09-23): Push project + review export uploads a versioned .oma and a flat PNG, then you save so the cloud link stays in the file. Owners invite a verified email as editor or reviewer. Other drafts on disk stay local. - [Publish to showcase](https://www.michaelchurley.com/blog/omadesign-0-5-8-publish-to-showcase.md) (2026-09-23): Publish selected export is a separate owner step. /showcase lists public flat images. /showcase/:id shows one. Source files, assets, and private review threads stay out. Unpublished and private ids stay off the gallery. - [Collaborator project view](https://www.michaelchurley.com/blog/omadesign-0-5-8-collaborator-project-view.md) (2026-09-23): /project/:id opens the signed-in workspace for that project. Pin, reply, and resolve on a flat export. Layout pins on the canvas stay with the frame. Without OMADESIGN_CLOUD_URL the desktop keeps ~/.local/share/omadesign/cloud-store.json. - [Compete waitlist](https://www.michaelchurley.com/blog/omadesign-0-5-8-compete-waitlist.md) (2026-09-23): /compete lists competitions, public entries, and withdrawal. A competition stays unpublished until it has a real brief and dates. Enter with a public showcase work. /account is identity. /api/cloud is the desktop transport. - [Lua 5.4 embedded](https://www.michaelchurley.com/blog/omadesign-0-5-8-lua-5-4-embedded.md) (2026-09-23): Omadesign 0.5.8 embeds Lua 5.4.9 and plugin API 1 inside the Linux packages. There is no separate runtime to install. An update leaves the plugins you already installed in place. - [Manage plugins](https://www.michaelchurley.com/blog/omadesign-0-5-8-manage-plugins.md) (2026-09-23): Plugins → Manage plugins installs a .lua file, a folder with main.lua, or an .omaplug bundle. Each plugin has an Enable checkbox. An update keeps a hidden backup of the previous folder. Reload after you edit. - [Studio starter twelve](https://www.michaelchurley.com/blog/omadesign-0-5-8-studio-starter-twelve.md) (2026-09-23): Studio starter ships twelve Lua actions on first install. A duotone, a shadow, an SVG icon, two brushes, a canvas tool, a pattern, a gradient, a palette, a batch nudge, and two behaviors that stay off until you opt in. - [Plugin undo safety](https://www.michaelchurley.com/blog/omadesign-0-5-8-plugin-undo-safety.md) (2026-09-23): A plugin runs off the UI thread. A finished document action is one Undo step. Errors, Cancel, and a document you edited mid-run leave the artwork untouched. - [Plugin categories](https://www.michaelchurley.com/blog/omadesign-0-5-8-plugin-categories.md) (2026-09-23): Plugin actions declare a category. Filters, effects, icons, brushes, tools, behaviors, batch, patterns, gradients, and swatches each have a precondition, and the manager groups them that way. - [Canvas tool plugins](https://www.michaelchurley.com/blog/omadesign-0-5-8-canvas-tool-plugins.md) (2026-09-23): A tool plugin arms with Activate tool. The drag previews as a line. Editable artwork appears on release. Escape exits. Pixel filters and brush presets still need a raster layer. - [Opt-in behaviors](https://www.michaelchurley.com/blog/omadesign-0-5-8-opt-in-behaviors.md) (2026-09-23): Document and selection behaviors ship off. One checkbox opts in, remembers the choice, and a behavior’s own output does not fire itself again. - [Host API surface](https://www.michaelchurley.com/blog/omadesign-0-5-8-host-api-surface.md) (2026-09-23): API 1 is a short list of oma calls. Shapes, fills, effects, brushes, palettes, SVG, pixels, and a status message. Each run gets a fresh Lua VM. - [Plugin CLI batch](https://www.michaelchurley.com/blog/omadesign-0-5-8-plugin-cli-batch.md) (2026-09-23): omadesign --plugin and --batch walk every immediate .oma in filename order, write new files, keep the inputs, and refuse a path that already exists. - [Install list plugins CLI](https://www.michaelchurley.com/blog/omadesign-0-5-8-install-list-plugins-cli.md) (2026-09-23): omadesign --install-plugin and --list-plugins run without a window. Brush, palette, and canvas tools still belong to the desktop. Document commands batch from the shell. - [Plugin sandbox limits](https://www.michaelchurley.com/blog/omadesign-0-5-8-plugin-sandbox-limits.md) (2026-09-23): A plugin cannot run programs, open the network, or read arbitrary files. The caps are 15 seconds, 64 MiB of Lua heap, and 20,000 edits. This release has no plugin marketplace. - [Starter source path](https://www.michaelchurley.com/blog/omadesign-0-5-8-starter-source-path.md) (2026-09-23): After install, the clean Studio starter tree is at ~/.local/share/omadesign/plugin-examples/studio-starter. Fork the repo, add a folder under plugins/, and open a pull request with the proof. - [Adobe familiar tool keys](https://www.michaelchurley.com/blog/omadesign-0-5-8-adobe-familiar-tool-keys.md) (2026-09-23): The tool letters are V A P N R O Y S L T G I U B E K J, Shift+J, M C W H Z. F1 opens the full list. The HUD at the bottom follows the tool you are holding. - [File and edit keys](https://www.michaelchurley.com/blog/omadesign-0-5-8-file-and-edit-keys.md) (2026-09-23): Undo is Ctrl+Z. Redo is Ctrl+Shift+Z, and Ctrl+Y also redos. Save, open, new, place, and export sit on the usual chords. Pixel Ctrl+D clears a selection. Super+D duplicates. - [Arrange and transform keys](https://www.michaelchurley.com/blog/omadesign-0-5-8-arrange-and-transform-keys.md) (2026-09-23): Ctrl+G groups and Ctrl+Shift+G ungroups. Compound is Ctrl+8 and release is Ctrl+Shift+8. Front and back, free transform, guides, snapping, the HUD, and F1 sit on the chords beside them. - [View and motion keys](https://www.michaelchurley.com/blog/omadesign-0-5-8-view-and-motion-keys.md) (2026-09-23): Ctrl+0 fits, Ctrl+1 is 100%, and Ctrl plus or minus zooms. Hold Space to pan. In Motion, Space plays, K sets keys, and Delete removes a key or the animation. - [Native dialogs context](https://www.michaelchurley.com/blog/omadesign-0-5-8-native-dialogs-context.md) (2026-09-23): Open, save, place, and export use the desktop file dialog. Right-click the canvas for Place, Trace, and the same edits. The status bar reports copy, cut, and paste. - [Plugins offline docs](https://www.michaelchurley.com/blog/omadesign-0-5-8-plugins-offline-docs.md) (2026-09-23): The 0.5.8 package carries Lua, the manual, the plugin guide, the creation skill, Studio starter, and the licenses. Author from the files on disk. The in-app Docs item still opens the website. - [One binary not three apps](https://www.michaelchurley.com/blog/omadesign-0-5-8-one-binary-not-three-apps.md) (2026-09-23): Design, Layout, Pixel, Photo, and Motion are personas in one Linux binary. The letters V, P, T, and B stay put. F1 lists the rest while the same .oma stays open. - [Vectors pixels photo motion](https://www.michaelchurley.com/blog/omadesign-0-5-8-vectors-pixels-photo-motion.md) (2026-09-23): Design, Pixel, Photo, and Motion share one layer stack in a .oma. Layout frames live in that file when the brief is a screen. The RAW stays beside it until you place. - [Brand kit on disk](https://www.michaelchurley.com/blog/omadesign-0-5-8-brand-kit-on-disk.md) (2026-09-23): Palettes live in .omacolors, font roles in .omatype, and logos in .omabrand/. Copy the project folder. No account is required to open the kit on another machine. - [RAW sidecars not destructive](https://www.michaelchurley.com/blog/omadesign-0-5-8-raw-sidecars-not-destructive.md) (2026-09-23): Save settings writes .omaphoto beside the camera file and leaves the pixels alone. Whole-folder apply writes sidecars only. Place in Design when the grade should enter the poster. - [Plugins without ExtendScript tax](https://www.michaelchurley.com/blog/omadesign-0-5-8-plugins-without-extendscript-tax.md) (2026-09-23): 0.5.8 runs Lua plugins inside the binary. Filters, effects, icons, brushes, tools, patterns, gradients, swatches, and a batch command. One undo. No separate runtime. - [Import honesty export choice](https://www.michaelchurley.com/blog/omadesign-0-5-8-import-honesty-export-choice.md) (2026-09-23): Open PSD, PDF, SVG, ORA, XCF, Affinity through the bridge, and RAW. Conversion notes stay in the .oma. Export PNG at 1× 2× 3×, SVG, Lottie, PSD, PDF, and ORA. There is no native AI or Affinity writer. - [Cloud optional showcase explicit](https://www.michaelchurley.com/blog/omadesign-0-5-8-cloud-optional-showcase-explicit.md) (2026-09-23): File → Sign in is optional. Push uploads a version when you ask. Publish to the showcase is a second step by the owner. Unpublished work stays off the public gallery. The .oma remains local. - [Linux first creative suite](https://www.michaelchurley.com/blog/omadesign-0-5-8-linux-first-creative-suite.md) (2026-09-23): curl installs omadesign into ~/.local/bin and writes nothing to /usr. Omarchy colors, Phosphor icons, ARM64 and x86_64, offline templates, and Lua inside the 0.5.8 package. - [Vanity phones and fake metrics scrubbed across five repos](https://www.michaelchurley.com/blog/vanity-phone-metrics-honesty-fleet.md) (2026-09-09): Follow-up: ten vanity-phone and fake-metrics issues closed. yourzaxbys and iPro dropped 555 vanity numbers; s12.in hero stats became honest capability copy; freview was a false positive; ileague metrics already fixed on main. - [Empty READMEs filled with real usage across three repos](https://www.michaelchurley.com/blog/empty-readme-fills-fleet.md) (2026-09-09): Follow-up: six content-factory-gap empty README issues closed via three PRs. animated-gradient-border, fab-analytics, and shagent now document real install/run usage. No fake stars or metrics. - [Coming Soon shells replaced with honest status landers](https://www.michaelchurley.com/blog/coming-soon-waitlist-honesty-fleet.md) (2026-09-09): Follow-up: waitlist Coming Soon shells closed across hustledesk, kitchen, merchwinner, coordinatorapp, iPro-main-web, and omnux. Planned SSO and Notify forms now say Planned/not shipped or Not available yet. ileague.app still Coming Soon after lander PR (Vercel/DNS park). - [LICENSE copyright aligned across ten repos](https://www.michaelchurley.com/blog/license-copyright-alignment-fleet.md) (2026-09-09): Follow-up: 20 issues across 10 repos closed. LICENSE copyright aligned to Michael Hurley / owning org on stripe-convex, slopops, orclawstrator, nvibe, compare, agent-os, agent-computer-use, WhisperCPPonEverything, s12.in, and ileague-app; MIT added where missing. Sample PRs stripe-convex#18 and s12.in#35. - [hustle* suite: DEPLOYMENT_NOT_FOUND cleared on *.vercel.app landers](https://www.michaelchurley.com/blog/hustle-suite-vercel-app-restores.md) (2026-09-09): Follow-up: hustledesk/crm/convert/chat/forms #1 and hustlemail #2 closed. Packs that showed DEPLOYMENT_NOT_FOUND now resolve on *.vercel.app hosts for desk, crm, convert, chat, forms, and mail. hustlemail also needed a PostCSS @tailwindcss/postcss fix. Custom DNS still parked overnight. - [hms: live again on hms-cyan-six.vercel.app](https://www.michaelchurley.com/blog/hms-cyan-six-vercel-live.md) (2026-09-09): Follow-up: hms#1 closed. Live host https://hms-cyan-six.vercel.app is up after packs flagged a missing deploy. - [codefolio: DEPLOY.md landed, live URL still needs Convex env](https://www.michaelchurley.com/blog/codefolio-deploy-md-no-live-url.md) (2026-09-09): Follow-up: codefolio#1 closed with DEPLOY.md only. No live URL yet; Convex env and codefolio.dev remain parked. - [simple: Firebase config env-only, service account wiped](https://www.michaelchurley.com/blog/simple-firebase-env-only-sa-wiped.md) (2026-09-09): Follow-up: michaelmonetized/simple PR #12 closed Issue #7. Client Firebase is env-only and fails closed without logging values; serviceAccount/admin credential patterns gitignored; leftover local SA file wiped. Prior keep-Firebase notes still stand. - [launchpad: Clerk, Convex, and PostHog finally mount in layout](https://www.michaelchurley.com/blog/launchpad-clerk-convex-posthog-wired.md) (2026-09-09): Follow-up: michaelmonetized/launchpad PR #7 closed Issue #3. Root layout mounts Clerk then Convex then PostHog; Convex uses @clerk/nextjs useAuth matching clerkMiddleware. Soft-guards when env is unset. Providers are no longer orphan files. - [mission-control-os: pending Clerk auth and agency create timeout](https://www.michaelchurley.com/blog/mission-control-os-pending-auth-agency-timeout.md) (2026-09-09): Follow-up: Hustle-Launch/mission-control-os PR #64 (d09322c) closed Issues #50 and #51. Shared auth hooks keep pending sessions from looking signed-out on AgencyGate/PortalGate/cockpit; agency org create/select gets a hard timeout plus error instead of infinite Working. Prod redeploy still blocked on bad VERCEL_TOKEN (infra). - [MyBathroomConversion: WP File Manager gone, Salespromis key moved to env](https://www.michaelchurley.com/blog/mybathroomconversion-wp-file-manager-gone-key-to-env.md) (2026-09-09): Follow-up: HurleyUS/www.mybathroomconversion.com PR #7 (928819d) closed Issues #4 and #5. Removed vendored WP File Manager 7.2.9 (~906 files) and moved Salespromis Opt In onto SALESPROMIS_API_KEY env/constant. Rotate the old key; set env before leads post again. - [BestWNC: I built a WNC directory that returns null instead of fake analytics](https://www.michaelchurley.com/blog/bestwnc-honest-analytics-local-directory.md) (2026-09-08): BestWNC is the WNC directory where listings start free and owner analytics return null when the measurement layer cannot defend a chart. September 8 shipped honest null availability, claim/billing hardening, and honeypots after spam flooded the inbox. - [omadesign: I shipped a native Linux design suite in 13 days](https://www.michaelchurley.com/blog/omadesign-native-linux-studio-13-days.md) (2026-09-08): omadesign is a native Rust + egui studio with Design, Pixel, Photo, and Motion personas on one document and layer stack. Four alphas in thirteen days, glibc 2.35 tarballs for aarch64 and x86_64, still alpha and shipping without Electron. - [GetAt.Me: I replaced the link list with a relationship console](https://www.michaelchurley.com/blog/getat-me-relationship-first-link-in-bio.md) (2026-09-08): getat.me is a relationship-first link-in-bio: Next.js + Convex contacts and booking rails, not just clicks. Honest about what is live vs scaffold. - [Naarchy 0.4.0: Preferences, doctor, and the privacy pass](https://www.michaelchurley.com/blog/naarchy-0-4-preferences-privacy.md) (2026-09-03): Update 2026-09-09: Naarchy 0.4.0 is tagged on GitHub (v0.4.0 / f95ce49). Preferences, naarchy doctor, atomic stores, travel opt-in, IPC cleanup, 86 tests. EXTEND after the Sep 3 island post. - [I rebuilt the Omarchy site in TanStack Start in three days](https://www.michaelchurley.com/blog/omarchy-site-tanstack-start-rebuild.md) (2026-09-03): I ported the Omarchy marketing site and full manual into TanStack Start — 52 chapters, 16 news posts, Quattro hero etch, Vercel URL. Not the OS. The site. - [Naarchy: a Dynamic Island for Linux that owns the notch](https://www.michaelchurley.com/blog/naarchy-linux-dynamic-island.md) (2026-09-03): I built a native GTK4 Dynamic Island for Omarchy and Hyprland. It fills the 16" M1 Pro camera hole, grows ears for a live timer, and takes over the screen when the countdown hits zero. - [uncap.us: I rebuilt the social layer around the repo](https://www.michaelchurley.com/blog/uncap-us-repo-native-social-layer.md) (2026-08-31): I spent eight months turning a file-cabinet Git host into a repo-native work hub — Lens, origin sync, CLI/UnGit, TanStack Start. Live at uncap.us. - [Mack's BBQ Shack: I built a Main Street site that still feels like the pit](https://www.michaelchurley.com/blog/macks-bbq-shack-canton-condensation-site.md) (2026-08-30): Mack's Shack BBQ is a private Next 16 / Bun / Convex / Resend Canton pit site with a condensation-glass hero, skeuomorphic chalkboard menu, and catering leads from notify@uncap.us. Live on www.macksbbqshack.com; sixty-seven commits through the Aug 30 Resend fix. - [Omnux: I wired Linux for Apple silicon without lying about the GPU](https://www.michaelchurley.com/blog/omnux-linux-apple-silicon-truth-table.md) (2026-08-27): Omnux is the HurleyUS Linux-on-Apple-Silicon umbrella: truth table for what installs today, software-render honesty on M3, and the monorepo that coordinates docs, GPU, and report siblings. - [WNC History Tours: I shipped the booking shell before the tour pages existed](https://www.michaelchurley.com/blog/omnux-report-one-command-diagnostics-redaction.md) (2026-08-27): omnux-report is an offline MIT Shell diagnostics tool (HEAD f9729a6) that ships a consenting redacted .tar.zst + SHA256 for Omnux on Apple Silicon, with SEP structure only. Validated on a real M1 Pro Omarchy box plus a ten-check fixture harness; mx-mac packaging and owner release still open. - [slopops: I put my whole ops stack in one Omarchy bar popup](https://www.michaelchurley.com/blog/slopops-omarchy-ops-bar-panel.md) (2026-08-26): slopops is a public Omarchy Quickshell bar widget (0.1.0, HEAD after two 2026-08-26 commits) with one icon and five tabs (Fleet / Deploys / Sentry / Traffic / Issues), bash JSON fetchers, optional secrets.env, and a badge that goes red/yellow/green without opening another Electron tray. - [Hurleyus: I put Catppuccin Mocha in Hurley rally livery on Omarchy](https://www.michaelchurley.com/blog/hurleyus-omarchy-catppuccin-rally-theme.md) (2026-08-26): Hurleyus is the Omarchy Catppuccin Mocha theme pack with rally walls, Plymouth unlock, and an Asahi U-Boot 160x160 splash patch. Four commits; last push 65f4da3. Omarchy 4.0.2+ drops hyprland.lua unless you clone + symlink. - [omnux-gpu: I opened an MIT siege for M3 pixels and refused to call it a driver](https://www.michaelchurley.com/blog/omnux-gpu-mit-clean-room-m3-siege.md) (2026-08-24): omnux-gpu is the clean-room MIT attempt at Apple M3 AGX: scaffold, fifteen public issues, capture-first method. No working driver. Hardware is the bottleneck. - [twelveux: I hosted a shadcn registry and shipped Glass in one afternoon](https://www.michaelchurley.com/blog/twelveux-hosted-shadcn-registry-glass.md) (2026-08-15): I stood up twelveux: a hosted shadcn registry with Max, a phi Catppuccin theme, and pen.dev Glass. Playground on Vercel. Five commits. Same day. - [HMS: I replaced WordPress + Elementor with a live site that is the editor](https://www.michaelchurley.com/blog/hms-hustle-management-system-live-editor.md) (2026-08-14): I built HMS (Hustle Management System) so pages, blog, shop, and blueprints are block trees on Vite+ / React 19 / Convex / Clerk. Same URL for visitors and ?edit=1 for operators. Six commits. Version 0.0.0. - [Best Jeep Decals: I shipped a dark-first vinyl storefront for Jeep identity](https://www.michaelchurley.com/blog/best-jeep-decals-convex-stripe-storefront.md) (2026-08-08): Private Next.js 16 Jeep vinyl e-commerce: Convex catalog, Clerk auth, Stripe checkout, wishlist, reviews. 30 seeded SKUs at $29.99. Live at bestjeepdecals.com. - [reaferral: I built a free referral tracker so agents stop bleeding commissions into spreadsheets](https://www.michaelchurley.com/blog/reaferral-agent-referral-platform.md) (2026-08-08): Reaferral is an agent referral platform: who sent it, what fee attaches, where it sits in the pipeline. Accountability, not a CRM dump. - [Your ZAXBYS: I built a franchise ops platform so the store and the above-store stop living in different spreadsheets](https://www.michaelchurley.com/blog/yourzaxbys-franchise-management-platform.md) (2026-08-08): I shipped Your ZAXBYS as a Next.js 16 + Convex + Clerk franchise management platform (employees, stores, schedules, CAPs, audits) with a public marketing lander and a private dashboard, package 1.0.0, 46 commits, HEAD 705473f. - [WNC History Tours: I shipped the booking shell before the tour pages existed](https://www.michaelchurley.com/blog/wnc-history-tours-booking-shell-before-detail-pages.md) (2026-08-08): WNC History Tours is my Western North Carolina history-tour directory (Next.js 16, Convex, Clerk, Stripe deps). Homepage + schema + Blacksmith CI shipped; tour detail and checkout still missing. Domain answers Cloudflare 526. - [modern-design-playground: I left agents running for ten hours and they rebuilt nine worlds](https://www.michaelchurley.com/blog/waynesville-zaxbys-single-store-ops-portal.md) (2026-08-08): I shipped waynesville.yourzaxbys.com as a Next.js 16 + Convex + Clerk single-store portal for 424 Russ Ave: public events/careers/community out front, live shift metrics, Steritech CAPs, hiring, training, and attendance behind the door. Package 0.1.0, 235 commits, HEAD c9f97b7. Not the franchise SaaS sibling. - [The National NC: live NC news feeds before the bias AI ships](https://www.michaelchurley.com/blog/thenationalnc-live-rss-before-bias-ai.md) (2026-08-08): TheNationalNC ships live RSS before the bias-AI story: real feeds on the page, model claims parked until the pipes work. - [s12.in: I shipped a short domain that tracks clicks and hosts files](https://www.michaelchurley.com/blog/s12-in-url-shortener-file-hosting.md) (2026-08-08): s12.in is a private HurleyUS shortener + file host on Next 16.1.6, Convex, and Clerk with geo/device click recording and a 302 redirect spine. HEAD d6c074f; hero stats are still static JSX; Clerk on the observed deploy is pk_test. - [Monarch Mountain Foundations: I migrated WordPress to Next — DNS still serves PHP](https://www.michaelchurley.com/blog/monarch-mountain-foundations-wordpress-to-next-dns-still-php.md) (2026-08-08): Monarch Mountain Foundations is a full Next rebuild (Resend, Schema, gallery, CSP) whose www still serves PHP/8.3.33 on LiteSpeed while the Vercel alias sits with main auto-deploy off. HEAD c7e4e30; homepage motion still hotlinks wp-content. - [mockup-gallery: I shipped 10 industry mockups as a cold-outreach portfolio](https://www.michaelchurley.com/blog/mockup-gallery-ten-industry-cold-outreach.md) (2026-08-08): I stood up mockup-gallery — ten conversion-focused industry landers in one tabbed Next.js 16 gallery for HurleyUS cold outreach. Live on Vercel. Tailwind v4 fight, then Blacksmith/Fallow/Sentry/PostHog, then index,follow. - [MerchWinner: I shipped a POD course marketplace before the catalog had courses](https://www.michaelchurley.com/blog/merchwinner-pod-course-marketplace.md) (2026-08-08): I shipped MerchWinner as a POD course marketplace on Next.js + Convex + Clerk + Stripe: catalog, checkout, and course rails in one stack, not a Google Sheet. - [modern-design-playground: I left agents running for ten hours and they rebuilt nine worlds](https://www.michaelchurley.com/blog/modern-design-playground-afk-webgl-nine-worlds.md) (2026-08-08): I shipped a WebGL instrument homepage and nine landing worlds, then left a marathon agent loop overnight after WebGL went blank. Live on mdp-seven.vercel.app. - [Kings Roofing NC: I didn't redesign the WordPress site — I lifted it to Next.js](https://www.michaelchurley.com/blog/kingsroofingnc-pixel-perfect-wp-to-next-lift.md) (2026-08-08): Kings Roofing NC is a pixel-faithful WordPress-to-Next lift that kept #FF7620, the lion, color pickers, and metal-structure tree under Hustle-Launch/kingsroofingnc.com. Working surface is kingsroofingnccom.vercel.app (apex Cloudflare 526); HEAD 984746b. - [iLeague.golf: I built Patreon meets 18Birdies for golf creators — and left Clerk on pk_test](https://www.michaelchurley.com/blog/ileague-golf-patreon-meets-18birdies.md) (2026-08-08): iLeague.golf is the HurleyUS golf creator platform: Patreon-meets-18Birdies with Stripe tiers and a fifteen percent fee. Distinct from the iTour national tour lander. - [hurleyus.com: I shipped the parent site as a performance-pay membership lander](https://www.michaelchurley.com/blog/hurleyus-com-membership-growth-parent-site.md) (2026-08-08): HurleyUS.com is live as a membership-growth revenue partner lander for private and resort golf clubs — Next.js 16, Resend, Sentry, optional Convex — after an A/B flip that kept HomeA. - [GetFarmin: I scaffolded a farm equipment marketplace with escrow math first](https://www.michaelchurley.com/blog/getfarmin-farm-equipment-marketplace-scaffold.md) (2026-08-08): I scaffolded GetFarmin as a Next.js farm-equipment marketplace with Convex listings and Clerk: tractor/implement catalog path, not a $49 SaaS seat. Honest about Connect onboarding still unfinished. - [EverythingMonetized: I built a parody LMS where course bros sell courses about selling courses](https://www.michaelchurley.com/blog/everythingmonetized-parody-lms-course-bros.md) (2026-08-08): I shipped EverythingMonetized.com, a Next.js 16 + Convex parody LMS with 5 AI course-bro personas, 10 absurd courses, admin CRUD, and a purchase flow that admits nothing ships, then left the live catalog at Showing 0 of 0 because Convex never got seeded on Vercel. - [DJ Side Three: WNC wedding DJ funnel after cutting Clerk](https://www.michaelchurley.com/blog/djsidethree-wnc-wedding-dj-funnel-after-clerk-cut.md) (2026-08-08): djsidethree.com is a purple Next.js wedding-DJ landing with $500 / $1,200 / $1,500 packages, Convex inquiry, and an admin board. Feb 27 ripped Clerk, Turnstile, and Sentry to kill a production 500. Zod, rate limits, and Phosphor stayed. - [Cravees: I built a catering martech agency site with dual pricing honesty](https://www.michaelchurley.com/blog/cravees-catering-martech-agency-site.md) (2026-08-08): I built Cravees, a Next.js 16 + Convex + Clerk catering marketing agency site, with $299-$999 packages on the page and a different Stripe bronze/silver/gold ladder in code. Lead enums, ROI calculator, newsletter double opt-in. Live on www.cravees.com. - [Coordinator: I shipped an API queue control plane before the queue worker existed](https://www.michaelchurley.com/blog/coordinatorapp-api-queue-control-plane.md) (2026-08-08): CoordinatorApp is an API queue control plane on Next.js + Convex: jobs, workers, and status you can operate, not a package demo. - [Convex NextFaster: I swapped Neon for a Convex e-commerce template before the demo store existed](https://www.michaelchurley.com/blog/convex-nextfaster-perf-meets-convex-ecommerce-scaffold.md) (2026-08-08): I took NextFaster PPR/prefetch DNA, ripped out Neon/Drizzle storefront pages, and shipped a public Convex + Clerk + Stripe + Sentry + PostHog + Resend e-commerce template with ~1.1k lines of Convex schema/mutations and a marketing homepage that still links to a /products route that does not exist yet. - [Citation Manager: I built an Uberall competitor with 958 directories — and left auth on three stacks](https://www.michaelchurley.com/blog/citation-manager-uberall-competitor-958-directories.md) (2026-08-08): HurleyUS/citation-manager is an Uberall-framed local listings SaaS with a real 958-row directories.json registry, Convex schema, and 83 commits to HEAD 78e9fb1. Pack-day probe: citation-manager-pi.vercel.app returns 500 MIDDLEWARE_INVOCATION_FAILED. - [BreazyApp pocket PEO draft pack](https://www.michaelchurley.com/blog/breazyapp-pocket-peo-aes-before-stripe.md) (2026-08-08): Next.js 16 + Convex + Clerk pocket PEO for chain restaurants. Four portals, live waitlist, field protection, billing UI without stripe package. HEAD 2402b35. - [Bar-B-Que Wagon: I built a Bryson City Main Street smokehouse site](https://www.michaelchurley.com/blog/barbquewagon-bryson-city-hickory-smokehouse-site.md) (2026-08-08): Bar-B-Que Wagon is a Next 16 / Bun / Convex / Resend smokehouse site for 610 Main St, Bryson City, with Zod catering leads and Yelp plates in the repo. Live on barbquewagoncom.vercel.app; apex DNS still dark; thirty-eight commits through the Aug 8 robots header. - [Appalachian Estate Sales: I rebuilt the Elementor site in Next before the gallery existed](https://www.michaelchurley.com/blog/appestatesales-elementor-to-next-wnc-liquidation.md) (2026-08-08): Appalachian Estate Sales is a private Next.js 16.1.7 + Convex + Resend rebuild (HEAD 7a87d72, 24 commits) with seven routes and live lead capture on www.appestatesales.com. Previous/Upcoming still embed Facebook; README still says Convex/Resend are coming soon. - [SantaBox.org: I rebuilt a BestWNC copy-paste into a Christmas charity lootbox platform](https://www.michaelchurley.com/blog/santabox-charity-lootbox-rebuild.md) (2026-08-08): SantaBox.org is a Next.js 16 + Convex + Clerk + Stripe Christmas gift-box charity rebuilt from a BestWNC directory copy-paste into a donate/wishlist/partner stack. Feb 13 deleted fabricated nonprofit logos before they became a liability. HEAD 67712cb. - [shipthing: I named it for shipping rates and shipped a contacts spine instead](https://www.michaelchurley.com/blog/shipthing-contacts-spine-not-carrier-rates.md) (2026-08-08): ShipThing is the public michaelmonetized/shipthing spine: Next 16.1.1, Clerk, Convex contacts, Resend lead mail, Sentry, PostHog, and proxy.ts. Thirty-nine commits; HEAD 9f91d97. PLAN.md still lists unchecked USPS/UPS/FedEx boxes. - [mission-control: I built a p10k Go TUI for the whole portfolio — not the agent thread plane](https://www.michaelchurley.com/blog/mission-control-go-tui-p10k-portfolio-ops.md) (2026-08-08): Mission Control is the public Go Bubble Tea mc TUI for Vercel, git, Swift, and GitHub across a local portfolio, separate from Hurley Mission Control's Convex human|agent plane. Phase 1 is the local strip; Phase 2 scaffolds metered Fly VMs and BYO Claude without claiming the other product's name. - [glass-design-system: I shipped Apple SVG refraction, not the WebGL registry](https://www.michaelchurley.com/blog/glass-design-system-apple-svg-refraction-showcase.md) (2026-08-08): March 2026 I built a Next.js 16 glass design system with SVG feDisplacementMap Apple Liquid Glass, animated conic borders, jelly nav, and an 80+ Catppuccin showcase, live on Vercel. Distinct from twelveux WebGL Glass. - [ascii-commit-graph: GitHub's heatmap in my terminal — then I made the cd hook fast](https://www.michaelchurley.com/blog/ascii-commit-graph-terminal-heatmap-cd-hook-fastpath.md) (2026-07-31): ascii-commit-graph is a public 209-line shell heatmap (README V1.0.4, HEAD 9221b76) with a zoxide cd hook and a Jul 2026 single-git log / one-GraphQL fast path. ROADMAP release checkbox still open; install snippet still mentions michael-k/. - [niri-macos: I ported niri's scrollable tiling to macOS, then wrote an autopsy on my own Swift](https://www.michaelchurley.com/blog/niri-macos-scrollable-tiling-swift-ax-port.md) (2026-06-22): Native Swift 0.1.0 port of YaLTeR/niri's infinite horizontal strip to macOS via Accessibility APIs: four commits, ~226KB Swift, AUTOPSY.md roasting the early prototype, then nightly commits that answered it with 112 tests, ConfigManager, and a NiriCore library split. HEAD d21a739. - [iTour.golf: I rewrote the national creator-tour lander for May 2027 — production still serves the 2026 fake season](https://www.michaelchurley.com/blog/itour-golf-tour-lander-ahead-of-deploy.md) (2026-06-22): iTour.golf is the HurleyUS national golf creator tour, separate from the iLeague creator platform. Repo HEAD markets a 36-week 2027 season; live www still shows a 2026 story. No Stripe in the web package. - [Kitchen: I built a cloud project store where files are rows and disk is a Mirror — no git](https://www.michaelchurley.com/blog/kitchen-cloud-native-project-store.md) (2026-06-22): Kitchen is a public Next 16.2.9 + Convex + Clerk + Mirror project store (HEAD 97bec56, 34 commits, web 0.1.0) aimed at live sync without git ceremony verbs. Marketing routes return 200; discover/vision/mission still 404 and the working tree is dirty. - [Codefolio: I shipped a GitHub-sync portfolio SaaS with 4k lines of specs — and claimed a domain that isn't mine](https://www.michaelchurley.com/blog/codefolio-spec-first-github-portfolio-saas.md) (2026-06-22): Private michaelmonetized/codefolio is a Next.js 16 + Clerk + Convex developer portfolio platform: /:username public pages, resume, dashboard analytics, Free/$9 Pro/$29 Team. Three commits. Marketing OG points at codefolio.dev, which currently serves someone else's portfolio. HEAD c499a72. - [HustlePay: guest claim tokens before the framework adapters stopped being TODOs](https://www.michaelchurley.com/blog/hustlepay-guest-claim-tokens-auth-agnostic-stripe.md) (2026-06-22): Private michaelmonetized/hustlepay is auth-agnostic Stripe + Convex payments glue. One nightly commit shipped hp_guest_sessions claim tokens, Pay/ClaimAccount in @core, and seven hp_* tables — while @hustlepay/react still exports VERSION only. - [bundx-init: every Next.js repo gets https://.localhost via Caddy](https://www.michaelchurley.com/blog/bundx-init-nextjs-unique-localhost-https-caddy.md) (2026-06-22): bundx-init is a public curl-install shell CLI (HEAD c8b59ad, 3 commits) that gives each Next repo a hashed *.localhost HTTPS origin via Caddy and allowedDevOrigins. Fixture basic-next lands on port 6422; no live web app. - [bashformer: terminal Flappy Bird in Ink — after I deleted the C/SDL pile](https://www.michaelchurley.com/blog/bashformer-ink-flappy-after-c-sdl-cleanup.md) (2026-06-22): bashformer is a public dual-stack TTY game: Ink Flappy in index.tsx (~263 LOC) and a pure-bash Nerd Font platformer in bashformer.sh (~314 LOC). HEAD 541d2bc, 21 commits, Unreleased; Feb 21 deleted the C/SDL pile and fixed Pipe.scored. - [compare: back-to-back time lies — so I git-isolated the benchmark](https://www.michaelchurley.com/blog/compare-git-isolated-command-benchmarker.md) (2026-06-22): Public michaelmonetized/compare is an 825-line bash CLI that snapshots a dirty tree, runs each command on its own compare/ branch from a shared baseline, and logs POSIX time. Motivating examples: biome vs oxlint. VERSION 0.1.0. 3 commits. HEAD c5dbcd6. Second nightly is an empty tree. - [nvibe: I wired Cursor Agent + CodeRabbit into Neovim, then fought the window manager until "it works!"](https://www.michaelchurley.com/blog/nvibe-neovim-cursor-coderabbit-layout-until-it-works.md) (2026-06-22): nvibe is a public Lua Neovim plugin (HEAD 56d0152, 35 commits) that auto-lays out Cursor Agent + CodeRabbit on the left and LazyGit/shells on the bottom with a hard NvChad dependency. Arc runs from Product Hunt README through "it works!" window-management hell, NvimTree #4/#5 fixes, a GitHub Actions gate that got deleted for "Vercel is our only CI/CD," and June 22 nightlies. HEAD 56d0152. - [hustlemail.com: $8/mo email-marketing lander with missing /signup](https://www.michaelchurley.com/blog/hustlemail-com-eight-dollar-lander-missing-signup.md) (2026-06-22): Private Next 16 marketing shell for HustleMail (Free / $8 Pro / $24 Business, Mailchimp vs ConvertKit comparison table, docs index full of dead child links, CTAs to /signup and /login that do not exist). No README. 3 commits. HEAD ab984f2. Sibling Resend+Convex hustlemail mail client is a different repo. - [Sonny's Shining: I wrote a rubber-hose beat-em-up tragedy before I picked an engine](https://www.michaelchurley.com/blog/sonnys-shining-rubber-hose-beat-em-up.md) (2026-06-22): I shipped Sonny's Shining as a Fleischer-noir beat-em-up bible (GDD, novel, screenplay) plus a Next.js 16 newspaper site with an $8 Stripe preorder aimed at Christmas 2026. - [orclawstrator: I built the OpenClaw command center in Swift, then archived it for a Go TUI](https://www.michaelchurley.com/blog/orclawstrator-swift-appkit-to-go-tui-openclaw-gateway.md) (2026-06-22): Orclawstrator is the dual-runtime OpenClaw agent command center: Feb 2026 Swift AppKit plus SQLite and Gateway WS on :3377, then a Jun nightly that archived AppKit under swift-appkit/ and made the Bubble Tea TUI the recommended surface on the same ~/.orclawstrator/cache.db. Eight commits. HEAD 01d9a20. Separate from mission-control and hurley-mission-control. - [iPro.golf: the golf course & resort marketing agency lander — not iLeague, not iTour](https://www.michaelchurley.com/blog/ipro-golf-agency-lander-ecosystem-hub.md) (2026-06-22): HurleyUS/iPro-main-web is the live agency site for courses and resorts (ipro.golf): Next 16 pages, retainers $1,997 to $9,997+, ecosystem Coming Soon shells, iconference.golf rewritten to /iconf. Sibling vault michaelmonetized/iPro was SKIP. Distinct from ileague creator SaaS and itour season lander. 10 commits, HEAD b8dfdba. - [hustledesk.com: $8 flat helpdesk lander vs WordPress domain](https://www.michaelchurley.com/blog/hustledesk-com-eight-dollar-helpdesk-lander-wordpress-domain.md) (2026-06-22): hustledesk-com is a three-commit Next 16 lander selling $8 flat helpdesk tickets while hustledesk.com still serves a WordPress Make extra income parking page. The app subdomain 301s to that WordPress home; the Vercel project alias is DEPLOYMENT_NOT_FOUND. - [redactthing: streamer PII Chrome extension that still ships unused jQuery and a 404 lander](https://www.michaelchurley.com/blog/redactthing-streamer-pii-mv3-jquery-ghost-iframe-gap.md) (2026-06-22): Public michaelmonetized/redactthing is a Manifest V3 Chrome extension for streamers: one-click redact/blur/mask/hide of emails, phones, and custom PII via MutationObserver. HEAD 5c4661c. 7 commits. June 22 nightly rewrote foreground.js to vanilla JS — jquery.js stays in the manifest unused. Google Sites fails on iframe piles. hustlelaunch.com/redactthing 404s. ROADMAP describes a different product. - [shipprep: default APPLY for the HurleyUS JS shipping standard](https://www.michaelchurley.com/blog/shipprep-default-apply-biome-tsgo-blacksmith-vercel-off.md) (2026-06-22): Private Bun CLI (403-line bin/shipprep.mts) that migrates a Next/Bun project root onto Biome+tsgo scripts, Caddy localhost helpers, freview pre-push, Blacksmith ship.yml, and vercel.json with Git auto-deploys disabled on main/master. --audit is opt-in. 6 commits. HEAD c35a9d8. - [Assessment-Toolbar: pink SEO chrome bar that promises Lighthouse and double-injects itself](https://www.michaelchurley.com/blog/assessment-toolbar-chrome-mv3-lighthouse-missing-double-inject.md) (2026-06-22): Assessment-Toolbar is a public Chrome MV3 pink SEO strip (HEAD 9652620, 6 commits, GPL-3.0) that ships SpyFu through Whois helpers but zero Lighthouse links despite Manifest/README claims. background.js dual-loads content.js; hustlelaunch.com/assessment-toolbar is 404. - [freview: README says five reviews — REVIEW.md actually stitches six](https://www.michaelchurley.com/blog/freview-five-reviews-six-sections-observability-gate.md) (2026-06-22): freview (@hurleyus/freview 0.1.0, HEAD 23237d9, 22 commits) is a public zsh pre-push gate that always writes six REVIEW.md sections even though README still sells Five reviews, with observability as the undocumented sixth plus a Claude fallow-gate rewrite to push-only. - [hustleconvert.com: $8/mo popup lander, OptinMonster table, NXDOMAIN](https://www.michaelchurley.com/blog/hustleconvert-com-eight-dollar-popup-lander-nxdomain.md) (2026-06-22): Private michaelmonetized/hustleconvert-com is a Next 16 popup marketing shell (Free / $8 Pro / $24 Team, OptinMonster vs Sumo table, docs index of dead child links). Auth CTAs point at app.hustleconvert.com; claimed domain NXDOMAIN; vercel.app DEPLOYMENT_NOT_FOUND. Stock create-next-app README. 3 commits. HEAD a5d3f13. - [hustlechat.com: Intercom-alt lander that claims Convex with no Convex dep](https://www.michaelchurley.com/blog/hustlechat-com-intercom-alt-convex-claims-parkweb.md) (2026-06-22): Private Next 16 live-chat marketing shell — Free/$0 · Teams $8 · Premium $18 · Enterprise $28 vs Intercom $74+/Drift $2,500+/Crisp $25+. Copy says powered by Convex; package.json has next/react only. Fake @hustlechat/* SDKs, github.com/hustlechat 404, app/cdn NXDOMAIN. Live hustlechat.com is GoDaddy parkweb (/lander). Demo ChatWidget is setState. 3 commits. HEAD a570134. - [buffer-cli: wp-to-buffer-pro parity in the terminal — OAuth, no scrape](https://www.michaelchurley.com/blog/buffer-cli-wpzinc-parity-agent-oauth-no-scrape.md) (2026-06-22): buffer-cli is a public Bun + commander CLI (HEAD 778ecaf, 3 commits, ~1892 LOC) with WPZinc-shaped OAuth via localhost:9876 into ~/.buffer-cli. Gaps: no queue.ts, no --from, token refresh TODO, CHANGELOG still Unreleased. - [hustlecrm.com: $8/user CRM lander vs legacy PHP login domain](https://www.michaelchurley.com/blog/hustlecrm-com-eight-per-user-crm-lander-legacy-php-domain.md) (2026-06-22): Private Next 16 marketing shell for HustleCRM (HEAD 2c50bbb, 3 commits): HubSpot/Pipedrive/Salesforce comparison, $8/user/mo Pro + 14-day free trial, Kanban demo chrome, docs/API cards with hash hrefs. Live hustlecrm.com still serves a 2023 Bootstrap PHP login; Vercel aliases 404. - [animated-gradient-border: transparent glass with a spinning conic ring](https://www.michaelchurley.com/blog/animated-gradient-border-transparent-mask-composite.md) (2026-06-22): animated-gradient-border is a public CSS lab (HEAD 5e9b611, 5 commits, empty README) that keeps a spinning conic ring via mask-composite: exclude so the video plate shows through the middle. Sibling to glass-design-system AnimatedBorder; open index.html locally. - [hustleforms.com: $8/mo CRM form lander, Typeform table, NXDOMAIN](https://www.michaelchurley.com/blog/hustleforms-com-eight-dollar-crm-form-lander-nxdomain.md) (2026-06-22): Private michaelmonetized/hustleforms-com is a Next 16 marketing shell (Free / $8 Pro / $24 Business, Typeform/Jotform/Wufoo table, docs hash-anchor index). CTAs hit local /signup|/login|/contact with no pages; hustleforms.com NXDOMAIN; vercel.app DEPLOYMENT_NOT_FOUND. Stock create-next-app README. 3 commits. HEAD 80c54f4. - [mkproject: bash scaffold that ships .env-safe git init + RUNAFTER](https://www.michaelchurley.com/blog/mkproject-bash-scaffold-template-git-init-runafter.md) (2026-06-22): Public 76-line Shell CLI that copies ~/.config/mkproject/template, git inits with branch+commit message from .env, and runs RUNAFTER (default code .). The tool that created empty-junk siblings mkproject-1 and 49. 9 commits. HEAD 321012c. - [new-design-gallery: I shipped a catalog UX, not another tabbed lander kit](https://www.michaelchurley.com/blog/new-design-gallery-embla-catalog-not-landers.md) (2026-06-22): I stood up new-design-gallery — Embla carousel, category filters, Radix lightbox, Web Share/clipboard, and per-slug detail routes for 11 catalog cards. Live on new-design-gallery.vercel.app. Same March 28 day as mockup-gallery; different product. - [WhisperCPPonEverything: Ctrl+W streaming STT menubar, cyan glow, whisper-stream](https://www.michaelchurley.com/blog/whispercpponeverything-ctrl-w-streaming-stt-cyan-glow.md) (2026-06-22): Private Swift Package macOS menubar dictation (WhisperCPPonEverything 2.0.0, HEAD d33e5e2, 11 commits): Ctrl+W toggles idle/streaming, spawns Homebrew whisper-stream + ggml-medium, injects via CGEvent with suffix-prefix dedupe and a soft cyan 20-layer glow, plus macOS 15+ listen/post event access. README still describes the batch orange/silence path; renamed from VoiceType. - [neovim-ide: the GIGACHAD of NvChad is a tmux layout (ollama ASCII, cursor-agent reality)](https://www.michaelchurley.com/blog/neovim-ide-tmux-gigachad-layout-cursor-agent.md) (2026-06-22): Public michaelmonetized/neovim-ide: zsh + tmux launcher that builds an IDE-shaped pane grid (Agent | Neovim | Tasks/Git + Console/Terminal). Banner still draws ollama; init starts cursor-agent. README says MIT, LICENSE.md is GPL-3.0, package.json ISC. install.sh is 0 bytes. Ink CLI never calls tmux. 9 commits. HEAD 38ed773. - [devhost: Ink TUI + Caddy :80 multi-project .localhost manager](https://www.michaelchurley.com/blog/devhost-ink-tui-caddy-port80-multihost.md) (2026-06-22): Private michaelmonetized/devhost is a Bun+Ink host manager: sequential ports from 3001, /etc/hosts DEVHOST markers, Caddy :80 reverse_proxy to each project, start/stop/open lifecycle + TUI. 1 commit (+1426). HEAD db69995. Live config still lists bestwnc-com.localhost. Distinct from bundx-init HTTPS Next patcher. - [shagent: Bun MCP + OpenRouter free loop after I deleted the Zsh shell harness](https://www.michaelchurley.com/blog/shagent-bun-mcp-openrouter-after-zsh-harness-delete.md) (2026-06-22): Public michaelmonetized/shagent: May init was a 258-line Zsh THOUGHT/COMMAND OpenRouter harness (rg/fd/eza/bat, mcp-cli, qmd). Jun 22 nightly deleted src/shagent.sh and shipped Bun CLI + MCP filesystem client + OpenRouter JSON tool loop (max 15 turns, default openrouter/free). Empty README. Marketing index.html is fiction (Rusty P. Shackelford). 3 commits. HEAD 61ecd7d. Sibling orclawstrator is a different repo. - [nfglyph: Bun raw-ANSI Nerd Font picker — fuzzy names, Linux quit fixed](https://www.michaelchurley.com/blog/nfglyph-bun-ansi-nerd-font-picker-linux-quit.md) (2026-06-03): Public michaelmonetized/nfglyph: zero-dep Bun TUI glyph picker with vim motions. Jan 30 shipped Unicode range-scan + hex search; Jun 1 upgraded to Nerd Fonts 3.4.0 glyphs.json (50,174 named) + fuzzyMatch; Jun 3 fixed Linux quit (keepAlive vs process.exit) and clipboard cascade pbcopy->wl-copy->xclip->xsel. README still says 15k+ / hex-only. 4 commits. HEAD 1dfdc82. - [svganimator: Electron Svgator clone → Canaveral monorepo SVG keyframe studio](https://www.michaelchurley.com/blog/svganimator-electron-to-canaveral-keyframe-export-studio.md) (2026-05-14): Public HurleyUS/svganimator: May 3 Electron Svgator-inspired animator + inspiration scrape; May 4 exports + migrate to Bun Canaveral monorepo. HEAD keeps package name canaveral but product identity SVG Animator; @canaveral/svg (963 LOC Zod schemas, svg/lottie/gif/mkv), TanStack web studio, Electron shell, Expo card. 13 commits. HEAD 86bb4a2. Sibling surfaces: private HurleyUS/canaveral launch pad; local draw-on Bun SVGanimator; tsxsvg plan-only. - [Canaveral: Bun TanStack Start launch pad — web, Electron, Expo, Caddy](https://www.michaelchurley.com/blog/canaveral-bun-tanstack-start-web-desktop-mobile-caddy.md) (2026-05-14): Canaveral is a private HurleyUS Bun monorepo launch pad (TanStack Start web, Electron, Expo, shared) with in-repo Caddy at canaveral.localhost:5337 and a ten-check freview gate. HEAD df2c041; eleven commits; AGENTS.md still oversells the Biome to house to freview path while package.json lint is oxlint-only. - [HurleyUS Agent SOP: Ship or shut up, OpenClaw memory layers, DHH gstack mandate](https://www.michaelchurley.com/blog/hurleyus-sop-ship-or-shut-up-openclaw-dhh-gstack.md) (2026-03-31): Public single-file AGENT_SOP.md (548 lines, v1.0, HEAD c418e5e, 2 commits) for HurleyUS agents: Ship or shut up, ~/.openclaw/workspace memory layers, hard rules, Graphite/send-agent, plus PR #2 DHH research-first + gstack + deploy discipline. Docs-only; distinct from hurley-mission-control. - [notion-cli: OpenClaw/Claude Code Notion API skill — CRUD, filters, Markdown](https://www.michaelchurley.com/blog/notion-cli-openclaw-skill-crud-markdown-property-filter.md) (2026-03-28): Public michaelmonetized/notion-cli: Bun/TS Notion API CLI for agents — db/page/pages/block/search subcommands, -p/-n property filters, page→Markdown, SKILL.md + setup for OpenClaw/Codex. package 1.0.0 vs banner v2.0.0; DELIVERY still flat list-databases; spaces/page/search modules unused by cli. 1 commit. HEAD 5309974. - [resendld: OpenClaw Resend inbound daemon — poll, hooks/agent, Caddy UI](https://www.michaelchurley.com/blog/resend-listening-daemon-openclaw-poll-hooks-agent-caddy.md) (2026-03-27): Public michaelmonetized/resend-listening-daemon (resendld 0.0.0-rc.0, HEAD 4fbec17, 46 commits): Bun daemon polls Resend /emails/receiving every 5s via pure fetch, stores markdown under ~/.openclaw/workspace/mail/, dispatches to OpenClaw /hooks/agent (cron fallback), with Convex + TanStack Start web at https://resendld.localhost via Caddy. install.sh covers macOS+Arch; afternoon of resend-cli PATH hell before zero-CLI. - [codemail: mail.config.ts founder email — per-domain, not per-seat](https://www.michaelchurley.com/blog/codemail-mail-config-as-code-founder-email-infra.md) (2026-03-26): Private michaelmonetized/codemail: Turborepo mail stack where mail.config.ts is law (Convex + Clerk + Resend + Fly SMTP + Next web mail/dashboard). Pricing Free Forever BYO keys / $8 Simple / $80 Managed. README still lists IMAP; PLAN marks IMAP out of MVP. Live codemail-web.vercel.app 200; codemail.vercel.app 500. 27 commits. HEAD e9fece4. - [milkup: TipTap+Convex WYSIWYG that still ships a Milkdown README](https://www.michaelchurley.com/blog/milkup-tiptap-convex-wysiwyg-stale-milkdown-readme.md) (2026-03-25): Public michaelmonetized/milkup: same-day 9-commit arc (2026-03-25). Milkdown+Convex POC → mock strip → TipTap WYSIWYG + Convex listMedia → React19/Next14.2 CVE pin → Next16 → Opus QA deletes milkdown-video-plugin. README still markets Next15/Milkdown/MOCK_MEDIA/!video[](). HEAD 45fb587. Video insert is ![Video](url). No upload mutations. - [simple: Next.js 16 Firebase auth starter — social login, keep-Firebase decision](https://www.michaelchurley.com/blog/simple-nextjs-firebase-auth-social-keep-decision.md) (2026-03-23): Public michaelmonetized/simple: Next 16 + React 19 Firebase Auth/Firestore starter — email+password, Twitter/Facebook/Google connect, AuthProvider, CRUD helpers, clamp spacing + Catppuccin tokens. Launch-week: CVE bumps, security headers, console strip, docs keep-Firebase vs Convex. HEAD da7e0b0 · 10 commits. Vercel homepage DEPLOYMENT_NOT_FOUND; create-next-app README; login page still named RegisterPage. - [hurley-mission-control: I put humans and agents in the same thread model](https://www.michaelchurley.com/blog/hurley-mission-control-human-agent-comms.md) (2026-03-22): Hurley Mission Control is the Convex + Clerk + Next plane where users.kind is human|agent. Deliveries, idempotent sends, 2s poll. Not the Go TUI. Not mission-control-os. Daemon still stubs. - [ConnectedIn: MV3 LinkedIn auto-connect Chrome extension](https://www.michaelchurley.com/blog/connectedin-mv3-linkedin-auto-connect-chrome-extension.md) (2026-03-21): Public michaelmonetized/ConnectedIn is a Manifest V3 Chrome extension that auto-clicks LinkedIn Connect buttons with configurable 100-5000ms delays, popup stats, chrome.storage.sync persistence, and a ~1,100 connects/week rate-limit warning. Same-afternoon 2-commit ship (init + INSTALL.md). HEAD a6236fa. Gaps: no content_scripts registration, missing images/icons. - [Agent OS: Bun + Ink + WebSocket shell-native orchestrator](https://www.michaelchurley.com/blog/agent-os-bun-ink-websocket-zsh-orchestrator.md) (2026-03-21): HurleyUS Agent OS is a public Ink + WebSocket + interactive-zsh orchestrator (HEAD f66fffa, 2 commits) with a Mission Control Phase 1 seed and a separate Anthropic core.ts persona queue. Local only on ws://localhost:9999; SDK dep missing and .env still tracked. - [stripe-convex: I shipped a Theo-compliant Stripe+Convex library — and never published the package](https://www.michaelchurley.com/blog/stripe-convex-email-payments-theo-unpublished.md) (2026-03-20): Public michaelmonetized/stripe-convex is a TypeScript Stripe + Convex payment library (email-indexed sc_* tables, cart/coupons, Pay/AddToCart/Checkout/Has, 19 webhook events, Theo sync/portal helpers). package.json 0.1.0 with a release workflow and README version badge, yet zero GitHub Releases and registry 404; LICENSE still says Michael Shilman. HEAD 22e099e (PR #13). 11 commits. - [MyBathroomConversion.com: Elementor Opt-In → SalesPromis, then I deleted the xdebug_info() probe](https://www.michaelchurley.com/blog/mybathroomconversion-elementor-salespromis-xdebug-purge.md) (2026-02-27): www.mybathroomconversion.com is a SalesPromis/Hustle Launch Elementor lander whose child theme already POSTs Opt In leads to api.salespromis.com while PLAN.md once claimed lead capture Not Started. Feb 27 HEAD dc5a091 deleted webroot xdebug_info(); WP File Manager 7.2.9 and a hardcoded API key remain. - [Boilerplate: Clerk + Drizzle + Stripe Next shell, CVE-bumped to 16](https://www.michaelchurley.com/blog/boilerplate-clerk-drizzle-stripe-next16-cve.md) (2026-02-22): boilerplate is a private michaelmonetized Next shell with Clerk, Drizzle/@vercel/postgres subscriptions, and a Stripe webhook, CVE-bumped to Next 16.1.6 at HEAD 6f2dd2f. Live on boilerplate-fawn-gamma.vercel.app and boilerplate.hustlelaunch.com; marketing routes are still ten-line stubs. - [launchpad: NYE 2024 Hustle Launch boilerplate — README stack, unwired providers](https://www.michaelchurley.com/blog/launchpad-nye2024-boilerplate-providers-unwired.md) (2026-02-06): Public michaelmonetized/launchpad is the NYE 2024 Hustle Launch premier-framework claim (Next 15 + shadcn + Clerk/Convex/PostHog providers + Resend /api/send) while app/page.tsx is still Create Next App, providers never mount in layout, no convex/ folder, Stripe unused except Theo STRIPE.md, and launchpad.hustlelaunch.com is NXDOMAIN. 8 commits. HEAD 5447591. - [iLeague-app: the generic influencer monorepo before the golf rebrand](https://www.michaelchurley.com/blog/ileague-app-influencer-monorepo-before-golf-rebrand.md) (2026-02-04): Public HurleyUS/ileague-app is the Jan 9 2026 Bun monorepo (Next.js 15 + Expo 52 + Convex + Clerk + Stripe Connect) for a generic influencer/fan league platform (violet #7c3aed, isInfluencer schema, gaming-to-lifestyle categories). HEAD 7af9d80 (Feb 4) wires mobile leagues to Convex and adds eas.json; www.ileague.app is Coming Soon static, distinct from ileague.golf. - [SalesPromis: GitHub still versions a WP Engine Elementor funnel — live left for Lovable](https://www.michaelchurley.com/blog/salespromis-wp-elementor-dump-vs-lovable-live.md) (2026-01-31): HurleyUS/SalesPromis is a private WordPress wp-content dump of an Elementor SSDI qualify funnel (Salert + HurryTimer + custom ssdi-qualify.js). Live salespromis.com is already a Lovable/Vite SPA. Two commits. HEAD only added a Stripe cursor rule. - [fab-analytics: same-day PHP+JS GA drop-in that writes JSON to disk](https://www.michaelchurley.com/blog/fab-analytics-same-day-php-js-ga-drop-in-json-disk.md) (2024-07-10): Public michaelmonetized/fab-analytics is a 0.1.3-rc first-party Google Analytics drop-in: fab-analytics.js posts pageviews, mailto/tel clicks, and form submit/abandonment to a PHP endpoint that writes logs/{domain}/*.json under hustlelaunch.com. 31 commits in one July 2024 day to first production run; HEAD 8218088 cleanup. Empty README/LICENCE. Sibling BestWNC 2026 directory analytics is a different product. - [Jennings Custom Homes: post-malware WP rebuild checklist — still on Bluehost](https://www.michaelchurley.com/blog/jenningscustomhomes-post-malware-checklist-still-on-bluehost.md) (2024-06-06): Public Hustle-Launch/jenningscustomhomes is a 4-file WordPress ops checklist for a Highlands/Cashiers NC luxury builder rebuild after Bluehost malware. Five commits Jun 4-6 2024; HEAD ad1f91f flips nine boxes to [x] and leaves TrustIndex plus login details open. Live www.jenningscustomhomes.com still resolves to Bluehost LiteSpeed at 75.98.174.238. --- # One-line install Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-one-line-install Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, install ## The habit You already know the install dance. On a Mac you open a disk image, drag a bundle into Applications, and eject the volume. On Windows you click through a setup wizard and hope it did not add a second updater. Illustrator and Photoshop train a third habit: a manager app fetches the studio, parks it where that manager wants it, and keeps a login between you and the icon. Affinity is closer to a normal desktop install. You still hunt a menu entry when the wizard finishes. Linux designers collect one more habit on top of that. `sudo` into a package manager. Approve a Flatpak portal. `chmod +x` an AppImage and lose track of which folder holds it. Immutable desktops add a hard stop. The system image is not yours to edit. The hand still wants one command and a launcher the menu can find. Omadesign meets that hand with a shell line. Paste it. The binary lands where your user can run it. The desktop entry lands beside your other home-directory launchers. You open the studio from the menu or from the path. That is the job of the install, and it is the whole job. ## The constraint The studio is one native binary. The file you save is one `.oma`. Undo is one step on the document you have open. There is no Creative Cloud hop between the download and the first rectangle, and there is no dialog that rewrites a camera file while you are still trying to get the app onto the machine. The machines in the room include Silverblue, Bazzite, NixOS, and SteamOS desktop. On those systems `/usr` belongs to the image. A package that needs root to copy itself into `/usr/bin` fails, or it asks you to layer the OS, or it pushes you into a container you did not ask for. Omarchy sits in the same list. A mutable Ubuntu or Arch box can write `/usr` too, and the install still refuses that path so the command means the same thing on every one of those machines. So the shape is a script fetched over HTTPS, a binary under `~/.local/bin`, and a desktop entry under your home directory. One line. Same line on an immutable image and on a normal install. Preferences stay in `~/.config/omadesign` (or `XDG_CONFIG_HOME`). The document you will make later stays an `.oma` on disk. The installer does not need a project, an account, or a cloud session to finish. ## What landed This is the install as it stands for 0.5.8. The command was already the front door. 0.5.8 is the build that public line put on the machine. ```sh curl -fsSL https://omadesign.app/install | sh ``` `curl -fsSL` fails on HTTP errors, stays quiet, and follows redirects. The script at `https://omadesign.app/install` resolves the current release, downloads the archive, and checks it. The 0.5.8 validation ran that unmodified public script on a local ARM64 machine. It resolved latest, downloaded the ARM64 archive, checksummed it, and installed 0.5.8. Welcome on that install shows 0.5.8. The running binary matches the published executable hash. What gets written: - The executable is `~/.local/bin/omadesign`. - A desktop entry is written under your home directory. One `omadesign.desktop` launcher remains after the 0.5.8 install. - Both `.oma` and `.omaphoto` defaults point at that launcher. - Preferences already on disk are left alone. - The prior binary, icon, and launcher are backed up before replacement. The binaries are glibc 2.35. They run on Asahi Omarchy, Ubuntu 22.04 and newer, and current Arch. They do not demand a glibc newer than 2.35. ARM64 and x86_64 are the two archives. The install picks the one that matches the machine. The deeper split between those tarballs is its own decision. Here the point is that the same shell line is how either one arrives. If the shell cannot find `omadesign` after the script returns, the binary is still at `~/.local/bin/omadesign`. Add `~/.local/bin` to `PATH`, or call the binary by that full path. `omadesign --version` prints the installed version without opening a window. That flag came with the portable packages. You can also just launch and read the version on the welcome screen. Installed branding, plugin source, and the offline plugin docs match the release. Lua and the RAW decoder travel inside the binary's package. You do not install a second runtime to open the studio or to develop a camera file. Reinstalling preserves a plugin you already changed in its installed copy. The update path inside the app, when you take it later from the wordmark menu, runs this same official installer, waits for work in progress, writes a recovery snapshot, and restarts with the same open documents and photo adjustments. ## In the hand Open a terminal in your own user session. Paste the line. ```sh curl -fsSL https://omadesign.app/install | sh ``` Wait until the shell prompt returns. Do not prefix it with `sudo`. The script is supposed to be you, writing into your home directory. Then either: ```sh omadesign ``` or, if the command is missing from `PATH`: ```sh ~/.local/bin/omadesign ``` The window that opens is the welcome screen. It is a local file browser with creation actions in the middle. Your Work lists `.oma` files. Projects lists folders that contain `.omabrand`. You can close it and come back from the desktop entry. The menu name is the launcher the script registered. File associations for `.oma` and `.omaphoto` point at that same launcher, so a double-click in the file manager opens the studio you just installed. Check the version without guessing which copy launched: ```sh ~/.local/bin/omadesign --version ``` On this release the welcome screen also shows 0.5.8. If you already had a copy, the previous executable and launcher metadata were backed up. Your config directory was not wiped to make the new binary fit. When a newer build is offered later, the in-app update uses this installer. It does not invent a second download path. You can keep using the shell line yourself. Either way the files land in the same home-directory places. ## The edge Nothing is written to `/usr`. The desktop entry stays under your home directory. System directories are outside the job. The checksum is a hard stop. A missing, malformed, or mismatched checksum stops installation. The failure checks on the installer confirm that an invalid download stops before extraction. You do not get a half-unpacked studio in the system tree, because the script was never going to unpack one there. A bad archive does not become the binary you launch tomorrow morning. The line also does not set up a distro package, a Flatpak, or a root-owned service. Silverblue, Bazzite, NixOS, SteamOS desktop, and Omarchy get the same home-directory result as Ubuntu and Arch. If your policy forbids piping a script into a shell, download the ARM64 or x86_64 tarball from the release and unpack it yourself. The command above is the supported one-line path, and it is the one the 0.5.8 machine actually ran. Paste `curl -fsSL https://omadesign.app/install | sh`, then run `~/.local/bin/omadesign`. ## The thread Part 1 of 144 in the Omadesign 0.5.8 feature thread. Start of the thread · [Next](/blog/omadesign-0-5-8-portable-arm64-and-x86-64) --- # Portable ARM64 and x86_64 Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-portable-arm64-and-x86-64 Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, binaries ## The habit You are used to one download that pretends to be every computer. A Mac universal binary. A Windows installer that detects the chip at the end. An Electron app that ships a browser, a stack of shared libraries, and the actual drawing code somewhere underneath. Illustrator and Photoshop show up through a manager that hides the architecture until something fails. Affinity ships desktop builds you pick by operating system. On Linux the honest version of that habit is two files. One for ARM64. One for x86_64. You learn to read the filename before you unpack it. Asahi on a Mac, a Framework or a ThinkPad on x86, a handheld desktop session, an immutable image, a plain Ubuntu 22.04 box. The studio has to be the same program on all of them, or you start keeping two kinds of documents and two sets of muscle memory. The hand wants to download a tarball, see a version, and run it. The hand does not want a wrapper that boots a second browser so a designer can draw a rectangle. ## The constraint One binary. One `.oma`. The same layer stack on every machine you sit down at. Undo stays one step on that document. There is no cloud round-trip required to open the file you saved on the other architecture. There is no dialog on install that rewrites a camera original so the two builds can "share" a photo. The floor is glibc. If the binary demands a glibc newer than the oldest machine in the room, that machine is out. Ubuntu 22.04 is the line that still shows up on real desks. Asahi Omarchy is ARM64 and current. Arch moves fast and already has a newer glibc, which can run a binary built for an older one. The decision is a ceiling on what the binary requires: glibc 2.35, and nothing newer. Both architectures share that ceiling so "the studio" means one program with two builds, not two products. Immutable systems still cannot take a write to `/usr`. The portable archive is what the home-directory installer unpacks. You can also take the tarball itself. The archive is the unit. The installer is only the fetch. A native Rust binary (eframe, egui, one process) is the other half of the shape. The tarball does not carry an Electron wrapper. Lua, the RAW decoder, and the JPEG and C++ pieces the studio needs are bundled inside the package. You do not apt-install a second stack to grade a NEF or to run a plugin. ## What landed Omadesign 0.5.8 ships portable ARM64 and x86_64 tarballs. Both archives report 0.5.8. Both require no glibc newer than 2.35. That is the build that runs on Asahi Omarchy, Ubuntu 22.04 and newer, and current Arch. The 0.5.8 check ran both packages. They inspect and render native documents. They run live-text transforms, a custom pixel filter that preserves alpha, and a two-file batch. They refuse to clobber an output that already exists. They install the starter plugins, the docs, and the licenses. Reinstalling keeps a plugin you already edited in its installed copy. Thirty-nine packaged source files match their originals. The public downloads match the local archives and the published checksum files. The x86_64 executable was tested through QEMU against Ubuntu 22.04 and glibc 2.35. That is a real glibc check. It is not a physical x86_64 GPU run. Say that plainly so you know what was proven. The ARM64 package was installed from the public installer on the local ARM64 machine, and the running binary matches the published hash. Welcome shows 0.5.8. Dependencies that used to tempt a system package are inside the archive. Lua is bundled. The RAW decoder is bundled. You open a camera file with the binary you downloaded. The same `.oma` opens on either architecture. Format notes travel in the document when a feature had to be converted. The file does not grow an architecture tag you have to strip before the other machine will read it. `omadesign --version` (or `-V`) prints the version without opening a window. Use it after you unpack if you want to see 0.5.8 before the welcome screen does. The installer at `https://omadesign.app/install` selects the archive for the machine you are on, checksums it, and places `omadesign` at `~/.local/bin/omadesign`. Taking the tarball from the release page is the same bits without the script. Either way you get one native executable and the desktop entry story from the install, not a browser runtime sitting next to it. ## In the hand If you want the script to choose the archive: ```sh curl -fsSL https://omadesign.app/install | sh ~/.local/bin/omadesign --version ``` If you want to see the tarball yourself, download the ARM64 archive or the x86_64 archive from the 0.5.8 release. Check it against the published SHA-256 before you unpack it. A missing, malformed, or mismatched checksum is a stop, same rule the installer uses. Unpack it as your user. Put the executable on `PATH` or call it by path. ```sh ~/.local/bin/omadesign ``` The process that starts is the studio. Design is the default persona. The welcome screen is the local browser for your `.oma` files and your project folders. Open a document you saved on the other architecture. It opens at its own size, in its own tab. Save still writes `.oma`. Photo settings still write a `.omaphoto` beside the original. The camera file stays the camera file on both chips. On Ubuntu 22.04 the glibc you already have is the floor these binaries were built for. On current Arch the newer glibc runs them. On Asahi Omarchy you want the ARM64 archive. On an x86_64 desktop you want the x86_64 archive. Mixing them up fails at load time, the way any ELF fails when the machine cannot execute it. The filename and the checksum are how you tell them apart before that happens. Launch from the desktop entry if the installer wrote it. One `omadesign.desktop` is the launcher. `.oma` and `.omaphoto` open with it. You should not have two menu entries fighting over the same suffix after this install. ## The edge The binary will not run on glibc older than 2.35. That is the boundary. Ubuntu releases before 22.04 are outside it. The build does not ship a private newer glibc to drag those machines along, and it does not demand a glibc from a rolling snapshot you do not have. There is no Electron wrapper in the tarball. You do not get a second browser, a second updater, or a second document format to make the two architectures agree. ARM64 and x86_64 are two builds of the same studio. The x86_64 validation did not claim a physical x86_64 GPU pass. If you are on real x86_64 hardware, you are past the check that was written down. The check that was written down is glibc 2.35 through QEMU, plus the archive tests both packages did run: documents, live text, a pixel filter, batches, checksums, and the version string 0.5.8. Run `~/.local/bin/omadesign --version` on the machine in front of you. You want to read 0.5.8 from the build that matches that machine. ## The thread Part 2 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-one-line-install) · [Next](/blog/omadesign-0-5-8-first-five-minutes) --- # First five minutes Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-first-five-minutes Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, welcome ## The habit The first five minutes in Illustrator are a new-document dialog. Artboard size. Color mode. Raster effects. Then the toolbar, if you have not hidden it. Photoshop starts from a canvas size or from Open, and the tools you need for type and vectors are a mode change inside a pixel document. Affinity Designer and Photo split the same way by app, with StudioLink when you want the other tool well without a second file. You spend the first minute choosing a room. You spend the next four remembering which key makes a rectangle. You also know the template browser. A grid of starters, a search box, a size you override because the starter was US Letter and the job is a poster. The good versions open as a real document you can edit. The bad versions open as a locked preview you have to "apply" before the type is type. On Linux you add one more first minute: did the app follow the desktop font, or did it ship a theme that fights the rest of the screen. The first five minutes should end with a mark on a page, not with a settings hunt. ## The constraint One binary. One `.oma`. Five personas over one layer stack. The first session cannot ask you to pick an app. Design has to be the default, because a mark, a poster, and a layout are the work you can start on an empty page. Pixel paint needs a pixel layer, and that layer lives in the same document. Photo needs a folder of pictures, not a new blank board. Motion needs artwork already on the canvas, so it has no empty-workspace button on the welcome screen. Undo is one step once you are drawing. The welcome screen is not allowed to become a cloud browser you must sign into before `R` does anything. Templates ship in the binary. They use fonts you already have. They need no network. Chrome follows the desktop: Omarchy colors, the font from `omarchy font current` or fontconfig, Phosphor Light icons. The first five minutes inherit that. You do not set a private light/dark switch before you can see the page. The feature note lists a demo in the same breath as a size and the templates. The manual's first five minutes do not name a separate demo control. The doors the manual gives you are the ones below. Use those. ## What landed Launch `omadesign`. The welcome screen is a local file browser. Creation actions sit in the center. 1. Choose **Vector** or **Layout** when you want templates. **+ Vector** opens 52 editable vector templates. **+ Layout** opens the frame starters: Fieldwork responsive prototype, mobile screen, landing hero, dashboard, and card stack. The file icon beside Vector or Layout opens a blank size chooser. Raster's file-side path is a blank raster size. You can also click a thumbnail of a document already on disk. 2. **Design** is the default persona once you are in a vector document. `R` draws a rectangle. `P` is the pen. `T` is type. 3. **Pixel** paints when you press `B`. Paint goes on a pixel layer. If the document is still vector-only, add that layer from the Layers studio first. 4. **Photo** opens a folder of pictures and grades them. Crop is `C`. The develop controls sit in Light, Color, and Detail. The camera file stays where it was. Startup preferences live under **omadesign → Config**. You can start on the welcome screen, lock a mode, or remember the last mode. A separate setting picks the welcome tab: New, Templates, Recent, or Recovered, including remember-last-tab. That preference is how the first five minutes look the second time, not a different app. Templates open as unsaved documents. Paper, artwork, and copy layers are editable. Work you already had stays in its tab. Search the 52 by name or idea, filter the nine categories, pick a built-in size or type width, height, and DPI. Double-click a card, or select it and choose **Use this template**. Very small sizes drop secondary copy that would be unreadable. Portrait, square, and wide pages each have their own artwork. The weekly drop plan is an editorial list. It does not schedule posts and it does not gate the files. All 52 are local on day one. **+ Project** opens a brand editor. A project is a folder with `.omabrand` in it. It does not need an account. **Learn with AI** and **Create with agent** hand a prompt to whatever agent Omarchy has set as the default. If you have not chosen one, the screen tells you to set it under **Omarchy → Setup → Default → Agent**. The studio does not pick an agent for you. You can ignore both boxes and draw. ## In the hand ```sh omadesign ``` If the command is not on `PATH`, call `~/.local/bin/omadesign`. On the welcome screen, click **+ Vector**. The chooser opens on the 52. Click a card. Click **Use this template**, or double-click the card. A new unsaved document opens. Design is the persona in front of you. Press `R`. Drag on the page. You have a rectangle. Press `P`. Click a corner, click-drag a smooth point, click the first point to close, or press Enter to finish an open path. Press `T`. Click once on the page. The word Type is a placeholder. The first character you type replaces it. Enter is a new line. Esc or a click away finishes the text. Press `V` if you need to move what you just made. That is Move. Eight handles scale. The handle above the box rotates. You are still in the same `.oma`. Switch to Pixel when you want paint. Add a pixel layer if Layers does not already have one. Press `B`. The brush paints on that layer. `[` and `]` change size. The vectors you drew stay vectors on their own layers. For a photograph, go back to welcome or use the Photo creation action. **+ Photo** opens the Photo workspace. The folder icon is the folder chooser. The image icon picks a file. Grade there. **Place in Design** when the developed picture belongs on the poster. That placement is an 8-bit pixel layer. The RAW and its `.omaphoto` stay beside the original for the next grade. Save with `Ctrl+S`. The document becomes an `.oma`. Templates do not need a save to be editable, and they do need a save if you want them in Recents as a file you actually kept. ## The edge Motion has no empty-workspace creation button. The artboard you animate is the artboard you already drew. Open Motion after there is something on the canvas. Space plays. `K` keys transforms there. That letter is Fill when you are in Pixel. The persona decides. The first five minutes also do not include a cloud login. **Sign up for cloud** is a link on the welcome column. Local templates, local folders, and local `.oma` files work with no account. **Team** shows up only after you are signed in and a shared project is actually there. Photo will not rewrite the camera file while you learn the sliders. Design will not absorb the RAW into the `.oma` when you save the poster. Those boundaries hold on minute five the same way they hold on hour five. Press `R`, then `P`, then `T`. That is the first drawing, in the document you just opened. ## The thread Part 3 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-portable-arm64-and-x86-64) · [Next](/blog/omadesign-0-5-8-welcome-logo-0-5-8) --- # Welcome logo 0.5.8 Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-welcome-logo-0-5-8 Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, welcome ## The habit You judge a studio in the first second, before a tool key matters. Illustrator's start screen is a gray field and a product mark you have seen for years. Photoshop's home is tiles and a cloud identity. Affinity opens closer to the document, with the app chrome already in the color you set in preferences. On the web, every tool you use has trained the same reflex: if the logo sits in a white box on a dark page, the asset is wrong. Someone exported a flattened PNG and called it done. You also know the splash that ignores the desktop. A fixed purple, a fixed charcoal, a wordmark in a color that clashes with the terminal you were just in. Omarchy users live in a theme file. Catppuccin, or whatever colors.toml you switched to this morning. The welcome mark has to sit in that room. A sticker on top of the room is a bug you see every launch. The other habit is scale. A shy logo in the corner reads as a placeholder. A logo that blows out the header reads as a poster nobody asked for. The mark should be obvious, and the creation buttons should still be the thing you click. ## The constraint One binary, and the welcome screen is inside it. There is no separate start-screen skin you download. The document model does not matter yet, because you have not made an `.oma`. What matters is that chrome follows the desktop on purpose. The app reads Omarchy theme colors. UI type comes from `omarchy font current`, then fontconfig. Icons are Phosphor Light. The welcome screen uses that palette and that font. It has no light/dark switch of its own. That constraint decides the logo. If the mark is an opaque rectangle, it punches a hole in whatever colors.toml you loaded. The asset has to be the transparent logo and wordmark SVGs the project already supplied, preserved, not redrawn as a baked bitmap. Dark surroundings give the transparent shapes a ground. The panels and the creation buttons cannot be a second unrelated gray. They take the lighter theme color and fade into that dark chrome, so the screen is one surface. Undo, camera files, and plugins are irrelevant on this screen. The constraint that still binds is local-first. **Your Work** and **Projects** read directories under your home folder. The logo is the header of a file browser, not the header of an account wall. **Sign up for cloud** can sit in the column. The mark does not wait on it. ## What landed 0.5.8 is the release that fixed this screen. The release notes name it directly. The supplied transparent logo and wordmark SVGs stay intact, and so does the project-folder SVG. The welcome logo is 25% larger than it was. It sits on a dark background. Browser panels and creation buttons fade from the lighter theme color into that dark chrome. What you see when the window opens: - The mark is the corrected transparent logo, larger, with the dark welcome ground showing through the open parts of the SVG. - The file browsers and the creation controls are not a flat slab in a different color family. Their gradient runs from the lighter theme color into the dark chrome around the logo. - The type on that screen is the desktop font, same rule as the rest of the app. - The icons stay Phosphor Light. The logo change did not swap the icon set. The welcome screen is still the local browser described in the manual. **Your Work → recent** finds `.oma` files under your home directory. **Recovered** lists recovery snapshots. **Projects → recent** finds directories that contain `.omabrand`. Center actions are **+ Vector**, the Vector file icon, **+ Raster**, **+ Layout**, the Layout file icon, **+ Photo**, the Photo folder and image icons, and **+ Project**. The larger logo sits over that layout. It does not replace it. About, opened from the wordmark in the title bar, shows the refined logo, the branded version, and the full semantic version. The welcome mark and the About mark come from the same decision: use the supplied artwork, keep it transparent, let the theme supply the room. Validation for 0.5.8 included native welcome screenshots. The public site got transparent branding in the same release. The in-app welcome is the screen you actually launch. That is the one with the 25% larger logo. This was a 0.5.8 change. Earlier builds already followed the desktop theme. They did not yet have this corrected transparent welcome mark at the new size, on the dark ground, with the lighter theme color fading through the panels and the creation buttons. ## In the hand Launch the studio: ```sh omadesign ``` Look at the header before you click anything. The logo should read as vectors on the dark welcome background, not as a white tile. If your Omarchy theme is a light Catppuccin or a custom colors.toml, the panels still fade from that theme's lighter color into the dark chrome. The mark stays on the dark ground either way. You do not toggle an in-app appearance mode to make this true. There is no such toggle on the welcome screen. Click **+ Vector**. The creation surface you enter uses the same fade. Come back. Click a recent `.oma` thumbnail. The document opens. The welcome logo's job is done for this session. The next launch shows it again, at the same size, from the same SVG. Change the desktop theme in Omarchy, then launch again. The panels pick up the new lighter color. The logo asset does not get recolored into a different drawing. Transparency is what lets the ground show through. If the mark looked muddy before, it was the asset. 0.5.8 is the correction and the enlargement together. Open **omadesign** in the title bar and choose **About** when you want the version next to the refined logo. Welcome itself shows 0.5.8 after this install. The logo is not a version stamp. The version is text. The logo is the mark. If you set `OMADESIGN_FONT` to a `.ttf`, the UI type follows that file. The logo geometry does not follow it. Wordmarks in SVG stay the supplied drawing. Body text and labels pick up the font. That split is the point. A theme font should not redraw the studio's name as outlines you did not ship. ## The edge The welcome screen will not grow a private light/dark switch to flatter the logo. The palette is the Omarchy palette. Stock Catppuccin is only the last stop in the theme read order, when the theme files are missing. The logo does not carry its own background color to fake a theme. The enlargement is the welcome logo. It is not a new document size, a new artboard preset, or a change to how **+ Vector** scales a template. Twenty-five percent is the mark on that screen. The creation buttons stay the buttons. The browsers stay at most three columns of masonry thumbnails. A bigger logo does not mean a bigger file grid. The SVG is preserved. 0.5.8 does not flatten it into a rectangle with a baked fill. If you are looking at a white box behind the wordmark, you are not looking at this build. Launch `omadesign` and read the mark against the dark ground before you press `R`. ## The thread Part 4 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-first-five-minutes) · [Next](/blog/omadesign-0-5-8-welcome-segmented-buttons) --- # Welcome segmented buttons Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-welcome-segmented-buttons Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, welcome ## The habit Segmented controls are how you switch a mode without opening a menu. Illustrator's start screen and Photoshop's home both use clusters of buttons that read as one control: New, Home, Learn, a row of document types. Affinity's new-document dialog groups type, size, and color in the same way. You expect the segments to look like they belong together. Hairline dividers are the usual glue. They work until the theme shifts and the hairline disappears, or until the divider is a hard rule that cuts the button in half. Utility links are the other habit. "Open the docs." "Join the chat." "Read the notes." They sit in a column as underlined text, or as tiny system icons that do not match the toolbar you get after the document opens. The front door looks like a website. The studio looks like a different program. Folder tiles are the third habit. A project is a directory. The icon is a yellow folder from the desktop, or a generic card with a drop shadow. You can tell a file from a folder only by reading the label. On a masonry of real work, that costs a second every time you scan. ## The constraint The welcome screen is a local file browser inside one binary. It is not a stub page that hands you to a browser and hopes you come back. Creation actions live in the center. **Your Work** and **Projects** are the two browsers. Both show masonry thumbnails, three columns at most, natural aspect ratio, names on hover. The icon language of the running studio is Phosphor Light. A welcome column that used a different icon family would be a second product sitting on top of the first. Utility links had to pick up Phosphor so the docs link, the conversation link, and the cloud link look like the same studio you enter when you press `R`. Color comes from the Omarchy theme, with the 0.5.8 welcome ground dark and the panels fading from the lighter theme color. A one-pixel divider in a fixed gray fights that gradient. The segment boundaries had to be made of the same fade as the panels, or the controls would look pasted on. Project folders needed a silhouette you can read at card size: squarer, rounded, the project-folder SVG preserved, not a photocopy of the file manager. A project is still a directory that contains `.omabrand`. No account. The button polish does not turn Projects into a service. **Team** appears only while you are signed into cloud and a shared team project is available. The card shape is local. ## What landed 0.5.8 welcome polish is three specific changes. The release notes list them next to the logo work. Segmented buttons use gradients. The old segmented dividers are gone. The boundary between segments is a gradient, the same family as the fade from the lighter theme color into the dark chrome. The control still reads as one segmented button. It does not read as a row of unrelated chips. Utility links use Phosphor icons. The column that holds **Join the conversation**, **Sign up for cloud**, release notes, docs, contribution guidance, and bug reports uses that icon set. Phosphor is already what the mode tabs and the tool chrome use. The welcome column stops being a text-only footer. Project folders get a squarer rounded outline. The project-folder SVG shipped with the app is the art. Folder cards still contain stacked asset previews. You click a folder card to browse subprojects first, then every descendant `.oma` by modification time. **← Projects** returns to the list. **Edit brand…** opens the brand editor for the project you are in. The squarer outline is how you spot that card among document thumbnails. The center actions stay labeled and specific: | Center action | What opens | | --- | --- | | **+ Vector** | 52 editable vector templates | | Vector file icon | Blank document size chooser | | **+ Raster** | Blank raster size chooser | | **+ Layout** | Layout starters with frames and prototypes | | Layout file icon | Blank Layout size chooser | | **+ Photo** | Photo workspace | | Photo folder icon | Folder chooser | | Photo image icon | Image chooser | | **+ Project** | Project brand editor | Vector and Raster on this screen lead into the Design and Pixel personas. Motion still has no empty-workspace button. The segmented control does not invent one. Native welcome screenshots were part of the 0.5.8 review. This is a 0.5.8 change to chrome you see before any document exists. ## In the hand ```sh omadesign ``` On the welcome screen, look at the segmented creation control before you read a single filename. The segments should meet in a gradient, not in a hairline rule. Move along **+ Vector**, **+ Layout**, **+ Photo**, **+ Project**. Each segment is a door. **+ Vector** opens the 52. The file icon next to Vector opens the blank size chooser. Those two are neighbors on purpose. Templates and a blank page are different clicks. The gradient groups them. It does not merge them into one action. Scan the utility column. The icons are Phosphor, the same light weight you get inside the document. **Join the conversation** opens the Omadesign Discord. **Sign up for cloud** opens cloud registration. Local browsing keeps working if you never touch that link. An available update opens the existing update details before any install-and-restart action. The icon tells you the row is a utility, not a document. Open **Projects → recent**. The cards are folders that contain `.omabrand` somewhere under your home directory. The outline is squarer and rounded, so a project card does not impersonate a loose `.oma` thumbnail. Click one. Subprojects come first. Documents inside follow, newest modification first. **Edit brand…** is the brand editor, palettes and assets, still on disk beside the work. Return with **← Projects**. The outline should still be the thing that separates folders from files at a glance. Hover a document thumbnail when you want its name. Hover is the name. The folder silhouette is the type. Change your Omarchy theme and launch again. The gradient segments pick up the theme color. The Phosphor icons stay Phosphor. The folder outline stays the squarer rounded SVG. You did not restyle three separate skins. ## The edge This polish does not turn the welcome screen into an account gate. A project needs no login. **Team** stays hidden until you are signed in and a shared project exists. The squarer folder is a local directory card. It also does not add Motion as an empty creation segment. Motion starts after the canvas has something to animate. The segmented control you see is Vector, Raster, Layout, Photo, and Project, with the file and folder icons beside the ones that need a size or a path. The gradients replace the segmented dividers. They do not recolor your documents, your `.omabrand` assets, or the template artwork. Chrome got the fade. The files stayed files. Launch `omadesign` and use the gradient segment for **+ Vector** when you want the 52, or the file icon beside it when you want a blank size. ## The thread Part 5 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-welcome-logo-0-5-8) · [Next](/blog/omadesign-0-5-8-persistent-title-bar) --- # Persistent title bar Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-persistent-title-bar Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, chrome ## The habit Start screens eat chrome. Illustrator's home, Photoshop's home, a dozen web apps: the window drops its normal bar, shows a billboard, and rebuilds the frame when you finally open a file. The document tabs you had a second ago jump. The menu you use for Preferences moves. You click the wrong Open because Open was a marketing button on the splash and a menu item two pixels away. Affinity is closer to a normal desktop window. The tab bar stays a tab bar. You still learn which Preferences live in the app menu and which live in a gear on the home screen. Duplicate labels are how people save the wrong thing or open a second copy of a file they already had. On a tiling window manager the title bar is also how you see which window is the studio. Hide it on the welcome screen and the compositor's decoration, or the lack of one, is all you have. Bring it back a moment later and every thumbnail reflows. Your eye loses the document you were about to click. ## The constraint One window. Several `.oma` documents, each in a tab, each with its own undo. The welcome screen is a mode of that same window, not a second process. If the title bar vanishes while you are choosing a file, the open-document thumbnails have nowhere stable to sit. The 0.5.8 decision is to leave the bar visible so those thumbnails stay put when you start working. The menu has to be one menu. The manual puts **Config | Update | About | Docs** on a click of **omadesign** in the title bar. Open is already `Ctrl+O` and a tab action. Preferences are Config, saved under `~/.config/omadesign` or `XDG_CONFIG_HOME`. A welcome screen that also prints Open and Preferences as extra links makes two of each. You will click the wrong one. The wordmark is the menu. The duplicate links go away. The bar also has to survive the dark welcome ground from the same release. The logo SVG and the wordmark SVG are the supplied transparent art. The menu uses that wordmark. It does not typeset a second name in the UI font and call it a logo. Document tabs themselves sit above the canvas once you are drawing. `Ctrl+N` and `Ctrl+O` belong to them. The title bar is the strip that stays when welcome is showing, so the jump from "pick a file" to "edit the file" does not relocate the thumbnails of what is already open. ## What landed 0.5.8 keeps the title bar visible on Welcome. That is a release-note item, reviewed with the native welcome screenshots. Open-document thumbnails stay where they were when you leave the splash and start working. The menu is the Omadesign wordmark. Click **omadesign** in the title bar. The entries are **Config**, **Update**, **About**, and **Docs**. - **Config** is the UI font and size, startup mode, welcome tab, rulers, shortcut hints, guide locking, photo provider keys, and optional anonymous usage. Anonymous usage is off unless you turn it on. - **Update** checks the release channel. When a newer compatible package exists, it runs the official installer, waits for ongoing work, writes a full local recovery snapshot, and restarts with the same open documents and photo adjustments. - **About** shows the refined logo, the branded version, and the full semantic version. On this install that version is 0.5.8. - **Docs** opens [omadesign.app/docs](https://omadesign.app/docs/). Duplicate Open and Preferences links are gone from that surface. Open remains `Ctrl+O`, the welcome thumbnails, and **File → Open**. Preferences remain Config. You do not get a second pair of labels competing with the wordmark. The title bar is also where the persona mode tabs live in the current workspace: Phosphor icons for the modes, hover text with the mode name, centered in the bar. The persistent bar is what keeps that strip from appearing and disappearing with the welcome screen. You start working, the thumbnails stay, the wordmark stays. Welcome can show an available update in its own column. That path opens the existing update details before the explicit install-and-restart. It does not add the old Preferences link back. One update story, two ways to reach it: the column when an update is sitting there, and **Update** under the wordmark when you go looking. ## In the hand ```sh omadesign ``` Before you click a thumbnail, look at the top edge. The title bar is there on the welcome screen. The wordmark is the menu, not a caption. Open a document you already had in the window, or create one with **+ Vector** and **Use this template**. Watch the open-document thumbnails. They stay put. The bar does not collapse to "make room" for the canvas and then shove those thumbnails somewhere else. Click the wordmark. ```text omadesign → Config | Update | About | Docs ``` Change the startup mode or the welcome tab in Config if you want the next launch to land on Recent or Recovered. Close Config. The bar is still there. Press `Ctrl+O` to open another `.oma`. That is Open. It is not a link under the logo. Press `Ctrl+N` for a new tab. The title bar does not become a different menu because a second document exists. Close a tab with its close button. If the work is unsaved, the dialog is Save, Discard, or Cancel. The wordmark is not part of that dialog. The dialog belongs to the tab. When you want the version, click the wordmark and choose **About**. Welcome also shows 0.5.8. You should see one version, from the build you installed, not a second version string typed into a leftover Preferences link. If you are on a window manager that draws no server-side decoration, this in-window bar is the studio's own strip. Leave it visible. The thumbnails use it as their anchor when the welcome browser and the canvas trade places. ## The edge The title bar does not swallow Open. It stops repeating Open. `Ctrl+O` still opens a document into a tab. Welcome thumbnails still open a document. **File → Open** still reads PSD, PSB, XCF, PDF, SVG, OpenRaster, and supported Affinity files through the bridge, into their own tab, at their original size. Save still writes `.oma` and leaves the source file alone. The bar does not hide on Welcome to enlarge the logo. The logo grew 25% in this same release on the dark ground. The bar stayed. Those are separate decisions. A bigger mark was not allowed to evict the thumbnails. Preferences are Config. There is no second preferences window reached by a leftover link. Guide locking, rulers, shortcut hints, and the UI font are in that one panel, stored under `~/.config/omadesign`. Click the **omadesign** wordmark, then **Config**, when you need the setting. Press `Ctrl+O` when you need a file. ## The thread Part 6 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-welcome-segmented-buttons) · [Next](/blog/omadesign-0-5-8-filter-icon-states) --- # Filter icon states Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-filter-icon-states Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, welcome ## The habit Every file browser you trust has a filter, and every filter you have ever missed has looked like a normal button. Photoshop's bridge-style folders, Illustrator's open dialog, Affinity's document views, the file manager on the desktop: a funnel or a magnifying glass sits in a rounded rectangle. At rest it looks pressed. On hover it looks pressed. When the filter is active it still looks pressed, maybe with a dot you notice after you have scrolled past the file you wanted. The cost shows up when the folder is your whole home directory. Omadesign's welcome browser discovers `.oma` files at any depth under home, newest modification first. Projects discovers directories that contain `.omabrand`. Three columns of masonry, natural aspect ratio, names only on hover. If a filter is on and the icon does not say so, you think the project is missing. You click Refresh. You dig through Trash in your head. The file was excluded because the funnel was set to Photo and the file is a poster. You need three readings from one icon, at a glance, while your eyes are on the thumbnails. Rest. Hover. Active. Color has to do that work. A filled button background makes all three look like a chip. ## The constraint The welcome screen is local. Discovery skips hidden directories, Trash, and symlinks. It does not skip a file because the cloud has not synced it. There is no cloud index. **Refresh** is how you pick up a file you moved in from outside the app. The filter is a view on that local list, not a search service. The funnel's categories are the personas' files: Vector, Raster, Layout, Photo, or Motion. **All modes** clears the filter. Those five are the same five rooms as the studio, so the filter language matches the work. A sixth made-up bucket would send you hunting a label the app does not use when you draw. The icon has to sit on the 0.5.8 welcome chrome: dark ground, panels fading from the lighter theme color, Phosphor icons, no extra button chrome fighting the gradient. A background plate behind the funnel would read as another segmented control. The funnel is a state indicator beside a browser, so the plate goes. Hover and active have to be readable against that dark ground and against a theme that can be almost any Omarchy palette. Blue for hover. Red for active. Two colors, two meanings, no sentence of UI copy required while you scan. The filter must not rewrite a file to "tag" it. Mode is a property of the document you already saved. The icon only tells you whether the list is narrowed. ## What landed 0.5.8 draws the welcome filter icon with no button background. The release notes say that directly: render the filter without a background, blue on hover, red when active. Native welcome screenshots covered this screen. At rest you see the funnel itself. No chip, no filled rectangle, no border pretending to be a toolbar button. Point at it and it turns blue. That is hover. It means the control is under the pointer. It does not mean a filter is applied. Turn a filter on and the icon turns red. Red means the list is narrowed. You can look at the thumbnails and still know the funnel is doing something, which is the whole point of a small piece of chrome. What the funnel narrows: - **Vector** - **Raster** - **Layout** - **Photo** - **Motion** **All modes** clears it. When the filter is clear, the icon is not red. Recent work shows again: every `.oma` under home that discovery is allowed to see, newest first, for **Your Work → recent**. **Recovered** stays its own tab. Crash leftovers do not mix into the mode filter as if they were a sixth kind of finished document. **Projects** is the other browser. A project folder is a directory with `.omabrand`, not a mode of `.oma`. **Shift-click** the first thumbnail to start a multi-select. Later clicks add or remove. **Open selected** opens the documents. **Recover selected** does that job on the Recovered tab. **Clear** or Escape leaves the selection. The filter sits beside that selection model. A red funnel tells you the masonry is a subset before you shift-click the wrong short list and think the rest of the work is gone. **Refresh** reruns discovery after you add or move files outside the app. The filter state is still the filter state after a refresh. Red stays red until you clear it. Blue still means hover. Startup Config can remember the welcome tab. It does not replace the funnel. You can land on Recent and still narrow Recent. ## In the hand ```sh omadesign ``` Open **Your Work → recent**. Look at the funnel before you read thumbnails. It should sit without a button plate. Pass the pointer over it. Blue. Move the pointer away. The blue leaves. The icon is idle again. Choose **Vector**. The icon turns red. The masonry drops anything that is not a vector document. Hover the names if you need them. If the poster you wanted is a Layout file, it is absent on purpose. Switch the funnel to **Layout**, or choose **All modes**. Red clears when the filter clears. The full recent list comes back, still skipping hidden directories, Trash, and symlinks. Try **Photo** when you are hunting graded work, **Raster** for pixel documents, **Motion** for documents that carry a clip. Each choice is the same red icon plus a different subset. The color is the "something is filtered" bit. The menu is the "which mode" bit. You need both. Color without the menu would only tell you that the list is short. The menu without the color is how filters get left on. Select several thumbnails with Shift-click while the icon is red. **Open selected** opens that subset. Come back to welcome. If the icon is still red, you are still looking through the funnel. Choose **All modes** before you decide a file is missing. Move a new `.oma` into a folder under home from the file manager. Click **Refresh**. If the funnel is red and the new file is another mode, refresh will not show it. Clear the filter. Refresh again if the new file is outside what discovery already had. Then it shows up, newest modification first. ## The edge The filter does not delete, move, or rewrite `.oma` files. It does not write a sidecar to remember the mode. Clear it with **All modes** and the browser lists what discovery lists. Discovery itself has a harder boundary than the funnel. Hidden directories, Trash, and symlinks stay out even when the filter is clear. A red icon is not the reason a trashed file is missing. **Refresh** will not pull Trash back in. Recovered is not a filter mode. Crash snapshots live on the Recovered tab. A red funnel on Recent does not list `.oma.swp` files as if they were posters. Save deletes the swap for a document you saved. That is a different control, on a different tab. The icon has no button background in 0.5.8. Hover is blue. Active is red. If the funnel is sitting in a filled chip, you are on an older build. Launch `omadesign`, set the funnel, and trust red. Choose **All modes** when you want the full recent list back. ## The thread Part 7 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-persistent-title-bar) · [Next](/blog/omadesign-0-5-8-template-chooser-width) --- # Template chooser width Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-template-chooser-width Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, templates ## The habit You have lost clicks to a panel that resizes under the pointer. Illustrator's template and stock browsers, Photoshop's new-document presets, Affinity's template grids, a hundred web galleries: the panel opens narrow, the pointer enters, a scrollbar appears, the cards reflow, and the thing you meant to click is now one row down. You hit the neighbor. You undo a document you did not mean to create. You stop trusting the first click. The other version is a chooser that animates open. Width eases from a sliver to the full sheet. Halfway through, hover states fire on cards that have not finished moving. On a 52-up grid that is not a flourish. It is a mis-click machine. Designers who live in the start screen learn to wait for the animation to finish. That wait is the bug. A stable sheet is the habit you actually want. Open. The columns are where they will be. Move the pointer in. Nothing reflows. Then you can search, filter, and double-click without aiming twice. ## The constraint The 52 vector templates ship inside the binary. They are editable paper, artwork, and copy. They use local fonts. They do not fetch a preview from a server when the pointer enters a card. **+ Layout** has its own starters, also local. The chooser can open at the width it needs on the first frame because it already knows the content. There is no second layout pass waiting on a network response that would force the sheet to grow. One window, welcome and documents together. Existing work stays in its tab when a template opens. The chooser is not allowed to shove that window's layout around while you browse, or the open-document thumbnails the title bar is trying to hold still will jump too. 0.5.8 already keeps the title bar visible for that reason. A chooser that changes width on pointer entry would spend that stability on the first hover. The gallery lays out visible rows, with previews that adapt to the size you pick. Portrait, square, and wide compositions are different artwork, not one design stretched. Search and the nine category filters change which cards you see. They should change the list inside a stable frame. The frame's width is not a function of which card is under the pointer. The regression that guards this is pointer entry over a run of frames, in both Vector and Layout. The behavior you feel is simpler than the test: the width on open is the width that stays. ## What landed 0.5.8 opens the template chooser at its final width. The width stays there as the pointer moves in. No layout jump while you browse the 52 starters. The release notes call it the stable final width. The chooser was reviewed with the welcome screenshots, and the pointer-entry check covers Vector and Layout. What the chooser contains once it is open, at that width: - **+ Vector** on the welcome screen, or **File → Template library** while you are already drawing. Both reach the 52. - **+ Layout** opens the frame starters: Fieldwork responsive prototype, mobile screen, landing hero, dashboard, and card stack. Those also live under **File → Template library → Layout starters**. - Search by name or by the idea you have in your head. - Filter the nine categories. The bank covers events, food, culture, community, editorial, branding, education, wellness, and products. Thirteen artwork families, 52 distinct compositions. - A built-in document size, or a custom width, height, and DPI. There are built-in sizes enough to cover the usual poster, screen, and page jobs. Previews adapt to the proportions you choose. - **Use this template**, or a double-click on the card. The document that opens is unsaved. You can edit the paper, the artwork, and the copy. The tab you were in keeps the work that was already there. Very tiny sizes omit secondary copy that would be unreadable. Nothing in that flow asks the panel to grow because the pointer crossed the boundary. Raster's welcome action is a size chooser, not the 52. The width fix is the template chooser. Vector and Layout are the two that the pointer-entry check watches. All 52 are available immediately. The weekly drop plan is a reading list and a remix prompt per design. It does not withhold cards, and it does not publish anything on a timer. The grid you see at the stable width is the full local set, narrowed only by the search box and the category filter you set. ## In the hand ```sh omadesign ``` Click **+ Vector**. The sheet opens at the width it is going to keep. Move the pointer in from outside the chooser, across the first row of cards, slowly. The columns stay. The card under the pointer is the card that was there when the sheet appeared. Hover can still show a name or a highlight. Hover is not allowed to change the width. Type in the search box. Filter a category. The list inside the sheet changes. The sheet does not leap to a new width because a category has fewer cards, and it does not leap because the pointer is now over a tall portrait card. Pick a size, or type a width, a height, and a DPI. Previews adapt. The chooser's own width stays. Double-click the card, or click it and press **Use this template**. An unsaved document opens in Design. `R`, `P`, and `T` work on that artwork. The template's type is live type. The shapes are shapes. Save with `Ctrl+S` when you want an `.oma`. The tab that held your previous document is still there. Do the same with **+ Layout**. The sheet opens at its final width. Pointer entry leaves that width alone. Choose Fieldwork, a mobile screen, a landing hero, a dashboard, or a card stack. You get frames you can edit with `F`, `R`, and `T` in the Layout persona. **File → Template library → Layout starters** is the same set once you are inside a document. Open the library from **File → Template library** while a real job is already on the canvas. Browse. The existing tab does not get replaced by the act of moving the pointer. Creating the template keeps that work and starts a fresh unsaved document beside it. If you are checking this against 0.5.8 specifically, do the pointer move before you click a card. The bug was the jump. The fix is that there is nothing to wait out. ## The edge A stable width does not freeze the grid. Search, category, and page size still change which previews you see and how those previews are cropped to the proportions you asked for. The edge is the chooser's frame. It does not reflow because the pointer entered. The chooser also does not phone home for the 52. No network means no late-arriving thumbnail that would force a second layout. If a font the design wants is missing on the machine, the template still uses what is installed locally. It does not widen the panel to explain that. Very small custom sizes drop secondary copy. That is a content rule, decided when the document is built, not a resize of the chooser under your hand. Existing work stays in its tab. The chooser will not discard an unsaved document to make room for a starter. If that document needs a decision, the tab's own Save / Discard / Cancel dialog is the one that asks. The template sheet does not ask by jumping. Click **+ Vector**. The width you see is the width you keep while the pointer moves in. ## The thread Part 8 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-filter-icon-states) · [Next](/blog/omadesign-0-5-8-crash-recovery-swap) --- # Crash recovery swap Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-crash-recovery-swap Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, recovery ## The habit You know crash recovery as a dialog you did not ask for. Photoshop reopens with a recovered panel and a timestamp. Illustrator offers a recovered file with a name you do not remember choosing. Affinity does the same job with a recovery prompt on the next launch. The good versions let you keep the recovered work or throw it away. The bad versions scatter `filename (recovered)` copies through the folder you were saving into, and six months later you do not know which one is the poster. The other habit is autosave that is a second document. A cloud copy. A `~` file next to the original that some other app treats as the original. You attach the tilde file to an email. The client opens garbage, or they open yesterday. On Linux the swap-file habit is older than any of those apps. Editors write `filename.swp` beside the work, or under a state directory, and delete it on a clean save. You only see the swap when the process died. That is the right shape for a studio that stores one `.oma` and means it. ## The constraint One document. One undo stack on that document. A crash copy cannot be a second format with its own history, or you will recover into a file that cannot redo the stroke you wanted. The snapshot has to be the same `.oma` the app already knows how to open, written aside, marked as a leftover. Idle is the trigger. A snapshot on every mouse move would fight the brush and the pen. A snapshot only on a timer you configure in three places is a dialog. One second of idle is long enough that a drag in progress is not the thing being serialized, and short enough that a kill from the compositor does not cost the morning. The path has to be out of the project folder. Clients and rsync jobs should not pick up swaps as deliverables. `~/.local/share/omadesign/.oma.swp` is per user, per document id, under the usual local-share directory. Save deletes it. A clean save means there is nothing to recover, so the splash must not keep offering yesterday's success. The welcome screen already has tabs. **Recovered** is the tab for leftovers. **Your Work → recent** is a different list: every `.oma` under home that discovery is allowed to see. The manual also says Recents lists `.oma` files you actually opened. Those sentences name two ideas on purpose. Opened files, discovered files, and crash leftovers are not one pile. Startup Config can open New, Templates, Recent, or Recovered, and it can remember the last tab. Recovery stays one tab, not a modal that blocks the other three. ## What landed This is the recovery behavior as it stands in 0.5.8. It is not a feature the release notes claim was born that day. The manual states it in one place, next to the document tabs. Idle for a second and Omadesign writes: ```text ~/.local/share/omadesign/.oma.swp ``` `` is the document's id, not a slug you are expected to type. Save deletes that file. If the process dies before the next save, the swap remains. The splash **Recovered** tab lists those crash leftovers. **Recents** lists `.oma` files you actually opened. **Your Work → recent** discovers every `.oma` beneath your home directory, at any depth, sorted by last modification, newest first. **Recovered** lists recovery snapshots separately from both. Discovery skips hidden directories, Trash, and symlinks. **Refresh** picks up files you moved in from outside. It is not how a swap becomes a poster. The swap shows up because it is a leftover, on the Recovered tab. **Shift-click** to start a multi-select on that tab. Further clicks add or remove. **Recover selected** opens the leftovers you marked. **Open selected** is the matching action on the ordinary browser. **Clear** or Escape drops the selection. Recovery work from earlier in the studio's life still describes the machinery under that tab. Changed snapshots compress in the background. A stale result is rejected before it can replace a saved state. Dirty-state edges around save, undo, live type, and closing an inactive tab were fixed so a tab you were not looking at does not corrupt the snapshot you need. You feel that as a simple rule. The Recovered file is the idle snapshot. The saved `.oma` is the saved `.oma`. One does not silently become the other. Quitting Photo is a cousin, not the same file. Unsaved photo settings get Save all, Discard, or Cancel before the palette and artwork save steps, and the app waits for those writes. A Photo `.omaphoto` is beside the camera file. It is not an `.oma.swp`. Design save does not store the RAW. ## In the hand Open a document and draw. ```sh omadesign ``` Press `R` and drag a rectangle. Stop moving the pointer. After about a second the studio writes the swap under `~/.local/share/omadesign/`. You do not have to watch that directory. The contract is the idle, not a progress bar. Press `Ctrl+S` and save the `.oma` somewhere under your home directory that is not hidden. The swap for that document is deleted. Check Recovered later and this session should not be sitting there as a crash. To see the tab do its job, you need a leftover: a kill, a session that ended without Save, a swap that save did not get to delete. Next launch, open the welcome screen. Choose **Recovered**. The leftover is listed there, apart from Recent. Select it. **Recover selected** opens it. Decide whether that snapshot is the work you wanted. Save it with `Ctrl+S` to a real path if it is. That save deletes its swap, same rule as any other save. If you want the next launch to land on that tab: ```text omadesign → Config → welcome tab → Recovered ``` Remember-last-tab will put you back on whichever welcome tab you left. Recent, Templates, New, and Recovered stay separate choices. A red filter icon on Your Work is a mode funnel. It is not Recovered. Clear it with **All modes** when the list looks short. Look at Recovered when the work was never saved. **Your Work → recent** will also show an `.oma` you saved, newest modification first, even if you did not open it through the Recents habit, because discovery walks home. A file you only opened and never saved still belongs to the "files you actually opened" list the manual calls Recents. A swap belongs to Recovered until you save or dismiss it. Three lists. Use the one that matches the failure. ## The edge Save deletes the swap. A document you saved does not linger as a crash leftover. Recovered is not a second project folder you are supposed to edit by hand and rsync to a client. The deliverable is the `.oma` you saved. The swap is the idle copy that exists because the process may die. Discovery will not treat hidden directories, Trash, or symlinks as your work. A swap lives under `~/.local/share/omadesign/`, which is a hidden path from the home browser's point of view. That is why it shows up through the Recovered tab and not as just another thumbnail in the masonry walk. Do not expect **Refresh** on Your Work to be the crash UI. A stale snapshot is rejected before it replaces a saved state. Recovery does not get to roll a good save backwards because a late write finished out of order. Idle for a second while the document is dirty. Save with `Ctrl+S` when you want that `~/.local/share/omadesign/.oma.swp` gone. ## The thread Part 9 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-template-chooser-width) · [Next](/blog/omadesign-0-5-8-document-tabs) --- # Document tabs Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-document-tabs Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, tabs ## The habit You keep more than one file open. Illustrator puts documents in tabs across the top of the window, and a close on a dirty file stops you. Photoshop does the same with images, and the tab is how you know which histogram you are looking at. Affinity's tabs are the muscle memory the close dialog is aiming at: Save, Discard, Cancel, in words a person uses, not in a developer prompt. You expect Cancel to mean the tab is still there and nothing was thrown away. You also expect `Ctrl+N` and `Ctrl+O` to mean new and open, even when you spend the day in a browser. New should be another tab, not a second process that splits the undo stacks into two windows you then alt-tab between. Open should land beside the work you already have. A template should do the same. The job you were in stays put. The bad version of tabs copies the whole document, pixels and undo, on every frame so the tab strip can draw a thumbnail. The window gets heavy. Closing an inactive tab dirties the one you can see. You learn not to open a reference file. That is the habit this studio had to refuse. ## The constraint One binary, one window, one `.oma` per tab. Each tab owns its document and its undo. A step in the poster does not undo a step in the reference. Close is the dangerous action, so unsaved work has to ask. The answers are Save, Discard, and Cancel. Cancel is a real answer. It leaves the tab open. Discard is the only answer that throws the unsaved changes away. Save writes `.oma` and, on a document that had an idle swap, deletes `~/.local/share/omadesign/.oma.swp`. The tabs sit above the canvas. That is the manual's placement, and it is the placement the feature follows. The title bar stays visible so open-document thumbnails do not jump when you leave welcome and start working. The tab strip and that bar are the chrome that has to hold still. A chooser that reflows, or a welcome screen that hides the bar, would break the reason the tabs are there: you can see which document your keys will hit. `Ctrl+N` and `Ctrl+O` are global document commands, the same chords as the shortcut table. They have to work when the canvas is focused, and they have to stay out of the way when you are typing in a text object. The tab you create is empty enough to draw, or it is the file you picked, at that file's size. Imported PSD, PSB, XCF, PDF, SVG, OpenRaster, and supported Affinity documents open in their own tab at their original dimensions. Save still writes `.oma` and leaves the source file on disk as it was. Templates follow the same rule. **Use this template** starts a fresh unsaved document. The tab you already had keeps its work. ## What landed Document tabs sit above the canvas. This is the behavior in 0.5.8. The release notes do not claim the tabs were invented that day. They do claim the title bar stays up so the thumbnails of open documents stay put, which is the chrome around this strip. `Ctrl+N` creates a new tab. `Ctrl+O` opens another file into another tab. Close with the tab's close button or with the tab's right-click menu. If the document has unsaved work, the dialog is Save, Discard, or Cancel. That trio is the Affinity-familiar prompt, on a Linux-native window, over one `.oma`. What a tab carries: - The document, including vector layers, pixel layers, layout frames, and the motion clip if there is one. - That document's undo. One step undoes one change in that tab. - The dirty flag that makes close ask the question. - The idle swap, after a second, at `~/.local/share/omadesign/.oma.swp`, until Save deletes it. Opening does not convert the tab into the foreign format. **File → Open** can read those formats. **Save** and **Save as** (`Ctrl+Shift+S`) write `.oma`. **View → Document conversion notes** lists what could not come across whole. The notes stay in the project. Drop a layered document on the canvas or on the welcome screen and it opens. An ordinary image places. An `.oma` opens. A Lottie imports. The tab is the open path. Placement is a different path, `Ctrl+Shift+P` or a drop of a plain image, and it lands inside the current document. Earlier work on the tab system stopped the frame loop from copying documents, undo history, and raster buffers just to draw the strip. Stale recovery results get rejected. Closing an inactive tab was one of the dirty-state bugs that had to die so the tab you are not looking at stays honest. You notice it as calm. Open a second file. The first file's undo is still the first file's undo. Persona switches do not open a tab. Design, Layout, Pixel, Photo, and Motion are tool wells on the document. A new tab is `Ctrl+N` or `Ctrl+O` or a template, not a mode click. ## In the hand Launch, open something or make something, then add a neighbor. ```sh omadesign ``` ```text Ctrl+N ``` A new tab appears above the canvas. Press `R` and draw. That rectangle belongs to the new tab. ```text Ctrl+O ``` Pick another `.oma`, or a PSD, or an SVG. It opens beside the first, at its own size. Click the first tab. The rectangle is still there. `Ctrl+Z` undoes the last change in the tab you are looking at. Right-click the tab you want to close, or use its close button. On a dirty tab the dialog is: ```text Save Discard Cancel ``` Cancel. The tab remains. Draw one more shape so you know the document is intact. Close again and choose Save. You get a native save dialog. The file on disk is `.oma`. The swap for that id goes away on the save. Open **+ Vector**, pick one of the 52, and **Use this template**. The template arrives as its own unsaved tab. The file you just saved is still the other tab. Close the template with Discard if it was only a look. Close it with Save if it became the job. `Ctrl+W` is not the chord the manual gives you for this. Use the close button or the right-click menu. The chords to remember are `Ctrl+N`, `Ctrl+O`, `Ctrl+S`, and `Ctrl+Shift+S`. Switch persona in the same tab and draw or paint. The tab does not multiply. Switch back. The layer stack is the one you just edited. ## The edge Cancel does not save and it does not discard. The tab stays. The only way through the dialog to a destroyed unsaved change is Discard. Save is the way out that writes `.oma`. Close does not close the whole studio. The window stays. The last tab is a document question, not a secret quit. Quitting with unsaved photo settings is a separate prompt: Save all, Discard, or Cancel for those settings, before palette and artwork saves, and the app waits for the writes. Do not confuse that Photo quit path with a document tab. A tab is one `.oma`. Photo settings are `.omaphoto` beside the originals. A tab will not rewrite a RAW file because you had Photo open earlier. Design save stores the design. The camera file stays on disk as the camera wrote it. Imported files stay imported. The tab does not save back over the PSD unless you export on purpose. Default save is `.oma`, source preserved. Press `Ctrl+N` for a new tab above the canvas. Press `Ctrl+O` when the next file should sit beside it. ## The thread Part 10 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-crash-recovery-swap) · [Next](/blog/omadesign-0-5-8-five-personas-one-document) --- # Five personas one document Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-five-personas-one-document Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, personas ## The habit You already switch rooms to finish one piece of work. Illustrator for the mark and the type. Photoshop for the retouch. After Effects for the move. Between them: export a PSD, export a PNG, import, notice the type is outlines, fix it in the first app, export again. Affinity built StudioLink so Designer, Photo, and Publisher can share a document without that bounce. The habit it replaced is the habit Adobe's three icons still teach. The habit it kept is "which app am I in." You know the failure mode. A linked file goes stale. A color profile shifts. The artboard size rounds off. The animation app cannot see the live text, so somebody outlines it "just for the render" and the outlines become the source. Two weeks later the headline is wrong in the only file anyone can edit. On Linux the extra failure is the missing app. One of the three is a Flatpak with a portal in the way. One is not built for the distro. You do the job in whichever window actually launched. The file format becomes whatever that window could save. ## The constraint Omadesign is one native binary. The document is one `.oma`. The layer stack is one stack. Undo is one step on that document, in the tab you have focused. A persona is which tools are in your hand, not which file you converted to. That constraint forbids a handoff format between drawing and painting. Raster lives on a pixel layer in the same `.oma` as the vectors. Layout frames live in that same document as the poster. Motion keys refer to the artboard you drew. The rest pose stays the drawing. Photo can grade a camera file, and it writes a `.omaphoto` beside the original. **Place in Design** brings an 8-bit developed layer into the document. The `.oma` save does not swallow the RAW, and Design does not rewrite the camera file. The persona boundary is exactly there: Photo's source of truth for the capture stays outside the design file. Everything you constructed lives in the `.oma`. Five tool wells, one process. No Creative Cloud session between them. No second window required to press `B` after you pressed `T`. Compact windows get a persona picker so the choice still fits. The title bar carries mode tabs with Phosphor icons: the curve, the brush, layout, images, the running figure. Hover text keeps the mode name. You can land the next launch in a remembered mode from **omadesign → Config**, or always start from the welcome screen. Plugins, cloud, and export stay optional doors. File → Sign in is there when you want a review upload. The personas do not wait on it. PNG, JPEG, SVG, animated SVG, Lottie, PSD, PDF, and OpenRaster are exports. They are not how you talk to yourself between Design and Pixel. ## What landed The five personas, as the manual tables them, in the 0.5.8 studio: | Persona | You are… | First tools | | --- | --- | --- | | **Design** | a mark, a poster, a layout | Move `V`, Pen `P`, Rectangle `R`, Type `T` | | **Layout** | a screen, a landing, a dashboard | Frame `F`, Rectangle `R`, Type `T` | | **Pixel** | painting or retouching | Brush `B`, Eraser `E`, Clone `J`, Wand `W` | | **Photo** | grading a photograph | Crop `C`, develop sliders, Place in Design | | **Motion** | animating the artboard | Space play, `K` key, File → Lottie | Design is the default when you open a vector document or a template. Vector on the welcome screen leads to Design. Raster leads to Pixel. **+ Layout** leads to frames. **+ Photo** opens the photo workspace, a folder, or a file. Motion has no empty-workspace button. You open it when the canvas has something to move. The layer stack is shared. Eye and lock work per object. A pixel layer is a layer. A frame is in the document. A group moves with its children. Opacity and blend on a vector object live in the transform inspector. Placed images use the layer opacity and blend above the Layers tree. A frame's opacity hits its whole subtree. Pass through on a group decides whether child blends reach the backdrop. None of that is a persona-specific file. You change persona and the stack is still the stack. This is the model 0.5.8 ships. The five rooms were the product well before this release's welcome polish and Lua plugins. 0.5.8 did not split them into five binaries, and it did not merge them into one tool well with every key live at once. Keys that do not belong to the current persona do not switch tools. `B` paints in Pixel. In Design the first keys are `V`, `P`, `R`, and `T`. ## In the hand ```sh omadesign ``` Open **+ Vector**, take a template, and land in Design. Press `T` and set a headline. Press `R` and drag a block behind it. You are making the poster in the `.oma`. Switch to Layout when the same file needs a screen. Press `F` and drag a frame. Draw another frame inside it and it nests. The headline you set in Design is still in the document. You did not export a PDF to "send it to layout." Switch to Pixel. If there is no pixel layer, add one in the Layers studio. Press `B` and paint. The type stays type. The rectangle stays a rectangle. Paint is pixels on that layer, in this file. Open Photo from its persona when the job is a capture. Crop with `C`. Move the Light, Color, and Detail controls. **Save settings** writes `.omaphoto` next to the original. **Place in Design** drops the developed 8-bit image into the document as a pixel layer, with undo. The RAW is still the RAW. Switch to Motion when the poster should move. The timeline sits under the canvas. The drawing you did is the rest pose. Press Space to play. Press `K` to key position, rotation, and scale. **File → Export Lottie…** or **File → Export animated SVG…** when you need a file someone can play. Static PNG, JPEG, and SVG export the rest pose. The clip stays in the `.oma`. Save once. ```text Ctrl+S ``` One `.oma`. Reopen it. The layers you painted, the frames you nested, and the keys you set come back with the vectors. Photo's RAW development comes back from the `.omaphoto`, not from inside that save. ## The edge A persona switch does not fork the document. There is no "export for Pixel" step inside the studio. If you wanted five files, you would export on purpose, from File, to a format you named. Photo will not hide the camera original inside the `.oma`. Place in Design is an 8-bit layer. Further grade work still belongs to the RAW and the settings file beside it. Design save does not rewrite that camera file. That boundary is the one the single document is not allowed to blur. Motion will not rewrite the rest pose to store an animation. The keys live in the clip. Delete the animation and the drawing remains. Lottie export errors clearly when the composition has pixel layers, layer masks, or effects it cannot keep. Animated SVG is the export that retains masks and effects. The persona will not silently drop them to make a file. Empty Motion is not a welcome button. Draw first. Then animate the artboard you already have. Switch persona on the open `.oma`. Press the first key for that room. The file under `Ctrl+S` is still the same file. ## The thread Part 11 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-document-tabs) · [Next](/blog/omadesign-0-5-8-design-persona-tools) --- # Design persona tools Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-design-persona-tools Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, design ## The habit Illustrator's left toolbar is a column you can hide and relearn after every reinstall. The keys underneath it are the part your hand kept. `V` selects. `P` draws a path. Illustrator's rectangle key is `M`. The manual here is explicit: Rectangle is `R`. `T` is type in all of them. Affinity Designer uses the same cluster, with the Move tool as the way back to a finished pen path. You know the bounce those keys sit inside. Draw in Illustrator. Jump to Photoshop to try a texture. Jump back because the type was still live over there. The keys are familiar. The file boundary is not. Every trip risks an outline, a rasterized headline, a missing font dialog. The first minute in Design should be those four keys and nothing else asking for attention. Move, pen, rectangle, type. A mark. A poster. A layout that is still vectors. The rest of the well can wait until the thing exists. ## The constraint One binary, so Design is not an Illustrator process beside a Photoshop process. One `.oma`, so the rectangle and the paragraph are objects in the file you will still have open when you switch to Pixel or Motion. One layer stack. One undo step per change. No Creative Cloud session between `T` and the glyph. The persona is a filter on the keyboard. A tool key switches tools only when that tool belongs to the persona you are in. Design's first row is Move, Pen, Rectangle, and Type. Brush, eraser, clone, and wand belong to Pixel. Frame's key belongs to Layout. Crop belongs to Photo. If `B` did something destructive in a vector poster "because the key is global," the persona split would be a costume. The shortcut table refuses that. The key returns without a tool change when the tool is not in this persona. Live objects are the other half. Type stays type under Move and under free transform. A rectangle keeps its parameters, including the corner radius you set with the corner dots, until you actually edit it as a path. The first tools have to support that. A Design persona that expanded everything on first click would be a different app. Group is `Ctrl+G`. Ungroup is `Ctrl+Shift+G`. Compound is `Ctrl+8`. Those are commands, not the first tools. They stay available. They are not what `V`, `P`, `R`, and `T` mean. Chrome follows the desktop. The tool you are in shows up in the Shortcut HUD at the bottom, upper row for the tool, lower row for letter keys. You do not have to read a manual to see that `P` is the pen once you have pressed it. `F1` is the full list when you want it. ## What landed Design is the default persona for a vector document and for the 52 templates. The manual's line for it: you are making a mark, a poster, or a layout. The first tools are Move `V`, Pen `P`, Rectangle `R`, and Type `T`. That is the 0.5.8 behavior because that is the manual's behavior in this release. The welcome polish in 0.5.8 did not rename these keys. **Move** `V`. Click to select. Drag to move. Eight handles scale. The handle above the box rotates. Shift-click adds or removes an object. Shift-drag a selection box to add. Dragging a selected object moves the whole selection. Shift constrains that move. Alt-drag clones. Corner dots round a rectangle. **Pen** `P`. Click a corner. Click-drag a smooth point. A twitch under 3 pixels stays a corner. Shift constrains to 45 degrees. Alt-drag breaks handle symmetry. The cubic draws as you go. Enter or a double-click finishes an open path. Esc drops the last point, then cancels. Click the first point to close. Click an open endpoint to continue it, or to join it to the path you are drawing. **Rectangle** `R`. Drag. Shift constrains. Corner radius lives in Transform, and on the corner dots while you are in Move. Ellipse is `O`, polygon `Y`, star `S`, line `L`, when you need them. They are the same drag-and-constrain family. They are not the first four. **Type** `T`. Click to place. Type on the canvas. The first keystroke replaces the "Type" placeholder. Enter is a new line. Esc or a click away finishes. Double-click existing type to edit. Character studio: font, size, tracking, leading, OpenType for kerning, ligatures, tabular figures, and small caps. Around them, still in Design: Node `A`, Pencil `N`, Gradient `G`, Eyedropper `I`, Artboard `Shift+O`, Zoom `Z`, Hand `H` or Space. Free transform is `Ctrl+T`. The first four are the priority. ## In the hand Open a blank vector document or one of the 52. Design is already the persona. ```text V ``` You are in Move. If something is selected, click empty canvas to let go, or press the key and then click the object you want. ```text R ``` Drag. Hold Shift if the poster wants a square. Let go. The rectangle is a rectangle. Drag a corner dot if it needs a radius. The object stays a rectangle with a radius, not a pile of paths. ```text P ``` Click, click, click for a polygon of corners. Click the first point to close. Or click-drag to pull handles, then Enter to leave the path open. Shift while you place a point locks 45 degrees. Esc once removes the last point. Esc again cancels the path in progress. ```text T ``` Click the page. Type the headline. The placeholder word disappears on the first character. Enter for a second line. Click away. Double-click the text later when the wording changes. The font list includes what the machine has. Project fonts from the brand library show up in that picker when the document is attached to a project. ```text V ``` Move the headline. Shift constrains the drag to horizontal, vertical, or 45 degrees. Alt-drag clones it. Eight handles scale the frame of the object. The handle above the box rotates it. `Ctrl+Z` undoes one of those steps. `Ctrl+Shift+Z` redoes. Look at the bottom of the window. The Shortcut HUD's upper row follows the tool you just chose. Hold Shift and the row shows the constrained gesture. Release and the normal hints return. `Ctrl+/` hides the strip if you want the canvas edge to yourself. Save. ```text Ctrl+S ``` The file is an `.oma`. The type is still type inside it. ## The edge These four keys do not open another application, and they do not export a handoff. Pixel is a persona switch away, on a pixel layer you add when the document is still vector-only. `B` does not flatten the poster from inside Design. The key does not switch to the brush while this persona is active. Move's corner dots do not run **Object → Break path**. A rectangle with a radius stays a parameterized rectangle. The first time you edit that shape with the node tool, it converts to a path. That conversion is the node tool's boundary, not `R` and not `V`. `T` does not outline the glyphs. Live text stays editable. **Object → Convert to path** is an explicit command, for when you want letter outlines and holes and are willing to give up the text object. Undo restores the text. Free transform, `Ctrl+T`, also keeps the text live. You do not fall into an expand by scaling with Move. Group, ungroup, and compound are `Ctrl+G`, `Ctrl+Shift+G`, and `Ctrl+8`. They are not hidden under `V`. Use them when you mean them. Press `V`, then `P`, then `R`, then `T`. You are in Design, in the `.oma`, with the objects still editable. ## The thread Part 12 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-five-personas-one-document) · [Next](/blog/omadesign-0-5-8-layout-persona-tools) --- # Layout persona tools Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-layout-persona-tools Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, layout ## The habit A UI mockup usually means a second app. Sketch, Figma, Adobe XD, or a page in Illustrator that you promised would stay "the source" and then redrew in the screen tool. Affinity's answer is Publisher or Designer with artboards, plus StudioLink when Photo has to retouch a picture on the same spread. The hand knows the drill. Draw a frame. Put a rectangle in it. Put type in it. Pin the children so a resize of the frame does not leave the button behind. Export a PNG for a deck, or SVG for a developer who asked nicely. The keys are simple when the file is the same file. Frame. Rectangle. Type. Everything else is a property of the frame you just drew. ## The constraint One `.oma`, one layer stack, one binary. A screen, a landing page, and a dashboard have to be frames in the document that already holds the mark. Otherwise Layout is a second product with a handoff, and the persona model is a label. There is no XD file to round-trip. There is no Figma URL required to see the button. The first tools stay few. Frame `F`, Rectangle `R`, Type `T`. Move, pen, and the rest of Design are still in the studio, on the Design persona, over the same objects. Layout's keyboard leads with the frame so a mockup starts as structure. `R` and `T` match Design on purpose. You should not learn a new rectangle key because you are drawing a screen. Frames have to nest, stack, and pin, or they are only rectangles with a different name. A child frame drawn inside a parent nests. **Stack children** packs the children in layer order, vertical or horizontal, with gap, padding, and stretch. Constraints pin a child to min, max, both edges, center, or scale when the parent resizes. Those are inspector states on the frame, not a plugin and not a second document. Export of a frame is PNG, SVG, or HTML for the frame you selected. It is an export. It does not replace the `.oma`. Cloud review is opt-in and separate: File → Sign in, then a push if you want a versioned design and a flat snapshot. You can comment on the canvas without that account. Write a note, pin it, resolve it. The inspector shows open counts on the frame. Undo stays one step. Nesting a frame, toggling stack, and moving a child are ordinary edits. Templates open unsaved, beside any tab you already had. ## What landed Layout is the persona for a screen, a landing, or a dashboard. First tools: Frame `F`, Rectangle `R`, Type `T`. This is the manual's table, as it stands in 0.5.8. The release did not invent Layout, and it did not move these keys. **Frame** `F`. Drag a frame. Draw another frame inside it and it nests. **Object → Wrap selection in frame** puts the current selection in a frame when you drew the contents first. Image placeholders live in the inspector. **Rectangle** `R` and **Type** `T` behave as they do in Design. Drag a rectangle. Shift constrains. Click for type, replace the "Type" placeholder on the first keystroke, Enter for a new line, Esc or click away to finish. Double-click to edit again. Character studio is the same studio: font, size, tracking, leading, OpenType. **Auto-layout.** Select a frame and turn on **Stack children**. Choose vertical or horizontal. Set gap, padding, and stretch. Children pack in layer order. Reorder a layer and the stack follows. `Ctrl+[` and `Ctrl+]` are the reorder chords when a layer row is selected. Shift with those chords sends the row to the back or front of its group. **Constraints.** A child of a frame can pin to min, max, both edges, center, or scale. Resize the parent and the pin is the behavior you set, not a fresh manual layout. **Export.** **File → Export frame PNG / SVG / HTML** writes the selected frame. The `.oma` remains the editable file. **Templates.** **+ Layout** on the welcome screen opens Fieldwork (a responsive prototype), a mobile screen, a landing hero, a dashboard, and a card stack. The same starters live under **File → Template library → Layout starters**. The Layout file icon on welcome is the blank size chooser. **Comments.** Write a note, pin it on the canvas, resolve it. Open counts show on the frame in the inspector. Photo and Layout can open from the start screen without creating an artboard. A saved artboard-less Layout document keeps that state when you reopen it. ## In the hand ```sh omadesign ``` Click **+ Layout**. The sheet stays at the width it opened. Choose a mobile screen, a landing hero, a dashboard, a card stack, or Fieldwork. **Use this template**, or double-click. The document is unsaved. Layout is the persona. ```text F ``` Drag a frame inside the screen. Drag a second frame inside the first. It nests. In the inspector, turn on **Stack children**. Set a gap and padding. Drag the parent's edge. The children follow the stack. ```text R ``` Drag a rectangle inside the frame for a button shape. Shift if it should be square. ```text T ``` Click and type the button label. Click away. Select the type object and pin it with constraints if it should stay centered when the button frame changes width. The pin options are min, max, both edges, center, or scale. Select the screen frame. **File → Export frame PNG**, or SVG, or HTML, for a snapshot someone can open without the studio. The `.oma` tab is still open. `Ctrl+S` writes the real file. Drop a comment on the frame if a note belongs on the canvas. Resolve it when the note is done. The inspector's count is the list you did not keep in a chat window. Need the mark from the poster in this same file? It is already on the layer stack if you drew it here. If it lives in another `.oma`, open that file in a tab with `Ctrl+O` and copy across. Paste lands objects at their original positions. You are not exporting a PDF to move a logo between two tabs of the same app. Switch to Design, press `P`, and draw a mark beside the frames. Switch back to Layout. The mark is in the document. `F` still makes frames. The persona changed the first keys, not the file. ## The edge Layout does not require a cloud account to draw, stack, constrain, comment, or export a frame. **File → Sign in** opens a browser approval when you want it. Push project and review export upload a versioned design and a flat snapshot. Cloud projects pull a shared design into a new document. Review annotations load cloud feedback. Publishing and competition entry are separate owner actions. None of those run because you pressed `F`. Frame export does not convert the `.oma` into HTML as the source of truth. PNG, SVG, and HTML are outputs of the selected frame. Reopen the work from the `.oma`. **Stack children** packs in layer order. It does not invent a new order behind your back. If the visual order is wrong, reorder the layers, including with `Ctrl+[` and `Ctrl+]`, and the stack follows. Groups move with their children. Motion is still not an empty welcome action. A Layout frame can be artwork you animate later, in the Motion persona, on this document. The rest pose stays the layout you built. Space plays the clip. The frame tool does not key a timeline by itself. Press `F`, then `R`, then `T`, in the Layout persona, in the same `.oma` as the drawing. ## The thread Part 13 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-design-persona-tools) · [Next](/blog/omadesign-0-5-8-pixel-persona-tools) --- # Pixel persona tools Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-pixel-persona-tools Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, pixel ## The habit Photoshop is a pixel document that learned vectors. You open it to paint, clone, and select. `B` is the brush. `E` is the eraser. `J` cycles the healing and clone family, and you Alt-click or Option-click to set a source. `W` is the quick selection or magic wand, depending on the year and the tool preset you last left behind. Affinity Photo matches that hand closely enough that you can sit down and retouch without reading. The file you get is a pixel file. Vectors in that world are guests, or they live in Designer and come across a link. The failure you know is the flatten. Someone paints a shadow on the poster and saves a TIFF. The headline is no longer type. The logo is no longer a compound path. Next week's copy change starts with a reconstruct. The other failure is the selection that paints the whole layer. You meant to drop a blemish. The brush ignored the marching ants. You undo, if undo is one step and not "the whole session since Tuesday." Pixel in this studio has to feel like that Photoshop hand, on a layer, in the vector document you already have open. ## The constraint One `.oma`. Vectors stay objects. Paint needs a place to live that is not "the whole file, now a bitmap." That place is a pixel layer. If the document is vector-only, the Layers studio is where you add one. The brush does not create that layer by converting the poster. You add it, then you paint. The persona owns the keys. `B`, `E`, `J`, and `W` are the first Pixel tools. In Design those letters are not a silent flatten. A tool key only changes tools when the tool exists in the current persona. Fill is `K` here. In Motion, `K` keys transforms. The letter follows the room you are in. That is the constraint that keeps one keyboard from meaning five destructive things at once. Undo is one step. A stroke you hate comes back with `Ctrl+Z`. Selections limit paint, fill, clone, heal, and smudge. Delete clears selected pixels. The ants stay until Esc. Shift adds to the selection. The brush does not get to wander outside the ants because a pixel tool "felt free." Filters and effects exist in the Raster studio, and they are a separate commit: Apply runs on the full image in the background and makes one undo step. Cancel leaves the document untouched. This post is the tools in the hand, not that dialog. The constraint they share is the same layer. You do not export a PNG to blur it and import it back. Masks stay editable in the project. Black hides, white reveals. The eraser hides on a mask. Apply to Pixels bakes, and one undo restores both the pixels and the mask. You can retouch without baking on the first stroke. ## What landed Pixel is the painting and retouching persona. First tools, from the manual: Brush `B`, Eraser `E`, Clone `J`, Wand `W`. Raster lives on a pixel layer in the same `.oma` as your vectors. Welcome's **+ Raster** opens a blank raster size chooser and leads here. A vector document can grow a pixel layer later without becoming a different file. **Brush** `B`. Size is `[` and `]`. Hardness is `Shift+[` and `Shift+]`. The brush paints on the active pixel target. **Eraser** `E`. On a mask, the eraser hides. **Clone** `J`. Alt-click sets the source. Then paint the destination. **Wand** `W`. Tolerance lives in Brush. The marching ants stay until you press Esc. Shift adds. Next to those, still in this persona: - **Fill** `K`. - **Smudge** `M`. - **Healing brush** `Shift+J`. Alt-click clean texture on the active image, then paint over the blemish. The stroke blends sampled texture with the destination's local color and keeps transparency. The source stays fixed for that stroke. Undo restores the whole stroke. - **Marquee** `Shift+M`, **elliptical marquee** `Shift+O`, **lasso** `Q`. Drag to select. Delete clears the selected pixels. Paint, fill, clone, heal, and smudge stay inside the selection. - **Eyedropper** `I`. The sampled color shows in the sidebar. `Ctrl+D` in Pixel clears an existing pixel selection. `Super+D` duplicates. Those two chords share a letter and do not share a job. Deselect is the pixel selection. Duplicate is the object. Add a layer mask from the layer context menu's **Mask** submenu, or **Add layer mask** in Pixel: reveal all, hide all, or start from the current pixel selection. Switch between Pixels and Mask in the inspector. Invert flips the mask. Remove reveals the untouched layer. Choose Pixels before the healing brush or the clone brush when you want those tools on the image. Raster studio filters and effects are the committed cousins of these tools. Apply is one undo. Duplicate the layer first if you still want the unfiltered pixels as their own layer. The tools above are the direct paint. The dialog is the bake. ## In the hand Open the poster `.oma`, or start from **+ Raster** with a size. ```sh omadesign ``` Switch to Pixel. If Layers has no pixel layer, add one. Select it. ```text B ``` Paint. Tap `[` a few times if the mark is too big. `]` goes the other way. Hold Shift and tap `[` to soften. The vectors underneath are still objects. Toggle the pixel layer's eye if you need to see them alone. ```text W ``` Click a region. Ants appear. Press `B` again and paint. The stroke stays inside the selection. Esc clears the ants. `Ctrl+D` clears them too, in this persona. ```text J ``` Alt-click a clean area to set the clone source. Paint over the spot you are covering. `Ctrl+Z` removes that stroke. ```text Shift+J ``` Alt-click clean texture. Paint the blemish. One undo restores the whole heal stroke, not a dab at a time. Need a hard-edged hole? `Shift+M`, drag a rectangle, press Delete. The pixels inside the marquee clear. The rest of the layer stays. Switch to Design and press `T`. Edit the headline. Switch back to Pixel. The pixel layer is still in the stack, still pixels. `Ctrl+S` writes one `.oma` containing both. On a mask: add one that hides all, press `B`, paint with white to reveal. Or press `E` and hide. Remove the mask if you want the layer untouched again. Apply to Pixels only when you mean to bake, and remember that one undo brings the mask and the pixels back. ## The edge The brush will not invent a pixel layer by flattening the document. Vector-only files stay vector-only until you add a pixel layer from Layers. Paint lives on that layer. The pen path you drew in Design is still a path. `B` does not fire in Design as a flatten shortcut. Change persona, then paint. `K` in this room is Fill. It is not the Motion key command. If you wanted animation keys, you are in the wrong persona, and the drawing has not been turned into a clip by accident. Selections hold the paint. Wand, marquee, ellipse, and lasso limit brush, fill, clone, heal, and smudge. They do not limit them "except for the healing brush." Heal stays inside the ants too. Esc or `Ctrl+D` is how you let go. Delete removes pixels inside the selection. It does not delete the layer. **Delete layer/object** in the layer menu is the command that removes the item. Filters do not half-apply. Cancel leaves the document alone. Apply is one undo step on the full-resolution image or mask. That dialog is not a second file. Press `B` on a pixel layer in the `.oma` you already use for the vectors. `[` and `]` set the size while you paint. ## The thread Part 14 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-layout-persona-tools) · [Next](/blog/omadesign-0-5-8-photo-persona-tools) --- # Photo persona tools Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-photo-persona-tools Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, photo ## The habit Lightroom and Capture One taught you a browser, a develop module, and a catalog. Photoshop taught you Camera Raw as a dialog on the way into a pixel document. Affinity Photo develops a RAW into the document you then retouch. The muscle memory inside the dialog is the same. Crop. Exposure. White balance. A tone curve. Color. Detail. Then the picture lands on a layout. The part you watch like a hawk is the original. A develop pass that writes into the camera file is a bad surprise. Sidecars exist so the NEF, CR3, or DNG you copied off the card stays the file you copied off the card. You can delete the sidecar and be back to the camera's file. You cannot undo a rewritten RAW with `Ctrl+Z` if the app already closed. ## The constraint One binary, so Photo is a persona, not a second install. One `.oma` for the design you are building. The camera file is not that document. A design save that embedded every RAW you ever placed would turn a poster into a disk. A design save that rewrote the RAW to "remember the grade" would destroy the capture. The constraint forces a split you can explain in one sentence. Development settings live in a small `.omaphoto` beside the original. The original bytes stay. **Place in Design** copies an 8-bit developed image into a pixel layer in the `.oma`, with undo. You keep the RAW and the sidecar if you want to grade again. Undo inside Photo is Photo's own history. It is not the poster's undo, and it is not a license to touch the camera file. Quitting asks Save all, Discard, or Cancel for unsaved photo settings before palette and artwork saves, and it waits for the writes to finish. The decoder is in the binary. You do not install a converter and you do not wait on a download to open a file. glibc 2.35 builds already bundle it. Recognized families include DNG, CR2, CR3, NEF, NRW, ARW, RAF, ORF, RW2, PEF, and the longer extension list in the format notes. An extension names a family. It does not promise every camera and every compression mode. There is no RAW writer. Export is JPEG, PNG, or TIFF, as a new file. Crop has to be `C`, because that is the key the rest of the studio already published for this job. The develop controls have to be sliders in named groups, not a wall of every vendor's label. Light, Color, Detail. Curve, mixer, and grading open when you need them. ## What landed Photo is the persona for grading a photograph. First tools: Crop `C`, the develop sliders, then **Place in Design**. This is the standing behavior in 0.5.8, including the RAW rules the format notes spell out. The 0.5.8 packages bundle the RAW decoder. They do not add a writer. Open a photo, browse a folder, drop files, or load samples. **+ Photo** on the welcome screen opens the workspace. The folder icon chooses a folder. The image icon chooses a file. Imports and folder scans run in the background. The library shows camera metadata when it has it. The first display preview's long edge is at most 1600 pixels. Zoom in and the full-resolution tiles arrive in the background while the current preview stays up. Results from an older photo or an older adjustment are discarded. The Develop panel groups **Light**, **Color**, and **Detail**. Tone curve, color mixer, and color grading expand when you need them. **Before** compares the default development. For a RAW, Before is the default camera-balanced development, the embedded JPEG left out of that comparison. **Auto light** balances exposure and contrast. RAW exposure and white balance use the 16-bit linear source. **Save settings** writes the adjustments beside the original. `photo.png` pairs with `photo.png.omaphoto`. A RAW looks like `DSC_0001.NEF.omaphoto`. The settings file does not contain the image. Keep the names matched and keep the pair together. Resume with **File → Open**, a drop, or **Photo → Library → ··· → Open photo or settings…**. Either file restores the original pixels and the saved adjustments. **Place in Design** adds an 8-bit developed pixel layer. Undo removes that placement as a step on the design. Retain the RAW and the `.omaphoto` for later. Export JPEG, PNG, or TIFF in the background at full developed resolution, including crop and rotation. RAW PNG and TIFF keep 16-bit channels. JPEG is 8-bit delivery. Exported files do not keep the sensor mosaic or the camera's edit history. Crop commits with Enter and cancels with Esc during the drag. A held mouse button cannot silently restart the crop after you cancel. Space or Hand pans. `Ctrl+0` fits. `Ctrl+1` is 100%. Pinch, Ctrl-scroll, and Alt-scroll zoom. ## In the hand ```sh omadesign ``` Open Photo and point it at a folder, or drop one RAW on the window. ```text C ``` Drag the crop. Enter applies. Esc cancels the drag in progress. Rotate if the camera orientation needs a human decision on top of the orientation the decoder already honored. Move **Light** until exposure and contrast sit. Open the tone curve if the slider is not the shape you want. Switch to **Color** for white balance and the mixer. **Detail** for sharpening and noise. **Before** shows the default development. **Auto light** is there when you want a starting balance and then your own numbers. ```text Ctrl+S ``` In Photo, save writes the `.omaphoto` next to the original. It does not write the camera file. Look at the directory. Two files, matched names. The RAW's modification time stays the camera's, aside from whatever the filesystem does when you only read it. The settings file is the new one. **Place in Design.** The poster, or a new design document, gains an 8-bit pixel layer of the developed image. `Ctrl+Z` on that document removes the placement. The `.omaphoto` is still beside the RAW. Grade again later and place again if the layout needs a new development. Export PNG or TIFF when a printer or a website needs pixels and you are not placing into this `.oma`. The export is a new file. The original path is untouched. Copy a look only when you mean to. **Copy adjustments** is `Ctrl+Shift+C`. Select other photos. **Paste adjustments** is `Ctrl+Shift+V`. Crop and rotation stay off unless you turn those categories on. That batch is its own topic. The tool in the hand for one picture is still `C` and the sliders. ## The edge The original photograph is never rewritten. There is no RAW writer. JPEG XL-compressed DNG, GPR, EIP packages, and R3D video are outside this build. Only the first image of a multi-image RAW is developed, with a conversion note. Images over 64 megapixels are rejected, and inputs over 512 MiB are rejected. Lens corrections, proprietary camera looks, and unsupported DNG opcodes are not recreated. The render is not trying to match Lightroom, Capture One, or the in-camera JPEG. Opening settings reports a missing original, a changed original, or invalid settings, and does not replace the photo you currently have open. Opening the original with unusable settings shows default development and a note. A failed save keeps your edits so you can retry. Edits you make while a save is finishing stay marked unsaved. A Design `.oma` save does not store the RAW source or its settings. If the sidecar and the camera file part ways, the grade does not hide inside the poster. Place in Design already baked an 8-bit layer for the layout. That layer is not the negative. Press `C`, grade, then **Save settings**. The `.omaphoto` is the grade. The camera file stays the camera file. ## The thread Part 15 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-pixel-persona-tools) · [Next](/blog/omadesign-0-5-8-motion-persona-tools) --- # Motion persona tools Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-motion-persona-tools Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, motion ## The habit After Effects is a timeline that imports what you drew somewhere else. You precompose. You convert text to shapes because the font did not travel. You discover the Illustrator file you linked was updated, or was not. Photoshop's timeline is a different habit: frames on a pixel document. Affinity's motion tools, where you have used them, still feel like a visit to another room. The file you export is an MP4 or a GIF. The file you can still edit is the one that stayed behind, if you were careful. Lottie changed the delivery habit for interface work. A JSON file, Bodymovin, plays in a product. Designers learned to keep a "simple shapes only" version of the mark because the exporter drops what it cannot say. The honest tools tell you about the drop. The quiet ones write a file that is missing the shadow and hope you do not notice. The hand, once you are in a timeline, is play and keyframes. Space plays. You set a key, move the clock, set another key. The artwork at time zero should be the artwork you drew, not a pose the timeline invented and then baked. ## The constraint The artboard you drew is the rest pose. Motion does not rewrite it. That single rule decides the persona. There is no empty Motion document on the welcome screen, because there is nothing to animate until Design or Layout has made objects. The clip lives in the `.oma` with the drawing. Static PNG, JPEG, and SVG export the rest pose. The animation is not a second source file you have to keep in sync by hand. Tracks are the properties the drawing already has, plus reveal: X, Y, rotation, scale, opacity, stroke reveal, and fill reveal. A preset has to become ordinary keys, with its own undo, or you would own a magic object the rest of the timeline cannot edit. Presets replace only the channels they affect, inside their time range, and they extend the clip if they need to. Unrelated animation stays. `K` keys transforms for the selection. In Pixel that letter is Fill. The persona is the constraint that makes the letter safe. You are in Motion, so `K` writes X, Y, rotation, and scale. It does not fill a pixel layer. Delete has three meanings and they have to stay in order. A selected diamond deletes that key. Delete with the object name selected, or with no key selected, removes the animation and leaves the drawing. Delete again removes the object. If the first Delete destroyed the artwork, nobody would key anything. Lottie is Bodymovin 5.x for shape animation, trim paths, and fill masks. It cannot carry pixel layers, layer masks, and effects. The export has to fail with a clear error. Animated SVG keeps those. The constraint is one document, two exports, no silent data loss. ## What landed Open the Motion persona. The timeline sits under the canvas. This is the behavior in 0.5.8. The thirteen presets and the Lottie path landed with the motion work and are what this release runs. Welcome still has no **+ Motion** button. Select vector artwork. The inspector offers **Draw stroke, Pop in, Slam, Shake, Fill up, Slide up, Slide down, Slide left, Slide right, Fly, Zoom, Buzz, and Fade in**. Draw stroke needs a visible stroke. Fill up needs a closed shape with a fill. Incompatible objects, locked objects, hidden objects, and guides are skipped. Duration sits above the presets. **Timing & energy** opens delay, stagger, intensity, and start-at-playhead. Each application is one undo. The preset becomes keys. Drag a diamond to retime one. Select a shape and drag it. That writes keys at the playhead. The first key at a time greater than zero also plants the rest pose at zero, so the object animates from where you drew it. `K` keys X, Y, rotation, and scale for the selection. Diamonds on the row are keys. Drag a diamond to retime. Click a diamond and Delete removes that key. Cycle ease on a selected key. Space plays. Home jumps to the start. End jumps to the end. The repeat icon is loop. **File → Export animated SVG…** writes animated transforms plus stroke and fill reveals, and it keeps masks and effects. Text is outlined in that exported file so the glyph geometry matches the canvas. The source text in the `.oma` stays editable. **File → Export Lottie…** writes Bodymovin 5.x. Pixel layers, layer masks, and effects produce a clear error. Use animated SVG for those compositions. **File → Import Lottie…** brings a shape-layer Lottie onto the timeline. Import is a basic shape subset. The `.oma` is what keeps the full editable animation. ## In the hand Draw the piece in Design or Layout. A logo, a button, a title. Leave the type as type. Save if you want a checkpoint, then switch persona. ```text Motion ``` The timeline is under the canvas. Select the mark. Pick **Fade in** or **Slide up**. Set duration. Open **Timing & energy** if you need stagger across several objects. Apply. Press Space. The preview plays. Press Space again to stop. Home returns to the start. The canvas at time zero shows the drawing you made. Select one shape. Move the playhead. Drag the shape. A key lands at the playhead. If that time is past zero, the rest pose is planted at zero too. Press `K` to key X, Y, rotation, and scale without dragging. Drag the diamond if the beat is late. Click it and press Delete to remove only that key. ```text K ``` ```text Space ``` Want the motion gone and the art kept? Click the object's name on the timeline, or make sure no diamond is selected. Press Delete. The animation leaves. The vectors remain. Press Delete again only if you mean to delete the object itself. Export when the preview is the one you want. ```text File → Export animated SVG… ``` or, for shape animation without pixels, masks, or effects: ```text File → Export Lottie… ``` If Lottie errors, it is telling you the composition has something that exporter does not keep. Export animated SVG for that file, or remove the pixel layer from the export plan. The `.oma` still has everything. Static SVG export, `Ctrl+E` and the SVG choice, remains the rest pose. The clip does not bake into that still. Reopen the `.oma` tomorrow. The keys are in the document. The type is still editable. The animated SVG you exported yesterday is a delivery file, outlines and all. Edit in the `.oma`, then export again. ## The edge Motion will not rewrite the rest pose to store the clip. The drawing at rest stays the drawing. Keys are data on top. Delete the animation before you delete the object, and the object is still there to prove it. Lottie will not quietly drop pixel layers, layer masks, or effects. The export errors. Animated SVG is the path that keeps masks and effects. Import will not turn an arbitrary Lottie into the full Omadesign document model. It brings in a basic shape-layer subset. Keep the `.oma` if you need the editable clip. Presets skip locked, hidden, and guide objects, and they skip artwork that does not match the preset. Draw stroke does nothing useful on an object with no visible stroke. Fill up does nothing useful on an open path with no fill. You get keys on the objects that qualify. You do not get a surprise conversion of guides into animated artwork. There is no welcome button that creates an empty Motion document. Make the artboard first. Then press Space. Press `K` to key the selection. Press Space to play it. The rest pose is still the thing you drew. ## The thread Part 16 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-photo-persona-tools) · [Next](/blog/omadesign-0-5-8-shortcut-hud) --- # Shortcut HUD Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-shortcut-hud Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, shortcuts ## The habit You learned Illustrator from a cheat sheet taped to the monitor, then from muscle memory that the sheet got wrong after a version change. Photoshop's tool hints live in the options bar, one sentence, easy to miss. Affinity shows a hint line that you stop reading once the keys are in your hand. The web apps put a question mark in the corner that opens a modal and steals the keystrokes you were about to use. Modifiers are the part cheat sheets botch. Ctrl-S is save when Ctrl is down. It is not a tool. The moment you hold Ctrl, Shift, or Alt, the interesting commands change. A strip that keeps showing "P pen" while Ctrl is held is showing you the keys you are not about to press. The strip has to change with the hand, then change back when you let go. ## The constraint One window, one canvas, keys that differ by persona. The hint cannot be a floating palette you dock, because a docked palette becomes one more layout to save and lose. It sits along the bottom. It has to stay out of the document. Reading it must not take keyboard focus, must not fire a command, and must not type into the text object you are editing. Two rows fit the model the app already has. The upper row follows the current tool or the current edit. The lower row shows letter keys. Letter keys are how you pick tools: `V`, `P`, `R`, `T`, `B`. Modified keys are how you run commands: save, undo, group, free transform. Holding Ctrl, Shift, Alt, or a combination replaces the view with the commands that match that hold. Release, and the tool hints return. If the strip showed both worlds at once it would be the cheat sheet again, too small to read. Text fields, menus, and drawing gestures need their own context. A HUD that advertises `Ctrl+S` while you are naming a layer is fine as information. A HUD that swallows the S and saves is a bug. The strip uses the same shortcut routing as the app. Focus decides who owns the key. The HUD does not become a second listener that races the canvas. `Ctrl+/` toggles it because slash is not a tool key. **View → Shortcut HUD** is the same toggle for the mouse. `F1` stays the complete list. The strip is the local hint. F1 is the book. Config can control shortcut hints with the rest of the UI preferences, stored under `~/.config/omadesign`. Personas change the upper row because they change the tool. Design's pen hints are not Pixel's brush hints. Photo and Motion get their own context. The document does not change when the hint changes. ## What landed The Shortcut HUD sits along the bottom of the window. This is the behavior in 0.5.8, written up in the manual's first pages and in the shortcut table. It came in with the "little keys" pass and has been part of the studio since. 0.5.8 did not remove it and did not replace it with a modal. The upper row follows the current tool or edit. Pick the pen and the path controls follow: angle constraints, handle controls, finishing the path. Pick Move and the select, scale, rotate, and clone hints follow. The lower row shows letter keys, the plain presses that choose tools. Hold Ctrl, Shift, Alt, or a combination. The strip shows the matching commands, and the gestures you are holding light up. Command keycaps stay compact. With Ctrl held, **S · Save** appears under the Ctrl heading. Let go. The normal tool hints return. The routing is the app's routing. What you see is what that modifier will do in this focus, not a generic poster of every chord in the binary. Text editing and menus get their own context. A focused field does not treat the strip as a place to type. Drawing gestures keep their hints. Reading the strip does not steal focus and does not trigger a command. **Ctrl+/** or **View → Shortcut HUD** shows or hides the strip. **F1** opens the complete shortcut list. The keys block in the manual is that list in short form: tools, undo, group, compound, free transform, guides, snapping, zoom, and the Motion pair Space and `K`. The strip stays a constant height while those modifier views swap. A drag in progress does not jump because you held Shift to constrain it. Overflow at small window sizes is the **+ more** hover, which is the next decision over. Here the decision is the two rows and the modifier swap. Hold a modifier. Read the row. Release. Draw. The command you read is the command the key runs. The act of reading did not run it. ## In the hand Open any document in Design. ```sh omadesign ``` Press `P`. Look at the bottom edge. The upper row is the pen. The lower row still offers the letter keys, so `V` and `T` are visible as ways out. Draw a point. Hold Shift. The row switches to the 45-degree constraint and the other Shift commands that apply. Place the point. Release Shift. The pen's normal hints are back. The path did not end because the HUD updated. Hold Ctrl. Read **S · Save** under the Ctrl heading. Release Ctrl without pressing S. The document does not save from the act of looking. Press `Ctrl+S` when you actually want the write. Hold Alt and drag a selected object with `V` if you want the clone the hint describes. The HUD lights the Alt gesture while you hold it. Release. The hint returns to the tool row. ```text Ctrl+/ ``` The strip hides. Draw again. The keys still work. `P` is still the pen. Toggle once more with `Ctrl+/`, or with **View → Shortcut HUD**. The strip returns on the bottom edge, same two-row layout. Press `F1` when you need the chord that the strip is not showing, group or compound or the photo adjustment copy. Close that list. Focus is back on the work. The HUD did not keep the key. Click into a text object with `T` and type. The HUD's context becomes the text edit. Characters you type go into the text. They do not activate the letter-key tool row underneath the caret. Switch to Pixel and press `B`. The upper row follows the brush. The `.oma` stays the `.oma`. ## The edge The HUD will not take keyboard focus. It will not run a shortcut because you hovered a hint or because the row redrew. Hints are information. The canvas, the text object, the menu, or the field keeps the keys. It will not list the entire shortcut table in the bottom strip. `F1` is the complete list. **+ more** is the overflow for a short window, not a second manual. If you need every export chord and every Photo batch chord, open F1. Hiding the strip does not disable the shortcuts. `Ctrl+/` is a view toggle. `Ctrl+Z` still undoes. `Ctrl+G` still groups. People who already know the keys can put the strip away. People who do not can bring it back without opening a browser. The row you see while Ctrl is held is the Ctrl command set for this focus. It is not a preview of a different app's cheat sheet. Illustrator's `M` for rectangle is not going to appear there. Rectangle in this studio is `R`, and the lower row is where that letter lives when no modifier is held. Press `P`, then hold Shift, and read the upper row before you place the point. Release. `Ctrl+/` puts the strip away when you want the bottom edge back. ## The thread Part 17 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-motion-persona-tools) · [Next](/blog/omadesign-0-5-8-hud-modifiers-while-drawing) --- # HUD modifiers while drawing Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-hud-modifiers-while-drawing Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, shortcuts ## The habit You hold Shift in the middle of a stroke. In Illustrator that constrains a line, a scale, or a brush. In Photoshop it constrains a marquee or paints a straight segment. In Affinity it does the same family of jobs. The options bar above the canvas often grows or swaps a control when the modifier goes down. If that bar changes height, the canvas moves. The point under your pen is no longer the point you aimed at. You finish the stroke, zoom in, and see the kink. Small laptop windows make it worse. The hint line wraps onto a second row only while Alt is held, then unwraps when you let go. Every toggle is a vertical jump. People stop using modifiers, or they undock every panel until nothing can reflow, and then they have no hints at all. Tooltips that are really buttons make the same class of mistake. The hint under the cursor is a focusable control. You tap a key to confirm the stroke and the tooltip eats it. Or a screen reader, or just Tab, lands in the hint strip and the next letter changes a setting you could not see. The behavior you want is dull. The hint area is a fixed slot. Modifiers change the words inside the slot. The slot does not change size. Extra words wait under a hover. Nothing in the slot is a text field. ## The constraint The Shortcut HUD is already the bottom strip: upper row for the tool, lower row for letter keys, modifier holds swap in the matching commands. That swap is exactly when a drag is in progress. Shift constrains pen points, handles, pencil and brush strokes, and object or artboard moves. Alt-drag clones. Ctrl during a drag reverses snapping if snapping is on, or the reverse of that toggle, then release returns you to the mode you had. Those holds are the gestures that must not move the canvas. So the strip keeps one height through every modifier combination. The canvas stays anchored. A pen point you are placing does not slide because the hint row grew a second line of keycaps. Layout checks at 960 by 640 and at 1600 by 1000, hints on and off, exist to prove the canvas stays put as modifiers change. The design consequence is the same at any size you actually use: height is reserved up front. A short window cannot show every hint at that fixed height. Growing the strip would break the rule you just paid for. **+ more** is a hover on the overflow. It does not reflow the canvas. It does not take keyboard focus. You point at it when you want the rest. You go back to the stroke when you are done. Hints stay informational. Text edits, focused fields, menus, and drawing gestures own the keyboard. The HUD is not a widget in that chain. `F1` opens the complete list because the strip must not grow into one. Pen and brush keep their own hints inside that same fixed rectangle. ## What landed As it stands in 0.5.8, the HUD keeps the same height while modifiers change, so a drag stays anchored to the same canvas. The manual states that next to the description of the two rows. Hover **+ more** to inspect overflow hints at smaller window sizes. Hints do not take keyboard focus. **F1** opens the complete shortcut list. **Ctrl+/** and **View → Shortcut HUD** still show or hide the whole strip. Hiding it is the other way to get the pixels back. It is not required for the height rule. The height rule holds while the strip is visible. Overflow is a hover. **+ more** shows the hints that did not fit. Leave the hover and you are back to the fixed row. You do not click **+ more** to apply anything. There is nothing to apply. F1 is a separate surface. It can take the focus a list needs. When you dismiss it, the drawing still has the keyboard, and the HUD is the short strip again. Config's shortcut-hint preference lives under `~/.config/omadesign`. The in-session toggle remains `Ctrl+/`. Changing the words in the strip is not allowed to move the art under your hand. ## In the hand Open a document, press `P`, and start a path. Do not finish it. ```text P ``` Hold Shift halfway to the next point. The upper row replaces the idle pen hints with the Shift gestures, including the 45-degree constraint. Watch the point you already placed and the canvas edge. They stay. The strip does not grow taller to make room for the Shift keycaps. Click the point. Release Shift. The words change back. The path does not kink from a layout shift, because there was no layout shift. Hold Ctrl during a drag that should ignore snapping, or restore it, depending on whether snapping is currently on. `Ctrl+Shift+;` is the toggle. A hold of Ctrl during the drag flips that choice temporarily. Release Ctrl and the snap mode returns. While you hold it, the HUD shows the Ctrl commands at the same height. The artboard does not bump. Alt-drag a copy with Move. ```text V ``` Select the object. Hold Alt and drag. The hint lights the clone gesture. The strip stays one height. You get a duplicate when you release the drag, which is the tool's behavior, not the HUD applying a click. Shrink the window until the bottom row cannot fit every letter. **+ more** appears. Hover it. Read the overflow. Move the pointer back to the canvas and continue the path. Do not press a key "into" the overflow. The overflow does not want the key. Press `F1` if the hint you needed is not in the overflow either. ```text F1 ``` The full list opens. Find Group, `Ctrl+G`, or free transform, `Ctrl+T`, or whatever the strip was right to omit. Close the list. Press the chord yourself. The command runs because you pressed it, not because the list or the strip captured it. Toggle the strip off in the middle of a pen path with `Ctrl+/`. Finish the path with Enter or by clicking the first point. The modifiers still constrain. The HUD was never the thing applying the constraint. It was the thing telling you the constraint exists, without moving the page. Switch to Pixel, press `B`, paint, and hold Shift to constrain the stroke. Same height rule. The brush anchor stays where your hand put it. ## The edge **+ more** will not grow the strip, and it will not reflow the canvas to show every hint. A small window keeps the reserved height. The rest is a hover. If the hover is not enough, `F1` is the list. The strip refuses to become the list. The HUD will not take a keypress away from a text edit, a field, a menu, or the drawing. Focus loss and modal ownership were part of the same work. A dialog has the keys while it is up. The hint row does not accept them in the background and fire Save. Hiding hints does not turn modifiers off. Shift still constrains. Alt still clones. Ctrl still flips snapping for the length of the drag. The edge of this feature is chrome stability and focus. It is not a second shortcut engine. A drag that is already on the canvas stays anchored when the modifier row changes. If the page moves, it is because you panned, zoomed, or scrolled. It is not because you held Shift. Hold Shift in the middle of a pen stroke and watch the canvas stay still. Press `F1` when the fixed row is not the whole story. ## The thread Part 18 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-shortcut-hud) · [Next](/blog/omadesign-0-5-8-omarchy-theme-chrome) --- # Omarchy theme chrome Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-omarchy-theme-chrome Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, theme ## The habit You theme the desktop once and then every app argues. Illustrator has its own brightness slider. Photoshop has another. Affinity has a UI gray you set in Preferences and forget until you change rooms. The browser has a prefers-color-scheme bit. The terminal has the theme you actually like, the one you spent an evening on. By Thursday the studio is a light gray island on a dark desktop, or the reverse, and the font in the menus is whatever the toolkit defaulted to in 2014. Omarchy users already made the choice in a theme file and a font command. `omarchy font current` is the face. `colors.toml` is the palette. Phosphor is the icon set a lot of that desktop already speaks. An app that ships a private dark skin and a private icon font on top of that is a guest that brought its own furniture and shoved yours against the wall. The habit you want from a native tool is dull and specific. Launch it. The chrome matches the desktop you were just in. The icons match the other Omarchy tools. If you need a different UI face for a talk or a screenshot, you can point at one file and not rebuild the app. ## The constraint One binary, running on Linux, aimed at Omarchy as a first-class desktop and still runnable on Ubuntu and Arch. The document is an `.oma` and does not store your desktop theme inside the poster. Client files should not change color because you switched from Mocha to Latte. Theme is chrome. Artwork is artwork. That split is the constraint. There is no in-app light/dark switch on the welcome screen. A second switch would fight `colors.toml` and you would never know which one won. The app reads the desktop theme on launch. UI type comes from `omarchy font current`, then from fontconfig's `sans-serif` if that has nothing to say. Icons are Phosphor Light, drawn from Phosphor's own font, not from a random system dingbat page. The override has to be one environment variable, because a designer who wants a specific face for the UI already has the file. `OMADESIGN_FONT=/path/to/font.ttf`. Config also has a UI font and size control, stored with the other preferences under `~/.config/omadesign` or `XDG_CONFIG_HOME`. Those are the two places a face can come from. The theme colors still come from Omarchy. A font override is not a theme override. 0.5.8's welcome screen uses that same palette and adds a dark ground, a larger transparent logo, and panels that fade from the lighter theme color. The chrome rule did not get replaced by the welcome polish. The polish sits on top of the palette you already chose. Phosphor stays the icon weight for utility links, mode tabs, and tools. Document color is a different system on purpose. The color studio, swatches, palettes in `.omacolors`, and brand assets in `.omabrand` belong to the work. They do not follow `colors.toml`. If they did, every poster would repaint when you changed desktops. ## What landed Chrome follows the desktop. The manual's theme section and the first-five-minutes note say the same thing, and it is the behavior 0.5.8 launches with. - Theme colors come from Omarchy. The read order is the next article. The short version: a current-theme `colors.toml`, then the theme directory's file, then stock Omarchy Catppuccin if both are missing. - UI type is `omarchy font current`, then fontconfig `sans-serif`. - Override the UI face with `OMADESIGN_FONT=/path/to/font.ttf`. - Icons are Phosphor Light. - The welcome screen uses the current Omarchy palette and the desktop font. It has no separate appearance toggle. - **omadesign → Config** sets UI font and size, startup mode, welcome tab, rulers, shortcut hints, guide locking, photo provider keys, and the anonymous usage toggle. Preferences persist under `~/.config/omadesign`. Mode tabs in the title bar use Phosphor icons for the personas: the curve, the brush, layout, images, the running figure. Hover text keeps the names Design, Pixel, Layout, Photo, and Motion. The 0.5.8 wordmark menu, filter icon, and segmented welcome controls use the same icon language and the same theme colors. Blue hover and red active on the filter are state colors on that Phosphor funnel, not a second theme. The logo and wordmark are the supplied transparent SVGs. They are not recolored type. `OMADESIGN_FONT` changes UI text. It does not redraw the welcome mark. About shows the refined logo next to the version string. On this release the version you installed is 0.5.8. Nothing in the theme path writes into the `.oma`. You can send the file to a machine with a different Omarchy theme and the vectors are the vectors. Their fills are the fills you set. The other person's chrome will follow their desktop. That is the feature working, not a missing embed. ## In the hand Set the desktop the way you want it in Omarchy. Theme and font. Then: ```sh omadesign ``` The welcome ground is the dark 0.5.8 chrome. The panels fade from your theme's lighter color. The utility icons are Phosphor. Open a document. The tool icons, the persona tabs, and the HUD sit in that same palette. Change the Omarchy theme. Launch the studio again. Chrome follows. The rectangle you drew does not recolor itself. To force a UI face for one session: ```sh OMADESIGN_FONT=/path/to/font.ttf omadesign ``` Menus and labels pick up that file. The wordmark SVG does not. Clear the variable and you are back to `omarchy font current`, then fontconfig `sans-serif`. Open the wordmark menu and choose **Config** if you want a UI font and a size stored in preferences. ```text omadesign → Config ``` Set the size you can read on this display. Shortcut hints can be turned off here too. Guide locking starts locked in this build. None of those checkboxes are the desktop palette. The palette is still `colors.toml`. Draw with `R` and set a fill in the color studio. Save. Reload. Quit, switch the Omarchy theme, launch, reopen. The fill matches what you saved. The window chrome matches the new theme. If those two ever move together, something is wrong. They are supposed to be independent. Phosphor Light is the weight you should see. If a panel has dropped back to a heavy system icon or a missing-glyph box, the icon font did not load. The license for Phosphor ships with the package. You do not install Phosphor from a package manager to get the tool icons. ## The edge The welcome screen will not offer a private light/dark switch. Stock Catppuccin is the fallback when Omarchy's theme files are absent, not a skin you toggle against a live theme. If the files exist, they win. The order is the whole of that decision. `OMADESIGN_FONT` will not theme the document and will not replace the logo SVG. It is one `.ttf` for UI type. Project fonts in the brand library are a different list. Those show up in the type tool's font picker so you can set headlines. They are not automatically the menu font. Chrome will not write theme colors into the `.oma`. Palettes and brand colors are explicit saves, `.omacolors` and `.omabrand`, when you want a color to travel with the project. Leaving the theme out of the file is the boundary that keeps a desktop preference from becoming client artwork. Anonymous usage, off unless you enable it, is not a theme sync. Nothing about the palette is an account. Launch `omadesign` after you set the Omarchy theme. For a different UI face, launch with `OMADESIGN_FONT=/path/to/font.ttf`. ## The thread Part 19 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-hud-modifiers-while-drawing) · [Next](/blog/omadesign-0-5-8-theme-fallback-chain) --- # Theme fallback chain Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-theme-fallback-chain Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, theme ## The habit You have watched apps invent a theme ladder and then lie about it. A desktop sets GTK. An Electron app reads a subset. A design tool ships "match system" and then opens in its own charcoal because the match failed quietly. Illustrator and Photoshop never promised to read a Linux theme file. Affinity's UI color is an app preference. On Omarchy the file is real and it is yours: `colors.toml`, in a known place, switched when you switch themes. The failure mode is a tool that only looks at one path. You use the state directory Omarchy updates when the current theme changes. The app looks at the config tree from a tutorial written last year. You get yesterday's palette, or you get the app's built-in skin, and you waste twenty minutes deciding you configured Omarchy wrong. The other failure is the reverse. The app requires its own copy of the theme in its own dot-directory. You keep two Catppuccin checkouts. They drift. The terminal and the studio disagree by one crust shade and it is all you can see. A short, ordered list of files is the habit that works. Try the current theme. Then the named theme in the config directory. Then a stock palette that ships with the app so the window can still open on a machine that has never run Omarchy's theme switcher. Stop at the first one that exists. ## The constraint Chrome follows the desktop. The `.oma` does not store that chrome. Welcome has no light/dark toggle, because a toggle would be a second source of truth next to the files. The binary has to know exactly which file it read, in an order you can check with `ls` before you file a bug. Omarchy already has two locations people meet in the wild. The current theme is linked or written under the state directory, so a switch is one place. The theme library lives under the config directory, one folder per theme, and "current" is a name. If the app read only the library, a switch that updates state and not your mental model of the folder name would look like a stuck theme. If it read only state, a fresh config with themes on disk and an empty state directory would open unthemed. The chain is the constraint made visible. Stock Omarchy Catppuccin has to be last. It is the floor when the other two reads find nothing. It is not a private brand skin that overrides a theme you actually set. Putting it first would make every Omarchy theme look like the default and the feature would be a lie. Putting a random "Omadesign dark" first would do the same thing with a different paint. The read happens on launch. There is no watcher described in the manual that live-reloads `colors.toml` while you drag a slider in another app. Relaunch and the chain runs again. That keeps the studio's frame loop out of the business of polling your whole home directory. Icons stay Phosphor Light across all three stops. The font chain is separate: `omarchy font current`, then fontconfig `sans-serif`, then `OMADESIGN_FONT` if you set it. Colors and type are two ladders. Do not expect a theme file to change the menu font, and do not expect a font file to change the crust color. 0.5.8's welcome fade uses the lighter color from whatever this chain resolved, against the dark ground. The chain is older than that polish. The polish consumes its result. ## What landed On launch the app reads theme colors in this order, and this is the order the manual prints: 1. `~/.local/state/omarchy/current/theme/colors.toml` 2. `~/.config/omarchy/themes//colors.toml` 3. Stock Omarchy Catppuccin, if nothing else is there. `` is the theme name Omarchy has selected. You do not pass it as an Omadesign flag. The studio asks the desktop's convention. The first file that is there supplies the chrome. Browser panels on the 0.5.8 welcome screen fade from that theme's lighter color into the dark welcome ground. Persona tabs, tools, the Shortcut HUD, and the wordmark menu sit in the same resolution. The filter icon's blue hover and red active are state colors drawn on top of that chrome, not alternate themes. Stock Catppuccin is the third stop. It is in the binary so a machine without Omarchy's theme files still gets a coherent window: the Catppuccin palette Omarchy itself treats as home, not a one-off gray. Ubuntu and Arch machines that are not running Omarchy land here unless those paths exist for some other reason. Asahi Omarchy with a normal theme setup lands on step one or step two and never needs the stock copy. The chain does not consult Creative Cloud, a user account, or the open `.oma`. Cloud sign-in can sit in the File menu and do nothing to `colors.toml`. Recovery swaps, templates, and plugins do not carry a theme. A Lua plugin can paint pixels in a document. It does not get to rewrite the three-step read. Config's UI font and size are preferences under `~/.config/omadesign`. They are not step four of the color chain. Anonymous usage is off by default and is not a theme service. This is standing behavior in 0.5.8. The release notes do not claim the ladder was added that day. They do ship a welcome screen that assumes the ladder, including the transparent logo on the dark ground and the lighter theme color in the panels. ## In the hand See what you have before you launch. ```sh ls -l ~/.local/state/omarchy/current/theme/colors.toml ls ~/.config/omarchy/themes ``` If the state file exists, that is the file Omadesign will use. Remember the colors. Then: ```sh omadesign ``` The chrome should match that file. Open **+ Vector** or a blank document and glance at the title bar and the HUD. Same palette. If the state file is absent and `~/.config/omarchy/themes//colors.toml` exists, the launch uses that second file. Put the state file back and launch again. State is first, so it wins while it is present. On a machine with neither path, stock Omarchy Catppuccin is the chrome. The window opens. You can draw. Add Omarchy's theme later, relaunch, and the chain picks up the file you added. ```sh omarchy font current ``` That command is the desktop face. `OMADESIGN_FONT` is a one-shot UI face. Neither edits `colors.toml`. Switch themes in Omarchy, then launch the studio again. Welcome has no second theme menu. **Config** under the wordmark is font, size, startup, rulers, hints, guides, photo keys, and usage. Palette edits belong in `colors.toml`. ## The edge The chain stops at the first file that is there. It does not merge the three stops into one palette. Stock Catppuccin is only the stop you reach when the two theme files are absent. Stock Catppuccin does not override a theme you have. It appears when steps one and two are absent. If your Omarchy theme is loaded and the studio still shows the stock palette, the current file is not where the chain looks, or the process you launched is an older binary. `~/.local/bin/omadesign --version` should report 0.5.8 for this install. The paths above are the ones that binary reads. The chain will not recolor open documents. Fills, strokes, palettes, and brand colors stay in the file and in `.omacolors` / `.omabrand`. Relaunch after a theme switch and the poster matches the save. Only the chrome moves. There is no per-document theme and no theme written into the idle swap. Recovered documents come back as documents. They do not come back as skins. Check `~/.local/state/omarchy/current/theme/colors.toml`, then launch `omadesign`. That file, when it is there, is the chrome you are about to see. ## The thread Part 20 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-omarchy-theme-chrome) · [Next](/blog/omadesign-0-5-8-move-tool) --- # Move tool Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-move-tool Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, vectors ## The habit `V` is the way back to a calm hand. In Illustrator, Selection is `V`, Direct Selection is `A`, and you live on `V` until a point is wrong. Drag moves. Shift constrains. Alt-drag or Option-drag clones. The bounding box has eight handles. The rotate cursor lives just outside, or on a handle, depending on how close you are and which version trained you. Corner widgets round a rectangle without converting it to a path, until you expand it on purpose. Affinity Designer's Move tool is the same grip. Scale from the handles. Hold Shift for aspect or for a constrained move. The clone is a modifier you trust more than a menu called Duplicate. Photoshop's Move tool is the cousin on pixels: drag the layer, arrows nudge, Alt duplicates. You bring that hand to a vector app and you want the object, not the layer thumbnail, to be what moves. The habit includes multi-select. Shift-click adds. Shift-click again removes. A marquee adds when Shift is down and replaces when it is not. Groups move together until you double-click to go inside. If any of those are "somewhere in a preferences checkbox," the tool is not finished. ## The constraint One `.oma`, one undo step per edit, objects that stay live. Move is the tool that has to honor that. Scaling type with the bounding box cannot outline the glyphs. Rounding a corner cannot run **Object → Break path** under you. A clone is a new object in the same layer stack, one undo back to the single original, not a "duplicate file" dialog. The persona is Design for this key as the primary select tool, and `V` is also the select key the shortcut table maps before the persona check. If the tool is not in the persona you are in, the key does not switch. In Design, `V` is home. Layout still has frames you move. The manual hangs the full Move paragraph on the Design chapter: click, drag, eight handles, the handle above the box, Shift-click, Shift-drag, Alt-drag, corner dots. Snapping and Shift have to cooperate with the rest of the studio. Snapping catches object and artboard edges and centers, guides, the grid, and equal spacing. `Ctrl+Shift+;` toggles it. Hold Ctrl during the drag to flip that choice for the length of the drag, then release to return. Shift constrains the move to horizontal, vertical, or 45 degrees, and it constrains pen and brush the same way. Alt-drag clones. Alt with Shift is a constrained copy. Move cannot invent a private constraint system or the HUD would be lying when it shows those gestures. Selection and layer reorder share chords on purpose, with a boundary. `Ctrl+[` and `Ctrl+]` reorder when a layer row is selected. Click an object on the canvas and those chords return to object stacking. Move's click is that return. The tool that selects on the canvas is the tool that hands the bracket keys back to stacking. One undo per reorder stays true. One undo per move stays true. They are different steps. Free transform, `Ctrl+T`, puts the current selection into Move with the scale and rotation handles ready. Move is the body. Free transform is the door. Parameters stay editable either way. ## What landed **Move** is `V`. This is the manual's behavior in 0.5.8. Click an object to select it. Drag to move it. The bounding box has eight handles and they scale. The handle above the box rotates. You do not hunt a rotate mode in a menu. Shift-click adds an object to the selection or removes it if it was already in. Shift-drag draws a selection box that adds objects. Dragging an already selected object moves the whole selection. Shift during that move constrains it. Alt-drag clones. The clone is a real object. `Ctrl+Z` removes the clone and leaves the original where it was. `Super+D` duplicates in place when you want a copy and no offset. Alt-drag is the copy that follows the mouse. Corner dots round a rectangle. The radius is a parameter. Transform also holds corner radius, along with polygon sides and star inner radius when those shapes are what you selected. Ellipse is `O`, polygon `Y`, star `S`, line `L`. You draw them with their own keys. You adjust them with Move and the inspector. Groups select, move, duplicate, and align as units. Double-click an item to edit it alone until you select something else or deselect. The group's children come along when the group moves. That matches the layer rule: groups move with their children. `Ctrl+T` lands in this same handle set from the keyboard or from the Object menu. Press `V` and select the object and you are already there. Same handles. Same live text. Same rectangle parameters. ## In the hand Open a Design document. ```text R ``` Drag a rectangle. Press `V` if the rectangle key did not leave you in Move. Click the rectangle. Eight handles appear. Drag a corner handle. The shape scales. Drag the handle above the box. It rotates. `Ctrl+Z` steps back one of those edits. Drag a corner dot toward the inside of the rectangle. The corners round. The object is still a rectangle. Look at Transform if you want the radius as a number. You have not created a path. Hold Shift and drag the object sideways. The motion stays horizontal, vertical, or at 45 degrees, whichever your drag is closest to. Release. Hold Alt and drag. A second rectangle appears. Release Alt and the drag. Click the original, then Shift-click the clone, so both are selected. Drag. Both move. Shift-click the clone again. It leaves the selection. Hold Shift and drag a box around several objects to add them. Release. Drag one selected object. The set moves. ```text Ctrl+T ``` You are in Move with the handles ready, which is where `V` already put you. Scale the type you placed with `T`. Double-click it and the characters are still characters. The scale did not run **Convert to path**. `Ctrl+Shift+;` toggles snapping if the equal-spacing lines are in your way. Or hold Ctrl for one drag and let go. The HUD will show that modifier while it is down, at the same strip height. Double-click into a group when one child has to move alone. Click outside, or select another object, when you want the group to behave as a unit again. `Ctrl+G` groups the current selection. `Ctrl+Shift+G` ungroups. Neither chord is required to use Move. They change what "the object" is. Save with `Ctrl+S`. Reopen. The radius, the rotation, and the clone are still in the `.oma`. ## The edge Corner dots round a rectangle. They do not convert it to a path. Node tool edits do that conversion the first time you edit the shape as points. **Object → Break path** is the explicit command. Move refuses that conversion so a radius stays a radius you can change again tomorrow. Move does not outline live text. Scale and rotate leave the text object editable. **Object → Convert to path** is how you ask for outlines, and undo restores the text. Free transform shares this refusal. Reshape is the mode that converts a live shape or live text when you move the first cage handle. That is a different tool, under **Object → Reshape**. A click on the canvas returns `Ctrl+[` and `Ctrl+]` to object stacking. Those chords reorder layers while a layer row is the selection target. Click the layer row when you want reorder. Selecting a path does not by itself create an undo step. Moving, scaling, rotating, cloning, and rounding do. Undo matches those edits, one step each. Press `V`. Drag. Hold Shift to constrain. Hold Alt and drag when you want the clone. ## The thread Part 21 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-theme-fallback-chain) · [Next](/blog/omadesign-0-5-8-layer-reorder-shortcuts) --- # Layer reorder shortcuts Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-layer-reorder-shortcuts Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, vectors ## The habit You reorder layers with a drag, and you reorder them with brackets. Illustrator uses `Ctrl+]` and `Ctrl+[` to step an object forward and backward, and `Ctrl+Shift+]` and `Ctrl+Shift+[` to send it to the front or the back. Photoshop uses the same chords on layers. Affinity matches them closely enough that your hand does not look down. The menu says Arrange, Bring Forward, Send Backward, Bring to Front, Send to Back. You use the menu once, then you never use it again. The drag habit is the insertion line. You grab the layer name, not the eyeball, and you drop it on a line between two rows. Drop it on the row itself and you might have meant "nest in this group," which is a different drop. Good panels show a line for between and a highlight for inside. Bad panels guess, and you nest a logo inside a button group you then have to undo in three drags. Groups make the chords dangerous if they ignore structure. Send to Front on a child should mean the front of its group, not the front of the whole poster, unless you asked for the whole poster. Otherwise every "front" rips the icon out of the button and paints it over the footer. You undo, you try again, you start dragging. Undo has to be one reorder, not "the reorder plus a resort plus a renamed layer." `Ctrl+Z` returns the stack to the previous order. That is the whole contract. ## The constraint One layer stack in one `.oma`. Personas do not keep private stacks. A rectangle you drew in Design, a frame you drew in Layout, and a pixel layer you added for paint are rows in the same tree. Reorder changes draw order. It does not convert a vector into pixels and it does not write a second file. The chord has to be the Illustrator chord, because that is the hand this studio said it would meet. The shortcut table maps them as: - `Ctrl+]` brings the target forward one step. - `Ctrl+[` sends it backward one step. - `Ctrl+Shift+]` sends it to the front. - `Ctrl+Shift+[` sends it to the back. The manual binds the Shift variants to the group you are inside. Select a layer row. `Ctrl+[` and `Ctrl+]` reorder it. Add Shift and it goes to the back or the front of its group. It does not leap out of the group into the document root. That is the constraint that keeps a button's label behind the button shape and in the button group. The same chords mean object stacking when the selection came from the canvas. Click an object with Move and you have returned the brackets to stacking. The app cannot have one meaning for "whatever was selected somewhere." The target is the layer row, or the target is the canvas object. You switch target by where you click. One undo either way. Drag is the mouse form of the same edit. An insertion line reorders. The center of a group or a Layout frame nests. Groups move with their children, so you do not reorder a parent and leave the children behind on the old index. Each reorder is one undo step. A drag that nests is that one step too, and undo restores the previous parent. Stack children in Layout reads layer order. Reorder is how auto-layout changes visual order. The shortcut and the insertion line are the controls. The stack does not keep a secret index. ## What landed Select a layer row. `Ctrl+[` moves it backward. `Ctrl+]` moves it forward. `Ctrl+Shift+[` sends it to the back of its group. `Ctrl+Shift+]` sends it to the front of its group. The manual's Keys line prints Front as `Ctrl+Shift+]` and Back as `Ctrl+Shift+[`. This is the behavior in 0.5.8. The chords are older than the welcome-screen work in this release. They are the chords the binary still ships. Drag a layer name onto an insertion line to reorder without the keyboard. The line is the target. Groups move with their children. Drag onto the center of a group or a Layout frame to nest. Hold Shift and click object rows to select several. The step is still one undo. Click an object on the canvas and the bracket chords go back to object stacking. Click the layer row again when you want reorder. The key performs the reorder. The HUD only shows the chord while Ctrl is held. Hidden and locked objects stay out of canvas selection matching. A reorder does not clear a lock and does not show a hidden row. Dragging a placed image layer onto the center of a Layout frame converts it to an image fill and keeps position, rotation, opacity, blend, and effects. An existing layer mask bakes into the image alpha. Undo restores the image layer and its editable mask. That drop nests. The insertion line reorders. ## In the hand Draw three rectangles. ```text R ``` Drag three times. Open the layer list. The last one you drew is at the top of its container. Click that row. Do not click the canvas. ```text Ctrl+[ ``` It steps backward. The middle rectangle paints above it. Press `Ctrl+[` again. It is at the back of the group or the layer it lives on. ```text Ctrl+Shift+] ``` It jumps to the front of its group. One undo: ```text Ctrl+Z ``` The order before that front-send is back. You did not undo the original draws unless you keep stepping. Each reorder was its own step. Redo is `Ctrl+Shift+Z`. Drag the layer name. A line appears between rows. Drop on the line. That is a reorder. Drag the same name onto the middle of a group row. That is a nest. `Ctrl+Z` lifts it back out. If the children came with the group, they still come with the group. You do not reattach them one by one. Now click one of the rectangles on the canvas with `V`. Press `Ctrl+]`. You are stacking the selected object, because the target is the canvas selection. Click the layer row again. Press `Ctrl+]`. You are reordering inside the list. The two behaviors are the switch the manual describes. Use the click to choose. Turn on **Stack children** on a Layout frame and reorder the children with `Ctrl+[`. The visual stack follows the layer order. Gap and padding stay. You did not redraw the frame. Save. ```text Ctrl+S ``` Reopen the `.oma`. The order you left is the order you get. The insertion line did not invent a different stack on load. ## The edge Shift-bracket sends the row to the back or front of its group. It does not promote the object out of the group to the root of the document. Drag onto the center of a parent when you mean to nest. Drag onto the insertion line when you mean to reorder among siblings. The two drops are different edits. Undo knows which one you did. The brackets do not reorder while the target is a canvas selection. They stack. If you wanted the layer list, click the row. The feature refuses to guess. Each reorder is one undo step. It is not bundled with a rename, a visibility toggle, or a convert-to-path. A rectangle you only reordered is still a rectangle. Live text you only reordered is still live text. A group moves with its children. You cannot reorder the group and leave the children at the old position in the tree. If you need one child to move alone, double-click into the group, or ungroup with `Ctrl+Shift+G`, then reorder. Click the layer row. Press `Ctrl+]` to step it forward. Press `Ctrl+Shift+]` to put it at the front of its group. ## The thread Part 22 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-move-tool) · [Next](/blog/omadesign-0-5-8-free-transform) --- # Free transform Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-free-transform Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, vectors ## The habit `Ctrl+T` is free transform in Photoshop, and your hand hits it even when you are holding a vector tool. A box appears. Corners scale. The area just outside rotates. Enter commits. Esc cancels. In Photoshop that commit is often pixels. You live with it because the layer was pixels already. Illustrator's version is the bounding box you usually already have, plus **Free Transform** as its own tool when you want to scale, rotate, reflect, or shear without hunting handles. The dangerous command next door is Expand. Expand turns the live rectangle, the live type, the live stroke, into paths. People hit it from a default button, or from a script, and the headline is outlines forever. Undo works until the file is saved, closed, and reopened by someone else. Affinity keeps a similar split. A transform panel and the selection handles edit the object you have. A convert-to-curves command is explicit. You can scale a text frame all day and still change the word. The habit this studio has to meet is the chord and the handles, without the silent expand. `Ctrl+T` should mean "give me scale and rotation on what I selected." It should not mean "bake the object so those handles have something easy to chew on." ## The constraint One `.oma`, and the objects in it stay the kind of objects you drew, for as long as you can stand it. Undo is one step. A transform that also converted to paths would be two edits wearing one chord, and `Ctrl+Z` would become a quiz. Either the chord converts, and undo must undo the convert and the transform together, or the chord does not convert. The second one is the decision. Live text stays text. A rectangle keeps its rectangle parameters, including corner radius. A polygon keeps its side count. You can still edit them in Transform after you scale. Move already has eight scale handles and a rotation handle above the box. Free transform does not need a second geometry engine. `Ctrl+T` puts the current selection into Move with those handles ready. The Object menu has the same command, for when the hand is on the mouse and the keyboard is the thing you do not trust yet. `F1` lists it with the other chords. The HUD can show it while Ctrl is held, as a Ctrl command, without running it. Reshape exists for the edits that are not a uniform scale or a rotation. Distort, skew, perspective, and a nine-handle warp mesh live under **Object → Reshape**. The first handle you move there converts live text and parameter shapes to paths, because a warp is not a rectangle anymore. Undo restores the original form. That conversion is allowed in Reshape because the operation needs paths. It is refused in free transform because a scale does not need paths. The constraint is "do the least destructive thing that makes the handle honest." The chord is `Ctrl+T`. Bare `T` is the type tool. The shortcut table puts free transform on Ctrl plus T, with Shift up. Adding Shift does not call a second transform. ## What landed **Free transform** is `Ctrl+T`. It puts the current selection into Move, scale handles and rotation handle ready. The same command is under **Object**. It keeps live text and shape parameters editable. That is the manual's sentence, and it is the behavior in 0.5.8. The clear menu entry and the F1 line landed with the transform work. This release did not replace them with an expand. What you get when you press it: - The tool is Move, `V`, even if you were in the pen or the type tool. - Eight handles scale the selection. - The handle above the box rotates it. - Shift constrains the move if you drag the object. The HUD states the modifier while you hold it. - Alt-drag still clones, because you are in Move and Move clones that way. - Corner dots still round a rectangle, because those dots are Move's dots. - Type stays type. Double-click to edit the string after you scale the frame. - Transform still shows the parameters: position, size, rotation, corner radius, and the rest that belong to that object. You drag the handle. The object updates. `Ctrl+Z` undoes that drag as one step. Reshape is the mode that uses Enter to finish and Esc to cancel the current drag. Free transform uses Move's undo. Object → Reshape remains one menu away for distort, skew, perspective, and the warp mesh. The first moved handle on live text or on a parameterized shape converts to paths. That boundary is what makes `Ctrl+T` safe to hit. ## In the hand Select a rectangle and a text object. ```text V ``` Shift-click so both are selected, or Shift-drag the box. ```text Ctrl+T ``` The handles are up. Drag a corner. Both scale. Drag the handle above the box. Both rotate around the selection. `Ctrl+Z` once. The rotation returns. `Ctrl+Z` again. The scale returns. Two edits, two undos. Click only the text. `Ctrl+T`. Scale it wide. Double-click the text. The caret is in the string. Change a word. The word changes. Character studio still has the font, the size, the tracking, the leading, the OpenType features. The scale did not swap the font for outlines. Click only the rectangle. Drag a corner dot. The radius changes. Look at Transform and edit the radius as a number. `Ctrl+T` did not remove that field. Press `A` for the node tool only when you are ready to convert. Until that edit, the rectangle is a rectangle. Open the menu path once so you trust it. ```text Object → Free transform ``` Same handles. Same Move tool. Press `P` when you want the pen back. The selection's geometry stays as you left it. The tool change does not revert the scale. Try Reshape on a copy if you want to see the other door. Alt-drag a clone. **Object → Reshape → Distort**. Drag a cage handle. The clone converts. `Ctrl+Z` restores the form. That undo is why the convert is allowed there. Go back to the original and press `Ctrl+T` again. It is still a parameterized shape, because you never reshaped it. Place a photo into the design and press `Ctrl+T`. Scale and rotate with the same handles. The RAW on disk is untouched. The layer in the `.oma` is the placed image. Reshape does not claim that photograph. Move does. Save with `Ctrl+S`. Reopen. The text is text. The corner radius is the radius. The rotation is the rotation. ## The edge Free transform will not expand live text, and it will not expand a parameterized shape into paths. There is no forced expand hiding in the chord. If the type is outlines, someone used **Object → Convert to path**, or the first handle of a Reshape, or a node edit that converted the shape. `Ctrl+T` is not that command. Undo on those other commands restores the live object when you undo them in the same session. `Ctrl+T` will not start the warp mesh, the distort cage, the skew, or the perspective. Those are **Object → Reshape**. Enter and Esc in that mode finish or cancel the reshape drag. They are not required to "apply" a free-transform scale. Bare `T` will not free-transform. It will set the type tool and, on a click, place a text object. The transform chord is Ctrl and T together. A placed photograph does not become a vector under this chord. It scales and rotates. The camera file, if the image came from Photo, stays where Place left the relationship: 8-bit pixels in the document, RAW and `.omaphoto` outside it. Press `Ctrl+T` on the selection. Scale it. Double-click the text and change the word. ## The thread Part 23 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-layer-reorder-shortcuts) · [Next](/blog/omadesign-0-5-8-node-tool) --- # Node tool Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-node-tool Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, vectors ## The habit `A` is direct selection. In Illustrator you press `A` and the bounding box steps aside for anchors and handles. You drag a point. You drag a handle. Alt pulls one handle without the other. You click a segment to bend it, or you use the anchor-point tool when the version you learned put convert on a different key. Delete removes the selected points. Shift-click adds points to the selection. A marquee selects the points inside it, not the whole object, which is the entire point of leaving `V`. Affinity's Node tool is the same grip: convert on a click, symmetry broken with a modifier. `P` creates the path. `A` edits it after it exists. You also expect a rectangle to stay a rectangle until you ask for nodes. The first node edit is the ask. After that you want anchors on the thing you see, including when the shape is rotated. A handle that floats in the unrotated box while the artwork sits at 30 degrees is a tool that lies. Edits have to land where your eye is. ## The constraint One `.oma`. A path is path data in that file. A rectangle is parameters until a node edit needs path data. The first time you edit a shape with this tool, it converts to a path. That conversion is the price of dragging a real anchor. Free transform and Move refuse to charge it, so `Ctrl+T` and `V` stay safe. Node charges it, once, and then you are in anchors. Undo has to restore the parameterized shape if you undo that first edit. One step. No "expanded, plus moved, plus you cannot get the radius field back." Rotated paths keep the visible points and Bézier handles aligned with the artwork. You edit at the displayed location. Saved rotations stay intact. Selecting the path does not create an undo step. A click is not an edit. A drag is an edit. That split keeps `Ctrl+Z` for the change you made, not for the moment you looked at the object. Esc does not cancel Node. In the pen, Esc drops the last point and then cancels a path still in progress. Node is editing a path that already exists. Delete removes selected points. It does not remove the whole object while those points are selected. 0.5.8 adds radius handles for corners that still have a radius. Alt-drag a corner-radius handle to change every corner. Shift-select corners, then drag one selected corner's radius handle to change that set. Compound paths from repeated booleans keep their contours and holes, and those nodes stay editable. **Object → Break path** converts without a drag. It sits next to the first node edit. ## What landed **Node** is `A`. The manual's Design chapter is the behavior 0.5.8 runs. The pen's table sits beside it so the two tools stay distinct: | Gesture | Pen `P` | Node `A` | | --- | --- | --- | | Click | Corner | Select, Shift adds | | Click-drag | Smooth point | Move selected points | | Alt-drag | Break handle | Break handle | | Shift | 45° on placement | 45° on handles | | Esc | Drop last point, then cancel | — | | Enter or double-click | Finish open path | — | | Box drag | — | Select those nodes | | Drag a segment | — | Move the line | | Click a curve | — | Insert | | Alt-click a point | — | Corner or smooth | | Delete | — | Remove selected points | Shapes convert to a path the first time you edit them. After that, the anchors you see are the anchors that save. Rotated artwork shows those anchors on the rotated geometry. Point, handle, and segment edits hit the displayed locations. The file keeps the rotation it already had. You are not required to unrotate, edit, and rotate back. 0.5.8 path editing on this tool: Alt-drag a corner-radius handle to set every corner. Shift-select a subset of corners, then drag a selected corner's radius handle to update that subset. Repeated unions and subtractions keep independent contours and holes. Nodes, handles, insertion, deletion, and a broken contour survive save, reopen, undo, redo, and SVG export. Double-click with Select or Node to edit inside the compound. ## In the hand Draw a rectangle and a pen path. ```text R ``` ```text P ``` Click three corners and close on the first point, or leave it open with Enter. Press `A`. ```text A ``` Click one anchor on the pen path. It selects. Shift-click a second anchor. Both are selected. Drag. Those points move. Anchors you did not select stay. Drag a box around a cluster to select them the same way. Drag a handle. The curve follows. Hold Shift while you drag the handle if you want 45 degrees. Hold Alt and drag the handle to break symmetry. The other side of the point stays where it was. Alt-click a smooth point. It becomes a corner. Alt-click again. It becomes smooth. Click the middle of a segment. A new point is inserted. Drag that segment's body and the line between its endpoints moves. Select a point you do not want. Press Delete. The point goes away. The object stays on the layer. While points are selected, Delete removes points. Click the rectangle with `A` and drag an anchor. That first edit converts it to a path. The corner-radius field is no longer the live rectangle parameter. `Ctrl+Z` if you wanted the rectangle back. On a shape you mean to keep parametric, use Move's corner dots or, in 0.5.8, the radius handles: Alt-drag one corner-radius handle to round every corner together, or Shift-select corners and drag one selected radius handle to round only that set. Rotate the path with `V` and the top handle, then press `A` again. The anchors sit on the rotated artwork. Drag the one you see. The edit lands there. Save with `Ctrl+S`, reopen, and the rotation and the anchors still agree. ## The edge Esc will not cancel Node, and it will not peel the last edit. That key belongs to the pen, where a path is still in progress. In Node, Delete removes selected points. It does not remove the whole object while those points are the selection. Convert with Alt-click is not a delete. Break with Alt-drag is not a delete. The first node edit on a rectangle, ellipse, polygon, or star converts it to a path. Node will not keep the side-count parameter live once you have dragged an anchor. Undo restores the original form. If you need the parameters tomorrow, do not make that edit. Use `V` and Transform. Free transform, `Ctrl+T`, also leaves the parameters in place. Selecting the path does not push an undo step. The entry appears when you change geometry. Alt-drag on a corner-radius handle changes every corner. It does not break only the handle you touched. Use the Shift-selected set when only some corners should move. That pair is the 0.5.8 radius behavior. The Alt-drag that breaks Bézier symmetry is the gesture on a Bézier handle, which is a different handle. Press `A`. Drag the point you see. Alt-drag a handle to break it. Delete removes the selected points and leaves the path on the layer. ## The thread Part 24 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-free-transform) · [Next](/blog/omadesign-0-5-8-break-path) --- # Break path Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-break-path Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, paths ## The habit You draw a rectangle. It has a width, a height, and a corner radius, and you expect those three numbers to stay numbers until you decide otherwise. In Illustrator the rectangle stays a rectangle until Expand, or until the Direct Selection tool grabs one anchor and the object quietly becomes a path. Affinity Designer makes the same trade the moment the Node tool pulls a point. The corner radius you were going to nudge in the Transform panel is gone, and the only record of it is the curve now sitting on the artboard. The second habit shows up after you rotate. The poster needs the card at fifteen degrees. You rotate with the top handle, you like it, and you go back in for one corner. In a lot of files the anchors still live in the box from before the rotation. Sometimes they live in a bounds rectangle that no longer matches the stroke. You drag. The curve leaves the ink you were looking at. You undo, you zoom, you drag a shorter distance. The hand stops trusting the screen, which is a bad place to be ten minutes before a client looks at the file. The workaround most of us learned is to expand early, rotate the anchors, and stop promising anyone a live corner radius. The file gets dumber on purpose. Break path is the decision to stop doing that before you meant to. ## The constraint Omadesign is one binary and one `.oma`. Undo is one step. A rotated path cannot keep a flat outline in one pocket and an angle in another, then hope the editor applies both on the way to the screen. If those two disagree, the drag lands off the ink, and the single undo step has to guess which pocket you meant to change. There is no second process on the machine to rebuild the outline after the fact, and there is no account hop that rewrites the document on the way through. The points you see are the points the file will save. Open that `.oma` tomorrow and the rotation you left is the rotation you edit. A rectangle still has to be a rectangle for as long as you are setting corner radius, width, and height. Forcing every shape into anchors at the moment of creation would throw Transform away to make the node tool look simple. The conversion waits until you ask for points. When you ask, the points have to be the visible artwork, rotation included, because that is the only geometry one undo step can honestly restore. ## What landed Object → Break path converts the selection to a path. You pick the moment. The command sits with the Node tool, because Node is the tool that needs anchors. Shapes also convert the first time you edit them. Until that edit, a rectangle keeps its corner radius, a polygon keeps its side count, and a star keeps its inner radius. Those fields stay in Transform. Ellipses convert to four smooth anchors. Free transform, Ctrl+T, keeps live text and shape parameters editable. Scale and rotation leave the parameters in place. The node edit converts. Once the path is a path, rotation lives in the geometry you are shown. Visible points and Bézier handles sit on the artwork. A point drag, a handle drag, and a segment drag all happen at those displayed locations. A file that was saved already rotated opens the same way. The saved rotation stays intact. Selecting a path does not create an undo step. You can click a rotated mark, read it, and leave. History stays on the last edit that changed something. Undo and redo put the geometry and the rotation back together. Moving one point leaves the other points where they are, even when the bounds of the path change. The box around the art is allowed to grow. The anchors you did not touch do not get a new pose out of courtesy. Node itself is A. Drag points and handles. Shift-click adds a node. A box selects nodes. Drag a segment to move the line. Click a curve to insert a point. Alt-click switches a point between corner and smooth. Alt-drag on a handle breaks symmetry. Delete removes the selected points. Shift on a handle holds it to 45 degrees. Break path is how you get a shape onto that tool on purpose. Flip follows the same visible-axis rule, and it is worth knowing where the two meet. A dashed rectangle or ellipse becomes a path so the dash can mirror, and undo restores that shape's parameters. Live text is a separate, explicit Object → Convert to path. Break path is the path command. It is aimed at points, on the art, where the stroke already is. ## In the hand Press R and drag a rectangle. Set a corner radius in Transform if the card needs one. Press V, grab the handle above the box, and rotate until the rectangle sits in the poster. The ink is turned. Transform still has a rectangle. Press A and look before you drag. The points belong on the corners you can see. Choose Object → Break path if you want that conversion as its own step, then drag a corner. Drag a point first and that drag is the conversion. Either way you are on the path. The corner you pull moves. The other corners stay. Pull a handle and the curve follows the handle under the cursor. Click a segment and the line moves as a piece. Alt-click a point when a smooth join needs to become a corner. Press Ctrl+Z. Geometry and rotation return together. Ctrl+Shift+Z or Ctrl+Y puts the edit back. Save. Open the `.oma` again. The points are still on the ink. Clicking the path on the way in adds nothing to the undo stack. You are where you left the real work. ``` R Rectangle V Move, then the rotate handle A Node Object → Break path Ctrl+Z Undo the edit Ctrl+Shift+Z or Ctrl+Y Redo ``` Walk a segment after the break. Click the curve between two points and a new point inserts where you clicked. That insert uses the same displayed path, so a rotated card gets the point on the rotated segment. Delete pulls selected points back off. None of this rebuilds a hidden unrotated copy beside the one you are editing. If the shape was a star, check Transform before the break and remember the inner radius. After the break that number is the curve. If you still want the star as a star, leave Node alone and keep editing the parameter. Break path is the door out, and you open it when the poster needs a point the star parameter cannot express. ## The edge Break path spends the parameter shape. After it, you edit points. Corner radius, sides, and inner radius belong to the object you have not converted. The command keeps one object. One conversion, then one undo back to the edit you just made. Looking is free. Selecting the path writes no history, so a rotated logo can be inspected all afternoon. Press A, choose Object → Break path, and drag the point that is already sitting on the ink. ## The thread Part 25 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-node-tool) · [Next](/blog/omadesign-0-5-8-flip-horizontal-vertical) --- # Flip horizontal vertical Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-flip-horizontal-vertical Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, vectors ## The habit Flip is one of the oldest gestures in the vector apps. Illustrator puts Flip Horizontal and Flip Vertical on the Transform panel and on the right-click menu. Affinity puts them on the same menu you use when a logo faces the wrong way. Photoshop flips a layer or a selection from Image or from Free Transform. The hand already knows the job. The mark points left. The layout needs it pointing right. You do not redraw it. The part that goes wrong is the axis. You rotated the wordmark twelve degrees so it would sit on a diagonal rule. Then you flip it. A lot of software mirrors the object in its own local box, or mirrors the unrotated geometry and leaves the angle looking inverted in a way you did not order. Gradients slide off the stroke. A dashed border mirrors in the bounding box and the dashes land on the wrong side of the corner. Uneven corner radii swap in local space and the heavy corner is suddenly on the wrong visual edge. The other failure is type. You flip a live text frame and the letters either refuse, or they turn into something you can no longer retype. You wanted a mirrored outline for a foil stamp. You still wanted the headline editable on the version that is not stamped. Those are two different objects, and the habit has been to duplicate first and hope you remember which copy is still text. ## The constraint One `.oma` holds the poster. One undo step has to put the whole flip back: geometry, the angle you already applied, a linear gradient, a dashed stroke, the individual corner radii on a rectangle. If the flip rewrote a hidden unrotated copy and left the on-screen angle alone, undo would restore the wrong picture. The axes are the canvas axes you can see. Horizontal means left and right on the page. Vertical means top and bottom on the page. A rotated path does not get a private horizon. The same rule that keeps node handles on the visible artwork keeps the mirror on the visible artwork. Otherwise the file contains two orientations and the screen has to pick one. Live text stays text until you say otherwise. A flip that silently converted every headline into outlines would destroy the thing the Type tool is for, and the single undo step would be the only way back to the words. The conversion is a separate command, with its own undo, so a mirror of letterforms is a decision you can see yourself make. Locked objects and hidden objects are out of reach. A flip that grabbed them anyway would change art you had already taken off the table. The command has to leave them alone. ## What landed Right-click the artwork, or right-click its object row, and choose Flip horizontal or Flip vertical. Flip horizontal is left and right. Flip vertical is top and bottom. The same two commands live under Object and in the inspector. Arrange, Align, Flip H, and Flip V sit in that right-hand inspector, so you can run the mirror without leaving the selection you already have. The flip follows the visible canvas axes after rotation. A card at fifteen degrees mirrors across the page, and the fifteen degrees stay part of the result you see. Linear gradients mirror with the art. Dashed strokes mirror. Individual rectangle corners mirror, so the heavy corner stays on the visual corner you intended after the flip. Dashed rectangles and dashed ellipses become paths. A dash pattern is a placement along the outline, and a parameter shape cannot always mirror that placement and still be the same parameter shape. The conversion is there so the dashes land on the mirrored outline. Undo restores the original shape parameters. You get the rectangle back, dash and all, in one step. Right-click behavior follows the selection you actually have. Right-click another object and that object is the target. Right-click a member that is already inside the selection and the whole selection flips. You do not lose a multi-selection because you aimed at one of its pieces. Locked and hidden objects are left alone. Live text does not flip in place. Choose Object → Convert to path first. That conversion keeps the letter outlines and the holes inside letters such as a, e, o, and 8. It replaces editable text. Undo restores the text. After the outlines exist, Flip horizontal or Flip vertical mirrors them like any other path. The holes stay holes. Undo of the flip itself restores the artwork. You do not rebuild the gradient by hand. ## In the hand Select the wordmark with V. If it is still live text and you need a mirrored outline, choose Object → Convert to path before anything else. Look at a counter, the inside of an o, and confirm the hole is still open. Press Ctrl+Z if you converted too early. The words come back. With the path selected, right-click it on the canvas. Choose Flip horizontal. The mark faces the other way across the page. If the path was rotated, the tilt is still the tilt you see, mirrored on the canvas axis. Open the inspector and hit Flip V if the job was top to bottom. Or use the Object menu. The three entrances run the same flip. Now a dashed rounded rectangle. Rotate it first. Right-click the object row in the layer list, not the canvas, and choose Flip horizontal. The dashes move to the mirrored side. The corner weights follow. Press Ctrl+Z. The rectangle's parameters come back, including the dash, because undo restores that shape. Try the selection rule. Shift-click two icons so both are selected. Right-click one of them. Both flip. Click empty canvas, select a third icon alone, right-click that one. The pair you flipped stays where you left it. ``` V Select Object → Convert to path Live text, before a flip Right-click → Flip horizontal Left / right on the canvas Right-click → Flip vertical Top / bottom on the canvas Ctrl+Z Restore ``` A linear gradient on the rotated card flips with the card. You should see the light end and the dark end trade places across the page axis, staying on the object. If that is wrong for the poster, Ctrl+Z is the whole gradient as well as the geometry. ## The edge Flip refuses live text. The letters stay editable, and the command will not turn them into outlines as a side effect of a mirror. Object → Convert to path is the door. It keeps counters, it removes editing, and undo gives the text back. After that, flip works on the outlines. Locked and hidden objects stay out. A flip aimed near them does not drag them into the mirror. Right-click the artwork or the object row, choose Flip horizontal or Flip vertical, and press Ctrl+Z if the axis was the wrong one. ## The thread Part 26 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-break-path) · [Next](/blog/omadesign-0-5-8-pen-tool) --- # Pen tool Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-pen-tool Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, pen ## The habit The pen is the tool you judge a drawing program by. Illustrator's Pen is P. Affinity's is P. You click a corner. You click and drag a smooth point, and two handles come out. You hold Shift when the next segment has to sit on a horizontal, a vertical, or a diagonal. You hold Alt when one handle has to break away from its twin so the curve can change direction without a cusp you did not want, or with a cusp you did. You also know the small humiliations. A nervous click, two pixels of movement, and the tool lays down a smooth point with tiny handles. You spend the next minute deleting them. Esc nukes the entire path when you only wanted the last point back. Closing the path requires a ritual hover over the first point, and continuing an open path means hunting a command named Join after you have already left the tool. The cubic has to appear while you draw. If the curve shows up only after you release the path, you are drawing blind and correcting from memory. Anyone who has cut a logotype out of a scan knows that delay in their wrist. ## The constraint The pen writes a path into the same `.oma` as the rectangles, the type, and the artboard. There is no sketch file beside the poster and no ink layer that has to be traced later by a different persona. A point you place is a point Node can edit, and a point you undo is one step. The gesture language has to match Node, because you switch between them every few seconds. Click, click-drag, Alt-drag, and Shift cannot mean one thing while the pen is down and another thing once the path exists. A designer who learns two dialects for the same handles will hit the wrong one under a deadline. The manual lists them in one table so the hand can memorize a single row. Shift is already the constraint key for moves, for pencil strokes, and for artboard drags: horizontal, vertical, or 45 degrees. The pen uses that same key for points and handles. A special pen-only modifier would mean the left hand changes jobs every time the tool changes. One binary, one document, one modifier for "make this straight." A twitch has to stay a corner. The threshold is a distance, 3 pixels, because a click is never perfectly still on a real mouse or a real trackpad. If every tremble became a curve, the corner tool would be a rumor. ## What landed Press P. Click to place a corner. Click and drag to place a smooth point. If the drag moves less than 3 pixels, the point stays a corner. That twitch rule is the whole difference between a clean polygon and a path covered in accidental handles. Shift constrains the pen to 45 degrees. That covers the segment you are placing and the handles you are dragging. Alt-drag breaks handle symmetry, on the pen and later on the node. The cubic is drawn as you go, so the curve on screen is the curve in the path. Enter finishes an open path. Double-click finishes an open path. You use this when the stroke is a line that should not close: a tick, a divider, an arrow shaft. Esc removes the last point. Esc again cancels. The first press is an undo of the point. The path you still want stays on the canvas. Click the first point to close. The path becomes a closed shape you can fill. Click an open endpoint to continue that path, or to join it to the path you are drawing. You do not leave the tool, copy both paths, and run a separate join command. The open end is a target. While you place points, snapping can see object edges, artboard edges, centers, guides, the grid, and equal spacing between nearby objects. Alignment lines and gap measurements show up as you move. Pen placement can use those repeated and balanced gaps, and Shift still constrains while it does. Ctrl+Shift+; toggles snapping. Hold Ctrl during the drag to reverse snapping for that drag, then release Ctrl and the previous choice returns. Open paths take a centered stroke. Closed paths can use inside, center, or outside placement, and the stroke width field accepts values above 64 pixels. Those stroke decisions stay in the inspector. The pen does not bake them into anchors. ## In the hand Press P. Click once where the mark should start. That is a corner. Move to the right, hold Shift, and click again. The segment locks to the horizontal. Release Shift. Click and drag at the shoulder of the curve. Handles appear, and the cubic bends while the mouse button is down. If the drag was a shiver and the point came out as a corner, that is the 3 pixel rule working. Click-drag again with a real pull. At the next change of direction, place the point, then Alt-drag one handle away from the other. The twin stays. The curve can come into the point from one angle and leave on another. Wrong point? Press Esc once. That point is gone. The earlier points remain. Press Esc again only when you mean to drop the in-progress path. To leave the path open, press Enter, or double-click. To close it, bring the pen back to the first point and click it. To extend a path you finished earlier, press P and click the open end, then keep drawing. Click the open end of a second path if the job is to join them. ``` P Pen Click Corner Click-drag Smooth point Drag under 3px Stays a corner Shift 45° Alt-drag Break the handles Enter Finish open Esc Drop the last point, then cancel Click first point Close ``` Switch to A when the path is done and you need to move a point you already placed. The same Alt-drag breaks a handle. The same Shift holds a handle at 45 degrees. You are editing the path the pen just wrote. Ctrl+Z takes the last pen step back. Because undo is one step, you can walk a bad curve off the page point by point without a history branch named "pen session." ## The edge A drag under 3 pixels refuses to become a curve. The point stays a corner. If you wanted handles, you drag them on purpose, far enough that the tool can tell. Esc refuses to throw away the whole path on the first press. It removes the last point. The next Esc cancels. You always get one chance to keep the work that was already good. Press P, click the corner, and drag the smooth point far enough that the handles are the ones you meant. ## The thread Part 27 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-flip-horizontal-vertical) · [Next](/blog/omadesign-0-5-8-artboard-tool) --- # Artboard tool Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-artboard-tool Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, artboards ## The habit The artboard is the page. In Illustrator you press Shift+O, you draw a board, you drag it, you pull a handle, and you rename it in the panel when "Artboard 3" stops meaning anything. Affinity uses a similar board for each exportable page. Photoshop's artboards showed up later and never quite felt like pages, but the move-and-rename habit is the same. You keep a poster, a story crop, and a square crop side by side, and you copy art from one to another. You also keep a second kind of box in your head. Figma and Affinity's layout tools, and Illustrator's artboards, get blurred together until a frame that stacks buttons is treated like a page, or a page starts growing padding and auto-layout by accident. The hand wants one key for "this is the sheet of paper" and a different key for "this is a UI frame." Cloning a board is Alt-drag in the apps you already know. You duplicate the page, slide it to the right, and start the next size. If that gesture moved the original instead of copying it, you would stop using it. Rotation of the board itself is rarer, and when you need it you need the same top handle you use on a shape. A separate rotate dialog for pages is how boards end up almost-aligned. ## The constraint Design and Layout share one `.oma`. The artboard is the Design page. The Layout frame is a different object, made with F, for screens that nest, stack, and export as a frame. If the artboard tool also became a stack container, every poster would grow UI structure it did not ask for, and every screen mock would be one accidental Shift+O away from turning into a bare page. One document can hold both. The tools stay distinct so the file stays readable. Undo is one step. Moving a board that has artwork on it has to bring the artwork back with the board. An undo that restored the page and left the logo where the page used to be would split one gesture into two repairs. You would not trust the next drag. The key cannot be O. O is the ellipse. The artboard is Shift+O, the same chord Illustrator trained into your left hand. A new chord would be a tax on the first hour. Shift is already the constraint key while you drag a board: horizontal, vertical, or 45 degrees. The clone chord is Alt-drag, the same chord that clones a path. Add Shift and a constrained copy is the same idea you use on objects. Motion can animate what you drew on that board. It does not rewrite the board. The artboard is the rest pose. Tracks for position, rotation, scale, opacity, and reveals hang off the artwork. The page you drew stays the page you export as a still PNG, JPEG, or SVG. ## What landed Press Shift+O. Drag on the canvas to draw a new artboard. Drag the board to move it. The handles scale it. The handle above the board rotates it, the same top handle Move uses on a shape. Alt-drag clones the board. Hold Shift while you move a board and the move stays on horizontal, vertical, or 45 degrees. Object → Wrap selection in artboard builds a board around the artwork you already selected. You use this when the drawing came first and the page should fit it. Click the name in Transform to rename the board. The name is there, in the inspector you already have open, so you are not hunting a separate artboard panel for a label. A move of a selection, or of an artboard with its contents, comes back together on undo. Duplicate work follows the same single-step rule when you are copying several objects. The board and the things on it are one gesture. Paste knows about more than one board. Objects copied inside Omadesign paste at their original positions, including when you paste onto another artboard. The status bar says so. You line up a poster and a crop by pasting, then you move the copy. You do not get a mystery offset because the destination board sits further right on the canvas. The artboard is also a snap target. Snapping uses object edges and centers, artboard edges and centers, guides, the grid, and equal spacing. Alignment lines and gap labels appear while you drag. Ctrl+Shift+; toggles snapping. Hold Ctrl during the drag to reverse that choice for the length of the drag. Shift+O is easy to miss if your finger is already on O for an ellipse. Watch the first drag. A filled oval means the Shift did not register. Undo and press the chord again. ## In the hand Start a poster. Press Shift+O and drag the board at the size you are actually printing. Click the name in Transform and type the name you will still understand in a month. "Poster" is enough. "Artboard 1" will not be. Draw the mark inside it with the usual tools. When you need a second size, press Shift+O, hold Alt, and drag the board. You get a clone. Hold Shift as well if the clone should stay on the same horizontal line. Move artwork onto the new board, or copy it and paste. The paste lands at the original position, so if the boards share a coordinate idea, the art lands in the matching spot. The status bar confirms the paste. If the drawing already exists and the board should wrap it, select the artwork with V and choose Object → Wrap selection in artboard. Rename in Transform. Rotate a board only when the sheet itself is the rotated thing. Grab the top handle. The artwork rotation you do with V on a single path is a different handle on a different object. Do not rotate the page to tilt a headline. ``` Shift+O Artboard Drag Draw Drag inside the board Move Handles Scale Top handle Rotate Alt-drag Clone Shift Constrain the move to H / V / 45° Object → Wrap selection in artboard Board around the selection Transform name Rename Ctrl+Z Board and contents together ``` Open Motion later if the poster needs a move. The board you drew is the rest pose. Space plays the clip in that persona. The page underneath the keys is still the page. Fit the view to the board when you are lost: with the Zoom tool, Z, Ctrl-click fits the artboard. Ctrl+0 is Fit from anywhere the shortcut is live. That is the camera. The board tool is the page. ## The edge The artboard tool refuses to become a Layout frame. Wrap selection in artboard makes a page. Frames, stacking children, constraints, and File → Export frame PNG, SVG, or HTML belong to the Frame tool, F. You can keep a screen and a poster in one `.oma`. You pick which box you are drawing by the key you hold. Motion refuses to rewrite the board. Animation is tracks on the art. The artboard stays the rest pose you drew with Shift+O. Press Shift+O, drag the page, and click its name in Transform to call it something you will remember. ## The thread Part 28 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-pen-tool) · [Next](/blog/omadesign-0-5-8-pencil-freehand) --- # Pencil freehand Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-pencil-freehand Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, drawing ## The habit Sometimes the pen is the wrong kind of careful. You know the silhouette. A quick leaf, a hand-drawn underline, a wiggly rule under a headline, the gesture of a signature that has to sit in the poster as vectors. In Illustrator that is the Pencil, and the key is N. In Affinity Designer it is the Pencil tool, same job, a freehand stroke that becomes a curve. You draw it the way you would draw it on paper, and then you edit the points if a bump is wrong. Photoshop's brush is a different habit wearing a similar icon. The brush lays down pixels. You smudge them, you erase them, you live with resolution. The vector pencil has to end as a path, because the thing you are making is a mark that still has a stroke width, a fill, and anchors. The other habit is the constraint. Halfway through a loose stroke you need one segment to be actually horizontal. In the apps you trust, Shift does that. It is the same Shift you hold to drag a shape in a straight line, and the same Shift you hold so a pen point sits on a diagonal. If the pencil invented its own "straighten" modifier, you would forget it, and the stroke would wander on the one part that had to be strict. A separate sketching app is how these strokes usually get orphaned. You draw them somewhere else, you export an SVG, you place the SVG, and the anchors come in dumb. The pencil in the studio you are already using keeps the stroke in the poster. ## The constraint Omadesign has one canvas for the drawing. Pencil is a Design tool, key N, next to Pen P in the same strip. The curve it writes is a path in the `.oma`. Node can take it. The stroke inspector can take it. Expand stroke can turn it into fills later if a vendor needs outlines. None of that requires a second file or a second program. Undo is one step, so a stroke you hate is Ctrl+Z, not a trip into a history panel with a pencil session collapsed into one opaque blob you cannot partially keep. You draw, you look, you undo, you draw again. Shift has one meaning across the drawing tools. The manual states it in a single sentence: hold Shift to constrain pen points and handles, pencil and brush strokes, and object or artboard movement to horizontal, vertical, or 45 degrees. The pencil does not get a private dialect. Your left hand stays on Shift whether the tool is N, P, or V. That constraint is the whole precision model. There is no smoothing dialog documented on the pencil, and there is no second "sketch persona" with a looser grid. The stroke is as exact as the Shift key you did or did not hold. People who want the pencil to guess a cleaner curve are asking for a second document living inside the stroke. This studio keeps the curve you dragged. Brush strokes have an extra rule the pencil does not borrow. During a brush stroke, pressing Shift anchors the constraint at the last free point. That sentence is about the brush. The pencil's documented deal is the shared one: horizontal, vertical, or 45 degrees. ## What landed Press N. Drag. You get a freehand curve. It lives on the canvas with everything else you drew. Hold Shift while you drag and the stroke constrains to horizontal, vertical, or 45 degrees, the same three directions Pen and Move already use. The curve is path data. Switch to Node, A, and the points and handles are there to drag, insert, delete, and convert between corner and smooth. Switch to Move, V, and the curve scales and rotates as an object, with the top handle for rotation and the eight handles for scale. Alt-drag clones it, and Shift with that Alt-drag keeps the copy on a constrained line. Those are the object gestures, and the pencil's output is an object. Stroke settings apply. Open paths use a centered stroke. You can set a width, including a width above 64 pixels. A freehand curve is usually open. If you need it closed, the pen can continue an open end: press P and click the endpoint. That join is a pen feature, and it works because the pencil left a real path with real ends. Snapping still applies while you work. Object and artboard edges and centers, guides, the grid, and equal spacing are the targets. Alignment lines and gap measurements appear as you move. Ctrl+Shift+; toggles snapping. Hold Ctrl during a drag to reverse snapping until you release it. A loose pencil line can still land on a guide when you want it to, and it can ignore the guide when Ctrl says so. Brush is a different tool. B paints pixels on a pixel layer, with size on `[` and `]`. Pencil draws a curve. A vector-only document still takes N. ## In the hand Press N. Put the cursor where the underline should start. Drag the shape of the stroke in one motion. Let go. Look at it at the zoom you will actually present. If it is wrong, Ctrl+Z and drag again. The undo is the stroke. When the last third of the gesture has to be flat, hold Shift for that part of the drag. The stroke locks to horizontal, vertical, or 45 degrees under the constraint. Release Shift when you want the line to wander again, and keep drawing if you are still holding the button. The directions available under Shift are only those three. A 10 degree rise is a stroke you draw without Shift. Press A. Clean up one bump. Drag the point. If a handle is fighting you, Alt-drag to break symmetry, or Alt-click the point to switch corner and smooth. Delete a point that the freehand gesture doubled up. You are in the node tool now. The pencil's job ended when you released the mouse. Give it a stroke color. X swaps fill and stroke if you are painting the wrong one. D restores the default fill and stroke. Those keys are the color studio's, and they apply to this curve because the curve is ordinary artwork. Duplicate a stroke you like with the duplicate shortcut, Super+D. Nudge the copy. A family of hand-drawn rules should be copies of a good one, edited, not six unrelated drags you then try to match by eye. ``` N Pencil Drag Freehand curve Shift Horizontal, vertical, or 45° Ctrl+Z Remove the stroke A Edit the points P Continue an open end if you need to ``` Save the `.oma`. The curve is in the project with the type and the rectangles. You do not export a sketch and place it back into the poster to keep it. ## The edge Shift refuses a free angle. While it is held, the pencil stroke is horizontal, vertical, or 45 degrees. That is the same rule as the pen and the move tool. The pencil will not keep a gentle diagonal and also call it constrained. The stroke is the curve you dragged. You edit it with A. Nothing beside it gets generated and silently substituted. Press N, drag the mark, and hold Shift on the part that has to be straight. ## The thread Part 29 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-artboard-tool) · [Next](/blog/omadesign-0-5-8-shape-tools) --- # Shape tools Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-shape-tools Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, shapes ## The habit Most of a poster is a rectangle behind the type, an ellipse behind a portrait crop, a polygon used as a badge, a star used once, and a line used as a rule. In Illustrator and Affinity you hit a shape key, you drag, you hold Shift for a square or a circle, and you set the corner radius after, as a number, because dragging a tiny corner widget is a bad way to hit a spec. You also know the moment the shape dies. You needed one corner different from the other three, or you needed to pull a single point on a polygon, and the object became anchors. That is fine when you meant it. It is a mess when the tool converted on the way in and the side count is now a pile of points you have to count by hand. Lines are the sleeper. A line is one drag, with Shift when it has to be straight. Drawing from the center is the other hand. Alt while you drag, in Illustrator and in Affinity, grows the shape around the point you started. You use it when the shape has to sit on a guide intersection or on the middle of a photo. Shift and Alt together are the whole sentence: from the center, equal proportions. ## The constraint These five tools write parameter shapes into the `.oma`. The parameters are the editable fact. A rectangle remembers its corner radius. A polygon remembers how many sides it has. A star remembers its inner radius. Those numbers live in Transform so you can change them after the drag, with the keyboard, at the size the poster actually needs. If the drag expanded them into anchors immediately, Transform would be a readout of a path you can no longer drive with a number, and every "make it 12 sides" would be a redraw. The conversion to points waits. Shapes become a path the first time you edit them with the node tool, or when you choose Object → Break path. Until then, scale and rotation through Move, including Free transform with Ctrl+T, keep the parameters. You can rotate a live rounded rectangle and still edit the radius. That only works if rotation and the shape parameters are allowed to coexist in one object. They are. Shift is the same constraint you use everywhere else: horizontal, vertical, or 45 degrees, and for these tools it is also the equal-proportion hold. Alt draws from the center. The manual states both in one place, next to the rest of the modifier language, so a shape drag does not invent a third way to say "regular" or "from the middle." One undo step covers the creation. A bad ellipse is Ctrl+Z. You are back to the empty canvas where that drag started. ## What landed Press R for a rectangle, O for an ellipse, Y for a polygon, S for a star, L for a line. Drag to create. Shift constrains the drag. Corner radius, sides, and inner radius live in Transform after the shape exists. You change the number there. The shape updates. Hold Alt to draw from the center. Hold Shift for equal proportions. The two combine. A circle built on the middle of a selection is Alt+Shift and the ellipse tool. A square from the center is the same chord on the rectangle. Corner dots on a rectangle, with Move, round the rectangle as a gesture. The number still ends up as the corner radius you can read. You use the dots when your eye is ahead of your keyboard, and Transform when the radius has to match a spec. Ellipses convert to four smooth anchors when they become paths. Until you need those anchors, an ellipse is an ellipse. Node corner-radius handles show up with the Node tool, including on paths, once you are in points. While the object is still a parameter shape, you stay in Transform. Polygons and stars keep their counts and radii as parameters. A badge that has to go from six sides to eight is a change in Transform, not a reconstruction. A star that is too sharp gets a larger inner radius the same way. Lines are L. Drag. Shift keeps the line on the allowed angles. A line takes a stroke. Open paths use a centered stroke, and a line is open. Widths above 64 pixels are legal in the stroke field. Inside, center, and outside placement apply to closed paths. A heavy rule is a wide stroke on that line. S is the star, and also the last letter of the save chord. Star is S alone. Save is Ctrl+S. If a save dialog appears, you were holding Ctrl. O is the ellipse. The artboard is Shift+O. If you get a board when you wanted a circle, the Shift was down. Move is V. Click the shape, drag it, scale from the eight handles, rotate from the handle above the box. Shift constrains the move. Alt-drag clones. Shift-click adds or removes an object from the selection. The shape tools create. Move arranges. The parameters survive that arrangement. ## In the hand Press R. Hold Shift. Drag a square behind the headline. Release. Click the corner radius in Transform and type the radius the grid wants. Press V and move the square into place. Hold Shift if the move has to stay horizontal. Press O. Hold Alt and Shift together. Drag from the center of a portrait until the circle crops the way you want. The ellipse stays an ellipse. You can still change it after the drag because you have not touched Node. Press Y. Drag a polygon. Set the side count in Transform. Press S if the badge should be a star instead, drag, and set the inner radius in Transform. Keep both if you are comparing. Delete the one you do not want. Each create was one undo, so you can also Ctrl+Z the star away and be back on the polygon. Press L. Hold Shift. Drag a horizontal rule under the deck. Set the stroke width. Leave the fill alone or clear it. X swaps fill and stroke if the color landed on the wrong chip. D restores defaults. When one corner of the rectangle has to differ, press A, or choose Object → Break path, and accept that you are leaving the parameter. Do that last, after the shared radius is right. ``` R Rectangle O Ellipse Y Polygon S Star L Line Shift Constrain, equal proportions Alt Draw from the center Transform Corner radius, sides, inner radius Ctrl+T Free transform, parameters stay A Nodes, and the shape converts on the edit ``` Ctrl+S saves the `.oma` when the shapes are right. The parameters save with the objects. Open the file next week and the corner radius is still a number. ## The edge The shape tools refuse to expand on the way in. You get a parameter shape. Corner radius, sides, and inner radius stay in Transform. Nodes arrive when you edit with A, or when you choose Object → Break path. Free transform will scale and rotate without taking that choice away. Alt draws from the center. Shift keeps equal proportions. A drag with neither is a free rectangle, ellipse, polygon, star, or line from the corner you started. Press R, hold Shift, and set the corner radius in Transform while it is still a number. ## The thread Part 30 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-pencil-freehand) · [Next](/blog/omadesign-0-5-8-corner-radius-multi-edit-0-5-8) --- # Corner radius multi-edit 0.5.8 Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-corner-radius-multi-edit-0-5-8 Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, corners ## The habit A rounded rectangle is easy. One radius, four corners, a number in Transform. The trouble starts when the corners are points on a path. You outlined a ticket shape. You broke a rectangle so two corners could differ. You traced a badge and now eight corners need the same bite taken out of them. In Illustrator you select the anchors with the Direct Selection tool and drag one Live Corner widget, and if you selected the right anchors they share the radius. If you selected one, you get one. Affinity Designer has corner tools too, and the slow version of the job is the one everyone remembers: click a corner, drag, click the next corner, drag, click the next. A symmetric mark takes a minute of repeated gestures and still comes out a pixel off. You also do this after rotation. The ticket is tilted. The corner widget has to sit on the corner you see, or you drag a handle that belongs to a corner on the other side of the shape. Undo becomes the real tool. You drag, you hate it, you undo, you try the next corner. The job is two gestures, used on purpose. All of the corners. Or this set of corners. Anything that makes you visit them one by one is the habit this release is finished with. ## The constraint Corner radius on a live rectangle already lives in Transform. That number changes every corner because the object only has one radius. The moment the artwork is a path, each corner can differ, and a single Transform field would lie. Omadesign keeps the path's corners as corners you can see with the Node tool. The handles are on the path, including a path that is already rotated, because a rotated path keeps its visible points on the artwork. Undo is one step. A drag that changes four radii has to come back as one drag. Four separate history entries would mean four undos to get the ticket back, and you would stop using the gesture. Alt-drag is that one step for every corner. A selected set is that one step for the corners you named. 0.5.8 is where this landed. The release checks rotated paths, a selected set of radii, a change to all of the radii, and undo and redo. The file is still one `.oma`. The handles are still the Node tool. Nothing about the corner edit leaves the document or asks another machine to round the path for you. A rectangle you have not converted still uses its parameter. Corner dots with Move round a rectangle. Transform still holds the radius. The multi-edit is for the Node handles on a path, which is where "these three, not the fourth" becomes a real sentence. ## What landed Press A. Node corner-radius handles appear with the Node tool, including on paths. You are looking at the corners of the geometry in front of you. Alt-drag a corner-radius handle. Every corner on that path takes the radius you are dragging. One handle is the input. The whole path is the result. You use this when a badge, a ticket, or a broken rectangle should share one radius again after the corners drifted. Shift-select several corners. Then drag one selected corner's radius handle. The selected set changes together. Corners you left unselected keep the radius they had. That is how one poster gets two sharp corners and two round ones without a second trip around the path. Both gestures are in 0.5.8. They sit on top of the Node tool you already use to move points, pull handles, insert, and delete. Shift-click in the node tool also adds a node when you are editing points. When you are on corner-radius handles, Shift-select builds the set of corners the next radius drag will share. Watch which handle you grabbed. A point move and a radius drag are different handles on the same tool. The edit follows a rotated path. The handle you pull is the handle on the corner you see. Undo and redo put the radii back with Ctrl+Z and Ctrl+Shift+Z or Ctrl+Y. You do not reconstruct the previous radii from memory. A path that came from a rectangle, an ellipse's four smooth anchors, or a run of Boolean contours can carry these handles once you are in Node. Compound contours keep their radii editable after repeated Boolean work. If you can see the corner handle, the radius drag applies to the rule above: Alt for every corner on the path, or the selected set if you built one. ## In the hand Draw a rectangle with R. Set a small radius in Transform so you can see the corners. Press A, or choose Object → Break path if you want the path before you touch a handle. Press A either way. The radius handles show up on the corners. Hold Alt and drag one corner's radius handle. All four corners move together. Release. Press Ctrl+Z. You are back to the radii from before the Alt-drag. Shift-select the two corners that should match, the top-left and the bottom-right if that is the ticket. Drag the radius handle on one of those selected corners. Those two update. The other two stay. If a third corner jumped, it was in the selection. Undo, Shift-click it off the set, and drag again. Rotate the path with V and the top handle. Press A again. The handles sit on the rotated corners. Alt-drag one of them. The radii change on the artwork you see, and Ctrl+Z restores them. ``` A Node, radius handles visible Alt-drag Every corner on the path Shift-select The corners that share the next drag Drag One selected corner's radius handle moves that set Ctrl+Z Put the radii back ``` Save the `.oma` when the ticket is right. The radii are on the path. Open the file later and the corners are still the corners you dragged, on a rotated path if that is how you left it. If the object is still a parameter rectangle and every corner should match, stay in Transform and type the number. Alt-drag is the path version of that same idea, for artwork that has already become points. ## The edge Alt-drag refuses to leave a corner behind. Every corner on the path takes the drag. If one corner has to stay sharp, leave Alt up. A selected set refuses to recruit the corners you did not Shift-select. Drag one selected corner's radius handle and the unselected corners keep their radius. The set is the selection you built before the drag. Press A, hold Alt, and drag one corner-radius handle when the whole path should match. ## The thread Part 31 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-shape-tools) · [Next](/blog/omadesign-0-5-8-type-tool) --- # Type tool Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-type-tool Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, type ## The habit You press T. In Illustrator, in Affinity, in Photoshop's type tool, the next click puts a text cursor on the page. You type the headline. Enter means a new line when you are in point type. Escape, or a click on empty canvas, means you are done editing and the letters are an object again. Double-click means you were wrong about being done. The placeholder is the part that wastes a minute. Some tools drop lorem, or the word "Text", or a sample of the font name, and your first characters append to it. You then select all and delete before you can write the real headline. The designers who got burned by that now hit Cmd+A or Ctrl+A as a reflex on every new text object. The reflex exists because the tool failed the first keystroke. The other habit is the character panel. Font, size, tracking, leading. Then the OpenType row, once the headline is the right words: kerning when a pair collides, ligatures when the wordmark wants them, tabular figures when a price has to line up, small caps when the deck is shouting in a smaller voice. You expect that panel next to the type, in the same inspection column as the fill. Paste is a split habit. If you are inside the text, paste should insert characters. If you are not, paste of copied words should become a text object. A tool that always makes a new object while you are editing will drop a second headline on top of the caret. A tool that always inserts will swallow a pasted sentence into the middle of a word when you thought you were placing a new layer. ## The constraint Type is an object in the `.oma`, in the same document as the paths. It has to stay editable. A studio that outlined every headline on the way in would make tracking impossible and would make "change the date" a redraw. Convert to path exists, and it is explicit, because flip and some reshapes need outlines. The Type tool's own life cycle keeps the words. Undo is one step. The first keystroke, a line break, a finished edit, a pasted sentence: each change you commit has to be walkable with Ctrl+Z. A placeholder that survives into the real string would mean your first undo removes the last letter of the headline and leaves the word Type glued to the front. Replacing the placeholder on the first keystroke keeps that history honest. There is no area-text ritual in this tool's description. You click to place. You type on the canvas. Enter is how a line happens. A drag-out text frame with auto wrap is a different product decision, and this one does not pretend to be that frame. Posters and marks are mostly point type. The key is T. The click is the origin. Five personas share the document. Design is where you draw the mark and the poster, and T is one of the four tools Design puts in your hand first, with Move, Pen, and Rectangle. Layout uses T as well, on frames. The text object is still text. You do not export to a second app to change a word. ## What landed Press T. Click the canvas. A text object appears with the placeholder Type. The first keystroke replaces that word. You are writing the headline immediately. There is nothing to select-all away. Enter inserts a new line. Esc finishes the edit. A click away finishes the edit. The object stays on the canvas as type. Double-click it when you need the caret back. That is the whole loop. While the caret is up, the usual typing happens in the object. Paste inserts into that text. You can drop a sentence from another program into the headline you are already editing. If you are not editing text, Ctrl+V of plain text from outside Omadesign creates an editable text layer. External content lands in the center of the visible canvas. Images in that same paste become pixel layers, and SVG becomes vectors. Words become type. The status of the caret decides which of those two text results you get. Character studio sits with the selection. Font, size, tracking, leading, and OpenType. The OpenType row covers kerning, ligatures, tabular figures, and small caps. You set them on the text you just typed. The font picker also lists Project fonts when a brand kit is loaded, so a face that lives in the project shows up here. The kit itself is Brand → Typography. The type tool is where the face gets used. Free transform, Ctrl+T, keeps live text editable. You can scale and rotate the headline and still double-click to change a word. Move, V, drags it, scales from the eight handles, and rotates from the top handle. Shift constrains the move. The letters stay letters through those gestures. Object → Convert to path is the exit. It keeps letter outlines and the holes inside them, and it replaces editable text. Undo restores the text. Flip asks you to take that exit first. Reshape converts on the first handle you actually move, and undo restores the original form. Until you take one of those exits, T still edits the words. Project-font text stays editable in the `.oma` after the project moves, including new characters. SVG export of those faces draws outlines so the picture survives on a machine that does not have the kit. The source file keeps the text. That split is the kit's export rule. The type tool's rule is simpler: what you see on the canvas is still editable type. ## In the hand Press T. Click under the poster title. The word Type is sitting there. Type the real title. The placeholder is gone on the first character. Press Enter for the second line. Press Esc. The caret leaves. The object remains. Press V if you need to move it. Drag. Hold Shift for a horizontal move. Double-click to fix a typo. Esc again when the word is right. Open Character studio. Set the font and the size. Add tracking if the headline is loose. Set leading if the two lines are colliding or drifting apart. Turn on tabular figures if the line is a price. Turn on ligatures if the wordmark has an fi or a fl that should be one drawing. Small caps for a short label. Kerning for a pair that clashes. Those four OpenType controls are the row. Use the ones the typeface actually contains. Copy a sentence from a brief. Double-click the text object. Put the caret where the sentence belongs. Ctrl+V. The words insert. Click away first if you wanted a new text object from that paste, then Ctrl+V. The new layer appears at the center of the view. Move it with V. ``` T Type Click Place, placeholder "Type" First key Replaces the placeholder Enter New line Esc Finish Click away Finish Double-click Edit again Ctrl+V Insert if the caret is up ``` Ctrl+Z walks the last change off. A bad line break, a bad paste, a bad size. One step each time you committed one. Save with Ctrl+S. The words are in the `.oma`. Tomorrow you double-click and keep typing. ## The edge The first keystroke refuses to append. The placeholder Type is replaced. You start the headline clean. Paste refuses to guess. A caret inside text inserts. A paste while you are not editing text creates an editable text layer at the center of the visible canvas. Click away, or press Esc, before you paste if the words should be their own object. Press T, click, and type the first letter of the real headline. ## The thread Part 32 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-corner-radius-multi-edit-0-5-8) · [Next](/blog/omadesign-0-5-8-opentype-character-studio) --- # OpenType character studio Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-opentype-character-studio Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, type ## The habit OpenType is the row you open after the words are right. In Illustrator it lives in the Character panel and the OpenType panel: kerning for a pair, standard ligatures for fi and fl, tabular figures so a column of prices lines up, small caps so a label stops looking like it was set in a shout. Affinity puts the same switches on its typography studio. Photoshop has a shorter list and you still go looking for it the moment a headline feels cheap. The font menu is the other habit, and it is where jobs go to die. The brand face is installed on your machine. It is not installed on the laptop that opens the file tomorrow. Illustrator substitutes. Affinity substitutes. You spend the morning reinstalling a license you already had, or outlining the text so the picture holds and the words die. A serious file carries the faces it uses. The system font menu is the wrong cupboard for a client kit. You also name roles. Heading, body, caption. You apply the role, you do not scroll a five-hundred-font menu looking for "Söhne Halbfett" spelled three different ways. The role is the decision. The file is the storage. ## The constraint The `.oma` has to move. Copy the project folder to another Linux machine and the headline has to open as text, with the same face, and you have to be able to type new characters. That only works if the font file travels inside the project. Omadesign copies TTF and OTF files into the project and lists them as Project fonts. It does not install them on the computer. A system install would spread client faces into every other app and would ask for a privilege the drawing tool should not have. One binary. Fonts stay in the folder you already share. The kit has a shape. `.omatype` names the roles. `.omabrand/fonts/` holds the files. Paths inside `.omatype` are relative to `.omabrand/`. No absolute path, so the folder still works when you move it. Saving artwork into another folder copies the faces that artwork uses into that folder's `.omabrand/fonts/`. The text stays editable after the move. SVG export has a different job. The person who opens an SVG may not have the kit. Project-font text exports as vector outlines so the picture holds. The `.oma` keeps the editable text. You share the SVG as a picture and the project as the source. You also share fonts only under their license. The tool will copy a file you added. The license is still yours to respect. Character studio is where the face meets the letters. Font, size, tracking, and leading are the measurements. Kerning, ligatures, tabular figures, and small caps are the OpenType decisions the face is able to honor. They have to sit on the text object in the same inspector, because a separate typography app would be the Creative Cloud hop this document exists to avoid. ## What landed Select type. Character studio offers the OpenType row: kerning, ligatures, tabular figures, small caps. Kerning adjusts a pair. Ligatures substitute the combined drawing where the font has one. Tabular figures use the figure spacing that lines up in columns, when the font shipped those figures. Small caps use the small-cap glyphs, when the font shipped them. You turn on what the face contains. The row is those four. The font picker in that same Character panel lists Project fonts beside the faces the machine already has. A kit font shows up there without a system install. Build the kit from Brand → Typography. Add fonts… copies TTF or OTF files into the project. The originals stay where they were. Name the kit and click Save name. Select a font row, name the role Heading or Body or Caption or whatever you will actually say out loud, and click Save role. Either save button keeps pending name and role edits together. Filter the list by role, family, or filename when the kit gets long. Click Apply beside a role. It hits the selected text, or it becomes the face for the next text you create with T. Applying a font to artwork supports undo. The kit's names and files have their own save controls, separate from Ctrl+S on the poster. You can change a role name without that change being the same undo as a moved rectangle. Removing a role keeps the font file for artwork that already uses it. The headline does not go blank because you cleaned a label. Click Apply again if a face changed on disk and existing text should adopt the new file. Existing text otherwise keeps the face it had. External font changes refresh in the background. If the kit changes while you are editing a name, the panel keeps your draft and reports the conflict. Typography → ··· → Reload typography discards the draft and loads the saved kit. Load kit… merges another `.omatype` and copies its font files. Keep that kit's `.omabrand/` beside the source file when you do. Save copy… writes the saved kit and its fonts into another project folder. Save pending name edits first. The font picker is the part you touch while setting type. The Brand tab is the part you touch while building the cupboard. Both see the same project fonts. ## In the hand Put the client files on disk. Open Brand → Typography. Add fonts… and choose the TTF or OTF files. Name the kit after the client. Save name. Click the display face, call the role Heading, Save role. Click the text face, call it Body, Save role. Press T, click, type the headline. In Character studio, open the font picker and choose the Heading face under Project fonts. Or select the headline and click Apply on the Heading row. The letters take the face. Ctrl+Z returns the previous face if you hit the wrong row. Apply is undoable on the artwork. Set the size. Then the OpenType row. Turn ligatures on for a wordmark that has a pairing the font drew on purpose. Turn tabular figures on for a price. Turn small caps on for an eyebrow label. Set kerning where a pair crashes. Tracking and leading are next to those, for the block as a whole. Move the project folder. Open the `.oma` on the other side. Double-click the headline and type another word. The face is still the project face. New characters use it. Export an SVG when someone needs a picture. The project-font text goes out as outlines. Keep the `.oma` if they need to change the words later. ``` Brand → Typography → Add fonts… TTF or OTF into the project Save role Heading, Body, Caption Apply Selected text, or the next text Character studio Font, size, tracking, leading OpenType Kerning, ligatures, tabular figures, small caps ``` `.omatype` and `.omabrand/fonts/` travel with the project. `.omacolors` is the palette file beside them if you are sharing the whole kit. Show hidden files when you copy by hand. The names start with a dot. ## The edge Project fonts refuse the system font directory. They are available inside Omadesign. They are not installed on the computer. Another application on the machine will not see the kit unless you install those faces yourself, on purpose, under the license you have. Removing a role refuses to strip the file out from under artwork that already uses it. The bytes stay. The label goes. Open Character studio, pick the Project font, and set the OpenType row on the headline you just typed. ## The thread Part 33 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-type-tool) · [Next](/blog/omadesign-0-5-8-gradient-eyedropper) --- # Gradient eyedropper Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-gradient-eyedropper Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, color ## The habit You select a shape. You press G. You drag, and a gradient appears along that drag. In Illustrator the gradient tool is the same gesture, and the annotator stays up so you can pull the endpoints after. Affinity's fill tool is the same idea with a different key. The drag is the decision about where the color changes. A dialog with an angle field is the backup, not the start. Then you sample. I, or the eyedropper, picks up a color from something already on the page so the next shape matches. You learn quickly whether the dropper grabbed the fill, the stroke, or a pixel out of a photograph. The useful version tells you which. The version that silently samples "whatever is under the cursor, blended" makes you undo. The color panel behind both tools is muscle memory too. A square of saturation and brightness, a hue slider, a hex field, swatches, the last few colors you used. Current and previous, so you can step back to the color this edit started from without undoing the geometry. Alpha in the same picker, because a gradient stop at 40% is a normal stop, not a special object. X swaps fill and stroke. D puts the defaults back. You hit X when you painted the stroke and meant the fill. You hit D when the object has wandered into a color you do not want to debug. ## The constraint Fill and stroke are both paint, and both can be a gradient. If the gradient tool always wrote the fill, you would have no straight gesture for a gradient stroke, and you would rebuild it in a panel. Appearance keeps an active Fill row and an active Stroke row. G edits whichever row is active. Existing stops and the gradient type stay. A drag repositions. It does not throw away a five-stop ramp and replace it with a black-to-white default because you touched the tool. The `.oma` keeps all four gradient types editable: linear, radial, shape, and conic. Solid sits beside them. SVG cannot carry all four the same way, and pretending it can is how files open with missing fills. Linear and radial export as real gradient stops. Shape and conic export as embedded image patterns, capped at 4096 pixels on an axis. The project file remains the editable one. Lottie says shape and conic are unsupported, out loud, which is the honest version of a format that cannot hold them. The eyedropper samples fill. One job. Stroke stays on the stroke chip until you swap with X or paint it on purpose. A dropper that copied the entire appearance would be a style paste, and style paste is already Ctrl+Alt+C and Ctrl+Alt+V. I is the color. Hex includes alpha. Eight digits, `#RRGGBBAA`. Every picker accepts alpha: gradient stops, paint, Layout color variables, Design effect colors. One field, one meaning. ## What landed Press G. Drag across a selected shape. The gradient lands on that drag. Endpoints stay visible while the Gradient tool is selected, so you can adjust the run without leaving the tool. The Appearance studio's active Fill or Stroke row chooses which paint you just edited. Appearance offers Solid, Linear, Radial, Shape, and Conic, for fills and for strokes. Click the gradient ramp to insert a stop. Drag a stop's handle to move it. Select a stop to edit its color, its alpha, and its percentage. Plus adds a stop. Minus removes one. Reverse flips the stop order. Even spaces them equally. Angle rotates linear, radial, and conic gradients. Radial gradients spread outward from the point where the drag started. Conic gradients sweep around that point. Shape gradients follow the actual silhouette, holes included, from the interior out to the boundary. A compound with a counter gets a shape gradient that knows the hole is there. Linear and radial SVG exports keep editable stops, including a gradient on a stroke. Shape and conic SVG exports use an embedded image pattern, up to 4096 pixels on each axis. Open the `.oma` and all four types are still editable. Lottie reports shape and conic as unsupported. Press I. The eyedropper samples a fill. The sampled color is the fill you can use next. Color studio shows saturation times brightness, hue, alpha, hex, swatches, and recent colors. Open a color chip and Current sits next to Previous. Click Previous to restore the color from before this edit. That restore is about the color, so you can abandon a bad pick without throwing away the shape. X swaps fill and stroke. D restores the defaults. Neither chord uses Control. Control+X is cut. The plain keys are the paint keys. Older two-color radial fills still exist in files that had them. Editing one in Appearance upgrades it to the multi-stop format. You do that on purpose, by editing, when you want the newer stops. ## In the hand Select a shape with V. In Appearance, click the Fill row. Press G and drag from the left edge of the shape to the right edge. A linear gradient follows the drag. The endpoints stay on screen. Drag an endpoint if the run should be shorter than the shape. Click the ramp where a third color should sit. A stop appears. Select it. Set the hex, or sample a fill from another object with I and bring that color to the stop. Give the stop an alpha if it should fade. Hit Even if the stops drifted into a clump. Hit Reverse if the fade runs the wrong way. Set Angle if the ramp should rotate and you would rather type than drag. Click the Stroke row. Press G and drag along the stroke. The stroke takes the gradient. The fill you already built stays, because the active row was the stroke. Press X if you need the two paints exchanged. Press D on an object that has become a mess of leftover paint. Defaults come back. Then build the gradient you actually wanted. For a radial, press G and drag from the center outward. The spread starts at the drag start. For a conic, choose Conic and drag. The sweep goes around that start. For a badge with a hole, choose Shape. The gradient follows the silhouette and the hole, interior to boundary. ``` G Gradient on the selected shape Fill or Stroke The Appearance row G edits I Eyedropper, samples fill X Swap fill and stroke D Restore defaults #RRGGBBAA Hex with alpha ``` Ctrl+S writes the `.oma`. The stops stay editable there. Export SVG when the gradient is linear or radial and the next person needs real stops. Export SVG of a shape or conic gradient when a pattern of the picture is enough, knowing the cap is 4096 pixels on an axis. ## The edge The eyedropper samples the fill. The stroke stays on the stroke. Copy style and paste style, Ctrl+Alt+C and Ctrl+Alt+V, are how a whole appearance moves. I is the fill color. Shape and conic gradients stay editable in the `.oma`. Their SVG export is an embedded image pattern. Lottie reports those two types unsupported. Linear and radial are the exports that keep stops. Press G, drag across the selected shape, and press I when the next stop should match a fill already on the page. ## The thread Part 34 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-opentype-character-studio) · [Next](/blog/omadesign-0-5-8-trace-raster-to-vector) --- # Trace raster to vector Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-trace-raster-to-vector Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, trace ## The habit You have pixels and you need paths. A logo from a decade-old PDF that rasterized on the way out. A stamp scanned on a flatbed. A signature on a white JPEG. An icon someone exported as PNG because that was the only download button. In Illustrator you place the image and run Image Trace, then Expand, and you fiddle a threshold and a color count until the paths match the picture well enough to edit. Affinity has a trace, same idea, often under a different name. The hand knows the knobs even when the menu moves. How much is "ink"? How many colors survive? How smooth are the curves allowed to get before they stop being the drawing? You also know the failure mode. Trace runs on the wrong layer. It traces a screenshot of the whole artboard, including the UI you accidentally captured, or it traces a placed photo when you meant the logo next to it. The command has to name its source. "The active pixel layer" is a sentence you can check in the Layers studio before you commit. The other habit is the tool switch. Sometimes you want the trace tool in your hand, U, because you are going to do several. Sometimes you are on the Move tool, the layer is already selected, and you want Object → Trace to vector without the tool change. Both entrances have to be the same operation. Two traces that disagree would be a bug you would hit on the first logo. Photoshop is where the pixels usually start. You clean them there, or you clean them here in Pixel, and then you trace. The trace result has to be vectors in the same document, because a trip out to Illustrator and back is the handoff this studio is built to skip. ## The constraint Paint lives on a pixel layer. A document that is only vectors has no pixels to trace. You add a pixel layer from the Layers studio, or you place an image, or you paste one. Trace reads the active pixel layer. It does not invent a raster from the paths already on the canvas, and it does not hunt through hidden layers for something that looks like a logo. The active layer is the contract. You select it. You see it. You trace it. The knobs stay in one studio, Trace: threshold, color count, smoothness. Threshold is the cut between ink and paper for a mark that is basically one color. Color count is how many paints a badge is allowed to keep. Smoothness is how hard the curves try to simplify. They live next to the command so you can change them and run again. They are not buried in a modal you cannot find after the paths exist. One `.oma` holds the pixels and the vectors. Undo is Ctrl+Z, one step, the same history as the rest of the document. A bad trace comes off the way a bad rectangle comes off. You are not managing a linked AI file beside a PSD. Object → Trace to vector and the U tool share the work. The menu is there so a selected pixel layer can be traced while your left hand stays on V. The tool is there so the key is memorable. Right-click the canvas and Trace is on that menu too, with Place and the other edits. Three doors, one operation. ## What landed Press U. Trace converts raster to vector on the active pixel layer. Threshold, color count, and smoothness are in the Trace studio. You set them there. You run the trace. The vectors land in the document, where Node, pen edits, fills, and strokes can take them. Object → Trace to vector does the same thing without switching tools. If U is not selected, the menu still traces the active pixel layer with the Trace settings. Right-click the canvas and choose Trace when your hand is already on the mouse. Same source, same knobs, same result. The source has to be a pixel layer, and it has to be the active one. Click the layer in the Layers studio before you trace. An image you placed, a paste that became a pixel layer, a brush stroke on a pixel layer you added yourself: those are sources. A rectangle, a type object, and a pen path are not the input. If the document is vector-only, add a pixel layer first, or place the raster with File → Place… or Ctrl+Shift+P, then make that layer the active one. The result is vector artwork in the same document. A traced mark comes back as paths. Node, A, is how you clean a lump. Pen, P, can continue an open end. Trace's job is the conversion off the active pixel layer. ## In the hand Place the logo. Ctrl+Shift+P, or File → Place…, or drop the PNG on the canvas. Click the pixel layer so it is the active one. Look at the Layers studio and confirm the eye is open and you are on that row. Open Trace. For a black mark on white, start with threshold. Run the trace with U, or with Object → Trace to vector. Look at the paths at the zoom you will actually use. If the counters filled in, ease the threshold and run again. If a three-color badge collapsed to one paint, raise the color count. If the curves are noisy, raise smoothness. If the curves have lost the notch that makes the mark recognizable, lower smoothness. Each run is something you can Ctrl+Z. Press A and delete a speck path. Drag a point that missed the corner. The trace got you onto anchors. You finish by hand, the way you always have. ``` Ctrl+Shift+P Place the raster Layers Make the pixel layer active Trace Threshold, color count, smoothness U Trace tool Object → Trace to vector Same trace, current tool stays Ctrl+Z Walk a bad result off A Clean the points ``` Save the `.oma`. The vectors are in the project with the rest of the poster. Make the next pixel layer active and trace again when the next logo shows up. The knobs stay in Trace. ## The edge Trace refuses every source except the active pixel layer. A selected rectangle is not the input. A hidden logo under a visible photograph is not the input, unless that logo's layer is the one you made active. Object → Trace to vector uses the same rule. The menu is not a wider net. The command does not rewrite a camera RAW. Photo development and Place in Design are how those pixels enter the document. Trace runs on the pixel layer inside the `.oma`. Press U with the right pixel layer active, or choose Object → Trace to vector and keep the tool you already had. ## The thread Part 35 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-gradient-eyedropper) · [Next](/blog/omadesign-0-5-8-zoom-and-hand) --- # Zoom and hand Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-zoom-and-hand Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, canvas ## The habit Z is zoom. You drag a box around the knot you need to see, and the view becomes that box. You click to step in. You Alt-click to step out. Illustrator does this. Affinity does this. Photoshop does this. The hand has done it since the tool had a magnifying glass icon. You do not want a tour of the zoom menu. You want the knot. Fit is the other half. You get lost at 1600% and you need the page back. Ctrl-click with the zoom tool fits the artboard. A modified click fits the selection when you are working on one mark and the artboard is huge. If nothing is selected, that same modified click fits every object, so a stray path off in the pasteboard still gets included and you can see why the fit felt wrong. The wheel is where apps embarrass themselves. You scroll to move down the page and the entire window, panels included, changes size. Or you pinch the trackpad and the sidebars grow until the canvas is a stamp in the middle. Zoom belongs to the canvas. The chrome stays put. Panels, tool strip, inspector: they are the desk. The canvas is the paper. Only the paper zooms. Pan is Space, held, in every drawing app you already use. H is the hand you can leave selected. You hold Space while a shape tool is active, you drag the paper, you release Space, and you are back on the rectangle. If Space stuck as the hand, you would draw a pan by accident on the next drag. ## The constraint The studio is one window. Document tabs, the tool strip, the layers, the inspector, and the canvas share it. A zoom that scaled egui's chrome would resize the controls you need in order to zoom back, which is a trap. Ctrl and the scroll wheel, Alt and the scroll wheel, Ctrl++, Ctrl+-, and a trackpad pinch all zoom the canvas. The keys say so because the mistake is so common. Fit has two jobs and they cannot share one click. The artboard is the page. The selection is the work. Ctrl-click on Z fits the artboard. Ctrl+Shift-click fits the selection, or every object when the selection is empty. Ctrl+0 is Fit from the shortcut list. Ctrl+1 is 100%, actual size, which is the only honest answer when you are judging type. Those chords stay available while you are on other tools, because making someone switch to Z to hit 100% is how people ship the wrong size. Space pans in Design. H pans. In Motion, Space plays the timeline. The hand key has to remain a pan, because Space's meaning follows the persona. You learn H once and it still moves the paper when you are animating. Photo uses Space or Hand to drag the view, middle-drag and two-finger scroll to pan, and pinch, Ctrl+scroll, and Alt+scroll to zoom. Ctrl+0 fits the photo there. Ctrl+1 is 100%. The Design page uses the artboard as its fit target. The photograph uses the photograph. Same fingers, the object in front of you. With Z selected, two-finger scroll zooms. That is the trackpad version of "I am in the zoom tool, so the gesture zooms." Pinch zooms the canvas even when you are not on Z. One `.oma`, one view of it. Fit and 100% are how you get back to a known magnification. History on the artwork stays the place you undo edits. ## What landed Press Z. Drag a box. The view becomes that area. Click to zoom in one step. Alt-click to zoom out one step. Ctrl-click fits the artboard. Ctrl+Shift-click fits the selection. If nothing is selected, Ctrl+Shift-click fits every object. Pinch the trackpad to zoom the canvas. Ctrl++ zooms in. Ctrl+- zooms out. The plus key on the chord also accepts the equals key, the one you actually hit without hunting for plus. Ctrl+scroll zooms the canvas. Alt+scroll zooms the canvas. With Z selected, two-finger scroll zooms. None of these resize the chrome. Press H, or hold Space, and drag to pan. Release Space and the previous tool is what you are holding. Leave H selected when you are only navigating. Ctrl+0 fits. Ctrl+1 is 100%. Front and back and the rest of the Ctrl chords stay themselves. Zoom in and out are the plus and minus chords above. You can run them while a shape tool is active. You do not park the rectangle to see it larger. In Motion, Space plays. Home and End jump. The hand tool is still how you pan when play owns the spacebar. In Photo, Space pans, because there is no timeline play on that spacebar. If a pan does nothing useful and a clip starts, you are in Motion. Press H and drag. Snapping and guides are unchanged by zoom. You see more or less of the same canvas. Ctrl+; still shows or hides guides. A zoomed-in node edit is the same Node tool. The points stay on the artwork. You got closer. You did not switch documents. ## In the hand Press Z. Drag a box around a wordmark. The view fills with it. Click once if you need one more step. Alt-click until the poster is back to a size you can judge. Or Ctrl-click to fit the artboard in one move. Select one icon. Press Z and Ctrl+Shift-click. The view fits that icon. Deselect, Ctrl+Shift-click again, and the view fits every object, including the one you left off the board. That is how you find it. Press Ctrl+1 before you call a body size done. 100% is the shortcut. Press Ctrl+0 when you want Fit and your hand is not on Z. Hold Space while you are on the pen. Drag the canvas until the next point is in view. Release Space. Click the point. The pen was the tool the whole time. The spacebar borrowed the hand. On a trackpad, pinch. The canvas zooms. The layers stay the width they were. If you are on Z, a two-finger scroll zooms as well. ``` Z Zoom Drag a box Zoom to that area Click Zoom in one step Alt-click Zoom out one step Ctrl-click Fit the artboard Ctrl+Shift-click Fit the selection, or every object Ctrl++ Ctrl+- Zoom the canvas Ctrl+0 Fit Ctrl+1 100% H or Space Pan ``` The scroll chords are Ctrl+scroll and Alt+scroll. They zoom the canvas. Pinch does the same. The panels stay put, which is the entire point of aiming the gesture at the paper. ## The edge Zoom refuses the chrome. Panels, the tool strip, and the inspector keep their size. The canvas takes Ctrl++, Ctrl+-, Ctrl+scroll, Alt+scroll, pinch, and the Z tool. A wheel gesture that resized the whole window is the bug this binding is there to prevent. Space pans the Design canvas. In Motion, Space plays the clip. H is the pan that stays a pan. Press Z, drag a box around the knot, and hold Space when you need to slide the paper without leaving the tool in your other hand. ## The thread Part 36 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-trace-raster-to-vector) · [Next](/blog/omadesign-0-5-8-select-same-properties) --- # Select same properties Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-select-same-properties Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, selection ## The habit You have forty icons and six of them are the wrong green. In Illustrator you choose Select → Same → Fill Color, and if you are lucky the selection is the six. If you are unlucky it is every object that contains that green anywhere in a gradient, plus a stroke you forgot, plus a locked layer from the template. Affinity's Select Same is the same bet. Photoshop's Select → Color Range is the pixel version, and it has the same way of grabbing more than you meant. The useful version compares the thing you think you compared. Same fill means the fill, the whole fill. A linear gradient with stops at 0, 40, and 100 is not the same fill as a linear gradient that merely starts and ends on the same two hex values. Same stroke means the stroke settings, not "any object with a red hairline." Same effects means the effect stack, not the first blur in a list of six. You also want the negative. Everything with no fill. Everything with no stroke. Everything with no effects. That is how you find the shapes that are invisible for a dumb reason, and the shapes that are still carrying a drop shadow from a template. All, None, and Invert are the three you use around those, when the selection you built is almost right and needs to be flipped or cleared. Locked and hidden art has to stay out. You locked it so a stray select would not grab it. A Same Fill that ignores the lock is why people stop using locks. ## The constraint The document is one `.oma` full of vectors, type, and placed images, sometimes across more than one artboard. A select-same that only understood flat hex colors would miss the paint this studio actually stores. Fills and strokes can be solid, linear, radial, shape, or conic, with stops, alpha, and positions. Effects are an SVG filter stack on the object. Matching has to compare the complete property. Gradient positions count. Stroke settings count. The effect stack counts. A looser match would select half the poster every time you used a brand green. Hidden objects and locked objects stay out of the selection. Canvas paper stays out. The page background is not an object you recolor by accident because it happened to be white and your icons are white. The command's job is artwork you can edit. Paper, locks, and closed eyes are the boundary. Undo is one step after you change the selection, not for the selection itself as a paint change. You select, then you recolor, and Ctrl+Z returns the color. The selection command is how you aim that one step at the right set. If the set is wrong, None and a more careful Same are cheaper than an undo of a recolor that hit thirty extra objects. One binary, no round trip to a "select similar" extension. The menu is Select. It is in the app. The comparison runs on the properties already in the file. ## What landed Select offers All, None, and Invert. All takes the artwork the command is allowed to take. None clears the selection. Invert flips it, inside the same rules. Locked and hidden objects stay out of all three. You do not invert your way into a locked logo. Select offers Same Fill, Same Stroke, and Same Effects. Matching compares the complete property. Same Fill looks at the fill you have, including gradient positions. Two gradients that share endpoint colors and differ in stop placement do not match. Same Stroke looks at the stroke settings as a whole. A stroke that only shares a color stays unselected when the rest of the settings differ. Same Effects looks at the effect stack. An object with a blur and a drop shadow does not match an object with only the blur, and it does not match an object whose stack is ordered differently if the stack itself differs. Select also offers With Fill, Without Fill, With Stroke, Without Stroke, With Effects, and Without Effects. These are the presence tests. Without Fill finds the holes in a layout where a shape has no paint. With Effects finds everything still carrying a stack, which is what you want before you flatten a poster for a vendor who asked for simple vectors. The tweet writes them as With and Without Fill, Stroke, and Effects. The menu is that grid. The comparison is the property on the object. Same Fill is the fill. Same Stroke is the stroke. Same Effects is the stack. You change blend mode and opacity after, in the transform inspector, on the selection you already aimed. Canvas paper stays out. A white artboard does not join a selection of white fills. The selection you get is a normal selection. Move it with V. Change the fill in Color studio or Appearance. X still swaps fill and stroke on the objects you are painting. Eyedropper I still samples a fill. You aimed with Select. You edit with the same tools as a click. ## In the hand Click one of the good icons. Choose Select → Same Fill. The icons that carry that complete fill join the selection. Look at the layer list. A locked duplicate on the bottom stays unselected. A hidden version in a scrap group stays unselected. If a gradient icon stayed out, its stops are not the same property, even when the end colors look close. Open Appearance on both and compare positions before you call it a bug. Choose Select → Same Stroke on a rule that has the weight and the dash you want repeated. The matches share those stroke settings. Change the stroke once. Ctrl+Z if the set was wider than you thought, and the stroke change comes back as one step. Choose Select → Same Effects on an object with the shadow stack you are about to edit. The objects with that stack select. Change the blur in FX. The set updates together because you selected it first. Choose Select → Without Fill when a poster has invisible hit areas you need to find. Choose Without Stroke when shapes should be filled only and some of them still have a hairline. Choose Without Effects when you want the clean objects left after a shadow pass. With Fill, With Stroke, and With Effects are the mirror images. ``` Select → All Select → None Select → Invert Select → Same Fill Select → Same Stroke Select → Same Effects Select → With Fill Without Fill Select → With Stroke Without Stroke Select → With Effects Without Effects ``` Ctrl+A is Select all from the keys, the shortcut form of All. Invert after Same Fill flips the editable set. Locked and hidden pieces still stay out. ## The edge Hidden objects stay out. Locked objects stay out. Canvas paper stays out. Same Fill will not pull a locked template object into the recolor, and it will not select the page. The match is the complete property. A shared hex value at one gradient stop is not a match. Stroke settings travel as a unit. The effect stack travels as a unit. A looser "contains this color" is not this menu. Click the object with the fill you mean, then choose Select → Same Fill. ## The thread Part 37 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-zoom-and-hand) · [Next](/blog/omadesign-0-5-8-compound-paths-0-5-8) --- # Compound paths 0.5.8 Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-compound-paths-0-5-8 Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, paths ## The habit A letterform, a donut, a window in a building, a logo with the counter knocked out of the bowl of the mark. You build it with Union and Subtract. In Illustrator the result is a compound path, and you can still direct-select the hole. Sometimes. After enough booleans the compound collapses into a single outline that filled the counter back in, or into a group of scraps you have to reassemble. Affinity's geometry operations have the same cliff. The first subtract looks right. The twentieth union, done because a client added another circle, welds the hole shut or refuses to let you grab the inner contour. You learn to expand, ungroup, and rebuild. You also learn to keep a hidden copy of the circles off to the side of the artboard, because the boolean result is a dead end. That hidden copy is an apology. The live object should have been the counters. Double-click to dive inside is the other habit. Illustrator uses it on groups and on clipping sets. You want it on the compound. Select or the node tool, double-click, edit the hole, click out. You should not have to release the compound into twenty pieces, edit one, and combine them again hoping the hole remembers its job. ## The constraint Repeated booleans have to stay one object with several contours. The hole is data. If each union flattened the silhouette into a single filled outline, the counter would be gone and no undo except the immediate one could bring it back. 0.5.8 keeps the contours. A subtraction followed by a long run of unions still has the hole. The release checks that case: one subtraction, then twenty-four unions, the picture identical after conversion, the hole still there, and the nodes editable with the pointer. Undo is normal. A point move, an inserted point, a deleted point, a broken contour: each one comes back with Ctrl+Z the way a simple path's edit comes back. A special compound history would mean you learn two undos. You learn one. Save and reopen have to keep the contours. An `.oma` that wrote a baked fill and dropped the hole on the way to disk would look right until tomorrow. SVG export has to keep the hole too, because the picture you hand someone is part of the test. The release covers native save and reopen, undo and redo, and SVG export, along with node movement, handles, insertion, deletion, and breaking a contour. Double-click is the door in. Move is the selection tool, V. Node is A. You already know both. The compound does not demand a third tool called Compound Editor. You double-click with the tool you use to select or the tool you use to edit points. This is the boolean result. A group is a different object, made with Ctrl+G, and it does not fuse contours. The compound is what repeated unions and subtractions produce. You edit the contours in place. ## What landed Run unions and subtractions more than once and the result is an editable compound path. The holes stay intact. The contours stay independently editable. You can do this across a stack of operations and the counters remain counters. Double-click with Select or with Node to edit. Select here is Move, V, the tool you already click with. Node is A. Inside the compound, the node edits you know still apply across it. Move points. Pull Bézier handles. Insert a point on a curve. Delete points. Break a contour. Those gestures work on the compound, not only on a path that has a single outline. Corner radii stay editable after the repeated operations. Node shows corner-radius handles on paths. Alt-drag changes every corner on the path. A Shift-selected set changes together. A compound you have been booleaning does not lose that. Convert to paths, or double-click with Select or Node, and the contours and holes are still there. You are not punished for leaving the boolean result and going to points. The geometry you see is the geometry you edit. The release exercise is the one to remember when a file gets large. Subtract, then union two dozen more shapes. The render matches after conversion. The hole remains. You can grab a node with the pointer and move it. That is the floor. A logo with a counter and a pile of extra parts stays a logo with a counter. Undo and redo are the normal chords. Ctrl+Z, Ctrl+Shift+Z, Ctrl+Y. Save the `.oma`. Open it. The compound is still a compound. Export SVG and the hole is part of the picture you exported. Groups still exist for art that should move together and stay separate objects. If you wanted a group, you wanted Ctrl+G. If the counters have to be holes in one fill, you wanted the boolean, and 0.5.8 leaves you a compound you can keep editing. ## In the hand Draw a filled circle with O. Draw a smaller circle on top of it. Select both. They have to be vector objects on the same layer for a Pathfinder op. Choose Object → Pathfinder → Subtract. You get a ring. The hole is the smaller circle. Draw more shapes that should join the outer contour. Select the ring and the new shapes. Union. Do it again when the next piece arrives. The hole stays. You can keep going. The case worth trusting is a hole that survives a subtraction and then many unions, on the order of a couple of dozen, and still takes a pointer edit. Press V. Double-click the compound. You are in it. Switch to A if you came in with Move and you need points. Or double-click with A in the first place. Drag a node on the outer contour. The hole stays put. Drag a node on the hole. The outer contour stays put. Click a curve to insert. Select a point and Delete. If a contour needs to open, break it. Ctrl+Z returns that edit. The rest of the compound stays. Alt-drag a corner-radius handle if the outer shape has corners that should match. The radius edit is the Node tool's, and it still applies once these contours are paths. ``` Object → Pathfinder → Subtract Knock the hole Object → Pathfinder → Union Add, and keep the hole V or A, double-click Edit the compound Drag a node That contour moves Ctrl+Z One step back ``` Click out, or select something else, when you are done inside. The compound is one object again in the layer list. Move it with V. It moves as one piece, hole included. Save. Reopen when you want the proof. The counter is still open. SVG export writes the artwork with that hole. If you are also sending a Lottie later, remember effects and pixel layers have their own limits. The compound's geometry is the Design object you just edited. ## The edge Further unions refuse to paint the hole shut. The contour you subtracted stays a hole through the repeated booleans that follow. A subtraction plus a long run of unions is still a compound with that hole, and the nodes on it still move. Release is a different command, Ctrl+Shift+8, for the day you want the contours as separate artwork again. Until you release, double-click and edit. The compound does not make you flatten it to change one point. Double-click the compound with V or with A, and drag the node on the contour you mean to change. ## The thread Part 38 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-select-same-properties) · [Next](/blog/omadesign-0-5-8-pathfinder-ops) --- # Pathfinder ops Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-pathfinder-ops Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, pathfinder ## The habit Pathfinder is the five words you already know. Union welds shapes into one. Subtract knocks the front pieces out of the back, or the other way around, depending on which app and which year taught you. You learned to look at stacking order because the order is the operation. Intersect keeps the overlap. XOR, or exclude, keeps the parts that do not overlap. Divide cuts everything into the pieces the overlaps imply, and you ungroup or direct-select the piece you want to delete. Illustrator puts them on a panel. Affinity puts them on a toolbar and a menu. The failure is the same in both when the selection is sloppy. Two shapes on different layers. A guide included with the artwork. A locked object you thought was in the selection. The operation either grabs the wrong thing or errors in a sentence you have to read twice. Empty intersections should stay empty. A hole should subtract as a hole, not as a filled disk that paints the counter back in. Rotated art is the quiet bug. The shapes are turned. The boolean runs on the axis-aligned bounds, or on the unrotated geometry, and the overlap you see is not the overlap that gets cut. You undo. You expand. You try again. The operation has to run where the ink actually is. ## The constraint One undo step per operation. Union is one step. Subtract is one step. You do not get a history entry per fragment, and you do not get a history entry that covers three booleans you have not run yet. Ctrl+Z returns the objects you had before that one command. That is the only way a destructive weld stays safe in a file with no version server sitting behind it. The objects have to be vectors, two or more, on the same layer. A boolean across layers would also be a reorder, a reparent, and a weld, and one undo would have to describe all of it while the layer list lied about where things lived. Same layer first. You drag rows before you boolean, if the stacking is wrong. Each reorder is its own undo. Then the pathfinder op is its own undo. The steps stay legible. Stacking order is the order of the operation. The manual does not dress that up with a second rule. You stack the objects the way the cut has to read, then you run the command. Divide produces separate pieces and preserves holes. A window you divide through still has the hole. Guides and artwork do not mix in one operation. Combine and Pathfinder take either artwork or guides, and they refuse the mixture. A guide is a non-printing contour. Welding it into a logo, or cutting a logo with a guide as if it were ink, would bake a construction line into the mark. If you want to boolean guides, select guides. If you want to boolean the poster, select the poster. Shape gradients follow the silhouette that results. The gradient hugs the new outline, holes included, which is what a shape gradient is for. An older two-color radial follows the bounds of the result. Edit it in Appearance and it upgrades to the multi-stop format. You opt in by editing. ## What landed Select two or more vector objects on the same layer. Object → Pathfinder offers Union, Subtract, Intersect, XOR, and Divide. Union makes one shape from the pile. Subtract uses the stacking order. Intersect keeps what overlaps. XOR keeps what does not. Divide cuts the selection into separate pieces along those overlaps, and holes in the artwork stay holes in the pieces. Each operation is one undo step. Ctrl+Z restores the objects from before that op. Redo is Ctrl+Shift+Z or Ctrl+Y. Rotated artwork is processed where it appears on the canvas. The overlap you see is the overlap the cut uses. You do not unrotate, boolean, and rotate back to make the result honest. An empty intersection stays empty. If the shapes do not overlap, Intersect does not invent a sliver. A hole subtracts area. The counter stays open. Repeated booleans share one geometry path, so the fifth union is the same kind of operation as the first. In 0.5.8 those repeated unions and subtractions remain editable compounds: contours and holes stay editable, double-click with Move or Node, and the node gestures work across the compound. Pathfinder is how you build that. The compound edit is what you do after. Compound shape and release sit next to this menu as keys. Ctrl+8 combines into a compound. Ctrl+Shift+8 releases it. Group is Ctrl+G and does not combine paths. Ungroup is Ctrl+Shift+G. You use Pathfinder when you want a boolean. You use Ctrl+8 when you want the compound combine. You use Ctrl+G when you want a group. Three different results, three different commands. Shape gradients follow the resulting silhouette. The new outline, including holes, drives a shape gradient. Older two-color radials follow the resulting bounds until you edit them in Appearance and take the multi-stop upgrade. ## In the hand Put the objects on one layer. Drag rows if you have to. Drop a row on an insertion line to reorder. Ctrl+[ and Ctrl+] move a selected layer row backward and forward. Add Shift to send it to the back or the front of its group. Click the canvas when you want those brackets to go back to object stacking. Get the order right while they are still separate objects. Select them with V. Shift-click to add. Choose Object → Pathfinder → Subtract. Look at the hole. If the wrong shape was subtracted, Ctrl+Z, reorder, and run Subtract again. The undo gave you the original objects. The reorder is the next step. The new subtract is the step after that. Union when the pieces should become one fill. Intersect when you want the lens where two shapes cross. XOR when you want that lens knocked out and the rest kept. Divide when you want the pieces as separate objects so you can delete one and keep the others. After Divide, select the scrap and Delete. Holes that were in the source are still holes in the pieces. ``` Same layer, two or more vectors Object → Pathfinder → Union Object → Pathfinder → Subtract Object → Pathfinder → Intersect Object → Pathfinder → XOR Object → Pathfinder → Divide Ctrl+Z That one operation ``` Run another union on the result and a new shape. The hole from the earlier subtract stays. Double-click with V or A when you need a node on one contour. You are in the compound 0.5.8 keeps editable. If the command refuses the selection, look for a mixture of guides and artwork. Release the guides back to art, or leave them out of the selection. Pathfinder will take a selection of guides, and it will take a selection of artwork. It will not take both at once. Save. The boolean is in the `.oma`. One undo would still walk the last op off, until you make another edit on top of it. ## The edge Pathfinder refuses a mixture of artwork and guides. The selection is artwork, or the selection is guides. A construction line left in the selection with a logo stops the operation. Pull the guide out of the selection, or release it, and run the command on one kind of thing. The objects also have to share a layer, and there have to be two or more of them. An empty intersection stays empty. A hole stays a hole. Each of those results is one undo. Stack the vectors on one layer, then choose Object → Pathfinder and the operation the stacking order is ready for. ## The thread Part 39 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-compound-paths-0-5-8) · [Next](/blog/omadesign-0-5-8-combine-and-release) --- # Combine and release Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-combine-and-release Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, compounds ## The habit Ctrl+G is group. It has been group in Illustrator for decades. You select the icon and the type, you group them, you move them as one, you double-click to edit inside, you ungroup when the lockstep is over. Affinity uses the same chord. The paths inside a group stay separate paths. Ungroup gives them back. Nobody expects Ctrl+G to weld the letterforms into a single outline. Combine is the other habit, and it has been glued to the group key in too many conversations. A compound path, the kind with a hole, is how a donut stays one fill. Illustrator makes that with Object → Compound Path → Make, and the shortcut a lot of hands remember is not the group shortcut. Release compound is how you get the contours back as separate objects when the hole was a mistake. The weld and the group are different objects. One moves children together. The other builds a single path with contours and counters. The sloppy version treats the group key as the compound key. You hit Ctrl+G expecting a hole, you get a group, the overlap paints twice, and you think the boolean failed. Or a cheat sheet tells you Ctrl+G combines and Ctrl+Shift+G releases, and you ungroup a logo trying to release a compound. The children scatter. The compound you wanted was never there. Guides get caught in the same gesture. You meant to combine two construction lines. A piece of the logo was still selected. A weld of ink and a non-printing guide is garbage. The command has to refuse. ## The constraint Omadesign keeps both objects, and it gives them different keys, because one key cannot mean both. Group is Ctrl+G. It creates an editable layer group. Ungroup is Ctrl+Shift+G. It releases the group and it does not combine paths. The paths come out as paths. Nothing was fused, so nothing has to be unfused. Combine, the compound, is Ctrl+8. Release compound is Ctrl+Shift+8. The 8 is the compound key so the G key can stay the group key your hand already trusts. Pathfinder Union and the other booleans stay their own commands under Object → Pathfinder. Three results: a group, a compound from Ctrl+8, a boolean from the menu. You pick the one you meant. Combine and Release, the compound pair, preserve guide state, rotation, stacking, and gradient endpoints in one undo step. A compound that forgot its rotation would jump on release. A linear gradient that forgot its endpoints would have to be redrawn. Radial fills stay radial and follow each resulting object's bounds. The file cannot keep one shared radial center stretched across contours that you just separated. That limit is in the open. You see each piece take its own bounds. Edit an older two-color radial in Appearance and it upgrades to multi-stop stops. One undo. Ctrl+Z after Ctrl+8 puts the separate objects back, with the guide state, the rotation, the stacking, and those gradient endpoints. You do not rebuild the stack by hand. Artwork and guides do not mix. Combine and Pathfinder require either artwork or guides. A mixture is rejected. Guides are construction. Artwork is the poster. One command does not get to average them. ## What landed Select the objects that should travel together and stay individually editable. Press Ctrl+G. You get a layer group. The group expands in the layer tree. You can rename it, hide it, lock it, and reorder it as a unit. Children move with it. Double-click an item to edit that item until you select something else or clear the selection. Groups select, move, duplicate, and align as units. Press Ctrl+Shift+G to ungroup. The group is gone. The paths are not combined. They were not combined on the way in. Ungroup is not a boolean and it is not a release-compound. If you wanted the contours fused, you have not done that yet. Select the vectors that should become one compound. Press Ctrl+8. That is Combine. The compound carries the contours. Press Ctrl+Shift+8 to release the compound. That is Release. Guide state, rotation, stacking, and gradient endpoints come through the round trip, and the pair is one undo per command. Linear gradients keep their endpoints through that combine and release. A radial fill stays radial. On a release that produces separate objects, each one follows its own bounds. Plan on resetting a radial if you had imagined one center shared by every released piece. Shape gradients follow the resulting silhouette, holes included, which is the shape-gradient rule everywhere else too. Stacking stays. The order you had is the order you get back on release. Rotation stays. A compound you built from rotated art does not snap back to axis-aligned on the way out. Pathfinder remains the boolean menu: Union, Subtract, Intersect, XOR, Divide. Repeated unions and subtractions in 0.5.8 produce editable compounds with holes intact. That is a boolean result you can double-click into. Ctrl+8 is the combine key beside that workflow, not a rename of Ctrl+G. Drag objects onto the center of a group or a Layout frame to nest them. Shift-click rows to select several. Reorder with insertion lines, or with Ctrl+[ and Ctrl+] on a layer row. None of those chords combine paths. They arrange the group you already made. ## In the hand Build a small mark from two strokes and a filled shape. Select all three. Press Ctrl+G. Drag the group. The three move together. Double-click one stroke. Nudge it. Click away. Press Ctrl+Shift+G. You have three objects again. Look at them. They are the same three paths. Nothing welded. Select two overlapping filled shapes that should share a compound. Press Ctrl+8. One compound. If you needed a hole from a boolean, use Object → Pathfinder → Subtract first, and keep the result. Ctrl+8 is the combine. The menu is the boolean. Use the one the drawing needs. Press Ctrl+Shift+8 when the compound should become separate contours again. Check the rotation and the linear gradient. They should be the ones you had. Press Ctrl+Z. The compound returns, one step, endpoints included. Try the refusal on purpose once, so you recognize it. Convert a rectangle to a guide with Object → Guides → Convert selection to guides. Select that guide and a real shape. Press Ctrl+8. The mixture does not run. Select two guides and combine those, or select two shapes and combine those. ``` Ctrl+G Group. Paths stay paths. Ctrl+Shift+G Ungroup. Does not combine paths. Ctrl+8 Combine into a compound. Ctrl+Shift+8 Release compound. Ctrl+Z One step, endpoints and rotation included. ``` Save the `.oma`. Groups and compounds reopen as what you saved. The keys will be the same tomorrow. G groups. 8 combines. If a cheat sheet in your head says Ctrl+G makes the compound, retire that sheet. In this studio Ctrl+G is the group. The compound is the 8 key, with Shift when you want it released. ## The edge Combine refuses a mixture of artwork and guides. Pathfinder refuses the same mixture. The selection is artwork, or the selection is guides. The command tells you by not doing the weld. Fix the selection and press Ctrl+8 again. Ungroup refuses to fuse anything on the way out. Ctrl+Shift+G releases a group. It leaves paths as paths. Release of a compound is Ctrl+Shift+8, and that is the key that separates contours which were actually combined. Press Ctrl+G to group. Press Ctrl+8 when the objects should become one compound. ## The thread Part 40 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-pathfinder-ops) · [Next](/blog/omadesign-0-5-8-expand-stroke) --- # Expand stroke Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-expand-stroke Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, strokes ## The habit Expand Stroke is the command you run at the end, when a vendor, a cutter, or a decade-old RIP needs fills and will not stroke a path. In Illustrator it is Object → Path → Outline Stroke. Affinity has an equivalent expand. The stroke width, the cap, the join, and the dash pattern become shapes. A round cap becomes a round end. A miter becomes a point. A dashed line becomes a row of filled marks. You do this once, late, because after it the width is no longer a number you can type. The bug you remember is the fill. Outline Stroke in a hurry replaces the object with the outline and the fill you wanted is gone, or the outline arrives and the fill is a separate object you did not ask to manage. The correct result keeps the fill where it was and puts the new outline geometry with it. The stroke you saw is the geometry you get. A heavier stroke becomes a heavier shape. An inside stroke on a closed path becomes the band inside the path. An outside stroke becomes the band outside it. Dashes are the test. A dashed rounded rectangle, rotated, should outline into pieces that sit where the dashes sat, not into a solid band with the dash painted on as a lie. Rotation has to be respected. The pieces land on the ink. Compound strokes are the other test. A ring with a hole, stroked, outlines into a compound. The hole stays. If you need to nudge the inner and outer contours together, you want a reshape that moves them as a set, not a node drag that tears one side of the ring. ## The constraint The visible stroke is the only honest source. Omadesign already stores width, and on a closed path it stores inside, center, or outside placement. Open paths use a centered stroke. Widths above 64 pixels are allowed. Expand reads that visible result, including caps, joins, and dashes, and writes filled geometry. A second approximation, "width times two, rectangle along the path," would miss miters and dashes and would disagree with the canvas you approved. The existing fill stays beneath the new outline. One object relationship, one command. You do not lose the fill and then paste it back from a hidden copy. The outline is the stroke, promoted. The fill is the fill, left in place under it. Undo of the command returns the stroke, which is why you can expand late and still walk back if the vendor's requirement was a rumor. Layer order is respected. The new geometry does not leap to the top of the document or sink under an unrelated layer. It stays where that object's stroke was, in the stack you were looking at. Compound outlines keep their holes. A stroked counter that became a filled disk would be a different logo. Reshape is how those contours move together after the expand. The expand itself does not invent a live stroke on top of the outlines. The stroke is the geometry now. One `.oma`. The outlined result saves in the project. You are not required to outline a copy in Illustrator and place the SVG back. If you still want the live stroke, you undo, or you duplicate before you expand and keep the duplicate with its stroke. ## What landed Select the stroked artwork. Choose Object → Expand stroke to outline. The visible stroke becomes filled geometry. Caps are in that geometry. Joins are in it. Dashes are in it. A dashed stroke becomes the dash shapes you were looking at, not a continuous ribbon. Rotation is respected, so a turned path outlines where it appears. The layer order you had is the layer order of the result. The fill that was already on the object stays in place, beneath the new outline. A filled circle with a heavy stroke becomes the fill you had, with the stroke's band as filled geometry along it. You can still select and recolor those pieces after the command. What you cannot do is type a new stroke width and expect the band to grow. The width was consumed into the shape of the fill. Compound outlines retain their holes. A compound path with a stroke keeps the counters in the outlined result. To move those contours together, use Object → Reshape. Distort, skew, perspective, and the warp mesh operate on the vector artwork, and a reshape moves the outlined contours as a set instead of leaving you to drag one side of a ring and hope the other side follows. The first reshape handle you move will convert remaining parameter shapes and live text, which is a different decision. On an outline you just made, you are already in geometry. Closed paths remember inside, center, and outside in the stroke you see, so the outline matches that placement. Open paths were centered, so their outline is the centered band. A line you drew with L, given a 40 pixel stroke, outlines as a 40 pixel band along that line, caps included. Pathfinder welds areas. Expand stroke promotes the stroke. Run the outline when the silhouette is final and the file has to be fills. ## In the hand Draw a rounded rectangle. Give it a fill and a dashed stroke. Rotate it with the top handle so you can see that placement matters. Choose Object → Expand stroke to outline. Look at the dashes. They should sit on the rotated corners where the dashes were. Look under them. The fill is still there, beneath the new outline. The stroke you saw is filled geometry now. The interior you had was not discarded to make room for it. Press Ctrl+Z. The stroke is a stroke again. The dash field is back. Change the width. Expand again if the new width is the one you want committed. Try a compound. Subtract a hole from a shape, give the result a stroke, and expand. The hole remains. If the inner and outer bands need to shift together, choose Object → Reshape → Distort, or whichever mode matches the shift, and move the cage. Esc cancels a bad drag. Enter finishes. Each completed drag is one undo. ``` Object → Expand stroke to outline Fill Stays beneath the new geometry Caps, joins, dashes Become the filled shapes you saw Compound holes Stay open Object → Reshape Move outlined contours together Ctrl+Z The stroke returns ``` Duplicate first, Super+D, when you need both a live stroke and an outline in the same file. Expand the copy. The original keeps its width field. Duplication stays in place, so the copy sits on the original until you move it. Save. The outlined geometry is ordinary vector in the `.oma`. SVG export will see fills. A shop that rejects strokes can take the export. You still have the project if the live stroke has to come back from an undo you have not overwritten, or from the duplicate you kept. ## The edge Expand refuses to throw away the fill. Existing fills stay beneath the new outline. The stroke becomes filled geometry on top of that relationship. You do not rebuild the interior from memory after the command. Compound outlines refuse to fill their holes. The counters stay. Reshape is how you move those contours together. A single-node nudge is the wrong tool when the ring has to stay a ring. Choose Object → Expand stroke to outline when the stroke you can see is the shape you need to keep. ## The thread Part 41 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-combine-and-release) · [Next](/blog/omadesign-0-5-8-reshape-distort-skew) --- # Reshape distort skew Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-reshape-distort-skew Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, reshape ## The habit Free transform rotates and scales. Sometimes the poster needs a skew, a fake perspective on a plane, a distort that pulls one corner, or a mesh warp that bends a wordmark like a flag. Illustrator splits these across Free Transform, Shear, and Envelope Distort. Affinity puts them on a node mode or a warp group. Photoshop's Free Transform covers scale, rotate, skew, distort, and perspective on pixels, and you learned the modifier keys there even if you do not remember the names. The part you care about is the cage. Handles appear. You drag one. The artwork follows. Shift keeps the drag straight. Enter commits. Esc bails out of the drag you are in the middle of. If Esc committed a mess, you would stop using Esc, and Esc is the key that has to stay safe. The other part is the conversion. A live rectangle and a live headline do not have a mesh. The first real drag turns them into paths. You want that to happen when you move a handle, not when you merely open the command and look. And you want undo to bring the rectangle back, and the text back, because opening warp to "see what it looks like" is normal and should not be a trap. Photographs are the boundary you already understand from Photoshop. A placed photo warps as pixels there. In a vector studio, a warp that silently rasterized the photo would be a different command. If the tool does not warp photos, it should leave them on move, scale, and rotate, the tools that already work. ## The constraint Reshape works on vector artwork in Design. The cage is a mode, not a second document. The inspector switches modes and finishes the edit, so Distort, Skew, Perspective, and Warp mesh are one menu and one place to change your mind. You do not export a mesh to a plugin and import a path. The mesh has nine handles. That is the cage you drag. Each completed drag is one undo step. You can pull a corner, release, pull another, and Ctrl+Z only the second pull. Esc cancels the drag you have not completed. If no drag is active, Esc leaves the mode. You get out without a phantom edit. Changing tools, documents, selections, or personas leaves the cage as well. You cannot strand a warp in progress by clicking Layout. Shift constrains the handle the way Shift constrains everything else you drag. Ctrl reverses snapping for that drag. Snapping is still Ctrl+Shift+; as a toggle. The hold-Ctrl rule is the temporary one, and reshape uses it so a handle can ignore a guide without you turning snapping off for the whole document. The first moved handle converts live text and parameter shapes to paths. Looking at the cage does not. Undo restores the original form. Stroke widths stay uniform. A warp that scaled the stroke with the distortion would turn a 4 pixel line into a wedge, and the poster would look like a raster filter. Radial gradients stay radial. A linear gradient on rotated artwork maps from the pose the art is actually in, so the fade does not slide off the skewed mark. Placed photographs keep Move, scale, and rotate. Reshape does not take them. One less surprise raster in the `.oma`. ## What landed Select vector artwork in Design. Choose Object → Reshape, then Distort, Skew, Perspective, or Warp mesh. The cage appears. Drag a handle. The inspector can switch modes while you are in the edit, and it can finish the edit. You are not locked to the mode you entered if the skew should have been perspective. Nine handles on the mesh. Drag the handle that sits on the part of the cage you want to bend. That is the whole mesh: nine grips, no denser grid hiding under them. Shift constrains the handle's movement. Ctrl, held during the drag, reverses snapping. Release Ctrl and snapping returns to whatever Ctrl+Shift+; had set. Enter finishes. Done in the inspector finishes too. Esc cancels the current drag and restores it. Esc with no drag active leaves the mode. Each drag you complete is one undo. Ctrl+Z removes that drag. The earlier drags stay until you undo those too. The first handle you actually move converts live text to paths and converts parameter shapes to paths. A rectangle becomes a path. A headline becomes paths. Undo of that edit restores the original form. The words come back. The rectangle's parameters come back. If you only opened Reshape and left with Esc, you never moved a handle, and you never paid that conversion. Stroke width stays uniform across the warp. Radial gradients stay radial. On rotated artwork, a linear gradient maps from the correct pose, so the endpoints follow the art you skewed. Placed photographs stay on move, scale, and rotate. Free transform, Ctrl+T, keeps live text and shape parameters editable. Reshape converts, and only after a handle moves. Use Ctrl+T when the text has to stay text. ## In the hand Set a headline with T. Select it with V. Choose Object → Reshape → Warp mesh. Look at the nine handles. Press Esc without dragging. You leave the mode. Double-click the headline. The caret still works. The type is type. Open Warp mesh again. Drag the middle handle on the top edge. That is the first moved handle. The text converts to paths. Bend until the flag shape is right. Hold Shift if a handle should move straight. Hold Ctrl if a guide is stealing the handle. Enter to finish. Ctrl+Z. The headline is text again if that drag was the conversion. That is the point of one undo restoring the original form. Redo with Ctrl+Shift+Z or Ctrl+Y if the warp was right and your hand was ahead of your judgment. Try Skew on a rectangle that is still a parameter shape. Drag. It becomes a path. Undo if you wanted the corner radius back as a number. Perspective is the same cage idea with the perspective mode. Distort pulls the free corners. Switch modes in the inspector if you started in the wrong one, and finish from there. ``` Object → Reshape → Distort Object → Reshape → Skew Object → Reshape → Perspective Object → Reshape → Warp mesh Nine handles Shift Constrain the handle Ctrl Reverse snapping while held Enter Finish Esc Cancel the drag, or leave the mode Ctrl+Z One completed drag, or the original form ``` A rotated path with a linear gradient: skew it and watch the fade. It should stay on the pose you see. If you are warping a stroked compound outline, the hole stays, and the contours move together under the cage. Save after Enter, not in the middle of a drag. The `.oma` stores the paths you finished. A photograph you wanted warped stays rectangular, at whatever scale and rotation Move gave it. ## The edge Reshape refuses placed photographs. They keep move, scale, and rotate. The cage is for vector artwork in Design. A photo in the poster does not become a mesh, and it does not get rasterized by this command. Esc refuses to commit the drag you are still holding. It restores that drag. A second Esc, with nothing in progress, leaves the mode. The first handle that actually moves is the one that converts live text and parameter shapes, and Ctrl+Z gives those back. Choose Object → Reshape → Warp mesh, drag the handle you mean, and press Enter when the bend is the one you want. ## The thread Part 42 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-expand-stroke) · [Next](/blog/omadesign-0-5-8-svg-fx-stack) --- # SVG FX stack Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-svg-fx-stack Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, effects ## The habit You select the mark. You add a drop shadow. Then a blur, or you blur the thing under it. Illustrator's Appearance panel stacks fills, strokes, and effects, and the order is the look. Photoshop stacks layer styles, and a drop shadow plus a color overlay is a list you can reorder and toggle. Affinity's Layer Effects are the same list with the same job. You expect the effect to stay live. Change the blur radius next week and the shadow updates. You do not expect to have painted the shadow into the pixels on the first try. You also expect a small, named set. Gaussian-style blur. Drop shadow and inner shadow. An offset. A thicken and a thin, which SVG calls dilate and erode. Saturation, hue rotation, brightness, contrast, invert. A color matrix when the simple sliders are not the grade. Turbulence and displacement when you want a texture that is still a filter and not a pasted scan. The stack has a target. The selection, first. Then the layer underneath, when the effect is supposed to land on what sits below. A shadow that only exists inside the object's own alpha is a different picture from a shadow that darkens the photograph under the logo. The order "selection, then the layer beneath" is the sentence you want the studio to keep. Pixel filters are a neighboring habit and a different commitment. Chroma key, a destructive blur, a sharpen: you preview, you Apply, the pixels change, one undo. That bank lives in Pixel. The Design FX stack is the live list. ## The constraint The parameters are the SVG parameters. Not a private dialect that approximates SVG on the way out. If the blur is an SVG blur, the number you set is the number SVG will see. The canvas rasterizes so you can judge the picture at the zoom you are working. The export writes `` and the `fe*` primitives. One definition, two views: pixels on screen while you design, the filter graph when you share an SVG. That choice keeps the `.oma` and the SVG from diverging into "the effect that dies on share." The graph travels. A client who opens the SVG in a tool that understands SVG filters gets the filter. A client who needs a PNG gets the rasterized look, because PNG has no filter graph. You pick the export for the consumer. You do not maintain a second effect stack for the file format. The stack is on the selected object, then the layer underneath. It is not a document-wide grade and it is not a pixel-layer filter dialog. Design stays vectors plus this filter list. Pixel keeps Raster studio → Filters and Effects, with Apply, Cancel, and a full-resolution commit. Mixing those into one menu would make every blur ambiguous. Is it live, or did it bake? The studios answer that by staying apart. Select → Same Effects compares the complete effect stack. Two objects match when the stack matches. That only works if the stack is a real property on the object, saved with the project, not a preview overlay. You can gather every logo that carries the same shadow and edit the stack once. Color for effect parameters uses the same picker as the rest of the paint. Alpha included. Current and Previous on the chip. An effect color is not a second, poorer color system. ## What landed Open the FX studio on the right. Select an object. Add SVG filter effects. They apply to the selection, then to the layer underneath. The list is: - blur - drop shadow and inner shadow - offset - dilate and erode - saturate - hue rotate - brightness - contrast - invert - color matrix - turbulence - displacement Those names are the SVG set. Dilate and erode grow and shrink the graphic the way the morphology filter does. Offset moves the result. Turbulence makes the noise field. Displacement pushes pixels of the filtered graphic with that field. Hue rotate, saturate, brightness, contrast, invert, and the color matrix are the color primitives. Blur is blur. The two shadows are the two shadows. You stack them. Same Effects treats that stack as the property. The canvas shows the filters rasterized, and the parameters stay the SVG ones you set. Underneath matters. Put the logo on the layer above the photograph. Select the logo. The FX stack hits the logo and then the layer under it, which is the photograph's layer when that is the layer underneath. A shadow can read on the photo because the target includes that layer. An object on some other layer, off to the side of the stack, is not the underneath layer. Pixel's filter list is the other door. Chroma key, levels, a gaussian blur you Apply, film grain, a swirl. Those commit pixels on the pixel layer, in the background, as one undo, and Cancel leaves the source untouched. Use them when the photograph itself has to change. Use FX when the vector artwork should keep a live filter graph. Opacity and blend mode are not this stack. They live on the object and on the layer: Normal, Multiply, Screen, and the rest. A Multiply logo with a drop shadow uses both. The blend is the blend. The shadow is an FX entry. Pass through, on an explicit group, decides whether child blends reach the backdrop. It does not replace the filter list. ## In the hand Select the wordmark. Open FX. Add a drop shadow. Set the parameters the SVG filter exposes. Look at the canvas. The shadow is rasterized there so you can see it. Add a blur if the edge should soften. Reorder your attention to the stack: selection first, the layer beneath included in the way the studio applies the list. Put a photograph on the layer under the wordmark if the shadow should fall on the photo. Confirm the layer order in the Layers studio. Eye and lock are per object. The photo has to be visible. Select the wordmark again and read the shadow on the photo. Select → Same Effects if several marks share the stack and should take the next edit together. Change the shadow once. Press Ctrl+1 and look at the edge at actual size. Press Z and Alt-click to step back out. The chrome does not zoom. The shadow does, because it is on the canvas. ``` FX studio blur, drop shadow, inner shadow, offset dilate / erode saturate, hue rotate, brightness, contrast, invert, color matrix turbulence, displacement Selection, then the layer underneath Select → Same Effects ``` Ctrl+Z walks the last FX change off the object. The stack is a property. Save the `.oma`. The filters are part of the project you reopen. Export is the next decision: SVG writes the graph, PNG and JPEG carry the rasterized look, and that split is the export's job. ## The edge The FX stack refuses to be a pixel bake. It stays a filter on the selection and then on the layer underneath. Raster studio filters, the ones you Apply in Pixel, are the bake. They have their own preview, their own Cancel, and their own commit. A drop shadow you wanted to keep editable belongs in FX. Objects that are not the selection, and are not the layer underneath that selection, are outside this stack. Put the artwork in order before you judge the shadow. Open FX, add the drop shadow on the selected mark, and leave the SVG parameters as the ones you are willing to export. ## The thread Part 43 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-reshape-distort-skew) · [Next](/blog/omadesign-0-5-8-fx-export-to-svg) --- # FX export to SVG Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-fx-export-to-svg Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, svg ## The habit You build a shadow in the design tool. You export an SVG for the web. You open the SVG and the shadow is a baked image, or it is gone, or it is a different blur than the one you approved. Illustrator has a long history of effects that are "Photoshop effects" inside an Illustrator file and a different thing, or nothing, in the SVG. Affinity's export dialog asks you, in so many words, whether effects should stay live or become raster. The honest version of that question is a rule you can remember without a dialog. The rule you want: the numbers you set are the SVG numbers. On the canvas, you see a rasterization, because the window has to show pixels. In the SVG, you get a `` and the `fe*` elements that implement it. Blur goes out as a blur. A drop shadow goes out as the shadow filter you stacked. The graph travels. There is no mystery effect that only exists inside the authoring file. Motion makes the same promise harder. Animated SVG can carry transforms, stroke and fill reveals, masks, and effects. Lottie, the Bodymovin 5.x shape export, cannot keep pixel layers, layer masks, and effects. The failure has to be a clear error. A Lottie that silently drops the shadow is how a loading animation ships without the shadow and nobody knows which export ate it. PNG and JPEG are the rest pose when you are in Motion, and they are a picture when you are not. They do not carry a filter graph. You choose them when a picture is the deliverable. You choose SVG when the filter has to remain a filter. ## The constraint One set of parameters. The FX studio's list is the SVG list: blur, drop and inner shadow, offset, dilate and erode, saturate, hue rotate, brightness, contrast, invert, color matrix, turbulence, displacement. If Omadesign stored a private "shadow distance" and tried to invent an `feDropShadow` or an `feGaussianBlur` at export time, the canvas and the file could drift. The parameters are the SVG ones from the start. Rasterize on the canvas so the eye can judge. Write `` and `fe*` on the way out so the file matches the judgment. The `.oma` is the project. SVG is an export. Native save keeps the document you were editing. Lottie is a narrower share. The exporter writes Bodymovin 5.x shape animation with trim paths and fill masks. It cannot preserve pixel layers, layer masks, and effects. It produces a clear error. You switch to animated SVG for those compositions. Animated SVG keeps masks and effects, and it outlines text in the exported file so glyph geometry and reveals match the canvas. The source text in the `.oma` stays editable. Two exports, two contracts, stated in the open. Static SVG follows the same FX rule. The filter graph is written. Masks survive SVG export, and the mask is applied before the effects. File → Export writes that file from this binary. ## What landed Build the stack in FX. The canvas rasterizes it. You see blur, shadow, turbulence, the color matrix, all of it, as pixels in the window. Zoom does not change the parameters. Ctrl+1 shows them at 100%. The rasterization is the view. It is not a destructive apply. Export SVG. The file contains `` and the `fe*` primitives for that stack. A blur you set is in the graph. A displacement you set is in the graph. The person on the other side is not dependent on Omadesign to know there was a filter. They are dependent on an SVG renderer that implements the primitives you used. That is the format's limit, and it is a better limit than a private effect blob. Export PNG or JPEG when the consumer is a slide, a social crop, or a printer who asked for a picture. Those files are the rasterized look. They do not contain ``. You knew that when you picked the format. The `.oma` still has the live stack for the next change. Export animated SVG when the composition moves and the effects have to move with it. Masks and effects are retained. Text is outlined in that export so the reveals match. The `.oma` text stays text. Export Lottie when the consumer asked for Bodymovin shape animation and the composition is shapes, trims, and fill masks. If the file has pixel layers, layer masks, or effects, the exporter errors clearly. You do not get a JSON file that pretends the shadow survived. Choose animated SVG for that piece, or remove the effect and export Lottie on purpose. View → Document conversion notes is where unsupported or converted features are listed for imports. For this export, the Lottie error is the note that matters in the moment. Read it. Switch formats. The shadow is still in the project. Same Effects still works before you export. Gather the objects with the stack you are about to share, confirm they match, then export. A stray object with a different blur would export a different graph. The select is how you audit. ## In the hand Select the logo. Add a drop shadow and a blur in FX. Look at the canvas. The shadow is visible because the view rasterized the filter. Change the blur. The canvas updates. The parameter you are changing is the SVG parameter. Choose the SVG export. Open the file in a browser or another editor that shows SVG source if you want to see the graph. You should find a `` and `fe*` elements, not an empty group where the shadow used to be. The logo in the browser should carry the filter. Export a PNG of the same artboard for the deck. The PNG looks like the canvas. It will not update if you later change the blur. The `.oma` will. Keep the project. If the logo is on a timeline, try Export Lottie… while the effect is still on it. Expect the clear error. Then File → Export animated SVG… and use that file for the motion preview. Space in Motion plays the clip so you can watch the shadow before you export. The artboard is the rest pose. The filter rides on the animated SVG. ``` FX SVG parameters, rasterized on the canvas SVG export and fe* PNG / JPEG The picture, no filter graph File → Export animated SVG Masks and effects retained File → Export Lottie Errors on pixel layers, masks, and effects ``` Ctrl+S the `.oma` after the stack is right. Export is not the save. A crash recovery looks for the project, the `.oma.swp` idle writes under `~/.local/share/omadesign/`. The SVG on the desktop is a copy of a moment. The filter you will edit tomorrow is in the project. ## The edge Lottie refuses effects. It also refuses pixel layers and layer masks. The refusal is a clear error, not a file with a hole where the shadow was. Animated SVG is the export that keeps the effects in motion. Static SVG is the export that writes `` and `fe*` for a still. The canvas refuses to pretend the window is the file format. It rasterizes so you can see. The SVG carries the graph. A PNG carries the pixels. You pick one when you export. Build the shadow in FX, export SVG, and treat the `` in that file as the effect you signed off on. ## The thread Part 44 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-svg-fx-stack) · [Next](/blog/omadesign-0-5-8-layers-eye-lock) --- # Layers eye lock Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-layers-eye-lock Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, layers ## The habit The layer row is how you pick up what you cannot click. In Illustrator the Layers panel twirls open to the objects, and you click a target circle when the thing on the canvas is behind something else. Photoshop's eye and lock are older than that. You close the eye to hide a scrap. You lock the row so a drag on the canvas cannot nudge it. Affinity's studio does the same job with the same two icons. The hand goes to the list when the canvas is crowded. You also click the name. The object on the canvas selects. You do not want a second selection model where the list highlights a row and the canvas highlights something else. One click, one selection, both places. Groups are the unit. Illustrator groups expand. You rename the group, you hide it, you lock it, you drag it up the stack, and the children come along. Ungroup is a different command. Reorder inside the group is how you fix a shadow that landed above the mark it was supposed to sit under. The reorder has to be one undo. A drag that silently reparents three objects and cannot be undone as a drag is how files get "organized" into a shape nobody can remember. Locked and hidden have to mean something to the other commands. A Same Fill that selects a locked logo, or a flip that mirrors a hidden scrap, makes the lock and the eye decorative. They are decorative in too many apps. They should be enforced. ## The constraint One `.oma`, one layer list. The list is not a sidecar and not a Creative Cloud library view. Objects you draw show up here. Eyes and locks are properties of those objects. Click a name and the canvas selection is that object, because a split brain between the list and the page would make every later command ambiguous. Which selection does Pathfinder use? The one you can see. Groups exist so paths can move, duplicate, and align as a unit. Ctrl+G makes the group. The list has to expand, rename, hide, lock, and reorder that row. Children move with it. Double-click edits one item until the next selection. Reorder is one undo step. Drag a name onto an insertion line. Or select a layer row and use Ctrl+[ and Ctrl+] to move backward and forward, and Ctrl+Shift+[ and Ctrl+Shift+] to send it to the back or the front of its group. If those brackets kept firing as object stacking while your selection was a row, you would reorder the wrong thing. Clicking an object on the canvas returns the brackets to object stacking. The list and the canvas share the chords and announce which one they mean by what you clicked last. Hidden objects and locked objects stay out of Select. All, None, Invert, Same Fill, Same Stroke, Same Effects, and the With and Without variants leave them alone. Flip leaves them alone. You hid the scrap so it would not join the recolor. You locked the grid so a flip of the logo would not mirror the grid. The commands agree with the icons. ## What landed Layers expand to show the objects on them. The disclosure is the twirl you already know. Open a layer and the objects are rows. Eye and lock sit on those rows, per object. Close the eye and the object hides. Lock the row and the object is locked. Click the name and that object selects on the canvas. You can run Move, change a fill, or open FX on what the list just handed you. Groups expand. You can rename a group, hide it, lock it, and reorder it as a unit. Hiding the group hides the unit. Locking the group locks the unit. Reordering the group moves the unit in the stack. The children stay children. Ctrl+Shift+G ungroups when the unit should stop being a unit. Ungroup does not combine paths. If the paths needed to be a compound, that was Ctrl+8, and the list will show that compound as the object it is. Drag a layer name onto an insertion line to reorder. Drag a row onto the center of a group or a Layout frame to nest it. Hold Shift and click rows to select several. A multi-object move keeps the hierarchy you built. Lock and cycle checks apply when a drop would not be a legal parent. Undo of the drag is one step, so a bad nest comes back out with Ctrl+Z. Select a layer row and press Ctrl+[ or Ctrl+] to step it backward or forward. Add Shift and it goes to the back or the front of its group. Each of those is one undo. Click any object on the canvas and the same chords return to object stacking. Watch what you clicked last. The chord is shared on purpose. The target follows the click. Opacity fades an object. The eye hides it, and a hidden object stays out of Select. The lock is the row. Guide locking is a separate switch under View → Guides. Locking a logo does not lock the ruler guides, and locking the guides does not lock the logo. ## In the hand Draw three shapes. Open Layers. Expand the layer until you see the three names. Click the middle name. The shape selects on the canvas. Close its eye. It leaves the view. Choose Select → All. It stays out. Open the eye. It is back, and it was never part of that selection. Lock the bottom shape. Select the top two on the canvas and flip them with the right-click Flip horizontal. The locked shape stays. Unlock it when you mean to edit it. The lock is the row, not a mode you have to remember later. Select all three and press Ctrl+G. The group is a row. Rename it to something you will recognize. Drag the group above another layer by its name, onto an insertion line. The whole group moves in the stack. Expand the group. Drag one child onto an insertion line inside the group if the order inside is wrong. Ctrl+Z returns that one reorder. Double-click a child on the canvas. Nudge it. Click empty canvas, or select another object. You are out of the isolated edit. The group is a unit again for the next move. ``` Click the name Select it on the canvas Eye Hide or show that object Lock Locked objects stay put Ctrl+G Group, as a row you can expand Ctrl+[ Ctrl+] Backward / forward on a layer row Ctrl+Shift+[ Ctrl+Shift+] Back / front of the group Click the canvas Brackets return to object stacking Ctrl+Z One reorder ``` Save. Eyes, locks, names, and order are in the `.oma`. Open it tomorrow and the hidden scrap is still hidden. Select → Same Fill still will not see it until you open the eye. ## The edge The eye and the lock are enforced. Hidden objects stay out of Select, including Same Fill, Same Stroke, Same Effects, and All, None, and Invert. Locked objects stay out of that selection. Flip leaves locked and hidden objects alone. A closed eye is not a dimmed object you can still recolor by accident. A lock is not a suggestion the flip command may ignore. The row is still there. You can open the eye and unlock when the edit is meant for that object. Until you do, the canvas commands that gather and mirror leave it where you put it. Click the name in the layer list when the canvas is too crowded, and trust the eye and the lock you already set. ## The thread Part 45 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-fx-export-to-svg) · [Next](/blog/omadesign-0-5-8-pass-through-blending) --- # Pass through blending Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-pass-through-blending Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, blending ## The habit Blend modes are how a logo darkens a photograph without you rasterizing the logo. Multiply, Screen, Overlay, Darken, Lighten, Color Dodge, Color Burn, Hard Light, Soft Light, Difference, Exclusion, Hue, Saturation, Color, Luminosity. Illustrator, Photoshop, and Affinity share that list closely enough that your hand knows Multiply means "burn the ink into what is underneath." The group question is the one that bites. In Photoshop a group defaults to Pass Through. Child layers inside the group blend with the world outside the group. Set the group to Normal, and the children blend with each other first, then the group blends as one result with the backdrop. That is group isolation. You use it when three Multiply shapes should darken each other and then sit on a photo as a single Normal object, instead of each one multiplying the photo. You use Pass Through when each child should reach the photo directly. Illustrator and Affinity expose the same idea under knockout, isolate, and blend-inside controls, and the names wander. The behavior you need is stable. Either the children see the backdrop, or they see their siblings and the container talks to the backdrop. You also know opacity lies in some tools. At 100% the isolation holds. At 40% the children leak. Or the object's fill, stroke, and a placed image each get the opacity applied, so a 50% object looks like 25% where they overlap. Opacity should hit the combined result once. The blend mode should be stable at every opacity, including 100%. ## The constraint Regular layers isolate. Layout frames isolate. Children inside them blend with their siblings. The layer's own blend mode, or the frame's, is what talks to artwork below. That stays true at every opacity. A frame at 100% and a frame at 40% use the same isolation rule. You do not get a surprise leak because you faded the screen mock. An explicit layer group is the object that can enable Pass through. Ctrl+G makes that group. Pass through controls whether child blend modes interact with the backdrop outside the group. Enable it and a Multiply child reaches art behind the group. Disable it and the group isolates: children blend inside, and the group's own blend mode meets the backdrop. That is the Photoshop isolation move, done on a group, in Design, in the same `.oma` as the poster. You do not leave for a raster app to get the group to behave. Pass through is not offered as the way out of a regular layer or a Layout frame. Those containers isolate. If you need children to punch through to the page, you want an explicit group and Pass through enabled. If you need a UI frame to composite as one picture, the frame already isolates, and its opacity and blend apply to the whole subtree. Object opacity applies once to the combined fill, image, and stroke. Layer opacity applies once to the complete contents. Placed images use the layer controls above the Layers tree. These settings survive the project save and SVG export. ## What landed Every vector object and every Layout frame has Opacity and Blend in its transform inspector. The blend list is Normal, Multiply, Screen, Overlay, Darken, Lighten, Color Dodge, Color Burn, Hard Light, Soft Light, Difference, Exclusion, Hue, Saturation, Color, and Luminosity. Placed images take opacity and blend from the layer controls above the Layers tree. A frame's opacity and blend apply to its complete subtree. An object's opacity applies once to its combined fill, image, and stroke. A layer's opacity applies once to everything in the layer. Set 40% and you get 40% of the combined result, not 40% of each piece stacked again. Regular layers and Layout frames isolate their contents. Child blend modes interact with siblings in the same container. The container's blend mode interacts with artwork below. This holds at every opacity, including fully opaque. Explicit layer groups can enable Pass through. With Pass through on, child blend modes interact with the backdrop outside the group. Disable Pass through for isolated group blending. The children resolve against each other. The group then uses its own blend mode on the way out. Photoshop users will recognize the isolated group. The control here is Pass through, and disabling it is the isolation. The group is still a group. It expands, renames, hides, locks, and reorders as a unit. Pass through does not combine paths and does not release the group. Ctrl+Shift+G ungroups. The blend switch is about compositing, not about geometry. A drop shadow in FX and a Multiply blend can both sit on the logo. The filter is the filter. The blend is the blend. One object gets one blend and one opacity. ## In the hand Place a photograph. Draw three overlapping shapes above it, on their own layer or in a group you are about to make. Set each shape to Multiply. If they live on a regular layer, they multiply against their siblings, and the layer's blend mode is what the photograph sees. Set the layer to Normal and the photo sees the already-composited layer. That is isolation, and you did not have to hunt for it. Regular layers isolate. Select the three shapes and press Ctrl+G if you want the explicit group. Enable Pass through. Each Multiply shape now reaches the photograph. The overlaps on the photo look like three separate multiplies. Disable Pass through. The three shapes multiply against each other inside the group, and the group meets the photo with the group's blend mode. Set the group to Normal. The photo is no longer multiplied by each child. It sees the group. Change the group's opacity to 50%. The isolation stays. You do not get the children leaking onto the photo because the group faded. Put the opacity back to 100% when you have seen it. On a single shape, set opacity to 50% with both a fill and a stroke. The fade hits the combined paint once. The overlap between fill and stroke does not go twice as thin. ``` Blend Multiply, Screen, Overlay, and the rest Regular layer, frame Children blend inside; the container meets the backdrop Ctrl+G Explicit group Pass through on Child blends reach outside the group Pass through off Isolated group blending Opacity Once, on the combined result ``` SVG export keeps the blend and the opacity. Save the `.oma` first. The export is a copy of the compositing you set, not a second place to author it. If a screen mock should isolate, leave it as a Layout frame. If a cluster of inks should each reach the photo, group them and enable Pass through. Undo is one step if the last change was the blend or the opacity. Ctrl+Z returns the previous mode. The geometry stays. ## The edge Pass through belongs to explicit layer groups. A regular layer will not pass child blends through to the backdrop. A Layout frame will not either. Both isolate. Children meet siblings. The container meets what is below, at every opacity. Disable Pass through when the group should isolate. Enable it when each child should blend with the backdrop. The switch does not weld paths, and it does not change the layer order. It changes who the blend modes can see. Press Ctrl+G on the inks that should reach the photograph, and set Pass through to the compositing you actually want. ## The thread Part 46 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-layers-eye-lock) · [Next](/blog/omadesign-0-5-8-ruler-guides) --- # Ruler guides Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-ruler-guides Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, guides ## The habit You pull a guide out of the ruler. Top ruler, horizontal line. Left ruler, vertical line. Illustrator has done this forever. Affinity has done this forever. Photoshop has done this forever. You drag the guide to the margin, the column, the cap height. You drag it again when the grid changes. You do not open a dialog to type an x coordinate unless the coordinate is the spec and the drag was the sketch. Removing one is three gestures, and you use all of them. Select it and press Delete. Drag it back onto the ruler, or off the canvas, when your hand is already dragging. Right-click and remove it when the guide is under something and Delete would hit the artwork. Clear All is the fourth, for the end of a layout when the construction lines should go and the art should stay. Show and hide is Ctrl+; in Illustrator. You learn it because clients look at guides and think they are rules. The hide has to be real. A hidden guide that still snaps, or still catches the pointer, will yank a logo onto a margin you cannot see. You will blame the mouse. The guide was the problem. Lock is the partner habit. You lock guides so a drag meant for the headline does not grab the margin. In this studio guides start locked. View → Guides → Unlock all guides is the door when a rail has to move. Lock all guides when the type is what you are finishing. That default is the point. Layout rails stay put while you set the letters. ## The constraint The rulers are on the canvas, in the same window as the art. A guide is a document object in the `.oma`, not a decoration of the view that vanishes on save. You hide it for a screenshot of the canvas. You clear ruler guides when the phase is over. You do not get a second "guide layer" file. Ctrl+; shows or hides ruler guides and object guides together. Object guides are the ones you made with Convert selection to guides. One chord hides both. Snapping's chord is Ctrl+Shift+;, the same key with Shift, so the two switches stay different. Hidden guides do not capture the pointer and do not participate in snapping. The hide is a real hide. You can drag a shape across a hidden margin and it will not stick. Show them again and snapping can see them. Snapping's own toggle remains Ctrl+Shift+;. Hold Ctrl during a drag to reverse snapping without hiding the guides. Two mechanisms. Hide when you do not want to see them. Reverse snapping when you want to see them and ignore them for one drag. Guides start locked. The ruler context menu and Object → Guides provide lock, unlock, and clear-all. 0.5.8 puts Lock all guides and Unlock all guides on View → Guides as separate commands, so you are not toggling blind. You unlock, you nudge a rail, you lock again, you set type. A single vague lock item is how people think they unlocked and did not. Clear ruler guides clears the guides you pulled from the rulers. It lives on View, and show, hide, and clear also live on the ruler context menus. Dragging a guide outside the canvas removes it. The pasteboard is not a storage shelf for guides you might want later. If you want it later, undo, or pull a new one. Delete on a selected guide removes that guide. The artwork selection is a different target. Select the guide, then Delete, so you do not delete a path that happened to be under the cursor. ## What landed Drag from the top ruler. You get a horizontal guide. Drag from the left ruler. You get a vertical guide. Drag an existing guide to move it, once guides are unlocked. Select a guide and press Delete to remove it. Drag it outside the canvas to remove it. Use its context menu to remove it. View → Clear ruler guides clears the ruler guides. Ctrl+; shows or hides ruler guides and object guides. The keys list it as Guides. Press it again to bring them back. While they are hidden they do not take clicks and they do not snap. View → Guides → Lock all guides and Unlock all guides are the 0.5.8 commands. The ruler context menu and Object → Guides still lock, unlock, and clear. Guides start locked, so a fresh document will not let a stray drag slide the rails. Unlock all when you are placing them. Lock all when you are setting type on top of them. Snapping uses guides, along with object and artboard edges and centers, the grid, and equal spacing. Alignment lines and gap measurements appear as you move. A visible, unlocked guide is a target you can feel. A hidden guide is not a target. Ctrl+Shift+; turns snapping off entirely when the session should be freehand. Hold Ctrl during the drag when only this drag should ignore the snap, or honor it if snapping was off. Release Ctrl and the toggle's choice returns. Shift still constrains the artwork you drag against the guides: horizontal, vertical, or 45 degrees. The guide holds the position. Shift holds the angle of the move. ## In the hand Open a poster. Guides start locked. Choose View → Guides → Unlock all guides before you place the rails. Drag from the top ruler and put a horizontal guide on the cap line. Pull a vertical guide from the left ruler for the left margin. Drag either guide again until it sits. Place the headline with T. When the type is what matters, View → Guides → Lock all guides. A drag on the canvas is for the letters. The rails stay. Press Ctrl+; before you show someone the canvas. The guides leave. Drag the headline a little. It should not snap to the hidden margin. Press Ctrl+; again. The guides return. If you want them visible and still want one free drag, hold Ctrl while you drag. Select one guide. Press Delete. It is gone. Ctrl+Z brings it back if you still needed the margin. Drag another guide off the canvas and release. That one is gone without Delete. Right-click a guide and use the context menu when Delete feels risky. When the construction phase is over, View → Clear ruler guides. The ruler guides leave. Object guides you made from artwork are the other family. Clear ruler guides is aimed at the ones from the rulers. Converted contours stay until you release them or remove them on their own terms. ``` Drag from the top ruler Horizontal guide Drag from the left ruler Vertical guide View → Guides → Unlock all Move them View → Guides → Lock all Leave them while you set type Delete Remove the selected guide Drag off the canvas Remove it Ctrl+; Show or hide ruler and object guides View → Clear ruler guides Clear the ones from the rulers ``` Ctrl+S. The guides stay in the `.oma`. PNG, JPEG, SVG, and Lottie leave them out. ## The edge A hidden guide refuses the pointer and refuses snapping. Ctrl+; is a real hide, for ruler guides and for object guides together. You do not discover an invisible snap in front of a client. A guide dragged outside the canvas is removed. The area past the page is not a drawer. Delete and the context menu remove a guide too. View → Clear ruler guides clears the ruler set. Undo if the clear was early. Drag the next guide out of the ruler, then View → Guides → Lock all guides before you go back to the type. ## The thread Part 47 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-pass-through-blending) · [Next](/blog/omadesign-0-5-8-convert-selection-to-guides) --- # Convert selection to guides Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-convert-selection-to-guides Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, guides ## The habit Sometimes the guide is not a straight line from the ruler. It is the curve of a logotype, the bowl of a letter, the edge of a compound path, the bounding box of a photograph. In Illustrator you select the path and choose View → Guides → Make Guides. The path stops printing and starts snapping. Release Guides turns it back into artwork, and if you are lucky the style comes back with it. A Bézier should stay a Bézier. Text should stay text. A compound should keep its holes. You edit guides. You move one. You pull a node on a curved guide so the snap follows the curve. You release it when the construction line turns out to be the final mark, and the edits you made as a guide are still in the artwork. A placed photo is the exception you want handled literally. The pixels stay. A guide appears around the bounds. You snap to the frame of the picture. You do not trace the photograph into vectors by asking for a guide. Exports forget guides. PNG, JPEG, SVG, Lottie. The `.oma` remembers them. That split is the whole reason guides exist. Construction in the working file. A clean picture on the way out. ## The constraint Convert selection to guides has to be reversible inside one undo model, and the reverse has to include the edits. Object → Guides → Convert selection to guides turns vector artwork into editable, non-printing contours. Release guides restores them as artwork, including geometry edits you made while they were guides. Both actions undo normally. Ctrl+Z after a convert gives you the artwork back as it was. Ctrl+Z after a release gives you the guides back. You do not keep a hidden duplicate "just in case the release is lossy." The command is not lossy on the data it claims to keep. What it keeps: curves, compound paths, shapes, and live text keep their original data and style. Béziers stay Béziers. Text stays text. Compounds keep their holes. A convert that ran Convert to path in secret would destroy the headline to make a snap target. Flip is the command that asks you to convert text to paths first. This one does not. You can guide a live headline, snap other type to its cap line or its curve, and release it later as type you can still retype. Move, Node, and Reshape still edit a guide. Snapping follows the actual curve, including a Shift-constrained drag. A curved guide that snapped as if it were its bounding box would be a ruler guide with extra steps. The point of converting a path is the path. A placed image does not become a vector trace. It gets a separate guide around its bounds. The pixels stay. You still have the photograph. You also have a rectangular construction line on its frame. PNG, JPEG, SVG, and Lottie leave guides out. The project save keeps them. Hidden guides, Ctrl+;, do not take the pointer and do not snap. Guides start locked, so a node edit waits on View → Guides → Unlock all guides. ## What landed Select vector artwork. Choose Object → Guides → Convert selection to guides. The vectors become non-printing contours. They stay editable. A curve is still a curve. A compound path still has its holes. A parameter shape still has its data. Live text still has its characters and its style. The look you had is the look the guide keeps, and it does not print. Move, V, still moves a guide when guides are unlocked. Node, A, still edits points and handles. Reshape still runs on vector artwork, and the first handle you move still converts live text and parameter shapes to paths. Undo restores the original form. If you only needed to move or edit nodes, stay on V and A. Snapping follows the real curve, including a Shift-constrained drag. Alignment lines and gap measurements still appear. A hidden guide does not join that set. Release guides restores the artwork. Geometry you edited while it was a guide is in the restored art. The style is intact. A headline you did not warp is still a headline. A compound still has its counters. Both the convert and the release are normal undo steps. A placed image takes a different result. The command creates a separate guide around the image bounds and retains the pixels. The photograph is still the photograph. The guide is the frame. You can snap to the frame. You can move the guide. The pixels were not outlined. Project saves keep the arrangement. Reopen the `.oma` and the guides are guides, edits included. Exports omit them: PNG, JPEG, SVG, animated SVG, Lottie. Ctrl+; hides them with the ruler guides. View → Clear ruler guides is the ruler set. A converted logo stays until you release it or remove it yourself. Combine and Pathfinder refuse a mixture of guides and artwork. A converted guide selected together with a live shape will not boolean and will not combine. Select guides with guides, or release the guide back to artwork and then run the boolean. The refusal is the fence between construction and ink. ## In the hand Select the logotype paths. Object → Guides → Convert selection to guides. The contours stop being ink. Pull a headline with T and drag it until it snaps to the curve. Hold Shift if the headline's move should stay horizontal while it catches that curve. Press A if a node on the guide should move. Unlock guides first if the drag will not take: View → Guides → Unlock all guides. Move the node. The snap target is the new curve. Lock all guides when the curve is right. Choose Release guides when that logotype should be ink again. The node you moved is still moved. The fills and strokes you had are back on a printing object. Press Ctrl+Z if the release was early. You are on guides again, edit included. Select a placed photograph. Convert selection to guides. You get a guide on the bounds. The photo is still visible as pixels. Snap a caption to the frame. The pixels did not become paths. ``` Object → Guides → Convert selection to guides V / A / Reshape Still edit, guides unlocked Release guides Artwork again, edits kept Ctrl+Z Convert or release, one step Ctrl+; Hide, including these guides PNG JPEG SVG Lottie Guides left out ``` Save. Reopen once if you do not trust guides yet. They are there. Export a PNG and look at it. The construction curve is not in the picture. The `.oma` still has it the next time you need to align a new line of type. If the selection will not combine with a shape, it is still a guide. Release guides, then Ctrl+8 or Pathfinder, when you mean to weld ink to ink. ## The edge Exports refuse guides. PNG, JPEG, SVG, and Lottie leave them out. The `.oma` keeps them, including curves, compounds, shape data, live text, and the geometry edits you made before release. You do not strip guides by hand to get a clean file. A placed image refuses to be traced by this command. The pixels stay. A separate guide appears around the bounds. Convert to path is still the explicit way to outline live text when a flip or a committed warp needs outlines. Convert to guides leaves the text as text. Select the curve, choose Object → Guides → Convert selection to guides, and snap the next object to the contour you can still edit. ## The thread Part 48 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-ruler-guides) · [Next](/blog/omadesign-0-5-8-guides-survive-save) --- # Guides survive save Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-guides-survive-save Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, guides ## The habit You drag a guide down from the top ruler because the headline has to sit on a line you can trust tomorrow. In Illustrator that line is part of the file. You save. You quit. You open the same file after lunch and the guide is still at the measure you set. You export a PNG for the deck, or an SVG for the site, and the guide is gone. It was never ink. It was a rail. Affinity works the same way in the hand. Guides live with the document. They do not print. Hide them when a client is looking over your shoulder, and they stop grabbing the cursor. Show them again and the rails are where you left them. The photograph you dropped on the board stays a photograph. You do not trade the pixels away to get a box around them. That is the job. The rail has to survive the file you keep. The rail has to disappear from the file you send. Hiding it has to be a real off switch, not a dimmer that still catches clicks and still tugs the next drag onto the line. ## The constraint Omadesign is one binary and one document. The project is a `.oma`. Vectors, pixel layers, frames, and the motion clip live in that file. There is no second "guides" file to lose in a folder, and there is no cloud round trip that has to remember your margins for you. Idle for a second writes a recovery snapshot beside the app data. Save deletes that swap and the `.oma` is the record. Undo is one step. Turning artwork into guides, releasing those guides, and clearing them each have to come back as a single undo, with style and geometry intact. A dialog that asks you to "include guides in the export" would put the decision in the wrong place. The export writers already know what a delivery file is. PNG, JPEG, SVG, and Lottie are the picture. The `.oma` is the studio. A placed image makes the constraint sharper. The camera file, or the PNG you placed, is pixels. A guide is a contour. If "make a guide from this photo" replaced the photo, you would have a rectangle and a hole in the layout. The document has to keep both. ## What landed Ruler guides and object guides both survive a project save. Drag from the top ruler for a horizontal guide. Drag from the left ruler for a vertical one. Drag an existing guide to move it. Select one and press Delete, drag it off the canvas, or use its context menu to remove it. View offers Clear ruler guides. Those ruler guides are stored with the document. Reopen the `.oma` and they are on the same positions. Object guides are the other kind. Object → Guides → Convert selection to guides turns vector artwork into editable, non-printing contours. Curves stay curves. Compound paths keep their holes. Shapes keep their parameters until you edit them into a path. Live text keeps its text and its style. Move, Node, and Reshape still edit the guide. Snapping follows the actual curve, including a Shift-constrained drag. Release guides restores that artwork, including geometry edits you made while it was a guide. Both actions undo in the normal way. The status line tells you which kind of conversion you just did. Artwork becomes "editable guides · original artwork preserved." A placed image becomes a count of guides with "image artwork kept." The image does not turn into the guide. The guide is a separate contour around the image bounds. The pixels stay on the layer. `Ctrl+;` shows or hides ruler guides and object guides. The status line says "Guides shown" or "Guides hidden." Hidden guides do not capture the pointer, and they do not participate in snapping. Hide is off. Show brings the same rails back, because they were never deleted. If you convert a selection while guides are hidden, the conversion shows them again so you can see the thing you just made. If guides are locked, the new object guides are created and then dropped from the selection, so a locked board does not hand you a guide you are about to nudge. Lock itself is the next control. This one is about persistence. Combining and releasing compound paths keeps guide state, rotation, stacking, and gradient endpoints in one undo step. The operation wants either artwork or guides. A mix of the two is refused, with a clear status, so a compound does not silently swallow a rail into a filled shape or the other way around. ## In the hand Open the poster. Turn rulers on from View if the top and left edges are bare. Drag down from the top ruler and park a horizontal guide on the cap height. Drag right from the left ruler and park a vertical guide on the left margin. Right-click a ruler if you need the unit readout to match the job. The guides are already in the document. Select the logo lockup, the hairline rules, whatever vectors you trust as structure. Object → Guides → Convert selection to guides. The fills stop printing as art. The contours stay. Press `A` and drag a node if a curve guide needs to sit on the real edge of a letter. Press `V` and move one if the whole rail should shift. `Ctrl+Z` returns the conversion, or the node edit, in one step. Place a photograph with File → Place, or drop it. Select it and convert again. You get a guide around the bounds. The picture is still the picture. Zoom the face. The pixels did not become a hollow rectangle. Save with `Ctrl+S`. Quit. Open the same `.oma`. The ruler guides, the converted contours, and the bounds guide around the photo are there. The text you released back to artwork with Release guides is text again, style included, including any node edits you made before the release. The menu item reads "Release guides to artwork." Hide the rails before you look at color. ``` Ctrl+; ``` The guides leave the screen. Click where a guide was. You select the artwork underneath. Drag a shape across that line. It does not snap to a hidden rail. `Ctrl+;` again and the rails return, unmoved. Export PNG, JPEG, SVG, or Lottie. Open the export. The guides are not in it. Open the `.oma` again. The guides are still in it. ## The edge The delivery writers refuse to draw guides. PNG, JPEG, SVG, and Lottie are the picture you send. Saving the project does not bake those rails into pixels inside the `.oma` either. They stay guides: editable, non-printing, present on the next open. If a line has to appear in the PNG, release that guide back to artwork first, then export. Clear is the other refusal. Clear all guides, from Object → Guides or the ruler menu, removes ruler guides and converted object guides in one step, and Undo restores them. Hide does not. Hide only keeps them out of the pointer and out of the snap until you press `Ctrl+;` again. ## The thread Part 49 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-convert-selection-to-guides) · [Next](/blog/omadesign-0-5-8-lock-unlock-guides-0-5-8) --- # Lock unlock guides 0.5.8 Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-lock-unlock-guides-0-5-8 Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, guides ## The habit You finish the grid, then you set the type. In Illustrator the guides stay visible because you still need to see the measure, and you lock them so a stray drag on a hairline does not shove the margin three pixels left. Photoshop's Lock Guides is the same muscle. Affinity's lock is the same muscle. The rails stay on screen. They stop being objects. The failure mode is a toggle you cannot see. One menu item that says Lock when they are free and Unlock when they are frozen. You click it from memory, the label flips, and you only learn which way you went when the next drag grabs a guide and the headline jumps. Then you undo the type, undo the nudge, and try to remember the lock state you had before the click. You want two commands. One locks. One unlocks. The one that does not apply sits there and does nothing. You can read the menu and know the board. ## The constraint Omadesign 0.5.8 is the release that split those commands. View → Guides now lists Lock all guides and Unlock all guides as separate items. The rest of the studio already had a single flipping label. Object → Guides still shows one button, "Lock all guides" or "Unlock all guides," depending on the current state. The ruler's right-click menu does the same. Those older entries still work. They are one control that changes its name. The 0.5.8 change is the View submenu, where both names are present at once and the one that does not match the document is disabled. Guides start locked. A new document, and a document opened into a tab, takes the startup preference, and that preference defaults to locked. The preference lives with the rest of Config, under `~/.config/omadesign` or `$XDG_CONFIG_HOME/omadesign`. Config is the omadesign menu in the title bar: font, startup mode, rulers, shortcut hints, guide locking. There is no account required to keep that preference, and there is no dialog on every new file asking whether rails should be grabbable. The default is the safe one for type. You unlock when you mean to move a rail. Lock is document state, stored with the ruler settings in the `.oma`, and it is one undo. It is not Hide. `Ctrl+;` still shows and hides. A locked guide you can see is a rail you can trust and cannot drag. ## What landed View → Guides → Lock all guides sets the document locked. View → Guides → Unlock all guides clears that. The status line says "All guides locked" or "All guides unlocked." When you lock, any guide currently in the selection is dropped, and node selection on those contours is cleared. You do not keep a live selection on something the pointer is no longer allowed to move. Hit testing skips guides while they are locked, the same way it skips them while they are hidden. A click on the rail falls through to the artwork. A marquee does not collect the guide. Object guides made with Convert selection to guides obey the same lock as the ruler guides you dragged out of the ruler. One switch covers both kinds. That is the point of "all." Lock does not delete. The positions stay. Save the `.oma` and the lock state is part of the ruler settings you reopen. Clear is the delete. Object → Guides → Clear all guides, and the same words on the ruler menu, remove ruler guides and converted object guides in one step. The status line says "All guides cleared · Undo restores them." View → Clear ruler guides is the view menu's clear. Use lock when the rails must stay. Use clear when the rails are finished. Hide remains `Ctrl+;`. Hidden guides drop out of the pointer and out of snapping. Locked guides drop out of the pointer so you cannot nudge them. Snapping to guides is its own checkbox under View: Snap to guides. If you still want objects to land on a locked rail, leave that checkbox on and leave the guides visible. You can see the line, snap to the line, and fail to drag the line. That is the type-setting posture. The Shortcut HUD along the bottom of the window keeps showing tool hints while you do this. Lock is a menu command, not a key you have to chord in the middle of a word. `F1` lists the keys. `Ctrl+/` shows or hides the HUD. Neither of those touches the lock. ## In the hand Draw the rails first, while you are still willing to move them. View → Guides → Unlock all guides. The status line confirms it. Drag a horizontal guide down from the top ruler. Drag a vertical guide out of the left ruler. Convert a logo contour if you want a curve to act as a rail: Object → Guides → Convert selection to guides. Nudge until the measure is right. `Ctrl+Z` undoes a bad nudge, one step. Then set the type. Before the first click in a text box, View → Guides → Lock all guides. Look at the menu. Lock all guides is disabled. Unlock all guides is the live command. The rails are still on screen. Click a guide. You get the object underneath, or nothing, not the guide. Drag a text frame across a margin. The guide stays. If Snap to guides is on, the text frame can still land on that margin. The margin does not come with it. ``` View → Guides → Lock all guides View → Guides → Unlock all guides ``` Need the rail to move again? Unlock all guides. Drag. Lock all guides before you go back to tracking and leading. The two commands are the whole cycle. You do not flip a single item and guess. If you prefer the ruler, right-click the top or left ruler. The menu shows one lock line, named for the action available right now, plus the unit list and Reset ruler zero. Object → Guides has the same single line, next to Clear all guides, Convert selection to guides, and Release guides to artwork. Those paths still lock and unlock. The 0.5.8 path is the one that shows both names. The startup default is locked. Open a fresh document, try to drag a guide, and the drag does not take. Unlock all guides, place the rails, lock them again. If you want new documents to start unlocked, that switch is guide locking in Config, saved under `~/.config/omadesign`. It does not rewrite guides already stored in an open `.oma`. It sets the lock those documents start with. ## The edge Lock all guides refuses to hide the rails, and it refuses to delete them. The lines stay in the file, stay on screen when guides are visible, and stop accepting the pointer. Unlock all guides is a different command. In the View submenu the command that does not match the document is present and does not run. You can read the pair and know the state. Clear is not a kind of lock. Clear removes the guides. Undo brings them back. Hide is not a kind of lock either. `Ctrl+;` takes them off the screen and out of snapping. When you are in the type, you want them visible, snapped to, and impossible to nudge. That is Lock all guides. Press it before the headline, and leave Unlock all guides for the moment you mean to move a rail. ## The thread Part 50 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-guides-survive-save) · [Next](/blog/omadesign-0-5-8-ruler-zero-and-units) --- # Ruler zero and units Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-ruler-zero-and-units Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, rulers ## The habit The zero on an Illustrator ruler is not the artboard corner forever. You drag the crosshair where the two rulers meet, drop it on the trim, or on the spine, or on the top-left of a phone screen you are drawing at two times scale. Every tick after that is measured from the place you care about. Double-click that corner and zero snaps home. You do this ten times a day and you never open a dialog to do it. Units are the other habit. A poster wants millimeters. A type spec wants points. A screen wants pixels. In Photoshop you change the ruler unit and the pixels of the photograph stay the pixels of the photograph. The ruler is a skin. Affinity is the same. Right-click the ruler, pick mm or inches, and the file does not resample. If the unit change resized the artwork, every logo you had placed would jump, and every guide you had dragged would mean something else. The hand already knows the corner. Grab the intersection. Drag. Let go. Double-click when you want the origin back. Right-click when the ticks should read in another unit. The numbers change. The drawing does not. ## The constraint Omadesign stores artwork in document pixels. Width, height, paths, type size, and guide positions are pixel values in the `.oma`. DPI is a document property, set when you pick a size or a template. It is how a physical unit is computed for display. It is not a second geometry. That forces the ruler to be a view. Changing millimeters to inches multiplies the labels by the document DPI. It does not rewrite path points, and it does not scale the artboard. A 300 DPI board and a 72 DPI board show different inch marks for the same pixel distance, because an inch is `DPI` pixels, a point is `DPI / 72` pixels, a millimeter is `DPI / 25.4` pixels, and a centimeter is `DPI / 2.54` pixels. If the document DPI is missing or not a positive number, the ruler math uses 72 for that conversion and still leaves the pixels alone. There is no Creative Cloud document setting to sync, and no modal that offers to "convert the file to inches." One binary, one file, one coordinate space. The ruler corner writes an origin into the ruler settings. Those settings save with the project. Undo restores the previous origin or the previous unit in one step, because the change is a single ruler update, the same command that locks guides or toggles their visibility. ## What landed Turn rulers on from View → Rulers if the edges are bare. The hover on that checkbox says what the corner does: drag it to set zero, double-click it to reset. The corner itself repeats the hint: "Drag to set ruler zero · double-click to reset." Drag the top-left intersection onto the canvas. Where you release becomes zero. Ticks to the right and down count up from there. Ticks on the other side count the other way. Guides you drag out of the rulers still land in document pixels. The origin only changes how the ruler numbers them, and how you read a position against the job. Double-click the corner and the origin returns to `0, 0`. The ruler menu has the same command in words: Reset ruler zero. Units are on the ruler menu and on View → Ruler units. The labels are Pixels (px), Millimeters (mm), Centimeters (cm), Inches (in), and Points (pt). Pick one. The ticks redraw. A guide that was 72 pixels from the old zero is still 72 pixels from that point in the file. At 72 DPI that distance reads as 1 inch and as 72 points. At 300 DPI it reads as 0.24 inch. The path did not move. Switch back to pixels and the tick is 72 again. Right-click the top ruler or the left ruler to open that menu without leaving the edge. The same menu holds show and hide for guides (`Ctrl+;`), the lock command, and clear. Unit, origin, and guide visibility are neighbors because they are all ruler settings. They travel with the `.oma`. Reopen the file and the zero you set is the zero you get. The unit you picked is the unit on the ticks. View → Rulers can hide the rulers entirely when you want the canvas clean. Hiding them does not zero the origin and does not switch the unit. Show them again and the same corner, the same ticks, and the same guides are back. Guides themselves can be locked, which is a separate switch. A locked guide still reads against the current unit. You just cannot drag it. The Shortcut HUD does not steal this gesture. The corner is a pointer target. Double-click is the reset. No key is required. `F1` is there if you want the rest of the map. ## In the hand Make a board at the size the job actually is. A print sheet at 300 DPI. A screen at 72 or 96. The DPI you chose is the DPI the ruler will use. Check it before you trust an inch mark. The ruler will not invent a DPI for you beyond the fallback of 72 when the number is unusable, and it will not resample the board to match a unit. ``` View → Rulers ``` Drag from the corner until the zero sits on the trim, or on the left edge of the live area, or on the center of a mark you are measuring from. Watch the ticks. They count from the drop point. Drag a guide out of the left ruler. It is a vertical line in pixels. The ruler labels it in the current unit, measured from the origin you just set. Right-click the ruler. Choose Millimeters (mm) for the print sheet, or Points (pt) for the type spec, or Pixels (px) when you are back on a screen. The artwork stays. Toggle once more if you need to read both. Each change is undoable with `Ctrl+Z`. Redo is `Ctrl+Shift+Z`. Double-click the corner when the next object should be measured from the document origin again. Or pick Reset ruler zero on the menu. The guides do not jump. Their pixel positions are unchanged. Only the numbering origin moved home. Save. The origin and the unit are in the project. The PNG you export later is pixels, guides omitted, unit labels omitted. The ruler was never part of the picture. It was how you read the picture while you drew. ## The edge Changing the unit refuses to change the artwork. Paths, type, frames, placed images, and guide positions stay in document pixels. The ruler is a display. Physical units follow the document DPI so a millimeter on a 300 DPI board is a real millimeter of that board, and the same pixel distance on a 72 DPI board reads as a longer physical length. That is the ruler telling the truth about DPI. It is not a scale tool. There is no command here that resamples, reflows, or "converts the document" into inches. If the work has to be a different pixel size, you change the artboard or the frame. If the work has to be read in points for an hour, right-click the ruler, pick Points (pt), and double-click the corner when zero needs to go home. ## The thread Part 51 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-lock-unlock-guides-0-5-8) · [Next](/blog/omadesign-0-5-8-snapping-system) --- # Snapping system Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-snapping-system Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, snapping ## The habit You drag a box in Illustrator and the pink lines appear. Edge to edge. Center to center. A gap number when three cards are about to be the same distance apart. You trust the line, you let go, and the box is where the line said. When the snap is wrong, you do not open Preferences. You hold Ctrl, or Cmd on the other platform, and the drag ignores the magnet for as long as the key is down. You release the key and the magnet returns. The preference never changed. Photoshop's extra-shift and Affinity's snapping candidates are the same idea. The candidates are the things you can see: object edges, artboard edges, centers, guides, the grid, and the gap that would make a row even. The feedback is drawn on the canvas, not described in a panel you have to glance at. If the feedback is late or greedy, you fight the tool. If the thing you are dragging can snap to itself, the box vibrates on its own shadow and you never get a clean release. The habit is a toggle you can hit without looking, and a hold that reverses that toggle for one gesture. ## The constraint One canvas has to serve Design, Layout, and Motion. The same artboard, the same guides, the same objects. A snap system that only understood rectangles would miss a curve you had converted into a guide, and a snap system that rebuilt the world on every mouse sample would hitch. So the candidates are collected and held for the gesture. Point targets are cached. The objects you are moving are left out of that frozen set. They cannot snap to their own shadow. When you release, the cache can rebuild. The toggle has to be a key, because a checkbox mid-drag is a trip to the menu. `Ctrl+Shift+;` flips snapping on or off and leaves it there. The status line says "Snapping on · hold Ctrl while dragging to invert" or the off version of that sentence. Holding Ctrl during the drag reverses whatever the current choice is. Release Ctrl and the choice you had saved comes back. The hold does not write the preference. That is the whole design. A permanent toggle for the session, and a momentary opposite for the gesture you are in. View still has the individual switches, because "snapping" is five decisions wearing one master key. You may want guides and not the grid. You may want equal gaps and not artboard centers. Those checkboxes are the saved set. The master key and the Ctrl hold operate on top of them. There is no dialog, and no account. The settings are in the session with the document. Alignment lines and gap measurements are drawn while you move, then they go away. They are not objects in the `.oma`. ## What landed Snapping looks at object edges and centers, artboard edges and centers, guides, the grid, and equal spacing between nearby objects. As you drag, alignment lines show the edge or the center you are about to hit. Gap measurements show up when the space you are about to leave matches a neighbor. Objects and guides win a tie against the grid, so a guide you placed on purpose beats a grid intersection that happens to sit nearby. Guides include the straight rails from the rulers and the contours you converted from artwork. Snapping follows the actual curve of an object guide, and a Shift-constrained drag keeps that constraint while it takes the snap. Hidden guides stay out of snapping. That is `Ctrl+;`. Lock is separate: a locked guide stays out of the pointer so you do not nudge it, and Snap to guides remains its own checkbox. Motion uses the visible animated geometry. At a given playhead, the snap targets are where the artwork is on screen, not only where it sits in the rest pose. You can land a key against the position you see. `Ctrl+Shift+;` toggles the master. The colon key with Shift and Ctrl does the same chord, so the keyboard's shifted semicolon still hits it. View mirrors the master as Disable snapping or Enable snapping, with the same shortcut written on the item. Under that item the menu says to hold Ctrl during a drag to invert snapping. Then the checkboxes: Snap to grid, Snap to guides, Snap to objects, Snap to artboards, Equal spacing. Turn one off and that candidate family drops out. The master still wraps the rest. Equal spacing also reaches tool points, not only moving objects and artboards. A pen click can find a repeated gap, and it still respects edge, center, and guide priority, and it still respects Shift. The moving selection is excluded from the target set for that drag. An artboard move excludes that artboard. You snap to the other things. You do not snap to the box in your hand. ## In the hand Draw three cards. Turn snapping on. ``` Ctrl+Shift+; ``` Read the status line. Drag the third card toward the gap that matches the first two. A measurement appears between them. Let go when the number matches. Drag the same card by its center toward the artboard center. The center line appears. Let go. Hold Ctrl and drag that card a few pixels off the line, to a place with no candidate you want. The magnet reverses while Ctrl is down. Release Ctrl. The next drag snaps again. The master never flipped. Open View and clear Snap to grid if the grid is shouting over the guides. Leave Snap to guides and Equal spacing on. Drag again. The grid stops competing. Guides and gaps remain. Put the grid back when you are blocking in a new board. Hide the guides with `Ctrl+;` and drag across where a rail was. Nothing pulls you onto it. Show the guides. The pull returns. If the rails are locked, you still see them, and Snap to guides can still use them, and you still cannot drag the rail itself. That split is what lets you snap type to a margin without moving the margin. In Motion, park the playhead where the shape has already moved. Drag another shape toward it. The candidate is the shape where it is now. The rest pose is not secretly winning. Pen work can use the same gap logic. Click a point, hold Shift if you want 45 degrees, and watch for the equal-spacing candidate. The HUD at the bottom of the window shows the modifier row while Ctrl or Shift is held, and it stays the same height so the canvas does not jump under the drag. `Ctrl+/` hides that strip if you want the room. It does not change snapping. `Ctrl+Z` undoes the move you committed. The snap lines are not a step in the history. They were feedback. ## The edge The Ctrl hold refuses to change the saved snapping choice. It reverses the master for the length of the drag. Release, and the master is what `Ctrl+Shift+;` last set, with the View checkboxes underneath it. The drag also refuses to snap a thing to itself. The objects in your hand, and the artboard in your hand, are left out of the frozen targets for that gesture. If a candidate is wrong, hold Ctrl and finish the drag. If a whole family is wrong, clear that one checkbox. If everything is wrong for the next hour, hit `Ctrl+Shift+;` and leave it off until you want the lines back. ## The thread Part 52 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-ruler-zero-and-units) · [Next](/blog/omadesign-0-5-8-shift-constrain-alt-clone) --- # Shift constrain Alt clone Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-shift-constrain-alt-clone Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, modifiers ## The habit Shift is the straight line. In Illustrator you hold it while you drag a point, a handle, a rectangle, or a selection, and the move collapses to horizontal, vertical, or 45 degrees. You hold it while you drag a brush in Photoshop and the stroke stops wandering. You do not pick a "constrain mode" from a menu. The key is down, the angle is locked, the key comes up, the angle is free. That is thirty years of the same finger. Alt is the copy. Alt-drag in Illustrator duplicates the selection and leaves the original where it was. Add Shift and the copy travels on a straight axis. You use it for a second card, a second icon, a repeated rule. The duplicate lands as a real object. One undo removes the copy, or the whole set of copies if you dragged several objects together. You do not get a dialog that asks how many copies, unless you asked for a step-and-repeat on purpose. The ordinary copy is the drag. The brush case is the one people get wrong in tools that reset the constraint to the start of the stroke. You draw a loose mark, then you want the tail of it to go straight. Shift should lock from the point where you pressed Shift, not swing the whole stroke back to the first sample. The last free point is the hinge. Everything after that hinge is horizontal, vertical, or 45 degrees. Let go of Shift and the hinge is forgotten. Press Shift again later in the same stroke and a new hinge is taken at the new last point. ## The constraint Omadesign has one pointer and one history. A constrain tool that was its own mode would fight Pen, Pencil, Brush, Move, and the artboard tool, each of which already has a key. Shift is already the modifier the Shortcut HUD is built to show. Hold Shift and the bottom strip swaps to the commands and gestures that match. The strip keeps its height, so the canvas does not jump while you are mid-drag. That only works if Shift means the same thing in each of those tools: 45-degree lock, taken from the gesture you are in. Alt-drag has to be a clone in the document. The copy is new geometry, and it does not stay linked to the original. `Ctrl+C` / `Ctrl+V` still paste from the clipboard, and objects copied inside Omadesign paste at their original positions. Alt-drag follows the pointer. Several objects in the selection duplicate together, and that duplication is one undo step. Moving a selection, or moving an artboard with its contents, also returns together on undo. The board and the things on it come back as one step. There is no dialog. The modifiers are the interface. Linux sends those keys as real events, including a release that arrives before the next frame. The gesture keeps the chord until you let go. ## What landed Hold Shift to constrain pen points and handles, pencil and brush strokes, and object or artboard movement to horizontal, vertical, or 45 degrees. Rectangle, ellipse, polygon, star, and line honor the same Shift while you drag them out. Corner radius, sides, and inner radius stay in Transform after the shape exists. Shift on the way out of the tool is the proportion or the angle. The parameters after that are numbers. Alt-drag clones the object under the drag. Add Shift and the copy is constrained. Artboards do this too: `Shift+O` is the artboard tool in Design, Alt-drag clones a board, handles scale it, the top handle rotates it. Object → Wrap selection in artboard is the other way to get a board around existing art. The clone path is the drag. During a brush stroke, a pencil stroke, a smudge, or a clone stamp, pressing Shift anchors the constraint at the last free point. The stroke does not re-aim at the first dab. The anchor is the last sample before Shift took over. From there, the pointer is pulled onto the 45-degree fan. Release Shift and that anchor is cleared. The rest of the stroke is free again. Press Shift later and a new anchor is taken. The HUD shows Shift while this is happening. It does not take the key away from the stroke. Pen uses the same angle on a click. Shift constrains the next point or the handle to 45 degrees. A twitch under 3 pixels stays a corner. Alt-drag on a handle breaks symmetry. Node uses Shift on a handle drag for that same fan. Shift-click on a point adds it to the node selection. Delete removes selected points. The angle lock does not convert the path. Ctrl during the drag still reverses snapping. Shift and Ctrl compose. Constrain the angle and, with Ctrl held, skip the magnet. Release Ctrl and snapping returns to whatever `Ctrl+Shift+;` last set. Release Shift and the angle is free. The two holds do not reset each other. ## In the hand Select the mark. Hold Alt and drag. A copy follows the pointer. The original stays. Add Shift before you let go if the copy should sit on the same baseline or the same center line. Release the mouse, then the keys. `Ctrl+Z` removes that copy. If several objects were selected, one undo removes the whole duplicated set. Draw a rectangle with `R`. Hold Shift while you drag if you want the square. Release Shift if the frame should be free. The shape remains editable. Corner dots still round it. You did not convert it to a path by constraining it. ``` Shift constrain to horizontal, vertical, or 45° Alt-drag clone Shift+Alt constrained copy ``` Take the brush in Pixel with `B`, on a pixel layer. Paint a loose edge. Mid-stroke, press Shift. The line from that moment runs straight, hinged at the last free dab. Release Shift, scrub a little, press Shift again. The new straight segment hinges at the new point. The earlier curve stays where you drew it. The same hinge works for Pencil `N`, Smudge `M`, and Clone `J`. Pen: `P`. Hold Shift on the next click. The point lands on the 45-degree line from the previous point. Hold Shift while you drag a handle. The handle locks to the fan. Alt-drag the handle if one side should break. The cubic is drawn as you go. Move an artboard with its contents. Shift keeps the board on axis. Undo puts the board and the contents back together. Alt-drag the board if you needed a second board, not a move. Watch the bottom strip. Hold Shift and the row changes. Let go and the letter keys return. The canvas does not resize to make room for the hint. If the window is small, hover + more for the overflow. The hints do not take focus from the drag. ## The edge Shift on a brush, pencil, smudge, or clone refuses to hinge at the start of the stroke. The anchor is the last free point at the moment Shift goes down. Release Shift and that anchor is dropped. The tool will not keep a stale hinge for the next wiggle. Alt-drag refuses to link the copy to the original. You get new objects, stacked with the selection you dragged, one undo for the set. It also refuses to open a count dialog. If you wanted one copy on a straight line, the keys are Alt and Shift, in the hand, for the length of the drag. ## The thread Part 53 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-snapping-system) · [Next](/blog/omadesign-0-5-8-frames-nest) --- # Frames nest Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-frames-nest Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, layout ## The habit A screen is a box that holds boxes. In Figma you press F, you drag a frame, you drag another frame inside it, and the inner one is a child. The header, the list, the card: each is a frame, and the file knows the parent. Illustrator's artboard is the cousin for print. It bounds a page. It does not parent the logo to the sidebar. Affinity's artboard is the same kind of bound. When the job is a phone screen sitting next to the icon you already drew, you want the parenting, and you want it in the file that already holds the icon. The other habit is the placeholder. A gray dashed rectangle with a name, waiting for a photograph. You drop the picture into that rectangle. The frame clips or fits the picture. The picture is in the document, not linked out to a folder the developer will not have. Undo puts the empty placeholder back if the drop was the wrong file. You also grab a bunch of existing shapes and say "make this a frame." The selection gets a parent. The shapes do not move to a new file. The poster and the mockup share a tab. ## The constraint Layout is a persona in the same binary, on the same `.oma`. Design draws the mark. Layout draws the screen. Pixel can sit on a layer in that stack. There is no export step whose job is to "send the illustration to the mockup tool," and there is no second document format for frames. Frames, their children, and the drawing are one layer tree. That forces nesting to be geometry. A frame drawn inside a frame becomes a child. Object → Wrap selection in frame parents what you already selected. Drag a layer row onto the center of a frame in the Layers studio and it nests there too. The parent is an id in the file, saved with the project. Undo removes the wrap in one step, the same way undo removes any other structure change. A placed image dropped on a frame becomes an image fill. Position, rotation, opacity, blend mode, and effects come along. An existing layer mask is baked into the image alpha, because the fill stores pixels and the mask was a separate buffer. Undo restores the original image layer and the editable mask together. Image fills are embedded. The status line says "Image placed · embedded in this document." The inspector accepts PNG, JPEG, WebP, GIF, and TIFF. The `.oma` carries the bytes. A frame's opacity and blend apply to everything inside it. An object's own opacity applies once to its combined fill, image, and stroke. Those values survive the save. The file that stores this is `.oma` format 5 or newer. Format 5 is what added frames, auto-layout, constraints, and the optional cloud metadata. An older build of the app cannot open that file. You keep the `.oma`. You do not keep a parallel "layout package." ## What landed Switch to the Layout persona. The first tools are Frame `F`, Rectangle `R`, and Type `T`. Press `F` and drag. You have a frame. Press `F` again and drag inside the first rectangle. The new frame nests. The parent clips only if you turn Clip content on. Nesting itself is the parent link. Clip is a checkbox on the frame, off until you want the children masked to the bounds. With nothing selected, the inspector says "Start with a frame" and reminds you: F to draw, T for text, Shift+A to arrange. Phone at 390 × 844, Tablet at 768 × 1024, and Desktop at 1440 × 900 insert a named blank frame. Open Fieldwork starter builds the responsive prototype at 1280 × 900, 96 DPI, as its own unsaved document. Those size buttons are blank frames. Fieldwork is a template. Object → Wrap selection in frame puts a frame around the current selection. The menu item is enabled when the selection can be wrapped. The children keep their layer order. You can drag objects between sidebar rows, and onto the center of a group or a Layout frame, to reparent them. Hold Shift while you click object rows to select several before the wrap. Image placeholders live in the inspector, under Image fill. Choose image… opens the file dialog. Replace image… is the same control once a fill exists. The fit row reads Fill, Fit, and Stretch. Fill covers the frame. Fit keeps the whole picture inside it. Stretch maps the picture to the bounds. Focus lets you slide the crop. A dashed placeholder is the stand-in until that fill arrives. When the image lands, the placeholder flag clears. If the document changed while the file was decoding, the load cancels and the status line says so. The decode runs in the background. The frame you had stays a frame. Dragging a placed image layer onto a Layout frame converts it to an image fill and keeps position, rotation, opacity, blend, and effects. The mask bake described above is part of that drop. `Ctrl+Z` restores the image layer and the editable mask together. A frame's name, size, and children save in the `.oma`. `Ctrl+N` opens another tab. The mockup and the illustration can share a tab, or live in two `.oma` files. The frame tool does not force the split. ## In the hand Open the icon file, or start from the welcome screen. + Layout opens starters. The Layout file icon opens a blank size. Either way the document can hold frames. Press `F`. Drag the phone bounds. Press `F` again and drag a header inside it. Press `T` and click in the header. Type the title. Esc or a click away finishes the text. Press `R` if you need a plain rectangle that is not a frame. Frames parent. Rectangles are shapes. Use the frame when the thing has to hold children. Select the logo on the design layer. Drag its layer row onto the center of the screen frame, or select it and choose Object → Wrap selection in frame if the logo itself should become the contents of a new frame. Save with `Ctrl+S`. For a photograph, select the frame that should show it. In the inspector, Image fill, Choose image…. Pick the file. The spinner reads "Loading image…" while it decodes. Then the fit row is live. Pick Fit if the whole photo has to be visible. Pick Fill if the frame should crop it. `Ctrl+Z` removes that fill edit. ``` F drag a frame F again drag inside to nest Object → Wrap selection in frame ``` Resize the outer frame by its handles when the screen size changes. Children with pins follow that resize. That pin behavior is the constraint system. Nesting is what gives them a parent to pin to. Clip content, if you turn it on, hides anything that draws outside the frame. Leave it off while you are still sliding children into place, so you can see what missed the box. ## The edge A frame refuses to become its own file. The nest is a parent link in this `.oma`. Export of one frame, later, writes a separate PNG, SVG, or HTML snapshot of that frame and its descendants. The project you keep editing still has the whole tree. The image drop has its own refusal. A layer mask on a placed image does not stay an editable mask after that layer becomes an image fill. The alpha is baked. Undo is the restore, one step, original layer and original mask together. If you still need to paint the mask, undo the drop, paint, and choose the fill when the mask is done. ## The thread Part 54 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-shift-constrain-alt-clone) · [Next](/blog/omadesign-0-5-8-auto-layout-stack) --- # Auto-layout stack Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-auto-layout-stack Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, layout ## The habit You have a column of cards. In Figma you turn on auto-layout, set a vertical stack, set the gap, set the padding, and the cards pack in the order they already had in the layer list. You drag one card above another in the list and the column reflows. You do not position the third card by hand after you change the gap. The frame is the layout. The children are contents. Illustrator never grew that habit in the same place as the drawing. You align once, you distribute once, and the next time the headline wraps you nudge again. Affinity's constraints and stacks live nearer the layout tools. The hand you bring here is the Figma hand: select the parent, turn stacking on, set gap and padding, let layer order be the reading order. Stretch is part of that habit. A button in a row should grow with the row. A sidebar should stay a fixed width while the content pane takes the rest. You say which axis packs, and which children fill. ## The constraint The stack has to be a property of the frame inside the `.oma`. A separate design-system file would mean the illustration, the screen, and the spacing rules diverge the first afternoon someone edits type in one of them. Omadesign already has one layer tree. Layer order is already how you send an object backward and forward: select the row and press `Ctrl+[` or `Ctrl+]`, add Shift to send it to the back or front of its group. Each reorder is one undo. The stack should read that same order. Inventing a second order, stored only in a panel, would guarantee the panel and the Layers studio disagree. So Stack children means: this frame arranges the children that are in the flow, in layer order, vertical or horizontal, with a gap and padding. The manual uses the name Stack children. The checkbox reads "Arrange children automatically." Same switch. The pack runs in the document, on resize and on edit. You see the gap on the canvas. Undo returns one change to that layout property. A plain number is enough. Layout color variables can feed a value later. They are not required for a column. Shift+A is the key, and only in the Layout persona. If a frame is selected, Shift+A turns the stack on. If you have objects selected and no frame, Shift+A wraps them in a frame and turns the stack on in the same action. The status line says "Auto layout · resize the frame to see it respond." The inspector button labeled Auto layout does that same command. The hover on the button reads "Shift+A · wrap and arrange selected objects." ## What landed Select a frame. In the inspector, under Layout, turn on Arrange children automatically. The stack controls open. The flow row is Stack, Wrap, or Grid. Stack is the single line or column the manual describes. Wrap continues onto another line. Grid uses a column count from 1 to 24. For Stack, you pick Horizontal or Vertical. Grid hides that pair and shows Columns. Wrap and Grid also grow a second gap, labeled Rows, for the cross gap. The main Gap control runs from 0 to 1000. Padding is four numbers: top, right, bottom, left, each from 0 to 1000. Align is Start, Center, End, or Stretch. Stretch on that control, or a child set to Fill on its sizing, is how a child takes the cross size of the line. Distribute is Start, Center, End, Space between, Space around, or Space evenly. Children in the flow are packed from those values, in layer order. They are not packed in the order you happened to click them. Earlier and Later, on a child, move it in that order. Detach from frame pulls it out of the parent. Absolute position, a checkbox on the child under Position in frame, takes that one child out of the flow. The stack skips it and places the others. The absolute child then follows the pin rules, Min, Max, Stretch, Center, and Scale, when the parent is resized. Leave Absolute position off when the child should sit in the gap with its siblings. Clip content is independent. A stack does not imply clipping. Turn the checkbox on when overflow should hide. Lock aspect ratio is also independent. It freezes the frame's proportion when you resize. The empty Layout inspector still offers Shift+A in the hint line, next to F and T. You can draw the frames with `F`, nest them, then stack the parent. Or select the loose objects and let Shift+A make the frame and the stack together. Resize the frame after the stack is on. The status line told you to. The children repack inside the padding. Change the gap and they repack again. Reorder a layer and they repack in the new order. `Ctrl+Z` returns the last of those edits. Hidden children and absolute children stay out of the measurement the stack uses for the flow. A child you hid does not leave a ghost gap. Show it again and it returns to its place in the order. ## In the hand Draw a frame with `F`, large enough for a column. Draw three smaller frames inside it, or three text objects, or use Wrap if they already exist outside. Select the outer frame. Press Shift+A. ``` Shift+A ``` The inspector shows Arrange children automatically checked. Set Vertical. Set Gap to 16. Set padding to 24 on all four sides, or set them independently if the top needs more air than the sides. Drag the outer frame's bottom handle. The column stays packed. The gap stays 16. The padding stays. In the Layers studio, drag the middle child above the first. The column updates. Or select the child's row and press `Ctrl+]` or `Ctrl+[`. Add Shift for front or back of the group. One undo per reorder. Clicking artwork on the canvas returns those shortcuts to object stacking, which is the same order the stack is reading. Select one child that should ignore the column, a close mark in the corner, and turn on Absolute position. The others close up. Set that child's Horizontal pin to Max and its Vertical pin to Min if it should sit in the top-right with a fixed inset. Those pin names are the constraint control. The stack no longer owns that child. Turn the flow to Wrap if the children should break onto a second line when the frame gets narrow. Set Rows to the gap between lines. Turn it to Grid and set Columns if the job is a tile of cards. Gap remains the column gap. Save the `.oma`. Reopen it. The checkbox is still on. The gap is still 16. The order is still the layer order. Nothing about the stack lived in a second tool. The Auto layout button at the top of the inspector is there when you are already in Layout and you want the command without the key. It wraps if you have no frame selected. It enables the stack if you do. ## The edge The stack refuses to invent an order. Children pack in layer order. If the reading order is wrong, change the layer order. The stack will follow. It also refuses to pack a child you marked Absolute position. That child is out of the flow. The gap math skips it. The pin on Position in frame is what moves it when the parent resizes. Turn Absolute position off to put it back in the column with its siblings, in its layer-order slot, on the next reflow. ## The thread Part 55 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-frames-nest) · [Next](/blog/omadesign-0-5-8-frame-constraints) --- # Frame constraints Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-frame-constraints Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, layout ## The habit You resize a phone frame from 390 wide to 430 wide. The close button stays 16 pixels off the right edge. The header title stays 16 pixels off the left. The hero image, the one that was inset 16 from both sides, grows by the same 40 pixels the frame grew. The wordmark in the middle stays in the middle. You set those rules once, on the child, and the parent resize does the rest. In Figma that panel is constraints: left, right, left and right, center, scale. Illustrator has a quieter version on symbols and on some layout features, and you mostly do the math yourself. Affinity's constraint panel is closer to the Figma hand. The rule you remember is the inset. Distance to an edge is sacred, or both distances are sacred and the object stretches, or the center stays put, or the object is a percentage of the parent and scales. Those rules have to live on the object you will still have tomorrow. A prototype player that forgets them when you return to the file is a demo. The file is the product. ## The constraint Omadesign stores the pin on the child, in the `.oma`, next to the parent link. Format 5 is the document version that added frames, auto-layout, and constraints. The pin is not a mode you toggle before presenting. Present is a separate button. It opens an interactive responsive preview. The pins already work on the canvas when you drag the parent's handles. Horizontal and Vertical are independent. A button can pin to the right and to the top. The inspector shows the choices under Position in frame, and only when the object has a parent frame. The menu names are Min, Max, Stretch, Center, and Scale. The manual's words are min, max, both edges, center, or scale. Stretch is the both-edges pin. The menu is what you click. The insets are what you meant. The solver is simple on purpose. It looks at the child's box before the resize and the parent's box before and after. Min keeps the child's size and the gap between the child and the parent's minimum edge. On X, that is the left inset. On Y, that is the top inset. Max keeps the child's size and the gap to the parent's maximum edge. Right, or bottom. Stretch keeps both insets. The child grows and shrinks with the parent. The size will not collapse below one pixel. Center keeps the child's size and keeps the child's center at the same offset from the parent's center. Scale keeps the child as fractions of the parent. Both the position and the size are ratios of the old span, applied to the new span. A child that started halfway across, at a quarter of the width, still starts halfway across, at a quarter of the new width. There is no dialog and no second file. One undo returns the resize, parent and affected children together. Older builds that only read formats 1 through 4 cannot open a file that has these pins. The pin is in the document, so it is still there after you save. ## What landed Select a child of a frame. The inspector section Position in frame shows Absolute position, then Horizontal, then Vertical. Each axis is a menu: Min, Max, Stretch, Center, Scale. Set them. Resize the parent frame. The child moves or scales by the rule above. If the parent is stacking its children, the stack places everyone who is still in the flow. Gap, padding, direction, and layer order own those children. The pin math runs for children the stack is not placing. Absolute position is the checkbox that takes one child out of the flow so the pin can own it. A frame with Arrange children automatically turned off uses the pins for its children directly. You do not need Absolute position in that case. You need it when a stack and a pinned corner object share a parent. Sizing sits beside this. Fixed, Hug, and Fill describe how a stacked child wants to be measured. Fill, or Align set to Stretch on the stack, makes an in-flow child take the cross size of the line. That is the stack's stretch. The Position in frame Stretch is the pin for a child outside that flow, or for a child of a frame that is not stacking. They share a word and they are different controls. Use the stack's Stretch when the object should pack with its siblings and grow across the line. Use the pin's Stretch when the object should keep both insets as the parent frame itself is resized. Min and max sizes on the child still clamp the result. A Stretch pin will not produce a zero-sized box. Hug measures the child from its contents, and it ignores absolute and hidden children when it measures, so a corner badge does not inflate the hug of a card. Responsive frame offers Phone, Tablet, and Desktop width jumps: 390, 768, and 1440, keeping the current height. + Phone and + Tablet add breakpoints at 600 and 1024. The pins are what the children do when that width changes. Dragging a handle runs the same pins. The pins save with the shape. Reopen the `.oma`. Select the child. The menus still read what you set. Drag the parent. The insets hold. ## In the hand Build a screen frame. Put a title text at the top left, 24 pixels in from the left and 16 from the top. Put a close mark at the top right, 16 from the right and 16 from the top. Put a full-bleed image under them, 16 from the left and 16 from the right, a fixed distance from the bottom. If the screen frame is stacking, select the close mark and turn on Absolute position. Do the same for any child that must use a pin while siblings pack. Then set the menus. Title: Horizontal Min, Vertical Min. Close mark: Horizontal Max, Vertical Min. Image: Horizontal Stretch, Vertical Min if it should keep its height and its side insets, or Horizontal Stretch and Vertical Stretch if it should keep every inset and grow in both axes. A logo that must stay visually centered: Horizontal Center, Vertical Center. A bar that should remain the middle third of the width: Horizontal Scale, with Vertical Min so the thickness stays put. Drag the frame's right handle outward. The title stays 24 from the left. The close mark stays 16 from the right. The stretched image's side gaps stay 16 and its width changes. The centered logo stays centered, same size. The scaled bar stays the same fraction of the width. ``` Position in frame Horizontal Min · Max · Stretch · Center · Scale Vertical Min · Max · Stretch · Center · Scale ``` `Ctrl+Z` returns the resize. The pins are still set. The undo removed the size change, not the rules. Change a pin and undo if the rule itself was wrong. Each of those is one step. Save. Reopen the `.oma` tomorrow. The menus match. The insets are still the ones you set. ## The edge The pin refuses to run on an object that has no frame parent. Position in frame appears once the object is inside a frame. Wrap it, or draw it inside, first. The pin also refuses to fight the stack for a child that is still in the flow. While Arrange children automatically is on, in-flow children are packed by gap, padding, and layer order. Turn on Absolute position for the child that must keep a Min, Max, Stretch, Center, or Scale inset when that parent resizes. The stack keeps the others. The corner object keeps its inset. ## The thread Part 56 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-auto-layout-stack) · [Next](/blog/omadesign-0-5-8-export-frame) --- # Export frame Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-export-frame Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, layout ## The habit You built the screen beside the brand board. The artboard is full of notes, alternate icons, a type ramp, two phone frames. The developer, or the slide, needs one of those phones. In Figma you select the frame and export that frame. The rest of the file stays put. You do not crop a PNG of the whole canvas in a second app and hope you hit the bounds. Illustrator's artboard export is the print version of the same idea. Export this board, not every board. Photoshop's "export selection" is the raster version. The selection is the boundary. Siblings outside it are someone else's problem, later. HTML, when you have used it, is a snapshot you can open in a browser to look at structure. It is not the production site. You still have the source file. You still edit the source file. The HTML is a picture of one frame, made of boxes, that you can hand to someone who will not open the studio. ## The constraint The `.oma` is the master. Exporting a frame has to write a different file: a PNG, an SVG, or an HTML document. It must not flatten the project you have open, and it must not delete the siblings that live next to the frame on the canvas. The boundary is the selected frame plus its descendants. Unrelated layers stay out. Sibling objects that are not inside that frame stay out. The frame's own name becomes the name of the exported document. Its bounds become the exported size. Coordinates shift so the frame's top left is the origin of that new file. That boundary is also a strip list. Guides are studio rails. They are cleared from the exported document. Comments pinned on the canvas are review marks. They are cleared too. A cloud link on the project is not copied into the snapshot. The PNG, the SVG, and the HTML are the frame's picture and structure. The `.oma` you save afterward still has the guides, the comments, the siblings, and the cloud link if you had pushed one. The command refuses to guess. If no frame is selected, the status line says "Select a frame to export" and the menu items stay disabled. If the frame's width or height is not a positive finite number, the export fails with "Frame dimensions must be finite and positive." A broken box does not become a one-pixel mystery file without telling you. HTML from this command is a snapshot. The manual lists it beside PNG and SVG: File → Export frame PNG / SVG / HTML. It writes a page you can open. It leaves the `.oma` as the file you keep editing. You look at the HTML. You edit the frame in the studio. PNG uses the same Scale row as the rest of the File export menu: 1×, 2×, or 3×. SVG and HTML do not multiply by that scale. They write the frame's vector and box structure. The file dialog is the native one. You pick the destination. The status line reports "exported" plus the path, or "write failed" / "export failed" with the reason. The open document's undo stack does not gain a step for a successful export, because the `.oma` did not change. ## What landed File → Export frame PNG…, Export frame SVG…, and Export frame HTML… sit under a Layout frame label in the File menu, below the document-wide exporters. They enable when a frame is selected. The inspector repeats them when the selection is a frame: a section titled Export frame, with buttons PNG, SVG, and HTML. Either path calls the same three commands. The export collects the frame and every descendant. Ancestor layers that are required to hold that tree come along, filtered down to those shapes. Other layers are dropped. The frame is un-parented in the copy so the snapshot is a root. Image fills that the frame's tree actually references can come along. Layout tokens are copied only if something in the export references them. You do not get the project's entire token list in a button export. The snapshot is transparent, sized to the frame's bounds, with a single artboard of that size. Then the writer for the format you picked runs on that temporary document. PNG goes through the raster exporter at the current 1×, 2×, or 3× scale. SVG goes through the frame SVG writer. HTML goes through the layout HTML writer, which turns the stack, the constraints, and the boxes into a page you can open. Guides that were ruling the screen are not in the snapshot. Comment pins are not in the snapshot. The rest of the artboard is not in the snapshot. Switch back to the canvas. All of that is still on the canvas. Save the `.oma` when you want the master. The export is already on disk as its own file. Document export is still there for the whole tab: PNG, JPEG, SVG, animated SVG, Lottie, PSD, PSB, PDF, OpenRaster. Those commands look at the document. The frame commands look at the selection. Use the frame commands when the phone is the deliverable and the scratch space around it is not. A photograph has to be an image fill or a pixel layer inside the frame's tree before it will export with the frame. A photo on another artboard stays behind. Outside the frame means outside the file. ## In the hand Select the screen frame. Click its name in the Layers studio if the click on the canvas keeps hitting a child. The inspector shows Export frame once the selection is the frame itself. Set Scale in the File menu if the PNG should be 2× or 3× for a sharp slide. Leave it at 1× for a 1:1 bitmap. Scale does not affect the SVG or the HTML. ``` File → Export frame PNG… File → Export frame SVG… File → Export frame HTML… ``` Pick a folder in the native dialog. The status line names the file you wrote. Open the PNG. You see the frame. You do not see the type ramp that was sitting 400 pixels to the left. Open the SVG in a browser or back in the studio through File → Open. You get that frame's artwork at its bounds. Open the HTML. You get a snapshot of the boxes. You do not get a project to keep designing in. Return to Omadesign. The `.oma` tab is untouched. Guides are still there. The other phone is still there. A successful export is not an undo step. Delete the PNG if the scale was wrong, change Scale, export again. Export with no frame selected and the items are gray, or the status line tells you to select a frame. Select a rectangle that is not a frame and the frame exporters stay off. Object → Wrap selection in frame if that rectangle should have been a frame. Then export. Name the frame before you export if the file name should match the screen. The snapshot takes the frame's name. Rename in the transform name field, then run the export. ## The edge Frame export refuses every object that is not the selected frame or a descendant of it. Siblings, other artboards, guides, and canvas comments stay in the `.oma` and stay out of the PNG, the SVG, and the HTML. It also refuses to turn that HTML into the master. The HTML is a snapshot of one frame. The file you reopen tomorrow, the file with the stacks, the pins, the type, and the rest of the board, is the `.oma`. Select the frame, run one of the three commands, and leave the project open. ## The thread Part 57 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-frame-constraints) · [Next](/blog/omadesign-0-5-8-layout-templates) --- # Layout templates Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-layout-templates Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, templates ## The habit You start a screen from a frame you already trust. In Figma that is a duplicated phone. In Illustrator it is last quarter's file with the words deleted. The version that does not wreck yesterday's tab is a local starter: real frames, real type, real stacking, opened as a new document. The vector side already has this as 52 designs. Welcome → + Vector, or File → Template library. Search, filter nine categories, pick a size, Use this template. They open unsaved, with editable paper, artwork, and copy. Existing work stays in its tab. Fonts come from the machine. No network. Very small sizes drop secondary lines that would be unreadable. The layout habit is the same gesture and a different set of files. A phone screen is not a poster with the type swapped. It is frames, nested, some of them already stacking, some of them already pinned. ## The constraint One template system has to live inside one binary. No download at the moment you pick a card. No account. The files are built in the app from the starter definitions and the size you asked for. They open unsaved, so saving is a decision you make with `Ctrl+S` and a path, into a `.oma` that is yours. The tab you were drawing in stays where it was. The tweet that introduced these starters folded them into "the same 52-template system." The manual draws the line, and the manual wins. + Layout on the welcome screen opens frame-based starters. + Vector opens the 52. File → Template library → Layout starters is where the frame set remains while you are already drawing. Raster is a size chooser, not a template card. The 52 are vector compositions. The layout set is five documents made of frames. Those five are: Fieldwork · responsive prototype. A complete responsive site with components, hover variants, and linked screens. Mobile screen. A phone frame with a stacked header, a hero image, and an action row. Landing hero. A wide hero with a stretching copy column and a pinned call to action. Dashboard. Sidebar navigation with a stretching content pane and metric cards. Card stack. A vertical auto-layout of product cards inside a marketing frame. They are included in the template library the way the 52 are included: local, immediate, no network, a card you click. They are not entries 48 through 52 of the vector bank. Searching the vector categories will not turn the dashboard into a poster. Opening a layout starter switches you to the Layout persona, selects the Move tool, marks the new document unsaved, and fits the canvas. The status line says "Layout template ready · frames stay nested and editable." There is also a direct button when the Layout inspector has nothing selected: Open Fieldwork starter. That one builds Fieldwork at 1280 × 900, 96 DPI. The library path lets you choose a size the way the other templates do. Blank frames, if you only wanted a sized box, are the Phone, Tablet, and Desktop buttons under that same empty inspector: 390 × 844, 768 × 1024, and 1440 × 900. Those are empty frames inserted into the current document. They are not the five starters. The starters are whole documents. ## What landed From the welcome screen, + Layout shows the frame starters. The Layout file icon next to it is a blank size chooser, for when you want an empty board and you will draw the frames yourself with `F`. From inside the editor, File → Template library, then Layout starters, lists the same five. Search and size controls belong to the library. Previews adapt to the proportions you pick. Click a card and Use this template, or double-click the card. The new document gets the frames, the stacks, and the pins the starter defined. Mobile screen is already a vertical stack. Landing hero already stretches the copy column and pins the call to action. Dashboard already stretches the content pane. Card stack is already a vertical auto-layout. Fieldwork is the long one: components, hover variants, linked screens. You edit them as ordinary frames. Shift+A, gap, padding, and the Min / Max / Stretch / Center / Scale pins are the same controls as on a frame you drew by hand. Nothing in the starter is frozen. Type in a starter is type. You replace it. Fonts resolve from the machine, the same rule as the 52. A missing face gets picked in the Character studio. The starter does not require a `.omabrand` directory to open. Existing work stays in its tab. The starter does not wipe the poster you had open. Close the starter tab if you opened it by accident. You had not saved it, so closing is a discard of a document that was never a file. The other tab is still dirty or still saved, exactly as it was. All 52 vector designs remain available from + Vector and from the template library's vector side. The weekly drop plan in the docs proposes an order and a remix prompt for each of those 52. It does not publish posts, and it does not schedule the layout starters. The layout five are in the app now, the same way the 52 are in the app now. Cloud is not involved. You can later push a project if you sign in. The act of opening a starter does not upload it. Unpublished work stays on the machine. ## In the hand You are in a logo file. You need a phone mockup beside that thinking, in its own tab. File → Template library → Layout starters. Choose Mobile screen. Set the size if the library is offering a size, or take the card's default. Use this template. The tab bar gains a document. The persona is Layout. The tool is Move, `V`. The status line confirms the frames are nested and editable. Click the header. Change the words. Select the outer frame and drag a handle if the phone size is wrong, and watch the stack and the pins respond. That response is the same system as a frame you drew with `F`. The starter only saved you the first twenty minutes of parenting. ``` Welcome → + Layout File → Template library → Layout starters ``` Want Fieldwork without browsing? In an empty Layout inspector, Open Fieldwork starter. You get the responsive prototype at 1280 × 900. Dig into a component. The frames stay frames. Save As when the exploration becomes the job. `Ctrl+Shift+S`. Want a blank phone in the current file, not a new document? Use the Phone button, 390 × 844. That inserts a frame. It does not load Mobile screen's header, hero, and action row. Use the starter when you want those children. Use the button when you want a rectangle with a name. The vector 52 are still one click away on + Vector. Open one in another tab if the campaign needs a poster and a screen. Two `.oma` files, or one file if you later paste between them. The starters did not merge those catalogs. ## The edge A layout starter refuses to replace the tab you are in, and it refuses to pretend it is one of the 52 vector designs. You get a new unsaved document, in the Layout persona, built from frames. The 52 stay the vector bank. The five names above are the frame bank. It also refuses to import someone else's tool. These are Omadesign frames. There is no Figma file reader hiding behind the card. If the structure is wrong for the job, you edit the frames, or you draw your own with `F` and Shift+A. The card is a start. The `.oma` you save is the file. ## The thread Part 58 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-export-frame) · [Next](/blog/omadesign-0-5-8-canvas-comments) --- # Canvas comments Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-canvas-comments Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, layout ## The habit Review on a screen is a pin. In Figma you click the button that is wrong, you type "this tracks too tight," and the pin stays on the frame until someone resolves it. The open count tells you the frame is unfinished. Illustrator often gets a magenta text object you hope the export hides. Photoshop keeps notes in a panel. The habit worth keeping is the pin on the canvas, attached to the frame, with a resolved state, saved in the same file as the screen. There is a second habit, the client one. They are not in your studio. They mark a flat image in a browser. Those marks belong to that export, at that version. They are not the same object as the pin you dropped while you were still moving the type. Mixing them is how a resolved thread comes back from the dead on the wrong snapshot. ## The constraint Canvas comments have to be data in the `.oma`. One document, one undo stack. A pin has a position, an author, a body, a resolved flag, an optional frame id, and a thread. Creating it, resolving it, and replying are document commands. `Ctrl+Z` returns the previous list of pins in one step. There is no separate comment database required for your own notes. Format 5, the same version that added frames, is the file that can hold them. They round-trip with the project. The author is your name if the local identity has one. Otherwise the pin says "You." That name is not a cloud login. Cloud sign-in is File → Sign in. Local notes do not wait on it. An empty body is refused. The status line says "Type a comment first." A pin with no words is noise. The click does not count. The frame link is taken from the frame you have selected, if you have one. Otherwise the pin attaches to the frame that contains the click. The inspector's job is the open count: unresolved pins on that frame. Resolve a pin and the count drops. The pin can still be drawn. It is not deleted. Cloud review is a different store, on purpose. File → Push project + review export uploads a versioned `.oma` and a flat snapshot. Reviewers mark that snapshot in the browser, or you load the same threads with Review annotations in the desktop. Those threads stay on the snapshot they were written on. A later push does not move them onto the new pixels by magic. Resolve and Reopen in that window call the cloud, then tell you to refresh. That path needs the account, the membership, and the network. The canvas pin does not. Frame export makes the split visible. Export frame PNG, SVG, or HTML clears comments from the snapshot document. The rails and the pins are studio state. The delivered file is the frame. Your `.oma` still has the pins after the export. You did not resolve them by exporting. ## What landed Write the note. Pin it on the canvas. The status line says "Comment pinned," the draft clears, and the pin is in the document. On the canvas an open pin draws as a numbered mark in orange. A resolved pin draws in green. The number is the pin's place in the list, starting at one. The position is the document point you clicked, so zoom and pan do not lose it. Save the `.oma`. Reopen. The pins are at the same points, with the same resolved flags, on the same frames. Resolve flips the flag. That edit is one undo. A reply appends to the thread and marks the pin open again, because a new sentence means the question is back. The reply needs words. A blank reply is ignored. The author on the reply follows the same rule as the pin: your identity name, or "You." The open count the inspector shows is the number of unresolved pins on the frame. Resolved pins stay in the file and stay out of that count. Hide a frame's artwork and the count is still about the pins, not about visibility of the pixels. The count is how you know the screen has leftover questions. These pins sit with the document, beside the frames. They are not layer rows you group with `Ctrl+G`, and they are not snap targets. They are marks. The cloud window is the other review tool, and it is opt-in. Sign in. Push project + review export. Invite a reviewer if the work is shared. Review annotations loads the threads for a chosen export version: author, open or resolved, body, replies. Resolve or Reopen updates that thread. The button label follows the state. The status line says "Thread updated. Refresh to load the latest review." None of that writes a canvas pin, and a canvas pin does not appear in that list by itself. Push the project when you want a snapshot. Pin on the canvas when the note is for you, in the file, while the frame is still moving. Publishing to the showcase is a further owner action. It sends a flat export and a title. Private pins stay out of that gallery. Leave the cloud signed out and the canvas pins still save in the `.oma`. ## In the hand Select the frame the note belongs to. Write the sentence in the comment draft. Click the canvas on the word, or the button, or the gap that is wrong. Read the status line. "Comment pinned" means the `.oma` has the mark. "Type a comment first" means the click was ignored and the document did not change. Look at the frame's open count in the inspector. It includes the new pin. Zoom in. The number sits on the point you clicked. Pan away and back. It is still there. Fix the tracking. Resolve the pin. The mark turns from the open color to the resolved color. The open count drops. `Ctrl+Z` if you resolved the wrong one. The flag returns with the rest of the pin list, one step. ``` Write the note Click the canvas to pin it Resolve when the frame is done ``` Save with `Ctrl+S`. The pins are in the project file, with the frames. They are not in the PNG you export from File → Export frame PNG. Open that PNG. No orange marks. Open the `.oma`. The marks are back. That split is what you want when a client should see the screen and you should still see the question. If the note is for someone else, on a version you are willing to freeze, use the cloud path. File → Sign in. Approve the code the desktop shows. Push project + review export. The reviewer marks the flat snapshot. You open Review annotations, read the thread, Resolve or Reopen, refresh. Keep using canvas pins for the questions that belong to the live file. They will not collide, because they are not the same list. Reply on a local pin when the note needs a second sentence. The pin opens again. The count comes back. Resolve it when that sentence is handled. Undo still walks those edits one at a time. ## The edge A canvas pin refuses to become a cloud thread. It lives in the `.oma`, on a document point, optionally on a frame, with an open count in the inspector. Cloud review lives on a pushed snapshot. Resolve in one place does not resolve the other. Export of the frame leaves the pins in the project and out of the PNG, the SVG, and the HTML. An empty note refuses to pin. You get "Type a comment first," and the file is unchanged. Write the sentence, click the place, and the mark is real. ## The thread Part 59 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-layout-templates) · [Next](/blog/omadesign-0-5-8-pixel-layer) --- # Pixel layer Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-pixel-layer Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, pixel ## The habit You have a logo made of paths and a photograph that needs a cleanup. In Photoshop you open the photo, you retouch, you place the logo, and the logo is either smart-object fragile or already outlines. In Illustrator you place the photo and you cannot heal a blemish without a trip to another app. The hand wants one stack. Vectors on their layers. Pixels on theirs. Eye and lock per row. Reorder with the same gestures you use for groups. Affinity Designer and Affinity Photo split this by persona and by file more often than you want on a Tuesday. You can move between them. You still feel the seam. The job is smaller than that seam. Paint on the shadow under the wordmark. Trace the painted edge back to vectors if the mark should be paths again. Leave the wordmark editable the whole time. A vector-only document has nowhere to put a dab. The first move is to add a pixel layer, then pick the brush. The paths do not convert because you added that layer. They sit under it, or over it, in the order you set. ## The constraint One `.oma` holds both. Rasters are packed in the project as PNG data. Vectors stay geometry. A paint stroke has to land on a pixel layer, because a path has no grid of samples to receive a brush. If the open document is nothing but vectors, the studio does not silently invent a bitmap the size of the canvas and hide it. You add the layer from Layers. You see the row. You know where the paint went. That also means the paint target is explicit. Brush, eraser, fill, clone, smudge, and heal look for an unlocked, visible pixel layer. If they cannot find one, the status line says so: "add a pixel layer to paint," or "Choose an unlocked pixel layer to paint," depending on the tool. The stroke does not fall through onto the paper and pretend it worked. Paper is the document background. It is not a secret layer. Undo is one step for the stroke once you release, and for adding the layer. You can delete the pixel layer later without deleting the vectors. You can trace the pixel layer back to paths with Trace `U`, or with Object → Trace to vector, which runs the same trace without switching tools. Threshold, color count, and smoothness live in the Trace studio. The vectors you already had are a different layer. Trace does not replace them unless you put the result there yourself. Persona is a lens, not a file conversion. Pixel is the persona for painting and retouching. Design is still one click away, on the same tab, on the same `.oma`. You do not save a PSD to retouch and a separate SVG to keep the mark. Save once. Both are in the project. ## What landed Paint lives on a pixel layer. In the Layers studio, the ··· menu has New vector layer, New pixel layer, and New group. New pixel layer adds a raster layer to the stack and you can select it. The row has an eye and a lock, like every other layer. Click the name to target it. Hidden and locked layers stay out of painting. The mask inspector says "Unlock this layer to paint" when the row is locked, and "Show this layer to paint" when it is hidden. Those are the gates. The Pixel persona's first tools are Brush `B`, Eraser `E`, Clone `J`, and Wand `W`. They operate on the active pixel layer. Design tools, Pen `P`, Type `T`, Rectangle `R`, keep working on vector layers in the same document. Switch persona when you want the paint tools in hand. Switch back when you want the node tool. The stack does not rearrange itself because you switched. Reorder is the same as the rest of the studio. Select a layer row and use `Ctrl+[` or `Ctrl+]`. Add Shift to send it to the back or front of its group. Drag a name onto an insertion line. Groups move with their children. A pixel layer can sit inside a group with vectors. Pass through on an explicit group controls whether child blend modes see the backdrop outside the group. Layer opacity applies once to the layer's contents. A placed image uses the layer opacity and blend above the Layers tree. A painted pixel layer uses its own pixels plus that opacity. Object masks and inside or outside strokes bump the file to `.oma` format 6 so an older build cannot quietly drop them. A document that does not need those features can stay compatible with format 5. Either way, Save writes `.oma`. The pixel layer is in that file. Export to PNG or JPEG bakes a picture. Export to SVG keeps vectors as vectors where the exporter can, and the manual is honest when a format has to turn something into pixels. The working file remains the `.oma`. Trace is the way back from paint to paths, on the active pixel layer. It does not delete the pixel layer as a side effect of making vectors. You can keep both. You can hide the paint when the paths are the deliverable. ## In the hand Open the poster. It is paths and type. Press `B`. If the status line asks for a pixel layer, open Layers, ···, New pixel layer. Click that row. Press `B` again. Paint the shadow, the grain, the repaired edge. `Ctrl+Z` lifts the stroke. The wordmark, still a vector layer underneath, did not become pixels. ``` Layers → ··· → New pixel layer B ``` Drag the pixel row above or below the type. `Ctrl+]` moves it forward. Eye it off to judge the vectors alone. Eye it on to judge the composite. Lock it when the paint is done and you are back in the type, so a later brush click does not land in the finished texture. Switch to Design. Press `P` or `T`. Draw. The pixel layer stays in the stack, untouched, until you select it again. Switch to Pixel. Press `U` if a painted shape should become vectors. Set threshold, colors, and smoothness. Object → Trace to vector does that job without leaving the tool you had, when you already know you want paths. Save with `Ctrl+S`. Quit. Open the `.oma`. The pixel layer is still a pixel layer. The paths are still paths. You did not flatten on the way out. Place a photograph with `Ctrl+Shift+P` when the paint should start from a picture. A blank layer is the other choice. Place loads in the background, then you click or drag to set it down. Undo removes the placement in one step. That placed image is its own layer. A new pixel layer is the blank one you paint. Use the one the job needs. Both can exist in the same stack. ## The edge A brush refuses to paint a vector layer, and it refuses to invent a hidden bitmap so the stroke can pretend to land. You add New pixel layer, you select it, and the dab has a home. The paths you already drew stay paths. Deleting or hiding that pixel layer does not convert, trace, or flatten the rest of the file. Trace is a separate command, on the pixel layer you point it at. The stack holds both until you decide one of them is finished. ## The thread Part 60 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-canvas-comments) · [Next](/blog/omadesign-0-5-8-brush-eraser-fill) --- # Brush eraser fill Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-brush-eraser-fill Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, pixel ## The habit The left hand already knows Photoshop's paint keys. `B` for brush. `[` and `]` for size. Shift with those brackets for hardness. `E` erases. `J` clones, and Alt-click, or Option-click, sets the source. Fill is the bucket. Smudge is the finger. You do not look down. You tap the key, you drag, you tap the bracket twice because the dab was too big, you keep going. Affinity Photo mapped a lot of this the same way, because the hand is older than the app. The failure is a brush that lives in a floating panel you have to click before the size changes, or a clone source that resets every stroke, or an eraser that appears to paint and writes nothing. You have used that eraser. The cursor moves. The pixels stay. You waste ten minutes deciding the layer is wrong, and you are right, but the tool should have said so. On Linux the keys have to be these keys. Ctrl is Ctrl. The bracket keys are the bracket keys. A studio that renamed them for taste would make every retoucher relearn a hand they have had since the late nineties. ## The constraint Pixel tools share the document with vectors, so the keys have to be persona-aware where a letter is already taken. `M` in Pixel is Smudge. Shift+M in Pixel is the marquee. Shift+J is the healing brush, which is the next tool over from Clone. `J` alone is Clone. The chord is the disambiguation. You do not get a second brush panel to resolve the collision. The stroke has to hit a real pixel buffer. The eraser used to draw into an empty preview and look busy. It now erases image pixels. That is the kind of fix you only notice because the hole is actually in the layer. Undo, Esc, a tool change, and a tab change all have to finish or cancel an in-flight stroke so you do not leave unrecorded dabs. One undo restores the stroke you committed. A half-applied scribble is not a state the file can save. Size and hardness are tool state, changed from the keyboard, bounded so a runaway key-repeat cannot make a 10,000 pixel brush. The bracket keys step size by 2 pixels, from 1 to 256. Shift plus the brackets steps hardness by 0.08, from 0 to 1. Curly braces hit the same shortcuts, because that is how the key event arrives with Shift on many layouts. The numbers live in the Brush studio too, if you want to drag them. The keys are for when you are looking at the canvas. Clone's source is an Alt-click on the active image. The source stays where you put it until you Alt-click again. Fill uses the current paint target: the pixel layer, or the mask if you have switched the inspector to Mask. There is no cloud brush library required to make a dab. Brushes you add later through plugins are extra. These five tools are in the binary. ## What landed Brush `B`. Eraser `E`. Fill `K`. Clone `J`, Alt-click to set the source. Smudge `M`. Those are the manual's keys, and they match the shortcut table. Healing is Shift+J, separate, because it blends. This set is the direct tools: paint, remove, flood, copy samples, push color. `[` and `]` change brush size. Shift+`[` and Shift+`]` change hardness. The HUD at the bottom of the window shows the active tool's gestures, and holding Shift swaps that row so you can see the hardness chord while your finger is on it. The strip does not take keyboard focus. `Ctrl+/` hides it. `F1` opens the full list. Color is the Color studio: saturation and brightness, hue, alpha, hex, swatches, recent colors. `X` swaps fill and stroke. `D` restores default fill and stroke. Open a chip and you get Current and Previous. Click Previous to return to the color from before this edit. Eight-digit hex is `#RRGGBBAA`. The brush uses that color. On a mask, the inspector offers Hide and Reveal, which set the brush to black or white, because black hides and white reveals. That is mask painting. On pixels, the chip is the paint. A marching-ant selection limits brush, fill, clone, heal, and smudge to the inside of the selection. Delete clears the selected pixels. Esc drops the ants. If you wanted to paint the whole layer, drop the selection first. `Ctrl+D` in Pixel clears a pixel selection. `Super+D` duplicates. Those two chords are easy to mix. In Pixel, Ctrl+D is deselect, not duplicate. The eraser hides on a mask. On pixels it removes pixels. Fill floods the current paint target. If the target is wrong, switch Pixels or Artwork versus Mask in the inspector before you hit `K`. Eyedropper `I` samples a color and shows it in the sidebar. If the layer is locked, hidden, or not a pixel layer, the tool tells you. "Choose an unlocked pixel layer to paint." "Choose an unlocked pixel layer to smudge." "Choose an unlocked pixel layer to clone." Add the layer, or select the one you meant. The tool will not write the paper. ## In the hand Switch to Pixel. Select the pixel layer. Press `B`. Tap `]` until the dab matches the pore or the shadow. Hold Shift and tap `[` if the edge is too hard. Paint. Release. `Ctrl+Z` if the stroke is wrong. The whole stroke comes back. ``` B brush [ ] size, 2 px steps, 1–256 Shift+[ ] hardness, 0.08 steps, 0–1 E eraser K fill J clone Alt-click clone source M smudge ``` Press `J`. Alt-click a clean patch of the wall. Paint over the socket you need gone. The samples come from that click. Alt-click again if the texture should change. Press `E` and remove a stray dab. Press `K` to fill a selection you made with the marquee or the wand. Press `M` and push a highlight along a curve. Each tool is a key. The layer stays the layer you selected. Swap color with the chip, or press `I` and click the canvas. Press `X` if you had filled the stroke chip and you meant the fill. Press `D` when you want the defaults back and you do not remember what they were. Watch the status line the first time on a new document. If it asks for a pixel layer, Layers → ··· → New pixel layer, then paint. Lock the layer when you return to type. A later `B` complains, and the finished paint stays finished. The Shortcut HUD is the reminder while you learn the chords you already knew in Photoshop. After a day you will not read it. The keys will be the keys. ## The edge These tools refuse to paint a missing, hidden, or locked pixel layer, and the eraser refuses to spend the stroke on an empty preview. You get a status line, or you get a real change in the buffer. Undo returns the stroke you committed. Clone refuses to guess a source. Alt-click sets it. Until that click, you do not have a source. Fill and brush follow the inspector's paint target. If Mask is selected, you are painting the mask. Switch back to Pixels before you expect color on the image. ## The thread Part 61 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-pixel-layer) · [Next](/blog/omadesign-0-5-8-healing-brush) --- # Healing brush Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-healing-brush Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, pixel ## The habit Clone copies pixels. Healing copies texture and lets the destination keep its color. You learned the difference the first time a clone from a cheek landed as a gray stamp on a forehead. Photoshop's healing brush is Shift+J in a lot of muscle memory, next to J for clone. Alt-click the clean skin. Paint the spot. The spot disappears and the lighting stays. Option-click on a Mac. Alt-click here. The source has to stay put for the whole stroke. If the source slid along with the brush, you would smear the blemish into the sample and then paint that smear back down. A fixed offset from the Alt-click is the contract. The place you sampled does not chase the cursor. When you release, one undo restores the entire stroke, not the last dab. A healing stroke is one decision. You judge it whole. Transparency stays transparency. A heal on a layer that already has a soft edge should not fill the empty pixels with skin. You are repairing the picture that is there. You are not inventing a rectangle of opaque paint. ## The constraint Healing reads and writes the active pixel layer inside the `.oma`. It does not open a camera raw, and it does not round-trip through an external retouch app. The photograph is either a placed image, a pixel layer you painted, or pixels you brought in from PSD, OpenRaster, or a flat file. The heal is samples in that buffer. The tool is Shift+J, and only meaningful in Pixel. `J` without Shift is Clone. The two share an Alt-click habit and they do different math, so the chord has to be stable. Shortcut handling keeps Shift variants distinct. A sloppy chord that sometimes healed and sometimes cloned would be worse than no heal at all. The source for the stroke is fixed when the stroke starts. The brush copies texture from that frozen source at the offset you set with Alt-click, and it blends that texture with the destination's local color. The manual's line is exact: sampled texture, destination local color, transparency preserved. Undo restores the whole stroke. You do not scrub history dab by dab. If you are painting the mask, heal stops. The status line says "Choose Pixels to use the healing brush." Masks are black and white coverage. Healing is a color blend. The inspector switch Pixels / Artwork versus Mask exists so you can choose. Heal will not guess. Clone has the same requirement. The manual says to choose Pixels before either brush. No source yet: "Alt-click clean texture to set the healing source." The stroke does not start. An empty source would stamp nothing and look like a bug. The message is the behavior. A selection still clips the heal, the same way it clips brush, fill, clone, and smudge. Ants up, heal inside them. Esc clears the ants if the repair should run free. ## What landed Press Shift+J on a pixel layer. The tool is Heal. Alt-click a clean patch on the active image. That click stores the source. Paint over the blemish. Each dab takes texture from the fixed source and fits it to the color under the brush. The transparent pixels around a cutout stay transparent. Release the mouse. The stroke is one history entry. `Ctrl+Z` puts the blemish back, entire stroke, source relationship and all. Paint again if the patch was the wrong texture. Alt-click a better patch first. The source buffer for that stroke is taken at the start. Paint you lay down during the stroke does not feed the next dab. You cannot heal from your own wet paint in the same gesture. That is what "source fixed for the stroke" means in the hand. The next stroke can Alt-click again, including on an area you just healed, because that click happens after the previous stroke has committed. Hardness and size are the brush keys. `[` and `]` step size by 2 pixels between 1 and 256. Shift plus the brackets steps hardness by 0.08 between 0 and 1. A soft heal for skin. A harder heal for a straight edge you are rebuilding. The Brush studio shows the same numbers. Shift during the stroke, as a constrain, hinges at the last free point, the same rule as brush, smudge, and clone. You can pull a heal along a horizontal crease without the stroke wandering. Release Shift and the hinge drops. That Shift is the angle. Shift+J was the tool change, before the stroke. Once the tool is Heal, a Shift you press mid-drag constrains. It does not bounce you back to Clone. The layer has to be a visible, unlocked pixel layer. Otherwise you get "Choose an unlocked pixel layer to paint," or the more specific line for this tool. Add the layer from Layers → ··· → New pixel layer if the document is still vectors. Placed images that are pixel layers can be healed. A vector shape cannot. If you need the repair on a photograph inside a frame as an image fill, the fill is embedded pixels of another kind. Heal targets the pixel layer you selected. Paint there, or heal before you convert a placed image into a frame fill. Raster filters are the other retouch path: blur, sharpen, chroma key, and the rest, under Raster studio, applied in the background as one undo. Healing is the brush. Use the brush when the repair is local. Use a filter when the repair is the whole selection. ## In the hand Open the portrait on a pixel layer. Zoom with `Z`, or Ctrl+scroll. Press Shift+J. Alt-click a clean patch of cheek, close to the spot, so the texture scale matches. Paint the spot in short strokes. Look at the edge. If a halo appeared, undo the stroke, soften the brush with Shift+`[`, Alt-click again, paint again. ``` Shift+J Alt-click clean texture paint the blemish Ctrl+Z restores the whole stroke ``` For a dust spot on a sky, Alt-click nearby sky, not the cloud edge. Paint the spot. The blue stays the blue of the destination. The grain comes from the sample. That is the blend. Clone, `J`, would have stamped the sample's exact color. Use Clone when you want a literal copy. Use Heal when the lighting changes across the surface and the texture should follow. If the status line asks you to choose Pixels, the inspector is on Mask. Click Pixels, or Artwork on a vector layer that has a mask you were editing. Then Shift+J again. Heal does not write the mask. Esc if a selection is clipping you and you did not want it. Or keep the selection when the repair must not spill past a product edge. Delete is not the heal. Delete clears selected pixels. Heal replaces them with blended texture. Save the `.oma`. The healed pixels are in the pixel layer. The original camera file, if this portrait also lives in Photo as a RAW, is a different session. Healing here does not rewrite that RAW. It rewrites the pixel layer in the design document. If you still need the raw file untouched, it already is. This stroke never pointed at it. ## The edge Healing refuses a moving source. For the length of one stroke the sample stays the Alt-click you set, frozen when the stroke began. Undo refuses to nibble. `Ctrl+Z` restores the whole stroke. It also refuses the mask. "Choose Pixels to use the healing brush." Switch the inspector to Pixels, Alt-click the clean texture, and paint the blemish there. ## The thread Part 62 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-brush-eraser-fill) · [Next](/blog/omadesign-0-5-8-selections-pixel) --- # Selections pixel Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-selections-pixel Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, pixel ## The habit Photoshop selections are a dialect. You drag a marquee, the ants march, Shift adds, Delete clears the pixels inside, and Esc or Ctrl+D drops the ants and leaves the picture. The selection is a fence for the next brush, the next fill, the next heal. It is a pixel fence. A path is a different command. Affinity's marching ants are the same fence. Illustrator's selection is a different animal. You click objects. You do not drag a dotted rectangle across a photograph and expect the pixels inside to become the only paintable region. When both kinds of work live in one file, the hand has to know which selection it is holding. Object selection moves shapes. Pixel selection fences samples. The wand's tolerance is the argument you always have. Too low and you click forty times. Too high and the background eats the hair. The number has to sit somewhere you can change without a modal. Next to the brush, because the wand is a pixel tool and the brush studio is already open in that persona. ## The constraint Pixel selection lives beside vector tools in one `.oma`. The same keyboard has to serve both. In Design, Shift+O is the artboard tool. In Pixel, Shift+O is the elliptical marquee. Shift+M in Pixel is the rectangular marquee. `Q` is the lasso. `W` is the wand. `M` without Shift, in Pixel, is Smudge. The persona is what makes the letter safe. You switch to Pixel before those chords mean selections. You switch to Design and Shift+O draws a board again. The ants are document state for the pixel edit, stored with the session. They are not a path in the layer tree. Save keeps the pixels you changed. The fence is how you limit the edit. Drag to select. The ants stay until Esc. Shift adds to the selection. Delete clears the selected pixels on the target layer. Brush, fill, clone, heal, and smudge stay inside the fence. A filter or effect from Raster studio also limits itself to a marquee, lasso, or wand selection, and the Strength slider blends that result back toward the source where the selection only partly covers a pixel. `Ctrl+D` in Pixel clears the pixel selection. The manual is blunt about this because `Ctrl+D` is duplicate in a lot of other software, and in this studio duplicate is `Super+D`. Press the wrong one and you will think the selection "did nothing" when it actually dropped the ants. Read the status of the canvas. Ants gone means Ctrl+D worked. The wand reads tolerance from the Brush studio. One place for the number. You do not hunt a hidden options bar. Eyedropper `I` still samples color to the sidebar, which is how you check what the wand is about to consider. There is no dialog that converts the selection into a vector as a side effect. If you wanted paths, Trace is the path tool. The selection stays a pixel fence. Object selection, Move `V`, still selects shapes when you are on vectors. The two selections are neighbors. They are not the same click. ## What landed Four tools. Marquee, Shift+M. Elliptical marquee, Shift+O. Lasso, `Q`. Wand, `W`. All in Pixel, all aimed at an unlocked visible pixel layer. If the layer is wrong, the status line says "Choose an unlocked pixel layer to select." Fix the row, then drag. Drag the marquee or the ellipse. Draw the lasso. Click the wand. Ants march around the result. Shift and drag or Shift and click adds. Esc removes the ants and leaves the pixels. Delete removes the pixels inside the ants. `Ctrl+Z` can bring those pixels back, because the clear is an edit. Esc is not an edit. Esc only drops the fence. Paint with `B` and the dab clips to the ants. Fill with `K` and the bucket stays inside. Clone and heal stay inside. Smudge stays inside. This is how you recolor a shirt without painting the wall, and how you heal a face without smearing the background. Drop the ants with Esc when the next stroke should be free. Wand tolerance is in Brush. Raise it when the click only caught a speck. Lower it when the click leaped a boundary. Click again. The previous ants are replaced or added depending on Shift. The number is the same brush panel you use for size and hardness, so it stays on screen while you work. You do not close a tool options popover to paint, then reopen it to change tolerance. Raster studio → Filters or Effects honor the same fence. Choose Mask first when the filter should hit the mask. Leave it on the image when the filter should hit the picture. Partial coverage plus Strength blends. Cancel leaves the document untouched. Apply runs at full resolution in the background and creates one undo step. The selection is the limit. The filter dialog is not a second document. In the layer list, Ctrl-click an item in Pixel to select its rendered outline. That selection stays in document coordinates when you switch targets. It is another way to fence pixels using something you already drew. Mask from item… on the layer menu can turn a chosen outline into a mask on a target. That is the mask feature. The ants themselves remain the pixel selection until you dismiss them. Vector tools do not consume this fence as a path. Move `V` still selects shapes. The pixels you already edited stay on the pixel layer either way. Drag a new fence when the next edit needs one. ## In the hand Pixel persona. Pixel layer selected. A product shot, white background, object in the middle. Press `W`. Set tolerance in Brush until a click on the white selects the white and stops at the product. Shift-click the remaining white bays. Press Delete. The white is gone. The product remains. Esc. The ants are gone. The transparency stays. ``` Shift+M marquee (Pixel) Shift+O elliptical marquee (Pixel) Q lasso W wand Esc drop the ants Ctrl+D drop the ants in Pixel Delete clear selected pixels ``` Press Shift+M. Drag a rectangle around a label you want to recolor. Press `B`. Paint. The paint stops at the rectangle. Press `E` if you overshot inside the fence. You cannot overshoot outside it. Esc. Paint a shadow that should extend past the label. It can, because the fence is gone. Press `Q`. Draw a loose loop around a strand of hair the wand ate. Shift was up, so this is the selection now. Or hold Shift as you draw if you meant to add. Heal inside that loop with Shift+J. The repair does not spill. Press `I` and click a color you might fill. Press `K`. The fill uses the current color, inside the ants. Remember Design. Shift+O over there is an artboard. If a board appeared, you were not in Pixel. Switch persona. Shift+O again. You get the ellipse. `Super+D` if you meant to duplicate a layer object. `Ctrl+D` if you meant to deselect. Say them apart once and the rest of the day is quiet. ## The edge A pixel selection refuses to be a path. It fences brush, fill, clone, heal, smudge, and the raster filters. It does not convert the logo to outlines, and it does not move vector objects. Esc and `Ctrl+D` drop the fence without requiring you to paint. The tools also refuse a layer that is not an unlocked visible pixel layer. "Choose an unlocked pixel layer to select." Point them at the row that holds the samples. The ants belong on those samples, in the same document as the paths, which keep their own selection, the object one, on `V`. ## The thread Part 63 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-healing-brush) · [Next](/blog/omadesign-0-5-8-layer-masks) --- # Layer masks Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-layer-masks Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, masks ## The habit A layer mask is how you hide pixels without deleting them. In Photoshop you click the mask thumbnail, you paint black, the picture disappears in that stroke, you paint white, it comes back. The layer thumbnail is untouched. You can throw the mask away and the photograph is the photograph you started with. Affinity's mask slot works the same. Illustrator uses opacity masks more often, and the mental model is still coverage. Black conceals. White shows. Gray is partial. You start from three places. A full white mask, everything visible, and you paint away what you do not want. A full black mask, everything hidden, and you paint in what you do. Or a mask built from the marching ants, the selection becoming the coverage you can soften with a brush. The inspector has to tell you whether the brush is aimed at the picture or at the mask. One brush, two targets. If you forget which thumbnail is active, you paint a black smear across someone's face and call it a bug. The switch should be a word: Pixels, or Mask. ## The constraint The mask is a second buffer on the layer, stored in the `.oma`, not a deleted region of the color. Remove has to put you back to the untouched layer. That requirement decides the data. Paint on the mask changes coverage. Paint on the pixels changes color. Apply, later, is the command that finally bakes coverage into alpha. Until then the original samples stay. Masks work on pixel layers and on vector layers. A vector mask is still editable coverage in the project. The brush, when the target is Mask, paints that coverage. The vector geometry stays the geometry. You do not convert the logo to pixels just because you wanted a soft edge on it. Apply to pixels is a separate, explicit bake, and it is offered for pixel layers. The status line if you ask a vector layer to bake says the mask should stay editable. That boundary belongs to the next note. This one is the mask you can still paint. Documents that use object masks save as `.oma` format 6, so an older build cannot open the file and silently drop the mask. Format 6 refuses that quiet load. Undo is one step for adding the mask, painting a stroke, and removing it. There is no sidecar mask file to lose. The layer row holds both buffers. Eye and lock apply to the layer. A locked or hidden layer does not take mask paint. The inspector says "Unlock this layer to paint" or "Show this layer to paint." SVG export keeps mask luminance, alpha, placement, and order in agreement with the canvas, masks before effects. The editable mask remains in the `.oma`. The SVG is the delivery. ## What landed Two places add the mask. The layer's context menu has a Mask submenu. In Pixel, the inspector has Add layer mask, and a ··· menu labeled as layer mask actions. The choices: Reveal all. A white mask. The layer looks unchanged. You paint black to hide. Hide all. A black mask. The layer disappears. You paint white to bring back the part you want. From selection. The current pixel selection becomes the mask. This item enables when ants exist. The status line if you fire it with no selection says "Make a pixel selection first." With a mask already present, the words change to Replace from selection, Reset to reveal all, and Reset to hide all. Same actions, honest labels, so you know you are replacing coverage and not adding a second mask. The inspector line reads Paint on, then Pixels or Artwork, then Mask. Pixels is the label on a raster layer. Artwork is the label on a vector layer. Mask enables once a mask exists. Click Mask and the brush targets coverage. The inspector then offers Hide and Reveal, which set the brush color to black or white and select the brush. The hint under them: paint black to hide, paint white to reveal. A line of small type says "Original pixels stay untouched." The eraser hides when the target is the mask. Fill, `K`, fills the current paint target. If Mask is selected, the bucket fills coverage. If Pixels is selected, the bucket fills color. Switch first. Then press the key. Add layer mask, the big button, creates a reveal-all mask and sends you into mask painting. The brush color goes black so the next stroke hides, and the status line says "Painting mask · black hides · white reveals." You can flip to Reveal before you drag if you wanted white. Grays work. A 50% dab is partial coverage. Hardness and size are the usual bracket keys. A selection still clips the stroke. You can ants-select a region and paint the mask only inside it. Ctrl-click a rendered outline in Pixel if you want the selection to come from an existing shape, then From selection. Or use Mask from item… on the layer or object menu: choose it on the source, then click the target in the layer list. The selection stays in document coordinates when you switch targets. That is the path from a vector silhouette to a pixel mask without a manual trace. Masks survive the project save. Reopen the `.oma`. Paint on still offers Mask. The coverage is the coverage you left. ## In the hand Select the pixel layer. Make the selection you trust, wand or marquee or lasso. Open the layer menu, Mask, From selection. Or use the inspector ··· and the same words. The layer's visibility now follows that shape. Click Mask if you are not already painting it. Press `B`. Tap Reveal or Hide so the color matches the edit. Soften an edge with a low hardness. `Ctrl+Z` undoes the stroke. The color pixels do not change. Only coverage does. No selection yet, and you want to paint the hide by hand: ``` Add layer mask ``` That is reveal all, and you are painting black. Drag across the background. The background drops out. Press Reveal, paint back the bit you clipped. Switch the Paint on control to Pixels. Press `B`. Paint color. The mask holds. Switch to Mask again when you need the edge. For a vector logo that needs a fade, select the logo's layer. Add layer mask. Paint the fade. The paths are still paths. Node tool, `A`, still edits them. The mask rides along. SVG export draws the coverage. The `.oma` still has the editable mask. Lock the layer when the cutout is approved. The mask paint stops, same as color paint. Unlock from the layer row when you mean to edit again. The word in the inspector is the gate, not a metaphor. Save. The mask is in the file. It is not a hidden delete. Remove mask, when you want the full layer back, is the next decision. It reveals the untouched layer. Apply to pixels is the decision that finally bakes. Leave both alone while you are still painting. ## The edge A mask refuses to delete the layer's pixels. Black hides, white reveals, the color buffer stays. The inspector says so while you paint the mask. Remove, later, shows the original layer. You are not digging samples out of an undo stack from last week. From selection refuses to invent ants. "Make a pixel selection first." Reveal all and Hide all do not need a selection. They are the full-white and full-black starts. Pick the one that matches the paint you are about to do, and keep Paint on set to Mask until you mean to change color. ## The thread Part 64 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-selections-pixel) · [Next](/blog/omadesign-0-5-8-mask-invert-apply) --- # Mask invert apply Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-mask-invert-apply Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, masks ## The habit You build the mask the long way, then you need the opposite. Photoshop's Invert on a mask thumbnail flips black and white in place. The hide becomes the show. You use it constantly on a selection you made of the background when you meant the subject. Remove Layer Mask, the one that says "without applying," throws the mask away and leaves the picture whole. Apply Layer Mask commits the coverage into the pixels and deletes the mask thumbnail. You do that when the cutout is done and you want a single buffer. The habit you want from Apply is one undo that restores both the baked pixels and the mask. The bake and the removal of the mask are one edit. Placed art makes the other demand. You mask a photograph, then you scale it, rotate it, move it. The mask has to stick to the picture. A mask that stays in document space while the photo moves is a hole in the wrong place. The coverage follows position, scale, and rotation. ## The constraint Until Apply, the color stays intact. That is the whole reason the mask is a second buffer in the `.oma`. Invert flips values in that buffer. It does not touch the picture. Remove drops the buffer. The status line says "Mask removed · pixels preserved." The layer looks fully revealed because nothing is hiding it anymore, and the samples were never deleted. Apply is the bake. It multiplies the pixel alpha by the mask's coverage, then removes the mask, as one batched edit. `Ctrl+Z` restores both the previous pixels and the previous mask. You can bake, hate it, and be back in the paintable mask in one step. You do not reconstruct the mask from memory. Apply is for pixel layers. A vector layer's mask stays editable. Ask Apply there and the status line says "Apply is available for pixel layers; vector masks stay editable." The paths remain paths. The mask remains a mask. Format 6 still protects that file from older builds that would drop object masks on load. You do not bake a logo to pixels as the price of saving. A mask that does not match the pixel layer's width, height, or buffer length is refused. "Mask size does not match its pixel layer." No partial bake. A locked layer is refused. You unlock the row, then you bake, if baking is actually what you want. Placed image masks follow the image's position, scale, and rotation. Move the photo, the coverage moves. Scale it, the coverage scales. Rotate it, the coverage rotates. The original pixels remain intact until Apply, and Apply itself is undoable. SVG export agrees with the canvas on mask luminance, alpha, placement, mirroring, and the order that puts the mask before effects. What you see is what the SVG carries. The `.oma` still has the live mask if you have not applied it. Healing and clone do not run while Paint on is Mask. Choose Pixels first. Invert and Apply are mask commands. They are not brushes. Invert, switch to Pixels, then heal the revealed image. The tools will not blend color into the mask buffer. ## What landed The mask menu, once a mask exists, lists Paint mask, Invert mask, Apply to pixels, and Remove mask. Apply to pixels enables for a layer that has pixel data. On a vector layer the item does not run the bake. Invert mask flips coverage. The amount at each pixel becomes 255 minus that amount, including pixels whose alpha had been erased, so a transparent mask sample inverts to white. The status line says "Mask inverted." The picture's color samples are the same samples. Only the hide flipped. Undo returns the previous coverage. Remove mask deletes the mask buffer and leaves the layer revealed. "Mask removed · pixels preserved." Undo puts the mask back. Use this when the experiment failed and you want the original layer, not a baked compromise. Apply to pixels writes the coverage into the pixel alpha and clears the mask in the same history step. The visual result matches what you saw. The difference is structural. There is no mask left to paint. `Ctrl+Z` restores the pre-bake pixels and the mask together. Redo bakes again. One step each way. Placed images keep that mask glued to the transform. Drag the image with `V`. Rotate the top handle. Scale a corner. The hole stays on the eye, or the product, or whatever you masked. It does not stay behind on the artboard. A frame that then swallows the placed image as an image fill bakes an existing layer mask into the fill's alpha. That is a different command, the drop onto a frame. Undo of that drop restores the image layer and the editable mask. Apply, by contrast, is the explicit menu item. Know which one you fired. Trace on the pixel layer is how a cutout becomes vectors. Apply first if Trace should see the baked alpha. Leave the mask editable if you still want the coverage as its own buffer. The bake's undo is the way back while that step is the last one. SVG and the canvas stay in agreement after invert, after remove, and after apply. Save the project after any of them. The `.oma` has the post-edit truth. Format 6 remains the guard when object masks are still in the file. A baked pixel layer with no remaining object mask is ordinary pixels in the project. You can still save, and you can still undo back to the mask while that step is in the session history. ## In the hand You selected the background by accident and turned it into a mask. The subject is hidden. The room is visible. Open the mask menu. ``` Invert mask ``` The subject returns. The room drops out. Paint a repair if the edge is messy. You are still on an editable mask. The pixels underneath are still the full photograph. Wrong cutout entirely. Remove mask. The status line confirms the pixels were preserved. The whole frame of the photo is back. Make a better selection. From selection. Continue. The cutout is approved and you want one buffer. Paint on: you are looking at the mask, the edge is right. Apply to pixels. The mask item leaves the inspector. The alpha of the layer holds the hole. `Ctrl+Z`. The mask is back, and the pixels are back to the pre-bake buffer. You are certain. Apply to pixels again. Save. A placed product shot, masked, needs to sit at 30 degrees. Select it. Rotate the handle. The mask rotates with it. Scale it down. The mask scales with it. You do not re-brush the silhouette after the transform. Nudge it into the layout. The hole stays on the product. If Apply is disabled or the status line mentions vector masks, you are on artwork, not on a pixel layer. Leave it editable, or rasterize by a path you intend, on a pixel layer, and mask that. Do not expect Apply to flatten the logo as a favor. Choose Pixels before Shift+J or `J`. Invert does not change that rule. A mask you inverted is still a mask. Healing still wants the color buffer. ## The edge Apply to pixels refuses to be a one-way door inside the session. One undo restores the pixels and the mask together. It also refuses vector layers. Those masks stay editable. "Apply is available for pixel layers; vector masks stay editable." Remove refuses to damage samples. The mask goes away. The layer you had before any hiding is the layer you see. Invert refuses to touch color. It flips coverage only. Placed-image coverage refuses to stay behind when you move, scale, or rotate. It follows the picture. ## The thread Part 65 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-layer-masks) · [Next](/blog/omadesign-0-5-8-open-photos-folder) --- # Open photos folder Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-open-photos-folder Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, photo ## The habit Lightroom's library is a catalog. You import, you wait, you are told the photos now live in its idea of a folder. Capture One is the same shape. The habit underneath is simpler. Point at a folder of pictures. See them. Click one. Start grading. The files stay where the camera, or the card, or you, put them. Photoshop's Open is one file. Bridge is the folder. You bounce. Affinity Photo opens a document and you develop inside it. For a roll, you want the roll visible: names, a thumbnail, the camera line if the file has one. ISO, shutter, aperture, the lens on hover. You do not want a modal that blocks the develop sliders while the next twelve files decode. Drop is the other habit. Drag a handful of files onto the window. They appear. You keep working on the one you already picked. A short queue that drops the rest on the floor loses the third shot. Samples exist for when you are learning the sliders and you do not want to point at a client's folder yet. They are pictures without an original path. You can grade them. You cannot pretend they have a sidecar destination. ## The constraint Photo is a persona in the same binary as Design. It is not a second install. Welcome has + Photo for the workspace, a folder icon for the folder chooser, and an image icon for one file. File → Open, a drop, and the system Open With action all land here for photographs and for camera RAW. The desktop entry that 0.5.8 installs is the same `omadesign.desktop` the `.oma` files use. Imports run on background workers. Folder scans do too. The frame loop keeps drawing the photo you are grading. A slow RAW does not freeze the sliders. That is the constraint the single UI thread forces. Decode off to the side. Show metadata when the decoder actually read it. Show nothing invented when the file has no ISO, no lens, no shutter. The library is not a catalog database you sync. The folder is the folder. Thumbnails for the open set are the ones on screen. The folder's small previews stay available as you scroll. Full decoded sources are not retained for every card you are not looking at. A roll can be large. The machine is the machine you have. Samples and pasted images can take adjustments. Save settings stays disabled until the photo has an original on disk. The disabled hint says: "Open a photo from disk before saving settings. Samples and pasted images have no original file." There is no silent write into a home directory you did not pick. The sidecar, when you do save, sits beside the original. This post is about opening. The sidecar rules are the next constraint over. Opening has to leave you able to grade before that save exists. Quitting with unsaved photo edits will ask later. Opening itself does not. Browse, drop, and look. Camera files stay the bytes they were. ## What landed Open a photo. Browse a folder. Drop files. Load samples. Those four are the doors. The Photo library lists what you opened. When the file is a RAW and the decoder stored camera data, the develop header shows RAW, the make, and the model. Under that, ISO, aperture, and shutter, when the values are actually present. Hover shows the lens and the focal length. A JPEG or TIFF with no camera block simply does not grow that line. Photo notes, if the open produced any, sit in a collapsing section. You can read why a file was partial. You are not blocked from the sliders. Empty develop says "A little light. A little color." and "Open a photo to make it yours." The panel is useless until a photo is selected, on purpose. Select one in the library and the sliders bind to it. Background import means you can move Exposure on the current photo while another file is still landing. The old two-file drop limit, the one that left later files unopened, is gone. A drop is a queue. The files you added open. You do not babysit them one dialog at a time. Library → … → Browse folder… is the folder door from inside the persona. Welcome's folder icon is the same idea from the start screen. File → Open is the single file, and it is also how an `.omaphoto` sidecar reopens its original. A file-manager Open With on a RAW or a photo uses the installed desktop entry. Navigation while you look: hold Space, or choose Hand, and drag. Middle-drag and two-finger scroll pan. Pinch, Ctrl+scroll, and Alt+scroll zoom. `Ctrl+0` fits the photo. `Ctrl+1` shows it at 100%. Those chords are the photo, when Photo is the active persona. The design canvas is not what they move. The first picture is a display preview, max edge 1600 pixels. Zoom in when you need the real detail. Opening's promise is that a spinner does not own the sliders. Metadata is read, not edited. Nothing on this path rewrites the camera file. You can close the app and checksum the RAW. It matches. Development lives in memory until you save settings, and in the sidecar after you do. The original stays the original. ## In the hand Launch Omadesign. On the welcome screen press + Photo, or the folder icon and pick the card you just copied off the camera. The library fills as the scan returns. Click the first frame you care about. If the file has a camera block, read make and model above the sliders. Hover the exposure line if you want the lens. Drag three more files from the file manager onto the window. Stay on the photo you were grading. Move Exposure. The drops land behind that work. Click one of the new names when you want it in the viewer. The previous grade is still on the previous photo. Each photo holds its own develop settings in the session. ``` + Photo folder icon browse a folder image icon one file drop queue the files ``` Press Space and drag to pan. `Ctrl+1` when you need actual pixels. `Ctrl+0` when you need the whole frame again. Fit is not a destructive resize. It is the view. Load a sample if you are teaching yourself the Tone curve and you do not want a real file in play. Grade it. Look at Save settings. It stays off, with the hint about samples. Open a file from disk when you want a sidecar. The sample did its job. It did not write a stranger's folder. File → Open on a single JPEG from a client. Same library, same sliders, metadata only if the JPEG had it. You already know this panel. The door changed. The develop surface did not. Save the design document, if one is open in another tab, with `Ctrl+S`. That `.oma` does not absorb the roll. The roll is files in the folder you opened. Photo's own save is Save settings, beside those files, when you are ready. Until then you are looking, and the imports can finish while you look. ## The edge Opening refuses to block the grade on a file that is still decoding, and it refuses to invent camera metadata the file does not carry. You get make, model, ISO, aperture, shutter, lens, and focal length when they were read. You get a normal photo when they were not. Samples refuse a sidecar. Save settings stays disabled until the picture has an original on disk. The camera file you did open is never rewritten by the act of browsing it. Drop the roll, grade the frame in front of you, and let the rest land. ## The thread Part 66 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-mask-invert-apply) · [Next](/blog/omadesign-0-5-8-camera-raw-decoder) --- # Camera RAW decoder Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-camera-raw-decoder Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, raw ## The habit You copy a card and you open the RAW. In Lightroom the develop module is the RAW. In Photoshop you wait on Adobe Camera Raw, a plugin that has its own update train. On Linux that plugin is the thing you do not have. The usual workaround is a converter: darktable, RawTherapee, a dcraw fork, an export to TIFF, then a second app. Every one of those steps can write a new file beside the camera original, or worse, touch the original. The habit you want is smaller. Double-click the NEF, or drop it, and see a picture made from the sensor, not from the tiny JPEG the camera embedded for the back of the LCD. Exposure and white balance should move the linear data, the numbers before the screen's curve, because that is where a stop still means a stop. The file on disk should checksum the same after you have dragged the slider and closed the app. You also know the lie of a file extension. `.dng` means a family. It does not mean every opcode, every compression, and every phone's computational stack will come apart cleanly. A honest decoder says what it did and what it refused. ## The constraint Omadesign ships one binary. The RAW decoder has to be inside it. LibRaw 0.22.2 is the pinned source, bundled with the packages, license and all. Opening a photo does not download a converter, and it does not shell out to an installed one. If LibRaw can read the sensor, you get an image. If it cannot, you get the failure, not a silent trip through a system library you happened to apt-install last year. The output of that decode is 16-bit linear sRGB. Black levels come off. Supported Bayer and X-Trans sensors are demosaiced. The camera white balance and the color matrix are applied. Orientation is honored. Automatic brightness is off, so the decoder does not "help" by stretching the file before you have touched Exposure. DNG baseline exposure is applied, and Photo's Exposure and white-balance edits run on that linear source, before the display transfer. You grade the sensor's range. The screen curve is how it is shown. The original is never the write target. There is no RAW writer. Export makes a new JPEG, PNG, or TIFF. Save settings makes an `.omaphoto` next to the original. The NEF, CR3, or DNG stays the bytes the camera wrote. Workers do the decode. The UI stays up. A 1600-pixel preview appears, and full-resolution tiles follow when you zoom. The linear pixels are kept separate from the develop settings and from that preview. You can reset the sliders and the sensor data is still the sensor data from this session. Limits are part of the contract, because a hostile or merely huge file should not take the machine down with it. Images over 64 megapixels are rejected. Inputs over 512 MiB are rejected. LibRaw's unpacking buffers are limited. A progress callback asks the decode to cancel after 120 seconds. That callback is cooperative. It is not a hard kill of the process at 120.000. Memory for the decoded pixels and the develop buffers sits on top of those caps. Working files in the verified set fit. A stitched scientific mosaic might not. You will hear about it. ## What landed Recognized extensions are `.3fr`, `.arw`, `.bay`, `.cap`, `.cr2`, `.cr3`, `.crw`, `.dcr`, `.dcs`, `.dng`, `.drf`, `.erf`, `.fff`, `.iiq`, `.k25`, `.kdc`, `.mdc`, `.mef`, `.mos`, `.mrw`, `.nef`, `.nrw`, `.orf`, `.pef`, `.ptx`, `.pxn`, `.raf`, `.raw`, `.rw2`, `.rwl`, `.rwz`, `.sr2`, `.srf`, `.srw`, `.sti`, and `.x3f`. An extension identifies a family. It does not promise every camera body and every compression mode inside that family. Not in this build: JPEG XL-compressed DNG, GPR, EIP packages, and R3D video. A multi-image RAW develops the first image only, and you get a conversion note. Later frames in that container are not a burst editor. Open through File → Open, the Photo library, a folder, Open With, or a drop. The header reads RAW, make, and model when metadata exists, plus ISO, aperture, shutter, and on hover the lens and focal length. Before shows the default camera-balanced development. It does not show the embedded JPEG. If you wanted the camera's JPEG look, that look is a different file, the JPEG on the card. This path is the sensor. The rendering is not a clone of Lightroom, Capture One, or the in-camera JPEG. Proprietary looks, full Adobe camera profiles, automatic lens correction, some DNG opcodes, and multi-frame computational stacks are not implemented. A lens that needs a profile will not get one quietly. Correct it yourself, or accept the difference. Clipping in the sensor or the channel stays clipped. A later Exposure move cannot invent the highlights that were never recorded. Three real files were checked against an independent LibRaw and an sRGB transfer, pixel for pixel, on the validated native build: an iPhone 16 Pro Max ProRAW DNG at 3024 × 4032, a compressed Canon EOS R6 CR3 at 3407 × 2271, and a compressed Fujifilm X-T30 II X-Trans RAF at 6246 × 4170. A sidecar round trip on the DNG restored exposure, rotation, and crop. Those checks do not certify every body in the extension list, and they do not promise bit-identical output on every architecture. They are the proof that the bundled decoder matches a known LibRaw on those sensors. Headless, the same reader runs: ``` omadesign --inspect photograph.NEF omadesign --convert photograph.dng --output photograph.tif ``` Inspect reports camera metadata, dimensions, precision, and saved develop settings if a sidecar is in play. Convert writes a new file. Same-file conversion is refused, so the original cannot be the destination. ## In the hand Copy the card to a folder. Do not "import" it into a catalog. In Photo, browse that folder, or drop one NEF on the window. Wait for the preview, which should be short. Read the make and model. If the line is missing, the metadata was not in the file. The picture can still be real. Move Exposure. You are moving the linear source. Move Temperature and Tint for white balance. They are on that same linear data, before the display curve. Press Before to compare with the default camera-balanced develop. Press it again to return to your grade. The embedded JPEG never entered that comparison. Zoom in. Tiles fill in from a background develop of the full resolution. The preview you had stays on screen until those tiles exist. A decode that fails the size cap or the 120 second cooperative cancel does not pretend to finish. You still have the original file, untouched. ``` drop the .NEF Exposure, Temperature, Tint Before ``` Export later, to a new TIFF or PNG, if you need 16-bit delivery. Place in Design if you need an 8-bit pixel layer in a layout. Neither writes the RAW. Checksum the camera file when you are doubtful. It matches the card. If a CR3 from a body you have not tried opens with a note, read the note. Family support is the extension list. A note is the specific file telling you what was simplified. Keep the RAW. The `.oma` you might place into does not contain it. ## The edge The decoder refuses to rewrite the camera file, and it refuses to download a helper. LibRaw 0.22.2 in the binary is the reader. There is no RAW writer at the end of the slider. It also refuses files over 64 megapixels, inputs over 512 MiB, JPEG XL DNG, GPR, EIP, and R3D, and it develops only the first image of a multi-image RAW. Clipped channels stay clipped. Lens profiles and Adobe's full camera looks are not applied. What you grade is the linear sRGB the bundled decoder produced, 16-bit, orientation honored, automatic brightness left off. ## The thread Part 67 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-open-photos-folder) · [Next](/blog/omadesign-0-5-8-develop-panel-groups) --- # Develop panel groups Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-develop-panel-groups Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, photo ## The habit The right rail in a develop app is a long scroll: basic tone, a curve, HSL, grading, detail. On a short screen an open-everything rail shoves the photo aside. The groups you use are three. Light. Color. Detail. The curve, the mixer, and the grading stay closed until the basic sliders run out of room. Before is the habit of holding a key or a button to see the file without your grade. In this studio, Before means the default development. On a RAW that default is the camera-balanced decode, not the JPEG the camera embedded for its screen. If you wanted that JPEG, it is a different file. Auto is the other habit. One click that sets exposure and the ends of the histogram so the picture is in the window, and then you take over. You do not accept Auto as the finished print. You accept it as a start that is better than a black frame. Reset sits at the top for the day you want every slider back to default on this photo only. The rest of the roll stays where you left it. ## The constraint The develop controls have to fit beside the photograph in one window, on Linux, with the same binary that also draws vectors. A rail that never collapses will shove the picture to a stamp. So the panel is three selectable groups, Light, Color, and Detail, and the heavier editors start closed: Tone curve, Color mixer, Color grading. You open the one you need. The others stay a single row. The sliders write develop settings for the selected photo. They do not write the camera file. On a RAW, Exposure and white balance run against the 16-bit linear source, before the display transfer. The other corrections ride the develop stack as stored settings. Undo for these edits is Photo's own history, per selected photo, not the Design document's undo. A `.oma` save in another tab does not store this grade. Save settings does, into an `.omaphoto` beside the original, when you ask. Auto light has to be deterministic and local. It reads the preview image for this photo, samples luma, and sets Exposure, Blacks, and Whites. The low end is the 1st percentile. The high end is the 99th. Exposure recenters the middle of those two. Blacks and Whites pull the ends. The hover text says it balances exposure and contrast. The contrast comes from those end sliders, not from a hidden rewrite of the Contrast slider. An empty preview does nothing and leaves the defaults. You can move every slider after that. Auto is not a lock. Before toggles `show original`. You see the default development. Your settings stay in the panel. Toggle again and your grade is back. Nothing is copied, nothing is saved, nothing is discarded by looking. Reset replaces the selected photo's develop parameters with the defaults and marks the photo dirty. One photo. The library selection is not a gang reset unless you pasted a look on purpose, which is a different command. ## What landed The panel is titled Develop. Reset is at the right, hover "Reset all adjustments for this photo." Under the file name, RAW files show make and model and the exposure line. Then the histogram. Then Auto light and Before. Light holds Exposure from −4 to 4, Contrast from 0.2 to 2.4, Highlights, Shadows, Whites, and Blacks, each from −1 to 1. Tone curve opens onto five points: Blacks, Shadows, Midtones, Highlights, Whites, each from 0 to 1. Color holds Temperature and Tint from −100 to 100, Vibrance and Saturation from 0 to 2, and Hue from −180 to 180. Color mixer opens onto a channel: Red, Orange, Yellow, Green, Aqua, Blue, Purple, Magenta. Each channel has Hue from −40 to 40, Saturation from −1 to 1, and Luminance from −1 to 1. Color grading opens onto Shadows and Highlights, each with Red, Green, and Blue from −0.4 to 0.4, plus Balance from −1 to 1. Detail holds Clarity, Dehaze, and Vignette from −1 to 1, and Grain from 0 to 1. Under those, Orientation is 0, 90, 180, or 270 degrees. Clear crop enables when a crop exists and sets the crop back to none. Crop itself is the Crop tool, `C`, with Enter to apply and Esc to cancel during the drag. The panel is where the rotation and the clear live once the crop exists. Every slider is a label, a number, and a bar. Photo history records the edit. The same values applied again do not need a second story. The panel is the current settings. Before shows the defaults underneath them without clearing the panel. The groups remember which one you clicked for the session's UI state. Light is where you land first. Color and Detail are one click. You do not scroll past a fully expanded curve to reach Grain. You open the curve when the five-point editor is the job, you close it, you move on. Auto light uses the preview you are looking at, the display-sized image, not a second secret analysis pass you have to wait on. On a RAW the preview is the 1600-edge stand-in until tiles arrive. Auto is therefore a preview measurement. If you need the ends judged on the full sensor, look at the full-resolution tiles, then set Exposure, Blacks, and Whites yourself. The button is a start. The sliders are the grade. ## In the hand Open a RAW. The picture appears. Click Auto light if the frame is muddy. Look. If the midtone is right and the sky clipped, pull Highlights down yourself. Auto did not freeze Highlights. It set Exposure, Blacks, and Whites. The rest of Light is yours. Open Tone curve only if the five basic sliders cannot make the shape. Pull Midtones. Close the curve. The points stay. They are settings, not a panel you must leave open to keep. ``` Light Exposure Contrast Highlights Shadows Whites Blacks Tone curve Color Temperature Tint Vibrance Saturation Hue Color mixer · Color grading Detail Clarity Dehaze Grain Vignette 0° 90° 180° 270° · Clear crop ``` Press Before. You are looking at the default camera-balanced develop. The sliders still show your numbers. Press Before again. Your grade returns. Use that when a client asks "what did you do?" and the true answer is a comparison, not a speech. Click Color. Set Temperature until the gray is gray. Open Color mixer, pick Blue, pull Luminance down if the sky is shouting. Close it. Click Detail. Add a little Clarity. Set Grain to 0 if you touched it by accident. Reset, at the top, if the photo should go back to default entirely. The other photos in the library do not move. `Ctrl+Z` walks Photo's history for this photo. The Design tab's history is a different stack. You can grade, undo the grade, and the poster in the other tab is where you left it. Save settings when the grade should survive the session. Until that click, the sliders are memory. The camera file is still the camera file. Before and Auto light do not write it either. ## The edge Before refuses to show the embedded JPEG. It shows the default development. On a RAW that is the camera-balanced linear decode after the display transform, the one the decoder produced with automatic brightness left off. Your sliders stay loaded while you look. Auto light refuses to be a full-sensor mystery pass. It measures the preview, sets Exposure, Blacks, and Whites from the 1% and 99% ends, and stops. Contrast in the hover means those ends. The Contrast slider is still yours. Reset refuses to touch the rest of the roll. One photo, back to defaults, marked unsaved, ready for a cleaner grade. ## The thread Part 68 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-camera-raw-decoder) · [Next](/blog/omadesign-0-5-8-omaphoto-sidecars) --- # omaphoto sidecars Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-omaphoto-sidecars Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, photo ## The habit XMP sidecars are the Lightroom habit that survived contact with a camera original. The RAW stays the RAW. The edits live next to it, small, text, disposable, copyable with the file if you remember to copy both. You learned to hate the catalog the day the catalog and the folder disagreed. The sidecar is dumber and therefore harder to strand. The name is the link. `DSC_0001.NEF` and `DSC_0001.NEF.xmp`. Lose the xmp and you still have the photograph. Lose the photograph and the xmp is notes about nothing. You also learned that a sidecar which secretly contains pixels is just a second TIFF with a cute extension. It stops being safe to email, and it stops being obvious that the camera file is the master. Settings only. No image inside. Reopen has to accept either name. You double-click the RAW on Monday and the sidecar on Tuesday. Both should show the same grade, the same crop, the same rotation, and the same original pixels, including RAW precision. If the original moved or changed, the reopen should say so and should not smash the photo you already have on screen. ## The constraint Omadesign's sidecar is `.omaphoto`. Save settings writes it beside the original. The manual's pair is `photo.png` and `photo.png.omaphoto`. A RAW looks like `DSC_0001.NEF.omaphoto`. The settings file does not contain the image. Keep the two names together. The link is the name, plus the source size, plus the source modification time. That check is a metadata match. It is not a cryptographic content identity. A file that was rewritten with the same size and the same timestamp can still fool it. Do not use the sidecar as a seal. Use it as develop memory. Checksum the RAW yourself when the bytes matter. The app's promise is narrower and strict: it does not write those bytes. Opening the `.omaphoto` explicitly, through File → Open, a drop, or Photo → Library → ··· → Open photo or settings…, restores the original pixels and the saved adjustments when the pair is intact. If the original is missing, changed, or the settings are invalid, that open fails and does not replace the photo you currently have. Opening the original instead, when its sidecar is unusable, shows the default development and a note. You see the picture. You are told the grade did not load. The current work is not swapped out from under you by a bad file. Failed saves keep the edits in the session so you can retry. Edits you make while a save is still finishing stay marked unsaved. The button shows "Saving…" while the write runs, and it disables. A dot on Save settings means the selected photo is dirty. Samples and pasted images have no original, so the button stays off. The hint tells you to open a photo from disk. The 0.5.8 desktop entry is the handler for both `.oma` and `.omaphoto`. Open With on a sidecar launches the same studio as Open With on a design file. You do not install a second app to resume a grade. A Design `.oma` does not store the RAW or these settings. The sidecar is the photo's memory. The project file is the layout's memory. They can sit in the same folder. They do not swallow each other. ## What landed Save settings writes the small file next to the original. The hover says you can resume by opening the photo or the `.omaphoto`. Ctrl+S does that save for the selected photos. One selected photo with a dirty grade shows "Save settings •". Several selected photos that all have originals show "Save N selected settings". The write is the selected set, each beside its own original. Reopen any of these ways: ``` File → Open drop the photo or the .omaphoto Library → ··· → Open photo or settings… ``` Pick either half of the pair. You get the original pixels, RAW precision included when the original is RAW, and the crop, rotation, and develop values that were saved. Saving again updates the settings file you opened, including when the extension's case is uppercase. The name match is the pair, not a new randomly named blob. Headless uses the same pairing. Inspect prints metadata, dimensions, precision, and the saved development. Convert applies the sidecar automatically and writes a different file. ``` omadesign --inspect photograph.NEF.omaphoto omadesign --convert photograph.NEF.omaphoto --output developed.tif ``` Same-file conversion is refused. The output cannot be the RAW, and it cannot be a trick that replaces the source. The TIFF is new. The `.omaphoto` is still settings. The RAW is still the RAW. If you change the original in another program, size or modification time moves, and an explicit open of the sidecar fails without replacing whatever photo is already up. Fix the pair, or accept default development by opening the original and reading the note. Copying the photo to a new name without copying the sidecar, or without renaming the sidecar to match, is a broken pair. `photo-edit.png` does not read `photo.png.omaphoto`. The names have to match. Unsaved grades exist only in the Photo session. Crash the machine before Save settings and the sidecar on disk is the last successful write. The recovery story for design documents is the `.oma.swp` swap. Photo settings do not hide inside that swap. Save the sidecar when the grade matters. The button is at the bottom of Develop. Folder jobs and pasted looks can write many sidecars later. The single-photo contract is the same file: settings, beside the original, original untouched, retry if the write fails. ## In the hand Grade the NEF. Crop. Rotate 90. Move Exposure. Look at the button. It reads "Save settings •" because the photo is dirty and it has a path. Click it. Next to `DSC_0001.NEF` you now have `DSC_0001.NEF.omaphoto`. The NEF's checksum has not changed. Quit. Tuesday, File → Open, pick the `.omaphoto`. The NEF's pixels come back at full RAW precision. The exposure, the crop, and the rotation come back with them. Open the NEF instead, from the file manager, with the sidecar still beside it. Same picture, same grade. Move the NEF to another folder and forget the sidecar. Open the sidecar from the old folder. The open fails. The photo you already had on screen stays. Go get the NEF, put it back beside the sidecar, open again. Or open the NEF alone, live with the default development and the note, and rebuild the grade. ``` photo.png photo.png.omaphoto ``` That is the whole naming rule. The suffix is `.omaphoto` stuck on the original filename, extension included. Do not strip `.png` off and hope. Do not rename one of them. While "Saving…" is up, move a slider. The new edit stays dirty after the in-flight save completes. Click Save settings again. You did not lose the tweak, and you did not get a false clean state. A sample image never enables the button. Open a real file. Then the sidecar has somewhere legal to sit. ## The edge The sidecar refuses to contain pixels, and the save refuses to rewrite the original. Settings only, beside the file, under the paired name. A missing, changed, or invalid original on an explicit settings open refuses to replace the current photo. A bad sidecar beside an original you opened directly falls back to default development and a note. The match is name, size, and modification time. It is not a hash of the contents. Treat the camera file as the master you can checksum. Treat `.omaphoto` as the grade you can delete and regret, without ever having touched the master. ## The thread Part 69 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-develop-panel-groups) · [Next](/blog/omadesign-0-5-8-photo-undo-history) --- # Photo undo history Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-photo-undo-history Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, photo ## The habit You grade a photograph and you hit Ctrl+Z. You expect the last slider to return. Photoshop keeps history per document. One undo stack makes the text box jump when you undid Exposure. The photo's history stays the photo's. Lightroom's history belongs to the photo you are standing on. Switch photos, the history you see is that photo's history. The other frame keeps its own. Quit is the other habit you care about. If grades are unsaved, the app asks. It does not quit through the dialog and drop the writes on the floor. It does not ask you about the poster first and then fail the photo save after the window is gone. Discard has to mean discard. Save all has to mean the sidecars hit disk before the next question. Cancel has to mean you are still in the app, grade intact, nothing thrown away. ## The constraint Design's undo is the `.oma` stack. One step, `Ctrl+Z`, redo `Ctrl+Shift+Z`. It records geometry, type, pixels you painted in a pixel layer, frames. Photo edits have their own Undo and Redo. The manual separates them on purpose. Slider moves, crop, rotation, reset, and the batch paste of a look all go to the Photo history. The selected photo is whose history you are walking. Undo and Redo belong to that photo. The poster in the other tab does not move. Saving the Design `.oma` does not store the RAW and does not store the develop settings. Those live in the session until Save settings writes an `.omaphoto` beside the original. A shared undo stack would imply a shared file. The files are different, so the histories are different. You can paint a pixel layer in Design, undo it, and the RAW grade stays. You can undo the grade, and the pixel layer stays. Quit order is the safety. Unsaved photo settings are asked first: Save all, Discard, or Cancel. The app waits for those writes to finish. Then palettes, if they are dirty. Then artwork. Palette save has the same three answers, and a failed or conflicting library save keeps you in the app. Photo's wait is the same idea. A quit that closes the window while a sidecar is half-written is how you corrupt the one file that is allowed to change. The original photograph is still never that file. The write being waited on is the settings file. Cancel leaves you inside, with the grades still dirty, the poster still unsaved if it was unsaved, nothing discarded. Discard drops the unsaved photo settings and continues the quit path. The last sidecar already on disk stays. The session's newer tweaks go away. That is what discard means. It does not delete the RAW. It does not delete a sidecar that already succeeded. Unsaved photo changes exist only in the current Photo session. There is no `.oma.swp` for them. Idle recovery is for the design document. If the machine dies before Save settings, the grade since the last sidecar write is gone. The history is real while the process is real. The sidecar is real after the write finishes. Quit's job is to force that choice while the process can still wait. ## What landed `Ctrl+Z` in Photo steps the selected photo's develop history. `Ctrl+Shift+Z` redoes. Switch the library selection and you are on that photo's history. The active photo is the one in the viewer. A multi-select used for paste is a different set. Ordinary slider edits use the Photo history. Applying the same pasted values again adds no extra undo entry, because nothing changed. A real change, including a batch apply, is one undo for that operation. The batch's single step is the paste feature's rule. The history it lands in is this one. Reset in the Develop header sets the selected photo back to default parameters and marks it dirty. That is a history step. Undo returns the grade you had. Auto light's write to Exposure, Blacks, and Whites is a history step. Before is not. Before only shows the default development. It does not edit, so it does not push history. Design shortcuts that would reorder layers, delete objects, or paste shapes stay out of a hidden Design document while you are in Photo. Photo commands stay in Photo. You can copy and paste adjustments with `Ctrl+Shift+C` and `Ctrl+Shift+V` without the design clipboard eating a frame. Typing in a field keeps the field's own clipboard and undo. A numeric slider's text edit does not leak into the canvas history. Quit with dirty photos. The dialog offers Save all, Discard, and Cancel for the photo settings, and it happens before the palette question and before the artwork question. Save all writes the sidecars and the quit path waits. If a write fails, the edits remain available to retry, the same rule as a failed Save settings from the button. You are not shown a green quit over a failed disk. Discard proceeds without those writes. Cancel aborts the quit. The `.oma` you save from Design, in the same session, still does not pick up the RAW. You can save the poster, keep grading, and quit later into the photo dialog. Two saves, two files, two histories. The status of one does not clear the other. Palette files, `.omacolors` and the rest, have their own save state too. They are third. The order is photo settings, then palettes, then artwork. You always get the chance to keep the grade before someone asks you about a swatch. ## In the hand Open a RAW. Move Exposure. Move Temperature. Press `Ctrl+Z`. Temperature returns. Press `Ctrl+Z` again. Exposure returns. Press `Ctrl+Shift+Z`. Exposure is back. The Design tab, if a poster is open, has not undone a single path. Click another photo in the library. `Ctrl+Z`. You are undoing that photo, or you are at the bottom of its history if you have not graded it. Click back. Your first photo's history is still its history. ``` Ctrl+Z undo the selected photo Ctrl+Shift+Z redo the selected photo ``` Hit Reset. The sliders go to default. `Ctrl+Z` brings your grade back. Press Before. The picture shows the default development and the sliders stay. `Ctrl+Z` does nothing new, because Before took no step. Press Before again to see the grade. Save the poster with `Ctrl+S` if you are on its tab. Return to Photo. The dirty dot on Save settings is still there, if you have not saved the sidecar. The `.oma` write did not clear it and did not embed the RAW. Quit. Read the dialog. Save all writes every dirty photo's `.omaphoto` and waits. Discard leaves the last on-disk sidecars as they were and drops the session tweaks. Cancel returns you to the sliders, nothing lost. If a palette is also dirty, that question comes after the photo writes succeed. If the poster is dirty, that question comes after the palettes. You deal with them in that order, one dialog at a time, still inside the app until you have answered. If Save all hits a disk error, you stay able to retry. The original camera file was not the file being written. Fix the folder permissions, Save settings, quit again. ## The edge Photo undo refuses to step the Design document, and a Design save refuses to store the RAW or the `.omaphoto` settings. The grade's history lives with the selected photo. The grade's durability lives in the sidecar you save, or in the Save all you accept on quit. Quit refuses to close through an unfinished settings write. Save all waits. Discard drops only the unsaved session settings. Cancel stays in the app. The camera file is not on that dialog's list of things to rewrite, because nothing in this history rewrites it. ## The thread Part 70 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-omaphoto-sidecars) · [Next](/blog/omadesign-0-5-8-photo-export-bit-depth) --- # Photo export bit depth Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-photo-export-bit-depth Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, photo ## The habit You export when the grade is done. 16-bit TIFF when the file is still going to move. JPEG when it is going to a page. Export writes a new file. The camera file stays. The other destination is the layout. The graded picture lands on the artboard as pixels you can mask, with the placement undoable. The master grade remains the RAW plus the sidecar, so next week you can change Exposure and place again. Crop and rotation have to be in the export. A TIFF that includes the pixels you cropped out, with a note that says "crop later," is a file you will misprint. The developed size is the cropped, rotated size. Full resolution means the sensor's resolution after that crop, not the 1600 pixel preview you have been staring at. ## The constraint Export runs in the background. A full RAW develop is too much work for the frame that also has to track the mouse. The button reads "Exporting…" while that job runs, and you do not start a second one on top of it. The preview stays up. The sliders stay up. The file writes when the develop finishes. The destination is a new file you pick in the native dialog, or a pixel layer in a Design document you already have. Same-file conversion is refused on the command line for the same reason the desktop will not offer the RAW as the output. The original is protected. JPEG, PNG, and TIFF are the three writers. Bit depth follows the source and the container. A RAW exported to PNG or TIFF keeps 16-bit channels. JPEG is an 8-bit delivery image. The pixels in all three are display sRGB values. They do not contain the sensor mosaic, the camera's edit history, or every metadata tag from the source. You are exporting a developed picture. You are not cloning the RAW into a friendlier extension. Place in Design develops the picture and adds an 8-bit pixel layer, with Undo. A new document takes the photo's actual size. The RAW and the `.omaphoto` stay where they were, and they remain the files you reopen to keep grading. The layer in the `.oma` is a rendering. Change the grade later, place again, and you get another 8-bit layer. The first one does not secretly track the sidecar. Headless convert uses the same pipeline. `--convert` on a RAW or on a RAW's sidecar writes 16-bit PNG or TIFF, or 8-bit JPEG, and applies saved settings automatically. Converting to a document format, `.oma` included, renders an 8-bit pixel layer. That matches Place in Design. The depth is a property of the destination, chosen on purpose, not an accident of whichever buffer was handy. Crop and rotation are included. The export does not add a transparent fringe where a crop used to confuse the bounds. You get the developed rectangle. ## What landed In the Photo persona the File menu's export block is Export PNG… with `Ctrl+E`, Export TIFF…, and Export JPEG…. The document exporters for SVG and PSD are out of the way while Photo is active. The develop panel repeats the choice: a menu of JPEG, PNG, TIFF, and an Export… button. The hover says "Full resolution. RAW PNG and TIFF retain 16-bit precision." Pick PNG or TIFF for a RAW when the next stop needs the channels. Pick JPEG when the next stop is a page, a slide, or a preview someone will not grade again. The develop runs at the full resolution of the source, then crop and rotation, then the writer. A JPEG from a RAW is 8-bit sRGB. A PNG or TIFF from a RAW is 16-bit. A PNG from a file that was already 8-bit is not a magic promotion. The 16-bit promise is the RAW path. Place in Design is the button under Export. It builds the 8-bit developed layer in the background and puts it in the design document, preserving the artwork already there. Undo removes that placement. The photo's RAW is not embedded as RAW. If you need the grade again, it is the sidecar beside the original, not a buried copy inside the poster. The command line, for a file you already saved settings on: ``` omadesign --convert photograph.NEF.omaphoto --output developed.tif omadesign --convert photograph.dng --output photograph.jpg ``` The TIFF is 16-bit developed, crop and rotation included. The JPEG is 8-bit. Neither path is allowed to use the source path as the output. Inspect first if you want the metadata without writing. While Exporting… is showing, the export controls disable. Let the worker finish. A failure reports on the status line. The original is still untouched, because it was never opened for write. Retry after you have disk space, or after you pick a directory you can write. Design's own File → Export PNG at 1×, 2×, or 3× is a different command. It rasterizes the `.oma`. Use it for the poster. Use Photo's export for the photograph at its developed resolution. Place in Design when the photograph should become a layer of the poster, 8-bit, undoable, and done. Verified behavior on a real sidecar included a cropped TIFF at 16-bit that matched an independent develop, and a JPEG at the same dimensions. Your file's support still depends on the decoder. The depth rule does not. If the RAW opened, PNG and TIFF export keep 16-bit channels, JPEG does not. ## In the hand Finish the grade. Crop with `C`. Enter applies the crop. Rotate to 90 in Detail if the camera orientation was wrong and the decoder's honor of the tag was not what you wanted, or leave rotation alone if it was. The export will include whichever crop and rotation are set. Open the format menu. Choose TIFF. Export…. Pick a new name. Wait out "Exporting…". Open the TIFF in a tool that shows bit depth. 16-bit channels. The dimensions match the crop, not the uncropped sensor, and not the 1600 preview. ``` PNG or TIFF 16-bit on a RAW JPEG 8-bit delivery Place in Design 8-bit pixel layer, one undo ``` Choose JPEG and export again, new name, when you need the small file. It is the same grade, 8-bit, sRGB, no mosaic. Send that. Keep the TIFF if a printer asks. Keep the RAW and the `.omaphoto` if you are the printer. Click Place in Design. Switch to the Design tab. The layer is there, developed, 8-bit. `Ctrl+Z` removes it. Place again if the position was wrong. Mask it, set type over it, save the `.oma`. Tomorrow, change Exposure on the RAW, save the sidecar, Place in Design again. You now have a new 8-bit layer. The old one does not update by itself. Delete it if it is stale. The master grade never lived inside it. `Ctrl+E` in Photo starts the PNG export. Same full-resolution path as the button. Do not confuse it with `Ctrl+E` in Design, which exports the document PNG at the Scale you set, 1×, 2×, or 3×. Persona decides which export you invoked. ## The edge Photo export refuses to write the camera file, and it refuses to hand Design a 16-bit RAW pretending to be a layer. PNG and TIFF from a RAW keep 16-bit developed channels, crop and rotation included, in a new file. JPEG is 8-bit. Place in Design is 8-bit, one undo, and the RAW plus `.omaphoto` remain the files you grade next week. The preview's 1600 pixel edge is not the export size. Export develops the full resolution. If you judged sharpness only on the preview, zoom the tiles first, then export. The file you write is the developed picture, display sRGB, mosaic and camera history left in the original where they belong. ## The thread Part 71 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-photo-undo-history) · [Next](/blog/omadesign-0-5-8-tiled-full-res-preview) --- # Tiled full-res preview Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-tiled-full-res-preview Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, photo ## The habit You open a large RAW and you want the sliders now. A smart preview is enough for exposure. Fine detail can arrive after. The failure is a window that draws nothing until the full sensor has been demosaiced, graded, and uploaded. On a laptop that window is frozen. On a big RAF it is frozen for longer. The other failure is a late tile from the previous photo, or from the previous Exposure. You moved on. The worker did not. That tile has to be thrown away. The face on screen stays the grade you are actually judging. Fit, 100%, pinch, and scroll have to keep working while the sharp tiles cook. A progress bar that eats the pointer is the frozen window again. ## The constraint The linear decode is the master inside the session. Develop settings are a separate object. The on-screen preview is a third thing, capped at a 1600 pixel long edge, so the first draw is bounded. A 6246 × 4170 RAF does not have to become a 6246-wide texture before you can move Temperature. The 1600 edge is the contract for that first picture. It keeps aspect. It is not a crop. It is a stand-in. Zooming in asks a background worker for full-resolution detail. The worker develops the full image. The viewer uploads only the tiles you can see. The previous preview stays on screen while those tiles are prepared, so the photo does not flash empty. When the tiles are ready, they replace the preview in the region you are looking at. Pan, and the next visible region can request its own. You do not upload the entire sensor to the GPU to inspect one eye. Results from an older photo, or from an older adjustment, are discarded. The worker may finish late. The viewer checks. If you have switched photos, or moved a slider, that finish does not paint. The current preview remains the current preview until a matching full-resolution result arrives. That is how the UI stays honest without waiting. The UI thread keeps the pointer. Space, Hand, middle-drag, and two-finger scroll pan. Pinch, Ctrl+scroll, and Alt+scroll zoom. `Ctrl+0` fits. `Ctrl+1` is 100%, which is the zoom that makes the full-resolution request matter. Fit is where the 1600 preview is enough. One hundred percent is where you are asking to see sensor detail. Export does not use this preview as its pixel source. Export develops the full resolution in the background, crop and rotation included. The tiles are for your eyes. The file is the full develop. Auto light, by contrast, measures the preview, because it is a quick start from the image already in hand. If you set Exposure from Auto and then zoom, you are looking at full-resolution tiles of a grade that was measured on the small picture. Nudge the slider if the full detail disagrees. The tiles will catch up, and the old tiles will be dropped. Decoder limits still apply. Over 64 megapixels, or over 512 MiB, the file does not become a preview. The 1600 cap is not a way around the reject. It is how a legal file stays responsive. ## What landed Open a RAW. The first picture's long edge is at most 1600 pixels. A small JPEG may already be under that cap, so it will not grow. A large RAW will shrink to the cap for the first draw, aspect preserved. The develop header, the histogram, and the sliders are live on that picture. You can grade. Zoom. `Ctrl+1` or a pinch inward. A background worker prepares full-resolution detail for the current photo and the current settings. Until it returns, you still see the preview, scaled. Then visible tiles replace it. The rest of the sensor is not required to be resident as a screen texture. Pan to a corner. That corner can come in the same way. The preview policy is the same: something stays on screen. Change Exposure while a tile job is running. The late result from the old Exposure is discarded. You do not see a flash of the previous grade snapped on top of the new one. Switch photos in the library while a job runs. The late tiles from the photo you left are discarded. The photo you selected keeps its own preview, then its own tiles. The linear pixels from the decode stay in the session, separate from the 1600 preview and separate from the settings. Reset and undo change settings. They do not throw away the decode and start LibRaw over unless the file itself is reopened. That is why undo of a slider feels like a slider, and why a tile refresh can run from data you already paid to decode. Before shows the default development. That view follows the same display path. You are comparing grades, not comparing a full-resolution default against a preview-sized edit without being able to zoom either one. Zoom while Before is held if you need the default at real detail. The worker's stale-result rule still applies when you release Before and your grade returns. The Shortcut HUD and the develop panel do not wait on tiles. Hardness keys are a Pixel concern. Here the keys are the view keys. The panel stays clickable. Auto light can run on the preview you have. Save settings writes numbers, not tiles. The sidecar is tiny because the preview was never the file. ## In the hand Drop a large RAF or DNG. Do not wait for a full-window sharp image before you touch Exposure. The 1600 preview is the picture. Move Exposure and Temperature until the grade is in the right place. Press `Ctrl+0` if you lost the fit. The whole frame is the preview, cheap to redraw. ``` Ctrl+0 fit Ctrl+1 100%, ask for full-resolution tiles pinch, Ctrl+scroll, Alt+scroll zoom Space, Hand, middle-drag, two-finger scroll pan ``` Press `Ctrl+1` on an eye, a fabric weave, a distant sign. The preview holds. Tiles arrive for the region in view. If they are slow, you can still pan. The worker is not the pointer. Move Highlights while you are at 100%. The old tiles are invalid. They do not stick. The preview of the new grade shows, then new tiles. That gap is the point. A frozen UI would have hidden the Highlights change until the full sensor finished. Here you see the direction immediately, and the detail catches up. Switch to the next photo. Its preview appears. Any tiles still computing for the previous photo are not allowed to paint this one. Grade the new frame. Zoom when you care about detail. Export when you care about a file. The export's resolution is the developed full size, not 1600. If the sign was illegible only because you were on the fit view, zoom before you decide the grade failed. Place in Design when the layout needs an 8-bit layer. That path also develops at full resolution in the background. It is not a screenshot of the 1600 preview. The tiles and the placement are two consumers of the same full-resolution work. Neither one blocks the other from letting you move the view. ## The edge The first view refuses to be the full sensor. The long edge caps at 1600 pixels so the sliders and the pointer stay alive. Full detail is visible tiles, prepared off the UI thread, uploaded for the region you can see. The previous picture stays up until the new tiles exist. A finished worker that belongs to an older photo, or to settings you already changed, is discarded. It does not paint. Export and Place in Design do not inherit the cap. They develop the full resolution. The 1600 edge is only the screen, and only until you zoom. ## The thread Part 72 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-photo-export-bit-depth) · [Next](/blog/omadesign-0-5-8-copy-paste-adjustments) --- # Copy paste adjustments Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-copy-paste-adjustments Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, photo ## The habit You finish one frame. Exposure sits on the linear file. White balance comes off the card, or off a wall you trust. The tone curve puts the shoulder where the print needs it. Color grading takes the shadow one way and the highlight another. Then you look at the rest of the table, the rest of the room, the rest of the hour, and your hand goes looking for Sync. In Lightroom that sync is a stack of checkboxes. Crop lives in the stack. Leave the box warm and every vertical inherits the horizontal crop from the hero. In Photoshop you open the next RAW through Camera Raw and paste the recipe. The paste is the whole recipe. Framing comes with it unless you cleared the crop before you copied. Affinity Photo's develop persona trains the same reach: one good grade, then the roll should match the frame you already decided. The job is the roll. A product set on one sweep of light. Three heights of the same room. A contact sheet you will still crop one frame at a time because the subject moved. The hand wants one copy and one paste. The hand also wants the frame you already drew on each photo to stay there. ## The constraint Omadesign is one binary. Photo is a persona in that binary, beside Design, Layout, Pixel, and Motion. There is no second app to launch for the grade, and no Creative Cloud hop between the slider and the file. The camera file stays the camera file. Development is a small `.omaphoto` sidecar beside the original. `table.RAF` stays `table.RAF`. The sidecar holds adjustments. It does not hold the image. A paste that "applies the look" by rewriting the RAW is the wrong shape. The paste has to move development values onto other photos, and the pixels on disk have to stay the pixels the camera wrote. Undo is one step. Pasting a look onto a selection cannot leave one history entry per frame, or you will spend the afternoon walking backwards through a wedding. Photo's keys stay in Photo. Copying a grade must not reach into a Design tab and restyle artwork you cannot see. The system clipboard has to survive as well. A logo you copied an hour ago is still a logo. An empty system clipboard must not block the grade copy. The adjustment snapshot is Photo's, and the chord has to fire on the key press you actually made. ## What landed Develop the source photo. **Copy adjustments**, or **Ctrl+Shift+C**, takes the settings as they stand at that moment. Edit the hero afterward and the snapshot stays where you left it. The copy is a frozen look, taken when you pressed the chord. It is not a live link back to the sliders. In the Library, choose who receives it. **Ctrl-click** toggles one photo. **Shift-click** selects a range. **Ctrl+A** selects every photo loaded in the library. The active photo remains the one in the viewer. The selection count tells you how many photos will receive the look. Those are different marks on purpose. Grading the frame in front of you and choosing the batch are different jobs. You can keep the hero on screen while the selection runs down the filmstrip. **Ctrl+Shift+V** opens **Paste…**. The boxes are **Light / tone**, **Tone curve**, **Color**, **Effects**, **Color mixer**, **Color grading**, **Crop**, and **Rotation**. Effects is the detail group from the Develop panel. Those boxes can travel on their own. Send Light / tone and leave the mixer. Send the grade and leave Effects, because the clarity, grain, or vignette that flattered the hero is wrong on the wide frame. Light, Color, and Detail are the groups you live in while grading. Tone curve, color mixer, and color grading are the sections you expand when the group sliders are not enough. The paste list uses those names, with Detail labeled Effects. **Crop and Rotation start off.** The status after the copy says so: crop and rotation are excluded. Each photo keeps its framing until you tick those two boxes. A look is tone and color. A frame is the picture. They stay apart until you say they travel together. The chord is a Photo command. It fires when you press it. It does not need the system clipboard to be holding anything, and it leaves that clipboard alone. Design artwork on another tab stays out of the operation. Photo selection, Delete, ordinary copy and paste, and stacking shortcuts stay inside Photo while you are grading. Typing into a field still uses the normal clipboard. The grade chords are for the pictures. Samples and images you pasted into the persona can wear the adjustments. They need an original on disk before a settings file can be saved. The paste puts the values on the loaded photos. It does not write the sidecars. That write is **Ctrl+S**, and it belongs to the save that follows. Apply the same values again and Photo adds no extra undo entry. History records a change. A second paste of an identical snapshot is the same grade sitting where it already sat. ## In the hand Open the folder, or drop the files. Imports run in the background, so you can start on the frame you trust while the rest land. Grade it. Press **Before** if you need the default development in front of you, then come back to the sliders. **Auto light** is there when exposure and contrast only need a first balance. RAW exposure and white balance are working on the 16-bit linear source. When the frame sits, copy it. ``` Ctrl+Shift+C ``` The copied look is the settings at that press. Nudge the hero again if you want a different direction for yourself. The frames you are about to choose will get the copy, not the nudge, until you copy again. Click the first target in the Library. Shift-click the last photo in the run, or Ctrl-click the frames that share the light and skip the one you already finished by hand. Read the selection count before you paste. Leave the viewer on the hero if that is the reference you still want to see. ``` Ctrl+Shift+V ``` The category list is the decision. Leave Crop and Rotation off. Leave Effects off when the noise floors do not match. Leave Color mixer off when only one frame needed a pulled green. Light / tone, Color, Tone curve, and Color grading can stay on. Apply. The selected photos take those categories. Their crops stay. Their rotations stay. One undo puts the whole paste back. You do not reverse the roll one frame at a time. When the photos already have originals on disk, **Ctrl+S** writes each selected `.omaphoto` beside its file. The pair is named together: `photo.png` and `photo.png.omaphoto`. The camera file's bytes do not move. ## The edge The paste refuses to treat framing as part of the look. Crop and rotation stay off until you turn them on in the category list. It also refuses to write the camera file, and it refuses to save itself. A loaded selection holds the new values in Photo's own history until you save settings. A sample with no original on disk can wear the look for the session and still cannot grow a sidecar. There is no file to stand beside. The snapshot stays frozen at the moment of **Ctrl+Shift+C**. Later moves on the hero do not push themselves onto the frames you already chose. Select the roll, press Ctrl+Shift+V, and leave crop off so each photo keeps the frame you gave it. ## The thread Part 73 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-tiled-full-res-preview) · [Next](/blog/omadesign-0-5-8-batch-save-settings) --- # Batch save settings Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-batch-save-settings Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, photo ## The habit You pasted the grade. In Lightroom the sync is already a catalog write, and Save is a different anxiety: the catalog, the XMP, the "write settings to files" command you sometimes remember and sometimes do not. In Photoshop, Camera Raw's Done writes the sidecar or the file header depending on the preference you set in 2014 and forgot. Affinity Photo saves the develop settings with its own document. The hand then does the thing it always does. Ctrl+S. You expect the work you just did to survive a quit. On a roll, that expectation gets expensive. If each photo is its own save, you will save the hero and assume the selection came along. If each photo is its own undo, you will undo the last frame and think the batch is gone, or you will undo twelve times and lose a slider move you made on the hero an hour ago. The save and the history have to describe the batch you selected, not the single photo the viewer happens to be showing. ## The constraint The original stays untouched. **Save settings** writes a small `.omaphoto` beside it. The pair shares a name: `photo.png` and `photo.png.omaphoto`. Settings do not contain the image. Reopen through **File → Open**, a drop, or **Photo → Library → ··· → Open photo or settings…**. Pick the original or the sidecar. Both restore the original pixels and the saved adjustments, as long as the pair stays together. A Design save cannot be the photo save. Saving a Design `.oma` does not store the RAW source or its settings. One document, one `.oma`, is the rule for artwork. The grade lives next to the camera file, because the camera file is not artwork you authored inside the document. Put the grade inside the poster and you have baked an 8-bit placement. The RAW and the sidecar are how you come back tomorrow and push the exposure again on the 16-bit linear source. Undo stays one step. A batch of selected photos is one history entry in Photo's own Undo and Redo. Ordinary slider edits use that same Photo history. Design's history is a different stack. Quitting has an order. Unsaved photo settings are offered **Save all**, **Discard**, or **Cancel** before palette saves and before artwork saves. The app waits for the writes to finish. You do not quit through a half-written sidecar. ## What landed After the paste, **Ctrl+S** saves the selected photos' settings beside their originals. The selection is the batch. The viewer can still be showing the hero. The files that receive a sidecar are the ones you selected, and the selection count is the count that matters. The batch is one Undo and one Redo. You pasted Light, Color, and the tone curve onto thirty frames. One Ctrl+Z restores that application. One redo puts it back. You are not scrubbing a per-file history that nobody can remember. Slider moves you make on a single photo still land in that Photo history, one change at a time, the way a develop slider should. Light / tone, Color, Effects, tone curve, color mixer, and color grading travel independently. Effects is the detail group. The save writes the categories you already chose to send. It does not smear every control onto every file because you hit save. A photo that kept its own mixer keeps that mixer. A photo that received only Light still has its own color. Crop and rotation stay where the paste left them, off unless you turned them on. Applying the same values again adds no extra undo entry. History is for changes. A repeated paste of an identical snapshot does not stack identical steps for you to peel off later. Folder jobs are a different write. A whole-folder apply puts `.omaphoto` files on disk in the background, and those files are already saved when the job finishes. A paste onto photos loaded in the Library is not that job. The loaded selection needs **Save settings**. Until Ctrl+S, the new values live in the session. Export still processes the active photo. Saving the batch does not export the batch. JPEG, PNG, and TIFF remain a decision about the frame in the viewer, at full developed resolution, crop and rotation included, written in the background. RAW PNG and TIFF keep 16-bit channels. The sidecar save is the grade. The export is a picture. Failed saves keep your edits available to retry. Edits you make while a save is still finishing stay marked unsaved. A write that fails does not get to pretend it landed. Opening a settings file reports a missing original, a changed original, or invalid settings, and it does not replace the photo you already have open. Opening an original whose settings are unusable shows default development and a note. ## In the hand Grade one frame. Copy it with **Ctrl+Shift+C**. Select the targets with Ctrl-click, a Shift-click range, or **Ctrl+A**. Paste with **Ctrl+Shift+V** and choose the categories. Leave crop and rotation off unless the framing is shared on purpose. Look at the selection count. Then save that selection. ``` Ctrl+S ``` Each selected photo grows or updates its `.omaphoto` beside the original. `chair.NEF` stays `chair.NEF`. `chair.NEF.omaphoto` holds the grade. The name match is the whole address. Move one without the other and the resume has nothing honest to pair. Press Ctrl+Z. The batch application comes back off the selected photos in one step. Press Ctrl+Shift+Z and the batch returns. Move one slider on the hero after that. That slider is its own history step. It does not reopen the batch. Quit with unsaved grades still dirty and the first prompt is the photo prompt: **Save all**, **Discard**, or **Cancel**. Palette prompts and the artwork prompt come after, and only if the photo writes are allowed to finish. Cancel keeps you in the chair. Reopen tomorrow from the Library menu, **··· → Open photo or settings…**, or by dropping either file. The pixels are the camera's. The sliders are the sidecar's. ## The edge Ctrl+S in Photo refuses to treat a Design save as a grade save. The `.oma` you write for the poster does not contain the RAW and does not contain the `.omaphoto`. Place in Design if you need an 8-bit developed pixel layer on the artboard, with its own Undo. Keep the RAW and the sidecar when you still mean to develop. A failed sidecar write refuses to throw the edit away. The values stay, marked so you can retry. A save that is still in flight refuses to call newer slider moves "saved." Select the photos, press Ctrl+S, and the batch of `.omaphoto` files is the grade you can reopen. ## The thread Part 74 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-copy-paste-adjustments) · [Next](/blog/omadesign-0-5-8-omapreset-library) --- # omapreset library Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-omapreset-library Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, photo ## The habit A look that works wants a name. In Lightroom you make a preset, and the preset remembers which boxes were checked when you created it. Crop sneaks in. A local mask sneaks in. Next month you apply "Kitchen warm" to a portrait and the crop is the kitchen's crop. In Photoshop, Camera Raw presets live in a folder Adobe chose, and moving them to another machine is a scavenger hunt through a settings directory. Affinity Photo stores develop presets with the app. They are yours until you change machines, and then they are a support article. The hand wants three things. Name the look while you can still see it. Find it later by that name, not by scrolling a dump of every experiment. Carry the file to the other computer without signing into anything. The look has to work on a photo it has never met. A wedding preset that only functions on the RAW it was born on is a sidecar with a nickname. ## The constraint Photo settings for a single original already have a home. The `.omaphoto` beside `photo.RAF` is that photo's grade. It is not a style you can drop on a different camera from a different year. Tie the reusable look to one original and you have copied a sidecar, not saved a preset. The preset has to store development values and the categories you meant to send. Light / tone can travel while Effects stays home. Effects is the detail group. The tone curve can travel while crop stays off. A file that only stores "all the sliders, always" will repeat the checkbox mistake. Categories are part of the preset because categories are part of the decision. The library has to persist between sessions inside the one binary. There is no cloud preset sync to lean on, and there should not be. A `.omapreset` is a small file you can put on a disk, mail to yourself, or keep next to a job. Import has to survive a name collision. Two looks named "Warm" are two looks. Replacing one with the other because the strings match is how a careful grade disappears. Reads and writes of that library run in the background. Naming a look cannot freeze the viewer on a large RAW. ## What landed The Photo preset library saves named looks. You filter that list by name. You import a `.omapreset` and you export a `.omapreset`. The file is the look leaving the machine. The library is the look staying here. A preset holds development values and the chosen categories. That is why it works across unrelated originals. The preset does not need the hero's pixels, the hero's crop, or the hero's filename. You built it on a Fujifilm RAF and you apply it to a Canon CR3, a DNG, a PNG scan, a JPEG from the phone. The categories you stored are the categories that move. Crop and rotation stay out unless you saved them on. The preset uses the same boxes as paste: Light / tone, Tone curve, Color, Effects, Color mixer, Color grading, Crop, Rotation. Effects is the detail group. The library persists between sessions. Quit, come back, the names are still there. You do not rebuild "North window" every morning. Imported name conflicts keep both looks, under distinct names. Bring in a `.omapreset` that says "North window" while you already have a "North window" and both remain in the library. Yours stays. Theirs stays. You can see which is which because the names were forced apart, and you can filter until you are looking at one of them. Nothing is overwritten because a string matched. Use a preset through **Presets… → Use preset…** when you are about to apply a look, including when the target is a whole folder. The preset is the source of the values. The original you happen to be viewing is only the photo in the viewer. Later edits to that photo do not rewrite the named preset. A preset changes when you save a preset. A sidecar changes when you save settings. ## In the hand Develop the frame that deserves a name. Decide the categories the way you would for a paste. If this look is tone and color, leave Effects off. Leave crop and rotation out unless you truly want every future photo to inherit that frame. Then save the named look into the preset library. Filter the library by name when the list gets long. "Warm" should narrow to the warms. You should not hunt a menu that sorts by the date you were tired. Export the one you want to keep: ``` .omapreset ``` That file is the portable look. Copy it to the other machine, the other user, the archive disk. On the far side, import the `.omapreset`. If the name is already taken, the library keeps both and gives them distinct names. Filter, pick the one you meant, and use it. On a photo that has nothing to do with the original hero, choose **Presets… → Use preset…**. The categories stored in the preset are the categories that land. The photo keeps whatever you excluded. Save that photo's own `.omaphoto` with **Ctrl+S** when the result belongs to that file. The preset remains the preset. The sidecar remains that photograph. Build a second look on a different camera the next day. Name it. The first name is still in the library when you reopen the app. The two looks do not point at each other's originals. ## The edge A preset refuses to be a hidden link to one RAW. It has no original to rewrite, and it does not follow later slider moves on the photo you were viewing when you saved it. Change the hero, and the named look stays the values and categories you stored. A name clash refuses to delete either side. Import keeps both, with distinct names. You do not get a silent replace, and you do not get a merge that averages two grades into a third grade nobody made. Filter to the name, use the preset, and the categories you stored are the only ones that travel. ## The thread Part 75 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-batch-save-settings) · [Next](/blog/omadesign-0-5-8-whole-folder-apply) --- # Whole folder apply Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-whole-folder-apply Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, photo ## The habit Loading a thousand RAWs into a filmstrip so you can sync a grade is a tax. Lightroom will do it, and the catalog will own the folder afterward. Bridge plus Camera Raw will do it, one progress bar at a time, writing XMP beside the files if that preference is on. Affinity Photo would rather you open the images. None of those are wrong for a catalog person. They are the wrong shape when the folder is already the catalog. You shot a job into one directory. The files are named. The light is shared. You developed one representative frame, or you already have a preset called the name of that light. The hand wants: point at the folder, write the settings, go do something else. The hand does not want every subframe, every reject folder, and every export folder from last week pulled into the same operation because they live underneath. ## The constraint The camera files stay byte-for-byte. A folder apply that "develops" by rewriting RAW, or by exporting a JPEG on top of the original name, is a destructive pass with a friendly label. The only honest write is a `.omaphoto` beside each original. The sidecar is small. The pixels are not in it. Undo has to be possible after the fact, which means the job has to know the previous settings before it overwrites them. That bookkeeping has to fit in memory. The pictures themselves must not. One binary, one process, a UI that still has to track. The decode of a full shoot cannot be a prerequisite for writing sidecars. Filename recognition can find the photo files in the directory you named. It cannot promise that every camera file will decode. So the job asks you to open a representative RAW first, on purpose, before it writes the rest. You see the look on a real file from that folder. Then the folder gets settings. Subfolders stay out. A recursive walk feels thorough and ruins jobs. `2026-04-client/raw` and `2026-04-client/exports` and `2026-04-client/selects` are different decisions. The folder you browsed is the folder you meant. ## What landed The path is short, and every step is a choice. **Library → … → Browse folder…** points at one directory. Open a representative photo from that folder. Copy its adjustments, or choose **Presets… → Use preset…** if the look already has a name and a `.omapreset` behind it. In **Apply adjustments**, choose **Whole folder**, then **Write settings for N photos**. N is the count of supported photo filenames sitting directly in that folder. The job writes each original's `.omaphoto` in the background. Image pixels stay untouched. The app does not load the whole shoot into memory to do it. You can watch progress. File-specific errors stay visible. Cancel stops the work that has not started. What already finished remains, and Undo can restore those completed changes. Existing adjustments in categories you excluded are retained. Whole folder uses the same category boxes as a paste. Light / tone can land while Effects stays as it was on each file. Effects is the detail group. Tone curve, color mixer, and color grading travel only if you sent them. Crop and rotation stay off unless you included them. A photo that already had a careful crop keeps that crop when the look you are sending is only the grade. The representative open matters. Recognizing a filename is not the same as decoding a camera. Open one RAW from the folder, confirm the look on real pixels, then write the N sidecars. Invalid or mismatched settings that already exist are reported. They are not quietly replaced with a shrug. These folder writes are already saved when they land. That is different from a paste onto a loaded Library selection, which still needs **Ctrl+S**. Export is still the active photo only. Writing settings for N photos does not export N JPEGs. The job also refuses to scan subfolders. Files in child directories stay out of N. If those directories need the look, browse them on purpose and run them as their own jobs. ## In the hand Put the shoot in one folder. Leave the rejects in a different folder if they are a different decision. In Photo, open the Library menu and browse. ``` Library → … → Browse folder… ``` Open one RAW you trust from that directory. Develop it, then **Copy adjustments** with **Ctrl+Shift+C**. Or skip the hero and choose **Presets… → Use preset…** for a look you already named. Open **Apply adjustments**. Choose **Whole folder**. Read N. If N is the folder you think you are in, confirm **Write settings for N photos**. The writes run behind the viewer. Progress stays on screen. A file that fails names itself. The originals do not change. Beside `frame_0042.RAF` you get `frame_0042.RAF.omaphoto`, same pairing rule as a single save: the settings file matches the original's name, and it does not contain the picture. Cancel if the look is wrong. Remaining files stay as they were. Undo restores the sidecars the job already wrote, rolling the batch back as a batch. Redo can put that completed work back, and it will not stomp a sidecar that changed outside the job. That protection is the folder job's own rule. When you want a nested folder, stop and browse that folder itself. It gets its own N, its own progress, its own undo. ## The edge Whole folder refuses to recurse. Subfolders are not an accident of a deep walk. They are other folders, and they stay other folders until you browse them. It also refuses to load the shoot in order to describe it. Supported names directly in the chosen directory are enough to write sidecars. Decode is what you did on the representative frame before you confirmed the write. Pixels on disk are never the thing the job edits. Browse the folder, write settings for N photos, and leave every camera file exactly as the camera wrote it. ## The thread Part 76 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-omapreset-library) · [Next](/blog/omadesign-0-5-8-folder-job-limits) --- # Folder job limits Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-folder-job-limits Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, photo ## The habit You have pointed a batch at a folder before and learned what "the whole disk" feels like. Lightroom will queue a giant import and then spend the afternoon building previews you did not ask to watch. A script that walks every RAW and writes XMP will run until the laptop fans sing, and if you Ctrl+C it you get a folder that is half the new look and half the old one, with no honest way back. Bridge's batch is a progress bar that hates being closed. Affinity's export persona will tell you it failed on file 400 after it has already written file 399. The hand wants a number it can see, a cancel that means cancel, and an undo that knows which files the job actually touched. The hand also wants a ceiling. A folder of ten thousand frames is a job. A folder of a hundred thousand, with a settings history that will not fit, is how you lose the afternoon and the undo stack together. ## The constraint Whole-folder apply writes `.omaphoto` sidecars. It does not rewrite camera files, and it does not load the shoot into memory. Undo still has to restore the settings that were on disk before the job. That means the previous settings have to be held before the first write. If that bookkeeping cannot fit, the honest answer is to refuse the job, not to start writing and hope. One undo step is the rule everywhere else in the studio. A folder job that finishes two thousand files cannot ask you to undo two thousand times. The completed work is one batch. Cancel has to leave that completed slice undoable, and it has to leave the untouched files untouched. There is no Creative Cloud queue to resume this for you. The progress and the errors have to be on the screen you are already looking at. A sidecar is an ordinary file. Something else can write it while the job is in flight, or between the write and the undo. A sync tool, a backup restore, your own hand in another window. Undo that blindly puts the old bytes back will destroy that outside edit. The job has to notice, keep the outside file, and say so. ## What landed Folder jobs check that the undo data fits before they write. The ceilings are **10,000 photos** and **32 MiB of settings history** per job. A larger folder needs to be split into smaller folders. The check happens first. You do not discover the limit as a crash in the middle of the roll. Under the ceiling, **Write settings for N photos** runs in the background. Progress stays visible. Errors name the file. Invalid or mismatched settings that were already beside an original are reported. They are not smoothed over. Filename recognition still does not promise that every camera file will decode. Open a representative RAW from the folder before you commit the job, so the look is a look you have seen. Cancel stops the remaining work. Files the job has not reached stay as they were. Files the job has already written stay written, and Undo restores those completed changes as a batch. Redo can put that completed slice back. You cancelled a mistake. You did not lose the ability to reverse the part that landed. A file changed outside the batch is preserved and reported on Undo and on Redo. The job will not overwrite that sidecar with the copy it remembered. Your outside edit wins, and you are told. The camera original is never the file being restored. The argument is always the `.omaphoto`. These folder writes are already on disk when they succeed. That is a different state from a paste onto a loaded Library selection, which still needs **Ctrl+S** before the sidecars exist. Export remains the active photo. The limit, the progress, and the undo apply to settings files, not to a hidden JPEG pass. N itself is only the supported photo names directly in the folder you browsed. Subfolders are not part of the count and not part of the 10,000. If you need them, they are their own jobs, each with its own ceiling and its own history budget. ## In the hand Browse the folder. Open one real RAW. Copy the adjustments or use a preset. Choose **Whole folder** and read N before you write. If N is over 10,000, stop. Split the directory into smaller ones and run each. If the settings history for that N would pass 32 MiB, the job tells you before it writes. Smaller folders are the fix. There is no switch that raises the ceiling for one heroic run. ``` Write settings for N photos ``` Watch the progress. When a file fails, the line names it. Fix that file later, or leave it. The rest of the job is allowed to continue, and the error stays visible so you are not guessing which frame was skipped. Hit cancel when the grade is wrong. The bar stops. The files not yet reached keep their old sidecars, or keep having none. Press Undo. The files the job did finish return to the settings they had before the write. Press Redo only if you want that slice back, and read the report if a sidecar changed outside the app. That file stays as the outside edit left it. When the job is one you meant, leave it. The `.omaphoto` files are saved. Quit does not need a second save for those folder writes. Tomorrow, open any original or its sidecar and the pair comes back, pixels untouched. ## The edge The job refuses to start when undo data will not fit. Ten thousand photos is the count. 32 MiB of settings history is the other wall. Both are per job. Cross either one and the job does not start. Split the folder. Undo refuses the other wall. A sidecar that changed outside the batch is preserved and reported. The remembered bytes do not get to clobber it. Split the folder until N fits, write the settings, and Undo if the finished slice was the wrong grade. ## The thread Part 77 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-whole-folder-apply) · [Next](/blog/omadesign-0-5-8-photo-navigation) --- # Photo navigation Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-photo-navigation Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, photo ## The habit The grade is a lie until you have seen the eyelashes. Photoshop trained the hand a long time ago. Hold Space, the cursor becomes the hand, you drag, you let go, and you are back on the tool you were using. Ctrl+1 is actual pixels. Ctrl+0 fits the window. Affinity Photo uses the same pair, and the Hand tool is still H when you want it latched. Lightroom's develop view is the same idea with a different set of reminders: click the loupe, drag, fit, 1:1. Scroll-wheel zoom never agreed with itself. Some apps want Ctrl and the wheel. Some want Alt. A trackpad wants a pinch. You have all three habits in the same week, because you move between a mouse at the desk and a laptop on the couch. Middle-drag is the other one, from the apps that treat the wheel button as the hand. Two-finger scroll on a trackpad is pan in some tools and zoom in others. You find out by ruining your place in the picture. Photo in this studio has one more reason to keep the view cheap. The first thing you see is a preview. The full resolution shows up when you go looking for it. The hand has to be able to pan and zoom while that work happens, or a big RAW is a frozen window with a spinner where the eye should be. ## The constraint One binary means Photo does not get its own zoom language. **Ctrl+0** fits a Design artboard. **Ctrl+0** fits a photo. **Ctrl+1** is 100% in both places. The keys list is one list. **F1** shows it. The Shortcut HUD shows the tool you are holding. A photo persona that invented Fit as Ctrl+9 would be a studio you have to relearn every time you switch tabs. Space is already spoken for in Motion, where Space plays. In Photo there is no clip to play. Space belongs to the hand, the way it belongs to the hand in Photoshop when you are retouching. The persona is the switch. You do not hold a modifier to tell the app which Space you meant. You are in Photo, so Space drags. The view is not a develop slider. Panning and zooming cannot write a sidecar, cannot crop, and cannot become an undo step you have to peel off before you can undo a real exposure change. Crop is its own tool, **C**. Enter applies a crop drag. Esc cancels it and the status says the crop was cancelled. Navigation stays off that commit. The preview underneath is bounded so the UI can stay live. The first display preview has a maximum edge of 1600 pixels. Zoom in and the full-resolution detail is prepared in the background and shown as the tiles you can see. The pan and the zoom have to keep working while those tiles arrive. A navigation gesture that waits for the full decode has failed the reason it exists. ## What landed Hold **Space** and drag the photo. Release Space and the previous tool is yours again. Press **H** and the Hand tool stays down until you pick something else. **Z** is Zoom. **C** is Crop. **I** is the eyedropper. Those are the Photo tool keys. Hand is the one that matches a latched pan when you are going to be in the corner of the file for a while. Middle-drag pans. Two-finger scroll pans. You can move through a frame with the wheel button held, or with the trackpad gesture you already use to move a page. Pinch zooms. **Ctrl+scroll** zooms. **Alt+scroll** zooms. Both modifier-scroll habits do the same job here, so the week you spent in the other app still works. ``` Ctrl+0 fit the photo Ctrl+1 100% ``` **Ctrl+0** fits the photo in the view and clears the pan, so you are looking at the whole frame and not at whatever corner you had dragged into. **Ctrl+1** shows the photo at 100% and clears the pan the same way. You land on actual pixels, centered, which is the check for sharpening, noise, and whether Detail was a good idea. **Ctrl++** and **Ctrl+-** are the keyboard zoom steps, the same chords as the rest of the studio. They scale the Photo view while you are in the persona. Scroll and pinch are there when the hand is already on the pointing device. The keys are there when it is not. None of this writes `.omaphoto`. None of this moves a develop slider. The grade you set is the grade you set. The view is how you inspect it. Zoom in far enough and the full-resolution tiles fill in behind the magnifying glass while the preview stays responsive. You can grade a large RAW without the window locking up to prepare a private full-size bitmap you did not need yet. ## In the hand Open a RAW. The viewer shows the preview, max edge 1600. Fit it if the window has you cropped by accident. ``` Ctrl+0 ``` Hold Space. Drag until the eye, or the label, or the edge of the product is in the middle. Let go of Space. You are back on the tool you had, sliders still the sliders. If you want the hand to stay, press **H** and drag without holding Space. Pick another Photo tool when you are done panning. **Z** zooms. **C** crops. **I** samples. Release Space and you are back on the tool you were holding. Zoom into the detail: ``` Ctrl+1 ``` That is 100%, pan cleared. Drag again with Space, or middle-drag, or a two-finger scroll, to walk the frame at actual pixels. Pinch, or hold Ctrl and roll the wheel, or hold Alt and roll the wheel, to go further. The tiles for the full image prepare in the background. The view keeps moving while they land. You can see which parts are still the preview and which parts have caught up, because the detail arrives as visible tiles. Press **Ctrl+0** when you need the whole picture again. The pan offset goes away with the fit. Decide about the grade from the whole frame, then come back to 100% before you trust Detail. Crop is a different gesture. Press **C**, drag, Enter to commit, Esc to cancel. Fitting the view never commits a crop. A sidecar appears when you save settings, not when you drag the picture around. ## The edge Navigation refuses to become an edit. Fit, 100%, pan, and zoom do not write a `.omaphoto`, do not change crop or rotation, and do not take a step on the Photo undo stack. The camera file stays as it was. The sliders stay as you left them. Space in this persona refuses to play anything. There is no timeline under a photograph. Hold Space and you drag the view. Play is what Space does after you switch to Motion, on a clip that actually exists. Press Ctrl+1, hold Space, and drag across the real pixels before you trust the grade. ## The thread Part 78 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-folder-job-limits) · [Next](/blog/omadesign-0-5-8-rest-pose-intact) --- # Rest pose intact Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-rest-pose-intact Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, motion ## The habit You draw the logo where it belongs. Then you animate it, and the file you get back is the logo at frame 12. Illustrator will let you make the mark, and After Effects will let you move it, and the two files drift. The AE comp holds a snapshot, or a linked AI that somebody converted to shapes, and the "clean" version is whichever file you remember to open. Photoshop's timeline has the same trap on a smaller scale. You nudge a layer at one second, and the document's idea of that layer is now the nudge. Affinity Designer plus a separate motion tool splits the same way. The drawing lives here. The keys live there. Export a PNG from the wrong app and you ship the in-between. The hand wants one artboard that stays the artboard. Play is a mode. The mark, the type, the poster, those stay where you drew them when the playhead is home and when you export a still. You should be able to keep designing the thing after you have given it a little motion, and the motion should not be baked into the anchors. ## The constraint The project is one `.oma`. The manual's file line is blunt about it: JSON, rasters packed as PNG, and the motion clip, in that document. There is no sidecar timeline and no second binary for the clip. If Motion rewrote the vectors to match the playhead, the only copy of the logo would be the logo in motion. Undo would have to reconstruct the drawing from history every time you wanted the poster back. One undo step cannot be a promise if the rest pose was destroyed to make the play possible. Still export and animated export have to be different files for that reason. A PNG, a JPEG, or a static SVG is the picture. An animated SVG or a Lottie is the clip. Mix those up and every "quick PNG of the logo" becomes a random frame. The still has to be the artboard you drew. The clip has to live in the `.oma` until you export it on purpose. Linux does not owe you an After Effects install before a wordmark can fade. The persona switch is the whole trip: Design to draw, Motion to play. Same document, same layer stack, same undo model. The tracks have to be a small set you can explain, because a clip that can key every filter in the building will not round-trip honestly. The tracks are the ones the exports can tell the truth about. ## What landed The artboard you drew is the rest pose. Motion does not rewrite it. You can move the playhead, preview the clip, and come back to the drawing. The anchors, the type, the fills you authored stay the authored thing. The clip is additional data on that artwork. The tracks are **X, Y, rotation, scale, opacity, stroke reveal, and fill reveal**. Position and rotation and scale are the transforms. Opacity is the fade. Stroke reveal and fill reveal are how a path can draw on and how a closed fill can come up. That is the whole channel list. A preset, a dragged key, and `K` all write into those tracks. They do not invent a private effect stack that the drawing cannot show you. The clip lives in the `.oma`. Save the document and the motion saves with it. Open it on another machine and the rest pose and the clip open together. There is no library link to repair and no fonts-from-the-cloud step hidden inside the timeline. Project fonts, if you are using them, are a brand-kit question. The motion itself is in the document you already saved. PNG, JPEG, and static SVG stay the rest pose. Export one of those and you get the artboard you designed, not whatever frame the playhead was parked on. Animated motion leaves through **File → Export animated SVG…** or **Export Lottie…**. Those are the files that carry transforms and reveals. The still path is allowed to be boring. Boring is the logo, sitting where you drew it. Design keeps working on the same objects. Change a color, edit the type, move a node. The rest pose is that edit. The clip plays from that pose. You did not have to flatten, expand, or hand the file to another app to keep the right to keep drawing. ## In the hand Draw the mark in Design. Pen, type, whatever the poster needs. Save. ``` .oma ``` Switch to the **Motion** persona. The timeline sits under the canvas. The artboard does not jump, and the objects do not pick up a surprise transform. You are looking at the rest pose with a timeline attached. Select a vector object. Give it a preset, or press `K`, or drag it once the playhead is somewhere useful. Those gestures write tracks. They are the next essays. What you should see right now is that the drawing's own geometry is still the drawing. Go back to Design, move a node, come back to Motion. The node move is the pose. The keys you added are still keys. Export a PNG when you need the still. You get the rest pose. Export a static SVG when a developer needs the mark. You get the rest pose. The playhead can be sitting in the middle of a slam. The PNG does not care. The clip is still in the `.oma` the next time you open the document. When you want the motion outside the app, export animated SVG or Lottie from the File menu. The `.oma` you keep is still the editable pose plus the clip. You did not burn the logo to ship the animation. ## The edge Motion refuses to rewrite the artboard. Tracks sit on the objects. The objects stay the objects you drew. A still export refuses to sneak the playhead into the file. PNG, JPEG, and static SVG are the rest pose, even when playback is stopped on another frame. The clip refuses to live anywhere but the `.oma`. There is no second timeline file to forget in the folder. Lose the document and you have lost the motion, because the motion was the document. That is the trade for having one file that tells the truth. Save the `.oma`. The drawing you see at rest is the drawing you designed. ## The thread Part 79 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-photo-navigation) · [Next](/blog/omadesign-0-5-8-motion-presets) --- # Motion presets Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-motion-presets Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, motion ## The habit After Effects gave you a dozen ways to fade a logo and a project panel full of presets you do not remember installing. The good ones become ordinary keyframes after you apply them, and that is the part worth keeping. The bad ones stay a black box with a hidden slider, and you cannot ease the second key because the effect never became keys. Illustrator's limited animation, and the various "smart animate" buttons in other apps, often leave you with a transition object you cannot edit as a timeline. Affinity's timeline is closer to keys, and you still start most moves by keying opacity and position yourself. The hand wants a named start. Draw the stroke on. Pop the mark in. Slam it. Shake it. Fill it from empty. Slide it in from an edge. Fly, zoom, buzz, fade. Thirteen starts, not a marketplace. Then the hand wants those starts to become keys you can drag, delete, and ease like any other keys. A preset that remains a preset is a second editing mode. You already have a timeline. You also want a mixed selection to behave. The logo, the guide you used to place it, the locked caption, the hidden construction line. Apply "fade" to all of that and a sloppy preset will fade the guide, or it will error the whole selection because one object has no stroke. ## The constraint The rest pose stays the artboard you drew. A preset cannot flatten the path into a baked outline, and it cannot rewrite the object into a movie clip that Design can no longer edit. The tracks available are X, Y, rotation, scale, opacity, stroke reveal, and fill reveal. Every preset has to be made out of those tracks. If a move needs a track the clip does not have, it does not get to invent one and then fail on export. Each application has its own Undo. You can try Slam, undo, and try Pop in. The second apply does not fuse to the first. You do not accumulate a stack of half-applied effects. The `.oma` holds the clip. Presets do not need a network, a library login, or a pack downloaded beside the binary. They ship in the Motion inspector. The inspector opens on them, with appearance and manual key controls folded away until you need them. Duration sits above the list so the length is decided before the name. Objects that cannot take the preset have to be skipped, not damaged. A guide is not artwork. A locked object is locked. A hidden object is hidden. Draw stroke on a fill with no visible stroke is the wrong object. Fill up on an open path with no fill is the wrong object. The rest of the selection should still get the move. ## What landed Select vector artwork. In the inspector, choose one of: **Draw stroke. Pop in. Slam. Shake. Fill up. Slide up, Slide down, Slide left, Slide right. Fly. Zoom. Buzz. Fade in.** That is thirteen starts. Draw stroke traces the path. It does not fade the object in and call that a drawing. Fill up reveals the interior from the bottom. It wants a closed shape with a fill. Draw stroke wants a visible stroke. Those two are reveal tracks, stroke reveal and fill reveal, the same channels the native canvas, animated SVG, and Lottie already know how to play. Pop in, Slam, Shake, the four slides, Fly, Zoom, Buzz, and Fade in are the other starts. Each one becomes keys on the tracks the clip already has. The name is the description you get. Once the preset has run, you are looking at ordinary keys. Diamonds on the timeline. You can retime them, delete them, and cycle ease on a selected key the same way you would if you had keyed the selection yourself. Incompatible objects are skipped. Locked objects are skipped. Hidden objects are skipped. Guide objects are skipped. A selection that mixes a logo and a guide fades the logo and leaves the guide alone. A Draw stroke on a shape with no visible stroke skips that shape. A Fill up on an open path that has no closed fill skips that path. The objects that qualify receive the preset. The objects that do not stay as they were. Each application has its own Undo. The preset replaces only the channels it affects, inside the time interval it occupies, and it can extend the clip if the move needs more time than the clip had. Unrelated animation on other channels stays. You can Fade in a mark that already has a position move, and the position move remains. Duration is the control above the presets. Set it before you apply if this entrance needs to be long. Timing and energy, the delay and the stagger and the rest, are the next decision. The preset itself is the named start. ## In the hand Switch to Motion. Select the wordmark, the rule, the dot. Leave the guides in the selection if they are still selected from the layout. You do not have to deselect them first. Set the duration above the list. Choose the preset. ``` Fade in ``` Or Draw stroke, if the thing is a path with a stroke you want traced. Or Fill up, if it is a closed shape whose interior should rise from the bottom. Space previews. The timeline shows ordinary keys on the channels that preset owns. Press Ctrl+Z. That application leaves. The drawing is still the drawing. Apply a different name. That is a new undo step. The first one is gone because you undid it. They did not stack into a mystery effect. Select a locked caption along with the mark and apply again. The caption stays put. Select a guide and apply again. The guide stays a guide. Check the row. Keys appear on the objects that could take the preset. The skipped ones have no new diamonds. If you need the keys to start later than zero, or to stagger across a selection, open **Timing & energy** before you apply. The preset will still become keys. The timing controls decide when those keys sit. After they exist, drag the diamonds if the stagger was almost right. ## The edge A preset refuses to touch what it cannot honestly animate. No visible stroke, no Draw stroke. No closed fill, no Fill up. Locked, hidden, and guide objects are skipped. The skip is quiet and local. The eligible objects still get keys. Nothing in the skipped set is rewritten, unlocked, or shown. A preset also refuses to stay a preset. The result is ordinary keys on the real tracks. You edit those keys from here. There is no hidden effect left behind to keep the move captive. Select the vectors, pick the preset, and press Space. The keys on the timeline are the move. ## The thread Part 80 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-rest-pose-intact) · [Next](/blog/omadesign-0-5-8-timing-and-energy) --- # Timing and energy Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-timing-and-energy Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, motion ## The habit A preset with one duration is a coin flip. After Effects makes you set the work area, then the key times, then the easy ease, and a stagger is an expression or a script you paste in. The result is right, and the setup is a seminar. Illustrator-to-AE workflows push the same work into the comp after the art is "done," which is how timing becomes somebody else's job. Affinity's timeline will let you drag keys once they exist. Getting five objects to enter 80 milliseconds apart is still five manual drags, and you will miss. The hand wants the length first, then a small set of timing choices, then the named move. How long. How late. How far apart across the selection. How strong. Whether this entrance starts at the playhead you already parked, because the clip already has a first act. After that, the keys should be normal keys. If the stagger is a little wrong you drag diamonds. You do not reopen a wizard. You also want a second preset to leave the first move alone. Fade the opacity. Keep the position keys you already liked. A timing pass that wipes the whole row is how people stop using presets. ## The constraint The clip lives in the `.oma`. The rest pose stays the drawing. Timing controls cannot bake a new outline or a new object to "hold" the delay. They have to move keys on the tracks that already exist: X, Y, rotation, scale, opacity, stroke reveal, fill reveal. Undo is one step per application. Changing duration, applying, hating it, and undoing has to restore the channels that application wrote, and only those. If Slam and Fade in fuse into one history entry, you cannot try a timing without betting the move you already accepted. A preset that always starts at zero cannot coexist with a clip you have already built. Start-at-playhead is the control that respects the playhead as the start of this gesture. Extend-the-clip is the other half. If the move runs past the current duration, the clip grows. You should not have to open a document setting, lengthen the comp, then come back and apply. The apply sees the need and lengthens the clip. Replacing "everything on the object" would make stagger dangerous. The rule is narrower. Replace the affected channels, inside the interval this preset occupies. Keys outside that interval stay. Channels this preset does not use stay. Unrelated animation stays. That is how a wordmark can slide and then, later, fade, without the fade eating the slide. ## What landed **Duration** sits above the presets. Set it there. The presets underneath use that length. You see the number before you see Draw stroke, Pop in, Slam, and the rest, because a three-second fade and a third-of-a-second fade are different decisions and the decision comes first. **Timing & energy** opens four options: **delay**, **stagger**, **intensity**, and **start-at-playhead**. Delay holds the move off the start. The keys land later than they would have. Stagger spreads a multi-selection so the objects do not share one instant. The first object leads. The next follows. Intensity sets how strong the move is. A slam can be a small slam. A slide can be a short slide. You are scaling the energy of that preset, not adding a new track. Start-at-playhead uses the playhead as the beginning of this application. Park at one second, turn the option on, apply Fade in, and the fade belongs to that moment. Leave it off when you want the preset's own start. Combined with delay and stagger, you can put a group entrance where the clip already is, then fan the members. The preset still becomes ordinary keys. Timing does not leave a live "stagger object" on the row. You get diamonds. Drag them if the fan needs a correction. Delete one if one object should stay still. Cycle ease on a selected key if the curve is the remaining problem. Each application has its own Undo, so the timing you just tried is one Ctrl+Z. The write is local. Affected channels inside the interval are replaced. Other channels on those objects stay. Keys outside the interval stay. If the new keys need a longer clip than you had, the clip extends. You do not lose the earlier act to make room. You gain time at the end. Space previews the result in the persona. You hear the timing by watching it. Then you either undo or you keep the keys and keep drawing. ## In the hand Open Motion. Select the three lines of a headline, or the four shapes in a mark. Look at the duration above the preset list. Change it if this entrance is short. Open **Timing & energy**. Set a delay if the whole group should wait. Set a stagger if they should arrive as a sequence. Set intensity if the default energy is too much or too little for this artboard. Turn on start-at-playhead if the playhead is already where this move should begin. Move the playhead first if it is not. ``` Timing & energy delay · stagger · intensity · start-at-playhead ``` Apply one preset. **Slide up**, or **Fade in**, or **Pop in**. Look at the rows. The diamonds sit on the channels that preset uses, shifted by the delay, fanned by the stagger, sized by the intensity, and anchored at the playhead if you asked for that. Press Space. Watch one cycle. If the fan is right and the channel was wrong, undo and apply a different name with the same timing still set. That second apply is its own undo. The first is gone. If a position move was already on the object and you only faded, scrub the old position keys. They are still there. The fade occupied opacity inside its interval. It did not clear X and Y. If the clip was shorter than the move, scrub to the end. The duration grew to hold the keys. The rest pose at the start of the document is still the drawing. The extra time is extra clip, not a rewritten artboard. Drag a diamond when the stagger is one step off. You are editing keys now. The timing panel does not own them anymore. ## The edge An apply refuses to clear animation it does not own. Channels outside the preset stay. Keys outside the interval stay. A fade does not eat a slide. A second preset does not weld itself to the first undo. The apply also refuses to leave the clip too short for the keys it just wrote. The clip extends when the move needs the room. It does not clip the keys off the end and call that a duration. Set the duration, open Timing & energy, apply the preset, and press Space. One Ctrl+Z takes that timing back. ## The thread Part 81 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-motion-presets) · [Next](/blog/omadesign-0-5-8-keyframe-editing) --- # Keyframe editing Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-keyframe-editing Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, motion ## The habit In After Effects you turn a stopwatch on, move the playhead, and nudge the layer. The second key appears because the stopwatch was armed. Forget the stopwatch and you just moved the layer for the whole comp. Illustrator has no such habit on the artboard. You drag a shape and it moves. Affinity's timeline is closer to armed properties: you enable a channel, then you change it. The hand that draws all day wants the drag itself to be the key. You are a designer. The playhead is the time. The move is the key. A separate "record" mode is how keys get forgotten. The other habit is Delete. On a timeline, Delete might mean the key, the layer, or the animation, and the apps do not agree. You learn by destroying a logo once. The safe ladder is specific. Delete the diamond you clicked. Delete the animation when you meant the motion and not the art. Delete the object only when you ask a second time. The drawing survives the first swing. There is a third habit, from anyone who has ever keyed a position at one second and watched the object pop there from nowhere. If your first key is after zero, the pose at zero has to be the pose you drew. Otherwise the mark jumps at frame one and the rest pose was a fiction. ## The constraint Motion does not rewrite the artboard. A drag in the Motion persona cannot mean "change the rest pose and also, maybe, add a key." Those are different edits. In Design, a drag moves the drawing. In Motion, a drag writes keys at the playhead, and the rest pose stays the place you drew the object. The clip in the `.oma` stores the difference over time. The object stores the pose. The first key after zero has to plant the rest at zero, automatically, in the same edit. If you had to remember a second gesture, "add a hold at frame 0," you would forget it, and every animation would start with a pop. One undo step has to cover both the key you made and the rest key that makes it honest. They are one decision: animate from where I drew it. `K` has to key the transforms people actually block in: X, Y, rotation, scale. Opacity and the two reveals stay available to the presets and to the channels already on the row. `K` is the transform key, the one your finger can hit without looking at a channel list. It keys the selection. It does not key the guides you did not mean, beyond whatever the selection already is. You select first. Delete has to read the selection before it destroys anything. A selected diamond is a key. An object row, or a selection with no diamond selected, is the animation. The drawing is the thing after that. Three meanings, one key, in an order you can learn in a minute. Undo still exists. The ladder exists so you do not need undo to recover from a guess. ## What landed Select a shape in Motion. Drag it. That writes keys at the playhead. The channels that drag owns, the position you just changed, land on the row as diamonds. You did not arm a stopwatch first. The playhead was the time, the drag was the value. If that first key sits at a time greater than zero, the same edit plants the rest pose at 0. The object holds where you drew it until the animation leaves. Scrub to the start and you are looking at the artboard. Scrub forward and you see the drag you made. The plant is why it animates from the drawing and not from a default origin. `K` keys **X, Y, rotation, and scale** for the selection, at the playhead. Use it when the pose is already where you want this frame, and you need the transforms recorded without a drag. Press it on the selection. The diamonds appear on those channels. Combined with the plant rule, a first `K` after zero still gives you a rest key at the start, so the hold exists. Diamonds on the row are the keys. Drag a diamond to retime it. You are not opening a time field unless you want to. The diamond is the handle. Slide it later, slide it earlier. The value stays. The time changes. That is the whole retime. Click a diamond. **Delete** removes that key. The other keys on the row stay. The drawing stays. Click the object name on the timeline, or leave the keys unselected, and **Delete** removes the animation from the selected artwork. Every track on that object goes. The drawing stays on the artboard, at the rest pose, as artwork you can still edit in Design. This is the "remove the motion, keep the mark" step. **Delete** again, with the object still selected and the animation already gone, removes the object itself. That second press is the destructive one. You asked twice. The first ask was the clip. The second ask is the shape. Cycle ease lives on a selected key when you want the curve changed. Playback is Space, Home, and End. Those are the transport. The editing gestures are the drag, `K`, the diamond, and the Delete ladder. ## In the hand Put the playhead where the move should be felt. Select the shape. Drag it to the place it should occupy at that time. ``` drag at the playhead ``` Scrub to zero. The shape is back on the rest pose, because the first key after zero planted that pose at 0. Scrub to your key. The shape is where you dragged it. Press Ctrl+Z if the drag was wrong. The key and the planted rest go together. They were one edit. Move the playhead again. Press `K` if you want X, Y, rotation, and scale recorded here without another drag. ``` K ``` Retiming is the diamond. Grab it on the row. Drag it along the time. Play with Space to hear whether the move now lands on the beat you meant. You did not change the rest pose by dragging the diamond. You changed when the keyed value arrives. Remove one key: click the diamond, Delete. Remove the whole animation: click the object's name on the timeline so the diamonds are not the selection, Delete. The shape is still on the board. Select it and Delete again only when you mean to throw the shape away. If a drag created keys and you wanted a rest-pose edit, undo, switch to Design, and drag there. Design moves the drawing. Motion writes the clip. The persona is the choice. ## The edge Delete refuses to eat the drawing on the first press. A selected key loses that key. No key selected, the animation leaves and the artwork stays. Only the next Delete removes the object. A first key after zero refuses to leave the start of the clip empty. The rest pose is planted at 0 in the same edit, so the object departs from the artboard you drew. You do not get a pop from nowhere, and you do not get a second command to remember. Drag the shape at the playhead. The diamond is the move, and time zero is still the drawing. ## The thread Part 82 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-timing-and-energy) · [Next](/blog/omadesign-0-5-8-playback-controls) --- # Playback controls Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-playback-controls Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, motion ## The habit After Effects taught the hand a number pad. 0 on the numeric keypad RAMs a preview. Space also plays, except when a text field is focused, except when a different workspace stole it. Home and End move the playhead, and looping is a toggle you find in the preview panel after you have already watched the clip once and missed the end. Ease is a right-click, F9, or the graph editor, depending on which decade you learned. Photoshop's timeline is the smaller version of the same hunt. You press Space and sometimes you get the hand tool, because Space is pan in the document window and play only in the timeline. Affinity's photo persona uses Space to pan. Its timeline, when you are in it, wants play. The collision is the whole problem. One key, two jobs, and the app guesses from focus. For a simple entrance you do not want a RAM preview, a disk cache, or a round trip into another application to see whether the stagger feels late. You want Space. You want the ends of the clip. You want a loop while you nudge one diamond. You want the ease on the key you clicked, cycled, not buried in a graph you will not open for a fade. ## The constraint Motion is a persona, not a second binary. The timeline sits under the canvas of the same `.oma`. Playback has to run there. A preview that shells out to a browser, or that requires an After Effects install, breaks the reason the clip lives in the document. Simple motion, the tracks you actually have, should play on the artboard you are editing. Space cannot mean pan and play in the same persona. In Photo, Space drags the view, because a photograph has no clip. In Motion, Space plays, because the timeline is the point of the persona. The switch is the persona switch you already made. Home and End have to jump, and a jump during playback has to stop the play, or the playhead fights your jump and you chase it. The status can say play or pause. You should not need a hovering tooltip to know which one you are in. Ease belongs to a selected key. Cycling it is a small edit on that diamond. It is not a document-wide timing mode, and it is not a new track. Loop is a preview repeat. The control is an icon on the transport, the repeat icon, because a labeled modal for "loop yes or no" is too much chrome for a yes. Undo stays out of the transport. Playing is not an edit. Jumping is not an edit. Looping the preview is not an edit. Cycling ease is an edit, because the key changed. The history should record the curve, not the fact that you watched it. ## What landed **Space** plays. Press it again and playback pauses. The status follows: play, then pause. You are in the Motion persona, on the canvas, and the timeline under it is what Space drives. This is the preview. It is in the persona. For a fade, a slide, a stroke reveal, a fill coming up from the bottom, you do not leave for After Effects to find out if the move works. **Home** jumps to the start of the clip. **End** jumps to the end, the clip's duration. Both stop playback. You land, and the playhead stays where you sent it. You can inspect the rest pose at Home. You can inspect the settled frame at End. Then Space plays from where you are. **Loop** is the repeat icon. Turn it on when you are tuning a short entrance and you need to see it again without walking the playhead back yourself. Turn it off when you want a single pass so you can catch the end and leave it there. The icon is the control. The clip in the `.oma` is still the clip you saved. **Cycle ease** on a selected key walks the ease of that diamond. Click the key first. Cycle. Play. If the curve is worse, cycle again, or undo the cycle. The other keys keep the ease they had. You did not restyle the row by accident. The transport sits with the timeline you are already using. Presets, drags, and `K` write keys. Space shows you those keys. You can apply a preset, press Space, undo, apply another, press Space. The preview is the judge. Export is a different menu, later, when the preview is right. ## In the hand Switch to Motion. You already have keys, from a preset or from a drag. Press Space. ``` Space ``` The artboard plays the clip. Press Space again. It pauses on the frame you were seeing. Press Home. ``` Home ``` The playhead is at 0 and playback is stopped. That frame is the rest pose, the artboard you drew, plus any hold the first keys planted there. Press End. ``` End ``` The playhead is at the clip duration, playback stopped. Press Space to play from the end's neighbor, or press Home and Space to run it from the start. Click the repeat icon when you want the pass to continue. Nudge a diamond while it loops. Press Space to pause when you need the playhead to sit still for a drag that writes new keys. A drag during a serious edit wants a parked playhead. You know where the key will land. Click one diamond. Cycle its ease. Press Space and watch that key. If you cycled the wrong diamond, undo. Select the right one and cycle that. The command is per selected key. When the preview is the move you want, leave the persona or stay and keep editing. The `.oma` already holds the clip. A PNG export will still be the rest pose. Animated SVG and Lottie are how the motion leaves, and only when you ask. ## The edge Playback refuses to be an export, and it refuses to be a trip into another application. Space plays the tracks on the artboard in front of you. Home and End only move the playhead, and they stop play while they do it. They do not delete keys and they do not change the rest pose. The repeat icon repeats the preview. Cycle ease changes the key you selected. The other keys keep the ease they had. Press Space. The clip plays where you drew it. ## The thread Part 83 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-keyframe-editing) · [Next](/blog/omadesign-0-5-8-export-animated-svg) --- # Export animated SVG Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-export-animated-svg Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, motion ## The habit You need the animation on a page. After Effects will give you a render, a Lottie from a plugin, or an SVG if you have done the extra dance with an extension. The SVG you get from a static Illustrator export is the art, dead. A developer who asks for "the SVG, but moving" usually receives a video, or a Lottie JSON, or a static file plus an apology. Photoshop's timeline export is a movie. It is the wrong file for a mark that was vectors this morning. The hand wants one menu item. The file should carry the transforms you keyed, the stroke that draws on, the fill that rises. If the artboard uses a mask, the mask should still be a mask in the SVG. If an effect is on the object, the effect should survive that export. Text is the sharp edge. A live font in an SVG depends on the font being installed on the visitor's machine. A reveal that follows glyph shapes depends on those shapes being in the file. You have been burned by a headline that falls back to a default face and a reveal that no longer matches the letters. You also want the source. Outlining type in the working document is a door you cannot open again. The export can outline. The `.oma` must not. ## The constraint The clip and the drawing live in one `.oma`. The rest pose stays the rest pose. A still PNG, JPEG, or static SVG is that pose, on purpose, so a casual export cannot become a random frame. The animated file has to be its own command. **File → Export animated SVG…** is that command. It has to write the channels the timeline actually has: animated transforms, stroke reveal, fill reveal. Inventing a second animation model at export time is how the preview and the file diverge. Masks and effects are part of the composition you can already see. Dropping them silently would ship a cleaner file than the one you approved. This export keeps them. Lottie, beside it in the File menu, cannot keep pixel layers, layer masks, and effects, and it says so with an error. The SVG path is the one that holds those compositions. The split is the constraint of the two formats, made visible as two commands. Text has to match the canvas in the exported file. Glyph geometry and reveals have to agree. The way to guarantee that, without a network and without embedding a font you may not have the right to embed, is to outline the text in the export. The source text in the `.oma` stays editable. Project fonts, when you use them, already outline on SVG export for the same reason: the shared file keeps its appearance, the document keeps its carets. Native file dialogs are the save UI everywhere else. This export asks for a path the same way. It does not upload the SVG anywhere. ## What landed **File → Export animated SVG…** writes an SVG with the animated transforms and the stroke and fill reveals. X, Y, rotation, scale, and opacity travel as the transforms you keyed. Stroke reveal travels as the draw-on. Fill reveal travels as the fill coming up. The native canvas, this SVG, and Lottie share those reveal channels, so a Draw stroke you previewed with Space is the Draw stroke in the file. A Fill up from the bottom is the Fill up in the file. Masks stay. Effects stay. A composition that depends on them does not get simplified into naked shapes. You approved the masked mark. The SVG is the masked mark, moving. Text is outlined in the exported file. The glyphs become geometry, and the reveals follow that geometry, so the letters on the page match the letters on your artboard. The font does not have to be installed on the machine that displays the SVG. The source text in the `.oma` is untouched. Open the document, double-click the type, and you are editing text. The outline existed in the export, for the export. The `.oma` remains the complete editable animation. Tracks, rest pose, live type, the layer stack. The SVG is a delivery file. Edit in the document. Export again when the edit is real. You do not round-trip the SVG back into the source to keep working. Import paths exist for Lottie and for static SVG as artwork. The working clip is the document you saved. A static SVG export is still the rest pose. If the menu you hit does not say animated, you asked for the still. Use the animated command when the page needs the clip. ## In the hand Finish the move in Motion. Space to preview. Home to see the rest pose. The keys you mean are on the timeline. Save the `.oma` first, the way you save before any export that leaves the machine. ``` File → Export animated SVG… ``` The native file dialog asks where the SVG goes. Name it for the page, not for the working file. Write it. Open that SVG where you will use it. The transforms play. A stroked path draws on if you used stroke reveal. A closed fill rises if you used fill reveal. Masks you set are still masking. Effects you set are still in the file. Headlines are outlines. They match the glyph shapes you saw on the canvas, including the way a reveal crosses those shapes. Go back to the `.oma`. Click the type with the Type tool. The caret is there. Change a word. The change is in the document. The SVG you already wrote still has the old word, outlined, which is what an export is. Export animated SVG again when the new word should ship. The new file outlines the new glyphs. The document still has text. If the composition has pixel layers, layer masks, or effects and someone asks for Lottie, this SVG is the file that can hold them. Lottie will stop on those and tell you. You already have the command that does not have to stop. ## The edge The export refuses to outline the source. Text becomes geometry in the SVG so the glyphs and the reveals match a machine that does not have your fonts. The `.oma` keeps the text editable. You can export ten times and the type tool still edits type. It also refuses to drop masks and effects on the way out. Those stay in the animated SVG. A still export remains a different file: PNG, JPEG, and static SVG stay the rest pose, playhead or not. Choose File → Export animated SVG… when the page needs the clip and the `.oma` needs to keep the words. ## The thread Part 84 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-playback-controls) · [Next](/blog/omadesign-0-5-8-export-lottie) --- # Export Lottie Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-export-lottie Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, motion ## The habit A developer asks for Lottie. In After Effects that means Bodymovin, a plugin, a composition that has already been beaten into shape layers, and a JSON file the player on the site can read. You learn the unsupported list by shipping a JSON that plays nothing like the comp. Expressions dropped. Effects dropped. A mask that was a layer mask becomes a blank. Sometimes the plugin warns. Sometimes the player warns. Sometimes the homepage is simply wrong until somebody notices. Illustrator does not export that JSON. Photoshop does not. The habit those apps trained is: go to AE, convert, pray, export. Affinity does not sit in that pipeline either. So the hand that lives in a design tool still ends the week in a different tool, rechecking trim paths, because trim paths are how a stroke "draws on" in the Lottie world, and fill masks are how a fill reveals. You want the menu here, and you want it to fail in words when the file cannot tell the truth. A quiet drop of the pixel layer is worse than a hard stop. You can choose another export. You cannot guess which half of the comp survived. ## The constraint The clip in the `.oma` can hold tracks on vector artwork: X, Y, rotation, scale, opacity, stroke reveal, fill reveal. It can also sit in a document that has pixel layers, layer masks, and effects, because the studio is one document. Design, Pixel, and Motion share the file. A Lottie exporter that pretends to carry all of that will lie. Bodymovin shape animation has a shape it can carry. Pixels and layer masks and effects are outside that shape for this exporter. The honest constraint is a clear error. The export does not half-apply. It does not write a JSON that plays the vectors and omits the pixels without telling you. It stops, and it says why. The other command, **File → Export animated SVG…**, retains masks and effects. That is the door for the compositions this exporter cannot preserve. Two commands, two ceilings, both visible. What Lottie can take should match the preview. Stroke reveal becomes trim paths. Fill reveal becomes fill masks. The same Draw stroke and Fill up you played with Space are the channels in the JSON. The version target is Bodymovin 5.x shape animation, the dialect players already speak. You do not get a private JSON that only this app can read. The rest pose stays in the document. Exporting Lottie does not bake the playhead into the artboard, and it does not outline the working text inside the `.oma`. The JSON is a delivery file. The `.oma` is the editable animation, and for anything beyond the basic shape subset you keep the `.oma` anyway. ## What landed **Export Lottie…** writes Bodymovin 5.x shape animation. Trim paths carry the stroke reveals. Fill masks carry the fill reveals. Transforms on the vector shapes travel with them. A mark built from shapes, with a draw-on stroke and a fill that rises, is the file this command is for. Preview it in Motion. Export it. The player that speaks Bodymovin 5.x is the audience. Pixel layers cannot be preserved by this exporter. Layer masks cannot. Effects cannot. The export produces a clear error. You are told. You do not receive a partial JSON that looks successful and plays incomplete. Take that composition to animated SVG, which keeps masks and effects and writes the animated transforms and reveals there. The error is the feature. A batch of homepage icons that are pure shapes should export. A poster that is a photograph with a vector headline animated on top should not sneak out as a Lottie of the headline alone. The photograph is part of what you approved. The exporter refuses to ship the headline as if it were the whole composition. You pick SVG, or you rebuild the motion from shapes if Lottie is a hard requirement from the developer. Still exports stay on their own road. PNG, JPEG, and static SVG remain the rest pose. Lottie is the clip. You do not use it as a still, and you do not use a still as a stand-in for the JSON. Import is the return path for a shape-layer Lottie, and it is a basic subset. The complete editable animation is still the `.oma` you saved before you exported. Treat the JSON as something you hand over. Treat the document as the place you keep working. ## In the hand Build the motion from vector shapes. Draw stroke on a path that has a visible stroke. Fill up on a closed shape that has a fill. Transforms on the same objects if they need to move. Space to preview. Save the `.oma`. ``` Export Lottie… ``` If the document is shapes and the tracks are the tracks this exporter knows, you get a JSON in the Bodymovin 5.x shape form. Trim paths are the stroke reveals. Fill masks are the fill reveals. Hand that file to the page. If the document contains a pixel layer, a layer mask, or an effect, the export stops with a clear error. Read it. The `.oma` is unchanged. The rest pose is unchanged. Nothing was half-written into a JSON you might accidentally commit. ``` File → Export animated SVG… ``` Use that command for the composition the error named. Masks and effects stay. Text in that SVG is outlined so the glyphs match, and the source text in the document stays editable. The developer gets an animated SVG. You keep the file you can still edit. When the developer insists on Lottie and the error was a pixel layer you do not need in the motion file, duplicate the idea into a shape-only document, or remove the offending layer from a copy, and export Lottie from the composition that qualifies. Do not argue the original poster into a silent strip. The error was the correct result on the file that had the pixels. ## The edge This exporter refuses to preserve pixel layers, layer masks, and effects. It does not drop them and continue. It stops with a clear error, and the document stays as it was. Animated SVG is the export that can carry those compositions. It also refuses to be the home of the editable animation. The JSON is Bodymovin 5.x for delivery. The full clip, including anything outside the shape subset, stays in the `.oma`. Export Lottie… when the art is shapes. Read the error when it is not, and export animated SVG for that file. ## The thread Part 85 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-export-animated-svg) · [Next](/blog/omadesign-0-5-8-import-lottie) --- # Import Lottie Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-import-lottie Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, motion ## The habit Somebody sends a Lottie. A Bodymovin JSON from After Effects, or a file you exported last week and now need back on a timeline. The AE habit is to open the comp, if you still have the comp. The JSON is a delivery format. Round-tripping it back into editable shapes is a partial trick at best. You get shapes, some transforms, some trim paths, and a list of features that became a single baked group or vanished. Illustrator will not open the JSON as a timeline. You drop the JSON into a preview site, nod, and ask for the AEP. The hand still wants the JSON on the canvas when the AEP is gone. A logo animation arrives as a file. You need it in the same document as the poster, on a timeline you can scrub, with Space to play it. You also need to know the ceiling before you promise a client you can "just tweak the Lottie." Basic shape layers can come in. The full editable show is the `.oma` if you made it here, and it is not hiding inside a JSON that was only ever a subset. Dropping a file on the canvas is the other habit. Images place. An `.oma` opens. A Lottie should import. You should not need a different door for a file the app already knows how to write. ## The constraint One `.oma` holds the clip. An import has to become objects and tracks in that document, on the timeline under the canvas, or it is a preview window and not an import. The rest pose rule still applies after the import. The artwork that lands is the artwork. The keys are the clip. A still PNG, JPEG, or static SVG export of this document remains the rest pose. Importing a Lottie does not switch the still exporters over to "current frame." You keep one rule for stills, whether the clip was authored here or brought in. The importer's ceiling is a basic shape subset. Bodymovin files carry more than this studio promises to rebuild as editable objects. Promising the whole dialect would mean silent substitutions, the kind of import that looks open and is not the animation. The subset is the contract. Shape layers. The tracks this timeline knows. What does not fit does not pretend to fit. Because the subset is basic, the `.oma` remains the file you keep when you want the complete editable animation. Export Lottie for delivery. Import Lottie to bring a shape-layer file onto the timeline. Save `.oma` if this document is now the working copy. The JSON you imported can stay in the folder as the delivery original. It is not a live link back to a cloud library. There is no Creative Cloud dependency in the import. The file is a file. The native dialog, or a drop, is the way in. The document you saved before the import is the way back if the file was wrong and you do not want to keep the result. Save the poster first. Import second. A bad JSON does not get to be the only copy of the job. ## What landed **Import Lottie…** brings a shape-layer Lottie onto the timeline. The shapes become artwork in the document. The animation they carry becomes the clip you scrub in Motion. Space plays it. Home and End jump. The repeat icon loops the preview. You are in the same persona you use for a preset you made yourself. The subset is basic shapes. **Export Lottie…** writes Bodymovin 5.x shape animation with trim paths and fill masks. **Import Lottie…** brings a shape-layer file back onto the timeline, and the manual is plain that this import is a basic subset. Keep the `.oma` when you need the complete editable animation. A JSON full of effects, images, and layer types outside that subset will not rebuild the comp you remember. Plan on the shapes that arrive. Author the rest here, or go get the original document. Dropping a Lottie on the canvas imports it, the same way the file rules treat a drop: layered documents open, ordinary images place, `.oma` opens, Lottie imports. **Import Lottie…** is the menu when you want the dialog. The drop is the menu when the file is already in your hand. PNG, JPEG, and static SVG export stay the rest pose after the import, as they do for any other clip. The playhead can sit mid-animation. The still is the rest pose of the document. Animated SVG and Lottie remain the exports that carry motion out again. If you imported a JSON, edited what the subset gave you, and need to deliver, export again from the File menu. The `.oma` is the version that still has the editable result. If this studio authored the animation, prefer the `.oma` over a round trip through JSON. Export, import, export will not grow fidelity. The document already has the complete clip. The import path is for a Lottie that arrived from outside, or for a shape JSON you need to place into a document and keep. ## In the hand Save the poster you are adding the mark to. Then: ``` Import Lottie… ``` Pick the JSON in the native file dialog. A shape-layer file lands on the timeline. Switch to Motion if you are not already there. Press Space. Scrub. Look at the rows. The keys you can see are the keys you can edit: drag a diamond to retime, click a diamond and Delete to remove that key, press `K` to key transforms on a selection. If the file was the wrong one, close it out of the document before you save, or go back to the `.oma` you saved before the import. Choose the other JSON. Drop a `.json` Lottie on the canvas when you do not want the menu. It imports. Drop a PNG and you place an image. Drop an `.oma` and you open that document. The suffix is the decision. Edit what came in. Save the `.oma`. That save is the working animation. The original JSON is still the original JSON on disk, unchanged by your edit, because you edited the document. Export Lottie again if the outside world needs a new delivery file. Export a PNG if they need the still, and know you are handing them the rest pose. When the imported file is only half the motion you remember from AE, stop. The subset ended. Ask for the source comp, or rebuild the missing move here with presets and keys. The timeline will not invent the tracks the JSON did not give you as shapes. ## The edge Import refuses to promise more than a basic shape subset. A shape-layer Lottie can land on the timeline and be edited as objects and keys. The rest of the Bodymovin dialect is not reconstructed into a complete comp. The `.oma` you save afterward is as complete as the subset that arrived, plus whatever you author on top. Still export refuses to follow the playhead. PNG, JPEG, and static SVG stay the rest pose. The imported clip leaves again only through animated SVG or Lottie. Choose Import Lottie…, press Space, and save the `.oma` if this timeline is now the one you will edit. ## The thread Part 86 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-export-lottie) · [Next](/blog/omadesign-0-5-8-fifty-two-templates) --- # Fifty-two templates Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-fifty-two-templates Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, templates ## The habit The template gallery you know is a store. Illustrator opens a grid of presets that want a network, a login, or a download before the third card will render. Photoshop's new-file dialog mixes blank sizes with Adobe Stock templates that are not on the machine. Affinity's new document is cleaner: sizes, a few presets, then you are on your own. When you do want a start, you hunt a file someone mailed you in 2019. The hand wants a bank that is already here. Search by the thing you are making, not by a SKU. A café card. An essay cover. A ticket. Filter when the search is too wide. Pick the page size you actually print, or type a width, a height, and a DPI that the built-in list does not have. See the layout at those proportions before you commit, because a square preview of a tall poster is a different poster. You also want the blank page to stay easy to find. A template gallery that replaces "new document" is a gallery in the way. The welcome screen can offer the bank and the blank. The File menu can open the bank while you are already drawing, without closing the file you have open. ## The constraint Fifty-two designs ship in the binary. They are available immediately. There is no weekly download, even though a weekly drop plan exists as an editorial sequence and a remix prompt for each design. That plan does not schedule posts and it does not publish anything. The gallery cannot wait on the network to decide which cards exist. Local fonts only, on purpose, so opening a card does not phone home for a typeface. The gallery has to stay light. Preview rendering runs in the background. Thumbnails stay small. The grid lays out the visible rows. Choosing a size reflows the preview to those proportions, including portrait, square, and wide pages, and including a custom size. A gallery that renders all 52 designs at all 20 built-in sizes up front will hitch the welcome screen. You are here to start a file, not to watch a contact sheet bake. **+ Vector** is this bank. **+ Layout** is a different door: frame-based starters for screens. Raster opens a size chooser. The template library is not allowed to swallow those. One welcome screen, three kinds of start, and the 52 are the vector designs. Search has to hit name and idea. The nine categories are the coarse filter: events, food, culture, community, editorial, branding, education, wellness, and products. Thirteen artwork families sit under the 52 compositions. You should be able to find Palm House because you thought "café," and Still Water because you thought "essay," without memorizing the week number in the editorial plan. ## What landed On the welcome screen, **+ Vector** opens **Templates · 52**. While you are drawing, **File → Template library** opens the same bank. The chooser has a stable width. You search by name or by idea. You filter the nine categories. You pick any of the 20 built-in document sizes, or you type a custom width, height, and DPI. Previews adapt to the proportions you chose. A tall size shows the tall layout. A square size shows the square layout. A wide size shows the wide layout. The designs include distinct artwork for those three, so the preview is not a uniform scale of one master. Very small custom sizes drop secondary copy that would be unreadable. You see that in the preview before you create the file. Click a card and press **Use this template**, or double-click the card. The template opens as a new unsaved document. Paper, artwork, and copy layers are editable. The file you were already working on stays in its tab. You did not replace it. You did not have to save it first in order to look at a starting point. Save the new document when it becomes yours. Until then it is an unsaved document, like any other new file. The 52 are specific. After Hours is an events poster. Palm House is a food card for hours without a stock photograph. Still Water is a quiet editorial cover. Admit One is a ticket you can also use as the poster. First Edition is a numbered cover for a newsletter. The weekly plan pairs each one with a remix prompt. That prompt is a suggestion for how to edit. It is not a lock on the file. All 52 are in the gallery on day one. You do not wait for week 29 to open Admit One. Dates, venue lines, and sample business copy in the bank are placeholders. They were written for these designs. You replace them before you publish. The artwork was made for Omadesign. It is vectors and live type, not a flattened poster with a text box floating near it. ## In the hand Launch the app. On the welcome screen press **+ Vector**. Or, with a document already open: ``` File → Template library ``` Type an idea. "Café," "ticket," "essay," "workshop." Or type a name you already know: Palm House, Admit One, Night School, Open Alphabet. If the list is still wide, filter. Food. Events. Editorial. Education. The nine are events, food, culture, community, editorial, branding, education, wellness, and products. Pick the size. A built-in page size if you print standard stock. Or type the width, the height, and the DPI for the slot this file actually has to fill. Watch the card. The preview follows those proportions. If the size is tiny, expect the secondary lines to leave so the remaining type can be read. Double-click the card. Or click it once and press **Use this template**. A new tab opens. The previous tab is still there. The new document is unsaved. Click the headline and type. The type is live. Click a shape and recolor it. The shape is a shape. Save when the file has a name you want on disk: ``` Ctrl+S ``` You get an `.oma`. The template is a start, not a link. Editing it does not write back into the gallery. Next time you open Templates · 52, Palm House is still Palm House. Need a screen with frames? That is **+ Layout**, the frame-based starters. Need a blank pixel canvas? Raster, and a size. Need a blank vector page with no artwork? The vector file icon beside **+ Vector** is the blank size chooser. The 52 are the designed starts. ## The edge The gallery refuses to fetch a design. All 52 are local, and the weekly drop plan does not gate them. Search and the nine filters only narrow what is already on disk. A custom size refuses to keep secondary copy that would be unreadably small. The preview shows the omission before you create the document. Opening a template refuses to close the work you already have. The new file is a new unsaved tab. Your current tab stays put. Press + Vector, double-click the card, and the unsaved document is yours to edit. ## The thread Part 87 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-import-lottie) · [Next](/blog/omadesign-0-5-8-templates-offline-editable) --- # Templates offline editable Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-templates-offline-editable Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, templates ## The habit You have opened a template and discovered it was a picture. The headline was outlined, or it was a JPEG, or it was a text frame linked to a font the template downloaded only while you were online. Illustrator's stock templates and Photoshop's cloud documents train that disappointment. You wanted to change the date. You got a flatten, or a missing-font dialog, or a spinner because the machine was on a train. Affinity's templates are closer to real documents when they are local files you already saved. The habit you want is that, from the first open. Paper you can resize the content on. Shapes you can recolor one by one. Words you can rewrite with the Type tool. No account. No request out to a font server. The file is unsaved until you decide it is a job, so browsing a start does not litter your disk with fifty-two "Untitled" files you never meant to keep. The other habit is the tab. You are mid-poster. You want to steal a structure from the gallery. The gallery must not eat the poster. New tab. Old tab still dirty, still there, still undoable. ## The constraint The 52 ship inside the app. Fonts come from the local system. The bank does not download fonts and it does not redistribute fonts. If a face is not installed on this machine, the template cannot conjure it from a CDN. That is the offline rule, and it is also the license rule. You work with the faces the computer already has. Project fonts in a brand kit are a different feature, for a job that has already decided to carry its own TTF and OTF files. The gallery itself does not reach for them over the network. There is no network step in opening a card. One `.oma` per document, unsaved until you save. A template is not a linked smart object back to the gallery. Edits stay in the new document. The gallery's copy of Palm House stays the gallery's copy. Undo in the new tab is that tab's undo. It does not rewind someone else's open file. The layouts cannot be one design scaled blindly. Portrait, square, and landscape pages have distinct artwork and distinct arrangements. A wide page is composed as a wide page. A tall page is composed as a tall page. A square is composed as a square. Custom sizes reflow into that system. Very tiny sizes omit secondary copy that would be unreadable. Keeping a line that has collapsed into grit is not a usable page. The omission is the layout decision for that size. Existing work stays in its tab because the studio is a document app, not a single canvas. You can hold the job and the candidate start at the same time. Save the one you mean. Close the one you do not. ## What landed A template opens as an unsaved document. The layers you get are the layers you edit: paper, artwork, and copy. Paper is the page. Artwork is the vector construction, shapes you can select, recolor, duplicate, and delete. Copy is live type. Double-click it and type. The first keystroke replaces a placeholder the way type works everywhere else in Design. Dates, venues, and sample business lines are creative placeholders. Change them before you publish. They were written for the bank. They are not your event. The document you already had open stays in its tab. The template does not replace it, does not merge into it, and does not force a save dialog on it. You can look at a start, decide it is wrong, close the new tab, and land back on the job. If you never saved the template tab, nothing was written. Fonts are the local fonts. Open the file on a train, on a machine with no network, in a room that blocks the obvious ports. The card still opens. The type still uses faces this computer can draw. The bank does not fetch a family to complete the look. If you need a face that is not installed, install it on the system, or bring it in later through a project typography kit as a file you copied yourself. The template open does not do that for you. Portrait, square, and landscape are separate compositions across the 52. Pick a built-in size or a custom width, height, and DPI in the gallery, and the preview adapts before you commit. The file you then get matches that preview, including the decision to drop unreadable secondary copy at very small sizes. You are not handed a full text stack shrunk until it turns to grit. The primary lines remain. The lines that cannot survive the size are gone, on purpose. Thirteen artwork families, 52 compositions, all local. The weekly sequence in the drop plan is an editorial order and a remix prompt per design. It does not delay a file. Week 40 is not a release gate. Local Network is in the gallery today if the category is community and the idea is a group of businesses. Everything in the new tab is ordinary document content. Recolor a leaf on its own. Duplicate a petal and move it. Rewrite a two-word manifesto. The pathfinder, the type panel, the swatches, the undo stack: they are the same tools as any `.oma` you drew from scratch. Save and you have a normal project file. The template gallery has no claim on it. ## In the hand Open **Templates · 52** from **+ Vector** or from **File → Template library**. Choose the proportions. Double-click the card. Look at the tab bar. The old document is still named there. The new tab has no filename yet. Click a word. ``` T ``` Type the real headline. Press Esc or click away to finish the text edit. Select a shape. Change its fill from the swatch or the inspector. Select another shape and change that one alone. The construction is not a single flattened group you have to release before you can recolor a part. Where a group exists, it behaves like any group: double-click in to edit a child. Try this on a machine with the network off. The document is already open. No font fetch starts. No "sign in to load this template" appears. The faces are the local faces. Make the size small on a second try. Open the gallery again, enter a tiny width and height, and watch the preview. Secondary copy that would not read is omitted. Create that document only if the remaining type is the type you want. The full wording still exists on the larger sizes. Pick the size that matches the slot. When the tab is good, Ctrl+S and name the `.oma`. When it is not, close the tab and discard. The gallery is unchanged. The other document is unchanged. Its undo stack is its own. ## The edge A template refuses the network. No font download, no account, no remote card. Local faces only. It refuses to overwrite the gallery when you edit, and it refuses to touch the tab you already had open. At very small sizes it refuses to keep secondary copy you could not read. The preview shows that omission. Choose a larger size if you need those lines. Double-click the card. The unsaved tab is editable paper, artwork, and type, and the job you were in is still the other tab. ## The thread Part 88 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-fifty-two-templates) · [Next](/blog/omadesign-0-5-8-three-right-tabs) --- # Three right tabs Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-three-right-tabs Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, palettes ## The habit You already know where your hand goes. Illustrator's Properties panel is the thing you selected. Swatches is the colors you refuse to re-mix. The Libraries panel, or a CC library, is the logo and the approved images. Those are three places, and the libraries one wants a login. Photoshop splits the same jobs across Properties, Swatches, and whatever library or asset panel the current version is pushing. Affinity Publisher puts the resource on a studio tab and the selection on another, and people who live there like that the page and the brand sit in one application. The people who do not live there keep Designer, Photo, and Publisher open because the habit was three apps. The job in the chair is smaller than a suite. You have an object selected. You need its fill, its type, its position. You need the brand red, not a red you sample by eye. You need the logo, the pattern, the font role, dropped on this artboard. You want those three without a panel safari and without a second install. The sidebar has to stay a sidebar. A floating pile of palettes from 2004 covers the poster. A tab in the right column does not. When the window is short, the panel has to scroll. The actions at the bottom still have to be reachable. Compact height is a real desk, a laptop, a tiled window. The chrome does not get to assume a 4K monitor. ## The constraint One binary. Design, Layout, Pixel, Photo, and Motion are personas, not applications. The right column has to serve the document you have open in that one process. Inspect reads the selection. Palettes apply color onto it. Brand places artwork into it. If those lived in three windows, you would be back to three apps and a clipboard between them. The document is one `.oma`. The brand is not required to live inside that JSON. Colors, font roles, and asset files sit beside the work as project files, so a second document in the same folder sees the same bank. The sidebar is the door to both: the selection in this file, and the kit next to it. A cloud library would put an account between those. There is no account in this column. Personal palettes are the colors you want across your work. Project palettes and the brand bank are the folder. The tabs have to make that split obvious, or you will edit a swatch and not know whether you changed this job or every job. Undo for the artwork stays the document undo. Palette saves and brand-kit saves are their own save state. The tabs have to show that separation in how they behave, not in a lecture. You can place a logo and Ctrl+Z the place. You do not Ctrl+Z the existence of the file in the bank. Different gestures, different tabs, one column. Photo can double-click a brand asset and land it in Design. The Brand tab is not trapped inside one persona. The selection you inspect might be a vector in Design or type on a layout frame. The column stays put when you switch personas, because the kit did not change when the tool did. ## What landed The right sidebar has three tabs. **Inspect** is the selected artwork. Fill, stroke, type, the properties of the thing you clicked. This is the panel you live in while you draw. Select a rectangle and Inspect is that rectangle. Select type and Inspect is that type. The Shortcut HUD at the bottom of the window still tells you the tool. Inspect tells you the object. **Palettes** is reusable color. Personal for colors you want across your work. Project for colors stored with the current folder. You build a named palette, filter it, add the current color or the selection's fill and stroke, type a hex, and apply to Fill or Stroke by clicking a swatch. The palette has its own Save. That save is not the artwork save. **Brand** is logos, images, fonts, and other assets. Load a bank, create one, add files, filter the tiles, drag one onto the canvas. Typography lives here too: font files copied into the project, roles like Heading and Body, Apply on the selected text or the next text you create. The bank is a folder of real files. The tab is the browser for that folder. Three tabs, one column. You do not dock and undock a swarm. You do not open a separate brand application to grab a leaf logo and a green. Affinity Publisher is the muscle memory this matches: the resource and the selection in one piece of chrome, and you are not keeping three apps alive to get it. The difference in this studio is that Design, the page, the photo, and the motion are already the same binary. The column does not have to call another process. The Brand panel scrolls when the window is short, so the actions stay reachable. Long names do not shove Save off the bottom of a laptop screen. You scroll the panel. The canvas stays where it was. Welcome already knows what a project is. **Projects → recent** finds directories that contain a brand bank. **Edit brand…** opens the current project's brand editor. **+ Project** opens that editor from the welcome screen. The right tabs are the same kit once a document is open. You do not learn two banks. ## In the hand Open a document. Look at the right edge. Click **Inspect**. Select an object. The column shows that object. Change a value you would have changed in any properties panel. Ctrl+Z returns the object. That was a document edit. Click **Palettes**. Choose Personal or Project. If the project side is empty, you are about to make the job's colors, or you point the folder button at a directory that already has them. The palette editor that used to be tucked away is this tab. Add a color, click a swatch with Fill armed, and the selection takes it. Click **Brand**. If the document knows a project folder, the tiles for that bank are the ones you see. Filter to a logo. Drag it onto the artboard. Ctrl+Z removes the placement. The file in the bank is still in the bank. You placed a copy. ``` Inspect · Palettes · Brand ``` Switch to Motion, or to Design again. The tabs are still the tabs. Select a keyframed shape, click Inspect, and you are looking at the selected artwork. The timeline did not steal the column. On a short window, scroll the Brand tab until Add and the rest of the actions are in reach. You do not undock the panel to find them. When you are on the welcome screen and the job is the kit itself, **Edit brand…** or **+ Project** opens the brand editor directly. Then open a document from that project and the Brand tab is the same cupboard. ## The edge The column refuses to be three applications. Inspect, Palettes, and Brand are tabs in the sidebar of the one studio. A palette save refuses to pretend it is an artwork save. A placed logo refuses to move the original file in the bank. You placed a copy. Ctrl+Z undoes the copy on the canvas. The tabs refuse a login. Personal colors, project colors, and the brand folder open because they are on disk, not because a library account answered. Click Inspect for the selection, Palettes for the color, Brand for the file. The column is the whole trip. ## The thread Part 89 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-templates-offline-editable) · [Next](/blog/omadesign-0-5-8-project-library-files) --- # Project library files Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-project-library-files Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, palettes ## The habit Creative Cloud Libraries taught a bad address. The swatches live "in the cloud," the logo lives "in the library," and the document on disk is a guest. Move the job to another machine and you sign in, wait for the sync, and hope the library name matches the one from memory. Illustrator and Photoshop both route that habit through the same account. Affinity is closer to files, and people still email a pack of PNGs plus a screenshot of the colors because nobody agreed where the kit lives. The hand wants a folder. Copy the folder, you copied the job. The colors are a file you can open in a text editor when a hex is wrong. The font roles are a file that points at font files inside the project, not at a font menu on one laptop. The logos are files in a directory, nested if the brand has sections. Hidden files are fine. Designers can turn on hidden files. A dot in the name is a better lock than an account. You also want the document to find that folder without a prompt every time. Save the poster inside the client directory. Open it next month. The sidebar should already be looking at the client kit, not at your personal swatches and not at the previous client's bank. ## The constraint One `.oma` is the artwork. It should not have to embed every logo, every weight, and every palette in order for the next file in the same job to match. Embed the one you placed. Share the kit as siblings. That split keeps the document small and the kit reusable. It also means the kit must be discoverable from the document's path. A project setting buried in a preference file will not travel when you copy the folder to another machine. The discovery has to be "look at the directories I am inside." The nearest enclosing folder wins. You might keep `client/brand/` and also `client/posters/april/poster.oma`. If any of the kit files exist at `client/` and none exist closer, the poster uses `client/`. If a tighter folder also has a kit, the tighter one wins. That is how a subproject can diverge without a settings dialog. If nothing encloses the document, the kit starts beside the document. You are not sent hunting for a default library in a hidden application support folder you have never seen. Unsaved documents have no path yet. They cannot do the walk. **Choose a project** is the explicit answer. The folder button is the same idea later: switch libraries on purpose when the nearest folder is not the one you want today. The names start with a dot so a casual directory listing stays about the art. `.omacolors`, `.omatype`, `.omabrand/`. Copying by hand means showing hidden files. The manual says so because people will forget, and a kit that "did not copy" is usually a kit the file manager hid. ## What landed Three files, beside the work. **`.omacolors`** holds the palettes. It is readable JSON. One file can hold several named palettes, each with hex colors. An eight-digit hex keeps transparency. You can export and import this file from the Palettes tab. You can also read it. A handwritten file may be the full collection, a single palette object, or a bare array of hex strings. Older saves that used RGBA objects still load, and they become this portable shape when you save them again. **`.omatype`** names the font roles. Paths inside it are relative to `.omabrand/`. The font files live in `.omabrand/fonts/`. A role has a name you chose, Heading or Body or whatever the job calls it, and a pointer at a TTF or OTF in that fonts folder. No absolute path. Copy the project and the pointer still resolves. **`.omabrand/`** holds the assets and the font files. Logos, images, SVG, other `.oma` artwork, nested folders if you grouped them. An optional `.omabrand/brand.json` sets the bank's display name. Without that file, the project folder's name is the display name. Artwork stays inside `.omabrand/`. The name file does not store absolute paths. A saved document uses the nearest enclosing folder that contains any of these. One of them is enough to mark the folder as the project. If none exist on the walk up, the library starts beside the document. For a document that has never been saved, use **Choose a project** and pick the folder. The folder button switches libraries explicitly when you need a different kit than the one the path found. Welcome uses the same mark. **Projects → recent** finds directories containing `.omabrand` anywhere under your home directory. A project needs no account. Click the folder card and you are browsing that kit's world, subprojects first, then the documents. The Fieldwork example in the app is a portable reference of this shape: palettes, SVG marks, the hidden sidecars. Copy those beside a job, or point the sidebar at the example folder, and the tab shows a real kit. ## In the hand Save the document inside the client folder, or create the kit beside it. In a terminal, hidden names are visible. In a file manager, turn hidden files on before you trust a copy. ``` client/.omacolors client/.omatype client/.omabrand/ client/.omabrand/fonts/ client/.omabrand/brand.json client/poster.oma ``` Open `poster.oma`. The sidebar's project palettes and brand bank are `client/`, because that folder encloses the document and holds the kit. Make a subfolder `client/posters/` and move only the `.oma`. The nearest enclosing kit is still `client/`, as long as `posters/` does not contain its own `.omacolors`, `.omatype`, or `.omabrand/`. Put a kit inside `posters/` and the poster switches to that nearer folder. The walk is the setting. New unsaved tab: **Choose a project**, pick `client`. The tabs bind to that folder even though the artwork has no path yet. Save the artwork into `client` afterward so the next open finds the same place without being told. The folder button is the override. Point it at another directory when this session should edit a different kit. You are switching libraries explicitly. You are not changing the hex inside a file you did not mean to open. Copy the job to a laptop. Copy `.omacolors`, `.omatype`, and the whole `.omabrand/` directory, hidden files included. Open the `.oma` over there. The colors, the roles, and the assets resolve. No sign-in. ## The edge Discovery refuses a farther folder when a nearer one already has any of the three. The nearest enclosing kit wins. It also refuses to invent a kit from an unsaved document. No path, no walk. **Choose a project** is the step. The files refuse absolute paths. Font roles are relative to `.omabrand/`. The brand name file does not point at artwork somewhere else on the disk. Move the folder and the references still point inside it. Show hidden files, copy `.omacolors`, `.omatype`, and `.omabrand/` with the document, and the kit opens where the poster opens. ## The thread Part 90 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-three-right-tabs) · [Next](/blog/omadesign-0-5-8-personal-vs-project-palettes) --- # Personal vs project palettes Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-personal-vs-project-palettes Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, palettes ## The habit Swatches in Illustrator are a document thing until you save them as a library, and then they are an `.ase` file you forget the path to. Photoshop swatches stick to the app, and a job's brand colors stick to you only if you remembered to save the book. Affinity's palette menu has the same fork: a palette that follows you, and a palette that belongs to this document. People mix them up. They add the client's green to the personal book, then every unrelated poster offers that green. Or they add their own ink to the job, and the client's file grows a color the client never had. The hand wants the fork in the open, with two words. Personal. Project. And it wants the add gestures you already use. Take the color that is already current. Take fill and stroke from the selection, because you built the color on the object and now it should be a swatch. Type a hex, because the brand guide was an email. Include alpha in that hex when the chip is a varnish, a shadow, a 50% bar. Apply has a target. Fill or Stroke. Click the swatch. Right-click when you meant stroke and you do not want to flip the target first. That right-click is years of "I applied it to the wrong channel." ## The constraint Project colors have a file, `.omacolors`, in the project folder the document already resolved. Personal colors are the ones available across your work, the book that is yours when no client folder is in play. The tab has to show which one you are editing. A single list with no switch will write the client red into the wrong book the first time you are moving fast. The color model in the file is hex, so a typed value and a saved value are the same kind of thing. `#RRGGBB` is the opaque chip. `#RRGGBBAA` includes transparency. You should not have to open a separate opacity field to store a translucent swatch, and you should not lose that alpha when the palette saves. Older palettes that stored RGBA objects still load. Saving them writes the portable hex. **From selection** collects the fill and stroke colors on the selected artwork. You built the color on the object. The swatch is how you keep it. Filter has to cover both a palette name and a hex. Twenty palettes, one of them has `#173F35`, and you remember the hex better than the name. Or you remember "Ink" and not the hex. Both searches hit the same box. Applying a swatch is a document edit. It belongs on the artwork undo stack. Building the palette is a palette edit, saved by the palette Save button, not by Ctrl+S on the poster. The two books, personal and project, share the gestures so you do not learn two panels. They do not share the file. ## What landed Open **Palettes**. Choose **Personal** or **Project**. Personal is across your work. Project is the current project folder, the `.omacolors` beside that job. The switch is the whole distinction. Look at it before you add a chip. **+ Palette**. Type a name. Click **Rename**. The new palette exists in the book you had selected. Name it for the job or for the use. "Fieldwork" belongs in a project. "My ink" belongs in Personal. You can hold several named palettes in the collection. Filter the collection by palette name or by hex when the list is longer than the panel. Add color in three ways. **+ Current color** takes whatever color is current and adds it. **From selection** collects the fill and stroke colors off the selected artwork. Or type a hex and click **+**. Include alpha when you need it: ``` #RRGGBBAA ``` `#D97C5B80` is a real example of that form, a color plus transparency in one string. Opaque colors stay six digits. You do not need a second control. Choose **Fill** or **Stroke**, then click a swatch. The selected artwork takes that color on the channel you armed. Right-click a swatch to apply it as the stroke directly, so a Fill target does not catch a stroke click. Each swatch has a **···** menu: replace the swatch with the current color, copy its hex, or remove it. Copy hex when you need the value in a conversation. Remove when the chip was an experiment. Replace when you refined the current color and the swatch should catch up. The palette **Save** writes the book. It does not save the poster. Ctrl+S on the artwork does not save the palette. Do both when both changed. Project save updates `.omacolors` for that folder. Personal save updates the book you carry across work. Clicking a swatch applies that color to the artwork on the channel you armed. The palette itself changes only when you add, replace, or remove a swatch, and it reaches disk only when you press the palette Save. Ctrl+S on the poster does not write the book. The palette Save does not write the poster. ## In the hand Select the object whose color is almost right. Open **Palettes**. If this green is for every personal sketch, click **Personal**. If it is the client's, click **Project**. If Project has no folder yet, choose the project folder first. An unsaved document needs **Choose a project** before a project book has an address. ``` + Palette ``` Name it. Rename. Then **From selection** if the fill and stroke on the object are the chips you want stored. Or set up the color, hit **+ Current color**. Or paste a hex from the brand mail and hit **+**. Arm **Fill**. Click the swatch. The selection fills. Arm **Stroke**. Click another swatch. Or right-click that swatch and skip the arm. The stroke updates either way on a right-click. Filter by typing the palette name, or by typing `173F35`, until the list is the one chip you came for. Save the palette with its own Save button before you quit, or the quit prompt will ask. Save the artwork with Ctrl+S if the objects changed. They are two writes. The tab is one tab. The files are not one file. Switch to Personal, add a chip, switch back to Project. The project list does not show the personal chip. That is the fork working. A color crosses only when you add it to the book you are looking at. ## The edge The panel refuses to guess which book you meant. Personal and Project stay separate lists. A chip added on one side stays on that side. There is no silent copy into the other book. A swatch click refuses to save the palette for you, and a palette Save refuses to save the artwork. Fill and Stroke are the apply target. Right-click is the stroke when you did not change the target. Alpha lives in the hex. You do not get a swatch that drops `#RRGGBBAA` down to six digits and calls it the same color. Choose Project or Personal, add the hex, and click the swatch onto the channel you armed. ## The thread Part 91 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-project-library-files) · [Next](/blog/omadesign-0-5-8-palette-save-export) --- # Palette save export Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-palette-save-export Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, palettes ## The habit You save the poster. In Illustrator, that save may or may not include the swatches you added, and an `.ase` export is a different command you run when you remember. Photoshop's "save swatches" is a dialog aimed at a preset folder. People hit Ctrl+S, close the laptop, and the new brand red existed only in RAM. Affinity's palette save is easy to miss because the document save feels like it should have covered it. It feels that way in every app. It is wrong in every app that stores reusable color outside the document on purpose. The other habit is sharing. You want one palette in an email, or the whole book. You want to load someone else's book without deleting yours. Name collisions are normal. Two files both contain "Ink." A load that replaces yours is a bug with a progress bar. A load that keeps both, with a suffix on the newcomer, is a load you can trust late at night. Duplicate and remove are the housekeeping. Duplicate when you want a variant book. Remove when a palette was a dead end. Neither should require a file manager. ## The constraint Project palettes are `.omacolors`, a JSON file in the project folder. Personal palettes are the other book. Both are outside the `.oma`. So the artwork's Ctrl+S cannot be the palette's save. If it were, a save of the poster would rewrite the shared kit, including for other documents that use that folder, and a save of the kit would pretend the poster was saved. Two dirty flags. Two buttons. Quit can ask for both, in order. It cannot collapse them. Load has to merge. The collection you have open may be dirty or clean. Incoming palettes add. Existing colors stay. A conflicting name gets a numbered suffix. You can see both Inks. The import is not kept to disk until you Save. That second step is deliberate. A load is a preview you can still abandon if you do not save. Closing without saving the palette leaves the file on disk as it was. Export is the share. **Export selected palette…** writes one. **Export collection…** writes them all. The format is the same readable JSON you can diff. Version, a list of palettes, names, hex colors. A hand-edited file is allowed to be smaller: one `{ "name", "colors" }` object, or a bare array of hex strings. The loader accepts those. You are not forced through the panel to add a single chip from a script. Duplicate and remove operate on the collection in the panel. They are palette edits. They hit disk when you Save, like any other palette edit. Remove does not reach into artwork and strip the color off objects that already used it. The swatch is a value you applied. The object keeps the value. ## What landed The palette **Save** button keeps the collection. It is a different control from saving the artwork. Dirty palette, clean document: Save in the Palettes tab. Clean palette, dirty document: Ctrl+S. Both dirty: do both. Quitting with unsaved palettes asks **Save all**, **Discard**, or **Cancel** before the artwork prompt. The app waits for the library write. A failed save or a conflict keeps you in the app. You do not quit past a palette that did not land. **··· → Load palettes…** adds palettes from a file. Your current colors stay. Incoming names that collide receive numbered suffixes. "Ink" can become a suffixed Ink beside the Ink you already had. Nothing is overwritten because the strings matched. The merge sits in the panel. **Save** afterward if you want the import on disk. Skip Save and the file you loaded from is unchanged, and your previous saved collection is what you will see next time you reload from disk. **Export selected palette…** shares the one you are on. **Export collection…** shares the book. Both write JSON another Omadesign can load, and that a person can read. The shape of a full file: ```json { "version": 1, "palettes": [ { "name": "Fieldwork", "colors": ["#173F35", "#F5EBDC", "#D97C5B80"] }, { "name": "Ink", "colors": ["#202420", "#FFFFFF"] } ] } ``` `#D97C5B80` is the translucent chip. Six-digit colors are opaque. Send the file. The other machine uses Load palettes. Their existing book stays, suffixes appear on clashes, and they Save to keep the merge. Duplicate makes another palette in the collection. Remove deletes a palette from the collection. Both wait on Save to become the file. Export does not require you to overwrite the project `.omacolors`. You can export a copy under any name, anywhere, and leave the working file alone. Older RGBA palettes still open. The next Save writes them as hex. A one-palette handwritten file and a bare array open too. You can bootstrap a project book with a text editor, then Load or just place `.omacolors` in the folder and let the sidebar see it. ## In the hand Build the chips. Press the palette Save. The button is on the palette, not in the File menu next to the poster. Then save the poster if the poster changed. ``` Save ``` That write is `.omacolors` for a project book. Confirm in the folder, with hidden files visible. Open the JSON if you want to see the hex. Close it without a clever edit if you are mid-session and the panel is dirty. External edits have their own rules when the panel is unsaved. Save first if you want the disk to match the panel. To bring a book in: **··· → Load palettes…**, choose the JSON. Scroll the list. Your old palettes are there. New ones are there. Collisions wear suffixes. If the merge is what you wanted: ``` Save ``` If it was the wrong file, do not Save. Reload the saved colors when you want the panel to match disk again, or Discard when quit asks, depending on where you are. Until Save, the import is in the session. To send a book out: **··· → Export selected palette…** for one, **Export collection…** for all. Pick a path. You can put that file in another project folder and load it there. The destination's existing palettes remain, with suffixes on the names that clash. Duplicate a palette before a risky experiment. Remove a palette you will not use. Save. The artwork's colors do not disappear with the swatch. They were already applied as values. ## The edge Load refuses to replace your book. It merges, and conflicting names get numbered suffixes. The file on disk refuses to change until you press the palette Save. Export writes a copy. It is not a shortcut that overwrites `.omacolors` in place unless you aim it there on purpose. Palette Save refuses to save the `.oma`. Artwork save refuses to save the palette. Quit will ask for the palette first, and it will stay open if that write fails. Press the palette Save when the chips are the chips you want on disk. ## The thread Part 92 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-personal-vs-project-palettes) · [Next](/blog/omadesign-0-5-8-palette-conflict-handling) --- # Palette conflict handling Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-palette-conflict-handling Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, palettes ## The habit Two windows, one swatch file. You have done this with a CSS file, with an `.ase`, with a shared drive full of "Brand-Final-v7". Illustrator will reload a library or it will not, and you find out when the color you just added is gone. Photoshop's preset sync has eaten a book. A text editor and a design app open on the same JSON is the modern version: you fix a hex in the file because typing is faster, and the panel still shows the old hex, and then you hit Save in the panel and your fix is gone. Or the panel reloads on its own and the six new chips you had not saved vanish. The hand wants a dull rule. If I have not saved, my edits stay on screen. If the file changed under me, do not let Save pretend it can merge by clobber. Give me a way out that keeps my version, and a way out that takes the disk's version. Ask me again at quit. Do not order those questions after the artwork question, or I will answer the poster prompt and think I answered the palette. ## The constraint `.omacolors` is an ordinary file. Anything can write it. Another copy of the app, a sync tool, your editor, a copy you dropped on top. The sidebar refreshes libraries in the background so a change you made on disk shows up without a restart. That refresh is on a short timer, about three seconds. Fast enough that the panel is not a stale screenshot. Slow enough that it is not a busy loop on the file. A refresh that always reloads will destroy unsaved panel edits. A refresh that never reloads will hide a change your other hand just saved. The split is the dirty flag. Clean panel: take the file. Dirty panel: keep the edits, and block Save. Blocked Save is the important half. If Save stayed armed, the next click would write your stale memory over the newer file, or write a mix nobody asked for. Blocking is the refusal. The two exits are explicit. **Export a copy** writes your version somewhere else, so the newer file on disk can stay, and you still have your chips. **Reload saved colors** throws your panel edits away and loads the file. You choose. The app does not choose for you in the background. Quit has to join this. Unsaved palettes get **Save all**, **Discard**, or **Cancel**. The app waits for the library save to finish. If the save fails, or the file conflicts, you stay in the app. The artwork prompt comes after, only once the palette question is settled. Photo settings, when they are dirty, come before the palette question. The order is the constraint: camera sidecars, then palettes, then the `.oma`. None of those writes are allowed to be a side effect of a different write. ## What landed Libraries refresh in the background about every three seconds. A `.omacolors` that changes while your palette panel is clean shows up in the panel. You see the hex that is on disk. You did not press a reload button. The timer did the check. If the file changes while you have unsaved palette edits, those edits stay in the panel. The chips you added, the names you changed, the swatch you removed: still on screen. Save is blocked. You cannot push the panel over the file that moved. The block is the whole protection. There is no "save anyway" that silently wins. Two ways forward. Export a copy to keep your version. That uses the same export you already use to share a palette or a collection. Your chips land in a JSON you named. The file that changed underneath is left as the other writer left it. Or choose **Reload saved colors**. The panel drops your unsaved palette edits and loads the file on disk. You are looking at the newer book. Your unsaved chips are gone, because you asked to take the disk. Quit with a dirty palette and the prompt is **Save all**, **Discard**, or **Cancel**. Save all writes the libraries and waits. If that write hits a failure or a conflict, you remain in the app. The document stays open. You can export a copy, reload, or resolve the file outside and try again. Discard drops the unsaved palette edits. Cancel returns you to the chair with the edits intact. Artwork that is also unsaved gets its own prompt afterward. You answer the palette first. You do not lose the poster question. You also do not get to answer it while a palette write is still failing. Personal and project books both sit behind this. A refresh is a library refresh. Whichever file the panel is bound to, the rule is the same. Dirty stays dirty. Save blocks when the file moved. The three-second check does not care which book it was. The panel will not merge a disk change into your unsaved chips by matching names. Merge is what **Load palettes…** does, when you ask, with suffixes on collisions, and even that merge waits for Save before it is the file. A background conflict is not a load. It is a stop. ## In the hand Open **Palettes**. Add a chip. Do not press Save yet. In another window, change the `.omacolors` on disk. Wait a few seconds. ``` Save ``` The button does not take the write. Your new chip is still in the panel. The file on disk still has the other version. You have both, and neither has been destroyed. **··· → Export collection…** or export the selected palette. Put that JSON somewhere safe. That is your version, preserved. Then, if the disk version is the one you want to keep working from, choose **Reload saved colors**. The panel matches the file. Your unsaved chip is gone from the panel and alive in the export you just wrote. Load that export later if you still want it, and Save when the merge should stick. If your panel is the one that should win, do not reload. Move the other file aside, or finish the outside edit so you are no longer in conflict, then Save. If Save is still blocked, export remains the way your bytes survive. You are never required to reload in order to keep a copy. Quit while the palette is dirty and the conflict is unresolved. The prompt appears. ``` Save all · Discard · Cancel ``` Cancel. You are still in the document. The palette edits are still in the panel. The poster has not been asked to save yet. Resolve the palette, then quit again. The artwork prompt follows when the palette write is allowed to finish. Photo sidecars, if those are dirty too, were asked before this. Answer them in the order the app asks. Do not hunt the File menu to force a different order. ## The edge A background refresh refuses to clobber unsaved palette edits, and Save refuses to clobber a file that changed under those edits. The panel stays. The button blocks. You export a copy to keep your side, or you **Reload saved colors** to take the disk. Quit refuses to leave during a failed or conflicting library save. **Cancel** keeps you in the app. The artwork prompt waits its turn. Wait for the block, export the copy, and reload only when the file on disk is the book you mean to keep. ## The thread Part 93 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-palette-save-export) · [Next](/blog/omadesign-0-5-8-brand-bank-load) --- # Brand bank load Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-brand-bank-load Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, brand ## The habit The brand folder on a studio server is a dump. Logos at the top, an "old" directory nobody may touch, a "final" directory with three finals. Illustrator's Libraries panel wants you to upload the good ones into an account. Photoshop's is the same account. You drag a PNG in, and the original on disk is now a cousin of a cloud asset, and you are never sure which one the intern updated. Affinity's asset panel can point at folders, which is closer. People still copy the logo into the document by hand because the panel was pointed at last year's directory. The hand wants Load. Point at the project, or point at the `.omabrand` folder itself. See the tiles. For a new job, Create. Name the bank the way the client says the name, not the way the folder happened to be named on Monday. Save that name. Then add files by copying them in. The originals stay in the dump, the camera card, the downloads folder. The bank holds a copy. Nested folders come along, because "logos / marks / lockups" is already how the dump is organized and you should not have to flatten it to use it. Escape has to cancel a load you started by mistake. A late thumbnail must not land in the wrong document because you switched tabs while the folder was still being read. ## The constraint The bank is `.omabrand/` beside the work, plus optional `brand.json` for the display name. Loading has to accept either the project folder or the `.omabrand` folder itself. People will click the parent one week and the dot-folder the next. Both are the same bank. If `brand.json` is missing, the project folder's name is the name you see. You can replace that with a real brand name without renaming the directory, because directories are shared with exports, builds, and other tools that already use the path. Add copies. It does not move. A bank that moves the original out of Downloads will lose files for every other tool that had the old path. Duplicate filenames inside the bank get new names, so a second `logo.png` does not destroy the first. The original outside the bank keeps its name. The copy inside gets a name that can coexist. Nested folders are the categories. The filter later searches name and folder path, which is useless if add flattened everything into one directory. The copy preserves the nest. Discovery runs in the background. A fat bank of images cannot block the canvas while thumbnails decode. Switching documents cannot accept a finished load into the tab that happens to be focused when the worker returns. The load belongs to the document that asked. Escape cancels a load still in flight. You are on Linux, one binary, no uploader. The dialog is the native file dialog. The bytes stay on disk. An unsaved document has no folder yet. **Choose a project** before **Create bank**, or you have a name with nowhere to put `.omabrand/`. ## What landed Open **Brand → Load bank…**. Choose a project folder or the `.omabrand` folder inside it. The tiles are that bank. The display name comes from `.omabrand/brand.json` when that file exists: ```json { "version": 1, "name": "Fieldwork" } ``` Without the file, the project folder supplies the name. Load is how an existing kit becomes the sidebar. The nearest-folder walk may have already pointed you here. Load is the explicit version, for when you want this bank now. A new collection is **Choose a project**, then **Create bank**. The directory is created as `.omabrand/` in that project. Edit the brand name. Click **Save**. The name you typed is the name the bank shows. The folder on disk can stay the folder the rest of the job uses. Save writes that name. It does not scatter assets anywhere else. **··· → Add assets…** copies artwork into the bank. The originals stay in place. Pick files from the dump, the desktop, another project. The bank gets copies. Nested folders are supported. Bring a directory with structure and the structure arrives. A filename that would collide inside the bank is given a new name. The first logo remains. The second logo remains. You can see both tiles. While a load or a scan is running, Escape cancels it. You are not committed to a folder you mis-clicked. If you switch documents before a background read finishes, the arrival does not drop into the tab you switched to. The bank you were loading stays associated with the document that started the load. Thumbnails catch up in the background after the files are in. You can filter and place once a tile is there. External changes refresh on their own timer. **··· → Refresh now** is the immediate check when you just copied a file in with the file manager and you do not want to wait. The originals are not linked live. You copied bytes into `.omabrand/`. Edit the original in Downloads later and the bank does not follow, until you add again or replace the file inside the bank. That is what "originals stay" means. Two files. The dump is still the dump. The project is the project. ## In the hand Put the job folder where the poster will live. In the Brand tab: ``` Load bank… ``` If `.omabrand` already exists, choose the project or choose the dot-folder. The tiles appear as the scan finishes. If this is a new client: ``` Choose a project → Create bank ``` Edit the name. Press **Save**. Then: ``` ··· → Add assets… ``` Select the logos, the patterns, the reference images. Include a folder if the nest matters. The copies land inside `.omabrand/`. Go back to the original directory and confirm the files are still there. They are. In the bank, a second file with the same leaf name did not erase the first. It wears a distinct name. Press Escape if you opened the wrong directory and the load is still going. The cancel stops that load. Choose the right folder and load again. Open a second document tab while a heavy bank is still drawing thumbnails. The first document keeps the load it asked for. You do not find client A's logo sitting in client B's file because a worker finished late. Save the poster into the same project when you are ready. Next open, the nearest enclosing `.omabrand` resolves, and Load is unnecessary unless you want a different bank on purpose. ## The edge Add assets refuses to move or delete the originals. It copies. A name clash inside the bank refuses to overwrite the file already stored there. The new copy gets a new name. A load refuses to finish in the wrong tab. Escape cancels the one in flight. Create bank refuses to invent a project for an unsaved document. Choose the folder first, then create. Load bank…, or Create bank and Save the name, then Add assets… and leave the originals where they were. ## The thread Part 94 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-palette-conflict-handling) · [Next](/blog/omadesign-0-5-8-brand-asset-place) --- # Brand asset place Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-brand-asset-place Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, brand ## The habit You drag a logo off a library panel onto the artboard. Illustrator's Libraries panel does this, after the asset has finished syncing. Photoshop places, and you get a layer. Affinity's assets studio is the same drag, and a double-click drops the asset in the middle when you cannot be bothered to aim. The failure modes are familiar. The thumbnail is a blank box for a second and you drop the wrong tile. A file you updated in the folder five minutes ago still shows the old preview. The drop lands on an artboard you were not looking at because focus moved. Undo puts back one object, or it puts back fifty, and you are not sure which you will get. Filter is the other habit. A bank of two hundred marks is not a grid you scroll by memory. You type the name. You type a piece of the folder path, `lockups/horizontal`. You hit a type chip: images, SVG, the native documents. Then you drag the one tile that survived. The place has to be one undo. A logo that arrives as a group, a mask, and three paths is still one gesture. Ctrl+Z should remove the placement, not the top path. ## The constraint The bank is files in `.omabrand/`. Thumbnails are a view. Decoding every image in a large bank on the UI thread will freeze the poster you are trying to place onto. Thumbnails load in the background. The grid lays out the visible rows and asks for the previews you can see. A texture cache has a bound. The panel stays a panel. Files change outside the app. You export a new PNG into the bank from another tool. The tile should catch up without a restart. The refresh is about every three seconds, the same cadence as the palette library. **··· → Refresh now** exists when three seconds is too long because a client is watching. Refresh updates the tile. It does not place anything. Placement targets matter. A drag puts the copy at the drop point. You aimed. A double-click has no aim. It uses the selected artboard's center. With no artboard selected, it uses the document center. You always know where an unaimed place will land. It will not land at a leftover coordinate from the previous document. Photo is not an artboard tool in the same way. Double-click in Photo places the asset into Design. Drag placement is there when you are on artboards. You do not get a brand image pasted into the RAW viewer as if it were a develop layer. A native `.oma` asset has to place as editable artwork. Shapes, text, masks, motion tracks. Fresh object IDs, so the placed copy is not a second pointer at the same objects as the file in the bank. One undo removes that placement. Escape cancels a load that has not finished. Switching documents cannot deliver a late place into the wrong tab. ## What landed Filter the tiles by name, by folder path, or by file type. The type chips are **Image**, **SVG**, and **omadesign**. Image narrows to the raster files the bank accepts. SVG narrows to SVG. omadesign narrows to `.oma` artwork stored in the bank. A typed filter hits the name and the path, so a nested folder is part of how you search. `marks/leaf` finds the leaf inside marks. You do not scroll the whole cupboard. Thumbnails load in the background. Empty tiles fill in as the decode finishes. You can scroll. Visible rows are the ones that spend the work. Files added or changed outside the app refresh about every three seconds. **··· → Refresh now** checks immediately. The canvas does not lock while this happens. Drag a tile onto the canvas. The copy lands at the drop point. Double-click a tile and the copy lands at the center of the selected artboard. No artboard selected: document center. Both are placements of a copy. The file in `.omabrand/` stays. Edit the placed vectors and the bank file is unchanged. Edit the bank file and the thing you already placed does not live-update under you. You placed bytes. You did not subscribe. **Ctrl+Z** undoes the place. One step. A native `.oma` that brought a stack of shapes, text, masks, and motion tracks still leaves in that one step. The IDs inside the placement were fresh on the way in, so the undo is not confused with objects that were already in the document. In Photo, double-click an asset to place it in Design. You leave the develop view with a Design document that has the asset. Drag placement stays available when you are on artboards. You aim on a page. You do not aim on a photograph. Escape cancels a pending load. A double-click or a drag that is still resolving can be refused before it lands. Switch tabs while something is in flight and the finished read cannot attach to the document that did not request it. ## In the hand Open **Brand**. Load the bank if it is not already the one beside this document. Click **SVG**, or type the path fragment you remember. ``` lockup ``` The grid narrows. If the thumbnail is still arriving, wait for the background load, or hit **··· → Refresh now** after you drop a new file into `.omabrand/` from the file manager. Three seconds will also do it. Drag the tile. Let go on the artboard where the mark belongs. It is a copy, at that point. If you wanted the center and not a careful drop, double-click the tile. Selected artboard: center of that artboard. Nothing selected: center of the document. Wrong place: ``` Ctrl+Z ``` The placement is gone. The tile is still in the bank. Drag again. If what you placed was an `.oma`, select a shape inside it. You can edit the shape. The text is text. A mask that was in the asset is a mask. Motion tracks that were in the asset are tracks, on these new object IDs. Saving the poster writes the poster. It does not rewrite the asset in the bank. From Photo, double-click the tile when the logo needs to become a Design layer beside a picture you are grading. The place happens in Design. The RAW and its `.omaphoto` stay the photograph. Press Escape if a load is pending and you chose the wrong tile or the wrong bank. Then filter again and place the one you meant. ## The edge Place refuses to move the bank file. You get a copy at the drop point, or at the selected artboard's center, or at the document center when no artboard is selected. **Ctrl+Z** removes that copy in one step, including a native document that arrived as many objects. A late load refuses the wrong tab. Refresh refuses to place on its own. It only updates tiles. Photo refuses a drag onto the develop surface as the way in. Double-click there sends the asset to Design. Filter the tile, drag it, and press Ctrl+Z if the copy landed in the wrong place. ## The thread Part 95 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-brand-bank-load) · [Next](/blog/omadesign-0-5-8-brand-accepted-formats) --- # Brand accepted formats Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-brand-accepted-formats Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, brand ## The habit A brand pack arrives as a zip of everything the last studio exported. PNG for the web logo. JPEG for a photograph you were told not to recompress, and then they recompressed it. WebP because someone was modern. TIFF from print. A BMP from a tool that should not still be in the building. A GIF that is a single frame pretending to be simple. SVG for the mark that has to stay sharp. And, if you are lucky, the actual working file, which in this studio is an `.oma`. Illustrator will place most of the rasters and then argue with the SVG. Photoshop will open the rasters and rasterize the SVG if you force it. Affinity will take a wide set and still choke on an SVG that used a filter the importer never promised. The hand wants the bank to say yes to the ordinary files and to be plain about SVG. The subset you can import as vectors is the subset the bank will hold as vectors. A complex SVG does not get to look accepted and then place as a surprise. The other habit is duplication. A new client spinoff needs the same bank. You want a command that clones the whole cupboard to the other project folder. Name included. Nested folders included. Not a zip you rebuild by hand, and not a copy that flattens `logos/print` into a single directory of collisions. ## The constraint `.omabrand/` is a directory of files, not a package format. The files inside have to be files the placer already knows how to turn into artwork. The studio places PNG, JPEG, WebP, TIFF, BMP, and GIF as images. It imports SVG through the SVG path it already has. It opens `.oma` as native artwork, with shapes, text, masks, and motion, new IDs on place. Adding a format the canvas cannot place would mean tiles you can see and cannot use. The list is the list of things place already understands. SVG is the uneven one. The app has a supported import subset. Complex SVG features may not carry over. The tile and the place use that same importer. If the mark depends on a structure outside the subset, place the native `.oma` you built it in. The `.oma` is the editable file. The SVG is the interchange file, with that ceiling. **Save bank copy…** clones to another project folder: assets, nest, display name, typography kit, and font files. Those fonts live under `.omabrand/`. Palettes do not. They are `.omacolors` beside the bank. Export the palette collection on its own when the other project needs the swatches. The native dialog asks for the destination. The source bank stays. The destination is a new copy. ## What landed Banks accept **PNG, JPEG, WebP, TIFF, BMP, GIF, SVG, and `.oma`**. Add assets of those types and they become tiles. Filter chips line up with the split you actually browse: **Image**, **SVG**, and **omadesign**. The rasters, including GIF, sit under Image. SVG sits under SVG. `.oma` sits under omadesign. SVG uses the supported import subset. Simple vector marks come in as vectors. Complex features may not carry over. The tile's preview is that import, drawn in the background like any other thumbnail. Place the tile and you place that result, as a copy, at the drop point or at the artboard center on a double-click. If the SVG arrived thinner than the source file, the source file in the bank is still the original SVG bytes you added. The place is the subset. You can still open the SVG in another editor. The bank did not rewrite it into a private form. `.oma` assets place as editable artwork. Shapes stay shapes. Text stays text. Masks and motion tracks come with the copy. Object IDs are fresh. Ctrl+Z removes the placement in one step. The file in the bank remains the file in the bank. **··· → Save bank copy…** copies the whole bank to another project folder. The display name comes along. Nested folders come along. Assets come along. The typography kit and its fonts come along, because **Save bank copy…** copies the assets, the typography kit, and the fonts. The destination can load that bank and see the same nest, the same name, and the same roles. Font files sit in the copied `.omabrand/fonts/`. Share those fonts only under their license. The command copies the files. Palettes stay a separate export. **Export collection…** on the Palettes tab writes `.omacolors`. Put that file in the destination folder if the swatches should travel. Save bank copy will not invent it. A hand copy of a project is the same split: `.omabrand/` for the cupboard and the fonts, `.omacolors` for the colors, `.omatype` for the role names. The role file points at `fonts/…` inside the brand directory. Copy the brand directory without `.omatype` and the fonts may be present while the role names are not. Use **Save bank copy…** when you want the command that knows the kit. Copy by hand when you want all three, with hidden files visible. ## In the hand **··· → Add assets…** and select the pack. ``` .png .jpg .jpeg .webp .tif .tiff .bmp .gif .svg .oma ``` Watch the tiles. Rasters show as images. SVG shows as the subset the importer could read. An `.oma` shows as a document tile. Drag to place. Double-click to center on the selected artboard, or on the document if no artboard is selected. Ctrl+Z if the copy was a test. When a second project needs the cupboard: ``` ··· → Save bank copy… ``` Choose the other project folder. The dialog is the native one. When it finishes, that folder has its own `.omabrand/`, with the name, the nested folders, the assets, and the fonts. Open a document saved inside that folder, or Load bank and point at it. The tiles match. The roles match, once the typography side of the copied kit is what you load there. If the colors need to travel too, switch to **Palettes** and **Export collection…** into the destination as `.omacolors`. Load palettes there if the destination already had a book. Conflicting names get suffixes. Save the palette so the merge sticks. The bank copy did not do this step. You do it with your eyes open. Leave the source project alone. Save bank copy wrote a second cupboard. Add assets never moved the files you picked from the dump. Two copies is the point. One to work from, one that stayed. ## The edge The bank refuses formats outside PNG, JPEG, WebP, TIFF, BMP, GIF, SVG, and `.oma`. SVG refuses to promise more than the supported import subset. Complex features may not carry over, and the place uses that same subset. **Save bank copy…** refuses to flatten the nest and refuses to drop the name. It copies the bank, the typography kit, and the fonts to the other project. It does not take `.omacolors` with it. Export the palette collection yourself when the swatches should follow. Add the SVG you can already import, and use Save bank copy… when the other folder needs the whole cupboard. ## The thread Part 96 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-brand-asset-place) · [Next](/blog/omadesign-0-5-8-brand-typography-kit) --- # Brand typography kit Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-brand-typography-kit Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, typography ## The habit In Illustrator the Character panel is where the face gets chosen. You click the family menu. You pick a weight. If the menu is missing the face, you leave the document, install the font, and come back. Paragraph styles then remember the names the layout actually uses: Heading, Body, Caption. The style stores a family name. The font file lives in the operating system, or in a service that turns the face on when an account is signed in. Affinity runs the same job through its font manager. The document records the family. The bytes sit in a system folder, or in a library that is not the job folder. Hand the file to another machine and you find out at open whether that machine has the face. Photoshop does it with the type tool and a font menu that searches installed faces. A missing font throws a warning. The fix is still an install, or a substitution you accept and then regret when the line wraps differently. The hand already knows the sequence. Type tool. Select the line. Pick the family. Name the style so the next headline does not land in the body face by accident. The bytes of the font are somebody else's problem until the file moves. ## The constraint Omadesign is one binary on Linux. The artwork is one `.oma`. The brand sits in the project folder beside that file. `.omatype` holds the role names. `.omabrand/fonts/` holds the font files. A saved document uses the nearest enclosing folder that already contains `.omacolors`, `.omatype`, or `.omabrand/`. If none of those exists, the library starts beside the document. An unsaved document has no folder yet. Choose a project, or use the folder button to point the libraries at the folder you mean. There is no account step that activates a face because you signed in. There is no dialog in this kit that registers a font for every application on the machine. Linux fontconfig will serve a face you installed for the user. That is a different decision, and this kit does not require it. If the headline has to set on the next machine, the face has to be a file in the project. Undo stays one step. Applying a face to live text is an edit to the artwork, so it undoes. The kit's names and files are a library, the way palettes are a library. They keep their own save. Quitting must not pretend a half-named role was part of the artboard undo stack. Palette edits already work this way. Typography follows the same split so a font file and a headline are not one blob. ## What landed Open Brand, then Typography. Add fonts… copies TTF or OTF files into the project. The originals stay where you picked them. The project gets its own copy. Faces you add through the panel receive stable filenames. You do not hand-edit a path to keep the kit valid, and you do not rename the file on disk to match a family string you typed. Name the kit and click Save name. Select a font row. Give the role a name you will still understand in six months. Heading, Body, and Caption are the useful ones. Click Save role. Either save button keeps both pending edits. A kit name you have not saved, and a role name you have not saved, go together. One button does not drop the other draft on the floor. Filter the list by role, family, or filename once the kit is longer than the three faces you started with. Click Apply beside a role. Selected text takes that role. With nothing selected, the next text you create uses it. The Character panel's font picker also lists Project fonts, so the same face is there when you are already in the type controls and you do not want to walk back to the Brand tab. Applying a font to artwork supports Undo. Ctrl+Z returns the text to the face it had. The kit's names and the files on disk do not ride that undo. You save the kit when the names are right. You undo the art when the apply was the wrong line. The names live in `.omatype`, which is readable JSON: ```json { "version": 1, "name": "Fieldwork typography", "roles": [ { "name": "Heading", "font": "fonts/Display.ttf" }, { "name": "Body", "font": "fonts/Reading.otf" } ] } ``` Paths are relative to `.omabrand/`. The font files stay inside its `fonts/` folder. The name file needs no absolute path. Those example filenames are examples. Yours will differ, and the panel's stable names are the ones Add fonts… actually wrote. Share the files only under their license terms. The kit copies bytes you pointed at. It does not grant a license the foundry did not. Fonts are available inside Omadesign without installing them on the computer. Shaping uses the project copy. External font changes refresh in the background. Text that already has a face keeps that face. Click Apply again when you want the line to pick up the file that changed on disk. If the kit file changes while you are editing a name, the panel preserves your draft and reports the conflict. Typography, then the ··· menu, then Reload typography discards the draft and loads the saved kit. ## In the hand Open the `.oma`, or choose a project for a document that has never been saved. 1. Open the right sidebar. Click Brand. Click Typography. 2. Click Add fonts…. Choose the TTF or OTF files. The panel copies them into `.omabrand/fonts/`. 3. Type the kit name. Click Save name. 4. Select the display-face row. Type Heading. Click Save role. 5. Select the reading face. Type Body. Click Save role. 6. Press T. Draw a text box, or select a headline already on the artboard. 7. Click Apply on the Heading row. The selected line, or the next line you create, uses that face. Project fonts in the Character picker lists it. Ctrl+Z undoes the apply on the artwork. The role remains in the kit, because Save role already wrote it with the kit's own control. If someone drops another file into `.omabrand/fonts/` while the panel is open, the list catches up in the background. Lines you already set keep their face until you Apply again. If you were mid-name when that file changed, your draft stays put and the panel tells you the kit on disk moved. Reload typography is the control that throws your draft away and reads the saved kit. Use it when the disk copy is the one you want. A second project is a different folder. The folder button switches libraries. Add fonts… copies into the project you have selected, not into a global font chest shared by every `.oma` on the machine. Personal palette colors are the ones that follow you across work. Project fonts follow the folder. ## The edge Add fonts… does not install the face for the rest of the computer. Other applications keep the font menu they already had. Omadesign reads the copy under `.omabrand/fonts/` when it sets project text. A machine that opens the `.oma` without that folder will not invent the outlines from a family name stored in the document. The bytes have to be in the kit. The license is yours to respect. The panel will copy a file you can read. It will not decide that the foundry allows the copy. You share fonts only under their license terms. Open Brand, then Typography, and click Add fonts…. ## The thread Part 97 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-brand-accepted-formats) · [Next](/blog/omadesign-0-5-8-typography-load-save-copy) --- # Typography load save copy Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-typography-load-save-copy Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, typography ## The habit Once a studio has paragraph styles, the next job is moving them. In Illustrator you load styles from another document, or you paste a text frame and let the style come along with a name collision you sort out later. Delete a style and the text usually keeps its formatting. The style name goes. The glyphs stay. The font file, if it was a document font or a packaged font, is a separate question you answer in a package dialog. Affinity's font manager and style list split the same way. You can import a set. You can remove a style. You still have to know whether the face is installed on the machine or trapped in the file you imported from. Photoshop is thinner here. Character styles travel inside the PSD. The font installer is still the operating system. None of these dialogs are a folder you can copy with the job when the job is a directory on Linux. The hand wants three operations and no fourth mystery. Bring a kit in. Send a kit out. Remove a role you no longer want in the menu, without stripping the face off headlines that already use it. ## The constraint The typography kit is `.omatype` plus the files under `.omabrand/fonts/`. Paths inside `.omatype` are relative to `.omabrand/`. A kit file sitting alone, with the font folder somewhere else on disk, is a list of names pointing at nothing. Load has to copy the faces, and it has to know where the source brand folder is. The manual is plain about that: keep the source `.omabrand/` beside the `.omatype` you are loading. One project already has roles. Loading another kit must not wipe Heading because the incoming file also has a life. The operation is a merge. Save copy has to write both the kit and the fonts into the destination project, because a `.omatype` without its `fonts/` folder is a broken relative path. Pending name edits are not on disk yet. Save copy has to be told to save those names first, or it would write the previous kit and leave your new role behind. Remove is the sharp case. A role is a name in JSON. The font file is bytes that existing text may still shape with. Deleting the name and deleting the file in one gesture would change old artwork as a side effect of cleaning a menu. Undo on the artboard is one step, and it is the wrong tool for a library delete. The file stays. ## What landed On the Typography tab, the ··· menu has Load kit… and Save copy…. Load kit… merges another `.omatype` into the project you have open and copies that kit's font files. The source `.omabrand/` has to stay beside the source `.omatype`. The loader reads the relative paths from that pair. It copies the faces into the destination project's brand folder. Roles you already saved remain. The incoming kit adds its roles and its files. This is a merge, the same idea as loading palettes: existing colors stay, and the import adds what you asked for. Save the kit after you are happy with the merge. The kit has its own save, separate from Ctrl+S on the artwork. Save copy… writes the saved kit and its fonts into another project folder. Save pending name edits first. If Heading is still a draft in the panel, Save copy writes the kit that is already saved, not the word you have not committed. Click Save name or Save role before Save copy…. Either of those buttons keeps both pending name and role edits, so one save clears both drafts onto disk. Then Save copy… has something real to write. Removing a role keeps its font file for artwork that already uses it. The role leaves the list. The TTF or OTF remains in `.omabrand/fonts/`. Text that was set with that face still has the file it needs. You can Apply a different role later, on purpose, with Undo on the artwork. Cleanup of the menu is not a silent restyle. External font changes still refresh in the background. Existing text keeps its applied face until you click Apply again. If the kit on disk changes while you are editing a name, the panel preserves the draft and reports the conflict. Reload typography, on the same ··· menu, discards the draft and loads the saved kit. Use that when the file from disk is the one you meant, including after a load you want to abandon before you have saved. The destination of Save copy… is another project folder, the same kind of folder Brand already uses for a bank. The written result is a `.omatype` and a `fonts/` directory under that project's `.omabrand/`. Open the destination later, and Typography lists the roles. Project fonts in the Character panel lists the faces. Nothing was installed on the system on either machine. ## In the hand You have two job folders. This one has a kit. The other one needs it. 1. In the source project, open Brand, then Typography. 2. If the kit name or a role name is still unsaved, click Save name or Save role. Confirm the rows show the names you want copied. 3. Leave `.omatype` and `.omabrand/` next to each other in the source folder. Do not ship the JSON to a USB stick and leave `fonts/` at home. 4. On the destination project, open Typography. Open ···. Click Load kit…. Choose the source `.omatype`. 5. The panel merges the roles and copies the font files. Existing roles in the destination stay. Click the kit save once the merge looks right. 6. The other direction is ···, then Save copy…. Pick the other project folder. The saved kit and its fonts are written there. To drop a role you no longer want on the menu, remove the role. Then look in `.omabrand/fonts/`. The face is still there. Select a headline that used it. The glyphs are still set. Apply a different role only if you mean to change that line. Ctrl+Z undoes that apply. It does not resurrect a role you removed, and it does not delete the font file, because neither of those was an artwork edit. If Load kit… cannot see the faces, check the source pair. The `.omatype` paths look like `fonts/Display.ttf`, relative to `.omabrand/`. The brand folder has to be beside that file, with the fonts inside it. A renamed folder or a kit emailed without `.omabrand/` is a list of missing paths. Put the pair back together and load again. ## The edge Remove role keeps the font file. The command will not delete the TTF or OTF out from under artwork that already uses it. The menu gets shorter. The bytes stay in `.omabrand/fonts/` until you remove the file yourself, on purpose, knowing which lines still depend on it. Load kit… will not invent font bytes from a family name. If the source `.omabrand/` is not beside the `.omatype`, there is nothing honest to copy. Save copy… will not scoop up unsaved role names. Save the names, then copy the kit. Open Typography, open ···, and click Load kit… with the source `.omabrand/` still beside that `.omatype`. ## The thread Part 98 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-brand-typography-kit) · [Next](/blog/omadesign-0-5-8-project-fonts-travel) --- # Project fonts travel Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-project-fonts-travel Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, typography ## The habit Packaging is the job you do at the end, when the file has to leave your machine. Illustrator has Package. It gathers links and a Document fonts folder so the next person can open the `.ai` and still edit the type. PDF export embeds a subset so a press can print the line even when they do not have the face. SVG export asks a different question: keep the text as text, or convert it to curves so the logo looks right in a browser that has never heard of the family. Photoshop flattens or leaves the type layer live inside the PSD. The PSD still needs the font installed if you want to retype a word. Affinity packages fonts with the document when you export a package. In every one of those tools the scary moment is the same. You copied the artwork to a laptop, or to a server, or into a different folder for a delivery, and the headline came back as a missing-font box or a substituted grotesque. The hand wants two outcomes that do not fight. The working file stays editable after the folder moves. The file you hand to someone who will never open the working file still looks like the headline you set. ## The constraint Omadesign keeps the working file as one `.oma` and the faces as files in the project. `.omatype` names the roles. `.omabrand/fonts/` holds the TTF and OTF copies. Nothing in that design reaches out to a font service when the file opens. If you move the project, the move is a directory copy. `rsync`, a USB stick, a tarball. The app has to resolve the kit from that folder before it shapes text, or the first open on the new machine is a guess. Save As into another folder is the case people forget. The `.oma` arrives in the new directory. The font files are still in the old project's `.omabrand/fonts/`. A save that only writes JSON would strand the text. The save has to copy the faces the artwork actually uses into the destination folder's `.omabrand/fonts/`. Faces the kit lists and the artwork never used are a separate copy, through Save copy… on the Typography tab. This path is the faces on the page. SVG is the other audience. A browser, a slide deck, a printer RIP, another editor. They do not read `.omatype`. They do not open `.omabrand/`. If the SVG referenced a project font by name, the outline would depend on a file the recipient does not have. The export draws project-font text as vector outlines. The `.oma` stays the editable source. Two files, two jobs. Failed writes have to leave the existing artwork alone. A half-copied font folder must not be the moment the headline disappears. ## What landed Native `.oma` text stays editable after you move the project. That includes typing new characters. You are not looking at a baked preview that lets you nudge the box and then blocks the cursor. Load the project in the new place. The kit resolves before shaping. Press T, click in the line, and type. The face is the project face. Portable font identities are how a copied project finds the same face on the other machine. The role in `.omatype` points at a relative path under `.omabrand/`. The file in `fonts/` is the face. Background refresh can see new files appear in that folder. It does not rewrite font choices already applied to text. A file that shows up while you work is available. The line you set keeps the face it has until you click Apply. Saving artwork into another folder also copies the font faces that artwork uses into that folder's `.omabrand/fonts/`. Save As, native brand assets, and recovery snapshots carry the faces their text uses. The destination grows a brand folder if that is what the text needs. You do not run a second collect step after a successful save and hope you remembered every weight. SVG export draws project-font text as vector outlines so the appearance survives sharing. The curves are the glyphs. The recipient sees the headline. They do not get a live text run that depends on your project font. Open the `.oma` when the words have to change. The source still has editable text, and the project fonts are still files beside it. Failed writes preserve existing artwork. If the destination cannot take the font copy, the art you already had is not the thing that gets sacrificed to a partial save. You still have the source folder, the `.oma`, and the faces it was using. Recovery follows the same rule. A recovery snapshot that contains text also carries the faces that text uses. Reopening the recovered document is not a missing-font puzzle you solve by reinstalling the family on the system. The snapshot brought the files. ## In the hand Set a headline with a project role. Apply Heading, or pick the face under Project fonts. Type a few words so you know the line is live. Save with Ctrl+S. Copy the whole project directory to another place. The copy has to include the `.oma`, the `.omatype`, and `.omabrand/fonts/`. Hidden names matter. If the file manager hides dotfiles, the artboard will travel and the kit will stay behind. Show hidden files, then copy. Open the `.oma` from the new directory. Click in the headline. Type a character that was not in the line before. The new character uses the project face. That is the move working. Now try Save As, or save the artwork into a different folder that does not yet have this brand. After the save, look in the destination `.omabrand/fonts/`. The faces the text uses are there. Open that saved `.oma` on a machine that has never had the font installed system-wide. The line is still editable. Export SVG when the delivery is for a browser or for someone who will not open Omadesign. The project-font text in that SVG is outlines. Zoom in. The letterforms hold. Open the SVG in another tool and you will see paths, not a font menu entry for your kit. Keep the `.oma` as the file you edit. Change the word there. Export SVG again when the appearance has to go back out. If a write fails, go back to the folder you saved from. The artwork there is intact. Fix the destination, disk space, or permissions, and save again. Do not retype the headline to recover from a failed copy. ## The edge SVG export outlines project-font text. The SVG carries the appearance. It does not carry the live run, the role name, or the TTF. Recipients who need to edit the words need the `.oma` and the project fonts beside it. Recipients who need to see the words can take the SVG. Moving the `.oma` alone, without `.omabrand/fonts/`, is not a completed move. The document stays a document. The face is the file in the kit. Copy the project, then click in the line and type the next character. ## The thread Part 99 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-typography-load-save-copy) · [Next](/blog/omadesign-0-5-8-share-project-kit) --- # Share project kit Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-share-project-kit Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, project-kit ## The habit A brand kit in Illustrator or Photoshop usually means a library tied to an account. Swatches, character styles, logos, a CC Library you pull from a panel that is not the job folder. You share it by inviting an email. The other person signs in. The assets arrive if the service is up and the invite was the right one. Affinity can store assets in the application and package a document for handoff. The package is a special export. The everyday folder, the one you already keep in git or on a drive, is not the kit. The hand still wants the dumb version. Copy the folder. Send the folder. Open the folder on the other machine. Colors, type roles, logos, and the working files are all there. No account. No "library not available" because a hostname failed. The failure mode you actually hit is the file manager. It hides names that start with a dot, you copy what you can see, and the other machine gets the `.oma` with none of the brand. ## The constraint Project libraries live beside the work. `.omacolors` is the palettes. `.omatype` is the font roles. `.omabrand/` is the assets and the font files. A saved document uses the nearest enclosing folder that contains any of those. A project needs no account. Welcome will find directories containing `.omabrand` under your home directory and list them as projects. That discovery is local. Sign-in is a different door, for cloud, and cloud stays opt-in. The kit has to be copyable by any tool that can copy a directory, including ones that have never heard of Omadesign. Those three names begin with a dot. They are hidden on purpose so a folder of artwork does not look like a config dump, and so a casual listing shows the work. Hidden also means a default file manager copy will skip them. The kit is ordinary files with ordinary JSON inside, so a human can read the palette in a text editor when the panel is the wrong tool. No absolute paths in the name files. The artwork stays inside `.omabrand/`. Move the directory and the relative layout still resolves. Save bank copy… is the in-app copy for the bank. It copies the assets, the typography kit, and the fonts. The palette collection is a separate export, because `.omacolors` is its own file and palette saves are already separate from the document and from the bank. Two controls, because they are two files. Hand copy remains the way you take all of it in one gesture, once hidden files are visible. ## What landed Share a kit by copying `.omacolors`, `.omatype`, and the complete `.omabrand/` folder with the project. Enable hidden files in the file manager before you do it by hand. A copy that includes only the `.oma` files and the visible previews is the artwork without the brand. The other machine will open the documents. The palettes, the roles, and the bank will be missing until the dotfiles arrive. Save bank copy… copies the whole bank to another project folder: the name, the nested folders, the assets, the typography kit, and the fonts. Export the palette collection separately and put that `.omacolors` in the destination folder. Export selected palette… is the one-palette version. Export collection… is all of them. Load palettes… on the other side adds palettes from a file, keeps the colors already there, and gives conflicting names numbered suffixes. Save afterwards if you want the import kept. The Fieldwork example ships as a portable reference. It has curated palettes and original SVG marks, illustrations, and patterns. Copy its hidden sidecars into a project, or point the sidebar at the example folder. It is the sample you can open when you want to see a kit that is already a folder, not a diagram. `.omacolors` is readable JSON. One file can hold several named palettes: ```json { "version": 1, "palettes": [ { "name": "Fieldwork", "colors": ["#173F35", "#F5EBDC", "#D97C5B80"] }, { "name": "Ink", "colors": ["#202420", "#FFFFFF"] } ] } ``` A handwritten file may also be a single palette, `{ "name": "Ink", "colors": ["#202420", "#FFFFFF"] }`, or a bare array of hex strings. Older saved palettes that used RGBA objects still load. They become this portable form when you save them. `#RRGGBBAA` is how a swatch carries transparency. The eight-digit swatch in the Fieldwork example is that form. The optional `.omabrand/brand.json` sets the bank's display name: ```json { "version": 1, "name": "Fieldwork" } ``` Without that file, the project folder supplies the display name. Keep the artwork inside `.omabrand/`. The name file does not store absolute paths, so the bank still resolves after the directory moves. `.omatype` names the roles. Paths are relative to `.omabrand/`, and the font files stay in `fonts/`: ```json { "version": 1, "name": "Fieldwork typography", "roles": [ { "name": "Heading", "font": "fonts/Display.ttf" }, { "name": "Body", "font": "fonts/Reading.otf" } ] } ``` Fonts added in the panel get stable filenames. The JSON you share should match the files that are actually in `fonts/`. Share the fonts only under their license terms. The kit will not license them for you. SVG assets in the bank use the supported import subset. Complex SVG features may not carry over. That limit is on the asset, and it is the same limit the bank already has when you place a tile. A shared kit does not grow a second, looser SVG parser. ## In the hand Open the project folder in the file manager. Turn on hidden files. You should see `.omacolors`, `.omatype`, and `.omabrand/` next to the documents. Copy all three with the project. On the other machine, put them in one directory. Open Omadesign. The welcome screen's Projects list looks for `.omabrand` under your home directory. Click the folder card. Edit brand… opens that project's brand editor if you want to check the bank before you draw. Or do it from the panel, in two steps, because the bank copy and the palette file are separate. 1. Brand, then ···, then Save bank copy…. Choose the destination project folder. Assets, nested folders, the bank name, the typography kit, and the fonts go across. 2. Palettes, then ···, then Export collection…. Save the `.omacolors` into that same destination folder. 3. Open a document in the destination. The nearest enclosing kit is the one you just wrote. Project swatches and Project fonts should list what you exported. To learn the shape before you commit a client kit, point the sidebar at the Fieldwork example, or copy its hidden sidecars into a scratch project. Change a hex value in `.omacolors` in a text editor if you want to see that the file is really JSON. Come back to the panel. Libraries refresh in the background about every three seconds. If you had unsaved palette edits, those edits stay in the panel and Save is blocked until you export a copy of your version or choose Reload saved colors. ## The edge A hand copy that leaves hidden files behind leaves the kit behind. The `.oma` will open. The palettes, the roles, and the bank will not be in that folder. Show dotfiles, then copy `.omacolors`, `.omatype`, and the complete `.omabrand/`. Save bank copy… does not bring the palette collection along. Export `.omacolors` into the destination yourself. The bank and the colors are neighbors. They are not one file. Show hidden files, then copy `.omacolors`, `.omatype`, and `.omabrand/` with the project. ## The thread Part 100 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-project-fonts-travel) · [Next](/blog/omadesign-0-5-8-native-oma-format) --- # Native oma format Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-native-oma-format Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, oma ## The habit The file you trust in Illustrator is an `.ai`. In Photoshop it is a `.psd` or a `.psb`. In Affinity it is `.afdesign`, `.afphoto`, or `.afpub`. Each one is a package that application understands completely and every other application understands halfway. You save because the save is the document. You export because the export is what you hand to someone else. When a feature is new, an older build of the same app sometimes opens the file and quietly drops the thing it does not know. You also keep sidecars without meaning to. A thumbnail cache. A lock file. A recovered swap. The real document is the one the app will open tomorrow with the type still live, the layers still named, and the pixels still pixels. On a desk that moves files with `cp` and `git`, that document has to be one file you can name. ## The constraint Omadesign is one binary. The working document is one `.oma`. Undo is one step on the artwork, and the file has to round-trip that artwork: groups, vectors, text, pixels, masks, effects, pages, layout frames, comments, and the notes from an import. There is no service you have to reach before the file opens. Cloud metadata can live in the file. Cloud itself stays opt-in, and the document on disk is still the document. Rasters are packed as PNG inside the `.oma`. A pixel layer should survive a save without a sibling `.png` you have to remember. Motion lives in the same file as a clip, so a poster and the move you put on it do not become two formats you can lose separately. The structure around them is JSON. You can inspect it. Headless `--inspect` writes JSON for canvas size, pages, layer metadata, and import notes. The file is allowed to be readable. It is not a favor the app does when it feels like it. Older builds have to fail closed when the file uses a structure they would damage. A silent open that throws away frames, or throws away an object mask, is worse than a refusal. The version field is that refusal. ## What landed A `.oma` is JSON, plus rasters packed as PNG, plus a motion clip. That is the whole working document. Version 5 adds frames, auto-layout, constraints, and opt-in cloud metadata. Older apps cannot open version 5 files. This build reads formats 1 through 6. A 0.5.0 file already wrote version 5. The project wrapper stayed version 5 when later canvas features arrived. Documents saved with the gradient data from 0.5.4 need 0.5.4 or later. An older 0.5.3 build cannot read those gradients. The rest of the open path still accepts the older files this build claims, which is 1 through 6. Format 6 is the narrower bump, and the manual is the place it is spelled out. Documents that use object masks, or inside and outside strokes, save as `.oma` format 6. That bump exists so an older version cannot open the file and silently remove those features. Documents that do not use them keep format 5 compatibility. You do not get a version bump for sport. You get one when a quiet strip would destroy work. Save imported work as `.oma` when you want Omadesign's editable document and the conversion notes in one place. File → Open reads the foreign file into a tab. The source file stays where it was. Save, or Save As with Ctrl+Shift+S, writes the `.oma`. The notes come along. View → Document conversion notes shows them later, and they are still in the file after you quit. The PSD, the PDF, the `.xcf`, the Affinity file: those remain the originals. The `.oma` is the master you edit from here. What the native file can hold is the layer tree you were already editing. Groups, vectors, layout frames, comments, text, pixels, masks, effects, pages, and the conversion notes. Layout frames are the version 5 addition the short claim names: auto-layout, constraints, and the cloud link when you have opted in and saved after a push. A `.oma` is not a camera raw, and it is not a photo development. Photo settings live in `.omaphoto` beside the original. A Design document does not contain the raw sensor data or those development settings. Place in Design is an 8-bit pixel layer. The `.oma` stores that placed result, not the mosaic. Brand libraries are neighbors, not members of the `.oma`. `.omacolors`, `.omatype`, and `.omabrand/` sit beside the file. Saving artwork into another folder copies the project fonts that the text uses into that folder's `.omabrand/fonts/`. The document stays one file. The kit stays a folder you can share on purpose. Recovery snapshots follow the same split. They carry the faces their text uses so a recovered `.oma` can still shape the line. Headless conversion writes `.oma` as a destination like any other supported output. `omadesign --convert artwork.afphoto --output artwork.oma` uses the same reader the desktop uses. Same-file conversion is refused, so a convert does not overwrite the path you handed it as the source. ## In the hand Open a PSD, a PDF, or an SVG with Ctrl+O. It comes up in its own tab at the original dimensions, as unsaved work. Read the conversion notes if the status of the import matters. Press Ctrl+Shift+S. Name a `.oma`. The foreign file is still in its original folder, untouched. The new file is the one Omadesign will reopen with the layer tree and the notes. Reopen that `.oma` tomorrow. Ctrl+O, or click it in Your Work on the welcome screen. Welcome discovers `.oma` files under your home directory, skips hidden directories, Trash, and symlinks, and sorts by modification time. The document opens local. No account. If the piece uses frames, turn on Stack children for auto-layout, or pin a child with constraints, and save. That file is version 5 territory. An older app that only understands formats 1 through 4 will not open it. If the piece uses an object mask or an inside or outside stroke, the save is format 6. Keep this build, or any build that reads format 6, for that file. Handing it to a build that stops at format 5 is a refusal, and the refusal is the point. The mask stays in the file you can still open here. Ctrl+S writes the same `.oma` again. One file. The rasters stay packed. The motion clip stays in the document. Export is a separate command, Ctrl+E for the PNG path, and the other writers when you need a PDF, an SVG, a PSD, or an OpenRaster archive. The export is delivery. The `.oma` is the edit. ## The edge Older applications cannot open a version 5 `.oma`. Format 6 goes further for object masks and inside or outside strokes: the file will not pretend an older reader can keep those features by dropping them on the floor. This build reads 1 through 6. A build that does not know the version should stop. Saving the `.oma` does not rewrite the file you imported. The source stays the source. The notes stay in the `.oma` so the losses stay visible. Press Ctrl+Shift+S and write the `.oma` next to the original, not on top of it. ## The thread Part 101 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-share-project-kit) · [Next](/blog/omadesign-0-5-8-open-layered-foreign-docs) --- # Open layered foreign docs Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-open-layered-foreign-docs Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, import ## The habit You double-click a PSD and Photoshop opens it at the pixel size it was saved, with the layer names you wrote. You open an `.ai` and Illustrator builds the artboards from the file. You open an `.afdesign` and Affinity shows the spreads. GIMP opens `.xcf` the same way. The expectation is old and fair. Open means a document, at the size the file already has, in a window of its own. The original on disk stays the original until you Save or Save As on purpose. The other expectation is the one that hurts. Open in a different application often means a flat preview, a single page, or a dialog that wants to convert in place and write back. You wanted to look at the layers. You got a new file you did not name, or you got the same file with a different application’s fingerprints on it. ## The constraint Omadesign opens into its own layer tree. One binary, no helper app you have to keep running, except the optional Affinity converter you install yourself and Ghostscript if you are coming from old PostScript. The import has to land at the file’s own dimensions. A poster that is 3300 by 5100 should not arrive scaled to a default artboard because the new document dialog won. The source file stays put. Imported work opens unsaved, so the first Save asks for a `.oma`. That is the master from here. The PSD, the PDF, the `.xcf` remain where you opened them. Large imports run off the frame loop so the window does not freeze while channels decode. A format listed in Open is not a promise that every feature of that application survived. The notes carry the losses. The open itself is still one gesture: File → Open, or Ctrl+O. Affinity is behind an optional bridge because the converter is a separate, GPL-licensed tool. The Rust application stays MIT. Open can offer the Affinity suffixes. It cannot download the converter for you. GIMP text and effects come across as pixels, and there is no `.xcf` writer. You export OpenRaster or PSD when the way back is GIMP. That boundary belongs to the open, so you hear it on the day you use the command. ## What landed File → Open reads layered PSD and PSB, GIMP `.xcf`, every page of a PDF, PDF-compatible Illustrator, OpenRaster, SVG and SVGZ, and supported Affinity documents through the optional bridge. Each import opens in its own tab at the original dimensions. You get the pages, the artboards, or the canvas the file already had. You do not get a fit-to-screen rewrite of the coordinate system. Save uses `.oma` and preserves the source file. Ctrl+S on an unsaved import asks for a destination. Ctrl+Shift+S is Save As when you want a name. The foreign file is not the thing that gets overwritten. Conversion notes travel with the working document. View → Document conversion notes lists what was unsupported or converted. Save the `.oma` and the notes stay in it. What comes across depends on the type, and the open is still the same command. A PSD or PSB comes through the native layered reader. Groups, names, placement, visibility, opacity, blends, pixel masks, and supported Normal color overlays can survive. Text and smart objects arrive as the pixel layers Photoshop already saved. The type tool will not reconstruct a live text layer from those pixels. A PDF brings every page in as artboards, with paths, supported text, images, and optional-content layers. A PDF-compatible `.ai` is that PDF artwork. Private Illustrator data is not reconstructed. If the PDF-compatible pages are blank, the tab is blank, and the note says why. Older PostScript `.ai`, plus EPS and PS, go through Ghostscript to PDF and then through the PDF importer. Ghostscript has to be installed. The process uses a private temporary directory. A missing Ghostscript produces setup guidance. It does not invent a reader. OpenRaster opens as pixel layers and groups from `stack.xml`. SVG and SVGZ open with objects, names, transforms, text, images, and the supported masks. Scripts, animation, and foreign content in an SVG stay out. GIMP `.xcf` opens through a native reader. Pixel layers, groups, names, visibility, opacity, offsets, supported blends, and applied layer masks become native layers. Live text, layer effects, paths, and floating selections are not reconstructed. High bit depth becomes 8-bit RGBA. There is no `.xcf` writer. Export OpenRaster or PSD for the trip back, and keep the `.oma` as the master. Affinity `.afdesign`, `.afphoto`, `.afpub`, `.aftemplate`, and `.afpackage` need the optional converter from `setup-affinity-import.sh`. Vectors, text, pixels, groups, visibility, opacity, masks, and artboards can come across via SVG. Adjustments and history often do not. The file manager can offer Open With for native and Affinity documents without changing the default apps you already set. Camera RAW is a different open. Those files go to Photo. They are not a layered document tab. Mentioned here so you do not wait for a PSD-style layer stack from a NEF. ## In the hand Press Ctrl+O. Choose the PSD, the PDF, the `.ai`, the `.ora`, the `.svg`, the `.xcf`, or the Affinity file. The tab opens at that file’s size. Look at the layer list. Groups expand. Names are the names from the file when the reader had them. Hidden layers stay hidden. A multi-page PDF shows an artboard per page, not page one alone. Open View → Document conversion notes before you trust a text layer or an effect. If the note says the text came in as pixels, it came in as pixels. Edit the pixels, or retype with T on a new text object. Do not hunt for a hidden live-type switch. Press Ctrl+Shift+S. Write a `.oma` beside the source, with a different name. The source file’s timestamp stays. Reopen the `.oma` later. The notes are still there. The source is still the file you can hand back to Photoshop, Illustrator, GIMP, or Affinity. If an Affinity file does nothing useful, run `./scripts/setup-affinity-import.sh` once, then Open again. The script installs the pinned converter. Import does not download software on its own. If an old `.ai` or an EPS fails, install Ghostscript and Open again. Encrypted PDFs have to be saved again with the password protection removed. The importer will not ask you for a password and then write a decrypted twin over the original. Drop does the same open when you drop a layered document on the canvas or the welcome screen. Ctrl+O is the command when you want the file dialog and a specific path. ## The edge Open does not write back to the source. The PSD, PSB, PDF, `.ai`, `.xcf`, OpenRaster, SVG, and Affinity file stay as they were. The tab is unsaved work until you save a `.oma`. A format in the Open list is not full application compatibility. Notes record the gap. The source is what you keep when a note says something was lost. Press Ctrl+O, open the foreign file, then Ctrl+Shift+S to a new `.oma`. ## The thread Part 102 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-native-oma-format) · [Next](/blog/omadesign-0-5-8-place-command) --- # Place command Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-place-command Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, place ## The habit Place is the command you reach for when the document is already open. In Illustrator, File → Place drops a loaded cursor on the artboard. You click for the file’s own size, or you drag a box and the artwork scales into it. Photoshop’s Place Embedded does the same with a transform you commit. Affinity’s Place asks you to draw the box. The file you are placing might be a logo, a photo, or a whole other layout with groups and masks. You want those pieces to arrive together. You want one undo if the landing spot was wrong. The bad version of Place blocks the window while a large PSD decodes, then dumps a flat image where you clicked, and then makes you delete twelve layers by hand if you meant to cancel. Linked place, the kind that watches a network path and updates when the other file changes, is a different product. It needs a link manager and a story about missing files. This command is the landing, into the document you already have. ## The constraint The document you have open is the document that receives the art. Open is the other command. Open starts a tab at the file’s original dimensions and leaves you unsaved until you write a `.oma`. Place has to keep you in the current tab. The load has to happen off the frame loop. A 37 MB PSD should not freeze the canvas while you wait to click. Until you click, drag, or press Enter, the document should be unchanged. Esc has to mean the load was a preview you refused. What lands has to be the layer tree the reader actually built. Nested layers and masks travel together. A place that flattens a grouped logo into one bitmap, then offers you the groups on a second try, is two commands wearing one name. The drag scales that hierarchy into the rectangle you draw. One undo removes the placement. You should not peel child layers apart to get back to the artboard you had. The readers are the same readers Open uses. Place is a destination, not a second, weaker importer. Rasters in a `.oma` are PNG-packed. After you save, the placed pixels are in that file. There is no sidecar link you have to keep alive for the place to open tomorrow. Brand tiles have their own drag-from-the-panel path. File → Place is the path for a file you pick in the dialog. ## What landed File → Place… is Ctrl+Shift+P. The artwork loads in the background. Then you click or drag to place it. A click lands it. A drag draws the rectangle, and the imported hierarchy scales into that rectangle. Nested layers and masks travel together. Groups stay groups. A mask keeps its placement relative to the art that came with it. Pass-through and isolated group blending still mean what they mean on any other group once the layers are in the tree. Undo removes the placement in one step. Ctrl+Z is that step. The nested layers go with it. You are back to the document you had before the place. Redo is Ctrl+Shift+Z if the place was right and the undo was habit. Enter places at the center. That is the click you do not want to aim. The loaded artwork goes to the center of the document. Esc cancels. The load is discarded. No layer is left behind, because you never committed the place. The same files Open can read are the files Place can load. Layered PSD and PSB, PDF pages, PDF-compatible AI, OpenRaster, SVG and SVGZ, GIMP `.xcf`, images, and Affinity documents when the optional bridge is installed. A format the reader only partly understands still places what it understood, and the conversion notes describe the rest. Placing does not skip the notes. It also does not write back to the file you picked. The source stays the source. The current `.oma` is what gains layers, and only after you commit. Right-click the canvas for Place. The canvas menu and the File menu start the same load. Native file dialogs are the chooser. You are not dropped into a custom browser that rewrites the directory. Large files stay on the background path. You can still look at the artboard while the reader works. When the load is ready, the click and the drag are yours. Enter and Esc are the keyboard pair: commit at center, or refuse. Photo has a related landing, Place in Design, which develops a full-resolution pixel layer into a Design document. That path is the photo pipeline. File → Place is the document command, with Ctrl+Shift+P, for artwork you choose from disk. Use the one that matches the file in your hand. ## In the hand Open the poster. The tab is already the right size. You need a diagram that exists as its own SVG, or a layered PSD of a product shot. 1. Press Ctrl+Shift+P. Choose the file. The read runs in the background. The canvas stays up. 2. Move over the artboard. Drag the rectangle where the artwork should sit, and how big it should be. The hierarchy scales into that box. Masks and nested layers come with it. 3. Or press Enter if center is the right spot and you will move it after, with V. 4. Look at the layer list. The group, the names, and the masks are in this document. 5. Press Ctrl+Z if the box was wrong. The whole place leaves in one step. Press Ctrl+Shift+P and try the rectangle again. Esc, pressed after the load and before the click, cancels. Use it when you picked the wrong file. Then Ctrl+Shift+P and pick the right one. Nothing from the cancelled load is in the layer list. Save with Ctrl+S. The `.oma` now contains the placed tree. The file you placed from is unchanged on disk. Open View → Document conversion notes if the placed file was a PSD, a PDF, or an Affinity document and you need to know what became pixels. Alt-drag with the Move tool clones whatever is already selected. That is a duplicate inside the document. It is not Place. Place is how a file from outside becomes layers in one undoable step. ## The edge Esc cancels the place. The document does not keep a partial layer from a load you refused. Enter, a click, or a drag is the commit. Until one of those happens, the artboard is the artboard you had. Ctrl+Z removes the committed placement in one step, nested layers and masks included. You do not delete child objects one by one to undo a place. Press Ctrl+Shift+P, wait for the load, then drag the rectangle or press Enter. Esc if it is the wrong file. ## The thread Part 103 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-open-layered-foreign-docs) · [Next](/blog/omadesign-0-5-8-drop-to-open-or-place) --- # Drop to open or place Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-drop-to-open-or-place Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, place ## The habit The file manager is already open. You drag a PSD onto Photoshop and it opens. You drag a PNG onto an open Illustrator document and it places. You drag an `.ai` onto the start screen and you get a document, not a picture frame inside yesterday’s poster. The gesture is the same. The file type decides the verb. You learn the rule once, and then you stop answering a dialog that asks "open or place?" every time. Clipboard is the twin habit. Ctrl+C, Ctrl+X, Ctrl+V. Inside one application, a paste of objects should land where those objects were, including on another artboard, so a logo you copied from the master still sits on the grid. A paste of a style should change fills and strokes, not move the path. Illustrator’s eyedropper and Affinity’s paste-style do that job. You want a status line that says the copy happened, because a silent clipboard is how you paste last week’s selection and wreck the frame. ## The constraint Omadesign has two legal landings for a file, and they already have commands. Open starts a tab at the file’s original dimensions and leaves the source untouched. Place loads artwork into the document you have open, and one undo removes it. A drop cannot invent a third mode. It has to pick open or place from the kind of file, on two surfaces: the canvas, and the welcome screen. Welcome is a local file browser. Dropping work there has to do the same thing a drop on the canvas does, or the welcome screen is a picture of the app that does not accept work. `.oma` is the native document. Dropping one opens it. It does not place a document inside a document as a mystery embed. Ordinary images are pixels. They place. Layered documents are documents. They open. Lottie is its own import. The Motion side of the app already has File → Lottie. A drop should import, not pretend a Lottie JSON is a still PNG. Copy and paste have to be visible. The status bar is the confirmation. Objects copied inside Omadesign paste at their original positions, including onto another artboard. The status bar says so. External clipboard data is a different paste, Ctrl+V as well: screenshots, browser images, copied image files, plain text, SVG source or SVG files. That content lands in the center of the visible canvas, because it has no original position in this document. The status bar is how you tell the two pastes apart without guessing. Style is not geometry. Copy style and paste style need their own chords so Ctrl+C keeps meaning "the objects." ## What landed Drop a layered document on the canvas or the welcome screen and it opens. PSD, PSB, PDF, PDF-compatible AI, OpenRaster, SVG, GIMP `.xcf`, and a supported Affinity file take the open path: own tab, original dimensions, source preserved, save as `.oma` when you want the master. Ordinary images place. PNG, JPEG, and the other single images follow the place rules. You still get one undo for that placement, Ctrl+Z, once it has landed. A `.oma` opens. Dropping your own document does not nest it as a placed group inside the current tab. You get the document. Lottie imports. The drop does not treat a Lottie file as a flat image and it does not pretend it is a PSD. It imports. File → Lottie remains the menu path when you would rather pick the file than drag it. The status bar confirms copy, cut, and paste. Ctrl+C copies. Ctrl+X cuts. Ctrl+V pastes. Objects that were copied in Omadesign paste at their original positions. Paste onto another artboard and they keep those positions. The status bar tells you that happened. You are not left to measure the gap with the rulers. Copy style is Ctrl+Alt+C. Paste style is Ctrl+Alt+V. Those chords are style only. Ctrl+C and Ctrl+V remain the objects. In Photo, Ctrl+Shift+C and Ctrl+Shift+V are copy adjustments and paste adjustments, which is a different pair. On the canvas, Alt in the chord is the style. Shift in the chord, in Photo, is the development. Do not swap them. External paste uses Ctrl+V as well. A screenshot or a browser image becomes a pixel layer. Plain text becomes an editable text layer. SVG source or an SVG file becomes vector artwork. All of that appears at the center of the visible canvas, pan and zoom included, so "center" means the center of what you are looking at. Command+V works through Omarchy’s universal paste binding. Shift+Insert from the Alt+V clipboard-history picker uses the same paste path. If you are editing text, paste inserts into that text. It does not spawn a new layer in the middle of a word. Alt-drag clones a selection you already have. The status bar and the clipboard are for the things that left the document. Alt-drag never left. ## In the hand Open the file manager beside the window. Drag a PSD onto the canvas. A new tab opens at that PSD’s size. The previous document is still a tab. The PSD on disk is unchanged. Drag the same kind of file onto the welcome screen and it opens the same way. Drag a PNG onto the open poster. It places. Move it with V. Ctrl+Z removes the placement in one step if the drop was an accident. Drag a `.oma` onto the welcome screen. It opens. Your Work would have opened it on click. The drop is the same result. Drag a Lottie file. It imports. Play from the Motion side with Space once the clip is in the document and you have something to run. Select a rectangle with a fill you want to reuse. Press Ctrl+Alt+C. Select another shape. Press Ctrl+Alt+V. The fill and the rest of the style land on the second shape. The path stays where it was. Watch the status bar. Then select both shapes, Ctrl+C, switch artboards, Ctrl+V. They paste at their original positions. The status bar says so. Ctrl+Z undoes that paste in one step. Copy a screenshot. Ctrl+V on the canvas. A pixel layer appears at the center of the view. Copy a sentence from the browser. Ctrl+V. An editable text layer appears at that same center. Click into an existing text object first if you meant to insert the words there. The paste goes into the text. ## The edge The drop does not ask you to pick open or place. A layered document opens. An ordinary image places. A `.oma` opens. Lottie imports. Drag a PSD onto a poster when you wanted it inside the poster, and you will get a new tab. That is the rule. Use Ctrl+Shift+P, File → Place…, when the layered file belongs inside the document you already have. Copy style will not move geometry. Ctrl+Alt+V paints the style onto the selection you already have. The objects stay on Ctrl+C and Ctrl+V. Drop the layered file to open it. Drop the PNG to place it. Press Ctrl+Alt+C, then Ctrl+Alt+V, when the only thing you meant to carry was the style. ## The thread Part 104 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-place-command) · [Next](/blog/omadesign-0-5-8-conversion-notes) --- # Conversion notes Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-conversion-notes Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, import ## The habit Every serious import has a confession screen, and most of them are bad at it. Photoshop’s compatibility dialog, Illustrator’s missing-font and unknown-effect warnings, Affinity’s "this feature isn’t supported" line. You click through because the artboard looks close enough. A week later the shadow is gone, the smart object is a bitmap, and nobody can say whether the loss happened at open or during an edit. The honest tools write the confession down. The dishonest ones flatten and smile. You already know which features travel badly. Live type in a PSD. A smart object that is really another PSD. A layer style with bevel and satin. Illustrator’s private data that never made it into the PDF-compatible part of the `.ai`. Affinity adjustments, live filters, and history. You want those named, next to the document, still there after you save. ## The constraint The working file is one `.oma`. A warning that exists only in a modal, or only in a log you will not reopen, dies at the end of the session. The note has to live in the document. Save the `.oma` and the note saves. Open it on another machine and the note is still the account of what the reader did. The reader will not invent a feature it does not have. Rebuilding Photoshop’s live text engine, Illustrator’s appearance stack, or Affinity’s history inside this binary is a different application. The constraint is one studio with one layer tree. Where a feature can become something native and editable, it should. A Normal color overlay that can become a native color effect should become one. Where it cannot, the pixels that were already saved can come across, and the note has to say the live object did not. Silent substitution, a gradient replaced by one flat color, a blend mode flipped to Normal with no sentence anywhere, is the failure this list exists to stop. Headless conversion prints the same notes to stderr. The desktop and the command line share readers. A note you can see in the terminal is the same note View will show after you open the `.oma`. ## What landed View → Document conversion notes lists unsupported or converted features. The list is per document. Notes stay in the `.oma`. They are not a session toast. Save, quit, reopen. The notes are still in the file. Keep the source file when a note reports a loss. The `.oma` is the editable Omadesign document plus the confession. The PSD, the `.ai`, the Affinity file, the `.xcf` are still the originals, because Open does not rewrite them. A format that appears in File → Open is not a promise of complete compatibility with that application. The notes are how you see the gap for this file, not as a generic manual chapter you might remember to read. Affinity native features are not universally supported. The optional bridge can bring vectors, text, pixels, groups, visibility, opacity, masks, and artboards across via SVG. Most adjustments, live effects, publishing structures, custom profiles, and edit history are not retained. Multi-spread documents can fail. The note is the list of what this file lost, which is more useful than the general sentence, and the general sentence is why you look. Photoshop live text, smart objects, and effects are not universally supported. Text and smart objects import as the pixel layers already saved in the PSD or PSB. A supported Normal color overlay can become an editable native color effect. Other adjustments, fills, and effects are not recreated. Their notes explain the limit. Feathering on a mask is not supported. Clipping is baked into an editable pixel mask, so later edits to the base do not keep flowing through a live clip. The note stops you from expecting that live relationship. Illustrator private editing data is not reconstructed. `.ai` support means the PDF-compatible artwork, or older PostScript artwork converted through Ghostscript. Live effects, symbols, and appearance stacks that exist only in the private data do not come back. A real Illustrator file with blank PDF-compatible pages stays blank here, and an independent PDF renderer showed those pages blank too. The note tells you the artwork was never in the PDF part. Export from Illustrator to a PDF or SVG that actually contains the art, then open that. Other readers write notes for their own reductions. High bit depth becomes 8-bit RGBA and says so. Unmapped blend modes display as Normal and the note exists so you do not think the blend survived. PDF export fallbacks write a note for every reduction in editability. Effects are not silently dropped or replaced with a single gradient color. GIMP live text and layer effects import as pixels. OpenRaster vectors and text become pixels per layer. Each of those is a note you can open from the same menu. `--inspect` includes the import notes in its JSON. `--convert` prints notes to stderr and writes a separate destination. The note is part of the result, in the file and on the stream. ## In the hand Press Ctrl+O and open the foreign file. Before you edit, open View → Document conversion notes. Read the lines. A line that says text arrived as pixels means the type tool will not find a caret in that layer. Retype with T if the words have to change, on a new text object, in a project font if you have a kit. A line that says an effect was not recreated means the look you see is whatever pixels or approximation the reader kept. Do not stack a second effect on the assumption that the first one is still live. Press Ctrl+S or Ctrl+Shift+S and write a `.oma`. Quit. Open the `.oma` again. View → Document conversion notes shows the same list. The notes survived because they are in the file. If you are converting a folder from a script, run `omadesign --convert artwork.psd --output artwork.oma` and read stderr. Then open the `.oma` and use the View menu. The two reports are the same conversion. Keep `artwork.psd` when stderr is not empty. When you export later, fallbacks write notes too. A PDF that had to rasterize a group will say so. The `.oma` still has the live objects. The PDF is the delivery. Check the notes before you send the delivery to a press and call it the master. ## The edge The notes do not repair the missing feature. They record it. Affinity history, Photoshop live type and smart objects, and private Illustrator data stay unrebuilt. The list will not quietly turn a pixel layer back into a text object because you opened the menu. The notes also do not vanish when you save. They stay in the `.oma`. If a note reports a loss, keep the source file. The `.oma` is honest about what it holds. It is not a full copy of the other application’s private model. Open View → Document conversion notes before you trust the import. ## The thread Part 105 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-drop-to-open-or-place) · [Next](/blog/omadesign-0-5-8-export-matrix) --- # Export matrix Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-export-matrix Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, export ## The habit Export in Illustrator is a long menu and a longer PDF preset. You pick SVG for the web, PDF/X for the press, and you still save the `.ai` because the export is not the file you will edit tomorrow. Photoshop’s Export As and Save a Copy split the same way: PNG and JPEG for delivery, PSD for the layers. Affinity exports PDF, SVG, PSD, and its own package. The habit is a matrix. You already know which box you need. You also know the lie a matrix tells when it offers a native format it cannot write faithfully. A "save as AI" that is really a PDF with an `.ai` extension has burned people. A "save as AFDESIGN" from a tool that does not know Affinity’s current structure will burn them again. You want the writers that exist, named, with the scale factors for PNG, and a straight sentence about the formats this app will not emit. ## The constraint The master is the `.oma`. Export is a second file, produced by a writer compiled into the same binary. There is no Creative Cloud export service and no Affinity runtime hiding behind the menu. If a writer is not in the binary, the format is not in the menu. PSD, PDF, and OpenRaster are in the binary. Illustrator’s private `.ai` structure is not. Affinity’s `.af`, `.afdesign`, `.afphoto`, and `.afpub` structures are not. Interchange with those applications is PDF, SVG, or PSD. They can open those. They can also open the files they themselves wrote, which you still have, because import never overwrote them. A layer the destination cannot represent has to become something visible. The rule here is a pixel layer, with a conversion note. The note is the same idea as import notes. Effects are not silently dropped. A group that had to rasterize becomes one named layer in the PDF, and the `.oma` still has the objects inside the group. You edit the `.oma`. You re-export when the delivery has to change. Ctrl+E is the export chord. In the shortcut table it is Export. In Photo, that chord exports the developed image as PNG, because Photo’s job is the developed picture. In the other studios the chord takes the PNG path. The rest of the matrix is there for the deliveries PNG is not. Headless `--convert` writes `.oma`, `.svg`, `.png`, `.jpg` or `.jpeg`, `.psd`, `.psb`, `.pdf`, and `.ora`. Animated SVG and Lottie JSON are export writers beside that list. Layout can also export the selected frame as PNG, SVG, or HTML. HTML is a snapshot of the frame, not a website. It sits next to the matrix. It is not a secret eighth print format. ## What landed The export set is PNG at 1×, 2×, and 3×, JPEG, SVG, animated SVG, Lottie JSON, layered PSD and PSB, PDF, and OpenRaster. PNG is the raster delivery, including the scaled variants when a screen needs 2× or 3×. JPEG is the other flat raster. SVG is the vector delivery. Project-font text in an SVG is drawn as outlines so the appearance survives a machine that does not have the kit. The `.oma` keeps the editable text. Animated SVG and Lottie JSON are the motion deliveries. File → Lottie is how motion comes back in. The JSON is how a Lottie player takes it out. Layered PSD and PSB export is RGB, 8-bit, with separate layers. Vectors, native text, transformations, and effects the PSD writer cannot keep as live objects are rendered into their individual pixel layers. A supported color overlay can stay editable. Opaque canvas paper exports as a background layer. You can open that PSD in Photoshop and see layers. You edit the master in the `.oma` if the type has to change as type. PDF export writes pages, paths, raster images with alpha, opacity and blending, and optional-content layer metadata: names, order, visibility, and locks. Text becomes vector outlines. Gradients, masks, effects, and complex compositing render at document resolution into the affected layer or group. Unaffected vector layers stay editable paths. A rendered group becomes one named PDF layer. Hidden layers keep their visibility state. Backdrop-dependent pass-through can force a whole page to render. Every fallback writes a note. Fallback images are bounded: 64 megapixels each, 512 MiB total pixel budget. OpenRaster export follows the public 0.0.6 layout. Layers, `stack.xml`, a merged preview, and a thumbnail no larger than 256×256. Vector artwork renders per layer. A masked or effected group becomes one pixel layer. That archive is a way back to GIMP and to anything else that reads ORA. There is no `.xcf` writer. Layers a destination cannot carry may become individual pixel layers. The conversion notes describe those changes. Read them before you call the export the master. There is no native writer for `.af`, `.afdesign`, `.afphoto`, `.afpub`, or `.ai`. Hand Affinity a PDF, an SVG, or a PSD. Hand Illustrator a PDF or an SVG. Keep the `.oma`. The optional Affinity bridge is an importer. It does not grow an exporter because you installed it. Layout’s frame export is File → Export frame PNG / SVG / HTML for the selected frame. Use it when the delivery is one screen, not the whole document matrix. ## In the hand Finish the edit in the `.oma`. Press Ctrl+S. For a bitmap at the document’s pixel size, press Ctrl+E. That is the PNG export. In Photo, Ctrl+E writes the developed PNG from the photo pipeline, full resolution, with crop and rotation. For a screen that needs a double-resolution asset, use the 2× PNG export. 3× is there for the same reason. JPEG when the channel has to be small and the image has no transparency to keep. For a logo on the web, export SVG. Open the SVG and confirm project-font words are paths. Edit the words in the `.oma`, then export again. For a press or for Illustrator, export PDF. Open View → Document conversion notes and read any fallback. The PDF page you rasterized on purpose is still a page. The live vectors that could stay vectors stayed. Send the PDF. Keep the `.oma`. For Photoshop, export PSD or PSB. You get RGB, 8-bit, layered. Text will be pixels in that file. The note says so. For GIMP, export OpenRaster or PSD. GIMP can open both. It will not receive a `.xcf` from this app. For motion, export Lottie JSON or animated SVG, matching the player you are handing it to. Same-file headless conversion is refused. `omadesign --convert poster.oma --output poster.pdf` writes a new path. Do not point `--output` at the `.oma` you still need. ## The edge There is no native `.af*` writer and no native `.ai` writer. The matrix will not emit an Affinity document or an Illustrator document and call it interchangeable. PDF, SVG, and PSD are the interchange. The `.oma` is the file you reopen tomorrow. A layer the export format cannot keep becomes pixels, with a note. The writer will not drop the effect and leave you a clean file that lies about it. Press Ctrl+E for the PNG. Export PDF, SVG, or PSD when that is the file the other application can actually open. ## The thread Part 106 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-conversion-notes) · [Next](/blog/omadesign-0-5-8-psd-psb-bridge) --- # PSD PSB bridge Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-psd-psb-bridge Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, psd ## The habit Photoshop’s file is the layer stack. You name the layers. You group them. You hide the ones that are options. You set opacity and a blend mode. You paint a pixel mask. You set type, and you nest a smart object when the logo is really another file. Save a Copy or Export As when you need a PNG. The PSD is what you reopen. When another application opens that PSD, you have learned to look for three failures. The text becomes a picture. The smart object becomes a picture. The layer style either vanishes or gets baked. A good bridge tells you which of those happened and still gives you the groups, the names, and the masks. A bad bridge hands you a flat composite and calls it a layered open. PSB is the same file past the old size limit. Big files, same expectations. ## The constraint The reader and the writer live in the one Omadesign binary. There is no Photoshop install on the Linux machine, and the bridge does not shell out to one. Channels have to decode locally: raw, RLE, ZIP, and ZIP with prediction. Grayscale, RGB, and CMYK documents are in scope. Source channels may be 8, 16, or 32 bit. The document you edit here is 8-bit RGBA, so higher depth is reduced and a note says so. CMYK conversion is approximate. Embedded ICC profiles are not applied. That is a visible limit, not a quiet color shift you are supposed to trust for a press proof. The `.oma` is the master after you save. Export back to PSD has to be layered RGB, 8-bit, because that is the writer. It will not emit 16-bit PSD, and it will not rebuild live type on the way out. Text you set in Omadesign renders into its pixel layer in the exported PSD. The `.oma` still has the live text. One undo in the studio is not a Photoshop history state. History does not travel. Bounds exist so a hostile or enormous file cannot take the process down with it. 64 megapixels. 512 MiB decoded working budget. 8,192 layers. 64 group levels. A real 37 MB PSD was part of the check. So were independent Photoshop samples. The bridge is for production files inside those bounds, not for an unbounded promise. ## What landed File → Open on a `.psd` or `.psb` uses the native layered reader. You get groups, Unicode names, placement including negative offsets, visibility, opacity, and the supported blend modes. Hidden layers stay hidden. Pixel masks keep their placement, their default fill, and their density. Feathering is not supported. Clipping relationships are baked into editable pixel masks. After the import, editing the base layer does not automatically push through the clip the way a live clipping mask does in Photoshop. The mask is real. The live link is not. A supported Normal color overlay becomes an editable native color effect. Other adjustments, fills, and effects are not recreated. The conversion notes explain that limit for the file you opened. Vector masks use a cached pixel mask when one is stored. If the PSD only had the vector mask and no cached pixels, you get what the reader can honestly keep, plus a note. Text and smart objects import as their saved layer pixels. Photoshop stores a rendered raster for those layers alongside the live data. The bridge keeps that raster as its own layer. It does not rebuild the type object, the font, the paragraph, or the embedded document inside the smart object. You can move the layer, mask it further, and paint. You cannot click it with T and edit the original string. Retype on a new text object if the words have to change. Project fonts apply to that new text. The imported pixels stay the picture of the old words. Export writes RGB 8-bit PSD or PSB. Separate layers, groups, pixel masks, offsets, and supported color overlays. Vectors, native text, transformations, and other effects render into their individual pixel layers. Opaque canvas paper exports as a background layer. Independent checks with `psd-tools` confirmed the supported color overlay stayed editable after export, the original layer pixels were unchanged, and the saved composite matched pixel for pixel. Nine focused codec tests cover PSD and PSB fixtures, including masks and groups. Open the result in Photoshop and you have a layered 8-bit RGB file. Open the `.oma` when you need the vectors and the live type again. View → Document conversion notes is the list of what the round trip reduced. Headless uses the same codecs: `omadesign --inspect artwork.psd` and `omadesign --convert artwork.oma --output artwork.psd`. Same-file conversion is refused. The command will not overwrite the PSD you passed as the input. Place, Ctrl+Shift+P, uses this same reader when the file you place is a PSD or PSB. Nested layers and masks travel together into the current document. Open is the path that starts a new tab at the file’s own dimensions. Both leave the source PSD untouched. ## In the hand Press Ctrl+O. Choose the `.psd` or the `.psb`. The tab opens at the original pixel size. Walk the layer list from the top. Groups expand. Names match. A hidden layer is still hidden. Opacity and blend are on the layer. Click a mask and confirm it is aligned with the art, including a mask that sat at a negative offset. Open View → Document conversion notes. Find the lines for type, smart objects, and effects. If type came in as pixels, believe the pixels. Select that layer when you need the picture of the old headline. Press T and set a new line in a project font when the words are allowed to change. Apply a Normal color overlay’s native effect if the note says that one came across as editable. Leave the other effects alone. They are not waiting in a collapsed panel. Press Ctrl+Shift+S and write a `.oma` next to the PSD. Do not use the PSD’s filename with a different suffix in a way that your next script will confuse, and do not point a convert at the same path. The PSD’s modification time stays. The `.oma` is the file you edit. To hand a layered file back, export PSD or PSB. You get 8-bit RGB. Text and vectors from the `.oma` are pixel layers in that export, each on its own layer, with a note where editability was reduced. The color overlay that qualified is still an effect you can edit. Send the PSD to Photoshop. Keep the `.oma`. If the file is over 64 megapixels, over the 512 MiB working budget, over 8,192 layers, or nested past 64 groups, the reader stops. That refusal is the bound. Split the file in Photoshop, or open a flattened delivery if all you needed was the composite. ## The edge Text and smart objects become the pixel layers saved inside the PSD. The bridge does not reconstruct live type or the embedded smart-object document. What you can edit as type is type you set here, in the `.oma`. What you can see of the old type is the raster Photoshop stored. Export does not write a 16-bit or CMYK PSD. The way back is RGB, 8-bit, layered. Higher depth on the way in is reduced to 8-bit RGBA with a note. Keep the original PSD if the extra bits still matter. Press Ctrl+O on the `.psd`, read the conversion notes, and save a `.oma` beside it. ## The thread Part 107 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-export-matrix) · [Next](/blog/omadesign-0-5-8-affinity-optional-bridge) --- # Affinity optional bridge Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-affinity-optional-bridge Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, affinity ## The habit Affinity Designer, Photo, and Publisher are the files people actually have: `.afdesign`, `.afphoto`, `.afpub`, plus templates and packages. You open them in Affinity and the layers are live. Vectors are vectors. Type is type. Adjustments sit in the stack and can be toggled. History is a private list of edits. When you have to leave Affinity, the usual advice is to export PDF or SVG first, because almost nothing else reads the native file well. You would rather use File → Open on the Affinity file and get a layer tree. You also know that "open an Affinity file" in another app has meant a flat PDF preview, or a converter that half-works and then fails on the one Photo document that matters. The honest version is optional, partial, and loud about what it dropped. ## The constraint The Omadesign binary is MIT. The Affinity converter is not part of that license story. Inkscape’s converter and the files under `scripts/affinity-bridge/` are GPL programs. Setup has to preserve their source and their license. A distribution that bundles them has to meet those obligations. The native application does not swallow the converter into the MIT binary and hope nobody notices. Import does not download software. You run `./scripts/setup-affinity-import.sh` once. The script installs a pinned external converter. Inkscape’s Python modules are required, plus Python zstandard, Pillow, and NumPy. If those are missing, the bridge is missing. Open will not fetch them in the background the first time you click an `.afdesign`. That is the same rule as the rest of the app. The binary you installed is the binary. Extra readers that carry another license are a step you take on purpose. There is no native writer for `.af`, `.afdesign`, `.afphoto`, `.afpub`, or the other Affinity suffixes. The way back is PDF, SVG, or PSD. The bridge is an importer. Partial means partial. Adjustments, live effects, publishing structures, custom profiles, and edit history are the things that usually do not survive. The conversion notes have to say so for the file in front of you. Multi-spread documents can fail. You keep the Affinity original. ## What landed After setup, File → Open reads `.afdesign`, `.afphoto`, `.afpub`, `.aftemplate`, and `.afpackage` through the converter. Supported vectors, text, pixel layers, groups, visibility, opacity, masks, and artboards come across via SVG, into a tab at the file’s own dimensions. The source file is preserved. Save writes a `.oma`, and the notes stay in it. The converter also knows the newer `.af` container through upstream Affinity V3 work. No real unified `.af` fixture has been verified in this change. Legacy Designer fixtures have been converted. Real `.afdesign` and `.afphoto` files have been exercised. That does not establish general fidelity for `.afpub`, `.aftemplate`, `.afpackage`, or a unified `.af`. Open will try the supported path. The notes and the layer list are the evidence for your file. A template or a package that misses is a miss, not a quiet flatten you should ship. A real Photo document hit a missing format-9 raster decoder. The separate GPL compatibility module now reads its floating-point channel tiles, including constant-one alpha tiles. That file produces seven image objects, with no conversion exceptions, where it used to produce four. Channels become 8-bit RGBA. Values outside 0–1 are clipped. Custom profile transforms are not applied. The import report says so. This is not an HDR-preserving conversion. If the Photo file’s point was the high range, the Affinity original is still the file that has it. Affinity resource files are not layered documents. `.afassets`, `.afbrushes`, `.afstyles`, `.afpalette`, and `.afmacro` will not open as a poster. `.afbook` references chapter documents. An `.afpackage` is a document plus resource folders, not necessarily a ZIP you can rename. Linked resources still have to be available on the conversion path. A package whose links point at a missing disk will convert the part it can see. Headless uses the same bridge. `omadesign --inspect artwork.afdesign` writes JSON with the canvas, the pages, the layer metadata, and the import notes. `omadesign --convert artwork.afphoto --output artwork.oma` writes the `.oma` and prints notes to stderr. Same-file conversion is refused. Place, Ctrl+Shift+P, can load an Affinity file into the current document once the bridge is installed. Nested layers and masks travel together, same as any other place. Dropping an Affinity document on the canvas or the welcome screen opens it, because a layered document takes the open path. The file manager can offer Open With for Affinity documents without changing the default application you already set. Affinity can remain the double-click app. Omadesign is there when you send the file to it. ## In the hand From the Omadesign source tree, run the setup once: ```sh ./scripts/setup-affinity-import.sh ``` You need Inkscape’s Python modules, plus zstandard, Pillow, and NumPy. The script pins the converter. It does not phone out for a newer one on the next open. Read the license files it preserves if you are bundling the result. The application you draw in stays MIT. The bridge stays GPL. Press Ctrl+O. Choose the `.afdesign` or `.afphoto`. The tab opens at the original size. Check groups, names, visibility, opacity, and masks. Artboards should be artboards when the converter returned them. Press T on a text object only after you confirm the text came across as text. Some complex text on other formats becomes outlines. Believe the layer, then believe the notes. Open View → Document conversion notes. Adjustments and history will often be on that list as dropped. A live filter you used every day in Affinity Photo is probably not a live filter here. The pixels or the SVG the converter produced are what you have. Save a `.oma` with Ctrl+Shift+S beside the Affinity file. Do not overwrite the `.afphoto`. If the document is Publisher, a template, or a package, open it knowing the verification bar is lower. Look at the notes. If the result is wrong, export PDF or SVG from Affinity and open that. PDF import is native. It does not need this script. Multi-spread files that fail should fail in the notes, not as a cropped first page you mistake for the whole job. To leave again, export PDF, SVG, or PSD from the `.oma`. There is no Save As `.afdesign`. ## The edge The bridge is optional, and Open will not download it. Without `setup-affinity-import.sh`, an Affinity file does not grow a hidden fallback reader inside the MIT binary. With the bridge, the import is still partial. Adjustments, live effects, publishing structures, custom profiles, and edit history are commonly dropped. There is no native Affinity writer on the way out. Keep the `.afdesign` or `.afphoto`. Run `./scripts/setup-affinity-import.sh` once, then press Ctrl+O on the Affinity file and read the conversion notes. ## The thread Part 108 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-psd-psb-bridge) · [Next](/blog/omadesign-0-5-8-pdf-ai-import-export) --- # PDF AI import export Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-pdf-ai-import-export Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, pdf ## The habit PDF is the file you send when the other person does not have your application. Illustrator’s `.ai` is usually a PDF with a private tail. You turn on "Create PDF Compatible File" and the world can preview it. You turn it off and the world sees blank pages. Photoshop and Affinity both place PDF and both export PDF. The artboards, the paths, the images, and the optional-content groups are the part you expect to see. Form fields, comments stuck in the margin, and a spot color you built for a press are the part you already suspect will not survive a casual open. The habit on the way out is the same. Export PDF. Keep the native file. Anyone who treats the PDF as the only master finds out, at the worst time, that the type has become outlines and the shadow has become a picture. ## The constraint PDF import and export are in the binary. Poppler-class checks exist so a page can be compared with an independent renderer. There is no Illustrator writer. Private `.ai` data is not a format this app emits or rebuilds. The PDF-compatible stream is the artwork. If that stream is empty, the import is empty. The note says so. You go back to Illustrator and save a PDF that contains the art, or you turn compatibility on and save again. Encrypted PDFs are locked on purpose. The importer will not break that protection, and it will not write a decrypted file over the original. You save a copy with the password protection removed, then open that copy. Ghostscript is the door for older PostScript `.ai`, EPS, and PS. It has to be installed. The conversion runs in a private temporary directory with a bounded runtime and output, then the PDF importer does the real read. A missing Ghostscript produces setup guidance. It does not shell out to a guessed path and dump a file into the source folder. Export has to stay editable where the page is simple, and honest where it is not. Text becomes outlines in the PDF, because a receiving RIP should not need your project fonts. The `.oma` keeps the live text. Every raster fallback writes a note. An effect is not replaced with a single gradient color and shipped as if nothing happened. ## What landed PDF import creates an artboard for every page. Paths and strokes come in editable. Nested optional-content groups become layer groups. Those are the PDF’s OCG layers: names, structure, and visibility, the same idea as Illustrator’s top-level layers when they were written into the PDF. Embedded images become pixel layers. Supported axial gradients, blending, opacity, clipped artwork, and alpha or luminosity soft masks are read. Clips and soft masks become cropped pixel masks. Straightforward text, and embedded OpenType, can stay editable. Embedded CFF, and text that is transformed or complex, can become editable outlines. An embedded subset may not contain the characters you need for a later edit. You can shape the outlines. You may not be able to type a new word in that subset font. Set the new word with a project font in the `.oma`. The import reports approximations. Unsupported drawing operators, radial and mesh and pattern shading, dash phases and patterns, custom miter limits, font substitutions, ICC and spot and pattern colors, overprint, and knockout. PDF transparency groups come in. Non-isolated groups are approximated as isolated groups. Annotations and form fields are not artwork. They do not become layers you can move. A real four-page kitchen drawing imported as 608 shapes, 48 groups, and 142 layers, and it was compared visually with Poppler. A 28-artboard Illustrator document exposed blank PDF-compatible pages. An independent PDF renderer showed those pages blank as well. Artwork that exists only in Illustrator’s private data cannot be recovered on this path. The diagnostic is the point. You do not hunt through empty artboards hoping the private tail will decode. PDF export writes pages, paths, raster images with alpha, opacity and blending, and optional-content layer metadata. That metadata includes names, order, visibility, and locks. Text becomes vector outlines. Gradients, masks, effects, and complex compositing render at document resolution into the affected layer or group. Vector layers the fallback did not touch stay editable. A rendered group becomes one named PDF layer. The objects inside that group remain in the `.oma`. Hidden layers keep their hidden state. Backdrop-dependent pass-through effects can require rendering entire pages. The page fallback for that case retained the exact native composite and matched Poppler’s Cairo backend pixel for pixel. A separate Splash-backend check covers layers and alpha. Fallback images stop at 64 megapixels each and a 512 MiB total pixel budget. `.ai` import is the PDF-compatible artwork, or older PostScript through Ghostscript and then PDF. There is no Illustrator-native writer. Export PDF or SVG when Illustrator is the other end. Private data, live effects, symbols, and appearance stacks are not reconstructed on import, and they are not written on export. Headless: `omadesign --convert artwork.oma --output artwork.pdf`. Notes go to stderr. The `.oma` is not overwritten. Same-file conversion is refused. ## In the hand Press Ctrl+O. Choose the PDF or the `.ai`. Count the artboards against the page count you expect. A 4-page PDF should give you 4 artboards, not page one stretched across a default canvas. Open the layer list and look for the optional-content groups. Hide and show them. They should match the PDF’s layers. Open View → Document conversion notes. Spot colors, overprint, and mesh gradients will be named there when the file used them. The paths you can edit with the Node tool, A, are real paths. The shading that became an approximation is what the note describes. Click a text line. If it is still text, edit it and remember the subset may be missing glyphs. If it is outlines, edit the outlines or retype. For an `.ai` that opens blank, the PDF-compatible section is empty or blank. Check the notes. Open the same file in a PDF viewer. If the viewer is blank too, Illustrator never wrote the art into the PDF part. Save a compatible PDF from Illustrator and open that. Do not keep re-importing the private file and hoping for a different decoder. Press Ctrl+Shift+S and save a `.oma`. Export PDF when you are ready to send pages. Read the notes after export. A group that became one pixel layer is named in the PDF and still editable in the `.oma`. Send the PDF. Keep the `.oma`. Old EPS or PostScript `.ai`: install Ghostscript, then Open again. The temporary conversion stays in a private directory. Your EPS is not rewritten. Encrypted file: save a copy with the password protection removed, from a tool that already has the password. Open the copy. Leave the encrypted original alone. ## The edge Private Illustrator data is not reconstructed. An `.ai` contributes its PDF-compatible artwork, or its older PostScript after Ghostscript turns that into a PDF. Live effects, symbols, and appearance stacks that live only in the private section do not come back. If the compatible pages are blank, the tab is blank. PDF export does not keep live project text. The outlines are the delivery. The `.oma` is the edit. There is no Save As `.ai`. Press Ctrl+O on the PDF, check every artboard, then save the `.oma` and export PDF when the pages have to travel. ## The thread Part 109 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-affinity-optional-bridge) · [Next](/blog/omadesign-0-5-8-openraster-gimp) --- # OpenRaster GIMP Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-openraster-gimp Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, openraster ## The habit GIMP’s file is `.xcf`. You keep the type as text, the layer effects as effects, and the paths in the paths list. When you have to leave GIMP, OpenRaster is the layered format that was built for this. `.ora` is a zip. `stack.xml` names the stack. PNG layers sit inside. A merged preview and a thumbnail ride along so a file manager can show something. Krita, GIMP, and a handful of other editors read it. PSD is the other way out, because Photoshop is still where a lot of layered files end up. You want Open to read a `.xcf` without launching GIMP, and you want a way back that GIMP can open. You do not want a writer that emits a half-valid `.xcf` and corrupts the file you still need. ## The constraint The `.xcf` reader follows GIMP’s XCF specification. It does not start GIMP. It does not write `.xcf`. Writing XCF well means writing GIMP’s current tile format, its text engine, its effect stacks, and its paths. That is GIMP. This binary reads the pixel stack it can defend, then stops. The way back is OpenRaster or PSD, both of which GIMP opens. The `.oma` stays the master, because GIMP text and effects arrive here as pixels and will not become live GIMP objects on the return trip either. OpenRaster is the public 0.0.6 ZIP and XML specification. The writer has to produce a legal archive: the first entry is the uncompressed `mimetype`, then `stack.xml`, `mergedimage.png`, and a thumbnail no larger than 256×256. ZIP entries are read directly. The importer does not extract the archive onto the filesystem, where a hostile path could land outside the file you thought you opened. Budgets are 512 MiB for the archive, the expanded data, and the aggregate pixels. The canvas stops at 32,768 pixels on a side and 64 megapixels. Unsupported compositing is rejected. An OpenRaster blend this app cannot map does not get silently rewritten to Normal. Extended layer sources that are not PNG are rejected. A quiet "fixed" file is worse than a refusal. ## What landed OpenRaster import reads pixel layers and groups, offsets including negative offsets, names, visibility, opacity, isolation, and supported blends. Hidden layers stay hidden. PNG layers stay separate. Groups keep their hierarchy. Imported pass-through group opacity is applied to the descendants, with a note, so OpenRaster’s compositing rules still hold. Layer locks can be stored with an Omadesign XML extension. Other applications may ignore that extension. Sixteen-bit PNG channels become 8-bit RGBA, with a note. Vectors and text in an OpenRaster file become pixels per layer on the way in, when they are not already PNG layers. The common `.ora` is already PNG layers. On the way out, export renders vector artwork separately for each layer. A masked or effected group becomes one pixel layer. Rendered layers are clipped to the document canvas. When a translucent native pass-through group is exported, a note explains that overlapping children may composite differently. Isolated groups are the consistent interchange. Use isolation when the OpenRaster file has to match. The archive you write includes the required `mimetype` entry first and uncompressed, `stack.xml`, `mergedimage.png`, and the thumbnail. Checks against that archive used Python’s `zipfile`, XML, and Pillow: entry order, nested groups, hidden layers, exact pixel channels, negative offsets, and the required previews. A fixture built independently with Python’s ZIP and PNG primitives was imported the other direction. The reader and the writer are not grading their own homework alone. GIMP `.xcf` uses the native reader. Pixel layers, groups, names, visibility, opacity, offsets, supported blend modes, and applied layer masks become native layers. Pointer width can be 32-bit or 64-bit. Tiles can be RLE, uncompressed, or zlib. 8-bit RGB, grayscale, and indexed documents are the preferred inputs. Higher precision is reduced to 8-bit RGBA with a note. Live text, layer effects, paths, and floating selections are not reconstructed. An unmapped blend mode displays as Normal. The note is how you know the blend did not survive. Verification used independently encoded fixtures for grouped RLE layers, 64-bit pointers, and zlib tiles, plus a bounded rejection of oversize input. There is no `.xcf` writer. Export OpenRaster or PSD, then open that in GIMP. Save the `.oma` if you still need editable vectors, live text you set here, or the imported stack in Omadesign’s own form. The `.xcf` you opened is unchanged. Open never writes it. Headless matches the desktop. `omadesign --inspect artwork.xcf` and `omadesign --convert artwork.oma --output artwork.ora`. Same-file conversion is refused. Notes print to stderr and stay in the `.oma` when the destination is a `.oma`. ## In the hand Press Ctrl+O and choose the `.xcf`. The tab opens at the file’s dimensions. Groups expand. Names, visibility, opacity, and offsets should match the pixel stack. Masks that were applied come in as masks. Open View → Document conversion notes before you look for the text tool. GIMP text is pixels here. GIMP layer effects are pixels. Paths and floating selections are not in the layer tree as live objects. An unmapped blend will look Normal. The note names that. Press Ctrl+Shift+S and write a `.oma` beside the `.xcf`. Edit in the `.oma`. New type, press T, is real type. It is not a revival of the GIMP text layer. To send the stack back, export OpenRaster. GIMP opens `.ora`. You get PNG layers, the stack, the merged preview, and the thumbnail. Or export PSD if the other end is Photoshop or a GIMP workflow that already prefers PSD. Do not look for Save As XCF. It is not there. If you are starting from Omadesign art and the destination is GIMP, export `.ora` while the groups are still simple. A group with a mask and an effect will land as one pixel layer. Split that work, or accept the bake, and read the note. Isolated groups travel more predictably than a translucent pass-through group. The note on pass-through tells you the overlap may composite differently. Read it before you call the `.ora` wrong. Dropping a `.xcf` or an `.ora` on the canvas or the welcome screen opens it, the same as Ctrl+O. Place, Ctrl+Shift+P, loads one into the current document when you meant to composite, not to switch files. ## The edge There is no `.xcf` writer. The reader will not round-trip a GIMP file back onto itself. Live GIMP text, layer effects, paths, and floating selections are not reconstructed on the way in. Export OpenRaster or PSD for the way back. Keep the `.oma` as the master, and keep the original `.xcf` if GIMP still has to edit the live text. OpenRaster rejects blends and layer payloads it cannot represent. It will not rewrite them to Normal and save. Press Ctrl+O on the `.xcf`, save a `.oma`, and export `.ora` when GIMP needs the stack. ## The thread Part 110 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-pdf-ai-import-export) · [Next](/blog/omadesign-0-5-8-headless-inspect-convert) --- # Headless inspect convert Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-headless-inspect-convert Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, cli ## The habit You already batch this work. ImageMagick for rasters. A Photoshop droplet when you still had a Mac in the corner. `magick` or `convert` in a shell loop, with a careful output name so you never clobber the source. Affinity and Illustrator are worse in a terminal. They want a GUI, or a script host that is really the GUI with the screen off. You want one binary you already installed, the same one that opens the file in the window, callable from a script. Inspect is the dry run. You want canvas size, page count, layer names, and the import notes before you spend the convert. Convert is the write. A different path. Notes on the error stream so a log can keep them. A hard stop if the output path is the input path. ## The constraint The command line uses the same readers and writers as the desktop. A PSD that opens in the window has to inspect as that PSD. A second headless codec that "mostly" matches will drift, and the drift will show up at two in the morning in a batch. One binary means the `omadesign` on your `PATH` is the studio. `~/.local` is where the installer puts it. You do not install a server edition. Conversion prints notes to stderr and writes a separate destination file. Same-file conversion is refused. The original is protected by that refusal, not by a hope that you typed a different name. General source size stops at 512 MiB. Individual readers add their own limits: object counts, pixel budgets, decompression, process time. A file the window would reject, the terminal rejects. Inspect writes JSON. Dimensions, pages, layer metadata, import notes. You can read it with the tools you already use on JSON. You do not get a pretty brochure on stdout and the real answer hidden in a GUI toast. The desktop still owns the brush. Headless is open, inspect, convert, and the plugin batch commands. It is not a second studio with its own file format. Output formats are the ones the writers already know. ## What landed `--inspect` reads the file and writes JSON. The useful set in the short claim is a PSD, an Affinity file, an XCF, a NEF, and a `.omaphoto`. The same flag works on the other files the desktop opens, because the readers are shared. Affinity inspect needs the optional bridge installed, the same as File → Open. A NEF inspect includes camera metadata, source dimensions, precision, and saved development settings when a sidecar applies. A `.omaphoto` inspect is the settings file talking about its original. ```sh omadesign --inspect artwork.afdesign omadesign --inspect artwork.psd omadesign --inspect artwork.xcf omadesign --inspect photograph.NEF omadesign --inspect photograph.NEF.omaphoto ``` `--convert` takes an input and `--output`. Headless document outputs are `.oma`, `.svg`, `.png`, `.jpg` or `.jpeg`, `.psd`, `.psb`, `.pdf`, and `.ora`. Notes go to stderr. The destination is a new file. ```sh omadesign --convert artwork.afphoto --output artwork.oma omadesign --convert artwork.oma --output artwork.psd omadesign --convert artwork.oma --output artwork.pdf omadesign --convert artwork.oma --output artwork.ora omadesign --convert photograph.NEF.omaphoto --output developed.tif omadesign --convert photograph.dng --output photograph.tif ``` RAW conversion is the photo pipeline, covered on its own. The command is the same `--convert`. TIFF and PNG from a raw file are 16-bit. JPEG is 8-bit. A raw file converted to a document format becomes an 8-bit pixel layer. Saved `.omaphoto` settings apply automatically when the sidecar matches. The camera file is not overwritten. A convert onto the same path is refused. `omadesign --convert artwork.psd --output artwork.psd` does not run. Pick `artwork.oma` or `artwork.pdf`. The refusal is the guardrail. A script bug should fail the file, not replace a client’s PSD with a partial write. Import notes in the JSON are the same notes as View → Document conversion notes. Stderr on convert is the same list. A batch can grep stderr, and a person can open the `.oma` and use the menu. If the note says Photoshop text came in as pixels, the `.oma` has pixels. The terminal did not get a secret better reader. Source files stay put. Open in the GUI does not write back, and convert does not write back to the input. The 512 MiB general cap sits in front of the per-reader caps. A PSD also stops at 64 megapixels, 512 MiB of decoded working set, 8,192 layers, and 64 group levels. OpenRaster stops at its archive and canvas budgets. RAW stops at 64 megapixels, 512 MiB input, and a 120 second cooperative cancel inside the decoder. The terminal reports the failure. It does not hang the session the way a stuck dialog would. Plugin runs are a different flag, `--plugin`, with `--command`, `--input`, and `--output`. They use the same "do not overwrite an existing output" rule. Inspect and convert are the codec tools. The plugin runner is for actions you installed. You do not need a plugin to turn a PSD into an `.oma`. ## In the hand Confirm the binary. `omadesign --inspect` on a small PSD should print JSON and exit. Look for the canvas size and the layer names you remember. Open the same PSD with Ctrl+O. The layer list should match the JSON. If Affinity inspect fails immediately, run `./scripts/setup-affinity-import.sh` and try again. The CLI does not download the bridge either. Convert one file to a new name: ```sh omadesign --convert artwork.psd --output artwork.oma ``` Read stderr. Then open `artwork.oma` and check View → Document conversion notes. Save nothing if you are only looking. The source `artwork.psd` is still the source. Loop a folder by giving each file its own output path. Keep the extension you actually want. `.oma` for a master you will edit. `.png` or `.jpg` for a flat. `.pdf` for pages. `.psd` for a layered handoff. `.ora` for GIMP. Do not build the output path by replacing nothing. If input and output resolve to the same file, the command refuses it. That includes a relative path and an absolute path that are the same inode. Point `--output` somewhere else. For a photograph, inspect the NEF first. Then convert to TIFF if you want the 16-bit develop. The camera file’s checksum should match before and after. The command never writes the NEF. ## The edge Same-file conversion is refused. `--convert` will not write the destination on top of the source. The original stays the original. Notes go to stderr and into the document. They are not a reason to overwrite the file they describe. The CLI will not grow a codec the window does not have. No `.ai` writer, no `.af` writer, no `.xcf` writer. The outputs are `.oma`, `.svg`, `.png`, `.jpg`, `.psd`, `.psb`, `.pdf`, and `.ora`, plus the RAW TIFF, PNG, and JPEG path. Run `omadesign --inspect` on the file, read the JSON, then `--convert` to a different path. ## The thread Part 111 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-openraster-gimp) · [Next](/blog/omadesign-0-5-8-raw-headless-develop) --- # RAW headless develop Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-raw-headless-develop Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, raw ## The habit You develop a raw file in Lightroom, Capture One, or Affinity Photo, and the original stays a raw file. The adjustments live in a catalog or a sidecar. Export writes a TIFF or a JPEG somewhere else. The cardinal sin is a dialog that saves "the photo" and replaces the camera file. A DNG that got overwritten with an 8-bit render is gone. You do not get the mosaic back. On a Linux box the batch version is usually `dcraw`, `darktable-cli`, or RawTherapee’s command line. You point them at a NEF and a destination. You check the source checksum afterwards because you have been burned. Omadesign’s Photo studio already does this in the window: open the raw, save settings beside it as `.omaphoto`, export a separate JPEG, PNG, or TIFF. The headless command has to be that same pipeline, with the same refusal. ## The constraint The decoder is in the binary. LibRaw 0.22.2 is bundled. Opening a raw file does not download a converter and does not require one installed on the system. The source and the license ship with the packages. Sensor data is read at full resolution. Black levels come off. Supported Bayer and X-Trans sensors are demosaiced. The camera white balance and color matrix are applied. Orientation is honored. The decode is 16-bit linear sRGB with automatic brightness disabled. DNG baseline exposure, and the Photo exposure and white-balance adjustments, happen before the display transfer. The camera file is never the output. Same-file conversion is refused for every convert, and raw has the stronger rule on top: there is no raw writer at all. A command cannot "save the DNG." Export is a new file. `.omaphoto` is settings, not pixels. It sits beside the original, name-matched. The source size and modification time have to still match or the settings are not applied as a silent guess. This check is not a cryptographic identity. It is enough to stop a sidecar from developing the wrong frame after the camera file was replaced. JPEG from this path is 8-bit, a delivery image. PNG and TIFF keep 16-bit developed channels. A batch that wants the bits asks for TIFF. A batch that wants a small proof asks for JPEG. Both leave the raw file’s bytes alone. ## What landed ```sh omadesign --convert photograph.dng --output photograph.tif ``` That develops the DNG through the Photo pipeline at full resolution and writes a 16-bit TIFF. PNG is the other 16-bit container. JPEG is 8-bit. ```sh omadesign --convert photograph.NEF --output photograph.jpg omadesign --convert photograph.NEF.omaphoto --output developed.tif ``` A matching `.omaphoto` is applied automatically. Save settings in Photo writes that sidecar. The name looks like `DSC_0001.NEF.omaphoto` next to `DSC_0001.NEF`. Headless convert of the raw file, or of the sidecar, uses those settings: exposure, crop, rotation, the development you saved. The original image data is not rewritten. Inspect shows the camera metadata, the source dimensions, the precision, and the saved development: ```sh omadesign --inspect photograph.NEF omadesign --inspect photograph.NEF.omaphoto ``` Recognized extensions include DNG, CR2, CR3, NEF, NRW, ARW, RAF, ORF, RW2, PEF, and the longer family list in the format guide, from `.3fr` through `.x3f`. An extension names a family. It does not mean every camera and every compression mode in that family works. JPEG XL compressed DNG, GPR, EIP packages, and R3D video are not supported. Only the first image of a multi-image raw is developed, with a conversion note. Exported files are display sRGB. They do not keep the sensor mosaic, the camera’s edit history, or every source metadata tag. Lens corrections, proprietary camera looks, full Adobe camera profiles, some DNG opcodes, and multi-frame computational rendering are not recreated. Rendering is not trying to match Lightroom, Capture One, or the in-camera JPEG. Clipping in the sensor or the channel is not recovered by a later exposure move. The decoder rejects images over 64 megapixels and inputs over 512 MiB. It limits LibRaw’s unpacking buffers. It asks LibRaw to cancel through the progress callback after 120 seconds. That callback is a cooperative limit, not a hard process kill you should bet a deadline on. Working memory still includes the decoded pixels and the development buffers. Design placement and a convert to a document format are 8-bit. `--convert photograph.dng --output photograph.oma` gives you an 8-bit pixel layer in a document, not a 16-bit Photo master. Keep the raw and the `.omaphoto` if you still need to develop. Place in Design from the Photo studio follows that same 8-bit rule. Checked files included an iPhone 16 Pro Max ProRAW DNG, a compressed Canon EOS R6 CR3, and a compressed Fujifilm X-T30 II X-Trans RAF. On the validated native build, the decoded 16-bit RGB and the full-resolution PNG exports matched independent LibRaw and sRGB references pixel for pixel. Sizes were 3024×4032, 3407×2271, and 6246×4170. A sidecar round trip restored exposure, rotation, and crop. Its 3024×1512 TIFF stayed 16-bit and matched an independent develop-and-crop reference. The JPEG kept the same dimensions. Source SHA-256 values did not change. Those three cameras are not a promise for every body on the extension list. In the window, the same rules hold. File → Open, the Photo library, a folder, Open With, or a drop. Save settings writes the sidecar. Export writes a different file. Before shows the default camera-balanced develop, not the embedded JPEG. Ctrl+E in Photo exports the developed PNG. ## In the hand Put `photograph.dng` in a folder. If you already developed it, `photograph.dng.omaphoto` sits beside it with the same stem. ```sh omadesign --inspect photograph.dng omadesign --convert photograph.dng --output photograph.tif ``` Open the TIFF in anything that shows bit depth. You want 16-bit channels, full resolution, crop and rotation included when the sidecar said so. Checksum the DNG before and after. It matches. The command has no raw output it could have written. For a contact sheet, write JPEG: ```sh omadesign --convert photograph.dng --output photograph.jpg ``` 8-bit, same dimensions as the develop, smaller file, still not the camera file. For a script over a directory, write outputs to a different directory. If `--output` resolves to the DNG, the CR3, or the NEF, the convert is refused. If it resolves to the same path as the input for any other reason, it is refused too. When the sidecar and the raw disagree, because the raw was replaced or resized, an explicit open of the settings fails without replacing the current photo. Opening the raw itself shows the default develop and a note when its settings cannot be used. Headless follows that binding. A stale `.omaphoto` is not a free pass to invent a crop. Failed saves in the studio keep the unsaved edits so you can retry. Unsaved slider moves exist in the Photo session only. A `.oma` does not store them. Save settings if the batch has to see the develop. ## The edge The camera file is never overwritten. There is no raw writer. `--convert` refuses a destination that is the source file. `.omaphoto` stores settings beside the original. It does not store a flattened copy inside the DNG. JPEG is 8-bit. TIFF and PNG are the 16-bit develop. A document conversion is 8-bit pixels. The raw file and its sidecar remain the master for another pass. Run `omadesign --convert photograph.dng --output photograph.tif` and leave the DNG where the camera wrote it. ## The thread Part 112 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-headless-inspect-convert) · [Next](/blog/omadesign-0-5-8-cloud-opt-in) --- # Cloud opt-in Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-cloud-opt-in Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, cloud ## The habit Creative Cloud, and every cousin of it, wants the file in the service before the application feels finished. You save. A sync icon spins. A teammate sees the file because it lives in a folder the account owns. Publish is a second switch, sometimes, and sometimes the share link is the same act as the save. You learn to keep a local copy because the service has been down, and you learn to distrust a gallery that filled itself with drafts. Photoshop and Illustrator still open a local PSD or AI. The pressure is the default. Libraries, fonts, and cloud documents sit one click from the canvas. Affinity has been the opposite pitch for years: the file is a file. The habit you bring to a Linux studio is the second one. The app has to run with no account. The file has to save with Ctrl+S to a path you picked. Cloud, if it exists, is a door you open. ## The constraint Omadesign is one binary and one `.oma`. Welcome browses local files. Projects are folders that contain `.omabrand`. A project needs no account. Sign up for cloud, on the welcome screen, opens registration at the cloud workspace. Local editing and project browsing stay available when you never click it. That split is the opt-in. An account is not a license check on the brush. The short claim says the document stays on disk until File → Enable cloud sync. The manual names the commands that actually exist now. File → Sign in opens a secure browser approval. Push project + review export uploads a versioned design and a flat snapshot. Cloud projects pulls a shared design into a new document. Review annotations loads the feedback. Publishing, and entering a competition, are separate owner actions. "Enable cloud sync" was the earlier way to describe pointing a document at the service. The gesture now is the push. The file you edit is still the file on disk. Transfers are explicit. A local edit does not upload itself and does not overwrite another designer's version. Every push adds files. You save the local `.oma` after the first push so the cloud link stays in the document. There is no background folder that drains `~/Projects` into a bucket. The workspace is `https://omadesign.app/cloud`. The browser is for sharing and review. Live multi-user canvas editing, presence, and authoring in the browser are outside this scope. You draw in the native app. You review a snapshot. This shape shipped as collaboration in 0.5.4: project files, assets, and immutable flat exports, with private membership and a separate public showcase. It is not a live shared canvas. 0.5.8 did not turn cloud into a requirement. The release you are reading still saves a local `.oma` first. ## What landed Cloud is opt-in. Until you sign in and push, the document is a file. Ctrl+S writes it. Ctrl+Shift+S names it. Nobody else can see it, because nothing was uploaded. Unpublished files do not appear in the gallery. The gallery is `/showcase`, and it lists work an owner published on purpose. A push is not that publish. What a push sends is a versioned `.oma` and a PNG of the current document. Project font assets are included. Raster pixels stay embedded in the design file. Upload project asset… adds another file when the project needs one. The service limits are concrete. Source and asset files stop at 100 MB each. Flat PNG, JPEG, and WebP exports stop at 20 MB. A project holds 200 files. An account starts at 100 projects. A project holds 100 members. Those are the bounds of the opt-in, not a reason to opt in. Cloud projects → Pull & open takes the latest source into a separate document and downloads shared assets into a new folder under the app's cloud-downloads directory. Your current tab is not replaced by someone else's latest push. Review annotations… loads the versioned exports and the threads. You reply and resolve there. The canvas you are drawing on stays local until you decide the next push. Archive, on the web workspace, hides a project from collaborators and unpublishes its public work. The owner can restore it. Archive is still an explicit act. It is not what happens when you close the laptop. If the desktop has no cloud URL configured, it can keep working with a local store. The public site does not. Production identity and data live with the services behind `omadesign.app`. The absence of an account on your machine does not block the pencil. The absence of a push does not block the save. ## In the hand Draw the poster. Press Ctrl+S. Look at the folder. The `.oma` is there. Open `https://omadesign.app/showcase` in a browser if you want to see the public gallery. Your new file is not in it. You have not published, and you may not even be signed in. When you want the service, use File → Sign in and approve the browser step. Come back to the document. Choose Push project + review export. The upload is the version you just saved, plus a flat PNG. Save the local `.oma` again after that first push so the link is in the file. Edit some more. Those edits are local. They do not appear for anyone else until you push again. Each push adds files. It does not roll the previous snapshot into the new one and throw the old comments away. Comments stay on the snapshot they were written on. Leave every other open document alone. They are not in the project you pushed. A folder of client work on the same disk is not a sync root. There is no setting in this flow that means "upload all my drafts." To put one finished image in the public gallery, publish that export as its own step. Until you do, `/showcase` does not list it. A private project id does not become a public page because you pushed. Pull, when you are the person receiving, is Cloud projects, then Pull & open. You get a separate document. You do not get your unsaved local tab overwritten. ## The edge Unpublished work stays off the gallery. A push shares a project with the people on that project. It does not publish. Publish is a later owner action, aimed at one flat export, with the source and the private threads left out. A local edit stays local. Closing the file does not sync it. The document on disk is the document, including on a machine where you never create an account. Press Ctrl+S. The `.oma` is on disk. Push project + review export only when this document is one you mean to share. ## The thread Part 113 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-raw-headless-develop) · [Next](/blog/omadesign-0-5-8-sign-in-identity) --- # Sign in identity Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-sign-in-identity Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, cloud ## The habit Adobe's sign-in is the application. You enter an email, a window polls, and the menus that were gray become the menus that upload. Affinity, for a long time, did not ask. When a tool does ask, you want to know three things. Where the session lives. How long it lasts. Whether typing your email into a local file is the same thing as being allowed in. You also know the collaborator failure. You invite `sam@studio.test` and Sam signs in as `sam@gmail.com`. The invite sits there. The fix is to match the email, not to forward a link through a third inbox and hope the session sticks. ## The constraint The native app is the place you draw. The browser is the place identity is proved. File → Sign in opens a secure browser approval. The cloud guide is specific: sign in with a verified email through Clerk. The desktop shows a device code. The browser asks you to approve a code. You approve only the code the desktop is displaying. A code from a mail you did not start, or a code from a different machine, is not this sign-in. The short claim says File → Sign in… writes an identity file at `~/.config/omadesign/cloud-identity.json` on the desktop, and that Account on the site stores the name and email. Both belong in the picture. Preferences already live under `~/.config/omadesign`, or under `XDG_CONFIG_HOME` when that is set. The identity file is written with owner-only permissions. It is a local record of the desktop session. It is not a token you paste into chat, and it is not access by itself. A name or an email alone never grants cloud access. Someone who can read a string on disk is not a member of your project. Membership is an invitation to a verified email, accepted after sign-in. Desktop credentials expire after 30 days. You revoke them from `/account`, or you disconnect in the app. Waiting out the month is the slow version. Revoke is the version you use when a laptop leaves. Welcome stays useful signed out. Team appears on the welcome screen only while you are signed into cloud and shared team projects are available. The file browser does not grow a Team tab to shame you into an account. ## What landed File → Sign in… starts the device flow. A browser opens. You sign in with the verified email you mean to use for this work. You compare the code in the browser with the code on the desktop. You approve that pair. The local identity file is written owner-only. The site account holds the name and the email. `/account` is where you see that identity and where you revoke a desktop credential before the 30 days are up. Use the same email you will be invited with. Owners invite a verified address as an editor or a reviewer. The invitation arrives through Resend and expires after seven days. You sign in with the invited email and accept from the workspace. A second email, even one you also own, is a different identity. Match them on purpose. The short claim says this in one line because it is the mistake that wastes the invite. Disconnect in the app when this machine should stop being a signed-in desktop. Revoke from `/account` when you cannot reach the machine, or when you want the credential dead now. An expired credential does not keep pulling. Previously downloaded files stay on the disk that already has them. Revocation blocks the next request. It does not reach into a folder you already saved and shred it. That is the same rule as removing a project member. Private downloads recheck membership on every request. Old copies are the recipient's files. The identity is per person. It is not a project. Signing in does not upload the open `.oma`. Push project + review export is the upload, and it is a separate command. Signing in does not publish. Publish selected export is a separate owner action. The session is the permission to use those commands. It is not those commands. Production uses the Clerk issuer at `https://clerk.omadesign.app`. Development uses a different Clerk instance. You should not mix keys, and you do not need to, because sign-in from the installed app is the production flow. The desktop transport for later calls is `/api/cloud`. Sign-in is the step that makes those calls yours. If you never sign in, `cloud-identity.json` does not need to exist for the studio to run. Ctrl+S still saves. The Brand panel still reads the project folder. Photo still writes `.omaphoto` beside the camera file. Identity is for the cloud door. ## In the hand Open the app. Confirm you can save a local file while signed out. Then File → Sign in…. The browser opens. Sign in with the email you want on the account. Read the device code on the desktop. Approve that code in the browser. Do not approve a different code because it arrived first. Come back to the app. The session is on. `/account` shows the name and email. On disk, the identity file is under the config directory, owner-only. You do not need to edit it. Editing it by hand does not make you someone else, and it does not grant access the server refused. Invite check, when you are the person being invited: look at the email address in the invitation. Sign out if the desktop is a different address. Sign in with the invited one. Accept in the workspace before the seven days are up. A late invitation is expired. The owner sends another. On a machine you are giving away, disconnect in the app, then revoke that desktop from `/account` on a machine you still trust. Thirty days is the cap if you forget. It is not the plan. Team, on the welcome screen, shows when this signed-in identity actually has shared team projects. An empty Team is not a failure of the file browser. Your Work still lists local `.oma` files. ## The edge A name or an email in `~/.config/omadesign/cloud-identity.json` does not grant cloud access. The file is the local session record, written owner-only after you approve the device code. Access still requires that approval, a verified email, and, for someone else's project, an invitation you accept. Desktop credentials expire after 30 days. Revoke them from `/account`, or disconnect in the app, when the machine should stop now. File → Sign in…, then approve only the code on the desktop. ## The thread Part 114 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-cloud-opt-in) · [Next](/blog/omadesign-0-5-8-enable-sync-invite) --- # Enable sync invite Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-enable-sync-invite Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, cloud ## The habit In a shared Creative Cloud folder, "sync" means the directory. You drop a file in. It uploads. You invite an email to the folder. They see everything in it, including the exports you meant to delete. The careful version is a share link on one document, with a role: can edit, can comment. You still check, afterwards, that the other twenty files in the folder did not go with it. Illustrator and Photoshop cloud documents attach an identity to the file the moment it becomes a cloud document. Affinity's shared approach, where you have used it, is usually an export plus a mail. The gesture you want is closer to the careful one. This document. This email. This role. The rest of the disk stays put. ## The constraint The short claim says File → Enable cloud sync attaches a project id to the `.oma`, and File → Invite collaborator… adds an email to that document. The manual and the cloud guide use the commands you run now, and they are stricter than "add an email." The upload is File → Push project + review export. It uploads a versioned `.oma` and a PNG of the current document. Project fonts go with the project. Raster pixels stay embedded in the design file. You save the local `.oma` after the first push. That save keeps the cloud link in the document. The link is how this file remembers which remote project those uploads belong to. Every later push adds files. Comments stay attached to the snapshot they were written on. A local edit does not silently upload, and it does not overwrite another designer's version. The invite is an owner action on a verified email. The owner chooses editor or reviewer. Resend sends the invitation. It expires after seven days. The person signs in with that same email and accepts from the workspace. Until they accept, they are not a member. An email string written into a file is not a grant. Owners can change a role, remove access, or cancel an invitation that has not been accepted. Nothing in that flow walks the rest of your directories. Sync, in the sense of "this project is shared," is the project you pushed. Private drafts in other tabs stay drafts. ## What landed Push project + review export is the command. One document. A versioned source file and a flat PNG for review. Upload project asset… adds a file that is not that pair: a reference, a font already in the project bundle, something the review needs. Source and asset files are limited to 100 MB each. Flat PNG, JPEG, and WebP exports are limited to 20 MB. Each project holds 200 files. The initial service limit is 100 projects per account and 100 members per project. Cloud projects → Pull & open opens the latest source in a separate document. Shared assets download into a new folder under the app's cloud-downloads directory. Pull does not save over the `.oma` you happened to have open. You get a new document. You decide what to keep. Roles, once the invite is accepted: | Role | What they can do | | --- | --- | | Owner | Project files, uploads, review, team management, archive, publishing | | Editor | Download source and assets, upload, comment, reply, resolve threads | | Reviewer | Flat exports, pins, rectangular annotations, comments, replies. They can resolve their own threads. | A reviewer does not get the source as their working set. An editor does. The owner is the one who invites, changes the role, removes access, cancels a pending invite, archives, and publishes. Archive, from the web workspace, hides the project from collaborators and unpublishes public work. The owner can restore it. The local `.oma` on your disk is not deleted by an archive. Invitations are capped at 20 per account per hour. A mistaken paste of a whole company directory into the invite field is not a feature. Seven days, then the invite is dead. Send it again if the person still needs in. The web project accepts source, asset, and snapshot uploads under the same idea. The desktop is the authoring app. The browser is the share. You do not edit the live canvas in the browser. Two people do not paint one artboard at once. The next push is a new version. The previous snapshot still has the threads that were written on it. Save after the first push is not optional bookkeeping. Skip it and the local file can lose the link that ties it to the project. The bytes of the poster are still in the `.oma`. The connection to the uploaded version is what that save stores. Push again later only from the file that has the link, or you are starting a relationship the guide told you to keep on purpose. ## In the hand Sign in first. Open the one `.oma` you mean to share. Press Ctrl+S so the disk matches the canvas. Choose Push project + review export. Wait for the versioned source and the PNG to finish. Press Ctrl+S again. The cloud link is now part of the local document. Quit, reopen, and the file still knows its project. File the invite from the owner side. Use the verified email the other person will sign in with. Pick editor if they need the source and the right to upload. Pick reviewer if they should see flat exports and comment. They accept in the workspace within seven days, signed in as that address. Keep working locally. The next hour of edits is yours. Push again when there is a version worth review. The new push adds files. Old comments stay on the old snapshot. The reviewer opens the export version they were asked about, not a live view of your unsaved nudges. On the receiving machine, Cloud projects → Pull & open. A separate document opens. Assets land in a new cloud-downloads folder. Save that document where you want it. It does not replace some other client file that was already open. Check the files you did not push. They are absent from the project. A private draft in the next tab is still only on disk. Unpublished status is still unpublished. The gallery does not list this project because you invited one editor. ## The edge A push uploads the project you chose. It does not upload every private draft on the machine. Other `.oma` files stay local until you push those files, one by one, on purpose. An invite adds a verified email as a pending editor or reviewer. It does not paste a string into the `.oma` and call that access. The person accepts before the seven days run out, with the same email. Cancel the invitation, or remove the member, and the next download is refused. Files they already saved remain on their disk. Push project + review export on this document, save the `.oma`, then invite the one email who should see it. ## The thread Part 115 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-sign-in-identity) · [Next](/blog/omadesign-0-5-8-publish-to-showcase) --- # Publish to showcase Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-publish-to-showcase Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, showcase ## The habit Behance, Instagram, a portfolio page, the "share" button in a cloud document. You know the difference between a file your client can comment on and a file the public can scroll past. The dangerous tools collapse the difference. The first save creates a link. The link is unlisted until it is not. A draft with a client's name in a text layer ends up in a gallery because a checkbox was inherited from the last export. The careful habit is a second act. Finish the piece. Export a flat image. Title it. Describe it. Publish that image. The working file, the fonts, the rejected frames, and the review thread where someone said the logo was wrong stay in the project. The public sees the picture you chose. ## The constraint Omadesign already separates the local `.oma`, the push, and the public gallery. A push uploads a versioned source and a review PNG for members. That PNG is for the people on the project. It is not `/showcase`. Publication is a separate owner action. The short claim calls it File → Publish to showcase… and calls it a second, explicit opt-in. The cloud guide names the control Publish selected export, in the desktop or the web workspace. You select a finished flat export, add a title and a description, and publish that. The second opt-in is real. The label on the control is Publish selected export. The public page has to be the flat image. Source files, assets, and private review threads are excluded. A gallery that serves the `.oma` serves the edit history, the notes, and any layer you only hid. Flat means the pixels you already rendered. `/showcase` is the list. `/showcase/:id` is one published project. A private id stays dark. Guessing a project id does not reveal an unpublished file. The gallery's early public works were the 0.5.0 Layout starters. They shipped with the site so the shelf was not empty. A publish you do now is still one finished export, with a title and a description. It is not "upload the starter source," and it is not "upload my `.oma`." Unpublishing has to remove the work from the gallery, from competition displays, and from the image endpoint. Copies a viewer already saved cannot be recalled. That is the limit of unpublish everywhere. Say it before you publish, not after. ## What landed You publish as the owner. Select the finished flat export. Add the title and the description. Choose Publish selected export on the desktop or in the web workspace. The showcase entry is that image plus the words you attached. Members of the project still have whatever access their role already gave them. The public does not gain the source, the asset files, or the private threads because the image went up. `/showcase` lists public works. Open one and `/showcase/:id` shows the complete flat image. Unpublished files are not on the list. A project you have only pushed is a private project. Its id does not render a public page. Archive, from the web workspace, hides the project from collaborators and unpublishes its public work in the same owner action. The owner can restore the archive. Unpublish alone takes the image down from the gallery, the competition displays, and the image endpoint, and leaves the project itself for the members. Competition entry consumes an owned public showcase work. If the image is not public, it is not an entry. Publishing is the step that makes the entry possible. It does not enter the competition for you. That decision stays on `/compete`, and a competition still needs a real brief and dates before it is open. The flat export you select has to already exist as a rendered image. The limits on flat PNG, JPEG, and WebP uploads are 20 MB. If the export is larger than that, it does not sneak into the gallery through the source-file limit of 100 MB. Source and showcase are different pipes. The showcase pipe is the flat one. Viewers of `/showcase/:id` see the image. They do not get a button that downloads your `.oma`, your `.omabrand/` fonts, or the review thread. If someone saves the image from the browser, that copy is theirs. Unpublish removes the endpoint. It does not crawl their disk. The homepage can show a cloud film at `/#cloud`. That film introduces the workspace. It does not open itself on every visit, and it does not publish your file. Publishing remains the owner control. ## In the hand Finish the piece in the `.oma`. Export the flat image you want the public to see. PNG when you need the edges clean. JPEG when you do not. Push the project first if the export has to live with the project as a snapshot. Then select that finished export. Write the title the way you want it read in a list. Write the description the same way. Choose Publish selected export. Open `/showcase`. The work is on the list. Open `/showcase/:id` for that entry. You should see the flat image, complete, not a crop you did not choose and not the layer stack. Confirm the private things stayed private. The `.oma` is not the page. A review comment about the client's legal name is not on the page. An unused asset in the project is not linked from the page. To take it down, unpublish. Reload `/showcase`. The entry is gone. A competition display that was using it drops it. The image endpoint stops serving it. If you archived the whole project, collaborators lose it too, until you restore. Use unpublish when the membership should stay and the public page should not. Use archive when both should stop. If you never publish, the push can still be there for your editor and your reviewer. Check `/showcase` in a private window. The draft is absent. That is the opt-in holding. ## The edge Publish selected export sends one finished flat image, with the title and description you wrote. It does not publish the `.oma`, the project assets, or the private review threads. Those stay with the members who already had access. Unpublish removes the work from the gallery, from competition displays, and from the image endpoint. A copy a viewer already saved is not pulled back. Private ids stay off `/showcase`. They do not become public because someone can spell the id. Select the finished export, then Publish selected export. `/showcase/:id` should show that image and nothing else you meant to keep in the project. ## The thread Part 116 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-enable-sync-invite) · [Next](/blog/omadesign-0-5-8-collaborator-project-view) --- # Collaborator project view Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-collaborator-project-view Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, cloud ## The habit Review in Figma, in a PDF comment, in Frame.io, in the Photoshop comment you leave for a teammate. You click a spot. You type. Someone replies. Someone resolves. The pin stays on the version you were looking at. A later version does not drag your pin onto pixels that have moved, unless the tool is lying to you. Inside the design app you also pin notes on the canvas while you work. Illustrator's notes, Figma's comments on a frame, a sticky you will delete before the client sees the file. Those pins match the thing on the canvas, because you are the person drawing. The client-facing pins match an export, because the client is not in your undo stack. You want both, and you want them named so you do not resolve a local note and think the client saw it. ## The constraint The browser does not author the canvas. Live multi-user editing, presence, and drawing in the browser are outside cloud's scope. Collaborators get a project view. The desktop gets a window onto the same threads. The file you edit remains the local `.oma`, pushed as versions. The short claim says `/project/:id` is the collaborator view: pin, reply, resolve. It also says Layout inspector pins match canvas comments. Both behaviors exist. They are not the same layer of pins. On the canvas, Layout comments are notes you write, pin, and resolve. The inspector shows open counts on the frame. Those pins belong to the document. You see them in the Layout persona, next to the frame, while you edit. They travel in the `.oma` with the rest of the file, including when version 5 stores the layout and the opt-in cloud metadata. In the project view, a pin is a comment on a flat export of a chosen version. You select the export version. You click to pin, or you drag a rectangular annotation, then you post the thread. Coordinates scale with the image, including on a phone. The desktop command is Review annotations…. It loads the same versioned exports and the same threads, with replies and with resolve or reopen. A comment stays on the snapshot it was written on. The next push does not move that thread onto the new pixels. You open the version the note belongs to. `/project/:id` redirects to the authenticated workspace. You are signed in, or you are not looking at the project. The short claim also says that without `OMADESIGN_CLOUD_URL` the desktop keeps a local store at `~/.local/share/omadesign/cloud-store.json`. That path is the desktop fallback when the cloud URL is unset. The public site does not use a file in the browser as a substitute. Production identity is Clerk. Production data and the threads are Convex. There is no browser-local stand-in for those services. The installed app, pointed at nothing, can still keep the local store. The site at `omadesign.app` is the real workspace. ## What landed Open `/project/:id` while signed in. You land in the workspace for that project. Pick an export version. Click a point to pin a comment, or drag a rectangle to annotate a region. Post the thread. Reply on the thread. Resolve it when the note is done. Reopen it if the resolve was early. Who can do what depends on the role accepted from the invite: | Role | Review | | --- | --- | | Owner | Uploads, review, team, archive, publishing | | Editor | Source and assets, uploads, comments, replies, resolve | | Reviewer | Flat exports, pins, rectangular annotations, comments, replies. Resolve their own threads. | A reviewer works on the flat export. They do not pick up the `.oma` as their authoring file through this view. An editor can download source and assets and can upload. Private downloads recheck membership on every request. Remove a member, or revoke a desktop, and the next request fails. Files already downloaded stay downloaded. You cannot un-send a TIFF that hit their disk yesterday. Layout's own pins stay in the inspector. Write a note. Pin it on the canvas. The frame's open count moves. Resolve it when the note was for you. That resolution is the document's. It is not a thread on `/project/:id` unless you also posted it on an export. The short claim lines the inspector up with the canvas, and that match holds: the pins you see in the Layout inspector are the canvas comments. Cloud threads line up with the export version in Review annotations… and in the browser. Use the window that matches the audience. Coordinates on the web pin scale with the image. A pin you drop on a desktop monitor is the same relative point on a phone. You do not get a second coordinate system per device. Rectangular annotations use the same scaling. The region you dragged is the region they see. The local store path matters when you are developing or running the desktop with no `OMADESIGN_CLOUD_URL`. The app keeps `~/.local/share/omadesign/cloud-store.json`. That is a desktop file under the local share directory. It is not the production gallery, and it is not a sync of every `.oma` on the machine. Production review for a published team goes through the workspace. Set the cloud URL when this desktop should talk to that workspace. Leave it unset when you mean the local store. ## In the hand Push a version that includes the flat export you want reviewed. Send the project to the people who accepted their invites. As the reviewer, sign in with the invited email. Open `/project/:id`. Select the export version in the web project. Click the headline that is wrong. Write the note. Or drag a rectangle around the nav that is wrong. Post. The owner or an editor replies. You resolve your own thread if the role is reviewer and the note was yours. An editor can resolve threads. The owner can too. On the desktop, open Review annotations…. The same versioned exports and the same threads load. Reply there if you are in the app. Resolve or reopen there. Then go back to the canvas. Your Layout pins are still the pins on the frame. The client's pin is on the snapshot. Push a new version after you fix the headline. The old thread remains on the old snapshot. The new export is a new place to pin. Tell the reviewer which version you mean. If you are testing on a machine with no `OMADESIGN_CLOUD_URL`, look at `~/.local/share/omadesign/cloud-store.json` only as the desktop's local store. Do not expect `omadesign.app` to read that file. Sign in and use `/project/:id` when the thread has to be the one the collaborator sees. Check the inspector in Layout if the note was never meant to leave the building. Open counts on the frame tell you what is still open in the document. They do not tell you what the reviewer resolved on last Tuesday's PNG. ## The edge A cloud comment stays on the snapshot it was written on. Editing the canvas, or pushing a newer export, does not move that pin onto the new pixels. Reply and resolve on the version that has the thread. Pin again on the new export if the note still matters. Reviewers see flat exports and threads. They do not receive the source through that role. Revoking them blocks the next download. It does not delete files they already have. Without `OMADESIGN_CLOUD_URL`, the desktop store is `~/.local/share/omadesign/cloud-store.json`. The public project view is still `/project/:id`, signed in, on the workspace. Open `/project/:id`, select the export version, and pin the note on that image. ## The thread Part 117 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-publish-to-showcase) · [Next](/blog/omadesign-0-5-8-compete-waitlist) --- # Compete waitlist Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-compete-waitlist Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, cloud ## The habit Design competitions usually open with a form. You read the brief, you upload a board, you get a confirmation, you wait. Sometimes the form is a waitlist because the brief is not ready, and the site still wants your email. You have also seen the other kind, where entering is a side effect of posting to a gallery you thought was a portfolio. The work is in the show before you decided it was an entry. Adobe's community sites and the various poster annuals train the same reflex. Read the dates. Submit the public piece. Keep the working file at home. Withdraw if the piece is wrong, before the close, using a control that is actually on the page. A waitlist you joined in May should not silently become a submission in September. ## The constraint The short claim says `omadesign.app/compete` is rules plus a waitlist, and that entries are not required on day one. It points at `/account` for sign-in identity and `/api/cloud` for sync, publish, and the waitlist, and it calls the API last-write-wins. The cloud guide is the current behavior, and where the short claim is older, the guide wins. `/compete` lists competitions, the public entries, and your withdrawal controls. You are not required to enter because you installed the app, signed in, or pushed a project. A competition stays unpublished until an operator supplies a real brief and real opening and closing dates. There is no public administrator endpoint for that configuration. Until those dates exist, there is no open contest hiding behind the page. An entry is an owned work that is already on the public showcase. You publish a flat export first. Then you enter that public work. Duplicate submissions are rejected. The server enforces the closing date. Withdrawal is a control on `/compete`, not a mail to an anonymous inbox. The older waitlist records are still retained. They live in Convex as `waitlistSignups`, indexed by list and normalized email. Cloud and competition audiences stay separate. There is no public signup-list or lookup endpoint. That older signup never sent email. Member invitations are a different system, through Resend, and they expire in seven days. An authenticated operator can review or export retained waitlist records, or remove an address. The removal function is internal. The public API will not do it for a caller. `/account` is the sign-in identity: the name and email, and the place you revoke a desktop credential. `/api/cloud` is the desktop transport. The guide's rule for documents is versioned pushes. Every push adds files. A local edit does not overwrite another designer's version. Comments stay on the snapshot they were written on. The older "last write wins" sentence is not the rule to follow for a project. There is no CRDT merging two live canvases, and there is no live shared canvas in this scope. Pushes add. They do not blend. The 0.5.4 site work replaced the outdated cloud-waitlist pitch with these workflows. A waitlist record from earlier is history. It is not the front door of `/compete` now. ## What landed Open `https://omadesign.app/compete`. You get the competition list, the public entries for competitions that are actually up, and the controls to withdraw an entry that is yours. If the operator has not published a brief with opening and closing dates, you are not late. The contest is not open. The page is not collecting a substitute entry through a side form. To enter, when one is open, you use a showcase work you own. That means Publish selected export already happened, the image is on `/showcase/:id`, and the source file was not what you submitted. The entry stores that public work. A second submission of the same work is rejected. After the closing date, the server refuses the entry. You do not get in because your clock was behind. Withdrawal is on `/compete`. Use it when the public image should stop being an entry. Unpublishing the showcase work also removes it from competition displays and from the image endpoint. Those are related and not identical. Withdraw addresses the entry. Unpublish addresses the public image. Read the page you are on before you assume one click did both. `/account` remains the identity page. Match this email to the email you use in the desktop sign-in. A competition entry is not a second account. `/api/cloud` is how the desktop talks to sync, publish, and the related cloud calls. Your script, if you are not the desktop app, is not invited to invent a private waitlist lookup. There is no public lookup endpoint. Signup, the historical one, did not send mail. If you are waiting for a confirmation message from an old waitlist, it was never going to arrive. Invitations you receive now are project invites, explicit, from Resend, to a verified email. Operators configure a competition internally with a title, a description, opening and closing timestamps, and an active flag. That is not a screen in the drawing app. You will see the contest on `/compete` when it is published. You will not see a draft brief the operator has not turned on. Day one of using Omadesign does not include an entry. The binary, the `.oma`, the local save, and even a private push are all usable with `/compete` left untouched. Enter when you have a public piece and an open brief. Skip it for the life of the install if that is what you want. The app does not gate the canvas on a contest. ## In the hand Sign in if you need the account. Look at `/account` and confirm the email. Open `/compete` in the same browser. Read what is actually listed. If the list is rules and no open contest, stop. There is nothing to submit. You are not failing a required step. When a contest is open, publish the flat export first. Confirm `/showcase/:id` shows the image you want judged, and does not show the `.oma`. Then enter from the owned public work. If the server rejects a duplicate, you already entered that work. If it rejects for the date, the contest is closed. Do not republish under a second id to sneak the same piece past a duplicate check you have not read. The rejection is the rule. To pull an entry, use the withdrawal control on `/compete`. Then check the showcase if the image itself should also leave the public gallery. Unpublish when that is the goal. Archive the project when collaborators should lose it too. The local `.oma` stays on your disk through all of these. None of them rewrite the camera file, the brand folder, or the document you have not pushed. Leave `OMADESIGN_CLOUD_URL` and the desktop out of this unless you are publishing from the app. Publish selected export can be done on the desktop or the web. The entry still requires the public showcase work. A file that exists only in `~/.local/share/omadesign/cloud-store.json` is not an entry. ## The edge You cannot enter with a private file. The entry is an owned public showcase work. Duplicate submissions are rejected. The server enforces the closing date. A competition with no real brief and no dates stays unpublished. An old waitlist signup is not an entry and never sent you mail. There is no public list to query. You are not required to enter on the day you install. Open `/compete`, read the dates, and enter only the showcase image you already meant to show. ## The thread Part 118 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-collaborator-project-view) · [Next](/blog/omadesign-0-5-8-lua-5-4-embedded) --- # Lua 5.4 embedded Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-lua-5-4-embedded Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, plugins ## The habit Scripts in Illustrator are ExtendScript, and the ones that still run are often older than the people running them. Photoshop has its own scripting dictionary and a plugin folder Adobe can move out from under you. Affinity's story has been panels and personas more than a language you ship inside the binary. On Linux the usual answer is worse. You install Lua from the distro, or Python, or a flatpak that cannot see the app, and then you write glue. The script works on your machine. It fails on a teammate's machine because their Lua is 5.3, or because `lua` is not on `PATH`, or because the package manager upgraded the interpreter and a C module stopped loading. You want the language in the same binary as the studio. You want an update of the studio to leave the scripts you already trusted where they are. ## The constraint 0.5.8 is one binary on Linux, ARM64 and x86_64, with a glibc ceiling of 2.35. Lua has to be inside that package. A second install step for a runtime would mean the plugin menu depends on a distro package the installer promised not to require. The release notes are plain. Lua 5.4.9 is embedded. Plugin API 1 is the host API. Lua and the examples ship inside the Linux packages. The packages bundle Lua the way they bundle the RAW decoder. You do not apt-get an interpreter before Plugins means something. The API is version 1 so a plugin can say what it was written for. `ctx.api` reports that version when an action runs. The plugin's own `version` field is the plugin's version, not the app's. Those two numbers are allowed to move on different days. API 1 is the contract. 0.5.8 is the app that ships that contract. An update that replaces `~/.local/share/omadesign/plugins` with the stock examples would wipe the edit you made to a starter, or a plugin you installed from a folder. The release check is explicit. Reinstallation preserves a deliberately customized installed plugin. Existing installed plugin modifications survive updates. Fresh source for the starter is available beside that, at `~/.local/share/omadesign/plugin-examples/studio-starter`, and as a downloadable `.omaplug`. The installed copy is yours. The example tree is the reference. The sandbox is part of embedding the language. A Lua that can `os.execute`, open the network, or read `~/.ssh` is not a plugin system you can leave enabled. The embedded Lua has table, string, math, utf8, and the basic functions. `io`, `os`, `package`, `debug`, `require`, file loaders, binary chunks, `pcall`, `xpcall`, and coroutines are unavailable. Plugins cannot execute programs, touch the network, or read arbitrary files. `oma.read_asset` reads a UTF-8 file inside the plugin's own folder. That is the disk they get. Each run gets a fresh Lua VM. Globals do not survive until the next click. Persistent artwork belongs in the document. Persistent presets belong in the plugin. `oma` exists when an action runs. It does not exist during manifest discovery, so a plugin cannot do work while the manager is only listing names. ## What landed Open the app from the 0.5.8 package. Plugins is there. You did not install Lua. The About version is 0.5.8. The same packages that report 0.5.8 on ARM64 and on x86_64 include the interpreter. Validation ran the x86_64 build under QEMU against Ubuntu 22.04's glibc 2.35, and ran ARM64 on the machine. Both packages inspect and render native documents, run a live-text transform, run a custom pixel filter that preserves alpha, and run a two-file batch. Those runs are the embedded host, not a system `lua` binary found on `PATH`. API 1 is what a manifest declares: ```lua return { api = 1, id = "org.example.color-dots", name = "Color dots", version = "1.0.0", description = "A small editable pattern generator.", actions = { -- ... }, } ``` The id is stable: letters, digits, dots, hyphens, underscores, no leading dot. Action ids are unique inside the plugin. The host checks parameters for desktop runs and for CLI runs. Unknown parameters are rejected. A plugin written to API 1 is the kind this build loads. You do not point the app at `/usr/bin/lua`. Studio starter installs on first installation. Its twelve actions cover the categories the API allows: filters, effects, icons, brushes, tools, behaviors, batch, patterns, gradients, swatches. Later updates do not replace your installed starter with a fresh one and throw away your edits. If you want the pristine source, it is in `~/.local/share/omadesign/plugin-examples/studio-starter`. The installed starter the batch examples call is `~/.local/share/omadesign/plugins/org.omadesign.studio-starter`. Those are two directories on purpose. There is no remote marketplace in 0.5.8. A plugin arrives as a file you already have, or as the starter the package installed. The manager installs it. The embed is what runs it. Limits sit on the host, not in a cloud policy: 15 seconds per run, 64 MiB of Lua heap, 20,000 document edits, 256 MiB of queued edit data, 128 MiB of raster input, 2 MiB of source, 4 MiB per asset or geometry, 128 actions, 24 parameters per action. Installed bundles allow 512 entries and 64 MiB total. You feel those limits locally, in one process, with one undo if the action was a document edit that finished. Plugins run on a background worker. The UI thread keeps the canvas. Cancellation, errors, and a stale result after you edit or switch documents do not apply a partial change. That safety is the host around the embedded VM. The VM being inside the process is why the host can throw the result away before it touches the `.oma`. An external interpreter with its own file access could not make the same promise. Offline plugin docs ship in the package, with the Lua and Phosphor licenses. The public guide is the same API. You can read it on the site or from the installed docs. The behavior does not depend on the site being up. The interpreter is already on disk. ## In the hand Install 0.5.8 the way you already install Omadesign, from the public installer, into the home directory. Do not look for a Lua package. Open the app. Open Plugins → Manage plugins. Studio starter is there if this is the first install. Enable it if you want its actions. Run one. A finished document action is one Ctrl+Z. Update the app later. Open Manage plugins again. The starter you customized is still the installed copy. Open `~/.local/share/omadesign/plugin-examples/studio-starter` if you need to compare with the copy the package considers fresh. Your edits are not in that examples folder unless you put them there. Drop a new plugin in only through the manager, as a `.lua`, a folder with `main.lua`, or an `.omaplug`. The embed runs it under API 1. It does not shell out. If a plugin tries to reach the network or read a path outside its folder, it cannot. The run ends in an error, and the document stays as it was. For a headless check of the same host: ```sh omadesign --list-plugins ``` You are talking to the embedded runtime. There is no `lua` version to mismatch. ## The edge 0.5.8 does not ask you to install a Lua runtime beside the app. Lua 5.4.9 and API 1 are in the Linux packages. A plugin cannot execute a program, open the network, or read an arbitrary file. The language is inside the sandbox, in-process, on a background worker. An update does not wipe installed plugins. The modifications you already have survive. Fresh starter source stays in the examples directory for you to read. The installed folder stays the one the app runs. Open Plugins → Manage plugins on the 0.5.8 build. The interpreter is already there. ## The thread Part 119 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-compete-waitlist) · [Next](/blog/omadesign-0-5-8-manage-plugins) --- # Manage plugins Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-manage-plugins Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, plugins ## The habit You install a Photoshop panel by dropping a folder in a Plug-ins directory, then you restart, then you hunt a menu. Illustrator scripts go in Presets/Scripts and show up after a restart. Affinity, when it takes an add-on, has had its own install dance. The failure you remember is an update that replaces the folder you had edited, with no copy of yesterday's version, and a manager that keeps running the code it loaded at launch while you stare at a file you just saved. You want one window. Install. Enable. Run. Reload after an edit. A backup of the previous folder when an update lands. No store you have to be signed into before a local `.lua` file is allowed to exist. ## The constraint 0.5.8 embeds Lua 5.4.9 and plugin API 1 in the binary. The manager is the door. There is no remote marketplace in this release, so the manager cannot be a storefront. It installs files you already have. Three shapes cover what people actually ship: one `.lua` file, a folder whose entry is `main.lua`, or an `.omaplug` ZIP with `main.lua` at the root of the archive. A ZIP of the parent directory, with `main.lua` nested one level down, is the wrong shape. The root of the archive is the plugin root. Enable is per plugin. A checkbox, not a restart of the whole app to unload one script. Behaviors, which listen for document and selection events, only see enabled plugins, and they stay off until their own checkbox opts in. The enable switch is the first gate. Editing a file on disk must not hot-swap the VM underneath a running canvas with no signal. Reload is the signal. You edit. You Reload. The next run reads the installed copy you just changed. Until Reload, the manager is still on the copy it loaded. Updates of an installed plugin keep a hidden backup of the previous folder. The new folder becomes the one that runs. The previous folder remains on disk, hidden, so an update is not a delete. App updates are a different preservation rule: reinstalling Omadesign keeps the installed plugins you already customized. The hidden backup is the plugin-folder update inside the manager. Both exist so an update has somewhere to go back to. Archives have to be boring. Path traversal and symlinks are rejected. Plugin file access stays inside the installed bundle. A ZIP that tries to write `../../.ssh` does not get a partial install outside the plugin directory. Bundles allow 512 entries and 64 MiB total. Those limits are the manager's refusal, enforced before a plugin action ever runs. ## What landed Open Plugins → Manage plugins. Install from a `.lua` file, from a folder that contains `main.lua`, or from an `.omaplug` with `main.lua` at its root. Each installed plugin has an Enable checkbox. Turn it on to use its actions. Turn it off to keep it installed and out of the way. The same window is where you run. Select an action, enter its parameters, choose Run. Categories the manager already knows are Filters, Effects, Icons, Brushes, Tools, Behaviors, Batch, Patterns, Gradients, and Swatches. Another category name is allowed. It shows up under All categories. Studio starter, installed on first launch, has twelve actions across those categories. You manage it here like any other plugin. After you edit an installed plugin, use Reload. The manager reads the folder again. A save in your editor is not Reload. The next Run uses the reloaded manifest. Top-level Lua should return the manifest and define functions. `oma` is available when the action runs, not while the manager is discovering manifests, so Reload can list a plugin without executing its `run`. Parameter forms are part of the manager. `number` is the default kind. `text`, `color`, and `boolean` are the others. Each needs an `id`, a `label`, and a `default` of the right type. Numbers may set `min` and `max`. Colors are `#RRGGBB` or `#RRGGBBAA`. The host validates values. Unknown parameters are rejected before the action proceeds. You see the fields in the manager. You do not hand-edit a side config the manager will ignore. Headless install uses the same installer without the window: ```sh omadesign --install-plugin ./my-plugin omadesign --list-plugins ``` `--list-plugins` confirms the id from a script. Installing and listing are the manager's job in a terminal. The installed starter lives at `~/.local/share/omadesign/plugins/org.omadesign.studio-starter`. Fresh example source, the kind an app update does not force over your edits, is at `~/.local/share/omadesign/plugin-examples/studio-starter`. To hand someone a plugin, ZIP the contents of the folder. `main.lua` at the root of the ZIP. Assets, README, and license inside. Name it `your-plugin.omaplug`. They install that file in the manager. A downloadable starter bundle is shipped the same way, `studio-starter-1.0.0.omaplug`. Ids have to be stable and unique. Letters, digits, dots, hyphens, underscores. No leading dot. Action ids are unique inside the plugin. The version field is yours. It is not required to match 0.5.8. API 1 is required to match the host. The hidden backup happens when the installed folder is updated. The previous folder is kept, hidden, on this machine. Combined with Reload, the loop is install, enable, run, edit, reload, run. Update when you have a new bundle. ## In the hand Write `main.lua` that returns a manifest with `api = 1` and one action. Put it in a folder. 1. Open Plugins → Manage plugins. 2. Install that folder. The plugin appears in the list. 3. Turn on its Enable checkbox. 4. Select the action. Fill the parameters the manifest declared. Run. 5. Edit `main.lua` in the installed folder. Come back to the manager. Click Reload. 6. Run again. The new code is the code that runs. Package it when someone else needs it: ```sh # from inside the plugin folder, so main.lua is at the archive root zip -r ../your-plugin.omaplug main.lua ``` Install `your-plugin.omaplug` on the other machine through Manage plugins. Enable it there. If their app is 0.5.8, API 1 is the host they have. They do not install Lua. Update that plugin by installing the newer bundle over it. Then look for the hidden backup of the previous folder. The manager kept it. Reload if you were mid-edit. Run the action. If the new one is wrong, the previous folder is still on disk because the update kept the backup. Check a script install with `omadesign --install-plugin ./my-plugin` and `omadesign --list-plugins`. The window and the terminal install the same way. Enable in the manager when you want the actions in the UI. If the ZIP is rejected, open it and look at the root. `main.lua` has to be there, not inside an extra directory. Symlinks and `..` paths are rejected on purpose. Rebuild the archive from the files, without links that point outside the folder. ## The edge Reload is required after you edit an installed plugin. Saving the file does not swap the running copy. The manager keeps the loaded plugin until you Reload. An update keeps a hidden backup of the previous installed folder. The update does not destroy the only copy of the plugin you had. Path traversal and symlinks in an archive are rejected. The install does not write a partial plugin outside the bundle. File access for the plugin stays inside that bundle. There is no marketplace tab waiting on a network. Plugins → Manage plugins installs the `.lua`, the folder, or the `.omaplug` you already have. Open Plugins → Manage plugins, install the bundle, enable it, and Reload after every edit. ## The thread Part 120 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-lua-5-4-embedded) · [Next](/blog/omadesign-0-5-8-studio-starter-twelve) --- # Studio starter twelve Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-studio-starter-twelve Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, plugins ## The habit You already keep a starter kit. In Illustrator it is an Actions set, a swatch library, and a folder of SVG icons you have to find again on the new machine. In Photoshop it is a recorded filter and a brush you exported so you could load it on the other desk. In Affinity it is a macro plus a brush pack. The first hour is reassembly. You want one shadow, one pixel treatment, one icon, two brushes, a way to draw a path from a drag, a pattern, a gradient, a palette with a name, a nudge you can aim at a folder of files, and two quiet reports that tell you what you selected and how big the document is. You want to see that kit run before you sit down to write your own. ## The constraint Omadesign 0.5.8 is one native Linux binary. The picture is one `.oma`. A finished document edit is one Undo step. Lua 5.4.9 and plugin API 1 are built into that binary. There is no second installer for a scripting runtime, and there is no panel that fetches a starter pack off the network before you can press Run. That forces the shape. The twelve actions ship inside the Linux package. The first installation copies them into the live plugin folder. Later updates of the app leave that folder alone if you have already got it, including a copy you edited. A clean tree is written beside it so you can read the source without touching the copy that actually runs. Behaviors stay out of the event stream until a checkbox you can see is on. A brush preset and a palette write settings, and those settings are not pretending to be a document undo step. The document actions are. ## What landed **Plugins → Manage plugins** lists **Studio starter**, id `org.omadesign.studio-starter`, version 1.0.0. The live copy after the first install is `~/.local/share/omadesign/plugins/org.omadesign.studio-starter`. The clean source the package keeps for you is `~/.local/share/omadesign/plugin-examples/studio-starter`. The same tree is what you download as `studio-starter-1.0.0.omaplug` if you want to install it by hand. The window is titled **Lua plugins**. Each plugin has an enable checkbox. Actions show as `Category · Name`. The category list is Filters, Effects, Icons, Brushes, Tools, Behaviors, Batch, Patterns, Gradients, Swatches. The twelve actions are one of each job the tweet names, with the two brushes and the two behaviors making the count. **Midnight duotone** is the pixel filter. It reads the active raster layer, mixes each pixel toward a dark navy and a pale ink by luminance, and returns the original alpha. Strength runs from 0 to 1 and starts at 1. **Soft offset shadow** writes a native drop shadow on the selected vector objects. Blur starts at 12. Offset starts at 8. The shadow color is `#11182780`. The effect stays in the native effect stack, so the FX controls can still edit it. **Orbit icon** reads `orbit.svg` from the plugin folder and places it centered, at a size that starts at 160. The paths come in as editable vectors. **Soft ink brush** and **Dry marker brush** call the native Raster brush. Soft ink starts at size 36, hardness 0.25, flow 0.28, opacity 0.85, spacing 0.08, color `#BAC2DE`. Dry marker is fixed: size 18, hardness 0.95, flow 0.55, opacity 0.9, spacing 0.32, color `#A6E3A1`. Running either one switches the studio to Pixel and selects the Brush tool. That change is a preset, not a document edit. **Ribbon path** is the canvas tool. Stroke width starts at 14. Color starts at `#A6E3A1`. You activate it, drag, and the gesture draws a preview line. On release it adds an open path named Ribbon, fill none, with that stroke. **Dot field** builds a grid of 10 pixel ellipses, centered on the canvas. Columns start at 8, rows at 6, spacing at 32, fill `#A6E3A1`. Rows and columns are capped at 40. Every circle is a normal shape. **Aurora gradient** paints the selection with a three-stop linear gradient, `#89B4FA`, `#CBA6F7`, `#A6E3A1`. The native gradient editor still owns the stops afterward. **Night studio swatches** adds a personal palette named Night studio: `#11111B`, `#1E1E2E`, `#BAC2DE`, `#89B4FA`, `#A6E3A1`, `#F38BA8`. It saves into the personal color library. That save is separate from the document's Undo stack. **Translate selection** moves the selected vectors. Horizontal starts at 16, vertical at 0. Its command id is `nudge`, which is the name the batch command line uses. **Selection count** listens for `selection_changed` and puts a count in the status bar. **Document dimensions** listens for `document_opened` and reports the document name and pixel size. Both sit in Behaviors. Both stay quiet until you opt in. ## In the hand Install the app. Open a document. Choose **Plugins → Manage plugins**. Studio starter is already in the list if this was a first install and the enable box is on. For the duotone, switch to a Raster document, select the raster layer, choose **Filters · Midnight duotone**, leave Strength at 1, and press **Run**. The status line reports the plugin message or **Plugin completed · Undo restores document edits**. Alpha stays. For the shadow, select the vector shapes, choose **Effects · Soft offset shadow**, and press **Run**. `Ctrl+Z` takes the whole effect off in one step. For the icon, choose **Icons · Orbit icon** and press **Run**. The SVG lands centered at 160 pixels unless you typed another size. For a brush, choose **Brushes · Soft ink brush** or **Dry marker brush** and press **Run**. The persona is Pixel. The Brush tool is up. Paint on a raster layer. For the ribbon, choose **Tools · Ribbon path** and press **Activate tool**. Drag. A line follows the pointer. Release. A stroked path named Ribbon is in the layer. Press Escape, or **Plugins → Exit plugin tool**, if you armed the tool and do not want the stroke. Fewer than two points does not invent a path. For the pattern, choose **Patterns · Dot field** and press **Run**. The circles are ordinary ellipses. Select one and move it. For the gradient, select a shape, choose **Gradients · Aurora gradient**, and press **Run**. Press `G` afterward if you want the Gradient tool on those stops. For the palette, choose **Swatches · Night studio swatches** and press **Run**. Open the Palettes tab. Night studio is in Personal. Saving the `.oma` is a different button from saving that library. For the nudge, select vectors, choose **Batch · Translate selection**, and press **Run**. They move 16 pixels on x unless you changed Horizontal. The command id is `nudge`. Leave the behavior checkbox alone until you want the status line talking. Its label is **Run enabled plugins’ document and selection behaviors**. The choice is written to `behaviors.json` in the plugin folder and read back next launch. ## The edge The folder under `plugin-examples` is the clean source. It is not the copy the manager runs. Editing that tree does nothing to the live plugin until you install the folder or the `.omaplug`. An app update does not replace `plugins/org.omadesign.studio-starter` when that folder is already there, so a local edit survives. Reinstalling through the manager renames the previous folder to a hidden `.backup-…` name and puts it back if the new copy fails to land. Behaviors do not run because the plugin is enabled. The manager checkbox is a second switch, and it is off until you turn it on. Open **Plugins → Manage plugins**, choose **Filters · Midnight duotone**, and press **Run**. ## The thread Part 121 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-manage-plugins) · [Next](/blog/omadesign-0-5-8-plugin-undo-safety) --- # Plugin undo safety Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-plugin-undo-safety Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, undo ## The habit You have run a script that died on the third object. In Illustrator the first two objects are already different, the action panel says it failed, and Undo walks backward one object at a time if you are lucky. In Photoshop a filter that you cancel after it has started sometimes puts a half-rendered layer in the history and sometimes does not, and you learn which one by looking. In Affinity a macro that errors mid-way leaves you hunting the history panel for the step that still matches the file you meant to keep. The other failure is quieter. You start a long action, click another document because you thought it had finished, and the result lands in the wrong tab. Or you nudge a selection while the script is still reading the old one, and the write comes back against a picture that has already moved. You wanted one step on the document you aimed at, or no step at all. ## The constraint Omadesign keeps one `.oma` open in a tab, and Undo is one command. A plugin that pushed twenty separate history entries would make `Ctrl+Z` mean the plugin's third ellipse. You want `Ctrl+Z` to mean the plugin. A plugin that wrote those ellipses as it went would have nothing honest to do when Lua raised an error on ellipse twelve. The canvas also cannot freeze for the whole run, because a 15 second pixel pass on a large layer is a long time to lock a window, and you need a Cancel that means something. So the plugin runs on a background worker. It receives a snapshot of the document, the selection, and the active layer. It builds a list of edits. The studio applies that list only after the run finishes, only if you did not cancel, and only if the document and the selection are still the ones it started from. The apply is a single batch. One Undo. One Redo. If any of the guards fail, the batch is thrown away and the picture stays where your hand left it. ## What landed In 0.5.8 the manager shows **Running plugin…** with a spinner and a **Cancel run** button while the worker is busy. A second Run during that time reports **A plugin is already running**. The worker gets a fresh Lua VM for that invocation. Globals from the previous run are gone. The host API is present for the action, not while the manifest is being read. When the worker returns, the studio checks four things before it touches the document. The run was not cancelled. The result still belongs to this tab. You are not in the middle of a drag or another operation. The selection and a fingerprint of the document still match the snapshot the plugin started with. Pass all four and the edits commit as one `Cmd` batch. The status line says **Plugin completed · Undo restores document edits**, or it says the text the plugin passed to `oma.message`. Fail any of the four and the status becomes an error: **Plugin result discarded because the document or selection changed, or the run was cancelled**. The document is the document you were looking at. Partial ellipses do not appear. A pixel filter does not leave a stripe of new pixels over old ones. `Ctrl+Z` after a successful document action reverses the whole batch. `Ctrl+Shift+Z` puts it back. Redo is also `Ctrl+Y`. That is the same undo machinery as a Pathfinder operation or a placed image. The plugin does not get a private history. Two results are settings, and the manual is explicit that they sit outside document Undo. `oma.brush` switches you to Pixel, loads the preset, and selects the Brush tool. `oma.palette` merges colors into the personal library and saves that library. Undoing the document does not unload the brush and does not remove the palette. Those are the same kind of change as picking a brush size with `[` and `]` or saving swatches from the Palettes tab. Errors inside Lua abort the run. `assert` and `error` are how a plugin refuses to continue. There is no `pcall` to swallow that and keep writing. Cancellation is the **Cancel run** button, which sets a flag the worker watches. A plugin that hits the 15 second limit, the 64 MiB Lua heap, or the 20,000 edit cap fails the same way: no batch applied. Hidden objects, locked objects, and guides are not editable from the host API. A plugin that tries to rewrite them does not get a quiet partial success. ## In the hand Open a poster. Select three rectangles. **Plugins → Manage plugins**, **Effects · Soft offset shadow**, **Run**. Wait for the status line. The three shadows appear together. Press `Ctrl+Z`. All three shadows leave together. Press `Ctrl+Shift+Z`. They return together. Run it again. While the spinner is up, click another document tab or move the selection. When the worker returns, the error line tells you the result was discarded. Look at the rectangles. No shadow. Run **Patterns · Dot field**. While it runs, press **Cancel run**. The grid does not appear. The history does not grow. Run **Filters · Midnight duotone** on a raster layer. When it completes, `Ctrl+Z` restores every pixel of that pass, alpha included, because the mapping was one edit. If the filter errors, the layer is the layer from before Run. Run **Swatches · Night studio swatches**. The palette shows up under Personal. Press `Ctrl+Z` on the artboard. The artwork steps backward. The palette stays, because it was saved to the personal library, not appended to the document history. Remove it from the Palettes tab if you do not want it. Switch documents in the middle of **Batch · Translate selection**. The nudge does not land on the tab you switched to, and it does not land on the tab you left if that tab's fingerprint moved. You get the discard message. ## The edge The plugin does not apply a prefix of its edits. Success writes the whole batch or the worker's failure writes nothing to the `.oma`. Editing the document, changing the selection, switching tabs, or cancelling throws the result away even if Lua already finished the math. A brush preset and a palette save are the exception the manual names: they change studio settings, and document Undo does not pretend to own them. The next time a plugin finishes, press `Ctrl+Z` once. The whole action leaves. ## The thread Part 122 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-studio-starter-twelve) · [Next](/blog/omadesign-0-5-8-plugin-categories) --- # Plugin categories Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-plugin-categories Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, plugins ## The habit Photoshop puts filters in one menu, actions in another, brushes in a panel, and swatches in a third. Illustrator splits effects, graphic styles, symbols, and scripts. Affinity splits macros, assets, and brushes. You learn the menu by the damage it does. A filter burns pixels. An effect stays live. A symbol is a master. A brush is a preset you paint with after the menu closes. When a plugin system collapses all of that into "run script," you stop trusting the menu, because you cannot tell whether Run will stamp geometry, bake pixels, or start listening to every selection change for the rest of the day. You also learn the precondition the hard way. A drop shadow on an empty selection does nothing or errors. A pixel filter on a vector layer does something you did not mean. A brush preset fired with no raster layer selected becomes a support ticket. The category should tell you the precondition before you press the button. ## The constraint One binary has one plugin manager. It cannot grow a private panel per category without turning Plugins into a second application. The document is still one `.oma`, so a category is not a file format. It is a label on an action, plus the host call that action is allowed to make, plus the studio state that call requires. API 1 keeps the label honest by keeping the calls narrow. A filter maps pixels on a layer index. An effect replaces a shape's native effect stack. An icon imports SVG paths. A brush sets the Raster preset and stops there. A tool consumes a gesture. A behavior names an event and stays off until the manager's behavior checkbox is on. A batch command edits documents and is the one you can point at a folder from the shell. A pattern adds shapes. A gradient builds a native multistop fill. A swatch writes the personal palette library. If the action needs a different kind of power, it does not get it by picking a friendlier category name. ## What landed The manager's category menu is fixed: **Filters, Effects, Icons, Brushes, Tools, Behaviors, Batch, Patterns, Gradients, Swatches**. An action's `category` string picks one of those. Any other string is still legal. It shows up when the menu is on **All categories**, and it does not get a private slot in the dropdown. The button in the list reads `Category · Name`. The search box hint is **Find a plugin or action…**. Studio starter is the map, one action per job. Filters run `oma.map_pixels` or `oma.pixel` on a raster layer. Midnight duotone is the example. You need a Raster document and a raster layer selected. The callback sees straight RGBA, 0 to 255, and it has to return alpha if you want the transparency kept. `oma.pixel` reads the source image for the whole pass, so a neighborhood filter does not feedback on pixels it already wrote. The pass is one Undo. Effects call `oma.set_effects` on selected vector objects. Soft offset shadow replaces that shape's stack with a native Shadow. The stacks the host understands are the same ones the FX studio writes: blur, shadow, inner shadow, offset, morphology, saturate, brightness, contrast, invert, hue rotate, color matrix, turbulence, displacement. Up to 32 effects. Blur stops at 512. Morphology radius stops at 64. Offsets and displacement stop at 4096. Turbulence stops at 8 octaves and a base frequency of 1. Hidden, locked, and guide shapes are not targets. Icons call `oma.svg` after `oma.read_asset`. Orbit icon is the example. Paths and presentation attributes come through. A local `url(#gradient)` fragment is allowed. External references, stylesheets, and CSS escapes are rejected. Icon fonts need to be outlined paths. The result is editable vector artwork, aspect preserved, one Undo. Brushes call `oma.brush`. Size is 1 to 2048. Hardness, opacity, and flow are 0 to 1. Spacing is 0.05 to 4. Color is hex. The studio switches to Pixel and the Brush tool. You still need a raster layer under the brush when you paint. The preset is not a document undo step. Tools set `tool = true` and read `ctx.gesture`. Ribbon path is that action. Patterns call `oma.add_shape`. Gradients call `oma.gradient` with `linear`, `radial`, `conic`, or `shape`, then `oma.set_fill`. Swatches call `oma.palette` and the colors persist as an ordinary personal palette. Batch actions are ordinary document edits with a command id. Translate selection's id is `nudge`. Behaviors set `event` to `selection_changed` or `document_opened` and do not run from the Run button's habit. They run when the behavior checkbox is on. `oma.add_shape` accepts `rect`, `ellipse`, `line`, `path`, and `geometry`. `oma.translate`, `oma.set_geometry`, and `oma.remove` address a shape by layer and id. `set_geometry` can write an editable compound path. `oma.message` is a status line, not a document edit. ## In the hand Open **Plugins → Manage plugins**. Leave the dropdown on **All categories** and scroll Studio starter. You should see ten rows if you count the two brushes and the two behaviors inside the twelve, each prefixed with its category. Switch the dropdown to **Filters**. Only Midnight duotone remains. Switch to **Brushes**. Soft ink and Dry marker remain. Switch to **Behaviors**. Selection count and Document dimensions remain. The enable checkbox on the plugin is above those rows. A disabled plugin does not offer its buttons. Pick the row that matches the artwork you have. Vector objects selected, same layer, neither hidden nor locked: Effects, Gradients, or Batch. A raster layer active in a Raster document: Filters, then paint after Brushes. Nothing selected and you want new artwork: Icons or Patterns. A drag you have not made yet: Tools, then **Activate tool**. A folder of files later: Batch, from the shell, with `--command` set to the action id. Parameters sit on the action before Run. Numbers have min and max. Colors are `#RRGGBB` or `#RRGGBBAA`. Booleans and text are the other kinds. The host checks them for the desktop and for the command line. An unknown parameter is rejected. You do not discover a typo by watching half the document change. ## The edge The category string does not grant a power the action forgot to call, and it does not relax a precondition. A filter without a raster layer fails the assert. An effect with an empty selection fails the assert. A brush does not become a document command because you filed it under Batch. The command line says so in one line: brush and palette actions run in the desktop Plugins menu, and batch commands have to edit documents. Canvas tools need a gesture. The shell has no gesture to give them. Set the category dropdown, select the artwork that category expects, and press **Run**. ## The thread Part 123 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-plugin-undo-safety) · [Next](/blog/omadesign-0-5-8-canvas-tool-plugins) --- # Canvas tool plugins Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-canvas-tool-plugins Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, tools ## The habit The Pencil tool in Illustrator, the brush in Photoshop, and the vector brush in Affinity all share one contract. You choose the tool. You drag. You see the mark while your hand is still down. You let go and the mark belongs to the document. Escape, or another tool letter, puts the pencil down. You do not want a plugin "tool" that is secretly a dialog: click five points, press OK, and hope the path lands where the dialog guessed. You also know the other contract, the one for pixels. A brush preset does nothing useful until a raster layer is the thing under the cursor. A filter that runs on the vector layer you forgot to leave will either error or bake the wrong target. The tool and the filter are neighbors in the plugin list. They are not the same gesture. ## The constraint A plugin cannot draw into the canvas on its own schedule. The studio owns the pointer, the preview, and the undo step. If the plugin received every mouse move inside Lua, a slow script would lag the stroke and a crashed script would leave a private preview on screen with no object behind it. The gesture has to be recorded by the host as document points, shown as a line the host paints, and handed over once, on release, as `ctx.gesture`. The plugin then builds native geometry and returns. That result goes through the same commit rules as any other document action: one batch, or nothing. Escape has to drop the armed tool before a release can fire. Switching to a drawing tool with a letter key has to drop it too, or you would drag a rectangle and also commit a plugin path. Pixel work stays on a raster layer because the brush and the filter write pixels, and a vector-only document has nowhere legal to put them until you add that layer. ## What landed An action with `tool = true` is a canvas tool. In the manager its button reads **Activate tool** rather than **Run**. Studio starter's tool is **Ribbon path**, command id `ribbon`, category Tools. Stroke width defaults to 14, range 1 to 100. Color defaults to `#A6E3A1`. Press **Activate tool**. The Plugins menu grows **Exit plugin tool Esc** while it is armed. The cursor on the canvas is a crosshair. Press the pointer and drag. The host records points in document coordinates, including whether Alt, Shift, or Ctrl were held. It will keep up to 8192 points. While the button is down it paints a 1.5 pixel line in the accent color. That line is a preview. It is not a shape, it has no layer row, and it is not in the `.oma`. Release the button. The host starts the plugin with `ctx.gesture.points`. Ribbon path requires at least two points. It calls `oma.add_shape` with `kind = "path"`, those points, fill `none`, the stroke color and width you set, and the name Ribbon. The commit is one Undo step. The path is editable with the Node tool, `A`, like any pencil stroke. Escape does two pieces of work. It clears the armed plugin tool and sets the status to **Plugin tool stopped**. It also follows the studio's normal Escape rules if a different operation was in progress. Choosing another studio tool, anything other than the Move tool, also clears the plugin tool so the letter keys keep their meaning. Space-pan is left to the hand tool path. The plugin does not receive the gesture while you are panning. A click or a drag that never builds two points fails the plugin's assert. You get the error. You do not get a stray one-point object. Pixel filters and brush presets are the other half of the tweet, and they are not this gesture. **Midnight duotone** needs `ctx.active_layer` on a raster layer. The starter asserts **Select a raster layer first**. **Soft ink brush** and **Dry marker brush** call `oma.brush` and the studio moves you to the Pixel persona with the Brush tool selected. Painting still happens on a raster layer. If the document is vectors only, add a pixel layer from the Layers studio before you expect ink to stick. The manual's line is the short version: choose a Raster document and a raster layer before pixel filters or brush presets. ## In the hand Open a vector document. **Plugins → Manage plugins**. Select **Tools · Ribbon path**. Set Stroke width if 14 is wrong. Press **Activate tool**. The manager can stay open or you can go back to the canvas. The status line already told you to choose parameters, then Run or Activate tool, when you opened the action. Drag a curve. Watch the line. It tracks the pointer and it does not create a layer. Release. A path named Ribbon is selected in the stack, stroked, no fill. Press `Ctrl+Z`. The path goes away in one step. Press `A` and the nodes are there if you redo. Arm the tool again. Drag a stroke you do not like and press Escape before you release. Status says **Plugin tool stopped**. Release does nothing useful because the tool is gone. Arm it again and press `P` for the Pen. The plugin tool drops because you left the Move tool. The Pen works as the Pen. For pixels, make or open a Raster document. Select the raster layer in the layer list. Run **Filters · Midnight duotone**. The active layer's pixels change. Alpha stays. Then run **Brushes · Dry marker brush**. The persona switches to Pixel. Paint. `[` and `]` still change brush size after the preset loads. The preset did not lock the tool. If you run the duotone with no raster layer active, the plugin stops on **Select a raster layer first** and the document does not change. ## The edge The preview line is not artwork. Until release, the `.oma` is unchanged, and Escape throws the gesture away. A finished stroke is one Undo, and a failed assert or a cancel applies nothing. The tool also refuses to stay armed once you pick a different studio tool, so a plugin cannot hijack `R` or `P` or `B`. Filters and brushes refuse the vector-only case. They need a raster layer. The ribbon does not write pixels, and the duotone does not read your drag. Press **Activate tool**, drag the stroke, and release. Escape if the line is wrong. ## The thread Part 124 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-plugin-categories) · [Next](/blog/omadesign-0-5-8-opt-in-behaviors) --- # Opt-in behaviors Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-opt-in-behaviors Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, plugins ## The habit You have installed a script that "helps" by talking every time the selection changes. In Illustrator that is a startup script or an event listener you forgot was in the Scripts folder. In Photoshop it is an event-based action that logs a line, or worse, duplicates a layer, on every click. In Affinity it is a macro you bound and then could not find the off switch for. The first day it feels clever. The second day the status line is noise and a behavior that edits artwork has started reacting to its own edit. The habit you actually want is closer to a panel you open. Selection info when you ask. A document size when you open a file, if you asked. Silence on a deadline. The off switch has to be one control you can see, and it has to stay off across launches until you flip it. ## The constraint Plugins run in-process, on a background worker, against the one `.oma` in the tab. An event listener with no gate would fire during manifest load, during a drag, and during the plugin's own commit. The commit updates the selection. If that update counted as a new selection event, a behavior that rewrites geometry would queue itself, and the 15 second cap would be the only thing that stopped it. Undo would fill with identical steps. The host can see two moments that matter for API 1: the document tab changed, and the vector selection changed. Those are the only events. They stay disconnected until a checkbox in the manager is on. The checkbox writes a file next to the plugins so the choice survives a restart. When a behavior's result is applied, the studio records the new selection as the baseline before it looks for the next event. The write the behavior just made does not count as a reason to run the behavior again. ## What landed A behavior is an action with `event` set. The two legal values are `selection_changed` and `document_opened`. Anything else is rejected at inspect time. The error is **Supported events: selection_changed, document_opened**. A normal action leaves `event` unset and runs from **Run**. A tool leaves it unset and uses `tool = true`. Studio starter ships two behaviors, both enabled as actions inside an enabled plugin, both still quiet. **Selection count** uses `selection_changed`. It sends `oma.message` with the number of vector objects in `ctx.selection`, in the form `3 vector objects selected`. **Document dimensions** uses `document_opened`. It sends the document name, width, and height: `Poster · 1920 × 1080` when those are the numbers. Neither one edits geometry. A behavior you write may edit geometry. The same gate applies. The checkbox label in **Lua plugins** is **Run enabled plugins’ document and selection behaviors**. It starts off. Turning it on writes `behaviors.json` in the plugin root, `~/.local/share/omadesign/plugins/behaviors.json`, containing `true`. Turning it off writes `false`. The next launch reads that file. A missing file is off. While the checkbox is on, the studio watches. Welcome screen open: no events. A drag or other operation in progress: no events, and any queued events are cleared. The plugin has to be enabled with its own checkbox. Disabled plugins are skipped. The action's event string has to match this moment. A document switch, including the tab identity changing, queues `document_opened` for every enabled plugin that has an action on that event. A selection that differs from the last baseline queues `selection_changed`. The queue holds at most 128 pending events. They run one at a time. If a plugin is already running, the next event waits. A second overlapping run is refused with **A plugin is already running**. When the worker returns, the usual commit rules apply. Cancel, a switched document, a moved selection, or a changed document fingerprint discards the result. On success, document edits are one Undo batch and the status line shows the message. The studio then sets its remembered selection and document id to the values after the commit. The event check later in the same turn sees no difference. That is the recursion stop. The behavior does not trigger on its own output. `oma.message` is a status line. It does not mark the document dirty by itself. A behavior that only reports a count leaves the `.oma` alone and leaves Undo alone. ## In the hand Open **Plugins → Manage plugins**. Confirm Studio starter is checked. Find **Behaviors · Selection count** and **Behaviors · Document dimensions** in the list so you know they are installed. Leave the behavior checkbox off. Click objects on the canvas. The status line stays on whatever you were doing. Open another tab. No dimension line. Turn on **Run enabled plugins’ document and selection behaviors**. Click one rectangle. When the worker finishes, the status line reads `1 vector objects selected`. Shift-click a second object. The line updates to `2 vector objects selected`. The document history does not grow, because the action only called `oma.message`. Switch tabs, or open a document with `Ctrl+O`. **Document dimensions** runs for the tab you landed on. The status line shows that document's name and pixel size. Switch back. It runs again for the document you returned to, because the tab identity changed. Turn the checkbox off. Select something else. Silence. Quit the app and launch it. The checkbox is where you left it, because `behaviors.json` was written. If you write a behavior that moves shapes on `selection_changed`, run it once by changing the selection. The move commits as one Undo. The selection the plugin returned is now the baseline. It does not immediately run again because of that move. Change the selection with the mouse and it runs once more. `Ctrl+Z` reverses the document edit from that run. Welcome mode does not feed events. An in-progress pen gesture does not feed events. Cancel run still means the result is discarded and the picture is unchanged. ## The edge Off is the default, and off is remembered. Enabling the plugin is not the opt-in. The behavior checkbox is. A behavior does not run because its row exists in the list, and its own commit does not queue another copy of itself. The only events are selection changes and document opens. There is no timer, no pointer-move stream, and no "every edit" hook in this API. Turn the checkbox on, then click the artwork. The count lands in the status line once. ## The thread Part 125 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-canvas-tool-plugins) · [Next](/blog/omadesign-0-5-8-host-api-surface) --- # Host API surface Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-host-api-surface Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, plugins ## The habit ExtendScript hands you the application. You can walk the object model, open files, and call almost anything the panel can call, which is why a script from 2011 still reaches into a palette that moved. Photoshop's scripting surface is the same idea with a different dictionary. Affinity's macros record gestures more than they offer a stable set of calls. When the surface is "the whole app," every internal rename is a broken script, and every script is one `require` away from reading a path you did not mean to hand it. What you actually call, job to job, is smaller. Add a shape. Move it. Replace its geometry. Set a fill or a gradient. Set the effect stack. Drop in SVG paths. Read and write pixels. Load a brush. Save a palette. Say something in the status bar. You want those calls to take native values, so the result is editable in the same inspectors as a shape you drew with `R` and `G`. ## The constraint The plugin runs on a worker, in a fresh Lua 5.4 VM, against a snapshot. It cannot hold a pointer into the live studio. Persistent memory has to live in the `.oma` or in the preset the host stores, because the VM is thrown away when the action returns. `oma` exists while `run` executes. It does not exist while the host is reading the manifest, so top-level code can only return the table and define functions. The calls have to produce native document commands, not a private scene graph. A gradient a plugin builds has to open in the gradient editor. A shadow has to open in FX. A path has to take nodes under `A`. A compound path has to stay a compound path. Pixel writes have to be one undo. The surface stays on that list so a plugin cannot grow a back door into the file system or the network by calling something that looked convenient. ## What landed API version is 1. The manifest sets `api = 1`. `ctx.api` reports the same number. `run(ctx, params)` receives the document width, height, and name, the active layer index or nil, the selection, the layers, and the gesture or nil. Selection entries carry `layer`, `id`, `name`, `geom`, `style`, and `rotation`. Layer entries carry `index`, `name`, `locked`, `visible`, and `raster` as `{width, height}` or nil. Gesture points are `{x, y}` in document coordinates, plus `alt`, `shift`, and `ctrl`. Lua arrays start at 1. Layer indexes and pixel coordinates start at 0. Shape ids are integers. Geometry and fills use the native tagged JSON. `omadesign --inspect document.oma` prints a real document if you need to see the tags. The source types live in `src/geom.rs`, `src/document.rs`, and `src/filter.rs`. The calls: `oma.add_shape` returns `layer, id`. Kinds are `rect`, `ellipse`, `line`, `path`, and `geometry`. Rectangles take `radius`. Paths take `points` and optional `closed`. Geometry takes a native `geom` table, including a path with anchors, handles, `smooth`, and `radius`. The host creates a vector layer when it needs one. `oma.translate(layer, id, dx, dy)` moves a shape. `oma.set_geometry` replaces geometry, including an editable compound. `oma.remove` deletes a vector shape. `oma.set_fill` takes a hex color, `"none"`, or a native fill table. `oma.gradient(colors, kind)` builds a native multistop fill. Kind is `linear`, `radial`, `conic`, or `shape`. Stops stay editable in the gradient editor. `oma.set_effects` replaces that shape's effect stack with tagged tables: Blur, Shadow, InnerShadow, Offset, Morphology, Saturate, Brightness, Contrast, Invert, HueRotate, ColorMatrix, Turbulence, Displacement. `oma.color(hex)` returns `{r, g, b, a}` with channels 0 to 255, which is what shadow colors want. `oma.brush` activates a Raster preset. `oma.palette(name, colors)` adds a named palette to the personal library. `oma.read_asset(relative_path)` reads a UTF-8 file inside the plugin folder. `oma.svg(svg_text, x, y, width)` imports paths as editable artwork and keeps the aspect. `oma.pixel(layer, x, y)` returns source `r, g, b, a`, or transparent black out of bounds. `oma.map_pixels(layer, callback)` replaces pixels. The callback is `function(r, g, b, a, x, y)` and returns four channels. `oma.message(text)` sets the status line. Parameters on the action are `number`, `text`, `color`, and `boolean`, each with `id`, `label`, and a typed `default`. Numbers may set `min` and `max`. Colors are `#RRGGBB` or `#RRGGBBAA`. The host validates them for the desktop and the command line. Unknown parameters are rejected. Each run is a new VM. Lua globals do not survive. Keep artwork in the document and presets in the plugin. The standard libraries you get are table, string, math, utf8, and the basic functions. `require` is absent, so a plugin is one `main.lua` plus assets read through `oma.read_asset`. ## In the hand Save a folder with `main.lua` that returns `api = 1`, a stable id, a name, a version, and one action. The id is letters, digits, dots, hyphens, underscores, no leading dot. Action ids are unique inside the plugin. The version is yours, not the app's. The guide's color-dots example is the smallest pattern. `run` loops and calls `oma.add_shape` with `kind = "ellipse"`, a position, a 12 pixel size, the color parameter, and the name Dot. `oma.message` tells you the circles are editable. Install the folder from **Plugins → Manage plugins → Install folder…**. Press **Run**. Press `V` and move one dot. Press `Ctrl+Z` and the whole row leaves. To see geometry tags, draw a curve with `P` and run `omadesign --inspect` on a saved `.oma`. The path table you read is the table `kind = "geometry"` accepts. Anchors carry `pt`, `h_in`, `h_out`, `smooth`, and `radius`. To recolor a selection, loop `ctx.selection` and call `oma.set_fill` or `oma.gradient`. Press `G` afterward. The stops are native. To shadow it, call `oma.set_effects` with a `Shadow` table. The FX studio shows the same shadow. To read pixels without feedback, call `oma.pixel` from inside `oma.map_pixels`. The source stays stable for the whole pass. Return alpha on purpose. Out-of-bounds reads are transparent black, so an edge kernel does not invent color. ## The edge `oma` is not available while the manifest is loading. Top-level code that calls it fails discovery. The VM does not keep globals, file handles, or a network socket, because those libraries are not there. `oma.read_asset` stays inside the plugin folder. Hidden, locked, and guide shapes are not writable. A call that builds document edits still commits as one Undo or not at all. Write the manifest, install the folder, and press **Run**. ## The thread Part 126 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-opt-in-behaviors) · [Next](/blog/omadesign-0-5-8-plugin-cli-batch) --- # Plugin CLI batch Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-plugin-cli-batch Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, plugins ## The habit You have a folder of fifty posters and one change: nudge the lockup, recolor a shape, run the same pattern. In Illustrator that is a batch action or a Bridge script, and the action wants the files open in a GUI. In Photoshop it is Image Processor, which is happy to overwrite if you point it at the same folder. In Affinity it is a macro you fire by hand because the batch story is a person at the keyboard. You want a shell command. You want the inputs left untouched. You want an existing output to stop that one file, not to get clobbered. You want file seven's error written down while file eight still runs. The order matters when the folder is a sequence. Filename order is the order you can predict. Recursive walks are how a script picks up last year's `_export` directory and treats it as source. ## The constraint The desktop app is a window. A folder job should not need that window, and it should not need a display. The same plugin, the same command id, and the same parameter check have to run headless, because a behavior that only exists in the GUI will drift from the behavior you test in the shell. The `.oma` you pass in is the source. Writing back onto that path would make a failed run unrecoverable. The output is a new file. If that path exists, the run refuses it. The batch is one directory, the files sitting in it, not a tree. A crash on one document cannot abort the loop, or a single bad file stops a night job. The process exit still has to be nonzero when anything failed, or a Makefile will believe the folder is done. Brush presets and palettes have no document to write. A canvas tool has no pointer. Those actions stay in the desktop manager. The shell runs document commands. ## What landed One document: ```sh omadesign --plugin ./my-plugin --command dots \ --input input.oma --output output.oma \ --params '{"count":12,"color":"#A6E3A1"}' ``` A folder, using Studio starter's nudge: ```sh omadesign --plugin ~/.local/share/omadesign/plugins/org.omadesign.studio-starter \ --command nudge --batch ./input --output-dir ./output \ --params '{"dx":20,"dy":0}' ``` `--plugin` is a `main.lua` or a folder that contains one. `--command` is the action id, not the display name. For the starter, the ids you will actually type include `duotone`, `soft-shadow`, `icon-orbit`, `dot-field`, `aurora`, and `nudge`. `--params` is JSON. Missing params mean an empty object, and the action's defaults apply. The host validates types, min, and max the same way the manager does. Unknown keys are rejected. `--batch` reads the directory you name. It keeps entries that are files and end in `.oma`. It does not walk subfolders. The paths are sorted, which for a flat folder is filename order. An empty folder errors with **Batch folder has no .oma documents**. `--batch` requires `--output-dir`. The directory is created if it is missing. Each output file uses the input's filename inside that directory. The input file is only read. The writer opens a new path. If the output already exists, that file fails with **Output exists; choose a new path:** and the source stays as it was. The loop continues. At the end, any failures are reported and the process exits nonzero. A clean run prints `input → output` for each file and exits zero. Headless selection is not your last mouse selection. The command line selects every visible vector shape that is not locked and not a guide, on an editable layer, and it uses the first editable layer as the active layer. Translate selection, which asserts that the selection is non-empty, therefore moves those shapes. A document with nothing editable fails that action and the batch moves on. PNG and SVG exports work as output suffixes too. The headless writers also know `.jpg`, `.jpeg`, `.psd`, `.psb`, `.pdf`, and `.ora`. Same-file conversion is refused. A brush or palette result from a command you aimed at the shell returns **Brush and palette actions run in the desktop Plugins menu; batch commands must edit documents**. Tools need a gesture. The shell passes none. There is no window. Undo does not apply, because you are not in a session. The output file is the result. Open it and the edits are ordinary document commands, so `Ctrl+Z` works once, in the studio, on that new file. ## In the hand Make `./input` with three posters, `a.oma`, `b.oma`, `c.oma`. Make sure `./output` does not already contain those names. ```sh omadesign --plugin ~/.local/share/omadesign/plugins/org.omadesign.studio-starter \ --command nudge --batch ./input --output-dir ./output \ --params '{"dx":20,"dy":0}' ``` You should see three lines, filename order, each input pointing at a new file under `./output`. Open `a.oma` from the input folder. It has not moved. Open `output/a.oma`. The visible vectors that were free to move are 20 pixels to the right. Press `Ctrl+Z` in that tab. They step back. The source file is still the source file. Run the command again without deleting `./output`. Each file reports that the output exists. The inputs are still untouched. The exit status is nonzero. Point `--output-dir` at a new folder, or remove the outputs, and run again. Break one file on purpose. Leave a document in the folder that has no editable vectors, so `nudge` hits **Select one or more vector objects first**. The other files still write. The broken name is listed. The exit status is nonzero. For a single file, drop `--batch` and `--output-dir` and pass `--input` and `--output`. The same existence check applies to that one output path. Parameters that fail validation fail the run before a write. `{"dx":"nope"}` does not produce a half-moved `.oma`. ## The edge The batch will not overwrite an output, will not modify the input, and will not recurse into subfolders. One bad document does not stop the rest, and it does not let the process exit as if the folder succeeded. Brush, palette, and canvas-tool actions are refused here. They belong to the desktop, where a preset and a gesture mean something. Run the `nudge` command against a fresh `--output-dir`. The sources stay put. ## The thread Part 127 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-host-api-surface) · [Next](/blog/omadesign-0-5-8-install-list-plugins-cli) --- # Install list plugins CLI Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-install-list-plugins-cli Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, plugins ## The habit You install a Photoshop panel by dropping a folder into a path Adobe documents once per major version, then you restart and hunt the Window menu. Illustrator scripts land in a Presets folder that moves. Affinity assets get imported through a panel. On a headless box, or over SSH to the machine that actually holds the files, none of those gestures exist. You want to copy a plugin into place from a shell, see that it registered, and run the document command. You still want the brush and the drawing tool in the GUI, because a preset you cannot paint with is a JSON file. Listing matters as much as installing. You want the command id, the category, and the display name in a terminal, so the `--command` you type later matches an id and not a label you misremembered. ## The constraint The binary is the same binary that opens the window. A second "plugin admin" tool would drift. Headless install and headless list are flags on `omadesign`. They have to work with no display, because the batch machine may not have one, and because an install step in a script cannot click **Install plugin…**. The plugin root is still `~/.local/share/omadesign/plugins`, or `$XDG_DATA_HOME/omadesign/plugins` when that variable is set. The manager and the shell read the same folders. A hidden backup on replace is the same backup. Enable state lives in `disabled.json` in that root. There is one catalog. What the shell cannot do is pretend a brush stroke happened. `oma.brush` and `oma.palette` need the desktop session that owns the Brush tool and the personal library. A canvas tool needs a pointer gesture. Those stay in **Plugins → Manage plugins**. Document actions are the ones `--command` can apply to files. ## What landed Install a file or a folder: ```sh omadesign --install-plugin ./my-plugin ``` The argument is a `.lua` file, a folder containing `main.lua`, or an `.omaplug` ZIP with `main.lua` at the root of the archive. The same three shapes the manager accepts from **Install plugin…** and **Install folder…**. On success the process prints `Installed NAME VERSION at PATH`. The path is the installed `main.lua` under the plugin id, not the source you pointed at. List: ```sh omadesign --list-plugins ``` Each plugin prints `id version enabled` or `id version disabled`. Each action prints an indented `id · category · name`. Discovery errors go to stderr. No window opens. The catalog is the same one the manager builds: directories in the plugin root, skipping names that start with a dot, reading `main.lua`, skipping duplicate ids with an error. Studio starter, after a normal install, shows up as `org.omadesign.studio-starter` `1.0.0` `enabled`, with rows for `duotone`, `soft-shadow`, `icon-orbit`, `soft-ink`, `dry-marker`, `ribbon`, `dot-field`, `aurora`, `night-palette`, `nudge`, `selection-info`, and `welcome-document`. The names after the category are what the manager buttons show. The ids are what `--command` wants. `ribbon` is the tool. `soft-ink`, `dry-marker`, and `night-palette` are the preset and swatch actions. `nudge` is the document command the batch example uses. Replace an existing id and the installer renames the old folder to `.backup--` before moving the new tree into place. If that move fails, it renames the backup back. The manager's **Reload** is the desktop way to pick up an edit you made in the installed folder. The shell picks up a new process immediately, because each invocation reads the disk. Enable state is the checkbox. The shell list reports it. It does not toggle it. You still flip the checkbox in the manager, which writes `disabled.json`. A disabled plugin remains listed. Document commands from here are the batch and single-file forms. `--plugin` can point at the installed folder: ```sh omadesign --plugin ~/.local/share/omadesign/plugins/org.omadesign.studio-starter \ --command nudge --input poster.oma --output poster-nudged.oma \ --params '{"dx":16,"dy":0}' ``` Point `--command` at `soft-ink` or `night-palette` and the run refuses: **Brush and palette actions run in the desktop Plugins menu; batch commands must edit documents**. Point it at `ribbon` and there is no gesture to supply. Activate that one from the manager with **Activate tool**. `--install-plugin` with no path prints **Use --install-plugin FILE_OR_FOLDER** and fails. The flag does not open a picker. The desktop buttons open the native file dialog. The shell takes a path you already have. ## In the hand Write a plugin folder. From a terminal with no studio open: ```sh omadesign --install-plugin ./my-plugin omadesign --list-plugins ``` Read the id line. Read the indented action ids. Copy the id you mean into `--command`. Run it against one `.oma` and a new output path. Open the output in the studio. The edit is native. `Ctrl+Z` drops it. Then launch the app. **Plugins → Manage plugins**. The same plugin is in the list, same version, enable box matching the word `enabled` or `disabled` you saw in the terminal. **Reload** if you edited `main.lua` in the installed folder while the window was open. The manager does not watch the file for you. For a brush, ignore the shell. Select **Brushes · Soft ink brush** and press **Run**. Paint with `B`. For Ribbon path, press **Activate tool** and drag. The shell already told you those rows exist. It will not perform them. If you reinstall the same id from the shell, list again. The version string is the new manifest. The previous tree is the hidden backup in the plugin root, which `--list-plugins` skips because the name starts with a dot. ## The edge Install and list do not open a window, and they do not run the plugin. They register a bundle and print the catalog. The shell will not paint, will not save a personal palette, and will not invent a canvas gesture. Those three stay in the desktop manager. Document commands are the ones you batch. Run `omadesign --list-plugins` and use the action id it prints. ## The thread Part 128 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-plugin-cli-batch) · [Next](/blog/omadesign-0-5-8-plugin-sandbox-limits) --- # Plugin sandbox limits Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-plugin-sandbox-limits Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, plugins ## The habit You have installed a Photoshop panel that phoned home, and an Illustrator script a coworker dropped in a folder that used `File.execute` because that was the easy way to resize a batch. ExtendScript gives the script a real filesystem and, historically, a way to shell out. That is convenient the day you write it and expensive the day someone else's plugin rides along. Affinity macros stay inside the app more tightly, and the trade is that they cannot be a small program you diff in git. You want the plugin to edit the document you pointed at. You do not want it to read `~/.ssh`, to open a socket, or to exec a binary because a pixel loop was easier in Python. You want a hard stop on time and memory so a bug becomes an error line and not a hung studio. You want to hand someone a single file. ## The constraint Lua lives inside the one binary. The worker shares the machine with the `.oma` you have open and with the rest of your home directory. If the script gets `io`, `os`, `package`, or `require`, the sandbox is a comment. Those libraries are absent. `debug` is absent. Binary chunks and the file loaders are absent. `pcall`, `xpcall`, and coroutines are absent, so a plugin cannot catch the abort and keep writing, and it cannot park a thread beside the UI. `assert` and `error` stop the run. The host then puts numbers on the run so "absent" is not the only guard. A plugin that allocates forever, or that emits a million shape commands, has to die inside a bound. The document commit stays atomic: a run that trips a limit applies nothing. Distribution is a ZIP you can inspect, with a size cap, with symlinks and path traversal rejected. There is no store in this release that would fetch that ZIP for you. ## What landed 0.5.8 embeds Lua 5.4. Plugins get table, string, math, utf8, and the basic functions. They cannot execute programs, reach the network, or read an arbitrary path. `oma.read_asset` reads UTF-8 inside the installed plugin folder. That is the file API. The run limits are 15 seconds, 64 MiB of Lua heap, and 20,000 document edits. Queued edit data stops at 256 MiB. Raster input stops at 128 MiB. Source stops at 2 MiB. Each asset or geometry blob stops at 4 MiB. An action list stops at 128 actions, with 24 parameters on an action. An installed bundle stops at 512 entries and 64 MiB total. Archives reject path traversal and symlinks. A ZIP that tries to write outside the stage fails before it replaces the installed folder. If a folder for that plugin id already exists, the installer renames it to a hidden `.backup-…` first and restores that backup when the new tree fails to land. A bad bundle does not leave you with half the old plugin and half the new one. Hidden shapes, locked shapes, and guides cannot be modified. The command line uses the same rule when it builds a selection: visible, not locked, and not a guide. You ship a plugin by zipping the contents of the folder, including `main.lua`, assets, a README, and a license, and naming the archive `your-plugin.omaplug`. `main.lua` sits at the root of the ZIP, not inside an extra directory that the installer has to guess. People install that file from **Plugins → Manage plugins** or with `omadesign --install-plugin`. There is no remote plugin marketplace in 0.5.8. Nothing in the app browses a catalog, downloads a bundle, or updates a plugin from a URL. Effect stacks have their own caps, because a plugin can ask for native effects: 32 effects, blur at 512, morphology radius at 64, offsets and displacement at 4096, turbulence at 8 octaves and base frequency at most 1. Pixel maps that are too expensive hit the time limit. The manual's advice is to shrink the input or to run the work per document in a batch, where one file's failure does not roll back the files that already wrote. ## In the hand Write `main.lua`. Keep it small enough to sit under the 2 MiB source cap, which is a wide ceiling for a script and a hard ceiling for a pasted binary. Put `orbit.svg` next to it if you need artwork. Read it with `oma.read_asset("orbit.svg")`. A path with `..` does not escape the folder. Try to call `io.open` or `os.execute` or `require`. The run fails. The document does not change. Press **Run** again after you delete the call. The fresh VM does not remember the failed attempt. Build a filter that loops forever or allocates without bound. At 15 seconds or 64 MiB the worker stops. The status is an error. `Ctrl+Z` has nothing new to undo, because the batch was never committed. Zip the folder's contents: ```sh cd my-plugin && zip -r ../my-plugin.omaplug main.lua README.md LICENSE orbit.svg omadesign --install-plugin ../my-plugin.omaplug omadesign --list-plugins ``` Install the same id again with a broken archive if you want to see the restore. The previous folder comes back. `--list-plugins` still shows the old version. The hidden backup is how the replace stays one step. In the manager, **Install plugin…** filters to `lua`, `omaplug`, and `zip`. **Install folder…** takes the directory. Both end at the same root. ## The edge This release will not fetch a plugin for you. You install a file you already have. That file cannot see the rest of the disk, cannot open a socket, and cannot start a process. When it blows a limit, the `.oma` stays as it was. The marketplace is the folder you zipped and the person you sent it to. Zip the folder as `your-plugin.omaplug` and install that file. ## The thread Part 129 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-install-list-plugins-cli) · [Next](/blog/omadesign-0-5-8-starter-source-path) --- # Starter source path Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-starter-source-path Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, plugins ## The habit You learn a plugin API by opening someone else's working plugin, not by reading a table and imagining the calls. In the Adobe world that example is often a SDK zip on a developer site, versioned against a specific application year, with a README that points at a forum thread. Affinity's examples, when you can find them, sit next to the macro panel more than they sit in a git tree you can branch. You want the example on disk after a normal install, in a path you can `cd` to, with a license that lets you copy it. When the example is good enough to ship, you want a place to put it back. A pull request with a folder, a README, a license, a sample document, and a note that says you tried the failure cases. You do not want to bind your plugin to a private function you spotted in the binary. ## The constraint The live plugin directory is `~/.local/share/omadesign/plugins`. That is where installed bundles run, where `disabled.json` and `behaviors.json` live, and where an app update refuses to clobber `org.omadesign.studio-starter` if you already have it. If the only copy of the source were that live folder, an edit and an example would be the same files, and an update story that preserves your edits would also freeze the example you meant to read fresh. So the package writes a second tree: the clean Studio starter, under `plugin-examples`. The app update can refresh that tree. Your installed copy stays. Contribution goes upstream, into the `plugins/` directory of the repo, as a uniquely named folder. The host API you may call is the one in the plugin guide. Unexposed internals are off limits because they move and because the sandbox will not see them anyway. ## What landed After install, the fresh source is: ```text ~/.local/share/omadesign/plugin-examples/studio-starter ``` `XDG_DATA_HOME` replaces `~/.local/share` when it is set. The folder contains `main.lua`, the Orbit SVG, a README, and the MIT license the starter carries. The README tells you to install the folder from **Plugins → Manage plugins → Install folder**, or: ```sh omadesign --install-plugin /path/to/studio-starter ``` That copies it into the live root under the manifest id `org.omadesign.studio-starter`. Editing the examples tree does not change the running plugin. Editing the live `main.lua` does, after **Reload** in the manager or a new process for the shell. The starter's README is also the map of preconditions. Select objects before Effects, Gradients, and Batch. Choose a pixel layer before Filters and Brushes. Activate Ribbon, then drag. Patterns and Icons create native objects. Swatches persist in the personal palette library. Behaviors run only when their checkbox is enabled. Upstream, the same tree lives at `plugins/studio-starter` in the Omadesign repo. Host coverage lives in `src/plugins/tests.rs`. The contribution path is a fork of [https://github.com/michaelmonetized/omadesign](https://github.com/michaelmonetized/omadesign), a new folder under `plugins/` with a unique name, and a pull request. The pull request has to include `main.lua`, a README that states inputs, output, and the app and API versions you support, and a license. Assets are original or licensed, with attribution. Include a small `.oma` example or steps someone else can follow, plus a screenshot of the output. Verify three behaviors: an error or a cancel leaves the input alone, the output saves and reopens, and document edits undo in one step. Test a batch into a new output folder, not onto the inputs. Manifest discovery stays fast and free of side effects. `oma` is not there yet when the host loads the file to list actions. If you need a host call that does not exist, open an issue or put the proposal in the pull request. Do not call into app internals that the guide does not list. The guide's table is the contract: add, translate, geometry, remove, fill, gradient, effects, color, brush, palette, asset, svg, pixel, map, message. Offline copies of that guide are on disk at `~/.local/share/omadesign/docs/plugins.md`, and `omadesign --agent-docs plugins` prints the copy baked into the binary you are running. `CONTRIBUTING.md` sits next to the other offline docs. ## In the hand ```sh ls ~/.local/share/omadesign/plugin-examples/studio-starter ``` Open `main.lua`. The twelve actions are the file. Copy the folder somewhere you can edit it, change the manifest `id` so you do not collide with `org.omadesign.studio-starter`, and install your copy: ```sh omadesign --install-plugin ~/src/my-starter omadesign --list-plugins ``` Two plugins. The original id still runs the package copy. Yours runs your edit. Break `run` on purpose, press **Run**, and confirm the document is unchanged. Fix it, **Reload**, run again, `Ctrl+Z` once. When the plugin is worth sending upstream, put it in a fork at `plugins/your-name/`, with the README, the license, and a tiny `.oma`. Run the batch into an empty output directory and keep the log. Screenshot the result. Open the pull request with those pieces named. If you only wanted the clean starter back after you edited the live copy, install from the examples path again. The manager keeps a hidden backup of the previous installed folder before the new one replaces it. The examples path remains the readable source either way. ## The edge The examples path is not the live plugin, and the live plugin is not the pull request. Shipping a change into Omadesign means a folder under `plugins/` with the README, the license, the sample, and the undo and cancel check. A plugin that depends on an unexposed function is not on the API. Propose the call. Until it is in the guide, it is not yours to require. Open `~/.local/share/omadesign/plugin-examples/studio-starter/main.lua` and read the action you want to copy. ## The thread Part 130 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-plugin-sandbox-limits) · [Next](/blog/omadesign-0-5-8-adobe-familiar-tool-keys) --- # Adobe familiar tool keys Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-adobe-familiar-tool-keys Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, shortcuts ## The habit Your left hand already knows the letters. `V` selects. `A` is the direct selection or node tool, depending on which app taught you. `P` is the pen. `T` is type. `B` is the brush. `I` is the eyedropper. `Z` is zoom. `H` is the hand. Illustrator, Photoshop, and Affinity do not agree on every letter. They agree on enough of them that a new tool with a cute letter is a tax. `R` for rectangle and `O` for ellipse are in your hand from Illustrator. `J` for clone and `Shift+J` for the healing brush are in your hand from Photoshop. You want those letters on day one, and you want a strip on screen that tells you the rest without a trip to a PDF. You also know the letters lie when the persona changes. Photoshop's `J` does nothing useful in Illustrator. The letter can stay. The tool it calls has to match the room you are in. ## The constraint One binary, five personas, one key table. Design, Layout, Pixel, Photo, and Motion share the process and the `.oma`, except Photo's develop session which keeps its own history beside the camera file. If each persona shipped a private keymap, muscle memory would reset every time you pressed the persona you needed for the next ten minutes. The letters stay. The table refuses a letter that has no tool in that persona, so `S` does not invent a star in a room that has no star, and Photo does not turn `P` into a pen over a RAW file. The teacher has to stay out of the way. A shortcut overlay that takes keyboard focus steals the next letter from the tool you just asked about. The HUD is a strip. Hints are informational. Text editing and menus get their own context. `F1` opens the full list when the strip is not enough. ## What landed These are the tool letters, with no modifier, from the manual and the key table: `V` Move. Click selects, drag moves, eight handles scale, the handle above the box rotates. Shift-click adds or removes. Alt-drag clones. `A` Node. Points and Bézier handles. Shift-click adds a node to the selection. Alt-click toggles corner and smooth. Alt-drag breaks a handle. Delete removes selected points. `P` Pen. Click a corner, click-drag a smooth point. Shift constrains to 45 degrees. Enter or double-click finishes an open path. Escape drops the last point, then cancels. Click the first point to close. `N` Pencil. `R` Rectangle. `O` Ellipse. `Y` Polygon. `S` Star, in Design and Layout. `L` Line. Shift constrains. Corner radius, sides, and inner radius sit in Transform. `T` Type. Click, type, Enter for a new line, Escape or a click away to finish. Double-click existing type to edit it. `G` Gradient, dragged across a selected shape. The active Fill or Stroke row in Appearance chooses which paint you are editing. Stops you already made stay. `I` Eyedropper. `U` Trace, raster to vector on the active pixel layer. `B` Brush. `E` Eraser. `K` Fill. `J` Clone, Alt-click to set the source. `Shift+J` Healing brush in Pixel, Alt-click clean texture, then paint. `M` Smudge in Pixel. `C` Crop. `W` Wand. `H` Hand. `Z` Zoom. A few letters sit next to that set because the same table owns them. `F` is Frame in Layout. `Q` is the lasso. `Shift+O` is the Artboard tool in Design, and the elliptical marquee in Pixel. `Shift+M` is the rectangular marquee in Pixel. `Shift+A` toggles auto-layout on a Layout selection. Photo's letter tools are Hand, Zoom, Crop, and Eyedropper. The other letters do nothing there. `[` and `]` change brush size. `Shift+[` and `Shift+]` change hardness. Those are plain keys. With Ctrl they belong to stacking, which is a different chord. The Shortcut HUD sits on the bottom of the window. The upper row follows the current tool. The lower row shows letter keys. Hold Ctrl, Shift, Alt, or a combination and the strip shows the matching commands. It keeps its height so a drag does not jump. Hover **+ more** when the window is too narrow for every hint. `Ctrl+/` or **View → Shortcut HUD** shows or hides it. `F1` opens the full shortcut list, and the same key closes it. While you are editing text, letters go into the text. A field in the inspector swallows non-global shortcuts. Save, open, and the HUD toggle still work. The tool letter does not. ## In the hand Open a blank vector document. The default persona is Design. Press `R` and drag a rectangle. Press `P` and draw a short path. Press `T`, click, and type. Press `V` and move the rectangle. Press `A` and drag a point. That is the first minute the manual describes. Press `B`. If you are still on vectors only, add a pixel layer before you expect paint. Press `[` twice and watch the brush shrink. Press `E` and erase. Press `I` and sample. Press `Z` and click to zoom in, Alt-click to zoom out, or drag a box. Press `H` and pan. Space does the same pan while you hold it, as long as you are not in Motion. Switch to Layout. Press `F` and drag a frame. `R` and `T` still work. Press `S`. The star is legal here. Switch to Pixel. Press `J`, Alt-click a source, and clone. Press `Shift+J` and heal. Press `M` and smudge. Press `W` and drag the wand. Press `Q` and draw a lasso. Switch to Photo and open a picture. Press `C` and crop. Press `I`. Press `Z`. Press `H`. Press `P`. Nothing arms a pen. The letter is reserved, and this persona does not have that tool. Press `F1` whenever a letter fails you. The list is the same table. Press `Ctrl+/` if the strip is in the way, and press it again when you want it back. Hold Shift while the strip is visible and read the constrained gestures before you drag. ## The edge A letter that the persona does not implement does not fall through to a different tool. Photo keeps four tool letters. Star stays in Design and Layout. Smudge and the healing brush stay in Pixel. Text editing eats the letter until you leave the text. The HUD never takes focus, so reading a hint does not steal the next key. Press `V`, then `F1` if you want the rest of the table in front of you. ## The thread Part 131 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-starter-source-path) · [Next](/blog/omadesign-0-5-8-file-and-edit-keys) --- # File and edit keys Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-file-and-edit-keys Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, shortcuts ## The habit `Ctrl+Z` undoes. `Ctrl+Shift+Z` redos. `Ctrl+S` saves. `Ctrl+C`, `Ctrl+V`, `Ctrl+X` move objects through the clipboard. `Ctrl+A` selects everything you meant. `Ctrl+N` is a new document. `Ctrl+O` opens one. `Ctrl+D` duplicates, except in Photoshop, where `Ctrl+D` drops a selection and you duplicate with a different chord. You have both habits in the same hand. A Linux app that picks one and stays silent about the other will eat a marquee the first time you try to copy a layer, or it will clone an object the first time you try to drop ants. Save As is `Ctrl+Shift+S` in your head even when a cheat sheet writes it as Shift+S next to a Ctrl that already covers Save. Place and Export need chords too, because those are file operations you do with the picture still open. ## The constraint The key table is one function. Ctrl or the command modifier is the prefix. Shift picks the alternate on the same letter. The table cannot special-case "duplicate" differently in every persona without breaking the one keymap, and it cannot ignore the Photoshop habit on a pixel selection without training you to undo a clone you did not want. So duplicate is the D chord, and Pixel adds one gate. If a pixel selection is up and the chord is Ctrl without Super, `Ctrl+D` clears the marching ants and does not clone. `Super+D` duplicates in place, including in Pixel, including while ants are up. The canvas menu prints **Duplicate** as Super+D so the label matches the chord that always means duplicate. The manual states both: Duplicate is Super+D, and a pixel selection clears on Ctrl+D. Redo has two chords because both are already in people's hands: `Ctrl+Shift+Z` and `Ctrl+Y`. The manual lists the Shift chord. The key table accepts both. File chords stay global. They still fire when an inspector field is focused. Edit chords do not. A text box keeps its letters. Paste while you are editing text inserts into the text. ## What landed `Ctrl+Z` undoes. `Ctrl+Shift+Z` redos. `Ctrl+Y` redos as well. `Ctrl+S` saves. `Ctrl+Shift+S` is Save As. The save dialog is the native file dialog, filtered to `.oma`, with the document name filled in. `Ctrl+O` opens. The dialog's filters are All supported, omadesign, Photo settings, Camera RAW, Layered documents, Images, and Vector. `Ctrl+N` is a new tab. `Ctrl+Shift+P` places. The file loads in the background, then you click or drag to set it down. Enter places at the center. Escape cancels. Nested layers and masks travel together, and Undo removes the placement in one step. `Ctrl+E` exports. The export dialog asks for a filename of the form `export` plus the suffix you are writing. `Ctrl+A` selects all. In Photo, `Ctrl+A` selects all loaded photos in the library, which is the selection paste-adjustments uses. It does not select vector objects, because you are not in that tool set. `Ctrl+C` copies. The status bar says `copied N object` or `copied N objects`. Objects copied inside Omadesign paste at their original positions, including onto another artboard. `Ctrl+X` copies and then deletes. The status bar says `cut` when the copy succeeded. `Ctrl+V` pastes. The status bar says `pasted` plus the count. Alt-drag clones under the pointer. The menu's duplicate and `Super+D` clone in place. `Ctrl+V` also accepts a screenshot, an image copied from a browser, a copied image file, plain text, and SVG source or an SVG file. External content lands in the center of the visible canvas. Images become pixel layers. Text becomes an editable text layer. SVG becomes vectors. Command+V works through Omarchy's universal paste. Shift+Insert from the Alt+V clipboard-history picker pastes too. Paste during text edit inserts into that text. `Ctrl+Alt+C` copies style. `Ctrl+Alt+V` pastes style. The status line says `style copied` when the copy lands. In Pixel, with a marquee, lasso, or wand selection active, `Ctrl+D` clears it. `Super+D` duplicates the object in place. With no pixel selection, `Ctrl+D` duplicates, same as the other personas. In Design, Layout, and Motion, `Ctrl+D` duplicates. Photo does not use the object copy, cut, paste, or duplicate chords. Copy adjustments is `Ctrl+Shift+C`. Paste adjustments is `Ctrl+Shift+V`. The status line on a copy says the adjustments were copied and that crop and rotation are excluded. Those two chords are how a look moves between pictures. They are documented with the Photo tools, and the key table only enables them in that persona. ## In the hand Draw two rectangles. Press `Ctrl+A`. Press `Ctrl+C`. Read the status bar. Press `Ctrl+N`, then `Ctrl+V` in the new tab. The rectangles land at the same positions. Press `Ctrl+Z` if you want them gone in one step. Press `Ctrl+Alt+C` on a styled shape. Select another. Press `Ctrl+Alt+V`. Fill, stroke, and effects that style copy carries move across. The geometry stays. Press `Ctrl+S`. Pick a folder in the native dialog. The file is a `.oma`. Press `Ctrl+Shift+S` when you want a second file. The first path stays the first path. Press `Ctrl+Shift+P`, choose a PNG or an SVG, and click the canvas. Escape if the ghost is wrong. Enter if you want the center. `Ctrl+Z` lifts the placement. Switch to Pixel. Drag a marquee. Press `Ctrl+D`. The ants clear. The layer count does not change. Press `Super+D`. A duplicate appears in place. Press `Ctrl+D` with the ants already gone. That one duplicates. Open Photo. Develop a frame. Press `Ctrl+Shift+C`. Select other thumbnails. Press `Ctrl+Shift+V`. Crop stays put unless you turn that category on. Press `Ctrl+S` there to write `.omaphoto` sidecars. That save is the photo save, not the poster save. While a text layer is in edit mode, `Ctrl+V` types the clipboard into the paragraph. Escape finishes the edit. Then `Ctrl+V` pastes objects again. ## The edge Once a pixel selection exists, `Ctrl+D` clears it and `Super+D` duplicates. Photo's clipboard chords are the adjustment pair, `Ctrl+Shift+C` and `Ctrl+Shift+V`. Paste into live text stays in the text. The file dialog is the desktop's dialog, and the chords above are what open it. Press `Ctrl+S` when the picture is the one you mean to keep. ## The thread Part 132 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-adobe-familiar-tool-keys) · [Next](/blog/omadesign-0-5-8-arrange-and-transform-keys) --- # Arrange and transform keys Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-arrange-and-transform-keys Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, shortcuts ## The habit In Illustrator, `Ctrl+G` groups and `Ctrl+Shift+G` ungroups. The Pathfinder and the compound-path command are different keys, because a group is a container you can still open and a compound is one path with holes. Photoshop's `Ctrl+G` is a group too, once you are in layers. Affinity keeps the same split between the container and the boolean. Your hand already reaches for `Ctrl+]` and `Ctrl+Shift+]` to walk the stack, for `Ctrl+T` when you want the box with rotate and scale, and for a guides toggle you can hit without leaving the drag. A cheat sheet that writes "Ctrl+G combine" collapses the container and the boolean into one chord. You will group when you meant to punch a hole, or you will look for Ungroup when the command that releases the hole is a different key. The labels have to say which job the chord does. ## The constraint One key table, shared by the personas that edit objects. Photo does not group vectors, so those chords stay dark there. Everywhere else the table has to keep four operations on four chords. Group creates an editable layer group. Ungroup releases that group and does not run a boolean. Compound builds one compound shape. Release compound takes it apart. Each of those is one undo step, same as a nudge. Guides and snapping are view state. They need chords you can hit during a drag without opening View. Snapping in particular has to invert for one gesture, because the classic move is: snapping is on, this one drop has to ignore it, then snapping returns when you let go. That temporary invert is hold Ctrl during the drag, and it has to be the same Ctrl you already trust from other apps, without sticking after the mouse comes up. Free transform has to land you in the move tool with the handles live, and it has to leave text and shape parameters editable. A transform that outlines type on the way in is a different command, and that command already exists as Convert to path. ## What landed The key table and the manual agree, and the canvas menu prints the same chords. `Ctrl+G` groups. The menu says **Group**. The selection becomes an editable layer group. You can still double-click in to edit a child. `Ctrl+Shift+G` ungroups. The menu says **Ungroup**. Children come out. Paths are not combined. `Ctrl+8` is compound. The menu says **Compound shape** and the item stays disabled until at least two objects are selected. `Ctrl+Shift+8` releases the compound. Shift on the number row can look like a punctuation key to the window system. The handler treats that physical 8 as 8, so the chord still resolves. Combine and Release keep guide state, rotation, stacking, and gradient endpoints, in one undo step. They want either artwork or guides, with no mixture. Shape gradients follow the silhouette that results. Pathfinder, under **Object → Pathfinder**, is the other boolean set: Union, Subtract, Intersect, XOR, and Divide. Those are menu operations on two or more vectors on the same layer. Divide makes separate pieces and keeps holes. Each one is one undo. They are not the G chord. Stacking: select a layer row and `Ctrl+]` moves it forward, `Ctrl+[` moves it backward, `Ctrl+Shift+]` sends it to the front of its group, `Ctrl+Shift+[` sends it to the back. The menu items **Bring to front** and **Send to back** call the Shift chords. Click an object on the canvas and the same shortcuts apply to object stacking. Each reorder undoes in one step. With no Ctrl, `[` and `]` belong to brush size. The modifier is the whole difference. `Ctrl+T` is free transform. The selection goes to the Move tool with scale and rotate handles ready. It is also under Object. Live text stays live text. Shape parameters stay parameters. Flip is the right-click or Object menu, horizontal or vertical, and it follows the canvas axes after rotation. Live text has to be converted to a path before a flip will outline it. Undo restores the text. `Ctrl+;` shows or hides ruler guides and object guides. `Ctrl+Shift+;` toggles snapping. Hold Ctrl during a drag to reverse snapping for that drag only. Release Ctrl and the toggle you saved comes back. View still has the individual snapping switches. Guides start locked. **View → Guides** has Lock all guides and a separate command that clears every lock. The ruler menu and **Object → Guides** offer lock, clear-all, and that same release. `Ctrl+/` toggles the Shortcut HUD. `F1` opens the full list and closes it again. ## In the hand Draw three rectangles. Press `Ctrl+G`. They move as a group. Double-click one and nudge it. Press `Ctrl+Shift+G`. Three objects again. Press `Ctrl+Z` and the group returns, because ungroup was one step. Select two of them. Press `Ctrl+8`. One compound. Look at the holes if they overlap. Press `A` and the contours are still editable. Press `Ctrl+Shift+8`. Two shapes. Press `Ctrl+Z`. The compound returns in one step, gradient endpoints included if you had painted one. Select a layer row. Press `Ctrl+Shift+]`. It sits at the front of its group. Press `Ctrl+[` and it steps back one. Press `Ctrl+Z` twice. You are where you started. Press `Ctrl+T` on live text. Scale the box. The characters are still characters. Press Escape or click away, then `T` and double-click to keep typing. Press `Ctrl+T` on a rectangle and drag a corner. The rectangle's parameters survive. Drag a guide out of the top ruler. Press `Ctrl+;`. It hides. Press the chord again. It shows. Press `Ctrl+Shift+;` and drag an object toward the guide. The snap line appears, or it does not, depending on the toggle. Hold Ctrl mid-drag. The snap decision flips for that gesture. Let go of Ctrl before you let go of the mouse if you want the saved mode back for the drop. Press `Ctrl+/` if you want the HUD gone while you judge spacing. Press `F1` when you need the whole table, including the chords this page is about. Right-click the canvas on a multi-selection if you would rather see the labels. Group, Ungroup, and Compound shape are printed there with the keys. Compound shape is grey until two objects are selected. ## The edge `Ctrl+G` groups. It does not build a compound, and `Ctrl+Shift+G` does not release one. The boolean pair is `Ctrl+8` and `Ctrl+Shift+8`. Combine refuses a mixture of artwork and guides. Holding Ctrl reverses snapping for the drag under your hand and then gives the toggle back. Photo does not take the group chords. Brush size keeps the bare bracket keys. Select two shapes and press `Ctrl+8` when you want one compound. Press `Ctrl+G` when you want a group. ## The thread Part 133 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-file-and-edit-keys) · [Next](/blog/omadesign-0-5-8-view-and-motion-keys) --- # View and motion keys Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-view-and-motion-keys Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, shortcuts ## The habit `Ctrl+0` fits the artboard. `Ctrl+1` is actual pixels. Plus zooms in, minus zooms out. Space grabs the canvas and pans, and you expect to keep holding it while the other hand clicks. A trackpad pinch does the same zoom. After Effects and Photoshop both use Space for the hand while you are looking, and both use Space again for play once a timeline has focus. You already live with that overload. The rule has to be obvious: which room has the playhead. On a timeline, `K` or a diamond is how you plant a key. Home and End jump the clip. Delete on a selected key removes the key. Delete on the layer removes the animation and leaves the drawing, and a second Delete removes the object. You do not want the first Delete to throw away the poster. ## The constraint The view chords are global enough to share, and they have to zoom the canvas, not the widget chrome. A Ctrl+plus that scaled the panels would wreck the HUD and the inspectors every time you framed a logo. Pinch, Ctrl-scroll, and Alt-scroll follow the same rule. The Zoom tool is allowed to be more specific, because you opted into it with `Z`. Space cannot mean pan and play in the same persona at the same moment. Motion owns Space as a toggle. Every other persona owns Space as a held pan, including Photo. The key table stays one table. The persona decides which interpretation runs. That is the "one keymap" in practice: the same letters, gated by where you are, not a second scheme you memorize for the timeline. Delete on the timeline has an order, because one key is doing three jobs. A selected diamond wins. If no diamond is selected and the object has animation, the animation goes and the drawing stays. If the object has no animation, Delete removes the object, which is the same Delete the rest of the studio uses. Motion does not get a private Delete that skips that order. ## What landed `Ctrl+0` fits. `Ctrl+1` is 100 percent. `Ctrl++` zooms in. The key table treats `=` as plus, so the chord works without Shift on a US layout and with Shift where plus is the shifted key. `Ctrl+-` zooms out. Pinch the trackpad. Ctrl-scroll and Alt-scroll zoom the canvas. With the Zoom tool selected, two-finger scroll zooms too. `Z` is the tool. Drag a box to fill the view with that box. Click zooms in one step. Alt-click zooms out one step. Ctrl-click fits the artboard. Ctrl+Shift-click fits the selection, or every object if nothing is selected. `H` is the hand. Hold Space and the canvas pans, in Design, Layout, Pixel, and Photo, as long as you are not editing text. Photo also pans on middle-drag and two-finger scroll. `Ctrl+0` fits the photo. `Ctrl+1` shows it at 100 percent. The same zoom chords apply. In Motion, Space does not pan. Space toggles playback. The status line says `play` or `pause`. `K` writes keys for X, Y, rotation, and scale on the selection, with ease-in-out on that command. Diamonds appear on the row. Drag a diamond to retime. Home sets the playhead to 0 and stops. End sets the playhead to the clip duration and stops. The loop control is the repeat icon, not a key. Delete in Motion: click a diamond, press Delete, and that key goes. The status says `key removed`. The object stays, and any other keys stay. Click the object name on the timeline, or leave the diamonds unselected, and Delete removes the animation from the selected artwork. The status says `animation removed`. The drawing stays. Delete again and the object itself goes, because the animation is already gone and Delete falls through to the normal object delete. Dragging a shape in Motion writes keys at the playhead. The first key at a time past zero also plants the rest pose at 0, so the motion starts from where you drew it. Presets in the inspector, Draw stroke, Pop in, Slam, Shake, Fill up, the slides, Fly, Zoom, Buzz, Fade in, become ordinary keys. Each application has its own Undo. Space previews. Incompatible, locked, hidden, and guide objects are skipped. Draw stroke wants a visible stroke. Fill up wants a closed shape with a fill. The drawing is the rest pose. Motion does not rewrite it. PNG, JPEG, and static SVG export the rest pose. The clip lives in the `.oma`. **File → Export animated SVG…** writes transforms plus stroke and fill reveals. **File → Export Lottie…** writes Bodymovin 5 shape animation. Pixel layers, layer masks, and effects make the Lottie export fail with a clear error. Use animated SVG for those. **Import Lottie…** brings a shape-layer Lottie onto the timeline. It is a basic subset. The `.oma` keeps the full edit. ## In the hand Open a poster in Design. Press `Ctrl+0`. The artboard fits. Press `Ctrl+1`. You are at 100 percent. Press `Ctrl++` twice and `Ctrl+-` once. Hold Space and drag. Let go. The tool you had, `V` or `P` or `T`, is still the tool. Space did not switch tools. It panned while it was down. Press `Z`. Drag a box around the wordmark. Alt-click once to step out. Ctrl-click to fit the board again. Switch to Motion. Select a rectangle. Press `K`. Diamonds show for position, rotation, and scale. Move the playhead and drag the rectangle. More keys. Press Space. The status says `play`. Press Space again. `pause`. Press Home. You are at the start and playback is stopped. Press End. You are at the end, stopped. Click one diamond. Press Delete. That key is gone. The shape is still on the canvas. Press Delete with no diamond selected. The animation leaves. The shape remains at the rest pose. Press Delete once more if you meant to remove the shape too. Press `Ctrl+Z` to walk that backward one decision at a time. Apply **Fade in** from the inspector. Press Space. The preset became keys. `Ctrl+Z` removes that application. Export Lottie from the File menu if the frame is vectors. If a pixel layer is in the way, the exporter tells you, and animated SVG is the path that keeps masks and effects. ## The edge Space pans everywhere except Motion. In Motion it plays and pauses, and it will not drag the canvas. Delete removes the selected key first, the animation second, the object third. It does not skip to deleting the drawing while keys or an animation are still selected. Lottie export refuses pixel layers, masks, and effects. The view chords zoom the canvas. They leave the panels at the UI scale you set. Switch to Motion and press Space. The playhead moves. Press `K` on the selection when you want keys at this frame. ## The thread Part 134 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-arrange-and-transform-keys) · [Next](/blog/omadesign-0-5-8-native-dialogs-context) --- # Native dialogs context Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-native-dialogs-context Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, shortcuts ## The habit You already know your file manager's dialog. Places in the sidebar, the recent folder, the keyboard focus that types a path. Illustrator's dialog is close to that on the desktop and then grows its own browser. Photoshop's is the same story. A toolkit that embeds a browser to show you files makes you learn a second picker, and it makes every Open pay for a web view you did not ask to start. You want the dialog the rest of the machine uses. You also want the right-click on the canvas to be the edits you were about to reach for in a menu: cut, copy, paste, duplicate, delete, guides, flip, front, back, group, compound, place, trace. And you want the status bar to say what the clipboard did, in numbers, so a paste of nothing is obvious. ## The constraint The app is one native binary. The file picker is the desktop's picker. There is no embedded browser supplying Open, Save, Place, or Export. While that dialog is up, the canvas has to go quiet. A `Ctrl+Z` that lands in the document behind a modal is how you undo the wrong thing because the dialog ate the click and the shortcut fell through. The studio disables input for the duration of the dialog and skips the shortcut handler until the dialog returns. The context menu has to show the same chords the key table uses, including the ones a short cheat sheet folds together. Group is `Ctrl+G`. Ungroup is `Ctrl+Shift+G`. Compound shape is `Ctrl+8`. If the menu printed a different story, the menu would be lying. Right-click on an object that is not selected has to select it first, or the menu would operate on the previous selection while your eye was on the thing under the pointer. ## What landed Open, Save, Save As, Place, and Export call the native file dialog. So do the palette load and export, the font and brand pickers, the plugin install file and folder buttons, and the photo open dialog. Save offers an omadesign filter and a filename ending in `.oma`. Open offers All supported, omadesign, Photo settings, Camera RAW, Layered documents, Images, and Vector. Place offers Place, Camera RAW, Images, and Vector. Export suggests `export` plus the suffix. Photo's dialog title is **Open a photo or saved settings**. RAW extensions are listed in both cases, because camera files show up uppercase and lowercase and the portal filter is case-sensitive. While the dialog is pending, the studio UI disables, and shortcuts do not run. You finish the dialog or cancel it. Then the keys mean keys again. Right-click the canvas. If the pointer is on a shape that is not in the selection, that shape becomes the selection before the menu draws. The items are: Cut, labeled `Ctrl+X`. Copy, `Ctrl+C`. Paste, `Ctrl+V`. Duplicate, labeled `Super+D`. Delete. Make guides, enabled when the selection can become guides. Release guides, enabled when it can. Flip horizontal and Flip vertical. Bring to front. Send to back. Group, `Ctrl+G`. Ungroup, `Ctrl+Shift+G`. Compound shape, `Ctrl+8`, enabled when two or more objects are selected. Place…. Trace to vector. Those are the same edits as the Object menu and the key table. Place starts the place operation: the file dialog, then a click or a drag on the canvas, Enter for center, Escape to cancel. Trace to vector runs on the active raster layer, the same job as the Trace tool, `U`, and as **Object → Trace to vector**. The status bar is the clipboard's receipt. A successful copy sets `copied N object` or `copied N objects`. Cut sets `cut` after that copy succeeds. Paste sets `pasted` and the new selection count. A failed copy sets `Could not copy:` and the reason. Style copy says `style copied`. Photo adjustment copy says adjustments were copied and that crop and rotation are excluded. Plugin completion and `oma.message` use the same strip. You do not need a toast. Dropping files is the other way in. A drop of a layered document on the canvas or the welcome screen opens it. An ordinary image places. A `.oma` opens. Lottie imports. The dialog is for when you want the filters and the sidebar. The drop is for when the file is already in your hand. ## In the hand Press `Ctrl+O`. The desktop dialog comes up. The studio behind it does not take a tool letter while you are in the dialog. Choose a `.oma` or a PSD or an SVG. Cancel once, on purpose, and press `V`. The Move tool still works, because the cancel released the pause. Press `Ctrl+S` on an unsaved document. Name it. The filter is `.oma`. Select two shapes. Right-click one of them. If it was not selected, it is selected now, and the other may have been replaced, because a right-click on an unselected shape takes the selection. Read the menu. Group shows `Ctrl+G`. Compound shape shows `Ctrl+8` and is live because two objects are selected. Choose Group if you want the container. Choose Compound shape if you want the boolean. Press `Ctrl+Z` to put it back. Right-click empty canvas. Choose **Place…**. Pick a PNG. The cursor waits. Click to drop it, or drag a box. Look at the status behavior on the next copy: select the placed image, `Ctrl+C`, and read `copied 1 object`. `Ctrl+X` and the status says `cut`. `Ctrl+V` and it says `pasted`. On a raster layer, right-click and choose **Trace to vector**. Or press `U` and click. Threshold, color count, and smoothness are in the Trace controls. Undo is one step. Install a plugin with **Install plugin…** if you want to see the same dialog family from the manager. The filter is lua, omaplug, and zip. Cancel it. The canvas takes keys again. ## The edge The picker is the desktop's picker. The studio does not run canvas shortcuts while it is open. The right-click menu will not compound a single object. That item stays disabled until two are selected. Right-click selects the shape under the pointer when it was not selected, so the menu applies to what you hit. Paste during text editing still inserts into the text. The status bar is the report. It does not grow a second clipboard UI. Right-click the artwork. The chords on the menu are the chords in the key table. Press `Ctrl+O` when you want the desktop dialog. ## The thread Part 135 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-view-and-motion-keys) · [Next](/blog/omadesign-0-5-8-plugins-offline-docs) --- # Plugins offline docs Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-plugins-offline-docs Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, docs ## The habit You sit down to write a script and the first tab is a documentation site. Illustrator's scripting guide, Photoshop's reference, a forum post that matches the version you are not running. If the network is down, or the page moved, you are reading a cache you hope is current. Affinity's help is the same shape: a site, or a manual that is not the build you installed. For a plugin API that changed in this release, the page and the binary have to be the same API. API 1 is small enough to ship beside the app. You should be able to read it on a train. You also want the license files in the package. Lua is MIT. Phosphor, the icon set in the chrome, is MIT. LibRaw is in there because Photo decodes RAW. "Included" means the text is on disk, not a URL in a README. ## The constraint 0.5.8 embeds Lua 5.4.9 in the binary. There is no separate runtime to download and no CDN that has to answer before a plugin runs. The documentation has to travel the same way, or the "no runtime" claim is only half true and you are back to a browser for the other half. The installer writes under your home directory, or under `$XDG_DATA_HOME`. It does not write `/usr`. The docs, the skill, the examples, and the licenses land in that share tree. The binary also has the docs compiled in, so `omadesign --agent-docs` prints the manual that matches the executable even if someone later edits the copy in the share folder. Those are two artifacts on purpose. The share folder is what you open in an editor. The flag is what the running build claims. The title-bar **Docs** item is a different door. The manual says it opens `https://omadesign.app/docs/`. That is the website. Offline authoring does not go through that click. ## What landed The 0.5.8 packages, ARM64 and x86_64, contain the binary, the desktop entry, the icon, the MIME XML, the README, the app license, the creation skill, and a `docs/` directory: `MANUAL.md`, `layout.md`, `format-support.md`, `cloud.md`, `plugins.md`, `CONTRIBUTING.md`, and `llms.txt`. They contain `LICENSE-Phosphor`, LibRaw's license and pinned source notice, the native toolchain notices, Lua's notices, and `plugins/studio-starter`. `install.sh` is in the archive. A normal install copies them to: ```text ~/.local/share/omadesign/docs/ ~/.local/share/omadesign/skills/omadesign-create/SKILL.md ~/.local/share/omadesign/plugin-examples/studio-starter/ ~/.local/share/omadesign/licenses/lua/ ~/.local/share/omadesign/licenses/libraw/ ~/.local/share/omadesign/licenses/native-notices/ ``` Lua's notices say 5.4.9, built from lua-src through mlua, MIT. Phosphor's MIT license ships as `LICENSE-Phosphor` in the archive. The installed plugin source for the live starter is a separate folder, `plugins/org.omadesign.studio-starter`, and an update leaves it alone if it is already there. From the binary, without opening those files: ```sh omadesign --agent-docs plugins omadesign --agent-docs manual omadesign --agent-docs layout omadesign --agent-docs formats omadesign --agent-docs index omadesign --agent-skill ``` `plugins` prints the plugin guide. `manual` prints the user manual. `layout` and `formats` print those guides. `index` prints the Markdown index. `--agent-skill` prints the creation skill. An unknown topic errors with the list: index, manual, layout, formats, or plugins. These strings are the ones compiled into that executable. They track the version you launched. **Learn with AI** on the welcome screen can fetch `https://omadesign.app/llms.txt` when you are online, and the prompt tells the agent to use `--agent-docs` for the offline, version-matched copy. **Create with agent** points at the skill file on disk and falls back to `--agent-skill`. The app does not pick an agent for you. Omarchy's default agent is the one that runs, if you have chosen one. The manager's **Authoring guide** link goes to `https://omadesign.app/docs/plugins`. Use it when you want the public page. Use the file and the flag when you want the pages that shipped with 0.5.8. Templates are local too. The 52 vector templates open with no network. That is a neighboring fact, not the plugin guide, and it is why a cold install can still produce a document before any host answers. ## In the hand Install 0.5.8. Then: ```sh ls ~/.local/share/omadesign/docs sed -n '1,40p' ~/.local/share/omadesign/docs/plugins.md omadesign --agent-docs plugins | head ``` The file and the flag should describe the same API: `api = 1`, the `oma` calls, the 15 second cap, the `.omaplug` ZIP, the batch command. If you edit the Markdown in `docs/` to scribble a note, `--agent-docs plugins` still prints the original, because it does not read that file. Your note stays in the file you edited. The binary stays the release. Open `licenses/lua` and read the MIT notices. Open the archive's `LICENSE-Phosphor` if you are checking the icon set. The chrome uses Phosphor Light. The license is the one in the package. ```sh ls ~/.local/share/omadesign/plugin-examples/studio-starter ``` That is the starter you read while you write. The guide next to it is `docs/plugins.md`. `CONTRIBUTING.md` in the same docs folder is the pull-request list: `main.lua`, README, license, sample `.oma`, undo and cancel verification. Click **omadesign** in the title bar and choose **Docs** when you want the site. It opens the website. It does not open the share folder. For the folder, use the path above. ## The edge The title-bar Docs command opens `omadesign.app`. Offline is the share tree plus `--agent-docs` and `--agent-skill`. Editing the installed Markdown does not change what the binary prints. The package you installed is the API you write against. Lua does not come from a package you install later. It is in the executable. There is still no plugin marketplace fetching docs or bundles from a host. The licenses sit beside those pages so you can read the MIT terms for Lua without opening a browser. Run `omadesign --agent-docs plugins` and write against that page. ## The thread Part 136 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-native-dialogs-context) · [Next](/blog/omadesign-0-5-8-one-binary-not-three-apps) --- # One binary not three apps Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-one-binary-not-three-apps Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, personas ## The habit A poster that needs type, a photograph, and a three-second move is three sittings in the Adobe set. Illustrator for the mark, Photoshop for the picture, After Effects for the move, and a round of exports between them so each app can see what the last one decided. Affinity's answer is StudioLink, and it is clever: Designer, Photo, and Publisher can hand work across without the export ritual, inside the suite you installed. You still learn where one app ends and the next begins. The file you double-click has a type. The tool letters reset at the boundary often enough that you hesitate. What the hand wants is duller. `V` still selects. `P` is still the pen. `T` is still type. `B` is still the brush. The window you already have is the window you keep. Switching the job switches the tools, not the process. ## The constraint Omadesign is one native binary on Linux. One launch, one dock icon, one `~/.local/bin/omadesign`. Personas are modes of that process: Design, Layout, Pixel, Photo, Motion. They share the key table, with gates where a tool does not exist. They share the Shortcut HUD and `F1`. A second process would mean a second undo stack and a file to throw over the wall every time the brief changed from vectors to pixels. The document that holds the poster is one `.oma`. Tabs are documents, not apps. `Ctrl+N` and `Ctrl+O` open tabs in the same window. Persona changes do not write a sibling file and do not ask you to export a PDF just to keep going. Photo is the exception you have to keep straight: development settings live in a `.omaphoto` beside the camera file, and they enter the `.oma` when you Place in Design, as pixels. The persona is still the same binary. ## What landed The manual's table is the map. Design is a mark, a poster, a layout. First tools: Move `V`, Pen `P`, Rectangle `R`, Type `T`. It is the default persona. Layout is a screen, a landing, a dashboard. Frame `F`, Rectangle `R`, Type `T`. Frames nest. Stack children, constraints, and frame export live here, in the same document as the drawing. Pixel is painting or retouching. Brush `B`, Eraser `E`, Clone `J`, Wand `W`. Paint sits on a pixel layer. You add one from the Layers studio if the document started as vectors. Photo is grading a photograph. Crop `C`, the develop sliders, Place in Design. The library browses a folder. Adjustments are Light, Color, and Detail. The original file stays the original file. Motion is animating the artboard. Space plays, `K` sets keys, **File → Lottie** exports. The artboard you drew is the rest pose. Motion does not rewrite it. The clip is stored in the `.oma`. Welcome sends you to the right room without a second install. **+ Vector** and the vector templates open into Design. **+ Raster** opens a blank pixel document. **+ Layout** opens frame starters. **+ Photo** opens the photo workspace. Motion has no empty-workspace button, because it starts from artwork that already exists. The funnel on the welcome browser filters by Vector, Raster, Layout, Photo, or Motion. One recent list. Files are `.oma`. The HUD sits at the bottom in every persona that shows the canvas. Upper row follows the tool. Lower row is the letters. `Ctrl+/` hides it. `F1` opens the full shortcut list. Hold a modifier and the strip shows that modifier's chords. Tool letters that do not exist in the persona do nothing. Photo keeps Hand, Zoom, Crop, and Eyedropper. `V`, `P`, `T`, and `B` are the four the day-one hand asks for, and they are legal in Design and Pixel as the table defines them. `B` in a vector document still means Brush. You need a raster layer under it before paint sticks. Chrome follows the desktop. Omarchy theme colors, the font from `omarchy font current` or fontconfig sans-serif, Phosphor Light icons. There is no separate light and dark switch inside the app. You are in one window the whole time. ## In the hand Launch `omadesign`. Stay on the welcome screen long enough to see one browser. Click **+ Vector** or a blank size. You are in Design. Press `R`, then `T`, then `P`. Press `V` and move what you made. Add a pixel layer. Switch to Pixel. Press `B` and paint. Press `E`. The layer stack is the same stack. The rectangle is still there. Press `Ctrl+Z` and you undo the stroke, in the same history as the rectangle, because it is one document. Switch to Layout if the brief grew a screen. Press `F` and drag a frame over the artwork, or wrap a selection. The poster and the frame share the file. Export a frame from the File menu when you need a PNG of that screen. The `.oma` remains the master. Open Motion when something should move. Select the type. Press `K`. Press Space. The playhead runs. Press Space again. Switch back to Design. The rest pose is the artboard. The keys are still in the file. `Ctrl+S` writes one `.oma`. For a photograph, switch to Photo, open the camera file, grade it, **Save settings**, then **Place in Design**. You are back in the poster. The placed layer is an 8-bit develop. The RAW and the `.omaphoto` stay beside each other on disk for the next grade. Same binary. You did not export a TIFF from another application to get there. Press `F1` in whichever persona you are in. The list is one list. The chords that persona cannot run are the ones its gate turns off. You do not load a second keymap. ## The edge One binary does not mean every tool letter works in every persona, and it does not mean the RAW file is embedded in the poster. Photo keeps its settings in the sidecar until you place pixels. Motion will not start from an empty welcome button. There is no second process to alt-tab into for the pen, the brush, or the timeline. StudioLink's idea, suite tools in reach of each other, is the habit this window is built to satisfy with personas and one file. Press `V`, then `P`, then `B`, in the same window. Press `F1` when a letter needs a name. ## The thread Part 137 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-plugins-offline-docs) · [Next](/blog/omadesign-0-5-8-vectors-pixels-photo-motion) --- # Vectors pixels photo motion Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-vectors-pixels-photo-motion Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, documents ## The habit The Adobe bounce is a file format bounce. Illustrator saves AI. Photoshop saves PSD. After Effects saves a project that references footage. You keep the three in a folder and you pray the versions match. A change to the type means re-export, relink, and a guess about whether the effect that looked right in one app survived. Affinity narrows that with documents that can carry vectors and pixels, and with StudioLink when you need the other toolset. You still feel the seam when the timeline is a different surface from the page. You want one layer stack. Vectors and pixel layers in the same list. A frame around them when the job is a UI. A timeline under the same canvas when something moves. A photograph graded without a one-way bake, then placed when the grade is the one you mean. ## The constraint The native file is `.oma`. JSON, rasters packed as PNG, motion clip included. One undo history for that document. Personas switch tools. They do not switch files. If Photo wrote the RAW into the `.oma`, every save of a poster would duplicate a sensor file, and a grade would be stuck inside one document. The constraint goes the other way. The camera file stays the camera file. `.omaphoto` stores the develop settings beside it. **Place in Design** copies an 8-bit developed image into the layer stack, with one Undo, and you keep the RAW and the sidecar for the next pass. Motion stores keys in the same `.oma` and treats the artboard as the rest pose. It does not bake the animation into the vectors. Static export stays the rest pose. The clip remains available the next time you open the file. Layout frames, auto-layout, and constraints are objects in that same tree, format 6 when the document uses the features that need it. Older builds cannot silently strip those features. Other documents stay on format 5. This build reads formats 1 through 6. ## What landed Design draws the vectors. Move, pen, type, shapes, gradients, effects, pathfinder, groups, compounds. Pixel paints on a pixel layer in that document: brush, eraser, clone, heal, smudge, masks, and the raster filters that apply pixels with their own one-step undo. A marquee limits a filter. Cancel in the filter dialog leaves the layer alone. Applied pixels save in the `.oma`. The layer list shows both. Eye and lock work per object. Groups expand. Pass through on a group decides whether child blend modes see the backdrop. Opacity and blend live on the object and on the layer. Placed images use the layer's opacity and blend. You reorder with `Ctrl+[`, `Ctrl+]`, and the Shift variants for front and back. One stack. One tab. Layout adds frames to that stack. `F` drags a frame. A frame drawn inside a frame nests. **Object → Wrap selection in frame**. Stack children, gap, padding, constraints. **File → Export frame** writes PNG, SVG, or HTML for the selected frame. Comments pin to the canvas. The poster and the screen mock live together. You do not maintain a second file for the UI unless you want one. Photo opens DNG, CR2, CR3, NEF, ARW, RAF, ORF, RW2, and the rest of the recognized extensions, through the decoder built into the binary. **Save settings** writes `name.NEF.omaphoto` next to the original. The original bytes stay. **Place in Design** adds the developed 8-bit layer to the document you are building. Export JPEG, PNG, or TIFF from Photo when you need a delivery file. RAW PNG and TIFF keep 16-bit channels. The `.oma` you save from Design does not contain the RAW or the sidecar. Keep the pair on disk with matching names. Motion opens on that same artboard. Tracks are X, Y, rotation, scale, opacity, stroke reveal, and fill reveal. Presets become keys. `K` keys the selection. Space plays. Delete peels a key, then the animation, then the object, in that order. The drawing survives the animation's removal. Lottie export wants vectors. Pixel layers, masks, and effects produce an error from that exporter. Animated SVG keeps them. Import Lottie brings a basic shape subset back onto the timeline. The editable master is the `.oma`. Document tabs sit above the canvas. Several `.oma` files can be open. Persona is per session of work on the document in front of you. `Ctrl+S` writes the tab you are in. ## In the hand Start in Design. `R` a board, `T` a headline, `P` a rule. Add a pixel layer. Switch to Pixel. Paint a shadow by hand, or run a raster filter on that layer. Look at the Layers studio. The type, the rule, and the pixels are rows in one list. Switch to Layout. Press `F` and draw a phone frame. Drag the headline's layer onto the frame if you want it nested. Turn on **Stack children** if the frame should pack. The vectors did not leave the file. Switch to Photo from **+ Photo** or the persona. Open a CR3 or a DNG. Move exposure. **Save settings**. You now have the camera file and a small `.omaphoto`. **Place in Design**. Choose the poster tab's world. The developed image is a pixel layer. `Ctrl+Z` removes the placement. The sidecar is still next to the RAW. Switch to Motion on the poster. Select the headline. Choose **Fade in** or press `K` and drag. Space plays. `Ctrl+S`. Quit. Open the `.oma` again. The type is editable. The pixels are there. The keys are there. Open the RAW again by opening the camera file or the sidecar. The grade is there, because it never lived only inside the poster. **View → Document conversion notes** is for files you imported from somewhere else. A native document you built this way does not need a conversion story. The notes matter when the layer arrived as PSD or PDF. The `.oma` is the file you keep either way. ## The edge The `.oma` holds the layer stack, the frames, the placed pixels, and the motion clip. It does not hold the RAW source or the Photo settings. Place is the door from a grade into the poster, and it is 8-bit. Lottie will not carry pixel layers. Static PNG and SVG export the rest pose, not the timeline. One stack does not mean one undo across Photo and Design. Photo has its own history until the pixels are placed. Save the `.oma`. Leave the `.omaphoto` next to the camera file. Press Space in Motion when you want the same artboard to move. ## The thread Part 138 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-one-binary-not-three-apps) · [Next](/blog/omadesign-0-5-8-brand-kit-on-disk) --- # Brand kit on disk Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-brand-kit-on-disk Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, brand ## The habit Creative Cloud Libraries are where a lot of kits went to live. Swatches, logos, character styles, synced through an account, available on the machines that are signed in, missing on the machines that are not. You have emailed a `.ase` and a zip of fonts as the backup plan. Affinity's assets and palettes export to files you can pass around, and you still end up writing the README that says which file is the logo and which file is the type. The kit you can trust on a Linux box with no login is a folder. The names are stable. Another machine opens the folder and sees the same colors, the same roles, the same files. You also know the failure mode of hidden files. A copy that skips dotfiles arrives without the kit and nobody notices until the swatches are empty. ## The constraint A project in Omadesign needs no account. Welcome finds project folders by the `.omabrand` directory anywhere under your home folder. Cloud sign-in is optional and separate. The kit cannot live in a library service if yesterday's job has to open on another machine that has never signed in. So the kit is three names beside the work. `.omacolors` is the palettes. `.omatype` names the font roles. `.omabrand/` holds the assets and the font files. A saved document uses the nearest enclosing folder that contains any of them. If none exists, the kit starts beside the document. An unsaved document asks you to **Choose a project**. The folder button can point at a different library on purpose. These names start with a dot. A file manager that hides dotfiles will hide the kit. Turn hidden files on when you copy by hand. Palette saves and artwork saves are different. Quitting asks about unsaved palettes first, then unsaved artwork. A kit that auto-wrote into the `.oma` would vanish the moment you sent someone the pictures without the folder, and a kit stored only in the document would not be shared by the next file in the same project. ## What landed The right sidebar has Inspect, Palettes, and Brand. Palettes are Personal or Project. Personal colors are available across your work and live in the personal library, which is also where a plugin's `oma.palette` writes. Project colors go in the current project folder. **+ Palette**, name it, **Rename**. Add the current color, pull fill and stroke **From selection**, or type a hex and press **+**. `#RRGGBBAA` carries alpha. Choose Fill or Stroke, then click a swatch. The swatch menu replaces, copies the hex, or removes. **Save** on the palette writes the collection. That save state is separate from `Ctrl+S` on the artwork. `.omacolors` is JSON. Several named palettes can sit in one file, or a single palette object, or a bare array of hex strings. Older RGBA objects still load and become the portable form on the next save. **Load palettes…** merges and suffixes conflicting names. **Export selected palette…** and **Export collection…** write files you can hand over. If the file changes on disk while you have unsaved palette edits, Save is blocked. Export a copy or **Reload saved colors**. Brand → **Load bank…** or **Create bank**. Assets copy into `.omabrand/`. Originals stay where they were. PNG, JPEG, WebP, TIFF, BMP, GIF, SVG, and `.oma` are accepted. Drag a tile onto the canvas, or double-click to place at the selected artboard's center. Placement undoes with `Ctrl+Z`. In Photo, double-click places the asset into Design. Optional `brand.json` sets the display name. Without it, the folder name is the name. **Save bank copy…** copies the bank, the typography kit, and the fonts into another project folder. Typography → **Add fonts…** copies TTF or OTF into the project. Fonts are usable inside the app without installing them on the computer. Name a role Heading, Body, Caption, and **Save role**. **Apply** uses that face on the selection or on the next text you create. The Character panel lists **Project fonts**. Applying a face to artwork supports Undo. The kit's names and files have their own save. `.omatype` stores roles with paths relative to `.omabrand/`, under `fonts/`. **Load kit…** merges another kit and copies its font files. **Save copy…** writes the kit into another folder. Removing a role keeps the font file for text that already uses it. SVG export draws project-font text as outlines so the picture survives. The `.oma` keeps the text editable, including after you move the project, including new characters. Saving artwork into another folder copies the faces that artwork uses into that folder's `.omabrand/fonts/`. Welcome's **Projects → recent** is this folder, found by `.omabrand`, with subprojects and descendant `.oma` files. **Edit brand…** opens the editor. **+ Project** starts one. **Team** shows up only while you are signed into cloud and a shared project exists. Local browsing does not wait on that. ## In the hand Make a folder for the job. In the app, **Choose a project** and point at it, or save the `.oma` inside it and let the nearest kit win. Open Palettes, choose Project, add the colors you are actually using, and press **Save**. You should see `.omacolors` in the folder once hidden files are visible. Open Brand, **Create bank**, add the logo SVG and a wordmark PNG. Drag the logo onto the artboard. `Ctrl+Z` if it landed wrong. Open Typography, add the two font files, name the roles, **Save role**, select the headline, **Apply**. Copy the folder to another machine, dotfiles included. Install Omadesign there if it is not already in `~/.local/bin`. Open the `.oma`. The type is still editable. The swatches are in Project. The bank tiles are the same files. No sign-in dialog stands in front of the folder. On the machine you started from, export the palette collection as well if you want `.omacolors` spelled out in the destination. **Save bank copy…** carries assets, the type kit, and the fonts. The manual's Fieldwork example in the repo is a portable sample of the same layout. If a plugin installed Night studio into Personal, that palette is yours across projects. Copy `.omacolors` when the colors belong to this job and should travel with it. ## The edge Skip the dotfiles and the kit does not arrive. `.omacolors`, `.omatype`, and `.omabrand/` are the kit. Palette save and font-kit save are not `Ctrl+S`. A conflict on disk blocks the palette save until you reload or export. SVG outlines the project fonts in the export. The `.oma` keeps them editable. Share the fonts under their licenses. An account is not required to open any of this. Cloud is a separate, optional share of a project you already have on disk. Copy the project folder with hidden files on. Open the `.oma` on the other machine. ## The thread Part 139 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-vectors-pixels-photo-motion) · [Next](/blog/omadesign-0-5-8-raw-sidecars-not-destructive) --- # RAW sidecars not destructive Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-raw-sidecars-not-destructive Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, photo ## The habit Lightroom taught the sidecar habit even when the catalog was the thing you actually opened. Develop settings live next to the camera file, or in a catalog that points at it. The RAW stays the RAW. You can throw the develop away and start again. Photoshop's Camera Raw is the same contract until you open the file as pixels and save a PSD, and then you have a baked copy plus the original you hope you kept. Affinity Photo develops and can save its own document. The fear is the same either way: a button that says Save and means "rewrite the camera file." You also want a folder of a shoot to take one look without loading every frame into RAM, and without a recursive walk into the selects folder you made yesterday. Then you want the one frame that belongs on the poster to land there as pixels, on purpose. ## The constraint Photo is inside the same binary as Design. That makes a destructive save easy to get wrong, because `Ctrl+S` already means "write the document" in every other persona. In Photo, save has to mean "write the settings beside the original." The original's bytes, size, and modification time are the identity the sidecar trusts. Rewriting the camera file would change that identity and would destroy the only full-precision source. The sidecar is small and contains no pixels. Resume by opening either file. A missing or changed original has to fail the explicit settings open without replacing the picture you are already looking at. A whole-folder job writes sidecars in the background, one directory, no recursion, and it has to be cancellable. Undo has to restore the settings it wrote. The pixels stay pixels. Place in Design is the bake, and it is a separate command. It produces an 8-bit layer. You keep the RAW and the `.omaphoto` if you intend to grade again. ## What landed Open a photo from **File → Open**, the Photo library, a drop, or a folder. RAW extensions the decoder recognizes include DNG, CR2, CR3, NEF, NRW, ARW, RAF, ORF, RW2, PEF, and the longer list in the format guide. An extension names a family. It does not promise every camera or every compression. JPEG XL DNG, GPR, EIP, and R3D are not in this build. Only the first image of a multi-image RAW is developed, with a note. The decoder is LibRaw 0.22.2, built in. No external converter, no download on open. You get 16-bit linear sRGB, camera white balance and color matrix, orientation honored, automatic brightness off. The display preview's long edge caps at 1600 pixels until you zoom. Full resolution arrives as tiles. **Before** is the default development, not the embedded JPEG. Rendering is not trying to match Lightroom, Capture One, or the camera JPEG. Lens corrections and proprietary camera looks are not recreated. **Save settings** writes a sibling such as `DSC_0001.NEF.omaphoto`. The pair must keep matching names. Settings record the source size and modification time. They do not contain the image. **File → Open**, a drop, or **Photo → Library → ··· → Open photo or settings…** resumes from either file. Opening the sidecar restores the original precision and the saved crop and rotation. If the original is missing, changed, or the settings are invalid, that explicit open fails and leaves the current photo in place. Opening the original with unusable settings shows default development and a note. A failed save keeps your edits so you can retry. `Ctrl+S` in Photo saves the selected photos' settings. Quitting asks Save all, Discard, or Cancel for unsaved photo settings before palettes and artwork. **Copy adjustments** is `Ctrl+Shift+C`. **Paste adjustments** is `Ctrl+Shift+V`. Light, color, detail, tone curve, color mixer, and color grading can travel separately. Crop and rotation are off by default. The batch paste is one Undo. Later edits to the source do not change a look you already copied. Whole folder: **Library → … → Browse folder…**, open one representative frame, copy its adjustments or choose **Presets… → Use preset…**. In **Apply adjustments**, choose **Whole folder**, then **Write settings for N photos**. The job writes each `.omaphoto` in that directory only. It does not recurse. It does not load the shoot into memory. It does not touch image pixels. Excluded categories keep their existing adjustments. Cancel stops the rest. Undo restores completed changes. A file that changed outside the batch is preserved and reported. The caps are 10,000 photos and 32 MiB of settings history. Larger shoots want smaller folders. Open a representative RAW first. A recognized filename can still fail to decode. Export JPEG, PNG, or TIFF at full developed resolution, including crop and rotation. RAW PNG and TIFF keep 16-bit. JPEG is 8-bit. **Place in Design** is the 8-bit layer in the poster, one Undo. The initial limits on a decode are 64 megapixels and 512 MiB input, with a 120 second cooperative cancel. Presets are `.omapreset` files in the preset library. They hold adjustment values, not pixels, and not a source identity. ## In the hand Drop a CR3 on the Photo workspace. Wait for the preview. Move exposure and white balance. You are on the 16-bit linear data. Press `Ctrl+S` or **Save settings**. In the file manager, the CR3's size and date are unchanged. Next to it is `something.CR3.omaphoto`. Quit. Launch. Open the `.omaphoto`. The grade is back. Open the CR3 on a day when you renamed it and left the sidecar behind. You get a note, default development, and the current photo is not replaced by a surprise if you opened settings that cannot find their original. Copy adjustments. Select a range in the library with Shift-click. Paste. Leave crop off. `Ctrl+Z` restores the whole paste. `Ctrl+S` writes sidecars for the selection. For a folder, browse it, open one frame that actually decodes, copy the look, **Whole folder**, write settings. Watch the progress. Cancel if a filename looks wrong. The JPEGs and RAWs in that folder are the same bytes. The new files are `.omaphoto`. When one frame belongs on the poster, **Place in Design**. It shows up as a pixel layer. Grade again later from the sidecar, place again, delete the old layer. The camera file was never the document you edited. `Ctrl+0` fits the photo. `Ctrl+1` is 100 percent, where the tiles fill in. Hold Space to pan. ## The edge Save settings never rewrites the camera file. Whole-folder apply never rewrites pixels, never walks subfolders, and never treats a recognized name as proof the file will decode. Place in Design is the moment pixels enter the `.oma`, at 8-bit. The sidecar is settings bound to size and modification time, not a hash of the sensor data. A `.oma` save does not store the RAW. Press `Ctrl+S` in Photo. Then look at the camera file. It is the same file. The `.omaphoto` beside it is the grade. ## The thread Part 140 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-brand-kit-on-disk) · [Next](/blog/omadesign-0-5-8-plugins-without-extendscript-tax) --- # Plugins without ExtendScript tax Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-plugins-without-extendscript-tax Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, plugins ## The habit ExtendScript is a real language with a real object model, and the tax is everything around the script. You install a matching application year. You put the file in a Presets path that moves. You grant it the filesystem because the sample used `File`. A panel that worked in 2019 asks you to debug a missing runtime in 2024. Photoshop's scripting and UXPs split that world again. Affinity macros record a gesture you can replay, which is the right tool for a repetitive click and a poor tool for a folder of documents or a pixel kernel you want to read. The jobs are ordinary. A filter. A live shadow. An SVG icon. Two brushes. A tool you drag. A pattern. A gradient. A swatch book. A nudge across a folder. You want those in the app you are already drawing in, with Undo meaning the whole action, and with the script unable to wander off into the home directory. ## The constraint 0.5.8 already embeds Lua 5.4.9 and plugin API 1. Adding a second runtime would be the tax again: another install, another version pin, another place for the starter kit to drift from the binary. The plugin runs on a background worker inside this process. It sees a snapshot. It returns native edits or it returns nothing. The studio commits one batch or discards the run. The API stays on a list you can memorize, because an API the size of the application is how scripts bind to panels that got renamed. Shapes, geometry, fills, gradients, effects, brushes, palettes, SVG, pixels, a status message. Fresh VM every run. No `io`, no `os`, no `require`, no network, no exec. Fifteen seconds, 64 MiB of Lua heap, 20,000 edits. There is no marketplace in this release fetching plugins you did not ask for. ## What landed **Plugins → Manage plugins** installs a `.lua` file, a folder with `main.lua`, or an `.omaplug` ZIP. Each plugin has an enable checkbox. **Reload** picks up an edit. A replace keeps a hidden backup of the previous folder and restores it if the new copy fails. Studio starter is installed on first launch. Twelve actions, id `org.omadesign.studio-starter`. Midnight duotone maps pixels and keeps alpha. Soft offset shadow writes a native, editable shadow. Orbit icon places SVG paths. Soft ink and Dry marker load Raster brush presets and switch you to Pixel. Ribbon path is **Activate tool**, a preview line, then an editable path on release. Dot field adds ellipses. Aurora gradient sets a native three-stop fill. Night studio swatches saves a personal palette, outside document Undo, same as a brush preset. Translate selection moves vectors. Its command id is `nudge`. Selection count and Document dimensions are behaviors. They stay off until you enable **Run enabled plugins’ document and selection behaviors**. That checkbox is remembered in `behaviors.json`. A behavior does not re-fire on its own output. A finished document action is one `Ctrl+Z`. Errors, **Cancel run**, and a document or selection that changed mid-run leave the `.oma` alone. The status line says the result was discarded, or it says **Plugin completed · Undo restores document edits**. The shell is the same binary: ```sh omadesign --install-plugin ./my-plugin omadesign --list-plugins omadesign --plugin ~/.local/share/omadesign/plugins/org.omadesign.studio-starter \ --command nudge --batch ./input --output-dir ./output \ --params '{"dx":20,"dy":0}' ``` Batch reads the immediate `.oma` files, filename order, writes new files, refuses existing outputs, keeps going after a bad file, and exits nonzero if anything failed. Brush, palette, and canvas-tool actions are refused on the command line. They need the desktop session. You distribute a ZIP of the folder contents named `something.omaplug`. You read the API from `~/.local/share/omadesign/docs/plugins.md` or from `omadesign --agent-docs plugins`. The clean starter source is `~/.local/share/omadesign/plugin-examples/studio-starter`. Upstream contributions are a folder under `plugins/` in a fork, with a README, a license, a sample `.oma`, and a check that cancel and undo behave. Categories in the manager are Filters, Effects, Icons, Brushes, Tools, Behaviors, Batch, Patterns, Gradients, Swatches. Other names show under All categories. The host calls are the ones in the guide. Hidden, locked, and guide objects are not targets. ## In the hand Open a poster. **Plugins → Manage plugins**. Run **Effects · Soft offset shadow** on a selection. One shadow stack appears. `Ctrl+Z` removes it. Run **Patterns · Dot field**. `Ctrl+Z` removes the grid, not one circle at a time. Arm **Tools · Ribbon path**. Drag. Release. A path named Ribbon is editable with `A`. Escape before release and the document is untouched. Switch to a raster layer. Run **Filters · Midnight duotone**. Alpha is the alpha you had. Run **Brushes · Soft ink brush** and paint with `B`. The preset is not an entry on the document history. Turn on the behavior checkbox. Select an object. The status line reports the count once. Turn it off if you want silence. The choice is still off tomorrow. From a terminal, list plugins and copy the action id. Batch `nudge` into an empty output directory. Open one result. The move is a normal document edit. The inputs in the source folder have not moved. When you write your own, start from the examples path, change the id, install the folder, and break `run` once to see the document stay put. ## The edge A plugin does not get ExtendScript's filesystem, and it does not get a private history with one step per object. Failure and cancel apply nothing. There is no remote catalog in 0.5.8. Brush and palette results are settings. Document Undo does not rewind them. Behaviors do nothing until the second checkbox is on. The shell will not invent a brush stroke. Open **Plugins → Manage plugins** and press **Run** on one document action. Then press `Ctrl+Z`. ## The thread Part 141 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-raw-sidecars-not-destructive) · [Next](/blog/omadesign-0-5-8-import-honesty-export-choice) --- # Import honesty export choice Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-import-honesty-export-choice Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, files ## The habit You open a PSD and you know the live text may arrive as pixels. You open an AI file and you know the private Illustrator data may not be in the PDF-compatible stream. You open an Affinity document and you know the adjustments and the history are not a promise. The honest apps tell you what changed. The other kind of import succeeds silently and you discover the missing effect at the client review. Export is the other half. You want PNG at 1×, 2×, and 3×, an SVG, a Lottie, a layered PSD, a PDF, an OpenRaster. You want to pick the one the next person can open. You do not want a button that claims to write a native `.ai` or `.af` when the writer does not exist. You keep the `.oma` as the editable file and you keep the source you imported when the notes say something was lost. ## The constraint One document format is the master. Importers have to land in the native layer tree, record what they could not carry, and leave the source file untouched. **Save** writes `.oma`. If an importer rewrote the PSD or the RAW in place, the round trip would be a destructive save with a friendly name. Notes have to live in the `.oma`, not only in a dialog you dismiss. **View → Document conversion notes** shows them again later. A format that appears in the Open dialog is not a promise of complete compatibility. The format guide says that in the first paragraph, and the exporters have to obey it. Where a feature cannot travel, the note says so. PDF export's rule is that effects are not silently dropped or replaced with a single gradient color. Fallback images are capped. Same-file conversion is refused so a headless `--convert` cannot clobber the path you passed as input. There is no native `.ai` writer and no native `.af` writer. Interchange for those sources is PDF, SVG, PSD, or ORA, plus the `.oma` you actually continue in. ## What landed **File → Open** reads layered PSD and PSB, GIMP `.xcf`, every page of PDF and PDF-compatible AI, OpenRaster, SVG and SVGZ, and supported Affinity documents through the optional bridge. EPS and PostScript go through Ghostscript if it is installed, then through the PDF importer. Each import opens in its own tab at the original dimensions. The source file stays where it was. PSD and PSB keep groups, names, placement, visibility, opacity, blends, pixel masks, and supported Normal color overlays. Text and smart objects arrive as their saved pixels. Other effects, fills, and adjustments produce notes. Export writes RGB 8-bit layered PSD or PSB. Vectors and live text render into pixel layers on the way out. PDF import builds artboards per page, paths, supported text, images, and optional-content groups. Complex text can become editable outlines. Illustrator's private data, symbols, and live effects are not reconstructed. `.ai` means the PDF-compatible part, or older PostScript via Ghostscript. PDF export writes pages, paths, images, opacity, and optional-content layers. Text becomes outlines in the PDF. The `.oma` still has the live text. Backdrop-dependent blending can rasterize a page, with a note. SVG import keeps objects, hierarchy, names, transforms, text, images, and supported masks. Scripts, animation, and foreign content do not come in. SVG export keeps vectors. Shape and conic gradients become image patterns, capped at 4096 pixels on a side. Linear and radial gradients keep stops. Project fonts export as outlines. The source text stays editable in the `.oma`. OpenRaster round-trips pixel layers and groups. Vectors and text become pixels per layer on export. Unsupported Porter-Duff modes are rejected rather than quietly mapped to Normal. GIMP `.xcf` imports pixels, groups, masks, and supported blends. Live text, effects, and paths are not reconstructed. There is no XCF writer. Send GIMP an ORA or a PSD. High bit depth becomes 8-bit RGBA, with a note. Affinity import is the optional bridge, `./scripts/setup-affinity-import.sh`, run once. It does not download during import. Legacy and newer containers are partial. Adjustments, live effects, publishing structure, and history do not come across as native edits. There is no `.afdesign` or `.afphoto` writer. RAW opens in Photo. `.omaphoto` is settings beside the camera file. Export from Photo is JPEG, PNG, or TIFF. Placement into Design is 8-bit. **View → Document conversion notes** lists the unsupported and converted features. The same notes stay in the `.oma`. Export from the File menu and from `Ctrl+E`: PNG at 1×, 2×, and 3×, JPEG, SVG, animated SVG, Lottie JSON, layered PSD and PSB, PDF, and OpenRaster. Layers a format cannot carry may become individual pixel layers. The notes describe that. Lottie wants shape animation. Pixel layers, masks, and effects error out of Lottie. Use animated SVG when you need them. Static PNG, JPEG, and SVG are the rest pose. The clip stays in the `.oma`. Headless, the same readers: ```sh omadesign --inspect artwork.psd omadesign --convert artwork.oma --output artwork.pdf omadesign --convert artwork.oma --output artwork.ora ``` Output suffixes include `.oma`, `.svg`, `.png`, `.jpg`, `.psd`, `.psb`, `.pdf`, and `.ora`. Notes go to stderr. The destination is a different path. ## In the hand Press `Ctrl+O`. Choose a PSD you know has live type. It opens at its pixel size. Read **View → Document conversion notes**. The type layers that arrived as pixels are named there. Save a `.oma`. The PSD on disk is unchanged. Edit what you can edit. `Ctrl+E` and write a PDF or an ORA for the next tool. Open the `.oma` tomorrow. The notes are still in the file. Open a PDF with several pages. You get an artboard per page. Text you can edit is text. Text that had to be outlined is paths, and the note says so. Export SVG for the logo page. Export PNG at 2× for a preview. Open an SVG. Drag a node with `A`. Export SVG again. Compare. If a mask became pixels, the note told you before the client did. For Affinity, run the setup script once on a machine that has the pinned converter's dependencies. Open the `.afdesign`. Read the notes. Save `.oma`. Export PDF or SVG. Leave the Affinity file as the Affinity file. For a camera file, stay in Photo. Do not expect **Save** to produce a layered document. Place or export when you need pixels. `--inspect photograph.NEF` prints metadata without writing a sibling unless you convert on purpose. Drop a file on the welcome screen if you do not want the dialog. Layered documents open. Ordinary images place. `.oma` opens. ## The edge Open does not mean the proprietary effect survived. There is no writer for native Illustrator or native Affinity. GIMP does not get an XCF back. Conversion notes stay in the `.oma` because the dialog is easy to close. Same-file conversion is refused. The source you opened is still the source. The file you continue in is the `.oma`. Open the PSD, then open **View → Document conversion notes** before you trust the layer list. ## The thread Part 142 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-plugins-without-extendscript-tax) · [Next](/blog/omadesign-0-5-8-cloud-optional-showcase-explicit) --- # Cloud optional showcase explicit Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-cloud-optional-showcase-explicit Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, cloud ## The habit Creative Cloud wants the document in the service. Libraries, sync folders, "saved to cloud" as the default checkbox. You can work offline, and you can also discover that a file you thought was a file is a stub. Affinity's model has stayed closer to documents on disk, with its own account story for the suite. The habit you want when the machine is yours is the folder. Sign in when a client has to see a snapshot. Do not sign in to open yesterday's poster. Publishing is a different decision from sharing with a reviewer. A client link and a public gallery get confused in tools that treat "anyone with the link" as the same knob as "on the homepage." You want an invite for the people with a role, and a separate act, by the owner, before a flat image is public. Taking it down should take it off the gallery. It will not reach into someone else's downloads folder. You already understand that part. ## The constraint The editable file is the local `.oma`. Brand kits are dotfiles in the project folder. Welcome browses home without an account. **Sign up for cloud** on the welcome screen opens registration. It does not gate the recent files. **Team** appears only while you are signed in and a shared project exists. Cloud collaboration, in the product since 0.5.4, shares project files, assets, and immutable flat exports. The browser reviews snapshots. You keep authoring in the native app. Live multi-user canvas editing, presence, and browser authoring are outside that scope. If a save in the studio silently uploaded, the local file would stop being the file. Transfers are explicit. A push adds files. It does not overwrite someone else's version quietly. Comments stick to the snapshot they were written on. Showcase publication is an owner action on a chosen flat export. Source files, assets, and private review threads stay out of that public object. Unpublishing removes it from the gallery, the competition displays, and the image endpoint. Copies a viewer already saved are theirs. ## What landed The workspace is `https://omadesign.app/cloud`. **File → Sign in…** opens the browser with a device code. You approve the code the desktop is showing. A name or an email alone does not grant access. Desktop credentials expire after 30 days. Revoke them from `/account` or disconnect in the app. The local identity file is owner-only on disk. **File → Push project + review export** uploads a versioned `.oma` and a PNG of the current document. Project fonts go along. Raster pixels stay embedded in the design file. **Upload project asset…** adds another file. **Cloud projects… → Pull & open** opens the latest source as a separate document and downloads shared assets into a new folder under the app's cloud-downloads directory. Save the local `.oma` after the first push so the cloud link stays with the document. Owners invite a verified email as an editor or a reviewer. Invitations go out through the mail sender, and they expire after seven days. The invited person signs in with that email and accepts in the workspace. Owners change roles, remove access, or cancel an invite. An owner can manage files, uploads, review, the team, archive, and publishing. An editor can download source and assets, upload, comment, reply, and resolve threads. A reviewer sees flat exports, pins, rectangular annotations, comments, and replies, and can resolve their own threads. Pins scale with the image, including on a phone. The desktop **Review annotations…** window loads the same versioned exports and threads. Limits on the service: source and asset files 100 MB each, flat PNG, JPEG, or WebP exports 20 MB, 200 files in a project, 100 projects on an account, 100 members on a project. Archive hides a project from collaborators and unpublishes its public work. The owner can restore it. **Publish selected export** is the showcase step, in the desktop or the web workspace. You pick a finished flat export and give it a title and a description. `/showcase` lists public work. `/showcase/:id` shows the flat image. Unpublish removes it from those surfaces. Entering a competition uses an owned public showcase work, against an open brief. Duplicate entries are rejected. Closing dates are enforced. `/compete` lists them. Competitions stay unpublished until a real brief and dates exist. Local edits do not upload themselves. Anonymous usage statistics are a separate toggle and start off. None of that is the document store. ## In the hand Draw the poster. `Ctrl+S` to a folder on disk. Do not sign in. Quit and reopen. The file is the file. The welcome recent list finds `.oma` under your home directory without a session. When a reviewer needs a snapshot, **File → Sign in…** and approve the device code. **Push project + review export**. Send the invite to their email. They comment on the flat PNG in the browser. You open **Review annotations…** in the app, reply, and keep drawing in the `.oma`. Push again when you want a new snapshot. The old comments stay on the old snapshot. When the piece is public, select the export, title it, and **Publish selected export**. Look at `/showcase`. Unpublish when it should leave. The `.oma` on disk is still private. The reviewer threads did not become the gallery page. Pull on a second machine with **Cloud projects… → Pull & open**. You get a document and a folder of assets. You do not get a live cursor from the other machine. Edit, save locally, push if you are an editor and you mean to add a version. Disconnect in the app when the laptop should stop holding a credential. The local files remain. ## The edge The service is not the default place a document lives. Sign-in, push, invite, and publish are separate acts. Publish is the owner's, on a flat export, and it leaves source and review threads behind. Unpublish does not recall a copy someone already saved. The browser does not author the canvas. There is no presence layer and no simultaneous editing session inside the `.oma`. Save the file locally. Choose **File → Sign in…** only when someone else must see a snapshot. ## The thread Part 143 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-import-honesty-export-choice) · [Next](/blog/omadesign-0-5-8-linux-first-creative-suite) --- # Linux first creative suite Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-linux-first-creative-suite Published: 2026-09-23 Author: Michael C. Hurley Tags: omadesign, 0.5.8, linux ## The habit The creative suite you already know how to use is a set of applications that, on Linux, has usually meant a VM, a second machine, or a web app in a browser tab. Illustrator, Photoshop, and InDesign are the muscle memory: pen, brush, frames, type, export. Affinity is the other suite people install when they want those jobs without that subscription, and its desktop builds have not been the thing you apt-get on the machine you actually work on. You want the letters, the layer stack, the RAW develop, and the timeline in a binary that installs into your home directory and runs on the glibc you already have. You also want the chrome to look like the rest of the desktop. A creative tool with its own theme, fighting the colors you set this morning, is a second set of preferences you will never maintain. ## The constraint The install target is `~/.local`. The public installer and `scripts/install.sh` put the binary in `~/.local/bin`, the desktop entry and MIME XML under `~/.local/share`, and the docs, skill, licenses, and plugin examples under `~/.local/share/omadesign`. Nothing is written to `/usr`. That is what makes the same line usable on Silverblue, Bazzite, NixOS, and SteamOS desktop, and on Omarchy, where `/usr` is not the place you drop a vendor tree. `$XDG_DATA_HOME` replaces `~/.local/share` when it is set. An isolated prefix exists for people packaging a copy. The default is home. The binary has to run on glibc 2.35, which covers Ubuntu 22.04, current Arch, and Asahi Omarchy. ARM64 and x86_64 are both release archives. Lua, RAW, JPEG, and the C++ runtime pieces ship inside the package so Photo and plugins do not wait on a distro package name. Templates and the plugin guide have to work with the network cable unplugged, because a first launch that needs a CDN is not an install. It is a download with extra steps. ## What landed ```sh curl -fsSL https://omadesign.app/install | sh ``` That resolves the current release, checks the archive, and installs. If `omadesign` is not on `PATH`, run `~/.local/bin/omadesign` or add `~/.local/bin` to `PATH`. One `omadesign.desktop` launcher remains. `.oma` and `.omaphoto` can point at it. Preferences live in `~/.config/omadesign`, or `$XDG_CONFIG_HOME`. Recovery swaps live in `~/.local/share/omadesign`. 0.5.8 is the release this package line installs as current. The archives are ARM64 and x86_64. Both report 0.5.8 and need no glibc newer than 2.35. The x86_64 executable was tested through QEMU against Ubuntu 22.04's glibc, not on a physical x86_64 GPU. Lua 5.4.9 is in the binary. Studio starter, the offline docs, the creation skill, and the Lua and Phosphor licenses are in the archive. Reinstalling preserves a plugin you already edited under `plugins/org.omadesign.studio-starter`. On launch the chrome reads, in order, `~/.local/state/omarchy/current/theme/colors.toml`, then `~/.config/omarchy/themes//colors.toml`, then the stock Omarchy palette if neither is there. UI type is `omarchy font current`, then fontconfig `sans-serif`. Override with `OMADESIGN_FONT=/path/to/font.ttf`. Icons are Phosphor Light. The welcome screen uses that palette. There is no in-app light/dark switch. The title bar stays visible. **omadesign** in that bar opens Config, Update, About, and Docs. About shows the version. Update checks the channel and, when you confirm, runs the official installer, waits for work in progress, writes a local recovery snapshot, and restarts with the same documents and photo adjustments. The five personas are the suite. Design, Layout, Pixel, Photo, Motion. `V`, `P`, `T`, and `B` are the letters. `F1` is the rest. Fifty-two vector templates and the Layout starters ship with the app and open offline. Photo decodes RAW without an external converter. Plugins run without a second runtime. The brand kit is `.omacolors`, `.omatype`, and `.omabrand/` in the project folder. Cloud sign-in is there when you push a snapshot, and the file opens without it. `omadesign --version` prints the build. `omadesign --agent-docs manual` prints the manual baked into it. `omadesign --list-plugins` works with no window. ## In the hand On a machine that does not have the app: ```sh curl -fsSL https://omadesign.app/install | sh ~/.local/bin/omadesign ``` The welcome screen is the file browser plus the creation buttons. Click **+ Vector**. Press `R`, `P`, `T`. The colors on the chrome match the Omarchy theme you are already using. The icons are the Phosphor set, not a private metaphor you have to learn. Open **+ Photo** and a RAW file if you have one. Grade it. `Ctrl+S` writes a `.omaphoto` beside it and leaves the camera file alone. Place into the poster when you want pixels in the `.oma`. Open **Plugins → Manage plugins**. Studio starter is there on a first install. Run one action. `Ctrl+Z`. If you are on a train, read the API with: ```sh omadesign --agent-docs plugins ``` The title-bar Docs item opens the website when you have a network and you want it. The share folder is the copy that installed with you. Click **omadesign → Update** when a newer package exists and you want it. Confirm. The installer runs. Your open documents come back. The plugin folder you edited is still the plugin folder. If you are checking a box without installing over your live copy, the archives and checksums are on the release. Both architectures. The installer is the path that puts them in `~/.local` and stops there. ## The edge The installer writes nothing to `/usr`. A machine that cannot run glibc 2.35 cannot run this binary. The x86_64 build's published check is QEMU against glibc 2.35, not a claim about every GPU. Templates do not fetch themselves. Lua does not come from the distro. Docs in the title bar open the site. The offline pages are `--agent-docs` and `~/.local/share/omadesign/docs`. Sign-in is not required to draw, grade, or animate. Run `curl -fsSL https://omadesign.app/install | sh`, then `~/.local/bin/omadesign`. ## The thread Part 144 of 144 in the Omadesign 0.5.8 feature thread. [Previous](/blog/omadesign-0-5-8-cloud-optional-showcase-explicit) · End of the thread --- # Vanity phones and fake metrics scrubbed across five repos Source: https://www.michaelchurley.com/blog/vanity-phone-metrics-honesty-fleet Published: 2026-09-09 Author: Michael C. Hurley Tags: follow-up, vanity-phone, fake-metrics, honesty, HurleyUS, portfolio ![Vanity phones and fake metrics scrubbed across five repos](/blog/vanity-phone-metrics-honesty-fleet/cover.png) ## Who Operators and visitors hitting HurleyUS public sites who still saw 555 vanity phone numbers or inflated hero metrics that did not match reality. ## What Closed ten vanity-phone and fake-metrics issues across five repos: - HurleyUS/www.yourzaxbys.com #24+#23 via PR #25 - (555) ZAXBYS to mailto:support@yourzaxbys.com; fake team phones dropped - HurleyUS/iPro-main-web #10+#8 via PR #12 - (555) GOLF-PRO removed; kept hello@ipro.golf - HurleyUS/freview #3+#2 - false positive (no phone in repo); STATUS close - HurleyUS/s12.in #33+#30 via PR #36 - hero 10M+/50K+/99.9% to honest capability copy - HurleyUS/ileague-app #7+#4 - already fixed on main via prior PR #10; closed; DNS/Coming Soon still parked ## Where Public HurleyUS repos above. Copy and contact truth in git, not a new product launch. ## When September 8, 2026 evening ET: the fleet closed after packs flagged vanity phones and fake hero stats. ## Why Vanity 555 numbers and inflated metrics are trust scars. Honest mailto and capability copy is the minimum bar for public portfolio sites. Would you keep vanity-phone and fake-metrics sweeps in the next pack day, or require contact/metrics review before a site goes live? --- # Empty READMEs filled with real usage across three repos Source: https://www.michaelchurley.com/blog/empty-readme-fills-fleet Published: 2026-09-09 Author: Michael C. Hurley Tags: follow-up, readme, content-factory, michaelmonetized, portfolio ![Empty READMEs filled with real usage](/blog/fab-analytics-same-day-php-js-ga-drop-in-json-disk/cover.png) ## Who Operators who cloned michaelmonetized demos and found empty README stubs after packs flagged content-factory gaps. ## What Closed six empty-README issues across three repos with real usage docs (no fake stars or metrics): - animated-gradient-border-on-transparent-background #1+#2 via PR #3 - CSS mask border demo; install/run from package.json - fab-analytics #1+#2 via PR #3 - PHP/JS disk analytics; honest usage - shagent #3+#6 via PR #7 - Bun + OpenRouter + MCP CLI docs ## Where Public michaelmonetized repos above. Paperwork in git README files, not deploys. ## When September 8, 2026 evening ET: the three PRs merged and closed the six issues. ## Why An empty README is a dead handoff. Real install/run copy is the minimum bar for a public demo repo. Would you keep README fills in the next pack day sweep, or require a usage section before a repo goes public? --- # Coming Soon shells replaced with honest status landers Source: https://www.michaelchurley.com/blog/coming-soon-waitlist-honesty-fleet Published: 2026-09-09 Author: Michael C. Hurley Tags: follow-up, coming-soon, waitlist, honesty, landers, HurleyUS, michaelmonetized ![Coming Soon shells replaced with honest status landers](/blog/coming-soon-waitlist-honesty-fleet/cover.png) ## Who Visitors and operators who hit waitlist Coming Soon shells, Notify me forms, or Planned SSO stubs that still read as live product instead of honest status. ## What Closed the Coming Soon honesty fleet across six shipped surfaces and logged one parked gap. Shipped: - michaelmonetized/hustledesk-com issues #6 and #4 via PR #7: SSO labeled Planned/not shipped - michaelmonetized/kitchen issues #4 and #2 via PR #5: Team Coming soon / Notify replaced with Not available yet / Use beta - HurleyUS/merchwinner.com issues #65 and #64 via PR #66: settings stub swapped for honest status - HurleyUS/coordinatorapp.com issues #37 and #36 via PR #38: real /docs status, example-only metrics, waitlist form removed - HurleyUS/iPro-main-web issues #9 and #7 via PR #11: /ileague /itour /iconf waitlist shells became honest status landers - michaelmonetized/omnux issues #12 and #10: STATUS close only (research/docs; lander stays on asahi Pages) Parked gap: - HurleyUS/ileague-app issues #8 and #5: lander PR #10 merged, but www.ileague.app still shows Coming Soon until Vercel/DNS points at apps/web ## Where Across hustledesk, kitchen, merchwinner, coordinatorapp, iPro-main-web, omnux, and the ileague.app park. Truth lives in the merged PRs and the live landers those PRs ship. ## When September 8, 2026 evening ET: the honesty fleet closed in one pass after packs flagged waitlist shells that still overclaimed readiness. ## Why Coming Soon and Notify forms that cannot fulfill a waitlist are product lies. Honest Planned/not shipped and Not available yet copy protects trust until the real surface ships. ileague.app stays open until DNS and Vercel catch the lander. Would you wire www.ileague.app to apps/web next, or leave the park until the next DNS window? --- # LICENSE copyright aligned across ten repos Source: https://www.michaelchurley.com/blog/license-copyright-alignment-fleet Published: 2026-09-09 Author: Michael C. Hurley Tags: license, copyright, mit, follow-up, portfolio, HurleyUS, michaelmonetized ![LICENSE copyright alignment across the portfolio](/blog/license-copyright-alignment-fleet/cover.png) ## Who Operators and forks reading LICENSE files across the Michael Hurley / HurleyUS / michaelmonetized portfolio who still saw mismatched copyright lines or missing MIT text. ## What Closed a 20-issue LICENSE copyright mismatch fleet across **10 repos**. Copyright lines now name Michael Hurley / the owning org. MIT was added where a license file was missing. Repos: - stripe-convex - slopops - orclawstrator - nvibe - compare - agent-os - agent-computer-use - WhisperCPPonEverything - s12.in - ileague-app Sample merges: https://github.com/michaelmonetized/stripe-convex/pull/18 and https://github.com/HurleyUS/s12.in/pull/35. All squash-merged. ## Where Across michaelmonetized and HurleyUS public repos listed above. This is paperwork truth in git, not a product deploy story. ## When September 8, 2026 evening ET: the fleet closed in one pass after packs flagged copyright drift. ## Why Mismatched LICENSE copyright is a trust and compliance scar. One narrative covers the batch so the catalog does not grow ten near-duplicate posts. Would you audit remaining private repos next, or leave LICENSE sweeps for the next pack day? --- # hustle* suite: DEPLOYMENT_NOT_FOUND cleared on *.vercel.app landers Source: https://www.michaelchurley.com/blog/hustle-suite-vercel-app-restores Published: 2026-09-09 Author: Michael C. Hurley Tags: hustle, vercel, deployment, landers, follow-up, hustlemail ![hustle suite vercel.app landers restored](/blog/hustledesk-com-eight-dollar-helpdesk-lander-wordpress-domain/cover.png) ## Who Operators who opened Hustle Launch product landers after packs flagged DEPLOYMENT_NOT_FOUND and found no live Vercel host. ## What Closed the restore batch across the hustle* suite and put *.vercel.app hosts back online: - hustledesk-com#1 - https://hustledesk-com.vercel.app - hustlecrm-com#1 - https://hustlecrm-com.vercel.app - hustleconvert-com#1 - https://hustleconvert-com.vercel.app - hustlechat-com#1 - https://hustlechat-com.vercel.app - hustleforms-com#1 - https://hustleforms-com.vercel.app - hustlemail-com#2 - PostCSS @tailwindcss/postcss fix plus https://hustlemail-com.vercel.app Git-linked Vercel prod and Ship wiring where needed. Custom DNS stays a separate overnight park. ## Where Repos under the hustle* product set. Live review surfaces are the vercel.app hosts above. ## When September 8, 2026 evening ET: restore issues closed; packs that read DEPLOYMENT_NOT_FOUND now resolve. ## Why A lander with no deploy is a dead demo. vercel.app hosts unblock review while custom DNS waits. Would you point custom DNS next, or smoke each vercel.app host against the pack checklist first? --- # hms: live again on hms-cyan-six.vercel.app Source: https://www.michaelchurley.com/blog/hms-cyan-six-vercel-live Published: 2026-09-09 Author: Michael C. Hurley Tags: hms, vercel, deployment, follow-up, hustle-management-system ![hms live on hms-cyan-six.vercel.app](/blog/hms-hustle-management-system-live-editor/cover.png) ## Who Operators who opened the Hustle Management System after packs flagged a missing Vercel deploy and found no live host to review. ## What Closed hms#1. The public review host https://hms-cyan-six.vercel.app is up again after the pack read DEPLOYMENT_NOT_FOUND. Git-linked Vercel prod and Ship wiring restored the cyan-six preview. ## Where Repo under the hms / Hustle Management System product set. Live review surface: https://hms-cyan-six.vercel.app. Prior field notes: [/blog/hms-hustle-management-system-live-editor](/blog/hms-hustle-management-system-live-editor). ## When September 8, 2026 evening ET: hms#1 closed; cyan-six vercel.app host resolves. ## Why A management system with no deploy is a dead demo. The cyan-six host unblocks review while custom DNS stays a separate park. Would you smoke the live editor against the pack checklist next, or point custom DNS once cyan-six looks clean? --- # codefolio: DEPLOY.md landed, live URL still needs Convex env Source: https://www.michaelchurley.com/blog/codefolio-deploy-md-no-live-url Published: 2026-09-09 Author: Michael C. Hurley Tags: codefolio, deploy, convex, docs, follow-up ![codefolio DEPLOY.md landed, no live URL yet](/blog/codefolio-spec-first-github-portfolio-saas/cover.png) ## Who Operators who expected a live codefolio host after packs flagged a missing deploy, and anyone tracking codefolio#1 as the ship gate for a public URL. ## What Closed codefolio#1 with docs only. DEPLOY.md landed so the restore path is written down. There is still no vercel.app URL and no production host. Convex env vars and codefolio.dev stay parked until those secrets exist. This is an honest docs-only close, not a live restore. ## Where Repo under the codefolio product set. Docs: DEPLOY.md in-tree. Prior field notes: [/blog/codefolio-spec-first-github-portfolio-saas](/blog/codefolio-spec-first-github-portfolio-saas). No live review URL yet. ## When September 8, 2026 evening ET: codefolio#1 closed on DEPLOY.md alone; Convex env and custom DNS still open. ## Why Shipping a deploy guide without Convex env still leaves a dead demo. Documenting the path closes the pack item without pretending a host exists. Would you fill Convex env and cut a vercel.app preview next, or keep codefolio.dev parked until the full auth stack is ready? --- # simple: Firebase config env-only, service account wiped Source: https://www.michaelchurley.com/blog/simple-firebase-env-only-sa-wiped Published: 2026-09-09 Author: Michael C. Hurley Tags: simple, firebase, secrets, security, follow-up, michaelmonetized ![Firebase residue cleared from simple starter](/blog/simple-nextjs-firebase-auth-social-keep-decision/screenshots/residue-gaps.png) ## Who Operators who cloned **michaelmonetized/simple** after the keep-Firebase decision and still had a hardcoded client config or a local service-account file sitting next to the tree. If the prior post left Issue #7 open on firebase leftovers, this is the close. ## What On **michaelmonetized/simple**, PR **#12** closed Issue **#7**. Client Firebase is env-only now and fails closed when the env is missing. It does not log config values. serviceAccount / admin credential patterns are gitignored. A leftover local SA file was wiped from the working tree (it was never committed). Pack had flagged hardcoded firebase leftovers. Those are gone from the tree. ## Where Repo: **https://github.com/michaelmonetized/simple** PR: **https://github.com/michaelmonetized/simple/pull/12** Prior field notes: [/blog/simple-nextjs-firebase-auth-social-keep-decision](/blog/simple-nextjs-firebase-auth-social-keep-decision). ## When **March 23, 2026:** keep-Firebase decision landed; residue (hardcoded config) still on the checklist. **September 8, 2026 evening ET:** PR #12 merges. Issue #7 CLOSED. ## Why A written keep-Firebase decision is not the same as a clean tree. Env-only client config and ignoring admin credential patterns close the security chapter the pack still listed. Would you rotate any Firebase web keys that ever lived in git history, or redeploy simple-ivory next now that the residue is gone? --- # launchpad: Clerk, Convex, and PostHog finally mount in layout Source: https://www.michaelchurley.com/blog/launchpad-clerk-convex-posthog-wired Published: 2026-09-09 Author: Michael C. Hurley Tags: launchpad, clerk, convex, posthog, nextjs, follow-up, michaelmonetized ![Providers now mounted in launchpad layout](/blog/launchpad-nye2024-boilerplate-providers-unwired/screenshots/providers-unwired.png) ## Who Anyone who cloned **michaelmonetized/launchpad** after the NYE 2024 boilerplate claim and found `providers/{clerk,convex,posthog}.tsx` written but never imported. If the prior post was about unwired providers, this is the wire-up close. ## What On **michaelmonetized/launchpad**, PR **#7** closed Issue **#3**. Root layout mounts **Clerk, then Convex, then PostHog**. Convex uses `@clerk/nextjs` `useAuth` so it matches `clerkMiddleware`. Soft-guards keep the app from exploding when env vars are unset. Deps were advertised for months. They finally mount. ## Where Repo: **https://github.com/michaelmonetized/launchpad** PR: **https://github.com/michaelmonetized/launchpad/pull/7** Prior field notes: [/blog/launchpad-nye2024-boilerplate-providers-unwired](/blog/launchpad-nye2024-boilerplate-providers-unwired). ## When **December 31, 2024:** providers written, never imported. **September 8, 2026 evening ET:** PR #7 merges. Issue #3 CLOSED. ## Why A README that lists Clerk, Convex, and PostHog is marketing until `layout.tsx` mounts them. Soft-guards mean local clones without secrets still boot. Would you point launchpad.hustlelaunch.com at this tree next, or keep the starter private until DNS matches the stack? --- # mission-control-os: pending Clerk auth and agency create timeout Source: https://www.michaelchurley.com/blog/mission-control-os-pending-auth-agency-timeout Published: 2026-09-09 Author: Michael C. Hurley Tags: mission-control-os, clerk, auth, agency, hustle-launch, follow-up ![mission-control-os auth gates after pending-session fix](/blog/hurley-mission-control-human-agent-comms/screenshots/dashboard-threads.png) ## Who Operators hitting **Hustle-Launch/mission-control-os** after Google OAuth who saw AgencyGate / PortalGate / cockpit flash signed-out while Clerk was still pending, or sat on infinite Working during agency create/select. This is the mission-control-os web product, not the Go p10k TUI and not hurley-mission-control. ## What On **Hustle-Launch/mission-control-os**, PR **#64** merged as `d09322c` and closed Issues **#50** and **#51**. Shared auth hooks keep pending sessions from looking signed-out on AgencyGate, PortalGate, and cockpit. Agency org create/select now has a hard timeout and surfaces an error instead of infinite Working. PortalGate pending shows a setup CTA. PR CI was green. **Prod redeploy is still blocked on an invalid VERCEL_TOKEN secret** (infra, separate from this code fix). ## Where Repo: **https://github.com/Hustle-Launch/mission-control-os** PR: **https://github.com/Hustle-Launch/mission-control-os/pull/64** Sibling names to keep straight: [/blog/mission-control-go-tui-p10k-portfolio-ops](/blog/mission-control-go-tui-p10k-portfolio-ops) (Go TUI) and [/blog/hurley-mission-control-human-agent-comms](/blog/hurley-mission-control-human-agent-comms) (Convex comms plane). ## When **September 8, 2026 evening ET:** PR #64 lands (`d09322c`). Issues #50 and #51 CLOSED. Prod token rotation still open. ## Why Pending Clerk sessions that render as signed-out train operators to re-auth loops. Infinite Working on agency create hides real failures. The code path is fixed; the Vercel token is the remaining ship gate. Would you rotate VERCEL_TOKEN first, or smoke AgencyGate against a preview deploy before touching prod? --- # MyBathroomConversion: WP File Manager gone, Salespromis key moved to env Source: https://www.michaelchurley.com/blog/mybathroomconversion-wp-file-manager-gone-key-to-env Published: 2026-09-09 Author: Michael C. Hurley Tags: mybathroomconversion, wordpress, security, wp-file-manager, salespromis, secrets, follow-up, hustle-launch ![Security residue removed from WordPress tree](/blog/mybathroomconversion-elementor-salespromis-xdebug-purge/screenshots/security-composite.png) ## Who Operators who inherit WP Engine content+plugins dumps. SalesPromis / Hustle Launch folks who still run Opt In into api.salespromis.com. Anyone who left Issue #4 and #5 open after the February xdebug delete. If the February post left you staring at WP File Manager 7.2.9 and a hardcoded intake key, this is the close of that chapter. ## What On **HurleyUS/www.mybathroomconversion.com**, PR **#7** merged as `928819d` and closed Issues **#4** and **#5**. Removed the vendored **WP File Manager 7.2.9** tree under `wp-content/plugins/wp-file-manager` (pack had flagged ~906 files). That plugin is gone from main. Moved the Salespromis Opt In intake off a hardcoded API key. Child theme now reads `SALESPROMIS_API_KEY` from the environment (or a `SALESPROMIS_API_KEY` constant in untracked `wp-config.php`). If unset, intake skips and logs instead of posting with a secret baked into git. Also added `.env.example` (empty placeholder) and extended `.gitignore` for `.env.*` and `.log/`. ## Where Repo: **https://github.com/HurleyUS/www.mybathroomconversion.com** PR: **https://github.com/HurleyUS/www.mybathroomconversion.com/pull/7** Live product still **https://www.mybathroomconversion.com**. Prior field notes: [/blog/mybathroomconversion-elementor-salespromis-xdebug-purge](/blog/mybathroomconversion-elementor-salespromis-xdebug-purge). ## When **February 27, 2026:** PR #2 deleted webroot `xdebug_info()` and left File Manager + the key on the open checklist. **September 8, 2026:** pack day still listed both as remaining residue. **September 8, 2026 evening ET (merge `2026-09-09T01:41:28Z` UTC):** PR #7 lands. Issues #4 and #5 CLOSED. ## Why Deleting a one-line xdebug probe is a chapter. Shipping a bathroom Opt In lander with a file-manager plugin and a key in the child theme is still an open security story. The key that was in git history is not magically clean. Rotate it at Salespromis if it is still active. Set `SALESPROMIS_API_KEY` on the host before Opt In leads will post again. Would you rotate the old Salespromis key first, or confirm File Manager is gone from the production host copy before the next lead form test? --- # BestWNC: I built a WNC directory that returns null instead of fake analytics Source: https://www.michaelchurley.com/blog/bestwnc-honest-analytics-local-directory Published: 2026-09-08 Author: Michael C. Hurley Tags: bestwnc, local-directory, western-north-carolina, asheville, nextjs, convex, stripe, clerk, martech, analytics ![BestWNC homepage. Find the places that make Western North Carolina feel local](screenshots/home.png) ## Who I live in the Blue Ridge ops lane. Locals need a directory that feels like a map, not a lead-gen trap. Owners need a page they can claim without a sales call. Operators need dashboards that do not invent click-through rates. BestWNC is for people hunting restaurants, coffee, contractors, wellness, and shops from Asheville to Boone, and for the owners of those places who will list free, claim if we already seeded them, and upgrade only when reach matters. If you build MarTech, local SEO products, or Stripe-backed owner tools, this post is field notes. ## What I shipped a local business directory for Western North Carolina. Stack on the box: **Next.js 16.2.6**, React 19, **Convex**, **Clerk**, **Stripe**, PostHog, Sentry, Resend, Tailwind v4, Bun, Phosphor icons, Vercel. Repo is private under `HurleyUS/bestwnc.com`. Site is public at [bestwnc.com](https://www.bestwnc.com/). ![Owners lander. Add or claim your BestWNC page in minutes](screenshots/owners.png) Listings start free. Paid plans live in code in `lib/billing.ts`, not as a fragile Stripe Dashboard catalog: - **Unlimited**: $10/mo or $80/yr. photos, video, posts, events, widgets, social links, contact, messaging - **Featured**: $50/mo or $480/yr. Unlimited plus priority / pinned placement - **Max**: $100/mo or $960/yr. Featured plus ad credits and the top tooling tier Manual add-ons stay manual until fulfilled: Online Presence Analysis $8, Vetted Badge $80, Listing Sync $480. Dynamic checkout builds the price at session time. Seed data is 101 real WNC businesses. Asheville-heavy, plus Waynesville, Brevard, Hendersonville, Black Mountain, and the rest of the corridor. Claim if we already have you. Add if we do not. The part that matters this week: **analytics honesty**. Owner analytics used to look busier than the measurement layer could defend. On September 8 I changed the API and UI so recorded cumulative views, review count, and average rating stay; period views, clicks, conversion rates, and weekly series return **`null`** with an explicit `availability` object. README says the same thing out loud. Cumulative views include repeat, owner, and bot traffic. they are not unique visitors. `null` is the product. Same two days: ownership claims and billing hardened, unsupported sales claims stripped from marketing surfaces, Stripe webhooks allowed through session middleware when signed, and honeypot fields on every public contact / newsletter form because spam was flooding Advertising and General Inquiry. Fill the hidden field and you get silent success, no email, no lead row. ![Owner plans. Free, Unlimited, Featured](screenshots/pricing.png) ## Where It runs on Vercel against Convex. Auth is Clerk. Money is Stripe. Mail is Resend. Errors go to Sentry. Product events go to PostHog. Surfaces that matter: - Public discovery: explore, categories, cities, top-rated, trending, search, business pages - Owner funnel: `/owners`, `/claim`, `/add-business`, dashboard edit / photos / reviews / analytics / upgrade - Pricing: `/pricing` with the plan cards that match `PLAN_DEFINITIONS` Audience sits in Western North Carolina and with builders who ship local directories instead of another generic "AI growth" wrapper. ![Explore. directory discovery surface](screenshots/explore.png) ## When **2026-01-08.** Init. Next.js + Convex + Tailwind. Directory MVP the same day. **January–February.** Categories, seed scripts, Mountain Modern redesign, mobile nav, instant listing (no review queue), Sentry, claim + email verification, PostHog, Schema.org, PWA, design variant lab, Phosphor icons, Stripe checkout/portal/webhook, **101 real WNC businesses**, photo upload via Convex storage, owner dashboard, admin moderation queue. **March.** Launch-week security and reliability. Env validation that fails fast. Stripe monetization sprint. Dynamic pricing documented. Revenue-ready checklist. Owner plans, billing roles, social tables and signed-in pages. **April–May.** Listing boosts on the owner dashboard. Real PostHog wiring passes. Blacksmith CI gates. Production deploy prep. Robots set to index, follow. **2026-09-07.** PR #109. secure ownership claims and billing; remove unsupported sales claims; keep purchased manual services pending until fulfillment. **2026-09-08.** PR #110. report only recorded business analytics (`null` where unmeasured). PR #111. honeypot fields on public contact forms. HEAD `ae65364`. Two hundred sixty-one commits on `main`. That is the clock from empty repo to an honest owner dashboard. ![About. BestWNC product story surface](screenshots/about.png) ## Why Directories lie by default. Fake weekly charts. Fake conversion. Fake "impressions" that never existed as rows. I refused that for BestWNC. If the counter is cumulative and polluted by owners and crawlers, say so. If period CTR is not instrumented, return `null`, not zero dressed as insight. I also refused a soft claim path and a spam inbox. Claims require verified identity. Billing routes have security tests. Inquiry forms fail closed for bots without giving them a bounce they can learn from. The product is still a directory: find a place, claim a place, pay for reach when you want it. Shipping the revenue path and deleting the metrics theater in the same week is the operator move. If you own a shop between Asheville and Boone, what would you fix first on your BestWNC page: photos, hours, or the claim so nobody else can edit it? --- # omadesign: I shipped a native Linux design suite in 13 days Source: https://www.michaelchurley.com/blog/omadesign-native-linux-studio-13-days Published: 2026-09-08 Author: Michael C. Hurley Tags: omadesign, rust, linux, egui, asahi, omarchy, design, photography, motion, martech ![omadesign welcome screen. Make something, 52 templates, New document presets](https://raw.githubusercontent.com/michaelmonetized/omadesign/master/media/design.jpg) ## Who I design on Linux now. Omarchy. Asahi. The camera hole has Naarchy. The creative suite did not make the move. Adobe stayed on the other OS. Affinity is partial. Browser tools are tabs that die when the laptop sleeps. Electron "studios" burn RAM and still feel like a website with window chrome. omadesign is for the operator who already lives in a themed Linux desktop and refuses to open four apps to finish one mark. Designers leaving macOS. Photographers who want LibRaw without leaving the seat. Brand freelancers who carry palettes and type roles as files, not screenshots in Slack. If you build MarTech, ship local tools, or just want `cargo` and a tarball instead of a Creative Cloud invoice, this is for you. ## What I built a native Rust studio on `eframe` / egui. GTK and Electron stayed off the table. Four personas share one document and one layer stack: **Design** (vector), **Pixel** (paint/retouch), **Photo** (RAW develop), **Motion** (timeline to animated SVG / Lottie). Geometry is defined once. Drawn for the live canvas and for PNG/SVG export. Mutations go through `Cmd` + `History`. ![Design persona. Block Party poster, rotated color block at 19°, Inspect panel](https://michaelmonetized.github.io/omadesign/media/showcase/design.webp) Current version is **0.0.4-alpha**, "The handles got the memo." aarch64 and x86_64 `*-unknown-linux-gnu` tarballs on GitHub Releases. Linked against **glibc 2.35**. Phosphor Light icons. Omarchy theme colors and `fontconfig` / `omarchy font current`. Max as the default face. Fifty-two editable vector templates. Brand kits travel as `.omacolors`, `.omatype`, `.omabrand/`. Photo side: LibRaw for DNG/CR2/CR3/NEF/ARW/RAF, 16-bit linear source, `.omaphoto` sidecars, `.omapreset` looks, folder batch. Motion: thirteen presets, Lottie JSON with unsupported-feature reporting. Interop is boring on purpose: PSD/PSB, PDF, AI-compatible PDF, OpenRaster, SVG/SVGZ, optional Affinity bridge, native `.oma`. CLI `--inspect` / `--convert`. MIT. Copyright 2026 Michael C Hurley. ![Photo persona. Coast at golden hour, Develop Color panel, Place in Design](https://michaelmonetized.github.io/omadesign/media/showcase/photo.webp) ## Where It runs where I run: Asahi Omarchy, Arch-class ARM, anything glibc 2.35 or newer. Same binary story on x86_64 via zig cross-compile. local release builds, uploaded by hand. No GitHub Actions bill. Repo: [michaelmonetized/omadesign](https://github.com/michaelmonetized/omadesign). Studio site on Pages: [michaelmonetized.github.io/omadesign](https://michaelmonetized.github.io/omadesign/). Manual under `/docs/manual/`. ![Repo layout. assets, docs, examples, media, remotion, scripts](https://raw.githubusercontent.com/michaelmonetized/omadesign/master/media/mark.png) On this machine the binary is `~/.local/bin/omadesign`, reporting `omadesign 0.0.4-alpha`. Install one-liner lives in `scripts/install-remote.sh`. The audience sits next to the Omarchy / Asahi tribe and the indie builders who already read Cargo.toml before they read the landing page. ![Motion persona. After Hours listening room, timeline keyframes, Make it move presets](https://michaelmonetized.github.io/omadesign/media/showcase/motion.webp) ## When **2026-08-26.** Spike as "Atelier v0.1". Rust + egui all-in-one. Prove the canvas before naming the product. **2026-08-28.** Rename to omadesign. glibc 2.35 link. Canvas handles, pen, live type, zoom-to-box. Phosphor + desktop theme. First lander and docs. **2026-08-29.** fontconfig enumeration, Google Fonts on demand, Max default. Palettes. Compound paths. Shape and asset browsers. Zig cross-compile for x86_64. Remotion hero. GPU texture reuse. First alpha-rc tags. **2026-09-02–03.** Motion timeline, Lottie, welcome that fits. QA pass on the early user-test list. **2026-09-05.** Precision guides, masks, healing. Fifty-two templates. Shortcut HUD. Portable brand libraries. Pen/type/logo film. Website rebuild. Catppuccin, studio tour, real recordings. **2026-09-06.** v0.0.1-alpha packages verified. Layered interop. Camera RAW. v0.0.2-alpha same day. **2026-09-07.** Selection and layer QA. Photo batch + presets to v0.0.3-alpha. Rotated node editing and context-menu flips to v0.0.4-alpha. **2026-09-08.** Sixty-eight commits from init. Four tagged alphas. Same-day docs experiment: a scoped First-File Setup offer went up and came back down; CHANGELOG keeps the record, app and license never changed. Thirteen days. That is the clock. ![omadesign mark. geometric design wordmark](https://raw.githubusercontent.com/michaelmonetized/omadesign/master/media/mark.png) ## Why Linux got my daily driver. The design suite did not. That gap is the reason. I needed one seat: vector precision, paint, RAW, light motion, without renting four subscriptions or babysitting an Electron process. I needed the chrome to follow my desktop colors and fonts, not a baked orange skin. I needed releases I can rebuild on the machine that ships them. So I linked glibc 2.35, zig-cross-compiled x86_64, uploaded tarballs with SHA-256, and kept shipping alphas until the handles behaved on rotated nodes. Own the toolchain. Build in public. omadesign is still alpha. advanced text layout, symbols, collab, PDF/X + CMYK are on the roadmap, not in the tarball. Affinity write is not there. RAW varies by camera. That is fine. The document model works. The personas share a layer stack. The downloads exist for ARM64 and x86_64 today. What would you put in the first `.oma` file if you sat down on a fresh Omarchy box tonight? --- # GetAt.Me: I replaced the link list with a relationship console Source: https://www.michaelchurley.com/blog/getat-me-relationship-first-link-in-bio Published: 2026-09-08 Author: Michael C. Hurley Tags: getat.me, link-in-bio, convex, clerk, nextjs, martech, creators, consultants, saas ![GetAt.Me landing. Turn your audience into fans & customers](screenshots/landing.png) ## Who I kept watching creators and consultants park their whole business behind a vertical stack of blue links. Linktree-class pages are fine for "here are my URLs." They are a dead end when someone is ready to book a call, leave a review, ask a question, or pay. That visitor opens five more tabs. Intent cools. The operator never sees the near-miss. GetAt.Me is for the people who already have attention and need a destination that behaves like a small CRM on a single handle. consultants, service-led shops, solo brands, creators who sell time and trust, not just clicks. If you build MarTech, ship Clerk + Convex stacks, or just hate bolting Calendly + Typeform + Intercom onto a bio link, this is for you. ## What I shipped an interactive landing page at [getat.me](https://getat.me). Claim a handle. Theme it. Drop links. Then unlock the relationship surfaces as you grow. Stack facts: **Next.js 16.1.6** (Turbopack) on Vercel, **Convex** for real-time data, **Clerk** for auth and billing (`has()` feature gates), Stripe through Clerk Billing, Resend for mail, Sentry + PostHog for the ops trail. Tailwind 4, Radix/shadcn, Phosphor icons. TipTap / markdown editor for posts. `@dnd-kit` for link reorder. Package version **0.1.0**. Public repo under HurleyUS. ![Features lander](screenshots/features.png) Nine themes live in the selector: Mocha, Frappe, Macchiato, Monokai, Tokyo, Tomorrow, One, Rosepine, Dracula. Links reorder by weight. Sections group them. Owners get an analytics dashboard. page views, link clicks, bookings, messages with PostHog and Convex events that ignore the owner so you do not inflate yourself. Plans from `.config/plans.ts`: Free starter (no card). Premium at $4.99/mo ($3.99 annual). Pro at $9.99 ($7.99). ProMax at $19.99 ($14.99). Pro is where booking, referrals, live chat, and conversion analytics harden. ProMax is payments, custom availability, rich posts with likes/replies/quote reposts/threads, and the verification surfaces. ![Owner profile edit view from the repo](screenshots/profile-edit-view.jpg) ## Where The product lives on the open web: [getat.me](https://getat.me). Profiles at `getat.me/{handle}`. Owner tools under `/{handle}/dashboard` and account routes. Marketing shell: features, pricing, FAQ, contact, privacy, terms, plus a small SEO blog cluster comparing link-in-bio options. Code: [github.com/HurleyUS/getat.me](https://github.com/HurleyUS/getat.me). Topics: `links`, `social`. Deploy path is Vercel continuous. no GitHub Release tarballs, because this is a hosted SaaS seat, not a desktop binary. The audience sits next to every creator tool thread that still treats a bio link like a footer. ![Pricing page](screenshots/pricing.png) ## When **2024-10-27.** Create Next App. Three commits. Then a long quiet. **2025-10-30.** "forming the profile." Themes (Tomorrow Night, Mocha/Frappe/Macchiato HSL). Clerk catch-all auth routes. Unlimited links gated through Clerk billing features. Convex provider switched to `@clerk/nextjs` so mutations stop lying about "User not found." **2025-11.** Ninety-eight commits in one month. Pricing page and PricingTable styling. Centralized plans config. Free / Premium / Pro / ProMax PlanInfo ladder. Features lander. FAQ, contact, footer, privacy, terms. Homepage hero that says the product out loud. **2026-01-31.** Posts grow a social graph. likes, replies, quote reposts, nested threads for ProMax. **2026-02.** Analytics dashboard. Click and view tracking for visitors only. Onboarding with live handle availability. Drag-and-drop link reorder. SEO sitemap/robots/manifest. Clerk webhooks for user.updated / user.deleted. Lucide to Phosphor. **2026-03.** Brand customization with live preview. QR and social share. Link sections. Next.js 16 and the middleware to proxy migration. Demo showcase profiles. SEO blog pages. **2026-05.** Blacksmith CI gates. Convex/Clerk build fallbacks. Sentry project routing. **2026-08-08.** `X-Robots-Tag: index, follow` on Vercel. **2026-09-08.** One hundred seventy-seven commits on the clock. PR #43 fixes profile ownership and Clerk billing fulfillment. PR #44 puts Bun on the production build. PR #45 includes the TypeScript packages production installs actually need. HEAD `0b96e39`. That is the journey from empty Next scaffold to a live relationship console with billing that fulfills. ## Why A bio link that only lists URLs trains your audience to leave. I wanted the stay. book the slot, send the referral, open the chat, leave the rating, pay when the work is ready. without exporting the visitor to a scavenger hunt. So I put the surfaces on the handle, gated them with Clerk plans, synced them on Convex, and kept shipping until ownership and billing fulfillment stopped being a customer-journey cliff. It is still 0.1.0. Custom domains and deeper analytics sit on the roadmap. That is fine. The document model for a profile already holds links, bookings, messages, posts, and referrals in one place. What would you put on `getat.me/yourname` first, the booking calendar, the live chat, or the three links you actually want people to hit this week? --- # Naarchy 0.4.0: Preferences, doctor, and the privacy pass Source: https://www.michaelchurley.com/blog/naarchy-0-4-preferences-privacy Published: 2026-09-03 Author: Michael C. Hurley Tags: naarchy, linux, hyprland, omarchy, gtk4, rust, privacy, preferences, clipboard, open-source ## Update — 2026-09-09 **0.4.0 is tagged.** [v0.4.0](https://github.com/michaelmonetized/naarchy/releases/tag/v0.4.0) is the Latest GitHub release. Commit `f95ce49`. Packages: `naarchy-aarch64-unknown-linux-gnu.tar.gz` (built on `m1pro16`), `naarchy-x86_64-unknown-linux-gnu.tar.gz` (built on `hpeliteclient`), source tarball, `SHA256SUMS`. GitHub Actions is still billing-locked, so the binaries were built on those two boxes and uploaded by hand, same as 0.3.3. Extract a package and run `bash scripts/install.sh`. Everything below is the 0.4 overhaul as I wrote it before the push. The "not tagged yet" lines were true on September 8. After the [Sep 3 island post](https://www.michaelchurley.com/blog/naarchy-linux-dynamic-island), I hardened Naarchy locally to **0.4.0**: native Preferences, `naarchy doctor`, atomic stores, travel opt-in, IPC cleanup. Validated on three machines. **Not pushed / not tagged yet.** GitHub tip remains **v0.3.3**. ![Naarchy 0.4 Home: Focus Timer and Now Playing, gear for Preferences](/blog/naarchy-0-4-preferences-privacy/screenshots/home.png) ## Who This one is for people already living in the island: Omarchy / Hyprland operators who installed Naarchy after the Sep 3 post and started trusting it with clipboard history, file drops, and calendar feeds. If you care whether state files are written atomically, whether the socket is mode 0600, whether travel estimates ask before they phone Nominatim, and whether `doctor` can tell you the session is sane without opening a GUI, you are the audience. Builders who ship local-first GTK tools. Multi-machine QA types who refuse to call a release done until it runs on more than the laptop that authored it. The origin story is already live. This post assumes you know what Naarchy is. ## What Local tip is **0.4.0**. GitHub tip is still **v0.3.3**. Say that out loud before anything else. 0.4 is the overhaul I validated on September 5 and have been running since: Preferences window, reduced motion, `naarchy --version`, read-only `naarchy doctor`, stricter CLI rejection, atomic clipboard/Inbox/Home state, feature-gated services, travel estimates behind an explicit opt-in, IPC/CLI cleanup, CI that drafts releases with checksums. Roughly fifty-one files, +4937/−4096 against `origin/main` @ `ef9cf87`. Uncommitted. Unpushed. No `v0.4.0` tag. ![Clipboard 0.4: newest-first history, pin, Clear history, gear for Preferences](/blog/naarchy-0-4-preferences-privacy/screenshots/clipboard.png) Concrete deltas that matter day-to-day: - **Native Preferences.** Gear opens a bounded floating window for appearance, motion, behavior, and feature controls. Advanced bits stay in `~/.config/naarchy/config.toml`. Appearance reloads live; feature flags and calendar feeds still want a restart. - **`naarchy doctor`.** Read-only desktop check: Wayland, Hyprland, session bus, daemon, config, timer sound, volume/brightness HUD detection. On this box it prints nine OKs and points at the config path. - **IPC honesty.** Second process talks JSON over `$XDG_RUNTIME_DIR/naarchy.sock` (mode 0600), waits for a bounded ack that the command entered the queue, not that the UI finished. Single-instance lock. Malformed durations exit 2. - **Privacy pass.** No telemetry (unchanged). Clipboard + shelf stay owner-only on disk; history is still **not encrypted** while capture is on (disabling Clipboard stops the watcher). Travel estimates (Nominatim / IPinfo|ipapi / OSRM) require opt-in. Clear Inbox does not delete originals. Duplicate drops rejected. Corrupt JSON gets a backup instead of a silent wipe. - **Atomic stores.** Clipboard, Inbox, and Home preferences write privately and atomically. Failed write keeps previous state. - **Perf sample (directional, 8s):** collapsed CPU 0.125% then below sample; expanded 0.625% then 0.375%; RSS roughly flat (~82 to 84 MiB collapsed, ~102 to 101 MiB expanded) vs installed 0.3.3. - **86 Rust tests.** fmt, Clippy `-D warnings`, debug + opt builds, smoke. Fixed a GTK 4.22.4 crash disposing a never-realized hidden window during monitor hotplug / prefs rebuild. Battery widget is already gone as of 0.3.3 (bar shows %, `hud battery` remains). 0.4 does not bring it back. ![Inbox 0.4](/blog/naarchy-0-4-preferences-privacy/screenshots/inbox.png) ![Widgets 0.4](/blog/naarchy-0-4-preferences-privacy/screenshots/widgets.png) ![Calendar 0.4](/blog/naarchy-0-4-preferences-privacy/screenshots/calendar.png) ## Where Same seat: Asahi Omarchy / Hyprland on the 16" M1 Pro (`m1pro16`), plus QA on `hpeliteclient` and `intelpro`. Local binary: `naarchy 0.4.0` via `~/.cargo/bin`. Local aarch64 dist archive exists under `target/release/dist/` with SHA256SUMS; that archive wants **glibc 2.39+**. Public GitHub Releases are still the 0.3.3 tarballs until push + remote CI succeed. Repo: [michaelmonetized/naarchy](https://github.com/michaelmonetized/naarchy). Origin story (Sep 3): [Naarchy: a Dynamic Island for Linux that owns the notch](https://www.michaelchurley.com/blog/naarchy-linux-dynamic-island). `doctor` on the machine that wrote this draft: ``` Naarchy 0.4.0 · desktop check OK Wayland session OK Hyprland integration OK Session bus OK Daemon OK Configuration OK File opening OK Timer sound OK Volume HUD detection OK Brightness HUD detection ``` ## When **2026-09-03.** Shipped 0.3.0–0.3.3 and published the island post. GitHub tip froze at `ef9cf87` / tag `v0.3.3`. **2026-09-05 (local).** 0.4.0 overhaul + `docs/VALIDATION.md`. Native prefs, doctor, atomic state, travel opt-in, IPC/CLI refactor. Desktop checks: calendar nav, 48h timer, prefs rebuild, 3× monitor hotplug, fullscreen hide/restore, notification queue. Three-machine QA: m1pro16, hpeliteclient, intelpro. **2026-09-08.** Still running 0.4.0 locally. Still dirty vs `origin/main`. Still no public release. This EXTEND draft is the trail for when that push lands, or for saying "RC on my boxes" if the story publishes first. ## Why The Sep 3 post proved the island. 0.4 is about trusting it with operator data. Preferences belong in a window, not only in TOML. A second process should not hang forever waiting for UI. Travel estimates should not quietly enrich a calendar event. A hidden GTK window should not crash on hotplug dispose. `doctor` should answer "is this session actually wired?" without expanding the panel. So I hardened the stores, gated the network, measured CPU against 0.3.3, ran eighty-six tests, and installed the same binary on three machines before calling the overhaul validated. Release gates that remain: push, remote GHA with Rust 1.92 pin, sustained everyday use, physical multi-monitor on the targets that matter. 0.4.0 is real on my desktops. It is not on GitHub until I push it. That gap is why this post exists as an EXTEND, not a rewrite. **Engagement Q:** If your island already sits in the camera notch, what should `naarchy doctor` check next that it does not check today? --- # I rebuilt the Omarchy site in TanStack Start in three days Source: https://www.michaelchurley.com/blog/omarchy-site-tanstack-start-rebuild Published: 2026-09-03 Author: Michael C. Hurley Tags: omarchy, tanstack-start, tanstack-router, typescript, vercel, hyprland, quickshell, dhh, linux, martech ![Omarchy hero end plate](https://raw.githubusercontent.com/michaelmonetized/omarchy-site-tanstack-start/master/public/assets/images/bg/home/hero-end.jpeg) ## Who I run Omarchy. Asahi. Hyprland. Quickshell draws the bar. Naarchy sits in the camera hole. The creative suite became omadesign. The public Omarchy site was still a static tree I had forked as omarchy-site. I ship other properties in TypeScript and TanStack. When the site I point people at does not match the stack I build in, I feel the seam every time I open a PR. This rewrite is for the operator who already lives on Omarchy and wants install, manual, news, and foundation raise inside a TanStack Start app. Also for anyone evaluating Start on a real content-heavy surface. I did not invent Omarchy. DHH and Omacom did. I rebuilt the site shell. ## What Repo on GitHub: michaelmonetized/omarchy-site-tanstack-start Live vercel.app deploy named after the repo. Stack is TanStack Start plus Router and React 19. Also Tailwind v4 and the shadcn UI kit. Root title stays the Omarchy brand line. ![Tokyo Night](https://raw.githubusercontent.com/michaelmonetized/omarchy-site-tanstack-start/master/public/assets/images/mocks/tokyo-night-preview.webp) Home plays Quattro first-boot, holds empty sky, then etches the mark. Below that: foundation raise chart, patrons, menu, themes, Hyprland, Quickshell, plugins. Manual: 52 chapters as typed modules. News: 16 posts. Install for PC, Intel Mac, and Apple Silicon community builds. ISO constant is 4.0.2. Import helpers pull HTML from the sibling static site checkout. ![Quickshell bar](https://raw.githubusercontent.com/michaelmonetized/omarchy-site-tanstack-start/master/public/assets/images/mocks/shell-bar.webp) Deploy config turns off git auto-deploy for main and master. Ship is deliberate. ## Where It runs on the Vercel preview URL. OS truth remains omarchy.org and omacom/omarchy. Import sibling on this machine: Projects/omarchy-site. App clone: Projects/omarchy-site-tanstack-start at d98a362. ![Four-way tiling](https://raw.githubusercontent.com/michaelmonetized/omarchy-site-tanstack-start/master/public/assets/images/mocks/navigation-fourway-tiling.webp) Audience sits next to Omarchy and Asahi people and next to builders already on TanStack Router. ## When **2026-08-31.** init scaffold. **2026-09-01.** Landing, layout, shell. Full manual chapters. Rest of site including PC and Mac install. Hero mark and empty sky etch. **2026-09-02.** Square patrons, 13M raise, Quickshell on home. Three commits on Safari etch. CTAs to install, manual, repo. Nav pinned right. **2026-09-03.** README plays the desktop walkthrough. Fifteen commits. About three days from scaffold to the URL that still answers. ![Clipboard history](https://raw.githubusercontent.com/michaelmonetized/omarchy-site-tanstack-start/master/public/assets/images/mocks/clipboard-history.webp) ## Why I needed the Omarchy public face in the same toolchain I use for everything else I ship. Static HTML is fine for the upstream project. My working copy wanted file routes, typed content modules, and a deploy I control. So I imported the manual and news, wired the raise chart, fixed the etch until Safari stopped lying about the canvas, and left auto-deploy off. The OS stays theirs. The Start app is mine to break and rebuild. ISO still downloads from omarchy.org. Plugins still live at omarchyplugins.com. This repo is the site rewrite as of September 3, 2026. --- Source: https://www.michaelchurley.com/blog/naarchy-linux-dynamic-island Published: 2026-09-03 Author: Michael C. Hurley Tags: naarchy, linux, omarchy, hyprland, open-source # I put a Dynamic Island on Linux Droppy is Mac-only. NotchNook is Mac-only. My 16" M1 Pro is running Omarchy. The camera hole was sitting there doing nothing. So I built **[Naarchy](https://github.com/michaelmonetized/naarchy)**. Native GTK4. Hyprland layer-shell. MIT. Not Electron. The idle island is **370×67** because that is the hole, measured on 3456×2234 at scale 1. When a timer is running it hangs **72px** with ears so the countdown sits on the glass, not in the webcam. ![Timer wrapping the camera](https://raw.githubusercontent.com/michaelmonetized/naarchy/main/docs/screenshots/v0.3/strip-timer-live.jpg) ## The island is the product Hover the top edge. A black glass card grows out of the notch. Timer on the left, media on the right, a dock underneath. Clicks only land on the capsule and the dock. Everything else at the top of the screen stays yours. That is `set_input_region`, not a slogan. ![Home](https://raw.githubusercontent.com/michaelmonetized/naarchy/main/docs/screenshots/v0.3/panel-home.jpg) The timer is a ruler. Scrub it. Let go. It starts. Click to pause. Click again to resume. When it hits zero the whole display strobes and an alarm loops until you click the flash, hit a key, or dismiss. The first version of that fire check was `remaining == 0 && running()`. `running()` requires remaining > 0. Dead code. It fires now. ## Drop a file anywhere on it GTK4 file managers send `GdkFileList`, not a `text/uri-list` string. We were listening for the string. Drops felt like they only worked on Inbox because that's the only tab that *looked* like a drop zone. Every tab accepts files now. Hold a file over the island and a dotted **Drop to Inbox** overlay fades in. Release and it jumps you to Inbox with a thumbnail grid. ![Inbox](https://raw.githubusercontent.com/michaelmonetized/naarchy/main/docs/screenshots/v0.3/panel-inbox.jpg) Collapsed, you get a stacked pile of the latest thumbs and a count. Timer still wins if one is running. ![Collapsed file pile](https://raw.githubusercontent.com/michaelmonetized/naarchy/main/docs/screenshots/v0.3/strip-files.jpg) ## The rest Clipboard history with search and pin. Calendar with ICS feeds, Meet/Zoom join, and "leave at 9:23." Volume and brightness HUDs. Omarchy theme follow. No telemetry. Socket is `$XDG_RUNTIME_DIR/naarchy.sock`, mode 600. ## Install ```bash sudo pacman -S --needed gtk4 gtk4-layer-shell rust git clone https://github.com/michaelmonetized/naarchy cd naarchy cargo install --path . --locked systemctl --user enable --now naarchy.service ``` Source, screenshots, and a 10-second recording: **[github.com/michaelmonetized/naarchy](https://github.com/michaelmonetized/naarchy)**. If you are still staring at a dead strip of pixels above your display, you already know what to do. — Michael C. Hurley --- # uncap.us: I rebuilt the social layer around the repo Source: https://www.michaelchurley.com/blog/uncap-us-repo-native-social-layer Published: 2026-08-31 Author: Michael C. Hurley Tags: uncap, devtools, tanstack, convex, clerk, git, cli, martech, opensource, vercel ![uncap.us Raycast-style signed-out landing — Your shortcut to shipped work](/blog/uncap-us-repo-native-social-layer/screenshots/landing-raycast.png) ## Who I ship software for a living. The repo is where the truth should live. It usually does not. Code sits in GitHub. Launch posts sit in a thread that dies in three days. Design decisions live as screenshots in Slack. Hiring signal is a PDF from last year. CI cost hides in a vendor dashboard. AI review output never becomes part of the project graph. uncap.us is for maintainers and operators who want that whole graph attached to the repository — not for people who need another place to perform. If you already think in owner/repo, org routes, and what shipped this week, you are the room. I also wrote the manifesto out loud: open source should not mean every byte is public every minute. Commits and branches are a bad primary primitive. Worktrees as we inherited them are an abomination. Agents and humans deserve a VFS that tracks work as it happens. ## What I built a repo-native social layer. Live site: www.uncap.us. App version in package.json: 0.2.0. Private product repo under HurleyUS. README-first repo pages at /:owner/:repo. Organizations that own repos. Issues and PRs. Explore. Dashboard. A Lens feed for launches, releases, jobs, design updates, and links tied to the project (Lens replaced the earlier wall naming). Pricing and docs. A downloadable Uncap CLI (UnGit) with device-flow login. ![README-first repository home from launch gallery](/blog/uncap-us-repo-native-social-layer/screenshots/repo-home.png) Origin sync connects public Git origins (GitHub, GitLab, Bitbucket, Codeberg, Forgejo, Gitea), caches shape, and reads files on demand. Native Uncap-hosted Git remotes are a separate rollout; empty states say that out loud instead of faking clone URLs. The CLI is the manifesto as a tool. uncap watch auto-tracks. Snapshots and virtual worktrees. Visibility flags for private/staging/public artifacts. Maintainer commands cover issues, pull requests, and continuous integration. Backend is Convex with Clerk auth. Stack at HEAD includes TanStack Start, Vite, pnpm, React 19, Convex, Clerk. Production deploys as tanstack-start on Vercel. That replaced an earlier Next.js plus Bun era; README badges lag the real stack. ![Origin code browser from launch gallery](/blog/uncap-us-repo-native-social-layer/screenshots/repo-code.png) ## Where Production is public even though the GitHub repo is private: www.uncap.us. Health at /api/health reports Clerk, Convex, PostHog, Resend, Sentry, and Stripe ready, with origin imports, native Git refs capability flags, and production telemetry enabled — verified 2026-09-08. Local clone on this machine: /home/michael/Projects/uncap.us @ ce53c96. CLI install assets under /cli. Brand kit under public/ including the u mark. The audience sits next to maintainers who already connect forges, and builders who read MANIFESTO.md before the pricing page. ![Explore — What's shipping today?](/blog/uncap-us-repo-native-social-layer/screenshots/explore.png) ## When **2026-01-08.** Init: Next.js, Convex, Tailwind. Early product was closer to a job-seeker platform loop than a forge. **2026-02.** Schema, Clerk auth, onboarding, job matching, then a hard security and auth hardening pass. **2026-05-04 to 06.** Rebuild as a social forge. Real services. Auth and repository creation. **2026-05-14 to 15.** Launch pack: screenshots, announcement drafts, runbook, health gates. Target drop May 15, 8:00 AM ET. Positioning locked. **2026-06-09 to 12.** Full app migration across nine phases. Legacy Next tree deleted. Downloadable CLI with device auth. Origin sync and Convex file cache. GTM integrations at the code layer. **2026-06-13 to 16.** Terminology: lens and repo. Marketing docs and signed-out home. Commit graph redesign. u mark lands in the brand set. **2026-08-08.** X-Robots-Tag set to index, follow on Vercel. **2026-08-30 to 31.** Signed-out landing redesigned like raycast.com. Command palette hero. Your shortcut to shipped work. Mobile signup overflow fixed. Duplicate Log in removed. AI-slop highlight pill removed. HEAD ce53c96. 338 commits from init to that tip. ![Historical May social-forge home from launch assets](/blog/uncap-us-repo-native-social-layer/screenshots/home.png) ## Why Git hosts optimized for storing files. The work I actually do spills sideways — launches, design versions, hiring signal, review output, deploy state. I wanted the repository to be the hub that holds that graph without turning the feed into performative noise. I also wanted source control primitives that match how I work now: auto-tracking instead of nagging myself to commit, granular visibility instead of all-or-nothing public, virtual worktrees instead of copying trees by hand for agents. So uncap.us is live with origin-aware repo pages, Lens, orgs, and a CLI on the site. Native hosted remotes are still a rollout, not a pretend clone button. The frame under the product changed once while shipping stayed continuous. Eight months. Three product shapes. One domain. The file cabinet was never going to grow the graph I needed. --- # Mack's BBQ Shack: I built a Main Street site that still feels like the pit Source: https://www.michaelchurley.com/blog/macks-bbq-shack-canton-condensation-site Published: 2026-08-30 Author: Michael C. Hurley Tags: macks-bbq-shack, nextjs, convex, resend, catering, local-business, canton-nc, martech, hustle-launch, framer-motion ![Mack's Shack homepage. Wood nav, shield logo, chalk value prop over pit fire](https://www.macksbbqshack.com/ribs-sack.avif) ## Who I build for operators. Sometimes that operator is me. Sometimes it is a pit crew on Main Street in Canton, North Carolina. Mack's Shack BBQ needed a site that looked like the shack, with a catering path that dropped leads somewhere durable when Chris was on the line. Guests needed hours, address, phone, and a menu that matched the board on the wall. Planners needed "mouths to feed" and an event date without playing phone tag first. I am the builder. Hustle Launch is on the footer. The food is theirs. The stack is mine to keep honest. ## What I shipped a private Next.js 16 App Router site. React 19. Bun. Tailwind 4. Biome. Framer Motion on the hero. Cabin Sketch for chalk. Source Sans for the rest. The homepage is a condensation-glass hero over looping pit video. Pointer clears fog. Mobile drops the canvas and keeps the video. Wood strip across the nav. Tomato catering CTA. Outline button into the menu board. ![Mack's Shack BBQ logo mark](https://www.macksbbqshack.com/logo.avif) The menu is a skeuomorphic blackboard: real blackboard texture, wood dividers with inset depth, taped Polaroids for brisket, pork, sides, banana pudding. Item copy lives in one content module so the Schema.org Restaurant JSON-LD and the chalk board drink from the same list. Wings brined, smoked, fried, tossed in Bama white. Mack Sack Combo. Family packs. Pick N Choose by the pound. Catering is a real form: name, email, phone, event date, mouths-to-feed slider, details. The API writes a Convex `cateringLeads` row and fires Resend from `Mack's Shack Bbq ` to `macks.shack.bbq@gmail.com` (CC me). Contact does the same with `contactSubmissions` and a "Message Chris" button next to Chris Lewallen's photo and a Maps embed. Sentry on the error boundaries. PostHog on the pageviews. Reviews via a Jotform widget that cannot take the page down with it. ![Brisket. Bark and smoke ring](https://www.macksbbqshack.com/brisket.avif) ## Where 366 Main St. Canton, NC 28716. Phone 828-631-2520. Hours Fri–Sun 11–7, Monday 12–7. Canonical URL is [www.macksbbqshack.com](https://www.macksbbqshack.com). Vercel also serves the project alias. Repo is private: [michaelmonetized/macksbbqshack-com](https://github.com/michaelmonetized/macksbbqshack-com). Footer socials hit Yelp, Instagram, Google Maps, Facebook. Footer credit names Hustle Launch for web design and app development. The audience for *this* write-up is builders who care how a local business site actually captures a wedding headcount, and anyone in WNC who already knows the Main Street pit. ![Pulled pork plate asset from the live site](https://www.macksbbqshack.com/pork.avif) ## When **2026-05-19.** Init. Condensation glass hero mockup. Mobile video fallback. Same day: live business copy, catering flow, hero refinements, mobile drawer, section photos. **2026-05-20.** Blackboard background. Wood textures. Hustle Launch footer credit. Hero CTA polish until the catering button went tomato. Chris on the contact page. Maps embed. Contact form + Convex persist. Menu Polaroids. Structured data. Sentry. PostHog. Social icons. **2026-05-22.** Merge PR #1 from `dev`. Deploy prod. **2026-06-04.** Launch prep. Button fix. **2026-06-27.** Phone update. **2026-07-12.** Dirty rice off the board. **2026-07-13.** SEO and custom error boundaries, twice, because the first pass lied. Google/Jotform reviews on about and the other pages. Script moved to the end of body so it stopped breaking the layout. **2026-07-29.** Sitewide vacation dialog for the Aug 2–3 closure. Cookie dismiss. Sticky banner. **2026-08-08.** `X-Robots-Tag: index, follow` locked in Vercel headers. **2026-08-30.** Ten commits titled `[fix] convex + resend connections`. HEAD settled. Sixty-seven commits from init. That is the clock. ![Nutter Butter banana pudding. Dessert Polaroid source](https://www.macksbbqshack.com/nana-puddin.avif) ## Why A Main Street BBQ needs the board, the hours, the phone, and a catering form that still works when the dining room is loud. I wanted the hero to feel wet and hot: condensation over real pit video. I wanted the menu to feel like chalk on wood. I wanted leads in Convex and in the inbox from a domain I control (`notify@uncap.us`). I wanted Schema.org to carry the same wings and family packs the chalkboard shows. So I linked the textures, wired the mutations, shipped the vacation banner before the holiday, and kept fixing the Resend path until August 30 stopped yelling. Operator stack. Local business. Own the notify domain. The repo is still private. The site is live. The Polaroids are still taped to the board. If you were standing at 366 Main tonight, which section of that chalkboard would you photograph first? --- # Omnux: I wired Linux for Apple silicon without lying about the GPU Source: https://www.michaelchurley.com/blog/omnux-linux-apple-silicon-truth-table Published: 2026-08-27 Author: Michael C. Hurley Tags: omnux, asahi, omarchy, apple-silicon, linux, m3, gpu, hyprland, martech ![OMNUX landing. curl install one-liner](screenshots/landing.png) ![OMNUX truth table. M1/M2 shipping, M3 experimental, M4 bring-up, M5/M6 nothing public](screenshots/truth-table.png) ## Who I run Linux on Macs. Omarchy. Asahi. The camera hole already has Naarchy. The install story for "buy an M3 in the store and walk out on Hyprland" did not. Upstream Asahi did the science. Caution is correct for a research project. Operators who want Omarchy-class polish still hit a wall of half-truths: installers that whisper "coming soon," GPU claims without pixels, M4 rows that pretend ADT dumps are optional. Omnux is for the person who already lives on Apple Silicon or is about to, and who wants the frontier packaged with a truth table that wins when marketing loses. M1/M2 daily drivers. M3 experimental owners who will share traces. Contributors and agents grinding DCP and AGX against real metal. Builders who read STEERING before the landing page. If you file issues with receipts and refuse vaporware, that is the room. ## What I stood up an integration-first umbrella: **omnux**. Not a mega-fork that absorbs the world. A monorepo that coordinates docs, steering, and component wiring. Kernel work lives in `linux/` on an `omnux` branch. Boot stack in `m1n1/`. Installer in the asahi-installer track. Packages in PKGBUILDs. GPU attempt in `omnux-gpu` under MIT. clean-room, hardware-trace driven, and honest that nothing is "working" until DRM render nodes exist on a physical M3. ![omnux monorepo layout. linux, m1n1, installer, pkgs, gpu, docs](screenshots/monorepo.png) One sentence of north star from GOAL.md: a person walks into an Apple Store, buys an M3 MacBook, and walks out running Omnux. Omarchy's Hyprland desktop on Asahi's foundation with everything working, installed without a single manual step. That sentence is not true yet. The definition of done is a checklist: install, boot, display, GPU, power, input, connectivity, audio, desktop meta package, recovery stick. M0 Foundation is checked. M1 "M3 installs are boring" is next: GPU acceleration is the moonshot long pole. Install today from macOS or recoveryOS: ```bash curl -fsSL https://raw.githubusercontent.com/michaelmonetized/asahi-installer/omnux/scripts/bootstrap-omnux.sh | sh ``` ![Install one-liner. curl bootstrap-omnux.sh](screenshots/install-command.png) M1/M2: daily-drivable, full acceleration on the Asahi baseline. M3: experimental track, `OMNUX_EXPERIMENTAL`, software rendering until the GPU and proper DCP land. NVMe, WiFi, Bluetooth, keyboard/trackpad, audio paths already in the working column. M4: bring-up patches ahead of upstream; blocked on physical ADT dumps. M5/M6: nothing public exists; September 22, 2026 customer availability is the earliest realistic dump date for the August 25 lineup. Sibling tooling: **omnux-report**. one command, offline-capable, consent + redaction, `.tar.zst` + SHA256. Validated on a real M1 Pro Omarchy box. SEP evidence collector added without scooping keys or biometrics. Live USB story: **omarchy-mx-mac-iso** S4 installer pipeline verified on loopback (plain + LUKS2, 51 assertions); hardware USB boot still open. STEERING is non-negotiable: truth is a feature, upstream first, integration not fabrication, local CI on own metal, MIT where we wrote it, user owns the machine, fire with receipts. Landing / truth table: [michaelmonetized.github.io/asahi-installer](https://michaelmonetized.github.io/asahi-installer/). ## Where It lives where Apple Silicon owners actually sit, and where the signed boot chain forces honesty. First install is still recoveryOS or macOS. Apple Silicon does not boot PC-style ISOs from cold. A USB stick becomes bootable after Linux is on internal storage. We track direct-USB-boot work; we do not pretend it shipped. Repo: [michaelmonetized/omnux](https://github.com/michaelmonetized/omnux). Landing and truth table on Pages: [michaelmonetized.github.io/asahi-installer](https://michaelmonetized.github.io/asahi-installer/). GPU siege issues on [omnux-gpu](https://github.com/michaelmonetized/omnux-gpu). Diagnostics: [omnux-report](https://github.com/michaelmonetized/omnux-report). On this machine the clone is `/home/michael/Projects/omnux` at `9b5e80d`. Builds and releases are meant to run on maintainer metal via Makefile + installer `build-local.sh`. no hosted Actions gatekeeper. Audience sits next to the Omarchy / Asahi tribe and anyone who already treats a SUPPORT matrix as sacred text. ## When **2026-08-25.** Monorepo init. README, GOAL, ROADMAP, STEERING, build targets. Submodules wired: kernel, m1n1, installer, pkgs, gpu. Same day Apple announces Mac mini M6/M5 Pro and Mac Studio M5 Max/Ultra. installer gate gains explicit M6 messaging; status snapshot notes September 22 as earliest ADT dump date. MX Mac live installer S4 lands in code with the asahi mkinitcpio hook fix that would have shipped every install without WiFi/GPU firmware. M3 GPU siege plan drafted; seventeen public issues filed. Announcement drafts get the marketing language stripped out. **2026-08-26.** omnux-report linked and validated at v0.1.0. complete redacted bundle on M1 Pro Omarchy, ten fixture checks green. Founder rewrite of the announcement; agent-army mandate recorded in GOAL (DHH / Omarchy community: deploy agents, owners share telemetry). TouchID confirmed on Omarchy for T1-chip TouchBar Macs by @0xBOYD3. first known Linux TouchID. SEP research program filed across omnux-gpu issues; report grows a `sep/` collector. **2026-08-27.** SEP enablement frontier verified from first principles against apstrand/m2-air-touchid work: `apple_sep` can bind, firmware stages, and still the AP cannot start the halted SEP. `CPU_CONTROL` read-as-zero, write-ignored. Lever is firmware / boot-policy, not a tidy Linux driver patch. Biometric `stac` reverse-engineering remains the multi-year wall. Sixteen commits from init to HEAD. Progress log closed the week with receipts. Three days of foundation. That is the clock so far. ![August 25–27 shipped snapshot](screenshots/progress-snapshot.png) ## Why Asahi proved locked silicon is not impossible silicon. Upstream moves at the speed of caution. I needed a shipping lane that compresses time-to-user for every public patch, and a voice that refuses to call software rendering a desktop. I wanted the truth table to beat the press release. I wanted M3 owners to install with eyes open: experimental, hot, battery-hungry until GPU and DCP land, and still useful for traces. I wanted diagnostics owners control, a live stick path rehearsed on loopback before metal, and a GPU project that stays MIT so upstream can take everything. So the umbrella exists. The curl bootstrap exists. The seventeen-issue siege map exists. The report produces a tarball with a SHA256. TouchID on T1 is celebrated without pretending M-series SEP is solved. Omnux is still early. M3 installs are not boring yet, Hyprland is not hardware-accelerated on M3, USB hardware boot is unverified, M4 needs ADTs nobody has dumped into our tree. That is fine. The steering holds. The receipts link. The silicon generations are labeled honestly. The truth table is for people who bought the Mac for the metal and stayed for the desktop. --- # WNC History Tours: I shipped the booking shell before the tour pages existed Source: https://www.michaelchurley.com/blog/omnux-report-one-command-diagnostics-redaction Published: 2026-08-27 Author: Michael C. Hurley Tags: omnux-report, omnux, asahi, apple-silicon, diagnostics, redaction, telemetry, sep, touchid, linux, omarchy ![OMNUX-REPORT OG: one-command diagnostics with consent + redaction](/blog/omnux-report-one-command-diagnostics-redaction/cover.png) ![Honest state: shipped, validated, unfinished named](/blog/omnux-report-one-command-diagnostics-redaction/screenshots/honest-state.png) ## Who I already published the Omnux truth table and the omnux-gpu siege wall. Those posts are live. This piece is the attachable evidence lane. This is for owners who will file a bug with receipts and refuse to paste a 400 MB journal that still contains a MAC address. For operators who need backlight, WiFi, DRM, speakersafetyd, and sleep lines before the archaeology starts. For SEP/TouchID researchers who need device-tree names and module hints without biometric material. For agent operators who are supposed to produce owner telemetry under the Omnux mandate with real consent, not theater. Curl install and the truth table live in the umbrella post. M3 AGX pixels with acceptance criteria live in the GPU post. An attachable evidence bundle the owner controls lives here. ## What I shipped **omnux-report**, a MIT Shell tool whose entire job is to make Apple Silicon Omnux machines comparable in an issue tracker without scooping secrets by default. One command. Offline. Installed system or live USB. Consent summary prints exactly what will be gathered. Redaction is on unless you opt out. The output is a single `.tar.zst` (gzip fallback if zstd is missing) plus a SHA256 sidecar. Attach both. ![Collection pipeline: Consent Collect Cap Triage Redact Archive](/blog/omnux-report-one-command-diagnostics-redaction/screenshots/pipeline.png) The pipeline is boring on purpose: 1. **Consent** list journal/sysinfo/SEP structure, optional benches, optional ADT helper; show redaction state; wait for `y` unless `--yes`. 2. **Collect** sysinfo, severity-scoped logs, SEP structure, optional benchmarks, optional ADT instructions. 3. **Cap** any single file over 50 MiB keeps its last 50 MiB with a TRUNCATED header so a pathological journal cannot produce an unattachable blob. 4. **Triage** `summary.txt` with PASS / INFO / WARN / FAIL / SKIP per subsystem. 5. **Redact** MACs to `XX:XX:XX:XX:XX:XX`; key=value secret backstops; count written to `redaction-count.txt`. 6. **Archive** deterministic tar (`--sort=name`, fixed mtime) compressed; checksum sidecar. ![Bundle schema v1: logs sysinfo sep benches adt skipped](/blog/omnux-report-one-command-diagnostics-redaction/screenshots/bundle-schema.png) The schema is documented in `docs/SCHEMA.md`. Logs arrive as current boot, previous boot, error, warning. Full history is opt-in because long-lived installs turn "helpful" into hundreds of megabytes. Sysinfo covers identity (model, compatible, chip ids), CPU/memory/kernel, package versions and Omarchy markers, device-tree `/chosen` **property names only**, block devices without serials, NVMe model+firmware, displays, Type-C roles, scrubbed network, audio/speaker-safety, GPU DRM/EGL state. Benchmarks default to CPU openssl + portable dd probes; glmark2 and vulkaninfo stay opt-in and always sit next to `benchmarks/context.txt` so a number without a model/kernel/mesa line is treated as noise. ![Redaction rules: scrubbed, never collected, kept deliberately](/blog/omnux-report-one-command-diagnostics-redaction/screenshots/redaction-rules.png) Redaction is the product feature, not the apology. Serial numbers, WiFi/Bluetooth keys, NetworkManager secrets, and `/etc/machine-id` are never collected. Interface addresses and SSIDs are scrubbed at collection time so a global regex cannot corrupt timestamps. Secret scrubbing matches only `key=value` / `key:value` forms. Journal prose like "Forward Password Requests to Wall" survives. Filesystem UUIDs stay because boot debugging needs them; hostnames stay because owners can edit a bundle before attaching if they care. The SEP section exists for the TouchID-on-Apple-Silicon research program tracked on omnux-gpu issues #10 and #13. It captures device-tree node names and `reg` bytes, `/dev` and `/sys` bus matches, module and modprobe hints, and firmware identifiers. It skips enrollment data, biometric templates, keys, tickets, nonces, TSS responses, and FDR dictionaries. Structure that helps match hardware. Nothing that reproduces security state. `--adt` does not dump an Apple Device Tree on the reporting machine. It writes guided instructions for a second host running m1n1's proxyclient, pre-filled with this machine's model, plus a scrub checklist before attach to omnux#4. ![Triage vocabulary: PASS INFO WARN FAIL SKIP](/blog/omnux-report-one-command-diagnostics-redaction/screenshots/triage-vocab.png) Triage is the first screen a bug reader should see: machine Apple-or-fail, DRM render node presence, backlight class, WiFi interface/soft-block, Omarchy release marker, mem_sleep states, speakersafetyd, SEP-named DT nodes. Honest and boring. On 2026-08-26 this produced a complete redacted 3.5 MB bundle on a real MacBook Pro 16-inch M1 Pro running Omarchy. The same day, `test/fixture-test.sh` ran ten checks against a synthetic Apple Silicon sysroot on ordinary Linux: model identity, compatible string, journal skip in fixture mode, triage PASS, ADT helper text, size cap, MAC scrub, secret scrub, prose survival, SHA256 verify. That is the receipt that the pipeline is more than "works on my journal." Still open: packaging into the mx-mac live image, glmark2/vulkaninfo validation on machines that actually have them installed, and an owner-facing release. Working tool. Validated. Unfinished product marketing. ## Where It lives where an Omnux or Omarchy Apple Silicon machine, or the live USB, can run a bash script without a network. Fixture mode lives anywhere Linux so CI cosplay is unnecessary for the collectors themselves. Repo: [michaelmonetized/omnux-report](https://github.com/michaelmonetized/omnux-report). Normative feature spec still points at [omnux#2](https://github.com/michaelmonetized/omnux/issues/2). ADT collection issue: [omnux#4](https://github.com/michaelmonetized/omnux/issues/4). SEP research adjacency: omnux-gpu #10/#13. Parent ship lane: [omnux](https://github.com/michaelmonetized/omnux). GPU siege: [omnux-gpu](https://github.com/michaelmonetized/omnux-gpu). Local clone used for this pack: `/home/michael/Projects/omnux-report` on m1pro16 @ `f9729a6`. ## When **2026-08-26.** Scaffold commit `1019bda`: honest state, omnux#2 pointer, layout, offline/consent/evidence principles. Same day: `aac9244` lands v0.1.0, working collectors, triage, redaction, archive. Same day validation: real M1 Pro Omarchy redacted bundle at 3.5 MB; fixture harness ten checks green. **2026-08-27.** Commit `f9729a6` adds the SEP/TouchID evidence collector wired to the omnux-gpu research issues: structure only, secrets excluded by design. Last push `2026-08-27T18:34:44Z`. **2026-09-08.** Content pack drafted. Live omnux and omnux-gpu posts already cover install truth and the AGX wall. This draft stays unique: diagnostics consent and redaction. Still no mx-mac packaging. Still no owner-facing release ceremony. ## Why Owner telemetry without owner control is just surveillance with a README. I wanted a command that tells you what it will gather before it gathers it. I wanted redaction as the default path so attaching a bug is not a privacy coin-flip. I wanted numbers that only mean something next to model, kernel, and Mesa lines. I wanted SEP research to have evidence without becoming a biometric vacuum. I wanted a fixture that proves the pipeline on machines that are not Apple silicon so the tool does not only exist in folklore. The Omnux story splits cleanly across three repos: the umbrella ships the install lane and the truth table; omnux-gpu names the driver wall without claiming pixels; omnux-report turns "please attach logs" into a consenting, redacted, checksummed artifact. That is the ask: run the command, read the triage, attach the two files. **Engagement Q:** When an owner files an Omnux bug, do you want a consenting redacted `.tar.zst` + SHA256 by default, or a raw journal dump they have to scrub by hand first? --- # slopops: I put my whole ops stack in one Omarchy bar popup Source: https://www.michaelchurley.com/blog/slopops-omarchy-ops-bar-panel Published: 2026-08-26 Author: Michael C. Hurley Tags: slopops, omarchy, qml, ops, tailscale, vercel, sentry, posthog, github, linux, martech ![slopops hero: fleet tab in the ops popup beside the one-icon five-tabs card](/blog/slopops-omarchy-ops-bar-panel/screenshots/app.png) ## Who I run a small fleet and a loud SaaS stack. Tailscale peers. Vercel production. Sentry unresolved. PostHog events. GitHub issues assigned to me. That used to mean five browser tabs I leave open like a superstition. Refresh. Squint. Miss the deploy that went ERROR while I was in a terminal. slopops is for the Omarchy operator who already lives in the bar and refuses another Electron tray for the same five APIs. Solo builders. MarTech people who ship on Vercel and still SSH into boxes. Anyone who already typed `gh auth login` once and does not want a sixth personal access token living in a random JSON file under `~/Library`. If your morning starts with "is the fleet up, did prod deploy, what is screaming in Sentry," this is the glance surface. ## What I built an Omarchy **bar-widget**. QML. Quickshell. Id `slopops`. Display name SlopOps. Version **0.1.0**. Category Monitoring. One instance. Default section: right. One icon. Nerd Font server glyph. Tiny badge dot in the corner. ![Fleet tab: peers, online count, ts / 22 / 5900 / t3 status lights](/blog/slopops-omarchy-ops-bar-panel/screenshots/fleet.png) Five tabs in a 620×450 popup: **Fleet**: every device on the tailnet. Online count. TCP probes for whatever ports you configure (default `22,5900`). Optional `t3Port` (default 3773) lights green when t3 serve is reachable on that peer's tailnet IP. **Deploys**: Vercel projects sorted by most recent production deploy. Anything with an ERROR since the last good deploy gets pinned to the top in pink. You see the fire before you open the dashboard. ![Deploys tab: failing projects pinned above READY rows](/blog/slopops-omarchy-ops-bar-panel/screenshots/deploys.png) **Sentry**: unresolved issues, last 24 hours, grouped by project, loudest first. A `+ issue` button runs `promote.sh`: open a GitHub issue in `issueRepo` with event count, level, culprit, and the Sentry URL, then `notify-send`. **Traffic**: PostHog all-time event total, a 30-day daily series, per-project totals down the side. **Issues**: `gh search` for open issues and PRs for you (or a configured `ghOwner`). ![Sentry tab: unresolved 24h groups with + issue promote](/blog/slopops-omarchy-ops-bar-panel/screenshots/sentry.png) The badge is the whole point when the popup is closed. Red if deploy errors or Sentry events. Yellow if tokens are missing or peers are offline. Green if nominal. `status.sh` rolls that up. Timer defaults to 90 seconds. Right-click the icon to refresh everything. Data plane is boring on purpose: bash scripts print JSON; QML `Process` parses a line. Tokens sit in `secrets.env` (gitignored, chmod 600). Missing a token does not crash the tab; it shows a hint and moves on. GitHub never needed a token in that file; it uses your existing `gh` login. Settings are widget keys, not forks: `omarchy bar set slopops fleetPorts "22,5900,3389"`, `sentryOrg`, `sentryUrl` for self-hosted, `posthogUrl` for EU, `vercelTeamId`, `issueRepo`, `t3Port 0` to hide the t3 light. ![PostHog traffic tab: all-time total, 30-day bars, per-project counts](/blog/slopops-omarchy-ops-bar-panel/screenshots/traffic.png) ## Where It runs where Omarchy's bar runs: Quickshell on the desktop I already stare at. Plugin path on this machine: `~/.config/omarchy/plugins/slopops`. Same tree mirrored under the dotfiles-omarchy project copy. Repo: [michaelmonetized/slopops](https://github.com/michaelmonetized/slopops). Public. Branch `master`. No Pages homepage. No GitHub Release tag. Linguist says mostly QML, then Shell, a little JavaScript helper for `ago` / `fmt` / `levelColor`. ![Repo layout: Ops.qml, tabs, scripts, secrets example](/blog/slopops-omarchy-ops-bar-panel/screenshots/repo-structure.png) The audience already has Omarchy, Tailscale, Vercel, Sentry, and PostHog CLIs and tokens. This is not a demo of what a deploy is. ![Issues tab: open issues and PRs via gh search](/blog/slopops-omarchy-ops-bar-panel/screenshots/issues.png) ## When **2026-08-26, 9:23 AM Eastern.** First commit. Message: `michael.ops 0.1.0: multi-service ops bar panel for Omarchy`. Twenty files. 1,780 lines. The whole panel lands in one shot: `Ops.qml`, five tabs, seven scripts, manifest, README, `secrets.env.example`. **Same morning, 9:55 AM Eastern.** Second commit. `rename plugin id to slopops`. Manifest id and name, README, moduleName/ipcTarget. `michael.ops` becomes the public name that matches the repo. **2026-08-26, 1:55 PM UTC.** Last push on GitHub. Still `0.1.0`. Still two commits. No LICENSE file in the tree. One star. **2026-09-08.** Pack day. Plugin still on disk. `omarchy bar put slopops` puts it on the bar. `secrets.env` is still not created on this machine; the degrade path is the live path until tokens land. Thirty-two minutes from first commit to rename. That is the clock. ## Why I was paying the context-switch tax every day. Fleet health in one place. Deploys in another. Sentry in a third. PostHog for "is anyone even using this." GitHub for the work the errors become. The bar was already the glance surface. Omarchy already had a widget schema. So I put the stack behind one icon and made the badge tell the truth when I am too busy to open the popup. Shell fetchers. JSON out. Tokens optional. Promote from Sentry to GitHub without leaving the seat. Ports as settings, not a new plugin fork. That is the tool I reached for when the dashboards started feeling like chores. **Engagement Q:** Which of the five tabs would earn the badge on your bar first: Fleet, Deploys, Sentry, Traffic, or Issues? --- # Hurleyus: I put Catppuccin Mocha in Hurley rally livery on Omarchy Source: https://www.michaelchurley.com/blog/hurleyus-omarchy-catppuccin-rally-theme Published: 2026-08-26 Author: Michael C. Hurley Tags: hurleyus, omarchy, catppuccin, mocha, theme, hyprland, asahi, u-boot, plymouth, linux, quattro, wallpaper ![Hurleyus hero. desktop preview beside Plymouth unlock](/blog/hurleyus-omarchy-catppuccin-rally-theme/screenshots/hero-desktop-unlock.png) ## Who I run Omarchy as the daily desk. Catppuccin Mocha is already the right palette: soft contrast, blue accent, pink errors, the whole mocha set. Stock Mocha still looks like everybody else's laptop. Hurleyus is for the Omarchy operator who wants Mocha without anonymity. For the Hurley / quattro / rally-art orbit who want the desk to match the brand. For Asahi Mac people who care that the unlock screen and the compiled-in U-Boot splash say the same name as the bar. For theme authors who already learned that shipping Foot or Ghostty palette files inside a theme is how you get a black terminal. If your Style menu is full of fine defaults and none of them feel like yours, this is the pack. ## What I built an Omarchy **theme package**. Id folder `hurleyus`. Display name **Hurleyus**. MIT. Public repo. Branch `main`. ![Desktop preview. Mocha chrome over rally wall](/blog/hurleyus-omarchy-catppuccin-rally-theme/screenshots/desktop-preview.png) **Palette.** `colors.toml` is the source of truth. Background `#1e1e2e`. Accent `#89b4fa`. Omarchy expands that into Foot, Ghostty, Kitty, Alacritty, btop, Chromium, shell chrome. README is blunt: do not ship terminal palette files in the theme. Those files block the templates. You get a black terminal. I have no interest in that support thread. **Window chrome.** `hyprland.lua`: 20px rounding, 20px gaps in and out, Mocha active/inactive borders. Editors: `neovim.lua` points LazyVim at Catppuccin; `vscode.json` names the Catppuccin Mocha extension. Icons: Yaru-purple. ![Repo layout. colors, lua, walls, branding, unlock](/blog/hurleyus-omarchy-catppuccin-rally-theme/screenshots/repo-structure.png) **Walls.** Seven 4K JPEGs, 3840×2160, pre-darkened so the bar and terminals stay readable. Titles in the gallery: Canyon run, Hairpin, The jump, Donuts, Tunnel blast, Service park, Last light. Cycle with `omarchy theme bg next` or Style to Background. ![Seven pre-darkened 4K Hurleyus walls](/blog/hurleyus-omarchy-catppuccin-rally-theme/screenshots/walls-gallery.png) **Boot.** `unlock.png` for Plymouth / SDDM. Set with `omarchy plymouth set by theme hurleyus` (same path Lumon uses). Branding ASCII for about + screensaver. Logo PNG. And the Asahi-specific piece: `branding/install-uboot-logo.sh` finds the 160×160 8-bit BMP slot inside `/usr/lib/asahi-boot/u-boot-nodtb.bin`, writes a patched copy under cache, runs `update-m1n1`. The splash that ships in U-Boot becomes Hurleyus. **Install.** Origin README still shows `omarchy theme install `. Pack-day local note (not pushed yet): Omarchy 4.0.2+ treats a nested `.git` directory as a stranger theme and drops `hyprland.lua` on install. Keep the Lua by cloning into `~/.local/share/themes/hurleyus`, symlinking into `~/.config/omarchy/themes/hurleyus`, then `omarchy theme set hurleyus`. A symlink counts as yours. A submodule's `.git` *file* also keeps the Lua. ![Install path that keeps hyprland.lua on Omarchy 4.0.2+](/blog/hurleyus-omarchy-catppuccin-rally-theme/screenshots/install-path.png) ## Where It lives where Omarchy themes live: `~/.config/omarchy/themes/hurleyus` on the machine I actually use. Same tree is the git checkout of [michaelmonetized/omarchy-hurleyus-theme](https://github.com/michaelmonetized/omarchy-hurleyus-theme). Boot path is Plymouth / SDDM for unlock, and Asahi's `u-boot-nodtb.bin` for the early splash. No Pages marketing site. No GitHub Release tag. Linguist is mostly the U-Boot install shell plus a little Lua; the walls are the byte mass. Audience sits next to Omarchy, Catppuccin, Asahi / Omnux Mac desks, and the quattro / Hurley brand lane. People who already know Style to Theme, not people who need a tutorial on what a wallpaper is. ![Boot unlock preview](/blog/hurleyus-omarchy-catppuccin-rally-theme/screenshots/unlock-preview.png) ## When **2026-08-21, 7:57 PM Eastern.** First commit. `Fork stock Omarchy Catppuccin into Hurleyus.` Twenty files. Mocha `colors.toml`, hyprland/neovim/vscode/icons, six PNG walls, branding ASCII, preview, MIT, README. **Same night, 8:09 PM Eastern.** Twelve minutes later. `Add Hurleyus unlock screen and Asahi U-Boot splash.` Unlock assets, BMPs, `install-uboot-logo.sh`. **2026-08-22, 6:36 AM Eastern.** `init`. Wall refresh, bigger about/screensaver ASCII, hyprland tweak. **2026-08-26, 5:58 AM Eastern.** `Package for distribution: 4K JPEG walls, README gallery, cleanup.` PNG walls out. Seven JPEGs in. Gallery in the README. Fat binaries gone so `theme install` is not a joke. Four commits. Still. Last push `65f4da3`. Zero stars. Local README has an unpushed install-path rewrite for the 4.0.2+ Lua drop. Pack day tells that truth instead of pretending origin already has it. ## Why Mocha was correct and still felt rental. I want one `colors.toml` driving the terminals and the rally art carrying the identity, not a fork of every emulator config. On an Asahi Mac the splash in U-Boot is part of the machine's face, and patching a 160×160 slot is a real receipt, not a settings-app wallpaper picker. Omarchy's theme system rewards packages that respect templates. I refuse to relearn the black-terminal lesson. Fork Thursday night. Unlock and U-Boot twelve minutes later. Slim JPEG distribution five mornings after that. Would you keep the nested `.git` and lose `hyprland.lua` on Omarchy 4.0.2+, or clone + symlink so the Lua stays yours? --- # omnux-gpu: I opened an MIT siege for M3 pixels and refused to call it a driver Source: https://www.michaelchurley.com/blog/omnux-gpu-mit-clean-room-m3-siege Published: 2026-08-24 Author: Michael C. Hurley Tags: omnux-gpu, omnux, asahi, apple-silicon, m3, gpu, agx, clean-room, mit, linux, reverse-engineering ![OMNUX-GPU, MIT clean-room siege for M3 pixels](/blog/omnux-gpu-mit-clean-room-m3-siege/cover.png) ![Honest state, scaffold only; no working driver](/blog/omnux-gpu-mit-clean-room-m3-siege/screenshots/honest-state.png) ## Who I am the person who already said the quiet part on the Omnux umbrella post: M3 installs today, software-rendered, and software rendering is not a desktop. This piece is for a narrower audience. M3 owners who will lend a machine for m1n1 proxyclient captures. Reverse engineers who already know Asahi's AGX story on M1/M2 and want the T603x/T8122 delta wall documented without contamination. DRM and Mesa people who treat "working" as render nodes plus glmark on metal, not a README adjective. License hawks who care that MIT stays clean before `src/` fills. Agent operators grinding capture loops under the same agent-army mandate that sits in Omnux GOAL. If you need an install one-liner or a diagnostics tarball, see the sibling posts. If you need the GPU long pole named as a project with acceptance criteria, this is it. ## What I stood up **omnux-gpu**, a standalone MIT repo whose value is honesty about what is missing. It is an attempt at a clean-room GPU driver for Apple M3-series silicon. License: MIT. Upstream may take everything; that is the point. The repository holds scaffolding, a research roadmap, tooling stubs, and eventually driver code. It does **not** yet contain a working driver. Nothing here claims otherwise until pixels appear on a physical M3. ![Repo layout, docs, tools, empty src, MIT LICENSE](/blog/omnux-gpu-mit-clean-room-m3-siege/screenshots/repo-layout.png) Three reverse-engineering walls stand between scaffold and acceleration: 1. **Command processor submission model.** How macOS userspace submits work to AGX firmware on T8122/T603x (differs from M2). 2. **Shader ISA deltas.** M3 brought Dynamic Caching, mesh shaders, hardware ray tracing; instruction encoding moved vs M1/M2. 3. **Firmware interface.** Version negotiation, queues, faults against shipping macOS AGX firmware (issues call 14.8.3+ as the field floor). ![Three RE walls, CP submission, ISA deltas, firmware interface](/blog/omnux-gpu-mit-clean-room-m3-siege/screenshots/re-walls.png) Each wall is only discoverable against real hardware, using m1n1's proxyclient tracing on a machine booted into macOS with instrumentation. Code written away from the metal is guesswork. The method is a loop: 1. **Capture.** Target M3 into m1n1 proxyclient from a second USB host; run macOS GPU workloads under `agx_*` experiments adapted for newer ABI. 2. **Diff.** Compare to known M1/M2 models; land deltas in `docs/`. 3. **Implement.** Clean-room sources in `src/` from documented behavior only. 4. **Validate.** kexec Omnux kernel with the driver; iterate until DRM render nodes exist and glmark runs. 5. **Publish.** Every milestone upstream to Asahi first. ![Method loop, Capture Diff Implement Validate Upstream](/blog/omnux-gpu-mit-clean-room-m3-siege/screenshots/method-loop.png) Fifteen public issues map the work. Issues #1–#8 are the GPU spine: capture harness, submission-model docs, ISA delta docs, firmware-interface docs, DRM skeleton (probe + firmware load + first render node **without** claiming acceleration), Mesa Honeykrisp/agx bring-up for G15-class, t603x power/PMP/thermal, and a local validation harness (kexec + glmark2/vkcube to signed JSON, own metal, no hosted CI theater). Issue #9 is the license immune system: `docs/CLEANROOM.md`, PR source disclosure, taint procedure, before any non-scaffold code lands. Issues #10–#15 park SEP/TouchID research on the same tracker so the wall is visible; the biometrics frontier and omnux-report evidence collectors are sibling stories. `src/` is empty on purpose. Contaminated contribution (decompiled Apple code, NDA headers, GPL-mixed paste) poisons the MIT claim. The audit process has to exist before the directory fills. ## Where It lives where M3 metal and a second USB host can sit on the same desk. Repo: [michaelmonetized/omnux-gpu](https://github.com/michaelmonetized/omnux-gpu). Parent umbrella wires it as `gpu/` under [michaelmonetized/omnux](https://github.com/michaelmonetized/omnux). Diagnostics sibling: [omnux-report](https://github.com/michaelmonetized/omnux-report). Logs, sysinfo, ADT helper, SEP collectors with redaction; useful for evidence, not a substitute for AGX traces. SoCs in scope: T603x / T8122 class. Capture target: shipping macOS firmware on those chips. Validation target: Omnux kernel kexec'd from m1n1 until `/dev/dri/renderD*` appears and the acceptance harness stops lying. If you have hardware to lend, the README says open an issue. That is the bottleneck. ## When **2026-08-24.** Single scaffold commit `a3b7f74`: MIT LICENSE, honest README, `docs/NOTES.md` placeholder, empty `src/`, stub `tools/`. Created the repo the day before the Omnux umbrella monorepo week. One commit. That is still the commit graph. **2026-08-25.** Siege map lands as public issues #1 through #9 while Omnux M0 is getting wired. Capture harness called the critical path. Clean-room audit filed as blocking-by-convention and cheap: do it before code, not after a lawsuit hypothetical. **2026-08-26 to 2026-08-27.** Issues #10–#15 add the SEP/TouchID research program to this tracker (T1 community win, T2 bridge, omnux-report SEP section, apstrand frontier notes). Useful adjacency. Still zero GPU capture artifacts under `docs/captures/`. Still no DRM skeleton. The calendar moved; the metal bottleneck did not. **2026-09-08.** This pack. Still scaffold. Still fifteen open issues. Still waiting on an M3-class machine in the harness loop. ![Issue map, GPU #1–#8, clean-room #9, SEP adjacency #10–#15](/blog/omnux-gpu-mit-clean-room-m3-siege/screenshots/issue-map.png) ## Why Omnux can already tell the truth about M3 installs. Truth without a GPU project is a permanent software-render sentence dressed up as a roadmap. I wanted a repo whose license Asahi can absorb without a negotiation. I wanted "working" defined as pixels and scores on metal. I wanted the three RE walls named so contributors argue about captures. I wanted the clean-room gate written before the first real C file, because MIT is easy to claim and hard to un-poison. The ask is not stars. The ask is hardware in the capture loop, and issue checkboxes that turn green only when the acceptance criteria say so. --- # twelveux: I hosted a shadcn registry and shipped Glass in one afternoon Source: https://www.michaelchurley.com/blog/twelveux-hosted-shadcn-registry-glass Published: 2026-08-15 Author: Michael C. Hurley Tags: twelveux, shadcn, glass, webgl, nextjs, catppuccin, max, martech, registry ![twelveux Glass playground, circle and NEW MATERIAL card](/blog/twelveux-hosted-shadcn-registry-glass/screenshots/playground.png) ## Who I still install UI the shadcn way. Copy the files. Own the code. The default face is still Inter. The default kit is still New York. That is fine for a starter. It is not the system I already run on uncap.us and hms: Max type, phi type scale, Catppuccin surfaces. twelveux is for operators who want that system as a registry namespace they can install. Frontend builders who already type `npx shadcn add`. People who remember twelve.ux from the WHATWG-era chrome. Anyone who wants liquid glass from the actual shader, not a CSS blur. ## What I built a hosted shadcn/ui registry. Next.js 15. React 19. Tailwind 4. Package version 0.1.0. Three installable items ship in `public/r` today: **max**, **theme**, **glass**. **Max** is the typeface. Local woff faces, weights 100 through 900, roman and italic. **theme** is phi type and spacing, Catppuccin light and dark CSS variables, Max as sans. Same grammar as uncap.us and hms. **Glass** is the first real component. Official pen.dev `glass.glsl`. Wrapper. Circle defaults baked in. GlassText is a second path with softer defaults for type. The playground is the product surface. Live circle. Live card. CIRCLE PROPS sliders. Same wrapper on a shadcn button and a navbar over a photo. ![2012 twelve.ux chrome mock](https://raw.githubusercontent.com/michaelmonetized/twelveux/main/public/og/mock-full.jpg) `/og` is the archive: exact 2009-2012 twelve.ux chrome. `/kit` is the same chrome under glass. ![glass kit](/blog/twelveux-hosted-shadcn-registry-glass/screenshots/kit-page.png) ## Where Live on Vercel: https://twelveux.vercel.app (playground, `/og`, `/kit`, and `/r` JSON). Repo: https://github.com/michaelmonetized/twelveux `registry.json` still says homepage https://twelveux.com. That name has no public DNS as of this writing. The host that answers is the Vercel app. ![scene backdrop](https://raw.githubusercontent.com/michaelmonetized/twelveux/main/public/scene.jpg) ## When **2009-2012.** twelve.ux chrome. The mock on `/og` still wears that date. **2026-08-15, 4:42 PM ET.** Initial commit: registry, Glass playground, Max fonts, og/kit mocks, built `public/r`. **4:44.** Vercel install freeze fix. **4:46.** Production typecheck fix for Glass. **4:58.** Phone playground. **5:15.** Accordion panes and price ribbon. HEAD `8b9773b`. Five Production deploys the same evening. Five commits. One afternoon. ## Why I already had a visual system (Max, phi, Catppuccin) living on uncap.us and hms as CSS and fonts. I could not `npx` it into a fresh app. I wanted Glass as installable files: the real shader and the circle props. I also still have the 2012 chrome in my head. studioTWELVE. twelve.ux. Putting `/og` and `/kit` next to the 2026 playground keeps the timeline honest. The registry is the portable form. The playground is the proof. The archive is the old chrome, still online. What would you install first on a greenfield Next app: `@twelveux/max`, `@twelveux/theme`, or `@twelveux/glass`? --- # HMS: I replaced WordPress + Elementor with a live site that is the editor Source: https://www.michaelchurley.com/blog/hms-hustle-management-system-live-editor Published: 2026-08-14 Author: Michael C. Hurley Tags: hms, hustle-management-system, page-builder, convex, clerk, vite-plus, wordpress-alternative, elementor, woocommerce, martech, blocks ![HMS public home mock. ship the site, edit the site](/blog/hms-hustle-management-system-live-editor/screenshots/public-home.png) ## Who I kept paying the WordPress tax. The other tax: Elementor for layout, Dynamic.ooo when Elementor ran out of road, WooCommerce when something had to sell, a pile of PHP templates that only one agency intern understood, and a publish button that still felt like deploying a missile. HMS is for operators who already know that stack and are done leasing their content model from it. Marketing teams that need a revision trail and a split-test variant without forking a theme. Builders who are happier in Convex + Clerk + Vite+ than in another page-builder SaaS seat. If you have ever opened a live URL, winced, and opened a totally different admin URL to fix one sentence, this is for you. ## What I shipped **HMS (Hustle Management System)**. The public site is the product. Pages, posts, products, forms, and templates are **block trees** stored as JSON. Signed-in, you append `?edit=1` and edit in place. Visitors keep the same URL. That is the whole thesis. Stack facts from the lockfile, not the README brochure: **Vite+** (`vp` for dev/build/lint/test), **React 19**, **react-router-dom 7**, **Tailwind 4**, shadcn + Base UI, **Zod 4** schemas driving **Convex** tables through `zodToConvexFields`, **Clerk** for auth, **Resend** as a Convex email action. Package version **0.0.0**. Public repo under michaelmonetized. Six commits. HEAD `6f8fa73`. ![Live editor three-panel mock](/blog/hms-hustle-management-system-live-editor/screenshots/live-editor.png) The catalog is blunt: Basics, Media, Layout, Proof, Dynamic. Hero, CTA, features, columns, stats, testimonials, FAQ, pricing, logos, collections, menu, search, add-to-cart, cart, forms, document-body. Collections pull pages, posts, products, or media into grid, table, carousel, gallery, or list. and can paint each row with a **loop item** blueprint. **Blueprints** are one kinded table (ADR 0001): component, form, loop-item, template. Templates carry `appliesTo`. header, footer, 404, search, blog archive/single, shop archive, product single. Tokens like `{{site.name}}`, `{{doc.title}}`, `{{cart.count}}` resolve with modifiers. Visibility groups AND/OR on field, schedule, role, referrer, cookie. Revisions snapshot on content update. Variants sit ready for split tests. Data layer: local store with seed data for offline/dev; Convex when Clerk + Convex env are real (`isLive`). Cart lines live in localStorage. Commerce key fields exist on site settings and products. treat full checkout as schema-ready, not a finished money path in this window. ![Manage pages workspace mock](/blog/hms-hustle-management-system-live-editor/screenshots/manage-pages.png) ## Where Code lives at [github.com/michaelmonetized/hms](https://github.com/michaelmonetized/hms). No homepage URL is set on the GitHub repo. this pack does not invent a marketing domain. Local path is `vp install` then `vp dev`. Manage workspace under `/manage/*`. Public surfaces: `/`, `/blog`, `/shop`, `/search`, plus typed entries and slugs. Audience sits next to every thread still arguing Elementor vs Webflow while the real cost is "two URLs for one sentence." Sibling mail default in the Resend action points at the `uncap.us` from-address. same operator family. ![Shop archive mock](/blog/hms-hustle-management-system-live-editor/screenshots/shop.png) ## When **2026-08-12, 11:17 ET.** `a62d31d` initialize. Vite+ scaffold, Max fonts, a full shadcn dump, hero PNG. Two hours later `07e6672` configured. **2026-08-13, 09:40 ET.** `d95789a` needs work. Convex pages, posts, products, media, sites, users. First page-builder. Manage shell routes. The CMS bones. **That evening, 20:57 ET.** `2404b20` Ship public site, live editor, revisions, and manage workspace. The thesis lands: the site is the editor. **2026-08-14, 10:34 ET.** `12835c6` new page builder. Catalog depth, finder, blueprints, collections, cart, theme templates. Four minutes later `6f8fa73` documents theme templates, search, cart, and library in the changelog. HEAD. Six commits total. Two per day for three days. ![Blueprint library mock](/blog/hms-hustle-management-system-live-editor/screenshots/library-blueprints.png) ## Why I did not want another theme marketplace. I wanted the visitor URL to be the edit surface, with block JSON on Convex, revisions you can restore, and variants you can assign. without opening a separate Elementor canvas that misrepresents the front end. So I compressed the escape into six commits: scaffold, configure, Convex bones, ship public+editor, deepen the builder, write the changelog. Version 0.0.0 on purpose. The README still talks like tanstack-start and zustand are still named there; the lockfile says Vite+ and react-router-dom. I would rather say that out loud than ship a brochure. What would you put on the first HMS page: the home hero, the shop archive, or the form that finally emails without a PHP plugin? --- # Best Jeep Decals: I shipped a dark-first vinyl storefront for Jeep identity Source: https://www.michaelchurley.com/blog/best-jeep-decals-convex-stripe-storefront Published: 2026-08-08 Author: Michael C. Hurley Tags: best-jeep-decals, nextjs, convex, clerk, stripe, ecommerce, jeep, vinyl-decals, martech, hustle-launch ![Best Jeep Decals. Upgrade Your Off-Road Identity](/blog/best-jeep-decals-convex-stripe-storefront/screenshots/landing.png) ## Who I kept seeing Jeep owners treat identity like an afterthought: a random sticker from a gas-station rack, peeling after one summer on a Wrangler hood. The people I built for already know their JL from their JK, their Gladiator JT from an XJ Cherokee. They want hood blackouts, "rated" fender badges, side graphics that look like they belong on the trail, not clip art. They want USA vinyl that lasts 5–7 years outdoors, air-release installs, and a checkout that does not feel like 2014 Magento. If you ship MarTech, Convex + Clerk + Stripe stacks, or local-commerce sites that have to take real money, this is for you. This is a private HurleyUS storefront (Best Jeep Decals), not a theme demo. ## What I shipped a dark-first e-commerce site at [bestjeepdecals.com](https://www.bestjeepdecals.com). Orange accent. Italic wordmark. Hero that says the quiet part: **Upgrade Your Off-Road Identity.** Made in the USA badge. Shop All Decals / Browse Wrangler. ![Shop filters. vehicle fitment and price range](/blog/best-jeep-decals-convex-stripe-storefront/screenshots/shop.png) Stack facts: **Next.js 16.2.6** (Turbopack) on Vercel, **React 19**, **Convex** for products/categories/carts/orders/reviews/wishlists/discounts/subscriptions, **Clerk** for auth, **Stripe** for checkout, webhooks, portal, and promo codes, **Resend** from `orders@bestjeepdecals.com`, **Sentry** + **PostHog** (+ GA). Tailwind 4, Phosphor icons, Zustand cart with persist + promo, Zod + React Hook Form on checkout/admin/newsletter. Package **0.1.0**. Private repo. Seeded catalog: **30** SKUs at **$29.99** across Hood Decals, Fender Decals, Side Graphics. Compass/Tread/Explorer/Star hood blackouts, Beach/Trail/Squatch/Rescue rated badges, Renegade mountain set, Zombie Outbreak Response Team badge, Since 1941 left/right, and the rest under `public/products/png`. Shop filters cover category, $0–$200 price range, and fitment checkboxes for Wrangler JL/JK/TJ, Gladiator JT, Cherokee XJ, Grand Cherokee, Renegade, Compass. ![Empty cart. trust path still on-brand](/blog/best-jeep-decals-convex-stripe-storefront/screenshots/cart.png) Admin lives under `(private)/admin` with `requireAdmin`. product CRUD (soft delete), orders, CSV export. Wishlist hearts persist in Convex. Reviews attach to products. Related products on PDPs. Billing defs include a **Decal of the Month Club** at **$14.99/mo** plus gift-card and rush-production add-ons. ROADMAP marks the Zod/RHF, Stripe+Resend, and Zustand modernization pass complete. ![Catalog collage from seeded PNGs](/blog/best-jeep-decals-convex-stripe-storefront/screenshots/catalog-grid.png) ## Where The storefront lives on the open web: [www.bestjeepdecals.com](https://www.bestjeepdecals.com) (apex redirects to www). GitHub homepage still lists the Vercel alias `bestjeepdecalscom.vercel.app`. Code: private [HurleyUS/bestjeepdecals.com](https://github.com/HurleyUS/bestjeepdecals.com). Public routes: shop, product, category, cart, checkout, wishlist. Auth under Clerk catch-alls. Robots.txt allows shop/product/category and blocks admin/api/sign-in/sign-up. Aug 8 added `X-Robots-Tag: index, follow` on Vercel so the deploy stops asking crawlers to ignore it. Audience sits next to Jeep forums, jamborees, and every Instagram wrap account that still cannot take a Stripe payment without a third-party form. ## When **2024-12-16.** Create Next App. Next day: shadcn init. Dec 27: a "ghostty released" commit. Three commits. Then silence through 2025. **2026-01-08.** The product actually starts. OPPORTUNITIES.md. Homepage pass. Migrate to convex-nextfaster. **Build MVP e-commerce storefront.** Seed script and `.env.example` for production readiness. **2026-02.** Superadmin email gate. then security move to a server component. Stripe webhook signature verification. Admin auth on product mutations. Cart wired with localStorage session and honest success toasts. Checkout rate limiting. Error and loading boundaries. Next.js thrash: bump to 16.1.6 for a CVE, temporary downgrade to 15.5.12 for middleware, Tailwind v3 to v4, dark-first production audit. **2026-03-09–17.** Next 15 to 16 for real. Delete deprecated `middleware.ts`. Restore Clerk on `proxy.ts`. Trust signals on cart. Sentry 10 for Next 16. Revenue unlock doc. Google Analytics. robots + next-sitemap. Stripe webhook + order payment flow. Admin dashboard for products and orders. Soft delete. Edit product. CSV export. Promo codes. Shop filters with category, price, Jeep model (#26). Reviews. Wishlists. Related products. Missing catalog images filled in. **2026-05-14–15.** Blacksmith CI gates on repeat until they stick. Convex URL and Clerk key fallbacks so CI builds do not cry. Deploy health check targets fixed. **2026-05-22–26.** Redesign commits. Package update PR #28. Sentry instrumentation. dx-maxxing PR #29. Broken images chased. UX passes. **Real catalog seeded** (`24758dd`). **2026-08-08.** Four commits to finish the clock: force `X-Robots-Tag: index, follow`, unblock that deploy in `next.config.ts`, pin Stripe to package default API version for build compatibility. HEAD `fd105bd`. **Ninety-nine commits** from empty Next scaffold to an indexable commerce deploy. On 2026-09-08 the live `/` sometimes hit the error boundary during capture while `/shop` and `/cart` still painted the dark chrome. Convex/runtime weather, not a rewrite of the journey. ## Why A Jeep without identity graphics is fine. A checkout that cannot take money, moderate reviews, or keep a wishlist is not a store. it is a mood board. I wanted the full path: seeded catalog, model fitment filters, Zustand cart with promos, Stripe webhooks that create orders, Resend that tells you it shipped, admin that can soft-delete a SKU and export CSV, CI that builds without secret theater, and a robots header that admits the site wants to be found. So I kept shipping until the redesign matched the trail aesthetic and the seed matched the PNG drawer. It is still 0.1.0. Decal Club and gift-card add-ons sit in billing defs waiting for the live Stripe dial. That is fine. The document model already holds products, carts, orders, reviews, wishlists, and subscriptions in one Convex project. What would you put on a Wrangler hood first: a compass blackout, a tread pattern, or a beach-rated badge that starts arguments at the trailhead? --- # reaferral: I built a free referral tracker so agents stop bleeding commissions into spreadsheets Source: https://www.michaelchurley.com/blog/reaferral-agent-referral-platform Published: 2026-08-08 Author: Michael C. Hurley Tags: reaferral, real-estate, referrals, convex, clerk, nextjs, martech, vercel, mdx, saas ![Reaferral Inman-style landing. BREAKING referral tracking hero](/blog/reaferral-agent-referral-platform/screenshots/home.png) ## Who I watch real estate agents make a quarter to a third of their income on referrals and still track those deals in spreadsheets, group texts, and vibes. That is not a CRM problem. That is a **referral accountability** problem. Who sent it. What fee was agreed. What stage the deal is in. Whether the check showed up when it closed. Reaferral is for licensed agents who are tired of losing money to ambiguity. Solo producers who want a link in the email signature that actually attributes. Team leads who need roles and a shared pipeline. Builders who already speak Next.js, Convex, and Clerk and want the vertical product without another Electron tray. If your week includes "did that referral ever close" and a half-finished Google Sheet. that is the room. ## What I built **Reaferral**: an agent-to-agent real estate referral platform. Stack on the wire: **Next.js 16.1.6** (App Router, Turbopack), **Convex** for the reactive backend, **Clerk** for auth, Tailwind v4, Radix/shadcn-style UI, Recharts, PostHog, Sentry configs, Resend, Stripe Connect routes. Bun monorepo. Workspaces: `web` (`reaferral-web` 0.1.0) and `mobile` (Expo `reaferral-app` 1.0.0). Hosted at [reaferral.vercel.app](https://reaferral.vercel.app). GitHub repo private under HurleyUS. ![Features page: product surfaces and mockups](/blog/reaferral-agent-referral-platform/screenshots/features.png) What ships in the product surface: **Referral tracking**. create, send, receive; fee percent or flat; status through the deal. **Trackable links**. short codes under `/r/[code]`, click analytics, campaign names. **Pipeline**. lead to active to under contract to closed (and payout adjacency in schema). **Teams**. slugs, invites, roles (owner/admin/financial/assistant/member), pending joins that survive signup. **Messaging**. per-referral threads in Convex. **Twelve dashboard themes**. neumorphism, gradient-wave, dark-pro, inman-style, minimal-light, plus brokerage-inspired skins (Keller Williams, RE/MAX, Zillow, eXp, Trulia), Raycast, Brutalist. **Content engine**. **423** MDX files under `web/content/stories/`. Linguist says MDX is ~76% of the repo by bytes. The landing still says "66+ Expert Articles." The filesystem disagrees in the agent's favor. ![Stories journal index](/blog/reaferral-agent-referral-platform/screenshots/stories.png) ![Example story. 47-second setup](/blog/reaferral-agent-referral-platform/screenshots/story-47.png) Public GTM pages: Inman-style editorial landing (orange LIVE strip, green REA**FERRAL** wordmark, navy stats band), Features, Stories, Investors. Auth: Clerk sign-in/up to `/dashboard`. ![Clerk sign-up](/blog/reaferral-agent-referral-platform/screenshots/sign-up.png) ![Investors page](/blog/reaferral-agent-referral-platform/screenshots/investors.png) Positioning in the ProductHunt/HN drafts on disk: core tracking **free forever**; premium later (follow-ups, agreements, payment tracking). I am not pretending the Stripe Connect routes mean the billing story is finished. ![Illustrative pipeline mock, not a live authenticated dashboard](/blog/reaferral-agent-referral-platform/screenshots/pipeline-mock.png) ## Where It runs on Vercel at **reaferral.vercel.app**. Clerk on the live deploy is still a **dev** instance (`pk_test_…`), the marketing pages render signed-out; the dashboard sits behind auth. Repo: [HurleyUS/reaferral](https://github.com/HurleyUS/reaferral). Private. Branch `main`. HEAD at pack time `8ba157a`. **501** commits. Zero GitHub Releases. MIT license file present (copyright line dated 2021). ![Monorepo map](/blog/reaferral-agent-referral-platform/screenshots/repo-structure.png) The audience sits next to agent networks, Inman-shaped media, and the same Convex/Clerk/Vercel family I use elsewhere, not next to "what is a referral fee" explainers. Chrome extension lives in a **sibling** repo (`michaelmonetized/reaferral-chrome-extension`). Not this pack. ## When **2024-08-07.** `init`. **2026-01-08.** Docs snap into focus: OPPORTUNITIES, PLAN, README. this is a real-estate referral platform, a tracked referral pipeline. **2026-02-04.** The real ship week. `feat: complete Convex backend and core web app`. GitHub repo created. Landing design variants. Vercel monorepo wiring. Clerk to Convex user sync. **2026-02-05–06.** Theme system expands. Features + Stories infrastructure. Investors, privacy, terms. Shared chrome. Mobile-friendly dashboard. **Late February through April.** The content firehose. Hundreds of `content: add new story` commits. 413 of them by pack count. Journal becomes the bulk of the tree. **2026-02-27 onward.** Security passes. auth checks on mutations, collect bounds, Svix verification on Clerk webhooks, console cleanup for production. **2026-05–06.** CI thrash (Blacksmith) then removal; promo banner overlap fix; nightly commits. **2026-08-08, morning Eastern.** X-Robots-Tag set to `index, follow`, then three unblock fixes so production deploy stops choking. Last push. That is the clock stop for this pack. **2026-09-08.** Pack day. Site up. `og-image.png` 404s. Draft only. no blog publish, no social blast, no git push from this task. ## Why Because referral income is real and the tooling agents use for it is mostly improvisation. Because I already had the Convex/Clerk/Next pattern and the missing piece was the vertical: links, fees, stages, teams, and a journal that attracts the people who feel the spreadsheet tax. Because shipping 423 MDX stories into a private monorepo is a weird flex and also a concrete SEO loop, even when the hero still says 66+. Because the badge of honesty here is the timeline: February MVP, spring content factory, August robots unblock, still private, still free-forever core, still no tagged release. What dashboard or sheet are you still using to remember who owes whom on a referral? --- # Your ZAXBYS: I built a franchise ops platform so the store and the above-store stop living in different spreadsheets Source: https://www.michaelchurley.com/blog/yourzaxbys-franchise-management-platform Published: 2026-08-08 Author: Michael C. Hurley Tags: yourzaxbys, zaxbys, franchise, nextjs, convex, clerk, resend, sentry, martech, ops ![Your ZAXBYS landing hero on Vercel](/blog/yourzaxbys-franchise-management-platform/screenshots/landing.png) ## Who I kept catching myself watching franchise operators bounce between a POS export, a Steritech PDF, a schedule spreadsheet, and a text thread that somehow became HR. I wanted the above-store view and the store-level pain in the same repo family: real auth, real email, and an honest note that the custom domain DNS still does not answer from my network. A generic restaurant dashboard template with fake charts would not cut it. Neither would a pitch that says multi-unit without a stores table. Your ZAXBYS is for multi-unit Zaxby's owners, above-store folks, and GMs who will sit with a CAP form. Operators who treat Clerk + Convex as a product decision. If you run MarTech by day and still care whether a food-safety observation has a written plan, this is for you. ## What I built a franchise management platform as a Next.js app with a marketing shell and a gated dashboard. Public surface: hero lander, features, pricing, about, testimonials, blog stub, contact, privacy. Clerk sign-in / sign-up. CTAs now point at `/signup`. That wiring was a March 1 fix, not day-one. Pricing page sells **Starter $99/mo**, **Professional $199/mo**, **Enterprise Custom**, plus add-ons ($25 per extra location, analytics, integrations setup, priority support). FinalCTA still shows a placeholder `(555) 123-ZAXBYS`. That is not a real phone bank. ![Pricing lander on Vercel](/blog/yourzaxbys-franchise-management-platform/screenshots/pricing.png) Private surface: `/dashboard` with stores, employees, schedule, audits, reports, settings. Convex schema is the spine: `employees` (roles from `franchise_owner` down to `team_member`, `eid` like `ZAX######`, **`ssnLast4` only**), `stores`, `schedules` + `shifts`, `caps` (food safety / RER with observation, solution, plan), `audits` (Steritech, health department, internal, RER), `salesData`, `feedback`, `notifications`. Resend routes send confirmation, invite, and notification mail from `notify@yourzaxbys.com`. Invite copy still names **Zaxby's Waynesville, NC**. The sibling store product is real. Stack on disk: Next **16.1.6**, React 19, Convex, Clerk, Radix + Tailwind, Sentry (`hustle-launch` / `shipthing`), PostHog provider, Bun lockfile, package **`zaxbys-franchise-management-platform` `1.0.0`**. `proxy.ts` is the Next 16 rename of middleware. Blacksmith `ship.yml` pulls Vercel env and Convex deploy keys. ![Illustrative dashboard composite from schema + routes](/blog/yourzaxbys-franchise-management-platform/screenshots/dashboard-composite.png) Honesty checks: the lander brags `500+` locations / `25%` cost reduction / `99.9%` uptime / `4.9★`. Those strings live in `app/page.tsx`. They are marketing copy, not a warehouse receipt. Unauthenticated `/dashboard` on the Vercel alias returned 404 at pack time, so there is no logged-in screenshot. README still tells you to clone `michaelmonetized/www.yourzaxbys.com`; the GitHub org is **HurleyUS**. AUTOPSY.md from February roasted a missing navbar and missing SEO files; HEAD has `manifest.ts`, `sitemap.ts`, `robots.ts`. Treat the autopsy as a scar, not the current build report. ## Where Code: [github.com/HurleyUS/www.yourzaxbys.com](https://github.com/HurleyUS/www.yourzaxbys.com). Public. Empty description. Empty topics. Zero stars. Live alias that answered HTTP 200 for this pack: [wwwyourzaxbyscom.vercel.app](https://wwwyourzaxbyscom.vercel.app). GitHub homepage field points there. Custom domain `www.yourzaxbys.com` did **not** resolve from the pack hosts (NXDOMAIN). Sibling store app: private [waynesville.yourzaxbys.com](https://github.com/HurleyUS/waynesville.yourzaxbys.com) with its own Vercel alias. Audience: franchise teams that want one login for labor, audits, and CAP follow-ups, and builders who know a SaaS pricing page without a `protect()` boundary is unfinished. ## When **2025-03-07 to 03-09.** First `init` commits under Michael Monetized. Fonts. Style passes. The classic "we gotta push to main to see minor changes yuck" loop. Repo created on GitHub 2025-03-08. **2025-10-02.** Another `init`, reboot marker on the timeline. **2025-10-15.** The YOLO pivot. Commit message literally: letting CodeRabbit and GPT5 duke out a refactor into a new project idea. Missing deps. Bun trusts. "Updated everything YOLO." "says ready for prod :shrug:" Accessibility vibing. That afternoon is when the franchise platform stopped being a mood and became a tree. **2025-10-16.** Marketing pages land. **2025-12-29.** CVE dependency passes. **2026-01-08.** `PLAN.md`: multi-store dashboard, unified reporting, document library, the above-store wishlist. **2026-01-31.** Big `chore: sync all changes`. **2026-02-04.** `Add complete franchise management dashboard`, the product-shaped commit. **2026-02-06 to 02-21.** Next 16 `proxy.ts` rename. Security: SSN off the wire, `ssnLast4` in schema, encrypt-at-rest notes in changelog, email domain / EID standardization (#11, #12). **2026-02-28 to 03-09.** TypeScript fix PR. Strip thirteen console statements. Wire CTAs to `/signup`. Bump Next to **16.1.6** for CVEs (#21). **2026-05-14.** Blacksmith CI gates standardized across a stack of commits. Lazy-init Resend clients. Skip Convex provider when public env is missing so the marketing shell does not die without a deployment. **2026-08-08.** `fix: set X-Robots-Tag to index, follow on Vercel`. HEAD `705473f`. Forty-six commits on the ledger. Pack day is September 8, 2026. Font fiddling to a versioned franchise ops platform with a live Vercel alias and a custom domain that still needs DNS honesty. ## Why I did not want the store and the above-store to keep living in different tabs forever. So I put employees, stores, schedules, CAPs, and audits in one Convex schema. I put Clerk in front of the dashboard and Resend on the invite path. I left the marketing stats labeled as marketing. I left the Waynesville invite copy as a breadcrumb to the sibling store app. I set package **1.0.0** knowing "1.0" here means the platform shape shipped, not that every `PLAN.md` checkbox is green. sitrep still says WIP and HIGH client priority. ROADMAP still wants owner UAT. That is fine. The repo has receipts in git: schema, auth, email, CI. Would you fix `www.yourzaxbys.com` DNS and run a real owner pilot next, or delete the fake 500+ lander stats before anyone quotes them as proof? --- # WNC History Tours: I shipped the booking shell before the tour pages existed Source: https://www.michaelchurley.com/blog/wnc-history-tours-booking-shell-before-detail-pages Published: 2026-08-08 Author: Michael C. Hurley Tags: wnc-history-tours, western-north-carolina, asheville, tour-booking, local-directory, nextjs, convex, clerk, stripe, martech, heritage-tourism ![WNC History Tours homepage mock: Discover the Rich History of Western North Carolina](/blog/wnc-history-tours-booking-shell-before-detail-pages/screenshots/home.png) ## Who I live in the Blue Ridge ops lane. Visitors want a walking tour, a ghost walk, a cemetery afternoon, or a Cherokee heritage day without bouncing between five operator websites and a generic Viator card. Guides want a place to list and take a booking without building their own Next app. WNC History Tours is for people hunting history experiences from Asheville to Cherokee, and for the small companies that run those walks. If you build MarTech, local directories, or Stripe-backed booking surfaces, this post is the field notes on what shipped and what did not. ## What I built a tour booking platform shell for Western North Carolina history tours. Stack on the box: **Next.js 16.2.6**, React 19, **Convex**, **Clerk**, Stripe (dependency + env slots), PostHog, Sentry, Resend, Tailwind v4, Bun, Vitest, Vercel config. Repo is private under `HurleyUS/wnchistorytours.com`. Package version **0.1.0**. README still says Next 15.5.6; the lockfile moved on. ![Convex schema: companies, tours, bookings, reviews](/blog/wnc-history-tours-booking-shell-before-detail-pages/screenshots/schema.png) What exists in code: - Amber/stone homepage with search form, city chips, category cards, featured tour grid - Convex tables for `companies`, `tours`, `bookings`, `reviews` with search indexes - Tour queries (featured, search, by company/slug) and booking create/status mutations - Seed mutation for three demo companies and five tours - Clerk middleware in `proxy.ts` with public matchers for the routes I planned to build - SEO (`sitemap.ts`, `robots.ts`), error boundaries, HTTP security headers, Blacksmith/shipprep CI gates What does **not** exist as `app/` routes despite the nav and cards linking to them: `/tours`, `/companies`, `/city/*`, `/category/*`, `/search`, `/tour/[company]/[tour]`, `/list-your-business`, booking checkout. No `components/` folder. No Stripe checkout route. Homepage falls back to hardcoded demo tours when Convex queries are still loading or empty. Category counts (24 walking, 12 ghost, …) are UI constants. The seed script drifts from the schema (`rating` vs `averageRating`, duration type, missing required company fields), so production readiness in that commit message is aspirational. ![Declared stack vs actual app tree](/blog/wnc-history-tours-booking-shell-before-detail-pages/screenshots/stack-gap.png) ## Where Intended host: [wnchistorytours.com](https://wnchistorytours.com). Layout OG, sitemap, and robots all point there. On pack day the domain returned **Cloudflare 526** (origin TLS/unreachable). `vercel.json` also sets `deploymentEnabled` for `main`/`master` to **false**; auto-deploy from those branches is off. Auth path is Clerk. Money path is declared Stripe in `.env.example`. Mail is Resend. Errors go to Sentry. Product events go to PostHog. Maps key slot exists for meeting points later. Audience sits in Western North Carolina heritage tourism and with builders shipping local vertical directories next to products like BestWNC. ## When **2026-01-08.** Init. Same day: opportunities doc, homepage pass, Convex tour booking MVP (schema + bookings + tours + provider), PLAN updates, `.env.example` + seed. Six January commits. The product idea and the shell landed together. **January 31.** Chore sync. **February.** Upgrade to Next.js 16 and React 19. Tailwind v4 CSS-first. Clerk middleware and `cn()`. Strict TypeScript. Error boundaries. Sitemap and robots. Security headers. Auth check on `bookings.updateStatus`. Query `.take(100)` bounds. TypeScript fix PRs. **March.** Vitest smoke coverage. More TS cleanup. Remove GitHub Actions; Vercel called out as CI. ESLint flat config. **May.** Densest month (15 commits). shipprep standards, Blacksmith CI gate standardization (many near-duplicate gate commits), local preflight, deploy URL and health-check fixes. Platform reliability work while detail pages stay unbuilt. **2026-08-08 6:55 AM ET.** `c298e54` set `X-Robots-Tag` to `index, follow` on Vercel. HEAD. Thirty-seven commits on `main`. That is the clock from empty repo to a hardened shell with an unfinished booking funnel. ![Commit journey Jan through Aug](/blog/wnc-history-tours-booking-shell-before-detail-pages/screenshots/journey.png) ## Why Directories and booking marketplaces fail two ways: vapor landers with no schema, or beautiful CI with no product routes. I did the honest middle (real Convex models and a real lander), then spent spring on Next 16, Clerk, SEO, and Blacksmith while the tour detail and Stripe checkout pages stayed on PLAN.md. The domain is named. The robots header asks to be indexed. The origin was 526 when I checked. That gap is part of the story. Sibling context: BestWNC is the wider WNC business directory. This repo is the narrower history-tour vertical. Same region, different job. If you run walking tours in Asheville or Cherokee, would you list on a WNC-only history board, or is Viator still the only checkout that matters? --- # modern-design-playground: I left agents running for ten hours and they rebuilt nine worlds Source: https://www.michaelchurley.com/blog/waynesville-zaxbys-single-store-ops-portal Published: 2026-08-08 Author: Michael C. Hurley Tags: waynesville, yourzaxbys, zaxbys, single-store, nextjs, convex, clerk, steritech, ops, martech, wnc ![Waynesville public lander composite from repo event assets](/blog/waynesville-zaxbys-single-store-ops-portal/screenshots/landing-composite.png) ## Who I kept watching one store, 424 Russ Avenue, Waynesville, NC, try to be three products at once: a community lander for locals and tourists, a hiring funnel, and a shift office that still lived in spreadsheets, Steritech PDFs, and whoever answered the group text first. This is not the multi-unit franchise SaaS story. That sibling already has its own pack. This one is for the GM and shift leaders who need lunch daypart labor against a ≤17% goal before the dinner rush, and for the parent looking up Kids Night on the same domain. If you run MarTech by day and still care whether a CAP finding has an observation, cause, prevention trail, this is for you. If you only want a pricing page with fake "500+ locations," read the franchise platform post. ## What I built a **single-store** Zaxby's management and customer portal. Public surface (`app/(public)`): home with local events (Wheelin' Wednesdays, bounce party, Kids Night), about, careers + apply, catering (redirects to corporate catering), community, events, menu (redirects to zaxbys.com menu), contact. Store truth lives in `_project.ts`: **424 Russ Avenue**, Waynesville NC 28786, phone **828-456-2888**, email `eat@waynesville.yourzaxbys.com`, hours 10:30–21:00, dayparts lunch / snack / dinner / late. ![Ops map, public, shift, people, numbers](/blog/waynesville-zaxbys-single-store-ops-portal/screenshots/ops-map-composite.png) Private surface: `/dashboard` with live shift, performance, metrics (including SOS and SMG pages), goals and 6-week trends, hiring Kanban, schedule + swaps + time-off, daypart checklists, Steritech CAP reports, training sessions + Zaxby's University tracking, attendance + points, leadership scores, maintenance, uniforms/smallwares orders, announcements, events CRUD, and CSV ingest under `/dashboard/injest`. Convex is the spine: thirty-plus modules. Employees, applicants, shifts, metrics daily/weekly/periodic, caps, checklists, training/ZU progress, attendance points, orders. Package name matches the host: **`waynesville.yourzaxbys.com` `0.1.0`**. Stack: Next **16.1.6**, React 19, Convex, Clerk, Resend + React Email, Sentry, PostHog, Radix + Tailwind 4, Bun, Blacksmith `ship.yml`. ![Illustrative live-shift dashboard composite](/blog/waynesville-zaxbys-single-store-ops-portal/screenshots/dashboard-composite.png) Honesty checks: sitrep still whispers WIP and "maybe superseded by www." HEAD has more store-ops depth than the franchise sibling. Custom domain `waynesville.yourzaxbys.com` did **not** resolve from pack hosts. Vercel alias answered with a **429 bot challenge**, so there is no clean live screenshot and composites are labeled. README growth notes and franchise-purchase storytelling are provenance in the repo. AUTOPSY roasted missing SEO files; HEAD has `manifest.ts`, `sitemap.ts`, `robots.ts`. ## Where Code: [github.com/HurleyUS/waynesville.yourzaxbys.com](https://github.com/HurleyUS/waynesville.yourzaxbys.com). **Private.** Empty description. Empty topics. Zero stars. GitHub homepage field: [waynesvilleyourzaxbyscom.vercel.app](https://waynesvilleyourzaxbyscom.vercel.app). Alias host resolves; HTTPS challenged at pack time. Canonical claim `https://waynesville.yourzaxbys.com/` sits in `_project.ts` and README. DNS unresolved here. Sibling franchise platform (already packed): [www.yourzaxbys.com](https://github.com/HurleyUS/www.yourzaxbys.com). Same family. Different job. Platform sells above-store; this repo runs **one** store's public face and back-office. Audience: independent franchisees who need store software that knows Russ Avenue dayparts, and builders who can tell a "restaurant dashboard" template from a Steritech CAP form. ## When **2025-05-02.** Create Next App to init to first Vercel deploy. Repo created on GitHub the same day. Package starts as a location product. **2025-05 to 08.** Fonts, navbar, employee headway, image upload fights, ranking UI. The slow work of making a store site feel like a store. **2025-10-16 to 10-21.** Security and Convex hardening week. Secure server-side SSN verification. External-link `rel` discipline. Full-text employee search. Build compilation fixes for production. **2026-01-03 to 01-04.** Dashboard redesign: Spotify Wrapped energy, scroll snap, metrics tables that calculate labor %, LY comparisons, placeholder rows for the current week, double-click cell edits, charts with 6-week averages and goals. **2026-01-08 to 01-09.** The suite ships in a day: CSV import/reporting, employee onboarding/self-registration, Indeed hiring Kanban, live shift dashboard, shift checklists, announcements, scheduling, uniforms/smallwares, Steritech CAP, training agendas, attendance tracker, then a training rebuild with scheduled sessions and ZU course tracking. **2026-01-12.** Delete buttons and missing-week detection on metrics. Practical GM requests. **2026-02-15 to 02-26.** Security that matters: remove hardcoded encryption key fallback; fix proxy middleware that had made routes public; encrypt SSN at rest AES-256-GCM (#34/#41); strip SSN context from console logs (#43/#50); enforce auth on Convex public mutations (#42/#51). **2026-03-21.** Remaining `console.*` to Sentry logging (#53). **2026-05-14.** Blacksmith CI gates standardized across a stack of commits. Clerk/email client check fixes. Lazy Resend. Skip Convex provider without public env. **2026-08-08.** `fix: set X-Robots-Tag to index, follow on Vercel`. HEAD **`c9f97b7`**. Two hundred thirty-five commits on the ledger. Pack day is September 8, 2026. May Create-Next-App on Russ Avenue to a versioned single-store ops portal that still wears package **0.1.0**. ## Why I did not want one location's public community face and its shift office to keep living on different planets. So I put events and careers on the same origin as live dayparts, Steritech CAPs, hiring, training, and attendance. I put Clerk on the door and Convex under the floor. I left package **0.1.0** because "store software that GMs touch" is not the same milestone as a franchise SaaS **1.0.0** marketing claim. I left the sibling relationship explicit: www is the platform pack; this is the store pack. sitrep can keep saying LOW priority. The git log disagrees with "empty." The schema disagrees with "template." Would you point real DNS at this alias and run a week of live shift entry next, or keep the franchise platform and the store portal honest as two products with two jobs? --- # The National NC: live NC news feeds before the bias AI ships Source: https://www.michaelchurley.com/blog/thenationalnc-live-rss-before-bias-ai Published: 2026-08-08 Author: Michael C. Hurley Tags: thenationalnc, north-carolina, news-aggregator, media-bias, rss, nextjs, convex, clerk, catppuccin, martech ![The National NC homepage: LIVE Latest NC News](/blog/thenationalnc-live-rss-before-bias-ai/screenshots/home-live.png) ## Who I got tired of opening five tabs to see how the same North Carolina story landed in Charlotte, Raleigh, and the wire services. then pretending I had "read around." Most "balanced news" products either editorialize or hide the sausage. I wanted a pipe: aggregate first, label leanings when we can prove them, compare coverage when the model earns the pixel. The National NC is for NC locals and remote watchers who want AP, Reuters, and regional headlines in one dark UI, and for builders who will tolerate an honest placeholder where the AI comparison card still says "coming." If you ship Next + Convex + Clerk and care about media literacy without a cable-news costume, this is for you. ## What Live at [thenationalnc.com](https://www.thenationalnc.com/). Private repo [HurleyUS/thenationalnc.com](https://github.com/HurleyUS/thenationalnc.com). Package **0.1.0**, Bun, Next.js **16.2.6**, React 19, Tailwind 4, Catppuccin Mocha by default. Stack in the lockfile: Convex, Clerk, PostHog, Sentry, Resend, Radix, lucide, next-themes, Vitest. Payments README still says N/A; freemium (ad-free / advanced comparison) is in the opportunities doc, not a Stripe catalog. ![News feed. Live / Editorial](/blog/thenationalnc-live-rss-before-bias-ai/screenshots/news.png) What actually works today: - **`GET /api/news`** pulls Google News RSS. AP site query, Reuters site query, NC `when:3d`. parses items, filters with 30+ NC city/region/team keywords, dedupes by title similarity, caches **15 minutes** in process memory. - Homepage embeds a **LIVE** feed with source tabs (All / AP / Reuters / NC News) and refresh. - `/news` defaults to Live; Editorial tab still serves eight demo NC articles with Left / Center / Right badges. - `/news/[id]` renders demo content + related sidebar + an **AI comparison placeholder**, not a live model call. - Convex `schema.ts` defines articles, sources, comparisons, categories (optional embeddings). Deployment not initialized; demo data still wins. On 2026-09-08 the live API returned **50** NC-relevant headlines in one sample (WXII, Citizen Times, Carolina Journal, WRAL, ABC11, NYT, …). AP/Reuters tabs can show zero when those fetches abort. 8s timeout per feed, independent failure. ![About. mission and L/C/R source lists](/blog/thenationalnc-live-rss-before-bias-ai/screenshots/about.png) ## Where Public product: [www.thenationalnc.com](https://www.thenationalnc.com/). Vercel alias `thenationalnc-com.vercel.app`. Routes: `/`, `/news`, `/news/[id]`, `/about`, `/api/news`. Robots allow site, disallow `/api/`. Sitemap is thin (homepage-weighted) as of last deploy. Code stays private under HurleyUS. Host Vercel; Blacksmith/prebuilt CI path; `vercel.json` disables git auto-deploy on `main` and sets `X-Robots-Tag: index, follow`. ## When **2026-01-08.** Scaffold Next + Convex + Tailwind. PLAN and OPPORTUNITIES spell the bias-comparison dream before a single feed parses. **2026-02-06–08.** Next 16.1.6, Tailwind v4 CSS import fix, wire dead homepage buttons, yank Clerk off the homepage when env keys are missing. **2026-02-13.** The real jump: Catppuccin dark mode, shared layout, demo news + article pages, Convex schema, and **live NC feeds** through Google News RSS (`44dfae3`). That is when the site stopped being a brochure. **Late February.** Clerk middleware, error boundaries, sitemap/robots, security headers, console to Sentry cleanup. **March.** Vitest smoke coverage. **May.** shipprep / Blacksmith CI standardization and deploy URL verification. lots of "Standardize Blacksmith CI gates," little product surface change. **2026-08-08.** HEAD `bfb6723`. robots index/follow header on Vercel. Thirty-three commits on the clock. ## Why Bias detection without articles is a slide deck. I wanted the NC headline pipe in production first: cache, filters, source badges, dark UI. so when OpenRouter (or whatever) comparison lands, it has real URLs to argue about. The gap is intentional and visible: leaning badges on demos, placeholder comparison card, Convex not live yet. Better than fake AI chrome. When the comparison model finally ships, which NC story do you want side-by-side first: legislature, weather disaster, or Carolina basketball? --- # s12.in: I shipped a short domain that tracks clicks and hosts files Source: https://www.michaelchurley.com/blog/s12-in-url-shortener-file-hosting Published: 2026-08-08 Author: Michael C. Hurley Tags: s12.in, url-shortener, file-hosting, convex, clerk, nextjs, analytics, vercel, chrome-extension, martech ![s12.in landing. Short links, powerful results](/blog/s12-in-url-shortener-file-hosting/screenshots/landing.png) ## Who I got tired of leasing short links from tools that treat a redirect like a subscription upsell. Operators (marketers, founders, agencies, anyone pasting campaign URLs into SMS and decks) need a domain they own, a dashboard that lists what they created, and click facts that survive the redirect. Developers already on Convex and Clerk do not need a fifth SaaS login for "paste URL, get code." s12.in is for that seat: MarTech folks, Next.js-on-Vercel shippers, and anyone who wants `s12.in/abc` instead of a twenty-character tracking URL. ## What I shipped a URL shortener and file host at [s12.in](https://s12.in). Stack facts: **Next.js 16.1.6** (App Router, Turbopack) on Vercel, **React 19.2**, **Convex** for links/files/clicks/users plus `_storage` uploads, **Clerk** for auth, **Tailwind CSS v4**, Radix primitives, Resend in the lockfile, Biome + oxlint + tsgo. Package name `s12`, version **0.1.0**, packageManager **bun@1.3.1**. Repo is **private** under HurleyUS. ![Dashboard. Shorten, upload, manage](/blog/s12-in-url-shortener-file-hosting/screenshots/dashboard.png) Paste a long URL on the homepage or dashboard. Convex `links.create` issues a short code (optional `customCode` / `password` / `expiresAt` exist on the schema; the public form only sends `url` + optional `userId`). `app/[code]/route.ts` resolves the code, parses user-agent, reads Vercel geo headers, hashes the IP with SHA-256 + a salt (16 hex chars), fires `recordClick` without blocking, then **302**s. Files take the same path: viewable MIME types redirect to the Convex storage URL; others return an attachment stream. Dashboard tabs list your links and files with copy/delete. Click and download counters are denormalized on the row. `getAnalytics` can roll up by day/country/browser/device/referrer. Charts are not on the dashboard yet. A Manifest V3 Chrome extension scaffold lives under `extension/` and posts to `/api/shorten` with CORS. Icon PNGs are still marked `ICONS_NEEDED.md` (SVG only in tree). Honesty operators can smell: the hero still prints **10M+ / 50K+ / 99.9%** as static JSX. The live Clerk publishable key I hit was **pk_test** on `*.clerk.accounts.dev`. README still names PostHog and Sentry; those packages were removed Feb 21. Footer GitHub still points at `michaelmonetized/s12.in` while the working private remote is **HurleyUS/s12.in**. Schema `users.plan` free|pro|team is ahead of any billing UI. ![Redirect pipeline](/blog/s12-in-url-shortener-file-hosting/screenshots/redirect-pipeline.png) ## Where Product: [s12.in](https://s12.in) (apex redirects toward www). Vercel project alias on the GitHub homepage field: [s12-in.vercel.app](https://s12-in.vercel.app). Routes that matter: `/`, `/dashboard`, `/sign-in`, `/sign-up`, `/privacy`, `/terms`, `/refunds`, `/api/shorten`, `/[code]`. Code: [github.com/HurleyUS/s12.in](https://github.com/HurleyUS/s12.in). Private, no topics, no license file, no tagged releases. Deploy path is Vercel continuous with a Blacksmith `ship.yml` gate. Local path: `bun install`, `bunx convex dev`, `bun dev`. Audience sits next to every "just use Bitly" thread and every Convex starter that never grew a redirect route. ![Chrome extension scaffold](/blog/s12-in-url-shortener-file-hosting/screenshots/extension.png) ## When **2026-01-08.** Initial commit: Next.js, Convex, Tailwind. Same day: OPPORTUNITIES.md and homepage improvements; PLAN.md improvement list. Early docs still daydream about a CDN. The product that shipped is the shortener + files. **2026-01-31 to 02-05.** Sync. Tailwind v4 `@import`. Prod build ready. **2026-02-06.** The spine. Core URL shortening. Upgrade to Next.js 16 and React 19; middleware renamed toward `proxy.ts` (and briefly back during Next's naming war). Convex backend configured and deployed. Clerk auth keys landed. **2026-02-11.** Dashboard UI improvements: links/files mental model. **2026-02-15–21.** Docs say `proxy.ts`. Footer gets real social URLs. Unused `posthog-js` and `@sentry/nextjs` leave the lockfile. **2026-02-27.** Security headers in `next.config.ts`. `sitemap.ts` / `robots.ts`. Error boundaries. Auth checks on links and files mutations. **2026-03-23.** PR #23 strips `console.error` noise. **2026-05-14–15.** Blacksmith CI standardized. Providers tolerate missing Clerk keys so builds do not die. Deploy URL / health-check fixes. **2026-08-08.** HEAD `d6c074f`. `vercel.json` sets `X-Robots-Tag: index, follow`. Thirty-eight commits on the clock. ## Why A short link you do not control is a tax with a dashboard skin. I wanted the domain, the Convex tables, and the redirect that writes analytics before the visitor leaves, on the same Clerk identity I already use everywhere else. Shortening and file hosting landed on `s12.in`. Clicks record geo and device fields. Shipping continued until CI and robots headers stopped being the embarrassment. It is still 0.1.0. Custom codes, password gates, plan limits, and analytics charts sit in schema or queries waiting for UI. Clerk on the observed deploy is still test-mode. That is fine to say out loud. The redirect path and the dashboard list already do the job a rented shortener charges monthly for. What would you put on `s12.in/yourcode` first: the campaign URL, the PDF, or the deck you keep resending as a thirty-line Google Drive link? --- # Monarch Mountain Foundations: I migrated WordPress to Next — DNS still serves PHP Source: https://www.michaelchurley.com/blog/monarch-mountain-foundations-wordpress-to-next-dns-still-php Published: 2026-08-08 Author: Michael C. Hurley Tags: monarch-mountain-foundations, highlands-nc, cashiers-nc, western-north-carolina, concrete-foundations, wordpress-migration, nextjs, vercel, resend, localbusiness-schema, martech, hustle-launch, client-site ![Monarch Mountain Foundations Next rebuild hero. Structural Concrete](/blog/monarch-mountain-foundations-wordpress-to-next-dns-still-php/screenshots/home.png) ## Who I build for operators in the mountains. Monarch Mountain Foundations pours structural concrete in Highlands, Cashiers, and the surrounding Western North Carolina ridge lines: footings, foundation walls, slabs, driveways, sidewalks, surveying and job-site prep. The contractor needed a lead surface that matched the job sites: Angi / Chamber / BBB trust, a phone that actually rings, a quote form that lands in email. Homeowners and builders needed to see real pours, not a stock theme. I am the builder under Hustle-Launch. The concrete is theirs. The stack is mine to keep honest, including when DNS and git disagree. If you ship local-business sites, MarTech lead paths, or WordPress exits: this is the field notes on a migration that finished in the repo and not at the nameserver. ## What I rebuilt monarchmountainfoundations.com as a private Next.js App Router site. Stack on the box: **Next.js ^16.1.6**, React 19, Tailwind v4, Bun, Resend, Sentry, PostHog provider, Radix, Zod/RHF on forms. README still says Next 15.5.6. Package **0.1.0**. Repo: `Hustle-Launch/monarchmountainfoundations.com`. ![WordPress www vs Next Vercel. Dual hosts](/blog/monarch-mountain-foundations-wordpress-to-next-dns-still-php/screenshots/dual-hosts.png) What exists in code: - Full page set: home, about, foundations, driveways + sidewalks, surveying + job site prep, gallery, contact - Resend contact API (`notify@uncap.us` to `monarchmountainfoundationsinc@gmail.com` + customer thank-you) - Schema.org LocalBusiness JSON-LD (phone 828-508-3602, Highlands geo, service offers) - SEO files (`sitemap.ts`, `robots.ts`, `manifest.ts`), CSP / HSTS / Permissions-Policy, Sentry configs - Job-site gallery categories with real `public/images` pours and slabs - Gold/navy contractor chrome, fixed bottom bar, Angi 2025 / Chamber / BBB trust row What is declared but empty or unused: - **Convex** in package.json. `convex/` is a `.gitkeep` - **Clerk** in package.json. No Clerk routes or wrappers in `app/` / `components/` - Gallery page exists; **not listed in `sitemap.ts`** - Google Search Console verification string is still the placeholder Homepage video still points at live WordPress uploads: `https://www.monarchmountainfoundations.com/wp-content/uploads/Monarch-Post-FX.m4v` Same for the rock-drilling GIF. The rebuild depends on the old host for motion assets. ## Where Customer DNS today: [www.monarchmountainfoundations.com](https://www.monarchmountainfoundations.com). Pack-day headers: **X-Powered-By: PHP/8.3.33**, LiteSpeed, `x-redirect-by: WordPress`. Next rebuild: [monarchmountainfoundations-com.vercel.app](https://monarchmountainfoundations-com.vercel.app). Headers: Vercel, CSP, HSTS preload, **X-Robots-Tag: index, follow**. `vercel.json` has `deploymentEnabled` for `main`/`master` set to **false**. Canonical metadata, sitemap, and LocalBusiness `@id` all name `https://monarchmountainfoundations.com`, the host that still serves PHP. Audience sits with Highlands–Cashiers construction buyers and with builders watching WNC client cutovers next to Mack's / Barbque-style apex-live sites. ![Foundations service page on the Next rebuild](/blog/monarch-mountain-foundations-wordpress-to-next-dns-still-php/screenshots/foundations.png) ## When **2026-01-08.** Init. Next.js, Convex slot, Tailwind. OPPORTUNITIES.md and PLAN.md the same day. Four January commits. Scaffolding, not the migration. **2026-01-31.** Chore sync. **2026-02-06.** Upgrade to Next 16 / React 19. Tailwind v4 `@import` fix. Then the load-bearing commit: `ba2a761`, full WordPress to Next.js migration, all pages, images, content. **2026-02-09–11.** Resend contact form. Compressed video backgrounds. Foundations page 1:1 polish, white navbar, fixed bottom bar. SEO and production polish. TypeScript `manifest` / `sitemap` / `robots`. **2026-02-21–28.** LocalBusiness structured data. Tailwind-first CSS. Sentry. Gallery with categorized photos. Kebab-case components. `lib/constants.ts`. Hero email fix. Strict tsconfig / next.config / Sentry PR. Build gate. Launch-week TypeScript fixes. Remove GitHub Actions. Vercel called out as CI. Security headers PR (HSTS, CSP, Permissions-Policy). Densest month: **24 commits**. **2026-05.** shipprep install, revert, ignore local M4V drops, then shipprep observability + Blacksmith deploys. Main Git auto-deploy stays off. **2026-08-08 6:55 AM ET.** `c7e4e30`, set `X-Robots-Tag` to `index, follow` on Vercel. HEAD. Thirty-three commits on `main`. That is the clock from empty client repo to a hardened Next rebuild that is not what www serves. ![Commit journey Jan through Aug](/blog/monarch-mountain-foundations-wordpress-to-next-dns-still-php/screenshots/journey.png) ## Why WordPress exits fail two ways: a pretty Vercel URL nobody's DNS points at, or a DNS flip with a half-ported theme. I did the hard content port (page parity, Resend leads, Schema, gallery, security headers), then left the apex on LiteSpeed PHP while the rebuild sat on a project alias with main auto-deploy disabled. sitrep.md still says PROTOTYPE and "last commit January 31." ROADMAP says Production. PLAN still has homepage unchecked. The tree and the Aug HEAD are the tie-breaker: the product pages exist; the cutover does not. Sibling context: Mack's Shack and Barbque Wagon are WNC food clients whose apex already speaks Next. This one is the concrete contractor where git moved and DNS did not. If you run a WordPress local-business site in the mountains, would you flip DNS the week the Next rebuild matches content 1:1, or keep WordPress live until every `wp-content` hotlink is gone? --- # mockup-gallery: I shipped 10 industry mockups as a cold-outreach portfolio Source: https://www.michaelchurley.com/blog/mockup-gallery-ten-industry-cold-outreach Published: 2026-08-08 Author: Michael C. Hurley Tags: mockup-gallery, hurleyus, cold-outreach, web-design, nextjs, tailwind, vercel, portfolio, martech, conversion ![Web Design Portfolio chrome with Prestige luxury real estate tab](/blog/mockup-gallery-ten-industry-cold-outreach/screenshots/gallery-home.png) ## Who I sell web design the old way sometimes. Email. Industry angle. Proof. Prospects do not want a Figma file. They want to see what *their* kind of site could look like — real estate, HVAC, SaaS, a restaurant booker — without waiting on a custom sprint. mockup-gallery is for that motion. Local operators. SMB owners. Anyone I can send a link and a short note. Frontend people who care how a sales kit is actually built under Next 16 and Tailwind 4. Not the twelveux registry crowd. Not a single vertical client delivery. ## What I built a public Next.js gallery. Package name `design-mockups`. Version **1.0.0**. React 19. Tailwind 4. Bun lockfile. Lucide icons. One client page. Sticky dark portfolio chrome. Ten tabs. Each tab mounts a full-page industry lander under `app/components/mockups/`. The brands on the tabs: **Prestige**, **Velocity Motors**, **Nexus Consulting**, **ARIA**, **ZENITH**, **HandyPro**, **Analytics Pro**, **LearnHub**, **TableHub**, **VoyageNow**. Roughly twenty-six hundred lines of mockup TSX. No per-mockup routes — tab state only. README ships a cold-outreach template (sign-off in-file still says “—Rusty”). Observability landed later: Sentry, PostHog, Fallow, Blacksmith `ship.yml`. ![Ten industry mockup cards](/blog/mockup-gallery-ten-industry-cold-outreach/screenshots/ten-mockups-grid.png) ## Where Live on Vercel: https://mockup-gallery-nu.vercel.app — at pack time `X-Robots-Tag: index, follow`. Repo: https://github.com/HurleyUS/mockup-gallery Contact chrome: hello@hurleyus.com · +1 (828) 593-1935 ![HandyPro home services mockup](/blog/mockup-gallery-ten-industry-cold-outreach/screenshots/home-services.png) ## When **2026-03-28, 7:21 PM ET.** Ten mockups land. Same evening Tailwind v4 / PostCSS / Vercel CSS fight through `61dda20`. **2026-05-14 afternoon.** Blacksmith CI standardization + Fallow/FReview scaffolding + formatter green. **2026-08-08, 6:55 AM ET.** HEAD `21f44ec` — robots `index, follow`. Twenty commits. Package 1.0.0. ## Why I needed a link for industry email without a bespoke repo per vertical. Ten tabbed landers is that link. Tailwind v4 pain is part of the truth. May CI/observability is the operator habit. August robots flip says the page is meant to be found. What industry tab would you send first? --- # MerchWinner: I shipped a POD course marketplace before the catalog had courses Source: https://www.michaelchurley.com/blog/merchwinner-pod-course-marketplace Published: 2026-08-08 Author: Michael C. Hurley Tags: merchwinner, print-on-demand, course-marketplace, nextjs, convex, clerk, stripe, resend, catppuccin, martech, pod ![MerchWinner live home. Start Learning / Become an Instructor](/blog/merchwinner-pod-course-marketplace/screenshots/home.png) ## Who I kept meeting people who wanted Amazon Merch, Etsy POD, and Shopify print-on-demand to pay rent, and who were buying theory courses from people who had not shipped a design in years. MerchWinner is for aspiring merch sellers who want practitioners. It is for active sellers who will teach if the split and the platform are not a second job. It is for operators who want to see a full Clerk + Convex + Stripe course-marketplace shape on Next.js 16 without pretending the catalog is full. If you have ever built the checkout before the first SKU, this is for you. ## What I shipped **MerchWinner.com**: a user-contributed course marketplace for learning how to sell merch online. Public surfaces: home, `/courses` with search/category/sort, course detail + lesson player, `/instructors`, `/about`. Instructor workspace for create/edit/publish, lesson reorder, earnings, analytics, profile. Student enrollment dashboard. Money path on Stripe: one-time course and bundle checkout, Pro/Unlimited memberships, webhook to Convex purchases/subscriptions, Resend mail for enrollment, invoice, renewal, review reminders. Stack from the lockfile, not the stale README line: **Next.js ^16.2.6**, **React ^19.2.6**, **Convex ^1.38.0**, **Clerk**, **Stripe ^22.1.1**, **Resend**, PostHog, Sentry, Tailwind v4, Radix, Catppuccin Mocha default. Package **0.1.0**, private under HurleyUS. **86** commits. HEAD `65c4413`. ![Live courses page: search UI with empty catalog](/blog/merchwinner-pod-course-marketplace/screenshots/courses-live.png) Billing facts in `lib/billing.ts`: `PLATFORM_FEE_PERCENT = 20` (creator net 80%). Plans: Pro **$29/mo** / **$278/yr** (3 courses/month) and Unlimited **$49/mo** / **$470/yr**. Homepage marketing still says "Earn 70% of every course sale." I would rather name the mismatch than paper it over. Convex schema covers users (student/instructor/admin), instructorProfiles, courses, lessons, enrollments with progress, subscriptions, coursePurchases, bundlePurchases, payoutRequests, reviews. Eighteen App Router pages. Fourteen API routes. Bun + Biome + ship script. ![About page: practitioner POD curriculum](/blog/merchwinner-pod-course-marketplace/screenshots/about.png) ## Where Code lives at [github.com/HurleyUS/merchwinner.com](https://github.com/HurleyUS/merchwinner.com) (private). Live site: [www.merchwinner.com](https://www.merchwinner.com). Vercel project URL on the repo homepage field: [merchwinner-com.vercel.app](https://merchwinner-com.vercel.app). Apex redirects to www. Audience sits next to every POD Discord still arguing Redbubble vs Merch while nobody ships curriculum. Contact in README: michael@hustlelaunch.com. Same operator family as the other HurleyUS Next/Convex products. ![Instructors page: shell loaded, no profiles yet](/blog/merchwinner-pod-course-marketplace/screenshots/instructors-live.png) ## When **2026-01-08.** `91c1e8e` Initial commit: Next.js, Convex, Tailwind. Same day: opportunities doc and homepage pass. **February.** Next 16 + React 19. Catppuccin dark mode, layout variants, stub `/courses` and `/about`. Feb 21 standards mega-fix closes a pile of issues. middleware, providers, schema, icons, fonts, email docs. **March 20–22.** Stripe checkout patterns, instructor profile and earnings, webhook typing. Money enters the building. **March 25.** Fourteen commits. Resend templates. Phase 2 webhook to email. Phase 3a–3c: instructor course creation, lesson manage UI, student enrollment and lesson player. **March 26.** Nineteen commits. Phase 4 search/filters/SEO/schema markup. Phase 5 reviews UI + reminders. Phase 6a/6b earnings and analytics merges. Production-ready checklist docs. **March 27.** Catppuccin unify across the site. Instructor and enrollment CTAs. **May 13–15.** shipprep standards and a Blacksmith CI/deploy verification flurry. **2026-08-08, 06:55 ET.** `65c4413` set X-Robots-Tag to index, follow on Vercel. HEAD. Eighty-six commits total. On September 8, 2026 the live `/courses` page still reads **"No courses available yet."** The marketplace shipped. The catalog did not. ![Illustrative instructor earnings mock. 20% platform fee math](/blog/merchwinner-pod-course-marketplace/screenshots/instructor-earnings-mock.png) ## Why I did not want another "courses coming soon" landing page with a Mailchimp box. I wanted the boring spine: auth roles, course/lesson tables, Stripe sessions, webhooks that enroll, email that confirms, instructor earnings that know the fee, student progress that is not a Google Sheet. So I compressed the spine across six phases and left the honesty visible: empty catalog, empty instructors grid, README still claiming Next 15.5.6 and Payments N/A while Stripe and Next 16 are in the lockfile. Version 0.1.0. Private on purpose until the first real courses earn the public launch. What would you publish first on MerchWinner: Amazon Merch niche research, Etsy POD ops, or TikTok ads that actually sell shirts? --- # modern-design-playground: I left agents running for ten hours and they rebuilt nine worlds Source: https://www.michaelchurley.com/blog/modern-design-playground-afk-webgl-nine-worlds Published: 2026-08-08 Author: Michael C. Hurley Tags: modern-design-playground, webgl, threejs, tanstack, vite, shadcn, max, martech, vercel, afk, agents ![Modern Design Playground: WebGL home instrument](/blog/modern-design-playground-afk-webgl-nine-worlds/screenshots/home.png) ## Who I do not want another marketing landing that scrolls like a PDF with bounce. I want an instrument. Pointer gravity. Camera dolly. A stage that listens when you strike it. modern-design-playground is for operators who already live in Max type, Catppuccin surfaces, and shadcn primitives, and who have watched a WebGL canvas go black after an HDRI 404 and refused to ship the apology screenshot. It is also for anyone curious what happens when you leave agents AFK with a Playwright density score and a mandate to polish the weakest PNG. ## What I built **Modern Design Playground**: Vite 8, React 19, TanStack Router, Three.js + R3F, GSAP, Lenis, Motion, Tailwind 4, shadcn base-nova, Max fonts. Private package, unversioned. Nine commits. HEAD `d16a7e3`. Home `/` is one continuous WebGL field. Liquid core. Cosmic backdrop. Helix of archive plates that becomes a tunnel. Strike the field. Hold to resonate. Glass / metal / matte. Case study sheet. `Stage.tsx` ~1k lines. `Chapters.tsx` ~1.4k. ![Landing gallery: nine instruments, zero templates](/blog/modern-design-playground-afk-webgl-nine-worlds/screenshots/landings-gallery.png) `/landings` is nine worlds, not nine templates: editorial, brutalist, noir, zen, neon, paper, atlas, pulse, prism. `pages.tsx` is **10,682 lines** of CSS micro-theaters. Immersive routes hide the site chrome. The honest middle of the story: on July 12 the homepage WebGL died. Remote HDRI Environment suspended the scene. CDN 404s. Postprocessing wiped alpha to zero. I did not hand-paint a hero. I left a marathon (iterate-loop, marathon-forever, smoke-interact): screenshot every surface, score by PNG density, upgrade the weakest, repeat for ~10 hours while I was AFK. STATUS-MARATHON.md is the log. Opaque clear `#11111b`. Local lights. No EffectComposer on the home stage. Liquid core came back. ![Brutalist world: paint floor instrument](/blog/modern-design-playground-afk-webgl-nine-worlds/screenshots/landing-brutalist.png) July 14 I shoved it onto Vercel as a static SPA, ripped Clerk, stripped Convex/PostHog providers for performance. August 8 I set `X-Robots-Tag: index, follow`. Live host: **https://mdp-seven.vercel.app**. Sibling surfaces to keep straight: twelveux (hosted shadcn registry + Glass), HMS (live-edit CMS). Helix stills share energy with uncap; different product surface. ## Where Live on Vercel: https://mdp-seven.vercel.app (home instrument, `/landings`, `/landings/{slug}`). Repo: https://github.com/michaelmonetized/modern-design-playground ## When **2026-07-12, 10:39 ET.** `baa3c67` init. **That afternoon into night.** Marathon AFK ~12:00 to ~23:00 ET. Passes A through I. WebGL repaired. Nine worlds densified. Commit `06d6555` next morning: *speechless, requires a full case study and log analysis*. **2026-07-14.** Push fixes. `.debug-screenshots` gitignored. Deploy to Vercel. SSR to static SPA. Clerk out. Performance strip. **2026-08-08, 06:55 ET.** `d16a7e3` robots index/follow. HEAD. Nine commits total. ## Why I wanted proof that scroll can be an instrument and that an agent loop with screenshot density as the scoreboard can recover a dead WebGL stage without me babysitting every frame. So the pack is the playground plus the marathon scar. The live URL is the proof. The STATUS log is the receipt. If your hero is still a paused MP4 pretending to be 3D, you already know the blank canvas I was staring at. Which world would you open first after the instrument: brutalist, neon, or zen? --- # Kings Roofing NC: I didn't redesign the WordPress site — I lifted it to Next.js Source: https://www.michaelchurley.com/blog/kingsroofingnc-pixel-perfect-wp-to-next-lift Published: 2026-08-08 Author: Michael C. Hurley Tags: kingsroofingnc, wordpress-migration, nextjs, western-north-carolina, roofing, local-business, resend, vercel, martech, pixel-perfect ![Kings Roofing homepage hero. Green metal roof, Free Quote CTA](/blog/kingsroofingnc-pixel-perfect-wp-to-next-lift/screenshots/home.png) ## Who I get hired when a contractor's site still works for referrals and dies for everyone else: plugins rotting, forms flaky, hosting bill arguing with the phone. Kings Roofing already had a personality: orange `#FF7620`, leaping lion, metal-roof hero, color pickers, carport kits, Waynesville-to-Highlands location pages. Redesigning that into a generic "modern roofing" template would have trained half of Haywood County that the company changed crews. This write-up is for operators who will keep the weird brand orange on purpose, and for WNC folks who just need the site to load when they search the truck wrap. ## What Public repo [Hustle-Launch/kingsroofingnc.com](https://github.com/Hustle-Launch/kingsroofingnc.com). Package **0.1.0**, Bun, Next.js **16.1.6**, React 19, Tailwind 4. Stack on HEAD: Resend for quote mail, PostHog, Sentry (`withSentryConfig` + CSP/HSTS/frame DENY), Phosphor via react-icons, Biome/Oxlint/tsgo local gates. No Stripe on `main`. A local branch has quote-fee experiments that never reached HEAD, so I am not narrating them as shipped. ![Residential services page](/blog/kingsroofingnc-pixel-perfect-wp-to-next-lift/screenshots/residential.png) What actually shipped: - Pixel-faithful homepage: Residential & Commercial hero, Free Quote / Call box, New Roof Installation with YouTube embed, Re-Roofing orange band, Roof Repair checklist, four location photo cards, "Roofers in Asheville NC" closer. - App Router pages matching the old WordPress menu: About, Residential, Commercial, Contact, Roofing Color Pickers (metal + shingles), Metal Structures (carport kits + pole truss kits + style detail pages). - Location SEO under `/residential/[location]` for Asheville, Cashiers, Highlands, Waynesville. Constants consolidated so phone numbers do not drift. - Contact + feedback flows: Resend from `notify@uncap.us` to `kingshaywood@gmail.com`, HTML escaped in templates, interactive star rating + loading skeleton from launch week. - Ops: Vercel prebuilt / Blacksmith path, git auto-deploy off on main, Aug 8 `X-Robots-Tag: index, follow`. Honesty: sitemap still lists legacy `/waynesville` style URLs while the app moved under `/residential/...`. Custom domain `kingsroofingnc.com` returns Cloudflare **526** from pack hosts. The working surface is the Vercel alias. ![Roofing color pickers](/blog/kingsroofingnc-pixel-perfect-wp-to-next-lift/screenshots/color-pickers.png) ## Where Working product: [kingsroofingnccom.vercel.app](https://kingsroofingnccom.vercel.app). Intended apex `kingsroofingnc.com` is DNS/SSL-incomplete at draft time. Code: Hustle-Launch (public; michaelmonetized mirror shares HEAD `984746b`). Host Vercel. Business phones in constants: **828-246-2193** / **828-279-6896**. Service area string spans Asheville through Weaverville. ![Metal structures hub](/blog/kingsroofingnc-pixel-perfect-wp-to-next-lift/screenshots/metal-structures.png) ## When **2026-02-12.** One long day. Create Next App to WP migration scaffold to Layout/Providers to exact copy to full sitemap pages to move locations under `/residential/[location]` to dropdowns for locations / metal structures / color pickers to white/orange restyle to lion + dark footer to lock Roboto/Poppins/#FF7620 to YouTube to "match WordPress exactly" commits. That afternoon is the product. **2026-02-15.** Escape user input in Resend HTML so quote mail is not an XSS souvenir. **2026-02-21.** Launch-week hygiene: README that is not boilerplate, LOCATIONS single source, star rating + skeleton, Phosphor icons, ContactCTA extract, carport/pole-truss content fleshed out, phone consolidation + `rel=noopener`. **2026-02-27–28.** Build gate, drop GitHub Actions (Vercel is CI), P1 fixes, security headers folded into `next.config.ts`. **2026-05.** shipprep / Blacksmith observability dance: install, revert, re-apply. **2026-08-08.** HEAD `984746b`. Robots index/follow on Vercel. Thirty-two commits on the clock. ## Why Referral businesses do not buy your taste. They buy continuity. I kept the orange, the lion, the color pickers, and the metal-structure tree because those were already the marketing system. Next.js, Resend, Sentry, and CSP are the parts that were rotting under WordPress, not the brand. When you migrate a referral-driven local business off WordPress, do you keep the weird brand orange, or do you "modernize" it until nobody recognizes the truck wrap? --- # iLeague.golf: I built Patreon meets 18Birdies for golf creators — and left Clerk on pk_test Source: https://www.michaelchurley.com/blog/ileague-golf-patreon-meets-18birdies Published: 2026-08-08 Author: Michael C. Hurley Tags: ileague, ileague-golf, golf, creator-economy, patreon, 18birdies, stripe, convex, clerk, nextjs, expo, itour, hurleyus ![iLeague.golf homepage mock. Patreon meets 18birdies, Top 54 qualify for iTour](/blog/ileague-golf-patreon-meets-18birdies/screenshots/home.png) ## Who I build products in public for operators, not for pitch decks. Golf creators already duct-tape a scorecard app to a Patreon to a tip jar. Fans already bounce between three tabs to follow a round and pay for the good stuff. iLeague.golf is for those creators and those fans, and for anyone shipping a Bun monorepo that has to hold a Next.js web app, an Expo companion, and a Convex backend without lying about what is finished. If you care about creator-economy plumbing, Stripe tier math, or how a generic "influencer" scaffold gets rebranded into a sport vertical: this is the field notes. ## What I shipped a golf creator platform under `HurleyUS/ileague.golf`. The README line is blunt: **Patreon meets 18birdies**. The live lander says the same badge, then: **Where Golf Creators Build Empires**. Track rounds. Build a following. Get paid through subscriptions and tips. **Top 54** creators qualify for [iTour.golf](https://itour.golf). Winners aim at iConference. Stack on the box: **Next.js 15.5.9**, React 18.3.1, **Convex**, **Clerk**, **Stripe**, PostHog, Sentry, Resend, Tailwind v4, Bun workspaces, Expo 52 mobile, Phosphor icons via react-icons, Vercel. Package version **1.0.0**. Repo is private. Site is public at [ileague.golf](https://ileague.golf). ![Creator profile mock. followers, subscribers, earnings, content grid](/blog/ileague-golf-patreon-meets-18birdies/screenshots/creator-profile.png) Monetization is not a slide. It is `apps/web/src/lib/billing.ts`: - Platform fee **15%** - Tip presets **$5 / $10 / $20 / $50 / $100** - Suggested creator tiers. Bronze $4.99, Silver $9.99, Gold $24.99 monthly (yearly suggested too) - Real tiers live in Convex `subscriptionTiers` and check out through dynamic Stripe `price_data` (web API + Convex actions) Content model covers video, shorts, images, links, text, and **round recaps** tied to scorecards. Visibility: public / followers / subscribers. Courses, course reviews, rounds (FIR/GIR/putts), standings, leagues, notifications. all in `convex/schema.ts`. What is also true: the dashboard still says **influencers**. Explore still calls `getInfluencers`. The README still says **Top 36** while the live hero says **Top 54**. Production HTML still loads Clerk **`pk_test`** from a `*.clerk.accounts.dev` instance. The scorecard schema is ahead of the hole-entry UI. That is the product, part of the story. ![Billing mock. Bronze / Silver / Gold + tip presets](/blog/ileague-golf-patreon-meets-18birdies/screenshots/billing-tiers.png) ## Where It runs on Vercel against Convex. Auth is Clerk. Money is Stripe. Mail is Resend. Errors go to Sentry. Product events go to PostHog. Mobile is Expo with EAS configs and placeholder Apple submit IDs. Surfaces that matter: - Public lander. emerald/slate hero, feature grid, creator/fan columns, ecosystem footer - Auth + onboarding. Clerk sign-in/up, role/profile setup - App shell. dashboard, explore, leagues, notifications - Creator profile. `/creator/[id]` with hero, featured courses, content grid - Payments. `/api/stripe/checkout`, portal, webhook; Convex `/stripe-webhook` Audience sits with golf creators, golf fans, and builders watching a sport vertical on a creator-economy stack. Sibling properties: iTour.golf and iConference.golf under the HurleyUS lane. ![Scorecard / schema mock. hole-by-hole model vs missing UI](/blog/ileague-golf-patreon-meets-18birdies/screenshots/scorecard.png) ## When **2026-01-09.** Initial monorepo. web + mobile. Convex stubs. React 19 downgraded to 18 for Clerk/Convex. Vercel monorepo config. First production deploy on a hustle-launch Vercel URL. CHANGELOG also remembers the earlier `michaelmonetized/ileague-app` GitHub link. **January 31.** Chore sync. **February 4.** Mobile leagues pushed toward Convex. EAS config. Roadmap honesty about hardcoded mobile screens. **February 11.** HurleyUS repo created. Golf schema and plan. Golf-focused homepage, header, footer, Convex functions. This is the rebrand day: influencer scaffold becomes iLeague.golf. **February 12–28.** Stripe API version bump. Search indexes. Metadata rewritten for golf. Golf category. lucide to react-icons. Root `/convex` consolidation. CI experiments. Mobile TypeScript cleanup. Vercel as the only CI/CD. **March.** Security headers. Middleware runtime fights. **www vs apex redirect loop**. normalize middleware, dual aliases, then disable middleware so Vercel stops bouncing. robots.txt, sitemap, trending creators carousel. Stripe checkout patterns standardized. **Creator profile page** ships. Dashboard starts showing real follower/subscriber/earnings stats. Convex package versions aligned. **May 14–15.** Blacksmith CI gate spam, then removed. **May 21.** Production Convex crash fix. Missing creator queries for the lander and profile pages. Stripe/Resend factories so deploy analysis does not need live keys. CSP `worker-src`. CHANGELOG **1.0.4**. **June 22.** Nightly commits. **August 8, 6:55 AM ET.** HEAD `508df8e`. set `X-Robots-Tag` to `index, follow` on Vercel. Fifty-four commits on `main`. **September 8 pack day.** Live site returns 200. Robots tag present. Clerk still on test keys. Draft only. ![Ecosystem mock. iLeague to iTour to iConference](/blog/ileague-golf-patreon-meets-18birdies/screenshots/ecosystem.png) ## When (clock) Fifty-four commits. January 9 to August 8. HEAD `508df8e`. Private monorepo. Public emerald lander. ## Why Creator tools fail in two directions: beautiful landers with no money path, or Stripe dashboards with no sport-specific object model. I wanted both. scorecards that can become content, tiers that can become iTour qualification, tips that are not a third-party link-in-bio. I also refused to pretend the rebrand was complete. Influencer function names, Top 36 vs Top 54, and `pk_test` on a real domain are operator signals. They tell you where the product still is. The ecosystem bet is explicit: iLeague feeds iTour feeds iConference. This pack is only the first node. If you create golf content today, what would you rather ship next on iLeague: a real hole-by-hole entry flow, or flipping Clerk to live keys so the first paying subscriber is not on test mode? --- # hurleyus.com: I shipped the parent site as a performance-pay membership lander Source: https://www.michaelchurley.com/blog/hurleyus-com-membership-growth-parent-site Published: 2026-08-08 Author: Michael C. Hurley Tags: hurleyus, golf, membership, nextjs, convex, resend, sentry, martech, vercel, parent-company ![Hurley US homepage — Membership Growth for Private & Resort Golf Clubs](/blog/hurleyus-com-membership-growth-parent-site/screenshots/home.png) ## Who I run Hurley US as the parent company for a golf-media and SaaS portfolio. Clubs do not buy a story about the future of golf. They buy membership revenue. This site is for private and resort club operators who treat acquisition as a lever, and for builders who want to see how a parent domain can stop performing brand theater and still keep the ecosystem pages in the repo. ## What I shipped hurleyus.com — private repo under HurleyUS, public site on Vercel. Live `/` mounts **HomeA**: membership growth for private and resort golf clubs. Compensation tied to incremental revenue. No upfront fees. Call CTA to (828) 269-8280. Named proof: Linville Land Harbor Golf Club and Laurel Ridge Resort & Country Club. Sticky mobile call bar. Stack at HEAD (`ac2327f`, package.json **0.1.0**): **Next.js 16.1.6**, React 19, Bun, Tailwind v4, shadcn/Radix, Resend, Sentry, Convex client that no-ops without a real `NEXT_PUBLIC_CONVEX_URL`, PostHog that no-ops without a key. README still says Next 15 — deps say 16. ![About — Hurley US](/blog/hurleyus-com-membership-growth-parent-site/screenshots/about.png) Still in the tree: **HomeB** — Growing the game together, iLeague / iTour / iCon pillars. Not what `/` renders today. Lead forms on partners / sponsors / investors / contact hit `/api/email` with Zod validation and an in-memory IP rate limit (5/min). Booking hits `/api/booking` and emails an ICS. From: `notify@uncap.us`. Owner: `contact@hurleyus.com`. Clerk routes `/sign-in` `/sign-up` are TODO stubs — **no `@clerk/*` in package.json**. Stripe exists as a cursor rule doc, not a dependency. ![Partners](/blog/hurleyus-com-membership-growth-parent-site/screenshots/partners.png) ## Where Production: [www.hurleyus.com](https://www.hurleyus.com). Apex 301 to www. Verified 2026-09-08: `X-Robots-Tag: index, follow`, CSP, HSTS, sitemap for home + partners/investors/sponsors/about/contact. Quirk: root layout metadata still titles the site Growing the game together while the body is HomeA — `page.tsx` imports HomeA without re-exporting its metadata. ![Contact](/blog/hurleyus-com-membership-growth-parent-site/screenshots/contact.png) ## When **2025-11-25.** Create Next App. **December 2025.** Theme, copy, A/B homepage (membership lander vs media ecosystem). **2026-02-05.** Page-B set as permanent homepage. Within two weeks the split collapses; `/` is HomeA only. Resend from notify@uncap.us. Booking form. Static/ISR pass. **2026-02-22 → 04-03.** Convex schema typed. Messaging Phases 1–3 through real-time subscriptions. **2026-05-13 → 15.** shipprep + Blacksmith CI + deploy health. **2026-08-08.** `ac2327f` — X-Robots-Tag index, follow. 109 commits total. ## Why A parent domain that only tells the influencer story leaves club operators with nothing to buy. HomeA is the commercial door: own the acquisition pipeline, get paid on incremental membership revenue, stay discreet. I kept HomeB in the repo because iLeague / iTour / iCon are real product mythology — just not the `/` bet right now. The A/B history is on the record: we tried always-B, then shipped always-A. The stack is honest: Convex and PostHog degrade. Clerk is a TODO. Stripe is a doc. Indexing was the last commit because a public site still needs to be findable. --- # GetFarmin: I scaffolded a farm equipment marketplace with escrow math first Source: https://www.michaelchurley.com/blog/getfarmin-farm-equipment-marketplace-scaffold Published: 2026-08-08 Author: Michael C. Hurley Tags: getfarmin, farm-equipment, marketplace, nextjs, convex, clerk, stripe-connect, escrow, agriculture, martech, catppuccin ![GetFarmin home mock. Find. Buy. Farm.](/blog/getfarmin-farm-equipment-marketplace-scaffold/screenshots/home.png) ## Who I got tired of watching six-figure iron move on hope and a Facebook comment thread. Farm equipment is not a $49 SaaS seat. A 2022 John Deere 8R 370 placeholder in this repo lists at **$385,000**. A Case IH 9250 combine sits at **$425,000**. That is not "add to cart and pray." That is escrow, hours, condition enums, oversize permits, and a seller who might be a dealer with a storefront slug. GetFarmin is for operators who want that marketplace shape on a real stack. Next, Convex, Clerk, Stripe Connect. before they pretend they have 10,000 live listings. Ag buyers and dealers who are done leasing trust from a local classifieds culture. Builders who would rather ship fee constants in a Convex mutation than wait for the perfect deploy key. If you have ever tried to move a center pivot across state lines with a spreadsheet and a handshake, this is for you. ## What I built **GetFarmin**. package name `getfarmin`, version **0.1.0**, private under **HurleyUS/getfarmin.com**. Metadata line: *Global Farm Equipment Marketplace*. Hero line: **Find. Buy. Farm.** Stack facts from the lockfile and tree, not the stale sitrep: **Next.js 16.2.6**, **React 19.2.6**, **Tailwind 4.3**, Bun, **Convex** schema + queries/mutations, **Clerk** auth surfaces, **Stripe** webhook route + Connect escrow scaffolding, Catppuccin Mocha default with green primary, Resend/Sentry/PostHog wired in `.env.example`. Thirty-six commits. HEAD `4b5df30`. ![Browse mock with placeholder iron](/blog/getfarmin-farm-equipment-marketplace-scaffold/screenshots/browse.png) Surfaces that exist in `app/`: `/` marketing, `/browse` with URL `searchParams` filters and placeholder cards from `lib/placeholder-data.ts`, `/about`, `/dashboard`, `/listing/new` multi-step form, `/dealers/[slug]`, Clerk sign-in/up catch-alls, `POST /api/shipping/estimate`, Stripe webhook route. Convex tables: `listings`, `categories`, `users`, `dealerProfiles`, `messages`, `savedListings`, `payments`. Listing tiers: basic / enhanced / dealer. Conditions: new through salvage. Escrow math in `convex/payments.ts`: platform **2.9% + $0.30**, escrow **1.5%**, optional buyer protection **+2%**. Comments say PaymentIntent creation is deferred. schema-ready money path, not a finished Connect onboarding wizard. Shipping estimator in `lib/shipping-estimate.ts`: base **$250**, **$2.10**/mile, **$38**/1000 lb, oversize and expedite flags, honest notes about permits and long-haul variance. ![Dashboard mock. inbox + escrow](/blog/getfarmin-farm-equipment-marketplace-scaffold/screenshots/dashboard.png) ## Where Code lives at [github.com/HurleyUS/getfarmin.com](https://github.com/HurleyUS/getfarmin.com). **private**. GitHub homepage points at [getfarmin-com.vercel.app](https://getfarmin-com.vercel.app). At pack time that URL returned **HTTP 500** with `X-Robots-Tag: index, follow` (the Aug 8 header fix). This pack does not invent a healthy production story. Audience sits next to MachineryTrader / TractorHouse energy, with a builder twist: Catppuccin dark-first UI, compound Convex indexes, issue-driven February feature closeout. Sibling operator furniture is present. `.hustlemc`, Blacksmith ship workflow, fallow REVIEW, Stripe sanity rule file. ![New listing multi-step mock](/blog/getfarmin-farm-equipment-marketplace-scaffold/screenshots/listing-new.png) ## When **2026-01-08.** `8107c34` Initial commit: Next.js, Convex, Tailwind. Same morning `f717dcf` OPPORTUNITIES + homepage. Afternoon `381035b` PLAN improvement list. The market thesis lands day one: global farm equipment, used market, listing tiers, escrow, shipping. **2026-01-31 / 2026-02-05.** Sync. Prod build ready. **2026-02-06.** `6f764f9` upgrade to Next.js 16, React 19. Tailwind v4 globals `@import` fix follows. **2026-02-13.** `0afe002` dark mode, Catppuccin theme system, browse/about pages, Convex schema. **2026-02-21–22.** Standards debt burned down. error/loading/not-found, env validation, Zod validations, providers, schema foreign keys as `v.id("users")`, merged fix PRs. **2026-02-23–24.** The marketplace week. Search and filtering. Clerk buyer/seller dashboard. Listing creation flow. In-app messaging. Stripe Connect + escrow scaffolding. Dealer storefronts + tiers. Convex bootstrap scripts. Heavy-equipment shipping estimate API. Issues **#1–#8** closed with matching PRs. **2026-05-14–15.** Eleven commits of Blacksmith CI gates, Biome, preflight, deploy URL verification. **2026-08-08, 06:54 ET.** `4b5df30` set X-Robots-Tag to index, follow on Vercel. HEAD. Thirty-six commits total. **Pack day 2026-09-08.** Draft and assets only. Live preview still 500s. `sitrep.md` still claims purpose unclear and last commit February 5. treat it as drift, not truth. ![Shipping estimate mock](/blog/getfarmin-farm-equipment-marketplace-scaffold/screenshots/shipping-estimate.png) ## Why I did not want another "agriculture somehow" landing page with emoji tractors. I wanted fee constants next to listing status enums. I wanted a shipping function that admits a 22-foot load needs permits. I wanted dealer slugs and message threads on the same Convex deployment shape as the browse grid, even while the browse grid still reads placeholder data and the Vercel deployment throws 500s. So the February issue board became the product: close search, auth, listing form, chat, escrow, dealers, shipping, bootstrap. May hardened the gates. August flipped robots headers on a site that still needs a living Convex URL. Would you price a six-figure tractor listing as free basic inventory, or charge the $49 enhanced tier before the first escrow release clears? --- # EverythingMonetized: I built a parody LMS where course bros sell courses about selling courses Source: https://www.michaelchurley.com/blog/everythingmonetized-parody-lms-course-bros Published: 2026-08-08 Author: Michael C. Hurley Tags: everythingmonetized, parody, satire, course-bro, lms, nextjs, convex, tailwind, martech, creator-economy, hurleyus ![EverythingMonetized live hero, Cohort 04 parody LMS academy redesign](/blog/everythingmonetized-parody-lms-course-bros/screenshots/home-live.png) ## Who I build products in public for operators who can smell a funnel from across the room. The course economy already sells the same promise on a loop: buy a course about making money by selling courses about making money. Fans of that bit, and builders who want the satire to have a real schema, are who this is for. If you already read the MerchWinner pack (POD course marketplace, empty catalog) or iLeague (golf creator Stripe), this is a different lane. EverythingMonetized is intentional parody of the guru machine. ## What I shipped a parody LMS under `HurleyUS/everythingmonetized.com`. README one-liner: **Parody site where AI generated course bros sell course bro courses to aspiring course bros.** Live metadata: **Where Course Bros Monetize Everything.** The May 22 redesign hero is blunter: **The parody LMS for learning how the course economy sells itself.** Stack on the box: **Next.js 16.2.6**, React 19.2.6, **Convex**, Tailwind v4, Phosphor, Framer Motion, Radix, next-themes (dark default), PostHog, Sentry, Resend. Bun. Package **everythingmonetized** **0.1.0**. Private HurleyUS repo. Public site at [www.everythingmonetized.com](https://www.everythingmonetized.com). ![Illustrative course catalog, 9 of 10 seeded absurd titles](/blog/everythingmonetized-parody-lms-course-bros/screenshots/courses.png) Convex schema is the product spine: - `courseBros`: name, slug, tagline, bio, catchphrases, specialties, socialProof, featured - `courses`: absurd titles, inflated `originalPrice`, modules/lessons, category, search index - `testimonials`: always-verified parody outcomes - `subscribers`: newsletter with email dedupe + source - `purchases`: pending/completed/refunded; paymentMethod includes **exposure** - `rateLimits`: newsletter + purchase mutations Seed ships **5** AI gurus (Chad Hustlemax, Brandon Scale, Tiffany Funnel, Derek Dropship, Maximilian Leverage) and **10** courses from $2,997 to $8,997, titles like *How to Find Your First Course Idea (By Buying This Course)* and *Mindset Mastery: Think Rich, Stay Poor (Until You Buy This)*. Avatars and thumbs are Pollinations prompt URLs. Admin is a full CRUD plane: bros, courses, testimonials, subscribers. Auth is a **password cookie** (`ADMIN_PASSWORD`, fail-secure after Feb 15). `@clerk/nextjs` sits in package.json; public Clerk billing is README fiction. TODO from Feb 13 says no Clerk auth. Purchase success copy is the thesis: congratulations, course bro, **no actual course will be delivered**. Commitment to the bit is the product. ![Illustrative course detail, High-Ticket Alchemy parody checkout](/blog/everythingmonetized-parody-lms-course-bros/screenshots/course-detail.png) What is also true on pack day: the live `/courses` page says **Showing 0 of 0 courses**. Seed exists. Production Convex is empty or unconfigured. TODO still blocks on GitHub issue #1, Convex env vars on Vercel. sitrep.md still claims PROTOTYPE / last commit Jan 31 / LOW priority. README still says Next 15.5.6. That drift is the story. ## Where It runs on Vercel. Apex `everythingmonetized.com` 307s to www. Alias `everythingmonetized-com.vercel.app` also 200. `vercel.json` disables git auto-deploy on main/master and, as of Aug 8, sends `X-Robots-Tag: index, follow`. Surfaces that matter: - Public lander: teal academy hero, learning-dashboard.tsx card, featured tracks, faculty, learner outcomes, weekly lab newsletter - Catalog: search / category / price / sort / pagination - Bro profiles: `/bros/[slug]` - Course detail: modules + parody purchase form - About: mission + core values (Hustle Over Health, etc.) - Admin: password gate via `proxy.ts` Audience: satire builders, MarTech operators, and anyone comparing a seeded Convex backend to a live empty catalog. ![Live courses page, Showing 0 of 0](/blog/everythingmonetized-parody-lms-course-bros/screenshots/courses-live.png) ## When **2026-01-08.** Initial Next.js + Convex + Tailwind. OPPORTUNITIES.md and PLAN.md the same day. The bit was named early. **January 31.** Chore sync. **February 6.** Next.js 16 + React 19. Tailwind v4 `@import` fix. **February 8.** The product day. Admin panel, tests, search/filter/SEO, edit pages. CHANGELOG **0.1.0** lists five tables, parody purchase, Pollinations images, 48 unit tests, Playwright config, PostHog, Sentry, rate limits. **February 13.** Production readiness: BUILDING.md button compliance, dark mode via next-themes, `proxy.ts` naming for Next 16. **February 15.** Security: remove hardcoded admin password + fallback. Fail closed if `ADMIN_PASSWORD` missing. **February 21.** lucide to `@phosphor-icons/react`. **April 6.** ESLint / vitest chore. **May 13–15.** shipprep + Blacksmith CI gate spam, then deploy health URL fixes. Same CI noise pattern as sibling HurleyUS templates, but this repo already had the Feb product pass underneath. **May 22.** **redesign.** Teal/slate academy UI on home, cards, header/footer, newsletter. Orange hustle-gradient brand in DESIGN.md becomes the light-theme leftover; dark default is the live look. **August 8, 6:54 AM ET.** HEAD `e7c6a61`, `X-Robots-Tag: index, follow`. Thirty-one commits on `main`. **September 8 pack day.** Live lander matches redesign. Catalog still 0/0. Draft only. ![Illustrative faculty row, five AI course-bro operators](/blog/everythingmonetized-parody-lms-course-bros/screenshots/bros.png) ## Why Satire of the course economy fails when it is only a landing meme. It needs personas, a catalog, an admin plane, and a checkout that confesses the joke. I wanted that spine in Convex. I also refused to pretend production was seeded. Robots say index,follow. Courses say 0 of 0. Seed.ts says ten high-ticket absurdities. Those three sentences together are the operator note. If you ship a parody LMS next, what do you fix first: seed Convex so the bit has inventory, or leave Showing 0 of 0 as the meta punchline? --- # DJ Side Three: WNC wedding DJ funnel after cutting Clerk Source: https://www.michaelchurley.com/blog/djsidethree-wnc-wedding-dj-funnel-after-clerk-cut Published: 2026-08-08 Author: Michael C. Hurley Tags: djsidethree, wedding-dj, western-north-carolina, asheville, local-service, nextjs, convex, inquiry-form, phosphor, vercel ![DJ Side Three homepage hero](/blog/djsidethree-wnc-wedding-dj-funnel-after-clerk-cut/screenshots/home.png) ## Who I wanted a wedding DJ site for Western North Carolina that put package prices on the page instead of hiding them behind “request a quote.” Couples in Asheville, Boone, and Highlands still get Instagram DMs and PDF menus. I wanted a one-pager: soundtrack promise up top, three packages with numbers, availability form at the bottom, admin board for whoever answers the phone. This is for engaged couples shopping entertainment, for venues that need a real link, and for operators who will ship Convex inquiries before they mount a full SaaS auth stack on a marketing page. ## What Live at [djsidethree.com](https://www.djsidethree.com/). Private repo [HurleyUS/djsidethree.com](https://github.com/HurleyUS/djsidethree.com). Package **djsideThree** **0.1.0**, Bun, Next.js **16.2.6**, React 19, Tailwind 4, purple/pink on black. Lockfile reality: Convex, Resend, PostHog, Stripe, Phosphor, Zod, React Hook Form, next-themes. README still lists Clerk/Sentry/Radix from the scaffold era. Those got cut or never shipped in the live tree. ![Services. ceremony, reception, lighting](/blog/djsidethree-wnc-wedding-dj-funnel-after-clerk-cut/screenshots/services.png) What the page actually sells: - **Ceremony** from **$500**. sound, two wireless mics, prelude/processional/recessional, officiant coordination - **Reception** from **$1,200** (Most Popular). four-hour DJ, lighting, MC, requests, cocktail hour, backup gear - **Full Day** from **$1,500**. ceremony + reception, up to eight hours, upgraded lighting, rehearsal/timeline help ![Wedding packages $500 / $1200 / $1500](/blog/djsidethree-wnc-wedding-dj-funnel-after-clerk-cut/screenshots/packages.png) Inquiry path: `#contact` form to Zod client validation to Convex `inquiries.create` (future date check, normalize email, **3/email/hour** server rate limit, Resend notification schedule). Client also throttles resubmits to 60 seconds. Testimonials pull approved Convex rows or fall back to three static couple quotes. `/admin` shows New / Contacted / Booked / Total. On 2026-09-08 capture: all zeros and skeleton rows (no live inquiry data in that session). ![Check Availability inquiry form](/blog/djsidethree-wnc-wedding-dj-funnel-after-clerk-cut/screenshots/contact.png) Honesty: hero claims **200+** weddings / **5.0** / **10+** years are marketing copy. Footer phone is `(828) 555-0123`. Stripe is a dependency and a Cursor rule, not a deposit checkout on the page. Clerk identity checks remain in Convex admin functions after the Clerk provider was removed. The admin board is not secure by UI hope. ## Where Public product: [www.djsidethree.com](https://www.djsidethree.com/) (apex 307s to www). Vercel alias `djsidethree-com.vercel.app`. Routes: `/`, `/admin`. Single-page anchors: `#services` `#packages` `#testimonials` `#contact`. Code stays private under HurleyUS. Host Vercel; Blacksmith/prebuilt CI path in May; HEAD sets `X-Robots-Tag: index, follow`. ## When **2026-01-08.** Scaffold Next + Convex + Tailwind. Same day: OPPORTUNITIES/PLAN, then **complete DJ landing MVP**. hero, services, packages, testimonials, contact, plus seed script, Resend path, and admin dashboard. That is the product surface. **2026-02-06 to 13.** Next 16 / React 19, Tailwind v4 CSS import fix, Convex generated types, dark mode defaults, build/design-rule cleanup. **2026-02-21-22.** Hardening week: inquiry auth checks + validation, focus rings, label associations, mobile hamburger, skeletons, Phosphor instead of lucide, Zod + rate limiting, Turnstile CAPTCHA, Server Component extract for static sections, Clerk middleware on `/admin`. **2026-02-27.** Plot twist: **`fix: remove Clerk, Turnstile, and Sentry, fix production 500 error`** (`be461fc`). Auth/CAPTCHA/error-tracking chrome was cheaper to delete than to keep misconfigured in prod. **Late Feb to early March.** P1 security follow-ups, inquiry rate limit **3/email/hour**, env validation fail-fast. **May.** Seven “Standardize Blacksmith CI gates” commits plus deploy URL verification. CI theater, little marketing surface change. **2026-08-08.** HEAD `2c804ad`. robots index/follow header. Forty-three commits on the clock. ## Why A wedding DJ site that hides price and inquiry behind “book a call” is a brochure. I wanted packages and a form in production first. When Clerk + Turnstile + Sentry 500’d the marketing page, I cut them. Zod, rate limits, Phosphor, and the Convex inquiry shape stayed. Deposits and real admin auth can earn their way back. The soundtrack page should not die for missing keys. Would you keep an unprotected `/admin` board after ripping Clerk, or is a single shared secret / Convex auth identity the next Saturday morning? --- # Cravees: I built a catering martech agency site with dual pricing honesty Source: https://www.michaelchurley.com/blog/cravees-catering-martech-agency-site Published: 2026-08-08 Author: Michael C. Hurley Tags: cravees, catering, martech, marketing-agency, nextjs, convex, clerk, stripe, roi-calculator, hospitality, local-seo, catppuccin ![Cravees home. Turn hungry searches into booked tables](/blog/cravees-catering-martech-agency-site/screenshots/home.png) ## Who I got tired of watching catering businesses buy generic agency retainers that treat a wedding buffet like a SaaS landing page. Catering is feast-or-famine. Wedding season starts when venues book. Corporate lunch messaging is not private-event messaging. Deposit timing matters. A Google Business profile can become tasting appointments, or a photo graveyard. Cravees is for caterers and restaurant catering arms who need demand this month: inquiry lift, reorder lists, review velocity. Operators who want a vertical agency site on a real stack (Next, Convex, Clerk, Stripe) with lead status enums that survive a failed Resend delivery. Builders who will admit the public pricing cards and the Stripe `planSlug` ladder are not the same numbers yet. If you have ever explained to a marketing generalist why plated service and buffet are different offers, this is for you. ## What I built **Cravees**, package name `cravees`, version **0.1.0**, private under **HurleyUS/cravees.com**. Metadata line: *Marketing Agency for the Catering Industry*. Hero line: **Turn hungry searches into booked tables, events, and repeat orders.** Stack facts from the lockfile and tree, not the stale sitrep: **Next.js 16.2.6**, **React 19.2.6**, **Tailwind 4.3**, Bun, **Convex** schema + mutations, **Clerk** auth surfaces, **Stripe** checkout/portal/webhook, Catppuccin Mocha with peach primary, Resend/Sentry/PostHog/GA wired. Seventy-three commits. HEAD `ab0c4cb`. ![Pricing. Starter Growth Premium](/blog/cravees-catering-martech-agency-site/screenshots/pricing.png) Surfaces that exist in `app/`: `/` marketing with demand cockpit + case studies + testimonials + FAQ + ROI embed, `/about`, `/pricing` with comparison table, `/blog` + three SEO posts, `/contact` enhanced form (business type + service interests), `/book` Calendly, `/roi-calculator`, four `/services/*` landings, Clerk catch-alls, private `/dashboard` (+ messages, assets), newsletter + Stripe API routes. Convex tables: `leads`, `subscribers`, `clients`, `subscriptions`, `addOnPurchases`. Lead pipeline: **new to contacted to qualified to proposal to won | lost**. Public packages: Starter **$299**, Growth **$599**, Premium **$999**. Stripe definitions in `lib/billing.ts`: Bronze **$499**, Silver **$999**, Gold **$1,999** (+ annual labels). Add-ons: extra social post $49, rush design $199, additional listing $99. That dual ladder is not a typo in this write-up. It is the product honesty. ![ROI calculator](/blog/cravees-catering-martech-agency-site/screenshots/roi-calculator.png) ## Where Code lives at [github.com/HurleyUS/cravees.com](https://github.com/HurleyUS/cravees.com). **private**. GitHub homepage points at [cravees-com.vercel.app](https://cravees-com.vercel.app). Custom domain [www.cravees.com](https://www.cravees.com) also **200** at pack time. Apex redirects to www. Auth-gated `/dashboard` and `/sign-in` returned **500** without living Clerk keys. marketing stays up because May commits taught the app to tolerate missing auth/env. Audience sits next to restaurant marketing shops, with a catering-only pitch: inquiry funnels, seasonal search pages, winback email for lapsed brunch guests. Sibling operator furniture is present. `.hustlemc`, Blacksmith ship workflow, Vitest/Playwright, Stripe type-safe webhooks. ![About. catering-only positioning](/blog/cravees-catering-martech-agency-site/screenshots/about.png) ## When **2026-01-08.** `8b80d36` Initial commit: Next.js, Convex, Tailwind. Same morning OPPORTUNITIES. Afternoon PLAN. Market thesis lands day one: ~12k dedicated caterers, $60B+ industry framing, Starter/Growth/Premium sketch. **2026-01-31 / 2026-02-05.** Sync. Prod build ready. Tailwind v4 globals `@import` fix. **2026-02-13.** Catppuccin Mocha default. Routes, contact, pricing, about. Four service landing pages. Replace a real business name with fictional **Copper Kettle Catering**. quiet ethics commit. **2026-02-21–24.** The agency week. Zod schemas shared across form + API. XSS sanitize on contact HTML email. Clerk middleware. Convex lead storage. FAQ. Testimonials. Pricing comparison table. Blog section. Calendly booking. PostHog. ROI calculator page. Portfolio/case-study cards. Newsletter + Resend double opt-in. Protected client portal scaffold. **2026-03.** SEO robots/sitemap/GA. Stripe type-safety and event guards. Enhanced contact form. Interactive ROI on the homepage. **2026-05-14–22.** Blacksmith CI and deploy verification. Keep public site up without auth env; disable Clerk UI for dummy keys; tolerate invalid Resend sender. Redesign commits. Merge blacksmith migration PR #64. **2026-08-08, 06:54 ET.** `ab0c4cb` set X-Robots-Tag to index, follow on Vercel. HEAD. Seventy-three commits total. **Pack day 2026-09-08.** Draft and assets only. Live marketing 200 on vercel + www. `sitrep.md` still claims purpose unclear and last commit February 5. treat it as drift, not truth. ![Blog index. three catering posts](/blog/cravees-catering-martech-agency-site/screenshots/blog.png) ## Why I did not want another hospitality brochure with stock plate photography and a contact form that emails into the void. I wanted lead status enums next to subscription sync. I wanted an ROI calculator that routes into the same contact pipeline as the pricing CTA. I wanted newsletter double opt-in and a client portal sidebar even while `/dashboard` still 500s without Clerk. I wanted the public $299/$599/$999 story and the Stripe bronze/silver/gold amounts to be visible in the same repo so nobody pretends they already match. So the February issue board became the agency: close FAQ, social proof, pricing table, blog, book, analytics, ROI, newsletter, portal. March hardened SEO and Stripe. May made the public site survive missing secrets. August flipped robots headers on a site that actually answers. Would you ship the public Starter/Growth/Premium ladder first, or make the Stripe bronze/silver/gold amounts match the pricing page before the next catering discovery call? --- # Coordinator: I shipped an API queue control plane before the queue worker existed Source: https://www.michaelchurley.com/blog/coordinatorapp-api-queue-control-plane Published: 2026-08-08 Author: Michael C. Hurley Tags: coordinator, coordinatorapp, api-queue, rate-limiting, nextjs, convex, clerk, stripe, zapier, backpressure, hurleyus ![Coordinator home. Reliable queues for the APIs your product depends on](/blog/coordinatorapp-api-queue-control-plane/screenshots/home.png) ## Who I got tired of watching production workers invent their own Stripe and GitHub throttles. Every SaaS eventually grows a Friday-night 429 story. Someone hard-codes a sleep. Someone else adds a Redis queue "just for this provider." Zapier looks fine until flood protection is the product and connector count is the brochure. Coordinator is for operators who want a **hosted control plane** for rate limits, retries, backoff, and replay. before they pretend the SDK already ships. Production teams who need queue depth and provider health readable in one zinc panel. Builders comparing Zapier / Make / n8n on backpressure instead of logo walls. If you have ever paused a HubSpot sync because the provider blinked, this is for you. ## What I built **Coordinator**. package name `coordinatorapp`, version **0.1.0**, private under **HurleyUS/coordinatorapp.com**. Metadata: *Smart API Queueing & Rate Limiting*. Tagline energy: **Connect. Queue. Execute.** Hero: **Reliable queues for the APIs your product depends on.** Stack facts from the lockfile and tree, not the stale sitrep: **Next.js 16.2.6**, **React 19.2.6**, **Tailwind 4.3**, Bun, **Convex**, **Clerk**, **Stripe** checkout/portal/webhook with dynamic `price_data`, Sentry, PostHog, GA. Fifty-two commits. HEAD `b7e9b3b`. ![Live pricing. Free / $29 / $79](/blog/coordinatorapp-api-queue-control-plane/screenshots/pricing.png) Surfaces that exist: `/` zinc lander with DashboardPreview (24,891 queued / 1,204 429s prevented / 2.4s retry; Stripe / GitHub / HubSpot rows), `/pricing`, `/docs` (every guide card says Coming soon), `/privacy`, `/terms`, `/sign-in`, `/sign-up`, `/events/[eventId]`, Stripe API routes under `app/api/stripe/`. Convex tables: `waitlist`, `events`, `attendees`, `users`, `subscriptions`, `addOnPurchases`. Waitlist mutation + `WaitlistForm` exist. **not wired into the May redesign homepage**. Event RSVP (Mar 20 #31) tracks capacity, dietary notes, confirmed/tentative/cancelled. ![Billing drift. pricing page vs lib/billing.ts](/blog/coordinatorapp-api-queue-control-plane/screenshots/billing-drift.png) Money path split brain: **live pricing page** sells Starter Free / Pro **$29**/user/mo / Business **$79**/user/mo. **`lib/billing.ts`** (what checkout reads) prices Starter **$19**, Pro **$49**, Business **$149**, plus Extra Executions **$19** and Priority Support **$99**. OPPORTUNITIES still promises an **n8n** backend. There is **no n8n** in the tree. Homepage teases `coordinator.queue(...)`, the SDK is marketing, not a package. ## Where Code: [github.com/HurleyUS/coordinatorapp.com](https://github.com/HurleyUS/coordinatorapp.com). **private**. Live: [www.coordinatorapp.com](https://www.coordinatorapp.com) (**HTTP 200**, apex 307 to www). GitHub homepage: [coordinatorapp-com.vercel.app](https://coordinatorapp-com.vercel.app) (same prerender etag). `X-Robots-Tag: index, follow` from the Aug 8 `vercel.json` fix. ![Docs. Coming soon](/blog/coordinatorapp-api-queue-control-plane/screenshots/docs.png) Audience sits next to Zapier / Make / n8n / Tray. flood protection and retry observability as the wedge. Sibling operator furniture: Blacksmith ship gates, Biome, Stripe snake_case webhook casting, Enterprise mailto `michael@hustlelaunch.com`. ![Sign-in. Clerk keys required](/blog/coordinatorapp-api-queue-control-plane/screenshots/sign-in.png) ## When **2026-01-08.** `ae06d85` init Next/Convex/Tailwind. Same day OPPORTUNITIES + PLAN. Zapier competitors, flood protection, n8n-powered claim on day one. **2026-02-06.** `2b54644` ship landing page: Next 16, dark theme, queue visualization. **2026-02-13–22.** Stub pages, Clerk middleware + env validation, strip unverified SOC 2 claim, Navbar/Footer, Sentry, PostHog, site config, error/loading/not-found. **2026-03-17–18.** www / Clerk middleware 500 debug cascade (aliases, middleware off, vercel.json thrash). robots, sitemap, GA, next-themes. Waitlist + Convex backend. **2026-03-20–22.** Event signup form + attendee management (#31). Stripe standardization + webhook type casting. **2026-05-13–15.** shipprep + Blacksmith CI / deploy verification burst. **2026-05-22, 06:09 ET.** `3eec7ae` **redesign**, the live zinc control-plane lander. **2026-08-08, 06:54 ET.** `b7e9b3b` set X-Robots-Tag to index, follow. HEAD. Fifty-two commits. **Pack day 2026-09-08.** Live Chromium shots. www 200. Sign-in: **Clerk keys required**. `sitrep.md` still says prototype / no auth / Feb 6. treat as drift. ![SDK teaser on the lander](/blog/coordinatorapp-api-queue-control-plane/screenshots/sdk-snippet.png) ## Why I did not want another automation brochure with a fake connector grid and no backoff story. I wanted a control plane face. queued requests, 429s prevented, retry delay. before the worker existed. I wanted Stripe subscription sync and Convex waitlist/events on disk even while docs say Coming soon and the homepage SDK call is a `
`.

So January named the category. February shipped the face. March fought www 500s, captured waitlist emails, bolted on event RSVP, and hardened Stripe. May redesign locked the zinc hero. August flipped robots on a live site that still needs Clerk env vars.

Would you fix the pricing page to match `billing.ts`, or ship Clerk keys to www before anyone can Start with GitHub?

---

# Convex NextFaster: I swapped Neon for a Convex e-commerce template before the demo store existed

Source: https://www.michaelchurley.com/blog/convex-nextfaster-perf-meets-convex-ecommerce-scaffold  
Published: 2026-08-08  
Author: Michael C. Hurley  
Tags: convex-nextfaster, nextfaster, nextjs, convex, clerk, stripe, sentry, posthog, resend, ecommerce, template, ppr, performance

![Convex NextFaster home mock](/blog/convex-nextfaster-perf-meets-convex-ecommerce-scaffold/screenshots/home.png)

## Who

I got tired of cloning e-commerce starters that worship Postgres or pretend performance is a CSS animation.

NextFaster proved PPR, prefetch, mouseDown nav, React Compiler, inline CSS. I wanted that DNA on Convex with Clerk, Stripe, Resend, Sentry, PostHog.

For operators who ship schema before fake catalog. If you clicked Shop Now on your own template and hit a 404, you are in the room.

## What

I built Convex NextFaster — package convex-nextfaster v1.0.0, public HurleyUS/convex-nextfaster. fork=false; 355 upstream + 5 mine.

Stack: Next.js 15.3.0, React 19, PPR + inlineCss + reactCompiler, Convex (1115 LOC), Clerk, Stripe, Sentry, PostHog, Resend.

## Where

https://github.com/HurleyUS/convex-nextfaster — public. No homepage. No demo.

## When

2026-01-08 a18c09c cutover; c635ab3 drop data.zip; e1164e9 PLAN.
2026-01-31 db28fe7 sync.
2026-08-08 125dd75 X-Robots-Tag. HEAD. 360 commits; 5 mine.

## Why

Perf demos that force SQL as destiny are a tax. A Convex cart with expiry beats Lighthouse of a deleted tree. Shipping 1.0.0 with PLAN Not Started is honesty.

Who else stars templates that document routes they never created?

---

# Citation Manager: I built an Uberall competitor with 958 directories — and left auth on three stacks

Source: https://www.michaelchurley.com/blog/citation-manager-uberall-competitor-958-directories  
Published: 2026-08-08  
Author: Michael C. Hurley  
Tags: citation-manager, citations, local-seo, nap, uberall, brightlocal, yext, directories, convex, nextjs, clerk, hurleyus, saas

![Citation Manager dashboard mock. locations, submit, directories, submissions](/blog/citation-manager-uberall-competitor-958-directories/screenshots/dashboard.png)

## Who

I build operator tools for people who get paid when the NAP is right: agencies, multi-location owners, anyone tired of logging into Google, Yelp, and a dozen legacy directories by hand.

Citation Manager is that lane. Business listings out. Tracking what stuck. Skip the academic bibliography frame and the WNC city-guide frame.

If you care about local SEO plumbing, directory registries, or how a two-week SaaS sprint accumulates three auth stacks and a 500 on the homepage URL, these are the field notes.

## What

I shipped `HurleyUS/citation-manager`. Public TypeScript repo, package **0.0.1**, **83** commits, HEAD `78e9fb1`.

GitHub description is blunt: manage business listings across 1000+ directories. **Uberall competitor.** The README narrows it to **958+** directories and names Uberall, BrightLocal, and Yext as the alternatives it wants to undercut on price ($99 vs $500–2000/mo copy in the roadmap).

Stack on the box: **Next.js 16.2**, React 19, **Convex**, Tailwind v4, Bun, Lucide, Puppeteer, Argon2. Clerk and `@convex-dev/auth` sit in `package.json`; Fallow marks both unused while the UI talks to `/api/auth` and `localStorage` tokens.

![Directory registry mock. rank, method, API flags from directories.json](/blog/citation-manager-uberall-competitor-958-directories/screenshots/directories.png)

The real artifact is `data/directories.json`: **958** rows. Rank 1 is Google Business Profile. Methods break down api 88 / form 773 / manual 70 / email 27. `apiAvailable` is true on 219. Convex schema mirrors that world: `locations`, `directories`, `submissions` with pending to submitted to verified to failed, plus `verifications`.

Surfaces that exist: auth, dashboard, locations CRUD, directories browser with "View All 958", bulk submit with search/filter, submissions tracker, seed-directories API, google/yelp/facebook API routes.

What is also true:

- `bulkSubmit` inserts `pending` rows. The schedule helpers flip to `submitted` only if env keys exist. They do **not** call the fetch helpers in that path.
- `generateGoogleJWT` is a documented **placeholder**.
- Login hashes the password with Argon2 again and string-compares to the stored hash. New salt means verify cannot work as written.
- Dashboard copy still says "100+ directories" while the registry is 958.

![Bulk submit mock. location + directory multi-select](/blog/citation-manager-uberall-competitor-958-directories/screenshots/submit.png)

## Where

It is supposed to run on Vercel against Convex. Dev uses Caddy `cm.localhost:8080` to Next on 3000.

Probed 2026-09-08:

- **https://citation-manager-pi.vercel.app** (GitHub homepageUrl) returns **500** `MIDDLEWARE_INVOCATION_FAILED`
- **https://citation-manager.vercel.app** returns **200**, but it is a different "research workflow" citation app with Admin/User portals. **Name collision, not this product.**

Audience: local-SEO operators, agencies replacing BrightLocal/Yext spend, builders watching auth and integration honesty in a Convex/Next SaaS.

![Deploy reality mock. 500 homepage vs name-collision 200](/blog/citation-manager-uberall-competitor-958-directories/screenshots/deploy-reality.png)

## When

Created **2026-03-22**. Same day: scaffold, directory research, API skeletons, Clerk blank-page fix, Convex Auth swap, "FULL PHASE 2 READY FOR 6PM SHIP," Google Maps submission claim, locations wired to Convex.

Late March: Argon2, push-to-directories UI, View All 958, submissions dashboard.

**2026-04-01–02.** Registry expanded 100 to 958, Issue #12 Google/Yelp/Facebook modules, Clerk middleware returns, Phase 2B bypass + test infra, form validation, GBP PR.

**2026-05-14–15.** Seven "Standardize Blacksmith CI gates" commits (same batch pattern as sibling repos) plus deploy verification fixes.

**2026-08-08.** HEAD sets `X-Robots-Tag: index, follow`. Same day pattern as other HurleyUS pushes.

README still says Phase 2A 100% complete and Phase 2B "current" as of early April. Stripe is still Phase 4 fiction.

![Auth whiplash timeline. Clerk to Convex Auth to Argon2 to Clerk middleware to bypass](/blog/citation-manager-uberall-competitor-958-directories/screenshots/auth-whiplash.png)

## Why

Local citations are still a grind. The expensive tools win on coverage and integrations, not on elegance. I wanted an API-first Convex backend, a ranked directory registry I own as JSON, and a submit queue I can reason about in one schema.

I also wanted to ship before the story was clean. That is why the homepage 500s, why login verify is wrong, why Clerk middleware and a bypass flag coexist, and why the integration modules look finished while bulkSubmit mostly writes `pending`.

The registry is real. The competitor framing is real. The production URL on the GitHub homepage is not a product yet. It is an error page.

If you run citations for clients: would you trust a 958-row registry with honest `pending` states more than a vendor dashboard that always says "submitted"? What is the minimum live integration (Google only) before this is worth putting a real domain on?

---

Source: https://www.michaelchurley.com/blog/breazyapp-pocket-peo-aes-before-stripe  
Published: 2026-08-08  
Author: Michael C. Hurley  
Tags: breazyapp, peo, hr, payroll, restaurants, nextjs, convex, clerk, hurleyus

# BreazyApp pocket PEO

**Slug:** breazyapp-pocket-peo-aes-before-stripe

**Excerpt:** Next.js 16 + Convex + Clerk pocket PEO for chain restaurants. Four portals. Waitlist live. Billing UI 49/location + 4/employee. No stripe package. HEAD 2402b35.

**Tags:** breazyapp, peo, hr, payroll, restaurants, nextjs, convex, clerk, hurleyus

---

![home](/blog/breazyapp-pocket-peo-aes-before-stripe/screenshots/home.png)

## Who

Multi-unit restaurant and franchise operators who need HR, payroll, accounting, and benefits in one pocket PEO.

## What

breazyapp 0.1.0 private HurleyUS. Live www.breazyapp.com. Portals: employee, manager, HR, admin. Commit 062ade1 at-rest field protection. Feb 6 smoke-shop lander reverted in 15 minutes.

![waitlist](/blog/breazyapp-pocket-peo-aes-before-stripe/screenshots/waitlist.png)

## Where

github.com/HurleyUS/breazyapp.com · www.breazyapp.com (200) · apex 307 · robots index,follow

## When

2026-01-08 init. 2026-02 PEO + Clerk + Catppuccin. 2026-08-08 HEAD robots. Pack 2026-09-08 draft only.

## Why

Portal shells and field protection before pretending checkout shipped.

Which tool would you delete first across five restaurants?

---

# Bar-B-Que Wagon: I built a Bryson City Main Street smokehouse site

Source: https://www.michaelchurley.com/blog/barbquewagon-bryson-city-hickory-smokehouse-site  
Published: 2026-08-08  
Author: Michael C. Hurley  
Tags: barbquewagon, bar-b-que-wagon, nextjs, convex, resend, catering, local-business, bryson-city-nc, martech, json-ld, smokehouse

![Bar-B-Que Wagon homepage. Slow Smoked / Hand Pulled / Soul Fed over the Main Street sign](/blog/barbquewagon-bryson-city-hickory-smokehouse-site/screenshots/home.png)

## Who

I build for operators. Sometimes that operator is me. Sometimes it is a pitmaster on Main Street in Bryson City, North Carolina.

Bar-B-Que Wagon needed a site that smelled like hickory, not a beige restaurant theme with a stock smoke PNG, and a catering path that did not die in a "we will call you back" void. Guests needed hours, address, phone, and a board that matched what Pat Monteith actually smokes. Planners needed guest count and event type without playing phone tag first.

I am the builder. The food is theirs. The repo is public under HurleyUS. The stack is mine to keep honest.

## What

I shipped a Next.js 16 App Router site. React 19. Bun. Tailwind 4. Biome and oxlint. Phosphor icons. Playfair Display for the smokehouse voice. DM Sans for the rest. Dark tokens: deep-smoke background, amber accents, cream type.

The homepage hero stacks three lines. Slow Smoked. Hand Pulled. Soul Fed. Over the exterior sign photo with a charcoal gradient. Nav is sticky and blurred. Logo mark on warm-white tile. Tagline Smoke · Soul · Flavor. Amber Order Now button that routes to `/contact` because there is no DoorDash integration pretending to be hospitality.

![Brisket plate. Yelp-sourced food photography in the repo](/blog/barbquewagon-bryson-city-hickory-smokehouse-site/screenshots/brisket-plate.jpg)

The menu is a real board in the page: smoked meat plates with two sides and cornbread, sandwiches including The Wagon Burger, sides made from scratch. Featured cards and a gallery pull Yelp food and exterior shots that landed in the repo on February 15. About is Pat's story: twenty-plus years, 610 Main Street, no franchise fiction.

Catering is a real form: name, email, phone, event date, guest-count select, event-type select, optional message. The API validates with Zod, writes a Convex `leads` row when Convex is configured, and fires Resend to the owner inbox. Contact does the same twin path. Sentry on the edges. PostHog on the pageviews. Restaurant, Menu, and FoodService JSON-LD from one business-info object.

![Pork ribs plate asset](/blog/barbquewagon-bryson-city-hickory-smokehouse-site/screenshots/pork-ribs.jpg)

## Where

610 Main St. Bryson City, NC 28713. Phone 828-488-9521. Hours Tue–Sat 11–8, Sunday 11–6, closed Monday.

The URL that answers today is [barbquewagoncom.vercel.app](https://barbquewagoncom.vercel.app). That is what GitHub lists as the homepage. Schema and copy still say `barbquewagon.com`. At pack time that apex has no DNS. Facebook is wired in the footer. Instagram is still a dead pound-sign href. Repo is public: [HurleyUS/barbquewagon.com](https://github.com/HurleyUS/barbquewagon.com).

The audience for *this* write-up is builders who care how a local BBQ site actually captures a wedding headcount, and anyone in the Smokies who already knows the wagon on Main.

![Exterior. Building hero source](/blog/barbquewagon-bryson-city-hickory-smokehouse-site/screenshots/building-1.jpg)

## When

**2026-02-13.** Create Next App. Same night: full restaurant website. Same night again: throw out the placeholder details for real Bar-B-Que Wagon facts.

**2026-02-15.** Zod on the contact form. Yelp photos across the homepage: food gallery, menu cards, exterior. Hero background with gradient overlay.

**2026-02-21.** Lexington was wrong. Bryson City is right. That correction shipped across metadata and catering copy. Then full try/catch and Zod on both forms.

**2026-02-22.** next.config. Drop unused ThemeProvider. PLAN and CHANGELOG. Wire contact and catering to Resend. JSON-LD for Google rich results.

**2026-03-01.** TODO.md. It still lists "wire Convex/Resend" as unchecked. The commits disagree.

**2026-05-13 through 15.** Shipprep. Logo assets. Bun on Vercel. Form refactor and Fallow cleanup. Roadmap. A string of Blacksmith CI gate commits. Observability scaffolding. Deploy URL verification until Blacksmith stopped lying.

**2026-08-08.** `X-Robots-Tag: index, follow` in Vercel headers. HEAD settled. Thirty-eight commits from init. That is the clock.

![Pulled pork platter](/blog/barbquewagon-bryson-city-hickory-smokehouse-site/screenshots/pulled-pork-platter.jpg)

## Why

A Main Street BBQ needs the board, the hours, the phone, and a catering form that still works when the dining room is loud.

I wanted the hero to feel like the sign on Main, not a stock smoke photo. I wanted leads in Convex and in the inbox, from Zod-validated routes, not a mailto cosplay. I wanted Schema.org to carry the same brisket and pulled pork the menu page shows. I wanted the city name to be Bryson City everywhere a crawler looks.

Yelp plates went into `public/`. Resend and JSON-LD shipped. Geography got fixed. May ops gauntlet ran. Robots header locked in August. Operator stack. Local business. Public repo.

The custom domain is still dark. The Vercel alias is live. The Instagram link is still a pound sign. PLAN.md still thinks the forms are unwired.

If you were standing at 610 Main tonight, which plate would you order before the kitchen sells out?

---

# Appalachian Estate Sales: I rebuilt the Elementor site in Next before the gallery existed

Source: https://www.michaelchurley.com/blog/appestatesales-elementor-to-next-wnc-liquidation  
Published: 2026-08-08  
Author: Michael C. Hurley  
Tags: appalachian-estate-sales, appestatesales, elementor, nextjs, western-north-carolina, waynesville, estate-sales, local-business, convex, resend, martech, hustle-launch

![Appalachian Estate Sales homepage, live capture](/blog/appestatesales-elementor-to-next-wnc-liquidation/screenshots/home.png)

## Who

I build local sites for operators who already have customers and an Elementor habit. Appalachian Estate Sales is Rene' Rickman Ballard's liquidation and downsizing practice in Waynesville: Haywood through Henderson Counties, phone on the header, Facebook already running the sale calendar.

The audience for the business is families mid-transition: bereavement, assisted living moves, divorce. The audience for this write-up is anyone shipping an Elementor-to-App-Router rebuild who still needs lead capture to work on day two.

## What

I rebuilt the marketing site as **Next.js 16.1.7** (App Router), React 19, Tailwind v4, Bun, shadcn/ui, under private `Hustle-Launch/appestatesales-com`. Package.json still names the app `web` at **0.1.0**.

![Convex schema: leads and subscribers](/blog/appestatesales-elementor-to-next-wnc-liquidation/screenshots/schema.png)

What exists in code and on the live host:

- Seven routes: home, about-rene, estate-sale-services, estate-sales-process, previous-estate-sales, upcoming-estate-sales, contact-us
- Layout stack: Header (logo + porch swing + phone), cyan Sidebar nav, Footer contact form, floating mobile bar
- Design tokens pulled from the Elementor screenshots: cream ground, cyan nav, coral subscribe/CTA
- Markdown under `content/` as the copy source of truth
- Schema.org LocalBusiness + FAQ JSON-LD (`app/schema.ts`) pointing at `https://appestatesales.com`
- Convex tables `leads` and `subscribers` with email indexes
- `/api/contact` and `/api/subscribe` writing Convex + Resend (notify@uncap.us to AES Gmail + a michaelmonetized lead alias)
- Sentry project `appestatesales`, PostHog provider slot, Blacksmith `ship.yml` prebuilt deploy
- Estate Sale Liquidator Pros badge on the home article

What is still a stand-in: Previous and Upcoming pages embed the Facebook Page plugin for `estate.tag.sales.wnc`. There is no Convex-backed sales gallery yet. README migration checklist still marks Convex and Resend as "coming soon" even though both paths are in the tree.

![Shipped pages vs Facebook-embed gap](/blog/appestatesales-elementor-to-next-wnc-liquidation/screenshots/stack-gap.png)

## Where

Live: [www.appestatesales.com](https://www.appestatesales.com/). HTTP 200 on pack day, `X-Robots-Tag: index, follow`. Apex redirects 307 to www. Vercel alias `appestatesalescom.vercel.app` serves the same prerender.

`vercel.json` turns **off** git auto-deploy for `main`/`master`; production moves through Blacksmith + `vercel deploy --prebuilt`.

Geography in schema and copy: Waynesville NC 28786, serving Haywood, Buncombe, Jackson, Macon, Swain, Henderson. Social sameAs: Facebook + Instagram.

One liquidator's marketing site for Appalachian Estate Sales.

## When

**2026-03-17.** Init (`916df66`). Same afternoon: images + Resend contact wiring, Convex and Vercel link, test deploy, styling/logo pass, standards.css + shadcn forms, Schema.org LocalBusiness + FAQ. Densest product day.

**2026-03-18.** viewTransition typing fix, sidebar styles, styling cleanup, Convex wiring cleanup, mobile bar "needs work", polish, Resend audience-id fix, build optimizations.

**2026-03-19 10:08 AM ET.** `669a106`: Facebook embed done. Upcoming/previous now lean on the social feed.

**May 13–15.** shipprep standards install (twice) then revert (twice). Churn, not product.

**2026-05-21.** Apply shipprep observability and Blacksmith deploys; harden Sentry runtime (env-driven DSN, no default PII).

**2026-08-08 6:54 AM ET.** `7a87d72`: set `X-Robots-Tag` to `index, follow` on Vercel. HEAD. Twenty-four commits on `main`.

![Commit journey March through August](/blog/appestatesales-elementor-to-next-wnc-liquidation/screenshots/journey.png)

## Why

Elementor migrations fail two ways: pixel-perfect CSS with a dead contact form, or a modern stack with a blank homepage. I aimed at the honest middle: familiar cream/cyan layout clients recognize, seven pages of real copy, and lead capture that actually posts to Convex and Resend.

The sales calendar still lives on Facebook. The robots header asks Google to index a site whose previous-sales gallery is an iframe. That gap is the field note.

**Engagement Q:** If you run estate sales in Western North Carolina, would you trust a Next rebuild that still embeds your Facebook page for the calendar, or do you refuse to ship until Convex owns upcoming and previous sales?

---

# SantaBox.org: I rebuilt a BestWNC copy-paste into a Christmas charity lootbox platform

Source: https://www.michaelchurley.com/blog/santabox-charity-lootbox-rebuild  
Published: 2026-08-08  
Author: Michael C. Hurley  
Tags: santabox, charity, christmas, nonprofit, lootbox, donations, nextjs, convex, clerk, stripe, resend, vercel

![SantaBox home. Christmas 2026 campaign](/blog/santabox-charity-lootbox-rebuild/screenshots/home.png)

## Who

I got tired of charity landers that look like Christmas and behave like a brochure.

Toy drives need funding progress. Parents need a tax receipt path. Partners need an inquiry form that emails a human. Operators who inherit a wrong-vertical Next scaffold need an autopsy that says the quiet part: this README used to be BestWNC.

SantaBox is for people funding age-tagged gift boxes before December 15 delivery cutoffs, and for builders who will delete fake partner names when legal risk shows up in a commit message.

## What

I built **SantaBox.org**. Package `santabox.org`, version **0.1.0**, private under **HurleyUS/santabox.org**. Metadata line: *Christmas Gift Boxes for Children in Need.* Campaign badge on the live hero: **Christmas 2026 Campaign Now Open.**

Stack facts from the lockfile and tree: **Next.js 16.1.6**, **React 19.2.4**, **Tailwind 4.1**, Bun, **Convex** schema for gift boxes / donations / donors / subscribers / nonprofits / partner inquiries / impact stories, **Clerk** (optional when keys missing), **Stripe** checkout + subscribe APIs, Resend, Sentry, PostHog. Forty-two commits. HEAD `67712cb`.

Donate presets: $25 stocking · $50 half box · $75 small · $100 full (default) · $150 premium · $250 two boxes. Cover processing fees (`2.9% + $0.30`) so the gift side can stay whole. Subscribe UI/schema: **$10/mo** or **$100/yr**.

![Donate presets](/blog/santabox-charity-lootbox-rebuild/screenshots/donate.png)

## Where

Code stays private on GitHub. Product answers at **https://www.santabox.org** (apex 307 to www) and the GitHub homepage URL **https://web-iota-topaz-45.vercel.app**. Both returned marketing **HTTP 200** with `X-Robots-Tag: index, follow` on pack day.

Routes that still 500 without service env: `/impact`, `/subscribe`, `/create-wishlist`. May 20 commits explicitly keep the public site up when service env is missing. The 500s are the other side of that bargain.

No `public/` directory in the tree. Layout still points at `/og-image.png` and favicons that are not on disk. Partners page now sells “Team Up with SantaBox” plus a grid of real team projects instead of invented orgs.

![Partners. inquiry + project grid](/blog/santabox-charity-lootbox-rebuild/screenshots/partners.png)

## When

**January 8, 2026:** `eebf7a1`. Next.js, Convex, Tailwind scaffold. GitHub `created_at` is later (Feb 6). The clock and the hosting console do not owe each other an apology; the commit log does.

**February 6:** Next 16 / React 19 / Tailwind v4 import fix.

**February 9:** Docs stop lying about BestWNC. Major rebuild commit lands the charity storytelling surface. Clerk becomes optional so builds survive missing keys. AUTOPSY.md records the crime scene: wrong layout title, empty Convex, dead buttons, wrong year, subscription-box confusion vs donation README.

**February 13:** `10377d8`. remove fabricated nonprofit/sponsor data. Commit body names the liability. Replacement: partner inquiry form + Resend `/api/partner-inquiry` + project grid of confirmed live sites.

**February 15:** Stripe webhook + signature verification + donate button actually calls checkout.

**Late February:** force-dynamic for Clerk/Convex pages, `proxy.ts` protection, error boundaries, sitemap/robots, security headers, auth on user mutations, `.take(100)` on collects.

**March:** Sentry instead of console.error spam; Vitest; then drop GitHub Actions because Vercel is CI.

**May:** Blacksmith ship gates, Santabox typecheck fixes, deploy URL verification, public-site-without-service-env.

**August 8, 2026 6:50 AM ET:** `67712cb`. X-Robots-Tag index, follow. HEAD. Forty-two commits.

![Boxes browse](/blog/santabox-charity-lootbox-rebuild/screenshots/boxes.png)

## Why

A Christmas charity site that still wears another product's metadata is worse than an unfinished one.

Fabricated partner logos are not placeholder content. They are a lawsuit with good lighting.

Dual honesty shows up here too: PLAN.md still lists Convex schema and Stripe as not started while `convex/schema.ts` and `/api/checkout` exist; AUTOPSY celebrates “production ready” with unchecked env boxes; homepage hardcodes `statesReached: 42` even on the live Convex path; marketing claims 501(c)(3) without an EIN file in-repo. Say the drift out loud.

The money path is real enough to document: fee cover math, taxReceiptSent boolean, subscriber Stripe IDs, wishlist create client, box status enum `pending` to `open` to `funded` to `shipped` to `delivered`.

![Stories. Maya narrative](/blog/santabox-charity-lootbox-rebuild/screenshots/stories.png)

![About](/blog/santabox-charity-lootbox-rebuild/screenshots/about.png)

What would you delete first if you found another vertical's partner logos still living in your charity repo: the logos, or the launch date?

---

# shipthing: I named it for shipping rates and shipped a contacts spine instead

Source: https://www.michaelchurley.com/blog/shipthing-contacts-spine-not-carrier-rates  
Published: 2026-08-08  
Author: Michael C. Hurley  
Tags: shipthing, nextjs, convex, clerk, resend, sentry, posthog, proxy-ts, contacts, shipping, vercel, michaelmonetized

![ShipThing home. lead form + signed-in contacts table](/blog/shipthing-contacts-spine-not-carrier-rates/screenshots/home-contacts.png)

## Who

I wanted a shipping-rate desk for e-commerce sellers: compare USPS, UPS, FedEx, print labels, stop guessing retail rates.

What I built instead is an honest stack spine: Clerk auth, Convex contacts, Resend lead email, Sentry, PostHog, and a Next 16 `proxy.ts` filename law. People who read PLAN.md, then open the tree, and do not pretend the carrier boxes are checked.

If you have ever named a repo after the product you meant to ship and then shipped the scaffolding that every later app copies, this is that receipt.

## What

I built **ShipThing**. Package `shipthing` **0.1.0**, public under **michaelmonetized/shipthing**. Layout metadata title: **Shipthing**. Description: **Combining convex, posthog, clerk and sentry**. That description is more accurate than the repo name.

Stack from the lockfile: **Next.js 16.1.1**, **React 19.2.3**, Tailwind **4**, Bun, **Clerk**, **Convex**, **Resend** + React Email, **Sentry** (org `hustle-launch`, project `shipthing`), PostHog, zod 4, react-hook-form, Radix/shadcn UI. `stripe` sits in dependencies with a long `.cursor/rules/STRIPE.md`. **Zero app imports.** Thirty-nine commits. HEAD `9f91d97`.

![PLAN.md Not Started vs what the tree actually contains](/blog/shipthing-contacts-spine-not-carrier-rates/screenshots/plan-vs-shipped.png)

Surfaces that exist: `/` lead form ("Be the first to contact us!" / Send Message with name, 10-digit phone, email, message) plus signed-in **Contacts** table with delete; `/login`; `/sentry-example-page`; API routes `/api/send/notification` and `/api/send/confirmation`; `proxy.ts` Clerk middleware file; `sitemap.ts` / `robots.ts`.

Convex schema is a single `contacts` table. Search index on name, indexes by name/phone/email/page. Notification mail sends from `Notifications ` to `michaelmonetized@gmail.com` and `8285931935@vtext.com`. Confirmation is a short "Hey {name}, we received your message" React Email.

Navbar lists Security, Auth, Layout, Typography, Forms, Analytics, Error Tracking, Email, Realtime Data Sync, APIs, More. **No `/features/*` pages** in the tree. Footer still links Learn / Examples / nextjs.org from create-next-app.

`PLAN.md` still sells the other product: carrier APIs, rate comparison, ZPL/PDF labels, address validation, Shopify import, batch labels, tracking, cost analytics. Success metrics: active users > 500, monthly labels > 10,000, savings > 30%. Every checkbox is empty.

## Where

Code: [github.com/michaelmonetized/shipthing](https://github.com/michaelmonetized/shipthing). **Public.** Live: [shipthing.vercel.app](https://shipthing.vercel.app) (**HTTP 200**, Clerk signed-out chrome, `X-Robots-Tag: index, follow`).

![proxy.ts + check:proxy Next 16 guardrail](/blog/shipthing-contacts-spine-not-carrier-rates/screenshots/proxy-guardrail.png)

Audience sits next to every "I'll bolt carriers on next sprint" SaaS skeleton. Sibling operator furniture: Fallow gate notes in `AGENTS.md`, Bun-only local law, Blacksmith/Vercel prebuilt rules, env.template for Clerk + Resend + Convex.

![Resend notification + confirmation lead path](/blog/shipthing-contacts-spine-not-carrier-rates/screenshots/resend-lead-path.png)

## When

**2025-03-22.** Create Next App. Same day: Convex + PostHog + Sentry, not-found + shadcn button, middleware build thrash, Clerk, forms.

**2025-03-26–27.** Convex contacts land. Resend starts. Real bugs: could not access `name` in notification email, copy-pasta, split emails so sending stops after the first try/catch, more Resend fixes, light-mode toggle attempt.

**2025-03-28–29.** Navbar, error boundary, login. Then: `convex dev, i finally recovered my github login 🎉`.

**2025-04.** Layout components; **box, stack, deck** + Next update.

**2025-12-29.** React Server Components CVE pass.

**2026-01-08.** `PLAN.md` with shipping-rate "improvement opportunities." Jan 31 chore sync.

**2026-02.** CVE PR #1; rename `middleware.ts` to `proxy.ts` (#7); env.template (#8); proxy filename guardrail (#10); security headers (#11); sitemap + robots (#12).

**2026-06-22.** Nightly ×2.

**2026-08-08.** HEAD `9f91d97`: set `X-Robots-Tag` to `index, follow` on Vercel.

![Commit arc Mar 2025 to Aug 2026](/blog/shipthing-contacts-spine-not-carrier-rates/screenshots/commit-arc.png)

## Why

The shipping product needed a spine before it needed a carrier SDK, and the spine is what survived.

Next 16 renamed the middleware file. I wanted a script that fails if `middleware.ts` comes back (`bun run check:proxy`).

A lead form that emails me and texts `8285931935@vtext.com` is a product loop I can prove. A FedEx rate matrix I never integrated is not.

Naming the repo ShipThing and leaving PLAN.md full of unchecked USPS boxes is more useful as an operator story than as a fake launch post.

When your PLAN.md still lists the vertical and your `layout.tsx` description lists the stack, which one should the blog title obey?

---

# mission-control: I built a p10k Go TUI for the whole portfolio — not the agent thread plane

Source: https://www.michaelchurley.com/blog/mission-control-go-tui-p10k-portfolio-ops  
Published: 2026-08-08  
Author: Michael C. Hurley  
Tags: mission-control, go, bubbletea, tui, p10k, vercel, openclaw, convex, flyio, portfolio-ops, michaelmonetized

![p10k-style TUI zones. status, search, project list, chat, totals](/blog/mission-control-go-tui-p10k-portfolio-ops/screenshots/tui-p10k-layout.png)

## Who

I keep too many projects hot at once. Vercel rows. Swift builds. git dirt. GitHub issues and PRs. Browser tabs do not scale.

Hurley Mission Control is a different product: humans and agents on one Convex thread model with deliveries. mission-control-os is another name. This pack is the local operator strip: a p10k-inspired Go TUI named Mission Control under michaelmonetized.

If you want one `mc` binary, a Nerd Font, and a scrollable portfolio instead of five CLIs in five tabs, this is for you.

## What

I built **Mission Control**, public **michaelmonetized/mission-control**. README: a p10k-inspired TUI for managing all your projects. Phase 1 complete badge. 18 tests. Go.

Shipped local stack: **Go 1.25.4**, Charm **Bubble Tea** + Lipgloss, `cmd/mc` builds to `mc`, discovery + `~/.hustlemc/` cache, OpenClaw client foundation. Shell suite under `bin/`: discover, git/gh/vercel/swift status, stats, cache, dev, caddy, chat, deploy, and more, with `--json`.

![Shell suite mc-* with --json](/blog/mission-control-go-tui-p10k-portfolio-ops/screenshots/shell-suite-json.png)

Phase 2 scaffold: `apps/web` `@mission-control/web@2.0.0`. Next **16.1.0**, React 19, Clerk, Convex on port **3410**. Schema: users (GitHub + BYO Claude key + Stripe customer + free minutes), repos, workspaces (Fly VM lifecycle), usageRecords, threads/messages (**sender user|openclaw**), webhookEvents. `services/vm-manager` Go service for Fly Machines, terminal WebSocket relay, $0.02/min, idle kill.

![Phase 2 cloud. repos workspaces usage Fly VMs](/blog/mission-control-go-tui-p10k-portfolio-ops/screenshots/phase2-cloud-vm.png)

Not the HurleyUS human|agent deliveries plane. PLAN.md still mentions Ink/React. the entrypoint is Bubble Tea. March 21 "Phases 3–7 Complete" is mostly docs + scaffold burst. No dedicated public homepage on this repo; `vercel.json` only sets robots index,follow. HEAD **fd25166**. **22** commits.

![Three Mission Control names cut apart](/blog/mission-control-go-tui-p10k-portfolio-ops/screenshots/name-cut.png)

## Where

Code: [github.com/michaelmonetized/mission-control](https://github.com/michaelmonetized/mission-control) (**public**, **main**).

Contrast: [hurley-mission-control.vercel.app](https://hurley-mission-control.vercel.app) is the other product.

Install: `go build -o mc-tui ./cmd/mc`, then symlink `~/.local/bin/mc`. Config: `~/.hustlemc/`.

## When

**2026-02-16.** initial TUI, tests, Phase 2 plan, OpenClaw foundation.

**2026-02-21.** TUI redesign matching original spec (#2).

**2026-02-27–28.** CI gate; Vercel-only; drop GH Actions config.

**2026-03-20.** LOCATIONS.md; relay/webhook/daemon/E2E.

**2026-03-21.** Phases 3–7 claim + Phase 2 Convex/docs/go-live stack.

**2026-08-08.** HEAD fd25166 robots tag.

**2026-09-08.** draft pack; slug unused.

## Why

One keyboard surface for deploy + git + issues beats gossip across tabs. `--json` scripts keep the TUI accountable. Phase 2's bet is BYO Claude + metered VMs. Say the three Mission Control names so they stay separate.

If deploy state and git dirt only live in browser tabs, what are you actually controlling?

---

# glass-design-system: I shipped Apple SVG refraction, not the WebGL registry

Source: https://www.michaelchurley.com/blog/glass-design-system-apple-svg-refraction-showcase  
Published: 2026-08-08  
Author: Michael C. Hurley  
Tags: glass-design-system, apple-glass, svg, displacement, catppuccin, nextjs, tailwind, shadcn, martech, design-system, twelveux-sibling

![Glass Design System home, video hero and glass contact form](/blog/glass-design-system-apple-svg-refraction-showcase/screenshots/home.png)

## Who

I needed liquid glass on the web that bent the photograph behind it. A CSS blur was not enough. A WebGL sphere from a registry was the wrong shape for this demo.

That operator is me on a March afternoon with a BRIEF.md that names five effects and hard rules: real Catppuccin `dark:` classes, no `filter: invert()`, Tailwind v4 only, Next 16.

It is also anyone comparing two glass paths in my queue. twelveux ships pen.dev **glass.glsl** as a hosted shadcn item. This repo is the other path: SVG `feDisplacementMap`, animated conic borders, jelly nav, Apple-style sidebar. A full demo site over HustleLaunch photo and video plates.

Frontend builders who live in shadcn New York primitives but want the chrome to refract. Catppuccin people who refuse grayscale hacks. MarTech / indie product people who need cards, forms, dialogs, and a contact sidebar that still read when the backdrop is a real campaign still.

## What

I built **glass-design-system**: Next.js 16.2.6, React 19.2.6, Tailwind 4.3, shadcn New York, package `0.1.0` private. Bun lockfile. Live title: Glass Design System.

Five effects from the project brief, all in the tree:

1. **Apple Liquid Glass.** `GlassPanel` + `src/lib/displacement.ts`. SVG displacement map, chromatic aberration, strength/depth/radius props, `backdropFilter: url(...)`.
2. **AnimatedBorder.** `@property --conic-gradient-angle`, mask compositing, optional glow. Pink to Blue Catppuccin conic.
3. **Glass morphism.** Layered `color-mix` gradients + blur. Opacity got walked down hard so the displacement stays visible.
4. **GlassNav.** framer-motion jelly indicator that follows the active route.
5. **Catppuccin Mocha / Latte.** Real tokens in `globals.css`. Geist on the page (Max stays on twelveux / uncap / hms).

Glass barrel at `src/components/glass/`: panel, border, card, nav, button, dialog, sheet, sidebar (+ provider), form controls, `use-glass-surface`.

Routes:

- `/`: video hero, YouTube embed, glass contact form in AnimatedBorder, value copy, CTA.
- `/components`: ~3,215 lines. Commit message says 80+ example variations across commerce, auth, analytics, productivity, messaging, and states.
- `/about`: content page using the system.

Layout shell: sticky GlassNav + non-modal right **GlassSidebar** titled Quick Contact.

Backgrounds under `public/bg/`: hero-video.webm, hero-michael.jpg, campaign-monitoring.webp, web-designer.png, and the rest of the HustleLaunch stills. March 21 replaced flat gradient section shells with full-width photographic plates so the glass has something to bend.

Reference originals stay in-tree (`reference-apple-glass/`, `reference-animated-border.css`). Fallow marks them unused. That is honest: they are the port sources, not runtime.

Fallow REVIEW snapshot: ~10,194 LOC, dead files 11.1%, dead exports 24.9%, one circular dep. GlassSheet / GlassButton / GlassSidebar sit in the high-CRAP table. Live response sends `X-Robots-Tag: index, follow`.

![Components showcase, glass cards over photographic backdrop](/blog/glass-design-system-apple-svg-refraction-showcase/screenshots/components-loaded.png)

## Where

Live: https://glass-design-system.vercel.app

Repo: https://github.com/michaelmonetized/glass-design-system

Homepage field on GitHub points at that Vercel app. Adjacent systems in the same operator map: twelveux (WebGL registry Glass), modern-design-playground (WebGL instrument + nine worlds), uncap.us and hms (Max + Catppuccin product surfaces, different jobs).

## When

**2026-03-05, 1:41 PM ET.** Brief + reference files.

**2:12.** Core feat: apple glass refraction, animated borders, jelly nav, Catppuccin.

**2:30–3:14.** Photo/video sections, HustleLaunch assets, local webm instead of a dead WordPress URL, fixed parallax plates.

**3:23–3:53.** Opacity and contrast wars: glass-morphism thin enough for displacement, Tailwind utility backgrounds overridden, nav readable, gradient repeat tuned, button/dialog/sheet contrast fixed.

**4:01–4:31.** Apple-style non-modal glass sidebar, glass form components, hero wireframe iterations until video left + form right matched the layout.

Sixteen commits the same afternoon.

**2026-03-21, 7:25–9:35 AM ET.** Comprehensive showcase (80+), mobile 375px stacking, animated gradients then real `/public/bg/` assets, full-width absolute section shells.

**2026-06-22.** Two `nightly` commits.

**2026-08-08, 6:49 AM ET.** HEAD `0814e1f`, X-Robots-Tag index, follow. Same robots batch as several sibling Vercel repos that morning. **24** commits on main.

## Why

I already had Apple-glass and animated-border references sitting as ports. I wanted them inside Next 16 / Tailwind 4 / shadcn with Catppuccin that does not cheat.

Glass only proves itself against a photograph or a video plate. Gradients flatter. The March 21 backdrop swap is the reason the showcase exists at that density.

twelveux answers a different question: can I `npx` Max, theme, and a real WebGL Glass shader. This repo answers: can the whole chrome stack refract with SVG displacement and still ship a contact sidebar and an 80-variation gallery.

## Engagement

If you already run twelveux Glass, what breaks first when you try SVG displacement over a busy campaign still: chromatic fringe, text contrast, or the nav jelly fighting the sidebar?

![About page on Glass Design System](/blog/glass-design-system-apple-svg-refraction-showcase/screenshots/about.png)

---

# ascii-commit-graph: GitHub's heatmap in my terminal — then I made the cd hook fast

Source: https://www.michaelchurley.com/blog/ascii-commit-graph-terminal-heatmap-cd-hook-fastpath  
Published: 2026-07-31  
Author: Michael C. Hurley  
Tags: ascii-commit-graph, bash, cli, git, github, heatmap, contribution-graph, zoxide, terminal, gh, ripgrep, michaelmonetized

![Terminal heatmap](/blog/ascii-commit-graph-terminal-heatmap-cd-hook-fastpath/screenshots/terminal-heatmap.png)

## Who

I wanted GitHub's contribution calendar without leaving the shell, and on every `cd` via zoxide, not only when I opened a browser tab.

For operators who hang visual git context off directory changes, and who will delete an alias the moment it gets slow.

## What

I built **ascii-commit-graph**, public `https://github.com/michaelmonetized/ascii-commit-graph`. README **V1.0.4**. Script `ascii-commit-graph.sh`, 209 lines. HEAD `9221b76`. 13 commits. 1 star. CLI only.

Paints a GitHub-style week grid (Nerd Font glyph, ANSI greens). `GRID_ROWS=6`, `GRID_COLS=51` sliding weeks. Buckets 0/1/2/3+.

Flags: `--this-year`, `--full-width`, `--author`, `--show-issues`, `--show-todos`.

![Flags](/blog/ascii-commit-graph-terminal-heatmap-cd-hook-fastpath/screenshots/flags-panel.png)

v1.0.4 local path: one `git log` pass, O(1) bumps, GNU+BSD dates. `--author`: one `gh` GraphQL contributionCalendar call. Extras opt-in so default/cd path stays lean.

![Fast path](/blog/ascii-commit-graph-terminal-heatmap-cd-hook-fastpath/screenshots/fastpath-rewrite.png)

README zoxide `zcd` runs `--full-width --show-issues --show-todos` on every cd. That habit forced the Jul 2026 rewrite. ROADMAP still unchecked: Create a release for 1.0.4. Install snippet still references `michael-k/`.

## Where

Code: [github.com/michaelmonetized/ascii-commit-graph](https://github.com/michaelmonetized/ascii-commit-graph), public. No live web app. Clone + chmod + symlink.

![zoxide hook](/blog/ascii-commit-graph-terminal-heatmap-cd-hook-fastpath/screenshots/zoxide-cd-hook.png)

## When

**2024-06-06.** rc + docs + PNGs + PR #1.

**2024-06-08.** customization; v1.0.3-rc prep.

**2026-06-03.** compatibility.

**2026-07-31.** HEAD `9221b76`: Speed up heatmap: single git pass and one GraphQL author fetch.

![Commit arc](/blog/ascii-commit-graph-terminal-heatmap-cd-hook-fastpath/screenshots/commit-arc.png)

## Why

Browser greens are a context switch. A cd-hook heatmap makes latency a product bug. One git pass plus one GraphQL call is the honest fix, and an unchecked release checkbox beats a fake tag.

**Engagement Q:** If your cd alias paints a heatmap, what latency makes you delete the alias?

---

# niri-macos: I ported niri's scrollable tiling to macOS, then wrote an autopsy on my own Swift

Source: https://www.michaelchurley.com/blog/niri-macos-scrollable-tiling-swift-ax-port  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: niri-macos, niri, swift, macos, tiling, window-manager, accessibility, scrollable-tiling, paperwm, yabai, skhd, spm, ipc, michaelmonetized

![niri-macos scrollable column strip concept](/blog/niri-macos-scrollable-tiling-swift-ax-port/screenshots/scroll-strip-concept.png)

## Who

I wanted niri's scrollable tiling on the Mac without living inside Hammerspoon.

YaLTeR's [niri](https://github.com/YaLTeR/niri) is a Wayland compositor: windows live in columns on an **infinite horizontal strip**; you scroll the strip like a document; opening a window does not crush the ones you already have. PaperWM.spoon already brings that idea to macOS in Lua on Hammerspoon. I wanted the same paradigm as a **native Swift daemon** with Accessibility APIs, spring animations, and a yabai-shaped IPC CLI so skhd can drive it.

If you have ever rewritten a compositor concept as a weekend Accessibility prototype and then left an autopsy in the repo for future-you, this is that diary.

## What

I built **niri-macos**, SPM package `niri-macos`, version **0.1.0** (`niri-macos --version`), public under **michaelmonetized/niri-macos**. Platforms: **macOS 13+**. Zero external Swift packages. Products: library **NiriCore**, daemon **niri-macos**, CLI **niri-msg**.

Stack from the tree: AppKit + CoreGraphics + QuartzCore, `AXObserver` / `AXUIElement` for event-driven window tracking, `CGWindowList` enumeration, `CVDisplayLink` 60fps spring animation, `CGEventTap` gestures, Unix socket IPC at `/tmp/niri-macos.sock` with JSON commands. Config is JSON via `ConfigManager` at `~/.config/niri-macos/config.json` (gaps, outer gaps, preset widths, spring params, scroll thresholds, windowRules Codable). Hotkeys stay external: **skhd** bindings documented in README and `HOTKEYS.md`. Gestures: **Cmd+Shift+scroll** (focus window), **Cmd+scroll** (workspace), **3-finger swipe** (free scroll with momentum).

![Architecture: NiriCore, daemon, niri-msg, skhd](/blog/niri-macos-scrollable-tiling-swift-ax-port/screenshots/architecture-ipc.png)

Surfaces that exist: horizontal layout engine (`LayoutEngine.swift` ~44KB), consume/expel column stacking, center/maximize/preset widths (33/50/66/100%), dynamic workspaces (up/down/create above/below), split groups (horizontal/vertical/quad), multi-monitor isolation (active monitor follows mouse), menubar operations, `niri-msg status` / `list-windows` / `quit`. Tests: **112** `func test*` under `NiriCoreTests` (layout, types, IPC). CI: GitHub Actions on `macos-14` (build, test, release build). HEAD `d21a739`. **Four** commits. ~226KB of Swift.

README still marks Planned: focus ring overlay, window-rule **enforcement** (structs parse; sitrep says not applied), overview mode, sketchybar integration, Homebrew formula, launchd plist. PLAN.md still dreams of KDL like upstream niri; the shipped parser is JSON.

![AUTOPSY.md roast vs sitrep FUNCTIONAL](/blog/niri-macos-scrollable-tiling-swift-ax-port/screenshots/autopsy-vs-sitrep.png)

`AUTOPSY.md` (dated 2026-02-09) is the scar: it calls the early tree a 3,672-line prototype with **two** commits, **zero** tests, seven singletons, hardcoded gaps, and a README that was aspirational fiction. It also credits real spring physics, a thoughtful IPC command set, and clean `ColumnWidth` modeling. `sitrep.md` at the same era (updated for the refactor) says **FUNCTIONAL**: 112 passing tests, JSON config, protocol-based DI, main-thread layout serialization. The June 22 `nightly` commits are where that contradiction resolves in git history.

## Where

Code: [github.com/michaelmonetized/niri-macos](https://github.com/michaelmonetized/niri-macos), **public**. No hosted demo. Run locally: `swift build -c release`, put `niri-macos` on your PATH, grant **Accessibility**, start the daemon, drive it with `niri-msg` / skhd. Socket default `/tmp/niri-macos.sock`. Log default `/tmp/niri-macos.log`.

## When

- **2026-02-06.** `640d314` feat: implement niri scrolling layout paradigm for macOS (initial README/PLAN + core sources).
- **2026-02-08.** `37f34ad` fix that is really a sequel: multi-monitor isolation, discrete scroll, workspace creation, split groups, animation/gestures/AX observer (+2391/−132). AUTOPSY calls the message an undersell.
- **2026-02-09.** AUTOPSY.md examination date; sitrep claims FUNCTIONAL + 112 tests (landed in tree with the later nightly push).
- **2026-06-22.** GitHub repo `created_at`; two `nightly` commits (`f3b1d77`, HEAD `d21a739`) ship NiriCore extraction, ConfigManager, full test suite, workflow, AUTOPSY in-tree, hustlemc/uncap crumbs. Last push `2026-06-22T22:20:39Z`.

![Four-commit arc](/blog/niri-macos-scrollable-tiling-swift-ax-port/screenshots/commit-arc.png)

## Why

I use macOS and I still want niri's rule: **new windows append; existing frames stay**; scroll the strip; isolate per monitor. Hammerspoon is fine. I wanted direct APIs, spring physics I own, and `niri-msg` that feels like talking to yabai while the layout model is niri's.

I also wanted the honesty layer. Shipping AUTOPSY.md next to a polished README is the point: document the Jenga tower, then answer it with tests and a library boundary instead of deleting the roast.

## Engagement Q

Would you rather run scrollable tiling as **native Swift + Accessibility + skhd**, or stay on **PaperWM.spoon** and keep the Lua runtime, and what would make you trust a 0.1.0 WM with four commits and 112 tests?

---

# iTour.golf: I rewrote the national creator-tour lander for May 2027 — production still serves the 2026 fake season

Source: https://www.michaelchurley.com/blog/itour-golf-tour-lander-ahead-of-deploy  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: itour, itour-golf, golf, golf-tour, creator-tour, sponsors, ileague, iconference, convex, nextjs, clerk, vercel, deploy-drift, hurleyus

![iTour.golf HEAD homepage mock. May 12 2027 inaugural, Join iLeague to Qualify](/blog/itour-golf-tour-lander-ahead-of-deploy/screenshots/home.png)

## Who

I build ecosystem products that have to stay distinct under one brand family. Golf creators already get a scorecard-and-tips platform on [iLeague.golf](https://ileague.golf). That pack is a different story. Patreon meets 18Birdies, Stripe tiers, fifteen percent fee.

iTour.golf is for the next layer: creators who need a **season** to aim at, courses that want to **host** a stop, and sponsors who buy **tour inventory** instead of a creator subscription.

If you care about monorepo landers that outrun their Vercel deploy, Convex schemas that model tournaments before any admin UI exists, or how not to collapse three golf domains into one blog post: this is the field notes.

## What

I shipped a national golf **creator tour** under `HurleyUS/itour.golf`.

README one-liner: iPro.golf's national influencer golf tour. Brain note (`iLeague Golf.md`): **36-week** tour; **top 54** qualify for iConference. HEAD lander badge: **The National Golf Influencer Tour**. H1: **iTour.golf**. Line: **Where Golf Creators Become Champions**.

Stack on the box: **Next.js 15.5.6**, **React 19**, Convex, Clerk (optional when the publishable key is missing), PostHog, Sentry, Resend, Tailwind v4, Bun workspaces (`web` + `mobile`), Inter + Oswald, Vercel. Package **itour-monorepo** **1.0.0**. Repo private. Site public at [www.itour.golf](https://www.itour.golf) (apex 307s to www).

![Championship path mock. iLeague to iTour to iConference](/blog/itour-golf-tour-lander-ahead-of-deploy/screenshots/championship-path.png)

This is separate from the creator billing product. There is **no Stripe dependency** in `web/package.json`. README monetization is blunt:

1. Sponsors, ads, vendors, partners, investors  
2. Courses bid to host stops on the iTour  

HEAD page CTAs: **Join iLeague to Qualify** (outbound to ileague.golf) and **Become a Sponsor** / **Host a Tour Stop** (on-page sections with buttons, not checkout).

Convex `web/convex/schema.ts` is tour-shaped: `users` (creator|fan|admin), `courses` (optional week), `tournaments` (weeks 1–36), `entries` (score + videoUrl), `standings`. No `subscriptionTiers`. No `tips`. No content feed. Those tables live on the iLeague sibling.

What is also true on pack day:

- **Live www** still markets **"2026 Season Now Open"**, **18 Stops / 18 Courses / 1 Champion**, a fake **Desert Classic** at Pebble Beach, a fake leaderboard, and a **$3.5M+** prize-pool story aimed at a Sept 9 **2026** Augusta finale.  
- **Repo HEAD** `page.tsx` is the amber rewrite: **36-week** season, **May 12, 2027** inaugural, **Top 54 to iConference**, qualify-via-iLeague funnel.  
- `layout.tsx` SEO says **36 weeks, 36 courses**; the hero says **18 courses of their choice**. PLAN/OPPORTUNITIES still say top **18**. Brain + footer say top **54**.  
- CHANGELOG 1.0.0 still claims "golf course discovery and booking." sitrep.md still says **PROTOTYPE** with last commit stamped **2026-01-31**.  
- Mobile is an Expo stub that renders the words **Mobile App**.  

That gap is the product story.

![Live vs HEAD deploy drift mock](/blog/itour-golf-tour-lander-ahead-of-deploy/screenshots/live-vs-head.png)

## Where

It is supposed to run on Vercel against Convex with Clerk when keys exist. Providers deliberately render without Clerk if `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` is unset (May 14 fix). Convex client only instantiates when `NEXT_PUBLIC_CONVEX_URL` is set.

Surfaces that matter on HEAD:

- Public amber lander. hero, how-to-qualify (4 steps), championship path, 2027 coming-soon chips, host-a-stop, sponsors, ecosystem footer  
- `robots.ts` / `sitemap.ts` / JSON-LD SportsOrganization + SportsEvent (2027-05-12 to 2027-09-09)  
- Middleware protect list for dashboard/account/settings/api. public marketing routes stay public  
- Mobile folder. scaffold only  

Live HTML on 2026-09-08 did **not** show Clerk `pk_test` / `pk_live` strings. The stale marketing shell is prerendered on Vercel (`x-nextjs-prerender: 1`).

![Sponsors + course hosting mock](/blog/itour-golf-tour-lander-ahead-of-deploy/screenshots/sponsors-hosting.png)

Related domains in the footer/copy: **iLeague.golf** (emerald. creator platform), **iTour.golf** (amber. this pack), **iConference.golf** (purple. championship). Do not collapse this post into the iLeague creator-economy pack.

## When

**January 8, 2026.** Initial Next.js + Convex + Tailwind setup. OPPORTUNITIES and homepage docs the same day. This tree starts as a tour concept, not a WNC directory.

**January 31.** Chore sync. sitrep still thinks this is the last meaningful stamp. it is wrong.

**February 11.** GitHub `HurleyUS/itour.golf` exists. Homepage gets ecosystem context. iTour/iCon details corrected from the OG vision. Placeholder season data removed. **TBD until May 12, 2027**.

**February 15.** Explicit commit: **replace bestwnc.com boilerplate with iTour content**. Lineage matters so this pack does not retell BestWNC.

**February 21.** Fonts, favicon, Tailwind v4 CSS-first, React 19 types, providers/middleware/schema/layout batch, CHANGELOG labeled 1.0.0.

**Late February.** CI build gate, security headers + regression script, SEO/analytics baseline, then GitHub Actions removed because Vercel is CI.

**March 28.** Workspace scripts stop using bun filters. `cd web && bun run …`. A Gumroad `/pricing` page also appears in history (Golfer Basic/Pro pre-order links), not the live shell's center of gravity.

**May 14–15.** Blacksmith CI standardize noise, then delete it. **Skip Clerk provider when key is unavailable**. lander must not hard-crash without auth config.

**June 22.** Two `nightly` commits. HEAD **`d2a49ef`**. Last push on the repo.

**September 8, 2026 (pack day).** www returns 200. Content is still the old 18-Stops / fake-season shell. Draft pack only. no publish, no push.

![Convex tour schema mock. users courses tournaments entries standings](/blog/itour-golf-tour-lander-ahead-of-deploy/screenshots/tour-schema.png)

## Why

Because the HurleyUS golf stack needs **three honest products**, not one blog post with three domains.

iLeague is where creators publish, subscribe, and tip. iTour is where a **season** and **sponsor/host** economy are supposed to live. iConference is the September championship punchline. If you describe iTour as "another creator SaaS," you erase the only reason the domain exists.

Because **schema-before-UI** is an operator move: tournaments 1–36, entries with video URLs, season standings. committed while the live site still invents Jake Matthews and a Desert Classic.

Because **deploy drift** is a better lesson than a launch party. Rewriting `page.tsx` for May 2027 does nothing for golf fans if Vercel keeps serving the 2026 placeholder season. Same pattern as shipping robots `index,follow` while leaving test keys on a sibling domain. different failure mode, same honesty requirement.

Because the money model is different on purpose. No platform fee constant. No tip presets. Sponsor tiers and course hosting bids are the README, even if the buttons still go nowhere.

## Engagement

If you run a marketing lander in a monorepo: what do you trust on pack day: `git show HEAD:app/page.tsx`, or `curl` the production HTML, and which one did you ship last?

---

# Kitchen: I built a cloud project store where files are rows and disk is a Mirror — no git

Source: https://www.michaelchurley.com/blog/kitchen-cloud-native-project-store  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: kitchen, cloud-native, sync, convex, clerk, nextjs, mirror, versioning, no-git, developer-tools, pierre, vercel

![Kitchen home: cloud-native project store](/blog/kitchen-cloud-native-project-store/screenshots/home.png)

## Who

I got tired of treating Editor, disk, git, and remote hosting as four different systems for the same daily loop.

Kitchen is for operators who want live sync and a real editor (nvim, VS Code, Zed) without renaming the product a cloud IDE. It is for people who will say "no git" and mean no add/commit/push/pull/rebase, while history and human merge stay.

If you have ever saved a file and still had a ceremony left before another machine could see it, this is for you.

## What

I built Kitchen (michaelmonetized/kitchen, web 0.1.0). Codename. Next 16.2.9 + Convex + Clerk + Mirror. HEAD `97bec56`. 34 commits.

![Pricing](/blog/kitchen-cloud-native-project-store/screenshots/pricing.png)

## Where

Live https://kitchen-gilt-nine.vercel.app. Home/pricing/docs/sign-in return 200; discover returns 404; vision/mission return 404 (untracked working tree). Web is tree/diff/blame in the browser; local editors stay native.

![Docs](/blog/kitchen-cloud-native-project-store/screenshots/docs.png)

## When

June 18 2026: public log opens at fork-merge; same day org-admin, collab-relay, landing, Vercel ship, recovery loops, then Mirror client, launch-gate, diff/blame, agent kit, offline queue. June 21 lakebed parity. June 22 nightly HEAD `97bec56`. Pack day 2026-09-08 draft-only; working tree dirty, not pushed.

![Sign-in](/blog/kitchen-cloud-native-project-store/screenshots/sign-in.png)

## Why

Honest codename. Launch gate blocks PH/HN until Mirror demo is true. Dual honesty: marketing routes return 200 while discover returns 404 and docs drift in the working tree. "No git" means no ceremony verbs; Versions and Pierre merge stay.

![Four layers](/blog/kitchen-cloud-native-project-store/screenshots/four-layers.png)

**Engagement Q:** If your editor already saves to disk, what would have to be true before you deleted `git add`: live Versions on another machine, or a merge UI you trust more than conflict markers?

---

# Codefolio: I shipped a GitHub-sync portfolio SaaS with 4k lines of specs — and claimed a domain that isn't mine

Source: https://www.michaelchurley.com/blog/codefolio-spec-first-github-portfolio-saas  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: codefolio, developer-portfolio, github-sync, nextjs, convex, clerk, stripe, saas, proxy-ts, resume, analytics, michaelmonetized, spec-first

![Codefolio marketing hero: Public Beta + Get started free](/blog/codefolio-spec-first-github-portfolio-saas/screenshots/marketing-hero.png)

## Who

I wanted a **developer portfolio platform** (multi-tenant, GitHub sync, pin six projects on free, custom domain on Pro, analytics that tell you whether recruiters came from Twitter or a blog referral) instead of another personal homepage.

Who this is for: operators who will read `DESIGN.md` and `CONTRACTS.md` before they trust a launch tweet, and people who have watched a SaaS invent 10K+ developers before the Vercel project exists.

If you have ever claimed a `.dev` domain in `openGraph.url` and then discovered the hostname already belongs to someone else's portfolio, this brief is for you.

## What

I built **Codefolio**, package `codefolio` **0.1.0**, **private** under **michaelmonetized/codefolio**. Layout title: **Codefolio - Developer Portfolio Platform**. Description: Showcase your code. Build your reputation. The modern portfolio platform for developers.

Stack from package.json: **Next.js 16.2.6**, **React 19.2.6**, Tailwind **4.3**, Bun, **Clerk** (`@clerk/nextjs` ^7.3.3), **Convex** ^1.38.0, `stripe` + `@stripe/stripe-js`, `resend`, `posthog-js`, `@sentry/nextjs`, radix-ui, next-themes (dark default). Three commits. HEAD `c499a72`.

![Free / Pro $9 / Team $29 pricing grid](/blog/codefolio-spec-first-github-portfolio-saas/screenshots/pricing-tiers.png)

Surfaces that exist in the tree:

- Marketing: `/`, `/features`, `/pricing`, `/about`, `/blog`, `/careers`, `/examples`, `/privacy`, `/terms`, `/login`, `/signup`
- Public portfolio: `/:username`, `/:username/:project`, `/:username/resume`
- Dashboard (Clerk-protected via `proxy.ts`): `/dashboard`, `/dashboard/projects`, `/dashboard/projects/[id]`, `/dashboard/analytics`, `/dashboard/settings`

Convex schema is five tables: `profiles`, `projects`, `analytics`, `subscriptions`, `githubSyncs`. GitHub sync action pulls `api.github.com/users/{username}/repos?per_page=100&sort=updated&type=owner` and upserts. Plan limits in `lib/types.ts`: Free 6 pins / 7-day analytics; Pro unlimited + custom domain + 90 days + case studies + remove branding; Team 5 members + 365-day analytics.

Pricing cards match: **Free**, **Pro $9/mo**, **Team $29/mo**.

What does **not** exist: `app/api/**` (no Stripe webhook route handlers), a `/demo` page (Hero still links there), a live deploy on the probed Vercel hostnames, or ownership of **codefolio.dev**.

Docs are not an afterthought. `DESIGN.md` (~1083 lines), `CONTRACTS.md` (~466), `COMPLIANCE.md` (~1171 WCAG 2.1 AA), `TECH-REQ.md` (~429), plus Fallow `REVIEW.md` (~9.2k LOC, 6 unused deps including stripe/resend/posthog, dashboard pages marked critical complexity).

Hero badge: **Now in Public Beta**. Social proof strip: **10K+ Developers / 50K+ Projects Showcased / 1M+ Portfolio Views**. Those numbers are marketing copy with no telemetry backing in the private tree.

## Where

Code: [github.com/michaelmonetized/codefolio](https://github.com/michaelmonetized/codefolio), **private**. Live app URL: **none** at pack time (`codefolio.vercel.app` returns 404).

![Five Convex tables vs Stripe/Resend unused deps](/blog/codefolio-spec-first-github-portfolio-saas/screenshots/schema-five-tables.png)

Layout `openGraph.url` and feature copy talk about **codefolio.dev**. Probe on 2026-09-08: `https://www.codefolio.dev` returns **HTTP 200** for Abdel Ahzab, Full-Stack Engineer shipping Applied AI: unrelated personal site on Cloudflare/Vercel. That is a **name collision**, not my deploy.

![codefolio.dev claimed in OG vs live third-party portfolio](/blog/codefolio-spec-first-github-portfolio-saas/screenshots/domain-collision.png)

Audience sits next to every portfolio SaaS that ships specs + dashboard chrome before billing webhooks and a domain you actually control.

## When

**2026-02-07.** `531f712` Create Next App. Bootstrap wrap-up in `.work/` claims Convex project `codefolio`, schema complete (5 tables / 16 indexes), CONTRACTS.md, shadcn button/card/input, build passes.

**2026-06-22 ~5:44 PM ET.** `01aa6cc` **nightly**. The product and the essay-length docs land together: marketing, dashboard, public portfolio/resume, Fallow gate hooks, DESIGN/COMPLIANCE/TECH-REQ/REVIEW.

**2026-06-22 ~6:19 PM ET.** `c499a72` **nightly** HEAD.

GitHub `created_at` / `pushed_at` both sit on **2026-06-22** even though the first commit is February (private repo timing vs local history).

![Three-commit arc Feb to June nightlies](/blog/codefolio-spec-first-github-portfolio-saas/screenshots/commit-arc.png)

## Why

A portfolio platform is a different product from a personal site, and I wanted the contracts written before the launch thread. Clerk + Convex + a real `/:username` surface is useful even when Stripe checkout is still schema fiction. Claiming `codefolio.dev` in metadata while the hostname serves another engineer is the kind of fact you put in the brief before you buy ads. Three commits can still carry nine thousand lines, and Fallow will still tell you stripe and resend never got imported.

**Engagement Q:** When your OG URL names a `.dev` you do not control and your Hero invents 10K users, do you fix the domain story first, or the fake social proof?

---

# HustlePay: guest claim tokens before the framework adapters stopped being TODOs

Source: https://www.michaelchurley.com/blog/hustlepay-guest-claim-tokens-auth-agnostic-stripe  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: hustlepay, stripe, convex, guest-checkout, claim-tokens, auth-agnostic, payments, turbo, bun, michaelmonetized, theo-stripe-recommendations

![Guest claim flow](/blog/hustlepay-guest-claim-tokens-auth-agnostic-stripe/screenshots/guest-claim-flow.png)

## Who

I needed auth-agnostic Stripe+Convex glue so a guest can buy first and claim after signup — without marrying Clerk into the schema.

## What

Private **michaelmonetized/hustlepay** · hustlepay@0.0.0 · @hustlepay/core@0.0.1 · HEAD `d2877fb` · 1 commit nightly (+6957). Seven hp_* tables including hp_guest_sessions (gs_ + claim_ tokens). UI in core: Pay, Cart, Checkout, Has, ClaimAccount. @hustlepay/react still exports VERSION only; other adapters are stubs. Theo stripe-recommendations cited in stripe.ts.

![Schema tables](/blog/hustlepay-guest-claim-tokens-auth-agnostic-stripe/screenshots/schema-tables.png)

![Pay + ClaimAccount in core](/blog/hustlepay-guest-claim-tokens-auth-agnostic-stripe/screenshots/pay-claim-components.png)

## Where

github.com/michaelmonetized/hustlepay (private). No demo host for this tree. hustlepay.com → /lander. hustlepay.vercel.app = unrelated Nigeria micro-pension app. Sibling stripe-convex = email-tracking lineage; publish-stack names hustlepay for guest claim / Pay(199).

![Core vs adapters](/blog/hustlepay-guest-claim-tokens-auth-agnostic-stripe/screenshots/core-vs-adapters.png)

## When

2026-06-22 repo created; sole commit d2877fb nightly same day. No follow-ups.

![Domain irony](/blog/hustlepay-guest-claim-tokens-auth-agnostic-stripe/screenshots/domain-deploy-irony.png)

## Why

Guest rows without a claim bridge orphan purchases at signup. Auth-agnostic means userId is your Convex auth string. Core shipped the spine; the adapter billboard did not.

**Engagement Q:** When @hustlepay/react only exports VERSION, do you import Pay from @core — or wait for the TODOs?

---

# bundx-init: every Next.js repo gets https://.localhost via Caddy

Source: https://www.michaelchurley.com/blog/bundx-init-nextjs-unique-localhost-https-caddy  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: bundx-init, nextjs, caddy, localhost, https, clerk, allowedDevOrigins, bash, cli, dev-tooling, local-dev, michaelmonetized

![Install flow](/blog/bundx-init-nextjs-unique-localhost-https-caddy/screenshots/install-flow.png)

## Who

I run a lot of Next.js apps side by side. Shared `http://localhost:3000` fights Clerk cookies, callback URLs, and `allowedDevOrigins` the second a second app boots.

For operators who need **stable HTTPS origins per repo** on a laptop without hand-writing a Caddyfile every time.

## What

I built **bundx-init**, public `https://github.com/michaelmonetized/bundx-init`. Shell CLI. HEAD `c8b59ad`. **3** commits. 0 stars. No tagged release.

`bin/bundx-init` is **379** lines. `install.sh` curls it into `~/.local/bin` (`BUNDX_INIT_RAW_URL` override). Target must be a Next project (`package.json` with `next`).

What it does (README + script):

- installs Caddy when possible (brew / apt Cloudsmith / dnf COPR / pacman)
- configures `~/.local/etc/Caddyfile` to import `~/.local/etc/caddy/dev-sites/*.caddy`
- writes a repo-scoped Caddy snippet
- adds `scripts/dev-localhost.mjs` + `dev-localhost-info.mjs`
- rewires `package.json` so `dev` runs the HTTPS flow (`dev:raw` keeps the old script; `dev:info` dumps JSON)
- patches `next.config.*` with `allowedDevOrigins: ["", "*.localhost"]` when it can

Slug = basename lowercased. Host = `.localhost`. Port = `3300 + (hash(slug) % 5000)`. Fixture `basic-next` maps to port **6422**; README `my-next-app` maps to **6996**.

![Hostname / port map](/blog/bundx-init-nextjs-unique-localhost-https-caddy/screenshots/hostname-port-map.png)

Next still binds an internal high port. Caddy owns `:443` and reverse-proxies. Env: `DEV_HOST`, `DEV_URL`, `PORT`.

![Architecture](/blog/bundx-init-nextjs-unique-localhost-https-caddy/screenshots/architecture.png)

Fixture after init shows the patch contract: Next **16.2.1** / React **19.2.0**, `dev` runs the localhost script, `allowedDevOrigins` for `basic-next.localhost` + `*.localhost`.

![Repo patches](/blog/bundx-init-nextjs-unique-localhost-https-caddy/screenshots/repo-patches.png)

## Where

Code: [github.com/michaelmonetized/bundx-init](https://github.com/michaelmonetized/bundx-init), public. No live web app.

```bash
curl -fsSL https://raw.githubusercontent.com/michaelmonetized/bundx-init/main/install.sh | bash
bundx-init ~/Projects/my-next-app
cd ~/Projects/my-next-app && bun install && bun run dev
# https://my-next-app.localhost
```

## When

**2026-03-30.** `3d4b752` Initialize bundx-init (589 insertions: CLI, install, README, fixture).

**2026-06-22.** `f356360` nightly adds `.uncap/config.json`.

**2026-06-22.** `c8b59ad` nightly empty tip (HEAD).

![Commit arc](/blog/bundx-init-nextjs-unique-localhost-https-caddy/screenshots/commit-arc.png)

## Why

Parallel Next apps on one port break auth providers. Clerk and friends want real HTTPS origins in local. A hashed port, a Caddy snippet, and `allowedDevOrigins` is the boring fix; shipping it as a curl-install CLI beats copy-pasting the same five files forever.

**Engagement Q:** How many Next apps do you run locally before `localhost:3000` starts lying to your auth provider?

---

# bashformer: terminal Flappy Bird in Ink — after I deleted the C/SDL pile

Source: https://www.michaelchurley.com/blog/bashformer-ink-flappy-after-c-sdl-cleanup  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: bashformer, flappy-bird, ink, react, bun, typescript, terminal-game, bash, platformer, nerd-font, cli, michaelmonetized

![Ink Flappy frame](/blog/bashformer-ink-flappy-after-c-sdl-cleanup/screenshots/ink-flappy-frame.png)

## Who

I wanted Flappy Bird that lives in the TTY, and I kept a pure-bash platformer under the same repo name because the first instinct was bash + former, then an Ink clone later.

For people who install Bun and still respect a 300-line bash game that only needs Nerd Fonts.

## What

I built **bashformer**, public https://github.com/michaelmonetized/bashformer. HEAD `541d2bc`. **21** commits. **0** stars. Default **master**. Version **Unreleased**. CLI/TTY only.

**Product A (README):** `index.tsx` (~263 LOC). Bun + React 19 + Ink 7. CONFIG: FPS 30, GRAVITY 0.32, FLAP_VY -1.7, PIPE_SPEED 3.1, PIPE_GAP 8. Space flaps/restarts; Q quits. Pipe.scored prevents double-count (`d1fa8d0`, #7). Terminal <40x10 exits (`6678fda`).

![CONFIG + scored](/blog/bashformer-ink-flappy-after-c-sdl-cleanup/screenshots/config-scored-fix.png)

**Product B (in-tree):** `bashformer.sh` (~314 LOC). Pure bash Nerd Font side-scroller with coins/spikes/goal and camera follow. A/D move, W/Space jump, Q quit.

![Bash platformer HUD](/blog/bashformer-ink-flappy-after-c-sdl-cleanup/screenshots/bash-platformer-hud.png)

**Feb 21 cleanup:** CONFIG extract (`6b9eead`), PLAN rewrite (`e27c79b`), bun tests (`810e5f4`), remove vex_sdl (#14) and C/SDL (`5a840cf`), CHANGELOG (#11), README match, scored flag, term size.

Residue: PLAN still lists deleted C experiments; sitrep says Unknown tool / PROTOTYPE; CHANGELOG [Unreleased]; package.json private.

![Cleanup arc](/blog/bashformer-ink-flappy-after-c-sdl-cleanup/screenshots/cleanup-arc.png)

## Where

Code: [github.com/michaelmonetized/bashformer](https://github.com/michaelmonetized/bashformer), public. No live web app.

```bash
bun install && bun run index.tsx
# or
./bashformer.sh
```

![Dual stack](/blog/bashformer-ink-flappy-after-c-sdl-cleanup/screenshots/dual-stack.png)

## When

**2025-12-21 to 12-27.** init, kong/baddies/tools, zoom/png, story cleanup.

**2026-01-31.** chore sync.

**2026-02-21.** CONFIG to tests to delete C/SDL to scored flag to term size.

**2026-06-22.** nightly x2 to HEAD `541d2bc`.

![Commit arc](/blog/bashformer-ink-flappy-after-c-sdl-cleanup/screenshots/commit-arc.png)

## Why

Deleting the C/SDL pile was a product decision. Pipe.scored is more honest than "score feels off." Unreleased with empty Phase 1 checkboxes beats inventing a 1.0 tag.

**Engagement Q:** Keep the pure-bash platformer beside Ink Flappy, or split so the README stops lying by omission?

---

# compare: back-to-back time lies — so I git-isolated the benchmark

Source: https://www.michaelchurley.com/blog/compare-git-isolated-command-benchmarker  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: compare, bash, cli, benchmark, posix-time, git-isolation, biome, oxlint, mdr, mermaid, devtools, michaelmonetized

![Git-isolated A/B workflow](/blog/compare-git-isolated-command-benchmarker/screenshots/workflow-isolation.png)

## Who

I kept running `time cmd-a; time cmd-b` and then arguing with myself about whether the second one won because it was faster, or because the first one warmed the cache and the tree had drifted.

For operators comparing formatters and linters who refuse that lie. Especially anyone already typing `vp exec biome format --write` next to `vp exec oxlint` and wanting the median, not the vibes.

## What

I built **compare**, public [michaelmonetized/compare](https://github.com/michaelmonetized/compare). Bash. `VERSION="0.1.0"`. `bin/compare` is **825** lines at HEAD (468 on init). `install.sh` drops it in `/usr/local/bin` (or `$PREFIX`). HEAD `c5dbcd6`. **3** commits. 0 stars. CLI only. No LICENSE. No release tag.

![CLI help surface](/blog/compare-git-isolated-command-benchmarker/screenshots/cli-help.png)

The product is git isolation for shell A/B:

1. If the working tree is dirty, commit `compare: snapshot before benchmark` (with `--no-verify`).
2. Force-create `compare/` and `compare/` from that shared SHA.
3. Checkout A, run command A `-n` times with `TIMEFORMAT`, append to the log; blank line; same for B.
4. Restore the original branch. Leave the snapshot and `compare/*` branches for inspection; delete them yourself.

Log lines look like POSIX `time`:

```text
vp exec biome lint --write  1.36s user 0.36s system 151% cpu 1.134 total
vp exec oxlint --write  0.21s user 0.18s system 73% cpu 0.528 total
```

Default path: `../tests/compare----.log`. Flags: `-n`, `-c`, `-o`, `-g/--graph`, `--md`. Subcommands: `compare graph `, `compare report `. Graphs: user / system / CPU% / total (blue A, yellow B). `--md` writes tables + mermaid and opens [mdr](https://github.com/CleverCloud/mdr) (else `open` / `xdg-open`). Commands go through `eval` (trusted only).

![Sample log + terminal charts](/blog/compare-git-isolated-command-benchmarker/screenshots/sample-log-graph.png)

Smoke on the pack box: `compare "sleep 0.05" "sleep 0.12" -n 3` totals ~0.051 vs ~0.121, branches `compare/sleep-0-05` and `compare/sleep-0-12` left behind. README caveats remain honest: no CPU pinning, mutating commands can diverge branches, one intentional variable is on you.

Residue: `plans/README.md` still says the repo had **no commits and no source code** (docs-only improve plans). Those four plans are marked DONE. `compare report` is in `--help` and code; it is not in the README Contents. Second nightly (`c5dbcd6`) shares the exact tree with the first nightly (empty HEAD commit).

![plans irony](/blog/compare-git-isolated-command-benchmarker/screenshots/plans-irony.png)

## Where

Code: [github.com/michaelmonetized/compare](https://github.com/michaelmonetized/compare), public, branch `main`. No homepage. No live web demo. Clone + `./install.sh` (or `PREFIX=$HOME/.local ./install.sh`).

Audience sits next to biome, oxlint, Vite Plus, and anyone who already treats `time` output as courtroom evidence.

## When

**2026-06-17, 6:57 AM Eastern.** `217d4ac` init: README, 468-line CLI, install script, four docs plans.

**2026-06-22, 5:40 PM Eastern.** `b4fea7b` nightly: CLI grows to 825 lines; README gains `--md` / mdr / CPU chart.

**2026-06-22, 6:17 PM Eastern.** `c5dbcd6` nightly: same tree as `b4fea7b`. HEAD. Empty.

![Commit arc](/blog/compare-git-isolated-command-benchmarker/screenshots/commit-arc.png)

## Why

Back-to-back `time` is a shared-state measurement pretending to be a tool measurement. A snapshot commit plus two named branches is the smallest honest isolator I would actually run. biome-vs-oxlint needs a log file outside the working tree, not another Slack debate.

**Engagement Q:** When you A/B two CLIs, what is the one variable you pretend you controlled, and which dirty-tree / warm-cache factor actually won?

---

# nvibe: I wired Cursor Agent + CodeRabbit into Neovim, then fought the window manager until "it works!"

Source: https://www.michaelchurley.com/blog/nvibe-neovim-cursor-coderabbit-layout-until-it-works  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: nvibe, neovim, nvim, lua, nvchad, cursor-agent, coderabbit, lazygit, vibe-coding, nvim-tree, terminal, ai-coding, michaelmonetized

![nvibe layout: left AI panel, editor, bottom tools](/blog/nvibe-neovim-cursor-coderabbit-layout-until-it-works/screenshots/layout-concept.png)

## Who

I wanted Cursor Agent and CodeRabbit **already open** when Neovim started: same frame as the buffer, no alt-tab to an Electron window, no extra tmux pane, no `` chord that makes me lose which split I was in.

If you run NvChad and keep AI CLIs on PATH, you already know the friction: leave for the agent, lose buffer context, come back, rebalance windows by hand. I wanted the layout to be the product.

## What

I built **nvibe**, a Lua Neovim plugin at [michaelmonetized/nvibe](https://github.com/michaelmonetized/nvibe). Version badge says **0.1.0**. Module lives at `lua/nvibe/init.lua` (~354 lines). Public. Stars: 0.

On `VimEnter`, if you are not already in a terminal buffer, `setup()` calls `create_terminal_split()`:

1. Hard-requires `nvchad.term` (ERROR notify + abort if missing).
2. Checks `vim.fn.executable` for `cursor_agent_cmd` / `coderabbit_cmd` (defaults `cursor-agent`, `coderabbit`).
3. Left panel width = `width_percent` x `$COLS` (or `vim.o.columns` fallback).
4. `nvchad.term.new` for Cursor Agent (top) and CodeRabbit (bottom) on that panel.
5. Bottom strip: LazyGit + two shells (`vim.o.shell`), with a pile of `wincmd` / `close` cleanup so empty buffers do not stick around.
6. `stopinsert` so the editor is not left in insert mode.

Config surface is small:

```lua
require('nvibe').setup({
  width_percent = 30,
  cursor_agent_cmd = "cursor-agent",
  coderabbit_cmd = "coderabbit",
})
```

Honesty check against the tree: the **code default** for `width_percent` is **20**, while README / `docs/API.md` / busted expectations still talk like **30**. LazyGit and the dual shells are **hardcoded**, not setup opts. ROADMAP still has "make bottom panel commands and sizes configurable" open.

![NvimTree #4/#5: height constrain + rebalance](/blog/nvibe-neovim-cursor-coderabbit-layout-until-it-works/screenshots/nvimtree-fix.png)

February fix (`6a931f6`, closes #4 and #5): opening NvimTree used to equalize windows and grow full height over the terminal panes. Plugin now caches editor-row height after layout, constrains `FileType NvimTree` windows to that height, and `rebalance_panels()` restores left-column terminal widths on open/close. README documents `preserve_window_proportions = true` as the paired NvimTree setting.

Tests: busted (`tests/test_nvibe.lua`, **18** `it(` cases) with mocked `vim` + `nvchad.term`. Makefile: `make test` / `lint` / `check` via busted + luacheck. CHANGELOG documents the early interactivity bug: raw `vim.cmd("terminal …")` vs NvChad's interactive `term.new`.

![CI added, then deleted for Vercel](/blog/nvibe-neovim-cursor-coderabbit-layout-until-it-works/screenshots/ci-vercel-irony.png)

CI subplot: `3ee0092` added `.github/workflows/build.yml` ("keep prod build green"). `5ab79db` corrected it for a Lua project. Next morning `365a17e` deleted **all** GitHub config with message **"Vercel is our only CI/CD"** on a Neovim Lua plugin with no web app in the tree. HEAD has no `.github/`. Local `make test` is the gate that remains.

Also true: MIT badge in README, **no LICENSE file**. Product Hunt badges link to producthunt.com root. PLAN.md still has Phase 3 "Product Hunt launch preparation." CHANGELOG dates 0.1.0 as 2025-01-17; git history starts **2025-10-17**.

## Where

Code: [github.com/michaelmonetized/nvibe](https://github.com/michaelmonetized/nvibe), **public**. No hosted demo.

Install path in README: Lazy.nvim or Packer snippets calling `require('nvibe').setup()`, after NvChad. Runtime needs Neovim 0.7+, NvChad (`nvchad.term`), and the CLIs you configured (plus `lazygit` for the bottom middle pane as written).

Checked-in `screenshot.png` (~948KB) from the Oct 18 roadmap commit is the visual proof the layout existed on a real session.

## When

- **2025-10-17 morning** `a65bb50` init, `e0766a4` initial plugin release, Product Hunt marketing README (`be96f88`), docs, tests, NvChad error handling, CodeRabbit PR cleanup, merge #2.
- **2025-10-17 evening to 2025-10-18** bottom panel / 3-column layout fight: auto-launch, separate creation paths, sizing context bugs, manual vim-cmd walking (`i've been using 1 not l but needed h`), then **`903e4d6` "it works!"**, merge #3 from `stage`, `5a790f2` roadmap + screenshot.
- **2026-02-21** `6a931f6` NvimTree height + panel width restore (closes #4, #5).
- **2026-02-27 to 28** Actions build gate, Lua workflow fix, delete GitHub config for Vercel; PLAN.md + `.hustlemc` land.
- **2026-06-22** `e7833a1` / HEAD `56d0152` nightlies. Last push `2026-06-22T22:17:08Z`. **35** commits total.

![35-commit arc highlights](/blog/nvibe-neovim-cursor-coderabbit-layout-until-it-works/screenshots/commit-arc.png)

## Why

"Vibe coding" for me means **terminals I already pay for**, laid out so the agent and the reviewer never leave the frame while I edit.

I also wanted the diary of actually making Neovim splits behave. The commit messages from Oct 18 are the product as much as the README ASCII: simplify, comment out close, walk the steps, use `h` not `1`, then ship **"it works!"** Same energy as deleting a brand-new Actions workflow because the org mantra said Vercel. Leave that commit in history instead of rewriting it.

## Engagement Q

Would you hard-depend on **NvChad's terminal module** to glue Cursor Agent + CodeRabbit into Neovim, or keep AI in separate panes forever? And does a commit titled **"it works!"** after a day of `wincmd` hell earn more trust than a polished 0.1.0 badge?

---

# hustlemail.com: $8/mo email-marketing lander with missing /signup

Source: https://www.michaelchurley.com/blog/hustlemail-com-eight-dollar-lander-missing-signup  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: hustlemail-com, hustlemail, email-marketing, marketing-site, nextjs, tailwind, pricing, convertkit, mailchimp, lander, michaelmonetized

![Home hero](/blog/hustlemail-com-eight-dollar-lander-missing-signup/screenshots/home-hero.png)

## Who

I keep a private GitHub org full of product shells. Some are real apps. Some are landers that talk like apps.

For operators who need the honest split between a **$8/mo email-marketing marketing site** and the separate Resend mail-client monorepo that actually moves mail.

## What

I built **hustlemail-com**, private https://github.com/michaelmonetized/hustlemail-com. Next.js marketing shell. HEAD `ab984f2`. **3** commits. 0 stars. package name `hustlemail.com@0.1.0`. **No README.md**.

Stack facts from `package.json`: Next **^16.2.6**, React **^19.2.6**, Tailwind **^4.3.0**, Bun lockfile. Dependencies stop there: no Clerk, no Convex, no Stripe package, no Resend.

What the UI claims:

- Hero: Email marketing, **minus the bloat**. CTAs to `/signup` and `/docs`.
- Social proof strip: **10K+ Active Users** / **50M+ Emails Sent** (no backend in this tree to back that).
- Competitor cards: Mailchimp `$20+/mo`, ConvertKit `$29+/mo`, HustleMail **`$8/mo`**.
- Pricing: **Starter Free** (500 subs / 1,000 emails), **Pro $8/mo** (5,000 subs, unlimited emails, sequences, A/B, custom branding), **Business $24/mo** (25,000, dedicated IP, API, phone). Annual Pro copy: **$80/year**.
- FAQ text says Stripe + PayPal, 14-day trial, 30-day refund (still no Stripe in deps).
- Brand red: Tailwind `brand.500 = #ef4444`.

![Pricing plans](/blog/hustlemail-com-eight-dollar-lander-missing-signup/screenshots/pricing-plans.png)

![Competitor table](/blog/hustlemail-com-eight-dollar-lander-missing-signup/screenshots/competitor-table.png)

Real `page.tsx` routes: `/`, `/features`, `/pricing`, `/docs`.

Linked but **missing**: `/signup`, `/login`, `/about`, `/blog`, `/contact`, `/privacy`, `/terms`, `/community`, and the docs children (`/docs/quick-start`, `/docs/api`, …). `/docs` is an index of cards pointing at pages that do not exist.

![Docs dead links](/blog/hustlemail-com-eight-dollar-lander-missing-signup/screenshots/docs-dead-links.png)

![Missing routes](/blog/hustlemail-com-eight-dollar-lander-missing-signup/screenshots/missing-routes.png)

Sibling in the same org: `michaelmonetized/hustlemail` is a Resend+Convex keyboard-first mail client for `notify@uncap.us`. Different repo. Different job.

## Where

Code: [github.com/michaelmonetized/hustlemail-com](https://github.com/michaelmonetized/hustlemail-com), private.

Live probes at pack time:

- `hustlemail-com.vercel.app` / `hustlemail.vercel.app`: **404** `DEPLOYMENT_NOT_FOUND`
- `hustlemail.com` DNS A: **54.243.117.197** (AWS); HTTPS TLS: **UNEXPECTED_EOF** (not this Next app)

Local inspect clone: `/tmp/cf-inspect/hustlemail-com` @ `ab984f2`.

## When

**2026-02-18.** `286bef3` feat: initial hustlemail.com marketing site (+1469 / 15 files).

**2026-06-22 17:38 ET.** `e96ceb6` nightly (Fallow hooks, AGENTS.md, REVIEW.md, `.uncap`, dep bumps).

**2026-06-22 18:16 ET.** `ab984f2` nightly empty tip (HEAD).

![Commit arc](/blog/hustlemail-com-eight-dollar-lander-missing-signup/screenshots/commit-arc.png)

## Why

A lander that prices against ConvertKit still needs a `/signup` route before it is a product story. 10K+/50M+ on a four-page private repo is copy, not telemetry. The real mail work in this org lives in the Resend client; this pack keeps those narratives apart.

**Engagement Q:** How many of your $8/mo SaaS repos are four marketing pages with a missing `/signup`?

---

# Sonny's Shining: I wrote a rubber-hose beat-em-up tragedy before I picked an engine

Source: https://www.michaelchurley.com/blog/sonnys-shining-rubber-hose-beat-em-up  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: sonnys-shining, indie-game, beat-em-up, rubber-hose, noir, nextjs, stripe, gdd, fleischer, martech

![The Shining Gazette landing composite](/blog/sonnys-shining-rubber-hose-beat-em-up/screenshots/landing.png)

## Who

I kept catching myself wanting a beat-em-up that felt like Max Fleischer had a bad New Year's Eve and a bowling league problem.

I wanted the cast, the fatalities, the alley transitions, and the joke about Lucy's heart following the leaderboard on disk before I pretended an empty engine folder meant progress. A Unity purple-capsule template would not get me there. A pitch deck that says "Cuphead vibes" and ships nothing readable would not either.

Sonny's Shining is for people who will sit with a Game Design Document. Indie animation nerds. Silent-film weirdos. Operators who treat Stripe Checkout and a refund policy as pre-production work. If you build MarTech by day and still care whether Bertie the bartender gets stuffed in a trash can, this is for you.

## What

I wrote the IP as a stack of plain Markdown and a marketing site that looks like a 1935 newspaper.

The story: New Year's Eve. Bertie's Bustling Bubbles. Sonny (lanky hound dog man, Sonny Tufts energy, ball-shining towel in the back pocket) holds Lucy, a red fox with Lucille Ball danger in the smile. The bartender whispers. She leaves. Kewpie's bowling-pin limo peels out. Eight levels of Kewpie's payroll stand between a tournament bowler and a girl whose heart tracks the standings.

![Eight bosses / levels](/blog/sonnys-shining-rubber-hose-beat-em-up/screenshots/bosses.png)

Combat is bowling-native. Towel catch-and-return. Barehand bottle returns when the timing is honest. Later, a modified ball-return device becomes an over-the-shoulder launcher. Bosses are silent-film and vaudeville ghosts wearing animal suits: Bert Williams behind the bar, Chaplin as a mouse puppeteering a Fabio crab, Desi on the fire escape, Tippi in the dance school, Bessie on the decks, Snub Pollard in the hangar, Ivy Lee on the docks, Kewpie Morgan as the pig kingpin with a shipboard alley that sways on purpose.

Art bible is rubber hose / Fleischer / Roger Rabbit: noodle limbs, pie-cut eyes, four-finger gloves, springy idle. `GDD.md` is ~1,250 lines. `NOVEL.md` ~1,600. `SCRIPT.md` ~800. `PLAY.md` stages it as a tragedy in three acts. `PLAN.md` still says engine TBD (Unity or Godot). That sentence is accurate. There is no `.unity` or Godot project in this repo yet.

The web half is real software. Next.js 16.2.6, React 19, Tailwind 4, Bun lockfile, package **0.1.0**. Landing is "The Shining Gazette": ticker tape, masthead, drop caps, classified boxes. `PreorderButton` collects email, hits `POST /api/checkout`, opens Stripe Checkout for **$8.00** (`unit_amount: 800`) with metadata `sonnys-shining-preorder` and expected release **Christmas 2026**. Privacy, terms, refunds, and success routes shipped with that checkout.

![Preorder / Stripe surface](/blog/sonnys-shining-rubber-hose-beat-em-up/screenshots/preorder.png)

## Where

Marketing lives at [sonnysshining.com](https://sonnysshining.com). Code at [github.com/michaelmonetized/Sonny-s-Shining](https://github.com/michaelmonetized/Sonny-s-Shining). Public. Empty GitHub description. Empty topics. Zero stars. That is fine. The README and the Gazette carry the pitch.

Audience: people who want Cuphead's lineage without Cuphead's budget, and builders who know a preorder page without a refund policy becomes an apology later.

## When

**2025-12-27.** Four commits in one afternoon. `init`. Web submodule and last names stripped from the celebrity inspo list. Novel pass. "initalize marketing website for the novel" (yes, that typo is in the commit message). Next 16.1.1 scaffold era.

**2026-01-31.** `chore: sync all changes`, one big dump, +1,640 / −79. Writing and early web catching up with each other.

**2026-03-17.** The money day. Privacy, terms, refunds, success. Stripe checkout route. PreorderButton. Newspaper redesign of `page.tsx`. `PLAN.md` lands. `ROADMAP.md` also lands, and it is wrong. It talks about a cleaning/detailing client site. I am leaving that stale stub in the tree rather than rewriting history.

**2026-06-22.** Two `nightly` commits. GDD, Stripe cursor rules, fallow review hooks, dependency bump to Next 16.2.6 and `stripe` ^22. HEAD `d6ae3ac`. Still no engine.

**2026-08-16.** GitHub `pushed_at` moves. Main still ends at the June nightly. Eight commits on the ledger. Pack day is September 8, 2026.

Empty repo to a design-complete tragedy with a live preorder path and a missing engine folder.

## Why

I did not want to lie to myself with a blank game project named after a feeling.

So I wrote the characters until Sonny's ears twitched in idle. I wrote the levels until the alley between Bertie and Charlie was a cut you can read. I put $8 and Christmas 2026 on a Stripe session so the promise had a price and a date. I left Unity/Godot as a checkbox in `PLAN.md` because checking a box is not shipping a towel mechanic.

It is still web 0.1.0. The newsletter form on the lander is client-side only, with no list backend. `ROADMAP.md` needs to be deleted or rewritten. The game does not run. The bible does.

Would you lock the Christmas 2026 preorder first, or open the engine folder and refuse to write another fatality until Bertie's bottle timing feels true?

---

# orclawstrator: I built the OpenClaw command center in Swift, then archived it for a Go TUI

Source: https://www.michaelchurley.com/blog/orclawstrator-swift-appkit-to-go-tui-openclaw-gateway  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: orclawstrator, openclaw, swift, appkit, go, bubbletea, tui, sqlite, graphite, vercel, github-cli, macos, agent-orchestration, michaelmonetized

![Orclawstrator TUI dashboard. projects, agents, branches, stacks](/blog/orclawstrator-swift-appkit-to-go-tui-openclaw-gateway/screenshots/tui-dashboard.png)

## Who

I wanted one keyboard surface for every AI coding agent running across `~/Projects`. Slack tabs and Vercel log windows were the wrong shape for sessions, tokens, and dirty git state.

Orclawstrator is for operators with an OpenClaw Gateway on localhost who need that portfolio strip in one place. It is also for anyone who opens both `swift-appkit/` and `tui/` and has to decide which runtime is the install path.

I keep three differently named mission / command center repos. This one is the OpenClaw Gateway story: `ws://localhost:3377/ws`. The others are separate products with separate stores.

## What

I built **Orclawstrator**. Public **michaelmonetized/orclawstrator**, bundle **0.1.0**, **8** commits, HEAD `01d9a20`. README one-liner: *Command center for orchestrating AI coding agents across your entire project portfolio.* Lobster branding. “Built with 🦞 by the OpenClaw ecosystem.” GitHub description field is empty.

![Dual runtime. swift-appkit archived, tui recommended, shared cache.db](/blog/orclawstrator-swift-appkit-to-go-tui-openclaw-gateway/screenshots/dual-runtime.png)

**Two UIs. One SQLite file.**

1. **Swift AppKit** (path `swift-appkit/`, README: archived). macOS 14+, Swift 5.9, Catppuccin chromeless window, dashboard table (agent, branches, stacks, untracked/staged, Vercel build), sidebar chat + inbox, project detail with **SwiftTerm** embedding nvim for README/PLAN/ROADMAP/CHANGELOG tabs, PR stack popover, branch checkout popup, Cmd+K quick switcher, menu-bar `NSStatusItem`, ErrorBanner. Services: `ShellExecutor`, `GitService`, `GitHubService` (`gh … --json`), `GraphiteService` (`gt log short --stack`), `VercelService` (`vercel ls --yes`), `OpenClawService` (REST + `ws://host:port/ws`), `ProjectScanner`, `DatabaseManager`.

2. **Go TUI** (path `tui/`, README: recommended). Go **1.21**, Charm **Bubble Tea** / Bubbles / Lipgloss, `go-sqlite3`, vim j/k/h/l, Nerd Font icons, dashboard + project detail + inbox views, `make run` / `make install` to binary `orclawstrator`. Module path still `github.com/michaelcolletti/orclawstrator`. Committed binary ~8.3MB in tree.

Shared schema at `~/.orclawstrator/cache.db`: `projects`, `sessions`, `messages`, `settings`, `recent_chats`. Gateway host/port from settings (defaults **localhost:3377**).

![OpenClaw Gateway REST + WebSocket session/token path](/blog/orclawstrator-swift-appkit-to-go-tui-openclaw-gateway/screenshots/openclaw-gateway.png)

Swift is still ~210KB of source vs ~39KB Go. Linguist reports Swift. README says use the TUI. `sitrep.md` last updated 2026-02-09 still calls the AppKit stack Active Development ~85%. `AUTOPSY.md` calls the Feb patient RESUSCITATED / ship-worthy. PLAN.md still has open Phase 4-6 checkboxes that the Feb code partially answered.

Name collision to avoid: `michaelmonetized/mission-control` is the p10k-style `mc` TUI under `~/.hustlemc/` with a Phase 2 Fly/Claude SaaS scaffold. `HurleyUS/hurley-mission-control` is the Clerk + Convex `users.kind` human|agent deliveries web app. Orclawstrator is the OpenClaw gateway portfolio strip only.

## Where

Code: [github.com/michaelmonetized/orclawstrator](https://github.com/michaelmonetized/orclawstrator). Public, branch **main**. No dedicated homepage / Vercel lander.

![AppKit chromeless Catppuccin dashboard (archived path)](/blog/orclawstrator-swift-appkit-to-go-tui-openclaw-gateway/screenshots/appkit-dashboard.png)

Local surfaces: `cd tui && make run` · `cd swift-appkit && swift build` · cache `~/.orclawstrator/cache.db` · Gateway `http://localhost:3377` + `ws://…/ws`. Optional CLIs: `gh`, `gt`, `vercel`, Nerd Font.

Audience sits with multi-repo agent fleets that outgrew a single IDE chat panel, and with anyone who needs the OpenClaw gateway pack named without stealing the p10k TUI or the Convex comms plane.

## When

**2026-02-06.** Initial README + PLAN mockup. Same afternoon: AppKit prototype with dashboard + git. Same evening: MVP split view sidebar + dashboard.

**2026-02-08.** Core services wired, project detail, SQLite persistence. Later: chromeless semi-transparent window, full-width status bars.

**2026-02-09.** SwiftTerm for proper nvim terminal emulation in markdown tabs. AUTOPSY / inbox / shortcuts / PR stack / menu bar wave lands in the patient chart.

**2026-06-22.** `nightly`: move AppKit under `swift-appkit/`, add complete Go TUI + committed binary, flip README to TUI-recommended. Second `nightly` becomes HEAD `01d9a20` (18:15 ET). GitHub `pushed_at` 2026-06-22T22:15:42Z.

![Commit arc. Feb sprint to Jun dual-runtime nightly](/blog/orclawstrator-swift-appkit-to-go-tui-openclaw-gateway/screenshots/commit-arc.png)

Eight commits. No post-June product commits in the log.

## Why

OpenClaw needed a portfolio command surface that spoke Gateway sessions and tokens, not just git dirty counts.

I proved the AppKit path in four days, SwiftTerm nvim tabs included, and still decided the install story operators would actually run was `make install` in a terminal.

Sharing `~/.orclawstrator/cache.db` across runtimes is the monorepo bet. Renaming folders under a `nightly` commit is how that bet got honest.

Saying “mission control” three times in the org without naming which one talks to `ws://localhost:3377/ws` is how readers get the wrong pack.

When README recommends the Go TUI but linguist still reports Swift, which runtime owns the product title?

---

# iPro.golf: the golf course & resort marketing agency lander — not iLeague, not iTour

Source: https://www.michaelchurley.com/blog/ipro-golf-agency-lander-ecosystem-hub  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: ipro, ipro-golf, golf-marketing, agency, golf-course, resort, country-club, ecosystem-hub, ileague, itour, iconference, nextjs, vercel, shadcn, hurleyus

![iPro.golf agency homepage mock: hero, ecosystem chips, services](/blog/ipro-golf-agency-lander-ecosystem-hub/screenshots/home.png)

## Who

I ship golf products that look like one brand family and are three different businesses. [iLeague.golf](https://ileague.golf) is the creator platform: scorecards, subscriptions, tips, fifteen percent fee. That pack already exists: `ileague-golf-patreon-meets-18birdies`. [iTour.golf](https://www.itour.golf) is the national creator-tour lander: sponsors, host bids, season schema, packed as `itour-golf-tour-lander-ahead-of-deploy`.

iPro.golf is the **agency**. Country clubs, courses, resorts. Retainers. Case-study theater. A brochure that also hubs the ecosystem.

If you care about multi-domain Vercel rewrites, agency landers that declare Stripe/Clerk/Convex and never wire them, or how to keep the creator-SaaS story off a URL that says "Pro": this is the field notes.

## What

I keep the live marketing site for the agency under `HurleyUS/iPro-main-web`.

README one-liner: connective tissue between influencers and golf courses, country clubs, resorts, and growing golf communities. Layout title that also wins on curl: **iPro.golf | Golf Course & Resort Marketing Agency**. Badge on the hero: the same phrase. H1: **iPro.golf**.

Stack on the box: **Next.js 16.2**, **React 19**, Tailwind v4, Geist, a full shadcn/Radix tree, Bun lockfile, Vercel. Package name **web**, version **0.1.0**. Repo private. Site public at [www.ipro.golf](https://www.ipro.golf) (apex 308s to www). Ten commits. HEAD `b8dfdba`.

![Retainer pricing mock: Starter / Growth / Premium](/blog/ipro-golf-agency-lander-ecosystem-hub/screenshots/services-pricing.png)

Agency retainers on the home page (this is the services brochure, separate from the creator billing product and the tour season product):

1. **Starter** $1,997/mo (refresh, social setup, GBP, reporting)
2. **Growth** $4,997/mo (most popular: full social, email, content, paid, strategy)
3. **Premium** $9,997+/mo (video, influencer campaigns, events, member acquisition, dedicated team)

Services grid: Brand Design, Web Development, Social Media, Local SEO, Email Marketing, Video Production. CTAs go to `/contact` and `/case-studies`.

What is also true on pack day:

- **Live www title matches HEAD**, unlike the iTour pack where production still serves a fake 2026 season shell.
- `layout.tsx` points Open Graph at `/og-image.png`. That file is missing from `public/`. curl returns **404**.
- Contact `handleSubmit` is a one-second `setTimeout` with a TODO for Resend. Phone is **(555) GOLF-PRO**. Email `hello@ipro.golf`.
- Case studies (Highland Links, Coastal Resort & Spa, Valley Municipal) are fiction. Feb 9 AUTOPSY said the quiet part out loud; home metrics later sit at 50+ / 35% / 2.5x / $1.8M instead of the autopsy's 120+ / 47% / $2.4M.
- `package.json` still lists Clerk, Stripe, Convex, PostHog, Resend, react-email. Fallow REVIEW: unused. No `convex/` tree. No `app/api`. Layout has no Clerk provider.
- `/ileague`, `/itour`, `/iconf` on this repo are **Coming Soon / waitlist shells**. The real products are separate repos and separate packs.
- Agency `/itour` copy still says **National Amateur Golf Tour** and a 2026 regional event grid. That fights the packed iTour creator-tour / May 2027 story. Call the conflict; do not merge the posts.
- Private vault `michaelmonetized/iPro` is already **SKIP**: logos, planning markdown, gitlink into this app. Do not retell it here.

![Ecosystem differentiation: agency vs creator SaaS vs tour vs summit](/blog/ipro-golf-agency-lander-ecosystem-hub/screenshots/ecosystem-diff.png)

## Where

It runs on Vercel as a prerendered marketing site (`x-nextjs-prerender: 1` on the 2026-09-08 probe). Dark theme, fixed header, footer with Services / Ecosystem / Company columns.

Surfaces that matter:

- Public agency pages: home, about, services, case studies, contact, blog listing, privacy, terms
- Ecosystem brochure pages: `/ileague` Coming Soon, `/itour` Coming 2026, `/iconf` Fall 2026
- `proxy.ts`: if host is `iconference.golf` or `www.iconference.golf` and path is `/`, rewrite to `/iconf`. Live iconference.golf returns **200** with `x-matched-path: /iconf`
- shadcn kit under `components/ui/*`: mostly unused; used pieces are Button, Sheet, form controls on contact

Live HTML on pack day did not need Clerk keys to render. There is no auth wall on the agency brochure.

![Honesty board: live wins vs remaining gaps](/blog/ipro-golf-agency-lander-ecosystem-hub/screenshots/honesty-board.png)

Related domains: **iPro.golf** (emerald agency, this pack), **iLeague.golf** (creator platform, packed), **iTour.golf** (tour, packed), **iConference.golf** (summit shell rewritten onto this project). Vault sibling SKIPPED.

## When

**November 24, 2025.** Create Next App. Init thrash. Empty license. Another init. The repo exists before the golf pitch is real.

**February 5, 2026.** `chore: prod deploy`: first push toward something live.

**February 9.** The meaningful day. `feat: Complete site rebuild with pages, navigation, and proper structure`: the App Router surface that still matches www. Same day: AUTOPSY updated with GoDaddy DNS instructions and a brutal inventory of placeholders. DNS later got fixed; the autopsy file did not get a matching rewrite.

**May 21.** `Fix iConference domain deployment`. CHANGELOG: production routing for iconference.golf onto the iPro Vercel project + UI compatibility so the Next production build passes. `proxy.ts` is the host rewrite.

**June 22.** Two `nightly` commits. HEAD `b8dfdba`. Package deps float forward; the brochure story does not.

## Why

The ecosystem needs a front door that sells **services to properties** without pretending it is the creator SaaS or the tour.

Operators reading three golf posts in a row deserve a clean split: agency retainers here, Patreon x 18Birdies there, sponsor/host season over there.

A domain rewrite for iconference.golf onto `/iconf` is a real ops story. A Coming Soon waitlist is still not shipping iConference.

The vault repo will keep tempting people to package logos as a product. Ship the app; leave the logo vault alone.

**Engagement Q:** When the agency lander links to Coming Soon ecosystem shells that already have live product domains elsewhere, do you keep the hub honest as a brochure, or do you outbound-link straight to the real products and delete the shells?

---

# hustledesk.com: $8 flat helpdesk lander vs WordPress domain

Source: https://www.michaelchurley.com/blog/hustledesk-com-eight-dollar-helpdesk-lander-wordpress-domain  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: hustledesk-com, hustledesk, helpdesk, zendesk-alternative, support-tickets, marketing-site, nextjs, tailwind, pricing, wordpress, lander, michaelmonetized

![Home hero](/blog/hustledesk-com-eight-dollar-helpdesk-lander-wordpress-domain/screenshots/home-hero.png)

## Who

I keep a private GitHub org full of product shells. Some are real apps. Some are landers that talk like apps. Some claim a domain that answers something else entirely.

This pack is for operators who need the honest split between a **$8/mo flat helpdesk marketing site** and the **WordPress parking page** currently living at hustledesk.com.

## What

I built **hustledesk-com**, private at [github.com/michaelmonetized/hustledesk-com](https://github.com/michaelmonetized/hustledesk-com). Next.js marketing shell. HEAD `d446e97`. **3** commits. 0 stars. package name `hustledesk.com@0.1.0`. README is **stock** create-next-app boilerplate.

Stack facts from `package.json`: Next **16.2.6**, React **19.2.6**, Tailwind **^4.3.0**, Bun lockfile. Dependencies stop there. no Clerk, no Convex, no Stripe package, no IMAP/email inbound library.

What the UI claims:

- Hero: "Support tickets, **nothing more**." Subhead inbox zero. Price line **$8/mo. Really.**
- CTAs: **Start Free Trial** to `https://app.hustledesk.com/signup`, **See Features** to `/features`. Header **Sign in** to `https://app.hustledesk.com/login`.
- Home **inbox mock**: Sarah Chen "Can't reset password" (SLA: 28m left), Mike Johnson, Emily Davis. chrome window, not a product screenshot.
- Feature grid: Email Inbound, Ticket Inbox, Canned Responses, SLA Timers, Team Assignment, **HustleChat Integration**.
- Competitor cards: Zendesk `$55+`, Freshdesk `$18+`, Help Scout `$25+` (per user) vs HustleDesk **`$8` flat · Unlimited users**.
- Pricing: **one** plan. $8/month flat; team-of-5 table ends at Zendesk **$3,300/yr** vs HustleDesk **$96/yr** ("Save $3,204/year…").
- Brand sky: Tailwind `--color-hustle-500 = #0ea5e9` / `600 = #0284c7`.

![Pricing flat $8](/blog/hustledesk-com-eight-dollar-helpdesk-lander-wordpress-domain/screenshots/pricing-flat.png)

![Competitor table](/blog/hustledesk-com-eight-dollar-helpdesk-lander-wordpress-domain/screenshots/competitor-table.png)

Real `page.tsx` routes: `/`, `/features`, `/pricing`, `/docs`.

Linked but **missing in-repo**: `/privacy`, `/terms`, and the docs children (`/docs/quick-start`, `/docs/email-forwarding`, `/docs/api`, `/docs/hustlechat`, …). `/docs` is an index of cards pointing at pages that do not exist.

![Docs dead links](/blog/hustledesk-com-eight-dollar-helpdesk-lander-wordpress-domain/screenshots/docs-dead-links.png)

![Missing routes + app 301](/blog/hustledesk-com-eight-dollar-helpdesk-lander-wordpress-domain/screenshots/missing-routes.png)

This is not `hustlemail-com`. that sibling is the **email-marketing** $8 lander (red `#ef4444`, Free/$8/$24, in-repo missing `/signup`). Different product claim. Different brand. Different honesty bug.

## Where

Code: [github.com/michaelmonetized/hustledesk-com](https://github.com/michaelmonetized/hustledesk-com) (private).

Live probes at pack time:

- `hustledesk.com` DNS A to **66.96.162.142**; HTTPS **200** WordPress PHP/7.4.33; title **Hustle Desk – Make extra income from the comfort of your home**; default "This is your front page" copy. **not** this Next helpdesk lander
- `www` / `app.hustledesk.com` to same A; `app` **301** `X-Redirect-By: WordPress` to `https://hustledesk.com/`
- `hustledesk-com.vercel.app` to **404** `DEPLOYMENT_NOT_FOUND`
- `hustledesk.vercel.app` to unrelated Vite SPA (`hustledesk`)

Local inspect clone: `/tmp/cf-inspect/hustledesk-com` @ `d446e97`.

## When

**2026-02-18 07:57 ET.** `4439645` feat: initial hustledesk.com marketing site (+2206 / 22 files).

**2026-06-22 17:23 ET.** `b45b480` nightly (Fallow hooks, AGENTS.md, REVIEW.md, `.uncap`, dep bumps).

**2026-06-22 18:14 ET.** `d446e97` nightly empty tip (HEAD).

![Commit arc](/blog/hustledesk-com-eight-dollar-helpdesk-lander-wordpress-domain/screenshots/commit-arc.png)

## Why

A Zendesk-price lander still needs an auth surface that is not a WordPress 301. "Save $3,204/year" on a four-page private repo is table copy, not a billed product. hustlemail-com already told the $8 lander story for email marketing. this pack is the **helpdesk** twin with a **domain that answers something else**.

How many of your SaaS domains currently serve a default WordPress "Make extra income" front page while the Next lander never shipped?

---

# redactthing: streamer PII Chrome extension that still ships unused jQuery and a 404 lander

Source: https://www.michaelchurley.com/blog/redactthing-streamer-pii-mv3-jquery-ghost-iframe-gap  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: redactthing, chrome-extension, manifest-v3, privacy, pii, streaming, mutation-observer, jquery, google-sites, hustlelaunch, michaelmonetized

![Five redaction modes](/blog/redactthing-streamer-pii-mv3-jquery-ghost-iframe-gap/screenshots/modes-matrix.png)

## Who

I stream. Inboxes and admin UIs leak emails and phones the second the browser is shared. I wanted one extension click that covers PII without waiting for every site to grow a privacy mode.

## What

Public **michaelmonetized/redactthing** · Chrome MV3 · extension **0.0.1** · HEAD `5c4661c` · **7** commits. Modes: redact / blur / mask / hide / show. Email + phone regex + custom lines. MutationObserver tree walk. Popup **Redact Now**. `chrome.storage.sync` settings. package.json says MIT 1.0.0; `LICENSE.md` is GPL-3.0. `jquery.js` (~84KB) still in `content_scripts` with zero `$()` usage after the Jun 22 vanilla rewrite. hustlelaunch.com/redactthing **404**. `ROADMAP.md` describes document/PDF SaaS; `PLAN.md` matches the extension.

![Settings + popup](/blog/redactthing-streamer-pii-mv3-jquery-ghost-iframe-gap/screenshots/settings-popup.png)

![jQuery ghost](/blog/redactthing-streamer-pii-mv3-jquery-ghost-iframe-gap/screenshots/jquery-ghost.png)

![Google Sites iframe gap](/blog/redactthing-streamer-pii-mv3-jquery-ghost-iframe-gap/screenshots/iframe-gap.png)

## Where

github.com/michaelmonetized/redactthing (public). Unpacked `src/`. No CWS. No demo host. hustlelaunch.com root is live; `/redactthing` is not.

![Commit arc](/blog/redactthing-streamer-pii-mv3-jquery-ghost-iframe-gap/screenshots/commit-arc.png)

## When

2024-08-21 init · 2024-08-23 Google Sites iframe commit · 2026-01-31 sync (+ STRIPE.md) · 2026-02-27 package.json · 2026-06-22 nightly rewrite `7881d00` · empty nightly HEAD `5c4661c` same day.

![ROADMAP irony](/blog/redactthing-streamer-pii-mv3-jquery-ghost-iframe-gap/screenshots/roadmap-irony.png)

## Why

Streamer PII is a content-script problem. The repo still ships the unused jQuery, the 404 lander, and a ROADMAP for a different product. The iframe limit is documented in a commit subject.

**Engagement Q:** Vanilla MutationObserver already shipped — delete the jQuery ghost tonight, or carry 84KB into the store listing?

---

# shipprep: default APPLY for the HurleyUS JS shipping standard

Source: https://www.michaelchurley.com/blog/shipprep-default-apply-biome-tsgo-blacksmith-vercel-off  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: shipprep, bun, cli, biome, tsgo, blacksmith, vercel, freview, shipping-standard, dev-tooling, caddy, michaelmonetized

## Who

I got tired of re-typing the same Biome/tsgo/Blacksmith/Vercel-off checklist into every Next/Bun root, so the checklist became a CLI that **applies by default**.

For operators who want Vercel Git auto-deploy **off** on main/master and Blacksmith owning `vercel deploy --prebuilt`.

## What

I built **shipprep**, private https://github.com/michaelmonetized/shipprep. HEAD `c35a9d8`. **6** commits. **0** stars. Version **0.1.0**. No README.

`bin/shipprep.mts` (**403** LOC). Shebang Bun. `parseArgs` defaults `apply: true`. `--audit` / `--check` flips read-only. Accepts `--pwd` even when chat clients paste U+2014 in place of ASCII hyphens.

![APPLY report](/blog/shipprep-default-apply-biome-tsgo-blacksmith-vercel-off/screenshots/apply-report.png)

**APPLY** writes package scripts (`tsc`/`typecheck` to `tsgo --noEmit`, `lint` to biome-lint), tsconfig bun+node + `**/*.mts`, `scripts/dev-localhost*.mjs`, `scripts/ship.mts`, freview `pre-push`, `.github/workflows/ship.yml` on `blacksmith-4vcpu-ubuntu-2404`, and `vercel.json` with `git.deploymentEnabled.main/master = false`.

![Vercel git off](/blog/shipprep-default-apply-biome-tsgo-blacksmith-vercel-off/screenshots/vercel-git-off.png)

![Blacksmith ship.yml](/blog/shipprep-default-apply-biome-tsgo-blacksmith-vercel-off/screenshots/blacksmith-ship-yml.png)

![Audit checks](/blog/shipprep-default-apply-biome-tsgo-blacksmith-vercel-off/screenshots/audit-checks.png)

Required roots for validRoot: `package.json`, `bun.lock`, `.vercel`, `.next`. Tests in `test/shipprep.test.ts` (**122** LOC).

Sibling tooling in the org: shipthing is a contacts CRM named for carrier rates; bundx-init is Caddy `.localhost` only. This CLI is the shipping-standard migrator.

## Where

Code only: [github.com/michaelmonetized/shipprep](https://github.com/michaelmonetized/shipprep), **private**. No live site.

```bash
bun bin/shipprep.mts --pwd ~/Projects/app
bun bin/shipprep.mts --audit --json
```

## When

**2026-05-13 14:08 ET.** `d234692` audit executable.

**14:17 ET.** `811a0ac` false positives.

**14:30 ET.** `6cf4af4` APPLY migrations; default flips to apply.

**14:54 ET.** `cf6928a` disable Vercel git deploys.

**2026-06-22.** nightly ×2, `.uncap`, HEAD `c35a9d8`.

![Commit arc](/blog/shipprep-default-apply-biome-tsgo-blacksmith-vercel-off/screenshots/commit-arc.png)

## Why

Audit-only tools leave you copy-pasting. Blacksmith prebuilt plus Vercel git disabled is a **gate**, not a README hope. Default APPLY is the point of the tool; `--audit` stays opt-in.

**Engagement Q:** Keep APPLY as the default, or flip to `--audit` default so a typo cannot rewrite twelve package scripts?

---

# Assessment-Toolbar: pink SEO chrome bar that promises Lighthouse and double-injects itself

Source: https://www.michaelchurley.com/blog/assessment-toolbar-chrome-mv3-lighthouse-missing-double-inject  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: assessment-toolbar, marketing-assessments, chrome-extension, manifest-v3, seo, spyfu, wave, lighthouse, content-scripts, hustlelaunch, michaelmonetized

![Pink marketing assessments bar](/blog/assessment-toolbar-chrome-mv3-lighthouse-missing-double-inject/screenshots/bar-mock.png)

## Who

I run marketing assessments. Every client tab needs the same SEO stack against the live URL, without hunting bookmarklets.

## What

Public **michaelmonetized/Assessment-Toolbar**. Chrome MV3. Name **Marketing Assessments**. v**1.0**. HEAD `9652620`. **6** commits. Pink #ffc9dd top strip: SpyFu, SiteLiner, Rich Results, Schema, Mobile-Friendly, WAVE, Wayback, Whois, plus FB/NAP prompts, missing-alt highlighter, title clipboard, and Ctrl+Alt+M. GPL-3.0. No package.json.

Manifest + README promise **google lighthouse**. `content.js` has **zero** Lighthouse/PageSpeed link. `background.js` admits it never knew what the file is for and re-runs `content.js` while declarative content scripts already load it. Perms: activeTab + scripting only. Rich Results / Mobile-Friendly get `location.hostname`. Five tools are static. hustlelaunch.com/assessment-toolbar returns **404**.

![Lighthouse gap](/blog/assessment-toolbar-chrome-mv3-lighthouse-missing-double-inject/screenshots/lighthouse-gap.png)

![Dual load](/blog/assessment-toolbar-chrome-mv3-lighthouse-missing-double-inject/screenshots/double-inject.png)

![URL bugs](/blog/assessment-toolbar-chrome-mv3-lighthouse-missing-double-inject/screenshots/url-bugs.png)

## Where

github.com/michaelmonetized/Assessment-Toolbar (public). Unpacked root. No CWS. No demo host. hustlelaunch.com root is live; product paths are not.

![Commit arc](/blog/assessment-toolbar-chrome-mv3-lighthouse-missing-double-inject/screenshots/commit-arc.png)

## When

**2024-08-16.** init + pre-flight.

**2024-08-22.** ready.

**2026-01-31.** STRIPE.md sync.

**2026-06-22.** nightly metadata; empty nightly HEAD `9652620` same day.

## Why

Assessment strip that advertises Lighthouse without shipping it, and a service worker that dual-loads while admitting it does not know its job.

**Engagement Q:** Add PageSpeed/Lighthouse and drop the dual-load SW tonight, or leave v1.0 lying in its own description?

---

# freview: README says five reviews — REVIEW.md actually stitches six

Source: https://www.michaelchurley.com/blog/freview-five-reviews-six-sections-observability-gate  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: freview, fallow, scribe, pre-push, review, observability, sentry, posthog, blacksmith, zsh, bun, hurleyus

![Five marketed reviews vs six REVIEW.md sections](/blog/freview-five-reviews-six-sections-observability-gate/screenshots/six-sections.png)

## Who

I got tired of "we'll catch it in CI" turning into a Slack autopsy after `main` already moved. Pre-push should hurt a little when the tree is sketchy and stay quiet when it is clean.

For JS/TS teams (and anyone whose public symbols need docstrings across TS/Swift/Rust/Python/shell) who want one command, one durable `REVIEW.md`, and the same gate locally and on Blacksmith.

## What

I built **freview**, public [`HurleyUS/freview`](https://github.com/HurleyUS/freview). Package `@hurleyus/freview` **0.1.0**. MIT. zsh. `bin/freview` is **404** lines; bundled `bin/scribe` is **555**. HEAD `23237d9`. **22** commits. 0 stars. CLI / package only.

![CLI / section pipeline](/blog/freview-five-reviews-six-sections-observability-gate/screenshots/six-sections.png)

The README and `TWITTER-RELEASE-THREAD.md` still sell **"One command. Five reviews."** The orchestrator always appends **six** sections into root `REVIEW.md`:

1. **OBSERVABILITY**: embedded Python detects Next / React Native-Expo / Electron / Swift; requires Sentry + PostHog deps, init, and env-signal strings; if `.vercel/project.json` exists, `vercel env ls production` must show the DSN/key/host set. Library/tooling repos with no platform pass.
2. **HEALTH**: `bunx --bun fallow … health --complexity` (soft-pass when exit≠0 but no fail glyphs)
3. **AUDIT**: `fallow audit`
4. **DEAD**: `fallow dead-code`
5. **DUPLICATION**: `fallow dupes`
6. **DOCSTRINGS**: bundled `scribe` (not Michael's old `~/bin/scribe`)

Before any of that: `fallow init` + `fallow setup-hooks`, then freview rewrites the Claude `fallow-gate.sh` matcher from `git commit|push` to **`git push` only** so local commits stay unblocked.

Flags you actually use: `--root`, `--format`, `--quiet`, `--explain`, `--summary`, `--ci` (SARIF + quiet + fail-on-issues), `--fail-on-issues`. Clean runs write the file and shut up. Dirty runs print the report and exit 1. Empty SARIF result sets do not fail CI mode.

![Observability platforms](/blog/freview-five-reviews-six-sections-observability-gate/screenshots/observability-platforms.png)

Install paths: curl both bins into `~/bin`, clone + symlink, or `bunx github:HurleyUS/freview` / `bun link`. Pre-push snippet in the README only fires on protected `main`/`master` refs.

Residue / irony: shell-only package (`scripts.lint` and `scripts.check` are literally `true`), yet `.github/workflows/ci.yml` still installs zsh and runs `bunx --bun github:HurleyUS/freview --ci` on `blacksmith-4vcpu-ubuntu-2404`. May 14 is seven commits all titled **"Standardize Blacksmith CI gates."** May 14 also briefly committed generated Claude hooks, deleted them seventeen minutes later, then re-tracked them a week later. Second June 22 nightly (`23237d9`) shares the exact tree with the `.uncap` nightly: empty HEAD.

![Blacksmith CI self-review](/blog/freview-five-reviews-six-sections-observability-gate/screenshots/ci-blacksmith.png)

## Where

Code: [github.com/HurleyUS/freview](https://github.com/HurleyUS/freview), public, branch `main`. No homepage. No live web demo. Sibling tooling surface: Fallow + Claude Code hooks + Blacksmith runners.

## When

**2026-05-13, 1:41–1:51 PM Eastern.** Init (150-line freview), Twitter thread, bundle scribe + package.json.

**2026-05-13, 7:23–8:06 PM Eastern.** Observability gate, then platform-aware rewrite (Next/RN/Electron/Swift + Vercel env).

**2026-05-14 afternoon Eastern.** Blacksmith CI standardization spam, empty SARIF handling, avoid oxlint on shell-only, hook churn, prettier CI commit.

**2026-05-21 morning Eastern.** Empty health soft-pass (PR #1), track fallow Claude hook, push-only gate + freview rewriter.

**2026-06-22, 5:20 PM Eastern.** `8fd392a` nightly: `.uncap/config.json`.

**2026-06-22, 6:12 PM Eastern.** `23237d9` nightly: empty tree. HEAD.

![Commit arc](/blog/freview-five-reviews-six-sections-observability-gate/screenshots/commit-arc.png)

## Why

Terminal scrollback is a graveyard. `REVIEW.md` survives. "Five reviews" was the pitch; observability became the sixth section the docs never renumbered. A pre-push hook that also rewrites your Claude gate to push-only is the boring guardrail I actually leave installed.

**Engagement Q:** If your pre-push suite marketed five checks, which sixth gate would you sneak in first: observability, license, or "did CI prettier already fight you"?

---

# hustleconvert.com: $8/mo popup lander, OptinMonster table, NXDOMAIN

Source: https://www.michaelchurley.com/blog/hustleconvert-com-eight-dollar-popup-lander-nxdomain  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: hustleconvert-com, hustleconvert, popups, exit-intent, conversion, optinmonster, sumo, marketing-site, nextjs, tailwind, pricing, lander, michaelmonetized

![Home hero](/blog/hustleconvert-com-eight-dollar-popup-lander-nxdomain/screenshots/home-hero.png)

## Who

I keep a private GitHub org full of product shells. Some are real apps. Some are landers that talk like apps.

For operators who need the honest split between an **$8/mo popup marketing site** and a claimed `app.` / CDN product that is not in this repo. Same org also holds email and helpdesk `$8` landers; this one is the popup lane.

## What

I built **hustleconvert-com**, private https://github.com/michaelmonetized/hustleconvert-com. Next.js marketing shell. HEAD `a5d3f13`. **3** commits. 0 stars. package name `hustleconvert.com@0.1.0`. README is stock create-next-app.

Stack facts from `package.json`: Next **16.2.6**, React **19.2.6**, Tailwind **^4.3.0**, Bun lockfile. Dependencies stop there: no Clerk, no Convex, no Stripe package, no popup runtime SDK.

What the UI claims:

- Hero: Popups that **don't annoy**. Badge: **Now with A/B testing.** CTAs to `https://app.hustleconvert.com/signup` and `/templates`.
- Trial strip: **14-day free trial · No credit card required**.
- Features: visual builder, smart triggers (exit-intent / scroll / time / click), display rules, A/B, analytics, **50+** templates.
- Competitor table: OptinMonster `$20+/mo`, Sumo `$49+/mo`, HustleConvert **`$8/mo`** with Unlimited Popups + No Branding checked.
- Pricing: **Free** (1 campaign / 1,000 impressions), **Pro $8/mo**, **Team $24/mo**. Annual Pro copy: **$80/year**.
- FAQ text says Stripe (+ Team invoicing); still no Stripe in deps.
- Brand: Tailwind `brand.500 = #0ea5e9`, `accent.500 = #8b5cf6`.
- Docs Quick Install snippet points at `https://cdn.hustleconvert.com/v1/hc.min.js`.

![Pricing plans](/blog/hustleconvert-com-eight-dollar-popup-lander-nxdomain/screenshots/pricing-plans.png)

![Competitor table](/blog/hustleconvert-com-eight-dollar-popup-lander-nxdomain/screenshots/competitor-table.png)

Real `page.tsx` routes: `/`, `/pricing`, `/templates`, `/docs`.

Linked but **missing**: footer `/blog`, `/help`, `/about`, `/contact`, `/privacy`, `/terms`, `/docs/changelog`, `/docs/api`, and the docs children (`/docs/quickstart`, `/docs/exit-intent`, `/docs/api/campaigns`, …). `/docs` is an index of cards pointing at pages that do not exist. Auth is externalized to **`app.hustleconvert.com`** (not a local `/signup` route).

![Docs dead links](/blog/hustleconvert-com-eight-dollar-popup-lander-nxdomain/screenshots/docs-dead-links.png)

![Missing routes](/blog/hustleconvert-com-eight-dollar-popup-lander-nxdomain/screenshots/missing-routes.png)

Sibling `$8` landers in the same org: hustlemail-com (email), hustledesk-com (helpdesk), hustleforms-com (forms).

## Where

Code: [github.com/michaelmonetized/hustleconvert-com](https://github.com/michaelmonetized/hustleconvert-com), private.

Live probes at pack time:

- `hustleconvert-com.vercel.app` / `hustleconvert.vercel.app`: **404** `DEPLOYMENT_NOT_FOUND`
- `hustleconvert.com`: **NXDOMAIN** (no A/AAAA)
- `app.hustleconvert.com`: **NXDOMAIN**

Local inspect clone: `/tmp/cf-inspect/hustleconvert-com` @ `a5d3f13`.

## When

**2026-02-18 07:57 ET.** `691b4b4` feat: initial hustleconvert.com marketing site (+2526 / 24 files).

**2026-06-22 17:13 ET.** `e97591f` nightly (Fallow hooks, AGENTS.md, REVIEW.md, `.uncap`, dep bumps).

**2026-06-22 18:11 ET.** `a5d3f13` nightly empty tip (HEAD).

![Commit arc](/blog/hustleconvert-com-eight-dollar-popup-lander-nxdomain/screenshots/commit-arc.png)

## Why

A lander that prices against OptinMonster still needs a resolvable product surface before it is a product story. `app.` and `cdn.` strings in JSX are marketing copy, not a shipped runtime. NXDOMAIN is a cleaner failure mode than a parked WordPress front page; it is still not a launch.

**Engagement Q:** How many of your $8/mo SaaS repos are four marketing pages pointing at an NXDOMAIN `app.` subdomain?

---

# hustlechat.com: Intercom-alt lander that claims Convex with no Convex dep

Source: https://www.michaelchurley.com/blog/hustlechat-com-intercom-alt-convex-claims-parkweb  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: hustlechat-com, hustlechat, live-chat, intercom-alternative, convex, marketing-site, nextjs, tailwind, pricing, parkweb, godaddy, lander, michaelmonetized

![Home hero](/blog/hustlechat-com-intercom-alt-convex-claims-parkweb/screenshots/home-hero.png)

## Who

I keep private product shells. Some ship. Some land. Some claim Convex without importing it.

For operators comparing a **live-chat Intercom-alternative lander** to the **GoDaddy parkweb page** answering hustlechat.com.

## What

I built **hustlechat-com** — private michaelmonetized/hustlechat-com. HEAD `a570134`. 3 commits. package hustlechat.com@0.1.0. Stock README.
Stack: Next 16.2.6 + React 19.2.6 + Tailwind 4. Deps: next/react only — **no convex** despite copy claiming Convex real-time.
Pricing: Free $0 / Teams $8 / Premium $18 / Enterprise $28. Competitors: Intercom $74+ / Drift $2500+ / Crisp $25+.
Fake SDKs `@hustlechat/*` + cdn widget; Sign In buttons have no href; docs → app.hustlechat.com NXDOMAIN.
Brand `#6366f1`/`#f472b6`/`#22d3ee`. Routes: `/` `/pricing` `/docs`.

![Pricing](/blog/hustlechat-com-intercom-alt-convex-claims-parkweb/screenshots/pricing-flat.png)

![Competitors](/blog/hustlechat-com-intercom-alt-convex-claims-parkweb/screenshots/competitor-table.png)

![Fake SDK](/blog/hustlechat-com-intercom-alt-convex-claims-parkweb/screenshots/missing-routes.png)

Not hustlemail-com. Not hustledesk-com.

## Where

Code: github.com/michaelmonetized/hustlechat-com — private.
## When
2026-02-18 4db53a8 initial site. 2026-06-22 nightlies 9fec5f3 then HEAD a570134.
![Commit arc](/blog/hustlechat-com-intercom-alt-convex-claims-parkweb/screenshots/commit-arc.png)
## Why
Convex in the hero without convex in package.json. Fake SDKs. Parkweb domain. Not a live chat product.
**Engagement Q:** How many landers claim a backend they never added as a dependency?
- hustlechat.com JS redirect to /lander GoDaddy parkweb
- app + cdn NXDOMAIN; vercel DEPLOYMENT_NOT_FOUND

---

# buffer-cli: wp-to-buffer-pro parity in the terminal — OAuth, no scrape

Source: https://www.michaelchurley.com/blog/buffer-cli-wpzinc-parity-agent-oauth-no-scrape  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: buffer-cli, buffer, cli, typescript, bun, oauth, wpzinc, wp-to-buffer-pro, social-media, scheduler, agent, michaelmonetized

## Who

I wanted Buffer scheduling without WordPress and without cookie-scraping CLIs.

For people who already know WPZinc wp-to-buffer-pro and want the same OAuth + config shape outside PHP.

## What

I built **buffer-cli**, public https://github.com/michaelmonetized/buffer-cli. HEAD `778ecaf`. **3** commits. **0** stars. Default **main**. Version **0.1.0** (CHANGELOG still **[Unreleased]**). CLI only.

**Shipped (~1892 LOC):** auth, profiles, post, config, tags, info. Bun + commander.

![CLI post / profiles](/blog/buffer-cli-wpzinc-parity-agent-oauth-no-scrape/screenshots/cli-post-profiles.png)

![OAuth WPZinc flow](/blog/buffer-cli-wpzinc-parity-agent-oauth-no-scrape/screenshots/oauth-wpzinc-flow.png)

**Gateway:** Buffer to wpzinc OAuth to localhost:9876 to `~/.buffer-cli`. X-Forwarded-Host www.hustlelaunch.com.

**Parity:** types/config match wp-to-buffer-pro; template tags `{title}` `{url}` `{excerpt}`.

![Docs vs shipped](/blog/buffer-cli-wpzinc-parity-agent-oauth-no-scrape/screenshots/readme-parity-gap.png)

**Gaps:** no queue.ts; no `--from`; token refresh TODO; PLAN mostly unchecked; FALLOW postCommand CRAP 1190.

![Architecture](/blog/buffer-cli-wpzinc-parity-agent-oauth-no-scrape/screenshots/architecture-stack.png)

## Where

Code: https://github.com/michaelmonetized/buffer-cli, public. No live web app.

```bash
bun install && bun run src/index.ts
```

## When

**2026-02-16.** `9b53095` feat: initial implementation (+3097 LOC).

**2026-06-22.** `95fe1c3` nightly: fallow-gate + REVIEW.md.

**2026-06-22.** `778ecaf` nightly to HEAD.

![Commit arc](/blog/buffer-cli-wpzinc-parity-agent-oauth-no-scrape/screenshots/commit-arc.png)

## Why

Buffer's own CLI is gone, and scraping gets you banned. This keeps the WPZinc OAuth + config shape in Bun so agents can schedule without PHP or cookie jars.

**Engagement Q:** Cut README to match HEAD, or finish queue.ts first?

---

# hustlecrm.com: $8/user CRM lander vs legacy PHP login domain

Source: https://www.michaelchurley.com/blog/hustlecrm-com-eight-per-user-crm-lander-legacy-php-domain  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: hustlecrm-com, hustlecrm, crm, pipeline, kanban, hubspot-alternative, salesforce-alternative, marketing-site, nextjs, tailwind, pricing, php, lander, michaelmonetized

![Home hero](/blog/hustlecrm-com-eight-per-user-crm-lander-legacy-php-domain/screenshots/home-hero.png)

## Who

I keep a private GitHub org full of product shells. Some are real apps. Some are landers that talk like apps. Some claim a domain that answers something else entirely.

For operators who need the honest split between a **$8/user CRM marketing site** and the **legacy PHP Bootstrap login** currently living at hustlecrm.com.

## What

I built **hustlecrm-com**, private `https://github.com/michaelmonetized/hustlecrm-com`. Next.js marketing shell. HEAD `2c50bbb`. **3** commits. 0 stars. package name `hustlecrm.com@0.1.0`. README is **stock** create-next-app boilerplate.

Stack facts from `package.json`: Next **16.2.6**, React **19.2.6**, Tailwind **^4.3.0**, Bun lockfile. Dependencies stop there: no Clerk, no Convex, no Stripe package, no CRM/database library.

What the UI claims:

- Hero: "CRM for People Who **Hate CRMs**." Subhead close deals. Price line **Simple. Fast. $8/mo.**
- CTAs: **Start Free Trial** `href="#"`, **See Features** `/features`. Header **Sign in** `href="#"`.
- Home **KanbanDemo**: Lead, Contacted, Proposal, Negotiation, Won. Sarah Chen / Emma Wilson / David Kim deal cards with drag-drop. Chrome UI, not a product screenshot.
- Feature grid: Contact & Company Management, Deal Pipeline (Kanban), Activity Timeline, Custom Fields, Import & Export, Integrations (HustleChat / HustleForms).
- Competitor cards: HubSpot `$45+/mo`, Pipedrive `$14+/mo` vs HustleCRM **`$8/mo`**, **per user**, cancel anytime.
- Pricing: **Free Trial** $0 / 14 days + **Pro** **$8 /user/month**; table adds Salesforce `$25+/mo`.
- Brand blue: CSS `--primary = #2563eb` / `--primary-dark = #1d4ed8`.

![Kanban demo](/blog/hustlecrm-com-eight-per-user-crm-lander-legacy-php-domain/screenshots/kanban-demo.png)

![Pricing Pro $8/user](/blog/hustlecrm-com-eight-per-user-crm-lander-legacy-php-domain/screenshots/pricing-pro.png)

![Competitor table](/blog/hustlecrm-com-eight-per-user-crm-lander-legacy-php-domain/screenshots/competitor-table.png)

Real `page.tsx` routes: `/`, `/features`, `/pricing`, `/docs`.

Linked but **dead in-repo**: **25** `href="#"` hits across header, footer, home, pricing, features, docs (Sign in, trial, Changelog, Privacy, Terms, every docs child, API docs, support). No `/privacy` or `/terms` pages. Docs shows REST endpoint cards (`/api/v1/contacts`, `/api/v1/deals`, …) with no backend.

![Docs dead hash links](/blog/hustlecrm-com-eight-per-user-crm-lander-legacy-php-domain/screenshots/docs-dead-links.png)

![Missing routes + PHP domain](/blog/hustlecrm-com-eight-per-user-crm-lander-legacy-php-domain/screenshots/missing-routes.png)

Sibling landers stay separate stories: `hustledesk-com` is the **helpdesk** $8 **flat** lander (sky `#0ea5e9`, Zendesk comparison, auth to `app.*` to WordPress parking). `hustlemail-com` is the **email-marketing** $8 lander (red `#ef4444`, Free/$8/$24). Different product claim. Different brand. Different domain failure mode.

## Where

Code: [github.com/michaelmonetized/hustlecrm-com](https://github.com/michaelmonetized/hustlecrm-com), private.

Live probes at pack time:

- `hustlecrm.com` DNS A **162.144.3.43**; HTTPS **200** Apache + `PHPSESSID`; title **Hustle CRM!**; Bootstrap **5.3** form `POST scripts/login.php` returns **302** `index.php?error=no`; `assets/cover.jpg` Last-Modified **2023-04-17**. That host serves the legacy PHP login, while this Next CRM lander never shipped there.
- `www.hustlecrm.com` same A / same PHP login
- `app.hustlecrm.com` **NXDOMAIN**
- `hustlecrm-com.vercel.app` / `hustlecrm.vercel.app` **404** `DEPLOYMENT_NOT_FOUND`

Local inspect clone: `/tmp/cf-inspect/hustlecrm-com` @ `2c50bbb`.

## When

**2026-02-18 07:53 ET** `f4494f2` feat: initial hustlecrm.com marketing site (+2566 / 23 files).

**2026-06-22 17:12 ET** `208c547` nightly (Fallow hooks, AGENTS.md, REVIEW.md, `.uncap`, dep bumps).

**2026-06-22 18:11 ET** `2c50bbb` nightly empty tip (HEAD).

![Commit arc](/blog/hustlecrm-com-eight-per-user-crm-lander-legacy-php-domain/screenshots/commit-arc.png)

## Why

A HubSpot-price lander still needs an auth surface that is more than `href="#"` while the public domain serves a 2023 PHP login. "$8 per user, all features included" on a four-page private repo is table copy, not a billed product. hustledesk-com already told the $8 lander story for **helpdesk + WordPress parking**. This pack is the **CRM** twin with **per-user** pricing and a **legacy PHP** domain.

**Engagement Q:** How many of your SaaS domains currently serve a Bootstrap PHP login from 2023 while the Next marketing shell never shipped and every "Start Free Trial" button is a hash?

---

# animated-gradient-border: transparent glass with a spinning conic ring

Source: https://www.michaelchurley.com/blog/animated-gradient-border-transparent-mask-composite  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: animated-gradient-border, css, glassmorphism, conic-gradient, mask-composite, css-property, transparent-border, hustlelaunch, static-html, michaelmonetized

![Native demo frame](/blog/animated-gradient-border-transparent-mask-composite/screenshots/hero-native.png)

## Who

I wanted the usual glass card with a rotating gradient border, and I wanted to see the video plate through the middle of the ring. A filled conic disk sitting on top of the glass was the wrong shape for the lab.

For front-end operators who already know `conic-gradient` and still lose an afternoon when the border paints the whole card. Especially anyone comparing this lab to my Next.js glass-design-system `AnimatedBorder` and needing the bare CSS cut.

## What

I built **animated-gradient-border-on-transparent-background**, public [`michaelmonetized/animated-gradient-border-on-transparent-background`](https://github.com/michaelmonetized/animated-gradient-border-on-transparent-background). HEAD `5e9b611`. **5** commits. **0** stars. Language **CSS**. License **GPL-3**. README is **empty** (0 bytes). No version tag. No GitHub Pages. No homepage.

![Card anatomy](/blog/animated-gradient-border-transparent-mask-composite/screenshots/glass-card-anatomy.png)

Product is a static lab:

- `index.html`, 31 lines. Title: Glass Animated Gradient Border. Card classes: `glass rounded border-gradient border-glow animate-rotate-angle`.
- `style.css`, 448 lines (~8.9KB). The technique lives here.
- Plate: `#hero` + `.video-bg` pointing at a remote HustleLaunch webm (`attraction-silent-backdrop.webm`). Repo also carries local `video.webm` (~47MB) and `bg.png`.
- Toggle: fixed darklight checkbox flips ☀️/🌙 via `--scheme` and `:has()`.

The border trick:

1. `::after` paints a full-box `conic-gradient(from var(--conic-gradient-angle), …)`.
2. Dual masks, outer `linear-gradient(#fff 0 0)` plus inner `content-box`, with `padding: var(--border-width)`.
3. `mask-composite: exclude` / `-webkit-mask-composite: xor` keeps **only the ring**.

![mask-composite exclude](/blog/animated-gradient-border-transparent-mask-composite/screenshots/mask-xor-diagram.png)

Spin comes from CSS `@property --conic-gradient-angle` (syntax ``) and `@keyframes background-spin` 0deg to 360deg, applied to both `::before` (glow) and `::after` (ring) at 3s linear infinite. Glow uses a larger inset, `filter: blur`, and `--intensity-glow` (0.4 light / 0.2 dark).

Same file also ships experimental CSS `@function --transparency` / `--light-dark` wrapping `color-mix` and scheme branching.

![ @property spin + layers ](/blog/animated-gradient-border-transparent-mask-composite/screenshots/css-property-spin.png)

**Sibling fence:** [glass-design-system](https://github.com/michaelmonetized/glass-design-system) reuses the `@property` + mask border idea as `AnimatedBorder` inside a Next.js 16 Apple-SVG glass showcase. This repo is the bare static HTML/CSS lab. Separate from that product spine and from twelveux WebGL glass.

![Sibling vs glass-design-system](/blog/animated-gradient-border-transparent-mask-composite/screenshots/sibling-vs-glass-design.png)

Residue I am not papering over:

- Empty README.
- Native screenshot from 2026-01-29 still shows "Fork on GitHub" / "Download" and a "How it works?" skip link that current `index.html` no longer has.
- Two empty commits: second `simplified` shares tree with first; second `nightly` shares tree with first.
- `video.webm` in-tree while the page loads the remote URL.

## Where

Code: [github.com/michaelmonetized/animated-gradient-border-on-transparent-background](https://github.com/michaelmonetized/animated-gradient-border-on-transparent-background), public. Open `index.html` locally (needs network for the remote webm plate).

```bash
git clone git@github.com:michaelmonetized/animated-gradient-border-on-transparent-background.git
open index.html   # or any static server
```

## When

**2024-08-24.** `8eeb210` mkproject scaffold (editorconfig, prettier, GPL-3 LICENSE, empty README).

**2026-01-30.** `95a48ad` / `de23de9` "simplified": demo HTML/CSS + screenshot + bg + video; second commit empty.

**2026-06-22.** `d087699` / `5e9b611` nightly: `.hustlemc` + `.uncap`; second nightly empty. HEAD.

Repo GitHub `created_at` is 2026-01-30; the mkproject commit timestamp is 2024-08-24 (history carried in).

![Commit arc](/blog/animated-gradient-border-transparent-mask-composite/screenshots/commit-arc.png)

## Why

A gradient border that fills the card is a different product than a ring you can see through. `mask-composite: exclude` is the sentence that makes the middle transparent. The static lab should stay distinct from the Next.js glass design system that absorbed the same border pattern.

**Engagement Q:** Fill the empty README with the mask-xor recipe and retire the stale Fork/Download screenshot, or leave the repo as a raw lab and point operators at glass-design-system for the polished story?

---

# hustleforms.com: $8/mo CRM form lander, Typeform table, NXDOMAIN

Source: https://www.michaelchurley.com/blog/hustleforms-com-eight-dollar-crm-form-lander-nxdomain  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: hustleforms-com, hustleforms, forms, form-builder, crm, typeform, jotform, wufoo, marketing-site, nextjs, tailwind, pricing, lander, michaelmonetized

![Home hero](/blog/hustleforms-com-eight-dollar-crm-form-lander-nxdomain/screenshots/home-hero.png)

## Who

I keep a private GitHub org full of product shells. Some are real apps. Some are landers that talk like apps.

For operators who need the honest split between an **$8/mo form-builder marketing site** and a claimed CRM/embed product that is not in this repo. Same org also holds popup, email, and helpdesk `$8` landers; this one is the forms lane.

## What

I built **hustleforms-com**, private https://github.com/michaelmonetized/hustleforms-com. Next.js marketing shell. HEAD `80c54f4`. **3** commits. 0 stars. package name `hustleforms.com@0.1.0`. README is stock create-next-app.

Stack facts from `package.json`: Next **16.2.6**, React **19.2.6**, Tailwind **^4.3.0**, Bun lockfile. Dependencies stop there: no Clerk, no Convex, no Stripe package, no form runtime SDK, no HubSpot/Salesforce client.

What the UI claims:

- Hero: Forms that feed **your CRM**. Badge: **Now with AI-powered form suggestions.** CTAs to `/signup` and `/templates`.
- Strip: **No credit card required · Free plan available**.
- Features: drag-and-drop builder, conditional logic, file uploads, embed anywhere, webhooks, **Direct CRM Sync** (HubSpot / Salesforce / Pipedrive).
- Home competitor cards: Typeform `$29+`, Jotform `$34+`, Wufoo `$19+`, HustleForms **`$8`** BEST VALUE.
- Pricing table: Unlimited forms / 10,000 submissions / CRM / remove branding at **$8/mo** vs Typeform `$29` / Jotform `$34`.
- Pricing plans: **Free** (3 forms / 100 submissions), **Pro $8/mo**, **Business $24/mo** (SSO/SAML, API, custom domains). Annual FAQ: **20% off** = `$6.40` / `$19.20`.
- FAQ text claims HubSpot/Salesforce/Pipedrive/Zoho + webhooks + 14-day trial; still no CRM or Stripe packages.
- Brand: Tailwind `brand.500 = #0ea5e9`, `accent.500 = #8b5cf6` (same sky+violet tokens as hustleconvert-com).
- Docs embed snippet: ``.

![Pricing plans](/blog/hustleforms-com-eight-dollar-crm-form-lander-nxdomain/screenshots/pricing-plans.png)

![Competitor table](/blog/hustleforms-com-eight-dollar-crm-form-lander-nxdomain/screenshots/competitor-table.png)

Real `page.tsx` routes: `/`, `/pricing`, `/templates`, `/docs`.

Linked but **missing**: `/signup`, `/login`, `/contact`, and footer `#` stubs (Integrations, Blog, Changelog, About, Privacy, Terms, API Reference). `/docs` is a single page of **hash-anchor** cards (not separate child routes) plus resource links that are literally `href="#"`. Auth is a local route that was never added; `app.hustleforms.com` is also NXDOMAIN.

![Docs hash anchors](/blog/hustleforms-com-eight-dollar-crm-form-lander-nxdomain/screenshots/docs-hash-anchors.png)

![Missing routes](/blog/hustleforms-com-eight-dollar-crm-form-lander-nxdomain/screenshots/missing-routes.png)

Sibling `$8` landers in the same org: hustleconvert-com (popups), hustlemail-com (email), hustledesk-com (helpdesk).

## Where

Code: [github.com/michaelmonetized/hustleforms-com](https://github.com/michaelmonetized/hustleforms-com), private.

Live probes at pack time:

- `hustleforms-com.vercel.app` / `hustleforms.vercel.app`: **404** `DEPLOYMENT_NOT_FOUND`
- `hustleforms.com`: **NXDOMAIN** (no A/AAAA)
- `www.hustleforms.com` / `app.hustleforms.com` / `cdn.hustleforms.com`: **NXDOMAIN**

Local inspect clone: `/tmp/cf-inspect/hustleforms-com` @ `80c54f4`.

## When

**2026-02-18 07:53 ET.** `a516ca1` feat: initial hustleforms.com marketing site (+2486 / 24 files).

**2026-06-22 17:11 ET.** `7b5aeb9` nightly (Fallow hooks, AGENTS.md, REVIEW.md, `.uncap`, dep bumps).

**2026-06-22 18:10 ET.** `80c54f4` nightly empty tip (HEAD).

![Commit arc](/blog/hustleforms-com-eight-dollar-crm-form-lander-nxdomain/screenshots/commit-arc.png)

## Why

A lander that prices against Typeform still needs a resolvable product surface before it is a product story. HubSpot/Salesforce strings in JSX and an embed script on an NXDOMAIN apex are marketing copy, not a shipped runtime. Relative `/signup` with no `page.tsx` is a quieter failure mode than an `app.` subdomain; it is still not a launch.

**Engagement Q:** How many of your forms-that-feed-your-CRM repos are four marketing pages where Start Free points at a route that does not exist?

---

# mkproject: bash scaffold that ships .env-safe git init + RUNAFTER

Source: https://www.michaelchurley.com/blog/mkproject-bash-scaffold-template-git-init-runafter  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: mkproject, bash, cli, scaffold, git-init, template, dotenv, dev-tooling, project-bootstrap, michaelmonetized

![Scaffold flow](/blog/mkproject-bash-scaffold-template-git-init-runafter/screenshots/scaffold-flow.png)

## Who

I kept hand-rolling new folders and occasionally pushing a dotenv. The fix is a small bash scaffolder with .env in the template ignore list.

## What

**mkproject** — public michaelmonetized/mkproject. HEAD `321012c`. 9 commits. 76-line `mkproject.sh`. User template under ~/.config/mkproject. Dotenv knobs: PROJECT_DIR, BRANCH, COMMIT_MESSAGE, RUNAFTER, DISABLE_GIT. Default RUNAFTER is `code .`.

![Config env](/blog/mkproject-bash-scaffold-template-git-init-runafter/screenshots/config-env.png)

![Template tree](/blog/mkproject-bash-scaffold-template-git-init-runafter/screenshots/template-tree.png)

Template now ships package.json + index.js; README file list still says six hygiene files. Root MIT vs template GPL-3. No releases despite README v0.1.0 zip.

![License mismatch](/blog/mkproject-bash-scaffold-template-git-init-runafter/screenshots/license-mismatch.png)

Empty scaffolds mkproject-1 / 49 are SKIP — outputs of this tool, not products.

## Where

https://github.com/michaelmonetized/mkproject — public CLI. No site.

## When

Aug 2024 birth. Jan 2026 sync. Mar 2026 Node stub in template. Jun 2026 PLAN/uncap + empty HEAD tip.

![Commit arc](/blog/mkproject-bash-scaffold-template-git-init-runafter/screenshots/commit-arc.png)

## Why

The factory story belongs on the scaffolder, not the empty children. Dotenv-safe init is the boring win.

**Engagement Q:** Align template LICENSE with MIT, or document the GPL default as intentional?

---

# new-design-gallery: I shipped a catalog UX, not another tabbed lander kit

Source: https://www.michaelchurley.com/blog/new-design-gallery-embla-catalog-not-landers  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: new-design-gallery, design-gallery, nextjs, embla, tailwind, catppuccin, vitest, vercel, portfolio, martech, catalog

![Design Gallery home with Embla strip and category filters](/blog/new-design-gallery-embla-catalog-not-landers/screenshots/gallery-home.png)

## Who

I needed a catalog — filter, scroll, lightbox, share a slug — not another tabbed full-lander kit.

Frontenders on Next 16 + Embla. Operators comparing this private catalog to public mockup-gallery. Not twelveux. Not the WebGL playground.

## What

Package `new-design-gallery` **0.1.0**. Next 16. React 19. Tailwind 4.

`DesignGallery`: Embla carousel + category filters + Radix lightbox + Phosphor share (`navigator.share` → clipboard). Eleven metadata cards in `designs.ts` — hospitality, contractors (incl. King's Roofing NC), professionals, SaaS. All thumbs `placeholder.co/800x600`. Detail routes via `generateStaticParams`. Golden-ratio + Catppuccin CSS. Vitest **39** cases; length assert still says **10** after the 11th card.

![Embla carousel strip](/blog/new-design-gallery-embla-catalog-not-landers/screenshots/embla-carousel.png)

## Where

Live: https://new-design-gallery.vercel.app — title Design Gallery.

Repo: https://github.com/michaelmonetized/new-design-gallery (private).

Sibling contrast: HurleyUS/mockup-gallery · mockup-gallery-nu.vercel.app (ten full landers / tabs).

![Contractors filter](/blog/new-design-gallery-embla-catalog-not-landers/screenshots/contractors-filter.png)

## When

**2026-03-28, 10:52 PM ET.** `a968bd6` — carousel, filters, lightbox, share, 39 tests.

**2026-06-22, 4:55 PM ET.** `9558f07` — AGENTS/uncap/CSS + King's Roofing NC.

**2026-06-22, 5:57 PM ET.** HEAD `95553df` — empty nightly. Three commits. Still 0.1.0.

## Why

Catalog UX vs lander kit. Same March 28 era, different product. Placeholder thumbs until real art. Test drift is the scar.

What belongs on card eleven — another contractor, or a real screenshot?

![Detail Mountain Lodge](/blog/new-design-gallery-embla-catalog-not-landers/screenshots/detail-mountain-lodge.png)

---

# WhisperCPPonEverything: Ctrl+W streaming STT menubar, cyan glow, whisper-stream

Source: https://www.michaelchurley.com/blog/whispercpponeverything-ctrl-w-streaming-stt-cyan-glow  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: whispercpponeverything, whisper.cpp, whisper-stream, speech-to-text, stt, dictation, menubar, macos, swift, swift-package, cgevent, accessibility, local-ai, michaelmonetized

![OG / menubar streaming](/blog/whispercpponeverything-ctrl-w-streaming-stt-cyan-glow/screenshots/menubar-streaming.png)

## Who

I build local tooling when cloud dictation is the wrong trust boundary.

For operators who want **Ctrl+W anywhere** to type into the focused field with **whisper.cpp on the machine**, and who need the honest split between the **streaming HEAD** and the **batch/orange README** still sitting in the same private repo.

## What

I built **WhisperCPPonEverything**, private [`michaelmonetized/WhisperCPPonEverything`](https://github.com/michaelmonetized/WhisperCPPonEverything). Native macOS **menubar** (`LSUIElement`) Swift Package executable. HEAD `d33e5e2`. **11** commits. 0 stars. Bundle `com.whispercpponeverything.app` **2.0.0**. Renamed from **VoiceType**.

Stack facts from `Package.swift` + sources:

- Platforms: **macOS 14+**
- Frameworks only: AppKit, CoreGraphics, ApplicationServices. **Zero SPM dependencies.**
- Hotkey: **Ctrl+W** via `CGEvent` tap (active session, then listenOnly, then HID listenOnly fallback)
- macOS **15+**: `CGPreflightListenEventAccess` / `CGPreflightPostEventAccess` + request APIs before tap create
- Transcription: subprocess `/opt/homebrew/bin/whisper-stream` with model `/opt/homebrew/share/whisper-cpp/models/ggml-medium.bin`, args `-l en --step 3000 --length 5000 --keep 200`
- Incremental inject: strip ANSI + `[timestamp]` prefixes, **suffix-prefix overlap dedupe**, then `TextInjector` Unicode `CGEvent` keystrokes (5ms between chars)
- Visual: fullscreen `.screenSaver` overlay, **cyan** 20-layer soft glow, 1.5Hz pulse, `ignoresMouseEvents`
- State machine: **`idle` / `streaming` only** (`State.swift`)
- Logging: `~/Library/Logs/WhisperCPPonEverything.log`
- Checked-in `WhisperCPPonEverything.app` with **arm64 Mach-O** binary

![Pipeline](/blog/whispercpponeverything-ctrl-w-streaming-stt-cyan-glow/screenshots/pipeline.png)

![Cyan glow](/blog/whispercpponeverything-ctrl-w-streaming-stt-cyan-glow/screenshots/cyan-glow.png)

What the README still claims (stale vs HEAD):

- Orange screen glow while listening
- **Silence detection** ends recording after 5 seconds
- Batch-style speak, stop, then transcribed text is typed

What CLAUDE.md + Swift say at HEAD: cyan soft glow, toggle stop, **streaming** text as whisper-stream prints lines. PLAN.md still names `AudioRecorder.swift` / `Transcriber.swift`. Those files are not in the tree. TODO.md phases are all unchecked. StatusItem streaming mic is still **`.systemOrange`** next to a cyan overlay.

![Stale docs gap](/blog/whispercpponeverything-ctrl-w-streaming-stt-cyan-glow/screenshots/stale-docs-gap.png)

![Permissions triad](/blog/whispercpponeverything-ctrl-w-streaming-stt-cyan-glow/screenshots/permissions-triad.png)

Distinct from `niri-macos` (Swift AX tiling) and from SaaS dictation landers. Local Homebrew whisper-stream or the app alerts that whisper-cpp is missing.

## Where

Code: [github.com/michaelmonetized/WhisperCPPonEverything](https://github.com/michaelmonetized/WhisperCPPonEverything), private.

Runtime probes at pack time: no public web product. Install path documented as `swift build -c release` then copy into `/Applications/WhisperCPPonEverything.app/Contents/MacOS/` + ad-hoc `codesign`. CLAUDE note: **each new binary invalidates TCC**. Remove and re-add Accessibility.

Required on disk: `whisper-stream` + `ggml-medium.bin` under Homebrew paths above.

## When

- **2026-02-15.** PLAN/README/TODO, rename, sources build, VoiceType rename, whisper-cli path fixes, app bundle, **cyan instead of orange**.
- **2026-02-16.** `92bb411` streaming STT + soft glow + macOS 15+ permission support.
- **2026-06-22.** Two `nightly` commits; HEAD `d33e5e2` (also last GitHub push).

![Commit arc](/blog/whispercpponeverything-ctrl-w-streaming-stt-cyan-glow/screenshots/commit-arc.png)

## Why

I wanted dictation that stays on-box and types into whatever already has focus (Slack, terminal, browser) without a cloud STT round-trip. Ctrl+W was the whole UX. Streaming beat record-a-WAV-then-wait. Cyan beat ugly orange. Sequoia event-access APIs were the tax for keeping the tap alive.

**Engagement Q:** How many private STT menubar apps still advertise orange silence-batch in README while HEAD streams cyan into the focused field?

Draft + assets only until Michael publishes.

---

# neovim-ide: the GIGACHAD of NvChad is a tmux layout (ollama ASCII, cursor-agent reality)

Source: https://www.michaelchurley.com/blog/neovim-ide-tmux-gigachad-layout-cursor-agent  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: neovim-ide, neovim, nvim, tmux, nvchad, zsh, cursor-agent, lazygit, ink, bun, ide-layout, michaelmonetized

![tmux IDE grid: Agent | Neovim | Tasks/Git + Console/Terminal](/blog/neovim-ide-tmux-gigachad-layout-cursor-agent/screenshots/layout-ascii.png)

## Who

I wanted a **project IDE** without buying another Electron shell: open a folder, get Neovim in the middle, and already have an agent, Taskwarrior, LazyGit, and spare shells in panes around it.

If you live in tmux + NvChad, you already know the ritual: split, title, send-keys, forget which pane had lazygit. I wanted that ritual to be one command.

This is the **outer** layout story. The sibling **nvibe** pack is the **inner** Neovim one (Lua + `nvchad.term`). Different repos. Different layers. Same operator itch.

## What

I keep [michaelmonetized/neovim-ide](https://github.com/michaelmonetized/neovim-ide) public. GitHub linguist says JavaScript. The product that actually lays out panes is **~7.5 KB of zsh** under `src/`.

`src/neovim-ide.sh` is the entry: sanitize a session name from argv, basename, or `.neovim` JSON `.name` via `jq`; offer attach/rename if the session exists; `tmux new -s` detached; `cd` into the project; then fire `~/.local/bin/nvide-layout` and `~/.local/bin/nvide-init`.

The banner at the top of that file still draws the intended grid:

```
- ollama -|- nvim ---------------------|- Tasks -
- ChtSh - |                            |- Git ---
          |- Console ----|- Terminal - |
```

`src/neovim-ide-layout.sh` splits and titles panes: Agent, Cheatsheet, Neovim, Tasks, Git, Console, Terminal. It aliases `tmux` to **`/opt/homebrew/bin/tmux`** (Homebrew-shaped, not portable).

`src/neovim-ide-init.sh` is where the honesty check bites: it `send-keys` **`cursor-agent`** into the Agent pane (banner still says ollama), starts `nvim` on pane 2, `task list && tasksh` on Tasks, `lazygit` on Git when `.git` exists, then sleeps and punches `:Minimap` + `C-n` for nvim-tree.

![MIT vs GPL-3.0 vs ISC](/blog/neovim-ide-tmux-gigachad-layout-cursor-agent/screenshots/license-gap.png)

License stack at HEAD `38ed773`:

- README footer + script header: **MIT**
- `LICENSE.md`: full **GNU GPL v3** text (GitHub `licenseInfo` agrees)
- `ts/package.json`: **`"license": "ISC"`**, version **1.0.0**

`install.sh` is **0 bytes**. README still says it moves `src/` to `~/bin/`, config to `~/.config/neovim-ide/`, and links `nvide-*` into `~/.local/bin`. Without that, the entry script's `nvide-layout` / `nvide-init` calls are wishful.

TypeScript side: `ts/cli.tsx` is an Ink path wizard (123 lines). It can `mkdir` or `execa('mkproject', …)`. It never calls `tmux`. On success it prints Project path ready and exits. `ts/neovim-ide.mjs` is a **1.7MB** React/Ink-style bundle. `ts/REVIEW.md` is a FALLOW dump over that bundle (Total LOC 39772, Dead Files 100%, CRAP scores in the tens of thousands on React internals). About **4095** tracked paths live under `ts/node_modules/`; only **29** tracked files sit outside it.

`PLAN.md` (Jan 2026) lists Critical Core IDE Features (LSP, nvim-cmp, Treesitter, neo-tree, Telescope) as if this repo were a Neovim config. It is not. `sitrep.md` says Status **SHIPPED**, stack Shell, Lua, last commit **2026-01-31**, Fully functional. HEAD is a June 22 nightly.

![docs vs shipped: PLAN / install / CLI](/blog/neovim-ide-tmux-gigachad-layout-cursor-agent/screenshots/docs-vs-shipped.png)

`src/neovim-ide-tmux.sh` is the fzf project picker over `$HOME/Projects/` with a shebang typo: `#1/usr/bin/env zsh`. Config default: `ide_project_path="$HOME/Projects/"`.

## Where

Code: [github.com/michaelmonetized/neovim-ide](https://github.com/michaelmonetized/neovim-ide), **public**. Stars: 0. No homepage URL. No hosted demo.

Runtime expectations from the tree/header: tmux, jq, mkproject, lazygit, ollama (still listed), tasksh, Neovim 0.9+ / NvChad, optional minimap + nvim-tree. Bun only if you insist on the Ink CLI.

Screenshots section in README is still a TODO stub.

## When

- **2024-08-25–28.** `2b79f59` mkproject scaffold, then `57e0164` initial commit, then `.neovim` session-name support (`f159d67`, retrievin), then `5290d69` tmux support. Four commits; the shell layout product lands.
- **2026-01-08.** `d81f21b` PLAN.md improvement opportunities (LSP wishlist).
- **2026-01-31.** `52f0e4a` chore sync (sitrep still points here).
- **2026-02-27.** `915f5d6` README install instructions + structure (still describing empty `install.sh`).
- **2026-06-22.** `2976b7e` / HEAD `38ed773` nightlies. Last push `2026-06-22T21:56:52Z`. **9** commits total.

![9-commit arc](/blog/neovim-ide-tmux-gigachad-layout-cursor-agent/screenshots/commit-arc.png)

## Why

My IDE was already panes I pay for: tmux, nvim, lazygit, an agent CLI. I wanted one command that refuses to start a project session half-empty. Keep the diary honest: the ASCII still says ollama while init starts cursor-agent; the license badges disagree; the installer file is empty; the Ink CLI is a path form, not a launcher; PLAN.md dreams of an LSP IDE this repo is not. Leave those in the draft next to the GIGACHAD one-liner.

**Engagement Q:** Would you trust a GIGACHAD of NvChad launcher whose banner still says **ollama**, whose init starts **cursor-agent**, whose README says **MIT** while `LICENSE.md` is **GPL-3.0**, and do you want that layout **outside** Neovim in tmux, or **inside** via something like nvibe?

---

# devhost: Ink TUI + Caddy :80 multi-project .localhost manager

Source: https://www.michaelchurley.com/blog/devhost-ink-tui-caddy-port80-multihost  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: devhost, caddy, ink, tui, bun, localhost, local-dev, reverse-proxy, hosts-file, cli, react, michaelmonetized

![DevHost Manager TUI](/blog/devhost-ink-tui-caddy-port80-multihost/screenshots/tui-manager.png)

## Who

I run a pile of Next/Bun apps on one Mac and got tired of remembering which project owns which port — and of typing `localhost:3xxx` when the hostname should just be the project name.

## What

Private **michaelmonetized/devhost** · v1.0.0 · HEAD `db69995` · **1** commit (+1426 / 14 files). Bun CLI + Ink/React TUI that:

1. `devhost add` — reads `package.json` name, requires `scripts.dev`, assigns next port from **3001**, registers `.localhost`
2. Writes `~/.config/devhost/config.json` + auto Caddyfile + marker-bounded `/etc/hosts` block
3. `devhost start` — ensures Caddy, spawns `bun run dev --port N` (env `PORT`/`HOSTNAME`), tracks PID, opens `http://.localhost`
4. Bare `devhost` — Ink manager: start/stop/open + Caddy toggle

![Architecture](/blog/devhost-ink-tui-caddy-port80-multihost/screenshots/architecture.png)

![Hosts + Caddyfile](/blog/devhost-ink-tui-caddy-port80-multihost/screenshots/hosts-caddyfile.png)

## Where

github.com/michaelmonetized/devhost (**private**). No product domain. Config lives at `~/.config/devhost/`. On pack machine m1Pro13 the live config still has **bestwnc-com.localhost → :3001**. Sibling story: **bundx-init** patches Next repos for **HTTPS** `.localhost` on **:443**; devhost is the **HTTP :80 multi-host lifecycle TUI**.

![Sibling vs bundx-init](/blog/devhost-ink-tui-caddy-port80-multihost/screenshots/sibling-vs-bundx-init.png)

## When

**2026-02-15** — sole commit `db69995` Initial commit (Caddy + Ink TUI + CLI + hosts + ports). Queue `pushed_at` 2026-06-22 (nightly/metadata push; tree still one commit). No follow-up features in git.

![Commit arc](/blog/devhost-ink-tui-caddy-port80-multihost/screenshots/commit-arc.png)

## Why

Port roulette does not scale past three apps. A marker-bounded hosts block + one Caddyfile + a TUI that knows PIDs is the boring operator loop I actually run. HTTP on :80 is intentional — this is not the Clerk-friendly HTTPS path (that's bundx-init).

**Engagement Q:** Do you want a TUI that owns `/etc/hosts` + Caddy :80 — or a one-shot installer that patches each Next repo for HTTPS `.localhost`?

---

# shagent: Bun MCP + OpenRouter free loop after I deleted the Zsh shell harness

Source: https://www.michaelchurley.com/blog/shagent-bun-mcp-openrouter-after-zsh-harness-delete  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: shagent, bun, typescript, mcp, model-context-protocol, openrouter, cli, agent, zsh, filesystem-mcp, chrome-devtools-mcp, michaelmonetized

![OG / zsh to bun](/blog/shagent-bun-mcp-openrouter-after-zsh-harness-delete/screenshots/zsh-to-bun.png)

## Who

I wanted a shell-native agent that already knew my Zsh profile. Then I tore that harness out and put a Bun MCP controller in its place.

For operators who care about the honest split between a **deleted Zsh THOUGHT/COMMAND loop** and the **HEAD Bun + OpenRouter + filesystem MCP** stack under the same public repo name.

## What

I built **shagent**, public https://github.com/michaelmonetized/shagent. HEAD `61ecd7d`. **3** commits. **0** stars. Default **main**. `package.json` **1.0.0**. README is **0 bytes**.

**At HEAD (Bun / TypeScript):**

- Entry `./shagent` (`#!/usr/bin/env bun`): requires `OPENROUTER_API_KEY`; model `SHAGENT_MODEL` or **`openrouter/free`**
- `Shagent` in `src/controller/shagent.ts` (~79 LOC): MCP `Client` + `StdioClientTransport` running `npx -y @modelcontextprotocol/server-filesystem` on `process.cwd()`
- OpenRouter `https://openrouter.ai/api/v1/chat/completions` loop, **max 15** turns
- System prompt forces JSON: `{"action":"tool_name","args":{...}}` or `{"message":"..."}`; regex scrape `/{.*?}/gs`; `client.callTool`
- Sibling probe `src/controller/index.ts` lists tools from `npx -y chrome-devtools-mcp`; that probe is **not** what the CLI entry wires

![Agent loop](/blog/shagent-bun-mcp-openrouter-after-zsh-harness-delete/screenshots/agent-loop.png)

**Deleted on the Jun 22 rewrite (`2ecb32f`):** `src/shagent.sh`, **258** lines of Zsh.

What that harness did (still recoverable from `0729083`):

- Source `~/.zshenv` / `.zprofile` / `.zshrc` + optional `.env`
- Session `-s/--session` (default `default`)
- Deps: `rg fd eza bat git jq curl … qmd mcp-cli`
- Response contract: `THOUGHT:` / `COMMAND:` then `eval`
- Optional async `qmd update` / `qmd embed`
- Same OpenRouter free default

![Deleted Zsh harness](/blog/shagent-bun-mcp-openrouter-after-zsh-harness-delete/screenshots/zsh-harness.png)

**Fiction still in-tree:** `index.html` sells Launch, scale, and monitor autonomous AI agents with CTA to `github.com/shagent/getting-started` and footer **Rusty P. Shackelford**. Fiction only; no deploy; unwired to `./shagent`.

![Fiction lander](/blog/shagent-bun-mcp-openrouter-after-zsh-harness-delete/screenshots/fiction-lander.png)

**Residue:** `stdout-test.txt` (~116KB) is a shell-era OpenRouter request dump that still narrates Zsh + mcp-cli + qmd. Second nightly `61ecd7d` is an **empty** commit (message only).

Sibling surfaces to keep straight: `orclawstrator` (OpenClaw Swift/Go command center), `mission-control` / `hurley-mission-control`. This public repo is local Bun + npx MCP or it does not run.

## Where

Code: [github.com/michaelmonetized/shagent](https://github.com/michaelmonetized/shagent), public.

Run (facts from entry): set `OPENROUTER_API_KEY`, optional `SHAGENT_MODEL`, then `./shagent 'task description'` with Bun available. `bun.lock` present; no scripts block in `package.json`.

No live product URL. No claimed domain.

## When

- **2026-05-26.** `0729083` init: empty README + Zsh harness
- **2026-06-22 16:50 ET.** `2ecb32f` nightly: delete `src/shagent.sh`; add Bun controller, fiction HTML, stdout dump, uncap
- **2026-06-22 17:56 ET.** `61ecd7d` nightly: empty tree diff; HEAD
- Pack prepared **2026-09-08 ~4:55 PM ET** (draft + assets only)

## Why

I needed a minimal OpenRouter agent that could call tools. The Zsh version lived inside my shell profile. The Bun version talks MCP over stdio to a filesystem server and keeps the model choice on OpenRouter free by default. The marketing HTML and the empty README are the honesty tax sitting next to that loop.

**Engagement:** how many public agent repos still ship a fiction lander and a 0-byte README after deleting the shell harness that `stdout-test.txt` still describes?

---

# nfglyph: Bun raw-ANSI Nerd Font picker — fuzzy names, Linux quit fixed

Source: https://www.michaelchurley.com/blog/nfglyph-bun-ansi-nerd-font-picker-linux-quit  
Published: 2026-06-03  
Author: Michael C. Hurley  
Tags: nfglyph, nerd-fonts, glyph-picker, tui, bun, typescript, ansi, vim, clipboard, linux, fuzzy-search, cli, michaelmonetized

## Who

I wanted a glyph picker that stays in the TTY — vim motions, alt-buffer, Enter copies the character — without dragging in Ink, Blessed, or Bubble Tea.

For people who already installed a Nerd Font and are tired of opening a browser just to steal a powerline arrow.


## What

I built **nfglyph** — public https://github.com/michaelmonetized/nfglyph. HEAD `1dfdc82`. **4** commits. **0** stars. Default **main**. Version **Unreleased** (sitrep still says **SHIPPED**). CLI/TTY only. Single file `nfglyph` (~431 LOC) on Bun. **No** package.json.

**v1 (`0c91b0d`, Jan 30):** walk hard-coded Unicode ranges (emojis + Nerd PUA), filter by **hex** only, copy via **pbcopy**, shebang pinned to `/Users/michael/.bun/bin/bun`, cleanup called `process.exit(0)`.

![Repo screenshot](/blog/nfglyph-bun-ansi-nerd-font-picker-linux-quit/screenshots/repo-screenshot.png)

**Upgrade (`047b54c`, Jun 1):** commit `.nfglyph-data/glyphs.json` from ryanoasis **glyphnames** — METADATA **3.4.0** / **50,174** named glyphs. `loadGlyphs()` + `fuzzyMatch` on `g.name` (hex path still works). Footer shows `name` + `U+XXXX`. `y` yanks the hex code. Shebang -> `#!/usr/bin/env bun`.

![glyphs.json pipeline](/blog/nfglyph-bun-ansi-nerd-font-picker-linux-quit/screenshots/glyphs-json-pipeline.png)


**Linux quit (`1dfdc82`, Jun 3):** cleanup resolves a `keepAlive` promise instead of `process.exit`; detaches stdin listeners and `pause()`s; quit matches `input.includes("q")`; `write` uses `process.stdout.write`; clipboard tries `pbcopy` -> `wl-copy` -> `xclip` -> `xsel`.

![Linux quit fix](/blog/nfglyph-bun-ansi-nerd-font-picker-linux-quit/screenshots/linux-quit-fix.png)

Residue: README still markets **15k+** and **hex** search; PLAN Phase 1 still has **Fuzzy name search** unchecked; error string still curls into `~/.nfglyph-data` while code loads next to `argv[1]`; `.uncap` `headSha` empty; sitrep last-commit date stuck on Jan 30.

![README / PLAN gap](/blog/nfglyph-bun-ansi-nerd-font-picker-linux-quit/screenshots/readme-plan-gap.png)

## Where

Code: [github.com/michaelmonetized/nfglyph](https://github.com/michaelmonetized/nfglyph) — public. No live web app.

```bash
git clone https://github.com/michaelmonetized/nfglyph.git
cd nfglyph
chmod +x nfglyph
ln -s "$(pwd)/nfglyph" ~/.local/bin/nfglyph
nfglyph
```

tmux popup from README: `bind-key g display-popup -E -w 80% -h 80% -T "Glyph Picker" "nfglyph"`.


## When

**2026-01-30** — Initial release (`0c91b0d`) — range-scan TUI + screenshot + MIT.
**2026-06-01** — init docs/tooling (`1d15ef5`); **upgraded** named JSON + fuzzy (`047b54c`).
**2026-06-03** — fixed quit on linux (`1dfdc82`) -> HEAD. Queue `pushed_at` 2026-06-03T13:54:06Z.

![Commit arc](/blog/nfglyph-bun-ansi-nerd-font-picker-linux-quit/screenshots/commit-arc.png)

## Why

Because a picker that only filters hex forces me to already know the codepoint. Because `process.exit` from an alt-buffer raw-mode loop is how Linux sessions feel stuck. Because shipping glyphs.json next to the binary beats pretending 15k range-scan glyphs are named.

**Engagement Q:** Update README/PLAN to match the fuzzy+50k HEAD, or leave the stale 15k/hex story as a museum label next to the working binary?

---

# svganimator: Electron Svgator clone → Canaveral monorepo SVG keyframe studio

Source: https://www.michaelchurley.com/blog/svganimator-electron-to-canaveral-keyframe-export-studio  
Published: 2026-05-14  
Author: Michael C. Hurley  
Tags: svganimator, svg, animation, keyframes, lottie, export, canaveral, bun, tanstack-start, tanstack-router, zustand, zod, electron, expo, resvg, blacksmith, freview, fallow, hurleyus

![Migrate arc](/blog/svganimator-electron-to-canaveral-keyframe-export-studio/screenshots/migrate-arc.png)

## Who

I wanted an SVG keyframe studio under my own roof: first as an Electron app with Svgator inspiration docs, then as a product surface inside the Canaveral Bun monorepo.

For operators who care about the honest rename tax: GitHub says **svganimator**, `package.json` says **canaveral**, `appConfig` and the UI eyebrow say **SVG Animator**.

## What

I built **svganimator**, public https://github.com/HurleyUS/svganimator. HEAD `86bb4a2`. **13** commits. **0** stars. Default **main**. Root package name **`canaveral`** **0.1.0**.

**At HEAD (Bun / TanStack / workspaces):**

- `@canaveral/svg`: **963** LOC Zod schemas for elements, keyframes, collaborators, projects; export formats **`svg` | `lottie` | `gif` | `mkv`**; `serializeStandaloneSvg`, `serializeLottie`, `applySvgAnimations`, `createStarterSvgProject`
- `web/src/routes/index.tsx` (~378): SVG Animator studio with projects sidebar, canvas scrubber, invite form, export grid
- `web/src/lib/export-renderer.ts` (~160): GIF/MKV via Resvg frame raster (`fps 30`, `880×720`)
- Zustand `useSvgWorkspaceStore` in `shared/state`
- Electron `desktop/` loads `appConfig.url` with title **SVG Animator**
- Expo `mobile/app/index.tsx` shows starter project element/keyframe counts
- `shared/config` sets `name: "SVG Animator"`, `support@svganimator.localhost`

![Studio layout](/blog/svganimator-electron-to-canaveral-keyframe-export-studio/screenshots/studio-layout.png)

**Deleted on migrate (`f2f6023`):** Electron `src/renderer/**` editor, `electron.vite.config.ts`, and the Svgator `inspiration-docs/` scrape from the May 3 init.

![Export formats](/blog/svganimator-electron-to-canaveral-keyframe-export-studio/screenshots/export-formats.png)

**Honesty gaps:** README + `pub/` + `.env.example` still market **Canaveral**. Seven same-afternoon commits titled Standardize Blacksmith CI gates. `REVIEW.md` is a large fallow dump. No public demo URL.

Sibling surfaces to keep straight: private `HurleyUS/canaveral` (generic launch pad, already packed); local no-git `~/Projects/svganimator` Bun path draw-on app; `tsxsvg` (SKIP / plan-only). This public repo is the SVG Animator product identity on a canaveral-named scaffold.

## Where

Code: [github.com/HurleyUS/svganimator](https://github.com/HurleyUS/svganimator), public.

Run (facts from README/scripts): `bun install`, `cp .env.example .env`, `bun run env:check`, `bun run dev` (Caddy) or `bun run dev:raw` (Vite).

No live product URL. No claimed domain.

## When

- **2026-05-03.** `0965ad7` needs work: Electron + Svgator inspiration-docs (~94 files)
- **2026-05-04.** `3e38809` exports: timeline/properties/exporter panels + tests
- **2026-05-04.** `f2f6023` migrate svganimator to canaveral monorepo
- **2026-05-14 afternoon ET.** Blacksmith CI ×7 + FReview observability + formatter; HEAD `86bb4a2`
- Pack prepared **2026-09-08 ~5:06 PM ET** (draft + assets only)

## Why

I needed the SVG animator product on the same Bun/TanStack/Caddy rails as the rest of the launch pad: shared Zod contracts, web studio, Electron shell, Expo card. The GitHub repo name and the root package name still disagree; the UI sells SVG Animator anyway.

**Engagement:** how many public product repos keep README saying the scaffold name while `appConfig` and the UI sell a different product?

---

# Canaveral: Bun TanStack Start launch pad — web, Electron, Expo, Caddy

Source: https://www.michaelchurley.com/blog/canaveral-bun-tanstack-start-web-desktop-mobile-caddy  
Published: 2026-05-14  
Author: Michael C. Hurley  
Tags: canaveral, tanstack-start, tanstack-router, bun, monorepo, caddy, localhost, electron, expo, clerk, convex, stripe, resend, sentry, posthog, biome, tsgo, freview, fallow, blacksmith, hurleyus

## Who

I keep starting the same product spine: TanStack Start web, Electron shell, Expo screen, shared Zod/forms/state, Clerk–Convex–Stripe–Resend–Sentry–PostHog wiring, then re-deriving Caddy HTTPS and the lint gate.

For operators who want that monorepo launch pad as one private repo. An empty hustlestack husk is the wrong clone.

## What

I built **Canaveral**, private https://github.com/HurleyUS/canaveral. HEAD `df2c041`. **11** commits. Version **0.1.0**. Bun **1.3.1** workspaces: web (TanStack Start), desktop (Electron), mobile (Expo), pub, shared/*.

![Workspace map](/blog/canaveral-bun-tanstack-start-web-desktop-mobile-caddy/screenshots/workspace-map.png)

Starter home: waitlist (RHF+Zod client parse), launch checklist (Zustand), Test Resend/Stripe via createServerFn. Lazy Clerk when key present. Caddy in-repo: `https://canaveral.localhost` port **5337**.

![Caddy localhost](/blog/canaveral-bun-tanstack-start-web-desktop-mobile-caddy/screenshots/caddy-localhost.png)

house-checks ban any/unknown and useEffect/useState. gate.mts runs ten checks including ~/bin/freview. Blacksmith CI ends with freview --ci. REVIEW.md 1003 lines / six sections.

![Gate + freview](/blog/canaveral-bun-tanstack-start-web-desktop-mobile-caddy/screenshots/gate-freview.png)

Gaps: package.json lint is oxlint-only while AGENTS.md sells Biome to house to freview; five OPEN manifesto issues still want loaders/useActionState/hydration fixes.

## Where

Code: [github.com/HurleyUS/canaveral](https://github.com/HurleyUS/canaveral), private. No public deploy.

```bash
bun install && cp .env.example .env && bun run env:check && bun run dev
# https://canaveral.localhost (Vite :5337 behind Caddy)
```

## When

**2026-05-04.** init (+6074).

**2026-05-14.** Blacksmith×7 + FReview RN observability + formatter. HEAD df2c041.

**2026-06-09.** five manifesto issues (still OPEN).

**2026-06-18.** queue pushed_at.

![Commit arc](/blog/canaveral-bun-tanstack-start-web-desktop-mobile-caddy/screenshots/commit-arc.png)

## Why

A launch pad should know its localhost slug, env contract, and which gates fail before the first feature branch.

**Engagement Q:** Web + desktop + mobile on day one. Wire Caddy HTTPS, freview, or Convex SSR loaders first?

---

# HurleyUS Agent SOP: Ship or shut up, OpenClaw memory layers, DHH gstack mandate

Source: https://www.michaelchurley.com/blog/hurleyus-sop-ship-or-shut-up-openclaw-dhh-gstack  
Published: 2026-03-31  
Author: Michael C. Hurley  
Tags: hurleyus-sop, agent-sop, openclaw, gstack, dhh, graphite, nextjs-16, convex, operations, hurleyus, agent-ops, standard-operating-procedure

![OG / Ship or shut up](/blog/hurleyus-sop-ship-or-shut-up-openclaw-dhh-gstack/screenshots/toc-map.png)

## Who

I write operating doctrine when agents keep rediscovering their own commits as found treasure.

For operators running **multi-agent HurleyUS sessions** who need a **single public SOP file**: philosophy, OpenClaw memory layers, hard rules earned from incidents, Graphite/`send-agent` team ops, and the **DHH** research-first / **gstack** mandate. This is doctrine on disk, separate from the Convex mission-control app and from empty scaffold SKIP class.

## What

I published **hurleyus-sop** at [`HurleyUS/hurleyus-sop`](https://github.com/HurleyUS/hurleyus-sop). One tracked file: **`AGENT_SOP.md`**. HEAD `c418e5e`. **2** commits. 0 stars. GitHub Linguist **language: null**. Version **1.0**. Header maintainers: Rusty P. Shackelford, Theo Browne, DHH. **548** lines of intentional doctrine. No README, no CI, no package.json. The product is the SOP.

Facts from the file at HEAD:

- Lead philosophy: **"Ship or shut up."** Revenue over polish cycles.
- Role: partner with equity stake, senior engineer, Michael's right hand
- Stack named: TypeScript, **Next.js 16+**, Convex, React, RN/Expo/NativeWind, Swift supporting, `sr-*` skills
- Workspace root documented: **`~/.openclaw/workspace/`** with `SOUL.md`, `AGENTS.md`, `MEMORY.md`, `USER.md`, `TEAM.md`, `TOOLS.md`, `HEARTBEAT.md`, `TODO.md`, daily `memory/YYYY-MM-DD.md`, `current-workload.md` compaction dump
- Memory model: long-term MEMORY.md, daily raw logs, compaction checkpoint at ~**80%** context ("goldfish brain")
- Hard rules: **no CSS filter hacks**, **Next.js 16+ only**, **never type Clerk keys by eye**, **no GitHub paid services** (Vercel is CI; strip `.github/workflows/`), **do the hard work** (all 25 files)
- Git: Conventional Commits + **Graphite** stacked PRs (`gt submit -p --ai`, `~/bin/stack`)
- Comms: `send-agent rusty|theo|dhh`, standups **4×/day** EST, ~30 min heartbeat (cron, Telegram, email/bird/moltbook, SITREP)
- Lessons: removed Actions from **12** repos; 29 parallel agents produced 50K non-compiling lines, then a single agent Convex rewrite in **11 minutes**; AVPlayerLooper leak (RustyP); reaferral invite persistence
- Anti-pattern table + Closing partner mandate

![OpenClaw memory layers](/blog/hurleyus-sop-ship-or-shut-up-openclaw-dhh-gstack/screenshots/memory-layers.png)

![Hard rules](/blog/hurleyus-sop-ship-or-shut-up-openclaw-dhh-gstack/screenshots/hard-rules.png)

**PR #2** (merged 2026-03-31, closes #1) appends **DHH Learnings & Operating Principles** (+46 lines):

- Research-first: questions are failures; delivered solutions are wins
- **gstack is default** for major workflows
- Deployment discipline: local `bun run build`, lint, preview, env synced, PR reviewed, then merge. **No blind deployments.**
- No `any` / `@ts-ignore`. Document self-review. Spawn via gstack/`sessions_spawn` with explicit scope.

![DHH gstack mandate](/blog/hurleyus-sop-ship-or-shut-up-openclaw-dhh-gstack/screenshots/dhh-mandate.png)

![Commit arc](/blog/hurleyus-sop-ship-or-shut-up-openclaw-dhh-gstack/screenshots/commit-arc.png)

Sibling distinction: [`HurleyUS/hurley-mission-control`](https://github.com/HurleyUS/hurley-mission-control) is the Next/Clerk/Convex **comms app**. This repo is the public SOP only.

## Where

Code/docs: [github.com/HurleyUS/hurleyus-sop](https://github.com/HurleyUS/hurleyus-sop), public.

No product domain. No Vercel app for this repo. OpenClaw paths are local (`~/.openclaw/workspace/…`). Clone used for pack: m1Pro13 `~/Projects/HurleyUS/hurleyus-sop` @ `c418e5e`.

## When

- **2026-03-25**: repo created; `f5fdc4a` adds `AGENT_SOP.md` (+502), v1.0 body through Closing
- **2026-03-31**: PR #2 merge `c418e5e`, DHH section (+46); `pushed_at` 2026-03-31T23:28:55Z
- Pack prepared **2026-09-08 ~5:15 PM ET**: draft + assets only

## Why

Agents without a written operating contract repeat the same expensive mistakes: filter-hack dark mode, Actions that bill, parallel agent thrash, typed Clerk secrets, goldfish-brain after compaction. This file is the HurleyUS answer in one Markdown path: philosophy, workspace layout, hard rules, lessons, then DHH's research-first / gstack / deploy checklist so contrib is doctrine, not vibes.

**Engagement Q:** What would you put in your agents' single public SOP that you refuse to negotiate, and which incident finally forced it onto disk?

---

# notion-cli: OpenClaw/Claude Code Notion API skill — CRUD, filters, Markdown

Source: https://www.michaelchurley.com/blog/notion-cli-openclaw-skill-crud-markdown-property-filter  
Published: 2026-03-28  
Author: Michael C. Hurley  
Tags: notion-cli, notion, cli, typescript, bun, openclaw, claude-code, agent, markdown, skill, crud, michaelmonetized

## Who

I wanted agents to talk to Notion without a scrape wrapper and without pasting JSON into the wrong curl.

For OpenClaw / Claude Code workflows that need db/page/block CRUD, property-filtered page lists, and Markdown out of page content.

## What

I built **notion-cli** — public https://github.com/michaelmonetized/notion-cli. HEAD `5309974`. **1** commit. **0** stars. Default **main**. Version **1.0.0** (CLI banner still says **v2.0.0**). CLI / agent skill only.

**Shipped (~1145 LOC src):** `notion db|page|pages|block|search|spaces` via Bun + @notionhq/client v3. Vitest + Biome. `bun build --compile` → `dist/notion`.

![CLI subcommands](/blog/notion-cli-openclaw-skill-crud-markdown-property-filter/screenshots/cli-subcommands.png)

![Agent skill install](/blog/notion-cli-openclaw-skill-crud-markdown-property-filter/screenshots/agent-skill-install.png)

**Agent path:** `./setup` → OpenClaw `~/.openclaw/workspace/bin/notion` or `./setup --host codex` → `~/.claude/skills/notion`. Full command reference in **SKILL.md**.

**Filters + Markdown:** `pages list  -p "Name,Tags"` / `-n "Status"`; `page content ` → block-to-Markdown.

![Docs vs HEAD](/blog/notion-cli-openclaw-skill-crud-markdown-property-filter/screenshots/docs-vs-head-gap.png)

**Gaps:** package 1.0.0 vs banner v2.0.0; DELIVERY/FINAL-DELIVERY still flat `list-databases`; unused `spaces.ts`/`page.ts`/`search.ts` modules; `spaces list` aliases db list; orphan empty postcss.config.js.

![Architecture](/blog/notion-cli-openclaw-skill-crud-markdown-property-filter/screenshots/architecture-stack.png)

## Where

Code: https://github.com/michaelmonetized/notion-cli — public. No live web app.

Run: `export NOTION_API_KEY=…` && `bun install` && `bun build src/cli.ts --compile --outfile dist/notion` (or `./setup`).

## When

**2026-03-28** — 5309974 Initial commit: production-ready Notion CLI (+29 files) → HEAD.

## Why

![Commit arc](/blog/notion-cli-openclaw-skill-crud-markdown-property-filter/screenshots/commit-arc.png)

Because agents need a typed Notion subcommand surface with Markdown out — not another unofficial scrape.
**Engagement Q:** Align package + banner to 1.0.0 and delete flat-command DELIVERY docs, or finish a real spaces API first?

---

# resendld: OpenClaw Resend inbound daemon — poll, hooks/agent, Caddy UI

Source: https://www.michaelchurley.com/blog/resend-listening-daemon-openclaw-poll-hooks-agent-caddy  
Published: 2026-03-27  
Author: Michael C. Hurley  
Tags: resend-listening-daemon, resendld, resend, openclaw, daemon, email, bun, typescript, convex, tanstack-start, caddy, hooks, telegram, macos, arch-linux, michaelmonetized

## Who

I needed inbound Resend mail to wake an OpenClaw agent on my machines. Local gateway path, readable UI, no SaaS inbox product.

For operators wiring Resend receiving boxes into a local agent gateway with a readable UI.

## What

I built **resendld**, public [`michaelmonetized/resend-listening-daemon`](https://github.com/michaelmonetized/resend-listening-daemon). HEAD `4fbec17`. **46** commits. Version **0.0.0-rc.0**.

![Architecture](/blog/resend-listening-daemon-openclaw-poll-hooks-agent-caddy/screenshots/architecture-stack.png)

`src/daemon/listen.ts` polls `https://api.resend.com/emails/receiving` every **5 seconds** with pure `fetch()` (no Resend CLI). List has no body. Each new id hits `/emails/receiving/{id}`, strips HTML if text empty, writes markdown under `~/.openclaw/workspace/mail/`, mirrors to Convex, then `POST`s OpenClaw `https://localhost:18789/hooks/agent` (TLS self-signed allowed). 404/401 falls back to `openclaw cron add --system-event`.

![Hooks dispatch](/blog/resend-listening-daemon-openclaw-poll-hooks-agent-caddy/screenshots/hooks-dispatch.png)

Web: TanStack Start **1.82.1** + Convex inbox/detail/boxes/reply at **https://resendld.localhost** behind Caddy. `install.sh` (640 LOC) targets macOS launchd + Arch systemd.

Afternoon of Mar 23 is mostly PATH hell against `resend-cli` across machines: try API, revert to CLI, then land pure fetch at `28041eac`. Tip #9 resolves `openclaw` cross-platform; ack path in `gateway.ts` still hardcodes `/Users/michael/.bun/bin/openclaw`.

![Web inbox](/blog/resend-listening-daemon-openclaw-poll-hooks-agent-caddy/screenshots/web-inbox.png)

## Where

Code: [github.com/michaelmonetized/resend-listening-daemon](https://github.com/michaelmonetized/resend-listening-daemon), public. No public deploy.

```bash
cd ~/Projects/resend-listening-daemon
bash install.sh          # or --dry-run / --force / --uninstall
# edit ~/.config/resendld/boxes.json
resendld start           # daemon + Convex + web :3000 + Caddy
# opens https://resendld.localhost
```

## When

**2026-03-23.** Phase 0, Telegram/Convex, ~20 PATH/CLI commits, `/hooks/agent`, pure fetch `28041eac`.

**2026-03-24.** Install/docs macOS+Arch (#3).

**2026-03-25.** HTML body fallback + web startup.

**2026-03-26.** TanStack Start pin to 1.82.1 (#8).

**2026-03-27.** Openclaw path (#9), HEAD `4fbec17`. Queue pushed_at 2026-03-27T23:11:34Z.

![Commit arc](/blog/resend-listening-daemon-openclaw-poll-hooks-agent-caddy/screenshots/commit-arc.png)

## Why

An agent that cannot receive email instructions is half-deaf. Poll Resend yourself, store locally, wake the gateway. Skip waiting on a webhook you do not control.

**Engagement Q:** Keep the 5s poll + seen-ids file, or move the tip to Resend webhooks into the same `/hooks/agent` path?

---

# codemail: mail.config.ts founder email — per-domain, not per-seat

Source: https://www.michaelchurley.com/blog/codemail-mail-config-as-code-founder-email-infra  
Published: 2026-03-26  
Author: Michael C. Hurley  
Tags: codemail, email, mail-config, infrastructure, convex, clerk, resend, smtp, nextjs, turborepo, founder, michaelmonetized

![Config as code](/blog/codemail-mail-config-as-code-founder-email-infra/screenshots/config-as-code.png)

## Who

I wanted company email on day 1 of an idea without paying Google or Microsoft per seat while the product was still a maybe.

For founders who think `mail.config.ts` in git should be law, ahead of another admin dashboard.

## What

I built **codemail**, private https://github.com/michaelmonetized/codemail (selection label HurleyUS/codemail; origin is michaelmonetized). HEAD `e9fece4`. **27** commits. Default **main**. Version **0.1.0**.

**Thesis:** email infrastructure for founders: config as code, per-domain pricing, unlimited mailboxes. Adjacent to Gmail, not a seat-for-seat swap.

![Day-1 workflow](/blog/codemail-mail-config-as-code-founder-email-infra/screenshots/day1-workflow.png)

**Shipped (weekend MVP arc):** Turborepo monorepo; `@codemail/config` (Zod + `defineMailConfig`); `@codemail/cli` (setup/deploy/status/dns/logs/users); `@codemail/smtp` on Fly; Convex backend; Next 15 web mail + dashboard; Clerk; Resend outbound; marketing Free Forever / Simple $8 / Managed $80 / Self-Hosted.

![Architecture](/blog/codemail-mail-config-as-code-founder-email-infra/screenshots/architecture-stack.png)

**Gaps:** PLAN still marks IMAP out while README + Simple tier market IMAP; GitHub public probe 404 (private) despite OSS CTA; `codemail.vercel.app` MIDDLEWARE 500 vs `codemail-web.vercel.app` 200; root `mail.config.ts` is informal t3.chat demo shape; `codemail.dev` unchecked.

![Docs vs shipped](/blog/codemail-mail-config-as-code-founder-email-infra/screenshots/docs-vs-shipped-gap.png)

## Where

Code: https://github.com/michaelmonetized/codemail (private).

Live marketing/app: https://codemail-web.vercel.app

SMTP: codemail-smtp.fly.dev (per TODO)

## When

**2026-02-12.** SMTP, then web mail, then dashboard, then Convex, then API, then auth/SEO, then Fly SMTP fixes, then marketing rewrite to business-plan thesis (same-day blast).

**2026-02-13.** public marketing routes + `(private)` auth route group.

**2026-02-15.** `62fcaed` better sales positioning (#1).

**2026-02-18.** `e9fece4` design updates. HEAD.

![Commit arc](/blog/codemail-mail-config-as-code-founder-email-infra/screenshots/commit-arc.png)

## Why

Day-1 company email should be a `git push`, not a $600/year seat tax. Config in the repo beats another dashboard click-path when the product is still a maybe.

**Engagement Q:** Ship IMAP before opening the repo public, or cut IMAP from the landing first?

---

# milkup: TipTap+Convex WYSIWYG that still ships a Milkdown README

Source: https://www.michaelchurley.com/blog/milkup-tiptap-convex-wysiwyg-stale-milkdown-readme  
Published: 2026-03-25  
Author: Michael C. Hurley  
Tags: milkup, tiptap, wysiwyg, convex, nextjs, react, markdown, html-to-markdown, media-picker, milkdown, typescript, michaelmonetized

## Who

I needed a drop-in editor for HustleLaunch client blogs — toolbar clicks instead of markdown homework, paste from Google Docs, and a media picker that doesn't dump files into `/public` forever.

For operators who already run Convex and Next and want the HTML model to keep emitting clean markdown behind the scenes.

## What

I built **milkup** — public https://github.com/michaelmonetized/milkup. HEAD `45fb587`. **9** commits. **0** stars / **1** fork. Default **master**. Version **0.1.0**. Local Next app only.

**Hour zero (`fea1956`, 06:03 ET):** Milkdown + Convex media picker + `lib/milkdown-video-plugin.ts` custom `!video[alt](url)` path.

**Two minutes later (`d73d1f9`):** strip Convex, ship `MOCK_MEDIA`, keep the textarea POC honest.

**README (`91fa36f`):** locks the public story on Next.js **15** / React **18** / Milkdown / mock library / `!video[]()` — and that file never got another commit.

![README vs HEAD](/blog/milkup-tiptap-convex-wysiwyg-stale-milkdown-readme/screenshots/readme-head-gap.png)

**Pivot (`35c109f`, 06:32):** TipTap `RichEditor` + `Toolbar` + `html-to-markdown` / `markdown-it` converters + Convex `Providers` back in. `EditorPage` subtitle says "WYSIWYG editor with TipTap + Convex media storage."

**CVE pin (`e11562e`):** React **19** + Next **14.2** because BUILDPLAN bans Next 15.x / React 18.x.

**Next 16 (`9b7b0e6`, 06:48):** jump to `next` ^16 "Vercel-ready" — BUILDPLAN stack blurb still says Next 15 + React 18 in places.

**Opus QA (`45fb587`, 06:59) → HEAD:** delete leftover `milkdown-video-plugin.ts`, ESLint config, MediaPicker tidy.

![Stack @ HEAD](/blog/milkup-tiptap-convex-wysiwyg-stale-milkdown-readme/screenshots/stack-head.png)

Residue that matters:

- `handleMediaSelect` inserts `![Video](url)` for videos — custom `!video[]()` is documentation only.
- `convex/media.ts` is **queries only** (`listMedia`, `listMediaByType`). ARCHITECTURE still diagrams `uploadFile` / `registerMedia`.
- `MediaPicker` empty state: "No media found. Upload some first!" with nowhere to upload.
- `providers.tsx` falls back to `https://your-deployment.convex.cloud`.

![Media picker empty](/blog/milkup-tiptap-convex-wysiwyg-stale-milkdown-readme/screenshots/media-picker-empty-convex.png)

![Video syntax lost](/blog/milkup-tiptap-convex-wysiwyg-stale-milkdown-readme/screenshots/video-syntax-lost.png)

## Where

Code: [github.com/michaelmonetized/milkup](https://github.com/michaelmonetized/milkup) — public. No live product URL.

```bash
git clone https://github.com/michaelmonetized/milkup.git
cd milkup
bun install
# set NEXT_PUBLIC_CONVEX_URL in .env.local
bunx next dev
```

Open http://localhost:3000 — TipTap left, raw markdown right, Insert Media opens the Convex modal.

## When

**2026-03-25 06:03–06:59 ET** — entire arc on one afternoon.

| SHA | Clock | Beat |
|---|---|---|
| `fea1956` | 06:03 | Milkdown + Convex + video plugin |
| `d73d1f9` | 06:05 | Mock strip |
| `91fa36f` | 06:06 | README (frozen face) |
| `30ff01a` | 06:25 | BUILDPLAN + ARCHITECTURE |
| `35c109f` | 06:32 | TipTap + Convex return |
| `e11562e` | 06:33 | React19 + Next14.2 |
| `c28d8d5` | 06:35 | CONVEX_SETUP |
| `9b7b0e6` | 06:48 | Next.js 16 |
| `45fb587` | 06:59 | Opus QA → HEAD |

Queue `pushed_at` 2026-03-25T10:59:17Z.

![Commit arc](/blog/milkup-tiptap-convex-wysiwyg-stale-milkdown-readme/screenshots/commit-arc.png)

## Why

Because client editors fail when the truth lives in TipTap and the README still teaches Milkdown. Because a media picker that can only `listMedia` is a museum of intent. Because CVE notes and a Next 16 bump in the same hour are how a POC ages in public.

**Engagement Q:** Rewrite README to TipTap+Next16+queries-only truth, or keep the Milkdown museum label and finish the upload mutations ARCHITECTURE already drew?

---

# simple: Next.js 16 Firebase auth starter — social login, keep-Firebase decision

Source: https://www.michaelchurley.com/blog/simple-nextjs-firebase-auth-social-keep-decision  
Published: 2026-03-23  
Author: Michael C. Hurley  
Tags: simple, nextjs, react, firebase, firestore, auth, social-login, typescript, tailwind, vercel, security-headers, convex, starter, boilerplate, michaelmonetized

## Who

I wanted a thin auth starter that did email + social without dragging Clerk or a Convex migration into launch week.

For anyone spinning a Next App Router app that needs Firebase Auth + a users doc and can live with honest leftovers.

## What

I built **simple** - public https://github.com/michaelmonetized/simple. HEAD da7e0b0. 10 commits. 0 stars. Default main. Version 0.1.0 (private: true). Claimed live URL https://simple-ivory.vercel.app is DEPLOYMENT_NOT_FOUND.

**Shipped (~1k LOC app/firebase/components):** Next 16 + React 19 + Firebase 12 Auth/Firestore. /, /login, /register. Email/password SignUpForm + LoginForm. Google / Facebook / Twitter Connect buttons (signInWithPopup / linkWithPopup). AuthProvider -> Firestore users. Generic CRUD in firebase/crud.js. Clamp spacing + Catppuccin color tokens. Security headers in next.config.mjs. Documented keep-Firebase decision.

![Auth surface](/blog/simple-nextjs-firebase-auth-social-keep-decision/screenshots/auth-surface.png)

![Keep Firebase decision](/blog/simple-nextjs-firebase-auth-social-keep-decision/screenshots/keep-firebase-decision.png)

**Launch arc:** four Sept 2024 init commits -> Dec 2025 / Feb 2026 CVE bumps -> console strip + HSTS/X-Frame headers -> architecture doc -> final console cleanup (#5).

![Security headers](/blog/simple-nextjs-firebase-auth-social-keep-decision/screenshots/security-headers.png)

**Gaps:** create-next-app README still; login/page.tsx exports RegisterPage; coerceeUserCredntial typo; console.warn left; hardcoded firebaseConfig; Hustle Launch branding; Tailwind pkg vs classic config; kitchen-sink env example; no middleware; Vercel deploy gone.

![Residue gaps](/blog/simple-nextjs-firebase-auth-social-keep-decision/screenshots/residue-gaps.png)

## Where

Code: https://github.com/michaelmonetized/simple - public.

Run: install deps then next dev.

Live: simple-ivory host is down (404).

## When

**2024-09-05** - fec428f to 1c2d5f5 four init commits.

**2025-12-29** - 7c5e849 cve vulnerabilities.

**2026-02-21 to 2026-03-23** - 03addd3 CVE; e0b5dee+407450f headers; 9b33e46 keep-Firebase; da7e0b0 HEAD.

## Why

![Commit arc](/blog/simple-nextjs-firebase-auth-social-keep-decision/screenshots/commit-arc.png)

Because launch week needed auth that already worked, with a written keep-Firebase decision.

**Engagement Q:** Redeploy simple-ivory and finish middleware, or keep as Firebase keep-decision reference until Convex revisit?

---

# hurley-mission-control: I put humans and agents in the same thread model

Source: https://www.michaelchurley.com/blog/hurley-mission-control-human-agent-comms  
Published: 2026-03-22  
Author: Michael C. Hurley  
Tags: hurley-mission-control, mission-control, convex, clerk, nextjs, agents, openclaw, hurleyus, realtime, deliveries

## Who

I run agents and humans on the same company. Slack does not know what an agent is. A terminal log does not know what a delivery receipt is. The Go repo named mission-control is a p10k-inspired TUI for deploys and git status. Hustle Launch mission-control-os is a different product. This room is the HurleyUS comms plane.

This piece is for operators who need a human and an agent in the same membership list. For people who want queued/delivered/failed rows next to a message instead of hoping a webhook fired. For anyone who has already confused the three Mission Control names in this portfolio and needs the cut named out loud.

## What

I shipped Hurley Mission Control — a public HurleyUS repo whose README says the quiet part: unified comms plane for agents and humans.

Stack facts: Next.js 16.2.1 App Router, React 19.2, Clerk, Convex, TypeScript, cmdk and framer-motion in the lockfile, package name flattened to `web` after the monorepo fight, packageManager bun@latest, local port 3410.

![Dashboard — threads with dm group project ops kinds](/blog/hurley-mission-control-human-agent-comms/screenshots/dashboard-threads.png)

The Convex schema is the product. users.kind is human or agent. Agents can carry agentId and machineId. Threads are dm, group, project, or ops. Messages support replyTo and optional type text|event|system. sendMessage checks clientMessageId before insert so retries do not double-post. Deliveries fan out to other members as queued rows with attempts and lastError. Presence tracks online and lastSeenAt.

The web path is a thread grid with kind icons, a detail route for the feed, optimistic yellow sending states in the sprint writeup, and a quick-stats strip that literally advertises 2s message refresh. getThreads still collects all threads and filters membership in the handler — honest, not cute.

Auth is hybrid. Clerk packages are real. useUser also reads localStorage userId and testUserId and can POST /api/sync-user to mint a Convex user for sprint testing without the full Clerk dance. The live root paints Redirecting... and aims at sign-in when that key is missing. Title in the document: HurleyUS Mission Control.

![Daemon stub and OpenClaw plugin WIP](/blog/hurley-mission-control-human-agent-comms/screenshots/daemon-plugin-wip.png)

Honesty on the edges: apps/daemon/src/index.ts is two console.log lines and a TODO to subscribe to assigned threads and relay into a local OpenClaw session. packages/channel-plugin is a README promising send, reply, and receive mapping — not an implemented adapter. PLAN.md checkboxes still look more empty than the sprint summary claims. That gap is the story, not a cover-up.

![Vercel monorepo thrash](/blog/hurley-mission-control-human-agent-comms/screenshots/vercel-thrash.png)

The secondary plot is deploy theater.

Forty-nine commits on master.
Most of those commits are deploy config thrash.
Roughly forty-one are Vercel root/bun/npm config flip-flops.

The fight ends by moving the web app to the repo root.

Also: Next 16.2.1 bump, npmrc legacy-peer-deps, HEAD b3d11ec env cleanup.

## Where

Live: https://hurley-mission-control.vercel.app (HTTP 200, title HurleyUS Mission Control).
Code: https://github.com/HurleyUS/hurley-mission-control master. HEAD b3d11ec. 49 commits. Public.
Contrast: michaelmonetized/mission-control is a Go TUI. mission-control-os is a different product. Do not merge the three names.
Dev port 3410. No fresh coding clone for this pack.

## When

2026-03-19 ET evening: 05f475c Phase 1 complete. Deploy docs same hour.
2026-03-20: Vercel monorepo storm; 8d9708e getThreads filter fix.
2026-03-21: sprint status; flatten monorepo; Next 16.2.1; HEAD b3d11ec ~10:52 PM ET.
2026-08-08: pushed_at bump, no new commit after HEAD.
2026-09-08: pack drafted; slug hurley-mission-control-human-agent-comms unused.

## Why

Agent work without a shared thread is gossip, not ops. Humans and agents as first-class users. clientMessageId so retries do not double-post. Delivery rows beat hope. Name the thrash. Cut three Mission Control names apart.

Engagement: if your agents and your humans do not share a thread id, what exactly are you operating?

---

# ConnectedIn: MV3 LinkedIn auto-connect Chrome extension

Source: https://www.michaelchurley.com/blog/connectedin-mv3-linkedin-auto-connect-chrome-extension  
Published: 2026-03-21  
Author: Michael C. Hurley  
Tags: connectedin, linkedin, chrome-extension, manifest-v3, auto-connect, content-script, popup, rate-limit, martech, michaelmonetized, hurleyus

## Who

I still hit LinkedIn people-search pages where the work is repetitive Connect clicks, and I still want the delay, the stop button, and the weekly ceiling visible before the loop runs away.

For operators who load-unpacked a tiny MV3 tool on their own account, not a SaaS growth bot.

## What

I shipped **ConnectedIn**, public https://github.com/michaelmonetized/ConnectedIn. HEAD `a6236fa`. **2** commits. Version **1.0.0**. Manifest V3 popup + `content.js` clicker.

![Content loop](/blog/connectedin-mv3-linkedin-auto-connect-chrome-extension/screenshots/content-loop.png)

Popup: delay input (100–5000ms, default 500), Start/Stop, clicked/remaining stats, yellow ~1,100 connects/week warning. `chrome.storage.sync` keeps delay + stats. Content script filters `button[type="button"]` whose trimmed text is exactly `connect`, clicks with `setTimeout` spacing, posts `updateStats` / `finished` messages.

![Manifest gaps](/blog/connectedin-mv3-linkedin-auto-connect-chrome-extension/screenshots/manifest-gaps.png)

Honest gaps on day one: manifest declares icons under `images/` but **no images directory**; **no `content_scripts` entry** so `content.js` is not auto-injected; popup `sendMessage` has nothing to talk to unless something else injects the file. `scripting` permission is unused.

![Rate limit](/blog/connectedin-mv3-linkedin-auto-connect-chrome-extension/screenshots/rate-limit.png)

README + INSTALL.md document LinkedIn’s weekly ceiling, delay bands, and that automation sits awkwardly against LinkedIn ToS (personal-use framing only).

## Where

Code: [github.com/michaelmonetized/ConnectedIn](https://github.com/michaelmonetized/ConnectedIn), public MIT.

```bash
git clone https://github.com/michaelmonetized/ConnectedIn.git
# chrome://extensions: Developer mode, Load unpacked, select folder
# open linkedin.com people search, click extension, Start Clicking
```

## When

**2026-03-21 15:30 ET.** init (+548, 7 files).

**15:31 ET.** INSTALL.md (+147). HEAD `a6236fa`.

Queue `pushed_at` **2026-03-21T19:31:06Z**.

![Commit arc](/blog/connectedin-mv3-linkedin-auto-connect-chrome-extension/screenshots/commit-arc.png)

## Why

A Connect clicker is only useful if the delay and the weekly ceiling are first-class, and if the manifest actually wires the content script.

**Engagement Q:** Fix `content_scripts` + icons first, or rewrite the selector against today’s LinkedIn DOM?

---

# Agent OS: Bun + Ink + WebSocket shell-native orchestrator

Source: https://www.michaelchurley.com/blog/agent-os-bun-ink-websocket-zsh-orchestrator  
Published: 2026-03-21  
Author: Michael C. Hurley  
Tags: agent-os, hurleyus, bun, ink, websocket, zsh, orchestrator, mission-control, openclaw, anthropic, terminal-ui, michaelmonetized

## Who

I still run multi-agent days where the bottleneck is the shell context, the task queue, and whether each agent can see my real `~/.zshrc` aliases instead of a sandboxed stub.

For operators wiring local agent pools against a Mission Control ship list on their own machine.

## What

I shipped **HurleyUS Agent OS**, public https://github.com/michaelmonetized/agent-os. HEAD `f66fffa`. **2** commits. package **hurleyus-agent-os@1.0.0**.

![Ink UI](/blog/agent-os-bun-ink-websocket-zsh-orchestrator/screenshots/ink-ui.png)

`orchestrator.ts` is an Ink React terminal UI plus a `WebSocketServer` on **ws://localhost:9999**. Agents connect with `?agent=`, pull `task_dispatch`, and stream `task_output` / complete / fail. State lives in `~/.hurleyus/agent-os/state.json`; every session transcript lands in `~/.hurleyus/agent-os/sessions/.log`.

![Zsh agent](/blog/agent-os-bun-ink-websocket-zsh-orchestrator/screenshots/zsh-agent.png)

`agent-client.ts` spawns **interactive zsh** (`zsh -i -c …`), sources `~/.zshrc`, optionally loads `~/.hurleyus/GOALS` and `~/.hurleyus/TASKS`, then evals the task description. If the cwd is a git repo it auto-commits `[agent-name] ` and pushes `origin HEAD`.

![Mission Control seed](/blog/agent-os-bun-ink-websocket-zsh-orchestrator/screenshots/mc-seed.png)

`mission-control-tasks.ts` seeds **seven Phase 1 tasks** aimed at `hurley-mission-control`: Convex backend, web layout, architecture review, message wiring, polling, Vercel deploy, QA smoke. `cli.ts load-mission-control` / `start.sh` load that list.

![Dual path gaps](/blog/agent-os-bun-ink-websocket-zsh-orchestrator/screenshots/dual-path-gaps.png)

Same tree also has `core.ts`: four personas (Codex Dev, SR Designer, SR Architect, QA Auditor) on `claude-opus-4-6` via Anthropic Messages API, with a dependency-aware priority queue. Honest gaps: `@anthropic-ai/sdk` is imported but **missing** from `package.json`; `.env` with `ANTHROPIC_API_KEY` is **still tracked** after the `.gitignore` commit (gitignore only covers `node_modules/`, `*.log`, `.DS_Store`). OpenClaw relay to `ws://192.168.1.134:18789` is stubbed. `agent-runner.sh` is a file inbox/outbox fallback, not live WS.

Sibling tools in the same shop: orclawstrator's OpenClaw `:3377` gateway, the Go p10k `mission-control` portfolio TUI, the Convex human|agent `hurley-mission-control` web plane, and `shagent`'s MCP/OpenRouter loop. Agent OS is the Ink/WS/zsh control plane with a Mission Control Phase 1 seed.

## Where

Code: [github.com/michaelmonetized/agent-os](https://github.com/michaelmonetized/agent-os), public, no LICENSE file (README: internal HurleyUS).

```bash
git clone https://github.com/michaelmonetized/agent-os.git
cd agent-os
bun install
# Terminal 1
bun cli.ts run-orchestrator
# Terminal 2+
bun cli.ts spawn-agent codex-dev
bun cli.ts spawn-agent sr-designer
# Load Phase 1
bun cli.ts load-mission-control
bun cli.ts status
```

Local only: orchestrator `ws://localhost:9999`. Optional env `OPENCLAW_GATEWAY`. Runtime state under `~/.hurleyus/agent-os/`.

## When

Created on GitHub **2026-03-20**. First commit **11:13 AM ET** same day: full v2 surface (Ink + WS + zsh + CLI + Mission Control seed). Second commit **10:25 PM ET**: `.gitignore` only. GitHub `pushed_at` **2026-03-21T02:25:13Z**. Pack prepared **2026-09-08 ~5:17 PM ET**. Draft + assets only.

## Why

I needed a shell-native control plane that treats agents like terminals with jobs. Ink for the operator view, WebSocket for fan-out, real zsh so aliases and git muscle memory stay intact, and a Mission Control Phase 1 seed so the queue is not empty on day one. The Anthropic `core.ts` path is the API-side twin when you want model output without a shell; the gaps (missing SDK dep, tracked `.env`) are the honest day-one scars.

**Engagement Q:** How would you unify the Ink/WS/zsh path and the Anthropic `core.ts` path: one CLI surface, or keep them as two intentional modes?

---

# stripe-convex: I shipped a Theo-compliant Stripe+Convex library — and never published the package

Source: https://www.michaelchurley.com/blog/stripe-convex-email-payments-theo-unpublished  
Published: 2026-03-20  
Author: Michael C. Hurley  
Tags: stripe-convex, stripe, convex, payments, subscriptions, cart, coupons, typescript, webhooks, theo, t3, saas, billing, michaelmonetized

![stripe-convex API surface: Pay, Cart, Checkout, Has, Convex exports](/blog/stripe-convex-email-payments-theo-unpublished/screenshots/api-surface.png)

## Who

I wanted one payment module I could drop into getat.me, hustlelaunch, and every other Convex SaaS instead of rewriting Stripe checkout + webhooks per repo.

Who it is for now: operators who bill by **email** before they finish auth binding; builders who want Convex `sc_*` tables and idempotent webhook logs without starting from Theo KV snippets; anyone who will check the registry before they trust a README badge.

## What

I built **stripe-convex**, public under **michaelmonetized/stripe-convex**, package **0.1.0**, MIT on paper. Peer deps: Convex ≥1, Stripe ≥14, React ≥18. Built with tsup + Bun. Exports: root types/components, `stripe-convex/convex`, `stripe-convex/components`.

React surface: `StripeConvexProvider`, `Pay`, compound `AddToCart` (with `CartItemPlan`), `Cart`, `Checkout`, `Has`. Hooks: `useStripeConvex`, `useCart`, `useCoupon`, `useCheckout`, `useHasAccess`.

![sc_* Convex schema tables](/blog/stripe-convex-email-payments-theo-unpublished/screenshots/schema-sc-tables.png)

Convex schema spreads six tables: `sc_customers`, `sc_payments`, `sc_subscriptions`, `sc_orders`, `sc_coupon_usage`, `sc_webhook_events`. Customers are indexed by **email**. `TRACKED_EVENTS` lists **19** Stripe types, checkout.session.completed through charge.refunded.

Theo lane (t3dotgg/stripe-recommendations): `getOrCreateStripeCustomer`, `syncCustomerData`, `createPortalSession`, brand/last4 on subscription payment method fields. Commit `77812a0` on 2026-02-06 is `feat: implement Theo's Stripe recommendations`. The compliance report file still opens with a summary table that marks several of those items Missing: stale header, live code.

![Theo helpers vs stale report header](/blog/stripe-convex-email-payments-theo-unpublished/screenshots/theo-compliance.png)

HEAD `22e099e` is PR **#13**: `AddToCart` gains `isSubscription` + `planId`; subscriptions default to direct checkout unless `addToCart` forces the cart path.

![AddToCart subscription compound API](/blog/stripe-convex-email-payments-theo-unpublished/screenshots/addtocart-subscription.png)

sitrep.md says **SHIPPED**. ROADMAP still has package publication unchecked. `.github/workflows/publish.yml` waits for a GitHub Release. Releases: **zero**. Public registry package stripe-convex: **404**. README still shows the version badge. LICENSE and README footer: **© Michael Shilman**. package.json author: Michael Hurley.

PENDING_ISSUES.md parks twelve real notes: Pay clearCart/addToCart race, unused onSuccess, Has returns null while loading, email checks only for `@`, duplicated formatPrice, cart not persisted, `as any` in syncCustomerData, and more.

## Where

Code: [github.com/michaelmonetized/stripe-convex](https://github.com/michaelmonetized/stripe-convex), **public**. No homepage / demo URL. Intended consumers named in ROADMAP: getat.me, hustlelaunch, other SaaS products. Revenue note in ROADMAP: **INDIRECT**.

![Badge vs registry 404](/blog/stripe-convex-email-payments-theo-unpublished/screenshots/registry-gap.png)

Local clone used for the pack: `/home/michael/Projects/_site-map/stripe-convex` on m1pro16. GitHub API shows **11** commits; that clone git log is squash-shaped to the single HEAD commit while the tree matches the library.

## When

**2026-02-04.** `1a3d250` Initial commit: stripe-convex payment package. Same day `0f5b81c` comprehensive docs.

**2026-02-06.** `c38c209` full type system + Convex functions. `f608a74` roadmap + license year. `77812a0` Theo recommendations.

**2026-02-11.** `635c61c` repo URLs + document all 19 webhook events.

**2026-02-21.** PR **#8** `cd172db`: dep conflicts, processRefund index, replace `v.any()`.

**2026-02-28.** PR **#11** CI/CD testing + publishing. Eight minutes later `c1e4a39`: remove GitHub Actions workflows; Vercel is our CI. `publish.yml` is still in the tree at HEAD.

**2026-03-01.** PR **#12** prep for registry publish.

**2026-03-20.** PR **#13** AddToCart subscription support. HEAD `22e099e`.

**2026-06-22.** GitHub `pushed_at` 22:20:53Z with no newer main commit beyond HEAD.

![Commit arc Feb to Mar 2026](/blog/stripe-convex-email-payments-theo-unpublished/screenshots/commit-arc.png)

## Why

Every monetized Convex app was going to need the same Stripe spine, and copying webhook handlers is how you get drift. Email-first customers match the products that take payment before they finish auth. Theo recommendations are a checklist I wanted encoded as exports, not a blog tab I reopen under pressure. The honest scar is the unpublished registry: badge, workflow, prep PR, sitrep SHIPPED, and a 404.

**Engagement Q:** When sitrep says SHIPPED and the registry returns 404, which status do you put in the blog title?

---

# MyBathroomConversion.com: Elementor Opt-In → SalesPromis, then I deleted the xdebug_info() probe

Source: https://www.michaelchurley.com/blog/mybathroomconversion-elementor-salespromis-xdebug-purge  
Published: 2026-02-27  
Author: Michael C. Hurley  
Tags: mybathroomconversion, wordpress, elementor, wp-engine, salespromis, remodelingloans, leadgen, bathroom-remodel, security, xdebug, hustle-launch

![Illustrative home composite from indexed marketing copy](/blog/mybathroomconversion-elementor-salespromis-xdebug-purge/screenshots/home-composite.png)

## Who

I keep meeting bathroom remodel landers that look expensive and behave like a mailto form.

Homeowners need a one-day tub-to-shower path, a phone that answers, and financing language that does not invent a bank. Operators need the Opt In record to leave WordPress and hit an intake API. Agencies that inherit WP Engine content+plugins repos need to find the money path in the child theme, not in another aspirational PLAN checkbox.

If you have ever found `xdebug_info()` at the webroot of a client site, this is the audit trail.

## What

I maintain **www.mybathroomconversion.com**. Private **HurleyUS/www.mybathroomconversion.com**, GPL-3.0, five commits, HEAD `dc5a091`.

README fact: commissioned by **SalesPromis** through **Hustle Launch** for **RemodelingLoans.com**. Marketing surface (search index; live TLS failed here): dream bath/shower in as little as one day; phone **888-859-8916**; free in-home design consultation.

Stack: Hello Elementor **3.1.1** + child **2.0.0**, Elementor **3.23.4**, Elementor Pro **3.23.3**, Dynamic.ooo **3.0.11**, Rank Math **1.0.225**, MonsterInsights **9.0.0**, Meta pixel **3.0.16**. WP Engine ignore strips core/uploads/config.

Operator spine: `elementor_pro/forms/new_record` to `my_bathroom_conversion_lead` to Opt In to POST `https://api.salespromis.com/endpoint/intake/` with `API-KEY` header; success writes `ABSPATH/.log/salespromis-$now-$id.log`. Hardcoded SalesPromis API key still in child theme. Value redacted here; rotate it.

![Lead pipe composite](/blog/mybathroomconversion-elementor-salespromis-xdebug-purge/screenshots/lead-pipe-composite.png)

## Where

Code private on HurleyUS (local remote still michaelmonetized; same HEAD). Product: **https://www.mybathroomconversion.com**.

Pack-day: DNS **141.193.213.10 / .11**; HTTPS TLS handshake alert; HTTP Cloudflare **409**. Composites labeled. WP File Manager **7.2.9** still vendored (906 files).

![Stack composite](/blog/mybathroomconversion-elementor-salespromis-xdebug-purge/screenshots/stack-composite.png)

## When

**August 15, 2024, 4:03 PM ET:** `8324926` init. Plugins, themes, SalesPromis logs, `.htaccess`, `local-xdebuginfo.php`.

**4:06 PM ET:** `17627c2` README commission chain.

**4:19 PM ET:** `ad06651` untrack `.htaccess` + `.log/salespromis-*.log`.

**January 8, 2026, 12:21 PM ET:** `eb7581e` PLAN.md claims lead capture Not Started while the hook already posts.

**February 27, 2026, 5:18 AM ET:** `dc5a091` PR #2. Delete `Page stub. /pricing does not call createStripeUrl. No tracked .env.example despite README. PLAN.md (Jan 2026) still lists Convex, PostHog, CLI scaffolding. .cursor/rules/STRIPE.md is Theo KV-sync essay pasted beside a Postgres webhook implementation.

![CVE bump](/blog/boilerplate-clerk-drizzle-stripe-next16-cve/screenshots/cve-bump.png)

Feb 22 2026: fix(security) upgrade Next.js 14.2.8 to 16.1.6 for CVE-2025-55184. package.json + bun.lockb only. Live: https://boilerplate-fawn-gamma.vercel.app and https://boilerplate.hustlelaunch.com both 200 with Boilerplate.

## Where

Code: [github.com/michaelmonetized/boilerplate](https://github.com/michaelmonetized/boilerplate), private MIT.

Clone requires michaelmonetized auth. README still expects mv .env.example .env (file not tracked). Edit data/app.ts, install deps, deploy Vercel.

Live shells: [boilerplate-fawn-gamma.vercel.app](https://boilerplate-fawn-gamma.vercel.app) · [boilerplate.hustlelaunch.com](https://boilerplate.hustlelaunch.com)

## When

**2024-09-07 to 09-11 ET.** Create Next App, then Clerk middleware/redirect/color fights, then Stripe ready (~50 commits in four days).

**2024-09-17.** shrug.

**2026-01-08.** PLAN.md.

**2026-01-31.** STRIPE.md sync.

**2026-02-22 07:42 ET.** Next 16 CVE bump to HEAD `6f2dd2f`. Queue push **2026-02-22T12:42:54Z**.

![Commit arc](/blog/boilerplate-clerk-drizzle-stripe-next16-cve/screenshots/commit-arc.png)

## Why

A SaaS starter is only honest if the auth boundary, the subscriptions row, and the webhook exist, and if you admit the marketing routes are still stubs when you CVE-bump sixteen months later.

**Engagement Q:** Wire /pricing + /billing to createStripeUrl next, or replace the Postgres webhook with Theo single KV sync before cloning this into the next hustle?

---

# launchpad: NYE 2024 Hustle Launch boilerplate — README stack, unwired providers

Source: https://www.michaelchurley.com/blog/launchpad-nye2024-boilerplate-providers-unwired  
Published: 2026-02-06  
Author: Michael C. Hurley  
Tags: launchpad, hustlelaunch, nextjs, boilerplate, clerk, convex, resend, posthog, stripe, shadcn, starter, michaelmonetized

![README vs tree](/blog/launchpad-nye2024-boilerplate-providers-unwired/screenshots/readme-vs-tree.png)

## Who

I wanted one Next repo I could clone for every Hustle Launch lander (auth, leads DB, email, payments, analytics) instead of re-wiring Clerk and Resend on each client site.

## What

Public **michaelmonetized/launchpad**. v0.1.0. HEAD `5447591`. **8** commits. README sells LaunchPad by Hustle Launch as the premier boilerplate for websites / apps / sales landers. Tree at HEAD actually has:

1. Stock **Create Next App** `app/page.tsx` + layout metadata still titled Create Next App
2. shadcn new-york kit: **16** UI primitives (~1487 LOC) with **no** lead form page
3. `providers/{clerk,convex,posthog}.tsx` written, **never imported** into `layout.tsx`
4. `middleware.ts` = bare `clerkMiddleware()` (edge without mounted provider tree)
5. `app/api/send` Resend route to `delivered@resend.dev`, `from` = env or **notify@uncap.us**
6. `stripe` + `@sentry/nextjs` in package.json: **zero** app imports; Stripe implementation is a dumped Theo `STRIPE.md` cursor rule
7. **No** `convex/` schema folder. ConvexReactClient wrapper only.

![Stack deps vs usage](/blog/launchpad-nye2024-boilerplate-providers-unwired/screenshots/stack-deps.png)

![Providers unwired](/blog/launchpad-nye2024-boilerplate-providers-unwired/screenshots/providers-unwired.png)

## Where

[github.com/michaelmonetized/launchpad](https://github.com/michaelmonetized/launchpad) (public, MIT (c) 2024 Hustle Launch). README points at launchpad.hustlelaunch.com and `/pro`. Pack-time DNS: **NXDOMAIN** for `launchpad.hustlelaunch.com`. Sibling contrast: **hustlestack-starter/template** are empty mkproject husks (SKIPPED); **convex-nextfaster** actually ships Convex ecommerce schema; **uncap.us** is the live product. LaunchPad is the unfinished public starter claim.

![Domain NXDOMAIN](/blog/launchpad-nye2024-boilerplate-providers-unwired/screenshots/domain-nxdomain.png)

## When

**2024-12-31** (~03:15–04:13 ET). Create Next App, empty `init`, shadcn+README, providers+Resend+LICENSE.

**2026-01-08.** `PLAN.md` pivots identity to a **client portal** checklist (none of it built).

**2026-01-31.** Sync Theo Stripe cursor rule.

**2026-02-06.** HEAD Resend `from` env fallback to notify@uncap.us. No further commits.

![Commit arc](/blog/launchpad-nye2024-boilerplate-providers-unwired/screenshots/commit-arc.png)

![PLAN drift](/blog/launchpad-nye2024-boilerplate-providers-unwired/screenshots/plan-drift.png)

## Why

A README that lists the whole MarTech stack is not a framework. Mounting providers, owning a Convex schema, and resolving the product domain are what separate a clone-ready LaunchPad from a New Year's Eve aspiration with a nice UI kit. The Resend notify@uncap.us fallback is the only line that still points at how I actually ship mail.

**Engagement Q:** Would you publish a starter whose README lists Stripe/Sentry/Convex before the providers are mounted, or keep it private until `layout.tsx` and DNS match the marketing?

---

# iLeague-app: the generic influencer monorepo before the golf rebrand

Source: https://www.michaelchurley.com/blog/ileague-app-influencer-monorepo-before-golf-rebrand  
Published: 2026-02-04  
Author: Michael C. Hurley  
Tags: ileague-app, ileague, influencer, creator-economy, monorepo, bun, nextjs, expo, convex, clerk, stripe-connect, scaffold, before-golf, hurleyus

![Landing hero, violet influencer/fan unite](/blog/ileague-app-influencer-monorepo-before-golf-rebrand/screenshots/landing-hero.png)

## Who

I needed a creator/fan product that was still honest about being a scaffold: leagues, posts, Stripe Connect, before I verticalized it into golf.

For operators comparing the public influencer monorepo to the later emerald golf stack. Skip if you only want scorecards or Top-54 iTour lore.

## What

I shipped **iLeague-app**, public [`HurleyUS/ileague-app`](https://github.com/HurleyUS/ileague-app). HEAD `7af9d80`. **7** commits. Package **1.0.0**. Bun workspaces: `@ileague/web` (Next 15), `@ileague/mobile` (Expo 52), `@ileague/convex`.

![Schema map](/blog/ileague-app-influencer-monorepo-before-golf-rebrand/screenshots/schema-map.png)

The schema still says **influencer**: `users.isInfluencer`, `influencerProfiles`, posts (text/image/video/poll/announcement), leagues with score+rank, follows, monthly/yearly `subscriptions`, Stripe Connect fields, tips in `transactions`. Twelve categories from gaming to lifestyle. Primary brand is violet (`#7c3aed` splash). The golf lander is emerald elsewhere.

![Mobile leagues](/blog/ileague-app-influencer-monorepo-before-golf-rebrand/screenshots/mobile-leagues.png)

Web: lander (Where Influencers and Fans Unite), Clerk auth, onboarding, dashboard, explore, leagues, notifications. Mobile tabs match. HEAD (Feb 4) wires mobile leagues to `getFeaturedLeagues` / `getUserLeagues` / paginated `getLeagues` + `joinLeague`, and adds `eas.json`.

![Gaps](/blog/ileague-app-influencer-monorepo-before-golf-rebrand/screenshots/honest-gaps.png)

Honest gaps: create-league button pushes `/create-league` with **no screen**; EAS submit + Apple IDs + Sentry org are placeholders; mobile icon/splash paths declared with **empty assets**; README claims MIT with **no LICENSE file**; lander vanity stats (50K+/2M+) are placeholders; commit message says Add mobile .env but the diff does not. `www.ileague.app` is a **Coming Soon** static page. The Jan Vercel preview URL now 308s there. The live golf product is a **different** repo and pack.

## Where

Code: [github.com/HurleyUS/ileague-app](https://github.com/HurleyUS/ileague-app), public.

```bash
git clone https://github.com/HurleyUS/ileague-app.git
cd ileague-app
bun install
# cp .env.example .env.local  # fill Clerk/Convex/Stripe/Resend/PostHog/Sentry
cd packages/convex && bunx convex dev
bun run dev:web    # or bun run dev:mobile
```

Domain: [www.ileague.app](https://www.ileague.app), Coming Soon (this Next app is not what serves that hostname). Golf sibling: [ileague.golf](https://ileague.golf). Keep the two repos separate in your head.

## When

**2026-01-09.** Five commits: init monorepo, Convex stubs/Sentry, CHANGELOG (notes `kindred-gnu-699` + early Vercel URL), React 18 for Clerk/Convex, root `vercel.json` monorepo build.

**2026-01-31.** `7b889f4` adds `.cursor/rules/STRIPE.md` only.

**2026-02-04 12:04 ET.** HEAD `7af9d80` mobile leagues + `eas.json` + ROADMAP/PLAN. Queue push **2026-02-05T18:08:47Z**.

![Commit arc](/blog/ileague-app-influencer-monorepo-before-golf-rebrand/screenshots/commit-arc.png)

## Why

If you only read the golf pack, you miss the **pre-rebrand** vocabulary: the same Bun/Next/Expo/Convex bones still wearing `isInfluencer` and vanity lander stats.

**Engagement Q:** Keep shipping the generic influencer scaffold, or treat this repo as archive and push all energy into ileague.golf?

---

# SalesPromis: GitHub still versions a WP Engine Elementor funnel — live left for Lovable

Source: https://www.michaelchurley.com/blog/salespromis-wp-elementor-dump-vs-lovable-live  
Published: 2026-01-31  
Author: Michael C. Hurley  
Tags: salespromis, wordpress, elementor, wp-engine, ssdi, lead-gen, lovable, hurleyus, salert, hurrytimer, martech

![WP Engine Elementor dump vs Lovable live domain](/blog/salespromis-wp-elementor-dump-vs-lovable-live/screenshots/wp-vs-lovable.png)

## Who

I needed a place to version the **SalesPromis production site** — the WordPress tree that actually ran the SSDI qualify funnels on WP Engine.

Who this write-up is for: operators who still have a private “production site” repo months after the public domain moved stack; builders who want to see how thin the custom layer is under an Elementor + Salert + HurryTimer kit; anyone who has ever opened GitHub expecting the live frontend and found a wp-content dump instead.

## What

I keep **HurleyUS/SalesPromis** private. README line one: **SalesPromis! Production Site.** Line three: “This is the production site for SalesPromis!”

What the tree actually is: a **WordPress wp-content-only** dump. Core, `wp-admin`, uploads, and `wp-config.php` are gitignored. Tracked: **7,723** files. GitHub disk ~36 MB. Linguist calls primary language **JavaScript** because Elementor’s JS weight wins — PHP is still ~16 MB of bytes.

Builder stack from the plugins directory: **Elementor 3.21.8**, Elementor Pro, Dynamic Content for Elementor (~45M on disk alone), Hello Elementor **3.0.2**, child theme **hello-theme-child-master 2.0.0**. Host signals: WP Engine `object-cache.php` (Memcached Redux) + `wpe-cache-plugin` mu-plugin + force-strong-passwords. Also in the kit: Really Simple SSL, Redirection, MonsterInsights, SVG Support, Admin Site Enhancements, **HurryTimer**, **Salert 1.2.5**.

![Custom ssdi-qualify.js enqueue + countdown/testimonial behavior](/blog/salespromis-wp-elementor-dump-vs-lovable-live/screenshots/ssdi-qualify-custom.png)

Custom operator code is small. `functions.php` enqueues `ssdi-qualify.js` only when the request is an Elementor `e-landing-page` whose permalink contains `ssdi-qualify`. The script (~294 lines) floors `.elementor-countdown-minutes` so the timer does not fall below **05**, prepends random faces from `100k-faces.glitch.me` into Salert wrappers, and renders two randomized “Qualify SSDI” testimonial cards (hardcoded first-name quotes + five-star glyphs) into Elementor widget `data-id="8c4d0c4"`. Salert’s own plugin header says it generates **fake sales notifications**.

There is **no** `package.json`. There is a **303-line** `.cursor/rules/STRIPE.md` (“How I Stay Sane Implementing Stripe”) with **zero** Stripe usage in the PHP/JS app tree.

![Plugin weight vs child-theme custom surface](/blog/salespromis-wp-elementor-dump-vs-lovable-live/screenshots/plugin-stack.png)

## Where

Code: [github.com/HurleyUS/SalesPromis](https://github.com/HurleyUS/SalesPromis) — **private**, org **HurleyUS**, default branch `main`, empty GitHub description and homepage fields. Local Projects checkout on m1Pro13 still lists origin `michaelmonetized/SalesPromis`; Brain `_src` tracks `HurleyUS/SalesPromis`. Same HEAD.

Live domain: [www.salespromis.com](https://www.salespromis.com/) — **HTTP 200**, Cloudflare, title **SalesPromis | AI-Powered Lead Generation - Pay Per Result**. HTML at pack time loads Vite-hashed `/assets/index-*.js|css` and `/lovable-uploads/…`. That is a **Lovable SPA**, not this WordPress tree. Canonical points at `https://www.salespromis.com/`. Portal copy elsewhere references a Tronador login — outside this repo.

Audience sits next to every client lead-gen site that got rebuilt on a new host while the old wp-content dump stayed the Git source of truth.

## When

**2024-08-15 15:48 ET** — `init` by Michael Monetized. Full dump: plugins, themes, mu-plugins, icons, GPL-3 `LICENSE.md`, three-line README.

**2026-01-31 05:37 ET** — `chore: sync all changes`. Diff is **one file**: `.cursor/rules/STRIPE.md` (+303). That is HEAD `c543864`. GitHub `pushed_at` 2026-01-31T10:39:16Z.

Two commits. Seventeen months between them. No Elementor upgrade commit in git history after init — whatever changed on the server between those dates did not land as a second content sync.

![Two-commit arc Aug 2024 → Jan 2026](/blog/salespromis-wp-elementor-dump-vs-lovable-live/screenshots/commit-arc.png)

## Why

Because a private wp-content dump is still useful as an **artifact** of the Elementor funnel even after the marketing domain moves — as long as you do not pretend the dump is still the live renderer.

Because the custom surface that mattered for the SSDI qualify landing page was one child-theme script and an enqueue gate, not the 45 MB Dynamic Content plugin folder.

Because the HEAD commit is a perfect operator scar: I synced a Stripe sanity rule into a WordPress lead-gen dump that never charged a card in-tree.

**Engagement Q:** When the README still says “production site” and the live HTML loads Lovable assets — which one should the content factory treat as the product under review?

---

# fab-analytics: same-day PHP+JS GA drop-in that writes JSON to disk

Source: https://www.michaelchurley.com/blog/fab-analytics-same-day-php-js-ga-drop-in-json-disk  
Published: 2024-07-10  
Author: Michael C. Hurley  
Tags: fab-analytics, first-party-analytics, google-analytics-drop-in, php, javascript, json-logs, elementor, hustlelaunch, form-abandonment, martech, michaelmonetized, hurleyus

## Who

I still get Elementor client sites where the ask is a drop-in that records who called, who emailed, and whether the form died halfway, without paying Google for a dashboard I will not open.

For operators who will accept JSON files on a PHP host as the source of truth.

## What

I shipped **fab-analytics**, public https://github.com/michaelmonetized/fab-analytics. HEAD `8218088`. **31** commits. Version **0.1.3-rc**.

![Client pipeline](/blog/fab-analytics-same-day-php-js-ga-drop-in-json-disk/client-pipeline.png)

`fab-analytics.js` (273 LOC) builds a visit object (domain, session token, viewport, UA, referrer, language, timezone, screen), hits ipify for IP, POSTs to a hardcoded `hustlelaunch.com/.../api/post/visit/` endpoint. Tracks pageview start/exit, `mailto:` / `tel:` clicks as conversions, form submit as conversion, and a noisy set of abandonment and presave events. Session lives in localStorage (+20 minutes) plus a cookie.

![PHP JSON ingest](/blog/fab-analytics-same-day-php-js-ga-drop-in-json-disk/php-json-ingest.png)

PHP `api/post/visit/index.php` validates `domain` + `session_token`, writes `logs/{domain}/{token}-{microtime}.json`, and if the domain contains `oxstu` and the category is form, includes a `mail()` lead path to sales@hustlelaunch.com.

![Test harness](/blog/fab-analytics-same-day-php-js-ga-drop-in-json-disk/test-harness.png)

`test.html` is a Tailwind CDN harness with tel, mailto, and a required name/phone/email form. Script tag still points at `/fab.js?v=0.1.3-b-36` while the tracked file is `fab-analytics.js`.

![Gaps honesty](/blog/fab-analytics-same-day-php-js-ga-drop-in-json-disk/gaps-honesty.png)

Honest gaps: README/CHANGELOG/LICENCE.md are empty (GPL only in headers). Header @todo claims restore form fields from localStorage on return; **no restore loop in the JS**. `JSON.stringify(new FormData(form))` is not a revive path. Abandonment listeners include blur/focus/mouseleave/touchmove. Endpoint echoes file path + payload (debug leftovers). CORS `*`.

Sibling surface to keep straight: BestWNC's 2026 null-honesty directory analytics is a different product. This repo is the PHP+JS GA drop-in that writes JSON under hustlelaunch.com.

## Where

Code: [github.com/michaelmonetized/fab-analytics](https://github.com/michaelmonetized/fab-analytics), public.

```bash
git clone https://github.com/michaelmonetized/fab-analytics.git
# drop fab-analytics.js on the page; PHP tree expects logs/ writable beside api/
# client endpoint constant points at hustlelaunch.com; change before self-host
```

## When

**2024-07-09 07:14 ET.** Empty init. Midday: base JS + endpoints + test. Evening: Tailwind thrash, trailing-slash facepalm, cache-busting, PHP error handling. **22:30 ET:** `tests passed - first production run`. **2024-07-10 09:07 ET:** cleanup, HEAD `8218088`. Queue `pushed_at` **2026-01-31T10:35:32Z** (no newer commits).

![Commit arc](/blog/fab-analytics-same-day-php-js-ga-drop-in-json-disk/commit-arc.png)

## Why

A first-party GA drop-in only earns its keep if conversions and abandonments land somewhere you own, even when that somewhere is a folder of JSON files.

**Engagement Q:** Fix the empty docs, fab.js rename, and FormData revive path first, or rip the abandonment spam down to submit plus intentional blur only?

---

# Jennings Custom Homes: post-malware WP rebuild checklist — still on Bluehost

Source: https://www.michaelchurley.com/blog/jenningscustomhomes-post-malware-checklist-still-on-bluehost  
Published: 2024-06-06  
Author: Michael C. Hurley  
Tags: jenningscustomhomes, wordpress, elementor, bluehost, malware-recovery, checklist, highlands-nc, cashiers-nc, custom-homes, hustle-launch, spf, martech

![Live home](/blog/jenningscustomhomes-post-malware-checklist-still-on-bluehost/screenshots/home-live.png)

## Who

I still get the call after Bluehost malware: rebuild the marketing site, keep the builder’s phone ringing, and put the work in git without committing `wp-content`.

Highlands / Cashiers luxury custom-home buyers need a gallery and a form. Operators need a checklist that survives a `.gitignore` that deletes the entire WordPress tree from the repo.

## What

I keep **jenningscustomhomes**. Public [Hustle-Launch/jenningscustomhomes](https://github.com/Hustle-Launch/jenningscustomhomes). HEAD `ad1f91f`. **5** commits. GitHub linguist empty. Tracked surface: **4** files.

![Repo surface](/blog/jenningscustomhomes-post-malware-checklist-still-on-bluehost/screenshots/repo-surface-composite.png)

README truth: *Design a new website following a malware infection due to a lack of security on Bluehost.* Next steps cover CSS cleanup, WP settings, CNAME, Analytics, Rank Math, socials, TrustIndex, form actions, SPF, login details.

![Checklist](/blog/jenningscustomhomes-post-malware-checklist-still-on-bluehost/screenshots/checklist-composite.png)

HEAD “Launched” flips nine boxes to `[x]`. Two stay honest: TrustIndex `[-] // client does not have any reviews`, and Send login details `[-]`.

Live product **https://www.jenningscustomhomes.com**. WordPress **7.1**, Hello Elementor **3.5.1**, child theme **`jch` 2.0.0**, Elementor **4.2.4**, Elementor Pro **3.24.4**, Elementor Contact form (name / email / message). Contact block: 83 Village Walk Wy, Cashiers, NC 28717 · **828-743-2307** · sam@jenningscustomhomes.com.

![Still Bluehost](/blog/jenningscustomhomes-post-malware-checklist-still-on-bluehost/screenshots/still-bluehost-composite.png)

Pack-day DNS: **75.98.174.238**, NS **ns1/ns2.bluehost.com**, LiteSpeed, PHP **8.3.33**, Let’s Encrypt, HTTP/2 **200**. SPF still includes `websitewelcome.com` plus `jch.hustlelaunch.com` and an A2 host. Malware story. Host unchanged.

![Gaps](/blog/jenningscustomhomes-post-malware-checklist-still-on-bluehost/screenshots/gaps-composite.png)

Honest gaps: `G-85CPJ4D64C` fires on about/contact/404, and does not fire on homepage HTML. Checklist claims Rank Math; home surface shows core `wp-sitemap.xml` only (no rank-math assets observed). About **Partners** block is still lorem ipsum. Repo gitignore means the live stack is HTTP-observable only.

## Where

Code: [github.com/Hustle-Launch/jenningscustomhomes](https://github.com/Hustle-Launch/jenningscustomhomes). Public, no license declared.

Product: [www.jenningscustomhomes.com](https://www.jenningscustomhomes.com). Cashiers / Highlands / Western NC custom luxury builder (marketing copy: in business since 2001).

```bash
git clone https://github.com/Hustle-Launch/jenningscustomhomes.git
# you get the checklist + gitignore, not wp-admin
```

## When

**2024-06-04 22:43 ET.** `833a3eb` init (.gitignore, empty README, workspace, SuperMaven recommend).

**22:47 ET.** `323c90a` empty “ready to launch” (same tree).

**2024-06-05 05:53 ET.** `7f7964d` checklist all open.

**06:30 ET.** `e36678b` preflight newline.

**2024-06-06 14:33 ET.** `ad1f91f` Launched to HEAD. Push `2024-06-06T18:34:29Z`.

**2026-09-08.** Pack day. Site live. Still Bluehost. Draft and assets only. Do not publish.

![Commit arc](/blog/jenningscustomhomes-post-malware-checklist-still-on-bluehost/screenshots/commit-arc-composite.png)

## Why

A malware rebuild that never leaves the host that got infected is a different story than a platform migration, and the README still tells it.

A public repo that gitignores WordPress is an ops checklist. Treat it like one, not like a content dump.

Checkboxes that leave TrustIndex and login handoff open are more honest than a fake green board.

Would you move NS off Bluehost next, or fix homepage GA injection and kill the About Partners lorem first?