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
2 changes: 1 addition & 1 deletion docs/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ Property controls are driven by the selected node's module schema (`src/core/mod

At the top of the Properties Panel, the selector picker is the single entry point for CSS rules that affect the selected element. Assigned class rules render as removable `TagPill` chips and are stored on `node.classIds`; matching ambient rules render as non-removable `TagPill` chips because they apply by selector matching, not assignment. Ambient rules that match only through a universal subject (`*`, `body.x *`, `*::before`) never render as pills — they style every element in their scope and stay reachable through the dropdown and the Selectors panel instead. The dropdown searches both class rules and ambient selectors, is capped to the picker width budget, and ellipsizes long selector labels instead of expanding across the editor. Ambient rows that do not match the selected canvas element stay visible but disabled with the mismatch reason, and selector-shaped input such as `.hero .title`, `h1`, or `a:hover` creates an ambient rule instead of a class.

The Framework panel's **Type** tab stores Google/custom font assets and editable font tokens together under `site.settings.fonts`. Installed font assets own the self-hosted `@font-face` files; font tokens own the builder-facing variables such as `--font-primary`, the assigned font asset, and the fallback stack. The property-panel `font-family` control is a rich picker: token rows write `var(--font-primary)` so the selected node or class keeps following future token swaps, direct font rows write a concrete family stack as an escape hatch, and the text input still accepts manual values.
The Framework panel's **Type** tab stores Google/custom font assets and editable font tokens together under `site.settings.fonts`. Installed font assets own the self-hosted `@font-face` files; font tokens own the builder-facing variables such as `--font-heading`, the assigned font asset, and the fallback stack. New sites start with the three role tokens `--font-heading`, `--font-body` and `--font-mono` as system-font fallbacks, and the "Manage framework" dialog adds any of them a site lacks (see [site-shell.md](features/site-shell.md) → font role tokens); the new-token dialog pre-fills the first missing role name. The property-panel `font-family` control is a rich picker: token rows write `var(--font-heading)` so the selected node or class keeps following future token swaps, direct font rows write a concrete family stack as an escape hatch, and the text input still accepts manual values.

When the user clicks a rule in the Selectors Panel, the Properties Panel switches to **selector-editing mode** — the body shows style controls for that rule directly, and the header renders `SelectorHeader` with the rule's CSS selector, an inline rename input, and a delete button. The delete and rename actions are only shown for non-generated rules and require `site.style.edit`.

Expand Down
2 changes: 1 addition & 1 deletion docs/features/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ Drivers that support explicit prompt-cache controls (Anthropic) apply `cache_con
`SYSTEM_PROMPT_DYNAMIC_BOUNDARY` is the literal `'__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'`, declared **once** in `server/ai/runtime/types.ts` and imported everywhere — prompt builders and every driver. A duplicate definition would silently break prompt caching on whichever driver drifted. Gated by `ai-driver-shared-helpers.test.ts`.

**Static prefix key rules** (full text lives in `server/ai/tools/site/systemPrompt.ts`):
- **Design system first.** Establish or reuse tokens before/while building (`site_set_color_tokens`, `site_set_type_scale`, `site_set_spacing_scale`, `site_set_font_tokens`), then reference them in CSS (`var(--<slug>)`, `var(--text-l)`, `var(--space-m)`, `var(--<font-var>)`) instead of raw hex/px/font-family. The dynamic suffix's `Tokens —` line shows what already exists; `(none …)` means no design system yet.
- **Design system first.** Establish or reuse tokens before/while building (`site_set_color_tokens`, `site_set_type_scale`, `site_set_spacing_scale`, `site_set_font_tokens`), then reference them in CSS (`var(--<slug>)`, `var(--text-l)`, `var(--space-m)`, `var(--font-heading)`) instead of raw hex/px/font-family. Fonts use the role tokens `font-heading`, `font-body` and `font-mono`, which new sites already have; the prompt tells the model to change a typeface by updating the role token rather than inventing a new font variable. The dynamic suffix's `Tokens —` line shows what already exists; `(none …)` means no design system yet.
- Structure as HTML (`site_insert_html` / `site_replace_node_html`); style with CSS in the same payload — a `<style>` block and/or `class=` attributes referencing the design tokens. The importer classifies selectors, so the agent never hand-builds classes at insert time.
- `<style>` blocks inside imported HTML are parsed: class-bearing selectors become Selectors-panel classes while preserving complex selector text, class-free selectors (`a:hover`) become ambient rules, and supported `@keyframes` publish as raw keyframes CSS. `style=` attributes land on the node's inline styles. These are applied — not stripped.
- CSS-only edits use an explicit `site_apply_css` operation: merge for additive patches, remove-properties for stale declarations, replace only with the selector's complete desired CSS, and delete for whole exact rules. Read the document first before destructive operations; grouped and ungrouped selectors are different identities.
Expand Down
2 changes: 1 addition & 1 deletion docs/features/publisher.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ Two invariants:

Both call sites read through the one helper so publish and Live preview can't drift.

Installed fonts are emitted through the CSS bundle, not external `<link>` tags. The font CSS includes self-hosted `@font-face` rules for `site.settings.fonts.items` plus `:root` declarations for editable tokens such as `--font-primary`. A page rule can therefore keep `font-family: var(--font-primary)` while the token assignment changes site-wide.
Installed fonts are emitted through the CSS bundle, not external `<link>` tags. The font CSS includes self-hosted `@font-face` rules for `site.settings.fonts.items` plus `:root` declarations for editable tokens such as `--font-heading`. A page rule can therefore keep `font-family: var(--font-heading)` while the token assignment changes site-wide.

Plugins inject at four anchors. The order matters — see [docs/features/plugin-system.md](plugin-system.md) for the splicing rules.

Expand Down
2 changes: 1 addition & 1 deletion docs/features/site-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ The escape hatch for "this sheet's resets/styles must not leak into other pages

- **`PageConflict`** — a desired slug collides with an existing page slug or with another slug in the same import batch.
- **`RuleConflict`** — a `kind:'class'` rule's name collides with an existing class name. Ambient rules never conflict. One row per name even when the name arrives as several cascade fragments (they rename together).
- **`TokenConflict`** — a design-token CSS custom property collides with an existing token. One type covers both colour tokens (keyed by `--<slug>`, against `framework.colors.tokens`) and font tokens (keyed by `--font-*`, against `fonts.tokens`), since both are just a `--var` contract referenced by `var(--x)` in the imported CSS. Imported tokens are deduped per kind upstream, so only site-vs-import collisions occur.
- **`TokenConflict`** — a design-token CSS custom property collides with an existing token. One type covers both colour tokens (keyed by `--<slug>`, against `framework.colors.tokens`) and font tokens (keyed by `--font-*`, against `fonts.tokens`), since both are just a `--var` contract referenced by `var(--x)` in the imported CSS. Imported tokens are deduped per kind upstream, so only site-vs-import collisions occur. Token conflicts default to auto-rename, with one exception: when the existing font token is an untouched seeded role token (`font-heading`, `font-body` or `font-mono` with its seeded name and fallback and no bound family, checked by `isUntouchedFontRoleToken` in `src/core/fonts/roles.ts`), the default is overwrite. The imported family and fallback land on the role token and imported rules keep `var(--font-body)`, so importing into a new site behaves as if the token were absent. A role token the user has edited keeps the rename default.
- **`CrossSheetClassConflict`** — two imported page cascades define the same class differently (see "Cross-sheet class conflicts" above). Keyed by `(desiredName, definitionId)`; resolutions apply FIRST in `applyConflictResolutions` so a renamed definition no longer participates in the site-vs-import conflict on the original name.

Page slugs can be slash-delimited public paths. Root `index.html` stays the homepage slug `index`; nested `index.html` files use their directory route, so `documentation/index.html` imports as `/documentation` and does not collide with `download-version/index.html`.
Expand Down
17 changes: 15 additions & 2 deletions docs/features/site-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,19 @@ type SiteSettings = {

The Colors / Framework Scale / Typography panels write to these sub-trees. All values are emitted into the published `framework.css` by `buildSiteFrameworkCss(site)` via `buildFrameworkPlan(settings)`, which returns the merged `:root` variable block and the locked utility classes from a single ordered traversal per family. (`generateFrameworkRootCss` / `generateFrameworkUtilityClasses` remain for single-output callers such as the canvas preview and the editor's class reconciler.)

`fonts` is the installed font library and font-token contract (`src/core/fonts/`). `fonts.items` is the installed self-hosted font asset library (Google downloads and custom media-backed font files). `fonts.tokens` is the editable builder-facing contract: each token owns a stable CSS variable such as `font-primary`, an optional assigned `FontEntry` id, and a fallback stack. The publisher emits those as `:root { --font-primary: "Family", sans-serif; }`; editor controls should prefer `font-family: var(--font-primary)` over raw family names when the design should follow future font swaps.
`fonts` is the installed font library and font-token contract (`src/core/fonts/`). `fonts.items` is the installed self-hosted font asset library (Google downloads and custom media-backed font files). `fonts.tokens` is the editable builder-facing contract: each token owns a stable CSS variable such as `font-heading`, an optional assigned `FontEntry` id, and a fallback stack. The publisher emits those as `:root { --font-heading: "Family", sans-serif; }`; editor controls should prefer `font-family: var(--font-heading)` over raw family names when the design should follow future font swaps.

**Font role tokens** (`src/core/fonts/roles.ts`). Three role variables are the shared contract that imported pages, copied sections and the AI agent write against, so content made for one site renders in another:

| Token | Name | Use | Seeded fallback |
|-------|------|-----|-----------------|
| `font-heading` | Heading | headings and display type | `system-ui, -apple-system, Segoe UI, Roboto, sans-serif` |
| `font-body` | Body | running text | same system sans stack |
| `font-mono` | Mono | code, data and mono labels | `ui-monospace, SFMono-Regular, Menlo, Consolas, monospace` |

They are seeded fallback-only (no `familyId`), so a new site renders them without downloading anything; the user binds real families in the Fonts panel. Nothing styles `body` or headings with them automatically: the publisher reset pins `body` to the system stack and authored CSS opts in with `var(--font-body)` / `var(--font-heading)`.

**New-site settings.** `createNewSiteSettings()` (`src/core/page-tree/siteSettings.ts`) is what every brand-new site starts with: `{ shortcuts: {}, fonts: { items: [], tokens: <the three role tokens> } }`. First-run setup writes it into the new shell (`newSiteShellStorage()` in `server/repositories/site.ts`) and the editor's draft bootstrap uses it through `createDefaultSiteDocument`. `DEFAULT_SITE_SETTINGS` (`{ shortcuts: {} }`) stays the read-time fallback for a stored shell without settings, so an existing site never gains tokens on upgrade. Applying the Core Framework preset (Full or Variables only) adds whichever role tokens a site lacks, matched by variable, and never changes tokens the site already has; choosing None leaves font tokens alone. Site import treats a role token that is still exactly as seeded as a placeholder: an imported `--font-body` fills it in place instead of being renamed (see [site-import.md](site-import.md) → `TokenConflict`).

**Variable normalization** (`src/admin/pages/site/store/slices/site/fontActions.ts`): the `variable` field stores a slug without the leading `--`. User input is trimmed, lowercased, and invalid character runs replaced with `-`. Leading `--` is stripped before storage. Duplicate variables within a site are rejected. Examples: `--font Brand` → `font-brand`, `Editorial` → `font-editorial`.

Expand Down Expand Up @@ -761,14 +773,15 @@ A plugin canvas module can then `import * as THREE from 'three'` and it resolves
- [docs/reference/design-tokens.md](../reference/design-tokens.md) — editor token catalog
- Source-of-truth files:
- `src/core/page-tree/siteDocument.ts` — `SiteShellSchema`, `SiteDocument`, `parseSiteDocument`
- `src/core/page-tree/siteSettings.ts` — `SiteSettingsSchema`, `DEFAULT_SITE_SETTINGS`, `parseSiteSettings`
- `src/core/page-tree/siteSettings.ts` — `SiteSettingsSchema`, `DEFAULT_SITE_SETTINGS`, `createNewSiteSettings`, `parseSiteSettings`
- `src/core/page-tree/breakpoint.ts` — `BreakpointSchema`, `DEFAULT_BREAKPOINTS`
- `src/core/page-tree/condition.ts` — `ConditionDefSchema`, `conditionId`, `conditionLabel`, `makeConditionDef`, `parseConditions`
- `src/core/page-tree/styleRule.ts` — `StyleRuleSchema`
- `src/core/framework-schema/schemas.ts` — `FrameworkSettingsSchema`, `FrameworkColorToken`, `FrameworkColorSettings`, `FrameworkPreferencesSettingsSchema`, `GeneratedClassMetadataSchema` (pure leaf — no engine dependency)
- `src/core/framework/generate.ts` — `buildFrameworkPlan`, `generateFrameworkRootCss`, `generateFrameworkUtilityClasses`
- `src/core/fonts/schemas.ts` — `SiteFontsSettingsSchema`, `FontEntry`, `FontToken`
- `src/core/fonts/css.ts` — `generateFontsCss`
- `src/core/fonts/roles.ts` — `FONT_ROLES`, `buildFontRoleTokens`, `mergeFontRoleTokens`
- `src/core/files/schemas.ts` — `SiteFileSchema`, `SiteFileType`, `SiteFileBlobSchema`
- `src/core/site-dependencies/manifest.ts` — `SitePackageJsonSchema`
- `src/core/site-runtime/schemas.ts` — `SiteRuntimeConfigSchema`
Expand Down
7 changes: 6 additions & 1 deletion server/ai/tools/site/systemPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ Building:
Design system first:
- A consistent design comes from TOKENS, not repeated literals. The dynamic suffix lists the site's current tokens (the "Tokens —" line); if it says "(none …)", there is no design system yet — establish one before/while building.
- Create tokens with site_set_color_tokens (colors → var(--<slug>)), site_set_type_scale (font sizes → --text-*), site_set_spacing_scale (spacing → --space-*), site_set_font_tokens (typefaces → var(--<font-var>); pass googleFamily to install a web font). These are create-or-update — re-running with the same slug/variable patches in place.
- Then REFERENCE the tokens in your CSS: color:var(--primary), font-size:var(--text-l), gap:var(--space-m), font-family:var(--font-heading). Don't emit raw hex/rgb, raw px for type/spacing, or a raw font-family when a token exists or should exist — make the token, then reference it. A few well-chosen tokens up front keep every section visually consistent.
- Fonts use three ROLE tokens: font-heading (headings, display type), font-body (running text), font-mono (code, data, mono labels). New sites already have all three as system-font fallbacks. To change a typeface, update the role token (site_set_font_tokens with variable "font-heading" + googleFamily) instead of inventing a new font variable; add one only for a genuinely extra role.
- Then REFERENCE the tokens in your CSS: color:var(--primary), font-size:var(--text-l), gap:var(--space-m), font-family:var(--font-heading) on headings and var(--font-body) on running text. Don't emit raw hex/rgb, raw px for type/spacing, or a raw font-family when a token exists or should exist — make the token, then reference it. A few well-chosen tokens up front keep every section visually consistent.

Structure as HTML, styling as CSS:
- Structure goes in site_insert_html/site_replace_node_html as semantic HTML. Style it with CSS in the SAME call: a <style> block and/or class= attributes (the importer turns these into reusable classes + ambient rules), referencing the design tokens above. This is the clean default; do NOT hand-build classes node-by-node.
Expand Down Expand Up @@ -107,6 +108,10 @@ function describeTokenDigest(tokens: SnapshotTokens): string {
if (parts.length === 0) {
return 'Tokens: (none — no design system yet; establish one first with site_set_color_tokens / site_set_type_scale / site_set_spacing_scale / site_set_font_tokens)'
}
// A new site starts with only the font role tokens: still no design system.
if (tokens.colors.length === 0 && tokens.typography.length === 0 && tokens.spacing.length === 0) {
parts.push('(no colors, type or spacing yet; establish them first with site_set_color_tokens / site_set_type_scale / site_set_spacing_scale)')
}
return `Tokens — ${parts.join('; ')}`
}

Expand Down
2 changes: 1 addition & 1 deletion server/ai/tools/site/writeTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ const setFontTokensTool: AiTool = {
execution: 'browser',
requiredCapabilities: SITE_STYLE_CAPS,
description:
'Create or update FONT tokens — named typefaces referenced as `var(--<variable>)`. Pass `googleFamily` (e.g. "Inter") to install a new Google web font (downloads the files, then binds the token to it); `variants` defaults to ["400","700"] and `subsets` to ["latin"]. Pass `familyId` to reference an already-installed family. Pass neither for a fallback-only/system token. Create-or-update is keyed by `variable` (defaults from `name`). Prefer exactly one of `googleFamily` or `familyId`; if both are sent, `googleFamily` wins and the stale `familyId` is ignored.',
'Create or update FONT tokens — named typefaces referenced as `var(--<variable>)`. Pass `googleFamily` (e.g. "Inter") to install a new Google web font (downloads the files, then binds the token to it); `variants` defaults to ["400","700"] and `subsets` to ["latin"]. Pass `familyId` to reference an already-installed family. Pass neither for a fallback-only/system token. Create-or-update is keyed by `variable` (defaults from `name`). Prefer the role variables `font-heading`, `font-body` and `font-mono`: new sites start with them as fallback-only system stacks, so setting a typeface means updating the role token. Prefer exactly one of `googleFamily` or `familyId`; if both are sent, `googleFamily` wins and the stale `familyId` is ignored.',
inputSchema: SetFontTokensInputSchema,
}

Expand Down
6 changes: 3 additions & 3 deletions server/handlers/cms/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import { nanoid } from 'nanoid'
import type { DbClient } from '../../db/client'
import { hashPassword } from '../../auth/tokens'
import { createSite, getSetupStatus } from '../../repositories/setup'
import { createSite, getSetupStatus, newSiteShellStorage } from '../../repositories/setup'
import { createUser } from '../../repositories/users'
import { createAuditEvent } from '../../repositories/audit'
import { createDataRow } from '../../repositories/data'
Expand Down Expand Up @@ -80,7 +80,7 @@ export async function handleSetupRoutes(req: Request, db: DbClient): Promise<Res
return serializeCollabAwareWrite(async () => {
let homePageId = ''
const response = await db.transaction(async (tx) => {
await createSite(tx, siteName, {})
await createSite(tx, siteName, newSiteShellStorage())
const owner = await createUser(tx, {
id: nanoid(),
email,
Expand Down Expand Up @@ -142,7 +142,7 @@ interface PublicSiteIdentity {
* favicon resolve to null. TypeBox objects allow extra properties by default,
* so validating only `faviconUrl` still type-checks it with zero `as` casts
* while staying immune to unrelated settings fields. Every level is optional
* so a freshly-created site (`settings_json = {}`) yields a null favicon
* so a shell without stored settings (`settings_json = {}`) yields a null favicon
* instead of throwing.
*/
const StoredSiteIdentitySchema = Type.Object({
Expand Down
Loading
Loading