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
17 changes: 9 additions & 8 deletions .claude/skills/add-block-type/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
---
name: add-block-type
description: Checklist for adding a new widget block type to WidgeCode (e.g. GitHub commits, pull requests, GitHub status, new data source) across server registry, stats fetching, SVG export, client editor/canvas, locales, and tests. Use whenever a new block, block option, or data source is being added.
description: Checklist for adding a new widget block type to WidgeCode (e.g. GitHub commits, pull requests, GitHub status, new data source) across server registry, stats fetching, the shared SVG renderer, client editor, locales, and tests. Use whenever a new block, block option, or data source is being added.
argument-hint: '[block-type-id]'
---

# Add a block type

A block type exists in several places that are not linked by types. Miss one and the block will
validate on the server but render blank in the editor, or render in the editor but not in the SVG export.
validate on the server but render as a blank box. Rendering itself is written once: the browser and
the `/image.svg` export use the same SVG components.

## 1. Server registry — `server/src/widgets/registry.ts` (source of truth)

Expand All @@ -24,26 +25,26 @@ validate on the server but render blank in the editor, or render in the editor b
- Branch on the new type in `getBlockData`. Throw `AppError` for "not found"; `renderWidgetStats` turns errors into `block.error`.
- Mind the budget: public SVG/iframe views call this per request (15 min cache per instance). Prefer one API call per block.

## 3. SVG export — `server/src/shared/widget/WidgetCanvas.tsx`
## 3. Rendering — `server/src/shared/widget/BlockContent.tsx` (used everywhere)

- Add a block renderer in `BlockContent` next to the existing ones. Use the `frame` (content width/height + `blockTypography`), `baseline()`, `fitText`, `StatsRow`/`TitleRow` helpers — no hard-coded font sizes.
- Put new labels in `server/src/shared/widget/theme.ts` (`widgetLabels`, ru + en) so both renderers share them.
- Handle loading/`error`/empty data states like the other blocks.
- Any image must be a data URI (see `buildAvatarDataUris` in `widgetController.ts`) — external `href`s don't load when the SVG is used as `<img>` on GitHub.
- Primitives live in `svgPrimitives.tsx`; the block shell and canvas pieces in `canvasParts.tsx`.
- Put new labels in `server/src/shared/widget/theme.ts` (`widgetLabels`, ru + en).
- Loading (`BlockSkeleton`), `error` and missing-username states are handled before the type switch; make sure the new block doesn't bypass them.
- Images: the export needs data URIs (see `buildAvatarDataUris` in `widgetController.ts`) because external `href`s don't load when the SVG is used as `<img>` on GitHub; in the browser the URL is used directly.

## 4. Client — `client/src/entities/widget/`

- `model/types.ts`: extend `BlockType` and the rendered data types.
- `model/registry.ts`: labels + ru/en descriptions, presets mirror.
- `ui/WidgetCanvas.tsx` + `.module.css`: add `sampleData[type]` (shown before live data) and the HTML renderer, mirroring the SVG element order and using only the `--block-*` custom properties — see the `widget-render-parity` skill.
- `pages/widget-editor/ui/WidgetEditorPage.tsx`: block palette entry and settings controls for each option.
- `shared/locale/content.ts`: all new strings in both `ru` and `en`.

## 5. Tests and docs

- `server/src/widgets.test.ts`: creating/updating a widget with the new block validates; bad config → 400.
- `server/src/statsService.test.ts`: mock `fetch`, assert the mapped shape and cache key.
- `server/src/services/widgetCanvas.test.ts`: SVG renders the block without throwing and contains expected text.
- `server/src/services/widgetCanvas.test.ts`: SVG renders the block without throwing and contains expected text; `client/src/entities/widget/ui/WidgetCanvas.test.tsx` for browser-only states if any.
- `CHANGELOG.md` → `[Unreleased] / Added`. Update `README*.md` feature list if it's user-visible.
- Finish with the `verify` skill.

Expand Down
55 changes: 29 additions & 26 deletions .claude/skills/widget-render-parity/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,44 @@
---
name: widget-render-parity
description: Diagnose and fix size, padding, gap, scale, font, or background differences between the widget in the editor, the public page/iframe, and the exported SVG image, and keep them in sync when changing widget geometry or block visuals. Use for bugs like "widget looks different in SVG", "blocks are smaller in export", "preview scale is off", or any change to block layout/typography.
description: Explains how WidgeCode renders widgets (one shared SVG renderer for the editor, public page, iframe and /image.svg export) and how to debug size, layout or text differences. Use for bugs like "widget looks different in SVG", "blocks are cut off", "preview scale is off", or when changing widget geometry or block visuals.
---

# Widget render parity
# Widget rendering

## Architecture
There is one renderer. Everything that draws a widget uses the same SVG React components from
`server/src/shared/widget/` (imported on the client as `@shared/widget/*`).

| Piece | File | Role |
| ------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Geometry | `server/src/shared/widget/geometry.ts` | Width 600, `canvasPadding` (24), `GRID_GAP` 18, square `cellSize` (267), `BLOCK_PADDING` 20, radii, line heights, `widgetDimensions`, `blockBox`, `blockTypography` |
| Theme | `server/src/shared/widget/theme.ts` | Palettes, language/difficulty colors, ru/en block labels, `formatStatValue` |
| HTML renderer | `client/src/entities/widget/ui/WidgetCanvas.tsx` + `.module.css` | Editor blocks, `/w/:slug`, iframe embed |
| HTML bridge | `client/src/entities/widget/lib/canvasStyle.ts` | Turns geometry into CSS custom properties (`canvasStyleVars`, `blockStyleVars`, `blockMetrics`) |
| Scaling | `client/src/entities/widget/ui/ScaledWidgetFrame.tsx` | Renders the canvas at stored size and scales it to fit (editor, public page) |
| SVG renderer | `server/src/shared/widget/WidgetCanvas.tsx` | `/api/public/widgets/:slug/image.svg` |
| Stored size | `widgetService.widgetDimensions` → shared `widgetDimensions` | Drives iframe size and SVG viewBox |
| Piece | File | Role |
| ------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Geometry | `geometry.ts` | Width 600, padding, gap, cell size, block padding, radii, line heights, `widgetDimensions`, `blockBox`, `blockTypography`, `estimateTextWidth`, heatmap sizing |
| Theme | `theme.ts` | Palettes, colors, ru/en block labels, `formatStatValue` |
| Primitives | `svgPrimitives.tsx` | `SvgText`, `MultilineText`, `TitleRow`, `StatsRow`, `fitText`, `linesOf`, `baseline`, `layoutOf` |
| Block content | `BlockContent.tsx` | Per-type drawing inside a block's content box, plus skeleton / error / preview states |
| Canvas parts | `canvasParts.tsx` | `CanvasBackground`, `BlockShell`, `CanvasEmptyState`, `canvasBoxes`, `canvasTokens` |
| Export | `WidgetCanvas.tsx` | Composes the parts into one standalone `<svg>` for `/api/public/widgets/:slug/image.svg` |
| Browser | `client/src/entities/widget/ui/WidgetSurface.tsx` | Same parts, but the background and each block are separate `<svg>`s placed with `canvasBoxes`, so the editor can attach DOM controls (drag handle, size, remove) to each block |
| Scaling | `client/src/entities/widget/ui/ScaledWidgetFrame.tsx` | Renders at stored size and scales to fit (editor, public page) |

Rules:

- A number that affects layout belongs in `geometry.ts` / `theme.ts`, never hard-coded in one renderer.
- The HTML canvas CSS must only use the custom properties — no `vw`, `cqi`, `%` font sizes or media queries. Responsiveness comes from `ScaledWidgetFrame`, not reflow.
- The SVG has no text layout: text widths are estimated (`fitText`, `linesOf`, glyph-width constants). HTML uses `nowrap` + ellipsis on the same elements so both truncate in the same places. `formatStatValue` picks full vs compact numbers from the same estimate in both renderers.
- The SVG uses Inter metrics for baselines (`baseline()`); the HTML canvas sets the same font stack (`WIDGET_FONT_FAMILY`) and `--block-line-height`.
- Layout numbers belong in `geometry.ts` / `theme.ts`.
- SVG has no text layout: widths come from `estimateTextWidth` (per-character-class widths
calibrated to the font). Truncate with `fitText`, wrap with `linesOf`; never assume CSS will do it.
- SVG ids (gradients, clip paths) must be prefixed per instance (`idPrefix`) — several widgets can
be on one page in the browser.
- The export inlines images as data URIs; the browser uses URLs.

## Changing block visuals
## Debugging differences

1. Change geometry/typography in `geometry.ts` if a size changes; expose new values in `blockStyleVars` and consume them in the CSS module.
2. Update the HTML markup/CSS and the SVG block renderer in the same change, keeping the element order and gaps identical (the SVG comments name the CSS classes they mirror).
3. Extend `server/src/shared/widget/geometry.test.ts` or `server/src/services/widgetCanvas.test.ts` with the new numbers.
Because the code is shared, differences between the editor, iframe and export come from inputs,
not drawing: different `height`/`rows` (the editor adds a spare row while dragging), different
`renderedBlocks` (editor previews vs public data), locale (`?locale=` for the export), or fonts
installed on the viewer's machine (affects real text width vs the estimate).

## Visual check

1. `docker compose -p widgecode up -d`, then start the `api` and `client` launch configs (`.claude/launch.json`).
2. Create/publish a test widget via the API (local test account), then open side by side at the same zoom:
- `/w/<slug>?embed=1` (iframe content, real size)
- `http://localhost:4000/api/public/widgets/<slug>/image.svg` (add `?locale=ru` to match a Russian UI)
- `/w/<slug>` and the editor `/widgets/<id>` (scaled)
3. Known intentional difference: the SVG locale comes from `?locale=` rather than the app setting.
4. Finish with the `verify` skill.
2. Create/publish a test widget via the API (local test account), then compare at the same zoom:
`/w/<slug>?embed=1`, `http://localhost:4000/api/public/widgets/<slug>/image.svg?locale=ru`,
`/w/<slug>` and the editor `/widgets/<id>`.
3. Finish with the `verify` skill.
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ server/src/
lib/ env, jwt, prisma singleton, AppError
server/prisma/ schema.prisma + migrations
client/src/ Feature-Sliced Design: app → pages → widgets → features → entities → shared
entities/widget/ widget types, presets, palettes (mirror server registry) and the HTML/CSS WidgetCanvas used in editor/iframe
entities/widget/ widget types, presets, palettes (mirror server registry); WidgetSurface/WidgetCanvas render the shared SVG parts in the browser
pages/widget-editor/ the grid editor: model/ (layout rules, normalizeWidget, useWidgetEditor
= load/autosave/save race handling, useGridDrag, useBlockPreviews) + ui/ components
shared/api/ fetch client with access-token + single-flight refresh
Expand All @@ -73,8 +73,8 @@ Server imports must use `.js` extensions (NodeNext).

## Invariants and gotchas

- **One geometry, two renderers.** All widget numbers (width 600, padding, gap, square cells, block padding, typography scale, palettes, labels, stat formatting) live in `server/src/shared/widget/{geometry,theme}.ts`. The HTML canvas (`client/src/entities/widget/ui/WidgetCanvas.tsx`, used by the editor, public page and iframe) gets them as CSS custom properties via `entities/widget/lib/canvasStyle.ts`; the SVG export (`server/src/shared/widget/WidgetCanvas.tsx`) uses them directly; `widgetService` stores sizes from `widgetDimensions`. The HTML canvas always renders at the stored size and is scaled by `ScaledWidgetFrame` — never add `vw`/`cqi`/`%`-based sizes to it. See the `widget-render-parity` skill.
- **Block types are defined twice.** Server `widgets/registry.ts` (zod schemas, presets) and client `entities/widget/model/{types,registry}.ts` (labels, ru/en descriptions). Adding a block type touches both plus `statsService` and both canvases — use the `add-block-type` skill.
- **One renderer.** Every surface (editor, public page, iframe, `/image.svg`) draws widgets with the same SVG components in `server/src/shared/widget/` (`BlockContent`, `canvasParts`, `svgPrimitives`), using numbers from `geometry.ts`/`theme.ts`. The export composes them into one `<svg>` (`WidgetCanvas.tsx`); the browser places the background and each block as separate `<svg>`s (`client/src/entities/widget/ui/WidgetSurface.tsx`) so the editor can attach controls, and scales the whole thing with `ScaledWidgetFrame`. Text widths are estimated (`estimateTextWidth`), so truncate/wrap in SVG code, not CSS. See the `widget-render-parity` skill.
- **Block types are defined twice.** Server `widgets/registry.ts` (zod schemas, presets) and client `entities/widget/model/{types,registry}.ts` (labels, ru/en descriptions). Rendering is written once in `shared/widget/BlockContent.tsx`. Use the `add-block-type` skill.
- Limits: `MAX_WIDGET_BLOCKS = 5` in the registry; `MAX_GRID_COLUMNS = 2` and block height ≤ 2 in shared `geometry.ts` (re-exported by the registry). Layout validation (bounds + no overlap) runs in `widgetService.validateLayouts`.
- Widget width is currently fixed at 600 and height is recomputed from rows by `widgetDimensions` on every update — client-supplied `width`/`height` are ignored.
- GitHub data: profile, repositories and languages use REST (a token only raises limits; languages switch to exact GraphQL byte counts with a token). Activity, pull request and status blocks use GraphQL only and show an error without `GITHUB_TOKEN`.
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ All notable changes to this project are documented here.

### Changed

- The editor, public page and iframe now render widgets with the same SVG components as the image export (one renderer instead of an HTML and an SVG copy), so every surface shows exactly the same widget.
- SVG text truncation and stat number formatting now use a per-character width estimate calibrated to the widget font (Cyrillic, bold and `%` are wider), so the SVG export cuts text in the same places as the HTML widget.
- Split the 1,200-line widget editor into layout rules, state/autosave, drag and preview hooks, and focused UI components, and shared the embed snippet builder between the gallery and the editor.
- Added client tests for widget rendering, scaling, editor layout rules and autosave, the API client token refresh, and the auth store.
Expand Down Expand Up @@ -51,6 +52,7 @@ All notable changes to this project are documented here.

### Security

- Removed the `tsc-alias` build dependency (its `chokidar`/`globby` chain pulled in `braces`, which has an unpatched high-severity advisory, GHSA-vfj7-8cjw-p6xm); server path aliases are now resolved by a small `server/scripts/resolve-aliases.mjs`. Updated `brace-expansion` and `ip-address` for newly published advisories.
- Rate limits now key on the real client IP behind the Vercel proxy instead of one shared proxy IP.
- Split auth rate limits: login, registration and OAuth stay strict; session refresh, `/me` and logout get a separate, higher limit.
- Yandex sign-in no longer links to an existing email/password account with the same email (which allowed pre-registered account takeover); such users get a clear message to sign in with their password.
Expand Down
8 changes: 2 additions & 6 deletions client/src/entities/widget/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ export {
WidgetCardSkeleton,
type WidgetCardLabels,
} from '@/entities/widget/ui/WidgetCard';
export { blockStyleVars, canvasStyleVars, getBlockLayout } from '@/entities/widget/lib/canvasStyle';
export {
WidgetBlockContent,
WidgetCanvas,
WidgetCanvasSkeleton,
} from '@/entities/widget/ui/WidgetCanvas';
export { WidgetCanvas, WidgetCanvasSkeleton } from '@/entities/widget/ui/WidgetCanvas';
export { WidgetSurface, type SurfaceBlockSlot } from '@/entities/widget/ui/WidgetSurface';
export { ScaledWidgetFrame } from '@/entities/widget/ui/ScaledWidgetFrame';
export * from '@/entities/widget/model';
26 changes: 0 additions & 26 deletions client/src/entities/widget/lib/canvasStyle.test.ts

This file was deleted.

Loading
Loading