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
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ The app includes **Agent**, an in-app AI assistant, and a local **MCP server** i
- `src/lib/persistence/projects.ts`: create a project, snapshot one workspace, reopen a recent project (values through `initialProperties`)
- `src/lib/persistence/recents.ts`: recent projects and their thumbnails (IndexedDB), recorded while editing and shown on the start screen (`/new`)
- `src/lib/icons/icons.ts`: the icon registry: the built-in Font Awesome set (`FaRocket`) plus the icon packs installed in the component library (`acme:cloud`). The Icon block and `@type:icon` variables of components offer both; `search_icons` reads it. `sanitize-svg.ts` cleans pack icons on import
- `src/lib/variables/variables.ts`: project variables (template slots). A workspace holds `variables`; text, code, window, QR and HTML blocks show `{{name}}` as the value through `useResolvedText` (`src/hooks/useProjectVariables.ts`) while the stored text keeps the placeholder. They are part of the canvas settings, so `setProjectVariables` is undoable; `set_variables` fills them
- `src/lib/brand/brand-kit.ts` / `src/stores/brand-store.ts`: the brand kit (colors, fonts, logos as data URLs, guidelines), kept in IndexedDB and edited in `BrandKitDialog`. The color and font pickers list it first; `get_brand_kit` and `add_brand_logo` give it to Agent and MCP clients
- `src/lib/variables/variables.ts`: project variables (template slots), edited in the Variables tab of the properties panel (`VariablesPanel`). A workspace holds `variables`; text, code, window, QR and HTML blocks show `{{name}}` as the value through `useResolvedText` (`src/hooks/useProjectVariables.ts`) while the stored text keeps the placeholder. They are part of the canvas settings, so `setProjectVariables` is undoable; `set_variables` fills them
- `src/lib/brand/brand-kit.ts` / `src/stores/brand-store.ts`: the brand kit (colors, fonts, logos as data URLs, guidelines), kept in IndexedDB and edited with `BrandKitEditor` (`src/components/Brand/`), shown in the Brand kit tab of the properties panel and, outside the editor, in `BrandKitDialog`. The color and font pickers list it first; `get_brand_kit` and `add_brand_logo` give it to Agent and MCP clients, and `update_brand_kit` / `save_brand_logo` let them write it (not part of the canvas undo)
- `src/lib/agent/core/design-guide.ts`: the design standards Agent and MCP clients follow (in the system prompt, the MCP instructions and `get_design_guide`)
- `src/lib/templates/starters.ts`: built-in templates on the start screen; each one is a size, a background and `addBlock` inputs (keys from `catalog.ts`)
- `src/components/Panels/RightPanel.tsx`: the properties panel and its tabs (hierarchy, control, workspace, variables, brand kit). Each tab is a `TabPage`: a title and a scroll area that takes only the height left (`min-h-0 flex-1`, never `h-full`, or the end of a long tab is cut off). `ui-store`'s `selectedTab` switches it from elsewhere
- `src/components/CustomControls/PropertyControls.tsx`: `PropertyRow`, `FieldInput`, `SliderField`, `ToggleGroup` / `ToggleButton` — use them for block property menus so rows line up
- `src/lib/editor/`: editor actions with arguments (`actions.ts`) and undo/redo helpers (`history.ts`)
- `src/lib/editor/layout.ts`: panel layouts (canvas, properties, agent, both). The layout is just `propertiesOpen` (`ui-store`, persisted) plus the agent's `panelOpen`; the status bar button and `view.layout-*` commands switch it. The agent docks as its own column at the left edge; the properties panel floats over the right edge of the canvas
Expand Down Expand Up @@ -110,6 +111,7 @@ Agent (assistant panel, `Mod+L`) and the MCP server share one set of tools. Full
- **History**: batch entries can mix property changes, `workspace-structure-*` snapshots and `workspace-settings-*` snapshots. Apply undo/redo with `undo()`/`redo()` from `src/lib/editor/history.ts`, which handles all of them.
- **Block catalog** (`src/lib/blocks/catalog.ts`): mirrors the `useControlState` keys and defaults of each block. Update it when a block gains or renames a property.
- **Component library** (`src/lib/agent/tools/components.ts`): `list_components`, `import_component`, `add_component`, `export_component` and `load_starter_pack` read and write the `.kcomponent` library (`src/stores/kcomponent-store.ts`, persisted) and put components on the canvas through `addBlock`. Adding one goes through `kcomponentBlockInput()` so an empty `css`/`js` section does not fall back to the demo content of a blank HTML block.
- **HTML block contract** (`src/lib/agent/tools/html-contract.ts`): HTML blocks written through `add_block`, `update_block` or `update_html_block` must have annotated `:root` CSS variables and `// @var` JS variables; the tools refuse other code with what to fix and turn `allow-scripts` on. `html-hints.ts` adds softer hints (too few variables, too big, too much text).
- **Command allowlist** (`src/lib/agent/tools/commands.ts`): `run_command` only runs `edit.*`, `arrange.*`, `view.*` and individually vetted ids. `tools.*` stays out on purpose (a model cannot drag on the canvas, so picking a tool would only strand the editor in a mode); dialogs, saving and `workspace.clean` stay out too. Widen it there, with the reason in the comment.
- **Providers** (`src/lib/agent/providers/`): pure adapters (Anthropic Messages, OpenAI Chat Completions, Gemini) that build requests and parse SSE streams; presets add base URLs and hints. The UI never depends on the provider.
- **Transports**: the web uses `fetch` from the page (the provider must allow CORS). Electron sends requests from the main process (`src-electron/agent/http.ts`), which adds the API key; keys are encrypted with `safeStorage` and bound to the origin they were saved for. Never log requests, headers or keys; use `redactSecrets()` for error text.
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## v 2.1.0 - Release (September 29th, 2026)

### 🚀 Features

- **Brand kit panel**: the brand kit has its own tab in the properties panel, next to the workspace settings and organized like them, in collapsible sections (name, colors, fonts, logos, guidelines) that show how many items each holds; File → **Brand kit…** opens it (outside the editor it still opens in a dialog)
- **Agent and MCP clients create and edit the brand kit**: `update_brand_kit` sets the name, palette, fonts by role (checked against Google Fonts), logo names and uses and the guidelines; `save_brand_logo` adds a logo from SVG markup, a data URL or an image block of the canvas. They do it when the user gives them their brand or asks for one
- **Variables tab**: project variables moved out of the workspace settings to their own tab of the properties panel
- **Every HTML block Agent and MCP clients write has CSS and JS variables**: the look as annotated `:root` variables and the content (labels, values, lists) as `// @var` JS variables that the script renders, so both are edited from the panel. `add_block`, `update_block` and `update_html_block` refuse a block without them, with a script that declares a variable twice or with a list that is not a JSON list of strings, say what to fix and turn scripts on for the block
- **Easier property panel for HTML blocks**: variables read as names ("Value size", not `--value-size`) in rows that line up with the other panels; numbers take a typed value next to the slider, lists edit each item in its own field (Enter applies, reorder, remove, add), objects edit each value in place or as JSON, and the shadow editor is a compact set of rows

### 🐛 Fixes

- The properties panel could not scroll to its end: its scroll area took the full height of the panel under the title and the align bar, so the last part of a tab with several sections open stayed hidden
- Going to the block editor and back (or anything else that shows the canvas again) could lose block positions and other values: every block painted its defaults for a frame and wrote them over its saved values before reading them back. Blocks now start from their saved values, and a custom background of a code block survives
- The first render of a code block showed raw `<pre><code>` markup before the code
- A text block exported while selected (or under the pointer) kept its blue outline in the image
- **Zoom to fit** and **Zoom to 100%** ignored the panels floating over the canvas: with Agent open the canvas ended up under the properties panel. They now fit and center it in the space left free
- HTML block scripts could not build elements: `document.createElement` threw "Illegal invocation", so lists rendered from a JS array stayed empty. Editing a list whose items were numbers did nothing, and a `// @var` value followed by a semicolon read as empty

## v 2.0.0 - Release (September 28th, 2026)

### 🚀 Features
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Karbonized is a visual editor for creating images of code snippets, mockups and

* **Agent, the design assistant** — describe the image you want and Agent builds it on the canvas, with Anthropic, OpenAI, Gemini, OpenRouter or local models (Ollama, LM Studio). It follows design standards made for social media, and everything it did undoes in one step.
* **MCP server** — Claude Desktop, Claude Code, Cursor and other MCP clients can design with Karbonized. They start it in the background when it is not open, and exports land in a folder without any dialog.
* **Templates with variables** — write `{{title}}`, `{{date}}` or `{{code}}` in any block and fill them from the Workspace panel (or let Agent do it): the next post is a new value, not a redesign.
* **Templates with variables** — write `{{title}}`, `{{date}}` or `{{code}}` in any block and fill them from the Variables panel (or let Agent do it): the next post is a new value, not a redesign.
* **Brand kit** — your colors, fonts, logos and guidelines in one place. The pickers offer them first and Agent reads them before designing.
* **Drawing tools** — a vector brush that follows pen pressure, shapes drawn by dragging, node editing, an eraser, rulers and guides.
* **Components and icon packs** — a reworked `.kcomponent` library with a starter pack, and icon packs that anyone can make from a folder of SVGs.
Expand Down
42 changes: 27 additions & 15 deletions docs/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ Agent and the MCP server share the same tools:
| `search_icons` | Icon names (Font Awesome and installed icon packs) for icon blocks and `@type:icon` variables |
| `search_fonts` | Google Fonts families by name, kind and weights, with the weights each one has |
| `get_brand_kit` | The brand kit: named colors, fonts, logos (without the images) and guidelines |
| `update_brand_kit` | Create or change the brand kit: name, palette, fonts by role (checked against Google Fonts), logo names and uses, guidelines |
| `save_brand_logo` | Add a logo to the brand kit from SVG markup (cleaned of scripts), a `data:image/…` URL or an image block of the canvas |
| `add_brand_logo` | Place a logo of the brand kit as an image block, by id or variant, keeping its proportions |
| `get_workspace` | Canvas size, background, selection and every block with its position, size and properties |
| `create_workspace` | New project with a canvas size, opened in the editor |
Expand Down Expand Up @@ -110,14 +112,20 @@ Change the guide there; the prompt, the instructions and the tool follow.
The guide asks models to build a design block by block: the background with
`set_canvas_background`, every headline and paragraph as a text block, and one
HTML block per component (a stat tile, a card, a badge row, a chart), sized to
its content and declaring its colors, sizes, radius, shadow and icon as
annotated `:root` variables. `add_block` and `update_html_block` check the
last two rules (`src/lib/agent/tools/html-hints.ts`) and answer with `hints`
when an HTML block declares no variables, covers most of the canvas or holds
paragraphs of text, so the model fixes it in the same turn. Content that
repeats (list items, chart data, rows) goes in `// @var` JS variables that the
block script renders, with `allow-scripts` on, so the user edits the data from
the panel; the hints flag a block whose script cannot run.
its content.

Every HTML block a model writes has html, css and js
(`src/lib/agent/tools/html-contract.ts`): the look as annotated `:root`
variables in the CSS and the content (labels, values, list items, chart data)
as `// @var` JS variables that the script writes into the markup, so the user
edits both from the panel. `add_block`, `update_block` and `update_html_block`
refuse code without them, with a script that declares a `// @var` again, or
with an array or object value that is not JSON (an array must be a list of
strings, which is what the panel edits), and say what to fix; they turn
`allow-scripts` on for the block. They also answer with `hints`
(`src/lib/agent/tools/html-hints.ts`) when an HTML block has too few
variables, covers most of the canvas or holds paragraphs of text, so the model
fixes it in the same turn.

Fonts: the guide sends models to `search_fonts` (the whole Google Fonts
catalog, with the weights of each family), gives pairings by tone and asks for
Expand All @@ -133,18 +141,22 @@ The snapshot waits for pending fonts so it does not show the fallback.

### Brand kit

File → **Brand kit…** (or the command palette) holds the colors, fonts,
logos and guidelines of the user's brand. The color picker shows the brand
colors first and the font picker the brand fonts. Agent and MCP clients are
told to call `get_brand_kit` before a new design and to follow it over the
palettes of the design guide; `add_brand_logo` places a logo without sending
the image through the model. A kit can be exported and imported as a
The **Brand kit** tab of the properties panel (also File → **Brand kit…** or
the command palette, which open it; outside the editor it opens in a dialog)
holds the colors, fonts, logos and guidelines of the user's brand. The color
picker shows the brand colors first and the font picker the brand fonts. Agent
and MCP clients are told to call `get_brand_kit` before a new design and to
follow it over the palettes of the design guide; `add_brand_logo` places a
logo without sending the image through the model. When the user gives them
their brand or asks for one, they save it with `update_brand_kit` and
`save_brand_logo` (`src/lib/agent/tools/brand.ts`); those changes are saved at
once, show in the tab, and are not part of the canvas undo. A kit can be exported and imported as a
`.kbrand` file (JSON). It is stored in IndexedDB (`src/stores/brand-store.ts`,
model in `src/lib/brand/brand-kit.ts`).

### Templates and project variables

A project can hold variables (Canvas panel → **Variables**): named texts,
A project can hold variables (the **Variables** tab of the properties panel): named texts,
long texts and dates. Any text, code, window, QR or HTML block that contains
`{{name}}` shows the value instead; the block keeps the placeholder, so the
same design becomes a template. Changing a value is one undo step.
Expand Down
17 changes: 13 additions & 4 deletions src/components/Base/MenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ export const MenuBar: React.FC = () => {
const [showDonations, setShowDonations] = useState(false);
const [showImportComponents, setShowImportComponents] = useState(false);
const [showBrandKit, setShowBrandKit] = useState(false);
/* In the editor the brand kit has its own tab in the properties panel;
elsewhere it opens in a dialog. */
const openBrandKit = () => {
if (location.pathname !== '/editor') {
setShowBrandKit(true);
return;
}
const ui = useUIStore.getState();
ui.setSelectedTab('brand');
ui.setPropertiesOpen(true);
};

/* KComponent Store */
/* The library dialog itself lives in the editor's left panel. */
Expand Down Expand Up @@ -328,7 +339,7 @@ export const MenuBar: React.FC = () => {
group: 'File',
icon: SwatchBook,
keywords: ['brand', 'colors', 'fonts', 'logo', 'palette'],
run: () => setShowBrandKit(true),
run: openBrandKit,
},
{
id: 'workspace.clean',
Expand Down Expand Up @@ -468,9 +479,7 @@ export const MenuBar: React.FC = () => {
</MenubarItem>

<MenubarSeparator />
<MenubarItem onClick={() => setShowBrandKit(true)}>
Brand Kit…
</MenubarItem>
<MenubarItem onClick={openBrandKit}>Brand Kit…</MenubarItem>
</MenubarContent>
</MenubarMenu>

Expand Down
50 changes: 37 additions & 13 deletions src/components/Blocks/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
IconTerminal,
IconX,
} from '@tabler/icons-react';
import React, { useEffect } from 'react';
import React, { useEffect, useRef } from 'react';
import { PropertyRow, SliderField } from '../CustomControls/PropertyControls';
import { Switch } from '../ui/switch';
import { ControlTemplate } from './ControlTemplate';
Expand All @@ -25,8 +25,9 @@ import { ColorPicker } from '../CustomControls/ColorPicker';
import { CloseSvg, MinimizeSvg } from '../Misc/Icons';
import { LanguajeTabIcon } from './LanguajeTabIcon';
import { useControlState } from '../../hooks/useControlState';
import { useControlsStore, useWorkspaceStore } from '@/stores';
import { useResolvedText } from '../../hooks/useProjectVariables';
import { themes } from '../../utils/PrismThemes';
import { codeThemeBackground, themes } from '../../utils/PrismThemes';
import { Label } from '../ui/label';
import { Input } from '../ui/input';
import {
Expand All @@ -46,11 +47,12 @@ const CodeControl: React.FC<Props> = ({ id }) => {

const [theme, setTheme] = useControlState('coldarkDark', `${id}-theme`);
const [language, setLanguage] = useControlState('jsx', `${id}-lang`);
const [code, setCode] = useControlState(
`<pre><code class="language-${language}"></code></pre>`,
`${id}-code`,
const [code, setCode] = useControlState('', `${id}-code`);
// A new block starts on the background of its theme.
const [color, setColor] = useControlState(
codeThemeBackground(theme) ?? '#111b28',
`${id}-bgcolor`,
);
const [color, setColor] = useControlState('#111b28', `${id}-bgcolor`);
const [controlsColor, setControlsColor] = useControlState(
'#b4b4b4',
`${id}-ccolor`,
Expand Down Expand Up @@ -86,18 +88,40 @@ const CodeControl: React.FC<Props> = ({ id }) => {
return themes.find((value) => value.label === theme)?.theme;
};

/* Handle Change Theme Colors */
/* A new block keeps the background it starts on, so Agent and saved
projects read the color it shows rather than the catalog default. */
useEffect(() => {
const newTheme = themes.find((value) => value.label === theme)?.theme;

setColor(
(newTheme as any)[':not(pre) > code[class*="language-"]'].background ||
(newTheme as any)[`code[class*="language-"]`].background,
const key = `${id}-bgcolor`;
const { ControlProperties, initialProperties, addControlProperty } =
useControlsStore.getState();
const known = [...ControlProperties, ...initialProperties].some(
(item) => item.id === key,
);
if (!known) {
addControlProperty(
{ id: key, value: color },
useWorkspaceStore.getState().currentWorkspaceID,
);
}
// Only the color the block mounted with.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);

/* Picking a theme or the paper style resets the background to theme it.
Only when they change: mounting again (after the block editor, a
workspace switch, a project load) keeps the background the user chose. */
const shownTheme = useRef(theme);
useEffect(() => {
if (shownTheme.current === theme) return;
shownTheme.current = theme;
const background = codeThemeBackground(theme);
if (background) setColor(background);
}, [theme]);

/* Handle Window Style Change - Set color for paper style */
const shownWindowStyle = useRef(windowStyle);
useEffect(() => {
if (shownWindowStyle.current === windowStyle) return;
shownWindowStyle.current = windowStyle;
if (windowStyle === 'paper') {
setColor('#fbfaf7');
}
Expand Down
Loading
Loading