I built a ultra-polished developer portfolio template using React & Tailwind v4 (with zero-JSX configuration)

Hey everyone! 👋

I spent the last couple of days engineering a minimal, premium dark-mode portfolio template for developers who want a high-end web presence (similar to the aesthetic of companies like Stripe or Linear) without spending weeks designing it from scratch.

I wanted to push performance to the absolute limit, so I built it using the latest frontend stack.

⚙️ The Technical Stack

  • Framework: React 18 (Modular Component Architecture)
  • Build Tool: Vite (For instant local hot reloading)
  • Styling: Tailwind CSS v4 (Leveraging the brand-new native lightning compiler plugin)
  • Animations: Framer Motion (Hand-tuned for fluid layouts)

🚀 Check it out!

I’d love to get your feedback on the layout performance, the Framer Motion animation timings, or the Tailwind v4 compilation speeds.

Let me know what you think in the comments below! If you have any questions about upgrading a project to Tailwind v4, drop them here and I’ll help out.

Gemini CLI Is Dead. Here’s the Better Thing That Replaced It

This is a submission for the Google I/O Writing Challenge

I opened my terminal on May 20th, ran gemini, and got a deprecation notice.

Google shipped Antigravity CLI at I/O 2026 — and killed Gemini CLI the same day. If you use Gemini CLI in your workflow, this already affects you. If you’ve never touched either, stick around anyway. The mental model shift here is worth understanding before you need it.

Why Replace Gemini CLI at All?

Gemini CLI was a prompt interface. You send a prompt, you get a response. That’s the whole model.

Antigravity CLI is an agent harness. The primitive isn’t a prompt — it’s a running agent that can spawn sub-tasks, use tools, and keep working while you do something else.

That’s not a rename. That’s a different abstraction.

The migration is clean though: everything that mattered in Gemini CLI carries over. Skills, Hooks, Subagents — all preserved. Extensions are now called Antigravity Plugins, but the concept is identical. If you had Gemini CLI workflows, the port is mechanical.

Install in 30 Seconds

curl -fsSL https://antigravity.google/cli/install.sh | bash
agy --version

No API key prompt during install. Auth happens the first time you invoke an agent. If you have an existing ~/.gemini/ directory, the CLI detects it and imports your Skills automatically.

First Command: agy inspect

Before you run anything, run this:

agy inspect

It shows you everything the CLI sees — config files loaded, Skills available (global and workspace), Hooks wired up, plugins installed. Think of it as git status for your agent environment.

Antigravity CLI v2.0.1

Configuration
  Global config:    ~/.antigravity/config.json
  Workspace config: .agents/config.json (not found)

Skills (global)
  code-review        ~/.antigravity/skills/code-review.md
  commit-message     ~/.antigravity/skills/commit-message.md

Skills (workspace)
  None

Hooks
  None configured

Run it first in any new project. It’ll save you 20 minutes of debugging why a Skill isn’t loading.

Skills: Saved Agent Roles

A Skill is a markdown file that gives the agent a role, tool access, and task framing. A saved system prompt, with metadata.

Create a workspace Skill:

mkdir -p .agents/skills
<!-- .agents/skills/ux-review.md -->
---
name: ux-review
description: Reviews a UI screenshot against Nielsen's heuristics
tools: [read_file]
---

You are a UX analyst. Analyze any UI screenshot against:
- Nielsen's 10 Usability Heuristics
- WCAG 2.1 accessibility guidelines
- Cognitive load principles

Return structured findings: what's working, what's broken, and the specific heuristic being violated.

Invoke it:

agy run --skill ux-review "Review the checkout flow in checkout.png"

Skills in .agents/skills/ travel with the project. Skills in ~/.antigravity/skills/ are global. The community Antigravity Awesome Skills repo has 1,400+ pre-built ones if you’d rather not start from scratch.

Bold opinion: The community Skills library is actually the most underrated thing about this ecosystem. You can drop in a production-grade code-review workflow in 90 seconds. Most people skip it and hand-write prompts forever.

Hooks: Intercept the Agent Loop

Hooks let you fire shell commands at lifecycle moments. Config is JSON:

// .agents/config.json
{
  "hooks": {
    "before_tool_call": [
      { "command": "echo '$TOOL_NAME' >> .agents/tool-log.txt" }
    ],
    "on_loop_stop": [
      { "command": "node scripts/notify.js" }
    ]
  }
}

Available hook points: before_tool_call, after_model_call, on_loop_stop, on_error.

The pattern I use on every project: on_loop_stop writes a summary to a log file, before_tool_call audits tool use so I can see what the agent actually did. Thirty lines of config, total.

Subagents: The Part Gemini CLI Couldn’t Do

This is the actual upgrade.

Gemini CLI was synchronous. You waited. The agent finished. You moved on. Antigravity CLI ships async subagents — dispatch a long-running task to a background agent and keep prompting in the foreground, same session.

From inside the TUI:

> Refactor the auth module to use the new token schema.
  [Dispatching to subagent...]

> While that runs — write a changelog entry for the last 10 commits.

Both run in parallel. The subagent reports back when it’s done.

From the CLI:

# Dispatch a background agent
agy dispatch "Run the full test suite and write a summary to test-report.md"

# Check active subagents
agy agents list

# Pull output from a specific agent
agy agents output <agent-id>

I Tried It: Parallelizing a Real Refactor

Here’s what I actually tested. I had a Next.js project with a type refactor that touched ~40 files.

I dispatched the refactor to a background subagent, then used the foreground agent to write the PR description and update the changelog — simultaneously.

The refactor completed in about 4 minutes. The PR description and changelog were ready before the refactor finished. Total wall-clock time: 4 minutes instead of 8–10 in sequence.

That’s not a benchmark. That’s just what parallel async agents feel like in practice.

Managed Agents: Serverless Version

Don’t want the CLI? There’s an API:

import google.generativeai as genai

genai.configure(api_key="YOUR_API_KEY")

agent = genai.create_agent(
    model="gemini-3.5-flash",
    instructions="You are a code reviewer. Review the diff for correctness and security issues.",
    tools=["code_execution"],
)

result = agent.run("Review this diff:n" + open("changes.diff").read())
print(result.output)

Same agent harness. Same Gemini 3.5 Flash intelligence. Zero local setup. The agent runs in an isolated Linux environment on Google’s infrastructure.

Migration Cheat Sheet

Gemini CLI Antigravity CLI
gemini run agy run
~/.gemini/skills/ ~/.antigravity/skills/
Extensions Plugins (same concept)
gemini inspect agy inspect
Synchronous only agy dispatch for async

Skills files don’t need to change. The biggest behavioral win: agy is built in Go. Startup is noticeably faster than the old CLI.

Bold opinion: The deprecation of Gemini CLI felt abrupt. But Google made the right call — maintaining two separate CLIs with diverging capability models would have been worse for the ecosystem than a clean cut.

What This Is Really About

Gemini CLI was a terminal wrapper around a language model.

Antigravity CLI is a terminal interface for an agent harness. The Skills/Hooks/Plugins model is also cross-compatible with Claude Code, Cursor, and Codex CLI — a Skill you write here works elsewhere.

The transition was abrupt. The thing that replaced it is better.

Antigravity CLI at antigravity.google. Codelabs at codelabs.developers.google.com.

Tags: googleio ai productivity devtools

Post-quantum cryptography for embedded and IoT: secure boot, TLS and OTA

Post-quantum cryptography is no longer just a research topic. It is starting to affect the way embedded teams design TLS, secure boot, OTA, firmware signing, device identity and long-term product maintenance.

NIST has finalized the first post-quantum standards. OpenSSL 3.5 now includes ML-KEM, ML-DSA and SLH-DSA support. The European roadmap points toward a coordinated transition, and embedded vendors are already moving PQC into MCU and firmware workflows.

For connected products that may stay in the field for 10, 15 or 20 years, this is not abstract security theater. It is architecture.

Why embedded teams should care

Embedded products freeze cryptographic choices earlier than many teams expect:

  • bootloader verification logic
  • firmware image and manifest formats
  • OTA package signatures
  • device certificates
  • production PKI
  • secure elements and trust anchors
  • TLS or VPN libraries in Linux gateways
  • update and rollback policies

Once the device is deployed, changing those choices becomes expensive. Sometimes it becomes almost impossible without a carefully designed migration path.

That is the real value of post-quantum planning: not replacing RSA and ECC everywhere overnight, but introducing crypto agility before the product becomes too rigid.

ML-KEM and ML-DSA in plain terms

The two names embedded teams should recognize first are:

  • ML-KEM: a key encapsulation mechanism for establishing shared secrets, especially relevant to TLS and similar protocols.
  • ML-DSA: a digital signature scheme, relevant to secure boot, firmware signing, package signing, certificates and device identity.

For Linux gateways, ML-KEM is often the first practical entry point because TLS stacks can be tested and upgraded more easily than immutable boot chains.

For firmware and boot flows, ML-DSA is very relevant but needs more careful engineering. Signature sizes, verification time, image layout and manifest formats all matter.

Where PQC enters an embedded architecture

Area What changes Why it matters
TLS and networking Hybrid groups, new key establishment, library updates Gateways and edge devices can start testing now
Secure boot Signature verification may need post-quantum readiness Boot chains are hard to change after deployment
OTA Manifests, package signing and rollback policies may need new formats Update reliability and security are part of the same trust chain
PKI Certificates, provisioning and trust anchors need migration planning Device identity is a long-term product dependency
Memory budget Stack, heap, flash and latency must be measured Papers and release notes are not a substitute for target testing

A practical adoption path

Do not turn on PQC everywhere and hope for the best. A healthier path looks like this:

  1. Inventory every cryptographic dependency in the product.
  2. Map TLS, VPN, secure boot, OTA, package signing, certificates and PKI.
  3. Identify code paths that cannot be updated after manufacturing.
  4. Run a Linux gateway pilot with OpenSSL 3.5 or lab tools such as Open Quantum Safe.
  5. Measure ML-KEM and ML-DSA impact on real hardware.
  6. Review image formats, manifests, rollback and recovery paths.
  7. Define a policy for trust anchor rotation and crypto agility.
  8. Move only the justified parts into production.

Example checklist

pqc_embedded_audit:
  lifecycle:
    expected_field_life_checked: true
    non_updatable_signature_verifier_identified: true

  protocols:
    tls_or_vpn_usage_mapped: true
    certificates_and_pki_inventory_done: true

  firmware_chain:
    secure_boot_flow_reviewed: true
    ota_manifest_and_signature_format_reviewed: true
    rollback_and_recovery_paths_verified: true

  implementation:
    hybrid_transition_need_evaluated: true
    stack_heap_flash_measured_on_real_target: true
    latency_variance_measured: true

  operations:
    trust_anchor_rotation_plan_available: true
    crypto_agility_requirements_defined: true
    release_and_support_workflow_documented: true

When PQC makes sense

PQC planning is most useful when the product is:

  • connected
  • updateable
  • deployed for a long time
  • dependent on secure boot, OTA, certificates or secure networking
  • expensive to access physically
  • subject to compliance or long support windows

That makes Linux gateways, edge appliances, industrial IoT devices and remotely maintained firmware platforms natural candidates for early evaluation.

Where to be careful

PQC is not automatically the right move for every MCU or every firmware build.

Very constrained devices may have strict limits around stack, heap, flash, latency or power. Hybrid approaches can help with migration, but they also add complexity and testing cost. The goal is not to put post-quantum algorithms everywhere. The goal is to know where they reduce real product risk.

Final takeaway

Post-quantum cryptography is becoming part of embedded product architecture. The smartest move today is not panic migration; it is inventory, measurement and crypto agility.

Teams that understand their boot chain, OTA process, PKI and field lifecycle now will have a much easier transition later.

Canonical source: Post-quantum cryptography for embedded and IoT: secure boot, TLS and OTA

Silicon LogiX helps teams review embedded Linux, secure boot, firmware signing, OTA and security architecture for connected products.

Understanding Optimistic Preloading in Modern Applications

In modern software development, performance and user experience play a major role in application design. Users expect applications to respond instantly, and even a small delay can impact engagement. One technique used to improve responsiveness is Optimistic Preloading.

What is Optimistic Preloading?

Optimistic preloading is a technique where an application loads data or resources before the user explicitly requests them. The system predicts what the user is likely to do next and prepares the required content in advance.

The term optimistic comes from the assumption that the prediction will most likely be correct.

Simple Example

Consider an e-commerce website:

  • A user hovers over a product link
  • The application silently starts loading the product page in the background
  • When the user clicks the link, the page opens almost instantly

Since the data was already loaded, the user experiences a much faster response time.

Where is it used?

Optimistic preloading is commonly used in:

  • Web applications
  • Mobile applications
  • Video streaming platforms
  • Gaming systems
  • APIs and database queries

Frameworks like React, Next.js, and many modern browsers use similar strategies to improve performance.

Advantages

  • Faster perceived
  • performance
  • Better user experience
  • Reduced waiting time
  • Smoother navigation between pages

Challenges

While optimistic preloading improves speed, it also has some trade-offs:

  • Increased bandwidth usage
  • Additional memory consumption
  • Wasted resources if predictions are incorrect

Because of this, applications must carefully decide what should be preloaded.

Conclusion

Optimistic preloading is a smart optimization technique that helps applications feel faster and more responsive. By prediction user behavior and loading resources in advance, developers can significantly improve the overall user experience.

As applications continue to focus on speed and seamless interactions, techniques like optimistic preloading are becoming increasingly important in modern software development.

Nobody Wants to Read Your Code (And You Don’t Want to Read Theirs)

There is a thing that happens when you join a new codebase. You open a file, read maybe fifteen lines, and then you close it. Not because you understood it. Because you stopped wanting to understand it.

I think about this a lot. We talk about clean code, readable code, well-documented code, as if the problem is always on the writing side. But the reading side has its own psychology and almost nobody talks about it honestly.

Reading someone else’s code is uncomfortable in a way that is hard to name. It is not just that the logic is unfamiliar. It is that you are stepping inside someone else’s head. Their assumptions are baked into every variable name. Their shortcuts make sense only if you know what they were rushing toward. Their weird choice of a for loop where a while loop would feel more natural is probably the result of a bug they fixed at 11pm six months ago and never bothered to refactor. You are not reading code. You are reading a person.

And most of us are not trained for that. We are trained to write, to produce, to ship. Reading feels passive. Reading someone else’s bad code feels like losing. Reading someone else’s good code feels even worse because then you have to sit with the fact that you would not have done it that way and you are not sure if your way is actually better.

There is also the time problem. When you write code, you have context. You know what the function is for, what came before it, what will come after. When you read, you have to build that context from scratch and the codebase usually does not help you. Comments are either missing or lying. Variable names like data and result and temp are everywhere. The folder structure made sense to the person who created it and to nobody else.

So you do what most people do. You read just enough to do your task. You make your change in the one file you actually understand. You submit your PR and you move on.

The problem is that this pattern compounds. The person after you does the same thing. And the person after them. Over time the codebase becomes a city where everybody knows their own neighborhood and nobody knows the roads in between. When something breaks in the space between neighborhoods, it takes three people and a long afternoon to figure out why.

I do not think the solution is to force people to read more code through code reviews or documentation requirements. Those things help but they do not fix the underlying discomfort. The discomfort is real and it deserves to be taken seriously.

What has actually helped me is reading code the way I read a book I am not enjoying. Not to finish it. Not to extract every piece of information. Just to follow one thread. Pick one function. Understand what it calls. Understand what calls it. Stop there. Do that a few times over a few days and the codebase starts to feel less like a foreign country.

The other thing that helps is accepting that confusion is not a sign of failure. The confusion you feel when you open an unfamiliar file is the same confusion the person who wrote it felt when they started. They just had more time. Time is the only real difference between a file that makes sense and one that does not.

We should probably talk more openly about how hard reading code actually is. Not as a complaint but as an acknowledgment. It is a skill that takes practice and patience in a field that rewards speed. That tension is not going anywhere. Might as well be honest about it.

A clothing pairing app

This is a submission for the Gemma 4 Challenge: Build with Gemma 4

What I Built

A clothing pairing app — upload a photo of one piece you own, get a complementary match from a curated catalogue, then see both styled together in an AI-generated editorial image.

The product catalogue is the Myntra fashion dataset (~44k products), available at kaggle.com/datasets/paramaggarwal/fashion-product-images-dataset.

Demo

Code

Pair.

A clothing pairing app — upload a photo of one piece you own, get a complementary match from a curated catalogue (which is downloaded at https://www.kaggle.com/datasets/paramaggarwal/fashion-product-images-dataset), then see both styled together in an AI-generated editorial image.

How it works

  1. Upload a photo of a top, bottom, shoes, or accessory
  2. Analyze — Gemma 4 identifies the piece: category, gender, style, palette
  3. Match — a complementary item is selected from ~44k Myntra products via MongoDB + Gemma 4
  4. Lookbook — Gemini generates a fashion editorial photo of both pieces styled on a model

Stack

Layer Tech
Frontend Vite + React + TypeScript + Tailwind CSS v4
Backend Hono on Node.js
AI Google GenAI SDK (Gemma 4 + Gemini)
Database MongoDB (Myntra product dataset)
Bot protection Google reCAPTCHA v3

Prerequisites

  • Node 24 (use nvm use — .nvmrc is included)
  • MongoDB running locally
  • Google AI Studio API key — aistudio.google.com
  • Google reCAPTCHA…

How I Used Gemma 4

Gemma 4 (gemma-4-31b-it) is used for two tasks where we need text output from a structured prompt, not image generation:

Clothing analysis — given an image, return a JSON object with category, gender, style, colors, etc.
Match selection — given 30 product candidates from MongoDB, pick the best one and return a JSON object with the index, match score, and stylist note.

E2B vs E4B vs 31B Dense: The Practical Guide to Choosing the Right Gemma 4 Model

This is a submission for the Gemma 4 Challenge: Write About Gemma 4

I want to tell you about the moment I stopped treating local AI as a compromise.

I was building a UX analysis tool. The idea was simple: give it a screenshot, get back a structured critique grounded in Nielsen’s heuristics, cognitive load theory, WCAG — the real frameworks, not vibes. I’d used cloud models before. They work. But they also mean your users’ screenshots leave their machine, hit an API, and cost money per call.

So I pulled Gemma 4 E4B locally via Ollama, pointed it at Stripe.com, and waited.

Fifty-six seconds later it handed me a structured JSON object with an overall UX score, three specific friction points each mapped to a Nielsen heuristic, accessibility flags, layout analysis, and prioritized recommendations with effort and impact ratings.

On CPU. No GPU. No API key. No data leaving the machine.

That’s when the compromise framing broke for me. This isn’t a fallback. It’s a capability shift.

What Gemma 4 Actually Is

Gemma 4 is Google’s latest open-weights model family, and the headline feature is multimodality — it can see images, not just read text. But the more interesting story is the range of what Google shipped:

Variant Parameters Context Window Best For
E2B ~2B 8K Edge, mobile, fast inference
E4B ~4B 8K Local development, CPU-viable
31B Dense ~31B 128K Complex reasoning, long documents

Three very different tools with the same name. Picking wrong costs you either quality or speed. Let me tell you what I’ve actually learned about each.

E2B: The One That Fits Anywhere

The 2-billion-parameter model is built for constraints. Think browser extensions, mobile apps, Raspberry Pi, anything where you cannot afford to wait or cannot carry a heavy runtime.

It handles basic instruction-following and short-form generation well. What it struggles with is structured output — complex JSON schemas, multi-step reasoning chains, tasks that require holding a lot of context simultaneously. If your task is “summarize this paragraph” or “classify this text into one of five categories,” E2B is excellent and blazingly fast. If your task requires the model to reason across multiple frameworks while producing a nested JSON object, you’ll feel the ceiling.

Use E2B when: speed > depth, you’re running on constrained hardware, or the task is classifying/summarizing rather than reasoning.

E4B: The Sweet Spot for Developers

This is the one I use. The 4-billion-parameter model is the first point in the family where you stop noticing the limitations on most practical tasks.

What changed for me: E4B can follow a complex, multi-section JSON schema reliably. I give it a system prompt that defines eight nested fields — cognitiveLoad, trustScore, frictionPoints, recommendations, accessibilityFlags, layoutAnalysis — and it fills them correctly, with real observations, not hallucinated filler.

It also handles format: "json" mode in Ollama properly. This flag constrains the output to valid JSON, and E4B respects it while still producing coherent, specific content. E2B can struggle here — the output is valid JSON but the reasoning quality inside the strings drops noticeably.

On a modern CPU (M-series Mac or reasonably recent x86), E4B runs in under two minutes for most tasks. That’s usable. It’s not instant, but it’s not “go make a coffee” either. On a GPU it’s much faster.

Use E4B when: you need structured output, multimodal input, or reasoning quality that holds up in a real product — and you want it to run locally without a GPU.

31B Dense: When You Need to Think Hard

The 31B model is a different kind of tool. The 128K context window alone opens up use cases the smaller models simply cannot touch: full codebase analysis, long-document reasoning, multi-turn conversations that span thousands of tokens without losing thread.

The reasoning depth is also qualitatively different. Tasks that require synthesizing across many pieces of information — legal document review, detailed technical explanations, nuanced comparative analysis — land differently at 31B than at 4B. The model doesn’t just answer; it considers.

The tradeoff is infrastructure. You need a GPU (or significant RAM for CPU inference), and the latency is real. This isn’t a laptop model. But if you’re building something that needs serious reasoning and you can provision the compute, 31B Dense is competitive with much larger proprietary models on a lot of benchmarks.

Use 31B Dense when: you have the compute, your task requires deep reasoning or long context, and quality is the primary constraint.

The Feature That Changed How I Build: Multimodal + JSON Mode

Here’s the combination that makes Gemma 4 genuinely special for application developers.

Most LLM applications are text in, text out. You send a prompt, you parse the response, you deal with whatever format the model decided to use that day. Gemma 4 changes two things at once:

1. You can send images. Not OCR’d text from images. Not bounding boxes. The actual image. The model reasons about what it sees — colors, layout, spatial relationships, visual hierarchy.

2. You can enforce JSON output. With format: "json" in the Ollama request, the model outputs valid JSON. Combined with a well-defined system prompt that describes the exact schema you want, you get structured, machine-readable output you can render directly in a UI.

Here’s what that looks like in practice:

const response = await fetch("http://localhost:11434/api/generate", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    model: "gemma4:e4b",
    prompt: systemPrompt + "nn" + userPrompt,
    images: [base64Image],  // raw base64, no data URI prefix
    format: "json",         // constrain output to valid JSON
    stream: true,
    options: {
      temperature: 0.3,     // lower = more deterministic structure
      num_ctx: 8192,
    },
  }),
});

The model receives the image, reasons about it, and returns a structured JSON object. No parsing heuristics. No regex. No “hope the model uses the format I asked for.” Validate it with Zod or any schema library and you’re done.

One practical note: sometimes the first pass at temperature: 0.3 produces a malformed response, especially on complex schemas. A simple retry at temperature: 0.1 with streaming disabled almost always fixes it:

// Retry pattern for malformed JSON
const retryResponse = await fetch("http://localhost:11434/api/generate", {
  method: "POST",
  body: JSON.stringify({
    model: "gemma4:e4b",
    prompt: systemPrompt + "nn" + userPrompt,
    images: [base64Image],
    format: "json",
    stream: false,          // non-streaming for retry
    options: { temperature: 0.1, num_ctx: 8192 },
  }),
});

This two-pass pattern makes structured multimodal output production-reliable.

Getting Started in Five Minutes

If you want to run Gemma 4 locally right now:

# Install Ollama (macOS/Linux)
curl -fsSL https://ollama.com/install.sh | sh

# Pull the E4B model (~3GB download)
ollama pull gemma4:e4b

# Test it immediately
ollama run gemma4:e4b "Explain the difference between cognitive load and visual noise in UX design"

For multimodal input via API:

# Send an image (base64 encoded)
curl http://localhost:11434/api/generate -d '{
  "model": "gemma4:e4b",
  "prompt": "Describe the visual hierarchy in this UI screenshot",
  "images": ["'$(base64 -i your-screenshot.png)'"],
  "format": "json",
  "stream": false
}'

That’s it. No API key, no rate limits, no billing dashboard.

What This Actually Means

I think we’re underreacting to what’s happening here.

A 4-billion-parameter multimodal model that runs on a consumer laptop, follows complex structured output schemas, reasons about images, and produces output good enough to use in a real product — that’s not a prototype capability. That’s a production capability.

For developers, this changes the build calculus for a whole category of applications. Anything that involves user data you’d rather not send to a cloud API. Any tool that needs to work offline. Any product targeting users who are rightfully skeptical about where their data goes. Any hobby project where you don’t want to watch a usage bill grow.

For the broader ecosystem, a capable open-weights multimodal model means researchers, educators, and developers in places with expensive or unreliable internet access can run serious AI locally. The capability is no longer gated behind an API key and a credit card.

The question I keep coming back to: if a 4B model running on CPU can do this now, what does the roadmap look like in two years? We’re probably not near the ceiling.

I’m not sure what that means yet. But I’m fairly sure the “local AI as compromise” framing is already wrong, and most people haven’t noticed.

Running Gemma 4 E4B locally via Ollama. Models are open-weights and available at ollama.com/library/gemma4.

I built an AI app store screenshot generator because Figma made me cry — looking for brutal feedback

I’m Rishab. Every time I shipped an iOS app, I’d lose 2 days fighting Figma
trying to make App Store screenshots that didn’t look amateur. I’m a dev,
not a designer, and the existing tools were either $200/mo Figma plug-ins
or $500 design gigs per launch.

So I built SnapMonk — drop in a raw screenshot, it picks a layout, writes
the tagline copy with AI, mocks a device frame, and exports every size
the App Store and Play Store ask for. Free tier has no watermark. Pro
adds team features + unlimited saves.

Where I’m at:

  • 33 signups in 5 weeks
  • 2 paid users
  • Just got featured on Product Hunt

What I’d love feedback on:

  1. The editor flow — anything slow, confusing, or stupid?
  2. Pricing — is free tier too generous? Too thin?
  3. Where else should an indie like me be posting? (Already on PH, AlternativeTo)

URL: https://snapmonk.com
No login needed to try it.

Roast me — that’s what I’m here for.

Hello DEV Community — My Developer Journey Begins

Hello everyone! I’m a web developer passionate about building modern, clean, and user-friendly web applications.

I’ve recently joined the DEV Community to share my journey, learn from other developers, and document everything I build along the way.

💻 What I’m Focused On

Right now, I’m exploring and improving my skills in:

  • 🌐 Frontend Development (HTML, CSS, JavaScript)
  • ⚙️ Building responsive and interactive UI/UX designs
  • 🔌 API integration and backend basics
  • 🤖 AI-powered web tools and automation ideas
    I enjoy turning ideas into real working products that people can actually use.

🛠️ What I Love Building

I’m especially interested in building:

Web tools (like converters, generators, and utilities)
AI-based automation tools
Modern landing pages and SaaS-style UI designs
Small but useful projects that solve real problems
📚 Why I Joined DEV

I joined DEV Community because I want to:

Share my learning journey openly
Improve my development skills through consistency
Connect with other developers
Get feedback on my projects
Stay motivated by building in public
🔥 What You Can Expect From Me

From here on, I’ll be posting:

My web development projects
UI/UX experiments
AI tool ideas and builds
Tips and lessons I learn while coding
🌱 Final Words

I’m just getting started, but I’m excited about this journey of building, learning, and growing as a developer.

If you’re also on a similar path, feel free to connect — I’d love to learn together!

Thanks for reading 🙌

Adaptable apps on ChromeOS: a post-mortem

In my previous article Building a custom launcher for ChromeOS I described how Be nice runs on Chromebooks: not as a real default home app, because default home settings (Settings.ACTION_HOME_SETTINGS) usually are not available on ChromeOS, but as a normal Android app in an ARC window. I pretended the app is the launcher in code (detectIsHomeApp() returns true on ChromeOS), worked around split-screen bugs that leave the app a black rectangle, and gave up transparent wallpaper for an opaque scaffold because ARC does not show the ChromeOS desktop behind the window the way a phone shows its wallpaper.

What that article did not cover is the fight that came right after. For about a week in May 2026 I tried to get rid of a system confirmation ChromeOS shows when users open the preset-size menu in the ARC window title bar and choose Resizable, at least on Play Store builds. During my experiments, manifest changes often looked fine from Android Studio; however, on Play, the dialog stayed. I tried adding XML, asking AI assistants, including Google’s own models, for the magic flag. Here’s what I learned along the way.

What users see

ChromeOS does not present Play Store Android apps the same way in every posture. On a convertible or clamshell Chromebook in laptop use, ARC usually places apps in individual windows (chromeos.dev). The title bar often labels the current layout (Phone, Tablet, or Resizable) and opens a menu to switch presets; choosing Resizable is what triggers the warning below, not dragging the window frame by itself (in fact, resizing the window by dragging one of the window edges does not work until it is allowed through the dialog). In tablet mode the picture is different: apps commonly launch and stay full screen, which matches how ChromeOS has long treated touch-first use on detachables (Chrome Unboxed on immersive mode). The resize presets and the warning below matter most when you are in a windowed, laptop-style layout, not when an app is already filling the screen in tablet mode.

From that menu, when you choose Resizable in a windowed, laptop-style layout, ChromeOS brings up a confirmation that has been written about since around 2021 (Chrome Unboxed, Chrome Nerd):

This app is designed for mobile and may not resize well. The app may experience issues or restart.

Some apps never offer Tablet or Resizable at all; they stay on Phone with “This app only supports this size”, which is the stricter case when developers set resizeableActivity="false" (About Chromebooks). Be nice was not locked like that; Tablet and Resizable appeared in the title-bar menu, but the mobile warning still appeared when I chose Resizable on Play installs.

What made debugging miserable was Studio versus Play. Sideloads felt better; store builds did not. Play Help describes Phone, Tablet, and Resizable presets accessible directly from the window title bar, which matches how I used them on my Chromebook, though the exact entry point may differ by ChromeOS version. Play Help also says resize presets apply to newly downloaded apps, so I shipped many builds in quick succession. Fortunately there is an internal testing track. Never do this in production.

What I started with

The manifest did not spell out ChromeOS windowing. There was no explicit resizeableActivity, no ChromeOS window metadata, and no optional touchscreen or PC hardware features. On ordinary Android that omission was deliberate: for apps targeting API level 24 and higher, resizeableActivity defaults to "true" when you leave it out (<application>, <activity>). Be nice targets API 37 with minSdk 28, so phone and tablet builds were already resizable in multi-window terms; I had never needed the attribute in XML. What I lacked were the Chromebook-specific hints, not the baseline Android flag. The launcher already listened for size-related configChanges, including smallestScreenSize, but I had not told ChromeOS how I wanted freeform launch or Play classification on devices without touch. The working theory was simple: add the flags Google documents for Chromebooks, and the dialog would go away.

What I tried

Declare the app resizable. I set android:resizeableActivity="true" on the application and told Play users I had “improved detection of the app’s resize capabilities on ChromeOS.” On Android I had mostly made explicit what the target SDK already implied; it did not unlock resize on phones or tablets that was missing before. It does not remove the “designed for mobile” prompt when someone picks Resizable on ChromeOS either. I had conflated “allowed to resize” with “ChromeOS will not warn.”

Tell the system I support size changes. I added android.supports_size_changes at the application level, later duplicated it on individual activities, and at one point switched from <meta-data> to <property>. The hope was that ChromeOS would treat the app as resize-aware and skip the warning. Nothing in the public docs ties that flag to suppressing the dialog; the warning outlived every variant.

Ask for a tablet-shaped launch. I set WindowManagerPreference:FreeformWindowSize to tablet on the home activity so the window would open in a tablet preset rather than phone-sized mobile. Changelog copy spoke of “default tablet window size.” That metadata only affects how the window opens in freeform mode (chromeos.dev), not whether the user sees the mobile warning when they switch to Resizable. I later removed the tablet preset while still chasing the same popup, so I had already abandoned my own hypothesis.

Pin static launch bounds. I added <layout> on the launcher (and eventually on other activities) with default width and height in dp; I tried 840×640 first, then larger bounds as the experiment went on. At the same time I still had, or had just had, tablet freeform metadata on the same activity. Documentation warns that static <layout> bounds and FreeformWindowSize can conflict; I ran both anyway. Without success, that is.

Declare Chromebook-friendly hardware. Following a suggestion from Gemini, I marked touchscreen, faketouch, and android.hardware.type.pc as not required, and added <supports-screens> with every size bucket enabled. The touchscreen and PC entries are appropriate for Play on Chromebooks (manifest compatibility), but they address installability and input, not the resize confirmation string.

Widen configChanges to avoid restarts. I started with a modest list, then expanded it, then trimmed it back down. At one point the manifest listed almost every flag I could find, including locale, font scale, and color mode, on the theory that activity restarts on resize triggered bad behavior or the warning. It became unreadable, and it did not help.

Activity embedding override. I even added PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE at the application level. That belongs to a different feature (embedding / multi-pane); it has no documented link to ChromeOS freeform resize warnings, and, to no one’s surprise, it did not help.

Time to recap

Declaring resizeableActivity tells the system the app accepts window resize (rather than staying in a fixed compatibility viewport on large screens); it does not opt out of the Resizable confirmation. supports_size_changes and launch metadata do not either. Optional touchscreen declarations matter for Chromebook distribution; they were never documented as silencing “designed for mobile.” The dialog fits Google’s preset-size UX, which is a way for users to pick Phone or Tablet without every app being fully adaptive (Android Community). I had spent a week tuning manifest knobs for a message the OS shows on purpose when the user chooses Resizable, and for which I have not found an officially documented, bulletproof way out.

The biggest learning was not to underestimate what changes when the app comes from a Play install. Sideloading from Android Studio had led me to think the problem was solved; the store build told a different story. Play distribution, install context, and ChromeOS window presets are part of the behavior you are debugging, and they are not fully visible if you only deploy from the IDE.

Nothing in this effort demonstrated that the “designed for mobile” dialog can be removed from the manifest alone. I continue with a manifest that reflects adaptability on Android in general: resizeableActivity on the application, android.supports_size_changes on the home activity, optional touchscreen / type.pc hardware declarations for desktop-class devices, and size-related configChanges as chromeos.dev‘s second option for window reshape. The adaptive UI itself stays in Compose (WindowSizeClass and layouts that follow the window).

That is the story in prose; here is what it looks like in app/src/main/AndroidManifest.xml today:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <queries>
        <intent>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent>
    </queries>

    <uses-feature
        android:name="android.hardware.touchscreen"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.faketouch"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.type.pc"
        android:required="false" />

    <application
        android:name=".BeNiceApplication"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:enableOnBackInvokedCallback="true"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:resizeableActivity="true"
        android:supportsRtl="true"
        android:theme="@style/Theme.BeNice"
        tools:ignore="UnusedAttribute">

        <activity
            android:name=".AppChooserActivity"
            android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize"
            android:exported="true"
            android:launchMode="singleTask"
            android:stateNotNeeded="true"
            android:windowSoftInputMode="adjustResize">
            <meta-data
                android:name="android.supports_size_changes"
                android:value="true" />
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.HOME" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

        <activity
            android:name=".ImageViewerActivity"
            android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize"
            android:exported="false">
            <intent-filter>
                <action android:name="android.intent.action.QUICK_VIEW" />
            </intent-filter>
        </activity>

        <activity
            android:name=".BeNiceActivity"
            android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize"
            android:excludeFromRecents="true"
            android:exported="true">
            <intent-filter>
                <action android:name="de.thomaskuenneth.benice.intent.action.ACTION_LAUNCH_APP" />
            </intent-filter>
            <intent-filter>
                <action android:name="de.thomaskuenneth.benice.intent.action.ACTION_LAUNCH_APP_PAIR" />
            </intent-filter>
        </activity>

        <service
            android:name=".BeNiceTileService"
            android:exported="true"
            android:icon="@drawable/tile_service_icon"
            android:label="@string/app_name"
            android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
            <intent-filter>
                <action android:name="android.service.quicksettings.action.QS_TILE" />
            </intent-filter>
        </service>

        <receiver
            android:name=".ShortcutPinnedReceiver"
            android:exported="false" />

    </application>

</manifest>

If you strip away the experiments, two resize declarations and one documented fallback are still worth keeping, and none of them is the magic flag that silences the dialog:

  • android:resizeableActivity="true" on <application>. Google expects apps that behave well when resized to declare resizeableActivity or supports_size_changes as enabled (device compatibility mode). At my target SDK the default is already true; I keep it explicit anyway.

  • <meta-data android:name="android.supports_size_changes" android:value="true" /> on AppChooserActivity. Same guidance: declare that the activity handles size changes without fighting compatibility mode. I put it on the home activity, not on every screen.

  • android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize" on activities. chromeos.dev lists this as its second option for resize; the first is ViewModel plus saved state so activity recreation stays cheap. I use the flags to skip a full tear-down when the ARC window changes shape. Current adaptive app guidance puts currentWindowAdaptiveInfo() and responsive Compose layouts first; I do not override onConfigurationChanged, and I am not claiming this manifest line is the adaptability strategy. It is optional lifecycle plumbing on top of WindowSizeClass.

The arc is not a win. It means accepting that Google never documented a clean way out of the Resizable warning, that Play behavior on Chromebooks stays largely opaque until you ship and test there, and that ChromeOS itself feels like a platform Google is letting fade while Aluminium OS and unified Android desktop take the spotlight.

What’s more, there is that feeling of being treated unfairly. Google pushes developers hard on making their apps behave well on as many device categories and form factors as possible, yet even first class citizens are stigmatised by this popup.

Has anyone found a workaround, or is this firmly an OS-level restriction? Let me know in the comments if you’ve felt this pain, too.

References