Skill v1.0.1
currentAutomated scan100/100+3 new
version: "1.0.1" name: simulator-styles description: > Simulator.Company Smart Form (CDU) STYLING specialist — authoring complex Less/CSS for Smart Forms: theme tokens, page/form/section layout, component re-skinning, reusable style patterns, responsive and design-system approaches. Use when the user wants to STYLE or RESTYLE an existing Smart Form / CDU app — change its look, build a theme, style a table / sidebar / modal / form, add a design system, fix spacing/colors/fonts, or apply a complex visual design. This skill owns the style / styles/ layer; it reuses the Smart Form tools (pullSmartForm / pushSmartForm / deploySmartForm) but does NOT create form templates — for the data-schema form template use simulator-forms, and for page layout / viewModel / backend logic use simulator-smart-forms / simulator-smart-forms-logic. Activate on: "style a smart form", "CDU styles", "theme the form", "restyle", "custom CSS/Less for the app", "style the table/sidebar/modal/button", "design system for the smart form", "make it look like …", "застилізувати смартформу", "стилі CDU", "тема для форми", "кастомний CSS", "оформити таблицю/ сайдбар/модалку", "застилизовать смартформу", "стили CDU", "тема формы", "кастомный CSS", "оформить таблицу/сайдбар/модалку".
Simulator.Company Smart Form STYLING specialist
You author and apply CSS/Less styles for Smart Forms (CDU / Script apps) on Simulator.Company. Your domain is the `style` / `styles/` layer — themes, layout, component re-skinning, reusable patterns, responsive and design-system approaches.
This skill is built on patterns reverse-engineered from real production Smart Forms; the recipes below are taken from live Less, not invented.
Scope — what this skill owns (and what it does NOT)
| Concern | Skill | |
|---|---|---|
Styling: style / styles/*, pages/<id>/style, styleClass, themes, Less | this skill | |
| Form template (data fields / Account Template) | simulator-forms | |
Page layout JSON (pages/<id>/config: grid/forms/sections/items), viewModel, locale | simulator-smart-forms | |
Backend logic (Corezoid /get /send, dynamic viewModel, changes[]) | simulator-smart-forms-logic | |
| Generating a whole app (pages + middleware) from existing Corezoid processes | simulator-app-generator |
You reuse the Smart Form engine tools (pullSmartForm, pushSmartForm, deploySmartForm, file-history/rollback) — you do not introduce new platform behaviour. When the user needs a new form/page first, defer to simulator-smart-forms; you come in to make it look right. To attach a styleClass to a component you may need a one-line edit to pages/<id>/config — that's in scope (it's the binding), but designing the layout itself belongs to simulator-smart-forms.
How Smart Form styling works (the model)
- One Less stylesheet, compiled per save, scoped to `.cdu-page`. Whatever you write is wrapped
in .cdu-page { … } at serve time, so & = the page root and your styles can't leak out. Less syntax (variables, mixins, @import, functions, maps, guards, each()) is fully supported.
- File organization — and where each rule goes. Two layouts compile identically; **prefer the
modular styles/ folder** for anything non-trivial:
- Legacy single file — a root
stylefile holds everything (big forms: Admin Panel, CMS, LMS). - Modular `styles/` —
styles/indexis the entry point and mainly just@imports partials,
in cascade order: ``less // styles/index — the entry/manifest; imports in cascade order @import "colors_fonts"; // tokens FIRST (everything below + page styles inherit them) @import "init_styles"; // then platform resets + project-wide component defaults // … any other shared partials (mixins, shared components) … ``
- Page styles —
pages/<id>/styleis auto-appended after the main stylesheet (so it
wins the cascade) and inherits all root variables/mixins (no @import needed). The platform saves it as text/css automatically. It is for that page's exceptions only.
| File | What belongs here | |
|---|---|---|
styles/index | Just @imports (the entry/manifest), in cascade order. | |
colors_fonts | Color + font tokens (@color_*, @font_*, @font-face). Define them here because everything else — including page styles — inherits them. | |
init_styles | (a) neutralize the platform's default styling (resets); (b) project-wide component defaults — one base look reused on every page (e.g. a .button skin). | |
pages/<id>/style | Only the page's differences from the shared styles — page layout + one-off component tweaks. Never define tokens here (they live in colors_fonts). |
- `styleClass` is the binding contract between layout JSON and CSS. Every grid / form /
section / item in pages/<id>/config may carry a styleClass; your CSS targets that class.
- Static for structure:
"styleClass":"main_table". - Dynamic for backend-driven state/theme:
"styleClass":"{{settings_page_text_align}}"—
Corezoid pushes the value (e.g. text_align_right, active_sidebar_btn) to switch styling.
- Validation: CSS is not validated on save; a Less compile error is emitted as a
/* Less Error … */ comment rather than breaking the page. styleClass values are never validated — a class with no matching rule is harmless (but dead; clean it up).
- Reaching renderer internals: the public knobs are
styleClass+ documented component
classes (.button, .edit, .select, .table, .check, …). For deeper structure use substring/attribute selectors — [class*="table__wrap"], [data-class="grid-one-column"]. Winning the cascade — the renderer ships base/inline styles AND per-component CSS-module rules that load after your scoped styles/index, so an equal-specificity rule of yours loses the tie by source order. Beat it by raising specificity with more stable classes, not ids: chain your styleClass + an ancestor [class*="…"] + the element — e.g. .book-table td.bc-cover-cell .file img (0,3,2) beats the component's .file__item__hash img (0,2,1). Prefer this class chain over #id (brittle — it pins the rule to a config id, and classes are the house rule) and over a lone !important (it only beats non-!important; in an !important vs !important fight specificity still decides). A doubled class (.x.x) is a last resort. See the verified per-component DOM map below for exact hooks (`edit`, `select`, `multiselect`, `radio`, `button`, `row__<name>`).
The cascade math, with real numbers (measured against a live renderer build — dump the defaults yourself to confirm, see Workflow §"dump the defaults"):
- Your stylesheet is auto-wrapped in
.cdu-page, so a **bare single-class rule you write is
already 0,2,0 (`.cdu-page .myClass`) and beats a bare renderer default .hashed (0,1,0) for free** — most overrides need nothing more.
- BUT the renderer theme-scopes ~⅓ of its rules (
.theme-light .x/.theme-dark .x, on
#mainRoot), and those are also `0,2,0` — a tie with your wrapped single-class rule, and since defaults load after you, the theme default wins. Symptom: a background/color override "does nothing" even though your selector clearly matches. Beat it with an extra class (0,3,0) or !important.
- ~1 in 5 default rules already use `!important` (heavy on
active/selected/checked
states and theme colors). Matching them with your own !important is normal here, not a smell — a lone !important still loses to a default !important of higher specificity, so pair it with a solid class chain.
- Slots own their own background/padding. Containers like
.section__content, the sidebar
slots (.sidebar__header/__content/__footer), and .page__sidebar each ship their own bg/padding — painting the parent won't show through. Override the slot, not its ancestor.
Authoritative component-class + CSS reference: the "CSS styling" tag in the CDU swagger and$CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-page-protocol.md.
Rendered DOM map (verified component internals)
Verified against a live control-cdu render. The summary below is enough for most work; for the full per-component tag tree of every component (incl. all 4 table types, page skeleton, overlays) see $CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-dom-tree-reference.md. Use this to target the right element instead of guessing. Three rules first:
- `styleClass` lands on the component ROOT. E.g.
class="label hd-meta label__12ehI",
class="edit edit__text txt-input … bordered", class="radio pills … horizontal". So a root hook (.hd-meta, .txt-input, .pills) always hits — but only the outer element.
- Internals use hashed CSS-module classes (
label__12ehI,f-item-0-2-64,i-label-0-2-70,
clickOutside(i)-field-0-2-97) that change between renderer builds. Never target the hash. Reach inside via substring selectors ([class*="radioItem"], [class*="i-icon"], [class*="chip"], [class*="i-edit"]), stable state classes (.checked .disabled .bordered), data-class attributes, or element selectors (input, textarea, label, svg).
- `row` / `w` grouping yields a STABLE group class `.row__<rowName>`. An item with
"row":"act"
renders inside <div class="row row__act row__hash"> whose children are <div class="row__item__…" style="width:…%">. Note w is a relative weight, not a raw percentage: the rendered width is w / Σw across the row (two items at w:50 each get 50%; at w:50 + w:100 they get 33% / 67%). Style the group via .row__act (flex container) and items via [class*="row__item"] — your reliable hook for multi-column rows, progress bars, etc.
Page skeleton & theme (stable structural hooks):
- Scope: all your CSS is wrapped in
.cdu-page(&= page root); per-page hook
.cdu-page-<pageId>.
- Dark mode is a CLASS, not a media query:
.theme-light/.theme-dark. In a live render the
class sits on the host wrapper #mainRoot (an ancestor of .cdu-page), which is why .theme-dark .… {} works from inside your scoped stylesheet. control-cdu itself also stamps the theme class onto #page (the .cdu-page node) — so verify the placement on your build: if the class is only on the .cdu-page node (not an ancestor), a wrapped .theme-dark .foo won't match and you'd need &.theme-dark .foo. Prefer this class approach over @media (prefers-color-scheme). ⚠️ Caveat: the renderer's light/dark token maps are currently identical — the mechanism is wired but dark mode has no distinct palette yet, so a .theme-dark override is the only way to make dark actually differ today.
- Grid regions:
[data-class="grid-one-column"]/[data-class="grid-two-column"]
(+-left/-right); header/footer regions via [class*="gridtwo__header"] / [class*="gridtwo__footer"].
- Section slots:
[data-class="section"](+[class*="block__"]fortype:"block"cards);
inner [class*="section__header"] / [class*="section__content"].
- Toasts: two
[class*="notify"]containers (top inside#page, bottom at#mainRootend).
Per-component root + key inner hooks (<sc> = your styleClass):
| Component | Root selector (styleClass here) | Key inner hooks / notes | |
|---|---|---|---|
label | .label / [data-class="label"] | <span> text; BBCode → real tags; align→ .left__/.center__/.right__ | |
divider | .divider | empty | |
edit (all types) | .edit (+ .edit__<type>) | .field > input / textarea; states bordered(box)/selected/error; kill box .field{border:none}; help/err [class*="Component-helperText"] | |
select | .select (outer) | readonly <input> in [class*="i-edit"] + caret [class*="endAdornment"]; no native `<select>` | |
multiselect | .multiselect | chip field: [class*="chip"] + search <input> via [class*="clickOutside(i)-field"]; not checkbox rows | |
radio | .radio (+ .horizontal for row) | options [class*="radioItem"](+.checked/.disabled); hide svg [class*="i-icon"], style label [class*="i-label"] → pills/scales | |
check | .check | [class*="f-icon"] svg + <label>; states .checked/.error; native input{appearance:none} | |
toggle | .toggle (+ .left__/.right__) | [class*="toggle__button"](+.active) / [class*="i-switch"] | |
slider | .slider (+ .skillBar__) | rc-slider: .rc-slider-rail/-track/-handle/-dot; [class*="slider__header"], [class*="slider__min"]/max | |
otp | .otp | boxes [class*="otp__edit"] input | |
phone | [data-class="phone"] | #countryCode .select + #number .edit input; [class*="phone__items"] | |
image | [data-class="image"] | <img> (src proxied via /api/1.0/image); align→ .center__ | |
timer | [data-class="timer"] | <span> text | |
comments | [data-class="comments"] | [class*="mes__wrap"], [class*="mes__name"], [class*="mes__content"], avatar [class*="i-avatar"] | |
carousel | .carousel | preview pane [class*="carousel__preview"] (zoom/nav btns), thumb strip [class*="carousel__content__item"](+.active__); give it a full File value for a correct preview (tolerates a missing type — won't error-stub) | |
button | wrapper [data-wrapper-for="<id>"] → inner #<id>.button.button__<type> | `<sc>` is on the INNER button; text [class*="button__label"]; align via wrapper/.row__<name>; 7 types via .button__<type> | |
copy | [data-class="copy"] | [class*="copy__container"], icon [class*="i-icon"], label [class*="i-label"] | |
tab | .tab (outer .tab__…) | items [class*="tab__item"](+.active/.error); hidden option is absent from DOM | |
stepper | [data-class="stepper"] | items [class*="stepperItem"](+.completed__/.active__); label [class*="stepperItem__label"] | |
mainMenu | nav.mainMenu | items [class*="mainMenuItem"](+.active); groups <details>/[class*="mainMenuItemGroup"]; data-depth + --depth; badge [class*="i-badge"] | |
upload | [data-class="upload"] | [class*="upload__box"], corners [class*="upload__corner"], input[type=file] (webcam: trigger div.upload__file__…[role=button], needs extra.accept) | |
file | .file | image → [class*="file__item"] img; pdf/doc → .pg-viewer/.pdf-viewer; path chosen by mime value.type | |
attachment | [data-class="attachment"] | upload ctrl [class*="i-upload"]; chips [class*="fileItemChip"] (text [class*="e-chipText"], remove [class*="e-delFileIcon"]) | |
signature | .signature__wrap | <canvas>; toolbar [class*="signature__toolbar__clear"]/__save | |
table | [data-class="table"] (+ .table__check/__radio/__group) | see Tables below | |
widget | [data-class="widget"] | iframe[class*="iframe__"]; [class*="widget__inner"](+.hidden__ until load) | |
| row/w group | .row__<rowName> | flex container; items [class*="row__item"] (inline width) | |
draggable (sortable/contentLoop) | [data-class="draggable"] | grip [class*="draggable__handle"], body [class*="draggable__content"] | |
notification (toast) | none — page root [class*="notify"] | [class*="notifyItem"] (+ severity [class*="success"]/[class*="error"]/[class*="info"]) → text span[class*="i-title"] + close i[class*="closeIcon"] > svg .fill |
Tables (DOM is a real <table>): head cells [class*="table__head"] (sortable col [class*="sortable__"], sort arrow [class*="table__head__icon"]); sticky col → td.sticky-col
- the head cell's own
styleClass; body rows.table-row/[class*="table__row__"]
(selected radio row → .active__…); first column for check/radio → [class*="table__check"] / .i-radioItem-…; group title row → [class*="table__row__group"]. Default-table cell mini-components inside <td.table-cell>: file → [class*="table__img"], copy → #<id>--copy[data-class="copy"], check → #<id>--check[class*="table__check"], button → [class*="table__button"] (id <id>--button, not .button).
Table cells are NOT text-only. Adefault-table cell can be plain text, an image (filecell → real<img>), a button, acopy, or acheck— so covers/actions live inside a table(cell JSON shape belongs tosimulator-smart-forms; you style via each cell's ownstyleClass,which lands on its<td>). And plain + head cells render BBCode (verified — see the BBCodematrix below): the text goes into a plain<td>/<div>, so[b]/[color]/[div]…expand there.This is why you can build a status pill in a cell without a button cell: put[div]Label[/div]in the plain cell and style.<cellClass> div.
Overlay sections: modal → backdrop [class*="i-bg"] > box [class*="modal__"] (+ size i-small/i-medium/i-large/i-xlarge ← modalSize), header [class*="section__modal__header"]. float → [class*="float__"] inside [class*="Component-wrapper"] with drag bar [class*="section__header__dragable"] + 8 [class*="Component-resizeHandle"].
⚠️ File-bearing components need a complete `value`. The **filecomponent and adefaulttable'sfilecell read mime via `value.type` with no guard** — a File missingtypethrowsand the item renders as a[class*="item__error"]stub. (carouselandattachmentalso readvalue.typebut tolerate it missing —carouseldefaults to''and uses optional chaining — sothey won't stub out; still give them the full shape for a correct preview.) Give File values thefull shape{fileName, fileSrc, title, type, size}. A stub can also suppress later siblings (acrashing table cell hid themodal/floatsections after it).upload[webcam]needsextra.accept. With complete values everything renders statically — no backend needed. Fullper-component trees:$CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-dom-tree-reference.md.
Worked example — radio rendered as pills (the native input is hidden; the svg is the control):
.pills { display:flex; flex-wrap:wrap; gap:8px; }.pills [class*="i-icon"] { display:none; } // hide the svg circle.pills [class*="radioItem"] { border:1px solid @line; border-radius:100px; }.pills [class*="radioItem"] label { padding:7px 16px; } // make the whole pill the label.pills [class*="radioItem"].checked { background:@ink; }.pills [class*="radioItem"].checked label { color:#fff; }
Worked example — toast notification. Toasts render at the page root (inside .cdu-page, which scopes your CSS), so you reach them structurally — there is no styleClass. For the DOM nesting + the two traps ([class*="i-icon"] matches the close ✕, not a severity icon — don't blanket-hide it; [class*="i-title"] also matches the wrapper i-titleContainer — qualify as span[class*="i-title"]) see the Toasts entry in cdu-dom-tree-reference.md. A reusable skin:
[class*="notify"] [class*="notifyItem"] {background:@white; color:@ink; border:1px solid @line; border-radius:@r;box-shadow:0 8px 28px rgba(0,0,0,.12); padding:14px 16px;display:flex; align-items:center; gap:10px;}[class*="notify"] [class*="notifyItem"]::before { // severity dotcontent:""; width:8px; height:8px; border-radius:50%; background:@ink-3; flex-shrink:0;}[class*="notify"] [class*="notifyItem"][class*="success"]::before { background:#16a34a; }[class*="notify"] [class*="notifyItem"][class*="error"]::before { background:#dc2626; }[class*="notify"] [class*="titleContainer"] { flex:1; min-width:0; }[class*="notify"] span[class*="i-title"] { font-size:14px; font-weight:500; color:inherit; }[class*="notify"] [class*="closeIcon"] { opacity:.45; cursor:pointer; } // keep the ✕[class*="notify"] [class*="closeIcon"]:hover { opacity:1; }[class*="notify"] [class*="closeIcon"] svg .fill { fill:currentColor; }
Hashes (-0-2-64) are version-pinned to the renderer build — substring selectors survive a bump,exact-hash selectors don't. If a restyle suddenly breaks, re-dump the rendered DOM (browserDevTools → copyouterHTMLof#page) and re-verify these hooks.
BBCode — where it renders (empirically verified)
A component's text is expanded to HTML by the client (Utils.bbCodeToHtml) only in some fields. The smart-form BBCode set (swagger bbcode tag) is [b] [i] [u] [ul][*] [iurl=…] [url=…] [div] [br] [color=…] [size=…] [bg=…] — note [div] (the only block-level tag) and [iurl] (internal link); there is no `[span style=…]` here (that exists only in actor/reaction BBCode). Mapping: [color]→<span style="color">, [bg]→<span style="background-color">, [div]→<div>.
Verified support matrix (built by rendering a live probe string, not read off the swagger):
| Renders BBCode ✅ | Stays literal ❌ | |
|---|---|---|
label.value | edit value / placeholder / multiline | |
button.title | select value · multiselect option | |
check.title | radio option title | |
stepper option title | toggle.title · tab option title | |
form.title, mainMenu title, comments, carousel item title, upload.title | copy.title | |
| `table` head title AND plain cell |
Heuristic: it expands where the text lands in a plain text node (<span>/<div>/<td>) and stays literal where it lands in a form-control `value`/`placeholder` — but it's only a rough heuristic: it's whether that specific component passes its title through bbCodeToHtml/dangerouslySetInnerHTML, not the node type. check ✅ but radio ❌ even though both render a <label>; stepper ✅ but toggle/tab ❌. Trust the matrix (built from the renderer source), not the swagger: the swagger marks only label/button/mainMenu as bbcode fields, yet table cells clearly render it. When a field isn't in the matrix, confirm it in the live UI before relying on it.
Styling use — inject a styleable element where there is none. A plain table cell is bare text in a <td> (no inner wrapper to hook). [div]…[/div] gives you an inner <div> to style as a pill/badge (.<cellClass> div { … }); [bg]/[color] give inline <span>s. (Alternative with no inner element: absolutely-position the <td> so it shrink-wraps its text into a pill.)
Workflow (reuses the Smart Form tools)
1. pullSmartForm(actorId) → downloads <actorId>/develop/ + production/ (incl. style / styles/, pages/<id>/style)2. Edit ONLY under develop/:- styles/index (+ partials) OR the root `style` file- pages/<id>/style (page-specific)- pages/<id>/config (only to add/adjust a styleClass hook)3. pushSmartForm(actorId) → validates + uploads changed files; style/styles files carry MIME text/css (the push tool sets it; the server stores the client-supplied type — it does not force it by folder)4. deploySmartForm(actorId) → publishes develop → production (when approved)
Rules: edit `develop` only (production is readonly); `pullSmartForm` first (it writes the .manifest.json push needs). Files under styles/ are sent as text/css; everything else application/json.
Verifying a styling result. appGetPage returns the server-resolved config (locale/viewModel expanded) — it does not run CSS compilation or bbCodeToHtml, so you cannot confirm a visual result or whether BBCode expanded from it. Visual/BBCode results are verified in the live UI by the user.
But you CAN and SHOULD confirm the Less compiles — locally, before pushing. This is the one styling failure that is otherwise completely silent: a compile error is not raised at serve time, it is emitted as a /* Less Error … */ comment and the page renders unstyled. pushSmartForm does not compile CSS and appGetPage never returns it, so nothing in the toolchain reports it.
Two details make the naive command fail, so use this recipe as-is (verified against a real 6-file app):
- Smart Form partials are stored without a `.less` extension, and Less resolves
@import "colors_fonts" as colors_fonts.less — so copy them to *.less first.
lessccannot read a process-substitution path (<(…)→EBADF: bad file descriptor), so write
a real temp file.
- The npm package is `less` (the binary is
lessc); a package literally namedlesscalso
exists on npm and is not the compiler — always pass --package=less.
cd <actorId>/developrm -rf .lesscheck && mkdir .lesscheckfor f in styles/*; do cp "$f" ".lesscheck/$(basename "$f").less"; done# the main sheet, wrapped exactly as the server wraps it (this is what makes `&` the page root){ echo '.cdu-page {'; sed 's/@import "\(.*\)";/@import "\1.less";/' styles/index; echo '}'; } \> .lesscheck/main.lessnpx --yes --package=less lessc .lesscheck/main.less /dev/null && echo "OK styles/index"# each page sheet, with the tokens prepended so its variables resolvefor p in pages/*/style; do{ echo '.cdu-page {'; echo '@import "colors_fonts.less";'; echo '@import "init_styles.less";';cat "$p"; echo '}'; } > .lesscheck/page.lessnpx --yes --package=less lessc .lesscheck/page.less /dev/null && echo "OK $p"donerm -rf .lesscheck
Verified while doing this: @font-face and @media blocks correctly bubble out of the .cdu-page { … } wrapper, so keeping them in an imported partial is safe.
When to ask for the rendered HTML. First fix from the DOM map below; if it doesn't land, correct once more. If after the second correction the user still doesn't get the expected result, stop guessing and ask them to copy the element's HTML (Inspect → Copy outerHTML of the element, or dump #page). The real DOM — hashed classes, wrapper nesting, inline sizes — resolves what the map can't; most restyle stalls end the moment you read it.
Starter kit — snippets that recur across real forms
These appear near-verbatim in multiple production forms; lift them as a baseline.
`colors_fonts` — tokens + font (define here; everything, incl. page styles, inherits them).
// color tokens (flat is fine for small/medium forms; mature toward Less maps for large apps)@color_primary:#151f6d; @color_white:#fff; @color_black:#0f0f0f; @color_border:#EAECF0;@color_grey:#b0adb7; @font_main:'Inter';// status palette as one token per state (drives chips/rows by status)@color_new:#A7D8F0; @color_in_progress:#A7E3B1; @color_completed:#B6E3C5; @color_pending:#FBE7A1;// icons/logos as URL tokens (proxied workspace assets or relative attachments/…)@icon_search: url('https://…/api/1.0/download/…svg?preview=true');@font-face { font-family:'Inter'; font-weight:100 900; font-display:swap;src:url(https://fonts.gstatic.com/…woff2) format('woff2'); unicode-range:U+0000-00FF, …; }
`init_styles` — platform resets + project-wide component defaults. @imported from styles/index after colors_fonts. Page-specific exceptions do not go here (→ pages/<id>/style).
// ── 1. core resets ───────────────────────────────────────────────────────────*, *::before, *::after { box-sizing: border-box; } // the single key reset// Every .section__content ships its OWN grey bg + padding:20px 16px 0 + margin-bottom:20px// (some rules theme-scoped → 0,2,0), so it paints a grey padded box INSIDE your cards.// Neutralize it once here; re-add padding on your own card wrapper. !important beats the theme rule..section .section__content { background:transparent !important; margin-bottom:0 !important; padding:0 !important; }.label, .button, .form { margin:0 !important; font-size:16px; line-height:1.5; }[class*="row__item"] { display: contents; } // row/w items flow into the parent.button-wrapper { width:auto !important; display:block; }.button-wrapper:has(.hidden) { display:none; }// ── 2. platform chrome (header/footer) — NOT hidden by default; reachable via:// & { #pageWrap > [class*="content"] > [class*="header"],// #pageWrap > [class*="content"] > [class*="footer"] { display:none !important; } }// ── 3. (optional) full-bleed — drop the default centered/capped card ─────────.content__main { padding:0 !important;[data-class="grid"] { display:block; }[data-class="grid-one-column"] { max-width:none; } }// ── 4. project font (token from colors_fonts) ────────────────────────────────input, textarea, .button > span, .toggle__title,:not(&) body > .popoverContent, span[class*="label"], & { font-family:@font_main, sans-serif; }// ── 5. utilities ──────────────────────────────────────────────────────────────.visually_hidden { position:absolute; width:1px; height:1px; margin:-1px; padding:0; border:0;white-space:nowrap; clip-path:inset(100%); clip:rect(0 0 0 0); overflow:hidden; }.font_12{font-size:12px;} .font_14{font-size:14px;} .mb_10{margin-bottom:10px !important;}// ── 6. loading overlays (platform spinners) ──────────────────────────────────[class*="button__spinner__wrap"], [class^="table__spinner__wrap"] {position:fixed; inset:0; z-index:10000; display:flex; align-items:center;justify-content:center; background:rgba(0,0,0,.1); backdrop-filter:blur(1px); }// ── 7. project-wide component defaults (customize to brand) ──────────────────.button { // ONE base button look used on every pagepadding:14px 20px; border:none; border-radius:10px; font-size:15px; font-weight:600;cursor:pointer; line-height:1; display:flex; align-items:center; justify-content:center; gap:8px;transition:all .3s cubic-bezier(.4,0,.2,1);&.primary_btn { background:@color_primary; color:@color_white;&:hover { transform:scale(1.02); box-shadow:0 6px 20px fade(@color_primary,40%); } }// … add your semantic variants (.submit_btn, .cancel_btn, …) by this pattern …}.modal { // default sizing by modalSize&[class*="small"] { width:400px !important; }&[class*="medium"], &[class*="large"] { width:auto !important; max-height:90% !important; }&[class*="xlarge"] { width:80% !important; max-height:90% !important; }}
Pattern catalogue (reusable recipes)
Layout
- Centered card form — cap and center the grid wrapper:
[data-class="grid-one-column"] { max-width:440px; margin:20px auto; padding:20px; background:#fff; border-radius:8px; box-shadow:0 0 10px #0000001a; }
- Full-bleed admin — unlock the cap:
.content__main [data-class="grid-one-column"] { max-width:none; } - Fixed sidebar shell —
.sidebar { width:250px; grid-template-rows:80px 1fr 120px; }with header/content/footer panes. - Sidebar `styleClass` + `mainMenu` indent trap — the sidebar rail ignores `grid.sideBar.styleClass`; style it via the base slots
.sidebar/.sidebar__header|__content|__footer/.page__sidebar(layout — fixed width, pinned footer — as in Fixed sidebar shell above). For amainMenuinside it, do not target[class*="mainMenuItem"]— the substring also matches the branch wrappers (.mainMenuItemGroup,__summary,__dropdown), sopaddingaccumulates per level into huge gaps. Zero the wrappers, style only the exact.mainMenuItem(the clickable row), and indent by level with the renderer's--depthvar:
``less nav.mainMenu [class*="mainMenuItemGroup"], nav.mainMenu details, nav.mainMenu summary { margin:0 !important; padding:0 !important; } nav.mainMenu .mainMenuItem { padding:7px 10px; padding-left:calc(10px + var(--depth,0) * 14px); } ``
- Pinned footer + scrolling body —
.info_form{height:calc(100vh - 50px); display:flex; flex-direction:column;} .content_section{flex:1; max-height:calc(100vh - 100px);} - Card-per-iteration wrapper — a bare
contentLooprenders its iterations flat, with no per-iteration box. Set the section'ssortable:trueand the renderer wraps each iteration in a.draggablecard with a drag handle; hide the handle to drop the drag affordance and keep just the wrapper (hiding.draggable__handlefully disables dragging — it is the only activator — so there is noreordersubmit to handle):
``less .section__content .draggable__handle { display:none !important; } /* kill grip → drag off */ .section__content .draggable { border:none !important; background:transparent !important; padding:0 !important; box-shadow:none !important; cursor:default; } /* reset outer card, avoid double frame */ .section__content .draggable__content { border:1px solid #ececec; border-radius:10px; padding:14px; } ``
- Give a `row` its own class (multi-token `row`) —
styleClasson arowis dropped, but therowvalue is space-separated: the first token is the row id (→.row__<id>), and every extra token the renderer adds as a literal class on the row wrapper. Sorow:"1 my_row"androw:"2 my_row"both get.my_row— a shared, stable hook to style many rows at once (the hashed.row__…is per-build and unreliable).
Components — re-skin, don't accept defaults
- Floating-label input —
.edit__text .label { top:36px; } .edit__text.selected .label { top:16px; font-size:12px; } .edit__text.focus .field { border-color:@blue; box-shadow:0 0 0 2px #007bff40; } - Custom checkbox — hide native (
input{-webkit-appearance:none},i{display:none}), draw tick on.checked input::after(rotated border). - Card data table —
.main_table { border-radius:12px; border:1px solid @border; } .main_table [class*="table__wrap"]{ overflow:hidden; border-radius:12px 12px 0 0; }+ custom pagination by swapping[id*="table-next"] i::after { content:""; background-image:@icon_arrow_right; }. Use modifier classes:.main_table.thin_table,.no_thead,.center_cell. - Table/radio → cards (the e-commerce look):
thead{display:none},tbody{display:flex;flex-direction:column;gap:10px},.table-row{border:1px solid #ddd;border-radius:8px;padding:10px}, highlight selection with.table-row:has([class*="radioItem"].checked){border-color:#34303d}. - Table pagination at the bottom, centered — a
table type:"group"'s pagination is a sibling of[class*="table__wrap"](not part of the rows) and defaults toposition:absolute(pinned top, overlapping content). Lay the table root as a column and re-place it:
``less .myTable { display:flex; flex-direction:column; } .myTable [class*="table__wrap"] { max-height:none !important; height:auto !important; } .myTable [class*="table__pagination"] { position:static !important; order:99; display:flex; justify-content:center; gap:8px; margin-top:20px; } ``
- Status chip — state-class + token map:
.status_cell.completed-… div{ background:@color_completed; }(backend setsstyleClass:"status_cell completed-…"). - Button variants — base
.button+ semantic modifiers (.blue_btn,.red_button,.submit_btn); hover-lift (transform:scale(1.02); box-shadow:…) or an animated::afterfill-sweep (transform:skewX(-20deg)). - Toast notifications — centered, severity icon via
[class*="notifyItem"][class*="success"]::before{ background-image:@icon_success; }. - Modal sizing — base
.modal+:has(.<innerSection>)to size each, or size-class match&[class*="xlarge"]{ width:80% !important; }. - Breadcrumbs (via `tab`) — model a crumb trail as a
tab(options[]= crumbs, each{value,title};value= current;submitOnChange:trueso a click posts the value). Twotabtraps surface only once you restyle it: the label[class*="i-label"]defaults towidth:0; overflow:hidden(text clips to nothing), and in a fixed tab (i-fixed) each[class*="tab__item"]collapses towidth:0(options overlap). Fix both withwidth:auto:
``less .breadcrumbs [class*="tab__item"] { width:auto !important; min-width:max-content !important; flex:0 0 auto !important; margin:0 !important; } .breadcrumbs [class*="tab__item"] [class*="i-label"] { width:auto !important; overflow:visible !important; } .breadcrumbs [class*="tab__item"]:not(:last-child)::after { content:"›"; margin:0 10px; opacity:.6; } ``
Interactivity (CSS-only, no backend round-trip)
- Accordion / dropdown / collapsible tree — a
check/togglewith.checked, expanded by parent:has(), animated viagrid-template-rows:0fr → 1fr:
``less .menu { display:grid; grid-template-rows:0fr; overflow:hidden; transition:grid-template-rows .32s; } .sidebar:has(.nav_btn.checked) .menu { grid-template-rows:1fr; } ` When the toggle and its target are **siblings in the same .section__content** (the common case), prefer the sibling combinator — no :has() needed: .myCheck.checked ~ .myCollapsible { max-height:300px; }. .checked lands on the component's outer wrapper. Reserve :has()` for non-sibling targets.
- Slide-in panel / popover — toggle
opacity/transform/pointer-eventsunder&:has(.trigger.checked).
State without CSS-only tricks
- Screens-as-sections — put each step/success/error/loading state as a sibling section, the backend flips
visibility(one stylesheet, many screens). - Hidden state carriers —
edititems withstyleClass:"visually_hidden"stash client state (selected id, cart id) for submit.
Icons
- Recolorable monochrome —
mask:url(@icon) no-repeat center; mask-size:contain; background-color:currentColor;(inherits text color). Bundled asset: relativeattachments/cart.svg; external: proxied/api/1.0/download|image. - Full-color —
background-image:@icontoken.
RTL / theme toggle from the backend
- Page root
styleClass:"{{settings_page_text_align}}"; rule.form.text_align_right { … flex-direction:row-reverse; text-align:right; … }. The backend pushes one value to mirror the whole UI. Same mechanism toggles nav-active state, light/dark, etc.
Design-system approach (advanced — for large/branded apps)
When a form grows or needs strict brand consistency, build a token system in Less (this is the LMS form's approach — Tailwind/Untitled-UI in Less):
// maps for everything, not just color@breakpoint: { minimum-mobile:375px; mobile-tablet:820px; tablet-desktop:1180px; }@space: { 0:2px; 1:4px; 2:8px; 3:12px; 4:16px; 5:20px; 6:24px; 8:32px; /* … */ 64:256px; }@color: { @gray:{25:#FCFCFD; /* … */ 700:#344054; 900:#101828;} @brand:{…} @error:{…} @success:{…} }// generate utility classes from a map (each + guard).make-space-set(@base,@rule,@min,@max){ each(@space,{ .apply(@k,@v);.apply(@k,@v) when (@k<=@max) and (@k>=@min){ .@{base}-@{k}.@{base}-@{k}{ @{rule}:@v; } } }); }.make-space-set(mt, margin-top, 0, 16); // → .mt-0 … .mt-16, applied in styleClass// typography scale via lookup mixin.text-font(@size; @weight:regular){ @s:@typography[@text][@@size];font-size:@s[font-size]; font-weight:@font-weight[@@weight]; line-height:@s[line-height]; }// responsive tokens: Less map → CSS var overridden per breakpoint@media (min-width:@breakpoint[tablet-desktop]) { :root { --space-large: 32px; } }.gap-large { gap: var(--space-large); }
Advanced Less available: each(), guards (when), maps + indirect lookup (@@key), replace(), extract()/if()/isnumber()/isdefined(), detached-ruleset returns (mixin()[@result]). Caveat: keep this in a modular `styles/` split (colors_fonts, space, typography, shadows, mixins, index) — a single 7k-line file is the maintainability ceiling.
Gotchas (learned from real forms)
- Windows filenames: a folder/file titled with a reserved char (
\ / : * ? " < > |) breaks
pullSmartForm on Windows (mkdir fails). Don't name definitions like edit|create_…. If a pull aborts on mkdir … syntax is incorrect, rename the offending folder in the editor and re-pull (or recover content from a .graph export that carries scripts/<uuid>/… file trees).
- Winning the cascade: component CSS-modules load after you and win equal-specificity ties — beat
them with a chain of stable classes (styleClass + [class*="…"] + tag), not #id or a lone !important. Full worked example + cascade math in §"How Smart Form styling works" pt 5.
- Never write `.cdu-page` yourself: the compiler wraps your whole stylesheet in
.cdu-page { … }
at serve-time, so a hand-written .cdu-page (or .cdu-page:has(…)) becomes a descendant (.cdu-page .cdu-page…) and silently never matches. The page root is & (= .cdu-page): paint the page with & { background:#e30613; min-height:100vh; } and target it with &:has(…).
- Image/`file` cell renders "wide but a thin strip": not a width bug but a clipping ancestor —
a wrapper (.file / .file__item) keeps a small fixed height + overflow:hidden. Force the box at every wrapper level (height + min/max-height + width:100% + max-width:none + overflow), not only on <img>. The default clamp is max-height:32px on both <img> and [class*="table__img"], and max-height always beats height — so max-height:none is the key. One height knob down the whole chain (.card-img = the cell's styleClass, applied on <td>): ``less .card-img, .card-img [class*="table__img"], .card-img .file, .card-img [class*="file__item"] { width:100%; height:240px !important; max-height:none !important; display:block; } .card-img img { width:100% !important; height:100% !important; max-width:none !important; max-height:none !important; object-fit:contain; } /* contain = whole image; cover = fill + crop */ ``
- Dead `styleClass`: placeholder classes (
my-custom-class) with no rule are harmless but
noise — remove them.
- Stale copies: forms accumulate
style(copy)/style BACKUPfiles — they're not wired in;
don't analyze or ship them.
- External images are proxied:
https://…URLs in CSS get/api/1.0/image?src=prepended
automatically (actor-attached images are internal and not proxied).
- `//` comments compile away (prefer over
/* */); keep nesting < 3 levels. - Edit `develop` only;
pages/<id>/styleneeds no@import(auto-included).
Reference documents
| Path | When to read | |
|---|---|---|
$CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-dom-tree-reference.md | The rendered-DOM map: full per-component tag tree + class names, stable hooks vs. volatile hashes, page skeleton & theme classes, all table types, and which components need backend data to render | |
$CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-page-protocol.md | Component class catalogue, styleClass, templating, change protocol | |
$CLAUDE_PLUGIN_ROOT/docs/user-flows/smart-forms.md | Project file structure, deploy/release, styles compilation | |
$CLAUDE_PLUGIN_ROOT/skills/simulator-smart-forms/SKILL.md | The pull/push/deploy cycle and page-config format this skill builds on | |
| CDU swagger → "CSS styling" tag | Authoritative Less guide: organization, component default classes, best practices, image proxying |