Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Test

on:
push:
branches:
- main
pull_request:

concurrency:
group: test-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Unit tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.1

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Run tests
run: bun run test
126 changes: 126 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Contributing to Framekit

Thank you for helping improve Framekit. This guide covers local setup, project conventions, and what we expect in pull requests.

## Ways to contribute

- **Report bugs** — [open an issue](https://github.com/mdadul/FrameKit/issues) with steps to reproduce, expected vs. actual behavior, and browser/OS
- **Suggest features** — describe the problem you are solving and how it fits store-screenshot workflows
- **Submit code** — bug fixes, tests, docs, and focused refactors are all welcome
- **Improve docs** — README, inline comments for non-obvious logic, and onboarding clarity

## Development setup

1. Fork and clone the repository:

```bash
git clone https://github.com/<your-username>/FrameKit.git
cd FrameKit
```

2. Install dependencies (Bun recommended):

```bash
bun install
```

3. Start the dev server:

```bash
bun run dev
```

4. Before opening a PR, run:

```bash
bun run test
bun run build
bun run lint
```

All three should pass. `build` runs TypeScript project references (`tsc -b`) in addition to the Vite bundle.

## Architecture

Framekit follows a simple layering model:

| Layer | Location | Responsibility |
|-------|----------|----------------|
| **Lib** | `src/lib/` | Pure functions: export planning, template apply, canvas math, asset persistence. No React, no Zustand. |
| **Stores** | `src/stores/` | Domain state and mutations. `project-store` delegates to `stores/project/*` modules. |
| **Hooks** | `src/hooks/` | Thin React adapters (selection, export form, canvas viewport). |
| **Components** | `src/components/` | UI only — panels, canvas, toolbar, landing. |

**Rules of thumb:**

- Put reusable business rules in `src/lib/` with unit tests.
- Keep Zustand actions as thin wrappers: clone project → call lib/module → persist history.
- Prefer extending existing helpers over duplicating logic (especially export, templates, and asset upload).
- Use `@/` path aliases (`@/lib/...`, `@/components/...`).

## Code style

- **TypeScript** — strict types; avoid `any` unless unavoidable at a library boundary.
- **Naming** — intention-revealing names; functions do one thing; constants in `src/lib/constants/`.
- **Components** — match surrounding patterns (Tailwind utility classes, Radix primitives, existing panel layout).
- **Comments** — only where behavior is non-obvious; let code and tests document contracts.
- **Scope** — keep PRs focused. A bug fix should not include unrelated refactors.

## Testing

Tests use [Vitest](https://vitest.dev/) with jsdom. Shared fixtures live in `src/test/fixtures/`.

- Add or update tests when changing `src/lib/` behavior.
- Characterization tests are preferred for refactors: lock current behavior before moving code.
- Run `bun run test` locally; CI expects a green suite.

Example:

```bash
bun run test # all tests
bun run test src/lib/export/zip.ts # path filter (Vitest)
```

## Pull request checklist

- [ ] `bun run test` passes
- [ ] `bun run build` passes
- [ ] `bun run lint` passes (or no new lint violations in touched files)
- [ ] Behavior change is intentional and described in the PR
- [ ] Refactors preserve existing UI, store actions, export output, and shortcuts unless the PR explicitly changes them
- [ ] No secrets, API keys, or personal data committed

### Commit messages

Use clear, imperative subjects. Prefer **why** over **what** when it helps reviewers:

```
fix export ZIP paths for Android tablet presets

Extract prepare-export-screen helper so ExportDialog and zip share resize logic
```

### PR description

Include:

1. **Summary** — what changed and why (1–3 bullets)
2. **Test plan** — commands run and any manual smoke steps (editor open, export preview, template apply, etc.)

## Manual smoke testing

For UI or export changes, quickly verify:

1. Landing page loads; create or open a project
2. Add text, image, shape, and device elements on the canvas
3. Apply a template; undo/redo
4. Export preview and download a PNG or ZIP
5. Import/export a `.ssgproj` file

## Project file format

`.ssgproj` files are JSON project bundles. Changes to the schema or import logic should remain backward compatible when possible, and should be called out in the PR if not.

## Questions

Open an issue if you are unsure whether an idea fits. For substantial features, filing an issue first helps align on scope before you invest in a large PR.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Emdadul Islam

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
95 changes: 66 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,103 @@
# Screenshot Studio OSS
# Framekit

A browser-based App Store and Google Play screenshot generator. Design once, export all required store sizes, and keep everything local.
**Store-ready screenshots** — a browser-based App Store and Google Play screenshot generator. Design once, export every required store size, and keep everything on your machine.

[GitHub](https://github.com/mdadul/FrameKit) · [Contributing](CONTRIBUTING.md)

## Why Framekit

- **Local-first** — projects autosave to IndexedDB in your browser; no account or cloud upload required
- **Store export built in** — Apple and Google Play size presets, bulk ZIP export with `ios/` and `android/` folders
- **Multi-platform workflow** — design on iOS frames, copy screens to Android with one action
- **Open source** — MIT licensed; fork it, self-host it, or contribute back

## Features

- Canvas editor with text, images, shapes, and device mockups
- 8 device frames (iPhone, iPad, Pixel, Galaxy)
- Layers, assets, templates, and brand kit
- Layers, assets, starter templates, and per-project brand kit
- Multi-screen projects (up to 10 screens)
- Undo/redo (100 states)
- Undo/redo (100 history states)
- IndexedDB autosave and `.ssgproj` import/export
- PNG/JPEG export at 1x/2x/3x
- PNG/JPEG export at 1×, 2×, and 3×
- Bulk ZIP export for Apple and Android store presets
- Light/dark theme and workspace preferences
- Project rename, save status, and confirmation dialogs
- Smart export with ios/ / android/ ZIP folders
- Screen overview mode and dashboard thumbnails
- Per-project brand kit and keyboard shortcuts guide
- First-run onboarding tour
- Keyboard shortcuts and first-run onboarding tour

## Tech stack

## Tech Stack
| Layer | Tools |
|-------|-------|
| UI | React 19, TypeScript, Tailwind CSS v4, Radix UI |
| Routing | TanStack Router |
| Canvas | Konva / React-Konva |
| State | Zustand + Immer |
| Storage | Dexie (IndexedDB) |
| Export | Canvas2D renderer, JSZip, Comlink workers |
| Build | Vite 8, Vitest, ESLint |

- React 19 + TypeScript + Vite
- TanStack Router
- Zustand + Immer
- Konva / React-Konva
- Dexie (IndexedDB)
- Tailwind CSS v4
- dnd-kit, Radix UI, JSZip
## Getting started

## Getting Started
**Prerequisites:** [Bun](https://bun.sh) (recommended) or Node.js 20+

```bash
git clone https://github.com/mdadul/FrameKit.git
cd FrameKit
bun install
bun run dev
```

Open `http://localhost:5173`.
Open [http://localhost:5173](http://localhost:5173).

## Scripts
With npm:

```bash
bun run dev # Start dev server
bun run build # Production build
bun run preview # Preview production build
bun run test # Run unit tests
bun run lint # Lint project
npm install
npm run dev
```

## Scripts

| Command | Description |
|---------|-------------|
| `bun run dev` | Start the Vite dev server |
| `bun run build` | Typecheck and production build → `dist/` |
| `bun run preview` | Preview the production build locally |
| `bun run test` | Run unit tests (Vitest) |
| `bun run lint` | Lint with ESLint |

## Project layout

```
src/
├── components/ # React UI (canvas, panels, toolbar, landing)
├── hooks/ # Thin React hooks (selection, export, canvas)
├── lib/ # Pure business logic (export, templates, canvas math)
├── routes/ # TanStack Router pages
├── stores/ # Zustand stores (project, editor, history)
└── test/ # Shared test fixtures and setup
```

Pure logic lives in `src/lib/`; stores orchestrate mutations; components stay thin. See [CONTRIBUTING.md](CONTRIBUTING.md) for conventions.

## Deployment

The app is a static SPA. Build with `bun run build` and deploy the `dist/` folder to:
Framekit is a static SPA. Build with `bun run build` and deploy the `dist/` folder to any static host:

- GitHub Pages
- Cloudflare Pages
- Netlify
- Vercel

## Project Format
## Project format

Projects can be exported as `.ssgproj` JSON files. A file contains screens, project settings, and embedded asset data so you can back up or share work outside the browser.

## Contributing

Projects can be exported as `.ssgproj` JSON files containing screens, settings, and embedded assets.
Bug reports, feature ideas, and pull requests are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) for setup, architecture notes, and the PR checklist.

## License

MIT
[MIT](LICENSE)
71 changes: 71 additions & 0 deletions src/components/canvas/CanvasTextEditOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { forwardRef, type KeyboardEvent } from 'react'
import type { TextElement } from '@/lib/types'

export interface CanvasTextEditOverlayProps {
element: TextElement
screenOffset: { x: number; y: number }
containerRect: DOMRect
panX: number
panY: number
workspaceZoom: number
value: string
onChange: (value: string) => void
onCommit: () => void
onCancel: () => void
}

export const CanvasTextEditOverlay = forwardRef<HTMLTextAreaElement, CanvasTextEditOverlayProps>(
function CanvasTextEditOverlay(
{
element,
screenOffset,
containerRect,
panX,
panY,
workspaceZoom,
value,
onChange,
onCommit,
onCancel,
},
ref,
) {
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
onCommit()
} else if (event.key === 'Escape') {
event.preventDefault()
onCancel()
}
}

return (
<textarea
ref={ref}
value={value}
onChange={(event) => onChange(event.target.value)}
onBlur={onCommit}
onKeyDown={handleKeyDown}
className="absolute z-20 resize-none overflow-hidden border-0 bg-transparent p-0 outline-none"
style={{
left: containerRect.left + panX + (screenOffset.x + element.x) * workspaceZoom,
top: containerRect.top + panY + (screenOffset.y + element.y) * workspaceZoom,
width: element.width * workspaceZoom,
height: element.height * workspaceZoom,
transformOrigin: 'top left',
transform: element.rotation ? `rotate(${element.rotation}deg)` : undefined,
fontFamily: element.fontFamily,
fontSize: element.fontSize * workspaceZoom,
fontWeight: element.fontWeight,
fontStyle: element.fontStyle,
lineHeight: element.lineHeight,
letterSpacing: element.letterSpacing * workspaceZoom,
color: element.fill,
textAlign: element.textAlign === 'justify' ? 'justify' : element.textAlign,
padding: (element.padding ?? 0) * workspaceZoom,
}}
/>
)
},
)
Loading
Loading