ReferenceStyle tools framework

Style tools framework

Paths below are relative to apps/frontend/src/app/(dashboard)/editor/[projectId]/[pageId]/Components/ unless they start with @/.

Files

FileRole
StyleController/StyleToolsPanel.tsxBuilds the context, renders the search box, groups and tools from the registry.
StyleTools/StyleToolContext.tsxStyleToolContext, useStyleTool, useStyleWriter, useStyleProperty.
StyleTools/PropertyRow.tsxOne labelled control: set indicator, CSS-wide keyword menu, remove, SourceBtn.
StyleTools/KeywordMenu.tsx⋯ menu for inherit / initial / unset / revert.
StyleTools/PropertyField.tsxRenders a FieldSpec as a PropertyRow + input.
StyleTools/ToolSection.tsxCollapsible tool block with a set-count badge. Shared with the Props tab.
StyleTools/toolDefinitions.tsxTOOL_GROUPS registry and FnDef lists for filters/transforms.
StyleTools/toolOptions.tsKeyword lists (border styles, cursors, blend modes, fonts, …).
StyleTools/tools/*.tsxCustom tools: BorderTool, ShadowTool, FunctionListTool, BackgroundLayersTool, TransitionTool, RawCssTool, LayoutTool.
StyleTools/SourceBtn.tsx, GroupSourceBtn.tsx, RemoveBtn.tsxPre-existing row controls. DisplayTool.tsx is wrapped by LayoutTool.
@/Utils/Style/cssValue.tsValue parsing helpers (see Utilities).
@/Utils/color.tsColor 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}
/>
CallercontrolledStylePayloadSource props
GeneralStylePanel (Style target)Specified properties with computed values, intersected across the selectiongetPayload per property (inline if set inline, else the cascade winner, honoring the forced state)Yes
InlineStylePanelParsed style attribute, intersectedFixed inline payloadNo
MatchedRulePanelThe rule’s declarationsFixed CLASS payload for the ruleNo

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

FieldTypeMeaning
controlledStyleRecord<string, string>Current value per property.
payloadStyleUpdateActionPayload | nullFixed payload template.
getPayload(property) => StyleUpdateActionPayload | nullUsed when payload is null.
getSource, onMoveSource, getMatchedRules, cssPagesContentJSON—Source routing; all four present ⇒ PropertyRow shows SourceBtn.
cssVariablesstring[]--* names declared in the sheets, for ColorInput.
keyframeNamesstring[]@keyframes names, for the animation-name suggestions.

useStyleTool() throws outside the provider.

useStyleWriter()

Returns setProperties(values: Record<string, string>, live = false).

  • One STYLE-UPDATE payload per property from payload ?? getPayload(property); properties without a payload are skipped. One property ⇒ object payload (eligible for ActionManager’s inline merge), several ⇒ array.
  • Reverse value is controlledStyle[property] ?? "". An empty value removes the declaration.
  • Live gestures. The first live: true call executes the action and keeps it in a ref keyed by the property list; later calls with the same key call action.update(...) instead of creating actions. The live: false call 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

PropTypeMeaning
labelstringRow label.
propertiesstring[]Properties the row writes. The first decides keyword state; remove/keyword write all of them.
childrenReactNodeThe input. Replaced by a keyword × button while every property holds the same CSS-wide keyword.
stackedbooleanPut the input under the label (wide editors).

SourceBtn is shown only for single-property rows with full source routing in the context.

ToolSection

PropTypeMeaning
idstringKey for the module-level open-state Set, which survives remounts for the session. HTML tools prefix ids with html-.
titlestringHeader text.
setCountnumberBadge value; hidden at 0.
forceOpenbooleanOpen 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].

kindInputExtra fieldsLayout
unitMeasureInputunits, step, min, placeholderinline
numberNumberInputmin, max, step, integer, keywordsinline
sliderSliderInputmin, max, step, defaultValue, suffixinline
selectSelectInputoptions, allowCustominline
segmentedSegmentedInputoptions, multiple, noneValueinline
colorColorInput + value text—inline
textTextInputplaceholder, suggestions (array or (ctx) => string[]), multilineinline; stacked if multiline
positionPositionInput—stacked
easingEasingInput—stacked
clipPathClipPathInput—stacked
gridAreasGridAreasInput—stacked
sidesSidesInputlabel, properties: Record<Side, string>, unitsstacked
cornersCornersInputlabel, 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

ToolWritesNotes
BorderToolborder-{side}-{width,style,color}all writes the four longhands; a side writes one. Reads show a value only when the targeted sides agree.
ShadowToolbox-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.
FunctionListToolfilter, backdrop-filter, transformdefs: FnDef[]; unknown functions are kept with a single raw argument.
BackgroundLayersToolbackground-image/size/position/repeatWrites only lists whose per-layer values changed; a list that is all defaults is removed.
TransitionTooltransition-property/duration/timing-function/delayAlways writes all four lists.
RawCssToolanyEvery non-empty entry of controlledStyle, plus an add line (/^(--)?[a-z-]+$/).
LayoutTooldisplay, flex and grid container propertiesAdapts 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 forcedState to GeneralStylePanel, which uses it in getPayload and resolveSource.

StyleController/forcedState.ts:

ExportMeaning
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_SHORTMenu and button labels.

See ADR-0002.

Utilities

@/Utils/Style/cssValue.ts

ExportMeaning
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

ExportMeaning
parseCssColor(v): RGBA | nullAny 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.