<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Boniface Z</title>
    <description>The latest articles on DEV Community by Boniface Z (@bonifacez).</description>
    <link>https://dev.to/bonifacez</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4058853%2F6d78e94b-7807-47cf-9acf-0a48fe91941a.jpg</url>
      <title>DEV Community: Boniface Z</title>
      <link>https://dev.to/bonifacez</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bonifacez"/>
    <language>en</language>
    <item>
      <title>Teaching Grok Build Taste: Tailwind v4, Aceternity, and Project Rules That Ship</title>
      <dc:creator>Boniface Z</dc:creator>
      <pubDate>Sun, 02 Aug 2026 08:55:57 +0000</pubDate>
      <link>https://dev.to/bonifacez/teaching-grok-build-taste-tailwind-v4-aceternity-and-project-rules-that-ship-1nni</link>
      <guid>https://dev.to/bonifacez/teaching-grok-build-taste-tailwind-v4-aceternity-and-project-rules-that-ship-1nni</guid>
      <description>&lt;p&gt;I shipped &lt;a href="https://www.imagestretcher.io" rel="noopener noreferrer"&gt;ImageStretcher&lt;/a&gt; — a free, in-browser tool that stretches width and height independently, without cropping and &lt;strong&gt;without uploading&lt;/strong&gt; your file to a server — using Grok Build as the coding agent.&lt;/p&gt;

&lt;p&gt;Coding agents are great at “make it run.” They are mediocre at “make it look like a product” unless you give them something stronger than &lt;em&gt;please make it pretty&lt;/em&gt;. Pretty is not a type. Semantic tokens are. Install paths are. Motion presets are. Privacy constraints are.&lt;/p&gt;

&lt;p&gt;This article walks through the three levers that kept the UI honest on ImageStretcher:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS v4&lt;/strong&gt; as a token-first design surface
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aceternity + shadcn&lt;/strong&gt; behind a registry guardrail
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grok Build rules &amp;amp; skills&lt;/strong&gt; so the agent reloads taste every turn
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Stack in one line: React Router v7 SSR · Cloudflare Workers · Tailwind v4 · shadcn/ui · Aceternity UI · Motion springs · multi-locale · light/dark.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Write UI priority into the repo, not the chat
&lt;/h2&gt;

&lt;p&gt;Every session, the agent should see the same ladder. Ours lives in always-on Grok Build rules (&lt;code&gt;.grok/rules/00-index.md&lt;/code&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind first&lt;/strong&gt; — layout, spacing, type, responsive: utilities only. No parallel CSS framework.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aceternity next&lt;/strong&gt; — if the Aceternity registry has an equivalent, install that.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;shadcn &lt;code&gt;ui/*&lt;/code&gt; after that&lt;/strong&gt; — buttons, cards, predictable chrome.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand-roll last&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That order matters. Without it, an agent will invent a one-off dropzone, a second button system, and three blues. With it, the conversation becomes: “add the file stage” → skill loads → &lt;code&gt;npm run shadcn -- add @aceternity/…&lt;/code&gt; → component lands where the app actually imports from.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rules vs skills (Grok Build loading model)
&lt;/h3&gt;

&lt;p&gt;Grok Build picks up project guidance as Markdown under &lt;code&gt;.grok/&lt;/code&gt;: &lt;strong&gt;rules&lt;/strong&gt; stay in every session; &lt;strong&gt;skills&lt;/strong&gt; load when the task matches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.grok/
  rules/     # session-always: 00-index, ui-style, core, file-protocol
  skills/    # on-demand: shadcn-config, apple-spring, network-proxy, seo-*, …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;What we put there&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rules&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.grok/rules/*.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Always on&lt;/td&gt;
&lt;td&gt;UI priority, visual constitution (&lt;code&gt;ui-style&lt;/code&gt;), doc loop, file headers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Skills&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.grok/skills/*/SKILL.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Task-triggered&lt;/td&gt;
&lt;td&gt;How to install shadcn/Aceternity, spring motion, SEO step logging, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Putting taste in the repo — not only in chat memory — is the difference between “agent remembers the product” and “agent rediscovers Bootstrap energy every prompt.”&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Tailwind v4: tokens are the taste API
&lt;/h2&gt;

&lt;p&gt;ImageStretcher is CSS-first Tailwind v4. Vite plugs it in with &lt;code&gt;@tailwindcss/vite&lt;/code&gt;; the design system lives in &lt;code&gt;app/app.css&lt;/code&gt;. &lt;code&gt;components.json&lt;/code&gt; points shadcn at that file and leaves &lt;code&gt;tailwind.config&lt;/code&gt; empty — v4 does not need the old JS config for this setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Entry + dark variant + theme bridge
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;@custom-variant&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dark&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="nb"&gt;inline&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--font-sans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"DM Sans"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ui-sans-serif&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system-ui&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Fraunces"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ui-serif&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Georgia&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="py"&gt;--color-background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--background&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--foreground&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-primary-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--primary-foreground&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c"&gt;/* …muted, card, border, sidebar, radius… */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three choices worth stealing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Class-based dark, not media-only.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Theme toggle writes &lt;code&gt;html.dark&lt;/code&gt;. The custom variant makes &lt;code&gt;dark:&lt;/code&gt; utilities follow that class (and descendants). Users who want “system” still go through the same theme contract; the stylesheet does not fork into two mental models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;@theme inline&lt;/code&gt; maps CSS variables to utilities.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Authors (human or agent) write &lt;code&gt;bg-background&lt;/code&gt;, &lt;code&gt;text-muted-foreground&lt;/code&gt;, &lt;code&gt;font-display&lt;/code&gt; — not &lt;code&gt;#f7f4ef&lt;/code&gt; in twelve files. When light/dark swap, components do not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic chrome only.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Light is warm paper (&lt;code&gt;--background: #f7f4ef&lt;/code&gt;). Dark is stone (&lt;code&gt;#0c0a09&lt;/code&gt;). Primary is teal (&lt;code&gt;#0f766e&lt;/code&gt; / dark &lt;code&gt;#2dd4bf&lt;/code&gt;). The UI rule is explicit: tool chrome does &lt;strong&gt;not&lt;/strong&gt; get sky/amber/red decoration for “fun.” Status feedback stays muted gray scale or a light primary mix. That single ban kills most AI-generated carnival UIs.&lt;/p&gt;
&lt;h3&gt;
  
  
  Root font-size 125% (density without &lt;code&gt;zoom&lt;/code&gt;)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* ≈ Cmd++; rem (including Tailwind spacing) scales together — not zoom */&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;125%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;scroll-behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;smooth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;@apply&lt;/span&gt; &lt;span class="err"&gt;bg-background&lt;/span&gt; &lt;span class="err"&gt;text-foreground&lt;/span&gt; &lt;span class="err"&gt;font-sans&lt;/span&gt; &lt;span class="err"&gt;antialiased;&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Tool density (labels ~&lt;code&gt;0.6875rem&lt;/code&gt;, pills ~&lt;code&gt;0.625rem&lt;/code&gt;, toolbar &lt;code&gt;Button size="xs"&lt;/code&gt;) is specified in rem under &lt;code&gt;ui-style&lt;/code&gt;. Bumping the root scales the whole product like accessibility zoom, without breaking &lt;code&gt;position: fixed&lt;/code&gt; the way CSS &lt;code&gt;zoom&lt;/code&gt; can.&lt;/p&gt;
&lt;h3&gt;
  
  
  Surfaces: one card language
&lt;/h3&gt;

&lt;p&gt;We use shadcn &lt;code&gt;Card&lt;/code&gt; / a small &lt;code&gt;surface-card&lt;/code&gt; helper — soft border via &lt;code&gt;color-mix&lt;/code&gt;, light shadow, no &lt;strong&gt;card-inside-card&lt;/strong&gt;. Hierarchy is background steps and &lt;code&gt;ring-1 ring-foreground/5&lt;/code&gt;, not hard border walls between every section.&lt;/p&gt;

&lt;p&gt;Atmosphere (soft primary radials + noise) is a single &lt;code&gt;.app-atmosphere&lt;/code&gt; layer on the shell. Agents are not invited to invent a new gradient per section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway for agent workflows:&lt;/strong&gt; if the only legal colors are semantic tokens, &lt;code&gt;bg-blue-500&lt;/code&gt; becomes a lint smell in review — and often never appears.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Aceternity + shadcn: registry, path guard, then tame the demo
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;components.json&lt;/code&gt; that matches the framework
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"style"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"new-york"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rsc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tailwind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"css"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app/app.css"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"baseColor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zinc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cssVariables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"registries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@aceternity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://ui.aceternity.com/registry/{name}.json"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;rsc: false&lt;/code&gt;&lt;/strong&gt; — this is React Router SSR, not Next App Router RSC.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@aceternity&lt;/code&gt; registry&lt;/strong&gt; — same CLI surface as official shadcn, different package source.
&lt;/li&gt;
&lt;li&gt;Aliases point at &lt;code&gt;@/components&lt;/code&gt; → &lt;code&gt;app/components&lt;/code&gt; in practice.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Never bare &lt;code&gt;npx shadcn&lt;/code&gt; in this monorepo
&lt;/h3&gt;

&lt;p&gt;The stock CLI loves dumping files into repo-root &lt;code&gt;components/ui&lt;/code&gt; or &lt;code&gt;src/components/ui&lt;/code&gt;. Our app only imports from &lt;strong&gt;&lt;code&gt;app/components/ui&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So installs go through a project script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run shadcn &lt;span class="nt"&gt;--&lt;/span&gt; add button
npm run shadcn &lt;span class="nt"&gt;--&lt;/span&gt; add @aceternity/&amp;lt;name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;scripts/shadcn-add.mjs&lt;/code&gt; runs &lt;code&gt;npx shadcn@latest …&lt;/code&gt;, then &lt;strong&gt;moves&lt;/strong&gt; any files that landed in the wrong &lt;code&gt;components/ui&lt;/code&gt; trees back into &lt;code&gt;app/components/ui&lt;/code&gt;. That logic is documented in the on-demand skill &lt;code&gt;.grok/skills/shadcn-config/SKILL.md&lt;/code&gt; so Grok does not “helpfully” bypass the guard.&lt;/p&gt;

&lt;p&gt;Agent constraints from that skill (abridged):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prefer &lt;code&gt;npm run shadcn -- add&lt;/code&gt; over hand-pasting registry source.
&lt;/li&gt;
&lt;li&gt;Never create UI under repo-root &lt;code&gt;components/&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;After add, strip Aceternity demo decoration (blue dashed frames, etc.) to semantic tokens.
&lt;/li&gt;
&lt;li&gt;Keep motion parameters restrained in tool chrome.
&lt;/li&gt;
&lt;li&gt;Update L3 file headers + &lt;code&gt;AGENTS.md&lt;/code&gt; when UI files change.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Real component: FileUpload on the stretch stage
&lt;/h3&gt;

&lt;p&gt;The preview empty state is an Aceternity-style drop zone (&lt;code&gt;react-dropzone&lt;/code&gt; + &lt;code&gt;motion&lt;/code&gt; + &lt;code&gt;@tabler/icons-react&lt;/code&gt;), consumed by &lt;code&gt;stretch-preview.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FileUpload&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;onFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What we changed from a generic demo:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Demo default&lt;/th&gt;
&lt;th&gt;Product rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Any file / multi&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;image/*&lt;/code&gt; only, single file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hard-coded English&lt;/td&gt;
&lt;td&gt;&lt;code&gt;getTranslations(useLocale()).stretch.upload&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“Upload to our product” vibe&lt;/td&gt;
&lt;td&gt;Local open only — &lt;strong&gt;Worker never receives the bitmap&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decorative blue&lt;/td&gt;
&lt;td&gt;Grid + semantic foreground/muted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleFileChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newFiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;images&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newFiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;setFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy still says “upload” in the UI sense of &lt;em&gt;choose a file&lt;/em&gt;. Privacy copy is unambiguous: processing is canvas-in-browser; leaving the page drops in-memory previews.&lt;/p&gt;

&lt;h3&gt;
  
  
  shadcn owns chrome; Aceternity owns the moment
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;shadcn:&lt;/strong&gt; &lt;code&gt;Button&lt;/code&gt; (including dense &lt;code&gt;xs&lt;/code&gt; toolbar actions), &lt;code&gt;Card&lt;/code&gt;, form controls.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aceternity-style stage:&lt;/strong&gt; the file drop experience users remember.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Motion:&lt;/strong&gt; not per-component magic numbers. Shared presets in &lt;code&gt;app/lib/motion.ts&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;snappy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Transition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spring&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;stiffness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;damping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tapScale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;pressed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spring&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stiffness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;damping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gentle&lt;/code&gt; / &lt;code&gt;bouncy&lt;/code&gt; / &lt;code&gt;smooth&lt;/code&gt; cover panels and entrances. The &lt;code&gt;apple-spring&lt;/code&gt; skill exists so the agent does not invent a fourth spring language mid-PR.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Rules that encode taste; skills that encode procedures
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why “make it pretty” fails
&lt;/h3&gt;

&lt;p&gt;Unconstrained generation optimizes for novelty: gradients, absolute badges, inconsistent radii, random accent colors. A tool site needs the opposite — &lt;strong&gt;calm, dense, predictable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ui-style&lt;/code&gt; (always-on) fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;semantic tokens only for chrome
&lt;/li&gt;
&lt;li&gt;surface / no nested cards
&lt;/li&gt;
&lt;li&gt;rem density scale
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;select-none&lt;/code&gt; on chrome; explicit select on inputs
&lt;/li&gt;
&lt;li&gt;flex height chains with &lt;code&gt;min-h-0&lt;/code&gt; so preview panes do not collapse on mobile
&lt;/li&gt;
&lt;li&gt;brand type: Fraunces display + DM Sans body
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are reviewable. An agent can be wrong against a rule; it cannot be wrong against “vibes.”&lt;/p&gt;

&lt;h3&gt;
  
  
  File headers as micro-contracts (L3)
&lt;/h3&gt;

&lt;p&gt;Business files carry a short header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * [INPUT]: Depends on motion, @tabler/icons-react, react-dropzone, ~/lib/utils, ~/i18n
 * [OUTPUT]: Exports FileUpload, GridPattern
 * [POS]: components/ui Aceternity-style drop zone; consumed by StretchPreview
 * [PROTOCOL]: On change, update this header, then check AGENTS.md
 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agents search POS/OUTPUT when deciding where to edit. Humans get a map. Directory-level &lt;code&gt;AGENTS.md&lt;/code&gt; lists members in one line each. The loop is: change code → refresh header → refresh AGENTS if files moved. We do not paste the whole philosophy doc into every PR — but we do keep the &lt;strong&gt;contract surface&lt;/strong&gt; machine-readable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The iteration that actually happened
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Skeleton prompt&lt;/strong&gt; — stretcher, local canvas, no server upload → runnable forms, not shippable UI.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI ladder + shadcn-config skill&lt;/strong&gt; — install into &lt;code&gt;app/components/ui&lt;/code&gt;, not random folders.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens + ui-style&lt;/strong&gt; — drop demo blues; primary teal; paper/stone themes.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy as a hard constraint&lt;/strong&gt; — no “upload API” invention; Worker only SSR + static.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout rule&lt;/strong&gt; — &lt;code&gt;min-h-0&lt;/code&gt; / flex chain so mobile preview matches desktop calm.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ugly first drafts can exist on disk for ten minutes. &lt;strong&gt;Ugly is not fine on the domain.&lt;/strong&gt; The rules exist to make that bar enforceable when the agent is moving faster than your eyeballs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy note (one paragraph)
&lt;/h3&gt;

&lt;p&gt;The app is React Router SSR on a Cloudflare Worker. Images never hit the Worker as uploads; stretch/crop run in the browser. That product decision is also an agent decision: if rules say “no upload backend,” Grok should not scaffold one “for later.”&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Checklist you can copy into your own agent repo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Always-on UI priority&lt;/strong&gt; (utility CSS → registry showpiece → design-system primitives → hand-roll)
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Semantic tokens + dual theme&lt;/strong&gt; (&lt;code&gt;@theme inline&lt;/code&gt; + &lt;code&gt;:root&lt;/code&gt; / &lt;code&gt;.dark&lt;/code&gt;)
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Class-based dark&lt;/strong&gt; aligned with your theme provider
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Registry install guard&lt;/strong&gt; so components land in the path your aliases use
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Strip demo decoration&lt;/strong&gt; from third-party registry components
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Single motion vocabulary&lt;/strong&gt; (one &lt;code&gt;motion.ts&lt;/code&gt;, one skill)
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Non-negotiable product rules&lt;/strong&gt; (ours: no server upload, stretch ≠ crop)
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Always-on rules under &lt;code&gt;.grok/rules/&lt;/code&gt;&lt;/strong&gt; so every Grok Build session reloads the same constitution
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;On-demand skills&lt;/strong&gt; under &lt;code&gt;.grok/skills/&lt;/code&gt; for procedures (install, SEO, proxy) so always-on rules stay short
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. Closing
&lt;/h2&gt;

&lt;p&gt;Grok Build did not replace design judgment. It compressed the loop between judgment and pixels.&lt;/p&gt;

&lt;p&gt;Tailwind v4 gave us a &lt;strong&gt;typed palette&lt;/strong&gt; in CSS. Aceternity + shadcn gave us &lt;strong&gt;velocity with a path guard&lt;/strong&gt;. Rules and skills gave the agent a &lt;strong&gt;constitution&lt;/strong&gt; it reloads every turn instead of a fading chat memory.&lt;/p&gt;

&lt;p&gt;If you want to stretch an image without cropping — width and height free or locked, preview on canvas, download PNG/JPEG, files stay on your device — use the tool:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.imagestretcher.io" rel="noopener noreferrer"&gt;https://www.imagestretcher.io&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Public pitch / links: &lt;a href="https://github.com/Bonifacez/imagestretcher.io" rel="noopener noreferrer"&gt;github.com/Bonifacez/imagestretcher.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ship the skeleton fast. Enforce taste in the repo. The domain is what users judge — not your first prompt.&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
  </channel>
</rss>
