A small, dependency-free web app to load images locally (no server, no upload), display them in a square letterboxed grid, and convert them. Built with pure JavaScript (native ES Modules), plain HTML and CSS — no framework, no build step, no bundler.
- Pure JavaScript only. Native ES Modules (
import/export). No TypeScript, no framework, no bundler, no transpilation, no npm runtime dependencies. - Code and comments in English. User-facing copy is never hard-coded —
it lives in i18n locale files and is rendered through
t(key). Supported locales:en-US(default) andpt-BR. A code string used for logic (e.g. a reason code like"memory") is English; a string shown on screen is a translation key resolved at runtime. - Everything runs client-side. Images are read from local memory via
URL.createObjectURL; nothing is ever uploaded. - No new dependencies without an explicit request.
Because it uses native ES Modules, it must be served over HTTP (file://
double-click is blocked by CORS for module imports):
python3 -m http.server 8000
# open http://localhost:8000Strict separation of responsibilities. Dependencies flow in one direction: controllers → services / views → core / config. Lower layers never import upward.
index.html Markup + element ids the UI binds to
styles.css All styling (see .claude/skills/styling.md)
app/
├── main.js Entry point; calls appController.init()
├── config/
│ └── constants.js Accepted types + memory limits (single place to tune)
├── core/
│ └── imageStore.js Single source of truth: in-memory image list,
│ memory accounting, object-URL lifecycle (revoke)
├── i18n/ Internationalization (no library)
│ ├── i18n.js Core: detect, t(key, params), get/setLocale,
│ │ onChange, localStorage persistence
│ └── locales/
│ ├── en-us.js Default locale (code, name, flag SVG, messages)
│ └── pt-br.js Portuguese locale
├── services/ Stateless business logic / specific tasks
│ ├── fileValidator.js isAccepted / getExtension
│ ├── memoryBudget.js computeBudgetBytes / measureDecodedBytes
│ ├── imageLoader.js Sequential, budget-aware loading orchestration
│ └── imageProcessor.js "Convert" action (image processing) — to implement
├── ui/ View layer: DOM only, no business rules
│ ├── dom.js Centralized element refs + escapeHtml
│ ├── gridView.js Render/remove/clear cards
│ ├── statusView.js Counter, busy state, skipped-images notice
│ ├── i18nView.js Applies translations to static markup
│ └── languageSwitcher.js Custom flag dropdown to change locale at runtime
└── controllers/
└── appController.js Wires user events to services + views; the only
layer that knows about all the others
| Layer | May import | Must NOT do |
|---|---|---|
controllers |
services, ui, core | manipulate pixels; hold domain state |
services |
config, core, other svcs | touch the DOM; register UI events |
core |
config | touch the DOM; know about services/views |
ui |
dom, pure utils | hold business rules; know about the store |
config |
— | contain logic |
- New screen interaction → handler in
controllers/appController.js. - New domain/processing task (e.g. the conversion logic) →
services/(put the Convert implementation inimageProcessor.js). - New visual piece / DOM change → a
ui/*View.jsmodule; keep it a pure function of its inputs. - New shared state →
core/imageStore.js(keep it the single source of truth; it also ownsURL.revokeObjectURLto prevent leaks). - New limit / accepted type →
config/constants.js. - New user-facing text → add the key to both
i18n/locales/*.js; render it viat(key)(dynamic strings) or adata-i18n*attribute (static markup). Never hard-code display text in views or HTML. - New language → add
i18n/locales/<code>.js(withflagSVG) and register it ini18n/i18n.js; the switcher picks it up automatically.
The browser does not expose real free memory. We use an estimated budget
(memoryBudget.js) and load sequentially, measuring each image's decoded
cost (width × height × 4 bytes) plus its raw file size. When the running
total would exceed the budget, loading stops and remaining files are reported
as skipped. imageStore decrements the running total on remove/clear and is
the only place that revokes object URLs.
- Module-per-responsibility; prefer named exports.
- Comments explain why, not what; keep them in English.
- No global variables; state lives in
core/imageStore.js.
Project-specific Claude Code skills live under .claude/skills/<name>/SKILL.md
and are auto-invoked by their description:
- styling — CSS rules and conventions (tokens, BEM, dark theme, layout, a11y). Triggers on CSS / visual changes.
- javascript — JS conventions (pure ESM, layering, naming, memory safety).
Triggers on changes under
app/.