From 7cc614f6dcf331a5bf71eac8d51acdd366877c5e Mon Sep 17 00:00:00 2001 From: Emdadul Islam Date: Mon, 15 Jun 2026 08:22:01 +0600 Subject: [PATCH 1/2] Refactor codebase into testable lib modules and thin UI layers. Extract shared business logic, split large panels and canvas hooks, and add Framekit docs without changing editor or export behavior. Co-authored-by: Cursor --- CONTRIBUTING.md | 126 +++ LICENSE | 21 + README.md | 95 ++- .../canvas/CanvasTextEditOverlay.tsx | 71 ++ src/components/canvas/CanvasWorkspace.tsx | 647 +++------------ src/components/canvas/ScreenContextMenu.tsx | 75 +- src/components/landing/ProjectsSection.tsx | 12 +- .../landing/StarterTemplatesRow.tsx | 27 +- .../landing/TemplatePickerModal.tsx | 37 +- src/components/panels/AssetsPanel.tsx | 9 +- src/components/panels/BackgroundControls.tsx | 524 +----------- src/components/panels/BrandKitPanel.tsx | 46 +- src/components/panels/LayerContextMenu.tsx | 115 +-- src/components/panels/LayersPanel.tsx | 40 +- src/components/panels/PropertiesPanel.tsx | 749 ++---------------- src/components/panels/TemplatesPanel.tsx | 81 +- .../background/BackgroundPresetsSection.tsx | 36 + .../panels/background/BackgroundPreview.tsx | 20 + .../panels/background/BackgroundTypeTabs.tsx | 43 + .../background/GradientBackgroundSection.tsx | 66 ++ .../background/ImageBackgroundSection.tsx | 115 +++ .../background/MeshBackgroundSection.tsx | 26 + .../background/PatternBackgroundSection.tsx | 55 ++ .../background/SolidBackgroundSection.tsx | 17 + src/components/panels/background/shared.tsx | 153 ++++ .../properties/DevicePropertiesSection.tsx | 184 +++++ .../properties/ElementEffectsSection.tsx | 55 ++ .../properties/ElementTransformSection.tsx | 65 ++ .../properties/ImagePropertiesSection.tsx | 161 ++++ .../properties/MultiSelectionProperties.tsx | 63 ++ .../properties/ScreenOnlyProperties.tsx | 23 + .../properties/ShapePropertiesSection.tsx | 107 +++ .../properties/TextPropertiesSection.tsx | 17 + src/components/panels/properties/shared.tsx | 22 + .../ProjectThumbnail.tsx | 26 +- src/components/templates/TemplateCard.tsx | 100 +++ .../templates/TemplatePreviewFrame.tsx | 24 + .../templates/TemplateThumbnail.tsx | 10 +- src/components/toolbar/EditorToolbar.tsx | 18 +- src/components/toolbar/ExportDialog.tsx | 578 ++++---------- .../toolbar/export/ExportAdvancedTab.tsx | 249 ++++++ .../toolbar/export/ExportQuickTab.tsx | 78 ++ src/components/ui/FloatingMenu.tsx | 82 ++ src/components/ui/SegmentedControl.test.tsx | 23 + src/components/ui/SegmentedControl.tsx | 66 ++ src/hooks/useCanvasDrop.ts | 89 +++ src/hooks/useCanvasSelection.ts | 193 +++++ src/hooks/useCanvasTextEdit.ts | 64 ++ src/hooks/useCanvasViewport.ts | 206 +++++ src/hooks/useExportFormReducer.ts | 19 + src/hooks/useExportPreview.ts | 129 +++ src/hooks/useFloatingMenu.ts | 43 + src/hooks/useHistoryNavigation.ts | 23 + src/hooks/useKeyboardShortcuts.ts | 13 +- src/hooks/usePersistAssetUpload.ts | 19 + src/hooks/useProjectsCatalog.test.ts | 29 + src/hooks/useProjectsCatalog.ts | 155 ++++ src/hooks/useScreenContextMenu.ts | 100 +++ src/hooks/useSelectedElements.ts | 14 + src/hooks/useSelectionCapabilities.ts | 27 + src/lib/assets/device-presets.ts | 8 + src/lib/assets/persist-project-asset.test.ts | 41 + src/lib/assets/persist-project-asset.ts | 15 + src/lib/canvas/backgrounds.ts | 38 +- src/lib/canvas/coordinates.test.ts | 73 ++ src/lib/canvas/coordinates.ts | 48 ++ src/lib/canvas/create-canvas-gradient.test.ts | 72 ++ src/lib/canvas/create-canvas-gradient.ts | 56 ++ src/lib/constants/brand.ts | 22 + .../{constants.ts => constants/defaults.ts} | 36 +- src/lib/constants/export.ts | 11 + src/lib/constants/index.ts | 7 + src/lib/constants/workspace.ts | 2 + src/lib/elements/element-meta.test.ts | 23 + src/lib/elements/element-meta.ts | 63 ++ src/lib/elements/selection-fields.test.ts | 16 + src/lib/elements/selection-fields.ts | 6 + src/lib/export/export-form-state.test.ts | 54 ++ src/lib/export/export-form-state.ts | 110 +++ src/lib/export/export-plan.ts | 6 +- src/lib/export/prepare-export-screen.test.ts | 21 + src/lib/export/prepare-export-screen.ts | 18 + src/lib/export/renderer.ts | 42 +- src/lib/export/types.ts | 1 + src/lib/export/zip.ts | 28 +- src/lib/landing/types.ts | 1 + .../selection/resolve-selected-elements.ts | 17 + .../apply-template-to-screen.test.ts | 43 + src/lib/templates/apply-template-to-screen.ts | 38 + src/lib/templates/filter-templates.test.ts | 25 + src/lib/templates/filter-templates.ts | 27 + src/lib/templates/index.ts | 1 + src/routes/index.tsx | 143 +--- src/stores/project-store.ts | 405 ++-------- src/stores/project/create-device-element.ts | 30 + src/stores/project/element-alignment.test.ts | 53 ++ src/stores/project/element-alignment.ts | 104 +++ src/stores/project/element-mutations.ts | 81 ++ .../project/element-update-coalesce.test.ts | 46 ++ src/stores/project/element-update-coalesce.ts | 39 + src/stores/project/screen-mutations.test.ts | 19 + src/stores/project/screen-mutations.ts | 157 ++++ src/stores/project/template-screen.ts | 18 + src/test/fixtures/minimal-project.ts | 9 + src/test/fixtures/minimal-screen.ts | 14 + 105 files changed, 5221 insertions(+), 3146 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 src/components/canvas/CanvasTextEditOverlay.tsx create mode 100644 src/components/panels/background/BackgroundPresetsSection.tsx create mode 100644 src/components/panels/background/BackgroundPreview.tsx create mode 100644 src/components/panels/background/BackgroundTypeTabs.tsx create mode 100644 src/components/panels/background/GradientBackgroundSection.tsx create mode 100644 src/components/panels/background/ImageBackgroundSection.tsx create mode 100644 src/components/panels/background/MeshBackgroundSection.tsx create mode 100644 src/components/panels/background/PatternBackgroundSection.tsx create mode 100644 src/components/panels/background/SolidBackgroundSection.tsx create mode 100644 src/components/panels/background/shared.tsx create mode 100644 src/components/panels/properties/DevicePropertiesSection.tsx create mode 100644 src/components/panels/properties/ElementEffectsSection.tsx create mode 100644 src/components/panels/properties/ElementTransformSection.tsx create mode 100644 src/components/panels/properties/ImagePropertiesSection.tsx create mode 100644 src/components/panels/properties/MultiSelectionProperties.tsx create mode 100644 src/components/panels/properties/ScreenOnlyProperties.tsx create mode 100644 src/components/panels/properties/ShapePropertiesSection.tsx create mode 100644 src/components/panels/properties/TextPropertiesSection.tsx create mode 100644 src/components/panels/properties/shared.tsx rename src/components/{dashboard => templates}/ProjectThumbnail.tsx (69%) create mode 100644 src/components/templates/TemplateCard.tsx create mode 100644 src/components/templates/TemplatePreviewFrame.tsx create mode 100644 src/components/toolbar/export/ExportAdvancedTab.tsx create mode 100644 src/components/toolbar/export/ExportQuickTab.tsx create mode 100644 src/components/ui/FloatingMenu.tsx create mode 100644 src/components/ui/SegmentedControl.test.tsx create mode 100644 src/components/ui/SegmentedControl.tsx create mode 100644 src/hooks/useCanvasDrop.ts create mode 100644 src/hooks/useCanvasSelection.ts create mode 100644 src/hooks/useCanvasTextEdit.ts create mode 100644 src/hooks/useCanvasViewport.ts create mode 100644 src/hooks/useExportFormReducer.ts create mode 100644 src/hooks/useExportPreview.ts create mode 100644 src/hooks/useFloatingMenu.ts create mode 100644 src/hooks/useHistoryNavigation.ts create mode 100644 src/hooks/usePersistAssetUpload.ts create mode 100644 src/hooks/useProjectsCatalog.test.ts create mode 100644 src/hooks/useProjectsCatalog.ts create mode 100644 src/hooks/useScreenContextMenu.ts create mode 100644 src/hooks/useSelectedElements.ts create mode 100644 src/hooks/useSelectionCapabilities.ts create mode 100644 src/lib/assets/device-presets.ts create mode 100644 src/lib/assets/persist-project-asset.test.ts create mode 100644 src/lib/assets/persist-project-asset.ts create mode 100644 src/lib/canvas/coordinates.test.ts create mode 100644 src/lib/canvas/coordinates.ts create mode 100644 src/lib/canvas/create-canvas-gradient.test.ts create mode 100644 src/lib/canvas/create-canvas-gradient.ts create mode 100644 src/lib/constants/brand.ts rename src/lib/{constants.ts => constants/defaults.ts} (68%) create mode 100644 src/lib/constants/export.ts create mode 100644 src/lib/constants/index.ts create mode 100644 src/lib/constants/workspace.ts create mode 100644 src/lib/elements/element-meta.test.ts create mode 100644 src/lib/elements/element-meta.ts create mode 100644 src/lib/elements/selection-fields.test.ts create mode 100644 src/lib/elements/selection-fields.ts create mode 100644 src/lib/export/export-form-state.test.ts create mode 100644 src/lib/export/export-form-state.ts create mode 100644 src/lib/export/prepare-export-screen.test.ts create mode 100644 src/lib/export/prepare-export-screen.ts create mode 100644 src/lib/export/types.ts create mode 100644 src/lib/landing/types.ts create mode 100644 src/lib/selection/resolve-selected-elements.ts create mode 100644 src/lib/templates/apply-template-to-screen.test.ts create mode 100644 src/lib/templates/apply-template-to-screen.ts create mode 100644 src/lib/templates/filter-templates.test.ts create mode 100644 src/lib/templates/filter-templates.ts create mode 100644 src/stores/project/create-device-element.ts create mode 100644 src/stores/project/element-alignment.test.ts create mode 100644 src/stores/project/element-alignment.ts create mode 100644 src/stores/project/element-mutations.ts create mode 100644 src/stores/project/element-update-coalesce.test.ts create mode 100644 src/stores/project/element-update-coalesce.ts create mode 100644 src/stores/project/screen-mutations.test.ts create mode 100644 src/stores/project/screen-mutations.ts create mode 100644 src/stores/project/template-screen.ts create mode 100644 src/test/fixtures/minimal-project.ts create mode 100644 src/test/fixtures/minimal-screen.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fc8293e --- /dev/null +++ b/CONTRIBUTING.md @@ -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//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. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dc377c6 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md index f9f19a4..12d300f 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/src/components/canvas/CanvasTextEditOverlay.tsx b/src/components/canvas/CanvasTextEditOverlay.tsx new file mode 100644 index 0000000..fbba2ab --- /dev/null +++ b/src/components/canvas/CanvasTextEditOverlay.tsx @@ -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( + function CanvasTextEditOverlay( + { + element, + screenOffset, + containerRect, + panX, + panY, + workspaceZoom, + value, + onChange, + onCommit, + onCancel, + }, + ref, + ) { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault() + onCommit() + } else if (event.key === 'Escape') { + event.preventDefault() + onCancel() + } + } + + return ( +