Style tools framework
Paths below are relative to apps/frontend/src/app/(dashboard)/editor/[projectId]/[pageId]/Components/ unless they start with @/.
Files
| File | Role |
|---|---|
StyleController/StyleToolsPanel.tsx | Builds the context, renders the search box, groups and tools from the registry. |
StyleTools/StyleToolContext.tsx | StyleToolContext, useStyleTool, useStyleWriter, useStyleProperty. |
StyleTools/PropertyRow.tsx | One labelled control: set indicator, CSS-wide keyword menu, remove, SourceBtn. |
StyleTools/KeywordMenu.tsx | ⋯ menu for inherit / initial / unset / revert. |
StyleTools/PropertyField.tsx | Renders a FieldSpec as a PropertyRow + input. |
StyleTools/ToolSection.tsx | Collapsible tool block with a set-count badge. Shared with the Props tab. |
StyleTools/toolDefinitions.tsx | TOOL_GROUPS registry and FnDef lists for filters/transforms. |
StyleTools/toolOptions.ts | Keyword lists (border styles, cursors, blend modes, fonts, …). |
StyleTools/tools/*.tsx | Custom tools: BorderTool, ShadowTool, FunctionListTool, BackgroundLayersTool, TransitionTool, RawCssTool, LayoutTool. |
StyleTools/SourceBtn.tsx, GroupSourceBtn.tsx, RemoveBtn.tsx | Pre-existing row controls. DisplayTool.tsx is wrapped by LayoutTool. |
@/Utils/Style/cssValue.ts | Value parsing helpers (see Utilities). |
@/Utils/color.ts | Color conversion, including alpha-aware parsing/formatting. |
StyleToolsPanel
<StyleToolsPanel
selectedEID={string[]}
iframeDoc={Document}
controlledStyle={Record<string, string>}
payload={StyleUpdateActionPayload | null}
getPayload?={(property: string) => StyleUpdateActionPayload | null}
viewSource={boolean} // unused, kept for callers
sourceChanger?={() => void} // unused, kept for callers
getSource?={StyleSourceGetter}
onMoveSource?={StyleSourceMover}
getMatchedRules?={MatchedRulesGetter}
cssPagesContentJSON?={JCSSSheets}
/>| Caller | controlledStyle | Payload | Source props |
|---|---|---|---|
GeneralStylePanel (Style target) | Specified properties with computed values, intersected across the selection | getPayload per property (inline if set inline, else the cascade winner, honoring the forced state) | Yes |
InlineStylePanel | Parsed style attribute, intersected | Fixed inline payload | No |
MatchedRulePanel | The rule’s declarations | Fixed CLASS payload for the rule | No |
cssVariables and keyframeNames in the context are collected from cssPagesContentJSON, so they are empty for the inline and matched-rule targets.
Search matches every whitespace-separated word against the tool title, group title and property list. A non-empty query sets forceOpen on every visible ToolSection.
StyleToolContextValue
| Field | Type | Meaning |
|---|---|---|
controlledStyle | Record<string, string> | Current value per property. |
payload | StyleUpdateActionPayload | null | Fixed payload template. |
getPayload | (property) => StyleUpdateActionPayload | null | Used when payload is null. |
getSource, onMoveSource, getMatchedRules, cssPagesContentJSON | — | Source routing; all four present ⇒ PropertyRow shows SourceBtn. |
cssVariables | string[] | --* names declared in the sheets, for ColorInput. |
keyframeNames | string[] | @keyframes names, for the animation-name suggestions. |
useStyleTool() throws outside the provider.
useStyleWriter()
Returns setProperties(values: Record<string, string>, live = false).
- One
STYLE-UPDATEpayload per property frompayload ?? getPayload(property); properties without a payload are skipped. One property ⇒ object payload (eligible forActionManager’s inline merge), several ⇒ array. - Reverse value is
controlledStyle[property] ?? "". An empty value removes the declaration. - Live gestures. The first
live: truecall executes the action and keeps it in a ref keyed by the property list; later calls with the same key callaction.update(...)instead of creating actions. Thelive: falsecall that ends the gesture updates and releases the action. The whole gesture is one undo step whose reverse is the pre-gesture value. - A non-live call that wouldn’t change any value is skipped.
Depends on ActionManager.execute returning the created (or merged-into) Action.
useStyleProperty(property)
{ value, isSet, set(next, live?), clear() } for one property.
PropertyRow
| Prop | Type | Meaning |
|---|---|---|
label | string | Row label. |
properties | string[] | Properties the row writes. The first decides keyword state; remove/keyword write all of them. |
children | ReactNode | The input. Replaced by a keyword × button while every property holds the same CSS-wide keyword. |
stacked | boolean | Put the input under the label (wide editors). |
SourceBtn is shown only for single-property rows with full source routing in the context.
ToolSection
| Prop | Type | Meaning |
|---|---|---|
id | string | Key for the module-level open-state Set, which survives remounts for the session. HTML tools prefix ids with html-. |
title | string | Header text. |
setCount | number | Badge value; hidden at 0. |
forceOpen | boolean | Open regardless of the remembered state. |
FieldSpec
PropertyField renders a FieldSpec. label defaults to the property name in sentence case; units defaults to CSS_PROPERTY_UNITS[property].
kind | Input | Extra fields | Layout |
|---|---|---|---|
unit | MeasureInput | units, step, min, placeholder | inline |
number | NumberInput | min, max, step, integer, keywords | inline |
slider | SliderInput | min, max, step, defaultValue, suffix | inline |
select | SelectInput | options, allowCustom | inline |
segmented | SegmentedInput | options, multiple, noneValue | inline |
color | ColorInput + value text | — | inline |
text | TextInput | placeholder, suggestions (array or (ctx) => string[]), multiline | inline; stacked if multiline |
position | PositionInput | — | stacked |
easing | EasingInput | — | stacked |
clipPath | ClipPathInput | — | stacked |
gridAreas | GridAreasInput | — | stacked |
sides | SidesInput | label, properties: Record<Side, string>, units | stacked |
corners | CornersInput | label, properties: Record<Corner, string> | stacked |
fieldProperties(field) returns every property a field writes.
Registry
type ToolDef = {
id: string;
title: string;
properties: string[]; // badge + search
fields?: FieldSpec[];
render?: () => ReactNode; // custom tool
};
type ToolGroupDef = { id: string; title: string; tools: ToolDef[] };
export const TOOL_GROUPS: ToolGroupDef[];Helpers inside toolDefinitions.tsx: fieldTool(id, title, fields) derives properties; customTool(id, title, properties, render); seg(values) builds segmented options; sides(prefix, suffix?) builds a side→property map.
Groups: Typography, Backgrounds & color, Box model & sizing, Borders & outlines, Layout & positioning, Flexbox item, Grid, Multi-column, Effects, Transforms, Transitions & animations, Interaction & UI, Media & images, Tables, Generated content, Advanced. The designer style tools reference lists every tool and property.
Custom tools
| Tool | Writes | Notes |
|---|---|---|
BorderTool | border-{side}-{width,style,color} | all writes the four longhands; a side writes one. Reads show a value only when the targeted sides agree. |
ShadowTool | box-shadow or text-shadow (property prop) | Parses layers with splitTopLevel; tokens starting with a digit/sign are lengths, inset is the flag, anything else is the color. |
FunctionListTool | filter, backdrop-filter, transform | defs: FnDef[]; unknown functions are kept with a single raw argument. |
BackgroundLayersTool | background-image/size/position/repeat | Writes only lists whose per-layer values changed; a list that is all defaults is removed. |
TransitionTool | transition-property/duration/timing-function/delay | Always writes all four lists. |
RawCssTool | any | Every non-empty entry of controlledStyle, plus an add line (/^(--)?[a-z-]+$/). |
LayoutTool | display, flex and grid container properties | Adapts DisplayTool’s getValue/setValue to the context. GroupSourceBtn is not shown (no class-list getter). |
type FnParam = { label: string; units?: readonly string[]; default: string };
type FnDef = { name: string; params: FnParam[]; separator?: "," | " " };A param without units is a free-text TextInput. separator: " " keeps the whole argument string as one param (drop-shadow, matrix).
Forced state
StyleControllerPanel holds forcedState ("" = neutral), resets it when the selection key changes, and:
- lists rules with
getMatchedRules(elem, forcedState ? { activePseudoClasses: [forcedState] } : {}); - calls
applyForcedState(iframeDoc, selectedEID, forcedState)in an effect (deps: doc, selection, state, both content JSONs) whose cleanup removes the style element; - passes
forcedStatetoGeneralStylePanel, which uses it ingetPayloadandresolveSource.
StyleController/forcedState.ts:
| Export | Meaning |
|---|---|
availableStates(el) | hover, active, focus-within always; focus, focus-visible if el matches the focusable selector; visited for a[href], area[href]. |
applyForcedState(doc, eids, state) | Removes #handrix-forced-state; when state and eids are non-empty, appends a <style id="handrix-forced-state" data-handrix-editor> to the iframe head with buildForcedStateCss(state, '[data-eid="…"], …'). |
STATE_LABELS, STATE_SHORT | Menu and button labels. |
See ADR-0002.
Utilities
@/Utils/Style/cssValue.ts
| Export | Meaning |
|---|---|
CSS_WIDE_KEYWORDS, isCssWideKeyword(v) | inherit, initial, unset, revert. |
splitTopLevel(value, "," | " " | "/") | Splits outside parentheses and quotes. |
listItem(list, i, fallback) | CSS list repetition (list[i % length]). |
parseCssFunction(v) | { name, args } or null. |
collectCssVariables(sheets) | Sorted --* names from rule declarations. |
collectKeyframeNames(sheets) | Sorted @keyframes / @-webkit-keyframes names from verbatim at-rule segments. |
@/Utils/color.ts additions
| Export | Meaning |
|---|---|
parseCssColor(v): RGBA | null | Any color the canvas 2D context understands, with alpha. null for empty, var() or invalid input (detected with two different fill sentinels). |
rgbToHsl, formatColor(rgba, "hex" | "rgb" | "hsl"), detectColorFormat(v) | Conversion and formatting; alpha < 1 emits #rrggbbaa, rgba(), hsla(). |
ActionManager.execute
Now returns Action: the new action, or the previous action when an inline STYLE-UPDATE was merged into it.
Related
- Explanation: Style and props tools architecture
- How-to: Add a style tool
- Reference: Editor inputs