From 937ac532dc70b0da914234f78d7dbfd0acc8938e Mon Sep 17 00:00:00 2001 From: Sean Sun <1194458432@qq.com> Date: Tue, 23 Jun 2026 14:07:05 +0800 Subject: [PATCH] =?UTF-8?q?chore(design-sync):=20add=20PixelForge=20?= =?UTF-8?q?=E2=80=94=20PS-style=20editor=20UI=20as=20a=20second=20DS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync the image editor's Photoshop-style web UI as a SECOND, separate Claude Design project ("PixelForge — Image Editor UI Kit"), distinct from the base shadcn/ui kit. Built from the same repo via a parallel config; no app/src changes. What's synced (30 components): the signature PS chrome (MenuBar, OptionsBar, ToolsPalette, StatusBar), dockable panels (Layers/Properties/History/Actions/ Paths/LayerComps/Adjust/Brushes), the full CC-style dialog set, and widgets (Slider/CurvesEditor/ContextMenu/DropZone). 29 with rich preview cards; SaveForWebDialog ships a floor card (its live needs the editor). Skipped (need a live canvas/pixels/state): Canvas, Workspace, RightSidebar, ChannelsPanel, ReplaceColorDialog, ColorRangeDialog. How: - pixelforge.config.json (globalName PixelForge, out dir pf-bundle), a synth entry that self-initializes react-i18next in English so t() renders real copy with no provider, and cssEntry = the app's index + ImageEditor CSS chunks (the .pf-* chrome lives in the editor route chunk). - Previews use an html.dark frame so portaled dialogs render dark too. - .d.ts ship as loose contracts (editor-shaped props) by design; usage lives in each prompt.md + the PixelForge conventions header. A 6-agent audit picked the sync set; a 7-agent fan-out authored + graded every preview. Render check clean (30/30), all graded good. Shared .design-sync/ previews are name-disjoint from the base kit. lint, build, and 388 tests green. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012gT1Vn216uvrJRGC8DbvVZ --- .design-sync/NOTES.md | 43 +++++ .design-sync/pf-groups/ActionsPanel.md | 3 + .design-sync/pf-groups/AdjustPanel.md | 3 + .design-sync/pf-groups/AdjustmentDialog.md | 3 + .design-sync/pf-groups/BrushesPanel.md | 3 + .design-sync/pf-groups/CanvasSizeDialog.md | 3 + .design-sync/pf-groups/ColorPickerDialog.md | 3 + .design-sync/pf-groups/ContextMenu.md | 3 + .design-sync/pf-groups/CurvesEditor.md | 3 + .design-sync/pf-groups/DropZone.md | 3 + .design-sync/pf-groups/FillDialog.md | 3 + .design-sync/pf-groups/FilterDialog.md | 3 + .design-sync/pf-groups/HistoryPanel.md | 3 + .design-sync/pf-groups/ImageSizeDialog.md | 3 + .design-sync/pf-groups/LayerCompsPanel.md | 3 + .design-sync/pf-groups/LayerStyleDialog.md | 3 + .design-sync/pf-groups/LayersPanel.md | 3 + .design-sync/pf-groups/MenuBar.md | 3 + .design-sync/pf-groups/NewDocumentDialog.md | 3 + .design-sync/pf-groups/OptionsBar.md | 3 + .design-sync/pf-groups/PathsPanel.md | 3 + .design-sync/pf-groups/PropertiesPanel.md | 3 + .../pf-groups/RotateArbitraryDialog.md | 3 + .design-sync/pf-groups/SaveForWebDialog.md | 3 + .design-sync/pf-groups/SelectModifyDialog.md | 3 + .design-sync/pf-groups/ShortcutsDialog.md | 3 + .design-sync/pf-groups/Slider.md | 3 + .design-sync/pf-groups/StatusBar.md | 3 + .design-sync/pf-groups/StrokeDialog.md | 3 + .design-sync/pf-groups/ToolsPalette.md | 3 + .design-sync/pf-groups/WarpTextDialog.md | 3 + .design-sync/pixelforge-conventions.md | 88 ++++++++++ .design-sync/pixelforge-entry.ts | 44 +++++ .design-sync/pixelforge.config.json | 151 ++++++++++++++++++ .design-sync/previews/ActionsPanel.tsx | 61 +++++++ .design-sync/previews/AdjustPanel.tsx | 46 ++++++ .design-sync/previews/AdjustmentDialog.tsx | 40 +++++ .design-sync/previews/BrushesPanel.tsx | 38 +++++ .design-sync/previews/CanvasSizeDialog.tsx | 39 +++++ .design-sync/previews/ColorPickerDialog.tsx | 31 ++++ .design-sync/previews/ContextMenu.tsx | 48 ++++++ .design-sync/previews/CurvesEditor.tsx | 41 +++++ .design-sync/previews/DropZone.tsx | 31 ++++ .design-sync/previews/FillDialog.tsx | 40 +++++ .design-sync/previews/FilterDialog.tsx | 40 +++++ .design-sync/previews/HistoryPanel.tsx | 32 ++++ .design-sync/previews/ImageSizeDialog.tsx | 39 +++++ .design-sync/previews/LayerCompsPanel.tsx | 63 ++++++++ .design-sync/previews/LayerStyleDialog.tsx | 57 +++++++ .design-sync/previews/LayersPanel.tsx | 118 ++++++++++++++ .design-sync/previews/MenuBar.tsx | 55 +++++++ .design-sync/previews/NewDocumentDialog.tsx | 35 ++++ .design-sync/previews/OptionsBar.tsx | 69 ++++++++ .design-sync/previews/PathsPanel.tsx | 95 +++++++++++ .design-sync/previews/PropertiesPanel.tsx | 82 ++++++++++ .../previews/RotateArbitraryDialog.tsx | 34 ++++ .design-sync/previews/SelectModifyDialog.tsx | 35 ++++ .design-sync/previews/ShortcutsDialog.tsx | 31 ++++ .design-sync/previews/Slider.tsx | 41 +++++ .design-sync/previews/StatusBar.tsx | 42 +++++ .design-sync/previews/StrokeDialog.tsx | 39 +++++ .design-sync/previews/ToolsPalette.tsx | 42 +++++ .design-sync/previews/WarpTextDialog.tsx | 41 +++++ .gitignore | 3 + 64 files changed, 1824 insertions(+) create mode 100644 .design-sync/pf-groups/ActionsPanel.md create mode 100644 .design-sync/pf-groups/AdjustPanel.md create mode 100644 .design-sync/pf-groups/AdjustmentDialog.md create mode 100644 .design-sync/pf-groups/BrushesPanel.md create mode 100644 .design-sync/pf-groups/CanvasSizeDialog.md create mode 100644 .design-sync/pf-groups/ColorPickerDialog.md create mode 100644 .design-sync/pf-groups/ContextMenu.md create mode 100644 .design-sync/pf-groups/CurvesEditor.md create mode 100644 .design-sync/pf-groups/DropZone.md create mode 100644 .design-sync/pf-groups/FillDialog.md create mode 100644 .design-sync/pf-groups/FilterDialog.md create mode 100644 .design-sync/pf-groups/HistoryPanel.md create mode 100644 .design-sync/pf-groups/ImageSizeDialog.md create mode 100644 .design-sync/pf-groups/LayerCompsPanel.md create mode 100644 .design-sync/pf-groups/LayerStyleDialog.md create mode 100644 .design-sync/pf-groups/LayersPanel.md create mode 100644 .design-sync/pf-groups/MenuBar.md create mode 100644 .design-sync/pf-groups/NewDocumentDialog.md create mode 100644 .design-sync/pf-groups/OptionsBar.md create mode 100644 .design-sync/pf-groups/PathsPanel.md create mode 100644 .design-sync/pf-groups/PropertiesPanel.md create mode 100644 .design-sync/pf-groups/RotateArbitraryDialog.md create mode 100644 .design-sync/pf-groups/SaveForWebDialog.md create mode 100644 .design-sync/pf-groups/SelectModifyDialog.md create mode 100644 .design-sync/pf-groups/ShortcutsDialog.md create mode 100644 .design-sync/pf-groups/Slider.md create mode 100644 .design-sync/pf-groups/StatusBar.md create mode 100644 .design-sync/pf-groups/StrokeDialog.md create mode 100644 .design-sync/pf-groups/ToolsPalette.md create mode 100644 .design-sync/pf-groups/WarpTextDialog.md create mode 100644 .design-sync/pixelforge-conventions.md create mode 100644 .design-sync/pixelforge-entry.ts create mode 100644 .design-sync/pixelforge.config.json create mode 100644 .design-sync/previews/ActionsPanel.tsx create mode 100644 .design-sync/previews/AdjustPanel.tsx create mode 100644 .design-sync/previews/AdjustmentDialog.tsx create mode 100644 .design-sync/previews/BrushesPanel.tsx create mode 100644 .design-sync/previews/CanvasSizeDialog.tsx create mode 100644 .design-sync/previews/ColorPickerDialog.tsx create mode 100644 .design-sync/previews/ContextMenu.tsx create mode 100644 .design-sync/previews/CurvesEditor.tsx create mode 100644 .design-sync/previews/DropZone.tsx create mode 100644 .design-sync/previews/FillDialog.tsx create mode 100644 .design-sync/previews/FilterDialog.tsx create mode 100644 .design-sync/previews/HistoryPanel.tsx create mode 100644 .design-sync/previews/ImageSizeDialog.tsx create mode 100644 .design-sync/previews/LayerCompsPanel.tsx create mode 100644 .design-sync/previews/LayerStyleDialog.tsx create mode 100644 .design-sync/previews/LayersPanel.tsx create mode 100644 .design-sync/previews/MenuBar.tsx create mode 100644 .design-sync/previews/NewDocumentDialog.tsx create mode 100644 .design-sync/previews/OptionsBar.tsx create mode 100644 .design-sync/previews/PathsPanel.tsx create mode 100644 .design-sync/previews/PropertiesPanel.tsx create mode 100644 .design-sync/previews/RotateArbitraryDialog.tsx create mode 100644 .design-sync/previews/SelectModifyDialog.tsx create mode 100644 .design-sync/previews/ShortcutsDialog.tsx create mode 100644 .design-sync/previews/Slider.tsx create mode 100644 .design-sync/previews/StatusBar.tsx create mode 100644 .design-sync/previews/StrokeDialog.tsx create mode 100644 .design-sync/previews/ToolsPalette.tsx create mode 100644 .design-sync/previews/WarpTextDialog.tsx diff --git a/.design-sync/NOTES.md b/.design-sync/NOTES.md index abb5f6d..2dc6c08 100644 --- a/.design-sync/NOTES.md +++ b/.design-sync/NOTES.md @@ -94,3 +94,46 @@ unusual shape. - **Synth-entry `.d.ts` are weaker** than a real library's. cva components (Button, Tabs) may need `dtsPropsFor` overrides — check the emitted `.d.ts` on every re-sync. + +--- + +# PixelForge kit (second design system) + +A SECOND, separate design system for the image editor's Photoshop-style web UI, +uploaded to the **"PixelForge — Image Editor UI Kit"** Claude Design project +(projectId in `pixelforge.config.json`). Built from the SAME repo with a parallel +config; the base shadcn kit is unaffected. + +- **Config:** `pixelforge.config.json` (globalName `PixelForge`, out dir `pf-bundle`, + entry `pixelforge-entry.ts`, conventions `pixelforge-conventions.md`). +- **Shape:** synth-entry, same as base. `srcDir = src/components/image-editor`. +- **cssEntry:** `cat dist/assets/index-*.css dist/assets/ImageEditor-*.css > + .design-sync/.cache/pixelforge.css` — the editor's `pixelforge.css` (`.pf-*` + chrome) lives in the ImageEditor route chunk, so BOTH app CSS chunks are needed. + Re-sync must rebuild (`pnpm build`) then re-cat before the converter. +- **i18n:** editor components use `react-i18next` `t()`. `pixelforge-entry.ts` + self-initializes the global i18n instance in English (no provider needed) so + components render real copy. To add zh-CN, merge that resource in the entry. +- **Dark:** previews set `html.dark` (Frame `useEffect`) so portaled dialogs are + dark too — the editor is dark-only. +- **dtsPropsFor: intentionally NOT written** for this kit — 30 app-internal + components with editor-shaped props (EditorState/LayerEffect[]/handlers) would + be very costly + error-prone to hand-model. The `.d.ts` ship as loose index + signatures; usage is carried by each `.prompt.md` (embeds the authored + preview) + the conventions header. Add dtsPropsFor in a later re-sync if needed. +- **Scope:** 28 audited SYNC + 2 smoke = 30 synced. SKIPPED (need live canvas/ + pixels/state): Canvas, Workspace, RightSidebar, ChannelsPanel, ReplaceColorDialog, + ColorRangeDialog. **SaveForWebDialog** ships as a FLOOR CARD (its live + preview can't paint without the running editor; chrome is real). +- **Shared dirs:** previews live in the shared `.design-sync/previews/` (disjoint + names from the base kit — safe; the other kit's build only warns "stale preview" + and never deletes). Always pass `--components` on pf captures so an unscoped run + can't prune the base kit's grades. +- **Re-sync:** `node .ds-sync/resync.mjs --config .design-sync/pixelforge.config.json + --node-modules ./node_modules --out ./pf-bundle` (first sync omits --remote), with + `DS_CHROMIUM_PATH=/usr/bin/google-chrome`. + +## PixelForge re-sync risks +- Source bug noticed during the sync: a `Duplicate key "toolHint"` esbuild warning + (a duplicate object key somewhere in the editor source) — non-fatal, but worth + fixing in the app. diff --git a/.design-sync/pf-groups/ActionsPanel.md b/.design-sync/pf-groups/ActionsPanel.md new file mode 100644 index 0000000..459142e --- /dev/null +++ b/.design-sync/pf-groups/ActionsPanel.md @@ -0,0 +1,3 @@ +--- +category: Panels +--- diff --git a/.design-sync/pf-groups/AdjustPanel.md b/.design-sync/pf-groups/AdjustPanel.md new file mode 100644 index 0000000..459142e --- /dev/null +++ b/.design-sync/pf-groups/AdjustPanel.md @@ -0,0 +1,3 @@ +--- +category: Panels +--- diff --git a/.design-sync/pf-groups/AdjustmentDialog.md b/.design-sync/pf-groups/AdjustmentDialog.md new file mode 100644 index 0000000..71591ce --- /dev/null +++ b/.design-sync/pf-groups/AdjustmentDialog.md @@ -0,0 +1,3 @@ +--- +category: Dialogs +--- diff --git a/.design-sync/pf-groups/BrushesPanel.md b/.design-sync/pf-groups/BrushesPanel.md new file mode 100644 index 0000000..459142e --- /dev/null +++ b/.design-sync/pf-groups/BrushesPanel.md @@ -0,0 +1,3 @@ +--- +category: Panels +--- diff --git a/.design-sync/pf-groups/CanvasSizeDialog.md b/.design-sync/pf-groups/CanvasSizeDialog.md new file mode 100644 index 0000000..71591ce --- /dev/null +++ b/.design-sync/pf-groups/CanvasSizeDialog.md @@ -0,0 +1,3 @@ +--- +category: Dialogs +--- diff --git a/.design-sync/pf-groups/ColorPickerDialog.md b/.design-sync/pf-groups/ColorPickerDialog.md new file mode 100644 index 0000000..71591ce --- /dev/null +++ b/.design-sync/pf-groups/ColorPickerDialog.md @@ -0,0 +1,3 @@ +--- +category: Dialogs +--- diff --git a/.design-sync/pf-groups/ContextMenu.md b/.design-sync/pf-groups/ContextMenu.md new file mode 100644 index 0000000..811f09c --- /dev/null +++ b/.design-sync/pf-groups/ContextMenu.md @@ -0,0 +1,3 @@ +--- +category: Widgets +--- diff --git a/.design-sync/pf-groups/CurvesEditor.md b/.design-sync/pf-groups/CurvesEditor.md new file mode 100644 index 0000000..811f09c --- /dev/null +++ b/.design-sync/pf-groups/CurvesEditor.md @@ -0,0 +1,3 @@ +--- +category: Widgets +--- diff --git a/.design-sync/pf-groups/DropZone.md b/.design-sync/pf-groups/DropZone.md new file mode 100644 index 0000000..811f09c --- /dev/null +++ b/.design-sync/pf-groups/DropZone.md @@ -0,0 +1,3 @@ +--- +category: Widgets +--- diff --git a/.design-sync/pf-groups/FillDialog.md b/.design-sync/pf-groups/FillDialog.md new file mode 100644 index 0000000..71591ce --- /dev/null +++ b/.design-sync/pf-groups/FillDialog.md @@ -0,0 +1,3 @@ +--- +category: Dialogs +--- diff --git a/.design-sync/pf-groups/FilterDialog.md b/.design-sync/pf-groups/FilterDialog.md new file mode 100644 index 0000000..71591ce --- /dev/null +++ b/.design-sync/pf-groups/FilterDialog.md @@ -0,0 +1,3 @@ +--- +category: Dialogs +--- diff --git a/.design-sync/pf-groups/HistoryPanel.md b/.design-sync/pf-groups/HistoryPanel.md new file mode 100644 index 0000000..459142e --- /dev/null +++ b/.design-sync/pf-groups/HistoryPanel.md @@ -0,0 +1,3 @@ +--- +category: Panels +--- diff --git a/.design-sync/pf-groups/ImageSizeDialog.md b/.design-sync/pf-groups/ImageSizeDialog.md new file mode 100644 index 0000000..71591ce --- /dev/null +++ b/.design-sync/pf-groups/ImageSizeDialog.md @@ -0,0 +1,3 @@ +--- +category: Dialogs +--- diff --git a/.design-sync/pf-groups/LayerCompsPanel.md b/.design-sync/pf-groups/LayerCompsPanel.md new file mode 100644 index 0000000..459142e --- /dev/null +++ b/.design-sync/pf-groups/LayerCompsPanel.md @@ -0,0 +1,3 @@ +--- +category: Panels +--- diff --git a/.design-sync/pf-groups/LayerStyleDialog.md b/.design-sync/pf-groups/LayerStyleDialog.md new file mode 100644 index 0000000..71591ce --- /dev/null +++ b/.design-sync/pf-groups/LayerStyleDialog.md @@ -0,0 +1,3 @@ +--- +category: Dialogs +--- diff --git a/.design-sync/pf-groups/LayersPanel.md b/.design-sync/pf-groups/LayersPanel.md new file mode 100644 index 0000000..459142e --- /dev/null +++ b/.design-sync/pf-groups/LayersPanel.md @@ -0,0 +1,3 @@ +--- +category: Panels +--- diff --git a/.design-sync/pf-groups/MenuBar.md b/.design-sync/pf-groups/MenuBar.md new file mode 100644 index 0000000..77fa873 --- /dev/null +++ b/.design-sync/pf-groups/MenuBar.md @@ -0,0 +1,3 @@ +--- +category: Chrome +--- diff --git a/.design-sync/pf-groups/NewDocumentDialog.md b/.design-sync/pf-groups/NewDocumentDialog.md new file mode 100644 index 0000000..71591ce --- /dev/null +++ b/.design-sync/pf-groups/NewDocumentDialog.md @@ -0,0 +1,3 @@ +--- +category: Dialogs +--- diff --git a/.design-sync/pf-groups/OptionsBar.md b/.design-sync/pf-groups/OptionsBar.md new file mode 100644 index 0000000..77fa873 --- /dev/null +++ b/.design-sync/pf-groups/OptionsBar.md @@ -0,0 +1,3 @@ +--- +category: Chrome +--- diff --git a/.design-sync/pf-groups/PathsPanel.md b/.design-sync/pf-groups/PathsPanel.md new file mode 100644 index 0000000..459142e --- /dev/null +++ b/.design-sync/pf-groups/PathsPanel.md @@ -0,0 +1,3 @@ +--- +category: Panels +--- diff --git a/.design-sync/pf-groups/PropertiesPanel.md b/.design-sync/pf-groups/PropertiesPanel.md new file mode 100644 index 0000000..459142e --- /dev/null +++ b/.design-sync/pf-groups/PropertiesPanel.md @@ -0,0 +1,3 @@ +--- +category: Panels +--- diff --git a/.design-sync/pf-groups/RotateArbitraryDialog.md b/.design-sync/pf-groups/RotateArbitraryDialog.md new file mode 100644 index 0000000..71591ce --- /dev/null +++ b/.design-sync/pf-groups/RotateArbitraryDialog.md @@ -0,0 +1,3 @@ +--- +category: Dialogs +--- diff --git a/.design-sync/pf-groups/SaveForWebDialog.md b/.design-sync/pf-groups/SaveForWebDialog.md new file mode 100644 index 0000000..71591ce --- /dev/null +++ b/.design-sync/pf-groups/SaveForWebDialog.md @@ -0,0 +1,3 @@ +--- +category: Dialogs +--- diff --git a/.design-sync/pf-groups/SelectModifyDialog.md b/.design-sync/pf-groups/SelectModifyDialog.md new file mode 100644 index 0000000..71591ce --- /dev/null +++ b/.design-sync/pf-groups/SelectModifyDialog.md @@ -0,0 +1,3 @@ +--- +category: Dialogs +--- diff --git a/.design-sync/pf-groups/ShortcutsDialog.md b/.design-sync/pf-groups/ShortcutsDialog.md new file mode 100644 index 0000000..71591ce --- /dev/null +++ b/.design-sync/pf-groups/ShortcutsDialog.md @@ -0,0 +1,3 @@ +--- +category: Dialogs +--- diff --git a/.design-sync/pf-groups/Slider.md b/.design-sync/pf-groups/Slider.md new file mode 100644 index 0000000..811f09c --- /dev/null +++ b/.design-sync/pf-groups/Slider.md @@ -0,0 +1,3 @@ +--- +category: Widgets +--- diff --git a/.design-sync/pf-groups/StatusBar.md b/.design-sync/pf-groups/StatusBar.md new file mode 100644 index 0000000..77fa873 --- /dev/null +++ b/.design-sync/pf-groups/StatusBar.md @@ -0,0 +1,3 @@ +--- +category: Chrome +--- diff --git a/.design-sync/pf-groups/StrokeDialog.md b/.design-sync/pf-groups/StrokeDialog.md new file mode 100644 index 0000000..71591ce --- /dev/null +++ b/.design-sync/pf-groups/StrokeDialog.md @@ -0,0 +1,3 @@ +--- +category: Dialogs +--- diff --git a/.design-sync/pf-groups/ToolsPalette.md b/.design-sync/pf-groups/ToolsPalette.md new file mode 100644 index 0000000..77fa873 --- /dev/null +++ b/.design-sync/pf-groups/ToolsPalette.md @@ -0,0 +1,3 @@ +--- +category: Chrome +--- diff --git a/.design-sync/pf-groups/WarpTextDialog.md b/.design-sync/pf-groups/WarpTextDialog.md new file mode 100644 index 0000000..71591ce --- /dev/null +++ b/.design-sync/pf-groups/WarpTextDialog.md @@ -0,0 +1,3 @@ +--- +category: Dialogs +--- diff --git a/.design-sync/pixelforge-conventions.md b/.design-sync/pixelforge-conventions.md new file mode 100644 index 0000000..74cffa4 --- /dev/null +++ b/.design-sync/pixelforge-conventions.md @@ -0,0 +1,88 @@ +# PixelForge — Photoshop-style web UI kit + +The toolbox image editor's **pro-tool UI**, as a design system: menu/options/tool +bars, dockable panels (Layers, Properties, History…), and the full set of CC-style +modal dialogs. It is a **separate kit from the base Toolbox primitives** but built +on top of them (it composes Button/Dialog/Label internally) and shares the same +zinc/dark tokens, plus a dedicated **`.pf-*` chrome layer**. Every component is a +real compiled export on `window.PixelForge`. + +## Setup (do this first) + +- **Dark only.** PixelForge mirrors the editor, which runs in permanent dark mode. + Put `class="dark"` on the design root (or `html`). There is **no light variant** + and **no React provider** to wire. +- **i18n is pre-wired.** Components are localized with react-i18next; the bundle + self-initializes English on load, so `t()` renders real copy with zero setup. + Switch languages with `i18n.changeLanguage('zh-CN')` after adding that resource. +- Modal dialogs portal to ``; the root `.dark` makes them dark automatically. + +## The `.pf-*` chrome vocabulary + +Tokens/utilities are the **same semantic set as the base Toolbox kit** (`bg-card`, +`text-muted-foreground`, `border-input`, `rounded-*`, the `var(--*)` tokens — and +the same build-snapshot caveat: use `var(--token)` / inline styles for any utility +not already in `styles.css`). On top of that, PixelForge adds these chrome classes +(all defined in `styles.css`'s `@import`ed `_ds_bundle.css`): + +- **Shell / layout:** `pf-root`, `pf-shell`, `pf-canvas-area`, `pf-canvas-wrap`, `pf-right` +- **Menu bar:** `pf-menubar`, `pf-menubar-name`, `pf-menu-item`, `pf-menu-dd`, `pf-menu-backdrop` +- **Options bar:** `pf-options`, `pf-opt-group`, `pf-opt-label`, `pf-opt-input`, `pf-opt-select`, `pf-opt-btn` +- **Tools rail:** `pf-tools`, `pf-tool-group`, `pf-tool-btn`, `pf-tool-colors`, `pf-tool-sep` +- **Panels:** `pf-panel-group`, `pf-panel-tabs`, `pf-panel-tab`, `pf-panel-body`, `pf-scroll-y` +- **Layer rows:** `pf-layer-row`, `pf-layer-selected`, color tags `pf-tone-blue|neutral|warm` +- **Status bar:** `pf-statusbar` · **Context menu:** `pf-context-menu`, `pf-context-menu-item`, `pf-context-menu-sep`, `pf-context-menu-header`, `pf-context-menu-shortcut`, `pf-context-menu-danger` +- **Misc:** `pf-kbd`, `pf-active`, `pf-disabled`, `pf-open`, `pf-sep` + +## Props are editor-shaped — read the per-component docs + +Unlike the base primitives, these are app-internal components: their props are the +editor's own data (an `EditorState`, a `layers` array, a `handlers` object of +callbacks, an `open` flag for dialogs). The emitted `.d.ts` are therefore +**intentionally loose** (an index signature, not a hand-modelled interface) — so +**read each component's `.prompt.md`, which embeds a working preview with +realistic props**, rather than trusting the `.d.ts` shape. Dialogs render via an +`open` prop (or an open-key string); panels take plain data objects + noop-able +callbacks. + +## A typical editor layout + +``` +pf-root +├── MenuBar (handlers={…}) — File/Edit/Image/Select/Filter/Layer/View +├── OptionsBar (tool="brush" …) — context-sensitive tool options +└── pf-shell + ├── ToolsPalette (tool, setTool, fg/bg) — vertical tool rail + ├── pf-canvas-area (your ) — NOT in this kit (app-owned) + └── pf-right + ├── LayersPanel (state, selectedId, …) + ├── PropertiesPanel · HistoryPanel · ActionsPanel · PathsPanel · … +StatusBar (dimensions, zoom, tool…) — bottom bar +``` + +Modal dialogs (New Document, Image/Canvas Size, Fill, Stroke, Adjustment, Filter, +Layer Style, Warp Text, Rotate, Select Modify, Color Picker, Shortcuts) mount over +that shell when opened. `SaveForWebDialog` ships as a floor card — its chrome is +real but its live `` preview needs the running editor. + +## Idiomatic snippet + +```tsx +import { LayersPanel } from 'toolbox' // resolves to window.PixelForge + +export default function Panels() { + return ( +
+ {}} setLayers={() => {}} patchLayer={() => {}} + patchImageLayer={() => {}} deleteLayer={() => {}} onOpenStyle={() => {}} + renamingId={null} onStartRename={() => {}} onCommitRename={() => {}} onSetColorTag={() => {}} + /> +
+ ) +} +``` diff --git a/.design-sync/pixelforge-entry.ts b/.design-sync/pixelforge-entry.ts new file mode 100644 index 0000000..9ef8934 --- /dev/null +++ b/.design-sync/pixelforge-entry.ts @@ -0,0 +1,44 @@ +// design-sync PixelForge bundle entry (committed, durable). Auto-covers the +// editor's PS-style web UI kit. See pixelforge-conventions.md. +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' +import en from '@/i18n/en.json' + +if (!i18n.isInitialized) { + void i18n.use(initReactI18next).init({ + resources: { en: { translation: en } }, + lng: 'en', fallbackLng: 'en', interpolation: { escapeValue: false }, + }) +} + +// Keep in sync with componentSrcMap in pixelforge.config.json. +export * from '@/components/image-editor/NewDocumentDialog' +export * from '@/components/image-editor/ImageSizeDialog' +export * from '@/components/image-editor/CanvasSizeDialog' +export * from '@/components/image-editor/SaveForWebDialog' +export * from '@/components/image-editor/FillDialog' +export * from '@/components/image-editor/StrokeDialog' +export * from '@/components/image-editor/AdjustmentDialog' +export * from '@/components/image-editor/FilterDialog' +export * from '@/components/image-editor/LayerStyleDialog' +export * from '@/components/image-editor/WarpTextDialog' +export * from '@/components/image-editor/RotateArbitraryDialog' +export * from '@/components/image-editor/SelectModifyDialog' +export * from '@/components/image-editor/ColorPickerDialog' +export * from '@/components/image-editor/ShortcutsDialog' +export * from '@/components/image-editor/MenuBar' +export * from '@/components/image-editor/OptionsBar' +export * from '@/components/image-editor/ToolsPalette' +export * from '@/components/image-editor/StatusBar' +export * from '@/components/image-editor/LayersPanel' +export * from '@/components/image-editor/PropertiesPanel' +export * from '@/components/image-editor/HistoryPanel' +export * from '@/components/image-editor/ActionsPanel' +export * from '@/components/image-editor/PathsPanel' +export * from '@/components/image-editor/LayerCompsPanel' +export * from '@/components/image-editor/AdjustPanel' +export * from '@/components/image-editor/BrushesPanel' +export * from '@/components/image-editor/Slider' +export * from '@/components/image-editor/CurvesEditor' +export * from '@/components/image-editor/ContextMenu' +export * from '@/components/image-editor/DropZone' diff --git a/.design-sync/pixelforge.config.json b/.design-sync/pixelforge.config.json new file mode 100644 index 0000000..6c5bb92 --- /dev/null +++ b/.design-sync/pixelforge.config.json @@ -0,0 +1,151 @@ +{ + "pkg": "toolbox", + "globalName": "PixelForge", + "projectId": "9b036bf8-80bb-46a2-9e3c-7984a4484bce", + "shape": "package", + "buildCmd": "pnpm build", + "entry": ".design-sync/pixelforge-entry.ts", + "srcDir": "src/components/image-editor", + "tsconfig": "tsconfig.json", + "cssEntry": ".design-sync/.cache/pixelforge.css", + "readmeHeader": ".design-sync/pixelforge-conventions.md", + "componentSrcMap": { + "NewDocumentDialog": "src/components/image-editor/NewDocumentDialog.tsx", + "ImageSizeDialog": "src/components/image-editor/ImageSizeDialog.tsx", + "CanvasSizeDialog": "src/components/image-editor/CanvasSizeDialog.tsx", + "SaveForWebDialog": "src/components/image-editor/SaveForWebDialog.tsx", + "FillDialog": "src/components/image-editor/FillDialog.tsx", + "StrokeDialog": "src/components/image-editor/StrokeDialog.tsx", + "AdjustmentDialog": "src/components/image-editor/AdjustmentDialog.tsx", + "FilterDialog": "src/components/image-editor/FilterDialog.tsx", + "LayerStyleDialog": "src/components/image-editor/LayerStyleDialog.tsx", + "WarpTextDialog": "src/components/image-editor/WarpTextDialog.tsx", + "RotateArbitraryDialog": "src/components/image-editor/RotateArbitraryDialog.tsx", + "SelectModifyDialog": "src/components/image-editor/SelectModifyDialog.tsx", + "ColorPickerDialog": "src/components/image-editor/ColorPickerDialog.tsx", + "ShortcutsDialog": "src/components/image-editor/ShortcutsDialog.tsx", + "MenuBar": "src/components/image-editor/MenuBar.tsx", + "OptionsBar": "src/components/image-editor/OptionsBar.tsx", + "ToolsPalette": "src/components/image-editor/ToolsPalette.tsx", + "StatusBar": "src/components/image-editor/StatusBar.tsx", + "LayersPanel": "src/components/image-editor/LayersPanel.tsx", + "PropertiesPanel": "src/components/image-editor/PropertiesPanel.tsx", + "HistoryPanel": "src/components/image-editor/HistoryPanel.tsx", + "ActionsPanel": "src/components/image-editor/ActionsPanel.tsx", + "PathsPanel": "src/components/image-editor/PathsPanel.tsx", + "LayerCompsPanel": "src/components/image-editor/LayerCompsPanel.tsx", + "AdjustPanel": "src/components/image-editor/AdjustPanel.tsx", + "BrushesPanel": "src/components/image-editor/BrushesPanel.tsx", + "Slider": "src/components/image-editor/Slider.tsx", + "CurvesEditor": "src/components/image-editor/CurvesEditor.tsx", + "ContextMenu": "src/components/image-editor/ContextMenu.tsx", + "DropZone": "src/components/image-editor/DropZone.tsx" + }, + "docsMap": { + "NewDocumentDialog": ".design-sync/pf-groups/NewDocumentDialog.md", + "ImageSizeDialog": ".design-sync/pf-groups/ImageSizeDialog.md", + "CanvasSizeDialog": ".design-sync/pf-groups/CanvasSizeDialog.md", + "SaveForWebDialog": ".design-sync/pf-groups/SaveForWebDialog.md", + "FillDialog": ".design-sync/pf-groups/FillDialog.md", + "StrokeDialog": ".design-sync/pf-groups/StrokeDialog.md", + "AdjustmentDialog": ".design-sync/pf-groups/AdjustmentDialog.md", + "FilterDialog": ".design-sync/pf-groups/FilterDialog.md", + "LayerStyleDialog": ".design-sync/pf-groups/LayerStyleDialog.md", + "WarpTextDialog": ".design-sync/pf-groups/WarpTextDialog.md", + "RotateArbitraryDialog": ".design-sync/pf-groups/RotateArbitraryDialog.md", + "SelectModifyDialog": ".design-sync/pf-groups/SelectModifyDialog.md", + "ColorPickerDialog": ".design-sync/pf-groups/ColorPickerDialog.md", + "ShortcutsDialog": ".design-sync/pf-groups/ShortcutsDialog.md", + "MenuBar": ".design-sync/pf-groups/MenuBar.md", + "OptionsBar": ".design-sync/pf-groups/OptionsBar.md", + "ToolsPalette": ".design-sync/pf-groups/ToolsPalette.md", + "StatusBar": ".design-sync/pf-groups/StatusBar.md", + "LayersPanel": ".design-sync/pf-groups/LayersPanel.md", + "PropertiesPanel": ".design-sync/pf-groups/PropertiesPanel.md", + "HistoryPanel": ".design-sync/pf-groups/HistoryPanel.md", + "ActionsPanel": ".design-sync/pf-groups/ActionsPanel.md", + "PathsPanel": ".design-sync/pf-groups/PathsPanel.md", + "LayerCompsPanel": ".design-sync/pf-groups/LayerCompsPanel.md", + "AdjustPanel": ".design-sync/pf-groups/AdjustPanel.md", + "BrushesPanel": ".design-sync/pf-groups/BrushesPanel.md", + "Slider": ".design-sync/pf-groups/Slider.md", + "CurvesEditor": ".design-sync/pf-groups/CurvesEditor.md", + "ContextMenu": ".design-sync/pf-groups/ContextMenu.md", + "DropZone": ".design-sync/pf-groups/DropZone.md" + }, + "overrides": { + "NewDocumentDialog": { + "cardMode": "single", + "viewport": "560x540" + }, + "ImageSizeDialog": { + "cardMode": "single", + "viewport": "560x540" + }, + "CanvasSizeDialog": { + "cardMode": "single", + "viewport": "560x540" + }, + "SaveForWebDialog": { + "cardMode": "single", + "viewport": "760x560" + }, + "FillDialog": { + "cardMode": "single", + "viewport": "560x540" + }, + "StrokeDialog": { + "cardMode": "single", + "viewport": "560x540" + }, + "AdjustmentDialog": { + "cardMode": "single", + "viewport": "560x540" + }, + "FilterDialog": { + "cardMode": "single", + "viewport": "560x540" + }, + "LayerStyleDialog": { + "cardMode": "single", + "viewport": "760x580" + }, + "WarpTextDialog": { + "cardMode": "single", + "viewport": "560x540" + }, + "RotateArbitraryDialog": { + "cardMode": "single", + "viewport": "560x540" + }, + "SelectModifyDialog": { + "cardMode": "single", + "viewport": "560x540" + }, + "ColorPickerDialog": { + "cardMode": "single", + "viewport": "600x540" + }, + "ShortcutsDialog": { + "cardMode": "single", + "viewport": "760x620" + }, + "ToolsPalette": { + "cardMode": "single", + "viewport": "140x560" + }, + "ContextMenu": { + "cardMode": "single", + "viewport": "280x340" + }, + "MenuBar": { + "cardMode": "column" + }, + "OptionsBar": { + "cardMode": "column" + }, + "StatusBar": { + "cardMode": "column" + } + } +} \ No newline at end of file diff --git a/.design-sync/previews/ActionsPanel.tsx b/.design-sync/previews/ActionsPanel.tsx new file mode 100644 index 0000000..84b6279 --- /dev/null +++ b/.design-sync/previews/ActionsPanel.tsx @@ -0,0 +1,61 @@ +import * as React from 'react' +import { ActionsPanel } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} + onStartRecording={() => {}} + onStopRecording={() => {}} + onCancelRecording={() => {}} + onPlayAction={() => {}} + onDeleteAction={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/AdjustPanel.tsx b/.design-sync/previews/AdjustPanel.tsx new file mode 100644 index 0000000..7414c0c --- /dev/null +++ b/.design-sync/previews/AdjustPanel.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' +import { AdjustPanel } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} + adjust={{ + brightness: 110, + contrast: 95, + saturation: 130, + grayscale: 0, + blur: 0, + hue: 15, + sepia: 0, + invert: 0, + }} + setAdjust={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/AdjustmentDialog.tsx b/.design-sync/previews/AdjustmentDialog.tsx new file mode 100644 index 0000000..b692291 --- /dev/null +++ b/.design-sync/previews/AdjustmentDialog.tsx @@ -0,0 +1,40 @@ +import * as React from 'react' +import { AdjustmentDialog } from 'toolbox' + +// PixelForge mirrors the editor's app-level dark mode (html.dark). Setting it on +// the document root makes BOTH in-flow panels and portaled dialogs (which mount +// on , outside any wrapper) resolve the dark tokens correctly. +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} + onApply={() => {}} + onCancel={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/BrushesPanel.tsx b/.design-sync/previews/BrushesPanel.tsx new file mode 100644 index 0000000..9a759df --- /dev/null +++ b/.design-sync/previews/BrushesPanel.tsx @@ -0,0 +1,38 @@ +import * as React from 'react' +import { BrushesPanel } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Presets() { + return ( + + {}} + onSaveCurrent={() => {}} + onDeleteCustom={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/CanvasSizeDialog.tsx b/.design-sync/previews/CanvasSizeDialog.tsx new file mode 100644 index 0000000..50f8b53 --- /dev/null +++ b/.design-sync/previews/CanvasSizeDialog.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' +import { CanvasSizeDialog } from 'toolbox' + +// PixelForge mirrors the editor's app-level dark mode (html.dark). Setting it on +// the document root makes BOTH in-flow panels and portaled dialogs (which mount +// on , outside any wrapper) resolve the dark tokens correctly. +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} + onCancel={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/ColorPickerDialog.tsx b/.design-sync/previews/ColorPickerDialog.tsx new file mode 100644 index 0000000..9264f87 --- /dev/null +++ b/.design-sync/previews/ColorPickerDialog.tsx @@ -0,0 +1,31 @@ +import * as React from 'react' +import { ColorPickerDialog } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} onCancel={() => {}} /> + + ) +} diff --git a/.design-sync/previews/ContextMenu.tsx b/.design-sync/previews/ContextMenu.tsx new file mode 100644 index 0000000..1820032 --- /dev/null +++ b/.design-sync/previews/ContextMenu.tsx @@ -0,0 +1,48 @@ +import * as React from 'react' +import { ContextMenu } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {} }, + { id: 'rasterize', label: 'Rasterize Layer', onClick: () => {} }, + { id: 'blending', label: 'Blending Options…', onClick: () => {} }, + { sep: true }, + { id: 'merge', label: 'Merge Down', shortcut: 'Ctrl+E', onClick: () => {} }, + { id: 'flatten', label: 'Flatten Image', disabled: true, onClick: () => {} }, + { sep: true }, + { id: 'delete', label: 'Delete Layer', shortcut: 'Del', danger: true, onClick: () => {} }, + ]} + onClose={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/CurvesEditor.tsx b/.design-sync/previews/CurvesEditor.tsx new file mode 100644 index 0000000..2532d68 --- /dev/null +++ b/.design-sync/previews/CurvesEditor.tsx @@ -0,0 +1,41 @@ +import * as React from 'react' +import { CurvesEditor } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} + /> + + ) +} diff --git a/.design-sync/previews/DropZone.tsx b/.design-sync/previews/DropZone.tsx new file mode 100644 index 0000000..5240606 --- /dev/null +++ b/.design-sync/previews/DropZone.tsx @@ -0,0 +1,31 @@ +import * as React from 'react' +import { DropZone } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} /> + + ) +} diff --git a/.design-sync/previews/FillDialog.tsx b/.design-sync/previews/FillDialog.tsx new file mode 100644 index 0000000..208878e --- /dev/null +++ b/.design-sync/previews/FillDialog.tsx @@ -0,0 +1,40 @@ +import * as React from 'react' +import { FillDialog } from 'toolbox' + +// PixelForge mirrors the editor's app-level dark mode (html.dark). Setting it on +// the document root makes BOTH in-flow panels and portaled dialogs (which mount +// on , outside any wrapper) resolve the dark tokens correctly. +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} + onCancel={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/FilterDialog.tsx b/.design-sync/previews/FilterDialog.tsx new file mode 100644 index 0000000..9bf640e --- /dev/null +++ b/.design-sync/previews/FilterDialog.tsx @@ -0,0 +1,40 @@ +import * as React from 'react' +import { FilterDialog } from 'toolbox' + +// PixelForge mirrors the editor's app-level dark mode (html.dark). Setting it on +// the document root makes BOTH in-flow panels and portaled dialogs (which mount +// on , outside any wrapper) resolve the dark tokens correctly. +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} + onApply={() => {}} + onCancel={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/HistoryPanel.tsx b/.design-sync/previews/HistoryPanel.tsx new file mode 100644 index 0000000..427160f --- /dev/null +++ b/.design-sync/previews/HistoryPanel.tsx @@ -0,0 +1,32 @@ +import * as React from 'react' +import { HistoryPanel } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} /> + + ) +} diff --git a/.design-sync/previews/ImageSizeDialog.tsx b/.design-sync/previews/ImageSizeDialog.tsx new file mode 100644 index 0000000..fc1a07a --- /dev/null +++ b/.design-sync/previews/ImageSizeDialog.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' +import { ImageSizeDialog } from 'toolbox' + +// PixelForge mirrors the editor's app-level dark mode (html.dark). Setting it on +// the document root makes BOTH in-flow panels and portaled dialogs (which mount +// on , outside any wrapper) resolve the dark tokens correctly. +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} + onCancel={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/LayerCompsPanel.tsx b/.design-sync/previews/LayerCompsPanel.tsx new file mode 100644 index 0000000..92104b7 --- /dev/null +++ b/.design-sync/previews/LayerCompsPanel.tsx @@ -0,0 +1,63 @@ +import * as React from 'react' +import { LayerCompsPanel } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +const state = { + layerComps: [ + { + id: 'lc-1', + name: 'Hero Layout', + createdAt: '2026-06-20T10:00:00Z', + layers: [{}, {}, {}, {}], + imageLayer: {}, + }, + { + id: 'lc-2', + name: 'Mobile Variant', + createdAt: '2026-06-20T11:30:00Z', + layers: [{}, {}], + imageLayer: {}, + }, + { + id: 'lc-3', + name: 'Dark Mode', + createdAt: '2026-06-21T09:15:00Z', + layers: [{}, {}, {}], + imageLayer: {}, + }, + ], +} + +export function Default() { + return ( + + {}} + onApplyComp={() => {}} + onDeleteComp={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/LayerStyleDialog.tsx b/.design-sync/previews/LayerStyleDialog.tsx new file mode 100644 index 0000000..335ea97 --- /dev/null +++ b/.design-sync/previews/LayerStyleDialog.tsx @@ -0,0 +1,57 @@ +import * as React from 'react' +import { LayerStyleDialog } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} + onCancel={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/LayersPanel.tsx b/.design-sync/previews/LayersPanel.tsx new file mode 100644 index 0000000..9e7eeef --- /dev/null +++ b/.design-sync/previews/LayersPanel.tsx @@ -0,0 +1,118 @@ +import * as React from 'react' +import { LayersPanel } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} + setLayers={() => {}} + patchLayer={() => {}} + patchImageLayer={() => {}} + deleteLayer={() => {}} + onOpenStyle={() => {}} + renamingId={null} + onStartRename={() => {}} + onCommitRename={() => {}} + onSetColorTag={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/MenuBar.tsx b/.design-sync/previews/MenuBar.tsx new file mode 100644 index 0000000..2dd2dac --- /dev/null +++ b/.design-sync/previews/MenuBar.tsx @@ -0,0 +1,55 @@ +import * as React from 'react' +import { MenuBar } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}, + redo: () => {}, + open: () => {}, + save: () => {}, + exportPng: () => {}, + newDocument: () => {}, + zoomIn: () => {}, + zoomOut: () => {}, + openAdjustment: () => {}, + openFilter: () => {}, + openLayerStyle: () => {}, + duplicateLayer: () => {}, + deleteLayer: () => {}, + }} + /> + + ) +} diff --git a/.design-sync/previews/NewDocumentDialog.tsx b/.design-sync/previews/NewDocumentDialog.tsx new file mode 100644 index 0000000..a542530 --- /dev/null +++ b/.design-sync/previews/NewDocumentDialog.tsx @@ -0,0 +1,35 @@ +import * as React from 'react' +import { NewDocumentDialog } from 'toolbox' + +// PixelForge mirrors the editor's app-level dark mode (html.dark). Setting it on +// the document root makes BOTH in-flow panels and portaled dialogs (which mount +// on , outside any wrapper) resolve the dark tokens correctly. +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function NewDocument() { + return ( + + {}} onCancel={() => {}} /> + + ) +} diff --git a/.design-sync/previews/OptionsBar.tsx b/.design-sync/previews/OptionsBar.tsx new file mode 100644 index 0000000..6a49e60 --- /dev/null +++ b/.design-sync/previews/OptionsBar.tsx @@ -0,0 +1,69 @@ +import * as React from 'react' +import { OptionsBar } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} + liquifySize={120} + setLiquifySize={() => {}} + liquifyStrength={65} + setLiquifyStrength={() => {}} + fgColor="#3b82f6" + setFgColor={() => {}} + bgColor="#ffffff" + setBgColor={() => {}} + strokeWidth={12} + setStrokeWidth={() => {}} + brushOptions={{ hardness: 0.8, spacing: 0.25, flow: 1, opacity: 1 }} + setBrushOptions={() => {}} + textOptions={{ + fontSize: 48, + fontFamily: 'Helvetica', + fontWeight: 'bold', + fontStyle: 'normal', + align: 'left', + letterSpacing: 0, + lineHeight: 1.2, + underline: false, + }} + setTextOptions={() => {}} + bucketTolerance={32} + setBucketTolerance={() => {}} + wandTolerance={32} + setWandTolerance={() => {}} + selectionMode="replace" + setSelectionMode={() => {}} + feather={0} + setFeather={() => {}} + isStubTool={false} + cropAspectId="free" + setCropAspectId={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/PathsPanel.tsx b/.design-sync/previews/PathsPanel.tsx new file mode 100644 index 0000000..5d0c1db --- /dev/null +++ b/.design-sync/previews/PathsPanel.tsx @@ -0,0 +1,95 @@ +import * as React from 'react' +import { PathsPanel } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +const state = { + imageLayer: { id: 'image', kind: 'image', name: 'Background', visible: true, opacity: 100, blend: 'normal' }, + transforms: { rotation: 0, flipH: false, flipV: false }, + adjust: { + brightness: 100, + contrast: 100, + saturation: 100, + grayscale: 0, + blur: 0, + hue: 0, + sepia: 0, + invert: 0, + }, + selection: { x: 40, y: 30, w: 220, h: 160 }, + layers: [ + { + id: 'path-leaf', + kind: 'annotation', + name: 'Leaf Outline', + visible: true, + opacity: 100, + blend: 'normal', + shape: { + kind: 'path', + closed: true, + color: '#34d399', + strokeWidth: 2, + anchors: [ + { x: 20, y: 80, hout: { x: 10, y: -40 } }, + { x: 90, y: 20, hin: { x: -30, y: -10 }, hout: { x: 30, y: 10 } }, + { x: 160, y: 80, hin: { x: 10, y: -40 } }, + { x: 90, y: 150, hin: { x: 30, y: -10 }, hout: { x: -30, y: 10 } }, + ], + }, + }, + { + id: 'path-wave', + kind: 'annotation', + name: 'Signature Curve', + visible: true, + opacity: 100, + blend: 'normal', + shape: { + kind: 'path', + closed: false, + color: '#60a5fa', + strokeWidth: 2, + anchors: [ + { x: 10, y: 60, hout: { x: 30, y: -40 } }, + { x: 80, y: 40, hin: { x: -20, y: 30 }, hout: { x: 20, y: -30 } }, + { x: 150, y: 70, hin: { x: -30, y: -30 } }, + ], + }, + }, + ], +} + +export function Default() { + return ( + + {}} + onMakeWorkPath={() => {}} + onMakeSelectionFromPath={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/PropertiesPanel.tsx b/.design-sync/previews/PropertiesPanel.tsx new file mode 100644 index 0000000..263121e --- /dev/null +++ b/.design-sync/previews/PropertiesPanel.tsx @@ -0,0 +1,82 @@ +import * as React from 'react' +import { PropertiesPanel } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} + patchImageLayer={() => {}} + onOpenStyle={() => {}} + onReplaceSmartObjectContents={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/RotateArbitraryDialog.tsx b/.design-sync/previews/RotateArbitraryDialog.tsx new file mode 100644 index 0000000..aad2833 --- /dev/null +++ b/.design-sync/previews/RotateArbitraryDialog.tsx @@ -0,0 +1,34 @@ +import * as React from 'react' +import { RotateArbitraryDialog } from 'toolbox' + +// PixelForge mirrors the editor's app-level dark mode (html.dark). Setting it on +// the document root makes BOTH in-flow panels and portaled dialogs (which mount +// on , outside any wrapper) resolve the dark tokens correctly. +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} onCancel={() => {}} /> + + ) +} diff --git a/.design-sync/previews/SelectModifyDialog.tsx b/.design-sync/previews/SelectModifyDialog.tsx new file mode 100644 index 0000000..d00a42b --- /dev/null +++ b/.design-sync/previews/SelectModifyDialog.tsx @@ -0,0 +1,35 @@ +import * as React from 'react' +import { SelectModifyDialog } from 'toolbox' + +// PixelForge mirrors the editor's app-level dark mode (html.dark). Setting it on +// the document root makes BOTH in-flow panels and portaled dialogs (which mount +// on , outside any wrapper) resolve the dark tokens correctly. +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} onCancel={() => {}} /> + + ) +} diff --git a/.design-sync/previews/ShortcutsDialog.tsx b/.design-sync/previews/ShortcutsDialog.tsx new file mode 100644 index 0000000..fc88adc --- /dev/null +++ b/.design-sync/previews/ShortcutsDialog.tsx @@ -0,0 +1,31 @@ +import * as React from 'react' +import { ShortcutsDialog } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} /> + + ) +} diff --git a/.design-sync/previews/Slider.tsx b/.design-sync/previews/Slider.tsx new file mode 100644 index 0000000..869b4b2 --- /dev/null +++ b/.design-sync/previews/Slider.tsx @@ -0,0 +1,41 @@ +import * as React from 'react' +import { Slider } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} /> + + ) +} + +export function Variants() { + return ( + + {}} /> + {}} /> + {}} /> + + ) +} diff --git a/.design-sync/previews/StatusBar.tsx b/.design-sync/previews/StatusBar.tsx new file mode 100644 index 0000000..3f6e489 --- /dev/null +++ b/.design-sync/previews/StatusBar.tsx @@ -0,0 +1,42 @@ +import * as React from 'react' +import { StatusBar } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} + onZoomOut={() => {}} + onZoomReset={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/StrokeDialog.tsx b/.design-sync/previews/StrokeDialog.tsx new file mode 100644 index 0000000..cf201e7 --- /dev/null +++ b/.design-sync/previews/StrokeDialog.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' +import { StrokeDialog } from 'toolbox' + +// PixelForge mirrors the editor's app-level dark mode (html.dark). Setting it on +// the document root makes BOTH in-flow panels and portaled dialogs (which mount +// on , outside any wrapper) resolve the dark tokens correctly. +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} + onCancel={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/ToolsPalette.tsx b/.design-sync/previews/ToolsPalette.tsx new file mode 100644 index 0000000..fe6d447 --- /dev/null +++ b/.design-sync/previews/ToolsPalette.tsx @@ -0,0 +1,42 @@ +import * as React from 'react' +import { ToolsPalette } from 'toolbox' + +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} + fgColor="#f43f5e" + bgColor="#ffffff" + setFgColor={() => {}} + setBgColor={() => {}} + swapColors={() => {}} + resetColors={() => {}} + onStubClick={() => {}} + onOpenColorPicker={() => {}} + /> + + ) +} diff --git a/.design-sync/previews/WarpTextDialog.tsx b/.design-sync/previews/WarpTextDialog.tsx new file mode 100644 index 0000000..617392d --- /dev/null +++ b/.design-sync/previews/WarpTextDialog.tsx @@ -0,0 +1,41 @@ +import * as React from 'react' +import { WarpTextDialog } from 'toolbox' + +// PixelForge mirrors the editor's app-level dark mode (html.dark). Setting it on +// the document root makes BOTH in-flow panels and portaled dialogs (which mount +// on , outside any wrapper) resolve the dark tokens correctly. +function Frame({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + React.useEffect(() => { + const el = document.documentElement + el.classList.add('dark') + return () => el.classList.remove('dark') + }, []) + return ( +
+ {children} +
+ ) +} + +export function Default() { + return ( + + {}} + onApply={() => {}} + onCancel={() => {}} + /> + + ) +} diff --git a/.gitignore b/.gitignore index 6ac2913..bc60333 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ ds-bundle/ *.njsproj *.sln *.sw? + +# design-sync PixelForge kit (second DS) — regenerated bundle +pf-bundle/